diff --git a/.planning/phases/16.2-plugin-provided-field-types-and-the-blog-editor-formwidget/16.2-RESEARCH.md b/.planning/phases/16.2-plugin-provided-field-types-and-the-blog-editor-formwidget/16.2-RESEARCH.md
new file mode 100644
index 0000000..3f4485c
--- /dev/null
+++ b/.planning/phases/16.2-plugin-provided-field-types-and-the-blog-editor-formwidget/16.2-RESEARCH.md
@@ -0,0 +1,767 @@
+# Phase 16.2: Plugin-provided field types and the Blog Editor formwidget - Research
+
+**Researched:** 2026-10-09
+**Domain:** Go admin framework extension API (pact/cabana), Vue admin SPA, TipTap/ProseMirror custom element, Nuxt layer HTML post-processing
+**Confidence:** HIGH for the framework integration points (read from source this session) and for TipTap's lossy behaviour (probed locally against 3.31.4). MEDIUM for the recommended editor-preservation design, which is a design proposal that no tests cover yet.
+
+
+## User Constraints (from CONTEXT.md)
+
+### Locked Decisions
+
+#### Blog Editor: what it is
+- **D-01:** The Blog Editor is a **WYSIWYG HTML editor with a source (raw HTML) toggle**, built on ProseMirror through TipTap. It is not a block editor and not a markdown editor.
+- **D-02:** **An admin save stores HTML in both columns.** `content` holds the edited HTML and `content_html` holds the same HTML after it passes `classes.CheckHTML`. The API response shape is unchanged; `content` is simply HTML, as the PHP export already carries for imported posts. — **Reversibility:** costly — every post saved from the admin becomes HTML-content. A later move back to markdown would need an HTML-to-markdown migration across live posts.
+- **D-03:** **Byte-preserving unless edited.** If the post body is not touched in the editor, the save writes back exactly the stored HTML; the editor must not re-serialize an untouched document. Once the body is edited in WYSIWYG, markup the schema doesn't know (custom classes, divs, attributes) is kept as far as possible through generic node and attribute preservation. The source tab always edits the exact HTML.
+- **D-04:** v1 features: basic text (headings, bold/italic, lists, links, blockquote), **images**, **tables**, and **embeds plus a raw HTML block**. The raw HTML block is still subject to `CheckHTML`.
+- **D-05:** **Embeds are an allowlisted placeholder node**, for example ``, that passes `CheckHTML` unchanged. **The XSS gate is not relaxed to allow iframes.** The frontend expands the placeholder into the real iframe (D-17). — **Reversibility:** costly — the placeholder markup becomes stored content in live posts, and the layer's renderer depends on it.
+- **D-06:** The editor is **translatable like today's `mlmarkdown`**: a locale switcher over a locale → HTML map, stored through the translate overlay.
+
+#### Markdown vs HTML posts
+- **D-07:** **A markdown post opens with its `content_html`**, and the next admin save stores HTML (D-02). This is a one-way migration on first edit, with **no per-post format flag** and no new column.
+- **D-08:** **The Blog Editor replaces `mlmarkdown` for Journal post `content`** in `models/post/fields.yaml`. `markdown` and `mlmarkdown` stay cabana core types for other plugins. The `excerpt` field is unchanged.
+- **D-09:** **The public write API is unchanged.** `POST`/`PUT /_journal/api/v1/posts` keep the PHP contract: `content` is markdown and the server renders `content_html` with `FormatHTML`. Only the admin form path stores HTML.
+- **D-10:** **Admin save gate: `CheckHTML` refuses, nothing is sanitized.** Unsafe HTML returns a 422 with a field error, as category content does today, and nothing is silently rewritten. No sanitizer dependency is added. `CheckHTML` must run on every admin write path for both the default and the non-default locale values. The admin save path must stop running `FormatHTML`/goldmark over HTML content, which is the current data-loss bug (todo `journal-imported-html-admin-resave.md`).
+
+#### Field type contract (framework)
+- **D-11:** **Plugins register field types under a short global alias**, for example `type: blogeditor`. Boot fails loudly if two plugins, or a plugin and a cabana core type, claim the same alias. This is closer to Winter's `registerFormWidgets` aliases than to a namespaced key. — **Reversibility:** costly — aliases end up in plugin `fields.yaml` files across repos; renaming means editing every consumer.
+- **D-12:** The capability is a new `pact` interface (for example `HasFormFieldTypes`). Its Go side owns four things:
+ 1. **Option validation at boot**: the type's own YAML keys, fail-loud like the built-ins.
+ 2. **Value decode and validation on save**: any JSON shape, with field errors returned as 422.
+ 3. **Load and format for the form**: turns the stored column value into what the element receives. For the Blog Editor, a markdown post is served as its `content_html` (D-07).
+ 4. **List column formatting**: for example, strip tags and truncate.
+
+ — **Reversibility:** one-way — a published plugin API in `pact`; later plugins (payment and others, per todo `cabana-plugin-registered-field-types.md`) build on it.
+- **D-13:** **The client-side element loads globally on admin boot.** A plugin declares its field-type assets once, and the SPA loads them on every admin page, so the type works in any controller's form, including other plugins' forms. The element binds a value through a documented contract instead of the widget action/fill round-trip. The todo's starting point for that contract is a `value` property plus a `summer-change` event; the planner finalizes it. The element reads only the public `--c-*` CSS variables.
+- **D-14:** **Translatable variants are registered by the plugin itself**: the plugin registers both `blogeditor` and `mlblogeditor`, mirroring the `markdown`/`mlmarkdown` pair. The planner decides how a plugin ML type plugs into cabana's existing lift/hydrate path (`field_ml.go`) and the translate adapter, so locale storage is not reimplemented per plugin.
+- **D-15:** The form schema and the generated TS types carry plugin types as a generic shape, so the SPA build does not need to know them (per the todo). The cabana README, `docs/`, the admin OpenAPI, the generated TS types and the committed `dist/` are updated in the same change (CLAUDE.md documentation rules).
+
+#### Libraries, media, category content
+- **D-16:** **The editor library is TipTap (ProseMirror, MIT).** It is wrapped in a plain custom element and bundled in **sm-journal-plugin's own JS build** (`assets/src` → `assets/dist`), with the bundle committed and embedded, like the admin SPA `dist/`. The host app needs no npm. The admin SPA itself gains no editor dependency.
+- **D-17:** **Embeds render in the `vue-journal-layer` blog views.** The layer expands allowlisted embed placeholders into privacy-friendly iframes (for example `youtube-nocookie.com`), so grzybyfunkcjonalne.pl shows them. A new layer tag is pinned in `vue-grzybyfunkcjonalne-app`.
+- **D-18:** **Images are uploaded through the existing backend-authenticated Journal media upload route**, by drag-and-drop or file picker. The editor inserts `` with alt text and an optional caption (`figure`/`figcaption`). There is no media library browser.
+- **D-19:** **Journal category content uses the Blog Editor** (`blogeditor`, or `mlblogeditor` if the category content attribute is translatable in the PHP tree) instead of the textarea. **No core cabana `richeditor` type** is added. This closes todo `cabana-richeditor-field-type.md` inside Journal. The existing `CheckHTML` gate in the Categories controller's `FormBeforeCreate`/`FormBeforeUpdate` stays.
+
+### Claude's Discretion
+- The exact `pact` interface name and method set, Go identifiers, the element value/event contract details, and the asset declaration shape, all within D-11 to D-15.
+- How "untouched" is detected for D-03: client-side dirty tracking versus server-side comparison with the loaded value. Either is acceptable as long as an untouched save is byte-identical.
+- The ProseMirror schema strategy for preserving unknown markup (generic passthrough nodes and marks, a global attribute preservation extension).
+- The exact embed placeholder markup and the provider allowlist (YouTube at minimum).
+- Whether a summercms tag release, the host bump and a UAT redeploy belong in this phase, following the 16.1 D-04 pattern. Recommended, because the goal is to edit imported posts on UAT.
+- Plan count and split, subject to the plan-count checkpoint and "unit tests are the last plan". Framework work comes before the Journal work.
+- Run the security-review agent: the phase adds a plugin API to `pact`, changes HTML write paths and adds an upload path into the editor.
+
+### Folded Todos
+- **`cabana-plugin-registered-field-types.md`**: the framework half of this phase (D-11 to D-15).
+- **`cabana-richeditor-field-type.md`**: resolved by reusing the Blog Editor for category content (D-19) instead of a core type.
+- **`journal-imported-html-admin-resave.md`**: the data-loss part is fixed by D-02, D-07 and D-10. Its required test applies: re-save an imported HTML post and assert `content` and `content_html` are unchanged. If 16.1 already shipped a stopgap, this phase replaces it.
+
+### Deferred Ideas (OUT OF SCOPE)
+- **Core cabana `richeditor`/`mlricheditor` type** for plugins without their own editor. Not needed now; any plugin can register its own type or reuse one.
+- **Media library browser** for picking previously uploaded images.
+- **HTML input on the public write API** (`/_journal/api/v1/posts`). This would extend the PHP contract; the API stays markdown-in for now.
+- **Bulk HTML conversion command** (`journal:content-to-html`). Rejected in favour of convert-on-first-edit (D-07); revisit if a uniform format is ever needed.
+- **Allowlisting iframe hosts in `CheckHTML`**. Rejected in favour of placeholders (D-05).
+
+
+
+## Phase Requirements
+
+No REQUIREMENTS.md IDs are mapped to this phase (ROADMAP: "Requirements: TBD"). Coverage is derived from the CONTEXT decisions and the folded todos. ADMIN-07 (Phase 10.1, complete) is the closest existing requirement; this phase extends it and must not regress it.
+
+| ID | Description | Research Support |
+|----|-------------|------------------|
+| D-01, D-04 | WYSIWYG + source toggle; text, images, tables, embeds, raw HTML block | Standard Stack (TipTap 3.31.4), Pattern 4 (editor schema), probe results in Pitfalls 1-4 |
+| D-02, D-07, D-10 | HTML in both columns; markdown post served as `content_html`; CheckHTML refuses on every locale; admin path stops running goldmark | Pattern 2 (save lifecycle), Pattern 6 (Journal integration), Pitfall 6 |
+| D-03 | Byte-identical untouched save | Pattern 3 (two-layer untouched detection), Pitfall 5 |
+| D-05, D-17 | Embed placeholder; layer expands it after the gate | Pattern 7 (embed expansion), Code Example 4 |
+| D-06, D-14 | ML variant through cabana lift/hydrate + translate adapter | Pattern 2 (`Translatable` flag), Pitfall 7 (non-default `content_html` is never written today) |
+| D-08, D-19 | `mlblogeditor` for post content, `blogeditor` for category content | Category content is not translatable (Go model and frozen PHP tree) |
+| D-11, D-12, D-13, D-15 | pact field-type API, boot validation, generic schema shape, global asset load | Pattern 1 (registry), Pattern 5 (element contract), Architecture Map |
+| D-16 | Plugin-owned JS build, committed dist | Pattern 8 (plugin build), Environment Availability |
+| D-18 | Image upload from the editor | **Open Question 1**: the existing route cannot be reached with the admin session cookie; Pattern 9 (cabana-owned field upload route) |
+| todo resave | Re-save of untouched imported HTML keeps both columns byte-identical | Validation Architecture (test map) |
+
+
+## Project Constraints (from CLAUDE.md)
+
+These directives come from `summercms.go/CLAUDE.md`, the meta-repo `CLAUDE.md` and the user's global `CLAUDE.md`. The planner must honor them like locked decisions:
+
+- **Lean planning.** Use fewer, larger plans. Before writing any PLAN.md, present the suggested plan count with a one-line scope for each and wait for confirmation.
+- **Unit tests are the last plan of the phase.** That plan brings full coverage. Earlier plans may carry smoke tests only.
+- **Go conventions.** Use the standard library first. Add a Go dependency only when research or a decision names it; this phase adds **no Go dependency**. `go vet` and `go test ./...` must be green at every commit.
+- **Compiled plugins only.** No runtime plugin loading. A JS custom element served from the embedded tree is the Phase 10.1 mechanism and is allowed.
+- **API parity.** The Journal public API keeps the PHP contract (D-09). Do not "improve" response shapes.
+- **Commits.** No co-author tags. One logical change per commit. Planning docs and code go in separate commits.
+- **Module documentation.** Any change to a `modules/` package's exported API, config keys, CLI or dependencies updates that module's `README.md` in the same change. `pact` and `cabana` both change here. The affected `docs/` pages change with them, and `go test ./cmd/summer -run TestDocsTree` must pass. Every identifier named in a README or doc must exist.
+- **Framework READMEs never name a consuming application.** Use `blog`/`acme` examples, not Journal or grzybyfunkcjonalne.
+- **Core plugin contracts.** Do not break the shared PHP contracts. The PHP originals are not changed.
+- **Concurrent session.** Another session may be editing `modules/` (16.1 D-02). Stage files explicitly by path and check for in-flight edits to shared cabana files before committing.
+
+## Summary
+
+The framework half is a new, value-bearing extension point that sits next to the valueless Phase 10.1 `type: widget`. Today every layer hard-codes its type list:
+
+- `formFieldTypes` in `form_schema.go`
+- `scalarFormField` in `crud.go`
+- `mlFieldType` in `field_ml.go`
+- `listColumnTypes` in `list_schema.go`
+- the SPA `renderers` map and `ML_FIELD_TYPES`
+
+Each list must consult a per-boot plugin type registry, built in `cabana.Activate` from plugins that implement a new `pact.HasFormFieldTypes`. Three constraints shape the design:
+
+- **Thread the registry through.** `compileFieldNode` is a pure function with no registry access, and `decodeFields` is shared by controller forms, settings forms and relation child forms. The registry has to be passed down, not read from a package global.
+- **Keep the YAML pre-check working.** The YAML key pre-check (`formFieldKeys`) runs before the type is known, so plugin-specific keys must be validated after the type is resolved.
+- **Run Decode and Load inside the transaction.** `liftMLValues` runs before the transaction and before the record is loaded. Plugin Decode and Load need the loaded record (D-07 serves `content_html`; D-03 compares with the served value), so they run inside the transaction after `loadRecord`.
+
+Two findings change the plan:
+
+- **The Journal media upload route is unreachable from the admin SPA (D-18).**
+ - The SPA authenticates with the HttpOnly `summer_admin` cookie, whose `Path` is the admin prefix.
+ - The route lives at `/_journal/api/v1/media/upload`, outside that path, and its guard reads only a Bearer header.
+ - The element is never given a token (Phase 10.1 rule).
+ - The recommended fix: a cabana-owned upload route for plugin field types. It sits under the admin API, so it gets the cookie, the CSRF header, permissions and the record scope. Journal implements it by calling the same storage code as the existing route.
+- **TipTap's default schema is very lossy on this site's real markup.** Probed against 3.31.4 with the author-footer HTML from the grzybyfunkcjonalne export, the default schema:
+ - drops `