# CAD shell playbook — branch `cad-shell`

Reshape the editor's chrome onto the five-band CAD frame specified in
`docs/cad-ui-spec.md` (source of truth) and illustrated by
`docs/cad-shell-mockup.html`. The spec was written app-agnostically; this
playbook is the blockworx mapping. Where the two disagree, this file wins.

**The load-bearing observation:** the spec's document model (§3, §4) is
already blockworx's — append-only log, restore-authors-forward,
undo-as-inverse-revs, machine-generated delta labels, tags-as-flags,
viewing-as-a-lens, no save action. This reshape is **chrome only**: no
document-model, store, or emitter changes. That is what makes it safe to
run on a branch (worktree `../blockworx-cad-shell`) while fixes continue
on `single-author`, and cheap to keep rebased.

## The mapping

| Spec band | Replaces | Contents (blockworx) |
|---|---|---|
| 1 Document bar | hamburger, lower-left cluster | File menu, undo/redo (kind-naming tooltips), document name, history trigger (clock), ⌘K chip, help, preferences |
| 2 Tool band | floating toolbar | authoring tools only, docked, left-aligned, digit shortcuts 1–n beside existing chords; right end free for tool-scoped state |
| 3 Parameter bar | — | **not built** (R14) |
| 3b Viewing band | watermark chrome + tape cluster + notice line | "Viewing rev N — ⟨age⟩", stepper, Restore this state, Return to current; Escape exits |
| 4 Workspace | history popup, navigator popup | ONE left rail → History and Hierarchy panels; collapsible, resizable, remembered per document and per view |
| 5 Status strip | — | left: scope breadcrumb (click ancestor = up) + back/forward + selection count; right: cursor grid coords, zoom % + fit, read-only padlock |

Stays floating (spec-sanctioned): the selection overlay, the tutorial
player. Stays as content: the PDF title block (§8 — sheets carry title
blocks; chrome does not).

## Resolutions (user, 2026-08-30)

- **R1 Redo stays visible, disabled when empty.** Spec §4.2 argues the
  opposite — a permanently dead button teaches users to ignore that region —
  and that argument was adopted here for a few hours on 2026-08-30 before the
  user reversed it the same day, on the OnShape precedent: *"Onshape keeps the
  redo button — just greyed out… a hidden button is still confusing."* So
  there is **no** exception: disabled-not-hidden is uniform across the whole
  editor, undo and redo alike, and both keep the kind-naming tooltip.
- **R2 The editor's painted title block dissolves**: name → document bar,
  path → status breadcrumb, rev/author/date → history panel + document
  bar. The canvas becomes fully clear (invariant 1). G4's shared `Grid`
  definition survives as the PDF sheet's layout — the seam test retargets
  to PDF-only.
- **R3 History filter = facets + text**: chips for author/date/kind AND
  the text box — blockworx labels are readable enough for text recall,
  unlike generic CAD deltas.
- **R4 Navigation leaves the tool band**: up = breadcrumb ancestor click;
  enter = canvas double-click (exists) + hierarchy focus; back/forward =
  small buttons beside the breadcrumb (view-kind undo may absorb them
  later). The tool band carries only creators.
- R5 Import stays in the File menu for now; spec §10.4 (import is a
  modeling op — D19 agrees) is flagged for later, not moved yet.
- R6 Nothing implies save (invariant 10): no "Saved" chip; the explicit
  projection-refresh command is renamed away from "Save" wording.
- **R7 The read-only triple signal is the spec's**: viewing band + dimmed
  tool band + canvas desaturation. Amended 2026-08-30: the mockup's band is
  confirmed as the right treatment — *"the bright sub-bar that shows the
  warning (you are viewing rev x) and the buttons… is better than writing the
  rev on the background of the canvas, which is harder to see."* So the
  canvas watermark **dies** when band 3b lands in Phase C rather than serving
  as the third signal. Phase A keeps it: it is the only read-only signal
  until the band exists. Carried out 2026-08-31 — the module, the role and
  `Viewing::watermark` are all gone.
- R8 Digit shortcuts added; existing chords kept.
- ~~R9~~ `view`-kind undo entries (camera/scope in the undo stack,
  §4.1/4.3/4.5) were deferred to a later phase on this branch. **Closed
  2026-08-31 by Phase H**, which is where they landed.
- R10 F6 persistent undo **overrides** spec §10.5's session-scoped
  recommendation; it is a founding requirement.
- **R11 The tool band is icon-over-word, not icon-only.** Phase A shipped the
  band as bare 14px image buttons; the user reviewed that build on 2026-08-30
  and reversed it — *"The icons should be bigger, and include a word under
  each."* Band 2 now draws the mockup's `.tool`: a 20px icon over one word in a
  56×46 cell, unframed until hovered or armed. The word is neither
  `ToolName::label` (multi-word) nor `ToolName::verb` (which answers "Add" for
  five of the seven) but the thing the tool brings into being — Select, Block,
  Area, Port, Image, Text, Route — carried with the band's membership and
  order in the one list `names::BAND_TOOLS`. The full label stays in the
  tooltip beside the digit.

## Phases

Each lands green (`cargo xtask ci`) with todo.md staged, committed on
`cad-shell`. kittest snapshots of the shell are the acceptance artifacts.

**Phase A — the frame.** eframe 0.36 has no `update(&Context)` any more —
`App::ui(&mut Ui)` is the only entry point — but egui 0.36's unified
`Panel::top`/`Panel::left`/`CentralPanel` all show *inside* a `Ui` and
reserve space from it, so the bands push exactly as required (invariant 1).
Build: document bar (band 1, with R1 redo and B's kind-naming tooltips), docked
tool band (band 2, digits), status strip (band 5, R10 contents,
breadcrumb replacing GoUp/back/forward buttons), workspace rail + one
collapsible resizable panel hosting the EXISTING history-panel and
nav-tree bodies as-is (panelized, not yet upgraded). Delete: hamburger,
floating toolbar, lower-left cluster, both popups, the editor title
block (R2). *Exit:* every previously reachable command still reachable
(registry tests hold); an invariant-1 test proves no persistent chrome
rect intersects the canvas rect; shell snapshots cut.

**Phase B — the panels earn the spec.** *Landed 2026-08-30.* History: day
grouping, faceted chips + text (R3), Current row, flag affordance,
muted/italic undo rows (§4.4). Hierarchy: focus re-roots with breadcrumb
(§5.3.1), filter flattens with left-truncated ancestor paths (§5.3.2),
reveal-from-canvas (§5.3.3), leaf counts, resizable. *Exit met:* the
three §5.3 mechanisms are each proven by a real-frame test.

Two decisions taken while building it. The panel width is remembered per
**view**, not per document: §5.3's own argument is that a tree and a list
want different widths, so one number per document served neither
(behaviour change from Phase A). And §5.3's instance counts (`×4`) are
**not built** — blockworx has no instancing, so there is no group to
collapse and nothing to count; the module says so. Sticky ancestor
headers stay unbuilt too, as the spec asks for them for production rather
than v1.

**R12 (2026-08-31) "Save as…" stays; nothing says plain "Save".** The
user resolved the flagged wording: *"We don't need 'save' since the
document is autosaved. But 'Save as' is useful."* The File-menu entry is
renamed from "Save as container…" to plain "Save as…" — the container is
an implementation detail the dialog reveals, not something the menu needs
to name — and it remains the only "Save" word in the chrome (invariant
10 satisfied by autosave, not by a command).

**Phase C — viewing and undo.** *Landed 2026-08-31.* Band 3b is a real
panel under the document bar, drawn only while a past rev is on the canvas:
the rev and its age in the history row's own words, the stepper on
`Viewing::stepped`, Restore this state, Return to current. It takes the
tape over from band 1, which now holds undo and redo alone. The canvas
watermark machinery is gone — `render::watermark`, `Viewing::watermark`,
`Role::RevWatermark` and the seek-latest icon with them (R7). Canvas
desaturation replaces it as the third signal: `Saturation` is a palette
transform, and the canvas's chrome and its drawing resolve through one
toned palette, so the drain cannot reach one and miss the other. The undo
caret opens the stack, each row the commit's label with its kind beside
it — **removed 2026-08-31 by R13**, along with its two decisions below and
the ratified depth and kind column. *Exit met:* `the_read_only_lens_raises_all_three_signals` proves §3.2's
three cues in one real frame, and `escape_returns_to_the_present_from_anywhere`
drives the key event through the shell.

Four decisions taken while building it.

- **Escape is claimed by the band, not by the keyboard handler.** The
  handler runs last, after the canvas; a tool armed before the lens opened
  would answer the key first and drop to Select instead of closing the
  lens. The band is drawn exactly when the key means "leave", so it claims
  it there — guarded by `egui_wants_keyboard_input`, which leaves the
  palette's own Escape alone.
- **§3.2's "the document-bar save indicator is replaced by *Viewing an
  earlier state*" is not built.** There is no save indicator to replace
  (R6, invariant 10), and adding the words to band 1 would put the same
  sentence two bands apart. The band says it.
- **`Action::UndoThrough(Rev)` names a commit, not a step count.** The
  editor's stack interleaves view entries the caret's list does not show,
  so "undo 4 steps" and "undo back through *that* edit" are different
  landings; the list shows commits, so the action carries one.
- **The caret is an icon.** "▾" is not in Roboto, and the chrome font is
  pinned to Roboto — the first cut drew a box.

Ratified at review (2026-08-31): `ViewingBand`/`ViewingBandText` at
B07/B00 — the band *is* the "bright sub-bar" the user asked for, and the
inverse-video pairing is TagBadge's proven one; the caret rows carry
their visible `rev` kind now rather than appearing when R9 adds a second
kind (structure shown, not hidden — the same call as disabled buttons);
caret depth 10, past which the history panel is the surface. One known
test gap, accepted and named: the "Escape leaves a focused text field
alone" case is enforced by the `egui_wants_keyboard_input` guard but not
asserted, because kittest cannot make a `TextEdit` report focus under
`Context::run_ui`; the consumption side is asserted.

**Phase D — polish.** *Landed 2026-08-31, and it closes the branch's
planned phases.* Band 3 is a real panel, drawn only while a tool is
active and holding that tool's name and the way out of it. Bands 3 and 3b
are one slot resolved by `shell::Conditional`, so neither forms its own
opinion about whether to draw; the lens wins, because a past rev is
read-only and has no active tool to describe. The ⌘K palette returns
typed results grouped by source (§6), with the log joining the registry
and the document as the third — `rev 2` and the words the history panel
gives that commit both find it. Band 5 gained the mockup's pills and the
unit its numbers are in. Every shell picture is now taken at 1280 *and*
1024. *Exit met:* the suite is green at both widths — nine pictures
each — and `the_tablet_widths_hold_at_the_largest_widget_size` walks the
whole shell at §9's two tablet widths. *(Superseded by R47: the widget-size
preference that test scaled the window by is deleted, so §9's tablet claim
now rests on the 44px targets alone and the test is
`the_tablet_widths_hold_at_the_taps_own_size`.)*

Five decisions taken while building it.

- **Band 3's occupant is the armed tool, not nothing.** The playbook's
  own mapping row calls the band "mostly empty until Phase 7", and §7 is
  off this branch — but §1's exclusivity rule is *argued* from the active
  tool ("the document is read-only while viewing an earlier rev, so no
  tool can be active"), so a slot whose occupancy nothing drives cannot
  hold that rule. What it draws is the mockup's band 3 with the fields
  removed: the tool's name, and Cancel. There is no Apply — a blockworx
  tool takes its arguments from the canvas gesture and commits on release
  (§3.1), so no form ever stands between the user and the rev.
- **The exclusivity is a second lock, not the only one.**
  `settle_on_viewed` already puts the tool down when the lens opens, so
  the two bands cannot collide today. `Conditional` is what lets the
  *frame* say so rather than inheriting it from the document layer.
- **Revs joined the palette.** §6 names revs as one of the four sources
  and band 1's palette chip has promised "tools, blocks and revs" since
  Phase A; grouping made the missing third obvious. Tutorials stay out
  (§7 is off this branch), so invariant 8 is not yet reachable here.
  Numeric search stays unbuilt — §6's own open decision, and out of scope.
- **Grouping happens after the ranking cut, not before.** Scores pick
  which twelve rows survive; the source picks the order they are listed
  in. Grouping first would let one source crowd another out of the list.
- **§1's snap toggles are not built**, and `status_strip`'s module doc
  says why: blockworx snaps everything to the grid, always, so the pill
  would be a switch with one position. The unit *is* named, since a bare
  coordinate pair does not say what it is measured in.

Ratified at review (2026-08-31). The selection count stays on band 5:
§2's "right end of the tool band" loses to the mapping table it disagrees
with, because band 2's right end is where tool-scoped state would collide
with the band's own one-word buttons at tablet widths — the spec's two
sentences disagree and the built one is the one that fits. The pill's
hover-frame gap is accepted and named, same class as Phase C's Escape
case. Band 3's occupancy (name + Cancel, no fields) is ratified
provisionally as the spec's own floor for the band ("options … plus
Cancel"), with the user's reduction question still open — cutting it back
to a bare slot is a one-file revert if they prefer the bar absent until
§7's walkthrough button needs it. Invariant 8 (tutorials reachable from
⌘K) is recorded as unreachable on this branch by construction — §7 lives
with Phase 7's UI half.

*Amended 2026-08-31 by R13 and R14*: the user tested this build on an iPad
and struck two of its features. Phase C's undo caret and Phase D's band 3
are both gone. The four Phase D decisions above stand except the first
("Band 3's occupant is the armed tool"), which the user's ruling settles
the other way, and Phase C's third ("`Action::UndoThrough` names a commit")
goes moot with the action itself.

Out of scope on this branch: §7 (tutorial library/player — Phase 7 of
the main playbook, which should consume §7 as its UI spec), §8 second
workspace (drafting — the PDF export already serves), numeric search
(§6 open). R9 was on this list; Phase H took it off.

## Resolutions (user, 2026-08-31 — the iPad punch list)

The user ran the built shell on an iPad and ruled on six things. These
override the phase notes above where they disagree.

- **R13 The undo caret is dropped.** *"Drop the caret on the undo. It's
  unnecessary."* Band 1 keeps the plain undo and redo of Phase A, each with
  the consequence tooltip naming the edit one press would take back — the
  head of the stack, which is all the plumbing that tooltip needs.
  `Action::UndoThrough`, `App::undo_through`, `UndoEntry` and the `past`
  slice go with it: reaching a commit several back is the history panel's
  job, and it has the filters and the whole log.
- **R14 Band 3 is dead.** *"The parameter bar is annoying. Remove it. Each
  tool action causes the UI to shift around, which is disconcerting."* The
  shift *was* the bar's whole cost — arming a tool moved every band below
  it — and what it bought was a name the tool band already shows selected
  and a Cancel that band's first button already is. Band 3b is untouched
  (*"the viewing band works well"*) and, being the slot's only occupant now,
  forms its own opinion about when to draw. `shell::Conditional` is deleted
  rather than reduced to two variants: the exclusivity rule it encoded is
  moot with nothing left to be exclusive against, and `Viewing::Past` is
  already the condition.
- **R15 §7's walkthrough button needs another surface.** *"The tutorial
  walk through button can appear elsewhere on the UI. I don't want the
  parameter bar for only that purpose."* Band 3 was the mapping table's home
  for it and that home is gone, so Phase 7's UI half must place it
  elsewhere. Candidates, named but deliberately not built: the Help menu
  (which already opens onto Tutorial), a status-strip pill beside the
  read-only padlock, or the ⌘K palette's fourth source — which would also
  close invariant 8, recorded above as unreachable on this branch.
- **R16 The hierarchy's rail icon is a tree.** *"The Navigator/Compass Icon
  no longer makes much sense. Some kind of tree icon would be easier to
  understand."* The compass was the floating navigator's glyph and outlived
  it; the panel shows an indented block tree, so `icon-tree.svg` says that.
  `icon-compass.svg` is deleted — nothing else used it.
- **R17 An unnamed block is named at creation, not at display.** *"Name
  collisions are confusing in the breadcrumbs. If the user declines to name
  a block, the fact that they are all named 'Untitled' is confusing."* A
  freshly stamped block takes "Block N", N the smallest positive integer no
  existing block title of that form uses. The name is real, editable and
  journaled, so one policy in the creation emitter fixes the canvas, the
  breadcrumb, the hierarchy, the palette and the PDF outline at once —
  where display-level dedup would have to be re-derived on each surface and
  would still leave the document holding N blocks called the same thing. A
  *pasted* block keeps its source title: copies are legitimately
  same-named, and the complaint was about unnamed blocks. Two decisions
  taken while building it. The search reads the titles the document holds
  rather than counting blocks, so a stamp after a delete refills the freed
  number instead of climbing past it — the name a block gets depends on the
  document, not on its history. And `wrap_top`'s `next_top_name` was already
  this function with `top_` hardcoded, so the two share one
  `next_free_title` rather than drifting apart. Areas keep "Untitled": an
  area appears in no breadcrumb, no hierarchy row and no PDF outline, which
  is where the collisions were confusing.
- **R18 Everything painted resolves through a role or an egui style.**
  *"Just make sure that anything that is rendered goes through either a
  canvas role or an egui visual style."* The CI step `palette` greps for
  it, so it is a property of the tree rather than of anyone's discipline —
  the same shape as the `waist` step. The audit found two real violations,
  both a swatch border hardcoded to mid-grey: the theme editor's now takes
  egui's non-interactive outline (it is chrome, so it follows the toolkit),
  and the role picker's takes the palette's `B03` (its *active* ring was
  already `B07`, and both rings surround palette colors, so both have to
  move when the scheme does). Three galleys laid out only to be measured
  carried `Color32::WHITE`; they carry egui's `PLACEHOLDER` now, which is
  what they meant. `TRANSPARENT` and `PLACEHOLDER` are not colors and pass
  anywhere; `src/canvas/palette.rs` is where the sixteen literals live; a
  single line may opt out with a `palette-exempt` comment saying why, and
  exactly one does (egui's image tint is a multiplier whose identity is
  white).

  This is a routing gate, not a retheme: the user separately deferred
  colour and role tuning (*"not worried about color and roles just yet"*),
  as they deferred the history panel's own visual polish (*"still in need
  of refinement, but that can be deferred"*). Both stay off this branch.


---

## Spec v2 — the unified floating layout (adopted 2026-08-31)

`docs/cad-ui-spec.md` is replaced wholesale by the user's unified-layout
variant (reference implementation `docs/cad-unified-layout.html`; the
banded mockup `cad-shell-mockup.html` is deleted per the spec's own
superseded list and lives in git history). Every §-reference in this
playbook *above this line* points at spec v1 — read those against git
history. The frame changes; the document model, the viewing semantics,
the history/hierarchy content, the palette, and the undo model all
carry over.

Rulings taken with the user at adoption, where spec v2 collided with
standing decisions:

- **R19 Undo and redo are both always visible, disabled when empty.**
  Spec §7.2's "redo is hidden entirely when its stack is empty" is
  overridden — *"Keep the undo/redo always. I'm not sure why it gets
  hidden in wide screen formats. It should always be there."* This
  re-affirms the original Onshape ruling; the spec's sentence loses.
- **R20 History filtering keeps facets AND the text box** (R3 stands —
  blockworx labels are readable gesture names, not generated CAD
  deltas), but the *presentation* is restyled to the mockup's: *"Keep
  the new history filtering functionality. But match the presentation
  style of the mockup."*
- **R21 The scope path is a bottom-left pill.** *"Use a bottom left
  pill to show the current path. It should say
  'Motor/Left Engine/Cylinder' and allow for clicking to select path
  segments, and allow for a forward/back navigation click."* The
  offered alternative (folding it into the top-left document chip) is
  declined: the chip already carries name, save state and the switcher,
  and the bottom-left corner balances the status chip. This is the
  *scope* path — spec §4.2's no-filesystem-paths question is separate
  and stays open (§12.3; file dialogs stand meanwhile).
- **R22 Viewing is the floating top-center pill**, replacing the
  full-width band 3b — *"I prefer the floating top-center pill."* Same
  content and signals: rev + tag + age, stepper, Restore, Return,
  Escape-from-anywhere; the desaturation and dimmed tool cluster stay.
- **R23 Glass without blur.** The mockup's `backdrop-filter: blur(24px)`
  has no egui/glow equivalent short of an offscreen pass; floating
  chrome renders as translucent fills with elevation shadows. To be
  confirmed on sight; revisit only if the user rejects the look.
- **Ledgered out of scope:** §10's instancing model ("editing through
  any instance changes all", `Amplifier[2]` breadcrumbs) — blockworx
  has no instancing and adopting one is a document-model decision, not
  a UI one. §10's interface-as-contract (surfaced pin/port mismatch
  with reconcile) is a wanted model feature, tracked in todo.md, not
  part of this refactor. §11 drafting stays out (the PDF export
  serves). Open decisions §12 stay open.

### The plan: four phases, agents dispatched per phase, lead reviews

**Phase E — the floating frame.** *Landed 2026-08-31.* The five docked
bands are gone. In their place, seven floating pieces over an
edge-to-edge canvas: document chip (top left — name, no save state per
invariant 12, switcher deferred with §4.2), action cluster (top right —
undo, redo, fit, sheet toggle; R19), vertical tool cluster (left,
centred, Select on top, digits, tap-active-returns-to-select), viewing
pill (top centre, R22), path pill (bottom left, R21), status chip
(bottom right), and the sheet (right). Every piece is an `egui::Area`,
so the layers put the chrome above the drawing whatever order the frame
draws them in — which is what lets the chrome be measured *before* the
canvas pass that frames against it. *Exit met:* the snapshot suite is
re-cut at 1280 and 1024 — `shell_frame`, `shell_sheet_hierarchy` and
`shell_frame_viewing`, three states of the whole frame rather than one
picture per band; `a_fit_lands_the_model_clear_of_the_measured_chrome`
proves §2.1 both ways (the model clears every piece that drew, and the
same fit centred on the raw viewport would not);
`the_read_only_lens_raises_all_three_signals` and
`escape_returns_to_the_present_from_anywhere` pass unchanged against
the pill.

Six decisions taken while building it.

- **One table places a piece and insets its edge.** `glass::Berth` says
  both, because they have to agree: a piece anchored somewhere the safe
  area does not know about lands the model underneath it. The edge is
  not derivable from the anchor — the action cluster and the sheet
  share `RIGHT_TOP` and inset different edges — so both are written
  down, once. `shell::Chrome` is the only way to draw a piece, and it
  measures what it draws, so a piece cannot exist unmeasured.
- **The framing centres in the safe region, not the viewport.**
  `View::set_safe_region` is set every frame from what the chrome
  measured, and `frame_rect` — the one function behind fit-to-content,
  the camera framings and the navigator's focus — sizes and centres
  against it. One change, every framing. An `Area` reports a stale
  position on the pass it is first sized in, so the very first frame of
  a session frames against a rough region and the next one is right;
  the snapshot harness re-frames each pass rather than pretend
  otherwise.
- **§2.3 beats §5 on the rule under Select.** §5 asks for Select to be
  "separated by a rule"; §2.3 says "no hairline rules anywhere —
  separate with space, tone, and elevation". The categorical rule wins,
  so the separation is a wider gap, and a test asserts it is wider than
  the gap between any two creators. Same call in the action cluster,
  where the mockup's `.divider` becomes air.
- **The cluster keeps its words (R11).** Spec v2's mockup is icon-only
  with tooltips; R11 is the user's own ruling on the band this replaces
  — *"The icons should be bigger, and include a word under each."*
  Nothing in v2 overturns it, and the spec itself says nothing about
  labels, so the cells stay icon-over-word at 22 px in a 58×54 cell
  (bigger than the band's 20 px), with the full name and the digit in
  the tooltip. Listed as a resolution candidate: it is the one place
  this frame does not look like the mockup.
- **The sheet is placed by the resolver, not by a constant.** It runs
  the right edge between the action cluster and the status chip, and it
  is drawn last, so it reads `Chrome::edge_span` — what the six pieces
  before it left — for its top and its height. The mockup lets the
  docked sheet overlap the top-right cluster; measuring instead of
  guessing avoids that without a second table of heights.
- **Motion is one duration, and reduced-motion is not reachable.**
  §2.3 asks for one curve honouring `prefers-reduced-motion`. egui
  exposes no such signal on any of the five platforms, so what the
  shell can do is keep its whole vocabulary to a single
  `style.animation_time` — set from `glass::MOTION` — and hold the
  chrome itself still: floating pieces do not fade in.

**Left transitional for Phase F**, and marked as such in
`shell::sheet`'s own module doc: the sheet is a floating right-edge
column with a two-segment header (History, Hierarchy) and a close
button, opened by the action cluster's one button. It keeps both panel
bodies untouched and the per-view remembered widths, but it has **no
Learn segment**, **no ≥1400px dock**, **no portrait bottom sheet**, **no
resize** (`Workspace::set_width` survives unused, marked for Phase F),
and the history filter keeps its Phase B presentation rather than
R20's restyle. The workspace rail is gone — the sheet's segments took
its job — and `shell::workspace` is now state only.

Also carried out here: `tool_band_video` became
`tool_cluster::tool_cluster_video`, so the tutorial player's embedded
chrome is the same column the live app draws (§1's one-layout
argument); `crate::shell::DOCUMENT_BAR` and the other band ids, and
`shell::panel_id`, are gone with the panels they named.

**Phase F — the sheet.** *Landed 2026-08-31.* One component, three
segments (History, Hierarchy, a Learn placeholder), three ways of
hanging: floating over the right edge, flush with it at ≥1400px, and up
from the bottom where the viewport is taller than it is wide (§8). The
three are not three components — they are three rows of the `Berth`
table, so the anchor that places the sheet and the safe area that
measures it cannot disagree about which mode it is in. Seven pieces,
nine berths. *Exit met:* `the_sheet_docks_at_the_threshold_and_floats_
under_it` proves both sides of 1400 through real frames — the same
`sheet()` drew, the docked one is flush and the floating one stands
off, and the region the canvas frames inside clears both;
`a_portrait_window_gets_the_sheet_up_from_the_bottom` proves the bottom
sheet spans the window and takes room from the bottom rather than the
right; `dragging_the_grab_sets_how_wide_the_view_stands` drives §8.2's
resize through a press, a motion and a release;
`the_current_row_returns_to_the_live_document` is §8.1's Current row;
the history and hierarchy behaviour tests pass inside the sheet.
Snapshots re-cut at 1280 and 1024, plus `shell_sheet_docked` (1440) and
`shell_sheet_portrait` (790×1010).

Seven decisions taken while building it.

- **The segment is called Hierarchy, not Parts.** §8.2's Parts is an
  assembly tree of parts and subassemblies with instance counts;
  blockworx's tree has no instancing to count (already ledgered out of
  §10) and no parts list to browse — every row is a block with its own
  id and what the tree presents is containment. The segment keeps the
  name of what it shows.
- **What the safe area sees of the dock is the standoff.** A docked
  sheet is not over the canvas, it is the end of it, so it keeps no
  margin on the side it docked to and the region gives up that much
  more; a floating one keeps the frame's margin and the canvas runs
  under it. Geometrically that is the whole difference — which is the
  honest answer, since the mockup's own docked and floating sheets are
  the same box.
- **The resize grab is the mockup's handle at the sheet's head, not a
  strip down its edge.** An `egui::Area` takes no interaction outside
  its own box (`layer_id_at` hit-tests the area rect), so an edge strip
  wide enough for a fingertip would have to eat an eighth of the
  sheet's width, and one that does not would be reachable on half of
  itself. A 44px row at the head is a real target on all five
  platforms. It is drawn only where it sets something: the portrait
  sheet spans the window, and a control that does nothing is worse than
  no control.
- **The sheet is a surface where the rest of the chrome is glass.** The
  mockup draws it `--solid` and the mockup is right: a canvas grid and
  a dimmed tool cluster showing through a column of labels is a column
  read twice. `Chrome::solid_piece`, one caller.
- **One row, one mark.** §8.1's `Current` row is the way back to the
  live document, so the head commit's row no longer wears the mark or
  the word "Latest" — two answers to "where am I" is one too many. The
  head row keeps its click (the same document; it asks for nothing new
  while it is already shown), and the rev number moved to the quiet
  line under every title, where it is information rather than a badge.
- **A row's text is one galley.** A child `Ui` hangs its content from
  the top of the room it is given, so a stacked title-and-attribution
  pair sat high against the glyph beside it; one two-line galley is a
  widget the row's own layout centres. It is laid out with the
  painter rather than handed to a label as a job, because a widget
  overwrites a job's wrapping and this row wants both lines with the
  title elided. Related: labels are selectable by default, and a
  selectable label senses the click before the row underneath it — the
  row turns that off inside itself.
- **The width is a constrained newtype.** `Workspace::set_width` takes
  a `Width` that clamps once, so the drag cannot leave a sheet too
  narrow to read or wider than the canvas beside it. Breaking: the
  persisted workspace state changes shape, so a document reopens at the
  default width once.

Listed as resolution candidates: **the portrait sheet covers the tool
cluster and the bottom-corner chrome**, as the mockup's does — spec
§12.6 leaves the portrait tool cluster open, and inventing a bottom
cluster here would be answering it; the sheet is dismissible in
portrait, which is the way out meanwhile. And **a docked sheet is not
auto-opened at ≥1400px**: the mockup opens it with the width, but the
sheet's state is remembered per document, and opening a panel because a
window was resized would overrule what the user last did.

**Phase G — the selection overlay contract (§3).** *Landed 2026-08-31.*
The file that held the floating toolbar has carried the overlay since
Phase E deleted the band; it is `tools::overlay` now, and it draws §3
whole. Placement reads the frame's own `SafeArea` rather than a
hard-coded 96px top gap and a list of obstacles that has been empty
since Phase E: centred above the selection with 14px of air, flipped
below when above would land under the top chrome, slid sideways into
the room the chrome left. The bar stands down while the camera is
worked and re-places on release (§3.4); beyond five commands the tail
goes behind one ellipsis (§3.5); right-click opens a menu built from
the same list (§3.6); a multi-selection wears its count (§3.2). The bar
is `glass::shell` now rather than `Frame::popup`, and every control in
it is a 44px target — icon cells, the I/O toggle, the accent swatch,
and the menu rows. *Exit met:*
`places_the_bar_above_the_selection_with_the_specs_air`,
`flips_below_when_above_would_land_under_the_top_chrome`,
`clamps_sideways_into_the_room_the_chrome_left` and
`the_bar_never_covers_what_is_selected` (a sweep of thirty-six
selections against a chromed region) are §3.3;
`the_bar_stands_down_while_the_camera_moves` drives three real frames
either side of a pan; `the_overflow_is_the_tail_and_the_row_never_
reorders` is invariant 6 against a block's twelve verbs;
`right_click_offers_exactly_the_overlays_commands` opens the real menu
and reads its rows out of its own area. Pictures: `shell_selection_
overlay` at 1280 and 1024; no existing picture moved, the bar drawing
only where something is selected.

Five decisions taken while building it.

- **The clamp is horizontal, and that is what proves invariant 5.**
  Both of §3.3's candidate bands are wholly clear of the selection in
  *y*, so a clamp that never touches *y* cannot put the bar on the
  thing it describes. The alternative — clamp both axes, then check
  for an intersection and do something else — has to invent the
  something else.
- **The bottom-of-viewport fallback is deleted, not ported.** It fired
  for a selection too big for the room around it, and what it did there
  was park the bar *on top of* the selection. Invariant 5 calls that a
  bug rather than a fallback, so a selection with no lawful placement
  now has no bar; zooming out is the way back to one. The
  sideways-slide machinery goes with the obstacles it dodged.
- **The 14px is flat, where the gap used to be 1.5 grid cells scaled
  by zoom.** §3.3 names a pixel figure, and a bar whose distance from
  its object breathes with the camera is a second thing moving while
  the user zooms.
- **`PanGesture` became `canvas::Camera`.** The view already answered
  "is a pan in flight" for the tools — a gesture the view consumes must
  not also reach a tool — and §3.4 asks the same question. One answer,
  two readers, rather than a second flag that could disagree with the
  first.
- **§3.2's intersection is degenerate here, and the module says so
  rather than computing one.** Every command a multi-shape selection
  offers is a verb over the whole *set* — copy, cut, delete, export the
  selection as one diagram — so the intersection is never partial. The
  per-member verbs (flip, accent, lock) are absent because their
  actions carry one target each, not because an intersection dropped
  them; giving them multi-target actions is an emitter change and not
  this phase's. What was missing was the presentation, and that is
  built: the count prefix, and the plain sentence for an empty
  intersection (proven at the split, since no blockworx selection
  reaches it). `Deletable::count` is now the one answer to "how much is
  selected", so the status chip and the bar cannot disagree — which
  also means a route or a pin group counts as one where the chip used
  to say nothing.

Listed as resolution candidates. **§12.4 is live**: a selected block
draws twelve commands, past the "~8 and the overflow menu becomes the
primary surface" line the spec draws for itself — Delete is behind the
ellipsis today. The order is the registry's and invariant 6 forbids
reordering it by use, so the answer is either a shorter per-type list
or a wider bar, and both are the user's call. **The 14px is measured
from the selection's bounding box, which for a block excludes the title
drawn above it** — so at high zoom the glass sits over the title, as
`shell_selection_overlay` shows. The selection *frame* draws to the
same box, so this is the app's own notion of where a selection ends;
widening it means measuring a shape's drawn extent, which moves the
frames too and is canvas geometry rather than chrome. **The right-click
menu is placed by egui**, which keeps it on screen but knows nothing of
the safe insets, so it may open over a floating piece; §3.6 asks only
that it carry the overlay's commands. And **an open picker is dismissed
when a pan begins**, since the bar it is anchored to has stood down.

**Phase H — view-kind undo entries.** *Landed 2026-08-31, and it closes
R9 and the spec-v2 phases.* Camera and scope changes are `view` entries on
the one stack, restoring without authoring; §7.2's ~1.5 s coalescing is
real; the tooltip names target *and* kind; under the lens undo serves the
view entries and withholds the document ones.

**The stack is egui's `Undoer<State>`, on the user's ruling** — *"Should we
just reuse the Undo/Redo stack from egui? Why are we writing our own? The
egui stack can reference a struct that includes the camera/etc, and the
document rev. A document rev change triggers a document undo commit
write."* The first cut of this phase was a hand-rolled interleaved stack;
it was not written past the design, and the ruling arrived in time to build
it this way instead. It is also CLAUDE.md's own rule: consume what a
dependency computes rather than approximating it.

*Exit met:*
`two_camera_moves_inside_the_window_are_one_entry_and_a_third_outside_is_a_second`
and `a_camera_worked_without_pause_leaves_one_entry` are §7.2's coalescing
from both sides; `the_undo_hover_names_the_target_and_which_of_the_two_kinds_it_is`
asserts both spellings verbatim;
`under_the_lens_a_view_entry_undoes_and_a_doc_entry_does_not` drives §7.2's
last line through the real dispatch with the lens open, and
`under_the_lens_a_view_entry_is_still_undoable` proves the cluster's half
through egui's own hit-testing; `doc_and_view_entries_undo_in_the_order_they_were_made`
interleaves an edit between two camera moves and walks all three back;
`a_view_undo_restores_the_camera_and_writes_nothing_to_the_log` asserts the
head and the log length are untouched either way; and
`a_doc_undo_lands_the_stack_exactly_where_the_journal_stands` is the
feedback-loop guard — after a document undo the state fed back stands where
the journal does, so idle frames add nothing and the future survives.

Six decisions taken while building it.

- **`Stood` is the journal's depth — never the head, never the top entry's
  rev.** Undo authors an inverse *forward*, so feeding the head would make
  every undo look like a fresh state and the stack would grow instead of
  walking; and a journal entry's rev changes as it crosses between the
  journal's two halves, so an undo followed by its redo would not return to
  the state it started from. The depth is stable across a round trip, and
  the difference between two of them is the number of journal steps a walk
  owes. The repo journal is not replaced: it stays the durable document
  undo (F6), and this stack is the session-level interleaving over it.
- **Coalescing is not written; it is the `Undoer`'s `stable_time`.** Set to
  a `core::time::Duration` constant of 1.5 s, converted to seconds only at
  egui's boundary. Everything §7.2 asks for falls out of it: a camera
  worked without pause never settles, so a pinch is one entry rather than
  one per frame, and "gesture end, not gesture frames" needs no entry point
  of its own. What *is* written is the exception — an edit punctuates the
  stack either side of itself, because burying one edit under another is
  what the window exists to prevent.
- **The registry asks the kind, not the command's identity.** `CommandId::
  Undo` used to be in `writes_the_document`, so the lens and a read-only
  container withheld it wholesale. It now answers with the kind of entry it
  stands over, which is what makes §7.2's "undo remains available for view
  entries only" a property of one function rather than a special case at
  each of the sites that ask.
- **Equality covers the camera, the scope and `Stood`, and nothing else.**
  It is what the `Undoer` compares every frame. The selection rides along
  outside it — picking something is how you *reach* an edit, and a step per
  selection would cost several presses to get back past one — and so does
  the label, since a fit that lands the camera where it already was is not
  a step.
- **A state is peeked by cloning the stack.** egui's `Undoer` keeps its
  points private and offers no peek, and both the tooltip's kind and the
  registry's gate need to know what one press would land on. A second stack
  kept alongside to answer it is exactly the drift this module exists to
  avoid, so the question is asked of a copy — two small clones a frame.
- **The label is set where the move is made.** A camera that has changed
  cannot say by itself whether a fit, a focus or a hand moved it, so
  `App::moved` is set at those three sites and the view reports only
  whether the user worked the camera themselves. Pan and zoom are one
  label: a hand navigating with both would otherwise leave entries that
  alternate and so never coalesce.

Listed as resolution candidates. **The path pill keeps its arrows.** R21
asked for them by name, and now that a scope change is an entry in the one
stack they are no longer a competing history: they are navigation, whose
moves undo takes back like any other. Dropping them is a small change if
the user would rather undo were the only way back. **A camera move drops
the forward half** — one-stack semantics, and what the old stack did for a
navigation too, but it means a reopened container loses its F6 redo the
moment the view moves. **A `view` entry's label is generic where a scope is
involved**: "the change of scope" rather than naming the level, since
naming it means resolving the path against the document in the tooltip
path. And **the reconstructed points hold the camera the document opens
at**, which is honest (§6.2: viewing state does not survive a reload) but
means a document swap that re-fits can clear the future it just rebuilt.

Phase 7's UI half (the Learn segment's library and player, §8.3,
invariant 13's palette source) follows on the main playbook's schedule
once the frame settles.

Ratified at Phase E review (2026-08-31): tool cells keep their words —
R11 is the user's standing ruling and nothing in v2 overturns it,
flagged for their eyes since it is the one deliberate departure from
the mockup; §5's "rule" under Select is rendered as air, §2.3's
no-hairline sentence winning; blockworx has seven tools and digits
1–7, no eighth invented; fit lives once in the action cluster and the
status chip only reads; the two new icons take stroke 2 to match the
set they join; reduced-motion is unreachable (egui exposes no such
signal on any platform) and is recorded rather than faked; the
first-frame fit uses a rough region because an egui Area reports a
stale position on its first sizing pass — every later fit is exact;
and the sheet stops clear of the action cluster by *measuring* it
(Chrome::edge_span), where the mockup itself overlaps.

**R24 (2026-08-31) The tool cluster is icon-only; tooltips carry the
words.** The user, on seeing the floating frame: *"I prefer fewer
words - the icon should describe the function, and the tooltip can
carry the words."* This reverses R11, which belonged to the docked
band, and lands the cluster on the mockup's own treatment: 44px square
cells, the 22px icon centred, name + digit + chord in the hover.

Ratified at Phase F review (2026-08-31): the portrait bottom sheet may
cover the tool cluster and bottom-corner chrome, as the mockup's does —
it is dismissible, and §12.6 (a portrait tool cluster) stays an open
decision rather than being answered by accident. A docked sheet does
not auto-open at ≥1400px: its state is remembered per document, and
opening a panel because a window was resized would overrule the user.
The resize grab as the mockup's head handle (an egui Area cannot take
interaction outside its own box), the opaque sheet body, the segment
keeping the name Hierarchy over §8.2's "Parts" (blockworx has
containment, not parts), and the retirement of the head row's "Latest"
in favour of the Current row all stand. A redo rev still titles itself
by the repo's raw label with only its quiet line naming it — minor,
pre-existing, ledgered.

Ratified at Phase G review (2026-08-31): the right-click menu is placed
by egui and may open over floating chrome — §3.6 asks only for command
parity, and a transient popup is not persistent chrome (invariant 4
untouched). The degenerate intersection stands: every multi-shape
command is a verb over the whole set, and computing an intersection of
one thing is machinery for a problem we don't have. Two items are
FLAGGED FOR THE USER, not ratified: §12.4 is live (a selected block
draws twelve commands, so Delete sits behind the ellipsis — the fix is
a curated fixed order or a shorter per-type list, a design call); and
the 14px clearance is measured from the selection's bounding box,
which excludes a block's title, so the bar can sit over the title text
(visible in shell_selection_overlay) — widening the measured box is
canvas geometry, not chrome.

Ratified at Phase H review (2026-08-31): the path pill keeps its
arrows — R21 asked for them by name, and with scope in the one stack
they are ordinary navigation that undo can take back, not a competing
history. A camera move abandoning the forward half is one-stack
semantics and stands, with its cost named (a reopened container's F6
redo becomes unreachable once the view moves — the history panel still
reaches every rev). Generic scope labels, the reconstructed camera
points, and the twice-a-frame stack clone (egui's Undoer exposes no
peek; a shadow stack would be the drift this module exists to avoid —
worth an upstream ask) all stand as taken. With this, R9 is closed and
**the spec-v2 refactor's four phases are complete**: what remains on
this branch is user testing, the two Phase G flags, and Phase 7's
Learn content on the main playbook's schedule.

**R25 (2026-08-31) The sheet has one width, whichever view it holds.**
*"The width per tab for the slide out panel feels weird. It should
just have one width, regardless of which tab is shown."* This reverses
the per-view widths taken at Phase B (and carried through F on §8.2's
argument): the sheet resizing itself on a segment click reads as the
layout moving on its own. Persisted workspace state changes shape
again; documents reopen at the default width once.

### The floating-shell polish punch list (2026-08-31/09-01)

The user ran the spec-v2 shell against `docs/cad-unified-layout.html` and
returned twenty-eight items. The numbering below is theirs and does not
change; `todo.md` carries the same list as a checklist. Twenty-four were
metrics, placement and behaviour the mockup already answered; four
(items 22, 25, 26, 27) are the selection overlay, and one of those was a
bug. Every ruling the list forced is recorded here as R26 onward.

Three of them reverse calls this playbook had already ratified. That is
the point of writing them down: a reversal the user asks for is cheap,
and a reversal nobody notices is drift.

**R26 (2026-09-01) The shell draws two hairlines, and only two.**
*"There is a separator between the undo/redo and the two view
controls."* and *"Put a horizontal separator between the selection tool
and the modelling tools in the main toolbar."* Phase E rendered both of
the mockup's rules as air, on §2.3's categorical "no hairline rules
anywhere — separate with space, tone, and elevation", and that call was
ratified at the Phase E review. The user asked for both back by name.
§2.3's sentence now reads as forbidding *incidental* hairlines: a rule
the user asked for is structure, and these two places — between what
takes an edit back and what moves the camera, and between what selects
and what creates — are the only two. One `glass::separator`, two runs;
a row's rule is short and unindented, a column's is inset from both
shoulders, which is the mockup's own difference between `.divider` and
`.toolsep`.

**R27 (2026-09-01) The sheet has one size, and no handle.** *"The sheet
should be a fixed size (350 px or so in the mockup), and a fixed
height."* This supersedes §8.2's resizable panel outright, and with it
R25 — a size the user cannot change is not a size to remember, so
`Width`, `Workspace::set_width` and the grab are gone and persisted
workspace state changes shape again. "Fixed height" is enforced by
pinning the body into the room the header leaves rather than by
`set_max_height`, which bounds the space offered and not the result.

**R28 (2026-09-01) The sheet is dismissed by clicking away from it.**
*"To dismiss the panel, clicking away from it should be enough (do not
need/want the right dismiss control next to History/Hierarchy/Learn)."*
The header's chevron is gone; the action cluster's toggle stands. The
dismissing press is *spent* — it must not also draw with whatever tool
is armed — and where that happens was the design question. A scrim over
the canvas does it and was built first, but an `egui::Area` that takes a
click takes the hover with it, and the readout under the cursor is not
what the user offered to give up. So the press is spent at the canvas
seam instead, where the frame already swaps the interaction out for
replay: `Interaction::spent` keeps the hover and drops the press, held
for the whole gesture so the canvas cannot pick a drag up half way
through it.

**R29 (2026-09-01) The head rev has no row.** *"With a 'Current' at the
top of the history view, the previous entry in the list is redundant
(clicking r10 does nothing since it's considered the 'Current' revision
- so hide r10 from the list."* The row was inert by construction at head
— `Standing::on_the_canvas` made the click a no-op — so it said the same
thing as the Current row above it and offered no way to act on it. The
Current row is the head's representation and carries the flag and the
"…" that were the only things the head's row still had. This retires the
last of Phase F's "Latest" treatment. Cost, named: the head's *label* is
no longer anywhere in the list, and no filter can bring it back. The
undo button's tooltip names it, which is where a reader asking "what did
I just do" already looks.

**R30 (2026-09-01) One bottom pill, and no navigation history.**
*"Combine the path pill (lower right) with the lower right pill, so that
it reads '<breadcrumbs> <zoom> <pos>'."* and *"Drop the navigation
undo/redo controls in the lower left pill - they are redundant with the
app-level undo/redo buttons, and no longer useful."* This reverses R21's
placement (the bottom-left corner "balances the status chip" was a claim
about symmetry the user did not buy) and the Phase H ratification that
kept the arrows. The arrows' removal is the load-bearing half: Phase H
made a scope change an entry in the one undo stack, so `path_history`,
`path_cursor` and `Action::PathBack`/`PathForward` became a second
history of record that no surface could reach, and they are deleted
rather than left. The word "cells" goes with them — blockworx measures
in nothing else, so it said the same thing on every document there has
ever been. Six pieces, eight berths.

**R31 (2026-09-01) The projection refresh is not a menu entry.**
*"'Refresh the document.json' seems weird as a menu option - I don't
know why this would be needed."* It is not: the projection has kept
itself fresh on a two-second settle after the head stops moving since
2026-08-28, so the entry offered to do by hand a thing that had already
happened. The File-menu entry is deleted. The *command* stays, registry-
and palette-reachable, for the one case the timer deliberately will not
touch: a projection somebody hand-edited is `Freshness::Unrecognized`,
and overwriting one has to stay a deliberate act (D11's contract).
`docs/json-format.md`'s provenance table, stale since the timer landed,
now names all four write moments.

**R32 (2026-09-01) The document is renamed on its own name.** *"Make the
document name renameable by double clicking it in the upper left pill
instead of requiring access to the hamburger menu."* The old hover text
argued that reaching a cascading submenu from the name would put that
submenu on the canvas — true, and not an argument against typing over
the name where it stands. The menu's Rename goes: two doors to one box
is the duplication that drifts. `Document` moves from `file_menu` to the
chip, which owns renaming now, and whether the box is open is egui's to
remember rather than `App`'s.

**R33 (2026-09-01) The document menu is a vertical ellipsis.**
*"Replace the hamburger menu icon with an elipses icon."* The mockup's
own glyph. The selection bar's overflow keeps the horizontal ellipsis,
so the two menus still differ — by axis now instead of by hamburger.

**R34 (2026-09-01) The add-pin grab is derived, not chosen.**
*"Increase the hit radius on the 'add pin' affordance around the blocks.
Make it as large as possible without overlapping other affordances (I
assume that it should be at least 1 grid unit in radius)."* The marker
committed on `PORT_RADIUS` — the radius it *draws* at — which made it
the only target on the canvas that grabbed from no further out than its
own picture. The radius is now the minimum of what each neighbour
leaves, computed from their own constants: the block body one grid cell
in, the next free slot half a pitch away, the corner handle at
`hypot(GRID_SIZE, PIN_TOP_MARGIN)` less its `HIT_RADIUS`, and an
occupied neighbour's ring and stub region a pitch off. It comes out at
one grid unit, bound by the block body, and both tools that offer the
marker use it. Ledgered: letting the disc reach *inside* the block would
buy 7.5px more and cost a press near the edge between two pins.

**R35 (2026-09-01) The overlay's split is categorical.** *"For sure the
primary actions should be 'Accent, Expand, Icon, Lock, Rip, flip lr, and
flip ud'. The 'copy/cut/delete' can all be put into the overflow menu, as
can the 'export to svg'."*, and for the port, *"…should also put the
copy/cut/delete stuff behind the extension, and use the toolbar for the
other controls."* This answers the Phase G flag (§12.4): §3.5's
split-at-five cut the list wherever the fifth command happened to fall,
so Delete's place depended on how many verbs the selected thing answered
to. `Placement` lives on `Command`, set once from one table, and `Bar`
partitions by it while keeping the registry's order inside each half and
the whole ordered list beside them for §3.6's menu. Placement is
orthogonal to applicability, which is why the port needed no work of its
own. **Recorded fallback, not built:** if the row proves too wide on a
selection that offers every primary, the answer is Figma's two-row
overlay rather than a return to a count.

**Item 25 was a bug, and its root cause was placement, not the
registry.** An area selection has always had its accent, copy, cut and
delete. `place` offered two candidate bands and both were *outside* the
selection; an area is the one selection routinely bigger than the canvas
around it, so both fell out of the safe region and the bar stood down.
Phase G took that deliberately on invariant 5 — the bar never covers what
is selected — and this is the cost showing up. There is a third band now,
just inside the selection's own leading edge, used only when there is no
outside. A selection with no commands at all also says so out loud now,
so this class of miss cannot be silent again.

**Item 26's glyphs follow the surface's own conventions.** The tag eye
shows the *state*, as the padlock beside it does — the registry offers
"Hide Tags" exactly when the tags are visible, so the command's identity
is the state — while the words keep naming the action. The I/O control
draws the user's own `|<-` sketch, read off the pins' current direction:
an arrow arriving at a wall for an input, its mirror for an output, one
at each end for both or for a selection that disagrees.

**Two deliberate deviations are recorded rather than fixed.** The status
readout is 40px on the user's own measurement, four short of invariant
11's tap target, so the breadcrumb's segments are a shorter target than
the rest of the shell offers — the words are wide even where the row is
short. And the mockup's second, larger elevation (`--shadow-lg`, for the
sheet and the selection bar) is not adopted: the item that brought the
shadows up asked for them to be *smaller*, and one `egui::Shadow` is one
layer, so the chrome's `--shadow` is used everywhere.

Ratified at the punch-list review (2026-09-01): the three recorded
reversals stand (R26 hairlines-as-structure, R27 fixed sheet over R25,
R30 breadcrumb-only navigation over Phase H's arrows), with the dead
navigation actions deleted rather than parked. Item 25's third
placement band — inside the selection's leading edge, only when both
outside bands fall off the safe region — is ratified as the honest
reading of invariant 5: a bar that cannot exist outside an area larger
than the canvas serves the invariant better by existing inside it than
by standing down. R29's cost (the head's label absent from the list,
named only by undo's tooltip) is accepted. The two named deviations
stand: sub-44px breadcrumb segments inside the user's own 40px pill,
and the single-layer shadow. Placement::of's overflow catch-all is
ratified as the safe default for future commands, with the inline set
pinned by test.

### The time machine grows a way out (2026-09-01)

*Landed 2026-09-01.* Three things the user asked for after the punch list,
in their words. The first two are one story: a lens you can *leave with the
document in your hands*, rather than one whose only exit was to rewrite the
head.

*Exit met:* `the_pill_drops_in_from_above_and_leaves_the_same_way` drives
R36's motion through real frames — off the top edge on the way in, on its
berth at rest, and inert while it leaves;
`the_read_only_lens_raises_all_three_signals` and
`escape_returns_to_the_present_from_anywhere` pass unchanged against the
reduced pill. `nothing_writes_the_document_through_the_lens` is R37's gate
over the whole offered set rather than one named command.
`the_saved_log_is_the_sources_own_lines` is the verbatim claim;
`the_saved_container_replays_to_the_rev_it_was_cut_at` is verify-grade
(replay, stamp, head at the cut); `a_saved_prefix_takes_the_next_edit_normally`
is "clean edits on top of it"; the tag pair covers what comes over and what
does not; `saving_while_viewing_a_rev_writes_the_document_as_shown` drives
all of it through the app, and asserts the source log is untouched.
`save_as_asks_for_the_rev_on_the_canvas` opens the real menu at both
standings. `a_said_toast_settles_after_it_has_been_said` is R38's settle
probe over three seconds of frames. Pictures: `shell_frame_viewing` re-cut
and `shell_toast` cut, both at 1280 and 1024.

**R36 (2026-09-01) The viewing pill arrives, and carries three things.**
*"The 'viewing an older rev' pill should 1. Drop in from the top with the
spring loaded overshoot action. 2. Include only the 'Rev # · time', then
some up and down arrows and a big 'Return' button."* The motion is the
sheet's — one curve (`glass::spring`) and now one duration
(`glass::TRAVEL`, the mockup's `.3s`, which its CSS writes on `.sheet`
and `.viewpill` alike), with `glass::off_stage` answering how far either
piece must go to take its shadow off screen with it. `Chrome::shaped`
carries the slide for every piece, so the rule that a travelling piece
insets the canvas by where it *settles* is stated once instead of once
per animated piece. The pill is drawn every frame, as the sheet is, and
holds what it was saying in egui's own memory for the length of its exit;
it answers no clicks once it is leaving, since the lens it would re-open
is already closed. What it says loses the word "Viewing" and the tag —
the history row is where a rev's name is read, and one surface for the
name beats two that can word it differently. Return is the shell's one
*filled* control (`glass::tap_filled`), drawn in the pill's own two roles
the other way round: the pill stands out from the chrome as the button
stands out from the pill.

Found and fixed while building it: `egui::Area` holds an area inside the
screen by default, so a piece sliding off its own edge is clamped back
onto it. **The sheet's spring has been clamped since it landed** — it
could travel only as far as its shadow slack. Every berth is anchored and
none is movable, so `glass::floating` turns the constraint off and both
pieces can now actually leave.

**R37 (2026-09-01) Restore dies; a Save-as writes the document as shown.**
*"Let's drop the whole notion of 'make this rev current'. I think a cleaner
solution is to have a way to save a log up to a given rev as a new
document. This allows you to time-machine back to an earlier rev and make
clean edits on top of it. I think just using 'Save as' would work…"*

**This supersedes spec §6.3's restore concept outright**, and the playbook's
own mapping row for band 3b with it: "Restore this state" is not moved, it
is deleted. What went: the pill's button (with R36), the history row's
"Make current", `CommandId::RestoreRev`, `RESTORE_HINT`, `Action::RestoreRev`
and its dispatch, `src/edit/restore.rs` (the two-document diff emitter) and
`Drawing::restore_to`. `CommandContext.head` went with them — it existed so
a restore invoked by name could target the rev on the canvas, and nothing
else ever read it — and so did the `&mut CommandSet` the history panel took,
since the restore was the one thing it asked the registry about.

The registry's read-only gate is simpler for it: a container this session
may not write and a past rev on the canvas were two questions with two
answers, and now have one. Nothing writes the document through the lens.

**The replacement is contextual, not a second entry.** While a past rev is
on the canvas the existing "Save as…" writes a new container whose log is
the prefix through that rev — the document exactly as shown — and says so
in its hover text. At head it behaves as it always has. A separate "Save
rev as" was offered and declined: the menu would then carry two entries
that differ only in a condition the chrome is already shouting about in the
pill above it, and the one entry is unambiguous *in the code's own terms* —
one `FileRequest`, one `FilePick`, one path through `save_as_container`,
with the rev carried from the moment the user asked rather than read again
when the dialog returns.

*Built 2026-09-01.* `src/store/prefix.rs` is the whole mechanism, and it is
small because the log is the document: `through` finds the leading run of
lines that folds to no more than the rev, `Container::create_holding` lays a
container out with those bytes already in it, and `Store::over` — the second
half of `Store::open`, now shared — replays what landed. The prefix is
*contiguous* by necessity: D12's chain is over the bytes a file holds, so a
line dropped from the middle would leave its successor naming a parent that
is not there.

Three things needed writing rather than copying. The projection and its
stamp, recomputed for the new head. The artwork the copied lines reference,
copied under the same content-addressed names. And the tag records for revs
inside the cut that were named *after* it — a rev tagged from the head is
still named in the container the cut writes, but its record cannot be copied,
so it is written again, by whoever asked for the save. A tag is a claim about
history rather than part of it, which is what makes re-stating one honest
where re-stating an edit would not be. The same reconciliation clears a name
the source has since taken back.

**The `Store::seeded` finding, and the ledger item it closes.** `seeded`
takes `&[Commit]` — commits are all it can take — so a Save-as of an *open
container* used to rebuild its log out of the folded commits alone: every
record came out an `edit` by the saving author at the saving clock's time,
and the tag records vanished (as would D17's notes). That was invisible
because Save-as had only ever been exercised on scratch sessions, which have
none of those columns to lose. The prefix path serves the whole-log case too
now — a Save-as at the head *is* a cut at the head — so the flattening is
gone wherever there was anything to flatten, and the ledger item closes.
`Store::seeded` stays for the one case it was written for: a scratch session,
which has no log to copy and no tags, notes, authors or wall times to lose.
`saving_a_container_at_head_carries_its_tags_across` pins it at the level a
user meets it.

**R38 (2026-09-01) One toast, and only for what makes a file.** The user,
on where a file operation's outcome should be said: *"fold the toast into
that dispatch."* It is wired to Save-as (success and failure) and to
export (success and failure), and to nothing else — not per edit, not per
tool. The document says what an edit did; a toast per gesture would be a
second thing moving while the user works. **The failure half is the reason
this is not only polish**: an export that could not be written said so in
the console and nowhere else, so a user whose PDF never appeared had no way
to learn why.

Geometry, tone and motion are the mockup's `.toast`: bottom centre, 22px
off the edge, capped at 22, `padding:10px 18px`, one 13px line, rising
twenty pixels on §2.3's one curve over `.25s`, holding 2.4s, and going the
way it came. `pointer-events:none` is `Area::interactable(false)`. The
colours are the palette's, not the mockup's near-black glass: `Role::Toast`
and `ToastText` take the inverse-video pairing `TagBadge` argues for, which
is the only one that clears AA in both schemes — the toast has two seconds
to be read over whatever is behind it.

It is **not a berth**. It takes no room from the canvas, cannot be pressed,
and is transient, so it draws itself rather than going through `Chrome`, and
the safe area never hears about it. `shell::toast::say(ctx, …)` is the one
enqueue, and it is callable from a worker thread — which is where the file
paths finish, so `spawn_export` says what it did from the thread that did
it, with no channel and no per-frame poll. A holding toast asks for exactly
one frame — the one it will leave on — rather than a stream of them, which
is what lets an idle frame with a toast on it still settle.

Ratified at the R36–R38 review (2026-09-01): the tag re-minting rule
(a tag is a claim about history, not part of it — re-minted fresh at a
prefix cut, cleared names stay cleared); one contextual Save-as entry
with no "Save rev as" sibling; a failed save/export is news, not a
standing fact — toast + log, never an acknowledgeable notice; export
toasts for every format alike. The Area-constrain find (the sheet's
spring had been clamped to its shadow slack since it landed) and the
Store::seeded flattening closure — a real silent data-loss path, gone
— are noted as the pass's two structural catches.

### Arming and dragging out are two gestures (2026-09-01)

*Landed 2026-09-01.* The user, on the creator that was the odd one out:
*"The 'add port' tool behaves differently than the rest of the tools. It
should require a drag to create (like the other tools) if the tool is just
clicked. If the user drags the tool off the toolbar, then use the current
'stamp' behavior. For the other tools, like the block, area, and text, use
the same semantics. Dragging the tool onto the canvas creates a default
'thing' with basic initial dimensions/contents. For the image tool, a drag
to the canvas should just open the import image box, and import the image
with some default size. For the route tool, dragging it onto the canvas
shouldn't really do anything. A route cannot exist without it's
endpoints."*

The stamp is not deleted — it *moves*. A cluster cell now answers two
gestures, and each one keeps what it was best at: a click arms a tool and
the canvas is then drawn on with it, and a press carried off the cluster
drops the tool's default thing where it lands.

*Exit met:* `no_armed_creator_writes_on_a_bare_click` is R40 over all five
creators; `the_port_tool_draws_a_boundary_port` is the rewritten stamp
case, on the new contract. `carrying_a_cell_off_the_cluster_reports_the_drop_and_arms_nothing`
and `a_dead_cell_cannot_be_carried_out` drive the cell's two gestures
through real laid-out frames. The `drag_out` module in `app.rs` drives the
whole path — the cell's drag sense, the chrome's boxes, the canvas
transform, the dispatch — for the block that lands, the block that is one
commit at the snapped drop point, the route that does nothing, the drop on
the glass that does nothing, and the drag that leaves the armed cell alone.
`every_creator_stamps_its_own_default`, `a_drop_that_cannot_finish_writes_nothing`,
`a_read_only_session_stamps_nothing` and
`an_image_drop_is_handed_back_carrying_its_drop_point` are the resolver's
own policy. No picture: the drag-out's only new paint is a ghost that
exists for the length of a gesture.

**R39 (2026-09-01) A tool cell has two gestures: click to arm, drag out to
stamp.** *"If the user drags the tool off the toolbar, then use the current
'stamp' behavior."* The cells sense `click_and_drag`, and egui tells the
two apart by how far the pointer travelled — so the press animation and the
arming click are untouched. A drag-out **does not arm**: the cluster's
armed cell is exactly what it was before the gesture. The cluster reports
only where the drop landed; what a drop *means* needs the canvas transform
and the chrome's own boxes, which belong to the app, so the app turns the
report into `Action::StampTool { tool, at }` and dispatches it through the
same door every other creation takes. The write therefore goes through
`Drawing`, the stamp is one commit, and it is labelled by the same verb its
dragged sibling opens a gesture under.

What each creator's default is lives **beside its emitter**, once
(`edit::create::stamped_block` / `stamped_area` / `stamped_port`; a text box
has no extent to default, so the drop point is the whole of it). A stamped
block takes the rect the document already gives a block nobody sized, and
the port's default width is now the same number the armed drag falls back
to — the tool had been carrying its own copy of it.

Three drops write nothing. Route, on the user's own reading — *"A route
cannot exist without it's endpoints"* — and Select, which creates nothing
anywhere. An image is the third and is different in kind: it is handed back
to the app, which opens the picker and lands the file at the drop point,
because there is nothing to place until a file is chosen.

A drop that never reached the canvas — on any glass piece, on the sheet, or
off the window — is no drop at all (`shell::over_the_chrome`); the gesture
ends and the cell springs back. The registry gates the drag exactly as it
gates the click: a cell a read-only session or the lens draws dead cannot be
carried out either, and `apply_scripted` refuses the stamp under the same
condition as the arm, so a script cannot reach what the button will not.

*Preview:* the carried cell ghosts its own icon under the pointer, at 0.6
opacity in an uninteractable top layer. The alternative — outlining the
default extent at the snap point — would have to run the resolver every
frame of the drag to know what it is outlining, and reads as something
already placed. The icon is the cheaper honest answer: it says *what* is
being carried, and the drop says where.

**R40 (2026-09-01) With a creator armed, a bare click on the canvas creates
nothing.** The session lead's generalization of the ruling above, and the
half that makes the two gestures worth separating: stamping now lives on the
drag-out path exclusively.

Add Port's drag needed no invention. The tool does not target a block edge,
whatever the old click's shape suggested — the port's *body* is drawn where
the gesture is and its boundary slot is searched for by the emitter — so the
drag it already had (press for the body's top-left, slide for its width,
release to commit and open the name) was always the only gesture the tool
could mean. Deleting the click arm is the whole change.

Add Text grows the same drag. A text box has no stored extent — it is as
big as what is typed into it — so the drag names the corner the text begins
at rather than a size, and the box lands at the drag box's top-left.

Add Image loses its click arm too: opening the picker and placing the image
at the cursor *is* a stamp, so it moves with the rest. This is the one place
the drag-out touches the armed tool, and deliberately: the pending dialog
lives in `NewImage::Pending`, which is the tool's own waiting room, so the
Image cell reads as armed while the picker is up and settles back to Select
once the image lands on its selection.

**New Block keeps its two-click placement.** Its *first* click already
creates nothing, which is what R40 asks, and the tutorial teaches the
corner-then-corner gesture by name (`01_first_block.kdl`). Taking it away
would be a removal nobody asked for.

Under a locked level the port tool now falls back to Select on a drag's
release and nothing else, so the locked and unlocked cases agree about what
a bare click is: nothing.

Ratified at the R39–R40 review (2026-09-01): the add-port gesture is
the one the tool already had (the click arm simply died); the ghost-
icon preview over the extent outline; New Block's two-click placement
survives (its first click already creates nothing); Text's drag naming
a corner. Image's armed click removed under the universal bare-click
ruling — flagged for the user's eyes on their pass, since their words
had grouped Image with "as today": an armed Image tool now needs a
drag (or the drag-out drop) to open the picker.


---

## Spec v3 — the top bar (adopted 2026-09-01)

`docs/cad-ui-spec.md` is replaced by the user's top-bar revision
(reference `docs/cad-unified-topbar.html`; the floating-pill mockup is
superseded and deleted per the spec's own list). The frame converges on
three persistent regions: a docked flat top bar (breadcrumb left, mode
centre, actions right), the floating tool rail, and the Navigator
overlaying from the right at every size. Rulings at adoption:

- **R41 Redo stays visible, disabled when empty.** The spec's §2.0
  sentence says "hidden when empty" a third time; the user's own
  mockup draws it `disabled`, which is R19's rule. The mockup wins and
  the sentence loses, as it has twice before.
- **R42 The dot is a liveness signal, not a save indicator.** Green
  while recording, amber under the lens (the mockup's `--live`/
  `--warn`). It joins the read-only signal set; invariant 12 stands —
  nothing implies a save *action*.
- **R43 Routine confirmations move to the status line; the toast is
  demoted to attention events.** "Saved as X" / "Exported X" and the
  ~2s per-action rev confirmations are ambient status-line text
  (§2.0.1); the toast (R38) keeps only failures and future
  attention-with-action events (§2.0.2). This re-wires R38's success
  cases, at the spec's own instruction.
- **R44 Go Up survives in the right cluster.** The spec's action list
  omits it, but the user asked for it by name (item 24) and the
  breadcrumb's parent segment now sits in the same bar; both stay —
  the button for the common step, the breadcrumb for the jump. To be
  confirmed on the user's pass.
- **R45 The frameless-native title bar (§2.0's "bar is the title bar")
  is its own later phase.** It is native-only work (decorations off,
  drag region, caption buttons) invisible to the web build under test;
  ledgered, not built in this pass.
- **R46 Status-line idle text is zoom · position.** The spec's generic
  "units · snap" have nothing to say here: the unit word died at item
  13 and blockworx always snaps (the R18-era finding).
- §9's "filter survives Escape" is superseded by v3's own text: Escape
  closes the Navigator first, and the Parts filter resets on dismiss.

**Phase I — the top bar.** One phase, one agent: the top bar (54px,
flat, translucent, no border; document menu + live dot + breadcrumb
left; amber viewing mode centre with rev/stepper/Return; undo, redo,
Go Up, fit, Browse right), the status line (§2.0.1's four states,
plain muted text, no container), the Navigator rework (overlay at
every size, flush right, full height, translucent, no scrim, no close
control, picks hand off and keep it open, canvas click dismisses AND
performs its selection in one gesture, Escape closes first, Parts
filter resets on dismiss, portrait variant dies — it narrows to
~320px), the toast/status re-wiring (R43), and the deletion of the
document chip, action cluster, viewing pill, and status pill berths.
The elevation rule (§2's docked-flat / floating-shadowed) is encoded
once in glass. *Exit:* snapshots re-cut (idle, viewing, navigator
open, narrow); the triple-signal and Escape tests keep passing against
the amber bar; a hand-off test per Navigator segment; the status
line's four states tested.

*Landed 2026-09-01.* Four berths where spec v2 had eight, and one of
them is not a surface at all. `glass::Elevation` is §2's rule written
once — docked flat and square, floating rounded and lifted, and `Bare`
for the status line, which draws no container and takes no pointer —
read off the berth, so a piece cannot be given a place without being
given the treatment that place implies. `SafeArea` now records each
edge's bare depth and adds the clearance in `region()`, which is what
lets the navigator hang from where the bar *ends* rather than from a
gap under it.

*Exit met:* `the_read_only_lens_raises_all_three_signals` and
`escape_returns_to_the_present_from_anywhere` pass unchanged against
the amber bar, joined by
`the_lens_changes_the_bars_state_and_moves_nothing_under_it` (the bar
is the same box either way, and so is the region the canvas frames
inside) and `escape_closes_the_navigator_before_it_closes_the_lens`
(two presses, two effects, in §8's order). The hand-off suite is one
test per segment — `a_rev_pick_opens_the_lens_and_keeps_the_panel_open`,
`a_part_pick_selects_on_the_canvas_and_keeps_the_panel_open`,
`a_scope_pick_changes_the_context_and_the_breadcrumb_follows` — plus
`a_tool_pick_dismisses_the_panel` and
`the_click_that_dismisses_the_navigator_also_reaches_the_canvas`, which
is item 10 reversed. `the_status_line_reads_all_four_of_its_states_in_priority_order`
walks §2.0.1 through the whole shell and
`a_confirmation_reverts_after_its_dwell_and_settles` proves the two
seconds cost no frames. `a_deep_path_collapses_from_the_middle_and_keeps_its_ends`
is §2.0's collapse at five levels. Pictures: `shell_frame` (at rest),
`shell_navigator`, `shell_navigator_hierarchy`, `shell_frame_viewing`,
`shell_selection_overlay` and `shell_toast`, each at 1280 and 1024 —
the 1024 pictures being the narrow navigator, which is the only thing a
small window changes about it. `shell_sheet_docked` and
`shell_sheet_portrait` are deleted with the modes they photographed.

Six decisions taken while building it.

- **The mode is measured, not added up.** egui lays a row out from where
  the cursor is, so centring the bar's middle in the room the two runs
  left means knowing the row's width first. It is measured on a sizing
  pass of its own; the alternative — summing the label's galley and the
  three controls' constants — is the same layout written twice, and the
  copy drifts the first time the mode gains a control.
- **The tint goes *over* the bar's fill, and carries its own alpha.**
  `Role::ViewingTint` is a faded amber composited onto the glass the bar
  already has (`Tint::Over`), which is what makes this the same bar in
  another state rather than a differently-coloured object. The mockup
  transitions the background and swaps the centre outright, so the
  colour is what animates and the words simply appear.
- **The dot's two colours are hues, not base tones.** The house rule
  (base colours for UI roles, accents for content) is about loudness;
  green-recording and amber-viewing say *which state*, which is what the
  hue ramp is for. `LiveDot` is B0B, the lens's amber is B09, and the
  bar's wash is the same B09 faded.
- **A segment toggling the panel shut had to go.** §8 is categorical —
  *nothing inside the panel ever closes it* — so `Workspace::pick`
  became `Workspace::show`, and the active segment is no longer a
  toggle. Browse remains the toggle it always was.
- **Dismissing is more than shutting.** `escape_closes` reports; the app
  dismisses, because dismissing takes the Hierarchy filter with it and
  the panel has no way to reach egui's memory for someone else's box.
  One `App::dismiss_navigator`, three callers (Escape, the tool pick,
  the canvas click), so the three cannot disagree about what dismissal
  means.
- **The confirmation is read off the log, not off the action.** After a
  dispatch that moved the head, the line says the commit's own label and
  the rev it landed at — so the status line and the history row word one
  edit the same way, and an action that turned out to write nothing says
  nothing.

**The safe-area call, recorded.** The navigator **does** inset the safe
region while it is open, and does **not** reflow the canvas. §8's "never
reflows the canvas" is about the drawing surface, which still runs edge
to edge under it; invariant 4's "fit-to-view respects measured insets"
and §2.1's "insets change when the sheet opens or closes" are about
where the *model* is allowed to land, and the mockup settles it in its
own `insets()`, which adds the panel's width whenever it is open. So a
fit taken with the navigator up frames into the room beside it. The cost
is named: opening the panel after a fit leaves the drawing where it was
until the next fit, which is the same bargain every other piece of
chrome already strikes.

Listed as resolution candidates. **The document's segment answers two
gestures.** §2.0 puts the document at the head of the breadcrumb and
R32 renames it where it stands, so one word is both the way back to the
root and the rename target — a double click from inside a block
therefore rises to the root *as* it opens the box. The one undo stack
takes that back, and the alternative (renaming only at the root, or a
rename that lives somewhere else again) is worse; flagged because it is
the one place the two rulings rub. **Go Up survives beside the
breadcrumb's parent segment** (R44), which is two doors to one move —
kept because the user asked for the button by name, to be confirmed on
their pass. **The narrow threshold is 1024 inclusive**, so a tablet-width
window gets the 320px panel; the spec says only "~320px" for narrow
viewports and names no number. And **the history filter still survives a
dismissal** — §8 names the Parts filter alone, so only the Hierarchy
one is cleared.

Ratified at Phase I review (2026-09-01): the safe-area call (the
navigator insets the region while open; the canvas surface stays
edge-to-edge beneath it — §8's "never reflows" is about the surface,
invariant 4 about the model); the document segment answering rename
and rise-to-root on one double-click, flagged for the user's pass as
the one place two rulings rub; the 1024-inclusive narrow threshold;
the history filter surviving dismissal where the spec names only the
Parts filter; and the six recorded build decisions. The dispatch-vs-
frame confirmation catch (most edits seal in the canvas pass, so the
status line reads the log, not the dispatcher) is noted as the pass's
structural find.

### The top-bar punch list (user, 2026-09-01)

Fourteen items on the landed spec-v3 chrome, read against
`docs/cad-unified-topbar.html`. Most are metrics the mockup already fixes
and the build missed; four are reversals, recorded as R47 onward.

**R47 (2026-09-01) The UI does not scale; the targets are the scale.**
*"I realized a lot of my mismatches between the Linux desktop and the
mockup were tied back to the application 'Zoom' which was set to
'Large' — this zoom control should be removed, along with the preference
entry."* `WidgetSize`, its menu, its `set_zoom_factor` call and its
persisted field are deleted; a stored `widget_size` is dropped on load
rather than migrated, since serde ignores a key no field claims. §9's
tablet claim rested on that preference (Phase D scaled the window by it
to prove the layout survived fewer points); it now rests on the 44px
target alone, which is what invariant 11 always said it was.

**R48 (2026-09-01) The chrome is opaque.** *"In the app, the toolbar and
panel are semi-transparent. I wonder if that's a good idea, or if opaque
wouldn't be easier to see and read."* It would. The mockup's translucency
rides on `backdrop-filter: blur(24px)`, which is what keeps what is
behind a surface from competing with the words on it; egui has no blur
short of an offscreen pass, so R23's translucency-without-blur put the
grid through the text. Every surface `glass::shell` draws — the top bar,
the rail, the navigator, the selection overlay and the toast — is now the
theme's own solid fill. This reverses R23's answer, not its question:
the elevation rule (§2) still carries the difference between docked and
floating, and the shadows and radii are untouched. The lens's amber is
unaffected, since the tint always washed *over* whatever fill the bar
had.

**R49 (2026-09-01) One click on the document rises; two rename, and the
rise is never taken.** *"Double click to root and to rename the document
is confusing. I would prefer a different solution there."* The two
gestures stay on the one word (§2.0 puts the document at the head of the
breadcrumb and R32 renames it where it stands), and are told apart in
time: egui reports the first half of a double click as a click of its
own, so the rise is *armed* and waited out for
`max_double_click_delay` — egui's own number, read rather than
restated — and disarmed the moment the second click lands. Deferring
beats absorbing: a scope pop is a view entry on the one undo stack, so a
rise taken and taken back is a rise the user still has to notice. The
cost is named: from inside a block, a single click on the document lands
about a third of a second late, and only there. This closes the Phase I
resolution candidate that flagged the two rulings rubbing.

**R50 (2026-09-01) The dot has four states, and each of them says which
it is.** *"I'm not sure I understand the color of the dot next to the
document name. I expect it to be green (all is good), red (no editing
allowed), and yellow (update to the disk representation in flight). But I
also get gray."* The reading is adopted whole: green when the projection
names the fold the log is at, yellow while the debounced write is due
(`Freshness::Stale`, and `Unrecognized` with it — to a reader both mean
the file is not what the log says), red where nothing may be written, and
grey kept for the fourth state the user had not been told about, a
scratch session with no file behind it at all. `Liveness::of` is the one
resolver; the lens outranks the file, since "no editing allowed" is
exactly what the lens is. That reconciles **R42**: under the lens the dot
is *red*, not a fifth colour, and the amber stays where it belongs, on
the bar itself.

Every state now carries its words in a tooltip, and that is the more
important half of the ruling. **What the grey actually was:** not a state
at all. The dot's two colours were palette *slots* (`B0B` for recording,
`B09` for the lens), and a slot's hue is the scheme's to choose — in
Tokyo Night dark, `B09` resolves to `#A9B1D6`, a pale grey, and in Rosé
Pine `B0B` is a muted teal. So the user was reading the lens's dot in a
scheme where the amber is not amber. Nothing in the code could paint that
grey; the palette could. Roles cannot fix that on their own, which is why
colour alone is no longer the whole signal.

**R51 (2026-09-01) The status line is a two-line title block.** *"The text
at the lower left should list the author's name"* and *"the text in the
lower left corner (let's consider that the title block) should also
indicate on a second line, the author, current rev, and date/time of that
rev."* §2.0.1's four states keep the first line; the second is
`<author> · rev N · <when>`, in the same muted, uncontained treatment,
and it says the same thing whichever state is above it. Two calls
recorded: the **author is the session's identity** (D9's, the one every
commit this session writes is attributed to) rather than the viewed rev's
recorded author — it is always there to print, and it agrees with the log
for everything this session wrote; and the **rev is the one on the
canvas**, so under the lens the block describes what the reader is
looking at rather than the head they are not. A scratch session keeps no
clock, so it prints author and rev and stops rather than trailing a
separator. The safe area is measured, so the second line takes its own
room and nothing lands on it.

**R52 (2026-09-01) The chrome's one word for the thing is *diagram*.**
*"What does 'open document' vs 'open container' mean to the end user? The
user only knows about one type of thing. For now, that's a container, but
they would probably consider that to be a 'diagram'."* Every user-visible
string that names the thing being opened, saved, renamed or exported says
*diagram*: menu entries, dialog titles, hovers, failure notices, the
history panel's row menu, the PDF sheet's own caption, and the commit
labels an import or a paste writes. The internal vocabulary is untouched —
`Store`, container, log, projection and `document.json` are what the code
and `docs/json-format.md` go on calling them — and so is the chrome
piece's own name, the document menu. Where a string reads better with no
noun at all it keeps none: the menu still says "Save as…" and the dialog
it opens says "Save diagram as". The one place the two entries had to stay
apart is File ▸ Open: *"Open diagram…"* takes the container with its
history, and *"Open JSON file…"* takes a single exported file as an
unsaved session, which is what the old "Open document…" meant and never
said.

**The glyphs, three items' worth.** The house draws its own icons — 24×24,
stroke 2, round caps, no vendored artwork — so phosphor was read rather
than imported. Going up a scope and entering one are one box with a gap at
its corner and one arrow through the gap, arriving or leaving
(arrow-square-in / arrow-square-out, item 7); `icon-exit.svg`, which
nothing but a test still referenced, goes with the pair it used to be half
of. The document menu takes the mockup's own three lines with a short last
one (item 10), which supersedes **R33**'s vertical ellipsis — the overflow
in the selection bar keeps the horizontal one, so no two surfaces wear a
single mark. The rev stepper takes the mockup's arrows, down for older and
up for newer, its own mapping (item 12); the tooltips keep saying which,
because an arrow says a direction and not which end of a log it is.

**Item 26's I/O glyph, amended (item 14).** The trigger that opens the
direction choice showed the pins' *current* direction, which item 26 asked
for; the user, on the build: *"do not change it based on the current state
of the pin. That is confusing. Pick one icon (e.g., the |<- icon) and use
it always."* A control is a place, and a place that changes its face is a
new control each time you look at it — so the button wears the input glyph
in every state and the direction is told in its tooltip, where a varying
thing belongs (invariant 8). The picker it opens drops its three words for
the three glyphs, each on a tap-sized cell with the word in *its* tooltip
and the current one drawn selected. The three glyphs move to the picker,
which is the surface that offers all of them; the overlay borrows the one
it wears.

*Landed 2026-09-01.* Fifteen items, six rulings and no new berths: this
pass changed how the chrome reads, not what it is made of. The metrics all
came out of `docs/cad-unified-topbar.html` — the crumb's 9-point shoulder
and 32-point box, the segmented control's `.segwrap` and `.seg` padding,
the mockup's own document and stepper glyphs — which is what the reference
is for. Two of the fixes were structural rather than cosmetic: a crumb is
painted rather than pressed, because egui reads a button's margins out of
the state it is in and no amount of tuning makes that box hold still; and
the dot's grey turned out to be the palette rather than a state, which is
why colour is no longer the whole of what it says.

*Exit met:* `the_level_the_canvas_stands_on_is_a_label_and_its_ancestors_are_not`
and `nothing_on_the_breadcrumb_moves_when_the_pointer_crosses_it` for the
trail; `one_click_on_the_document_rises_to_the_root` and
`a_double_click_renames_the_document_without_moving_the_canvas` for R49's
two gestures; `no_surface_the_chrome_draws_is_see_through` for R48;
`the_segments_sit_in_a_groove_that_shows_and_stand_apart_inside_it`, read
against the app's own palette, for items 4 and 5;
`the_rule_under_select_is_centred_on_the_column_it_divides` for the rail;
`the_dot_tells_its_four_states_apart_in_colour_and_in_words` and
`the_dot_says_which_state_it_is_in_when_the_pointer_rests_on_it` for R50;
`the_line_carries_a_title_block_under_whatever_it_is_saying`,
`the_room_the_line_keeps_clears_both_of_its_lines` and
`the_title_block_names_the_author_and_the_rev_the_canvas_is_showing` for
R51; `the_bar_wears_the_mockups_own_glyphs`,
`the_hierarchy_icons_share_a_box_and_oppose_their_arrows` and
`every_direction_has_a_glyph_of_its_own_and_a_word_to_go_with_it` for the
artwork; and `a_stored_widget_size_is_dropped_and_the_rest_of_the_blob_still_loads`
for R47's retirement. All twelve shell pictures re-cut.

Two judgment calls to confirm. The **document's own segment still lights
up under the pointer** even where it is the leaf, because it still answers
a gesture there (the rename) and a control that answers must look like
one — the mockup's `.here:hover{background:none}` assumed a segment with
nothing left to do. And the **document menu's tooltip still says
"Document menu"**, under R52's own carve-out for the chrome piece's name;
if that reads as mixed vocabulary on the user's pass it is one string to
change.

**The ink trap, named once (review, 2026-09-01).** The document's name
shipped invisible: `Voice::Full` asked for `Visuals::strong_text_color`,
which egui defines as `widgets.active.fg_stroke` — and this palette sets
that to its darkest base, because it is the ink for a word sitting *on* an
accent-filled control, not a louder body text. Measured off the picture,
the name read at luminance 32 against a bar of 25. Every test was green
because egui's stock dark visuals make the same call come out white, so
the harness was asking the question about a different application.
`glass::full_ink` is the shell's one answer now — `tutorial::cues` had
found the same trap first and kept its own local note; that note becomes
the shared one — and the top bar's tests are dressed in the app's own
palette, where a colour question can be asked honestly.

**R53 (2026-09-01) A projection is not a diagram to open.** *"'open Json
file' is unneeded detail for the end user. The file selection filter
should include directories with the suffix bwx. Logically, the JSON file
is not a self contained archive of the document (it does not, for example,
include the assets). So it can't really be 'loaded' as a document in an
unsaved session."* The reasoning is the ruling: `document.json` is the
readable projection beside a log (D11), carrying neither that log nor the
assets, so opening one hands the user half a diagram wearing its name. The
chrome's door closes — the File menu offers one Open, and it asks for a
`.bwx` diagram; `FileRequest::OpenDocument` and `FilePick::Document` go
with it. No native folder picker filters by suffix, so the picker opens on
folders and the *choice* is judged after the fact by
`file::refused_as_a_diagram`, whose refusal goes out through the ordinary
failure path and names what to pick instead of complaining that a folder
has no `log.jsonl`. A renamed container still opens, on the same
log-inside rule the extension was only ever a hint about. The command
line keeps its plain-file door, marked in `--help` as the developer's; the
import flows (SVG, PNG, D19's document import, paste) are untouched, since
an import is not an open.

Ratified at the 16-item review (2026-09-02): all fifteen original
items plus the two follow-ups stand. The contrast root cause —
egui's `strong_text_color` is `widgets.active.fg_stroke`, which this
palette darkens for accent-filled controls — is now encoded once as
`glass::full_ink`, the trap `tutorial::cues` had privately known;
three luminance guards pin it in the app's own palette (the stock-
visuals harness gap is why no test caught it). The menu glyph at 168
is rasterisation (2px horizontal strokes off the pixel grid), flagged
not fudged; the lever is the icon box size if wanted brighter. R53
stands: a projection is not a diagram to open — the chrome offers one
Open, a .bwx; the CLI keeps the dev door and says what it is.

**R54 (2026-09-01) The share bundle lands on desktop first, and a bundle
is never worked in place.** *"I suspect that the application should allow
for opening a .zip file of a .bwx, since that is something that can be
sent via e-mail or dropped on a thumb drive. Directories don't cross OS
boundaries all that well."* D1 already said this — the container is a
directory and the zip is "the single-file form for email/web, not the
working form" — and parked the zip in Phase 8 with the web build. The
user's observation is that the need is not the web's: a `.bwx` on a thumb
drive is a directory on desktop too, and directories are what the OS
boundary loses. So F9's share bundle comes forward to desktop now, with
its ledgered conclusion intact.

**The working container stays a directory.** A commit is an append and an
fsync, and the single-writer claim is a file another process can see;
neither has anywhere to live inside an archive. So opening a bundle is
*always* unpack-then-open, and there is no work-in-place path to keep in
step with the directory one. What comes out of the unpack is an ordinary
container, which means the chain, the stamp and the replay are checked by
the open it goes through rather than by a second verification written for
bundles.

**The unpack lands beside the zip**, under the name the zip travelled
under (`engine.bwx.zip` → `engine.bwx`), which is the directory a mail
attachment was opened from. Where that name is taken — by anything, not
just a diagram — the destination becomes a save dialog: an unpack that
overwrote would destroy a log to deliver one. The zip is left untouched
either way, inert source material.

**Two Open entries, and the constraint that forces it.** rfd exposes two
dialog modes with no third that admits both, and on every backend the
folder mode is an exclusive flag — the XDG portal's `directory`, Windows'
`FOS_PICKFOLDERS`, NSOpenPanel's `canChooseDirectories` with
`canChooseFiles` off. One entry would therefore have made one of the two
forms unreachable, which is worse than a second line in a menu. So *Open
diagram…* stays exactly as item 16 left it (folder picker, judged after
the fact by `file::refused_as_a_diagram`) and *Open shared diagram…* is a
file picker filtered to `.bwx.zip` and `.zip`. Revisit if rfd ever grows
the combined mode NSOpenPanel is capable of.

**What a bundle holds** is the container's own bytes, verbatim and sorted:
the log (which *is* the diagram, so re-serializing it would be rewriting
it), the projection, the assets and the git template, under one top-level
folder named as the container is — so unzipping with any tool yields the
`.bwx` folder rather than its contents loose in a download directory. The
names given to revs are `tag` records in that log rather than a sidecar
(D18), so they travel without being asked to. The lock does not travel: a
copy of it is a claim by a process that is not running. Deterministic —
sorted paths, one fixed timestamp — so the same diagram shares to the same
archive twice.

**Deferred:** D1's without-history variant. Cutting a bundle at a rev is
Save-as's question (R37), and asking it twice in two places is two answers
to one thing. Also deferred: the web build's share form, which is Phase
8's, and which this module's `pack`/`unpack` are native-only until.

`store::bundle` is the whole of it, with `zip` 8.6 (deflate through
flate2, which the tree already pulls; none of the eight other codecs the
default feature set turns on). The refusals are the interesting half — a
zip with no log in it, one with two diagrams, an entry naming a path
outside the diagram, and a destination already occupied — each named, and
a failure part-way removes what it had written, because half a container
is worse than none: the half still looks openable.

## Phase 7's UI half — the Learn segment and the player (2026-09-02)

> **Struck by R56 (2026-09-03).** Everything this section records was
> deleted; it is kept as the account of what was built and why. R55's camera
> is the one part that survived, on `src/spotlight.rs`.

Branch `phase7-ui`, on the main playbook's schedule, built against
`docs/cad-ui-spec.md` §8.3 and the surface `docs/choreographer-playbook.md`
froze. What it closes on *this* playbook's ledger:

- **R15 is answered on two surfaces**, which is what the ruling's own list of
  candidates was reaching for. The selection overlay's **overflow** carries
  *Walkthrough: `<title>` (m:ss)* for every walkthrough whose taught tools this
  selection's command set holds — §8.3's contextual entry, with the duration it
  asks for. The **Help menu** carries *Tutorials*, which opens the navigator
  onto Learn. The status-strip pill is not built: the padlock's neighbour is
  ambient state, and a walkthrough is an offer.
- **The right-click menu does not grow one.** §3.6 carries *exactly* the
  overlay's commands, and a walkthrough is an offer *about* the selection
  rather than a verb on it. Its own test says so, beside the invariant-3 test
  it would otherwise have broken.
- **Invariant 13 closes**, and with it the Phase D note recording it as
  unreachable by construction. The ⌘K palette grows a fifth source, `Learn`,
  with one row per walkthrough *and per chapter* — chapters are the addressable
  unit in the library, so a palette offering only the tutorials would satisfy
  the invariant in letter and reach a fifth of what Learn does.
- **The Learn segment's placeholder is gone**, and its filter joins the
  Hierarchy's in being cleared on dismissal (§8's transient-search rule, which
  names only the Parts filter but argues for both).
- **The player is not a berth.** Spec §2's three persistent regions stay
  three: like the toast it is transient, so it draws itself, takes no room
  from the canvas, and the safe area never hears about it. It is *placed*
  inside the room the chrome measured, so it lands in a corner of the canvas
  rather than under the navigator. The cost is the toast's, named: a fit taken
  while a player is up can land the model under it.
- **The ring is on the live cluster.** `tool_cluster_video` died with the old
  player and is not rebuilt: what a viewer is shown is the button they will
  actually press, in the place it actually is, which is the §1 one-layout
  argument the picture of a toolbar was only approximating.

Two things flagged for the user's pass. The shipped walkthroughs are titled by
their container names — `first-block`, not "Your first block" — because the
format ruling ties the display title to the diagram's own name and D20 says
that is the container's; renaming the three directories is the one-line fix if
prose titles are wanted. And R15's match is against the selection's whole
command *set* rather than the verbs the bar draws, because no shipped
walkthrough teaches an overlay verb — with §8.3's eventual ~20 walkthroughs
that intersection wants narrowing, and the natural narrowing is the drawn row
once one does.

Ratified at the share-bundle review (2026-09-02): R54 stands whole.
Two menu entries because rfd's pickers are exclusive modes on every
backend (folder OR file, never both) and one entry would make one
form unreachable; the archive holds one top-level `<name>.bwx/`
folder so any OS unzip yields the container; detection on read is
by where the log is, never the folder name; zip-slip refuses and a
part-failed unpack removes its half-written destination outright;
archives are deterministic (sorted paths, fixed timestamps) and land
through the atomic write. Tags travel free because they are log
records — the "sidecar" language in older notes was stale. The
without-history bundle stays deferred: cutting at a rev is Save-as's
question (R37), and two answers to one thing is how vocabularies rot.

**R55 (2026-09-02) A document step lands in sight — the two-layer
contract.** The user: *"camera position and active scope were not
tracked as part of the history operations. This leads to confusing
behavior where edits are happening that you cannot see."* Phase H's
stored-state restoration was working; what failed were the cases no
stored vantage can serve — a reopened session's reconstructed points
(all carrying the opening camera), an edit authored off-screen
through the palette, and a latent redo-destroyer in the re-pin path.
The contract now: restore the popped state's vantage and scope first;
where that cannot show the change, derive the framing from the
inverse commit itself via the choreographer's own camera plan (one
derivation, no second footprint implementation) — same scope brings
into view, a different scope opens and focuses. Nothing view-shaped
enters the log; §6.2, prefix Save-as and bundle determinism stand.
Named cost: an undo that had to move the camera lands one extra view
entry (egui's Undoer cannot replace its top point, and the
alternative destroys redo); an undo of something already in sight
adds nothing.

**R56 (2026-09-03) The tutorial subsystem is struck; R55 keeps its
camera on a tenth of the code.** The user: *"I have an alternate
solution... it looks like the existing approach will take significantly
more effort to reach a professional level of polish, and I need
something simpler."* Deleted whole: the choreographer
(`src/choreography/`, ~6,600 lines and 55 golden timelines), the
tutorial reader, library and player (`src/tutorial/`), the Learn
segment (`src/shell/learn.rs`), the shipped walkthroughs
(`fixtures/tutorials/`), and every surface that reached them — R15's
overflow offer and its Help ▸ Tutorials twin, invariant 13's palette
rows, the tool cluster's taught-tool ring, `Action::Walkthrough`, the
three `Walkthrough*` theme roles, and the xtask headlessness guard.
About 9,800 lines, just under a tenth of the tree.

R55's contract is unchanged and its eight probes still pass; only its
derivation moved. `src/spotlight.rs` (~350 lines) reads the same
question off the commit directly — the scope its ops worked in and the
union of its subjects' footprints, with a wire framed by its solved
polyline where the presentation has one and by its endpoints and
authored waypoints where it does not. Still one derivation, now
consumed twice rather than once: the camera aims at the region and the
canvas draws a rounded ring round it, `Role::ChangeRing` on the
brightest base, holding a second and fading over half of one.

Behaviour changes, all deliberate:
- The navigator has **two** segments. `PanelView::Learn` is gone, and
  the Help submenu keeps GitHub alone.
- A **history-panel rev pick** now lights the ring too, and brings the
  region into view. *(Amended the same day by R57: the pick opens the
  owning scope like every other step. The `Reach` distinction this
  paragraph introduced is gone.)*
- Invariants 3 and 8 are untouched; invariant 13 is **void**, not
  broken — the library it quantified over no longer exists.

Also fixed on the way through: the R55 harness held two egui contexts —
dispatch on one, paint on the other — which is why nothing raised in a
dispatch could be seen in a frame. It holds one now, which is what let
`a_step_rings_what_it_took_back` prove the ring through the real canvas
pass rather than through the module that computes it.

**R57 (2026-09-03) A visualized step brings its own scope, reversing
R56's `Reach`.** The user: *"Reverting to an older view does not change
the scope that is visible. When visualizing a step, the owning scope for
that step needs to be on the canvas. This should be computable from the
opcode."* R56 gave the rev pick a narrower reach than undo/redo on the
argument that browsing a log should not move a reader between levels.
That was wrong, and for the reason R55 was raised in the first place: a
lens showing a level the picked rev never touched is a drawing that did
not change, which is exactly the *"edits are happening that you cannot
see"* complaint. The distinction is deleted — `Reach` and both its
variants are gone, and `show_what_changed` opens the owning scope for
every caller.

The scope needed no new machinery: `spotlight::worked` already derives
it from the commit's ops (a shape's owner, a block's parent, a wire
label's wire's owner, and the pin exception, which belongs to the scope
its owner is a child of because a pin shows at its slot anchor on the
owner's outside). The pick was simply refusing to use what it was
handed.

Cost, named: `copy_out_of_the_past_pastes_into_the_present` now walks
back into the level it copies from, because that fixture's one seeding
commit works at the root while the block it copies lives a level in.
That is the honest shape of the test — a hand would do the same — not a
workaround.
