/* alg10_svg.js — библиотека SVG-хелперов для Алгебры 10
 *
 * Главные модули:
 *   ALG10.tri   — тригонометрическая (единичная) окружность
 *   ALG10.func  — графики функций (sin, cos, tg, ctg, многочлены, корни)
 *   ALG10.nthRoot — графики y = ⁿ√x
 *
 * Без зависимостей. Все функции возвращают строку SVG.
 *
 * Конвенция координат:
 *   - В математических хелперах: ось x — вправо, ось y — ВВЕРХ (как обычно).
 *   - Внутри SVG: ось y инвертируется (через `pxY = cy - y*scale`).
 *
 * Подключение:
 *   <script src="/js/alg10_svg.js?v=1" defer></script>
 */
(function(){
'use strict';

if (window.ALG10 && window.ALG10.__installed) return;
const A = window.ALG10 = window.ALG10 || {};
A.__installed = true;
A.version = '1.0.0';

/* ============================================================
   УТИЛИТЫ
   ============================================================ */
A.util = {
  /* Округление с заданной точностью (для подписей) */
  round: (v, n) => Math.round(v * Math.pow(10, n||3)) / Math.pow(10, n||3),

  /* Форматирование числа: 0.866 → '0.87', 1.0 → '1' */
  fmt: (v, n) => {
    n = n || 2;
    if (Math.abs(v) < 1e-9) return '0';
    const s = A.util.round(v, n).toString();
    return s;
  },

  /* Форматирование угла в виде π-дроби или градусов */
  fmtAngleRad: (rad, mode) => {
    if (mode === 'deg') return Math.round(rad * 180 / Math.PI) + '°';
    /* Попытка распознать π/n */
    const r = rad / Math.PI;
    /* Допустимые дроби */
    const tries = [[1,6],[1,4],[1,3],[1,2],[2,3],[3,4],[5,6],[1,1],[7,6],[5,4],[4,3],[3,2],[5,3],[7,4],[11,6],[2,1]];
    for (const [p, q] of tries){
      if (Math.abs(r - p/q) < 0.01) {
        if (p === 1 && q === 1) return 'π';
        if (q === 1) return p + 'π';
        if (p === 1) return 'π/' + q;
        return p + 'π/' + q;
      }
      if (Math.abs(r + p/q) < 0.01) {
        if (p === 1 && q === 1) return '-π';
        if (q === 1) return '-' + p + 'π';
        if (p === 1) return '-π/' + q;
        return '-' + p + 'π/' + q;
      }
    }
    return A.util.round(rad, 2);
  },

  /* SVG-обёртка с responsive width:100% */
  svgWrap: (W, H, content, opts) => {
    opts = opts || {};
    const bg = opts.bg || '#fff';
    const border = opts.border !== false ? '1px solid #e2e8f0' : 'none';
    const margin = opts.margin || '0 auto';
    return '<svg viewBox="0 0 '+W+' '+H+'" preserveAspectRatio="xMidYMid meet"'
      + ' style="width:100%;height:auto;display:block;margin:'+margin+';'
      + 'background:'+bg+';border-radius:10px;border:'+border+'">'
      + content
      + '</svg>';
  }
};

/* ============================================================
   МОДУЛЬ TRI — тригонометрическая (единичная) окружность
   ============================================================ */
A.tri = {};

/* Создать canvas для тригонометрической окружности.
 * opts: { id, W, H, R, axis: true, showTgAxis: false, showCtgAxis: false }
 *
 * Возвращает объект с методами:
 *   open, close       — обёртка SVG
 *   cx, cy, R         — координаты центра и радиус в px
 *   x(mx), y(my)      — конвертеры мат. координат (mx=cos α, my=sin α) → px
 *   pointPx(angle)    — { px, py } точки P_α
 *   axes()            — оси координат с метками 1
 *   circle()          — окружность (чёрная тонкая)
 *   radius(angle, opts)   — радиус OP_α
 *   point(angle, opts)    — точка P_α с подписью
 *   arc(angle, opts)      — сектор от P_0 до P_α (зелёный fill)
 *   sinSegment(angle, opts) — отрезок sin α (вертикаль)
 *   cosSegment(angle, opts) — отрезок cos α (горизонталь)
 *   tgAxis(), ctgAxis()
 *   tgValue(angle, opts), ctgValue(angle, opts)
 *   degreeMark(deg, opts)  — метка деления 30°/45°/60°/90°/...
 *   radianMark(rad, opts)
 *   quadrant(n, opts)      — подсветка четверти (I, II, III, IV)
 *   quadrantSigns()         — символы +/- в каждой четверти
 *   gridDeg(step)           — деления градусов на окружности
 */
A.tri.canvas = function(opts){
  opts = opts || {};
  const W = opts.W || 320;
  const H = opts.H || 320;
  const margin = opts.margin || 32;
  const R = opts.R || Math.min(W, H)/2 - margin;
  const cx = W/2;
  const cy = H/2;
  const id = opts.id || ('tri-' + Math.floor(Math.random()*100000));

  /* Сетка-фон (опционально) */
  let gridSvg = '';
  if (opts.gridStep) {
    const step = opts.gridStep;
    const lines = [];
    for (let x = step; x < W; x += step) lines.push('<line x1="'+x+'" y1="0" x2="'+x+'" y2="'+H+'" stroke="#f1f5f9" stroke-width="1"/>');
    for (let y = step; y < H; y += step) lines.push('<line x1="0" y1="'+y+'" x2="'+W+'" y2="'+y+'" stroke="#f1f5f9" stroke-width="1"/>');
    gridSvg = lines.join('');
  }

  const C = {
    W, H, cx, cy, R, id,
    open: '<svg viewBox="0 0 '+W+' '+H+'" preserveAspectRatio="xMidYMid meet" style="width:100%;height:auto;display:block;margin:0 auto;background:'+(opts.bg||'#fafafa')+';border-radius:10px;border:1px solid #e2e8f0">' + gridSvg,
    close: '</svg>',

    /* Конвертеры мат. координат → px (R = 1 в мат. ед.) */
    x: function(mx){ return cx + mx * R; },
    y: function(my){ return cy - my * R; }, /* SVG y инвертирован */

    /* Точка P_α в пикселях */
    pointPx: function(angle){
      return { px: cx + R * Math.cos(angle), py: cy - R * Math.sin(angle) };
    }
  };

  /* === Оси координат === */
  C.axes = function(opts){
    opts = opts || {};
    const color = opts.color || '#475569';
    const xExt = opts.xExt || R + 18;
    const yExt = opts.yExt || R + 18;
    let s = '';
    /* Ось X */
    s += '<line x1="'+(cx - xExt)+'" y1="'+cy+'" x2="'+(cx + xExt)+'" y2="'+cy+'" stroke="'+color+'" stroke-width="1.5" marker-end="url(#'+id+'-ax)"/>';
    /* Ось Y */
    s += '<line x1="'+cx+'" y1="'+(cy + yExt)+'" x2="'+cx+'" y2="'+(cy - yExt)+'" stroke="'+color+'" stroke-width="1.5" marker-end="url(#'+id+'-ay)"/>';
    /* Стрелки */
    s += '<defs>'
      + '<marker id="'+id+'-ax" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="'+color+'"/></marker>'
      + '<marker id="'+id+'-ay" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="'+color+'"/></marker>'
      + '</defs>';
    /* Подписи x, y */
    s += '<text x="'+(cx + xExt + 4)+'" y="'+(cy + 4)+'" font-size="13" font-style="italic" font-family="Inter,sans-serif" fill="'+color+'">x</text>';
    s += '<text x="'+(cx + 6)+'" y="'+(cy - yExt - 4)+'" font-size="13" font-style="italic" font-family="Inter,sans-serif" fill="'+color+'">y</text>';
    /* Метка O */
    s += '<text x="'+(cx - 12)+'" y="'+(cy + 14)+'" font-size="12" font-style="italic" font-family="Inter,sans-serif" fill="'+color+'">O</text>';
    /* Деления 1 */
    s += '<line x1="'+(cx + R)+'" y1="'+(cy - 4)+'" x2="'+(cx + R)+'" y2="'+(cy + 4)+'" stroke="'+color+'" stroke-width="1.5"/>';
    s += '<text x="'+(cx + R - 4)+'" y="'+(cy + 16)+'" font-size="11" font-family="Inter,sans-serif" fill="'+color+'">1</text>';
    s += '<line x1="'+(cx - 4)+'" y1="'+(cy - R)+'" x2="'+(cx + 4)+'" y2="'+(cy - R)+'" stroke="'+color+'" stroke-width="1.5"/>';
    s += '<text x="'+(cx + 8)+'" y="'+(cy - R + 4)+'" font-size="11" font-family="Inter,sans-serif" fill="'+color+'">1</text>';
    return s;
  };

  /* === Окружность === */
  C.circle = function(opts){
    opts = opts || {};
    const color = opts.color || '#1e293b';
    const w = opts.width || 2;
    return '<circle cx="'+cx+'" cy="'+cy+'" r="'+R+'" fill="none" stroke="'+color+'" stroke-width="'+w+'"/>';
  };

  /* === Радиус OP_α === */
  C.radius = function(angle, opts){
    opts = opts || {};
    const color = opts.color || '#dc2626';
    const w = opts.width || 2.2;
    const p = C.pointPx(angle);
    return '<line x1="'+cx+'" y1="'+cy+'" x2="'+p.px+'" y2="'+p.py+'" stroke="'+color+'" stroke-width="'+w+'"/>';
  };

  /* === Точка P_α === */
  C.point = function(angle, opts){
    opts = opts || {};
    const p = C.pointPx(angle);
    const r = opts.r || 4;
    const color = opts.color || '#dc2626';
    const label = opts.label;
    let s = '<circle cx="'+p.px+'" cy="'+p.py+'" r="'+r+'" fill="'+color+'" stroke="#fff" stroke-width="1.5"/>';
    if (label !== undefined) {
      const lOff = opts.labelOffset || 14;
      const lx = p.px + lOff * Math.cos(angle);
      const ly = p.py - lOff * Math.sin(angle);
      const fs = opts.fontSize || 13;
      const lColor = opts.labelColor || color;
      s += '<text x="'+lx+'" y="'+(ly + 4)+'" text-anchor="middle" font-size="'+fs+'" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="'+lColor+'">'+label+'</text>';
    }
    return s;
  };

  /* === Дуга сектора от P_0 до P_α === */
  C.arc = function(angle, opts){
    opts = opts || {};
    const r = opts.r || R * 0.25;
    const color = opts.color || '#10b981';
    const fill = opts.fill || 'rgba(16,185,129,.20)';
    /* SVG-арка: углы в SVG-конвенции (y вниз).
       Наш angle — в мат. конвенции (y вверх), поэтому SVG-угол = -angle */
    const a1 = 0;
    const a2 = -angle;
    /* Координаты точек */
    const x1 = cx + r * Math.cos(a1);
    const y1 = cy + r * Math.sin(a1);
    const x2 = cx + r * Math.cos(a2);
    const y2 = cy + r * Math.sin(a2);
    let delta = a2 - a1;
    /* Нормализация */
    while (delta > Math.PI) delta -= 2 * Math.PI;
    while (delta < -Math.PI) delta += 2 * Math.PI;
    const large = Math.abs(angle) > Math.PI ? 1 : 0;
    const sweep = angle > 0 ? 0 : 1; /* CCW в SVG y-inv = sweep=0 для +angle */
    /* Заполненный сектор */
    let s = '<path d="M '+cx+' '+cy+' L '+x1+' '+y1+' A '+r+' '+r+' 0 '+large+' '+sweep+' '+x2+' '+y2+' Z" fill="'+fill+'" stroke="'+color+'" stroke-width="1.5"/>';
    return s;
  };

  /* === Отрезок sin α (вертикаль от точки до оси x) === */
  C.sinSegment = function(angle, opts){
    opts = opts || {};
    const color = opts.color || '#dc2626';
    const w = opts.width || 2;
    const p = C.pointPx(angle);
    let s = '<line x1="'+p.px+'" y1="'+p.py+'" x2="'+p.px+'" y2="'+cy+'" stroke="'+color+'" stroke-width="'+w+'" stroke-dasharray="'+(opts.dash||'4 3')+'"/>';
    if (opts.label !== false){
      const labelY = (p.py + cy) / 2;
      const labelX = p.px + (p.px > cx ? 6 : -6);
      const anchor = p.px > cx ? 'start' : 'end';
      s += '<text x="'+labelX+'" y="'+(labelY + 4)+'" text-anchor="'+anchor+'" font-size="11" font-family="JetBrains Mono,monospace" fill="'+color+'" font-weight="600">'+(opts.label || 'sin α')+'</text>';
    }
    return s;
  };

  /* === Отрезок cos α (горизонталь от точки до оси y) === */
  C.cosSegment = function(angle, opts){
    opts = opts || {};
    const color = opts.color || '#2563eb';
    const w = opts.width || 2;
    const p = C.pointPx(angle);
    let s = '<line x1="'+p.px+'" y1="'+cy+'" x2="'+p.px+'" y2="'+cy+'" stroke="'+color+'" stroke-width="'+w+'" stroke-dasharray="'+(opts.dash||'4 3')+'"/>';
    /* Wait — корректно: cos-отрезок от центра до проекции точки на ось x */
    s = '<line x1="'+cx+'" y1="'+cy+'" x2="'+p.px+'" y2="'+cy+'" stroke="'+color+'" stroke-width="'+w+'" stroke-dasharray="'+(opts.dash||'4 3')+'"/>';
    if (opts.label !== false){
      const labelX = (cx + p.px) / 2;
      const labelY = cy + (p.py < cy ? 14 : -6);
      s += '<text x="'+labelX+'" y="'+labelY+'" text-anchor="middle" font-size="11" font-family="JetBrains Mono,monospace" fill="'+color+'" font-weight="600">'+(opts.label || 'cos α')+'</text>';
    }
    return s;
  };

  /* === Ось тангенсов (вертикальная касательная x=1) === */
  C.tgAxis = function(opts){
    opts = opts || {};
    const color = opts.color || '#16a34a';
    const xAx = cx + R;
    const ext = opts.ext || R * 0.85;
    let s = '<line x1="'+xAx+'" y1="'+(cy - ext)+'" x2="'+xAx+'" y2="'+(cy + ext)+'" stroke="'+color+'" stroke-width="2" stroke-dasharray="5 3"/>';
    s += '<text x="'+(xAx + 6)+'" y="'+(cy - ext + 12)+'" font-size="11" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="'+color+'">ось tg</text>';
    return s;
  };

  /* === Ось котангенсов (горизонтальная касательная y=1) === */
  C.ctgAxis = function(opts){
    opts = opts || {};
    const color = opts.color || '#7c3aed';
    const yAx = cy - R;
    const ext = opts.ext || R * 0.85;
    let s = '<line x1="'+(cx - ext)+'" y1="'+yAx+'" x2="'+(cx + ext)+'" y2="'+yAx+'" stroke="'+color+'" stroke-width="2" stroke-dasharray="5 3"/>';
    s += '<text x="'+(cx + ext - 24)+'" y="'+(yAx - 6)+'" font-size="11" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="'+color+'">ось ctg</text>';
    return s;
  };

  /* === Значение tg α на оси тангенсов ===
   * Продлевает OP_α до пересечения с x=1, отмечает точку A_α */
  C.tgValue = function(angle, opts){
    opts = opts || {};
    const color = opts.color || '#16a34a';
    const t = Math.tan(angle);
    if (!isFinite(t)) return ''; /* нет тангенса */
    const xAx = cx + R;
    const yA = cy - t * R;
    /* Линия от центра через P_α до A_α */
    let s = '<line x1="'+cx+'" y1="'+cy+'" x2="'+xAx+'" y2="'+yA+'" stroke="'+color+'" stroke-width="1.5" stroke-dasharray="3 2"/>';
    /* Точка A_α */
    s += '<circle cx="'+xAx+'" cy="'+yA+'" r="3.5" fill="'+color+'" stroke="#fff" stroke-width="1.5"/>';
    if (opts.label !== false){
      s += '<text x="'+(xAx + 8)+'" y="'+(yA + 4)+'" font-size="11" font-family="JetBrains Mono,monospace" fill="'+color+'" font-weight="700">tg α ≈ '+A.util.fmt(t, 2)+'</text>';
    }
    return s;
  };

  /* === Значение ctg α на оси котангенсов === */
  C.ctgValue = function(angle, opts){
    opts = opts || {};
    const color = opts.color || '#7c3aed';
    const c = 1 / Math.tan(angle);
    if (!isFinite(c)) return '';
    const yAx = cy - R;
    const xA = cx + c * R;
    let s = '<line x1="'+cx+'" y1="'+cy+'" x2="'+xA+'" y2="'+yAx+'" stroke="'+color+'" stroke-width="1.5" stroke-dasharray="3 2"/>';
    s += '<circle cx="'+xA+'" cy="'+yAx+'" r="3.5" fill="'+color+'" stroke="#fff" stroke-width="1.5"/>';
    if (opts.label !== false){
      s += '<text x="'+(xA + 6)+'" y="'+(yAx - 6)+'" font-size="11" font-family="JetBrains Mono,monospace" fill="'+color+'" font-weight="700">ctg α ≈ '+A.util.fmt(c, 2)+'</text>';
    }
    return s;
  };

  /* === Подсветка четверти === */
  C.quadrant = function(n, opts){
    opts = opts || {};
    const color = opts.color || '#10b981';
    const fill = opts.fill || 'rgba(16,185,129,.10)';
    /* Углы для секторов: I — 0..π/2, II — π/2..π, III — π..3π/2 (-π..-π/2), IV — 3π/2..2π (-π/2..0) */
    const ranges = {1:[0, Math.PI/2], 2:[Math.PI/2, Math.PI], 3:[-Math.PI, -Math.PI/2], 4:[-Math.PI/2, 0]};
    const [a1, a2] = ranges[n];
    const r = R;
    const x1 = cx + r * Math.cos(a1);
    const y1 = cy - r * Math.sin(a1);
    const x2 = cx + r * Math.cos(a2);
    const y2 = cy - r * Math.sin(a2);
    /* Сектор */
    return '<path d="M '+cx+' '+cy+' L '+x1+' '+y1+' A '+r+' '+r+' 0 0 0 '+x2+' '+y2+' Z" fill="'+fill+'" stroke="none"/>';
  };

  /* === Метки четвертей === */
  C.quadrantLabels = function(opts){
    opts = opts || {};
    const color = opts.color || '#64748b';
    const off = R * 0.55;
    let s = '';
    s += '<text x="'+(cx + off)+'" y="'+(cy - off)+'" text-anchor="middle" font-size="14" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="'+color+'">I</text>';
    s += '<text x="'+(cx - off)+'" y="'+(cy - off)+'" text-anchor="middle" font-size="14" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="'+color+'">II</text>';
    s += '<text x="'+(cx - off)+'" y="'+(cy + off + 4)+'" text-anchor="middle" font-size="14" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="'+color+'">III</text>';
    s += '<text x="'+(cx + off)+'" y="'+(cy + off + 4)+'" text-anchor="middle" font-size="14" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="'+color+'">IV</text>';
    return s;
  };

  /* === Метка деления градуса (рисочка снаружи окружности + подпись) === */
  C.degreeMark = function(deg, opts){
    opts = opts || {};
    const angle = deg * Math.PI / 180;
    const color = opts.color || '#64748b';
    const tickLen = opts.tickLen || 6;
    const lOff = opts.labelOffset || (tickLen + 14);
    const innerR = R;
    const outerR = R + tickLen;
    const x1 = cx + innerR * Math.cos(angle);
    const y1 = cy - innerR * Math.sin(angle);
    const x2 = cx + outerR * Math.cos(angle);
    const y2 = cy - outerR * Math.sin(angle);
    let s = '<line x1="'+x1+'" y1="'+y1+'" x2="'+x2+'" y2="'+y2+'" stroke="'+color+'" stroke-width="1.5"/>';
    if (opts.label !== false){
      const lx = cx + (R + lOff) * Math.cos(angle);
      const ly = cy - (R + lOff) * Math.sin(angle);
      const lab = opts.label || (deg + '°');
      s += '<text x="'+lx+'" y="'+(ly + 4)+'" text-anchor="middle" font-size="10" font-family="JetBrains Mono,monospace" fill="'+color+'" font-weight="600">'+lab+'</text>';
    }
    return s;
  };

  /* === Метка радиана (π/n) === */
  C.radianMark = function(rad, opts){
    opts = opts || {};
    return C.degreeMark(rad * 180 / Math.PI, Object.assign({}, opts, {label: opts.label || A.util.fmtAngleRad(rad)}));
  };

  /* === Сетка делений по 30° === */
  C.gridDeg = function(step, opts){
    opts = opts || {};
    step = step || 30;
    const color = opts.color || '#cbd5e1';
    let s = '';
    for (let d = 0; d < 360; d += step){
      const a = d * Math.PI / 180;
      const x1 = cx + (R - 3) * Math.cos(a);
      const y1 = cy - (R - 3) * Math.sin(a);
      const x2 = cx + (R + 3) * Math.cos(a);
      const y2 = cy - (R + 3) * Math.sin(a);
      s += '<line x1="'+x1+'" y1="'+y1+'" x2="'+x2+'" y2="'+y2+'" stroke="'+color+'" stroke-width="1"/>';
    }
    return s;
  };

  /* === Дуга-сектор для угла (со стрелкой направления вращения) === */
  C.rotationArrow = function(angle, opts){
    opts = opts || {};
    const color = opts.color || (angle > 0 ? '#10b981' : '#dc2626');
    const r = opts.r || R * 0.18;
    const p1 = { x: cx + r * Math.cos(0), y: cy };
    const p2 = { x: cx + r * Math.cos(-angle), y: cy + r * Math.sin(-angle) };
    const large = Math.abs(angle) > Math.PI ? 1 : 0;
    const sweep = angle > 0 ? 0 : 1;
    let s = '<path d="M '+p1.x+' '+p1.y+' A '+r+' '+r+' 0 '+large+' '+sweep+' '+p2.x+' '+p2.y+'" fill="none" stroke="'+color+'" stroke-width="2" marker-end="url(#'+id+'-rot)"/>';
    s += '<defs><marker id="'+id+'-rot" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="'+color+'"/></marker></defs>';
    return s;
  };

  return C;
};

/* ============================================================
   МОДУЛЬ FUNC — графики функций
   ============================================================ */
A.func = {};

/* Создать canvas для графика.
 * opts: { id, W, H, xRange:[xMin,xMax], yRange:[yMin,yMax], gridStep, bg }
 */
A.func.canvas = function(opts){
  opts = opts || {};
  const W = opts.W || 560;
  const H = opts.H || 240;
  const xRange = opts.xRange || [-5, 5];
  const yRange = opts.yRange || [-3, 3];
  const margin = opts.margin || 24;
  const id = opts.id || ('func-' + Math.floor(Math.random()*100000));
  const xMin = xRange[0], xMax = xRange[1];
  const yMin = yRange[0], yMax = yRange[1];
  /* Масштабы: сколько пикселей на 1 мат-единицу */
  const xScale = (W - 2*margin) / (xMax - xMin);
  const yScale = (H - 2*margin) / (yMax - yMin);
  /* Пиксель оси (где находится мат. 0) */
  const px0 = margin - xMin * xScale;
  const py0 = H - margin + yMin * yScale;

  const C = {
    W, H, xMin, xMax, yMin, yMax, xScale, yScale, px0, py0, id,
    open: '<svg viewBox="0 0 '+W+' '+H+'" preserveAspectRatio="xMidYMid meet" style="width:100%;height:auto;display:block;margin:0 auto;background:'+(opts.bg||'#fff')+';border-radius:10px;border:1px solid #e2e8f0">',
    close: '</svg>',

    pxX: function(x){ return px0 + x * xScale; },
    pxY: function(y){ return py0 - y * yScale; }
  };

  /* === Сетка === */
  C.grid = function(opts){
    opts = opts || {};
    const xStep = opts.xStep || 1;
    const yStep = opts.yStep || 1;
    const color = opts.color || '#f1f5f9';
    let s = '';
    /* Вертикальные линии */
    for (let x = Math.ceil(xMin); x <= Math.floor(xMax); x += xStep){
      const px = C.pxX(x);
      s += '<line x1="'+px+'" y1="0" x2="'+px+'" y2="'+H+'" stroke="'+color+'" stroke-width="1"/>';
    }
    /* Горизонтальные */
    for (let y = Math.ceil(yMin); y <= Math.floor(yMax); y += yStep){
      const py = C.pxY(y);
      s += '<line x1="0" y1="'+py+'" x2="'+W+'" y2="'+py+'" stroke="'+color+'" stroke-width="1"/>';
    }
    return s;
  };

  /* === Оси === */
  C.axes = function(opts){
    opts = opts || {};
    const color = opts.color || '#475569';
    const xTicks = opts.xTicks; /* массив {val, label} */
    const yTicks = opts.yTicks;
    let s = '';
    /* Ось X */
    s += '<line x1="0" y1="'+py0+'" x2="'+W+'" y2="'+py0+'" stroke="'+color+'" stroke-width="1.5"/>';
    /* Ось Y */
    s += '<line x1="'+px0+'" y1="0" x2="'+px0+'" y2="'+H+'" stroke="'+color+'" stroke-width="1.5"/>';
    /* Стрелка X справа */
    s += '<polyline points="'+(W-8)+','+(py0-4)+' '+W+','+py0+' '+(W-8)+','+(py0+4)+'" fill="none" stroke="'+color+'" stroke-width="1.5"/>';
    /* Стрелка Y сверху */
    s += '<polyline points="'+(px0-4)+',8 '+px0+',0 '+(px0+4)+',8" fill="none" stroke="'+color+'" stroke-width="1.5"/>';
    /* Подписи x, y */
    s += '<text x="'+(W-12)+'" y="'+(py0 + 14)+'" font-size="13" font-style="italic" font-family="Inter,sans-serif" fill="'+color+'">x</text>';
    s += '<text x="'+(px0 + 8)+'" y="12" font-size="13" font-style="italic" font-family="Inter,sans-serif" fill="'+color+'">y</text>';
    /* Метка O */
    s += '<text x="'+(px0 - 12)+'" y="'+(py0 + 14)+'" font-size="11" font-style="italic" font-family="Inter,sans-serif" fill="'+color+'">O</text>';
    /* Тики */
    if (xTicks){
      xTicks.forEach(t => {
        const px = C.pxX(t.val);
        s += '<line x1="'+px+'" y1="'+(py0-4)+'" x2="'+px+'" y2="'+(py0+4)+'" stroke="'+color+'" stroke-width="1.5"/>';
        s += '<text x="'+px+'" y="'+(py0 + 18)+'" text-anchor="middle" font-size="10" font-family="JetBrains Mono,monospace" fill="'+color+'">'+(t.label || t.val)+'</text>';
      });
    }
    if (yTicks){
      yTicks.forEach(t => {
        const py = C.pxY(t.val);
        s += '<line x1="'+(px0-4)+'" y1="'+py+'" x2="'+(px0+4)+'" y2="'+py+'" stroke="'+color+'" stroke-width="1.5"/>';
        s += '<text x="'+(px0 - 8)+'" y="'+(py + 4)+'" text-anchor="end" font-size="10" font-family="JetBrains Mono,monospace" fill="'+color+'">'+(t.label || t.val)+'</text>';
      });
    }
    return s;
  };

  /* === График функции y=fn(x) на xRange === */
  C.plot = function(fn, opts){
    opts = opts || {};
    const color = opts.color || '#0d9488';
    const w = opts.width || 2.5;
    const step = opts.step || ((xMax - xMin) / 400);
    const breakOnNaN = opts.breakOnNaN !== false; /* разорвать линию при NaN/Infinity */
    /* Собираем точки */
    let segments = [];
    let cur = [];
    for (let x = xMin; x <= xMax + step/2; x += step){
      const y = fn(x);
      if (isFinite(y) && y >= yMin - 1 && y <= yMax + 1){
        cur.push([C.pxX(x), C.pxY(y)]);
      } else if (cur.length) {
        segments.push(cur);
        cur = [];
      }
    }
    if (cur.length) segments.push(cur);
    /* Рисуем path-ы */
    let s = '';
    for (const seg of segments){
      if (seg.length < 2) continue;
      let d = 'M ' + seg[0][0] + ' ' + seg[0][1];
      for (let i = 1; i < seg.length; i++) d += ' L ' + seg[i][0] + ' ' + seg[i][1];
      s += '<path d="'+d+'" fill="none" stroke="'+color+'" stroke-width="'+w+'" stroke-linecap="round" stroke-linejoin="round"'+(opts.dash?' stroke-dasharray="'+opts.dash+'"':'')+'/>';
    }
    return s;
  };

  /* === Точка с координатами === */
  C.pointXY = function(x, y, opts){
    opts = opts || {};
    const color = opts.color || '#dc2626';
    const r = opts.r || 4;
    const px = C.pxX(x), py = C.pxY(y);
    let s = '<circle cx="'+px+'" cy="'+py+'" r="'+r+'" fill="'+color+'" stroke="#fff" stroke-width="1.5"/>';
    if (opts.label){
      const lx = px + (opts.dx || 8);
      const ly = py + (opts.dy || -8);
      s += '<text x="'+lx+'" y="'+ly+'" font-size="'+(opts.fontSize||12)+'" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="'+(opts.labelColor||color)+'">'+opts.label+'</text>';
    }
    return s;
  };

  /* === Касательная к графику fn в точке x0 === */
  C.tangentLine = function(fn, x0, opts){
    opts = opts || {};
    const color = opts.color || '#dc2626';
    const w = opts.width || 2;
    /* Численная производная */
    const h = 0.0001;
    const k = (fn(x0 + h) - fn(x0 - h)) / (2 * h);
    const y0 = fn(x0);
    /* y = k(x - x0) + y0 */
    const x1 = xMin, y1 = k * (x1 - x0) + y0;
    const x2 = xMax, y2 = k * (x2 - x0) + y0;
    let s = '<line x1="'+C.pxX(x1)+'" y1="'+C.pxY(y1)+'" x2="'+C.pxX(x2)+'" y2="'+C.pxY(y2)+'" stroke="'+color+'" stroke-width="'+w+'" stroke-linecap="round"/>';
    return s;
  };

  /* === Вертикальная асимптота === */
  C.asymptoteV = function(x, opts){
    opts = opts || {};
    const color = opts.color || '#dc2626';
    const px = C.pxX(x);
    return '<line x1="'+px+'" y1="0" x2="'+px+'" y2="'+H+'" stroke="'+color+'" stroke-width="1.5" stroke-dasharray="4 3"/>';
  };

  /* === Горизонтальная асимптота === */
  C.asymptoteH = function(y, opts){
    opts = opts || {};
    const color = opts.color || '#dc2626';
    const py = C.pxY(y);
    return '<line x1="0" y1="'+py+'" x2="'+W+'" y2="'+py+'" stroke="'+color+'" stroke-width="1.5" stroke-dasharray="4 3"/>';
  };

  /* === Закрашенная область под графиком === */
  C.areaUnder = function(fn, a, b, opts){
    opts = opts || {};
    const fill = opts.fill || 'rgba(13,148,136,.18)';
    const step = (b - a) / 200;
    let d = 'M ' + C.pxX(a) + ' ' + C.pxY(0);
    for (let x = a; x <= b; x += step){
      d += ' L ' + C.pxX(x) + ' ' + C.pxY(fn(x));
    }
    d += ' L ' + C.pxX(b) + ' ' + C.pxY(0) + ' Z';
    return '<path d="'+d+'" fill="'+fill+'" stroke="none"/>';
  };

  return C;
};

/* ============================================================
   МОДУЛЬ NTHROOT — графики y = ⁿ√x
   ============================================================ */
A.nthRoot = {};

A.nthRoot.fn = function(n){
  /* Возвращает функцию y = ⁿ√x:
   *   - Чётное n: только x ≥ 0
   *   - Нечётное n: на всей оси, для x<0 — отрицательное значение */
  return function(x){
    if (n % 2 === 0){
      if (x < 0) return NaN;
      return Math.pow(x, 1/n);
    } else {
      if (x < 0) return -Math.pow(-x, 1/n);
      return Math.pow(x, 1/n);
    }
  };
};

/* ============================================================
   KaTeX render (как в geom7_svg.js)
   ============================================================ */
A.renderMath = function(root){
  if (!root || !window.renderMathInElement) return;
  try {
    window.renderMathInElement(root, {
      delimiters: [
        { left: '$$', right: '$$', display: true },
        { left: '$',  right: '$',  display: false },
        { left: '\\[', right: '\\]', display: true },
        { left: '\\(', right: '\\)', display: false }
      ],
      throwOnError: false
    });
  } catch(e){}
};

})();
