<!-- RUBY-BACKUP · part 68 -->
x4(inv);
      var pq=new T.Quaternion();
      p.getWorldQuaternion(pq).invert();
      obj.quaternion.copy(pq.multiply(qw));
    }else{ obj.position.copy(pw); obj.quaternion.copy(qw); }
  }catch(e){}
}
function worldPosOf(o){ try{ return o.getWorldPosition(new T.Vector3()); }catch(e){ return new T.Vector3(); } }
function worldQuatOf(o){ try{ return o.getWorldQuaternion(new T.Quaternion()); }catch(e){ return new T.Quaternion(); } }
function textSprite(text,scale){
  try{
    var c=document.createElement('canvas'); c.width=512; c.height=96;
    var g=c.getContext('2d');
    g.fillStyle='rgba(5,3,1,0.82)'; g.fillRect(0,0,512,96);
    g.strokeStyle='#d8a920'; g.lineWidth=3; g.strokeRect(3,3,506,90);
    g.fillStyle='#ffd700'; g.font='bold 34px Georgia,serif';
    g.textAlign='center'; g.textBaseline='middle';
    g.fillText(text,256,50);
    var tex=new T.CanvasTexture(c);
    var s=new T.Sprite(new T.SpriteMaterial({map:tex,transparent:true,depthTest:false}));
    var sc=scale||1; s.scale.set(3.4*sc,0.64*sc,1);
    return s;
  }catch(e){ return null; }
}
function forwardPoint(dist,up){
  var pw=playerWorld();
  var dir=new T.Vector3(0,0,1);
  try{
    if(typeof camera!=='undefined'&&camera){
      camera.getWorldDirection(dir); dir.y=0;
      if(dir.lengthSq()<1e-6) dir.set(0,0,1);
      dir.normalize();
    }
  }catch(e){}
  var x=pw.x+dir.x*dist, z=pw.z+dir.z*dist;
  return new T.Vector3(x, groundAtWorld(x,z)+(up||3), z);
}

/* ---------------- materials ---------------- */
var MAT=null;
function mats(){
  if(MAT) return MAT;
  MAT={
    gold:  new T.MeshStandardMaterial({color:0xffd700,metalness:0.9,roughness:0.25,emissive:0x664d00,emissiveIntensity:0.55}),
    rope:  new T.MeshStandardMaterial({color:0xd8a920,metalness:0.55,roughness:0.45}),
    steel: new T.MeshStandardMaterial({color:0x555560,metalness:0.8,roughness:0.35}),
    red:   new T.MeshStandardMaterial({color:0x8a0f0f,metalness:0.4,roughness:0.5}),
    hit:   new T.MeshBasicMaterial({visible:false})
  };
  return MAT;
}

/* ---------------- picking ----------------
   resolveTarget: walk up from the hit object.
   - rig parts and deform-ring hit spheres are never targets (other tools own them)
   - a deform frame found in the chain -> frame target
   - otherwise the first real mesh, size-filtered (ground/sky excluded) */
function resolveTarget(hitObj){
  var o=hitObj, firstMesh=null, frame=null, pulley=null;
  while(o && !o.isScene){
    var ud=o.userData||{};
    if(ud.rigPart || ud.deformHandle) return null;
    if(ud.deformFrame && !frame) frame=ud.deformFrame;
    if(ud.rigPulley && ud.pulleyRef && !pulley) pulley={obj:o, ref:ud.pulleyRef};
    if(!firstMesh && o.isMesh) firstMesh=o;
    o=o.parent;
  }
  if(pulley) return {kind:'pulley', obj:pulley.obj, pulley:pulley.ref};
  if(frame) return {kind:'frame', frame:frame, obj:frame.group};
  if(!firstMesh || !firstMesh.visible) return null;
  var nm=(firstMesh.name||'').toLowerCase();
  if(nm.indexOf('practice-')===0 || nm.indexOf('infra-')===0) return null;
  var mt=firstMesh.material;
  if(mt && mt.transparent && mt.opacity<0.6) return null;
  var g=firstMesh.geometry;
  if(!g) return null;
  try{ if(!g.boundingSphere) g.computeBoundingSphere(); }catch(e){ return null; }
  if(!g.boundingSphere) return null;
  var s=new T.Vector3(); firstMesh.getWorldScale(s);
  var r=g.boundingSphere.radius*Math.max(s.x,s.y,s.z);
  if(r<0.22 || r>60) return null;
  return {kind:'mesh', obj:firstMesh};
}
function rayPick(cx,cy,objects){
  try{
    if(typeof scene==='undefined'||typeof camera==='undefined') return null;
    setNDC(cx,cy);
    raycaster.setFromCamera(ndc,camera);
    var hits=raycaster.intersectObjects(objects||scene.children,true);
    return hits.length?hits[0]:null;
  }catch(e){ return null; }
}
function targetKey(t){
  if(t.kind==='pulley') return 'pulley:'+t.obj.id;
  return t.kind==='frame' ? 'frame:'+t.frame.group.id : 'mesh:'+t.obj.id;
}
function targetBody(t){ return t.obj; }

/* ---------------- GRAB — pick up and move ----------------
   tap a block: it lifts. drag: it follows. tap: it drops, y baked to ground.
   The grab button (toggleGrabMode) drives this too. */
var grabMode=false, carried=null, grabTouch=null;
function setGrabMode(on){
  closeMenu();
  grabMode=!!on;
  if(!grabMode && carried) dropCarried();
  grabTouch=null;
  status(grabMode?'grab — tap a block to pick it up':'grab off');
}
function toggleRigGrab(){ setGrabMode(!grabMode); }
function startCarry(t, pointW){
  if(getAnchor(t)){ status('unanchor it first — pinned things stay'); return; }
  var body=targetBody(t);
  var wp=worldPosOf(body);
  var halfH=0.5;
  try{
    var bb=new T.Box3().setFromObject(body);
    halfH=Math.max(0.2,(bb.max.y-bb.min.y)/2);
  }catch(e){}
  carried={t:t, planeY:wp.y+1.2, halfH:halfH};
  status('carrying — drag to move, tap to drop');
}
function carryMoveTo(cx,cy){
  if(!carried) return;
  try{
    if(typeof camera==='undefined') return;
    setNDC(c