<!-- RUBY-BACKUP · part 558 -->
ouchmove', function(e){
      if(e.touches.length===2 && lastPinchDist>0){
        var d = Math.hypot(
          e.touches[0].clientX - e.touches[1].clientX,
          e.touches[0].clientY - e.touches[1].clientY
        );
        var delta = lastPinchDist - d;
        if(typeof camDist!=='undefined'){
          camDist = Math.max(0.3, Math.min(60, camDist + delta * 0.02));
        }
        lastPinchDist = d;
      }
    }, {passive:true});
    canvas.addEventListener('touchend', function(e){
      if(e.touches.length<2) lastPinchDist = 0;
    }, {passive:true});
  }
  findCanvas();
})();


===== FILE: public/ruby-backup/salvage-block.js =====
/* ============================================================================
   SALVAGE BLOCK — the seed's mirror, equal and opposite.
   The King: "Create another block for the equal and opposite reason for each
   of these to be disassembled and put back in to your inventory."

   - `salvage`                  arm it, tap a block -> SALVAGE BLOCK (dark iron core)
   - tap salvage, tap machine   disassembles IN REVERSE, parts FLY home
   - `inventory`                lists collected parts
   - `inventory place <part>`   tap ground -> the part rises there again
   - `inventory clear`          empties it (parts dissolve)

   The seed builds. The salvage unbuilds. Parts are never lost.
   ============================================================================ */
(function(){
  if(window.__salvageBlock) return;
  window.__salvageBlock = true;

  function status(m){ try{ if(typeof codeStatus==='function') codeStatus(m); }catch(e){} }

  var INV_KEY = 'ruby-inventory-v1';
  var inventory = {pulley:0, gear:0, rope:0, web:0};

  function loadInv(){
    try{
      var raw = localStorage.getItem(INV_KEY);
      if(raw){ var d = JSON.parse(raw); for(var k in inventory) if(typeof d[k]==='number') inventory[k]=d[k]; }
    }catch(e){}
  }
  function saveInv(){
    try{ localStorage.setItem(INV_KEY, JSON.stringify(inventory)); }catch(e){}
  }
  loadInv();

  var salvageArmed = false;      // planting a salvage block
  var salvageBlocks = [];        // {obj, pos, core}
  var collectArmed = null;       // salvage block selected, awaiting machine tap
  var placeArmed = null;         // part kind awaiting ground tap

  var ironMat = null, goldMat = null;
  function mats(){
    if(ironMat) return;
    ironMat = new THREE.MeshStandardMaterial({color:0x2a2d33, metalness:0.9, roughness:0.35, emissive:0x0a0c10});
    goldMat = new THREE.MeshStandardMaterial({color:0xd8a920, metalness:0.85, roughness:0.25});
  }

  function groundY(x, z){
    try{ if(typeof fortifiedGroundY === 'function') return fortifiedGroundY(x, z); }catch(e){}
    return 0;
  }

  function pickAt(sx, sy){
    try{
      if(typeof rayPick === 'function'){
        var hit = rayPick(sx, sy);
        if(hit){
          if(hit.object) return {obj:hit.object, point:hit.point||null};
          if(hit.obj) return {obj:hit.obj, point:hit.point||null};
        }
      }
    }catch(e){}
    return null;
  }

  function groundPoint(sx, sy){
    /* fall back to a ray onto y=0 plane when no block hit */
    try{
      if(typeof rayPick === 'function'){
        var hit = rayPick(sx, sy);
        if(hit && hit.point) return hit.point.clone();
      }
    }catch(e){}
    return null;
  }

  /* ---------- plant the salvage block ---------- */
  function plantSalvage(obj){
    if(!obj) return;
    try{ obj.userData.isSalvage = true; }catch(e){}
    var p = new THREE.Vector3();
    try{ obj.getWorldPosition(p); }catch(e){}
    mats();
    var core = new THREE.Mesh(
      new THREE.OctahedronGeometry(0.45),
      new THREE.MeshStandardMaterial({color:0x3a3f47, metalness:0.95, roughness:0.3, emissive:0x101216})
    );
    core.position.copy(p); core.position.y += 0.3;
    core.userData.isSalvageCore = true;
    if(window.scene) window.scene.add(core);
    /* slow heavy rotation — iron, not gold */
    (function spin(){
      try{
        if(!core.parent) return;
        core.rotation.y -= 0.015;
        requestAnimationFrame(spin);
      }catch(e){}
    })();
    salvageBlocks.push({obj:obj, pos:p.clone(), core:core});
    status('salvage block planted — tap it, then tap a machine to unbuild it.');
  }

  function isSalvageObj(obj){
    try{
      var o = obj;
      while(o){ if(o.userData && o.userData.isSalvage) return true; o = o.parent; }
    }catch(e){}
    return false;
  }

  /* ---------- the fly-home: parts travel, never teleport ---------- */
  function flyHome(objGroup, fromPos, toPos, kind, done){
    /* lift the part off, arc it to the salvage block, shrink into it */
    var t0 = Date.now(), dur = 1100;
    var mid = fromPos.clone().add(toPos).multiplyScalar(0.5);
    mid.y = Math.max(fromPos.y, toPos.y) + 3.5; /* the arc */
    var startScale = objGroup.scale.x;
    (function step(){
      try{
        var t = Math.min(1, (Date.now()-t0)/dur);
        var e = t<0.5 ? 2*t*t : -1+(4-2*t)*