HP-1543-01 (medium): exiting an editor AFTER switching floors inside it left
the editor working zoom on screen in view mode — the snapshot guard
(snap.space) dropped the restore and nothing else put the viewport back.
A space-mismatched exit now falls back to _restoreZoom() of the current
floor's saved view zoom (per-space store/LS_ZOOM were never polluted, the
view-only _saveZoom guard is untouched). Regression: crossFloor* asserts in
demo/smoke_zoom_out.mjs — red on v1.54.3, green now.
HP-1543-02 (low): a rapid off->on retrip before the current flash ended did
not restart the CSS animation: the senseflash class and animation-name never
changed, so the browser kept the old timeline and the second detection played
nothing once the first one-shot had finished (base opacity 0). Every
witnessed trip now bumps a generation counter; its parity alternates the
identical keyframes hp-sense / hp-sense-b via the .sf2 marker — a new
animation identity forces a fresh timeline per detection. flashTs and the
window timer re-arm as before; prefers-reduced-motion keeps the static ring
(retrip only extends the window). Regression: rapid* asserts in
demo/smoke_motion_sense.mjs — red on v1.54.3, green now.
Owner's contract (2026-08-01, вариант «б») replaces yesterday's 'sense' class
(29dae45, lived <1 day, stored nowhere — no migration):
- MOTION (device_class motion): a SHORT one-shot flash at the off→on
transition — 3 beats of the yellow ring (hp-sense 1.1s x3, ~3.3s), then
silence even while the entity still reports 'on': «движение = разовая
вспышка в момент обнаружения; cool-down не пульсирует». A new off→on trip
flashes again. Tracking lives in _senseRt (markerId → last state + flash
ts), stamped by _senseTick on the hass tick (the _vacTick pattern); one
setTimeout per entry repaints the card when the window closes so the
'senseflash' class is dropped without a state change (cleared in
disconnectedCallback). First sight already-'on' and unavailable→on do not
flash — not a witnessed detection.
- OCCUPANCY/PRESENCE while 'on': 'sensehold' — a STATIC yellow ring, no
animation, opacity 0.4 (the reduced-motion brightness): «присутствие =
статичное кольцо пока обитаемо».
- Unchanged: the yellow FILL stays reserved for «включено», alarm's red ring
wins on the shared ::after, ghosts draw nothing.
- prefers-reduced-motion: the flash becomes a static ring for the same ~3.3s
window (consistent with alarm); sensehold is static by design.
Smoke smoke_motion_sense.mjs rewritten to the new contract (13 asserts fail
on the pre-fix bundle): finite iteration count '3', flash gone after 3.6s
while state is still 'on' (the key assert), re-trip flashes again,
occupancy/presence static ring, neutral badge, hidden ghost inert.
shot_motion_sense.mjs now cycles off→on to arm the flash.
Owner's rule (2026-08-01): the yellow FILL is reserved for 'on' — a tripped
sensor keeps the neutral badge and gets a new 'sense' state class instead:
a gentle .dev.sense::after ring in the .dev.on yellow (--hp-on), 2.4s period,
max opacity 0.5 — the soft sibling of the red .dev.alarm siren. Declared
before .dev.alarm so red wins on the shared ::after if both ever apply;
ghosts don't pulse (hidden gate already strips the state class).
prefers-reduced-motion: static faint ring, same treatment as alarm.
Smoke smoke_motion_sense.mjs (fails on the pre-fix bundle): sense class on/off,
::after animation, neutral background (not --hp-on), occupancy/presence,
hidden ghost. shot_motion_sense.mjs captures the ring for review.