> For the complete documentation index, see [llms.txt](https://docs.kouch.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kouch.dev/docs/plan_v2.md).

# Kouch Plan v2 — native-feeling, Big Picture-style, very customizable

Living plan, approved by the owner 2026-09-26. Every phase that lands updates its **Status** line here and the "Current state" section of `CLAUDE.md`. Decisions are recorded in ADRs 0008–0016.

## Alpha status (2026-09-26)

**Built and verified by the agents** (per-phase details in each Status line below):

* All ten phases are implemented. Phases 1–10 are verified with tests, CDP screenshots and live runs of the real app on isolated builds: a separate identifier, temp config/data, no Steam.
* **Release candidate:** an NSIS installer and a SteamPipe depot. Install, run, tester report, exit and uninstall are all clean.
* **Linux:** an AppImage and a .deb, with a headless smoke test.
* **Real emulators:** four local test profiles install and configure their emulators on their own. Each was launched live with one of the owner's games (screenshot read back, graceful quit). They're bundled as `profiles/local/alpha-emulators.kod`.
* **LAN:** discovery, pairing, transfer and co-op, verified with two instances on one PC.
* **Controllers:** SDL motion, DSU, rumble delivery and every hotplug scenario are covered by tests, and seated pads were read live over DSU.
* **Reviews:** two independent code reviews; every finding is fixed.
* **Tester guide:** `docs/ALPHA.md`.

**Only the owner (or hardware) can close these:**

1. **Steam Input + motion, live, in a game.** The Steam-launched Kouch runs elevated and can't be restarted by agents. Close it, or run Steam normally, then tilt a pad. This run also checks seated pads in all four emulators.
2. **Unplugging pads for real.** Early Tests 2/5/6/15/16: the logic is covered by tests, the hardware run is pending.
3. **Booting a game that needs the owner's own keys.** Kouch never touches keys.
4. **The Deck performance gate.** It needs a Deck.
5. **Kouch's own Steam app id.** Needed for the Workshop, netplay invites, Remote Play Together and real Steam Cloud writes.

## Why

The owner wants Kouch to be very customizable and to feel native and smooth, like Steam's Big Picture:

* **Look:** themes and custom themes, a Black/White toggle, an ambient floating-shapes background, and sounds.
* **Art and devices:** automatic game art, system/device images, and every major controller treated as first-class — including every split-pair combination, and keyboard + mouse as a player.
* **Connect screen:** a new one built from the owner's mockups, with smooth pop-in and resize.
* **Setup and storage:** an EmuDeck-style, multi-disk `Emulation` folder with a first-run disk picker.
* **Launching:** per-game launch options, game-style settings menus, and a per-game mod menu backed by Workshop.
* **Online:** Steam netplay where every player's mods match.
* **Extensibility:** sandboxed plugins.

**Stack (2026-09-26).** We keep Rust core + Tauri 2 + Svelte 5. The owner briefly chose to fork ES-DE, then asked whether forking was too much work. The recommendation adopted instead is to borrow ES-DE's *ideas* without its code:

* grid / carousel / list views with a hero panel
* theme variants and aspect ratios
* reuse of the art a user already has in `downloaded_media` / `gamelist.xml`

**Safety net:** Phase 1 ends with a performance gate on the Steam Deck in Game Mode. If the gate fails, the fork is reconsidered with measured numbers.

**What the code looked like at the start** (exploration, 2026-09-26):

* **Theme engine:** only resolves the accent colour; nothing loads themes.
* **Library grid:** remounts on every page turn.
* **Art:** local file-path art cannot display, because the asset protocol is off.
* **Launch:**
  * `window_mode`, `rom.profile_id` and `Library::hash` are unused.
  * The validator rejects `{saves}`-style placeholders.
* **Mods:** refused.
* **Native-feel gaps:**
  * The window flashes white at startup.
  * Kouch doesn't remember the window's size/position or fullscreen state.
  * Context menu and zoom are not blocked.
  * Sheets pop in instead of sliding.
* **Steam:** the vendored steamworks-rs already wraps Workshop, lobbies, networking sockets and rich presence; none of it is used yet.
* **Battery:** never read.

## Parallel sessions

| Session          | Role                           | Owns                                                                                                                                                                                            | First task                                               |
| ---------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| kouch side 3     | **A — shell & look**           | `app/ui` shell / home / library / game page / theme engine / ambient background / sounds / shared components (`Card`, `Sheet`, `Prompt`, `tokens.css`); `app/src-tauri` window + asset protocol | Phase 1, then Phase 2                                    |
| kouch side 4     | **B — core & storage**         | `crates/kouch-profiles`, `kouch-library`, `kouch-launch`, `kouch-updater`; `app/src-tauri` commands + `payloads.rs` (the contract)                                                              | Profile schema v2 + `config_edit` xml, then Phase 4 core |
| kouch side (ui?) | **C — controllers & setup UI** | `app/ui/src/screens/{Claim*,Setup*,Settings/Controllers,Settings/Storage}`; `crates/kouch-input`, `kouch-core`, `kouch-steam`; the SVG sets                                                     | Phase 3                                                  |
| later            | **D — online**                 | Workshop, netplay, plugins                                                                                                                                                                      | once Kouch's app id lands                                |
| kouch central    | coordinator                    | this plan, ADRs, `CLAUDE.md`, reviews, gates, acceptance run                                                                                                                                    | acceptance run                                           |

Rules for working in parallel:

* **Separate clones.** Each session works in its own clone outside Dropbox (`C:\dev\kouch-a`, `-b`, `-c`). That gives each one its own index, tree and `target\`, so there's no shared-index damage and no Dropbox mtime lag. Git worktrees don't work on this checkout.
* **Committing.** Rebase on `origin/main` before every push, and keep commits small.
* **Live Steam runs.** Only one session at a time runs Kouch under Steam. The "Kouch Lab" shortcut points at the Dropbox checkout's `target\steamlaunch`. Ask kouch central before taking it.
* **Contract changes.** Changes to `payloads.rs` ↔ `types.ts` go through B. Request them with a cross-session message.

## Phase 1 — Smooth and native + Deck gate (A)

**Status:** done; the Deck gate met on fps for the alpha on 2026-09-27, with p95 two-frame spikes remaining (A); re-met after the Home + Library redesign, in Game Mode on the extracted Linux tree the Steam depot will ship (every phase at or over 85.5 fps except one connect tour at 84.5; HOME\_LIBRARY\_PLAN H9, 2026-09-27) — 2e347ac: `kmedia` scoped protocol (library roots, data dir, storage media root; system/bios refused) with a WebP thumbnail cache, lazy/async images, pages keyed by index (no remount), black window shown after first paint (5 s fallback), F11 + `display.fullscreen` + auto borderless under SteamDeck/SteamGamepadUI, window-state, context menu/drag/zoom/overscroll blocked, Sheet/Modal enter/exit transitions (`lib/motion.ts`), width-capped root size, frame-time HUD (`window.__kouchPerf`). Checked at 960×540 → 4K, 21:9, portrait, 150 %. Headless Chrome with GPU on the owner's PC (uncapped, window.\_\_kouchPerf, 2026-09-26): idle with the ambient background p95 2.2 ms / max 2.3 ms; Home shelf navigation p95 2.2 / max 6.2 ms; classic page turns p95 2.2 / max 6.3 ms — far inside the 20 ms budget. **Deck, first numbers 2026-09-26 (kouch central):** the AppImage (525b1ee + abc1b0c) on the owner's Deck OLED (SteamOS, glibc 2.41, mesa 26.1, no host WebKitGTK) over SSH, `KOUCH_NO_STEAM=1`, temp config/data in `~/kouch-dev`, `KOUCH_PERF_TOUR=1`. Desktop Mode (KWin, 90 Hz): library 88.6 fps p95 12 ms, connect 88.4 / 16, ambient 89.9 / 12, sheet 86.3 / 17. Nested gamescope 1280×800 (the Game Mode compositor, X11 path): 62 fps in every phase, p95 17–18 ms, max 18–42 ms. All PASS (≥ 55 fps, p95 ≤ 20 ms). **With 2,000 games (dev AppImage 1aed43e, 2026-09-26): the Library phase FAILS**:

* nested gamescope: 59.9 / 60.1 fps, p95 23 / 21 ms, max 65 / 58 ms;
* KDE: 59.2 fps, p95 25 ms, max 85 ms;
* connect, ambient and sheet still pass (p95 17–19 ms). Sent to A: the grid's scroll cost grows with the game count under WebKitGTK.
* **The gate is now the display's own refresh** (owner, 2026-09-26: "it has to match the decks frame rate, which on this model is 90", then "make sure it syncs the hz to the monitor of each user"). On Windows, WebView2 already follows the monitor: 476 fps measured on the owner's 480 Hz display. Under gamescope that needed ADR 0015 (`1a98d97`). Pass means fps ≥ 0.95 × refresh and p95 ≤ 1.1 × the frame budget (≤ 12.2 ms at 90 Hz); the tour's rule becomes relative to the measured refresh.
  * **KDE Wayland, panel on, 2,000 games (64615a5):** library 75.5 fps / p95 23 ms, connect 85.8 / 17, sheet 84.7 / 17 (all FAIL at 90), ambient 89.7 / 12 (PASS).
  * Earlier "60 fps" runs were measured with the panel asleep (DPMS off), where WebKit falls back to a fixed 60 fps timer.
  * **Game Mode:** under gamescope WebKitGTK fell back to that 60 fps timer, because its DRM vblank monitor matches the panel by physical size and gamescope's Xwayland reports a made-up one. ADR 0015 fixes it (`1a98d97`): in real Game Mode the tour measures hz = 90 and the ambient phase holds 89.8 fps.
  * **Real Game Mode, 90 Hz, 2,000 games (6b9a3bb):** library 80.6 fps / p95 19 ms, connect 86.1 / 17, sheet 84.7 / 17 (FAIL at 90), ambient 89.7 / 12 (PASS).
  * **Source-mapped WebKit profiles on the Deck** (a sourcemap AppImage and the remote inspector) led A to: one Svelte update per grid row instead of per scroll frame, no forced layouts in the focus reveal or slide sizes, the connect screen skipping FLIP when no seat moves (`99a9fb8`, `ce985da`, `133b921`), raw state for the games list, and a hidden Home that stops reacting (`3fa06f3`). JS main-thread time fell \~19 %, but the frame numbers barely moved. Next: `KOUCH_PERF_TOUR_GAMES` runs at 42 / 500 / 2,000 to see what scales with the library.
  * **2026-09-27, Game Mode at 90 Hz (`c7b0257`), two tours:** library 87.1 / 84.8 fps, connect 85.4 / 85.4, sheet 86.7 / 85.4, ambient 89.8. A library step now takes 12.8 ms from press to the drawn frame (it was 23), of which 5.8 ms is WebKit's own frame work. First paint 1.7 s cold, 1.05 s warm (it was 6 s).
  * **How we got there** (measured on the Deck with runtime CSS injection through the WebKit inspector, a source-mapped profile and per-step timing, `2bb59a8`/`a54c740`):
    * Library size isn't a factor: 42, 500 and 2,000 games measure the same.
    * WebKit's renderer settings are already the best; no environment knob helps.
    * Compositing the scroller or its cells changes nothing.
    * Card painting isn't the limit (83 fps with the cards hidden).
    * The cost was the per-card focus transitions: any transition on the old or new focused card cost \~5 fps and \~8 ms per step, and pre-promoting layers didn't help. The fix is one persistent focus ring that glides between cells while the cards change state instantly (`c7b0257`).
    * Also fixed on the way: FLIP keyframes animating `transform-origin`, which WebKit runs on the main thread (a 67 fps connect screen, `d708c86`); the GStreamer registry rescanned on every AppImage launch (`2089c65`, `ea9c3e9`); UI sounds decoding before first paint (`ff2e698`).
  * **Sounds and the sheet (`7cf9a69`, `1ea9772`, `acd50ad`):** on the Deck UI sounds go through Web Audio (`web_audio=1`, no `<audio>` fallback). Each cue restarted the idle output, though, which cost the sheet phase \~30 hitches. A silent keep-alive now holds the output open while Kouch is in front, so sounds on and off measure the same (sheet 87.0 / 37 hitches vs 87.3 / 33). The focus reveal now runs after paint, and the sheet's first-open frame dropped to 48–49 ms. **Status for the alpha (accepted 2026-09-27):** fps is at the 85.5 bar on every navigation phase (library 85–87, connect 85–87, sheet 87, ambient 90), and first paint is 1.05–1.7 s. p95 stays at 16–18 ms: about 5 % of frames still take two vsyncs, and the sheet's first open costs one long frame. WebView2 on Windows follows the monitor (476 fps on a 480 Hz display).
* **Deck rig (kouch central):** `~/kouch-dev` on the Deck holds temp config/data, 2,000 placeholder games, a stand-in emulator (xterm that logs the game file and every key) and `tools.sh`. A dev AppImage (`-DevVpad`) runs inside an invisible headless gamescope at 1280×800 (`kouch_hl`), driven by the virtual controller over `/dev/tcp` (`vpad`) and captured with `gamescopectl screenshot` (`gshot`). Verified there:
  * drive names ("Internal storage" + model, "microSD card", no read-only system partition);
  * the first pad to press A becomes Player 1 with auto-claim off;
  * wizard → Set up later → Rescan (2,000 games) → game page → Play: the stand-in launched full screen with the right file. Found and fixed: no pad could navigate a fresh install (`74eeb71`), invisible focus rings on buttons (`dccf951`), no Quick Menu under gamescope (`7dfaf9f`). In real Game Mode (a gm-run round: the shortcut launches the AppImage, a stand-in game, the vpad chord, full-composition captures), games launched from Kouch were black and the Quick Menu was never drawn. Fixed by B: the launch ids restored for emulators (`9ffbc23`), and the Quick Menu as a same-app override-redirect panel with pinned size hints (`dcc19eb`, `6b9a3bb`). Verified over the running game (Early Test 10).
* **Deck gate: build ready, needs the device (B, 2026-09-26).** Linux release bundles build in Docker (`scripts/linux-build.ps1`): AppImage 104 MB (carries WebKitGTK), `.deb` 13 MB, binary 32 MB, SDL3 static, `libsteam_api.so` via RUNPATH. Headless smoke passes: the release binary runs under Xvfb 1280×800 with `KOUCH_NO_STEAM=1` and renders the first-run wizard (after fixing a Linux-only startup abort: click-through on the never-shown overlay). Workspace clippy and tests (539) pass on Linux. Still Linux gaps: emulator window discovery / focus / borderless / hotkeys (C, in progress), the keyboard-seat reader (evdev), and every measurement on the Deck itself. **Scale check 2026-09-26** (real app, isolated build, 10,042 games: the owner's 42 + 10,000 generated files in a temp folder, online art off): full scan 1.1 s, `library_list` 54 ms, 80 rows of grid scrolling at p95 2.2 ms / max 8.5 ms with 868 DOM nodes.

**Images**

* Turn on Tauri's asset protocol, scoped to the library and media roots, and load local files through `convertFileSrc`.
* Rust generates tile-sized WebP thumbnails into a cache.
* Use `decoding="async"` and `loading="lazy"`.
* No base64 art.

**Library**

* Stop remounting pages on every turn by fixing the `{#each}` key.
* Keep only the current page ±1 mounted.
* Set `will-change` only on elements that are moving.

**Native feel**

* Black `backgroundColor`, and don't show the window until the first paint.
* Block the context menu, image drag, pinch/Ctrl+wheel zoom and overscroll.
* Fullscreen via F11 and a new `settings.display.fullscreen`.
* Auto borderless fullscreen under Big Picture / Game Mode (`SteamDeck` / `SteamGamepadUI`).
* Remember window size and position with `tauri-plugin-window-state`.
* Real enter/exit transitions for sheets and modals.

**Responsive**

* Breakpoints by size and aspect: 16:10, 16:9, 21:9, small window, portrait.
* Check every screen from 960×540 up to 4K.

**Gate:** frame-time HUD numbers on Windows and on the Deck in Game Mode. Library page turns, the connect-screen re-layout and the ambient background must each hold **≥ 55 fps with p95 frame time ≤ 20 ms at 1280×800 on the Deck**. If they don't, stop and report to the owner.

## Phase 2 — Big Picture-style shell + built-in theme (A)

**Status:** done (A) — 715bbcf + 77dabba: Home (hero + shelves), Library (system tabs, sort/filter sheet, windowed scrolling grid, classic 5×3 as a view), left main menu (Start) + right quick access (View), GamePage (Options wired to Phase 7, Mods/Netplay tabs, Details), shared Card, Black/White with the accent re-gated on white and darker player variants, AmbientBackground, 13 sound slots + generated music + volumes, Look/Sound settings. Screens in both schemes at 1280×800 / 1920×1080 / 2560×1080 / 3840×2160 / 150 % / 960×540 / portrait. DESIGN.md §4b documents it.

We follow Big Picture's structure and interaction model. We do **not** copy Valve's art, icons or font; Kouch keeps its own art and Inter.

**Screens**

* **Home:** a hero area (hero art + logo of the focused game) above horizontal shelves: Recently played, Favorites, one shelf per system, and Downloads/updates.
* **Library:** a grid with filter/sort and a tab per system. The classic 5×3 grid stays available as a view option.
* **Main menu:** slides out from the left — Home, Library, Players, Downloads, Settings, Power.
* **Quick access:** a panel on the right — players, friends, notifications, battery.
* **Game page:** hero + logo; Play is the primary action, with tabs for Options, Mods, Netplay and Details. It replaces today's detail sheet.
* **Everywhere:** a clickable bottom prompt bar, and an X on every sheet.
* **One shared `Card` component** for every card on every screen.

**Built-in theme**

* **Black/White toggle.** White mirrors the black theme. The accent is re-checked for contrast on white, and any player colour below 3:1 gets a darker variant.
* **Ambient background.** On by default in Dark, and can be turned off.
  * Kouch's own slow-drifting shapes at 3–6 % opacity: rounded tiles, circles and glyph outlines.
  * Transform/opacity only, at 30 fps or less.
  * Paused while a game runs or the window is unfocused; frozen under reduced motion.
* **Sounds.**
  * Slots: move, accept, back, error, launch, page, sheet open, sheet close, pad connected, pad disconnected, player joined, toast, boot.
  * Optional music, off by default.
  * Master, UI and music volume.
  * `scripts/gen-sounds.mjs` is extended to generate every slot; still no third-party samples.
* **Lint.** `design-lint` still enforces the built-in theme. Theme values reach the UI only as CSS custom properties through `#k-theme`.

## Phase 3 — Controllers + connect screen (C)

**Status:** code complete (C). Hardware checks remain.

* Done:
  * **Connect screen**
    * hero → 1×4 → 2×4 → pages, with FLIP pop-in (bcbe0d0)
    * waiting pads shown as white boxes that light up in place (114080f)
    * re-grip from Reorder: drop one half on the other to join, RT splits (f92f947)
    * in-house silhouettes and drive icons; generic per rule 1 (f80574b)
  * **Battery and link** from SDL3 on every pad, plus the DSU battery byte (389531a)
  * **Nav**
    * X / Y / View / LT / RT
    * pad nav for Kouch's own UI (`UiNav`); swallow held buttons
    * deadzone and confirm layout
  * **Motion recalibration** (389531a)
  * **Keyboard + mouse seat** (75d6ed3)
    * Raw Input with `RIDEV_INPUTSINK`
    * remappable
    * mouse drives the right stick or motion
  * **Glyphs**
    * Steam's own SVGs per family through `TranslateActionOrigin`
    * style setting (7a1d1d9)
    * prompts use them (24f324f)
  * **Settings › Controllers** (ece1874)
    * live input test
    * rumble / light / recalibrate
    * nav and prompt style
    * keyboard seat remap
  * **Split-pair pairing by Kouch** (978812c)
    * pair, sideways singles, mixed, re-grip
    * pure and tested
    * gated by `KOUCH_SPLIT_PAIRS=1`
  * **Remote Play session** on every Steam pad (3db79a6). The connect screen badges it with the guest's name (82a3ff8).
  * **Theme device images:** the connect screen takes a theme's image per silhouette class (c7c72e6, ADR 0009).
  * **B's contract work:** 4562d42, 039a2e2
* Remaining:
  * **Hardware checks:**
    * Split-pair: does Steam Input grab the halves? Then turn Kouch pairing on by default. There is no pair on the owner's PC yet.
    * Keyboard seat in a real emulator.
    * Glyph SVGs live under Steam.
    * Pad nav of the main window. Live runs are blocked while Steam runs elevated.
  * **Device-matrix rows:** rumble and LED tests.

### Connect screen

Built from the owner's mockups. The silhouettes are flat and static, and our own: white while waiting, filled with the player colour once claimed. We don't copy the reference console's screen or icons (ground rule 1).

How the layout grows with the number of seats in use:

| Seats | Layout                                                                                                                                                                             |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0     | **Hero.** A large card on the left: empty silhouette plus four empty player squares. On the right: "No controller connected" and a Continue button.                                |
| 1     | **Same hero, titled "Controller setup".** The silhouette is filled in P1's colour, square 1 is lit, and it shows battery/USB.                                                      |
| 2–4   | The text column slides away and the hero card shrinks into box 1 of a single row (max 4). Each new pad **pops a box in**; the others resize and slide over.                        |
| 5–8   | Reflows to 2 rows × 4.                                                                                                                                                             |
| 9–16  | *Dropped by ADR 0026 (8 players at most): with all 8 seats taken, a waiting pad gets no box and the header says "All 8 seats are taken. A controller joins when a seat frees up."* |

**Animation** is FLIP: measure, then animate transform translate/scale only with `--k-ease-spring`.

* A new box pops in: scale 0.85 → 1 plus a fade.
* A leaving box shrinks and fades, then the gap closes.
* Several pads connecting in the same tick animate as one batch.
* Reduced motion makes all of this instant.

**Each box shows:**

* player chip + LED dots
* the silhouette
* USB or battery level
* the motion badge

**Around the boxes:**

* An instruction line: "Press \[A glyph] on each controller to join".
* A prompt bar: Controller not connecting, Reorder, Motion (X), Back, Done.
* A dev hotkey that adds or removes a mock pad, for testing the layout without hardware.

### Silhouette set

Single-path SVGs using `fill="currentColor"`, with neutral file names. Themes may override them (ADR 0009).

| File                              | Device class                                                    |
| --------------------------------- | --------------------------------------------------------------- |
| `pad-standard`                    | Xbox-class, generic pads                                        |
| `pad-touchpad`                    | touchpad-class pads                                             |
| `pad-trackpad`                    | Steam Controller (first generation: two trackpads, one stick)   |
| `pad-trackpad-2`                  | Steam Controller (second generation: two sticks, two trackpads) |
| `pad-classic`                     | symmetric-stick pads with no touch surface                      |
| `phone`                           | phones and tablets used as controllers                          |
| `wand-pair`                       | a pair of separate hand controllers                             |
| `handheld`                        | Steam Deck                                                      |
| `pad-pro`                         | `MotionPro`                                                     |
| `pad-pair`                        | `SplitPair`, both halves held as one pad                        |
| `pad-half-left`, `pad-half-right` | `SplitPair`, one half on its own                                |
| `keyboard-mouse`                  | keyboard + mouse seat                                           |
| `remote`                          | reserved for later                                              |
| `lan`                             | badge for a LAN seat                                            |

### Split-pair pads — every combination

* **Pair as one pad:** press both top shoulder buttons.
* **Single half held sideways:** press the half's two side-rail buttons. Each half becomes its own player, with a rotated stick and button map.
* **Mixed:** pairs, single halves and other pads can all be seated at once.
* **Re-grip from Reorder:** merge two halves into a pair, or split a pair into two halves. The boxes merge or split with the same animation.
* **Input path:** SDL3's HIDAPI driver with SDL's own auto-combine turned off. Kouch pairs the halves itself on the input thread.
* **Per half:** motion (a pair uses the right half's by default), rumble and LEDs.
* **Early check:** does Steam Input grab the halves while Steam's setting for this pad family is on? If it does, fall back to a raw-HID source.

### Keyboard + mouse as a controller

* It gets its own box on the connect screen; press Enter to join.
* It can also be an opt-in player seat per profile:
  * A new `KeyboardSource` maps keys to buttons and sticks, remappable.
  * The mouse maps to the right stick or to motion.
  * On Windows it reads raw input on a message-only window with `RIDEV_INPUTSINK`, so it keeps working while the emulator has focus. On Linux it reads evdev.
  * Adds `ControllerKind::Keyboard`.
  * Profiles show a double-input warning when this is enabled.

### Battery

`OsSource` reads SDL3 power info, reports it in `Player`, and forwards it into the DSU battery byte.

### Glyphs

Glyphs always come from Steam at runtime and are never shipped:

* **Steam Input pads:** `GetGlyphSVGForActionOrigin`.
* **SDL-only pads:** map the SDL type and button to the matching Steam origin, then ask Steam for that glyph.
* **Offline:** neutral in-house glyphs.
* **Glyph style setting:** Auto / Xbox-style / PlayStation-style / Neutral.

### Settings › Controllers

* live input test per pad
* rumble and LED tests
* motion recalibration
* confirm-button layout
* navigation deadzone

**Device matrix:** a new section in `docs/EARLY_TESTS.md`, filled in by testing the owner's actual pads.

## Phase 4 — Storage layout + first-run setup (B core, C UI)

**Status:** core done (B). UI done (C):

* wizard + Settings › Storage (bdc4d32)
* saves-to-Steam-Cloud opt-in with undo, and Paths → Storage routing (65e6d4d)

The wizard opens on first run, can be re-run from Settings, and is verified in Black and White. Live check on the real drive list is pending.

* 432e821: `kouch_profiles::storage` — `StorageLayout` (legacy paths until the wizard runs), create-only `ensure_layout`, and `saves\<emu>` junctions into `<config>\cloud\<steam id>` (re-pointed on an account switch).
* 432e821 also: SHA-256-verified moves that never follow links or read `bios`, and the drive list.
* 24e9158: the `storage.*` commands and the legacy migration; launch, LAN receive, `.kod`, updater and export all resolve through the layout; `{roms} {bios} {storage} {media}` placeholders.
* **Decided (B, 2026-09-26): API only.** The junction points at `<config>\cloud\<steam id>` and `cloud.rs` syncs it through `ISteamRemoteStorage`; no Auto-Cloud (it couldn't keep `system`/private paths off the cloud or keep both sides of a conflict). Steamworks settings and live checks: `docs/RELEASE.md` "Steam Cloud".
* Steam Cloud readiness (B, 2026-09-26): `cloud_sync.rs` puts a `CloudBackend` trait in front of Steam, with an in-memory cloud for tests. Three-way change detection per Steam user (`<config>\cloud-state\<steam id>\<profile>.json`); changed on both sides keeps both (the older copy is `<file>.conflict-<time>`, never uploaded); full quota, the per-profile cap and Steam's 100 MB per-file limit stop cleanly; Steam Cloud switched off for the account or app is reported. When a Steam user signs in, saves made before sign-in go to that first user only, the `saves\<emu>` links are re-pointed, then every cloud profile downloads. With the legacy layout the shared folder syncs for its first Steam user only. Live Steam writes still need the app id.
* 2d66999: the opt-in move of an adopted tree's saves into the cloud folder (`storage.cloud_candidates` preview, verified backup, junction, one-click `storage.restore_saves`). Once the tree exists, `settings.set` refuses `user_data_root`.

### Disk picker

Drive cards with drawn icons for each drive type: internal SSD, HDD, USB, SD card, network, and Deck internal. Each card shows:

* the drive letter and label
* a free-space bar
* a "recommended" chip on the suggested drive

It uses the same pop-in animation as the connect screen.

### Wizard

The wizard steps are:

1. Pick the main disk and the folder name (default `Emulation`).
2. Add more disks for games.
3. Pick Black or White.
4. Import emulators.
5. Done.

It can be re-run from Settings › Storage. **"Use an existing Emulation folder"** adopts an EmuDeck tree in place instead of creating a new one.

### Layout

```
<disk>\Emulation\
  bios\<emu>\                 created empty + readme; Kouch never reads/syncs/sends it
  roms\<system-folder>\       on every registered disk
  saves\<emu>\                junction → the Steam user's cloud folder
  storage\<emu>\              large non-cloud data
  storage\downloaded_media\   art (same place ES-DE / EmuDeck use)
  tools\kouch\                Kouch's own helpers only
  emulators\<profile-id>\<version>\
  mods\
```

### Cloud-safe data, per Steam user

* `saves\<emu>` is a directory junction (`mklink /J`, no admin rights needed; a symlink on Linux).
* It points at `%APPDATA%\kouch\cloud\<steam id>\<emu>` and holds saves, states, screenshots and config; `cloud.rs` syncs it through the Steam Cloud API (no Auto-Cloud, decided 2026-09-26 — see `docs/RELEASE.md`).
* Switching Steam accounts re-points the junction.

### `storage::ensure_layout()`

Idempotent. It runs after the wizard, on every profile import, whenever a disk is added, and from a "Repair folders" button.

1. **Top-level folders.** Create any that are missing. Never touch existing files.
2. **Per profile.** Uses the new schema v2 fields `folder` and `data_folder`. Creates:
   * `roms\<folder>` on every disk
   * `bios\<data_folder>` + a readme
   * the `saves\<data_folder>` junction
   * `storage\<data_folder>`
3. **Point the emulator at these folders** through the profile's `config_edits`. New placeholders: `{roms} {bios} {storage} {media}`. The emulator's config file is backed up once first.
4. **Register games.** Each `roms\<folder>` becomes a library root. Folders that don't match any profile are listed as unmatched.
5. **Adopting an existing tree.** Only create what's missing. Show a preview and ask for confirmation before changing any emulator paths. Moving existing saves into Steam Cloud is opt-in, with a backup first and a one-click restore.
6. **"Move main folder."** Copy → verify by hash → re-point → delete the source only after the user confirms. Progress shows in the download queue.

### Code

* A `StorageLayout` in `kouch-profiles` replaces both `Settings.user_data_root` and `<data>/emulators`.
* A migration moves existing installs onto it.
* **Tests only ever use temp dirs and a scratch EmuDeck-style tree.** Development never modifies the owner's real `R:\Emulation`.

## Phase 5 — Art and system images (B + A)

**Status:** done (kouch central): discovery bf7f413 (local media, the public thumbnail index — system names are user/profile data, 42/42 titles matched on the owner's library — and SteamGridDB with the user's own key), profile-supplied index names 35ea084, Settings › Game art fe5e2e8; A crossfades art in when a scan's art job finishes.

**Local art first, found automatically on scan:**

* `storage\downloaded_media` and `gamelist.xml`
* same-stem images or videos next to the ROM
* `media\` folders

**Online art, opt-in:**

* SteamGridDB, with the user's own API key
* ScreenScraper, with Kouch's own dev ID
* TheGamesDB
* Games are matched by file hash plus name.
* The scraper platform is picked from each provider's own list at runtime, so **Kouch's code contains no platform names**.

**Library schema v3** adds `hero_path`, `logo_path`, `video_path` and `grid_path`.

**System images, in priority order:**

1. the user's own local image
2. the theme's image (ADR 0009)
3. Kouch's neutral device art

## Phase 6 — Theme engine + Workshop themes (A)

**Status:** backend done (kouch central): the `kouch-theme/2` validator/loader (`theme_pack.rs`: allowlisted tokens with strict grammars incl. gradients/shadows/easing, per-scheme tokens, accent gated per scheme, text-contrast check, assets type/size/containment-checked, SVG active-content scan), discovery from `<config>/themes` + Workshop, `theme_list`/`theme_set { id }` persisting `settings.display.theme`, `ThemeState.extras`, theme accent tier (user > theme > OS > default), fonts via `@font-face`, `kmedia` serving fonts/sounds/SVG only from theme folders. Spec: `docs/THEME_FORMAT.md`. UI done (A, 160a7f8): Settings › Theme picker (preview, author/version, description, single-scheme and device-imagery notes, broken themes greyed with the reason, suggested Library view applied once), "Get more themes" (Workshop browse + subscribe), and every extra applied — background image/video, per-system backgrounds on Library tabs, ambient density/opacity, card shape, all 13 sound slots, a music file, the display font for headings, silhouettes to the connect screen; `--k-bg-gradient`/`--k-card-shadow` read through var-only hooks. Theme maker (A, 2026-09-26): Settings › Theme › Make your own — New theme (name typed on the on-screen keyboard or a real one, `theme.create`, then selected), and for the active local theme Open theme folder (`theme_open_folder`, resolved in Rust, directories only), Reload (re-validates from disk) and Publish to Workshop (WorkshopPublish prefilled). Controller text entry everywhere a `TextRow` is: a pad's A asks Steam for its floating keyboard (`text_input_show`), else Kouch's own on-screen keyboard (letters/symbols/email/numeric; B delete, Y space, X shift, LB/RB caret, Start done). Remaining: Workshop browse/publish live needs Kouch's own app id.

**`kouch-theme/2`** (spec in `docs/THEME_FORMAT.md`) — what a theme can set:

* token sets for dark and light
* an expanded allowlist: colours, gradients, shadows, radii, durations, fonts (`.woff2`)
* which view each screen uses
* tile shape and aspect ratio
* per-system backgrounds and video
* ambient background settings
* sounds and music
* silhouettes and system art (ADR 0009)

**The Rust validator is the security boundary.** It only ever emits CSS custom properties and asset URLs. It rejects:

* any CSS, selectors or scripts
* paths that aren't relative (checked with `kod::is_safe_relative_path`)
* file types outside the allowlist, and files over the size caps
* remote URLs

**Existing drift fixed along the way:** `include_str!` the allowlist in Rust so Rust and TS read the same file, and use a single saturation rule.

**Where themes come from:** `<config>/themes/` and Workshop installs. Settings › Theme gets a picker and preview.

**Workshop** follows the `cloud.rs` pattern:

* use a cloned `steamworks::Client`
* run blocking calls in `spawn_blocking`
* guard with `is_borrowed_app_id`
* browse/subscribe/install and publish through `ugc()`

## Phase 7 — Launch options + game-style settings (B + A)

**Status:** backend done (B); the game page's Options tab (emulator picker, options by tab, reset) is done (A, 715bbcf). Emulator-wide options in Settings › Emulators and the pre-launch sheet (confirm\_launch or long-press Play) done (A, ec4303b). **Verified live 2026-09-26** (isolated build, real emulator): the local test profile's options (resolution, aspect, frame-rate overlay, volume) set per game were written into the emulator's own config at launch, and its next screenshot came out at 3× the native size.

* 6e3de41: profile schema v2 `options` (typed toggle/choice/slider/text, tabs, apply arg or config\_edit) and `options::apply_options` layering default ← emulator values ← game overrides.
* 6f345c5: launch applies options; one profile resolver (explicit → the game's own `profile_id` → first for its system); `mark_played` is called; the literal `"default"` id is gone.
* 6f345c5 also adds the commands: `game.options_get`, `set_options`, `set_profile`, `hash` and `emulators.set_option_values`; plus `launch_game(extra_args)` for netplay.

**Profile schema v2 — `options`.** Each option has:

* `id`, `label`
* `tab`: graphics / controls / audio / multiplayer / advanced
* `type`: toggle / choice / slider / text
* `values`, `default`
* `apply`: `arg` or `config_edit`

Values are set per emulator, and can be overridden per game (stored in library v3). A profile with no `options` just shows an Advanced tab listing its raw args.

**Game-style options menu.** Tabs: Graphics / Controls / Audio / Multiplayer / Advanced. Reachable per emulator from Settings, and per game from the game page.

**Pre-launch.** Play on the game page. With `confirm_launch` on, or on a long press of A, show first:

* an emulator picker filtered to the game's system
* quick options
* Mods
* Netplay

**Fixes that land here:**

* apply `window_mode`
* the validator's placeholder check
* honour `rom.profile_id`
* call `mark_played`

## Phase 8 — Mods per game (B + A)

**Status:** local + `.kod` + Workshop sources done (B); the game page's Mods tab (toggle, Left/Right load order, refused mods shown) and mods-only `.kod` import done (A, a72959b + a6f19ce). **Verified live 2026-09-26** (isolated build, real emulator): a local mod enabled for one game was copied into the emulator's graphics-mods folder with mod loading switched on for the session, and after quit the folder was empty again, the switch reverted and the source mod untouched.

* 6382243: data-only mod packages (`kouch-mod.json` + `files/`); apply before spawn and revert after exit, quit, a failed spawn or a crash (manifest-first, never overwrites); `mods.list`, `game.set_mods`; `.kod` `mods/` installed.
* Follow-up commit: Workshop items as `ws-<item>` mods; `mods.refs` with a cached content hash per mod for netplay.

**Profile `mods` spec:**

* a `dir` template
* `method`: `link` or `copy`
* optional `enable` via `config_edit`
* an optional `order` file

**Where mods come from:**

* Workshop items tagged `mod`, with key-value tags naming the target profile and game hash / title id
* `<main>\mods\`
* `.kod` files with a `mods/` folder (stop refusing these)

All mods are data only: they go through the same validator as themes, plus an executable denylist.

**Mods tab (per game):** enable/disable each mod and set load order.

**Applying mods safely:**

* Apply before the emulator spawns.
* Revert after it exits.
* A manifest records every file placed, so a crash is reverted at the next start.

## Phase 9 — Steam netplay with matched mods (D)

**Status:** backend done (kouch central): kouch-netplay crate (3cf20d3: lobby key/values with the game hash only, join planner, loopback relay with a real round-trip test) and the Steam side (ea3c9e9 + ea9d485: friends-only lobbies, invite dialog, friends' join requests, relay over Steam networking messages accepted only from lobby members, launch with the profile's host/join args, peer always 127.0.0.1). Never runs under the borrowed dev id. GamePage Netplay tab (Remote Play Together card + the emulator's own netplay), the join confirm sheet and the Quick Menu invite/plugin rows are done (A, a72959b); live test needs app 480 or Kouch's id and two Steam accounts. Note: some emulators (incl. the reference one) have no command-line netplay; for those, **Remote Play Together** (Steam streams the host's session and forwards friends' pads, which Kouch then sees as ordinary Steam Input pads) is the planned universal path — to verify once Kouch's app id can enable it.

**Lobby data (`matchmaking()`):**

* game hash
* emulator profile id + version
* ordered mod ids + a version hash
* slots and motion slots

**The ROM is never transferred.**

**Joining:** invites and rich presence (`friends()`) bring the joiner to a pre-join check:

* **Same game hash?** If not: "you need your own copy".
* **Emulator version:** offer the updater's pinned version if it differs.
* **Mods:** missing Workshop mods download automatically. Local-only mods must match by hash.

**Transport.** `networking_sockets()` over Steam's relay, with a local 127.0.0.1 proxy in front of the emulator.

**Profile `netplay` adapter** for the emulator's own netplay:

* host/join args using `{peer_addr}` / `{port}`
* protocol
* port

**Quick Menu** gets a netplay row: ping, invite, kick/approve. The game is never paused while netplay is running.

**LAN play (Mode A) stays** as the no-Steam option.

**Testing.** Lobbies can be tested on app 480 with two accounts. Invites need Kouch's own app id.

## Phase 10 — Plugins (D)

**Status:** done (kouch central + C): runtime 626d993, sessions + Quick Menu 7d86331, Settings › Plugins with capability approval + Workshop browse fe5e2e8, Quick Menu plugin rows a72959b (A); sample plugin + docs/PLUGIN\_AUTHORING.md. Workshop items need Kouch's own app id. **Verified live 2026-09-26** (isolated build, real emulator): the sample plugin loaded for the session, its Quick Menu "Quick save" made the emulator write a save state through the profile hotkey, "Quick load" ran, and the session ended cleanly. Workshop publishing UI done (C, 4dbc6f1/20c9cae).

**Runtime:** WebAssembly (`wasmtime`). No filesystem or network access by default.

**Versioned host API.** A plugin's manifest declares each capability, and the user approves it:

* Quick Menu actions and pre-launch options
* session events
* sending the profile's hotkeys
* read/write only that emulator's `saves` / `storage` folders — **never `bios`**
* toasts
* small settings

**Distribution:** Workshop items tagged `plugin`, and `.kod` files with a `plugins/` folder. Every plugin is off until the user enables it.

## Phase 11 — The Steam library companion + game information (kouch central, B, A)

Owner request 2026-09-26: "fork steam rom manager for the images of the games, image data, game data, descriptions, information, add to steam shortcuts and launching via kouch". Decision: ADR 0014. **Revised the same day:** the owner wants it inside Kouch, "make it a plugin for kouch". ADR 0016 makes it the first-party `steam-library` plugin: `crates/kouch-shortcuts` holds the logic, plugin ABI v2 adds app-level plugins with `library_read` and host-mediated `steam_shortcuts`, and the forked companion is kept only for scratch dry runs.

* **Companion (kouch central):** a fork of the GPL-3.0 shortcut manager at `C:devkouch-srm`, kept a separate program with a private repo for now. It gains a "Kouch library" parser that reads Kouch's export. Each game becomes a shortcut that runs Kouch with `--launch=<id>`. Kouch's own art is the default artwork, with SteamGridDB and the other providers still available. Steam is closed, written, backed up and restarted the way the upstream tool already does it. Tested only on scratch `userdata` trees until the owner says go.
* **Kouch side (B):**
  * Stable game ids across rescans, and the export `<config>/exports/steam-library.json` (`kouch-steam-library/1`, ADR 0014).
  * `--launch=<id>` on a first start, exiting after the game when Kouch was started only for it.
  * Game information in the library: description, developer, publisher, release date, genres, players, rating. It comes from the user's existing `gamelist.xml` first, then from opt-in online providers.
* **UI (A):**
  * The game page's Details tab shows the information.
  * Settings › Steam library: update the export file, open the companion, and show how shortcuts launch.
* **Checks:**
  * Round-trip a scratch `shortcuts.vdf`: Kouch's shortcuts added, other shortcuts untouched, re-sync removes deleted games.
  * A Kouch shortcut launch under Steam keeps Steam Input.
  * The Deck in Desktop Mode, then Game Mode.

**Status:** Kouch side done (B, 2026-09-26):

* **Stable ids:** library schema v4 gives every game a `stable_id` (`g` + 16 hex digits of a hash of the system and the path, stored when the row is made and backfilled on migration). It survives rescans, a folder removed and re-added, and moving the library. `--launch=` takes it or a row id.
* **The export:** `library_export_steam` writes `<config>/exports/steam-library.json` (`kouch-steam-library/1`) atomically. `launch.target` is `$APPIMAGE` when set, else this executable. Art is the files Kouch has (cover as `tall`, `hero`, `logo`); `wide`/`icon` are null. `settings.steam_library.export_after_scan` (off by default) rewrites it after every scan.
* **First start:** `--launch=<id>` launches the game, and Kouch closes when that game's session ends, after its Steam Cloud upload. Starting any other game from Kouch first cancels that, so the user who switches to using Kouch keeps it (`app/src-tauri/src/steam_launch.rs`).
* **Game information:** description, developer, publisher, release date, genres, players and rating are read after every scan from the user's own `gamelist.xml`: beside the games, or in `~/ES-DE/gamelists/<folder>/` or `~/.emulationstation/gamelists/<folder>/`. They are in the game payload as `info`. Online providers come later.
* **Companion (kouch central):** f560d31 in `golfista/kouch-srm`, private as the owner prefers, adds the "Kouch library" parser and its preset. Kouch's art is the default image choice, and the system becomes the Steam category. 8/8 tests pass, including a `shortcuts.vdf` round-trip on a scratch Steam tree that leaves another shortcut untouched.
* **As a plugin (ADR 0016):**
  * `crates/kouch-shortcuts` (2a89823) round-trips the owner's real `shortcuts.vdf` byte for byte, and its app id matches the one Steam gave a shortcut on the Deck.
  * The first-party `steam-library` plugin (2cf9de1, Rust → wasm32, 187 KB) runs in B's ABI 2 host (0e5f9cd: app scope, `library_read`, host-checked `steam_shortcuts`, the detached close-write-reopen helper, Steam discovery) under an integration test.
  * The Details tab is done (A, fa6cadd).
* **Host wired and bundled** (B, cbaa887, 183ebb6):
  * the host's check uses the crate;
  * the plugin ships in the app's resources, disabled by default;
  * the export file rewrites itself once the plugin is enabled.
* **Dry run PASS 2026-09-26** (kouch central, Windows verify build, `KOUCH_STEAM_DIR` pointing at a scratch Steam folder that holds one other shortcut, the owner's 135-game test library):
  1. enable, then "Add games to Steam": 135 adds staged for the scratch user;
  2. confirm: 135 shortcuts written, launching Kouch with `--launch=<stable id>`, and 83 grid files; the other shortcut kept, first and unchanged;
  3. a second sync stages nothing;
  4. "Remove": the file is back to its original SHA-256 and the grid folder is empty; backups kept.
* **Collections** (owner: "the library is the console name"): one Steam collection per system, named after it. The pure merge is in `kouch-shortcuts` (c429643). The helper writes it after `shortcuts.vdf`, with a backup, and Remove retires them (B, bd0ff9e). The confirm sheet names them (A, 00770cb). Scratch-tree tests only; the cloud-merge risk is in ADR 0016.
* **Steam Deck** (owner: "for steam deck it can be a decky plugin"): in Game Mode Steam can't be closed to write the file, so a Decky Loader plugin (`integrations/decky`: 6465f67, c229bb8, 3d756c6, with preview, confirm and a Remove that works without the export) adds the same shortcuts live through Steam's in-client API, reading Kouch's export. The in-app plugin refuses in Game Mode and points to it.
* **Remaining:** a live Deck run of the Decky plugin (installing it into Decky needs the owner's OK); a live write to the owner's own Steam only with the owner's go.

## Windows release build (A)

**Status:** done 2026-09-26 — `scripts/build-release.ps1` builds with `--no-default-features` (the borrowed-app mode is compiled out), embeds the UI, and lays out `dist/windows/` as the SteamPipe depot: Kouch.exe (27 MB), steam\_api64.dll, SDL3.dll, vcruntime140.dll (app-local, SDL3 needs it), `steam/kouch_actions.vdf` — 31.5 MB, no steam\_appid.txt; NSIS installer 8.4 MB. The script fails if any DLL the shipped binaries import is neither Windows' nor in the depot (the first depot missed SDL3.dll; central caught it). A release exe started outside Steam no longer writes steam\_appid.txt at run time. Smoke-tested from a copy of the depot and from a silent NSIS install into a temp folder (run, Power › Exit, silent uninstall leaves nothing). Smoke run on the release exe with temp dirs and a throwaway identifier: wizard skipped via a scratch storage folder, Home, Library, Settings, Night Sky theme, Power › Exit; logs clean apart from the expected busy-DSU-ports and no-Steam warnings. `docs/RELEASE.md`: SteamPipe upload, Steam Input API + forced-on, the app-id checklist, go-public cross-check. Empty/error pass: no emulators → "Add an emulator" (opens Settings › Emulators), drive unplugged → names the folder + Rescan, no games → names `<Emulation>\roms` + Rescan, Workshop unavailable → the reason + how local themes still work.

## Acceptance run — the owner's stop condition (kouch central)

**Status:** mostly done (2026-09-26). Steam route (ffa8ea7): the profile installs the emulator from its release feed (7z), 42 games scanned, launched fullscreen, graceful quit. Isolated-build route (a second real app beside the elevated one, no Steam; 4be2f58, 4dd630e, 2582ceb): import → queue install → scan → art 42/42 → launch → emulator screenshot via Kouch's hotkey (frame read back) → Quick Menu over the game → graceful quit with the emulator's save flush; pads auto-seated and read over DSU (connected, live sticks, 0 CRC errors). Found + fixed on the way: DSU ports hardcoded in profiles (now `{dsu_host}`/`{dsu_port}`), plus the earlier ini/dialog/window\_mode/claim/focus fixes. **Remaining:** Steam Input seating and live motion (gyro/accel through Steam → DSU → the game) — needs the owner to close the elevated Kouch (or run Steam normally) and tilt a pad; the SDL motion fallback (C) lets the resting-pad gravity check run without Steam. **Release candidate accepted 2026-09-26:** the NSIS installer and SteamPipe depot install, run, report and uninstall cleanly (see CLAUDE.md Current state).

**Goal:** prove the whole flow end to end with real hardware:

* a real emulator profile that auto-updates (or at least shows available updates)
* a real game from the owner's `Emulation` folder
* controllers and **full motion through Steam Input → DSU**
* a clean quit

**Setup:**

* **Emulator.** A private local test profile (lint-exempt `profiles/`) for the reference motion-native emulator:
  * It publishes zip releases on GitHub, so `source: github_release` exercises install, update check and rollback for real.
  * Kouch installs its own portable copy. The owner's EmuDeck copy is never touched.
* **Keys.** The owner copies their own keys file into it once. Kouch and its agents never read, copy or move keys (ground rule 2).
* **Code needed first:**
  * `config_edit` `format: xml`, for the emulator's controller profiles
  * the `window_mode` and validator fixes

**Steps.** Everything runs on the Kouch Lab route, and the owner is told before each launch.

1. **Import** the `.kod`. The download queue shows the install; the version and "Check for updates" appear in Settings › Emulators.
2. **The owner copies the keys file.**
3. **Scan** the games folder. Tiles appear.
4. **Claim pads.** Steam Input is up and the DSU servers are bound.
5. **Launch a game:**
   * The window is found, and `window_mode` is respected.
   * The emulator's DSU client registers with Kouch.
   * **Motion check:** the owner tilts the pad; `kouch-lab dsu-client` shows the gyro moving, and the game responds.
   * The owner plays for a minute.
   * A screenshot is taken with the emulator's own hotkey, sent through Kouch, and read back as proof.
6. **Quick Menu** over the game: players and motion slots show. Resume.
7. **Graceful quit:**
   * The save's hash matches before and after.
   * Kouch is back to idle, with focus on the game's tile.
   * Cloud sync is skipped under the borrowed app id.
8. **Record results** in `docs/EARLY_TESTS.md`: Early Tests 4, 9, 11, 12 (and 5/15 where possible), plus device-matrix rows.

**Needs the owner:** copying the keys file once, being at the PC with a pad for steps 4–7, and saying "go" before each launch.

## Verification (every phase)

* **Full local gate** (`CLAUDE.md`), every time:
  * Rust: fmt, clippy, test
  * UI: check, design-lint, brand-lint, vitest, build
* **UI phases:**
  * CDP screenshots at 1280×800, 1920×1080, 2560×1080 and 3840×2160, plus 150 % scaling, in both Black and White
  * recorded frames of the connect-screen animation, captured with the mock add/remove hotkey
  * frame-time HUD numbers from Windows and from the Deck
* **Storage:** tests over temp dirs and a scratch EmuDeck tree, covering adopt, junctions, move, repair, and never touching existing files.
* **Workshop / netplay:**
  * the borrowed-id guard skips correctly
  * lobbies tested on 480 with two accounts
  * unit tests for mismatch cases


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kouch.dev/docs/plan_v2.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
