diff --git a/content/docs/learn/playground/active-row-column-bands.mdx b/content/docs/learn/playground/active-row-column-bands.mdx
index 40eae6e..eace2ae 100644
--- a/content/docs/learn/playground/active-row-column-bands.mdx
+++ b/content/docs/learn/playground/active-row-column-bands.mdx
@@ -26,7 +26,7 @@ shows/hides live on the grid behind the panel.
## Verified behavior
-_Matches Google Sheets:_ Google Sheets' default active-cell emphasis is HEADERS-ONLY: selecting C4 bolds/tints only the "C" column-letter and "4" row-number header labels — the grid body gets no extra tint beyond the selection outline itself. Sheets has no first-party setting to additionally wash the whole active row/column band across the body (this is a Studio-only opt-in enhancement, not a Sheets-parity requirement), so the Sheets-faithful default this toggle must preserve is OFF — the render layer (already built and unit-tested) has carried this default since and this issue only adds a way to flip it at runtime.
+_Matches Google Sheets:_ Google Sheets' default active-cell emphasis is HEADERS-ONLY: selecting C4 bolds/tints only the "C" column-letter and "4" row-number header labels — the grid body gets no extra tint beyond the selection outline itself. Sheets has no first-party setting to additionally wash the whole active row/column band across the body (this is a Studio-only opt-in enhancement, not a Sheets-parity requirement), so the Sheets-faithful default this toggle must preserve is OFF — the render layer (already built and unit-tested) has carried this default from the start, and this issue only adds a way to flip it at runtime.
#### The toggle is reachable from Settings and starts unchecked (OFF, the Sheets-faithful default)
diff --git a/content/docs/learn/playground/chart-insert.mdx b/content/docs/learn/playground/chart-insert.mdx
new file mode 100644
index 0000000..19cdaaf
--- /dev/null
+++ b/content/docs/learn/playground/chart-insert.mdx
@@ -0,0 +1,451 @@
+---
+title: "Insert a chart from a range"
+description: "Selecting a range and choosing Insert ▸ Chart charts it into a docked chart panel bound to that range — a header row becomes the series names, a blank cell plots as a gap rather than a zero, a cell's number format carries onto the value axis, and the whole insert undoes in one step. The panel shows whichever chart the active sheet holds, however it got there, so a chart stays on screen across a reload and View ▸ Show ▸ Show chart brings back one you closed."
+---
+
+{/* GENERATED — do not edit by hand */}
+
+## What it does
+
+Select a range and choose **Insert ▸ Chart**. A chart appears bound to that range, in a
+**docked panel** beside the grid — pick a cell/range, open **Insert**, click **Chart**. There
+is no type-picker dialog first: the chart draws immediately.
+
+The binding is the part that matters. A chart is not a snapshot of some numbers; it holds a
+**reference to a range**, resolved fresh every time the panel redraws — edit a charted cell
+and the chart updates with it.
+
+Two rules the renderer enforces, both visible in the scenarios above:
+
+- **A blank cell is a gap, not a zero.** A bar is skipped, a line breaks. Plotting a missing
+ value at the baseline is a lie about the data, and it is the single most common way charts
+ mislead.
+- **The cell's number format is the axis format.** A currency column charts with a currency
+ axis, because the format belongs to the data, not to the chart.
+
+A single unnamed series renders with **no legend** — matching Google Sheets (oracle behaviour
+7). A header row becomes the series names and brings a legend with it. An empty or single-cell
+selection creates nothing — there is no data to chart.
+
+## Try it
+
+
+
+## When the chart is on screen — the panel binds to the document, not to the click
+
+Studio **docks the chart itself** (see the ADR note above), so this panel is not an editor you
+opt into the way Sheets' "Chart editor" is: it is the chart's only rendering surface. A chart in
+the document that draws nowhere is a chart you have no reason to believe was ever saved. So the
+rule is:
+
+**The active sheet's chart is shown whenever there is one, unless you turned it off and nothing
+has changed since — a choice that now survives a reload.**
+
+Concretely, the app shell subscribes to `Store.chartsOnActive()` and derives the panel's bound chart
+from it. Every route that can produce a chart therefore ends with the chart on screen — the menu,
+the command palette, an agent's `chart:insert`, a reload, a restored document, or redo of an
+undone insert — and every route that can remove one unbinds the panel, including switching to a
+sheet the chart isn't on. Whenever a chart **arrives**, the panel binds to *that* chart, so
+inserting a second one shows the second one rather than snapping back to the sheet's first —
+through the wire API exactly as through the menu.
+
+**Closing it is a real choice, not a one-way door.** The ✕ (or Escape) hides the panel; the chart
+record is untouched. **View ▸ Show ▸ Show chart** brings it back — a checkbox row beside `Show
+gridlines` and `Show formula bar`, because it is the same kind of thing: one chrome surface's
+visibility, not a change to the document. It is disabled ("No chart on this sheet.") when the
+active sheet has none, and — unlike `Insert ▸ Chart` — it is **not** gated on the charts Preview
+flag: that flag stages *creating* a chart, and a chart already in your document is document
+content. `Insert ▸ Chart` is never overloaded into "open the existing one"; it always creates.
+
+**Unchecking it sticks across a reload**. All three `View ▸ Show` rows are stored
+together as local UI preferences (their own versioned storage key — *not* the
+preview-features one, which means "opt-in staging flag"), so the box you unchecked is still
+unchecked when you come back. They used to be session-only, which meant unchecking `Show chart`
+and reloading brought both the chart *and* its tick back; that was consistent with its two
+neighbours, since all three were session-only, so all three were fixed rather than adding a
+third instance of the divergence.
+
+**Two things override a dismissal**, because each is new information that your earlier "not now"
+could not have been about:
+
+- **A chart arrives.** You closed a panel showing some other chart, or none at all; that cannot
+ sensibly suppress a chart which did not exist yet. Without this rule an agent's `chart:insert`
+ reproduced the original defect exactly — a chart in the document, nothing on screen. Note the *first*
+ read after boot is a baseline, not an arrival: a document restoring its own charts is the very
+ set you dismissed, so a reload honours your choice instead of undoing it.
+- **The document is replaced** (File ▸ Load from browser, Import JSON backup, Import data). The
+ new document's charts were never dismissed; the previous document's were.
+
+This is what keeps an agent-created chart visible while the preference persists: the guarantee was never "a
+reload always re-opens the panel", it is "a chart the user has not turned off is on screen, and
+there is always a way back to one they did".
+
+> **Possible divergence — REPORTED, NOT VERIFIED.** Google Sheets is *said* to store `Show
+> gridlines` as a sheet property inside the document (travelling with the file, so everyone
+> opening a shared workbook sees the author's choice) while `Show formula bar` is a per-user app
+> preference. **This repo has not observed either claim** — the oracle brief §7 still lists
+> reload/persistence behaviour as unobserved, and moving an unverified claim into this file would
+> not make it true, it would only move it out of the file that tracks what is unverified. Treat it
+> as a lead for a future oracle pass, not as ground truth, and do not build on it.
+>
+> What IS settled is our own behaviour: **all three toggles are per-browser here.** Whether
+> gridlines should instead live in the document is a schema decision this PR deliberately does not
+> take.
+
+**Closing another docked panel returns the slot to the chart.** History, Settings and Conditional
+formatting borrow the one docked slot, so the chart is that slot's resting state
+whenever there is a chart you haven't closed. Opening History over a chart and closing it again
+leaves the chart showing, rather than silently emptying the slot. This is done at each panel's own
+close, *not* by keying the auto-open effect off `activePanel` — that would re-open the chart the
+instant you opened another panel, fighting you for the slot you just asked for.
+
+> **Open question — needs a live oracle observation.** *Which* chart the panel shows when a sheet
+> holds several is not decided here: it shows the first, and there is no chart picker. The
+> reopen interaction that would inform it — Sheets' own chart ⋮ menu and its "Edit chart" route
+> back into the editor for an *existing* chart — is listed as NOT OBSERVED in
+> the oracle brief (gaps 3 and 4), and nothing here guesses at it. Note that the oracle cannot
+> settle the question this manifest *does* answer: Sheets floats the chart as a grid object and
+> docks only the editor, so it has no surface corresponding to Studio's docked panel. The
+> "a chart in the document is on screen" rule above follows from the docked-placement ADR, and
+> is recorded as a product decision, not as observed Sheets behaviour.
+>
+> One consequence worth naming: the command palette's catalogue is built from the *static* menu
+> model (`PALETTE_ENTRIES`), so both `Chart` and `Show chart` appear
+> there permanently greyed regardless of live state — the same pre-existing staleness
+> `Insert ▸ Chart` already had in the palette, not something new to this row. The menu bar
+> itself is live. `CommandPaletteActions.onToggleChartPanel` is therefore **dead on arrival**:
+> wired so the switch stays a complete mirror of the menu bar's own `runItem`, and so the row works
+> the day the catalogue goes live, but unreachable until then.
+
+> **Known coverage gap.** The scenario harness's `insertChart` verb always drives
+> `CanvasGrid.insertChart()` — the `chart.insert` operation — never App's React `insertChart()`
+> handler, deliberately, because the real `Insert ▸ Chart` menu row is gated on a
+> `localStorage` preview flag the harness reuses one browser page across (see the harness's own
+> reasoning). So **no scenario exercises the React handler**: its notice-on-degenerate-selection,
+> its `setActivePanel('chart')` and its preference write are covered only by the manual
+> browser pass recorded on this feature's issue, not by the suite. Everything the scenarios above assert
+> about a chart appearing is therefore asserted about the *operation* path — which is precisely
+> the path an agent, the wire API and MCP take, so the gap is on the human route, not the
+> machine one.
+
+## Charts and the Preview-features toggle — what "Enable charts" actually gates
+
+**"Enable charts" gates the `Insert ▸ Chart` MENU ITEM only.** It is a *UI preference*, stored
+in this browser's `localStorage` (`truecalc.studio.previewFeatures.v1`), not a property of the
+document.
+
+The consequence, stated plainly because it is otherwise surprising enough to be filed as a
+bug: **with the toggle off, an agent can still create a chart in your document.** The command
+succeeds, the `ChartRecord` lands, and it survives save/load like any other document content.
+That is intended, not a defect.
+
+The reason is that the alternative is worse. Promoting a browser preference into something a
+headless caller could read would make your document's capabilities depend on one browser's
+settings — a workbook would behave differently through the API depending on which machine last
+opened it. So the flag stays what it is: a local staging control for an in-progress menu item.
+
+**An agent-created chart IS visible in the app — with the toggle in either position.** The
+panel's binding is derived from the document, not from the insert event: the app shell
+subscribes to `Store.chartsOnActive()` and shows the active sheet's chart whichever way it
+arrived — the menu, `chart:insert`, a reload, or a restored document — and an arriving chart
+binds the panel to itself and overrides an earlier dismissal, so this holds even in a session
+where the user has closed the panel by hand. The toggle stays a
+staging control for the *menu row*; it has never gated the insert path, and it does not gate
+looking at a chart the document already holds either. Showing document content must not depend
+on a browser-local preference — that combination is exactly what made an agent's chart
+invisible before this feature landed.
+
+## Charts from the API, an agent, or MCP
+
+Charting is reachable from all three of Studio's agent-facing surfaces, not just the menu:
+
+| Surface | Entry point |
+|---|---|
+| Operation registry (keyboard, command palette, headless) | `chart.insert` |
+| Wire commands | `chart:insert`, `chart:update`, `chart:remove` |
+| MCP tools | `studio__chart__insert`, `studio__chart__update`, `studio__chart__remove` |
+
+`chart:insert` takes an **A1 range** (`{ from: "A1", to: "B3" }` — the same way every other
+range-taking command addresses cells, since an API caller has no selection to infer one from),
+an explicit `chartType`, and optional renderer `options`; it returns the new chart's
+**`chartId`**, which `chart:update` and `chart:remove` address the chart by. The whole insert
+is **one undoable step**, exactly like the menu path.
+
+Two things it will refuse, rather than create something that says nothing:
+
+- **A single-cell range.** There is no data to chart — the same v1 rule the menu path applies.
+- **A chart form that cannot draw the selected data.** `chart:insert` runs the *same*
+ applicability rule the panel's type picker does — one shared rule, not a second
+ implementation — and returns the *same sentence* the
+ picker shows on hover — e.g. asking for a scatter over a single numeric column returns
+ "Scatter needs at least two numeric series (an x column and a y column)." The rule and its
+ wording live in one place and are shared by both surfaces, so an agent gets a correctable
+ error where the picker would have shown a tooltip. Note this covers a **form/shape
+ mismatch** only, and four forms are exempt from it entirely — see the next section before
+ relying on it.
+
+`chart:update` applies the same check before switching an existing chart's form, and validates
+against the options being set in that same command — so fixing a Sankey by re-assigning its
+`columnRoles` in one call works, rather than being rejected against the stale ones. Neither
+command can re-point a chart's `range`: a new range means a new chart.
+
+### What the applicability guard does and does not promise
+
+The guard checks **type against shape** — whether *this chart form* can draw *the data
+currently in the range*. It is **not** an emptiness check, and it is not a promise that every
+chart in your document draws something. Three consequences follow; none is a bug, and the third
+is the one most likely to surprise.
+
+**Four forms are never gated at all.** `column`, `bar`, `line` and `area` degrade gracefully to
+any `ChartData`, so there is no shape they can misrepresent and nothing to reject them on.
+Every other form — `pie`, `scatter`, `box-plot`, `slope`, `dumbbell`, `small-multiples`,
+`calendar-heatmap`, `sankey` — has a rule.
+
+**So whether you can chart first and fill the cells afterwards depends on the form.** With the
+four ungated forms it works: `chart:insert` with `chartType: "column"` over an empty range
+succeeds, returns a `chartId`, and the chart fills in as data arrives. With any gated form it
+does not — `chartType: "scatter"` over an empty range is rejected with `chart:insert rejected
+chartType "scatter": Scatter needs at least two numeric series (an x column and a y column).`,
+because at that instant the range genuinely cannot be scattered. For those, write the cells
+first (`edit:setCell`), then insert.
+
+**`column` is the default, and it will happily persist a chart that draws nothing.** That is
+the honest reading of the two points above: `Insert ▸ Chart`, the `chart.insert` operation and
+`chart:insert` with `chartType: "column"` all accept a real-but-empty range and store a chart
+with no data in it. That is the known empty-chart defect (a chart that renders happily and says nothing)
+still reachable at insert time through the ungated forms — the guard narrowed that hole, it did
+not close it. The empty chart at least *appears* now — the panel opens on the
+agent's insert and draws nothing — so the outcome is visible rather than silent. Visible is not
+the same as refused: nothing rejects the insert or warns about it.
+
+**And the check is one-shot, not an invariant.** Nothing re-validates a chart after it exists,
+even for a gated form. Insert a `pie` over `1/2/3`, then clear those cells with `edit:clear`,
+and the chart stays in the document while applicability now reports *"Pie needs one numeric
+series with at least one value."* — persisted, inapplicable, unnoticed.
+
+Whether an empty range should be refused, and whether emptying a charted range should re-check,
+warn, or do nothing, are separate decisions — deliberately not taken here.
+
+### Known gap — a chart's range does not yet re-point through a structural edit
+
+Google Sheets re-points a chart's data range through insert/delete the way a formula reference
+does (oracle behaviour 12) — insert 3 rows above `B6:B8` and Sheets moves the source to
+`B9:B11` with no broken reference. **Studio's `insertRows`/`insertColumns`/`deleteRows`/
+`deleteColumns` do not adjust `model.charts` at all** (`Store.applyStructuralShift` rewrites
+`cells`/`formats`/`colWidths`/`rowHeights`/`merges` for the active sheet but never touches
+`charts`) — a chart's bound range is a static rectangle today. This is a real, confirmed gap
+against Sheets ground truth, not a design choice; wiring the chart range through the same
+`ReferenceAdjuster` formulas already use is future work, tracked against this issue. No
+scenario in this manifest asserts range re-pointing, since the shipped behaviour does not have
+it yet.
+
+### Not built, not claimed
+
+The full chart-type list, the Customize tab, and the chart's own ⋮ menu are marked NOT
+OBSERVED in the oracle brief — nothing here asserts them. Dual-axis and 3D chart types are
+deliberate, permanent divergences from Sheets (the oracle brief's own "Accepted divergences"
+section).
+
+## Verified behavior
+
+_Matches Google Sheets:_ In Google Sheets, Insert ▸ Chart is a plain menu item — no submenu, no keyboard shortcut, and no chart-type picker before insertion — sitting in the insert-object group beside Pivot table, Image and Drawing. Choosing it inserts a chart IMMEDIATELY with a type chosen from the shape of the selected data (a single numeric column, B6:B8 holding 1/2/3, drew a Column chart); a spilled array range charts as an ordinary numeric range, with no special handling. A single unnamed series renders with NO legend — the default column chart drew bars and a value axis with no legend and no visible title. The chart is inserted as a FLOATING object over the grid, CELL-ANCHORED to the selection (inserting a row above the anchor moves the chart down with it; inserting rows below the anchor leaves it in place), while a separate "Chart editor" side panel docks on the right with Setup and Customize tabs. The chart's own data range re-points through a structural edit exactly like a formula reference — inserting 3 rows above B6:B8 moved its source to B9:B11 with no broken reference and no user action. A single ⌘Z undoes the whole insert — chart, anchored position and all — in one step, closing the editor with it. (Live-observed in the reference workbook on 2026-07-27 — see this feature's oracle brief, behaviours 1, 2, 3 as corrected by 11, 7, 8, 9, 11, 12, 13.)
+
+#### Inserting a chart from a single numeric column binds it to that range
+
+**Given**
+- **B6** = `1`
+- **B7** = `2`
+- **B8** = `3`
+
+**When** I select **B6:B8**, then choose Insert ▸ Chart
+
+**Then**
+- the chart is sourceRange=`"B6:B8"`, type=`"column"`
+
+#### A single unnamed series renders without a legend
+
+**Given**
+- **B6** = `1`
+- **B7** = `2`
+- **B8** = `3`
+
+**When** I select **B6:B8**, then choose Insert ▸ Chart
+
+**Then**
+- the chart is seriesCount=`1`, hasLegend=`false`
+
+#### A header row becomes the series names, and two series get a legend
+
+**Given**
+- **A1** = `Quarter`
+- **B1** = `North`
+- **C1** = `East`
+- **A2** = `Q1`
+- **B2** = `10`
+- **C2** = `20`
+- **A3** = `Q2`
+- **B3** = `12`
+- **C3** = `22`
+
+**When** I select **A1:C3**, then choose Insert ▸ Chart
+
+**Then**
+- the chart is seriesNames=`["North","East"]`, categories=`["Q1","Q2"]`, hasLegend=`true`
+
+#### A blank cell inside the range is a gap, never plotted as zero
+
+**Given**
+- **A1** = `Q`
+- **B1** = `North`
+- **A2** = `Q1`
+- **B2** = `10`
+- **A3** = `Q2`
+- **A4** = `Q3`
+- **B4** = `30`
+
+**When** I select **A1:B4**, then choose Insert ▸ Chart
+
+**Then**
+- the chart is values=`[10,null,30]`
+
+#### A cell's number format carries onto the value axis
+
+**Given**
+- **A1** = `Q`
+- **B1** = `Revenue`
+- **A2** = `Q1`
+- **B2** = `128400`
+- **A3** = `Q2`
+- **B3** = `141200`
+
+**When** I click **B2**, then press `Control+Shift+4`, then click **B3**, then press `Control+Shift+4`, then select **A1:B3**, then choose Insert ▸ Chart
+
+**Then**
+- the chart is valueAxisFormat=`"currency"`
+
+#### Inserting a chart then undoing removes it in ONE step
+
+**Given**
+- **B6** = `1`
+- **B7** = `2`
+- **B8** = `3`
+
+**When** I select **B6:B8**, then choose Insert ▸ Chart, then click **B6**, then undo
+
+**Then**
+- no chart exists
+
+#### A chart is still on screen after a save and reload, not just still in the document
+
+**Given**
+- **B6** = `1`
+- **B7** = `2`
+- **B8** = `3`
+
+**When** I select **B6:B8**, then choose Insert ▸ Chart, then save, reload the page, and load the workbook back
+
+**Then**
+- `[data-testid="chart-canvas"]` is visible
+- `[data-testid="chart-panel-range"]` is visible containing "B6:B8"
+- the chart is sourceRange=`"B6:B8"`, type=`"column"`
+
+#### An insert through the chart.insert operation puts the chart on screen, not just in the document
+
+**Given**
+- **B6** = `1`
+- **B7** = `2`
+- **B8** = `3`
+
+**When** I select **B6:B8**, then choose Insert ▸ Chart
+
+**Then**
+- `[data-testid="chart-canvas"]` is visible
+
+#### A chart arriving after the panel was closed re-opens it — a dismissal cannot suppress a chart that did not exist yet
+
+**Given**
+- **B6** = `1`
+- **B7** = `2`
+- **B8** = `3`
+- **D2** = `5`
+- **D3** = `9`
+- **D4** = `4`
+
+**When** I select **B6:B8**, then choose Insert ▸ Chart, then click **chart-panel-close**, then select **D2:D4**, then choose Insert ▸ Chart
+
+**Then**
+- `[data-testid="chart-canvas"]` is visible
+- `[data-testid="chart-panel-range"]` is visible containing "D2:D4"
+
+#### Closing another docked panel returns the slot to the chart, not to nothing
+
+**Given**
+- **B6** = `1`
+- **B7** = `2`
+- **B8** = `3`
+
+**When** I select **B6:B8**, then choose Insert ▸ Chart, then click **toolbar-history**, then click **history-panel-close**
+
+**Then**
+- `[data-testid="chart-canvas"]` is visible
+
+#### Closing the chart panel hides the chart without removing it from the document
+
+**Given**
+- **B6** = `1`
+- **B7** = `2`
+- **B8** = `3`
+
+**When** I select **B6:B8**, then choose Insert ▸ Chart, then click **chart-panel-close**
+
+**Then**
+- `[data-testid="chart-canvas"]` is hidden
+- the chart is sourceRange=`"B6:B8"`
+
+#### Unchecking Show chart survives a reload — the chart stays hidden, it does not silently come back
+
+**Given**
+- **B6** = `1`
+- **B7** = `2`
+- **B8** = `3`
+
+**When** I select **B6:B8**, then choose Insert ▸ Chart, then click menu item **view.chart**, then wait 1200ms, then reload the page
+
+**Then**
+- `[data-testid="chart-canvas"]` is hidden
+- the chart is sourceRange=`"B6:B8"`
+
+#### Escape that cancels a cell edit leaves the chart panel open — one Escape belongs to one handler
+
+**Given**
+- **B6** = `1`
+- **B7** = `2`
+- **B8** = `3`
+
+**When** I select **B6:B8**, then choose Insert ▸ Chart, then double-click **E10**, then press `h`, then press `Escape`, then wait 600ms
+
+**Then**
+- **E10** shows `null`
+- `[data-testid="chart-canvas"]` is visible
+
+#### View ▸ Show ▸ Show chart brings a closed chart back
+
+**Given**
+- **B6** = `1`
+- **B7** = `2`
+- **B8** = `3`
+
+**When** I select **B6:B8**, then choose Insert ▸ Chart, then click **chart-panel-close**, then click menu item **view.chart**
+
+**Then**
+- `[data-testid="chart-canvas"]` is visible
+
+#### Insert with an empty (single-cell) selection does not create a chart
+
+**Given**
+- **B6** = `1`
+
+**When** I select **E20:E20**, then choose Insert ▸ Chart
+
+**Then**
+- no chart exists
diff --git a/content/docs/learn/playground/clear-paint-format.mdx b/content/docs/learn/playground/clear-paint-format.mdx
index 1ac1c26..5d34a6f 100644
--- a/content/docs/learn/playground/clear-paint-format.mdx
+++ b/content/docs/learn/playground/clear-paint-format.mdx
@@ -18,7 +18,7 @@ Applying **replaces** the target's format wholesale, the same way Sheets does: i
## Verification
-The scenario runner can drive Ctrl+\\ end-to-end (a real keyboard chord), so clear-formatting's scenarios above are fully automated. Paint format's arm/apply gesture is a toolbar click followed by a click on a DIFFERENT target cell — the runner has no verb for driving a toolbar action button yet (the same gap `format-fill-color.mdx` documents for its color swatch), so it's browser-verified instead: arming via a single click on the roller shows the pressed toolbar state and the source range gets a dashed "copying" outline; clicking a target cell applies the source's format, replaces the target's own prior formatting, and the roller un-presses itself (one-shot); double-clicking the roller instead keeps it pressed across repeated target clicks until Escape.
+The scenario runner can drive Ctrl+\\ end-to-end (a real keyboard chord), so clear-formatting's scenarios above are fully automated. Paint format's arm/apply gesture is a toolbar click followed by a click on a DIFFERENT target cell — the runner has no verb for driving a toolbar action button yet (the same gap the fill-color feature documents for its color swatch), so it's browser-verified instead: arming via a single click on the roller shows the pressed toolbar state and the source range gets a dashed "copying" outline; clicking a target cell applies the source's format, replaces the target's own prior formatting, and the roller un-presses itself (one-shot); double-clicking the roller instead keeps it pressed across repeated target clicks until Escape.
## Verified behavior
diff --git a/content/docs/learn/playground/column-row-resize.mdx b/content/docs/learn/playground/column-row-resize.mdx
index 3b02f7a..bf547e5 100644
--- a/content/docs/learn/playground/column-row-resize.mdx
+++ b/content/docs/learn/playground/column-row-resize.mdx
@@ -17,7 +17,7 @@ description: "Dragging a column/row header border resizes it; double-clicking a
## Verified behavior
-_Matches Google Sheets:_ In Google Sheets, dragging the border between two column-letter headers (or row-number headers) resizes the column/row to the left (or row above) of the dragged border, with a live vertical/horizontal guide line while dragging. Double-clicking that same border auto-fits the column/row to the widest (or tallest) NON-EMPTY cell's rendered content in it, plus a small padding margin; double-clicking an empty column/row's border is a no-op (it stays/reverts to the default width/height). Selecting multiple columns (via a shift/drag multi-column header selection) or row headers, or selecting the whole sheet (the top-left corner box), then dragging or double-clicking ANY ONE of the selected borders resizes/auto-fits every selected column/row together — a drag applies the SAME width/height to all of them, while an auto-fit sizes EACH one independently to its own widest content (so a multi-column auto-fit can produce different final widths per column). A resized column/row is an explicit override that persists with the file (reload / re-share shows the same widths) and is exported with the sheet. This is long-standing, widely-documented Sheets/Excel behavior, not something ambiguous enough to need live verification against the reference workbook; a follow-up can add the `features/column-row-resize.oracle.md` study-sheets brief if a review finds a discrepancy.
+_Matches Google Sheets:_ In Google Sheets, dragging the border between two column-letter headers (or row-number headers) resizes the column/row to the left (or row above) of the dragged border, with a live vertical/horizontal guide line while dragging. Double-clicking that same border auto-fits the column/row to the widest (or tallest) NON-EMPTY cell's rendered content in it, plus a small padding margin; double-clicking an empty column/row's border is a no-op (it stays/reverts to the default width/height). Selecting multiple columns (via a shift/drag multi-column header selection) or row headers, or selecting the whole sheet (the top-left corner box), then dragging or double-clicking ANY ONE of the selected borders resizes/auto-fits every selected column/row together — a drag applies the SAME width/height to all of them, while an auto-fit sizes EACH one independently to its own widest content (so a multi-column auto-fit can produce different final widths per column). A resized column/row is an explicit override that persists with the file (reload / re-share shows the same widths) and is exported with the sheet. This is long-standing, widely-documented Sheets/Excel behavior, not something ambiguous enough to need live verification against the reference workbook; a follow-up can add this feature's own oracle brief if a review finds a discrepancy.
#### Dragging a column border sets an explicit width that persists across save and reload
diff --git a/content/docs/learn/playground/command-palette.mdx b/content/docs/learn/playground/command-palette.mdx
new file mode 100644
index 0000000..4bf4e1c
--- /dev/null
+++ b/content/docs/learn/playground/command-palette.mdx
@@ -0,0 +1,107 @@
+---
+title: "Command palette — search the menus"
+description: "Alt+/ (Option+/ on Mac) opens a fuzzy-find search box over every active menu command; typing filters live, and picking a result runs it through the exact same seam the menu bar itself uses."
+---
+
+{/* GENERATED — do not edit by hand */}
+
+## What it does
+
+Press **Alt+/** (**Option+/** on Mac) anywhere in the grid to open a search
+box over every active menu command — Google Sheets' own "Search the menus"
+chord. Type to filter live (a plain, case-insensitive substring match on the
+command's label or its menu breadcrumb, e.g. "Format > Text"); the top match
+is pre-highlighted, so **Enter** runs it immediately. Arrow keys move the
+highlight, clicking a row runs that one instead, and **Escape** closes the
+box and returns focus to the grid.
+
+A not-yet-built or account-gated command still appears in results — Studio's
+established visible-roadmap convention, matching how the menu bar itself
+never hides a command, only greys it — but picking one does nothing (same
+`aria-disabled` pattern the menu bar's own rows use).
+
+## Try it
+
+
+
+## Verified behavior
+
+_Matches Google Sheets:_ Google Sheets binds Option+/ (Alt+/) to "Search the menus": an inline box docked at the toolbar's top-left, with live substring filtering (typing "bold" matches "Bold" instantly), a pre-highlighted top result that Enter runs immediately (verified live: running "Bold" through the box flipped the toolbar's own Bold button to active for the selected cell), and Escape to close. Its real match surface is broader than menus alone (it also finds spreadsheet FUNCTION names whose text contains the query, e.g. "insert row" surfaces the GROWTH function) and shows a personalized empty-query suggestion plus a pinned "Find in document" action — see this feature's oracle brief for the full observed behavior and the specific, documented divergences Studio's implementation makes from it (centered overlay instead of an inline toolbar-docked box; menu commands only, not spreadsheet functions; no personalized suggestions or pinned find action, since Studio has neither a usage-history store nor a built find-in-document feature yet).
+
+#### Alt+/ opens the palette, focused and empty
+
+**Given**
+
+**When** I click **A1**, then press `Alt+/`
+
+**Then**
+- `[data-testid="command-palette"]` is visible
+
+#### Typing filters live to matching commands, showing their menu breadcrumb
+
+**Given**
+
+**When** I click **A1**, then press `Alt+/`, then type `bold`
+
+**Then**
+- `[data-testid="command-palette-item-format.bold"]` is visible containing "Format > Text"
+
+#### Enter on the pre-highlighted top result runs the SAME command the toolbar/menu use, and closes the palette
+
+**Given**
+- **A1** = `hello`
+
+**When** I click **A1**, then press `Alt+/`, then type `bold`, then press `Enter`
+
+**Then**
+- **A1** is bold
+- `[data-testid="command-palette"]` is hidden
+
+#### Clicking a result runs it too, not just Enter
+
+**Given**
+- **A1** = `hello`
+
+**When** I click **A1**, then press `Alt+/`, then type `italic`, then click **command-palette-item-format.italic**
+
+**Then**
+- **A1** is italic
+- `[data-testid="command-palette"]` is hidden
+
+#### A disabled item (Insert > Chart, locked in try mode) still appears — visible roadmap — but Enter on it does nothing
+
+**Given**
+
+**When** I click **A1**, then press `Alt+/`, then type `chart`, then press `Enter`
+
+**Then**
+- `[data-testid="command-palette-item-insert.chart"]` is visible
+- `[data-testid="command-palette"]` is visible
+
+#### No match shows a plain "no results" message, not a broken empty list
+
+**Given**
+
+**When** I click **A1**, then press `Alt+/`, then type `zzzxyq123nomatch`
+
+**Then**
+- `[data-testid="command-palette"]` is visible containing "No matching commands"
+
+#### Escape closes the palette and returns focus to the grid
+
+**Given**
+
+**When** I click **A1**, then press `Alt+/`, then see `[data-testid="command-palette"]` on screen, then press `Escape`, then press `b`, then press `Enter`
+
+**Then**
+- **A1** shows `b`
+- `[data-testid="command-palette"]` is hidden
+
+#### File's actions are out of scope for this first pass — searching "import" finds nothing
+
+**Given**
+
+**When** I click **A1**, then press `Alt+/`, then type `import`
+
+**Then**
+- `[data-testid="command-palette"]` is visible containing "No matching commands"
diff --git a/content/docs/learn/playground/conditional-formatting.mdx b/content/docs/learn/playground/conditional-formatting.mdx
new file mode 100644
index 0000000..97a7f2e
--- /dev/null
+++ b/content/docs/learn/playground/conditional-formatting.mdx
@@ -0,0 +1,148 @@
+---
+title: "Conditional formatting (daily-driver slice)"
+description: "A single-condition value-comparison rule or a 2-stop color scale, applied over a range from the Format ▸ Conditional formatting dialog, colors matching cells live (tracking edits) without touching their value/formula; a rule undoes/redoes like any edit. The dialog ITSELF previews every field edit on the grid immediately, before Done — Done only finalizes it as one undoable command, and Cancel discards the live preview entirely with no trace."
+---
+
+{/* GENERATED — do not edit by hand */}
+
+## What it does
+
+Select a range and open **Format ▸ Conditional formatting** to color-code cells by their own value, the way Google Sheets does — this daily-driver slice covers the two most common shapes: a **single-condition rule** ("format cells if greater than / less than / equal to / between a value, in a chosen fill color") and a **color scale** (a 2-stop gradient from a min color to a max color across the range's own numbers). Pick a condition and a value (and a second value for "Is between"/"Is not between"), or switch to the Color scale tab — **every change colors the range on the sheet immediately**, exactly like real Google Sheets. **Done** doesn't "apply" anything; it just closes the panel, keeping whatever you last saw on the grid. **Cancel** (or Escape, an outside click, or the panel's own close button) discards the live preview entirely — the grid returns to its exact pre-panel state, with no rule left behind.
+
+A rule is **live**: it re-evaluates every time the sheet recalculates, so a cell that crosses the threshold as a dependent formula changes recolors automatically — nothing is baked in at the moment you click Done. It's **presentation only**: coloring `=A1+B1` tints the cell without touching the formula or its computed value. However many fields you tune before clicking Done, the rule still **undoes/redoes as ONE edit** — Ctrl+Z after Done always fully removes it in a single step, never leaving you partway through your own tuning session.
+
+## Verified behavior
+
+_Matches Google Sheets:_ Google Sheets' Format ▸ Conditional formatting opens a "Conditional format rules" panel with two tabs. The "Single color" tab offers a "Format cells if…" condition dropdown (Is empty/not empty, five text conditions, three date conditions, six numeric-comparison operators — Greater than/Greater than or equal to/Less than/Less than or equal to/Is equal to/Is not equal to, plus Is between/Is not between, and "Custom formula is") and a formatting-style picker (bold/italic/underline/strikethrough/text color/ fill color; default a light green ~#b7e1cd). The "Color scale" tab offers a min/mid/maxpoint picker (default a 2-stop White→Green preset, midpoint "None") with a live gradient preview. Multiple rules can stack over the same or overlapping ranges via "+Add another rule". A rule is grid-owned presentation, re-evaluated against each cell's live value on every recalc (never baked into a static per-cell format), and undoes/redoes like any other edit. This daily-driver slice ships all eight numeric operators (Greater than/Greater than or equal to/Less than/Less than or equal to/Is equal to/Is not equal to/Is between/Is not between), restricts their values to numbers (no text/date/formula conditions), narrows the color scale to 2-stop (matching Sheets' own default, no midpoint), and presents it in a right-docked, non-modal panel matching Sheets' own docked side panel (a fixed, non-resizable width is the one residual chrome gap) — see the oracle's "Accepted divergences" for the full list. Full parity (text/date conditions, custom formula, 3-stop scale, alternating colors, themes) is tracked separately. The panel's OWN fields preview live on the grid before Done — real Google Sheets applies conditional- format highlighting live as the panel's own condition/style/color-scale fields are adjusted; Done just closes the panel, it is not an apply gate, and Cancel discards whatever was previewed with no trace left behind.
+
+#### Opening Format > Conditional formatting shows the Single color tab by default
+
+**Given**
+
+**When** I click menu item **format.conditionalFormatting**
+
+**Then**
+- `[data-testid="conditional-format-dialog"]` is visible
+- `[data-testid="conditional-format-operator"]` is visible
+
+#### A single-condition rule (value > 10) colors a matching cell's fill, leaving its value untouched
+
+**Given**
+- **A1** = `15`
+
+**When** I click **A1**, then click menu item **format.conditionalFormatting**, then fill **conditional-format-value** with `10`, then click **conditional-format-done**
+
+**Then**
+- **A1** shows `15`
+- **A1** renders as fill=#b7e1cd
+
+#### A single-condition rule does NOT color a cell whose value fails the condition
+
+**Given**
+- **A1** = `5`
+
+**When** I click **A1**, then click menu item **format.conditionalFormatting**, then fill **conditional-format-value** with `10`, then click **conditional-format-done**
+
+**Then**
+- **A1** renders as fill=false
+
+#### Conditional formatting is presentation only — a formula's value/formula are untouched
+
+**Given**
+- **A1** = `=5+10`
+
+**When** I click **A1**, then click menu item **format.conditionalFormatting**, then fill **conditional-format-value** with `10`, then click **conditional-format-done**
+
+**Then**
+- **A1** shows `15`
+- **A1** has formula `=5+10`
+- **A1** renders as fill=#b7e1cd
+
+#### Undo removes the rule's effect; redo reapplies it
+
+**Given**
+- **A1** = `15`
+
+**When** I click **A1**, then click menu item **format.conditionalFormatting**, then fill **conditional-format-value** with `10`, then click **conditional-format-done**, then click **A1**, then undo
+
+**Then**
+- **A1** renders as fill=false
+
+#### Adjusting the value previews the fill LIVE on the grid, BEFORE Done is clicked
+
+**Given**
+- **A1** = `15`
+
+**When** I click **A1**, then click menu item **format.conditionalFormatting**, then fill **conditional-format-value** with `10`
+
+**Then**
+- **A1** renders as fill=#b7e1cd
+
+#### Switching to the Color scale tab previews IT live too, before Done
+
+**Given**
+- **A1** = `0`
+- **A2** = `10`
+
+**When** I select **A1:A2**, then click menu item **format.conditionalFormatting**, then click **conditional-format-tab-color-scale**
+
+**Then**
+- **A1** renders as fill=#ffffff
+- **A2** renders as fill=#57bb8a
+
+#### Cancel discards the live-previewed rule entirely — the grid returns to its pre-panel state
+
+**Given**
+- **A1** = `15`
+
+**When** I click **A1**, then click menu item **format.conditionalFormatting**, then fill **conditional-format-value** with `10`, then click **conditional-format-cancel**
+
+**Then**
+- **A1** renders as fill=false
+
+#### Done after several live-adjusted values still commits cleanly — undo fully reverts in ONE step, not partway
+
+**Given**
+- **A1** = `15`
+
+**When** I click **A1**, then click menu item **format.conditionalFormatting**, then fill **conditional-format-value** with `5`, then fill **conditional-format-value** with `20`, then fill **conditional-format-value** with `10`, then click **conditional-format-done**, then click **A1**, then undo
+
+**Then**
+- **A1** renders as fill=false
+
+#### A color-scale rule paints the range's own min/max values at its two endpoint colors
+
+**Given**
+- **A1** = `0`
+- **A2** = `5`
+- **A3** = `10`
+
+**When** I select **A1:A3**, then click menu item **format.conditionalFormatting**, then click **conditional-format-tab-color-scale**, then click **conditional-format-done**
+
+**Then**
+- **A1** renders as fill=#ffffff
+- **A3** renders as fill=#57bb8a
+
+#### A color-scale rule never affects a cell OUTSIDE its range
+
+**Given**
+- **A1** = `0`
+- **A2** = `10`
+- **B1** = `999`
+
+**When** I select **A1:A2**, then click menu item **format.conditionalFormatting**, then click **conditional-format-tab-color-scale**, then click **conditional-format-done**
+
+**Then**
+- **B1** renders as fill=false
+
+#### Is between evaluates inclusive, order-independent bounds via the operator dropdown
+
+**Given**
+- **A1** = `5`
+- **A2** = `15`
+
+**When** I select **A1:A2**, then click menu item **format.conditionalFormatting**, then choose `between` in **conditional-format-operator**, then fill **conditional-format-value** with `10`, then fill **conditional-format-value2** with `1`, then click **conditional-format-done**
+
+**Then**
+- **A1** renders as fill=#b7e1cd
+- **A2** renders as fill=false
diff --git a/content/docs/learn/playground/date-parsing.mdx b/content/docs/learn/playground/date-parsing.mdx
index 871d33c..f1bd74a 100644
--- a/content/docs/learn/playground/date-parsing.mdx
+++ b/content/docs/learn/playground/date-parsing.mdx
@@ -24,7 +24,7 @@ text too.
## Verified behavior
-_Matches Google Sheets:_ In Google Sheets, typing a recognized date literal creates a REAL date, not text: the cell stores a date serial (days since the 1899-12-30 epoch; 2026-07-15 = 46218) and Sheets AUTO-APPLIES a date number format on recognition, so it displays as a date (M/D/YYYY in this US-locale workbook) and is right-aligned. Because the value is a serial, `=+1` gives the next day. Editing the cell (or reading the formula bar) shows the DATE (7/15/2026), never the raw serial — and because that date string is an accepted input, committing it unchanged re-parses to the same serial (round-trip). Only dates are shown formatted while editing; other number formats show the raw number (you edit 1234.5, not $1,234.50). The accepted formats (US M/D/Y locale) include M/D/YYYY, M/D/YY, YYYY-MM-DD, YYYY/MM/DD, Mon D[,] YYYY, Month D[,] YYYY, and D-Mon-YYYY — all mapping 2026-07-15 to serial 46218. Validity is enforced (2/29/2024 is a date; 2/29/2026 and 13/13/2026 are not, and stay text), and the month comes first (15/7/2026 is not a date in this locale). A lone month name (Jan), ordinary text, and a bare number (46218 is already numeric) are NOT auto-converted. This is the input-parsing companion to, which rendered a serial as a date but did nothing to typed text. Separately, a Date-TYPED engine value — =TODAY, =DATE(y,m,d), or date arithmetic that stays a date like =DATE(...)+1 — displays as a date automatically, with no explicit numberFormat, because the engine owns the date-ness (Sheets: arithmetic on a date stays a date). (A known gap: `=+1` currently shows the serial, because Studio stores a typed date as a plain-number serial rather than a Date-typed value — the engine has no date-typed literal input yet; tracked as a core follow-up.)
+_Matches Google Sheets:_ In Google Sheets, typing a recognized date literal creates a REAL date, not text: the cell stores a date serial (days since the 1899-12-30 epoch; 2026-07-15 = 46218) and Sheets AUTO-APPLIES a date number format on recognition, so it displays as a date (M/D/YYYY in this US-locale workbook) and is right-aligned. Because the value is a serial, `=+1` gives the next day. Editing the cell (or reading the formula bar) shows the DATE (7/15/2026), never the raw serial — and because that date string is an accepted input, committing it unchanged re-parses to the same serial (round-trip). Only dates are shown formatted while editing; other number formats show the raw number (you edit 1234.5, not $1,234.50). The accepted formats (US M/D/Y locale) include M/D/YYYY, M/D/YY, YYYY-MM-DD, YYYY/MM/DD, Mon D[,] YYYY, Month D[,] YYYY, and D-Mon-YYYY — all mapping 2026-07-15 to serial 46218. Validity is enforced (2/29/2024 is a date; 2/29/2026 and 13/13/2026 are not, and stay text), and the month comes first (15/7/2026 is not a date in this locale). A lone month name (Jan), ordinary text, and a bare number (46218 is already numeric) are NOT auto-converted. This is the input-parsing companion to date display, which rendered a serial as a date but did nothing to typed text. Separately, a Date-TYPED engine value — =TODAY(), =DATE(y,m,d), or date arithmetic that stays a date like =DATE(...)+1 — displays as a date automatically, with no explicit numberFormat, because the engine owns the date-ness (Sheets: arithmetic on a date stays a date). (A known gap: `=+1` currently shows the serial, because Studio stores a typed date as a plain-number serial rather than a Date-typed value — the engine has no date-typed literal input yet; tracked as a core follow-up.)
#### Typing M/D/YYYY stores a serial and auto-applies a date format
@@ -106,7 +106,7 @@ _Matches Google Sheets:_ In Google Sheets, typing a recognized date literal crea
- **A1** shows `46218`
- **A1** is numberFormat=date
-#### A Date-typed formula result (=DATE+1) stays a date the ValueFormatter shows automatically
+#### A Date-typed formula result (=DATE()+1) stays a date the ValueFormatter shows automatically
**Given**
diff --git a/content/docs/learn/playground/date-picker.mdx b/content/docs/learn/playground/date-picker.mdx
index e779c63..32feb1e 100644
--- a/content/docs/learn/playground/date-picker.mdx
+++ b/content/docs/learn/playground/date-picker.mdx
@@ -25,7 +25,7 @@ opens the normal in-cell editor, never the picker.
## Verified behavior
-_Matches Google Sheets:_ In Google Sheets, double-clicking a DATE-formatted cell opens a small CALENDAR popover anchored at the cell, showing the month of the cell's current date with that day selected (an empty date cell opens on the current month with nothing selected). Picking a day writes that date into the cell: because the cell is a real date, the stored value is the date SERIAL (days since the 1899-12-30 epoch; 2026-07-15 = 46218) and the cell keeps its date number format, so it still displays as M/D/YYYY (US locale) and `=+1` still gives the next day — real calc, not text. The popover has month and year navigation (a header showing the displayed Month Year) and is keyboard-accessible: arrow keys move the highlighted day (left/right by a day, up/down by a week, crossing month boundaries), Enter picks the highlighted day, and Escape closes WITHOUT committing (the cell keeps its prior value). A NON-date cell double-click opens the normal in-cell editor instead — the picker is specific to date cells, and the picker and the inline editor are never open at the same time. The value ground truth is anchored to the same engine DATEVALUE authority as (7/15/2026 = 46218, 7/16 = 46219, 7/20 = 46223, 7/22 = 46225): a picked day is written as a US M/D/YYYY literal, which re-parses to the serial through the existing date-literal coercion. This is the visual, faster companion to (typing a date) and (rendering a date).
+_Matches Google Sheets:_ In Google Sheets, double-clicking a DATE-formatted cell opens a small CALENDAR popover anchored at the cell, showing the month of the cell's current date with that day selected (an empty date cell opens on the current month with nothing selected). Picking a day writes that date into the cell: because the cell is a real date, the stored value is the date SERIAL (days since the 1899-12-30 epoch; 2026-07-15 = 46218) and the cell keeps its date number format, so it still displays as M/D/YYYY (US locale) and `=| +1` still gives the next day — real calc, not text. The popover has month and year navigation (a header showing the displayed Month Year) and is keyboard-accessible: arrow keys move the highlighted day (left/right by a day, up/down by a week, crossing month boundaries), Enter picks the highlighted day, and Escape closes WITHOUT committing (the cell keeps its prior value). A NON-date cell double-click opens the normal in-cell editor instead — the picker is specific to date cells, and the picker and the inline editor are never open at the same time. The value ground truth is anchored to the same engine DATEVALUE authority as typing a date (7/15/2026 = 46218, 7/16 = 46219, 7/20 = 46223, 7/22 = 46225): a picked day is written as a US M/D/YYYY literal, which re-parses to the serial through the existing date-literal coercion. This is the visual, faster companion to typing a date and to rendering one.
#### Double-clicking a date cell opens the calendar popover
@@ -101,7 +101,7 @@ _Matches Google Sheets:_ In Google Sheets, double-clicking a DATE-formatted cell
- **A1** shows `46218`
- `[data-slot="date-picker"]` is hidden
-#### A non-date cell double-click does not open the picker
+#### A non-date cell double-click opens the plain inline editor, not the picker
**Given**
- **A1** = `hello`
@@ -109,4 +109,5 @@ _Matches Google Sheets:_ In Google Sheets, double-clicking a DATE-formatted cell
**When** I double-click **A1**
**Then**
+- `[data-testid="cell-editor"]` is visible with value "hello"
- `[data-slot="date-picker"]` is hidden
diff --git a/content/docs/learn/playground/display-formatting.mdx b/content/docs/learn/playground/display-formatting.mdx
index db983e0..c4c7078 100644
--- a/content/docs/learn/playground/display-formatting.mdx
+++ b/content/docs/learn/playground/display-formatting.mdx
@@ -14,8 +14,8 @@ for a date + time (`46218.5` → `7/15/2026 12:00:00`) — or use the toolbar's
**$ · % · decrease/increase-decimals · 123 · date · date-time** group. Re-applying an
A value that carries a time-of-day reads as a date + time **automatically**, no format
-needed: `=NOW` comes back from the engine as a Date-typed fractional serial and shows
-as `7/15/2026 0:39:39` (Google Sheets' default), while a whole-day date like `=TODAY`
+needed: `=NOW()` comes back from the engine as a Date-typed fractional serial and shows
+as `7/15/2026 0:39:39` (Google Sheets' default), while a whole-day date like `=TODAY()`
shows as just `7/15/2026`.
active category (or clicking its pressed button) reverts to automatic.
@@ -26,7 +26,7 @@ untouched — only the display changes. Errors always show their code clearly (`
## Verified behavior
-_Matches Google Sheets:_ A cell's number format is grid-owned PRESENTATION: it changes only the displayed string, never the stored value or formula (the formula bar still shows the raw value, and the format undoes/redoes like any edit). Applying currency to 1234.5 shows $1,234.50; to -42 shows -$42.00 (Google Sheets puts the minus before the $, not accounting parentheses). Percent scales by 100 and suffixes % with two decimals and no thousands grouping, so 0.1257 shows 12.57%. The Number format is thousands-grouped with two decimals (1,234.50). A date format renders a serial as M/D/YYYY (serial 45852 → 7/14/2025; Sheets' serial epoch is 1899-12-30). A datetime format renders a serial as M/D/YYYY H:MM:SS (the fraction is the time of day: 46218.5 → 7/15/2026 12:00:00; the whole-day 46218 → 7/15/2026 0:00:00). Google Sheets also DEFAULTS a value that carries a time-of-day to a datetime display: =NOW returns a Date-typed fractional serial and reads as date + time out of the box, while a whole-day date (=TODAY) reads as just the date. Increase/ decrease-decimals add or remove one decimal place from the current number format. Re-applying an already-active category clears it back to automatic. In Google Sheets the shortcuts are Number Ctrl+Shift+1, Date Ctrl+Shift+3, Currency Ctrl+Shift+4, Percent Ctrl+Shift+5, Time Ctrl+Shift+2 (Studio's datetime format takes that slot); decrease/increase-decimals are toolbar-only. A formula error shows its code clearly (#DIV/0!, #REF!, #VALUE!, …), regardless of any number format.
+_Matches Google Sheets:_ A cell's number format is grid-owned PRESENTATION: it changes only the displayed string, never the stored value or formula (the formula bar still shows the raw value, and the format undoes/redoes like any edit). Applying currency to 1234.5 shows $1,234.50; to -42 shows -$42.00 (Google Sheets puts the minus before the $, not accounting parentheses). Percent scales by 100 and suffixes % with two decimals and no thousands grouping, so 0.1257 shows 12.57%. The Number format is thousands-grouped with two decimals (1,234.50). A date format renders a serial as M/D/YYYY (serial 45852 → 7/14/2025; Sheets' serial epoch is 1899-12-30). A datetime format renders a serial as M/D/YYYY H:MM:SS (the fraction is the time of day: 46218.5 → 7/15/2026 12:00:00; the whole-day 46218 → 7/15/2026 0:00:00). Google Sheets also DEFAULTS a value that carries a time-of-day to a datetime display: =NOW() returns a Date-typed fractional serial and reads as date + time out of the box, while a whole-day date (=TODAY()) reads as just the date. Increase/ decrease-decimals add or remove one decimal place from the current number format. Re-applying an already-active category clears it back to automatic. In Google Sheets the shortcuts are Number Ctrl+Shift+1, Date Ctrl+Shift+3, Currency Ctrl+Shift+4, Percent Ctrl+Shift+5, Time Ctrl+Shift+2 (Studio's datetime format takes that slot); decrease/increase-decimals are toolbar-only. A formula error shows its code clearly (#DIV/0!, #REF!, #VALUE!, …), regardless of any number format.
#### Currency format is applied without changing the value
@@ -83,17 +83,17 @@ _Matches Google Sheets:_ A cell's number format is grid-owned PRESENTATION: it c
- **A1** shows `46218.5`
- **A1** is numberFormat=datetime
-#### NOW is a Date-typed serial anchored to today (its time-of-day fraction reads as a datetime)
+#### NOW() is a Date-typed serial anchored to today (its time-of-day fraction reads as a datetime)
**Given**
-- **A1** = `=NOW`
-- **B1** = `=INT(A1)=TODAY`
+- **A1** = `=NOW()`
+- **B1** = `=INT(A1)=TODAY()`
**When** I click **A1**
**Then**
- **B1** shows `true`
-- **A1** has formula `=NOW`
+- **A1** has formula `=NOW()`
#### Re-applying the same category clears it back to automatic
diff --git a/content/docs/learn/playground/error-tooltip.mdx b/content/docs/learn/playground/error-tooltip.mdx
index fc16186..02f9d37 100644
--- a/content/docs/learn/playground/error-tooltip.mdx
+++ b/content/docs/learn/playground/error-tooltip.mdx
@@ -15,7 +15,7 @@ When a cell resolves to an error — `#REF!`, `#DIV/0!`, `#NAME?`, `#VALUE!`, `#
## Verified behavior
-_Matches Google Sheets:_ In Google Sheets, a cell that resolves to an error (#REF!, #DIV/0!, #NAME?, #VALUE!, #NUM!, #N/A, …) shows a small red-accented tooltip with an "Error" header and a plain-language diagnostic line — both when you hover the cell and when it's the active selection. The tooltip tracks the cell it's anchored to and disappears when you move the mouse away or scroll; a non-error cell never shows one. TrueCalc's engine (@truecalc/workbook) does not yet emit a rich per-error diagnostic message (core#731, itself blocked upstream on the Google Sheets conformance-fixtures pipeline capturing real error-message strings, which hasn't landed yet) — so this feature ships with a FALLBACK per-code message table (the six codes the engine currently surfaces) rather than the engine's own wording, and is structured to prefer an engine-supplied message the moment one exists (see `domain/error-message.ts`).
+_Matches Google Sheets:_ In Google Sheets, a cell that resolves to an error (#REF!, #DIV/0!, #NAME?, #VALUE!, #NUM!, #N/A, …) shows a small red-accented tooltip with an "Error" header and a plain-language diagnostic line — both when you hover the cell and when it's the active selection. The tooltip tracks the cell it's anchored to and disappears when you move the mouse away or scroll; a non-error cell never shows one. TrueCalc's engine (@truecalc/workbook) does not yet emit a rich per-error diagnostic message — tracked upstream as truecalc/core#731 — so this feature ships with a FALLBACK per-code message table (the six codes the engine currently surfaces) rather than the engine's own wording, and is structured to prefer an engine-supplied message the moment one exists.
#### A
diff --git a/content/docs/learn/playground/explain-result.mdx b/content/docs/learn/playground/explain-result.mdx
index 79592c2..4480243 100644
--- a/content/docs/learn/playground/explain-result.mdx
+++ b/content/docs/learn/playground/explain-result.mdx
@@ -15,13 +15,13 @@ The stage is an **animated node graph**: the formula's leaf inputs sit on the bo
**The INPUT frame lists resolved references cleanly.** Rather than one run-on line mixing cell references and bare literals, the INPUT frame shows a short framing line plus a chip per resolved cell/range reference (e.g. `Inputs!B1 = 0.05`, `Inputs!B4:B6 = {100, 200, 300}`) — bare literals are left out of this summary (they're already visible in the formula text and their own leaf node).
-**Explain is LIVE — any formula, not just two demos.** Right-clicking ANY formula cell calls the real trace producer (`@truecalc/pro`'s `trace_cell_at`, a client WASM package vendored into this repo — see `packages/core/vendor/truecalc-pro/VENDORED-FROM.md`) against the workbook's CURRENT state, reusing the exact `(timestamp_ms, timezone, rng_seed)` the engine used for its last on-screen recalc (`EngineAdapter.lastRecalcContext`) so a volatile cell explains against what's actually on screen. Three distinct, honest non-graph states replace the old "two demo formulas" placeholder: an empty/non-formula cell shows "nothing to explain" (`explain-no-formula`); a cell caught in a circular reference shows a dedicated circular-reference message (`explain-cycle` — core's own `Workbook::trace_cell` short-circuits a cycle member before ever invoking the trace hook, the same wire signature as "no formula", so Studio disambiguates the two using its OWN knowledge of whether the cell has a stored formula at all); and a genuine trace failure surfaces its raw message (`explain-trace-error`) rather than crashing.
+**Explain is LIVE — any formula, not just two demos.** Right-clicking ANY formula cell calls the real engine trace producer (`@truecalc/pro`'s `trace_cell_at`, a client WASM package) against the workbook's CURRENT state, reusing the exact `(timestamp_ms, timezone, rng_seed)` the engine used for its last on-screen recalc (`EngineAdapter.lastRecalcContext()`) so a volatile cell explains against what's actually on screen. Three distinct, honest non-graph states replace the old "two demo formulas" placeholder: an empty/non-formula cell shows "nothing to explain" (`explain-no-formula`); a cell caught in a circular reference shows a dedicated circular-reference message (`explain-cycle` — core's own `Workbook::trace_cell` short-circuits a cycle member before ever invoking the trace hook, the same wire signature as "no formula", so Studio disambiguates the two using its OWN knowledge of whether the cell has a stored formula at all); and a genuine trace failure surfaces its raw message (`explain-trace-error`) rather than crashing.
-**Formula-token highlighting is wired.** Every `TraceNode` carries a `span` — a byte range into the cell's own formula text, read straight off the real generated trace wire type. As each frame plays, the live node's span slices the formula shown above the stage and highlights that substring (`explain-formula-highlight`), so pressing Play visibly "lights up" the exact token currently being evaluated.
+**Formula-token highlighting is wired.** Every `TraceNode` carries a `span` — a byte range into the cell's own formula text, read straight off the engine's own generated wire type (`@truecalc/pro/bindings/TraceNode` — no hand-maintained type copy exists). As each frame plays, the live node's span slices the formula shown above the stage and highlights that substring (`explain-formula-highlight`), so pressing Play visibly "lights up" the exact token currently being evaluated.
**Respects `prefers-reduced-motion`.** With that OS preference set, the stage skips the animation and opens straight on its end state: every node seen, the result lit. Nothing to wait for, nothing moving unexpectedly.
-**Reopening on a different cell always starts fresh.** Explain is modal now, so the flow is close → right-click a different cell → Explain again, not retarget-while-open. Every open always starts that cell's graph from frame 0 (or, under reduced motion, straight to its own end state) — never a stale mid-animation frame left over from a previous cell, and never a crash if the new trace has fewer steps than a previous one was stepped into (the underlying safety mechanism, `openId`-keyed remounting, still exists — see "How it works" — as insurance against any future code path that could otherwise swap cells on a live-mounted instance).
+**Reopening on a different cell always starts fresh.** Explain is modal now, so the flow is close → right-click a different cell → Explain again, not retarget-while-open. Every open always starts that cell's graph from frame 0 (or, under reduced motion, straight to its own end state) — never a stale mid-animation frame left over from a previous cell, and never a crash if the new trace has fewer steps than a previous one was stepped into (the underlying safety mechanism, `openId`-keyed remounting, still exists — as insurance against any future code path that could otherwise swap cells on a live-mounted instance).
## Try it
@@ -31,7 +31,7 @@ Right-click cell **C1** and choose **Explain**, then press **Play**.
## Verified behavior
-_Matches Google Sheets:_ There is no Google Sheets or Excel feature that produces a step-by-step animated computation graph like this one — Sheets has no "Explain" surface, and Excel's closest analogs (Formula Evaluator's single-step-through modal, Trace Precedents' arrows) don't render an ordered, playable node graph. This feature's ground truth is therefore NOT a Google-Sheets/Excel UI oracle (no `study-sheets` brief backs this manifest, per design D5's own scope for TrueCalc-original UX) — it's the real TrueCalc engine's own evaluation, cross-checked via the `truecalc` MCP `evaluate` tool at authoring time: `0.05/12` → `0.004166666666666667`, `30*12` → `360`, `PMT(0.05/12, 30*12, -500000)` → `2684.108115060699`, and `C1*2` with `C1=30` → `60`. ** (Slice C) wired the REAL trace producer** (`@truecalc/pro`'s `trace_cell_at`) — every scenario below now drives a LIVE trace of whatever the clicked cell actually holds, not a lookup against a hand-authored fixture (an earlier slice's canned fixture data still backs Storybook/unit tests, but the live "Explain" path never reads it). Every scenario runs under `prefers-reduced-motion: reduce` (set once for the whole scenario harness), which the graph itself reads to open straight on its deterministic END STATE (every node seen, the result node live) instead of animating — the same accessibility affordance a real user with that OS preference gets, and the harness's only way to assert the graph's outcome without a Play/Step dance.
+_Matches Google Sheets:_ There is no Google Sheets or Excel feature that produces a step-by-step animated computation graph like this one — Sheets has no "Explain" surface, and Excel's closest analogs (Formula Evaluator's single-step-through modal, Trace Precedents' arrows) don't render an ordered, playable node graph. This feature's ground truth is therefore NOT a Google-Sheets/Excel UI oracle (no oracle brief backs this manifest, per this repo's own scope for TrueCalc-original UX) — it's the real TrueCalc engine's own evaluation, cross-checked via the `truecalc` MCP `evaluate` tool at authoring time: `0.05/12` → `0.004166666666666667`, `30*12` → `360`, `PMT(0.05/12, 30*12, -500000)` → `2684.108115060699`, and `C1*2` with `C1=30` → `60`. **Slice C wired the REAL engine trace producer** (`@truecalc/pro`'s `trace_cell_at`) — every scenario below now drives a LIVE trace of whatever the clicked cell actually holds, not a lookup against a hand-authored fixture (an earlier slice's canned fixture data still backs Storybook/unit tests, but the live "Explain" path never reads it). Every scenario runs under `prefers-reduced-motion: reduce` (set once for the whole scenario harness), which the graph itself reads to open straight on its deterministic END STATE (every node seen, the result node live) instead of animating — the same accessibility affordance a real user with that OS preference gets, and the harness's only way to assert the graph's outcome without a Play/Step dance.
#### Right-click a formula cell and choose Explain shows the LIVE computed graph, references resolved, result live, formula token highlighted
diff --git a/content/docs/learn/playground/file-open.mdx b/content/docs/learn/playground/file-open.mdx
new file mode 100644
index 0000000..649ea36
--- /dev/null
+++ b/content/docs/learn/playground/file-open.mdx
@@ -0,0 +1,41 @@
+---
+title: "File > Open — local document switcher"
+description: "File > Open lists every document in this browser's local registry by name, most-recently-updated first, and lets you switch to a different one — reusing the exact `?doc=` new-tab mechanism File > Make a copy already opens its copies with."
+---
+
+{/* GENERATED — do not edit by hand */}
+
+## What it does
+
+**File > Open** lists every document stored in this browser's local registry — by name, most-recently-updated first — so a document that only ever existed as a one-time "Make a copy" new-tab link can be found and switched to again. The document you're currently on is shown too, marked **Current** instead of an Open button — opening the tab you're already in onto itself would have no useful effect.
+
+Picking a different document's **Open** button switches to it in a new, focused browser tab — reusing the exact same `?doc=` `window.open` mechanism File > Make a copy already opens its copies with (see [make-a-copy](./make-a-copy)), not a second "switch document" mechanism. The tab you clicked from is left open and unchanged, same as Make a copy's own new-tab behavior.
+
+This is local (try-mode) storage only — no account, server, or Drive-equivalent is involved. There is no Sheets-style Recent/Shared/Starred picker chrome to browse, because none of what it browses (a Drive account's files) has a Studio equivalent yet; this is a flat list of what's actually in this ONE browser.
+
+## Verified behavior
+
+_Matches Google Sheets:_ Google Sheets' closest analog is File > Open, which opens a Drive file picker (Recent/Shared with me/Starred, search, thumbnails) scoped to the signed-in account's Drive. Studio has no account, Drive, or sharing concept in try mode at all — the same scope boundary the Make a copy oracle brief's own "Accepted divergences" already established for Make a copy — so there is no Drive picker chrome to replicate; this feature's own oracle brief documents that a live re-verification of Sheets' picker itself was attempted this session and could not be completed (no authenticated Google session was reachable), and flags a follow-up re-check rather than asserting an unverified finding. What IS carried over, and WAS already live-verified (the Make a copy oracle brief, point 3, the same reference workbook): confirming a document-opening action in Sheets opens it in a new, focused browser tab, leaving the tab you acted from unchanged in the background — the exact interaction shape this feature's `window.open` `?doc=` mechanism reuses (the same one File > Make a copy already uses), rather than a same-tab redirect that could silently discard whatever the user was doing.
+
+#### File > Open is available and lists the current document by name
+
+**Given**
+- **A1** = `1`
+
+**When** I click menu item **file.open**
+
+**Then**
+- **A1** shows `1`
+- `[data-menu-item="file.open"]` is visible
+- `[data-testid="open-document-dialog"]` is visible containing "Untitled workbook"
+
+#### Closing the Open dialog leaves the current document untouched
+
+**Given**
+- **A1** = `1`
+
+**When** I click menu item **file.open**, then click **open-document-cancel**
+
+**Then**
+- **A1** shows `1`
+- `[data-testid="open-document-dialog"]` is hidden
diff --git a/content/docs/learn/playground/format-rotation.mdx b/content/docs/learn/playground/format-rotation.mdx
index b5824da..6f1f48e 100644
--- a/content/docs/learn/playground/format-rotation.mdx
+++ b/content/docs/learn/playground/format-rotation.mdx
@@ -32,7 +32,7 @@ formatting**.
## Verified behavior
-_Matches Google Sheets:_ In Google Sheets, the toolbar's "Text rotation" popover offers six presets (None, Tilt up, Tilt down, Stack vertically, Rotate up, Rotate down) plus a free custom-angle input; Tilt up = 45°, Tilt down = -45°, Rotate up = 90°, Rotate down = -90° (verified live — features/format-rotation.oracle.md — by reading the custom-angle dropdown's highlighted value while each preset was active, never self-confirmed). A positive angle pivots the text at the cell's bottom-left corner and reads counter-clockwise (ascending left-to-right for a tilt, bottom-to-top for a 90° rotation); a negative angle pivots at the top-left and reads clockwise. Sheets grows the row to fit the rotated bounding box. "Stack vertically" is a distinct layout (one character per line, no angled baseline at all) — not a point on the angle continuum — and is out of scope for this slice; so is row-height growth for rotation. Rotation is a per-cell presentation attribute like every other CellFormat field: it never touches the value or formula, and undoes/redoes with the command log.
+_Matches Google Sheets:_ In Google Sheets, the toolbar's "Text rotation" popover offers six presets (None, Tilt up, Tilt down, Stack vertically, Rotate up, Rotate down) plus a free custom-angle input; Tilt up = 45°, Tilt down = -45°, Rotate up = 90°, Rotate down = -90° (verified live — see this feature's oracle brief — by reading the custom-angle dropdown's highlighted value while each preset was active, never self-confirmed). A positive angle pivots the text at the cell's bottom-left corner and reads counter-clockwise (ascending left-to-right for a tilt, bottom-to-top for a 90° rotation); a negative angle pivots at the top-left and reads clockwise. Sheets grows the row to fit the rotated bounding box. "Stack vertically" is a distinct layout (one character per line, no angled baseline at all) — not a point on the angle continuum — and is out of scope for this slice; so is row-height growth for rotation. Rotation is a per-cell presentation attribute like every other CellFormat field: it never touches the value or formula, and undoes/redoes with the command log.
#### Tilt up sets a 45° rotation (Format menu)
diff --git a/content/docs/learn/playground/formula-range-preview.mdx b/content/docs/learn/playground/formula-range-preview.mdx
new file mode 100644
index 0000000..bb5bf6c
--- /dev/null
+++ b/content/docs/learn/playground/formula-range-preview.mdx
@@ -0,0 +1,194 @@
+---
+title: "Formula range-select live-preview bubble"
+description: "While a formula is open and a multi-cell range is selected as its argument — drag-selected with the mouse, OR shift-clicked/shift-arrowed with the keyboard (e.g. mid-typing `=SUM(` and pressing Shift+Down repeatedly to grow D3..D6) — a small floating, read-only chip near the selection shows the live-computed aggregate MATCHING THE ENCLOSING FUNCTION (SUM/PRODUCT/AVERAGE/COUNT/COUNTA/MIN/MAX) of the numeric cells being selected, labeled with which operation it is; no chip for a function outside that list. Never affects the formula text itself, and clears itself on commit or cancel."
+---
+
+{/* GENERATED — do not edit by hand */}
+
+## What it does
+
+Start a formula — type `=` and a function name, e.g. `=SUM(` or `=PRODUCT(`
+— and instead of referencing one cell, select a **range** to supply it as
+the argument, either by **dragging** across it with the mouse or by
+**pressing Shift+Arrow** repeatedly with the keyboard (e.g. Shift+Down to
+grow the reference one row at a time). A small floating chip appears near
+the selection showing the **live-computed aggregate MATCHING THE ENCLOSING
+FUNCTION** of the numeric cells being selected — `Sum 100` inside `=SUM(`,
+`Product 600` inside `=PRODUCT(` for the same range, and likewise `Average`/
+`Count`/`Min`/`Max` inside `=AVERAGE(`/`=COUNT(` or `=COUNTA(`/`=MIN(`/
+`=MAX(` — updating live as the range grows or shrinks, and labeled with
+which operation it is so a `600` (Product) is never mistaken for a `50`
+(Sum) of the same cells. A function outside that supported list (e.g.
+`=VLOOKUP(`, `=IF(`, `=CONCATENATE(`) shows **no chip at all** — there is no
+single obviously-correct live aggregate for those, and guessing one (always
+falling back to a SUM, as this feature originally — and incorrectly — did)
+would just be a differently-shaped version of showing a misleading number.
+Release the mouse (or stop pressing Shift+Arrow) and the chip stays up so
+you can actually read it — it's purely read-only, with no dismiss control of
+its own. A plain
+**single-cell click**, or a plain (non-shifted) **arrow key**, while typing
+a formula still just inserts/steers that one cell's reference, exactly as
+before — nothing to aggregate over one cell, so no chip appears. Releasing
+Shift and pressing a plain arrow while a keyboard range is growing collapses
+it back to a single cell, one step further from the range's last edge — the
+same shift-then-plain-arrow convention the grid's own (non-formula) range
+selection already uses.
+
+The chip is **presentation only**: whether it's showing or what it says
+never changes the reference actually written into the formula — that's
+still the plain `D3:D6` token the drag inserts. Committing the formula (or
+cancelling it) clears the chip along with the rest of the editing state —
+so does the range collapsing back to a single cell.
+
+## Try it
+
+
+
+## Verified behavior
+
+_Matches Google Sheets:_ In Google Sheets, while a formula is being typed and the caret is at a reference-valid position, selecting a multi-cell range as the argument — by dragging with the mouse, OR by shift-clicking/shift-arrowing with the keyboard — shows a small floating chip near the selection with a live-computed aggregate of the numeric cells being selected MATCHING WHICHEVER FUNCTION THE RANGE IS AN ARGUMENT TO (e.g. "100" inside `=SUM(`, "600" inside `=PRODUCT(` for the same two cells that would sum to 50), dismissible. A plain single-cell click (or a plain, non-shifted arrow key) while typing a formula inserts a reference the same way it always has and shows no chip — there's nothing to aggregate over one cell. The chip is presentation only: it never changes what reference gets inserted into the formula (still the plain `D3:D6` token) and disappears once the formula commits or is cancelled. NOTE (see this feature's oracle brief): this manifest's ground truth is UNVERIFIED against the live Sheets oracle this slice — the oracle study connected to a live Chrome/CDP session but could not reliably drive keyboard-driven cell data entry in that environment (see the oracle brief for the full connection-status writeup); the claims above reflect well-established, uncontroversial prior knowledge of this Sheets behavior, not a pixel-for-pixel re-verification. The owner must confirm against the live oracle before treating this as parity-complete. The keyboard (shift-arrow) half specifically was NEVER live-verified even in this weaker "well-established prior knowledge" sense during the original slice — it was silently unimplemented rather than deliberately deferred (see the oracle's "Accepted divergences" section) — so its exact cell-by-cell growth semantics (which cell the range starts from on the first Shift+Arrow, what a plain arrow does after Shift is released) are this codebase's own reasoned design, mirroring the grid's existing non-formula Shift+Arrow convention (`operations/select`), not a re-derived Sheets pixel-check. The FUNCTION-AWARE aggregate is also this codebase's own reasoned design, not a re-derived Sheets pixel-check: the owner reported the chip always showing a SUM even inside `=PRODUCT(` as a bug against this manifest's OWN stated ground truth ("live-computed aggregate" — never scoped to "always SUM" in the first place), not a new behavior request. MANUAL DISMISS is a deliberate, accepted divergence: Sheets' chip is dismissible via its own close affordance, but this codebase's first implementation of that affordance (an in-pill "×") turned out to be practically unreachable during an active mouse drag — the only path to it crossed grid cells while the mouse button was still held down, which just kept extending the range instead of ever landing a click on the button. The owner's call was to remove the control rather than chase a fix; the chip now relies solely on its non-dismiss lifecycle (commit, cancel, or the range collapsing to a single cell) to disappear.
+
+#### Dragging a numeric range as a formula argument shows the live-preview chip with the SUM, labeled "Sum"
+
+**Given**
+- **D3** = `10`
+- **D4** = `20`
+- **D5** = `30`
+- **D6** = `40`
+
+**When** I click **F1**, then press `=`, then press `S`, then press `U`, then press `M`, then press `(`, then drag from **D3** to **D6**
+
+**Then**
+- `[data-slot="range-preview-chip"]` is visible containing "Sum 100"
+
+#### Committing the formula inserts the plain range reference and the chip disappears
+
+**Given**
+- **D3** = `10`
+- **D4** = `20`
+- **D5** = `30`
+- **D6** = `40`
+
+**When** I click **F1**, then press `=`, then press `S`, then press `U`, then press `M`, then press `(`, then drag from **D3** to **D6**, then press `)`, then press `Enter`
+
+**Then**
+- **F1** shows `100`
+- **F1** has formula `=SUM(D3:D6)`
+- `[data-slot="range-preview-chip"]` is hidden
+
+#### A plain single-cell click while typing a formula shows no chip (unchanged point-mode/click-insert behavior)
+
+**Given**
+- **D3** = `10`
+
+**When** I click **F1**, then press `=`, then press `S`, then press `U`, then press `M`, then press `(`, then click **D3**
+
+**Then**
+- `[data-testid="formula-input"]` is visible with value "=SUM(D3"
+- `[data-slot="range-preview-chip"]` is hidden
+
+#### Dragging UP/LEFT from the anchor cell tracks the chip to the pointer, not a fixed range corner
+
+**Given**
+- **D3** = `10`
+- **D4** = `20`
+- **D5** = `30`
+- **D6** = `40`
+
+**When** I click **F1**, then press `=`, then press `S`, then press `U`, then press `M`, then press `(`, then drag from **D6** to **D3**
+
+**Then**
+- `[data-slot="range-preview-chip"]` is visible containing "100" near the top of the screen (≤260px)
+
+#### An all-text multi-cell range shows no chip — nothing numeric to aggregate
+
+**Given**
+- **B3** = `a`
+- **B4** = `b`
+
+**When** I click **F1**, then press `=`, then press `S`, then press `U`, then press `M`, then press `(`, then drag from **B3** to **B4**
+
+**Then**
+- `[data-testid="formula-input"]` is visible with value "=SUM(B3:B4"
+- `[data-slot="range-preview-chip"]` is hidden
+
+#### Dragging a range inside =PRODUCT( shows the PRODUCT labeled "Product", not the SUM
+
+**Given**
+- **B1** = `20`
+- **C1** = `30`
+
+**When** I click **F1**, then press `=`, then press `P`, then press `R`, then press `O`, then press `D`, then press `U`, then press `C`, then press `T`, then press `(`, then drag from **B1** to **C1**
+
+**Then**
+- `[data-slot="range-preview-chip"]` is visible containing "Product 600"
+
+#### Committing a =PRODUCT( formula from the drag inserts the plain range and computes the real PRODUCT (not the chip's-former hardcoded SUM)
+
+**Given**
+- **B1** = `20`
+- **C1** = `30`
+
+**When** I click **F1**, then press `=`, then press `P`, then press `R`, then press `O`, then press `D`, then press `U`, then press `C`, then press `T`, then press `(`, then drag from **B1** to **C1**, then press `)`, then press `Enter`
+
+**Then**
+- **F1** shows `600`
+- **F1** has formula `=PRODUCT(B1:C1)`
+- `[data-slot="range-preview-chip"]` is hidden
+
+#### Dragging a range inside a function outside the live-preview's supported list (e.g. =CONCATENATE()) shows no chip rather than a guessed aggregate
+
+**Given**
+- **D3** = `10`
+- **D4** = `20`
+- **D5** = `30`
+- **D6** = `40`
+
+**When** I click **F1**, then press `=`, then press `C`, then press `O`, then press `N`, then press `C`, then press `A`, then press `T`, then press `E`, then press `N`, then press `A`, then press `T`, then press `E`, then press `(`, then drag from **D3** to **D6**
+
+**Then**
+- `[data-testid="formula-input"]` is visible with value "=CONCATENATE(D3:D6"
+- `[data-slot="range-preview-chip"]` is hidden
+
+#### Shift+Arrow (keyboard) also grows a range reference and shows the live-preview chip
+
+**Given**
+- **D3** = `10`
+- **D4** = `20`
+- **D5** = `30`
+- **D6** = `40`
+
+**When** I click **D2**, then press `=`, then press `S`, then press `U`, then press `M`, then press `(`, then press `ArrowDown`, then press `Shift+ArrowDown`, then press `Shift+ArrowDown`, then press `Shift+ArrowDown`
+
+**Then**
+- `[data-slot="range-preview-chip"]` is visible containing "100"
+
+#### Committing a Shift+Arrow-built range reference inserts the plain range and the chip disappears
+
+**Given**
+- **D3** = `10`
+- **D4** = `20`
+- **D5** = `30`
+- **D6** = `40`
+
+**When** I click **D2**, then press `=`, then press `S`, then press `U`, then press `M`, then press `(`, then press `ArrowDown`, then press `Shift+ArrowDown`, then press `Shift+ArrowDown`, then press `Shift+ArrowDown`, then press `)`, then press `Enter`
+
+**Then**
+- **D2** shows `100`
+- **D2** has formula `=SUM(D3:D6)`
+- `[data-slot="range-preview-chip"]` is hidden
+
+#### Releasing Shift and pressing a plain arrow collapses the keyboard range to a single cell
+
+**Given**
+- **D3** = `10`
+- **D4** = `20`
+- **D5** = `30`
+- **D6** = `40`
+
+**When** I click **D2**, then press `=`, then press `S`, then press `U`, then press `M`, then press `(`, then press `ArrowDown`, then press `Shift+ArrowDown`, then press `Shift+ArrowDown`, then press `ArrowDown`, then press `)`, then press `Enter`
+
+**Then**
+- **D2** shows `40`
+- **D2** has formula `=SUM(D6)`
+- `[data-slot="range-preview-chip"]` is hidden
diff --git a/content/docs/learn/playground/grid-context-menu.mdx b/content/docs/learn/playground/grid-context-menu.mdx
new file mode 100644
index 0000000..30b6394
--- /dev/null
+++ b/content/docs/learn/playground/grid-context-menu.mdx
@@ -0,0 +1,401 @@
+---
+title: "Right-click grid context menu"
+description: "Right-clicking a cell (or the current selection) in the grid prevents the browser's native context menu and opens a custom one instead — Cut, Copy, Paste, Insert row above/below, Insert column left/right, Delete row, Delete column, and Explain — every row routed through the SAME commands the top Edit/Insert menu already uses, so it's undo/redo compatible by construction. The insert labels show the REAL row/column count for a multi-row/column selection (e.g. \"Insert 3 rows above\"), matching Sheets' own grammar. Right-clicking a row-number/column-letter HEADER directly — or a cell that's part of a header-originated whole-row/whole-column selection — opens a second, richer, axis-only menu instead — only that axis's Insert above/below (or left/right), plus a real-range **Delete rows/columns X-Y** and a new **Clear rows/columns X-Y** (empties content without deleting the rows/columns), reusing the exact same commands (and, for Clear, the exact same `clear.selection` command `Delete`/`Backspace` already run) — no new capability needed."
+---
+
+{/* GENERATED — do not edit by hand */}
+
+## What it does
+
+Right-clicking a cell (or the current selection) anywhere in the grid prevents the browser's native context menu — no more "Save Image As…"/"Inspect" over what looks like a plain ` | | | |