diff --git a/decisions/ADR-0007-render-html-not-a-port.md b/decisions/ADR-0007-render-html-not-a-port.md
new file mode 100644
index 0000000..264d44b
--- /dev/null
+++ b/decisions/ADR-0007-render-html-not-a-port.md
@@ -0,0 +1,173 @@
+# ADR-0007: render to HTML, and do not declare the port yet
+
+status: accepted
+date: 2026-08-02
+decided by: agent, under the standing loop authorization. **Two items are
+explicitly reserved for the maintainer and are not decided here** — see
+§Reserved.
+tier: L (structural L — the declaration was made when this pass would
+create a capability port; chaos d4=1 → no override. Review removed the
+port, and the tier was **not** re-derived — see §Consequences)
+references: [CB-RES-0006](../research/CB-RES-0006-render-port.md),
+`history/260802-render-port-{research,challenge,response}.md`,
+[INTENT.md](../INTENT.md) §Implementation order,
+[ArchitectureBlueprint.md](../specs/ArchitectureBlueprint.md) §Port pattern,
+[ADR-0004](ADR-0004-am4-ratification.md) (AM-4 budgets),
+[CB-EV-0009](../evidence/CB-EV-0009-inspectable-table.md) (the inspector)
+
+## Context
+
+INTENT stage 1 is *"Inspectable 2D table — card/token/hand/relationship-
+graph visualization, drag-to-propose, debug inspector, hot-seat play."*
+CB-WP-0011 shipped the debug inspector. This decides how the other three
+get drawn.
+
+The obvious shape — `cb-render-api` / `cb-render-null` / `cb-render-wgpu`,
+straight from ArchitectureBlueprint §Port pattern — did not survive review.
+Two things went wrong with the survey that produced it, and both are
+recorded because the decision below is only trustworthy given what was
+wrong with the reasoning that first reached it.
+
+## Decision 1 — render by emitting HTML, SVG and JavaScript
+
+`cb-render-html` emits a document; the browser draws it. SVG covers cards,
+tokens and the relationship graph natively — the graph being the one
+element every Rust 2D toolkit would have left us to hand-roll. Pointer
+events cover drag-to-propose. One tab covers hot-seat.
+
+**Rejected alternatives, with measured marginal cost against the
+23-crate shipped-runtime base:**
+
+| rejected | marginal lines | why not |
+|---|---:|---|
+| `egui` + `eframe` | 2,782,849 | 30× the corrected headroom |
+| `wgpu` + `winit` | 1,741,979 | stage 2's, and stage 2 should buy it |
+| `softbuffer` + `tiny-skia` + `winit` | 1,147,081 | windowing dominates the rasterizer |
+| `ratatui` + `crossterm` | 1,067,013 | costs more than a GPU game framework; `rustix` → `linux-raw-sys` at 479,901 |
+| `macroquad` | 480,501 | cheapest GPU-era stack, still 5.2× |
+| `sdl2` | 191,973 + a C library | see Decision 3 |
+| **`fltk`** | **140,079** + a C library | **the real floor. Affordable if the target moved — rejected on allocation, not cost** |
+| `tiny-skia` alone | 83,956 | fits, but rasterizes without windowing or input |
+
+**The reason is allocation, not affordability.** `fltk` is 1.5× the
+corrected headroom — a negotiation, not an impossibility. But `wgpu` at
+1,741,979 is named by INTENT for stage 2, is unavoidable, and is **twelve
+times** the cost of the stage-1 toolkit it would replace. Spending 1.5× the
+remaining budget on windowing that stage 2 discards is the wrong
+allocation whether or not the budget can be stretched. This argument does
+not depend on AM-4a's target being 250,000; it depends only on stage 2's
+bill exceeding stage 1's benefit, which is measured and true by 12×.
+
+The first draft of the survey argued affordability — "128×, two orders of
+magnitude, not a near miss." That was false, drawn from a candidate list
+containing no cheap windowed toolkits. It is recorded here because a
+decision whose stated reason was wrong once should carry the correction
+where the decision lives, not only where the survey does.
+
+## Decision 2 — do **not** declare `cb-render-api` or `cb-render-null`
+
+INTENT: *"No concept becomes canonical merely because it looks general. It
+becomes canonical after surviving a second concrete use."*
+
+A port designed against one implementation that emits whole documents
+acquires a document's shape — retained mode, full state per update, string
+identity, no frame timing — and stage 2 finds it unimplementable and
+rewrites it. `CommitWindow` is already in this repo with a delete-by date
+and two declined second-use opportunities, for exactly this error.
+
+`cb-render-html` therefore ships against the **existing `Project` trait**,
+which is a real interface with real implementations. The port is declared
+at stage 2, derived from two implementations rather than imagined from one.
+`cb-render-null` goes with it: a null implementation of an interface that
+does not exist is a null implementation of nothing.
+
+**This overrides ArchitectureBlueprint §Port pattern for this capability,
+and only for its timing.** The blueprint says every important capability
+ships null, reference and optimized implementations; it does not say when
+the interface is fixed. Where the two rules met, INTENT's second-use rule
+won, because it is the one INTENT states without exception.
+
+## Decision 3 — AM-4 counts what the project causes to be acquired
+
+Scoring the browser at zero is true under AM-4a and suspect in substance.
+If a target can be satisfied by relocating a dependency somewhere the
+target does not look, it constrains nothing — and the same move was already
+flattering `sdl2` and `fltk`, whose `.rs` counts exclude the C libraries
+that do the work.
+
+> **AM-4 counts third-party code the project causes to be acquired.** It
+> does not count runtimes the user already has independently of us — the
+> kernel, the system C library, the shell, the terminal, a web browser. It
+> **does** count a library our build or install instructions cause to be
+> fetched, pinned, or linked, whether or not its source is Rust.
+
+Consequences, applied consistently: a browser is not counted; **`sdl2` and
+`fltk` are counted at more than their Rust bindings**, since they oblige a
+user to install a `-dev` package. The rule makes the target harder to
+satisfy, not easier, which is the only direction a rule proposed by its
+beneficiary should be trusted to run.
+
+## Decision 4 — AM-4a's proc-macro defect is a separate change
+
+AM-4a counts `--edges normal`, which includes proc-macro crates that run in
+the compiler and never reach a binary: `syn` (66,916), `proc-macro2`,
+`quote`, `unicode-ident`, `serde_derive` — **89,048 lines, 36.2%** of the
+"shipped-runtime" figure. Real headroom is **92,798**, not the 3,750 this
+repo has cited in every pass that mentioned it, including CB-WP-0011's
+reason for deferring this declaration.
+
+The metric should count `--edges normal,no-proc-macro`. **Filed as its own
+change, not bundled here.** A budget correction that arrives attached to the
+request it unblocks is indistinguishable from motivated reasoning even when
+it is right — and it is right, which is why it must arrive separately.
+
+## Decision 5 — six controls bind the implementation
+
+| # | control | why |
+|---|---|---|
+| 1 | unguessable token minted per process, required on every request | a loopback listener is reachable by any process **and any web page the user visits** |
+| 2 | `Origin` / `Sec-Fetch-Site` checked, rejected by default | cross-origin POST to `127.0.0.1` is the attack, not a hypothetical |
+| 3 | listener bound explicitly to `127.0.0.1` | not `0.0.0.0` by default |
+| 4 | **a test that a token-less request is refused**, plus an M-D1-MUT mutation removing the check and turning it red | 1–3 without 4 are three claims and no evidence |
+| 5 | **JavaScript may not construct commands** — the page reports raw pointer facts (`down on id`, `up on id`); Rust decides what command they mean | confines JS to input transport, making the decision testable in Rust against synthetic events |
+| 6 | **the coverage gate crosses the language boundary** — the HTML counterpart of `every_view_field_is_classified` asserts over the **parsed emitted document** | asserting over the Rust that emits the document reproduces CB-WP-0011's defect one layer up |
+
+Controls 5 and 6 exist because CB-WP-0011 established that a renderer's
+defect class is silent omission, and this decision moves the interactive
+half of stage 1 into a language `cargo test`, `clippy` and `M-D1-MUT`
+cannot reach. Without them, this ADR would spend a pass's finding one pass
+after paying for it.
+
+**K13 binds unchanged.** The renderer consumes a projection and can never
+feed back into validation; a drag that proposes a move goes through the
+same command path a CLI move takes.
+
+## Reserved for the maintainer — not decided here
+
+1. **AM-4a is incompatible with INTENT stage 2.** `wgpu` + `winit` is
+ 1,741,979 marginal lines against a **250,000** total target — 7× the
+ whole budget, 19× the corrected headroom. No sequencing, feature-gating
+ or metric correction closes that. Either the target moves, the render
+ port sits outside the AM-4a configuration by an argued rule, or stage 2
+ changes. **A pass that discovers a budget conflict and also settles it
+ has reviewed nothing**, so this pass does not settle it.
+2. **Whether Decision 3's acquisition rule is the right rule**, given it is
+ proposed by the pass that benefits from it. It is written to cost more
+ than it saves, but that is an argument, not a ratification.
+
+## Consequences
+
+- Stage 1 ships without a capability port. That is a deliberate deferral
+ with a named trigger (stage 2's `wgpu` implementation), not an omission.
+- **The tier is now wrong and was not re-derived.** Tier L's structural
+ trigger was "creates a new capability port"; review removed the port.
+ Re-rolling would make the tier a function of the outcome, which is the
+ one thing a tier declaration must not be. The pass runs at L. This is the
+ CHAOS calibration window's second entry, and unlike the first it comes
+ from a *non*-override: full-weight review deleted its own trigger.
+- A second language enters the build, gated only by control 6. If the
+ emitted JavaScript grows beyond input transport, control 5 has failed and
+ the decision should be revisited rather than the control widened.
+- `cb-play` acquires a third mode. CB-EV-0009 §5 flagged that a third mode
+ is the second use at which its single-binary shape should be
+ reconsidered; that reconsideration is now due.
diff --git a/workplans/CB-WP-0012-render-port.md b/workplans/CB-WP-0012-render-port.md
index c6fbe46..8708ab7 100644
--- a/workplans/CB-WP-0012-render-port.md
+++ b/workplans/CB-WP-0012-render-port.md
@@ -161,7 +161,7 @@ outcome. Recorded as the CHAOS window's second entry.
```task
id: CB-WP-0012-T03
-status: todo
+status: done
priority: high
```
@@ -169,6 +169,17 @@ Record the decision, the rejected alternatives with their measured costs,
and the controls the implementation must carry. No implementation code
lands before this commits.
+**Done 2026-08-02.**
+[ADR-0007](../decisions/ADR-0007-render-html-not-a-port.md) — *render to
+HTML, and do not declare the port yet.* Five decisions, eight rejected
+alternatives with measured costs, six binding controls, and **two items
+reserved for the maintainer**: AM-4a's 7× incompatibility with stage 2, and
+whether the acquisition rule proposed by the pass that benefits from it is
+the right rule.
+
+The hard gate held: no render implementation code existed before this
+commit.
+
## Task: the port and its first implementation
```task