<!-- SAGA-STAGING · part 5 -->
eq = 3;
    var steps = 40;
    for(var j=0; j<steps; j++){
      var y0 = (j/steps)*size, y1 = ((j+1)/steps)*size;
      var x0 = x + waveAmp*Math.sin(waveFreq*(y0/size)*Math.PI*2 + i*0.1);
      var x1 = x + waveAmp*Math.sin(waveFreq*(y1/size)*Math.PI*2 + i*0.1);
      var tone = i%3===0 ? '#5a6270' : (i%3===1 ? '#4a525e' : '#6a7280');
      ctx.strokeStyle = tone; ctx.lineWidth = w;
      ctx.beginPath(); ctx.moveTo(x0,y0); ctx.lineTo(x1,y1); ctx.stroke();
      btx.strokeStyle = '#a0a0a0'; btx.lineWidth = w;
      btx.beginPath(); btx.moveTo(x0,y0); btx.lineTo(x1,y1); btx.stroke();
    }
  }
  var chevY = size*0.3;
  for(var ci=0; ci<3; ci++){
    var cy = chevY + ci*size*0.08;
    var cw = size*0.15, ch = size*0.04;
    ctx.fillStyle = '#c8a84b';
    ctx.beginPath();
    ctx.moveTo(size/2 - cw, cy);
    ctx.lineTo(size/2, cy + ch);
    ctx.lineTo(size/2 + cw, cy);
    ctx.lineTo(size/2 + cw, cy + ch*0.4);
    ctx.lineTo(size/2, cy + ch*1.4);
    ctx.lineTo(size/2 - cw, cy + ch*0.4);
    ctx.closePath(); ctx.fill();
    btx.fillStyle = '#ffffff';
    btx.beginPath();
    btx.moveTo(size/2 - cw, cy);
    btx.lineTo(size/2, cy + ch);
    btx.lineTo(size/2 + cw, cy);
    btx.lineTo(size/2 + cw, cy + ch*0.4);
    btx.lineTo(size/2, cy + ch*1.4);
    btx.lineTo(size/2 - cw, cy + ch*0.4);
    btx.closePath(); btx.fill();
  }
  var ct = new THREE.CanvasTexture(cv);
  ct.anisotropy = 8;
  var bt = new THREE.CanvasTexture(bv);
  return {map: ct, bumpMap: bt};
}

/* ============================================================
   EPISTLES: Scrollwork Texture (Mender language)
   Organic flowing vines — NOT geometric, NOT directional
   ============================================================ */
function generateScrollwork(opts) {
  opts = opts || {};
  var size = opts.size || 1024;
  var seed = opts.seed || 21;
  var s = seed;
  function rand() { s = (s * 16807) % 2147483647; return (s - 1) / 2147483646; }
  var cv = document.createElement('canvas'); cv.width = cv.height = size;
  var ctx = cv.getContext('2d');
  ctx.imageSmoothingEnabled = true;
  var bv = document.createElement('canvas'); bv.width = bv.height = size;
  var btx = bv.getContext('2d');

  // Warm cloth base (tan)
  ctx.fillStyle = '#b8a888'; ctx.fillRect(0, 0, size, size);
  btx.fillStyle = '#808080'; btx.fillRect(0, 0, size, size);

  // Scrolling vines — bezier curves with teardrop tapers
  function vine(x0, y0, x1, y1, x2, y2, w, col) {
    var nx = x1 + (rand()-0.5)*size*0.05;
    var ny = y1 + (rand()-0.5)*size*0.05;
    function pt(t){
      var mt = 1-t;
      return [
        mt*mt*x0 + 2*mt*t*nx + t*t*x2,
        mt*mt*y0 + 2*mt*t*ny + t*t*y2
      ];
    }
    var steps = 30;
    for(var i=0; i<steps; i++){
      var t0 = i/steps, t1 = (i+1)/steps;
      var p0 = pt(t0), p1 = pt(t1);
      var wt = w * (1 - t0*0.7);
      ctx.strokeStyle = col; ctx.lineWidth = wt; ctx.lineCap = 'round';
      ctx.beginPath(); ctx.moveTo(p0[0], p0[1]); ctx.lineTo(p1[0], p1[1]); ctx.stroke();
      btx.strokeStyle = '#a0a0a0'; btx.lineWidth = wt;
      btx.beginPath(); btx.moveTo(p0[0], p0[1]); btx.lineTo(p1[0], p1[1]); btx.stroke();
    }
    var tip = pt(1);
    ctx.fillStyle = col;
    ctx.beginPath();
    ctx.ellipse(tip[0], tip[1], w*1.2, w*0.6, Math.atan2(y2-ny, x2-nx), 0, Math.PI*2);
    ctx.fill();
  }

  // Flowing vine field — asymmetric, organic
  var vines = 14;
  for(var v=0; v<vines; v++){
    var sx = rand()*size, sy = rand()*size;
    var ang = rand()*Math.PI*2;
    var len = size*(0.15 + rand()*0.25);
    var cx = sx + Math.cos(ang)*len*0.5, cy = sy + Math.sin(ang)*len*0.5;
    var ex = sx + Math.cos(ang+0.5)*len, ey = sy + Math.sin(ang+0.5)*len;
    var col = v%3===0 ? '#8a7a5a' : (v%3===1 ? '#9a8a6a' : '#7a6a4a');
    vine(sx, sy, cx, cy, ex, ey, size*0.008, col);
  }

  // Small dots (seeds/berries) scattered
  for(var d=0; d<40; d++){
    var dx = rand()*size, dy = rand()*size;
    ctx.fillStyle = '#6a5a3a';
    ctx.beginPath(); ctx.arc(dx, dy, size*0.004, 0, Math.PI*2); ctx.fill();
    btx.fillStyle = '#909090';
    btx.beginPath(); btx.arc(dx, dy, size*0.004, 0, Math.PI*2); btx.fill();
  }

  var ct = new THREE.CanvasTexture(cv);
  ct.anisotropy = 8;
  var bt = new THREE.CanvasTexture(bv);
  return {map: ct, bumpMap: bt};
}

/* ============================================================
   EPISTLES: Radial Motion Texture (Spinner language)
   Centrifugal — lines radiating from center, suggesting spin
   ============================================================ */
function generateRadialMotion(opts) {
  opts = opts || {};
  var size = opts.size || 1024;
  var seed = opts.seed || 99;
  var s = seed;
  function rand() { s = (s * 16807) % 2147483647; return (s - 1) / 2147483646; }
  var cv = document.createElement('canvas'); cv.width = cv.height = size;
  var ctx = cv.getContext('2d');
  var bv = document.createElement('canvas'); bv.width = bv.height = size;
  var btx = bv.getContext('2d');

  //