diff --git a/.claude/skills/fieldworks-avalonia-ui/SKILL.md b/.claude/skills/fieldworks-avalonia-ui/SKILL.md
new file mode 100644
index 0000000000..6a95bbc05d
--- /dev/null
+++ b/.claude/skills/fieldworks-avalonia-ui/SKILL.md
@@ -0,0 +1,143 @@
+---
+name: fieldworks-avalonia-ui
+description: "Build, review, or fix Avalonia UI code in FieldWorks: XAML, MVVM, view models, owned controls, headless tests, preview host, accessibility identity, and product-vs-preview wiring. Use for any change under Src/Common/FwAvalonia/, Src/Common/FwAvaloniaPreviewHost/, or Src/**/*.Avalonia/, and for net48/net8 Avalonia test changes — even if the request only mentions a control, a binding, a style, or a flaky UI test. For whole-surface migration planning use fieldworks-winforms-to-avalonia-migration first."
+---
+
+# FieldWorks Avalonia UI
+
+## Use This For
+
+- Avalonia XAML, view models, commands, lifetimes, dispatching, and
+ resource/style changes.
+- New or changed projects under `Src/**/**/*.Avalonia/`,
+ `Src/Common/FwAvalonia/`, and `Src/Common/FwAvaloniaPreviewHost/`.
+- Preview Host module registration, sample data providers, and UI
+ diagnostics (see `.github/instructions/avalonia.instructions.md` for
+ build/preview commands and project layout rules).
+- UI host wiring that selects between Avalonia and legacy UI — apply
+ `fieldworks-ui-wiring-review` alongside this skill.
+
+## Start From the Established Patterns
+
+Do not design controls or seams from scratch. The migration hub skill
+(`fieldworks-winforms-to-avalonia-migration`) documents the decided
+architecture; its `references/architecture-patterns.md` covers owned
+controls, writing-system text fields, dialogs/flyouts, validation, and
+lifetime. Canonical code to imitate:
+
+- Owned field controls: `Src/Common/FwAvalonia/Region/FwFieldControls.cs`,
+ `FwOptionPicker.cs`, `RegionMenuFlyout.cs`, `HoverReveal.cs`
+- Region view + focus memory: `LexicalEditRegionView.cs`,
+ `RegionFocusMemory.cs`
+- Seams (scheduler, lifetime, clipboard, edit sessions):
+ `Src/Common/FwAvalonia/Seams/ISeams.cs`
+- Headless test setup: `Src/Common/FwAvalonia/FwAvaloniaTests/TestAppBuilder.cs`;
+ examples in `RegionEditingTests.cs`, `VisualParityAndDensityTests.cs`
+- Density constants: `Src/Common/FwAvalonia/Poc/PocDensity.cs`
+- **Dialog kit (XAML + CommunityToolkit.Mvvm + compiled bindings):**
+ `Src/Common/FwAvaloniaDialogs/` — `OptionsDialogView.axaml`/`.axaml.cs` +
+ `OptionsDialogViewModel.cs`; headless tests in `FwAvaloniaDialogsTests/`.
+ This is the verified template for hand-authored dialogs — see
+ "Converting a WinForms dialog (MVVM kit)" below.
+
+## Converting a WinForms dialog (MVVM kit)
+
+Hand-authored dialogs/wizards use **XAML + CommunityToolkit.Mvvm + compiled
+bindings** — NOT the region/IR pattern (that is only for XML-view-definition
+surfaces). Decided 2026-06-15; rationale in
+`avalonia-migration-roadmap/complete-migration-program.md` §11.3. Full
+step-by-step + the working template:
+`references/dialog-conversion.md`. The shape, per dialog:
+
+1. **View** `XyzDialogView.axaml` (+ `.axaml.cs`): a `UserControl` (not a
+ `Window` — see modality below), `x:DataType` set to the view-model,
+ compiled `{Binding}`s, and a stable `AutomationProperties.AutomationId`
+ on every interactive control. Reuse owned controls (`FwMultiWsTextField`,
+ `FwOptionPicker`) for writing-system fields and list pickers.
+2. **View-model** `XyzDialogViewModel.cs`: `ObservableObject` with
+ `[ObservableProperty]` state and `[RelayCommand]` actions; expose the
+ result (e.g. `Accepted`). Keep it LCModel-free for the view; bind real
+ settings/domain through the app-settings/edit-session seams.
+3. **Tests** `XyzDialogTests.cs` (headless `[AvaloniaTest]`): assert the
+ compiled bindings propagate both directions and the commands fire — this
+ is the per-dialog definition of done.
+
+Rules specific to dialogs:
+
+- **It lives in `Src/Common/FwAvaloniaDialogs/`** (the dedicated XAML project),
+ never in the pure-C# `FwAvalonia` foundation. Add new dialog projects to
+ `FieldWorks.sln` (restore + VS) but build them in `build.ps1`'s Avalonia
+ loop; keep the XAML compile off the main `FieldWorks.proj` traversal (the
+ exclude pattern there). Exclude any nested test folder from the library's
+ compile glob (``).
+- **Modality during coexistence:** no Avalonia `Window.ShowDialog`. Show the
+ dialog `UserControl` via **`AvaloniaDialogHost.ShowModal(owner, view, vm, title)`**
+ (in `Src/Common/FwAvalonia/`), which hosts it in a WinForms-owned modal `Form`
+ (per the hub's `dialog-ownership.md`). The view-model implements
+ **`IDialogViewModel`** and raises `CloseRequested(bool)` from OK/Cancel — no
+ windowing in the VM. A dialog is "view + VM + `ShowModal`."
+- **Scope:** simple/confirmation/settings dialogs are good junior+AI work;
+ Views-engine-coupled dialogs (Find/Replace, Styles host `IVwRootSite`)
+ belong with the document engine (Stage 9), NOT this kit.
+
+## Required Checks
+
+- Use current Avalonia docs for uncertain APIs; do not guess dispatcher,
+ headless, automation, or binding behavior.
+- Keep product UI strings localizable (`FwAvaloniaStrings.resx` or the
+ StringTable lane); prototype hardcoded strings must be called out as gaps.
+- Stamp stable, nonlocalized `AutomationProperties.AutomationId` (derived
+ from IR `StableId` where applicable) and localized
+ `AutomationProperties.Name` on user-facing controls.
+- UI logic stays in bindings/view models where practical; avoid
+ logic-heavy code-behind.
+- For any Avalonia "select from a list" surface, prefer the shared
+ `FwOptionPicker` pattern in `Src/Common/FwAvalonia/Region/FwOptionPicker.cs`
+ (AutoCompleteBox-based, keyboard-safe, search-capable, compact density)
+ over ad hoc `ListBox` popups or one-off editable selectors. Reach for a raw
+ `ComboBox` only when the UX explicitly needs an always-visible inline combo
+ rather than the shared flyout selector.
+- Do not fix Avalonia keyboard, focus, filtering, selection, popup, or
+ rendering bugs by patching `System.Windows.Forms` hosts, WinForms
+ interop message handling, or other legacy host-only routes unless the
+ task explicitly targets interop behavior. Default to fixing the issue
+ inside the Avalonia control tree or Avalonia-owned seams.
+- Marshal to the UI thread through `IUiScheduler` (or Avalonia dispatcher
+ in non-region code); no hidden `Task.Run`, no sync-over-async.
+- Keep preview data lightweight unless the change explicitly opts into
+ LCModel/project data; product-facing paths use real edit-session/domain
+ contracts — detached DTO-only models remain preview-only.
+- Headless tests: simulate input on `Window`, flush with
+ `Dispatcher.UIThread.RunJobs()`, and capture visual regression frames
+ with Skia (`UseHeadlessDrawing=false` + `CaptureRenderedFrame()`).
+- Evidence runs through `./build.ps1` and `./test.ps1` via the normal repo
+ graph, not branch-only lanes.
+
+## Review Red Flags
+
+- A Common project directly references a feature module without an
+ explicit architecture decision.
+- Preview-only code launched from product UI without a feature gate.
+- Tests manually call `OnPropertyChanged(...)`, `ShowRecord()`, or similar
+ instead of proving the real broadcast/wiring path.
+- The active Avalonia path drives hidden legacy rendering/menu
+ infrastructure (see the hub skill's hard rules).
+- Sleep-based or timing-sensitive UI tests.
+- Claims of accessibility, localization, IME, or keyboard parity without
+ executable evidence (see the hub skill's
+ `references/parity-evidence.md` §"Evidence language").
+
+## Handoff
+
+Report Avalonia docs consulted, tests run, remaining prototype gaps,
+whether the change is product-facing or preview-only, and how the live
+wiring path was validated for each affected host. For parity work, say
+whether visual evidence is control-level headless capture or live desktop
+capture, and which automation identities were assigned.
+
+## Keep This Skill Current
+
+When a control pattern, headless-test technique, or Avalonia API gotcha
+proves out (or a pointer above goes stale), update this skill in the same
+PR — and route durable architecture lessons through the protocol in
+`fieldworks-winforms-to-avalonia-migration/references/lessons-learned.md`.
diff --git a/.claude/skills/fieldworks-avalonia-ui/references/dialog-conversion.md b/.claude/skills/fieldworks-avalonia-ui/references/dialog-conversion.md
new file mode 100644
index 0000000000..f7d00b1fde
--- /dev/null
+++ b/.claude/skills/fieldworks-avalonia-ui/references/dialog-conversion.md
@@ -0,0 +1,206 @@
+# Converting a WinForms dialog to Avalonia (MVVM kit)
+
+The verified playbook for migrating a hand-authored WinForms dialog/wizard to Avalonia using the
+**XAML + CommunityToolkit.Mvvm + compiled bindings** kit. Proven by the **real** Tools→Options
+migration (`Src/Common/FwAvaloniaDialogs/`): four tabs wired to the live settings bus via an
+`OptionsState` DTO seam, launched New-mode-gated from `LexTextApp`/`WelcomeToFieldWorksDlg`
+(`Src/LexText/LexTextControls/AvaloniaOptionsDialogLauncher.cs`), headless tests green on net48
+through `build.ps1`. Decision + rationale: `avalonia-migration-roadmap/complete-migration-program.md` §11.3.
+
+> Use this for **hand-authored** dialogs/wizards. Do NOT use the region/IR/composer pattern — that is
+> only for surfaces driven by FieldWorks XML view-definitions (entry/detail/browse). Dialogs have no
+> XML layout to compile.
+>
+> Do NOT convert Views-engine-coupled dialogs here (e.g. `FwFindReplaceDlg`, `FwStylesDlg` host
+> `IVwRootSite`/`SimpleRootSite`) — those go with the document engine (Stage 9), not this kit.
+
+## 0. Where dialogs live
+
+All MVVM dialogs go in **`Src/Common/FwAvaloniaDialogs/`** — the dedicated XAML-enabled project. Never
+add XAML to the pure-C# `FwAvalonia` foundation. The project enables `EnableDefaultAvaloniaItems` +
+`AvaloniaUseCompiledBindingsByDefault` and references `CommunityToolkit.Mvvm` + the foundation.
+
+## 1. The three artifacts (copy this shape)
+
+### View-model — `XyzDialogViewModel.cs`
+```csharp
+using System.Collections.Generic;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+
+namespace FwAvaloniaDialogs
+{
+ public partial class XyzDialogViewModel : ObservableObject
+ {
+ [ObservableProperty] private bool _someFlag;
+ [ObservableProperty] private string _someChoice = "A";
+ public IReadOnlyList Choices { get; } = new[] { "A", "B" };
+
+ public bool? Accepted { get; private set; } // null until closed; true=OK, false=Cancel
+
+ [RelayCommand] private void Ok() => Accepted = true;
+ [RelayCommand] private void Cancel() => Accepted = false;
+ }
+}
+```
+`[ObservableProperty]` generates the public property + change notification; `[RelayCommand]` generates
+`OkCommand`/`CancelCommand`. The class MUST be `partial`. Keep it LCModel-free for the view; bind real
+settings/domain through the app-settings/edit-session seams (not directly).
+
+### View — `XyzDialogView.axaml` (+ `.axaml.cs`)
+```xml
+
+
+
+
+
+
+
+
+
+```
+Visible text comes from the resource accessor (`{x:Static …}`), not literals — see §5. `AutomationId`
+stays a nonlocalized literal.
+```csharp
+using Avalonia.Controls;
+namespace FwAvaloniaDialogs
+{
+ public partial class XyzDialogView : UserControl
+ {
+ public XyzDialogView() => InitializeComponent(); // InitializeComponent is generated by the XAML compiler
+ }
+}
+```
+- **`x:DataType` is mandatory** for compiled bindings — it's how `{Binding}` is statically checked at
+ build time (the whole point: a wrong property name fails the *build*).
+- **Use a `UserControl`, not a `Window`** — see modality below.
+- Stamp a stable, nonlocalized `AutomationProperties.AutomationId` on every interactive control.
+- Reuse owned controls: `FwMultiWsTextField` for writing-system text, `FwOptionPicker` for "select from
+ a list". Use a raw `ComboBox` only for an always-visible inline settings combo.
+
+### Tests — `FwAvaloniaDialogsTests/XyzDialogTests.cs`
+Headless `[AvaloniaTest]`: create the view with a VM as `DataContext`, `window.Show()`,
+`Dispatcher.UIThread.RunJobs()`, then assert (a) compiled binding propagates **both** directions and
+(b) the generated commands fire. This is the per-dialog definition of done. See
+`FwAvaloniaDialogsTests/OptionsDialogTests.cs` for the exact pattern (find controls by AutomationId via
+`GetVisualDescendants`).
+
+## 2. Modality during coexistence — use `AvaloniaDialogHost` (built)
+
+There is **no Avalonia `Window.ShowDialog`** while WinForms owns the app. Use the reusable wrapper:
+
+```csharp
+// view-model implements IDialogViewModel; OK/Cancel raise CloseRequested(bool).
+var vm = new XyzDialogViewModel();
+var view = new XyzDialogView { DataContext = vm };
+bool? accepted = AvaloniaDialogHost.ShowModal(ownerForm, view, vm, "Xyz"); // owner = the WinForms host form
+if (accepted == true) { /* apply vm state */ }
+```
+
+`AvaloniaDialogHost.ShowModal` (in `Src/Common/FwAvalonia/AvaloniaDialogHost.cs`) shows the dialog
+`UserControl` inside a **WinForms-owned modal `Form`** (per the hub's `dialog-ownership.md`: owner-relative,
+restores focus on close), and the view-model closes it by raising `CloseRequested` — **no windowing code in
+the view-model**. The view-model implements **`IDialogViewModel`** (`Src/Common/FwAvalonia/IDialogViewModel.cs`):
+```csharp
+public partial class XyzDialogViewModel : ObservableObject, IDialogViewModel
+{
+ public event EventHandler CloseRequested;
+ [RelayCommand] private void Ok() { /* set result */ CloseRequested?.Invoke(this, true); }
+ [RelayCommand] private void Cancel() { CloseRequested?.Invoke(this, false); }
+}
+```
+So a new dialog is genuinely **"view + VM + `ShowModal`."** (Avalonia init funnels through the single
+`FwAvaloniaRuntime.EnsureInitialized()` — don't add another init guard.)
+
+## 2a. Density — compact, matching the legacy WinForms dialogs
+
+Dialogs must match legacy WinForms density, **not** the roomy Fluent defaults. The baseline is
+`Src/Common/FwAvalonia/CompactDialogStyles.cs` — `AvaloniaDialogHost.ShowModal` applies it to **every**
+hosted dialog body automatically (font 12, line-control min-height ~23, tight padding, no Fluent
+min-height floors), so new dialogs inherit compact density with **zero per-dialog work**; don't
+re-define density per dialog and don't reach for `FwAvaloniaDensity` (that owns the region/table, a
+different surface). In the view: use tight `Margin="8"` / `Spacing="4"–"6"` (see `OptionsDialogView.axaml`)
+and let `ShowModal`'s compact size defaults (420×320) fit the content — don't pass large explicit sizes.
+If a control type still looks roomy, add its compact setter to `CompactDialogStyles` (one place, all dialogs).
+
+## 2b. Real settings via a DTO seam; live-apply over restart
+
+The VM stays LCModel/PropertyTable-free. Carry real settings across the boundary with a **plain DTO**
+(`OptionsState`): the product edge populates it from the live bus, the VM edits it, and a product-side
+launcher applies it on OK — replicating the legacy dialog's exact apply order. Canonical:
+`AvaloniaOptionsDialogLauncher` (builds `OptionsState`, calls `ShowModal`, applies Reporting/Update/
+UI-mode/UI-language/plugins/Save). **Prefer live apply to "restart required":** for a setting that can
+take effect immediately, put an `Action` callback on the DTO and an `Apply` command on the VM (e.g. the
+Lexical Edit UI Mode — broadcasting the `PropertyTable` "UIMode" property re-resolves the open surfaces
+live, so the button says **"Apply"**, not "Restart to apply"). Only keep a restart prompt for settings
+that genuinely cannot apply mid-session (e.g. UI-language).
+
+## 3. Build wiring for a NEW dialog *project* (only when adding one; usually you just add files)
+
+`FwAvaloniaDialogs` already exists. If you ever add another XAML dialog project, replicate:
+- Add it (and its `*Tests`) to **`FieldWorks.sln`** (`dotnet sln FieldWorks.sln add `) — for
+ restore + Visual Studio.
+- Add `Add-ProjectIfPresent` lines in **`build.ps1`**'s net48 Avalonia loop (main project + test under
+ `if ($IncludeTests)`).
+- **Exclude** it (and its test) from the **`FieldWorks.proj`** glob/`*Tests` includes — the XAML compile
+ stays on build.ps1's isolated Avalonia-loop path, off the main solution traversal.
+- In the library `.csproj`, exclude the nested test folder from the compile glob:
+ `` and ``.
+
+## 4. Build & verify
+
+```powershell
+./test.ps1 -SkipNative -TestProject FwAvaloniaDialogsTests
+```
+Evidence runs through the normal repo scripts. Compiled-binding errors surface at build time; runtime
+binding/command behavior is proven by the headless tests.
+
+## 5. Localization (don't ship hardcoded strings)
+
+Dialog text is **product messages → `.resx`** (apply `fieldworks-localization-review`). Each Avalonia UI
+project owns its own resources: the foundation uses `FwAvaloniaStrings`; the dialog kit uses
+**`FwAvaloniaDialogsStrings`** (`Src/Common/FwAvaloniaDialogs/FwAvaloniaDialogsStrings.resx` + the
+hand-written accessor `FwAvaloniaDialogsStrings.cs`). To add a dialog's strings:
+
+1. Add `…translator note` entries to
+ `FwAvaloniaDialogsStrings.resx` (the `.resx` is auto-embedded; the project's `` is what
+ makes the Crowdin satellite build work — never drop it).
+2. Add a `public static string Xxx => Resources.GetString("ksXxx");` property to the accessor (mirror
+ `FwAvaloniaStrings.cs`; `ResourceManager("FwAvaloniaDialogs.FwAvaloniaDialogsStrings", …)`).
+3. Bind from XAML with **`{x:Static res:FwAvaloniaDialogsStrings.Xxx}`** (declare
+ `xmlns:res="clr-namespace:FwAvaloniaDialogs"`). Use it for `Content`, `Header`, `Text`, and the
+ `ShowModal` title.
+
+Rules: **`AutomationId` stays a nonlocalized literal**; visible text (which is also the accessible name on
+a `ContentControl`) is localized. Combo/list *values* that are codes or data (e.g. `"en"`, channel names)
+are not UI strings — leave them. Test it: assert the accessor resolves (`…GeneralTab == "General"`) and
+that a control's `Content` equals the accessor value (proves the `x:Static` binding), per
+`OptionsDialogTests`.
+
+## 6. Gotchas learned in the spike
+
+- **Nested test files leak into the library** unless excluded — the SDK glob compiles
+ `FwAvaloniaDialogsTests/**/*.cs` into `FwAvaloniaDialogs` (which lacks NUnit/Headless refs) → CS0246.
+ Fix: the `` in §3 (mirrors `FwAvalonia.csproj`).
+- **Restore is solution-scoped** (`dotnet restore FieldWorks.sln`) but the managed build globs
+ `Src\**`. A project not in the `.sln` builds in the traversal without `project.assets.json`
+ (`NETSDK1004`). Be in the sln OR excluded from the traversal (this kit does both: in sln, built in the
+ Avalonia loop).
+- **Compiled bindings need `x:DataType`** on the view root, or `{Binding}` falls back / fails to compile.
+- The Avalonia XAML compiler composes fine with the customized net48 build **because** the Avalonia loop
+ runs each project through plain `MSBuild /t:Restore;Build`, away from the main traversal's ILRepack.
+
+## 7. Author tiers / scope
+
+- **Junior + AI:** small/confirmation/settings dialogs (mechanical; compiled bindings + headless VM tests
+ are the safety net).
+- **Mid:** wizards, writing-system setup, project properties.
+- **Not here (→ Stage 9):** Views-engine-coupled dialogs (Find/Replace, Styles).
diff --git a/.claude/skills/fieldworks-localization-review/SKILL.md b/.claude/skills/fieldworks-localization-review/SKILL.md
new file mode 100644
index 0000000000..a20d7e53bf
--- /dev/null
+++ b/.claude/skills/fieldworks-localization-review/SKILL.md
@@ -0,0 +1,78 @@
+---
+name: fieldworks-localization-review
+description: "Review or change FieldWorks user-facing strings: .resx resources, localization keys, the StringTable lane for field labels, Crowdin-facing assets, and localization-sensitive automation metadata. Use whenever a change adds or edits any user-visible text in WinForms or Avalonia, adds a new UI project, touches resource files, or claims localization parity — even for a single new label or error message."
+---
+
+# FieldWorks Localization Review
+
+## Use This For
+
+- Product-facing text in WinForms, Avalonia, settings UI, dialogs,
+ validation messages, fallback or unsupported-surface text, and promoted
+ preview paths.
+- `.resx` additions or changes, localization key flow, and Crowdin-sensitive
+ resource updates.
+- Automation metadata where `Name`, tooltip, or label is localized but
+ stable `AutomationId` must remain nonlocalized.
+
+## The Two Lanes (Avalonia surfaces)
+
+1. **Field labels** come from layout data and resolve through the legacy
+ StringTable lane (`XmlUtils.GetLocalizedAttributeValue`,
+ `strings-{locale}.xml`) at render time. The view-definition IR carries a
+ `LocalizationKey` per node; never bake English label text into the IR or
+ region model.
+2. **Product messages** (Save, Cancel, validation errors, unsupported-row
+ text, dialog labels) live in a `.resx` with translator comments + a
+ hand-written static accessor (`ResourceManager("RootNamespace.ResxBase", …)`).
+ **Each Avalonia UI project owns its own product-message resources** (so each
+ produces its own Crowdin satellite, keyed off the project's ``):
+ - Foundation: `Src/Common/FwAvalonia/FwAvaloniaStrings.resx` + `FwAvaloniaStrings.cs`
+ (key coverage locked by `FwAvaloniaTests/RegionEditingTests.cs`).
+ - Dialog kit: `Src/Common/FwAvaloniaDialogs/FwAvaloniaDialogsStrings.resx` +
+ `FwAvaloniaDialogsStrings.cs`. XAML binds via `{x:Static res:FwAvaloniaDialogsStrings.Xxx}`;
+ `AutomationId` stays a nonlocalized literal. See the
+ `fieldworks-avalonia-ui` skill's `references/dialog-conversion.md` §5.
+
+## Required Checks
+
+- Product-facing user-visible strings live in `.resx` or the established
+ localization mechanism; preview-only hardcoded text stays clearly
+ preview-only.
+- New UI mode labels, fallback or unsupported messages, validation errors,
+ and diagnostics are localized before a product path is exposed.
+- Stable `AutomationId` and other selectors remain nonlocalized; localized
+ names, tooltips, and labels may vary by locale.
+- Resource keys and files align with existing Crowdin and repo conventions.
+- New SDK-style csprojs declare `` explicitly — the Crowdin
+ satellite-assembly build
+ (`Build/Src/FwBuildTasks/Localization/ProjectLocalizer.cs`) fails
+ without it.
+- If localization parity is claimed, tests or evidence cover the localized
+ path and confirm selectors do not depend on localized text. English on
+ the Avalonia surface where legacy shows translations is a parity
+ failure, not cosmetics.
+
+## Review Red Flags
+
+- Hardcoded English text in product C#, XAML, or product-facing
+ preview-promotion paths.
+- Field labels rendered raw from the IR without StringTable resolution.
+- Tests or automation selectors depending on localized labels when stable
+ IDs exist or are required.
+- A product route reusing preview-only placeholder text.
+- Localization claims without resource updates or without identifying
+ remaining hardcoded strings.
+
+## Handoff
+
+List the resource files or keys touched, remaining hardcoded product
+strings, automation identity strategy, and whether localized behavior has
+executable evidence or is still pending.
+
+## Keep This Skill Current
+
+When a new localization lane, Crowdin constraint, or resource convention
+appears (or a gap like the `` one is found), record it here
+in the same PR; route durable lessons through
+`fieldworks-winforms-to-avalonia-migration/references/lessons-learned.md`.
diff --git a/.claude/skills/fieldworks-managed-netfx-review/SKILL.md b/.claude/skills/fieldworks-managed-netfx-review/SKILL.md
new file mode 100644
index 0000000000..3243a5dab5
--- /dev/null
+++ b/.claude/skills/fieldworks-managed-netfx-review/SKILL.md
@@ -0,0 +1,57 @@
+---
+name: fieldworks-managed-netfx-review
+description: "Review or change FieldWorks managed C# code that crosses the .NET Framework 4.8 / C# 7.3 vs SDK-style net8 boundary: project files, language-feature compatibility, test discovery across both runtimes, UI-thread marshaling, and deterministic disposal. Use whenever a change touches a .csproj, adds C# to an unfamiliar project, moves code between net48 and net8 projects, or changes test runners — even if the compile passes locally."
+---
+
+# FieldWorks Managed NetFx Review
+
+## Compatibility Split
+
+- Legacy product code is .NET Framework 4.8 and C# 7.3 unless a project
+ explicitly targets modern .NET. The compiler will not always save you:
+ check the project's `LangVersion`/target before writing modern syntax.
+- New Avalonia modules may target `net8.0-windows`; do not leak C# 8+
+ syntax or net8-only APIs into net48 projects. Note that
+ `Src/Common/FwAvalonia/` itself is consumed from net48 hosts — verify a
+ project's actual target rather than assuming Avalonia ⇒ net8.
+- Legacy `.csproj` files require explicit source inclusion; SDK-style
+ projects glob by default. A file added on disk is not necessarily in the
+ build.
+- SDK-style projects need an explicit `` for the Crowdin
+ satellite-assembly build (see `fieldworks-localization-review`).
+
+## Required Checks
+
+- User-visible strings use `.resx` patterns where product-facing.
+- UI and async code marshals to the correct UI thread (via `IUiScheduler`
+ in region code) and does not use sync-over-async.
+- Disposable WinForms/GDI/LCModel/test resources are owned and disposed
+ deterministically; region code follows the `IRegionLifetime` rules
+ (idempotent disposal, late-callback suppression, event unsubscribe).
+- Test discovery changes are validated across both net48 and net8 test
+ assemblies.
+- Use repo scripts for evidence: `./build.ps1` and `./test.ps1` — never
+ bare `dotnet build` conclusions.
+
+## Review Red Flags
+
+- Nullable annotations, records, file-scoped namespaces, switch
+ expressions, or `using var` in net48/C# 7.3 projects.
+- Broad project/test-runner changes justified only by one local test
+ passing.
+- Hardcoded Debug paths or absolute repo assumptions in tests.
+- Skipped tests used as evidence of covered behavior.
+- A new project added to disk but missing from `FieldWorks.proj`
+ (traversal build) or `FieldWorks.sln` (IDE discovery).
+
+## Handoff
+
+Report target frameworks touched, project-file implications, test
+commands/results, and any remaining compatibility risks.
+
+## Keep This Skill Current
+
+When a new cross-target pitfall, project-file gotcha, or runtime
+difference bites a migration, add it here in the same PR; route durable
+lessons through
+`fieldworks-winforms-to-avalonia-migration/references/lessons-learned.md`.
diff --git a/.claude/skills/fieldworks-migration-scope-review/SKILL.md b/.claude/skills/fieldworks-migration-scope-review/SKILL.md
new file mode 100644
index 0000000000..bdbdb4cb7f
--- /dev/null
+++ b/.claude/skills/fieldworks-migration-scope-review/SKILL.md
@@ -0,0 +1,68 @@
+---
+name: fieldworks-migration-scope-review
+description: "Review the scope and evidence claims of large FieldWorks migration PRs, OpenSpec changes, and foundational branches. Use when sizing or splitting a branch, judging draft-PR readiness, verifying that checked tasks match their evidence, or whenever a reviewer or author asks whether a migration PR is too big, mixed, or trustworthy."
+---
+
+# FieldWorks Migration Scope Review
+
+## Review Posture
+
+Treat foundational migration PRs as architecture and evidence packages.
+The main question is whether reviewers can trust the scope, claims, and
+validation boundary.
+
+## Required Checks
+
+- Scope review is branch-relative: compare `main..HEAD` or the merge-base
+ diff, not calendar-time commit lists. Same-day commits already on `main`
+ are not branch scope.
+- Compare PR title/body/tasks against the actual diff.
+- Classify files as plan/spec, characterization test, infrastructure,
+ prototype, product behavior, or unrelated change.
+- When product or global UI wiring appears, trace preview-vs-product
+ routing and host/listener wiring separately from plan/test changes
+ (apply `fieldworks-ui-wiring-review`).
+- Verify checked tasks match evidence language; downgrade claims when
+ evidence says substitute, placeholder, skipped, future, partial, or
+ live-verification-only — the taxonomy is defined in
+ `fieldworks-winforms-to-avalonia-migration/references/parity-evidence.md`
+ §"Evidence language". A behavior/workflow claim with no headless integration
+ scenario (parity-evidence.md §2a) when one is feasible is a downgrade.
+- Confirm validation gates are explicit: OpenSpec validation
+ (`openspec validate --strict`), targeted tests, normal
+ `./build.ps1` and `./test.ps1` coverage for Avalonia, and
+ `CI: Full local check` when ready.
+
+## Split Triggers
+
+- Product-visible behavior appears in a planning/test PR.
+- Branch-only diff mixes product-visible wiring with planning/test/docs/
+ prototype work.
+- Common infrastructure directly depends on the first feature module
+ without an explicit decision.
+- Test-runner/build graph changes are mixed with UI migration work.
+- Unrelated behavior changes require their own review context.
+
+## Review Red Flags
+
+- A draft PR so broad that each reviewer must reverse-engineer intent.
+- Scope complaints based on "commits made today" instead of the
+ branch-only diff against `main`.
+- Evidence stale after rebase or differing from visible CI state.
+- A prototype wired as if it were a product feature.
+- Skill/playbook updates from the migration retrospective missing from a
+ PR that completed a migration phase (see the hub skill's workflow
+ step 10) — institutional knowledge is part of the deliverable.
+
+## Handoff
+
+Lead with blockers, then list what to remove, split, reword, or validate
+before review. Call out false scope signals separately from real
+branch-only scope problems.
+
+## Keep This Skill Current
+
+When a new split trigger, evidence-language term, or scope failure mode
+shows up in a real review, add it here in the same PR; route durable
+lessons through
+`fieldworks-winforms-to-avalonia-migration/references/lessons-learned.md`.
diff --git a/.claude/skills/fieldworks-semantic-render-parity/SKILL.md b/.claude/skills/fieldworks-semantic-render-parity/SKILL.md
new file mode 100644
index 0000000000..5f40e689d3
--- /dev/null
+++ b/.claude/skills/fieldworks-semantic-render-parity/SKILL.md
@@ -0,0 +1,82 @@
+---
+name: fieldworks-semantic-render-parity
+description: "Capture or review FieldWorks parity evidence: semantic snapshots, render/visual baselines, layout parity, failure artifacts, XML view definitions, and the Avalonia presentation IR. Use whenever a task creates or evaluates snapshot tests, screenshot baselines, view-definition compilation output, or any claim that an Avalonia surface matches its WinForms predecessor."
+---
+
+# FieldWorks Semantic Render Parity
+
+Shared definitions (Path 3 bundle, evidence lanes, artifact naming) live in
+`fieldworks-winforms-to-avalonia-migration/references/parity-evidence.md`.
+This skill covers how to build and review the snapshots themselves.
+
+## Snapshot Discipline
+
+Semantic snapshots preserve behaviorally meaningful identity and omit
+incidental layout noise. The snapshot is the anchor artifact of a parity
+bundle: when visual evidence diverges, the snapshot explains whether the
+cause is the XML import, slice filtering, editor registry, or rendering.
+
+## Include
+
+- Stable node ID and source layout/part identity.
+- Which route produced the artifact (`Avalonia`, legacy fallback, or
+ blocked state) when a scenario can run through multiple hosts.
+- Object/class binding, field/flid binding, editor kind, writing-system
+ metadata, visibility, ghost state, expansion, focus order, localization
+ key, and accessibility identity.
+- Unsupported construct diagnostics with enough path context to fix the
+ source layout.
+
+## Exclude Or Normalize
+
+- Pixel bounds, transient generated names, timestamps, machine paths,
+ culture-dependent ordering, and realized-control counts unless the test
+ explicitly owns them.
+
+## Canonical Examples
+
+- IR model and snapshot projection:
+ `Src/Common/FwAvalonia/ViewDefinition/ViewDefinitionModel.cs`
+- Snapshot/parity tests:
+ `Src/Common/FwAvalonia/FwAvaloniaTests/RegionViewingParityTests.cs`,
+ `ViewDefinitionTests.cs`, `BrowseAndCanonicalJsonTests.cs`,
+ `Path3BundleTests.cs`
+- Import coverage tracking: `LayoutImportCoverageTests.cs` and
+ `Src/Common/FwAvalonia/ViewDefinition/LayoutImportCoverage.cs`
+- Visual/density evidence: `VisualParityAndDensityTests.cs`
+
+## Render Evidence
+
+- Pixel/render tests need deterministic fixtures, clear thresholds, and
+ failure artifacts reviewers can inspect (classified failure summary, not
+ a raw diff image).
+- A semantic snapshot is not a substitute for visual/render parity when
+ typography, density, wrapping, or native rendering seams are under
+ review — and vice versa. One lane per axis; see parity-evidence.md §2.
+- Control-level Avalonia visual evidence may come from Avalonia.Headless
+ rendered frames when the scenario is explicitly control-scoped; desktop
+ workflow/accessibility claims still need live-window evidence.
+
+## Review Red Flags
+
+- A preview-only or lossy route presented as if it proved product parity.
+- Placeholder metadata presented as real binding or writing-system parity.
+- Snapshot tests updating large JSON blobs without a small behavioral
+ explanation of what changed and why.
+- Cache invalidation tests that depend on sleeps or filesystem timestamp
+ luck.
+- A new layout construct silently dropped by the importer instead of
+ producing a diagnostic node and a coverage-tracking entry.
+
+## Handoff
+
+State whether evidence is semantic, visual, accessibility/workflow, or
+performance parity, and identify remaining unproven axes. When a Path 3
+bundle is used, name each artifact and which lane it proves.
+
+## Keep This Skill Current
+
+When snapshot fields, normalization rules, or fixture patterns change, or
+a new artifact type joins the bundle, update this skill and
+parity-evidence.md together in the same PR; record durable lessons via
+`fieldworks-winforms-to-avalonia-migration/references/lessons-learned.md`.
diff --git a/.claude/skills/fieldworks-ui-wiring-review/SKILL.md b/.claude/skills/fieldworks-ui-wiring-review/SKILL.md
new file mode 100644
index 0000000000..513e51a0ff
--- /dev/null
+++ b/.claude/skills/fieldworks-ui-wiring-review/SKILL.md
@@ -0,0 +1,74 @@
+---
+name: fieldworks-ui-wiring-review
+description: "Review or change FieldWorks UI wiring — app-setting and PropertyTable routing, mediator notifications, current-content switching, host replacement, preview-vs-product boundaries, and the global legacy-vs-Avalonia UI selection. Use whenever a change touches which UI host is active, how a setting reaches a screen, RecordEditView/currentContentControl routing, save/PrepareToGoAway paths, or fallback behavior — even if the diff looks like a one-line settings change."
+---
+
+# FieldWorks UI Wiring Review
+
+## Use This For
+
+- Global or screen-level UI mode selection.
+- `PropertyTable`, app-setting, mediator, or listener changes that affect
+ which UI host is active.
+- `RecordEditView`, `currentContentControl`, host replacement, save or
+ `PrepareToGoAway()` routing, focus or command target routing, and
+ preview-to-product promotion work.
+
+## Canonical Wiring
+
+The decided routing model is explicit per-host behavior — supported
+Avalonia, explicit legacy fallback, or blocked — never silent fallback:
+
+- Surface selection: `Src/Common/FwAvalonia/LexicalEditSurfaceSelectionService.cs`,
+ `LexicalEditSurfaceResolver.cs` (behavior enum + routing logic)
+- Approved legacy adapters: `Src/Common/FwAvalonia/Seams/ActiveHostContract.cs`
+- Contract tests to imitate:
+ `Src/xWorks/xWorksTests/RecordEditViewActiveHostContractTests.cs`,
+ `Src/Common/FwAvalonia/FwAvaloniaTests/SurfaceAndHostContractTests.cs`,
+ `LexicalEditSurfaceResolverTests.cs`
+
+## Required Checks
+
+- Review scope against the branch-only diff (`main..HEAD`) and list every
+ host or consumer affected.
+- Trace the full wiring path end to end: setting source, persisted state,
+ `PropertyTable` key, mediator or property broadcast, listener
+ registration, host reload path, focus or command target routing, save or
+ `PrepareToGoAway()` path, and fallback or blocked state.
+- For global switches, verify each current consumer has an explicit
+ contract: supported Avalonia surface, explicit legacy fallback, or
+ resource-backed unsupported state.
+- The active Avalonia route must not instantiate or drive hidden legacy
+ rendering or menu infrastructure except through `ActiveHostContract`
+ approved adapters — and prove the negative with a contract test, not by
+ inspection alone.
+- Product wiring and preview wiring are reviewed separately; preview DTOs,
+ preview hosts, and spike-only semantics do not satisfy product routing.
+- Validation uses the normal repo build and test path (`./build.ps1`,
+ `./test.ps1`) plus host-specific tests when wiring changes.
+
+## Review Red Flags
+
+- Tests manually call `OnPropertyChanged(...)`, `ShowRecord()`, or similar
+ handlers instead of driving the real setting and broadcast path.
+- A preview-only mapper or detached DTO model sits on a product-facing
+ route.
+- Hidden legacy `DataTree`, menu handler, or renderer is still initialized
+ and driven while Avalonia is the active host.
+- A global setting changes unrelated screens without a manifest or
+ explicit fallback story.
+- Build or test evidence relies mainly on branch-only optional lanes or ad
+ hoc commands.
+
+## Handoff
+
+Report the setting source, listeners, affected hosts, per-host fallback
+state, executable proof of the live wiring path, and any remaining hidden
+legacy dependencies.
+
+## Keep This Skill Current
+
+When a new host type, routing pattern, or wiring failure mode appears in a
+migration, add it here (and a red flag if it is a review smell) in the same
+PR. Durable lessons also go through
+`fieldworks-winforms-to-avalonia-migration/references/lessons-learned.md`.
diff --git a/.claude/skills/fieldworks-uia2-parity-testing/SKILL.md b/.claude/skills/fieldworks-uia2-parity-testing/SKILL.md
new file mode 100644
index 0000000000..4d2e19b139
--- /dev/null
+++ b/.claude/skills/fieldworks-uia2-parity-testing/SKILL.md
@@ -0,0 +1,85 @@
+---
+name: fieldworks-uia2-parity-testing
+description: "Design or review FieldWorks UI automation and accessibility tests: UIA2, FlaUI, Appium, WinAppDriver, Avalonia.Headless, keyboard, focus, IME, and automation-id strategy. Use whenever a task adds, changes, or evaluates automated UI tests or accessibility/workflow parity claims for WinForms or Avalonia surfaces — including deciding whether a test belongs in the headless or desktop lane."
+---
+
+# FieldWorks UIA2 Parity Testing
+
+## Lane Separation
+
+- Avalonia.Headless is for fast in-process control, layout, view-model,
+ binding, and input tests.
+- UIA2/FlaUI/Appium/WinAppDriver tests require realized desktop windows and
+ validate native accessibility trees, focus, invoke patterns, and product
+ integration.
+- Do not call a headless smoke test a UIA2 baseline.
+
+## Role in the Parity Bundle
+
+In a Path 3 parity bundle (defined in
+`fieldworks-winforms-to-avalonia-migration/references/parity-evidence.md`),
+desktop automation contributes the workflow/accessibility lane only:
+launcher/chooser reachability, focus movement and return, invoke/cancel/
+accept paths, native automation tree identity, and shell-level keyboard
+behavior. It does not replace semantic snapshots or visual/render evidence;
+report it alongside those artifacts for the same scenario id.
+
+## Canonical Examples
+
+- Headless app/test setup:
+ `Src/Common/FwAvalonia/FwAvaloniaTests/TestAppBuilder.cs`; input and
+ focus patterns in `RegionEditingTests.cs`, `RegionFocusMemoryTests.cs`
+- Realized-window UIA smoke on the legacy product path:
+ `Src/xWorks/xWorksTests/WinFormsUiaSmokeTests.cs`
+- Preview-host UIA:
+ `Src/Common/FwAvalonia/FwAvaloniaPreviewHostTests/PreviewHostUiaTests.cs`
+- Automation-id locking: `Src/Common/FwAvalonia/FwAvaloniaTests/PocLexEntrySliceTests.cs`
+
+## Automation Identity
+
+Derive AutomationIds from the IR `StableId` (`{StableId}`,
+`{StableId}.Label`, `{StableId}.{WsAbbrev}`), defined as code constants —
+never resource keys, never localized text. Localized names/tooltips go on
+`AutomationProperties.Name`. Owned controls need custom automation peers
+when stock peers do not expose the required patterns.
+
+## Required Evidence
+
+- Stable automation IDs or accessible names for controls under test.
+- Explicit coverage of focus movement, invoke/click path, popup/chooser
+ reachability, keyboard shortcuts, and failure artifacts.
+- When UI mode or host wiring changes, desktop automation must cover the
+ real switch-driven host refresh or fallback behavior on realized windows;
+ manual handler calls or headless-only assertions do not prove product
+ wiring.
+- Clear CI lane: headless can run broadly; desktop automation needs an
+ interactive Windows desktop or a configured automation host.
+
+## Review Red Flags
+
+- "Runs in the background" used for UIA2/Appium without explaining the
+ required desktop/session.
+- Manual `OnPropertyChanged(...)` or similar handler invocation presented
+ as proof of live UI-mode wiring.
+- Tests assert implementation internals instead of user-observable
+ accessibility behavior.
+- Automation selectors rely on localized labels when stable IDs are
+ available or required.
+- IME coverage claimed without a real text editor/control surface and
+ input-method evidence. (IME composition/commit is a known open gap for
+ rich-text scope — do not let a checkbox claim it implicitly.)
+- Sleep-based waits instead of event-driven synchronization.
+
+## Handoff
+
+Classify each test as headless, native desktop automation, or smoke
+substitute, and state what parity claim it can and cannot support. For
+bundle work, say which workflow/accessibility assertions the desktop lane
+proved, whether switch wiring/fallback was exercised on a realized window,
+and which claims still need another lane.
+
+## Keep This Skill Current
+
+When a new automation pattern, peer implementation, CI-lane constraint, or
+flakiness fix proves out, add it here in the same PR; route durable lessons
+through `fieldworks-winforms-to-avalonia-migration/references/lessons-learned.md`.
diff --git a/.claude/skills/fieldworks-winapp/navigation/screenshot-evidence.md b/.claude/skills/fieldworks-winapp/navigation/screenshot-evidence.md
index ce8ea59202..cc451aa6c9 100644
--- a/.claude/skills/fieldworks-winapp/navigation/screenshot-evidence.md
+++ b/.claude/skills/fieldworks-winapp/navigation/screenshot-evidence.md
@@ -53,8 +53,11 @@ Use names that tell the story in sorted order:
- `02-before-.png`
- `03-after-.png`
- `04-after-.png`
+- Path 3 parity bundle: `01-winforms-.png`, `02-avalonia-.png`, `03-diff-.png`
- `sequence--001.png`, `sequence--002.png`, ...
+When the task is migration parity, capture matched WinForms and Avalonia framing for the same scenario id and store them under `openspec/changes//evidence/parity//` so the visual lane lines up with the semantic snapshot and the workflow/accessibility evidence.
+
## Expected Signals
- Screenshots should show FieldWorks, not VS Code or another foreground app.
diff --git a/.claude/skills/fieldworks-winforms-to-avalonia-migration/SKILL.md b/.claude/skills/fieldworks-winforms-to-avalonia-migration/SKILL.md
new file mode 100644
index 0000000000..3e576647d8
--- /dev/null
+++ b/.claude/skills/fieldworks-winforms-to-avalonia-migration/SKILL.md
@@ -0,0 +1,141 @@
+---
+name: fieldworks-winforms-to-avalonia-migration
+description: "End-to-end playbook for migrating any FieldWorks WinForms surface (DataTree slices, XMLViews browse/table, dialogs, choosers, launchers, shell panes) to Avalonia using the established region/seam architecture. Use whenever planning, implementing, or reviewing WinForms-to-Avalonia work — including seam extraction, region composition, owned controls, plugin editors, parity evidence, or retiring legacy surfaces — even if the request only says port, modernize, replace WinForms, or new Avalonia view. Also use after finishing a migration to run the retrospective step that folds new lessons back into these skills."
+---
+
+# FieldWorks WinForms To Avalonia Migration
+
+This is the hub skill for the migration program. It tells you what
+architecture already exists (do not reinvent it), what order to work in,
+which companion skill to apply at each step, and how to keep this skill
+set current as more surfaces are migrated.
+
+## Core Rule
+
+Migrate by proving behavior first, extracting seams second, and introducing
+Avalonia controls only after legacy behavior has executable parity evidence.
+A region is not "migrated" until it passes the symbol audit, parity gates,
+and has zero runtime dependency on native Views/DataTree infrastructure —
+otherwise you have only wrapped the old system.
+
+## Established Architecture — Reuse, Don't Reinvent
+
+Past migrations already decided the paradigms below. Before writing any new
+abstraction, read `references/architecture-patterns.md` (table of contents at
+top) for the decision, the why, and the gotchas. Quick map:
+
+| Pattern | Canonical code | Details |
+| --- | --- | --- |
+| Typed view-definition IR compiled from XML layouts | `Src/Common/FwAvalonia/ViewDefinition/ViewDefinitionModel.cs`, `XmlLayoutImporter.cs`, `ViewDefinitionCompiler.cs` | architecture-patterns.md §1 |
+| Region model + composer (boundary sits *above* DataTree) | `Src/xWorks/FullEntryRegionComposer.cs`, `Src/Common/FwAvalonia/Region/LexicalEditRegionModel.cs`, `LexicalEditRegionMapper.cs` | §2 |
+| Explicit surface selection per host (`HostUiBehavior`) | `Src/Common/FwAvalonia/LexicalEditSurfaceSelectionService.cs` | §3 |
+| Owned dense controls, not stock property grids | `Src/Common/FwAvalonia/Region/FwFieldControls.cs`, `FwOptionPicker.cs`, `RegionMenuFlyout.cs` | §4 |
+| Plugin registry for custom/legacy slice classes | `Src/xWorks/RegionEditorPlugins.cs`, `Src/xWorks/ChorusNotesPlugin.cs` | §5 |
+| Seam contracts (edit session, undo, validation, scheduler, lifetime, refresh) | `Src/Common/FwAvalonia/Seams/ISeams.cs` | `references/seam-catalog.md` |
+| Writing-system-aware text fields (font, RTL, keyboard per WS) | `Src/Common/FwAvalonia/Region/FwFieldControls.cs` (`FwMultiWsTextField`) | architecture-patterns.md §6 |
+| Dialog ownership across the WinForms/Avalonia boundary | `openspec/changes/lexical-edit-avalonia-migration/dialog-ownership.md` | §7 |
+| Headless integration-test harness (scenario/workflow drivers + real-clerk layer) | `Src/Common/FwAvalonia/FwAvaloniaTests/Workflows/HeadlessWorkflowHarness.cs`, `Src/xWorks/xWorksTests/ClerkRoutedFilterTests.cs` | architecture-patterns.md §13 |
+
+## Workflow
+
+Work through the phases in order. Copy
+`references/migration-checklist.md` into your task notes and check items
+off — it is the per-region definition of done.
+
+1. **Inventory and scope.** Identify the legacy surface, its entry points,
+ layouts/parts, custom slice classes, dialogs, and command wiring.
+ Produce a coverage map (surface × behavior × test status). Apply
+ `fieldworks-migration-scope-review` when sizing the PR/branch.
+2. **Characterize before refactor.** Lock current behavior in executable
+ tests (semantic baselines, timing baselines, UIA smoke) *before*
+ extracting anything. Gates: every behavior is tested, consciously
+ deferred with an owner, or blocked by a named seam. Examples:
+ `Src/xWorks/xWorksTests/WinFormsUiaSmokeTests.cs`,
+ `Src/Common/Controls/DetailControls/DetailControlsTests/`.
+3. **Extract seams.** Reuse the existing contracts in
+ `Src/Common/FwAvalonia/Seams/ISeams.cs`; only add a new seam when
+ `references/seam-catalog.md` has no fit, and record why there.
+4. **Select controls.** Default to the owned-control decisions in
+ architecture-patterns.md §4. Re-evaluate only when a pivot trigger in
+ seam-catalog.md §"Pivot triggers" has fired.
+5. **Compose the region.** Walk the compiled IR in a composer, project into
+ a region model, route custom classes through the plugin registry, and
+ render unclaimed classes as explicit "unsupported" rows — never silent
+ fallback. Apply `fieldworks-avalonia-ui` for the control work.
+6. **Wire the host.** Explicit per-host contract: supported Avalonia,
+ explicit legacy fallback, or blocked. Apply `fieldworks-ui-wiring-review`.
+7. **Prove parity.** Build the evidence bundle defined in
+ `references/parity-evidence.md` (semantic + visual + workflow lanes).
+ Apply `fieldworks-semantic-render-parity` and
+ `fieldworks-uia2-parity-testing`. **Front-and-center: write headless
+ integration tests that walk the surface's real scenarios/workflows**
+ (filter → clear, select → detail follows, edit → refresh, navigate) via the
+ harness (architecture-patterns.md §13) — at the surface layer
+ (`FwAvaloniaTests`) and, for domain claims like real list narrowing/undo, the
+ real-clerk layer (`xWorksTests`). These replace deferred "live verification."
+8. **Localize.** Apply `fieldworks-localization-review`; field labels go
+ through the StringTable lane, product messages through
+ `FwAvaloniaStrings.resx`.
+9. **Retire and gate.** Run the symbol audit
+ (`Src/Common/FwAvalonia/FwAvaloniaTests/EngineIsolationAuditTests.cs`),
+ active-host contract tests
+ (`Src/xWorks/xWorksTests/RecordEditViewActiveHostContractTests.cs`),
+ and the normal repo gates (`./build.ps1`, `./test.ps1`).
+10. **Retrospective.** Update these skills — see "Keep this skill set
+ current" below. This step is part of the migration, not optional polish.
+
+## Hard Rules
+
+- Active Avalonia hosts must not instantiate or drive hidden legacy
+ `DataTree`, `Slice`, `RootSite`, menu, or renderer infrastructure except
+ through approved baseline adapters
+ (`Src/Common/FwAvalonia/Seams/ActiveHostContract.cs`).
+- Migrated-region production code must stay free of the forbidden symbols
+ listed in parity-evidence.md §"Forbidden symbols" (enforced by
+ `EngineIsolationAuditTests.cs`).
+- Evidence comes from the normal repo path: `./build.ps1` and `./test.ps1`.
+ Branch-only lanes or ad hoc commands are not integration evidence.
+- One global undo/redo stack (LCModel action handler). Never a parallel
+ Avalonia-only history for committed state.
+- Avalonia modal windows are not supported during coexistence; anything
+ modal uses a WinForms dialog with the host form as owner (see
+ architecture-patterns.md §7).
+- Performance budgets are measured against legacy baselines, not estimated
+ (parity-evidence.md §"Performance budgets").
+
+## Review Red Flags
+
+- Tests manually invoke `OnPropertyChanged`, `ShowRecord`, or similar
+ handlers to simulate runtime wiring instead of driving the real path.
+- Active Avalonia routing depends on a lossy DTO mapper or preview-only
+ code without an explicit product contract.
+- Task checkboxes claim parity while evidence says substitute, placeholder,
+ skipped, or future work (see parity-evidence.md §"Evidence language").
+- A custom slice class silently renders wrong instead of producing an
+ explicit unsupported row.
+- A PR mixes plans, tests, infrastructure, product wiring, and unrelated
+ changes — apply `fieldworks-migration-scope-review`.
+
+## Handoff
+
+State what is legacy baseline, what is extracted seam, what is Avalonia
+product surface, what each affected host does under the global switch, what
+remains outside parity, and what you changed in this skill set during the
+retrospective.
+
+## Keep This Skill Set Current
+
+These skills are the institutional memory of the migration. Every completed
+migration teaches something; if it stays in your head or in a PR thread it
+is lost. The retrospective step (workflow step 10) is how the skills stay
+ahead of the codebase instead of trailing it:
+
+1. Read `references/lessons-learned.md` and follow its update protocol —
+ it maps each kind of discovery (new pattern, new gotcha, fired pivot
+ trigger, new canonical example, stale pointer) to the exact file and
+ section to update.
+2. Make the skill edits in the same PR as the migration, so reviewers see
+ the lesson next to the evidence that produced it.
+3. If a file pointer in any of these skills is stale (file moved, openspec
+ change archived), fix the pointer immediately — do not work around it
+ silently.
diff --git a/.claude/skills/fieldworks-winforms-to-avalonia-migration/references/architecture-patterns.md b/.claude/skills/fieldworks-winforms-to-avalonia-migration/references/architecture-patterns.md
new file mode 100644
index 0000000000..a1655f7774
--- /dev/null
+++ b/.claude/skills/fieldworks-winforms-to-avalonia-migration/references/architecture-patterns.md
@@ -0,0 +1,297 @@
+# Established Migration Architecture Patterns
+
+Decisions already made by the lexical-edit migration. Each section gives the
+decision, why it was made, the canonical code, and gotchas. Provenance for
+every decision lives in `openspec/changes/lexical-edit-avalonia-migration/`
+(if that change has been archived, look under `openspec/changes/archive/`).
+
+Contents:
+
+1. Typed view-definition IR (the long-term contract)
+2. Region model + composer (boundary above DataTree)
+3. Explicit surface selection per host
+4. Owned dense controls (control-selection decisions)
+5. Plugin registry for custom slice classes
+6. Writing-system behavior (font, RTL, keyboard, multi-WS)
+7. Dialog ownership and modality across the interop boundary
+8. Undo/redo, edit sessions, and refresh
+9. Validation
+10. Custom fields and ghost rows
+11. Localization lanes
+12. Density and performance
+
+## 1. Typed view-definition IR (the long-term contract)
+
+**Decision.** XML Parts/Layouts are compiled into a typed
+`ViewDefinitionModel` (one `ViewNode` per field carrying StableId, editor
+kind, writing system, visibility, expansion, custom-field placeholder
+metadata, accessibility id, and localization key). Avalonia consumes the IR,
+never raw XML. XML is an import format during transition, not the runtime
+abstraction; the retirement path is deterministic JSON
+(`ViewDefinitionJsonSerializer.cs`) plus customer override patches.
+
+**Why.** Keeps customer layout customizations alive, creates a clean
+DI/test boundary, enables off-thread compilation and snapshot-based parity
+tests, and gives XML a retirement path.
+
+**Canonical code.** `Src/Common/FwAvalonia/ViewDefinition/` —
+`ViewDefinitionModel.cs`, `XmlLayoutImporter.cs`, `ViewDefinitionCompiler.cs`
+(caches by immutable source-snapshot fingerprint),
+`ViewDefinitionJsonSerializer.cs`, `LayoutImportCoverage.cs`.
+Tests: `Src/Common/FwAvalonia/FwAvaloniaTests/ViewDefinitionTests.cs`,
+`LayoutImportCoverageTests.cs`, `BrowseAndCanonicalJsonTests.cs`.
+
+**Gotchas.** Compilation must stay deterministic (same source snapshot →
+identical IR) because parity snapshots key off it. Track element/attribute
+import coverage explicitly; an unimported construct must surface as a
+diagnostic node, not vanish.
+
+## 2. Region model + composer (boundary above DataTree)
+
+**Decision.** The migration boundary sits at the region-model layer above
+`DataTree`, not inside it. A composer walks the compiled IR the way legacy
+DataTree walks layouts and emits a region model (renderable fields keyed by
+IR StableId) plus an edit context. DataTree internals are never extracted —
+they are deleted at the end of coexistence, so extracting them is throwaway
+work.
+
+**Canonical code.** `Src/xWorks/FullEntryRegionComposer.cs` (walks IR,
+emits `ComposedEntryRegion`),
+`Src/Common/FwAvalonia/Region/LexicalEditRegionModel.cs`,
+`LexicalEditRegionMapper.cs`, `IRegionEditContext.cs`,
+`LexicalEditRegionView.cs`.
+Tests: `RegionModelTests.cs`, `RegionEditingTests.cs`,
+`RegionViewingParityTests.cs` in `Src/Common/FwAvalonia/FwAvaloniaTests/`.
+
+**Gotchas.** The region model is presentation data, not LCModel objects —
+it is projected from `IRegionValueProvider` style seams so it can be built
+and tested off-thread without WinForms or a real project.
+
+## 3. Explicit surface selection per host
+
+**Decision.** Every host that can show legacy or Avalonia UI resolves an
+explicit `HostUiBehavior`: supported Avalonia, explicit legacy fallback, or
+blocked. No silent fallback. The active host must never drive hidden legacy
+DataTree/menu/renderer infrastructure except through approved baseline
+adapters.
+
+**Canonical code.**
+`Src/Common/FwAvalonia/LexicalEditSurfaceSelectionService.cs`,
+`LexicalEditSurfaceResolver.cs`, `LexicalEditSurfaceFactory.cs`,
+`Src/Common/FwAvalonia/Seams/ActiveHostContract.cs` (approved-adapter
+whitelist).
+Tests: `LexicalEditSurfaceResolverTests.cs`,
+`SurfaceAndHostContractTests.cs`,
+`Src/xWorks/xWorksTests/RecordEditViewActiveHostContractTests.cs`.
+
+**Gotchas.** "Convenience" calls into legacy internals while Avalonia is
+visible (for example, to harvest metadata) defeat the boundary — the
+contract tests exist to catch exactly that.
+
+## 4. Owned dense controls (control-selection decisions)
+
+**Decision.** Build FieldWorks-owned row/field controls on top of stock
+virtualization primitives instead of adopting a stock property grid or
+TreeDataGrid:
+
+- Detail view (DataTree replacement): owned slice list over
+ `ListBox`/`VirtualizingStackPanel` — flatten in the model, virtualize
+ with stock primitives, own the row.
+- Browse/table (XMLViews replacement): owned virtualized table — flattened
+ row list + shared column header + owned cell layout
+ (`Src/Common/FwAvalonia/Region/LexicalBrowseView.cs`).
+- Bounded popup trees (≤500 items): stock `TreeView` with an explicit
+ item-count ceiling, validated at 100%/150% DPI.
+- Unbounded trees: the owned flattened virtualized list with
+ expander/indent row chrome.
+
+**Why.** Stock grids fit poorly with nested senses, multi-WS alternatives,
+custom choosers, dense rows, and FieldWorks keyboard behavior; owning the
+row keeps the UI framework out of domain semantics. TreeDataGrid was
+rejected on licensing and editing/automation gaps (see pivot triggers in
+`seam-catalog.md` — revisit if those facts change).
+
+**Canonical code.** `Src/Common/FwAvalonia/Region/FwFieldControls.cs`
+(`RegionFieldKind`: Text, Chooser, Boolean, Image, Command,
+ReferenceVector, Custom), `FwOptionPicker.cs`, `RegionMenuFlyout.cs`,
+`HoverReveal.cs`, `RegionFocusMemory.cs`.
+
+## 5. Plugin registry for custom slice classes
+
+**Decision.** Legacy layouts reference custom slice classes by name (for
+example `SIL.FieldWorks.XWorks.LexEd.MessageSlice`). A plugin registry maps
+those same class identities to factories that build Avalonia controls.
+Resolution order: plugin → companion-strip WinForms coexistence →
+explicit "unsupported" row. Never silent mis-render. Keying by legacy class
+identity means zero layout edits and measurable burn-down (census vs.
+registry coverage).
+
+**Canonical code.** `Src/xWorks/RegionEditorPlugins.cs`,
+`Src/xWorks/ChorusNotesPlugin.cs`, registry contracts in
+`Src/Common/FwAvalonia/Region/LexicalEditRegionModel.cs`.
+Tests: `Src/xWorks/xWorksTests/DialogLauncherPluginTests.cs`,
+`LexemeEditorBurnDownTests.cs`, `MessagesCompanionLaneTests.cs`.
+
+**When migrating a new surface:** census its custom slice classes first,
+check the registry for existing plugins, and add plugins (with tests) for
+the rest. Add each new plugin to the burn-down tracking.
+
+## 6. Writing-system behavior (font, RTL, keyboard, multi-WS)
+
+**Decision.** Every text field renders per-writing-system rows: WS
+abbreviation gutter + value box, with font family/size, flow direction
+(RTL/LTR), and keyboard activation projected from LCModel WS metadata.
+Keyboard switches on focus (legacy `EditingHelper.SetKeyboardForWs`
+behavior). OpenType features ship via HarfBuzz; native Graphite is never
+loaded on the Avalonia path — Graphite-dependent writing systems are
+classified and warned, not blocked.
+
+**Canonical code.** `FwMultiWsTextField` in
+`Src/Common/FwAvalonia/Region/FwFieldControls.cs`; `RegionWsValue`
+(WsAbbrev, FontFamily, FontSize, RightToLeft, WsTag) in
+`LexicalEditRegionModel.cs`.
+Tests: `TreeSpikeAndRtlTests.cs`, `VisualParityAndDensityTests.cs`.
+
+**Gotchas.** Never assume one font, one direction, or one script per
+field. Test mixed-script content at 100% and 150% DPI with real fonts.
+
+## 7. Dialog ownership and modality across the interop boundary
+
+**Decision.** During coexistence there is one UI thread and one message
+loop. Rules (provenance:
+`openspec/changes/lexical-edit-avalonia-migration/dialog-ownership.md`):
+
+- Anything modal is a WinForms dialog, owned by the hosting WinForms
+ top-level form (`Control.FindForm()` of the host) — never `null`, never
+ an Avalonia handle. Avalonia modal windows are not used (unsupported on
+ the 11.x coexistence path).
+- Record the focused Avalonia control before `ShowDialog` and restore focus
+ explicitly after close.
+- Use Avalonia flyouts inside the hosted surface, not free popup windows
+ (mixed-DPI positioning).
+- No cross-boundary Tab order between WinForms siblings and the Avalonia
+ surface; own focus inside the surface.
+- No WinForms modeless tool windows owned by an Avalonia surface.
+
+## 8. Undo/redo, edit sessions, and refresh
+
+**Decision.** Edits ride a fenced `IEditSession`
+(Active → Saved/Canceled → Disposed) wrapping an LCModel undo task — one
+undoable action per save regardless of field count. Transient text undo
+stays local to the focused TextBox. Global undo/redo routes through
+`IUndoRedoCoordinator` to the LCModel action handler, then refreshes the
+region. Cancel rolls back the session and must not create a committed undo
+action. Refresh coordination mirrors legacy
+`DoNotRefresh`/`RefreshListNeeded` semantics via the refresh-coordinator
+seam.
+
+**Canonical code.** `Src/Common/FwAvalonia/Seams/ISeams.cs`,
+`SeamImplementations.cs`, `RefreshCoordinator.cs`.
+Tests: `SeamTests.cs`, `RegionEditingTests.cs`.
+
+**Gotchas.** Two undo stacks produce user-visible data weirdness. Never
+disable global undo while a session is dirty — route it. Defer PropChanged
+fan-out during multi-field edits until commit/cancel.
+
+## 9. Validation
+
+**Decision.** Validation runs over immutable presentation snapshots, not
+live LCModel. Errors are ordered by presentation/focus order (deterministic
+for headless tests), skip unmaterialized lazy items, and carry node id,
+object/flid, severity, localized message key + args, and accessibility
+text. Only severity=Error blocks save; warnings do not. Stale async results
+(from older snapshots) are discarded.
+
+**Canonical code.** `IValidationService` in
+`Src/Common/FwAvalonia/Seams/ISeams.cs`; composer wiring in
+`Src/xWorks/FullEntryRegionComposer.cs`.
+
+## 10. Custom fields and ghost rows
+
+**Decision.** Stored view definitions contain `CustomFieldPlaceholder`
+nodes (typed equivalent of legacy `customFields="here"`), expanded from
+LCModel metadata at compile time. Custom fields are never baked into stored
+definitions (they differ per project). Ghost rows ("type to add"
+placeholders) are runtime UI state managed by the composer/model, never
+stored layout structure.
+
+**Canonical code.** `ViewDefinitionModel.cs` (placeholder node kind),
+composer expansion in `FullEntryRegionComposer.cs`.
+Tests: `RegionCustomFieldRenderingTests.cs`.
+
+## 11. Localization lanes
+
+**Decision.** Two lanes:
+
+- **Field labels** resolve through the legacy StringTable lane
+ (`XmlUtils.GetLocalizedAttributeValue`, `strings-{locale}.xml`) at render
+ time; the IR carries `LocalizationKey` per node, never baked English.
+- **Product messages** (Save, Cancel, validation, unsupported-row text)
+ live in `Src/Common/FwAvalonia/FwAvaloniaStrings.resx` with translator
+ comments.
+
+**Gotchas.** SDK-style csprojs need an explicit `` element
+or the Crowdin satellite-assembly build
+(`Build/Src/FwBuildTasks/Localization/ProjectLocalizer.cs`) fails — verify
+when adding any new Avalonia project. English-on-Avalonia where legacy
+shows translations is a parity failure, not cosmetics.
+
+## 12. Density and performance
+
+**Decision.** Visual *density* (row spacing, gutters, box heights) is owned
+by FieldWorks density constants, measured against legacy WinForms
+baselines. Performance budgets are measured, not estimated: capture legacy
+init/populate/total timings with the characterization harness, then hold
+Avalonia to within 20% of legacy total (or record an explicitly accepted
+delta in the region manifest).
+
+> **Density parity ≠ look parity (migration-program decision 2026-06-15).** The
+> program is chartered to *upgrade the look*: it adopts a modernized Fluent-based
+> theme rather than mimicking legacy WinForms chrome. Keep this distinction sharp —
+> *density* (information per screen, alignment, gutters) stays matched to legacy
+> baselines and is asserted by the parity lanes; *styling* (colors, control
+> templates, focus visuals, corner radii) may intentionally diverge. The visual
+> parity lane therefore checks density/layout, not pixel-for-pixel chrome.
+
+**Canonical code.** `Src/Common/FwAvalonia/Poc/PocDensity.cs`;
+legacy harness
+`Src/Common/Controls/DetailControls/DetailControlsTests/DataTreeRenderTests.cs`;
+committed thresholds `DataTreeTimingBaselines.json` (same directory).
+Tests: `VisualParityAndDensityTests.cs`.
+
+**Gotchas.** Validate virtualization against the large fixtures (253-slice
+detail, 10k-row browse) before committing a control choice. Include the
+150% DPI path — it exposes real layout regressions.
+
+## 13. Headless integration-test harness (scenarios & workflows)
+
+**Decision (2026-06-16).** Avalonia **headless integration tests that walk real
+scenarios/workflows are the front-and-center verification style** — preferred
+over deferring to "live verification" or unit tests that poke handlers. Build in
+**two fidelities** (hosting Avalonia vs. standing up the real domain differ in
+cost/risk): a **surface-workflow** layer in an Avalonia-headless assembly
+(`FwAvaloniaTests`) — co-host the owned control(s) and drive them through
+page-object drivers (filter/clear/select/type/commit), asserting observable
+state and round-trips like select→detail and edit→refresh; and a **real-domain**
+layer (`xWorksTests`) — a real `RecordClerk` over an in-memory LCModel cache
+asserting the real list narrows/reorders/restores, replacing "needs live
+verification" for domain claims. A read-only grid needs **neither**: cell/sort/
+filter extraction runs through `CollectorEnv : IVwEnv` (managed, SDA-only, no
+`RootBox`), so the cutover is seam re-sourcing, not a text-engine rewrite.
+
+**Canonical code.** `Src/Common/FwAvalonia/FwAvaloniaTests/Workflows/HeadlessWorkflowHarness.cs`
+(`HeadlessStage`, `BrowseTableDriver`, `LexicalEditorDriver`), exemplar
+`FwAvaloniaTests/BrowseEditorIntegrationTests.cs`, real-domain
+`Src/xWorks/xWorksTests/ClerkRoutedFilterTests.cs`. Provenance + the per-phase
+expansion plan:
+`openspec/changes/shared-editable-virtualized-table/headless-integration-harness.md`.
+
+**Gotchas.** Never add `[assembly: AvaloniaTestApplication]` to `xWorksTests`
+(it changes the host for ~1400 tests) — Avalonia hosting lives only in dedicated
+Avalonia-headless assemblies; the full-stack co-host (real clerk → adapter →
+view) belongs in a *new* such project. On the restored test base, create domain
+objects directly (a nested `NonUndoableUnitOfWorkHelper.Do` throws "Nested tasks
+are not supported"). Stand the entries clerk up with the `ConfiguredXHTMLGeneratorTests`/
+`RecordListTests` recipe (`MockFwX(App|Window)`, ``, then `ActivateUI` + `SetSuppressingLoadList(false)` +
+`ReloadList`) or it stays empty. Pump the dispatcher after every acting verb.
diff --git a/.claude/skills/fieldworks-winforms-to-avalonia-migration/references/lessons-learned.md b/.claude/skills/fieldworks-winforms-to-avalonia-migration/references/lessons-learned.md
new file mode 100644
index 0000000000..17926a9084
--- /dev/null
+++ b/.claude/skills/fieldworks-winforms-to-avalonia-migration/references/lessons-learned.md
@@ -0,0 +1,97 @@
+# Lessons Ledger and Skill-Update Protocol
+
+The fieldworks-* migration skills only stay useful if every migration
+updates them. This file defines (a) the routing table that maps each kind
+of discovery to the exact place to record it, and (b) an append-only ledger
+of migration retrospectives so future agents can see how the skill set
+evolved and why.
+
+## Update protocol — where each discovery goes
+
+Run this at the end of every migration (workflow step 10 in SKILL.md), and
+immediately whenever you hit a stale pointer mid-task. Make the edits in
+the same PR as the migration.
+
+| You discovered… | Update |
+| --- | --- |
+| A new architectural pattern, or a refinement of an existing one | `architecture-patterns.md` — add/extend the numbered section (decision, why, canonical code, gotchas); add a row to the SKILL.md quick map if it is load-bearing |
+| A new seam contract | `seam-catalog.md` §1/§2 plus its pivot trigger in §3 |
+| A pivot trigger fired (decision re-evaluated) | Record the outcome inline in `seam-catalog.md` §3 and summarize in the ledger below |
+| A new plugin for a custom slice class | `architecture-patterns.md` §5 canonical-code list; keep the burn-down test list current |
+| A new gotcha / failure mode (interop, DPI, fonts, focus, threading, lifetime…) | The Gotchas paragraph of the matching `architecture-patterns.md` section; if it is a review smell, also add a red flag to the most relevant satellite skill |
+| A new forbidden legacy symbol | `EngineIsolationAuditTests.cs` (the enforcement) and `parity-evidence.md` §4 (the documentation) — both in the same PR |
+| A new evidence lane, artifact type, or evidence-language term | `parity-evidence.md` |
+| A new mandatory step in the per-region process | `migration-checklist.md` (and the workflow list in SKILL.md if it is a new phase) |
+| A trigger phrase that failed to invoke a skill when it should have | The `description` frontmatter of that skill — add the missing vocabulary; keep descriptions quoted (YAML colons) and third-person |
+| A stale file pointer (file moved/renamed, openspec change archived) | Fix the pointer in whichever skill file holds it; prefer pointing at code and tests over change docs |
+| Updated performance baselines | `DataTreeTimingBaselines.json` stays the source of truth; update budget notes in `parity-evidence.md` §5 only if the policy (not the numbers) changed |
+
+Rules of thumb:
+
+- **Skills point, references explain, openspec records provenance.** Do
+ not paste large doc content into skills; capture the durable decision and
+ point at code/tests, citing the openspec doc as provenance.
+- **Generalize before writing.** Record the class of problem, not the
+ one-off instance. If it only applies to one region, it goes in that
+ region's openspec change, not here.
+- **Prune as you add.** If a section no longer pays for its tokens
+ (pattern superseded, gotcha fixed at the framework level), delete or
+ collapse it. Skills are working memory, not an archive — the archive is
+ git history and openspec.
+- **Keep SKILL.md bodies under ~150 lines** and references one level deep
+ from SKILL.md. If a reference outgrows ~300 lines, split it by domain
+ and update the pointers.
+
+## Ledger
+
+Append one entry per completed migration (newest first). Keep entries to
+~10 lines: link to the change, what was migrated, what was learned, which
+skill files changed.
+
+### 2026-06 — Entries browse-table rendering cutover + headless integration harness
+
+- Change: `openspec/changes/shared-editable-virtualized-table/`
+ (`rendering-cutover-design.md`, `headless-integration-harness.md`).
+- Migrated: the lexicon Entries table off the native C++ Views rendering for its
+ surface — owned WS-aware cell renderer (`BrowseCellRenderer`), rich-cell value
+ source via `RegionRichTextAdapter.FromTsString`, and clerk-routed sort/filter
+ (`BrowseViewer.MakeColumnSorter`/`MakeColumnFilter` → `Clerk.OnSorterChanged`/
+ `OnChangeFilter`) replacing the lossy string mirror and the client-side filter
+ projection. Legacy `BrowseViewer` still constructed underneath (F1); its
+ retirement is F2/Stage-13.
+- Key lessons now encoded: **headless integration scenario tests are the
+ front-and-center verification style** (new architecture-patterns.md §13;
+ parity-evidence.md §2a + the "live-verification-only" downgrade;
+ migration-checklist.md Phase 7 gate; SKILL.md workflow step 7 + quick map).
+ A read-only grid needs **neither the C++ engine** (cell/sort/filter extraction
+ runs through the managed `CollectorEnv : IVwEnv`, no `RootBox`) **nor live
+ verification** (real `RecordClerk` narrowing is provable headlessly). Two-layer
+ harness: surface-workflow drivers in an Avalonia-headless assembly + real-clerk
+ layer in `xWorksTests`. Gotchas: never put `[AvaloniaTestApplication]` in
+ `xWorksTests` (~1400 tests share the host); the restored test base holds the
+ undoable task open (no nested `NonUndoableUnitOfWorkHelper`); `OnChangeFilter`
+ takes an (added, removed) delta that `RecordList` composes into its `AndFilter`.
+- Skill files changed: `references/architecture-patterns.md` (§13),
+ `references/parity-evidence.md` (§2a, §3), `references/migration-checklist.md`
+ (Phase 7), `SKILL.md` (quick map + workflow step 7), this ledger.
+
+### 2026-06 — Lexical Edit (full entry view), phases 1–2 (seed entry)
+
+- Change: `openspec/changes/lexical-edit-avalonia-migration/` (plus
+ `avalonia-migration-roadmap`, `lexical-edit-avalonia-poc-spike`).
+- Migrated: first Avalonia lexical-edit region — typed IR pipeline, region
+ composer, owned field controls (`FwMultiWsTextField`, `FwOptionPicker`,
+ menus/flyouts), plugin registry, surface selection service, seam
+ contracts, Path 3 parity harness.
+- Key lessons now encoded: boundary above DataTree (don't extract
+ internals); owned dense controls over stock grids; explicit
+ unsupported rows over silent fallback; one global undo stack; WinForms
+ dialogs own all modality during coexistence; measured (not estimated)
+ performance budgets; StringTable + `.resx` dual localization lanes;
+ `` required for Crowdin satellite builds.
+- Skill set restructured (this commit): skills moved from
+ `.github/skills/` to `.claude/skills/` per AI_GOVERNANCE no-mirror rule;
+ hub skill rewritten with references/ (architecture-patterns, seam-catalog,
+ parity-evidence, migration-checklist, this ledger); satellite skill
+ descriptions rewritten for triggering; fixed YAML-colon bug that broke
+ `fieldworks-ui-wiring-review` triggering.
diff --git a/.claude/skills/fieldworks-winforms-to-avalonia-migration/references/migration-checklist.md b/.claude/skills/fieldworks-winforms-to-avalonia-migration/references/migration-checklist.md
new file mode 100644
index 0000000000..f4a8758c36
--- /dev/null
+++ b/.claude/skills/fieldworks-winforms-to-avalonia-migration/references/migration-checklist.md
@@ -0,0 +1,107 @@
+# Per-Region Migration Checklist
+
+Copy this checklist into your working notes (or the OpenSpec change tasks)
+at the start of a migration and keep it updated. It is the per-region
+definition of done. Items map to the workflow phases in SKILL.md.
+
+## Phase 1 — Inventory and scope
+
+- [ ] Legacy surface identified: entry points, layouts/parts, custom slice
+ classes, dialogs, choosers, command/listener wiring
+- [ ] Custom slice class census taken and compared against the plugin
+ registry (`Src/xWorks/RegionEditorPlugins.cs`) — list of missing
+ plugins recorded
+- [ ] Coverage map drafted (behavior × test status: covered / deferred
+ with owner / blocked by named seam)
+- [ ] Branch scope reviewed with `fieldworks-migration-scope-review`
+ (branch-only diff, split triggers checked)
+
+## Phase 2 — Characterize before refactor
+
+- [ ] Semantic baseline captured for the legacy surface (bindings, labels,
+ editor kinds, visibility, ghost state, focus order, WS metadata,
+ accessibility identity)
+- [ ] Legacy timing baseline measured and committed
+- [ ] Legacy UIA smoke coverage exists for launcher/chooser reachability
+- [ ] All characterization tests run via `./test.ps1` (not branch-only lanes)
+
+## Phase 3 — Seams
+
+- [ ] Existing seams reused from `Src/Common/FwAvalonia/Seams/ISeams.cs`
+- [ ] Any new seam added to `references/seam-catalog.md` with purpose,
+ rules, and pivot trigger
+- [ ] No region code reaches directly into PropertyTable/mediator/LCModel
+ outside a seam
+
+## Phase 4 — Controls
+
+- [ ] Control choices follow architecture-patterns.md §4 (owned controls;
+ bounded TreeView ceiling respected)
+- [ ] Any deviation justified by a fired pivot trigger, recorded in
+ seam-catalog.md §3
+
+## Phase 5 — Region composition
+
+- [ ] Composer walks compiled IR; region model keyed by StableId
+- [ ] Custom classes resolve plugin → companion strip → explicit
+ unsupported row (no silent fallback)
+- [ ] Custom-field placeholders expand from LCModel metadata at compile
+ time; ghost rows are runtime state only
+- [ ] Stable AutomationIds derived from StableId
+ (`{StableId}`, `{StableId}.Label`, `{StableId}.{WsAbbrev}`)
+
+## Phase 6 — Host wiring
+
+- [ ] Every affected host has an explicit `HostUiBehavior` (supported /
+ explicit legacy fallback / blocked)
+- [ ] Full wiring path traced: setting source → persisted state →
+ PropertyTable key → broadcast → listener → host reload → focus and
+ command routing → save/`PrepareToGoAway()` → fallback
+- [ ] Active-host contract holds: no hidden legacy DataTree/menu/renderer
+ driven while Avalonia is active
+- [ ] Reviewed with `fieldworks-ui-wiring-review`
+
+## Phase 7 — Parity evidence
+
+- [ ] **Headless integration scenarios cover the surface's key workflows**
+ (filter → clear, select → detail follows, edit → commit → refresh,
+ navigate), driven via the harness (architecture-patterns.md §13) on
+ `./test.ps1` — surface layer in an Avalonia-headless assembly, plus the
+ real-clerk layer (`xWorksTests`) for domain claims like list narrowing/
+ sort/undo. No behavior/workflow claim left to "live verification" that a
+ headless scenario could prove (parity-evidence.md §2a / §3)
+- [ ] Path 3 bundle produced per scenario (see parity-evidence.md §1)
+- [ ] Semantic, visual, workflow, and performance lanes each prove their
+ own axis; no lane substitutes for another
+- [ ] Performance within budget (≤ legacy total × 1.2, or accepted delta
+ recorded)
+- [ ] 100% and 150% DPI captured
+
+## Phase 8 — Localization
+
+- [ ] Field labels resolve through the StringTable lane via the IR's
+ `LocalizationKey`
+- [ ] Product messages in `FwAvaloniaStrings.resx` with translator comments
+- [ ] New csprojs carry `` (Crowdin satellite build)
+- [ ] AutomationIds nonlocalized; automation Names localized
+- [ ] Reviewed with `fieldworks-localization-review`
+
+## Phase 9 — Retirement and gates
+
+- [ ] Forbidden-symbol audit passes (`EngineIsolationAuditTests.cs`);
+ new forbidden symbols added to the audit and parity-evidence.md §4
+- [ ] Active-host contract tests pass
+ (`RecordEditViewActiveHostContractTests.cs`)
+- [ ] `./build.ps1` and `./test.ps1` pass; `openspec validate
+ --strict` passes when an OpenSpec change is attached
+- [ ] Legacy code scheduled for removal is listed explicitly (what, when,
+ behind which gate)
+
+## Phase 10 — Retrospective (updates this skill set)
+
+- [ ] New patterns/gotchas/pivots recorded per the protocol in
+ `references/lessons-learned.md`
+- [ ] New plugins added to the canonical examples in
+ architecture-patterns.md §5
+- [ ] Stale file pointers in any fieldworks-* skill fixed
+- [ ] Skill edits included in the same PR as the migration
diff --git a/.claude/skills/fieldworks-winforms-to-avalonia-migration/references/parity-evidence.md b/.claude/skills/fieldworks-winforms-to-avalonia-migration/references/parity-evidence.md
new file mode 100644
index 0000000000..b9bffe3689
--- /dev/null
+++ b/.claude/skills/fieldworks-winforms-to-avalonia-migration/references/parity-evidence.md
@@ -0,0 +1,134 @@
+# Parity Evidence — Shared Definitions
+
+Canonical home for the evidence vocabulary used across all fieldworks-*
+migration skills. Other skills reference these definitions instead of
+redefining them.
+
+Contents:
+
+1. The Path 3 bundle (triangulated parity evidence)
+2. Evidence lanes and what each can prove
+3. Evidence language (claim-downgrading taxonomy)
+4. Forbidden symbols (engine isolation)
+5. Performance budgets
+6. Artifact naming
+
+## 1. The Path 3 bundle (triangulated parity evidence)
+
+"Path 3" is the migration-quality visual-fidelity lane. A single artifact
+cannot prove a region is migrated; the bundle triangulates. For one
+scenario id, produce:
+
+- `semantic.json` — semantic snapshot of the IR/region (the anchor; both
+ legacy import and Avalonia compose must produce it)
+- `visual.legacy.png` — WinForms screenshot (100% and 150% DPI)
+- `visual.avalonia.png` — Avalonia capture, same framing/DPI
+ (Avalonia.Headless rendered frames are acceptable when the scenario is
+ explicitly control-scoped)
+- diff/variance artifact interpreted against stable binding/focus/
+ accessibility identity — never a raw pixel diff in isolation
+- `workflow.legacy.md` / `workflow.avalonia.md` — accessibility/keyboard
+ workflow evidence (UIA2 on realized windows for desktop claims)
+- `performance.json` — init/populate/refresh timings
+- `failure-summary.md` — when something fails, classify the broken lane
+ with diagnostics; do not hand reviewers a raw image diff
+
+Canonical harness: `Src/Common/FwAvalonia/FwAvaloniaTests/Path3BundleTests.cs`;
+bundle contract provenance:
+`openspec/changes/lexical-edit-avalonia-migration/coverage-map.md` §9.
+
+## 2. Evidence lanes and what each can prove
+
+| Lane | Tooling | Proves | Cannot prove |
+| --- | --- | --- | --- |
+| Semantic snapshot | IR `ToSnapshot()`, JSON per scenario | Binding, labels, editor kind, visibility, ghost state, focus order, WS metadata, accessibility identity | Typography, density, wrapping, native rendering |
+| Visual/render | Avalonia.Headless Skia capture; legacy screenshots | Layout, density, fonts, wrapping | Why a field is missing (semantic lane explains that) |
+| Workflow/accessibility | Avalonia.Headless input simulation; UIA2/FlaUI on realized windows | Focus movement, invoke paths, chooser reachability, keyboard shortcuts, native automation tree | Pixel fidelity |
+| Performance | Timing harness + committed baselines | Budgets vs. measured legacy | Anything functional |
+
+Headless and desktop are different lanes: a headless smoke test is never a
+UIA2 baseline, and desktop workflow claims need realized-window evidence.
+
+### 2a. Headless integration scenarios (the prioritized default)
+
+Behavior and workflow claims are proven **first** by Avalonia **headless
+integration tests that script real user scenarios** — not deferred to manual
+"live verification," and not a unit test that pokes a handler. Two fidelities
+(pick by what the claim needs), both on `./test.ps1`:
+
+- **Surface-workflow** (`FwAvaloniaTests`): co-host the owned control(s) in a
+ headless window and drive them through page-object drivers (filter, clear,
+ select, type, commit), asserting observable state. Proves control + seam
+ round-trips (e.g. select-row→detail-follows, edit→cell-refresh).
+- **Real-domain** (`xWorksTests`): a real `RecordClerk` over an in-memory
+ LCModel cache, asserting the real list narrows/reorders/restores — the
+ domain-fidelity claim that used to require running FLEx.
+
+Harness + how it scales across phases: architecture-patterns.md §13 and
+`openspec/changes/shared-editable-virtualized-table/headless-integration-harness.md`.
+A surface is not "ready for manual testing" until its key workflows have headless
+scenario coverage; manual/UIA2 desktop runs then confirm pixel/native-tree axes
+the headless lane cannot.
+
+## 3. Evidence language (claim-downgrading taxonomy)
+
+When verifying task checkboxes or PR claims, scan the evidence text for
+these words and downgrade the claim accordingly:
+
+- **substitute** — a different artifact stands in for the claimed one;
+ the claim is unproven.
+- **placeholder** — data or metadata is fake; parity is not demonstrated.
+- **skipped** — the test exists but did not run; no evidence.
+- **future / planned** — work item, not evidence.
+- **partial** — name exactly which axes are proven and which are not.
+- **live-verification-only / manual-only** — a behavior/workflow claim resting
+ solely on running FLEx by hand. Downgrade it: nearly all such claims (filter,
+ sort, selection, edit→refresh, navigation, list narrowing) are provable by a
+ headless integration scenario (§2a). Manual is the supplement for pixel/native
+ axes, not the primary proof.
+
+A checked task whose evidence says any of the above is a review blocker:
+either the evidence improves or the checkbox is unchecked.
+
+## 4. Forbidden symbols (engine isolation)
+
+Migrated-region production code must not reference, in any runtime path:
+
+- `System.Windows.Forms.Control`
+- `DataTree`, `Slice`, `SliceFactory`, `RootSiteControl`
+- `XmlView`, `BrowseViewer`
+- `IVwRootBox`, `IVwEnv`, `IVwGraphics`, `IRenderEngine`
+- `GraphiteEngineClass`, `UniscribeEngineClass`
+- `GeckoWebBrowser`, `XWebBrowser`, `GeckofxHtmlToPdf`
+
+Enforced by `Src/Common/FwAvalonia/FwAvaloniaTests/EngineIsolationAuditTests.cs`.
+When a migration discovers a new legacy symbol that must not leak, add it
+to the audit test AND this list in the same PR. Custom linguistic services
+(XAmple, spelling, parsers, ICU, encoding converters) may remain behind
+explicit service seams when they do not own the render/editor surface.
+
+## 5. Performance budgets
+
+Budgets are measured, never estimated:
+
+1. Capture legacy timings with the characterization harness
+ (`Src/Common/Controls/DetailControls/DetailControlsTests/DataTreeRenderTests.cs`)
+ on a named machine profile; thresholds are committed in
+ `DataTreeTimingBaselines.json`.
+2. Hold the Avalonia surface to within 20% of measured legacy total, or
+ record an explicitly accepted delta with justification in the region
+ manifest.
+3. Measure cold vs. warm separately; IR compile latency must stay
+ deterministic and small; refresh-after-edit has its own baseline.
+4. Always include the 150% DPI path and the large fixtures before
+ accepting a control choice.
+
+## 6. Artifact naming
+
+`{scenarioId}/{bundleId}/semantic.json`, `visual.legacy.png`,
+`visual.avalonia.png`, `workflow.legacy.md`, `workflow.avalonia.md`,
+`performance.json`, `failure-summary.md`.
+
+Snapshots must normalize away pixel bounds, transient generated names,
+timestamps, machine paths, and culture-dependent ordering — see
+`fieldworks-semantic-render-parity` for include/exclude rules.
diff --git a/.claude/skills/fieldworks-winforms-to-avalonia-migration/references/seam-catalog.md b/.claude/skills/fieldworks-winforms-to-avalonia-migration/references/seam-catalog.md
new file mode 100644
index 0000000000..98337847db
--- /dev/null
+++ b/.claude/skills/fieldworks-winforms-to-avalonia-migration/references/seam-catalog.md
@@ -0,0 +1,76 @@
+# Seam Catalog
+
+The seams that separate Avalonia UI from LCModel/xCore/WinForms. All
+contracts live in `Src/Common/FwAvalonia/Seams/ISeams.cs` with
+implementations in `SeamImplementations.cs` and tests in
+`Src/Common/FwAvalonia/FwAvaloniaTests/SeamTests.cs`. Per-seam design docs
+(current state, alternatives considered, required tests) live in
+`openspec/changes/lexical-edit-avalonia-migration/avalonia-*.md`.
+
+Before inventing a new abstraction for a migration, check this table. If a
+seam fits, reuse it. If none fits, add the new seam here (name, purpose,
+rules, pivot trigger) in the same PR that introduces it.
+
+Contents:
+
+1. Seam table
+2. Supporting seams
+3. Pivot triggers (when to revisit a decision)
+
+## 1. Seam table
+
+| Seam | Purpose | Key rules |
+| --- | --- | --- |
+| `IEditSession` | Fenced LCModel undo-task lifecycle: Active → Saved/Canceled → Disposed | One undoable action per save; cancel rolls back without creating an undo action; writes outside a session are a bug |
+| `IUndoRedoCoordinator` | Routes global undo/redo through the LCModel action handler | Control-local text undo stays local until commit; never a parallel committed-state history; refresh region after global undo/redo |
+| `IValidationService` | Deterministic validation over immutable presentation snapshots | Focus-order error ordering; skip unmaterialized lazy items; localized message keys; only severity=Error blocks save; discard stale async results |
+| `IXCoreCommandBridge` | Bridges xCore mediator command routing to Avalonia commands | Region-local commands first; shell-scope wiring happens in the shell phase, not per region |
+| `IUiScheduler` | Thin UI-thread marshalling (`IsOnUiThread`, `Post`) | No hidden `Task.Run`; fakeable in tests; keeps threading visible at the seam |
+| `IRegionLifetime` | Region disposal discipline | Idempotent disposal, late-callback suppression, event-handler cleanup; protects against async work completing after close |
+| `ILexicalRefreshCoordinator` | Mirrors legacy `DoNotRefresh`/`RefreshListNeeded` gating (LT-22414) | Defer PropChanged fan-out during multi-field edits until commit/cancel; characterize legacy behavior before extending (`RefreshCoordinator.cs`) |
+| `IRecordNavigationContext` | Bidirectional selection bridge with the xCore "current record" bus | Follow external navigation and publish selection back; never reach into PropertyTable directly from a region |
+| `IFwClipboard` | Clipboard access without WinForms dependency | See `FwClipboardSeamTests.cs` |
+| `IHostSurface` (focus API) | Host-side focus save/restore around WinForms dialogs | Pairs with the dialog-ownership rules (architecture-patterns.md §7) |
+
+## 2. Supporting seams
+
+- **View definition pipeline:** `IViewDefinitionImporter` /
+ `ViewDefinitionCompiler` / cache keyed by immutable source snapshot
+ (`ViewDefinitionCacheKey.cs`). Off-thread compilation, deterministic
+ output.
+- **Region value provision:** the composer consumes value-provider style
+ seams so region models can be built and tested without LCModel or
+ WinForms.
+- **Active-host contract:** `ActiveHostContract.cs` whitelists the only
+ approved adapters through which an active Avalonia host may touch legacy
+ infrastructure.
+- **Drag/drop and sync-context hygiene:** `FwDragDrop.cs`,
+ `FinalizerSafeSynchronizationContext.cs`.
+
+## 3. Pivot triggers (when to revisit a decision)
+
+A decision below stands until its trigger fires. When a trigger fires,
+record the re-evaluation outcome here and in the lessons ledger.
+
+- **Edit sessions:** adopt a staged-draft model only if fenced direct
+ LCModel sessions prove unacceptably complex or risky in practice.
+- **Undo/redo:** add richer document-local undo only for a specific owned
+ control that needs it, still committing through LCModel.
+- **Validation:** collapse to Avalonia-native validation only for isolated
+ dialogs with no LCModel/cross-object semantics.
+- **UI scheduler / lifetime:** collapse wrappers that demonstrably provide
+ no test or architecture value.
+- **TreeDataGrid:** re-evaluate for browse surfaces if it is relicensed
+ permissively (or SIL accepts a commercial license) AND upstream closes
+ the editing/automation gaps.
+- **VirtualizingStackPanel:** escalate to a fully owned realization-window
+ virtualizer if scroll/expand or open-time budgets fail on the production
+ fixtures (253-slice detail, 10k-row browse).
+- **TreeView ceiling (≤500 items):** raise/remove if a consumable Avalonia
+ release ships TreeView virtualization.
+- **ItemsRepeater:** reconsider as the owned-control substrate if it is
+ un-deprecated with maintained virtualization.
+- **Owned-control cost:** if owned controls overrun, re-open the
+ TreeDataGrid commercial option with measured cost as the baseline.
+- **Stock-control accessibility:** if any adopted stock control fails an
+ accessibility gate, owning its automation peers becomes mandatory.
diff --git a/.claude/skills/smart-screenshot-capture/SKILL.md b/.claude/skills/smart-screenshot-capture/SKILL.md
index 3786b87b60..b616f1f6c3 100644
--- a/.claude/skills/smart-screenshot-capture/SKILL.md
+++ b/.claude/skills/smart-screenshot-capture/SKILL.md
@@ -62,6 +62,7 @@ For this repository, default to:
- transient evidence: `Output/ManualEvidence//`
- OpenSpec review evidence: `openspec/changes//evidence/manual-winapp/`
+- Path 3 parity bundle evidence: `openspec/changes//evidence/parity//`
- ad hoc screenshots: `Output/ManualEvidence/screenshots/`
Create the folder if needed. Do not put scratch screenshots in committed
@@ -73,6 +74,7 @@ Use sorted, descriptive names:
- single capture: `-.png`
- before/after: `01-before-.png`, `02-after-.png`
+- Path 3 parity: `01-winforms-.png`, `02-avalonia-.png`, `03-diff-.png`
- sequence: `step-01-.png`, `step-02-.png`
- app tour: `-.png`
- temporary fallback: `screenshot-YYYY-MM-DD-HHMMSS.png`
@@ -121,6 +123,10 @@ Use multiple captures when one image cannot tell the story:
screenshot;
- comparison: capture both images, then run a screenshot diff when available.
+For migration parity bundles, keep framing, DPI, zoom, and window size matched across WinForms and Avalonia captures whenever density, wrapping, or spacing is under review.
+
+For a Path 3 parity bundle, pair screenshots with the matching semantic snapshot and workflow/accessibility evidence for the same scenario id; a screenshot pair alone is not a full parity claim.
+
For sequences, keep the same target, window size, and framing across captures
unless the task is specifically about responsive or layout behavior.
diff --git a/.github/agents/fieldworks.avalonia-expert.agent.md b/.github/agents/fieldworks.avalonia-expert.agent.md
index 8933db04a8..23895a1783 100644
--- a/.github/agents/fieldworks.avalonia-expert.agent.md
+++ b/.github/agents/fieldworks.avalonia-expert.agent.md
@@ -28,6 +28,20 @@ When you need Avalonia API details, patterns, or examples, you MUST use Context7
- Build: `./build.ps1`
- Tests: `./test.ps1`
+## Repo skills (read before designing anything)
+The migration playbook and decided architecture live in skills under
+`.claude/skills/` (picked up by Copilot and Claude Code alike):
+- `fieldworks-winforms-to-avalonia-migration` — hub playbook; its
+ `references/architecture-patterns.md` and `references/seam-catalog.md`
+ document the decided patterns (typed IR, region composer, owned controls,
+ seams). Do not reinvent abstractions those files already settle.
+- `fieldworks-avalonia-ui` — control/XAML/headless-test conventions and
+ canonical code to imitate.
+- Supporting reviews: `fieldworks-ui-wiring-review`,
+ `fieldworks-uia2-parity-testing`, `fieldworks-semantic-render-parity`,
+ `fieldworks-localization-review`, `fieldworks-migration-scope-review`,
+ `fieldworks-managed-netfx-review`.
+
## Avalonia development guidelines
- Prefer MVVM patterns that are idiomatic for Avalonia.
- Keep UI logic out of XAML code-behind where practical; use view models and bindings.
diff --git a/.github/instructions/avalonia.instructions.md b/.github/instructions/avalonia.instructions.md
new file mode 100644
index 0000000000..134546216a
--- /dev/null
+++ b/.github/instructions/avalonia.instructions.md
@@ -0,0 +1,99 @@
+---
+applyTo: "**/*"
+name: "avalonia.instructions"
+description: "Guidance for FieldWorks Avalonia modules and the shared Preview Host"
+---
+
+# Avalonia Modules (FieldWorks)
+
+## Purpose & Scope
+- Provide a consistent way to **create, build, test, and preview** Avalonia UI modules in FieldWorks.
+- Applies to the Advanced Entry Avalonia work under `specs/010-advanced-entry-view/` and future Avalonia modules.
+- This file covers mechanics (build, layout, logging, preview). The
+ migration playbook, decided architecture patterns, and parity/evidence
+ rules live in the skills under `.claude/skills/` — start with
+ `fieldworks-winforms-to-avalonia-migration` (hub) and
+ `fieldworks-avalonia-ui`.
+
+## Key Rules
+
+### Build & test (always use repo scripts)
+- Build the repo using the traversal script:
+ - `./build.ps1`
+- Run tests using the repo test runner:
+ - `./test.ps1`
+- Do **not** rely on `dotnet build` for repo-wide builds; FieldWorks build targets include tasks that require full Visual Studio/MSBuild.
+
+### Project locations & naming
+- Feature modules live under `Src//.Avalonia/`.
+ - Example: `Src/LexText/AdvancedEntry.Avalonia/`
+- Shared Avalonia utilities live under `Src/Common/FwAvalonia/`.
+- Preview tooling lives under `Src/Common/FwAvaloniaPreviewHost/`.
+
+### Solution + traversal integration (required)
+For every new Avalonia module or tool:
+- Add the project(s) to the traversal build so `./build.ps1` and `./test.ps1` naturally cover them:
+ - `FieldWorks.proj`
+- Add the project(s) to the solution so developers can open/build/debug in Visual Studio:
+ - `FieldWorks.sln`
+
+### Logging (use FieldWorks diagnostics)
+- Module logging must route through the existing FieldWorks diagnostics pipeline (`System.Diagnostics`, `TraceSwitch`, `EnvVarTraceListener`).
+- Add a `TraceSwitch` entry for each module/component in the dev diagnostics config:
+ - `Src/Common/FieldWorks/FieldWorks.Diagnostics.dev.config`
+
+### Preview Host diagnostics (log file)
+- The Preview Host writes startup errors and trace output to a log file next to the executable:
+ - `Output//FieldWorks.trace.log` (e.g. `Output/Debug/FieldWorks.trace.log`)
+- To override the log path, set environment variable `FW_PREVIEW_TRACE_LOG` to a full file path.
+
+### Preview Host (fast UI iteration)
+To preview UI without launching the full FieldWorks app, use the shared Preview Host.
+
+**How modules opt-in**
+- Register the module using an assembly-level attribute:
+ - `FwPreviewModuleAttribute` in `Src/Common/FwAvalonia/Preview/`
+- Provide an optional data provider implementing:
+ - `IFwPreviewDataProvider`
+
+**Run the preview**
+- Use the agent script (build + run):
+ - `./scripts/Agent/Run-AvaloniaPreview.ps1 -Module advanced-entry -Data sample`
+- Supported `-Data` modes depend on the module’s data provider; the current convention is:
+ - `empty` (minimal/default DataContext)
+ - `sample` (representative sample data)
+
+## Expected Structure (current)
+
+- Module:
+ - `Src/LexText/AdvancedEntry.Avalonia/`
+- Shared utilities/contracts:
+ - `Src/Common/FwAvalonia/`
+ - `Diagnostics/` (logging shim)
+ - `Preview/` (module registration + data provider contracts)
+- Preview host executable:
+ - `Src/Common/FwAvaloniaPreviewHost/`
+- Launcher script:
+ - `scripts/Agent/Run-AvaloniaPreview.ps1`
+
+## Examples
+
+### Build everything (recommended)
+```powershell
+./build.ps1
+```
+
+### Run tests
+```powershell
+./test.ps1
+```
+
+### Preview the Advanced Entry module
+```powershell
+./scripts/Agent/Run-AvaloniaPreview.ps1 -Module advanced-entry -Data sample
+```
+
+## Notes & Constraints
+- Avalonia modules should remain **detached from LCModel** for preview scenarios (use DTO/view-model sample data) to keep the Preview Host lightweight.
+- Keep all user-visible strings localizable (use `.resx` patterns where applicable; do not hardcode translatable UI text).
+- Treat any input that crosses managed/native boundaries as untrusted; sanitize and validate per repo security guidance.
diff --git a/.github/prompts/opsx-apply.prompt.md b/.github/prompts/opsx-apply.prompt.md
index e23ec64d14..fedf7b8b88 100644
--- a/.github/prompts/opsx-apply.prompt.md
+++ b/.github/prompts/opsx-apply.prompt.md
@@ -1,149 +1,9 @@
---
description: Implement tasks from an OpenSpec change (Experimental)
+agent: "agent"
+argument-hint: "Optional change name"
---
-Implement tasks from an OpenSpec change.
+Use the `openspec-apply-change` skill from `.claude/skills/openspec-apply-change/SKILL.md`.
-**Input**: Optionally specify a change name (e.g., `/opsx:apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
-
-**Steps**
-
-1. **Select the change**
-
- If a name is provided, use it. Otherwise:
- - Infer from conversation context if the user mentioned a change
- - Auto-select if only one active change exists
- - If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
-
- Always announce: "Using change: " and how to override (e.g., `/opsx:apply `).
-
-2. **Check status to understand the schema**
- ```bash
- openspec status --change "" --json
- ```
- Parse the JSON to understand:
- - `schemaName`: The workflow being used (e.g., "spec-driven")
- - Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
-
-3. **Get apply instructions**
-
- ```bash
- openspec instructions apply --change "" --json
- ```
-
- This returns:
- - `contextFiles`: artifact ID -> array of concrete file paths (varies by schema)
- - Progress (total, complete, remaining)
- - Task list with status
- - Dynamic instruction based on current state
-
- **Handle states:**
- - If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue`
- - If `state: "all_done"`: congratulate, suggest archive
- - Otherwise: proceed to implementation
-
-4. **Read context files**
-
- Read every file path listed under `contextFiles` from the apply instructions output.
- The files depend on the schema being used:
- - **spec-driven**: proposal, specs, design, tasks
- - Other schemas: follow the contextFiles from CLI output
-
-5. **Show current progress**
-
- Display:
- - Schema being used
- - Progress: "N/M tasks complete"
- - Remaining tasks overview
- - Dynamic instruction from CLI
-
-6. **Implement tasks (loop until done or blocked)**
-
- For each pending task:
- - Show which task is being worked on
- - Make the code changes required
- - Keep changes minimal and focused
- - Mark task complete in the tasks file: `- [ ]` → `- [x]`
- - Continue to next task
-
- **Pause if:**
- - Task is unclear → ask for clarification
- - Implementation reveals a design issue → suggest updating artifacts
- - Error or blocker encountered → report and wait for guidance
- - User interrupts
-
-7. **On completion or pause, show status**
-
- Display:
- - Tasks completed this session
- - Overall progress: "N/M tasks complete"
- - If all done: suggest archive
- - If paused: explain why and wait for guidance
-
-**Output During Implementation**
-
-```
-## Implementing: (schema: )
-
-Working on task 3/7:
-[...implementation happening...]
-✓ Task complete
-
-Working on task 4/7:
-[...implementation happening...]
-✓ Task complete
-```
-
-**Output On Completion**
-
-```
-## Implementation Complete
-
-**Change:**
-**Schema:**
-**Progress:** 7/7 tasks complete ✓
-
-### Completed This Session
-- [x] Task 1
-- [x] Task 2
-...
-
-All tasks complete! You can archive this change with `/opsx:archive`.
-```
-
-**Output On Pause (Issue Encountered)**
-
-```
-## Implementation Paused
-
-**Change:**
-**Schema:**
-**Progress:** 4/7 tasks complete
-
-### Issue Encountered
-
-
-**Options:**
-1.
diff --git a/Src/Common/FieldWorks/BuildInclude.targets b/Src/Common/FieldWorks/BuildInclude.targets
index 47f39f2eee..600d43a2e2 100644
--- a/Src/Common/FieldWorks/BuildInclude.targets
+++ b/Src/Common/FieldWorks/BuildInclude.targets
@@ -7,7 +7,6 @@
-
diff --git a/Src/Common/FieldWorks/FieldWorks.Diagnostics.dev.config b/Src/Common/FieldWorks/FieldWorks.Diagnostics.dev.config
index 60d99fc258..b5b8ae202a 100644
--- a/Src/Common/FieldWorks/FieldWorks.Diagnostics.dev.config
+++ b/Src/Common/FieldWorks/FieldWorks.Diagnostics.dev.config
@@ -13,5 +13,7 @@
+
+
diff --git a/Src/Common/FieldWorks/FieldWorks.cs b/Src/Common/FieldWorks/FieldWorks.cs
index 9e01e6df2c..b6c385bb5b 100644
--- a/Src/Common/FieldWorks/FieldWorks.cs
+++ b/Src/Common/FieldWorks/FieldWorks.cs
@@ -2912,6 +2912,8 @@ internal static bool CreateAndInitNewMainWindow(FwApp app, bool fNewCache, Form
EnsureValidReversalIndexConfigFile(app.Cache);
s_activeMainWnd.PropTable.SetProperty("AppSettings", s_appSettings, false);
s_activeMainWnd.PropTable.SetPropertyPersistence("AppSettings", false);
+ s_activeMainWnd.PropTable.SetProperty("UIMode", string.IsNullOrWhiteSpace(s_appSettings.UIMode) ? "Legacy" : s_appSettings.UIMode, false);
+ s_activeMainWnd.PropTable.SetPropertyPersistence("UIMode", false);
}
catch (StartupException ex)
{
@@ -3863,6 +3865,25 @@ private static void ExitCleanly()
{
DataUpdateMonitor.ClearSemaphore();
+ // Defense in depth: an undo task somebody left open (e.g. an editing surface that
+ // failed to close its fenced session) would make the shutdown Save() throw "Commit
+ // at wrong place." and lose ALL unsaved work. Roll the leaked task back HERE, on
+ // the UI thread that owns the UOW write lock — CommitAndDisposeCache may run on the
+ // progress dialog's worker thread, where the rollback would be rejected.
+ var actionHandler = s_cache.ActionHandlerAccessor;
+ if (actionHandler.CurrentDepth > 0)
+ {
+ Logger.WriteEvent("Shutdown found an undo task still open; rolling it back so the save can proceed.");
+ try
+ {
+ actionHandler.Rollback(0);
+ }
+ catch (Exception e)
+ {
+ Logger.WriteError(e);
+ }
+ }
+
using (var progressDlg = new ProgressDialogWithTask(s_threadHelper))
{
progressDlg.Title = string.Format(ResourceHelper.GetResourceString("kstidShutdownCaption"),
diff --git a/Src/Common/FieldWorks/WelcomeToFieldWorksDlg.cs b/Src/Common/FieldWorks/WelcomeToFieldWorksDlg.cs
index 867ebe9b23..0bf19dc8b3 100644
--- a/Src/Common/FieldWorks/WelcomeToFieldWorksDlg.cs
+++ b/Src/Common/FieldWorks/WelcomeToFieldWorksDlg.cs
@@ -298,6 +298,15 @@ private void Import_Click(object sender, EventArgs e)
private void m_btnOptions_Click(object sender, EventArgs e)
{
+ // Migrated Options dialog: in New (Avalonia) UI mode show the owned Avalonia Options dialog.
+ // This is the pre-project (bare-bones) path — no cache/mediator/project — so Plugins are
+ // unavailable. Legacy mode keeps the WinForms dialog.
+ var settings = new FwApplicationSettings();
+ if (AvaloniaOptionsDialogLauncher.ShouldUseAvaloniaOptionsDialog(settings.UIMode))
+ {
+ AvaloniaOptionsDialogLauncher.Show(null, null, null, settings, null, this);
+ return;
+ }
var optionsDlg = new LexOptionsDlg();
optionsDlg.InitBareBones(m_helpTopicProvider);
optionsDlg.Show(this);
diff --git a/Src/Common/FwAvalonia/AvaloniaDialogHost.cs b/Src/Common/FwAvalonia/AvaloniaDialogHost.cs
new file mode 100644
index 0000000000..e9b315d061
--- /dev/null
+++ b/Src/Common/FwAvalonia/AvaloniaDialogHost.cs
@@ -0,0 +1,210 @@
+// Copyright (c) 2026 SIL International
+// This software is licensed under the LGPL, version 2.1 or later
+// (http://www.gnu.org/licenses/lgpl-2.1.html)
+
+using System;
+using System.Windows.Forms;
+using Avalonia.Win32.Interoperability;
+using AvControl = Avalonia.Controls.Control;
+
+namespace SIL.FieldWorks.Common.FwAvalonia
+{
+ ///
+ /// Reusable host that shows an Avalonia dialog body (a UserControl) inside a WinForms-owned
+ /// modal during coexistence — the turn-key piece for the MVVM dialog kit. Because
+ /// Avalonia modal windows are not supported while WinForms owns the message loop (dialog-ownership.md),
+ /// the dialog body is hosted in a WinForms modal window owned by the caller's form; the view-model
+ /// closes it by raising (no windowing in the VM).
+ ///
+ /// A new dialog is then: build the view + view-model, call .
+ ///
+ public static class AvaloniaDialogHost
+ {
+ ///
+ /// Shows modally over . Returns the accepted
+ /// result (true = OK, false = Cancel), or null if the window was closed without an OK/Cancel.
+ ///
+ /// The optional parameters extend the fixed-size default WITHOUT changing it for existing callers:
+ /// * — when true the modal gets a sizable border and a minimum size
+ /// (defaulting to the initial / unless
+ /// / are supplied). Default false keeps the
+ /// legacy behavior.
+ /// * / — an optional
+ /// size-persistence hook (mirrors the label-column-width persistence pattern: the caller owns the
+ /// remembered value, keyed by dialog identity, so a resized dialog reopens at its last size). The
+ /// get-hook (when it returns a value) seeds the initial client size in place of
+ /// /; the set-hook is invoked on close with the
+ /// final client size. Only honored when is true.
+ ///
+ public static bool? ShowModal(
+ IWin32Window owner,
+ AvControl dialogBody,
+ IDialogViewModel viewModel,
+ string title,
+ int width = 420,
+ int height = 320,
+ bool resizable = false,
+ int? minWidth = null,
+ int? minHeight = null,
+ Func getRememberedSize = null,
+ Action rememberedSizeChanged = null)
+ {
+ if (dialogBody == null) throw new ArgumentNullException(nameof(dialogBody));
+ if (viewModel == null) throw new ArgumentNullException(nameof(viewModel));
+
+ // Modal hosting + Avalonia share the single WinForms UI thread / message loop during coexistence
+ // (dialog-ownership.md). Showing a modal Form or touching Avalonia controls off that thread is a
+ // re-entrancy/cross-thread bug; fail fast rather than corrupt the message loop. The owner control
+ // (when supplied) is the WinForms host whose thread we must be on.
+ if (owner is Control ownerControl && ownerControl.InvokeRequired)
+ throw new InvalidOperationException(
+ "AvaloniaDialogHost.ShowModal must be called on the UI thread (the WinForms message-loop thread).");
+
+ FwAvaloniaRuntime.EnsureInitialized();
+
+ // Compact density so every kit dialog matches the legacy WinForms dialog density, not the
+ // roomy Fluent defaults. Applied here (the single dialog chokepoint) so new dialogs inherit it.
+ CompactDialogStyles.Apply(dialogBody);
+
+ // dialog-ownership.md: remember the WinForms focus so it returns to the owner after close.
+ var priorFocus = Form.ActiveForm?.ActiveControl;
+
+ try
+ {
+ using (var form = new Form
+ {
+ Text = title ?? string.Empty,
+ StartPosition = FormStartPosition.CenterParent,
+ MinimizeBox = false,
+ MaximizeBox = false,
+ ShowInTaskbar = false
+ })
+ {
+ // Border / min-size / initial (possibly remembered) size — extracted so it is unit-testable
+ // without spinning a real modal window.
+ ApplySizing(form, width, height, resizable, minWidth, minHeight, getRememberedSize);
+
+ var host = new WinFormsAvaloniaControlHost { Dock = DockStyle.Fill, Content = dialogBody };
+ form.Controls.Add(host);
+
+ bool? result = null;
+ using (WireClose(viewModel, accepted =>
+ {
+ result = accepted;
+ form.DialogResult = accepted ? DialogResult.OK : DialogResult.Cancel;
+ form.Close();
+ }))
+ {
+ form.ShowDialog(owner);
+ }
+
+ // Persist the final size (only meaningful for resizable dialogs with a set-hook).
+ if (resizable)
+ rememberedSizeChanged?.Invoke(form.ClientSize);
+
+ if (priorFocus != null && !priorFocus.IsDisposed)
+ priorFocus.Focus();
+ return result;
+ }
+ }
+ finally
+ {
+ // Done in a finally so a failed ShowDialog still releases them.
+ DisposeDialogResources(dialogBody, viewModel);
+ }
+ }
+
+ ///
+ /// Releases the resources owns once the modal closes: the host owns the
+ /// dialog body it was handed and disposes it; the view-model owns its own resources
+ /// () and is disposed here if it is . Idempotent
+ /// and null-tolerant; factored out so the ownership/disposal contract is unit-testable without
+ /// spinning a real modal window.
+ ///
+ public static void DisposeDialogResources(AvControl dialogBody, IDialogViewModel viewModel)
+ {
+ (dialogBody as IDisposable)?.Dispose();
+ (viewModel as IDisposable)?.Dispose();
+ }
+
+ ///
+ /// Applies the border style, minimum size and initial (possibly remembered) client size to the hosting
+ /// modal . Factored out of so the sizing/persistence
+ /// contract is unit-testable without spinning a real modal window:
+ /// * false → and no min-size
+ /// (the legacy default; //get-hook ignored);
+ /// * true → with a min client size
+ /// (/, defaulting to the initial
+ /// /), and the get-hook (when it returns a value)
+ /// seeds the initial client size in place of /.
+ /// The initial size is clamped up to the minimum so a stale remembered size can never open below it.
+ ///
+ public static void ApplySizing(
+ Form form,
+ int width,
+ int height,
+ bool resizable,
+ int? minWidth = null,
+ int? minHeight = null,
+ Func getRememberedSize = null)
+ {
+ if (form == null) throw new ArgumentNullException(nameof(form));
+
+ if (!resizable)
+ {
+ // Legacy fixed-size behavior, unchanged for existing callers.
+ form.FormBorderStyle = FormBorderStyle.FixedDialog;
+ form.ClientSize = new System.Drawing.Size(width, height);
+ return;
+ }
+
+ form.FormBorderStyle = FormBorderStyle.Sizable;
+
+ // Minimum size defaults to the initial size so a resizable dialog can never shrink below its
+ // design size unless the caller opts into a smaller floor.
+ var minW = minWidth ?? width;
+ var minH = minHeight ?? height;
+
+ // MinimumSize is an OUTER (window) size in WinForms; converting the client minimum to an outer
+ // minimum needs a realized handle. Setting the client size first lets us derive the chrome delta.
+ var initial = getRememberedSize?.Invoke() ?? new System.Drawing.Size(width, height);
+
+ // Clamp the (possibly stale/remembered) initial client size up to the client minimum.
+ var initialW = Math.Max(initial.Width, minW);
+ var initialH = Math.Max(initial.Height, minH);
+ form.ClientSize = new System.Drawing.Size(initialW, initialH);
+
+ // Derive the chrome delta from the realized form so MinimumSize (an outer size) corresponds to the
+ // requested CLIENT minimum. Falls back to the client minimum if the handle is not yet realized.
+ var chromeW = form.Width - form.ClientSize.Width;
+ var chromeH = form.Height - form.ClientSize.Height;
+ form.MinimumSize = new System.Drawing.Size(minW + Math.Max(0, chromeW), minH + Math.Max(0, chromeH));
+ }
+
+ ///
+ /// Subscribes a dialog view-model's close signal to ; dispose the result
+ /// to unsubscribe. Exposed (and used by ) so the close wiring is unit-testable
+ /// without spinning a real modal window.
+ ///
+ public static IDisposable WireClose(IDialogViewModel viewModel, Action onClose)
+ {
+ if (viewModel == null) throw new ArgumentNullException(nameof(viewModel));
+ if (onClose == null) throw new ArgumentNullException(nameof(onClose));
+
+ void Handler(object sender, bool accepted) => onClose(accepted);
+ viewModel.CloseRequested += Handler;
+ return new Unsubscriber(() => viewModel.CloseRequested -= Handler);
+ }
+
+ private sealed class Unsubscriber : IDisposable
+ {
+ private Action _dispose;
+ public Unsubscriber(Action dispose) => _dispose = dispose;
+ public void Dispose()
+ {
+ _dispose?.Invoke();
+ _dispose = null;
+ }
+ }
+ }
+}
diff --git a/Src/Common/FwAvalonia/AvaloniaRegionHostControl.cs b/Src/Common/FwAvalonia/AvaloniaRegionHostControl.cs
new file mode 100644
index 0000000000..8cfea827a6
--- /dev/null
+++ b/Src/Common/FwAvalonia/AvaloniaRegionHostControl.cs
@@ -0,0 +1,205 @@
+// Copyright (c) 2026 SIL International
+// This software is licensed under the LGPL, version 2.1 or later
+// (http://www.gnu.org/licenses/lgpl-2.1.html)
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Windows.Forms;
+using Avalonia.Win32.Interoperability;
+using SIL.FieldWorks.Common.FwAvalonia.Region;
+using SIL.FieldWorks.Common.FwAvalonia.Seams;
+
+namespace SIL.FieldWorks.Common.FwAvalonia
+{
+ ///
+ /// Reusable WinForms host for an Avalonia region surface (Stage 2.1: generalized out of
+ /// so a second region host does not re-derive the in-process
+ /// net48 plumbing). Owns Avalonia bootstrap, the , the
+ /// companion-control strip, the WinForms/Avalonia directional-key interop, focus-safe content
+ /// swapping, context menus, and the message/clear states. Region-specific projection (building the
+ /// region view) belongs to the derived class via .
+ ///
+ public abstract class AvaloniaRegionHostControl : System.Windows.Forms.UserControl
+ {
+ private static readonly TraceSwitch s_interopTrace =
+ new TraceSwitch("FwAvaloniaHostInterop", "WinForms/Avalonia keyboard interop diagnostics");
+
+ /// The Avalonia content host. Protected so derived region hosts can set content directly.
+ protected readonly WinFormsAvaloniaControlHost Host;
+ private readonly Panel _companionStrip;
+
+ /// Raised after a hosted region reports an edit completed (wired by the derived host).
+ public event EventHandler RegionEditCompleted;
+
+ protected AvaloniaRegionHostControl()
+ {
+ FwAvaloniaRuntime.EnsureInitialized();
+
+ Dock = DockStyle.Fill;
+ TabStop = true;
+
+ Host = new WinFormsAvaloniaControlHost
+ {
+ Dock = DockStyle.Fill,
+ Name = "AvaloniaHost",
+ AccessibleName = FwAvaloniaStrings.AvaloniaHostName
+ };
+ Host.PreviewKeyDown += OnHostPreviewKeyDown;
+
+ _companionStrip = new Panel
+ {
+ Dock = DockStyle.Top,
+ Name = "CompanionStrip",
+ AccessibleName = "RecordEditView.AvaloniaHost.CompanionStrip",
+ Visible = false,
+ Height = 0,
+ TabStop = false
+ };
+
+ Controls.Add(Host);
+ Controls.Add(_companionStrip);
+ Clear();
+ }
+
+ protected void RaiseRegionEditCompleted() => RegionEditCompleted?.Invoke(this, EventArgs.Empty);
+
+ /// Swaps the hosted Avalonia content and shows the control.
+ protected void SetHostContent(Avalonia.Controls.Control content)
+ {
+ Host.Content = content;
+ Show();
+ }
+
+ /// The current Avalonia content, or null.
+ protected Avalonia.Controls.Control CurrentContent => Host.Content as Avalonia.Controls.Control;
+
+ private void LogInterop(string message)
+ {
+ if (s_interopTrace.TraceInfo)
+ Trace.WriteLine("[" + GetType().Name + "] " + message);
+ }
+
+ private static bool IsDirectionalKey(int keyCode)
+ {
+ switch (keyCode)
+ {
+ case 0x26:
+ case 0x28:
+ case 0x25:
+ case 0x27:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static bool ShouldBypassWinFormsDirectionalKeyHandling(bool hostContainsFocus, int keyCode)
+ => hostContainsFocus && IsDirectionalKey(keyCode);
+
+ private void OnHostPreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
+ {
+ var keyCode = (int)(e.KeyData & Keys.KeyCode);
+ if (ShouldBypassWinFormsDirectionalKeyHandling(Host != null && Host.ContainsFocus, keyCode))
+ {
+ e.IsInputKey = true;
+ LogInterop("PreviewKeyDown -> IsInputKey=true for " + ((Keys)keyCode));
+ }
+ }
+
+ protected override bool IsInputKey(Keys keyData)
+ {
+ var keyCode = (int)(keyData & Keys.KeyCode);
+ if (ShouldBypassWinFormsDirectionalKeyHandling(Host != null && Host.ContainsFocus, keyCode))
+ {
+ LogInterop("IsInputKey -> true for " + ((Keys)keyCode));
+ return true;
+ }
+
+ return base.IsInputKey(keyData);
+ }
+
+ protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
+ {
+ var keyCode = (int)(keyData & Keys.KeyCode);
+ if (ShouldBypassWinFormsDirectionalKeyHandling(Host != null && Host.ContainsFocus, keyCode))
+ {
+ LogInterop("ProcessCmdKey bypass for " + ((Keys)keyCode)
+ + " while Avalonia host contains focus.");
+ return false;
+ }
+
+ return base.ProcessCmdKey(ref msg, keyData);
+ }
+
+ public void SetCompanionControls(IReadOnlyList controls)
+ {
+ for (var i = _companionStrip.Controls.Count - 1; i >= 0; i--)
+ {
+ var existing = _companionStrip.Controls[i];
+ existing.SizeChanged -= OnCompanionControlSizeChanged;
+ _companionStrip.Controls.RemoveAt(i);
+ }
+
+ if (controls != null)
+ {
+ for (var i = controls.Count - 1; i >= 0; i--)
+ {
+ var control = controls[i];
+ if (control == null)
+ continue;
+ control.Dock = DockStyle.Top;
+ control.SizeChanged += OnCompanionControlSizeChanged;
+ _companionStrip.Controls.Add(control);
+ }
+ }
+
+ UpdateCompanionStripHeight();
+ }
+
+ private void OnCompanionControlSizeChanged(object sender, EventArgs e)
+ => UpdateCompanionStripHeight();
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && _companionStrip != null)
+ {
+ if (Host != null)
+ Host.PreviewKeyDown -= OnHostPreviewKeyDown;
+ for (var i = _companionStrip.Controls.Count - 1; i >= 0; i--)
+ {
+ var companion = _companionStrip.Controls[i];
+ companion.SizeChanged -= OnCompanionControlSizeChanged;
+ _companionStrip.Controls.RemoveAt(i);
+ }
+ }
+ base.Dispose(disposing);
+ }
+
+ private void UpdateCompanionStripHeight()
+ {
+ var height = 0;
+ foreach (Control child in _companionStrip.Controls)
+ height += child.Height;
+ _companionStrip.Height = height;
+ _companionStrip.Visible = height > 0;
+ }
+
+ public void ShowContextMenu(IReadOnlyList items)
+ {
+ if (Host.Content is Avalonia.Controls.Control target)
+ RegionMenuFlyout.Show(items, target);
+ }
+
+ public void ShowMessage(string message)
+ {
+ Host.Content = new Avalonia.Controls.TextBlock { Text = message ?? string.Empty };
+ Show();
+ }
+
+ public void Clear()
+ {
+ ShowMessage(FwAvaloniaStrings.NoEntrySelected);
+ }
+ }
+}
diff --git a/Src/Common/FwAvalonia/CompactDialogStyles.cs b/Src/Common/FwAvalonia/CompactDialogStyles.cs
new file mode 100644
index 0000000000..088f47e0dd
--- /dev/null
+++ b/Src/Common/FwAvalonia/CompactDialogStyles.cs
@@ -0,0 +1,83 @@
+// Copyright (c) 2026 SIL International
+// This software is licensed under the LGPL, version 2.1 or later
+// (http://www.gnu.org/licenses/lgpl-2.1.html)
+
+using System.Collections.Generic;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Layout;
+using Avalonia.Styling;
+
+namespace SIL.FieldWorks.Common.FwAvalonia
+{
+ ///
+ /// Compact density for Avalonia dialogs — the design baseline so migrated dialogs match the legacy
+ /// WinForms dialog density (small font, tight padding, no Fluent min-height floors) rather than the
+ /// roomy Fluent defaults. Applied once by to every hosted dialog
+ /// body, so EVERY dialog shown through the kit inherits it automatically — new dialogs need no
+ /// per-dialog density work. Scoped to the dialog's control subtree (added to its Styles), so
+ /// it never affects the region/table surfaces, which own their own density ().
+ ///
+ public static class CompactDialogStyles
+ {
+ /// Dialog body font (≈ the legacy 8.25pt WinForms dialog font, vs the ~14px Fluent default).
+ public const double DialogFontSize = 12.0;
+
+ /// Min height for compact line controls (buttons/combos/text boxes), vs the Fluent ~32px floor.
+ public const double LineControlMinHeight = 23.0;
+
+ ///
+ /// Marks a control whose subtree already has the compact styles, so is genuinely
+ /// idempotent (a second call is a no-op rather than appending a duplicate style set).
+ ///
+ private static readonly AttachedProperty AppliedProperty =
+ AvaloniaProperty.RegisterAttached("CompactDialogStylesApplied", typeof(CompactDialogStyles));
+
+ ///
+ /// Adds the compact dialog styles to a dialog body's control subtree. Idempotent: calling it again on
+ /// the same control does nothing (the styles are added at most once), so re-hosting or a double call
+ /// can't stack duplicate styles.
+ ///
+ public static void Apply(Control dialogBody)
+ {
+ if (dialogBody == null || dialogBody.GetValue(AppliedProperty))
+ return;
+ dialogBody.SetValue(AppliedProperty, true);
+ foreach (var style in Build())
+ dialogBody.Styles.Add(style);
+ }
+
+ private static IEnumerable Build()
+ {
+ yield return Templated