<!-- PLAYA · part 7 -->
: function(){ sun.castShadow = !sun.castShadow; say(sun.castShadow ? 'Shadows on.' : 'Shadows off.'); refresh(); },
  fog: function(){ scene.fog.far = scene.fog.far > 1000 ? 420 : 100000; scene.fog.near = scene.fog.far > 1000 ? 99999 : 120; say(scene.fog.far > 1000 ? 'Fog off.' : 'Fog on.'); refresh(); },
  spin: function(){ if(edit('Spin', function(o){ o.userData.spin = o.userData.spin ? 0 : 0.8; })) say(selected.userData.spin ? 'Spinning ' + label(selected) + '.' : 'Spin stopped.'); },
  bob: function(){ if(edit('Float', function(o){ o.userData.bob = o.userData.bob ? 0 : 1; o.userData.bobBase = o.position.y; })) say(selected.userData.bob ? 'Floating ' + label(selected) + '.' : 'Float stopped.'); },
  stopanim: function(){ var n = 0; blocks.forEach(function(b){ if(b.userData.spin || b.userData.bob){ n++; b.userData.spin = 0; if(b.userData.bob){ b.userData.bob = 0; b.position.y = b.userData.bobBase; } } }); say(n ? 'Stopped ' + n + ' animation(s).' : 'Nothing is animating.'); refresh(); },
  libsave: function(){
    var o = need(); if(!o) return;
    var lib = readLib(); var r = recipe(o); r.name = label(o) + ' · ' + new Date().toLocaleDateString();
    lib.unshift(r); writeLib(lib); say('Added ' + label(o) + ' to the Library.'); renderLib();
  },
  libspawn: function(i){
    var r = readLib()[parseInt(i, 10)]; if(!r){ say('That library item is gone.', true); return; }
    var b = fromRecipe(r);
    if(!placeFree(b, orbit.target.x, orbit.target.z)){ say('Cannot place — no free space (no overlap).', true); return; }
    attach(b); pushHist({label: 'Add from Library', kind: 'add', obj: b}); select(b); say('Placed ' + r.name + '.');
  },
  libremove: function(i){
    var lib = readLib(); var r = lib.splice(parseInt(i, 10), 1)[0];
    if(!r) return; if(!confirm('Remove "' + r.name + '" from the Library?')) return;
    writeLib(lib); say('Removed ' + r.name + ' from the Library.'); renderLib();
  },
  savescene: function(){
    localStorage.setItem(SCENE_KEY, JSON.stringify({v: 1, blocks: blocks.map(recipe), saved: Date.now()}));
    say('Scene saved on this device — ' + blocks.length + ' block(s).');
  },
  loadscene: function(){
    var s = localStorage.getItem(SCENE_KEY); if(!s){ say('No saved scene on this device yet.', true); return; }
    if(blocks.length && !confirm('Load the saved scene? The blocks in the view now will be replaced.')) return;
    var d = JSON.parse(s);
    blocks.slice().forEach(detach); undoStack.length = 0; redoStack.length = 0; isolated = null;
    d.blocks.forEach(function(r){ attach(fromRecipe(r)); });
    select(null); say('Loaded scene — ' + d.blocks.length + ' block(s) from ' + new Date(d.saved).toLocaleString() + '.');
  },
  png: function(m){ exportPNG(parseFloat(m)); },
  obj: function(){ exportOBJ(); },
  /* layout-only actions */
  sheet: function(k){ openSheet(k); },
  closesheet: function(){ openSheet(null); },
  tab: function(t){ setTab(t); },
  openexport: function(){ setTab('export'); say('Pick PNG, PNG HD or OBJ.'); }
};
function byId(id){ id = parseInt(id, 10); for(var i=0;i<blocks.length;i++) if(blocks[i].userData.id === id) return blocks[i]; return null; }

/* ONE delegated click handler for every button on the page. */
document.addEventListener('click', function(e){
  var el = e.target.closest ? e.target.closest('[data-act]') : null;
  if(!el) return;
  var act = el.getAttribute('data-act'), arg = el.getAttribute('data-arg');
  var fn = ACT[act];
  if(!fn){ say('⚠ Button "' + act + '" has no action.', true); return; }
  try { fn(arg); }
  catch(err){ console.error(err); say('⚠ ' + act + ' failed: ' + err.message, true); }
});

/* ---------- SLIDERS (material) ---------- */
var sliderBefore = null;
document.querySelectorAll('[data-slider]').forEach(function(inp){
  var key = inp.getAttribute('data-slider');
  inp.addEventListener('input', function(){
    if(!selected){ say('Select a block first.', true); return; }
    if(!sliderBefore) sliderBefore = {obj: selected, sn: snap(selected)};
    var v = parseFloat(inp.value);
    eachMat(selected, function(mt){
      if(key === 'roughness') mt.roughness = v;
      else if(key === 'metalness') mt.metalness = v;
      else if(key === 'opacity'){ mt.opacity = v; mt.transparent = v < 1; mt.depthWrite = v >= 1; }
      else if(key === 'glow'){ mt.emissive.copy(mt.color); mt.emissiveIntensity = v; }
    });
  });
  inp.addEventListener('change', function(){
    if(!sliderBefore) return;
    pushHist({label: key + ' ' + inp.value, kind: 'state', obj: sliderBefore.obj, before: sliderBefore.sn, after: snap(sliderBefore.obj)});
    sliderBefore = null; say(key + ' ' + inp.value + ' on ' + label(selected) + '.'); refresh();
  });
});
/* lighting sliders */
function syncLightSliders(){
  document.getElementById('sunI').value = sun.intensity; document.getElementById('sunAz').value = sunAz; document.getElementById('ambI').value = hemi.intensity;
}
document.getElementById('sunI').addEventListener('input', fu