` actions, zero inline `style=` button sizing hacks, zero
+navigation-interceptor "must be a button" comments. And `btn-group` is not zero — 6 correct groups
+already exist and served as the exemplars.
+
+Real inventory: ~45 grouping candidates (15 dialog feet, 15 form feet, 2 filter bars, 3 pagers,
+9 toolbars, ~14 row-action cells); **4** genuine link→action conversions; 4 `btn-link` Back buttons
+against 7 siblings already using `btn-outline-secondary btn-sm` (three idioms for one affordance);
+~26 inter-button spacers to drop (and 12 lookalikes that must be kept); exactly **one** inline
+arm→confirm flow (`TransportExport.razor:499-518`), restyle-only.
+
+Biggest levers: `DialogHost.razor` (1 render site, 23 `ConfirmAsync` call sites),
+`MonacoEditor.razor` (6 files), `SchemaBuilder.razor` (4 files / 7 instances).
+
+### 1c. Prose inventory
+
+The UI was already lean: ~12 unconditionally-rendered explainer blocks across 98 files, **zero**
+rendered milestone labels, **zero** dead links, **zero** wrong config keys, **zero** wrong
+numerics. 10 DELETE, 4 RELOCATE-then-delete, ~120 KEEP.
+
+The real defects were in `docs/`, not on the pages — and per the playbook, wrong facts outrank
+style:
+
+- `Component-Transport.md` claimed a `NotificationList` pulls in its `SmtpConfiguration`. It does
+ not — `DependencyResolver` only walks `ExportSelection.SmtpConfigurationIds`. The *UI banner* was
+ the correct statement and the doc was wrong.
+- `Component-Transport.md` listed **API keys as exportable** in five places. They are deliberately
+ not transportable (see the comment on `ExportSelection`), and the shipped **SMS configs** group
+ was missing from the same lists.
+- `Component-Transport.md` described a "6-Step" import wizard; the UI renders 5 pills, with Map a
+ sub-section inside Diff.
+- `Component-CentralUI.md` (×4) and `Component-ClusterInfrastructure.md` still said "via
+ ClusterClient" — removed in the gRPC migration's Phase 4.
+- `Component-Commons.md` omitted `Expression` from `AlarmTriggerType`.
+- The HiLo **partial-merge vs whole-replace** override rule existed only in a June plan doc, never
+ in `docs/requirements/`.
+
+Plus one vestigial UI field: `SiteForm.razor` collects a per-site **Akka Address** that has had no
+runtime consumer since gRPC Phase 4 — only writers remain; every central→site dial resolves
+`GrpcNodeAAddress`/`GrpcNodeBAddress`.
+
+#### Out-of-scope doc drift found while verifying the above (NOT fixed here)
+
+Chasing the UI prose's citations surfaced a wider ClusterClient de-drift job that this sweep
+deliberately did **not** take on — it is a docs initiative, not a UI cleanup, and silently
+expanding into it would have made this change unreviewable. Recorded so it does not get lost:
+
+- `docs/requirements/HighLevelReqs.md:49,51,52,405,523` still describes
+ **ClusterClient/ClusterClientReceptionist as the live command/control transport**, including
+ "Central creates a ClusterClient per site using both Akka addresses as contact points". All of
+ that went in the migration's Phase 4.
+- `docs/requirements/Component-ManagementService.md` **contradicts itself**: `:27` says the
+ ManagementActor is "**not** advertised via ClusterClientReceptionist", while `:16`, `:21`, and
+ the whole `:48` "ClusterClientReceptionist Registration" section say it registers and that
+ failover routes through ClusterClient. `:272` and `:278` repeat the claim.
+- `docs/requirements/Component-NotificationOutbox.md:31` labels the site→central store-and-forward
+ edge "(ClusterClient)" in its diagram.
+- `docs/requirements/Component-Commons.md:292` — a namespace-tree comment reading
+ "HTTP/ClusterClient management commands".
+
+`Component-Host.md` is already correct (it has an explicit REQ-HOST-6a "no
+ClusterClientReceptionist" section) and is the model for what the others should say.
+
+### 1d. Density / layout
+
+The scan that matters most, and the one that fires hardest here.
+
+- **Unbounded free-text columns — ~30 cells.** Worst: `SecuredWrites.razor:218`
+ (`@row.ExecutionError`, raw device exception text, last of 11 columns, six `text-nowrap`
+ neighbours), `TransportImport.razor:265` (`@item.FieldDiffJson`, externally-produced JSON in the
+ only `` in the codebase without `pre-wrap`, inside a `colspan="6"` row), and
+ `ConnectionCertificates.razor:63-64` (two adjacent remote X.509 DNs, bare, columns 1–2 of 7).
+ Notable pattern: several rows had already bounded their `LastError` correctly but left a sibling
+ column bare.
+- **Sections running together — 8 pages.** Worst: `Health.razor:234-434` (five `` groups in one
+ card body per site) and `TemplateEdit.razor:436` (four heading+table groups in one card body,
+ and that card is emitted *above* the page's own `` title).
+- **Identity slam — ~10.** Worst: `Health.razor:208` (name + machine id fused in one ``),
+ `NotificationLists.razor:77` (recipient name + contact in one chip, uncapped count, one cell).
+- **Raw slices.** No unguarded ones in markup. Three unguarded on toast paths
+ (`Topology.razor:905,963,964`) plus a parser bug at `ParameterValueForm.razor:382`.
+- **Detail surfaces holding the row OBJECT rather than its id — 5.** Latent today (none of those
+ pages is on a timer) but wrong, and mandatory to fix before any lands on a timer page.
+
+Reference pages already doing it right: `Admin/Sites.razor` (stacked identity),
+`Monitoring/ParkedMessages.razor` (line-clamped error + drawer).
+
+### 1e. Tree tables — verdict: **REJECT**
+
+Do not port the router's generic `TreeTable`/`TreeVisibility`. Zero `rowspan`, zero
+indent-by-padding table rows, zero repeated path-prefix columns, zero faked group-header rows. The
+only hierarchy-ish shape (dotted composed member names in `InstanceConfigure.razor`) is one column,
+flat for non-composed instances, carries per-row edit controls, and already has a proper tree
+presentation in `DebugView.razor`. The six nav/picker trees (`TreeView`, `TemplateFolderTree`,
+`NodeBrowserDialog`+`TreeRow`, `ExecutionTree`, `SchemaBuilder`) are the "different animal" the
+playbook says to leave alone. An unused component would be an orphan.
+
+## 2. Changes made
+
+### Foundation (landed first — page batches reference it by name)
+
+`src/ZB.MOM.WW.ScadaBridge.CentralUI/wwwroot/css/site.css` (already loaded *after* ``,
+so cascade order needed no change):
+
+- The `.btn` / `.btn-sm` **Bootstrap CSS-variable override block**, verbatim from the playbook, with
+ the upstreaming header comment. Normalizes every unsized button app-wide without touching call
+ sites. Deliberately overrides the *variables* only — `btn-group` seam and radius machinery
+ depends on the box properties.
+- `--accent` moved into `:root` here, and `Accent="#2f5fd0"` **removed** from
+ `MainLayout.razor`'s `ThemeShell`. As a `:root` declaration it loses to the dark block (equal
+ specificity, later in the same file) and wins over theme.css (equal specificity, earlier sheet).
+ Light mode is byte-identical because the value already matched theme.css's own light default.
+- `.form-label-sm` — the one ghost class that lost intent, given the rule it always implied.
+- Table-containment utilities the page batches use: `.cell-clip` (+ `-sm` / `-lg` width
+ modifiers), `.cell-clamp-2`, `.detail-pre`. Modelled on the existing `.parked-error-clamp`
+ pattern. **House rule: every clip/clamp is paired with a `title` so the full value stays
+ reachable.**
+- `AlarmTriggerEditor.razor` deadband SVG fills → `var(--bad-bg)` / `var(--ok-bg)`.
+
+Docs (landed before the page deletions, so no deletion raced its relocation):
+`Component-Transport.md` (API keys not transportable + the recovery hint, SMS configs added, the
+SMTP dependency-edge claim corrected, 6-Step → 5-Step, manifest samples and CLI synopsis),
+`Component-CentralUI.md` (ClusterClient → gRPC command channel ×4, Akka address marked legacy),
+`Component-ClusterInfrastructure.md` (ClusterClient → gRPC), `Component-Commons.md`
+(`AlarmTriggerType.Expression`), `Component-TemplateEngine.md` (the HiLo partial-merge rule).
+
+### Page batches
+
+Seven batches on disjoint file sets, each briefed with its own `file:line` inventory and the
+standing instruction to *verify by reading before editing and refuse items the markup contradicts*.
+Behavior preserved byte-for-byte throughout: `@onclick`, `disabled`, bindings, `AuthorizeView`
+gates, `data-test`/`data-testid` hooks, and the arm→confirm two-step.
+
+| Batch | Files | Highlights |
+|---|---|---|
+| 1 — Transport + Ops | SecuredWrites, TransportImport/Export, ConnectionCertificates, TestBindingsDialog | The two worst unbounded cells bounded; 5 prose deletions + the 2 relocated banners; the app's only inline arm→confirm restyled (two-step preserved) |
+| 2 — Monitoring | Health, EventLogs, ParkedMessages, AlarmSummary | Health per-site rework (see refusal below); identity slam split; EventLogs expander re-keyed off the loop index onto the event GUID; ParkedMessages drawer converted from row-object to row-id + re-resolve |
+| 3 — Admin + form shells | 6 Admin pages, both endpoint editors, 4 Design forms | SiteForm/DataConnectionForm/ApiMethodForm split into one card per group; the 7-group OpcUaEndpointEditor given `fieldset`/`legend`; Back-link idiom unified; Akka address relabelled legacy |
+| 4 — Shared + dialogs | DialogHost, MonacoEditor, SchemaBuilder + 14 dialogs | `DialogHost` footer grouped — one edit reaching **23 confirm call sites**; 4 Monaco toolbar links → a real button group across 6 files; 12 dialog feet grouped |
+| 5 — Reports/Audit/Notifications | SiteCalls, 6 Notifications pages, 2 Audit pages, AuditResultsGrid/FilterBar, Dashboard | Row-action groups (anchor + buttons, destructive red); recipient chip slam capped at 5 + "N more"; **three** object-holding detail surfaces converted to id + re-resolve |
+| 6 — Deployment + Design | Deployments, InstanceCreate, Topology, TemplateCreate, SchemaLibrary, SharedScriptForm, Templates, DataConnections, AuditLogPage | Deployment identity split; **three unguarded `[..8]` revision-hash slices fixed** (a real latent `ArgumentOutOfRangeException` on the deploy path) |
+| 7 — The two guard-pinned giants | InstanceConfigure, TemplateEdit | TemplateEdit's inherited-members card moved *below* the page title (ordering bug) and split into four carded groups; all **67** pinned substrings re-verified present, both negative assertions still absent |
+
+### Refusals worth recording (the briefs were wrong, the agents were right)
+
+The playbook's instruction to *verify by reading before editing and refuse what the markup contradicts*
+earned its place four times:
+
+1. **Health per-site groups (batch 2) — brief premise false.** The brief said "five headings stack flush
+ in one card body" with "metadata paragraphs between heading and table". Neither was true: the headings
+ sit in four `col-md-6` grid columns, three already behind collapse toggles, and no such paragraph
+ exists. Following it literally would have nested two cards inside each of two collapse regions with the
+ toggles floating outside any boundary. The agent restructured to one card per grid column, promoting
+ each existing collapse toggle into a real `card-header`.
+2. **`ApiMethodForm` "Real I/O" strings (batch 3) — brief would have broken the suite.** The brief said to
+ preserve `"Real I/O"` there. `TestRunWarningTests` asserts the opposite: `ApiMethodForm` must
+ **not** contain it (the Inbound API sandbox has no side-effect surface). Verified against the test.
+3. **`AuditResultsGrid` Target column (batch 5) — `.cell-clip` was the wrong tool.** `white-space: nowrap`
+ + `max-width` raises a cell's min-content width, which would stop a *resized* column shrinking below the
+ text width and break drag-to-resize at exactly the narrow widths the feature exists for. Fixed with a
+ character cap instead, mirroring the grid's own `TruncateError`.
+4. **`dc-kebab` restyle (batch 6) — contextual, not accidental.** The page `