---
path: /atelier/motion
surface: customer
domain: atelier
auth: signed-in
source: apps/oshun/web/src/app/atelier/motion/page.tsx
status: walked
last_walked:
  '2026-05-29 automated runtime walk (Playwright headless) — render, /v1 data
  (2xx), console/page-errors, expected content, screenshot verified; live
  screen-reader, touch, offline, and telemetry-delivery checks pending a manual
  AT pass. Evidence: WALKTHROUGH/results/runtime-sweep-2026-05-29.md; refreshed
  2026-06-28 in apps/oshun/web/e2e/atelier-motion-to-cinematheque.spec.ts: route
  anchors, fixture-derived timeline/takes, placeholder action semantics,
  read-only playhead behavior, real /atelier/video handoff, reduced-motion
  static state, offline-stable loaded markup, and 390 px no-overflow stack;
  refreshed 2026-06-30 with explicit URL-scoped motion→Cinematheque handoff
  context and inbound Cinematheque panel assertions.'
---

# Atelier · XIV · Stage of Motion

## Purpose

Animated 3D atelier — "a figure, set in motion." Five-step workflow: imported
form → rigging → takes capture → bake keyframes → kept plate clip. Server-
rendered by `apps/oshun/web/src/app/atelier/motion/page.tsx` via
`getAtelierMotion()` → `<AtelierMotion data>`.

## Entry points

- **Atelier index** (`/atelier`) — fifth room card
- **Lilith index** (`/lilith`) — section VIII card 46 "XIV · Stage of Motion"
- **From Forms atelier (intended)** — "Open in Stage · rig" exists on
  `/atelier/forms`, but is still an `LBtn` placeholder with no `href`/handler
- **Direct URL / bookmark** — yes (signed-in)

## Layout regions

`AtelierMotion` (`apps/oshun/web/src/components/lilith/atelier.tsx`) renders
inside `LWebShell` with `LCustomerNav active="explore"`.

The route root exposes `data-atelier-motion-page` with
`data-atelier-motion-track-count="5"`, `data-atelier-motion-take-count="3"`,
`data-atelier-motion-current-take="take 02"`,
`data-atelier-motion-current-frame="74"`, and
`data-atelier-motion-bake-progress="41"`. Its body exposes
`data-atelier-motion-layout`; below 980 px the layout stacks to one column and
the 390 px Playwright path verifies no horizontal overflow.

- **Plate head** (`LAPlateHead no="XIV"`):
  - kicker: "The atelier · stage of motion"
  - title: `A figure, <em>set in motion</em>.`
  - right chips: "Take 02 · 06s", "Rig · 24 joints", "Baking · 41%" (accent)
- **Three-column body** (`230px 1fr 320px`):
  - **Left rail** (`LAStepRail`, current=4): five steps — "Form · imported",
    "Rig · skeleton, weights", "Takes · capture", "Baking · keyframes", "Plate ·
    clip kept"
  - **Center**:
    - **Stage viewport** (`Stage · viewport`): grid-paper background; 16:9 SVG
      showing a rigged stick figure with onion-skin echo poses at 0.15 / 0.3 /
      0.55 opacity, accent IK markers on hands and heels, a horizon line, and
      labels `STAGE · PERSP · 38 mm` / `f · 074 / 144` (frame counter) /
      `RIG · 24 joints · IK on heels`
    - **Timeline** (`Timeline · 06 s · 24 fps · 144 frames`): frame ruler
      (`000 / 012 / 024 / … / 144`) plus 5 keyframe tracks from
      `data.timelineTracks` (Spine · root, Head · pitch, Hand L · IK, Hand R ·
      IK, Breath · param) with diamond keyframe markers (frame 72 highlighted
      accent); playhead at 51% labelled `f.074 · 03:04`
    - **Bake progress card** (accent border):
      `LAProgress value=0.41 eta="03:42"`, italic caption "Baking keys → motion
      clip · take 02", counter "59 / 144 frames"
    - **Action row** (`data-atelier-motion-actions`): "New take" and "Re-target
      rig" are disabled-placeholder `LBtn` controls; "Open in Cinematheque" is a
      real primary link to
      `/atelier/video?from=motion&shot=sh-03&take=take-02&voice=foundry-take-03&light=forms-cream-north`
  - **Right**:
    - **Clip · take 02** card (accent border) with `LField` rows: Length "06.000
      s", Frames "144 · 24 fps", Joints "24 · IK heels", Source "Capture · live
      actor", Curves "bezier · clamped", Loop "Yes · 1 cycle in 4 s", Format
      "usd · alembic · glb"
    - **`LRule`**
    - **Other takes** list from `data.takes` (take 01 archived, take 02 kept —
      accent, take 03 draft)
    - **Compose · downstream** panel — italic note "Sends to Cinematheque (sh
      03), inherits voice (foundry · take 03) and light (forms · cream-north)."
      The panel now exposes `data-atelier-motion-handoff-source="motion"`,
      `data-atelier-motion-handoff-state="url-context"`,
      `data-atelier-motion-handoff-shot="sh 03"`,
      `data-atelier-motion-handoff-take="take 02"`,
      `data-atelier-motion-handoff-voice="foundry · take 03"`, and
      `data-atelier-motion-handoff-light="forms · cream-north"`.

## States

- [x] **Loading boundary absence** — no segment `loading.tsx` exists under
      `apps/oshun/web/src/app/atelier/motion`; `getAtelierMotion()` currently
      returns the synchronous fixture through the server component
- [x] **Populated** — fixture provides 5 timeline tracks + 3 takes, asserted via
      `data-atelier-motion-track-count="5"` and
      `data-atelier-motion-take-count="3"`
- [x] **Mid-bake** — accent progress card exposes
      `data-atelier-motion-bake-progress="41"`,
      `data-atelier-motion-bake-complete-frames="59"`, total frames `144`, and
      ETA `03:42`
- [x] **Kept take selected** — take 02 row exposes
      `data-atelier-motion-take-status="kept"` and
      `data-atelier-motion-take-selected="true"`
- [x] **Error boundary absence** — no route-local `error.tsx` exists; failures
      fall through the inherited app boundary
- [x] **Offline** — after the route is loaded, browser offline mode keeps the
      static stage, timeline, playhead, and clip markup visible
- [x] **Reduced motion** — the route has zero subtree animations under
      `data-atelier-motion-page`; onion-skin opacity and bake progress are
      static rendered values
- [x] **Mobile stack** — 390 px viewport stacks rail → stage/timeline/bake →
      clip/takes with no horizontal overflow

## Interactions

### Stage viewport

- [x] **Viewport** — read-only SVG render; clicking it does not scrub, rotate,
      mutate `data-atelier-motion-playhead-frame`, or navigate

### Timeline

- [x] **Frame ruler** — clicking frame `120` leaves the playhead at f.074 / 51%;
      no click handler is wired
- [x] **Spine · root track** (7 keys at frames 0, 24, 48, 72, 96, 120, 144;
      curve `ease`)
- [x] **Head · pitch track** (5 keys; curve `ease`)
- [x] **Hand L · IK track** (5 keys; curve `step`)
- [x] **Hand R · IK track** (5 keys; curve `step`)
- [x] **Breath · param track** (7 keys; curve `sine`)
- [x] **Accent keyframe (frame 72)** — keyframes at frame 72 expose
      `data-atelier-motion-keyframe-selected="true"`; tracks without frame 72
      expose zero selected keyframes
- [x] **Playhead marker** (at 51%, label `f.074 · 03:04`) — marker exposes
      `data-atelier-motion-playhead-frame="74"` and
      `data-atelier-motion-playhead-position="51"`; dragging/clicking is not
      wired

### Center actions

- [x] **"New take" button** (ghost) — placeholder `LBtn` with
      `aria-disabled="true"`, `data-placeholder="true"`, tooltip
      `No handler wired — placeholder`, and no navigation
- [x] **"Re-target rig" button** (ghost) — same disabled-placeholder semantics
- [x] **"Open in Cinematheque" button** (primary) — real link with
      `href="/atelier/video?from=motion&shot=sh-03&take=take-02&voice=foundry-take-03&light=forms-cream-north"`;
      Playwright follows it to the Cinematheque route and verifies the inbound
      handoff context panel

### Right column

- [x] **Clip metadata** — read-only panel `data-atelier-motion-clip-panel` with
      take 02, length, frames, joints, source, curves, loop, and format rows
- [x] **Take rows** (3, from `data.takes`):
  - **take 01** — archived, muted
  - **take 02** — kept, accent, selected
  - **take 03** — draft
- [x] **Compose · downstream panel** — read-only note referencing Cinematheque
      (sh 03), foundry (take 03), forms (cream-north), with stable URL-context
      handoff attributes for target shot, current motion take, inherited voice,
      and inherited light

## Data & contracts

- **Reads**: `getAtelierMotion()` from
  `apps/oshun/web/src/lib/lilith-data/atelier-depth.ts` — returns
  `AtelierMotionData` with `timelineTracks` and `takes` (planned BFF at
  `/atelier/plates/motion`)
- **Writes**: none in current static composition
- **Realtime**: none
- **Caching**: server fetch
- **Auth/role check**: middleware enforces signed-in

## Cross-references

- Atelier index: [`atelier.md`](./atelier.md)
- Sibling rooms: [`atelier-image.md`](./atelier-image.md),
  [`atelier-audio.md`](./atelier-audio.md),
  [`atelier-video.md`](./atelier-video.md),
  [`atelier-forms.md`](./atelier-forms.md)
- Upstream — `AtelierForms`' "Open in Stage · rig" CTA targets this room;
  `AtelierAudio` voice take 03 is inherited; `AtelierForms` cream-north light
  study is inherited
- Downstream — "Open in Cinematheque" routes to
  [`atelier-video.md`](./atelier-video.md)
- Component source: `apps/oshun/web/src/components/lilith/atelier.tsx`
  (`AtelierMotion`)
- Data: `apps/oshun/web/src/lib/lilith-data/atelier-depth.ts`

## E2E coverage

Spec: `apps/oshun/web/e2e/atelier-motion-to-cinematheque.spec.ts` (4 tests)

**Coverage depth:** deep for the current static/read-only route contract:
fixture-derived stage/timeline/take rendering, disabled-placeholder semantics,
read-only playhead non-mutation, real Cinematheque navigation, mobile
no-overflow, reduced-motion static state, and offline-stable loaded markup.
Remaining gaps are product gaps rather than test gaps: no route-local
loading/error boundaries, no new-take/retarget write flow, and no durable
cross-room project-state object beyond the URL-scoped handoff context.

## Open questions / known gaps

- [x] Confirm whether the timeline supports keyframe dragging or only visual
      display — current route is read-only; playhead remains f.074 after ruler,
      selected-keyframe, and viewport clicks
- [x] Document the cross-room handoff state — the "Compose · downstream" note
      now exposes a URL-scoped handoff object and sends it to Cinematheque,
      where it renders as an inbound context panel. This is not yet durable
      project persistence.
- [x] Verify whether "New take" creates a new entry in `data.takes` or is a
      no-op — current control is an honest disabled-placeholder `LBtn`
