+
+
+
+
+
\ No newline at end of file
diff --git a/prototypes/zoom-v1.htm:Zone.Identifier b/prototypes/zoom-v1.htm:Zone.Identifier
new file mode 100644
index 0000000..053d112
--- /dev/null
+++ b/prototypes/zoom-v1.htm:Zone.Identifier
@@ -0,0 +1,3 @@
+[ZoneTransfer]
+ZoneId=3
+HostUrl=about:internet
diff --git a/specification/ai-agent-architecture.md b/specification/ai-agent-architecture.md
new file mode 100644
index 0000000..f2e40ef
--- /dev/null
+++ b/specification/ai-agent-architecture.md
@@ -0,0 +1,94 @@
+# AI Agent Architecture (Deferred — Reference Specification)
+
+This documents the agent architecture developed in the v1 codebase. AI integration is deferred for the initial rebuild, but the patterns here inform future implementation.
+
+## Router-Specialist Pattern
+
+A lightweight router agent classifies user intent and delegates to domain specialists:
+
+- **Router** (gpt-4o-mini) — intent classification only, no direct data access. Forces tool calls for every message (except greetings). Cheap.
+- **Storage Specialist** (gpt-4o) — manages templates, modules, inserts, storage structure
+- **Inventory Specialist** (gpt-4o) — manages items, assignments, location queries
+
+The router has only two tools: `runStorageAgent` and `runInventoryAgent`. Each takes a `task` string. For compound requests ("create MUSE with 11 levels and put resistors in level 5"), the router breaks into separate specialist calls with accumulated context.
+
+## Tool Definition Structure
+
+Tools are the interface between agents and business logic:
+
+```
+name: "createItem"
+description: "Create a new item in the inventory"
+category: "items"
+handler: "items.create" ← dispatch key
+parameters: [{ name, type, description, required, enum? }]
+```
+
+Handler string format determines dispatch:
+- `agents.runStorage` — delegate to specialist (recursive agent execution)
+- `items.create` — direct handler lookup in registry
+
+Tools are formatted for OpenAI's function-calling API at runtime.
+
+## Handler Dispatch
+
+Centralized handler registry maps handler strings to functions:
+
+```
+handlerMap = {
+ 'templates.create': templateHandlers.create,
+ 'modules.list': moduleHandlers.list,
+ 'items.merge': itemHandlers.merge,
+ 'assignments.assign': assignmentHandlers.assign,
+ // ~30+ handlers
+}
+```
+
+All handlers follow a consistent signature:
+- Arguments object (tool parameters) + userId
+- Return result object on success, `{ error: "msg" }` on failure
+- Errors thrown in repositories are caught and returned gracefully — the LLM can reason about failures and retry
+
+## Agent Execution Loop
+
+1. Format tools for this agent into OpenAI schema
+2. Build message array from system prompt, history, and current message
+3. If specialist (not router): set `tool_choice: 'required'` to prevent hallucinated answers
+4. Call OpenAI
+5. **Tool iteration** (max 10 rounds):
+ - For each tool call in response:
+ - If `agents.*` handler → recursively execute specialist agent
+ - Otherwise → look up and execute handler
+ - Push results back as tool messages
+ - Continue until LLM produces a text response (no more tool calls)
+6. Post-process: strip filler phrases ("Feel free to ask", "Let me know if...")
+7. Return content + agent name + optional tool call audit trail
+
+## Patterns Worth Preserving
+
+### Name-to-ID Resolution
+AI passes names ("MUSE") not database IDs. Handlers resolve automatically — accept either, look up by name if not a valid ID.
+
+### Path Normalization
+Grid cells referenced in multiple formats ("A1", "A,1", "A 1"). Handler normalizes all to a canonical form.
+
+### Multi-Level Path Inference
+If AI provides a flat path like "MUSE level 3 cell A1", handler auto-splits into module path + insert-internal path.
+
+### Atomic Multi-Step Operations
+Operations like item merge (reassign all assignments from duplicates to keeper, delete duplicates) are atomic — no partial failures.
+
+### Specialist Tracking
+When router → specialist → response, the UI shows which specialist answered, not just "router."
+
+### Caching
+In-memory maps for tools and agents. Tools are global, agents are per-user. No TTL — session-scoped. Cleared between test scenarios via `clearAgentCaches()`.
+
+## Reimplementation Notes
+
+The architecture is database-agnostic. When reimplementing on PostgreSQL:
+- Agent and tool definitions can be database tables or static configuration (static is simpler for a small agent count)
+- Handler registry stays as code — no need to be database-driven
+- Keep name-to-ID resolution and path normalization patterns
+- Session/history storage moves to PostgreSQL with proper indexing
+- Error handling model (throw in repo, catch in handler, return to LLM) carries over unchanged
diff --git a/specification/ai-collaboration-poc-to-production.md b/specification/ai-collaboration-poc-to-production.md
new file mode 100644
index 0000000..aee5ceb
--- /dev/null
+++ b/specification/ai-collaboration-poc-to-production.md
@@ -0,0 +1,277 @@
+# Working with Claude (or any capable AI) to take a POC to a well-architected system
+
+A working guide distilled from the WhereTF build. Not theory — written while
+the project itself was evolving from a rough idea into a structured codebase
+with AI doing most of the typing.
+
+The short version: the AI is a fast, confident, inexperienced collaborator.
+Treat it like a brilliant intern who has never seen this codebase before and
+doesn't care about the blast radius of what it's about to type. Your job is
+to supply the judgment, the memory, and the accountability.
+
+---
+
+## The phases
+
+POC → production is not a single refactor. It's a sequence with different
+failure modes at each stage.
+
+1. **Rough scaffold.** Everything works, nothing is trustworthy.
+2. **Model settling.** Core nouns and verbs of the domain lock in. Schema
+ stops churning daily.
+3. **Interaction refinement.** UX surfaces drive new edges. Data model
+ usually absorbs them without structural change — if it can't, you're
+ back at #2.
+4. **Correctness hardening.** Tests, constraints, transaction boundaries,
+ cascades. The day you discover a bug that caused silent data loss is
+ the day you realize #3 was premature.
+5. **Cleanup + convention.** Duplication extracted, names normalized, types
+ tightened, migrations idempotent, footguns guarded.
+
+You don't get to skip phases. AI can accelerate each one by 3-10x, but
+applying the wrong phase's habits to another phase is where things go
+sideways. A POC with phase-5 discipline dies of ceremony. Production with
+phase-1 discipline burns on contact.
+
+---
+
+## The core collaboration loop
+
+Every meaningful change should pass through:
+
+### 1. Capture
+
+Get the problem into writing before any code is generated. A one-sentence
+prompt is not a capture — it's a bet that the AI will infer the right
+abstraction. It won't. It'll infer *an* abstraction and defend it.
+
+Minimum viable capture: the problem, why it matters, what has been ruled
+out, the constraint you most care about. Ideally in a living doc (this
+project uses `specification/*-issues.md`) you can grep, reference, and
+extend.
+
+### 2. Clarify
+
+Before generating code, the AI should list its clarifying questions and
+*wait*. You should refuse to answer questions that feel like they're
+trying to short-circuit a design decision. Answers that shape the data
+model or the interaction are yours to make. Implementation details
+(naming a helper, picking an internal data structure) are fine to delegate.
+
+Heuristic: if the question starts with "should I" and the answer changes
+the *observable* behavior of the system, answer it yourself. If it starts
+with "how should I" and only changes internal structure, let the AI choose.
+
+### 3. Plan
+
+For anything non-trivial, get a plan back before any code lands. The plan
+should be specific enough that you can predict the diff: files touched,
+migrations added, API routes changed, tests added. A plan that reads
+"I'll add the feature and test it" is not a plan.
+
+Plans also serve as a commit-message draft. The best commits are plans
+whose prose survived verbatim from the pre-code step.
+
+### 4. Execute
+
+Small commits, each one a single reversible unit. One schema change +
+its migration + its backfill + its repo methods + its tests + its API
+route + its UI hook is fine as a single commit *if* you can describe it
+in one sentence. Two sentences = two commits.
+
+The AI will sometimes want to fix adjacent things while it's in there.
+Push back. "Just this thing, the other is a separate commit." The
+blast radius of a compound commit is where most regression hunts end up.
+
+### 5. Verify
+
+Reality check the diff before trusting the green tests. Known traps:
+
+- Tests that run but assert nothing meaningful.
+- Tests that re-assert what the code does rather than what the *spec*
+ requires.
+- "Passed with 0 failures" on an empty suite.
+- Integration tests that share state with the dev environment. (We hit
+ this one — `npm test` was truncating the dev database because
+ `DATABASE_URL` wasn't overridden. Running the suite wiped the user's
+ workshop. This is the single best example of a POC habit — "it works
+ on my machine, ship it" — surviving into production setup.)
+
+For any PR that touches persistence, the check is: *can I point at the
+single commit that introduces this state change, and does that commit
+include the migration, the backfill, the repo method, and the test that
+fails without the migration*? If any one is missing, you have drift in
+flight.
+
+---
+
+## Habits that work
+
+**Treat the issues doc as source of truth.** When the user says "capture
+this, don't fix it yet" — that's an invitation to make the doc the
+artifact. Code is how we respond to it. Over weeks, the doc accumulates
+the real shape of the product.
+
+**Answer questions inline in the conversation.** Every clarifying question
+gets a one-line answer, then the plan is finalized. This keeps the
+context small enough that the AI's working memory isn't overrun.
+
+**Commit at sensible boundaries.** One commit per logical unit. Don't
+batch. A single well-written commit message is the best documentation
+you'll ever write, and it only exists when the commit is small enough
+to need one sentence.
+
+**Keep a running migration count.** If the project has `0007_foo.sql`,
+the next one is `0008_bar.sql`. Never skip. Never rename. Write the
+migration SQL by hand (or generate it and read it line by line). Let the
+AI write the Drizzle schema — but the SQL, *you* own.
+
+**Name the footguns.** When you hit one, write it down as a
+caveat in the issue doc (or in the file the footgun lives in), and add
+the fence in code. Silent data loss cannot be rediscovered — it has to
+be *impossible*.
+
+**Extract at three, not two.** Two copies of something is duplication
+but not a problem yet. Three copies is a pattern. Four is a refactor.
+Extracting at two produces the wrong abstraction half the time.
+
+**Defer what you don't need.** Every sentence of the spec that can wait
+should wait. The AI wants to be complete; completeness is the enemy of
+shipping.
+
+---
+
+## Habits that go wrong
+
+**Trusting generated code without reading it.** Even for "simple" things.
+The AI will cheerfully write a transaction-log query that scans the
+entire table, a React useEffect with a missing dep, a migration that
+works on empty tables and silently truncates on populated ones.
+
+**Letting the AI rewrite instead of edit.** Ask for an edit, get an edit.
+Ask for "clean this up" and you'll get a full rewrite that loses context
+you didn't know the file carried.
+
+**Accepting a commit without reading the diff.** This is the single most
+reliable way to introduce silent regressions. The AI will write confident
+commit messages for commits that change things the message doesn't
+mention.
+
+**Treating the generated tests as ground truth.** The tests will be
+against the implementation as written, not the spec. Your job is to
+supply the tests — or at least the assertions — that reflect the spec
+the code is trying to meet.
+
+**Letting feature creep redefine the data model.** Every UI sketch looks
+like it wants a new column. Most of them are variations on existing
+columns. If you find yourself adding the third column to support a fourth
+UI variation, stop. The model is wrong.
+
+**Chasing the AI's confident wrong answer.** When the AI asserts a
+framework behaves a certain way and you're 80% sure it doesn't, go
+verify. Don't let the chat keep going with the wrong premise. It
+*will* build a rococo house on the wrong foundation and defend every
+brick.
+
+---
+
+## What the AI actually does well
+
+- Boilerplate: schema + migration + repo + route + test for a new entity,
+ in ~5 minutes of conversation. Do this often.
+- Mechanical refactors: "rename X to Y across the codebase", "extract
+ these three copies into a shared helper", "convert this SVG renderer
+ to CSS Grid". High win rate when the before/after is well-defined.
+- Reading unfamiliar code: ask it to survey a module and describe what
+ it sees, check its reading against reality. Much faster than doing it
+ by hand, given you read the summary critically.
+- Test scaffolding: spinning up the boilerplate for a test case, letting
+ you fill in the assertions.
+- Commit messages: a good prompt + the diff → a respectable message.
+
+## What it does poorly
+
+- Anything involving judgment about tradeoffs. It will produce a
+ recommendation with confident framing whether or not it has the
+ information to make one. Always ask it to list the tradeoffs before
+ acting.
+- State that's subtle. Event loops, race conditions, transaction
+ isolation, cascading deletes. The wrong choice looks identical to the
+ right choice until the 10th user.
+- Distinguishing POC shortcuts from load-bearing architecture. It will
+ happily reach into a core abstraction to patch a UI bug because the
+ fix "fits better there".
+- Long-running context. Give it 500 lines of prior decisions and ask for
+ the 501st — it may contradict decision 37. Keep the durable decisions
+ in a file, not only in the chat.
+
+---
+
+## Signal that the project is on the right track
+
+- **Spec files grow faster than code.** If you're churning specs, you're
+ preventing churn in code.
+- **Commits are small and easy to name.** The commit title = the feature.
+- **Tests are numerous and fast.** You run them often. They're honest
+ (they'd catch the thing the feature prevents).
+- **The migrations log is clean.** Sequential, each one explainable, none
+ destructive.
+- **Naming stays stable.** When you see a word in a commit message, you
+ know what part of the system it refers to.
+- **Deletion works correctly.** You can delete any entity and the
+ transaction log records the cascade. Nothing orphans.
+
+## Signal the project is drifting
+
+- **Recurring "oh, right" bugs.** Same kind of mistake in a new place.
+ Missing constraint, duplicated logic, inconsistent name. → Invest in a
+ convention + a lint or test that enforces it.
+- **Commits that touch six areas.** Either the change is too big or the
+ areas are wrongly separated.
+- **You'd rather rewrite a feature than read it.** The AI made it in the
+ first place; it shouldn't be unreadable after a week. Read + simplify
+ before extending.
+- **You can't remember why a column exists.** Go annotate it. Migration
+ comments are cheap.
+- **You avoid running tests because they're slow or flaky.** A test
+ suite you don't trust is a test suite you don't run.
+
+---
+
+## Minimum guardrails for production-readiness
+
+A POC graduating to production needs all of these in place. None is
+optional. AI can write all of them in an afternoon; you need to ask.
+
+1. **Test DB is not the dev DB.** Verified by a guard that refuses to
+ run destructive operations on any DB whose name doesn't contain
+ `test`. (We caught this the hard way.)
+2. **Migrations are numbered and append-only.** No rewriting history.
+3. **Every destructive action is logged.** Transaction log with
+ before/after state, type-discriminated.
+4. **Every cascade is explicit.** Either an FK `ON DELETE CASCADE` or a
+ repo method that does the cleanup in a transaction.
+5. **Every API error has a status code that lets the UI distinguish
+ kinds.** 409 for "state conflict", 404 for "not found", etc. Not
+ everything is 400.
+6. **Every state transition has a test that tries to violate the
+ invariant and expects a throw.** Not just the happy path.
+7. **No magic strings for enums.** Either a literal union type or a
+ database check constraint.
+8. **Secrets are not in the repo.** `.env.local.example`, not
+ `.env.local`.
+9. **The README (or equivalent) tells a new developer how to seed, how
+ to run tests, and what service the dev DB runs on.**
+10. **When a feature is deferred, it's noted somewhere retrievable
+ — not just in commit history.**
+
+---
+
+## One last thing
+
+The AI will say "shipped" when what it means is "the code compiled and
+the tests I wrote passed". Your sign-off is different — "this changes
+the behavior of the system in the way I intended, nothing else, and I
+understand every line of the diff." Those are not the same event.
+
+Ship on your own terms. The AI is the keyboard, not the architect.
diff --git a/specification/deployment.md b/specification/deployment.md
new file mode 100644
index 0000000..00aa1e5
--- /dev/null
+++ b/specification/deployment.md
@@ -0,0 +1,285 @@
+# Deployment
+
+WhereTF is FOSS (AGPL-3.0). CI publishes container images to the
+public GitHub Container Registry; anyone can pull, run, and
+self-host. The project's own homelab deployment pulls from the same
+registry.
+
+The open repo produces Docker artifacts only. Deploy orchestration
+(hostnames, secrets, Ansible, Caddy config) lives in a separate
+private infrastructure repo and is not part of this codebase.
+
+---
+
+## Artifacts
+
+| Path | Purpose |
+|------|---------|
+| `web/Dockerfile` | Multi-stage: `deps`, `builder`, `migrator`, `runner`. |
+| `web/.dockerignore` | Keeps `node_modules`, `.next`, secrets, and local Claude state out of the build context. |
+| `docker-compose.yml` | Reference compose at repo root: runs app + Postgres + migration job end-to-end on a laptop. |
+| `docker-compose.dev.yml` | Dev-only Postgres. Used with `npm run dev` against the host. |
+| `web/app/api/health/route.ts` | Liveness probe. Always 200 if Node is servicing HTTP. |
+| `web/app/api/health/ready/route.ts` | Readiness probe. 200 if `SELECT 1` round-trips to Postgres; 503 otherwise. |
+| `web/db/migrations/meta/_journal.json` | Drizzle journal — authoritative, covers 0000..head. `drizzle-kit migrate` is a no-op against a DB at head, and applies everything against a fresh DB. |
+
+---
+
+## Supported Postgres
+
+**Target: 16.x. Accepted: 15–17.**
+
+The schema uses UUID, JSONB, text arrays, correlated subqueries, and
+`SELECT DISTINCT ON` — all stable since 13. We pin the reference
+compose to `postgres:16` because that's the current LTS. CI should run
+against the minimum version we claim to support.
+
+The `postgres` node driver and Drizzle are version-agnostic across
+this range.
+
+---
+
+## Environment contract
+
+Single image, differs only by environment variables.
+
+| Variable | Required | Purpose |
+|----------|----------|---------|
+| `DATABASE_URL` | yes | `postgresql://user:pw@host:port/db`. Never baked into the image. |
+| `NODE_ENV` | yes (runtime) | `production` in the runner image. |
+| `PORT` | no | Default `3000`. |
+| `HOSTNAME` | no | Default `0.0.0.0` so the container accepts external traffic. |
+| `NEXT_TELEMETRY_DISABLED` | no | Set by the image; can be unset. |
+
+Anything secret is read on first request from the process environment.
+Nothing gets compiled into the bundle.
+
+---
+
+## Build
+
+### Local (single-arch)
+
+```bash
+cd web
+docker build --target runner -t wheretf/web: .
+docker build --target migrator -t wheretf/migrate: .
+```
+
+### Multi-arch (done by CI)
+
+GitHub Actions (`.github/workflows/ci.yml`) builds both stages for
+`linux/amd64` and `linux/arm64` on every push to `main` and every
+release tag, then publishes to GHCR:
+
+- `ghcr.io/ndemarco/wheretf/web:sha-` (always)
+- `ghcr.io/ndemarco/wheretf/web:latest` (on `main`)
+- `ghcr.io/ndemarco/wheretf/web:v` (on release tags)
+- `ghcr.io/ndemarco/wheretf/migrate:*` — same tag schema.
+
+Manual equivalent if you need it locally:
+
+```bash
+docker buildx create --use --name wtf-builder
+docker buildx build \
+ --platform linux/amd64,linux/arm64 \
+ --target runner \
+ -t ghcr.io/ndemarco/wheretf/web: \
+ --push ./web
+```
+
+Tag with the git short-sha. Avoid `:latest` for prod.
+
+---
+
+## Run
+
+### End-to-end with compose (simplest — good for smoke tests)
+
+```bash
+docker compose up --build -d
+# http://localhost:3000
+docker compose down -v # stop + wipe the local DB volume
+```
+
+Orchestration sequence:
+1. `postgres` starts, waits for healthy.
+2. `migrate` runs `drizzle-kit migrate` against `DATABASE_URL`, exits 0.
+3. `app` starts only after `migrate` exits successfully.
+
+### Manual / deploy-system path
+
+```bash
+# 1. Provision a Postgres DB and put DATABASE_URL somewhere secret.
+
+# 2. Run the one-shot migration task.
+docker run --rm \
+ -e DATABASE_URL="postgresql://..." \
+ ghcr.io/ndemarco/wheretf/migrate:
+
+# 3. Start the app (scale to N replicas as needed).
+docker run -d \
+ -e DATABASE_URL="postgresql://..." \
+ -p 3000:3000 \
+ ghcr.io/ndemarco/wheretf/web:
+```
+
+Roll forward: run the new migrator image, then roll the app replicas.
+Roll back: point the app at the old image. Schema rollback needs a
+separate down-migration strategy — not covered here.
+
+---
+
+## Health endpoints
+
+- `GET /api/health` — liveness. 200 unconditionally. Use for container
+ restart policies (`HEALTHCHECK` in the Dockerfile already does).
+- `GET /api/health/ready` — readiness. 200 when the app can execute
+ `SELECT 1`; 503 otherwise. Use to gate load-balancer traffic and
+ rolling deploys.
+
+Any deploy system that rolls replicas should poll `/ready` before
+marking a new container healthy and before removing an old one.
+
+---
+
+## Dev → prod migration strategy
+
+Migration authority is the drizzle journal at
+`web/db/migrations/meta/_journal.json` plus the numbered `.sql` files
+next to it. The deploy flow:
+
+1. CI builds the `migrator` image at the same git sha as the `runner`
+ image.
+2. Deploy system runs the migrator as a one-shot task against the prod
+ `DATABASE_URL`. Blocks on exit 0.
+3. Deploy system rolls the `runner` image, gated on readiness.
+
+There is no entrypoint-migrate in the runner image — migrations are
+always a separate task so scaled replicas don't race each other.
+
+For catastrophic rollback, `pg_restore` from the last pre-deploy dump.
+The prod DB should take automated dumps at least hourly.
+
+---
+
+## Environments
+
+| Environment | Database | Auth | Purpose |
+|-------------|----------|------|---------|
+| Local dev | Local Postgres via `docker-compose.dev.yml` | None (to come) | Development, manual testing |
+| CI | Ephemeral Postgres service container | None | Automated tests |
+| Staging | Homelab Postgres (separate DB) | OIDC (to come) | Pre-prod smoke |
+| Production | Homelab Postgres (prod DB) | OIDC (to come) | Live app |
+
+Same image across staging and prod — they differ only in
+`DATABASE_URL` and (eventually) auth config.
+
+---
+
+## Logging
+
+App logs go to stdout as newline-delimited text (default Next.js
+formatting). Deploy system is responsible for shipping and retention.
+Nothing app-side configures files, rotation, or remote sinks.
+
+---
+
+## Performance notes
+
+- Standalone output + alpine base → runtime image ~180 MB.
+- BuildKit cache mounts for `npm ci` and `.next/cache` keep warm
+ rebuilds under a minute for small diffs.
+- Single process per container. Scale horizontally for load.
+ Multi-replica is safe once auth + DB-backed sessions land.
+
+---
+
+## Future work — TODOs so the deploy system knows what's coming
+
+These are **planned, not implemented.** Deployment as described above
+works without them. When any one of them lands, this doc and the
+deployment system both get revisited.
+
+### Identity provider on the homelab (TODO)
+
+The homelab currently has **no IdP deployed**. Before authentication
+can ship, one has to be stood up and maintained.
+
+Scope for that effort (separate project, separate plan cycle):
+- Evaluate Authentik, Keycloak, Zitadel. Pick one.
+- Deploy (Proxmox LXC or container), put behind Caddy, back with the
+ homelab Postgres VM or its own DB.
+- Automate lifecycle with Ansible so it's reproducible.
+- Add monitoring + backups alongside existing services.
+
+Until that lands, WhereTF runs without auth and must not be internet-
+reachable.
+
+### Authentication (TODO)
+
+Depends on the IdP decision above. Plan:
+
+- **App side**: Auth.js (next-auth v5) with an OIDC provider. DB-backed
+ sessions — new `users`, `sessions` tables, users identified by
+ `(auth_provider, auth_subject)` for provider portability.
+- **Dev**: local "impersonate" login gated on
+ `NODE_ENV !== "production"` so dev doesn't require the IdP to be
+ running.
+- **CSRF**: Auth.js covers `/api/auth/*`; our mutation routes get a
+ shared helper.
+
+### Authorization (TODO)
+
+Model: **org-scoped, role per user-org pair.**
+
+- New tables: `orgs`, `user_orgs (user_id, org_id, role)`.
+ Roles: `owner | admin | member | viewer`.
+- Every authenticated request carries
+ `{ userId, currentOrgId, role }`, derived from session + an
+ "active org" cookie.
+- **Per-org** tables: `modules`, `locations`, `inserts`, `assignments`,
+ `templates`, `template_versions`, `co_storability`.
+- **Global** tables: `items`, `item_aspects`, `item_parameter_values`,
+ `aspects`, `parameters`, `standards`, `designations`, `categories`.
+ Items are deliberately shared — see project memory on the global
+ catalog vision.
+- Enforcement starts application-layer (every repo method takes
+ `{ orgId }`), moves to Postgres RLS when the threat model justifies
+ the complexity.
+
+### API access + rate limiting (TODO)
+
+Two surfaces:
+
+1. **Internal** — session-cookie auth, CSRF for mutations.
+ Generous per-user limits (e.g. 60 rps burst, 300 rpm sustained).
+2. **External** — API keys. New table `api_keys` with hashed keys,
+ scopes, per-key rate limits tied to the org's plan (subscription
+ hook).
+
+Limiter: token bucket, sliding window. In-memory in dev; Redis (or
+similar) in prod.
+
+Middleware lives in `web/middleware.ts`, intercepts `/api/*`, runs
+auth + rate-limit + org hydration before the route handler. Exempts
+`/api/health*`.
+
+### Multi-tenancy migration (TODO, depends on auth + authz)
+
+Execution order when picked up:
+
+1. Migration adds `users`, `orgs`, `user_orgs`, `sessions`. Adds
+ nullable `org_id` on every per-org table; backfills existing rows
+ to a "default" org; follow-up migration flips NOT NULL.
+2. Repo refactor: every org-scoped method takes `{ orgId }`.
+ Integration test with two orgs enforces isolation per repo.
+3. API middleware populates request-local org context.
+4. Org switcher UI.
+5. Items stay global. Write-heavy catalog paths audit into the
+ existing `transactions` table.
+6. `orgs.plan` → rate limits + feature flags. Stripe (or whatever)
+ webhook updates it.
+
+None of the above blocks the deploy work. Ship the image now; layer
+auth + tenancy in when the IdP is ready.
diff --git a/specification/item-maintenance.md b/specification/item-maintenance.md
new file mode 100644
index 0000000..9113a79
--- /dev/null
+++ b/specification/item-maintenance.md
@@ -0,0 +1,148 @@
+# Item Maintenance — Use Cases
+
+These use cases define how users create, find, edit, and manage items in WhereTF. They inform the item maintenance UX design.
+
+---
+
+## To address
+
+1. Adding a family or group: I have a set of M3 SHCS in lengths 5, 6, 10, 12, 14, 20 mm. All have the same properties.
+
+## UC-1: Create a New Item
+
+Trigger: User acquires a new type of item (just bought, just received, found in a drawer).
+
+Precondition: None. Items can be created standalone or during assignment.
+
+Postcondition: Item exists in the system. Optionally assigned to a location.
+
+Edge cases:
+- User creates a vague item ("resistors") — allowed, but the system should encourage specificity when disambiguation becomes necessary (see UC-7).
+- Duplicate detected — user can merge with existing (see UC-5) or proceed with distinct item.
+
+---
+
+## UC-2: Find an Item
+
+Trigger: User wants to locate an item or check if it exists.
+
+Precondition: At least one item exists.
+
+
+Postcondition: User sees the item and its locations, or confirms it doesn't exist.
+
+Edge cases:
+- No results — system offers to create a new item with the search term as the name.
+- Item exists but is unassigned — shown with "unassigned" status, offer to assign.
+
+---
+
+## UC-3: Edit an Item
+
+Trigger: User notices incorrect or incomplete information while browsing, searching, or assigning.
+
+Precondition: Item exists.
+
+
+
+Postcondition: Item definition updated everywhere it appears.
+
+Edge cases:
+- Renaming to match an existing item — system warns and offers merge (UC-5).
+- Editing an item that has assignments — fine, no confirmation needed. The item identity hasn't changed.
+
+---
+
+## UC-4: Split an Item
+
+Trigger: User realizes an existing item is actually two or more distinct things. Common as collections grow and initial vague definitions need refinement.
+
+Precondition: Item exists, typically with multiple assignments.
+
+
+
+Postcondition: Original item is refined. New item(s) exist with their own assignments. No assignments are orphaned.
+
+Edge cases:
+- User splits but doesn't reassign all locations — system blocks completion until all assignments are resolved.
+- Single-assignment item — split is technically allowed (the item definition changes and a new item is created) but unusual.
+
+---
+
+## UC-5: Merge Items
+
+Trigger: User discovers duplicates — two items that are actually the same thing, possibly entered at different times with different names or parameter detail.
+
+Precondition: Two or more items exist that represent the same real-world thing.
+
+
+Postcondition: One item remains with all assignments. Duplicates removed.
+
+Edge cases:
+- Merged items were at the same location — assignments collapse (same item, same location = one assignment).
+- Parameter conflicts — surviving item's definition may need editing to capture the union of information.
+
+---
+
+## UC-6: Delete an Item
+
+Trigger: User no longer has or tracks this type of item.
+
+Precondition: Item exists.
+
+
+
+Postcondition: Item and all its assignments no longer exist.
+
+Edge cases:
+- Item with no assignments — delete immediately (with undo toast, not confirmation dialog).
+- Accidental delete — undo window (consistent with project's undo+notify pattern).
+
+---
+
+## UC-7: Progressive Refinement
+
+Trigger: User adds a new item that is too similar to an existing one. The system or user recognizes that one or both definitions need more detail to be distinguishable.
+
+Precondition: Two or more items exist with overlapping names or parameters.
+
+
+
+Postcondition: Items are unambiguous. Assignments are correct.
+
+Edge cases:
+- User declines to refine — allowed. The system notes the ambiguity but doesn't block. Items can coexist with similar names if the user accepts it.
+
+---
+
+## UC-8: Bulk Parameter Edit
+
+Trigger: User needs to change the same parameter across many items at once. Common during data cleanup, reclassification, or after discovering a systematic error.
+
+Precondition: Multiple items share a parameter that needs changing.
+
+
+
+Postcondition: All selected items updated. Assignments unchanged.
+
+Edge cases:
+- Change creates ambiguity between items — system warns (see UC-7).
+- Change affects items at many locations — fine, item identity hasn't changed, just metadata.
+- User wants to change different values on different items — that's not bulk edit, that's individual edits. This use case is for uniform changes across a filtered set.
+- Partial application — user deselects some items from the batch. Only selected items are changed.
+
+---
+
+## Cross-Cutting Concerns
+
+### Undo
+All destructive operations (delete, merge, split reassignment) follow the project's undo+notify pattern: action executes immediately, toast with undo action appears, auto-dismisses after a timeout. No confirmation dialogs.
+
+### Item-Location Navigation
+From any item view, the user can navigate to any of its assigned locations. From any location view, the user can navigate to the item and see all its other locations ("Also at" links).
+
+### Future: AI-Assisted Creation
+Item creation will eventually leverage AI for fuzzy matching, auto-categorization, and parameter extraction from photos or text descriptions. The manual flow defined here is the foundation — AI assists but doesn't replace it.
+
+### Future: Fuzzy Dedup
+The system will eventually proactively surface potential duplicates for merge consideration. The merge flow (UC-5) is designed to support both user-initiated and system-suggested merges.
diff --git a/specification/item-management-design.md b/specification/item-management-design.md
new file mode 100644
index 0000000..0cfdbe6
--- /dev/null
+++ b/specification/item-management-design.md
@@ -0,0 +1,173 @@
+# Item Management — UI/UX Design
+
+How users browse, create, edit, and organize items in WhereTF.
+
+---
+
+## Page Structure
+
+Separate page from the storage navigator, accessible from the sidebar menu at `/items`. The item editor is reachable from any item reference in the app (grid cells, assignment lists, search results).
+
+Three-panel layout:
+- **Left panel** — search bar + filter pills + category filter
+- **Center panel** — item grid (primary workspace)
+- **Right panel** — selected item detail, aspect management, filter-from-value interaction
+
+---
+
+## Navigation
+
+Sidebar icon rail is the root layout — shared across all pages. Routes:
+- Modules icon → `/navigator`
+- Items icon → `/items`
+- Templates → `/templates` (future)
+- Activity → `/activity` (future)
+
+---
+
+## State in URL
+
+Active filters, sort column/direction, and search query are reflected in URL query parameters. Enables sharing and bookmarking filtered views. Example: `/items?filter=thread_diameter:M3,head_type:SHCS&sort=name:asc&q=stainless`
+
+---
+
+## Left Panel: Search and Filters
+
+### Search
+
+Text search bar at the top. Searches across item name, description, and parameter values. Executed at the database level. Results narrow the grid in real time (2+ characters).
+
+### Filter Pills
+
+Below the search bar. Each pill shows `Parameter: Value` (e.g., `Thread diameter: M3`). Pills are added from the detail panel (see Right Panel). Multiple pills combine with AND logic. Click X on a pill to remove it. Removing all pills returns to the unfiltered view. All filtering happens server-side — pills translate to query parameters sent to the API.
+
+Dynamic facet counts: when pills are active, the system indicates how many items match each remaining filter option. Prevents dead-end filtering.
+
+### Category Filter
+
+Below the pills. Shows system categories with item counts. Click a category to add it as a filter pill (`Category: Fasteners`). Counts update as other filters are applied.
+
+---
+
+## Center Panel: Item Grid
+
+The primary workspace. Items as rows, parameters as columns. Built with TanStack Table (headless — we control rendering).
+
+### Columns
+
+- **Name** column is always first, always frozen (does not scroll horizontally).
+- **Primary category** icon column, frozen after name.
+- **Dynamic parameter columns** — determined by context. Algorithm and calculation method deferred. Initially show a fixed set of common columns; dynamic adaptation is a future enhancement.
+- **Column chooser** — button to manually show/hide/reorder columns. User column choices override any automatic selection.
+- Horizontal scroll for overflow columns.
+
+### Inline Editing
+
+Click a cell to edit in place. The editor type matches the parameter's data type:
+- Numeric: number input with unit label
+- Text: text input
+- Boolean: checkbox
+- Enum: dropdown with valid options
+
+Changes save on blur or Enter. Esc cancels.
+
+### Row Selection
+
+Click a row to select it and populate the detail panel. Ctrl+click for multi-select. Selected rows have accent highlight. Multi-select shows "N items selected" in detail panel (bulk operations deferred).
+
+### Sorting
+
+Click a column header to sort. Click again to reverse. Sort indicator arrow in header. Sort is executed server-side.
+
+---
+
+## Right Panel: Item Detail
+
+Shows the full description of the selected item. This is the only place where filter-from-value interaction occurs.
+
+### Header
+
+Item name (editable inline), description (editable inline).
+
+### Categories
+
+Tags/chips for each category. One can be starred as primary. X to remove. "+ Add" button opens a category picker dropdown.
+
+### Aspects
+
+Each applied aspect as a collapsible section. Section header shows aspect name + completeness indicator: `Thread (2/4)` — filled/total params. Color: green=complete, orange=partial, gray=empty. X button to remove aspect.
+
+Inside: parameter rows with:
+- Parameter name, value (editable), unit label
+- **Filter funnel icon** — clicking adds a `Param: Value` pill to the left panel. Only shown when value is non-empty. This is the only mechanism for adding parameter filters.
+
+Below aspects: "+ Apply Aspect" button with dropdown of available aspects.
+
+### Standalone Parameters
+
+Section below aspects. Same row layout (name, value, unit, filter icon). "+ Add Parameter" button to attach a parameter definition.
+
+### Locations
+
+"Stored at" section. Location paths as clickable links (navigate to `/navigator` with that location focused). Assignment type badge (placed/provisional).
+
+### Actions
+
+- Delete item — immediate with undo toast
+
+---
+
+## Item Creation
+
+Floating action button (`+ Item`) bottom-right of center panel. Creates a new item, selects it, focuses the name field. Item saves immediately with just a name. All other fields populated via detail panel or inline grid editing.
+
+---
+
+## Data Fetching
+
+### Rich Item Endpoint
+
+`GET /api/items` returns items with taxonomy data included — categories, applied aspects, and parameter values. Supports query parameters:
+- `q` — text search across name, description, parameter values
+- `filter` — parameter value filters (AND logic)
+- `sort` — column and direction
+- `category` — category filter
+
+All filtering, searching, and sorting is executed at the database level.
+
+### Mutations
+
+Individual API calls for each mutation (update name, set parameter value, add category, apply aspect, etc.). Optimistic updates in the UI.
+
+---
+
+## Cross-Cutting Patterns
+
+### Completeness Indicators
+
+Aspect sections show filled/total parameter count. Supports "get items in fast, refine later."
+
+### No Right-Click
+
+All actions via visible UI elements. See [ui-paradigms.md](ui-paradigms.md).
+
+### Undo, Not Confirm
+
+Destructive actions execute immediately with undo toast.
+
+### Item Reachability
+
+Every item reference in the app links to `/items?selected={id}`.
+
+---
+
+## Deferred
+
+- Split/merge items (UC-4, UC-5)
+- Bulk parameter edit across selected items (UC-8)
+- Progressive refinement / dedup detection (UC-7)
+- AI-assisted item creation
+- Saved/named views (filter + column + sort configurations)
+- Multi-item comparison view in detail panel
+- Dynamic column prevalence calculation
+- Pagination / virtual scrolling strategy
diff --git a/specification/item-parametric-model.md b/specification/item-parametric-model.md
new file mode 100644
index 0000000..073e986
--- /dev/null
+++ b/specification/item-parametric-model.md
@@ -0,0 +1,307 @@
+# Item Parametric Model — Design Discussion
+
+## First Principle
+
+**Parameters are elemental physical properties, globally scoped, with no domain semantics.** Pitch is pitch — whether on a propeller, a screw thread, or a pipe fitting. Length is length. Mass is mass. A parameter definition describes a measurement, not a use case.
+
+Domain constraints (pipes don't come in 80 TPI) are properties of the domain, not of the parameter. Those constraints live in standards. Search narrowing by domain uses categories and aspects as filters layered on top of global parameter queries.
+
+**Designations are the primary affordance, not standards.** Users pick "M3" or "#8-32" or "0603" — they don't pick "ISO 261" or "JEDEC." Standards are backstage plumbing: they power the lookup tables that resolve a designation to parameter values. The standard name is available as metadata (for reference, filtering, disambiguation) but is never required knowledge for ordinary use.
+
+---
+
+## The Pattern
+
+Physical goods are manufactured to standards. A standard defines a table of valid designations, each mapping to a set of parameter values. Picking a designation determines those values.
+
+Examples:
+- UNC threading: designation "#8-32" → pitch=32 TPI, major_dia=0.164", minor_dia=0.1302"
+- SMD packages: designation "0603" → length=1.6mm, width=0.8mm, height=0.45mm (NOTE: SMD packages exist in both imperial and metric designations — there's a 0603M package also)
+- Wire gauge: designation "18 AWG" → diameter=1.024mm, resistance=20.95 Ω/km, ampacity=16A (NOTE: Wire gauges also exist in metric, indicating cross-sectional area in mm²)
+- Bearings: designation "6201" → bore=12mm, OD=32mm, width=10mm
+- O-rings: designation "AS568-210" → ID=19.99mm, cross_section=3.53mm
+
+The pattern: **standard + designation → parameter values**.
+
+---
+
+## Core Concepts
+
+### Parameter Definition
+
+The atomic unit. A physical measurement: name, dataType, unit. System-wide, unique by name. No domain semantics, no aspect scoping.
+
+Examples: pitch (numeric, mm/thread), length (numeric, mm), major_diameter (numeric, mm), resistance (numeric, Ω/km), tapered (boolean), drive_type (enum: cross, slotted, hex, square, star, spanner, ...), drive_system (enum: Phillips, Pozidriv, Torx, Robertson, ...), drive_size (numeric, mm).
+
+Each parameter definition declares a **canonical unit** — the single unit used for storage and comparison, always SI. All values are normalized to SI internally, enabling global queries ("all items with length < 10mm") without unit conversion at query time.
+
+Parameters have optional constraints (min, max, enumValues) that reflect physical limits of the measurement itself, not domain-specific valid ranges.
+
+### Value Representation
+
+Numeric parameter values carry three fields:
+
+```
+{
+ "value": 12.7, // canonical, in the parameter's declared unit (mm)
+ "source_value": "1/2", // as entered or as defined by the standard
+ "source_unit": "in" // the original unit system
+}
+```
+
+**`value`** is what the system queries, compares, and indexes. It is always in the parameter's canonical unit.
+
+**`source_value` + `source_unit`** preserve the original representation. "1/2 inch pipe" displays as "1/2″" — not "12.7mm" — because that's how the domain identifies it. The source fields are display-only; they never participate in search or comparison.
+
+This applies everywhere parameter values appear:
+- `item_parameter_values.value` (JSONB) stores the compound representation
+- `standard_designations.values` (JSONB) stores compound values per parameter in the lookup table
+
+Trade designations that aren't real measurements (pipe nominal sizes, wire gauge numbers) are handled by making the designation string itself the human label. The parameter values behind it are the actual physical measurements. "1/2 inch pipe" is the designation; the `values` contain OD=0.840", ID=0.622" in canonical units with source representations preserved.
+
+Non-numeric parameters (boolean, text, enum) store `value` only — no unit conversion applies.
+
+### Unit Conversion
+
+Each parameter definition declares a canonical unit (always SI). Source values in other units must be converted before storage. Conversions fall into three categories:
+
+**Direct scaling** — multiply by a constant.
+- inches → mm: `value * 25.4`
+- feet → m: `value * 0.3048`
+- ounces → grams: `value * 28.3495`
+- mil (thou) → mm: `value * 0.0254`
+
+**Inverse conversion** — the source unit measures the reciprocal of the canonical unit.
+- TPI (threads per inch) → mm/thread: `25.4 / value`
+- Gauge numbers (AWG) → mm diameter: lookup table (non-linear, no formula)
+
+TPI is a count-per-length unit; the canonical SI representation of pitch is distance-per-thread (mm). A #8-32 screw at 32 TPI stores `pitch.value = 0.79375` (mm/thread), with `source_value = "32"`, `source_unit = "TPI"`. Searching "pitch < 1mm" returns fine-thread fasteners regardless of whether they were entered as TPI, mm, or metric pitch.
+
+**Non-linear formula** — a formula exists but it is not a simple ratio.
+- AWG → diameter: `diameter_mm = 0.127 × 92^((36 - n) / 39)` (geometric progression)
+- Cross-section and resistance derive from diameter.
+- AWG gauge numbers are not measurements — they are designation labels. But unlike truly arbitrary designations, the underlying values are computable.
+
+The parameter definition stores which conversion category applies. The system must know how to round-trip: given `source_value` + `source_unit`, compute `value`; given `value` + `source_unit`, recover `source_value` for display. For inverse conversions, the formula is its own inverse. For lookup-only, both directions require the table.
+
+### Aspect
+
+A domain grouping that references parameter definitions and optionally contains standards. Aspects define the interface — "these are the parameters that describe this facet of an item."
+
+Examples:
+- "Machine Screw Threading" → parameters: pitch, major_dia, minor_dia, thread_angle, class_of_fit
+- "Pipe Threading" → parameters: pitch, major_dia, minor_dia, thread_angle, tapered
+- "SMD Package" → parameters: length, width, height, lead_count
+- "Fastener Drive" → parameters: drive_type, drive_system, drive_size
+An aspect can have zero standards (freeform — user enters values manually) or multiple standards (user picks one, values cascade from lookup).
+
+### Standard
+
+A named classification system belonging to an aspect. Carries a lookup table of designations → parameter values. The standard provides the domain constraints — which combinations of values are valid.
+
+Examples:
+- Standard "UNC" (belongs to "Machine Screw Threading") → designations: #4-40, #6-32, #8-32, ...
+- Standard "NPT" (belongs to "Pipe Threading") → designations: 1/8"-27, 1/4"-18, 1/2"-14, ...
+- Standard "ISO 261" (belongs to "Machine Screw Threading") → designations: M3x0.5, M4x0.7, ...
+- Standard "JEDEC" (belongs to "SMD Package") → designations: 0603, 0805, SOT-23, SOT-223, ...
+- Standard "Phillips" (belongs to "Fastener Drive") → designations: #0, #1, #2, #3, #4
+- Standard "Torx" (belongs to "Fastener Drive") → designations: T6, T8, T10, T15, T20, T25, ...
+
+A standard's parameters are a subset of its parent aspect's parameters. The standard may not cover all of them — uncovered parameters remain user-entered.
+
+### Designation
+
+A specific entry within a standard. Maps to concrete parameter values.
+
+- Belongs to one standard
+- Has a display name (the designation string) — this is the trade label, which may not be a real measurement (e.g., "1/2 inch" for pipe nominal size)
+- Carries a values map: parameter_definition_id → compound value ({ value, source_value, source_unit })
+
+---
+
+## Hierarchy
+
+```
+Aspect: Machine Screw Threading
+ ├─ parameters: pitch (mm), major_dia (mm), minor_dia (mm), thread_angle (°), class_of_fit (enum)
+ ├─ Standard: UNC
+ │ ├─ #4-40 → { pitch: {v:0.635, src:"40", su:"TPI"}, major_dia: {v:2.845, src:"0.112", su:"in"}, ... }
+ │ ├─ #8-32 → { pitch: {v:0.794, src:"32", su:"TPI"}, major_dia: {v:4.166, src:"0.164", su:"in"}, ... } // 25.4/32 = 0.794 mm/thread
+ │ └─ ...
+ ├─ Standard: UNF
+ │ └─ ...
+ └─ Standard: ISO 261
+ ├─ M3x0.5 → { pitch: {v:0.5, src:"0.5", su:"mm"}, major_dia: {v:3.0, src:"3.0", su:"mm"}, ... }
+ └─ ...
+
+Aspect: Pipe Threading
+ ├─ parameters: pitch (mm), major_dia (mm), minor_dia (mm), thread_angle (°), tapered (boolean)
+ ├─ Standard: NPT
+ │ ├─ 1/2"-14 → { pitch: {v:1.814, src:"14", su:"TPI"}, major_dia: {v:21.336, src:"0.840", su:"in"}, tapered: true }
+ │ └─ ...
+ └─ Standard: BSPT
+ └─ ...
+
+Aspect: SMD Package
+ ├─ parameters: length (mm), width (mm), height (mm), lead_count (numeric)
+ ├─ Standard: JEDEC
+ │ ├─ 0603 → { length: {v:1.6, src:"0603", su:"JEDEC"}, width: {v:0.8}, height: {v:0.45}, lead_count: {v:2} }
+ │ ├─ SOT-23 → { length: {v:2.9}, width: {v:1.3}, height: {v:1.0}, lead_count: {v:3} }
+ │ └─ ...
+ └─ Standard: IPC
+ └─ ...
+
+Aspect: Fastener Drive
+ ├─ parameters: drive_type (enum), drive_system (enum), drive_size (mm)
+ ├─ Standard: Phillips
+ │ ├─ #0 → { drive_type: "cross", drive_system: "Phillips", drive_size: {v:2.0, src:"#0"} }
+ │ ├─ #2 → { drive_type: "cross", drive_system: "Phillips", drive_size: {v:5.0, src:"#2"} }
+ │ └─ ...
+ ├─ Standard: Torx
+ │ ├─ T10 → { drive_type: "star", drive_system: "Torx", drive_size: {v:2.74, src:"T10"} }
+ │ ├─ T25 → { drive_type: "star", drive_system: "Torx", drive_size: {v:4.43, src:"T25"} }
+ │ └─ ...
+ └─ Standard: Pozidriv
+ ├─ #2 → { drive_type: "cross", drive_system: "Pozidriv", drive_size: {v:5.0, src:"#2"} }
+ └─ ...
+```
+
+Note: "pitch" and "major_dia" appear in multiple aspects. They are the same parameter definitions — not copies. Values on items are stored once per parameter per item, globally scoped. Canonical units are SI (mm for pitch as distance between threads, mm for diameters, ° for angles). Source representations preserve the original domain notation (TPI, inches) for display.
+
+---
+
+## Proposed Schema
+
+```
+standards
+ id uuid PK
+ name text UNIQUE NOT NULL -- "UNC", "ISO 261", "AWG"
+ aspect_id uuid FK → aspects -- the aspect this standard belongs to
+ description text
+ created_at timestamp
+ updated_at timestamp
+
+standard_parameters
+ id uuid PK
+ standard_id uuid FK → standards
+ parameter_definition_id uuid FK → parameter_definitions
+ role text NOT NULL -- "key" | "derived" | "info"
+ sort_order integer
+ UNIQUE(standard_id, parameter_definition_id)
+
+standard_designations
+ id uuid PK
+ standard_id uuid FK → standards
+ designation text NOT NULL -- "#8-32", "M3x0.5", "0603"
+ values jsonb NOT NULL -- { param_def_id: { value, source_value, source_unit }, ... }
+ metadata jsonb -- notes, aliases, cross-references
+ UNIQUE(standard_id, designation)
+
+item_standards
+ id uuid PK
+ item_id uuid FK → items
+ standard_id uuid FK → standards
+ designation_id uuid FK → standard_designations -- NULL if non-standard/custom
+ is_custom boolean DEFAULT false -- true if user overrode derived values
+ created_at timestamp
+ UNIQUE(item_id, standard_id)
+```
+
+Existing tables unchanged:
+- `parameter_definitions` — atomic specs, globally scoped
+- `aspects` — domain groupings referencing parameters
+- `aspect_parameters` — join: which parameters belong to which aspects
+- `item_aspects` — aspect applied to item
+- `item_parameter_values` — actual values, stored per item per parameter as compound representation ({ value, source_value, source_unit }). Populated manually or from standard lookup.
+
+---
+
+## User Flow
+
+### Applying an aspect with standards
+
+1. User selects item, clicks "Add Aspect"
+2. Picker shows available aspects
+3. User selects "Machine Screw Threading"
+4. Aspect is applied. System shows its parameters with empty value slots.
+5. System also shows available standards for this aspect: UNC, UNF, ISO 261
+6. User picks "UNC" → designation picker appears with searchable list
+7. User picks "#8-32" → system fills derived parameter values from lookup
+8. Uncovered parameters (class_of_fit) remain for manual entry
+9. User can override any derived value — system flags it as custom
+
+### Applying a freeform aspect
+
+1. User selects "Physical Dimensions"
+2. No standards available — empty parameter slots appear
+3. User fills in length, width, height manually
+
+### Search
+
+- "All items with pitch < 10" → global parameter query, returns pipes and screws
+- "All items with pitch < 10 in category Fasteners" → parameter + category filter
+- "All items with Machine Screw Threading aspect and pitch < 10" → parameter + aspect filter
+- "All UNC #8-32 items" → standard + designation filter
+
+---
+
+## Access Control Boundaries
+
+The model must support tiered access at the data layer:
+
+- **Standard names, designation strings, parameter definition names**: low-privilege data. Required for search and identification.
+- **Structured parameter values within a designation (the `values` JSONB)**: high-privilege data. The parametric breakdown is the catalog's deep value.
+- **Full designation tables (all entries for a standard)**: must never be returned in a single API call. Pagination and per-account rate limiting required regardless of privilege tier.
+
+The `values` JSONB on `standard_designations` is a discrete, gatable field. The API layer can return designation records with or without it based on caller privilege.
+
+---
+
+## Resolved Questions
+
+### 1. Parameter value storage for standard-derived values
+
+**Leaning Option B** — computed at read time from the designation reference. Lookup updates propagate automatically. Revisit in detail during implementation; may need caching or materialized views for performance.
+
+### 2. Compound designations
+
+Decompose in the data model. "M3x0.5x10" is two standards applied to one item: threading (M3x0.5) + geometry (10mm length). Reconstitute for display — the user sees "M3x0.5x10" but the system stores two separate standard/designation pairs.
+
+### 3. Standard families
+
+Flat with a domain tag. UNC and UNF both tagged "Unified Thread Standard" but no parent/child hierarchy. Can revisit if grouping becomes necessary.
+
+### 4. Designation aliases
+
+No alias table. AI handles fuzzy matching of "#8-32" / "8-32" / "#8-32 UNC" / "No. 8-32" at the search/input layer.
+
+### 5. Lookup table population
+
+Out of scope for this spec. Pipeline design (system seeds, admin entry, community contribution, bulk import) is a separate concern.
+
+### 6. Cross-standard equivalence
+
+No equivalence mappings in the data model. If cross-standard matching becomes necessary (e.g., "#8-32 UNC" ≈ "M4x0.7"), it belongs in the AI layer.
+
+---
+
+## What This Means for Existing Code
+
+### Migration path
+
+- `aspects` and `aspect_parameters` unchanged
+- New tables: `standards`, `standard_parameters`, `standard_designations`, `item_standards`
+- `item_parameter_values` unchanged — both standards and aspects write to it
+- Existing seed data: "Threading" aspect stays, gains standards beneath it. "Dimensions" stays freeform.
+
+### UI impact
+
+- Item detail: aspects now optionally show available standards with designation pickers
+- Taxonomy admin: new Standards section for creating standards and managing lookup tables within aspects
+- Search: global parameter queries + category/aspect narrowing
+
+### API impact
+
+- New endpoints: `/api/standards`, `/api/standards/[id]/designations`
+- Item endpoints: apply standard + designation to item
+- Search: filter by parameter values globally, narrow by category/aspect/standard
diff --git a/specification/item-taxonomy.md b/specification/item-taxonomy.md
new file mode 100644
index 0000000..b6a348f
--- /dev/null
+++ b/specification/item-taxonomy.md
@@ -0,0 +1,141 @@
+# Item Taxonomy
+
+How WhereTF classifies, describes, and organizes items. The goal is fast, reliable identification — if you can't find it, it may as well not exist.
+
+---
+
+## Design Principles
+
+Hierarchical classification forces a dimension choice. Filing bank statements by date then account, or account then date — you can't know which query you'll need. Parametric description avoids this by describing items as a set of observable properties. Search across any combination of parameters without committing to a hierarchy.
+
+Categories are useful for quick human scanning but fail at boundaries — a spork is neither spoon nor fork, an LED is both optical and electronic. Categories in WhereTF are lightweight visual tags, not structural classification. The real identity of an item is its parameters.
+
+Wrong is better than empty. Approximate categorization reduces large set scan cost but may introduce errors.
+
+---
+
+## Category
+
+A broad, human-readable label used for visual grouping. Categories drive grid tile icons and color hints — a screw icon for fasteners, an SOIC glyph for ICs.
+
+- An item can have zero or more categories.
+- One category may be marked primary. The primary category drives the visual representation on grid tiles.
+- If no primary is set, no icon is shown. The cell still displays the item name.
+- Categories are a fixed system list and should be broad and shallow. Regular users do not create categories.
+- Categories are filterable in search but are not the primary search mechanism. The parametric data drives search; categories provide a quick narrowing filter.
+
+---
+
+## Parameter
+
+A named property with a typed value. Parameters are the atomic unit of item description.
+
+Examples:
+- Length: 14 mm
+- Color: red
+- Voltage rating: 50 V
+- Thread direction: right (enum)
+- RoHS compliant: true (boolean)
+
+### Parameter Definition
+
+A parameter definition prescribes the key name, data type, unit (if applicable), and constraints. Definitions are system-managed and reusable across items and aspects.
+
+- Name — the key (e.g., "Thread diameter", "Length", "Drive size")
+- Data type — numeric, text, boolean, enum (pick from a list)
+- Unit — optional, declares the measurement domain (mm, inches, volts, ohms). When a unit is declared, the parameter value is numeric and unit-aware. Entry in non-native units is supported with autoconversion (consistent with the storage model's unit handling).
+- Default value — optional. Pre-filled when the parameter is added to an item via an aspect. The user can accept or change it. No inheritance, no update propagation — just a starting value.
+- Constraints — optional restrictions on valid values:
+ - Enum values — a fixed list of valid options (e.g., Drive style: Phillips, Torx, Hex, Slotted)
+ - Numeric range — min/max bounds (e.g., Thread pitch: min 0.2, max 6.0)
+ - Required vs. optional — whether the parameter must have a value when the aspect is applied. Required means the system flags it as incomplete, not that it blocks saving.
+
+---
+
+## Aspect
+
+An aspect is a reusable group of parameter definitions that describes one facet of an item. Aspects are the core normalization mechanism — they prescribe what parameters an item should have, ensuring consistent description across similar items.
+
+(The Charm/Odoo addon called this concept a "class." We use "aspect" to avoid collision with inventory management and programming terminology.)
+
+An aspect defines:
+- Name — what facet it describes (e.g., "Thread", "Drive", "Head", "Package", "Material")
+- Parameter definitions — the set of parameters belonging to this aspect, each with its data type, default value, and required/optional flag
+- Description — what this aspect represents physically
+
+Aspects do not carry instance values. They define structure and defaults. Values are supplied when an aspect is applied to an item.
+
+### Applying Aspects
+
+An item gains parameters by applying one or more aspects. Each application copies the aspect's parameter definitions (with defaults) onto the item. The user fills in or adjusts the values.
+
+Multiple applications of the same aspect on one item (e.g., two Thread aspects on a pipe nipple) require a role to distinguish them. Role handling is deferred — the concept is noted here; the mechanism will be specified when pipe fittings and similar multi-aspect items are actively modeled.
+
+### Composition Example
+
+A machine screw:
+- Aspect: Thread
+ - Thread system: metric
+ - Thread diameter: M3
+ - Thread pitch: 0.5 (default: standard for M3)
+ - Thread direction: right (default)
+- Aspect: Drive
+ - Drive style: hex
+ - Drive size: 2.5 mm
+- Aspect: Head
+ - Head group: cylindrical
+ - Head name: socket head cap
+- Aspect: Material
+ - Material type: 18-8 stainless steel
+ - Finish: black oxide
+- Length: 10 mm (standalone parameter, not part of any aspect)
+
+An SMD resistor:
+- Aspect: Electrical
+ - Resistance: 10 kΩ
+ - Tolerance: ±1%
+ - Power rating: 0.125 W
+- Aspect: Package
+ - Package type: SMD
+ - Package code: 0805
+
+### Standalone Parameters
+
+Not every parameter belongs to an aspect. Some parameters are properties of the whole item, not of one facet — like Length on a fastener, which is the item-level dimension that typically varies across a product family.
+
+### Aspects Are Suggestions, Not Constraints
+
+Aspects prescribe what parameters should exist, but the system does not block an item that is missing parameters or has extra ones. An item can:
+- Have an aspect applied with some parameters left blank (incomplete but valid — flagged, not blocked)
+- Have parameters that don't belong to any applied aspect (ad-hoc description)
+- Have no aspects at all (fully ad-hoc, just loose parameters)
+
+Get items in fast, refine later.
+
+---
+
+## Item Families and Matrix Expansion
+
+Deferred. Items that share parameters across a product family (e.g., M3 SHCS in lengths 5, 6, 8, 10, 12, 14, 16, 20) need a creation and management mechanism. Whether this is an explicit family entity or derived from shared parameter signatures — and how shared-parameter edits propagate — requires further design.
+
+The parametric system and aspects defined here are the foundation for whatever family mechanism is chosen.
+
+---
+
+## Search
+
+Search is AI-driven. Users describe what they're looking for in natural language; the system interprets the query against the parametric data.
+
+The structure defined in this document — typed parameters, unit-aware values, aspects grouping related parameters — is what makes AI search effective. Without consistent, structured data, search degrades to fuzzy text matching. With it, the AI can resolve "M3 socket head" to Thread diameter=M3 + Head name=socket head cap, and "0805 resistor under 100kΩ" to Package code=0805 + Resistance < 100kΩ.
+
+Categories provide an additional narrowing filter but are not the primary search axis.
+
+Search design is specified separately.
+
+---
+
+## Relationship to Odoo/ERP
+
+Odoo requires every product to belong to exactly one category. When items sync to Odoo, WhereTF maps the primary category (or derives one from applied aspects) to Odoo's single-category requirement. The parametric richness stays in WhereTF; Odoo gets the simplified view it needs.
+
+Integration details are specified separately.
diff --git a/specification/location-tracker-ux-issues.md b/specification/location-tracker-ux-issues.md
new file mode 100644
index 0000000..78fc1bd
--- /dev/null
+++ b/specification/location-tracker-ux-issues.md
@@ -0,0 +1,215 @@
+# Location Tracker — UX Issues (living doc)
+
+Running list of issues and decisions for the `/modules` and `/modules/[id]` areas. Updated during review sessions. No fixes applied until explicitly approved.
+
+---
+
+## Global / navigation
+
+### GN-1 — Left toolbar expand/collapse
+- **Problem:** Toolbar shows icons only; menu item names not visible.
+- **Decision:** Add expanded mode (icon + name) and collapsed mode (icons only). Toggleable. Default: expanded. Persisted to localStorage.
+
+### GN-2 — Breadcrumb trail
+- **Problem:** No persistent location-path indicator as user navigates.
+- **Decision:** Always-visible breadcrumb at top of main content area, using brief display form from storage-model.md §Display Formats (e.g., `MUSE 1 / A3`).
+
+---
+
+## `/modules` (list page)
+
+### ML-1 — Module card editing mode
+- **Problem:** Card fields are inadvertently editable.
+- **Decision:** Whole card gated by edit mode. Entering edit mode reveals Save/Cancel + Delete. Outside edit mode, all fields are read-only.
+- **Note:** Card is the canonical display of a module as a first-class object. All module-level affordances stay with the card.
+- **TODO:** Add stats to card: `% occupied` (full locations / total locations), physical location hint (from metadata).
+
+### ML-2 — Module deletion (GitHub-repo pattern)
+- **Problem:** Single-click "Delete" is too dangerous even with undo.
+- **Decision:** Follow GitHub repo deletion UX.
+ 1. Delete button only visible in edit mode.
+ 2. Opens a dialog that first explains the module's contents: inserts placed, items assigned, locations to resolve. If non-empty, user must either *move* or *orphan* contents before proceeding.
+ 3. Once conditions met, user must type the module name to confirm deletion.
+- **Orphan semantics:** affected items become **unassigned** (their assignment records are removed). The deletion is recorded in the transaction log so it's reversible via undo.
+
+### ML-3 — "Add a module"
+- **Status:** Not actually missing. User was mistaken. `/modules/new` exists. No change.
+
+### ML-4 — Module editing lives on the detail page
+- **Decision:** `/modules` cards are **read-only**. List + add only, no edit/delete affordances on list cards. All module editing (and deletion per ML-2) happens in the right panel of `/modules/[id]`.
+- **TODO:** Stats still display on the list cards per ML-1 (% occupied, physical location hint) — just non-interactive.
+
+---
+
+## `/modules/[id]` (module detail page)
+
+### MD-1 — Add level control
+- **Problem:** No button to add a level when a module is open.
+- **Open:** Is "add level" just incrementing `primaryDimensionCount` and materializing the new level location, or are levels first-class records created individually? **Deferred.**
+
+### MD-2 — Parent (module) vs. children (levels) distinction
+- **Decision:** Module header on detail page is **non-interactive label text only**. All editing of the module itself happens in the right panel (per ML-4 revision). Levels are clearly the editable children of the module.
+
+### MD-3 — "1 level" copy on module header
+- **Problem:** Ambiguous text.
+- **Decision:** Eliminate the "N level" line from the module header on this page. The level list itself communicates the count.
+
+### MD-4 — Default level selection
+- **Problem:** No level selected by default; user must click one to see anything.
+- **Decision:** Auto-select a level on page load.
+ - Ideal: last-selected level for this module, persisted to localStorage (per-device).
+ - Fallback: first level.
+
+### MD-5 — Level rename and property editing
+- **Problem:** Unclear how to edit a level's name (e.g., rename "3" to "Power supplies") or set its properties.
+- **Decision:** Level editing lives in the **right panel** when a level is selected. Right panel also hosts module-level editing (per ML-4) and eventually stats + bulk actions.
+- **Level properties (initial):** label, locationType (receptacle / fixed / leaf), interfaceTypeAccepted, description, notes. Same edit-mode gating as the module card.
+
+---
+
+---
+
+## `/templates` and `/templates/[id]`
+
+### TP-1 — "New template" button missing
+- **Problem:** No entry point to create a template from the list page or detail page. `/templates/new` exists as a route.
+- **Status:** Capturing; needs clarification.
+
+### TP-2 — Delete a template
+- **Problem:** No affordance to delete a template.
+- **Decision:**
+ - If the template is **unreferenced** (no insert or location points at any of its versions): hard delete allowed via the same GitHub-repo pattern (type-to-confirm).
+ - If the template is **referenced**: hide instead of delete. A hidden template stays usable for existing inserts/locations but does not appear in pickers for new ones.
+- **Future:** Delete-with-move — on delete of a referenced template, offer to replace its usages with a different template before removing.
+- **Schema impact:** add `templates.isHidden boolean default false`. Picker/listing code filters it out. Existing inserts/locations resolve their templateVersion as normal.
+
+### TP-3 — No way back to templates list from detail
+- **Problem:** When viewing `/templates/[id]`, the only path back to the list is the sidebar menu button. Feels wrong.
+- **Direction (asked by user):** How do other UIs handle this?
+ - **Breadcrumb** (GitHub, GitLab, admin dashboards): `Templates › Plano 3600`. The crumb itself is the back-nav. This matches what we just added on `/modules/[id]` (GN-2).
+ - **Back arrow in page header** (iOS, Notion): explicit `← Templates` button at top-left of the detail page.
+ - **Sibling list kept visible** (master-detail on tablets / Notion sidebar / Finder): the list is a persistent left column, the detail fills the right. Click a different item, the right updates.
+- **Recommended MVP:** Breadcrumb (matches GN-2 and the conventions the user already agreed to). Add a back-arrow on narrow viewports as a bonus.
+- **Bigger picture (new from user):** Consider a "template editor" mode layered over the list — view the list, select/edit a template, return to the list. This is the *master-detail* / *stacked-navigation* pattern (Slack channels, Gmail labels, Xcode settings). Needs its own spec pass; deferred until TP-1/TP-2 land.
+
+### TP-4 — Template editor: master-detail layout
+- **Decision:** `/templates` becomes master-detail. List on the left, detail/editor on the right. Click a row → list stays visible, detail fills the right pane. URL reflects selection (`/templates?selected=`).
+- **Scope:** Templates only. Items and modules keep their current patterns.
+- **Supersedes TP-3:** With master-detail, there's no navigation-away, so the "how do I get back" problem disappears.
+- **Detail route:** `/templates/[id]` remains for deep-linking but becomes a thin redirect to `/templates?selected=[id]`.
+
+---
+
+---
+
+## Inserts
+
+### IN-1 — No `/inserts` page
+- **Problem:** Inserts are only manageable through the module that hosts them. No way to see all inserts across the system, no way to create an unplaced insert from the UI (API only).
+- **Decision:** Add a left-menu item **Inserts** → `/inserts`. List should be filterable **by type** (template) and **by interface type** (e.g. plano-3600, gridfinity-42mm) so the user can find "where does this bin fit?". Supports browsing placed + unplaced inventory.
+- **Open (secondary):** Master-detail layout like templates, or something else.
+
+### IN-3 — Module level UI conflates receptacle with its insert
+- **Problem:** On `/modules/[id]` a level like MUSE:1 shows grid cells (A1..D6) directly, hiding the fact that those cells belong to a specific **insert** (a physical instance of Plano 3600). User owns a *stack* of Plano 3600s; each is a distinct insert with its own name, overrides, and contents. The current UI doesn't name the insert in the level view and doesn't frame overrides as "on this insert" vs. "on this receptacle".
+- **Name ownership:** the insert's name is a property of the insert. The level's label is usually a sequential stub (1, 2, 3 or A, B, C). Renaming the insert *may* be offered as a convenience from the receptacle context, but authoritative edit happens on the insert itself.
+- **Direction:**
+ - Module level header should read something like: **MUSE 1** · receptacle · holds *"construction screws"* (Plano 3600 Stowaway)
+ - Or show a breadcrumb on the grid: MUSE › 1 › *construction screws* (Plano 3600)
+ - Overrides (merge/divide/disable/restrict) applied to cells *inside an insert* are insert-scoped — should persist as `inserts.overrides`, not on the location. They travel with the insert when relocated.
+ - Offer "Remove insert" and "Replace insert" at the receptacle level.
+- **Open:** For cells inside an insert we've been writing to `locations.{isDisabled,maxWidthMm,…}` which lives on the child location row. That works when the insert never moves, but the spec says overrides must travel with the insert. Needs the structured `inserts.overrides` JSONB format we deferred earlier. Punt to when we implement merge (which already has to deal with insert-scoped persistence).
+
+### IN-5 — "New insert" entry point
+- **Problem:** `/inserts` has no way to create a new, unplaced insert. Today the only way to get an insert is through the Place Insert wizard on a module level.
+- **Decision:** Add a `+ New` button on the `/inserts` list. Opens a small form (pick template, optional name) → creates an unplaced insert → auto-selects it in the master-detail pane.
+
+### IN-6 — Hide UID chrome on the inserts page
+- **Problem:** Insert UID is shown both in the list row and above the detail header. It's internal scaffolding — users don't want to see it in normal use.
+- **Decision:** Remove both. UID can resurface later as a dim footer detail on the insert page once RFID / label-printing workflows arrive.
+
+### IN-7 — Insert detail is the item↔insert central surface
+- **Problem:** The insert detail page currently shows metadata only. The user wants it to be the *primary* place to see an insert's layout, assign/unassign items to cells, and apply overrides (merge/unmerge/disable/restrict/divide). Module detail stays as a where-is-it view.
+- **Decision:**
+ - Insert detail renders the insert's cell grid (same renderer idea as module detail today).
+ - Click a cell → cell-detail side panel: assigned items CRUD, overrides (disable/restrict).
+ - Multi-select cells (Ctrl/Cmd-click) → Merge action in the selection summary.
+ - Single-cell actions on a merged cell → Unmerge.
+ - Single-cell "Divide…" → splits into named children.
+ - This supersedes the in-progress cell-edit affordances on the module detail page; module detail will eventually stay read-only on cells and link to the insert page for edits.
+- **Open — layout:**
+ - Current `/inserts` is master-detail (list + detail). Adding a full grid + cell detail means the detail pane needs more horizontal room.
+ - Options:
+ - (a) Keep master-detail; the right pane grows and the grid scrolls horizontally as needed
+ - (b) Full-page detail when a row is selected (collapse the list into a small drawer/header)
+ - (c) Hide the list on narrow screens, side-by-side on wide
+- **Open — module page overlap:**
+ - Keep the module detail's grid + cell controls, or strip cell interactions there and route users to the insert page for anything beyond viewing?
+- **Supersedes:** IN-2/3 Merge and IN-2/4 Divide now live here.
+
+### IN-4 — Placement from the insert side
+- **Problem:** Today placement is receptacle-first (go to a level, pick a template). User also wants insert-first: "I'm holding this Plano, find somewhere it fits." And also wants to **kick an insert out** from either side.
+- **Direction:**
+ - On `/inserts` detail for an unplaced insert: a "Place in…" button that lists compatible receptacles (filter by interface type match + currently empty).
+ - If already placed, show "Move to…" offering the same picker, plus "Unplace" (kick out without replacement).
+ - On `/modules/[id]` at a receptacle level that holds an insert: "Remove insert" (= unplace) and "Replace insert" (= unplace + reopen placement flow).
+- **Naming clarification (per user):** the compatibility name is the **interface type**. Insert template `interfaceTypeProvided` must match receptacle `interfaceTypeAccepted`.
+
+### IN-2 — Where do I edit an insert's overrides (merge / divide / disable / restrict)?
+- **Problem:** No UI exists for any of the four override types (see storage-model.md §Override Types). Schema supports:
+ - `locations.mergedIntoId` for merge aliasing on module-scoped locations
+ - `locations.isDisabled` + `disableReason` for disable on module-scoped locations
+ - `locations.maxWidthMm/maxHeightMm/maxDepthMm/restrictReason` for restrict (MVP)
+ - `inserts.overrides` JSONB for insert-scoped overrides (unstructured today — no validator)
+- **Gaps:**
+ - No API endpoint to apply an override to an insert
+ - No API endpoint to divide a location (materialize children)
+ - No UI for any of this
+- **Direction:** Overrides should be editable from the cell detail panel (right pane) on the module detail page. Multi-select a range of cells → "Merge" shows up in the panel. Right-click or action menu on a single cell → "Disable" / "Restrict height" / "Divide".
+- **Status:** Deferred. Big feature area; needs its own spec pass before implementation. Related to IN-1 (if inserts get a dedicated UI, insert-scoped overrides may live there too).
+
+---
+
+---
+
+## Navigation
+
+### NV-1 — Admin section in left menu
+- **Problem:** Operations that structurally change the workshop (creating/removing modules, creating/removing/hiding templates) are mixed in with everyday navigation.
+- **Decision:** Group admin-style entries in a distinct section (visually separated) in the left menu. At minimum: modules admin, templates admin. Maybe taxonomy admin belongs there too.
+- **Open:** Does this mean separate routes (`/admin/modules`, `/admin/templates`) or the same routes with read/admin modes? Probably same routes, just the menu grouping communicates intent.
+
+---
+
+## Place Insert flow
+
+### PI-1 — "Next" button is off-screen at bottom of template list
+- **Problem:** On `/modules/[id]/levels/[levelId]/place-insert`, the Next button lives at the bottom of the template list. With more than a handful of templates, it's below the fold and feels undiscoverable.
+- **Direction (likely):** Move primary action (Next / Place) to a sticky footer bar, or a fixed header action, independent of the scroll position of the list.
+
+---
+
+### HX-1 — Assignment history (cross-cutting)
+- **Problem:** there's no surfaced history of who/what lived where before the current state. "Previously at this receptacle," "this item used to be in A3," etc. Comes up per-receptacle (previous inserts), per-insert (previous receptacles), per-cell (previous items), per-item (previous locations).
+- **Direction (deferred):** leverage the existing `transactions` log (already records beforeState/afterState for every mutation). Build a per-entity history view that filters the log by entity id and renders as a timeline. A clean history UX inherently enables undo — each transaction entry is a reversible delta.
+- **Scope:** applies to assignments, inserts, cells, receptacles, items. Would replace the current ad-hoc per-feature undo plans (e.g. module delete cascade) with a single transaction-log-driven pattern.
+- **Status:** future feature. Note it here so it doesn't get re-invented per-entity.
+
+### IN-8 — Smart subdivision label suggestions
+- **Problem:** divide dialog accepts any comma-separated strings. User pointed out the template already knows enough to suggest the right terms. A drawer's front/back axis, a shelf's left/right, or a template-declared subdivision accessory (e.g. Akro-Mils 40716 divider → front + rear) all imply better defaults.
+- **Direction:**
+ - If the cell's template version has a non-empty `subdivisionOptions` JSONB (already a schema field), populate a dropdown of those as the first UI offering. User picks one → children created with predeclared labels. "Custom…" route stays available.
+ - If no subdivision option exists: fall back to a heuristic based on cell orientation (aspect ratio + primary axis + template kind) to propose `left, right` vs `front, rear` vs `top, bottom` as the placeholder. User can still type anything.
+- **Work:** backend already has the JSONB field, nothing to add there. Need: TS type for the JSONB shape, a small helper that picks a suggestion, UI refactor from single text input to "dropdown of presets + custom" component.
+- **Status:** deferred — not trivial (heuristic wants thought, options schema wants a type, UI wants a real picker).
+
+---
+
+## Cross-cutting open questions
+- **MD-1** add-level semantics — refined: on a module, ISBAT insert a new
+ level *before* or *after* an existing level X. No drag-to-reorder
+ (too easy to foot-gun). Typically an uncommon operation since
+ module structure doesn't change often.
+- **IN-2** override UX — all 4 done (Disable, Restrict, Merge, Divide)
+- **IN-3** module level header now surfaces the insert (done)
+- **IN-4** insert-first placement (done)
+- **IN-8** smart subdivision label suggestions from template
diff --git a/specification/project-intent.md b/specification/project-intent.md
new file mode 100644
index 0000000..a25ffed
--- /dev/null
+++ b/specification/project-intent.md
@@ -0,0 +1,38 @@
+# WhereTF — Project Intent
+
+R&D workshop item tracker. Users model their physical storage layout, catalog items, and get help storing, finding, and organizing their stuff. AI-assisted natural language cataloging is a feature layer, not the foundation — build core storage and item management first. Single-user for initial implementation. Designed for future multi-user, multi-tenant (users belong to orgs); item data will be global (shared across orgs) in the multi-tenant version. Not inventory management — no quantities, BOMs, or stock transfers.
+
+## Interaction Model
+
+GUI and AI each own different concerns:
+
+- **Storage layout definition** — GUI-first. Users build module/level/grid structures visually. Templates (e.g. Plano Stowaway 3600) accelerate setup. Minimal AI involvement.
+- **Item cataloging** — AI-first. User describes items in natural language, AI structures into canonical form with deduplication. Goal: build a valuable item identity DB where network effects improve matching over time.
+- **Item assignment** — AI-first. User says where they're putting something, or asks for a suggestion. AI considers access frequency (tracked implicitly via search/retrieve actions) and storage accessibility.
+- **Search** — AI-first. Natural language queries, results displayed as a list with corresponding locations highlighted in the storage GUI.
+- **Housekeeping** — Bulk reorganization ("defrag"): combine like items, suggest discards, move frequently-used items to accessible locations.
+- **ERP integration** — Items link to ERP products (e.g. Odoo). WhereTF is the R&D complement, not a replacement for MRP/stock.
+
+## Domain Concepts
+
+- **Item** — what a thing *is*, independent of where it is. A type/category, not an instance or count. Thorough, structured item characterization is core to WhereTF — finding an item requires describing it well. Items are described by name and typed parameters organized by aspects (reusable parameter groups). See [item-taxonomy.md](item-taxonomy.md) for the classification system. Equivalent to a product in ERP. Items belong to WhereTF globally — as items are refined and improved, they benefit all users. Storage and assignments are per-org; items are shared. Future: private items as a paid feature.
+- **Assignment** — connects an item to a location. Own entity, not a field on item or location. Either *placed* (specific leaf location, one per location unless co-storable) or *provisional* (at a location, position undetermined). Many assignments per item. Unassigned items and empty locations are both valid states.
+- **Module** — a top-level, independent physical storage unit (cabinet, shelf, drawer unit). Never nested. Defines valid location path structures.
+- **Template** — versioned blueprint for a storage product's layout (e.g. Plano Stowaway 3600 = 4×6 grid). Applied via inserts (receptacle locations) or directly (fixed locations). Instances pin to an applied version.
+- **Insert** — a distinct physical object that occupies a receptacle and provides its own internal locations. Relocatable as a unit.
+- **Interface type** — named physical contract governing insert/receptacle compatibility. Strictly validated on placement.
+- **Location path** — hierarchical address within a module. Module names are short identifiers, not descriptions. Descriptions belong in metadata.
+
+## AI Agent Model
+
+Router/specialist pattern. Router classifies intent, delegates to specialists. Specialists have scoped access to domain operations. The system loops tool calls until a text response is produced.
+
+## Context Management
+
+Sessions track conversation history with token estimation. Context thresholds trigger compression — summarize the conversation, archive it, and continue in a new session with the summary as context.
+
+## What WhereTF Is Not
+
+- Not inventory management (no quantities, BOMs, stock transfers)
+- Not MRP (no purchase orders, suppliers, lead times)
+- Not a catalog (items are user-defined, not sourced from a product database — though network effects may build one over time)
diff --git a/specification/storage-definition-design.md b/specification/storage-definition-design.md
new file mode 100644
index 0000000..e719ce8
--- /dev/null
+++ b/specification/storage-definition-design.md
@@ -0,0 +1,461 @@
+# Storage Definition — UI/UX Specification
+
+Defines the workflows for creating and configuring physical storage structures: modules, levels, templates, and inserts. This is the setup phase — building the scaffold before items get assigned.
+
+Complements [storage-navigator-design.md](storage-navigator-design.md) (browsing and interacting with storage) and [storage-model.md](storage-model.md) (data model reference).
+
+---
+
+## Scope
+
+In scope:
+- Module CRUD (create, view, edit, delete)
+- Level/drawer generation and per-level configuration
+- Template CRUD and version publishing
+- Insert creation and placement into receptacles
+- SVG grid visualization as structural confirmation
+- Associating templates with receptacle locations
+
+- Overrides: merge, divide, disable (grid-interactive operations)
+- Continuous-dimension locations (louver panels, open shelves)
+
+Out of scope (covered elsewhere or deferred):
+- Item assignment to locations (storage-navigator-design)
+- Drag-and-drop insert relocation (future)
+- Template community catalog (future)
+
+---
+
+## Navigation
+
+Module management lives at `/modules`. Accessed from sidebar icon rail.
+
+```
+/modules — module list
+/modules/new — module creation wizard
+/modules/:id — module detail (level table + grid preview)
+/modules/:id/levels/:id — level detail (insert config, grid view)
+/templates — template list
+/templates/new — template creation
+/templates/:id — template detail + version history
+```
+
+---
+
+## Module List (`/modules`)
+
+Card grid. Each card shows:
+- Module name (prominent)
+- Description (one line, truncated)
+- Primary dimension summary: "11 levels" or "9 drawers"
+- Occupancy bar — simple fill indicator (assigned locations / total leaf locations)
+
+**Actions:**
+- Card click → navigate to module detail
+- "New Module" button (top-right, prominent) → navigate to creation wizard
+
+**Empty state:** "No modules yet. Create your first storage module to start organizing."
+
+**Sort:** by name (default), by recently modified, by occupancy
+
+---
+
+## Module Creation Wizard (`/modules/new`)
+
+Multi-step form. Not a modal — a full page. Modules are created infrequently, so a deliberate process is appropriate.
+
+### Step 1: Identity
+
+- **Name** — short identifier (e.g., "MUSE", "ALEX"). Required.
+- **Description** — what this module physically is (e.g., "Red metal cabinet, 11 shelf levels, under workbench"). Optional.
+
+### Step 2: Primary Dimension
+
+- **Dimension label** — what are the top-level subdivisions called? Freeform text with suggestions: "level", "drawer", "shelf", "row", "bay". Required.
+- **Count** — how many? Numeric input, minimum 1. Required.
+- **Preview** — as the user types, show a vertical stack diagram of the generated levels with auto-labels. Labels follow the convention: sequential numbers (1, 2, 3...) by default.
+
+### Step 3: Level Configuration
+
+Table of the generated levels. Columns:
+- **Label** — editable (default: "1", "2", "3"...)
+- **Type** — dropdown: "receptacle" (default) or "fixed"
+- **Notes** — freeform text, optional
+
+All levels start as receptacles. The user can change individual levels or multi-select + batch apply.
+
+**Batch operations** (via multi-select checkboxes):
+- Set type (receptacle/fixed) for selected levels
+- Set notes for selected levels
+
+### Step 4: Review & Create
+
+Summary card showing:
+- Module name and description
+- Dimension label and count
+- Level configuration table (read-only)
+
+"Create Module" button. On success → navigate to module detail page.
+
+---
+
+## Module Detail (`/modules/:id`)
+
+Two-panel layout within the main content area (sidebar remains).
+
+### Left: Module Info + Level Table
+
+**Module header** — name (editable inline), description (editable inline), dimension summary.
+
+**Level table** — all levels for this module. Columns:
+- Label
+- Type (receptacle / fixed)
+- Insert (name of placed insert, or "—" if empty)
+- Status (active / disabled + reason)
+- Occupancy (filled / total leaf locations, or "—" if no sub-structure)
+
+Row click → selects level, updates right panel.
+
+**Actions on module:**
+- Edit name/description (inline)
+- Add levels (append to end)
+- Delete module (immediate with undo toast — per ui-paradigms.md, no confirmation dialog)
+
+**Actions on level (via row selection or level detail):**
+- Place insert (if receptacle and empty)
+- Remove insert (if receptacle and occupied)
+- Configure fixed structure (if fixed type)
+- Disable / enable
+- Delete level
+
+### Right: Level Preview
+
+When a level is selected:
+- If the level has sub-structure (insert placed or fixed template applied) → **SVG grid preview** showing the position layout. Labels on axes (rows alpha, columns numeric). Cells show occupancy state (empty, occupied, disabled). Read-only in this context — clicking a cell does nothing here (that's the navigator's job).
+- If the level is an empty receptacle → "No insert placed. Place an insert to define this level's internal structure." with a "Place Insert" button.
+- If the level is an empty fixed location → "No structure defined. Apply a template to define this level's layout." with an "Apply Template" button.
+
+When no level is selected → "Select a level to view its layout."
+
+---
+
+## Place Insert Flow
+
+Triggered from level detail when the user clicks "Place Insert" on an empty receptacle.
+
+### Step 1: Choose Template
+
+Searchable list of templates. Each row shows:
+- Template name
+- Type (fixed / parametric)
+- Dimensions (e.g., "4 rows × 6 columns")
+- Version number
+
+Click a template → shows a grid preview of the template's positions below the list.
+
+### Step 2: Configure (parametric templates only)
+
+If the template is parametric, the user specifies dimensions:
+- Grid size (e.g., 6 × 4 for a Gridfinity baseplate)
+- Constrained by template's min/max
+
+Live grid preview updates as dimensions change.
+
+Fixed templates skip this step.
+
+### Step 3: Name & Confirm
+
+- **Insert name** — defaults to template name + auto-incrementing number (e.g., "Plano 3600 #4"). Editable.
+- Grid preview showing the final layout within the level
+- "Place Insert" button
+
+On confirm:
+1. Insert record created (references template + version)
+2. Child locations generated from template positions
+3. Receptacle's compatibility is set by cloning the template's interface data
+4. Level preview updates to show the new grid
+5. Notification: "Plano 3600 #4 placed in MUSE Level 3"
+
+---
+
+## Apply Template to Fixed Location
+
+Similar to Place Insert, but for fixed locations. The template is applied permanently — no insert record, locations are created directly as children of the fixed location.
+
+Same step sequence (choose template → configure if parametric → confirm), but:
+- No insert name (there's no insert object)
+- Messaging reflects permanence: "Apply Template" instead of "Place Insert"
+- Notification: "Gridfinity 6×4 applied to ALEX Drawer 3"
+
+---
+
+## Template List (`/templates`)
+
+Table view. Columns:
+- Name
+- Type (fixed / parametric)
+- Current version
+- Dimensions (rows × columns for latest version)
+- Instance count (how many inserts + fixed applications use this template)
+
+**Actions:**
+- Row click → navigate to template detail
+- "New Template" button → navigate to template creation
+
+**Empty state:** "No templates defined. Create a template to define reusable storage layouts."
+
+---
+
+## Template Creation (`/templates/new`)
+
+Single-page form (not a wizard — templates are simpler than modules).
+
+**Fields:**
+- **Name** — e.g., "Plano 3600 Stowaway". Required.
+- **Description** — optional.
+- **Type** — fixed or parametric. Required.
+- **Rows** — number. For parametric: this is the default, with min/max constraints.
+- **Columns** — number. Same as rows.
+- **Min/Max rows** (parametric only)
+- **Min/Max columns** (parametric only)
+- **Row labels** — radio: alpha (A, B, C…) or numeric (1, 2, 3…). Default: alpha. Row and column labels must differ — validation prevents selecting the same type for both.
+- **Column labels** — radio: numeric or alpha. Default: numeric.
+- **Origin** — dropdown: top-left (default), top-right, bottom-left, bottom-right. Controls which corner the label sequence starts from. Changing origin reverses the label rendering order on affected axes (e.g., bottom-left → row A is at the bottom, column 1 is at the left).
+- **Row dividers** — checkbox: fixed (checked) or removable (unchecked). Default: removable. Fixed means permanent physical dividers — merging across rows is blocked. Example: Plano 3600 row walls are molded plastic.
+- **Column dividers** — checkbox: fixed or removable. Default: removable. Independent of row dividers. Example: Plano 3600 column dividers are removable tabs.
+- **Unit system** — radio: imperial or metric. Default: metric.
+
+**Live preview** — SVG grid updates as the user changes any property. Shows labels on axes with correct origin ordering. Fixed dividers render as thick lines between cells. Origin marker (accent triangle) appears in the label gutter corner outside cells.
+
+"Create Template" button. Creates the template with version 1 containing the specified configuration.
+
+---
+
+## Template Detail (`/templates/:id`)
+
+Three-panel layout: left panel (version history + instances), center panel (grid preview), right panel (properties).
+
+### Left Panel
+
+**Header** — template name (editable inline), description (editable inline), type badge.
+
+**Version History** — table of versions, most recent first:
+- Version number
+- Dimensions (rows × columns)
+- Date published
+- Instance count (inserts/fixed locations using this version)
+- Active badge — one version is marked "active" (the default for new inserts)
+- Remove action (×) — visible only on versions with 0 instances. Hides the version from the list; data remains in the database.
+
+Version rows are clickable. Clicking a version updates the center grid preview and right properties panel to show that version's configuration.
+
+**Instances** — list below version history. Each row shows:
+- Checkbox (for batch operations)
+- Insert name (or "fixed" for direct applications)
+- Module name → level label
+- Version badge — shows current version, highlighted if not on the selected version
+
+**"Apply v[X] to Selected"** button below the instance list. X is the currently selected version in the history table. Supports both upgrade (moving to a newer version) and downgrade (reverting to an older version).
+
+### Center Panel
+
+SVG grid rendering of the selected version's layout. Large cells (72px), 8px gaps between cells. Fixed dividers render as thick lines. Origin marker (accent triangle) in the label gutter corner outside cells. Labels reflect the version's labeling scheme and origin ordering.
+
+### Right Panel — Properties
+
+Properties are **always editable** — no edit/view mode toggle. Controls are always live form inputs. All changes immediately update the grid preview.
+
+When the current form state differs from the selected version's saved data, two buttons appear:
+- **"Publish as v[N]"** — creates a new version with the current property values
+- **"Revert"** — resets all controls back to the selected version's values
+
+When the form matches the selected version, neither button is shown.
+
+**"Set as Active"** button — appears when viewing a non-active version. Sets the selected version as the default for new inserts.
+
+**Property controls:**
+- Dimensions — two number inputs (rows × cols)
+- Row labels — radio: Alpha / Numeric
+- Column labels — radio: Numeric / Alpha (validation: must differ from row labels)
+- Origin — dropdown: Top-left, Top-right, Bottom-left, Bottom-right
+- Row dividers — checkbox: Fixed (checked = permanent, blocks cross-row merging)
+- Column dividers — checkbox: Fixed (checked = permanent, blocks within-row merging)
+- Unit system — radio: Imperial / Metric
+
+### Version Application Flow
+
+Applying a version to instances (both upgrade and downgrade):
+
+1. User selects a version in the history table, then checks target instances
+2. Click "Apply v[X] to Selected" → **preview panel** shows per-instance impact:
+ - **No conflicts** — structure is compatible, application is clean. Shows before/after grid side by side.
+ - **Override conflicts** — the instance has overrides (merges, divides) that conflict with the target version's structure. Lists each conflict with resolution options: keep override, drop override, or skip this instance.
+ - **Assignment conflicts** — locations that exist in the current version but not the target have active assignments. Lists affected assignments with options: reassign, unassign, or skip this instance.
+3. User resolves conflicts per instance, or skips instances that need manual attention
+4. Click "Apply" → instances updated, child locations restructured, toast with undo
+5. Skipped instances remain on their current version — no partial changes per instance
+
+The application is a compound transaction — all changes for one instance are grouped and undone atomically.
+
+---
+
+## Overrides (Grid-Interactive Operations)
+
+Overrides modify the structure of a placed insert or a fixed location. All three types are accessed from the grid view — select cells, then apply the operation.
+
+### Merge
+
+Combine adjacent cells into a single larger cell.
+
+1. User selects two or more adjacent cells in the grid (click first, shift-click additional)
+2. Selected cells highlight with accent border
+3. "Merge" action appears in a toolbar above the grid
+4. Click merge → cells combine into a single region, labeled by the origin cell
+5. Merged region renders as one `` spanning the combined area (or `` for non-rectangular shapes)
+6. Toast: "Merged A3–A4" with undo
+
+**Constraints:**
+- Cells must be adjacent and contiguous
+- Template's **row dividers** and **column dividers** settings are enforced independently. If row dividers are fixed, the system blocks any merge that spans rows and explains why ("Row dividers on this template are permanent"). If column dividers are fixed, same for column-spanning merges. The merge action button is disabled for invalid selections.
+- Existing assignments at affected cells must be migrated or removed first (enforced, not warned)
+
+### Divide
+
+Split a cell into named child positions.
+
+1. User selects a single cell in the grid
+2. "Divide" action appears in the toolbar
+3. Click divide → panel shows options:
+ - **Template-defined subdivision** — if the template defines subdivision options (e.g., "front/rear divider"), show those as named presets
+ - **Ad-hoc** — user specifies count and labels (e.g., 2 children: "Left", "Right")
+4. Preview shows the cell with internal subdivision lines
+5. Confirm → child locations created, parent cell becomes non-assignable
+6. Toast: "Divided B2 into Left, Right" with undo
+
+**Constraints:**
+- Existing assignment at the cell must be reassigned to a child or elsewhere before dividing (enforced)
+
+### Disable
+
+Mark a cell as unavailable.
+
+1. User selects a cell in the grid
+2. "Disable" action appears in the toolbar
+3. Click disable → optional reason prompt (e.g., "cracked divider")
+4. Cell renders with diagonal stripe fill, reduced opacity
+5. Toast: "Disabled C5: cracked divider" with undo
+
+Re-enable: select a disabled cell → "Enable" action appears → restores cell to active state.
+
+**Constraints:**
+- Existing assignment must be reassigned or removed first (enforced)
+
+---
+
+## Continuous-Dimension Locations
+
+Some storage defines capacity by physical dimensions rather than discrete grid positions. Louver panels and open shelves are the primary examples.
+
+### Template Configuration
+
+When creating a template for continuous-dimension storage, additional fields:
+- **Dimension type** — "discrete" (default, grid-based) or "continuous"
+- **Row width** — total available width per row (e.g., 36 inches)
+- **Row pitch** — vertical spacing between rows (e.g., 3.5 inches)
+- **Overflow direction** — "down" (hanging, like louver panels) or "up" (sitting, like shelves)
+
+### Visualization
+
+Continuous-dimension levels render differently from grids:
+- Each row is a horizontal bar showing total width
+- Placed inserts appear as blocks within the bar, sized proportionally to their consumed width (insert width + buffer)
+- Remaining capacity shown as empty space
+- Utilization percentage displayed per row
+- Overflow indicators: if an insert's height exceeds row pitch, a visual indicator extends into the adjacent row
+
+### Placement
+
+Placing an insert into a continuous-dimension location:
+1. System checks dimensional fit (insert width + buffer ≤ remaining width)
+2. Insert appears in the row visualization
+3. Ordering within a row is optional — inserts can be reordered or treated as unordered
+
+---
+
+## SVG Grid Visualization
+
+Used in three contexts during storage definition:
+1. **Module detail** — level preview (read-only)
+2. **Place insert / apply template** — preview of what will be created
+3. **Template detail** — canonical layout preview
+
+### Rendering Rules
+
+- SVG within a React component. Scales to fit available width, maintains aspect ratio.
+- Rows labeled on left axis, columns labeled on top axis, per labeling scheme (alpha or numeric)
+- Label ordering follows origin setting — labels always ascend outward from the origin corner (e.g., bottom-left origin → row A at bottom, column 1 at left)
+- Each cell is a `` with label text centered (row label + column label, e.g., "A1")
+- Cell border: `#475569` (slate-600)
+- Cell fill: transparent (empty), `#1e3a5f` dark blue (occupied — has an assignment)
+- Disabled cells: diagonal stripe pattern (`#f87171` at 40% opacity), reduced overall opacity
+- Merged cells: single `` spanning combined area, labeled by origin cell (e.g., "A3–A4")
+- Divided cells: parent cell split into sub-rects with custom labels (e.g., "L", "R")
+- **Fixed dividers** — thick lines (`#94a3b8`, 3px, 80% opacity) between rows and/or columns where dividers are marked fixed. Rendered independently per axis.
+- **Origin marker** — accent triangle (`#ff6600`) in the label gutter corner outside all cells, at the origin position. Not inside any cell.
+- Hover: border thickens, shows cell label tooltip (DOM overlay)
+
+### Sizing
+
+- Cells are square by default
+- Template detail context: 72px cells, 8px gaps, 14px axis labels, 12px cell labels
+- Module detail / modal contexts: 52px cells, 2px gaps, 11px axis labels, 9px cell labels
+- Grid scales down for large templates (many columns), minimum cell size 24px
+- Horizontal scroll if the grid exceeds available width at minimum cell size
+
+---
+
+## Empty States
+
+| Context | Message | Action |
+|---|---|---|
+| Module list, no modules | "No modules yet. Create your first storage module to start organizing." | "New Module" button |
+| Module detail, no levels | Should not happen — levels are auto-generated | — |
+| Level selected, empty receptacle | "No insert placed." | "Place Insert" button |
+| Level selected, empty fixed | "No structure defined." | "Apply Template" button |
+| Template list, no templates | "No templates defined. Create a template to define reusable storage layouts." | "New Template" button |
+| Place insert, no templates exist | "No templates available. Create a template first." | Link to /templates/new |
+
+---
+
+## Data Flow
+
+### Module creation
+1. POST `/api/modules` → creates module
+2. POST `/api/locations` × N → creates one location per level (children of module)
+3. Each location: `moduleId`, `label`, `path` (e.g., "MUSE:3"), `locationType: "receptacle"`
+
+### Place insert
+1. POST `/api/inserts` → creates insert record (references template + version)
+2. POST `/api/inserts/:id/place` → places insert at receptacle location (validates compatibility)
+3. System generates child locations from template version's position definitions
+4. GET `/api/locations?moduleId=X` → refresh level table and grid preview
+
+### Apply template to fixed location
+1. Locations created directly as children of the fixed location, referencing the template version
+2. No insert record — the structure is permanent
+
+### Template creation
+1. POST `/api/templates` → creates template with version 1
+2. Subsequent versions: POST `/api/templates/:id/versions`
+
+---
+
+## Resolved Questions
+
+1. **Level reordering** — deferred. No clear use case yet.
+2. **Module photos** — deferred. Metadata field supports it; upload UI comes later.
+3. **Template sharing** — no import/export. Multi-tenant: templates promoted to system level via referential links, not per-tenant copies.
+4. **Undo** — always implemented. Toast notification with undo button on every mutation. Undo via transaction log per ui-paradigms.md. Only omit undo when explicitly agreed.
+5. **Custom row/column labels** — deferred. Current options are alpha and numeric. Custom labels (e.g., color names like "black, blue, red, green") are a future feature.
+6. **Template version list length** — versions with 0 instances can be hidden from the list to prevent clutter. Data is retained in the database. No pagination needed if unused versions are pruned.
+
diff --git a/specification/storage-model.md b/specification/storage-model.md
new file mode 100644
index 0000000..12baf83
--- /dev/null
+++ b/specification/storage-model.md
@@ -0,0 +1,478 @@
+# Storage Model Specification
+
+This document defines the foundational data model for WhereTF's storage system. It was developed through interactive design sessions and represents the agreed-upon concepts, terminology, and relationships.
+
+## Global Points
+- All names, labels, and descriptions are UTF-8 and unrestricted.
+- The system imposes no character restrictions on user-supplied text.
+- Display formatting and internal representation are separate concerns.
+
+---
+
+## Glossary
+
+### Module
+A top-level, independent physical storage unit. Has a user-chosen name, a description, and a single primary dimension. Modules are never nested inside other modules. Spatial relationships between modules (e.g., "NEON lives under the workbench") are descriptive metadata, not structural.
+
+A module's primary dimension defines its top-level locations (e.g., 11 levels, 16 drawers, 3 named sections). Each of those locations is either a **receptacle** or has **fixed** sub-structure (see Location Types below).
+
+Examples: a red cabinet (MUSE), an IKEA ALEX drawer unit, a shelving unit.
+
+### Template
+An abstract blueprint representing a real storage product, including a user's custom design. Defines positions, their arrangement, and physical constraints. Templates are never modified by instance data — they remain pristine reference definitions.
+
+A template defines:
+- **Unit system (display)** — metric or imperial. Declares how dimensions are presented and entered in the UI. Canonical storage is always millimeters (SI). The UI converts on read/write (e.g., an imperial template displays "1 in" for a stored value of 25.4).
+- **Origin** — which position is the reference point
+- **Primary axis** — orientation for insert compatibility and labeling direction
+- **Labeling scheme** — how positions are named (numeric, alpha, row-col, custom)
+- **Positions** — the arrangement and count (for discrete-position templates) or the physical dimensions of the location (for continuous-dimension templates)
+- **Subdivision options** — ways positions can be divided, including the labels for resulting child positions
+- **Physical constraints** — soft limits (warn) and hard limits (block) on dimensions or other physical properties (e.g. powders, liquids, gases, spools, rolls)
+- **Interface types accepted** — what inserts this template's positions can receive (if any)
+- **Interface types provided** — what receptacle types this template fits into (for insert templates). An insert template may provide multiple interface types (e.g., an Akro bin provides both a louver-hang interface and an open-surface interface).
+
+Templates carry a fixed structural core plus extensible metadata with no prescribed shape. Photos, manufacturer details, product numbers, physical dimensions, weight capacity, material — whatever is useful.
+
+Every location resolves its dimensions through a template version. Templates have a **scope**:
+- **Shared** (default) — represents a real product or reusable user design. Appears in template pickers.
+- **Single-instance** — auto-created to back an ad-hoc location (e.g., a custom shelf the user defines once). Hidden from pickers. This lets the system have one capacity-resolution path without a separate ad-hoc code path.
+
+There are three kinds of templates:
+- **Fixed templates** — represent a specific product with a fixed layout. A Plano 3600 Stowaway is always 4 rows × 6 columns. An Akro-Mils 30220 AkroBin is a fixed single-compartment bin with known dimensions.
+- **Parametric templates** — represent a system with a standard unit, instantiated at user-specified dimensions. A Gridfinity baseplate defines the 42mm grid unit; the user specifies N×M at instantiation. Parametric templates define their unit, constraints (min/max grid size), and labeling scheme. The user supplies dimensions when applying the template.
+- **Continuous-dimension templates** — define locations by physical dimensions rather than discrete positions. A louver panel row has a width; inserts placed in it consume that width. See Continuous-Dimension Locations below.
+
+Insert templates may declare a **buffer** — a flat clearance value added to the insert's primary dimension when computing fit within a continuous-dimension location. For example, a 2" wide bin with a ¼" buffer effectively consumes 2.25" of row width. Buffer is a property of the insert's form factor, not the location.
+
+#### Template Versioning
+
+Templates are versioned. Each version is immutable once published. When a template is applied to an insert or a fixed location, the instance records which version it was applied from.
+
+- Editing a template publishes a new version. Existing instances stay on their applied version.
+- Updating deployed instances to a newer version is an explicit operation — preview structural changes, resolve conflicts (broken overrides, displaced assignments), then apply.
+- Version history is append-only. Old versions are never deleted, even when no instances reference them. They remain accessible for undo, audit, and instantiating older product models.
+- The UI presents a template as a single entity with a current version. Version history is a detail view, not a primary interaction.
+
+Examples: Plano 3600 Stowaway (fixed), Gridfinity baseplate (parametric), Akro-Mils 10116 16-drawer cabinet (fixed).
+
+### Position
+An abstract place defined within a template. Has no path, holds no items. A position is a blueprint concept only. When a template is applied to a module, each position becomes a location.
+
+### Location
+A concrete, addressable place within a module instance. Has a path. Created when a template is applied to a module location, when a location is divided, or when a module's primary dimension is defined.
+
+A location is either:
+- A **leaf** — can hold item assignments
+- A **parent** — has child locations, cannot directly hold item assignments
+
+There is no special term for a parent location. It is simply a location with children.
+
+#### Location Types
+
+Every location that can have sub-structure is one of two types:
+
+**Receptacle** — an empty location that accepts inserts. It declares an interface type (e.g., "plano-3600", "gridfinity-42mm"). Sub-locations are created by whatever insert occupies it. The insert is movable — it can be removed, replaced, or relocated to another compatible receptacle.
+
+Example: a MUSE shelf level is a receptacle that accepts Plano-compatible inserts. A Plano box can be relocated to any other compatible receptacle, or replaced with any insert that provides the same interface type.
+
+**Fixed** — sub-structure is defined directly on the module or by a template applied permanently. The structure is built-in and not relocatable.
+
+Example: an ALEX drawer unit's 9 drawers are fixed — they are part of the furniture. A Gridfinity baseplate layout inside an ALEX drawer is also fixed — once configured, the baseplate grid is structural.
+
+A location is one or the other. A receptacle's sub-structure comes from its insert. A fixed location's sub-structure comes from the module's own configuration or a permanently applied template.
+
+#### Continuous-Dimension Locations
+
+A location may define capacity by physical dimensions rather than discrete positions. Instead of containing N positions, it has a measurable width (and optionally height and depth). Inserts placed in the location consume space along those dimensions. The system tracks utilization: the sum of (insert dimension + buffer) for all placed inserts, compared against the location's capacity.
+
+- **Dimensional utilization** — the system computes total consumed width vs. available width. Soft limit warns when nearing capacity; hard limit blocks placement when an insert would exceed capacity.
+- **Ordering** — inserts within a continuous-dimension location may optionally be ordered (e.g., left-to-right). Ordering is not enforced — the location may be treated as an unordered set if spatial sequence is not meaningful.
+
+Examples: a louver panel row (bins consume width), an open shelf (bins consume width and must fit within shelf height).
+
+#### Overflow Direction
+
+A location may declare an **overflow direction** — the direction in which an oversized insert extends into adjacent locations:
+
+- **Down** — the insert hangs from the location, and excess height extends into the row/level below. Used for louver rails and hanging storage where bins are suspended from a rail.
+- **Up** — the insert sits on the location, and excess height extends into the row/level above. Used for shelves where tall items stand upward.
+
+Overflow direction is a property of the location, not the insert. The same bin template may be placed in a hanging location (overflow down) or a shelf location (overflow up). When an insert's height exceeds the location's row pitch, the system checks for clearance conflicts in the overflow direction.
+
+### Insert
+A distinct physical object that occupies one or more receptacle locations and provides its own internal locations. Each insert is an individual instance — if you own 8 Plano boxes, each is a separate insert record, potentially with different overrides.
+
+Key properties:
+- **Relocatable as a unit** — moving an insert carries all its internal structure, overrides, and assignments to the new receptacle location
+- **Overrides live on the insert** — structural modifications (merged cells, divided compartments, disabled positions) describe the physical state of this specific object, not the receptacle it sits in
+- **Footprint** — how many receptacle locations the insert occupies (for discrete-position locations, e.g., a Gridfinity 2×1 bin spans two baseplate positions) or the physical dimensions consumed (for continuous-dimension locations, e.g., a 4⅛" wide bin consumes 4⅛" + buffer of row width)
+- **Buffer** — a flat clearance value declared on the insert's template, added to the insert's dimension when computing fit within a continuous-dimension location
+- **Must respect origin and primary axis alignment** of the receptacle
+
+An insert can be configured:
+- **Template-based** — references a template, as-is
+- **Template with overrides** — references a template, with structural modifications layered on top
+- **Structurally defined** — custom internal layout, no template reference
+
+An insert can exist unassigned — a new Plano box not yet placed in any module.
+
+Compatibility between inserts and receptacles is governed by interface types (see below). Placement is rejected unless the insert provides an interface type the receptacle accepts.
+
+Examples: a Plano box on a MUSE shelf level, a Gridfinity bin on a baseplate.
+
+### Interface Type
+A named physical contract that governs compatibility between inserts and receptacles. Defines the form factor boundary — what fits into what.
+
+An interface type specifies:
+- **Identifier** — a unique name (e.g., "plano-3600", "gridfinity-42mm")
+- **Physical contract** — the dimensional and mounting constraints the name represents (footprint, attachment mechanism, clearance requirements)
+- **Directionality** — a template either *provides* an interface type (insert side: "I fit into plano-3600 receptacles") or *accepts* one (receptacle side: "I accept plano-3600 inserts"), never both on the same boundary
+
+Compatibility rules:
+- Placement of an insert into a receptacle is **strictly validated** — the insert must provide an interface type that the receptacle accepts. No implicit compatibility. For continuous-dimension locations, dimensional fit is also checked.
+- An insert template can provide **multiple interface types** (e.g., an Akro-Mils bin provides both a louver-hang interface and an open-surface interface, because it can hang on a rail or sit on a shelf).
+- Multiple insert templates can share the same interface type (e.g., several third-party organizers that all fit the Plano 3600 form factor).
+- A receptacle can accept multiple interface types if physically compatible.
+- The taxonomy of interface types is intentionally open and will evolve as real storage products are modeled. Interface types are system-defined, not user-created. Users select from known types when configuring templates.
+
+### Item
+What a thing is, independent of where it is. Has a name, description, and parameters (key/value/unit triples, images). Represents a type or category, not an individual instance or count.
+
+Items can exist at multiple locations via multiple assignments. An item at two locations is one item definition with two assignments — referential, not duplicative.
+
+Item definitions must be unambiguous within the user's collection. Adding a similar item may require refining an existing item's definition (adding parameters, sharpening the name) to maintain distinguishability. Definitions become progressively more detailed as the collection grows.
+
+Equivalent to a product in ERP systems (e.g., Odoo). Future integration with ERP systems is a design guardrail. The deep detail of item management (supplier info, datasheets, equivalents) should be abstracted to a separate concern — an ERP system or a simpler standalone tool for home workshop users.
+
+Examples: "10k 0805 resistor", "M3x10 socket head cap screw", "CA glue", "3D printer filament", "14 AWG stranded red wire".
+
+### Assignment
+The relationship between an item and a location. "This item is assigned to this location."
+
+An assignment is either **placed** or **provisional**:
+
+**Placed** — the item occupies a specific leaf location. Subject to the one-per-location rule (with co-storability exceptions). This is the normal, organized state.
+
+**Provisional** — the item is *at* a location but not *in* a specific position. The item is physically present somewhere within that location's scope, but the user hasn't specified (or doesn't care about) the exact position. Valid at any level in the hierarchy, including parent locations.
+
+Example: "Resistors are on MUSE 3" — the user set them on the shelf level but hasn't sorted them into a grid cell yet. The system answers "where are my resistors?" with "MUSE 3, unplaced." Later refinement to `MUSE 3 / B4` converts the provisional assignment to a placed one.
+
+Provisional assignments:
+- Are queryable and appear in search results, clearly marked as unplaced
+- Do not occupy a position — they don't block placed assignments in child locations
+- Create organizational pressure but don't force immediate resolution
+- Are not subject to the one-per-location rule (a parent can have multiple provisional items)
+
+Multiple placed assignments per location are allowed when items are co-storable — related items that are practical to store together and separated at time of use. This avoids expanding storage capacity unnecessarily for items whose differences are easily discerned by hand.
+
+Example: M3x10 SHCS in black oxide and bright zinc in one bin (finish is obvious at a glance). Maintaining separate locations for every permutation of drive, length, thread, head, and finish becomes impractical.
+
+Co-storability is an item-level relationship. Items declare which other items they can share a location with. The system must surface co-stored items clearly so the user knows a location contains multiple items.
+
+### Subdivision
+Any location can be subdivided into named child locations. There are two forms:
+
+**Template-defined** — a subdivision option on a template, often corresponding to a physical accessory. The option defines the labels for the resulting child locations. Example: the Akro-Mils 40716 divider splits a drawer into "front" and "rear" — those labels are part of the subdivision option, not user-supplied.
+
+**Ad-hoc** — the user splits a location informally, specifying the number of children and their labels. No template or accessory required. Example: a piece of cardboard divides a bin into "left" and "right."
+
+In both cases, the original location becomes a parent and is no longer a valid assignment target. See the Divide override for prerequisites.
+
+---
+
+## Override Types
+
+Overrides are structural modifications or capacity clamps that deviate from a template's default layout. There are four types: Merge, Divide, Disable, and Restrict.
+
+Overrides can apply to:
+- **An insert** — describes the physical state of that specific object. Moves with the insert when relocated. (e.g., "this Plano box has cells 3 and 4 merged")
+- **A module location** — describes the physical state of the module itself. Stays with the module. (e.g., "this shelf slot is damaged")
+
+### Merge
+Combine two or more adjacent locations into a single location.
+
+- Target locations must be adjacent and form a contiguous region (not necessarily rectangular — L-shapes and other contiguous arrangements are valid)
+- Templates may constrain which axes allow merging. Example: a Plano 3600's rows are molded walls — columns can be merged within a row, but rows cannot be merged across.
+- The merged location's path uses the position closest to the template-defined origin
+- Non-origin locations become **aliases** that redirect to the origin location (e.g., querying col-4 returns col-3's contents when merged with col-3)
+- Prerequisite: assignments at affected locations must be migrated or removed before merging. Strictly enforced.
+
+### Divide
+Split a single location into child locations.
+
+- Can apply a subdivision option defined on the template
+- Can apply an insert's template
+- Can define a custom ad-hoc split (user specifies number of children and their labels)
+- The subdivision option, insert template, or user input defines the labels for the resulting child locations
+- The original location becomes a parent and is no longer a valid assignment target
+- Prerequisite: existing assignment must be reassigned to a child location, reassigned elsewhere, or unassigned. The operation cannot complete with unresolved assignments. Strictly enforced.
+
+### Disable
+Mark a location as unavailable for assignment.
+
+- Reason is optional (descriptive text: "cracked divider", "reserved for tool")
+- The location still exists in the structure but cannot hold assignments
+- Reversible — enable restores availability
+- Prerequisite: existing assignment must be reassigned or unassigned. Strictly enforced.
+
+### Restrict
+Clamp a location's usable capacity below its template's nominal capacity. Does not change structure — reduces the usable envelope.
+
+- Applies independently to width, height, or depth (any subset)
+- Reason is optional (descriptive text: "must slide under shelf above", "finger groove at front")
+- Effective dimension at placement time is `min(template.dim, location.maxDim)` for each axis
+- Examples: a drawer whose nominal height is 80 mm but only 60 mm of that is clear under an obstruction; a shelf cell that must leave 15 mm at the front for finger access
+- Reversible — clearing the clamp restores full capacity
+- Prerequisite: assignments that no longer fit must be resolved before the clamp is applied. Strictly enforced.
+
+---
+
+## Path Structure
+
+### Internal Representation (immutable convention)
+
+Paths have three layers. The internal and serialized forms are fixed and must never change.
+
+**Source of truth:** ordered array of segments. No delimiter, no encoding issues.
+```
+["MUSE", "3", "B4"]
+["ALEX", "4", "B2", "Front"]
+```
+
+**Serialized form:** colon-delimited string for storage, indexing, and prefix queries. Colons have near-zero collision with workshop nomenclature (part numbers, dimensions, fastener specs). Segments must not contain colons.
+```
+MUSE:3:B4
+ALEX:4:B2:Front
+```
+
+### Display Formats (flexible, may evolve)
+
+User-facing display is a separate concern from internal representation.
+
+**Brief** — compact, optimized for scanning and speech:
+```
+MUSE 3 / B4
+ALEX 4 / B2 / Front
+```
+
+**Verbose** — explicit dimension labels for clarity:
+```
+MUSE 3 / Row B, Col 4
+ALEX Drawer 4 / Row B, Col 2 / Front
+```
+
+Display formatting rules:
+- Module name + primary dimension value are space-separated: `MUSE 3`
+- Sub-dimension boundaries use slash: `/`
+- Grid cells use row-letter + column-number notation: `B4` (row B, column 4)
+- Dimension labels (Row, Col, Drawer) come from the template's labeling scheme
+- The number immediately following a module name always indicates the primary dimension value
+- Default labeling convention: rows are alpha (A, B, C, ...) top-to-back, columns are numeric (1, 2, 3, ...) left-to-right. Origin is top/back, left side.
+
+### Spoken Form
+Paths are designed to be naturally speakable: "Muse 3, B-4" or "Alex 4, B-2, front." No delimiters are verbalized.
+
+### Path Behavior Under Overrides
+
+**Merge:** Non-origin paths become aliases redirecting to the merged location's origin path. Querying an alias returns the origin location's data.
+
+**Divide:** The divided location's path is no longer valid for assignment. Child location paths extend the parent path with labels from the subdivision option or insert template.
+
+**Disable:** Path remains valid and addressable but the location is marked unavailable for assignment.
+
+---
+
+## How Templates Create Locations
+
+Templates create child locations in two distinct ways, matching the two location types:
+
+**Via insert (receptacle locations)** — placing an insert into a receptacle creates child locations defined by the insert's template. The insert is a physical instance; the template defines its structure. Example: placing a Plano 3600 insert into MUSE level 3 creates a 4×6 grid of child locations under that level.
+
+**Via direct application (fixed locations)** — applying a template permanently to a module location creates built-in child locations. Example: applying a Gridfinity baseplate template to ALEX drawer 3 with dimensions 6×4 creates a fixed 6×4 grid.
+
+For parametric templates, the user supplies dimensions at application time.
+
+### Template Limits
+Templates define physical constraints on their dimensions:
+- **Soft limits** — warn the user ("This exceeds the Plano 3600's standard layout — are you using a modified insert?")
+- **Hard limits** — block the configuration ("A Plano 3600 physically cannot have more than 6 columns")
+
+### Changing Structure
+Removing an insert from a receptacle removes its child locations. Replacing a fixed template on a module location is a destructive reconfiguration. In both cases, existing assignments under the affected location must be resolved first.
+
+---
+
+## Concrete Examples
+
+### MUSE (Red Cabinet with Shelf Levels)
+
+MUSE is a cabinet with 11 levels. Each level is a **receptacle** that accepts Plano-compatible inserts.
+
+```
+Module: MUSE
+ Primary dimension: level (1-11)
+
+ Location: level 1 ← receptacle (accepts: plano-3600)
+ Insert: plano-box-001 ← a specific Plano 3600 instance
+ Location: A1 ← leaf, can hold an assignment
+ Location: A2
+ ...
+ Location: D6
+
+ Location: level 4 ← receptacle
+ Insert: plano-box-004 ← another Plano 3600 instance which happens to have overrides
+ Location: A1
+ Location: A2
+ Location: A3+A4 ← merged (override on this insert)
+ Location: A5
+ Location: A6
+ ...
+
+ Location: level 10 ← receptacle, currently no insert
+ ← leaf by default, can hold an assignment directly
+ ("Construction Screws are on level 10")
+```
+
+Moving plano-box-004 from level 4 to level 10: the insert, its overrides, and all its assignments relocate as a unit. Level 4 becomes an empty receptacle. Level 10's direct assignment (if any) must be resolved first.
+
+### ALEX (IKEA Drawer Unit with Gridfinity)
+
+ALEX has 9 drawers. Drawers are **fixed** — they are part of the furniture. Inside some drawers, a Gridfinity baseplate layout is configured, which becomes fixed for our purposes. Gridfinity bins sit on the baseplate as **inserts**.
+
+```
+Module: ALEX
+ Primary dimension: drawer (1-9)
+
+ Location: drawer 3 ← fixed (module-defined)
+ Location: A1 ← fixed (GF baseplate grid, parametric 6×4)
+ Insert: gf-2x1-3comp-017 ← a specific GF 2×1 bin with 3 compartments
+ Location: comp 1 ← leaf
+ Location: comp 2 ← leaf
+ Location: comp 3 ← leaf
+ Location: A3 ← next baseplate position (bin spans 2 cols)
+ Insert: gf-1x1-bin-042 ← a specific GF 1×1 bin, single compartment
+ Location: (single cell) ← leaf
+ ...
+
+ Location: drawer 7 ← fixed, no baseplate configured
+ ← leaf, can hold an assignment directly
+```
+
+### AKRO (Akro-Mils Small Parts Cabinet)
+
+AKRO has 16 drawers in a fixed layout. Some drawers have been divided using divider accessories.
+
+```
+Module: AKRO
+ Primary dimension: drawer (1-16)
+ Template: Akro-Mils 10116 (fixed, defines 16 drawer positions)
+
+ Location: drawer 1 ← fixed (module-defined)
+ ← leaf, single undivided compartment
+
+ Location: drawer 7 ← fixed
+ Override: divide (using 40716 divider → front + rear)
+ Location: front ← leaf
+ Location: rear ← leaf
+
+ Location: drawer 12 ← fixed
+ Override: disable (reason: "cracked drawer, on order")
+```
+
+### LOUVER (Akro-Mils Louvered Panel with Hanging Bins)
+
+LOUVER is a wall-mounted louvered panel. It has rows defined by the louver rail spacing. Each row is a **continuous-dimension location** — bins consume width, not discrete positions. Bins hang from the rail, so overflow direction is **down** (a tall bin extends into the row below).
+
+```
+Module: LOUVER
+ Primary dimension: row (1-8)
+ Template: Akro-Mils 30636 (continuous-dimension, imperial)
+ Row width: 36 in
+ Row pitch: 3.5 in (vertical spacing between rails)
+ Overflow direction: down
+
+ Location: row 1 ← continuous-dimension (width: 36 in, overflow: down)
+ Insert: akro-30220-001 ← 4⅛" wide bin, ¼" buffer → consumes 4.375"
+ Location: (single cell) ← leaf
+ Insert: akro-30220-002 ← another 4⅛" bin → consumes 4.375"
+ Location: (single cell) ← leaf
+ Insert: akro-30230-005 ← 5½" wide bin → consumes 5.75"
+ Override: divide (ad-hoc → left + right)
+ Location: left ← leaf
+ Location: right ← leaf
+ ... ← total consumed: 14.5" of 36" available
+
+ Location: row 4 ← continuous-dimension
+ Insert: akro-30250-010 ← 10⅞" wide, 7" tall (spans 2 row pitches)
+ Location: (single cell) ← leaf, overflow extends into row 5
+ ...
+
+ Location: row 7 ← continuous-dimension, currently empty
+ ← leaf, can hold a provisional assignment
+```
+
+The same Akro-Mils bin templates can also be placed on a shelf (overflow direction: up) without any change to the bin template — the bin provides multiple interface types (louver-hang and open-surface).
+
+### SHELF (Open Shelving Unit)
+
+SHELF is a utility shelving unit. Each level is a **continuous-dimension location** — bins and items consume width. Items sit on the shelf, so overflow direction is **up**.
+
+```
+Module: SHELF
+ Primary dimension: level (1-5)
+ Template: custom open shelf (continuous-dimension, imperial)
+ Level width: 48 in
+ Level height: 15 in
+ Overflow direction: up
+
+ Location: level 2 ← continuous-dimension (width: 48 in, overflow: up)
+ Insert: akro-30220-015 ← same bin type as LOUVER, sitting on shelf
+ Location: (single cell) ← leaf
+ Insert: plano-box-012 ← Plano box sitting on the shelf (open-surface interface)
+ Location: A1 ← discrete grid inside the Plano box
+ ...
+```
+
+---
+
+## Established Points with Rationales
+
+### Modules are always top-level
+Modules do not nest. This keeps the model simple and avoids recursive module resolution. Physical containment relationships (a drawer unit inside a workbench) are captured as metadata today. A future construct may formalize inter-module relationships without introducing nesting.
+
+### Items are independent of locations
+Items and locations are distinct entities connected by assignments. This separation supports multiple assignments per item, future ERP integration, and clean relocation semantics.
+
+### One assignment per location, with co-storability
+The default is one item per location. Multiple assignments are allowed only when items are co-storable — related items that are practical to store together and separated at time of use. Co-storability is an item-level relationship, not a location property. The system must surface co-stored items clearly so the user knows a location contains multiple items.
+
+### No item compatibility constraints (deferred)
+Storage medium types (binned, racked, bulk) and item/location compatibility validation are recognized as valuable but deferred. The model can accommodate these as template and item metadata when needed.
+
+### No quantity tracking
+The system answers "where is it?" not "how many?" An item represents a category, not a count. Inventory tracking, including counts, is the domain of ERP (e.g., Odoo-based Charm).
+
+### Progressive organization via provisional assignments
+Items can be provisionally assigned at any level in the hierarchy, including parent locations. Assigning to `MUSE 3` without specifying a grid position creates a provisional assignment — the item is at that level, position undetermined. Refining to `MUSE 3 / B4` converts it to a placed assignment. This supports the real-world workflow of setting items down now and organizing later, without violating the structural rules for placed assignments.
+
+### Compatibility via interface types
+Insert/receptacle compatibility is strictly enforced through named interface types. A template declares what interface type it provides (insert side) and/or accepts (receptacle side). Placement is rejected on mismatch. Multiple insert types can share an interface type. Interface types are system-defined, not user-created.
+
+### Templates are pristine and versioned
+Templates are never modified by instance data. Overrides live on inserts or module locations. Templates are versioned — editing publishes a new version; existing instances stay on their applied version. Updating instances to a newer version is an explicit operation with conflict resolution. Version history is append-only.
+
+### No linking between levels
+Each module location independently references its template. Batch operations ("apply this template to levels 2-5") are a UI/API convenience, not a data model concept. This keeps the data model simple.
+
+### Discrete vs. continuous-dimension locations
+Not all storage fits a grid. Louver panels, open shelves, and similar storage define locations by physical dimensions. Inserts consume measurable space rather than occupying discrete positions. Both modes coexist in the same model — a module can have discrete-position locations (Gridfinity baseplates) and continuous-dimension locations (open shelves) in different parts of its structure.
+
+### Unit system is per-template (display only)
+Different storage products use different measurement systems. An Akro-Mils panel is natively imperial; a European shelving system is metric. The template declares its display unit system; all dimensions are canonically stored in millimeters. The UI converts on entry and presentation. A single canonical unit avoids mixed-unit aggregation bugs when computing utilization across heterogeneous locations.
+
+### Overflow direction is a location property
+The same bin can hang on a louver rail (overflow down) or sit on a shelf (overflow up). The physical context — the location — determines which direction excess height extends, not the insert.
+
+### Interface type taxonomy is open
+The set of interface types will evolve as real storage products are modeled. The model defines the compatibility mechanism (provide/accept) but intentionally leaves the taxonomy open. Over-specifying interface types before real-world usage would create artificial constraints.
diff --git a/specification/storage-navigator-design.md b/specification/storage-navigator-design.md
new file mode 100644
index 0000000..8c391cd
--- /dev/null
+++ b/specification/storage-navigator-design.md
@@ -0,0 +1,365 @@
+# Storage Navigator — UI/UX Specification
+
+## Overview
+
+GUI-first visual interface for modeling, browsing, and managing physical storage. The grid is the primary interaction surface — not a secondary view driven by chat or search. Every mutation is logged to a transaction log, enabling undo of any action.
+
+---
+
+## Layout
+
+Three-pane adaptive layout with collapsible sidebar.
+
+```
+┌────┬──────────────────────────┬──────────────────────────┐
+│ │ │ │
+│ S │ Module Explorer │ Storage Grid / Detail │
+│ I │ │ │
+│ D │ Card list, drill-down │ SVG grid, detail panel │
+│ E │ hierarchy, search │ insert mgmt, actions │
+│ B │ │ │
+│ A │ │ │
+│ R │ │ │
+└────┴──────────────────────────┴──────────────────────────┘
+```
+
+**Sidebar** — icon rail when collapsed, expands to labeled nav. Links: Dashboard, Modules, Items, Search, Templates, Activity (transaction log). Collapse state persists.
+
+**Center pane (Module Explorer)** — module card list → drill into levels → drill into positions. Breadcrumb navigation at top: `Modules / MUSE / Level 3`. Search bar at top of module list.
+
+**Right pane (Storage Grid / Detail)** — appears when a location with sub-structure is selected. Shows SVG grid for grid-based locations, or detail panel for leaf locations. Contextual actions in a toolbar above the grid.
+
+### Responsive Behavior
+
+- **Desktop (≥1200px)** — all three panes visible, resizable
+- **Tablet (768–1199px)** — sidebar collapses to icon rail, two panes visible
+- **Mobile (<768px)** — single pane with navigation stack, swipe or back-button to return
+
+---
+
+## Module Explorer (Center Pane)
+
+### Module List
+
+Card grid. Each card shows:
+- Module name (prominent)
+- Description (truncated)
+- Primary dimension summary (e.g., "11 levels", "9 drawers")
+- Occupancy indicator — simple fill bar, not a percentage
+- Quick stats: total locations, assigned locations
+
+Cards are filterable by text search and sortable (name, occupancy, recent activity).
+
+Empty state: "No modules configured. Create your first module to start organizing."
+
+### Drill-Down
+
+Click a module card → level/drawer list replaces the card grid. Breadcrumb updates.
+
+Each level/drawer row shows:
+- Dimension value and optional name
+- Location type badge: `receptacle` or `fixed`
+- Insert name (if occupied receptacle)
+- Occupancy indicator
+- Provisional assignment count (if any, shown as a distinct badge)
+
+Click a level/drawer → right pane opens with SVG grid (if sub-structure exists) or detail view (if leaf).
+
+### Breadcrumb
+
+Path segments are clickable for navigation back up the hierarchy. Current segment is not a link. Format follows the display brief format: `MUSE / Level 3 / B4`.
+
+---
+
+## Storage Grid (Right Pane)
+
+SVG-rendered grid of positions within a level, drawer, or insert. This is the primary visualization surface.
+
+### Grid Structure
+
+- Rows labeled alphabetically (A, B, C, ...) top-to-back on left axis
+- Columns labeled numerically (1, 2, 3, ...) left-to-right on top axis
+- Origin: top-left (A1)
+- Labels come from the template's labeling scheme when available
+
+### Cell Visual Vocabulary
+
+Each cell is an SVG group (``) containing layered visual elements that encode state at a glance:
+
+**Border shape** — outer cell boundary
+- Rectangle (default) — standard position
+- Rounded rectangle — TBD category mapping
+- Additional shapes reserved for future category encoding
+
+**Border color** — encodes primary category or status
+- Default border (neutral gray) — empty or uncategorized
+- Category-mapped color — item's primary category determines border hue
+- Orange accent (#ff6600) — active selection or action target
+- Red — disabled position
+
+**Fill** — encodes occupancy and assignment type
+- No fill (transparent) — empty, available
+- Solid light fill — occupied (placed assignment)
+- Hatched or dotted fill — provisional assignment (item is here, position undetermined)
+- Striped fill (diagonal) — disabled (with reason on hover)
+
+**Inner content** — encodes item identity within the cell
+- Glyph or icon — categorical visual shorthand (e.g., a resistor symbol, a screw silhouette)
+- Text label — item name, truncated to fit
+- Co-storability indicator — split cell or stacked indicator when multiple items share the location
+- Search result badge — numbered overlay matching search results
+
+**Merged cells** — rendered as a single SVG region spanning the merged positions. Non-rectangular merges (L-shapes) render as a compound path. The merged region uses the origin position's label.
+
+**Divided cells** — rendered with internal subdivision lines. Child positions labeled per the subdivision scheme. Each child is independently interactive.
+
+### Cell Interactions
+
+**Hover** — tooltip appears adjacent to the cell (DOM overlay, not SVG). Tooltip shows:
+- Position label (e.g., B4)
+- Item name (if assigned)
+- Assignment type (placed / provisional)
+- Co-stored items (if any)
+- Override info (merged from, divided into, disabled reason)
+
+**Click** — opens detail panel below or beside the grid (within the right pane). Detail panel shows:
+- Full item information (name, description, parameters)
+- Assignment details (placed/provisional, date assigned)
+- Co-stored items list
+- Override history
+- Actions: reassign, unassign, move, edit override
+
+**Drag** (future) — drag an insert to relocate it. Drag an item to reassign. Visual feedback shows compatible drop targets (interface type validation).
+
+### Grid Toolbar
+
+Above the grid, contextual actions:
+- **Zoom controls** — fit to pane, zoom in/out
+- **View toggle** — grid view / list view of positions
+- **Filter** — show only occupied, only empty, only provisional
+- **Actions menu** — add insert, apply template, override operations
+
+---
+
+## Search Integration
+
+Search lives in the module explorer's header. Two modes:
+
+**Basic search** — keyword match against item names, descriptions, parameters. Instant results as you type.
+
+**AI search** (when available) — natural language queries. Deferred feature, but the UI slot exists from day one.
+
+### Search Results in Grid
+
+When search returns results:
+1. Results list appears in the center pane, replacing or overlaying the current view
+2. Each result shows: item name, location path, match context
+3. Clicking a result navigates the explorer to that module/level and opens the grid
+4. The result's cell gets a numbered badge overlay (1, 2, 3...) with a distinct color per result
+5. If multiple results are in the same grid, all badges appear simultaneously
+6. Non-result occupied cells remain visible but visually recede (reduced opacity)
+
+Results are grouped by module/level for efficient scanning.
+
+---
+
+## Insert Management
+
+### Placing an Insert
+
+1. User selects "Add insert" from grid toolbar on a receptacle location
+2. System shows compatible templates (filtered by interface type)
+3. User selects a template → preview appears in the grid showing the insert's positions as ghost cells
+4. User confirms → insert is created, positions become locations, transaction is logged
+5. Notification: "Plano 3600 placed in MUSE Level 3" with undo option
+
+### Relocating an Insert
+
+1. User selects an insert (via grid toolbar or detail panel)
+2. "Relocate" action shows compatible receptacles across all modules (filtered by interface type)
+3. User selects destination → preview shows the insert in the new location
+4. User confirms → insert moves with all overrides and assignments, transaction is logged
+5. Notification: "Plano 3600 moved from MUSE Level 3 to MUSE Level 7" with undo option
+
+### Interface Type Validation
+
+Incompatible actions are prevented, not just warned:
+- Placing an insert into a receptacle that doesn't accept its interface type → action is blocked, message explains why
+- Compatible receptacles are visually indicated when placing/relocating (green highlight on valid targets, no highlight on invalid)
+
+---
+
+## Assignment UX
+
+### Placing an Item
+
+1. User clicks an empty cell → detail panel opens with "Assign item" action
+2. Item picker: search/browse items, select one
+3. Assignment is created immediately → cell updates with item visual encoding
+4. Notification: "M3x10 SHCS assigned to MUSE 3 / B4" with undo option
+
+### Provisional Assignment
+
+1. User assigns an item to a parent location (e.g., a level, not a specific cell)
+2. Provisional badge appears on the parent in the explorer drill-down view
+3. In the grid, provisional items appear in a banner above or below the grid (not in a cell, since they have no position)
+4. "Place" action on a provisional item: click a cell to convert it to a placed assignment
+
+### Co-Stored Items
+
+When items share a location:
+- Cell shows a split or stacked visual (two colors, divided diagonally or stacked)
+- Tooltip lists all items
+- Detail panel shows all items with their co-storability relationship
+- Adding a non-co-storable item to an occupied cell → action is blocked with explanation
+
+---
+
+## Transaction Log
+
+Every mutation is recorded as an immutable transaction entry. This is foundational infrastructure, not an afterthought.
+
+### What Gets Logged
+
+Every state change to the storage model:
+- Assignment: create, move, convert (provisional→placed), remove
+- Insert: place, relocate, remove
+- Override: merge, divide, disable, revert
+- Module: create, edit, delete
+- Template: create, new version, apply, update instance
+- Location: create, remove (cascading from insert/template operations)
+
+### Transaction Entry Structure
+
+Each entry records:
+- **Timestamp**
+- **Actor** — user who performed the action
+- **Action type** — the operation performed
+- **Entity** — what was affected (item, assignment, insert, location, module, template)
+- **Before state** — snapshot of affected data before the change
+- **After state** — snapshot of affected data after the change
+- **Related transactions** — parent transaction ID for compound operations (e.g., relocating an insert creates sub-transactions for each assignment move)
+- **Undo status** — whether this transaction has been undone, and by which transaction
+
+### Compound Transactions
+
+Some user actions produce multiple state changes. These are grouped under a single parent transaction:
+- Relocating an insert → move insert + move all assignments + update all paths
+- Merging cells → merge override + migrate/remove affected assignments
+- Removing an insert → remove insert + remove all child locations + unassign all items
+
+Undoing a compound transaction unwinds all sub-transactions atomically.
+
+### Undo Mechanics
+
+- Any transaction can be undone if its affected entities haven't been further modified
+- If a conflicting change has occurred since the transaction, undo is blocked with an explanation of the conflict
+- Undo itself is a transaction (logged, and can be re-done)
+- There is no arbitrary undo depth limit, but undo is strictly sequential per entity — you cannot undo transaction 5 if transaction 8 modified the same entity
+
+### Activity View
+
+The sidebar's "Activity" link opens a transaction log view:
+- Chronological list of transactions, most recent first
+- Filterable by entity type, actor, date range
+- Each entry shows: timestamp, actor, action summary, affected entity
+- Expandable to show before/after state diff
+- Undo button on eligible transactions
+
+### Notifications
+
+Every mutation triggers a toast notification:
+- Brief action summary: "M3x10 SHCS assigned to MUSE 3 / B4"
+- Undo button (available for a configurable duration, e.g., 10 seconds)
+- After timeout, undo is still available via the Activity view
+- Notifications stack, most recent on top, auto-dismiss after timeout
+
+---
+
+## Override Visualization
+
+### Merge
+
+Merged cells render as a single region spanning all merged positions. The region's border follows the contiguous shape (may be non-rectangular). The origin position's label is displayed; alias positions show a subtle redirect indicator on hover.
+
+User flow: select cells to merge → preview merged region → confirm → notification with undo.
+
+### Divide
+
+Divided cells show internal subdivision lines with child labels. Each child cell is independently interactive (hover, click, assign). The parent cell's border remains visible as the outer boundary.
+
+User flow: select cell → choose subdivision (template-defined option or ad-hoc) → preview children → confirm → notification with undo.
+
+### Disable
+
+Disabled cells render with diagonal stripe fill and reduced opacity. Hover shows the disable reason. Disabled cells cannot receive assignments but remain visible in the grid structure.
+
+User flow: select cell → disable with optional reason → notification with undo.
+
+---
+
+## Dashboard
+
+The sidebar's "Dashboard" link shows a summary view:
+- Module cards with occupancy indicators (same as Module List but read-only summary)
+- Recent activity feed (last N transactions)
+- Provisional assignments queue — items needing placement, grouped by location
+- Quick search bar
+
+---
+
+## Template Browser
+
+Accessible from sidebar. Shows available templates:
+- Card grid with template name, type (fixed/parametric), interface type provided/accepted
+- Version indicator (current version number)
+- Instance count (how many inserts/fixed locations use this template)
+- Click to view template detail: position layout preview, version history, list of instances
+
+---
+
+## Empty States
+
+Every view has a purposeful empty state with a clear next action:
+- No modules → "Create your first storage module"
+- Module with no levels configured → "Define this module's levels"
+- Level with no insert → "Place an insert or apply a template"
+- Empty grid → "Start assigning items to positions"
+- No search results → "No items match. Try different terms."
+- No provisional assignments → (don't show the section)
+
+---
+
+## Visual Constants
+
+- Accent color: #ff6600 (orange) — selection, active states, primary actions
+- Grid cell default border: neutral gray (#d1d5db)
+- Occupied cell fill: light blue (#dbeafe)
+- Provisional fill: dotted pattern on light amber (#fef3c7)
+- Disabled fill: diagonal stripes on light red (#fee2e2)
+- Search result badge colors: cycle through a palette of 8 distinguishable colors
+- Hover state: border thickens + accent color
+- Selected state: accent color border + subtle glow
+
+---
+
+## Technical Notes
+
+### SVG Rendering
+
+Grid cells are SVG `` or `` elements (paths for merged non-rectangular regions). Text labels, glyphs, and badges are SVG `` and `