<!-- RUBY-BACKUP · part 55 -->
arts.web;
    return false;
  }

  function growPulley(sd){
    sd = sd || activeSeed();
    if(!sd || !sd.pos){ status('plant a seed first — `seed`, tap a block'); return; }
    if(hasPart(sd, 'pulley')){ status('pulleys already stand'); return; }
    var R = 6;
    var dirs = [[0,-R],[0,R],[-R,0],[R,0]];
    for(var i=0;i<4;i++){
      buildPulleyAt(sd.pos.x + dirs[i][0], sd.pos.z + dirs[i][1], i, sd);
    }
    status('4 pulleys rising at the cardinal points.');
  }

  function growGear(sd){
    sd = sd || activeSeed();
    if(!sd || !sd.pos){ status('plant a seed first — `seed`, tap a block'); return; }
    if(hasPart(sd, 'gear')){ status('gears already mesh'); return; }
    var gap = GEAR_R*2 + 0.18;
    buildGearAt(sd.pos.x - gap/2, sd.pos.z + 3.5, 1, sd);
    buildGearAt(sd.pos.x + gap/2, sd.pos.z + 3.5, -1, sd);
    status('twin gears rising — meshed, counter-turning.');
  }

  function growRope(sd){
    sd = sd || activeSeed();
    if(!sd){ status('plant a seed first'); return; }
    if(!hasPart(sd, 'pulley') || !hasPart(sd, 'gear')){ status('grow pulleys and gears first'); return; }
    if(hasPart(sd, 'rope')){ status('ropes already threaded'); return; }
    buildRopesFor(sd);
  }

  function growWeb(sd){
    sd = sd || activeSeed();
    if(!sd){ status('plant a seed first'); return; }
    if(hasPart(sd, 'web')){ status('the web already spans'); return; }
    buildWebFor(sd);
    status('the spider weaves across the machine.');
  }

  function growAll(sd){
    sd = sd || activeSeed();
    if(!sd || !sd.pos){ status('plant a seed first — `seed`, tap a block'); return; }
    growPulley(sd);
    setTimeout(function(){ growGear(sd); }, 500);
    setTimeout(function(){ growRope(sd); }, 1400);
    setTimeout(function(){ growWeb(sd); }, 2200);
    driveLoop();
    status('the machine grows from the seed.');
  }

  /* ---------- disassemble (the antonym) ---------- */
  function clearSeedParts(sd, keepBlock){
    try{ if(typeof window.unspider === 'function') window.unspider(); }catch(e){}
    sd.parts.web = false;
    var ropes = sd.parts.ropes.slice(); sd.parts.ropes = [];
    ropes.forEach(function(r, i){
      setTimeout(function(){
        try{
          if(window.scene) window.scene.remove(r.line);
          r.line.geometry.dispose(); r.line.material.dispose();
        }catch(e){}
      }, i*120);
    });
    var gears = sd.parts.gears.slice(); sd.parts.gears = [];
    gears.forEach(function(gr, i){
      setTimeout(function(){ sinkAway(gr.group, 800); }, 400 + i*250);
    });
    var puls = sd.parts.pulleys.slice(); sd.parts.pulleys = [];
    puls.forEach(function(pr, i){
      setTimeout(function(){ sinkAway(pr.group, 900); }, 900 + i*200);
    });
  }

  function unseed(sd){
    sd = sd || activeSeed();
    if(!sd){ status('no seed planted'); return; }
    /* sever its links first */
    severLinksOf(sd.id);
    status('disassembling — piece by piece.');
    clearSeedParts(sd);
    setTimeout(function(){
      try{
        if(sd.core && sd.core.parent){
          sd.core.material.opacity = 0.25;
        }
        if(sd.block){ try{ sd.block.userData.isSeed = false; }catch(e){} }
      }catch(e){}
      var ix = seeds.indexOf(sd);
      if(ix >= 0) seeds.splice(ix, 1);
      status('the machine is gone — the block remains.');
    }, 2200);
  }

  function unseedAll(){
    var all = seeds.slice();
    if(!all.length){ status('no machines stand'); return; }
    all.forEach(function(sd, i){ setTimeout(function(){ unseed(sd); }, i*300); });
  }

  /* ============================================================================
     TEMPLATES — design once, bloom everywhere
     ============================================================================ */
  function loadTemplates(){
    try{
      var raw = localStorage.getItem(TPL_KEY);
      if(raw){ var o = JSON.parse(raw); if(o && o.templates) return o.templates; }
    }catch(e){}
    return {};
  }
  function saveTemplates(t){
    try{ localStorage.setItem(TPL_KEY, JSON.stringify({templates:t})); }catch(e){}
  }
  function cleanName(n){
    return String(n == null ? '' : n).trim().toLowerCase().replace(/[^a-z0-9\-_ ]/g, '').slice(0, 40);
  }

  function saveTemplate(name){
    name = cleanName(name);
    if(!name){ status('name it — `seed save <name>`'); return; }
    var sd = null;
    for(var i=seeds.length-1;i>=0;i--){
      if(hasPart(seeds[i],'pulley')||hasPart(seeds[i],'gear')){ sd=seeds[i]; break; }
    }
    if(!sd){ sd = activeSeed(); }
    if(!sd || !sd.pos){ status('grow a machine first, then save it'); return; }
    if(!hasPart(sd, 'pulley') && !hasPart(sd, 'gear')){ status('nothing grown yet — `seed grow` first'); return; }
    var t = {
      t: Date.now(),
      pulleys: sd.parts.pulleys.map(function(p){ return {dx: +(p.x - sd.pos.x).toFixed(2), dz: +(p.z - sd.pos.z).toFixed(2)}; }),
      gears: sd.parts.gears.map(function(g){ return {dx: +(g.x - sd.pos.x).toFixed(2), dz: +(g.z - sd.pos.z).toFixed