/* viz.jsx — all programmatic SVG visualizations (no rasters, ever) */

/* =================================================================
   EOANI
   ================================================================= */

/* Tidal-lock cross-section. Host page hero. */
function EoaniTidalLock({ w = 640, h = 340, interactive = true }) {
  const [x, setX] = React.useState(0.5);
  const onMove = e => {
    if (!interactive) return;
    const r = e.currentTarget.getBoundingClientRect();
    setX(Math.max(0, Math.min(1, (e.clientX - r.left) / r.width)));
  };
  const zones = [
    { x0: 0.00, x1: 0.18, label: '永昼热区',  temp: '~380 K', color: '#ff6b35' },
    { x0: 0.18, x1: 0.38, label: '日下环',    temp: '~320 K', color: '#e8821f' },
    { x0: 0.38, x1: 0.62, label: '晨昏带',    temp: '~270 K', color: '#8b3a1f' },
    { x0: 0.62, x1: 0.82, label: '夜侧冻区',  temp: '~180 K', color: '#3a1c14' },
    { x0: 0.82, x1: 1.00, label: '永夜冰区',  temp: '~95 K',  color: '#0a0604' },
  ];
  return (
    <svg viewBox={`0 0 ${w} ${h}`} width="100%" onMouseMove={onMove} style={{ display: 'block' }}>
      <defs>
        <linearGradient id="eoani-limb" x1="0" x2="1">
          {zones.map((z, i) => (
            <stop key={i} offset={`${((z.x0 + z.x1) / 2) * 100}%`} stopColor={z.color} />
          ))}
        </linearGradient>
      </defs>
      {/* star to the left */}
      <g>
        <circle cx={-40} cy={h/2} r={80} fill="#ff3300" opacity="0.35" filter="blur(16px)" />
        <text x={12} y={26} fill="#f5e6d3" opacity="0.6" fontSize="10" style={{letterSpacing:'0.15em'}}>← Ross 128 (M4V)</text>
      </g>
      {/* planet disc */}
      <g transform={`translate(${w/2 - 130}, ${h/2})`}>
        <clipPath id="eoani-disc"><circle r="130" /></clipPath>
        <g clipPath="url(#eoani-disc)">
          {zones.map((z, i) => (
            <rect key={i} x={-130 + 260 * z.x0} y={-130} width={260 * (z.x1 - z.x0)} height={260} fill={z.color} />
          ))}
          {/* limb shading */}
          <circle r="130" fill="url(#eoani-limb)" opacity="0.2" />
          {/* grid */}
          {[-90,-45,0,45,90].map(d => (
            <ellipse key={d} cx="0" cy="0" rx={130} ry={Math.abs(Math.cos(d*Math.PI/180))*130} fill="none" stroke="#f5e6d3" strokeOpacity="0.07" />
          ))}
        </g>
        <circle r="130" fill="none" stroke="#f5e6d3" strokeOpacity="0.3" />
        {/* cursor readout */}
        {interactive && (()=>{
          const px = -130 + 260 * x;
          const idx = zones.findIndex(z => x >= z.x0 && x < z.x1);
          const z = zones[Math.max(0, idx)];
          return (
            <g>
              <line x1={px} x2={px} y1={-130} y2={130} stroke="#f5e6d3" strokeOpacity="0.5" strokeDasharray="2 4" />
              <g transform={`translate(${px + 10}, -110)`}>
                <rect x="0" y="0" width="120" height="44" fill="#1a0e0acc" stroke="#f5e6d355" />
                <text x="10" y="18" fill="#f5e6d3" fontSize="11" style={{fontFamily:'var(--font-sans-cjk)', letterSpacing:'0.1em'}}>{z.label}</text>
                <text x="10" y="34" fill="#e8821f" fontSize="11" style={{fontFamily:'var(--font-serif-latin)'}}>{z.temp} · Δ {(0.1 + Math.random()*0.4).toFixed(2)} mK</text>
              </g>
            </g>
          );
        })()}
      </g>
      {/* zone labels */}
      <g fontFamily="var(--font-sans-cjk)" fontSize="10" fill="#f5e6d3" opacity="0.65" letterSpacing="0.15em">
        {zones.map((z, i) => (
          <text key={i} x={w/2 - 130 + 260*((z.x0+z.x1)/2) - 24} y={h - 14}>{z.label}</text>
        ))}
      </g>
      <text x="12" y={h - 14} fill="#f5e6d3" opacity="0.45" fontSize="10" fontFamily="var(--font-mono)">
        {interactive ? 'cursor · 热分辨 ~0.1 mK' : '潮汐锁定剖面 · Ross 128 b'}
      </text>
    </svg>
  );
}

/* Slow magnetic-field vector field that drifts */
function EoaniMagneticField({ w = 800, h = 260 }) {
  const [t, setT] = React.useState(0);
  React.useEffect(() => {
    let raf, start = performance.now();
    const loop = n => { setT((n - start) / 1000); raf = requestAnimationFrame(loop); };
    raf = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(raf);
  }, []);
  const cols = 28, rows = 8;
  const arrows = [];
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      const x = (c + 0.5) * (w / cols);
      const y = (r + 0.5) * (h / rows);
      // dipole-ish: angle depends on position + slow drift
      const dx = x - w/2, dy = y - h/2;
      const base = Math.atan2(dy, dx) + Math.PI/2;
      const angle = base + 0.25 * Math.sin(t * 0.1 + r * 0.3) + 0.15 * Math.cos(t * 0.07 + c * 0.2);
      const len = 14;
      arrows.push({ x, y, angle, len, key: `${r}-${c}` });
    }
  }
  return (
    <svg viewBox={`0 0 ${w} ${h}`} width="100%" style={{display:'block'}}>
      {arrows.map(a => (
        <g key={a.key} transform={`translate(${a.x} ${a.y}) rotate(${a.angle * 180 / Math.PI})`}>
          <line x1={-a.len/2} x2={a.len/2} y1="0" y2="0" stroke="#d4a574" strokeOpacity="0.55" strokeWidth="1" />
          <polygon points={`${a.len/2},0 ${a.len/2 - 3},-2 ${a.len/2 - 3},2`} fill="#d4a574" fillOpacity="0.6" />
        </g>
      ))}
    </svg>
  );
}

/* Millennium slider: project a human age onto Eoani caste lifespans */
function EoaniMillenniumSlider() {
  const [age, setAge] = React.useState(30);
  const castes = [
    { zh: '大衡',   en: 'Magni',          min: 1000, max: 1300, color: '#8b3a1f' },
    { zh: '衡者',   en: 'Eoan Integri',   min: 600,  max: 900,  color: '#b8431e' },
    { zh: '半衡',   en: 'Mediari',        min: 200,  max: 400,  color: '#d4641f' },
    { zh: '微衡',   en: 'Infra-Eoani',    min: 80,   max: 150,  color: '#e8821f' },
    { zh: '失衡',   en: 'Ephemeri',       min: 30,   max: 60,   color: '#ff6b35' },
  ];
  const max = 1300;
  return (
    <div style={{marginTop:40}}>
      <div style={{display:'flex', justifyContent:'space-between', alignItems:'baseline', marginBottom:12}}>
        <span className="label">你的年龄投影到衡格寿命刻度</span>
        <span className="num" style={{fontSize:20, fontFamily:'var(--font-serif-latin)'}}>{age} 岁</span>
      </div>
      <input type="range" min="1" max="100" value={age} onChange={e => setAge(+e.target.value)}
             style={{width:'100%', accentColor:'#e8821f'}} />
      <div style={{position:'relative', marginTop:24}}>
        {castes.map((c, i) => (
          <div key={c.en} style={{display:'grid', gridTemplateColumns:'140px 1fr 80px', alignItems:'center', gap:16, marginBottom:8, fontSize:13}}>
            <div style={{textAlign:'right', fontFamily:'var(--font-serif-cjk)'}}>
              {c.zh}<span style={{display:'block', fontFamily:'var(--font-serif-latin)', fontStyle:'italic', fontSize:11, opacity:0.6}}>{c.en}</span>
            </div>
            <div style={{position:'relative', height:18, background:'#f5e6d311'}}>
              <div style={{position:'absolute', left:`${(c.min/max)*100}%`, width:`${((c.max-c.min)/max)*100}%`, height:'100%', background:c.color, opacity:0.85}} />
              <div style={{position:'absolute', left:`${(age/max)*100}%`, top:-4, bottom:-4, width:2, background:'#f5e6d3'}} />
            </div>
            <div style={{fontFamily:'var(--font-serif-latin)', fontSize:11, opacity:0.7}}>{c.min}–{c.max} y</div>
          </div>
        ))}
        <div style={{fontSize:12, marginTop:14, opacity:0.7, fontFamily:'var(--font-serif-cjk)', lineHeight:1.65}}>
          一位大衡看你的一生，像你看一个蜉蝣的一天。
          若折现率 r = 0.01%/年（衡族）对比 r = 5%/年（人类），同一份千年契约的现值相差 ≈ 10<sup>21</sup> 倍。
        </div>
      </div>
    </div>
  );
}

/* =================================================================
   KETOI
   ================================================================= */

/* Flowing spectrogram band used as title ornament + hero */
function KetoiSpectrogram({ w = 900, h = 200, bars = 180 }) {
  const [t, setT] = React.useState(0);
  React.useEffect(() => {
    let raf, start = performance.now();
    const loop = n => { setT((n - start) / 1000); raf = requestAnimationFrame(loop); };
    raf = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(raf);
  }, []);
  const cells = [];
  const rows = 16;
  const bw = w / bars;
  const rh = h / rows;
  for (let i = 0; i < bars; i++) {
    for (let r = 0; r < rows; r++) {
      // layered sinusoids produce speech-like envelope
      const x = i / bars * 8 - t * 0.6;
      const base = Math.exp(-Math.pow((r - 7) / 4, 2));
      const wobble =
        0.7 * Math.sin(x * 1.2 + r * 0.25) +
        0.4 * Math.sin(x * 2.5 - r * 0.5 + t * 0.2) +
        0.3 * Math.sin(x * 0.6 + t * 0.1);
      const v = Math.max(0, base + 0.15 * wobble);
      if (v < 0.04) continue;
      const hue = 190 + 40 * (r / rows) + 20 * Math.sin(t * 0.1);
      cells.push(
        <rect key={`${i}-${r}`}
              x={i * bw} y={(rows - 1 - r) * rh}
              width={bw + 0.5} height={rh + 0.5}
              fill={`hsl(${hue}, 70%, ${20 + v * 45}%)`}
              opacity={Math.min(1, v * 1.4)} />
      );
    }
  }
  return (
    <svg viewBox={`0 0 ${w} ${h}`} width="100%" style={{display:'block'}}>
      {cells}
    </svg>
  );
}

/* Echo radar: one 'word' (text) rendered as an echogram */
function KetoiEchoRadar({ w = 520, h = 340, word = '鲸语族你好' }) {
  const N = word.length;
  const cx = w / 2, cy = h / 2;
  const rings = Array.from({length: 6}).map((_, i) => 40 + i * 32);
  const echoes = [];
  for (let i = 0; i < N; i++) {
    const ch = word.charCodeAt(i);
    const angle = (i / N) * Math.PI * 2 - Math.PI / 2;
    const r1 = 50 + (ch % 13) * 10;
    const r2 = r1 + 10 + (ch % 7) * 5;
    echoes.push(
      <path key={i}
            d={`M ${cx + Math.cos(angle - 0.05) * r1} ${cy + Math.sin(angle - 0.05) * r1}
                A ${r1} ${r1} 0 0 1 ${cx + Math.cos(angle + 0.05) * r1} ${cy + Math.sin(angle + 0.05) * r1}
                L ${cx + Math.cos(angle + 0.05) * r2} ${cy + Math.sin(angle + 0.05) * r2}
                A ${r2} ${r2} 0 0 0 ${cx + Math.cos(angle - 0.05) * r2} ${cy + Math.sin(angle - 0.05) * r2} Z`}
            fill="#7fc4d9" fillOpacity={0.3 + (ch % 7) / 20} />
    );
  }
  return (
    <svg viewBox={`0 0 ${w} ${h}`} width="100%" style={{display:'block'}}>
      {rings.map(r => <circle key={r} cx={cx} cy={cy} r={r} fill="none" stroke="#7fc4d9" strokeOpacity="0.18" />)}
      {echoes}
      <circle cx={cx} cy={cy} r="6" fill="#e8f4f8" />
      <text x={12} y={h - 14} fill="#e8f4f8" opacity="0.55" fontSize="10" fontFamily="var(--font-mono)">
        echogram · "{word}" · 同一名字不同深度的回波签名
      </text>
    </svg>
  );
}

/* Downward astronomy: phonon array pointing DOWN into the ocean floor */
function KetoiDownwardAstronomy({ w = 800, h = 320 }) {
  return (
    <svg viewBox={`0 0 ${w} ${h}`} width="100%" style={{display:'block'}}>
      {/* surface */}
      <defs>
        <linearGradient id="ketoi-col" x1="0" x2="0" y1="0" y2="1">
          <stop offset="0" stopColor="#2e86ab" stopOpacity="0.1" />
          <stop offset="1" stopColor="#050e1a" stopOpacity="0" />
        </linearGradient>
      </defs>
      <rect x="0" y="0" width={w} height={h} fill="url(#ketoi-col)" />
      {/* surface waves */}
      <path d={`M 0 40 ${Array.from({length:40}).map((_,i)=>`Q ${i*20+10} ${30+Math.sin(i*0.7)*6} ${i*20+20} 40`).join(' ')}`}
            fill="none" stroke="#7fc4d9" strokeOpacity="0.5" />
      <text x={14} y={24} fill="#89a8bd" fontSize="10" letterSpacing="0.2em" fontFamily="var(--font-sans-cjk)">海面 SURFACE</text>
      {/* array of phonon receivers on sea floor pointing down */}
      <g transform={`translate(${w/2}, ${h - 40})`}>
        {Array.from({length: 7}).map((_, i) => {
          const x = (i - 3) * 60;
          return (
            <g key={i}>
              <polygon points={`${x-10},0 ${x+10},0 ${x},20`} fill="#7fc4d9" fillOpacity="0.8" />
              {/* beams going down */}
              {Array.from({length:5}).map((_,j)=>(
                <line key={j} x1={x} y1={20} x2={x + (j-2)*18} y2={20 + 60 + j*6}
                      stroke="#00d9ff" strokeOpacity="0.15" />
              ))}
              <line x1={x} x2={x} y1={-30} y2={0} stroke="#7fc4d9" strokeOpacity="0.3" strokeDasharray="2 4" />
            </g>
          );
        })}
        <line x1={-220} x2={220} y1="0" y2="0" stroke="#7fc4d9" strokeOpacity="0.4" />
        <text x={-220} y={-6} fill="#89a8bd" fontSize="10" letterSpacing="0.2em" fontFamily="var(--font-sans-cjk)">海床 BENTHIC ARRAY · 声子望远镜</text>
      </g>
      {/* arrow UP = last frontier */}
      <g transform={`translate(${w-80}, 80)`}>
        <line x1="0" x2="0" y1="40" y2="-10" stroke="#ff4500" strokeOpacity="0.55" strokeWidth="1.5" />
        <polygon points="0,-16 -4,-6 4,-6" fill="#ff4500" opacity="0.7" />
        <text x={10} y="20" fill="#ff4500" opacity="0.8" fontSize="10" letterSpacing="0.15em" fontFamily="var(--font-sans-cjk)">天空 · 最晚发现</text>
      </g>
    </svg>
  );
}

/* Polarization skin swatch — responds to mouse */
function KetoiPolarization({ w = 260, h = 260 }) {
  const [m, setM] = React.useState({ x: 0.5, y: 0.5 });
  const onMove = e => {
    const r = e.currentTarget.getBoundingClientRect();
    setM({ x: (e.clientX - r.left)/r.width, y: (e.clientY - r.top)/r.height });
  };
  const tiles = [];
  for (let r = 0; r < 10; r++) {
    for (let c = 0; c < 10; c++) {
      const dx = c/9 - m.x, dy = r/9 - m.y;
      const d = Math.sqrt(dx*dx + dy*dy);
      const hue = 180 + 180 * ((c + r + d * 3) % 1);
      const angle = Math.atan2(dy, dx) * 180/Math.PI + d * 200;
      tiles.push(
        <g key={`${r}-${c}`} transform={`translate(${c * w/10 + w/20}, ${r * h/10 + h/20}) rotate(${angle})`}>
          <polygon points={`-10,0 0,-10 10,0 0,10`} fill={`hsl(${hue}, 70%, 55%)`} opacity={0.75 - d * 0.3} />
        </g>
      );
    }
  }
  return <svg viewBox={`0 0 ${w} ${h}`} width="100%" onMouseMove={onMove}>{tiles}</svg>;
}

/* Wave divider for section breaks */
function KetoiWaveDivider({ w = 1440, h = 20 }) {
  const path = `M 0 ${h/2} ` + Array.from({length: 60}).map((_, i) =>
    `Q ${i*24+12} ${h/2 + (i%2===0 ? -h/2 : h/2)} ${(i+1)*24} ${h/2}`
  ).join(' ');
  return (
    <svg viewBox={`0 0 ${w} ${h}`} width="100%" className="wave-divider">
      <path d={path} fill="none" stroke="#7fc4d9" strokeWidth="1" />
    </svg>
  );
}

/* =================================================================
   HUVARI
   ================================================================= */

/* Flowing field lines — interactive */
function HuvariFieldLines({ w = 900, h = 340 }) {
  const [src, setSrc] = React.useState({ x: 0.6, y: 0.5 });
  const onMove = e => {
    const r = e.currentTarget.getBoundingClientRect();
    setSrc({ x: (e.clientX - r.left)/r.width, y: (e.clientY - r.top)/r.height });
  };
  const cx = src.x * w, cy = src.y * h;
  const cx2 = w - cx, cy2 = h - cy;
  const lines = [];
  const N = 24;
  for (let i = 0; i < N; i++) {
    const theta = (i / N) * Math.PI * 2;
    const pts = [];
    let x = cx + Math.cos(theta) * 10;
    let y = cy + Math.sin(theta) * 10;
    for (let s = 0; s < 160; s++) {
      const dx1 = x - cx, dy1 = y - cy, r1 = Math.max(8, Math.hypot(dx1,dy1));
      const dx2 = x - cx2, dy2 = y - cy2, r2 = Math.max(8, Math.hypot(dx2,dy2));
      const fx = dx1/(r1*r1*r1) - dx2/(r2*r2*r2);
      const fy = dy1/(r1*r1*r1) - dy2/(r2*r2*r2);
      const m = Math.hypot(fx, fy);
      if (m < 1e-7) break;
      x += fx/m * 4;
      y += fy/m * 4;
      pts.push(`${x.toFixed(1)} ${y.toFixed(1)}`);
      if (x < -10 || x > w + 10 || y < -10 || y > h + 10) break;
    }
    lines.push(<polyline key={i} points={pts.join(' ')} fill="none" stroke="#00d9ff" strokeWidth="0.8" strokeOpacity="0.7" />);
  }
  // equipotentials
  const equipot = [];
  for (let k = 1; k <= 5; k++) {
    const rr = k * 28;
    equipot.push(<circle key={`a${k}`} cx={cx} cy={cy} r={rr} fill="none" stroke="#ffd700" strokeOpacity="0.15" strokeDasharray="2 4" />);
    equipot.push(<circle key={`b${k}`} cx={cx2} cy={cy2} r={rr} fill="none" stroke="#ffd700" strokeOpacity="0.15" strokeDasharray="2 4" />);
  }
  return (
    <svg viewBox={`0 0 ${w} ${h}`} width="100%" onMouseMove={onMove} style={{display:'block', cursor:'crosshair'}}>
      {equipot}
      {lines}
      <circle cx={cx} cy={cy} r="6" fill="#00d9ff" />
      <circle cx={cx2} cy={cy2} r="6" fill="#00ffa3" />
      <text x={cx+10} y={cy-8} fill="#00d9ff" fontSize="10" fontFamily="var(--font-mono)">+q</text>
      <text x={cx2+10} y={cy2-8} fill="#00ffa3" fontSize="10" fontFamily="var(--font-mono)">−q</text>
      <text x={12} y={h-14} fill="#e8faff" opacity="0.5" fontSize="10" fontFamily="var(--font-mono)">
        cursor · 等位线 (金) · 场线 (青)
      </text>
    </svg>
  );
}

/* Hex semiconductor lattice — nods to electrical railways */
function HuvariLattice({ w = 520, h = 320 }) {
  const hexes = [];
  const s = 28; const hw = Math.sqrt(3) * s;
  for (let r = 0; r < 8; r++) {
    for (let c = 0; c < 12; c++) {
      const cx = c * hw + (r % 2 ? hw/2 : 0) + 20;
      const cy = r * s * 1.5 + 28;
      const pts = Array.from({length:6}).map((_,i)=>{
        const a = i * Math.PI/3 - Math.PI/2;
        return `${(cx + Math.cos(a) * s).toFixed(1)},${(cy + Math.sin(a)*s).toFixed(1)}`;
      }).join(' ');
      const doped = (r*7+c*3) % 11 === 0;
      hexes.push(
        <polygon key={`${r}-${c}`} points={pts}
                 fill={doped ? '#00ffa322' : 'none'}
                 stroke="#00d9ff" strokeOpacity={doped ? 0.8 : 0.3} strokeWidth="0.8" />
      );
    }
  }
  return <svg viewBox={`0 0 ${w} ${h}`} width="100%">{hexes}</svg>;
}

/* Clean-industry vs carbon-industry timeline */
function HuvariCleanVsCarbon({ w = 900, h = 260 }) {
  const yrs = [-300, 0, 300, 600, 900, 1200, 1500, 1712, 1900, 2000, 2100];
  const scale = x => 60 + (x - yrs[0]) / (yrs[yrs.length-1] - yrs[0]) * (w - 120);
  // invented curves illustrating co2 equivalent
  const huvari = yrs.map(y => [scale(y), 170 - Math.min(1, Math.max(0, (y+200)/2200)) * 8]);
  const human  = yrs.map(y => [scale(y), 220 - (y < 1712 ? 4 : Math.min(80, (y-1700)*0.5))]);
  return (
    <svg viewBox={`0 0 ${w} ${h}`} width="100%">
      <line x1="40" x2={w-20} y1="220" y2="220" stroke="#00d9ff" strokeOpacity="0.3" />
      <line x1="40" x2="40" y1="40" y2="220" stroke="#00d9ff" strokeOpacity="0.3" />
      <text x="40" y="32" fill="#e8faff" fontSize="10" fontFamily="var(--font-mono)" opacity="0.6">大气碳当量 (任意单位)</text>
      {/* Huvari curve — basically flat */}
      <polyline points={huvari.map(p=>p.join(',')).join(' ')} fill="none" stroke="#00ffa3" strokeWidth="2" />
      <text x={w-30} y={huvari[huvari.length-1][1] - 4} fill="#00ffa3" fontSize="11" textAnchor="end" fontFamily="var(--font-sans-cjk)">涣族</text>
      {/* Human curve — hockey stick */}
      <polyline points={human.map(p=>p.join(',')).join(' ')} fill="none" stroke="#ff4500" strokeWidth="2" />
      <text x={w-30} y={human[human.length-1][1] - 4} fill="#ff4500" fontSize="11" textAnchor="end" fontFamily="var(--font-sans-cjk)">人类</text>
      {/* year ticks */}
      {yrs.map((y, i) => (
        <g key={y}>
          <line x1={scale(y)} x2={scale(y)} y1="220" y2="226" stroke="#00d9ff" strokeOpacity="0.4" />
          <text x={scale(y)} y="240" fill="#e8faff" opacity="0.55" fontSize="10" fontFamily="var(--font-mono)" textAnchor="middle">{y}</text>
        </g>
      ))}
      {/* 1712 marker */}
      <g>
        <line x1={scale(1712)} x2={scale(1712)} y1="50" y2="220" stroke="#ffd700" strokeOpacity="0.5" strokeDasharray="3 3" />
        <text x={scale(1712)+6} y="60" fill="#ffd700" fontSize="10" fontFamily="var(--font-sans-cjk)">1712 · 首次接触</text>
      </g>
    </svg>
  );
}

/* =================================================================
   THAVARI
   ================================================================= */

/* Strain tensor rose */
function ThavariTensorRose({ w = 340, h = 340 }) {
  const cx = w/2, cy = h/2;
  const N = 36;
  const rays = [];
  for (let i = 0; i < N; i++) {
    const a = (i / N) * Math.PI * 2;
    // symmetric tensor: σxx cos² + σyy sin² + 2σxy sinθcosθ
    const σxx = 0.9, σyy = 0.4, σxy = 0.25;
    const r = 40 + 90 * (σxx * Math.cos(a)**2 + σyy * Math.sin(a)**2 + 2 * σxy * Math.sin(a) * Math.cos(a));
    rays.push({ x1: cx, y1: cy, x2: cx + Math.cos(a) * r, y2: cy + Math.sin(a) * r });
  }
  return (
    <svg viewBox={`0 0 ${w} ${h}`} width="100%">
      {[40, 70, 100, 130].map(r => (
        <circle key={r} cx={cx} cy={cy} r={r} fill="none" stroke="#b8c5d6" strokeOpacity="0.15" />
      ))}
      <polygon points={rays.map(r => `${r.x2},${r.y2}`).join(' ')} fill="#00ffc8" fillOpacity="0.15" stroke="#00ffc8" strokeOpacity="0.7" />
      {rays.map((r, i) => <line key={i} {...r} stroke="#00ffc8" strokeOpacity="0.25" />)}
      <circle cx={cx} cy={cy} r="2" fill="#ff4500" />
      <text x={cx} y={h-14} fill="#b8c5d6" opacity="0.55" fontSize="10" textAnchor="middle" fontFamily="var(--font-mono)">应变玫瑰 σᵢⱼ</text>
    </svg>
  );
}

/* Ice-shell cross-section */
function ThavariIceShell({ w = 900, h = 420 }) {
  return (
    <svg viewBox={`0 0 ${w} ${h}`} width="100%">
      <defs>
        <linearGradient id="thavari-col" x1="0" x2="0" y1="0" y2="1">
          <stop offset="0"   stopColor="#0a1a2e" />
          <stop offset="0.25" stopColor="#b8c5d610" />
          <stop offset="0.26" stopColor="#050709" />
          <stop offset="1"   stopColor="#020405" />
        </linearGradient>
      </defs>
      <rect x="0" y="0" width={w} height={h} fill="url(#thavari-col)" />
      {/* space */}
      <text x={14} y={22} fill="#6c7a8e" fontSize="10" letterSpacing="0.25em" fontFamily="var(--font-sans-cjk)">真空 · VACUUM</text>
      {Array.from({length:30}).map((_,i)=>(
        <circle key={i} cx={Math.random()*w} cy={Math.random()*60} r="0.8" fill="#b8c5d6" opacity="0.6" />
      ))}
      {/* ice shell */}
      <g>
        <line x1="0" x2={w} y1="80" y2="80" stroke="#b8c5d6" strokeOpacity="0.5" />
        <line x1="0" x2={w} y1="180" y2="180" stroke="#b8c5d6" strokeOpacity="0.5" />
        <text x={14} y="100" fill="#b8c5d6" opacity="0.7" fontSize="11" fontFamily="var(--font-sans-cjk)">冰壳 ICE SHELL</text>
        <text x={14} y="116" fill="#b8c5d6" opacity="0.5" fontSize="10" fontFamily="var(--font-mono)">~4.2 km</text>
        {/* crack / Magellan-I melt probe */}
        <g transform={`translate(${w*0.62}, 80)`}>
          <line x1="0" x2="0" y1="0" y2="100" stroke="#ff4500" strokeOpacity="0.8" strokeWidth="1.5" />
          <circle cx="0" cy="98" r="4" fill="#ff4500" />
          <text x={10} y="14" fill="#ff4500" fontSize="10" fontFamily="var(--font-sans-cjk)">麦哲伦一号 · 2300</text>
        </g>
      </g>
      {/* subsurface ocean */}
      <rect x="0" y="180" width={w} height={h - 180} fill="#040b14" />
      <text x={14} y="204" fill="#b8c5d6" opacity="0.7" fontSize="11" fontFamily="var(--font-sans-cjk)">地下海 SUBGLACIAL OCEAN · 震族所在</text>
      {/* hydrothermal vents + biolume */}
      {[0.18, 0.40, 0.55, 0.75, 0.88].map((px, i) => (
        <g key={i} transform={`translate(${px * w}, ${h - 20})`}>
          <polygon points="-10,0 10,0 0,-30" fill="#1a0f14" stroke="#6c7a8e" strokeOpacity="0.3" />
          <circle cx="0" cy="-34" r="12" fill="#ff4500" opacity="0.35" />
          <circle cx="0" cy="-34" r="5"  fill="#ff4500" opacity="0.8" />
          {/* biolume sparks */}
          {Array.from({length: 5}).map((_, j) => (
            <circle key={j} cx={(Math.random()-0.5)*120} cy={-30 - Math.random()*140} r="1.3" fill="#00ffc8" opacity={0.5 + Math.random()*0.4} />
          ))}
        </g>
      ))}
      {/* host giant implied by tidal strain */}
      <g transform={`translate(${w - 80}, 40)`}>
        <circle r="22" fill="none" stroke="#b8c5d6" strokeOpacity="0.5" strokeDasharray="2 3" />
        <text x={0} y="50" fill="#b8c5d6" opacity="0.55" fontSize="10" textAnchor="middle" fontFamily="var(--font-sans-cjk)">[推断] 宿主巨行星</text>
      </g>
    </svg>
  );
}

/* Octothrone: eight-minded rosette */
function ThavariOctothrone({ w = 320, h = 320 }) {
  const cx = w/2, cy = h/2, R = 110;
  const nodes = Array.from({length:8}).map((_,i)=>{
    const a = i * Math.PI * 2 / 8 - Math.PI/2;
    return { x: cx + Math.cos(a) * R, y: cy + Math.sin(a) * R };
  });
  const edges = [];
  for (let i = 0; i < 8; i++) for (let j = i+1; j < 8; j++) {
    edges.push(<line key={`${i}-${j}`} x1={nodes[i].x} y1={nodes[i].y} x2={nodes[j].x} y2={nodes[j].y}
                     stroke="#00ffc8" strokeOpacity="0.22" />);
  }
  return (
    <svg viewBox={`0 0 ${w} ${h}`} width="100%">
      {edges}
      {nodes.map((n, i) => (
        <g key={i}>
          <circle cx={n.x} cy={n.y} r="12" fill="#050709" stroke="#00ffc8" />
          <circle cx={n.x} cy={n.y} r="4" fill="#00ffc8" />
        </g>
      ))}
      <circle cx={cx} cy={cy} r="22" fill="none" stroke="#ff4500" strokeOpacity="0.6" strokeDasharray="2 3" />
      <text x={cx} y={cy+4} fill="#ff4500" fontSize="10" textAnchor="middle" fontFamily="var(--font-sans-cjk)">共焦</text>
    </svg>
  );
}

/* =================================================================
   HUMAN
   ================================================================= */

/* Solar system (to-scale-ish, stylized) with 2200–2300 settlements */
function HumanSolarSystem({ w = 1000, h = 260 }) {
  const bodies = [
    { name: '太阳', en: 'Sun',     au: 0.00, r: 16, color: '#ff8c00', label: '' },
    { name: '水星', en: 'Mercury', au: 0.39, r: 2.5, color: '#8a8172', label: '' },
    { name: '金星', en: 'Venus',   au: 0.72, r: 4,   color: '#d4a86e', label: '研究站' },
    { name: '地球', en: 'Earth',   au: 1.00, r: 4,   color: '#4a90c9', label: '母行星' },
    { name: '月球', en: 'Luna',    au: 1.01, r: 1.8, color: '#c5c5c5', label: '工业 + 月背阵列' },
    { name: '火星', en: 'Mars',    au: 1.52, r: 3,   color: '#c54d2c', label: '永久定居 · 2060' },
    { name: '谷神', en: 'Ceres',   au: 2.77, r: 1.2, color: '#8c7e70', label: '带矿' },
    { name: '木卫二',en:'Europa',  au: 5.20, r: 2.5, color: '#c8b898', label: '冰下研究' },
    { name: '土卫六',en:'Titan',   au: 9.58, r: 3,   color: '#e0a86e', label: '大气殖民 · 2140' },
  ];
  const scaleAU = x => 40 + Math.pow(x, 0.45) * 220;
  return (
    <svg viewBox={`0 0 ${w} ${h}`} width="100%" style={{display:'block'}}>
      <line x1="20" x2={w-20} y1={h/2} y2={h/2} stroke="#0a0e1a22" />
      {bodies.map((b, i) => {
        const x = scaleAU(b.au);
        return (
          <g key={b.name}>
            <circle cx={x} cy={h/2} r={b.r} fill={b.color} />
            <text x={x} y={h/2 - b.r - 8} fill="#0a0e1a" opacity="0.85" fontSize="11" textAnchor="middle" fontFamily="var(--font-sans-cjk)">{b.name}</text>
            {b.label && <text x={x} y={h/2 + b.r + 18} fill="#0a0e1a" opacity="0.55" fontSize="10" textAnchor="middle" fontFamily="var(--font-sans-cjk)">{b.label}</text>}
            {b.au > 0 && <text x={x} y={h/2 + b.r + 32} fill="#0066cc" opacity="0.7" fontSize="9" textAnchor="middle" fontFamily="var(--font-mono)">{b.au.toFixed(2)} AU</text>}
          </g>
        );
      })}
      {/* axis label */}
      <text x={20} y={h-10} fill="#0a0e1a" opacity="0.5" fontSize="10" fontFamily="var(--font-mono)">人类聚居地 · 2200–2300</text>
    </svg>
  );
}

/* Magellan-I technical sketch */
function HumanMagellan({ w = 520, h = 220 }) {
  return (
    <svg viewBox={`0 0 ${w} ${h}`} width="100%">
      {/* body */}
      <rect x="120" y="90" width="240" height="40" fill="none" stroke="#0a0e1a" strokeOpacity="0.9" />
      <rect x="360" y="80" width="60" height="60" fill="none" stroke="#0a0e1a" strokeOpacity="0.9" />
      <circle cx="80" cy="110" r="24" fill="none" stroke="#0a0e1a" />
      <circle cx="80" cy="110" r="10" fill="#0066cc" />
      {/* fusion exhaust */}
      {Array.from({length:10}).map((_,i)=>(
        <line key={i} x1={420+i*8} x2={450+i*10} y1={110 - (i-5)*2} y2={110 - (i-5)*3}
              stroke="#ff8c00" strokeOpacity={0.6 - i*0.05} strokeWidth="2" />
      ))}
      {/* radiators */}
      <g stroke="#0a0e1a" strokeOpacity="0.6">
        <line x1="180" y1="90" x2="180" y2="40" />
        <line x1="180" y1="40" x2="240" y2="40" />
        <line x1="180" y1="130" x2="180" y2="180" />
        <line x1="180" y1="180" x2="240" y2="180" />
        <line x1="260" y1="90" x2="260" y2="40" />
        <line x1="260" y1="40" x2="320" y2="40" />
        <line x1="260" y1="130" x2="260" y2="180" />
        <line x1="260" y1="180" x2="320" y2="180" />
      </g>
      {/* annotations */}
      <g fontFamily="var(--font-mono)" fontSize="9" fill="#0a0e1a" opacity="0.7">
        <text x="56" y="150">[1] 熔探舱</text>
        <text x="150" y="38" textAnchor="end">[2] 辐射翅</text>
        <text x="250" y="38" textAnchor="end">[3] 燃料储槽</text>
        <text x="375" y="75">[4] 聚变炉</text>
        <text x="470" y="105">[5] 喷流 · 0.1c</text>
      </g>
      <text x="12" y="200" fill="#0a0e1a" opacity="0.55" fontSize="10" fontFamily="var(--font-sans-cjk)">麦哲伦一号 · 2250 发射 · 2300 抵波江 ε 冰卫星</text>
    </svg>
  );
}

/* Lifespan comparison — horizontal bars */
function HumanLifespan({ w = 900, h = 320 }) {
  const bars = [
    { label: '人类 (2025)',    en: 'Human 2025',    val: 80,   color: '#6c757d' },
    { label: '人类 (2280)',    en: 'Human 2280',    val: 150,  color: '#0066cc' },
    { label: '鲸语族 · 文化',  en: 'Ketoi culture', val: 120,  color: '#2e86ab' },
    { label: '涣族 · 工程',    en: 'Huvari eng.',   val: 250,  color: '#00d9ff' },
    { label: '震族 · 自然',    en: 'Thavari nat.',  val: 450,  color: '#00ffc8' },
    { label: '衡族 · 衡者',    en: 'Eoan Integri',  val: 800,  color: '#e8821f' },
    { label: '衡族 · 大衡',    en: 'Magni',         val: 1200, color: '#8b3a1f' },
  ];
  const maxV = 1300;
  const scale = v => 180 + v / maxV * (w - 220);
  return (
    <svg viewBox={`0 0 ${w} ${h}`} width="100%">
      {bars.map((b, i) => {
        const y = 28 + i * 38;
        return (
          <g key={b.label}>
            <text x="170" y={y + 14} fill="#0a0e1a" fontSize="12" textAnchor="end" fontFamily="var(--font-sans-cjk)">{b.label}</text>
            <rect x="180" y={y} width={scale(b.val) - 180} height="22" fill={b.color} opacity="0.85" />
            <text x={scale(b.val) + 8} y={y + 16} fill="#0a0e1a" fontSize="11" fontFamily="var(--font-mono)">{b.val} y</text>
          </g>
        );
      })}
      {/* human 2280 benchmark line */}
      <line x1={scale(150)} x2={scale(150)} y1="20" y2={h-20} stroke="#0066cc" strokeOpacity="0.35" strokeDasharray="3 3" />
      <text x={scale(150)+4} y={h-24} fill="#0066cc" fontSize="10" fontFamily="var(--font-mono)">当代人类健康寿命</text>
    </svg>
  );
}

/* Contact-delay diagram — light-years */
function HumanContactDelay({ w = 520, h = 260 }) {
  const cx = w/2, cy = h/2;
  const stars = [
    { name: '波江 ε',   ly: 10.5, color: '#00ffc8', angle: -1.1 },
    { name: 'Ross 128', ly: 11.0, color: '#e8821f', angle:  0.2 },
    { name: '天仓',     ly: 11.9, color: '#2e86ab', angle:  1.4 },
    { name: '82 G. Eri',ly: 19.7, color: '#00d9ff', angle:  2.7 },
  ];
  const scale = ly => 30 + ly * 7;
  return (
    <svg viewBox={`0 0 ${w} ${h}`} width="100%">
      {[5,10,15,20].map(r => (
        <g key={r}>
          <circle cx={cx} cy={cy} r={scale(r)} fill="none" stroke="#0a0e1a" strokeOpacity="0.15" strokeDasharray="2 3" />
          <text x={cx + scale(r) + 4} y={cy+4} fill="#0a0e1a" opacity="0.5" fontSize="10" fontFamily="var(--font-mono)">{r} ly</text>
        </g>
      ))}
      <circle cx={cx} cy={cy} r="5" fill="#ff8c00" />
      <text x={cx} y={cy - 10} fill="#0a0e1a" fontSize="10" textAnchor="middle" fontFamily="var(--font-sans-cjk)">太阳</text>
      {stars.map(s => {
        const x = cx + Math.cos(s.angle) * scale(s.ly);
        const y = cy + Math.sin(s.angle) * scale(s.ly);
        return (
          <g key={s.name}>
            <line x1={cx} y1={cy} x2={x} y2={y} stroke={s.color} strokeOpacity="0.55" strokeDasharray="3 3" />
            <circle cx={x} cy={y} r="5" fill={s.color} />
            <text x={x+8} y={y+4} fill="#0a0e1a" fontSize="11" fontFamily="var(--font-sans-cjk)">{s.name}</text>
            <text x={x+8} y={y+18} fill="#0a0e1a" opacity="0.6" fontSize="10" fontFamily="var(--font-mono)">
              {s.ly} ly · rt {(s.ly*2).toFixed(1)} y
            </text>
          </g>
        );
      })}
    </svg>
  );
}

/* =================================================================
   Home — species card previews (each a tiny specialized viz)
   ================================================================= */

function HomeCardViz({ id }) {
  if (id === 'eoani') {
    return (
      <svg viewBox="0 0 200 140" width="100%" height="100%" style={{background:'linear-gradient(90deg,#ff6b35,#8b3a1f 60%,#050302)'}}>
        <circle cx="100" cy="70" r="60" fill="url(#eoani-home)" />
        <defs>
          <radialGradient id="eoani-home" cx="0.25" cy="0.5">
            <stop offset="0" stopColor="#ff6b35" />
            <stop offset="0.5" stopColor="#8b3a1f" />
            <stop offset="1" stopColor="#1a0e0a" />
          </radialGradient>
        </defs>
      </svg>
    );
  }
  if (id === 'ketoi') {
    return (
      <div style={{width:'100%', height:'100%', background:'#0a2540'}}>
        <KetoiSpectrogram w={320} h={140} bars={120} />
      </div>
    );
  }
  if (id === 'huvari') {
    return (
      <svg viewBox="0 0 200 140" width="100%" height="100%" style={{background:'#050b12'}}>
        {Array.from({length:10}).map((_,i)=>(
          <line key={`v${i}`} x1={i*22} x2={i*22} y1="0" y2="140" stroke="#00d9ff" strokeOpacity="0.25" />
        ))}
        {Array.from({length:7}).map((_,i)=>(
          <line key={`h${i}`} x1="0" x2="200" y1={i*22} y2={i*22} stroke="#00d9ff" strokeOpacity="0.25" />
        ))}
        <circle cx="100" cy="70" r="28" fill="none" stroke="#ffd700" strokeOpacity="0.6" />
        <circle cx="100" cy="70" r="48" fill="none" stroke="#ffd700" strokeOpacity="0.3" />
        <circle cx="100" cy="70" r="6" fill="#00ffa3" />
      </svg>
    );
  }
  if (id === 'thavari') {
    return (
      <div style={{width:'100%', height:'100%', background:'#000', display:'flex', alignItems:'center', justifyContent:'center'}}>
        <ThavariTensorRose w={160} h={160} />
      </div>
    );
  }
  if (id === 'human') {
    return (
      <div style={{width:'100%', height:'100%', background:'#f8f9fa', display:'flex', alignItems:'center'}}>
        <HumanSolarSystem w={320} h={140} />
      </div>
    );
  }
  return null;
}

Object.assign(window, {
  EoaniTidalLock, EoaniMagneticField, EoaniMillenniumSlider,
  KetoiSpectrogram, KetoiEchoRadar, KetoiDownwardAstronomy, KetoiPolarization, KetoiWaveDivider,
  HuvariFieldLines, HuvariLattice, HuvariCleanVsCarbon,
  ThavariTensorRose, ThavariIceShell, ThavariOctothrone,
  HumanSolarSystem, HumanMagellan, HumanLifespan, HumanContactDelay,
  HomeCardViz
});
