// ow-anim.jsx — OnWeight logo motion: head+arms "O on W" mark trains, weighs in,
// fights, and wins — resolving into the exact classic W mark.
// The TRAINING beat offers several classic boxing drills (pick via the tweak).

(function () {
// self-contained easing (works with or without animations.jsx loaded)
const E = (window.Easing && window.Easing.easeOutBack) ? window.Easing : {
  linear: (t) => t,
  easeOutQuad: (t) => t * (2 - t),
  easeOutCubic: (t) => (--t) * t * t + 1,
  easeInOutCubic: (t) => (t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1),
  easeOutBack: (t) => { const c1 = 1.70158, c3 = c1 + 1; return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); },
};
// timeline hook when inside a <Stage>; a no-op fallback when embedded standalone
const useTimeSafe = window.useTime || (() => 0);

// ── figure model (viewBox 0 0 100 108, body centre C, head ring above) ───────
const C = [50, 56];
// pose = { lE,lF, rE,rF (elbow/fist points), hy head-y, hx head-x }
const POSE = {
  // shadowbox — tight high guard, short jabs
  GUARD_HI: { lE: [41, 58], lF: [28, 38], rE: [59, 58], rF: [72, 38], hy: 26, hx: 0 },
  TJAB_L:   { lE: [42, 48], lF: [27, 29], rE: [59, 58], rF: [72, 38], hy: 26, hx: 1.5 },
  TJAB_R:   { lE: [41, 58], lF: [28, 38], rE: [58, 48], rF: [73, 29], hy: 26, hx: -1.5 },
  // speed bag — rig mounted up to the right, rapid taps
  SB_A: { lE: [42, 54], lF: [33, 40], rE: [64, 42], rF: [74, 28], hy: 26, hx: 1.5 },
  SB_B: { lE: [42, 54], lF: [33, 40], rE: [66, 38], rF: [76, 20], hy: 26, hx: 2 },
  // jump rope — hands low at the sides
  JR:   { lE: [41, 64], lF: [31, 72], rE: [59, 64], rF: [69, 72], hy: 26, hx: 0 },
  // heavy bag — power hooks into a bag at the right
  HB_G: { lE: [42, 66], lF: [34, 50], rE: [58, 64], rF: [66, 50], hy: 26, hx: 0 },
  HB_H: { lE: [44, 60], lF: [40, 44], rE: [60, 50], rF: [72, 42], hy: 25, hx: -2 },
  // double-end bag — quick taps to the right
  DE_A: { lE: [42, 54], lF: [33, 42], rE: [62, 46], rF: [74, 34], hy: 26, hx: 1 },
  DE_B: { lE: [42, 54], lF: [33, 42], rE: [60, 52], rF: [70, 40], hy: 26, hx: 0 },
  // striking fight — wide stance, full-reach power
  GUARD:   { lE: [40, 70], lF: [32, 52], rE: [60, 70], rF: [68, 52], hy: 26, hx: 0 },
  JAB_L:   { lE: [40, 48], lF: [20, 18], rE: [60, 70], rF: [68, 52], hy: 26, hx: 3 },
  CROSS_R: { lE: [40, 70], lF: [32, 52], rE: [60, 48], rF: [80, 18], hy: 26, hx: -3 },
  HOOK_R:  { lE: [40, 70], lF: [32, 52], rE: [58, 46], rF: [80, 12], hy: 25, hx: -3.5 },
  // muay thai — elbows + clinch (knees/kicks impossible without legs)
  ELBOW_R: { lE: [40, 66], lF: [34, 52], rE: [68, 42], rF: [46, 44], hy: 26, hx: -2 },
  ELBOW_L: { lE: [32, 42], lF: [54, 44], rE: [60, 66], rF: [66, 52], hy: 26, hx: 2 },
  CLINCH:  { lE: [38, 52], lF: [46, 46], rE: [62, 52], rF: [54, 46], hy: 27, hx: 0 },
  // karate — rising knife-hand strike
  KSTRIKE: { lE: [40, 66], lF: [34, 52], rE: [60, 40], rF: [66, 14], hy: 25, hx: -1 },
  // mma / wrestling — level-change takedown shot (head ducks, hands drive low)
  SHOT:    { lE: [42, 72], lF: [34, 86], rE: [58, 72], rF: [66, 86], hy: 32, hx: 0 },
  // grappling — ready stance, hand-fight grip, pull, and a throw sweep
  GSTANCE: { lE: [42, 66], lF: [34, 60], rE: [58, 66], rF: [66, 60], hy: 27, hx: 0 },
  GRIP:    { lE: [44, 64], lF: [40, 74], rE: [56, 64], rF: [60, 74], hy: 27, hx: 0 },
  PULL:    { lE: [40, 56], lF: [44, 48], rE: [60, 56], rF: [56, 48], hy: 26, hx: 0 },
  THROW_A: { lE: [56, 54], lF: [68, 40], rE: [64, 62], rF: [80, 52], hy: 25, hx: 4 },
  THROW_B: { lE: [36, 62], lF: [18, 74], rE: [44, 54], rF: [28, 44], hy: 28, hx: -5 },
  // weigh-in / win
  WEIGHIN: { lE: [40, 72], lF: [28, 84], rE: [60, 72], rF: [72, 84], hy: 28, hx: 0 },
  WIN:     { lE: [34, 88], lF: [14, 46], rE: [66, 88], rF: [86, 46], hy: 34, hx: 0 },
};

const Q = E.easeOutQuad, B = E.easeOutBack, L = E.linear, IC = E.easeInOutCubic;

const GRAPPLE = { bjj: 1, judo: 1, wrestling: 1 };

// shared weigh-in + victory dwells (universal across every combat sport)
const TAIL_WEIGH = [
  { f: 0.43, p: 'WEIGHIN', e: IC },
  { f: 0.575, p: 'WEIGHIN', e: L },   // long weigh-in hold
];
const TAIL_WIN = [
  { f: 0.79, p: 'WIN', e: B },
  { f: 1.00, p: 'WIN', e: L },        // long victory hold
];

// per-sport FIGHT / competition (0.595 → 0.755) — what the arms actually do
const _PUNCH = [
  { f: 0.595, p: 'GUARD', e: B }, { f: 0.62, p: 'JAB_L', e: B }, { f: 0.645, p: 'CROSS_R', e: B },
  { f: 0.67, p: 'JAB_L', e: B }, { f: 0.695, p: 'GUARD', e: Q }, { f: 0.72, p: 'HOOK_R', e: B }, { f: 0.755, p: 'GUARD', e: Q },
];
const FIGHT = {
  boxing: _PUNCH,
  kickboxing: _PUNCH,
  muaythai: [
    { f: 0.595, p: 'GUARD', e: B }, { f: 0.62, p: 'CROSS_R', e: B }, { f: 0.648, p: 'ELBOW_R', e: B },
    { f: 0.68, p: 'CLINCH', e: B }, { f: 0.715, p: 'ELBOW_L', e: B }, { f: 0.755, p: 'GUARD', e: Q },
  ],
  karate: [
    { f: 0.595, p: 'GUARD', e: B }, { f: 0.625, p: 'CROSS_R', e: B }, { f: 0.655, p: 'CROSS_R', e: L },
    { f: 0.68, p: 'GUARD', e: Q }, { f: 0.71, p: 'KSTRIKE', e: B }, { f: 0.738, p: 'KSTRIKE', e: L }, { f: 0.755, p: 'GUARD', e: Q },
  ],
  mma: [
    { f: 0.595, p: 'GUARD', e: B }, { f: 0.62, p: 'JAB_L', e: B }, { f: 0.645, p: 'CROSS_R', e: B },
    { f: 0.68, p: 'SHOT', e: IC }, { f: 0.72, p: 'SHOT', e: L }, { f: 0.755, p: 'GUARD', e: Q },
  ],
  taekwondo: [
    { f: 0.595, p: 'GUARD', e: B }, { f: 0.617, p: 'JAB_L', e: B }, { f: 0.64, p: 'GUARD', e: Q },
    { f: 0.665, p: 'CROSS_R', e: B }, { f: 0.69, p: 'GUARD', e: Q }, { f: 0.715, p: 'JAB_L', e: B }, { f: 0.755, p: 'GUARD', e: Q },
  ],
  wrestling: [
    { f: 0.595, p: 'GSTANCE', e: B }, { f: 0.625, p: 'SHOT', e: IC }, { f: 0.658, p: 'SHOT', e: L },
    { f: 0.688, p: 'THROW_A', e: B }, { f: 0.722, p: 'THROW_B', e: B }, { f: 0.755, p: 'GSTANCE', e: Q },
  ],
  judo: [
    { f: 0.595, p: 'GSTANCE', e: B }, { f: 0.62, p: 'GRIP', e: B }, { f: 0.65, p: 'PULL', e: Q },
    { f: 0.682, p: 'THROW_A', e: B }, { f: 0.718, p: 'THROW_B', e: B }, { f: 0.755, p: 'GSTANCE', e: Q },
  ],
  bjj: [
    { f: 0.595, p: 'GSTANCE', e: B }, { f: 0.625, p: 'GRIP', e: B }, { f: 0.662, p: 'PULL', e: Q },
    { f: 0.698, p: 'THROW_A', e: Q }, { f: 0.73, p: 'THROW_B', e: Q }, { f: 0.755, p: 'GSTANCE', e: Q },
  ],
};

// per-drill training (striking, f 0 → 0.37)
const TRAIN = {
  shadowbox: [
    { f: 0.00, p: 'GUARD_HI', e: L }, { f: 0.06, p: 'GUARD_HI', e: Q },
    { f: 0.105, p: 'TJAB_L', e: Q }, { f: 0.14, p: 'GUARD_HI', e: Q },
    { f: 0.18, p: 'TJAB_R', e: Q }, { f: 0.215, p: 'GUARD_HI', e: Q },
    { f: 0.255, p: 'TJAB_L', e: Q }, { f: 0.29, p: 'GUARD_HI', e: Q },
    { f: 0.33, p: 'TJAB_R', e: Q }, { f: 0.37, p: 'GUARD_HI', e: Q },
  ],
  speedbag: [
    { f: 0.00, p: 'SB_A', e: L }, { f: 0.06, p: 'SB_A', e: Q },
    { f: 0.10, p: 'SB_B', e: Q }, { f: 0.135, p: 'SB_A', e: Q }, { f: 0.17, p: 'SB_B', e: Q },
    { f: 0.205, p: 'SB_A', e: Q }, { f: 0.24, p: 'SB_B', e: Q }, { f: 0.275, p: 'SB_A', e: Q },
    { f: 0.31, p: 'SB_B', e: Q }, { f: 0.345, p: 'SB_A', e: Q }, { f: 0.37, p: 'SB_A', e: Q },
  ],
  jumprope: [
    { f: 0.00, p: 'JR', e: L }, { f: 0.06, p: 'JR', e: L }, { f: 0.37, p: 'JR', e: L },
  ],
  heavybag: [
    { f: 0.00, p: 'HB_G', e: L }, { f: 0.06, p: 'HB_G', e: Q },
    { f: 0.115, p: 'HB_H', e: B }, { f: 0.17, p: 'HB_G', e: Q }, { f: 0.225, p: 'HB_H', e: B },
    { f: 0.28, p: 'HB_G', e: Q }, { f: 0.345, p: 'HB_H', e: B }, { f: 0.37, p: 'HB_G', e: Q },
  ],
  doubleend: [
    { f: 0.00, p: 'DE_B', e: L }, { f: 0.06, p: 'DE_B', e: Q },
    { f: 0.10, p: 'DE_A', e: Q }, { f: 0.135, p: 'DE_B', e: Q }, { f: 0.17, p: 'DE_A', e: Q },
    { f: 0.205, p: 'DE_B', e: Q }, { f: 0.24, p: 'DE_A', e: Q }, { f: 0.275, p: 'DE_B', e: Q },
    { f: 0.31, p: 'DE_A', e: Q }, { f: 0.345, p: 'DE_B', e: Q }, { f: 0.37, p: 'DE_B', e: Q },
  ],
};

// grappling training — uchikomi-style grip drilling (no bag)
const TRAIN_GRAPPLE = [
  { f: 0.00, p: 'GSTANCE', e: L }, { f: 0.06, p: 'GSTANCE', e: Q },
  { f: 0.11, p: 'GRIP', e: Q }, { f: 0.15, p: 'PULL', e: Q }, { f: 0.19, p: 'GSTANCE', e: Q },
  { f: 0.24, p: 'GRIP', e: Q }, { f: 0.28, p: 'PULL', e: Q }, { f: 0.32, p: 'GSTANCE', e: Q }, { f: 0.37, p: 'GSTANCE', e: Q },
];

// karate kata — slow, precise solo forms (no opponent, no apparatus)
TRAIN.kata = [
  { f: 0.00, p: 'GUARD_HI', e: L }, { f: 0.07, p: 'GUARD_HI', e: Q },
  { f: 0.14, p: 'CROSS_R', e: B }, { f: 0.20, p: 'CROSS_R', e: L }, { f: 0.25, p: 'GUARD_HI', e: Q },
  { f: 0.31, p: 'KSTRIKE', e: B }, { f: 0.355, p: 'KSTRIKE', e: L }, { f: 0.37, p: 'GUARD_HI', e: Q },
];
TRAIN.drilling = TRAIN_GRAPPLE;

// which training each sport actually does — no heavy bag where it doesn't belong
const SPORT_DRILLS = {
  boxing:    ['shadowbox', 'heavybag', 'speedbag', 'jumprope', 'doubleend'],
  kickboxing:['shadowbox', 'heavybag', 'jumprope'],
  muaythai:  ['shadowbox', 'heavybag', 'jumprope'],
  mma:       ['shadowbox', 'heavybag'],
  karate:    ['kata'],
  taekwondo: ['shadowbox', 'jumprope'],
  bjj:       ['drilling'],
  judo:      ['drilling'],
  wrestling: ['drilling'],
};
const resolveDrill = (sport, ex) => {
  const allowed = SPORT_DRILLS[sport] || ['shadowbox'];
  return allowed.includes(ex) ? ex : allowed[0];
};

// optional kicks (legs) — only the striking arts that actually kick
const KICKS = {
  taekwondo: [{ t: 0.625, side: 'R', h: 'high' }, { t: 0.69, side: 'L', h: 'high' }, { t: 0.74, side: 'R', h: 'mid' }],
  kickboxing:[{ t: 0.70, side: 'R', h: 'high' }],
  muaythai:  [{ t: 0.705, side: 'R', h: 'mid' }],
  karate:    [{ t: 0.66, side: 'R', h: 'mid' }],
};
// when legs carry the fight (taekwondo), the hands just hold guard
const FIGHT_KICKGUARD = [
  { f: 0.595, p: 'GUARD', e: B }, { f: 0.66, p: 'GUARD', e: L }, { f: 0.72, p: 'GUARD', e: L }, { f: 0.755, p: 'GUARD', e: Q },
];

// fight impacts per sport — strikes spark; throws land a slam ring on the mat
const FIGHT_IMPACTS = {
  boxing:    [{ f: 0.62, s: 'lF' }, { f: 0.645, s: 'rF' }, { f: 0.67, s: 'lF' }, { f: 0.72, s: 'rF', big: 1 }],
  kickboxing:[{ f: 0.62, s: 'lF' }, { f: 0.645, s: 'rF' }, { f: 0.67, s: 'lF' }, { f: 0.72, s: 'rF', big: 1 }],
  muaythai:  [{ f: 0.62, s: 'rF' }, { f: 0.648, s: 'rE' }, { f: 0.715, s: 'lE' }],
  karate:    [{ f: 0.625, s: 'rF' }, { f: 0.71, s: 'rF', big: 1 }],
  mma:       [{ f: 0.62, s: 'lF' }, { f: 0.645, s: 'rF' }, { f: 0.71, slam: 1 }],
  taekwondo: [{ f: 0.617, s: 'lF', sm: 1 }, { f: 0.665, s: 'rF', sm: 1 }, { f: 0.715, s: 'lF', sm: 1 }],
  wrestling: [{ f: 0.724, slam: 1, big: 1 }],
  judo:      [{ f: 0.72, slam: 1, big: 1 }],
  bjj:       [{ f: 0.732, slam: 1 }],
};
const TRAIN_IMPACTS = {
  shadowbox: [{ f: 0.105, s: 'lF', sm: 1 }, { f: 0.18, s: 'rF', sm: 1 }, { f: 0.255, s: 'lF', sm: 1 }, { f: 0.33, s: 'rF', sm: 1 }],
  speedbag:  [{ f: 0.10, s: 'rF', sm: 1 }, { f: 0.17, s: 'rF', sm: 1 }, { f: 0.24, s: 'rF', sm: 1 }, { f: 0.31, s: 'rF', sm: 1 }],
  jumprope:  [],
  heavybag:  [{ f: 0.115, s: 'rF' }, { f: 0.225, s: 'rF' }, { f: 0.345, s: 'rF' }],
  doubleend: [{ f: 0.10, s: 'rF', sm: 1 }, { f: 0.17, s: 'rF', sm: 1 }, { f: 0.24, s: 'rF', sm: 1 }, { f: 0.31, s: 'rF', sm: 1 }],
};

TRAIN_IMPACTS.kata = [{ f: 0.14, s: 'rF', sm: 1 }, { f: 0.31, s: 'rF', sm: 1 }];
TRAIN_IMPACTS.drilling = [];

const buildKF = (sport, ex, legsOn) => {
  const train = TRAIN[resolveDrill(sport, ex)] || TRAIN.shadowbox;
  let fight = FIGHT[sport] || FIGHT.boxing;
  if (legsOn && sport === 'taekwondo') fight = FIGHT_KICKGUARD;
  return train.concat(TAIL_WEIGH, fight, TAIL_WIN);
};
const buildImpacts = (sport, ex, legsOn) => {
  const tImp = TRAIN_IMPACTS[resolveDrill(sport, ex)] || [];
  let fImp = FIGHT_IMPACTS[sport] || FIGHT_IMPACTS.boxing;
  if (legsOn && sport === 'taekwondo') fImp = [];
  return tImp.concat(fImp);
};

const lerp = (a, b, t) => a + (b - a) * t;
const lerpPt = (a, b, t) => [lerp(a[0], b[0], t), lerp(a[1], b[1], t)];

// emphasis bump: ramp a→b up to amp, hold b→c, ease c→d back down
function bump(f, a, b, c, d, amp) {
  if (f < a || f > d) return 0;
  if (f < b) return amp * E.easeOutCubic((f - a) / (b - a));
  if (f <= c) return amp;
  return amp * (1 - E.easeInOutCubic((f - c) / (d - c)));
}

function evalPose(f, KF) {
  f = Math.max(0, Math.min(1, f));
  let i = 0;
  for (let k = 0; k < KF.length - 1; k++) { if (f >= KF[k].f && f < KF[k + 1].f) { i = k; break; } if (f >= KF[KF.length - 1].f) i = KF.length - 2; }
  const a = POSE[KF[i].p], b = POSE[KF[i + 1].p];
  const span = KF[i + 1].f - KF[i].f || 1;
  const te = KF[i + 1].e(Math.max(0, Math.min(1, (f - KF[i].f) / span)));
  return {
    lE: lerpPt(a.lE, b.lE, te), lF: lerpPt(a.lF, b.lF, te),
    rE: lerpPt(a.rE, b.rE, te), rF: lerpPt(a.rF, b.rF, te),
    hy: lerp(a.hy, b.hy, te), hx: lerp(a.hx, b.hx, te),
  };
}

// ── per-sport gear (mirrors fc-logo-sports.jsx: swap fists + head/body accents) ─
const SPORT = {
  boxing:    { fist: 'solid', head: null,        body: null },
  mma:       { fist: 'open',  head: null,        body: null },
  muaythai:  { fist: 'wrap',  head: 'mongkol',   body: 'armbands' },
  kickboxing:{ fist: 'solid', head: 'headguard', body: null },
  bjj:       { fist: 'bare',  head: null,        body: 'gi-knot' },
  judo:      { fist: 'bare',  head: null,        body: 'gi-wide' },
  karate:    { fist: 'knife', head: null,        body: 'gi-knot' },
  taekwondo: { fist: 'bare',  head: 'headguard', body: null },
  wrestling: { fist: 'bare',  head: 'earguards', body: null },
};

// a fist drawn at a live (moving) point; `ang` = forearm direction for the knife blade
function Fist({ p, ang, type, ink, accent }) {
  if (type === 'open')  return <circle cx={p[0]} cy={p[1]} r="8.5" fill="none" stroke={accent} strokeWidth="6" />;
  if (type === 'wrap')  return <circle cx={p[0]} cy={p[1]} r="6.5" fill="none" stroke={accent} strokeWidth="4.5" />;
  if (type === 'bare')  return <circle cx={p[0]} cy={p[1]} r="7.5" fill={ink} />;
  if (type === 'knife') {
    const dx = Math.cos(ang) * 9, dy = Math.sin(ang) * 9;
    return <line x1={p[0] - dx} y1={p[1] - dy} x2={p[0] + dx} y2={p[1] + dy} stroke={ink} strokeWidth="8" strokeLinecap="round" />;
  }
  return <circle cx={p[0]} cy={p[1]} r="9.5" fill={accent} />; // solid (boxing)
}

// head add-ons, anchored to the live head centre (cx,cy) inside the head's scale group
function HeadGear({ type, cx, cy, accent }) {
  const r = 15;
  if (type === 'mongkol') return (
    <g>
      <path d={`M ${cx - r + 1} ${cy - r * 0.5} Q ${cx} ${cy - r - 12} ${cx + r - 1} ${cy - r * 0.5}`} fill="none" stroke={accent} strokeWidth="5" strokeLinecap="round" />
      <circle cx={cx + r - 1} cy={cy - r * 0.5} r="2.6" fill={accent} />
    </g>
  );
  if (type === 'headguard') { const R = r + 4; return <path d={`M ${cx - R} ${cy + 2} A ${R} ${R} 0 1 1 ${cx + R} ${cy + 2}`} fill="none" stroke={accent} strokeWidth="5" strokeLinecap="round" />; }
  if (type === 'earguards') return (<g><circle cx={cx - r} cy={cy} r="3.4" fill={accent} /><circle cx={cx + r} cy={cy} r="3.4" fill={accent} /></g>);
  return null;
}

// gi lapel in the torso gap (anchored near the W peak, which doesn't move)
function BodyGear({ type, accent }) {
  if (!type || !type.startsWith('gi')) return null;
  const wide = type === 'gi-wide', knot = type === 'gi-knot', x = wide ? 9 : 6;
  return (
    <g>
      <line x1="50" y1="49.5" x2={50 - x} y2="59" stroke={accent} strokeWidth="4.5" strokeLinecap="round" />
      <line x1="50" y1="49.5" x2={50 + x} y2="59" stroke={accent} strokeWidth="4.5" strokeLinecap="round" />
      {knot && <circle cx="50" cy="50" r="2.8" fill={accent} />}
    </g>
  );
}

// prajioud armband on an upper arm (C → elbow segment), perpendicular tick
function Armband({ e, accent }) {
  const t = 0.58, x = C[0] + (e[0] - C[0]) * t, y = C[1] + (e[1] - C[1]) * t;
  const ax = e[0] - C[0], ay = e[1] - C[1], len = Math.hypot(ax, ay) || 1;
  const px = -ay / len * 5, py = ax / len * 5;
  return <line x1={x - px} y1={y - py} x2={x + px} y2={y + py} stroke={accent} strokeWidth="4" strokeLinecap="round" />;
}

// ── legs / kicks (optional, flat style — only the arts that kick) ─────────────
function legGeom(f, sport) {
  const hip = [50, 61];
  let lK = [45, 80], lFt = [43, 99], rK = [55, 80], rFt = [57, 99];
  for (const k of (KICKS[sport] || [])) {
    const d = Math.abs(f - k.t);
    if (d < 0.035) {
      const prog = E.easeOutCubic(1 - d / 0.035);
      const fy = k.h === 'mid' ? 60 : 44;
      if (k.side === 'R') { rK = lerpPt([55, 80], [60, 66], prog); rFt = lerpPt([57, 99], [94, fy], prog); }
      else { lK = lerpPt([45, 80], [40, 66], prog); lFt = lerpPt([43, 99], [6, fy], prog); }
    }
  }
  return { hip, lK, lFt, rK, rFt };
}
function Legs({ g, line, alpha }) {
  const lin = { fill: 'none', stroke: line, strokeWidth: 9, strokeLinecap: 'round', strokeLinejoin: 'round' };
  return (
    <g opacity={alpha}>
      <path d={`M ${g.hip[0]} ${g.hip[1]} L ${g.lK[0]} ${g.lK[1]} L ${g.lFt[0]} ${g.lFt[1]}`} {...lin} />
      <path d={`M ${g.hip[0]} ${g.hip[1]} L ${g.rK[0]} ${g.rK[1]} L ${g.rFt[0]} ${g.rFt[1]}`} {...lin} />
    </g>
  );
}

// ── the fighter mark ─────────────────────────────────────────────────────────
function Fighter({ pose, line, accent, alpha, yShift, headScale, winRing, sport }) {
  const cfg = SPORT[sport] || SPORT.boxing;
  const lin = { fill: 'none', stroke: line, strokeWidth: 9, strokeLinecap: 'round', strokeLinejoin: 'round' };
  const arm = (Ept, Fpt) => `M ${C[0]} ${C[1]} L ${Ept[0]} ${Ept[1]} L ${Fpt[0]} ${Fpt[1]}`;
  const hcx = 50 + pose.hx, hcy = pose.hy;
  const angL = Math.atan2(pose.lF[1] - pose.lE[1], pose.lF[0] - pose.lE[0]);
  const angR = Math.atan2(pose.rF[1] - pose.rE[1], pose.rF[0] - pose.rE[0]);
  return (
    <g opacity={alpha} transform={`translate(0 ${yShift})`}>
      <path d={arm(pose.lE, pose.lF)} {...lin} />
      <path d={arm(pose.rE, pose.rF)} {...lin} />
      {cfg.body === 'armbands' && <><Armband e={pose.lE} accent={accent} /><Armband e={pose.rE} accent={accent} /></>}
      <BodyGear type={cfg.body} accent={accent} />
      <g transform={`translate(${hcx} ${hcy}) scale(${headScale}) translate(${-hcx} ${-hcy})`}>
        <circle cx={hcx} cy={hcy} r="15" fill="none" stroke={line} strokeWidth="9" />
        <HeadGear type={cfg.head} cx={hcx} cy={hcy} accent={accent} />
      </g>
      <Fist p={pose.lF} ang={angL} type={cfg.fist} ink={line} accent={accent} />
      <Fist p={pose.rF} ang={angR} type={cfg.fist} ink={line} accent={accent} />
      {winRing > 0.001 && (
        <circle cx={hcx} cy={hcy} r={15 + winRing * 16} fill="none" stroke={accent} strokeWidth={3} opacity={(1 - winRing) * 0.9} />
      )}
    </g>
  );
}

// ── training apparatus, drawn only during the training beat ───────────────────
function TrainingRig({ f, ex, accent, line, pose, yShift }) {
  const op = Math.min(1, Math.max(0, (f - 0.03) / 0.03), Math.max(0, (0.40 - f) / 0.03));
  if (op <= 0.001 || f > 0.41) return null;

  if (ex === 'speedbag') {
    const jx = Math.sin(f * 200) * 1.2, jy = Math.abs(Math.sin(f * 200)) * 1.6;
    const bx = 76 + jx, by = 11 + jy;
    return (
      <g opacity={op} style={{ pointerEvents: 'none' }}>
        <line x1="60" y1="3" x2="92" y2="3" stroke={line} strokeWidth="7" strokeLinecap="round" />
        <line x1={bx} y1="6" x2={bx} y2={by - 5} stroke={line} strokeWidth="3" strokeLinecap="round" />
        <circle cx={bx} cy={by} r="6.5" fill={line} />
      </g>
    );
  }

  if (ex === 'doubleend') {
    const taps = [0.10, 0.17, 0.24, 0.31];
    let dx = 0; for (const t of taps) { if (f >= t) dx = 5.5 * Math.sin((f - t) / 0.05 * Math.PI) * Math.exp(-(f - t) / 0.05); }
    const bx = 80 + dx, by = 34 + dx * 0.5;
    return (
      <g opacity={op} style={{ pointerEvents: 'none' }}>
        <line x1="80" y1="4" x2={bx} y2={by - 6} stroke={line} strokeWidth="3" strokeLinecap="round" />
        <line x1="80" y1="64" x2={bx} y2={by + 6} stroke={line} strokeWidth="3" strokeLinecap="round" />
        <circle cx={bx} cy={by} r="6.5" fill={line} />
      </g>
    );
  }

  if (ex === 'heavybag') {
    const hits = [0.115, 0.225, 0.345];
    let ang = 0; for (const t of hits) { if (f >= t) ang = 7 * Math.sin((f - t) / 0.06 * Math.PI) * Math.exp(-(f - t) / 0.07); }
    return (
      <g opacity={op} style={{ pointerEvents: 'none' }} transform={`rotate(${ang} 89 5)`}>
        <line x1="89" y1="4" x2="89" y2="22" stroke={line} strokeWidth="5" strokeLinecap="round" />
        <line x1="89" y1="24" x2="89" y2="62" stroke={line} strokeWidth="23" strokeLinecap="round" />
      </g>
    );
  }

  if (ex === 'jumprope') {
    const ph = (f - 0.06) / 0.31 * Math.PI * 2 * 3;
    const cy = 56 - 72 * Math.cos(ph);                 // control point swings over → under
    const lf = pose.lF, rf = pose.rF;
    const y0 = yShift || 0;
    return (
      <g opacity={op} style={{ pointerEvents: 'none' }} transform={`translate(0 ${y0})`}>
        <path d={`M ${lf[0]} ${lf[1]} Q 50 ${cy} ${rf[0]} ${rf[1]}`} fill="none" stroke={line} strokeWidth="4" strokeLinecap="round" />
      </g>
    );
  }
  return null;
}

// punch impact spark at a fist
function Spark({ x, y, inten, big, sm, accent, style = 'sparks' }) {
  if (inten <= 0.001) return null;
  if (style === 'rings') {
    const rr = (big ? 5 : sm ? 3 : 4) + (1 - inten) * (big ? 11 : sm ? 5 : 7);
    return <circle cx={x} cy={y} r={rr} fill="none" stroke={accent} strokeWidth={big ? 3.4 : sm ? 2.2 : 2.8} opacity={inten} style={{ pointerEvents: 'none' }} />;
  }
  const n = big ? 9 : (sm ? 5 : 6);
  const r1 = big ? 11 : (sm ? 4 : 7), r2 = (big ? 24 : (sm ? 9 : 15)) * (0.5 + 0.5 * inten);
  const sw = big ? 2.6 : (sm ? 1.3 : 1.8);
  const lines = [];
  for (let k = 0; k < n; k++) {
    const ang = (k / n) * Math.PI * 2 + (big ? 0.2 : 0.5);
    lines.push([x + Math.cos(ang) * r1, y + Math.sin(ang) * r1, x + Math.cos(ang) * r2, y + Math.sin(ang) * r2]);
  }
  return (
    <g opacity={inten} style={{ pointerEvents: 'none' }}>
      {lines.map((l, k) => <line key={k} x1={l[0]} y1={l[1]} x2={l[2]} y2={l[3]} stroke={accent} strokeWidth={sw} strokeLinecap="round" />)}
      {big && <circle cx={x} cy={y} r={6 + (1 - inten) * 10} fill="none" stroke={accent} strokeWidth="2.4" opacity={inten * 0.8} />}
    </g>
  );
}

// win celebration: red rays — or expanding target rings — from the head ring
function WinBurst({ f, hx, hy, accent, style = 'sparks' }) {
  const t = (f - 0.79) / 0.14;
  if (t < 0 || t > 1) return null;
  const inten = t < 0.5 ? t / 0.5 : 1 - (t - 0.5) / 0.5;
  const cx = 50 + hx, cy = hy;
  if (style === 'rings') {
    const rings = [];
    for (let k = 0; k < 2; k++) { const rp = t * 1.4 - k * 0.34; if (rp > 0 && rp < 1) rings.push({ r: 21 + rp * 32, op: 1 - rp * 0.85 }); }
    return (
      <g style={{ pointerEvents: 'none' }}>
        {rings.map((rg, k) => <circle key={k} cx={cx} cy={cy} r={rg.r} fill="none" stroke={accent} strokeWidth="4" opacity={rg.op} />)}
      </g>
    );
  }
  const rays = [];
  const N = 12;
  for (let k = 0; k < N; k++) {
    const ang = (k / N) * Math.PI * 2 - Math.PI / 2;
    const r1 = 23 + t * 12, r2 = 32 + t * 34;
    rays.push([cx + Math.cos(ang) * r1, cy + Math.sin(ang) * r1, cx + Math.cos(ang) * r2, cy + Math.sin(ang) * r2]);
  }
  return (
    <g opacity={inten} style={{ pointerEvents: 'none' }}>
      {rays.map((l, k) => <line key={k} x1={l[0]} y1={l[1]} x2={l[2]} y2={l[3]} stroke={accent} strokeWidth="2.8" strokeLinecap="round" />)}
    </g>
  );
}

// ── the scene ────────────────────────────────────────────────────────────────
function Scene({ dur = 8.4, line = '#16140f', accent = '#b23218', sub = 'cream', exercise = 'shadowbox', burst = 'sparks', sport = 'boxing', legs = false, time = null, svgW = 540, svgH = 583 }) {
  const ctxTime = useTimeSafe();
  const t0 = time != null ? time : ctxTime;
  const f = dur > 0 ? Math.max(0, Math.min(1, t0 / dur)) : 0;

  const drill = resolveDrill(sport, exercise);
  const legsOn = legs && !!KICKS[sport];
  const KF = React.useMemo(() => buildKF(sport, exercise, legsOn), [sport, exercise, legsOn]);
  const IMPACTS = React.useMemo(() => buildImpacts(sport, exercise, legsOn), [sport, exercise, legsOn]);
  const pose = evalPose(f, KF);
  const isGrap = !!GRAPPLE[sport];

  const intro = Math.max(0, Math.min(1, f / 0.06));
  const outro = Math.max(0, Math.min(1, (1.0 - f) / 0.04));
  const alpha = Math.min(intro, outro);

  // life: jump rope bounces; other drills bob gently; the fight bobs harder
  let yShift = 0;
  const inTrain = f > 0.04 && f < 0.40;
  if (inTrain) {
    if (drill === 'jumprope') { const ph = (f - 0.06) / 0.31 * Math.PI * 2 * 3; yShift = -2.8 * Math.abs(Math.sin(ph)); }
    else yShift = 1.0 * Math.sin(f * 210);
  } else if (f > 0.60 && f < 0.755) yShift = 1.7 * Math.sin(f * 300);

  // head pulse + ring on the win raise (bigger now)
  let headScale = 1, winRing = 0;
  if (f >= 0.79 && f <= 0.90) { const p = (f - 0.79) / 0.11; headScale = 1 + 0.18 * Math.sin(p * Math.PI); winRing = Math.min(1, p / 0.9); }
  else if (f > 0.90) headScale = 1 + 0.025 * Math.sin(f * 150);

  // active impact: strike spark at a fist/elbow, or a slam ring on the mat for throws
  let spark = null;
  for (const imp of IMPACTS) {
    const win = imp.big ? 0.05 : (imp.sm ? 0.022 : 0.03);
    if (f >= imp.f && f - imp.f < win) {
      const pt = imp.slam ? [50, 92] : pose[imp.s];
      spark = { x: pt[0], y: pt[1], inten: 1 - (f - imp.f) / win, big: !!imp.big, sm: !!imp.sm };
    }
  }

  // legs / kicks (only the kicking arts, only during the fight beat)
  const legsAlpha = legsOn ? Math.min(1, Math.max(0, (f - 0.575) / 0.02), Math.max(0, (0.775 - f) / 0.02)) : 0;
  const lg = legsAlpha > 0.001 ? legGeom(f, sport) : null;
  if (legsOn && lg) {
    for (const k of (KICKS[sport] || [])) {
      if (Math.abs(f - k.t) < 0.014) { const ft = k.side === 'R' ? lg.rFt : lg.lFt; spark = { x: ft[0], y: ft[1], inten: 1 - Math.abs(f - k.t) / 0.014, big: 1 }; }
    }
  }

  // weigh-in scale — a platform line the figure stands on + a pointer that settles
  const wStart = 0.43, wEnd = 0.575;
  const showW = f >= wStart && f <= wEnd;
  let wOp = 0, tickX = 33, wOk = false, lockProg = 0;
  if (showW) {
    wOp = Math.min(1, (f - wStart) / 0.02, (wEnd - f) / 0.02);
    const cs = 0.47, ce = 0.51;
    if (f < cs) tickX = 33;
    else if (f < ce) { const p = (f - cs) / (ce - cs); tickX = 33 + 17 * E.easeOutCubic(p); }
    else { tickX = 50; wOk = true; lockProg = Math.min(1, (f - ce) / 0.05); }
  }

  // emphasis zoom: weigh-in lock and the victory are the two big moments
  const moment = 1 + bump(f, 0.475, 0.515, 0.575, 0.595, 0.12) + bump(f, 0.79, 0.865, 1.0, 1.0, 0.17);
  const mt = `translate(50 50) scale(${moment}) translate(-50 -50)`;

  return (
    <div style={{ position: 'absolute', inset: 0 }}>
      <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
        <svg viewBox="0 0 100 108" width={svgW} height={svgH} fill="none" style={{ display: 'block', overflow: 'visible' }}>
          <g transform={mt}>
            {!isGrap && <TrainingRig f={f} ex={drill} accent={accent} line={line} pose={pose} yShift={yShift} />}
            {lg && <Legs g={lg} line={line} alpha={legsAlpha} />}
            <Fighter pose={pose} line={line} accent={accent} alpha={alpha} yShift={yShift} headScale={headScale} winRing={winRing} sport={sport} />
            {spark && <Spark {...spark} accent={accent} style={burst} />}
            <WinBurst f={f} hx={pose.hx} hy={pose.hy} accent={accent} style={burst} />
            {showW && (
              <g opacity={wOp}>
                <line x1="22" y1="103" x2="78" y2="103" stroke={wOk ? accent : line} strokeWidth="6" strokeLinecap="round" />
                <line x1={tickX} y1="97" x2={tickX} y2="109" stroke={wOk ? accent : line} strokeWidth="5" strokeLinecap="round" />
                {wOk && lockProg < 1 && (
                  <circle cx={tickX} cy="103" r={7 + lockProg * 22} fill="none" stroke={accent} strokeWidth="3" opacity={1 - lockProg} />
                )}
              </g>
            )}
          </g>
        </svg>
      </div>
    </div>
  );
}

window.OWScene = Scene;
window.OW_SPORT_DRILLS = SPORT_DRILLS;
window.OW_KICKS = KICKS;

// FC sport label ("Muay Thai") → animation key ("muaythai")
const SPORT_KEY = { Boxing: 'boxing', MMA: 'mma', 'Muay Thai': 'muaythai', Kickboxing: 'kickboxing', BJJ: 'bjj', Judo: 'judo', Karate: 'karate', Taekwondo: 'taekwondo', Wrestling: 'wrestling' };
window.OW_sportKey = (s) => SPORT_KEY[s] || (typeof s === 'string' ? s.toLowerCase().replace(/[^a-z]/g, '') : 'boxing') || 'boxing';

// Drop-in looping mark for loading screens & moments of joy. Self-clocked.
function OWMark({ sport = 'boxing', size = 180, dark = true, accent = '#a82a18', once = false, clip = null }) {
  const CLIPS = { weighin: [0.435, 0.565, 1.7] };   // [f0, f1, seconds] — just the scale settling on weight
  const [t, setT] = React.useState(0);
  React.useEffect(() => {
    let raf, start = null; const dur = 8.4;
    const cl = clip && CLIPS[clip];
    const step = (ts) => {
      if (start == null) start = ts;
      const s = (ts - start) / 1000;
      let el;
      if (cl) { const p = Math.min(1, s / cl[2]); el = (cl[0] + (cl[1] - cl[0]) * p) * dur; setT(el); if (p >= 1) return; }
      else if (once) { el = Math.min(s, dur - 0.001); setT(el); if (el >= dur - 0.01) return; }
      else { setT(s % dur); }
      raf = requestAnimationFrame(step);
    };
    raf = requestAnimationFrame(step);
    return () => raf && cancelAnimationFrame(raf);
  }, [once, clip]);
  const line = dark ? '#f4efe6' : '#16140f';
  const key = window.OW_sportKey(sport);
  return (
    <div style={{ position: 'relative', width: size, height: size, overflow: 'visible' }}>
      <Scene time={t} dur={8.4} line={line} accent={accent} sub={dark ? 'cream' : 'ink'} sport={key} burst="rings" legs={!!KICKS[key]} svgW={size} svgH={size} />
    </div>
  );
}
window.OWMark = OWMark;
})();
