diff --git a/README.md b/README.md
index 68165ce..d89353b 100644
--- a/README.md
+++ b/README.md
@@ -4,15 +4,7 @@
[](https://github.com/WordPress/experimental-wp-dev-env/releases/latest)
[](https://github.com/WordPress/experimental-wp-dev-env/releases)
-The [WordPress Core Dev Environment Toolkit](https://github.com/WordPress/experimental-wp-dev-env) is a desktop electron application (available for macOS on Apple Silicon, Windows, and Linux) that sets up a full WordPress core development environment with zero prerequisites.
-
-You install it, choose a directory for `wordpress-develop`, click a button, and you have:
-
-- A cloned `wordpress-develop` repository
-- A running WordPress dev server
-- The ability to make code changes and generate a patch
-
-No Git, no Node.js, no npm, no Docker needed. Everything is bundled inside the application as JS/WASM, powered by [WordPress Playground](https://wordpress.github.io/wordpress-playground/).
+The [WordPress Contributor Toolkit](https://github.com/WordPress/contributor-toolkit) is a desktop Electron application (macOS on Apple Silicon, Windows, and Linux) that sets up a full WordPress core development environment with zero prerequisites — no Git, Node.js, npm or Docker on the host.
### Why
@@ -22,18 +14,12 @@ Before writing a single line of code, a first-time WordPress core contributor ty
The **WordPress Core Dev Environment Toolkit** aims to eliminate this friction entirely.
-## What does it do?
-
-Once installed, the app lets you:
-
-- Clone `wordpress-develop` into a directory of your choice
-- Run `npm install`, `npm run build`, and `npm run dev` automatically
-- Start a WordPress dev server using Playground’s CLI
-- Make changes to core files directly
-- Generate a patch from your changes, ready to attach to a Trac ticket
-- Update an existing site to the latest trunk at any time, without recreating it
+## Documentation
-The entire toolchain — npm, Node, Git — runs as JavaScript/WASM bundled with the app. There’s no terminal work required for the basic contributor workflow.
+**The user guide lives at ** — installing
+the app, creating a site, working on a Trac ticket, applying patches and PRs, and submitting your
+changes as a pull request, a Trac attachment, or a patch for a mentor. What follows here is only
+what a contributor to *this repository* needs.
Here’s the full setup flow — from a fresh install to a running WordPress development environment:
@@ -51,30 +37,11 @@ Once your environment is running, generating a patch to submit to Trac takes jus
### Just using the app
-1. Download the latest packaged build for your platform from the [Releases page](https://github.com/WordPress/experimental-wp-dev-env/releases/latest). Pick the file that matches your OS:
- - **macOS on Apple Silicon:** Choose the `.dmg` file whose name contains `arm64`. Intel Macs are not currently supported.
- - **Windows:** Choose the `.exe` installer.
- - **Linux:** Choose the `.AppImage`; `.deb` and `.snap` packages may also be available.
-2. Open the app
-3. **macOS only:** The app is signed and notarized by Automattic. macOS should open it without issues. If Gatekeeper still blocks the app (this can happen when the file was downloaded via a browser), try either of these:
- - Right-click the `.app` file and choose **Open**, then confirm in the dialog that appears.
- - Or remove the quarantine attribute from the `.app` bundle itself:
- ```
- xattr -d com.apple.quarantine "WordPress Contributor Toolkit.app"
- ```
- > **Note:** Use `-d`, **not** `-dr`. The app is code-signed; using the recursive flag (`-r`) tries to strip attributes from files inside the sealed bundle, which macOS will reject with permission errors. Removing the attribute from the top-level bundle is sufficient.
-4. Click "Create WordPress Core site" and choose a destination folder for your site.
-5. Click "Install dependencies"
-6. Click "Run command > npm run build"
-7. Click "Start dev server"
-8. A browser window should open automatically. If not, you can manually open it by clicking "Launch site".
-9. Make changes in the code
-10. Click "Generate patch" to create a diff of your changes.
-
-> **Tip:** You never need to create a new site just to get newer code. **Update to latest trunk**,
-> in the ☰ menu at the top right of any site, is available at any time and on any site, however old
-> or new the site is: it fetches the latest trunk, reinstalls dependencies if they changed, and
-> rebuilds. See [Keeping a site up to date with trunk](#keeping-a-site-up-to-date-with-trunk).
+Download the latest packaged build from the
+[Releases page](https://github.com/WordPress/contributor-toolkit/releases/latest) and follow
+[Getting started](https://wordpress.github.io/contributor-toolkit/guide/getting-started) in
+the user guide — it covers picking the right file per platform, the macOS Gatekeeper notes, and
+the first-contribution walkthrough.
### Build from source
@@ -105,63 +72,22 @@ Electron Builder picks these up via `build` configuration in `package.json`.
## Technical notes
-### How does it work?
-
-* Git operations are handled by the `isomorphic-git` npm package. It is a pure JS implementation of Git that works in the browser and Node.js.
-* Node scripts and npm commands are run using the Node.js runtime bundled with the Electron app. A small shim directory is injected into the PATH so subprocesses can find `node`, `npm`, and `npx` without requiring a system install.
-* WordPress server is run using the `@wp-playground/cli` npm package from [WordPress Playground](https://w.org/playground/).
-* Patches are generated using the `diff` npm package.
-
-### Keeping a site up to date with trunk
-
-A site is a clone of `wordpress-develop` frozen at the moment it was created, so it starts drifting
-behind trunk immediately. **You never have to create a new site to get newer code.**
-
-**Update to latest trunk**, in the ☰ menu at the top right of any site, brings an existing site up
-to the current trunk: it fetches the newest commits, reinstalls dependencies if
-`package-lock.json` moved, and rebuilds. **This option is always there** — on every site, at any
-time, no matter how old or new the snapshot is and whether or not the app has flagged it as stale.
-If nothing has moved, you simply get _Already up to date_.
-
-The header of each site shows which snapshot it currently holds — `trunk as of 5 Aug 2026`.
-
-The app also flags sites that have fallen behind, with a coloured dot next to the site name in the
-sidebar:
-
-- **Amber** — the snapshot is more than 14 days old. The site view also shows a notice with an
- Update button.
-- **Red** — a previous update moved the code but never finished installing or rebuilding, so the
- built assets no longer match the source. Run the update again.
-
-Staleness is judged **locally**, from the date of the commit your site is sitting on, and never by
-asking GitHub what the tip of trunk is. That keeps the app working offline and stops it from
-making a network request every time it launches. Two consequences worth knowing:
-
-- A site created today is never marked out of date, even though trunk gets commits several times a
- day and yours is already behind by a few. Flagging that would mark every site stale within hours
- of creation, which makes the warning worthless.
-- The only way to find out exactly how far behind you are is to run the update.
+How the app works from a user's point of view — the toolchain it bundles, keeping a site up to
+date with trunk, what SQLite does and doesn't cover — is documented in the
+[user guide](https://wordpress.github.io/contributor-toolkit/), not here.
### Why Electron?
* WordPress core relies on Node.js, npm, and webpack for its build system. Electron is an easy way to install Node.js on all major platforms.
* It's a single, self-contained file. It's easy to distribute and install – it can be distributed on a USB sticks if everything else fails.
-### Is SQLite enough for WordPress?
-
-SQLite should suffice for most new contributors. The SQLite support is miles ahead of where it used to be (e.g. most plugins and core unit tests work, query monitor works, we track failures and missing features and, thanks to the query parser, we can improve things fairly easily).
-
-For cases when MySQL is required, local Playground can work with MySQL. The only missing part is shipping the MySQL server with the app.
-
### Ideas and future work
- Integrate Playground's XDebug.
- Explore bundling MySQL server with the app.
-- Built‑in SMTP catcher for local email testing.
- Migrate to the PHP Git client in https://github.com/wordpress/php-toolkit.
- Potentially integrate with Studio to benefit from PHP version selector, wp-cli integration and other Studio features.
- An ergonomic way of managing the git repository from the UI (commit, conflicts, pushes etc.) Or would it make sense to just endorse another git client?
-- Integrate with Trac and GitHub to apply and submit patches directly. Related: [grunt-patch-wordpress](https://github.com/WordPress/grunt-patch-wordpress)
### Download stats
diff --git a/docs/guide/applying-patches.md b/docs/guide/applying-patches.md
new file mode 100644
index 0000000..90aa9b5
--- /dev/null
+++ b/docs/guide/applying-patches.md
@@ -0,0 +1,45 @@
+# Applying patches and PRs
+
+The **Apply a patch or PR** panel applies a pull request or a `.diff`/`.patch` file to your site's checkout and rebuilds, so you can test someone else's work before adding your own. Your own changes are left alone.
+
+
+
+## Choose what to apply
+
+There are three ways to get a patch into the panel:
+
+- Paste a pull request URL or number into the field and click **Apply PR**.
+- Click **or choose a .diff / .patch file…** and pick a file from disk.
+- Click **Apply…** next to a pull request or attachment in the [Trac ticket panel](trac-tickets).
+
+## The preview
+
+Nothing is changed yet. The panel first shows what the patch would do:
+
+- The list of files it changes.
+- A warning if you have your own edits to any of those files. The patch is applied on top of them: it succeeds if the changes do not overlap, and fails without touching anything if they do. Save a patch of your work first if you want a copy — see [Submitting your changes](submitting-changes).
+- Which files are binary and will be skipped.
+- Whether it changes `package-lock.json`, in which case dependencies will be installed before the rebuild.
+
+Click **Apply and rebuild** to go ahead, or **Cancel** to back out.
+
+## Apply and rebuild
+
+The panel shows each step as it runs: applying the patch, installing dependencies if needed, and rebuilding. When it finishes, the panel reports what is applied — the patch's name, how many files it changed, and when.
+
+If a step fails, the error says what went wrong and the checkout was not changed.
+
+## Reverting an applied patch
+
+While a patch is applied, the panel shows it in a green box with a **Revert this patch** button. Reverting removes the patch's changes and rebuilds, again leaving your own edits alone.
+
+Very large patches cannot be undone automatically. The panel says so; use **Update to latest trunk** to reset the checkout instead — see [Staying up to date with trunk](trunk-updates).
+
+## Your own changes
+
+Applying and reverting patches never discards your own edits. The only risk is overlap: if a patch touches the same lines you changed, the apply fails cleanly rather than mixing the two. The preview names the files where you both have changes, so you see this before pressing anything.
+
+## Next steps
+
+- [Link the ticket you are testing](trac-tickets)
+- [Submit your own changes](submitting-changes)
diff --git a/docs/guide/creating-a-site.md b/docs/guide/creating-a-site.md
new file mode 100644
index 0000000..f0a9595
--- /dev/null
+++ b/docs/guide/creating-a-site.md
@@ -0,0 +1,45 @@
+# Creating a site
+
+A site is your own copy of `wordpress-develop`, the WordPress core development repository, cloned into a folder you choose. You can create as many sites as you like — each one is an independent checkout with its own working tree.
+
+When the app starts with no sites, the main area shows a short prompt to create your first one.
+
+
+
+## Start the creation flow
+
+Click **Create WordPress Core site** at the bottom of the sidebar. A dialog opens with two fields.
+
+
+
+- **Site name** — the label shown in the sidebar. It also determines the folder name: spaces and characters that are not valid in file names become hyphens, so a site named `My WordPress site` lives in a folder called `My-WordPress-site`.
+- **Site location** — the parent folder where the site will be created. The app adds a new directory inside it for the project; it does not clone into the folder you pick directly.
+
+Click **Create site** (or press Enter) to start. **Cancel** or Escape closes the dialog without creating anything.
+
+## What happens during setup
+
+The app clones the `wordpress-develop` repository from GitHub. Git is bundled with the app as `isomorphic-git`, a pure JavaScript implementation, so no system Git installation is involved.
+
+While the clone runs, the site view shows a **Setting up new site…** card with the current phase and a terminal panel streaming progress output. The clone downloads the full repository, so expect it to take several minutes depending on your connection.
+
+The clone is the first step of the [initial setup checklist](./setup-wizard); the remaining steps (installing dependencies, building, starting the dev server) stay locked until it finishes, then you drive them yourself.
+
+If setup fails, the half-created site is removed from the list — the row simply disappears. The
+reason goes to the application log rather than to a dialog, so **Help → Open App Log** is where
+to look when a site never finishes.
+
+## Where sites live on disk
+
+Each site is an ordinary folder on your disk: the parent folder you chose in **Site location**, plus a directory named after the site. Inside is a normal `wordpress-develop` checkout — you can open it in any editor or file manager. The app only records the path and some metadata; deleting a site from the app can also delete this folder (see [Managing sites](./managing-sites)).
+
+## The site list
+
+Every site appears in the sidebar, newest first. Click a site to switch to it; the button for the active site is highlighted. The chevron at the top collapses the sidebar to a narrow strip showing only each site's initial.
+
+A colored dot next to a site name warns that it has fallen behind trunk:
+
+- **Amber** — the checkout is more than 14 days old.
+- **Red** — a previous update moved the code but never finished installing or rebuilding.
+
+Both are fixed by [updating to the latest trunk](./trunk-updates) — you never need to create a new site just to get newer code.
diff --git a/docs/guide/database.md b/docs/guide/database.md
new file mode 100644
index 0000000..ef9e016
--- /dev/null
+++ b/docs/guide/database.md
@@ -0,0 +1,41 @@
+# Browsing the database
+
+The dev server runs WordPress on SQLite, and the app bundles [Adminer](https://www.adminer.org/)
+so you can look inside that database from your browser.
+
+## Open Adminer
+
+While the dev server is running, an **Open Adminer** button appears next to the server URL in the
+site view. Clicking it opens Adminer in your browser, already logged into the site's SQLite
+database — no credentials to enter. From there you can browse tables, inspect rows, and run SQL,
+the same way you would against a MySQL-backed install.
+
+The database file itself lives inside the Playground environment at
+`/wordpress/wp-content/database/.ht.sqlite`. It is not a file you can open directly on disk; go
+through Adminer.
+
+Adminer is only reachable while the dev server runs — the button disappears when the server
+stops, and so does the page it opened.
+
+## Is SQLite enough for core development?
+
+For most new contributors, yes. WordPress's SQLite support has matured considerably: most plugins
+and most core unit tests work, and thanks to the query parser, remaining gaps are tracked and
+steadily closed.
+
+It is not a complete substitute for MySQL, though. Be aware of the limits:
+
+- Some queries and features behave differently on SQLite than on MySQL, and a small number do not
+ work at all.
+- If your contribution touches the database layer itself — `wpdb`, schema changes, MySQL-specific
+ SQL — you should verify it against a real MySQL install before submitting. The toolkit cannot
+ do that for you: WordPress Playground can work with MySQL, but this app does not ship a MySQL
+ server.
+
+For the typical first contribution — a fix in PHP, JavaScript, or CSS that reads and writes
+ordinary posts, options, and users — the difference will not affect you.
+
+## Related pages
+
+- [Running the site](./running-the-site) — starting and stopping the dev server.
+- [Logs and debugging](./logs-and-debugging) — where database errors surface as PHP notices.
diff --git a/docs/guide/editors.md b/docs/guide/editors.md
new file mode 100644
index 0000000..f135c14
--- /dev/null
+++ b/docs/guide/editors.md
@@ -0,0 +1,40 @@
+# Opening the site in your editor
+
+The app is not an editor. To change WordPress code you open the site's directory in whatever editor you already use, and the site header gives you one control for that.
+
+
+
+## The Open directory in menu
+
+Under the site's path in the header, click **Open directory in**. The menu contains:
+
+- The system file manager — **Finder** on macOS, **File Explorer** on Windows, **File manager** on Linux.
+- Any editors the app detected on your machine, by name — for example **Visual Studio Code**, **Cursor**, **PhpStorm**, **Sublime Text**, or **Zed**.
+- **Other application…** — a picker for choosing any application yourself.
+
+Choosing an entry opens the site's directory in that application.
+
+## How detection works
+
+Detection checks the standard install locations for a short list of common editors: VS Code, Cursor, PhpStorm, Sublime Text, and Zed. It runs each time you open the menu — while it is checking, the menu shows **Looking for applications…** — so an editor you install while the app is running appears the next time you open the menu.
+
+The list is a shortcut, not a limit. Detection deliberately checks fixed filesystem paths rather than your shell's `PATH` (a packaged desktop app does not inherit it), so it can miss editors installed somewhere unusual — JetBrains standalone installers with version-numbered directories, for instance, or most Linux packaging variants. An editor the list misses is not one the app refuses to use: that is what **Other application…** is for, and it is always in the menu, not only as a fallback.
+
+## Other application…
+
+**Other application…** opens your system's application picker. Choose any application and the site directory is handed to it. This works for any editor, detected or not, and is the reliable path when detection comes up empty.
+
+## Copy path and the file manager
+
+Two related controls sit nearby:
+
+- The copy icon next to the path in the header copies the site's full path to the clipboard (it briefly shows **Copied!**). Use it to open the directory from an editor's own **Open Folder** dialog or from a terminal.
+- The ☰ **More** menu also offers **Copy path** and **Show in Finder** / **Show in Explorer** (**Show in file manager** on Linux), which reveals the directory in the system file manager.
+
+## When an open fails
+
+If an application fails to open the directory — it was uninstalled, or the launch was refused — an inline notice appears directly under the **Open directory in** menu explaining what went wrong. When it makes sense, the notice carries a **Choose application…** button that opens the same picker as **Other application…**, so you can point at a working application without hunting for the menu again.
+
+## What to edit
+
+WordPress source lives under `src/` in the site directory. While the dev server is running, a watcher rebuilds your edits into `build/` automatically — see [Running the site](./running-the-site). When your change is ready, see [Submitting changes](./submitting-changes).
diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md
new file mode 100644
index 0000000..d095d2a
--- /dev/null
+++ b/docs/guide/getting-started.md
@@ -0,0 +1,62 @@
+# Getting started
+
+The WordPress Contributor Toolkit is a desktop application for macOS (Apple Silicon), Windows, and Linux that sets up a full WordPress core development environment with zero prerequisites.
+
+You install it, choose a directory for `wordpress-develop`, click a button, and you have:
+
+- A cloned `wordpress-develop` repository
+- A running WordPress dev server
+- The ability to make code changes and turn them into a patch or pull request
+
+No Git, no Node.js, no npm, no Docker needed. Everything is bundled inside the application as JavaScript/WASM, powered by [WordPress Playground](https://wordpress.github.io/wordpress-playground/).
+
+## Install the app
+
+1. Download the latest packaged build for your platform from the [Releases page](https://github.com/WordPress/contributor-toolkit/releases/latest). Pick the file that matches your OS:
+ - **macOS on Apple Silicon:** the `.dmg` file whose name contains `arm64`. Intel Macs are not currently supported.
+ - **Windows:** the `.exe` installer.
+ - **Linux:** the `.AppImage`; `.deb` and `.snap` packages may also be available.
+2. Open the app.
+
+### If macOS blocks the app
+
+The app is signed and notarized by Automattic, so macOS should open it without issues. If Gatekeeper still blocks it (this can happen when the file was downloaded via a browser), try either of these:
+
+- Right-click the `.app` file and choose **Open**, then confirm in the dialog that appears.
+- Or remove the quarantine attribute from the `.app` bundle itself:
+
+ ```sh
+ xattr -d com.apple.quarantine "WordPress Contributor Toolkit.app"
+ ```
+
+ ::: warning Use `-d`, not `-dr`
+ The app is code-signed. The recursive flag (`-r`) tries to strip attributes from files inside the sealed bundle, which macOS rejects with permission errors. Removing the attribute from the top-level bundle is sufficient.
+ :::
+
+## Your first contribution, in ten steps
+
+1. Click **Create WordPress Core site** and choose a destination folder for your site.
+2. Wait while the app downloads `wordpress-develop`.
+3. Click **Install npm dependencies**.
+4. Click **Run full build**.
+5. Click **Start dev server**.
+6. A browser window opens automatically. If not, open it by clicking the site URL.
+7. Make changes to the code — [open the site in your editor](./editors) straight from the app.
+8. Click **Submit changes** to see a diff of everything you changed.
+9. Pick where the patch goes: [a pull request](./submit-github-pr), [a Trac ticket](./submit-trac), or [a file for your mentor](./submit-mentor).
+10. That's it — you've contributed to WordPress core.
+
+
+
+The rest of this guide walks through each of these screens in detail, starting with [creating a site](./creating-a-site).
+
+::: tip You never need a new site to get newer code
+**Update to latest trunk**, in the ☰ menu at the top right of any site, is available at any time on any site, however old: it fetches the latest trunk, reinstalls dependencies if they changed, and rebuilds. See [Staying up to date with trunk](./trunk-updates).
+:::
+
+## How it works under the hood
+
+- Git operations are handled by `isomorphic-git`, a pure JavaScript implementation of Git.
+- Node scripts and npm commands run on the Node.js runtime bundled with the Electron app. A small shim directory is injected into the `PATH` so subprocesses find `node`, `npm`, and `npx` without a system install.
+- The WordPress server runs on `@wp-playground/cli` from [WordPress Playground](https://wordpress.github.io/wordpress-playground/), backed by SQLite.
+- Patches are generated with the `diff` npm package.
diff --git a/docs/guide/logs-and-debugging.md b/docs/guide/logs-and-debugging.md
new file mode 100644
index 0000000..899bd69
--- /dev/null
+++ b/docs/guide/logs-and-debugging.md
@@ -0,0 +1,52 @@
+# Logs and debugging
+
+The **Logs** section of the site view has two tabs:
+
+- **Server** — the dev server's own output: everything the Playground server prints while
+ starting and running.
+- **debug.log** — WordPress's PHP log for this site, streamed live while the dev server runs.
+
+Output from `npm install` and `npm run` commands appears in the [Terminal](./terminal), not here.
+
+
+
+## The debug.log tab
+
+Anything WordPress or your code writes to the PHP error log — `error_log()` calls, notices,
+warnings, deprecations, `_doing_it_wrong()`, fatals — appears here while the dev server runs.
+This works because every site is booted with WordPress's debug constants already set. They are
+not configurable:
+
+| Constant | Value | Effect |
+| --- | --- | --- |
+| `WP_DEBUG` | `true` | Notices, warnings, and deprecations are reported. |
+| `WP_DEBUG_LOG` | `true` | They are written to `wp-content/debug.log`, which this panel tails. |
+| `WP_DEBUG_DISPLAY` | `true` | Errors are also printed in the browser. |
+| `SCRIPT_DEBUG` | `true` | Core serves unminified JS and CSS. |
+| `WP_DISABLE_FATAL_ERROR_HANDLER` | `true` | A fatal shows the actual error instead of WordPress's "critical error" recovery screen. |
+| `AUTOMATIC_UPDATER_DISABLED` | `true` | Core's automatic updater does not run (and does not fill the log with its own messages). |
+
+Note that `WP_DEBUG_DISPLAY` has a known cost: a notice fired during a REST or AJAX request is
+printed into the response and can corrupt the JSON it expects. That trade is made deliberately —
+seeing the error beats a silent blank page for a newcomer.
+
+While you are reading another tab, the **debug.log** tab shows an unread count, for example
+**debug.log (3)**, so a notice landing while you watch the server output does not go unseen.
+Selecting the tab resets the count. The panel keeps the most recent 512 KB of the log.
+
+Under the pane:
+
+- The full path to the log file (inside the site's `build/wp-content/` directory) is shown and can
+ be selected and copied — useful for tailing it in a real terminal or attaching it to a ticket.
+- **Show in folder** reveals the file in your file manager.
+- **Copy** puts the panel's contents on the clipboard, ready to paste into a Trac ticket or a pull
+ request comment.
+- **Clear** empties both the panel and the file on disk. If the file cannot be cleared, the panel
+ says so — otherwise the same lines would replay the next time the server starts.
+
+## The app's own log
+
+The application keeps a separate log of its own activity — server starts, installs, errors. Do not
+confuse it with the site's debug.log. Open it from the menu: **Help → Open App Log**, or
+**Help → Show Logs Folder** to reveal the directory. This is the file to attach when
+[reporting a problem with the app itself](./troubleshooting).
diff --git a/docs/guide/mail.md b/docs/guide/mail.md
new file mode 100644
index 0000000..3a87381
--- /dev/null
+++ b/docs/guide/mail.md
@@ -0,0 +1,38 @@
+# The Mail panel
+
+WordPress sends email constantly — new user notifications, password resets, comment moderation.
+On a development site none of that should reach a real inbox, and with this app none of it can:
+each site gets its own built-in SMTP catcher, listening only on `127.0.0.1`. WordPress's
+`wp_mail()` is pointed at it, so every email the site sends lands in the **Mail** panel instead
+of leaving your machine.
+
+
+
+## How it works
+
+The SMTP catcher starts and stops together with the dev server. While the server is running, the
+panel shows the address it listens on, for example `SMTP listening on 127.0.0.1:54321` — the port
+is assigned by the operating system, so it varies. Before the server has started, the panel says
+`SMTP will start with the dev server.`
+
+No configuration is needed on the WordPress side: the app installs a small must-use plugin in the
+Playground environment that routes `wp_mail()` through SMTP to the catcher.
+
+## Reading emails
+
+Captured emails appear in a list showing the date, the sender, and the subject. Click an email
+(or press Enter on it) to open it in a full-screen viewer with the **From**, **To**, **CC**, and
+**Date** headers and two tabs:
+
+- **Rendered** — the HTML body as a mail client would show it, or the plain-text body if the
+ email has no HTML part.
+- **Raw** — the raw message source, headers and all. Useful when the bug you are chasing is in
+ how core builds the email itself.
+
+**Clear emails** deletes all captured emails for the site.
+
+## Trying it out
+
+Trigger any email-sending flow on the dev site — the password reset form at `/wp-login.php?action=lostpassword`
+is the quickest — and the email appears in the panel. See [Running the site](./running-the-site)
+for starting the server.
diff --git a/docs/guide/managing-sites.md b/docs/guide/managing-sites.md
new file mode 100644
index 0000000..746c2da
--- /dev/null
+++ b/docs/guide/managing-sites.md
@@ -0,0 +1,52 @@
+# Managing sites
+
+Everything about a site's identity and lifecycle lives in its header: the name, a few status
+badges, the path, and the **More** menu (☰) at the top right.
+
+
+
+## The site header
+
+- **Rename** — click the pencil icon next to the site title to give the site a different display
+ name. This changes the label in the app only; the directory on disk keeps its name.
+- **Status badge** — **INITIALIZED** once the repository is cloned; **UNINITIALIZED** before
+ that.
+- **Created date** — when the site was created.
+- **Trunk age** — how old the site's snapshot of `wordpress-develop` trunk is. When the snapshot
+ gets stale, an amber dot appears next to the label; hover it to see the age in days. See
+ [Staying up to date with trunk](./trunk-updates).
+- **Path** — the site's directory, with a copy button next to it. The **Open directory in** menu
+ underneath opens it in your file manager or an editor; see [Editors](./editors).
+
+## The More menu
+
+The ☰ menu at the top right of the site view contains:
+
+- **Copy path** — puts the site's directory path on the clipboard.
+- **Show in Finder** (macOS) / **Show in Explorer** (Windows) — reveals the directory in
+ your file manager.
+- **Update to latest trunk** — fetches the latest `wordpress-develop` trunk and rebuilds. Also
+ reachable from the staleness notice; on an already-fresh site it just prints "Already up to
+ date." See [Staying up to date with trunk](./trunk-updates).
+- **Forget this site** — removes the site from the app's list. The directory and everything in
+ it stay on disk; you can add the folder back later.
+- **Delete this site** — removes the site from the list **and deletes its directory from disk**.
+ This cannot be undone. The app will only ever delete a directory it has on record as a site —
+ never an arbitrary path.
+
+Both **Forget** and **Delete** ask for confirmation first. If you are unsure which you want:
+forget is reversible, delete is not.
+
+## Updating with uncommitted changes
+
+If you start **Update to latest trunk** while the site has local edits, the app does not silently
+throw them away. A dialog titled **Update to latest trunk?** lists every changed file and offers
+two choices:
+
+- **Save them as a patch first (as a local file)** — writes a `.diff` to your machine, then
+ updates. Nothing is sent to Trac.
+- **Discard them** — your changes are lost; this cannot be undone.
+
+Confirm with **Save patch & update** or **Discard & update**, or **Cancel** to keep everything as
+it is. If you meant to keep the changes as a contribution instead, see
+[Submitting your changes](./submitting-changes).
diff --git a/docs/guide/running-the-site.md b/docs/guide/running-the-site.md
new file mode 100644
index 0000000..d7b5a07
--- /dev/null
+++ b/docs/guide/running-the-site.md
@@ -0,0 +1,46 @@
+# Running the site
+
+Once the [setup wizard](./setup-wizard) is complete, the site view shows a **Start dev server** button. This starts a local WordPress that serves the code in your site's `build/` directory, so you can see your changes running.
+
+
+
+## Start and stop
+
+Click **Start dev server**. The button cycles through three states:
+
+- **Start dev server** — nothing is running.
+- **Starting dev server...** — the server is booting. A "Dev server is starting…" line below the button shows the elapsed time.
+- **Stop dev server** — the server is up. The button shows a red dot; clicking it stops the server.
+
+When the server is ready, its URL appears below the button, for example `http://127.0.0.1:/`. Click it to open the site in your default browser.
+
+The button is disabled while a trunk update is running, and a trunk update is blocked while the server runs — the two would fight over the same files.
+
+## Logging in
+
+Every site uses the same credentials, shown next to the URL:
+
+- Username: `admin`
+- Password: `password`
+
+Append `/wp-admin/` to the site URL to reach the dashboard.
+
+## What the dev server actually runs
+
+The dev server is not a stub — it is WordPress Playground running your checkout:
+
+- The app spawns the [Playground CLI](https://wordpress.github.io/wordpress-playground/) (`@wp-playground/cli`) in server mode, with your site's `build/` directory mounted as the WordPress root. PHP runs as WebAssembly inside the bundled Node.js runtime, so no PHP install is needed.
+- The database is **SQLite**, stored inside the Playground instance. This covers most core contribution work; if a ticket specifically needs MySQL behaviour, this environment cannot reproduce it.
+- Alongside the server, the app runs the core build watcher, so edits under `src/` are rebuilt into `build/` while the server is up. A page reload then shows the change.
+- The server binds to the loopback interface only. It is reachable from your machine, not from the rest of your network.
+- Outgoing mail is captured locally instead of being sent — see [Mail](./mail).
+
+## Open Adminer
+
+While the server is running, an **Open Adminer** button appears next to **Stop dev server**. It opens Adminer, a database browser, against the site's SQLite database — useful for inspecting what a code change wrote. See [Database](./database) for details.
+
+The button disappears when the server stops, because there is no database to connect to.
+
+## Where the output goes
+
+Everything the server and the watcher print streams into the site's terminal panel, which is also where startup errors land. See [Terminal](./terminal) and [Logs and debugging](./logs-and-debugging).
diff --git a/docs/guide/setup-wizard.md b/docs/guide/setup-wizard.md
new file mode 100644
index 0000000..006767f
--- /dev/null
+++ b/docs/guide/setup-wizard.md
@@ -0,0 +1,44 @@
+# The setup wizard
+
+After [creating a site](./creating-a-site), its view shows the **Initial setup checklist**. Complete each step to prepare the site for development. The steps run in order — each one unlocks the next.
+
+
+
+## The four steps
+
+### 1. Download WordPress development version
+
+The clone of `wordpress-develop` that started when you created the site. It runs on its own; while it is in progress the checklist reads "Cloning the WordPress develop repository… the next step unlocks when it finishes." and every later step stays locked.
+
+### 2. Install npm dependencies
+
+Click **Install npm dependencies** to run `npm install` using the Node.js runtime bundled with the app — no system Node or npm is needed. If the install fails, the button changes to **Retry npm install**; a leftover `node_modules` folder from a failed run does not count as completed.
+
+Once installed, the button reads **Dependencies installed** and stays disabled. If you later add a dependency to `package.json`, run `npm install` yourself in the [Terminal](./terminal).
+
+### 3. Run full build
+
+Click **Run full build** to compile WordPress core and generate the `dist` files the dev server serves. This is the longest step after the clone.
+
+You only run it manually once: [trunk updates](./trunk-updates) and [applied patches](./applying-patches) rebuild on their own. If you edit files in `src/` by hand, run `npm run build` in the Terminal so the site picks them up.
+
+### 4. Start dev server & finish wizard
+
+Click **Start dev server and finish the wizard** to launch the WordPress dev server for the first time. This completes the checklist and permanently replaces it with the compact action bar described below. The server URL appears next to the button once it is up — see [Running the site](./running-the-site).
+
+## Step states
+
+Each step carries a state label:
+
+- **Completed** — done, marked with a green check.
+- **In progress** — the step to do next; its button is enabled.
+- **Pending** — ready but waiting for you to reach it.
+- **Locked** — its prerequisites are not met yet; the button is disabled.
+
+Steps also lock temporarily while a [trunk update](./trunk-updates) is running, since the update owns the working tree during that time.
+
+## Skipping the wizard
+
+Below the checklist is a **Skip initialization wizard** link. Clicking it hides the checklist for this site, for good, and shows the compact action bar instead: the dev server start/stop button, **Submit changes**, and — while the server is running — **Open Adminer**.
+
+Skipping does not run any of the steps for you. If the dependencies were never installed or the build never ran, the dev server will not have anything to serve, so only skip on a site you know is already set up (for example, a `wordpress-develop` checkout you prepared outside the app). For everything else, finishing step 4 gets you to the same action bar with the work actually done.
diff --git a/docs/guide/submit-github-pr.md b/docs/guide/submit-github-pr.md
new file mode 100644
index 0000000..bffebf5
--- /dev/null
+++ b/docs/guide/submit-github-pr.md
@@ -0,0 +1,39 @@
+# Opening a pull request
+
+**Open a pull request** is the one destination on the [Submit changes](submitting-changes) screen where the app does the sending: it forks `wordpress-develop` to your GitHub account, pushes your change to a branch there, and opens the pull request — all through GitHub's API, as a single commit.
+
+You need a GitHub account, and the site must have a [linked Trac ticket](trac-tickets) — a core pull request has to cite one. The app cannot create the GitHub account for you, and it cannot post to Trac on your behalf.
+
+## Sign in with GitHub
+
+Click **Sign in with GitHub**. The app signs you in through your browser, using GitHub's device flow:
+
+1. The card shows a short code, and your browser opens `github.com/login/device`.
+2. Enter the code there — **Copy the code** puts it on your clipboard — and confirm the authorization on GitHub.
+3. The app waits until GitHub reports the sign-in went through.
+
+You never type a password into the app, and no credential is written to disk: the authorization is held in memory and forgotten when you quit. Click **Cancel** to abandon the sign-in, or **Not now** to decline it — the patch file is still yours to save, and the other two destinations are unchanged.
+
+Once signed in, the card says which account you are on and where the fork and branch will go: **your-username/wordpress-develop**. **Sign out** discards the authorization.
+
+## The pull request form
+
+- **Title** — what the change does, in one line. Reviewers scan these. Left empty, the pull request is titled **Ticket #NNNNN**.
+- **Notes for reviewers (optional)** — what the change does and why, how to see it working, anything you are unsure about. These go at the top of the description; the ticket link and your WordPress.org username are added underneath.
+
+Click **Open pull request** to send it. If anything fails, the error says why, and **Save the patch file instead** is always there — the patch exists regardless of what GitHub did.
+
+## How pull requests work in core
+
+Two things a first-timer has no way to know, stated on the card before the button:
+
+- Nobody watches the pull request list. Your pull request is seen because its link is posted on the Trac ticket — which is why the flow ends by sending you back there.
+- Nothing is merged on GitHub. A committer applies the change themselves, and the ticket is where they decide to.
+
+The card links to [the core handbook page on pull requests](https://make.wordpress.org/core/handbook/contribute/git/github-pull-requests-for-code-review/).
+
+## Test mode
+
+Developers of the app itself can point this flow at a sandbox repository, or make it a dry run that pushes a branch without opening a pull request. When either switch is set, a **Test mode** banner appears on the card saying exactly what will happen, and the dry-run button reads **Push branch (dry run)** instead of **Open pull request**. In a normal build you will never see this banner.
+
+If a build has no GitHub application configured, the card says so — it cannot open a pull request, but the other destinations still work.
diff --git a/docs/guide/submit-mentor.md b/docs/guide/submit-mentor.md
new file mode 100644
index 0000000..37dda34
--- /dev/null
+++ b/docs/guide/submit-mentor.md
@@ -0,0 +1,42 @@
+# Handing a patch to a mentor
+
+**Hand it to a mentor** is the destination on the [Submit changes](submitting-changes) screen for the contributor who will not create a GitHub account today — common at a contributor day, where a mentor collects patches and pushes them on the contributors' behalf. It needs no accounts at all. Someone else pushes the patch, and the props still land on you.
+
+## What the app asks for
+
+The first time, the card asks for two things:
+
+- **WordPress.org username** — required. This is who the work is credited to.
+- **Event** — optional. Where the patch was written: "WordCamp Europe 2026", a meetup, a company contributor day. It is free text, because most of these are named the day they happen.
+
+Click **Remember this**. Both values are remembered for every site — they are facts about you, not about one checkout. On a shared laptop, click **Change these** and clear them before the next person takes over.
+
+## Saving the patch
+
+Click **Save patch as your-username**. The app saves a patch file that carries its own provenance, as comment lines before the diff:
+
+```
+# WordPress Contributor Toolkit patch
+# Contributor: janedoe (wordpress.org)
+# Event: WordCamp Europe 2026
+# Ticket: https://core.trac.wordpress.org/ticket/62281
+# Base: trunk @ a1b2c3d, 2026-08-09
+# Generated: 2026-08-10
+```
+
+The header says who made the patch, at which event, for which ticket, against which trunk revision, and when. A field the app does not know is left out rather than written as "unknown", so a mentor can trust the lines that are there. The `#` lines are ignored by `git apply`, `patch`, and Trac's own tooling, so the file applies exactly like a plain diff.
+
+Under the button, the card confirms what the header will claim — **The patch will say it was written at …**, or "No event on the patch" — on every save, so a remembered event from last year cannot stamp patches unnoticed.
+
+## The file name
+
+The suggested filename carries the ticket and your username:
+
+- `62281.janedoe.diff` when a ticket is [linked to the site](trac-tickets)
+- `janedoe.diff` when none is
+
+The username is in the filename because it is what a mentor sorts a folder of patches by; the header is what survives if someone renames the file.
+
+## What the mentor does with it
+
+The mentor uploads the patch to the Trac ticket (or turns it into a pull request) from their own account. The header tells them — and whoever assigns props — whose work it is and what it was based on, so the credit follows the file, not the account that uploaded it.
diff --git a/docs/guide/submit-trac.md b/docs/guide/submit-trac.md
new file mode 100644
index 0000000..a5397b0
--- /dev/null
+++ b/docs/guide/submit-trac.md
@@ -0,0 +1,30 @@
+# Attaching a patch to Trac
+
+**Attach to Trac** is the traditional way to contribute a WordPress core change: a `.diff` file attached to the ticket. It needs a WordPress.org account — which you need anyway, for props and to comment on the ticket.
+
+## What the app does, and what you do
+
+The app does not post to Trac on your behalf — that would mean it holding a WordPress.org session, which it deliberately never does. Instead, the destination on the [Submit changes](submitting-changes) screen splits the work:
+
+1. Click **Save, then open #NNNNN**.
+2. The app saves your patch as a file wherever you choose.
+3. Once the file exists, the app opens the ticket's attach page in your browser.
+4. On that page, sign in to WordPress.org if you are not already, choose the file you just saved, and upload it yourself.
+
+The browser is only opened after a file exists, so you never land on an attach form with nothing to attach.
+
+The saved file is the plain diff shown under **Your changes** — no header is added, because a patch attached to Trac conventionally carries none.
+
+## If no ticket is linked
+
+The card says there is nowhere to attach the patch yet, and offers the same field as the [Trac ticket panel](trac-tickets): type the ticket number or URL and click **Link ticket**. The ticket is stored with the site, and the **Save, then open** button appears in its place.
+
+## What happens afterwards
+
+No automated checks run on a Trac attachment. It is common for a reviewer to then ask for the same change as a pull request, where the test suite runs — if that happens, the [pull request destination](submit-github-pr) sends the identical diff.
+
+After uploading, leave a comment on the ticket saying what the patch does and how you tested it. An attachment with no comment is easy to miss.
+
+## Before you attach
+
+If the warning at the top of the **Submit changes** screen says your site's WordPress code is old, the patch may not apply cleanly on Trac. Update to the latest trunk first — see [Staying up to date with trunk](trunk-updates) — and check your change still works.
diff --git a/docs/guide/submitting-changes.md b/docs/guide/submitting-changes.md
new file mode 100644
index 0000000..c9d1d96
--- /dev/null
+++ b/docs/guide/submitting-changes.md
@@ -0,0 +1,36 @@
+# Submitting your changes
+
+When your change works and you want to contribute it, open the **Submit changes** screen. It shows your work as a patch and, next to it, the places that patch can go.
+
+## Your changes
+
+The left side of the screen is the diff, titled **Your changes** — everything this site has that its copy of trunk does not. This is exactly what will be submitted, so read it before choosing a destination.
+
+Two buttons sit above the diff:
+
+- **Save** — saves the diff as a patch file wherever you choose.
+- **Copy** — copies the whole diff to the clipboard.
+
+If the site has no changes against its copy of trunk, the screen says there is nothing to send yet. If the site's WordPress code is old, a warning says the patch may not apply on Trac and suggests updating to the latest trunk first — see [Staying up to date with trunk](trunk-updates).
+
+## Where this patch goes
+
+The right side lists three destinations. The pull request is the one the app sends for you; the other two save a file for you to send. Each card states what it costs to use and what happens afterwards, so you can choose with the trade-offs in front of you.
+
+- **Open a pull request** — needs a GitHub account. The app forks `wordpress-develop` to your account, pushes your change to a branch, and opens the pull request. Automated checks run on it. [Opening a pull request](submit-github-pr)
+- **Attach to Trac** — needs a WordPress.org account, which you need anyway for props and to comment. The app saves the patch file and opens the ticket's attach page; you upload it yourself. No automated checks run. [Attaching a patch to Trac](submit-trac)
+- **Hand it to a mentor** — needs no accounts at all. The app saves a patch file carrying your WordPress.org username and the event you are at; someone else pushes it, and the props still land on you. [Handing a patch to a mentor](submit-mentor)
+
+## Your WordPress.org username and event
+
+The mentor destination asks for your WordPress.org username and, optionally, the event you are contributing from (for example a WordCamp contributor day). Both are asked once and remembered for every site — they are facts about you, not about one checkout.
+
+The username and event are embedded in the patch file itself, as comment lines at the top, along with the ticket, the trunk revision the patch is based on, and the date. A filename survives until someone renames the download; the header survives with the file. This is what lets a mentor upload your patch with your name on the work.
+
+The event is shown on every save — **The patch will say it was written at …** — so a remembered event from last year cannot keep stamping patches unnoticed. Click **Change these** to update either value, or clear them.
+
+## Next steps
+
+- [Opening a pull request](submit-github-pr)
+- [Attaching a patch to Trac](submit-trac)
+- [Handing a patch to a mentor](submit-mentor)
diff --git a/docs/guide/terminal.md b/docs/guide/terminal.md
new file mode 100644
index 0000000..6a23c77
--- /dev/null
+++ b/docs/guide/terminal.md
@@ -0,0 +1,46 @@
+# The Terminal panel
+
+Each site view includes an embedded terminal. It is not a general-purpose shell: it accepts a
+small, fixed set of commands, all of which run in the site's directory using the Node.js runtime
+bundled with the app. This is enough to rebuild the site after editing code — the reason the panel
+exists — without requiring Node or npm on your machine.
+
+
+
+## Supported commands
+
+Type `help` to see the list at any time:
+
+| Command | What it does |
+| --- | --- |
+| `help` | Lists the supported commands. |
+| `npm install` | Runs `npm install` in the site directory. `npm i` and `install` work too. |
+| `npm run