<!-- LIVE · part 3 -->
-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Avatar View</title>
<style>body{margin:0;background:#1a1a20;}canvas{display:block;}</style>
</head><body>
<script src="/saga/three.min.js"></script>
<script src="/saga-solid/avatars.js?v=33"></script>
<script>
var scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a20);
var camera = new THREE.PerspectiveCamera(50, 390/844, 0.1, 100);
var renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(390, 844);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
var key = new THREE.DirectionalLight(0xffffff, 0.8);
key.position.set(3, 5, 4); key.castShadow = true; scene.add(key);
var rim = new THREE.DirectionalLight(0x8888ff, 0.4);
rim.position.set(-3, 2, -2); scene.add(rim);
var ground = new THREE.Mesh(new THREE.PlaneGeometry(20,20),
  new THREE.MeshStandardMaterial({color:0x2a2a30, roughness:0.9}));
ground.rotation.x = -Math.PI/2; ground.receiveShadow = true; scene.add(ground);
// Params: avatar and view
var m = /[?&]avatar=([a-z]+)/.exec(location.search||'');
var v = /[?&]view=(front|side|back)/.exec(location.search||'');
var id = (m && m[1]) || 'sentinel';
var view = (v && v[1]) || 'front';
setTimeout(function(){
  if (window.EPISTLES_AVATARS && window.EPISTLES_AVATARS.builders[id]) {
    var av = window.EPISTLES_AVATARS.builders[id]();
    // Rotate avatar to show requested view (camera stays front)
    if (view === 'side') av.rotation.y = Math.PI/2;
    if (view === 'back') av.rotation.y = Math.PI;
    scene.add(av);
    document.title = av.userData.avatarName + ' ' + view;
  }
  camera.position.set(0, 1.5, 5);
  camera.lookAt(0, 1.2, 0);
  renderer.render(scene, camera);
}, 500);
</script>
</body></html>


===== FILE: public/live/avatars.js =====
/* EPISTLES AVATARS — 20 Original Designs */
/* Each builder returns a THREE.Group. No collision registration. */
/* Limbs (legL/legR/armL/armR) optional — animation guards with if checks. */

(function(){
'use strict';

/* Shared materials — reused across avatars for performance */
var MAT = {};

/* ============================================================
   EPISTLES DETAIL GRAMMAR: Procedural Engraved Material System
   Generates color + bump from same line source
   ============================================================ */
function generateEngravedPanel(opts) {
  opts = opts || {};
  var size = opts.size || 2048;
  var seed = opts.seed || 1;
  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');
  // Enable smoothing for anti-aliasing
  ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high';
  var bv = document.createElement('canvas'); bv.width = bv.height = size;
  var btx = bv.getContext('2d');
  btx.imageSmoothingEnabled = true;
  
  var cx = size/2, cy = size/2;
  // Deep black background (negative space)
  ctx.fillStyle = '#0a0c0f'; ctx.fillRect(0, 0, size, size);
  btx.fillStyle = '#808080'; btx.fillRect(0, 0, size, size);
  
  function dline(x1,y1,x2,y2,w,col,bcol){
    ctx.strokeStyle=col; ctx.lineWidth=w; ctx.lineCap='round';
    ctx.beginPath(); ctx.moveTo(x1,y1); ctx.lineTo(x2,y2); ctx.stroke();
    btx.strokeStyle=bcol; btx.lineWidth=w; btx.lineCap='round';
    btx.beginPath(); btx.moveTo(x1,y1); btx.lineTo(x2,y2); btx.stroke();
  }
  function dcirc(x,y,r,w,col,bcol){
    ctx.strokeStyle=col; ctx.lineWidth=w;
    ctx.beginPath(); ctx.arc(x,y,r,0,Math.PI*2); ctx.stroke();
    btx.strokeStyle=bcol; btx.lineWidth=w;
    btx.beginPath(); btx.arc(x,y,r,0,Math.PI*2); btx.stroke();
  }
  
  // === OUTER GOLD BORDER (MESO frame, preserved) ===
  var m1 = size*0.03;
  ctx.strokeStyle='#d4af37'; ctx.lineWidth=size*0.012;
  ctx.strokeRect(m1,m1,size-2*m1,size-2*m1);
  btx.strokeStyle='#ffffff'; btx.lineWidth=size*0.012;
  btx.strokeRect(m1,m1,size-2*m1,size-2*m1);
  
  // === SECONDARY BORDER (thin silver) ===
  var m2 = m1 + size*0.025;
  dline(m2,m2,size-m2,m2, size*0.004, '#a8b0b8', '#d0d0d0');
  dline(size-m2,m2,size-m2,size-m2, size*0.004, '#a8b0b8', '#d0d0d0');
  dline(size-m2,size-m2,m2,size-m2, size*0.004, '#a8b0b8', '#d0d0d0');
  dline(m2,size-m2,m2,m2, size*0.004, '#a8b0b8', '#d0d0d0');
  
  // === TERTIARY BORDER (hairline gold, border-within-border) ===
  var m3 = m2 + size*0.015;
  ctx.strokeStyle='#8a6d2f'; ctx.lineWidth=size*0.002;
  ctx.setLineDash([size*0.01, size*0.005]);
  ctx.strokeRect(m3,m3,size-2*m3,size-2*m3);
  ctx.setLineDash([]);
  btx.strokeStyle='#a0a0a0'; btx.lineWidth=size*0.002;
  btx.setLineDash([size*0.01, size*0.005]);
  btx.strokeRect(m3,m3,size-2*m3,size-2*m3);
  btx.setLineDash([]);
  
  // === FIELD: Interwoven rose-engine families ===
  var fm = m3 + size*0.02;
  var fx = size/2, fy = size/2;
  var R = (size/2 - fm) * 0.92;
  
  // Family 1