<!-- RUBY-BACKUP · part 97 -->
(baseGeo(base), (ghostly || LINEWORK_ON) ? intaglioMat : (FAMILY[base] ? familyMat : stoneMat));
    var fs = r*k1;
    m.scale.set(fs,fs,fs);
    m.position.y = fs*window.MINIMUM/2;
    g.add(m);
  }
}
function stoneGroupOf(o){ while(o){ if(o.userData && o.userData.stone) return o; o = o.parent; } return null; }
function stoneAt(cx, cz, yRubies){
  yRubies = yRubies||0;
  for(var i=0;i<stones.children.length;i++){
    var s = stones.children[i];
    if(Math.abs(s.position.x-cx)<0.01 && Math.abs(s.position.z-cz)<0.01 && Math.abs((s.userData.y||0)-yRubies)<0.01) return s;
  }
  return null;
}
function largestStone(){
  var best = null, br = 0;
  stones.children.forEach(function(g){ var r = g.userData.rubies||0; if(r>br){ br = r; best = g; } });
  return best;
}
function save(){
  if(SHARED){ broadcastWatch(); }
  else if(!VIEW_ONLY){
    try{ localStorage.setItem(LSKEY, JSON.stringify(watchCells())); }catch(e){}
  }
  updateAnchors();
}
function placeStone(cx, cz, rubies, shapeKey, instant, quiet, yRubies, onLand){
  /* branching: auto-enter creator mode on manual builds */
  if(!quiet && typeof creatorTouch==='function') creatorTouch();
  yRubies = yRubies||0;
  /* branching: if occupied, stack upward instead of failing */
  var tryY = yRubies;
  while(stoneAt(cx, cz, tryY) && tryY < yRubies+20){
    tryY++;
  }
  if(stoneAt(cx, cz, tryY)) return null;
  yRubies = tryY;
  rubies = Math.max(1, Math.round(rubies||1));
  var base = shapeBase(shapeKey), ghostly = shapeIsGhost(shapeKey);
  var g = new T.Group();
  var land = (onLand!==false);
  var baseY = land ? fortifiedGroundY(cx, cz) : 0;
  g.userData = {stone:true, rubies:rubies, shape:base, ghost:ghostly, k1:legalK1(base), y:yRubies, land:land};
  var stoneBaseY = baseY + yRubies*window.MINIMUM;
  g.position.set(cx, stoneBaseY, cz);
  /* slope foundation — small blocks fill the gap where the land falls away */
  if(land && !ghostly){
    var foot = rubies * (g.userData.k1||1) * window.MINIMUM;
    [[-1,-1],[1,-1],[-1,1],[1,1]].forEach(function(cn){
      var fx = cx + cn[0]*foot*0.35, fz = cz + cn[1]*foot*0.35;
      var fh = groundY(fx, fz);
      var gap = stoneBaseY - fh, n = Math.min(6, Math.floor(gap/window.MINIMUM)-1);
      for(var fi=0; fi<n; fi++){
        var fg = new T.Group();
        fg.userData = {stone:true, rubies:1, shape:'cube', ghost:false, k1:legalK1('cube'), y:0, land:true, filler:true};
        fg.position.set(fx, fh + fi*window.MINIMUM, fz);
        buildStoneVisual(fg);
        stones.add(fg);
      }
    });
  }
  buildStoneVisual(g);
  stones.add(g);
  if(!instant){ g.scale.set(0.01,0.01,0.01); anims.push({m:g, t:0}); if(!quiet) showShapeCode(g); }
  save();
  return g;
}
function growStone(g){
  /* each tap grows the shape by one ruby, no ceiling */
  g.userData.rubies = (g.userData.rubies||1) + 1;
  buildStoneVisual(g); save(); showShapeCode(g);
}
function shrinkStone(g){
  /* each tap makes the shape one ruby smaller, until nothing */
  var r = (g.userData.rubies||1) - 1;
  if(r < 1){ liftStone(g); return; }
  g.userData.rubies = r;
  buildStoneVisual(g); save(); showShapeCode(g);
}
function liftStone(g){
  stones.remove(g); save();
}
var anims = [];
var dying = []; /* self-destruct: stones crumbling out */
function selfDestruct(){
  /* the whole frame self-destructs — every stone crumbles at once.
     bodies (his, fellow travelers') live outside the stones group: untouched. */
  var fs = [];
  for(var fi=0; fi<stones.children.length; fi++){
    var st0 = stones.children[fi], u0 = st0.userData;
    fs.push({x:st0.position.x, z:st0.position.z, r:u0.rubies||1, s:u0.shape||'cube', v:u0.ghost?1:0, y:u0.y||0, p:u0.proc||null});
    dying.push({m:st0, t:0, sx:st0.scale.x, sy:st0.scale.y, sz:st0.scale.z});
  }
  if(fs.length){ undoStack.push({victim:'field', killer:null, mass:0, stones:fs}); if(undoStack.length>10) undoStack.shift(); saveUndo(); }
  clearSelection(); updateAnchors();
  return fs.length;
}
function easeOutBack(t){ var c=1.70158; return 1 + (c+1)*Math.pow(t-1,3) + c*Math.pow(t-1,2); }

/* the radiating intaglio disc, laid on the ground under the largest structure */
function makeRadiateTex(){
  var c = document.createElement('canvas'); c.width = c.height = 512;
  var x = c.getContext('2d');
  x.clearRect(0,0,512,512);
  x.strokeStyle = 'rgba(255,215,0,0.6)'; x.lineWidth = 1.5;
  for(var r=14;r<256;r+=12){ x.beginPath(); x.arc(256,256,r,0,Math.PI*2); x.stroke(); }
  x.strokeStyle = 'rgba(255,215,0,0.35)';
  for(var a=0;a<48;a++){
    var t = a/48*Math.PI*2;
    x.beginPath(); x.moveTo(256+Math.cos(t)*14, 256+Math.sin(t)*14);
    x.lineTo(256+Math.cos(t)*250, 256+Math.sin(t)*250); x.stroke();
  }
  x.strokeStyle = 'rgba(255,215,0,0.22)';
  for(var r2=20;r2<256;r2+=12){
    x.beginPath();
    for(var a2=0;a2<=Math.PI*2+0.02;a2+=0.06){
      var rr = r2 + Math.sin(a2*12)*4;
      var p = [256+Math.cos(a2)*rr, 256+Math.sin(a2)*rr];
      if(a2===0) x.moveTo(p[0],p[1]); else x.lineTo(p[0],p[