<!-- RUBY-BACKUP · part 388 -->
4 24"><path d="M12 4 L18 8 L18 16 L12 20 L6 16 L6 8 Z" fill="none" stroke="#ffd700" stroke-width="2"/><path d="M12 4 L12 20 M6 8 L18 16 M18 8 L6 16" fill="none" stroke="#ffd700" stroke-width="1"/></svg>',
  dodeca: '<svg viewBox="0 0 24 24"><path d="M12 4 L19 9 L16 18 L8 18 L5 9 Z" fill="none" stroke="#ffd700" stroke-width="2"/><path d="M12 4 L12 12 M5 9 L12 12 M19 9 L12 12 M8 18 L12 12 M16 18 L12 12" fill="none" stroke="#ffd700" stroke-width="1"/></svg>',
  tri: '<svg viewBox="0 0 24 24"><path d="M6 19 L18 19 L6 6 Z" fill="none" stroke="#ffd700" stroke-width="2"/></svg>',
  pdown: '<svg viewBox="0 0 24 24"><path d="M12 19 L5 5 L19 5 Z" fill="none" stroke="#ffd700" stroke-width="2"/></svg>',
  poff: '<svg viewBox="0 0 24 24"><path d="M14 5 L19 19 L5 19 Z" fill="none" stroke="#ffd700" stroke-width="2"/></svg>',
  poffdown: '<svg viewBox="0 0 24 24"><path d="M10 19 L5 5 L19 5 Z" fill="none" stroke="#ffd700" stroke-width="2"/></svg>'
};
var SHAPE_ORDER = [];
['cube','pyramid','tri','pdown','poff','poffdown','sphere','cyl','torus','cone','octa','tetra','icosa','dodeca'].forEach(function(s){ SHAPE_ORDER.push(s, s+'~ghost'); });
var SHAPE_LABELS = {cube:'Cube',pyramid:'Pyramid',sphere:'Sphere',cyl:'Cylinder',torus:'Torus',cone:'Cone',octa:'Octahedron',tetra:'Tetrahedron',icosa:'Icosahedron',dodeca:'Dodecahedron',tri:'Triangle',pdown:'Upside Down Pyramid',poff:'Offset Pyramid',poffdown:'Offset Upside Down Pyramid'};
var SHAPE_CODES = {cube:'c',pyramid:'p',sphere:'s',cyl:'l',torus:'t',cone:'v',octa:'o',tetra:'r',icosa:'i',dodeca:'d',tri:'g',pdown:'u',poff:'f',poffdown:'w'};
/* equal and opposite: every shape has its intaglio-ghost twin */
function shapeBase(key){ return String(key||'cube').split('~')[0]; }
function shapeIsGhost(key){ return String(key||'').indexOf('~ghost') >= 0; }
function ghostIcon(svg){ return String(svg).replace(/#ffd700/g,'#caa64b').replace(/stroke-width=/g,'stroke-dasharray="3 2" stroke-width='); }
function shapeLabel(key){
  var b = shapeBase(key), l = SHAPE_LABELS[b]||b;
  return shapeIsGhost(key) ? l+' ghost' : l;
}
var curShapeKey = 'cube';
function shapeGeo(shape, side){
  /* every shape is built at 1-ruby size, then settled so its base rests at -side/2 */
  var g;
  if(shape==='pyramid'){ g = new T.ConeGeometry(side*0.7071, side, 4); g.rotateY(Math.PI/4); }
  else if(shape==='sphere') g = new T.SphereGeometry(side/2, 20, 14);
  else if(shape==='cyl') g = new T.CylinderGeometry(side/2, side/2, side, 20);
  else if(shape==='torus'){ g = new T.TorusGeometry(side*0.32, side*0.18, 12, 28); g.rotateX(Math.PI/2); }
  else if(shape==='cone') g = new T.ConeGeometry(side/2, side, 24);
  else if(shape==='octa') g = new T.OctahedronGeometry(side/2);
  else if(shape==='tetra') g = new T.TetrahedronGeometry(side/2);
  else if(shape==='icosa') g = new T.IcosahedronGeometry(side/2);
  else if(shape==='dodeca') g = new T.DodecahedronGeometry(side/2);
  else if(shape==='tri'){
    var tsh = new T.Shape();
    tsh.moveTo(-side/2,-side/2); tsh.lineTo(side/2,-side/2); tsh.lineTo(-side/2,side/2); tsh.closePath();
    g = new T.ExtrudeGeometry(tsh, {depth:side, bevelEnabled:false});
    g.translate(0,0,-side/2);
  }
  else if(shape==='pdown'){ g = new T.ConeGeometry(side*0.7071, side, 4); g.rotateY(Math.PI/4); g.rotateZ(Math.PI); }
  else if(shape==='poff'){ g = new T.ConeGeometry(side*0.7071, side, 4); g.rotateY(Math.PI/4); shearGeo(g, side*0.5); }
  else if(shape==='poffdown'){ g = new T.ConeGeometry(side*0.7071, side, 4); g.rotateY(Math.PI/4); shearGeo(g, side*0.5); g.rotateZ(Math.PI); }
  else g = new T.BoxGeometry(side, side, side);
  g.computeBoundingBox();
  g.translate(0, -g.boundingBox.min.y - side/2, 0);
  return g;
}
/* the offset, found: shear proportional to height — the lean flows, fluid, no kinks.
   the base stays flat (t=0 unshifted) so the stone still stacks properly. */
function shearGeo(g, amt){
  var pos = g.attributes.position, i, y, ymin=1e9, ymax=-1e9;
  for(i=0;i<pos.count;i++){ y=pos.getY(i); if(y<ymin)ymin=y; if(y>ymax)ymax=y; }
  var span = (ymax-ymin)||1;
  for(i=0;i<pos.count;i++){ var t=(pos.getY(i)-ymin)/span; pos.setX(i, pos.getX(i)+amt*t); }
  pos.needsUpdate = true;
  return g;
}
/* the law, per shape: the legal scale of the 1-ruby base (1 for uniform shapes,
   more for shapes whose smallest dimension would otherwise dip under Minimum) */
function legalK1(shape){
  if(!shapeK1[shape]){
    var m = new T.Mesh(shapeGeo(shape, window.MINIMUM));
    window.applyMinimum(m);
    shapeK1[shape] = m.scale.x;
    m.geometry.dispose();
  }
  return shapeK1[shape];
}
var shapeBtn = document.getElementById('shape-btn');
var shapePanel = document.getElementById('shapepanel');
SHAPE_ORDER.forEach(function(k){
  var b = document.createElement('button');
  var base = shapeBase(k), ghostly = shapeIsGhost(k);
  var letter = SHAPE_CODES[base]||'?';
  b.innerHTML = (ghostly ? ghostIcon(SHAPE_ICONS[base]) : SHAPE_ICONS[base]) +
    '<div style="font-size:9px;co