Owner ruling on the empty graph (honesty principle — dead controls are
lies): the top-chrome actions row now gates per control.
- Import Notes renders ALWAYS (graph enabled) — the one action that
works on an empty graph, the entry point itself. It used to hide with
everything else, leaving the empty state a dead end with no entry.
- Search + settings render only when the snapshot has nodes, and their
appear-once mount carries a deliberate 200ms fade (graph-chrome-fade-in,
globals.css — the settings-pop register, reduced-motion appears
instantly). Computed animationName verified live.
- The row's body is now the exported pure GraphChromeRow (the
DeleteNodeBody pattern) — the no-match note's truth (visibleIds +
snapshot) stays upstream, computed by the wrapper.
- The empty-state placeholder copy is LEFT AS-IS (SCOPE rule: it already
has copy — icon, title, and the session-mapping paragraph).
Pins (tests/render-graph-chrome.test.tsx): empty → Import Notes present,
search/settings absent, no fade carriers; populated → all three, exactly
two fade carriers; the no-match note follows its upstream truth. SSR of
GraphChromeRow both ways.
Live-verified on :3001 with scratch data: empty graph → Import Notes
only + placeholder intact; a real import through the button (file input
via DataTransfer) → nodes land, search + settings fade in (computed
chrome-fade-in on both carriers); wipe → reload → back to Import Notes
only (the reverse path). Scratch graph restored byte-identical.
932/932, tsc/lint/build green.