// robot-arm.jsx — tower-builder + knockdown + occasional failures (v4)
//
// 3-axis arm (base yaw + shoulder + elbow + constrained wrist) stacks 4 cubes
// into a tower, knocks the tower down with a swinging arm, then resets.
// On every other cycle a different cube's grip "slips" mid-pickup — the arm
// shakes briefly (frustrated), then retries. Cubes have real physics during
// the knockdown (gravity + bounce damping).

function _clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function _easeInOut(t) { return t < 0.5 ? 2*t*t : 1 - Math.pow(-2*t + 2, 2)/2; }

// ─── Scene + arm geometry ───────────────────────────────────────────────
const SVG_W = 540, SVG_H = 340;
const ORIGIN = { x: 250, y: 175 };
const DEPTH_A = Math.PI / 6;
const DEPTH_COS = Math.cos(DEPTH_A);
const DEPTH_SIN = Math.sin(DEPTH_A);
const DEPTH_K = 0.55;

const SHOULDER = { x: -40, y: 12, z: 28 };
const FLOOR_Y = -100;
const BENCH_TOP = 0;
const BENCH_BOT = -32;
const BENCH = { x0: -80, x1: 150, z0: -38, z1: -8 };
const STACK_PAD = { x0: -100, x1: -60, z0: -42, z1: -4 };

const L1 = 100, L2 = 100, L3 = 16, TOOL_OFFSET = 12;
const WRIST_TO_TIP = L3 + TOOL_OFFSET;
const CUBE_SIZE = 18, CUBE_HALF = 9;
const CUBE_TOP_Y = BENCH_TOP + CUBE_HALF;
const STACK_X = -80, STACK_Z = -23;

const SOURCES = [
  { x: 20,  z: -28 },
  { x: 60,  z: -18 },
  { x: 100, z: -28 },
  { x: 140, z: -18 },
];
const CUBES_INIT = SOURCES.map((s, i) => ({
  srcX: s.x, srcY: CUBE_TOP_Y, srcZ: s.z,
  dstX: STACK_X, dstY: CUBE_TOP_Y + i * CUBE_SIZE, dstZ: STACK_Z,
}));

const LIFT_Y = 70;
const PARK = { x: SHOULDER.x + 40, y: 50, z: SHOULDER.z - 30 };

// ─── Formations: where each cube ends up ────────────────────────────────
// dStack(i) = vertical tower; dGrid(i) = 2×2 grid; dLine(i) = horizontal row.
function dStack(i) {
  return { x: STACK_X, y: CUBE_TOP_Y + i * CUBE_SIZE, z: STACK_Z };
}
function dGrid(i) {
  const col = i % 2;
  const row = (i / 2) | 0;
  const s = CUBE_SIZE + 4;
  return {
    x: STACK_X + (col - 0.5) * s,
    y: CUBE_TOP_Y,
    z: STACK_Z + (row - 0.5) * s,
  };
}
function dLine(i) {
  // 4 cubes in a left-right row, slightly tilted in z for an iso-friendly read.
  const span = 4;
  return { x: STACK_X + (i - 1.5) * (CUBE_SIZE + 4), y: CUBE_TOP_Y, z: STACK_Z };
}
function dPyramid(i) {
  const step = CUBE_SIZE + 2;
  if (i === 3) return { x: STACK_X, y: CUBE_TOP_Y + CUBE_SIZE, z: STACK_Z };
  return { x: STACK_X + (i - 1) * step, y: CUBE_TOP_Y, z: STACK_Z };
}
function dDomino(i) {
  const step = CUBE_SIZE + 2;
  return { x: STACK_X + (i - 1.5) * step, y: CUBE_TOP_Y, z: STACK_Z };
}
function dPisa(i) {
  const lean = i * 4.5;
  return { x: STACK_X + lean, y: CUBE_TOP_Y + i * CUBE_SIZE, z: STACK_Z };
}
function dSort(i) {
  const ODD_X  = STACK_X - 14;
  const EVEN_X = STACK_X + 14;
  if (i === 0) return { x: ODD_X,  y: CUBE_TOP_Y, z: STACK_Z };
  if (i === 1) return { x: EVEN_X, y: CUBE_TOP_Y, z: STACK_Z };
  if (i === 2) return { x: ODD_X,  y: CUBE_TOP_Y + CUBE_SIZE, z: STACK_Z };
  return         { x: EVEN_X, y: CUBE_TOP_Y + CUBE_SIZE, z: STACK_Z };
}

// ─── Phase builders ─────────────────────────────────────────────────────
function buildPickPlace(cubeIdx, dest) {
  const c = CUBES_INIT[cubeIdx];
  return [
    { type: "approach",   cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 0, d: 0.45 },
    { type: "descend",    cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 0, d: 0.30 },
    { type: "grip-close", cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 1, d: 0.20 },
    { type: "lift",       cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 1, d: 0.40 },
    { type: "travel",     cubeIdx, x: dest.x, y: LIFT_Y, z: dest.z, g: 1, d: 0.70 },
    { type: "place",      cubeIdx, x: dest.x, y: dest.y, z: dest.z, g: 1, d: 0.28 },
    { type: "release",    cubeIdx, x: dest.x, y: dest.y, z: dest.z, g: 0, d: 0.18 },
    { type: "lift-clear", cubeIdx, x: dest.x, y: LIFT_Y, z: dest.z, g: 0, d: 0.40 },
  ];
}

function buildPickPlaceWithFail(cubeIdx, dest) {
  const c = CUBES_INIT[cubeIdx];
  return [
    { type: "approach",    cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 0, d: 0.45 },
    { type: "descend",     cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 0, d: 0.30 },
    // Grip closes — but the cube slips.
    { type: "grip-fail",   cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 1, d: 0.18 },
    // Arm rises, cube stayed on bench.
    { type: "lift-empty",  cubeIdx, x: c.srcX, y: c.srcY + 22, z: c.srcZ, g: 1, d: 0.25 },
    // Frustration: jitter side-to-side, grip open.
    { type: "frustrated-1", cubeIdx, x: c.srcX - 5, y: c.srcY + 24, z: c.srcZ + 3, g: 0, d: 0.10 },
    { type: "frustrated-2", cubeIdx, x: c.srcX + 5, y: c.srcY + 24, z: c.srcZ - 3, g: 0, d: 0.10 },
    { type: "frustrated-3", cubeIdx, x: c.srcX - 4, y: c.srcY + 22, z: c.srcZ + 2, g: 0, d: 0.10 },
    { type: "frustrated-settle", cubeIdx, x: c.srcX, y: c.srcY + 22, z: c.srcZ, g: 0, d: 0.14 },
    // Retry.
    { type: "retry-descend", cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 0, d: 0.28 },
    { type: "grip-close",    cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 1, d: 0.20 },
    { type: "lift",          cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 1, d: 0.40 },
    { type: "travel",        cubeIdx, x: dest.x, y: LIFT_Y, z: dest.z, g: 1, d: 0.70 },
    { type: "place",         cubeIdx, x: dest.x, y: dest.y, z: dest.z, g: 1, d: 0.28 },
    { type: "release",       cubeIdx, x: dest.x, y: dest.y, z: dest.z, g: 0, d: 0.18 },
    { type: "lift-clear",    cubeIdx, x: dest.x, y: LIFT_Y, z: dest.z, g: 0, d: 0.40 },
  ];
}

// Picks a cube, then "inspects" it by yawing the arm side-to-side at viewing
// height before placing — like the arm is examining a part it just picked up.
function buildPickPlaceWithInspect(cubeIdx, dest) {
  const c = CUBES_INIT[cubeIdx];
  const ix = SHOULDER.x + 75, iy = 68, iz = SHOULDER.z - 12;
  return [
    { type: "approach",   cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 0, d: 0.45 },
    { type: "descend",    cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 0, d: 0.30 },
    { type: "grip-close", cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 1, d: 0.20 },
    { type: "lift",       cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 1, d: 0.40 },
    // Inspect: yaw side to side while holding.
    { type: "inspect-bring",  cubeIdx, x: ix,      y: iy, z: iz,      g: 1, d: 0.55 },
    { type: "inspect-left",   cubeIdx, x: ix - 18, y: iy, z: iz - 18, g: 1, d: 0.45 },
    { type: "inspect-right",  cubeIdx, x: ix + 18, y: iy, z: iz + 12, g: 1, d: 0.55 },
    { type: "inspect-center", cubeIdx, x: ix,      y: iy, z: iz,      g: 1, d: 0.40 },
    { type: "travel",     cubeIdx, x: dest.x, y: LIFT_Y, z: dest.z, g: 1, d: 0.55 },
    { type: "place",      cubeIdx, x: dest.x, y: dest.y, z: dest.z, g: 1, d: 0.28 },
    { type: "release",    cubeIdx, x: dest.x, y: dest.y, z: dest.z, g: 0, d: 0.18 },
    { type: "lift-clear", cubeIdx, x: dest.x, y: LIFT_Y, z: dest.z, g: 0, d: 0.40 },
  ];
}

// Horizontal sweep through a tall stack (the original demolition).
function buildSweepDemo() {
  return [
    { type: "kd-windup",  cubeIdx: -1, x: STACK_X + 95, y: 38, z: STACK_Z + 6, g: 0, d: 0.55 },
    { type: "kd-cock",    cubeIdx: -1, x: STACK_X + 100, y: 32, z: STACK_Z + 4, g: 0, d: 0.22 },
    { type: "kd-swing",   cubeIdx: -1, x: STACK_X - 70, y: 30, z: STACK_Z - 6, g: 0, d: 0.28 },
    { type: "kd-follow",  cubeIdx: -1, x: STACK_X - 100, y: 36, z: STACK_Z - 10, g: 0, d: 0.30 },
    { type: "kd-observe", cubeIdx: -1, x: STACK_X - 90, y: 55, z: STACK_Z - 8, g: 0, d: 1.20 },
  ];
}

// Slam straight down onto a low/spread formation (grid, line).
function buildCrushDemo() {
  return [
    { type: "kd-rise",    cubeIdx: -1, x: STACK_X + 20, y: 88, z: STACK_Z - 5,  g: 0, d: 0.55 },
    { type: "kd-center",  cubeIdx: -1, x: STACK_X,      y: 88, z: STACK_Z,      g: 0, d: 0.28 },
    { type: "kd-hold",    cubeIdx: -1, x: STACK_X,      y: 92, z: STACK_Z,      g: 0, d: 0.20 },
    { type: "kd-strike",  cubeIdx: -1, x: STACK_X,      y: 24, z: STACK_Z,      g: 0, d: 0.16 },
    { type: "kd-recoil",  cubeIdx: -1, x: STACK_X + 6,  y: 64, z: STACK_Z - 3,  g: 0, d: 0.45 },
    { type: "kd-observe", cubeIdx: -1, x: STACK_X + 10, y: 64, z: STACK_Z - 6,  g: 0, d: 1.05 },
  ];
}

// Push horizontally along a line, like sweeping pieces off a table.
function buildLineSweep() {
  const x0 = STACK_X - (CUBE_SIZE + 4) * 1.5 - 30;
  const x1 = STACK_X + (CUBE_SIZE + 4) * 2.5 + 10;
  return [
    { type: "kd-windup",  cubeIdx: -1, x: x0, y: 36, z: STACK_Z + 6, g: 0, d: 0.55 },
    { type: "kd-cock",    cubeIdx: -1, x: x0 - 6, y: 30, z: STACK_Z + 4, g: 0, d: 0.20 },
    { type: "kd-swing",   cubeIdx: -1, x: x1, y: 22, z: STACK_Z - 6, g: 0, d: 0.42 },
    { type: "kd-follow",  cubeIdx: -1, x: x1 + 12, y: 36, z: STACK_Z - 8, g: 0, d: 0.30 },
    { type: "kd-observe", cubeIdx: -1, x: x1, y: 56, z: STACK_Z - 6, g: 0, d: 1.10 },
  ];
}

// Domino chain: tap the leftmost cube and let collisions cascade the rest.
function buildDominoTap() {
  const firstX = STACK_X - (CUBE_SIZE + 2) * 1.5;
  return [
    { type: "kd-windup", cubeIdx: -1, x: firstX - 42, y: 46, z: STACK_Z + 6,  g: 0, d: 0.55 },
    { type: "kd-cock",   cubeIdx: -1, x: firstX - 26, y: 30, z: STACK_Z + 4,  g: 0, d: 0.22 },
    { type: "kd-tap",    cubeIdx: -1, x: firstX + 12, y: 28, z: STACK_Z - 4,  g: 0, d: 0.20 },
    { type: "kd-follow", cubeIdx: -1, x: firstX + 28, y: 38, z: STACK_Z - 6,  g: 0, d: 0.30 },
    { type: "kd-observe",cubeIdx: -1, x: firstX + 40, y: 58, z: STACK_Z - 8,  g: 0, d: 1.30 },
  ];
}

// Picks a cube, flourishes, vanishes it mid-air, and it reappears at the bin.
// Sleight-of-hand routine — cube teleports rather than gets carried.
function buildMagicTeleport(cubeIdx) {
  const c = CUBES_INIT[cubeIdx];
  const stage = { x: SHOULDER.x + 55, y: 90, z: SHOULDER.z - 4 };
  const dest = dStack(cubeIdx);
  return [
    { type: "approach",         cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 0, d: 0.38 },
    { type: "descend",          cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 0, d: 0.24 },
    { type: "grip-close",       cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 1, d: 0.16 },
    { type: "magic-lift",       cubeIdx, x: stage.x, y: stage.y, z: stage.z, g: 1, d: 0.42 },
    { type: "magic-flourish-l", cubeIdx, x: stage.x - 12, y: stage.y + 4, z: stage.z + 14, g: 1, d: 0.18 },
    { type: "magic-flourish-r", cubeIdx, x: stage.x + 12, y: stage.y + 4, z: stage.z - 14, g: 1, d: 0.18 },
    { type: "magic-flourish-c", cubeIdx, x: stage.x,      y: stage.y,     z: stage.z,      g: 1, d: 0.14 },
    // Cube fades to invisible while still in gripper.
    { type: "magic-vanish",     cubeIdx, x: stage.x,      y: stage.y + 4, z: stage.z,      g: 1, d: 0.34 },
    // Empty gripper makes its way to the bin (closed — preserves the misdirection).
    { type: "magic-traverse",   cubeIdx, x: dest.x, y: LIFT_Y, z: dest.z, g: 1, d: 0.50 },
    // Gripper opens — cube materialises on the stack.
    { type: "magic-reveal",     cubeIdx, x: dest.x, y: LIFT_Y, z: dest.z, g: 0, d: 0.30 },
    { type: "magic-retract",    cubeIdx, x: dest.x, y: LIFT_Y, z: dest.z, g: 0, d: 0.22 },
  ];
}

// Picks each cube and unceremoniously chucks it. No bin, no care.
function buildThrowAway(cubeIdx) {
  const c = CUBES_INIT[cubeIdx];
  // Per-cube windup + fling apex (varied so the four throws don't look identical)
  const windups = [
    { x: SHOULDER.x - 22, y: 88, z: SHOULDER.z + 26 },
    { x: SHOULDER.x - 28, y: 92, z: SHOULDER.z + 18 },
    { x: SHOULDER.x - 18, y: 90, z: SHOULDER.z + 30 },
    { x: SHOULDER.x - 32, y: 96, z: SHOULDER.z + 14 },
  ];
  const apexes = [
    { x: SHOULDER.x + 95,  y: 56, z: SHOULDER.z - 24 },
    { x: SHOULDER.x + 100, y: 60, z: SHOULDER.z - 30 },
    { x: SHOULDER.x + 105, y: 52, z: SHOULDER.z - 20 },
    { x: SHOULDER.x + 95,  y: 64, z: SHOULDER.z - 28 },
  ];
  const w = windups[cubeIdx];
  const a = apexes[cubeIdx];
  return [
    { type: "approach",     cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 0, d: 0.38 },
    { type: "descend",      cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 0, d: 0.26 },
    { type: "grip-close",   cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 1, d: 0.16 },
    { type: "lift",         cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 1, d: 0.26 },
    { type: "throw-windup", cubeIdx, x: w.x, y: w.y, z: w.z, g: 1, d: 0.32 },
    { type: "throw-fling",  cubeIdx, x: a.x, y: a.y, z: a.z, g: 1, d: 0.14 }, // fast
    { type: "throw-recoil", cubeIdx, x: a.x + 14, y: a.y + 10, z: a.z + 2, g: 0, d: 0.30 },
  ];
}

function buildHome() {
  return [
    { type: "home",  cubeIdx: -1, x: PARK.x, y: PARK.y, z: PARK.z, g: 0, d: 0.60 },
    { type: "reset", cubeIdx: -1, x: PARK.x, y: PARK.y, z: PARK.z, g: 0, d: 0.55 },
  ];
}

// Snap all 4 cubes into a stack at BIN-A, then pick top-down back to source.
function buildDisassemblySetup() {
  return [
    { type: "disas-setup",  cubeIdx: -1, x: PARK.x, y: PARK.y, z: PARK.z, g: 0, d: 0.05 },
    { type: "disas-survey", cubeIdx: -1, x: SHOULDER.x + 65, y: 80, z: SHOULDER.z - 5, g: 0, d: 0.50 },
  ];
}

// Step back and watch a precarious tower topple on its own.
function buildPisaCollapse() {
  return [
    { type: "pisa-step-back", cubeIdx: -1, x: SHOULDER.x + 70, y: 55, z: SHOULDER.z + 22, g: 0, d: 0.55 },
    { type: "pisa-watch",     cubeIdx: -1, x: SHOULDER.x + 70, y: 55, z: SHOULDER.z + 22, g: 0, d: 1.10 },
    { type: "pisa-collapse",  cubeIdx: -1, x: SHOULDER.x + 70, y: 55, z: SHOULDER.z + 22, g: 0, d: 0.04 },
    { type: "kd-observe",     cubeIdx: -1, x: SHOULDER.x + 70, y: 55, z: SHOULDER.z + 22, g: 0, d: 1.40 },
  ];
}

// Toss a cube up, gripper opens, catches it on the way down.
function buildJuggleOne(cubeIdx) {
  const c = CUBES_INIT[cubeIdx];
  const sx = SHOULDER.x + 60, sy = 72, sz = SHOULDER.z - 5;
  return [
    { type: "approach",     cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 0, d: 0.40 },
    { type: "descend",      cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 0, d: 0.26 },
    { type: "grip-close",   cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 1, d: 0.16 },
    { type: "lift",         cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 1, d: 0.30 },
    { type: "juggle-bring", cubeIdx, x: sx, y: sy, z: sz, g: 1, d: 0.45 },
    { type: "juggle-toss",  cubeIdx, x: sx, y: sy + 18, z: sz, g: 0, d: 0.20 },
    { type: "juggle-arc",   cubeIdx, x: sx, y: sy + 14, z: sz, g: 0, d: 0.55 },
    { type: "juggle-catch", cubeIdx, x: sx, y: sy, z: sz, g: 1, d: 0.18 },
    { type: "travel",       cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 1, d: 0.48 },
    { type: "place",        cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 1, d: 0.24 },
    { type: "release",      cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 0, d: 0.16 },
    { type: "lift-clear",   cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 0, d: 0.30 },
  ];
}
function buildDisasPickPlace(stackLevel, cubeIdx) {
  const c = CUBES_INIT[cubeIdx];
  const stackY = CUBE_TOP_Y + stackLevel * CUBE_SIZE;
  return [
    { type: "approach",   cubeIdx, x: STACK_X, y: LIFT_Y + 10, z: STACK_Z, g: 0, d: 0.45 },
    { type: "descend",    cubeIdx, x: STACK_X, y: stackY,      z: STACK_Z, g: 0, d: 0.28 },
    { type: "grip-close", cubeIdx, x: STACK_X, y: stackY,      z: STACK_Z, g: 1, d: 0.18 },
    { type: "lift",       cubeIdx, x: STACK_X, y: LIFT_Y,      z: STACK_Z, g: 1, d: 0.40 },
    { type: "travel",     cubeIdx, x: c.srcX,  y: LIFT_Y,      z: c.srcZ,  g: 1, d: 0.65 },
    { type: "place",      cubeIdx, x: c.srcX,  y: c.srcY,      z: c.srcZ,  g: 1, d: 0.28 },
    { type: "release",    cubeIdx, x: c.srcX,  y: c.srcY,      z: c.srcZ,  g: 0, d: 0.18 },
    { type: "lift-clear", cubeIdx, x: c.srcX,  y: LIFT_Y,      z: c.srcZ,  g: 0, d: 0.38 },
  ];
}

// Wave hello — no cube interaction. Arm rises high and waves.
function buildWaveHello() {
  const cx = SHOULDER.x + 60, cy = 100, cz = SHOULDER.z + 8;
  return [
    { type: "hi-rise",   cubeIdx: -1, x: cx, y: cy,     z: cz,        g: 0, d: 0.55 },
    { type: "hi-wave-l", cubeIdx: -1, x: cx, y: cy + 4, z: cz - 22,   g: 1, d: 0.26 },
    { type: "hi-wave-r", cubeIdx: -1, x: cx, y: cy + 4, z: cz + 32,   g: 0, d: 0.26 },
    { type: "hi-wave-l", cubeIdx: -1, x: cx, y: cy + 4, z: cz - 22,   g: 1, d: 0.26 },
    { type: "hi-wave-r", cubeIdx: -1, x: cx, y: cy + 4, z: cz + 32,   g: 0, d: 0.26 },
    { type: "hi-center", cubeIdx: -1, x: cx, y: cy,     z: cz,        g: 0, d: 0.50 },
  ];
}

// Hands a cube directly at the viewer, holds, retracts, places back.
function buildPresentToViewer(cubeIdx) {
  const c = CUBES_INIT[cubeIdx];
  const PX = SHOULDER.x + 38, PY = 70, PZ = SHOULDER.z + 52;
  return [
    { type: "approach",        cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 0, d: 0.42 },
    { type: "descend",         cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 0, d: 0.28 },
    { type: "grip-close",      cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 1, d: 0.18 },
    { type: "lift",            cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 1, d: 0.36 },
    { type: "present-extend",  cubeIdx, x: PX, y: PY, z: PZ, g: 1, d: 0.55 },
    { type: "present-hold",    cubeIdx, x: PX, y: PY, z: PZ, g: 1, d: 0.85 },
    { type: "present-retract", cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 1, d: 0.55 },
    { type: "place",           cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 1, d: 0.24 },
    { type: "release",         cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 0, d: 0.16 },
    { type: "lift-clear",      cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 0, d: 0.30 },
  ];
}

// Picks a cube and figure-8 sways with it.
function buildDance(cubeIdx) {
  const c = CUBES_INIT[cubeIdx];
  const cx = SHOULDER.x + 55, cy = 75, cz = SHOULDER.z - 6;
  return [
    { type: "approach",    cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 0, d: 0.40 },
    { type: "descend",     cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 0, d: 0.26 },
    { type: "grip-close",  cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 1, d: 0.16 },
    { type: "lift",        cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 1, d: 0.30 },
    { type: "dance-bring", cubeIdx, x: cx, y: cy, z: cz, g: 1, d: 0.40 },
    { type: "dance-1",     cubeIdx, x: cx - 22, y: cy + 6,  z: cz - 14, g: 1, d: 0.22 },
    { type: "dance-2",     cubeIdx, x: cx,      y: cy + 12, z: cz - 6,  g: 1, d: 0.20 },
    { type: "dance-3",     cubeIdx, x: cx + 22, y: cy + 6,  z: cz - 14, g: 1, d: 0.22 },
    { type: "dance-4",     cubeIdx, x: cx + 16, y: cy - 6,  z: cz + 12, g: 1, d: 0.20 },
    { type: "dance-5",     cubeIdx, x: cx,      y: cy - 10, z: cz + 14, g: 1, d: 0.20 },
    { type: "dance-6",     cubeIdx, x: cx - 16, y: cy - 6,  z: cz + 12, g: 1, d: 0.20 },
    { type: "dance-7",     cubeIdx, x: cx,      y: cy,      z: cz,      g: 1, d: 0.30 },
    { type: "travel",      cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 1, d: 0.45 },
    { type: "place",       cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 1, d: 0.24 },
    { type: "release",     cubeIdx, x: c.srcX, y: c.srcY, z: c.srcZ, g: 0, d: 0.16 },
    { type: "lift-clear",  cubeIdx, x: c.srcX, y: LIFT_Y, z: c.srcZ, g: 0, d: 0.30 },
  ];
}

// Cycle variants — each a meaningfully different routine.
const VARIANTS = [
  // 0 · Build a vertical tower → swing-demolish.
  [
    ...buildPickPlace(0, dStack(0)),
    ...buildPickPlace(1, dStack(1)),
    ...buildPickPlace(2, dStack(2)),
    ...buildPickPlace(3, dStack(3)),
    ...buildSweepDemo(),
    ...buildHome(),
  ],
  // 1 · Tower, but cube 1 fails → retry → tower → swing-demolish.
  [
    ...buildPickPlace(0, dStack(0)),
    ...buildPickPlaceWithFail(1, dStack(1)),
    ...buildPickPlace(2, dStack(2)),
    ...buildPickPlace(3, dStack(3)),
    ...buildSweepDemo(),
    ...buildHome(),
  ],
  // 2 · 2×2 grid → top-down crush.
  [
    ...buildPickPlace(0, dGrid(0)),
    ...buildPickPlace(1, dGrid(1)),
    ...buildPickPlace(2, dGrid(2)),
    ...buildPickPlace(3, dGrid(3)),
    ...buildCrushDemo(),
    ...buildHome(),
  ],
  // 3 · Tower, but inspect cube 2 mid-pickup → swing-demolish.
  [
    ...buildPickPlace(0, dStack(0)),
    ...buildPickPlace(1, dStack(1)),
    ...buildPickPlaceWithInspect(2, dStack(2)),
    ...buildPickPlace(3, dStack(3)),
    ...buildSweepDemo(),
    ...buildHome(),
  ],
  // 4 · Line them up → side-sweep them off.
  [
    ...buildPickPlace(0, dLine(0)),
    ...buildPickPlace(1, dLine(1)),
    ...buildPickPlace(2, dLine(2)),
    ...buildPickPlace(3, dLine(3)),
    ...buildLineSweep(),
    ...buildHome(),
  ],
  // 5 · Doesn't care — picks each up and flings it.
  [
    ...buildThrowAway(0),
    ...buildThrowAway(1),
    ...buildThrowAway(2),
    ...buildThrowAway(3),
    ...buildHome(),
  ],
  // 6 · Stage magic — vanish each cube, reappear on the stack, knock down.
  [
    ...buildMagicTeleport(0),
    ...buildMagicTeleport(1),
    ...buildMagicTeleport(2),
    ...buildMagicTeleport(3),
    ...buildSweepDemo(),
    ...buildHome(),
  ],
  // 7 · Wave hello — no cubes.
  [
    ...buildWaveHello(),
    ...buildHome(),
  ],
  // 8 · Juggle.
  [
    ...buildJuggleOne(1),
    ...buildJuggleOne(2),
    ...buildHome(),
  ],
  // 9 · Pyramid → top-down crush.
  [
    ...buildPickPlace(0, dPyramid(0)),
    ...buildPickPlace(1, dPyramid(1)),
    ...buildPickPlace(2, dPyramid(2)),
    ...buildPickPlace(3, dPyramid(3)),
    ...buildCrushDemo(),
    ...buildHome(),
  ],
  // 10 · Sort odds/evens → sweep.
  [
    ...buildPickPlace(0, dSort(0)),
    ...buildPickPlace(1, dSort(1)),
    ...buildPickPlace(2, dSort(2)),
    ...buildPickPlace(3, dSort(3)),
    ...buildSweepDemo(),
    ...buildHome(),
  ],
  // 11 · Hands a cube to the viewer.
  [
    ...buildPresentToViewer(1),
    ...buildHome(),
  ],
  // 12 · Dance with a cube.
  [
    ...buildDance(2),
    ...buildHome(),
  ],
  // 13 · Disassembly.
  [
    ...buildDisassemblySetup(),
    ...buildDisasPickPlace(3, 3),
    ...buildDisasPickPlace(2, 2),
    ...buildDisasPickPlace(1, 1),
    ...buildDisasPickPlace(0, 0),
    ...buildHome(),
  ],
  // 14 · Tower of Pisa → self-collapse.
  [
    ...buildPickPlace(0, dPisa(0)),
    ...buildPickPlace(1, dPisa(1)),
    ...buildPickPlace(2, dPisa(2)),
    ...buildPickPlace(3, dPisa(3)),
    ...buildPisaCollapse(),
    ...buildHome(),
  ],
  // 15 · Domino chain.
  [
    ...buildPickPlace(0, dDomino(0)),
    ...buildPickPlace(1, dDomino(1)),
    ...buildPickPlace(2, dDomino(2)),
    ...buildPickPlace(3, dDomino(3)),
    ...buildDominoTap(),
    ...buildHome(),
  ],
];

function routineDuration(routine) {
  return routine.reduce((s, w) => s + w.d, 0);
}

function getArmPoseInRoutine(routine, t) {
  const totalDur = routineDuration(routine);
  const u = ((t % totalDur) + totalDur) % totalDur;
  let acc = 0;
  for (let i = 0; i < routine.length; i++) {
    const w = routine[i];
    if (u <= acc + w.d) {
      const local = (u - acc) / w.d;
      const next = routine[(i + 1) % routine.length];
      const e = _easeInOut(local);
      return {
        x: w.x + (next.x - w.x) * e,
        y: w.y + (next.y - w.y) * e,
        z: w.z + (next.z - w.z) * e,
        g: w.g + (next.g - w.g) * e,
        phase: i,
        phaseType: w.type,
        local,
        cubeIdx: w.cubeIdx,
      };
    }
    acc += w.d;
  }
  const w = routine[0];
  return { ...w, phase: 0, phaseType: w.type, local: 0 };
}

// ─── Projection + arm IK ────────────────────────────────────────────────
function project(x, y, z) {
  return {
    sx: ORIGIN.x + x - z * DEPTH_COS * DEPTH_K,
    sy: ORIGIN.y - y - z * DEPTH_SIN * DEPTH_K,
  };
}

function solveArm(targetX, targetY, targetZ) {
  const dx = targetX - SHOULDER.x;
  const dz = targetZ - SHOULDER.z;
  const yaw = Math.atan2(dz, dx);
  const wristTargetY = targetY + WRIST_TO_TIP;
  const fwd = Math.hypot(dx, dz);
  const up = wristTargetY - SHOULDER.y;
  let wF = fwd, wU = up;
  let r = Math.hypot(wF, wU);
  const maxR = L1 + L2 - 0.6;
  const minR = Math.abs(L1 - L2) + 0.6;
  if (r > maxR) { const k = maxR / r; wF *= k; wU *= k; r = maxR; }
  if (r < minR && r > 0.001) { const k = minR / r; wF *= k; wU *= k; r = minR; }

  const cos2 = _clamp((r*r - L1*L1 - L2*L2) / (2*L1*L2), -1, 1);
  const theta2 = Math.acos(cos2);
  const gamma  = Math.atan2(wF, wU);
  const beta   = Math.acos(_clamp((L1*L1 + r*r - L2*L2) / (2*L1*r), -1, 1));
  const theta1 = gamma - beta;
  const theta3 = Math.PI - (theta1 + theta2);

  const eF = L1 * Math.sin(theta1);
  const eU = L1 * Math.cos(theta1);
  const wF2 = eF + L2 * Math.sin(theta1 + theta2);
  const wU2 = eU + L2 * Math.cos(theta1 + theta2);
  const fF = wF2;
  const fU = wU2 - L3;

  const cy = Math.cos(yaw), sy = Math.sin(yaw);
  function awp(f, u) {
    return { x: SHOULDER.x + f * cy, y: SHOULDER.y + u, z: SHOULDER.z + f * sy };
  }
  return {
    yaw,
    shoulder: SHOULDER,
    elbow: awp(eF, eU),
    wrist: awp(wF2, wU2),
    flange: awp(fF, fU),
    tip: awp(fF, fU - TOOL_OFFSET),
  };
}

// ─── Render primitives ──────────────────────────────────────────────────
function IsoCube({ cx, cy, cz, size = CUBE_SIZE, color = "var(--accent)", index = -1, dim = false, tilt = 0 }) {
  const h = size / 2;
  const c = {};
  for (const xs of [-h, h]) for (const ys of [-h, h]) for (const zs of [-h, h]) {
    const k = (xs > 0 ? "R" : "L") + (ys > 0 ? "T" : "B") + (zs > 0 ? "F" : "K");
    c[k] = project(cx + xs, cy + ys, cz + zs);
  }
  const front = `M${c.LBF.sx} ${c.LBF.sy} L${c.RBF.sx} ${c.RBF.sy} L${c.RTF.sx} ${c.RTF.sy} L${c.LTF.sx} ${c.LTF.sy} Z`;
  const top   = `M${c.LTF.sx} ${c.LTF.sy} L${c.RTF.sx} ${c.RTF.sy} L${c.RTK.sx} ${c.RTK.sy} L${c.LTK.sx} ${c.LTK.sy} Z`;
  const right = `M${c.RTF.sx} ${c.RTF.sy} L${c.RBF.sx} ${c.RBF.sy} L${c.RBK.sx} ${c.RBK.sy} L${c.RTK.sx} ${c.RTK.sy} Z`;
  const cxS = (c.LBF.sx + c.RTK.sx) / 2;
  const cyS = (c.LBF.sy + c.RTK.sy) / 2;
  const inner = (
    <g>
      <path d={right} fill={color} opacity="0.40" stroke="rgba(0,0,0,0.5)" strokeWidth="0.6" strokeLinejoin="round" />
      <path d={top}   fill={color} opacity="0.95" stroke="rgba(0,0,0,0.5)" strokeWidth="0.6" strokeLinejoin="round" />
      <path d={front} fill={color} opacity="0.70" stroke="rgba(0,0,0,0.5)" strokeWidth="0.6" strokeLinejoin="round" />
      {index >= 0 && (
        <text x={(c.LBF.sx + c.RTF.sx) / 2} y={(c.LBF.sy + c.RTF.sy) / 2 + 3.5}
          textAnchor="middle" fontFamily="var(--font-mono, monospace)" fontSize="9" fontWeight="600"
          fill="rgba(0,0,0,0.55)">
          {String(index + 1).padStart(2, "0")}
        </text>
      )}
    </g>
  );
  if (tilt) {
    return <g transform={`rotate(${tilt} ${cxS} ${cyS})`} opacity={dim ? 0.5 : 1}>{inner}</g>;
  }
  return <g opacity={dim ? 0.5 : 1}>{inner}</g>;
}

function ArmCapsule({ p1, p2, width, fill = "url(#armSeg)", stroke = "rgba(255,255,255,0.18)" }) {
  const a = project(p1.x, p1.y, p1.z);
  const b = project(p2.x, p2.y, p2.z);
  const dx = b.sx - a.sx, dy = b.sy - a.sy;
  const len = Math.hypot(dx, dy);
  if (len < 0.5) return null;
  const ang = Math.atan2(dy, dx) * 180 / Math.PI;
  return (
    <g transform={`translate(${a.sx} ${a.sy}) rotate(${ang})`}>
      <rect x="-2" y={-width/2} width={len + 4} height={width} rx={width/2}
        fill={fill} stroke={stroke} strokeWidth="1" />
      <rect x="2" y={-width/2 + 2} width={len} height="1.2" fill="rgba(255,255,255,0.10)" />
      {Array.from({ length: Math.max(1, Math.floor(len / 22)) }).map((_, i) => (
        <line key={i} x1={10 + i * 22} y1={-width/2 + 2} x2={10 + i * 22} y2={width/2 - 2}
          stroke="rgba(255,255,255,0.10)" strokeWidth="0.8" />
      ))}
    </g>
  );
}

function Joint({ p, r = 7, accent = false }) {
  const s = project(p.x, p.y, p.z);
  return (
    <g>
      <circle cx={s.sx} cy={s.sy} r={r} fill="#1d1f23" stroke="rgba(255,255,255,0.22)" />
      <circle cx={s.sx} cy={s.sy} r={r - 3.5} fill="none" stroke="rgba(255,255,255,0.10)" />
      {accent && <circle cx={s.sx} cy={s.sy} r={r * 0.32} fill="var(--accent)" opacity="0.85" />}
    </g>
  );
}

function Gripper({ flange, grip }) {
  const s = project(flange.x, flange.y, flange.z);
  const tipS = project(flange.x, flange.y - TOOL_OFFSET, flange.z);
  const spread = 9 - grip * 5;
  const jawLen = Math.abs(tipS.sy - s.sy) + 4;
  return (
    <g transform={`translate(${s.sx} ${s.sy})`}>
      <rect x="-11" y="-5" width="22" height="8" rx="1.5" fill="#1c1e22" stroke="rgba(255,255,255,0.22)" />
      <circle cx="0" cy="-1" r="1.4" fill="var(--accent)" opacity="0.85" />
      <line x1={-spread} y1="3" x2={-spread} y2={3 + jawLen} stroke="#d4d4d8" strokeWidth="3" strokeLinecap="round" />
      <line x1={ spread} y1="3" x2={ spread} y2={3 + jawLen} stroke="#d4d4d8" strokeWidth="3" strokeLinecap="round" />
      <line x1={-spread + 1.5} y1="3" x2={-spread + 1.5} y2={3 + jawLen - 2} stroke="rgba(255,255,255,0.18)" strokeWidth="1.1" />
      <line x1={ spread - 1.5} y1="3" x2={ spread - 1.5} y2={3 + jawLen - 2} stroke="rgba(255,255,255,0.18)" strokeWidth="1.1" />
    </g>
  );
}

function Surface({ x0, x1, z0, z1, yTop, yBot, label }) {
  const t = {
    LBF: project(x0, yTop, z1), RBF: project(x1, yTop, z1),
    LBK: project(x0, yTop, z0), RBK: project(x1, yTop, z0),
    LFB: project(x0, yBot, z1), RFB: project(x1, yBot, z1),
    LBB: project(x0, yBot, z0), RBB: project(x1, yBot, z0),
  };
  const top   = `M${t.LBF.sx} ${t.LBF.sy} L${t.RBF.sx} ${t.RBF.sy} L${t.RBK.sx} ${t.RBK.sy} L${t.LBK.sx} ${t.LBK.sy} Z`;
  const front = `M${t.LBF.sx} ${t.LBF.sy} L${t.RBF.sx} ${t.RBF.sy} L${t.RFB.sx} ${t.RFB.sy} L${t.LFB.sx} ${t.LFB.sy} Z`;
  const right = `M${t.RBF.sx} ${t.RBF.sy} L${t.RBK.sx} ${t.RBK.sy} L${t.RBB.sx} ${t.RBB.sy} L${t.RFB.sx} ${t.RFB.sy} Z`;
  return (
    <g>
      <path d={right} fill="#0f1114" stroke="rgba(255,255,255,0.14)" strokeLinejoin="round" />
      <path d={front} fill="url(#benchFront)" stroke="rgba(255,255,255,0.14)" strokeLinejoin="round" />
      <path d={top}   fill="url(#benchTop)" stroke="rgba(255,255,255,0.22)" strokeLinejoin="round" />
      {label && (
        <text x={(t.LBF.sx + t.RBK.sx) / 2} y={(t.LBF.sy + t.RBF.sy) / 2 + 1}
          textAnchor="middle" fontFamily="var(--font-mono, monospace)" fontSize="7.5"
          fill="rgba(255,255,255,0.18)" letterSpacing="0.2em">{label}</text>
      )}
    </g>
  );
}

function SlotMarker({ cx, cz, w = CUBE_SIZE + 4, d = CUBE_SIZE + 4, label, active, warn }) {
  const a = project(cx - w/2, BENCH_TOP + 0.5, cz + d/2);
  const b = project(cx + w/2, BENCH_TOP + 0.5, cz + d/2);
  const c = project(cx + w/2, BENCH_TOP + 0.5, cz - d/2);
  const d2 = project(cx - w/2, BENCH_TOP + 0.5, cz - d/2);
  const labPos = project(cx, BENCH_TOP + 0.5, cz - d/2 - 4);
  const color = warn ? "#f5a623" : "var(--accent)";
  const dim   = warn ? "rgba(245,166,35,0.25)" : "rgba(124,252,77,0.22)";
  return (
    <g>
      <path d={`M${a.sx} ${a.sy} L${b.sx} ${b.sy} L${c.sx} ${c.sy} L${d2.sx} ${d2.sy} Z`}
        fill="none"
        stroke={active ? color : dim}
        strokeDasharray="2 3"
        strokeWidth={active ? 1.4 : 1}
        opacity={active ? 0.9 : 0.5} />
      {label && (
        <text x={labPos.sx} y={labPos.sy} textAnchor="middle"
          fontFamily="var(--font-mono, monospace)" fontSize="8"
          fill={active ? color : "var(--fg-4)"} letterSpacing="0.18em">{label}</text>
      )}
    </g>
  );
}

// ─── Phase-type groupings ───────────────────────────────────────────────
const FRUSTRATED_TYPES = new Set([
  "grip-fail", "lift-empty",
  "frustrated-1", "frustrated-2", "frustrated-3", "frustrated-settle",
]);
const PICKUP_TYPES = new Set([
  "approach", "descend", "grip-close", "grip-fail", "lift-empty",
  "frustrated-1", "frustrated-2", "frustrated-3", "frustrated-settle",
  "retry-descend", "lift",
]);
const PLACE_TYPES = new Set(["travel", "place", "release", "lift-clear"]);

// ─── Main component ─────────────────────────────────────────────────────
function RobotArm() {
  const [pose, setPose] = React.useState(() => getArmPoseInRoutine(VARIANTS[0], 0));
  const stateRef = React.useRef({
    acc: 0,
    last: performance.now(),
    cycleIdx: 0,
    routine: VARIANTS[0],
    cubes: CUBES_INIT.map((c) => ({
      x: c.srcX, y: c.srcY, z: c.srcZ,
      vx: 0, vy: 0, vz: 0,
      tilt: 0, vRot: 0,
      mode: "rest",
      opacity: 1,
      resetFrom: null,
    })),
    prevPhaseType: null,
    trail: [],
    particles: [],
    frustration: 0,
  });

  // Spawn N sparkle particles centered at (x,y,z).
  const spawnSparkles = React.useCallback((x, y, z, t, n = 24) => {
    const s = stateRef.current;
    for (let i = 0; i < n; i++) {
      const ang = Math.random() * Math.PI * 2;
      const speed = 22 + Math.random() * 48;
      s.particles.push({
        x: x + (Math.random() - 0.5) * 4,
        y: y + (Math.random() - 0.5) * 6,
        z: z + (Math.random() - 0.5) * 4,
        vx: Math.cos(ang) * speed,
        vy: 18 + Math.random() * 36,
        vz: Math.sin(ang) * speed,
        t,
        life: 0.55 + Math.random() * 0.45,
        r: 1 + Math.random() * 1.4,
      });
    }
  }, []);

  React.useEffect(() => {
    let raf;
    const tick = (now) => {
      const s = stateRef.current;
      const dt = Math.min((now - s.last) / 1000, 0.05);
      s.last = now;
      const mVar = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--motion"));
      const m = isFinite(mVar) ? mVar : 1;
      if (m > 0.01) {
        s.acc += dt * m;
        const totalDur = routineDuration(s.routine);
        if (s.acc >= totalDur) {
          s.acc -= totalDur;
          s.cycleIdx = (s.cycleIdx + 1) % VARIANTS.length;
          s.routine = VARIANTS[s.cycleIdx];
          // Reset prevPhaseType so the first phase of new cycle triggers as "just entered"
          s.prevPhaseType = null;
        }
        const newPose = getArmPoseInRoutine(s.routine, s.acc);

        s.trail.push({ x: newPose.x, y: newPose.y, z: newPose.z, t: now });
        while (s.trail.length && now - s.trail[0].t > 1300) s.trail.shift();

        // Frustration meter
        const isFrust = FRUSTRATED_TYPES.has(newPose.phaseType);
        s.frustration = isFrust ? Math.min(1, s.frustration + dt * 6)
                                : Math.max(0, s.frustration - dt * 2.5);

        const justEntered = s.prevPhaseType !== newPose.phaseType;
        if (justEntered) {
          if (newPose.phaseType === "grip-close" && newPose.cubeIdx >= 0) {
            s.cubes[newPose.cubeIdx].mode = "held";
            s.cubes[newPose.cubeIdx].tilt = 0;
            s.cubes[newPose.cubeIdx].vRot = 0;
          }
          if (newPose.phaseType === "release" && newPose.cubeIdx >= 0) {
            const c = s.cubes[newPose.cubeIdx];
            c.mode = "rest";
            c.x = newPose.x; c.y = newPose.y; c.z = newPose.z;
            c.vx = c.vy = c.vz = 0; c.tilt = 0; c.vRot = 0;
          }
          if (newPose.phaseType === "disas-setup") {
            s.cubes.forEach((c, i) => {
              c.x = STACK_X;
              c.y = CUBE_TOP_Y + i * CUBE_SIZE;
              c.z = STACK_Z;
              c.vx = c.vy = c.vz = 0;
              c.vRot = 0;
              c.tilt = 0;
              c.mode = "rest";
              c.opacity = 1;
            });
          }
          if (newPose.phaseType === "pisa-collapse") {
            s.cubes.forEach((c) => {
              if (c.mode !== "rest") return;
              if (Math.abs(c.x - STACK_X) > 36 || Math.abs(c.z - STACK_Z) > 20) return;
              c.mode = "falling";
              const h = Math.max(0, c.y - CUBE_TOP_Y);
              c.vx = 32 + h * 0.9 + Math.random() * 18;
              c.vy = 12 + h * 0.30 + Math.random() * 10;
              c.vz = (Math.random() - 0.5) * 30;
              c.vRot = 180 + Math.random() * 180;
            });
          }
          if (newPose.phaseType === "juggle-toss" && newPose.cubeIdx >= 0) {
            const c = s.cubes[newPose.cubeIdx];
            const tossDur = (s.routine[newPose.phase].d
                          + (s.routine[newPose.phase + 1]?.d ?? 0.55)) * 1000;
            c.mode = "tossed";
            c.tossT0 = now;
            c.tossDur = tossDur;
            c.tossStart = { x: c.x, y: c.y, z: c.z };
            const catchPhase = s.routine[newPose.phase + 2];
            c.tossEnd = catchPhase
              ? { x: catchPhase.x, y: catchPhase.y, z: catchPhase.z }
              : { x: c.x, y: c.y, z: c.z };
            c.tossApex = Math.max(c.y, c.tossEnd.y) + 55;
          }
          if (newPose.phaseType === "juggle-catch" && newPose.cubeIdx >= 0) {
            const c = s.cubes[newPose.cubeIdx];
            c.mode = "held";
            c.tilt = 0;
            c.vRot = 0;
          }
          if (newPose.phaseType === "kd-tap") {
            // Push the leftmost resting cube near the stack +x; collisions
            // cascade the rest of the row via the cube-cube pass below.
            const candidates = s.cubes.filter(c =>
              c.mode === "rest" && Math.abs(c.z - STACK_Z) < 14
              && c.x > BENCH.x0 - 4 && c.x < BENCH.x1 + 4);
            if (candidates.length) {
              const leftmost = candidates.reduce((a, b) => a.x < b.x ? a : b);
              leftmost.mode = "falling";
              leftmost.vx = 120;
              leftmost.vy = 14;
              leftmost.vz = (Math.random() - 0.5) * 6;
              leftmost.vRot = 260;
            }
          }
          if (newPose.phaseType === "kd-swing") {
            // Horizontal sweep — push cubes along the swing direction.
            const phase = s.routine[newPose.phase];
            const prevPhase = s.routine[newPose.phase - 1] || phase;
            const dirX = phase.x - prevPhase.x;
            const dirZ = phase.z - prevPhase.z;
            const mag = Math.hypot(dirX, dirZ) || 1;
            const ux = dirX / mag, uz = dirZ / mag;
            s.cubes.forEach((c) => {
              if (c.mode === "rest"
                  && Math.abs(c.x - STACK_X) < 60
                  && Math.abs(c.z - STACK_Z) < 30) {
                c.mode = "falling";
                const h = Math.max(0, c.y - CUBE_TOP_Y);
                const speed = 90 + Math.random() * 40 + h * 0.4;
                c.vx = ux * speed + (Math.random() - 0.5) * 22;
                c.vy = 28 + Math.random() * 30 + h * 0.55;
                c.vz = uz * speed + (Math.random() - 0.5) * 40;
                c.vRot = (Math.random() - 0.5) * 320;
              }
            });
          }
          if (newPose.phaseType === "magic-vanish" && newPose.cubeIdx >= 0) {
            s.cubes[newPose.cubeIdx].mode = "vanishing";
            spawnSparkles(newPose.x, newPose.y, newPose.z, now, 22);
          }
          if (newPose.phaseType === "magic-traverse" && newPose.cubeIdx >= 0) {
            // Snap cube to destination, invisible.
            const c = s.cubes[newPose.cubeIdx];
            const dest = dStack(newPose.cubeIdx);
            c.mode = "vanished";
            c.opacity = 0;
            c.x = dest.x; c.y = dest.y; c.z = dest.z;
            c.vx = c.vy = c.vz = 0; c.tilt = 0; c.vRot = 0;
          }
          if (newPose.phaseType === "magic-reveal" && newPose.cubeIdx >= 0) {
            const c = s.cubes[newPose.cubeIdx];
            const dest = dStack(newPose.cubeIdx);
            c.mode = "revealing";
            c.x = dest.x; c.y = dest.y; c.z = dest.z;
            spawnSparkles(dest.x, dest.y + 4, dest.z, now, 32);
          }
          if (newPose.phaseType === "magic-retract" && newPose.cubeIdx >= 0) {
            const c = s.cubes[newPose.cubeIdx];
            c.mode = "rest";
            c.opacity = 1;
          }
          if (newPose.phaseType === "throw-recoil" && newPose.cubeIdx >= 0) {
            // Fling: cube leaves the gripper with arc velocity toward a target.
            const c = s.cubes[newPose.cubeIdx];
            if (c.mode === "held") {
              const throwTargets = [
                { x: SHOULDER.x + 200, z: SHOULDER.z + 28 },
                { x: SHOULDER.x + 155, z: SHOULDER.z + 58 },
                { x: SHOULDER.x + 195, z: SHOULDER.z - 26 },
                { x: SHOULDER.x + 130, z: SHOULDER.z + 8 },
              ];
              const tg = throwTargets[newPose.cubeIdx];
              const T = 1.0;
              const gA = 260;
              const restY = CUBE_TOP_Y;
              c.mode = "falling";
              c.vx = (tg.x - c.x) / T + (Math.random() - 0.5) * 14;
              c.vy = (restY - c.y + 0.5 * gA * T * T) / T + Math.random() * 10;
              c.vz = (tg.z - c.z) / T + (Math.random() - 0.5) * 14;
              c.vRot = (Math.random() - 0.5) * 540;
            }
          }
          if (newPose.phaseType === "kd-strike") {
            // Crush from above — scatter cubes radially from strike center.
            s.cubes.forEach((c) => {
              if (c.mode === "rest"
                  && Math.abs(c.x - STACK_X) < 60
                  && Math.abs(c.z - STACK_Z) < 30) {
                c.mode = "falling";
                const dx = c.x - STACK_X;
                const dz = c.z - STACK_Z;
                const dist = Math.hypot(dx, dz);
                let ux, uz;
                if (dist < 0.5) {
                  const ang = Math.random() * Math.PI * 2;
                  ux = Math.cos(ang); uz = Math.sin(ang);
                } else {
                  ux = dx / dist; uz = dz / dist;
                }
                const speed = 95 + Math.random() * 45;
                c.vx = ux * speed + (Math.random() - 0.5) * 25;
                c.vy = 32 + Math.random() * 22;
                c.vz = uz * speed + (Math.random() - 0.5) * 25;
                c.vRot = (Math.random() - 0.5) * 360;
              }
            });
          }
          if (newPose.phaseType === "reset") {
            s.cubes.forEach((c) => {
              c.resetFrom = { x: c.x, y: c.y, z: c.z, tilt: c.tilt };
              c.mode = "resetting";
            });
          }
        }

        // Per-frame cube physics + held tracking
        s.cubes.forEach((c, i) => {
          if (c.mode === "held") {
            c.x = newPose.x; c.y = newPose.y; c.z = newPose.z;
            c.opacity = 1;
          } else if (c.mode === "vanishing") {
            // Still in the gripper, fading out
            c.x = newPose.x; c.y = newPose.y; c.z = newPose.z;
            c.opacity = Math.max(0, 1 - newPose.local);
          } else if (c.mode === "vanished") {
            c.opacity = 0;
          } else if (c.mode === "revealing") {
            c.opacity = Math.min(1, newPose.local);
          } else if (c.mode === "tossed") {
            const t = Math.min(1, (now - c.tossT0) / c.tossDur);
            const st = c.tossStart, en = c.tossEnd;
            c.x = st.x + (en.x - st.x) * t;
            c.z = st.z + (en.z - st.z) * t;
            const linY = st.y + (en.y - st.y) * t;
            const peakAdd = c.tossApex - Math.max(st.y, en.y);
            c.y = linY + 4 * peakAdd * t * (1 - t);
            c.tilt += 540 * dt;
          } else if (c.mode === "falling") {
            const g = 260;
            c.vy -= g * dt;
            c.x += c.vx * dt;
            c.y += c.vy * dt;
            c.z += c.vz * dt;
            c.tilt += c.vRot * dt;
            const inBenchXZ  = (c.x >= BENCH.x0 - 4 && c.x <= BENCH.x1 + 4 && c.z >= BENCH.z0 - 4 && c.z <= BENCH.z1 + 4);
            const inStackXZ  = (c.x >= STACK_PAD.x0 - 4 && c.x <= STACK_PAD.x1 + 4 && c.z >= STACK_PAD.z0 - 4 && c.z <= STACK_PAD.z1 + 4);
            const restY = (inBenchXZ || inStackXZ) ? CUBE_TOP_Y : FLOOR_Y + CUBE_HALF;
            if (c.y < restY) {
              c.y = restY;
              c.vy = -c.vy * 0.30;
              c.vx *= 0.62;
              c.vz *= 0.62;
              c.vRot *= 0.45;
              if (Math.abs(c.vy) < 4 && Math.abs(c.vx) < 3 && Math.abs(c.vz) < 3) {
                c.vx = c.vy = c.vz = 0; c.vRot = 0; c.mode = "settled";
              }
            }
            c.x = _clamp(c.x, -180, 220);
            c.z = _clamp(c.z, -85, 80);
          } else if (c.mode === "resetting") {
            const e = _easeInOut(newPose.local);
            const src = CUBES_INIT[i];
            const from = c.resetFrom;
            c.x = from.x + (src.srcX - from.x) * e;
            c.y = from.y + (src.srcY - from.y) * e;
            c.z = from.z + (src.srcZ - from.z) * e;
            c.y += Math.sin(e * Math.PI) * 14;
            c.tilt = from.tilt * (1 - e);
            if (e >= 0.98) {
              c.mode = "rest";
              c.x = src.srcX; c.y = src.srcY; c.z = src.srcZ; c.tilt = 0;
            }
          }
        });

        // Pisa wobble while the arm is watching — animate a small tilt
        // on each stack cube so it looks live.
        if (newPose.phaseType === "pisa-watch") {
          const t = newPose.local;
          s.cubes.forEach((c) => {
            if (c.mode !== "rest") return;
            if (Math.abs(c.x - STACK_X) > 36 || Math.abs(c.z - STACK_Z) > 20) return;
            const h = Math.max(0, c.y - CUBE_TOP_Y);
            const amp = 0.6 + h * 0.04;
            c.tilt = amp * Math.sin(t * Math.PI * 6) * (1 + t * 0.8);
          });
        }

        // Cube-cube collisions — falling cubes wake resting cubes.
        for (let i = 0; i < s.cubes.length; i++) {
          const a = s.cubes[i];
          if (a.mode !== "falling") continue;
          for (let j = 0; j < s.cubes.length; j++) {
            if (i === j) continue;
            const b = s.cubes[j];
            if (b.mode !== "rest" && b.mode !== "settled") continue;
            const dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z;
            const minDist = CUBE_SIZE * 0.92;
            const d2 = dx*dx + dy*dy + dz*dz;
            if (d2 < minDist * minDist && d2 > 0.05) {
              const d = Math.sqrt(d2);
              const overlap = minDist - d;
              const nx = dx / d, ny = dy / d, nz = dz / d;
              // Push apart
              a.x += nx * overlap * 0.5;
              a.y += ny * overlap * 0.5;
              a.z += nz * overlap * 0.5;
              b.x -= nx * overlap * 0.5;
              b.y -= ny * overlap * 0.5;
              b.z -= nz * overlap * 0.5;
              // Wake b
              b.mode = "falling";
              b.vx = a.vx * 0.55 + nx * -22;
              b.vy = Math.max(12, a.vy * 0.5);
              b.vz = a.vz * 0.55 + nz * -22;
              b.vRot = (Math.random() - 0.5) * 240;
              // Damp a
              a.vx *= 0.55;
              a.vy *= 0.85;
              a.vz *= 0.55;
            }
          }
        }

        // Update particles
        const live = [];
        for (const p of s.particles) {
          const age = (now - p.t) / 1000;
          if (age < p.life) {
            p.x += p.vx * dt;
            p.y += p.vy * dt;
            p.z += p.vz * dt;
            p.vy -= 70 * dt;
            p.vx *= 1 - dt * 0.8;
            p.vz *= 1 - dt * 0.8;
            live.push(p);
          }
        }
        s.particles = live;

        s.prevPhaseType = newPose.phaseType;
        setPose({ ...newPose, _now: now });
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, []);

  const s = stateRef.current;
  const arm = solveArm(pose.x, pose.y, pose.z);
  const now = pose._now || performance.now();
  const cubes = s.cubes;

  // "Held" for rendering purposes includes the fading-out cube and the
  // mid-toss cube — they're rendered on top of the arm.
  const heldIdx = cubes.findIndex((c) =>
    c.mode === "held" || c.mode === "vanishing" || c.mode === "tossed");
  const renderCubes = cubes.map((c, i) => ({ c, i }))
    .filter(({ c }) =>
      c.mode !== "held" && c.mode !== "vanishing" && c.mode !== "vanished"
      && c.mode !== "tossed")
    .sort((a, b) => b.c.z - a.c.z);

  const activeSourceIdx = (PICKUP_TYPES.has(pose.phaseType) && pose.cubeIdx >= 0) ? pose.cubeIdx : -1;
  const stackActive = PLACE_TYPES.has(pose.phaseType);
  const isKnockdown = pose.phaseType && pose.phaseType.startsWith("kd-");
  const isInspect = pose.phaseType && pose.phaseType.startsWith("inspect-");
  const isThrow = pose.phaseType && pose.phaseType.startsWith("throw-");
  const isMagic = pose.phaseType && pose.phaseType.startsWith("magic-");
  const isHi    = pose.phaseType && pose.phaseType.startsWith("hi-");
  const isPresent = pose.phaseType && pose.phaseType.startsWith("present-");
  const isDance = pose.phaseType && pose.phaseType.startsWith("dance-");
  const isPisa  = pose.phaseType && pose.phaseType.startsWith("pisa-");
  const isJuggle = pose.phaseType && pose.phaseType.startsWith("juggle-");
  const yawDeg = arm.yaw * 180 / Math.PI;
  const frustration = s.frustration;
  const warn = frustration > 0.05;

  // Status line text + color
  let statusLine, statusColor;
  if (frustration > 0.05) {
    statusLine = "▲ GRIP-SLIP · RETRY";
    statusColor = "#f5a623";
  } else if (pose.phaseType === "kd-strike") {
    statusLine = "× IMPACT";
    statusColor = "#ef4444";
  } else if (isKnockdown) {
    statusLine = "× DEMOLITION";
    statusColor = "#ef4444";
  } else if (pose.phaseType === "throw-recoil" || (isThrow && pose.g < 0.5)) {
    statusLine = "↗ FLING";
    statusColor = "#a78bfa";
  } else if (isHi) {
    statusLine = "◐ HELLO";
    statusColor = "#7dd3fc";
  } else if (isPresent) {
    statusLine = "◐ PRESENT";
    statusColor = "#7dd3fc";
  } else if (isDance) {
    statusLine = "♪ DANCE";
    statusColor = "#f0abfc";
  } else if (pose.phaseType === "pisa-watch") {
    statusLine = "⚠ UNSTABLE";
    statusColor = "#f5a623";
  } else if (pose.phaseType === "pisa-collapse") {
    statusLine = "× COLLAPSE";
    statusColor = "#ef4444";
  } else if (pose.phaseType === "juggle-toss" || pose.phaseType === "juggle-arc") {
    statusLine = "↑ JUGGLE";
    statusColor = "#facc15";
  } else if (pose.phaseType === "juggle-catch") {
    statusLine = "● CATCH";
    statusColor = "var(--accent)";
  } else if (pose.phaseType === "kd-tap") {
    statusLine = "⋯ DOMINO";
    statusColor = "#fb923c";
  } else if (pose.phaseType === "magic-vanish") {
    statusLine = "✦ VANISH";
    statusColor = "#c084fc";
  } else if (pose.phaseType === "magic-reveal") {
    statusLine = "✦ REVEAL";
    statusColor = "#c084fc";
  } else if (pose.phaseType === "magic-traverse") {
    statusLine = "◌ EMPTY";
    statusColor = "#c084fc";
  } else if (isMagic) {
    statusLine = "✦ FLOURISH";
    statusColor = "#c084fc";
  } else if (isInspect) {
    statusLine = "◇ INSPECTING";
    statusColor = "#7dd3fc";
  } else if (pose.g > 0.5) {
    statusLine = "● GRIP-CLOSED";
    statusColor = "var(--accent)";
  } else {
    statusLine = "○ GRIP-OPEN";
    statusColor = "var(--fg-4)";
  }

  // Mode line reflects the current routine variant.
  const variantLabels = [
    "tower → sweep",
    "tower (retry) → sweep",
    "2×2 grid → crush",
    "tower + inspect → sweep",
    "line → push-off",
    "couldn't be bothered",
    "now you see it…",
    "hello",
    "juggle act",
    "pyramid → crush",
    "sort odds & evens → sweep",
    "for the viewer",
    "dance routine",
    "disassembly",
    "tower of pisa → collapse",
    "domino chain",
  ];
  let modeLine = "arm-04 · " + (variantLabels[s.cycleIdx] || "stack + demo");
  if (isKnockdown && pose.phaseType !== "kd-tap") modeLine = "arm-04 · DEMOLITION";
  else if (pose.phaseType === "kd-tap") modeLine = "arm-04 · domino chain";
  else if (isInspect) modeLine = "arm-04 · inspect routine";
  else if (isThrow) modeLine = "arm-04 · fling mode";
  else if (isMagic) modeLine = "arm-04 · stage magic";
  else if (isHi) modeLine = "arm-04 · greeting";
  else if (isPresent) modeLine = "arm-04 · for the viewer";
  else if (isDance) modeLine = "arm-04 · dance routine";
  else if (isPisa) modeLine = "arm-04 · tower of pisa";
  else if (isJuggle) modeLine = "arm-04 · juggle act";
  else if (frustration > 0.05) modeLine = "arm-04 · retry sequence";

  return (
    <svg viewBox={`0 0 ${SVG_W} ${SVG_H}`}
         xmlns="http://www.w3.org/2000/svg"
         style={{ width: "100%", height: "auto", display: "block" }}
         aria-label="3-axis robot arm stacking and knocking down cubes">
      <defs>
        <pattern id="armGrid" width="24" height="24" patternUnits="userSpaceOnUse">
          <path d="M24 0 L24 24 M0 24 L24 24" stroke="rgba(255,255,255,0.04)" />
        </pattern>
        <linearGradient id="armSeg" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor="#22262b" />
          <stop offset="100%" stopColor="#0c0d11" />
        </linearGradient>
        <linearGradient id="benchTop" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor="#1c1f24" />
          <stop offset="100%" stopColor="#12141a" />
        </linearGradient>
        <linearGradient id="benchFront" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor="#14171c" />
          <stop offset="100%" stopColor="#08090d" />
        </linearGradient>
        <linearGradient id="armBase" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor="#262a30" />
          <stop offset="100%" stopColor="#0b0c0f" />
        </linearGradient>
      </defs>

      <rect width={SVG_W} height={SVG_H} fill="url(#armGrid)" opacity="0.55" />

      <g stroke="rgba(255,255,255,0.18)" strokeWidth="1">
        <path d="M8 8 L8 18 M8 8 L18 8" />
        <path d={`M${SVG_W - 8} 8 L${SVG_W - 8} 18 M${SVG_W - 8} 8 L${SVG_W - 18} 8`} />
        <path d={`M8 ${SVG_H - 8} L8 ${SVG_H - 18} M8 ${SVG_H - 8} L18 ${SVG_H - 8}`} />
        <path d={`M${SVG_W - 8} ${SVG_H - 8} L${SVG_W - 8} ${SVG_H - 18} M${SVG_W - 8} ${SVG_H - 8} L${SVG_W - 18} ${SVG_H - 8}`} />
      </g>

      <g fontFamily="var(--font-mono, monospace)" letterSpacing="0.14em">
        <circle cx="22" cy="22" r="3.5" fill={warn ? "#f5a623" : "var(--accent)"}>
          <animate attributeName="opacity" values="1;0.35;1" dur={warn ? "0.6s" : "2s"} repeatCount="indefinite" />
        </circle>
        <text x="32" y="25" fontSize="10" fill="var(--fg-2)">OW · ROBOTICS</text>
        <text x="32" y="38" fontSize="9" fill={warn ? "#f5a623" : "var(--fg-4)"}>{modeLine}</text>
      </g>
      <g fontFamily="var(--font-mono, monospace)" letterSpacing="0.10em">
        <text x={SVG_W - 22} y="25" textAnchor="end" fontSize="9" fill="var(--fg-3)">
          YAW {(yawDeg >= 0 ? "+" : "")}{yawDeg.toFixed(0).padStart(3, "0")}°
          {"   "}
          X{Math.round(pose.x).toString().padStart(3, "0")}
          {"   "}
          Z{Math.round(pose.z).toString().padStart(3, "0")}
        </text>
        <text x={SVG_W - 22} y="38" textAnchor="end" fontSize="9" fill={statusColor}>{statusLine}</text>
      </g>

      {/* floor depth lines */}
      <g opacity="0.25" stroke="rgba(124,252,77,0.55)" strokeDasharray="2 5">
        {[-40, -20, 0, 20, 40].map((zz) => {
          const a = project(-110, FLOOR_Y, zz);
          const b = project(170, FLOOR_Y, zz);
          return <line key={zz} x1={a.sx} y1={a.sy} x2={b.sx} y2={b.sy} />;
        })}
      </g>

      {(() => {
        const a = project(-150, FLOOR_Y, 60);
        const b = project(200, FLOOR_Y, 60);
        return <g>
          <line x1={a.sx} y1={a.sy} x2={b.sx} y2={b.sy} stroke="rgba(255,255,255,0.18)" />
          <line x1={a.sx} y1={a.sy} x2={b.sx} y2={b.sy} stroke="var(--accent)" strokeDasharray="2 6" opacity="0.30" />
        </g>;
      })()}

      <Surface x0={STACK_PAD.x0} x1={STACK_PAD.x1} z0={STACK_PAD.z0} z1={STACK_PAD.z1}
        yTop={BENCH_TOP} yBot={BENCH_BOT} label="BIN-A" />
      <Surface x0={BENCH.x0} x1={BENCH.x1} z0={BENCH.z0} z1={BENCH.z1}
        yTop={BENCH_TOP} yBot={BENCH_BOT} label="SOURCE" />

      {SOURCES.map((src, i) => (
        <SlotMarker key={"s" + i} cx={src.x} cz={src.z}
          label={`S${i + 1}`} active={activeSourceIdx === i}
          warn={activeSourceIdx === i && frustration > 0.05} />
      ))}
      <SlotMarker cx={STACK_X} cz={STACK_Z} label="DST" active={stackActive} />

      {/* trail */}
      {s.trail.map((p, i) => {
        const age = (now - p.t) / 1300;
        if (age >= 1) return null;
        const proj = project(p.x, p.y, p.z);
        return <circle key={i} cx={proj.sx} cy={proj.sy} r={2.5 * (1 - age)}
          fill={isKnockdown ? "#ef4444" : "var(--accent)"} opacity={(1 - age) * 0.35} />;
      })}

      {/* static + falling cubes (back to front) */}
      {renderCubes.map(({ c, i }) => (
        <g key={"c" + i} opacity={c.opacity ?? 1}>
          <IsoCube cx={c.x} cy={c.y} cz={c.z} index={i} tilt={c.tilt} />
        </g>
      ))}

      {/* Robot base + column + yaw turret */}
      {(() => {
        const baseTop = FLOOR_Y + 26;
        const baseSurface = (
          <Surface x0={SHOULDER.x - 28} x1={SHOULDER.x + 28}
            z0={SHOULDER.z - 26} z1={SHOULDER.z + 26}
            yTop={baseTop} yBot={FLOOR_Y} />
        );
        const col1 = project(SHOULDER.x - 9, baseTop, SHOULDER.z + 9);
        const col2 = project(SHOULDER.x + 9, baseTop, SHOULDER.z + 9);
        const col3 = project(SHOULDER.x + 9, SHOULDER.y - 8, SHOULDER.z + 9);
        const col4 = project(SHOULDER.x - 9, SHOULDER.y - 8, SHOULDER.z + 9);
        const colBack1 = project(SHOULDER.x + 9, baseTop, SHOULDER.z - 9);
        const colBack2 = project(SHOULDER.x + 9, SHOULDER.y - 8, SHOULDER.z - 9);
        const turretCenter = project(SHOULDER.x, SHOULDER.y - 6, SHOULDER.z);
        const nx = SHOULDER.x + Math.cos(arm.yaw) * 11;
        const nz = SHOULDER.z + Math.sin(arm.yaw) * 11;
        const np = project(nx, SHOULDER.y - 6, nz);
        const led = project(SHOULDER.x + 24, baseTop + 4, SHOULDER.z + 22);
        return (
          <g>
            {baseSurface}
            <circle cx={led.sx} cy={led.sy} r="2.2" fill={warn ? "#f5a623" : "var(--accent)"} opacity="0.85">
              <animate attributeName="opacity" values="0.85;0.3;0.85" dur={warn ? "0.5s" : "1.4s"} repeatCount="indefinite" />
            </circle>
            <path d={`M${col2.sx} ${col2.sy} L${col3.sx} ${col3.sy} L${colBack2.sx} ${colBack2.sy} L${colBack1.sx} ${colBack1.sy} Z`}
              fill="#0a0b0e" stroke="rgba(255,255,255,0.14)" />
            <path d={`M${col1.sx} ${col1.sy} L${col2.sx} ${col2.sy} L${col3.sx} ${col3.sy} L${col4.sx} ${col4.sy} Z`}
              fill="url(#armBase)" stroke="rgba(255,255,255,0.18)" />
            <ellipse cx={turretCenter.sx} cy={turretCenter.sy}
              rx="14" ry={14 * DEPTH_SIN * DEPTH_K * 1.6 + 1.5}
              fill="#262a30" stroke="rgba(255,255,255,0.24)" />
            <circle cx={np.sx} cy={np.sy} r="2.4" fill={warn ? "#f5a623" : "var(--accent)"} />
          </g>
        );
      })()}

      <ArmCapsule p1={arm.shoulder} p2={arm.elbow}  width={16} />
      <ArmCapsule p1={arm.elbow}    p2={arm.wrist}  width={14} />
      <ArmCapsule p1={arm.wrist}    p2={arm.flange} width={12} />
      <Joint p={arm.shoulder} r={11} accent />
      <Joint p={arm.elbow}    r={9}  accent />
      <Joint p={arm.wrist}    r={7}  accent />
      <Gripper flange={arm.flange} grip={pose.g} />

      {/* Held cube — on top of arm */}
      {heldIdx >= 0 && (() => {
        const c = cubes[heldIdx];
        if ((c.opacity ?? 1) < 0.01) return null;
        return (
          <g opacity={c.opacity ?? 1}>
            <IsoCube cx={c.x} cy={c.y} cz={c.z} index={heldIdx} />
          </g>
        );
      })()}

      {/* Sparkle particles — drawn on top of everything */}
      {s.particles.map((p, i) => {
        const age = (now - p.t) / 1000;
        const a = 1 - age / p.life;
        const proj = project(p.x, p.y, p.z);
        return (
          <circle key={"sp" + i} cx={proj.sx} cy={proj.sy} r={p.r * (1 - age * 0.4)}
            fill="#c084fc" opacity={a * 0.9} />
        );
      })}

      {/* Frustration "!" — floats above the arm during retry */}
      {frustration > 0.15 && (() => {
        const eff = project(pose.x, pose.y + 35, pose.z);
        return (
          <g transform={`translate(${eff.sx} ${eff.sy})`} opacity={Math.min(1, frustration * 1.6)}>
            <circle r="9" fill="#f5a623" opacity="0.18" />
            <text x="0" y="3.5" textAnchor="middle"
              fontFamily="var(--font-mono, monospace)" fontWeight="700"
              fontSize="14" fill="#f5a623">!</text>
          </g>
        );
      })()}
    </svg>
  );
}

Object.assign(window, { RobotArm });
