Half of this was already built. A floating dock's height is written at tear-off,
clamped, persisted and restored — and nothing could change it, because there was
no handle. A docked dock had no height at all, being `position: fixed` with both
`top` and `bottom` anchored. `grep row-resize src/` was empty.
Both docks get a bottom-edge splitter, the width mechanism turned ninety
degrees: one `Draggable` with no feedback, one monitor triple, one clamp. Its
own `DND.dockHeight` type rather than a suffix on `splitter`, because
`watchSplitters` selects with `startsWith(DND.splitter)` and an
`"airship:splitter-v"` would match it and be parsed as a width drag.
A docked panel keeps filling its edge until you pin it: `.dock-h` drops the
`bottom` anchor and takes `--*-h`, and `h: 0` means unpinned. That sentinel is
the load-bearing part. `clampHeight`'s floor is `MIN_DOCK_H`, so a stored 0 run
through it comes back as 200 — every docked panel in every install silently
pinning itself to a fifth of the window on the first reload. `restoreSizes`
preserves it, `applyPlacement` removes the custom property rather than
publishing a clamped 0 (so a class regression degrades to the anchors instead of
snapping to 200px), and a cancelled drag writes the raw stored pair straight
back rather than going through the clamp — `dockHeight` substitutes
`offsetHeight` for the sentinel, so restoring from it would end an Escape by
pinning a panel that was filling its edge, and persist it.
Both branches of `dockHeight` start from what the panel is *painted* at.
`applyPlacement` paints `min(h, room below y)` and deliberately leaves `h`
alone, so a panel torn off at full height and carried down the screen had a
stored 860 against a painted 380 — and the grip did nothing for the first 480px
of travel.
Docked height goes in the size store, renamed from `dock-widths` with a
migration off the old key, rather than joining the placement blob:
`restoreDocks` skips anything that is not floating, so a docked height stored
there would be written on every drop and never read back — a worse failure than
not persisting it, because it looks like it works until you reload.
`resetWidth` becomes `resetSize` and answers both axes. Width goes back to a
literal because a panel's default width is one; height *unpins* instead,
because a docked column's right height is "the edge" — a fact about the window
that a literal would get wrong the moment it changed size, and that `bottom`
answers for free.
Both resize gestures shipped undocumented — the splitter is a 7px transparent
strip that shows a hairline on hover — so the catalog gains rows for them and
they appear in the sheet and `CONTROLS.md` like everything else.