<!-- RUBY-BACKUP · part 45 -->
on');
    }});
    s.lbl=textSprite('',0.7); g.add(s.lbl);
    scene.add(g);
    spans.push(s);
    M.cleanup=function(){ var i=spans.indexOf(s); if(i>=0) spans.splice(i,1); };
    layoutSpan(s);
    startSpanLoop();
    status('span set — 1:1 SYNCED. move A, B answers. tap the gold ring to mirror.');
  }catch(e){ status('span failed: '+e.message); }
}
if(typeof window!=='undefined') window.__buildSpan = buildSpan;
function spanAnchors(s){
  return {
    pa:s.aObj.localToWorld(s.offA.clone()),
    pb:s.bObj.localToWorld(s.offB.clone())
  };
}
function layoutSpan(s){
  try{
    var an=spanAnchors(s);
    var mid=an.pa.clone().add(an.pb).multiplyScalar(0.5);
    s.toggle.position.copy(mid);
    s.thit.position.copy(mid);
    var want=(s.mode===1?'SPAN 1:1 SYNCED':'SPAN −1:1 MIRRORED');
    if(s.label!==want){
      s.label=want;
      try{ s.machine.group.remove(s.lbl); }catch(e){}
      s.lbl=textSprite(want,0.68);
      s.machine.group.add(s.lbl);
    }
    s.lbl.position.copy(mid); s.lbl.position.y+=0.75;
    var end=an.pa.clone().lerp(an.pb, Math.max(0.02,Math.min(1,s.grow)));
    aimCyl(s.rope, an.pa, end);
  }catch(e){}
}
function dropSpan(s){
  var M=s.machine, i=MACHINES.indexOf(M);
  if(i>=0) MACHINES.splice(i,1);
  try{ M.cleanup(); }catch(e){}
  try{ if(M.group.parent) M.group.parent.remove(M.group); }catch(e){}
  unregisterHits(M.group);
}
function unspanAll(){
  var n=spans.length;
  for(var i=spans.length-1;i>=0;i--) dropSpan(spans[i]);
  status(n?('unspanned — '+n+' cut, roads move on their own'):'no spans to cut');
}
function startSpanLoop(){ if(!spanRunning){ spanRunning=true; _lastSpanT=0; requestAnimationFrame(spanTick); } }
function spanTick(now){
  var dt=Math.min(0.05,((now-_lastSpanT)/1000)||0.016); _lastSpanT=now;
  var any=false;
  try{
    for(var i=spans.length-1;i>=0;i--){
      var s=spans[i];
      if(!s.aObj.parent||!s.bObj.parent){ dropSpan(s); continue; }
      any=true;
      if(s.grow<1) s.grow=Math.min(1,s.grow+dt/0.45); /* rope grows in — nothing teleports */
      var pa=worldPosOf(s.aObj), pb=worldPosOf(s.bObj);
      if(!s.lock){
        var da=pa.clone().sub(s.lastA), db=pb.clone().sub(s.lastB);
        var eps=1e-4;
        if(da.lengthSq()>eps*eps){
          s.lock=true;
          moveObjBy(s.bObj, da.multiplyScalar(s.mode));
          s.lastB.copy(worldPosOf(s.bObj)); s.lastA.copy(worldPosOf(s.aObj));
          s.lock=false;
        }else if(db.lengthSq()>eps*eps){
          s.lock=true;
          moveObjBy(s.aObj, db.multiplyScalar(s.mode));
          s.lastA.copy(worldPosOf(s.aObj)); s.lastB.copy(worldPosOf(s.bObj));
          s.lock=false;
        }else{ s.lastA.copy(pa); s.lastB.copy(pb); }
      }
      layoutSpan(s);
    }
  }catch(e){}
  if(any) requestAnimationFrame(spanTick); else spanRunning=false;
}
/* span taps must preempt rigging.js (its #view capture listener would open the
   pulley menu on road taps). window capture fires first. */
function initSpanTaps(){
  try{
    window.addEventListener('touchstart',function(e){
      if(!spanArmed) return;
      try{ var t=e.target; if(!t||!t.closest||!t.closest('#view')) return; }catch(x){ return; }
      var used=false;
      for(var i=0;i<e.changedTouches.length;i++){
        var th=e.changedTouches[i];
        if(spanTap(th.clientX,th.clientY)) used=true;
      }
      if(used){ e.preventDefault(); e.stopPropagation(); }
    },{passive:false,capture:true});
    window.addEventListener('mousedown',function(e){
      if(!spanArmed||e.button!==0) return;
      try{ var t=e.target; if(!t||!t.closest||!t.closest('#view')) return; }catch(x){ return; }
      if(spanTap(e.clientX,e.clientY)){ e.preventDefault(); e.stopPropagation(); }
   },{capture:true});
  }catch(e){}
}

/* ---------------- touch wiring ---------------- */
function initGrand(){
  var cv=null;
  try{ cv=document.getElementById('view'); }catch(e){}
  if(cv){
    cv.addEventListener('touchstart',function(e){
      var used=false;
      for(var i=0;i<e.changedTouches.length;i++){
        var t=e.changedTouches[i];
        if(gDown(t.identifier,t.clientX,t.clientY)) used=true;
      }
      if(used){ e.preventDefault(); e.stopPropagation(); }
    },{passive:false,capture:true});
    cv.addEventListener('touchmove',function(e){
      var used=false;
      for(var i=0;i<e.changedTouches.length;i++){
        var t=e.changedTouches[i];
        if(gMove(t.identifier,t.clientX,t.clientY)) used=true;
      }
      if(used){ e.preventDefault(); e.stopPropagation(); }
    },{passive:false,capture:true});
    cv.addEventListener('touchend',function(e){
      for(var i=0;i<e.changedTouches.length;i++) gUp(e.changedTouches[i].identifier);
    },{passive:false,capture:true});
    cv.addEventListener('touchcancel',function(e){
      for(var i=0;i<e.changedTouches.length;i++) gUp(e.changedTouches[i].identifier);
    },{passive:false,capture:true});
    cv.addEventListener('mousedown',function(e){
      if(e.button!==0) return;
    