diff --git a/docs/PAGEDJS_IMAGE_STALL.md b/docs/PAGEDJS_IMAGE_STALL.md
new file mode 100644
index 0000000..068ddf5
--- /dev/null
+++ b/docs/PAGEDJS_IMAGE_STALL.md
@@ -0,0 +1,89 @@
+# Paged.js stalls on an image at a page boundary — fixed
+
+Findings from the Phase D investigation in `docs/V2_FRONTEND_PROMPT.md` §5, which
+started from `docs/PAGEDJS_RTL_DEBUG_PROMPT.md`. The original hypothesis there —
+`overflow-x: auto` + `dir=rtl` preventing the chunker from converging — is
+**wrong**. The trigger is narrower and has nothing to do with RTL.
+
+**Status: fixed.** `src/pdf/flattenImages.ts` swaps every `` for an
+equivalent `background-image` box before pagination. The `image-stall` fixture
+went from an unbounded hang to 3 pages in under 3 seconds, and the visual suite
+is green at 26/26.
+
+## What happened
+
+Paged.js 0.4.3 laid out two pages and then stopped dead. The page count never
+grew, `PagedConfig.after` never fired, and nothing threw. It was a **stall, not
+a runaway loop** — an important distinction, because a runaway would have been
+capped by a page limit.
+
+The 45 s watchdog in `src/pdf/renderPaged.ts` turned this into a clean error
+rather than a frozen tab. That safety net stays.
+
+## Repro
+
+`tests/visual/fixtures/image-stall.md` — the minimal case, bisected down from the
+`checklists` fixture. Seven sections of ordinary content followed by a paragraph
+containing one 1×1 GIF data-URI image, which has to be placed at a page
+boundary.
+
+```bash
+SCRIPTO_VISUAL=1 npx vitest run tests/visual/layout.test.ts
+```
+
+`layout.test.ts` now carries `paginates an image sitting on a page boundary` as
+an ordinary regression test.
+
+## Ruled out
+
+Each of these was tested against the minimal repro and did **not** fix the stall:
+
+| Hypothesis | Result |
+| --- | --- |
+| RTL / `dir=rtl` | Not involved — the repro is pure LTR |
+| Task lists / checkbox items | Not involved — a plain paragraph stalls identically |
+| `break-inside: avoid` on `img` | Still stalls with `break-inside: auto` |
+| `overflow-x: auto` on `pre` / `.table-wrap` | Not involved; already `overflow: visible` while paginating |
+| Unresolved image box | Still stalls with an explicit `width`/`height` in the page CSS |
+| `display: block` + `margin: auto` | Still stalls as `display: inline; margin: 0` |
+| Image not decoded before layout | Still stalls after awaiting every image |
+| Image is the last node in the document | Still stalls with a paragraph and a whole section after it |
+| Wrapping the `img` in an explicitly sized `div` | Still stalls |
+
+## The cause
+
+Replacing the `` with **any non-replaced element** — a `div` carrying the
+same picture as a `background-image` — paginates cleanly, every time. The
+chunker's difficulty is with the replaced element itself.
+
+## Why the fix was safe to ship this time
+
+The workaround was written up but held back because browsers do not print
+background images unless `print-color-adjust: exact` applies, and Chrome's
+"Background graphics" toggle is **off by default**. Getting that wrong would
+silently drop images from exported PDFs — far worse than the stall.
+
+That objection is now closed. `.pagedjs_page, .pagedjs_page *` carry
+`print-color-adjust: exact`, and the flattened boxes set it inline as well, so
+the picture no longer depends on the dialog. Verified end to end by driving the
+real export over the DevTools Protocol with `printBackground: false`: the
+exported PDF contains the image as a 64×64 XObject and renders it correctly.
+
+## Two ordering bugs found along the way
+
+Both were real, and either alone would have defeated the workaround:
+
+1. **`preloadImages` ran *after* `buildExportContent`**, so the DOM transforms
+ saw images that had not decoded and reported no intrinsic size.
+2. **The renderer marks images `loading="lazy"`.** Anything below the fold never
+ loads, and an export clone is never scrolled — so those images reported no
+ intrinsic size at all. `preloadImages` now forces `loading="eager"` and
+ `decoding="sync"` on the clone before waiting.
+
+## Sizing
+
+`flattenImages` sizes each box from the image's **intrinsic** dimensions, not a
+measured box: it runs against the editor pane, whose width is not the printed
+page's width, so a baked pixel size would be wrong on paper. `width` +
+`max-width: 100%` + `aspect-ratio` reproduces how an `` with `height: auto`
+behaves in flow. An image with no intrinsic size is left as an ``.
diff --git a/docs/QA_VERIFICATION_PROMPT.md b/docs/QA_VERIFICATION_PROMPT.md
new file mode 100644
index 0000000..2831847
--- /dev/null
+++ b/docs/QA_VERIFICATION_PROMPT.md
@@ -0,0 +1,170 @@
+# Prompt — Independently verify the v2 + handwriting work
+
+> Paste into a **fresh** Claude Code session with a browser available. Your job is to
+> **test, not to build.** Do not implement features. Fix only what you can prove is broken,
+> and say so explicitly when you do.
+>
+> Assume nothing in this document is true. Everything below is a *claim made by the
+> engineer who wrote the code*. Your job is to confirm or refute each one.
+
+---
+
+## 0. Ground rules
+
+- Work on branch `dev`. The baseline for every "did this regress?" question is **`bc6eba0`**
+ (the commit before this work started). Create a worktree for it:
+ ```bash
+ git worktree add /tmp/scripto-base bc6eba0
+ ln -sfn "$PWD/node_modules" /tmp/scripto-base/node_modules
+ ```
+- **Report per commit.** For each of the 14 commits below: PASS / FAIL / NOT VERIFIED, with
+ the evidence. "Looks fine" is not evidence — a screenshot, a test name, or a measured
+ number is.
+- When something fails, say whether it also fails on `bc6eba0`. A pre-existing bug is not a
+ regression, and the distinction matters.
+- Do not trust the commit messages. Several make specific measurable claims. Check them.
+
+## 1. Gates that must be green
+
+```bash
+npm install
+npx tsc -b --force && npm run lint && npm run build && npm run test
+SCRIPTO_VISUAL=1 npm run test
+```
+
+Expected, and worth confirming rather than assuming:
+
+| Gate | Expected |
+| --- | --- |
+| `tsc` | clean |
+| `lint` | **0 errors**, 12 warnings (all pre-existing `react-refresh` / unused-directive) |
+| `test` | all pass, 26 skipped |
+| `SCRIPTO_VISUAL=1` | **19 passed · 6 failed · 1 expected-fail** |
+
+⚠️ Those **6 visual failures are claimed to be pre-existing**. Verify that claim by running
+the same suite in `/tmp/scripto-base` after copying `tests/visual/harness.ts` across (the
+baseline's own harness produces no output at all — see §3). If the counts differ, that is a
+regression and a finding.
+
+## 2. The invariant that matters most
+
+`.scripto-doc` is cloned verbatim into the PDF / HTML / Word exports. Everything the Stage
+system added is supposed to live outside it.
+
+- [ ] Export a PDF from a document with tables, code, math, Mermaid, ASCII art and images.
+ Export the same document from `/tmp/scripto-base`. **Diff the layout.** Any difference
+ that is not explained by the deliberate `document.css` changes (commit `e96999e`) is a
+ regression.
+- [ ] Export HTML and Word. Grep the output for `stage-`, `skin-thumb`, `break-layer`,
+ `skin-palette`. **Any hit is a bug.**
+- [ ] With handwriting off, confirm the exported PDF is byte-identical in layout to the
+ baseline, and that the network tab shows **zero** handwriting font requests.
+
+## 3. Claims to check, commit by commit
+
+**`8a3d312` decompose App.tsx** — claims no behaviour change except three deliberate fixes.
+Exercise every ⌘K command, every header menu item, every dialog. Check the AI abort actually
+fires on unmount (DevTools → Network, start an AI action, navigate away).
+
+**`9785a18` Preview Stage system** — claims 21 distinct stages, ≤12 KB gzipped CSS, 0 image
+bytes, and that typing never re-renders `StageBackdrop`.
+- [ ] Screenshot all 35 skins now. Are any two indistinguishable at 240 px?
+- [ ] Measure the CSS: `npx esbuild src/preview/stage/stage.css --minify | gzip -9 | wc -c`.
+- [ ] React Profiler: type 60 characters. Does `StageBackdrop` re-render? It must not.
+- [ ] Rapidly switch skins 20 times. Any stuck backdrop or half-faded label is a failure.
+
+**`e8b4611` page-break editor** — claims insert→remove restores the document byte for byte.
+- [ ] Do it in the UI on a real document. Compare the source before and after, exactly.
+- [ ] Check Keep-together and Landscape actually change pagination.
+- [ ] Confirm undo (⌘Z) reverses a break insertion.
+
+**`3b19365` export presets** — claims a preset never carries document `meta`.
+- [ ] Save a preset from a document with a title and author. Export the `.json`. **Open it
+ and confirm no title or author is in there.**
+- [ ] Import a deliberately malformed `.json`. It must be rejected, not crash the app.
+
+**`7b3dd56` Paged.js stall** — claims the harness was broken on Chrome ≥132 and the stall is
+an upstream defect, unfixed on purpose.
+- [ ] Confirm `SCRIPTO_VISUAL=1` produces **no output at all** on the untouched baseline.
+- [ ] Confirm the `it.fails` tripwire is genuinely failing-as-expected, not silently passing.
+- [ ] Read `docs/PAGEDJS_IMAGE_STALL.md` and spot-check two of its "ruled out" rows yourself.
+
+**`e96999e` skin component language** — this one **deliberately changes exported PDFs**.
+- [ ] Confirm the change is confined to callouts, code blocks, quotes, tables, rules and
+ lists, and that no skin became *less* legible.
+- [ ] Check Terminal and Dark specifically, in light and dark UI. Claim: they used to render
+ pale text on a white sheet in light mode. Verify that is fixed and that preview matches
+ PDF for both.
+
+**`7dd6102` / `51f441a` new skins** — 35 total, each with a stage and a marketing page.
+- [ ] `npm run build` should prerender 118 pages. Open `/skins/invoice` and `/skins/chalkboard`.
+- [ ] Confirm every skin has an Arabic label (switch the UI to العربية and open the picker).
+
+**`42cb1e3` templates** — claims all 54 except Blank declare a skin, and that the rewritten
+ones contain no placeholder text.
+- [ ] Script it: parse `src/data/templates.ts`, assert each has `skin:` in its front-matter.
+- [ ] Open every template in the app. Any that still reads as filler is a finding — note it.
+ **`menu` is known to still be placeholder-heavy.**
+
+**`40c2848` numbering + record validation**
+- [ ] Turn on numbered headings, then paste a document whose headings are `# 1 · Thing`,
+ `# 2 · Other`. There must be no double numbering.
+- [ ] Check `2024 Annual Report` and `3D Rendering` are still auto-numbered (not mistaken for
+ manual numbering).
+- [ ] Put junk in `localStorage['scripto:library:v1']` — `{"docs":[{"content":"x","config":{"meta":null}}]}` —
+ reload. The app must survive.
+
+**`b1a32d0` / `a70fb97` skin palette**
+- [ ] Hover a card: the preview changes but the config must **not** be written. Confirm via
+ `localStorage['scripto:library:v1']`.
+- [ ] Escape and click-away must restore the committed skin.
+- [ ] Keyboard: arrows walk the grid, Enter applies. Check with a screen reader if you can.
+
+**`94e6735` / `51f441a` / `ef49350` handwriting**
+- [ ] **`hand: 'none'` is a total no-op.** No `data-hand` attribute, no `--hw-*` property, no
+ font request. This is the single most important handwriting claim.
+- [ ] Determinism: open Print Preview five times on the same document. Page count and page
+ breaks must be identical every time.
+- [ ] Ruled paper: export a **5-page** ruled document. Check page 1, page 3 and the last page.
+ Every line of body text must sit on a rule. **If page 4 drifts, this is not done.**
+- [ ] Arabic: export with `hand: ruqaa` and with `hand: nastaliq`. Letterforms must be
+ connected, direction right-to-left, numbering in Arabic-Indic digits.
+- [ ] Confirm the exported PDF text is still **selectable and searchable** — nothing rasterised.
+- [ ] Code, maths and Mermaid must stay in their own typefaces, never the hand.
+- [ ] Drag the neatness slider on a 20-page document. It must be instant, with no
+ re-pagination.
+- [ ] Offline (DevTools): a previously used hand still works; a new one fails with an honest
+ message and **does not apply**.
+- [ ] 20k-word document: confirm it degrades to no variation rather than freezing.
+
+## 4. Cross-cutting checks
+
+- [ ] العربية end to end: RTL chrome, RTL document, code and maths still LTR, page numbers
+ `1 / N` LTR. **Look for any English string left in the Arabic UI** — two were found this
+ way already (the paginator's progress messages, and the error boundary).
+- [ ] `prefers-reduced-motion: reduce` — no movement anywhere, textures still present.
+- [ ] 375 px width — everything usable; the palette and rail must not trap the layout.
+- [ ] Passphrase lock → reload → unlock: documents, presets and hand settings all intact.
+- [ ] Offline (PWA): edit and export still work.
+- [ ] Lighthouse on `/` ≥ 95 Perf/SEO/A11y, and confirm marketing pages still ship **zero**
+ framework JS.
+- [ ] Bundle: report the CSS gzip delta against `bc6eba0`. It has grown; confirm by how much
+ and whether that is justified.
+
+## 5. Tooling you may want
+
+There is no committed screenshot harness. If you want one, drive Chrome over the DevTools
+Protocol — **not** `--dump-dom` with `--virtual-time-budget`, which silently does nothing on
+Chrome ≥132 (that was the harness bug fixed in `7b3dd56`). `tests/visual/harness.ts` has a
+working minimal CDP client to copy.
+
+Useful: seed `localStorage` from the marketing origin (`/`) rather than `/app` — the editor
+flushes its own library on `pagehide` and will overwrite your seed on the way out.
+
+## 6. Report back
+
+For each commit: verdict, evidence, and any finding with a minimal reproduction. Then:
+
+1. Anything that regressed against `bc6eba0`.
+2. Anything the commit messages claim that you could not confirm.
+3. The three findings you would fix first, in order, with your reasoning.
diff --git a/docs/V2_FRONTEND_PROMPT.md b/docs/V2_FRONTEND_PROMPT.md
new file mode 100644
index 0000000..684a61b
--- /dev/null
+++ b/docs/V2_FRONTEND_PROMPT.md
@@ -0,0 +1,489 @@
+# Scripto v2.0 — Frontend Master Brief
+
+> Hand this file to a fresh Claude Code session. It is **self-contained**: it explains the codebase,
+> the invariants, and specifies every workstream for v2.0.
+>
+> **Hard constraint: NO BACKEND.** No server, no accounts, no sync, no share links, no API.
+> Everything here runs in the browser. Anything requiring a server is explicitly out of scope
+> and listed in §12.
+>
+> **The headline work is §2 — the Preview Stage system.** Everything else is supporting.
+
+---
+
+## 0. Orientation — read this first
+
+**What Scripto is.** A 100% client-side Markdown → PDF studio at `md.atom.sa`. Vite 5 + React 18 +
+TypeScript (strict) + Tailwind 3. Markdown renders through react-markdown (remark/rehype); PDF is
+produced by **Paged.js + the browser's native print-to-PDF**. There is a multi-document library in
+`localStorage`, optional AES-256 passphrase lock, PWA offline, 21 document skins, 58 templates,
+22 presets, full EN + AR with real RTL, and a prerendered zero-JS marketing site (~102 pages).
+
+**Run / verify (all three must stay green):**
+```bash
+npm install
+npm run dev # http://localhost:5173/app
+npx tsc -b --force && npm run lint && npm run build
+npm run test # 4 suites; visual harness is opt-in
+SCRIPTO_VISUAL=1 npm run test # opt-in Paged.js layout assertions
+```
+
+**Read before writing code:** `ARCHITECTURE.md` (§5 rendering pipeline, §6 PDF engine, §17 RTL),
+`docs/RENDERING_AUDIT.md` (why the layout code looks the way it does), `docs/P1_FEATURES.md` §0
+(the dialog/command wiring recipe).
+
+### Conventions — non-negotiable
+- **TypeScript strict, no `any`.** Use `unknown` + narrowing. Explicit types on exported APIs.
+- **Immutable updates** everywhere (spread; never mutate).
+- **No `console.log`** — use `logger` from `src/lib/logger.ts` and `getErrorMessage(err)`.
+- **Reuse `src/components/ui/*`** — `Button`, `Dialog`, `Field` (`TextInput`/`Select`/`Switch`/
+ `Slider`/`Segmented`), `Menu`, `Tooltip`, `Confirm` (`useConfirm()`), toasts via `sonner`.
+- **i18n is mandatory.** Every user-visible string goes in `src/lib/i18n.ts` (`EN_STRINGS` **and**
+ `STRINGS.ar`) and is read via `useLanguage().t`. A green `tsc` proves no key is missing.
+- **Logical CSS only** — `ms-/me-`, `ps-/pe-`, `start-/end-`, `text-start`, `rtl:`. Never
+ `ml-/mr-/left-/right-/text-left` in new code. Everything must be correct in LTR **and** RTL.
+- **Light + dark** both correct. Design tokens live as CSS vars in `src/index.css`.
+- **`prefers-reduced-motion` respected.** The app wraps in ``;
+ reused presets live in `src/lib/motion.ts`.
+- Path alias `@/` → `src/`.
+
+### 🚨 The five invariants you must not break
+
+1. **`preview === PDF`.** The live preview element and the exported PDF are produced from the *same*
+ rendered DOM and the *same* stylesheet (`src/styles/document.css`). This is the product's moat.
+2. **`.scripto-doc` (the `docRef` element in `Preview.tsx`) is cloned verbatim by the export path.**
+ `src/pdf/buildExportContent.ts` and `src/io/exporters.ts` both `cloneNode(true)` it. **Anything you
+ put inside it ships into the PDF, the HTML export, and the Word export.** Screen-only decoration
+ must live *outside* it. This is the single most important rule in this document.
+3. **`PreviewHandle.getDocElement()` must keep returning that element.** `PrintPreview.tsx`,
+ `exporters.ts` and `useScrollSync.ts` all depend on it. Preserve the contract exactly.
+4. **CodeMirror props must be identity-stable.** `useCodeMirror` reconfigures the editor when
+ `extensions` / `onChange` / `basicSetup` change by reference, and reconfiguring destroys
+ runtime-attached extensions (most visibly the ⌘F search panel). `MarkdownEditor` reads callbacks
+ through a ref — keep that pattern.
+5. **Print isolation.** `src/styles/print.css` hides everything except `.scripto-print-portal` when
+ `body.scripto-printing` is set. Any new chrome must be hidden by that rule or live inside the
+ portal. Verify by actually exporting a PDF.
+
+### Key files
+| File | Purpose |
+| --- | --- |
+| `src/App.tsx` | Orchestrator (1,150 lines): state, dialogs, `commands` array, shortcuts, deep links |
+| `src/components/preview/Preview.tsx` | **The live preview — the main subject of §2** |
+| `src/components/preview/PrintPreview.tsx` | Paged.js modal + Save-as-PDF (subject of §3) |
+| `src/pdf/renderPaged.ts` | Runs Paged.js; image preload, 45s watchdog, `fitToPage` |
+| `src/pdf/buildExportContent.ts` | Clones the live doc, namespaces ids, prepends cover + TOC |
+| `src/pdf/pageStyles.ts` | Generates the `@page` CSS |
+| `src/styles/document.css` | **Shared** preview + PDF stylesheet; skins live here (49 KB) |
+| `src/styles/print.css` | Print isolation + on-screen page chrome |
+| `src/data/skins.ts` | `SKIN_OPTIONS` (21) + `SKIN_VALUES` |
+| `src/data/presets.ts` | `DOCUMENT_PRESETS` (22) |
+| `src/data/templates.ts` | `TEMPLATES` (58) |
+| `src/types/index.ts` | `PdfConfig`, `DocumentSkin`, `DocumentRecord`, `ExportProgress` |
+| `src/lib/constants.ts` | `DEFAULT_CONFIG`, `PAPER_SIZES`, `MARGIN_PRESETS`, `FONT_STACKS` |
+| `src/lib/motion.ts` | Shared easing/duration/variant presets |
+| `src/lib/analytics.ts` | `trackEvent` (Vercel Analytics) |
+| `src/hooks/useDocumentLibrary.ts` | The localStorage document library |
+
+---
+
+## 1. The central idea: **the Stage and the Sheet**
+
+Today the preview pane is one white rounded card on a flat grey ground. It is honest and readable —
+and completely undesigned. Every skin looks like the same product.
+
+The fix is a hard architectural split:
+
+```
+┌─────────────────────────────────── THE STAGE ────────────────────────────────────┐
+│ Screen-only. Never cloned. Never printed. Never exported. │
+│ Ground, texture, lighting, depth, rulers, chrome, motion, transitions. │
+│ This is where 100% of the visual drama lives — with ZERO risk to the PDF. │
+│ │
+│ ┌───────────────────── THE SHEET (.scripto-doc) ─────────────────┐ │
+│ │ SACRED. Cloned verbatim into PDF / HTML / Word. │ │
+│ │ Governed by document.css. Typography only. No animation. │ │
+│ │ Changing anything here changes the exported document. │ │
+│ └─────────────────────────────────────────────────────────────────┘ │
+└───────────────────────────────────────────────────────────────────────────────────┘
+```
+
+**Why this matters:** it lets you build something that looks like Framer built it, while the export
+path stays byte-for-byte as trustworthy as it is today. Every animation, every texture, every glow
+lives on the Stage. The Sheet only ever changes through real typography work in `document.css`.
+
+**The designer's rule:** the Stage must never outshine the Sheet. The document is the hero; the
+Stage is the room it sits in. If a reviewer's eye goes to the background first, the Stage is wrong.
+
+---
+
+## 2. PHASE A — The Preview Stage system ⭐ *the headline work*
+
+**Goal:** 21 skins → 21 genuinely distinct **stages**, each with its own ground, paper treatment,
+lighting and motion signature, built as one coherent design system rather than 21 one-offs.
+Switching skins should feel like walking into a different room.
+
+### A1 · Module layout (build this separately — do not bolt it into `Preview.tsx`)
+
+Create a new, self-contained module. `src/components/preview/Preview.tsx` becomes a **thin adapter**
+that renders `PreviewSurface` and forwards the `PreviewHandle` ref, so `App.tsx` barely changes.
+
+```
+src/preview/
+├── PreviewSurface.tsx # composes Stage + Sheet; owns docRef; keeps the PreviewHandle contract
+├── stage/
+│ ├── stages.ts # the 21 stage descriptors — DATA, not components
+│ ├── StageBackdrop.tsx # renders ground/texture/lighting for the active stage
+│ ├── PaperFrame.tsx # renders the paper treatment around (never inside) the Sheet
+│ ├── stage.css # ALL stage styling, keyed by [data-stage='x']
+│ └── types.ts # StageDescriptor, StageMotion, StageTexture
+├── motion/
+│ ├── vocabulary.ts # the 10 shared motion primitives (see A3)
+│ └── useStageTransition.ts # orchestrates the skin→skin transition
+├── chrome/
+│ ├── PreviewToolbar.tsx # mode switch, zoom, sync, front-matter toggle
+│ ├── SkinRail.tsx # hover-to-preview skin rail (A6)
+│ ├── PageRuler.tsx # page-boundary overlay (A7)
+│ └── PreviewEmpty.tsx # designed empty state
+└── index.ts
+```
+
+**Rules for this module:**
+- `stage.css` is imported once by `PreviewSurface`. It must contain **no `.scripto-doc` selectors**
+ other than read-only positioning of the wrapper — never typography, never colour of document text.
+- Stages are **data** (`StageDescriptor[]`), not 21 bespoke components. A stage that needs a
+ component gets one small named component; the other 20 must fall out of the descriptor + CSS.
+- Every stage descriptor is keyed by `DocumentSkin`, so `tsc` proves all 21 exist
+ (`Record` — exhaustive by construction).
+
+### A2 · Design foundation (do this before any stage)
+
+Add a small token layer in `stage.css` so the 21 stages are one system:
+
+- **Depth scale** — `--stage-lift-1/2/3`: layered multi-stop shadows (never a single flat shadow).
+- **Ground scale** — `--stage-ground`, `--stage-ground-2`, `--stage-vignette`.
+- **Edge scale** — `--stage-edge` (paper border), `--stage-edge-glow`.
+- **Motion scale** — durations `--m-snap: 90ms`, `--m-quick: 160ms`, `--m-base: 280ms`,
+ `--m-slow: 480ms`, `--m-drift: 700ms`; easings `--e-out` (reuse `EASE_OUT` from `lib/motion.ts`),
+ `--e-spring`, `--e-linear`, `--e-steps`.
+- **Texture primitives, CSS-only** — no image assets. Grids, scanlines, halftone dots, paper grain
+ and blueprint rules must all be built from `repeating-linear-gradient` / `radial-gradient` /
+ `conic-gradient`. **Bundle budget: the entire stage system adds ≤ 12 KB gzipped CSS and 0 image bytes.**
+
+### A3 · The shared motion vocabulary (10 primitives)
+
+Every stage composes its entrance from these — parameterised, not reinvented. Implement in
+`motion/vocabulary.ts` as named variant factories.
+
+| Primitive | Motion | Feels like |
+| --- | --- | --- |
+| `rise` | opacity 0→1, translateY 12→0 | calm, default |
+| `wipe` | a masked bar sweeps along the inline axis, content resolves behind | editorial |
+| `draw` | borders/rules scale from 0 length, then content fades | technical |
+| `type` | children reveal sequentially with micro x-jitter | mechanical |
+| `snap` | 90 ms, no easing curve, no overshoot | efficient |
+| `spring` | scale 0.96→1 with bounce 0.22 | friendly |
+| `glow` | a light travels the paper perimeter, then content fades | nocturnal |
+| `cascade` | children stagger top→bottom, tight 24 ms steps | tabular |
+| `bloom` | vignette/spotlight widens while content fades up | cinematic |
+| `stamp` | scale 1.04→1 with a hard short settle | institutional |
+
+### A4 · The 21 stages
+
+One per skin in `src/data/skins.ts`. Ground/paper are **screen-only**; the motion column names the
+primitive from A3.
+
+| # | Skin | Stage | Ground | Paper treatment | Motion |
+| --- | --- | --- | --- | --- | --- |
+| 1 | `modern` | Studio | soft neutral vertical gradient | floating sheet, layered `lift-2`, gentle scroll parallax | `rise` |
+| 2 | `classic` | Library | warm cream, faint grain | bound-page look: spine gradient on the inner edge | `wipe` (page-turn, rotateY from spine) |
+| 3 | `editorial` | Spread | accent colour-field, oversized muted heading watermark behind | offset sheet, magazine gutter | `wipe` (masthead bar) |
+| 4 | `technical` | Spec bench | cool slate + faint isometric grid | corner registration ticks, thin rulers on both edges | `draw` |
+| 5 | `compact` | Contact sheet | tight dense ground | snug frame, minimal margin | `snap` |
+| 6 | `manuscript` | Typewriter desk | warm paper-bag, fibre texture | platen shadow along the top edge | `type` |
+| 7 | `blueprint` | Drafting table | deep cyan blueprint grid | white-on-blue sheet with a title-block corner | `draw` (plotter trace) |
+| 8 | `corporate` | Boardroom | clean cool ground, accent band at top of stage | crisp square shadow | `rise` + band fills from start edge |
+| 9 | `brutalist` | Concrete | flat raw ground | hard offset shadow, zero blur, thick border | `snap` with `--e-steps` (deliberately jarring) |
+| 10 | `notebook` | Desk | soft warm ground | punch-hole/spiral edge, red margin rule, faint ruled lines *behind* the sheet | `spring` (flip-down, rotateX from top) |
+| 11 | `resume` | Folder | very clean neutral, generous air | manila folder tab peeking behind the sheet | `rise` (lift out, shadow deepens) |
+| 12 | `swiss` | Grid wall | visible modular grid with interval markers | snapped to grid, hairline edge | `cascade` along grid lines, no bounce |
+| 13 | `terminal` | CRT | near-black, phosphor vignette, scanlines, very faint flicker | dark panel with a glowing edge | `type` (cursor blink → boot sweep) |
+| 14 | `newsprint` | Press | warm grey halftone dots | off-white sheet, CMYK registration marks at corners | `bloom` (ink-set: blur+desaturate → sharp) |
+| 15 | `elegant` | Gallery | deep ink ground, soft centred spotlight | hairline accent frame | `bloom` |
+| 16 | `playful` | Sticker board | bright pastel blobs | rounded corners, chunky coloured shadow | `spring` (rotation settle) |
+| 17 | `dark` | Night desk | true dark, ambient accent glow | luminous border | `glow` |
+| 18 | `ledger` | Accounting desk | muted ledger-green | fine ruled columns visible in the gutter | `cascade` |
+| 19 | `zen` | Void | almost nothing, very soft light | edge barely exists, no frame | `rise` at `--m-drift`, **no translate** — pure fade |
+| 20 | `memo` | Interoffice | flat institutional ground, ghost "INTERNAL" mark **on the stage, not the paper** | header band | `stamp` |
+| 21 | `poster` | Gallery wall | dramatic ground, spotlight cone | large centred sheet, strong drop shadow | `bloom` + scale 0.92→1 |
+
+**Acceptance for A4:** screenshot all 21 at 240 px wide. A person who has never seen Scripto must be
+able to tell all 21 apart at that size. If two are confusable, one of them is not finished.
+
+### A5 · Skin-change transition
+
+Changing a skin must not be an instant repaint. On `config.skin` change:
+1. Cross-fade the old Stage backdrop out and the new one in (`--m-base`).
+2. Play the new stage's motion signature on the Sheet **wrapper** (never on `.scripto-doc` itself —
+ no transforms on the exported element; animate the parent).
+3. Show a brief, tasteful stage-name label (e.g. "Blueprint") that fades after ~900 ms.
+
+Must be interruptible: switching skins rapidly must never leave a stuck backdrop or a half-faded
+label. Under `prefers-reduced-motion`, do the cross-fade only — no movement.
+
+### A6 · Skin Rail — hover to preview, click to commit
+
+A slim vertical rail pinned to the inline-end edge of the preview (collapsible, remembered in
+`localStorage` as `scripto:preview-rail`).
+
+- 21 small live thumbnails driven by `SKIN_OPTIONS`.
+- **Hover** temporarily applies the skin to the live preview (do **not** write to `config`) —
+ release restores the committed skin. This is the Figma/Framer move that makes the whole feature
+ feel alive.
+- **Click** commits via the existing `updateConfig({ skin })`.
+- Keyboard accessible: the rail is a `role="listbox"`, arrow keys move a roving tabindex, focus
+ previews, Enter commits, Escape restores.
+- Hidden below `lg` (mobile gets the existing Theme Gallery instead).
+- **Performance:** thumbnails must be static CSS representations, not 21 mounted `MarkdownRenderer`s.
+ Reuse the approach in `marketing/content/skinStyles.ts` if it fits.
+
+### A7 · Preview modes + Page Ruler
+
+Add a segmented control to `PreviewToolbar` (persisted as `scripto:preview-mode`):
+
+- **Flow** (default, today's behaviour) — one continuous sheet.
+- **Pages** — the **Page Ruler** overlay: measure the rendered document height against the usable
+ page height derived from `resolvePageDimensions(config)` minus `config.margins`, then draw
+ screen-only page separators with page numbers in the gutter.
+ - ⚠️ **Be honest about accuracy.** This is a *geometric estimate*; it does not run the paginator
+ and will not account for `break-inside: avoid`, `::page-break`, `:::keep-together`, table-header
+ repetition or `chunkCodeBlocks`. Label it as approximate in the UI (i18n string) with a
+ "Open Print Preview for exact pages" affordance. **Do not claim exactness.**
+ - The overlay is absolutely positioned in the Stage layer. It must not touch `.scripto-doc`.
+- **Focus** — dims everything except the block under the cursor (reuse the `rehypeSourceLine`
+ data attributes that `useScrollSync` already relies on).
+
+### A8 · Contracts this phase must honour
+
+- **Typing latency must not regress.** The Stage must not re-render on keystroke — memoise it on
+ `config.skin` / `config.accentColor` only. Verify with a React profile: typing 60 chars must not
+ re-render `StageBackdrop` once.
+- **GPU-composited only** — animate `transform` and `opacity`. No animated `box-shadow`, `filter` on
+ large surfaces, `width`/`height`, or `background-position` on the whole ground.
+- The existing **`zoom`** on the preview inner wrapper and the **pinch-to-zoom** touch handlers must
+ keep working. If a stage transform fights `zoom`, put the stage transform on a different element.
+- **Reduced motion:** all stage entrances collapse to a plain 120 ms opacity fade. Textures stay;
+ motion goes.
+- **RTL:** every stage must be correct with `dir="rtl"`. Asymmetric stages (2 spine, 10 margin rule,
+ 11 folder tab, 18 gutter) must mirror. Use logical properties; add `[dir='rtl']` tweaks only where
+ logical props genuinely cannot express it.
+- **Escape hatch:** a setting `scripto:preview-stage` = `full | minimal | off`. `off` restores
+ today's plain card exactly. Zen/focus mode forces `minimal`.
+- **i18n:** stage names, mode labels, rail labels, ruler labels — EN + AR.
+
+### A9 · Acceptance for Phase A
+- [ ] 21 stages, all visually distinct at 240 px.
+- [ ] Skin change plays a designed transition; rapid switching never breaks.
+- [ ] Skin Rail previews on hover, commits on click, fully keyboard-navigable.
+- [ ] Flow / Pages / Focus modes work and persist.
+- [ ] **Export a PDF and diff it against `main`: byte-level layout must be unchanged.** Nothing from
+ the Stage appears in the PDF, the HTML export, or the Word export.
+- [ ] `SCRIPTO_VISUAL=1 npm run test` still passes.
+- [ ] Correct in LTR + RTL, light + dark, reduced-motion, and at 375 px width.
+- [ ] `npx tsc -b --force && npm run lint && npm run build && npm run test` green.
+
+---
+
+## 3. PHASE B — Visual page-break editor ⭐ *the launch feature*
+
+**Why this is the moat:** every markdown→PDF tool's #1 complaint is "the page break landed in the
+wrong place and I can't fix it." Pandoc means editing LaTeX. Typora means guessing. And Scripto
+already has the write target: `src/markdown/plugins/remarkPageDirectives.ts` implements
+`::page-break`, `:::keep-together` and `:::landscape` **today**. This phase is a UI layer over a
+working directive system — not a new pipeline.
+
+**In `PrintPreview.tsx`, over the paginated `.pagedjs_page` boxes:**
+
+1. On hover between two block elements, show a thin insertion affordance: **"✂ Break here"**.
+2. Clicking it inserts `\n\n::page-break\n\n` into the markdown at the right source position, then
+ re-renders. **Source mapping:** `rehypeSourceLine.ts` already stamps source line numbers onto
+ rendered elements and `useScrollSync` already consumes them — reuse that, and account for
+ `parsed.bodyLineOffset` (front-matter offset) exactly as `useScrollSync` does.
+3. Existing `::page-break` markers render as a removable chip on the page edge (click ✕ → delete
+ those lines from the source).
+4. Select a run of blocks → "Keep together" wraps them in `:::keep-together … :::`.
+5. A wide table shows an inline suggestion: "Too wide for portrait — make this page landscape?" →
+ wraps in `:::landscape`.
+6. Surface the `fit` result that `renderPagedPreview` already returns (`{ scaled, clipped }`) as a
+ visible warning strip: "3 elements were scaled to fit; 1 is still clipped."
+
+**Rules:** every mutation goes through the normal `setMarkdown` path so undo works. Never write into
+the rendered DOM as the source of truth — the markdown is always authoritative. Round-trip test:
+insert a break, remove it, and the document must be byte-identical to the original.
+
+---
+
+## 4. PHASE C — Export presets
+
+The last feature the repo explicitly names as unstarted (`docs/RENDERING_AUDIT.md` §8: *"Named
+bundles of paper size, margins, skin and font — not started"*).
+
+- New type `ExportPreset { id, name, config: Partial, createdAt }`, persisted at
+ `scripto:export-presets`.
+- "Save current settings as preset…" in `ConfigPanel` + a ⌘K command; rename and delete.
+- User presets appear alongside `DOCUMENT_PRESETS` in the Theme Gallery, visually separated.
+- Export/import presets as a `.json` file (frontend-only portability — this is the closest thing to
+ "sync" available without a backend, and it is genuinely useful for teams sharing a house style).
+- Applying a preset must merge exactly like `applyPreset` in `App.tsx` does today (note the
+ `marginPreset` → `margins` mapping).
+
+---
+
+## 5. PHASE D — Root-cause the Paged.js RTL pagination hang
+
+**This is a real, shipped bug on the product's key differentiator.** Some Arabic RTL documents stall
+at "Laying out pages"; a 45 s watchdog makes it fail gracefully but they still cannot export.
+
+Full repro and investigation plan already exist in **`docs/PAGEDJS_RTL_DEBUG_PROMPT.md`** — follow it.
+Leading hypothesis stated there: `overflow-x: auto` on `pre` / `.table-wrap` combined with `dir=rtl`
+prevents the chunker from converging (the same class of defect as the scroll-container/fragmentation
+bug documented in `RENDERING_AUDIT.md` §2).
+
+Keep the watchdog and the image timeout as the safety net regardless of the fix. Add an RTL fixture
+to `tests/visual/fixtures/` that reproduces it, so the harness covers it.
+
+---
+
+## 6. PHASE E — Mobile
+
+`App.tsx:216` gates split view on `isDesktop` (`min-width: 1024px`) and `App.tsx:267` only
+auto-opens the config panel on desktop. Meanwhile résumé and AI-output traffic is heavily mobile.
+
+- Tabbed Editor ⇄ Preview with a proper bottom switcher (not a degraded desktop layout).
+- Config panel becomes a bottom sheet on small screens.
+- Verify the Save-as-PDF path end-to-end on **iOS Safari** and **Android Chrome** — this is the
+ riskiest part of the whole product on mobile and it is currently unverified. Document what works.
+- Touch targets ≥ 44 px; the editor toolbar must be reachable one-handed.
+- Stage system: `minimal` on small screens by default (perf).
+
+---
+
+## 7. PHASE F — IndexedDB migration
+
+`useDocumentLibrary.ts` `JSON.stringify`s the entire library — including base64 images — into
+`localStorage` on a 300 ms debounce. Quota exhaustion is handled gracefully
+(`scripto:quota-exceeded`) but the ceiling is low and real.
+
+- Move documents and image blobs to IndexedDB (or OPFS), keeping `localStorage` for small settings.
+- **Migrate on first run, non-destructively**: copy from `scripto:library:v1`, verify, and only then
+ stop writing the old key. Keep the old key readable for one release.
+- Preserve the debounced-write / `pagehide` flush behaviour — it is the reason typing is smooth.
+- **The passphrase vault must keep working.** `lib/vault.ts` snapshots `scripto:*` localStorage
+ entries; it needs to learn about the new store, and `AppRoot`'s lock gate must still never mount
+ `App` with plaintext readable. Re-verify the full lock → reload → unlock cycle.
+- Add local **version snapshots** (last 20 per document) now that space allows — the single cheapest
+ trust feature for a local-first app.
+
+---
+
+## 8. PHASE G — Decompose `App.tsx`
+
+1,150 lines, ~20 dialog `useState`s, a 40-entry `commands` array. Adding the dialogs above will make
+it unworkable. Extract, with no behaviour change:
+
+- `hooks/useDialogs.ts` — one reducer for all overlay state.
+- `hooks/useCommands.ts` — builds the `commands` array from injected handlers.
+- `hooks/useDeepLinks.ts` — the `?template=` / `?skin=` allowlist effect.
+- `hooks/useAiActions.ts` — the AI selection/transform handlers.
+
+Do this **before** Phases B and C if you are running them in the same session; otherwise after A.
+
+---
+
+## 9. PHASE H — Funnel analytics
+
+`lib/analytics.ts` tracks actions but no funnel, so there is no way to tell where users are lost.
+Add to `AppAnalyticsEvent` (keep props flat, structural only — **never document content or PII**):
+
+`Time To First Export` · `Export Dialog Opened` · `Print Dialog Reached` · `Paste Detected`
+(with `{ kind: 'rich' | 'plain' | 'image' }`) · `Page Break Inserted` · `Stage Viewed`
+(`{ skin }`) · `Skin Previewed` (rail hover→commit) · `Mobile Export Attempted`.
+
+Define and write down the targets: **activation** = first export in session 1 (>35%);
+**north star** = weekly returning exporters.
+
+---
+
+## 10. PHASE I — Content sprint (no code, pure leverage)
+
+The marketing infrastructure auto-generates a page, sitemap entry, hreflang cluster, JSON-LD and OG
+card for every template, skin, use-case and blog post. It is built for hundreds and currently holds
+**4 blog posts, 3 comparisons, 9 use-cases**. Feeding it is the cheapest growth available.
+
+1. **The 5 unbuilt templates** named in `RENDERING_AUDIT.md` §8 — Arabic/bilingual invoice, incident
+ postmortem, ADR, statement of work, investor update. (These are also the business/team wedge.)
+2. **A `/ai-output-to-pdf` use-case page.** Paste-to-Markdown already ships
+ (`MarkdownEditor.tsx:228`) — the feature exists and nobody knows. Target *"chatgpt to pdf"*,
+ *"claude output to pdf"*, *"ai answer formatting"*. Add the Arabic variant to `USE_CASES_AR`.
+3. **10 blog posts** against the keyword map in `docs/SEO_PLAYBOOK.md`.
+4. **More comparisons:** VS Code Markdown-PDF, Dillinger, StackEdit, Obsidian, md-to-pdf, Overleaf.
+5. Update the landing hero: it says *"Markdown in."* It should also say **"paste anything."**
+
+Follow the recipes in `ARCHITECTURE.md` §14 — do not hand-write routes.
+
+---
+
+## 11. Global verification
+
+Before declaring any phase done:
+
+```bash
+npx tsc -b --force && npm run lint && npm run build && npm run test
+SCRIPTO_VISUAL=1 npm run test
+```
+
+Then **manually**:
+- [ ] Export a PDF from a document with tables, code, math, Mermaid, ASCII and images — compare
+ against a PDF exported from `main`. **Any layout difference is a regression unless intended.**
+- [ ] Export HTML and Word — confirm no stage markup leaked in.
+- [ ] Switch to العربية: full RTL chrome, RTL document, code/math still LTR, page numbers still
+ `1 / N` LTR.
+- [ ] Enable the passphrase lock, reload, unlock — data intact.
+- [ ] Go offline (DevTools) — the PWA still edits and exports.
+- [ ] `prefers-reduced-motion: reduce` — no movement anywhere.
+- [ ] 375 px width — everything usable.
+- [ ] Lighthouse on `/` ≥ 95 Perf/SEO/A11y (marketing pages must still ship zero framework JS).
+
+---
+
+## 12. Explicitly OUT of scope (needs a backend — do not build)
+
+Accounts · cloud sync · share links · comments/collaboration · server-side rendering (and therefore
+PDF bookmarks, PDF/A, tagged PDF, password-protected PDF, batch/merge) · REST API · CLI · GitHub
+Action · e-signature.
+
+If you find yourself wanting a server, stop and note it in the final report instead.
+
+---
+
+## 13. Suggested execution order
+
+| Session | Contains | Why |
+| --- | --- | --- |
+| 1 | §8 Phase G (decompose `App.tsx`) | Cheap, unblocks everything, zero behaviour change |
+| 2 | §2 Phase A — **the Stage system** | The headline. Big enough to own a whole session. |
+| 3 | §3 Phase B + §4 Phase C | Both live in the preview/export surface |
+| 4 | §5 Phase D (RTL hang) + §6 Phase E (mobile) | Both are "fix what's shipped" |
+| 5 | §7 Phase F (IndexedDB) + §9 Phase H (analytics) | Both touch persistence/instrumentation |
+| — | §10 Phase I (content) | Runs continuously alongside all of the above |
+
+Ship each phase independently. Do not batch — this repo's strength is that every feature landed
+complete, documented, and green.
+
+---
+
+## 14. Final report
+
+End with: what shipped per phase, before/after screenshots of all 21 stages, the PDF regression
+diff result, bundle-size delta, anything deferred and why, and any invariant you had to bend
+(with justification).
diff --git a/docs/V3_HANDWRITING_PROMPT.md b/docs/V3_HANDWRITING_PROMPT.md
new file mode 100644
index 0000000..3facbd5
--- /dev/null
+++ b/docs/V3_HANDWRITING_PROMPT.md
@@ -0,0 +1,635 @@
+# Scripto — The Handwriting Engine
+
+> **Follow-up to `docs/V2_FRONTEND_PROMPT.md`.** That brief is largely delivered: the Stage system,
+> the visual page-break editor, export presets, the `App.tsx` decomposition, the Paged.js stall
+> root-cause, and 8 new skins have all shipped. This document is the next body of work.
+>
+> **Still frontend-only. No backend, no network calls beyond font CDN.**
+>
+> This is the most design-sensitive feature Scripto has attempted. Read §1 and §3 before writing
+> a line of code — the naive implementation (pick a handwriting font, ship it) looks fake, and the
+> difference between fake and convincing is entirely in the details specified here.
+
+---
+
+## 0. Where the codebase is now
+
+Re-orient before starting; several things named in the v2 brief have changed.
+
+- **29 skins**, not 21 — `src/data/skins.ts` now carries a `SkinGroup` taxonomy
+ (`essentials | business | technical | editorial | academic | expressive`) with `SKIN_GROUPS`.
+- **The Stage system exists.** `src/preview/stage/stages.ts` is
+ `Record` — **adding a skin without a stage is a compile error.**
+ Stage vocabulary: `StageMotion` (10 primitives), `StageFrame` (12 decorations), `StageMark`.
+ All screen-only, keyed `[data-stage='']` in `src/preview/stage/stage.css`.
+- **`PdfConfig.hand` does not exist yet** — you are adding it.
+- `DocumentFont` is still a 5-value union (`serif | sans | lora | system | arabic`).
+- **Fonts load from one Google Fonts URL**, duplicated in **two** places that must stay in sync:
+ `index.html` (preload → stylesheet swap) and `FONTS_HREF` in `src/io/exporters.ts`.
+- The five invariants from `V2_FRONTEND_PROMPT.md` §0 still hold. **Re-read them.** The most
+ important, restated: **`.scripto-doc` is cloned verbatim into the PDF/HTML/Word exports.**
+
+---
+
+## 1. The thesis: why handwriting fonts look fake, and what fixes it
+
+Drop Caveat on a document and it reads as *a font*, not as writing. Six things give it away, and
+each has a specific fix. **This table is the feature.**
+
+| Tell | Why it reads fake | The fix |
+| --- | --- | --- |
+| Every `a` is identical | Real hands never repeat a glyph exactly | OpenType `calt` + stylistic sets; per-word variation palette (§3) |
+| Perfect baseline | Real lines drift and wobble | Per-word `translateY` jitter, ±0.6 px |
+| Uniform slant | Real slant varies word to word | Per-word `rotate` jitter, scaled by *neatness* |
+| Uniform stroke weight | Real pens vary with pressure | Per-word `font-variation-settings: 'wght'` on variable hands (§3.4) |
+| Text floats above the paper | Real writing sits **on** the rule | Rule-pitch locking (§4.2) — the hardest part of this feature |
+| Rules, bullets and boxes are machine-straight | Nothing drawn by hand is straight | Hand-drawn element layer (§5) |
+
+**The single most important sentence in this document:** all variation must be **deterministic** —
+a pure function of `(seed, wordIndex)`, never `Math.random()`.
+
+Three independent reasons:
+1. Paged.js re-renders on every pagination. Fresh randomness ⇒ different word widths ⇒ **different
+ page breaks every time you open Print Preview.**
+2. `preview === PDF` would break — the preview and the export would jitter differently.
+3. The preview would visibly shimmer on every keystroke re-render.
+
+Use a small seeded PRNG (`mulberry32`) in `src/lib/handwriting/random.ts`. Seed from a stable hash of
+the document id. **Add a unit test asserting that the same input produces byte-identical output
+across two independent runs.**
+
+---
+
+## 2. The data model — a new orthogonal axis
+
+Handwriting is **not a skin**. It is `hand × ink × stationery × neatness × slant × variation ×
+aging × drawn-elements`. Expressed as skins that would need 20+ entries; expressed as an axis it
+composes with all 29 existing skins.
+
+Add to `src/types/index.ts`:
+
+```ts
+/** Which hand writes the document. `none` = normal typeset text. */
+export type HandStyle =
+ | 'none'
+ // Latin — everyday
+ | 'casual' // Caveat — relaxed ballpoint, variable weight axis
+ | 'neat-print' // Patrick Hand — tidy printing, very legible
+ | 'architect' // Architects Daughter — drafting hand, all-caps feel
+ | 'marker' // Gloria Hallelujah — thick felt tip
+ | 'scratchy' // Reenie Beanie — fast, untidy, ballpoint
+ | 'rushed' // Just Another Hand — tall, condensed, hurried
+ // Latin — expressive
+ | 'script' // Dancing Script — flowing, connected
+ | 'copperplate' // Great Vibes — formal calligraphic
+ | 'monoline' // Sacramento — even-width script
+ | 'cursive' // Cedarville Cursive — schoolbook cursive
+ | 'chalk' // a chalkboard hand
+ // Arabic
+ | 'ruqaa' // Aref Ruqaa — the everyday Arabic hand
+ | 'naskh-hand' // Lateef — softer handwritten Naskh
+ | 'diwani' // decorative Arabic
+ // User-supplied
+ | 'custom' // an uploaded font — see §7
+
+export type InkStyle =
+ | 'ballpoint-blue' | 'ballpoint-black' | 'fountain-blue' | 'fountain-black'
+ | 'pencil' | 'marker' | 'red-pen' | 'gel' | 'chalk-white' | 'sepia'
+
+export type Stationery =
+ | 'blank' | 'ruled-college' | 'ruled-wide' | 'ruled-narrow'
+ | 'graph' | 'dot-grid' | 'isometric'
+ | 'legal-pad' | 'engineering' | 'cornell' | 'index-card' | 'steno'
+ | 'practice-lines' // 4-line with dashed midline — handwriting worksheets
+ | 'music-staff'
+ | 'parchment' | 'kraft' | 'graph-blue'
+
+/** How far the variation engine goes. Higher tiers cost DOM weight (§3.5). */
+export type HandVariation = 'none' | 'word' | 'expressive'
+
+export interface HandConfig {
+ readonly hand: HandStyle
+ /** A second hand for headings — real notes title more carefully than they write. */
+ readonly headingHand: HandStyle | 'same'
+ readonly ink: InkStyle
+ readonly stationery: Stationery
+ /** 0 = careful and even, 1 = rushed and messy. Scales all jitter amplitudes. */
+ readonly neatness: number
+ /** -1 = left-handed back-slant, 0 = upright, 1 = strong right lean. */
+ readonly slant: number
+ readonly variation: HandVariation
+ /** 0 = fresh, 1 = yellowed paper, faded ink, fold creases. */
+ readonly aging: number
+ /** Hand-drawn rules, bullets, checkboxes, underlines, table borders (§5). */
+ readonly drawnElements: boolean
+ /** A stable seed so jitter never changes under the same document. */
+ readonly seed: number
+}
+```
+
+Then `readonly hand: HandConfig` on `PdfConfig`, with `DEFAULT_HAND` (`hand: 'none'`) in
+`src/lib/constants.ts`. **`hand: 'none'` must be a total no-op** — no extra DOM, no extra CSS, no
+plugin work, no font requests. A user who never touches handwriting must not pay one byte for it.
+
+**Front-matter** (`src/lib/frontmatter.ts`): map `hand`, `ink`, `paper`/`stationery`, `neatness`,
+`aging` with the existing validation style (validate against the unions; ignore unknown values).
+That is what lets a template declare its own hand.
+
+---
+
+## 2.5 Libraries — what to use, what to build
+
+**Do not hand-roll what is already solved.** Verified against this repo's installed tree:
+
+| Need | Use | Size / licence | Notes |
+| --- | --- | --- | --- |
+| Hand-drawn rules, boxes, underlines, ticks (§5) | **`roughjs`** | ~9 KB gz, MIT | **Already in the tree** — Mermaid 11.16.0 depends on `roughjs ^4.6.6`. Adding it as a direct dependency dedupes to ~0 extra bundle. |
+| Sketchy Mermaid diagrams (§5.2) | **`look: 'handDrawn'`** | **free** | Mermaid 11.16.0 supports it natively (that is *why* it bundles rough.js). A config flag, not a dependency. |
+| Pressure-sensitive stroke capture (§7) | **`perfect-freehand`** | ~4 KB gz, MIT | Turns pointer input into a variable-width outline. Also powers the signature line. |
+| Generating a real font file in-browser (§7) | **`opentype.js`** | ~200 KB, MIT | Can **write** `.otf`, not just parse. Must be lazy-loaded. |
+| The hands themselves | **Google Fonts** (OFL) | — | Caveat (variable `wght`), Patrick Hand, Architects Daughter, Aref Ruqaa… |
+| Seeded PRNG, word wrapping, stationery, rule-pitch locking | **build our own** | ~150 lines | Too specific to generalise; `mulberry32` is five lines. |
+| Font readiness | **native `FontFace` / `document.fonts`** | — | No `fontfaceobserver`. |
+
+### Why rough.js rather than our own stroke generator
+
+It takes a **`seed`** option — so it satisfies §1's determinism requirement directly — and exposes
+`roughness` and `bowing`, which map onto the *neatness* slider with no translation layer. It emits
+SVG `` elements, so output stays vector: **selectable, scalable, and correct at print DPI.**
+
+**Bundle discipline:** lazy-load `roughjs` only when `drawnElements` is on, and `opentype.js` +
+`perfect-freehand` only when the "create my hand" flow opens. A user with `hand: 'none'` must
+download none of it.
+
+---
+
+## 3. The variation engine
+
+### 3.1 The plugin
+
+`src/markdown/plugins/rehypeHandwriting.ts` — a rehype plugin, registered in
+`MarkdownRenderer.tsx` **last**, after `rehype-prism-plus` and `rehype-katex`.
+
+**Skip list — never descend into these:**
+`pre`, `code`, `.katex`, `.katex-display`, `svg` (Mermaid), `.mermaid-figure`, `.ascii-figure`,
+`.hljs`, anything with `data-code-chunk`, and any element already carrying `.hw`.
+
+Rationale: Prism has already produced a precise span tree; KaTeX layout is positionally exact;
+Mermaid is SVG. Wrapping inside any of them corrupts them.
+
+### 3.2 Word wrapping — use a class palette, not inline styles
+
+The naive approach writes `style="--hw-r:0.7deg;--hw-y:0.4px"` on every word. On a 100-page
+document that is ~50,000 inline style attributes, which bloats the DOM, the export clone, the
+standalone HTML export, and pagination time.
+
+**Do this instead:** precompute **16 jitter buckets** as CSS classes.
+
+```html
+handwriting
+```
+
+```css
+.scripto-doc[data-hand] .hw { display: inline-block; will-change: auto }
+.scripto-doc[data-hand] .hw-7 { transform: translateY(.35px) rotate(.42deg); font-variation-settings: 'wght' 412 }
+/* …16 buckets, amplitudes multiplied by --hw-neatness at the root… */
+```
+
+Bucket = `prng(seed, wordIndex) * 16 | 0`. Sixteen buckets is enough to defeat the eye and costs
+16 CSS rules instead of 50,000 style attributes. Amplitudes scale from one root custom property
+(`--hw-neatness`) so the *neatness* slider is a single CSS variable change — **no re-parse, no
+re-wrap, no re-pagination.** That is the difference between a slider that feels instant and one
+that hangs the tab.
+
+### 3.3 Never split inside a word
+
+Split on whitespace only. **Never split a word into per-character spans.**
+
+- For Latin it destroys kerning and ligatures.
+- **For Arabic it is catastrophic** — Arabic is a connected script; splitting a word breaks glyph
+ shaping and the text renders as disconnected isolated forms. It becomes unreadable.
+
+Therefore `variation: 'expressive'` must **never** mean glyph-level DOM splitting. It means:
+more buckets (32), wider amplitude, and OpenType stylistic sets — nothing structural.
+
+### 3.4 Pressure, via variable fonts
+
+Caveat ships a `wght` axis. Where the active hand is variable, add a jittered
+`font-variation-settings: 'wght' <380–460>` to each bucket. This simulates pen pressure and is the
+single highest-payoff detail in the whole engine — it is what stops the text looking flat.
+Gate it behind a per-hand `variable: true` flag in the hand registry; for static fonts, omit it.
+
+### 3.5 Performance contract
+
+- `variation: 'none'` → the plugin returns the tree untouched. Zero cost.
+- Above **20,000 words**, degrade to `'none'` automatically and toast once (i18n string). Do not
+ let a thesis freeze the tab.
+- Wrapping runs inside the existing debounced render path — it must not add more than ~15 ms for a
+ typical 10-page document. Measure it; report the number.
+- `transform` on inline-blocks is compositor-friendly, but 50k composited layers are not. Do **not**
+ set `will-change: transform` on `.hw`.
+
+---
+
+## 4. Stationery — and the hard part
+
+### 4.1 Papers, all CSS-only
+
+Every stationery is built from `repeating-linear-gradient` / `radial-gradient` / `conic-gradient`.
+**Zero image bytes.** Live in a new `src/styles/stationery.css`, keyed `[data-stationery='x']`.
+
+Note `cornell` (cue column + summary band), `practice-lines` (4 lines with a dashed midline — this
+one exists for teachers making handwriting worksheets, a real and underserved audience), and
+`music-staff`.
+
+### 4.2 ⚠️ Rule-pitch locking — read this twice
+
+**Handwriting on ruled paper must sit ON the rules.** If the text baseline and the rule are even
+1 px out of phase, the whole illusion collapses and it looks worse than plain text. Three
+constraints, all mandatory:
+
+1. **`line-height` must equal the rule pitch exactly.** When a ruled stationery is active, derive
+ `--doc-leading` from the pitch instead of `config.lineHeight`, and **disable the line-height
+ slider in the UI** with a short explanation (i18n) rather than letting the user silently break it.
+2. **The top margin must be an integer multiple of the pitch.** Add
+ `snapMarginsToRule(margins, pitch)` in `src/lib/handwriting/rules.ts` and apply it when a ruled
+ stationery is active. Surface the adjusted value in `ConfigPanel` so nothing appears to change
+ behind the user's back.
+3. **The rules must restart in phase on every page.** This is the real problem. Under Paged.js each
+ `.pagedjs_page` is its own box, so a background painted on the document body will drift. Paint
+ the rules on the **page box** in `src/pdf/pageStyles.ts` (`@page { background-image: … }` /
+ `.pagedjs_page` background), not on `.scripto-doc`, and ensure the content box top sits at an
+ exact multiple of the pitch from the page top.
+
+**Acceptance for §4.2:** export a 5-page ruled document. On **every** page, every line of body text
+must sit on a rule. Check page 1, page 3 and the last page. If page 4 drifts, this is not done.
+
+### 4.3 Aging
+
+`aging` (0–1) drives: paper yellowing (a warm overlay), ink fade (opacity + slight desaturation),
+edge vignetting, and fold creases (two faint gradient lines). All CSS. Fold creases and vignette are
+subtle at 1.0 — this is a seasoning control, not a filter.
+
+---
+
+## 5. Hand-drawn elements
+
+With `drawnElements: true`, the machine-straight furniture is replaced by hand-drawn strokes.
+Implement as **inline SVG paths generated from the seeded PRNG**, in
+`src/lib/handwriting/strokes.ts` — three stroke variants per element type, chosen by bucket.
+
+| Element | Hand-drawn treatment |
+| --- | --- |
+| `h1`–`h3` | A wobbly underline stroke; H1 optionally double-struck |
+| `hr` | A squiggle, never a straight rule |
+| `blockquote` | A drawn bracket down the inline-start edge |
+| `ul` bullets | Drawn dots / dashes / asterisks, varying per item |
+| Task checkboxes | A drawn box; checked items get a drawn tick that overshoots the box |
+| `table` | Hand-ruled borders with slight wobble |
+| Callouts | A drawn box plus a margin doodle for the type |
+| `==mark==` | A highlighter swipe with uneven ends, not a rectangle |
+| `a` | A hand underline in the ink colour |
+| Images | Photo-corner mounts or washi tape |
+
+### 5.1 Code, math and diagrams — the "taped-in printout"
+
+**Do not set code, math or Mermaid in a handwriting font.** It becomes unreadable, and this is the
+mistake every handwriting theme makes.
+
+Instead present them as artefacts that were *printed and taped into the notebook*: keep the
+monospace/KaTeX/SVG rendering exactly as-is, and give the block a screen-and-print treatment — a
+white card, a fraction of a degree of rotation (seeded, so it is stable), a soft drop shadow, and
+tape corners. It reads as deliberate. It is also the honest engineering answer, because it preserves
+legibility, selectable text and vector output.
+
+### 5.2 Sketchy diagrams — free
+
+Mermaid 11.16.0 (installed) supports `look: 'handDrawn'` natively. When `drawnElements` is on, pass
+it in the Mermaid init config in `src/markdown/components/Mermaid.tsx`. Combined with
+`handDrawnSeed` (set it from `HandConfig.seed`) the diagrams are deterministic too. **No new
+dependency, roughly ten lines.** It is the highest payoff-to-effort item in this brief — do it early
+so the demo lands.
+
+## 6. Fonts — loading, offline, and PDF weight
+
+Handwriting fonts must **not** join the global font URL in `index.html`. That URL is preloaded on
+every visit including the marketing site; adding 15 display faces would wreck LCP and CLS on pages
+that will never use them.
+
+**Required approach:**
+- A hand registry in `src/lib/handwriting/hands.ts`:
+ `Record`.
+- Load **on demand** when a hand is selected — inject a `` for that family
+ only, once, and track loaded families in a module-level `Set`.
+- **`await document.fonts.load(...)` before allowing export.** `renderPaged.ts` already awaits
+ `document.fonts.ready`; a hand that is still loading when Paged.js measures will paginate against
+ the fallback metrics and the PDF will be wrong. Block the Print Preview until the hand is ready,
+ showing the existing progress UI.
+- **PWA/offline:** Google Fonts are runtime-cached, so a hand used once works offline afterwards,
+ but a *newly chosen* hand will not load on a plane. Detect the failure and toast honestly
+ (i18n) rather than silently rendering the fallback.
+- **`src/io/exporters.ts` `FONTS_HREF`** must include the *active* hand for the standalone HTML and
+ Word exports — build it dynamically from the config instead of the current constant.
+- **PDF size:** browser print embeds the full face. Display scripts are large; a handwriting PDF may
+ be several MB heavier. Note it in the export UI when `hand !== 'none'`.
+
+### 6.1 Arabic hands — the differentiator
+
+No markdown→PDF tool does Arabic handwriting. Ruqʿah (`Aref Ruqaa`) is the everyday Arabic hand and
+this is genuinely unprecedented in this product category.
+
+Rules: Arabic hands are `scripts: ['arabic']` and must be filtered out of the picker for LTR
+documents (and vice-versa) — or clearly marked, since a Latin hand renders Arabic as tofu. Word-level
+jitter is safe; §3.3's no-glyph-splitting rule is **absolute** here. `slant` is meaningless for
+Arabic — hide the control when an Arabic hand is active.
+
+---
+
+## 7. "Your own handwriting" — two paths
+
+Fully client-side, and the reason people will tell other people about this.
+
+### 7.1 Path A — import a font (ship this first)
+
+1. **Scripto generates the capture sheet.** A new template (`handwriting-capture`) renders a grid of
+ boxes, one per character, sized for a service like Calligraphr. The user exports it as a PDF from
+ Scripto itself — the product produces its own onboarding artefact.
+2. They print it, write in it, scan it, convert it (Calligraphr has a free tier) to `.ttf`/`.otf`/`.woff2`.
+3. **They upload the font back.** Store the bytes in IndexedDB, register at runtime via the
+ `FontFace` API, expose as `hand: 'custom'`.
+4. It embeds in the PDF like any other font.
+
+**Constraints:** validate magic bytes and reject non-fonts; cap ~5 MB; handle `FontFace` rejection
+with a real message; let the user name, list and delete hands. Never send the file anywhere — say so
+plainly in the dialog.
+
+### 7.2 Path B — draw your alphabet in the browser (the moonshot)
+
+Path A depends on a printer, a scanner and a third-party service. **Path B removes all three.**
+
+1. A canvas glyph grid: the user writes each character with a finger, stylus or trackpad.
+2. **`perfect-freehand`** converts each pointer stroke into a variable-width outline — capture the
+ stroke as vectors from the start, so there is **no raster-tracing step at all**.
+3. **`opentype.js`** assembles those outlines into glyphs and **writes a real `.otf`** in the browser.
+4. Register via `FontFace`, store in IndexedDB, use it like any other hand.
+
+Fully offline, no account, no upload, no external service. This is the single most shareable thing
+in this brief — see §9.
+
+**Scope control:** Latin upper + lower + digits + common punctuation (~70 glyphs) is enough. Derive
+sensible side bearings from the drawn bounding box; do not attempt kerning pairs. Autosave progress
+so a half-finished alphabet survives a reload. **Do not attempt Arabic in Path B** — Arabic needs
+four positional forms per letter plus shaping rules, which is a different and much larger project.
+Say so in the UI rather than shipping something broken.
+
+## 8. Working across all 29 skins and every template
+
+The handwriting axis composes with **29 skins × 58+ templates**. Most combinations are fine; some
+are actively wrong. Handle this with data, not hope.
+
+### 8.1 Hand affinity — a required field on every skin
+
+Add to `SkinOption` in `src/data/skins.ts` (required, so `tsc` forces a decision for all 29):
+
+```ts
+/** How well this skin receives a handwriting hand. */
+export type HandAffinity = 'native' | 'good' | 'adapts' | 'discouraged'
+```
+
+| Affinity | Meaning | Skins |
+| --- | --- | --- |
+| `native` | Designed for a hand; ships one by default | the 6 new skins (§10.1) |
+| `good` | Reads well immediately | `modern` `classic` `notebook` `manuscript` `zen` `editorial` `playful` `handout` `poster` `elegant` |
+| `adapts` | Works once the skin's own furniture yields (§8.2) | `compact` `memo` `letter` `thesis` `journal` `newsprint` `brutalist` `dark` `corporate` `technical` |
+| `discouraged` | Handwriting fights the skin's purpose | `terminal` `blueprint` `swiss` `ledger` `invoice` `contract` `rfc` `changelog` `resume` |
+
+**`resume` deserves a specific warning.** A handwritten CV defeats the entire point of the ATS skin.
+Warn clearly and offer to switch skins — but still allow it. Users get to make their own choices.
+
+**Policy — guide, never block:**
+- The skin picker and Skin Rail show a small hand-affinity badge.
+- Applying a hand to a `discouraged` skin shows a **one-time, dismissible inline notice** (not a
+ modal, not on every change) offering a one-click switch to `handwritten`.
+- Nothing is ever hard-blocked or silently overridden.
+
+### 8.2 Yield rules — the compatibility work
+
+Every skin decorates with its own furniture. Some of it contradicts a hand and must yield when
+`[data-hand]` is present on the root. Because `documentDataAttrs` already emits `data-skin`, adding
+`data-hand` makes combined selectors natural:
+
+```css
+.scripto-doc[data-skin='corporate'][data-hand] h2 { background: none; color: var(--ink) }
+```
+
+**Audit all 29.** Most need nothing; write an override block only where the skin genuinely fights
+the hand. Known cases to check:
+
+| Skin | What must yield |
+| --- | --- |
+| `corporate` | Filled heading bars → a drawn underline |
+| `technical` | Boxed sidebars → drawn boxes |
+| `swiss` / `brutalist` | Heavy machine rules → drawn rules, or hold the skin's rules deliberately |
+| `terminal` / `dark` | Ink colour must invert to a light chalk/gel ink; `chalk-white` is the sane default |
+| `ledger` / `invoice` | **Keep tabular figures monospace** — handwritten numerals in a totals column are unreadable |
+| `newsprint` | The kicker rule → a drawn stroke |
+| `memo` / `letter` | The header band → a drawn rule |
+| `poster` | Display sizes need jitter amplitude scaled *down* — large type magnifies wobble |
+
+**That last point is a general rule:** jitter amplitude must scale **inversely** with font size, or
+headings look drunk while body text looks fine. Bake it into the bucket CSS via `em`-relative units,
+not `px`.
+
+### 8.3 Templates
+
+All 58+ templates must render acceptably with a hand applied. Add a **dev-only contact-sheet route**
+(`/app?dev=hand-matrix`, excluded from the SSG route table) that renders every template at thumbnail
+size with `hand: 'casual'` — one screen, one glance, obvious breakage. Fix what breaks; for templates
+where a hand is genuinely wrong (`invoice`, `contract`, ATS résumés), rely on §8.1's affinity badge
+rather than special-casing.
+
+### 8.4 Applying smoothly — the transition contract
+
+Turning a hand on or off swaps the font, which changes metrics, which reflows the document. Done
+naively it is a jarring flash-and-jump. Required sequence:
+
+1. **Load the font first.** `await document.fonts.load('1em ')` **before** touching config.
+ Never apply a hand that has not loaded — the fallback flash followed by a metric jump is the
+ single worst thing this feature can do.
+2. Show a brief inline pending state on the Handwriting toggle while loading.
+3. Apply in **one frame**, and run the transition through the existing
+ `src/preview/motion/useStageTransition.ts` — reuse the vocabulary, do not invent a second
+ transition system.
+4. `neatness`, `slant` and `aging` are **CSS-variable-only** changes (§3.2) — instant, no re-wrap,
+ no re-pagination. Verify by dragging the slider on a 20-page document.
+5. Under `prefers-reduced-motion`, cross-fade only.
+6. If the font fails to load (offline, blocked), **do not apply the hand.** Toast honestly and leave
+ the document as it was.
+
+---
+
+## 9. The viral moment — design for it deliberately
+
+The output has to be worth posting, and posting it has to bring people back. Four mechanics, all
+frontend:
+
+1. **The artefact is the ad.** A handwritten PDF page is recognisable at thumbnail size in a
+ timeline — which is exactly what the Stage system already optimises for. When a hand is active,
+ set the **"Made with Scripto" attribution footer in the same hand**. Small, charming, and it
+ signs every shared document without being an ad.
+2. **Shareable hand recipes.** `HandConfig` is tiny. Encode it into the existing deep-link system
+ (`src/hooks/useDeepLinks.ts`, already allowlist-validated) as `/app?hand=`, so
+ someone can post *"here's my exact setup"* and a stranger lands in the editor with it applied.
+ Reuse the existing validation pattern — **never `eval` or trust the parameter shape.**
+3. **"I made a tool that writes in my handwriting."** §7.2 is the story. Path B — draw your alphabet
+ with your finger, get a real font, export a PDF — is a 20-second screen recording that explains
+ itself with no voiceover. Build it, then record it.
+4. **Feed the content machine** (`ARCHITECTURE.md` §14 — pages generate automatically):
+ a `/handwriting` use-case page (+ Arabic variant), the six new skin pages, the new template pages,
+ and a blog post. Add a **before/after slider** on the landing page: the same Markdown as a plain
+ PDF and as a handwritten one.
+
+**Honest framing:** market it as *notes, letters, journals, cards and worksheets* — not as
+"make documents look authentic." That framing is both truthful and, per §12, the right boundary.
+
+---
+
+## 10. Skins, stages, templates and presets
+
+### 10.1 Six new skins — each needs a stage (compile-enforced)
+
+Add to `DocumentSkin`, `SKIN_OPTIONS` (group `expressive`, except `worksheet` → `academic`;
+all six `handAffinity: 'native'`), a `document.css` block, an i18n label, **and a `STAGES` entry** —
+`stages.ts` is `Record`, so `tsc` fails until every one exists.
+
+| Skin | Character | Stage (screen-only) | Motion |
+| --- | --- | --- | --- |
+| `handwritten` | The default hand — casual, college-ruled | **Desk** — warm wood ground, soft lamp pool, pen shadow at the edge | `type` |
+| `journal-hand` | Personal diary, aged paper, sepia ink | **Nightstand** — dim ground, warm lamp vignette | `bloom` |
+| `field-notes` | Pocket notebook, small, dense, graph | **Field** — canvas texture, notebook elastic band | `snap` |
+| `chalkboard` | Chalk on slate, white/pastel ink | **Classroom** — dark slate ground, chalk dust bloom | `draw` |
+| `letter-hand` | Personal correspondence, fountain ink | **Writing desk** — linen ground, envelope shadow behind the sheet | `wipe` |
+| `worksheet` | Practice lines, teaching material | **Schoolroom** — pale ground, pencil-tray edge | `cascade` |
+
+⚠️ Name it `journal-hand`, not `journal` — an academic `journal` skin already exists.
+
+Reuse existing `StageFrame` values where they fit; add new ones only where genuinely needed, and
+keep them in `stage.css` — **never on the sheet.**
+
+### 10.2 Templates
+
+Seven, each declaring its hand in front-matter: **Handwritten letter**, **Journal entry**,
+**Lecture notes** (Cornell), **Recipe card** (index card), **Handwriting worksheet** (practice
+lines — for teachers), **Handwriting capture sheet** (§7.1), and an **Arabic handwritten letter**
+using `ruqaa`. Add `nameKey`/`descKey` to `EN_STRINGS` **and** `STRINGS.ar`.
+
+### 10.3 Presets
+
+Add hand-aware entries to `DOCUMENT_PRESETS`, and confirm the `ExportPreset` flow round-trips a
+`hand` block through save → JSON export → import.
+
+---
+
+## 11. The UI
+
+A **Handwriting** section in `ConfigPanel`, collapsed by default, opening with a single `Switch`.
+When off, everything below is hidden and nothing loads.
+
+- **Hand** — a visual picker rendering the sample phrase *in each hand*, not a text dropdown.
+ Grouped by script; Arabic hands only shown for RTL/auto documents.
+- **Ink** — colour swatches with the pen name.
+- **Paper** — thumbnail grid of the stationeries.
+- **Neatness**, **Slant** — `Slider`s updating a CSS variable live (§3.2). Slant hidden for Arabic.
+- **Aging** — `Slider`.
+- **Hand-drawn elements** — `Switch` (lazy-loads `roughjs`).
+- **Two hands** — heading hand selector.
+- **My handwriting** — §7 upload / draw flow.
+
+Plus a `⌘K` command, hand-affinity badges in the skin picker and Skin Rail (§8.1), and hover-preview
+in the rail for the six new skins.
+
+---
+
+## 12. Guardrails — handwriting, signatures and authenticity
+
+Handwriting plus aging plus signatures is, taken together, a document-forgery kit. This feature is
+legitimate and creative and should ship — but keep these boundaries:
+
+- **Do not build handwriting replication from a photo or writing sample.** §7 is the user's *own*
+ hand, produced by their own deliberate effort. Never "match this handwriting" from an image.
+- **Signature blocks are signature *lines*** — a labelled ruled space to sign, not a rendered
+ imitation of a real person's signature.
+- **Do not ship preset combinations framed as making a document look like a genuine aged or official
+ record.** Aging is a stylistic control, not an authenticity simulator.
+- Leave the "Made with Scripto" attribution default **on**.
+- Keep the framing creative — notes, letters, journals, worksheets, cards.
+
+---
+
+## 13. Verification
+
+```bash
+npx tsc -b --force && npm run lint && npm run build && npm run test
+SCRIPTO_VISUAL=1 npm run test
+```
+
+**New tests (required):**
+- **Determinism** — the PRNG yields byte-identical bucket sequences across two runs for one seed.
+- **Skip list** — the plugin leaves `pre`, `code`, `.katex` and `svg` subtrees untouched (assert node
+ counts unchanged).
+- **Arabic integrity** — an Arabic paragraph splits only at spaces; no word contains a nested `.hw`.
+- **Rule snapping** — `snapMarginsToRule` returns an exact multiple of the pitch.
+- **Affinity exhaustiveness** — every one of the 29 skins declares a `handAffinity`.
+- **Visual fixture** — `tests/visual/fixtures/handwriting.md` (ruled + drawn elements + a code block
+ + a table) in the Paged.js harness.
+
+**Manual:**
+- [ ] **`hand: 'none'` is a total no-op** — PDF byte-identical to `main`, and **zero** handwriting
+ font / `roughjs` / `opentype.js` requests in the network tab.
+- [ ] A 5-page ruled document: text sits on the rules on **every** page (§4.2).
+- [ ] Code, math and Mermaid stay legible in a handwriting document.
+- [ ] Arabic Ruqʿah exports correctly RTL with connected letterforms intact.
+- [ ] Exported PDF text is still **selectable and searchable** — nothing rasterised.
+- [ ] The hand-matrix contact sheet (§8.3): no template is visibly broken.
+- [ ] Neatness slider on a 20-page document: instant, no re-pagination.
+- [ ] Toggling a hand on/off: no fallback flash, no metric jump (§8.4).
+- [ ] 20k-word document degrades gracefully with the toast.
+- [ ] Offline: a used hand works; a new one fails with an honest message.
+- [ ] Reduced motion, LTR + RTL, light + dark, 375 px.
+
+---
+
+## 14. Out of scope
+
+Anything needing a server. Also explicitly **not** now: rasterising SVG-filter ink texture into the
+PDF (it destroys the text layer — prototype it screen-only and opt-in if at all), scan/photocopy
+simulation, glyph-level DOM splitting (§3.3), and Arabic in the §7.2 draw-your-own flow.
+
+---
+
+## 15. Suggested order
+
+**Release 1 — "Handwriting"**
+1. §2 data model + prove `hand: 'none'` is a no-op
+2. §6 font registry + on-demand loading (nothing works without it)
+3. §5.2 sketchy Mermaid — ten lines, biggest demo payoff, do it early
+4. §3 variation engine + determinism tests
+5. §4 stationery, then **§4.2 rule-pitch locking — budget real time for this**
+6. §5 hand-drawn elements via `roughjs`
+7. §8 the 29-skin affinity + yield audit, §8.4 smooth application
+8. §10 skins/stages/templates, §11 UI
+
+**Release 2 — "Your own hand"**
+9. §7.1 font import
+10. §7.2 draw-your-alphabet (`perfect-freehand` + `opentype.js`)
+11. §9 viral mechanics + content
+
+Ship the two releases separately. Do not batch.
+
+---
+
+## 16. Final report
+
+What shipped; screenshots of the 6 new skins, the stationery grid, and the §8.3 hand matrix; the
+`hand: 'none'` PDF diff result; the §3.5 wrap-time measurement; PDF size delta with a hand active;
+bundle delta with `drawnElements` off vs on; and anything deferred, with the reason.
diff --git a/docs/V4_VISUAL_EDITOR_PROMPT.md b/docs/V4_VISUAL_EDITOR_PROMPT.md
new file mode 100644
index 0000000..9729bfa
--- /dev/null
+++ b/docs/V4_VISUAL_EDITOR_PROMPT.md
@@ -0,0 +1,485 @@
+# Scripto — The Visual Document Editor
+
+> **The third brief.** `V2_FRONTEND_PROMPT.md` (Stage system, page-break editor, export presets,
+> `App.tsx` decomposition) is delivered. `V3_HANDWRITING_PROMPT.md` is the handwriting engine.
+> **This one is the largest and the most dangerous** — it changes what Scripto *is*.
+>
+> **Goal:** *"A professional visual document editor powered by Markdown"* — not *"a Markdown editor
+> with a PDF preview."* Click anything in the preview, edit it, and the Markdown updates.
+>
+> **Still frontend-only. No backend.**
+>
+> §1 contains five architectural decisions. **Four of them contradict the obvious approach, and
+> getting any one wrong makes the feature unshippable.** Read §1 before anything else.
+
+---
+
+## 0. What already exists — do not rebuild it
+
+A generic version of this brief would say "create a document AST", "build a theme system", "add
+source mapping". **All three already exist here.** Building parallel versions is the main way this
+project can fail.
+
+| The generic instruction | The reality in this repo |
+| --- | --- |
+| "Create a document AST" | **mdast already is it.** react-markdown parses to mdast → hast. `mdast-util-*`, `remark-parse`, `remark-stringify` and `mdast-util-to-markdown` are **already in `node_modules`** (transitively — declare them directly if you import them). |
+| "Add source position mapping" | **`src/markdown/plugins/rehypeSourceLine.ts`** already stamps `data-source-line` on every block element, and `useScrollSync` already consumes it. It needs *extending* (§2.1), not writing. |
+| "Build a theme system" | `src/pdf/documentStyle.ts` maps `PdfConfig` → CSS custom properties + `data-*`; `document.css` reads them. It needs *widening* from 4 tokens to a full set (§6). |
+| "Templates all look the same" | **Audit before rebuilding.** Commit `e96999e` ("give every skin its own component language") and `42cb1e3` ("pair every template with a skin") landed recently. There are now **29 skins in 6 groups**. Screenshot all 29 first and find what is *actually* still undifferentiated. |
+| "Add a directive system" | `remark-directive` is already a dependency and already powers callouts (`:::tip`) and page directives (`::page-break`, `:::keep-together`, `:::landscape`). |
+| "Add undo/redo" | CodeMirror 6 already owns a full history. §1.4 explains why you must not build a second one. |
+
+**Also still true — the five invariants from `V2_FRONTEND_PROMPT.md` §0.** Re-read them. The one
+this brief stresses hardest: **`.scripto-doc` is cloned verbatim into the PDF, HTML and Word
+exports.** Every decision in §1 follows from it.
+
+---
+
+## 1. The five decisions
+
+### 1.1 mdast is the document model
+
+Do not invent a new AST. mdast is a mature, positioned, spec'd document model with a complete
+utility ecosystem, and it is already what the renderer consumes. A parallel model would need to stay
+in sync with it forever — a permanent bug source for zero benefit.
+
+**The document model is: the Markdown string + its mdast parse.** Nothing else is authoritative.
+
+### 1.2 ⭐ Positional splicing — never whole-document re-serialization
+
+**This is the decision that determines whether the feature ships.**
+
+The obvious design is `edit → mutate AST → remark-stringify → new Markdown`. **Do not do this.**
+`remark-stringify` normalises the *entire* document on every write:
+
+| The user wrote | It comes back as |
+| --- | --- |
+| `* item` | `- item` (bullet marker normalised) |
+| `_emphasis_` | `*emphasis*` |
+| `Setext heading\n===` | `# Setext heading` |
+| A hand-aligned table | Re-aligned to its own convention |
+| Their line wrapping | Re-wrapped |
+| Their raw HTML | Reformatted |
+
+So the first click on a checkbox silently rewrites the user's whole file. Their diff explodes. Their
+git history is noise. **This is the single most common reason bidirectional Markdown editors fail**,
+and the brief you are replacing asks for exactly this ("Do NOT introduce ugly, unreadable generated
+Markdown" — while specifying the architecture that guarantees it).
+
+**Do this instead.** Every mdast node carries
+`position: { start: { line, column, offset }, end: { … } }`. An edit is therefore a **byte-range
+splice on the Markdown string**:
+
+```ts
+const next = markdown.slice(0, node.position.start.offset)
+ + replacement
+ + markdown.slice(node.position.end.offset)
+```
+
+Everything outside that range stays **byte-identical**. The user's formatting, spacing and style are
+untouched. The diff is one line.
+
+**The rule, stated once:** *serialize only what is new; never re-serialize what already exists.*
+When inserting a genuinely new node, use `mdast-util-to-markdown` on **that node alone** and splice
+the result. Never stringify the root.
+
+### 1.3 Overlay editing — not `contenteditable`
+
+Making `.scripto-doc` editable is tempting and wrong, for three reasons:
+
+1. It is **cloned into the PDF/HTML/Word export**. `cleanClone()` in `src/io/exporters.ts` already
+ strips `contenteditable` — someone has already been bitten by this. Do not add more of it.
+2. `contenteditable` + React + a Markdown round-trip is a well-known disaster: the browser injects
+ ` `, `
`, `` and pasted formatting that has no Markdown representation.
+3. React will fight the browser over DOM ownership on every re-render.
+
+**Instead:** clicking a text node opens a **positioned overlay** — a plain `