'use strict';

/* ═══════════════════════════════════════════════════════════════════════
   TrigCircleSim — premium interactive unit-circle + graph visualisation
   v3 — maximum polish
   ═══════════════════════════════════════════════════════════════════════ */

const _TC_NOTABLE = [
  { a: 0,               l: '0',     d: '0°'   },
  { a: Math.PI / 6,     l: 'π/6',   d: '30°'  },
  { a: Math.PI / 4,     l: 'π/4',   d: '45°'  },
  { a: Math.PI / 3,     l: 'π/3',   d: '60°'  },
  { a: Math.PI / 2,     l: 'π/2',   d: '90°'  },
  { a: 2*Math.PI / 3,   l: '2π/3',  d: '120°' },
  { a: 3*Math.PI / 4,   l: '3π/4',  d: '135°' },
  { a: 5*Math.PI / 6,   l: '5π/6',  d: '150°' },
  { a: Math.PI,         l: 'π',     d: '180°' },
  { a: 7*Math.PI / 6,   l: '7π/6',  d: '210°' },
  { a: 5*Math.PI / 4,   l: '5π/4',  d: '225°' },
  { a: 4*Math.PI / 3,   l: '4π/3',  d: '240°' },
  { a: 3*Math.PI / 2,   l: '3π/2',  d: '270°' },
  { a: 5*Math.PI / 3,   l: '5π/3',  d: '300°' },
  { a: 7*Math.PI / 4,   l: '7π/4',  d: '315°' },
  { a: 11*Math.PI / 6,  l: '11π/6', d: '330°' },
];

const _TC = {
  sin: '#EF476F', cos: '#06D6E0', tan: '#FFD166', cot: '#7BF5A4',
  point: '#9B5DE5', violet: '#9B5DE5',
};

function _tcRgb(hex) {
  const n = parseInt(hex.slice(1), 16);
  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
function _tcRgba(hex, a) {
  const [r, g, b] = _tcRgb(hex);
  return `rgba(${r},${g},${b},${a})`;
}

class TrigCircleSim {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.W = 0; this.H = 0; this.dpr = 1;

    this.angle     = Math.PI / 4;
    this.showSin   = true;
    this.showCos   = true;
    this.showTan   = false;
    this.showCot   = false;
    this.showGraph  = true;
    this.graphFn   = 'sin';
    this.snapToNotable = true;
    this.animating = false;

    this._cx = 0; this._cy = 0; this._r = 0;
    this._gx = 0; this._gw = 0; this._gh = 0; this._gy = 0;

    this._drag   = false;
    this._hover  = false;
    this._raf    = null;
    this._animTarget = null;
    this._animSpeed  = 3;
    this._idlePulse  = 0;
    this._idleRaf    = null;

    /* snap particles */
    this._particles = [];
    this._lastSnap  = -1;

    this.onUpdate = null;

    this._bindEvents();
    this._ro = new ResizeObserver(() => { this.fit(); this.draw(); });
    this._ro.observe(canvas.parentElement);
  }

  /* ═══ Public ═══════════════════════════════════════════════════════ */

  fit() {
    const p = this.canvas.parentElement.getBoundingClientRect();
    this.dpr = window.devicePixelRatio || 1;
    this.W = p.width || 800; this.H = p.height || 500;
    this.canvas.width  = this.W * this.dpr;
    this.canvas.height = this.H * this.dpr;
    this.canvas.style.width  = this.W + 'px';
    this.canvas.style.height = this.H + 'px';
    this._layout();
  }

  draw() {
    const c = this.ctx;
    c.save(); c.scale(this.dpr, this.dpr);
    c.clearRect(0, 0, this.W, this.H);

    this._drawBg(c);
    this._drawCircle(c);
    if (this.showGraph) { this._drawDivider(c); this._drawGraph(c); }
    this._drawParticles(c);
    if (window.LabFX) LabFX.particles.draw(c);

    c.restore();
    this._fireUpdate();
  }

  setAngle(a)  { this.angle = this._norm(a); this.draw(); }
  setGraphFn(f){ this.graphFn = f; this.draw(); }

  toggleLayer(n, v) {
    if (n === 'sin')  this.showSin = v;
    if (n === 'cos')  this.showCos = v;
    if (n === 'tan')  this.showTan = v;
    if (n === 'cot')  this.showCot = v;
    if (n === 'graph') this.showGraph = v;
    this._layout(); this.draw();
  }

  goToAngle(rad) {
    this._animTarget = this._norm(rad);
    if (!this.animating) this._startAnim();
  }

  start() { this._startIdle(); }
  stop()  { this._stopAnim(); this._stopIdle(); }

  stats() {
    const a = this.angle, s = Math.sin(a), co = Math.cos(a);
    const t  = Math.abs(co) > 1e-9 ? s / co : undefined;
    const ct = Math.abs(s)  > 1e-9 ? co / s : undefined;
    const deg = a * 180 / Math.PI;
    const q = a < Math.PI/2 ? 1 : a < Math.PI ? 2 : a < 3*Math.PI/2 ? 3 : 4;
    return { angle: a, deg, radLabel: this._radLbl(a), sin: s, cos: co, tan: t, cot: ct, quadrant: q };
  }

  /* ═══ Layout ═══════════════════════════════════════════════════════ */

  _layout() {
    const m = 44;
    if (this.showGraph) {
      const cW = this.W * 0.50;
      this._r  = Math.min(cW - m * 2, this.H - m * 2) / 2 * 0.76;
      this._cx = cW / 2;
      this._cy = this.H / 2;
      this._gx = cW + 24;
      this._gw = this.W - this._gx - m;
      this._gh = this.H - m * 2;
      this._gy = m;
    } else {
      this._r  = Math.min(this.W - m * 2, this.H - m * 2) / 2 * 0.76;
      this._cx = this.W / 2;
      this._cy = this.H / 2;
    }
    this._r = Math.max(55, this._r);
  }

  /* ═══ Background ═══════════════════════════════════════════════════ */

  _drawBg(c) {
    const g = c.createRadialGradient(this._cx, this._cy, 0, this._cx, this._cy, this._r * 2.4);
    g.addColorStop(0, 'rgba(155,93,229,0.055)');
    g.addColorStop(0.5,'rgba(155,93,229,0.02)');
    g.addColorStop(1, 'rgba(0,0,0,0)');
    c.fillStyle = g; c.fillRect(0, 0, this.W, this.H);

    /* decorative rings */
    c.strokeStyle = 'rgba(255,255,255,0.016)'; c.lineWidth = 1;
    for (let i = 1; i <= 3; i++) {
      c.beginPath(); c.arc(this._cx, this._cy, this._r * (0.5 + i * 0.35), 0, Math.PI * 2);
      c.stroke();
    }
  }

  /* ═══ Unit Circle ══════════════════════════════════════════════════ */

  _drawCircle(c) {
    const cx = this._cx, cy = this._cy, r = this._r;
    const a = this.angle;
    const cosA = Math.cos(a), sinA = Math.sin(a);
    const px = cx + r * cosA, py = cy - r * sinA;
    const ext = Math.min(55, r * 0.35);

    /* ── quadrant soft fill ── */
    const q = this.stats().quadrant;
    const qS = [0, Math.PI/2, Math.PI, 3*Math.PI/2][q-1];
    c.fillStyle = 'rgba(155,93,229,0.022)';
    c.beginPath(); c.moveTo(cx, cy);
    c.arc(cx, cy, r, -(qS + Math.PI/2), -qS);
    c.closePath(); c.fill();

    /* ── degree tick marks (every 10°, bigger every 30°) ── */
    for (let deg = 0; deg < 360; deg += 10) {
      const rad = deg * Math.PI / 180;
      const big = deg % 30 === 0;
      const len = big ? 8 : 4;
      const x1 = cx + (r - len) * Math.cos(rad);
      const y1 = cy - (r - len) * Math.sin(rad);
      const x2 = cx + r * Math.cos(rad);
      const y2 = cy - r * Math.sin(rad);
      c.strokeStyle = big ? 'rgba(255,255,255,0.12)' : 'rgba(255,255,255,0.05)';
      c.lineWidth = big ? 1.5 : 1;
      c.beginPath(); c.moveTo(x1, y1); c.lineTo(x2, y2); c.stroke();
    }

    /* ── axes (gradient fade) ── */
    const axGrad = (x1,y1,x2,y2) => {
      const g = c.createLinearGradient(x1,y1,x2,y2);
      g.addColorStop(0,   'rgba(255,255,255,0.0)');
      g.addColorStop(0.08,'rgba(255,255,255,0.30)');
      g.addColorStop(0.5, 'rgba(255,255,255,0.50)');
      g.addColorStop(0.92,'rgba(255,255,255,0.30)');
      g.addColorStop(1,   'rgba(255,255,255,0.0)');
      return g;
    };
    c.lineWidth = 1.5;
    c.strokeStyle = axGrad(cx - r - ext, cy, cx + r + ext, cy);
    c.beginPath(); c.moveTo(cx - r - ext, cy); c.lineTo(cx + r + ext, cy); c.stroke();
    c.strokeStyle = axGrad(cx, cy + r + ext, cx, cy - r - ext);
    c.beginPath(); c.moveTo(cx, cy + r + ext); c.lineTo(cx, cy - r - ext); c.stroke();

    /* arrows */
    this._arrowH(c, cx + r + ext, cy, 0, 'rgba(255,255,255,0.5)');
    this._arrowH(c, cx, cy - r - ext, -Math.PI/2, 'rgba(255,255,255,0.5)');

    /* axis labels */
    c.font = '700 13px Manrope,sans-serif'; c.fillStyle = 'rgba(255,255,255,0.45)';
    c.textAlign = 'left'; c.textBaseline = 'top';
    c.fillText('x', cx + r + ext - 12, cy + 8);
    c.textAlign = 'right'; c.textBaseline = 'bottom';
    c.fillText('y', cx - 10, cy - r - ext + 16);

    /* ±1 ticks & labels */
    c.strokeStyle = 'rgba(255,255,255,0.30)'; c.lineWidth = 1.5;
    c.font = '600 11px Manrope,sans-serif'; c.fillStyle = 'rgba(255,255,255,0.45)';
    const tk = 6;
    c.beginPath(); c.moveTo(cx+r, cy-tk); c.lineTo(cx+r, cy+tk); c.stroke();
    c.textAlign='center'; c.textBaseline='top'; c.fillText('1', cx+r, cy+9);
    c.beginPath(); c.moveTo(cx-r, cy-tk); c.lineTo(cx-r, cy+tk); c.stroke();
    c.fillText('−1', cx-r, cy+9);
    c.beginPath(); c.moveTo(cx-tk, cy-r); c.lineTo(cx+tk, cy-r); c.stroke();
    c.textAlign='right'; c.textBaseline='middle'; c.fillText('1', cx-10, cy-r);
    c.beginPath(); c.moveTo(cx-tk, cy+r); c.lineTo(cx+tk, cy+r); c.stroke();
    c.fillText('−1', cx-10, cy+r);

    /* origin dot */
    c.fillStyle = 'rgba(255,255,255,0.35)';
    c.beginPath(); c.arc(cx, cy, 2.5, 0, Math.PI*2); c.fill();

    /* ── unit circle (multi-layer) ── */
    c.strokeStyle = _tcRgba(_TC.violet, 0.05 + Math.sin(this._idlePulse) * 0.02);
    c.lineWidth = 14;
    c.beginPath(); c.arc(cx, cy, r, 0, Math.PI*2); c.stroke();
    c.strokeStyle = 'rgba(255,255,255,0.13)'; c.lineWidth = 2;
    c.beginPath(); c.arc(cx, cy, r, 0, Math.PI*2); c.stroke();

    /* ── notable angle dots + labels ── */
    for (const n of _TC_NOTABLE) {
      const nx = cx + r * Math.cos(n.a), ny = cy - r * Math.sin(n.a);
      const act = Math.abs(a - n.a) < 0.03;
      if (act) {
        c.fillStyle = _tcRgba(_TC.violet, 0.5);
        c.shadowColor = _TC.violet; c.shadowBlur = 10;
        c.beginPath(); c.arc(nx, ny, 5, 0, Math.PI*2); c.fill();
        c.shadowBlur = 0;
        c.strokeStyle = _TC.violet; c.lineWidth = 1.5;
        c.beginPath(); c.arc(nx, ny, 5, 0, Math.PI*2); c.stroke();
      } else {
        c.fillStyle = 'rgba(255,255,255,0.12)';
        c.beginPath(); c.arc(nx, ny, 2.5, 0, Math.PI*2); c.fill();
      }
      if (n.l && n.l !== '0') {
        const d = act ? 24 : 20;
        const lx = cx + (r + d) * Math.cos(n.a);
        const ly = cy - (r + d) * Math.sin(n.a);
        c.font = act ? '700 11px Manrope,sans-serif' : '400 9px Manrope,sans-serif';
        c.fillStyle = act ? _tcRgba(_TC.violet, 0.95) : 'rgba(255,255,255,0.18)';
        c.textAlign = 'center'; c.textBaseline = 'middle';
        c.fillText(n.l, lx, ly);
      }
    }

    /* ── angle arc ── */
    if (a > 0.015) {
      const ar = Math.min(r * 0.22, 44);
      c.fillStyle = _tcRgba(_TC.violet, 0.06);
      c.beginPath(); c.moveTo(cx, cy); c.arc(cx, cy, ar, 0, -a, true); c.closePath(); c.fill();
      const ag = c.createConicGradient(0, cx, cy);
      ag.addColorStop(0, _tcRgba(_TC.violet, 0.7));
      ag.addColorStop(Math.min(a / (Math.PI*2), 0.99), _tcRgba(_TC.violet, 0.25));
      ag.addColorStop(1, _tcRgba(_TC.violet, 0.0));
      c.strokeStyle = ag; c.lineWidth = 2.5;
      c.beginPath(); c.arc(cx, cy, ar, 0, -a, true); c.stroke();
      /* label */
      const mid = a / 2, lr = ar + 18;
      c.font = 'bold 12px Manrope,sans-serif'; c.fillStyle = _TC.violet;
      c.textAlign = 'center'; c.textBaseline = 'middle';
      c.fillText(this._radLbl(a), cx + lr * Math.cos(-mid), cy + lr * Math.sin(-mid));
    }

    /* ── radius ── */
    const rg = c.createLinearGradient(cx, cy, px, py);
    rg.addColorStop(0, 'rgba(255,255,255,0.12)'); rg.addColorStop(1, 'rgba(255,255,255,0.40)');
    c.strokeStyle = rg; c.lineWidth = 1.5;
    c.beginPath(); c.moveTo(cx, cy); c.lineTo(px, py); c.stroke();

    /* ── projection dashes ── */
    c.strokeStyle = 'rgba(255,255,255,0.08)'; c.lineWidth = 1;
    c.setLineDash([4, 4]);
    c.beginPath(); c.moveTo(px, py); c.lineTo(px, cy); c.stroke();
    c.beginPath(); c.moveTo(px, py); c.lineTo(cx, py); c.stroke();
    c.setLineDash([]);

    const projX = cx + r * cosA;

    /* ── triangle fill (sin+cos) ── */
    if (this.showSin && this.showCos && Math.abs(cosA) > 0.04 && Math.abs(sinA) > 0.04) {
      c.fillStyle = 'rgba(155,93,229,0.035)';
      c.beginPath(); c.moveTo(cx, cy); c.lineTo(projX, cy); c.lineTo(px, py); c.closePath(); c.fill();
    }

    /* ═══ trig segments ═══ */

    if (this.showCos) {
      if (window.LabFX) {
        LabFX.glow.drawGlow(c, () => { this._glowLine(c, cx, cy, projX, cy, _TC.cos, 4); }, { color: '#06D6E0', intensity: 4 });
      } else {
        this._glowLine(c, cx, cy, projX, cy, _TC.cos, 4);
      }
      c.font = 'bold 12px Manrope,sans-serif'; c.fillStyle = _TC.cos;
      c.textAlign = 'center'; c.textBaseline = sinA >= 0 ? 'top' : 'bottom';
      c.fillText('cos', (cx + projX) / 2, cy + (sinA >= 0 ? 12 : -12));
    }

    if (this.showSin) {
      if (window.LabFX) {
        LabFX.glow.drawGlow(c, () => { this._glowLine(c, projX, cy, px, py, _TC.sin, 4); }, { color: '#06D6E0', intensity: 4 });
      } else {
        this._glowLine(c, projX, cy, px, py, _TC.sin, 4);
      }
      c.font = 'bold 12px Manrope,sans-serif'; c.fillStyle = _TC.sin;
      c.textAlign = cosA >= 0 ? 'left' : 'right'; c.textBaseline = 'middle';
      c.fillText('sin', projX + (cosA >= 0 ? 9 : -9), (cy + py) / 2);
    }

    if (this.showTan && Math.abs(cosA) > 0.025) {
      const tanV = sinA / cosA;
      if (Math.abs(tanV) < 10) {
        const tX = cosA >= 0 ? cx + r : cx - r;
        const tY = cosA >= 0 ? cy - r * tanV : cy + r * tanV;
        /* faint tangent guide line */
        c.strokeStyle = _tcRgba(_TC.tan, 0.06); c.lineWidth = 1;
        c.beginPath(); c.moveTo(tX, cy - r - ext); c.lineTo(tX, cy + r + ext); c.stroke();
        this._glowLine(c, tX, cy, tX, tY, _TC.tan, 3.5);
        c.strokeStyle = _tcRgba(_TC.tan, 0.18); c.lineWidth = 1;
        c.setLineDash([5, 4]); c.beginPath(); c.moveTo(px, py); c.lineTo(tX, tY); c.stroke(); c.setLineDash([]);
        c.fillStyle = _TC.tan; c.shadowColor = _TC.tan; c.shadowBlur = 8;
        c.beginPath(); c.arc(tX, tY, 4.5, 0, Math.PI*2); c.fill(); c.shadowBlur = 0;
        c.font = 'bold 12px Manrope,sans-serif'; c.fillStyle = _TC.tan;
        c.textAlign = cosA >= 0 ? 'left' : 'right'; c.textBaseline = 'middle';
        c.fillText('tg', tX + (cosA >= 0 ? 8 : -8), (cy + tY) / 2);
      }
    }

    if (this.showCot && Math.abs(sinA) > 0.025) {
      const cotV = cosA / sinA;
      if (Math.abs(cotV) < 10) {
        const cX = sinA >= 0 ? cx + r * cotV : cx - r * cotV;
        const cY = sinA >= 0 ? cy - r : cy + r;
        c.strokeStyle = _tcRgba(_TC.cot, 0.06); c.lineWidth = 1;
        c.beginPath(); c.moveTo(cx - r - ext, cY); c.lineTo(cx + r + ext, cY); c.stroke();
        this._glowLine(c, cx, cY, cX, cY, _TC.cot, 3.5);
        c.strokeStyle = _tcRgba(_TC.cot, 0.18); c.lineWidth = 1;
        c.setLineDash([5, 4]); c.beginPath(); c.moveTo(px, py); c.lineTo(cX, cY); c.stroke(); c.setLineDash([]);
        c.fillStyle = _TC.cot; c.shadowColor = _TC.cot; c.shadowBlur = 8;
        c.beginPath(); c.arc(cX, cY, 4.5, 0, Math.PI*2); c.fill(); c.shadowBlur = 0;
        c.font = 'bold 12px Manrope,sans-serif'; c.fillStyle = _TC.cot;
        c.textAlign = 'center'; c.textBaseline = sinA >= 0 ? 'bottom' : 'top';
        c.fillText('ctg', (cx + cX) / 2, cY + (sinA >= 0 ? -8 : 8));
      }
    }

    /* ── right-angle marker ── */
    if (this.showSin && this.showCos && Math.abs(cosA) > 0.06 && Math.abs(sinA) > 0.06) {
      const sz = 8, dx = cosA > 0 ? -sz : sz, dy = sinA > 0 ? sz : -sz;
      c.strokeStyle = 'rgba(255,255,255,0.18)'; c.lineWidth = 1;
      c.beginPath(); c.moveTo(projX+dx, cy); c.lineTo(projX+dx, cy-dy); c.lineTo(projX, cy-dy); c.stroke();
    }

    /* ── axis value badges ── */
    if (this.showSin && Math.abs(sinA) > 0.04)
      this._badge(c, cx - 12, py, this._fmt(sinA), _TC.sin, 'right', 'middle');
    if (this.showCos && Math.abs(cosA) > 0.04)
      this._badge(c, projX, cy + 17, this._fmt(cosA), _TC.cos, 'center', 'top');

    /* ── main point ── */
    const ps = this._hover || this._drag ? 10 : 8;
    const blur = this._hover || this._drag ? 22 : 16;
    c.fillStyle = _tcRgba(_TC.point, 0.10);
    c.beginPath(); c.arc(px, py, ps + 10, 0, Math.PI*2); c.fill();
    c.shadowColor = _TC.point; c.shadowBlur = blur;
    c.fillStyle = _TC.point;
    c.beginPath(); c.arc(px, py, ps, 0, Math.PI*2); c.fill();
    c.shadowBlur = 0;
    c.fillStyle = 'rgba(255,255,255,0.85)';
    c.beginPath(); c.arc(px, py, ps * 0.35, 0, Math.PI*2); c.fill();
    c.strokeStyle = 'rgba(255,255,255,0.50)'; c.lineWidth = 2;
    c.beginPath(); c.arc(px, py, ps, 0, Math.PI*2); c.stroke();

    /* ── coordinate tooltip ── */
    this._tooltip(c, px, py, cosA, sinA);

    /* ── quadrant roman numeral ── */
    const qOff = r * 0.46;
    const qx = (q===1||q===4) ? cx+qOff : cx-qOff;
    const qy = (q<=2) ? cy-qOff : cy+qOff;
    c.font = 'bold 22px Manrope,sans-serif'; c.fillStyle = _tcRgba(_TC.violet, 0.07);
    c.textAlign = 'center'; c.textBaseline = 'middle';
    c.fillText(['I','II','III','IV'][q-1]||'', qx, qy);

    /* ── sign pills per quadrant ── */
    this._quadSigns(c, cx, cy, r);

    /* ── Pythagorean identity bar ── */
    this._pythBar(c);

    /* ── connection line to graph ── */
    if (this.showGraph) this._connLine(c, px, py, sinA, cosA);
  }

  /* ═══ Connection line: circle <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> graph ═══════════════════════════════ */

  _connLine(c, px, py, sinA, cosA) {
    const fn = this.graphFn;
    const val = fn === 'sin' ? sinA : fn === 'cos' ? cosA :
                fn === 'tan' ? (Math.abs(cosA)>0.02 ? sinA/cosA : null) :
                (Math.abs(sinA)>0.02 ? cosA/sinA : null);
    if (val === null || !isFinite(val)) return;

    const yR = (fn === 'tan' || fn === 'cot') ? 4 : 1.5;
    if (Math.abs(val) > yR * 2) return;

    const gy = this._gy, gh = this._gh;
    const targetY = gy + gh/2 - val / yR * (gh/2);

    /* source Y = py for sin, cy for cos, depends on fn */
    const srcY = (fn === 'sin') ? py : (fn === 'cos') ? this._cy : py;
    const srcX = (fn === 'sin' || fn === 'tan') ? px : this._cx;

    c.strokeStyle = _tcRgba(_TC[fn] || _TC.sin, 0.12);
    c.lineWidth = 1;
    c.setLineDash([3, 5]);
    c.beginPath(); c.moveTo(srcX, srcY); c.lineTo(this._gx, targetY); c.stroke();
    c.setLineDash([]);
  }

  /* ═══ Quadrant sign pills ═══════════════════════════════════════════ */

  _quadSigns(c, cx, cy, r) {
    const signs = [
      { q: 1, s:'+', co:'+', t:'+' }, { q: 2, s:'+', co:'−', t:'−' },
      { q: 3, s:'−', co:'−', t:'+' }, { q: 4, s:'−', co:'+', t:'−' },
    ];
    const curr = this.stats().quadrant;
    const off = r * 0.78;
    for (const sg of signs) {
      const sx = (sg.q===1||sg.q===4) ? cx+off : cx-off;
      const sy = (sg.q<=2) ? cy-off : cy+off;
      const isCurr = sg.q === curr;
      c.font = '500 8px Manrope,sans-serif';
      c.fillStyle = isCurr ? 'rgba(255,255,255,0.25)' : 'rgba(255,255,255,0.07)';
      c.textAlign = 'center'; c.textBaseline = 'middle';
      const txt = `s${sg.s} c${sg.co} t${sg.t}`;
      c.fillText(txt, sx, sy);
    }
  }

  /* ═══ Pythagorean identity bar ══════════════════════════════════════ */

  _pythBar(c) {
    const s = Math.sin(this.angle), co = Math.cos(this.angle);
    const sin2 = s * s, cos2 = co * co;
    const bw = Math.min(this._r * 1.4, 180);
    const bh = 6;
    const bx = this._cx - bw / 2;
    const by = this._cy + this._r + 38;
    if (by + bh + 16 > this.H) return;

    /* background track */
    c.fillStyle = 'rgba(255,255,255,0.04)';
    c.beginPath(); c.roundRect(bx, by, bw, bh, 3); c.fill();

    /* sin² portion */
    const sw = bw * sin2;
    if (sw > 0.5) {
      c.fillStyle = _tcRgba(_TC.sin, 0.5);
      c.beginPath(); c.roundRect(bx, by, sw, bh, [3,0,0,3]); c.fill();
    }

    /* cos² portion */
    const cw = bw * cos2;
    if (cw > 0.5) {
      c.fillStyle = _tcRgba(_TC.cos, 0.5);
      c.beginPath(); c.roundRect(bx + sw, by, cw, bh, [0,3,3,0]); c.fill();
    }

    /* label */
    c.font = '500 9px Manrope,sans-serif'; c.fillStyle = 'rgba(255,255,255,0.25)';
    c.textAlign = 'center'; c.textBaseline = 'top';
    c.fillText(`sin² + cos² = 1`, this._cx, by + bh + 4);
  }

  /* ═══ Divider ══════════════════════════════════════════════════════ */

  _drawDivider(c) {
    const x = this._gx - 14;
    const pad = 20;
    const lg = c.createLinearGradient(x, pad, x, this.H - pad);
    lg.addColorStop(0,   'rgba(155,93,229,0.0)');
    lg.addColorStop(0.15,'rgba(155,93,229,0.18)');
    lg.addColorStop(0.5, 'rgba(155,93,229,0.28)');
    lg.addColorStop(0.85,'rgba(155,93,229,0.18)');
    lg.addColorStop(1,   'rgba(155,93,229,0.0)');
    c.strokeStyle = lg; c.lineWidth = 1;
    c.beginPath(); c.moveTo(x, pad); c.lineTo(x, this.H - pad); c.stroke();
    /* glow */
    const gg = c.createLinearGradient(x - 16, 0, x + 16, 0);
    gg.addColorStop(0, 'rgba(155,93,229,0.0)');
    gg.addColorStop(0.5, 'rgba(155,93,229,0.035)');
    gg.addColorStop(1, 'rgba(155,93,229,0.0)');
    c.fillStyle = gg; c.fillRect(x - 16, pad, 32, this.H - pad*2);
  }

  /* ═══ Graph ════════════════════════════════════════════════════════ */

  _drawGraph(c) {
    const gx = this._gx, gy = this._gy, gw = this._gw, gh = this._gh;
    if (gw < 50 || gh < 50) return;

    const fn = this.graphFn;
    const col = _TC[fn] || _TC.sin;
    const lbl = fn==='sin'?'y = sin x':fn==='cos'?'y = cos x':fn==='tan'?'y = tg x':'y = ctg x';
    const evFn = fn==='sin'?Math.sin:fn==='cos'?Math.cos:fn==='tan'?Math.tan:(x=>1/Math.tan(x));
    const yR = (fn==='tan'||fn==='cot') ? 4 : 1.5;
    const xMin = -0.25*Math.PI, xMax = 2.25*Math.PI;
    const sx = x => gx + (x-xMin)/(xMax-xMin)*gw;
    const sy = y => gy + gh/2 - y/yR*(gh/2);

    /* ── glass panel ── */
    const pp = 12;
    const px1 = gx-pp, py1 = gy-pp, pw = gw+pp*2, ph = gh+pp*2;
    c.fillStyle = 'rgba(10,10,20,0.50)';
    c.beginPath(); c.roundRect(px1, py1, pw, ph, 20); c.fill();
    /* gradient border */
    const bg = c.createLinearGradient(px1, py1, px1+pw, py1+ph);
    bg.addColorStop(0, 'rgba(155,93,229,0.18)');
    bg.addColorStop(0.3,'rgba(255,255,255,0.06)');
    bg.addColorStop(0.7,'rgba(255,255,255,0.06)');
    bg.addColorStop(1, 'rgba(155,93,229,0.18)');
    c.strokeStyle = bg; c.lineWidth = 1.5;
    c.beginPath(); c.roundRect(px1, py1, pw, ph, 20); c.stroke();
    /* top highlight */
    const hg = c.createLinearGradient(px1, py1, px1, py1+50);
    hg.addColorStop(0, 'rgba(255,255,255,0.025)'); hg.addColorStop(1, 'rgba(255,255,255,0.0)');
    c.fillStyle = hg;
    c.beginPath(); c.roundRect(px1+1, py1+1, pw-2, 50, [20,20,0,0]); c.fill();

    /* ── zero axis ── */
    const zy = sy(0);
    c.strokeStyle = 'rgba(255,255,255,0.14)'; c.lineWidth = 1;
    c.beginPath(); c.moveTo(gx, zy); c.lineTo(gx+gw, zy); c.stroke();
    /* y-axis on graph */
    const x0 = sx(0);
    if (x0 > gx + 4 && x0 < gx + gw - 4) {
      c.strokeStyle = 'rgba(255,255,255,0.08)'; c.lineWidth = 1;
      c.beginPath(); c.moveTo(x0, gy); c.lineTo(x0, gy+gh); c.stroke();
    }

    /* ±1 lines */
    if (fn==='sin'||fn==='cos') {
      c.strokeStyle = 'rgba(255,255,255,0.05)'; c.setLineDash([4, 4]);
      [1,-1].forEach(v => { c.beginPath(); c.moveTo(gx, sy(v)); c.lineTo(gx+gw, sy(v)); c.stroke(); });
      c.setLineDash([]);
      c.font='500 10px Manrope,sans-serif'; c.fillStyle='rgba(255,255,255,0.22)';
      c.textAlign='right'; c.textBaseline='middle';
      c.fillText('1', gx-5, sy(1)); c.fillText('−1', gx-5, sy(-1));
    }

    /* x ticks */
    const ticks = [[0,'0'],[Math.PI/2,'π/2'],[Math.PI,'π'],[3*Math.PI/2,'3π/2'],[2*Math.PI,'2π']];
    c.font='500 10px Manrope,sans-serif'; c.fillStyle='rgba(255,255,255,0.20)';
    c.textAlign='center'; c.textBaseline='top';
    for (const [v,l] of ticks) {
      const xx = sx(v);
      if (xx < gx+6 || xx > gx+gw-6) continue;
      c.strokeStyle='rgba(255,255,255,0.05)'; c.lineWidth=1;
      c.setLineDash([3,3]);
      c.beginPath(); c.moveTo(xx, gy); c.lineTo(xx, gy+gh); c.stroke();
      c.setLineDash([]);
      c.fillText(l, xx, gy+gh+6);
    }

    /* ── ghost curves (other functions, dimmed) ── */
    c.save();
    c.beginPath(); c.rect(gx, gy, gw, gh); c.clip();

    const allFns = [
      { id: 'sin', ev: Math.sin, c: _TC.sin },
      { id: 'cos', ev: Math.cos, c: _TC.cos },
      { id: 'tan', ev: Math.tan, c: _TC.tan },
      { id: 'cot', ev: x => 1/Math.tan(x), c: _TC.cot },
    ];
    const step = (xMax - xMin) / (gw * 1.5);

    for (const f of allFns) {
      if (f.id === fn) continue; /* skip active — draw it last */
      const show = (f.id==='sin'&&this.showSin) || (f.id==='cos'&&this.showCos) ||
                   (f.id==='tan'&&this.showTan) || (f.id==='cot'&&this.showCot);
      if (!show) continue;
      const yRg = (f.id==='tan'||f.id==='cot') ? 4 : 1.5;
      const syG = y => gy + gh/2 - y/yRg*(gh/2);
      c.strokeStyle = _tcRgba(f.c, 0.18); c.lineWidth = 1.5;
      c.beginPath(); let on = false;
      for (let x = xMin; x <= xMax; x += step) {
        const yv = f.ev(x);
        if (!isFinite(yv) || Math.abs(yv) > yRg*2) { on = false; continue; }
        const spx = sx(x), spy = syG(yv);
        if (!on) { c.moveTo(spx, spy); on = true; } else c.lineTo(spx, spy);
      }
      c.stroke();
    }

    /* gradient fill under active curve (sin/cos) */
    if (fn==='sin'||fn==='cos') {
      const pts = [];
      for (let x = xMin; x <= xMax; x += step) {
        const yv = evFn(x);
        if (isFinite(yv)) pts.push({ sx: sx(x), sy: sy(yv) });
      }
      if (pts.length > 2) {
        const fg = c.createLinearGradient(0, gy, 0, gy+gh);
        fg.addColorStop(0, _tcRgba(col, 0.10));
        fg.addColorStop(0.5, _tcRgba(col, 0.0));
        fg.addColorStop(1, _tcRgba(col, 0.10));
        c.fillStyle = fg; c.beginPath();
        c.moveTo(pts[0].sx, zy);
        pts.forEach(p => c.lineTo(p.sx, p.sy));
        c.lineTo(pts[pts.length-1].sx, zy);
        c.closePath(); c.fill();
      }
    }

    /* active curve: glow + main */
    c.strokeStyle = _tcRgba(col, 0.12); c.lineWidth = 10; c.lineCap='round'; c.lineJoin='round';
    c.beginPath(); let on2 = false;
    for (let x = xMin; x <= xMax; x += step) {
      const yv = evFn(x);
      if (!isFinite(yv)||Math.abs(yv)>yR*2) { on2 = false; continue; }
      const spx = sx(x), spy = sy(yv);
      if (!on2) { c.moveTo(spx, spy); on2 = true; } else c.lineTo(spx, spy);
    }
    c.stroke();
    c.strokeStyle = col; c.lineWidth = 2.5;
    c.beginPath(); on2 = false;
    for (let x = xMin; x <= xMax; x += step) {
      const yv = evFn(x);
      if (!isFinite(yv)||Math.abs(yv)>yR*2) { on2 = false; continue; }
      const spx = sx(x), spy = sy(yv);
      if (!on2) { c.moveTo(spx, spy); on2 = true; } else c.lineTo(spx, spy);
    }
    c.stroke();

    /* ── current angle marker ── */
    const curY = evFn(this.angle);
    if (isFinite(curY) && Math.abs(curY) <= yR*2) {
      const mx = sx(this.angle), my = sy(curY);
      c.strokeStyle = _tcRgba(_TC.violet, 0.18); c.lineWidth = 1;
      c.setLineDash([4, 4]);
      c.beginPath(); c.moveTo(mx, gy); c.lineTo(mx, gy+gh); c.stroke();
      c.strokeStyle = _tcRgba(col, 0.18);
      c.beginPath(); c.moveTo(gx, my); c.lineTo(mx, my); c.stroke();
      c.setLineDash([]);
      /* dot */
      c.fillStyle = _tcRgba(_TC.point, 0.12);
      c.beginPath(); c.arc(mx, my, 13, 0, Math.PI*2); c.fill();
      c.fillStyle = _TC.point; c.shadowColor = _TC.point; c.shadowBlur = 12;
      c.beginPath(); c.arc(mx, my, 5.5, 0, Math.PI*2); c.fill();
      c.shadowBlur = 0;
      c.fillStyle = 'rgba(255,255,255,0.7)';
      c.beginPath(); c.arc(mx, my, 2, 0, Math.PI*2); c.fill();
      /* value badge */
      const txt = this._fmt(curY);
      c.font = 'bold 11px Manrope,sans-serif';
      const tm = c.measureText(txt);
      const bx2 = mx+10, by2 = my-22, bw2 = tm.width+14, bh2 = 20;
      c.fillStyle='rgba(12,12,22,0.85)';
      c.beginPath(); c.roundRect(bx2, by2-bh2/2, bw2, bh2, 6); c.fill();
      c.strokeStyle = _tcRgba(col, 0.4); c.lineWidth = 1;
      c.beginPath(); c.roundRect(bx2, by2-bh2/2, bw2, bh2, 6); c.stroke();
      c.fillStyle = col; c.textAlign='left'; c.textBaseline='middle';
      c.fillText(txt, bx2+7, by2);
    }

    c.restore();

    /* fn name badge */
    c.font='bold 13px Manrope,sans-serif';
    const tm2 = c.measureText(lbl);
    const bw3 = tm2.width+18, bh3 = 26;
    c.fillStyle='rgba(12,12,22,0.7)';
    c.beginPath(); c.roundRect(gx+8, gy+8, bw3, bh3, 8); c.fill();
    c.strokeStyle = _tcRgba(col, 0.25); c.lineWidth = 1;
    c.beginPath(); c.roundRect(gx+8, gy+8, bw3, bh3, 8); c.stroke();
    c.fillStyle = col; c.textAlign='left'; c.textBaseline='middle';
    c.fillText(lbl, gx+17, gy+21);
  }

  /* ═══ Snap particles ═══════════════════════════════════════════════ */

  _spawnSnap(px, py) {
    for (let i = 0; i < 8; i++) {
      const ang = Math.random() * Math.PI * 2;
      const speed = 30 + Math.random() * 50;
      this._particles.push({
        x: px, y: py,
        vx: Math.cos(ang) * speed,
        vy: Math.sin(ang) * speed,
        life: 1,
        col: _TC.violet,
      });
    }
  }

  _drawParticles(c) {
    const dt = 0.016;
    for (let i = this._particles.length - 1; i >= 0; i--) {
      const p = this._particles[i];
      p.x += p.vx * dt; p.y += p.vy * dt;
      p.life -= dt * 1.8;
      if (p.life <= 0) { this._particles.splice(i, 1); continue; }
      c.fillStyle = _tcRgba(p.col, p.life * 0.6);
      c.shadowColor = p.col; c.shadowBlur = 6;
      c.beginPath(); c.arc(p.x, p.y, 2 * p.life, 0, Math.PI*2); c.fill();
      c.shadowBlur = 0;
    }
  }

  /* ═══ Drawing helpers ══════════════════════════════════════════════ */

  _glowLine(c, x1, y1, x2, y2, col, w) {
    c.lineCap = 'round';
    c.strokeStyle = _tcRgba(col, 0.14); c.lineWidth = w + 8;
    c.beginPath(); c.moveTo(x1,y1); c.lineTo(x2,y2); c.stroke();
    c.strokeStyle = col; c.lineWidth = w;
    c.beginPath(); c.moveTo(x1,y1); c.lineTo(x2,y2); c.stroke();
    c.strokeStyle = _tcRgba(col, 0.45); c.lineWidth = 1;
    c.beginPath(); c.moveTo(x1,y1); c.lineTo(x2,y2); c.stroke();
  }

  _arrowH(c, x, y, angle, col) {
    c.save(); c.translate(x, y); c.rotate(angle);
    c.fillStyle = col;
    c.beginPath(); c.moveTo(0,0); c.lineTo(-9,-4.5); c.lineTo(-9,4.5); c.closePath(); c.fill();
    c.restore();
  }

  _badge(c, x, y, txt, col, tA, tB) {
    c.font='600 10px Manrope,sans-serif';
    const m = c.measureText(txt);
    const pw = m.width+10, ph = 17;
    let bx = x, by = y;
    if (tA==='right') bx = x - pw;
    else if (tA==='center') bx = x - pw/2;
    if (tB==='middle') by = y - ph/2;
    c.fillStyle='rgba(12,12,22,0.75)';
    c.beginPath(); c.roundRect(bx, by, pw, ph, 4); c.fill();
    c.strokeStyle = _tcRgba(col, 0.35); c.lineWidth = 1;
    c.beginPath(); c.roundRect(bx, by, pw, ph, 4); c.stroke();
    c.fillStyle = col; c.textAlign='center'; c.textBaseline='middle';
    c.fillText(txt, bx + pw/2, by + ph/2);
  }

  _tooltip(c, px, py, cosA, sinA) {
    const txt = `(${this._fmt(cosA)};  ${this._fmt(sinA)})`;
    c.font='600 11px Manrope,sans-serif';
    const m = c.measureText(txt);
    const pw = m.width+16, ph = 24;
    const offX = cosA >= 0 ? 16 : -pw-16;
    const offY = sinA >= 0 ? -ph-12 : 12;
    const bx = px+offX, by = py+offY;
    c.fillStyle='rgba(12,12,22,0.80)';
    c.beginPath(); c.roundRect(bx, by, pw, ph, 8); c.fill();
    c.strokeStyle = _tcRgba(_TC.violet, 0.3); c.lineWidth = 1;
    c.beginPath(); c.roundRect(bx, by, pw, ph, 8); c.stroke();
    c.fillStyle='rgba(255,255,255,0.82)';
    c.textAlign='center'; c.textBaseline='middle';
    c.fillText(txt, bx+pw/2, by+ph/2);
  }

  /* ═══ Formatting ═══════════════════════════════════════════════════ */

  _fmt(v) {
    const a = Math.abs(v), s = v < 0 ? '−' : '';
    if (a < 5e-4) return '0';
    if (Math.abs(a-0.5)<1e-3)             return s+'½';
    if (Math.abs(a-1)<1e-3)               return s+'1';
    if (Math.abs(a-Math.SQRT2/2)<1e-3)    return s+'√2/2';
    if (Math.abs(a-Math.sqrt(3)/2)<1e-3)  return s+'√3/2';
    if (Math.abs(a-Math.sqrt(3)/3)<1e-3)  return s+'√3/3';
    if (Math.abs(a-Math.sqrt(3))<1e-3)    return s+'√3';
    if (Math.abs(a-2)<1e-3)               return s+'2';
    if (Math.abs(a-2*Math.sqrt(3)/3)<1e-3)return s+'2√3/3';
    return v.toFixed(3);
  }

  _radLbl(a) {
    for (const n of _TC_NOTABLE) { if (Math.abs(a-n.a)<0.02) return n.l; }
    return (a*180/Math.PI).toFixed(1)+'°';
  }

  _norm(a) { return ((a%(2*Math.PI))+2*Math.PI)%(2*Math.PI); }
  _fireUpdate() { if (this.onUpdate) this.onUpdate(this.stats()); }

  /* ═══ Events ═══════════════════════════════════════════════════════ */

  _bindEvents() {
    const cv = this.canvas;

    const mp = e => {
      const r = cv.getBoundingClientRect();
      return { x:(e.clientX??e.touches?.[0]?.clientX??0)-r.left, y:(e.clientY??e.touches?.[0]?.clientY??0)-r.top };
    };

    const snapAngle = e => {
      const m = mp(e);
      let a = Math.atan2(-(m.y-this._cy), m.x-this._cx);
      if (a<0) a += 2*Math.PI;
      if (this.snapToNotable) {
        for (const n of _TC_NOTABLE) {
          if (Math.abs(a-n.a)<0.09) { a = n.a; break; }
        }
      }
      return a;
    };

    const hit = e => {
      const m = mp(e);
      const px = this._cx + this._r*Math.cos(this.angle);
      const py = this._cy - this._r*Math.sin(this.angle);
      if (Math.hypot(m.x-px, m.y-py) < 30) return true;
      return Math.abs(Math.hypot(m.x-this._cx, m.y-this._cy) - this._r) < 22;
    };

    const checkSnap = (newA) => {
      for (const n of _TC_NOTABLE) {
        if (Math.abs(newA-n.a)<0.02 && this._lastSnap !== n.a) {
          this._lastSnap = n.a;
          const nx = this._cx + this._r*Math.cos(n.a);
          const ny = this._cy - this._r*Math.sin(n.a);
          this._spawnSnap(nx, ny);
          return;
        }
      }
    };

    const end = () => {
      if (this._drag) { this._drag = false; this.draw(); }
      cv.style.cursor = 'default';
    };

    cv.addEventListener('mousedown', e => {
      if (!hit(e)) return;
      this._drag = true; this._stopAnim();
      const na = snapAngle(e); checkSnap(na);
      this.angle = na; this.draw();
      cv.style.cursor = 'grabbing';
    });

    this._lastDragSoundTs = 0;
    cv.addEventListener('mousemove', e => {
      if (this._drag) {
        const na = snapAngle(e); checkSnap(na);
        this.angle = na; this.draw();
        const now = performance.now();
        if (window.LabFX && now - this._lastDragSoundTs > 100) {
          this._lastDragSoundTs = now;
          const pitch = 0.8 + (this.angle / (2 * Math.PI)) * 0.8;
          LabFX.sound.play('tick', { pitch, volume: 0.05 });
          LabFX.haptic(5);
        }
      } else {
        const h = hit(e);
        if (h !== this._hover) { this._hover = h; this.draw(); }
        cv.style.cursor = h ? 'grab' : 'default';
      }
    });
    cv.addEventListener('mouseup', end);
    cv.addEventListener('mouseleave', () => { if (this._hover){this._hover=false;this.draw();} end(); });

    /* scroll wheel fine-tune */
    cv.addEventListener('wheel', e => {
      e.preventDefault();
      const step = e.shiftKey ? 0.01 : (Math.PI / 180);
      this.angle = this._norm(this.angle - Math.sign(e.deltaY) * step);
      this._lastSnap = -1;
      this.draw();
    }, { passive: false });

    /* keyboard arrows */
    cv.setAttribute('tabindex', '0');
    cv.style.outline = 'none';
    cv.addEventListener('keydown', e => {
      const step = e.shiftKey ? (Math.PI/180) : (Math.PI/36); /* 1° or 5° */
      if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
        e.preventDefault(); this.angle = this._norm(this.angle + step); this.draw();
      } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
        e.preventDefault(); this.angle = this._norm(this.angle - step); this.draw();
      }
    });

    /* touch */
    cv.addEventListener('touchstart', e => {
      e.preventDefault();
      if (!hit(e)) return;
      this._drag = true; this._stopAnim();
      const na = snapAngle(e); checkSnap(na); this.angle = na; this.draw();
    }, { passive: false });
    cv.addEventListener('touchmove', e => {
      e.preventDefault();
      if (this._drag) { const na = snapAngle(e); checkSnap(na); this.angle = na; this.draw(); }
    }, { passive: false });
    cv.addEventListener('touchend', end);
  }

  /* ═══ Animation ════════════════════════════════════════════════════ */

  _startAnim() {
    this.animating = true;
    let last = performance.now();
    const loop = now => {
      if (!this.animating) return;
      const dt = (now-last)/1000; last = now;
      let d = this._animTarget - this.angle;
      if (d > Math.PI) d -= 2*Math.PI;
      if (d < -Math.PI) d += 2*Math.PI;
      if (Math.abs(d) < 0.012) {
        this.angle = this._animTarget;
        this.animating = false;
        /* snap particle at end */
        const nx = this._cx + this._r*Math.cos(this.angle);
        const ny = this._cy - this._r*Math.sin(this.angle);
        this._spawnSnap(nx, ny);
        this.draw(); return;
      }
      const speed = this._animSpeed * Math.max(0.3, Math.min(1, Math.abs(d)/0.5));
      this.angle += Math.sign(d) * Math.min(Math.abs(d), speed * dt);
      this.angle = this._norm(this.angle);
      this.draw();
      this._raf = requestAnimationFrame(loop);
    };
    this._raf = requestAnimationFrame(loop);
  }

  _stopAnim() {
    this.animating = false;
    if (this._raf) { cancelAnimationFrame(this._raf); this._raf = null; }
  }

  _startIdle() {
    if (this._idleRaf) return;
    let last = performance.now();
    const loop = now => {
      const dt = (now-last)/1000; last = now;
      this._idlePulse += dt * 1.5;
      if (window.LabFX) LabFX.particles.update(dt);
      /* update particles */
      if (this._particles.length > 0 || (!this._drag && !this.animating)) this.draw();
      this._idleRaf = requestAnimationFrame(loop);
    };
    this._idleRaf = requestAnimationFrame(loop);
  }

  _stopIdle() {
    if (this._idleRaf) { cancelAnimationFrame(this._idleRaf); this._idleRaf = null; }
  }
}

if (typeof window !== 'undefined') window.TrigCircleSim = TrigCircleSim;

/* ─── lab UI init ─────────────────────────────────── */
  var trigSim = null;

  function _openTrigCircle() {
    document.getElementById('sim-topbar-title').textContent = 'Тригонометрическая окружность';
    _simShow('sim-trigcircle');
    _simShow('ctrl-trigcircle');

    requestAnimationFrame(() => requestAnimationFrame(() => {
      if (!trigSim) {
        trigSim = new TrigCircleSim(document.getElementById('trigcircle-canvas'));
        trigSim.onUpdate = _trigUpdateUI;
      }
      trigSim.fit();
      trigSim.start();
      _trigUpdateUI(trigSim.stats());
    }));
  }

  function trigToggle(layer, rowEl) {
    if (!trigSim) return;
    const isActive = rowEl.classList.toggle('active');
    trigSim.toggleLayer(layer, isActive);
  }

  function trigSetGraphFn(fn, el) {
    if (!trigSim) return;
    document.querySelectorAll('.trig-fn-btn').forEach(b => b.classList.remove('active'));
    el.classList.add('active');
    trigSim.setGraphFn(fn);
  }

  function trigGoTo(rad) {
    if (!trigSim) return;
    trigSim.goToAngle(rad);
  }

  function trigReset() {
    if (!trigSim) return;
    trigSim.setAngle(Math.PI / 4);
    if (window.LabFX) LabFX.sound.play('click');
  }

  function _trigUpdateUI(s) {
    const _f = v => {
      if (v === undefined) return '—';
      const a = Math.abs(v), sg = v < 0 ? '−' : '';
      if (a < 5e-4) return '0';
      if (Math.abs(a - 0.5) < 1e-3)            return sg + '½';
      if (Math.abs(a - 1) < 1e-3)              return sg + '1';
      if (Math.abs(a - Math.SQRT2/2) < 1e-3)   return sg + '√2/2';
      if (Math.abs(a - Math.sqrt(3)/2) < 1e-3) return sg + '√3/2';
      if (Math.abs(a - Math.sqrt(3)/3) < 1e-3) return sg + '√3/3';
      if (Math.abs(a - Math.sqrt(3)) < 1e-3)   return sg + '√3';
      return v.toFixed(4);
    };
    const degStr = s.deg.toFixed(1) + '°';

    // Panel values (nice fractions)
    document.getElementById('trig-v-sin').textContent = _f(s.sin);
    document.getElementById('trig-v-cos').textContent = _f(s.cos);
    document.getElementById('trig-v-tan').textContent = _f(s.tan);
    document.getElementById('trig-v-cot').textContent = _f(s.cot);

    // Angle badge
    document.getElementById('trig-angle-badge').innerHTML =
      `${degStr} = ${s.radLabel}<br><span style="font-size:0.72rem;opacity:0.6">${s.angle.toFixed(4)} рад</span>`;

    // Stats bar (nice fractions)
    document.getElementById('trigbar-angle').textContent = degStr;
    document.getElementById('trigbar-sin').textContent = _f(s.sin);
    document.getElementById('trigbar-cos').textContent = _f(s.cos);
    document.getElementById('trigbar-tan').textContent = _f(s.tan);
    document.getElementById('trigbar-cot').textContent = _f(s.cot);
    document.getElementById('trigbar-quad').textContent = ['I', 'II', 'III', 'IV'][s.quadrant - 1];
  }

  /* ── KaTeX live preview ── */

  /** Convert user ascii expression <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> LaTeX string for KaTeX preview */
