<!-- RUBY-BACKUP · part 399 -->
  if(!e) return;
  didAnchorLine = e.line; awaitingDidAnchor = true;
  codeStatus('tap the anchor stone for: '+e.line);
}
function didAnchorTo(grp){
  awaitingDidAnchor = false;
  cursor.x = grp.position.x; cursor.z = grp.position.z; saveCursor(); paintCursor();
  var l = didAnchorLine; didAnchorLine = null;
  runLine(l);
}
/* send your build to your friends: `send myfence` turns the save into a code
   (copied to the bar, ready to copy). a friend types `take <code>` and the
   build joins their library. no server — the code IS the build. */
function shareEncode(name){
  var sv = SAVES[name];
  if(!sv) return null;
  var b64 = btoa(unescape(encodeURIComponent(JSON.stringify({n:name, s:sv.stones}))));
  return 'RUBY1.' + b64.replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'');
}
function shareDecode(code){
  code = String(code||'').trim();
  if(code.indexOf('RUBY1.')!==0) return null;
  try{
    var b64 = code.slice(6).replace(/-/g,'+').replace(/_/g,'/');
    var p = JSON.parse(decodeURIComponent(escape(atob(b64))));
    if(!p || typeof p.n!=='string' || !/^[a-z][a-z0-9]*$/.test(p.n)) return null;
    if(!Array.isArray(p.s) || !p.s.length || p.s.length>500) return null;
    var known = {cube:1,pyramid:1,sphere:1,cyl:1,torus:1,cone:1,octa:1,tetra:1,icosa:1,dodeca:1,tri:1,pdown:1,poff:1,poffdown:1};
    for(var i=0;i<p.s.length;i++){ var st=p.s[i];
      if(!isFinite(st.dx)||!isFinite(st.dz)||!isFinite(st.y)||!isFinite(st.r)) return null;
      var bs = String(st.s).split('~')[0];
      if(!known[bs] || (st.s!==bs && st.s!==bs+'~ghost')) return null;
      if((st.v!==0&&st.v!==1)) return null;
      if(!(st.r>=1&&st.r<=200)) return null;
    }
    return p;
  }catch(e){ return null; }
}
function saveOps(name, ec){
  var sv = SAVES[name].stones, o = [];
  for(var i=0;i<sv.length;i++){ var c=sv[i];
    o.push({shape:c.s+(c.v?'~ghost':''), size:c.r, x:ec.x+c.dx, z:ec.z+c.dz, y:c.y||0, proc:name});
  }
  return o;
}
/* oops-my-bad: surgical undo for huge intricate builds. arm with the words,
   tap one anchor stone — every stone connected to it is offered:
   `cut` removes them, `keep` releases them. */
var awaitingAnchor = false;
var selection = [];
var selHelpers = new T.Group(); scene.add(selHelpers);
function clearSelection(){
  selection = [];
  while(selHelpers.children.length) selHelpers.remove(selHelpers.children[0]);
}
function stoneTouches(a, b){
  var au=a.userData, bu=b.userData;
  if(Math.max(Math.abs(a.position.x-b.position.x), Math.abs(a.position.z-b.position.z)) > 2.01) return false;
  var ay=au.y||0, ah=(au.rubies||1)*(au.k1||1);
  var by=bu.y||0, bh=(bu.rubies||1)*(bu.k1||1);
  return ay <= by+bh+0.01 && by <= ay+ah+0.01;
}
function floodFrom(anchor){
  var seen=[anchor], queue=[anchor];
  while(queue.length){
    var a=queue.pop();
    for(var i=0;i<stones.children.length;i++){
      var b=stones.children[i];
      if(seen.indexOf(b)>=0) continue;
      if(stoneTouches(a,b)){ seen.push(b); queue.push(b); }
    }
  }
  return seen;
}
function selectConnected(anchor){
  if(!anchor){ codeStatus('no anchor'); return; }
  clearSelection();
  selection = floodFrom(anchor);
  for(var i=0;i<selection.length;i++) selHelpers.add(new T.BoxHelper(selection[i], 0xffd700));
  var au = anchor.userData, abase = au.shape, aghost = !!au.ghost, nm = 0;
  for(var j=0;j<selection.length;j++){ var su = selection[j].userData; if(su.shape===abase && !!su.ghost===aghost) nm++; }
  codeStatus(selection.length+' connected · '+nm+' '+abase+(aghost?' ghost':'')+' — `swap <code>` restyles · `cut` removes · `keep` releases');
}
function swapSelection(code){
  if(!selection.length){ codeStatus('nothing selected'); return; }
  var key = codeToShapeKey(code);
  if(!key){ codeStatus('refused · unknown code'); return; }
  var nbase = shapeBase(key), nghost = shapeIsGhost(key);
  var au = selection[0].userData, abase = au.shape, aghost = !!au.ghost, n = 0;
  for(var i=0;i<selection.length;i++){
    var s2 = selection[i], u = s2.userData;
    if(u.shape!==abase || !!u.ghost!==aghost) continue;
    u.shape = nbase; u.ghost = nghost; u.k1 = legalK1(nbase);
    buildStoneVisual(s2);
    n++;
  }
  save(); updateAnchors();
  codeStatus(n+' → '+nbase+(nghost?' ghost':'')+' — `swap <code>` again · `keep` releases');
}
function cutSelection(){
  if(!selection.length){ codeStatus('nothing selected'); return; }
  var st=[];
  for(var i=0;i<selection.length;i++){
    var s=selection[i], u=s.userData;
    st.push({x:s.position.x, z:s.position.z, r:u.rubies||1, s:u.shape||'cube', v:u.ghost?1:0, y:u.y||0, p:u.proc||null});
  }
  for(var j=0;j<selection.length;j++) stones.remove(selection[j]);
  var n=st.length;
  clearSelection();
  undoStack.push({victim:'selection', killer:null, mass:0, stones:st});
  if(undoStack.length>10) undoStack.shift();
  saveUndo(); save(); updateAnchors();
  codeStatus(n+' cut');
}
function runOps(ops, mass){
  var raised=0, stepped=0, cleared=0, newGs=[];
  var runProc = null, bi