<!-- RUBY-BACKUP · part 149 -->
atus('no saves yet'); return; }
  codeStatus('saves: '+ks.map(function(k){ return k+' ('+SAVES[k].stones.length+')'; }).join(', '));
}
/* did: no audio, just a small "this is what you did" — 1. 2. 3. — so the progress stays visible. */
var didLog = [];
var pendingDid = null;
function loadDid(){ try{ didLog = JSON.parse(localStorage.getItem('ruby-did-v1')||'[]'); }catch(e){ didLog=[]; } }
function persistDid(){ try{ localStorage.setItem('ruby-did-v1', JSON.stringify(didLog.slice(-8))); }catch(e){} }
function escapeHtml(s){ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function loadAnchors(){ try{ lockedAnchors = JSON.parse(localStorage.getItem('ruby-anchors-v1')||'[]'); }catch(e){ lockedAnchors=[]; } }
function persistAnchors(){ try{ localStorage.setItem('ruby-anchors-v1', JSON.stringify(lockedAnchors)); }catch(e){} }
function drawAnchorRings(){
  for(var i=anchorGrp.children.length-1;i>=0;i--) anchorGrp.remove(anchorGrp.children[i]);
  lockedAnchors.forEach(function(a){
    var ring = new T.Mesh(new T.RingGeometry(1.0, 1.25, 40),
      new T.MeshBasicMaterial({color:0xffe27a, transparent:true, opacity:0.85, side:T.DoubleSide}));
    ring.rotation.x = -Math.PI/2;
    ring.position.set(a.x, 0.12+(a.y||0)*0.13, a.z);
    anchorGrp.add(ring);
  });
}
function tapLock(grp){
  awaitingLock = false;
  var a = {x:grp.position.x, z:grp.position.z, y:grp.userData.y||0};
  var exists = lockedAnchors.some(function(e){ return Math.abs(e.x-a.x)<0.01 && Math.abs(e.z-a.z)<0.01 && Math.abs((e.y||0)-a.y)<0.01; });
  if(!exists) lockedAnchors.push(a);
  cursor.x = a.x; cursor.z = a.z; saveCursor(); paintCursor();
  persistAnchors(); drawAnchorRings();
  codeStatus('anchor locked in');
}
function tapUnlock(grp){
  awaitingUnlock = false;
  var px=grp.position.x, pz=grp.position.z, py=grp.userData.y||0, idx=-1;
  for(var i=0;i<lockedAnchors.length;i++){ var e=lockedAnchors[i];
    if(Math.abs(e.x-px)<1.5 && Math.abs(e.z-pz)<1.5 && Math.abs((e.y||0)-py)<0.01){ idx=i; break; } }
  if(idx<0){ codeStatus('that is not a locked anchor'); return; }
  lockedAnchors.splice(idx,1);
  persistAnchors(); drawAnchorRings();
  codeStatus('anchor released');
}
/* portals — an anchor of a block as a portal to a build site.
   `portal <id>`, tap the block; tapping it again opens the watch —
   you meet me there, and we play together. */
function loadPortals(){ try{ portals = JSON.parse(localStorage.getItem('ruby-portals-v1')||'[]'); }catch(e){ portals=[]; } }
function persistPortals(){ try{ localStorage.setItem('ruby-portals-v1', JSON.stringify(portals)); }catch(e){} }
function drawPortals(){
  for(var i=portalGrp.children.length-1;i>=0;i--) portalGrp.remove(portalGrp.children[i]);
  portals.forEach(function(p){
    var ring = new T.Mesh(new T.RingGeometry(1.15, 1.45, 40),
      new T.MeshBasicMaterial({color:0x7df9ff, transparent:true, opacity:0.9, side:T.DoubleSide}));
    ring.rotation.x = -Math.PI/2;
    ring.position.set(p.x, 0.12+(p.y||0)*0.13, p.z);
    portalGrp.add(ring);
  });
}
function portalAt(grp){
  var px=grp.position.x, pz=grp.position.z, py=grp.userData.y||0;
  for(var i=0;i<portals.length;i++){ var e=portals[i];
    if(Math.abs(e.x-px)<1.5 && Math.abs(e.z-pz)<1.5 && Math.abs((e.y||0)-py)<0.01) return e; }
  return null;
}
function tapPortal(grp){
  var id = awaitingPortal; awaitingPortal = null;
  portals.push({x:grp.position.x, z:grp.position.z, y:grp.userData.y||0, watch:id});
  persistPortals(); drawPortals();
  codeStatus('portal open — tap it to meet me there');
}
function tapUnportal(grp){
  awaitingUnportal = false;
  var e = portalAt(grp);
  if(!e){ codeStatus('that is not a portal'); return; }
  portals.splice(portals.indexOf(e),1);
  persistPortals(); drawPortals();
  codeStatus('portal closed');
}
/* the Brain — keeper of every code. punch 'brain' and it remembers for you. */
var BRAIN = [
 ['day / night / storm / cosmic / cave / dawn / ember / aurora','the eight skies'],
 ['rain','the Rain Law — totality clouds + rain'],
 ['clouds','clouds alone (never kills rain)'],
 ['whiteout','hold the sky white — two codes at once, on purpose'],
 ['floor','lay the stone layer — the whole is 1'],
 ['land','raise the landscape — our feet rest upon it'],
 ['dig','carve the earth open where you stand'],
 ['come','call Solid — he flies to you'],
 ['go','send Solid back to his post'],
 ['new','something new grows — a surprise'],
 ['eyeslive','eyes non-stop \u2014 eyesoff ends it'],
  ['eyeson / eyesoff','let Solid see every load, or look away'],
  ['eyes','share your view — Solid sees'],
 ['babystep','one step, for my children'],
 ['edge','to the bottom of the map'],
 ['b1','home — to the ruby at the center'],
 ['brane','the membrane view — low, gazing up'],
 ['walk','a body — joystick runs, triangle jumps'],
 ['fly','the sword — drag aims 360, you follow'],
 ['crown','first-person inside the crown'],
 ['mirror','the camera stands before you'],
 ['king / nokings