// motion-ch.jsx — Track 07: Motion & the Real World
const { useState: uM, useRef: rM, useEffect: eM } = React;

// ════════════════════════════════════════════════════════════════
// CH1 — The axis & MC_Power
// ════════════════════════════════════════════════════════════════
defineChapter({
  id: "axis", act: "PLCopen · Axis", num: 1, emoji: "🔌", navTitle: "The axis & MC_Power",
  title: "The axis: a motor you talk to by name",
  sub: "In PLCopen, a real motor + its encoder is wrapped in an AXIS_REF. Before it will move a millimetre you must enable it with MC_Power. No power, no motion — it just sits in Disabled.",
  Body: function AxisBody() {
    const [enable, setEnable] = uM(false);
    const [regulator, setReg] = uM(true);
    const powered = enable && regulator;
    const status = !enable ? "Disabled" : !regulator ? "ErrorStop" : "Standstill";
    const color = !enable ? "#9aa" : !regulator ? "var(--coral)" : "var(--mint)";

    return (
      <div className="ch-body">
        <div className="stage">
          <div className="row" style={{ justifyContent: "center", gap: 30, alignItems: "center" }}>
            <div style={{ textAlign: "center" }}>
              <div style={{ width: 130, height: 130, borderRadius: "50%", border: "6px solid var(--line)", background: "#fff", boxShadow: "5px 5px 0 var(--line)", display: "flex", alignItems: "center", justifyContent: "center", position: "relative" }}>
                <div style={{ width: 70, height: 70, borderRadius: "50%", border: "5px dashed var(--line)", background: powered ? "#D6F6EC" : "#F1F1F4", animation: powered ? "spin 1.6s linear infinite" : "none" }} />
                <div style={{ position: "absolute", fontSize: 30 }}>{powered ? "⚙️" : "🛑"}</div>
              </div>
              <div className="tag" style={{ marginTop: 12, background: color, color: status === "Disabled" ? "var(--ink)" : "#fff" }}>Axis.Status = {status}</div>
            </div>

            <div className="stack" style={{ gap: 12, minWidth: 240 }}>
              <button className="btn" style={{ background: enable ? "var(--mint)" : "#fff" }} onClick={() => setEnable(v => !v)}>MC_Power.Enable := {enable ? "TRUE" : "FALSE"}</button>
              <button className="btn ghost" onClick={() => setReg(v => !v)}>bRegulatorOn := {regulator ? "TRUE" : "FALSE"}</button>
              <div className="readout">
                <span className="lbl">.bEnabled  </span> = <span style={{ color: powered ? "#7DEAC4" : "#FF8FA8", fontWeight: 700 }}>{powered ? "TRUE" : "FALSE"}</span>{"\n"}
                <span className="lbl">.ActPos    </span> = <span className="val">0.0</span> mm{"\n"}
                <span className="lbl">.ActVelocity</span> = <span className="val">0.0</span>
              </div>
            </div>
          </div>
          <Bubble style={{ marginTop: 16 }}>
            {powered ? <>Powered and idle — status is <b>Standstill</b>, the green light to command a move. The shaft holds position under control.</>
              : !enable ? <>Disabled. <code>MC_Power.Enable</code> is FALSE, so the drive is off and any move command is ignored.</>
                : <>Enabled but the regulator is off — the axis can't hold torque. Flip <code>bRegulatorOn</code> back to TRUE.</>}
          </Bubble>
        </div>

        <pre className="code">
<span className="kw">VAR</span>{"\n"}
  <span className="var">Axis</span>     : <span className="typ">AXIS_REF</span>;   <span className="com">{`// the motor + encoder, by reference`}</span>{"\n"}
  <span className="var">fbPower</span>  : <span className="typ">MC_Power</span>;{"\n"}
<span className="kw">END_VAR</span>{"\n\n"}
<span className="var">fbPower</span>(<span className="var">Axis</span> := <span className="var">Axis</span>, <span className="var">Enable</span> := <span className="var">bDriveOn</span>, <span className="var">bRegulatorOn</span> := <span className="kw">TRUE</span>, <span className="var">bDriveStart</span> := <span className="kw">TRUE</span>);{"\n"}
<span className="var">bAxisReady</span> := <span className="var">fbPower</span>.<span className="var">Status</span>;   <span className="com">{`// TRUE once Standstill`}</span>
        </pre>

        <div className="grid2">
          <Callout kind="key">Every motion FB takes the same <code>Axis : AXIS_REF</code> as its first input — that's how they all talk to the same physical drive. Call <code>MC_Power</code> every scan to keep it enabled.</Callout>
          <Callout kind="tip">Axis states are a fixed PLCopen set: <code>Disabled → Standstill → Moving → …</code>. A move is only legal from <b>Standstill</b> (or while already moving).</Callout>
        </div>

        <Check q="You send MC_MoveAbsolute but the axis doesn't budge. The most common first thing to check?" correct={1}
          options={["The PLC is broken", "Is the axis enabled? MC_Power.Status must be TRUE (Standstill) before any move runs", "Increase the velocity to maximum", "Re-download the program"]}
          explain="No power, no motion. If MC_Power isn't enabled and showing Standstill, the move FB has nothing to command. It's the number-one motion gotcha." />
      </div>
    );
  },
});

// ════════════════════════════════════════════════════════════════
// CH2 — Absolute vs relative moves
// ════════════════════════════════════════════════════════════════
defineChapter({
  id: "moves", act: "PLCopen · Moves", num: 2, emoji: "🎯", navTitle: "Absolute vs relative",
  title: "Moves: go TO a spot, or go BY a step",
  sub: "MC_MoveAbsolute sends the axis to a fixed coordinate — position 350, no matter where it started. MC_MoveRelative shifts it BY an amount from wherever it is now. Same axis, two very different mental models.",
  Body: function MovesBody() {
    const RAIL = 500;
    const [pos, setPos] = uM(120);
    const posRef = rM(120);
    const [busy, setBusy] = uM(false);
    const [done, setDone] = uM(true);
    const [vel, setVel] = uM(220);
    const [target, setTarget] = uM(350);
    const raf = rM(null);
    eM(() => () => cancelAnimationFrame(raf.current), []);

    const animateTo = (tgt) => {
      tgt = Math.max(0, Math.min(RAIL, tgt));
      cancelAnimationFrame(raf.current);
      const from = posRef.current;
      const dist = tgt - from;
      if (Math.abs(dist) < 0.5) { setDone(true); return; }
      const dur = Math.max(250, Math.abs(dist) / vel * 1000);
      const t0 = performance.now();
      setBusy(true); setDone(false);
      const tick = (now) => {
        const k = Math.min(1, (now - t0) / dur);
        const e = k < 0.5 ? 2 * k * k : 1 - Math.pow(-2 * k + 2, 2) / 2;
        const cur = from + dist * e;
        posRef.current = cur; setPos(cur);
        if (k < 1) raf.current = requestAnimationFrame(tick);
        else { posRef.current = tgt; setPos(tgt); setBusy(false); setDone(true); }
      };
      raf.current = requestAnimationFrame(tick);
    };

    const pctPos = (pos / RAIL) * 100;
    const pctTgt = (target / RAIL) * 100;

    return (
      <div className="ch-body">
        <div className="stage">
          <div style={{ position: "relative", height: 92, margin: "8px 6px 0" }}>
            {/* target ghost */}
            <div style={{ position: "absolute", left: `${pctTgt}%`, top: 0, bottom: 28, width: 0, borderLeft: "3px dashed var(--coral)", transform: "translateX(-1px)" }} />
            <div style={{ position: "absolute", left: `${pctTgt}%`, top: -2, transform: "translateX(-50%)", fontSize: 11, fontWeight: 700, fontFamily: '"JetBrains Mono",monospace', color: "var(--coral-deep)" }}>target {Math.round(target)}</div>
            {/* rail */}
            <div style={{ position: "absolute", left: 0, right: 0, bottom: 28, height: 14, background: "repeating-linear-gradient(90deg,#fff 0 9px,rgba(27,27,47,.12) 9px 10px)", border: "3px solid var(--line)", borderRadius: 8 }} />
            {/* carriage */}
            <div style={{ position: "absolute", left: `${pctPos}%`, bottom: 22, transform: "translateX(-50%)", transition: "none" }}>
              <div style={{ width: 46, height: 34, background: busy ? "var(--yellow)" : "var(--mint)", border: "3px solid var(--line)", borderRadius: 8, boxShadow: "3px 3px 0 var(--line)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 18 }}>🔩</div>
            </div>
            {/* scale ends */}
            <div style={{ position: "absolute", left: 0, bottom: 4, fontSize: 11, fontFamily: '"JetBrains Mono",monospace', color: "var(--ink-soft)", fontWeight: 700 }}>0</div>
            <div style={{ position: "absolute", right: 0, bottom: 4, fontSize: 11, fontFamily: '"JetBrains Mono",monospace', color: "var(--ink-soft)", fontWeight: 700 }}>{RAIL}</div>
          </div>

          <div className="readout" style={{ marginTop: 6 }}>
            <span className="lbl">Axis.ActPos</span> = <span className="val">{pos.toFixed(1)}</span> mm   ·   <span className="lbl">Busy</span> = <span style={{ color: busy ? "#FFD43B" : "#7B7B95", fontWeight: 700 }}>{busy ? "TRUE" : "FALSE"}</span>   ·   <span className="lbl">Done</span> = <span style={{ color: done ? "#7DEAC4" : "#7B7B95", fontWeight: 700 }}>{done ? "TRUE" : "FALSE"}</span>
          </div>

          <div style={{ marginTop: 16 }}>
            <div className="row" style={{ justifyContent: "space-between" }}>
              <span className="tag">Target position</span>
              <span className="tag" style={{ background: "var(--coral)", color: "#fff" }}>{Math.round(target)} mm</span>
            </div>
            <input type="range" min={0} max={RAIL} value={target} onChange={e => setTarget(parseInt(e.target.value, 10))} style={{ width: "100%", accentColor: "var(--coral)" }} />
            <div className="row" style={{ justifyContent: "space-between", marginTop: 8 }}>
              <span className="tag">Velocity</span>
              <span className="tag" style={{ background: "var(--yellow)" }}>{vel} mm/s</span>
            </div>
            <input type="range" min={60} max={500} step={10} value={vel} onChange={e => setVel(parseInt(e.target.value, 10))} style={{ width: "100%", accentColor: "var(--mint)" }} />
          </div>

          <div className="row" style={{ justifyContent: "center", marginTop: 16, gap: 10, flexWrap: "wrap" }}>
            <button className="btn coral sm" onClick={() => animateTo(target)}>🎯 MC_MoveAbsolute → {Math.round(target)}</button>
            <button className="btn sm" style={{ background: "var(--blue)", color: "#fff" }} onClick={() => animateTo(posRef.current + 80)}>MC_MoveRelative +80</button>
            <button className="btn sm" style={{ background: "var(--blue)", color: "#fff" }} onClick={() => animateTo(posRef.current - 80)}>MC_MoveRelative −80</button>
          </div>

          <Bubble style={{ marginTop: 16 }}>
            <b>Absolute</b> always lands on the dashed target, wherever it began. <b>Relative</b> jumps ±80 from the carriage's current spot — run it twice and it keeps stepping. That's the whole difference.
          </Bubble>
        </div>

        <pre className="code">
<span className="var">fbMoveAbs</span>(<span className="var">Axis</span>:=<span className="var">Axis</span>, <span className="var">Execute</span>:=<span className="var">bGo</span>, <span className="var">Position</span>:=<span className="num">350.0</span>, <span className="var">Velocity</span>:=<span className="num">{vel}.0</span>);  <span className="com">{`// to 350`}</span>{"\n"}
<span className="var">fbMoveRel</span>(<span className="var">Axis</span>:=<span className="var">Axis</span>, <span className="var">Execute</span>:=<span className="var">bStep</span>, <span className="var">Distance</span>:=<span className="num">80.0</span>, <span className="var">Velocity</span>:=<span className="num">{vel}.0</span>);  <span className="com">{`// by +80`}</span>{"\n"}
<span className="kw">IF</span> <span className="var">fbMoveAbs</span>.<span className="var">Done</span> <span className="kw">THEN</span> <span className="var">eStep</span> := <span className="var">eStep</span> + <span className="num">1</span>; <span className="kw">END_IF</span>   <span className="com">{`// Done is a one-shot pulse`}</span>
        </pre>

        <div className="grid2">
          <Callout kind="key"><code>Execute</code> is rising-edge triggered — the move fires when it goes FALSE→TRUE. <code>Done</code> pulses TRUE for one cycle when the target is reached; latch it if you need to remember.</Callout>
          <Callout kind="warn">Absolute moves only mean anything once the axis is <b>homed</b> — position 350 is relative to a zero you haven't set yet. That's the next-but-one chapter.</Callout>
        </div>

        <Check q="The axis is at 200. You run MC_MoveRelative with Distance := 80 twice. Where does it end up?" correct={2}
          options={["80", "280", "360", "200"]}
          explain="Relative adds to the current position each time: 200 → 280 → 360. Absolute would have gone to 80 and stayed at 80. Relative steps; absolute targets." />
      </div>
    );
  },
});

// ════════════════════════════════════════════════════════════════
// CH3 — Motion profile
// ════════════════════════════════════════════════════════════════
defineChapter({
  id: "profile", act: "PLCopen · Moves", num: 3, emoji: "📈", navTitle: "The motion profile",
  title: "The profile: ramp up, cruise, ramp down",
  sub: "A move isn't instant. The axis accelerates to velocity, cruises, then decelerates to a stop — a trapezoid. Push the distance too short and it never reaches full speed: the trapezoid collapses into a triangle.",
  Body: function ProfileBody() {
    const [vel, setVel] = uM(300);     // mm/s
    const [acc, setAcc] = uM(800);     // mm/s²
    const [dist, setDist] = uM(300);   // mm

    const tRamp = vel / acc;
    const dRamp = (vel * vel) / (2 * acc);
    let trapezoid, peak, tTotal, pts;
    if (2 * dRamp >= dist) {
      trapezoid = false;
      peak = Math.sqrt(dist * acc);
      const tr = peak / acc;
      tTotal = 2 * tr;
      pts = [[0, 0], [tr, peak], [tTotal, 0]];
    } else {
      trapezoid = true;
      peak = vel;
      const dCruise = dist - 2 * dRamp;
      const tCruise = dCruise / vel;
      tTotal = 2 * tRamp + tCruise;
      pts = [[0, 0], [tRamp, vel], [tRamp + tCruise, vel], [tTotal, 0]];
    }

    const W = 380, H = 150, PADL = 8, PADB = 8;
    const maxV = Math.max(peak, vel) * 1.08;
    const sx = (W - PADL) / tTotal, sy = (H - PADB) / maxV;
    const X = (t) => PADL + t * sx;
    const Y = (v) => H - PADB - v * sy;
    const poly = pts.map(p => `${X(p[0]).toFixed(1)},${Y(p[1]).toFixed(1)}`).join(" ");

    return (
      <div className="ch-body">
        <div className="stage">
          <div style={{ display: "grid", gridTemplateColumns: "auto 1fr", gap: 14, alignItems: "center" }}>
            <div style={{ writingMode: "vertical-rl", transform: "rotate(180deg)", fontFamily: '"JetBrains Mono",monospace', fontWeight: 700, fontSize: 12, color: "var(--ink-soft)", textAlign: "center" }}>velocity →</div>
            <svg viewBox={`0 0 ${W} ${H + 16}`} style={{ width: "100%", height: "auto" }}>
              <line x1={PADL} y1={H - PADB} x2={W} y2={H - PADB} stroke="rgba(27,27,47,.3)" strokeWidth="2" />
              <line x1={PADL} y1="4" x2={PADL} y2={H - PADB} stroke="rgba(27,27,47,.3)" strokeWidth="2" />
              <polygon points={poly} fill="rgba(31,168,142,.20)" stroke="var(--mint)" strokeWidth="3" strokeLinejoin="round" />
              {trapezoid && <line x1={X(tRamp)} y1={Y(vel)} x2={X(tRamp)} y2={H - PADB} stroke="rgba(27,27,47,.25)" strokeWidth="2" strokeDasharray="4 4" />}
              {trapezoid && <line x1={X(tRamp + (dist - 2 * dRamp) / vel)} y1={Y(vel)} x2={X(tRamp + (dist - 2 * dRamp) / vel)} y2={H - PADB} stroke="rgba(27,27,47,.25)" strokeWidth="2" strokeDasharray="4 4" />}
              <text x={W - 4} y={H + 12} textAnchor="end" fontSize="11" fontFamily="JetBrains Mono" fill="#3A3A52" fontWeight="700">time → {tTotal.toFixed(2)}s</text>
              {trapezoid
                ? <><text x={X(tRamp / 2)} y={H + 12} textAnchor="middle" fontSize="10" fill="#3A3A52">accel</text>
                  <text x={X(tRamp + (dist - 2 * dRamp) / vel / 2)} y={H + 12} textAnchor="middle" fontSize="10" fill="#3A3A52">cruise</text></>
                : <text x={W / 2} y={H + 12} textAnchor="middle" fontSize="10" fill="#E63A5C" fontWeight="700">triangular — never reaches set velocity</text>}
            </svg>
          </div>

          <div style={{ display: "grid", gap: 12, marginTop: 8 }}>
            {[["Set velocity", vel, setVel, 60, 500, 10, "mm/s", "var(--coral)"],
              ["Acceleration", acc, setAcc, 100, 2500, 50, "mm/s²", "var(--mint)"],
              ["Distance", dist, setDist, 30, 500, 10, "mm", "var(--blue)"]].map(([lbl, val, set, mn, mx, st, unit, col], i) => (
              <div key={i}>
                <div className="row" style={{ justifyContent: "space-between" }}>
                  <span className="tag">{lbl}</span>
                  <span className="tag" style={{ background: col, color: "#fff" }}>{val} {unit}</span>
                </div>
                <input type="range" min={mn} max={mx} step={st} value={val} onChange={e => set(parseInt(e.target.value, 10))} style={{ width: "100%", accentColor: col }} />
              </div>
            ))}
          </div>

          <div className="readout" style={{ marginTop: 14 }}>
            <span className="lbl">peak velocity</span> = <span className="val">{peak.toFixed(0)}</span> mm/s {!trapezoid && <span className="miss">(capped — distance too short)</span>}{"\n"}
            <span className="lbl">total time   </span> = <span className="val">{tTotal.toFixed(2)}</span> s   ·   <span className="lbl">shape</span> = <span style={{ color: trapezoid ? "#7DEAC4" : "#FFD43B", fontWeight: 700 }}>{trapezoid ? "trapezoid" : "triangle"}</span>
          </div>
          <Bubble style={{ marginTop: 14 }}>Crank <b>distance</b> down or <b>acceleration</b> down and watch the trapezoid pinch into a triangle — there isn't room to reach the set velocity before it must start braking.</Bubble>
        </div>

        <div className="grid2">
          <Callout kind="key">Every move FB takes <code>Velocity</code>, <code>Acceleration</code>, <code>Deceleration</code> and <code>Jerk</code>. They shape the ramp — higher accel = faster but harsher; jerk-limiting rounds the corners to spare the mechanics.</Callout>
          <Callout kind="tip">For short index moves you often <i>can't</i> hit the set velocity. That's normal — the drive plans the fastest legal triangle and still reports <code>Done</code>.</Callout>
        </div>

        <Check q="A 20 mm index move is set to 300 mm/s but barely seems to reach speed. Why?" correct={0}
          options={["Too little distance to accelerate to 300 and brake again — the profile goes triangular", "The velocity input is ignored on short moves", "The axis isn't homed", "Acceleration must equal velocity"]}
          explain="Reaching 300 mm/s and stopping needs a certain ramp distance. Over just 20 mm there isn't room, so the axis tops out below 300 in a triangular profile. Set velocity is a ceiling, not a promise." />
      </div>
    );
  },
});

// ════════════════════════════════════════════════════════════════
// CH4 — Homing & jogging
// ════════════════════════════════════════════════════════════════
defineChapter({
  id: "homing", act: "PLCopen · Reference", num: 4, emoji: "🏠", navTitle: "Homing & jogging",
  title: "Homing & jogging: find zero, nudge by hand",
  sub: "An absolute position means nothing until the axis knows where zero is. Homing drives to a reference switch and sets the datum. Jogging is the manual override — hold a button to inch the axis while you set up.",
  Body: function HomingBody() {
    const RAIL = 500;
    const [pos, setPos] = uM(280);
    const posRef = rM(280);
    const [homed, setHomed] = uM(false);
    const [homing, setHoming] = uM(false);
    const raf = rM(null);
    const jogInt = rM(null);
    eM(() => () => { cancelAnimationFrame(raf.current); clearInterval(jogInt.current); }, []);

    const home = () => {
      if (homing) return;
      cancelAnimationFrame(raf.current); clearInterval(jogInt.current);
      setHoming(true); setHomed(false);
      const from = posRef.current, dur = Math.max(500, from / 260 * 1000), t0 = performance.now();
      const tick = (now) => {
        const k = Math.min(1, (now - t0) / dur);
        const cur = from * (1 - k);
        posRef.current = cur; setPos(cur);
        if (k < 1) raf.current = requestAnimationFrame(tick);
        else { posRef.current = 0; setPos(0); setHoming(false); setHomed(true); }
      };
      raf.current = requestAnimationFrame(tick);
    };
    const jog = (dir) => {
      if (homing) return;
      clearInterval(jogInt.current);
      jogInt.current = setInterval(() => {
        const next = Math.max(0, Math.min(RAIL, posRef.current + dir * 6));
        posRef.current = next; setPos(next);
      }, 16);
    };
    const stopJog = () => clearInterval(jogInt.current);

    const status = homing ? "HOMING" : homed ? "HOMED" : "NOT HOMED";
    const sColor = homing ? "var(--yellow)" : homed ? "var(--mint)" : "var(--coral)";
    const pctPos = (pos / RAIL) * 100;

    return (
      <div className="ch-body">
        <div className="stage">
          <div style={{ position: "relative", height: 86, margin: "6px 6px 0" }}>
            <div style={{ position: "absolute", left: 0, top: -2, fontSize: 20 }}>🚩</div>
            <div style={{ position: "absolute", left: 0, top: 22, fontSize: 10, fontFamily: '"JetBrains Mono",monospace', fontWeight: 700, color: "var(--coral-deep)" }}>ref switch = 0</div>
            <div style={{ position: "absolute", left: 0, right: 0, bottom: 26, height: 14, background: "repeating-linear-gradient(90deg,#fff 0 9px,rgba(27,27,47,.12) 9px 10px)", border: "3px solid var(--line)", borderRadius: 8 }} />
            <div style={{ position: "absolute", left: `${pctPos}%`, bottom: 20, transform: "translateX(-50%)" }}>
              <div style={{ width: 46, height: 34, background: homing ? "var(--yellow)" : homed ? "var(--mint)" : "#fff", border: "3px solid var(--line)", borderRadius: 8, boxShadow: "3px 3px 0 var(--line)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 18 }}>🔩</div>
            </div>
          </div>

          <div className="readout">
            <span className="lbl">Axis.ActPos</span> = {homed || homing ? <span className="val">{pos.toFixed(1)} mm</span> : <span className="miss">undefined — not homed</span>}   ·   <span className="lbl">Status</span> = <span style={{ color: sColor === "var(--coral)" ? "#FF8FA8" : sColor === "var(--mint)" ? "#7DEAC4" : "#FFD43B", fontWeight: 700 }}>{status}</span>
          </div>

          <div className="row" style={{ justifyContent: "center", marginTop: 16, gap: 10, flexWrap: "wrap" }}>
            <button className="btn coral sm" disabled={homing} onClick={home}>🏠 MC_Home</button>
            <button className="btn sm" style={{ background: "var(--blue)", color: "#fff" }}
              onMouseDown={() => jog(-1)} onMouseUp={stopJog} onMouseLeave={stopJog}
              onTouchStart={(e) => { e.preventDefault(); jog(-1); }} onTouchEnd={stopJog}>◀ Jog −</button>
            <button className="btn sm" style={{ background: "var(--blue)", color: "#fff" }}
              onMouseDown={() => jog(1)} onMouseUp={stopJog} onMouseLeave={stopJog}
              onTouchStart={(e) => { e.preventDefault(); jog(1); }} onTouchEnd={stopJog}>Jog + ▶</button>
          </div>

          <Bubble style={{ marginTop: 16 }}>
            {homing ? <>Seeking the reference switch… the drive creeps toward the flag and will latch zero when it hits.</>
              : homed ? <>Homed ✓ — position is now <b>absolute</b> and trustworthy. Absolute moves finally mean what they say.</>
                : <>Before homing, <code>ActPos</code> is meaningless — the axis has no idea where zero is. Press <b>MC_Home</b>, or hold a <b>Jog</b> to nudge it by hand.</>}
          </Bubble>
        </div>

        <pre className="code">
<span className="var">fbHome</span>(<span className="var">Axis</span>:=<span className="var">Axis</span>, <span className="var">Execute</span>:=<span className="var">bDoHome</span>, <span className="var">Position</span>:=<span className="num">0.0</span>);   <span className="com">{`// seek switch, set datum`}</span>{"\n"}
<span className="var">bReady</span> := <span className="var">Axis</span>.<span className="var">Status</span>.<span className="var">Homed</span>;{"\n\n"}
<span className="var">fbJogFwd</span>(<span className="var">Axis</span>:=<span className="var">Axis</span>, <span className="var">JogForward</span>:=<span className="var">bBtnPlus</span>, <span className="var">Velocity</span>:=<span className="num">25.0</span>);   <span className="com">{`// hold to inch`}</span>
        </pre>

        <div className="grid2">
          <Callout kind="key">Homing establishes the <b>zero datum</b>. Incremental encoders forget zero on every power-off, so they re-home at startup. Absolute encoders remember and may home only once.</Callout>
          <Callout kind="warn">Jog ignores the soft target but still respects <b>limit switches</b> and software limits — manual doesn't mean reckless. Keep velocities low while setting up.</Callout>
        </div>

        <Check q="Why must an axis with an incremental encoder be homed after every power-up?" correct={1}
          options={["To warm up the motor", "An incremental encoder counts steps but forgets its absolute zero when powered off — homing re-establishes the datum", "To charge the drive", "It doesn't — homing is optional"]}
          explain="Incremental encoders only know how far they've moved since power-on, not where they are. Homing drives to a known reference and sets zero, so absolute positions become meaningful again." />
      </div>
    );
  },
});

// ════════════════════════════════════════════════════════════════
// CH5 — Analog I/O & scaling
// ════════════════════════════════════════════════════════════════
defineChapter({
  id: "scaling", act: "Real World · Sensors", num: 5, emoji: "🌡️", navTitle: "Analog & scaling",
  title: "Analog & scaling: raw counts → real units",
  sub: "A sensor doesn't hand you '7.4 bar'. The terminal gives a raw integer — 0…32767. Your job is the linear map from counts to engineering units, and to notice when a 4–20 mA loop drops out and a wire has broken.",
  Body: function ScalingBody() {
    const PRESETS = {
      temp: { label: "0–10 V temp probe", rawLo: 0, rawHi: 32767, euLo: 0, euHi: 250, unit: "°C", faultLo: -1 },
      pres: { label: "4–20 mA pressure", rawLo: 6553, rawHi: 32767, euLo: 0, euHi: 10, unit: "bar", faultLo: 5800 },
    };
    const [key, setKey] = uM("temp");
    const [raw, setRaw] = uM(16000);
    const p = PRESETS[key];
    const fault = raw < p.faultLo;
    const clampedRaw = Math.max(p.rawLo, Math.min(p.rawHi, raw));
    const eu = (clampedRaw - p.rawLo) / (p.rawHi - p.rawLo) * (p.euHi - p.euLo) + p.euLo;
    const euPct = (eu - p.euLo) / (p.euHi - p.euLo) * 100;

    return (
      <div className="ch-body">
        <div className="stage">
          <div className="row" style={{ gap: 10, marginBottom: 14 }}>
            {Object.entries(PRESETS).map(([k, v]) => (
              <button key={k} className="btn sm" style={{ background: key === k ? "var(--mint)" : "#fff" }} onClick={() => { setKey(k); setRaw(k === "pres" ? 16000 : 16000); }}>{v.label}</button>
            ))}
          </div>

          <div className="row" style={{ justifyContent: "space-between" }}>
            <span className="tag">Raw count from terminal</span>
            <span className="tag" style={{ background: "var(--blue)", color: "#fff" }}>{raw}</span>
          </div>
          <input type="range" min={0} max={32767} step={1} value={raw} onChange={e => setRaw(parseInt(e.target.value, 10))} style={{ width: "100%", accentColor: "var(--blue)" }} />

          <div style={{ marginTop: 18 }}>
            <div style={{ position: "relative", height: 40, border: "3px solid var(--line)", borderRadius: 12, background: "#fff", overflow: "hidden", boxShadow: "4px 4px 0 var(--line)" }}>
              <div style={{ height: "100%", width: `${fault ? 0 : Math.max(0, Math.min(100, euPct))}%`, background: fault ? "var(--coral)" : "var(--mint)", transition: "width .12s" }} />
              <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", fontFamily: '"Bricolage Grotesque",sans-serif', fontWeight: 800, fontSize: 19 }}>
                {fault ? "⚠ wire break / under-range" : `${eu.toFixed(1)} ${p.unit}`}
              </div>
            </div>
            <div className="row" style={{ justifyContent: "space-between", marginTop: 6, fontFamily: '"JetBrains Mono",monospace', fontSize: 11.5, fontWeight: 700, color: "var(--ink-soft)" }}>
              <span>{p.euLo} {p.unit}</span><span>{p.euHi} {p.unit}</span>
            </div>
          </div>

          <Bubble style={{ marginTop: 16 }}>
            {fault ? <>On a 4–20 mA loop, <b>0 mA means the wire is cut</b> — the raw count sits below the live range, so you flag a fault instead of reporting a fake 0 bar.</>
              : <>Raw <b>{raw}</b> maps linearly to <b>{eu.toFixed(1)} {p.unit}</b>. Slide toward 0 on the 4–20 mA sensor to trip the broken-wire detector.</>}
          </Bubble>
        </div>

        <pre className="code">
<span className="com">{`// linear scale: counts -> engineering units (LREAL math)`}</span>{"\n"}
<span className="var">rValue</span> := <span className="typ">INT_TO_LREAL</span>(<span className="var">nRaw</span> - <span className="var">nRawLo</span>) / (<span className="var">nRawHi</span> - <span className="var">nRawLo</span>){"\n"}
        * (<span className="var">rEuHi</span> - <span className="var">rEuLo</span>) + <span className="var">rEuLo</span>;{"\n\n"}
<span className="kw">IF</span> <span className="var">nRaw</span> &lt; <span className="var">nFaultLo</span> <span className="kw">THEN</span> <span className="var">bWireBreak</span> := <span className="kw">TRUE</span>; <span className="kw">END_IF</span>   <span className="com">{`// 4-20mA dropout`}</span>
        </pre>

        <div className="grid2">
          <Callout kind="key">The scale is just <code>y = m·x + c</code> across two known points: (rawLo→euLo) and (rawHi→euHi). Do the maths in <b>LREAL</b> so integer division doesn't quietly truncate.</Callout>
          <Callout kind="tip">Prefer <b>4–20 mA</b> over 0–10 V for anything important: a cut wire reads 0 mA, which is outside the live band — so you can <i>tell</i> the sensor failed instead of trusting a false zero.</Callout>
        </div>

        <Check q="Why is 4–20 mA often preferred over 0–10 V for a critical sensor?" correct={2}
          options={["It's cheaper", "Voltage is more accurate", "A broken wire reads 0 mA — below the 4 mA live floor — so the fault is detectable instead of looking like a real zero", "It needs no scaling"]}
          explain="With 4–20 mA the lowest valid signal is 4 mA. A cut wire gives 0 mA, plainly outside the band, so you can raise a wire-break alarm. A 0–10 V sensor that fails to 0 V looks just like a legitimate minimum reading." />

        <Callout kind="tip" icon="🏁">That's Track 07 — you can enable an axis, command absolute and relative moves, shape a profile, home and jog, and turn raw counts into trustworthy units. Motion is where logic finally meets the machine. Back to the Academy!</Callout>
      </div>
    );
  },
});
