diff --git a/docs/superpowers/plans/2026-07-05-mp1a-content-extraction.md b/docs/superpowers/plans/2026-07-05-mp1a-content-extraction.md
new file mode 100644
index 00000000..79aab3c1
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-05-mp1a-content-extraction.md
@@ -0,0 +1,221 @@
+# MP1a — AcDream.Content Extraction 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:** Extract the GL-free CPU half of the WB mesh/texture pipeline out of `AcDream.App` into a new `AcDream.Content` assembly, so the MP1b bake tool can run it without an OpenGL context — with **zero behavior change** (the client keeps running the exact same code, relocated).
+
+**Architecture:** Mechanical move, not a rewrite. `ObjectMeshManager`'s `Prepare*` family (dat read → mesh extraction → inline texture decode → `ObjectMeshData`) moves verbatim into a new `MeshExtractor` class in `AcDream.Content`; `ObjectMeshManager` keeps its queue/worker/GL-upload lifecycle and delegates. The one GL-domain entanglement is `TextureAtlasManager.TextureKey` (a pure-data struct nested in a GL class) — it gets lifted out first. Spec: `docs/superpowers/specs/2026-07-05-modern-pipeline-design.md` §6.1 ("Extraction prerequisite"). MP1b (bake tool + pak) and MP1c (streaming cutover) are separate plans.
+
+**Tech Stack:** .NET 10 class library (no Silk.NET), `BCnEncoder.Net.ImageSharp` 1.1.2 + `SixLabors.ImageSharp` 3.1.12 (the inline DXT decode's packages), `AcDream.Core` project reference (TextureHelpers etc.).
+
+**Binding rules:**
+- **Verbatim bodies.** Method bodies move unchanged — same statements, same inline decode (do NOT "clean up" by switching to `SurfaceDecoder`; the two decoders are not proven bit-identical and byte-identity is MP1b's conformance foundation). Only mechanical edits allowed: namespace/using lines, `private` → `public` on moved members the old class must still reach, field references rehomed onto the new class.
+- **The compiler enumerates dependencies.** If moving a method drags in a type, move it if it's GL-free; if a dependency turns out to touch GL (`Silk.NET`, `OpenGLGraphicsDevice`, `GlobalMeshBuffer`, `TextureAtlasManager` beyond the lifted key, `ManagedGL*`), **STOP and report BLOCKED** — do not improvise a redesign.
+- **No divergence-register rows** — a pure relocation introduces no behavior deviation. Any observed behavior change is a bug in the move.
+- Verified boundary facts (2026-07-05, this worktree): the `Prepare*` region's only GL-domain reference is `TextureAtlasManager.TextureKey` (`ObjectMeshManager.cs:1286,1631,2236,2254`); first real GL call is `UploadGfxObjMeshData` at `:1833`. Line numbers drift — match by symbol.
+
+---
+
+## File map
+
+| File | Action | Responsibility |
+|---|---|---|
+| `src/AcDream.Content/AcDream.Content.csproj` | Create | GL-free content/asset assembly (net10.0) |
+| `src/AcDream.Content/TextureKey.cs` | Create (lift) | The atlas dedup key struct, moved out of `TextureAtlasManager` |
+| `src/AcDream.Content/ObjectMeshData.cs` | Create (move) | The boundary records: `ObjectMeshData`, `MeshBatchData`, `TextureBatchData`, `VertexPositionNormalTexture`, and sibling GL-free data records from `ObjectMeshManager.cs`'s data region |
+| `src/AcDream.Content/MeshExtractor.cs` | Create (move) | The `Prepare*` family + its private helpers/caches, verbatim |
+| `src/AcDream.Content/` (as compiler demands) | Create (move) | `IDatReaderWriter.cs`, `DatCollectionAdapter.cs`, `AcSurfaceMetadata.cs`, `AcSurfaceMetadataTable.cs`, `EdgeLineBuilder.cs`, `GeometryUtils.cs` — GL-free deps of the extractor (move only the ones the build actually requires) |
+| `src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs` | Modify | Keeps queue/worker/upload lifecycle; delegates mesh prep to `MeshExtractor`; loses the moved types/methods |
+| `src/AcDream.App/Rendering/Wb/TextureAtlasManager.cs` | Modify | Uses the lifted `TextureKey` |
+| `src/AcDream.App/AcDream.App.csproj` | Modify | Add `ProjectReference` to Content |
+| `AcDream.slnx` | Modify | Register the new project |
+| `docs/architecture/worldbuilder-inventory.md` | Modify | Record the relocation (Code Structure Rule 2 requires the inventory note for project-reference changes) |
+
+`AcDream.Core` is untouched. `AcDream.Content` references Core; App references Content; Core must NOT reference Content.
+
+---
+
+### Task 1: Create the AcDream.Content project
+
+**Files:**
+- Create: `src/AcDream.Content/AcDream.Content.csproj`
+- Modify: `AcDream.slnx`, `src/AcDream.App/AcDream.App.csproj`
+
+- [ ] **Step 1: Project file**
+
+```xml
+
+
+
+ net10.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+NOTE: `Chorizite.DatReaderWriter` flows transitively via AcDream.Core. If the moved code fails to resolve DRW types, add the explicit `PackageReference` pinned to the same version as Core (2.1.7) rather than bumping anything.
+
+- [ ] **Step 2: Register in `AcDream.slnx`** — add the project entry following the existing `` pattern (open the file, copy the shape).
+
+- [ ] **Step 3: App references Content** — in `src/AcDream.App/AcDream.App.csproj`, add alongside the existing ProjectReferences:
+
+```xml
+
+```
+
+- [ ] **Step 4: Build**
+
+Run: `dotnet build`
+Expected: green (empty project compiles, references resolve).
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/AcDream.Content/ AcDream.slnx src/AcDream.App/AcDream.App.csproj
+git commit -m "feat(pipeline): MP1a - AcDream.Content assembly scaffold"
+```
+
+---
+
+### Task 2: Lift TextureKey out of TextureAtlasManager
+
+**Files:**
+- Create: `src/AcDream.Content/TextureKey.cs`
+- Modify: `src/AcDream.App/Rendering/Wb/TextureAtlasManager.cs` (delete the nested struct, use the lifted one)
+- Modify: every `TextureAtlasManager.TextureKey` reference site (find with `grep -rn "TextureAtlasManager.TextureKey" src/`)
+
+- [ ] **Step 1: Move the struct.** Copy the nested `TextureKey` struct out of `TextureAtlasManager` VERBATIM (fields, equality/GetHashCode if present, doc comments) into:
+
+```csharp
+namespace AcDream.Content;
+
+// MP1a (2026-07-05): lifted verbatim out of TextureAtlasManager (GL class)
+// so the GL-free mesh-extraction path (MeshExtractor / ObjectMeshData) can
+// reference the atlas dedup key without a Silk.NET dependency. Field set,
+// equality semantics, and hash behavior are UNCHANGED.
+public struct TextureKey
+{
+ // ... the verbatim body from TextureAtlasManager.TextureKey ...
+}
+```
+
+(The plan intentionally does not restate the body — copy it exactly from the source; it is pure data: SurfaceId/PaletteId/Stippling/IsSolid per the boundary survey.)
+
+- [ ] **Step 2: Update all reference sites.** `TextureAtlasManager.TextureKey` → `TextureKey` with `using AcDream.Content;`. Keep a `using` alias only if a name collision appears (none expected).
+
+- [ ] **Step 3: Build + full test suite**
+
+Run: `dotnet build` then `dotnet test`
+Expected: green / all passing (this is a pure type relocation).
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add -A
+git commit -m "refactor(pipeline): MP1a - lift TextureKey out of the GL atlas class"
+```
+
+---
+
+### Task 3: Move the boundary data records
+
+**Files:**
+- Create: `src/AcDream.Content/ObjectMeshData.cs`
+- Modify: `src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs` (types removed), all reference sites (`grep -rn "ObjectMeshData\|VertexPositionNormalTexture\|TextureBatchData\|MeshBatchData" src/ tests/`)
+
+- [ ] **Step 1: Move these types from `ObjectMeshManager.cs`'s data region into `AcDream.Content` (namespace `AcDream.Content`), VERBATIM:** `VertexPositionNormalTexture` (the 32-byte GPU vertex — this is the pak layout target, do not alter layout/stride), `ObjectMeshData`, `MeshBatchData`, `TextureBatchData`, plus any small GL-free records the compiler then demands (e.g. setup-part / emitter-info records referenced by `ObjectMeshData` fields).
+
+ **Explicitly stays in App:** `ObjectRenderData` and `ObjectRenderBatch` (they hold `TextureAtlasManager Atlas` — a GL class) and anything else the compiler shows touching GL. If `ObjectMeshData` itself turns out to hold a GL-typed field the survey missed, STOP and report BLOCKED.
+
+- [ ] **Step 2: Update reference sites** (`using AcDream.Content;` in the App files + any tests).
+
+- [ ] **Step 3: Build + full test suite** — green / all passing.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add -A
+git commit -m "refactor(pipeline): MP1a - ObjectMeshData family moved to AcDream.Content"
+```
+
+---
+
+### Task 4: Extract MeshExtractor (the Prepare* family)
+
+**Files:**
+- Create: `src/AcDream.Content/MeshExtractor.cs`
+- Move as the compiler demands: `IDatReaderWriter.cs`, `DatCollectionAdapter.cs`, `AcSurfaceMetadata.cs`, `AcSurfaceMetadataTable.cs`, `EdgeLineBuilder.cs`, `GeometryUtils.cs` from `src/AcDream.App/Rendering/Wb/` → `src/AcDream.Content/` (verbatim, namespace change only)
+- Modify: `src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs`
+
+- [ ] **Step 1: Create the extractor class.** New class in Content:
+
+```csharp
+namespace AcDream.Content;
+
+///
+/// MP1a (2026-07-05): the GL-free CPU half of the former ObjectMeshManager —
+/// dat read → polygon walk → vertex/index build → inline texture decode →
+/// . Extracted VERBATIM so the MP1b bake tool
+/// and the live client run the SAME extraction code (byte-identical output
+/// is the pak conformance foundation). ObjectMeshManager (App) retains the
+/// queue/worker lifecycle and all GL upload; it delegates here.
+/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §6.1.
+///
+public sealed class MeshExtractor
+{
+ // Constructor takes the extraction dependencies the moved methods used
+ // as ObjectMeshManager fields — expected from the boundary survey:
+ // IDatReaderWriter dats, AcSurfaceMetadataTable surfaceMetadata
+ // plus the CPU-side caches that belong with extraction:
+ // the ThreadLocal, the decoded-texture cache, bounds cache.
+ // Let the compiler produce the exact list — add fields here, do not
+ // leave them behind in App if only Prepare* uses them.
+}
+```
+
+- [ ] **Step 2: Move the methods VERBATIM onto `MeshExtractor`, making the entry points `public`:** `PrepareMeshData`, `PrepareGfxObjMeshData`, `PrepareEnvCellMeshData`, `PrepareCellStructMeshData`, `PrepareSetupMeshData`, `PrepareCellStructEdgeLineData`, and the private helpers they call (`CollectParts`, `ComputeBounds`, `CollectDrawingBspPolygonIds`, `BuildPolygonIndices`, `BuildCellStructPolygonIndices`, `CollectEmittersFromScript`, plus whatever else the compiler names). Move the fields/caches only they use. Bodies unchanged.
+
+- [ ] **Step 3: Delegate from ObjectMeshManager.** It constructs one `MeshExtractor` in its constructor (passing its dat reader + metadata table) and every former `Prepare*` call site becomes `_extractor.Prepare*`. The staged-queue enqueue, worker pool, retry, and `Dispose` quiesce logic stay in `ObjectMeshManager` untouched (the MP0-investigated teardown discipline is a runtime concern; the bake tool will drive the extractor with its own simple loop in MP1b).
+
+- [ ] **Step 4: Move the support files** the compiler demands (list above), namespace `AcDream.Content`, bodies verbatim. `Building.cs` is NOT in scope (its GL-query fields keep it in App for now).
+
+- [ ] **Step 5: Build + FULL test suite**
+
+Run: `dotnet build` then `dotnet test`
+Expected: green / all passing. The full suite is the behavior gate — no test may change.
+
+- [ ] **Step 6: Update the inventory doc.** In `docs/architecture/worldbuilder-inventory.md`, add an MP1a note under the extracted-code section: the CPU mesh-extraction half now lives in `src/AcDream.Content/` (list the moved files), App retains GL upload/lifecycle, Core untouched, reason = MP1 bake tool needs GL-free extraction (spec §6.1).
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add -A
+git commit -m "refactor(pipeline): MP1a - MeshExtractor extracted to AcDream.Content (verbatim move)"
+```
+
+---
+
+### Task 5: Launch smoke (user-gated)
+
+- [ ] **Step 1:** `dotnet build -c Release` — green.
+- [ ] **Step 2:** Launch per the standard live-launch recipe (CLAUDE.md), user in-world for ~2 minutes: Holtburg buildings + an interior + a dungeon teleport. Acceptance: world renders identically — no missing/white/wrong geometry or textures (the #105-class signature would indicate the move broke a per-frame driver; per `feedback_extraction_perframe_drivers` the lifecycle stayed in App precisely to avoid this, so any anomaly = STOP and investigate the move).
+- [ ] **Step 3:** On user confirmation, update the roadmap Track MP table (MP1a slice shipped) and commit.
+
+```bash
+git add docs/plans/2026-04-11-roadmap.md
+git commit -m "docs(pipeline): MP1a shipped - Content extraction user-gated"
+```