<!-- RUBY-BACKUP · part 391 -->
(){ el.classList.add('idle'); }, 10000);
}
function codeTextHide(){
  var el = document.getElementById('codetext');
  if(el) el.classList.remove('show');
  clearTimeout(codeIdleT);
}

/* ---- room chat: talk without leaving ---- */
(function(){
  var cb = document.getElementById('chatbtn');
  if(!cb) return;
  kbReset();
  cb.addEventListener('click', function(){
    chatOpen = !chatOpen;
    if(chatOpen){
      kbUse('chat'); pullChat();
      codeOpen = false; codeTextHide();
      cursorDot.visible=false;
    }
    syncDock();
  });
  document.getElementById('chatsend').addEventListener('click', function(){ sendChat(); });
  var cinput = document.getElementById('chatinput');
  cinput.addEventListener('keydown', function(e){ if(e.key==='Enter') sendChat(); });
  cinput.addEventListener('focus', function(){ document.body.classList.add('chatting'); });
  cinput.addEventListener('blur', function(){ document.body.classList.remove('chatting'); });
  /* ---- go: a collapsed place for a link, just in case ---- */
  var gb = document.getElementById('gobtn'), gr = document.getElementById('gorow'), gi = document.getElementById('goinput');
  function goLink(){
    var v = gi.value.trim();
    if(!v) return;
    var url;
    if(/^[a-z0-9-]{1,32}$/i.test(v)) url = '/ruby/?watch='+encodeURIComponent(v.toLowerCase());
    else if(/^https?:\/\//i.test(v)) url = v;
    else if(v.charAt(0)==='/') url = v;
    else url = 'https://'+v;
    location.href = url;
  }
  gb.addEventListener('click', function(){
    var open = gr.classList.toggle('open');
    if(open) gi.focus(); else gi.blur();
  });
  document.getElementById('gosend').addEventListener('click', goLink);
  gi.addEventListener('keydown', function(e){ if(e.key==='Enter') goLink(); });
  gi.addEventListener('focus', function(){ document.body.classList.add('chatting'); });
  gi.addEventListener('blur', function(){ document.body.classList.remove('chatting'); });

/* ---- our keyboard: compact, gold, loaded from the website we made ---- */
})();
document.getElementById('flybtn').addEventListener('click', function(){ if(!WALK) setWalk(true); setFly(!FLYM); });
document.getElementById('jumpbtn').addEventListener('pointerdown', function(e){ e.preventDefault(); jumpQ = true; });
(function(){
  var joy = document.getElementById('joy'), knob = document.getElementById('joyknob'), jid = null;
  function setKnob(dx, dy){
    knob.style.transform = 'translate('+dx+'px,'+dy+'px)';
    joyX = dx/40; joyY = -dy/40;
  }
  joy.addEventListener('pointerdown', function(e){ jid = e.pointerId; try{ joy.setPointerCapture(jid); }catch(err){} setKnob(0,0); });
  joy.addEventListener('pointermove', function(e){
    if(e.pointerId!==jid) return;
    var r = joy.getBoundingClientRect();
    var dx = e.clientX-(r.left+r.width/2), dy = e.clientY-(r.top+r.height/2);
    var m = Math.hypot(dx,dy);
    if(m>40){ dx*=40/m; dy*=40/m; }
    setKnob(dx,dy);
  });
  function jend(e){ if(e.pointerId===jid){ jid = null; setKnob(0,0); } }
  joy.addEventListener('pointerup', jend); joy.addEventListener('pointercancel', jend);
})();

/* ---- render options: one button opens them all ---- */
var renderBtn = document.getElementById('render-btn');
var renderPanel = document.getElementById('renderpanel');
RENDER_MODES.forEach(function(m){
  var b = document.createElement('button');
  b.innerHTML = RENDER_ICONS[m];
  b.setAttribute('aria-label', RENDER_LABELS[m]);
  b.dataset.rmode = m;
  b.addEventListener('click', function(e){ e.stopPropagation(); setRenderMode(m); });
  renderPanel.appendChild(b);
});
function paintRenderUI(){
  renderBtn.innerHTML = RENDER_ICONS[renderMode];
  var kids = renderPanel.children;
  for(var i=0;i<kids.length;i++) kids[i].classList.toggle('sel', kids[i].dataset.rmode===renderMode);
}
function setRenderMode(m){
  renderMode = m;
  try{ localStorage.setItem('ruby-render-mode', m); }catch(e){}
  paintRenderUI();
  renderPanel.classList.remove('open');
  stones.children.forEach(buildStoneVisual);
  updateAnchors();
}
renderBtn.addEventListener('click', function(e){
  e.stopPropagation();
  closeShapePanel();
  renderPanel.classList.toggle('open');
});
paintRenderUI();
/* ---- the one button: a refresh is only ever a new load of the SCREEN, never the game ---- */
document.getElementById('refresh').addEventListener('click', function(){
  /* the stones persist in localStorage and return untouched — the game is never cleared.
     the camera stays where you left it; only the screen loads fresh. */
  saveCam();
  /* his law: refresh always brings him back in the king body, crown camera on.
     the room (watch) and everything else ride along. */
  var u = new URL(location.href);
  u.searchParams.set('body', 'king');
  u.searchParams.delete('crown');
  u.searchParams.set('walk', '1');
  u.searchParams.set('far', '1');
  u.searchParams.set('fresh', Date.now());
  location.replace(u.toString());
});

/* ---- frame ---- */
document.getElementById('boot').style.d