The overlap test read the badge's own box, which bobs, shrinks 10% when it
yields, and grows 4% on hover — so the mascot's answer to "is a card in my way"
depended on what it was currently doing. Against the live build, with three
cards up and no bubble, it flipped between drawn widths of 293 and 264 on its
own: the 0.9 yield scale, cycling.
Measure a probe span instead. It sits exactly where the badge sits, never
animates, and moves the size variable to .petal-corner so both are sized from
the same number. Nothing the mascot does can now change what it yields to.
Cards also settle a pixel from the corner routinely — a rail re-pack, a resize,
a browser bar appearing — so hold a yield until the card has retreated 24px
rather than deciding on the exact edge.
Verified in a real browser over CDP: seven overlap depths against the mascot's
top edge, each settling once and holding, drawn width steady where it used to
swing.
Claude-Session: https://claude.ai/code/session_01GJHNvirh7Hzhc9RL3HAvz7
The mascot shrinks a step when it yields to a card, and the overlap test read
getBoundingClientRect, which reports the scaled box. So a card resting at the
mascot's edge drove a loop: overlap, shrink, no overlap, grow, overlap again,
every poll tick.
Measure the layout box instead — centre plus offsetWidth/offsetHeight, which
the yield transform doesn't move. Hysteresis would have hidden the pulsing;
this removes the path that caused it.
Claude-Session: https://claude.ai/code/session_01GJHNvirh7Hzhc9RL3HAvz7
The overlap hook only watched .petal-rail-card, so the History and Garden
drawers still sat under the mascot — with a real control ("写作证明 ·
Writing passport") buried under the halo on the live build.
Match [role="dialog"][aria-modal="true"] as well. Both drawers already
render it, so this covers them and any future drawer without a selector
list to keep in sync.
Two things the follow-up note didn't anticipate:
- The hook now reports { cards, modal } separately. A card overlap still
lets the kitten wake for a bubble; a modal overlap yields
unconditionally — a cheer isn't worth covering the panel she just
opened on purpose.
- The speech bubble is its own layer, so fading the badge didn't hide it.
Hold it back while a panel is open; useCompanion keeps it in state, so
it reappears when she closes the panel.
Also guard the poll's setState on value equality, so the 500 ms tick
stops re-rendering the companion for an unchanged answer.
UX_REVIEW item 1 (redo does not re-apply an accepted suggestion) is
recorded as NOT REPRODUCIBLE. Read the prosemirror-history state directly
and hooked view.dispatch: redo works pressed immediately, after an 18 s
pause that lets a full re-check land, and with the editor never focused.
The doc's hypothesis is false — every re-check transaction is
decoration-only, which prosemirror-history ignores, and canRedo stayed
true throughout. Two real findings from that dig are written into the doc
instead: keyboard undo dies when focus isn't in the editor, and an undone
suggestion stays accepted server-side so its card doesn't reliably return.
Item 5's premise is also partly wrong and now re-scoped: the Chinese
sentence does produce a card with an English rendering, just labeled
Clarity rather than a first-class Translate type.
Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
When a suggestion card drifts into the mascot's corner, the kitten turns
translucent (15%), steps back 10% (standalone `scale` so it composes with
the bob animation), and lets clicks pass through to the card. It wakes
while its bubble or the picker is open, or once the corner clears.
Also adds UX_REVIEW_2026-07-27.md — the hands-on review of the live deploy
turned into implementation-ready items (repro, location, fix, acceptance).
Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua