diff --git a/docs/superpowers/plans/2026-06-16-d2b-toolbar-phase1.md b/docs/superpowers/plans/2026-06-16-d2b-toolbar-phase1.md
new file mode 100644
index 00000000..1a083dbd
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-16-d2b-toolbar-phase1.md
@@ -0,0 +1,1104 @@
+# D.5.1 Toolbar (action bar) — Phase 1 Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Ship the retail action bar (`gmToolbarUI`, `LayoutDesc 0x21000016`) as acdream's first data-driven game panel: 18 shortcut slots populated from the persisted `PlayerDescription` shortcut block, each pinned item rendering its real composited icon, with click-to-use.
+
+**Architecture:** Reuse the shipped D.2b assembly pattern (dat `LayoutDesc` → `LayoutImporter` → `DatWidgetFactory` → thin find-by-id controller). Two new shared widgets (`UiItemSlot`, `UiItemList`) + a CPU icon-composite pipeline (`IconComposer`) + the wire plumbing to carry `IconId` from `CreateObject` into `ItemRepository` and to persist the shortcut list. The 18 toolbar slots already resolve to `UIElement_ItemList` (class `0x10000031`) through the dat `BaseElement`/`BaseLayoutId` chain (slot → `0x100001B2` → `0x10000339`@`0x2100003D`, Type `0x10000031`), so one `DatWidgetFactory` branch makes them `UiItemList`s automatically; the item cell is created procedurally by the list.
+
+**Tech Stack:** C# .NET 10, Silk.NET OpenGL, the in-tree `AcDream.App/UI` retained-mode toolkit, `DatCollection` for RenderSurface decode, xUnit.
+
+**Spec:** [`docs/superpowers/specs/2026-06-16-d2b-toolbar-phase1-design.md`](../specs/2026-06-16-d2b-toolbar-phase1-design.md).
+**Research anchors:** [`docs/research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md`](../../research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md), [`docs/research/2026-06-16-action-bar-toolbar-deep-dive.md`](../../research/2026-06-16-action-bar-toolbar-deep-dive.md).
+
+**Spec deltas discovered during planning (elaboration, not contradiction):**
+- Spec §4.4 assumed "just capture IconId." Reality: acdream's `CreateObject.TryParse` discards IconId (`CreateObject.cs:516`) AND there is **no** `CreateObject`→`ItemRepository` wiring at all — the repo is populated only from `PlayerDescription` with stub `ItemInstance`s (`ObjectId`+`WeenieClassId`). So Tasks 2–4 add: capture IconId, enrich the repo from the spawn event, and persist `Parsed.Shortcuts` (currently parsed then discarded in `GameEventWiring`).
+- IconId source is CONFIRMED to be `CreateObject` for contained pack items (ACE `WorldObject_Networking.cs:79` writes `WritePackedDwordOfKnownType(IconId, 0x6000000)` unconditionally; Chorizite `PublicWeenieDesc` reads `Icon` with no flag gate). No fallback needed.
+- Phase-1 `IconComposer` scope: CPU-composite the layers whose source data `ItemInstance` already exposes (custom underlay `IconUnderlayId` + base `IconId` + custom overlay `IconOverlayId`, alpha-over). The retail `IconData::RenderIcons` (decomp 407524) `GetByEnum` type-default-underlay, the overlay `ReplaceColor` tint, and the effect overlay need wire data not yet parsed (overlay tint color, `IconEffects`) — DEFERRED with divergence rows (Task 12). This keeps Approach A (faithful CPU pre-composite) while scoping to available data.
+
+---
+
+## Task 0: Register D.5.1 in the roadmap
+
+**Files:**
+- Modify: `docs/plans/2026-04-11-roadmap.md` (the D.5 entry, ~line 433)
+
+- [ ] **Step 1: Add the D.5.1 sub-phase entry under D.5**
+
+In `docs/plans/2026-04-11-roadmap.md`, immediately after the `D.5 — Core panels` bullet (the one at ~line 433), add:
+
+```markdown
+- **D.5.1 — Toolbar (action bar) [IN PROGRESS].** First D.5 sub-phase. `gmToolbarUI` (`LayoutDesc 0x21000016`) as the first data-driven game panel: 18 shortcut slots from the persisted `PlayerDescription` SHORTCUT block, real composited icons, click-to-use. New shared widgets `UiItemSlot` (`UIElement_UIItem` 0x10000032, procedural) + `UiItemList` (`UIElement_ItemList` 0x10000031, factory-registered) + `IconComposer` (CPU 5-layer composite, `IconData::RenderIcons` @407524) + the `CreateObject`→`ItemRepository` IconId wiring. Spec/plan: `docs/superpowers/{specs,plans}/2026-06-16-d2b-toolbar-phase1*.md`. Deferred to later D.5 sub-phases: drag/reorder, the AddShortcut/RemoveShortcut mutate wire, meters/slider, spell shortcuts, faithful window manager, inventory, paperdoll.
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add docs/plans/2026-04-11-roadmap.md
+git commit -m "docs(D.5.1): register toolbar phase-1 in the roadmap"
+```
+
+---
+
+## Task 1: Capture `IconId` in `CreateObject.Parsed`
+
+**Files:**
+- Modify: `src/AcDream.Core.Net/Messages/CreateObject.cs` (the `Parsed` struct ~lines 105-142; the parse at lines 515-516)
+- Test: `tests/AcDream.Core.Net.Tests/CreateObjectTests.cs` (add a test; create the file if no CreateObject test exists — verify with `Glob tests/AcDream.Core.Net.Tests/*reate*bject*`)
+
+- [ ] **Step 1: Write the failing test**
+
+Add to `tests/AcDream.Core.Net.Tests/CreateObjectTests.cs` (mirror an existing CreateObject test's byte-buffer construction; if none exists, build a minimal body using the same field order as `TryParse`). The assertion that matters:
+
+```csharp
+[Fact]
+public void TryParse_capturesIconId()
+{
+ // A CreateObject body for a simple contained item. Build the bytes with the
+ // exact field order TryParse reads (guid, ... name, packed WeenieClassId,
+ // packed-of-known-type IconId 0x06xxxxxx, u32 itemType, ...). Reuse the helper
+ // that an existing CreateObject test uses to assemble a body; the new assertion:
+ var parsed = CreateObject.TryParse(BuildContainedItemBody(iconId: 0x06001234u));
+
+ Assert.NotNull(parsed);
+ Assert.Equal(0x06001234u, parsed!.Value.IconId);
+}
+```
+
+- [ ] **Step 2: Run the test, verify it fails**
+
+Run: `dotnet test tests/AcDream.Core.Net.Tests --filter TryParse_capturesIconId`
+Expected: FAIL — `Parsed` has no member `IconId` (compile error), or `IconId` is 0.
+
+- [ ] **Step 3: Add `IconId` to the `Parsed` struct and capture it**
+
+In `src/AcDream.Core.Net/Messages/CreateObject.cs`, add a field to the `Parsed` struct (the readonly struct around lines 105-142):
+
+```csharp
+public uint IconId; // 0x06xxxxxx RenderSurface id of the item icon (0 = none)
+```
+
+In `TryParse`, change the discard at line 516 to capture, and assign it into the returned `Parsed`. Replace:
+
+```csharp
+_ = ReadPackedDwordOfKnownType(body, ref pos, IconTypePrefix);
+```
+with:
+```csharp
+uint iconId = ReadPackedDwordOfKnownType(body, ref pos, IconTypePrefix);
+```
+Then add `IconId = iconId,` to the object/struct initializer where `Parsed` is constructed (the `return new Parsed { ... }` near the end of `TryParse`). Leave the `WeenieClassId` discard at line 515 as-is for now (the spawn event already carries it separately; capturing it is out of phase-1 scope).
+
+- [ ] **Step 4: Run the test, verify it passes**
+
+Run: `dotnet test tests/AcDream.Core.Net.Tests --filter TryParse_capturesIconId`
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/AcDream.Core.Net/Messages/CreateObject.cs tests/AcDream.Core.Net.Tests/CreateObjectTests.cs
+git commit -m "feat(D.5.1): capture IconId in CreateObject.Parsed (was discarded at cs:516)"
+```
+
+---
+
+## Task 2: `ItemRepository.EnrichItem` (icon enrichment, enrich-existing)
+
+**Files:**
+- Modify: `src/AcDream.Core/Items/ItemRepository.cs` (add a method; events already exist at lines 49-59)
+- Test: `tests/AcDream.Core.Tests/Items/ItemRepositoryTests.cs` (verify the dir with `Glob tests/AcDream.Core.Tests/**/*ItemRepository*`; if absent, create it)
+
+- [ ] **Step 1: Write the failing test**
+
+```csharp
+[Fact]
+public void EnrichItem_updatesIconOnExistingStub_andRaisesUpdated()
+{
+ var repo = new ItemRepository();
+ repo.AddOrUpdate(new ItemInstance { ObjectId = 0x5001u, WeenieClassId = 42u }); // stub from PlayerDescription
+ ItemInstance? updated = null;
+ repo.ItemPropertiesUpdated += i => updated = i;
+
+ bool hit = repo.EnrichItem(0x5001u, iconId: 0x06001234u, name: "Mana Stone", type: ItemType.Misc);
+
+ Assert.True(hit);
+ Assert.Equal(0x06001234u, repo.GetItem(0x5001u)!.IconId);
+ Assert.Equal("Mana Stone", repo.GetItem(0x5001u)!.Name);
+ Assert.NotNull(updated);
+}
+
+[Fact]
+public void EnrichItem_returnsFalse_whenItemUnknown()
+{
+ var repo = new ItemRepository();
+ Assert.False(repo.EnrichItem(0x9999u, 0x06001234u, "x", ItemType.Misc));
+}
+```
+
+- [ ] **Step 2: Run the test, verify it fails**
+
+Run: `dotnet test tests/AcDream.Core.Tests --filter EnrichItem`
+Expected: FAIL — `EnrichItem` not defined.
+
+- [ ] **Step 3: Implement `EnrichItem`**
+
+Add to `src/AcDream.Core/Items/ItemRepository.cs` (near `AddOrUpdate`):
+
+```csharp
+///
+/// Enrich an already-known item (a stub created from PlayerDescription) with the
+/// fuller data carried by its CreateObject (icon, name, type). Returns false if the
+/// item isn't tracked yet — phase 1 enriches existing items only; full
+/// CreateObject ingestion of newly-acquired items is the inventory phase.
+/// Raises ItemPropertiesUpdated on success so bound widgets (the toolbar) re-render.
+///
+public bool EnrichItem(uint objectId, uint iconId, string name, ItemType type)
+{
+ if (!_items.TryGetValue(objectId, out var item)) return false;
+ if (iconId != 0) item.IconId = iconId;
+ if (!string.IsNullOrEmpty(name)) item.Name = name;
+ if (type != default) item.Type = type;
+ ItemPropertiesUpdated?.Invoke(item);
+ return true;
+}
+```
+
+- [ ] **Step 4: Run the test, verify it passes**
+
+Run: `dotnet test tests/AcDream.Core.Tests --filter EnrichItem`
+Expected: PASS (both).
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/AcDream.Core/Items/ItemRepository.cs tests/AcDream.Core.Tests/Items/ItemRepositoryTests.cs
+git commit -m "feat(D.5.1): ItemRepository.EnrichItem (icon/name/type from CreateObject)"
+```
+
+---
+
+## Task 3: Thread `IconId` through the spawn event into `ItemRepository`
+
+This is integration wiring (no new pure unit; covered by Task 1/2 units + the visual gate). Three edits.
+
+**Files:**
+- Modify: the `EntitySpawn` record (locate: `Grep "record EntitySpawn" src/AcDream.Core.Net` — likely `src/AcDream.Core.Net/WorldSession.cs` or a sibling)
+- Modify: `src/AcDream.Core.Net/WorldSession.cs:701-719` (the `EntitySpawned?.Invoke(new EntitySpawn(...))`)
+- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (the `OnLiveEntitySpawned` handler — subscribed at line 2216)
+
+- [ ] **Step 1: Add `IconId` to the `EntitySpawn` record**
+
+Run: `Grep "record EntitySpawn" src/AcDream.Core.Net -n`. Add a `uint IconId` parameter to the record's positional parameter list (append it at the end to minimize call-site churn; note the one constructor call in WorldSession is updated next).
+
+- [ ] **Step 2: Pass `parsed.Value.IconId` at the invoke site**
+
+In `src/AcDream.Core.Net/WorldSession.cs`, in the `EntitySpawned?.Invoke(new EntitySpawn(...))` block (lines 701-719), add `parsed.Value.IconId` as the final constructor argument (matching the new record parameter position).
+
+- [ ] **Step 3: Enrich the repo in the spawn handler**
+
+In `src/AcDream.App/Rendering/GameWindow.cs`, find `OnLiveEntitySpawned` (the handler subscribed at line 2216). Add, near the top of the handler body (after the `EntitySpawn` arg is in scope, call it `e`):
+
+```csharp
+// D.5.1: enrich a known inventory/equipped item (stubbed from PlayerDescription)
+// with the icon/name/type its CreateObject carries, so the toolbar can render it.
+Items.EnrichItem(e.Guid, e.IconId, e.Name, e.ItemType);
+```
+
+(`Items` is the `ItemRepository` field at `GameWindow.cs:598`. `EnrichItem` is a no-op returning false for non-item spawns — players, NPCs, furniture — because they aren't in the repo, so this is safe to call unconditionally.)
+
+- [ ] **Step 4: Build + run the full suite**
+
+Run: `dotnet build` then `dotnet test`
+Expected: green (no behavior regression; the new arg threads through).
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/AcDream.Core.Net/WorldSession.cs src/AcDream.App/Rendering/GameWindow.cs
+git commit -m "feat(D.5.1): thread CreateObject IconId into ItemRepository via spawn event"
+```
+
+---
+
+## Task 4: Persist `Parsed.Shortcuts` (the durable holder)
+
+`Parsed.Shortcuts` is parsed in `GameEventWiring.WireAll`'s PlayerDescription handler then discarded. Surface it to a durable holder the toolbar reads.
+
+**Files:**
+- Modify: `src/AcDream.Core.Net/GameEventWiring.cs` (the `WireAll` signature + the PlayerDescription lambda)
+- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (add a `Shortcuts` field; pass a callback at the `WireAll(...)` call ~line 2269)
+- Test: `tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs`
+
+- [ ] **Step 1: Write the failing test**
+
+In `tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs`, add a test that feeds a `PlayerDescription` message carrying a SHORTCUT block through `WireAll` and asserts the new `onShortcuts` callback receives the parsed list. Mirror an existing `GameEventWiringTests` PlayerDescription test for the message-construction + dispatch harness:
+
+```csharp
+[Fact]
+public void WireAll_PlayerDescription_invokesOnShortcuts()
+{
+ IReadOnlyList? got = null;
+ // ... build the same harness an existing PD test uses, but pass the new
+ // onShortcuts callback into WireAll: onShortcuts: list => got = list
+ // then dispatch a PD message whose SHORTCUT block has one entry (idx=0, guid=0x5001, spell=0, layer=0).
+
+ Assert.NotNull(got);
+ Assert.Single(got!);
+ Assert.Equal(0x5001u, got![0].ObjectGuid);
+}
+```
+
+- [ ] **Step 2: Run the test, verify it fails**
+
+Run: `dotnet test tests/AcDream.Core.Net.Tests --filter WireAll_PlayerDescription_invokesOnShortcuts`
+Expected: FAIL — `WireAll` has no `onShortcuts` parameter.
+
+- [ ] **Step 3: Add the callback to `WireAll` and invoke it**
+
+In `src/AcDream.Core.Net/GameEventWiring.cs`:
+- Add a parameter to `WireAll`: `Action>? onShortcuts = null` (optional, so existing callers/tests compile unchanged).
+- In the PlayerDescription handler lambda (where `Parsed` is in scope, ~lines 281-433), after the existing inventory population, add:
+
+```csharp
+onShortcuts?.Invoke(parsed.Shortcuts);
+```
+
+- [ ] **Step 4: Run the test, verify it passes**
+
+Run: `dotnet test tests/AcDream.Core.Net.Tests --filter WireAll_PlayerDescription_invokesOnShortcuts`
+Expected: PASS.
+
+- [ ] **Step 5: Store the shortcuts in GameWindow**
+
+In `src/AcDream.App/Rendering/GameWindow.cs`:
+- Add a field near `Items` (line 598):
+
+```csharp
+/// Persisted hotbar shortcuts from the last PlayerDescription (D.5.1 toolbar source).
+public IReadOnlyList Shortcuts { get; private set; }
+ = System.Array.Empty();
+```
+- At the `GameEventWiring.WireAll(...)` call (~line 2269), pass `onShortcuts: list => Shortcuts = list`.
+
+- [ ] **Step 6: Build + commit**
+
+Run: `dotnet build` then `dotnet test`
+Expected: green.
+
+```bash
+git add src/AcDream.Core.Net/GameEventWiring.cs src/AcDream.App/Rendering/GameWindow.cs tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs
+git commit -m "feat(D.5.1): persist PlayerDescription shortcuts (were parsed then discarded)"
+```
+
+---
+
+## Task 5: `IconComposer` — CPU icon composite + cache
+
+**Files:**
+- Create: `src/AcDream.App/UI/IconComposer.cs`
+- Modify: `src/AcDream.App/Rendering/TextureCache.cs` (add a public `UploadRgba8` wrapper — it's currently private)
+- Test: `tests/AcDream.App.Tests/UI/IconComposerTests.cs`
+
+The pure compositing core is testable; the dat-decode + GL-upload is a thin shell exercised by the visual gate.
+
+- [ ] **Step 1: Write the failing test (pure composite)**
+
+`tests/AcDream.App.Tests/UI/IconComposerTests.cs`:
+
+```csharp
+using AcDream.App.UI;
+using Xunit;
+
+public class IconComposerTests
+{
+ private static byte[] Solid(int w, int h, byte r, byte g, byte b, byte a)
+ {
+ var px = new byte[w * h * 4];
+ for (int i = 0; i < w * h; i++) { px[i*4]=r; px[i*4+1]=g; px[i*4+2]=b; px[i*4+3]=a; }
+ return px;
+ }
+
+ [Fact]
+ public void Compose_alphaOver_topOpaqueLayerWins()
+ {
+ var bottom = (Solid(2, 2, 255, 0, 0, 255), 2, 2); // red, opaque
+ var top = (Solid(2, 2, 0, 0, 255, 255), 2, 2); // blue, opaque
+ var (rgba, w, h) = IconComposer.Compose(new[] { bottom, top });
+ Assert.Equal(2, w); Assert.Equal(2, h);
+ Assert.Equal(0, rgba[0]); // R
+ Assert.Equal(0, rgba[1]); // G
+ Assert.Equal(255, rgba[2]); // B — top layer won
+ Assert.Equal(255, rgba[3]); // A
+ }
+
+ [Fact]
+ public void Compose_alphaOver_transparentTopKeepsBottom()
+ {
+ var bottom = (Solid(1, 1, 255, 0, 0, 255), 1, 1);
+ var top = (Solid(1, 1, 0, 0, 255, 0), 1, 1); // fully transparent blue
+ var (rgba, _, _) = IconComposer.Compose(new[] { bottom, top });
+ Assert.Equal(255, rgba[0]); // bottom red preserved
+ Assert.Equal(0, rgba[2]);
+ }
+}
+```
+
+- [ ] **Step 2: Run the test, verify it fails**
+
+Run: `dotnet test tests/AcDream.App.Tests --filter IconComposer`
+Expected: FAIL — `IconComposer` not defined.
+
+- [ ] **Step 3: Implement `IconComposer`**
+
+`src/AcDream.App/UI/IconComposer.cs`:
+
+```csharp
+using System;
+using System.Collections.Generic;
+using AcDream.App.Rendering;
+using DatReaderWriter;
+using DatReaderWriter.DBObjs;
+
+namespace AcDream.App.UI;
+
+///
+/// Builds an item icon by alpha-compositing its RenderSurface layers into one 32×32
+/// texture, mirroring retail IconData::RenderIcons (decomp 407524). Each layer is a
+/// 0x06 RenderSurface decoded DIRECTLY (the D.2b RenderSurface-vs-Surface rule).
+/// Phase 1 composites the layers ItemInstance exposes (custom underlay + base +
+/// custom overlay); the GetByEnum type-default underlay, the overlay ReplaceColor
+/// tint, and the effect overlay are deferred (see plan Task 12 / divergence rows).
+/// Composited textures are cached by their layer-id tuple.
+///
+public sealed class IconComposer
+{
+ private readonly DatCollection _dats;
+ private readonly TextureCache _cache;
+ private readonly Dictionary<(uint, uint, uint), uint> _byTuple = new();
+
+ public IconComposer(DatCollection dats, TextureCache cache)
+ {
+ _dats = dats;
+ _cache = cache;
+ }
+
+ /// Pure alpha-over composite, bottom→top. Layers may differ in size;
+ /// the result is sized to the FIRST (bottom) layer and upper layers are sampled
+ /// top-left aligned (all icon layers are 32×32 in practice).
+ public static (byte[] rgba, int w, int h) Compose(IReadOnlyList<(byte[] rgba, int w, int h)> layers)
+ {
+ if (layers.Count == 0) return (Array.Empty(), 0, 0);
+ var (baseRgba, w, h) = layers[0];
+ var outp = (byte[])baseRgba.Clone();
+ for (int li = 1; li < layers.Count; li++)
+ {
+ var (src, sw, sh) = layers[li];
+ int cw = Math.Min(w, sw), ch = Math.Min(h, sh);
+ for (int y = 0; y < ch; y++)
+ for (int x = 0; x < cw; x++)
+ {
+ int di = (y * w + x) * 4, si = (y * sw + x) * 4;
+ float sa = src[si + 3] / 255f;
+ if (sa <= 0f) continue;
+ float da = 1f - sa;
+ outp[di] = (byte)(src[si] * sa + outp[di] * da);
+ outp[di + 1] = (byte)(src[si + 1] * sa + outp[di + 1] * da);
+ outp[di + 2] = (byte)(src[si + 2] * sa + outp[di + 2] * da);
+ outp[di + 3] = (byte)Math.Min(255f, src[si + 3] + outp[di + 3] * da);
+ }
+ }
+ return (outp, w, h);
+ }
+
+ /// Resolve (and cache) the composited GL texture for an item's icon
+ /// layers. Returns 0 if no base icon is available.
+ public uint GetIcon(uint iconId, uint underlayId, uint overlayId)
+ {
+ if (iconId == 0) return 0;
+ var key = (iconId, underlayId, overlayId);
+ if (_byTuple.TryGetValue(key, out var tex)) return tex;
+
+ var layers = new List<(byte[] rgba, int w, int h)>();
+ AddLayer(layers, underlayId);
+ AddLayer(layers, iconId);
+ AddLayer(layers, overlayId);
+ if (layers.Count == 0) return 0;
+
+ var (rgba, w, h) = Compose(layers);
+ uint handle = _cache.UploadRgba8(rgba, w, h, nearest: true);
+ _byTuple[key] = handle;
+ return handle;
+ }
+
+ private void AddLayer(List<(byte[], int, int)> layers, uint renderSurfaceId)
+ {
+ if (renderSurfaceId == 0) return;
+ if (!_dats.Portal.TryGet(renderSurfaceId, out var rs) &&
+ !_dats.HighRes.TryGet(renderSurfaceId, out rs))
+ return;
+ var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette: null);
+ layers.Add((decoded.Rgba8, decoded.Width, decoded.Height));
+ }
+}
+```
+
+- [ ] **Step 4: Add the public `UploadRgba8` wrapper to `TextureCache`**
+
+In `src/AcDream.App/Rendering/TextureCache.cs`, expose the existing private upload (the one `GetOrUploadRenderSurface` calls). Add:
+
+```csharp
+/// Upload raw RGBA8 bytes as a GL texture (used by IconComposer for
+/// CPU-composited icons). Returns the GL handle.
+public uint UploadRgba8(byte[] rgba, int width, int height, bool nearest)
+ => UploadRgba8Internal(rgba, width, height, nearest); // rename the existing private method to *Internal if needed, or call it directly if it already has this shape
+```
+
+(Verify the existing private upload's name/signature with `Grep "UploadRgba8" src/AcDream.App/Rendering/TextureCache.cs`; if it already takes `(byte[], int, int, bool)`, just change its accessibility to `public` instead of adding a wrapper.)
+
+- [ ] **Step 5: Run the tests, verify they pass; build**
+
+Run: `dotnet test tests/AcDream.App.Tests --filter IconComposer` then `dotnet build`
+Expected: PASS + green build.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/AcDream.App/UI/IconComposer.cs src/AcDream.App/Rendering/TextureCache.cs tests/AcDream.App.Tests/UI/IconComposerTests.cs
+git commit -m "feat(D.5.1): IconComposer — CPU alpha-over icon composite + cache"
+```
+
+---
+
+## Task 6: `UiItemSlot` widget (the item cell)
+
+**Files:**
+- Create: `src/AcDream.App/UI/UiItemSlot.cs`
+- Test: `tests/AcDream.App.Tests/UI/UiItemSlotTests.cs`
+
+- [ ] **Step 1: Write the failing test**
+
+```csharp
+using AcDream.App.UI;
+using Xunit;
+
+public class UiItemSlotTests
+{
+ [Fact]
+ public void IsLeafWidget()
+ => Assert.True(new UiItemSlot().ConsumesDatChildren);
+
+ [Fact]
+ public void DefaultEmptySprite_isToolbarBorder()
+ => Assert.Equal(0x060074CFu, new UiItemSlot().EmptySprite);
+
+ [Fact]
+ public void Empty_whenNoItem()
+ {
+ var s = new UiItemSlot();
+ Assert.Equal(0u, s.ItemId);
+ Assert.Equal(0u, s.IconTexture);
+ }
+
+ [Fact]
+ public void SetItem_setsIdAndTexture()
+ {
+ var s = new UiItemSlot();
+ s.SetItem(0x5001u, 0x99u);
+ Assert.Equal(0x5001u, s.ItemId);
+ Assert.Equal(0x99u, s.IconTexture);
+ }
+}
+```
+
+- [ ] **Step 2: Run the test, verify it fails**
+
+Run: `dotnet test tests/AcDream.App.Tests --filter UiItemSlot`
+Expected: FAIL — `UiItemSlot` not defined.
+
+- [ ] **Step 3: Implement `UiItemSlot`**
+
+`src/AcDream.App/UI/UiItemSlot.cs`:
+
+```csharp
+using System;
+using System.Numerics;
+
+namespace AcDream.App.UI;
+
+///
+/// One item-in-a-slot cell (port of retail UIElement_UIItem, class 0x10000032).
+/// A behavioral LEAF: it draws the empty-slot sprite when unbound, else a
+/// pre-composited icon texture (set by the controller). Holds the bound weenie
+/// guid (retail UIElement_UIItem::itemID, +0x5FC).
+///
+public sealed class UiItemSlot : UiElement
+{
+ public UiItemSlot() { ClickThrough = false; }
+
+ public override bool ConsumesDatChildren => true;
+
+ /// Bound weenie guid (0 = empty). Retail UIElement_UIItem::itemID.
+ public uint ItemId { get; private set; }
+
+ /// Pre-composited icon GL texture for the bound item (0 = none).
+ public uint IconTexture { get; private set; }
+
+ /// Empty-slot sprite. Default = the generic toolbar empty-slot border
+ /// 0x060074CF (uiitem template 0x21000037, state ItemSlot_Empty). Configurable so
+ /// paperdoll equip slots can use their per-slot silhouettes later.
+ public uint EmptySprite { get; set; } = 0x060074CFu;
+
+ /// RenderSurface id → (GL texture, w, h). Set by the factory/controller.
+ public Func? SpriteResolve { get; set; }
+
+ public void SetItem(uint itemId, uint iconTexture)
+ {
+ ItemId = itemId;
+ IconTexture = iconTexture;
+ }
+
+ public void Clear() { ItemId = 0; IconTexture = 0; }
+
+ protected override void OnDraw(UiRenderContext ctx)
+ {
+ if (ItemId != 0 && IconTexture != 0)
+ {
+ ctx.DrawSprite(IconTexture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
+ return;
+ }
+ if (SpriteResolve is not null && EmptySprite != 0)
+ {
+ var (tex, _, _) = SpriteResolve(EmptySprite);
+ if (tex != 0)
+ ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
+ }
+ }
+}
+```
+
+- [ ] **Step 4: Run the tests, verify they pass**
+
+Run: `dotnet test tests/AcDream.App.Tests --filter UiItemSlot`
+Expected: PASS (all four).
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/AcDream.App/UI/UiItemSlot.cs tests/AcDream.App.Tests/UI/UiItemSlotTests.cs
+git commit -m "feat(D.5.1): UiItemSlot widget (UIElement_UIItem cell port)"
+```
+
+---
+
+## Task 7: `UiItemList` widget + `DatWidgetFactory` branch
+
+**Files:**
+- Create: `src/AcDream.App/UI/UiItemList.cs`
+- Modify: `src/AcDream.App/UI/Layout/DatWidgetFactory.cs` (the `Create` switch, lines 63-71)
+- Test: `tests/AcDream.App.Tests/UI/UiItemListTests.cs` + `tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs` (verify the factory-test file exists with `Glob tests/AcDream.App.Tests/**/*WidgetFactory*`; if absent, create it)
+
+- [ ] **Step 1: Write the failing tests**
+
+`tests/AcDream.App.Tests/UI/UiItemListTests.cs`:
+
+```csharp
+using AcDream.App.UI;
+using Xunit;
+
+public class UiItemListTests
+{
+ [Fact]
+ public void IsLeafWidget() => Assert.True(new UiItemList().ConsumesDatChildren);
+
+ [Fact]
+ public void StartsWithOneCell_forSingleCellSlot()
+ {
+ var list = new UiItemList();
+ Assert.Equal(1, list.GetNumUIItems());
+ Assert.NotNull(list.GetItem(0));
+ }
+
+ [Fact]
+ public void Cell_returnsTheFirstSlot()
+ {
+ var list = new UiItemList();
+ Assert.Same(list.GetItem(0), list.Cell);
+ }
+}
+```
+
+Add to the factory test file:
+
+```csharp
+[Fact]
+public void Create_buildsUiItemList_forItemListClassId()
+{
+ var info = new AcDream.App.UI.Layout.ElementInfo { Id = 0x100001A7u, Type = 0x10000031u, Width = 32, Height = 32 };
+ var w = AcDream.App.UI.Layout.DatWidgetFactory.Create(info, _ => (0u, 0, 0), null);
+ Assert.IsType(w);
+}
+```
+
+- [ ] **Step 2: Run the tests, verify they fail**
+
+Run: `dotnet test tests/AcDream.App.Tests --filter "UiItemList|UiItemList_forItemListClassId"`
+Expected: FAIL — `UiItemList` not defined / factory returns `UiDatElement`.
+
+- [ ] **Step 3: Implement `UiItemList`**
+
+`src/AcDream.App/UI/UiItemList.cs`:
+
+```csharp
+using System;
+using System.Collections.Generic;
+
+namespace AcDream.App.UI;
+
+///
+/// A container of item cells (port of retail UIElement_ItemList, class 0x10000031).
+/// Behavioral LEAF: it creates/owns its UiItemSlot children procedurally, so the
+/// LayoutImporter must NOT build dat children. The toolbar uses single-cell
+/// instances (one slot); the inventory phase will grow this to an N-cell grid.
+///
+public sealed class UiItemList : UiElement
+{
+ private readonly List _cells = new();
+
+ public UiItemList(Func? spriteResolve = null)
+ {
+ SpriteResolve = spriteResolve;
+ // Single-cell default: every toolbar slot always shows one cell (empty or filled).
+ AddItem(new UiItemSlot { SpriteResolve = spriteResolve });
+ }
+
+ public override bool ConsumesDatChildren => true;
+
+ public Func? SpriteResolve { get; set; }
+
+ /// Convenience for single-cell slots (the toolbar): the first cell.
+ public UiItemSlot Cell => _cells[0];
+
+ public int GetNumUIItems() => _cells.Count;
+
+ public UiItemSlot? GetItem(int index)
+ => index >= 0 && index < _cells.Count ? _cells[index] : null;
+
+ public void AddItem(UiItemSlot cell)
+ {
+ cell.SpriteResolve ??= SpriteResolve;
+ cell.Left = 0; cell.Top = 0; cell.Width = Width; cell.Height = Height;
+ _cells.Add(cell);
+ AddChild(cell);
+ }
+
+ public void Flush()
+ {
+ foreach (var c in _cells) RemoveChild(c);
+ _cells.Clear();
+ }
+
+ protected override void OnDraw(UiRenderContext ctx)
+ {
+ // The factory sets THIS list's Width/Height AFTER construction, so the cell
+ // (added in the ctor) starts 0x0. For the single-cell toolbar slot, keep the
+ // cell sized to the list each frame; the cell paints itself in the children
+ // pass that follows. (N-cell grid layout is the inventory phase.)
+ if (_cells.Count > 0)
+ {
+ var cell = _cells[0];
+ cell.Left = 0; cell.Top = 0; cell.Width = Width; cell.Height = Height;
+ }
+ }
+}
+```
+
+Note: `ConsumesDatChildren` stops the IMPORTER from adding dat children, but the list still draws its own `UiItemSlot` children (added via `AddChild`) through the normal `DrawSelfAndChildren` traversal — `ConsumesDatChildren` only gates the importer, not runtime children. The cell's `Width`/`Height` are synced in the list's `OnDraw` (which runs before the children pass), so the cell is correctly sized + hit-testable from the first rendered frame.
+
+- [ ] **Step 4: Add the factory branch**
+
+In `src/AcDream.App/UI/Layout/DatWidgetFactory.cs`, add to the `Create` switch (lines 63-71), before the `_` fallback:
+
+```csharp
+0x10000031u => new UiItemList(resolve), // UIElement_ItemList — toolbar/inventory/paperdoll slots
+```
+
+(The item *cell* class `0x10000032` is created procedurally by `UiItemList`, not via a static dat element in the toolbar, so it needs no factory branch this phase.)
+
+- [ ] **Step 5: Run the tests, verify they pass; build**
+
+Run: `dotnet test tests/AcDream.App.Tests --filter "UiItemList|ItemListClassId"` then `dotnet build`
+Expected: PASS + green.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/AcDream.App/UI/UiItemList.cs src/AcDream.App/UI/Layout/DatWidgetFactory.cs tests/AcDream.App.Tests/UI/UiItemListTests.cs tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs
+git commit -m "feat(D.5.1): UiItemList widget + factory branch for class 0x10000031"
+```
+
+---
+
+## Task 8: `ToolbarController` (the `gmToolbarUI::PostInit` analogue)
+
+**Files:**
+- Create: `src/AcDream.App/UI/Layout/ToolbarController.cs`
+- Test: `tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs`
+
+- [ ] **Step 1: Write the failing test**
+
+```csharp
+using System;
+using System.Collections.Generic;
+using AcDream.App.UI;
+using AcDream.App.UI.Layout;
+using AcDream.Core.Items;
+using AcDream.Core.Net.Messages;
+using Xunit;
+
+public class ToolbarControllerTests
+{
+ private static readonly uint[] Row1 =
+ { 0x100001A7,0x100001A8,0x100001A9,0x100001AA,0x100001AB,0x100001AC,0x100001AD,0x100001AE,0x100001AF };
+ private static readonly uint[] Row2 =
+ { 0x100006B7,0x100006B8,0x100006B9,0x100006BA,0x100006BB,0x100006BC,0x100006BD,0x100006BE,0x100006BF };
+
+ private static (ImportedLayout layout, Dictionary slots) FakeToolbar()
+ {
+ var dict = new Dictionary();
+ var slots = new Dictionary();
+ var root = new UiPanel();
+ foreach (var id in Row1) AddSlot(id);
+ foreach (var id in Row2) AddSlot(id);
+ return (new ImportedLayout(root, dict), slots);
+
+ void AddSlot(uint id)
+ {
+ var list = new UiItemList(_ => (0u, 0, 0)) { Width = 32, Height = 32 };
+ dict[id] = list; slots[id] = list; root.AddChild(list);
+ }
+ }
+
+ [Fact]
+ public void Populate_bindsShortcutToCorrectSlot()
+ {
+ var (layout, slots) = FakeToolbar();
+ var repo = new ItemRepository();
+ repo.AddOrUpdate(new ItemInstance { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
+ var shortcuts = new List
+ { new(Index: 0, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
+
+ ToolbarController.Bind(layout, repo, () => shortcuts,
+ iconIds: (_,_,_) => 0x77u, useItem: _ => { });
+
+ Assert.Equal(0x5001u, slots[Row1[0]].Cell.ItemId);
+ Assert.Equal(0x77u, slots[Row1[0]].Cell.IconTexture);
+ Assert.Equal(0u, slots[Row1[1]].Cell.ItemId); // others empty
+ }
+
+ [Fact]
+ public void DeferredRebind_whenItemArrivesLate()
+ {
+ var (layout, slots) = FakeToolbar();
+ var repo = new ItemRepository(); // item NOT present yet
+ var shortcuts = new List
+ { new(Index: 2, ObjectGuid: 0x5002u, SpellId: 0, Layer: 0) };
+
+ ToolbarController.Bind(layout, repo, () => shortcuts,
+ iconIds: (_,_,_) => 0x88u, useItem: _ => { });
+ Assert.Equal(0u, slots[Row1[2]].Cell.ItemId); // not bound yet
+
+ repo.AddOrUpdate(new ItemInstance { ObjectId = 0x5002u, WeenieClassId = 1u, IconId = 0x06005678u });
+
+ Assert.Equal(0x5002u, slots[Row1[2]].Cell.ItemId); // rebound on ItemAdded
+ }
+
+ [Fact]
+ public void Click_emitsUseForBoundItem()
+ {
+ var (layout, slots) = FakeToolbar();
+ var repo = new ItemRepository();
+ repo.AddOrUpdate(new ItemInstance { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
+ var shortcuts = new List
+ { new(Index: 0, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
+ uint used = 0;
+
+ ToolbarController.Bind(layout, repo, () => shortcuts,
+ iconIds: (_,_,_) => 0x77u, useItem: g => used = g);
+ slots[Row1[0]].Cell.OnEvent(new UiEvent { Type = UiEventType.MouseDown });
+
+ Assert.Equal(0x5001u, used);
+ }
+}
+```
+
+(Adapt `UiEvent` construction + the click-emit seam to the toolkit's actual event shape — see Step 3; if `UiItemSlot` needs a `Clicked` callback rather than handling `OnEvent`, wire that in Step 3 and update this assertion to invoke it.)
+
+- [ ] **Step 2: Run the tests, verify they fail**
+
+Run: `dotnet test tests/AcDream.App.Tests --filter ToolbarController`
+Expected: FAIL — `ToolbarController` not defined.
+
+- [ ] **Step 3: Implement `ToolbarController`**
+
+`src/AcDream.App/UI/Layout/ToolbarController.cs`:
+
+```csharp
+using System;
+using System.Collections.Generic;
+using AcDream.Core.Items;
+using AcDream.Core.Net.Messages;
+
+namespace AcDream.App.UI.Layout;
+
+///
+/// Binds the imported gmToolbarUI window (LayoutDesc 0x21000016) to live data —
+/// the gm*UI::PostInit analogue. Finds the 18 shortcut slots (UiItemList) by id,
+/// populates them from the persisted PlayerDescription shortcuts (UpdateFromPlayerDesc),
+/// re-binds deferred slots when an item's CreateObject arrives (SetDelayedShortcutNum),
+/// and on click uses the bound item (UseShortcut → ItemHolder::UseObject → use-item).
+///
+public sealed class ToolbarController
+{
+ // Slot element ids, in slot-index order (toolbar pre-dump 0x21000016).
+ private static readonly uint[] SlotIds =
+ {
+ 0x100001A7,0x100001A8,0x100001A9,0x100001AA,0x100001AB,0x100001AC,0x100001AD,0x100001AE,0x100001AF,
+ 0x100006B7,0x100006B8,0x100006B9,0x100006BA,0x100006BB,0x100006BC,0x100006BD,0x100006BE,0x100006BF,
+ };
+ // Hidden-by-default elements (gmToolbarUI::PostInit): selected-object meters + stack slider.
+ private static readonly uint[] HiddenIds = { 0x100001A1, 0x100001A2, 0x100001A4 };
+
+ private readonly UiItemList?[] _slots = new UiItemList?[SlotIds.Length];
+ private readonly ItemRepository _repo;
+ private readonly Func> _shortcuts;
+ private readonly Func _iconIds; // (iconId, underlay, overlay) → GL texture
+ private readonly Action _useItem;
+
+ private ToolbarController(ImportedLayout layout, ItemRepository repo,
+ Func> shortcuts,
+ Func iconIds, Action useItem)
+ {
+ _repo = repo; _shortcuts = shortcuts; _iconIds = iconIds; _useItem = useItem;
+
+ for (int i = 0; i < SlotIds.Length; i++)
+ {
+ _slots[i] = layout.FindElement(SlotIds[i]) as UiItemList;
+ if (_slots[i] is { } list)
+ WireClick(list);
+ }
+ foreach (var id in HiddenIds)
+ if (layout.FindElement(id) is { } e) e.Visible = false;
+
+ repo.ItemAdded += _ => Populate();
+ repo.ItemPropertiesUpdated += _ => Populate();
+ }
+
+ public static ToolbarController Bind(ImportedLayout layout, ItemRepository repo,
+ Func> shortcuts,
+ Func iconIds, Action useItem)
+ {
+ var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem);
+ c.Populate();
+ return c;
+ }
+
+ /// Port of gmToolbarUI::UpdateFromPlayerDesc — flush then bind each shortcut.
+ public void Populate()
+ {
+ foreach (var list in _slots) list?.Cell.Clear();
+
+ foreach (var sc in _shortcuts())
+ {
+ if (sc.ObjectGuid == 0) continue; // spell shortcuts — deferred phase
+ if (sc.Index >= _slots.Length) continue;
+ var list = _slots[(int)sc.Index];
+ if (list is null) continue;
+ var item = _repo.GetItem(sc.ObjectGuid);
+ if (item is null) continue; // SetDelayedShortcutNum: re-bound on ItemAdded
+ uint tex = _iconIds(item.IconId, item.IconUnderlayId, item.IconOverlayId);
+ list.Cell.SetItem(sc.ObjectGuid, tex);
+ }
+ }
+
+ private void WireClick(UiItemList list)
+ {
+ list.Cell.Clicked = () =>
+ {
+ if (list.Cell.ItemId != 0) _useItem(list.Cell.ItemId);
+ };
+ }
+}
+```
+
+This requires a `Clicked` callback on `UiItemSlot`. Add to `UiItemSlot` (Task 6 file) and have `OnEvent` invoke it on mouse-down:
+
+```csharp
+public Action? Clicked { get; set; }
+
+public override bool OnEvent(in UiEvent e)
+{
+ if (e.Type == UiEventType.MouseDown) { Clicked?.Invoke(); return true; }
+ return false;
+}
+```
+
+(If `UiEvent`/`UiEventType` member names differ, match the toolkit's actual definitions — `Grep "enum UiEventType" src/AcDream.App/UI`.)
+
+- [ ] **Step 4: Run the tests, verify they pass; build**
+
+Run: `dotnet test tests/AcDream.App.Tests --filter ToolbarController` then `dotnet build`
+Expected: PASS (all three) + green.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/AcDream.App/UI/Layout/ToolbarController.cs src/AcDream.App/UI/UiItemSlot.cs tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs
+git commit -m "feat(D.5.1): ToolbarController — bind 18 slots, populate, deferred rebind, click-to-use"
+```
+
+---
+
+## Task 9: Wire the toolbar into `GameWindow`
+
+Integration (covered by the visual gate). Mirror the vitals import + mount.
+
+**Files:**
+- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (the `if (_options.RetailUi)` block, ~lines 1761-1898, after the chat block)
+
+- [ ] **Step 1: Construct the IconComposer once**
+
+In the `if (_options.RetailUi)` block (after `_uiHost` + the sprite resolver `ResolveChrome` exist, ~line 1778), add:
+
+```csharp
+var iconComposer = new AcDream.App.UI.IconComposer(_dats!, cache);
+```
+
+- [ ] **Step 2: Import the toolbar layout + bind the controller**
+
+After the chat block (~line 1898), add (mirroring the vitals import at 1800-1828):
+
+```csharp
+AcDream.App.UI.Layout.ImportedLayout? toolbarLayout;
+lock (_datLock)
+ toolbarLayout = AcDream.App.UI.Layout.LayoutImporter.Import(
+ _dats!, 0x21000016u, ResolveChrome, vitalsDatFont);
+if (toolbarLayout is not null)
+{
+ AcDream.App.UI.Layout.ToolbarController.Bind(
+ toolbarLayout, Items,
+ () => Shortcuts,
+ iconIds: (icon, under, over) => iconComposer.GetIcon(icon, under, over),
+ useItem: guid => UseItemByGuid(guid)); // existing use-item path (see Step 3)
+
+ var toolbarRoot = toolbarLayout.Root;
+ toolbarRoot.Left = 10; toolbarRoot.Top = 300; // initial position; user-draggable
+ toolbarRoot.Anchors = AcDream.App.UI.AnchorEdges.Left | AcDream.App.UI.AnchorEdges.Top;
+ toolbarRoot.Draggable = true;
+ _uiHost.Root.AddChild(toolbarRoot);
+}
+```
+
+- [ ] **Step 3: Provide `UseItemByGuid`**
+
+acdream already builds + sends use-item at `GameWindow.cs:11577-11579` (`InteractRequests.BuildUse(seq, guid)` → `_liveSession.SendGameAction`). Extract that into a small helper if it isn't already callable by guid:
+
+```csharp
+private void UseItemByGuid(uint guid)
+{
+ if (_liveSession is null) return;
+ var seq = _liveSession.NextGameActionSequence();
+ var body = AcDream.Core.Net.Messages.InteractRequests.BuildUse(seq, guid);
+ _liveSession.SendGameAction(body);
+}
+```
+
+(If a guid-based use helper already exists near line 11577, call it instead of duplicating.)
+
+- [ ] **Step 4: Build**
+
+Run: `dotnet build`
+Expected: green.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/AcDream.App/Rendering/GameWindow.cs
+git commit -m "feat(D.5.1): mount the toolbar window under ACDREAM_RETAIL_UI"
+```
+
+---
+
+## Task 10: Full suite + manual smoke gate
+
+- [ ] **Step 1: Build + full test suite**
+
+Run: `dotnet build` then `dotnet test`
+Expected: all green.
+
+- [ ] **Step 2: Launch + visual verification (the user's gate)**
+
+Launch per CLAUDE.md (PowerShell, background, Tee to `launch.log`):
+
+```powershell
+$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
+$env:ACDREAM_LIVE = "1"
+$env:ACDREAM_RETAIL_UI = "1"
+$env:ACDREAM_TEST_HOST = "127.0.0.1"; $env:ACDREAM_TEST_PORT = "9000"
+$env:ACDREAM_TEST_USER = "testaccount"; $env:ACDREAM_TEST_PASS = "testpassword"
+dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object -FilePath "launch.log"
+```
+
+Acceptance (user confirms by looking):
+- An 18-slot action bar (2 rows of 9) renders with the dat chrome + empty-slot sprites.
+- Any persisted `+Acdream` shortcuts show their real composited item icons.
+- Clicking a pinned item uses it (observe server-side / in-world effect).
+- The bar drags as a whole window.
+
+(If `+Acdream` has no persisted shortcuts, the empty-slot render is still a valid gate; pinning real items to test icons may need the inventory phase or a server-side pre-pin — note this to the user.)
+
+---
+
+## Task 11: Bookkeeping — divergence register, roadmap shipped, memory
+
+**Files:**
+- Modify: `docs/architecture/retail-divergence-register.md`
+- Modify: `docs/plans/2026-04-11-roadmap.md`
+- Modify: `claude-memory/project_d2b_retail_ui.md` (durable lesson, if any)
+
+- [ ] **Step 1: Add divergence rows**
+
+Add rows to `docs/architecture/retail-divergence-register.md` for the phase-1 icon deferrals + the empty-sprite constant:
+
+- Icon composite omits the retail `GetByEnum` type-default underlay (`IconData::RenderIcons` 407524, enum 0x10000004), the overlay `ReplaceColor` tint, and the effect overlay (enum 0x10000005) — their source data (overlay tint color, `IconEffects`) isn't parsed yet. Risk: items with a material/effect overlay render without it. Retire when the inventory phase parses the full `PublicWeenieDesc`.
+- `UiItemSlot.EmptySprite` defaults to the constant `0x060074CF` instead of importing the empty-slot state from the uiitem template `0x21000037`. Risk: paperdoll equip-slot silhouettes need per-slot empty sprites (already configurable). Retire when the cell imports its template states.
+- (Reuse the existing IA-12 row for whole-window-drag — no new row.)
+
+- [ ] **Step 2: Flip the roadmap entry to shipped**
+
+In `docs/plans/2026-04-11-roadmap.md`, change the D.5.1 entry from `[IN PROGRESS]` to `✓ SHIPPED` with the commit range, mirroring the other D.2b shipped entries.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add docs/architecture/retail-divergence-register.md docs/plans/2026-04-11-roadmap.md claude-memory/project_d2b_retail_ui.md
+git commit -m "docs(D.5.1): divergence rows + roadmap shipped + memory for the toolbar"
+```
+
+---
+
+## Task 12 (FOLLOW-UP, optional within phase): faithful icon layers
+
+Deferred from Task 5 to keep phase 1 shippable; do only if the toolbar icons visibly lack the standard background. NOT required for the phase-1 acceptance gate.
+
+- Port `DBObj::GetByEnum(0x10000004, lsb(itemType)+1)` (the type-default underlay) — first confirm what `0x10000004` maps to (dump an EnumMapper DBObj) to decide whether it's the universal icon background. Add it as the bottom layer in `IconComposer.GetIcon`.
+- Parse `IconEffects`/`IconOverlay` tint from `CreateObject` (extend `CreateObject.Parsed` + `ItemInstance`), then add the `ReplaceColor` overlay tint + the effect overlay (`GetByEnum 0x10000005`).
+- Delete the corresponding divergence rows from Task 11 Step 1 as each layer lands.
+
+---
+
+## Self-review notes (author)
+
+- **Spec coverage:** spec §2 widgets → Tasks 6,7; §4.3 icon → Task 5 (+12); §4.4 CreateObject/ItemInstance → Tasks 1,2,3; §4.5 ToolbarController → Task 8; §4.6 wiring/gating → Task 9; §5 testing → per-task TDD + Task 10; §6 acceptance → Task 10; §8 bookkeeping → Tasks 0,11. The shortcut-holder (Task 4) was implicit in §4.5's "reads Parsed.Shortcuts" and is made explicit here.
+- **Type consistency:** `UiItemSlot.SetItem(uint,uint)` / `.Clear()` / `.ItemId` / `.IconTexture` / `.EmptySprite` / `.Clicked`; `UiItemList.Cell` / `.GetItem(int)` / `.GetNumUIItems()` / `.AddItem(UiItemSlot)` / `.Flush()`; `IconComposer.Compose(IReadOnlyList<(byte[],int,int)>)` / `.GetIcon(uint,uint,uint)`; `ItemRepository.EnrichItem(uint,uint,string,ItemType)`; `ToolbarController.Bind(ImportedLayout, ItemRepository, Func>, Func, Action)`. These match across Tasks 5-9.
+- **Known executor confirmations (grep-to-confirm, not placeholders):** the exact name/signature of `TextureCache`'s private RGBA upload (Task 5 Step 4); the `EntitySpawn` record location (Task 3 Step 1); the `UiEvent`/`UiEventType` member shape (Tasks 6/8); whether a guid-based use helper already exists near `GameWindow.cs:11577` (Task 9 Step 3). Each step names the grep + the change.
diff --git a/docs/superpowers/specs/2026-06-16-d2b-toolbar-phase1-design.md b/docs/superpowers/specs/2026-06-16-d2b-toolbar-phase1-design.md
index d1130bdb..ade2dd7e 100644
--- a/docs/superpowers/specs/2026-06-16-d2b-toolbar-phase1-design.md
+++ b/docs/superpowers/specs/2026-06-16-d2b-toolbar-phase1-design.md
@@ -156,6 +156,13 @@ mirrors the shipped vitals/chat re-drive exactly: dat `LayoutDesc` → `LayoutIm
- **Change:** in `CreateObject.TryParse`, capture the `IconId` (currently discarded at
`CreateObject.cs:516`) — and the underlay/overlay/effect ids if present in the same block —
onto the parsed object so `ItemRepository` stores them on `ItemInstance` (fields already exist).
+- **Planning delta (see the plan):** fact-gathering found this is wider than "just capture IconId."
+ acdream has NO `CreateObject`→`ItemRepository` wiring at all (the repo is populated only from
+ `PlayerDescription` with stub `ItemInstance`s), and `Parsed.Shortcuts` is parsed then discarded
+ in `GameEventWiring`. So the plan adds three small wiring pieces: capture IconId (Task 1), enrich
+ the repo from the `WorldSession.EntitySpawned` event (Tasks 2–3, `ItemRepository.EnrichItem`),
+ and persist the shortcut list (Task 4). The icon source is CONFIRMED to be `CreateObject` for
+ contained pack items (ACE `WorldObject_Networking.cs:79` writes IconId unconditionally).
- **Step 0 verification:** confirm against **ACE source** (`WorldObject.SerializeCreateObject`
/ the weenie property serialization) that a *contained* pack item's `CreateObject` actually
carries `IconId` (synthesis risk #3 — LIKELY, not yet byte-traced). Reading ACE is sufficient;