feat(#81): IMP-54 frontend zone editing UI (u1~u4 edit-mode body-drag + emerald highlight + pure drag-math helper + vitest)
u1: 4 perimeter edge strips (~8px) + top-left grip chip at zone wrapper
provide an edit-mode pointer-event surface (zIndex 25) so wrapper-level
handleZoneMouseDown becomes reachable in edit mode. Wrapper stays
pointerEvents:none and iframe stays pointerEvents:auto to preserve
text-edit reachability (A8 guardrail). Resize handles (z-30) win in
overlap regions. Iframe pointer-events temporarily forced none during
drag to prevent mouseup leak.
u2: Edit-mode isSelected branch reuses selectedZoneId with emerald visual
(border-emerald-500 / bg-emerald-500/10) distinct from pendingLayout
blue, decorative-only (pointerEvents:none inherits via wrapper rules).
u3: Pure drag math extracted to slideCanvasDragMath.ts — DRAG_THRESHOLD_PX,
crossedDragThreshold(dx, dy) strict Math.hypot > 5, and clampZoneMove
pixel→fraction conversion with x∈[0, 1-w] / y∈[0, 1-h] clamp.
Resize math (makeResizeHandler) untouched.
u4: Vitest coverage (12 tests, 3 describe blocks) on the pure helper:
threshold strict boundary at (3,4)/(5,0)/(0,5), above-threshold,
negative-symmetric, clamp negative→0, max-edge → 1-w / 1-h, per-axis
independence, non-square 500×250 slide-body, return-shape {x,y} only.
Stage 4 verify: pnpm exec vitest run client/src/components/slideCanvasDragMath.test.ts → 12/12 PASS.
Scope: edit-mode UX only. No HTML text modification, no automatic frame swap, no MDX touched.
Depends on: #9 IMP-09 (--override-zone-geometry backend wire), #80 IMP-52 (user_overrides.json zone_geometries persistence).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>