diff --git a/GUIDE.md b/GUIDE.md
new file mode 100644
index 0000000..4757771
--- /dev/null
+++ b/GUIDE.md
@@ -0,0 +1,846 @@
+# Toes User Guide
+
+Toes is a personal web appliance that runs multiple web apps on your home network. Plug it in, turn it on, and forget about the cloud.
+
+## Table of Contents
+
+- [Quick Start](#quick-start)
+- [Creating an App](#creating-an-app)
+ - [App Templates](#app-templates)
+ - [App Structure](#app-structure)
+ - [The Bare Minimum](#the-bare-minimum)
+ - [Using Hype](#using-hype)
+ - [Using Forge](#using-forge)
+- [Creating a Tool](#creating-a-tool)
+ - [What's a Tool?](#whats-a-tool)
+ - [Tool Setup](#tool-setup)
+ - [Theme Tokens](#theme-tokens)
+ - [Accessing App Data](#accessing-app-data)
+- [CLI Reference](#cli-reference)
+ - [App Management](#app-management)
+ - [Lifecycle](#lifecycle)
+ - [Syncing Code](#syncing-code)
+ - [Environment Variables](#environment-variables)
+ - [Versioning](#versioning)
+ - [Cron Jobs](#cron-jobs-1)
+ - [Metrics](#metrics)
+ - [Sharing](#sharing)
+ - [Configuration](#configuration)
+- [Environment Variables](#environment-variables-1)
+- [Health Checks](#health-checks)
+- [App Lifecycle](#app-lifecycle)
+- [Cron Jobs](#cron-jobs)
+- [Data Persistence](#data-persistence)
+
+---
+
+## Quick Start
+
+```bash
+# Install the CLI
+curl -fsSL http://toes.local/install | bash
+
+# Create a new app
+toes new my-app
+
+# Enter the directory, install deps, and develop locally
+cd my-app
+bun install
+bun dev
+
+# Push to the server
+toes push
+
+# Open in browser
+toes open
+```
+
+Your app is now running at `http://my-app.toes.local`.
+
+---
+
+## Creating an App
+
+### App Templates
+
+Toes ships with three templates. Pick one when creating an app:
+
+```bash
+toes new my-app # SSR (default)
+toes new my-app --bare # Minimal
+toes new my-app --spa # Single-page app
+```
+
+**SSR** — Server-side rendered with a pages directory. Best for most apps. Uses Hype's built-in layout and page routing.
+
+**Bare** — Just an `index.tsx` with a single route. Good when you want to start from scratch.
+
+**SPA** — Client-side rendering with `hono/jsx/dom`. Hype serves the HTML shell and static files; the browser handles routing and rendering.
+
+### App Structure
+
+A generated SSR app looks like this:
+
+```
+my-app/
+ .npmrc # Points to the private registry
+ .toesignore # Files to exclude from sync (like .gitignore)
+ package.json # Must have scripts.toes
+ tsconfig.json # TypeScript config
+ index.tsx # Entry point (re-exports from src/server)
+ src/
+ server/
+ index.tsx # Hype app with routes
+ pages/
+ index.tsx # Page components
+```
+
+### The Bare Minimum
+
+Every app needs three things:
+
+1. **`package.json`** with a `scripts.toes` entry
+2. **`index.tsx`** that exports `app.defaults`
+3. **A `GET /ok` route** that returns 200 (health check)
+
+**package.json:**
+
+```json
+{
+ "name": "my-app",
+ "module": "index.tsx",
+ "type": "module",
+ "private": true,
+ "scripts": {
+ "toes": "bun run --watch index.tsx"
+ },
+ "toes": {
+ "icon": "🎨"
+ },
+ "dependencies": {
+ "@because/hype": "*",
+ "@because/forge": "*"
+ }
+}
+```
+
+The `scripts.toes` field is how Toes discovers your app. The `toes.icon` field sets the emoji shown in the dashboard.
+
+**.npmrc:**
+
+```
+registry=https://npm.nose.space
+```
+
+Required for installing `@because/*` packages.
+
+**index.tsx:**
+
+```tsx
+import { Hype } from '@because/hype'
+
+const app = new Hype()
+
+app.get('/', c => c.html(
Hello World
))
+app.get('/ok', c => c.text('ok'))
+
+export default app.defaults
+```
+
+That's it. Push to the server and it runs.
+
+### Using Hype
+
+Hype wraps [Hono](https://hono.dev). Everything you know from Hono works here. Hype adds a few extras:
+
+**Basic routing:**
+
+```tsx
+import { Hype } from '@because/hype'
+
+const app = new Hype()
+
+app.get('/', c => c.html(
Home
))
+app.get('/about', c => c.html(
About
))
+app.post('/api/items', async c => {
+ const body = await c.req.json()
+ return c.json({ ok: true })
+})
+app.get('/ok', c => c.text('ok'))
+
+export default app.defaults
+```
+
+**Sub-routers:**
+
+```tsx
+const api = Hype.router()
+api.get('/items', c => c.json([]))
+api.post('/items', async c => {
+ const body = await c.req.json()
+ return c.json({ ok: true })
+})
+
+app.route('/api', api) // mounts at /api/items
+```
+
+**Server-Sent Events:**
+
+```tsx
+app.sse('/stream', (send, c) => {
+ send({ hello: 'world' })
+ const interval = setInterval(() => send({ time: Date.now() }), 1000)
+ return () => clearInterval(interval) // cleanup on disconnect
+})
+```
+
+**Constructor options:**
+
+```tsx
+const app = new Hype({
+ layout: true, // Wraps pages in an HTML layout (default: true)
+ prettyHTML: true, // Pretty-print HTML output (default: true)
+ logging: true, // Log requests to stdout (default: true)
+})
+```
+
+### Using Forge
+
+Forge is a CSS-in-JS library that creates styled JSX components. Define a component once, use it everywhere.
+
+**Basic usage:**
+
+```tsx
+import { define, stylesToCSS } from '@because/forge'
+
+const Box = define('Box', {
+ padding: 20,
+ borderRadius: '6px',
+ backgroundColor: '#f5f5f5',
+})
+// content renders
content
+```
+
+Numbers auto-convert to `px` (except `flex`, `opacity`, `zIndex`, `fontWeight`).
+
+**Set the HTML element:**
+
+```tsx
+const Button = define('Button', { base: 'button', padding: '8px 16px' })
+const Link = define('Link', { base: 'a', textDecoration: 'none' })
+const Input = define('Input', { base: 'input', padding: 8, border: '1px solid #ccc' })
+```
+
+**Pseudo-classes (`states`):**
+
+```tsx
+const Item = define('Item', {
+ padding: 12,
+ states: {
+ ':hover': { backgroundColor: '#eee' },
+ ':last-child': { borderBottom: 'none' },
+ },
+})
+```
+
+**Nested selectors:**
+
+```tsx
+const List = define('List', {
+ selectors: {
+ '& > li:last-child': { borderBottom: 'none' },
+ },
+})
+```
+
+**Variants:**
+
+```tsx
+const Button = define('Button', {
+ base: 'button',
+ padding: '8px 16px',
+ variants: {
+ variant: {
+ primary: { backgroundColor: '#2563eb', color: 'white' },
+ danger: { backgroundColor: '#dc2626', color: 'white' },
+ },
+ },
+})
+//
+```
+
+**Serving CSS:**
+
+Forge generates CSS at runtime. Serve it from a route:
+
+```tsx
+import { stylesToCSS } from '@because/forge'
+
+app.get('/styles.css', c =>
+ c.text(stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8' })
+)
+```
+
+Then link it in your HTML:
+
+```tsx
+
+```
+
+---
+
+## Creating a Tool
+
+### What's a Tool?
+
+A tool is an app that appears as a tab inside the Toes dashboard instead of in the sidebar. Tools render in an iframe and receive the currently selected app as a `?app=` query parameter. Good for things like a code editor, log viewer, env manager, or cron scheduler.
+
+From the server's perspective, a tool is identical to an app — same lifecycle, same health checks, same port allocation. The only differences are in `package.json` and how you render.
+
+### Tool Setup
+
+A tool needs three extra things compared to a regular app:
+
+1. Set `"tool": true` in `package.json`
+2. Include `` in the HTML body
+3. Prepend `baseStyles` to CSS output
+
+**package.json:**
+
+```json
+{
+ "name": "my-tool",
+ "module": "index.tsx",
+ "type": "module",
+ "private": true,
+ "scripts": {
+ "toes": "bun run --watch index.tsx"
+ },
+ "toes": {
+ "icon": "🔧",
+ "tool": true
+ },
+ "dependencies": {
+ "@because/forge": "*",
+ "@because/hype": "*",
+ "@because/toes": "*"
+ }
+}
+```
+
+Set `"tool"` to `true` for a tab labeled with the app name, or to a string for a custom label (e.g., `"tool": ".env"`).
+
+**index.tsx:**
+
+```tsx
+import { Hype } from '@because/hype'
+import { define, stylesToCSS } from '@because/forge'
+import { baseStyles, ToolScript, theme } from '@because/toes/tools'
+import type { Child } from 'hono/jsx'
+
+const app = new Hype({ prettyHTML: false })
+
+const Container = define('Container', {
+ fontFamily: theme('fonts-sans'),
+ padding: '20px',
+ paddingTop: 0,
+ maxWidth: '800px',
+ margin: '0 auto',
+ color: theme('colors-text'),
+})
+
+function Layout({ title, children }: { title: string; children: Child }) {
+ return (
+
+
+
+
+ {title}
+
+
+
+
+ {children}
+
+
+ )
+}
+
+app.get('/ok', c => c.text('ok'))
+app.get('/styles.css', c =>
+ c.text(baseStyles + stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8' })
+)
+
+app.get('/', async c => {
+ const appName = c.req.query('app')
+ if (!appName) {
+ return c.html(
No app selected
)
+ }
+
+ return c.html(
+
+
{appName}
+
Tool content for {appName}
+
+ )
+})
+
+export default app.defaults
+```
+
+Key points:
+
+- `` handles dark/light mode syncing and iframe height communication with the dashboard.
+- `baseStyles` sets the body background to match the dashboard theme.
+- `prettyHTML: false` is recommended for tools since their output is inside an iframe.
+- The `?app=` query parameter tells you which app the user has selected in the sidebar.
+
+### Theme Tokens
+
+Tools should use theme tokens to match the dashboard's look. Import `theme` from `@because/toes/tools`:
+
+```tsx
+import { theme } from '@because/toes/tools'
+
+const Card = define('Card', {
+ color: theme('colors-text'),
+ backgroundColor: theme('colors-bgElement'),
+ border: `1px solid ${theme('colors-border')}`,
+ borderRadius: theme('radius-md'),
+ padding: theme('spacing-lg'),
+})
+```
+
+Available tokens:
+
+| Token | Description |
+|-------|-------------|
+| `colors-bg` | Page background |
+| `colors-bgSubtle` | Subtle background |
+| `colors-bgElement` | Element background (cards, inputs) |
+| `colors-bgHover` | Hover background |
+| `colors-text` | Primary text |
+| `colors-textMuted` | Secondary text |
+| `colors-textFaint` | Tertiary/disabled text |
+| `colors-border` | Borders |
+| `colors-link` | Link text |
+| `colors-primary` | Primary action color |
+| `colors-primaryText` | Text on primary color |
+| `colors-error` | Error color |
+| `colors-dangerBorder` | Danger state border |
+| `colors-dangerText` | Danger text |
+| `colors-success` | Success color |
+| `colors-successBg` | Success background |
+| `colors-statusRunning` | Running indicator |
+| `colors-statusStopped` | Stopped indicator |
+| `fonts-sans` | Sans-serif font stack |
+| `fonts-mono` | Monospace font stack |
+| `spacing-xs` | 4px |
+| `spacing-sm` | 8px |
+| `spacing-md` | 12px |
+| `spacing-lg` | 16px |
+| `spacing-xl` | 24px |
+| `radius-md` | 6px |
+
+### Accessing App Data
+
+**Reading app files:**
+
+```tsx
+import { join } from 'path'
+
+const APPS_DIR = process.env.APPS_DIR!
+
+app.get('/', c => {
+ const appName = c.req.query('app')
+ if (!appName) return c.html(
No app selected
)
+
+ const appPath = join(APPS_DIR, appName, 'current')
+ // Read files from appPath...
+})
+```
+
+Always go through the `current` symlink — never access version directories directly.
+
+**Calling the Toes API:**
+
+```tsx
+const TOES_URL = process.env.TOES_URL!
+
+// List all apps
+const apps = await fetch(`${TOES_URL}/api/apps`).then(r => r.json())
+
+// Get a specific app
+const app = await fetch(`${TOES_URL}/api/apps/${name}`).then(r => r.json())
+```
+
+**Linking between tools:**
+
+```html
+Edit in Code
+```
+
+Tool URLs go through `/tool/:name` which redirects to the tool's subdomain with query params preserved.
+
+**Listening to lifecycle events:**
+
+```tsx
+import { on } from '@because/toes/tools'
+
+const unsub = on('app:start', event => {
+ console.log(`${event.app} started at ${event.time}`)
+})
+
+// Event types: 'app:start', 'app:stop', 'app:create', 'app:delete', 'app:activate'
+```
+
+---
+
+## CLI Reference
+
+The CLI connects to your Toes server over HTTP. By default it connects to `http://toes.local`. Set `TOES_URL` to point elsewhere, or set `DEV=1` to use `http://localhost:3000`.
+
+Most commands accept an optional app name. If omitted, the CLI uses the current directory's `package.json` name.
+
+### App Management
+
+**`toes list`** — List all apps and their status.
+
+```bash
+toes list # Show apps and tools
+toes list --apps # Apps only (exclude tools)
+toes list --tools # Tools only
+```
+
+**`toes new [name]`** — Create a new app from a template.
+
+```bash
+toes new my-app # SSR template (default)
+toes new my-app --bare # Minimal template
+toes new my-app --spa # SPA template
+```
+
+Creates the app locally, then pushes it to the server. If run without a name, scaffolds the current directory.
+
+**`toes info [name]`** — Show details for an app (state, URL, port, PID, uptime).
+
+**`toes get `** — Download an app from the server to your local machine.
+
+```bash
+toes get my-app # Creates ./my-app/ with all files
+cd my-app
+bun install
+bun dev # Develop locally
+```
+
+**`toes open [name]`** — Open a running app in your browser.
+
+**`toes rename [name] `** — Rename an app. Requires typing a confirmation.
+
+**`toes rm [name]`** — Permanently delete an app from the server. Requires typing a confirmation.
+
+### Lifecycle
+
+**`toes start [name]`** — Start a stopped app.
+
+**`toes stop [name]`** — Stop a running app.
+
+**`toes restart [name]`** — Stop and start an app.
+
+**`toes logs [name]`** — View logs for an app.
+
+```bash
+toes logs my-app # Today's logs
+toes logs my-app -f # Follow (tail) logs in real-time
+toes logs my-app -d 2026-01-15 # Logs from a specific date
+toes logs my-app -s 2d # Logs from the last 2 days
+toes logs my-app -g error # Filter logs by pattern
+toes logs my-app -f -g error # Follow and filter
+```
+
+Duration formats for `--since`: `1h` (hours), `2d` (days), `1w` (weeks), `1m` (months).
+
+### Syncing Code
+
+Toes uses a manifest-based sync protocol. Each file is tracked by SHA-256 hash. The server stores versioned snapshots with timestamps.
+
+**`toes push`** — Push local changes to the server.
+
+```bash
+toes push # Push changes (fails if server changed)
+toes push --force # Overwrite server changes
+```
+
+Creates a new version on the server, uploads changed files, deletes removed files, then activates the new version. The app auto-restarts.
+
+**`toes pull`** — Pull changes from the server.
+
+```bash
+toes pull # Pull changes (fails if you have local changes)
+toes pull --force # Overwrite local changes
+```
+
+**`toes status`** — Show what would be pushed or pulled.
+
+```bash
+toes status
+# Changes to push:
+# * index.tsx
+# + new-file.ts
+# - removed-file.ts
+```
+
+**`toes diff`** — Show a line-by-line diff of changed files.
+
+**`toes sync`** — Watch for changes and sync bidirectionally in real-time. Useful during development when editing on the server.
+
+**`toes clean`** — Remove local files that don't exist on the server.
+
+```bash
+toes clean # Interactive confirmation
+toes clean --force # No confirmation
+toes clean --dry-run # Show what would be removed
+```
+
+**`toes stash`** — Stash local changes (like `git stash`).
+
+```bash
+toes stash # Save local changes
+toes stash pop # Restore stashed changes
+toes stash list # List all stashes
+```
+
+### Environment Variables
+
+**`toes env [name]`** — List environment variables for an app.
+
+```bash
+toes env my-app # List app vars
+toes env -g # List global vars
+```
+
+**`toes env set [name] [value]`** — Set a variable.
+
+```bash
+toes env set my-app API_KEY sk-123 # Set for an app
+toes env set my-app API_KEY=sk-123 # KEY=value format also works
+toes env set -g API_KEY sk-123 # Set globally (shared by all apps)
+```
+
+Setting a variable automatically restarts the app.
+
+**`toes env rm [name] `** — Remove a variable.
+
+```bash
+toes env rm my-app API_KEY # Remove from an app
+toes env rm -g API_KEY # Remove global var
+```
+
+### Versioning
+
+Every push creates a timestamped version. The server keeps the last 5 versions.
+
+**`toes versions [name]`** — List deployed versions.
+
+```bash
+toes versions my-app
+# Versions for my-app:
+#
+# → 20260219-143022 2/19/2026, 2:30:22 PM (current)
+# 20260218-091500 2/18/2026, 9:15:00 AM
+# 20260217-160845 2/17/2026, 4:08:45 PM
+```
+
+**`toes history [name]`** — Show file changes between versions.
+
+**`toes rollback [name]`** — Rollback to a previous version.
+
+```bash
+toes rollback my-app # Interactive version picker
+toes rollback my-app -v 20260218-091500 # Rollback to specific version
+```
+
+### Cron Jobs
+
+Cron commands talk to the cron tool running on your Toes server.
+
+**`toes cron [app]`** — List all cron jobs, or jobs for a specific app.
+
+**`toes cron status `** — Show details for a specific job.
+
+```bash
+toes cron status my-app:backup
+# my-app:backup ok
+#
+# Schedule: day
+# State: idle
+# Last run: 2h ago
+# Duration: 3s
+# Exit code: 0
+# Next run: in 22h
+```
+
+**`toes cron run `** — Trigger a job immediately.
+
+```bash
+toes cron run my-app:backup
+```
+
+**`toes cron log [target]`** — View cron logs.
+
+```bash
+toes cron log # All cron logs
+toes cron log my-app # Cron logs for an app
+toes cron log my-app:backup # Logs for a specific job
+toes cron log -f # Follow logs
+```
+
+### Metrics
+
+**`toes metrics [name]`** — Show CPU, memory, and disk usage.
+
+```bash
+toes metrics # All apps
+toes metrics my-app # Single app
+```
+
+### Sharing
+
+**`toes share [name]`** — Create a public tunnel to share an app over the internet.
+
+```bash
+toes share my-app
+# ↗ Sharing my-app... https://abc123.trycloudflare.com
+```
+
+**`toes unshare [name]`** — Stop sharing an app.
+
+### Configuration
+
+**`toes config`** — Show the current server URL and sync state.
+
+---
+
+## Environment Variables
+
+Toes injects these variables into every app process automatically:
+
+| Variable | Description |
+|----------|-------------|
+| `PORT` | Assigned port (3001-3100). Your app must listen on this port. |
+| `APPS_DIR` | Path to the apps directory on the server. |
+| `DATA_DIR` | Per-app data directory for persistent storage. |
+| `TOES_URL` | Base URL of the Toes server (e.g., `http://toes.local:3000`). |
+| `TOES_DIR` | Path to the Toes config directory. |
+
+You can set custom variables per-app or globally. Global variables are inherited by all apps. Per-app variables override globals.
+
+```bash
+# Set per-app
+toes env set my-app OPENAI_API_KEY sk-123
+
+# Set globally (shared by all apps)
+toes env set -g DATABASE_URL postgres://localhost/mydb
+```
+
+Access them in your app:
+
+```tsx
+const apiKey = process.env.OPENAI_API_KEY
+```
+
+---
+
+## Health Checks
+
+Toes checks `GET /ok` on every app every 30 seconds. Your app must return a 2xx response.
+
+Three consecutive failures trigger an automatic restart with exponential backoff (1s, 2s, 4s, 8s, 16s, 32s). After 5 restart failures, the app is marked as errored and restart is disabled.
+
+The simplest health check:
+
+```tsx
+app.get('/ok', c => c.text('ok'))
+```
+
+---
+
+## App Lifecycle
+
+Apps move through these states:
+
+```
+invalid → stopped → starting → running → stopping → stopped
+ ↓
+ error
+```
+
+- **invalid** — Missing `package.json` or `scripts.toes`. Fix the config and start manually.
+- **stopped** — Not running. Start with `toes start` or the dashboard.
+- **starting** — Process spawned, waiting for `/ok` to return 200. Times out after 30 seconds.
+- **running** — Healthy and serving requests.
+- **stopping** — SIGTERM sent, waiting for process to exit. Escalates to SIGKILL after 10 seconds.
+- **error** — Crashed too many times. Start manually to retry.
+
+On startup, `bun install` runs automatically before the app's `scripts.toes` command.
+
+Apps are accessed via subdomain: `http://my-app.toes.local` or `http://my-app.localhost`. The Toes server proxies requests to the app's assigned port.
+
+---
+
+## Cron Jobs
+
+Place TypeScript files in a `cron/` directory inside your app:
+
+```ts
+// cron/daily-cleanup.ts
+export const schedule = "day"
+
+export default async function() {
+ console.log("Running daily cleanup")
+ // Your job logic here
+}
+```
+
+The cron tool auto-discovers jobs by scanning `cron/*.ts` in all apps. New jobs are picked up within 60 seconds.
+
+### Schedules
+
+| Value | When |
+|-------|------|
+| `1 minute` | Every minute |
+| `5 minutes` | Every 5 minutes |
+| `15 minutes` | Every 15 minutes |
+| `30 minutes` | Every 30 minutes |
+| `hour` | Top of every hour |
+| `noon` | 12:00 daily |
+| `midnight` / `day` | 00:00 daily |
+| `week` / `sunday` | 00:00 Sunday |
+| `monday` - `saturday` | 00:00 on that day |
+
+Jobs inherit the app's working directory and all environment variables.
+
+---
+
+## Data Persistence
+
+Use the filesystem for data storage. The `DATA_DIR` environment variable points to a per-app directory that persists across deployments and restarts:
+
+```tsx
+import { join } from 'path'
+import { readFileSync, writeFileSync, existsSync } from 'fs'
+
+const DATA_DIR = process.env.DATA_DIR!
+
+function loadData(): MyData {
+ const path = join(DATA_DIR, 'data.json')
+ if (!existsSync(path)) return { items: [] }
+ return JSON.parse(readFileSync(path, 'utf-8'))
+}
+
+function saveData(data: MyData) {
+ writeFileSync(join(DATA_DIR, 'data.json'), JSON.stringify(data, null, 2))
+}
+```
+
+`DATA_DIR` is separate from your app's code directory, so pushes and rollbacks won't affect stored data.