## The first run Every walk and every deploy so far started from Kyle's own phone — signed in, library folder set, a config full of his settings. What a phone that has just installed the app actually has is different in three ways, and only one of them is what you would guess. **The library root is never missing.** `config.rs`'s `download_dir` has an `#[cfg(target_os = "android")]` branch that falls back to `app_data_dir()/Library` and returns before the desktop's `audio_dir()` fallback is reached, so a fresh install has a real, writable, empty library root at `/data/user/0/com.soulstream.app/files/Library` — invisible to every other app on the phone, including the one that put the music there. `pipeline::init` scans it at startup and `library_albums` honestly answers `[]`. This is why "no folder yet" cannot be a null check anywhere: the test is the PATH, `isPrivateFolder` in `setup.svelte.js`, which You.svelte had first and now imports rather than keeping a second copy of. **`setup_done` was already there and the phone never touched it.** It round-trips through `get_settings`/`set_settings` like any other field and `config::setup_done` unwraps a missing one to `false`, so **every phone in existence reads as unstamped**, Kyle's included. Gating the tour on that flag alone — which is what the desktop does — would have opened a setup sheet on a phone already full of his own music, and then written the flag into his config on dismissal. **Nothing is signed in, and everything says so honestly and helpfully nowhere.** `connectStored()` returns at once with no account saved, the connection stays `offline`, and every stream and every Save ends as a red toast with a sentence in it and no way out. So: `src/mobile/setup.svelte.js` is the one store, and there are two surfaces. **The tour** is a half sheet (`kind === "setup"` in `HalfSheet.svelte`) — one scroll, two cards, a foot. Where your music lives: the current folder, a row that opens the storage plugin's folder picker, and the all-files permission row beside it once a real folder has been picked. Soulseek: what it unlocks, and one row that goes to the sign-in card on Community. **The tour never handles a password** — there is no field in it; Kyle signs in himself, on the card that already existed. Keeping the private folder and staying signed out are both legitimate answers, so the sheet is answerable by doing nothing, and the scrim and the back gesture close it like any other half sheet. **The boot gate is a three-way AND** — `!setup_done && !slsk.savedUser && !lib.albums.length` — and it is evaluated only after `loadLibrary()` and `connectStored()` have both settled (`Promise.all` in `Mobile.svelte`'s `onMount`). Read either of those early and they are `[]` and `""` on every phone there is. Kyle's phone fails the second and third clause, which is the only safe way to ship an auto-opening layer into a pipeline that installs onto his device. **The note** (`SetupNote.svelte`) is the fallback for the screens that are empty because of the same two things, with the row that fixes each: Home's empty state (whose copy still said music "lives in the app's own Library folder for now", written before the picker landed in You), Library → Albums, Library → **Artists** — which had no empty state at all, just a `0 artists · 0 followed` line over nothing — and Search's empty page, where "Sign in on Community to play what the library does not hold" was a flat sentence and is now the row that does it. A row is drawn only for a thing actually missing, and the component renders nothing when both are answered, so a phone that is set up never sees it. There are two doors onto the same folder — the tour's row and You's own picker — and they tell each other: You re-reads when `setup.folder` moves, and its picker calls `loadSetup()` on the way past, or the note on Home would go on offering a folder that had already been picked on You. The all-files row does the same trick You does with the grant: the system screen takes the app away and gives it back, so `requestAccess` re-reads the answer on the way back rather than leaving a row up that has been answered. **The preview door** is one row in You → This phone: "Set up this phone again", `openSetup("preview")`. A preview reads the config and writes nothing — `setup.writes`, the number of times the store has stamped `setup_done`, stays where it found it. The phone walk drives exactly that door on Kyle's phone and asserts the counter at zero on the way in and on the way out; it types nothing, picks nothing and clears nothing. The harness hosts the other device. `localStorage.mockFresh`, or `#fresh` in the URL (which is what the walk uses — a reload cannot lose a fragment), switches `src/lib/mockLibrary.js` to a fresh install: every library command answers `[]`, `get_credentials` answers with no account, `all_files_access` is not granted, and `mockSettings` is mutated in place so the library root is the sandbox path and `setup_done` is false — mutated rather than replaced so `set_settings` still works and picking a folder in the walk genuinely moves the state the notes read. The fresh-install block is the LAST thing in `MOCK_STEPS` because its reload wipes every module's state, and it turns the flag off and reloads again at the end so that anything appended after it is not silently running against an empty device. Two things a reload does NOT clear on its own, both handled in that block because it is the first one on the mock leg. The webview's storage comes back with the page, and the player writes the session out on `pagehide`, which a reload fires — so removing the key up front puts it straight back; the block adds a `pagehide` listener of its own, last and therefore run last, and a phone with nothing in it comes up with no now-playing bar. And the viewport emulation lives on the DevTools session rather than on the page, so the re-attach after a reload drops it: the walk keeps what it set and hands it back, or every still from there on would be the Chrome window's own size at 1×. Two commands the phone can reach had no mock at all until this round — `player_control` and `open_external` — found by mapping every `api.js` wrapper used under `src/mobile` against the mock (126 wrappers, 124 mocked). Unmocked they fall through to the real IPC, which on a `file://` page rejects: a console warning and a toast, in whatever step happens to touch them first. The transport now answers the way the real core does, with an event; `resume` cannot restart the position ticks, so the state is right and the position stands still.