# Mascot Bear Animation — Conversation Scope

## Scope of THIS conversation thread
**Only topic:** Mascot bear animation work.
Do not drift into KDS components, screens, Figma file edits, or other Kinko design system work in this thread.

## Subject
- Mascot: **bear** (referred to as "tedi")
- Past reference file: `tedi.svg` (Lottie bear "hi" animation — logged in claude-account-history.md line 44)
- `tedi.svg` is NOT currently present in `/Users/ravia/Documents/Kinko Design Code/` or `/Users/ravia/Desktop/Kinko Designs/` — user needs to re-share if continuing that exact file

## Working approach
1. Understand the vector layer structure of the bear mascot:
   - face / head
   - eyes
   - mouth / nose
   - ears
   - hands / arms
   - legs / feet
   - body / torso
   - accessories (if any)
2. Identify which layers need to move for each animation state (e.g. "hi" wave = arm + hand rotation; blink = eye scaleY).
3. Decide output format per request: CSS keyframes / SVG SMIL / Lottie JSON / Rive.

## Output preference
- User iterates quickly with short feedback — keep responses tight.
- No emojis in deliverables.
- Mobile-friendly sizing if used in app prototypes.

## Pending
- Need user to re-share or point to current mascot bear source (SVG / Figma node).

## 2026-05-28 — Video → Lottie session

### Source
- User-supplied: `/Users/ravia/Downloads/designarena_video_m92vibyp.mp4`
  - 1440×1440, 24fps, 5.04s, h264, solid black bg
- Reference SVG (static, one pose): `/Users/ravia/Downloads/Group 24.svg`
  - 243×271 viewBox, 45 flat paths (no `<g>` groups), no bg, arms-down idle pose
  - Mascot is koala-style (pink ear cups, no muzzle ridge) — user calls it "bear"

### Layer map of `Group 24.svg`
| Region | Path indices | Notes |
|---|---|---|
| Body outline | 1 | `#2B3A5C` navy stroke shape |
| Body fill | 2 | `#B4CBD4` |
| Belly white | 3 | `#EBF3F5` |
| Belly shadow | 4, 5, 14, 15, 16 | `#AFC0D1`/`#94ADC1`/`#829DAD` |
| Head base | 19 | `#B4CBD4` |
| Head shadow | 27, 28 | `#94ADC1` |
| Left eye / white / glint | 32 / 33 / 34 | dark / white / `#6A6E9C` |
| Right eye / white / glint | 29 / 30 / 31 | same |
| Muzzle dark patch | 24, 25 | navy |
| Muzzle highlight | 26 | `#9D9FC4` |
| Muzzle white | 20 | `#EBF3F5` |
| Nose / lip | 21 | `#2E325C` |
| Tongue | 22 | `#E07C88` pink |
| Lip shadow | 23 | `#2D325B` |
| Left ear outer | 40–45 | `#B4CBD4` + shadows |
| Left ear inner pink | 36, 37 | `#F28D8D` / `#E77479` |
| Right ear outer | 35 | `#AFC4CF` |
| Right ear inner pink | 38, 39 | `#F28D8D` / `#E77479` |
| Feet / legs | 8, 9, 10, 11, 12, 13, 17 | bottom shadows |

### Motion timeline (24fps, 5.04s)
- 0.0s — R arm up high, mouth open + tongue (greet)
- 1.6s — eyes SQUINT closed, mouth wide laughing, L arm out
- 2.3s — both arms spread, mouth open
- 3.7s — both arms wide, mouth closed smile
- 4.7s — idle, arms at sides

### Deliverables (in `/Users/ravia/Desktop/Kinko Designs/bear-output/`)
- `bear-wave.webp` — 1.6 MB, 400×400, transparent, animated WebP (best for production)
- `bear-wave.json` — 19 KB Lottie JSON, refs `images/f01.png`–`f61.png` (300×300, 12fps, 61 frames)
- `bear-wave.lottie` — 5.66 MB dotLottie zip (manifest + JSON + frames bundled)
- `images/` — 61 transparent PNG keyframes
- `preview.html` — side-by-side viewer (WebP + lottie-web playback + dotLottie note)

### How transparency was done
ffmpeg `colorkey=0x000000:0.10:0.05` — black bg keyed to alpha. Bear's darkest line is `#2B3A5C` so threshold of 0.10 is safe.

### Honest caveats
- This is a **raster-sequence Lottie**, not a vector Lottie. Vector Lottie of this motion would require splitting `Group 24.svg` into separate arm/mouth/eye paths (they're baked into one body silhouette) — not possible from this single-pose source.
- For a true scalable vector Lottie, need: (a) multi-pose source SVGs from designer, or (b) hand-rig the arms by editing path data, or (c) rebuild in Bodymovin/Rive from scratch.
- WebP is the cleaner deliverable for the same visual result at 1.6 MB.

### Tooling installed
- `/tmp/ffmpeg` — static binary 8.1.1 from evermeet.cx (no Homebrew). Use this path for any future ffmpeg work in this account.
- **SVG → PNG rendering**: cairosvg unusable (no native cairo). Use **sharp** via node: `cd /tmp/svgrender && node -e "...sharp(...).resize(N).png().toFile(...)"`. The package is installed at `/tmp/svgrender/node_modules/sharp` — persistent.

## 2026-05-28 — Onboarding flow from 3 pose SVGs

### Sources
- `/Users/ravia/Downloads/kola 4.svg` → pose1 (256×274, 40 paths) — **"Hi" wave**, koala with hand raised near face
- `/Users/ravia/Downloads/kola2.svg` → pose2 (269×285, 83 paths) — **Confused/searching**, holding world map with red X + location pin, mouth-O, eyes worried
- `/Users/ravia/Downloads/kola3.svg` → pose3 (282×282, 106 paths) — **Notifications**, sitting with phone, "Enable" button visible, bells on both sides, "ping" text

### Interpretation
These are **3 separate onboarding scenes**, NOT a frame sequence. Color palette consistent (navy `#2B3A5C` outlines, blue-gray `#B4CBD4` body, pink ear cups `#F28D8D`).

### Copied to project at
`/Users/ravia/Desktop/Kinko Designs/bear-output/poses/`
- `pose1.svg`, `pose2.svg`, `pose3.svg` (originals)
- `pose1.png/pose2.png/pose3.png` (400px PNG renders, ~50 KB each)
- `pose1_800.png/pose2_800.png/pose3_800.png` (800px for Lottie embed)

### Deliverables
- `onboarding.html` — **vector slideshow** (CSS crossfade + per-scene micro-anim: pose1 bob, pose2 sway+❓pop, pose3 settle+ping fade). Loops every 2.8s × 3 = 8.4s. ~96 KB total (HTML + 3 SVGs). **Recommended for production** — scales perfectly, tiny.
- `koala-onboarding.json` — **Lottie JSON**, 429 KB, 7.5s loop, 30fps, 800×800. Embeds the 3 PNG renders as base64. Crossfade keyframes: 15-frame overlap between poses. Plays in lottie-web.
- `preview.html` — combined viewer (slideshow + Lottie + original wave WebP + raw SVGs gallery)

### Onboarding copy used
1. "Hi, I'm Kinko" / "Your health-insurance buddy. Let's get you set up."
2. "Find your way" / "Discover plans, networks, and coverage in one place."
3. "Stay in the loop" / "Enable notifications so you never miss an update."

### Future enhancement ideas
- Replace embedded PNGs in Lottie with inline SVG paths → true vector Lottie (~80 KB instead of 429 KB).
- Add hand-wave keyframe to pose1 by isolating the arm path and rotating it (arm is a single path with bake-in body — would need path-split).
- Pose2: animate the question mark inside the SVG (currently overlay div). Same for ping/bells in pose3.

## 2026-05-28 — v2 rig with per-part animation

### Animation rule of thumb (CRITICAL)
The body outline (single closed `#2B3A5C` path) wraps the WHOLE silhouette including arms/legs. Rotating arm/leg fills leaves the outline behind → looks broken.

**Animate only:**
- Eyes/pupils (sit ON TOP of head — independent paths)
- Pink ear/cheek interiors (sit INSIDE the ear/face — independent)
- Mouth interior (small inset)
- Objects HELD by the koala (map, phone, bells — have own complete outlines)

**Avoid animating:**
- Body outline path 0
- Arm/hand/leg fills (baked into outline)
- Head/ear silhouette fills (baked into outline)

### Verified path indices (via jsdom in /tmp/svgrender)
| Part | Pose 1 (kola 4.svg, 39 paths) | Pose 2 (kola2.svg, 82 paths) | Pose 3 (kola3.svg, 105 paths) |
|---|---|---|---|
| Left eye (dark+white+glint) | idx 9, 10, 11 | idx 14, 15, 16 | idx 11, 12, 13 |
| Right eye (dark+white+glint) | idx 12, 13, 14 | idx 17, 18, 19 | idx 14, 15, 16 |
| Pink ear/cheek fills | `#F39394`/`#EB7880` (4 paths) | `#F28D8D`/`#E77479` (4 paths) | `#F28D8D`/`#E77479` (6 paths — incl. cheek blush) |
| Mouth-O | — | `#E07C88` (1 path) | — |
| Held object | (arm baked in — skip) | Map: tan palette `#E6D7AE`/`#DEC9A0`/`#F4EEDE`/`#A6A374`/`#635E45`/`#4F4532`/`#DEBF93`/`#B47F52`/`#D76F60` (37 paths) | Bells: yellow `#FDCF4C`/`#F3B240`/`#FDF2B6` (6 paths — split L/R by x < 141) |

### Verified animation behavior (puppeteer at runtime)
- Pose 1 eyes: `blink 3.2s` — scaleY collapse cycles around fill-box center
- Pose 2 eyes: `dart 2.6s` — translateX ±2px + scaleY blink at 90%
- Pose 2 map: `mapsway 3.2s` — rotate ±1.8° around `50% 100%` (bottom-center where hands hold it)
- Pose 2 mouth-O: `opulse 1.4s` — scale 1 ↔ 1.25
- Pose 3 bells: `ringL/ringR 0.42s` — rotate ±10°, opposite phase, pivot at `50% 0%` (top of bell)

### Tooling notes
- **jsdom** at `/tmp/svgrender` — use for DOM-accurate SVG parsing/path-counting
- **puppeteer** at `/tmp/svgrender` — use for runtime screenshot capture + computed-style sampling
- Use these to VERIFY indices before committing — naive regex counting was off by 1 in pose 1.

### Built-in vs overlay decorations
SVGs contain path-drawn glyphs that look like text (no `<text>` nodes):
- Pose 2: a small `?` near the right ear in `kola2.svg` (path-drawn)
- Pose 3: "ping" text + "Enable" pill + bell decoration lines in `kola3.svg`

Current onboarding still adds CSS overlay divs (`.qmark`, `.ping`) on top because the built-in versions are static. Future enhancement: identify those built-in paths by bounding box and animate them in place instead of overlay duplicates.

### Files in `bear-output/`
- `onboarding.html` — v2 with inline SVG + JS rigger + per-part CSS animations
- `onboarding-v1.html` — original whole-body-bob version (kept for compare)
- `koala-onboarding.json` — Lottie image-sequence variant (no per-part rig, just crossfade)
- `preview.html` — combined gallery
- `wave-test.html` — isolation test that proved the arm is separable

## 2026-05-28 — CRITICAL CORRECTION: body outline does NOT bake in the arm

### What I assumed wrong in v2
I said "body outline path wraps the WHOLE silhouette including arms/legs" — and concluded arms could not be animated. **This was wrong for pose 1.**

### Proof (via svgpathtools)
Parsed `pose1.svg` path 0 (the navy outline). Sampled 60 points around the closed path. The outline traces a smooth body silhouette **without bulging out for the raised arm**. The arm is drawn as 3 OVERLAY paths sitting on top of the body:
- idx 28: `#B6CDD9` arm main (starts 145.8, 172.7) — shoulder origin
- idx 29: `#95AFC5` arm shadow (starts 195.6, 195.2)
- idx 30: `#95AFC5` hand detail (starts 172.9, 171.4)

### Method: detecting bake-in vs overlay
For each pose, parse the body-outline path with `svgpathtools`, sample N points along it, and inspect the curve:
- If the path bulges out where the visible limb is → limb is BAKED IN, cannot animate without path surgery
- If the path traces a smooth body silhouette in that region → limb is OVERLAY, animate freely

For pose 1: arm is overlay ✓
For pose 2 & 3: not yet sampled — TODO.

### Animating the arm — shoulder pivot via transform-origin
Wrap the arm paths in `<g class="wave-arm-group">`, then compute:
```js
const bb = armG.getBBox();
const px = ((shoulderX - bb.x) / bb.width)  * 100;  // % within fill-box
const py = ((shoulderY - bb.y) / bb.height) * 100;
armG.style.transformOrigin = `${px}% ${py}%`;
```
For pose 1 shoulder at (146, 173): result → `18.9% 26.1%` of the arm bbox.

Apply CSS:
```css
.wave-arm-group{ transform-box: fill-box; }
.slide[data-pose="1"].is-active .wave-arm-group{ animation: armidle 2.4s ease-in-out infinite; }
@keyframes armidle{ 0%,100%{ transform: rotate(-3deg);} 50%{ transform: rotate(5deg);} }
```

### Important gotcha for THIS source artwork
Pose 1's static pose is a "chin-touch / pondering" gesture, NOT a wave. The hand starts NEAR the face. Large rotations (>15°) detach the hand from the face and look unnatural. Use SMALL rotations (±3 to ±5°) for an "alive" thinking gesture rather than a true wave. To get a real wave you need a source SVG where the hand is raised above the head with an open palm.

### Tools used
- `python3 -m pip install --quiet svgpathtools` — path d-attribute parsing & sampling
- `jsdom` at `/tmp/svgrender` — accurate DOM-indexed path queries
- `puppeteer` at `/tmp/svgrender` — runtime screenshot verification at forced rotation values

### Status of poses 2 and 3 — outline sampled, arms are BAKED IN
- **Pose 2 outline** (56 segments): bulges outward at idx 10–12 sampled points (right side y≈140–190 jumps x from 207 to 243 then back). Arms baked in.
- **Pose 3 outline** (59 segments): same pattern — outline traces around the right hand holding phone (bulges from idx 9–11) and the pointing/sitting posture below. Arms baked in.
- **Cannot rotate arms in pose 2 or 3 without splitting the body outline path** (path surgery). Deferred.

### What WAS added for pose 2 & 3 (per-part, no path surgery needed)

**Pose 2 additions:**
- `.map-pin` — paths idx 42 (199,165) + idx 50 (137,206), both fill `#D76F60` (red location pin + X mark). Pulse animation: `scale 1↔1.18`, `opacity 1↔0.85`, 1.4s loop.

**Pose 3 additions:**
- `.phone-group` — paths idx 56, 57, 59, 61, 75 (phone case + screen + highlights). Tiny rotational vibrate + xy jitter, 0.42s loop synced with bell ring.
- `.enable-pill` — paths idx 25, 27, 30, 32, 34, 35 (light blue `#D3F2F9` pill + letters of "Enable" on the belly). Glow pulse: `scale 1↔1.04`, `opacity 1↔0.85`, 1.6s loop.
- `.swoosh-l` — paths idx 97, 98, 102, 103 (small navy strokes around left bell). Opacity flash + scale 0.92↔1.08, synced 0.42s with bell-L.
- `.swoosh-r` — paths idx 91, 93, 95, 96, 99, 100, 101, 104 (small navy strokes around right bell). Same as swoosh-l, synced with bell-R.

### Full per-part rig table (final)
| Pose | Animated body part | Path indices / fill | Animation |
|---|---|---|---|
| 1 | Left eye | idx 9,10,11 (grouped) | blink scaleY 1↔0.08, 3.2s |
| 1 | Right eye | idx 12,13,14 (grouped) | blink |
| 1 | Pink ear interior | fill `#F39394`/`#EB7880` | opacity pulse 1↔.72, 2.6s |
| 1 | Raised arm (overlay) | idx 28,29,30 (grouped) | rotate -3°↔+5° around shoulder (146,173), 2.4s |
| 2 | Left eye / right eye | idx 14-16 / 17-19 | dart translateX ±2 + blink, 2.6s |
| 2 | Pink ear interior | fill `#F28D8D`/`#E77479` | opacity pulse |
| 2 | Map | tan palette (37 paths grouped) | sway rotate ±1.8° from bottom-center, 3.2s |
| 2 | Map pin + X | idx 42, 50 fill `#D76F60` | scale 1↔1.18 + opacity, 1.4s |
| 2 | Mouth-O | fill `#E07C88` (1 path) | scale 1↔1.25, 1.4s |
| 3 | Left eye / right eye | idx 11-13 / 14-16 | blink 3.2s |
| 3 | Pink ear/cheek blush | fill `#F28D8D`/`#E77479` (6 paths) | opacity pulse |
| 3 | Bell L / Bell R | yellow palette split x<141 | rotate ±10° opposite phase, 0.42s |
| 3 | Notification swoosh L/R | idx 97-103 / 91-104 (grouped per side) | opacity .35↔1 + scale .92↔1.08, 0.42s |
| 3 | Phone | idx 56,57,59,61,75 (grouped) | xy jitter + rotate, 0.42s |
| 3 | "Enable" pill | idx 25,27,30,32,34,35 (grouped) | scale 1↔1.04 + opacity, 1.6s |

### Test method (runtime verification)
- Disable autoplay before sampling: `for (let i = 1; i < 1000; i++) clearInterval(i);`
- Set is-active per slide, wait 1000ms for fade transition to settle before screenshot
- Verify transforms via `getComputedStyle(el).transform` per .slide.is-active descendant

## 2026-05-29 — Creative-direction framework

### User principle (verbatim feedback to internalize)
> "Your job is to understand the SVG image, read the emotions and what is the best way to do the animation that you need to think, which appropriate for that image or svg, you can do the explorations, like how midjourney, gemini, seeddance creates, I cannot tell you everytime, you need to understand the premise and accordingly take the decision and show me we can do this or that."

So: **stop asking what motion to apply.** Read the pose, decide the emotion, propose 2–3 motion directions per pose with rationale, show them side-by-side, let user pick.

### How to read each pose
- **Pose 1** = chin-touch + soft brow + closed mouth → "pondering / curious / slight worry" (NOT a Hi wave)
- **Pose 2** = mouth-O surprise + holding open map + built-in `?` + red X + pin → "lost / searching"
- **Pose 3** = sitting + smile + phone + bells L+R + "ping" text + "Enable" pill → "alert / CTA-ready"

### Three motion directions I propose per pose (deliverable: `bear-output/exploration.html`)
| Pose | A (default-calm) | B (alt-direction) | C (alt-direction) |
|---|---|---|---|
| 1 | **Tap the chin** — chin bop + slow blink + ear breathe | **Searching mind** — eye darts side-to-side + ear twitch + arm still | **Eureka!** — eyes widen + arm drops + sparkle pop |
| 2 | **Calm scan** — map sway + eye dart + `?` pop + mouth-O pulse | **Found it!** — pin pulses big + screen flash + eye lock | **Lost & spinning** — bigger map rotation + frantic dart + fast `?` bounce |
| 3 | **Notification storm** — bells ring + swooshes flash + phone buzz + pill glow | **Tap to enable** — pill pulses big with green glow + arrow points + bells subtle | **Calm alert** — slow bell sway + soft swoosh fade + gentle pill breathe |

### Build pattern for exploration pages
- One HTML file with `data-pose` × `data-variant` attributes per cell
- All cells share the same JS rigger functions (rigPose1Into / 2 / 3)
- All variants share the same anatomy classes (`.eye-group`, `.wave-arm-group`, `.map-group`, `.bell-l-group`, etc.)
- Per-cell animation defined by `.cell[data-pose="N"][data-variant="V"] .part { animation: ... }` — scoped, doesn't bleed
- Each cell has a tag chip + name + 1-line rationale so reviewer can pick quickly

### Workflow going forward
1. New SVG arrives → read pose & emotion (state explicitly in chat)
2. Propose 2–3 motion directions with rationale
3. Build a 1-row exploration board (don't wait for go-ahead on the concept)
4. User picks A/B/C
5. Polish the pick into final Lottie + WebP via puppeteer frame-capture pipeline (same as koala-wave.json / .webp)

## 2026-05-29 — Path surgery method (CRITICAL discovery)

### The "outline ghost" problem
Earlier I claimed the arm in pose 1 was NOT baked into the body outline. **That was wrong.** Rendering path 0 (the navy outline) alone with no fills shows the silhouette DOES include the raised arm shape. Rotating arm-fill paths alone leaves the navy arm silhouette behind → visible "double arm" ghost.

### Diagnosis steps (use this for new SVGs)
1. Render `path 0` of the SVG to a PNG with no other paths → does it include the limb shape?
2. If yes → limb is baked in. Need path surgery OR skip animating that limb.
3. If no → limb is overlay, animate freely.

### Path surgery procedure (Python + svgpathtools)
For pose 1, the arm portion of path 0 was segments 10–12 (3 contiguous bezier curves):
- Entry (shoulder): start of seg 10 = (185.5, 161.4)
- Exit (armpit):    end of seg 12 = (189.1, 209.2)

Splitting algorithm:
```python
from svgpathtools import parse_path, Path, Line
p = parse_path(outline_d)
ARM_IDX = [10, 11, 12]  # determined by sampling segment midpoints in arm bbox
entry = p[ARM_IDX[0]].start
exit  = p[ARM_IDX[-1]].end

# Closed arm-outline = arm segments + closing line back to start
arm_outline = Path(*[p[i] for i in ARM_IDX], Line(exit, entry))

# Closed body-no-arm = pre-arm segments + bridge line + post-arm segments
body_no_arm = Path(
    *[p[i] for i in range(0, ARM_IDX[0])],
    Line(entry, exit),
    *[p[i] for i in range(ARM_IDX[-1]+1, len(p))]
)
```
Both new paths self-closed and renderable. Together they tile back to the original silhouette (overlapping shoulder-line cancels visually).

### Resulting file
`bear-output/poses/pose1-rigged.svg` (18 KB, 40 paths vs original's 39):
- idx 0: body-no-arm outline
- idx 1..27: original paths (face, eyes, body fills, ears, etc.) unchanged
- idx 28: NEW arm-outline path (the navy outline split out)
- idx 29..31: original arm-fill paths
- idx 32..39: remaining paths

Wrap idx [28, 29, 30, 31] in `<g class="arm-rig">` → arm rotates as ONE unit (outline + 3 fills together) → NO GHOST.

### Verified
- `cmp_orig.png` vs `cmp_rigged.png` rendered at 400px → pixel-identical (surgery preserves visual)
- `rigged_35deg.png` (arm rotated +35°) shows hand moved to face with NO leftover navy silhouette
- `ghost_proof.png` (same rotation on UN-rigged SVG) shows the double-arm ghost clearly

### Updated deliverables (2026-05-29)
- `wave-koala-rigged.html` — uses `pose1-rigged.svg` with proper arm-rig group + shoulder pivot at (185.5, 161.4)
- `koala-wave.json` — **regenerated** from rigged frames, 1.77 MB, 48 frames @ 24fps, 2s loop, ghost-free
- `koala-wave.webp` — **regenerated**, 132 KB, transparent, ghost-free
- `koala-frames/f00–f47.png` — clean PNG sequence from rigged source

### Decision tree for animation approach (for an animator)
| Source SVG state | Best approach |
|---|---|
| Limb has its own outline path | Animate live SVG via CSS, optionally frame-capture for Lottie |
| Limb baked into body outline | Path surgery → re-rigged SVG → animate as above |
| Multiple needed poses (e.g. open mouth, closed mouth) | Designer redraws or 2D animator hand-illustrates frames |
| Need ultra-small file | Vector Lottie hand-authored as shape layers |
| Need maximum fidelity, file size flexible | PNG sequence Lottie (what we use, ~1.7 MB embedded base64) |

## 2026-06-01 — Multi-arm path surgery + z-stack-safe animation

### Extended path surgery to pose 2 & pose 3
- `poses/pose2-rigged.svg` — 84 paths (was 82). Sliced **both arms** (segs 11-17 right, 31-37 left). Both shoulders identified at SVG coords (193.4, 152.5) and (64.8, 155.0).
- `poses/pose3-rigged.svg` — 106 paths (was 105). Sliced **right phone-arm only** with narrow range (segs 11-17). Left arm has complex sitting geometry — skipped.
- Pose 1: still 40 paths (was 39), arm segs 10-12.

### CRITICAL fix: z-stack-safe arm animation (don't use wrapInGroup for split parts)
**Problem:** When arm-outline (early in z-stack, e.g. idx 1) and hand fills (late, e.g. idx 79-80) get wrapped into one `<g>`, the group lands at the LAST path's position. The navy arm-outline ends up drawn LAST = on top of the map → visual breakage.

**Fix:** Don't group split arm parts. Apply the animation to each path INDIVIDUALLY but use SVG world-space transform-origin:
```css
.arm-r-outline, .arm-r-fill{
  transform-box: view-box;
  transform-origin: 193.4px 152.5px;  /* SVG user coords = shoulder */
  animation: armRsway 3.2s ease-in-out infinite;
}
```
`transform-box: view-box` makes the `px` values resolve in viewBox user coords, so all parts pivot around the same world point regardless of each path's own bbox.

### Final per-part rig (3 poses, end-to-end)
| Pose | Sliced (path surgery) | Sliced parts animate as | Non-sliced animated parts |
|---|---|---|---|
| 1 | Right arm (segs 10-12) | `.wave-arm-group` wraps outline+3 fills, rotate -12°↔+22° around shoulder (185.5, 161.4) | eyes ×2 blink, pink-ear pulse |
| 2 | Right arm (segs 11-17), Left arm (segs 31-37) | `.arm-r-*` rotate -4°↔+4°, `.arm-l-*` opposite phase ±4° using `transform-box: view-box` with absolute SVG-coord pivots | eyes dart+blink, ear pulse, map sway, pin pulse, mouth-O pulse |
| 3 | Right arm (segs 11-17, narrow) | Not animated (existing pose 3 anims already rich) | eyes blink, ear/cheek blush pulse, bells L/R ring, swoosh L/R flash, phone buzz, enable pill glow |

### Index shifts after path surgery
Adding N sliced paths just after body-outline shifts all subsequent original paths by +N in DOM index.
- Pose 1 rigged: +1 shift (arm-r-outline added)
- Pose 2 rigged: +2 shift (arm-r + arm-l outlines)
- Pose 3 rigged: +1 shift (arm-r only)

So rigger code must update its pickPaths indices accordingly. Example for pose 2:
- Original eye idx 14-16 → now 16-18
- Original idx 75-76 (left hand fills) → now 77-78
- Original idx 77-78 (right hand fills) → now 79-80

### Visual verification protocol
After surgery, ALWAYS:
1. Static render the rigged SVG → compare to original (must be pixel-identical at rest)
2. Apply test rotation +35° → confirm no navy ghost behind moved limb
3. Run in onboarding with full animation → all 3 poses should show no breakage when active
