diff --git a/dashboard/observablehq.config.js b/dashboard/observablehq.config.js
index 570bcf1..91cf8c8 100644
--- a/dashboard/observablehq.config.js
+++ b/dashboard/observablehq.config.js
@@ -2,29 +2,54 @@ export default {
root: "src",
title: "Custodian State Hub",
pages: [
+ // ── Overview ──────────────────────────────────────────────────────────────
{ name: "Overview", path: "/" },
- { name: "Workstreams", path: "/workstreams" },
- { name: "Tasks", path: "/tasks" },
- { name: "Decisions", path: "/decisions" },
- { name: "Progress", path: "/progress" },
+ { name: "Todo", path: "/todo" },
+ // ── Organizational Entity Views ───────────────────────────────────────────
{ name: "Domains", path: "/domains" },
- { name: "Repos", path: "/repos" },
- { name: "Extension Points", path: "/extensions" },
- { name: "Technical Debt", path: "/techdept" },
+ { name: "Repos", path: "/repos" },
+ {
+ name: "Workstreams",
+ path: "/workstreams",
+ collapsible: true,
+ open: false,
+ pages: [
+ { name: "Decisions", path: "/decisions" },
+ { name: "Tasks", path: "/tasks" },
+ { name: "Debt", path: "/techdept" },
+ { name: "Extends", path: "/extensions" },
+ { name: "Dependencies", path: "/dependencies" },
+ ],
+ },
+ // ── Functional Report Views ────────────────────────────────────────────────
{ name: "Contributions", path: "/contributions" },
- { name: "SBOM", path: "/sbom" },
+ { name: "SBOM", path: "/sbom" },
+ { name: "Progress", path: "/progress" },
+ // ── Reference ─────────────────────────────────────────────────────────────
{
name: "Reference",
+ path: "/reference",
collapsible: true,
+ open: false,
pages: [
- { name: "Decision Health", path: "/docs/decisions-kpi" },
- { name: "Decisions", path: "/docs/decisions" },
+ { name: "Contributions", path: "/docs/contributions" },
+ { name: "Decision Health", path: "/docs/decisions-kpi" },
+ { name: "Decisions", path: "/docs/decisions" },
+ { name: "Dependencies", path: "/docs/dependencies" },
+ { name: "Domains", path: "/docs/domains" },
+ { name: "Extension Points", path: "/docs/extensions" },
{ name: "Inter-Repo Communication", path: "/docs/inter-repo-communication" },
- { name: "Live Data", path: "/docs/live-data" },
- { name: "Progress Log", path: "/docs/progress-log" },
- { name: "SBOM", path: "/docs/sbom" },
- { name: "Workstream Health", path: "/docs/workstream-health-index" },
- { name: "Workstreams", path: "/docs/workstreams" },
+ { name: "Live Data", path: "/docs/live-data" },
+ { name: "Overview", path: "/docs/overview" },
+ { name: "Progress Log", path: "/docs/progress-log" },
+ { name: "Reference & Context Help", path: "/docs/reference" },
+ { name: "Repos", path: "/docs/repos" },
+ { name: "SBOM", path: "/docs/sbom" },
+ { name: "Tasks", path: "/docs/tasks" },
+ { name: "Technical Debt", path: "/docs/debt" },
+ { name: "Todo", path: "/docs/todo" },
+ { name: "Workstream Health", path: "/docs/workstream-health-index" },
+ { name: "Workstreams", path: "/docs/workstreams" },
],
},
],
diff --git a/dashboard/src/components/doc-overlay.js b/dashboard/src/components/doc-overlay.js
index bc501e0..54493b7 100644
--- a/dashboard/src/components/doc-overlay.js
+++ b/dashboard/src/components/doc-overlay.js
@@ -12,7 +12,21 @@
* The ? button is invisible until the user hovers over the element.
*/
-const _STYLE_ID = "doc-overlay-styles";
+const _STYLE_ID = "doc-overlay-styles";
+const _titleCache = new Map();
+
+async function _fetchDocTitle(docPath) {
+ if (_titleCache.has(docPath)) return _titleCache.get(docPath);
+ try {
+ const res = await fetch(docPath);
+ if (!res.ok) return null;
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(await res.text(), "text/html");
+ const title = doc.querySelector("h1")?.textContent?.trim() ?? null;
+ if (title) _titleCache.set(docPath, title);
+ return title;
+ } catch { return null; }
+}
function _ensureStyles() {
if (typeof document === "undefined" || document.getElementById(_STYLE_ID)) return;
@@ -195,6 +209,14 @@ export function withDocHelp(element, docPath) {
btn.setAttribute("aria-label", "Open documentation");
btn.addEventListener("click", e => { e.stopPropagation(); _openOverlay(docPath); });
+ // Lazy-load the h1 of the target doc page as a native tooltip (bubblehelp)
+ btn.addEventListener("mouseenter", async () => {
+ if (btn.dataset.titleFetched) return;
+ btn.dataset.titleFetched = "1";
+ const title = await _fetchDocTitle(docPath);
+ if (title) btn.title = title;
+ }, {once: true});
+
element.append(btn);
return element;
}
diff --git a/dashboard/src/contributions.md b/dashboard/src/contributions.md
index 9c759e4..0915195 100644
--- a/dashboard/src/contributions.md
+++ b/dashboard/src/contributions.md
@@ -33,12 +33,17 @@ const _ts = contribState.ts;
```js
import {injectTocTop} from "./components/toc-sidebar.js";
+import {withDocHelp} from "./components/doc-overlay.js";
const _liveEl = html`
●
${_ok ? `Live · ${_ts?.toLocaleTimeString()}` : html`API offline`}
`;
+withDocHelp(_liveEl, "/docs/live-data");
injectTocTop("live-indicator", _liveEl);
+
+const _h1 = document.querySelector("#observablehq-main h1");
+if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/contributions"); }
```
```js
diff --git a/dashboard/src/dependencies.md b/dashboard/src/dependencies.md
new file mode 100644
index 0000000..de7164f
--- /dev/null
+++ b/dashboard/src/dependencies.md
@@ -0,0 +1,158 @@
+---
+title: Dependencies
+---
+
+```js
+const API = "http://127.0.0.1:8000";
+const POLL = 15_000;
+```
+
+```js
+// Fetch workstreams + topics + summary (summary carries dep edges on open_workstreams)
+const depState = (async function*() {
+ while (true) {
+ let wsMap = {}, edges = [], ok = false;
+ try {
+ const [rw, rto, rs] = await Promise.all([
+ fetch(`${API}/workstreams/`),
+ fetch(`${API}/topics/`),
+ fetch(`${API}/state/summary`),
+ ]);
+ ok = rw.ok && rto.ok && rs.ok;
+ if (ok) {
+ const [wsList, topicList, summary] = await Promise.all([
+ rw.json(), rto.json(), rs.json(),
+ ]);
+ const topicMap = Object.fromEntries(topicList.map(t => [t.id, t]));
+ wsMap = Object.fromEntries(wsList.map(w => [w.id, {
+ ...w,
+ domain: topicMap[w.topic_id]?.domain_slug ?? "unknown",
+ }]));
+ // Build directed edge list from open_workstreams depends_on arrays
+ for (const ow of (summary.open_workstreams ?? [])) {
+ for (const depId of (ow.depends_on ?? [])) {
+ edges.push({from_id: ow.id, to_id: depId});
+ }
+ }
+ }
+ } catch {}
+ yield {wsMap, edges, ok, ts: new Date()};
+ await new Promise(res => setTimeout(res, POLL));
+ }
+})();
+```
+
+```js
+const wsMap = depState.wsMap ?? {};
+const edges = depState.edges ?? [];
+const _ok = depState.ok ?? false;
+const _ts = depState.ts;
+```
+
+# Dependencies
+
+```js
+import {injectTocTop} from "./components/toc-sidebar.js";
+import {withDocHelp} from "./components/doc-overlay.js";
+
+// ── KPI sidebar card ──────────────────────────────────────────────────────────
+const _wsWithDeps = new Set([...edges.map(e => e.from_id), ...edges.map(e => e.to_id)]);
+const _kpiBox = html`
+
Dependencies
+
+
+
workstreams involved
+
+
+
`;
+
+const _liveEl = html`
+ ●
+ ${_ok
+ ? `Live · updated ${_ts?.toLocaleTimeString()}`
+ : html`Offline — run: make api`}
+
`;
+
+const _h1 = document.querySelector("#observablehq-main h1");
+if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/dependencies"); }
+
+injectTocTop("dep-kpi-box", _kpiBox);
+injectTocTop("live-indicator", _liveEl);
+```
+
+Directed edges between active workstreams. An edge **A → B** means A cannot
+fully proceed until B reaches a satisfactory state.
+
+```js
+if (edges.length === 0) {
+ display(html`No dependency edges registered.
`);
+} else {
+ const rows = edges.map(e => {
+ const from = wsMap[e.from_id];
+ const to = wsMap[e.to_id];
+ return {
+ from_domain: from?.domain ?? "—",
+ from_title: from?.title ?? e.from_id,
+ from_status: from?.status ?? "—",
+ to_domain: to?.domain ?? "—",
+ to_title: to?.title ?? e.to_id,
+ to_status: to?.status ?? "—",
+ };
+ });
+
+ display(html`
+
+
+ | Depends-on domain |
+ Depends-on workstream |
+ |
+ Blocked-by domain |
+ Blocked-by workstream |
+ Status |
+
+
+ ${rows.map(r => html`
+
+ | ${r.from_domain} |
+ ${r.from_title} |
+ → |
+ ${r.to_domain} |
+ ${r.to_title} |
+ ${r.to_status} |
+
+ `)}
+
`);
+}
+```
+
+
diff --git a/dashboard/src/docs/contributions.md b/dashboard/src/docs/contributions.md
new file mode 100644
index 0000000..2e35dae
--- /dev/null
+++ b/dashboard/src/docs/contributions.md
@@ -0,0 +1,120 @@
+---
+title: Contributions — Reference
+---
+
+# Contributions — Reference
+
+Contributions track **outbound upstream work** — things the Custodian has
+identified that belong in a repo it does not own or control. Each contribution
+is a structured artifact filed locally in the repo's `contrib/` directory and
+registered in the state hub so it is never lost.
+
+---
+
+## Contribution types
+
+| Type | Full name | Use when |
+|------|-----------|----------|
+| `br` | Bug Report | You found a defect in an upstream tool or library |
+| `fr` | Feature Request | You need functionality that upstream does not yet provide |
+| `ep` | Extension Point | You identified a future enhancement opportunity in upstream code |
+| `upr` | Upstream PR | You have written (or are writing) a patch for an upstream repo |
+
+---
+
+## Lifecycle
+
+```
+draft → submitted → acknowledged → accepted → merged
+ ↘ ↘
+ rejected withdrawn
+```
+
+| Status | Meaning |
+|--------|---------|
+| **draft** | Artifact written locally; not yet sent upstream |
+| **submitted** | Filed as a GitHub issue, PR, or email — awaiting upstream response |
+| **acknowledged** | Upstream has seen it and responded (e.g. triaged, commented) |
+| **accepted** | Upstream agreed to take action |
+| **merged** | PR accepted and merged; issue resolved |
+| **rejected** | Upstream declined; record kept for future reference |
+| **withdrawn** | We decided not to pursue it |
+
+Transitions are enforced by the API — you cannot skip stages arbitrarily.
+`submitted_at` is stamped automatically when status moves to `submitted`;
+`resolved_at` is stamped when status moves to `merged`, `rejected`, or `withdrawn`.
+
+---
+
+## Relation to the Todo classification
+
+Contributions map directly to the **Third-party** class in the inter-repo
+communication taxonomy:
+
+| Todo class | Mechanism |
+|------------|-----------|
+| Internal | Workplan file + task in this repo's workstream |
+| Ecosystem | State hub task with `[repo:]` prefix |
+| **Third-party** | **Contribution artifact in `contrib/` + state hub registration** |
+
+Contributions in `draft`, `submitted`, or `acknowledged` status appear as
+open Third-party todos on the [Todo](/todo) page.
+
+---
+
+## File layout
+
+Each artifact lives in the current repo under `contrib/`:
+
+```
+contrib/
+ bug-reports/ br-YYYY-MM-DD------.md
+ feature-requests/ fr-YYYY-MM-DD------.md
+ extension-points/ EP--NNN------.md
+ upstream-prs/ upr-YYYY-MM-DD------.md
+```
+
+Templates live in `~/the-custodian/canon/standards/contrib-templates/`.
+Convention details: `~/the-custodian/canon/standards/contribution-convention_v0.1.md`.
+
+---
+
+## Adding a contribution
+
+**1. Write the artifact file** using the appropriate template.
+
+**2. Register it in the state hub** via MCP:
+
+```
+register_contribution(
+ type = "fr",
+ title = "Add sidebar TOC injection API",
+ target_org = "observablehq",
+ target_repo = "framework",
+ body_path = "contrib/feature-requests/fr-2026-02-26--observablehq--framework--toc.md",
+ related_workstream_id = ""
+)
+```
+
+**3. Close the loop** when you file it upstream:
+
+```
+update_contribution_status(contribution_id="", status="submitted")
+```
+
+**4. Keep updating** as upstream responds — `acknowledged`, `accepted`, `merged`.
+
+---
+
+## Kanban board
+
+The Contributions page groups artifacts by status column. Only columns with at
+least one entry are shown. The **⚠ follow-up banner** appears when any
+contribution has been in `submitted` or `acknowledged` for an extended period
+without further movement — a prompt to check in with upstream.
+
+---
+
+*Contributions are append-only. Rejected or withdrawn artifacts are retained as
+institutional memory — they explain why certain approaches were tried and
+dropped.*
diff --git a/dashboard/src/docs/debt.md b/dashboard/src/docs/debt.md
new file mode 100644
index 0000000..977f533
--- /dev/null
+++ b/dashboard/src/docs/debt.md
@@ -0,0 +1,91 @@
+---
+title: Technical Debt — Reference
+---
+
+# Technical Debt — Reference
+
+The Technical Debt page tracks known quality compromises across all six project
+domains — intentional shortcuts, design weaknesses, missing tests, and similar
+issues that reduce codebase health but have been consciously deferred.
+
+---
+
+## Debt types
+
+| Type | Examples |
+|------|---------|
+| **design** | Architectural decisions that should be revisited |
+| **implementation** | Hacky or fragile code that works but shouldn't stay |
+| **test** | Missing or incomplete test coverage |
+| **docs** | Missing or outdated documentation |
+| **dependencies** | Pinned old versions, unused packages, missing lockfiles |
+| **performance** | Known bottlenecks not yet worth addressing |
+| **security** | Hardcoded values, missing input validation, weak auth |
+| **other** | Anything that doesn't fit the above |
+
+---
+
+## Severity levels
+
+| Severity | Meaning |
+|----------|---------|
+| **critical** | Blocks release or poses an active risk |
+| **high** | Should be resolved before the next major milestone |
+| **medium** | Normal triage priority |
+| **low** | Nice-to-fix; acceptable to defer indefinitely |
+
+---
+
+## Statuses
+
+| Status | Meaning |
+|--------|---------|
+| **open** | Known and unaddressed |
+| **in_progress** | Being actively worked on |
+| **deferred** | Acknowledged but intentionally postponed |
+| **resolved** | Fixed |
+| **wont_fix** | Accepted as permanent — documented for future reference |
+
+Items are sorted by status (open → in_progress → deferred → resolved → wont_fix)
+then by severity (critical → high → medium → low) within each group.
+
+---
+
+## Filters
+
+| Filter | Effect |
+|--------|--------|
+| **Status** | Multi-select |
+| **Severity** | Multi-select |
+| **Domain** | Multi-select |
+| **Type** | Multi-select |
+
+---
+
+## Registering debt
+
+Via MCP:
+
+```
+register_technical_debt(
+ domain = "custodian",
+ title = "Hard-coded API URL in data loaders",
+ debt_type = "implementation",
+ severity = "high",
+ description = "All data loaders use http://127.0.0.1:8000 directly. Should read from an env var or config.",
+ location = "state-hub/dashboard/src/data/*.json.py",
+ workstream_id = "" # optional
+)
+```
+
+```
+update_td_status(td_uuid="", status="resolved")
+```
+
+---
+
+## Human-readable IDs
+
+Each debt item carries a human-readable ID in the form `TD--NNN`
+(e.g. `TD-CUST-001`). IDs are optional at creation and auto-assigned if omitted.
+They appear in the table for easy reference in commit messages and comments.
diff --git a/dashboard/src/docs/dependencies.md b/dashboard/src/docs/dependencies.md
new file mode 100644
index 0000000..2dd56ed
--- /dev/null
+++ b/dashboard/src/docs/dependencies.md
@@ -0,0 +1,90 @@
+---
+title: Dependencies — Reference
+---
+
+# Dependencies — Reference
+
+The Dependencies page shows the directed dependency graph between active
+workstreams — which workstreams are waiting on others to reach a satisfactory
+state before they can fully proceed.
+
+---
+
+## What is a dependency edge?
+
+A dependency edge **A → B** means workstream A cannot fully proceed until
+workstream B is in a satisfactory state (typically `completed` or `archived`).
+
+Edges are used to model real sequencing constraints: for example, a shared
+library must reach a stable release before downstream domains can build on it.
+The Custodian's dependency order is:
+
+```
+Railiance → Markitect → Coulomb.social → Personhood / Foerster → Custodian
+```
+
+---
+
+## Edge table
+
+Each row shows:
+
+| Column | Meaning |
+|--------|---------|
+| **Depends-on domain** | Domain of the dependent workstream (the one waiting) |
+| **Depends-on workstream** | Title of the workstream that has the dependency |
+| **→** | Direction arrow |
+| **Blocked-by domain** | Domain of the prerequisite workstream |
+| **Blocked-by workstream** | Title of the workstream that must complete first |
+| **Status** | Current status of the prerequisite (green = active, grey = completed) |
+
+---
+
+## KPI sidebar card
+
+Shows the total number of edges and the number of distinct workstreams involved
+in at least one dependency relationship.
+
+---
+
+## Registering a dependency
+
+Via MCP:
+
+```
+create_dependency(
+ from_workstream_id = "",
+ to_workstream_id = "",
+ description = "Cannot build auth layer until shared-library API is stable"
+)
+```
+
+Via REST:
+
+```bash
+curl -X POST http://127.0.0.1:8000/workstreams//dependencies/ \
+ -H "Content-Type: application/json" \
+ -d '{"to_workstream_id": "", "description": "..."}'
+```
+
+To list dependencies for a workstream:
+
+```
+list_dependencies(workstream_id="")
+```
+
+---
+
+## Cycle detection
+
+The Workstream Health Index (WHI) includes a **Cycle Penalty Index (CPI)**
+metric that detects circular dependencies using depth-first search. If CPI = 1,
+a cycle exists and the WHI is penalised by 50%. The WHI KPI card on the
+[Workstreams](/workstreams) page will display a cycle alert.
+
+---
+
+## Data source
+
+Dependency edges are derived from the `depends_on` arrays on `open_workstreams`
+in `GET /state/summary`. Polls every **15 seconds**.
diff --git a/dashboard/src/docs/domains.md b/dashboard/src/docs/domains.md
new file mode 100644
index 0000000..2bc9b8a
--- /dev/null
+++ b/dashboard/src/docs/domains.md
@@ -0,0 +1,82 @@
+---
+title: Domains — Reference
+---
+
+# Domains — Reference
+
+The Domains page shows all registered project domains and the repositories
+associated with each one. Domains are the top-level organisational unit of the
+Custodian ecosystem.
+
+---
+
+## What is a domain?
+
+A domain corresponds to one of the six tracked project areas:
+
+| Slug | Project |
+|------|---------|
+| `custodian` | The Custodian agent system itself |
+| `railiance` | DevOps & infrastructure reliability |
+| `markitect` | Knowledge artifact management |
+| `coulomb_social` | Co-creation marketplace |
+| `personhood` | Rights & obligations framework |
+| `foerster_capabilities` | Agency capability taxonomy |
+
+Each domain has a slug (URL-friendly identifier), a human-readable name, an
+optional description, and a status.
+
+---
+
+## Domain statuses
+
+| Status | Meaning |
+|--------|---------|
+| **active** | Live domain — topics, workstreams, and tasks are being tracked |
+| **archived** | Soft-deleted; no active work. Fails to archive if active topics exist |
+
+---
+
+## KPI row
+
+Four counters at the top of the page:
+
+| Counter | Meaning |
+|---------|---------|
+| Total domains | All registered domains regardless of status |
+| Active | Domains with status `active` |
+| Total repos | Sum of all registered repositories across all domains |
+| Newest domain | Name of the most recently created domain |
+
+---
+
+## Domain cards
+
+One card per domain showing:
+
+- **Slug** — monospace identifier
+- **Status badge** — green `active` or grey `archived`
+- **Name** — display name
+- **Description** — first 160 characters
+- **Repos** — list of registered repositories for this domain, each showing name, local path, and remote URL
+
+---
+
+## Managing domains
+
+Via MCP:
+
+```
+create_domain(slug="my_project", name="My Project", description="…")
+rename_domain(slug="old_slug", new_slug="new_slug", new_name="New Name")
+archive_domain(slug="my_project") # fails if active topics exist
+```
+
+Via Makefile:
+
+```bash
+make add-domain SLUG=my_project NAME="My Project"
+make rename-domain OLD_SLUG=my_project NEW_SLUG=myproject NEW_NAME="My Project"
+```
+
+*Domains are never hard-deleted — only archived.*
diff --git a/dashboard/src/docs/extensions.md b/dashboard/src/docs/extensions.md
new file mode 100644
index 0000000..08a3615
--- /dev/null
+++ b/dashboard/src/docs/extensions.md
@@ -0,0 +1,102 @@
+---
+title: Extension Points — Reference
+---
+
+# Extension Points — Reference
+
+The Extension Points page tracks known future enhancement opportunities across
+all six domains — design forks the system *could* take, parked deliberately
+for later consideration rather than acted on immediately.
+
+---
+
+## What is an extension point?
+
+An extension point (EP) captures a place in the design where additional
+capability could be added — an API surface that could be extended, a schema
+that could grow, an integration that could be built. Recording an EP
+acknowledges the opportunity without committing to it.
+
+Extension points are distinct from technical debt: debt is a known compromise
+that should be fixed; EPs are optional future directions that may or may not
+be pursued.
+
+---
+
+## EP types
+
+| Type | Examples |
+|------|---------|
+| **api** | New endpoints, query parameters, response fields |
+| **schema** | New tables, columns, relationships |
+| **mcp** | New MCP tools or resources |
+| **dashboard** | New pages, charts, or components |
+| **architecture** | Structural changes to the system design |
+| **integration** | Connections to external systems |
+| **other** | Anything that doesn't fit the above |
+
+---
+
+## Statuses
+
+| Status | Meaning |
+|--------|---------|
+| **open** | Identified, not yet acted on |
+| **in_progress** | Being implemented as part of an active workstream |
+| **addressed** | The capability has been built |
+| **deferred** | Intentionally postponed |
+| **wont_fix** | Decided not to pursue — kept for documentation |
+
+Items are sorted by status (open → in_progress → deferred → addressed → wont_fix)
+then by priority (critical → high → medium → low).
+
+---
+
+## Priorities
+
+| Priority | Meaning |
+|----------|---------|
+| **critical** | Needed to unblock other work |
+| **high** | High-value enhancement for the near term |
+| **medium** | Would be useful but not urgent |
+| **low** | Speculative or long-horizon idea |
+
+---
+
+## Filters
+
+| Filter | Effect |
+|--------|--------|
+| **Status** | Multi-select |
+| **Priority** | Multi-select |
+| **Domain** | Multi-select |
+| **Type** | Multi-select |
+
+---
+
+## Registering an extension point
+
+Via MCP:
+
+```
+register_extension_point(
+ domain = "custodian",
+ title = "Configurable poll interval per dashboard page",
+ ep_type = "dashboard",
+ priority = "low",
+ description = "Each page hard-codes POLL = 15_000. An env var or per-page config would allow slowing down low-priority pages to reduce API load.",
+ location = "state-hub/dashboard/src/*.md",
+ workstream_id = "" # optional
+)
+```
+
+```
+update_ep_status(ep_uuid="", status="addressed")
+```
+
+---
+
+## Human-readable IDs
+
+Each EP carries an ID in the form `EP--NNN` (e.g. `EP-CUST-001`).
+IDs are optional at creation and auto-assigned if omitted.
diff --git a/dashboard/src/docs/overview.md b/dashboard/src/docs/overview.md
new file mode 100644
index 0000000..d0bf8e5
--- /dev/null
+++ b/dashboard/src/docs/overview.md
@@ -0,0 +1,84 @@
+---
+title: Overview — Reference
+---
+
+# Overview — Reference
+
+The Overview page is the operational home screen of the Custodian State Hub.
+It shows the live health of the entire ecosystem at a glance — active work,
+blocking decisions, and system-derived next-step suggestions.
+
+---
+
+## Sections
+
+### Open Workstreams by Domain
+
+A horizontal stacked bar chart showing every active workstream across all six
+domains. Each bar is broken into four task-status segments:
+
+| Colour | Segment |
+|--------|---------|
+| green | done |
+| blue | in progress |
+| orange-red | blocked |
+| light grey | todo |
+
+The left axis shows domain labels (one per group of workstreams). The `done/total`
+count is printed to the right of each bar. Workstreams with no tasks yet show
+a grey "— no tasks yet" label.
+
+### Contribution & SBOM Health
+
+Three summary cards linked to the Contributions and SBOM pages:
+
+| Card | Shows |
+|------|-------|
+| **Contributions** | Total artifact count; orange warning if any are awaiting upstream response |
+| **Licence Risk** | Count of SBOM packages with copyleft licences in direct dependencies |
+| **SBOM** | Breakdown by contribution type (BR / FR / EP / UPR) |
+
+### Status
+
+Four metric cards:
+
+| Card | Meaning |
+|------|---------|
+| **Active Workstreams** | Count of non-completed, non-archived workstreams |
+| **Blocking Decisions** | Pending decisions with status `open` or `escalated` — orange border if > 0 |
+| **Blocked Tasks** | Click to expand the list with blocking reasons |
+| **Events Today** | Progress events created on today's date |
+
+### What's next?
+
+System-derived action suggestions from `GET /state/next_steps`. Suggestions are
+generated when a decision is resolved or a workstream dependency is cleared, and
+they point to the first open task in the relevant workstream. These are derived
+on request and never persisted.
+
+### Blocking Decisions
+
+Inline resolution form for each pending decision. Expand a card, enter a
+rationale and "decided by" name, and click **Record & close**. The decision is
+resolved via `POST /decisions/{id}/resolve` and disappears from the list
+without a page reload.
+
+### Registered Projects
+
+Table of projects registered with `make register-project`, sourced from
+`milestone` progress events whose summary starts with
+`"Project registered with State Hub:"`.
+
+### Recent Activity
+
+Last 20 progress events across all domains, showing time, event type, author,
+and summary.
+
+---
+
+## Data source
+
+Polls `GET /state/summary` every **15 seconds**. Blocking decisions are fetched
+separately via `GET /decisions/?decision_type=pending` and only re-fetched
+after a successful resolve action — this prevents the inline form from being
+wiped on every poll.
diff --git a/dashboard/src/docs/reference.md b/dashboard/src/docs/reference.md
new file mode 100644
index 0000000..951c844
--- /dev/null
+++ b/dashboard/src/docs/reference.md
@@ -0,0 +1,119 @@
+---
+title: Reference & Context Help — Reference
+---
+
+# Reference & Context Help
+
+The **Reference** section is a collection of in-depth documentation pages
+explaining the data model, design conventions, and mechanics of each dashboard
+view. Reference pages are readable as standalone articles and also surfaced
+inline via the **? context-help button** on dashboard pages.
+
+---
+
+## The ? context-help button
+
+Every dashboard page exposes one or more **?** buttons — small circular
+controls that open the relevant reference page in an overlay without leaving
+the current view.
+
+### Where ? buttons appear
+
+| Location | Opens |
+|----------|-------|
+| Page **h1** heading | Reference page for that dashboard view |
+| **KPI sidebar cards** | Reference page for the specific metric shown |
+| **Live indicator** | [Live Data](/docs/live-data) — poll interval, offline recovery |
+
+### How to use it
+
+1. Hover over the element — the **?** button fades in at the top-right corner.
+2. Click **?** — the reference page opens in a modal overlay.
+3. Read the docs, then dismiss with **✕ close**, **Esc**, or by clicking the
+ backdrop.
+
+The overlay does not interrupt the live data polling loop — the dashboard
+continues refreshing in the background while the overlay is open.
+
+---
+
+## Overlay behaviour
+
+| Detail | Value |
+|--------|-------|
+| Size | `min(780px, 92vw)` wide · `82vh` tall |
+| Content | Observable Framework page rendered in an `