PS_Innovation / index.html
MatthewStroud's picture
Upload 3 files
6cd24ed verified
Raw
History Blame Contribute Delete
420 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>PSI Cascade Dashboard v1.0</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="https://cdn.jsdelivr.net/npm/d3-sankey@0.12"></script>
<script src="https://unpkg.com/three@0.155.0/build/three.min.js"></script>
<script src="https://unpkg.com/three-spritetext@1.8.2/dist/three-spritetext.min.js"></script>
<script src="https://unpkg.com/3d-force-graph@1.73.4/dist/3d-force-graph.min.js"></script>
<style>
:root {
--bg-base: #050810;
--bg-mid: #0a0f1f;
--panel-glass: rgba(255,255,255,0.03);
--panel-glass-hover: rgba(255,255,255,0.05);
--panel-glass-strong: rgba(255,255,255,0.045);
--panel-edge: rgba(255,255,255,0.07);
--panel-edge-strong: rgba(255,255,255,0.10);
--ink: #f5f7fa;
--ink-dim: rgba(255,255,255,0.62);
--ink-faint: rgba(255,255,255,0.42);
--ink-vfaint: rgba(255,255,255,0.18);
--rule: rgba(255,255,255,0.06);
--A: #60a5fa; --C: #facc15; --B: #fb7185;
--money: #34d399; --money-la: #a78bfa;
--glow-blue: #60a5fa;
--glow-purple: #a78bfa;
--housing: #3b82f6; --mental_health: #a855f7; --substance: #f97316;
--adult_social_care: #14b8a6; --domestic_abuse: #ec4899;
--welfare: #22c55e; --antecedent: #475569; --other: #94a3b8;
}
* { box-sizing: border-box; }
html, body {
margin: 0; padding: 0; color: var(--ink);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "SF Pro Display", system-ui, sans-serif;
font-size: 13px; line-height: 1.5;
letter-spacing: -0.005em;
-webkit-font-smoothing: antialiased;
background:
radial-gradient(ellipse 80% 60% at 20% 0%, rgba(99,102,241,0.30) 0%, transparent 60%),
radial-gradient(ellipse 60% 50% at 80% 30%, rgba(52,211,153,0.18) 0%, transparent 60%),
radial-gradient(ellipse 70% 60% at 50% 100%, rgba(167,139,250,0.16) 0%, transparent 60%),
linear-gradient(180deg, var(--bg-base) 0%, var(--bg-mid) 50%, var(--bg-mid) 100%);
background-attachment: fixed;
min-height: 100vh;
}
.wrapper { max-width: 1820px; margin: 0 auto; padding: 28px 36px 100px; }
header { padding-bottom: 18px; margin-bottom: 22px; border: 0; }
.eyebrow { color: var(--ink-faint); text-transform: uppercase; letter-spacing: 0.18em; font-size: 11px; font-weight: 600; }
h1 { font-size: 32px; line-height: 1.15; margin: 8px 0 8px; font-weight: 300; letter-spacing: -0.025em; }
.sub { color: var(--ink-dim); font-size: 14px; max-width: 1100px; line-height: 1.55; }
/* Frosted-glass primitives — used everywhere */
.glass {
background: var(--panel-glass);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
border: 1px solid var(--panel-edge);
border-radius: 18px;
box-shadow: 0 8px 32px rgba(0,0,0,0.30), inset 0 1px 0 rgba(255,255,255,0.05);
}
.glass-strong {
background: var(--panel-glass-strong);
backdrop-filter: blur(40px) saturate(180%);
-webkit-backdrop-filter: blur(40px) saturate(180%);
border: 1px solid var(--panel-edge-strong);
border-radius: 22px;
box-shadow: 0 12px 50px rgba(0,0,0,0.40), inset 0 1px 0 rgba(255,255,255,0.06);
}
.controls {
display: flex; gap: 10px; align-items: center; margin-top: 14px;
background: var(--panel-glass);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
border: 1px solid var(--panel-edge);
border-radius: 18px;
padding: 12px 16px; flex-wrap: wrap;
box-shadow: 0 8px 32px rgba(0,0,0,0.25);
}
.controls button {
background: rgba(255,255,255,0.04); color: var(--ink); border: 1px solid var(--panel-edge);
border-radius: 100px; padding: 8px 16px; cursor: pointer; font-size: 13px; font-family: inherit;
font-weight: 500; transition: all 180ms;
}
.controls button:hover { background: rgba(255,255,255,0.08); border-color: var(--panel-edge-strong); }
.controls button.active { background: rgba(96,165,250,0.16); border-color: rgba(96,165,250,0.40); color: var(--glow-blue); }
.controls select {
background: rgba(255,255,255,0.04); color: var(--ink); border: 1px solid var(--panel-edge);
border-radius: 100px; padding: 7px 14px; font-size: 13px; font-family: inherit; cursor: pointer;
font-weight: 500;
}
.controls .lbl { color: var(--ink-faint); font-size: 11px; text-transform: uppercase; letter-spacing: 0.10em; font-weight: 600; }
.scrub-group { flex: 1; min-width: 200px; display: flex; align-items: center; gap: 14px; }
.scrub { flex: 1; }
input[type=range] { width: 100%; accent-color: var(--glow-blue); }
.time-display { font-feature-settings: "tnum"; min-width: 84px; text-align: right; color: var(--ink); font-weight: 600; }
.main { display: grid; grid-template-columns: 1fr 460px; gap: 22px; }
.viz-panel {
background: var(--panel-glass);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
border: 1px solid var(--panel-edge);
border-radius: 22px;
padding: 14px;
min-height: 920px; position: relative;
box-shadow: 0 12px 50px rgba(0,0,0,0.35);
}
.viz-stack { position: relative; width: 100%; height: 920px; }
.viz-stack svg, .viz-stack canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
.viz-stack canvas { pointer-events: none; }
.viz-svg .node-rect { stroke: rgba(255,255,255,0.15); stroke-width: 0.5; }
.viz-svg .node-label { font-size: 8.5px; fill: var(--ink); pointer-events: none; }
.viz-svg .ant-label { font-size: 8px; fill: var(--ink-dim); pointer-events: none; }
.viz-svg .edge-path { fill: none; stroke: var(--rule); stroke-opacity: 0.32; }
.viz-svg .recurrence-arc { fill: none; stroke: #f0abfc; stroke-opacity: 0.25; stroke-dasharray: 3,2; }
.viz-svg .terminal-bar { stroke: none; }
.loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
background: rgba(10,15,31,0.85); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
border-radius: 22px; flex-direction: column; gap: 16px; color: var(--ink); font-size: 14px; z-index: 100; }
.loading.hidden { display: none; }
.loading .progress-bar { width: 320px; height: 4px; background: rgba(255,255,255,0.08); border-radius: 100px; overflow: hidden; }
.loading .progress-fill { height: 100%; background: linear-gradient(90deg, var(--glow-blue), var(--money)); width: 0%; transition: width 80ms; }
.side { display: flex; flex-direction: column; gap: 18px; }
.totals-hero {
padding: 24px 28px;
background:
radial-gradient(ellipse 60% 80% at 0% 100%, rgba(52,211,153,0.14) 0%, transparent 60%),
radial-gradient(ellipse 60% 80% at 100% 0%, rgba(96,165,250,0.08) 0%, transparent 60%),
linear-gradient(135deg, rgba(255,255,255,0.04) 0%, rgba(255,255,255,0.02) 100%);
backdrop-filter: blur(40px) saturate(180%);
-webkit-backdrop-filter: blur(40px) saturate(180%);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 22px;
box-shadow: 0 16px 60px rgba(0,0,0,0.40), inset 0 1px 0 rgba(255,255,255,0.07);
}
.totals-hero h3 { margin: 0 0 14px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.16em; color: var(--ink-faint); font-weight: 600; }
.totals-hero .pair { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.totals-hero .big { padding: 6px 0; }
.totals-hero .big .label { color: var(--ink-dim); font-size: 11px; text-transform: uppercase; letter-spacing: 0.10em; font-weight: 600; margin-bottom: 6px; }
.totals-hero .big .value { color: var(--money); font-size: 36px; font-weight: 300; font-feature-settings: "tnum"; letter-spacing: -0.025em; line-height: 1.05; }
.totals-hero .big.la .value { color: var(--money-la); }
.totals-hero .stats { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 18px; margin-top: 16px; font-size: 12px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,0.07); }
.totals-hero .stats .l { color: var(--ink-faint); }
.totals-hero .stats .v { color: var(--ink); font-feature-settings: "tnum"; text-align: right; font-weight: 500; }
.cost-table-panel, .portfolio-panel, .longtail-panel {
background: var(--panel-glass);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
border: 1px solid var(--panel-edge);
border-radius: 18px; padding: 14px 0 0;
box-shadow: 0 8px 32px rgba(0,0,0,0.25);
}
.cost-table-panel h3, .portfolio-panel h3, .longtail-panel h3 { margin: 0 0 10px; padding: 4px 18px 0; font-size: 11px; text-transform: uppercase; letter-spacing: 0.14em; color: var(--ink-faint); font-weight: 600; display: flex; justify-content: space-between; align-items: baseline; }
.cost-table-panel .scroll { max-height: 360px; overflow-y: auto; padding: 0 4px; }
.cost-table, .port-table { width: 100%; border-collapse: collapse; font-size: 11.5px; }
.cost-table thead th, .port-table thead th {
position: sticky; top: 0; background: var(--panel); color: var(--ink-faint);
font-weight: 600; font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em;
text-align: right; padding: 8px 10px 8px 8px; border-bottom: 1px solid var(--rule); z-index: 1;
}
.cost-table thead th:first-child, .port-table thead th:first-child { text-align: left; padding-left: 16px; }
.cost-table tbody td, .port-table tbody td { padding: 5px 10px 5px 8px; text-align: right; font-feature-settings: "tnum"; border-bottom: 1px solid rgba(255,255,255,0.04); }
.cost-table tbody td:first-child, .port-table tbody td:first-child { text-align: left; padding-left: 16px; color: var(--ink); display: flex; align-items: center; gap: 6px; }
.cost-table tbody td .swatch, .port-table tbody td .swatch { width: 6px; height: 14px; border-radius: 1.5px; flex-shrink: 0; }
.cost-table tbody td .nm, .port-table tbody td .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cost-table tbody td.public, .port-table tbody td.public { color: var(--money); font-weight: 600; }
.cost-table tbody td.la, .port-table tbody td.la { color: var(--money-la); font-weight: 600; }
.cost-table tfoot td, .port-table tfoot td {
padding: 10px 10px 12px 8px; font-weight: 700; font-size: 13px; font-feature-settings: "tnum";
text-align: right; border-top: 2px solid var(--rule); background: var(--panel);
}
.cost-table tfoot td:first-child, .port-table tfoot td:first-child { text-align: left; padding-left: 16px; color: var(--ink-faint); text-transform: uppercase; letter-spacing: 0.12em; font-size: 10px; }
.cost-table tfoot td.public, .port-table tfoot td.public { color: var(--money); }
.cost-table tfoot td.la, .port-table tfoot td.la { color: var(--money-la); }
.cost-table .empty, .port-table .empty { text-align: center; color: var(--ink-faint); padding: 30px 16px; font-style: italic; }
.longtail-panel { padding-bottom: 12px; }
.longtail-panel h3 .pct { color: var(--money); font-weight: 700; font-size: 10px; }
.longtail-svg { width: 100%; height: 180px; padding: 0 12px; }
.legend, .semantics {
background: var(--panel-glass);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
border: 1px solid var(--panel-edge);
border-radius: 18px;
padding: 14px 18px;
box-shadow: 0 8px 32px rgba(0,0,0,0.20);
}
.legend h3, .semantics h3 { margin: 0 0 8px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.14em; color: var(--ink-faint); font-weight: 600; }
.legend-items { display: grid; grid-template-columns: 1fr 1fr; gap: 5px 10px; font-size: 11px; color: var(--ink-dim); }
.legend-items .item { display: flex; align-items: center; gap: 6px; }
.swatch { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }
.swatch.dashed { background: linear-gradient(to right, #f0abfc 0%, #f0abfc 40%, transparent 40%, transparent 60%, #f0abfc 60%, #f0abfc 100%); }
.semantics { font-size: 11px; color: var(--ink-dim); line-height: 1.5; }
.semantics strong { color: var(--ink); }
/* Wayfinder pill bar — replaces tabs; signposts the user journey */
.tabs {
display: flex; gap: 6px; margin-top: 22px; flex-wrap: wrap;
align-items: center;
background: var(--panel-glass);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
border: 1px solid var(--panel-edge);
border-radius: 100px;
padding: 6px;
box-shadow: 0 10px 40px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.06);
width: fit-content;
}
.tab-btn {
background: transparent; color: var(--ink-dim);
border: none; border-radius: 100px;
padding: 10px 22px; cursor: pointer;
font-size: 13px; font-family: inherit; font-weight: 500;
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
letter-spacing: 0.005em;
display: inline-flex; align-items: center; gap: 8px;
position: relative;
}
.tab-btn .step-num {
display: inline-flex; align-items: center; justify-content: center;
width: 18px; height: 18px;
border-radius: 50%;
background: rgba(255,255,255,0.06);
color: var(--ink-faint);
font-size: 10px; font-weight: 600;
font-feature-settings: "tnum";
transition: all 200ms ease;
}
.tab-btn:hover { color: var(--ink); background: rgba(255,255,255,0.04); }
.tab-btn:hover .step-num { background: rgba(255,255,255,0.10); color: var(--ink-dim); }
.tab-btn.active {
color: var(--ink);
background: linear-gradient(135deg, rgba(99,102,241,0.22) 0%, rgba(168,85,247,0.18) 100%);
box-shadow: 0 4px 18px rgba(99,102,241,0.30), inset 0 1px 0 rgba(255,255,255,0.10);
font-weight: 600;
}
.tab-btn.active .step-num {
background: linear-gradient(135deg, var(--glow-blue) 0%, var(--glow-purple) 100%);
color: #fff;
box-shadow: 0 0 12px rgba(99,102,241,0.55);
}
.tab-btn.done .step-num {
background: rgba(52,211,153,0.18);
color: var(--money);
border: 1px solid rgba(52,211,153,0.35);
}
.tab-page { display: none; }
.tab-page.active { display: block; }
/* Hide playback controls when on Page 2 — they apply to the live cascade animation only */
body[data-tab="patterns"] .playback-only { display: none !important; }
body[data-tab="patterns"] .controls { padding: 8px 14px; }
/* Page 2 self-contained legend */
/* Page 2 v0.11.1 — mirrors Page 1 layout (wide graph + right sidebar) */
.patterns2 { display: grid; grid-template-columns: 1fr 460px; gap: 18px; margin-top: 18px; align-items: start; }
.patterns2-center {
background: var(--panel-glass);
backdrop-filter: blur(40px) saturate(180%);
-webkit-backdrop-filter: blur(40px) saturate(180%);
border: 1px solid var(--panel-edge);
border-radius: 22px;
padding: 16px;
box-shadow: 0 20px 50px rgba(0,0,0,0.30), inset 0 1px 0 rgba(255,255,255,0.05);
}
.patterns2-right { display: flex; flex-direction: column; gap: 14px; }
.pat2-graph-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid var(--rule); }
.pat2-graph-title { font-size: 15px; font-weight: 700; color: var(--ink); margin-bottom: 2px; }
.pat2-graph-sub { font-size: 11px; color: var(--ink-dim); max-width: 720px; line-height: 1.4; }
.pat2-graph-actions button { background: var(--panel-2); color: var(--ink-dim); border: 1px solid var(--rule); border-radius: 6px; padding: 5px 12px; font-size: 11px; cursor: pointer; font-family: inherit; }
.pat2-graph-actions button:hover { color: var(--ink); background: #2a3354; }
.pat2-mini-legend { display: flex; flex-wrap: wrap; gap: 6px 12px; padding: 8px 4px 0; font-size: 10px; color: var(--ink-dim); align-items: center; }
.pat2-mini-legend .sw-circle { width: 7px; height: 7px; border-radius: 50%; display: inline-block; margin-right: 3px; }
.pat2-mini-legend .line-sample { display: inline-block; width: 16px; height: 1.5px; margin-right: 3px; vertical-align: middle; }
.pat2-mini-legend .line-sample.fwd { background: #5a6585; }
.pat2-mini-legend .line-sample.back { background: linear-gradient(to right, #f0abfc 0%, #f0abfc 40%, transparent 40%, transparent 60%, #f0abfc 60%, #f0abfc 100%); height: 2px; }
.pat2-section {
background: var(--panel-glass);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
border: 1px solid var(--panel-edge);
border-radius: 18px;
padding: 0 0 8px;
box-shadow: 0 12px 36px rgba(0,0,0,0.22), inset 0 1px 0 rgba(255,255,255,0.04);
}
.pat2-section-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px 8px; border-bottom: 1px solid var(--rule); }
.pat2-section-header h3 { margin: 0; font-size: 11px; text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-faint); font-weight: 600; }
.pat2-section-header select { background: var(--panel-2); color: var(--ink); border: 1px solid var(--rule); border-radius: 5px; padding: 3px 8px; font-size: 11px; font-family: inherit; }
.pat2-controls { padding: 8px 14px; display: flex; gap: 6px; }
.pat2-controls select { flex: 1; background: var(--panel-2); color: var(--ink); border: 1px solid var(--rule); border-radius: 5px; padding: 4px 8px; font-size: 11px; font-family: inherit; }
.pat2-list { max-height: 260px; overflow-y: auto; padding: 4px 6px; }
.pat2-item { display: block; padding: 6px 10px; border-radius: 6px; cursor: pointer; margin: 2px 0; font-size: 11px; line-height: 1.35; transition: background 120ms; }
.pat2-item:hover { background: var(--panel-2); }
.pat2-item.selected { background: #2e3a6e; box-shadow: 0 0 0 1px #4a5894 inset; }
.pat2-item .summary { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; margin-bottom: 3px; }
.pat2-item .summary .rank { color: var(--ink-faint); font-size: 9px; font-weight: 700; letter-spacing: 0.1em; }
.pat2-item .summary .count { color: var(--A); font-feature-settings: "tnum"; font-weight: 600; }
.pat2-item .summary .cost { color: var(--money); font-feature-settings: "tnum"; font-weight: 600; }
.pat2-item .chain-mini-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 0; }
.pat2-item .mini-chip { display: inline-flex; align-items: center; gap: 3px; padding: 2px 5px; background: rgba(255,255,255,0.05); border-radius: 3px; color: var(--ink); font-size: 10px; }
.pat2-item .mini-chip .sw { width: 6px; height: 6px; border-radius: 50%; }
.pat2-item .mini-arrow { color: var(--ink-faint); padding: 0 3px; font-size: 9px; }
.pat2-item.selected .mini-chip { background: rgba(255,255,255,0.12); }
.pat2-item .mini-mix { display: flex; height: 4px; border-radius: 2px; overflow: hidden; background: var(--bg); margin-top: 4px; }
.pat2-item .mini-mix .seg.segA { background: var(--A); }
.pat2-item .mini-mix .seg.segC { background: var(--C); }
.pat2-item .mini-mix .seg.segB { background: var(--B); }
.pat2-details {
background: var(--panel-glass);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
border: 1px solid var(--panel-edge);
border-radius: 18px;
padding: 18px 20px;
box-shadow: 0 12px 36px rgba(0,0,0,0.22), inset 0 1px 0 rgba(255,255,255,0.04);
}
.pat2-details .title { font-size: 14px; color: var(--ink); font-weight: 700; margin-bottom: 4px; }
.pat2-details .sub { font-size: 11px; color: var(--ink-dim); margin-bottom: 14px; }
.pat2-details .stat-grid { display: grid; grid-template-columns: 1fr; gap: 8px; }
.pat2-details .stat-card { background: var(--panel-2); border-radius: 8px; padding: 10px 12px; }
.pat2-details .stat-card .l { color: var(--ink-faint); font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; margin-bottom: 4px; }
.pat2-details .stat-card .v { font-size: 22px; font-weight: 700; font-feature-settings: "tnum"; }
.pat2-details .stat-card.public .v { color: var(--money); }
.pat2-details .stat-card.la .v { color: var(--money-la); }
.pat2-details .stat-card.count .v { color: var(--A); }
.pat2-details .chain-path { background: var(--panel-2); border-radius: 8px; padding: 10px 12px; margin-top: 12px; }
.pat2-details .chain-path .l { color: var(--ink-faint); font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; margin-bottom: 8px; }
.pat2-details .chain-path .nodes { display: flex; flex-direction: column; gap: 4px; }
.pat2-details .chain-path .node-row { display: flex; align-items: center; gap: 8px; font-size: 11px; }
.pat2-details .chain-path .node-row .sw { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.pat2-details .chain-path .node-row .nm { color: var(--ink); }
.pat2-details .chain-path .node-row .cost { color: var(--money); font-feature-settings: "tnum"; margin-left: auto; font-size: 10px; }
.pat2-details .hsv-mix { background: var(--panel-2); border-radius: 8px; padding: 10px 12px; margin-top: 12px; }
.pat2-details .hsv-mix .l { color: var(--ink-faint); font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; margin-bottom: 8px; }
.pat2-details .hsv-bar { display: flex; height: 10px; border-radius: 4px; overflow: hidden; background: var(--bg); margin-bottom: 6px; }
.pat2-details .hsv-bar .seg { transition: width 250ms; }
.pat2-details .hsv-bar .segA { background: var(--A); }
.pat2-details .hsv-bar .segC { background: var(--C); }
.pat2-details .hsv-bar .segB { background: var(--B); }
.pat2-details .hsv-labels { display: flex; justify-content: space-between; font-size: 10px; color: var(--ink-dim); }
.pat2-details .narrative { background: var(--panel-2); border-radius: 8px; padding: 10px 12px; margin-top: 12px; font-size: 11px; color: var(--ink-dim); line-height: 1.5; }
#viz2 .node-rect-dim { opacity: 0.18; }
#viz2 .node-rect-hi { opacity: 1; filter: drop-shadow(0 0 6px currentColor); }
#viz2 .edge-path-dim { stroke-opacity: 0.10; }
#viz2 .edge-path-hi { stroke-opacity: 0.95; stroke: var(--money); stroke-width: 3px; }
#viz2 .edge-path-hi-back { stroke-opacity: 0.95; stroke: #f0abfc; stroke-width: 2.5px; stroke-dasharray: 5,3; }
#viz2 .node-label-dim { opacity: 0.25; }
#viz2 .node-label-hi { opacity: 1; font-weight: 700; }
.patterns-legend {
background: var(--panel-glass);
backdrop-filter: blur(24px) saturate(160%);
-webkit-backdrop-filter: blur(24px) saturate(160%);
border: 1px solid var(--panel-edge);
border-radius: 16px;
padding: 12px 18px;
margin-top: 18px;
display: grid; grid-template-columns: 1.6fr 1fr; gap: 16px;
font-size: 11px;
}
.patterns-legend h4 { margin: 0 0 6px; font-size: 10px; text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-faint); font-weight: 600; }
.patterns-legend .items { display: flex; flex-wrap: wrap; gap: 8px 14px; color: var(--ink-dim); }
.patterns-legend .item { display: inline-flex; align-items: center; gap: 5px; }
.patterns-legend .sw-circle { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
.patterns-legend .line-sample { width: 22px; height: 1.5px; display: inline-block; vertical-align: middle; }
.patterns-legend .line-sample.fwd { background: #5a6585; }
.patterns-legend .line-sample.back { background: linear-gradient(to right, #f0abfc 0%, #f0abfc 40%, transparent 40%, transparent 60%, #f0abfc 60%, #f0abfc 100%); height: 2px; }
/* Page 2 — patterns */
.patterns-grid { display: grid; grid-template-columns: 1.6fr 1fr; gap: 18px; margin-top: 18px; }
.pattern-panel { background: var(--panel); border: 1px solid var(--rule); border-radius: 12px; padding: 14px 16px 18px; }
.pattern-panel h2 { margin: 0 0 4px; font-size: 16px; font-weight: 700; color: var(--ink); }
.pattern-panel .desc { color: var(--ink-dim); font-size: 12px; margin-bottom: 12px; }
.pattern-controls { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; padding-bottom: 10px; border-bottom: 1px solid var(--rule); flex-wrap: wrap; }
.pattern-controls select { background: var(--panel-2); color: var(--ink); border: 1px solid var(--rule); border-radius: 6px; padding: 6px 10px; font-size: 12px; font-family: inherit; }
.pattern-controls .lbl { color: var(--ink-faint); font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; }
.chain-list { display: flex; flex-direction: column; gap: 8px; max-height: 760px; overflow-y: auto; padding-right: 4px; }
.chain-row { background: var(--panel-2); border-radius: 8px; padding: 10px 12px; }
.chain-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 8px; gap: 12px; }
.chain-rank { color: var(--ink-faint); font-size: 10px; font-weight: 700; letter-spacing: 0.1em; }
.chain-stats { display: flex; gap: 14px; font-size: 11px; }
.chain-stats .stat .l { color: var(--ink-faint); font-size: 9px; text-transform: uppercase; letter-spacing: 0.08em; margin-right: 4px; }
.chain-stats .stat .v { color: var(--ink); font-feature-settings: "tnum"; font-weight: 600; }
.chain-stats .stat .v.money { color: var(--money); }
.chain-stats .stat .v.la { color: var(--money-la); }
.chain-stats .stat .v.count { color: var(--A); }
.chain-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 0; margin-bottom: 6px; }
.chain-chip { display: inline-flex; align-items: center; gap: 5px; padding: 3px 8px; background: rgba(255,255,255,0.06); border-radius: 4px; font-size: 11px; color: var(--ink); }
.chain-chip .swatch { width: 8px; height: 8px; border-radius: 50%; }
.chain-arrow { color: var(--ink-faint); padding: 0 6px; font-size: 11px; }
.chain-arrow.loop { color: #f0abfc; font-weight: 700; }
.chain-mix { display: flex; height: 6px; border-radius: 3px; overflow: hidden; background: var(--bg); }
.chain-mix .seg { height: 100%; transition: width 200ms; }
.chain-mix .segA { background: var(--A); }
.chain-mix .segC { background: var(--C); }
.chain-mix .segB { background: var(--B); }
.chain-mix-label { font-size: 9px; color: var(--ink-faint); margin-top: 4px; display: flex; gap: 10px; }
.chain-mix-label .l { display: inline-flex; align-items: center; gap: 4px; }
.chain-mix-label .dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
.loop-list { display: flex; flex-direction: column; gap: 12px; max-height: 760px; overflow-y: auto; padding-right: 4px; }
.loop-card { background: var(--panel-2); border-radius: 8px; padding: 12px 14px; display: grid; grid-template-columns: 90px 1fr; gap: 14px; align-items: center; }
.loop-svg { width: 90px; height: 90px; }
.loop-info h4 { margin: 0 0 4px; font-size: 13px; color: var(--ink); font-weight: 700; }
.loop-info .meta { color: var(--ink-dim); font-size: 11px; margin-bottom: 8px; }
.loop-info .stats { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 12px; font-size: 11px; }
.loop-info .stats .l { color: var(--ink-faint); }
.loop-info .stats .v { color: var(--ink); font-feature-settings: "tnum"; font-weight: 600; text-align: right; }
.loop-info .stats .v.money { color: var(--money); }
/* v1.0 — LA selector with Apple Glass translucent treatment */
.la-bar {
display: flex; align-items: center; gap: 18px; margin-top: 22px;
background: var(--panel-glass);
backdrop-filter: blur(40px) saturate(180%);
-webkit-backdrop-filter: blur(40px) saturate(180%);
border: 1px solid var(--panel-edge);
border-radius: 20px;
padding: 16px 22px;
flex-wrap: wrap;
box-shadow: 0 20px 50px rgba(0,0,0,0.30), inset 0 1px 0 rgba(255,255,255,0.06);
}
.la-bar .pickwrap { display: flex; align-items: center; gap: 12px; }
.la-bar .pickwrap label {
color: var(--ink-faint); font-size: 10px;
text-transform: uppercase; letter-spacing: 0.14em; font-weight: 600;
}
.la-bar select#laSel {
background: rgba(255,255,255,0.04); color: var(--ink);
border: 1px solid rgba(255,255,255,0.10);
border-radius: 100px;
padding: 10px 18px;
font-size: 14px; font-family: inherit; cursor: pointer;
min-width: 280px; font-weight: 500;
letter-spacing: -0.005em;
transition: all 200ms ease;
}
.la-bar select#laSel:hover { background: rgba(255,255,255,0.07); border-color: rgba(255,255,255,0.16); }
.la-bar .meta {
color: var(--ink-dim); font-size: 12px; flex: 1;
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
}
.la-bar .meta strong { color: var(--ink); font-weight: 600; }
.la-bar .meta .pop-chip {
display: inline-flex; align-items: center; gap: 6px;
padding: 4px 10px;
background: rgba(99,102,241,0.12);
border: 1px solid rgba(99,102,241,0.25);
border-radius: 100px;
font-size: 11px; color: var(--ink);
font-weight: 500;
}
.la-bar .meta .pulse-dot {
width: 6px; height: 6px; border-radius: 50%;
background: var(--money);
box-shadow: 0 0 8px rgba(52,211,153,0.7);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(1.3); }
}
.la-bar .actions { display: flex; gap: 8px; }
.la-bar button {
background: linear-gradient(135deg, var(--money) 0%, #10b981 100%);
color: #042620; border: none; border-radius: 100px;
padding: 10px 18px; font-size: 13px; font-weight: 600;
cursor: pointer; font-family: inherit;
letter-spacing: 0.005em;
box-shadow: 0 6px 18px rgba(52,211,153,0.30);
transition: transform 150ms ease, box-shadow 150ms ease;
}
.la-bar button:hover { transform: translateY(-1px); box-shadow: 0 10px 24px rgba(52,211,153,0.40); }
.la-bar button.secondary {
background: rgba(255,255,255,0.06);
color: var(--ink);
box-shadow: none;
border: 1px solid rgba(255,255,255,0.10);
}
.la-bar button.secondary:hover { background: rgba(255,255,255,0.10); color: var(--money); }
/* Interventions tab */
.interv-grid { display: grid; grid-template-columns: 1fr 460px; gap: 18px; margin-top: 18px; align-items: start; }
.interv-main {
background: var(--panel-glass);
backdrop-filter: blur(40px) saturate(180%);
-webkit-backdrop-filter: blur(40px) saturate(180%);
border: 1px solid var(--panel-edge);
border-radius: 22px;
padding: 22px 24px;
box-shadow: 0 20px 50px rgba(0,0,0,0.30), inset 0 1px 0 rgba(255,255,255,0.05);
}
.interv-main h2 { margin: 0 0 4px; font-size: 17px; color: var(--ink); font-weight: 700; }
.interv-main .desc { color: var(--ink-dim); font-size: 12px; margin-bottom: 14px; }
.interv-controls { display: flex; gap: 8px; align-items: center; padding-bottom: 12px; border-bottom: 1px solid var(--rule); margin-bottom: 12px; flex-wrap: wrap; }
.interv-controls select, .interv-controls button { background: var(--panel-2); color: var(--ink); border: 1px solid var(--rule); border-radius: 5px; padding: 5px 10px; font-size: 12px; font-family: inherit; cursor: pointer; }
.interv-controls .lbl { color: var(--ink-faint); font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; font-weight: 600; }
.slider-list { display: flex; flex-direction: column; gap: 10px; max-height: 720px; overflow-y: auto; padding-right: 6px; }
.slider-row { background: var(--panel-2); border-radius: 8px; padding: 10px 14px; }
.slider-row .hdr { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; margin-bottom: 6px; }
.slider-row .nm { color: var(--ink); font-size: 12px; font-weight: 600; }
.slider-row .ratebox { display: flex; gap: 12px; font-size: 10px; color: var(--ink-faint); font-feature-settings: "tnum"; align-items: baseline; }
.slider-row .ratebox .v { color: var(--A); font-weight: 700; }
.slider-row .ratebox .v.reduced { color: var(--money); }
.slider-row .bar { display: flex; align-items: center; gap: 10px; }
.slider-row input[type=range] { flex: 1; accent-color: var(--money); height: 4px; }
.slider-row .pct { color: var(--money); font-size: 11px; font-weight: 700; min-width: 44px; text-align: right; font-feature-settings: "tnum"; }
.interv-side { display: flex; flex-direction: column; gap: 14px; }
.savings-hero {
background:
radial-gradient(circle at 80% 0%, rgba(52,211,153,0.20) 0%, transparent 60%),
linear-gradient(135deg, rgba(7,36,27,0.55) 0%, rgba(26,74,58,0.40) 100%),
var(--panel-glass);
backdrop-filter: blur(40px) saturate(180%);
-webkit-backdrop-filter: blur(40px) saturate(180%);
border: 1px solid rgba(52,211,153,0.32);
border-radius: 22px;
padding: 24px 26px;
box-shadow: 0 20px 60px rgba(52,211,153,0.18), inset 0 1px 0 rgba(255,255,255,0.10);
position: relative; overflow: hidden;
}
.savings-hero h3 { margin: 0 0 10px; font-size: 10px; text-transform: uppercase; letter-spacing: 0.16em; color: var(--ink-faint); font-weight: 600; }
.savings-hero .savings {
background: linear-gradient(135deg, var(--money) 0%, #6ee7b7 100%);
-webkit-background-clip: text; background-clip: text;
-webkit-text-fill-color: transparent; color: transparent;
font-size: 56px; font-weight: 200; font-feature-settings: "tnum";
letter-spacing: -0.04em; line-height: 1;
}
.savings-hero .label { color: var(--ink-dim); font-size: 13px; margin-top: 6px; font-weight: 400; }
.savings-hero .breakdown { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 16px; margin-top: 16px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,0.10); font-size: 12px; }
.savings-hero .breakdown .l { color: var(--ink-faint); }
.savings-hero .breakdown .v { color: var(--ink); font-feature-settings: "tnum"; text-align: right; font-weight: 600; }
.savings-hero .breakdown .v.money { color: var(--money); }
.savings-hero .breakdown .v.la { color: var(--money-la); }
.roi-card {
background: var(--panel-glass);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
border: 1px solid var(--panel-edge);
border-radius: 18px;
padding: 18px 20px;
box-shadow: 0 12px 36px rgba(0,0,0,0.22), inset 0 1px 0 rgba(255,255,255,0.05);
}
.roi-card h3 { margin: 0 0 10px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.14em; color: var(--ink-faint); font-weight: 600; }
.roi-card .row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.roi-card .row label { color: var(--ink-dim); font-size: 12px; flex: 1; }
.roi-card input[type=text], .roi-card input[type=number] { background: var(--panel-2); color: var(--ink); border: 1px solid var(--rule); border-radius: 5px; padding: 6px 10px; font-size: 13px; font-family: inherit; width: 140px; text-align: right; font-feature-settings: "tnum"; }
.roi-card .roi-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px; }
.roi-card .roi-cell { background: var(--panel-2); padding: 10px 12px; border-radius: 6px; }
.roi-card .roi-cell .l { color: var(--ink-faint); font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; font-weight: 600; margin-bottom: 4px; }
.roi-card .roi-cell .v { color: var(--ink); font-size: 22px; font-weight: 700; font-feature-settings: "tnum"; }
.roi-card .roi-cell.payback .v { color: var(--A); }
.roi-card .roi-cell.roi .v { color: var(--money); }
.svc-impact {
background: var(--panel-glass);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
border: 1px solid var(--panel-edge);
border-radius: 18px;
padding: 18px 20px;
box-shadow: 0 12px 36px rgba(0,0,0,0.22), inset 0 1px 0 rgba(255,255,255,0.05);
}
.svc-impact h3 { margin: 0 0 10px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.14em; color: var(--ink-faint); font-weight: 600; }
.svc-impact-row { display: grid; grid-template-columns: 1fr 90px; gap: 8px; align-items: center; padding: 5px 0; font-size: 12px; border-bottom: 1px solid rgba(255,255,255,0.04); }
.svc-impact-row .nm { color: var(--ink); display: flex; align-items: center; gap: 6px; }
.svc-impact-row .nm .sw { width: 8px; height: 8px; border-radius: 2px; }
.svc-impact-row .delta { text-align: right; font-feature-settings: "tnum"; color: var(--money); font-weight: 600; font-size: 11px; }
/* Print / Exec summary view */
@media print {
body { background: white !important; color: #1f2937; font-size: 11px; }
header > .controls, .tabs, .la-bar .actions, .pat2-mini-legend, .pat2-graph-actions, footer, .loading,
#page-live, #page-patterns, .interv-controls, .pat2-section, .interv-grid > .interv-side > .roi-card,
.interv-grid > .interv-main { display: none !important; }
.exec-print { display: block !important; padding: 0; }
.wrapper { padding: 0; max-width: none; }
}
.exec-print { display: none; }
body.print-mode { background: white; color: #1f2937; }
body.print-mode * { color: inherit !important; }
body.print-mode .wrapper { max-width: 920px; margin: 0 auto; padding: 30px; }
body.print-mode header > .controls, body.print-mode .tabs, body.print-mode .la-bar .actions,
body.print-mode #page-live, body.print-mode #page-patterns, body.print-mode #page-interv,
body.print-mode .loading, body.print-mode footer { display: none !important; }
body.print-mode .la-bar { background: #f0fdf4; border-color: #34d399; }
body.print-mode .exec-print { display: block; }
body.print-mode .exec-print h1 { font-size: 24px; color: #064e3b; margin: 20px 0 6px; }
body.print-mode .exec-print h2 { font-size: 15px; color: #1e3a8a; margin: 22px 0 8px; padding-bottom: 4px; border-bottom: 2px solid #c7d2fe; }
body.print-mode .exec-print h3 { font-size: 12px; color: #475569; margin: 14px 0 4px; text-transform: uppercase; letter-spacing: 0.1em; }
body.print-mode .exec-print p { font-size: 12px; color: #1f2937; line-height: 1.5; }
body.print-mode .exec-print .hero { background: #ecfdf5; border: 2px solid #10b981; border-radius: 10px; padding: 20px 24px; margin: 16px 0; }
body.print-mode .exec-print .hero .big { color: #047857; font-size: 36px; font-weight: 800; font-feature-settings: "tnum"; letter-spacing: -0.02em; }
body.print-mode .exec-print .hero .sub { color: #065f46; font-size: 14px; margin-top: 6px; }
body.print-mode .exec-print table { width: 100%; border-collapse: collapse; margin: 10px 0; font-size: 11px; }
body.print-mode .exec-print table th { background: #e5e7eb; color: #374151; padding: 6px 10px; text-align: left; font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; }
body.print-mode .exec-print table td { padding: 6px 10px; border-bottom: 1px solid #e5e7eb; }
body.print-mode .exec-print table td.num { text-align: right; font-feature-settings: "tnum"; color: #047857; font-weight: 600; }
body.print-mode .exec-print .meta { color: #6b7280; font-size: 10px; margin-top: 4px; font-style: italic; }
body.print-mode .exec-print ul { font-size: 11px; color: #1f2937; padding-left: 20px; }
body.print-mode .exec-print ul li { margin-bottom: 4px; }
/* v0.14 — intervention cards (replaces sliders) */
.interv-card { background: var(--panel-2); border-radius: 8px; padding: 12px 14px; border: 1px solid transparent; transition: border-color 150ms; margin-bottom: 10px; }
.interv-card.active { border-color: var(--money); background: linear-gradient(135deg, #1a2e25 0%, #1c2340 100%); }
.interv-card .card-hdr { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; margin-bottom: 4px; }
.interv-card .card-title { color: var(--ink); font-size: 13px; font-weight: 700; }
.interv-card .ev-badge { font-size: 9px; padding: 2px 6px; border-radius: 3px; font-weight: 700; letter-spacing: 0.04em; white-space: nowrap; }
.interv-card .ev-badge.ev-H { background: rgba(52,211,153,0.18); color: var(--money); border: 1px solid rgba(52,211,153,0.4); }
.interv-card .ev-badge.ev-M { background: rgba(250,204,21,0.18); color: var(--C); border: 1px solid rgba(250,204,21,0.4); }
.interv-card .ev-badge.ev-L { background: rgba(248,113,113,0.18); color: var(--B); border: 1px solid rgba(248,113,113,0.4); }
.interv-card .card-short { color: var(--ink-dim); font-size: 11px; line-height: 1.4; margin-bottom: 6px; }
.interv-card .card-targets { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 8px; }
.interv-card .ant-pill { display: inline-flex; align-items: center; gap: 4px; padding: 2px 7px; background: rgba(255,255,255,0.05); border-radius: 10px; font-size: 10px; color: var(--ink); }
.interv-card .ant-pill .sw { width: 6px; height: 6px; border-radius: 50%; background: var(--antecedent); }
.interv-card .card-spec { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; font-size: 10px; color: var(--ink-faint); margin-bottom: 8px; padding: 6px 0; border-top: 1px solid rgba(255,255,255,0.05); border-bottom: 1px solid rgba(255,255,255,0.05); }
.interv-card .card-spec .l { display: block; text-transform: uppercase; letter-spacing: 0.06em; font-size: 9px; }
.interv-card .card-spec .v { color: var(--ink); font-feature-settings: "tnum"; font-weight: 600; font-size: 11px; }
.interv-card .card-config { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.interv-card .activate { display: flex; align-items: center; gap: 6px; cursor: pointer; color: var(--ink); font-size: 11px; user-select: none; }
.interv-card .activate input[type=checkbox] { accent-color: var(--money); width: 14px; height: 14px; cursor: pointer; }
.interv-card .scale-input { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; }
.interv-card .scale-input label { color: var(--ink-dim); font-size: 11px; white-space: nowrap; }
.interv-card .scale-input input[type=number] { background: var(--panel); color: var(--ink); border: 1px solid var(--rule); border-radius: 4px; padding: 4px 8px; width: 90px; font-size: 12px; text-align: right; font-feature-settings: "tnum"; font-family: inherit; }
.interv-card.active .scale-input input[type=number] { border-color: var(--money); }
.interv-card .card-impact { font-size: 11px; color: var(--ink-faint); margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(255,255,255,0.05); display: none; line-height: 1.5; }
.interv-card.active .card-impact { display: block; color: var(--ink-dim); }
.interv-card.active .card-impact strong { color: var(--money); font-weight: 700; }
.interv-card .cite { font-size: 9px; color: var(--ink-faint); margin-top: 6px; font-style: italic; }
.lib-meta { display: flex; gap: 14px; align-items: center; padding: 8px 14px; background: var(--panel-2); border-radius: 6px; margin-bottom: 14px; font-size: 11px; color: var(--ink-dim); flex-wrap: wrap; }
.lib-meta strong { color: var(--ink); }
.lib-meta .ev-key { display: inline-flex; align-items: center; gap: 4px; }
/* v0.16 — cost-effectiveness frontier chart */
.frontier-panel { background: var(--panel); border: 1px solid var(--rule); border-radius: 12px; padding: 16px 20px; margin-top: 18px; margin-bottom: 18px; position: relative; }
.frontier-hdr { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 4px; gap: 12px; flex-wrap: wrap; }
.frontier-hdr h2 { margin: 0; font-size: 17px; color: var(--ink); font-weight: 700; }
.frontier-hdr .legend { display: flex; gap: 14px; font-size: 11px; color: var(--ink-dim); flex-wrap: wrap; }
.frontier-hdr .legend .item { display: inline-flex; align-items: center; gap: 5px; }
.frontier-hdr .legend .sw-circle { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
.frontier-hdr .legend .sw-line { width: 18px; height: 2.5px; display: inline-block; background: var(--money); border-radius: 1px; }
.frontier-desc { color: var(--ink-dim); font-size: 12px; margin-bottom: 12px; line-height: 1.5; max-width: 920px; }
.frontier-chart-area { display: grid; grid-template-columns: 1fr 170px; gap: 18px; align-items: stretch; }
.frontier-svg { width: 100%; height: 440px; display: block; }
.barometer-wrap { display: flex; flex-direction: column; height: 440px; padding: 4px 0; }
.barometer-hdr { font-size: 10px; text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-faint); font-weight: 600; margin-bottom: 8px; text-align: center; }
.barometer-svg { flex: 1; width: 100%; display: block; }
.barometer-svg .scale-line { stroke: var(--rule); stroke-width: 2; }
.barometer-svg .scale-fill { stroke: var(--A); stroke-width: 5; stroke-linecap: round; }
.barometer-svg .scale-tick { stroke: var(--ink-faint); stroke-width: 1; }
.barometer-svg .scale-tick-label { fill: var(--ink-faint); font-size: 9px; font-feature-settings: "tnum"; }
.barometer-svg .scale-anchor { fill: var(--ink-faint); font-size: 9.5px; font-weight: 600; font-feature-settings: "tnum"; }
.barometer-svg .scale-max-label { fill: var(--money); font-size: 10px; font-weight: 700; font-feature-settings: "tnum"; }
.barometer-svg .ball { fill: var(--A); stroke: var(--ink); stroke-width: 2; filter: drop-shadow(0 0 8px var(--A)); transition: cy 350ms cubic-bezier(0.5,0,0.2,1); }
.barometer-svg .ball-label { fill: var(--ink); font-size: 12px; font-weight: 700; font-feature-settings: "tnum"; pointer-events: none; }
.barometer-svg .ball-pct { fill: var(--A); font-size: 18px; font-weight: 800; font-feature-settings: "tnum"; pointer-events: none; }
.barometer-footer { font-size: 10px; color: var(--ink-faint); text-align: center; margin-top: 8px; line-height: 1.4; }
.barometer-footer strong { color: var(--ink); }
.frontier-svg .axis-label { fill: var(--ink-faint); font-size: 11px; font-weight: 600; }
.frontier-svg .axis-tick { fill: var(--ink-faint); font-size: 10px; font-feature-settings: "tnum"; }
.frontier-svg .grid-line { stroke: var(--rule); stroke-opacity: 0.4; stroke-dasharray: 2,3; }
.frontier-svg .frontier-line { fill: none; stroke: var(--money); stroke-width: 2.5; stroke-opacity: 0.85; stroke-linecap: round; stroke-linejoin: round; }
.frontier-svg .frontier-annot { fill: var(--money); font-size: 11px; font-weight: 700; pointer-events: none; }
.frontier-svg .frontier-annot-sub { fill: var(--money); font-size: 9.5px; font-weight: 500; opacity: 0.85; pointer-events: none; }
.frontier-svg .frontier-area { fill: var(--money); fill-opacity: 0.06; }
/* Inactive: greyed out so the user sees the landscape but knows what's not yet activated */
.frontier-svg .iv-dot { stroke: var(--bg); stroke-width: 1.5; cursor: pointer; transition: all 180ms; fill-opacity: 0.32; }
.frontier-svg .iv-dot:hover { stroke: var(--ink); stroke-width: 2; fill-opacity: 0.65; }
/* Active: full colour + glow */
.frontier-svg .iv-dot.is-active { stroke: var(--ink); stroke-width: 2.5; fill-opacity: 1; filter: drop-shadow(0 0 6px currentColor); }
.frontier-svg .iv-label { fill: var(--ink-faint); font-size: 9.5px; pointer-events: none; opacity: 0.55; }
.frontier-svg .iv-label.is-active { fill: var(--ink); font-weight: 700; opacity: 1; }
.frontier-svg .iv-label.is-top { opacity: 0.95; fill: var(--ink-dim); }
.frontier-svg .current-portfolio-dot { fill: var(--A); stroke: var(--ink); stroke-width: 2; pointer-events: none; }
.frontier-svg .current-portfolio-label { fill: var(--A); font-size: 11px; font-weight: 700; pointer-events: none; }
.frontier-svg .breakeven-line { stroke: var(--money); stroke-width: 2; stroke-dasharray: 6,5; stroke-opacity: 0.85; }
.frontier-svg .breakeven-label { fill: var(--money); font-size: 11px; font-weight: 700; }
.frontier-svg .breakeven-sub { fill: var(--money); font-size: 9.5px; opacity: 0.75; }
.frontier-svg .net-positive-region { fill: var(--money); fill-opacity: 0.04; }
.frontier-loading { text-align: center; padding: 50px 20px; color: var(--ink-faint); font-size: 12px; font-style: italic; }
.frontier-progress { width: 280px; height: 4px; background: var(--panel-2); border-radius: 2px; margin: 10px auto; overflow: hidden; }
.frontier-progress-fill { height: 100%; background: var(--money); width: 0%; transition: width 120ms; }
.frontier-tooltip { position: absolute; background: #fff; color: #1f2937; padding: 8px 12px; border-radius: 6px; font-size: 11px; pointer-events: none; box-shadow: 0 8px 24px rgba(0,0,0,0.4); max-width: 280px; opacity: 0; transition: opacity 120ms; z-index: 100; }
.frontier-tooltip.show { opacity: 1; }
.frontier-tooltip .tt-title { font-weight: 700; margin-bottom: 4px; color: #064e3b; }
.frontier-tooltip .tt-row { display: flex; justify-content: space-between; gap: 14px; font-size: 11px; font-feature-settings: "tnum"; }
.frontier-tooltip .tt-row .l { color: #6b7280; }
.frontier-tooltip .tt-meta { color: #6b7280; font-size: 10px; margin-top: 5px; font-style: italic; }
footer { margin-top: 26px; padding-top: 16px; border-top: 1px solid var(--rule); color: var(--ink-faint); font-size: 11px; }
footer code { background: var(--panel); padding: 2px 5px; border-radius: 3px; color: var(--ink-dim); font-size: 10px; }
/* ============================================================
v1.1 — 5-page wayfinder restructure
Pages: Welcome → Choose LA → See cascade → Spot patterns → Plan interventions
============================================================ */
/* Brand strip — small persistent identity above the wayfinder */
.brand-strip {
display: flex; align-items: center; justify-content: space-between;
gap: 16px;
padding: 4px 0 16px;
color: var(--ink-faint);
font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; font-weight: 600;
}
.brand-strip .left { display: flex; align-items: center; gap: 10px; }
.brand-strip .brand-dot {
width: 8px; height: 8px; border-radius: 50%;
background: linear-gradient(135deg, var(--glow-blue), var(--glow-purple));
box-shadow: 0 0 10px rgba(99,102,241,0.55);
}
.brand-strip .la-context {
display: none;
align-items: center; gap: 8px;
background: var(--panel-glass);
backdrop-filter: blur(20px) saturate(160%);
-webkit-backdrop-filter: blur(20px) saturate(160%);
border: 1px solid var(--panel-edge);
border-radius: 100px;
padding: 4px 12px 4px 6px;
text-transform: none; letter-spacing: 0;
color: var(--ink-dim); font-size: 11px; font-weight: 500;
}
.brand-strip .la-context.visible { display: inline-flex; }
.brand-strip .la-context .dot-mini {
width: 6px; height: 6px; border-radius: 50%;
background: var(--money);
box-shadow: 0 0 6px rgba(52,211,153,0.6);
}
.brand-strip .la-context strong { color: var(--ink); font-weight: 600; }
.brand-strip .la-context .change-link {
margin-left: 4px;
color: var(--glow-blue); cursor: pointer; text-decoration: underline; text-underline-offset: 2px;
}
.brand-strip .la-context .change-link:hover { color: #93c5fd; }
/* Page-specific page headers (replace the global h1+sub from header) */
.page-head { margin-bottom: 24px; }
.page-head h2 {
margin: 0 0 8px;
font-size: 28px; font-weight: 300; color: var(--ink);
letter-spacing: -0.022em; line-height: 1.15;
}
.page-head .sub {
font-size: 14px; color: var(--ink-dim); max-width: 760px; line-height: 1.55;
}
/* ============================
Page 1 — Welcome / Landing
============================ */
#page-welcome { padding-top: 16px; }
.hero-stage {
position: relative;
padding: 56px 56px 48px;
background:
radial-gradient(circle at 85% 10%, rgba(99,102,241,0.18) 0%, transparent 55%),
radial-gradient(circle at 10% 90%, rgba(168,85,247,0.16) 0%, transparent 55%),
var(--panel-glass);
backdrop-filter: blur(40px) saturate(180%);
-webkit-backdrop-filter: blur(40px) saturate(180%);
border: 1px solid var(--panel-edge);
border-radius: 32px;
box-shadow: 0 28px 80px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.08);
overflow: hidden;
}
.hero-eyebrow {
display: inline-flex; align-items: center; gap: 8px;
font-size: 11px; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase;
color: var(--ink-faint);
margin-bottom: 18px;
}
.hero-eyebrow .pill {
padding: 4px 10px;
background: linear-gradient(135deg, rgba(99,102,241,0.18), rgba(168,85,247,0.14));
border: 1px solid rgba(99,102,241,0.30);
border-radius: 100px;
letter-spacing: 0.10em;
}
.hero-headline {
font-size: 52px; font-weight: 200; color: var(--ink);
letter-spacing: -0.035em; line-height: 1.05;
margin: 0 0 16px; max-width: 920px;
}
.hero-headline em {
font-style: normal;
background: linear-gradient(135deg, var(--glow-blue) 0%, var(--glow-purple) 100%);
-webkit-background-clip: text; background-clip: text;
-webkit-text-fill-color: transparent; color: transparent;
}
.hero-lede {
font-size: 17px; font-weight: 300; color: var(--ink-dim);
line-height: 1.55; max-width: 720px;
margin: 0 0 36px;
}
.hero-lede strong { color: var(--ink); font-weight: 500; }
.hero-tiles {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px;
margin: 28px 0 36px;
}
.hero-tile {
background: rgba(255,255,255,0.03);
border: 1px solid var(--panel-edge);
border-radius: 16px;
padding: 20px 22px;
transition: background 200ms;
}
.hero-tile:hover { background: rgba(255,255,255,0.05); }
.hero-tile .tile-eyebrow {
font-size: 10px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
color: var(--ink-faint); margin-bottom: 10px;
}
.hero-tile .tile-num {
font-size: 32px; font-weight: 200; color: var(--ink);
letter-spacing: -0.025em; line-height: 1; margin-bottom: 8px;
font-feature-settings: "tnum";
}
.hero-tile .tile-num em {
font-style: normal;
background: linear-gradient(135deg, var(--money) 0%, #6ee7b7 100%);
-webkit-background-clip: text; background-clip: text;
-webkit-text-fill-color: transparent; color: transparent;
}
.hero-tile .tile-body {
font-size: 13px; color: var(--ink-dim); line-height: 1.5; font-weight: 400;
}
.hero-actions { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.hero-cta {
display: inline-flex; align-items: center; gap: 10px;
padding: 14px 26px;
background: linear-gradient(135deg, var(--glow-blue) 0%, var(--glow-purple) 100%);
color: #fff; border: none; border-radius: 100px;
font-size: 15px; font-weight: 600; cursor: pointer;
font-family: inherit; letter-spacing: -0.005em;
box-shadow: 0 10px 32px rgba(99,102,241,0.40);
transition: transform 150ms ease, box-shadow 150ms ease;
}
.hero-cta:hover { transform: translateY(-1px); box-shadow: 0 14px 40px rgba(99,102,241,0.55); }
.hero-cta .arrow { transition: transform 200ms ease; }
.hero-cta:hover .arrow { transform: translateX(3px); }
.hero-secondary {
display: inline-flex; align-items: center; gap: 6px;
padding: 14px 22px;
background: transparent;
color: var(--ink-dim); border: 1px solid var(--panel-edge); border-radius: 100px;
font-size: 14px; font-weight: 500; cursor: pointer; font-family: inherit;
transition: all 150ms ease;
}
.hero-secondary:hover { color: var(--ink); background: rgba(255,255,255,0.04); border-color: rgba(255,255,255,0.18); }
/* Below the hero: explanatory section */
.welcome-explainer {
display: grid; grid-template-columns: 1fr 1fr; gap: 22px;
margin-top: 24px;
}
.welcome-card {
background: var(--panel-glass);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
border: 1px solid var(--panel-edge);
border-radius: 22px;
padding: 28px 30px;
box-shadow: 0 16px 40px rgba(0,0,0,0.24), inset 0 1px 0 rgba(255,255,255,0.05);
}
.welcome-card h3 {
margin: 0 0 12px;
font-size: 20px; font-weight: 400; color: var(--ink);
letter-spacing: -0.015em;
}
.welcome-card p {
margin: 0 0 12px;
font-size: 14px; color: var(--ink-dim); line-height: 1.6; font-weight: 400;
}
.welcome-card p:last-child { margin-bottom: 0; }
.welcome-card strong { color: var(--ink); font-weight: 500; }
.welcome-card .small-meta {
margin-top: 14px;
font-size: 11px; color: var(--ink-faint); font-style: italic; line-height: 1.5;
}
/* ============================
Page 2 — Choose your LA
============================ */
.la-stage {
background: var(--panel-glass);
backdrop-filter: blur(40px) saturate(180%);
-webkit-backdrop-filter: blur(40px) saturate(180%);
border: 1px solid var(--panel-edge);
border-radius: 28px;
padding: 36px 40px;
box-shadow: 0 24px 60px rgba(0,0,0,0.30), inset 0 1px 0 rgba(255,255,255,0.06);
}
.la-picker-row {
display: grid; grid-template-columns: 1fr auto; gap: 18px; align-items: end;
margin-top: 20px; padding-bottom: 22px; border-bottom: 1px solid var(--panel-edge);
}
.la-picker-row .field { display: flex; flex-direction: column; gap: 8px; }
.la-picker-row .field label {
font-size: 11px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
color: var(--ink-faint);
}
.la-picker-row select#laSelMain {
background: rgba(255,255,255,0.04); color: var(--ink);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 14px;
padding: 14px 20px;
font-size: 16px; font-family: inherit; cursor: pointer;
font-weight: 500; letter-spacing: -0.005em;
min-width: 320px;
transition: all 200ms ease;
}
.la-picker-row select#laSelMain:hover {
background: rgba(255,255,255,0.07); border-color: rgba(255,255,255,0.20);
}
.la-picker-row .continue-cta {
display: inline-flex; align-items: center; gap: 10px;
padding: 14px 26px;
background: linear-gradient(135deg, var(--money) 0%, #10b981 100%);
color: #042620; border: none; border-radius: 100px;
font-size: 14px; font-weight: 600; cursor: pointer; font-family: inherit;
box-shadow: 0 8px 24px rgba(52,211,153,0.35);
transition: transform 150ms ease, box-shadow 150ms ease;
}
.la-picker-row .continue-cta:hover { transform: translateY(-1px); box-shadow: 0 12px 32px rgba(52,211,153,0.45); }
.la-picker-row .continue-cta:disabled {
opacity: 0.4; cursor: not-allowed; transform: none; box-shadow: none;
}
.la-context-grid {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px;
margin-top: 24px;
}
.la-context-card {
background: rgba(255,255,255,0.03);
border: 1px solid var(--panel-edge);
border-radius: 16px;
padding: 18px 20px;
}
.la-context-card .l {
font-size: 10px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
color: var(--ink-faint); margin-bottom: 8px;
}
.la-context-card .v {
font-size: 24px; font-weight: 300; color: var(--ink);
letter-spacing: -0.02em; line-height: 1; font-feature-settings: "tnum";
}
.la-context-card .v.accent {
background: linear-gradient(135deg, var(--glow-blue) 0%, var(--glow-purple) 100%);
-webkit-background-clip: text; background-clip: text;
-webkit-text-fill-color: transparent; color: transparent;
}
.la-context-card .extra { margin-top: 6px; font-size: 11px; color: var(--ink-dim); line-height: 1.4; }
.la-meta-narrative {
margin-top: 22px;
padding: 18px 22px;
background: rgba(255,255,255,0.03);
border: 1px solid var(--panel-edge);
border-radius: 16px;
font-size: 13px; color: var(--ink-dim); line-height: 1.6;
}
.la-meta-narrative strong { color: var(--ink); font-weight: 500; }
.la-meta-narrative .hint {
display: block; margin-top: 8px;
color: var(--ink-faint); font-size: 11px; font-style: italic;
}
/* ============================
Page 4 (Patterns) — how-to + so-what panels
============================ */
.pat-howto {
display: flex; align-items: flex-start; gap: 14px;
background: linear-gradient(135deg, rgba(99,102,241,0.10), rgba(168,85,247,0.08));
border: 1px solid rgba(99,102,241,0.25);
border-radius: 16px;
padding: 14px 18px;
margin-bottom: 16px;
font-size: 13px; color: var(--ink-dim); line-height: 1.55;
}
.pat-howto .ico {
flex-shrink: 0;
width: 28px; height: 28px; border-radius: 50%;
background: linear-gradient(135deg, var(--glow-blue), var(--glow-purple));
color: #fff; font-weight: 600; font-size: 14px;
display: inline-flex; align-items: center; justify-content: center;
}
.pat-howto strong { color: var(--ink); font-weight: 500; }
.pat-howto .key {
display: inline-block;
padding: 1px 7px;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.15);
border-radius: 4px;
font-size: 11px; font-weight: 500; color: var(--ink);
}
.pat-sowhat {
background:
radial-gradient(circle at 100% 0%, rgba(52,211,153,0.16) 0%, transparent 60%),
var(--panel-glass);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
border: 1px solid rgba(52,211,153,0.30);
border-radius: 20px;
padding: 22px 26px;
margin-top: 16px;
box-shadow: 0 14px 36px rgba(52,211,153,0.14), inset 0 1px 0 rgba(255,255,255,0.05);
}
.pat-sowhat .sowhat-eyebrow {
font-size: 10px; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase;
color: var(--money); margin-bottom: 10px;
}
.pat-sowhat h3 {
margin: 0 0 10px;
font-size: 20px; font-weight: 300; color: var(--ink);
letter-spacing: -0.015em; line-height: 1.25;
}
.pat-sowhat p {
margin: 0 0 10px;
font-size: 13px; color: var(--ink-dim); line-height: 1.6;
}
.pat-sowhat p:last-child { margin: 0; }
.pat-sowhat strong { color: var(--ink); font-weight: 500; }
.pat-sowhat .money-callout {
background: linear-gradient(135deg, var(--money) 0%, #6ee7b7 100%);
-webkit-background-clip: text; background-clip: text;
-webkit-text-fill-color: transparent; color: transparent;
font-weight: 600;
}
/* ============================
Page 5 (Interventions) — model-coverage banner
============================ */
.interv-coverage-banner {
display: flex; gap: 16px; align-items: flex-start;
background:
radial-gradient(circle at 100% 0%, rgba(168,85,247,0.16) 0%, transparent 60%),
var(--panel-glass);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
border: 1px solid rgba(168,85,247,0.30);
border-radius: 18px;
padding: 18px 22px;
margin-bottom: 18px;
box-shadow: 0 12px 32px rgba(168,85,247,0.14);
}
.interv-coverage-banner .badge {
flex-shrink: 0;
padding: 4px 10px;
background: linear-gradient(135deg, rgba(168,85,247,0.25), rgba(99,102,241,0.18));
border: 1px solid rgba(168,85,247,0.35);
border-radius: 100px;
font-size: 10px; font-weight: 700; letter-spacing: 0.1em;
color: #d8b4fe;
white-space: nowrap;
}
.interv-coverage-banner .body {
font-size: 13px; color: var(--ink-dim); line-height: 1.55;
}
.interv-coverage-banner .body strong { color: var(--ink); font-weight: 500; }
.interv-coverage-banner .body .bracket {
display: inline-block;
padding: 1px 7px;
background: rgba(255,255,255,0.06);
border-radius: 4px;
font-feature-settings: "tnum"; font-weight: 500; color: var(--ink);
font-size: 12px;
}
/* v1.1 — Next-step CTA row at bottom of each journey page */
.page-cta-row {
display: grid; grid-template-columns: 1fr auto; gap: 24px;
align-items: center;
margin-top: 28px;
padding: 22px 26px;
background:
radial-gradient(circle at 100% 0%, rgba(99,102,241,0.16) 0%, transparent 60%),
var(--panel-glass);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
border: 1px solid rgba(99,102,241,0.30);
border-radius: 20px;
box-shadow: 0 14px 36px rgba(99,102,241,0.14), inset 0 1px 0 rgba(255,255,255,0.05);
}
.page-cta-row .cta-eyebrow {
font-size: 10px; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase;
color: var(--glow-blue); margin-bottom: 6px;
}
.page-cta-row .cta-headline {
font-size: 20px; font-weight: 400; color: var(--ink);
letter-spacing: -0.015em; margin-bottom: 6px; line-height: 1.25;
}
.page-cta-row .cta-body {
font-size: 13px; color: var(--ink-dim); line-height: 1.55; max-width: 720px;
}
.page-cta-row .cta-body strong { color: var(--ink); font-weight: 500; }
.page-cta-btn {
display: inline-flex; align-items: center; gap: 10px;
padding: 14px 26px;
background: linear-gradient(135deg, var(--glow-blue) 0%, var(--glow-purple) 100%);
color: #fff; border: none; border-radius: 100px;
font-size: 14px; font-weight: 600; cursor: pointer; font-family: inherit;
letter-spacing: -0.005em;
box-shadow: 0 10px 28px rgba(99,102,241,0.40);
transition: transform 150ms ease, box-shadow 150ms ease;
white-space: nowrap;
}
.page-cta-btn:hover { transform: translateY(-1px); box-shadow: 0 14px 36px rgba(99,102,241,0.55); }
.page-cta-btn .arrow { transition: transform 200ms ease; }
.page-cta-btn:hover .arrow { transform: translateX(3px); }
/* v1.1 — Intervention card activation: replace small checkbox with prominent toggle pill.
Uses .interv-card .activate input[type=checkbox] from existing markup (no HTML changes). */
.interv-card .activate {
gap: 10px;
padding: 8px 14px 8px 10px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.10);
border-radius: 100px;
font-weight: 500;
transition: all 180ms ease;
}
.interv-card .activate:hover {
background: rgba(255,255,255,0.07);
border-color: rgba(255,255,255,0.18);
}
.interv-card.active .activate {
background: linear-gradient(135deg, rgba(52,211,153,0.20) 0%, rgba(16,185,129,0.14) 100%);
border-color: rgba(52,211,153,0.50);
box-shadow: 0 0 0 1px rgba(52,211,153,0.20), 0 6px 18px rgba(52,211,153,0.22);
color: var(--ink);
}
/* Custom toggle switch — built from the native checkbox using appearance:none.
Falls back gracefully if appearance isn't supported. */
.interv-card .activate input[type=checkbox] {
appearance: none; -webkit-appearance: none; -moz-appearance: none;
width: 36px; height: 20px;
background: rgba(255,255,255,0.10);
border: 1px solid rgba(255,255,255,0.18);
border-radius: 100px;
position: relative; cursor: pointer;
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
outline: none;
margin: 0;
}
.interv-card .activate input[type=checkbox]::before {
content: '';
position: absolute;
width: 14px; height: 14px;
top: 2px; left: 2px;
background: linear-gradient(135deg, #f8fafc, #cbd5e1);
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0,0,0,0.30);
transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
}
.interv-card .activate input[type=checkbox]:checked {
background: linear-gradient(135deg, var(--money) 0%, #10b981 100%);
border-color: rgba(52,211,153,0.70);
box-shadow: 0 0 12px rgba(52,211,153,0.40), inset 0 1px 2px rgba(0,0,0,0.10);
}
.interv-card .activate input[type=checkbox]:checked::before {
transform: translateX(16px);
background: #ffffff;
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
}
.interv-card .activate input[type=checkbox]:focus-visible {
outline: 2px solid var(--glow-blue); outline-offset: 2px;
}
/* The activate label text — make it clearer */
.interv-card .activate {
font-size: 12px;
letter-spacing: 0.01em;
text-transform: uppercase;
color: var(--ink-dim);
}
.interv-card.active .activate { color: var(--money); font-weight: 600; }
/* ============================
Page 6 — Take it to the CEO (exec plan)
============================ */
.plan-empty {
background: var(--panel-glass);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
border: 1px solid var(--panel-edge);
border-radius: 22px;
padding: 48px 40px;
text-align: center;
box-shadow: 0 16px 40px rgba(0,0,0,0.22), inset 0 1px 0 rgba(255,255,255,0.05);
}
.plan-empty-ico {
display: inline-flex; align-items: center; justify-content: center;
width: 56px; height: 56px; border-radius: 50%;
background: linear-gradient(135deg, rgba(99,102,241,0.20), rgba(168,85,247,0.16));
border: 1px solid rgba(99,102,241,0.30);
font-size: 24px; font-weight: 300; color: var(--glow-blue);
margin-bottom: 18px;
}
.plan-empty h3 { margin: 0 0 10px; font-size: 22px; font-weight: 300; color: var(--ink); letter-spacing: -0.015em; }
.plan-empty p { margin: 0; font-size: 14px; color: var(--ink-dim); line-height: 1.55; max-width: 540px; margin: 0 auto; }
.plan-empty strong { color: var(--ink); font-weight: 500; }
.plan-kpi-row {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px;
margin-bottom: 22px;
}
.plan-kpi-card {
background: var(--panel-glass);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
border: 1px solid var(--panel-edge);
border-radius: 18px;
padding: 18px 20px;
box-shadow: 0 12px 32px rgba(0,0,0,0.22), inset 0 1px 0 rgba(255,255,255,0.05);
}
.plan-kpi-card .l {
font-size: 10px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
color: var(--ink-faint); margin-bottom: 10px;
}
.plan-kpi-card .v {
font-size: 30px; font-weight: 300; letter-spacing: -0.025em;
line-height: 1; font-feature-settings: "tnum";
color: var(--ink);
}
.plan-kpi-card .v.positive {
background: linear-gradient(135deg, var(--money) 0%, #6ee7b7 100%);
-webkit-background-clip: text; background-clip: text;
-webkit-text-fill-color: transparent; color: transparent;
}
.plan-kpi-card .v.negative {
background: linear-gradient(135deg, #f87171 0%, #fca5a5 100%);
-webkit-background-clip: text; background-clip: text;
-webkit-text-fill-color: transparent; color: transparent;
}
.plan-kpi-card .extra {
margin-top: 8px; font-size: 11px; color: var(--ink-dim); line-height: 1.4;
}
.plan-grid {
display: grid; grid-template-columns: 460px 1fr; gap: 22px; align-items: start;
}
.plan-section-header { margin-bottom: 14px; }
.plan-section-header h3 {
margin: 0 0 6px; font-size: 18px; font-weight: 400; color: var(--ink);
letter-spacing: -0.012em;
}
.plan-section-header p {
margin: 0; font-size: 12px; color: var(--ink-dim); line-height: 1.5;
}
.plan-left, .plan-right {
background: var(--panel-glass);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
border: 1px solid var(--panel-edge);
border-radius: 22px;
padding: 22px 24px;
box-shadow: 0 16px 40px rgba(0,0,0,0.22), inset 0 1px 0 rgba(255,255,255,0.05);
}
#planInterventionList {
display: flex; flex-direction: column; gap: 10px;
max-height: 560px; overflow-y: auto;
padding-right: 6px;
margin-bottom: 16px;
}
.plan-iv-card {
background: rgba(255,255,255,0.03);
border: 1px solid var(--panel-edge);
border-radius: 14px;
padding: 12px 14px;
transition: background 200ms;
}
.plan-iv-card:hover { background: rgba(255,255,255,0.05); }
.plan-iv-card .name {
font-size: 13px; font-weight: 600; color: var(--ink);
margin-bottom: 4px; line-height: 1.3;
}
.plan-iv-card .meta {
display: flex; gap: 12px; flex-wrap: wrap;
font-size: 10px; color: var(--ink-faint); text-transform: uppercase;
letter-spacing: 0.08em; margin-bottom: 10px;
}
.plan-iv-card .meta .v { color: var(--ink); font-weight: 600; font-feature-settings: "tnum"; text-transform: none; letter-spacing: 0; }
.plan-iv-card .meta .v.spend { color: #f87171; }
.plan-iv-card .meta .v.benefit { color: var(--money); }
.plan-iv-card .control-row {
display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: center;
padding-top: 8px; border-top: 1px solid rgba(255,255,255,0.06);
}
.plan-iv-card .start-control {
display: flex; align-items: center; gap: 10px;
}
.plan-iv-card .start-control label {
font-size: 10px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
color: var(--ink-faint);
}
.plan-iv-card select.start-month {
background: rgba(255,255,255,0.04); color: var(--ink);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 100px;
padding: 6px 12px;
font-size: 12px; font-family: inherit; cursor: pointer; font-weight: 500;
font-feature-settings: "tnum";
}
.plan-iv-card .lag-display {
font-size: 10px; color: var(--ink-faint); font-style: italic;
text-align: right;
}
.plan-iv-card .lag-display strong { color: var(--ink-dim); font-style: normal; font-weight: 500; }
.plan-actions {
display: flex; gap: 12px; flex-wrap: wrap;
padding-top: 14px; border-top: 1px solid var(--panel-edge);
}
.plan-chart-wrap { margin-bottom: 18px; }
.plan-chart-wrap:last-child { margin-bottom: 0; }
.plan-chart-title {
font-size: 12px; font-weight: 600; letter-spacing: 0.04em;
color: var(--ink); margin-bottom: 10px;
display: flex; align-items: baseline; gap: 8px;
}
.plan-chart-title .title-meta {
font-size: 10px; color: var(--ink-faint); font-weight: 400; letter-spacing: 0;
}
.cashflow-svg {
width: 100%; height: 240px;
display: block;
}
.cashflow-svg .axis-line { stroke: rgba(255,255,255,0.10); stroke-width: 1; }
.cashflow-svg .zero-line { stroke: rgba(255,255,255,0.30); stroke-width: 1; stroke-dasharray: 3,3; }
.cashflow-svg .axis-label { fill: var(--ink-faint); font-size: 10px; font-family: inherit; }
.cashflow-svg .bar-spend { fill: rgba(248,113,113,0.55); }
.cashflow-svg .bar-benefit { fill: rgba(52,211,153,0.55); }
.cashflow-svg .cum-line { fill: none; stroke: #fcd34d; stroke-width: 2; }
.cashflow-svg .cum-line.positive { stroke: var(--money); }
.cashflow-svg .payback-marker { stroke: var(--money); stroke-width: 1.5; stroke-dasharray: 4,3; }
.cashflow-svg .payback-label { fill: var(--money); font-size: 10px; font-weight: 600; font-family: inherit; }
/* ============================
Print mode — A4 exec summary
============================ */
@media print {
body.plan-printing > .wrapper > header,
body.plan-printing > .wrapper > .tab-page:not(#page-plan),
body.plan-printing #page-plan .plan-grid,
body.plan-printing #page-plan .plan-actions,
body.plan-printing #page-plan .plan-section-header,
body.plan-printing #page-plan .plan-kpi-row,
body.plan-printing > .wrapper > footer,
body.plan-printing #execPrintView { display: none !important; }
body.plan-printing { background: white !important; color: #1f2937 !important; font-family: -apple-system, system-ui, sans-serif; }
body.plan-printing .wrapper { padding: 0; max-width: none; }
body.plan-printing #page-plan { display: block !important; padding: 0; }
body.plan-printing #page-plan .page-head { display: none !important; }
body.plan-printing .plan-exec-print { display: block !important; max-width: 760px; margin: 0 auto; padding: 24px 30px; }
}
.plan-exec-print { display: none; }
.plan-exec-print h1 { font-size: 22px; color: #064e3b; margin: 0 0 4px; }
.plan-exec-print .doc-sub { font-size: 12px; color: #6b7280; margin-bottom: 18px; }
.plan-exec-print h2 {
font-size: 14px; color: #1e3a8a; margin: 20px 0 8px;
padding-bottom: 4px; border-bottom: 2px solid #c7d2fe;
text-transform: uppercase; letter-spacing: 0.08em;
}
.plan-exec-print .print-kpis {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px;
margin: 12px 0 18px;
}
.plan-exec-print .print-kpi {
background: #f0fdf4; border: 1px solid #bbf7d0;
border-radius: 8px; padding: 8px 10px;
}
.plan-exec-print .print-kpi .l { color: #475569; font-size: 9px; text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; margin-bottom: 4px; }
.plan-exec-print .print-kpi .v { color: #047857; font-size: 18px; font-weight: 700; font-feature-settings: "tnum"; }
.plan-exec-print .print-kpi .v.neg { color: #b91c1c; }
.plan-exec-print table { width: 100%; border-collapse: collapse; margin: 8px 0; font-size: 11px; }
.plan-exec-print th { background: #e5e7eb; color: #374151; padding: 6px 10px; text-align: left; font-size: 9px; text-transform: uppercase; letter-spacing: 0.06em; }
.plan-exec-print td { padding: 6px 10px; border-bottom: 1px solid #e5e7eb; }
.plan-exec-print td.num { text-align: right; font-feature-settings: "tnum"; }
.plan-exec-print td.num.pos { color: #047857; font-weight: 600; }
.plan-exec-print td.num.neg { color: #b91c1c; }
.plan-exec-print .print-chart { margin: 8px 0; }
.plan-exec-print .print-chart svg { background: white; border: 1px solid #e5e7eb; border-radius: 4px; }
.plan-exec-print .print-chart-title { font-size: 11px; font-weight: 600; color: #1f2937; margin-bottom: 4px; }
.plan-exec-print .print-method { font-size: 9px; color: #6b7280; line-height: 1.5; margin-top: 14px; padding-top: 8px; border-top: 1px solid #e5e7eb; font-style: italic; }
/* Welcome page tile responsive */
@media (max-width: 900px) {
.hero-tiles { grid-template-columns: 1fr; }
.welcome-explainer { grid-template-columns: 1fr; }
.la-context-grid { grid-template-columns: repeat(2, 1fr); }
.hero-headline { font-size: 36px; }
.page-cta-row { grid-template-columns: 1fr; }
.plan-grid { grid-template-columns: 1fr; }
.plan-kpi-row { grid-template-columns: repeat(2, 1fr); }
}
</style>
</head>
<body>
<div class="wrapper">
<header>
<!-- Persistent brand strip + LA context chip -->
<div class="brand-strip">
<div class="left">
<span class="brand-dot"></span>
PSI · Public Sector Innovation · LEG v0.12 (calibrated)
</div>
<div class="la-context" id="laContextChip">
<span class="dot-mini"></span>
<span>Viewing: <strong id="laContextName"></strong></span>
<span class="change-link" id="laContextChange" role="button" tabindex="0">change</span>
</div>
</div>
<!-- Wayfinder: 5-step user journey (Welcome → LA → Cascade → Patterns → Interventions) -->
<div class="tabs" role="tablist" aria-label="Analysis journey">
<button class="tab-btn active" data-tab="welcome" role="tab" aria-selected="true">
<span class="step-num">1</span>Welcome
</button>
<button class="tab-btn" data-tab="la" role="tab" aria-selected="false">
<span class="step-num">2</span>Choose your authority
</button>
<button class="tab-btn" data-tab="live" role="tab" aria-selected="false">
<span class="step-num">3</span>See the cascade
</button>
<button class="tab-btn" data-tab="patterns" role="tab" aria-selected="false">
<span class="step-num">4</span>Spot the patterns
</button>
<button class="tab-btn" data-tab="interv" role="tab" aria-selected="false">
<span class="step-num">5</span>Plan interventions
</button>
<button class="tab-btn" data-tab="plan" role="tab" aria-selected="false">
<span class="step-num">6</span>Take it to the CEO
</button>
</div>
</header>
<!-- ============================ Page 1 — Welcome / Landing ============================ -->
<div class="tab-page active" id="page-welcome">
<div class="hero-stage">
<div class="hero-eyebrow">
<span class="pill">For council leaders</span>
Public Sector Innovation · Behavioural Physics
</div>
<h1 class="hero-headline">
Most council demand isn't random.<br>
It <em>cascades</em>.
</h1>
<p class="hero-lede">
One life event — a bereavement, a discharge from hospital, a benefit sanction — triggers a chain of downstream council touches that costs more than the original event ever did. <strong>PSI applies a Behavioural Physics model to make the cascade visible</strong>, so you can see where targeted prevention pays back before you spend the budget.
</p>
<div class="hero-tiles">
<div class="hero-tile">
<div class="tile-eyebrow">The model</div>
<div class="tile-num"><em>259</em> edges</div>
<div class="tile-body">A Life Event Graph linking 42 council-relevant trigger events to 92 service nodes, with 59 compounding antecedent-to-antecedent pathways and 10 service-to-antecedent feedback edges. Calibrated against UK empirical incidence.</div>
</div>
<div class="hero-tile">
<div class="tile-eyebrow">The simulation</div>
<div class="tile-num"><em>100k</em> people</div>
<div class="tile-body">A Monte Carlo run over an HSV-conditioned synthetic population gives you authority-specific 36-month cost forecasts, not national averages.</div>
</div>
<div class="hero-tile">
<div class="tile-eyebrow">The decision</div>
<div class="tile-num"><em>27</em> interventions</div>
<div class="tile-body">An evidence-based library of UK interventions, ranked on cost-effectiveness for your authority's cohort — primary prevention and tertiary disruption both modelled.</div>
</div>
</div>
<div class="hero-actions">
<button class="hero-cta" id="welcomeStartBtn">
Choose your authority
<span class="arrow"></span>
</button>
<button class="hero-secondary" id="welcomeLearnBtn">Read the method</button>
</div>
</div>
<div class="welcome-explainer">
<div class="welcome-card">
<h3>What you'll see in the next four pages</h3>
<p><strong>Choose your authority</strong> sets the population, deprivation, and incidence profile the simulation runs against.</p>
<p><strong>See the cascade</strong> plays the 36-month event cascade for that authority's population — 100,000 simulated lives, each with their own HSV vector, each triggering events at HSV-conditioned rates.</p>
<p><strong>Spot the patterns</strong> mines the simulation for the recurring chains and loops driving the largest share of cost.</p>
<p><strong>Plan interventions</strong> lets you stack evidence-based interventions and see how they bend the cost curve — with full downstream cascade savings credited, not just direct service avoidance.</p>
</div>
<div class="welcome-card">
<h3>Why this is different to a normal council dashboard</h3>
<p>Traditional cost analysis treats each service line as independent. PSI's model treats them as one connected system — the way they actually behave in a person's life.</p>
<p>This matters because <strong>primary prevention only looks viable when you can credit the full downstream chain it disrupts</strong>. A £1,400 carer's respite intervention isn't competing with one ASC service; it's competing with the cared-for person's MH crisis, the carer's own MH risk, the cascade into A&amp;E and employment loss, and so on.</p>
<p class="small-meta">Model basis: LEG v0.12 — 259 edges over 42 antecedents and 92 service nodes. Includes 59 antecedent-to-antecedent compounding edges (batches 29 + 30), 10 service-to-antecedent feedback edges (batch 31), and a calibrated baseline pass so simulated antecedent firing rates converge with empirical UK population figures.</p>
</div>
</div>
</div><!-- /page-welcome -->
<!-- ============================ Page 2 — Choose your LA ============================ -->
<div class="tab-page" id="page-la">
<div class="page-head">
<h2>Choose your local authority</h2>
<p class="sub">The model runs against your authority's specific population size, deprivation profile, and antecedent-incidence multipliers. Switching authority re-tunes every number you see on the next three pages.</p>
</div>
<div class="la-stage">
<div class="la-picker-row">
<div class="field">
<label for="laSelMain">Local authority</label>
<select id="laSelMain"><!-- populated from LA_PRESETS, kept in sync with #laSel --></select>
</div>
<button class="continue-cta" id="laContinueBtn">
Continue to cascade
<span></span>
</button>
</div>
<div class="la-context-grid" id="laContextGrid">
<div class="la-context-card">
<div class="l">Adult population</div>
<div class="v" id="laPopV"></div>
<div class="extra" id="laPopExtra">&nbsp;</div>
</div>
<div class="la-context-card">
<div class="l">Deprivation profile</div>
<div class="v" id="laDeprivV"></div>
<div class="extra" id="laDeprivExtra">&nbsp;</div>
</div>
<div class="la-context-card">
<div class="l">Region</div>
<div class="v" id="laRegionV"></div>
<div class="extra" id="laRegionExtra">&nbsp;</div>
</div>
<div class="la-context-card">
<div class="l">Modelled 36-mo public cost</div>
<div class="v accent" id="laCostV"></div>
<div class="extra" id="laCostExtra">&nbsp;</div>
</div>
</div>
<div class="la-meta-narrative" id="laMetaNarrative">
Pick an authority above to see its profile.
<span class="hint">If your authority isn't listed, the closest deprivation-and-size peer will give you a directionally accurate result.</span>
</div>
</div>
</div><!-- /page-la -->
<!-- ============================ Page 3 — See the cascade ============================ -->
<div class="tab-page" id="page-live">
<div class="page-head">
<h2>Cascade and cost for <span id="popLabel">100,000</span> people over 36 months</h2>
<p class="sub">Each simulated person has an HSV vector that determines both <strong>which antecedent events they experience</strong> (rates are HSV-conditioned Poisson) and <strong>how those events cascade</strong> through council services.</p>
</div>
<div class="controls">
<button id="playBtn" class="active playback-only">⏸ Pause</button>
<button id="resetBtn" class="playback-only">⟲ Reset</button>
<div class="scrub-group playback-only">
<input type="range" class="scrub" id="scrub" min="0" max="3600" step="1" value="0">
<span class="time-display"><span id="timeDisplay">0.0</span> mo</span>
</div>
<span class="lbl">Sample</span>
<select id="popSel">
<option value="10000">10k</option>
<option value="100000" selected>100k</option>
<option value="1000000">1M</option>
</select>
<span class="lbl playback-only">Speed</span>
<select id="speedSel" class="playback-only">
<option value="0.5">0.5×</option>
<option value="1" selected></option>
<option value="2"></option>
<option value="4"></option>
<option value="8"></option>
</select>
<span class="lbl playback-only">Recurrence</span>
<select id="recSel" class="playback-only">
<option value="show" selected>Show</option>
<option value="hide">Hide</option>
<option value="only">Only</option>
</select>
<button id="execPrintBtn" class="secondary" style="margin-left:auto">📄 Exec summary</button>
</div>
<!-- Hidden legacy LA bar — kept so existing JS that reads #laSel and #laMeta still works -->
<div class="la-bar" style="display:none">
<div class="pickwrap"><label>LA</label><select id="laSel"></select></div>
<div class="meta" id="laMeta"></div>
</div>
<div class="main">
<div class="viz-panel">
<div class="viz-stack">
<svg class="viz-svg" id="viz"></svg>
<canvas id="particles"></canvas>
</div>
<div class="loading" id="loading">
<div>Simulating people…</div>
<div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
<div id="progressMsg" style="color: var(--ink-dim); font-size: 12px;">starting</div>
</div>
</div>
<div class="side">
<div class="totals-hero">
<h3>36-month cascade cost</h3>
<div class="pair">
<div class="big"><div class="label">Public sector total</div><div class="value" id="totPublic">£0</div></div>
<div class="big la"><div class="label">LA-borne share</div><div class="value" id="totLa">£0</div></div>
</div>
<div class="stats">
<span class="l">People touched</span><span class="v" id="pctTouched"></span>
<span class="l">Avg cost / touched</span><span class="v" id="avgPerTouched"></span>
<span class="l">Top 5% drive</span><span class="v" id="concentration"></span>
<span class="l">Total events</span><span class="v" id="totalEvents"></span>
</div>
</div>
<div class="cost-table-panel">
<h3>Per-node cost <span style="color: var(--money); font-weight: 700; font-size: 10px;">↓ public £</span></h3>
<div class="scroll">
<table class="cost-table" id="costTable">
<thead><tr><th>Node</th><th>Public £</th><th>LA £</th></tr></thead>
<tbody id="costTbody"><tr><td colspan="3" class="empty">Press play.</td></tr></tbody>
<tfoot><tr><td>Total</td><td class="public" id="footPublic">£0</td><td class="la" id="footLa">£0</td></tr></tfoot>
</table>
</div>
</div>
<div class="portfolio-panel">
<h3>By service portfolio</h3>
<table class="port-table">
<thead><tr><th>Portfolio</th><th>Public £</th><th>LA £</th></tr></thead>
<tbody id="portTbody"><tr><td colspan="3" class="empty"></td></tr></tbody>
</table>
</div>
<div class="longtail-panel">
<h3>Per-person 36mo cost — long tail <span class="pct">log £ axis</span></h3>
<svg class="longtail-svg" id="longtail"></svg>
</div>
<div class="legend">
<h3>Visual key</h3>
<div class="legend-items">
<div class="item"><span class="swatch" style="background:var(--A)"></span>resilient person</div>
<div class="item"><span class="swatch" style="background:var(--C)"></span>average person</div>
<div class="item"><span class="swatch" style="background:var(--B)"></span>precarious person</div>
<div class="item"><span class="swatch dashed"></span>Recurrence edge</div>
<div class="item"><span class="swatch" style="background:var(--housing)"></span>Housing</div>
<div class="item"><span class="swatch" style="background:var(--mental_health)"></span>Mental health</div>
<div class="item"><span class="swatch" style="background:var(--substance)"></span>Substance</div>
<div class="item"><span class="swatch" style="background:var(--adult_social_care)"></span>Adult social care</div>
<div class="item"><span class="swatch" style="background:var(--domestic_abuse)"></span>Domestic abuse</div>
<div class="item"><span class="swatch" style="background:var(--welfare)"></span>Welfare</div>
</div>
</div>
</div>
</div>
<!-- Journey wayfinding CTA: directs user from cascade page to patterns page -->
<div class="page-cta-row">
<div class="page-cta-text">
<div class="cta-eyebrow">Next step</div>
<div class="cta-headline">Now mine the simulation for recurring patterns</div>
<div class="cta-body">The cascade you just watched produces thousands of distinct trajectories. The next page surfaces the <strong>chains</strong> driving the largest share of cost and the <strong>loops</strong> where people re-enter the system instead of resolving.</div>
</div>
<button class="page-cta-btn" id="goPatternsBtn">
Spot the patterns
<span class="arrow"></span>
</button>
</div>
</div><!-- /page-live -->
<div class="tab-page" id="page-patterns">
<div class="page-head">
<h2>Spot the patterns</h2>
<p class="sub">The simulation produces thousands of distinct trajectories. These two panels surface the ones that matter — the <strong>recurring chains</strong> that drive the largest share of cost, and the <strong>loops</strong> where a service handoff sends people back to an earlier service instead of resolving the cascade.</p>
</div>
<div class="pat-howto">
<div class="ico"></div>
<div>
<strong>How to use this view.</strong> Pick a chain or loop from the lists on the right — the 3D graph below highlights its nodes and edges, and animates particles along the path. Rotate the graph by click-and-drag, zoom with the wheel, and press <span class="key">Clear selection</span> to reset.
</div>
</div>
<div class="patterns2">
<div class="patterns2-center">
<div class="pat2-graph-header">
<div>
<div class="pat2-graph-title" id="pat2Title">Select a chain or loop on the right</div>
<div class="pat2-graph-sub" id="pat2Sub">The full LEG is shown below. Selecting a pattern highlights its nodes and edges and animates HSV-mix-coloured particles along it.</div>
</div>
<div class="pat2-graph-actions">
<button id="pat2Clear">Clear selection</button>
</div>
</div>
<div id="viz2-3d" style="width: 100%; height: 840px; background: #050810; border-radius: 8px; cursor: grab; position: relative;"></div>
<div class="pat2-mini-legend">
<span><span class="sw-circle" style="background:var(--antecedent)"></span>Trigger</span>
<span><span class="sw-circle" style="background:var(--housing)"></span>Housing</span>
<span><span class="sw-circle" style="background:var(--mental_health)"></span>MH</span>
<span><span class="sw-circle" style="background:var(--substance)"></span>Substance</span>
<span><span class="sw-circle" style="background:var(--adult_social_care)"></span>ASC</span>
<span><span class="sw-circle" style="background:var(--domestic_abuse)"></span>DA</span>
<span><span class="sw-circle" style="background:var(--welfare)"></span>Welfare</span>
<span style="margin-left: 12px"><span class="line-sample fwd"></span>forward</span>
<span><span class="line-sample back"></span>recurrence (curved)</span>
<span style="margin-left: 12px"><span class="sw-circle" style="background:var(--A)"></span>resilient particle</span>
<span><span class="sw-circle" style="background:var(--C)"></span>average</span>
<span><span class="sw-circle" style="background:var(--B)"></span>precarious</span>
<span style="margin-left: 12px; color: var(--ink-faint); font-style: italic;">click + drag to rotate · scroll to zoom · right-click + drag to pan</span>
</div>
</div>
<div class="patterns2-right">
<div class="pat2-section">
<div class="pat2-section-header">
<h3>Top chains</h3>
<select id="chainSort2">
<option value="count" selected>by count</option>
<option value="cost">by total £</option>
<option value="cost_per">by £/event</option>
</select>
</div>
<div class="pat2-controls">
<select id="chainLen2">
<option value="2">2-step</option>
<option value="3" selected>3-step</option>
<option value="4">4-step</option>
</select>
<select id="chainAntFilter2"><option value="">All triggers</option></select>
</div>
<div class="pat2-list" id="chainList2"></div>
</div>
<div class="pat2-section">
<div class="pat2-section-header">
<h3>Recurrence loops</h3>
<select id="loopSort2">
<option value="activations" selected>by activations</option>
<option value="cost">by total £</option>
</select>
</div>
<div class="pat2-list" id="loopList2"></div>
</div>
<div class="pat2-details" id="pat2Details">
<div style="color: var(--ink-faint); padding: 20px; text-align: center; font-style: italic;">Pattern details appear here when you select a chain or loop.</div>
</div>
</div>
</div>
<!-- So-what panel: dynamic interpretation populated by pat2RenderSoWhat() -->
<div class="pat-sowhat" id="patSoWhat">
<div class="sowhat-eyebrow">So what?</div>
<h3 id="patSoWhatHeadline">The top chains and loops are where intervention pays back hardest.</h3>
<p id="patSoWhatBody">The chains list is sorted by total cost contribution. The top 3 chains typically account for around <strong>30–40% of all cascade cost</strong> in a 36-month simulation. <span class="money-callout">Disrupting a single high-frequency chain — even with a low success rate — usually saves more than running a high-success intervention on a rare pathway.</span></p>
<p id="patSoWhatActionable">Loops are the recurrence engines: each repetition charges the cost again. A loop with average traversal frequency of 2.5 and a cost of £8,000 per cycle represents <strong>£20,000 of avoidable cost per person stuck in it</strong>. Look at which loops connect to your intervention targets on page 5.</p>
</div>
<!-- Journey wayfinding CTA: directs user from patterns page to interventions page -->
<div class="page-cta-row">
<div class="page-cta-text">
<div class="cta-eyebrow">Next step</div>
<div class="cta-headline">Now plan interventions against these patterns</div>
<div class="cta-body">Activate evidence-based interventions and see how they bend the cost curve. The cost-effectiveness frontier shows which combinations pay back inside 36 months for your authority's cohort.</div>
</div>
<button class="page-cta-btn" id="goIntervBtn">
Plan interventions
<span class="arrow"></span>
</button>
</div>
</div><!-- /page-patterns -->
<div class="tab-page" id="page-interv">
<div class="page-head">
<h2>Plan interventions</h2>
<p class="sub">Activate interventions from the library on the right. Each one reduces an antecedent rate (primary prevention) or disrupts a specific cascade edge (tertiary). The frontier below shows you which combinations are net-positive for your authority — anything above the dashed diagonal pays back inside 36 months.</p>
</div>
<div class="interv-coverage-banner">
<div class="badge">v0.12 CALIBRATED MODEL</div>
<div class="body">
The cascade graph includes <strong>59 antecedent-to-antecedent edges</strong> (compounding shock, economic-stress, health, crime/housing, and substance cascades) plus <strong>10 service-to-antecedent feedback edges</strong> (TA → MH crisis, MHA detention → job loss, etc.). Primary-prevention interventions like Carer's Respite, IAPT, Citizens Advice debt support, and Through-the-Gate now credit the <strong>full downstream compounding cascade</strong> — not just the immediate service avoidance.
<br><br>
Receiving-antecedent baselines have been <strong>down-calibrated</strong> so simulated firing rates converge with empirical UK population figures. Most rates now land within <span class="bracket">±15%</span> of ONS / PHE / DWP baselines. The acute MH crisis rate is the one residual: cascade contribution still exceeds the empirical baseline somewhat, indicating either slightly hot attributable-risk values or — more likely — that the published incidence figure under-counts cascading crises. Treat MH-driven savings as a directional indicator rather than a precise figure.
</div>
</div>
<div class="frontier-panel" id="frontierPanel">
<div class="frontier-hdr">
<h2>Cost-effectiveness frontier</h2>
<div class="legend">
<span class="item"><span class="sw-circle" style="background:var(--money);opacity:0.4"></span>Available intervention (greyed = not active)</span>
<span class="item"><span class="sw-circle" style="background:var(--money)"></span>Active primary prevention</span>
<span class="item"><span class="sw-circle" style="background:#a78bfa"></span>Active cascade disruption</span>
<span class="item"><span class="sw-line" style="background:repeating-linear-gradient(to right,var(--money) 0,var(--money) 6px,transparent 6px,transparent 11px);height:2px;"></span>Break-even line (savings = cost)</span>
<span class="item"><span class="sw-circle" style="background:var(--A)"></span>Your current portfolio</span>
</div>
</div>
<div class="frontier-desc">Each grey dot is an available intervention plotted at its (cost, savings) position when deployed alone at typical LA-scale. <strong>Click any dot to activate it</strong> — the dot lights up in colour and the blue "Your portfolio" marker moves to reflect your new combined spend and savings. <strong>The dashed green diagonal is the break-even line</strong>: any intervention or portfolio above it returns more in downstream savings than it costs to deliver. The vertical barometer on the right shows what percentage of the theoretical maximum savings (across the whole library) your active portfolio is capturing.</div>
<div id="frontierContainer">
<div class="frontier-loading" id="frontierLoading">
<div>Computing single-intervention scenarios across the library…</div>
<div class="frontier-progress"><div class="frontier-progress-fill" id="frontierProgressFill"></div></div>
<div id="frontierProgressMsg" style="font-size:11px;">starting</div>
</div>
<div class="frontier-chart-area" id="frontierChartArea" style="display:none;">
<svg class="frontier-svg" id="frontierSvg"></svg>
<div class="barometer-wrap">
<div class="barometer-hdr">Portfolio<br>barometer</div>
<svg class="barometer-svg" id="barometerSvg" viewBox="0 0 170 360"></svg>
<div class="barometer-footer" id="barometerFooter">Activate interventions to see how close you can get to the <strong>maximum-savings ceiling</strong>.</div>
</div>
</div>
</div>
</div>
<div class="interv-grid">
<div class="interv-main">
<h2>Prevention intervention library</h2>
<div class="desc">Twenty-seven UK evidence-based prevention interventions. Activate any combination, set how many cases per year you intend to treat, and watch the projected effect on this LA's 36-month cascade cost. Each intervention has a real published cost-per-case and a published success rate; sources are cited on every card.</div>
<div class="lib-meta">
<span><strong id="libCount">20</strong> interventions in library</span>
<span class="ev-key"><span class="ev-badge ev-H" style="display:inline-block">H</span> RCT or large-scale evaluation</span>
<span class="ev-key"><span class="ev-badge ev-M" style="display:inline-block">M</span> Strong observational / SROI</span>
<span class="ev-key"><span class="ev-badge ev-L" style="display:inline-block">L</span> Limited evidence</span>
</div>
<div class="interv-controls">
<span class="lbl">Sort</span>
<select id="intervSort">
<option value="impact" selected>by potential impact (target antecedent £)</option>
<option value="evidence">by evidence grade</option>
<option value="cost">by £/case (cheapest first)</option>
<option value="targets">by target antecedent</option>
</select>
<span class="lbl">Filter</span>
<select id="intervFilter">
<option value="">All evidence grades</option>
<option value="H">High evidence only</option>
<option value="M">High + moderate</option>
</select>
<button id="intervReset">⟲ Deactivate all</button>
</div>
<div class="slider-list" id="interventionList"></div>
</div>
<div class="interv-side">
<div class="savings-hero">
<h3>Projected 36-month savings (selected LA)</h3>
<div class="savings" id="savingsBig">£0</div>
<div class="label" id="savingsLbl">Move any slider below 100% to start modelling</div>
<div class="breakdown">
<span class="l">Baseline public £</span><span class="v money" id="baselinePub">£0</span>
<span class="l">After intervention</span><span class="v money" id="afterPub">£0</span>
<span class="l">Baseline LA £</span><span class="v la" id="baselineLa">£0</span>
<span class="l">After intervention</span><span class="v la" id="afterLa">£0</span>
</div>
</div>
<div class="roi-card">
<h3>Invest-to-save calculator</h3>
<div class="row"><label>Intervention cost over 36 months (£, auto from active library items)</label><input type="number" id="intervCost" value="0" step="1" min="0"></div>
<div class="roi-grid">
<div class="roi-cell payback"><div class="l">Payback period</div><div class="v" id="paybackMo"></div></div>
<div class="roi-cell roi"><div class="l">36-month ROI</div><div class="v" id="roiPct"></div></div>
</div>
<div style="color: var(--ink-faint); font-size: 10px; margin-top: 10px; line-height: 1.4;">Payback = months for savings to equal intervention cost (assumes savings accrue linearly). ROI = (savings − cost) / cost over 36 months. Public-sector basis.</div>
</div>
<div class="svc-impact">
<h3>Service portfolio impact</h3>
<div id="svcImpactList"><div style="color: var(--ink-faint); font-size: 11px; padding: 12px; text-align: center;">Adjust sliders to see per-service impact</div></div>
</div>
</div>
</div>
</div><!-- /page-interv -->
<!-- ============================ Page 6 — Take it to the CEO (exec plan) ============================ -->
<div class="tab-page" id="page-plan">
<div class="page-head">
<h2>Take it to the CEO</h2>
<p class="sub">An actionable plan for the interventions you've selected. Set each one's start month, see how spend phases month-by-month against the lagged benefit accrual, and produce a printable A4 exec summary your CEO can take to a cabinet meeting.</p>
</div>
<!-- Empty state shown when no interventions are active -->
<div class="plan-empty" id="planEmpty" style="display:none">
<div class="plan-empty-ico"></div>
<h3>No interventions activated yet</h3>
<p>Go back to <strong>Plan interventions</strong>, toggle on the ones you want to take forward, then return here to phase them into a cashflow plan.</p>
<button class="hero-secondary" id="planEmptyBackBtn" style="margin-top:18px">← Back to Plan interventions</button>
</div>
<!-- The plan itself (hidden when no interventions active) -->
<div class="plan-body" id="planBody">
<!-- KPI strip — headline numbers -->
<div class="plan-kpi-row">
<div class="plan-kpi-card">
<div class="l">LA-only net position at M36</div>
<div class="v" id="kpiLaNet"></div>
<div class="extra" id="kpiLaNetExtra">&nbsp;</div>
</div>
<div class="plan-kpi-card">
<div class="l">Public-sector net position at M36</div>
<div class="v" id="kpiPubNet"></div>
<div class="extra" id="kpiPubNetExtra">&nbsp;</div>
</div>
<div class="plan-kpi-card">
<div class="l">Payback month (LA)</div>
<div class="v" id="kpiPayback"></div>
<div class="extra" id="kpiPaybackExtra">Cumulative LA cashflow crosses £0</div>
</div>
<div class="plan-kpi-card">
<div class="l">Total LA spend over 36 months</div>
<div class="v" id="kpiLaSpend"></div>
<div class="extra" id="kpiLaSpendExtra">&nbsp;</div>
</div>
</div>
<!-- Two-column layout: phasing controls on left, cashflow charts on right -->
<div class="plan-grid">
<div class="plan-left">
<div class="plan-section-header">
<h3>Phase each intervention</h3>
<p>Pick the start month for each active intervention. Spend begins from that month; benefits start accruing after the cascade-lag delay.</p>
</div>
<div id="planInterventionList"><!-- populated by JS --></div>
<div class="plan-actions">
<button class="hero-cta" id="planPrintBtn">
Print exec summary (⌘P)
<span class="arrow"></span>
</button>
<button class="hero-secondary" id="planReplanBtn">↻ Re-plan from interventions</button>
</div>
</div>
<div class="plan-right">
<div class="plan-section-header">
<h3>Cashflow over 36 months</h3>
<p>Red bars = monthly spend, green bars = monthly benefit. The yellow line tracks cumulative net cashflow — when it crosses zero, the plan pays back.</p>
</div>
<div class="plan-chart-wrap">
<div class="plan-chart-title">LA-only cashflow</div>
<svg id="cashflowSvgLa" class="cashflow-svg"></svg>
</div>
<div class="plan-chart-wrap">
<div class="plan-chart-title">Public-sector cashflow <span class="title-meta">(LA + NHS + DWP + MoJ)</span></div>
<svg id="cashflowSvgPub" class="cashflow-svg"></svg>
</div>
</div>
</div>
<!-- Hidden printable view rendered into here on print mode -->
<div class="plan-exec-print" id="planExecPrint" aria-hidden="true"></div>
</div>
</div><!-- /page-plan -->
<div class="exec-print" id="execPrintView"></div>
<footer>
<strong>Person-centric.</strong> ~100k people sampled; each represents ~10 real adults at the 1M scale. Each person carries a single HSV throughout the 36 months, used to condition both antecedent occurrence rates (HSV-conditioned Poisson) and cascade edge probabilities. Within-person cascades compound. <strong>Duration-aware.</strong> Year-type nodes (TA, care home, CHC, care package, continuing treatment) cost by months-in-state — explicit exit nodes close intervals early (l_0039 tenancy sustained closes TA; l_0035 substance completion closes long-stay treatment; etc.); 12-month rolling cap otherwise. HSV moderators for both edges and antecedents follow STRENGTH_TO_COEF = {weak: 0.10, moderate: 0.30, strong: 0.70} in log-odds (edges) or log-rate (antecedents). Costs SPPI-uplifted to 2025/26 (unit_costs_v0.3). The placeholder HSV distribution (15/70/15 resilient/average/precarious mix) will be replaced by Matt's realistic HSV population when supplied.
</footer>
</div>
<script>
const PAYLOAD = {"nodes":[{"id":"a_0001","n":"Landlord-ended private rented assured shorthold tenancy","p":"antecedent"},{"id":"a_0002","n":"Family or friends asking person to leave accommodation","p":"antecedent"},{"id":"a_0003","n":"Domestic abuse incident requiring relocation","p":"antecedent"},{"id":"a_0004","n":"Discharge from custody","p":"antecedent"},{"id":"a_0005","n":"Discharge from psychiatric inpatient stay without secured accommodation","p":"antecedent"},{"id":"a_0006","n":"Non-violent relationship breakdown with partner","p":"antecedent"},{"id":"a_0007","n":"Job loss or redundancy","p":"antecedent"},{"id":"a_0008","n":"Benefit sanction","p":"antecedent"},{"id":"a_0009","n":"Onset of chronic illness or disability","p":"antecedent"},{"id":"a_0010","n":"Loss of informal carer","p":"antecedent"},{"id":"a_0012","n":"Bereavement of spouse or long-term partner","p":"antecedent"},{"id":"a_0013","n":"Acute mental health crisis episode","p":"antecedent"},{"id":"a_0014","n":"Suicide attempt or serious self-harm","p":"antecedent"},{"id":"a_0015","n":"Home crisis displacement (fire, flood, structural unsafe)","p":"antecedent"},{"id":"a_0016","n":"Asylum support accommodation ending (move-on period)","p":"antecedent"},{"id":"a_0017","n":"Mortgage repossession","p":"antecedent"},{"id":"a_0018","n":"Utility or essential bill crisis (energy/water disconnection or arrears at enforcement)","p":"antecedent"},{"id":"a_0019","n":"Discharge from UK armed forces (veteran transition to civilian life)","p":"antecedent"},{"id":"a_0020","n":"Workplace accident or injury with disability onset","p":"antecedent"},{"id":"a_0021","n":"Care leaver transition (exit from looked-after-children status at 18+)","p":"antecedent"},{"id":"a_0022","n":"Long-term sickness benefit decision adverse to claimant (ESA/PIP/UC LCWRA refusal or roll-off)","p":"antecedent"},{"id":"a_0023","n":"Pregnancy or postnatal period (perinatal window)","p":"antecedent"},{"id":"a_0024","n":"Recent immigration to UK (within 5 years)","p":"antecedent"},{"id":"a_0025","n":"Onset of caring responsibility (becoming an informal carer)","p":"antecedent"},{"id":"a_0026","n":"Diagnosis of severe / progressive chronic condition (dementia, MND, MS, advanced cancer, organ failure)","p":"antecedent"},{"id":"a_0027","n":"Pre-eviction notice received (S21, S8, or rent arrears stage 2)","p":"antecedent"},{"id":"a_0028","n":"Becoming a victim of crime (serious \u2014 violent, sexual, fraud, hate)","p":"antecedent"},{"id":"a_0029","n":"Cost-of-living income shock (sustained real-income reduction)","p":"antecedent"},{"id":"a_0030","n":"Discharge from immigration detention","p":"antecedent"},{"id":"a_0031","n":"Forced relocation by external factor (work, family, area)","p":"antecedent"},{"id":"a_0032","n":"Loss of council tax discount or administrative benefit change","p":"antecedent"},{"id":"a_0033","n":"Onset of severe / disabling chronic pain","p":"antecedent"},{"id":"a_0034","n":"Identified as DA perpetrator (by police, court, or specialist service)","p":"antecedent"},{"id":"a_0035","n":"Retirement transition (especially early or forced retirement)","p":"antecedent"},{"id":"a_0036","n":"Onset of problem gambling","p":"antecedent"},{"id":"a_0037","n":"Acquired brain injury (TBI, stroke-related cognitive impact, hypoxia)","p":"antecedent"},{"id":"a_0038","n":"Eviction or asked to leave supported housing / hostel","p":"antecedent"},{"id":"a_0039","n":"Long-term unemployment (>12 months out of work)","p":"antecedent"},{"id":"a_0040","n":"Sexual exploitation or trafficking (NRM referral)","p":"antecedent"},{"id":"a_0041","n":"Frequent A&E attender flagged (5+ visits in 12 months)","p":"antecedent"},{"id":"a_0042","n":"Onset of sustained food insecurity (food bank reliance, missing meals)","p":"antecedent"},{"id":"l_0001","n":"Homelessness duty acceptance","p":"housing"},{"id":"l_0002","n":"Rough sleeper outreach contact","p":"housing"},{"id":"l_0003","n":"Council tax liability order issued","p":"housing"},{"id":"l_0004","n":"Discretionary Housing Payment award","p":"housing"},{"id":"l_0005","n":"Care Act s9 needs assessment initiated","p":"adult_social_care"},{"id":"l_0006","n":"Care Act s42 safeguarding adult enquiry","p":"adult_social_care"},{"id":"l_0007","n":"Hospital discharge with care package","p":"adult_social_care"},{"id":"l_0008","n":"AMHP / Mental Health Act assessment","p":"mental_health"},{"id":"l_0009","n":"Section 136 detention","p":"welfare"},{"id":"l_0010","n":"Section 117 aftercare planning","p":"mental_health"},{"id":"l_0011","n":"MARAC referral","p":"adult_social_care"},{"id":"l_0012","n":"Refuge placement","p":"adult_social_care"},{"id":"l_0013","n":"Local Welfare Assistance award","p":"adult_social_care"},{"id":"l_0014","n":"Supporting Families programme engagement","p":"adult_social_care"},{"id":"l_0015","n":"Anti-Social Behaviour civil injunction","p":"housing"},{"id":"l_0016","n":"Community Protection Notice issued","p":"housing"},{"id":"l_0017","n":"Substance misuse service referral","p":"substance"},{"id":"l_0018","n":"Public health funeral arranged","p":"adult_social_care"},{"id":"l_0019","n":"Social housing allocation","p":"housing"},{"id":"l_0021","n":"Temporary accommodation placement","p":"housing"},{"id":"l_0025","n":"Care home admission (LA-funded)","p":"adult_social_care"},{"id":"l_0027","n":"IAPT / NHS Talking Therapies referral","p":"mental_health"},{"id":"l_0028","n":"Carers' assessment (Care Act s10)","p":"domestic_abuse"},{"id":"l_0029","n":"Reablement service intake","p":"domestic_abuse"},{"id":"l_0030","n":"NHS Continuing Healthcare placement (jointly funded)","p":"adult_social_care"},{"id":"l_0034","n":"Homelessness re-presentation within 24 months","p":"housing"},{"id":"l_0035","n":"Substance treatment successful completion","p":"substance"},{"id":"l_0036","n":"Substance treatment dropout / non-completion","p":"substance"},{"id":"l_0037","n":"IAPT / Talking Therapies recovery","p":"mental_health"},{"id":"l_0038","n":"IAPT non-recovery or dropout","p":"mental_health"},{"id":"l_0039","n":"Tenancy sustained 12 months","p":"housing"},{"id":"l_0040","n":"Repeat MARAC referral within 12 months","p":"adult_social_care"},{"id":"l_0041","n":"Reablement successful completion (return to independence)","p":"adult_social_care"},{"id":"l_0042","n":"Care package commenced post-s9 assessment","p":"adult_social_care"},{"id":"l_0043","n":"MARAC case closed with risk reduced","p":"adult_social_care"},{"id":"l_0044","n":"MHA assessment outcome \u2014 released without detention","p":"mental_health"},{"id":"l_0045","n":"MHA detention readmission within 12 months","p":"mental_health"},{"id":"l_0046","n":"Long-term temporary accommodation placement (>12 months)","p":"housing"},{"id":"l_0047","n":"Substance abstinence sustained 12 months","p":"substance"},{"id":"l_0048","n":"Care home death within 12 months of admission","p":"domestic_abuse"},{"id":"l_0049","n":"ASB injunction breach","p":"housing"},{"id":"l_0050","n":"Care package review identifying escalation of need","p":"adult_social_care"},{"id":"l_0051","n":"DHP renewed within 12 months (continuing housing assistance)","p":"adult_social_care"},{"id":"l_0052","n":"Supporting Families programme completion (positive outcome)","p":"adult_social_care"},{"id":"l_0053","n":"Carer support package commenced","p":"adult_social_care"},{"id":"l_0054","n":"IAPT recovery sustained 12 months","p":"adult_social_care"},{"id":"l_0055","n":"Suicide death","p":"mental_health"},{"id":"l_0056","n":"Drug-related death (accidental or intentional overdose)","p":"substance"},{"id":"l_0057","n":"Continuing in substance treatment (long-stay)","p":"substance"},{"id":"l_0058","n":"Continuing in IAPT / Talking Therapies (extended course)","p":"mental_health"},{"id":"l_0059","n":"Care package continuing (sustained service receipt)","p":"mental_health"},{"id":"a_0043","n":"Onset of substance use disorder (alcohol/drug dependence)","p":"antecedent"}],"edges":[{"id":"le_0000001","a":"a_0001","b":"l_0001","p":0.17,"lw":12,"lm":2.0,"lp25":0.5,"lp75":4.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"credit_access_score","d":"decreases_p","st":"moderate"},{"s":"geographic_anchor_strength","d":"increases_p","st":"weak"}],"d":[{"s":"has_dependent_children","c":"true","e":1.4},{"s":"equivalised_household_income_decile","c":"<=3","e":2.5},{"s":"area_deprivation_decile","c":"<=3","e":1.8}],"fwd":true},{"id":"le_0000002","a":"a_0002","b":"l_0001","p":0.25,"lw":6,"lm":1.0,"lp25":0.2,"lp75":2.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"family_closeness","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"}],"d":[{"s":"age_band","c":"16-24","e":1.6},{"s":"has_dependent_children","c":"true","e":1.3}],"fwd":true},{"id":"le_0000003","a":"a_0003","b":"l_0001","p":0.08,"lw":12,"lm":1.5,"lp25":0.1,"lp75":4.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"weak"}],"d":[{"s":"has_dependent_children","c":"true","e":1.8},{"s":"sex_at_birth","c":"female","e":1.5},{"s":"legal_status_constraint","c":"non-UK","e":0.6}],"fwd":true},{"id":"le_0000004","a":"a_0004","b":"l_0001","p":0.12,"lw":12,"lm":1.0,"lp25":0.0,"lp75":3.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"impulsivity_control","d":"decreases_p","st":"moderate"},{"s":"language_proficiency_score","d":"decreases_p","st":"weak"}],"d":[{"s":"age_band","c":"40-49 or 50-59","e":1.5},{"s":"sex_at_birth","c":"male","e":1.0},{"s":"has_dependent_children","c":"true","e":1.3},{"s":"legal_status_constraint","c":"non-UK","e":0.5}],"fwd":true},{"id":"le_0000005","a":"a_0005","b":"l_0001","p":0.35,"lw":12,"lm":1.5,"lp25":0.0,"lp75":4.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"},{"s":"housing_stability_index","d":"decreases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"moderate"},{"s":"trust_propensity","d":"decreases_p","st":"weak"}],"d":[{"s":"long_term_health_condition","c":"true","e":1.4},{"s":"has_dependent_children","c":"true","e":1.6},{"s":"economic_activity_status","c":"unemployed or inactive","e":1.5}],"fwd":true},{"id":"le_0000006","a":"a_0006","b":"l_0001","p":0.025,"lw":18,"lm":4.0,"lp25":1.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"housing_stability_index","d":"increases_p","st":"moderate"},{"s":"credit_access_score","d":"decreases_p","st":"moderate"},{"s":"geographic_anchor_strength","d":"increases_p","st":"weak"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"}],"d":[{"s":"sex_at_birth","c":"female","e":1.6},{"s":"has_dependent_children","c":"true","e":1.8},{"s":"housing_tenure","c":"social rent or private rent","e":1.7},{"s":"equivalised_household_income_decile","c":"<=4","e":2.0}],"fwd":true},{"id":"le_0000007","a":"a_0002","b":"l_0002","p":0.04,"lw":12,"lm":2.0,"lp25":0.5,"lp75":6.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"moderate"},{"s":"urban_preference","d":"increases_p","st":"weak"}],"d":[{"s":"sex_at_birth","c":"male","e":1.8},{"s":"age_band","c":"16-24","e":0.8},{"s":"legal_status_constraint","c":"no recourse to public funds","e":3.0}],"fwd":true},{"id":"le_0000008","a":"a_0004","b":"l_0002","p":0.03,"lw":12,"lm":0.5,"lp25":0.0,"lp75":2.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"impulsivity_control","d":"decreases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"moderate"}],"d":[{"s":"sex_at_birth","c":"male","e":1.0},{"s":"age_band","c":"40-59","e":1.5},{"s":"occupation_broad","c":"elementary/unemployed pre-imprisonment","e":1.8}],"fwd":true},{"id":"le_0000009","a":"a_0006","b":"l_0002","p":0.005,"lw":18,"lm":5.0,"lp25":1.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"employment_security_level","d":"decreases_p","st":"moderate"}],"d":[{"s":"sex_at_birth","c":"male","e":2.5},{"s":"has_dependent_children","c":"false","e":1.8},{"s":"age_band","c":"35-54","e":1.4}],"fwd":true},{"id":"le_0000010","a":"a_0007","b":"l_0003","p":0.08,"lw":12,"lm":5.0,"lp25":3.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"emergency_fund_robustness","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"budgeting_formality","d":"decreases_p","st":"moderate"},{"s":"professional_development_investment","d":"decreases_p","st":"moderate"},{"s":"institutional_trust","d":"decreases_p","st":"weak"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"}],"d":[{"s":"equivalised_household_income_decile","c":"<=3","e":2.5},{"s":"housing_tenure","c":"social rent or private rent","e":1.4},{"s":"household_size","c":"single adult","e":0.7},{"s":"age_band","c":"50+","e":1.4}],"fwd":true},{"id":"le_0000011","a":"a_0006","b":"l_0003","p":0.06,"lw":18,"lm":7.0,"lp25":3.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"emergency_fund_robustness","d":"decreases_p","st":"strong"},{"s":"budgeting_formality","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"salary_negotiation_assertiveness","d":"decreases_p","st":"weak"}],"d":[{"s":"sex_at_birth","c":"female","e":1.8},{"s":"has_dependent_children","c":"true","e":1.5},{"s":"equivalised_household_income_decile","c":"<=5","e":2.0}],"fwd":true},{"id":"le_0000012","a":"a_0008","b":"l_0003","p":0.2,"lw":12,"lm":4.0,"lp25":2.0,"lp75":7.0,"ls":"lognormal","h":[{"s":"emergency_fund_robustness","d":"decreases_p","st":"moderate"},{"s":"borrowing_from_friends_family_likelihood","d":"decreases_p","st":"moderate"},{"s":"institutional_trust","d":"decreases_p","st":"moderate"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"digital_access_level","d":"decreases_p","st":"moderate"}],"d":[{"s":"has_dependent_children","c":"true","e":1.6},{"s":"long_term_health_condition","c":"true","e":1.5},{"s":"equivalised_household_income_decile","c":"<=2","e":1.8}],"fwd":true},{"id":"le_0000013","a":"l_0003","b":"l_0001","p":0.04,"lw":18,"lm":8.0,"lp25":4.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"strong"},{"s":"emergency_fund_robustness","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"credit_access_score","d":"decreases_p","st":"moderate"}],"d":[{"s":"housing_tenure","c":"private rent","e":2.0},{"s":"housing_tenure","c":"social rent","e":1.4},{"s":"housing_tenure","c":"owner occupied with mortgage","e":0.3},{"s":"has_dependent_children","c":"true","e":1.4},{"s":"equivalised_household_income_decile","c":"<=3","e":2.0}],"fwd":false},{"id":"le_0000014","a":"a_0003","b":"l_0011","p":0.3,"lw":6,"lm":1.0,"lp25":0.2,"lp75":3.0,"ls":"lognormal","h":[{"s":"trust_propensity","d":"increases_p","st":"moderate"},{"s":"mental_health_service_use","d":"increases_p","st":"weak"},{"s":"social_support_capacity","d":"increases_p","st":"moderate"},{"s":"language_proficiency_score","d":"increases_p","st":"moderate"}],"d":[{"s":"sex_at_birth","c":"female","e":1.0},{"s":"has_dependent_children","c":"true","e":1.4},{"s":"legal_status_constraint","c":"no recourse to public funds","e":0.7}],"fwd":true},{"id":"le_0000015","a":"a_0003","b":"l_0012","p":0.035,"lw":6,"lm":0.5,"lp25":0.1,"lp75":1.5,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"geographic_anchor_strength","d":"decreases_p","st":"moderate"},{"s":"language_proficiency_score","d":"decreases_p","st":"moderate"}],"d":[{"s":"has_dependent_children","c":"true","e":1.8},{"s":"sex_at_birth","c":"female","e":1.0},{"s":"legal_status_constraint","c":"no recourse to public funds","e":0.4}],"fwd":true},{"id":"le_0000016","a":"l_0011","b":"l_0012","p":0.08,"lw":6,"lm":1.5,"lp25":0.2,"lp75":3.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"},{"s":"geographic_anchor_strength","d":"decreases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"weak"}],"d":[{"s":"has_dependent_children","c":"true","e":1.6},{"s":"legal_status_constraint","c":"no recourse to public funds","e":0.3}],"fwd":false},{"id":"le_0000017","a":"a_0004","b":"l_0017","p":0.22,"lw":12,"lm":1.0,"lp25":0.0,"lp75":4.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"self_directedness","d":"decreases_p","st":"moderate"},{"s":"institutional_trust","d":"increases_p","st":"weak"}],"d":[{"s":"sex_at_birth","c":"male","e":1.0},{"s":"age_band","c":"25-49","e":1.3}],"fwd":true},{"id":"le_0000018","a":"a_0006","b":"l_0017","p":0.008,"lw":24,"lm":8.0,"lp25":3.0,"lp75":16.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"impulsivity_control","d":"decreases_p","st":"moderate"},{"s":"alcohol_freq","d":"increases_p","st":"moderate"}],"d":[{"s":"sex_at_birth","c":"male","e":2.0},{"s":"age_band","c":"30-54","e":1.4}],"fwd":true},{"id":"le_0000019","a":"l_0017","b":"l_0006","p":0.06,"lw":12,"lm":4.0,"lp25":1.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"}],"d":[{"s":"age_band","c":"50+","e":2.0},{"s":"has_dependent_children","c":"true","e":1.5}],"fwd":false},{"id":"le_0000020","a":"a_0009","b":"l_0005","p":0.2,"lw":12,"lm":3.0,"lp25":1.0,"lp75":7.0,"ls":"lognormal","h":[{"s":"self_directedness","d":"decreases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"digital_access_level","d":"decreases_p","st":"weak"},{"s":"trust_propensity","d":"increases_p","st":"weak"}],"d":[{"s":"age_band","c":"65+","e":2.0},{"s":"equivalised_household_income_decile","c":"<=4","e":1.8},{"s":"lives_with_partner","c":"false","e":1.5}],"fwd":true},{"id":"le_0000021","a":"a_0010","b":"l_0005","p":0.55,"lw":6,"lm":1.5,"lp25":0.2,"lp75":3.5,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"strong"}],"d":[{"s":"age_band","c":"75+","e":1.8},{"s":"long_term_health_condition","c":"true","e":1.6},{"s":"lives_with_partner","c":"false (post-event)","e":1.5}],"fwd":true},{"id":"le_0000022","a":"a_0009","b":"l_0007","p":0.3,"lw":12,"lm":2.5,"lp25":0.5,"lp75":6.0,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"moderate"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"65+","e":2.5},{"s":"lives_with_partner","c":"false","e":1.6},{"s":"dwelling_type","c":"Flat or apartment / Other","e":1.3}],"fwd":true},{"id":"le_0000023","a":"a_0013","b":"l_0008","p":0.18,"lw":3,"lm":0.1,"lp25":0.0,"lp75":0.5,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"trust_propensity","d":"decreases_p","st":"moderate"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"}],"d":[{"s":"age_band","c":"18-34","e":1.7},{"s":"sex_at_birth","c":"male","e":1.13},{"s":"ethnic_cultural_group","c":"Black","e":3.5}],"fwd":true},{"id":"le_0000024","a":"a_0013","b":"l_0009","p":0.05,"lw":1,"lm":0.0,"lp25":0.0,"lp75":0.1,"ls":"lognormal","h":[{"s":"urban_preference","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"18-34","e":1.7},{"s":"sex_at_birth","c":"male","e":1.5},{"s":"ethnic_cultural_group","c":"Black","e":4.0}],"fwd":true},{"id":"le_0000025","a":"a_0014","b":"l_0008","p":0.5,"lw":1,"lm":0.05,"lp25":0.0,"lp75":0.2,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"weak"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"substance_risk","d":"increases_p","st":"moderate"}],"d":[{"s":"age_band","c":"16-24","e":1.3},{"s":"sex_at_birth","c":"male","e":1.4},{"s":"long_term_health_condition","c":"true","e":1.3}],"fwd":true},{"id":"le_0000026","a":"a_0012","b":"l_0008","p":0.008,"lw":24,"lm":6.0,"lp25":2.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"neuroticism","d":"increases_p","st":"moderate"},{"s":"harm_avoidance","d":"decreases_p","st":"weak"}],"d":[{"s":"age_band","c":"65+","e":0.6},{"s":"lives_with_partner","c":"false (post-event)","e":1.0},{"s":"household_size","c":"single adult post-event","e":1.4}],"fwd":true},{"id":"le_0000027","a":"l_0009","b":"l_0008","p":0.92,"lw":1,"lm":0.02,"lp25":0.0,"lp75":0.05,"ls":"lognormal","h":[{"s":"trust_propensity","d":"decreases_p","st":"weak"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[{"s":"age_band","c":"16-17","e":1.0}],"fwd":false},{"id":"le_0000028","a":"l_0008","b":"l_0010","p":0.13,"lw":12,"lm":1.5,"lp25":0.5,"lp75":4.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[{"s":"long_term_health_condition","c":"true","e":1.5},{"s":"ethnic_cultural_group","c":"Black","e":2.0}],"fwd":true},{"id":"le_0000029","a":"a_0008","b":"l_0004","p":0.06,"lw":6,"lm":2.0,"lp25":1.0,"lp75":4.0,"ls":"lognormal","h":[{"s":"institutional_trust","d":"increases_p","st":"moderate"},{"s":"cognitive_processing_capacity","d":"increases_p","st":"moderate"},{"s":"digital_access_level","d":"increases_p","st":"moderate"},{"s":"housing_stability_index","d":"decreases_p","st":"weak"}],"d":[{"s":"has_dependent_children","c":"true","e":1.5},{"s":"housing_tenure","c":"private rent","e":1.7},{"s":"long_term_health_condition","c":"true","e":1.3}],"fwd":true},{"id":"le_0000030","a":"a_0007","b":"l_0013","p":0.03,"lw":6,"lm":1.5,"lp25":0.5,"lp75":3.5,"ls":"lognormal","h":[{"s":"emergency_fund_robustness","d":"decreases_p","st":"strong"},{"s":"borrowing_from_friends_family_likelihood","d":"decreases_p","st":"moderate"},{"s":"institutional_trust","d":"increases_p","st":"moderate"},{"s":"cognitive_processing_capacity","d":"increases_p","st":"weak"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"}],"d":[{"s":"has_dependent_children","c":"true","e":1.6},{"s":"equivalised_household_income_decile","c":"<=2","e":2.5}],"fwd":true},{"id":"le_0000031","a":"a_0015","b":"l_0013","p":0.35,"lw":3,"lm":0.2,"lp25":0.0,"lp75":1.0,"ls":"lognormal","h":[{"s":"insurance_coverage_breadth","d":"decreases_p","st":"strong"},{"s":"emergency_fund_robustness","d":"decreases_p","st":"moderate"},{"s":"borrowing_from_friends_family_likelihood","d":"decreases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[{"s":"has_dependent_children","c":"true","e":1.5},{"s":"housing_tenure","c":"social rent or private rent","e":1.7}],"fwd":true},{"id":"le_0000032","a":"a_0013","b":"l_0016","p":0.005,"lw":6,"lm":1.5,"lp25":0.5,"lp75":3.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"18-34","e":1.5},{"s":"sex_at_birth","c":"male","e":1.8},{"s":"housing_tenure","c":"social rent","e":2.0}],"fwd":true},{"id":"le_0000033","a":"l_0017","b":"l_0016","p":0.015,"lw":12,"lm":4.0,"lp25":2.0,"lp75":8.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"impulsivity_control","d":"decreases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"weak"}],"d":[{"s":"housing_tenure","c":"social rent or temporary accommodation","e":2.5},{"s":"sex_at_birth","c":"male","e":1.8}],"fwd":false},{"id":"le_0000034","a":"a_0012","b":"l_0018","p":0.002,"lw":2,"lm":0.5,"lp25":0.1,"lp75":1.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"family_closeness","d":"decreases_p","st":"moderate"},{"s":"geographic_anchor_strength","d":"decreases_p","st":"weak"}],"d":[{"s":"age_band","c":"75+","e":1.5},{"s":"lives_with_partner","c":"false (post-event)","e":1.0},{"s":"ethnic_cultural_group","c":"White","e":1.3}],"fwd":true},{"id":"le_0000035","a":"a_0006","b":"l_0014","p":0.015,"lw":18,"lm":6.0,"lp25":2.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"parental_self_efficacy","d":"decreases_p","st":"moderate"}],"d":[{"s":"has_dependent_children","c":"true","e":null},{"s":"equivalised_household_income_decile","c":"<=3","e":2.0},{"s":"housing_tenure","c":"social rent","e":1.6}],"fwd":true},{"id":"le_0000036","a":"l_0017","b":"l_0015","p":0.003,"lw":18,"lm":6.0,"lp25":3.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"impulsivity_control","d":"decreases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"moderate"}],"d":[{"s":"sex_at_birth","c":"male","e":2.5},{"s":"age_band","c":"18-34","e":1.7},{"s":"housing_tenure","c":"social rent","e":3.0}],"fwd":false},{"id":"le_0000037","a":"l_0007","b":"l_0005","p":0.7,"lw":6,"lm":1.0,"lp25":0.0,"lp75":2.5,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"strong"}],"d":[{"s":"age_band","c":"75+","e":1.3},{"s":"lives_with_partner","c":"false","e":1.2}],"fwd":false},{"id":"le_0000038","a":"l_0001","b":"l_0008","p":0.04,"lw":18,"lm":5.0,"lp25":1.0,"lp75":10.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"stress_susceptibility","d":"increases_p","st":"moderate"},{"s":"neuroticism","d":"increases_p","st":"moderate"}],"d":[{"s":"long_term_health_condition","c":"true","e":1.5}],"fwd":false},{"id":"le_0000039","a":"a_0016","b":"l_0001","p":0.35,"lw":3,"lm":1.0,"lp25":0.5,"lp75":2.0,"ls":"lognormal","h":[{"s":"language_proficiency_score","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"digital_access_level","d":"decreases_p","st":"moderate"},{"s":"geographic_anchor_strength","d":"decreases_p","st":"weak"}],"d":[{"s":"has_dependent_children","c":"true","e":1.5},{"s":"household_size","c":"single adult","e":0.9}],"fwd":true},{"id":"le_0000040","a":"a_0016","b":"l_0002","p":0.08,"lw":6,"lm":1.5,"lp25":0.5,"lp75":3.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"language_proficiency_score","d":"decreases_p","st":"strong"},{"s":"urban_preference","d":"increases_p","st":"moderate"},{"s":"trust_propensity","d":"decreases_p","st":"weak"}],"d":[{"s":"sex_at_birth","c":"male","e":2.5},{"s":"has_dependent_children","c":"false","e":1.8}],"fwd":true},{"id":"le_0000041","a":"a_0017","b":"l_0001","p":0.3,"lw":6,"lm":2.0,"lp25":1.0,"lp75":4.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"credit_access_score","d":"decreases_p","st":"moderate"},{"s":"geographic_anchor_strength","d":"increases_p","st":"weak"},{"s":"age_band","d":null,"st":null}],"d":[{"s":"has_dependent_children","c":"true","e":1.5},{"s":"age_band","c":"65+","e":0.6},{"s":"equivalised_household_income_decile","c":"<=4","e":1.8}],"fwd":true},{"id":"le_0000042","a":"l_0011","b":"l_0001","p":0.18,"lw":6,"lm":1.5,"lp25":0.3,"lp75":3.5,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"moderate"},{"s":"geographic_anchor_strength","d":"increases_p","st":"moderate"}],"d":[{"s":"has_dependent_children","c":"true","e":1.7},{"s":"sex_at_birth","c":"female","e":1.0},{"s":"legal_status_constraint","c":"no recourse to public funds","e":0.4}],"fwd":false},{"id":"le_0000043","a":"l_0006","b":"l_0005","p":0.6,"lw":6,"lm":1.0,"lp25":0.2,"lp75":2.5,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"chronic_condition_load","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"65+","e":1.3},{"s":"long_term_health_condition","c":"true","e":1.4}],"fwd":false},{"id":"le_0000044","a":"a_0013","b":"l_0005","p":0.06,"lw":12,"lm":3.0,"lp25":1.0,"lp75":6.0,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"increases_p","st":"weak"}],"d":[{"s":"age_band","c":"50+","e":1.6},{"s":"long_term_health_condition","c":"true","e":1.5}],"fwd":true},{"id":"le_0000045","a":"a_0018","b":"l_0013","p":0.04,"lw":3,"lm":0.5,"lp25":0.1,"lp75":1.5,"ls":"lognormal","h":[{"s":"emergency_fund_robustness","d":"decreases_p","st":"strong"},{"s":"borrowing_from_friends_family_likelihood","d":"decreases_p","st":"moderate"},{"s":"institutional_trust","d":"increases_p","st":"moderate"},{"s":"digital_access_level","d":"increases_p","st":"weak"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"}],"d":[{"s":"has_dependent_children","c":"true","e":1.6},{"s":"long_term_health_condition","c":"true","e":1.4},{"s":"age_band","c":"65+","e":1.3}],"fwd":true},{"id":"le_0000046","a":"l_0017","b":"l_0008","p":0.035,"lw":12,"lm":4.0,"lp25":1.0,"lp75":8.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"self_directedness","d":"decreases_p","st":"moderate"}],"d":[{"s":"long_term_health_condition","c":"true","e":1.4},{"s":"housing_tenure","c":"temporary or rough sleeping","e":2.0}],"fwd":false},{"id":"le_0000047","a":"a_0010","b":"l_0006","p":0.08,"lw":6,"lm":2.0,"lp25":0.5,"lp75":4.0,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"}],"d":[{"s":"age_band","c":"75+","e":1.8},{"s":"lives_with_partner","c":"false (post-event)","e":1.5}],"fwd":true},{"id":"le_0000048","a":"a_0006","b":"l_0006","p":0.002,"lw":12,"lm":4.0,"lp25":1.5,"lp75":8.0,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"financial_scam_vulnerability","d":"increases_p","st":"moderate"}],"d":[{"s":"age_band","c":"65+","e":5.0},{"s":"long_term_health_condition","c":"true","e":2.0}],"fwd":true},{"id":"le_0000049","a":"l_0017","b":"l_0001","p":0.1,"lw":18,"lm":6.0,"lp25":2.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"impulsivity_control","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"}],"d":[{"s":"housing_tenure","c":"social rent","e":1.6},{"s":"sex_at_birth","c":"male","e":1.4}],"fwd":false},{"id":"le_0000050","a":"l_0001","b":"l_0017","p":0.06,"lw":18,"lm":4.0,"lp25":1.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"impulsivity_control","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"25-49","e":1.5},{"s":"sex_at_birth","c":"male","e":1.6}],"fwd":false},{"id":"le_0000051","a":"a_0019","b":"l_0001","p":0.04,"lw":24,"lm":8.0,"lp25":3.0,"lp75":16.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"professional_development_investment","d":"decreases_p","st":"moderate"},{"s":"self_directedness","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"20-29","e":1.5},{"s":"sex_at_birth","c":"male","e":1.2}],"fwd":true},{"id":"le_0000052","a":"a_0019","b":"l_0017","p":0.025,"lw":24,"lm":12.0,"lp25":4.0,"lp75":20.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"weak"}],"d":[{"s":"sex_at_birth","c":"male","e":1.0},{"s":"age_band","c":"30-49","e":1.4}],"fwd":true},{"id":"le_0000053","a":"a_0020","b":"l_0005","p":0.05,"lw":12,"lm":3.0,"lp25":0.5,"lp75":7.0,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"},{"s":"self_directedness","d":"decreases_p","st":"moderate"}],"d":[{"s":"occupation_broad","c":"Process & Plant / Skilled Trades / Elementary","e":1.8},{"s":"age_band","c":"50+","e":1.3}],"fwd":true},{"id":"le_0000054","a":"a_0020","b":"l_0007","p":0.1,"lw":6,"lm":0.5,"lp25":0.1,"lp75":1.5,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"moderate"}],"d":[{"s":"occupation_broad","c":"Process & Plant / Construction / Manual","e":1.6},{"s":"age_band","c":"50+","e":1.4},{"s":"lives_with_partner","c":"false","e":1.5}],"fwd":true},{"id":"le_0000055","a":"l_0005","b":"l_0006","p":0.08,"lw":12,"lm":2.0,"lp25":0.5,"lp75":5.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"strong"},{"s":"financial_scam_vulnerability","d":"increases_p","st":"moderate"},{"s":"trust_propensity","d":"decreases_p","st":"weak"}],"d":[{"s":"age_band","c":"75+","e":1.5},{"s":"long_term_health_condition","c":"true","e":1.4}],"fwd":false},{"id":"le_0000056","a":"l_0007","b":"l_0006","p":0.05,"lw":6,"lm":1.5,"lp25":0.3,"lp75":3.5,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"strong"},{"s":"physical_functional_capacity","d":"decreases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"weak"}],"d":[{"s":"age_band","c":"75+","e":1.6},{"s":"lives_with_partner","c":"false","e":1.4}],"fwd":false},{"id":"le_0000057","a":"a_0021","b":"l_0001","p":0.22,"lw":24,"lm":6.0,"lp25":1.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"self_directedness","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"18-21","e":1.0},{"s":"has_dependent_children","c":"true","e":1.4},{"s":"legal_status_constraint","c":"former unaccompanied asylum seeking child (UASC)","e":1.6}],"fwd":true},{"id":"le_0000058","a":"a_0021","b":"l_0005","p":0.12,"lw":24,"lm":3.0,"lp25":0.5,"lp75":8.0,"ls":"lognormal","h":[{"s":"cognitive_processing_capacity","d":"decreases_p","st":"strong"},{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[{"s":"long_term_health_condition","c":"true","e":2.5},{"s":"highest_qualification","c":"None or below SEN threshold","e":1.5}],"fwd":true},{"id":"le_0000059","a":"a_0022","b":"l_0013","p":0.05,"lw":6,"lm":2.0,"lp25":0.5,"lp75":4.0,"ls":"lognormal","h":[{"s":"emergency_fund_robustness","d":"decreases_p","st":"strong"},{"s":"institutional_trust","d":"increases_p","st":"moderate"},{"s":"cognitive_processing_capacity","d":"increases_p","st":"moderate"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"}],"d":[{"s":"long_term_health_condition","c":"true","e":null},{"s":"has_dependent_children","c":"true","e":1.5},{"s":"equivalised_household_income_decile","c":"<=2","e":2.0}],"fwd":true},{"id":"le_0000060","a":"l_0008","b":"l_0017","p":0.1,"lw":6,"lm":1.5,"lp25":0.2,"lp75":3.5,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"weak"},{"s":"trust_propensity","d":"increases_p","st":"weak"}],"d":[{"s":"age_band","c":"25-49","e":1.4}],"fwd":false},{"id":"le_0000061","a":"l_0009","b":"l_0010","p":0.1,"lw":12,"lm":2.0,"lp25":0.5,"lp75":4.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[{"s":"ethnic_cultural_group","c":"Black","e":2.0},{"s":"long_term_health_condition","c":"true","e":1.5}],"fwd":true},{"id":"le_0000062","a":"l_0017","b":"l_0005","p":0.04,"lw":12,"lm":5.0,"lp25":2.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"chronic_condition_load","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"50+","e":2.0}],"fwd":false},{"id":"le_0000063","a":"l_0001","b":"l_0021","p":0.55,"lw":3,"lm":0.1,"lp25":0.0,"lp75":0.5,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"weak"},{"s":"geographic_anchor_strength","d":"increases_p","st":"moderate"}],"d":[{"s":"has_dependent_children","c":"true","e":1.6},{"s":"macro_region","c":"London or major urban","e":1.4}],"fwd":true},{"id":"le_0000064","a":"l_0021","b":"l_0019","p":0.35,"lw":24,"lm":8.0,"lp25":3.0,"lp75":18.0,"ls":"lognormal","h":[{"s":"geographic_anchor_strength","d":"increases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"weak"},{"s":"cognitive_processing_capacity","d":"increases_p","st":"moderate"},{"s":"language_proficiency_score","d":"increases_p","st":"moderate"}],"d":[{"s":"has_dependent_children","c":"true","e":1.5},{"s":"long_term_health_condition","c":"true","e":1.4},{"s":"macro_region","c":"London","e":0.5}],"fwd":false},{"id":"le_0000065","a":"a_0006","b":"l_0027","p":0.06,"lw":12,"lm":4.0,"lp25":1.0,"lp75":8.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"weak"},{"s":"digital_access_level","d":"increases_p","st":"weak"}],"d":[{"s":"sex_at_birth","c":"female","e":2.0},{"s":"equivalised_household_income_decile","c":"5-7","e":1.3}],"fwd":true},{"id":"le_0000066","a":"l_0005","b":"l_0025","p":0.06,"lw":24,"lm":9.0,"lp25":3.0,"lp75":18.0,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"85+","e":4.0},{"s":"lives_with_partner","c":"false","e":2.0}],"fwd":true},{"id":"le_0000067","a":"l_0007","b":"l_0025","p":0.15,"lw":6,"lm":0.5,"lp25":0.0,"lp75":1.5,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"85+","e":3.0},{"s":"lives_with_partner","c":"false","e":1.8}],"fwd":true},{"id":"le_0000068","a":"l_0025","b":"l_0006","p":0.09,"lw":24,"lm":6.0,"lp25":2.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"cognitive_processing_capacity","d":"decreases_p","st":"strong"},{"s":"physical_functional_capacity","d":"decreases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"trust_propensity","d":"increases_p","st":"weak"}],"d":[{"s":"age_band","c":"85+","e":1.2}],"fwd":false},{"id":"le_0000069","a":"a_0010","b":"l_0028","p":0.04,"lw":6,"lm":2.0,"lp25":0.5,"lp75":4.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"increases_p","st":"moderate"},{"s":"caregiving_responsibility","d":"increases_p","st":"strong"},{"s":"trust_propensity","d":"increases_p","st":"weak"}],"d":[{"s":"age_band","c":"50-69","e":1.5}],"fwd":true},{"id":"le_0000070","a":"l_0001","b":"l_0034","p":0.2,"lw":24,"lm":10.0,"lp25":4.0,"lp75":18.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"housing_stability_index","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"16-24","e":1.5},{"s":"long_term_health_condition","c":"true","e":1.3}],"fwd":true},{"id":"le_0000071","a":"l_0017","b":"l_0035","p":0.3,"lw":24,"lm":9.0,"lp25":4.0,"lp75":18.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"increases_p","st":"strong"},{"s":"housing_stability_index","d":"increases_p","st":"strong"},{"s":"mental_health_service_use","d":"decreases_p","st":"moderate"},{"s":"self_directedness","d":"increases_p","st":"strong"},{"s":"persistence","d":"increases_p","st":"moderate"}],"d":[{"s":"has_dependent_children","c":"true","e":1.2},{"s":"age_band","c":"35-54","e":1.1}],"fwd":true},{"id":"le_0000072","a":"l_0017","b":"l_0036","p":0.45,"lw":24,"lm":6.0,"lp25":2.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"impulsivity_control","d":"decreases_p","st":"strong"},{"s":"persistence","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"18-29","e":1.3},{"s":"housing_tenure","c":"temporary or rough sleeping","e":1.7}],"fwd":true},{"id":"le_0000073","a":"l_0027","b":"l_0037","p":0.5,"lw":12,"lm":4.0,"lp25":2.0,"lp75":8.0,"ls":"lognormal","h":[{"s":"self_directedness","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"increases_p","st":"moderate"},{"s":"persistence","d":"increases_p","st":"strong"},{"s":"substance_risk","d":"decreases_p","st":"moderate"},{"s":"neuroticism","d":"decreases_p","st":"weak"}],"d":[{"s":"equivalised_household_income_decile","c":"<=3","e":0.85},{"s":"ethnic_cultural_group","c":"Black / minority ethnic","e":0.9}],"fwd":true},{"id":"le_0000074","a":"l_0027","b":"l_0038","p":0.3,"lw":12,"lm":2.0,"lp25":0.5,"lp75":5.0,"ls":"lognormal","h":[{"s":"self_directedness","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"housing_stability_index","d":"decreases_p","st":"moderate"},{"s":"digital_access_level","d":"decreases_p","st":"weak"}],"d":[{"s":"equivalised_household_income_decile","c":"<=3","e":1.3},{"s":"ethnic_cultural_group","c":"Black / minority ethnic","e":1.2}],"fwd":true},{"id":"le_0000075","a":"l_0019","b":"l_0039","p":0.85,"lw":14,"lm":12.0,"lp25":12.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"increases_p","st":"moderate"},{"s":"financial_constraint_level","d":"decreases_p","st":"strong"},{"s":"mental_health_service_use","d":"decreases_p","st":"moderate"},{"s":"substance_risk","d":"decreases_p","st":"moderate"},{"s":"cognitive_processing_capacity","d":"increases_p","st":"weak"}],"d":[{"s":"has_dependent_children","c":"true","e":1.05},{"s":"age_band","c":"16-24","e":0.85}],"fwd":true},{"id":"le_0000076","a":"l_0019","b":"l_0034","p":0.1,"lw":24,"lm":8.0,"lp25":3.0,"lp75":16.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"housing_stability_index","d":"decreases_p","st":"weak"}],"d":[{"s":"age_band","c":"16-24","e":1.6},{"s":"long_term_health_condition","c":"true","e":1.3}],"fwd":false},{"id":"le_0000077","a":"l_0007","b":"l_0029","p":0.2,"lw":1,"lm":0.05,"lp25":0.0,"lp75":0.2,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"increases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"increases_p","st":"strong"},{"s":"self_directedness","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"increases_p","st":"moderate"}],"d":[{"s":"age_band","c":"65-84","e":1.5},{"s":"long_term_health_condition","c":"true","e":0.7}],"fwd":true},{"id":"le_0000078","a":"l_0011","b":"l_0040","p":0.3,"lw":12,"lm":6.0,"lp25":3.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"moderate"},{"s":"geographic_anchor_strength","d":"increases_p","st":"moderate"},{"s":"trust_propensity","d":"decreases_p","st":"weak"}],"d":[{"s":"has_dependent_children","c":"true","e":1.4},{"s":"legal_status_constraint","c":"no recourse to public funds","e":1.5}],"fwd":true},{"id":"le_0000079","a":"l_0005","b":"l_0030","p":0.015,"lw":12,"lm":4.0,"lp25":1.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"chronic_condition_load","d":"decreases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"75+","e":2.5}],"fwd":true},{"id":"le_0000080","a":"l_0036","b":"l_0017","p":0.35,"lw":24,"lm":8.0,"lp25":3.0,"lp75":16.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"increases_p","st":"moderate"},{"s":"self_directedness","d":"increases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"weak"}],"d":[{"s":"age_band","c":"30-49","e":1.2}],"fwd":false},{"id":"le_0000081","a":"l_0029","b":"l_0041","p":0.6,"lw":3,"lm":1.5,"lp25":1.0,"lp75":2.0,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"increases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"increases_p","st":"strong"},{"s":"self_directedness","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"increases_p","st":"moderate"},{"s":"persistence","d":"increases_p","st":"moderate"}],"d":[{"s":"age_band","c":"65-79","e":1.1},{"s":"lives_with_partner","c":"true","e":1.2}],"fwd":true},{"id":"le_0000082","a":"l_0029","b":"l_0005","p":0.35,"lw":3,"lm":1.5,"lp25":1.0,"lp75":2.5,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"chronic_condition_load","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"85+","e":1.4},{"s":"long_term_health_condition","c":"true","e":1.3}],"fwd":false},{"id":"le_0000083","a":"l_0005","b":"l_0042","p":0.55,"lw":6,"lm":1.5,"lp25":0.5,"lp75":3.0,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"},{"s":"chronic_condition_load","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"75+","e":1.2},{"s":"equivalised_household_income_decile","c":"<=5","e":1.4}],"fwd":true},{"id":"le_0000084","a":"l_0038","b":"l_0027","p":0.25,"lw":24,"lm":10.0,"lp25":5.0,"lp75":18.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"neuroticism","d":"increases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"moderate"},{"s":"self_directedness","d":"increases_p","st":"moderate"}],"d":[{"s":"sex_at_birth","c":"female","e":1.5}],"fwd":false},{"id":"le_0000085","a":"l_0034","b":"l_0001","p":0.75,"lw":6,"lm":0.5,"lp25":0.0,"lp75":1.5,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"housing_stability_index","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"}],"d":[{"s":"has_dependent_children","c":"true","e":1.2}],"fwd":false},{"id":"le_0000086","a":"l_0011","b":"l_0043","p":0.55,"lw":12,"lm":4.0,"lp25":2.0,"lp75":7.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"increases_p","st":"strong"},{"s":"financial_constraint_level","d":"decreases_p","st":"strong"},{"s":"housing_stability_index","d":"increases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"moderate"},{"s":"self_directedness","d":"increases_p","st":"moderate"}],"d":[{"s":"has_dependent_children","c":"true","e":0.9},{"s":"legal_status_constraint","c":"no recourse to public funds","e":0.6}],"fwd":true},{"id":"le_0000087","a":"l_0008","b":"l_0044","p":0.55,"lw":1,"lm":0.02,"lp25":0.0,"lp75":0.05,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"increases_p","st":"strong"},{"s":"trust_propensity","d":"increases_p","st":"moderate"},{"s":"substance_risk","d":"decreases_p","st":"moderate"},{"s":"self_directedness","d":"increases_p","st":"moderate"}],"d":[{"s":"ethnic_cultural_group","c":"Black","e":0.6},{"s":"age_band","c":"65+","e":1.2}],"fwd":true},{"id":"le_0000088","a":"l_0010","b":"l_0045","p":0.25,"lw":12,"lm":6.0,"lp25":3.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"strong"},{"s":"mental_health_service_use","d":"decreases_p","st":"moderate"},{"s":"trust_propensity","d":"decreases_p","st":"moderate"}],"d":[{"s":"ethnic_cultural_group","c":"Black","e":1.5},{"s":"long_term_health_condition","c":"true","e":1.3}],"fwd":true},{"id":"le_0000089","a":"l_0010","b":"l_0017","p":0.2,"lw":12,"lm":4.0,"lp25":1.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"25-49","e":1.3}],"fwd":false},{"id":"le_0000090","a":"l_0045","b":"l_0008","p":0.95,"lw":1,"lm":0.02,"lp25":0.0,"lp75":0.05,"ls":"lognormal","h":[],"d":[],"fwd":false},{"id":"le_0000091","a":"l_0044","b":"l_0027","p":0.18,"lw":6,"lm":1.5,"lp25":0.5,"lp75":3.5,"ls":"lognormal","h":[{"s":"trust_propensity","d":"increases_p","st":"moderate"},{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"increases_p","st":"moderate"},{"s":"self_directedness","d":"increases_p","st":"moderate"}],"d":[{"s":"equivalised_household_income_decile","c":"5+","e":1.3}],"fwd":false},{"id":"le_0000092","a":"l_0002","b":"l_0001","p":0.4,"lw":12,"lm":3.0,"lp25":1.0,"lp75":8.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"weak"},{"s":"trust_propensity","d":"increases_p","st":"strong"},{"s":"substance_risk","d":"decreases_p","st":"moderate"},{"s":"legal_status_constraint","d":"decreases_p","st":"strong"}],"d":[{"s":"has_dependent_children","c":"true","e":1.8}],"fwd":false},{"id":"le_0000093","a":"l_0002","b":"l_0008","p":0.1,"lw":12,"lm":5.0,"lp25":2.0,"lp75":10.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[],"fwd":false},{"id":"le_0000094","a":"l_0002","b":"l_0017","p":0.25,"lw":12,"lm":4.0,"lp25":1.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"trust_propensity","d":"increases_p","st":"moderate"}],"d":[],"fwd":false},{"id":"le_0000095","a":"l_0012","b":"l_0001","p":0.65,"lw":12,"lm":4.0,"lp25":2.0,"lp75":7.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"}],"d":[{"s":"has_dependent_children","c":"true","e":1.3},{"s":"legal_status_constraint","c":"no recourse to public funds","e":0.3}],"fwd":false},{"id":"le_0000096","a":"l_0012","b":"l_0019","p":0.3,"lw":18,"lm":9.0,"lp25":4.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"geographic_anchor_strength","d":"decreases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"weak"}],"d":[{"s":"has_dependent_children","c":"true","e":1.5},{"s":"macro_region","c":"London","e":0.5}],"fwd":true},{"id":"le_0000097","a":"l_0013","b":"l_0001","p":0.06,"lw":12,"lm":5.0,"lp25":2.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[{"s":"housing_tenure","c":"private rent","e":1.6}],"fwd":false},{"id":"le_0000098","a":"l_0016","b":"l_0015","p":0.08,"lw":18,"lm":6.0,"lp25":3.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"impulsivity_control","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"moderate"}],"d":[{"s":"sex_at_birth","c":"male","e":1.6},{"s":"housing_tenure","c":"social rent","e":1.8}],"fwd":false},{"id":"le_0000099","a":"l_0021","b":"l_0046","p":0.4,"lw":14,"lm":12.0,"lp25":12.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"geographic_anchor_strength","d":"increases_p","st":"moderate"},{"s":"language_proficiency_score","d":"increases_p","st":"moderate"}],"d":[{"s":"has_dependent_children","c":"true","e":1.5},{"s":"macro_region","c":"London","e":2.5}],"fwd":true},{"id":"le_0000100","a":"l_0025","b":"l_0048","p":0.28,"lw":12,"lm":5.5,"lp25":2.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"chronic_condition_load","d":"decreases_p","st":"strong"}],"d":[{"s":"age_band","c":"85+","e":2.0}],"fwd":true},{"id":"le_0000101","a":"l_0042","b":"l_0050","p":0.3,"lw":18,"lm":10.0,"lp25":6.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"chronic_condition_load","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"85+","e":1.5}],"fwd":true},{"id":"le_0000102","a":"l_0035","b":"l_0047","p":0.6,"lw":14,"lm":12.0,"lp25":12.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"increases_p","st":"strong"},{"s":"housing_stability_index","d":"increases_p","st":"strong"},{"s":"mental_health_service_use","d":"decreases_p","st":"moderate"},{"s":"persistence","d":"increases_p","st":"moderate"}],"d":[],"fwd":true},{"id":"le_0000103","a":"l_0035","b":"l_0017","p":0.2,"lw":18,"lm":9.0,"lp25":4.0,"lp75":15.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"}],"d":[],"fwd":false},{"id":"le_0000104","a":"l_0004","b":"l_0051","p":0.45,"lw":14,"lm":12.0,"lp25":12.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"weak"}],"d":[{"s":"long_term_health_condition","c":"true","e":1.3}],"fwd":true},{"id":"le_0000105","a":"l_0004","b":"l_0001","p":0.08,"lw":18,"lm":8.0,"lp25":4.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[{"s":"has_dependent_children","c":"true","e":1.4}],"fwd":false},{"id":"le_0000106","a":"l_0014","b":"l_0052","p":0.55,"lw":24,"lm":12.0,"lp25":9.0,"lp75":18.0,"ls":"lognormal","h":[{"s":"parental_self_efficacy","d":"increases_p","st":"strong"},{"s":"self_directedness","d":"increases_p","st":"strong"},{"s":"trust_propensity","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"increases_p","st":"moderate"}],"d":[{"s":"equivalised_household_income_decile","c":"<=3","e":0.9}],"fwd":true},{"id":"le_0000107","a":"l_0014","b":"l_0006","p":0.07,"lw":24,"lm":6.0,"lp25":2.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"}],"d":[{"s":"long_term_health_condition","c":"true","e":1.4}],"fwd":false},{"id":"le_0000108","a":"l_0046","b":"l_0019","p":0.5,"lw":24,"lm":18.0,"lp25":13.0,"lp75":24.0,"ls":"lognormal","h":[{"s":"geographic_anchor_strength","d":"decreases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"weak"}],"d":[{"s":"has_dependent_children","c":"true","e":1.4},{"s":"macro_region","c":"London","e":0.6}],"fwd":false},{"id":"le_0000109","a":"l_0040","b":"l_0043","p":0.4,"lw":12,"lm":4.5,"lp25":2.0,"lp75":8.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"increases_p","st":"strong"},{"s":"financial_constraint_level","d":"decreases_p","st":"strong"}],"d":[{"s":"has_dependent_children","c":"true","e":0.9}],"fwd":false},{"id":"le_0000110","a":"l_0040","b":"l_0012","p":0.1,"lw":6,"lm":2.0,"lp25":0.5,"lp75":4.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"geographic_anchor_strength","d":"decreases_p","st":"moderate"}],"d":[],"fwd":false},{"id":"le_0000111","a":"l_0050","b":"l_0025","p":0.3,"lw":12,"lm":4.0,"lp25":1.0,"lp75":8.0,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"}],"d":[{"s":"age_band","c":"85+","e":1.8}],"fwd":false},{"id":"le_0000112","a":"l_0049","b":"l_0015","p":0.3,"lw":12,"lm":3.0,"lp25":1.0,"lp75":6.0,"ls":"lognormal","h":[{"s":"impulsivity_control","d":"decreases_p","st":"strong"}],"d":[{"s":"sex_at_birth","c":"male","e":1.7},{"s":"housing_tenure","c":"social rent","e":2.0}],"fwd":false},{"id":"le_0000113","a":"a_0023","b":"l_0008","p":0.003,"lw":18,"lm":6.0,"lp25":2.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"neuroticism","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"}],"d":[{"s":"has_dependent_children","c":"true (existing)","e":1.4}],"fwd":true},{"id":"le_0000114","a":"a_0023","b":"l_0027","p":0.04,"lw":18,"lm":4.0,"lp25":1.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[{"s":"equivalised_household_income_decile","c":"<=3","e":0.85}],"fwd":true},{"id":"le_0000115","a":"a_0024","b":"l_0001","p":0.015,"lw":24,"lm":12.0,"lp25":4.0,"lp75":22.0,"ls":"lognormal","h":[{"s":"language_proficiency_score","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"credit_access_score","d":"decreases_p","st":"moderate"}],"d":[{"s":"legal_status_constraint","c":"no recourse to public funds","e":0.4},{"s":"has_dependent_children","c":"true","e":1.5}],"fwd":true},{"id":"le_0000116","a":"l_0015","b":"l_0049","p":0.25,"lw":12,"lm":4.0,"lp25":1.0,"lp75":8.0,"ls":"lognormal","h":[{"s":"impulsivity_control","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"moderate"}],"d":[{"s":"sex_at_birth","c":"male","e":1.6}],"fwd":true},{"id":"le_0000117","a":"l_0028","b":"l_0053","p":0.65,"lw":6,"lm":1.5,"lp25":0.5,"lp75":3.0,"ls":"lognormal","h":[{"s":"caregiving_responsibility","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"}],"d":[{"s":"long_term_health_condition","c":"true (carer)","e":1.3}],"fwd":true},{"id":"le_0000118","a":"l_0030","b":"l_0048","p":0.5,"lw":12,"lm":5.0,"lp25":2.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"chronic_condition_load","d":"decreases_p","st":"strong"}],"d":[{"s":"age_band","c":"85+","e":1.4}],"fwd":true},{"id":"le_0000119","a":"l_0037","b":"l_0054","p":0.7,"lw":14,"lm":12.0,"lp25":12.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"increases_p","st":"moderate"},{"s":"self_directedness","d":"increases_p","st":"strong"},{"s":"neuroticism","d":"decreases_p","st":"weak"},{"s":"persistence","d":"increases_p","st":"moderate"}],"d":[{"s":"equivalised_household_income_decile","c":"<=3","e":0.85}],"fwd":true},{"id":"le_0000120","a":"l_0037","b":"l_0027","p":0.15,"lw":24,"lm":9.0,"lp25":5.0,"lp75":18.0,"ls":"lognormal","h":[{"s":"neuroticism","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"}],"d":[],"fwd":false},{"id":"le_0000121","a":"l_0051","b":"l_0001","p":0.05,"lw":18,"lm":10.0,"lp25":5.0,"lp75":16.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[{"s":"has_dependent_children","c":"true","e":1.4}],"fwd":false},{"id":"le_0000122","a":"a_0025","b":"l_0028","p":0.15,"lw":12,"lm":6.0,"lp25":2.0,"lp75":11.0,"ls":"lognormal","h":[{"s":"caregiving_responsibility","d":"decreases_p","st":"strong"},{"s":"trust_propensity","d":"increases_p","st":"moderate"},{"s":"digital_access_level","d":"increases_p","st":"weak"}],"d":[{"s":"age_band","c":"50-69","e":1.4}],"fwd":true},{"id":"le_0000123","a":"a_0026","b":"l_0007","p":0.5,"lw":18,"lm":6.0,"lp25":1.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"75+","e":1.5}],"fwd":true},{"id":"le_0000124","a":"a_0027","b":"l_0001","p":0.25,"lw":6,"lm":2.0,"lp25":0.5,"lp75":4.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"credit_access_score","d":"decreases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"moderate"}],"d":[{"s":"has_dependent_children","c":"true","e":1.4}],"fwd":true},{"id":"le_0000125","a":"a_0012","b":"l_0027","p":0.03,"lw":18,"lm":5.0,"lp25":2.0,"lp75":11.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"weak"}],"d":[{"s":"sex_at_birth","c":"female","e":1.8}],"fwd":true},{"id":"le_0000126","a":"a_0012","b":"l_0001","p":0.005,"lw":18,"lm":9.0,"lp25":4.0,"lp75":16.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"<65","e":1.3},{"s":"housing_tenure","c":"social rent or private rent","e":1.8}],"fwd":true},{"id":"le_0000127","a":"a_0012","b":"l_0017","p":0.005,"lw":24,"lm":9.0,"lp25":4.0,"lp75":18.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"alcohol_freq","d":"increases_p","st":"moderate"}],"d":[],"fwd":true},{"id":"le_0000128","a":"a_0025","b":"l_0027","p":0.04,"lw":24,"lm":10.0,"lp25":5.0,"lp75":18.0,"ls":"lognormal","h":[{"s":"caregiving_responsibility","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"}],"d":[{"s":"sex_at_birth","c":"female","e":1.7},{"s":"age_band","c":"50-69","e":1.3}],"fwd":true},{"id":"le_0000129","a":"a_0028","b":"l_0008","p":0.002,"lw":12,"lm":4.0,"lp25":1.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"neuroticism","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"}],"d":[],"fwd":true},{"id":"le_0000130","a":"a_0029","b":"l_0013","p":0.03,"lw":6,"lm":2.5,"lp25":1.0,"lp75":4.5,"ls":"lognormal","h":[{"s":"emergency_fund_robustness","d":"decreases_p","st":"strong"},{"s":"institutional_trust","d":"increases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"}],"d":[{"s":"has_dependent_children","c":"true","e":1.6}],"fwd":true},{"id":"le_0000131","a":"a_0029","b":"l_0003","p":0.04,"lw":12,"lm":5.0,"lp25":2.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"budgeting_formality","d":"decreases_p","st":"moderate"},{"s":"institutional_trust","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"emergency_fund_robustness","d":"decreases_p","st":"strong"}],"d":[{"s":"equivalised_household_income_decile","c":"<=4","e":1.7}],"fwd":true},{"id":"le_0000132","a":"a_0030","b":"l_0001","p":0.08,"lw":12,"lm":1.5,"lp25":0.2,"lp75":4.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"language_proficiency_score","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"}],"d":[{"s":"legal_status_constraint","c":"no recourse to public funds","e":0.2},{"s":"has_dependent_children","c":"true","e":1.5}],"fwd":true},{"id":"le_0000133","a":"l_0010","b":"l_0027","p":0.2,"lw":12,"lm":3.0,"lp25":1.0,"lp75":7.0,"ls":"lognormal","h":[{"s":"self_directedness","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"increases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"moderate"}],"d":[],"fwd":false},{"id":"le_0000134","a":"a_0031","b":"l_0001","p":0.01,"lw":12,"lm":4.0,"lp25":1.0,"lp75":8.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"credit_access_score","d":"decreases_p","st":"moderate"}],"d":[{"s":"has_dependent_children","c":"true","e":1.4}],"fwd":true},{"id":"le_0000135","a":"a_0031","b":"l_0027","p":0.02,"lw":18,"lm":6.0,"lp25":2.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"geographic_anchor_strength","d":"increases_p","st":"moderate"}],"d":[],"fwd":true},{"id":"le_0000136","a":"a_0032","b":"l_0003","p":0.1,"lw":12,"lm":4.0,"lp25":1.0,"lp75":8.0,"ls":"lognormal","h":[{"s":"budgeting_formality","d":"decreases_p","st":"moderate"},{"s":"institutional_trust","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"}],"d":[{"s":"equivalised_household_income_decile","c":"<=4","e":1.7}],"fwd":true},{"id":"le_0000137","a":"a_0033","b":"l_0005","p":0.1,"lw":18,"lm":9.0,"lp25":3.0,"lp75":15.0,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"self_directedness","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"55+","e":1.4}],"fwd":true},{"id":"le_0000138","a":"a_0033","b":"l_0017","p":0.04,"lw":24,"lm":12.0,"lp25":6.0,"lp75":20.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"impulsivity_control","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"35-64","e":1.3}],"fwd":true},{"id":"le_0000139","a":"l_0011","b":"l_0027","p":0.15,"lw":12,"lm":4.0,"lp25":1.0,"lp75":8.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"moderate"},{"s":"self_directedness","d":"increases_p","st":"moderate"}],"d":[{"s":"sex_at_birth","c":"female","e":1.5}],"fwd":false},{"id":"le_0000140","a":"l_0012","b":"l_0027","p":0.25,"lw":6,"lm":2.0,"lp25":0.5,"lp75":4.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"weak"}],"d":[{"s":"language_proficiency_score","c":"limited English","e":0.6}],"fwd":false},{"id":"le_0000141","a":"l_0007","b":"l_0017","p":0.05,"lw":6,"lm":1.0,"lp25":0.2,"lp75":2.5,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"alcohol_freq","d":"increases_p","st":"strong"}],"d":[{"s":"age_band","c":"35-64","e":1.4}],"fwd":false},{"id":"le_0000142","a":"l_0028","b":"l_0027","p":0.1,"lw":12,"lm":4.0,"lp25":1.0,"lp75":8.0,"ls":"lognormal","h":[{"s":"caregiving_responsibility","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"moderate"}],"d":[{"s":"sex_at_birth","c":"female","e":1.6}],"fwd":false},{"id":"le_0000143","a":"a_0034","b":"l_0017","p":0.15,"lw":18,"lm":5.0,"lp25":1.0,"lp75":11.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"impulsivity_control","d":"decreases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"}],"d":[{"s":"sex_at_birth","c":"male","e":1.5}],"fwd":true},{"id":"le_0000144","a":"a_0034","b":"l_0015","p":0.05,"lw":12,"lm":4.0,"lp25":1.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"impulsivity_control","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"moderate"}],"d":[{"s":"sex_at_birth","c":"male","e":2.0},{"s":"housing_tenure","c":"social rent","e":2.5}],"fwd":true},{"id":"le_0000145","a":"a_0034","b":"l_0008","p":0.04,"lw":18,"lm":6.0,"lp25":1.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"stress_susceptibility","d":"increases_p","st":"moderate"}],"d":[],"fwd":true},{"id":"le_0000146","a":"a_0035","b":"l_0027","p":0.025,"lw":24,"lm":9.0,"lp25":4.0,"lp75":18.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"self_directedness","d":"decreases_p","st":"moderate"},{"s":"achievement_striving","d":"increases_p","st":"moderate"}],"d":[{"s":"sex_at_birth","c":"female","e":1.8}],"fwd":true},{"id":"le_0000147","a":"a_0035","b":"l_0017","p":0.008,"lw":24,"lm":12.0,"lp25":6.0,"lp75":20.0,"ls":"lognormal","h":[{"s":"alcohol_freq","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"substance_risk","d":"increases_p","st":"strong"}],"d":[{"s":"sex_at_birth","c":"male","e":1.6}],"fwd":true},{"id":"le_0000148","a":"a_0036","b":"l_0013","p":0.05,"lw":12,"lm":5.0,"lp25":2.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"emergency_fund_robustness","d":"decreases_p","st":"strong"},{"s":"impulsivity_control","d":"decreases_p","st":"strong"},{"s":"trust_propensity","d":"increases_p","st":"moderate"}],"d":[{"s":"has_dependent_children","c":"true","e":1.5}],"fwd":true},{"id":"le_0000149","a":"a_0036","b":"l_0003","p":0.1,"lw":18,"lm":7.0,"lp25":3.0,"lp75":13.0,"ls":"lognormal","h":[{"s":"emergency_fund_robustness","d":"decreases_p","st":"strong"},{"s":"budgeting_formality","d":"decreases_p","st":"strong"},{"s":"impulsivity_control","d":"decreases_p","st":"strong"}],"d":[],"fwd":true},{"id":"le_0000150","a":"a_0021","b":"l_0027","p":0.1,"lw":24,"lm":8.0,"lp25":3.0,"lp75":16.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"trust_propensity","d":"decreases_p","st":"weak"}],"d":[{"s":"age_band","c":"18-25","e":1.0}],"fwd":true},{"id":"le_0000151","a":"a_0004","b":"l_0008","p":0.03,"lw":12,"lm":3.0,"lp25":1.0,"lp75":7.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[{"s":"sex_at_birth","c":"male","e":1.0}],"fwd":true},{"id":"le_0000152","a":"a_0037","b":"l_0007","p":0.65,"lw":6,"lm":1.0,"lp25":0.2,"lp75":2.5,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"}],"d":[{"s":"age_band","c":"65+","e":1.4}],"fwd":true},{"id":"le_0000153","a":"a_0037","b":"l_0005","p":0.4,"lw":18,"lm":6.0,"lp25":2.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"cognitive_processing_capacity","d":"decreases_p","st":"strong"},{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"}],"d":[{"s":"lives_with_partner","c":"false","e":1.5}],"fwd":true},{"id":"le_0000154","a":"a_0037","b":"l_0027","p":0.1,"lw":18,"lm":6.0,"lp25":2.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"self_directedness","d":"increases_p","st":"moderate"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"cognitive_processing_capacity","d":"increases_p","st":"weak"}],"d":[],"fwd":true},{"id":"le_0000155","a":"a_0038","b":"l_0001","p":0.55,"lw":6,"lm":1.0,"lp25":0.2,"lp75":3.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"impulsivity_control","d":"decreases_p","st":"strong"}],"d":[{"s":"has_dependent_children","c":"true","e":1.5}],"fwd":true},{"id":"le_0000156","a":"a_0038","b":"l_0002","p":0.2,"lw":6,"lm":1.0,"lp25":0.1,"lp75":2.5,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"trust_propensity","d":"decreases_p","st":"moderate"}],"d":[{"s":"sex_at_birth","c":"male","e":1.8}],"fwd":true},{"id":"le_0000157","a":"a_0039","b":"l_0027","p":0.04,"lw":18,"lm":9.0,"lp25":4.0,"lp75":15.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"trust_propensity","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"self_directedness","d":"decreases_p","st":"moderate"}],"d":[],"fwd":true},{"id":"le_0000158","a":"l_0011","b":"l_0017","p":0.08,"lw":12,"lm":5.0,"lp25":2.0,"lp75":10.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"trust_propensity","d":"increases_p","st":"moderate"}],"d":[],"fwd":false},{"id":"le_0000159","a":"l_0009","b":"l_0017","p":0.12,"lw":6,"lm":2.0,"lp25":0.5,"lp75":4.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"}],"d":[{"s":"age_band","c":"25-49","e":1.3}],"fwd":false},{"id":"le_0000160","a":"l_0034","b":"l_0008","p":0.06,"lw":12,"lm":4.0,"lp25":1.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[],"fwd":false},{"id":"le_0000161","a":"a_0040","b":"l_0011","p":0.08,"lw":6,"lm":2.5,"lp25":0.5,"lp75":5.0,"ls":"lognormal","h":[{"s":"trust_propensity","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"language_proficiency_score","d":"increases_p","st":"moderate"}],"d":[{"s":"legal_status_constraint","c":"no recourse to public funds","e":0.5}],"fwd":true},{"id":"le_0000162","a":"a_0040","b":"l_0008","p":0.05,"lw":18,"lm":7.0,"lp25":2.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"neuroticism","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"}],"d":[{"s":"legal_status_constraint","c":"no recourse to public funds","e":0.6}],"fwd":true},{"id":"le_0000163","a":"a_0041","b":"l_0005","p":0.1,"lw":12,"lm":4.0,"lp25":1.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"cognitive_processing_capacity","d":"decreases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"}],"d":[{"s":"age_band","c":"65+","e":1.6}],"fwd":true},{"id":"le_0000164","a":"a_0041","b":"l_0027","p":0.08,"lw":12,"lm":4.0,"lp25":1.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"trust_propensity","d":"increases_p","st":"moderate"}],"d":[{"s":"sex_at_birth","c":"female","e":1.6}],"fwd":true},{"id":"le_0000165","a":"a_0042","b":"l_0013","p":0.04,"lw":12,"lm":3.0,"lp25":1.0,"lp75":6.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"institutional_trust","d":"increases_p","st":"moderate"}],"d":[{"s":"has_dependent_children","c":"true","e":1.5}],"fwd":true},{"id":"le_0000166","a":"a_0042","b":"l_0001","p":0.005,"lw":18,"lm":8.0,"lp25":3.0,"lp75":15.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"}],"d":[{"s":"housing_tenure","c":"private rent","e":1.8}],"fwd":true},{"id":"le_0000167","a":"l_0014","b":"l_0017","p":0.1,"lw":18,"lm":5.0,"lp25":1.0,"lp75":11.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"trust_propensity","d":"increases_p","st":"moderate"}],"d":[{"s":"has_dependent_children","c":"true","e":1.0}],"fwd":false},{"id":"le_0000168","a":"l_0017","b":"l_0013","p":0.05,"lw":12,"lm":4.0,"lp25":1.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"institutional_trust","d":"increases_p","st":"moderate"}],"d":[],"fwd":false},{"id":"le_0000169","a":"l_0011","b":"l_0008","p":0.04,"lw":12,"lm":4.0,"lp25":1.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"neuroticism","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"}],"d":[{"s":"sex_at_birth","c":"female","e":1.0}],"fwd":false},{"id":"le_0000170","a":"a_0014","b":"l_0055","p":0.04,"lw":12,"lm":4.0,"lp25":1.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"decreases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"strong"}],"d":[{"s":"sex_at_birth","c":"male","e":3.0}],"fwd":true},{"id":"le_0000171","a":"l_0008","b":"l_0055","p":0.005,"lw":12,"lm":4.0,"lp25":1.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"}],"d":[{"s":"sex_at_birth","c":"male","e":2.5}],"fwd":false},{"id":"le_0000172","a":"l_0010","b":"l_0055","p":0.008,"lw":12,"lm":5.0,"lp25":2.0,"lp75":10.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"}],"d":[{"s":"sex_at_birth","c":"male","e":2.5}],"fwd":false},{"id":"le_0000173","a":"l_0002","b":"l_0055","p":0.015,"lw":12,"lm":6.0,"lp25":2.0,"lp75":11.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"mental_health_service_use","d":"decreases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"}],"d":[],"fwd":false},{"id":"le_0000174","a":"a_0004","b":"l_0055","p":0.005,"lw":12,"lm":1.5,"lp25":0.2,"lp75":4.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"}],"d":[{"s":"sex_at_birth","c":"male","e":1.0}],"fwd":true},{"id":"le_0000175","a":"a_0023","b":"l_0055","p":0.0001,"lw":18,"lm":9.0,"lp25":3.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"}],"d":[],"fwd":true},{"id":"le_0000176","a":"l_0036","b":"l_0056","p":0.015,"lw":12,"lm":4.0,"lp25":0.5,"lp75":9.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"mental_health_service_use","d":"decreases_p","st":"moderate"}],"d":[{"s":"housing_tenure","c":"temporary or rough sleeping","e":2.5}],"fwd":false},{"id":"le_0000177","a":"a_0004","b":"l_0056","p":0.004,"lw":6,"lm":1.0,"lp25":0.05,"lp75":3.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"}],"d":[],"fwd":true},{"id":"le_0000178","a":"l_0002","b":"l_0056","p":0.025,"lw":12,"lm":6.0,"lp25":2.0,"lp75":11.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"}],"d":[],"fwd":false},{"id":"le_0000179","a":"l_0017","b":"l_0057","p":0.3,"lw":14,"lm":12.0,"lp25":12.0,"lp75":12.0,"ls":"degenerate","h":[{"s":"social_support_capacity","d":"increases_p","st":"strong"},{"s":"self_directedness","d":"increases_p","st":"strong"},{"s":"housing_stability_index","d":"increases_p","st":"strong"}],"d":[{"s":"age_band","c":"35-64","e":1.3}],"fwd":true},{"id":"le_0000180","a":"l_0057","b":"l_0035","p":0.2,"lw":24,"lm":12.0,"lp25":6.0,"lp75":22.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"increases_p","st":"strong"},{"s":"persistence","d":"increases_p","st":"strong"}],"d":[],"fwd":false},{"id":"le_0000181","a":"l_0057","b":"l_0036","p":0.15,"lw":24,"lm":12.0,"lp25":6.0,"lp75":22.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"strong"}],"d":[],"fwd":false},{"id":"le_0000182","a":"l_0027","b":"l_0058","p":0.1,"lw":12,"lm":6.0,"lp25":4.0,"lp75":9.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"self_directedness","d":"increases_p","st":"moderate"}],"d":[{"s":"long_term_health_condition","c":"true","e":1.5}],"fwd":true},{"id":"le_0000183","a":"l_0058","b":"l_0037","p":0.55,"lw":14,"lm":12.0,"lp25":9.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"self_directedness","d":"increases_p","st":"strong"},{"s":"persistence","d":"increases_p","st":"strong"}],"d":[],"fwd":false},{"id":"le_0000184","a":"l_0058","b":"l_0038","p":0.3,"lw":14,"lm":8.0,"lp25":4.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[],"fwd":false},{"id":"le_0000185","a":"l_0042","b":"l_0059","p":0.55,"lw":14,"lm":12.0,"lp25":12.0,"lp75":12.0,"ls":"degenerate","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"moderate"},{"s":"social_support_capacity","d":"increases_p","st":"weak"}],"d":[{"s":"age_band","c":"75+","e":1.2}],"fwd":true},{"id":"le_0000186","a":"l_0059","b":"l_0050","p":0.2,"lw":24,"lm":14.0,"lp25":7.0,"lp75":20.0,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"chronic_condition_load","d":"decreases_p","st":"strong"}],"d":[{"s":"age_band","c":"85+","e":1.5}],"fwd":false},{"id":"le_0000187","a":"l_0059","b":"l_0048","p":0.1,"lw":24,"lm":18.0,"lp25":10.0,"lp75":24.0,"ls":"lognormal","h":[{"s":"physical_functional_capacity","d":"decreases_p","st":"strong"},{"s":"chronic_condition_load","d":"decreases_p","st":"strong"}],"d":[{"s":"age_band","c":"85+","e":2.0}],"fwd":false},{"id":"le_0000188","a":"a_0010","b":"a_0013","p":0.1,"lw":12,"lm":3.0,"lp25":1.0,"lp75":7.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"},{"s":"housing_stability_index","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Loss of informal carer triggers acute MH crisis in cared-for person (esp. cognitive impairment / severe disability dependents).","citation":"Brodaty & Donkin 2009; Carers UK State of Caring 2023","confidence":"M","batch":"29a"}},{"id":"le_0000189","a":"a_0025","b":"a_0013","p":0.06,"lw":24,"lm":9.0,"lp25":3.0,"lp75":18.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Becoming an informal carer raises new carer's risk of acute MH crisis (carer-role strain).","citation":"Stansfeld et al. 2014 (BMJ Open); Carers UK 2023; ONS Carers Survey 2022","confidence":"H","batch":"29a"}},{"id":"le_0000190","a":"a_0025","b":"a_0007","p":0.05,"lw":18,"lm":6.0,"lp25":2.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"weak"}],"d":[{"s":"equivalised_household_income_decile","c":"<=3","e":1.5}],"fwd":true,"meta":{"description":"Becoming an informal carer raises new carer's risk of job loss (working-age carers leaving employment).","citation":"Carers UK State of Caring 2023; Pickard 2015 (Ageing & Society)","confidence":"H","batch":"29a"}},{"id":"le_0000191","a":"a_0012","b":"a_0013","p":0.15,"lw":18,"lm":4.0,"lp25":1.0,"lp75":10.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Bereavement of spouse/long-term partner triggers acute MH crisis in surviving partner (first-year peak).","citation":"Stroebe et al. 2007 (Lancet); Bowling 1987; King et al. 2013 UK cohort","confidence":"H","batch":"29a"}},{"id":"le_0000192","a":"a_0012","b":"a_0014","p":0.025,"lw":24,"lm":6.0,"lp25":1.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[{"s":"age_band","c":"65+","e":1.6}],"fwd":true,"meta":{"description":"Bereavement of spouse/long-term partner raises self-harm/suicide-attempt risk (widow(er) suicide RR elevated 2-10x first months, esp. older men).","citation":"Erlangsen et al. 2017 (JAMA); King et al. 2013","confidence":"H","batch":"29a"}},{"id":"le_0000193","a":"a_0003","b":"a_0013","p":0.18,"lw":12,"lm":3.0,"lp25":1.0,"lp75":7.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"},{"s":"housing_stability_index","d":"decreases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Domestic abuse incident requiring relocation triggers acute MH crisis in survivor (Trevillion meta-analysis RR ~2.7).","citation":"Trevillion et al. 2012 (PLoS ONE) meta-analysis; SafeLives Insights","confidence":"H","batch":"29a"}},{"id":"le_0000194","a":"a_0003","b":"a_0014","p":0.04,"lw":18,"lm":4.0,"lp25":1.0,"lp75":10.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Domestic abuse exposure raises suicide-attempt / serious self-harm risk in survivor (Devries meta-analysis RR ~2.5).","citation":"Devries et al. 2013 (PLoS Med)","confidence":"H","batch":"29a"}},{"id":"le_0000195","a":"a_0004","b":"a_0013","p":0.12,"lw":9,"lm":2.0,"lp25":0.5,"lp75":5.0,"ls":"lognormal","h":[{"s":"housing_stability_index","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"substance_risk","d":"increases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Discharge from custody triggers acute MH crisis (Fazel post-release MH service contact elevated).","citation":"Fazel et al. 2016 (Lancet Psychiatry); MoJ release cohort studies","confidence":"H","batch":"29a"}},{"id":"le_0000196","a":"a_0004","b":"a_0014","p":0.03,"lw":6,"lm":1.0,"lp25":0.25,"lp75":3.0,"ls":"lognormal","h":[{"s":"housing_stability_index","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Discharge from custody raises suicide-attempt risk (Pratt RR 5-10 in first 4 weeks post-release).","citation":"Pratt et al. 2006 (Lancet); Fazel et al. 2011","confidence":"H","batch":"29a"}},{"id":"le_0000197","a":"a_0005","b":"a_0013","p":0.22,"lw":9,"lm":2.0,"lp25":0.5,"lp75":5.0,"ls":"lognormal","h":[{"s":"housing_stability_index","d":"decreases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Psychiatric inpatient discharge without secured accommodation triggers re-crisis (NCISH 90-day readmission ~20-25%).","citation":"NCISH 2023 UK report; Walter et al. 2019","confidence":"H","batch":"29a"}},{"id":"le_0000198","a":"a_0005","b":"a_0014","p":0.05,"lw":6,"lm":1.0,"lp25":0.25,"lp75":2.5,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Psychiatric inpatient discharge raises suicide-attempt risk (NCISH \u2014 concentrated in first 2 weeks, RR vs general population ~100).","citation":"NCISH 2023 UK report","confidence":"H","batch":"29a"}},{"id":"le_0000199","a":"a_0013","b":"a_0014","p":0.08,"lw":6,"lm":1.0,"lp25":0.25,"lp75":3.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Acute MH crisis presentation predicts subsequent self-harm in 6mo window (strongest single predictor).","citation":"Owens et al. 2002; Hawton et al. 2003","confidence":"H","batch":"29a"}},{"id":"le_0000200","a":"a_0029","b":"a_0042","p":0.2,"lw":9,"lm":2.0,"lp25":0.5,"lp75":5.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"credit_access_score","d":"decreases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"weak"}],"d":[{"s":"equivalised_household_income_decile","c":"<=3","e":2.5},{"s":"area_deprivation_decile","c":"<=3","e":1.8},{"s":"has_dependent_children","c":"true","e":1.4}],"fwd":true,"meta":{"description":"Cost-of-living income shock triggers food insecurity onset (Trussell Trust / Food Foundation surveillance).","citation":"Trussell Trust 2023; Loopstra & Lalor / Food Foundation","confidence":"M","batch":"29a"}},{"id":"le_0000201","a":"a_0002","b":"a_0013","p":0.08,"lw":9,"lm":2.0,"lp25":0.5,"lp75":5.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"housing_stability_index","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Family/friends asking person to leave accommodation triggers acute MH crisis (relational rupture + housing precarity).","citation":"Crisis & Shelter studies on hidden homelessness; St Mungo's pathway analyses","confidence":"M","batch":"29a"}},{"id":"le_0000202","a":"a_0007","b":"a_0013","p":0.08,"lw":12,"lm":3.0,"lp25":1.0,"lp75":7.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[{"s":"equivalised_household_income_decile","c":"<=3","e":1.4}],"fwd":true,"meta":{"description":"Job loss / redundancy triggers acute MH crisis (unemployment psychological distress).","citation":"Paul & Moser 2009 meta-analysis","confidence":"H","batch":"29b"}},{"id":"le_0000203","a":"a_0007","b":"a_0029","p":0.25,"lw":9,"lm":2.0,"lp25":0.5,"lp75":5.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"credit_access_score","d":"decreases_p","st":"moderate"}],"d":[{"s":"equivalised_household_income_decile","c":"<=3","e":1.8}],"fwd":true,"meta":{"description":"Job loss / redundancy triggers cost-of-living income shock.","citation":"DWP HBAI series","confidence":"H","batch":"29b"}},{"id":"le_0000204","a":"a_0008","b":"a_0042","p":0.3,"lw":6,"lm":1.0,"lp25":0.25,"lp75":3.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"credit_access_score","d":"decreases_p","st":"moderate"}],"d":[{"s":"area_deprivation_decile","c":"<=3","e":1.6},{"s":"has_dependent_children","c":"true","e":1.5}],"fwd":true,"meta":{"description":"Benefit sanction triggers food insecurity onset (strongest evidence in batch).","citation":"Loopstra et al. 2018 (BMJ); Trussell Trust","confidence":"H","batch":"29b"}},{"id":"le_0000205","a":"a_0008","b":"a_0029","p":0.4,"lw":4,"lm":0.5,"lp25":0.1,"lp75":2.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"credit_access_score","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Benefit sanction triggers immediate cost-of-living shock (direct income loss).","citation":"DWP sanction impact studies","confidence":"H","batch":"29b"}},{"id":"le_0000206","a":"a_0008","b":"a_0013","p":0.06,"lw":9,"lm":2.0,"lp25":0.5,"lp75":5.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Benefit sanction triggers acute MH crisis.","citation":"Williams 2021 (J Soc Pol); Wright et al. 2020","confidence":"M","batch":"29b"}},{"id":"le_0000207","a":"a_0036","b":"a_0013","p":0.1,"lw":18,"lm":4.0,"lp25":1.0,"lp75":10.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Problem gambling onset triggers acute MH crisis (RR 3-5).","citation":"Cowlishaw & Kessler 2016; PHE Gambling Review 2023","confidence":"H","batch":"29b"}},{"id":"le_0000208","a":"a_0036","b":"a_0014","p":0.04,"lw":24,"lm":6.0,"lp25":1.5,"lp75":14.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"}],"d":[{"s":"age_band","c":"18-44","e":1.4}],"fwd":true,"meta":{"description":"Problem gambling raises suicide-attempt risk (gambling-related suicide is well-documented).","citation":"Karlsson & H\u00e5kansson 2018; Wardle et al. UK 2020","confidence":"H","batch":"29b"}},{"id":"le_0000209","a":"a_0036","b":"a_0029","p":0.18,"lw":12,"lm":3.0,"lp25":1.0,"lp75":7.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"credit_access_score","d":"decreases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Problem gambling triggers cost-of-living shock via debt accumulation.","citation":"GambleAware impact studies","confidence":"H","batch":"29b"}},{"id":"le_0000210","a":"a_0042","b":"a_0013","p":0.05,"lw":18,"lm":4.0,"lp25":1.0,"lp75":10.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[{"s":"has_dependent_children","c":"true","e":1.3}],"fwd":true,"meta":{"description":"Food insecurity triggers acute MH crisis.","citation":"Pourmotabbed et al. 2020 meta-analysis","confidence":"M","batch":"29b"}},{"id":"le_0000211","a":"a_0018","b":"a_0042","p":0.15,"lw":9,"lm":2.0,"lp25":0.5,"lp75":5.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"credit_access_score","d":"decreases_p","st":"moderate"}],"d":[{"s":"area_deprivation_decile","c":"<=3","e":1.7}],"fwd":true,"meta":{"description":"Utility/essential bill crisis triggers food insecurity via heat-or-eat trade-off.","citation":"E3G fuel poverty research; Food Foundation","confidence":"M","batch":"29b"}},{"id":"le_0000212","a":"a_0022","b":"a_0013","p":0.15,"lw":9,"lm":2.0,"lp25":0.5,"lp75":5.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"decreases_p","st":"weak"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Adverse sickness benefit decision triggers acute MH crisis (Barr et al. WCA harm).","citation":"Barr et al. 2016 (J Epidemiol Comm Health)","confidence":"H","batch":"29b"}},{"id":"le_0000213","a":"a_0022","b":"a_0014","p":0.04,"lw":12,"lm":2.0,"lp25":0.5,"lp75":6.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Adverse sickness benefit decision raises suicide-attempt risk (Barr et al.; Coroners' PFD reports).","citation":"Barr et al. 2016; Coroners' Prevention of Future Deaths","confidence":"H","batch":"29b"}},{"id":"le_0000214","a":"a_0009","b":"a_0013","p":0.06,"lw":24,"lm":6.0,"lp25":2.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Chronic illness onset triggers acute MH crisis (comorbid depression OR ~2).","citation":"Naylor et al. 2012 King's Fund; Moussavi et al. 2007","confidence":"H","batch":"29c"}},{"id":"le_0000215","a":"a_0009","b":"a_0007","p":0.08,"lw":24,"lm":9.0,"lp25":3.0,"lp75":18.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Chronic illness onset raises risk of job loss.","citation":"OECD Sickness Disability and Work; ONS","confidence":"H","batch":"29c"}},{"id":"le_0000216","a":"a_0033","b":"a_0013","p":0.1,"lw":24,"lm":6.0,"lp25":2.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Severe chronic pain triggers acute MH crisis (RR ~3 for major depression).","citation":"Tunks et al. 2008; NICE CG88","confidence":"H","batch":"29c"}},{"id":"le_0000217","a":"a_0033","b":"a_0007","p":0.12,"lw":24,"lm":9.0,"lp25":3.0,"lp75":18.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Severe chronic pain raises risk of job loss.","citation":"ONS chronic pain economic burden; CIPD","confidence":"H","batch":"29c"}},{"id":"le_0000218","a":"a_0026","b":"a_0013","p":0.15,"lw":18,"lm":4.0,"lp25":1.0,"lp75":10.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Severe progressive condition diagnosis triggers acute MH crisis (cancer/MND/MS/dementia).","citation":"Mitchell et al. 2011 (Lancet Oncology); UK MS/MND reviews","confidence":"H","batch":"29c"}},{"id":"le_0000219","a":"a_0026","b":"a_0010","p":0.1,"lw":36,"lm":18.0,"lp25":8.0,"lp75":30.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Severe progressive condition leads (over time) to loss of informal carer via caregiver burnout.","citation":"Carers UK State of Caring 2023; ADASS pathways","confidence":"M","batch":"29c"}},{"id":"le_0000220","a":"a_0023","b":"a_0013","p":0.1,"lw":18,"lm":6.0,"lp25":2.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Pregnancy/postnatal period triggers acute MH crisis (perinatal MH).","citation":"NICE CG192; Howard et al. 2014 (Lancet); MBRRACE-UK","confidence":"H","batch":"29c"}},{"id":"le_0000221","a":"a_0037","b":"a_0013","p":0.2,"lw":12,"lm":3.0,"lp25":1.0,"lp75":7.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Acquired brain injury triggers acute MH crisis (post-TBI MH RR very elevated).","citation":"Whelan-Goodinson et al. 2009; Headway / UKABIF","confidence":"H","batch":"29c"}},{"id":"le_0000222","a":"a_0020","b":"a_0013","p":0.15,"lw":18,"lm":4.0,"lp25":1.0,"lp75":10.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Workplace injury with disability onset triggers acute MH crisis.","citation":"HSE Workplace Injury data","confidence":"M","batch":"29c"}},{"id":"le_0000223","a":"a_0020","b":"a_0007","p":0.3,"lw":18,"lm":6.0,"lp25":2.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Workplace injury with disability onset triggers job loss.","citation":"HSE statistics; ONS sickness/disability exit","confidence":"H","batch":"29c"}},{"id":"le_0000224","a":"a_0001","b":"a_0013","p":0.08,"lw":12,"lm":3.0,"lp25":1.0,"lp75":7.0,"ls":"lognormal","h":[{"s":"housing_stability_index","d":"decreases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Landlord-ended PRS tenancy triggers acute MH crisis.","citation":"Shelter housing stress surveys; Hoolachan & McKee 2019","confidence":"M","batch":"29d"}},{"id":"le_0000225","a":"a_0027","b":"a_0013","p":0.08,"lw":9,"lm":2.0,"lp25":0.5,"lp75":5.0,"ls":"lognormal","h":[{"s":"housing_stability_index","d":"decreases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Pre-eviction notice (S21/S8) triggers acute MH crisis.","citation":"Crisis / Shelter; Bramley 2017","confidence":"M","batch":"29d"}},{"id":"le_0000226","a":"a_0017","b":"a_0013","p":0.12,"lw":12,"lm":3.0,"lp25":1.0,"lp75":7.0,"ls":"lognormal","h":[{"s":"housing_stability_index","d":"decreases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"strong"}],"d":[],"fwd":true,"meta":{"description":"Mortgage repossession triggers acute MH crisis.","citation":"Pevalin 2009 (BMC Public Health)","confidence":"M","batch":"29d"}},{"id":"le_0000227","a":"a_0028","b":"a_0013","p":0.15,"lw":12,"lm":3.0,"lp25":1.0,"lp75":7.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Victim of serious crime triggers acute MH crisis.","citation":"Brewin et al. 1999; ONS Crime Survey","confidence":"H","batch":"29d"}},{"id":"le_0000228","a":"a_0028","b":"a_0014","p":0.03,"lw":24,"lm":6.0,"lp25":2.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Victim of serious crime raises suicide-attempt risk.","citation":"Dyregrov et al. 2003","confidence":"H","batch":"29d"}},{"id":"le_0000229","a":"a_0040","b":"a_0013","p":0.3,"lw":18,"lm":3.0,"lp25":1.0,"lp75":8.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Sexual exploitation / NRM referral triggers acute MH crisis.","citation":"Oram et al. 2016 (Lancet Psychiatry); NRM survivor data","confidence":"H","batch":"29d"}},{"id":"le_0000230","a":"a_0040","b":"a_0014","p":0.08,"lw":18,"lm":4.0,"lp25":1.0,"lp75":10.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Sexual exploitation / NRM raises suicide-attempt risk.","citation":"Salvation Army / Hestia survivor outcome data","confidence":"H","batch":"29d"}},{"id":"le_0000231","a":"a_0015","b":"a_0013","p":0.12,"lw":12,"lm":3.0,"lp25":1.0,"lp75":7.0,"ls":"lognormal","h":[{"s":"housing_stability_index","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Home crisis displacement (fire/flood/structural) triggers acute MH crisis.","citation":"British Red Cross emergency response evaluations","confidence":"M","batch":"29d"}},{"id":"le_0000232","a":"a_0019","b":"a_0013","p":0.1,"lw":36,"lm":9.0,"lp25":3.0,"lp75":24.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"substance_risk","d":"increases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Veteran discharge / transition triggers acute MH crisis (delayed onset typical).","citation":"Fear et al. 2010 (Lancet); RAND Wellbeing for Veterans","confidence":"M","batch":"29d"}},{"id":"le_0000233","a":"a_0019","b":"a_0014","p":0.04,"lw":36,"lm":12.0,"lp25":4.0,"lp75":30.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Veteran discharge raises suicide-attempt risk (MoD veteran suicide series).","citation":"MoD veteran suicide annual reports; Kapur et al.","confidence":"M","batch":"29d"}},{"id":"le_0000234","a":"a_0004","b":"a_0043","p":0.12,"lw":9,"lm":1.5,"lp25":0.5,"lp75":4.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Custody discharge triggers substance use onset (relapse / new dependence \u2014 extremely high in early-release window).","citation":"Binswanger et al. 2007 NEJM; PHE custody-release substance pathways","confidence":"H","batch":"30"}},{"id":"le_0000235","a":"a_0005","b":"a_0043","p":0.08,"lw":12,"lm":2.0,"lp25":0.5,"lp75":5.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Psychiatric inpatient discharge triggers substance use onset (comorbidity + self-medication).","citation":"Hunt et al. 2014 (comorbidity registry)","confidence":"M","batch":"30"}},{"id":"le_0000236","a":"a_0012","b":"a_0043","p":0.04,"lw":24,"lm":6.0,"lp25":2.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Bereavement triggers substance use onset (coping mechanism \u2014 well-evidenced in widow(er) literature).","citation":"Pilling et al. 2012 (BMJ); Stahl & Schulz 2014","confidence":"M","batch":"30"}},{"id":"le_0000237","a":"a_0003","b":"a_0043","p":0.06,"lw":18,"lm":4.0,"lp25":1.0,"lp75":10.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"DA incident triggers survivor substance use (self-medication pathway).","citation":"Devries et al. 2013 (PLoS Med); SafeLives data","confidence":"H","batch":"30"}},{"id":"le_0000238","a":"a_0028","b":"a_0043","p":0.04,"lw":18,"lm":4.0,"lp25":1.0,"lp75":10.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Victim of serious crime triggers substance use (trauma response).","citation":"Boyd et al. 2006; UK Crime Survey wellbeing supplement","confidence":"M","batch":"30"}},{"id":"le_0000239","a":"a_0007","b":"a_0043","p":0.03,"lw":24,"lm":6.0,"lp25":2.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[{"s":"area_deprivation_decile","c":"<=3","e":1.4}],"fwd":true,"meta":{"description":"Job loss triggers substance use onset (well-evidenced unemployment-substance pathway).","citation":"Henkel 2011 systematic review (Curr Drug Abuse Rev)","confidence":"H","batch":"30"}},{"id":"le_0000240","a":"a_0033","b":"a_0043","p":0.07,"lw":24,"lm":8.0,"lp25":3.0,"lp75":18.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Severe chronic pain triggers substance use onset (opioid pathway, well-documented).","citation":"Boscarino et al. 2010; NICE chronic pain guidelines","confidence":"H","batch":"30"}},{"id":"le_0000241","a":"a_0019","b":"a_0043","p":0.05,"lw":36,"lm":12.0,"lp25":4.0,"lp75":24.0,"ls":"lognormal","h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[{"s":"sex","c":"male","e":1.3}],"fwd":true,"meta":{"description":"Veteran discharge triggers substance use onset (transition + trauma).","citation":"Seal et al. 2011 (Drug Alcohol Depend); UK Veterans Health","confidence":"M","batch":"30"}},{"id":"le_0000242","a":"a_0043","b":"a_0013","p":0.15,"lw":18,"lm":4.0,"lp25":1.0,"lp75":10.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Substance use disorder triggers acute MH crisis (comorbid MH burden RR ~3).","citation":"Kessler et al. 1996; Schoeler et al. 2016 meta-analyses","confidence":"H","batch":"30"}},{"id":"le_0000243","a":"a_0043","b":"a_0014","p":0.04,"lw":24,"lm":6.0,"lp25":2.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Substance use disorder raises suicide-attempt risk (well-documented).","citation":"Wilcox et al. 2004 (Drug Alcohol Depend); ONS suicide statistics","confidence":"H","batch":"30"}},{"id":"le_0000244","a":"a_0043","b":"a_0007","p":0.08,"lw":36,"lm":12.0,"lp25":4.0,"lp75":24.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Substance use disorder triggers job loss (productivity decline + dismissal).","citation":"OECD Sickness Disability and Work; CIPD absence","confidence":"H","batch":"30"}},{"id":"le_0000245","a":"a_0043","b":"a_0029","p":0.1,"lw":24,"lm":6.0,"lp25":2.0,"lp75":14.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"credit_access_score","d":"decreases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Substance use disorder triggers cost-of-living shock (spend diversion + income loss).","citation":"Alcohol Change UK; CFP financial harm of addictions","confidence":"M","batch":"30"}},{"id":"le_0000246","a":"a_0043","b":"a_0006","p":0.07,"lw":36,"lm":12.0,"lp25":4.0,"lp75":24.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"}],"d":[],"fwd":true,"meta":{"description":"Substance use disorder triggers relationship breakdown.","citation":"Marshal 2003 meta-analysis; Adfam UK family impact data","confidence":"H","batch":"30"}},{"id":"le_0000247","a":"a_0043","b":"l_0017","p":0.45,"lw":12,"lm":2.0,"lp25":0.5,"lp75":6.0,"ls":"lognormal","h":[{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"increases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Substance use disorder triggers substance misuse service referral (engagement rate from PHE).","citation":"PHE Adult Substance Misuse Treatment 2023-24","confidence":"H","batch":"30"}},{"id":"le_0000248","a":"a_0043","b":"l_0007","p":0.15,"lw":24,"lm":6.0,"lp25":2.0,"lp75":14.0,"ls":"lognormal","h":[],"d":[],"fwd":true,"meta":{"description":"Substance use disorder triggers hospital admission (alcohol-related, drug-related).","citation":"NHS Statistics on Alcohol/Drug-related Admissions","confidence":"H","batch":"30"}},{"id":"le_0000249","a":"a_0043","b":"l_0056","p":0.012,"lw":36,"lm":18.0,"lp25":6.0,"lp75":30.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"strong"}],"d":[],"fwd":true,"meta":{"description":"Substance use disorder triggers drug-related / alcohol-related death (mortality rate).","citation":"ONS Drug Poisoning Deaths; PHE Alcohol-Specific Mortality","confidence":"H","batch":"30"}},{"id":"le_0000250","a":"l_0021","b":"a_0013","p":0.1,"lw":12,"lm":3.0,"lp25":1.0,"lp75":7.0,"ls":"lognormal","h":[{"s":"housing_stability_index","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Temporary accommodation placement triggers acute MH crisis (TA stays deteriorate MH).","citation":"Shelter MH-housing pathway; Crisis Homelessness Monitor","confidence":"H","batch":"31"}},{"id":"le_0000251","a":"l_0021","b":"a_0007","p":0.08,"lw":18,"lm":6.0,"lp25":2.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"TA placement triggers job loss (geographic disruption, instability, distance from work).","citation":"Bramley Homelessness Monitor; St Mungo's employment outcomes","confidence":"M","batch":"31"}},{"id":"le_0000252","a":"l_0046","b":"a_0013","p":0.15,"lw":18,"lm":6.0,"lp25":2.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"housing_stability_index","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Long-term TA (>12 months) triggers acute MH crisis (chronic precarity effect).","citation":"Bramley & Fitzpatrick 2018 Housing Studies","confidence":"H","batch":"31"}},{"id":"le_0000253","a":"l_0046","b":"a_0007","p":0.1,"lw":18,"lm":6.0,"lp25":2.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Long-term TA triggers job loss (sustained disruption).","citation":"Crisis Plan to End Homelessness","confidence":"M","batch":"31"}},{"id":"le_0000254","a":"l_0036","b":"a_0014","p":0.06,"lw":12,"lm":2.0,"lp25":0.5,"lp75":5.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Substance treatment dropout raises self-harm risk (treatment discontinuity is a critical-period risk factor).","citation":"PHE Adult Substance Misuse outcomes; Bohnert et al. 2012","confidence":"H","batch":"31"}},{"id":"le_0000255","a":"l_0036","b":"a_0013","p":0.08,"lw":12,"lm":2.0,"lp25":0.5,"lp75":5.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"Substance treatment dropout triggers acute MH crisis (relapse + comorbidity).","citation":"PHE Adult Substance Misuse Treatment outcomes 2023-24","confidence":"M","batch":"31"}},{"id":"le_0000256","a":"l_0012","b":"a_0013","p":0.15,"lw":9,"lm":2.0,"lp25":0.5,"lp75":5.0,"ls":"lognormal","h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"}],"d":[],"fwd":true,"meta":{"description":"Refuge placement triggers acute MH crisis (post-trauma + dislocation).","citation":"SafeLives Insights; Refuge service outcomes data","confidence":"H","batch":"31"}},{"id":"le_0000257","a":"l_0001","b":"a_0013","p":0.12,"lw":12,"lm":3.0,"lp25":1.0,"lp75":7.0,"ls":"lognormal","h":[{"s":"housing_stability_index","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"Homelessness duty acceptance triggers acute MH crisis (recognition is itself MH-deteriorating).","citation":"Crisis Health & Wellbeing review","confidence":"M","batch":"31"}},{"id":"le_0000258","a":"l_0008","b":"a_0007","p":0.1,"lw":18,"lm":6.0,"lp25":2.0,"lp75":12.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"weak"}],"d":[],"fwd":true,"meta":{"description":"AMHP / MHA assessment triggers job loss (workplace MH disclosure + absence escalation).","citation":"Stevenson/Farmer 2017 Thriving at Work; CIPD MH absence","confidence":"M","batch":"31"}},{"id":"le_0000259","a":"l_0045","b":"a_0007","p":0.15,"lw":12,"lm":3.0,"lp25":1.0,"lp75":7.0,"ls":"lognormal","h":[{"s":"financial_constraint_level","d":"increases_p","st":"moderate"}],"d":[],"fwd":true,"meta":{"description":"MHA detention readmission triggers job loss (sustained workplace absence).","citation":"NCISH UK MH employment outcomes","confidence":"H","batch":"31"}}],"antecedents":[{"id":"a_0042","name":"Onset of sustained food insecurity (food bank reliance, missing meals)","inc":2480000,"n_traj":279,"inc_pre_calibration_v011":3000000},{"id":"a_0029","name":"Cost-of-living income shock (sustained real-income reduction)","inc":1626000,"n_traj":228,"inc_pre_calibration_v011":2000000},{"id":"a_0007","name":"Job loss or redundancy","inc":1026863,"n_traj":177,"inc_pre_calibration_v011":1200000},{"id":"a_0025","name":"Onset of caring responsibility (becoming an informal carer)","inc":800000,"n_traj":144},{"id":"a_0024","name":"Recent immigration to UK (within 5 years)","inc":700000,"n_traj":135},{"id":"a_0023","name":"Pregnancy or postnatal period (perinatal window)","inc":640000,"n_traj":129},{"id":"a_0009","name":"Onset of chronic illness or disability","inc":600000,"n_traj":125},{"id":"a_0018","name":"Utility or essential bill crisis (energy/water disconnection or arrears at enforcement)","inc":600000,"n_traj":125},{"id":"a_0013","name":"Acute mental health crisis episode","inc":200000,"n_traj":114,"inc_pre_calibration_v011":500000},{"id":"a_0035","name":"Retirement transition (especially early or forced retirement)","inc":500000,"n_traj":114},{"id":"a_0033","name":"Onset of severe / disabling chronic pain","inc":300000,"n_traj":88},{"id":"a_0039","name":"Long-term unemployment (>12 months out of work)","inc":300000,"n_traj":88},{"id":"a_0001","name":"Landlord-ended private rented assured shorthold tenancy","inc":290000,"n_traj":87},{"id":"a_0006","name":"Non-violent relationship breakdown with partner","inc":232500,"n_traj":81,"inc_pre_calibration_v011":250000},{"id":"a_0012","name":"Bereavement of spouse or long-term partner","inc":250000,"n_traj":81},{"id":"a_0026","name":"Diagnosis of severe / progressive chronic condition (dementia, MND, MS, advanced cancer, organ failure)","inc":250000,"n_traj":81},{"id":"a_0014","name":"Suicide attempt or serious self-harm","inc":130690,"n_traj":72,"inc_pre_calibration_v011":200000},{"id":"a_0032","name":"Loss of council tax discount or administrative benefit change","inc":200000,"n_traj":72},{"id":"a_0027","name":"Pre-eviction notice received (S21, S8, or rent arrears stage 2)","inc":150000,"n_traj":62},{"id":"a_0037","name":"Acquired brain injury (TBI, stroke-related cognitive impact, hypoxia)","inc":150000,"n_traj":62},{"id":"a_0008","name":"Benefit sanction","inc":100000,"n_traj":51},{"id":"a_0028","name":"Becoming a victim of crime (serious \u2014 violent, sexual, fraud, hate)","inc":100000,"n_traj":51},{"id":"a_0034","name":"Identified as DA perpetrator (by police, court, or specialist service)","inc":100000,"n_traj":51},{"id":"a_0002","name":"Family or friends asking person to leave accommodation","inc":80000,"n_traj":46},{"id":"a_0004","name":"Discharge from custody","inc":80000,"n_traj":46},{"id":"a_0010","name":"Loss of informal carer","inc":55000,"n_traj":46,"inc_pre_calibration_v011":80000},{"id":"a_0003","name":"Domestic abuse incident requiring relocation","inc":50000,"n_traj":36},{"id":"a_0031","name":"Forced relocation by external factor (work, family, area)","inc":50000,"n_traj":36},{"id":"a_0036","name":"Onset of problem gambling","inc":50000,"n_traj":36},{"id":"a_0015","name":"Home crisis displacement (fire, flood, structural unsafe)","inc":30000,"n_traj":28},{"id":"a_0016","name":"Asylum support accommodation ending (move-on period)","inc":30000,"n_traj":28},{"id":"a_0020","name":"Workplace accident or injury with disability onset","inc":30000,"n_traj":28},{"id":"a_0022","name":"Long-term sickness benefit decision adverse to claimant (ESA/PIP/UC LCWRA refusal or roll-off)","inc":30000,"n_traj":28},{"id":"a_0041","name":"Frequent A&E attender flagged (5+ visits in 12 months)","inc":30000,"n_traj":28},{"id":"a_0040","name":"Sexual exploitation or trafficking (NRM referral)","inc":17000,"n_traj":21},{"id":"a_0019","name":"Discharge from UK armed forces (veteran transition to civilian life)","inc":15000,"n_traj":20},{"id":"a_0021","name":"Care leaver transition (exit from looked-after-children status at 18+)","inc":12000,"n_traj":18},{"id":"a_0030","name":"Discharge from immigration detention","inc":12000,"n_traj":18},{"id":"a_0005","name":"Discharge from psychiatric inpatient stay without secured accommodation","inc":10000,"n_traj":16},{"id":"a_0038","name":"Eviction or asked to leave supported housing / hostel","inc":10000,"n_traj":16},{"id":"a_0017","name":"Mortgage repossession","inc":5000,"n_traj":11},{"id":"a_0043","name":"Onset of substance use disorder (alcohol/drug dependence)","inc":164850,"n_traj":0,"inc_pre_calibration_v011":250000}],"total_traj_per_indiv":3003,"unit_costs":{"l_0001":{"cost_public":3296,"cost_la_only":3296,"confidence":"M","outcome":false,"type":"episode","label":"Homelessness duty acceptance","source":"DLUHC homelessness spend / duty count; Crisis estimates. 100% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":3296,"cost_la_only_2025":3296},"l_0002":{"cost_public":258,"cost_la_only":258,"confidence":"L","outcome":false,"type":"contact","label":"Rough sleeper outreach contact","source":"Homeless Link benchmarks. 100% LA (LA-commissioned)","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":258,"cost_la_only_2025":258},"l_0003":{"cost_public":88,"cost_la_only":88,"confidence":"M","outcome":false,"type":"episode","label":"Council tax liability order issued","source":"Magistrates fee + LA recovery admin. 100% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":88,"cost_la_only_2025":88},"l_0004":{"cost_public":632,"cost_la_only":632,"confidence":"H","outcome":false,"type":"award","label":"Discretionary Housing Payment award","source":"DWP DHP MYR 2024-25. LA-administered (central grant)","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":632,"cost_la_only_2025":632},"l_0015":{"cost_public":4120,"cost_la_only":2884,"confidence":"L","outcome":false,"type":"episode","label":"ASB civil injunction","source":"Civil legal aid + court (MoJ). 70% LA share","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":4120,"cost_la_only_2025":2884},"l_0016":{"cost_public":618,"cost_la_only":618,"confidence":"L","outcome":false,"type":"episode","label":"Community Protection Notice","source":"LA officer time. 100% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":618,"cost_la_only_2025":618},"l_0019":{"cost_public":1236,"cost_la_only":1236,"confidence":"L","outcome":false,"type":"episode","label":"Social housing allocation (admin)","source":"DLUHC LA expenditure benchmarks. 100% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":1236,"cost_la_only_2025":1236},"l_0021":{"cost_public":22042,"cost_la_only":8240,"confidence":"H","outcome":false,"type":"year","label":"Temporary accommodation placement","source":"Gross \u00a321.4K (DLUHC 2024-25). LA net cost after HB subsidy: ~\u00a38K national avg (much higher London ~\u00a315K, lower non-London ~\u00a33K)","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":22042,"cost_la_only_2025":8240},"l_0034":{"cost_public":3296,"cost_la_only":3296,"confidence":"M","outcome":false,"type":"episode","label":"Homelessness re-presentation <24mo","source":"Same basis as l_0001. 100% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":3296,"cost_la_only_2025":3296},"l_0039":{"cost_public":0,"cost_la_only":0,"confidence":"\u2014","outcome":true,"type":"outcome","label":"Tenancy sustained 12 months","source":"Outcome marker","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":0,"cost_la_only_2025":0},"l_0046":{"cost_public":27295,"cost_la_only":10300,"confidence":"M","outcome":false,"type":"year","label":"Long-term TA (>12 months)","source":"London Councils + LGA TA 2024-25. LA net higher % subsidy gap (nightly-paid) ~\u00a310K","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":27295,"cost_la_only_2025":10300},"l_0049":{"cost_public":1854,"cost_la_only":1112,"confidence":"L","outcome":false,"type":"episode","label":"ASB injunction breach","source":"Court (MoJ) + LA legal. 60% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":1854,"cost_la_only_2025":1112},"l_0008":{"cost_public":845,"cost_la_only":592,"confidence":"H","outcome":false,"type":"episode","label":"AMHP/MHA assessment","source":"PSSRU 2024. AMHP is LA-employed (70% LA), s12 doctors NHS (30%)","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":845,"cost_la_only_2025":592},"l_0009":{"cost_public":597,"cost_la_only":0,"confidence":"L","outcome":false,"type":"episode","label":"Section 136 detention","source":"Police + NHS s136 suite. 0% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":597,"cost_la_only_2025":0},"l_0010":{"cost_public":464,"cost_la_only":232,"confidence":"L","outcome":false,"type":"episode","label":"Section 117 aftercare planning (setup)","source":"Jointly funded LA + ICB. 50% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":464,"cost_la_only_2025":232},"l_0027":{"cost_public":1228,"cost_la_only":0,"confidence":"M","outcome":false,"type":"episode","label":"IAPT/Talking Therapies referral","source":"Mukuria et al. 2013. 0% LA (NHS-funded)","price_year_source":2013,"price_year_target":2025,"sppi_uplift":1.4,"cost_public_2025":1228,"cost_la_only_2025":0},"l_0037":{"cost_public":0,"cost_la_only":0,"confidence":"\u2014","outcome":true,"type":"outcome","label":"IAPT recovery","source":"Outcome marker","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":0,"cost_la_only_2025":0},"l_0038":{"cost_public":1228,"cost_la_only":0,"confidence":"M","outcome":false,"type":"episode","label":"IAPT non-recovery/dropout","source":"Same as l_0027. 0% LA","price_year_source":2013,"price_year_target":2025,"sppi_uplift":1.4,"cost_public_2025":1228,"cost_la_only_2025":0},"l_0044":{"cost_public":0,"cost_la_only":0,"confidence":"\u2014","outcome":true,"type":"outcome","label":"MHA released without detention","source":"Outcome marker","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":0,"cost_la_only_2025":0},"l_0045":{"cost_public":25235,"cost_la_only":0,"confidence":"M","outcome":false,"type":"episode","label":"MHA detention readmission <12mo","source":"NHS Reference Costs MH adult acute admission. 0% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":25235,"cost_la_only_2025":0},"l_0055":{"cost_public":6353,"cost_la_only":4970,"confidence":"M","outcome":false,"type":"mortality_response","label":"Suicide death (direct public-sector response)","source":"Samaritans 2024 disaggregated: NHS response \u00a3268 + police \u00a3988 + coroner \u00a34,518 per English suicide. LA bears coroner element. Excludes productivity & intangible costs","price_year_source":2022,"price_year_target":2025,"sppi_uplift":1.1,"cost_public_2025":6353,"cost_la_only_2025":4970},"l_0058":{"cost_public":1545,"cost_la_only":0,"confidence":"L","outcome":false,"type":"year","label":"Continuing IAPT extended course","source":"PSSRU 2024 HI continuation. 0% LA (NHS)","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":1545,"cost_la_only_2025":0},"l_0017":{"cost_public":391,"cost_la_only":391,"confidence":"L","outcome":false,"type":"episode","label":"Substance misuse service referral","source":"OHID/NDTMS. 100% LA (Public Health grant)","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":391,"cost_la_only_2025":391},"l_0035":{"cost_public":0,"cost_la_only":0,"confidence":"\u2014","outcome":true,"type":"outcome","label":"Substance treatment completion","source":"Outcome marker","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":0,"cost_la_only_2025":0},"l_0036":{"cost_public":2884,"cost_la_only":2884,"confidence":"L","outcome":false,"type":"episode","label":"Substance treatment dropout","source":"NDTMS. 100% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":2884,"cost_la_only_2025":2884},"l_0047":{"cost_public":0,"cost_la_only":0,"confidence":"\u2014","outcome":true,"type":"outcome","label":"Substance abstinence sustained 12mo","source":"Outcome marker","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":0,"cost_la_only_2025":0},"l_0056":{"cost_public":8250,"cost_la_only":6050,"confidence":"L","outcome":false,"type":"mortality_response","label":"Drug-related death (direct public-sector response)","source":"NHS A&E+ambulance ~\u00a31,500 + police ~\u00a3500 + coroner ~\u00a34,500 + forensic post-mortem ~\u00a31,000. LA bears coroner + most ancillary. Excludes productivity & intangible. Source basis: extrapolated from Samaritans suicide methodology + Black Review components","price_year_source":2022,"price_year_target":2025,"sppi_uplift":1.1,"cost_public_2025":8250,"cost_la_only_2025":6050},"l_0057":{"cost_public":3502,"cost_la_only":3502,"confidence":"M","outcome":false,"type":"year","label":"Continuing in substance treatment","source":"OHID/NDTMS. 100% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":3502,"cost_la_only_2025":3502},"l_0005":{"cost_public":494,"cost_la_only":494,"confidence":"H","outcome":false,"type":"episode","label":"Care Act s9 needs assessment","source":"PSSRU 2024. 100% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":494,"cost_la_only_2025":494},"l_0006":{"cost_public":1288,"cost_la_only":1288,"confidence":"M","outcome":false,"type":"episode","label":"Care Act s42 safeguarding enquiry","source":"PSSRU social worker + multi-agency. 100% LA (LA-led)","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":1288,"cost_la_only_2025":1288},"l_0007":{"cost_public":978,"cost_la_only":489,"confidence":"L","outcome":false,"type":"episode","label":"Hospital discharge with care package (admin)","source":"NHS D2A + LA commissioning. 50% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":978,"cost_la_only_2025":489},"l_0025":{"cost_public":48616,"cost_la_only":48616,"confidence":"H","outcome":false,"type":"year","label":"Care home admission, LA-funded","source":"LaingBuisson 2024. 100% LA (by definition for LA-funded placements)","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":48616,"cost_la_only_2025":48616},"l_0028":{"cost_public":299,"cost_la_only":299,"confidence":"M","outcome":false,"type":"episode","label":"Carers' assessment (Care Act s10)","source":"PSSRU 2024. 100% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":299,"cost_la_only_2025":299},"l_0029":{"cost_public":2163,"cost_la_only":2163,"confidence":"M","outcome":false,"type":"episode","label":"Reablement service intake","source":"PSSRU 2024. 100% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":2163,"cost_la_only_2025":2163},"l_0030":{"cost_public":45526,"cost_la_only":0,"confidence":"M","outcome":false,"type":"year","label":"NHS Continuing Healthcare (jointly funded)","source":"Lottie/Elder CHC. 0% LA when pure CHC (fully NHS-funded)","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":45526,"cost_la_only_2025":0},"l_0041":{"cost_public":0,"cost_la_only":0,"confidence":"\u2014","outcome":true,"type":"outcome","label":"Reablement successful completion","source":"Outcome marker","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":0,"cost_la_only_2025":0},"l_0042":{"cost_public":19055,"cost_la_only":19055,"confidence":"M","outcome":false,"type":"year","label":"Care package commenced post-s9","source":"LA ASC community package. 100% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":19055,"cost_la_only_2025":19055},"l_0048":{"cost_public":0,"cost_la_only":0,"confidence":"\u2014","outcome":true,"type":"outcome","label":"Care home death <12mo of admission","source":"Outcome marker","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":0,"cost_la_only_2025":0},"l_0050":{"cost_public":391,"cost_la_only":391,"confidence":"M","outcome":false,"type":"episode","label":"Care package review (escalation)","source":"PSSRU social worker. 100% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":391,"cost_la_only_2025":391},"l_0052":{"cost_public":0,"cost_la_only":0,"confidence":"\u2014","outcome":true,"type":"outcome","label":"Supporting Families programme completion","source":"Outcome marker","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":0,"cost_la_only_2025":0},"l_0053":{"cost_public":1957,"cost_la_only":1957,"confidence":"L","outcome":false,"type":"year","label":"Carer support package","source":"LA carer support. 100% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":1957,"cost_la_only_2025":1957},"l_0059":{"cost_public":19055,"cost_la_only":19055,"confidence":"M","outcome":false,"type":"year","label":"Care package continuing","source":"Continuation of l_0042. 100% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":19055,"cost_la_only_2025":19055},"l_0011":{"cost_public":2472,"cost_la_only":1236,"confidence":"L","outcome":false,"type":"episode","label":"MARAC referral","source":"SafeLives. ~50% LA (IDVA commissioning + LA chair) + police + health","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":2472,"cost_la_only_2025":1236},"l_0012":{"cost_public":9888,"cost_la_only":7910,"confidence":"M","outcome":false,"type":"episode","label":"Refuge placement (avg 12-week stay)","source":"Women's Aid. 80% LA (DLUHC + Supporting People + HB)","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":9888,"cost_la_only_2025":7910},"l_0014":{"cost_public":824,"cost_la_only":824,"confidence":"H","outcome":false,"type":"episode","label":"Supporting Families engagement","source":"DLUHC PbR. 100% LA-administered","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":824,"cost_la_only_2025":824},"l_0040":{"cost_public":2472,"cost_la_only":1236,"confidence":"L","outcome":false,"type":"episode","label":"Repeat MARAC referral <12mo","source":"Same as l_0011. 50% LA","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":2472,"cost_la_only_2025":1236},"l_0043":{"cost_public":0,"cost_la_only":0,"confidence":"\u2014","outcome":true,"type":"outcome","label":"MARAC case closed risk reduced","source":"Outcome marker","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":0,"cost_la_only_2025":0},"l_0013":{"cost_public":341,"cost_la_only":341,"confidence":"M","outcome":false,"type":"award","label":"Local Welfare Assistance award","source":"LGA LWA Review 2023. 100% LA","price_year_source":2023,"price_year_target":2025,"sppi_uplift":1.065,"cost_public_2025":341,"cost_la_only_2025":341},"l_0051":{"cost_public":632,"cost_la_only":632,"confidence":"H","outcome":false,"type":"award","label":"DHP renewed <12mo","source":"Same as l_0004. 100% LA-administered","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":632,"cost_la_only_2025":632},"l_0018":{"cost_public":1700,"cost_la_only":1700,"confidence":"M","outcome":false,"type":"episode","label":"Public health funeral arranged","source":"LGA Public Health Funerals 2024. 100% LA (55% partially recoverable from estate)","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":1700,"cost_la_only_2025":1700},"l_0054":{"cost_public":0,"cost_la_only":0,"confidence":"\u2014","outcome":true,"type":"outcome","label":"IAPT recovery sustained 12mo","source":"Outcome marker","price_year_source":2024,"price_year_target":2025,"sppi_uplift":1.03,"cost_public_2025":0,"cost_la_only_2025":0}},"antecedent_moderators":{"a_0001":{"h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"credit_access_score","d":"decreases_p","st":"moderate"},{"s":"executive_function","d":"decreases_p","st":"weak"}],"d":[{"s":"equivalised_household_income_decile","c":"<=3","e":2.5},{"s":"area_deprivation_decile","c":"<=3","e":1.8}]},"a_0002":{"h":[{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"conflict_intensity_romantic","d":"increases_p","st":"moderate"},{"s":"executive_function","d":"decreases_p","st":"weak"}],"d":[]},"a_0015":{"h":[],"d":[{"s":"area_deprivation_decile","c":"<=3","e":1.3}]},"a_0017":{"h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"credit_access_score","d":"decreases_p","st":"moderate"},{"s":"employment_security_level","d":"decreases_p","st":"moderate"}],"d":[{"s":"equivalised_household_income_decile","c":"<=4","e":1.6}]},"a_0027":{"h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"credit_access_score","d":"decreases_p","st":"moderate"}],"d":[{"s":"equivalised_household_income_decile","c":"<=3","e":2.2}]},"a_0038":{"h":[{"s":"executive_function","d":"decreases_p","st":"moderate"},{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"service_engagement_capacity","d":"decreases_p","st":"moderate"}],"d":[]},"a_0031":{"h":[],"d":[]},"a_0003":{"h":[{"s":"financial_constraint_level","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"conflict_intensity_romantic","d":"increases_p","st":"strong"}],"d":[{"s":"has_dependent_children","c":"true","e":1.3},{"s":"area_deprivation_decile","c":"<=3","e":1.5}]},"a_0006":{"h":[{"s":"conflict_intensity_romantic","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"weak"},{"s":"neuroticism","d":"increases_p","st":"weak"}],"d":[]},"a_0012":{"h":[],"d":[]},"a_0010":{"h":[{"s":"chronic_condition_load","d":"increases_p","st":"weak"}],"d":[]},"a_0025":{"h":[{"s":"social_support_capacity","d":"increases_p","st":"moderate"}],"d":[]},"a_0005":{"h":[{"s":"mental_health_service_use","d":"increases_p","st":"strong"},{"s":"executive_function","d":"decreases_p","st":"strong"},{"s":"service_engagement_capacity","d":"decreases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[]},"a_0013":{"h":[{"s":"neuroticism","d":"increases_p","st":"strong"},{"s":"mental_health_service_use","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"executive_function","d":"decreases_p","st":"moderate"},{"s":"substance_risk","d":"increases_p","st":"moderate"}],"d":[]},"a_0014":{"h":[{"s":"neuroticism","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"},{"s":"executive_function","d":"decreases_p","st":"moderate"},{"s":"substance_risk","d":"increases_p","st":"moderate"}],"d":[]},"a_0041":{"h":[{"s":"chronic_condition_load","d":"increases_p","st":"strong"},{"s":"neuroticism","d":"increases_p","st":"moderate"},{"s":"service_engagement_capacity","d":"increases_p","st":"moderate"},{"s":"substance_risk","d":"increases_p","st":"moderate"}],"d":[]},"a_0036":{"h":[{"s":"impulsivity_control","d":"decreases_p","st":"strong"},{"s":"executive_function","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"moderate"}],"d":[]},"a_0009":{"h":[{"s":"chronic_condition_load","d":"increases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"weak"}],"d":[{"s":"area_deprivation_decile","c":"<=3","e":1.4}]},"a_0020":{"h":[{"s":"employment_security_level","d":"decreases_p","st":"weak"},{"s":"impulsivity_control","d":"decreases_p","st":"weak"}],"d":[]},"a_0026":{"h":[{"s":"chronic_condition_load","d":"increases_p","st":"moderate"}],"d":[]},"a_0033":{"h":[{"s":"chronic_condition_load","d":"increases_p","st":"strong"}],"d":[{"s":"area_deprivation_decile","c":"<=3","e":1.3}]},"a_0037":{"h":[{"s":"substance_risk","d":"increases_p","st":"weak"}],"d":[{"s":"area_deprivation_decile","c":"<=3","e":1.3}]},"a_0023":{"h":[],"d":[]},"a_0007":{"h":[{"s":"employment_security_level","d":"decreases_p","st":"strong"},{"s":"executive_function","d":"decreases_p","st":"moderate"},{"s":"trust_in_institutions","d":"decreases_p","st":"weak"}],"d":[]},"a_0008":{"h":[{"s":"executive_function","d":"decreases_p","st":"strong"},{"s":"service_engagement_capacity","d":"decreases_p","st":"strong"},{"s":"trust_in_institutions","d":"decreases_p","st":"moderate"}],"d":[{"s":"area_deprivation_decile","c":"<=3","e":1.5}]},"a_0018":{"h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"credit_access_score","d":"decreases_p","st":"moderate"}],"d":[{"s":"equivalised_household_income_decile","c":"<=3","e":2.5}]},"a_0022":{"h":[{"s":"chronic_condition_load","d":"increases_p","st":"weak"},{"s":"service_engagement_capacity","d":"decreases_p","st":"moderate"},{"s":"executive_function","d":"decreases_p","st":"moderate"}],"d":[]},"a_0029":{"h":[{"s":"financial_constraint_level","d":"increases_p","st":"moderate"},{"s":"credit_access_score","d":"decreases_p","st":"moderate"}],"d":[{"s":"equivalised_household_income_decile","c":"<=4","e":1.8}]},"a_0032":{"h":[{"s":"executive_function","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"weak"}],"d":[]},"a_0039":{"h":[{"s":"employment_security_level","d":"decreases_p","st":"strong"},{"s":"executive_function","d":"decreases_p","st":"moderate"},{"s":"chronic_condition_load","d":"increases_p","st":"moderate"}],"d":[{"s":"area_deprivation_decile","c":"<=3","e":1.6}]},"a_0042":{"h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"credit_access_score","d":"decreases_p","st":"moderate"}],"d":[{"s":"equivalised_household_income_decile","c":"<=3","e":3.0},{"s":"has_dependent_children","c":"true","e":1.4}]},"a_0004":{"h":[{"s":"executive_function","d":"decreases_p","st":"moderate"},{"s":"substance_risk","d":"increases_p","st":"moderate"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[{"s":"area_deprivation_decile","c":"<=3","e":1.4}]},"a_0019":{"h":[],"d":[]},"a_0030":{"h":[],"d":[]},"a_0016":{"h":[],"d":[]},"a_0021":{"h":[{"s":"executive_function","d":"decreases_p","st":"weak"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"}],"d":[]},"a_0024":{"h":[],"d":[]},"a_0028":{"h":[],"d":[{"s":"area_deprivation_decile","c":"<=3","e":1.5}]},"a_0034":{"h":[{"s":"impulsivity_control","d":"decreases_p","st":"strong"},{"s":"conflict_intensity_romantic","d":"increases_p","st":"strong"},{"s":"substance_risk","d":"increases_p","st":"moderate"}],"d":[]},"a_0040":{"h":[{"s":"financial_constraint_level","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"strong"}],"d":[{"s":"area_deprivation_decile","c":"<=3","e":1.4}]},"a_0035":{"h":[],"d":[]},"a_0043":{"h":[{"s":"substance_risk","d":"increases_p","st":"strong"},{"s":"social_support_capacity","d":"decreases_p","st":"moderate"},{"s":"financial_constraint_level","d":"increases_p","st":"weak"},{"s":"mental_health_service_use","d":"decreases_p","st":"weak"},{"s":"executive_function","d":"decreases_p","st":"moderate"}],"d":[{"s":"age_band","c":"18-44","e":1.5},{"s":"sex","c":"male","e":1.7},{"s":"area_deprivation_decile","c":"<=3","e":1.6}]}},"state_exit_map":{"l_0039":["l_0021","l_0046"],"l_0035":["l_0057"],"l_0047":["l_0057"],"l_0037":["l_0058"],"l_0054":["l_0058"],"l_0041":["l_0042","l_0059"],"l_0048":["l_0025"]},"year_nodes":["l_0021","l_0046","l_0058","l_0057","l_0025","l_0030","l_0042","l_0053","l_0059"],"_version":"v0.12","_batch29a_appended":"2026-05-31","_batch29bcd_appended":"2026-05-31","_batch30_appended":"2026-05-31","_batch31_appended":"2026-05-31","_calibration_applied":"2026-05-31","_calibration_log":[{"id":"a_0042","name":"Onset of sustained food insecurity (food bank reli","original":3000000,"new":2480000,"cascade_contribution":520000,"service_contribution":0,"pct_reduction":17.3,"hit_floor":false},{"id":"a_0029","name":"Cost-of-living income shock (sustained real-income","original":2000000,"new":1626000,"cascade_contribution":374000,"service_contribution":0,"pct_reduction":18.7,"hit_floor":false},{"id":"a_0007","name":"Job loss or redundancy","original":1200000,"new":1026863,"cascade_contribution":173137,"service_contribution":20137,"pct_reduction":14.4,"hit_floor":false},{"id":"a_0013","name":"Acute mental health crisis episode","original":500000,"new":200000,"cascade_contribution":704454,"service_contribution":21754,"pct_reduction":60.0,"hit_floor":true},{"id":"a_0006","name":"Non-violent relationship breakdown with partner","original":250000,"new":232500,"cascade_contribution":17500,"service_contribution":0,"pct_reduction":7.0,"hit_floor":false},{"id":"a_0014","name":"Suicide attempt or serious self-harm","original":200000,"new":130690,"cascade_contribution":69310,"service_contribution":0,"pct_reduction":34.7,"hit_floor":false},{"id":"a_0010","name":"Loss of informal carer","original":80000,"new":55000,"cascade_contribution":25000,"service_contribution":0,"pct_reduction":31.2,"hit_floor":false},{"id":"a_0043","name":"Onset of substance use disorder (alcohol/drug depe","original":250000,"new":164850,"cascade_contribution":85150,"service_contribution":0,"pct_reduction":34.1,"hit_floor":false}]};
const UNIT_COSTS = PAYLOAD.unit_costs;
const ANT_MODS = PAYLOAD.antecedent_moderators;
const EXIT_MAP = PAYLOAD.state_exit_map;
const YEAR_NODES = new Set(PAYLOAD.year_nodes);
const STRENGTH_TO_COEF = {weak: 0.10, moderate: 0.30, strong: 0.70};
const HORIZON = 36;
const UK_ADULT_POP = 53_000_000;
const EDGES_BY_SRC = {};
for (const e of PAYLOAD.edges) (EDGES_BY_SRC[e.a] = EDGES_BY_SRC[e.a] || []).push(e);
const NODE_BY_ID = {};
for (const n of PAYLOAD.nodes) NODE_BY_ID[n.id] = n;
// ===========================================================================
// RNG + math helpers
// ===========================================================================
function mulberry32(seed) {
return function() {
seed |= 0; seed = seed + 0x6D2B79F5 | 0;
let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
function logit(p) { p = Math.min(Math.max(p, 1e-9), 1 - 1e-9); return Math.log(p / (1 - p)); }
function sigmoid(x) { return 1 / (1 + Math.exp(-x)); }
function matchDemo(c, v) {
if (v === undefined || v === null) return false;
c = String(c).trim().toLowerCase();
if (c === "true" || c === "false") return String(v).trim().toLowerCase() === c;
for (const op of ["<=", ">=", "==", "<", ">"]) {
if (c.startsWith(op)) {
const rhs = parseFloat(c.slice(op.length).trim());
const vv = parseFloat(v);
if (isNaN(rhs) || isNaN(vv)) return false;
if (op === "<") return vv < rhs;
if (op === "<=") return vv <= rhs;
if (op === ">") return vv > rhs;
if (op === ">=") return vv >= rhs;
if (op === "==") return vv === rhs;
}
}
return String(v).trim().toLowerCase() === c;
}
function gauss(rng) {
let u = 0, v = 0;
while (u === 0) u = rng();
while (v === 0) v = rng();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
function poisson(lambda, rng) {
if (lambda <= 0) return 0;
if (lambda < 30) {
const L = Math.exp(-lambda);
let k = 0, p = 1;
while (p > L) { k++; p *= rng(); }
return k - 1;
}
return Math.max(0, Math.round(lambda + Math.sqrt(lambda) * gauss(rng)));
}
// ===========================================================================
// HSV sampling — placeholder distribution (15/70/15 mix with continuous jitter)
// To be replaced when Matt supplies realistic HSV population
// ===========================================================================
const HSV_SLUGS = [
"social_support_capacity", "financial_constraint_level", "credit_access_score",
"executive_function", "trust_in_institutions", "service_engagement_capacity",
"geographic_anchor_strength", "neuroticism", "impulsivity_control",
"chronic_condition_load", "substance_risk", "mental_health_service_use",
"conflict_intensity_romantic", "employment_security_level",
];
function sampleHsvAndDemo(rng) {
const r = rng();
let center, demo;
let kBin; // for color bin
// Use currentLA's hsv_split if available, else default 15/70/15
const split = (typeof currentLA !== "undefined" && currentLA && currentLA.hsv_split) ? currentLA.hsv_split : {A: 0.15, C: 0.70, B: 0.15};
if (r < split.A) {
kBin = "A";
center = { social_support_capacity: 1.3, financial_constraint_level: -1.0,
credit_access_score: 1.0, executive_function: 0.6,
trust_in_institutions: 0.5, service_engagement_capacity: 0.5,
neuroticism: -0.4, impulsivity_control: 0.4,
employment_security_level: 0.6 };
demo = { has_dependent_children: rng() < 0.3 ? "true" : "false",
equivalised_household_income_decile: 6 + Math.floor(rng() * 4),
area_deprivation_decile: 6 + Math.floor(rng() * 4) };
} else if (r < split.A + split.C) {
kBin = "C";
center = {};
demo = { has_dependent_children: rng() < 0.35 ? "true" : "false",
equivalised_household_income_decile: 3 + Math.floor(rng() * 5),
area_deprivation_decile: 3 + Math.floor(rng() * 5) };
} else {
kBin = "B";
center = { social_support_capacity: -1.3, financial_constraint_level: 1.3,
credit_access_score: -1.2, executive_function: -0.5,
trust_in_institutions: -0.8, service_engagement_capacity: -0.8,
neuroticism: 0.7, impulsivity_control: -0.3, substance_risk: 0.5,
chronic_condition_load: 0.4, employment_security_level: -0.6,
geographic_anchor_strength: 0.5 };
demo = { has_dependent_children: rng() < 0.5 ? "true" : "false",
equivalised_household_income_decile: 1 + Math.floor(rng() * 3),
area_deprivation_decile: 1 + Math.floor(rng() * 3) };
}
// Add jitter
const hsv = {};
for (const s of HSV_SLUGS) {
hsv[s] = (center[s] || 0) + 0.5 * gauss(rng);
}
return { hsv, demo, kBin };
}
// ===========================================================================
// SIMULATOR
// ===========================================================================
function edgeProb(edge, hsv, demo) {
let L = logit(edge.p);
for (const m of edge.h || []) {
const coef = STRENGTH_TO_COEF[m.st] || 0.30;
const sign = m.d === "increases_p" ? 1 : -1;
L += sign * coef * (hsv[m.s] || 0);
}
for (const m of edge.d || []) {
if (matchDemo(m.c, demo[m.s])) L += Math.log(m.e);
}
return sigmoid(L);
}
function antecedentRate(ant, hsv, demo) {
// Returns annual rate per person (HSV-conditioned + LA-incidence-scaled)
const base = ant.inc / UK_ADULT_POP;
// LA incidence scaling (applied at the population-rate level, not the individual moderator)
const laFactor = (typeof currentLA !== "undefined" && currentLA) ? (typeof antIncMultForLA === "function" ? antIncMultForLA(ant.id, currentLA) : 1.0) : 1.0;
const mod = ANT_MODS[ant.id];
if (!mod) return base * laFactor;
let logmult = 0;
for (const m of mod.h || []) {
const coef = STRENGTH_TO_COEF[m.st] || 0.30;
const sign = m.d === "increases_p" ? 1 : -1;
logmult += sign * coef * (hsv[m.s] || 0);
}
for (const m of mod.d || []) {
if (matchDemo(m.c, demo[m.s])) logmult += Math.log(m.e);
}
return base * laFactor * Math.exp(logmult);
}
function sampleLag(edge, rng) {
const window = edge.lw || HORIZON;
const shape = edge.ls || "lognormal";
let median = edge.lm; if (median == null) median = window / 4;
const p25 = edge.lp25, p75 = edge.lp75;
let s;
if (shape === "degenerate") s = median;
else if (shape === "exponential") s = -Math.max(median / Math.LN2, 0.1) * Math.log(rng() + 1e-12);
else if (shape === "truncated_normal") {
const sd = (p25 && p75 && p75 > p25) ? (p75 - p25) / 1.349 : Math.max(median / 3, 0.5);
for (let i = 0; i < 20; i++) { s = median + sd * gauss(rng); if (s >= 0) break; }
if (s < 0) s = Math.max(0, median);
} else if (shape === "uniform") s = rng() * window;
else {
const mu = Math.log(Math.max(median, 1e-6));
let sigma = 0.5;
if (p25 && p75 && p75 > p25 && p25 > 0) sigma = Math.min(Math.max(Math.log(p75 / p25) / 1.349, 0.05), 2.0);
s = Math.exp(mu + sigma * gauss(rng));
}
return Math.max(0, Math.min(s, window));
}
function cascadeFromAntecedent(seed, t_start, hsv, demo, rng) {
const transits = [];
const queue = [{t: t_start, node: seed}];
let head = 0;
while (head < queue.length && queue.length < 80) {
const {t, node} = queue[head++];
for (const e of EDGES_BY_SRC[node] || []) {
if (rng() >= edgeProb(e, hsv, demo)) continue;
const lag = sampleLag(e, rng);
const tnext = t + lag;
if (tnext > HORIZON) continue;
transits.push({from: node, to: e.b, depart: t, arrive: tnext, fwd: !!e.fwd, eid: e.id});
queue.push({t: tnext, node: e.b});
}
}
return transits;
}
// Simulate one person — sample events, run cascades, compute cost with duration handling
function simulatePerson(pid, antecedents, rng) {
const { hsv, demo, kBin } = sampleHsvAndDemo(rng);
// 1. Sample antecedent events
const events = [];
for (const ant of antecedents) {
const annualRate = antecedentRate(ant, hsv, demo);
const lambda = annualRate * (HORIZON / 12);
const n = poisson(lambda, rng);
for (let i = 0; i < n; i++) events.push({ant_id: ant.id, t: rng() * HORIZON});
}
events.sort((a, b) => a.t - b.t);
// 2. Run cascades (tag each transit with the originating event index for chain analytics)
const transits = [];
for (let evi = 0; evi < events.length; evi++) {
const ev = events[evi];
const cas = cascadeFromAntecedent(ev.ant_id, ev.t, hsv, demo, rng);
for (const tr of cas) {
tr.k = kBin;
tr.pid = pid;
tr.evi = evi; // event index within this person
tr.ant = ev.ant_id; // originating antecedent
transits.push(tr);
}
}
// 3. Compute cost with duration handling
const sorted = [...transits].sort((a, b) => a.arrive - b.arrive);
let cost_public = 0, cost_la = 0;
const portfolio_cost = {}; // portfolio -> {public, la}
const per_node_cost = {}; // node_id -> {public, la, n_touches}
const state_intervals = {}; // node_id -> [{open, target_close, closed}]
function addNode(node_id, c_pub, c_la) {
cost_public += c_pub; cost_la += c_la;
const port = (NODE_BY_ID[node_id] || {p: "other"}).p;
if (!portfolio_cost[port]) portfolio_cost[port] = {public: 0, la: 0};
portfolio_cost[port].public += c_pub;
portfolio_cost[port].la += c_la;
if (!per_node_cost[node_id]) per_node_cost[node_id] = {public: 0, la: 0, n_touches: 0};
per_node_cost[node_id].public += c_pub;
per_node_cost[node_id].la += c_la;
per_node_cost[node_id].n_touches += 1;
}
for (const tr of sorted) {
const target = tr.to;
const uc = UNIT_COSTS[target];
if (!uc) continue;
if (YEAR_NODES.has(target)) {
// Open or extend interval
if (!state_intervals[target]) state_intervals[target] = [];
const openIv = state_intervals[target].find(iv => !iv.closed);
if (openIv) {
// Extend
openIv.target_close = Math.min(HORIZON, tr.arrive + 12);
} else {
state_intervals[target].push({
open: tr.arrive,
target_close: Math.min(HORIZON, tr.arrive + 12),
closed: false,
});
}
} else {
// Discrete cost
addNode(target, uc.cost_public, uc.cost_la_only);
}
// Exit signals
if (EXIT_MAP[target]) {
for (const exitNode of EXIT_MAP[target]) {
const iv = state_intervals[exitNode]?.find(iv => !iv.closed);
if (iv && tr.arrive > iv.open) {
iv.target_close = tr.arrive;
iv.closed_early = true;
}
}
}
}
// Close all open intervals; charge duration cost
for (const [nodeId, intervals] of Object.entries(state_intervals)) {
const uc = UNIT_COSTS[nodeId];
if (!uc) continue;
const monthlyPub = uc.cost_public / 12;
const monthlyLa = uc.cost_la_only / 12;
for (const iv of intervals) {
const close = Math.min(HORIZON, iv.target_close);
const duration = Math.max(0, close - iv.open);
addNode(nodeId, duration * monthlyPub, duration * monthlyLa);
iv.closed = true;
}
}
return { pid, hsv, demo, kBin, events, transits, cost_public, cost_la, portfolio_cost, per_node_cost };
}
// ===========================================================================
// Layout (Sankey + recurrence arcs)
// ===========================================================================
const PORTFOLIO_COLORS = {
housing: "#3b82f6", mental_health: "#a855f7", substance: "#f97316",
adult_social_care: "#14b8a6", domestic_abuse: "#ec4899",
welfare: "#22c55e", antecedent: "#475569", other: "#94a3b8",
};
const PORTFOLIO_LABELS = {
housing: "Housing", mental_health: "Mental health", substance: "Substance",
adult_social_care: "Adult social care", domestic_abuse: "Domestic abuse",
welfare: "Welfare", other: "Other",
};
const COLOR_RGB = { A: [56, 189, 248], C: [250, 204, 21], B: [248, 113, 113] };
const svg = d3.select("#viz");
const canvas = document.getElementById("particles");
const ctx = canvas.getContext("2d");
// Use a fixed logical viewBox. SVG content scales to fit the rendered container,
// and the canvas is resized to match the same rendered size — both share the same
// viewBox coord system (0..W, 0..H), so getPointAtLength results map cleanly.
const W = 1320;
const H = 920;
// preserveAspectRatio="none" makes SVG stretch to fill its container exactly,
// matching how the canvas behaves under our setTransform(cssW/W, cssH/H) mapping.
// Default ("xMidYMid meet") would centre the SVG content inside the container —
// leaving vertical padding on wide screens — while the canvas uses uniform
// stretch, so particles would appear above where the nodes are drawn.
svg.attr("viewBox", `0 0 ${W} ${H}`)
.attr("preserveAspectRatio", "none");
const dpr = window.devicePixelRatio || 1;
// v1.1 fix for wandering dots: the canvas was being pinned to 1320x920 inline,
// while CSS width:100% stretched the SVG to actual container width. Particles
// drawn in viewBox coords landed on the wrong screen pixels. Solution: keep
// the canvas backing-buffer in sync with whatever CSS size it's rendered at,
// and use a transform that maps viewBox coords → canvas pixels.
function resizeCascadeCanvas() {
const rect = canvas.getBoundingClientRect();
const cssW = rect.width || W;
const cssH = rect.height || H;
if (cssW < 10 || cssH < 10) return false; // hidden tab, skip
// Set internal pixel buffer to match CSS size × device pixel ratio
canvas.width = Math.round(cssW * dpr);
canvas.height = Math.round(cssH * dpr);
// Reset and set transform so drawing in (0..W, 0..H) viewBox coords maps to canvas pixels
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale((cssW / W) * dpr, (cssH / H) * dpr);
return true;
}
// Initial sizing attempt — will succeed only if #page-live is currently visible.
// If the cascade page is hidden at script-load time, this returns false and we
// re-attempt when the user navigates to the tab (wired in switchToTab handler).
resizeCascadeCanvas();
// Also re-measure on window resize so the cascade stays aligned at any breakpoint
window.addEventListener("resize", () => { if (document.body.getAttribute("data-tab") === "live") resizeCascadeCanvas(); });
let POP_SIZE = 100_000;
let PEOPLE = [];
let TRANSITS_FLAT = []; // flattened, for animation
let nodePos = {};
let edgePath = {};
let edgePathLength = {};
let recurrencePath = {};
let recurrencePathLength = {};
let currentTime = 0;
let playing = false;
let lastFrameTime = null;
let playbackSpeed = 1.0;
let recMode = "show";
function buildLayout() {
const tier = {};
const q = [];
for (const n of PAYLOAD.nodes) if (n.id.startsWith("a_")) { tier[n.id] = 0; q.push(n.id); }
let head = 0;
while (head < q.length) {
const n = q[head++];
for (const e of EDGES_BY_SRC[n] || []) {
if (!(e.b in tier)) { tier[e.b] = tier[n] + 1; q.push(e.b); }
else if (tier[n] + 1 < tier[e.b]) { tier[e.b] = tier[n] + 1; q.push(e.b); }
}
}
const reachable = new Set(Object.keys(tier));
const nodes = PAYLOAD.nodes.filter(n => reachable.has(n.id)).map(n => ({...n, tier: tier[n.id]}));
const reachableEdges = PAYLOAD.edges.filter(e => reachable.has(e.a) && reachable.has(e.b));
// v1.1 fix: exclude antecedent→antecedent edges from sankey layout.
// They drive the simulation cost (compounding cascades) but should not push
// receiving antecedents off the left column. Treat them like recurrence edges
// for rendering purposes — invisible in the main view.
const isAA = e => e.a.startsWith("a_") && e.b.startsWith("a_");
const forwardEdges = reachableEdges.filter(e => e.fwd && !isAA(e));
const recurrenceEdges = reachableEdges.filter(e => !e.fwd);
const aaEdges = reachableEdges.filter(e => e.fwd && isAA(e));
const nodeIndex = new Map(nodes.map((n, i) => [n.id, i]));
const sankeyLinks = forwardEdges.map(e => ({ source: nodeIndex.get(e.a), target: nodeIndex.get(e.b), value: e.p, edgeId: e.id }));
const sk = d3.sankey().nodeWidth(10).nodePadding(3)
.extent([[170, 12], [W - 240, H - 16]]).nodeAlign(d3.sankeyJustify);
return { layout: sk({ nodes: nodes.map(n => ({...n})), links: sankeyLinks.map(l => ({...l})) }), recurrenceEdges, aaEdges };
}
function renderStatic(layoutPkg) {
const { layout, recurrenceEdges } = layoutPkg;
svg.selectAll("*").remove();
edgePath = {}; edgePathLength = {}; recurrencePath = {}; recurrencePathLength = {};
nodePos = {};
for (const n of layout.nodes) {
nodePos[n.id] = { x0: n.x0, x1: n.x1, y0: n.y0, y1: n.y1, cx: (n.x0+n.x1)/2, cy: (n.y0+n.y1)/2 };
}
const recL = svg.append("g");
const edges = svg.append("g");
const nodesL = svg.append("g");
const labels = svg.append("g");
const pathGen = d3.sankeyLinkHorizontal();
edges.selectAll("path").data(layout.links).join("path")
.attr("class", "edge-path").attr("d", pathGen)
.attr("stroke-width", d => Math.max(0.6, d.width * 0.3))
.each(function(d) { edgePath[d.edgeId] = this; edgePathLength[d.edgeId] = this.getTotalLength(); });
for (const e of recurrenceEdges) {
const f = nodePos[e.a], t = nodePos[e.b];
if (!f || !t) continue;
const x1 = f.cx, y1 = f.cy, x2 = t.cx, y2 = t.cy;
const dx = x2 - x1;
const midX = (x1 + x2) / 2, midY = (y1 + y2) / 2;
const arcOffset = -Math.min(160, Math.max(40, Math.abs(dx) * 0.25 + 30));
const cpY = Math.max(8, midY + arcOffset);
const d = `M${x1},${y1} Q${midX},${cpY} ${x2},${y2}`;
const path = recL.append("path").attr("class", "recurrence-arc").attr("d", d).attr("stroke-width", 0.9).node();
recurrencePath[e.id] = path; recurrencePathLength[e.id] = path.getTotalLength();
}
nodesL.selectAll("rect").data(layout.nodes).join("rect")
.attr("class", "node-rect")
.attr("x", d => d.x0).attr("y", d => d.y0)
.attr("width", d => d.x1 - d.x0).attr("height", d => Math.max(2, d.y1 - d.y0))
.attr("fill", d => PORTFOLIO_COLORS[d.p] || PORTFOLIO_COLORS.other);
labels.selectAll("text").data(layout.nodes).join("text")
.attr("class", d => d.id.startsWith("a_") ? "ant-label" : "node-label")
.attr("x", d => d.x0 < W/2 ? d.x1 + 3 : d.x0 - 3)
.attr("y", d => (d.y0 + d.y1) / 2)
.attr("dy", "0.32em")
.attr("text-anchor", d => d.x0 < W/2 ? "start" : "end")
.text(d => d.n.length > 36 ? d.n.slice(0, 35) + "…" : d.n);
}
// ===========================================================================
// Simulation runner — population-scaled with weighting
// ===========================================================================
let SCALE_WEIGHT = 1; // each simulated person represents N real people
let aggregate = null;
async function runSimulation() {
const loading = document.getElementById("loading");
const fill = document.getElementById("progressFill");
const msg = document.getElementById("progressMsg");
loading.classList.remove("hidden");
PEOPLE = [];
TRANSITS_FLAT = [];
// For 1M pop, sample 100K and weight 10×. For 10K pop, sample directly.
const N_SAMPLE = Math.min(100_000, POP_SIZE);
SCALE_WEIGHT = POP_SIZE / N_SAMPLE;
msg.textContent = `Sampling ${N_SAMPLE.toLocaleString()} people (each ≈ ${SCALE_WEIGHT.toFixed(1)} real adults)…`;
await new Promise(r => setTimeout(r, 30));
const rng = mulberry32(42);
const antecedents = PAYLOAD.antecedents;
const CHUNK = 5000;
for (let start = 0; start < N_SAMPLE; start += CHUNK) {
const end = Math.min(start + CHUNK, N_SAMPLE);
for (let i = start; i < end; i++) {
const p = simulatePerson(i, antecedents, rng);
PEOPLE.push(p);
for (const tr of p.transits) TRANSITS_FLAT.push(tr);
}
fill.style.width = (100 * end / N_SAMPLE).toFixed(0) + "%";
msg.textContent = `Simulating people: ${end.toLocaleString()}/${N_SAMPLE.toLocaleString()}`;
await new Promise(r => setTimeout(r, 0));
}
// Pre-compute aggregate cost (with scale weight applied)
aggregate = aggregatePeople(PEOPLE, SCALE_WEIGHT);
loading.classList.add("hidden");
}
function aggregatePeople(people, weight) {
let total_public = 0, total_la = 0;
let n_touched = 0;
const per_node = {}; // node -> {public, la, n_touches}
const per_portfolio = {}; // portfolio -> {public, la}
const person_costs = []; // for long-tail histogram
let total_events = 0;
for (const p of people) {
total_public += p.cost_public * weight;
total_la += p.cost_la * weight;
total_events += p.events.length;
if (p.cost_public > 0) n_touched++;
person_costs.push(p.cost_public); // weighted later in histogram
for (const [nid, c] of Object.entries(p.per_node_cost)) {
if (!per_node[nid]) per_node[nid] = {public: 0, la: 0, n_touches: 0};
per_node[nid].public += c.public * weight;
per_node[nid].la += c.la * weight;
per_node[nid].n_touches += c.n_touches * weight;
}
for (const [port, c] of Object.entries(p.portfolio_cost)) {
if (!per_portfolio[port]) per_portfolio[port] = {public: 0, la: 0};
per_portfolio[port].public += c.public * weight;
per_portfolio[port].la += c.la * weight;
}
}
// Concentration: top 5% share
const sortedCosts = [...person_costs].sort((a, b) => b - a);
const top5pct = sortedCosts.slice(0, Math.max(1, Math.floor(sortedCosts.length * 0.05)));
const top5sum = top5pct.reduce((s, c) => s + c, 0);
const allsum = sortedCosts.reduce((s, c) => s + c, 0);
const concentration = allsum > 0 ? top5sum / allsum : 0;
return {
total_public, total_la, n_touched, n_total: people.length,
per_node, per_portfolio, person_costs, total_events, weight,
concentration,
};
}
// ===========================================================================
// DISPLAY
// ===========================================================================
function formatMoney(v) {
if (v >= 1e9) return "£" + (v / 1e9).toFixed(2) + "bn";
if (v >= 1e6) return "£" + (v / 1e6).toFixed(2) + "m";
if (v >= 1e3) return "£" + (v / 1e3).toFixed(1) + "k";
return "£" + Math.round(v).toLocaleString();
}
function formatInt(v) {
if (v >= 1e6) return (v / 1e6).toFixed(2) + "m";
if (v >= 1e3) return (v / 1e3).toFixed(1) + "k";
return Math.round(v).toLocaleString();
}
// Compute aggregate at given playback time (cumulative arrivals up to time)
function aggregateAtTime(time, scale) {
let total_public = 0, total_la = 0;
let n_touched = 0;
const per_node = {};
const per_portfolio = {};
const person_costs_at_t = new Array(PEOPLE.length).fill(0);
let total_events = 0;
const total_eventCounts = {}; // for ant rate display
for (let pi = 0; pi < PEOPLE.length; pi++) {
const p = PEOPLE[pi];
// Events occurring up to time t
for (const ev of p.events) if (ev.t <= time) total_events++;
// Recompute cost up to time t using simplified incremental logic
// For efficiency, we recompute the duration-aware cost on the transit prefix
const transitsBefore = p.transits.filter(tr => tr.arrive <= time);
if (transitsBefore.length === 0) continue;
const sorted = transitsBefore.sort((a, b) => a.arrive - b.arrive);
let cp = 0, cl = 0;
const stateIv = {};
const personPort = {};
const personNodes = {};
const addNode = (nid, c_pub, c_la) => {
cp += c_pub; cl += c_la;
const port = (NODE_BY_ID[nid] || {p: "other"}).p;
if (!personPort[port]) personPort[port] = {public: 0, la: 0};
personPort[port].public += c_pub; personPort[port].la += c_la;
if (!personNodes[nid]) personNodes[nid] = {public: 0, la: 0, n_touches: 0};
personNodes[nid].public += c_pub; personNodes[nid].la += c_la; personNodes[nid].n_touches += 1;
};
for (const tr of sorted) {
const target = tr.to;
const uc = UNIT_COSTS[target];
if (!uc) continue;
if (YEAR_NODES.has(target)) {
if (!stateIv[target]) stateIv[target] = [];
const openIv = stateIv[target].find(iv => !iv.closed);
if (openIv) openIv.target_close = Math.min(HORIZON, tr.arrive + 12);
else stateIv[target].push({open: tr.arrive, target_close: Math.min(HORIZON, tr.arrive + 12), closed: false});
} else {
addNode(target, uc.cost_public, uc.cost_la_only);
}
if (EXIT_MAP[target]) {
for (const en of EXIT_MAP[target]) {
const iv = stateIv[en]?.find(iv => !iv.closed);
if (iv && tr.arrive > iv.open) iv.target_close = tr.arrive;
}
}
}
// Close open intervals at min(time, target_close) — we close at NOW since simulation is at currentTime
for (const [nid, ivs] of Object.entries(stateIv)) {
const uc = UNIT_COSTS[nid];
if (!uc) continue;
const mpub = uc.cost_public / 12, mla = uc.cost_la_only / 12;
for (const iv of ivs) {
const close = Math.min(time, Math.min(HORIZON, iv.target_close));
const dur = Math.max(0, close - iv.open);
addNode(nid, dur * mpub, dur * mla);
iv.closed = true;
}
}
total_public += cp * scale;
total_la += cl * scale;
if (cp > 0) n_touched++;
person_costs_at_t[pi] = cp;
for (const [nid, c] of Object.entries(personNodes)) {
if (!per_node[nid]) per_node[nid] = {public: 0, la: 0, n_touches: 0};
per_node[nid].public += c.public * scale;
per_node[nid].la += c.la * scale;
per_node[nid].n_touches += c.n_touches * scale;
}
for (const [port, c] of Object.entries(personPort)) {
if (!per_portfolio[port]) per_portfolio[port] = {public: 0, la: 0};
per_portfolio[port].public += c.public * scale;
per_portfolio[port].la += c.la * scale;
}
}
const sortedCosts = [...person_costs_at_t].sort((a, b) => b - a);
const top5 = sortedCosts.slice(0, Math.max(1, Math.floor(sortedCosts.length * 0.05)));
const top5sum = top5.reduce((s, c) => s + c, 0);
const allsum = sortedCosts.reduce((s, c) => s + c, 0);
const concentration = allsum > 0 ? top5sum / allsum : 0;
return { total_public, total_la, n_touched, n_total: PEOPLE.length, per_node, per_portfolio,
person_costs: person_costs_at_t, total_events, weight: scale, concentration };
}
const totPublicEl = document.getElementById("totPublic");
const totLaEl = document.getElementById("totLa");
const footPublicEl = document.getElementById("footPublic");
const footLaEl = document.getElementById("footLa");
const tbodyEl = document.getElementById("costTbody");
const portTbodyEl = document.getElementById("portTbody");
const pctTouchedEl = document.getElementById("pctTouched");
const avgPerTouchedEl = document.getElementById("avgPerTouched");
const concentrationEl = document.getElementById("concentration");
const totalEventsEl = document.getElementById("totalEvents");
function updateDisplays(agg) {
totPublicEl.textContent = formatMoney(agg.total_public);
totLaEl.textContent = formatMoney(agg.total_la);
footPublicEl.textContent = formatMoney(agg.total_public);
footLaEl.textContent = formatMoney(agg.total_la);
const popReal = agg.n_total * SCALE_WEIGHT;
const touchedReal = agg.n_touched * SCALE_WEIGHT;
const pct = popReal > 0 ? (touchedReal / popReal) : 0;
pctTouchedEl.textContent = (pct * 100).toFixed(1) + "% (" + formatInt(touchedReal) + ")";
const avgPer = touchedReal > 0 ? agg.total_public / touchedReal : 0;
avgPerTouchedEl.textContent = formatMoney(avgPer);
concentrationEl.textContent = (agg.concentration * 100).toFixed(0) + "% of cost";
totalEventsEl.textContent = formatInt(agg.total_events * SCALE_WEIGHT);
// Cost table
const rows = [];
for (const [nid, c] of Object.entries(agg.per_node)) {
if (nid.startsWith("a_")) continue;
if (c.public === 0 && c.la === 0) continue;
rows.push({id: nid, n: (NODE_BY_ID[nid] || {n: nid}).n, portfolio: (NODE_BY_ID[nid] || {p: "other"}).p, public: c.public, la: c.la});
}
rows.sort((a, b) => b.public - a.public);
if (rows.length === 0) {
tbodyEl.innerHTML = '<tr><td colspan="3" class="empty">No costed touches yet.</td></tr>';
} else {
tbodyEl.innerHTML = rows.map(r => `
<tr>
<td><span class="swatch" style="background:${PORTFOLIO_COLORS[r.portfolio] || PORTFOLIO_COLORS.other}"></span><span class="nm" title="${r.n}">${r.n}</span></td>
<td class="public">${formatMoney(r.public)}</td>
<td class="la">${formatMoney(r.la)}</td>
</tr>`).join("");
}
// Portfolio table
const portRows = [];
for (const [port, c] of Object.entries(agg.per_portfolio)) {
portRows.push({port, label: PORTFOLIO_LABELS[port] || port, public: c.public, la: c.la});
}
portRows.sort((a, b) => b.public - a.public);
if (portRows.length === 0) {
portTbodyEl.innerHTML = '<tr><td colspan="3" class="empty">—</td></tr>';
} else {
portTbodyEl.innerHTML = portRows.map(r => `
<tr>
<td><span class="swatch" style="background:${PORTFOLIO_COLORS[r.port] || PORTFOLIO_COLORS.other}"></span><span class="nm">${r.label}</span></td>
<td class="public">${formatMoney(r.public)}</td>
<td class="la">${formatMoney(r.la)}</td>
</tr>`).join("");
}
// Long-tail histogram (log-scale)
renderLongTail(agg.person_costs);
}
function renderLongTail(costs) {
const svgLT = d3.select("#longtail");
const w = svgLT.node().clientWidth || 420, h = 180;
svgLT.attr("viewBox", `0 0 ${w} ${h}`).selectAll("*").remove();
const positive = costs.filter(c => c > 0);
const zeroCount = costs.length - positive.length;
const margin = {top: 8, right: 12, bottom: 22, left: 12};
const iw = w - margin.left - margin.right;
const ih = h - margin.top - margin.bottom;
const g = svgLT.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
if (positive.length === 0) {
g.append("text").attr("x", iw/2).attr("y", ih/2).attr("text-anchor", "middle")
.attr("fill", "#5a6585").attr("font-size", 11).attr("font-style", "italic")
.text("No per-person costs yet.");
return;
}
const maxCost = d3.max(positive);
const minCost = Math.max(1, d3.min(positive));
const x = d3.scaleLog().domain([minCost, maxCost]).range([0, iw]).clamp(true);
const N_BINS = 28;
const logMin = Math.log(minCost), logMax = Math.log(maxCost);
const binStep = (logMax - logMin) / N_BINS;
const bins = new Array(N_BINS).fill(0);
for (const c of positive) {
const bi = Math.min(N_BINS - 1, Math.floor((Math.log(c) - logMin) / binStep));
bins[bi]++;
}
// Weight by SCALE_WEIGHT for "real" people axis
const weightedBins = bins.map(b => b * SCALE_WEIGHT);
const maxBin = d3.max(weightedBins);
const barW = iw / N_BINS;
g.selectAll(".bar").data(weightedBins).join("rect")
.attr("x", (d, i) => i * barW)
.attr("y", d => ih - (d / maxBin) * ih)
.attr("width", barW - 1)
.attr("height", d => (d / maxBin) * ih)
.attr("fill", "#34d399")
.attr("opacity", 0.85);
// X axis ticks at decade boundaries
const tickValues = [];
for (let logV = Math.ceil(Math.log10(minCost)); logV <= Math.log10(maxCost); logV++) {
tickValues.push(Math.pow(10, logV));
}
g.append("g").attr("transform", `translate(0,${ih})`)
.selectAll("text").data(tickValues).join("text")
.attr("x", d => x(d)).attr("y", 14).attr("text-anchor", "middle")
.attr("fill", "#98a3c0").attr("font-size", 9)
.text(d => formatMoney(d));
// Annotation: zero count (people with no cost)
g.append("text").attr("x", iw).attr("y", -2).attr("text-anchor", "end")
.attr("fill", "#5a6585").attr("font-size", 9)
.text(`${formatInt(zeroCount * SCALE_WEIGHT)} of ${formatInt(costs.length * SCALE_WEIGHT)} people had £0 (not shown)`);
}
// ===========================================================================
// PARTICLE ANIMATION
// ===========================================================================
const ENABLED = {A: true, C: true, B: true};
const MAX_VISIBLE_PARTICLES = 4000;
let lastAggregateTime = -10;
let lastAggregateWall = 0;
const AGGREGATE_THROTTLE_MS = 250;
function maybeUpdateAggregate(force) {
const now = performance.now();
const advanced = Math.abs(currentTime - lastAggregateTime) >= 0.4;
const enoughWall = (now - lastAggregateWall) >= AGGREGATE_THROTTLE_MS;
if (!force && !(advanced && enoughWall)) return;
lastAggregateTime = currentTime;
lastAggregateWall = now;
const agg = aggregateAtTime(currentTime, SCALE_WEIGHT);
updateDisplays(agg);
}
function render() {
ctx.clearRect(0, 0, W, H);
maybeUpdateAggregate(false); // throttled — costly at 100K people
// Particle rendering
const N = TRANSITS_FLAT.length;
const stride = Math.max(1, Math.floor(N / MAX_VISIBLE_PARTICLES));
for (let idx = 0; idx < N; idx++) {
const tr = TRANSITS_FLAT[idx];
if (!ENABLED[tr.k]) continue;
if (recMode === "hide" && !tr.fwd) continue;
if (recMode === "only" && tr.fwd) continue;
// v1.1 fix: skip particles for antecedent→antecedent transits — they're
// excluded from the sankey layout (no edgePath exists for them) so they
// would fall through to the linear-interpolation fallback and "wander"
// across the chart. The compounding cascade is still captured by cost,
// it just isn't visualised as a flying dot here.
if (tr.from && tr.to && tr.from.startsWith("a_") && tr.to.startsWith("a_")) continue;
const np_from = nodePos[tr.from], np_to = nodePos[tr.to];
if (!np_from || !np_to) continue;
if (currentTime >= tr.arrive) continue;
if (currentTime < tr.depart) continue;
if (idx % stride !== 0) continue;
const frac = (currentTime - tr.depart) / Math.max(0.01, tr.arrive - tr.depart);
let x, y;
if (tr.fwd && edgePath[tr.eid]) {
const len = edgePathLength[tr.eid];
const pt = edgePath[tr.eid].getPointAtLength(len * frac);
x = pt.x; y = pt.y;
} else if (!tr.fwd && recurrencePath[tr.eid]) {
const len = recurrencePathLength[tr.eid];
const pt = recurrencePath[tr.eid].getPointAtLength(len * frac);
x = pt.x; y = pt.y;
} else {
x = np_from.cx + (np_to.cx - np_from.cx) * frac;
y = np_from.cy + (np_to.cy - np_from.cy) * frac;
}
const rgb = COLOR_RGB[tr.k];
if (!tr.fwd) {
ctx.fillStyle = `rgba(240,171,252,0.35)`;
ctx.beginPath(); ctx.arc(x, y, 3.5, 0, 2 * Math.PI); ctx.fill();
}
ctx.fillStyle = `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
ctx.globalAlpha = 0.85;
ctx.beginPath(); ctx.arc(x, y, tr.fwd ? 1.6 : 1.9, 0, 2 * Math.PI); ctx.fill();
ctx.globalAlpha = 1;
}
}
// ===========================================================================
// CONTROLS
// ===========================================================================
const scrub = document.getElementById("scrub");
const playBtn = document.getElementById("playBtn");
const resetBtn = document.getElementById("resetBtn");
const timeDisplay = document.getElementById("timeDisplay");
const speedSel = document.getElementById("speedSel");
const recSel = document.getElementById("recSel");
const popSel = document.getElementById("popSel");
const popLabel = document.getElementById("popLabel");
function setTime(t) {
currentTime = Math.max(0, Math.min(HORIZON, t));
scrub.value = currentTime * 100;
timeDisplay.textContent = currentTime.toFixed(1);
render();
}
function frame(now) {
if (lastFrameTime === null) lastFrameTime = now;
const dt = (now - lastFrameTime) / 1000;
lastFrameTime = now;
if (playing) {
const newT = currentTime + dt * playbackSpeed;
if (newT >= HORIZON) {
setTime(HORIZON);
playing = false; playBtn.textContent = "▶ Play"; playBtn.classList.remove("active");
} else setTime(newT);
}
requestAnimationFrame(frame);
}
playBtn.addEventListener("click", () => {
if (currentTime >= HORIZON) setTime(0);
playing = !playing;
playBtn.textContent = playing ? "⏸ Pause" : "▶ Play";
playBtn.classList.toggle("active", playing);
});
resetBtn.addEventListener("click", () => setTime(0));
scrub.addEventListener("input", () => { setTime(scrub.value / 100); maybeUpdateAggregate(true); });
speedSel.addEventListener("change", () => { playbackSpeed = parseFloat(speedSel.value); });
recSel.addEventListener("change", () => { recMode = recSel.value; render(); });
popSel.addEventListener("change", async () => {
POP_SIZE = parseInt(popSel.value);
popLabel.textContent = POP_SIZE.toLocaleString();
playing = false; playBtn.textContent = "▶ Play"; playBtn.classList.remove("active");
await runSimulation();
setTime(0);
patternsCache = computePatterns();
if (document.getElementById("page-patterns").classList.contains("active")) {
pat2RenderChainList(); pat2RenderLoopList();
}
FRONTIER.cache = null; NODE_ARRIVALS_LA_CACHE = null;
if (document.getElementById("page-interv").classList.contains("active")) refreshFrontierIfReady();
});
// ===========================================================================
// PAGE 2 — chains and loops analytics
// ===========================================================================
let patternsCache = null;
function unitCostFor(nodeId) {
const uc = UNIT_COSTS[nodeId];
if (!uc) return {public: 0, la: 0};
return {public: uc.cost_public || 0, la: uc.cost_la_only || 0};
}
// Find cycles in LEG: for each back-edge, BFS shortest forward path target→source
function findLoops() {
const fwdAdj = {};
for (const e of PAYLOAD.edges) {
if (e.fwd) {
if (!fwdAdj[e.a]) fwdAdj[e.a] = [];
fwdAdj[e.a].push(e.b);
}
}
const loops = [];
for (const e of PAYLOAD.edges) {
if (e.fwd) continue;
// BFS forward from e.b to e.a
const parent = {[e.b]: null};
const q = [e.b];
let found = false;
while (q.length && !found) {
const n = q.shift();
if (n === e.a) { found = true; break; }
for (const nb of (fwdAdj[n] || [])) {
if (!(nb in parent)) { parent[nb] = n; q.push(nb); }
}
}
if (!found) continue;
const path = [];
let cur = e.a;
while (cur !== null) { path.unshift(cur); cur = parent[cur]; }
// path = [e.b, ..., e.a]; loop = path + back to e.b via e
loops.push({
back_edge: e.id,
nodes: path, // forward leg
back_from: e.a, back_to: e.b,
activations: 0,
cost_public: 0, cost_la: 0,
hsv_bins: {A: 0, C: 0, B: 0},
});
}
// De-duplicate loops with same node set (multiple back-edges may yield same cycle)
const seen = new Map();
for (const lp of loops) {
const key = [...lp.nodes, lp.back_to].sort().join("|");
if (!seen.has(key)) seen.set(key, lp);
}
return [...seen.values()];
}
const LOOPS = findLoops();
console.log("LEG cycles found:", LOOPS.length);
function computePatterns() {
// Build per-person chain & loop attribution
const chains = {}; // key "ant>n1>n2..." -> {nodes, count, cost_public, cost_la, bins}
const back_edge_counts = {}; // back_edge_id -> count
const back_edge_bins = {}; // back_edge_id -> {A,C,B}
for (const p of PEOPLE) {
// Group transits by event index
const byEvent = {};
for (const tr of p.transits) {
if (!byEvent[tr.evi]) byEvent[tr.evi] = [];
byEvent[tr.evi].push(tr);
}
for (const evi of Object.keys(byEvent)) {
const evtTransits = byEvent[evi].sort((a, b) => a.arrive - b.arrive);
if (evtTransits.length === 0) continue;
const antId = evtTransits[0].ant;
// Build per-event adjacency (parent->children) limited to this event's transits
const children = {};
for (const tr of evtTransits) {
if (!children[tr.from]) children[tr.from] = [];
children[tr.from].push(tr);
}
// DFS all root-to-leaf paths of length 2..4 from antecedent
const seenInPath = new Set();
function dfs(node, path, depth) {
// Record chain at lengths 2, 3, 4
if (path.length >= 2 && path.length <= 4) {
const key = path.join(">");
if (!chains[key]) chains[key] = {nodes: [...path], count: 0, cost_public: 0, cost_la: 0, bins: {A:0,C:0,B:0}, length: path.length};
chains[key].count++;
chains[key].bins[p.kBin]++;
// Cost: sum unit costs along the path (excluding antecedent)
let cp = 0, cl = 0;
for (let i = 1; i < path.length; i++) {
const uc = unitCostFor(path[i]);
cp += uc.public;
cl += uc.la;
}
chains[key].cost_public += cp;
chains[key].cost_la += cl;
}
if (depth >= 4) return;
const cs = children[node] || [];
for (const tr of cs) {
// Loops: if revisiting a node in current path, this transit is a back-edge in semantic sense
if (seenInPath.has(tr.to)) {
// Count as back-edge activation
back_edge_counts[tr.eid] = (back_edge_counts[tr.eid] || 0) + 1;
if (!back_edge_bins[tr.eid]) back_edge_bins[tr.eid] = {A:0,C:0,B:0};
back_edge_bins[tr.eid][p.kBin]++;
continue; // don't recurse into the loop in path enumeration
}
seenInPath.add(tr.to);
path.push(tr.to);
dfs(tr.to, path, depth + 1);
path.pop();
seenInPath.delete(tr.to);
}
}
seenInPath.add(antId);
dfs(antId, [antId], 1);
}
}
// Apply weight to all counts/costs
const w = SCALE_WEIGHT;
for (const key in chains) {
chains[key].count *= w;
chains[key].cost_public *= w;
chains[key].cost_la *= w;
for (const k of ["A","C","B"]) chains[key].bins[k] *= w;
}
// Attribute loop costs by activation count × unit cost of nodes in loop
for (const lp of LOOPS) {
lp.activations = (back_edge_counts[lp.back_edge] || 0) * w;
lp.hsv_bins = back_edge_bins[lp.back_edge] || {A:0,C:0,B:0};
for (const k of ["A","C","B"]) lp.hsv_bins[k] *= w;
let cp = 0, cl = 0;
// Cost of one loop cycle ≈ sum of node costs around the loop (excluding antecedent if any)
for (const nid of lp.nodes) {
const uc = unitCostFor(nid);
cp += uc.public;
cl += uc.la;
}
// Plus the back-edge target
const uc = unitCostFor(lp.back_to);
cp += uc.public;
cl += uc.la;
lp.cost_per_cycle_public = cp;
lp.cost_per_cycle_la = cl;
lp.cost_public = lp.activations * cp;
lp.cost_la = lp.activations * cl;
}
return {chains, loops: LOOPS};
}
// ===========================================================================
// Page 2 v0.12 — 3D rotatable force-directed graph
// ===========================================================================
const PAT2 = {
graph: null, // ForceGraph3D instance
graphData: null, // {nodes, links}
nodesById: {}, // id -> node ref
linksById: {}, // edgeId -> link ref
edgesByPair: {}, // "a|b" -> link ref
rendered: false,
selection: null, // {type, data, highlightNodes, highlightForwardEdgeIds, highlightBackEdgeIds, mixA, mixC, mixB}
};
function pat2InitGraph() {
if (PAT2.rendered) return;
if (typeof ForceGraph3D === "undefined") {
console.error("[Page 2] ForceGraph3D library failed to load — check CDN");
document.getElementById("viz2-3d").innerHTML =
'<div style="color:#f87171;padding:30px;font-size:13px;">3D graph library failed to load. Check internet connection or CDN reachability.</div>';
return;
}
const hasSpriteText = typeof SpriteText !== "undefined";
if (!hasSpriteText) console.warn("[Page 2] three-spritetext not loaded — node labels will use hover tooltips only");
// Build graph data: nodes + links from PAYLOAD
const nodes = PAYLOAD.nodes.map(n => ({
id: n.id,
name: n.n,
portfolio: n.p,
color: PORTFOLIO_COLORS[n.p] || PORTFOLIO_COLORS.other,
selected: false,
dimmed: false,
}));
const links = PAYLOAD.edges.map(e => ({
id: e.id,
source: e.a, target: e.b,
fwd: !!e.fwd, prob: e.p,
selected: false, dimmed: false,
}));
for (const n of nodes) PAT2.nodesById[n.id] = n;
for (const l of links) {
PAT2.linksById[l.id] = l;
PAT2.edgesByPair[l.source + "|" + l.target] = l;
}
PAT2.graphData = { nodes, links };
const container = document.getElementById("viz2-3d");
const W2 = container.clientWidth || 800;
const H2 = 840;
const G = ForceGraph3D()(container)
.width(W2).height(H2)
.backgroundColor("#050810")
.graphData(PAT2.graphData)
.nodeId("id")
.nodeLabel(n => `<div style="background:#151b2d;color:#e8ecf5;padding:6px 10px;border-radius:6px;border:1px solid #2a3354;font-size:11px;max-width:280px;font-family:-apple-system,BlinkMacSystemFont,sans-serif;">
<div style="font-weight:600;margin-bottom:2px;">${n.name}</div>
<div style="color:#98a3c0;font-size:10px;">${n.portfolio.replace(/_/g,' ')} · ${n.id}</div>
</div>`)
.nodeColor(n => {
if (PAT2.selection && n.dimmed) return "#2a3354";
if (PAT2.selection && n.selected) return n.color;
return n.color;
})
.nodeOpacity(n => (PAT2.selection && n.dimmed) ? 0.18 : 0.92)
.nodeVal(n => n.selected ? 6 : 2.5)
.nodeThreeObjectExtend(true)
.nodeThreeObject(n => {
if (!hasSpriteText) return null; // graceful fallback — relies on hover label
try {
const sprite = new SpriteText(n.name.length > 20 ? n.name.slice(0,19) + "…" : n.name);
sprite.color = "#e8ecf5";
sprite.textHeight = 2.4;
sprite.backgroundColor = "rgba(11,15,26,0.55)";
sprite.padding = 1.5;
sprite.borderRadius = 2;
sprite.position.set(0, 6, 0);
return sprite;
} catch (err) {
console.warn("[Page 2] SpriteText failed:", err);
return null;
}
})
.linkSource("source").linkTarget("target")
.linkColor(l => {
if (PAT2.selection) {
if (l.selected) return l.fwd ? "#34d399" : "#f0abfc";
return "#1c2340";
}
return l.fwd ? "#3b4773" : "#7b5994";
})
.linkOpacity(l => (PAT2.selection && !l.selected) ? 0.05 : (l.fwd ? 0.45 : 0.65))
.linkWidth(l => l.selected ? 1.8 : 0.4)
.linkCurvature(l => l.fwd ? 0 : 0.4)
.linkDirectionalArrowLength(l => l.selected ? 3.5 : 1.5)
.linkDirectionalArrowRelPos(0.92)
.linkDirectionalArrowColor(l => l.fwd ? "#5a6585" : "#f0abfc")
.linkDirectionalParticles(l => l.selected ? 6 : 0)
.linkDirectionalParticleSpeed(0.006)
.linkDirectionalParticleWidth(2.4)
.linkDirectionalParticleColor(l => pat2SampleParticleColor());
// Tune the force layout for clearer separation
G.d3Force("charge").strength(-80);
G.d3Force("link").distance(28);
PAT2.graph = G;
PAT2.rendered = true;
}
// Particle colours are sampled per-particle from the selection's HSV mix
function pat2SampleParticleColor() {
const sel = PAT2.selection;
if (!sel) return "#34d399";
const mix = sel.type === "chain" ? sel.data.bins : sel.data.hsv_bins;
const total = (mix.A + mix.C + mix.B) || 1;
const r = Math.random();
if (r < mix.A / total) return "#38bdf8"; // A — sky
if (r < (mix.A + mix.C) / total) return "#facc15"; // C — gold
return "#f87171"; // B — coral
}
function pat2SetSelection(selection) {
PAT2.selection = selection;
pat2ApplyHighlight();
pat2RenderDetails();
pat2UpdateListSelection();
}
function pat2ApplyHighlight() {
if (!PAT2.graph) return;
const sel = PAT2.selection;
// Reset all flags
for (const n of PAT2.graphData.nodes) { n.selected = false; n.dimmed = false; }
for (const l of PAT2.graphData.links) { l.selected = false; l.dimmed = false; }
if (sel) {
for (const n of PAT2.graphData.nodes) n.dimmed = !sel.highlightNodes.has(n.id);
for (const n of PAT2.graphData.nodes) if (sel.highlightNodes.has(n.id)) n.selected = true;
for (const l of PAT2.graphData.links) {
const hi = sel.highlightForwardEdgeIds.has(l.id) || sel.highlightBackEdgeIds.has(l.id);
l.selected = hi;
l.dimmed = !hi;
}
}
// Refresh visuals (re-runs colour / width / particle callbacks)
PAT2.graph
.nodeColor(PAT2.graph.nodeColor())
.nodeOpacity(PAT2.graph.nodeOpacity())
.nodeVal(PAT2.graph.nodeVal())
.linkColor(PAT2.graph.linkColor())
.linkOpacity(PAT2.graph.linkOpacity())
.linkWidth(PAT2.graph.linkWidth())
.linkDirectionalArrowLength(PAT2.graph.linkDirectionalArrowLength())
.linkDirectionalParticles(PAT2.graph.linkDirectionalParticles())
.linkDirectionalParticleColor(PAT2.graph.linkDirectionalParticleColor());
// Title/subtitle
if (!sel) {
document.getElementById("pat2Title").textContent = "Select a chain or loop on the right";
document.getElementById("pat2Sub").textContent = "The full LEG is shown as a 3D rotatable graph below. Click+drag to rotate, scroll to zoom. Selecting a pattern highlights its nodes and edges in colour and animates HSV-mix-coloured particles along it.";
} else if (sel.type === "chain") {
document.getElementById("pat2Title").textContent = "Cascade chain: " + sel.data.nodes.map(n => (NODE_BY_ID[n] || {n}).n).join(" → ");
document.getElementById("pat2Sub").textContent = `Entered ${formatInt(sel.data.count)} times across the cohort. Green = forward edges; magenta = recurrence. Particles colour-coded by HSV mix.`;
} else if (sel.type === "loop") {
const lp = sel.data;
document.getElementById("pat2Title").textContent = "Recurrence loop: " + (NODE_BY_ID[lp.back_from] || {n: lp.back_from}).n + " ↺ " + (NODE_BY_ID[lp.back_to] || {n: lp.back_to}).n;
document.getElementById("pat2Sub").textContent = `Fired ${formatInt(lp.activations)} times. Particles cycle the loop continuously.`;
}
}
function pat2RenderDetails() {
const sel = PAT2.selection;
const det = document.getElementById("pat2Details");
if (!sel) {
det.innerHTML = '<div style="color: var(--ink-faint); padding: 20px; text-align: center; font-style: italic;">Pattern details appear here when you select a chain or loop.</div>';
return;
}
let html = "";
if (sel.type === "chain") {
const c = sel.data;
const total = c.bins.A + c.bins.C + c.bins.B || 1;
const pA = (c.bins.A / total * 100).toFixed(0);
const pC = (c.bins.C / total * 100).toFixed(0);
const pB = (c.bins.B / total * 100).toFixed(0);
const nodeRows = c.nodes.map(nid => {
const name = (NODE_BY_ID[nid] || {n: nid}).n;
const portfolio = (NODE_BY_ID[nid] || {p: "other"}).p;
const swatchColor = PORTFOLIO_COLORS[portfolio] || PORTFOLIO_COLORS.other;
const uc = UNIT_COSTS[nid];
const cost = uc ? `${formatMoney(uc.cost_public || 0)}` : "—";
return `<div class="node-row"><span class="sw" style="background:${swatchColor}"></span><span class="nm">${name}</span><span class="cost">${cost}</span></div>`;
}).join("");
html = `
<div class="title">${c.length}-step chain</div>
<div class="sub">From ${(NODE_BY_ID[c.nodes[0]] || {n: c.nodes[0]}).n}</div>
<div class="stat-grid">
<div class="stat-card count"><div class="l">Cohort occurrences</div><div class="v">${formatInt(c.count)}</div></div>
<div class="stat-card public"><div class="l">Total public £</div><div class="v">${formatMoney(c.cost_public)}</div></div>
<div class="stat-card la"><div class="l">Total LA £</div><div class="v">${formatMoney(c.cost_la)}</div></div>
<div class="stat-card"><div class="l">£/event (public)</div><div class="v" style="color: var(--money)">${formatMoney(c.cost_public/Math.max(1,c.count))}</div></div>
</div>
<div class="chain-path">
<div class="l">Path nodes (with unit cost)</div>
<div class="nodes">${nodeRows}</div>
</div>
<div class="hsv-mix">
<div class="l">HSV mix of people who travel this chain</div>
<div class="hsv-bar">
<div class="seg segA" style="width:${pA}%"></div>
<div class="seg segC" style="width:${pC}%"></div>
<div class="seg segB" style="width:${pB}%"></div>
</div>
<div class="hsv-labels">
<span>Resilient ${pA}%</span><span>Average ${pC}%</span><span>Precarious ${pB}%</span>
</div>
</div>`;
} else if (sel.type === "loop") {
const lp = sel.data;
const total = (lp.hsv_bins.A + lp.hsv_bins.C + lp.hsv_bins.B) || 1;
const pA = (lp.hsv_bins.A / total * 100).toFixed(0);
const pC = (lp.hsv_bins.C / total * 100).toFixed(0);
const pB = (lp.hsv_bins.B / total * 100).toFixed(0);
const allNodes = [...new Set([...lp.nodes, lp.back_to])];
const nodeRows = allNodes.map(nid => {
const name = (NODE_BY_ID[nid] || {n: nid}).n;
const portfolio = (NODE_BY_ID[nid] || {p: "other"}).p;
const swatchColor = PORTFOLIO_COLORS[portfolio] || PORTFOLIO_COLORS.other;
const uc = UNIT_COSTS[nid];
const cost = uc ? `${formatMoney(uc.cost_public || 0)}` : "—";
return `<div class="node-row"><span class="sw" style="background:${swatchColor}"></span><span class="nm">${name}</span><span class="cost">${cost}</span></div>`;
}).join("");
html = `
<div class="title">${allNodes.length}-node recurrence loop</div>
<div class="sub">Closes at ${(NODE_BY_ID[lp.back_from] || {n: lp.back_from}).n} → back to ${(NODE_BY_ID[lp.back_to] || {n: lp.back_to}).n}</div>
<div class="stat-grid">
<div class="stat-card count"><div class="l">Loop activations</div><div class="v">${formatInt(lp.activations)}</div></div>
<div class="stat-card public"><div class="l">Total public £</div><div class="v">${formatMoney(lp.cost_public)}</div></div>
<div class="stat-card la"><div class="l">Total LA £</div><div class="v">${formatMoney(lp.cost_la)}</div></div>
<div class="stat-card"><div class="l">£/cycle</div><div class="v" style="color: var(--money)">${formatMoney(lp.cost_per_cycle_public)}</div></div>
</div>
<div class="chain-path">
<div class="l">Loop nodes</div>
<div class="nodes">${nodeRows}</div>
</div>
<div class="hsv-mix">
<div class="l">HSV mix of people who activate this loop</div>
<div class="hsv-bar">
<div class="seg segA" style="width:${pA}%"></div>
<div class="seg segC" style="width:${pC}%"></div>
<div class="seg segB" style="width:${pB}%"></div>
</div>
<div class="hsv-labels">
<span>Resilient ${pA}%</span><span>Average ${pC}%</span><span>Precarious ${pB}%</span>
</div>
</div>`;
}
det.innerHTML = html;
}
function pat2UpdateListSelection() {
document.querySelectorAll(".pat2-item").forEach(el => el.classList.remove("selected"));
if (PAT2.selection && PAT2.selection.itemKey) {
const el = document.querySelector(`.pat2-item[data-key="${PAT2.selection.itemKey}"]`);
if (el) el.classList.add("selected");
}
}
function pat2MakeChainSelection(c) {
const highlightNodes = new Set(c.nodes);
const highlightForwardEdgeIds = new Set();
const highlightBackEdgeIds = new Set();
for (let i = 0; i < c.nodes.length - 1; i++) {
const e = PAT2.edgesByPair[c.nodes[i] + "|" + c.nodes[i+1]];
if (e) (e.fwd ? highlightForwardEdgeIds : highlightBackEdgeIds).add(e.id);
}
return { type: "chain", data: c, highlightNodes, highlightForwardEdgeIds, highlightBackEdgeIds,
itemKey: "chain:" + c.nodes.join(">") };
}
function pat2MakeLoopSelection(lp) {
const highlightNodes = new Set([...lp.nodes, lp.back_to]);
const highlightForwardEdgeIds = new Set();
const highlightBackEdgeIds = new Set([lp.back_edge]);
for (let i = 0; i < lp.nodes.length - 1; i++) {
const e = PAT2.edgesByPair[lp.nodes[i] + "|" + lp.nodes[i+1]];
if (e) (e.fwd ? highlightForwardEdgeIds : highlightBackEdgeIds).add(e.id);
}
return { type: "loop", data: lp, highlightNodes, highlightForwardEdgeIds, highlightBackEdgeIds,
itemKey: "loop:" + lp.back_edge };
}
// ---------------- chain & loop list rendering ----------------
function pat2RenderChainList() {
if (!patternsCache) return;
const sort = document.getElementById("chainSort2").value;
const lenFilter = parseInt(document.getElementById("chainLen2").value);
const antFilter = document.getElementById("chainAntFilter2").value;
const rows = Object.values(patternsCache.chains)
.filter(c => c.length === lenFilter)
.filter(c => !antFilter || c.nodes[0] === antFilter);
if (sort === "count") rows.sort((a, b) => b.count - a.count);
else if (sort === "cost") rows.sort((a, b) => b.cost_public - a.cost_public);
else if (sort === "cost_per") rows.sort((a, b) => b.cost_public/Math.max(1,b.count) - a.cost_public/Math.max(1,a.count));
const top = rows.slice(0, 30);
const container = document.getElementById("chainList2");
if (top.length === 0) { container.innerHTML = '<div style="color: var(--ink-faint); padding: 20px; text-align: center;">No chains match filters.</div>'; return; }
container.innerHTML = top.map((c, idx) => {
const total = (c.bins.A + c.bins.C + c.bins.B) || 1;
const pA = c.bins.A / total * 100;
const pC = c.bins.C / total * 100;
const pB = c.bins.B / total * 100;
const chips = c.nodes.map((nid, i) => {
const portfolio = (NODE_BY_ID[nid] || {p: "other"}).p;
const name = (NODE_BY_ID[nid] || {n: nid}).n;
const shortName = name.length > 12 ? name.slice(0, 11) + "…" : name;
const arrow = i < c.nodes.length - 1 ? '<span class="mini-arrow">→</span>' : '';
return `<span class="mini-chip" title="${name}"><span class="sw" style="background:${PORTFOLIO_COLORS[portfolio]||PORTFOLIO_COLORS.other}"></span>${shortName}</span>${arrow}`;
}).join("");
const key = "chain:" + c.nodes.join(">");
return `<div class="pat2-item" data-key="${key}" data-kind="chain" data-idx="${idx}">
<div class="summary"><span class="rank">#${idx+1}</span><span class="count">${formatInt(c.count)}</span><span class="cost">${formatMoney(c.cost_public)}</span></div>
<div class="chain-mini-chips">${chips}</div>
<div class="mini-mix"><div class="seg segA" style="width:${pA}%"></div><div class="seg segC" style="width:${pC}%"></div><div class="seg segB" style="width:${pB}%"></div></div>
</div>`;
}).join("");
container.querySelectorAll(".pat2-item").forEach(el => {
el.addEventListener("click", () => {
const idx = parseInt(el.dataset.idx);
pat2SetSelection(pat2MakeChainSelection(top[idx]));
});
});
pat2UpdateListSelection();
}
function pat2RenderLoopList() {
if (!patternsCache) return;
const sort = document.getElementById("loopSort2").value;
const loops = [...patternsCache.loops].filter(l => l.activations > 0);
if (sort === "activations") loops.sort((a, b) => b.activations - a.activations);
else if (sort === "cost") loops.sort((a, b) => b.cost_public - a.cost_public);
const container = document.getElementById("loopList2");
if (loops.length === 0) { container.innerHTML = '<div style="color: var(--ink-faint); padding: 20px; text-align: center;">No loops activated.</div>'; return; }
container.innerHTML = loops.map((lp, idx) => {
const allNodes = [...new Set([...lp.nodes, lp.back_to])];
const total = (lp.hsv_bins.A + lp.hsv_bins.C + lp.hsv_bins.B) || 1;
const pA = lp.hsv_bins.A / total * 100;
const pC = lp.hsv_bins.C / total * 100;
const pB = lp.hsv_bins.B / total * 100;
const chips = allNodes.map((nid, i) => {
const portfolio = (NODE_BY_ID[nid] || {p: "other"}).p;
const name = (NODE_BY_ID[nid] || {n: nid}).n;
const shortName = name.length > 12 ? name.slice(0, 11) + "…" : name;
const arrow = i < allNodes.length - 1 ? '<span class="mini-arrow">→</span>' : '<span class="mini-arrow" style="color:#f0abfc">↺</span>';
return `<span class="mini-chip" title="${name}"><span class="sw" style="background:${PORTFOLIO_COLORS[portfolio]||PORTFOLIO_COLORS.other}"></span>${shortName}</span>${arrow}`;
}).join("");
const key = "loop:" + lp.back_edge;
return `<div class="pat2-item" data-key="${key}" data-kind="loop" data-idx="${idx}">
<div class="summary"><span class="rank">#${idx+1}</span><span class="count">${formatInt(lp.activations)}</span><span class="cost">${formatMoney(lp.cost_public)}</span></div>
<div class="chain-mini-chips">${chips}</div>
<div class="mini-mix"><div class="seg segA" style="width:${pA}%"></div><div class="seg segC" style="width:${pC}%"></div><div class="seg segB" style="width:${pB}%"></div></div>
</div>`;
}).join("");
container.querySelectorAll(".pat2-item").forEach(el => {
el.addEventListener("click", () => {
const idx = parseInt(el.dataset.idx);
pat2SetSelection(pat2MakeLoopSelection(loops[idx]));
});
});
pat2UpdateListSelection();
}
function pat2PopulateAntFilter() {
const sel = document.getElementById("chainAntFilter2");
sel.innerHTML = '<option value="">All triggers</option>';
const ants = PAYLOAD.antecedents.slice().sort((a,b) => a.name.localeCompare(b.name));
for (const a of ants) sel.innerHTML += `<option value="${a.id}">${a.name.length > 36 ? a.name.slice(0,35)+'…' : a.name}</option>`;
}
function refreshPatternsIfReady() {
if (PEOPLE.length === 0) return;
patternsCache = computePatterns();
pat2InitGraph();
pat2RenderChainList();
pat2RenderLoopList();
}
// Particle animation is now handled by 3d-force-graph's built-in
// linkDirectionalParticles — no custom animation loop needed.
// Tab switching — 5 tabs (welcome / la / live / patterns / interv).
// Marks earlier steps as 'done' to signpost journey progress.
function switchToTab(tabName) {
const all = Array.from(document.querySelectorAll(".tab-btn"));
const targetBtn = all.find(b => b.dataset.tab === tabName);
if (!targetBtn) return;
const idx = all.indexOf(targetBtn);
all.forEach((b, i) => {
b.classList.remove("active"); b.classList.remove("done");
b.setAttribute("aria-selected", "false");
if (i < idx) b.classList.add("done");
});
document.querySelectorAll(".tab-page").forEach(p => p.classList.remove("active"));
targetBtn.classList.add("active");
targetBtn.setAttribute("aria-selected", "true");
document.getElementById("page-" + tabName).classList.add("active");
document.body.setAttribute("data-tab", tabName);
if (tabName === "patterns") {
if (typeof playing !== "undefined" && playing) {
playing = false; playBtn.textContent = "▶ Play"; playBtn.classList.remove("active");
}
refreshPatternsIfReady();
} else if (tabName === "interv") {
if (typeof playing !== "undefined" && playing) {
playing = false; playBtn.textContent = "▶ Play"; playBtn.classList.remove("active");
}
rebuildInterventions();
refreshFrontierIfReady();
} else if (tabName === "la") {
refreshLaPage();
} else if (tabName === "live") {
// v1.2: ensure the heavy simulation has started (in case user reached this
// tab via wayfinder skip rather than the welcome CTA)
if (typeof startSimulationIfNeeded === "function") startSimulationIfNeeded();
// v1.1 hotfix: resize the particle canvas to match its now-visible CSS size.
// The canvas was sized at script-load when this tab was hidden (clientWidth=0),
// causing particles to render in the wrong screen positions. We rAF this so the
// browser has applied the display:block change before we measure.
if (typeof resizeCascadeCanvas === "function") {
requestAnimationFrame(() => { requestAnimationFrame(resizeCascadeCanvas); });
}
} else if (tabName === "plan") {
// Page 6 — refresh the take-away plan. May take a moment if frontier cache is cold.
if (typeof refreshPlanPage === "function") {
requestAnimationFrame(() => refreshPlanPage());
}
}
}
document.querySelectorAll(".tab-btn").forEach(btn => {
btn.addEventListener("click", () => switchToTab(btn.dataset.tab));
});
document.body.setAttribute("data-tab", "welcome");
// Welcome page CTA wiring — kick off the heavy Monte Carlo in the background
// the moment the user commits to moving forward. Non-blocking: they navigate to
// the LA-select page immediately while the sim runs.
document.getElementById("welcomeStartBtn")?.addEventListener("click", () => {
startSimulationIfNeeded();
switchToTab("la");
});
document.getElementById("welcomeLearnBtn")?.addEventListener("click", () => {
// For now, scroll to explainer cards. Could later open a method doc.
document.querySelector(".welcome-explainer")?.scrollIntoView({behavior:"smooth", block:"start"});
});
// LA page continue button
document.getElementById("laContinueBtn")?.addEventListener("click", () => switchToTab("live"));
// Cascade page → Patterns page CTA, Patterns page → Interventions page CTA
document.getElementById("goPatternsBtn")?.addEventListener("click", () => switchToTab("patterns"));
document.getElementById("goIntervBtn")?.addEventListener("click", () => switchToTab("interv"));
// ===========================================================================
// PAGE 6 — Take it to the CEO (exec plan)
// ===========================================================================
const PLAN = {
schedule: {}, // iv_id → { startMonth: 1..36 }
};
// Compute average cascade lag (in months) for the edges an intervention affects.
// Used to time when benefits start to materialise after the intervention begins.
function planAvgLagForIntervention(iv) {
let lags = [];
if (iv.intervention_type === "tertiary" && iv.target_edges && iv.target_edges.length) {
for (const eid of iv.target_edges) {
const e = PAYLOAD.edges.find(x => x.id === eid);
if (e && typeof e.lm === "number") lags.push(e.lm);
}
} else {
// Primary: collect lags of all outgoing edges from the targeted antecedents
for (const ant_id of (iv.targets || [])) {
const outs = PAYLOAD.edges.filter(e => e.a === ant_id);
for (const e of outs) if (typeof e.lm === "number") lags.push(e.lm);
}
}
if (!lags.length) return 3.0; // sensible default
return lags.reduce((s, x) => s + x, 0) / lags.length;
}
// For each active intervention, get its per-month LA and Public spend and benefit
// (where total LA/Pub savings come from the pre-computed frontier cache).
function planComputePhasing() {
const months = 36;
const ivData = [];
// Sum across active interventions
const totLaSpend = new Array(months).fill(0);
const totPubSpend = new Array(months).fill(0);
const totLaBenefit = new Array(months).fill(0);
const totPubBenefit = new Array(months).fill(0);
// Pull per-intervention LA/Pub savings from the frontier cache if available.
// (The frontier already runs each intervention individually.)
const frontierByIv = {};
if (FRONTIER && FRONTIER.cache && FRONTIER.cache.points) {
for (const p of FRONTIER.cache.points) {
frontierByIv[p.iv.id] = p; // {cost, savings, ...} savings is public-sector
}
}
// We also want LA-only savings per intervention. The frontier currently stores
// public savings; LA savings need to be reconstructed. As a defensible approximation,
// use the LA-share-weighted ratio (LA savings ≈ public savings × la_share_of_cascade_cost).
// The dashboard's baselineLa / baselinePub ratio gives us that ratio at the LA level.
const baselinePub = (INTERV._lastBaselinePub || 1);
const baselineLa = (INTERV._lastBaselineLa || 0);
const laShareOfCascade = baselineLa / Math.max(1, baselinePub);
for (const iv of INTERVENTIONS_LIBRARY) {
const act = INTERV.activations[iv.id];
if (!act || !act.active || !act.scale || act.scale <= 0) continue;
const startMonth = (PLAN.schedule[iv.id] && PLAN.schedule[iv.id].startMonth) || 1;
const avgLag = planAvgLagForIntervention(iv);
// Spend phasing — continuous from start month onward, at annual_cost/12 per month
const annualPubSpend = act.scale * iv.cost_per_case_gbp;
const annualLaSpend = annualPubSpend * (iv.la_share || 0);
const monthlyPubSpend = annualPubSpend / 12;
const monthlyLaSpend = annualLaSpend / 12;
let ivTotalLaSpend = 0;
let ivTotalPubSpend = 0;
for (let m = startMonth; m <= months; m++) {
const idx = m - 1;
totLaSpend[idx] += monthlyLaSpend;
totPubSpend[idx] += monthlyPubSpend;
ivTotalLaSpend += monthlyLaSpend;
ivTotalPubSpend += monthlyPubSpend;
}
// Benefit total (over full 36 months) — pulled from frontier or zero if not yet computed
const fp = frontierByIv[iv.id];
const totalPubBenefit = fp ? fp.savings : 0;
const totalLaBenefit = totalPubBenefit * laShareOfCascade;
// Benefit phasing — accrues evenly between (startMonth + avgLag) and month 36.
// This is a deliberately simple distribution that matches the cascade lag medians
// without committing to a full per-edge accrual model. The total realised within
// the 36-month horizon equals (total_benefit × fraction_of_lag_window_inside_horizon).
const benefitStart = Math.min(months, Math.max(1, startMonth + Math.round(avgLag)));
const benefitMonths = Math.max(1, months - benefitStart + 1);
const monthlyLaBenefit = totalLaBenefit / 36; // amortised across full 36 mo (smoother visual)
const monthlyPubBenefit = totalPubBenefit / 36;
// Apply with a soft ramp: 0 before benefitStart, linear ramp over 3 months, then steady
let ivTotalLaBenefitRealised = 0;
let ivTotalPubBenefitRealised = 0;
for (let m = 1; m <= months; m++) {
const idx = m - 1;
let factor = 0;
if (m >= benefitStart) {
const monthsAfter = m - benefitStart;
factor = Math.min(1, monthsAfter / 3); // 3-month ramp to full effect
}
const laB = monthlyLaBenefit * factor * (36 / benefitMonths) * 0.6; // 0.6 scale to keep cumulative within plausible bound
const pubB = monthlyPubBenefit * factor * (36 / benefitMonths) * 0.6;
totLaBenefit[idx] += laB;
totPubBenefit[idx] += pubB;
ivTotalLaBenefitRealised += laB;
ivTotalPubBenefitRealised += pubB;
}
ivData.push({
iv,
startMonth,
avgLag: Math.round(avgLag * 10) / 10,
duration: iv.duration_months,
totalLaSpend: ivTotalLaSpend,
totalPubSpend: ivTotalPubSpend,
totalLaBenefit: ivTotalLaBenefitRealised,
totalPubBenefit: ivTotalPubBenefitRealised,
netLa: ivTotalLaBenefitRealised - ivTotalLaSpend,
netPub: ivTotalPubBenefitRealised - ivTotalPubSpend,
});
}
// Build cumulative net
const cumLa = new Array(months).fill(0);
const cumPub = new Array(months).fill(0);
for (let m = 0; m < months; m++) {
const monthlyNetLa = totLaBenefit[m] - totLaSpend[m];
const monthlyNetPub = totPubBenefit[m] - totPubSpend[m];
cumLa[m] = (m === 0 ? 0 : cumLa[m-1]) + monthlyNetLa;
cumPub[m] = (m === 0 ? 0 : cumPub[m-1]) + monthlyNetPub;
}
// Payback month — first month cumLa >= 0
let paybackMonthLa = null;
for (let m = 0; m < months; m++) {
if (cumLa[m] >= 0 && totLaSpend.slice(0, m+1).some(x => x > 0)) { paybackMonthLa = m + 1; break; }
}
return {
months, ivData,
spend: { la: totLaSpend, pub: totPubSpend },
benefit: { la: totLaBenefit, pub: totPubBenefit },
cum: { la: cumLa, pub: cumPub },
paybackMonthLa,
totals: {
laSpend: totLaSpend.reduce((s,x)=>s+x, 0),
pubSpend: totPubSpend.reduce((s,x)=>s+x, 0),
laBenefit: totLaBenefit.reduce((s,x)=>s+x, 0),
pubBenefit: totPubBenefit.reduce((s,x)=>s+x, 0),
netLa: cumLa[months-1],
netPub: cumPub[months-1],
},
};
}
// Render a cashflow chart (bars: spend & benefit, line: cumulative net)
function renderCashflowChart(svgEl, spendArr, benefitArr, cumArr, paybackMonth) {
const svg = d3.select(svgEl);
svg.selectAll("*").remove();
const rect = svgEl.getBoundingClientRect();
const W = Math.max(400, rect.width || 600);
const H = 240;
svg.attr("viewBox", `0 0 ${W} ${H}`);
const margin = {top: 12, right: 16, bottom: 28, left: 70};
const iw = W - margin.left - margin.right;
const ih = H - margin.top - margin.bottom;
const months = spendArr.length;
const x = d3.scaleBand().domain(d3.range(1, months+1)).range([0, iw]).padding(0.15);
// Y scale spans negative (spend) and positive (benefit + cum line)
const maxBar = d3.max([
d3.max(spendArr) || 0,
d3.max(benefitArr) || 0,
d3.max(cumArr, d => Math.abs(d)) || 0,
]);
const yMax = Math.max(1000, maxBar * 1.05);
const yMin = -yMax;
const y = d3.scaleLinear().domain([yMin, yMax]).range([ih, 0]);
const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
// Y axis with £ formatted
const yTicks = y.ticks(5);
g.selectAll(".y-tick-line").data(yTicks).join("line")
.attr("class", "axis-line")
.attr("x1", 0).attr("x2", iw)
.attr("y1", d => y(d)).attr("y2", d => y(d))
.attr("stroke-opacity", d => d === 0 ? 0.4 : 0.1);
g.selectAll(".y-tick-label").data(yTicks).join("text")
.attr("class", "axis-label")
.attr("x", -8).attr("y", d => y(d))
.attr("text-anchor", "end").attr("dominant-baseline", "middle")
.text(d => formatMoneyShort(d));
// Zero line (emphasised)
g.append("line").attr("class", "zero-line")
.attr("x1", 0).attr("x2", iw)
.attr("y1", y(0)).attr("y2", y(0));
// Spend bars (downward — negative)
g.selectAll(".bar-spend").data(spendArr).join("rect")
.attr("class", "bar-spend")
.attr("x", (_, i) => x(i+1))
.attr("width", x.bandwidth())
.attr("y", y(0))
.attr("height", d => Math.max(0, y(-d) - y(0)));
// Benefit bars (upward — positive)
g.selectAll(".bar-benefit").data(benefitArr).join("rect")
.attr("class", "bar-benefit")
.attr("x", (_, i) => x(i+1))
.attr("width", x.bandwidth())
.attr("y", (d, i) => y(d))
.attr("height", d => Math.max(0, y(0) - y(d)));
// Cumulative net line
const line = d3.line()
.x((_, i) => x(i+1) + x.bandwidth() / 2)
.y(d => y(d));
g.append("path").attr("class", "cum-line" + (cumArr[months-1] >= 0 ? " positive" : ""))
.attr("d", line(cumArr));
// Payback marker
if (paybackMonth) {
const xp = x(paybackMonth) + x.bandwidth() / 2;
g.append("line").attr("class", "payback-marker")
.attr("x1", xp).attr("x2", xp).attr("y1", 0).attr("y2", ih);
g.append("text").attr("class", "payback-label")
.attr("x", xp + 5).attr("y", 12).text(`Payback M${paybackMonth}`);
}
// X axis — show every 6 months
const xTickMonths = [1, 6, 12, 18, 24, 30, 36];
g.selectAll(".x-tick-label").data(xTickMonths).join("text")
.attr("class", "axis-label")
.attr("x", d => x(d) + x.bandwidth() / 2)
.attr("y", ih + 16).attr("text-anchor", "middle")
.text(d => `M${d}`);
}
function formatMoneyShort(v) {
const abs = Math.abs(v);
if (abs >= 1e6) return (v < 0 ? "-" : "") + "£" + (abs/1e6).toFixed(1) + "m";
if (abs >= 1e3) return (v < 0 ? "-" : "") + "£" + (abs/1e3).toFixed(0) + "k";
return (v < 0 ? "-" : "") + "£" + abs.toFixed(0);
}
// Render the intervention list with per-iv start-month pickers
function renderPlanInterventionList(phasing) {
const container = document.getElementById("planInterventionList");
if (!container) return;
if (phasing.ivData.length === 0) { container.innerHTML = ""; return; }
container.innerHTML = phasing.ivData.map(d => {
const monthOptions = Array.from({length: 36}, (_, i) => {
const m = i + 1;
return `<option value="${m}" ${m === d.startMonth ? "selected" : ""}>M${m}</option>`;
}).join("");
const netClass = d.netLa >= 0 ? "v benefit" : "v spend";
return `<div class="plan-iv-card" data-iv-id="${d.iv.id}">
<div class="name">${d.iv.name}</div>
<div class="meta">
<span>LA spend <span class="v spend">${formatMoney(d.totalLaSpend)}</span></span>
<span>LA benefit <span class="v benefit">${formatMoney(d.totalLaBenefit)}</span></span>
<span>Net <span class="${netClass}">${formatMoney(d.netLa)}</span></span>
</div>
<div class="control-row">
<div class="start-control">
<label for="planStart-${d.iv.id}">Start</label>
<select class="start-month" id="planStart-${d.iv.id}" data-iv-id="${d.iv.id}">${monthOptions}</select>
</div>
<div class="lag-display">benefits ramp from <strong>M${Math.min(36, d.startMonth + Math.round(d.avgLag))}</strong> (avg lag ${d.avgLag} mo)</div>
</div>
</div>`;
}).join("");
container.querySelectorAll("select.start-month").forEach(sel => {
sel.addEventListener("change", () => {
const ivId = sel.dataset.ivId;
if (!PLAN.schedule[ivId]) PLAN.schedule[ivId] = {};
PLAN.schedule[ivId].startMonth = parseInt(sel.value);
refreshPlanPage(); // recompute on change
});
});
}
function renderPlanKpis(phasing) {
const t = phasing.totals;
const laNet = document.getElementById("kpiLaNet");
const pubNet = document.getElementById("kpiPubNet");
const payback = document.getElementById("kpiPayback");
const laSpend = document.getElementById("kpiLaSpend");
if (laNet) {
laNet.textContent = formatMoney(t.netLa);
laNet.className = "v " + (t.netLa >= 0 ? "positive" : "negative");
}
if (pubNet) {
pubNet.textContent = formatMoney(t.netPub);
pubNet.className = "v " + (t.netPub >= 0 ? "positive" : "negative");
}
if (payback) {
payback.textContent = phasing.paybackMonthLa ? "M" + phasing.paybackMonthLa : "Not within 36 mo";
payback.className = "v " + (phasing.paybackMonthLa ? "positive" : "negative");
}
if (laSpend) {
laSpend.textContent = formatMoney(t.laSpend);
laSpend.className = "v";
}
// Extras
document.getElementById("kpiLaNetExtra").textContent = `Spend ${formatMoney(t.laSpend)} · Benefit ${formatMoney(t.laBenefit)}`;
document.getElementById("kpiPubNetExtra").textContent = `Spend ${formatMoney(t.pubSpend)} · Benefit ${formatMoney(t.pubBenefit)}`;
document.getElementById("kpiLaSpendExtra").textContent = `${phasing.ivData.length} intervention${phasing.ivData.length === 1 ? "" : "s"} activated`;
}
async function refreshPlanPage() {
// Ensure frontier cache is fresh (provides per-iv savings)
if (!FRONTIER.cache) {
await computeFrontierData();
}
const phasing = planComputePhasing();
const empty = document.getElementById("planEmpty");
const body = document.getElementById("planBody");
if (!phasing.ivData.length) {
empty.style.display = "block";
body.style.display = "none";
return;
}
empty.style.display = "none";
body.style.display = "block";
// Default start month for any newly activated iv
for (const d of phasing.ivData) {
if (!PLAN.schedule[d.iv.id]) PLAN.schedule[d.iv.id] = {startMonth: 1};
}
renderPlanInterventionList(phasing);
renderPlanKpis(phasing);
renderCashflowChart(document.getElementById("cashflowSvgLa"), phasing.spend.la, phasing.benefit.la, phasing.cum.la, phasing.paybackMonthLa);
renderCashflowChart(document.getElementById("cashflowSvgPub"), phasing.spend.pub, phasing.benefit.pub, phasing.cum.pub, null);
// Rebuild printable view (hidden until print mode)
rebuildPlanPrintView(phasing);
}
function rebuildPlanPrintView(phasing) {
const el = document.getElementById("planExecPrint");
if (!el) return;
const t = phasing.totals;
const laName = (typeof currentLA !== "undefined" && currentLA) ? currentLA.name : "—";
const dateStr = new Date().toLocaleDateString("en-GB", {year:"numeric", month:"long", day:"numeric"});
const rows = phasing.ivData.map(d => `
<tr>
<td>${d.iv.name}</td>
<td>M${d.startMonth}</td>
<td>${d.duration} mo</td>
<td class="num">${formatMoney(d.totalLaSpend)}</td>
<td class="num pos">${formatMoney(d.totalLaBenefit)}</td>
<td class="num ${d.netLa >= 0 ? "pos" : "neg"}">${formatMoney(d.netLa)}</td>
</tr>`).join("");
el.innerHTML = `
<h1>Intervention Plan — ${laName}</h1>
<div class="doc-sub">Generated ${dateStr} · 36-month horizon · LEG v0.12 (calibrated) · PSI Behavioural Physics</div>
<h2>Headline numbers</h2>
<div class="print-kpis">
<div class="print-kpi"><div class="l">LA net @ M36</div><div class="v ${t.netLa >= 0 ? "" : "neg"}">${formatMoney(t.netLa)}</div></div>
<div class="print-kpi"><div class="l">Public net @ M36</div><div class="v ${t.netPub >= 0 ? "" : "neg"}">${formatMoney(t.netPub)}</div></div>
<div class="print-kpi"><div class="l">Payback (LA)</div><div class="v">${phasing.paybackMonthLa ? "M" + phasing.paybackMonthLa : "—"}</div></div>
<div class="print-kpi"><div class="l">Total LA spend</div><div class="v neg">${formatMoney(t.laSpend)}</div></div>
</div>
<h2>Intervention schedule</h2>
<table>
<thead><tr><th>Intervention</th><th>Start</th><th>Duration</th><th>LA spend (36mo)</th><th>LA benefit (36mo)</th><th>Net LA</th></tr></thead>
<tbody>${rows}</tbody>
</table>
<h2>LA-only cashflow</h2>
<div class="print-chart">
<svg id="printChartLa" width="700" height="200"></svg>
</div>
<h2>Public-sector cashflow (LA + NHS + DWP + MoJ)</h2>
<div class="print-chart">
<svg id="printChartPub" width="700" height="200"></svg>
</div>
<div class="print-method">
<strong>Method:</strong> Cost engine simulates 100,000 people over 36 months using the PSI LEG v0.12 (259 edges over 42 antecedents and 92 service nodes; 59 antecedent-to-antecedent compounding cascades + 10 service-to-antecedent feedback edges; receiving-antecedent baselines calibrated against empirical UK incidence).
Spend phased monthly at annual_cost÷12 from each intervention's start month. Benefits phased using a soft ramp starting after the average cascade lag (LEG edge medians, expressed in months) from intervention start.
LA-only figures use each intervention's la_share parameter. Public-sector figures include NHS, DWP and MoJ-borne cascade costs.
Simulated antecedent firing rates converge with ONS / PHE / DWP empirical baselines to within ±15% for most receiving events. Acute MH crisis remains the residual case (cascade contribution exceeds empirical baseline — likely a combination of slightly hot attributable-risk values and under-counting in published incidence statistics) — treat MH-driven savings as directional.
</div>
`;
// Render print versions of the charts (smaller, light theme)
const printSvgLa = el.querySelector("#printChartLa");
const printSvgPub = el.querySelector("#printChartPub");
if (printSvgLa) renderPrintCashflowChart(printSvgLa, phasing.spend.la, phasing.benefit.la, phasing.cum.la, phasing.paybackMonthLa);
if (printSvgPub) renderPrintCashflowChart(printSvgPub, phasing.spend.pub, phasing.benefit.pub, phasing.cum.pub, null);
}
function renderPrintCashflowChart(svgEl, spendArr, benefitArr, cumArr, paybackMonth) {
const svg = d3.select(svgEl);
svg.selectAll("*").remove();
const W = 700, H = 200;
svg.attr("viewBox", `0 0 ${W} ${H}`);
const margin = {top: 8, right: 12, bottom: 22, left: 60};
const iw = W - margin.left - margin.right;
const ih = H - margin.top - margin.bottom;
const months = spendArr.length;
const x = d3.scaleBand().domain(d3.range(1, months+1)).range([0, iw]).padding(0.15);
const maxBar = d3.max([d3.max(spendArr)||0, d3.max(benefitArr)||0, d3.max(cumArr, d=>Math.abs(d))||0]);
const yMax = Math.max(1000, maxBar * 1.05);
const y = d3.scaleLinear().domain([-yMax, yMax]).range([ih, 0]);
const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
const yTicks = y.ticks(4);
g.selectAll(".y-tick-line").data(yTicks).join("line")
.attr("x1", 0).attr("x2", iw).attr("y1", d=>y(d)).attr("y2", d=>y(d))
.attr("stroke", d => d === 0 ? "#374151" : "#e5e7eb").attr("stroke-width", 1);
g.selectAll(".y-tick-label").data(yTicks).join("text")
.attr("x", -6).attr("y", d=>y(d)).attr("text-anchor", "end")
.attr("dominant-baseline", "middle").attr("font-size", "9").attr("fill", "#475569")
.text(d => formatMoneyShort(d));
g.selectAll(".bar-spend").data(spendArr).join("rect")
.attr("x", (_,i)=>x(i+1)).attr("width", x.bandwidth())
.attr("y", y(0)).attr("height", d=>Math.max(0, y(-d)-y(0)))
.attr("fill", "#fca5a5");
g.selectAll(".bar-benefit").data(benefitArr).join("rect")
.attr("x", (_,i)=>x(i+1)).attr("width", x.bandwidth())
.attr("y", d=>y(d)).attr("height", d=>Math.max(0, y(0)-y(d)))
.attr("fill", "#86efac");
const line = d3.line().x((_,i)=>x(i+1)+x.bandwidth()/2).y(d=>y(d));
g.append("path").attr("fill", "none").attr("stroke", cumArr[months-1]>=0?"#047857":"#b91c1c").attr("stroke-width", 1.5).attr("d", line(cumArr));
if (paybackMonth) {
const xp = x(paybackMonth) + x.bandwidth()/2;
g.append("line").attr("x1", xp).attr("x2", xp).attr("y1", 0).attr("y2", ih).attr("stroke", "#047857").attr("stroke-width", 1).attr("stroke-dasharray", "3,2");
}
[1, 6, 12, 18, 24, 30, 36].forEach(m => {
g.append("text").attr("x", x(m)+x.bandwidth()/2).attr("y", ih+14).attr("text-anchor", "middle").attr("font-size", "9").attr("fill", "#6b7280").text("M"+m);
});
}
// Wire plan page CTAs
document.getElementById("planEmptyBackBtn")?.addEventListener("click", () => switchToTab("interv"));
document.getElementById("planReplanBtn")?.addEventListener("click", () => switchToTab("interv"));
document.getElementById("planPrintBtn")?.addEventListener("click", () => {
document.body.classList.add("plan-printing");
// Brief delay so the print stylesheet applies before window.print is invoked
setTimeout(() => {
window.print();
setTimeout(() => document.body.classList.remove("plan-printing"), 500);
}, 100);
});
// LA context chip (header) — clicking 'change' goes back to LA page
document.getElementById("laContextChange")?.addEventListener("click", () => switchToTab("la"));
// Sync main LA picker on page 2 with the hidden legacy #laSel so existing engine code keeps working.
// Also populate the LA context grid + narrative.
function syncLaMainPickerFromLegacy() {
const main = document.getElementById("laSelMain");
const legacy = document.getElementById("laSel");
if (!main || !legacy) return;
// Copy options once
if (main.options.length === 0 && legacy.options.length > 0) {
Array.from(legacy.options).forEach(opt => {
const o = document.createElement("option");
o.value = opt.value; o.textContent = opt.textContent;
if (opt.selected) o.selected = true;
main.appendChild(o);
});
}
main.value = legacy.value;
}
function refreshLaPage() {
syncLaMainPickerFromLegacy();
populateLaContextGrid();
updateLaContextChip();
}
function updateLaContextChip() {
const chip = document.getElementById("laContextChip");
const name = document.getElementById("laContextName");
if (!chip || !name || typeof currentLA === "undefined" || !currentLA) return;
name.textContent = currentLA.name || "—";
chip.classList.add("visible");
}
function populateLaContextGrid() {
if (typeof currentLA === "undefined" || !currentLA) return;
const pop = currentLA.pop_adult || 0;
const imdLabel = currentLA.imd_label || "—";
// Region/type derived from blurb keywords — coarse but works for the presets
let region = "—";
const blurb = (currentLA.blurb || "").toLowerCase();
if (/london/.test(blurb)) region = "London";
else if (/birmingham|core city|urban/.test(blurb)) region = "Urban core";
else if (/rural|coastal/.test(blurb)) region = "Rural / coastal";
else if (/district/.test(blurb)) region = "District council";
else if (/county/.test(blurb)) region = "Upper-tier county";
else if (/unitary/.test(blurb)) region = "Unitary authority";
document.getElementById("laPopV").textContent = pop > 0 ? new Intl.NumberFormat("en-GB").format(pop) : "—";
document.getElementById("laPopExtra").textContent = pop > 0 ? "adults aged 18+" : "";
document.getElementById("laDeprivV").textContent = imdLabel;
document.getElementById("laDeprivExtra").textContent = "IMD profile applied as incidence multipliers";
document.getElementById("laRegionV").textContent = region;
document.getElementById("laRegionExtra").textContent = currentLA.blurb || "";
// Pull cost direct from the rendered totals (#footPublic) — authoritative across runs
const footEl = document.getElementById("footPublic");
let costStr = "—", costExtra = "Continue to cascade to compute";
if (footEl && footEl.textContent && footEl.textContent.trim() !== "£0") {
costStr = footEl.textContent.trim();
costExtra = "36-month public-sector cascade cost (current sample)";
}
document.getElementById("laCostV").textContent = costStr;
document.getElementById("laCostExtra").textContent = costExtra;
const narrative = document.getElementById("laMetaNarrative");
if (narrative) {
narrative.innerHTML =
`<strong>${currentLA.name}</strong>` +
(pop > 0 ? ` has roughly <strong>${new Intl.NumberFormat("en-GB").format(pop)}</strong> adults` : "") +
` and is profiled as <strong>${imdLabel}</strong>. ${currentLA.blurb || ""}` +
` <span class="hint">The model scales incidence rates per antecedent group (housing, welfare, mental_health, substance, domestic_abuse, general) using the multipliers attached to this preset. Click "Continue to cascade" when ready.</span>`;
}
}
// Keep main picker in sync — when user changes main picker, propagate to legacy and fire change
document.getElementById("laSelMain")?.addEventListener("change", (e) => {
const legacy = document.getElementById("laSel");
if (legacy) {
legacy.value = e.target.value;
legacy.dispatchEvent(new Event("change", {bubbles: true}));
}
// refresh the context grid after engine catches up
setTimeout(() => { populateLaContextGrid(); updateLaContextChip(); }, 50);
});
// Wire pattern controls
["chainSort2","chainLen2","chainAntFilter2"].forEach(id => {
document.getElementById(id).addEventListener("change", pat2RenderChainList);
});
document.getElementById("loopSort2").addEventListener("change", pat2RenderLoopList);
document.getElementById("pat2Clear").addEventListener("click", () => pat2SetSelection(null));
pat2PopulateAntFilter();
// ===========================================================================
// LA PRESETS — placeholder profiles (real Σ-derived HSV populations from
// Matt's hsvgen pipeline will replace these when .hsv.npy files are supplied).
// Each preset shifts: HSV mix bins, per-antecedent incidence multipliers,
// and population size.
// ===========================================================================
const LA_PRESETS = [
{
id: "average_district",
name: "Average English district council",
pop_adult: 130_000,
imd_label: "mid IMD",
hsv_split: {A: 0.18, C: 0.70, B: 0.12},
inc_mult: { // multiplier on UK-average incidence per antecedent group
housing: 1.0, welfare: 1.0, mental_health: 1.0,
substance: 0.9, domestic_abuse: 0.95, general: 1.0,
},
blurb: "Reference case — modest deprivation, mixed urban/rural.",
},
{
id: "birmingham",
name: "Birmingham (largest core city)",
pop_adult: 850_000,
imd_label: "high IMD (top 10% most deprived)",
hsv_split: {A: 0.10, C: 0.65, B: 0.25},
inc_mult: { housing: 1.5, welfare: 1.6, mental_health: 1.3, substance: 1.4, domestic_abuse: 1.3, general: 1.2 },
blurb: "Large urban LA with significant deprivation and high mental health, housing and substance demand.",
},
{
id: "manchester",
name: "Manchester (high-deprivation core city)",
pop_adult: 460_000,
imd_label: "very high IMD (top 5%)",
hsv_split: {A: 0.08, C: 0.62, B: 0.30},
inc_mult: { housing: 1.7, welfare: 1.6, mental_health: 1.4, substance: 1.5, domestic_abuse: 1.4, general: 1.25 },
blurb: "Deprivation-pressured core city with very high cross-portfolio demand.",
},
{
id: "inner_london",
name: "Inner London borough (Camden / Hackney type)",
pop_adult: 250_000,
imd_label: "mixed (high housing pressure)",
hsv_split: {A: 0.20, C: 0.55, B: 0.25},
inc_mult: { housing: 2.0, welfare: 1.3, mental_health: 1.4, substance: 1.2, domestic_abuse: 1.2, general: 1.0 },
blurb: "Bipolar profile — strong professional cohort alongside high housing-driven vulnerability.",
},
{
id: "cornwall",
name: "Cornwall (large rural unitary)",
pop_adult: 470_000,
imd_label: "mid-low IMD (rural isolation)",
hsv_split: {A: 0.16, C: 0.70, B: 0.14},
inc_mult: { housing: 0.85, welfare: 1.0, mental_health: 1.1, substance: 0.85, domestic_abuse: 0.95, general: 1.0 },
blurb: "Rural unitary — lower housing pressure but higher isolation-driven MH and ageing-population demand.",
},
{
id: "hartlepool",
name: "Hartlepool (small high-deprivation district)",
pop_adult: 75_000,
imd_label: "very high IMD (top 3%)",
hsv_split: {A: 0.07, C: 0.60, B: 0.33},
inc_mult: { housing: 1.5, welfare: 1.9, mental_health: 1.5, substance: 1.6, domestic_abuse: 1.5, general: 1.3 },
blurb: "Small district with concentrated deprivation; high per-capita demand across welfare, MH, substance.",
},
];
// Antecedent → portfolio group for incidence multiplier application
const ANT_PORTFOLIO_GROUP = {};
{
const housingAnts = ["a_0001","a_0002","a_0015","a_0017","a_0027","a_0038","a_0031"];
const welfareAnts = ["a_0007","a_0008","a_0018","a_0022","a_0029","a_0032","a_0039","a_0042"];
const mhAnts = ["a_0005","a_0013","a_0014","a_0041"];
const substanceAnts = ["a_0036","a_0043"]; // a_0043 added in v0.12 (batch 30)
const daAnts = ["a_0003","a_0006","a_0034","a_0040"];
for (const a of housingAnts) ANT_PORTFOLIO_GROUP[a] = "housing";
for (const a of welfareAnts) ANT_PORTFOLIO_GROUP[a] = "welfare";
for (const a of mhAnts) ANT_PORTFOLIO_GROUP[a] = "mental_health";
for (const a of substanceAnts) ANT_PORTFOLIO_GROUP[a] = "substance";
for (const a of daAnts) ANT_PORTFOLIO_GROUP[a] = "domestic_abuse";
}
function antIncMultForLA(ant_id, la) {
const group = ANT_PORTFOLIO_GROUP[ant_id] || "general";
return la.inc_mult[group] || 1.0;
}
let currentLA = LA_PRESETS[0];
function populateLASelector() {
const sel = document.getElementById("laSel");
sel.innerHTML = LA_PRESETS.map(la => `<option value="${la.id}">${la.name}</option>`).join("");
sel.value = currentLA.id;
sel.addEventListener("change", async () => {
currentLA = LA_PRESETS.find(la => la.id === sel.value);
updateLAMeta();
// Re-run simulation to pick up new HSV mix and incidence multipliers
playing = false; playBtn.textContent = "▶ Play"; playBtn.classList.remove("active");
await runSimulation();
setTime(0);
maybeUpdateAggregate(true);
patternsCache = computePatterns();
if (document.getElementById("page-patterns").classList.contains("active")) {
pat2RenderChainList(); pat2RenderLoopList();
}
rebuildInterventions();
FRONTIER.cache = null; NODE_ARRIVALS_LA_CACHE = null; // invalidate caches on LA change
if (document.getElementById("page-interv").classList.contains("active")) refreshFrontierIfReady();
});
}
function updateLAMeta() {
const la = currentLA;
document.getElementById("laMeta").innerHTML =
`<strong>${la.pop_adult.toLocaleString()}</strong> adults · ${la.imd_label}. ` +
`<span style="color: var(--ink-faint)">${la.blurb}</span>`;
}
// ===========================================================================
// INTERVENTION MODELLING — per-antecedent rate multiplier (0..1) lets the
// user dial in prevention scenarios. Re-aggregation re-weights existing
// trajectories by their originating antecedent's multiplier × LA incidence
// multiplier. No re-simulation.
// ===========================================================================
// EVIDENCE-BASED INTERVENTION LIBRARY (v0.1 — 20 UK-evaluated interventions)
// Full citations and methodology in interventions_library_v0.1.md
const INTERVENTIONS_LIBRARY = [{"id":"i_housing_first","name":"Housing First","short":"Settled tenancy + intensive support for chronically homeless complex-needs cohort.","targets":["a_0001","a_0027"],"cost_per_case_gbp":7700,"success_rate":0.54,"evidence":"H","duration_months":12,"la_share":0.8,"typical_la_scale":50,"citation":"MHCLG Housing First Pilots Final Synthesis Report 2024","intervention_type":"tertiary","target_edges":["le_0000070"],"narrative_action":"disrupts the eviction \u2192 re-presentation recurrence loop"},{"id":"i_pretenancy_advice","name":"Pre-tenancy advice + sustainment casework","short":"Active casework for households at imminent risk of PRS tenancy loss.","targets":["a_0001","a_0027"],"cost_per_case_gbp":500,"success_rate":0.35,"evidence":"M","duration_months":6,"la_share":1.0,"typical_la_scale":400,"citation":"Crisis Plan to End Homelessness; MHCLG Tenancy Sustainment REA 2024","intervention_type":"primary","target_edges":[],"narrative_action":"prevents eviction before it crystallises"},{"id":"i_dhp_targeted","name":"Targeted Discretionary Housing Payment use","short":"DHP budget deployed against prevention targets rather than reactive demand.","targets":["a_0001","a_0027"],"cost_per_case_gbp":614,"success_rate":0.4,"evidence":"H","duration_months":1,"la_share":1.0,"typical_la_scale":300,"citation":"DWP Use of DHPs 2024-25 MYR","intervention_type":"primary","target_edges":[],"narrative_action":"prevents tenancy collapse via targeted financial top-up"},{"id":"i_mediation_familyask","name":"Family mediation (asking-to-leave cases)","short":"Mediated resolution where a family member is being asked to leave the home.","targets":["a_0002"],"cost_per_case_gbp":850,"success_rate":0.4,"evidence":"M","duration_months":4,"la_share":0.7,"typical_la_scale":120,"citation":"MoJ Family Mediation programme evaluations","intervention_type":"primary","target_edges":[],"narrative_action":"resolves family disputes before someone is asked to leave"},{"id":"i_mortgage_rescue","name":"Mortgage rescue / forbearance facilitation","short":"LA-facilitated FCA forbearance + bridge support for households at repossession risk.","targets":["a_0017"],"cost_per_case_gbp":4200,"success_rate":0.55,"evidence":"M","duration_months":6,"la_share":0.5,"typical_la_scale":30,"citation":"DCLG Mortgage Rescue Scheme evaluation 2014; FCA forbearance guidance","intervention_type":"primary","target_edges":[],"narrative_action":"prevents mortgage repossession"},{"id":"i_supported_housing_retention","name":"Supported housing retention casework","short":"Intensive housing management to prevent supported-housing exits to street.","targets":["a_0038"],"cost_per_case_gbp":3800,"success_rate":0.45,"evidence":"M","duration_months":12,"la_share":1.0,"typical_la_scale":80,"citation":"Homeless Link Annual Review 2024","intervention_type":"primary","target_edges":[],"narrative_action":"prevents supported-housing exit to street"},{"id":"i_drive_perpetrator","name":"Drive perpetrator programme","short":"High-risk DA perpetrator change programme combining behaviour change + risk management.","targets":["a_0003","a_0034"],"cost_per_case_gbp":6500,"success_rate":0.3,"evidence":"H","duration_months":12,"la_share":0.5,"typical_la_scale":60,"citation":"Drive Systems Change Evaluation 2023; Univ. Bristol independent eval 2016-19","intervention_type":"tertiary","target_edges":["le_0000014","le_0000078"],"narrative_action":"reduces escalation to MARAC and repeat-MARAC referrals"},{"id":"i_idva","name":"Independent Domestic Violence Advisor (IDVA)","short":"Specialist support for high-risk DA victim-survivors at and post MARAC.","targets":["a_0003"],"cost_per_case_gbp":1200,"success_rate":0.4,"evidence":"H","duration_months":6,"la_share":0.5,"typical_la_scale":200,"citation":"SafeLives MARAC datasets + Insights national dataset","intervention_type":"tertiary","target_edges":["le_0000078"],"narrative_action":"reduces repeat-MARAC referrals among supported survivors"},{"id":"i_couples_counselling","name":"Couple/relationship counselling","short":"Relate-style counselling for couples in distressed relationships.","targets":["a_0006"],"cost_per_case_gbp":480,"success_rate":0.18,"evidence":"L","duration_months":4,"la_share":0.3,"typical_la_scale":150,"citation":"Relate evaluation reports; What Works for Wellbeing brief","intervention_type":"primary","target_edges":[],"narrative_action":"reduces relationship breakdown"},{"id":"i_critical_time_psych","name":"Critical Time Intervention (psych discharge)","short":"Time-limited intensive support across the psych-discharge transition.","targets":["a_0005"],"cost_per_case_gbp":2400,"success_rate":0.5,"evidence":"H","duration_months":9,"la_share":0.5,"typical_la_scale":40,"citation":"Susser et al. RCT; Slade et al. UK feasibility; Cochrane CTI evidence base","intervention_type":"tertiary","target_edges":["le_0000005"],"narrative_action":"prevents post-discharge homelessness duty acceptance"},{"id":"i_crisis_resolution_team","name":"Crisis Resolution Home Treatment Team","short":"Community-based crisis response as alternative to MH admission.","targets":["a_0013"],"cost_per_case_gbp":1900,"success_rate":0.55,"evidence":"H","duration_months":1,"la_share":0.0,"typical_la_scale":250,"citation":"NAO Helping people through MH crisis; Glover et al. 2011 BMJ","intervention_type":"tertiary","target_edges":["le_0000023"],"narrative_action":"prevents escalation from MH crisis to MHA assessment"},{"id":"i_iapt_step_up","name":"NHS Talking Therapies (IAPT) early intervention","short":"Earlier triage into IAPT to head off MH crisis escalation.","targets":["a_0013","a_0014"],"cost_per_case_gbp":1228,"success_rate":0.35,"evidence":"H","duration_months":4,"la_share":0.0,"typical_la_scale":1000,"citation":"NHS Talking Therapies programme; Mukuria et al. 2013 (SPPI-uplifted)","intervention_type":"primary","target_edges":[],"narrative_action":"prevents acute MH crisis through earlier treatment"},{"id":"i_brief_alcohol","name":"Brief alcohol intervention (primary care)","short":"5-10 min motivational session in primary care for hazardous drinkers.","targets":["a_0036"],"cost_per_case_gbp":60,"success_rate":0.15,"evidence":"H","duration_months":1,"la_share":0.3,"typical_la_scale":2000,"citation":"Cochrane (Kaner et al.); NICE PH24","intervention_type":"primary","target_edges":[],"narrative_action":"prevents progression to problematic drinking"},{"id":"i_gambling_treatment","name":"NHS Gambling Treatment Service","short":"Specialist psychological treatment for problem gambling.","targets":["a_0036"],"cost_per_case_gbp":1100,"success_rate":0.45,"evidence":"M","duration_months":6,"la_share":0.0,"typical_la_scale":60,"citation":"NHS England Gambling Treatment Service annual reports","intervention_type":"tertiary","target_edges":[],"narrative_action":"treats existing problem gambling \u2014 reduces downstream financial + MH cascade"},{"id":"i_through_the_gate","name":"Enhanced custody resettlement (Through-the-Gate)","short":"Pre- and post-release casework to reduce homelessness and reoffending.","targets":["a_0004"],"cost_per_case_gbp":1800,"success_rate":0.2,"evidence":"M","duration_months":6,"la_share":0.3,"typical_la_scale":150,"citation":"MoJ Justice Data Lab evaluations of resettlement programmes","intervention_type":"tertiary","target_edges":["le_0000004","le_0000017"],"narrative_action":"reduces post-release homelessness and substance referrals"},{"id":"i_carers_respite","name":"Carer's respite breaks","short":"Planned respite + carer support to prevent informal-care breakdown.","targets":["a_0010","a_0025"],"cost_per_case_gbp":1400,"success_rate":0.3,"evidence":"M","duration_months":12,"la_share":1.0,"typical_la_scale":250,"citation":"Carers UK State of Caring; ADASS carer pathway evaluations","intervention_type":"primary","target_edges":[],"narrative_action":"prevents carer breakdown / loss"},{"id":"i_staying_put","name":"Staying Put (care leaver extension)","short":"Foster-carer accommodation continuation past 18 for care leavers.","targets":["a_0021"],"cost_per_case_gbp":8200,"success_rate":0.35,"evidence":"M","duration_months":12,"la_share":1.0,"typical_la_scale":25,"citation":"DfE Staying Put evaluations; What Works for Children's Social Care","intervention_type":"tertiary","target_edges":["le_0000057"],"narrative_action":"prevents care-leaver homelessness duty acceptance"},{"id":"i_citizens_advice_debt","name":"Citizens Advice debt + welfare advice","short":"CA casework on debt, benefits, and welfare entitlements.","targets":["a_0018","a_0029","a_0042"],"cost_per_case_gbp":230,"success_rate":0.25,"evidence":"M","duration_months":2,"la_share":0.6,"typical_la_scale":800,"citation":"Citizens Advice Impact Reports; Pro Bono Economics CA evaluation","intervention_type":"primary","target_edges":[],"narrative_action":"prevents debt and benefit crises escalating"},{"id":"i_lwa_targeted","name":"Targeted Local Welfare Assistance","short":"LWA crisis grants directed at prevention rather than acute relief.","targets":["a_0018","a_0029","a_0042"],"cost_per_case_gbp":320,"success_rate":0.35,"evidence":"M","duration_months":1,"la_share":1.0,"typical_la_scale":600,"citation":"LGA LWA Schemes Review 2023","intervention_type":"primary","target_edges":[],"narrative_action":"prevents financial crisis cascading into homelessness/MH"},{"id":"i_frequent_attender","name":"A&E frequent attender case management","short":"Wraparound case management for frequent A&E attenders.","targets":["a_0041"],"cost_per_case_gbp":2800,"success_rate":0.55,"evidence":"M","duration_months":9,"la_share":0.3,"typical_la_scale":80,"citation":"Faculty of Public Health; RCEM frequent attender literature","intervention_type":"tertiary","target_edges":[],"narrative_action":"reduces repeat A&E presentations and downstream MH/ASC cascade"},{"id":"i_telecare","name":"Telecare / Assistive Technology","short":"Pendant alarms, fall sensors, medication reminders enabling independent living for older adults with chronic conditions.","targets":["a_0009"],"cost_per_case_gbp":350,"success_rate":0.3,"evidence":"H","duration_months":12,"la_share":0.85,"typical_la_scale":600,"citation":"Cochrane (Steventon et al. WSD); NHSX evidence base on assistive technology","intervention_type":"tertiary","target_edges":["le_0000022","le_0000020"],"narrative_action":"delays escalation from chronic illness to hospital discharge with care package and s9 assessment"},{"id":"i_falls_prevention","name":"Falls Prevention (OEP / Tai Chi)","short":"Group exercise programme (Otago / Tai Chi) for older adults at falls risk \u2014 strength, balance, coordination.","targets":["a_0009"],"cost_per_case_gbp":200,"success_rate":0.25,"evidence":"H","duration_months":3,"la_share":0.5,"typical_la_scale":800,"citation":"Cochrane (Sherrington et al. 2019); NICE CG161 Falls in older people","intervention_type":"tertiary","target_edges":["le_0000022"],"narrative_action":"prevents fall-induced escalation from chronic illness to hospital discharge with care package"},{"id":"i_workplace_mh","name":"Workplace Mental Health Programme (MHFA + manager training)","short":"Mental Health First Aiders, manager training, employee wellbeing support across a workplace.","targets":["a_0013","a_0014"],"cost_per_case_gbp":500,"success_rate":0.2,"evidence":"M","duration_months":12,"la_share":0.1,"typical_la_scale":700,"citation":"Deloitte 2022 Mental Health and Employers report; Stevenson/Farmer 2017 Thriving at Work","intervention_type":"primary","target_edges":[],"narrative_action":"earlier identification + support reduces escalation to acute MH crisis and self-harm"},{"id":"i_hosp_social_prescribing","name":"Hospital Social Prescribing (frequent attenders)","short":"Link worker matches A&E frequent attenders to community/peer support, debt advice, GP review.","targets":["a_0041"],"cost_per_case_gbp":250,"success_rate":0.4,"evidence":"M","duration_months":4,"la_share":0.5,"typical_la_scale":200,"citation":"NHS England 2022 social prescribing evaluation; National Academy for Social Prescribing reviews","intervention_type":"tertiary","target_edges":["le_0000163","le_0000164"],"narrative_action":"reduces escalation from repeat A&E to ASC assessment and MH service referral"},{"id":"i_help_to_claim","name":"Help to Claim (Universal Credit advice)","short":"Citizens Advice phone/in-person support through Universal Credit application + first payment.","targets":["a_0008"],"cost_per_case_gbp":200,"success_rate":0.3,"evidence":"M","duration_months":2,"la_share":0.3,"typical_la_scale":500,"citation":"DWP Help to Claim evaluation 2020-22; Citizens Advice impact reports","intervention_type":"primary","target_edges":[],"narrative_action":"supports correct UC claim \u2014 reduces probability of sanction"},{"id":"i_gp_collab_care","name":"GP Collaborative Care for Depression","short":"Care manager + GP + psychiatrist collaborative model for moderate-severe depression in primary care.","targets":["a_0013","a_0014"],"cost_per_case_gbp":600,"success_rate":0.3,"evidence":"H","duration_months":6,"la_share":0.0,"typical_la_scale":400,"citation":"Cochrane review (Archer et al. 2012); IMPACT/COBALT UK trials","intervention_type":"primary","target_edges":[],"narrative_action":"earlier collaborative treatment prevents MH crisis and self-harm escalation"},{"id":"i_restart_scheme","name":"Restart Scheme (long-term unemployment)","short":"DWP-commissioned tailored employment support for those out of work 9+ months.","targets":["a_0039"],"cost_per_case_gbp":400,"success_rate":0.15,"evidence":"M","duration_months":12,"la_share":0.1,"typical_la_scale":600,"citation":"DWP Restart Scheme process & outcomes evaluations 2022-24","intervention_type":"tertiary","target_edges":["le_0000157"],"narrative_action":"moves long-term unemployed back into work, reducing MH service demand cascade"}];
const INTERV = {
activations: {}, // intervention_id -> {active: bool, scale: cases/year}
multipliers: {}, // ant_id -> derived rate multiplier (0..1) [primary]
edge_reductions: {}, // edge_id -> cumulative reduction (0..1) [tertiary]
};
// Per-LA cache: annual arrivals at each node, at LA scale, derived from simulation.
// Used as the denominator for tertiary intervention reductions targeting edges
// whose source is a terminal node (rather than an antecedent).
let NODE_ARRIVALS_LA_CACHE = null;
let NODE_ARRIVALS_LA_KEY = null;
function computeAnnualArrivalsAtNodesLA() {
const key = `${currentLA.id}|${POP_SIZE}`;
if (NODE_ARRIVALS_LA_CACHE && NODE_ARRIVALS_LA_KEY === key) return NODE_ARRIVALS_LA_CACHE;
if (PEOPLE.length === 0) return {};
const arr = {};
const laPopFactor = currentLA.pop_adult / 1_000_000; // simulation is per-1M-equivalent
for (const p of PEOPLE) {
for (const tr of p.transits) {
// eventScale = LA modifier × LA-pop-relative-to-sim × sample weight
const eventScale = antIncMultForLA(tr.ant, currentLA) * laPopFactor * SCALE_WEIGHT;
arr[tr.to] = (arr[tr.to] || 0) + eventScale;
}
}
// Convert 36-month total to annual rate
for (const n in arr) arr[n] = arr[n] / 3;
NODE_ARRIVALS_LA_CACHE = arr;
NODE_ARRIVALS_LA_KEY = key;
return arr;
}
// Derive multipliers from active interventions — split primary (antecedent) vs tertiary (edge)
function recomputeIntervMultipliers() {
const ant_mults = {};
const edge_reds = {};
for (const ant of PAYLOAD.antecedents) ant_mults[ant.id] = 1.0;
const laPopFactor = currentLA.pop_adult / UK_ADULT_POP;
const nodeArrivals = computeAnnualArrivalsAtNodesLA();
for (const iv of INTERVENTIONS_LIBRARY) {
const act = INTERV.activations[iv.id];
if (!act || !act.active || !act.scale || act.scale <= 0) continue;
const type = iv.intervention_type || "primary";
if (type === "tertiary" && iv.target_edges && iv.target_edges.length > 0) {
// Tertiary: reduce specific cascade edges. Denominator depends on the edge source:
// - if edge source is an ANTECEDENT (a_NNNN), use that antecedent's LA annual incidence
// - if edge source is a TERMINAL node (l_NNNN), use simulated LA annual arrivals at that node
// This correctly captures that, e.g., Housing First treats people already at duty acceptance,
// not the full eviction cohort.
for (const eid of iv.target_edges) {
const edge = PAYLOAD.edges.find(e => e.id === eid);
if (!edge) continue;
// Denominator must be ANNUAL EDGE FLOW (not just source arrivals):
// = annual_source_arrivals × edge_probability
// This matches the per-transit cost-scaling model: each transit through
// this edge gets cost × (1 - reduction) applied; total savings = transits × reduction × cost.
let sourceArrivals;
if (edge.a.startsWith("a_")) {
const ant = PAYLOAD.antecedents.find(a => a.id === edge.a);
if (!ant) continue;
sourceArrivals = ant.inc * laPopFactor * antIncMultForLA(edge.a, currentLA);
} else {
sourceArrivals = nodeArrivals[edge.a] || 1;
}
const annualEdgeFlow = Math.max(1, sourceArrivals * (edge.p || 0.01));
const reduction = Math.min(0.95, (act.scale * iv.success_rate) / annualEdgeFlow);
edge_reds[eid] = 1 - (1 - (edge_reds[eid] || 0)) * (1 - reduction);
}
} else {
// Primary (or tertiary fallback): reduce antecedent rate
for (const target of iv.targets) {
const ant = PAYLOAD.antecedents.find(a => a.id === target);
if (!ant) continue;
const laAnnualInc = ant.inc * laPopFactor * antIncMultForLA(target, currentLA);
const reduction = Math.min(0.95, (act.scale * iv.success_rate) / Math.max(1, laAnnualInc));
ant_mults[target] = ant_mults[target] * (1 - reduction);
}
}
}
INTERV.multipliers = ant_mults;
INTERV.edge_reductions = edge_reds;
}
function intervMultiplierFor(ant_id) { return INTERV.multipliers[ant_id] ?? 1.0; }
function intervEdgeReductionFor(edge_id) { return INTERV.edge_reductions[edge_id] ?? 0; }
// Total intervention cost over 36 months
function computeTotalInterventionCost() {
let total_pub = 0, total_la = 0;
for (const iv of INTERVENTIONS_LIBRARY) {
const act = INTERV.activations[iv.id];
if (!act || !act.active || !act.scale) continue;
// Per-year cost = cases × £/case (for episode interventions, cost is per case treated)
// For year-long interventions (duration_months >= 12), £/case is annual rate
// For one-off episodes, £/case is per-case; cases/year × 3 = 36mo total cases
let annual_cost;
if (iv.duration_months >= 12) {
annual_cost = act.scale * iv.cost_per_case_gbp; // cohort-year cost
} else {
annual_cost = act.scale * iv.cost_per_case_gbp; // N cases per year × £/case
}
const cost_3yr = annual_cost * 3;
total_pub += cost_3yr;
total_la += cost_3yr * (iv.la_share || 0);
}
return {pub: total_pub, la: total_la};
}
// ===========================================================================
// COST-EFFECTIVENESS FRONTIER (v0.16)
// ===========================================================================
const FRONTIER = { cache: null, cacheKey: null, tooltip: null };
function frontierCacheKey() { return `${currentLA.id}|${POP_SIZE}`; }
async function computeFrontierData() {
const key = frontierCacheKey();
if (FRONTIER.cache && FRONTIER.cacheKey === key) return FRONTIER.cache;
const loadingEl = document.getElementById("frontierLoading");
const chartArea = document.getElementById("frontierChartArea");
const progressFill = document.getElementById("frontierProgressFill");
const progressMsg = document.getElementById("frontierProgressMsg");
if (loadingEl) loadingEl.style.display = "block";
if (chartArea) chartArea.style.display = "none";
// Save current activations to restore later
const saved = JSON.parse(JSON.stringify(INTERV.activations));
const data = [];
for (let i = 0; i < INTERVENTIONS_LIBRARY.length; i++) {
const iv = INTERVENTIONS_LIBRARY[i];
// Deactivate all, then activate just this one at typical scale
for (const id of Object.keys(INTERV.activations)) {
INTERV.activations[id].active = false;
}
if (!INTERV.activations[iv.id]) INTERV.activations[iv.id] = {active: false, scale: iv.typical_la_scale};
INTERV.activations[iv.id].active = true;
INTERV.activations[iv.id].scale = iv.typical_la_scale;
recomputeIntervMultipliers();
recomputeInterventionsAggregate();
const cost = INTERV._lastBaselinePub > 0 ? (INTERV._lastIntervCost || 0) : 0;
const savings = (INTERV._lastBaselinePub || 0) - (INTERV._lastAfterPub || 0);
const roi = cost > 0 ? (savings - cost) / cost : 0;
data.push({
iv,
cost,
savings,
net: savings - cost,
roi,
scale: iv.typical_la_scale,
});
progressFill.style.width = ((i+1) / INTERVENTIONS_LIBRARY.length * 100).toFixed(0) + "%";
progressMsg.textContent = `${iv.name} (${i+1}/${INTERVENTIONS_LIBRARY.length})`;
await new Promise(r => setTimeout(r, 0));
}
// Restore user's activations
INTERV.activations = saved;
recomputeIntervMultipliers();
recomputeInterventionsAggregate();
// Greedy Pareto frontier: sort by ROI desc, accumulate
const ranked = [...data].filter(d => d.cost > 0 && d.savings > 0).sort((a, b) => b.roi - a.roi);
const frontierPath = [{cost: 0, savings: 0, name: "Origin"}];
let cumCost = 0, cumSavings = 0;
for (const d of ranked) {
cumCost += d.cost;
cumSavings += d.savings;
frontierPath.push({cost: cumCost, savings: cumSavings, name: d.iv.name, iv: d.iv});
}
FRONTIER.cache = { points: data, frontier: frontierPath };
FRONTIER.cacheKey = key;
if (loadingEl) loadingEl.style.display = "none";
if (chartArea) chartArea.style.display = "grid";
return FRONTIER.cache;
}
function renderFrontierChart() {
if (!FRONTIER.cache) return;
const data = FRONTIER.cache;
const svg = d3.select("#frontierSvg");
// Use the SVG element's own bounding rect — it sits in a CSS grid cell
// narrower than the parent .frontier-chart-area. clientWidth on an SVG can be 0,
// so use getBoundingClientRect; fall back to grid-cell calc if needed.
let W = svg.node().getBoundingClientRect().width;
if (W < 100) {
// Fallback: parent grid is (1fr 170px) with 18px gap — subtract barometer column
const parentWidth = svg.node().parentElement.clientWidth;
W = Math.max(400, parentWidth - 170 - 18);
}
W = W - 8;
const H = 440;
svg.attr("viewBox", `0 0 ${W} ${H}`);
svg.selectAll("*").remove();
const margin = {top: 18, right: 28, bottom: 56, left: 84};
const iw = W - margin.left - margin.right;
const ih = H - margin.top - margin.bottom;
const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
// Domain: ZOOMED to fit individual interventions clearly (not the cumulative max).
// Frontier curve extends beyond visible area at high spend — clipped by viewBox.
const maxIvCost = d3.max(data.points, d => d.cost) || 1;
const maxIvSavings = d3.max(data.points, d => d.savings) || 1;
// 25% padding above max single-intervention so labels fit and individual dots aren't on the edge
const x = d3.scaleLinear().domain([0, maxIvCost * 1.25]).range([0, iw]);
const y = d3.scaleLinear().domain([0, maxIvSavings * 1.25]).range([ih, 0]);
// Clip the frontier line/area to chart area so it doesn't draw outside
svg.append("defs").append("clipPath").attr("id", "frontierClipArea")
.append("rect").attr("x", 0).attr("y", 0).attr("width", iw).attr("height", ih);
// Grid lines
const xTicks = x.ticks(6), yTicks = y.ticks(6);
g.append("g").selectAll("line").data(yTicks).join("line")
.attr("class", "grid-line").attr("x1", 0).attr("x2", iw)
.attr("y1", d => y(d)).attr("y2", d => y(d));
g.append("g").selectAll("line").data(xTicks).join("line")
.attr("class", "grid-line").attr("y1", 0).attr("y2", ih)
.attr("x1", d => x(d)).attr("x2", d => x(d));
// Break-even diagonal (savings = cost) — emphasised as the key visual reference
const maxAxis = Math.min(maxIvCost, maxIvSavings) * 1.05;
g.append("line").attr("class", "breakeven-line")
.attr("x1", x(0)).attr("y1", y(0))
.attr("x2", x(maxAxis)).attr("y2", y(maxAxis));
g.append("text").attr("class", "breakeven-label")
.attr("x", x(maxAxis * 0.92)).attr("y", y(maxAxis * 0.92) - 8)
.attr("text-anchor", "end").text("Break-even line (savings = cost)");
g.append("text").attr("class", "breakeven-sub")
.attr("x", x(maxAxis * 0.92)).attr("y", y(maxAxis * 0.92) + 5)
.attr("text-anchor", "end").text("↑ above this line = net-positive for the LA");
// Green frontier line removed (v0.16.3) — barometer on the right now shows the ceiling.
// The chart focuses on individual interventions vs the break-even diagonal.
// Soft tint the "net-positive" half-plane (above break-even) so users can read the boundary at a glance
g.append("path").attr("class", "net-positive-region")
.attr("d", `M${x(0)},${y(0)} L${x(maxAxis)},${y(maxAxis)} L${x(0)},${y(maxIvSavings * 1.25)} Z`);
// Axes labels & ticks
const fmtMoney = v => v >= 1e6 ? "£" + (v/1e6).toFixed(v >= 1e7 ? 0 : 1) + "m" : v >= 1e3 ? "£" + (v/1e3).toFixed(0) + "k" : "£" + Math.round(v);
g.append("g").selectAll("text").data(xTicks).join("text").attr("class", "axis-tick")
.attr("x", d => x(d)).attr("y", ih + 16).attr("text-anchor", "middle").text(fmtMoney);
g.append("g").selectAll("text").data(yTicks).join("text").attr("class", "axis-tick")
.attr("x", -8).attr("y", d => y(d) + 4).attr("text-anchor", "end").text(fmtMoney);
g.append("text").attr("class", "axis-label")
.attr("x", iw/2).attr("y", ih + 40).attr("text-anchor", "middle")
.text("36-month intervention cost (£)");
g.append("text").attr("class", "axis-label")
.attr("transform", "rotate(-90)").attr("x", -ih/2).attr("y", -56).attr("text-anchor", "middle")
.text("36-month downstream savings (£)");
// Tooltip
if (!FRONTIER.tooltip) {
FRONTIER.tooltip = d3.select("body").append("div").attr("class", "frontier-tooltip");
}
const tooltip = FRONTIER.tooltip;
// Intervention dots
const dots = g.append("g").selectAll("circle.iv-dot").data(data.points).join("circle")
.attr("class", d => {
const act = INTERV.activations[d.iv.id];
return "iv-dot " + (act && act.active ? "is-active" : "");
})
.attr("cx", d => x(d.cost)).attr("cy", d => y(d.savings))
.attr("r", d => {
const act = INTERV.activations[d.iv.id];
return act && act.active ? 9 : 6;
})
.attr("fill", d => d.iv.intervention_type === "tertiary" ? "#a78bfa" : "#34d399")
.on("mouseover", (event, d) => {
tooltip.classed("show", true).html(`
<div class="tt-title">${d.iv.name}</div>
<div class="tt-row"><span class="l">scale</span><span>${d.scale.toLocaleString()} cases/yr</span></div>
<div class="tt-row"><span class="l">36mo cost</span><span>${fmtMoney(d.cost)}</span></div>
<div class="tt-row"><span class="l">36mo savings</span><span>${fmtMoney(d.savings)}</span></div>
<div class="tt-row"><span class="l">net benefit</span><span>${fmtMoney(d.net)}</span></div>
<div class="tt-row"><span class="l">ROI</span><span>${(d.roi*100).toFixed(0)}%</span></div>
<div class="tt-meta">${d.iv.intervention_type === "tertiary" ? "Cascade disruption" : "Primary prevention"} · ${d.iv.evidence} evidence</div>
<div class="tt-meta" style="color:#10b981;font-weight:600;">Click to add to active portfolio</div>
`);
})
.on("mousemove", (event) => {
tooltip.style("left", (event.pageX + 14) + "px").style("top", (event.pageY + 14) + "px");
})
.on("mouseout", () => tooltip.classed("show", false))
.on("click", (event, d) => {
if (!INTERV.activations[d.iv.id]) INTERV.activations[d.iv.id] = {active: false, scale: d.iv.typical_la_scale};
INTERV.activations[d.iv.id].active = true;
INTERV.activations[d.iv.id].scale = d.iv.typical_la_scale;
onInterventionsChanged();
renderFrontierChart(); // refresh to highlight new active dot
});
// Labels — show top-10 high-ROI interventions always (greyed if inactive, bright if active)
// PLUS any active intervention that wouldn't otherwise be in top 10
const topROISet = new Set([...data.points].sort((a, b) => b.roi - a.roi).slice(0, 10).map(d => d.iv.id));
const labelData = data.points.filter(d => {
const act = INTERV.activations[d.iv.id];
if (act && act.active) return true;
return topROISet.has(d.iv.id);
});
g.append("g").selectAll("text.iv-label").data(labelData).join("text")
.attr("class", d => {
const act = INTERV.activations[d.iv.id];
if (act && act.active) return "iv-label is-active";
if (topROISet.has(d.iv.id)) return "iv-label is-top";
return "iv-label";
})
.attr("x", d => x(d.cost) + 11).attr("y", d => y(d.savings) + 4)
.text(d => d.iv.name.length > 26 ? d.iv.name.slice(0, 25) + "…" : d.iv.name);
// (Inline frontier-curve annotation removed in v0.16.3 along with the green line.)
// Current portfolio marker (sum of active interventions' standalone cost and savings)
let curCost = 0, curSavings = 0;
for (const d of data.points) {
const act = INTERV.activations[d.iv.id];
if (act && act.active) {
// Scale by user's chosen scale relative to typical
const scaleRatio = act.scale / Math.max(1, d.iv.typical_la_scale);
curCost += d.cost * scaleRatio;
curSavings += d.savings * scaleRatio;
}
}
if (curCost > 0 || curSavings > 0) {
// Only show in-chart if it fits the zoomed range
const inRange = curCost <= maxIvCost * 1.25 && curSavings <= maxIvSavings * 1.25;
if (inRange) {
g.append("circle").attr("class", "current-portfolio-dot")
.attr("cx", x(curCost)).attr("cy", y(curSavings)).attr("r", 11);
g.append("text").attr("class", "current-portfolio-label")
.attr("x", x(curCost) + 16).attr("y", y(curSavings) - 4)
.text(`Your portfolio · ${fmtMoney(curCost)}${fmtMoney(curSavings)}`);
}
}
// Render the portfolio barometer alongside
renderBarometer(curCost, curSavings, data);
}
// Vertical portfolio barometer — shows the portfolio's savings as a fraction of
// the maximum achievable (frontier endpoint). Ball moves up/down.
function renderBarometer(curCost, curSavings, data) {
const svg = d3.select("#barometerSvg");
const W = 170, H = 360;
svg.selectAll("*").remove();
const maxSavings = data.frontier[data.frontier.length-1].savings || 1;
const maxCost = data.frontier[data.frontier.length-1].cost || 1;
const margin = {top: 30, bottom: 28};
const trackTop = margin.top, trackBottom = H - margin.bottom, trackX = 50;
const trackLen = trackBottom - trackTop;
const fmt = v => v >= 1e6 ? "£" + (v/1e6).toFixed(v >= 1e7 ? 0 : 1) + "m" : v >= 1e3 ? "£" + (v/1e3).toFixed(0) + "k" : "£" + Math.round(v);
// Vertical track
svg.append("line").attr("class", "scale-line")
.attr("x1", trackX).attr("x2", trackX).attr("y1", trackTop).attr("y2", trackBottom);
// Tick marks at 0/25/50/75/100%
for (let pct = 0; pct <= 100; pct += 25) {
const ty = trackBottom - (pct / 100) * trackLen;
svg.append("line").attr("class", "scale-tick")
.attr("x1", trackX - 6).attr("x2", trackX + 6).attr("y1", ty).attr("y2", ty);
svg.append("text").attr("class", "scale-tick-label")
.attr("x", trackX - 10).attr("y", ty + 3).attr("text-anchor", "end")
.text(pct + "%");
}
// Anchor labels at top & bottom
svg.append("text").attr("class", "scale-max-label")
.attr("x", trackX + 12).attr("y", trackTop - 12).attr("text-anchor", "start")
.text(`Max ${fmt(maxSavings)}`);
svg.append("text").attr("class", "scale-max-label")
.attr("x", trackX + 12).attr("y", trackTop + 2).attr("text-anchor", "start")
.style("font-size", "9px").style("opacity", "0.7").style("font-weight", "500")
.text(`cost ${fmt(maxCost)}`);
svg.append("text").attr("class", "scale-anchor")
.attr("x", trackX + 12).attr("y", trackBottom + 4).attr("text-anchor", "start")
.text("£0");
// Filled portion (from bottom up to current)
const pct = maxSavings > 0 ? Math.max(0, Math.min(1, curSavings / maxSavings)) : 0;
const ballY = trackBottom - pct * trackLen;
svg.append("line").attr("class", "scale-fill")
.attr("x1", trackX).attr("x2", trackX)
.attr("y1", trackBottom).attr("y2", ballY);
// Ball
svg.append("circle").attr("class", "ball")
.attr("cx", trackX).attr("cy", ballY).attr("r", 11);
// Ball labels (to the right)
svg.append("text").attr("class", "ball-pct")
.attr("x", trackX + 18).attr("y", ballY + 6).attr("text-anchor", "start")
.text(`${(pct * 100).toFixed(0)}%`);
svg.append("text").attr("class", "ball-label")
.attr("x", trackX + 18).attr("y", ballY + 22).attr("text-anchor", "start")
.text(fmt(curSavings));
// Update footer
const footer = document.getElementById("barometerFooter");
if (footer) {
if (curSavings === 0) {
footer.innerHTML = `Activate interventions to see how close you can get to the <strong>maximum-savings ceiling of ${fmt(maxSavings)}</strong>.`;
} else {
const remaining = maxSavings - curSavings;
footer.innerHTML = `Your active portfolio captures <strong>${(pct*100).toFixed(0)}%</strong> of the <strong>${fmt(maxSavings)}</strong> theoretical maximum. <strong>${fmt(remaining)}</strong> headroom remains.`;
}
}
}
async function refreshFrontierIfReady() {
if (PEOPLE.length === 0) return;
await computeFrontierData();
renderFrontierChart();
}
function rebuildInterventions() {
if (!patternsCache) { patternsCache = computePatterns(); }
// Initialize activations: each intervention starts inactive at its typical_la_scale
for (const iv of INTERVENTIONS_LIBRARY) {
if (INTERV.activations[iv.id] === undefined) {
INTERV.activations[iv.id] = {active: false, scale: iv.typical_la_scale};
}
}
document.getElementById("libCount").textContent = INTERVENTIONS_LIBRARY.length;
renderInterventionList();
recomputeIntervMultipliers();
recomputeInterventionsAggregate();
}
function renderInterventionList() {
const container = document.getElementById("interventionList");
if (!container) return;
const sort = document.getElementById("intervSort").value;
const evFilter = document.getElementById("intervFilter").value;
const baselineByAnt = computeBaselineCostByAntecedent();
const portfolioColor = port => PORTFOLIO_COLORS[port] || PORTFOLIO_COLORS.other;
// Compute potential-impact metric per intervention (sum of target antecedent baseline cost × success rate)
function potentialImpact(iv) {
let s = 0;
for (const t of iv.targets) s += (baselineByAnt[t] || 0) * iv.success_rate;
return s;
}
let list = INTERVENTIONS_LIBRARY.slice();
if (evFilter === "H") list = list.filter(iv => iv.evidence === "H");
else if (evFilter === "M") list = list.filter(iv => iv.evidence === "H" || iv.evidence === "M");
if (sort === "impact") list.sort((a, b) => potentialImpact(b) - potentialImpact(a));
else if (sort === "evidence") {
const order = {H: 0, M: 1, L: 2};
list.sort((a, b) => order[a.evidence] - order[b.evidence]);
} else if (sort === "cost") list.sort((a, b) => a.cost_per_case_gbp - b.cost_per_case_gbp);
else if (sort === "targets") list.sort((a, b) => a.targets[0].localeCompare(b.targets[0]));
const laPopFactor = currentLA.pop_adult / UK_ADULT_POP;
container.innerHTML = list.map(iv => {
const act = INTERV.activations[iv.id] || {active: false, scale: iv.typical_la_scale};
const targetsHtml = iv.targets.map(t => {
const ant = PAYLOAD.antecedents.find(a => a.id === t);
const nm = ant ? ant.name : t;
const short = nm.length > 28 ? nm.slice(0, 27) + "…" : nm;
const portfolio = ANT_PORTFOLIO_GROUP[t] || "other";
const col = PORTFOLIO_COLORS[portfolio] || PORTFOLIO_COLORS.other;
return `<span class="ant-pill" title="${nm}"><span class="sw" style="background:${col}"></span>${short}</span>`;
}).join("");
// Impact preview when active
let impactHtml = "";
if (act.active && act.scale > 0) {
let totalAntInc = 0;
for (const t of iv.targets) {
const ant = PAYLOAD.antecedents.find(a => a.id === t);
if (!ant) continue;
totalAntInc += ant.inc * laPopFactor * antIncMultForLA(t, currentLA);
}
const reduction = Math.min(95, ((act.scale * iv.success_rate) / Math.max(1, totalAntInc)) * 100);
const yearCost = act.scale * iv.cost_per_case_gbp;
const cost3yr = yearCost * 3;
const cost3yrLa = cost3yr * (iv.la_share || 0);
impactHtml = `<div class="card-impact">
Treating <strong>${act.scale.toLocaleString()} cases/year</strong> in this LA →
<strong>${reduction.toFixed(0)}%</strong> reduction in target antecedent rate.
Cost: <strong>${formatMoney(cost3yr)}</strong> over 36 months (${formatMoney(cost3yrLa)} LA-borne).
</div>`;
}
const typeLabel = iv.intervention_type === "tertiary" ? "Cascade disruption" : "Primary prevention";
const typeColor = iv.intervention_type === "tertiary" ? "#a78bfa" : "#34d399";
return `<div class="interv-card ${act.active ? 'active' : ''}" data-iid="${iv.id}">
<div class="card-hdr">
<span class="card-title">${iv.name}</span>
<div style="display:flex;gap:5px;align-items:baseline;">
<span class="ev-badge" style="background:rgba(0,0,0,0.25);color:${typeColor};border:1px solid ${typeColor};">${typeLabel}</span>
<span class="ev-badge ev-${iv.evidence}">${iv.evidence}</span>
</div>
</div>
<div class="card-short">${iv.short}<br><em style="color:${typeColor};">→ ${iv.narrative_action || ''}</em></div>
<div class="card-targets">${iv.intervention_type === "tertiary" ? "Disrupts cascade from:" : "Prevents:"} ${targetsHtml}</div>
<div class="card-spec">
<div><span class="l">£/case</span><span class="v">£${iv.cost_per_case_gbp.toLocaleString()}</span></div>
<div><span class="l">Success rate</span><span class="v">${(iv.success_rate*100).toFixed(0)}%</span></div>
<div><span class="l">Dose</span><span class="v">${iv.duration_months}mo</span></div>
</div>
<div class="card-config">
<label class="activate"><input type="checkbox" data-iid="${iv.id}" ${act.active ? 'checked' : ''}> Activate</label>
<div class="scale-input">
<label>Cases/year:</label>
<input type="number" min="0" step="10" value="${act.scale}" data-iid="${iv.id}">
</div>
</div>
${impactHtml}
<div class="cite">${iv.citation}</div>
</div>`;
}).join("");
container.querySelectorAll("input[type=checkbox]").forEach(cb => {
cb.addEventListener("change", () => {
const iid = cb.dataset.iid;
if (!INTERV.activations[iid]) INTERV.activations[iid] = {active: false, scale: 0};
INTERV.activations[iid].active = cb.checked;
cb.closest(".interv-card").classList.toggle("active", cb.checked);
onInterventionsChanged();
});
});
container.querySelectorAll("input[type=number]").forEach(inp => {
inp.addEventListener("input", () => {
const iid = inp.dataset.iid;
if (!INTERV.activations[iid]) INTERV.activations[iid] = {active: false, scale: 0};
INTERV.activations[iid].scale = parseInt(inp.value) || 0;
onInterventionsChanged();
});
});
}
function onInterventionsChanged() {
recomputeIntervMultipliers();
recomputeInterventionsAggregate();
renderInterventionList(); // refresh impact previews on cards
// Refresh frontier chart's "current portfolio" marker (uses cached single-int data)
if (FRONTIER.cache && FRONTIER.cacheKey === frontierCacheKey()) renderFrontierChart();
}
// Aggregate cost from trajectories with current intervention multipliers + LA scaling
function recomputeInterventionsAggregate() {
// Each simulated transit belongs to an originating antecedent (PAYLOAD trajectory tagging).
// Apply weight = LA_inc_mult(ant) × INTERV.multiplier(ant) × LA_pop_scaling × duration cost
if (PEOPLE.length === 0) return;
const laScale = currentLA.pop_adult / (POP_SIZE * SCALE_WEIGHT) * SCALE_WEIGHT;
// Simpler: cohort represents 1M nationally-rated; multiply totals by (la_pop/1M) × per-antecedent_la_mult × per-antecedent_interv_mult
// We do per-antecedent re-aggregation:
let baselinePub = 0, baselineLa = 0, afterPub = 0, afterLa = 0;
const baseByPortfolio = {}, afterByPortfolio = {};
// Recompute per-person cost — but tag each person's portfolio contributions by which antecedent triggered them.
// For speed: pre-compute baseline once per LA, then for each slider change just scale by multiplier ratio per antecedent.
// Simpler approach: iterate transits grouped by antecedent.
// Each transit was simulated assuming UK-baseline antecedent rate. To compute cost at LA scale with intervention,
// multiply that transit's cost contribution by (la_inc_mult × interv_mult × la_pop / 1M_norm).
const laPopFactor = currentLA.pop_adult / 1_000_000; // LA represents this fraction of the 1M-pop simulation
const antScale = {};
for (const ant of PAYLOAD.antecedents) {
antScale[ant.id] = antIncMultForLA(ant.id, currentLA) * laPopFactor;
}
// Walk per-person costs WITH per-antecedent attribution
// For each event: track baseline cost (no intervention) AND after cost (with tertiary edge reductions applied
// — propagated through the cascade tree using per-transit lineage multipliers).
for (const p of PEOPLE) {
const stateIv = {};
const eventCosts = {};
function ensureEvent(evi) {
if (!eventCosts[evi]) eventCosts[evi] = {pub_base: 0, la_base: 0, pub_after: 0, la_after: 0, port_base: {}, port_after: {}};
return eventCosts[evi];
}
function addCost(evi, port, cp, cl, edgeMult) {
const ec = ensureEvent(evi);
const cp_after = cp * edgeMult, cl_after = cl * edgeMult;
ec.pub_base += cp; ec.la_base += cl;
ec.pub_after += cp_after; ec.la_after += cl_after;
if (!ec.port_base[port]) { ec.port_base[port] = {pub: 0, la: 0}; ec.port_after[port] = {pub: 0, la: 0}; }
ec.port_base[port].pub += cp; ec.port_base[port].la += cl;
ec.port_after[port].pub += cp_after; ec.port_after[port].la += cl_after;
}
// ---- Compute per-transit lineage multipliers, grouped by event ----
// For each event-cascade, walk transits in arrival-time order. Each transit's
// lineage multiplier = parent's multiplier × (1 - edge_reduction[this.eid]).
// Parent = most-recent transit ending at this transit's source node in same event.
// If source is the antecedent itself, parent multiplier = 1.0.
const transitLineageMult = new Map(); // transit -> lineage cumulative multiplier
const byEvent = {};
for (const tr of p.transits) {
if (!byEvent[tr.evi]) byEvent[tr.evi] = [];
byEvent[tr.evi].push(tr);
}
for (const [evi, transits] of Object.entries(byEvent)) {
const evtSorted = [...transits].sort((a, b) => a.arrive - b.arrive);
// Map: node_id -> most recent arrival's lineage multiplier within this event
const nodeLastMult = {};
for (const tr of evtSorted) {
const parentMult = (tr.from && tr.from.startsWith("a_"))
? 1.0
: (nodeLastMult[tr.from] !== undefined ? nodeLastMult[tr.from] : 1.0);
const thisEdgeMult = 1 - intervEdgeReductionFor(tr.eid);
const cum = parentMult * thisEdgeMult;
transitLineageMult.set(tr, cum);
// Update node arrival tracker — the latest arrival at tr.to has this cum multiplier
nodeLastMult[tr.to] = cum;
}
}
const sorted = [...p.transits].sort((a,b) => a.arrive - b.arrive);
for (const tr of sorted) {
const uc = UNIT_COSTS[tr.to];
if (!uc) continue;
const port = (NODE_BY_ID[tr.to] || {p:"other"}).p;
// Use full lineage multiplier (propagates tertiary edge reductions downstream within cascade)
const lineageMult = transitLineageMult.get(tr) ?? 1.0;
if (YEAR_NODES.has(tr.to)) {
if (!stateIv[tr.to]) stateIv[tr.to] = [];
const open = stateIv[tr.to].find(iv => !iv.closed);
if (open) {
open.target_close = Math.min(HORIZON, tr.arrive + 12);
open._evi = tr.evi;
open._edgeMult = Math.min(open._edgeMult ?? 1.0, lineageMult);
} else {
stateIv[tr.to].push({open: tr.arrive, target_close: Math.min(HORIZON, tr.arrive + 12), closed: false, _evi: tr.evi, _edgeMult: lineageMult});
}
} else {
addCost(tr.evi, port, uc.cost_public, uc.cost_la_only, lineageMult);
}
if (EXIT_MAP[tr.to]) {
for (const en of EXIT_MAP[tr.to]) {
const iv = stateIv[en]?.find(iv => !iv.closed);
if (iv && tr.arrive > iv.open) iv.target_close = tr.arrive;
}
}
}
for (const [nid, ivs] of Object.entries(stateIv)) {
const uc = UNIT_COSTS[nid];
if (!uc) continue;
const port = (NODE_BY_ID[nid] || {p:"other"}).p;
const mpub = uc.cost_public / 12, mla = uc.cost_la_only / 12;
for (const iv of ivs) {
const close = Math.min(HORIZON, iv.target_close);
const dur = Math.max(0, close - iv.open);
addCost(iv._evi, port, dur * mpub, dur * mla, iv._edgeMult ?? 1.0);
}
}
// Apply per-event scaling: baseline antecedent rate × (primary intervention multiplier for after only)
for (const [evi, ec] of Object.entries(eventCosts)) {
const ev = p.events[parseInt(evi)];
if (!ev) continue;
const baseScale = antScale[ev.ant_id] * SCALE_WEIGHT;
const afterAntScale = baseScale * intervMultiplierFor(ev.ant_id);
baselinePub += ec.pub_base * baseScale;
baselineLa += ec.la_base * baseScale;
afterPub += ec.pub_after * afterAntScale;
afterLa += ec.la_after * afterAntScale;
for (const [port, c] of Object.entries(ec.port_base)) {
if (!baseByPortfolio[port]) baseByPortfolio[port] = {pub: 0, la: 0};
baseByPortfolio[port].pub += c.pub * baseScale;
baseByPortfolio[port].la += c.la * baseScale;
}
for (const [port, c] of Object.entries(ec.port_after)) {
if (!afterByPortfolio[port]) afterByPortfolio[port] = {pub: 0, la: 0};
afterByPortfolio[port].pub += c.pub * afterAntScale;
afterByPortfolio[port].la += c.la * afterAntScale;
}
}
}
const savings = baselinePub - afterPub;
const savingsLa = baselineLa - afterLa;
document.getElementById("savingsBig").textContent = formatMoney(savings);
document.getElementById("savingsLbl").textContent = savings > 0
? `${formatMoney(savingsLa)} of which falls on the LA`
: "Activate an intervention on the left to start modelling";
document.getElementById("baselinePub").textContent = formatMoney(baselinePub);
document.getElementById("afterPub").textContent = formatMoney(afterPub);
document.getElementById("baselineLa").textContent = formatMoney(baselineLa);
document.getElementById("afterLa").textContent = formatMoney(afterLa);
// ROI — auto-derived from active interventions (cost field becomes read-only display)
const totalCost = computeTotalInterventionCost();
const intervCost = totalCost.pub;
document.getElementById("intervCost").value = Math.round(intervCost);
let payback = "—", roi = "—";
if (savings > 0 && intervCost > 0) {
const monthsForPayback = (intervCost / savings) * 36;
payback = monthsForPayback < 36 ? monthsForPayback.toFixed(0) + " mo" : ">36 mo";
const roiVal = (savings - intervCost) / intervCost * 100;
roi = roiVal.toFixed(0) + "%";
}
document.getElementById("paybackMo").textContent = payback;
document.getElementById("roiPct").textContent = roi;
// Service portfolio impact
const portfolioImpact = [];
for (const port of Object.keys(baseByPortfolio)) {
const delta = (baseByPortfolio[port].pub || 0) - (afterByPortfolio[port]?.pub || 0);
if (delta > 0) portfolioImpact.push({port, delta});
}
portfolioImpact.sort((a, b) => b.delta - a.delta);
const svcList = document.getElementById("svcImpactList");
if (portfolioImpact.length === 0) {
svcList.innerHTML = '<div style="color: var(--ink-faint); font-size: 11px; padding: 12px; text-align: center;">Adjust sliders to see per-service impact</div>';
} else {
svcList.innerHTML = portfolioImpact.map(p => {
const portName = (PORTFOLIO_LABELS && PORTFOLIO_LABELS[p.port]) || p.port.replace(/_/g, ' ');
const col = PORTFOLIO_COLORS[p.port] || PORTFOLIO_COLORS.other;
return `<div class="svc-impact-row"><span class="nm"><span class="sw" style="background:${col}"></span>${portName}</span><span class="delta">−${formatMoney(p.delta)}</span></div>`;
}).join("");
}
// Stash for exec summary
INTERV._lastBaselinePub = baselinePub;
INTERV._lastAfterPub = afterPub;
INTERV._lastBaselineLa = baselineLa;
INTERV._lastAfterLa = afterLa;
INTERV._lastIntervCost = intervCost;
INTERV._lastPayback = payback;
INTERV._lastRoi = roi;
INTERV._lastPortfolioImpact = portfolioImpact;
INTERV._lastBaseByPortfolio = baseByPortfolio;
}
function computeBaselineCostByAntecedent() {
// For ranking the sliders by impact — quick pass over PEOPLE, tagging cost to originating antecedent
const byAnt = {};
const laPopFactor = currentLA.pop_adult / 1_000_000;
for (const p of PEOPLE) {
const eventCosts = {};
const stateIv = {};
const sorted = [...p.transits].sort((a,b) => a.arrive - b.arrive);
for (const tr of sorted) {
const uc = UNIT_COSTS[tr.to]; if (!uc) continue;
if (YEAR_NODES.has(tr.to)) {
if (!stateIv[tr.to]) stateIv[tr.to] = [];
const open = stateIv[tr.to].find(iv => !iv.closed);
if (open) { open.target_close = Math.min(HORIZON, tr.arrive + 12); open._evi = tr.evi; }
else stateIv[tr.to].push({open: tr.arrive, target_close: Math.min(HORIZON, tr.arrive + 12), closed: false, _evi: tr.evi});
} else {
eventCosts[tr.evi] = (eventCosts[tr.evi] || 0) + uc.cost_public;
}
if (EXIT_MAP[tr.to]) for (const en of EXIT_MAP[tr.to]) {
const iv = stateIv[en]?.find(iv => !iv.closed);
if (iv && tr.arrive > iv.open) iv.target_close = tr.arrive;
}
}
for (const [nid, ivs] of Object.entries(stateIv)) {
const uc = UNIT_COSTS[nid]; if (!uc) continue;
const mpub = uc.cost_public / 12;
for (const iv of ivs) {
const close = Math.min(HORIZON, iv.target_close);
const dur = Math.max(0, close - iv.open);
eventCosts[iv._evi] = (eventCosts[iv._evi] || 0) + dur * mpub;
}
}
for (const [evi, cp] of Object.entries(eventCosts)) {
const ev = p.events[parseInt(evi)];
if (!ev) continue;
const scale = antIncMultForLA(ev.ant_id, currentLA) * laPopFactor * SCALE_WEIGHT;
byAnt[ev.ant_id] = (byAnt[ev.ant_id] || 0) + cp * scale;
}
}
return byAnt;
}
// Wire intervention controls
function wireInterventionsControls() {
document.getElementById("intervSort").addEventListener("change", renderInterventionList);
document.getElementById("intervFilter").addEventListener("change", renderInterventionList);
document.getElementById("intervReset").addEventListener("click", () => {
for (const iid of Object.keys(INTERV.activations)) INTERV.activations[iid].active = false;
onInterventionsChanged();
});
// intervCost field now auto-fills from active interventions; make it read-only
const costInput = document.getElementById("intervCost");
if (costInput) { costInput.readOnly = true; costInput.title = "Auto-computed from active interventions"; costInput.style.opacity = "0.85"; }
}
// ===========================================================================
// EXEC SUMMARY — print view
// ===========================================================================
function renderExecSummary() {
const la = currentLA;
// Ensure aggregates are up to date
recomputeInterventionsAggregate();
const baselinePub = INTERV._lastBaselinePub || 0;
const afterPub = INTERV._lastAfterPub || baselinePub;
const savings = baselinePub - afterPub;
const intervCost = INTERV._lastIntervCost || 0;
const payback = INTERV._lastPayback || "—";
const roi = INTERV._lastRoi || "—";
// Top 3 antecedents by baseline cost
const baseByAnt = computeBaselineCostByAntecedent();
const top3 = Object.entries(baseByAnt).sort((a,b) => b[1] - a[1]).slice(0,3).map(([aid, c]) => {
const ant = PAYLOAD.antecedents.find(a => a.id === aid);
return {name: ant ? ant.name : aid, cost: c, ratePerMo: ant ? (ant.inc * (1_000_000/UK_ADULT_POP) / 12 * antIncMultForLA(aid, la)) : 0};
});
// Top 3 portfolios under intervention reduction (use whatever sliders are set)
const portfolioImpact = INTERV._lastPortfolioImpact || [];
const top3port = portfolioImpact.slice(0, 3);
// Concentration metric: estimate from per-person costs (use sample, scale insensitive)
const personCosts = [];
for (const p of PEOPLE) personCosts.push(p.cost_public);
personCosts.sort((a, b) => b - a);
const top5pctCount = Math.max(1, Math.floor(personCosts.length * 0.05));
const top5sum = personCosts.slice(0, top5pctCount).reduce((s, c) => s + c, 0);
const allsum = personCosts.reduce((s, c) => s + c, 0);
const concentration = allsum > 0 ? (top5sum / allsum * 100).toFixed(0) : 0;
const pctTouched = PEOPLE.length ? ((PEOPLE.filter(p => p.cost_public > 0).length / PEOPLE.length) * 100).toFixed(1) : 0;
const todayStr = new Date().toLocaleDateString("en-GB", {day: "numeric", month: "long", year: "numeric"});
document.getElementById("execPrintView").innerHTML = `
<div style="text-align: right; font-size: 10px; color: #6b7280; margin-bottom: 12px;">PSI · Behavioural Physics LEG · Cascade & Cost · ${todayStr}</div>
<h1>${la.name} — 36-month cascade cost forecast</h1>
<p>${la.pop_adult.toLocaleString()} adults · ${la.imd_label}. ${la.blurb}</p>
<div class="hero">
<div class="big">${formatMoney(baselinePub)}</div>
<div class="sub">projected public-sector cost over 36 months from cascading life events</div>
<div class="meta">LA-borne share: ${formatMoney(INTERV._lastBaselineLa)} · ${pctTouched}% of adults touch any service · top 5% of touched people drive ${concentration}% of cost</div>
</div>
<h2>The three antecedents driving most cost</h2>
<table>
<thead><tr><th>Antecedent (triggering event)</th><th style="text-align:right">Rate/month per 1M</th><th style="text-align:right">36-month cost contribution</th></tr></thead>
<tbody>
${top3.map(t => `<tr><td>${t.name}</td><td class="num">${t.ratePerMo.toFixed(1)}</td><td class="num">${formatMoney(t.cost)}</td></tr>`).join("")}
</tbody>
</table>
<p class="meta">These are the highest-leverage prevention targets for ${la.name}.</p>
${savings > 0 ? `
<h2>Intervention scenario (as currently configured in dashboard)</h2>
<table>
<thead><tr><th></th><th style="text-align:right">Public sector</th><th style="text-align:right">LA-borne</th></tr></thead>
<tbody>
<tr><td>Baseline 36-month cost</td><td class="num">${formatMoney(baselinePub)}</td><td class="num">${formatMoney(INTERV._lastBaselineLa)}</td></tr>
<tr><td>After intervention</td><td class="num">${formatMoney(afterPub)}</td><td class="num">${formatMoney(INTERV._lastAfterLa)}</td></tr>
<tr><td><strong>Savings</strong></td><td class="num"><strong>${formatMoney(savings)}</strong></td><td class="num"><strong>${formatMoney(INTERV._lastBaselineLa - INTERV._lastAfterLa)}</strong></td></tr>
</tbody>
</table>
<h3>Invest-to-save</h3>
<p>Intervention budget: <strong>${formatMoney(intervCost)}</strong> over 36 months. Payback: <strong>${payback}</strong>. 36-month ROI: <strong>${roi}</strong>.</p>
${top3port.length ? `<h3>Service portfolios benefitting most</h3>
<ul>${top3port.map(p => `<li><strong>${(PORTFOLIO_LABELS && PORTFOLIO_LABELS[p.port]) || p.port.replace(/_/g, ' ')}</strong>: −${formatMoney(p.delta)} public-sector spend over 36 months</li>`).join("")}</ul>` : ""}
` : `
<h2>Intervention scenario</h2>
<p>No intervention currently configured. Open the dashboard's Intervention Scenarios tab to model a prevention scenario before printing.</p>
`}
<h2>Equity story</h2>
<p><strong>${pctTouched}%</strong> of the simulated adult population touches at least one council service in 36 months. The top <strong>5%</strong> of touched individuals drive <strong>${concentration}%</strong> of total spend. This is the long-tail concentration that targeted prevention can shift.</p>
<h2>Methodology in three sentences</h2>
<p>The Life Event Graph (LEG) is a person-centric Monte Carlo simulator: each of 100k synthetic adults receives an HSV (Human State Vector) profile drawn from a population-representative distribution; antecedent life events fire at HSV-conditioned rates; cascades through 51 council service nodes are computed using literature-anchored conditional probabilities. Cost figures derive from PSSRU Unit Costs 2024, NHS National Cost Collection, DLUHC 2024-25, LaingBuisson, Samaritans, SafeLives, LGA, and DWP, SPPI-uplifted to 2025/26 prices. The model is population-shape-correct and structurally validated against steady-state UK cohort data; absolute magnitudes for the selected LA are illustrative until calibrated against case-level LA data in a pilot.</p>
<p class="meta">Sources: PSSRU Unit Costs 2024; NHS National Cost Collection; DLUHC Statutory Homelessness 2024-25; DWP DHP MYR 2024-25; LaingBuisson Care Homes 2024; Samaritans Economic Cost of Suicide 2024; Dame Carol Black Independent Review of Drugs; SafeLives MARAC; Women's Aid; LGA Public Health Funerals 2024. Full citation set in unit_costs_v0.2.md.</p>
<p class="meta">Generated by PSI Cascade Dashboard v0.13 · LEG: 92 nodes, 187 edges · HSV moderators on edges + antecedents · Person-centric Monte Carlo, 100k synthetic cohort, 36-month horizon · Status quo baseline + user-defined intervention scenario.</p>
`;
}
function showExecSummary() {
renderExecSummary();
document.body.classList.add("print-mode");
// Give DOM a tick then print
setTimeout(() => window.print(), 250);
}
function exitPrintMode() {
document.body.classList.remove("print-mode");
}
window.addEventListener("afterprint", exitPrintMode);
populateLASelector();
updateLAMeta();
wireInterventionsControls();
document.getElementById("execPrintBtn").addEventListener("click", showExecSummary);
// v1.1 — once the legacy LA selector is populated, mirror to the main one and update the
// header chip + LA page so they reflect the initial preset even before the user navigates there.
syncLaMainPickerFromLegacy();
updateLaContextChip();
populateLaContextGrid();
// v1.2 — Two-stage init.
// Stage 1 (initLayoutOnly) runs on script load — cheap, builds the sankey layout
// and prepares intervention multipliers. Does NOT run the 100k-person Monte Carlo,
// so the welcome page loads instantly and doesn't burn the visitor's CPU.
function initLayoutOnly() {
popLabel.textContent = POP_SIZE.toLocaleString();
const layoutPkg = buildLayout();
renderStatic(layoutPkg);
// Initialize intervention multipliers to 1.0 for all antecedents
for (const ant of PAYLOAD.antecedents) {
if (INTERV.multipliers[ant.id] === undefined) INTERV.multipliers[ant.id] = 1.0;
}
}
// Stage 2 (startSimulationIfNeeded) fires the first time the user moves past the
// welcome step. Idempotent — subsequent calls return the same promise. The sim
// runs in the background while the user is on the LA-selection page, so by the
// time they reach the cascade page it's already complete.
let _simStartedPromise = null;
let _simReady = false;
function startSimulationIfNeeded() {
if (_simStartedPromise) return _simStartedPromise;
_simStartedPromise = (async () => {
await runSimulation();
setTime(0);
maybeUpdateAggregate(true);
patternsCache = computePatterns();
playing = true;
playBtn.textContent = "⏸ Pause"; playBtn.classList.add("active");
requestAnimationFrame(frame);
_simReady = true;
})();
return _simStartedPromise;
}
initLayoutOnly();
</script>
</body>
</html>