diff --git a/docs/native-theme-migration.md b/docs/native-theme-migration.md
new file mode 100644
index 00000000..336a0042
--- /dev/null
+++ b/docs/native-theme-migration.md
@@ -0,0 +1,294 @@
+# Migrate the UI to GPUI-Kit, then native-theme
+
+Plan to move coop's custom UI elements onto [GPUI-Kit](https://gpui-kit.com)
+(`gpui-kit` 0.7.1), delete the custom theme implementation (`crates/theme`),
+and then integrate [`native-theme`](https://github.com/tiborgats/native-theme)
+through its official `native-theme-gpui` connector.
+
+> **Revision.** This supersedes the earlier approach in this file (depend on
+> the `native-theme` core crate and hand-write a `ResolvedTheme` → coop token
+> mapping). Going through GPUI-Kit first makes that mapping unnecessary: the
+> connector already projects native themes onto gpui-component's `Theme`, and
+> every call site reads `cx.theme()` from gpui-component directly.
+
+## 1. What the git history shows
+
+- `516eb0e` — *move gpui-components to ui crate*: the UI crate **was** the
+ upstream component library, vendored into `crates/ui`. Call sites used
+ library components and its theme vocabulary
+ (`cx.theme().elevated_surface_background`, `surface_background`,
+ `border_variant`, …).
+- `0c1c23f` — *migrate to gpui-base (#51)*: the vendored library was deleted
+ and replaced by the `gpui-base` crate plus hand-written wrappers (Button,
+ Switch, Dialog, Input, Dock, Menu, Notification, …) and the custom
+ `crates/theme`.
+
+"Migrate like before" therefore means: go back to the upstream component
+library — this time as a dependency (`gpui-kit`), not vendored — and let it
+own the theme.
+
+## 2. Strategy
+
+```mermaid
+graph LR
+ A[Phase A: GPUI-Kit] --> B[Phase B: native-theme-gpui]
+ A1[A1 Bootstrap
init + open_window + Root] --> A2[A2 Component swap
~20 custom modules]
+ A2 --> A3[A3 Theme removal
cx.theme = gpui-component]
+ A3 --> B1[B1 Connector
apply_system_theme]
+ B1 --> B2[B2 Appearance + presets
settings wiring]
+```
+
+**Phase A** replaces the custom elements with `gpui_kit::component`
+components and moves all color reads onto gpui-component's `Theme`
+(`ThemeColor`, 139 semantic fields). The app runs on gpui-component's
+built-in light/dark palettes at the end of Phase A; `crates/theme` is gone.
+
+**Phase B** adds `native-theme-gpui`, which installs native OS themes (or any
+of 16 bundled presets) into that same `Theme`. No call site changes — the
+connector is verified against exactly the versions Phase A pins.
+
+### Target dependency set (exact versions)
+
+| Crate | Version | Note |
+|---|---|---|
+| `gpui-kit` | 0.7.1 | facade: `gpui_kit::*` = GPUI (`gpui-pre`), `gpui_kit::component` (gpui-component 0.7.1), `gpui_kit::base` (gpui-base 0.7.1), `gpui_kit::assets` |
+| `gpui-pre` | =0.3.8 | already pinned; gpui-kit 0.7.1 requires it |
+| `native-theme-gpui` | 0.6 | Phase B; verified against gpui-kit 0.7.1 / gpui-pre 0.3.8; needs Rust ≥ 1.95 (toolchain is 1.98) |
+
+Do not jump to `gpui-kit` 0.8: `native-theme-gpui` 0.6.1 pins 0.7.1, and a
+gpui-component patch release has broken the connector before.
+
+## 3. Current custom UI inventory
+
+`crates/ui` is ~8,700 lines across 34 files. Consumers import it at ~138
+sites (`workspace` 14 files, `chat_ui`, `nip29_ui`, `nip4e`, `settings`,
+`desktop`, `web`); theme tokens are read at ~340 sites across the same
+crates.
+
+| Custom module | Lines | GPUI-Kit replacement |
+|---|---|---|
+| `menu/` (popup, dropdown, item) | 983 | `menu::{PopupMenu, PopupMenuItem, DropdownMenu, ContextMenu}` |
+| `dock/` (mod, panel) | 1,032 | `dock` (entity-backed DockArea / panels) |
+| `motion.rs` | 668 | keep — app-level animation, no theme dependency |
+| `notification.rs` | 594 | `notification::Notification` + `WindowExt::push_notification`; placement via `Theme.notification` |
+| `button.rs` | 581 | `button::{Button, ButtonGroup, Toggle, DropdownButton}` |
+| `message.rs` | 553 | evaluate `message::{Message, Bubble, Attachment, MessageScroller}`; keep as app composite where it fits better |
+| `avatar.rs` | 445 | `avatar::Avatar` |
+| `root.rs` | 430 | framework `Root` via `gpui_kit::open_window` (owns dialogs, notifications, tooltip/menu layers, focus traps) |
+| `dialog.rs` | 394 | `WindowExt::open_dialog` / `AlertDialog`; custom footer buttons → Dialog's footer API |
+| `input/` | ~290 | `input::{Input, InputState, Textarea, NumberInput}`; delete the custom styling (its `input_bg`/`selection`/`caret` reads die with it) |
+| `icon.rs` | 274 | `Icon` + `IconName` (Lucide); keep `ui::icon` as a thin mapping shim (see §4.3) |
+| `kbd.rs` | 270 | `Kbd` |
+| `tab/` | 385 | `tab::{Tab, TabBar, Tabs}` |
+| `title_bar.rs` | 218 | `TitleBar` (keep the client-side-decoration constants) |
+| `popover.rs` | 170 | `popover::Popover` |
+| `styled.rs` | 161 | `h_flex`/`v_flex`/`StyledExt` from the kit; drop coop-specific helpers as their users migrate |
+| `switch.rs` | 157 | `switch::Switch` |
+| `markdown.rs` | 146 | `text::TextView::markdown` (code styling via `Theme.highlight_theme`) |
+| `nav.rs` + `nav_item.rs` | 268 | `sidebar::{Sidebar, SidebarMenu, …}`; if the coop layout doesn't fit, keep as an app composite on top of kit primitives — decide during the slice |
+| `group_box.rs` | 126 | `group_box::GroupBox` |
+| `scroll/` | ~124 | `scroll::{Scrollbar, Scrollable}` |
+| `resizable/` | 31 | `resizable::{h_resizable, v_resizable, resizable_panel}` |
+| `tooltip.rs` | 34 | `.tooltip()` |
+| `indicator.rs` | 59 | `Marker`/`Badge`, or keep if neither fits |
+| `window_ext.rs` | 90 | `gpui_kit::component::WindowExt` |
+
+## 4. Phase A — GPUI-Kit migration
+
+### 4.1 Bootstrap
+
+- `desktop/main.rs` and `web/lib.rs`: `gpui_kit::init(cx)` first, then open
+ the window with `gpui_kit::open_window(opts, cx, |window, cx| …)`, which
+ installs the framework `Root` (dialog, sheet, notification and tooltip
+ layers). Delete `ui::Root` and its layers; `workspace::init` stays as the
+ window's content view.
+- `TitlebarOptions` / client-side decoration setup stays coop's
+ (`CLIENT_SIDE_DECORATION_*`, `TABBAR_HEIGHT` move to `ui`).
+- Keep `bind_keys` **before** `set_menus` (guide rule); coop already does.
+
+### 4.2 Component slices (each PR compiles)
+
+Replace one family at a time; consumers update in the same PR:
+
+1. **Deps + bootstrap** (above). Custom components still work alongside the
+ framework Root.
+2. **Icons** — introduce the `ui::icon` shim so later slices see kit icons
+ through the old call sites (see §4.3).
+3. **Small controls** — Button, Switch, Kbd, GroupBox, Avatar, Tooltip,
+ Indicator, Popover.
+4. **Menu** — PopupMenu/DropdownMenu/ContextMenu (+ the popup-menu actions
+ already imported from `gpui_base::actions`).
+5. **Input** — Input/InputState/Textarea; port the chat composer, dialog
+ fields, relay editor; drop the clear-button and input-styling modules.
+6. **Dialog + Notification** — `open_dialog`/`AlertDialog`/
+ `push_notification`; port the settings, relays, profile, new-chat,
+ import… dialogs; the custom footers move to the Dialog footer API.
+7. **Scroll + Resizable + Tab** — panels and the workspace tab strip.
+8. **Sidebar/Nav** — Sidebar family (or keep composites, per the table).
+9. **Title bar** — kit `TitleBar` + traffic-light handling.
+10. **Dock** — the biggest single item (805 + 205 lines, `dock::add`
+ registry, `DockPlacement`). Port panels onto `DockArea`; do it last so
+ the panels it hosts are already kit-based.
+11. **Message + Markdown** — TextView::markdown; Message/Bubble where they
+ fit, otherwise keep `ui::message` as an app composite (it is domain
+ layout, allowed by the guides' app-component layer).
+12. **Cleanup** — delete the replaced modules; decide what `crates/ui` still
+ is (see §4.4).
+
+### 4.3 Icons
+
+coop's `IconName` has 128 variants backed by `assets/icons/*.svg`; the kit's
+`IconName` (Lucide) has 106. Keep `ui::icon` as a mapping module: a
+coop-`IconName` enum (same variant names) that resolves to the kit `IconName`
+where a Lucide equivalent exists, and to a custom asset path (coop's bundled
+SVGs) where it doesn't (brand marks, a few specials). Most `Icon::new(...)`
+call sites then keep compiling with import changes only. Delete
+`assets/icons` entries that get Lucide equivalents; keep `assets/brand`.
+
+### 4.4 Theme removal
+
+- **Call sites**: `theme::ActiveTheme` → `gpui_kit::component`'s `ActiveTheme`
+ (`cx.theme() -> &Theme`). Rewrite the ~340 token reads with the mapping
+ below (done per consumer crate, one slice each: `ui`, `workspace`,
+ `chat_ui`, `nip29_ui`, `settings` + `nip4e`).
+
+| coop token (`cx.theme().x`) | gpui-component `ThemeColor` |
+|---|---|
+| `bg` | `background` |
+| `surface` | `sidebar` |
+| `surface_raised` | `secondary` |
+| `surface_card` | `muted` |
+| `surface_dialog`, `surface_overlay` | `popover` |
+| `element_hover` | `accent` (gpui-component's accent **is** the item highlight) |
+| `element_active` | `list_active` |
+| `border` | `border` |
+| `border_faint`, `border_strong` | `border` (one border token; faintness loss accepted) |
+| `ring` | `ring` |
+| `text` | `foreground` |
+| `text_muted`, `text_faint`, `text_dim` | `muted_foreground` |
+| `cursor`, `caret` | `caret` |
+| `selection` | `selection` |
+| `solid` / `on_solid` | `primary` / `primary_foreground` |
+| `accent`, `accent_strong` | `primary` (emphasis ink) |
+| `danger` / `danger_muted` | `danger` |
+| `danger_strong` | `button_danger` |
+| `warning` / `warning_muted` | `warning` / `warning.alpha(≈0.15)` at the call site |
+| `success` | `success` |
+| `input_bg`, `selection`, `caret` inside the custom input | die with the custom Input (§3) |
+| `code_text`, `code_wash` | die with `ui::markdown` → `TextView` |
+| `drop_line` / `drop_target` | `drag_border` / `drop_target` |
+
+- **Non-color fields** move to `Theme` directly: `font_sans`/`font_mono` →
+ `theme.font_family`/`mono_font_family`; `shadow` → `theme.shadow`;
+ `scrollbar_mode` → `theme.scrollbar_mode`; notification placement →
+ `theme.notification` (gpui-component ships the same `NotificationSettings`
+ coop's was modeled on); `is_dark()` → `theme.mode`.
+- **`crates/theme` disposition**:
+ - delete: `lib.rs` (Theme global, `AppExt`, `sync_base`, `PaletteBuilder`),
+ `colors.rs`, `palette.rs`, `brand.rs`, `appearance.rs`, `paint.rs`,
+ `color.rs`, `syntax.rs`, `notification.rs`, `scrollbar_mode.rs`,
+ `platform_kind.rs`;
+ - move to `crates/ui` unchanged: `typography.rs` (`TextStyle`/`Typeset`,
+ ~200 call sites — retained deliberately as coop's type ladder),
+ `layout.rs` (spacing constants, radius accessors), `geometry.rs`
+ (`Edges`/`Side`/`AxisExt`), and the three `CLIENT_SIDE_DECORATION_*`
+ constants;
+ - `AppearanceMode` (System/Light/Dark, serde-persisted) moves into
+ `crates/settings`.
+- The gpui-base projection coop did in `sync_base` is now the framework's job
+ (`Theme::update` / `Theme::change` keep colors, tokens and the base layer
+ in step per the coding guides).
+
+### 4.5 Appearance handling (end of Phase A)
+
+- `settings::apply_theme` → `Theme::change(mode, None, cx)` for a pinned
+ Light/Dark, `Theme::sync_system_appearance(None, cx)` for System.
+- `workspace`'s `Theme::sync_system_appearance(Some(window), cx)` call stays,
+ now against the kit's `Theme`; window-appearance observation comes with the
+ framework Root/theme (verify; if not, keep the existing
+ `observe_window_appearance` wiring).
+
+## 5. Phase B — native-theme-gpui
+
+1. Add `native-theme-gpui = "0.6"` (pulls `native-theme`; keep default
+ features or trim icon features coop doesn't use).
+2. Boot, after `gpui_kit::init` and before opening the window:
+
+ ```rust,ignore
+ match native_theme::SystemTheme::from_system() {
+ Ok(sys) => native_theme_gpui::apply_system_theme(&sys, cx),
+ Err(error) => {
+ log::warn!("system theme unavailable: {error}");
+ // per-platform fallback preset: macos-sonoma / kde-breeze / windows-11
+ let prefs = native_theme_gpui::AccessibilityPreferences::default();
+ if let Ok((theme, resolved)) = native_theme_gpui::from_preset("macos-sonoma", true, &prefs) {
+ native_theme_gpui::apply(theme, &resolved, &prefs, cx);
+ }
+ }
+ }
+ ```
+
+ `apply_system_theme` stores **both** variants and a `ThemeConfig` per
+ mode; afterwards the Phase A controls keep working unchanged:
+ `Theme::sync_system_appearance` / `Theme::change` switch between the
+ native palettes.
+3. Optional preset picker (the same one proposed in the superseded plan):
+ `settings.theme_preset: Option`, a dropdown in Preferences →
+ Appearance; selecting one calls `from_preset(name, is_dark, &prefs)` +
+ `apply` for both variants.
+4. Fonts, radius, scrollbar geometry and the base layer are written by the
+ connector (it also re-asserts them after each upstream rebuild via its
+ observer). Coop writes nothing theme-related itself.
+
+wasm: `SystemTheme::from_system` is unavailable on wasm32 — cfg-gate step 2
+to native targets; on web, apply the fallback preset (or extend
+`native-theme` with a web story later; presets need no OS reader).
+
+## 6. Testing and QA
+
+- Per slice: `cargo check` workspace, `cargo clippy`, `cargo fmt --check`,
+ plus the affected crate's tests; UI behavior verified by hand in the
+ desktop app.
+- **Early spike (before slice 2)**: `cargo check -p web --target
+ wasm32-unknown-unknown` with the `gpui-kit` dependency added. The vendored
+ library was wasm-capable; confirm the published crates are too before
+ committing to the migration. If they are not, stop and reassess — the web
+ target is a launch blocker.
+- End of Phase A:
+ - all dialogs open/close, focus trap and restore works; nested
+ popover-in-dialog works (framework Root owns the layers);
+ - notifications stack and auto-hide; settings' appearance dropdown switches
+ Light/Dark/System with a full repaint;
+ - dock panels move, split and zoom; sidebar resizes; scrollbars behave in
+ Hover/Always/Scrolling modes;
+ - markdown code blocks render through `TextView` with syntax colors from
+ `Theme.highlight_theme`.
+- End of Phase B:
+ - macOS: OS light/dark follows; pinned modes; accent changes picked up at
+ next launch (documented limitation — no `watch` feature);
+ - Linux (KDE + GNOME/portal) and Windows: theme reads work or fall back to
+ the platform preset with a logged warning;
+ - preset switch renders in both modes; inputs/menus/dialogs/tooltips/
+ scrollbars all paint from the native palette;
+ - startup shows no wrong-palette flash (apply before the first window).
+
+## 7. Risks
+
+| Risk | Mitigation |
+|---|---|
+| Scale: ~8.7k lines of custom UI, ~140 `ui::` imports, ~340 token reads | Slices in §4.2 each compile; `crates/ui` acts as the facade during the migration so consumer churn is incremental |
+| Dock port (largest, most stateful) | Scheduled last; panels are kit-based by then; DockArea owns layout state coop currently hand-rolls |
+| gpui-component on wasm | Spike in §6 before committing; fallback would be staying on 0.7.1 desktop-only and parking the web target |
+| Visual identity changes (shadcn-style stock palette in Phase A) | Accepted — requirement says the current palette need not survive; Phase B replaces it with native themes anyway |
+| Icon gaps (coop 128 vs Lucide 106) | Mapping shim keeps coop names; unmatched variants fall back to bundled SVGs |
+| API drift between coop's wrappers and the kit (e.g. `Sizable`, `Disableable`, callback shapes) | Per the coding guides: never invent APIs; verify each component page / source signature during its slice |
+| `TextStyle` ladder is a deliberate holdout from rem-based text | Documented exception; sizes are absolute points and don't claim to zoom. Revisit only if the kit's text scale is adopted later |
+| native-theme-gpui version coupling | Pin `gpui-kit = "=0.7.1"`-era set (0.7.1, gpui-pre =0.3.8) matching the connector's verified table; bump all together |
+
+## 8. Out of scope
+
+- Migrating `motion.rs` or the panel-background image feature.
+- Accessibility text scaling / reduce-motion plumbing (connector supports
+ it; adopt after Phase B if wanted).
+- Replacing coop's composited panels (chat, screening, backup…) with kit
+ `Sheet`/`Settings` — revisit after the migration settles.