diff --git a/docs/superpowers/specs/2026-06-20-d2b-drag-drop-spine-design.md b/docs/superpowers/specs/2026-06-20-d2b-drag-drop-spine-design.md
new file mode 100644
index 00000000..56cb25d6
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-20-d2b-drag-drop-spine-design.md
@@ -0,0 +1,367 @@
+# D.2b drag-drop spine (Stream B.1) — design
+
+**Date:** 2026-06-20
+**Phase:** D.2b retail-UI engine → D.5 core panels. Stream **B.1** of the
+2026-06-18 handoff (the shared drag-drop infra that BOTH the shortcut-drag stream
+(B) and the inventory window (C) sit on). Build it once, well — it is the
+critical-path lynchpin.
+**Branch:** `claude/hopeful-maxwell-214a12` (== main == `31d7ffd`).
+**Spec author:** lead-engineer brainstorm 2026-06-20 (4 design decisions confirmed
+by the user).
+
+**Read alongside:**
+- `docs/research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md` §5 / §5.7
+ (the pseudocode IS the spec; sub-element + flag anchors).
+- `docs/research/2026-06-18-d53-bar-finish-and-inventory-handoff.md` §B.1
+ (current-code readiness).
+- `claude-memory/project_d2b_retail_ui.md` (the toolkit), `feedback_ui_resolve_zero_magenta`
+ (the 0-id → magenta footgun; guard on the id, not the GL handle).
+
+---
+
+## 1. Goal & non-goals
+
+**Goal.** Complete the widget-level drag-drop machine so a player can pick up an
+item icon, see a translucent ghost track the cursor, watch hovered slots flip to
+**accept (green) / reject (red)**, and drop — at which point the drop is dispatched
+to the owning panel's handler with a fully-resolved payload. `UiRoot` already holds
+the *device-level* drag state machine (`BeginDrag`/`UpdateDragHover`/`FinishDrag`,
+promoted on >3 px move, live-wired to Silk.NET). This stream supplies the five
+missing pieces (handoff §B.1): payload injection, the cursor ghost, drop-target
+hooks on the cell, the accept/reject overlay, and the panel-handler interface.
+
+**In scope (the spine — generic, shared):**
+1. Payload injection into `UiRoot.BeginDrag` (today passes `payload: null`).
+2. A cursor-following drag ghost, painted by `UiRoot`, item-agnostic.
+3. Drop-target hooks on `UiItemSlot` (DragEnter/Over/DropReleased → accept/reject
+ overlay + dispatch).
+4. The `IItemListDragHandler` interface + registration on `UiItemList`.
+5. A typed `ItemDragPayload` describing what is dragged and where from.
+6. Moving the item-cell's use-trigger from MouseDown → Click (drag/click
+ disambiguation; also more retail-faithful).
+7. A **visible toolbar stub handler** so the chain is verifiable this session.
+
+**Out of scope (explicitly deferred — later streams):**
+- The `AddShortcut 0x019C` / `RemoveShortcut 0x019D` wire and the mutable
+ `ShortcutStore` (Stream **B.2**).
+- The inventory window, `UiItemList` N-cell grid mode, window manager
+ (Streams **C / window-mgr**).
+- Stack-split drag (the entry/slider `0x100001A3/A4`), give-to-NPC, drop-to-ground,
+ wield wire — all per-panel **HandleDropRelease** opcode selection (the deep-dive
+ §5.7 opcode table; lands with each consuming panel).
+- The faithful `m_pDragIcon` (base+overlay-no-underlay) drag composite — MVP reuses
+ the existing full icon at reduced alpha (AP-47).
+- Esc-to-cancel-drag (retail has it; future polish).
+
+---
+
+## 2. Retail grounding (the four confirmed decisions)
+
+The named decomp (`acclient_2013_pseudo_c.txt`) was greped to anchor each piece;
+the spine introduces **no new wire format** (opcodes are B.2/C), so the workflow's
+grep→cross-ref→pseudocode step applies to the *event-chain semantics* and
+`InqDropIconInfo`, both confirmed below.
+
+### 2.1 The retail chain (deep-dive §5.1–5.5)
+- The cell `UIElement_UIItem` is BOTH drag source and drop target. On
+ left-press-and-move it walks to its parent `UIElement_ItemList` and calls
+ `ItemList_BeginDrag` (`ListenToElementMessage` decomp 229344, msg `0x21`).
+- Every cell is a drop target *by construction* — `PostInit` sets the
+ `CatchDroppedItem` attribute `0x36` true (decomp 229744).
+- On drag-over the cell forwards to `ItemList_DragOver`; the LIST routes to its
+ registered `m_dragHandler` (`RegisterItemListDragHandler`, decomp 230461;
+ confirmed at acclient 0x004a539e + the gmToolbarUI block 0x004bdd89).
+- `InqDropIconInfo(dragElement, &objId, &containerId, &flags)` reads the dragged
+ element's properties (decomp 230533); the **flags** word (confirmed live at
+ `gmToolbarUI` 0x004bd162 / 0x004bd1af): **`flags & 0xE == 0`** ⇒ fresh-from-
+ inventory; **`flags & 4`** ⇒ within-list reorder. Accept/reject overlay state ids:
+ neutral `0x1000003f`, **reject `0x10000040`**, accept `0x10000041`
+ (`SetDragAcceptState`, confirmed 0x004bd16d).
+- The drag ghost `m_dragIcon` (id `0x10000345`) is a root-level translucent copy of
+ the icon, tracking the cursor (decomp 229738; §5.5).
+
+### 2.2 How the four picks map (all confirmed with the user)
+| # | Decision | Pick | Retail basis |
+|---|---|---|---|
+| 1 | Payload shape | typed `ItemDragPayload` record, snapshotted at begin | mirrors `InqDropIconInfo`'s {objId, container, flags}; our `flags` derived at drop from SourceKind+target |
+| 2 | Ghost render | painted by `UiRoot` via a generic `GetDragGhost()` hook | retail's root-level `m_dragElement` floats above all windows |
+| 3 | Drop unit | **cell** hits + shows overlay; **list** owns the handler | retail cell→`ItemList_DragOver`→`m_dragHandler` |
+| 4 | PR scope | infra + a **visible toolbar stub** handler | so the ghost/overlay/dispatch are confirmable now; wire is B.2 |
+
+---
+
+## 3. Architecture — components & boundaries
+
+All code lives in `src/AcDream.App/UI/` (the retail UI tree; NOT the ImGui devtools
+path). No new project references; `UiRoot` stays item-agnostic (it learns the
+payload/ghost only through two `UiElement` virtuals).
+
+```
+ Silk.NET mouse ─► UiRoot (device drag machine; UNCHANGED chain, + payload pull + ghost)
+ │ BeginDrag(source): payload = source.GetDragPayload(); cancel if null
+ │ Draw(): paint source.GetDragGhost() at the cursor (overlay layer)
+ ▼
+ UiItemSlot (the CELL — drag source + drop target + overlay owner)
+ │ GetDragPayload() → ItemDragPayload | null (null = empty cell, no drag)
+ │ GetDragGhost() → (tex,w,h) | null
+ │ OnEvent: Click→use; DragEnter→ask handler→overlay; DropReleased→dispatch
+ ▼ (walks Parent to)
+ UiItemList (the GRID — owns the registered handler)
+ │ DragHandler : IItemListDragHandler
+ ▼
+ IItemListDragHandler (a panel controller implements this)
+ bool OnDragOver(list, cell, payload) → accept/reject (overlay only)
+ void HandleDropRelease(list, cell, payload) → the action (wire = per panel)
+ ▲
+ ToolbarController (registers itself; STUB this stream — logs, no wire = TS-33)
+```
+
+**Boundary rules honored:** Rule 1 (no fat feature body in `GameWindow` — all logic
+in the UI classes + the controller); Rule 3 (panels target the toolkit, never a
+backend); `UiRoot` depends on nothing item-specific (Rule-2-spirit: the generic
+toolkit core doesn't reach up into the item layer — it pulls through virtuals).
+
+---
+
+## 4. New & changed types (precise signatures)
+
+### 4.1 NEW — `ItemDragPayload` + `ItemDragSource` (`UI/ItemDragPayload.cs`)
+```csharp
+namespace AcDream.App.UI;
+
+/// Where a dragged item came from — the retail InqDropIconInfo flag
+/// distinction (flags&0xE==0 fresh-from-inventory vs flags&4 within-list reorder)
+/// expressed as a typed enum. The drop handler maps SourceKind+target back to the
+/// fresh-vs-reorder decision. Decomp: gmToolbarUI 0x004bd162 / 0x004bd1af.
+public enum ItemDragSource { Inventory, ShortcutBar, Equipment, Ground }
+
+/// Snapshot of a drag-in-progress, taken at drag-begin (so a server move
+/// arriving mid-drag can't mutate it under us). Port of retail's m_dragElement +
+/// InqDropIconInfo out-params (objId/container/flags, decomp 230533).
+/// SourceContainer is NOT stored — the handler resolves the LIVE container from
+/// ClientObjectTable.Get(ObjId).ContainerId at drop (single source of truth; same
+/// value retail reads off the element, see §7).
+public sealed record ItemDragPayload(
+ uint ObjId, // dragged weenie guid (retail itemID, +0x5FC)
+ ItemDragSource SourceKind, // what kind of slot it left
+ int SourceSlot, // the source cell's SlotIndex (retail m_lastShortcutNumDragged)
+ UiItemSlot SourceCell); // back-ref: reorder-restore / clear source state / ghost texture
+```
+
+### 4.2 CHANGED — `UiElement` (two new virtuals; default null)
+```csharp
+/// The data this element carries when a drag begins. UiRoot.BeginDrag pulls
+/// this; a NULL return CANCELS the drag (retail: ItemList_BeginDrag only arms an
+/// occupied cell). Default null = not draggable.
+public virtual object? GetDragPayload() => null;
+
+/// The texture UiRoot paints at the cursor while this element is the drag
+/// source: (GL handle, width, height). Null = no ghost. Keeps UiRoot item-agnostic.
+public virtual (uint tex, int w, int h)? GetDragGhost() => null;
+```
+
+### 4.3 CHANGED — `UiRoot`
+- `BeginDrag(UiElement source)` (drop the `payload` param): pull
+ `var payload = source.GetDragPayload();` → if `null`, set `_dragCandidate = false`
+ and **return without arming** (no `DragSource`, no event). Else set
+ `DragSource`/`DragPayload`, fire `DragBegin` with the payload. Call site (line 188)
+ becomes `BeginDrag(Captured);`.
+- `Draw(ctx)`: after `DrawOverlays`, still inside the overlay layer, draw the ghost:
+ ```csharp
+ if (DragSource?.GetDragGhost() is { tex: var t, w: var gw, h: var gh } && t != 0)
+ ctx.DrawSprite(t, MouseX - gw/2f, MouseY - gh/2f, gw, gh, 0,0,1,1,
+ new Vector4(1,1,1, GhostAlpha)); // GhostAlpha = 0.6f
+ ```
+ Root transform is at origin during `Draw`, so cursor coords are absolute. The
+ ghost is NOT a tree element → it never intercepts hit-tests.
+
+### 4.4 CHANGED — `UiItemSlot`
+- Add `public int SlotIndex { get; set; } = -1;` (the cell's own index within its
+ panel — 0..17 toolbar, container slot for inventory; distinct from `ShortcutNum`,
+ the 1–9 LABEL which is -1 on the bottom row).
+- Add `public ItemDragSource SourceKind { get; set; } = ItemDragSource.Inventory;`
+ (controller overrides; toolbar = `ShortcutBar`).
+- Add drag-accept overlay sprites (confirmed ids; configurable; guard `id != 0`
+ before resolving — the magenta footgun):
+ ```csharp
+ public uint DragAcceptSprite { get; set; } = 0x060011F9u; // ItemSlot_DragOver_Accept
+ public uint DragRejectSprite { get; set; } = 0x060011F8u; // ItemSlot_DragOver_Reject
+ private enum DragAccept { None, Accept, Reject }
+ private DragAccept _dragAccept = DragAccept.None;
+ ```
+- `GetDragPayload()` → `ItemId != 0 ? new ItemDragPayload(ItemId, SourceKind, SlotIndex, this) : null`.
+- `GetDragGhost()` → `ItemId != 0 && IconTexture != 0 ? (IconTexture, (int)Width, (int)Height) : null`.
+- **Move use from MouseDown → Click** (the disambiguation fix): `OnEvent` keeps
+ `MouseDown` → `return true` (still consume the press, but DON'T fire use), and adds
+ `Click` (0x01) → `Clicked?.Invoke(); return true`. (`UiRoot` already suppresses the
+ post-drag Click at line 311, so a completed drag won't also use the item; a
+ press-release-without-move still emits Click → use.) `WireClick` in
+ `ToolbarController` is unchanged — it still assigns `Clicked`.
+- `OnEvent` new cases (acdream's UiEventType is the contract — these are acdream's
+ DragEnter/DragOver/DropReleased, NOT retail's raw per-cell 0x21/0x3e/0x15 codes;
+ see §6 note):
+ ```csharp
+ case UiEventType.DragBegin: return true; // we're the source; payload already pulled
+ case UiEventType.DragEnter: // pointer entered me mid-drag
+ _dragAccept = (FindList() is {} l && l.DragHandler is {} h
+ && e.Payload is ItemDragPayload p && h.OnDragOver(l, this, p))
+ ? DragAccept.Accept : DragAccept.Reject;
+ return true;
+ case UiEventType.DragOver: _dragAccept = DragAccept.None; return true; // UiRoot fires on LEAVE
+ case UiEventType.DropReleased:
+ _dragAccept = DragAccept.None;
+ if (e.Data0 == 1 // accepted = dropped on a DIFFERENT element (skips self/empty)
+ && FindList() is {} dl && dl.DragHandler is {} dh
+ && e.Payload is ItemDragPayload dp)
+ dh.HandleDropRelease(dl, this, dp);
+ return true;
+ ```
+ `FindList()` walks `Parent` until a `UiItemList`. The accept/reject overlay draws
+ in `OnDraw` on top of the icon+digit, `id != 0` guarded.
+
+### 4.5 NEW — `IItemListDragHandler` (`UI/IItemListDragHandler.cs`)
+```csharp
+/// A panel controller implements this and registers itself on each of its
+/// UiItemLists. Port of retail's m_dragHandler vtable (RegisterItemListDragHandler,
+/// decomp 230461). OnDragOver decides the accept/reject OVERLAY only (advisory);
+/// HandleDropRelease is authoritative — it does the action (or no-ops to reject).
+public interface IItemListDragHandler
+{
+ bool OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload);
+ void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload);
+}
+```
+
+### 4.6 CHANGED — `UiItemList`
+- Add `public IItemListDragHandler? DragHandler { get; private set; }` +
+ `public void RegisterDragHandler(IItemListDragHandler h) => DragHandler = h;`.
+
+### 4.7 CHANGED — `ToolbarController` (the stub handler — TS-33)
+- `: IItemListDragHandler`. In the ctor, after caching slots, set each cell's
+ `SlotIndex = i` and `SourceKind = ItemDragSource.ShortcutBar`, and
+ `list.RegisterDragHandler(this)`.
+- `OnDragOver` → `payload.ObjId != 0` (accept any real item; retail's
+ `IsShortcutEligible` gate is B.2).
+- `HandleDropRelease` → `Console.WriteLine($"[B.2 TODO] drop obj 0x{payload.ObjId:X8} " +
+ $"from {payload.SourceKind} slot {payload.SourceSlot} → toolbar slot {targetCell.SlotIndex}");`
+ No store mutation, no wire. (TS-33; replaced wholesale in B.2.)
+
+---
+
+## 5. Data flow — frame by frame
+
+1. **Press** on an occupied toolbar slot → `UiRoot.OnMouseDown` sets `Captured = cell`,
+ `_dragCandidate = true` (cell isn't `CapturesPointerDrag`, isn't a window-drag).
+2. **Move > 3 px** → `UiRoot.OnMouseMove` calls `BeginDrag(cell)` → pulls
+ `cell.GetDragPayload()` = `ItemDragPayload{ObjId, ShortcutBar, srcSlot, cell}`
+ (non-null since occupied) → arms `DragSource`/`DragPayload`, fires `DragBegin`.
+3. **Each move while dragging** → `UpdateDragHover(x,y)` hit-tests; on target CHANGE
+ it fires `DragOver` (LEAVE) on the old cell → its overlay resets to neutral, and
+ `DragEnter` on the new cell → it asks its list's handler `OnDragOver` → overlay
+ flips accept/reject. Meanwhile `UiRoot.Draw` paints the ghost at the cursor.
+4. **Release** → `UiRoot.OnMouseUp`: `DragSource` set → `FinishDrag(x,y)` delivers
+ `DropReleased` to the cell under the cursor (`Data0 = 1` if a different element).
+ The cell → its list's `HandleDropRelease(list, cell, payload)` → (stub) logs.
+ `DragSource`/`DragPayload` cleared → ghost stops. Capture released.
+5. **Empty-slot drag attempt** → step 2 `GetDragPayload()` returns null →
+ `_dragCandidate=false`, nothing armed; the eventual mouse-up emits Click (no-op,
+ `Clicked` guards `ItemId != 0`).
+
+---
+
+## 6. Edge cases & notes
+
+- **Event-id namespace.** acdream's `UiEventType` (DragBegin 0x15, DragEnter 0x21,
+ DragOver 0x1C, DropReleased 0x3E) is the contract the cell handles — these are
+ `UiRoot`'s constants and differ from retail's raw per-cell `ListenToElementMessage`
+ codes (0x21 begin / 0x3e over / 0x15 drop). Do NOT try to match the retail per-cell
+ codes; the SEMANTICS match, the numbers are `UiRoot`'s. (IA-12 already covers "UI
+ toolkit mirrors behavior, not byte-layout.")
+- **Hover re-eval cadence.** `UiRoot.UpdateDragHover` early-returns when the target
+ is unchanged, so accept/reject is computed once per cell-enter, not every move.
+ For item cells the decision is constant per (cell, payload), so this is
+ behavior-equivalent to retail's per-move `MouseOverTop` — no register row.
+- **Self-drop / drop-on-nothing.** `FinishDrag` sets `Data0=0` when the target is the
+ source or null; the cell gates `HandleDropRelease` on `Data0==1`, so both are clean
+ no-ops (= "put it back").
+- **Magenta footgun.** `DragAcceptSprite`/`DragRejectSprite` default non-zero; the
+ `OnDraw` overlay still guards `if (id != 0)` before `SpriteResolve`, never on the
+ returned GL handle (`feedback_ui_resolve_zero_magenta`).
+- **No heavy diagnostics in the render loop.** The stub's `Console.WriteLine` fires
+ only on a discrete drop event (not per frame), so it can't eat dt-based animations.
+
+---
+
+## 7. Divergence register rows (add in the IMPLEMENTATION commit)
+
+- **AP-47** (Documented approximation): *Drag ghost reuses the full composited
+ `m_pIcon` at reduced alpha (`GhostAlpha=0.6`) instead of retail's dedicated
+ `m_pDragIcon` (base + custom-overlay, NO underlay).* Where: `UiRoot.Draw` +
+ `UiItemSlot.GetDragGhost`. Why safe: cosmetic only — the dragged item is still
+ unambiguously identifiable; building the second composite is deferred polish. Risk:
+ the ghost shows the opaque type-default underlay backing rather than retail's
+ underlay-less translucent copy. Oracle: `IconData::RenderIcons` 407594-407625
+ (m_pDragIcon path); deep-dive §3.2/§5.5.
+- **TS-33** (Temporary stopgap): *Toolbar `IItemListDragHandler.HandleDropRelease`
+ is a logging stub — no `AddShortcut 0x019C`/`RemoveShortcut 0x019D` wire, no
+ `ShortcutStore` mutation.* Where: `ToolbarController.HandleDropRelease`. Awaiting:
+ Stream B.2. Risk: a drop onto the bar visibly does nothing but log; reorder/add/
+ remove are inert until B.2. Oracle: `gmToolbarUI::HandleDropRelease`
+ acclient_2013_pseudo_c.txt:197971.
+
+**Note (not a register row):** the payload omits `SourceContainer`; the handler
+resolves the live container via `ClientObjectTable.Get(ObjId).ContainerId` at drop.
+Same container id retail reads off the dragged element — single source of truth, no
+stale snapshot → no observable behavior deviation.
+
+---
+
+## 8. Testing (conformance)
+
+App-layer tests in `tests/AcDream.App.Tests/UI/` (no GL — pure logic):
+1. `GetDragPayload` returns null for an empty cell, a correct `ItemDragPayload` for a
+ bound cell (ObjId/SourceKind/SourceSlot/SourceCell).
+2. `BeginDrag` does NOT arm (`DragSource == null`) when the source's payload is null;
+ arms + fires `DragBegin` when non-null.
+3. Drop dispatch: a fake `IItemListDragHandler` registered on a `UiItemList` receives
+ `OnDragOver` on DragEnter and `HandleDropRelease` on DropReleased with the right
+ `(list, cell, payload)`; NOT called on a `Data0==0` self/empty drop.
+4. Accept/reject overlay state flips to Accept when the handler returns true, Reject
+ when false, None on leave.
+5. Use-vs-drag: a Click (no intervening drag) fires `Clicked`; a completed drag does
+ not (drive `UiRoot` MouseDown→move>3px→MouseUp and assert `Clicked` didn't fire).
+6. `FindList()` resolves the parent `UiItemList` through the cell's `Parent` chain.
+
+Drive these through `UiRoot`'s public `OnMouseDown/OnMouseMove/OnMouseUp` where
+possible (integration-level), so the device chain is exercised, not just the cell in
+isolation.
+
+---
+
+## 9. Acceptance criteria
+
+- [ ] `dotnet build` green; `dotnet test` green (existing + the new §8 tests).
+- [ ] `UiRoot` has zero compile-time dependency on `UiItemSlot`/`ItemDragPayload`
+ (item-agnostic; only the two `UiElement` virtuals bridge).
+- [ ] AP-47 + TS-33 rows added in the implementation commit; counts bumped.
+- [ ] **Visual (user):** with `ACDREAM_RETAIL_UI=1`, in-world, grab a hotbar item
+ and drag it: a translucent ghost follows the cursor; the hovered slot shows the
+ accept (green) frame, a slot that rejects shows red; on release over another
+ slot the log prints `[B.2 TODO] drop obj … → toolbar slot N`; the source item
+ stays put (no wire yet). A plain click still USES the item; a drag does not.
+- [ ] Memory updated if a durable lesson emerged; roadmap/ISSUES note if needed.
+
+---
+
+## 10. Subagent task slices (for the plan)
+
+1. **Toolkit core** — `ItemDragPayload`/`ItemDragSource`, the two `UiElement`
+ virtuals, `UiRoot.BeginDrag` payload pull + cancel, `UiRoot.Draw` ghost.
+2. **Cell hooks** — `UiItemSlot`: `SlotIndex`/`SourceKind`, `GetDragPayload`/
+ `GetDragGhost`, MouseDown→Click move, the DragEnter/Over/DropReleased cases +
+ accept/reject overlay; `IItemListDragHandler`; `UiItemList.DragHandler`.
+3. **Toolbar stub + wiring** — `ToolbarController : IItemListDragHandler`, register +
+ set SlotIndex/SourceKind, the logging `HandleDropRelease`; register rows.
+4. **Tests** — the §8 suite.
+
+(Slices 1→2 are sequential; 3 depends on 2; 4 can follow each.)