diff --git a/docs/integration/REDMINE-INTEGRATION-PLAN.md b/docs/integration/REDMINE-INTEGRATION-PLAN.md
new file mode 100644
index 0000000..0a86b3f
--- /dev/null
+++ b/docs/integration/REDMINE-INTEGRATION-PLAN.md
@@ -0,0 +1,413 @@
+# Redmine Integration Plan — width × depth, parallel-scopable
+
+> **Reality check, calcified.** The Northstar plan (`CLASSVIEW-MATERIALIZATION-PLAN.md`)
+> built the substrate. The Redmine harvest (`REDMINE-QUERY-HARVEST.md`)
+> proved 17 years of Redmine GUI evolution maps onto our askama kit
+> 1:1. This doc is the **wiring plan** — how to deliver an actually-running
+> Redmine-shaped web app on top of that substrate, scoped so multiple
+> contributors / sessions can work in parallel without stepping on each
+> other.
+
+## 0. TL;DR — the picture
+
+```text
+┌─────────────────────────────────────── DONE (the foundation) ───────────────────────────────────────┐
+│ │
+│ ogar-vocab ogar-render-askama op-codegen-projection UnifiedBridge
│
+│ (codebook + (HtmlListView / (Class → SurrealQL DDL (registry + g_lock, │
+│ Class fns + HtmlDetailView / via askama; codebook-aware │
+│ ports::PortSpec) HtmlForm + cells + byte-identical pinned) entity() override) │
+│ formatters) │
+│ │
+└───────────────────────────────────────────────┬───────────────────────────────────────────────────────┘
+ │
+ ▼ this PR's scope
+┌────────────────────────────────────── TODO (the wiring plan) ────────────────────────────────────────┐
+│ │
+│ rm-server → rm-handlers → ogar_render_askama::render_* → HTML to browser │
+│ ↑ ↑ │
+│ │ └─ Redmine 17-yr UX inherited │
+│ ▼ for free (Query/QueryColumn │
+│ rm-store → RenderColumn/ColumnKind) │
+│ (SurrealDB │
+│ via from_class) rm-auth (session + RBAC) │
+│ │
+└───────────────────────────────────────────────────────────────────────────────────────────────────────┘
+```
+
+**The bet:** per-resource handler code is **mechanical** because the
+render kit takes `(canonical Class, Vec) → HTML` and Redmine's
+17 years of UX learnings are already inside the kit as data
+(`ColumnKind`, default formatters, sort/group/total flags). Adding a new
+resource page = a `for col in default_columns(class)` loop, not a new
+template.
+
+## 1. The four axes — orthogonal dimensions of progress
+
+Every workstream below positions itself on these four axes. Mixing them
+is how parallel scoping works; conflating them is how scope creeps.
+
+| Axis | What it varies | Where progress shows up |
+|---|---|---|
+| **Width** | how many canonical concepts have functional pages | URL count: `/issues`, `/projects`, `/time_entries`, … |
+| **Depth** | how complete one concept's experience is | per-resource feature: list → +filter → +sort → +CSV → +saved-query → +bulk-edit |
+| **GUI** | the visual layer (CSS, JS, theming, layout chrome) | what the page looks like, with NO behaviour change |
+| **Templates** | the askama kit itself (new `ColumnKind`s, new formatters, new `InputKind`s) | OGAR-side; cross-port (every consumer benefits) |
+| **Wiring** | the glue: routes, store, middleware, auth, sessions | `rm-server` scaffolding, then bound to handlers |
+
+**Width × Depth = the resource × feature matrix.** Filling a row = one
+resource gets every feature. Filling a column = every resource gets one
+feature. The plan below makes both shapes parallel-friendly.
+
+## 2. Phase 0 — Foundation (sequential gate, ~1 week, 1 stream)
+
+The foundation is the only **sequential** part. Everything else fans out
+parallel after this lands.
+
+### W0.1 — `rm-server` scaffold
+- New crate in `redmine-rs/crates/rm-server`.
+- Deps: `axum`, `askama`, `askama_axum`, `tokio`, `tower-http` (cors,
+ trace, compression), `tower-cookies` (sessions).
+- One main.rs: `serve()` fn that wires the router, middleware stack
+ (trace → compression → cookies → CSRF → auth), and static-file fallback.
+- Hello-world handler at `/` that renders an empty `HtmlListView` for
+ `class_ids::PROJECT` — proves the askama_axum bridge works.
+- **DoD:** `cargo run -p rm-server`, visit `localhost:3000/`, see a page.
+
+### W0.2 — `rm-store` substrate
+- New crate `redmine-rs/crates/rm-store`.
+- Connect to SurrealDB (in-memory for dev, file-backed for prod).
+- Apply the schema emitted by
+ `op_codegen_projection::render_classes_schema(&ogar_canonical_classes())`
+ at startup. (The schema generator is already a sibling crate; we just
+ drive it.)
+- `CRUD` trait: `find_one`, `find_all`, `find_by_filter`, `insert`,
+ `update`, `delete`. Generic over a `T: PortSpec`-derived row type.
+- Seed fixture: at minimum 3 Projects, 10 Issues, 5 TimeEntries —
+ enough for the first list page to render with realistic content.
+- **DoD:** `rm_store::Store::open()` returns a connected instance with
+ schema applied; integration test inserts an Issue, reads it back.
+
+### W0.3 — `rm-auth` core
+- New crate `redmine-rs/crates/rm-auth`.
+- Session middleware over signed cookies (HS256 or similar).
+- `Login` form + handler (the only handler in this crate).
+- `current_user(req)` extractor.
+- **Defer:** RBAC enforcement; just expose the User, downstream
+ handlers gate later (see D3).
+- **DoD:** `POST /login` with seed-user credentials sets session cookie;
+ `GET /me` returns the username.
+
+**Dependency:** W0.1 (server) before W0.2 (store can register routes for
+seed-import endpoint), before W0.3 (auth needs the server's
+middleware-builder API). Single contributor, sequential, ~1 week total.
+
+## 3. Phase 1 — Width pass (parallel after Phase 0, N streams)
+
+Every canonical concept gets a **list page** and a **detail page**.
+Forms come in Phase 2 (depth pass). This phase is breadth-first: ship
+26 concepts at depth-1 before any single concept hits depth-5.
+
+### The width track — one stream per resource group
+
+| Track | Resources | Owner-shaped | Effort/track |
+|---|---|---|---|
+| **W1 — Work item core** | Issue (`project_work_item`), Journal | hot-path UX | 5 days |
+| **W2 — Project core** | Project, Version, EnabledModule | container | 4 days |
+| **W3 — Time tracking** | TimeEntry, Activity (via PortSpec extension) | reports | 4 days |
+| **W4 — Actors & access** | User, Member (Membership), Role, MemberRole, Watcher | admin | 4 days |
+| **W5 — Taxonomy** | Tracker, IssueStatus, IssuePriority, CustomField, CustomValue | admin | 4 days |
+| **W6 — Comms** | Comment, News, Message, Board (Forum), WikiPage, Attachment | content | 5 days |
+| **W7 — SCM-light** | Repository, Changeset (read-only; no Git driver yet) | dev-facing | 3 days |
+| **W8 — Saved views & rels** | Query, IssueRelation | power-user | 3 days |
+
+**Shape of one width track** (this is what makes them parallelizable):
+
+```rust
+// rm-handlers/src/issue.rs — pattern repeats per resource
+pub async fn list(
+ State(store): State>,
+ Query(q): Query,
+) -> Result {
+ let class = ogar_vocab::project_work_item();
+ let cols = default_columns_for(&class); // ← from ColumnKind defaults
+ let rows = store.find_issues(&q).await?;
+ let row_sources = rows.iter().map(|r| build_row(r, &cols)).collect::>();
+ ogar_render_askama::render_list(
+ "Issues", 0x0102, "project_work_item",
+ &cols, &[], &row_sources,
+ )
+}
+
+pub async fn detail(
+ State(store): State>,
+ Path(id): Path,
+) -> Result {
+ let issue = store.find_issue(id).await?.ok_or(NotFound)?;
+ let class = ogar_vocab::project_work_item();
+ let cols = detail_columns_for(&class);
+ let cells = build_cells(&issue, &cols);
+ ogar_render_askama::render_detail(
+ 0x0102, "project_work_item", id,
+ &headline_html(&issue), &subtitle(&issue),
+ &cols, &cells,
+ )
+}
+```
+
+Every resource follows this shape. Eight contributors can land eight
+tracks in parallel. **No track touches another track's file** — they
+each own `rm-handlers/src/.rs` plus a route registration.
+
+### Width-pass DoD per track
+- `GET /` returns a list page that renders.
+- `GET //:id` returns a detail page.
+- Seed-data rows are visible.
+- Empty state renders correctly when no rows.
+- Co-located handler test that hits both URLs and asserts the HTML
+ contains the resource name and the canonical class_id.
+
+## 4. Phase 2 — Depth pass (parallel cross-cutting tracks)
+
+Phase 2 adds features that **every resource needs** but each is a
+single cross-cutting concern. These are tracks (D1..D5) that, after
+landing, retrofit every Phase-1 resource page automatically.
+
+### D1 — Forms (create + edit)
+- New handlers per resource: `new`, `create`, `edit`, `update`,
+ `destroy`. RESTful URL convention.
+- Each calls `ogar_render_askama::render_form` with the right
+ `FormSource`.
+- CSRF token from the session middleware.
+- Server-side validation: pull from `Class::attributes[i].options.required`
+ + simple per-kind validation (date parsing, etc).
+- **Width compatibility:** every Phase-1 resource gets forms in one
+ pass. D1 lands one set of handlers in `rm-handlers/src/forms.rs`
+ generic over `T: PortSpec`-derived row type.
+
+### D2 — Filters, sort, group, total (Redmine Query)
+- Adopt Redmine's `query.inline_columns` / `query.block_columns` shape
+ directly — the substrate already has it (`RenderColumn::sortable()`,
+ `RenderColumn::groupable()`, `RenderColumn::totalable()`).
+- Parse query-string `?filter[status]=open&sort=priority:desc&group=tracker`.
+- Render filter chrome (a `` panel with input rows) via a
+ new askama template `dispatch/filter_panel.askama`.
+- Saved Queries become first-class: the `Query` resource from W8
+ already exists; persistence is a SurrealDB row.
+- **Width compatibility:** generic over `PortSpec`; every resource
+ inherits.
+
+### D3 — RBAC enforcement
+- Use the `Membership` / `Role` / `MemberRole` canonical concepts.
+- Permission table per (Role, action_set): mirror Redmine's
+ `Redmine::AccessControl.permission` (well-documented spec).
+- Middleware extractor: `RequirePermission<{ project_id, perm_name }>`
+ rejects with 403 if the current user's role on the project lacks the
+ permission.
+- **Width compatibility:** add one extractor per route; mechanical.
+
+### D4 — Workflows (state transitions)
+- `IssueStatus` carries `is_closed: bool`. Redmine's workflows are a
+ per-(Tracker, Role) → allowed-transition table.
+- Store the transition table as SurrealDB rows (canonical concept
+ `project_workflow` — promotion candidate, not in codebook yet; OGAR
+ side-PR to add `0x011B`).
+- Handler enforces: `update` rejects status changes outside the allowed
+ transition set.
+- **Width compatibility:** retrofits any state-machine-shaped resource
+ (Issue today, future Versions if they grow status).
+
+### D5 — Custom fields (dynamic schema)
+- `CustomField` (0x0110) + `CustomValue` (0x0119) canonical concepts.
+- Dynamic per-resource attribute set: load all `CustomField`s for the
+ current Tracker at request time, append to `cols: Vec`
+ before render. The askama template already takes a `&[RenderColumn]`
+ slice — no template change needed.
+- Heaviest of the depth tracks; budget 1 week.
+- **Width compatibility:** every resource that supports custom fields
+ inherits, mechanical wiring per resource.
+
+### D6 — Full-text search (deferred, parallel)
+- SurrealDB has FULLTEXT indexes; emit them via a new
+ `op_surreal_ast::IndexKind::Fulltext` variant + `from_class` opt-in.
+- One `/search?q=…` route; renders an aggregate `HtmlListView` over
+ hits with a `_type` column showing the resource kind.
+- **Defer:** until D1+D2 land; the substrate isn't blocking.
+
+### Depth-pass DoD per track
+- The cross-cutting feature works on at least 2 Phase-1 resources.
+- An integration test exercises the feature on Issue (the busiest
+ resource).
+- The feature wired through the `PortSpec` substrate — no
+ resource-specific code in the depth track itself.
+
+## 5. Phase-parallel — Template + GUI tracks (parallel with everything)
+
+These tracks are **OGAR-side** (templates) or **redmine-rs-side** (CSS,
+JS, layouts) and can run concurrent with any phase.
+
+### T1 — Column-kind extensions
+- New `ColumnKind` variants Redmine uses that aren't in the 10 we
+ shipped: `RelativeTime` ("3 hours ago"), `UserAvatar`,
+ `BadgeStatus` (coloured status pill), `MultiTag`, `BoolCheck`,
+ `Sparkline` (for done_ratio over time).
+- Each lands as `ogar-render-askama/src/artifact_kinds/cells/.askama`
+ + a `CellData` enum variant + a default-kind-for resolver entry.
+- **OGAR PR per kind.** Highly parallel — one contributor per kind.
+
+### T2 — InputKind extensions
+- Mirror T1 on the form side: `InputKind::FileUpload`,
+ `InputKind::RichTextarea` (TinyMCE / Trix substitute),
+ `InputKind::DateRange`, `InputKind::AutocompleteUser`.
+- OGAR-side; each is one askama sub-template + a binding-struct variant.
+
+### G1 — Layout chrome
+- Master template (`base.askama`): top nav, sidebar with project
+ selector, footer.
+- Page slot: `{{ content|safe }}` for the per-resource render output.
+- **No behavioural code.** Pure CSS + HTML structure.
+
+### G2 — CSS port
+- Port Redmine's stylesheet (`public/stylesheets/application.css`,
+ ~7000 LOC organized by area). Modern revision: Tailwind utility layer
+ underneath, Redmine class names preserved so old plugin themes still
+ resolve.
+- **No coupling to handlers** — CSS port can land before, during, or
+ after Phase 1 with zero conflict.
+
+### G3 — Client-side widgets (deferred-ish)
+- Hotwire-style enhancement: Turbo (or htmx) for partial swaps,
+ Stimulus controllers for the date-range / autocomplete / collapse
+ widgets. Defer until G1+G2 land; the page is server-rendered HTML
+ by default and gracefully degrades without JS.
+
+## 6. The dependency graph
+
+```text
+ ┌──────────────┐
+ │ Phase 0 │
+ │ (W0.1-3) │ ← sequential, 1 week
+ │ Foundation │
+ └───┬───────┬──┘
+ │ │
+ ┌──────┬──────┬────────┴───────┴───────┬──────┬──────┐
+ ▼ ▼ ▼ ▼ ▼ ▼
+ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐
+ │ W1 │ │ W2 │ │ W3 │ … Width … │ W6 │ │ W7 │ │ W8 │ ← parallel, 3-5 days each
+ │Iss │ │Proj│ │Time│ │Cmnt│ │ SCM│ │Qry │
+ └─┬──┘ └─┬──┘ └─┬──┘ └─┬──┘ └─┬──┘ └─┬──┘
+ │ │ │ │ │ │
+ └──────┴──────┴───────────┬───────────┴──────┴──────┘
+ │
+ ▼
+ ┌──────────────┐
+ │ Phase 2 │
+ │ (D1-D6) │ ← parallel cross-cutting
+ │ Depth pass │
+ └──────────────┘
+
+ ─── parallel-anytime, OGAR-side ─── ─── parallel-anytime, redmine-rs-side ───
+ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐
+ │ T1 │ │ T2 │ column / input kinds │ G1 │ │ G2 │ │ G3 │ chrome / CSS / JS
+ └────┘ └────┘ └────┘ └────┘ └────┘
+```
+
+**Hard dependencies** (must-land-before):
+- W0 before any W*.
+- D1 (forms) before D5 (custom fields render in forms).
+- D2 (filters) before D6 (search uses the filter parser).
+- W4 (User/Membership/Role) before D3 (RBAC needs the substrate).
+
+**Soft dependencies** (one helps the other but they can swap order):
+- T1 before W* — but every W* can ship using the existing 10
+ `ColumnKind`s and pick up the richer ones in a refresh.
+- G2 (CSS) is independent of everything else.
+
+## 7. Effort estimate
+
+| Phase | Streams | Per-stream effort | Wall-clock with 1 contributor | Wall-clock with 4 contributors |
+|---|---|---|---|---|
+| Phase 0 | 1 (sequential) | 1 week | **1 week** | **1 week** |
+| Phase 1 | 8 width tracks | 4 days each | ~6 weeks serial | **~1.5 weeks parallel** |
+| Phase 2 | 6 depth tracks | 3-7 days each | ~5 weeks serial | **~2 weeks parallel** |
+| T-tracks | 6 template/GUI | 2-4 days each | ~3 weeks serial | **~1 week parallel** |
+| **Total** | | | **~15 weeks** | **~5-6 weeks** |
+
+**MVP cut-line** (Issue browse + create + login): Phase 0 + W1 + D1 +
+minimum G1 = ~2 weeks with 1 contributor, ~1 week with 2.
+
+**Feature parity cut-line** (everything Redmine does, minus SCM
+driver): Phase 0 + W1-W8 + D1-D5 + T1-T2 + G1-G2 = ~5-6 weeks with 4
+contributors.
+
+## 8. Parallel-safety — file ownership
+
+The streams are designed so **no two parallel streams touch the same
+file**. Per-stream file ownership:
+
+| Stream | Owns |
+|---|---|
+| W0.1 | `rm-server/src/main.rs`, `rm-server/Cargo.toml` |
+| W0.2 | `rm-store/src/**`, `rm-store/Cargo.toml` |
+| W0.3 | `rm-auth/src/**`, `rm-auth/Cargo.toml` |
+| W1 | `rm-handlers/src/issue.rs` + `journal.rs` + 1 route block in `rm-server/src/routes.rs` |
+| W2 | `rm-handlers/src/project.rs` + `version.rs` + `enabled_module.rs` + 1 route block |
+| W3..W8 | Same shape — own one file per resource + one route block |
+| D1 | `rm-handlers/src/forms.rs`, generic |
+| D2 | `rm-handlers/src/filter.rs`, generic |
+| D3 | `rm-auth/src/rbac.rs` |
+| D4 | `rm-handlers/src/workflow.rs` |
+| D5 | `rm-handlers/src/custom_fields.rs` |
+| T1 | `ogar-render-askama/src/artifact_kinds/cells/.askama` + `cells.rs` add variant |
+| T2 | `ogar-render-askama/src/artifact_kinds/inputs/.askama` + `inputs.rs` add variant |
+| G1 | `rm-server/templates/base.askama` + layout templates |
+| G2 | `rm-server/assets/css/**` |
+| G3 | `rm-server/assets/js/**` + Stimulus controllers |
+
+**`rm-server/src/routes.rs`** is the one shared file. Convention: each
+W* track adds its route block at the bottom in alphabetical order; merge
+conflicts there are trivial.
+
+## 9. Calibration gates per stream
+
+Every stream lands with the three gates Northstar §3 calibrated:
+
+1. **Round-trip parse** — emitted HTML parses as valid HTML5 (use
+ `html5ever` in tests).
+2. **Target-toolchain compile** — `cargo check` green; askama templates
+ compile.
+3. **ClassView drift guard** — every `RenderColumn::new(name, …)` for a
+ class references a `Class::attributes` entry. A test enumerates
+ the columns and asserts each name is in the class's attributes.
+
+Plus stream-specific gates from the DoD lists above.
+
+## 10. Open questions — to decide before / during
+
+1. **Datastore choice** — SurrealDB everywhere (consistent with the
+ schema emission story), OR support reading existing Redmine
+ Postgres/MySQL via a `rm-store-pg` sibling? Recommendation:
+ SurrealDB-only for MVP; import tool comes later as a separate PR.
+2. **i18n** — Redmine ships 50+ locales. Use `rust-i18n` (already in
+ OpenProject's deps) or `fluent`? Defer choice; design handlers to
+ take labels from a `Localizer` trait so the impl is swappable.
+3. **Plugin API surface** — Redmine has a plugin system
+ (`Redmine::Plugin.register`). Out of scope for MVP. Long term, the
+ PortSpec / canonical-concept pattern is the modern equivalent: a
+ plugin defines new concepts in OGAR + handlers in `rm-handlers`.
+4. **REST API parity** — JSON variants of every handler. Add a
+ `serde_json::Serialize` impl on `RowSource` and one extractor that
+ switches on `Accept: application/json`. Defer to a depth track
+ after Phase 1 settles.
+
+## 11. The single-paragraph "ship it" summary
+
+The substrate is done. The askama kit already inherits Redmine's 17
+years of UX organic discovery (column dispatch, formatter library,
+default index hints — all sitting in `ColumnKind` and the per-kind
+sub-templates). What's left is **mechanical wiring**: an axum server,
+a SurrealDB store, per-resource handlers that compose
+`(class, query) → render_list / render_detail / render_form`. Eight
+contributors can land MVP-shaped Redmine in ~2 weeks of parallel work,
+feature-complete in ~6 weeks. The substrate is the hard part, and the
+substrate is the part already in main.