diff --git a/docs/dev/core-model.md b/docs/dev/core-model.md
index a26aa3d9..10260836 100644
--- a/docs/dev/core-model.md
+++ b/docs/dev/core-model.md
@@ -1409,3 +1409,107 @@ or field, so moving them means moving `WindowManager`, the blockers of section 1
`WorkAreaOffset`, ...): thin calls of `WindowManager` methods that edit a workspace and re-layout, the
same coupling;
- queries, configuration, borders, animations, transparency, schema: statics and the platform.
+
+## 22. What Windows keeps platform-specific for a shared `WindowManager
` (kfw, wip-u-win-deep)
+
+Written against `wip-cand7` plus the revisions of `wip-u-win-deep`: `PlatformMonitorId
` is in
+`komorebi-core` (blocker 5 of section 18), the Windows `monitor_idx_from_current_pos` has KDE's text, and
+`promote-focus` on a custom layout is the `WorkspacePlatform::promote_focus_container_idx` hook (blocker
+6, the fallback part). This is the hook list kfde can implement: every item says what Windows does, what
+KDE and macOS do (from sections 17, 18 and 20), and the proposed shape. Items are ordered by how many
+shared functions they touch.
+
+### 22.1 The unmanaged-window gate (blocker 1): not converged, and why
+
+`handle_unmanaged_window_behaviour()` (refuse a command when `OperationBehaviour::NoOp` is set and the
+focused window is not in a container of the focused workspace) is called at the top of 18 functions on
+Windows: `move_container_to_monitor`, `move_container_to_workspace`, `focus_container_in_direction`,
+`move_container_in_direction`, `focus_container_in_cycle_direction`, `move_container_in_cycle_direction`,
+`cycle_container_window_in_direction`, `cycle_container_window_index_in_direction`,
+`focus_container_window`, `stack_all`, `unstack_all`, `add_window_to_container`,
+`promote_container_to_front`, `promote_container_swap`, `promote_focus_to_front`,
+`remove_window_from_container`, `toggle_monocle`, `toggle_maximize`. KDE and macOS call it once in the
+router, before the `match`, for every message. Moving Windows to the router is **not** behaviour-neutral
+and was not done: the reading of `process_command` shows four ways in which the two forms differ.
+
+1. The router form refuses every message, including queries, rule and configuration commands, the
+ workspace-focus commands and `stop` (those never call one of the 18); the per-function form refuses
+ only what reaches one. A router that gated the same set would need a message list
+ (`requires_managed_focus(&SocketMessage)`) that nothing shares.
+2. An arm that does other work first is not refused as a whole on Windows: `FocusStackWindow` focuses the
+ monitor under the pointer and only then calls `focus_container_window`; `MoveContainerToNamedWorkspace`
+ and the `Cycle*Container*` arms look up the target first; a router gate would refuse before any of it.
+3. The layer split: `FocusWindow`, `MoveWindow` and `CycleFocusWindow` call a gated function in the Tiling
+ layer and an ungated floating function in the Floating layer, so one message is refused in one layer
+ only; a router gate refuses both.
+4. Nested and direct callers that do not pass the router: `focus_container_window` is also called from the
+ arm of `process_command` that focuses a window by its location (`WorkspaceWindowLocation::Monocle` and
+ `Container`), `move_container_to_workspace` from `move_container_to_monitor` and from
+ `move_container_in_direction`, `toggle_monocle` and `toggle_maximize` from
+ `move_container_in_direction` and the cycle functions, and the unit tests of `window_manager.rs` call
+ all of them directly. The nested calls are covered by the outer gate; the direct ones are not.
+
+The hook that makes both shapes expressible is a defaulted method on the shared router,
+`fn gate_message(&self, message: &SocketMessage) -> eyre::Result<()>`: the default (KDE, macOS) calls
+`handle_unmanaged_window_behaviour` for every message; Windows returns `Ok(())` and keeps the call inside
+the 18 functions (they stay Windows text until the owner decides that Windows should refuse more).
+
+### 22.2 Hooks of the window manager (`WindowManagerPlatform`)
+
+| Windows keeps | What it is | KDE / macOS | Shape |
+|---|---|---|---|
+| `new`, `init` | `new` takes the command listener, the event channel and the socket; `init` loads monitor and workspace information through `WindowsApi`, uncloaks left-behind windows, starts the border and stackbar machinery | KDE: backend monitors and desktops; macOS: run loop and AX | `fn init(&mut WindowManager)`, `fn new(..)` as associated functions; the struct's platform fields in `P::State` (section 2: `known_hwnds`, `virtual_desktop_id`, `uncloack_to_ignore`, `has_pending_raise_op`, `pending_move_op` with handle and rect, `already_moved_window_handles`) |
+| `stop` | writes `komorebi.state.json`, switches animations off, waits for them, restores every window, turns focus-follows-mouse off, shuts the subscriber sockets, removes the socket file, waits for a bar restart in progress, `process::exit(0)` | KDE: `backend().shutdown()`; macOS: its own | `fn stop(&mut WindowManager, ignore_restore: bool)`; the shared part is the restore, the rest is the hook |
+| `restore_all_windows` | restores each window by `WindowsApi` (cloak/hide/minimize, the maximized and monocle windows), skipping windows the user hid | KDE and macOS have their own | `fn restore_all_windows(..)`; Windows' body stays |
+| `resize_window` | resizes by edge with DPI scaling and the min-size rules (58 lines); `ResizeWindowEdge` additionally goes through the live-resize guard | macOS 52 lines of its own, KDE none (the compositor) | `fn resize_window(..)`; the arm `ResizeWindowEdge` is a hook too |
+| `focus_workspace` | plans a slide transition (`workspace_transition_plan`, `focus_workspace_towards`) and runs the animation, or shows and hides windows | KDE asks the compositor for the desktop | plan/execute split: `fn plan_workspace_transition(..) -> Plan` (shared) and `fn execute(plan)` (hook) |
+| `enforce_workspace_rules`, `manage_focused_window`, `unmanage_focused_window`, `monitor_idx_from_window` | rules apply to windows found by `WindowsApi` and move them with `SetWindowPos`; manage/unmanage are fed by an event on Windows and a notification on KDE | differ in input | hooks `fn windows_to_enforce(..)`, `fn manage_focused(..)` |
+| `move_container_in_direction` | a DPI check across monitors (`monitors_have_same_dpi`) before a cross-monitor move | none | `fn before_cross_monitor_move(a, b) -> eyre::Result<()>`, no-op default |
+| `move_floating_window_in_direction`, `focus_floating_window_in_direction` | border offsets and the floating-window search (105 lines) | KDE and macOS: 29-line variants | stay platform; extension trait |
+
+### 22.3 Borders, stackbars, animations: the `BorderPlatform` hook
+
+`window_manager.rs` calls the border manager in few places: `cycle_monocle` destroys the borders
+(`border_manager::destroy_all_borders`), `stop` reads the border implementation, the ghost kinds are set in
+one free function; the router arms `Border*`, `Retile*`, `ReloadStaticConfiguration` and `TogglePause` set
+`force_update_borders` and notify the border thread. `monocle_on` and `monocle_off` also hide and restore
+the floating windows (through the backend). The stackbar manager is not called from `window_manager.rs`. KDE draws borders in the
+compositor and calls none of these, macOS has its own border thread. Shape: a defaulted
+`BorderPlatform` with `fn borders_changed(&mut self)`, `fn destroy_borders(&mut self)`,
+`fn hide_floating_for_monocle(&mut self, hide: bool)`; defaults do nothing (KDE) and macOS implements
+its own. The `force_update_borders` flag of the router becomes the return value of those hooks, as
+`replace_configuration` already returns it.
+
+### 22.4 Wallpaper, window-handling behaviour, workspace names, custom layouts
+
+- **Wallpaper.** `apply_wallpaper_for_monitor_workspace` finds the wallpaper of a monitor/workspace pair
+ and calls `Workspace::apply_wallpaper` (which is the `WorkspacePlatform::apply_wallpaper` hook:
+ `WindowsApi::set_wallpaper` through COM on Windows, the backend on KDE, nothing on macOS); the shared
+ function takes a `PlatformMonitorId` now and can move as it is.
+- **Window handling behaviour.** `WINDOW_HANDLING_BEHAVIOUR` (Sync/Async, the `ASYNC_WINDOW_POS` flag of
+ `SetWindowPos`) is read inside `WindowsApi::position_window`, never in `window_manager.rs`: it is a
+ setting of the Windows `set_rect`, not a hook of the window manager. Keep it in `Settings::platform`.
+- **Workspace names.** `set_workspace_name` records the name in the monitor's `Extra` (`workspace_names`),
+ and `known_workspace_name` is already the `MonitorPlatform` hook; a shared `set_workspace_name` calls a
+ `fn record_workspace_name(monitor: &mut Monitor, idx: usize, name: &str)` hook (default: nothing).
+- **Custom layouts.** `Layout::Custom` stays in `komorebi-layouts`; the Windows arms of `cycle_layout`,
+ `promote_container_swap` and `change_workspace_layout_default` fail for a custom layout without a primary
+ column, which `primary_container_idx` already expresses; the one real difference (`promote-focus` falls
+ back to the first column) is `promote_focus_container_idx`, done in this branch.
+
+### 22.5 Cosmetic differences: nothing left to do
+
+Section 18 lists which near-identical functions were cosmetic and converged (`bail!`, `Lockable`, log
+text, the `SessionFloatRules` and `State` arms); the rest of its table is real. Re-reading the Windows and
+KDE copies of the functions that differ by at most four lines against that table found no cosmetic
+difference left (`float_window` and the `focus_*` family read `mouse_follows_focus` into a local on KDE
+because of a borrow, and Windows has no such need: they stay as they are).
+
+### 22.6 Order for kfde
+
+1. `fn gate_message` on the shared router (22.1), Windows `Ok(())`.
+2. `BorderPlatform` (22.3) with no-op defaults: unblocks `cycle_monocle`, `retile_all`, `monocle_on/off`.
+3. `record_workspace_name` (22.4) and the `PlatformMonitorId` use in the shared wallpaper function.
+4. The plan/execute split of `focus_workspace`, then `init`/`stop`/`new`/`restore_all_windows` as hooks.
+Gates for each: Windows `commands`, `workspaces`, `focus`, `stacking`, `monocle` (maximizing), `borders`,
+`layouts` scenarios; KDE fast tier and VM for the router change.