diff --git a/docs/specs/dor-browser.md b/docs/specs/dor-browser.md
index d3cd4ee7..32925a95 100644
--- a/docs/specs/dor-browser.md
+++ b/docs/specs/dor-browser.md
@@ -75,6 +75,15 @@ Both CLI entry points use the same content-surface placement rule in
otherwise split next to the reference surface. `dor iframe` also accepts
`--surface`, `--minimize`, and `--json`.
+Both open the surface **focus-neutrally** (like `dor ensure`): it renders in the
+background and the caller keeps focus — except when replacing the pane the user
+is currently selected on, where selection follows to the replacement (it would
+otherwise dangle on the removed panel); replacing an active-but-unselected pane
+leaves the user's selection, including a door selection, untouched.
+`createContentSurface`'s
+`focusNeutral` routes through the shared `runSurfaceAdd` helper (see
+`docs/specs/layout.md` corner case #12).
+
Surface lifetime owns backing resources:
- Killing an agent-browser-rendered pane marks the session closed and runs
diff --git a/docs/specs/layout.md b/docs/specs/layout.md
index 6f5b693c..b7a68dc5 100644
--- a/docs/specs/layout.md
+++ b/docs/specs/layout.md
@@ -358,7 +358,7 @@ Shell-selection replacement uses the same pane add/remove primitives but also sh
### Kill (in-place fade + FLIP reclaim)
-`orchestrateKill(api, killedId)` in `lib/src/lib/kill-animation.ts` runs on kill confirmation. `Wall.tsx` owns the command dispatch and calls it after the user confirms. It fades the real pane element in place (its content dissolves against the same-colored background), then removes the panel and FLIP-reveals the survivors:
+`orchestrateKill(api, killedId, isSelected, …, onRemoved?)` in `lib/src/lib/kill-animation.ts` runs on kill confirmation. `Wall.tsx` owns the command dispatch and calls it after the user confirms. `isSelected` is a live reader, not a snapshot: the selection tail evaluates `isSelected(killedId)` at removal time (up to ~1s after the fade starts), and the overlay ring reads it once at fade start. The optional `onRemoved` callback fires once, on every path, immediately after `removePanel`: collapsing a branch can re-parent and blur the surviving selected pane, so `killPaneImmediately` uses it to re-assert that pane's DOM focus (through the shared `reassertPaneFocus` guard, see corner case #12). It fades the real pane element in place (its content dissolves against the same-colored background), then removes the panel and FLIP-reveals the survivors:
1. Add `.pane-fading-out` (or `.pane-fading-and-shrinking-to-br` for a last-pane kill) to the killed pane's group element. Block pointer events during the fade.
2. On `animationend`, snapshot `getBoundingClientRect` for every surviving panel's group element.
@@ -368,11 +368,13 @@ Shell-selection replacement uses the same pane add/remove primitives but also sh
Case handling is purely rect-based (measure before and after removal), so 2-pane splits, linear 3+ rows/columns, and nested splits all fall through the same code path with no per-case branching.
+Selection tail: after removal, `orchestrateKill` moves selection to a survivor (`panels[0]`, or `null` → auto-spawn when the last pane goes) **only when `isSelected(killedId)` reads true at removal time** — the killed pane is the selected one. `killPaneImmediately` passes a live reader (`selectedType === 'pane' && selectedId === killedId`) rather than a snapshot, so killing a background surface leaves the user's selection untouched, and a selection move *during* the ~1s fade is honored by design: navigating away from a dying selected pane means the tail no longer yanks selection to `panels[0]`, and navigating onto a dying pane means the tail adopts a survivor instead of leaving selection dangling on the removed panel. (The removal is tagged with `withProgrammaticActivation`, so dockview's activate-the-survivor echo — which previously covered those two edges by accident — no longer reads as user intent; the live check covers them explicitly.) The header kill button cannot produce an unselected kill: dockview activates the pane on pointerdown (`doSetGroupActive`) before the button's click handler runs, so mouse kills are always selected-pane kills. The not-selected cases are `dor kill` of a background surface (`dor kill surface:3`) and ensure's throwaway teardown.
+
### Auto-spawn delay
When `onDidRemovePanel` triggers the "always keep one pane visible" auto-spawn (see corner case #10), the `api.addPanel` call is deferred by 440ms **for the minimize path** — letting the selection-overlay slide to the door finish before the replacement's reveal starts. The kill path uses no extra delay (0ms): kill sequencing is already handled inside `orchestrateKill`, which removes the panel only after the fade's `animationend`. Reduced-motion users also get 0ms. The deferred spawn re-checks `totalPanels` at fire time and becomes a no-op if anything repopulated the pane area during the delay (e.g. a door reattach). If it does create a replacement pane, that pane spawns with the current default shell selection, matching manual splits and the standalone `[+]` action.
-The deferred spawn also only calls `selectPane` if selection is null. The kill handler clears selection to null, so the new pane takes focus. The minimize flow sets selection to the just-created door; preserving that door focus across the delay is the point.
+The deferred spawn adopts the replacement (`selectPane`) only when the current selection no longer points at anything real: null (`orchestrateKill`'s selection tail cleared it after a selected last-pane kill) or dangling (selection still names the removed pane, which the 0ms kill timer can observe if it beats React's flush of `setSelectedId(null)` into `selectedIdRef`). A *valid* selection is left alone — the just-created door on the minimize path (preserving that door focus across the delay is the point) or a live pane after an unselected kill — because the auto-spawn exists to keep a pane visible, not to steal selection.
## Corner cases
@@ -386,7 +388,8 @@ The deferred spawn also only calls `selectPane` if selection is null. The kill h
8. **Center drop merges panels**: intercepted at group-level `model.onWillDrop` and converted to a swap.
9. **Group drag has null panelId**: falls back to `api.getGroup(groupId).activePanel.id`.
10. **Auto-spawn on empty**: `onDidRemovePanel` creates a new session whenever the last visible pane is removed, whether or not doors exist — there is always a pane visible. The `addPanel` call is delayed 440ms on the minimize path (see "Auto-spawn delay" under Animations); the kill path is sequenced by `orchestrateKill` instead.
-11. **Door focus survives auto-spawn**: `api.addPanel` auto-activates the new panel, firing `onDidActivePanelChange`. When the current selection is a door (e.g., just-minimized last pane), that listener must not flip `selectedId` to the new pane — otherwise `selectedType === 'door'` + `selectedId === newPaneId` desyncs and the door loses its highlight while the `WorkspaceSelectionOverlay` is stuck on the stale door rect. The listener early-returns when `selectedType === 'door'`.
+11. **Door focus survives auto-spawn**: `api.addPanel` auto-activates the new panel, firing `onDidActivePanelChange`. When the current selection is a door (e.g., just-minimized last pane), selection must not flip to the new pane — otherwise `selectedType === 'door'` + `selectedId === newPaneId` desyncs and the door loses its highlight while the `WorkspaceSelectionOverlay` is stuck on the stale door rect. The auto-spawn's `addPanel` is tagged with `withProgrammaticActivation` and applies its adopt-or-leave selection policy explicitly at the spawn site (adopt only when selection is null or dangling; a valid door or live-pane selection is left alone), so door survival lives in the spawn-site policy — the listener no longer has a door special case. A dockview-native user interaction, though, DOES move selection off a door now — dragging a panel, or DOM focus landing in a surface (an embed focusing itself) — matching how those already move selection off a pane; explicit user selection of a pane always wins over door stickiness.
+12. **Focus-neutral surface creation (`dor ensure` / `dor iframe` / `dor ab`)**: unlike `dor split`, these must never move focus off the caller (`docs/specs/dor-cli.md`, `docs/specs/dor-browser.md`). Adding the pane `inactive` does **not** work: dockview renders and lays out a pane only once it becomes its group's active panel (a new group's `setActive` runs `doSetActivePanel`), so an `inactive` add — which skips both — leaves the pane behind a blank tile until the first click activates it. Instead the shared `runSurfaceAdd(focusNeutral, add)` helper adds the pane **active** (so it renders), then `add` hands the active group straight back to the caller via `settleFocusAfterAdd` (`caller.api.setActive()`); the new pane stays active *within its own group* and keeps rendering. The helper runs the add inside `withProgrammaticActivation` (`lib/src/lib/programmatic-activation.ts`), a depth-counted tag the `onDidActivePanelChange` listener (#11) checks so this programmatic activation churn never reads as user intent, and `selectedId`/mode never leave the caller. But adding the pane re-parents the caller's grid subtree in the DOM, which **blurs its xterm textarea** — and since `selectedId` never changed, `TerminalPanel`'s focus effect does not re-run to reclaim it, so the caller would silently stop receiving keystrokes until clicked. So the helper re-asserts the selected pane's focus with a deferred `focusSession` through the shared `reassertPaneFocus` rAF helper (deferred to beat dockview's post-split focus handling, like the click-focus rAF in `enterTerminalMode`), gated on three conditions: the pane is still the selected pane in passthrough (the condition `TerminalPanel` uses for `isFocused`, so a legitimate selection move makes it a no-op); the window still has focus (`document.hasFocus()` — a background `dor` command must never yank cross-frame focus out of the host editor, e.g. VS Code); and DOM focus is not in an in-page editable control (the inline-rename input, a non-terminal ``/`