<!-- PLAYA · part 10 -->
on esc(s){ return String(s).replace(/[&<>"]/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]; }); }
function refresh(){
  var o = selected;
  if(o && blocks.indexOf(o) < 0){ selected = o = null; }
  selBox.visible = !!o && o.visible; if(o) selBox.setFromObject(o);
  /* tool + toggle states */
  document.querySelectorAll('[data-act="tool"]').forEach(function(b){ b.classList.toggle('on', b.getAttribute('data-arg') === tool); });
  document.querySelectorAll('[data-act="grid"]').forEach(function(b){ b.classList.toggle('on', parseInt(b.getAttribute('data-arg'),10) === gridDiv); });
  document.querySelectorAll('[data-act="isolate"]').forEach(function(b){ b.classList.toggle('on', !!isolated); });
  document.querySelectorAll('[data-act="shadows"]').forEach(function(b){ b.classList.toggle('on', sun.castShadow); });
  document.querySelectorAll('[data-act="fog"]').forEach(function(b){ b.classList.toggle('on', scene.fog.far < 1000); });
  document.querySelectorAll('[data-act="spawn"]').forEach(function(b){ b.classList.toggle('on', b.getAttribute('data-arg') === selectedShape); });
  var flags = o ? {section: o.userData.section, hollow: meshesOf(o)[0].material.wireframe, sharpen: meshesOf(o)[0].material.flatShading, spin: !!o.userData.spin, bob: !!o.userData.bob} : {};
  ['section','hollow','sharpen','spin','bob'].forEach(function(k){ document.querySelectorAll('[data-act="' + k + '"]').forEach(function(b){ b.classList.toggle('on', !!flags[k]); }); });
  document.querySelectorAll('[data-act="res"]').forEach(function(b){ b.classList.toggle('on', !!o && parseInt(b.getAttribute('data-arg'),10) === o.userData.res); });
  /* metadata */
  var size = o ? new THREE.Vector3() : null; if(o) boxOf(o).getSize(size);
  var m0 = o ? meshesOf(o)[0].material : null;
  document.getElementById('meta').innerHTML = o ?
    '<b>Type</b><span>' + esc(label(o)) + '</span><b>Material</b><span>#' + m0.color.getHexString().toUpperCase() + ' · rough ' + m0.roughness.toFixed(2) + ' · metal ' + m0.metalness.toFixed(2) + '</span>' +
    '<b>Size</b><span>' + size.x.toFixed(2) + ' × ' + size.y.toFixed(2) + ' × ' + size.z.toFixed(2) + '</span>' +
    '<b>Mesh</b><span>' + polyCount(o) + ' triangles · ' + vertCount(o) + ' verts · res ' + o.userData.res + '×</span>' +
    '<b>Blocks</b><span>' + blocks.length + ' in scene</span>'
    : '<b>Type</b><span>nothing selected — tap a block</span><b>Blocks</b><span>' + blocks.length + ' in scene</span>';
  document.querySelectorAll('[data-xf]').forEach(function(inp){
    if(document.activeElement === inp) return;
    inp.disabled = !o;
    if(!o){ inp.value = ''; return; }
    var k = inp.getAttribute('data-xf').split('.');
    var v = k[0] === 'p' ? o.position[k[1]] : k[0] === 'r' ? o.rotation[k[1]] * 180 / Math.PI : o.scale[k[1]];
    inp.value = Math.round(v * 100) / 100;
  });
  document.querySelectorAll('[data-slider]').forEach(function(inp){
    if(!m0) return; var k = inp.getAttribute('data-slider');
    inp.value = k === 'roughness' ? m0.roughness : k === 'metalness' ? m0.metalness : k === 'opacity' ? m0.opacity : m0.emissiveIntensity;
  });
  /* phone metadata card */
  document.getElementById('card').innerHTML = '<span class="ttl">ARTIST WORKFLOW &amp; METADATA ›</span>' +
    (o ? '<span>Type: ' + esc(label(o)) + '</span><span>Pos: ' + fmtPos(o) + '</span><span>Mesh: ' + polyCount(o) + ' tris · Size ' + size.y.toFixed(1) + ' tall</span>'
       : '<span>No block selected</span><span>Tap a block, or Mesh Primitives to add one</span>') +
    '<span>History: ' + undoStack.length + ' step(s)' + (redoStack.length ? ' · ' + redoStack.length + ' redo' : '') + '</span>';
  /* history timeline */
  var h = '';
  for(var i=redoStack.length-1;i>=0;i--) h += '<li class="redo">↷ ' + esc(redoStack[i].label) + '</li>';
  for(var j=undoStack.length-1;j>=Math.max(0, undoStack.length-25);j--) h += '<li>' + esc(undoStack[j].label) + '</li>';
  document.getElementById('hist').innerHTML = h || '<li>No steps yet.</li>';
  /* layers */
  document.getElementById('layerlist').innerHTML = blocks.length ? blocks.map(function(b){
    return '<div class="layer"><button class="btn name' + (b === o ? ' on' : '') + '" data-act="layersel" data-arg="' + b.userData.id + '">' + esc(label(b)) + '</button>' +
      '<button class="btn' + (b.visible ? ' on' : '') + '" data-act="layervis" data-arg="' + b.userData.id + '" aria-label="visible">' + (b.visible ? '👁' : '—') + '</button>' +
      '<button class="btn" data-act="layersel" data-arg="' + b.userData.id + '">Pick</button></div>';
  }).join('') : '<p class="note">No blocks yet.</p>';
}
function renderLib(){
  var lib = readLib();
  document.getElementById('liblist').innerHTML = lib.length ? lib.map(function(r, i){
    return '<div class="layer" style="grid-template-columns:1fr 64px"><button class="btn name" data-act="libspawn" data-arg="' + i + '">＋ ' + esc(r.name) + '</button>' +
      '<button class="btn" data-act="libremove" da