Three concrete fixes plus a documented architectural direction.
1. Bars are now 2 rows: sbar = sep + content (top border only), cbar =
content + sep (bottom border only). The inward edges abut dapui's
own chrome (pane winbar + nvim's native horiz separator), which
serve as the inner visual boundary. Cuts each bar's vertical
footprint by one row.
2. Reverts the `fillchars.horiz = " "` hack from c1c3143. That fix
just converted nvim's dark phantom separator into a blank-but-still-
present row, still wasting vertical space. With 2-row bars the
inward edge is dapui/nvim-provided chrome by design — no need to
hide it.
3. New bug: closing a second buffer made bufferline's tabline
disappear, nvim gave the reclaimed row to the Breakpoints pane,
and because dapui has no WinResized listener (confirmed by reading
dapui/windows/layout.lua:82-138) its `win_states` went stale —
Breakpoints ended up 1 row taller than its buffer, rendering a
stray `~` EOB marker. Fix: track total dapui area in
`last_dapui_total_height`; on WinResized, if the total changed
(tabline toggled), call pin_dapui_sizes() to redistribute. User-
internal resizes (dragging a pane border within dapui) don't
change the total, so their customization still survives.
Plan/critique saved at ~/.claude/plans/snoopy-shimmying-puzzle.md —
the honest architectural verdict is that splits are the wrong
primitive for non-interactive chrome bars (3 parallel agents
converged on this). If 2-row bars still hit bugs, the documented
migration path is 1-row splits with `winbar = '%!v:lua.foo()'`
expression rendering.