Animation Design Plan — Deck-Builder blueprint-morph + card-flight
Confirmed alignment page for /animation-design-planner. This page is the approved motion contract and proof gate for deck-builder implementation.
docs/history/archive/2026-06-10/173258/alignment/docs/history/archive/2026-06-10/174721/alignment/apps/skills-showcase/docs/animation-plan-deck-builder.mddocs/history/archive/2026-06-11/142531/research/skills-showcase/_working/preliminary-animation-design-planner-research.mddocs/history/archive/2026-06-11/142531/alignment/animation-design-planner-deck-builder-transitions.htmlsection_feedback: [], no requested edits (verbatim record in §14)apps/skills-showcase/docs/deck-builder-ux.md (commit 6e2e6aec)$exec deck-builder implementation — Next 16.2.6 pushState spike first1. Context & Evidence
The deck-builder UX design (apps/skills-showcase/docs/deck-builder-ux.md, committed 6e2e6aec) introduces two new framer-motion transitions that don't exist in the /prototype flow:
- blueprint-morph — deck blueprint card on the table ⇄ builder overlay at
/deck/[slug]. - card-flight — fan card flies into a deck slot on tap, plus an add-all stagger variant.
/animation-design-planner was invoked to plan these before building — naming the visible motion contracts, lifecycle owner, state phases, and proof method so implementation doesn't start with local timing tweaks. Framework detected: framer-motion 12.40 (imported as "framer-motion") → motion-framer subskill guardrails apply.
Evidence behind the load-bearing decisions
| Claim / decision | Evidence (observed) | Inference | Confidence |
|---|---|---|---|
| Both transitions are new compositions, not new primitives | docs/deck-builder-ux.md design principle 3 ("Promote, don't rewrite"); existing primitives in SealedPack.tsx, PackOpener.tsx, SkillCard.tsx, BottomSheet | The plan reuses layoutId morph + collapse-to-target motion; no new animation library surface needed | High |
layoutId + declarative animate on transforms conflict | PackOpener.tsx:212-213 — existing workaround in the pack flow | Flight clones must be driven imperatively with motion values; slot cards (declarative pulses) never carry a layoutId | High (repo-observed) |
| Route-first close would break the morph-back | App Router route-change unmounts are not AnimatePresence-aware (framework behavior) | Client-state-first with shallow pushState is required; real /deck/[slug] route kept for hard loads/SEO | Medium — spike required on Next 16.2.6 (see risk register) |
| Sheet children cannot paint above the slot strip | BottomSheet's z-50 stacking context in the prototype flow | Card-flight needs a portal clone overlay (FlightLayer) rather than unclipping the sheet | High |
2. Motion Contract A — blueprint-morph
| Trigger | Tap/click a deck blueprint on the table (/); close via Back button or dismiss affordance. |
|---|---|
| Start → end | Small blueprint card on the table → full builder panel (/deck/[slug]) with phase slots; reverse on close. URL changes both ways. |
| Focal / continuity element | The blueprint card chrome is the single visually continuous rectangle (shared layoutId="deck-blueprint-${slug}"); builder content (slot columns, header, shelf glow) staggers in after the morph lands. |
Never list
- Never: double-vision — blueprint and panel both visible at full opacity.
- Never: flash at morph-back.
- Never: builder content popping before the morph completes.
- Never: table scroll jump.
- Never: focus loss — focus moves to the builder heading on open, returns to the originating blueprint on close.
- Never: URL/visual desync.
Interruption rules
- Blueprint taps ignored unless phase is
table. - Dismiss legal only from
builder-open— plus dismiss-during-blueprint-morphing, which retargets the layout morph from its current projected position. - Hard load of
/deck/[slug]skips the open morph (no origin) but still morphs back on close, because the table renders underneath.
Gate: blueprint-morph contract acceptance Updated — confirmed 2026-06-11
Do you accept the blueprint-morph visible motion contract (trigger, continuity element, never list, interruption rules) as the binding spec for implementation?
Answered: Accept as specified
Recorded from final compiled YAML · status: answered · 2026-06-11
3. Motion Contract B — card-flight
| Trigger | Tap an uncollected card in the open fan drawer; "Collect all N" triggers the staggered batch variant. |
|---|---|
| Start → end | Card visually leaves the fan, flies above the sheet to its phase slot, slot fills and pulses once, deck counter ticks; the fan card is dimmed with an "in deck ♦" badge from the tap frame onward. |
| Focal element | The flying clone; the slot is the continuity target (pinned visible above the sheet on mobile). |
Never list
- Never: double-vision — full-opacity fan card while the clone flies.
- Never: clone clipped by the sheet's overflow or painted under the scrim/slot strip.
- Never: slot filling before the clone lands (reduced motion excepted).
- Never: counter desync after interruptions.
- Never: accidental collect from an inspect tap — flip and fly remain separate affordances.
Interruption rules
- Re-tap of an in-flight or collected card is a no-op.
- Concurrent flights of different cards are allowed.
- Drawer close or builder dismiss mid-flight snaps all flights to end (no cancel/rollback) before proceeding.
- Deck-complete sequence defers until all flights land.
Gate: card-flight contract acceptance Updated — confirmed 2026-06-11
Do you accept the card-flight visible motion contract (trigger, focal element, never list, interruption rules), including the add-all stagger variant, as the binding spec for implementation?
Answered: Accept as specified
Recorded from final compiled YAML · status: answered · 2026-06-11
4. Interactive: Motion Demos & Phase Timeline Scrubbers
- Demo scope: yes — the demo shows the card movement flow only. The 1:1 card→slot mirror is a demo simplification, not the deck model. This animation plan binds how cards travel, not which slots exist.
- What the locked UX design actually specifies (
deck-builder-ux.md): ghost slots are suggestions, not requirements — a filled slot can be swapped, and replacing a suggestion converts the deck to a custom variant with an editable name and explicit CLI output. A freeform overlay row accepts any card. [Custom deck +] creates a deck with empty freeform phases — user-defined building exists there. Add-all ("Collect all N for this deck") exists precisely to kill the chore on big decks (13-cardbusiness-opsis one tap). Canonical decks mirror their card lists deliberately: the locked decision was "the deck teaches the pipeline as you build," and the build ritual converges on unlocking theinstall-deckcommand. - Open question routed out of this plan: whether canonical decks should loosen toward user-defined phases challenges the locked "phase-labeled slots vs freeform tray" UX decision — that belongs to the deck-creation user-flow design (
/user-flow-map, product-design pack), not this animation plan. If that work changes the slot model, the card-flight contract's target (slot identity/position) is re-reviewed here before implementation; the movement mechanics (optimistic commit, clone overlay, snap-to-end) are unaffected.
⚠ These are ephemeral previews, not production code. The demos show directional motion; the scrubbers show the phase timing data. All timings are design targets that pass through dbg.scaleT at implementation time. No state persists.
Directional demo — blueprint-morph
Tap the Devtool AFPS blueprint (or press Open). The chrome rectangle is the only continuously visible element; the blueprint sits at opacity 0 underneath while the panel owns the morph, and builder content staggers in only after the morph lands. Close runs the reverse: content vanishes near-instantly so the morph-back reads.
Directional demo — card-flight
Tap any card in the fan: it dims with an "in deck ♦" badge the same frame (optimistic commit), a clone flies above the sheet to its slot, the slot fills and pulses, the counter ticks. Collect all staggers launches 70 ms apart. Dismiss mid-flight demos snap-to-end: clones vanish, slots force-fill, the counter reconciles to the committed size. Re-tapping a collected card is a no-op. Scope note: this demo shows card movement only — the 1:1 card→slot mirror is a simplification, not the deck model (see Amendment 2 above; slots in the real design are suggestions with swap/overlay/custom-deck affordances).
blueprint-morph timeline
| Sequence | Phase | Window (ms) | What happens |
|---|---|---|---|
| Open | Tap commit | 0–20 | pushState; blueprint claims opacity 0; panel claims layoutId |
| Open | Layout spring morph | 20–470 | ~450 ms framer layout spring; chrome rectangle morphs table card → panel |
| Open | Phase flip | 470–490 | onLayoutAnimationComplete (one-shot ref) → builder-open |
| Open | Content stagger-in | 490–690 | Slot columns then shelf, ~200 ms total, 40 ms stagger |
| Close | Content fade-out | 0–10 | Near-instant (0.01 s) so the morph reads |
| Close | layoutId handback | 10–30 | AnimatePresence onExitComplete → blueprint reclaims id |
| Close | Morph-back (apex) | 30–380 | ~350 ms; completion frame is the flash-risk apex |
| Close | Table restored | 380–400 | Phase → table; focus returns to originating blueprint |
card-flight timeline
| Sequence | Phase | Window (ms) | What happens |
|---|---|---|---|
| Single tap | Tap commit | 0–16 | Optimistic commit (collectedCardIds + localStorage); fan card dims with "in deck ♦" badge, same frame |
| Single tap | Measure | 16–48 | Measure source rect; if slot off-screen, scrollIntoView({behavior:"instant"}) then re-measure next rAF |
| Single tap | Clone flight | 48–448 | Clone mounts in FlightLayer at source rect; imperative animate() x/y/scale, spring {stiffness: 260, damping: 26} (~400 ms) |
| Single tap | Land + pulse | 448–698 | .then(): clone removed, slot ghost→filled, declarative pulse scale [1, 1.08, 1] ~250 ms, counter ticks |
| Add-all | Staggered launches | 0–840 | Filtered queue, 70 ms stagger per flight (13-card worst case ≈ 0.84 s launch tail) |
| Add-all | Last clone in flight | 840–1240 | Concurrent flights overlap; each follows the single-tap spring |
| Add-all | Last land + batch-complete | 1240–1490 | Final pulse; batch-complete via decrement ref; single tap is a batch of 1 |
5. Lifecycle Ownership Map
Hierarchical composition; the existing PackFlowPhase machine is nested unchanged:
DeckTableShell (client; rendered by both app/page.tsx and app/deck/[slug]/page.tsx)
├── owns DeckFlowPhase: table → blueprint-morphing → builder-open → builder-dismissing → table
├── owns activeDeck + collectedCardIds (localStorage-backed, per slug)
├── one <LayoutGroup> wrapping table + builder
├── TableSurface (always mounted)
├── BuilderPanel (AnimatePresence; mounted while activeDeck != null)
│ └── BuilderPackFlow — PackFlowPhase owner extracted from PrototypeInner
│ (sealed → opening-apex → drawer-open → closing-* chain, verbatim)
│ └── owns flights Map<cardId, FlightRecord> (parallel records, NOT a scalar phase:
│ queued → measuring → in-flight → settling → done per record)
└── FlightLayer (fixed inset-0, pointer-events-none, z-[70]) — flight clones
window.history.pushState (+ popstate listener), because App Router route-change unmounts are not AnimatePresence-aware — a route-first intercepting-route close would unmount the builder before the blueprint can reclaim the layoutId. /deck/[slug] exists as a real route for hard loads/SEO and renders the same shell with initialDeckSlug.
pushState + usePathname/popstate behavior on Next 16.2.6 — flagged as the top pre-implementation risk.
Identity rules
- Fan cards keyed by
skill.id, never filtered/reordered on collect (preserves PackOpener's fan/collapse math). - Flight clones keyed
flight-${skill.id}, max one per card. - Slots keyed
${deckSlug}:${slotId}. - Exactly one mounted owner of
deck-blueprint-${slug}at a time — source stays mounted at opacity 0, the SealedPack pattern.
Commit timing
Optimistic — collectedCardIds + localStorage commit at tap time; animation is pure presentation; a presentation-level displayedCount ticks on flight-land and reconciles on any interrupt. No rollback paths exist.
Interruption rule
finishAllFlightsImmediately() (snap-to-end: stop controls, remove clones, force slots filled, reconcile counter) runs before any drawer close or builder dismiss. Back/dismiss with the drawer open uses a fast drawer teardown (BottomSheet's existing 0.01s fadeExit path, skip the collapse ritual) so leaving feels immediate.
Gate: lifecycle ownership & routing Updated — confirmed 2026-06-11
Do you approve the hierarchical ownership map (DeckTableShell owns DeckFlowPhase; the existing PackFlowPhase machine nests unchanged) and the client-state-first routing decision (shallow pushState, with the Next 16.2.6 spike run before implementation)?
Answered: Approve — shell ownership + pushState routing, spike first
Recorded from final compiled YAML · status: answered · 2026-06-11
Gate: interruption semantics & commit timing Updated — confirmed 2026-06-11
Do you approve snap-to-end interruption handling (finishAllFlightsImmediately() before any close/dismiss; fast drawer teardown on back) and the optimistic commit model (state + localStorage commit at tap time, no rollback paths)?
Answered: Approve both
Recorded from final compiled YAML · status: answered · 2026-06-11
6. Interactive: State Machine Explorer
⚠ This is an ephemeral preview, not production code. Click a state node to highlight its legal transitions and guards. The DeckFlowPhase chain is the new shell machine; the FlightRecord chain is per-card (a Map of parallel records, not a scalar phase). No state persists.
| Machine | From | To | Trigger / guard |
|---|---|---|---|
| DeckFlowPhase | table | blueprint-morphing | Blueprint tap — taps ignored unless phase is table |
| DeckFlowPhase | blueprint-morphing | builder-open | onLayoutAnimationComplete (one-shot ref) |
| DeckFlowPhase | blueprint-morphing | builder-dismissing | Dismiss-during-morph — retargets the layout morph from its current projected position |
| DeckFlowPhase | builder-open | builder-dismissing | Back/dismiss (only legal here); runs finishAllFlightsImmediately() + fast drawer teardown first |
| DeckFlowPhase | builder-dismissing | table | Morph-back onLayoutAnimationComplete; focus restored to originating blueprint |
| DeckFlowPhase | (hard load) | builder-open | Direct load of /deck/[slug] — no origin, open morph skipped; close still morphs back |
| FlightRecord | queued | measuring | Dequeue (add-all stagger 70 ms) or immediate for single tap |
| FlightRecord | measuring | in-flight | Source rect measured (scrollIntoView + re-measure next rAF if slot off-screen); clone mounts in FlightLayer |
| FlightRecord | in-flight | settling | Imperative spring resolves (.then()); clone removed; slot ghost→filled |
| FlightRecord | settling | done | Pulse complete; displayedCount ticks |
| FlightRecord | any non-done | done | finishAllFlightsImmediately() — snap-to-end, counter reconciled |
7. Storyboards & Mechanism Decision
All timings pass through dbg.scaleT and are debug-speed scalable.
blueprint-morph open
- Tap →
pushState+ blueprint claims opacity 0 / panel claims layoutId. - Layout spring morph (~450 ms, framer layout spring).
onLayoutAnimationComplete(one-shot ref) →builder-open.- Content stagger-in (slot columns then shelf, ~200 ms, 40 ms stagger).
blueprint-morph close
- Content near-instant fade-out (0.01 s, so the morph reads).
- AnimatePresence
onExitComplete→ blueprint reclaims id. - Morph-back (~350 ms) →
onLayoutAnimationComplete→table, focus restored.
Morph-back completion frame is the flash-risk apex. It gets a dedicated gate step in the debug harness and a named manual slow-motion check in the proof gate.
card-flight
- Tap → optimistic commit + fan card dims (same frame).
- Measure source rect; if the target slot is off-screen,
scrollIntoView({behavior:"instant"})then re-measure next rAF. - Clone mounts in FlightLayer at source rect.
- Imperative
animate()x/y/scale to slot rect, spring{stiffness: 260, damping: 26}(~400 ms). .then(): clone removed, slot ghost→filled, declarative pulsescale [1, 1.08, 1]~250 ms, counter ticks.
Add-all: filtered queue, 70 ms stagger per flight (13-card worst case ≈ 0.84 s launch tail), batch-complete via decrement ref; single tap is a batch of 1.
PackOpener.tsx:212-213) is why the clone is driven imperatively with motion values, and why slot cards (which need declarative pulses) never carry a layoutId.
8. Implementation Guardrails (motion-framer subskill + baseline)
- Transform/opacity only; the morph animates the panel chrome rectangle, never the slot grid layout; no filter/shadow animation.
- BuilderPanel's layoutId container takes no declarative
animateon transforms; content motion lives on child wrappers. - AnimatePresence placement: BuilderPanel's presence is owned by DeckTableShell (the component that decides mounting), mode
sync; BottomSheet keeps its existing presence handling. - Reduced motion (
useReducedMotion(), read once in the shell): same state chain and callbacks fire in the same order (debug/test parity), durations degrade — blueprint-morph becomes a 120 ms opacity crossfade with layoutId omitted; card-flight mounts no clone, launch/land marks fire back-to-back, slot fills with a 120 ms fade, no pulse, add-all has no stagger. Retrofitting the existing pack flow is a named follow-up, out of scope. - Pointer events: FlightLayer
pointer-events-none; scrim/dismiss disabled duringblueprint-morphingandbuilder-dismissing; focus management per contract A.
Gate: scope / non-goals Updated — confirmed 2026-06-11
The two new transitions ship with full reduced-motion behavior, but retrofitting prefers-reduced-motion support onto the existing pack flow (SealedPack tear, fan-out, collapse) is a named follow-up and out of scope for this plan. Confirm?
Answered: Confirmed — retrofit stays out of scope
Recorded from final compiled YAML · status: answered · 2026-06-11
9. Debug-Harness Extension
New step arrays in steps.ts (+ nodes/lanes deck-shell, builder, flight-layer and runtime slices in animationMachine.ts):
| Step array | Steps |
|---|---|
DECK_OPEN_STEPS | blueprint-tap → url-push → builder-mount → blueprint-morph-in (gate) → builder-content-in |
DECK_CLOSE_STEPS | dismiss-trigger → flights-flushed (cond.) → drawer-fast-teardown (cond.) → builder-exit (gate) → blueprint-morph-out (gate, apex) → table-restored |
FLIGHT_STEPS | flight-tap → flight-measure → flight-launch (gate) → flight-land (gate) → flight-batch-complete |
New debug drivers: openDeck, dismissDeck, flyCard, flyAll, extended reset. All new transitions pass through dbg.scaleT.
10. Proof Gate
User direction during planning: add Playwright at implementation time.
Vitest sequence tests (existing pattern — mocked debug report() channel, mocked getBoundingClientRect)
- Morph open/close phase order.
- Flight optimistic-commit + interrupt reconciliation (close-mid-flight leaves counter == collected size).
- Re-tap no-op.
- Hard-load
/deck/[slug]entersbuilder-openwithout morph.
Playwright (devDependency added in the deck-builder implementation phase, local runs only — no GitHub Actions)
- Screenshots at each phase boundary.
- Video of morph open/close and a 3-card add-all.
- Assert no frame shows double-vision or a clipped clone.
- A
prefers-reduced-motionproject run asserting crossfade/instant-fill behavior.
Manual slow-motion review (at --debug-speed 0.25)
- Morph-back apex frame (flash risk).
- Clone z-order over scrim/sheet/slot strip.
- Perceived continuity of the chrome rectangle.
- Interruption feel (back-during-flight).
Acceptance: every "never" item in contracts A/B has a corresponding automated or named-manual check; onExitComplete-dependent cleanup verified to run after exit, not during.
Gate: proof tooling Updated — confirmed 2026-06-11
Confirm adding Playwright as a devDependency during the deck-builder implementation phase (local runs only, no GitHub Actions), alongside the Vitest sequence tests and the named manual slow-motion checks?
Answered: Confirmed — add Playwright at implementation time
Recorded from final compiled YAML · status: answered · 2026-06-11
11. Assumptions & Risk Register
| Risk / assumption | Status | Mitigation | What would change the plan |
|---|---|---|---|
Next 16 shallow-pushState spike — shallow window.history.pushState + usePathname/popstate behavior on Next 16.2.6 | Load-bearing, unproven | ~30 min spike before any implementation | If shallow pushState fights the App Router, the routing layer is redesigned (e.g. full client-canvas with route only on hard load) before any animation work starts |
| Morph legibility across extreme aspect-ratio change (small blueprint → full panel) | Provisional | Chrome-rectangle-only morph; content staggers in after landing | If the morph still reads poorly at slow motion, fall back to scale+fade with a shorter morph distance |
| Mobile slot-strip scroll-then-measure timing (scrollIntoView → re-measure next rAF) | Provisional | Manual device check in the proof gate | If measurement races persist, pin the slot strip during flights |
| Scroll drift during a ~400 ms flight (user scrolls while clone is mid-air) | Accepted limitation | Documented; clone targets the rect measured at launch | Only revisit if real-device review finds it disorienting |
12. Artifact Destination & Proposed File Changes
Review run created
| Path | Action |
|---|---|
prompts/animation-design-planner/skill-prompt-20260610-135323-deck-builder-transitions.md | create — prompt history log |
research/skills-showcase/_working/preliminary-animation-design-planner-research.md | created as Stage 1 working packet, then archived during confirmation |
alignment/animation-design-planner-deck-builder-transitions.html | created as review page, then converted to confirmed during approval handling |
alignment/index.html | update — add entry under Product Design & Spec |
Confirmation mutations
| Path | Action |
|---|---|
apps/skills-showcase/docs/animation-plan-deck-builder.md | create — canonical approved animation plan |
docs/history/archive/2026-06-11/142531/research/skills-showcase/_working/preliminary-animation-design-planner-research.md | archive — working packet moved out of the active tree |
docs/history/archive/2026-06-11/142531/alignment/animation-design-planner-deck-builder-transitions.html | archive — prior review page preserved before conversion |
alignment/animation-design-planner-deck-builder-transitions.html | convert — review → confirmed |
alignment/index.html | update — mark this page as confirmed |
The first implementation spike runs after this confirmation, scoped to Next 16.2.6 shallow pushState behavior.
Gate: artifact destination Updated — confirmed 2026-06-11
Approve apps/skills-showcase/docs/animation-plan-deck-builder.md as the canonical destination for the approved animation plan (written only after approval, next to deck-builder-ux.md)?
Answered: Approve destination
Recorded from final compiled YAML · status: answered · target path: apps/skills-showcase/docs/animation-plan-deck-builder.md · 2026-06-11
Gate: proposed file changes Updated — confirmed 2026-06-11
Approve the file-change scope and timing: this run's four review artifacts now, and the post-approval mutation set (archive the working packet, write the canonical plan, confirm this page) only after final compiled YAML?
Answered: Approve scope and timing as listed
Recorded from final compiled YAML · status: answered · 2026-06-11
13. Post-Approval Route
After approval, the natural next step is the deck-builder implementation handoff: the canonical plan plus deck-builder-ux.md become the spec inputs, with the Next 16.2.6 pushState spike as the first implementation task.
Gate: post-approval route Updated — confirmed 2026-06-11
After the canonical plan is written, how should the work route?
Answered: Hand off to /exec for deck-builder implementation (spike first)
Recorded from final compiled YAML · status: answered · 2026-06-11
14. Approval Record Updated — confirmed 2026-06-11
Final compiled YAML received 2026-06-11: all 9 gates answered, section_feedback: [], no needs-clarification, and no requested edits. The user's compiled YAML is preserved verbatim below as the read-only approval record. The compile controls that produced it were removed when this page converted from review to confirmed.
alignment_page: alignment/animation-design-planner-deck-builder-transitions.html
approval_status: ready-for-agent-review
gate_answers:
- section: "Motion Contract A — blueprint-morph"
gate_type: "motion-contract"
status: answered
answer: "Accept as specified"
- section: "Motion Contract B — card-flight"
gate_type: "motion-contract"
status: answered
answer: "Accept as specified"
- section: "Lifecycle Ownership Map"
gate_type: "lifecycle-ownership"
status: answered
answer: "Approve — shell ownership + pushState routing, spike first"
- section: "Lifecycle Ownership Map"
gate_type: "interruption-semantics"
status: answered
answer: "Approve both"
- section: "Implementation Guardrails"
gate_type: "scope/non-goals"
status: answered
answer: "Confirmed — retrofit stays out of scope"
- section: "Proof Gate"
gate_type: "proof-tooling"
status: answered
answer: "Confirmed — add Playwright at implementation time"
- section: "Artifact Destination & Proposed File Changes"
gate_type: "artifact destination"
status: answered
answer: "Approve destination"
target_path: "apps/skills-showcase/docs/animation-plan-deck-builder.md"
- section: "Artifact Destination & Proposed File Changes"
gate_type: "proposed file changes"
status: answered
answer: "Approve scope and timing as listed"
- section: "Post-Approval Route"
gate_type: "post-approval route"
status: answered
answer: "Hand off to /exec for deck-builder implementation (spike first)"
section_feedback: []
What changed during confirmation
- Canonical plan written to
apps/skills-showcase/docs/animation-plan-deck-builder.md. - The prior
reviewpage was archived todocs/history/archive/2026-06-11/142531/alignment/animation-design-planner-deck-builder-transitions.html, then this page converted toconfirmed: gate controls replaced with read-only answered-decision records, section-feedback and compile controls removed, and this approval record added. - The non-canonical working packet was moved to
docs/history/archive/2026-06-11/142531/research/skills-showcase/_working/preliminary-animation-design-planner-research.md.