<!-- RUBY-BACKUP · part 393 -->
 shape at cursor.             e.g. b
     x <x> <z>  |  x            remove at cell / at cursor.
     oops                        bring back the last project rendered obsolete.
     oops-my-bad                 arm the anchor: tap one stone, every stone connected
                                 to it is offered — `swap <code>` restyles the caps,
                                 `cut` removes, `keep` releases.
     save <name> / saves         name the selection — it becomes a word. `saves`
                                 lists your library; punch a name to build it anywhere.
     newmap                      a fresh field. old saves travel with you;
                                 `oops` brings the old field back.
     send <name> / take <code>    send your build to your friends — the code
                                 IS the build. they `take` it, it joins their library.
     did                         no audio — just "this is what you did": 1. 2. 3.,
                                 the progress, staying visible. tap a row, tap an
                                 anchor stone — the code runs right there.
     lock / unlock / anchors      lock in to new anchor points — tap stones, gold
                                 rings mark them; `anchors` counts them.
     portal <id> / unportal       an anchor of a block as a portal to a build site —
                                 tap the block, tap it again and you meet me there.
   Repeat:     3(b2r)            run the inner line N times.
   Procedure:  tower, wall...    (MARK: PROCEDURES)
   Flip:       flip <proc>       mirrored expansion. (MARK: MIRROR)
   Multi:      split on ';'.     e.g. l 8 10 10; c 4 0 10
   Stamps: procedures build AT the cursor but leave it where it stood —
           atoms move the cursor, procedures do not. castle; castle steps over.
   EDIT: token shapes, new verbs, new directions.
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
var DIRS = {r:[1,0], l:[-1,0], u:[0,-1], d:[0,1]};   /* 1 block = 1 grid cell = 2 units */
var BLOCK = 2;

function parseLine(line, ec){
  var ops = [], parts = String(line).split(';');
  for(var pi=0; pi<parts.length; pi++){
    var p = parts[pi].trim();
    if(!p) continue;
    var sub = parseOne(p, 0, ec);
    if(!sub.ok) return sub;
    ops = ops.concat(sub.ops);
  }
  return {ok:true, ops:ops};
}
function parseOne(p, depth, ec){
  var m = p.match(/^(\d+)\s*\(([^()]*)\)$/);          /* repeat: 3(...) */
  if(m){
    var n = parseInt(m[1],10), inner = m[2].trim();
    if(!(n>=1 && n<=HARDEN.MAX_REPEAT)) return {ok:false, error:'repeat out of range'};
    if(depth+1 > HARDEN.MAX_DEPTH) return {ok:false, error:'too deep'};
    var ops = [];
    for(var i=0;i<n;i++){
      var r = parseOne(inner, depth+1, ec);
      if(!r.ok) return r;
      ops = ops.concat(r.ops);
    }
    return {ok:true, ops:ops};
  }
  var f = p.match(/^flip\s+([a-zA-Z][a-zA-Z0-9_-]*)$/);  /* flip: mirror a procedure */
  if(f){
    var cx0 = ec.x;
    var e = expandProc(f[1], ec, depth);
    if(!e.ok) return e;
    return {ok:true, ops:mirrorOps(e.ops, cx0)};
  }
  if(PROCEDURES[p]) return expandProc(p, ec, depth);  /* procedure word */
  if(SAVES[p]) return {ok:true, ops:saveOps(p, ec)};  /* a saved build, your word */
  return parseAtom(p, ec);                            /* atom */
}
function parseAtom(p, ec){
  var t = p.trim();
  if(/^x$/i.test(t)) return {ok:true, ops:[{remove:true, x:ec.x, z:ec.z}]};
  var xm = t.match(/^x\s+(-?\d+)\s+(-?\d+)$/i);
  if(xm) return {ok:true, ops:[{remove:true, x:snap(+xm[1]), z:snap(+xm[2])}]};
  var rm = t.match(/^([a-zA-Z])(\d*)\s*(\d+)([rlud])$/);   /* relative: b2r, l8 2r */
  if(rm){
    var key = codeToShapeKey(rm[1]); if(!key) return {ok:false, error:'unknown code'};
    var size = rm[2] ? parseInt(rm[2],10) : 1;
    var n = parseInt(rm[3],10), d = DIRS[rm[4]];
    var nx = ec.x + d[0]*n*BLOCK, nz = ec.z + d[1]*n*BLOCK;
    ec.x = nx; ec.z = nz;
    return {ok:true, ops:[{shape:key, size:size, x:nx, z:nz, y:0, proc:null}]};
  }
  var bm = t.match(/^([a-zA-Z])$/);                        /* bare: seed at cursor */
  if(bm){
    var key2 = codeToShapeKey(bm[1]); if(!key2) return {ok:false, error:'unknown code'};
    return {ok:true, ops:[{shape:key2, size:1, x:ec.x, z:ec.z, y:0, proc:null}]};
  }
  var sm = t.match(/^([a-zA-Z])(\d*)\^$/);                   /* stack: on the stone at cursor */
  if(sm){
    var keyS = codeToShapeKey(sm[1]); if(!keyS) return {ok:false, error:'unknown code'};
    var sizeS = sm[2] ? parseInt(sm[2],10) : 1;
    var top = 0;
    for(var si=0; si<stones.children.length; si++){
      var ss = stones.children[si], su = ss.userData;
      if(Math.abs(ss.position.x-ec.x)<0.01 && Math.abs(ss.position.z-ec.z)<0.01)
        top = Math.max(top, (su.y||0) + (su.rubies||1)*(su.k1||1));
    }
    return {ok:true, ops:[{shape:keyS, size:sizeS, x:ec.x, z:ec.z, y:top, proc:null}]};
  }
  var am4 = t.match(/^([a-zA-Z])\s+(-?\d