diff --git a/docs/superpowers/plans/2026-06-25-ui-studio-previewer.md b/docs/superpowers/plans/2026-06-25-ui-studio-previewer.md
new file mode 100644
index 00000000..3fab83a5
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-25-ui-studio-previewer.md
@@ -0,0 +1,332 @@
+# acdream UI Studio 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:** Build a standalone Silk.NET dev tool that renders any acdream UI panel through the production render stack, with an ImGui click-to-inspect inspector, sample-data fixtures, the live 3-D doll, markup hot-reload + write-back, and doll-camera sliders.
+
+**Architecture:** A new `StudioWindow` (Silk GL app, launched via a `ui-studio` subcommand in `Program.cs`) builds its render stack through a new `RenderBootstrap` (a clean construction of just the subset the studio needs — GL, `DatCollection`, `TextureCache`, the WB mesh pipeline, `UiHost` — leaving `GameWindow` untouched). A `LayoutSource` loads a panel (dat `LayoutDesc` id or markup file) into a `UiElement` tree; a `FixtureProvider` populates it via the production controllers; a `StudioInspector` (ImGui) renders the tool chrome with the panel shown as an FBO image.
+
+**Tech Stack:** C# .NET 10, Silk.NET (Windowing/Input/OpenGL), ImGui.NET 1.91.6.1 + Silk.NET.OpenGL.Extensions.ImGui 2.23.0 (via the existing `AcDream.UI.ImGui` project), the existing `AcDream.App.UI` retained-mode tree + `LayoutImporter`/`DatWidgetFactory`, `DatReaderWriter`.
+
+**Spec:** `docs/superpowers/specs/2026-06-25-ui-studio-previewer-design.md`
+
+**Subagent note (project policy):** implementers are Sonnet and INHERIT CLAUDE.md; they read the cited code themselves. Tasks give the contract, the key code, the files to read, the test, the gate, and the commit message — not every line. GL/window tasks verify via a visual gate (the studio is itself the visual tool); pure-logic tasks (LayoutSource infos, FixtureProvider, MarkupWriteBack) use real unit tests.
+
+**Branch:** `claude/hopeful-maxwell-214a12`. Commit after every task. Full build (`dotnet build src/AcDream.App/AcDream.App.csproj`) green before any commit; relevant tests green.
+
+**Launch (for every visual gate):**
+```powershell
+$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
+dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug -- ui-studio "$env:USERPROFILE\Documents\Asheron's Call" --layout 0x2100006C
+```
+Do NOT auto-kill a running client; if a rebuild is locked, ask the user to close it ([[feedback_dont_kill_clients_before_launch]]).
+
+---
+
+## File structure
+
+New, all under `src/AcDream.App/`:
+- `Rendering/RenderBootstrap.cs` — `RenderStack Create(GL, RenderBootstrapOptions)`; the shared render-stack builder + the `RenderStack` record.
+- `Studio/StudioWindow.cs` — the Silk window + frame loop + owns the stack/inspector/sources.
+- `Studio/StudioOptions.cs` — parsed `ui-studio` args (layout id | markup path | dat dir).
+- `Studio/LayoutSource.cs` — load/reload a panel from a dat id or a markup file.
+- `Studio/FixtureProvider.cs` — sample data + per-layout controller binding.
+- `Studio/SampleData.cs` — the canned items / vitals / creature fixture constants.
+- `Studio/StudioInspector.cs` — the ImGui chrome (canvas/tree/properties/render-config).
+- `Studio/PanelFbo.cs` — render-to-texture target for the previewed panel (mirrors `PaperdollViewportRenderer`'s FBO).
+- `Studio/MarkupWriteBack.cs` — targeted in-place markup attribute edits.
+- `Studio/HotReloadWatcher.cs` — debounced `FileSystemWatcher`.
+- Modify: `src/AcDream.App/Program.cs` — `ui-studio` subcommand dispatch.
+
+Tests under `tests/AcDream.App.Tests/Studio/`:
+- `LayoutSourceTests.cs`, `FixtureProviderTests.cs`, `MarkupWriteBackTests.cs`.
+
+---
+
+## Task 1: RenderBootstrap (shared render stack)
+
+**Files:**
+- Create: `src/AcDream.App/Rendering/RenderBootstrap.cs`
+- Read first: `src/AcDream.App/Rendering/GameWindow.cs:1026-1815,2313-2408` (the construction order), `src/AcDream.App/UI/UiHost.cs`, `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (ctor), `src/AcDream.App/UI/Layout/UiDatFont.cs`.
+
+**Contract:**
+```csharp
+namespace AcDream.App.Rendering;
+
+public sealed record RenderStack(
+ GL Gl,
+ DatReaderWriter.DatCollection Dats,
+ string ShaderDir,
+ BindlessSupport Bindless,
+ TextureCache TextureCache,
+ Shader MeshShader,
+ Wb.WbMeshAdapter MeshAdapter,
+ Wb.EntitySpawnAdapter EntitySpawnAdapter,
+ Wb.WbDrawDispatcher DrawDispatcher,
+ SceneLightingUboBinding LightingUbo,
+ AcDream.App.UI.UiHost UiHost,
+ AcDream.App.UI.Layout.UiDatFont? VitalsDatFont)
+{
+ /// sprite id (0x06xxxxxx) → (GL handle, width, height) for the importer's resolve func.
+ public (uint, int, int) ResolveChrome(uint spriteId)
+ => TextureCache.GetOrUploadRenderSurface(spriteId, out var w, out var h) is var h2
+ ? (h2, w, h) : (0u, 0, 0); // match GameWindow.ResolveChrome — read its exact body first
+}
+
+public sealed record RenderBootstrapOptions(QualitySettings Quality);
+
+public static class RenderBootstrap
+{
+ /// Build the subset of the production render stack the UI Studio needs — same classes,
+ /// same construction order as GameWindow.OnLoad, minus terrain/sky/physics/streaming. Throws
+ /// NotSupportedException if bindless/draw-params are missing (same as the game).
+ public static RenderStack Create(GL gl, DatReaderWriter.DatCollection dats, RenderBootstrapOptions opts);
+}
+```
+
+- [ ] **Step 1:** Read the cited GameWindow construction lines and `ResolveChrome` (grep it) so the order + the exact `GetOrUploadRenderSurface`/sequencer-factory signatures are copied, not guessed.
+- [ ] **Step 2:** Write `RenderBootstrap.Create` constructing, in order: `BindlessSupport.TryCreate` (throw if absent/no draw-params, copy the GameWindow message), `shadersDir = Path.Combine(AppContext.BaseDirectory,"Rendering","Shaders")`, `SceneLightingUboBinding(gl)`, `MeshShader = new Shader(gl, mesh_modern.vert/.frag)`, `TextureCache(gl, dats, bindless)`, `WbMeshAdapter(gl, dats, NullLogger.Instance)`, the `SequencerFactory` lambda (copy from GameWindow:2335-2361, capturing `dats` + an `AnimLoader`), `EntitySpawnAdapter(textureCache, sequencerFactory, meshAdapter)`, `WbDrawDispatcher(gl, meshShader, textureCache, meshAdapter, entitySpawnAdapter, bindless, new )` with `AlphaToCoverage = opts.Quality.AlphaToCoverage`, the vitals dat font (copy GameWindow's load of `0x40000000` via `UiDatFont`), and `UiHost(gl, shadersDir, defaultFont)`. Return the `RenderStack`.
+- [ ] **Step 3:** Build: `dotnet build src/AcDream.App/AcDream.App.csproj -c Debug`. Expected: green (no test yet — GL construction is verified by Task 2's boot gate).
+- [ ] **Step 4:** Commit.
+
+**Gate:** Build green. (Functional gate is Task 2.)
+
+**Commit:** `feat(studio): RenderBootstrap — shared render stack for the UI previewer`
+
+---
+
+## Task 2: StudioWindow + LayoutSource (dat id) + Program dispatch
+
+**Files:**
+- Create: `src/AcDream.App/Studio/StudioOptions.cs`, `src/AcDream.App/Studio/LayoutSource.cs`, `src/AcDream.App/Studio/StudioWindow.cs`
+- Modify: `src/AcDream.App/Program.cs` (before `new GameWindow`)
+- Test: `tests/AcDream.App.Tests/Studio/LayoutSourceTests.cs`
+- Read first: `src/AcDream.App/UI/Layout/LayoutImporter.cs:57-190`, `src/AcDream.App/Rendering/GameWindow.cs:972-1017` (window+Run), `src/AcDream.App/UI/UiHost.cs:65-96` (Draw/WireMouse/WireKeyboard).
+
+**`StudioOptions` contract:**
+```csharp
+namespace AcDream.App.Studio;
+public sealed record StudioOptions(string DatDir, uint? LayoutId, string? MarkupPath)
+{
+ public static StudioOptions Parse(string[] args); // args AFTER the "ui-studio" token
+}
+```
+Parse: positional dat dir (or `ACDREAM_DAT_DIR`), `--layout 0xNNNN` (hex), `--markup `. Default layout when neither given: `0x2100006Cu` (vitals).
+
+**`LayoutSource` contract:**
+```csharp
+public enum LayoutSourceKind { DatLayout, Markup }
+public sealed class LayoutSource
+{
+ public LayoutSource(DatCollection dats, Func resolve, UiDatFont? datFont);
+ public LayoutSourceKind Kind { get; }
+ public uint? LayoutId { get; }
+ public string? MarkupPath { get; }
+ public string? LastError { get; private set; }
+ /// Imported tree, or null on error (LastError set). Dat → LayoutImporter.Import; markup → MarkupDocument.Build with an empty binding.
+ public ImportedLayout? CurrentLayout { get; private set; }
+ public UiElement? Load(StudioOptions opts); // sets Kind/LayoutId/MarkupPath + CurrentLayout
+ public UiElement? Reload(); // re-run the current source; markup re-reads the file
+}
+```
+
+- [ ] **Step 1 (test, GL-free):** `LayoutSourceTests.LoadsDatLayout_byId`. Open real dats via `ConformanceDats.ResolveDatDir()` (skip if null), build a `LayoutSource` with a STUB resolve `_ => (1u,1,1)` + null font, `Load(new StudioOptions(dir, 0x2100006Cu, null))`, assert `CurrentLayout` non-null and `FindElement(0x2100006Cu) != null` and `Kind == DatLayout`. (Use `LayoutImporter.ImportInfos` is GL-free; `Import`'s resolve is only called for sprites — the stub is fine for tree-shape.)
+- [ ] **Step 2:** Run it — expect FAIL (LayoutSource missing). `dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter LayoutSourceTests`.
+- [ ] **Step 3:** Implement `StudioOptions.Parse` + `LayoutSource` (dat path only; markup path returns LastError "markup unsupported" for now — wired in Task 6). Dat path = `LayoutImporter.Import(dats, id, resolve, datFont)`.
+- [ ] **Step 4:** Run the test — expect PASS.
+- [ ] **Step 5:** Implement `StudioWindow`: mirror `GameWindow.Run()` window creation (1280×720, GL 4.3 Core, stencil 8). On `Load`: `gl = GL.GetApi(window)`, `input = window.CreateInput()`, `dats = new DatCollection(opts.DatDir, DatAccessType.Read)`, `stack = RenderBootstrap.Create(gl, dats, new(QualitySettings...))`, wire `stack.UiHost.WireMouse/WireKeyboard` for each mouse/keyboard, `source = new LayoutSource(dats, stack.ResolveChrome, stack.VitalsDatFont)`, `root = source.Load(opts)`, and if non-null add it to `stack.UiHost.Root` (use the same add-child API GameWindow uses — grep `_uiHost.Root` adds). On `Render`: `gl.ClearColor` mid-grey, `Clear`, `stack.UiHost.Tick(dt)` + `stack.UiHost.Draw(new Vector2(window.Size.X, window.Size.Y))`. On `Closing`: dispose.
+- [ ] **Step 6:** `Program.cs`: before `new GameWindow`, add `if (args.Length >= 1 && args[0] == "ui-studio") { var so = AcDream.App.Studio.StudioOptions.Parse(args[1..]); using var sw = new AcDream.App.Studio.StudioWindow(so); sw.Run(); return 0; }`.
+- [ ] **Step 7:** Build green, then VISUAL GATE (launch command above). Expect: a window showing the vitals panel chrome (three empty bars + frame) rendered by the real UiHost.
+- [ ] **Step 8:** Commit.
+
+**Gate (visual):** `ui-studio --layout 0x2100006C` opens a window showing the vitals panel.
+
+**Commit:** `feat(studio): StudioWindow + LayoutSource — render a dat panel standalone`
+
+---
+
+## Task 3: StudioInspector (ImGui) + canvas FBO + tree + props + click-to-inspect
+
+**Files:**
+- Create: `src/AcDream.App/Studio/PanelFbo.cs`, `src/AcDream.App/Studio/StudioInspector.cs`
+- Modify: `StudioWindow.cs` (init ImGui, render panel→FBO, draw inspector)
+- Read first: `src/AcDream.UI.ImGui/ImGuiBootstrapper.cs:35-46`, `src/AcDream.App/Rendering/PaperdollViewportRenderer.cs` (the FBO RTT pattern + `GLStateScope`), `src/AcDream.App/UI/UiRoot.cs` (grep `HitTestTopDown`), `src/AcDream.App/UI/UiElement.cs` (Id/Type/Left/Top/Width/Height/Anchors/ZOrder/Children accessors).
+
+**`PanelFbo`:** mirror `PaperdollViewportRenderer`'s FBO (RGBA8 + Depth24Stencil8, lazy resize). `uint Render(int w, int h, UiHost host)` = bind FBO, viewport, clear transparent, `host.Draw(new Vector2(w,h))` inside a `GLStateScope`, return color tex. Reuse the exact FBO setup from `PaperdollViewportRenderer.EnsureFramebuffer`.
+
+**`StudioInspector`:** ImGui windows, drawn between `BeginFrame`/`Render`:
+```csharp
+public sealed class StudioInspector
+{
+ public UiElement? Selected { get; set; }
+ // Canvas window: ImGui.Image(panelTex) sized to the panel; capture click → return panel-local (x,y)
+ public (int x,int y)? DrawCanvas(nint panelTex, int w, int h);
+ public void DrawTree(UiElement root); // recursive ImGui.TreeNode; click sets Selected
+ public void DrawProperties(); // Selected's Id/Type/Rect/Anchors/ZOrder/sprites (read-only here)
+}
+```
+Click-to-inspect: when `DrawCanvas` returns a coord, call `root` `HitTestTopDown(x,y)` (grep its exact signature/return) → set `Selected`.
+
+- [ ] **Step 1:** Implement `PanelFbo` (copy `PaperdollViewportRenderer` FBO lifecycle; swap the doll draw for `host.Draw`).
+- [ ] **Step 2:** `StudioWindow.Load`: construct `new ImGuiBootstrapper(gl, window, input)` (no `DevToolsEnabled` gate — the studio is always devtools) + `new PanelFbo(gl)` + `new StudioInspector()`.
+- [ ] **Step 3:** `StudioWindow.Render`: `panelTex = panelFbo.Render(w,h, stack.UiHost)`; `imgui.BeginFrame(dt)`; `var click = inspector.DrawCanvas((nint)panelTex, w, h); if (click is {} c) inspector.Selected = root.HitTestTopDown(c.x, c.y); inspector.DrawTree(root); inspector.DrawProperties();`; `imgui.Render()`.
+- [ ] **Step 4:** Implement `StudioInspector` windows (ImGui.NET: `ImGui.Begin`, `ImGui.Image`, `ImGui.TreeNodeEx`, `ImGui.Text`). Tree recurses `element.Children`; each node label = `$"{name} 0x{Id:X8} [{Type}]"`. Properties: Id, Type, Rect `(Left,Top,Width,Height)`, Anchors, ZOrder, and each state sprite id (grep how `UiDatElement`/`UiButton` expose their sprite ids; if not trivially exposed, list "—" for v1 and note follow-up).
+- [ ] **Step 5:** Build green, VISUAL GATE: window shows a Canvas pane (the panel) + a Tree pane + a Properties pane; clicking the panel or a tree node selects an element and shows its id/rect.
+- [ ] **Step 6:** Commit.
+
+**Gate (visual):** click an element in the canvas or tree → its id / Type / rect appear in Properties.
+
+**Commit:** `feat(studio): ImGui inspector — canvas FBO, element tree, props, click-to-inspect`
+
+---
+
+## Task 4: FixtureProvider — populate 2-D panels with sample data
+
+**Files:**
+- Create: `src/AcDream.App/Studio/SampleData.cs`, `src/AcDream.App/Studio/FixtureProvider.cs`
+- Modify: `StudioWindow.cs` (call `FixtureProvider.Populate` after `Load`)
+- Test: `tests/AcDream.App.Tests/Studio/FixtureProviderTests.cs`
+- Read first: `src/AcDream.Core/Items/ClientObjectTable.cs:41-420` (`AddOrUpdate`/`Get`/`GetContents`/`ClientObject` fields), the four controllers' `Bind` (signatures in the plan header research), `src/AcDream.Core/Items/ItemType.cs` + `EquipMask`.
+
+**`SampleData`:** static factory building a `ClientObjectTable` seeded with: a player object (guid `0xPLAYER`, `ItemsCapacity=102`, `ContainersCapacity=7`), ~6 loose items in the main pack (varied `Type`/`IconId` — pick real icon ids from existing tests or a known wcid; `ContainerId = player`, gapless `ContainerSlot`), ~2 side bags (`Type=Container`, `ItemsCapacity>0`), and a few equipped items (`CurrentlyEquippedLocation` set to Head/Chest/etc). Plus sample vitals (`health=0.8f, stam=0.6f, mana=0.9f` + text). Constants only — no wire.
+
+**`FixtureProvider.Populate(uint layoutId, ImportedLayout layout, RenderStack stack)`** switch on `layoutId`:
+- `0x2100006C` → `VitalsController.Bind(layout, ()=>0.8f, ()=>0.6f, ()=>0.9f, ()=>"80/100", ()=>"60/100", ()=>"90/100")`.
+- `0x21000016` → `ToolbarController.Bind(layout, sampleTable, ()=>PLAYER, IconIds, _=>{}, ...digits from GameWindow's load...)`.
+- `0x21000023` → `InventoryController.Bind(layout, sampleTable, ()=>PLAYER, IconIds, ()=>100, stack.VitalsDatFont, contents/sideBag/mainPack sprites, ...)`.
+- default → no-op (structural preview).
+`IconIds` = reuse GameWindow's `iconIds` lambda (grep it — `IconComposer`-backed); if heavy, a stub returning the passed base id is acceptable for v1 (note it).
+
+- [ ] **Step 1 (test):** `FixtureProviderTests.SampleTable_hasPackContents`. Build `SampleData.BuildObjectTable()`, assert `table.GetContents(PLAYER).Count >= 6` and a known item's `Type`/`IconId` set.
+- [ ] **Step 2:** Run — FAIL (missing).
+- [ ] **Step 3:** Implement `SampleData` + the table seeding.
+- [ ] **Step 4:** Run — PASS.
+- [ ] **Step 5:** Implement `FixtureProvider.Populate`; call it from `StudioWindow` after `source.Load`.
+- [ ] **Step 6:** Build green, VISUAL GATE: `--layout 0x21000023` shows the inventory with sample items in the grid + a non-zero burden bar; `--layout 0x21000016` shows toolbar slots populated; `--layout 0x2100006C` shows filled vital bars.
+- [ ] **Step 7:** Commit.
+
+**Gate (visual):** inventory / toolbar / vitals panels render POPULATED (sample items, filled bars).
+
+**Commit:** `feat(studio): FixtureProvider — sample data populates the 2-D panels`
+
+---
+
+## Task 5: Live doll in the paperdoll viewport
+
+**Files:**
+- Modify: `FixtureProvider.cs` (build a sample creature entity for `0x21000023`/paperdoll), `StudioWindow.cs` (wire `PaperdollViewportRenderer` + the pre-UI doll render, mirroring GameWindow:9148-9160)
+- Read first: `src/AcDream.App/Rendering/GameWindow.cs:3786-3897` (`RefreshPaperdollDoll`/`ApplyPaperdollPose`), `9148-9160` (the pre-UI doll hook), `src/AcDream.App/Rendering/DollEntityBuilder.cs` (`Build`), `src/AcDream.App/Rendering/PaperdollViewportRenderer.cs` (ctor + `SetDoll`/`Render`), `EntitySpawnAdapter.OnCreate`.
+
+**Sample creature:** a base human Setup id (grep the test character / a known humanoid Setup `0x0200....`; or reuse the player Setup constant if one exists in tests). Build a `WorldEntity { SourceGfxObjOrSetupId = setupId, MeshRefs = , ... }`, run it through `stack.EntitySpawnAdapter.OnCreate` to resolve MeshRefs (same path GameWindow uses), then `DollEntityBuilder.Build` + `ApplyPaperdollPose`-equivalent. Keep a small `StudioDoll` helper if this grows.
+
+- [ ] **Step 1:** In `StudioWindow.Load`, construct `new PaperdollViewportRenderer(gl, stack.DrawDispatcher, stack.LightingUbo)`.
+- [ ] **Step 2:** When the layout is the inventory/paperdoll, have `FixtureProvider` build the sample creature, call `RefreshPaperdollDoll`-equivalent (clone → `ApplyPaperdollPose`) → `paperdollRenderer.SetDoll(doll)`; expose the doll-viewport widget (FindElement `0x100001D5`).
+- [ ] **Step 3:** In `StudioWindow.Render`, BEFORE the panel FBO draw, render the doll into the viewport widget's texture exactly as GameWindow:9156-9159 (`widget.TextureHandle = paperdollRenderer.Render(w,h)`), gated on the viewport widget being present + visible.
+- [ ] **Step 4:** Build green, VISUAL GATE: `--layout 0x21000023` shows the paperdoll with a DRESSED 3-D doll in the held pose (reuses the camera/heading/pose shipped in `8fa66c2`).
+- [ ] **Step 5:** Commit.
+
+**Gate (visual):** the paperdoll viewport shows the sample creature as a posed 3-D doll.
+
+**Commit:** `feat(studio): live 3-D doll in the paperdoll preview`
+
+---
+
+## Task 6: Markup source + hot-reload
+
+**Files:**
+- Create: `src/AcDream.App/Studio/HotReloadWatcher.cs`
+- Modify: `LayoutSource.cs` (markup path via `MarkupDocument.Build`), `StudioWindow.cs` (watch + reload), `StudioOptions` already parses `--markup`
+- Read first: `src/AcDream.App/UI/MarkupDocument.cs:21-82`, an existing first-party markup file (grep `assets/*.xml`, e.g. the retired `vitals.xml` in git history or a current plugin panel) for the schema.
+- Test: extend `LayoutSourceTests`.
+
+**LayoutSource markup path:** `Kind=Markup`; `Load` reads the file → `MarkupDocument.Build(xml, EmptyBinding, resolve, null)` → wrap its `UiNineSlicePanel` as the root; `Reload` re-reads the file. `EmptyBinding` = `new object()` (no `{Binding}` resolution needed for preview; unresolved bindings render literally or empty — acceptable).
+
+**HotReloadWatcher:**
+```csharp
+public sealed class HotReloadWatcher : IDisposable
+{
+ public HotReloadWatcher(string filePath, Action onChanged); // debounce ~150ms; marshal onChanged via a thread-safe flag the window polls
+}
+```
+The watcher sets a `volatile bool _dirty`; `StudioWindow.Render` checks it and calls `source.Reload()` + `FixtureProvider.Populate` + re-add to the UiRoot on the GL thread (FileSystemWatcher fires on a worker thread — never touch GL off-thread).
+
+- [ ] **Step 1 (test):** `LayoutSourceTests.LoadsMarkupFile`. Write a temp `.xml` with a minimal `UiNineSlicePanel`, `Load(new StudioOptions(dir, null, path))`, assert non-null root + `Kind==Markup`.
+- [ ] **Step 2:** Run — FAIL.
+- [ ] **Step 3:** Implement the markup path in `LayoutSource` + `HotReloadWatcher`.
+- [ ] **Step 4:** Run — PASS.
+- [ ] **Step 5:** Wire the watcher in `StudioWindow` (poll `_dirty` in Render; reload on the GL thread).
+- [ ] **Step 6:** Build green, VISUAL GATE: launch `--markup `; edit the file in an editor + save → the studio reloads the panel within ~1s.
+- [ ] **Step 7:** Commit.
+
+**Gate (visual):** edit a markup file externally → studio re-renders it live.
+
+**Commit:** `feat(studio): markup source + hot-reload`
+
+---
+
+## Task 7: Editable markup props + write-back
+
+**Files:**
+- Create: `src/AcDream.App/Studio/MarkupWriteBack.cs`
+- Modify: `StudioInspector.cs` (editable rect/anchor fields when `Kind==Markup`), `StudioWindow.cs` (apply edit live + persist)
+- Test: `tests/AcDream.App.Tests/Studio/MarkupWriteBackTests.cs`
+
+**`MarkupWriteBack`:** targeted in-place attribute edits — NOT a full re-serialize (preserve formatting/comments):
+```csharp
+public static class MarkupWriteBack
+{
+ /// In the markup text, find the element whose id attribute == elementId and set attr=value
+ /// (add the attribute if absent). Returns the new text. Leaves all other bytes untouched.
+ public static string SetAttribute(string markup, uint elementId, string attr, string value);
+}
+```
+Implement by locating the element's opening tag (match the id attribute literal `Id="0x...."` — grep the markup schema for the exact id attribute name), then regex-replace just that attribute within that tag span.
+
+- [ ] **Step 1 (test):** `MarkupWriteBackTests.SetAttribute_updatesInPlace`. Given a 2-element markup string, `SetAttribute(m, id, "Width", "200")` → re-parse, assert the target's Width==200 AND the sibling element + any comment are byte-identical.
+- [ ] **Step 2:** Run — FAIL.
+- [ ] **Step 3:** Implement `MarkupWriteBack.SetAttribute`.
+- [ ] **Step 4:** Run — PASS (add an absent-attribute case + a not-found case returning the input unchanged).
+- [ ] **Step 5:** `StudioInspector.DrawProperties`: when `Kind==Markup`, render rect (`Left/Top/Width/Height`) + anchors as ImGui `InputInt`/combo; on change, set the live `UiElement` field AND call back to the window to `MarkupWriteBack.SetAttribute(...)` + write the file (the watcher then reloads, or skip the watcher for self-writes via a guard flag).
+- [ ] **Step 6:** Build green, VISUAL GATE: select an element in a markup panel, change its Width in Properties → the panel updates AND the file on disk shows the new value.
+- [ ] **Step 7:** Commit.
+
+**Gate (visual):** edit a rect in the inspector → live panel updates + the markup file is rewritten.
+
+**Commit:** `feat(studio): editable markup props with in-place write-back`
+
+---
+
+## Task 8: Render-config sliders (doll camera)
+
+**Files:**
+- Modify: `src/AcDream.App/Rendering/DollCamera.cs` (make Eye/Target/Fov instance-settable — currently `static readonly`), `StudioInspector.cs` (sliders), `PaperdollViewportRenderer.cs` (expose its `DollCamera` or accept overrides)
+- Read first: `DollCamera.cs` (current static fields), `PaperdollViewportRenderer.cs:35` (`private readonly DollCamera _camera = new();`).
+
+**DollCamera change:** convert the `static readonly` Eye/Target/Up/FovRadians to instance properties with the retail defaults as initializers (keep the decomp citations). This is a safe, behavior-preserving change (same defaults) — verify the existing `DollCameraTests` still pass (they read `cam.View`).
+
+**Inspector:** a "Render config" ImGui window shown when a doll viewport is selected: `SliderFloat3("eye", ...)`, `SliderFloat("fov", ...)`, `SliderFloat("heading", ...)` bound to the `PaperdollViewportRenderer`'s camera (expose it) + the doll's heading. A "Copy to clipboard" button emits the C# literals.
+
+- [ ] **Step 1:** Make `DollCamera` Eye/Target/Up/FovRadians instance properties (defaults = current retail values). Build + run `DollCameraTests` — expect PASS (defaults unchanged).
+- [ ] **Step 2:** Expose the camera from `PaperdollViewportRenderer` (a `public DollCamera Camera => _camera;`).
+- [ ] **Step 3:** `StudioInspector`: render the Render-config window with sliders mutating `renderer.Camera.{Eye,FovRadians}` + the doll heading; a copy button.
+- [ ] **Step 4:** Build green, VISUAL GATE: select the doll, drag the eye/FOV sliders → the doll re-frames live; copy emits the literals.
+- [ ] **Step 5:** Commit.
+
+**Gate (visual):** dragging the camera sliders re-frames the doll in real time.
+
+**Commit:** `feat(studio): render-config sliders for the doll camera`
+
+---
+
+## Wrap-up (after Task 8)
+
+- [ ] Update `claude-memory/project_d2b_retail_ui.md` (or a new `project_ui_studio.md`) with the studio's architecture + the launch command + DO-NOT-RETRY notes; add a `MEMORY.md` pointer.
+- [ ] Update the roadmap: note the UI Studio tool shipped (it's tooling, not a milestone phase — a one-line ledger entry).
+- [ ] Full suite green; final commit.
+
+## Self-review notes
+
+- **Spec coverage:** both sources (Tasks 2 dat / 6 markup) ✓; full fixtures (Task 4 + 5) ✓; inspector read (3) + editable markup (7) + render sliders (8) ✓; hot-reload (6) ✓; live doll (5) ✓; RenderBootstrap (1) ✓; ImGui chrome (3) ✓.
+- **GL-untestable tasks** (1,2-window,3,5,6-window,8) use visual gates by design — the studio is a visual tool; pure-logic units (LayoutSource infos, FixtureProvider, MarkupWriteBack) have unit tests.
+- **Type consistency:** `RenderStack`/`ImportedLayout`/`StudioOptions`/`LayoutSource`/`ClientObjectTable` member names match the research reference; `ResolveChrome` body must be copied from GameWindow (flagged in Task 1 Step 1).
+- **Risk:** Task 1 builds NEW code (no GameWindow edit) → zero game-regression risk, the de-risk decision from the spec's open risk. Task 5's sample Setup id is the one unknown literal — Task 5 Step reads the test fixtures to find a real humanoid Setup before coding.