<!-- RUBY-BACKUP · part 39 -->
0.35,0));
    for(var s=0;s<n;s++){
      pts.push(new T.Vector3(M.movX[s],loadY+0.55,0));
      pts.push(new T.Vector3(M.fixX[s],H-0.35,0));
    }
    pts.push(M.handle.position.clone());
    for(var q=0;q<M.ropes.length && q<pts.length-1;q++){
      aimCyl(M.ropes[q],pts[q],pts[q+1]);
    }
    /* spin the sheaves with the rope */
    var spin=M.pull*2;
    M.fixBar.children.forEach(function(c){ c.rotation.z=spin; });
    M.movBar.children.forEach(function(c){ c.rotation.z=-spin; });
  }catch(e){}
}

/* ============================================================================
   COUNTERWEIGHT — balance the load. m1 = m2 -> a finger moves it.
   `counterweight` builds it. Tap the weight crate to cycle 5/10/20 KG.
   `unweight` removes it.
   ============================================================================ */
function buildCounterweight(){
  try{
    if(typeof scene==='undefined') return;
    mats();
    var base=forwardPoint(7,0);
    var g=new T.Group();
    var H=3.8;
    var p1=new T.Mesh(new T.BoxGeometry(0.3,H,0.3),mats().red); p1.position.set(-1.4,H/2,0); g.add(p1);
    var p2=p1.clone(); p2.position.x=1.4; g.add(p2);
    var bar=new T.Mesh(new T.BoxGeometry(3.2,0.25,0.3),mats().red); bar.position.set(0,H,0); g.add(bar);
    var sheave=new T.Mesh(new T.TorusGeometry(0.3,0.09,12,26),mats().gold);
    sheave.position.set(0,H-0.42,0); g.add(sheave);
    var M=regMachine('counterweight',g);
    M.H=H; M.yL=1.6; M.yR=1.6; /* crate heights */
    M.massL=10; M.massR=10;
    function crate(mass){
      var c=new T.Mesh(new T.BoxGeometry(0.85,0.85,0.85),mats().crate);
      var ring=goldRing(0.2); ring.position.y=0.75; c.add(ring);
      var hit=hitSphere(0.6); hit.position.y=0.75; c.add(hit);
      var lb=textSprite(mass+' KG',0.55); lb.position.y=-0.85; c.add(lb);
      c.userData.lb=lb; c.userData.ring=ring; c.userData.hit=hit;
      return c;
    }
    M.loadC=crate(10); M.loadC.position.set(-0.85,M.yL,0); g.add(M.loadC);
    M.wtC=crate(10); M.wtC.position.set(0.85,M.yR,0); g.add(M.wtC);
    var lbl=textSprite('COUNTERWEIGHT',0.85); lbl.position.set(0,H+0.9,0); g.add(lbl);
    var formula=textSprite('m1 = m2 — BALANCED',0.7); formula.position.set(0,H+0.35,0); g.add(formula); M.formula=formula;
    M.ropeL=ropeSeg(); g.add(M.ropeL);
    M.ropeR=ropeSeg(); g.add(M.ropeR);
    function layout(){
      M.loadC.position.y=M.yL; M.wtC.position.y=M.yR;
      aimCyl(M.ropeL,new T.Vector3(-0.3,H-0.42,0),new T.Vector3(-0.85,M.yL+0.95,0));
      aimCyl(M.ropeR,new T.Vector3(0.3,H-0.42,0),new T.Vector3(0.85,M.yR+0.95,0));
      sheave.rotation.z=(M.yL-M.yR)*1.5;
      var bal=(M.massL===M.massR);
      try{
        g.remove(M.formula);
        M.formula=textSprite(bal?'m1 = m2 — BALANCED':'m'+M.massL+' vs m'+M.massR+' — HEAVY SINKS',0.7);
        M.formula.position.set(0,H+0.35,0); g.add(M.formula);
      }catch(e){}
    }
    M.layout=layout;
    /* drag either ring: rope is fixed length, they move opposite 1:1 */
    function ringDrag(crate,isLeft,onTap){
      var y0=0, v0=0;
      var hit=crate.userData.hit;
      regHit(hit,{
        onDown:function(x,y){
          var wp=worldPosOf(crate.userData.ring); var wy=vPlaneY(wp,x,y);
          if(wy!==null){ y0=wy; v0=isLeft?M.yL:M.yR; }
        },
        onMove:function(x,y){
          var wp=worldPosOf(crate.userData.ring); var wy=vPlaneY(wp,x,y);
          if(wy===null) return;
          var d=wy-y0;
          var nl=Math.max(0.6,Math.min(3.0,v0+d));
          var delta=nl-v0;
          if(isLeft){ M.yL=nl; M.yR=Math.max(0.6,Math.min(3.0,M.yR-delta)); }
          else{ M.yR=nl; M.yL=Math.max(0.6,Math.min(3.0,M.yL-delta)); }
          layout();
        },
        onTap:onTap||null
      });
    }
    ringDrag(M.loadC,true,null);
    /* tap weight crate cycles mass */
    var wtTap=function(){
      var ms=[5,10,20]; var i=ms.indexOf(M.massR); M.massR=ms[(i+1)%3];
      try{
        var c=M.wtC; c.remove(c.userData.lb);
        c.userData.lb=textSprite(M.massR+' KG',0.55); c.userData.lb.position.y=-0.85; c.add(c.userData.lb);
      }catch(e){}
      layout();
      status('counterweight '+M.massR+' KG'+(M.massL===M.massR?' — BALANCED, a finger moves it':' — heavy side sinks'));
    };
    ringDrag(M.wtC,false,wtTap);
    g.position.copy(base);
    scene.add(g);
    layout();
    riseIn(g,base.y,2.5,0.6);
    status('counterweight — drag a ring, the other side answers. tap the weight to change it.');
  }catch(e){ status('counterweight failed: '+e.message); }
}

/* ============================================================================
   WINCH — the crank. Turn it in circles, the drum winds the rope.
   True circumference: ΔROPE = Δθ × r. No faking.
   `winch` builds it. Drag the crank knob in a circle. `unwinch` removes it.
   ============================================================================ */
function buildWinch(){
  try{
    if(typeof scene==='undefined') return;
    mats();
    var base=forwardPo