<!-- RUBY-BACKUP · part 56 -->
(2), dir: g.dir}; }),
      ropes: sd.parts.ropes.length > 0,
      web: !!sd.parts.web,
      webLatch: 4
    };
    var all = loadTemplates();
    all[name] = t;
    saveTemplates(all);
    status('template "' + name + '" saved — ' + t.pulleys.length + ' pulleys, ' + t.gears.length + ' gears.');
  }

  function listTemplates(){
    var all = loadTemplates();
    var names = Object.keys(all);
    if(!names.length){ status('no templates saved — grow a machine, `seed save <name>`'); return; }
    status(names.length + ' templates: ' + names.join(', '));
  }

  function forgetTemplate(name){
    name = cleanName(name);
    var all = loadTemplates();
    if(!all[name]){ status('no template "' + name + '"'); return; }
    delete all[name];
    saveTemplates(all);
    status('template "' + name + '" forgotten.');
  }

  function growFromTemplate(sd, tpl){
    if(!sd || !sd.pos) return;
    /* pulleys at relative offsets */
    tpl.pulleys.forEach(function(p, i){
      buildPulleyAt(sd.pos.x + p.dx, sd.pos.z + p.dz, i, sd);
    });
    setTimeout(function(){
      tpl.gears.forEach(function(g){
        buildGearAt(sd.pos.x + g.dx, sd.pos.z + g.dz, g.dir, sd);
      });
    }, 500);
    setTimeout(function(){
      if(tpl.ropes) buildRopesFor(sd);
    }, 1400);
    setTimeout(function(){
      if(tpl.web){
        try{
          if(typeof window.spiderDist === 'function') window.spiderDist(tpl.webLatch || 4);
          if(sd.pos && typeof window.__weaveAt === 'function') window.__weaveAt(sd.pos);
          sd.parts.web = true;
        }catch(e){}
      }
    }, 2200);
    driveLoop();
    status('template "' + (sd.templateName || '') + '" blooms from the seed.');
  }

  function armPlantTemplate(name){
    name = cleanName(name);
    var all = loadTemplates();
    if(!all[name]){ status('no template "' + name + '" — `seed templates` to see'); return; }
    plantTplArmed = all[name];
    plantTplArmed.name = name;
    status('template "' + name + '" armed — tap any block to plant it. it grows on its own.');
  }

  /* ============================================================================
     LINKING — two hearts, one body
     ============================================================================ */
  function armLink(){
    if(seeds.length < 2){ status('plant two seeds first'); return; }
    linkArmed = true; linkFirst = null;
    status('seed link — tap the first seed block, then the second.');
  }
  function armUnlink(){
    unlinkArmed = true;
    status('seed unlink — tap a linked seed to sever its bonds.');
  }

  function createLink(a, b){
    /* already linked? */
    for(var i=0;i<links.length;i++){
      var L = links[i];
      if((L.a === a.id && L.b === b.id) || (L.a === b.id && L.b === a.id)){
        status('those seeds are already linked'); return;
      }
    }
    var mx = (a.pos.x + b.pos.x)/2, mz = (a.pos.z + b.pos.z)/2;
    /* bridge gear rises midway — meshed across the gap */
    var bridge = buildGearAt(mx, mz, 1, null); /* not owned by either seed */
    /* ropes: each seed's pulleys -> bridge gear */
    var bgy = groundY(mx, mz) + 1.9;
    function ropeToBridge(sd){
      for(var pi=0; pi<sd.parts.pulleys.length; pi++){
        var pr = sd.parts.pulleys[pi];
        var line = ropeLine(pr.x, groundY(pr.x, pr.z) + 3.4, pr.z, mx, bgy, mz, 600);
        bridge._ropes = bridge._ropes || [];
        bridge._ropes.push(line);
      }
    }
    ropeToBridge(a); ropeToBridge(b);
    /* one web across both */
    try{
      if(typeof window.spiderDist === 'function') window.spiderDist(4);
      if(typeof window.__weaveAt === 'function'){
        var mid = new THREE.Vector3(mx, groundY(mx, mz), mz);
        setTimeout(function(){ window.__weaveAt(mid); }, 900);
      }
    }catch(e){}
    /* gold thread between the seeds, pulsing with shared rhythm */
    var tgeo = new THREE.BufferGeometry().setFromPoints([
      new THREE.Vector3(a.pos.x, a.pos.y + 1.2, a.pos.z),
      new THREE.Vector3(b.pos.x, b.pos.y + 1.2, b.pos.z)
    ]);
    var thread = new THREE.Line(tgeo, new THREE.LineBasicMaterial({
      color:0xffd700, transparent:true, opacity:0.6
    }));
    if(window.scene) window.scene.add(thread);
    links.push({a:a.id, b:b.id, bridge:bridge, thread:thread});
    a.linked.push(b.id); b.linked.push(a.id);
    driveLoop();
    status('seeds linked — two hearts, one body. the bridge gear turns for both.');
  }

  function severLinksOf(seedId){
    for(var i=links.length-1; i>=0; i--){
      var L = links[i];
      if(L.a === seedId || L.b === seedId){
        (function(link){
          try{
            if(link.bridge && link.bridge.group) sinkAway(link.bridge.group, 800);
            if(link.bridge && link.bridge._ropes){
              link.bridge._ropes.forEach(function(ln, k){
                setTimeout(function(){
                  try{
                    if(window.scene) window.scene.remove(ln);
                    ln.geometry.dispose(); ln.materi