/**
 * Canvas Renderer — mirrors FaceRenderer.kt
 * Draws all 21 primitive types.
 */

function renderFace(c, face, ctx, size, selectedEl, skipClear) {
  // Resolve custom @var declarations
  const rctx = resolveCustomVars(face, ctx);
  if (!skipClear) {
    c.clearRect(0, 0, size, size);
  }
  c.fillStyle = resolveColor(face.bg, rctx);
  c.fillRect(0, 0, size, size);
  for (const node of face.elements) drawNode(c, node, rctx, size);

  // Draw selection highlight
  if (selectedEl && !selectedEl.isConditional && !selectedEl.isForLoop) {
    drawSelectionBox(c, selectedEl, ctx, size);
  }
}

function drawNode(c, node, ctx, s) {
  if (node._hidden) return;
  if (node.isGroup) {
    for (const child of node.children || []) drawNode(c, child, ctx, s);
    return;
  }
  if (node.isConditional) {
    if (evalCondition(node.condition, ctx)) {
      for (const child of node.children) drawNode(c, child, ctx, s);
    }
    return;
  }
  if (node.isForLoop) {
    const fromVal = resolveFloat(node.from, ctx);
    const toVal = resolveFloat(node.to, ctx);
    const stepVal = resolveFloat(node.step, ctx) || 1;
    // Guard against wrong direction
    if ((stepVal > 0 && fromVal > toVal + 0.0001) || (stepVal < 0 && fromVal < toVal - 0.0001)) return;
    const savedVar = ctx._customVars ? ctx._customVars[node.varName] : undefined;
    if (!ctx._customVars) ctx._customVars = {};
    let iterations = 0;
    for (let v = fromVal; stepVal > 0 ? v <= toVal + 0.0001 : v >= toVal - 0.0001; v += stepVal) {
      if (++iterations > 1000) break;
      ctx._customVars[node.varName] = String(v);
      for (const child of node.children || []) drawNode(c, child, ctx, s);
    }
    if (savedVar !== undefined) ctx._customVars[node.varName] = savedVar;
    else delete ctx._customVars[node.varName];
    return;
  }
  // Blink: skip drawing during the "off" phase
  const blinkMs = node.props && node.props.blink ? resolveFloat(node.props.blink, ctx) : 0;
  if (blinkMs > 0) {
    const phase = (Date.now() % blinkMs) / blinkMs;
    if (phase > 0.5) return;
  }
  drawElement(c, node, ctx, s);
}

function pct(val, s, ctx) { return resolveFloat(val, ctx) / 100 * s; }
function deg(val, ctx) { return resolveFloat(val, ctx); }
function deg2rad(d) { return d * Math.PI / 180; }
function propPct(el, key, s, ctx, def) {
  const v = el.props[key]; if (v == null) return def !== undefined ? def : 0;
  return resolveFloat(v, ctx) / 100 * s;
}
function propFloat(el, key, ctx, def) {
  const v = el.props[key]; if (v == null) return def !== undefined ? def : 0;
  return resolveFloat(v, ctx);
}
function propColor(el, key, ctx, def) {
  const v = el.props[key]; if (v == null) return def !== undefined ? def : '#FFFFFF';
  return resolveColor(v, ctx);
}
function getAlpha(el, ctx) {
  const a = el.props['alpha']; if (a == null) return 1;
  const v = ctx ? resolveFloat(a, ctx) : parseFloat(a);
  return Math.max(0, Math.min(1, isNaN(v) ? 1 : v));
}
function getCap(el) { return el.props['cap'] === 'square' ? 'square' : el.props['cap'] === 'round' ? 'round' : 'butt'; }

function applyGlow(c, el, ctx, s) {
  const g = parseFloat(el.props['glow']); if (!g || g <= 0) return false;
  const px = Math.max(1, g / 100 * s);
  const gc = el.props['glow_color'];
  c.shadowBlur = px;
  c.shadowColor = gc ? resolveColor(gc, ctx) : c.strokeStyle || c.fillStyle;
  return true;
}
function clearGlow(c) { c.shadowBlur = 0; c.shadowColor = 'transparent'; }

function applyDash(c, el, s) {
  const d = el.props['dash']; if (!d) return;
  const parts = d.split(',');
  if (parts.length >= 2) {
    c.setLineDash([(parseFloat(parts[0])||4)/100*s, (parseFloat(parts[1])||2)/100*s]);
  }
}
function clearDash(c) { c.setLineDash([]); }

function lerpColor(from, to, t) {
  const f = hexToRgb(from), tt = hexToRgb(to);
  if (!f || !tt) return from;
  const r = Math.round(f.r + (tt.r - f.r) * t);
  const g = Math.round(f.g + (tt.g - f.g) * t);
  const b = Math.round(f.b + (tt.b - f.b) * t);
  return `rgb(${r},${g},${b})`;
}
function hexToRgb(color) {
  if (!color) return null;
  if (color.startsWith('rgba')) {
    const m = color.match(/[\d.]+/g);
    if (m) return {r:+m[0],g:+m[1],b:+m[2]};
  }
  if (color.startsWith('rgb')) {
    const m = color.match(/\d+/g);
    if (m) return {r:+m[0],g:+m[1],b:+m[2]};
  }
  let hex = color.replace('#','');
  if (hex.length===3) hex=hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
  if (hex.length===8) hex=hex.substring(2);
  const n = parseInt(hex,16);
  return {r:(n>>16)&255,g:(n>>8)&255,b:n&255};
}

// ── Rounded-rect helpers for corner property ───────────────────────
/** Trace a clockwise rounded-rect path centered at (cx,cy) with half-size r and corner radius cr. */
function traceRoundedRectPath(c, cx, cy, r, cr) {
  c.beginPath();
  c.moveTo(cx + r, cy);
  c.lineTo(cx + r, cy + r - cr);
  if (cr > 0.1) c.arc(cx + r - cr, cy + r - cr, cr, 0, Math.PI / 2);
  else c.lineTo(cx + r, cy + r);
  c.lineTo(cx - r + cr, cy + r);
  if (cr > 0.1) c.arc(cx - r + cr, cy + r - cr, cr, Math.PI / 2, Math.PI);
  else c.lineTo(cx - r, cy + r);
  c.lineTo(cx - r, cy - r + cr);
  if (cr > 0.1) c.arc(cx - r + cr, cy - r + cr, cr, Math.PI, 3 * Math.PI / 2);
  else c.lineTo(cx - r, cy - r);
  c.lineTo(cx + r - cr, cy - r);
  if (cr > 0.1) c.arc(cx + r - cr, cy - r + cr, cr, 3 * Math.PI / 2, 2 * Math.PI);
  else c.lineTo(cx + r, cy - r);
  c.closePath();
}

/** Compute total perimeter of a rounded rect with half-size r and corner radius cr. */
function roundedRectPerim(r, cr) {
  const straight = Math.max(0, r - cr);
  return 8 * straight + 2 * Math.PI * cr;
}

/** Map angle (0°=right, CW) to perimeter distance on rounded rect. */
function angleToDist(angleDeg, r, cr) {
  const straight = Math.max(0, r - cr);
  const straight2 = 2 * straight;
  const arcLen = Math.PI * cr / 2;
  const totalPerim = 4 * straight2 + 4 * arcLen;
  if (totalPerim <= 0) return 0;
  // Use 4*straight2 = 8*straight which equals our formula
  const normAngle = ((angleDeg % 360) + 360) % 360;
  return (normAngle / 360) * totalPerim;
}

/**
 * Map an angle (0°=right, CW) to a point on the rounded rect perimeter + inward normal.
 * Returns {x, y, nx, ny} where (nx,ny) is the unit vector pointing inward.
 */
function pointOnRoundedRect(cx, cy, r, cr, angleDeg) {
  const straight = Math.max(0, r - cr);
  const straight2 = 2 * straight;
  const arcLen = Math.PI * cr / 2;
  const segLens = [straight, arcLen, straight2, arcLen, straight2, arcLen, straight2, arcLen, straight];
  const totalPerim = segLens.reduce((a,b) => a+b, 0);
  if (totalPerim <= 0) return {x: cx+r, y: cy, nx: -1, ny: 0};

  const normAngle = ((angleDeg % 360) + 360) % 360;
  let dist = (normAngle / 360) * totalPerim;

  for (let i = 0; i < segLens.length; i++) {
    if (dist <= segLens[i] + 0.001 || i === segLens.length - 1) {
      const t = segLens[i] > 0 ? Math.min(1, dist / segLens[i]) : 0;
      switch (i) {
        case 0: return {x: cx+r, y: cy + t*straight, nx: -1, ny: 0};
        case 1: { const a = t*Math.PI/2; return {x: cx+r-cr + cr*Math.cos(a), y: cy+r-cr + cr*Math.sin(a), nx: -Math.cos(a), ny: -Math.sin(a)}; }
        case 2: return {x: cx+r-cr - t*straight2, y: cy+r, nx: 0, ny: -1};
        case 3: { const a = Math.PI/2 + t*Math.PI/2; return {x: cx-r+cr + cr*Math.cos(a), y: cy+r-cr + cr*Math.sin(a), nx: -Math.cos(a), ny: -Math.sin(a)}; }
        case 4: return {x: cx-r, y: cy+r-cr - t*straight2, nx: 1, ny: 0};
        case 5: { const a = Math.PI + t*Math.PI/2; return {x: cx-r+cr + cr*Math.cos(a), y: cy-r+cr + cr*Math.sin(a), nx: -Math.cos(a), ny: -Math.sin(a)}; }
        case 6: return {x: cx-r+cr + t*straight2, y: cy-r, nx: 0, ny: 1};
        case 7: { const a = 3*Math.PI/2 + t*Math.PI/2; return {x: cx+r-cr + cr*Math.cos(a), y: cy-r+cr + cr*Math.sin(a), nx: -Math.cos(a), ny: -Math.sin(a)}; }
        case 8: return {x: cx+r, y: cy-r+cr + t*straight, nx: -1, ny: 0};
      }
    }
    dist -= segLens[i];
  }
  return {x: cx+r, y: cy, nx: -1, ny: 0};
}

function drawElement(c, el, ctx, s) {
  const t = el.type;
  if (t==='circle') drawCircle(c,el,ctx,s);
  else if (t==='arc') drawArc(c,el,ctx,s);
  else if (t==='rect') drawRect(c,el,ctx,s);
  else if (t==='line') drawLine(c,el,ctx,s);
  else if (t==='text') drawText(c,el,ctx,s);
  else if (t==='ticks') drawTicks(c,el,ctx,s);
  else if (t==='ring') drawRing(c,el,ctx,s);
  else if (t==='polygon') drawPolygon(c,el,ctx,s);
  else if (t==='shape') drawShape(c,el,ctx,s);
  else if (t==='ellipse') drawEllipse(c,el,ctx,s);
  else if (t==='dot') drawDot(c,el,ctx,s);
  else if (t==='gradient_ring') drawGradientRing(c,el,ctx,s);
  else if (t==='progress_arc') drawProgressArc(c,el,ctx,s);
  else if (t==='progress_bar') drawProgressBar(c,el,ctx,s);
  else if (t==='progress_ring') drawProgressRing(c,el,ctx,s);
  else if (t==='needle') drawNeedle(c,el,ctx,s);
  else if (t==='value_dot') drawValueDot(c,el,ctx,s);
  else if (t==='zone_arc') drawZoneArc(c,el,ctx,s);
  else if (t==='zone_ring') drawZoneRing(c,el,ctx,s);
  else if (t==='sweep_ticks') drawSweepTicks(c,el,ctx,s);
  else if (t==='digital_bar') drawDigitalBar(c,el,ctx,s);
}

// ── Selection box overlay ───────────────────────────────────────────
function drawSelectionBox(c, el, ctx, s) {
  const bounds = getElementBounds(el, ctx, s);
  if (!bounds) return;
  c.save();
  c.strokeStyle = '#4FC3F7';
  c.lineWidth = 1.5;
  c.setLineDash([4, 3]);
  c.strokeRect(bounds.x - 3, bounds.y - 3, bounds.w + 6, bounds.h + 6);
  // Corner handles
  c.fillStyle = '#4FC3F7';
  c.setLineDash([]);
  const hs = 4;
  const corners = [
    [bounds.x - 3 - hs/2, bounds.y - 3 - hs/2],
    [bounds.x + bounds.w + 3 - hs/2, bounds.y - 3 - hs/2],
    [bounds.x - 3 - hs/2, bounds.y + bounds.h + 3 - hs/2],
    [bounds.x + bounds.w + 3 - hs/2, bounds.y + bounds.h + 3 - hs/2]
  ];
  for (const [cx, cy] of corners) {
    c.fillRect(cx, cy, hs, hs);
  }
  c.restore();
}

// ── Hit testing & bounding boxes ────────────────────────────────────
function getElementBounds(el, ctx, s) {
  if (!el || el.isConditional || el.isGroup || el.isForLoop) return null;
  const t = el.type;
  try {
    switch (t) {
      case 'circle': {
        if (el.args.length < 3) return null;
        const cx = pct(el.args[0],s,ctx), cy = pct(el.args[1],s,ctx), r = pct(el.args[2],s,ctx);
        return { x: cx-r, y: cy-r, w: r*2, h: r*2 };
      }
      case 'arc':
      case 'progress_arc': {
        if (el.args.length < 3) return null;
        const cx = pct(el.args[0],s,ctx), cy = pct(el.args[1],s,ctx), r = pct(el.args[2],s,ctx);
        return { x: cx-r, y: cy-r, w: r*2, h: r*2 };
      }
      case 'rect':
      case 'progress_bar': {
        if (el.args.length < 4) return null;
        return { x: pct(el.args[0],s,ctx), y: pct(el.args[1],s,ctx), w: pct(el.args[2],s,ctx), h: pct(el.args[3],s,ctx) };
      }
      case 'line': {
        if (el.args.length < 4) return null;
        const x1=pct(el.args[0],s,ctx),y1=pct(el.args[1],s,ctx),x2=pct(el.args[2],s,ctx),y2=pct(el.args[3],s,ctx);
        const margin = 5;
        return { x: Math.min(x1,x2)-margin, y: Math.min(y1,y2)-margin, w: Math.abs(x2-x1)+margin*2, h: Math.abs(y2-y1)+margin*2 };
      }
      case 'text': {
        if (el.args.length < 3) return null;
        const x = pct(el.args[1],s,ctx), y = pct(el.args[2],s,ctx);
        const sz = propPct(el,'size',s,ctx, 0.1*s);
        const content = resolveText(el.args[0], ctx);
        const approxW = content.length * sz * 0.6;
        const align = el.props['align'] || 'center';
        let bx = x - approxW/2;
        if (align === 'left') bx = x;
        else if (align === 'right') bx = x - approxW;
        return { x: bx, y: y - sz/2, w: approxW, h: sz };
      }
      case 'ticks':
      case 'sweep_ticks': {
        if (el.args.length < 3) return null;
        const cx = pct(el.args[0],s,ctx), cy = pct(el.args[1],s,ctx), r = pct(el.args[2],s,ctx);
        return { x: cx-r, y: cy-r, w: r*2, h: r*2 };
      }
      case 'ring':
      case 'gradient_ring':
      case 'progress_ring':
      case 'zone_ring': {
        if (el.args.length < 3) return null;
        const cx = pct(el.args[0],s,ctx), cy = pct(el.args[1],s,ctx), r = pct(el.args[2],s,ctx);
        return { x: cx-r, y: cy-r, w: r*2, h: r*2 };
      }
      case 'polygon':
      case 'dot': {
        if (el.args.length < 3) return null;
        const cx = pct(el.args[0],s,ctx), cy = pct(el.args[1],s,ctx), r = pct(el.args[2],s,ctx);
        return { x: cx-r, y: cy-r, w: r*2, h: r*2 };
      }
      case 'ellipse': {
        if (el.args.length < 4) return null;
        const cx = pct(el.args[0],s,ctx), cy = pct(el.args[1],s,ctx);
        const rx = pct(el.args[2],s,ctx), ry = pct(el.args[3],s,ctx);
        return { x: cx-rx, y: cy-ry, w: rx*2, h: ry*2 };
      }
      case 'needle': {
        if (el.args.length < 3) return null;
        const cx = pct(el.args[0],s,ctx), cy = pct(el.args[1],s,ctx), len = pct(el.args[2],s,ctx);
        return { x: cx-len, y: cy-len, w: len*2, h: len*2 };
      }
      case 'value_dot': {
        if (el.args.length < 3) return null;
        const cx = pct(el.args[0],s,ctx), cy = pct(el.args[1],s,ctx), r = pct(el.args[2],s,ctx);
        return { x: cx-r, y: cy-r, w: r*2, h: r*2 };
      }
      case 'zone_arc': {
        if (el.args.length < 3) return null;
        const cx = pct(el.args[0],s,ctx), cy = pct(el.args[1],s,ctx), r = pct(el.args[2],s,ctx);
        return { x: cx-r, y: cy-r, w: r*2, h: r*2 };
      }
      case 'digital_bar': {
        if (el.args.length < 4) return null;
        return { x: pct(el.args[0],s,ctx), y: pct(el.args[1],s,ctx), w: pct(el.args[2],s,ctx), h: pct(el.args[3],s,ctx) };
      }
      case 'shape': {
        if (el.args.length < 6) return null;
        let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
        let i = 0;
        while (i < el.args.length) {
          if (el.args[i] === 'M') { i++; continue; }
          if (i + 1 >= el.args.length) break;
          const px = pct(el.args[i],s,ctx), py = pct(el.args[i+1],s,ctx);
          minX = Math.min(minX, px); maxX = Math.max(maxX, px);
          minY = Math.min(minY, py); maxY = Math.max(maxY, py);
          i += 2;
        }
        return { x: minX, y: minY, w: maxX-minX, h: maxY-minY };
      }
      default: return null;
    }
  } catch(e) { return null; }
}

/**
 * Get the position arg indices (cx, cy) for a given element type.
 * Returns [xIdx, yIdx] or null if not movable.
 */
function getPositionArgs(type) {
  switch (type) {
    case 'circle': case 'arc': case 'ticks': case 'ring': case 'polygon': case 'dot': case 'ellipse':
    case 'gradient_ring': case 'progress_arc': case 'progress_ring': case 'needle':
    case 'value_dot': case 'zone_arc': case 'zone_ring': case 'sweep_ticks':
      return [0, 1]; // cx, cy
    case 'rect': case 'line': case 'progress_bar': case 'digital_bar': case 'shape':
      return [0, 1]; // x, y
    case 'text':
      return [1, 2]; // content is arg0, x=arg1, y=arg2
    default:
      return null;
  }
}

// 1. CIRCLE
function drawCircle(c, el, ctx, s) {
  if (el.args.length < 3) return;
  const cx=pct(el.args[0],s,ctx), cy=pct(el.args[1],s,ctx), r=pct(el.args[2],s,ctx);
  const alpha = getAlpha(el,ctx);
  c.save();
  c.globalAlpha = alpha;
  const fill = el.props['fill'];
  if (fill) {
    applyGlow(c,el,ctx,s);
    c.fillStyle = resolveColor(fill, ctx);
    c.beginPath(); c.arc(cx,cy,r,0,Math.PI*2); c.fill();
    clearGlow(c);
  }
  const stroke = el.props['stroke'];
  if (stroke) {
    applyGlow(c,el,ctx,s);
    c.strokeStyle = resolveColor(stroke, ctx);
    c.lineWidth = propPct(el,'w',s,ctx, 0.01*s);
    applyDash(c,el,s);
    c.beginPath(); c.arc(cx,cy,r,0,Math.PI*2); c.stroke();
    clearGlow(c); clearDash(c);
  }
  c.restore();
}

// ELLIPSE
function drawEllipse(c, el, ctx, s) {
  if (el.args.length < 4) return;
  const cx=pct(el.args[0],s,ctx), cy=pct(el.args[1],s,ctx);
  const rx=pct(el.args[2],s,ctx), ry=pct(el.args[3],s,ctx);
  const rot = el.props['rotation'] ? deg2rad(resolveFloat(el.props['rotation'], ctx)) : 0;
  c.save();
  c.globalAlpha = getAlpha(el,ctx);
  const fill = el.props['fill'];
  if (fill) {
    applyGlow(c,el,ctx,s);
    c.fillStyle = resolveColor(fill, ctx);
    c.beginPath(); c.ellipse(cx, cy, Math.abs(rx), Math.abs(ry), rot, 0, Math.PI*2); c.fill();
    clearGlow(c);
  }
  const stroke = el.props['stroke'];
  if (stroke) {
    applyGlow(c,el,ctx,s);
    c.strokeStyle = resolveColor(stroke, ctx);
    c.lineWidth = propPct(el,'w',s,ctx, 0.01*s);
    c.beginPath(); c.ellipse(cx, cy, Math.abs(rx), Math.abs(ry), rot, 0, Math.PI*2); c.stroke();
    clearGlow(c);
  }
  c.restore();
}

// 2. ARC
function drawArc(c, el, ctx, s) {
  if (el.args.length < 5) return;
  const cx=pct(el.args[0],s,ctx), cy=pct(el.args[1],s,ctx), r=pct(el.args[2],s,ctx);
  const start=deg(el.args[3],ctx), sweep=deg(el.args[4],ctx);

  // Rounded rect mode
  const cornerProp = el.props['corner'];
  if (cornerProp != null) {
    const cornerPct = resolveFloat(cornerProp, ctx);
    const cr = Math.max(0, Math.min(r, r * cornerPct / 100));
    const totalPerim = roundedRectPerim(r, cr);
    if (totalPerim <= 0) return;
    const normStart = ((start % 360) + 360) % 360;
    const startDist = (normStart / 360) * totalPerim;
    const sweepDist = Math.abs(sweep / 360) * totalPerim;
    c.save();
    c.globalAlpha = getAlpha(el,ctx);
    const stroke = el.props['stroke'] || el.props['color'];
    if (stroke) {
      applyGlow(c,el,ctx,s);
      c.strokeStyle = resolveColor(stroke, ctx);
      c.lineWidth = propPct(el,'w',s,ctx, 0.01*s);
      c.lineCap = getCap(el);
      applyDash(c,el,s);
      c.setLineDash([sweepDist, totalPerim]);
      c.lineDashOffset = -startDist;
      traceRoundedRectPath(c, cx, cy, r, cr);
      c.stroke();
      clearGlow(c); c.setLineDash([]);
    }
    c.restore();
    return;
  }

  const alpha = getAlpha(el,ctx);
  c.save();
  c.globalAlpha = alpha;
  const fill = el.props['fill'];
  if (fill) {
    c.fillStyle = resolveColor(fill, ctx);
    c.beginPath();
    c.moveTo(cx, cy);
    c.arc(cx, cy, r, deg2rad(start), deg2rad(start+sweep), sweep < 0);
    c.closePath();
    c.fill();
  }
  const stroke = el.props['stroke'] || el.props['color'];
  if (stroke) {
    applyGlow(c,el,ctx,s);
    c.strokeStyle = resolveColor(stroke, ctx);
    c.lineWidth = propPct(el,'w',s,ctx, 0.01*s);
    c.lineCap = getCap(el);
    applyDash(c,el,s);
    c.beginPath();
    c.arc(cx, cy, r, deg2rad(start), deg2rad(start+sweep), sweep < 0);
    c.stroke();
    clearGlow(c); clearDash(c);
  }
  c.restore();
}

// 3. RECT
function drawRect(c, el, ctx, s) {
  if (el.args.length < 4) return;
  const x=pct(el.args[0],s,ctx), y=pct(el.args[1],s,ctx);
  const w=pct(el.args[2],s,ctx), h=pct(el.args[3],s,ctx);
  const corner = propPct(el,'corner',s,ctx);
  const alpha = getAlpha(el,ctx);
  c.save();
  c.globalAlpha = alpha;
  const fill = el.props['fill'] || el.props['bg'];
  if (fill) {
    c.fillStyle = resolveColor(fill, ctx);
    if (corner > 0) { roundRect(c, x, y, w, h, corner); c.fill(); }
    else c.fillRect(x, y, w, h);
  }
  const stroke = el.props['stroke'];
  if (stroke) {
    c.strokeStyle = resolveColor(stroke, ctx);
    c.lineWidth = propPct(el,'sw',s,ctx, 0.01*s);
    if (corner > 0) { roundRect(c, x, y, w, h, corner); c.stroke(); }
    else c.strokeRect(x, y, w, h);
  }
  c.restore();
}

function roundRect(c, x, y, w, h, r) {
  c.beginPath();
  c.moveTo(x+r, y);
  c.lineTo(x+w-r, y);
  c.quadraticCurveTo(x+w, y, x+w, y+r);
  c.lineTo(x+w, y+h-r);
  c.quadraticCurveTo(x+w, y+h, x+w-r, y+h);
  c.lineTo(x+r, y+h);
  c.quadraticCurveTo(x, y+h, x, y+h-r);
  c.lineTo(x, y+r);
  c.quadraticCurveTo(x, y, x+r, y);
  c.closePath();
}

// 4. LINE
function drawLine(c, el, ctx, s) {
  if (el.args.length < 4) return;
  c.save();
  c.globalAlpha = getAlpha(el,ctx);
  c.strokeStyle = propColor(el,'stroke',ctx, propColor(el,'color',ctx));
  c.lineWidth = propPct(el,'w',s,ctx, 0.005*s);
  c.lineCap = getCap(el);
  applyDash(c,el,s);
  c.beginPath();
  c.moveTo(pct(el.args[0],s,ctx), pct(el.args[1],s,ctx));
  c.lineTo(pct(el.args[2],s,ctx), pct(el.args[3],s,ctx));
  c.stroke();
  clearDash(c);
  c.restore();
}

// Font mapping: DSL name → CSS font-family
const FONT_MAP = {
  'orbitron': "'Orbitron', sans-serif",
  'russo': "'Russo One', sans-serif",
  'teko': "'Teko', sans-serif",
  'rajdhani': "'Rajdhani', sans-serif",
  'mono': "'Share Tech Mono', monospace",
  'audiowide': "'Audiowide', sans-serif",
  'dseg7': "'DSEG7', monospace",
  'barlow': "'Barlow Semi Condensed', sans-serif",
  'saira': "'Saira Condensed', sans-serif",
  'play': "'Play', sans-serif"
};
function resolveFont(name) {
  if (!name || name === 'sans-serif') return 'sans-serif';
  return FONT_MAP[name.toLowerCase()] || name;
}

// 5. TEXT
function drawText(c, el, ctx, s) {
  if (el.args.length < 3) return;
  const content = resolveText(el.args[0], ctx);
  const x = pct(el.args[1], s, ctx), y = pct(el.args[2], s, ctx);
  const textSize = propPct(el,'size',s,ctx, 0.1*s);
  const alpha = getAlpha(el,ctx);
  c.save();
  c.globalAlpha = alpha;
  c.fillStyle = propColor(el,'color',ctx);
  const bold = el.props['bold'] != null;
  const italic = el.props['italic'] != null;
  let weight = bold ? 'bold' : 'normal';
  let style = italic ? 'italic' : 'normal';
  const font = resolveFont(el.props['font']);
  c.font = `${style} ${weight} ${textSize}px ${font}`;
  const align = el.props['align'] || 'center';
  c.textAlign = align;
  c.textBaseline = 'middle';
  const glowPct = parseFloat(el.props['glow']);
  if (glowPct > 0) {
    const glowPx = Math.max(1, glowPct / 100 * s);
    c.shadowBlur = glowPx;
    c.shadowColor = el.props['glow_color'] ? resolveColor(el.props['glow_color'], ctx) : c.fillStyle;
  }
  c.fillText(content, x, y);
  c.restore();
}

// 6. TICKS
function drawTicks(c, el, ctx, s) {
  if (el.args.length < 5) return;
  const cx=pct(el.args[0],s,ctx), cy=pct(el.args[1],s,ctx), r=pct(el.args[2],s,ctx);
  const count = parseInt(el.args[3]) || 0;
  const len = pct(el.args[4],s,ctx);
  const startAngle = propFloat(el,'start',ctx,-225);
  const sweepAngle = propFloat(el,'sweep',ctx,270);
  if (count < 2) return;
  c.save();
  c.globalAlpha = getAlpha(el,ctx);
  c.strokeStyle = propColor(el,'stroke',ctx);
  c.lineWidth = propPct(el,'w',s,ctx, 0.005*s);
  c.lineCap = getCap(el);

  // Rounded rect mode
  const cornerProp = el.props['corner'];
  if (cornerProp != null) {
    const cornerPct = resolveFloat(cornerProp, ctx);
    const cr = Math.max(0, Math.min(r, r * cornerPct / 100));
    for (let i = 0; i < count; i++) {
      const angle = startAngle + sweepAngle * i / (count - 1);
      const pt = pointOnRoundedRect(cx, cy, r, cr, angle);
      c.beginPath();
      c.moveTo(pt.x + pt.nx * len, pt.y + pt.ny * len);
      c.lineTo(pt.x, pt.y);
      c.stroke();
    }
    c.restore();
    return;
  }

  for (let i = 0; i < count; i++) {
    const angle = startAngle + sweepAngle * i / (count - 1);
    const rad = deg2rad(angle);
    c.beginPath();
    c.moveTo(cx + (r-len)*Math.cos(rad), cy + (r-len)*Math.sin(rad));
    c.lineTo(cx + r*Math.cos(rad), cy + r*Math.sin(rad));
    c.stroke();
  }
  c.restore();
}

// 7. RING
function drawRing(c, el, ctx, s) {
  if (el.args.length < 6) return;
  const cx=pct(el.args[0],s,ctx), cy=pct(el.args[1],s,ctx);
  const rOuter=pct(el.args[2],s,ctx), rInner=pct(el.args[3],s,ctx);
  const start=deg(el.args[4],ctx), sweep=deg(el.args[5],ctx);
  const midR = (rOuter+rInner)/2, thickness = rOuter-rInner;

  // Rounded rect mode
  const cornerProp = el.props['corner'];
  if (cornerProp != null) {
    const cornerPct = resolveFloat(cornerProp, ctx);
    const cr = Math.max(0, Math.min(midR, midR * cornerPct / 100));
    const totalPerim = roundedRectPerim(midR, cr);
    if (totalPerim <= 0) return;
    const normStart = ((start % 360) + 360) % 360;
    const startDist = (normStart / 360) * totalPerim;
    const sweepDist = Math.abs(sweep / 360) * totalPerim;
    c.save();
    c.globalAlpha = getAlpha(el,ctx);
    c.strokeStyle = propColor(el,'fill',ctx, propColor(el,'stroke',ctx));
    c.lineWidth = thickness;
    c.lineCap = 'butt';
    c.setLineDash([sweepDist, totalPerim]);
    c.lineDashOffset = -startDist;
    traceRoundedRectPath(c, cx, cy, midR, cr);
    c.stroke();
    c.setLineDash([]);
    c.restore();
    return;
  }

  c.save();
  c.globalAlpha = getAlpha(el,ctx);
  c.strokeStyle = propColor(el,'fill',ctx, propColor(el,'stroke',ctx));
  c.lineWidth = thickness;
  c.lineCap = 'butt';
  c.beginPath();
  c.arc(cx, cy, midR, deg2rad(start), deg2rad(start+sweep), sweep<0);
  c.stroke();
  c.restore();
}

// 8. POLYGON
function drawPolygon(c, el, ctx, s) {
  if (el.args.length < 4) return;
  const cx=pct(el.args[0],s,ctx), cy=pct(el.args[1],s,ctx), r=pct(el.args[2],s,ctx);
  const sides = Math.round(resolveFloat(el.args[3], ctx)) || 0;
  if (sides < 3) return;
  const rotation = propFloat(el,'rotation',ctx,-90);
  c.save();
  c.globalAlpha = getAlpha(el,ctx);
  c.beginPath();
  for (let i = 0; i < sides; i++) {
    const angle = rotation + 360 * i / sides;
    const rad = deg2rad(angle);
    const px = cx + r*Math.cos(rad), py = cy + r*Math.sin(rad);
    if (i===0) c.moveTo(px,py); else c.lineTo(px,py);
  }
  c.closePath();
  const fill = el.props['fill'];
  if (fill) {
    applyGlow(c,el,ctx,s);
    c.fillStyle = resolveColor(fill,ctx); c.fill();
    clearGlow(c);
  }
  const stroke = el.props['stroke'];
  if (stroke) {
    applyGlow(c,el,ctx,s);
    c.strokeStyle = resolveColor(stroke,ctx); c.lineWidth = propPct(el,'w',s,ctx,0.005*s); c.stroke();
    clearGlow(c);
  }
  c.restore();
}

// 8b. SHAPE (free-form polygon)
function drawShape(c, el, ctx, s) {
  if (el.args.length < 6) return; // min 3 points (6 coords)
  c.save();
  c.globalAlpha = getAlpha(el,ctx);
  c.beginPath();
  let compound = false, first = true;
  let i = 0;
  while (i < el.args.length) {
    if (el.args[i] === 'M') { c.closePath(); first = true; compound = true; i++; continue; }
    if (i + 1 >= el.args.length) break;
    const px = pct(el.args[i], s, ctx), py = pct(el.args[i+1], s, ctx);
    if (first) { c.moveTo(px, py); first = false; } else c.lineTo(px, py);
    i += 2;
  }
  c.closePath();
  const fillRule = compound ? 'evenodd' : 'nonzero';
  const fill = el.props['fill'];
  if (fill) {
    applyGlow(c,el,ctx,s);
    c.fillStyle = resolveColor(fill,ctx); c.fill(fillRule);
    clearGlow(c);
  }
  const stroke = el.props['stroke'];
  if (stroke) {
    applyGlow(c,el,ctx,s);
    c.strokeStyle = resolveColor(stroke,ctx); c.lineWidth = propPct(el,'w',s,ctx,0.005*s); c.stroke();
    clearGlow(c);
  }
  c.restore();
}

// 9. DOT
function drawDot(c, el, ctx, s) {
  if (el.args.length < 3) return;
  const cx=pct(el.args[0],s,ctx), cy=pct(el.args[1],s,ctx), r=pct(el.args[2],s,ctx);
  c.save();
  c.globalAlpha = getAlpha(el,ctx);
  c.fillStyle = propColor(el,'fill',ctx, propColor(el,'color',ctx));
  if (applyGlow(c,el,ctx,s)) {
    c.beginPath(); c.arc(cx,cy,r,0,Math.PI*2); c.fill();
    clearGlow(c);
  }
  c.beginPath(); c.arc(cx,cy,r,0,Math.PI*2); c.fill();
  c.restore();
}

// 10. GRADIENT_RING
function drawGradientRing(c, el, ctx, s) {
  if (el.args.length < 7) return;
  const cx=pct(el.args[0],s,ctx), cy=pct(el.args[1],s,ctx);
  const rOuter=pct(el.args[2],s,ctx), rInner=pct(el.args[3],s,ctx);
  const start=deg(el.args[4],ctx), sweep=deg(el.args[5],ctx);
  const count = Math.max(2, Math.min(360, parseInt(el.args[6]) || 36));
  const fromColor = propColor(el,'from',ctx, propColor(el,'color',ctx));
  const toColor = propColor(el,'to',ctx, propColor(el,'color',ctx));
  const midR = (rOuter+rInner)/2, thickness = rOuter-rInner;
  const segSweep = sweep / count;

  // Rounded rect mode
  const cornerProp = el.props['corner'];
  if (cornerProp != null) {
    const cornerPct = resolveFloat(cornerProp, ctx);
    const cr = Math.max(0, Math.min(midR, midR * cornerPct / 100));
    const totalPerim = roundedRectPerim(midR, cr);
    if (totalPerim <= 0) return;
    c.save();
    c.globalAlpha = getAlpha(el,ctx);
    c.lineWidth = thickness;
    c.lineCap = 'butt';
    for (let i = 0; i < count; i++) {
      const t = i / (count - 1);
      c.strokeStyle = lerpColor(fromColor, toColor, t);
      const sa = start + segSweep * i;
      // Each segment: from sa to sa+segSweep+overlap
      const segSweepWithOverlap = segSweep + 0.3;
      const normSa = ((sa % 360) + 360) % 360;
      const segStartDist = (normSa / 360) * totalPerim;
      const segLen = Math.abs(segSweepWithOverlap / 360) * totalPerim;
      c.setLineDash([segLen, totalPerim]);
      c.lineDashOffset = -segStartDist;
      traceRoundedRectPath(c, cx, cy, midR, cr);
      c.stroke();
    }
    c.setLineDash([]);
    c.restore();
    return;
  }

  c.save();
  c.globalAlpha = getAlpha(el,ctx);
  c.lineWidth = thickness;
  c.lineCap = 'butt';
  for (let i = 0; i < count; i++) {
    const t = i / (count - 1);
    c.strokeStyle = lerpColor(fromColor, toColor, t);
    c.beginPath();
    const sa = start + segSweep * i;
    c.arc(cx, cy, midR, deg2rad(sa), deg2rad(sa + segSweep + 0.3), false);
    c.stroke();
  }
  c.restore();
}

// 11. PROGRESS_ARC
function drawProgressArc(c, el, ctx, s) {
  if (el.args.length < 5) return;
  const cx=pct(el.args[0],s,ctx), cy=pct(el.args[1],s,ctx), r=pct(el.args[2],s,ctx);
  const start=deg(el.args[3],ctx), maxSweep=deg(el.args[4],ctx);
  const sweep = maxSweep * Math.max(0, Math.min(1, ctx.progress));
  const strokeW = propPct(el,'w',s,ctx, 0.02*s);

  // Rounded rect mode: corner:0 = square, corner:100 = circle
  const cornerProp = el.props['corner'];
  if (cornerProp != null) {
    drawProgressArcRounded(c, el, ctx, s, cx, cy, r, start, maxSweep, sweep, strokeW);
    return;
  }

  c.save();
  c.globalAlpha = getAlpha(el,ctx);
  c.lineWidth = strokeW;
  c.lineCap = getCap(el);
  const bgColor = el.props['bg'];
  if (bgColor) {
    c.strokeStyle = resolveColor(bgColor, ctx);
    c.beginPath();
    c.arc(cx, cy, r, deg2rad(start), deg2rad(start+maxSweep), maxSweep<0);
    c.stroke();
  }
  c.strokeStyle = propColor(el,'stroke',ctx, propColor(el,'color',ctx));
  if (applyGlow(c,el,ctx,s)) {
    const savedAlpha = c.globalAlpha;
    c.globalAlpha = savedAlpha * 0.5;
    c.beginPath();
    c.arc(cx, cy, r, deg2rad(start), deg2rad(start+sweep), sweep<0);
    c.stroke();
    clearGlow(c);
    c.globalAlpha = savedAlpha;
  }
  c.beginPath();
  c.arc(cx, cy, r, deg2rad(start), deg2rad(start+sweep), sweep<0);
  c.stroke();
  c.restore();
}

/** Rounded-rect progress arc using lineDash trick.
 *  Path starts at angle 0 (right center), traces clockwise. */
function drawProgressArcRounded(c, el, ctx, s, cx, cy, r, startAngle, maxSweep, sweep, strokeW) {
  const cornerPct = propFloat(el, 'corner', ctx, 100);
  const cr = Math.max(0, Math.min(r, r * cornerPct / 100));

  const totalPerim = roundedRectPerim(r, cr);
  if (totalPerim <= 0) return;

  // Map angles to perimeter distances (linear proportional mapping)
  const normStart = ((startAngle % 360) + 360) % 360;
  const startDist = (normStart / 360) * totalPerim;
  const sweepDist = Math.abs(maxSweep / 360) * totalPerim;
  const progressDist = Math.abs(sweep / 360) * totalPerim;

  function tracePath() { traceRoundedRectPath(c, cx, cy, r, cr); }

  c.save();
  c.globalAlpha = getAlpha(el, ctx);
  c.lineWidth = strokeW;
  c.lineCap = getCap(el);

  // Background (full sweep portion)
  const bgColor = el.props['bg'];
  if (bgColor) {
    c.strokeStyle = resolveColor(bgColor, ctx);
    c.setLineDash([sweepDist, totalPerim]);
    c.lineDashOffset = -startDist;
    tracePath();
    c.stroke();
  }

  // Progress
  if (progressDist > 0.5) {
    c.strokeStyle = propColor(el, 'stroke', ctx, propColor(el, 'color', ctx));
    if (applyGlow(c, el, ctx, s)) {
      const savedAlpha = c.globalAlpha;
      c.globalAlpha = savedAlpha * 0.5;
      c.setLineDash([progressDist, totalPerim]);
      c.lineDashOffset = -startDist;
      tracePath();
      c.stroke();
      clearGlow(c);
      c.globalAlpha = savedAlpha;
    }
    c.setLineDash([progressDist, totalPerim]);
    c.lineDashOffset = -startDist;
    tracePath();
    c.stroke();
  }

  c.setLineDash([]);
  c.restore();
}

// 12. PROGRESS_BAR
function drawProgressBar(c, el, ctx, s) {
  if (el.args.length < 4) return;
  const x=pct(el.args[0],s,ctx), y=pct(el.args[1],s,ctx);
  const w=pct(el.args[2],s,ctx), h=pct(el.args[3],s,ctx);
  const corner = propPct(el,'corner',s,ctx);
  const dir = el.props['dir'] || 'right';
  const p = Math.max(0, Math.min(1, ctx.progress));
  c.save();
  c.globalAlpha = getAlpha(el,ctx);
  const bgColor = el.props['bg'];
  if (bgColor) {
    c.fillStyle = resolveColor(bgColor, ctx);
    if (corner>0) { roundRect(c,x,y,w,h,corner); c.fill(); }
    else c.fillRect(x,y,w,h);
  }
  c.fillStyle = propColor(el,'fill',ctx, propColor(el,'color',ctx));
  let fx,fy,fw,fh;
  switch(dir) {
    case 'left': fx=x+w*(1-p); fy=y; fw=w*p; fh=h; break;
    case 'up': fx=x; fy=y+h*(1-p); fw=w; fh=h*p; break;
    case 'down': fx=x; fy=y; fw=w; fh=h*p; break;
    default: fx=x; fy=y; fw=w*p; fh=h;
  }
  if (fw > 0 && fh > 0) {
    if (corner>0) { roundRect(c,fx,fy,fw,fh,corner); c.fill(); }
    else c.fillRect(fx,fy,fw,fh);
  }
  c.restore();
}

// 13. PROGRESS_RING
function drawProgressRing(c, el, ctx, s) {
  if (el.args.length < 6) return;
  const cx=pct(el.args[0],s,ctx), cy=pct(el.args[1],s,ctx);
  const rOuter=pct(el.args[2],s,ctx), rInner=pct(el.args[3],s,ctx);
  const start=deg(el.args[4],ctx), maxSweep=deg(el.args[5],ctx);
  const midR = (rOuter+rInner)/2, thickness = rOuter-rInner;

  // Rounded rect mode
  const cornerProp = el.props['corner'];
  if (cornerProp != null) {
    const cornerPct = resolveFloat(cornerProp, ctx);
    const cr = Math.max(0, Math.min(midR, midR * cornerPct / 100));
    const totalPerim = roundedRectPerim(midR, cr);
    if (totalPerim <= 0) return;
    const normStart = ((start % 360) + 360) % 360;
    const startDist = (normStart / 360) * totalPerim;
    const sweepDist = Math.abs(maxSweep / 360) * totalPerim;
    const sweep = maxSweep * Math.max(0, Math.min(1, ctx.progress));
    const progressDist = Math.abs(sweep / 360) * totalPerim;
    c.save();
    c.globalAlpha = getAlpha(el,ctx);
    c.lineWidth = thickness;
    c.lineCap = 'butt';
    const bgColor = el.props['bg'];
    if (bgColor) {
      c.strokeStyle = resolveColor(bgColor, ctx);
      c.setLineDash([sweepDist, totalPerim]);
      c.lineDashOffset = -startDist;
      traceRoundedRectPath(c, cx, cy, midR, cr);
      c.stroke();
    }
    if (progressDist > 0.5) {
      c.strokeStyle = propColor(el,'fill',ctx, propColor(el,'color',ctx));
      c.setLineDash([progressDist, totalPerim]);
      c.lineDashOffset = -startDist;
      traceRoundedRectPath(c, cx, cy, midR, cr);
      c.stroke();
    }
    c.setLineDash([]);
    c.restore();
    return;
  }

  c.save();
  c.globalAlpha = getAlpha(el,ctx);
  c.lineWidth = thickness;
  c.lineCap = 'butt';
  const bgColor = el.props['bg'];
  if (bgColor) {
    c.strokeStyle = resolveColor(bgColor, ctx);
    c.beginPath();
    c.arc(cx, cy, midR, deg2rad(start), deg2rad(start+maxSweep), maxSweep<0);
    c.stroke();
  }
  c.strokeStyle = propColor(el,'fill',ctx, propColor(el,'color',ctx));
  const sweep = maxSweep * Math.max(0, Math.min(1, ctx.progress));
  c.beginPath();
  c.arc(cx, cy, midR, deg2rad(start), deg2rad(start+sweep), sweep<0);
  c.stroke();
  c.restore();
}

// 14. NEEDLE
function drawNeedle(c, el, ctx, s) {
  if (el.args.length < 3) return;
  const cx=pct(el.args[0],s,ctx), cy=pct(el.args[1],s,ctx), len=pct(el.args[2],s,ctx);
  const from = propFloat(el,'from',ctx,-225);
  const sweep = propFloat(el,'sweep',ctx,270);
  const tail = propPct(el,'tail',s,ctx);
  const angle = from + sweep * Math.max(0, Math.min(1, ctx.progress));
  const rad = deg2rad(angle);
  c.save();
  c.globalAlpha = getAlpha(el,ctx);
  c.strokeStyle = propColor(el,'color',ctx);
  c.lineWidth = propPct(el,'w',s,ctx, 0.01*s);
  c.lineCap = el.props['cap'] ? getCap(el) : 'round';

  // Rounded rect mode: needle tip on rounded rect perimeter
  const cornerProp = el.props['corner'];
  if (cornerProp != null) {
    const cornerPct = resolveFloat(cornerProp, ctx);
    const cr = Math.max(0, Math.min(len, len * cornerPct / 100));
    const pt = pointOnRoundedRect(cx, cy, len, cr, angle);
    const x2 = pt.x, y2 = pt.y;
    // Direction from center to tip
    const dx = x2 - cx, dy = y2 - cy;
    const d = Math.sqrt(dx*dx + dy*dy) || 1;
    const ux = dx/d, uy = dy/d;
    const x1 = tail > 0 ? cx - tail*ux : cx;
    const y1 = tail > 0 ? cy - tail*uy : cy;
    if (applyGlow(c,el,ctx,s)) {
      const gc = el.props['glow_color'];
      if (gc) c.shadowColor = resolveColor(gc, ctx);
      const savedAlpha = c.globalAlpha;
      c.globalAlpha = savedAlpha * 0.5;
      c.beginPath(); c.moveTo(x1,y1); c.lineTo(x2,y2); c.stroke();
      clearGlow(c);
      c.globalAlpha = savedAlpha;
    }
    c.beginPath(); c.moveTo(x1,y1); c.lineTo(x2,y2); c.stroke();
    const dotR = propPct(el,'dot',s,ctx);
    if (dotR > 0) {
      c.fillStyle = c.strokeStyle;
      c.beginPath(); c.arc(cx,cy,dotR,0,Math.PI*2); c.fill();
    }
    c.restore();
    return;
  }

  const x1 = tail > 0 ? cx - tail*Math.cos(rad) : cx;
  const y1 = tail > 0 ? cy - tail*Math.sin(rad) : cy;
  const x2 = cx + len*Math.cos(rad);
  const y2 = cy + len*Math.sin(rad);
  if (applyGlow(c,el,ctx,s)) {
    const gc = el.props['glow_color'];
    if (gc) c.shadowColor = resolveColor(gc, ctx);
    const savedAlpha = c.globalAlpha;
    c.globalAlpha = savedAlpha * 0.5;
    c.beginPath(); c.moveTo(x1,y1); c.lineTo(x2,y2); c.stroke();
    clearGlow(c);
    c.globalAlpha = savedAlpha;
  }
  c.beginPath(); c.moveTo(x1,y1); c.lineTo(x2,y2); c.stroke();
  const dotR = propPct(el,'dot',s,ctx);
  if (dotR > 0) {
    c.fillStyle = c.strokeStyle;
    c.beginPath(); c.arc(cx,cy,dotR,0,Math.PI*2); c.fill();
  }
  c.restore();
}

// 15. VALUE_DOT
function drawValueDot(c, el, ctx, s) {
  if (el.args.length < 6) return;
  const cx=pct(el.args[0],s,ctx), cy=pct(el.args[1],s,ctx), r=pct(el.args[2],s,ctx);
  const start=deg(el.args[3],ctx), maxSweep=deg(el.args[4],ctx);
  const dotR=pct(el.args[5],s,ctx);
  const angle = start + maxSweep * Math.max(0, Math.min(1, ctx.progress));

  // Rounded rect mode
  const cornerProp = el.props['corner'];
  if (cornerProp != null) {
    const cornerPct = resolveFloat(cornerProp, ctx);
    const cr = Math.max(0, Math.min(r, r * cornerPct / 100));
    const pt = pointOnRoundedRect(cx, cy, r, cr, angle);
    const dx = pt.x, dy = pt.y;
    c.save();
    c.globalAlpha = getAlpha(el,ctx);
    c.fillStyle = propColor(el,'fill',ctx, propColor(el,'color',ctx));
    if (applyGlow(c,el,ctx,s)) {
      c.beginPath(); c.arc(dx,dy,dotR,0,Math.PI*2); c.fill();
      clearGlow(c);
    }
    c.beginPath(); c.arc(dx,dy,dotR,0,Math.PI*2); c.fill();
    c.restore();
    return;
  }

  const rad = deg2rad(angle);
  const dx = cx + r*Math.cos(rad), dy = cy + r*Math.sin(rad);
  c.save();
  c.globalAlpha = getAlpha(el,ctx);
  c.fillStyle = propColor(el,'fill',ctx, propColor(el,'color',ctx));
  if (applyGlow(c,el,ctx,s)) {
    c.beginPath(); c.arc(dx,dy,dotR,0,Math.PI*2); c.fill();
    clearGlow(c);
  }
  c.beginPath(); c.arc(dx,dy,dotR,0,Math.PI*2); c.fill();
  c.restore();
}

// 16. ZONE_ARC
function drawZoneArc(c, el, ctx, s) {
  if (el.args.length < 6) return;
  const cx=pct(el.args[0],s,ctx), cy=pct(el.args[1],s,ctx), r=pct(el.args[2],s,ctx);
  const start=deg(el.args[3],ctx), sweep=deg(el.args[4],ctx);
  const strokeW=pct(el.args[5],s,ctx);
  const normalC = propColor(el,'normal',ctx,'#444444');
  const warningC = propColor(el,'warning',ctx,'#FFD96B');
  const dangerC = propColor(el,'danger',ctx,'#FF6B6B');

  // Rounded rect mode
  const cornerProp = el.props['corner'];
  if (cornerProp != null) {
    const cornerPct = resolveFloat(cornerProp, ctx);
    const cr = Math.max(0, Math.min(r, r * cornerPct / 100));
    c.save();
    c.globalAlpha = getAlpha(el,ctx);
    c.lineWidth = strokeW;
    c.lineCap = 'butt';
    drawZoneSegmentsRounded(c, cx, cy, r, cr, start, sweep, normalC, warningC, dangerC, ctx);
    c.restore();
    return;
  }

  c.save();
  c.globalAlpha = getAlpha(el,ctx);
  c.lineWidth = strokeW;
  c.lineCap = 'butt';
  drawZoneSegments(c, cx, cy, r, start, sweep, normalC, warningC, dangerC, ctx);
  c.restore();
}

// 17. ZONE_RING
function drawZoneRing(c, el, ctx, s) {
  if (el.args.length < 6) return;
  const cx=pct(el.args[0],s,ctx), cy=pct(el.args[1],s,ctx);
  const rOuter=pct(el.args[2],s,ctx), rInner=pct(el.args[3],s,ctx);
  const start=deg(el.args[4],ctx), sweep=deg(el.args[5],ctx);
  const normalC = propColor(el,'normal',ctx,'#444444');
  const warningC = propColor(el,'warning',ctx,'#FFD96B');
  const dangerC = propColor(el,'danger',ctx,'#FF6B6B');
  const midR = (rOuter+rInner)/2, thickness = rOuter-rInner;

  // Rounded rect mode
  const cornerProp = el.props['corner'];
  if (cornerProp != null) {
    const cornerPct = resolveFloat(cornerProp, ctx);
    const cr = Math.max(0, Math.min(midR, midR * cornerPct / 100));
    c.save();
    c.globalAlpha = getAlpha(el,ctx);
    c.lineWidth = thickness;
    c.lineCap = 'butt';
    drawZoneSegmentsRounded(c, cx, cy, midR, cr, start, sweep, normalC, warningC, dangerC, ctx);
    c.restore();
    return;
  }

  c.save();
  c.globalAlpha = getAlpha(el,ctx);
  c.lineWidth = thickness;
  c.lineCap = 'butt';
  drawZoneSegments(c, cx, cy, midR, start, sweep, normalC, warningC, dangerC, ctx);
  c.restore();
}

function drawZoneSegments(c, cx, cy, r, start, sweep, normalC, warningC, dangerC, ctx) {
  const wl = ctx.warnLowProgress, wh = ctx.warnHighProgress;
  const dl = ctx.dangerLowProgress, dh = ctx.dangerHighProgress;
  const segments = [];
  let cursor = 0;
  if (dl != null && dl > cursor) { segments.push({from:cursor,to:dl,color:dangerC}); cursor=dl; }
  if (wl != null && wl > cursor) { segments.push({from:cursor,to:wl,color:warningC}); cursor=wl; }
  const highStart = wh ?? dh ?? 1;
  if (highStart > cursor) { segments.push({from:cursor,to:highStart,color:normalC}); cursor=highStart; }
  if (wh != null && dh != null && dh > wh) { segments.push({from:cursor,to:dh,color:warningC}); cursor=dh; }
  if (cursor < 1) { segments.push({from:cursor,to:1,color: dh!=null?dangerC : wh!=null?warningC : normalC}); }
  if (segments.length === 0) {
    c.strokeStyle = normalC;
    c.beginPath(); c.arc(cx,cy,r,deg2rad(start),deg2rad(start+sweep)); c.stroke();
    return;
  }
  for (const seg of segments) {
    c.strokeStyle = seg.color;
    c.beginPath();
    const sa = start + sweep * seg.from;
    const sw = sweep * (seg.to - seg.from);
    c.arc(cx, cy, r, deg2rad(sa), deg2rad(sa + sw), sw < 0);
    c.stroke();
  }
}

function drawZoneSegmentsRounded(c, cx, cy, r, cr, start, sweep, normalC, warningC, dangerC, ctx) {
  const wl = ctx.warnLowProgress, wh = ctx.warnHighProgress;
  const dl = ctx.dangerLowProgress, dh = ctx.dangerHighProgress;
  const segments = [];
  let cursor = 0;
  if (dl != null && dl > cursor) { segments.push({from:cursor,to:dl,color:dangerC}); cursor=dl; }
  if (wl != null && wl > cursor) { segments.push({from:cursor,to:wl,color:warningC}); cursor=wl; }
  const highStart = wh ?? dh ?? 1;
  if (highStart > cursor) { segments.push({from:cursor,to:highStart,color:normalC}); cursor=highStart; }
  if (wh != null && dh != null && dh > wh) { segments.push({from:cursor,to:dh,color:warningC}); cursor=dh; }
  if (cursor < 1) { segments.push({from:cursor,to:1,color: dh!=null?dangerC : wh!=null?warningC : normalC}); }

  const totalPerim = roundedRectPerim(r, cr);
  if (totalPerim <= 0) return;

  if (segments.length === 0) {
    c.strokeStyle = normalC;
    const normStart = ((start % 360) + 360) % 360;
    const startDist = (normStart / 360) * totalPerim;
    const sweepDist = Math.abs(sweep / 360) * totalPerim;
    c.setLineDash([sweepDist, totalPerim]);
    c.lineDashOffset = -startDist;
    traceRoundedRectPath(c, cx, cy, r, cr);
    c.stroke();
    c.setLineDash([]);
    return;
  }
  for (const seg of segments) {
    c.strokeStyle = seg.color;
    const sa = start + sweep * seg.from;
    const sw = sweep * (seg.to - seg.from);
    const normSa = ((sa % 360) + 360) % 360;
    const segStartDist = (normSa / 360) * totalPerim;
    const segLen = Math.abs(sw / 360) * totalPerim;
    c.setLineDash([segLen, totalPerim]);
    c.lineDashOffset = -segStartDist;
    traceRoundedRectPath(c, cx, cy, r, cr);
    c.stroke();
  }
  c.setLineDash([]);
}

// 18. SWEEP_TICKS
function drawSweepTicks(c, el, ctx, s) {
  if (el.args.length < 5) return;
  const cx=pct(el.args[0],s,ctx), cy=pct(el.args[1],s,ctx), r=pct(el.args[2],s,ctx);
  const count = parseInt(el.args[3]) || 0;
  const len = pct(el.args[4],s,ctx);
  const startAngle = propFloat(el,'start',ctx,-225);
  const sweepAngle = propFloat(el,'sweep',ctx,270);
  const onColor = propColor(el,'on',ctx, propColor(el,'color',ctx));
  const offColor = propColor(el,'off',ctx,'#333333');
  if (count < 2) return;
  const activeCount = Math.floor(count * Math.max(0, Math.min(1, ctx.progress)));
  c.save();
  c.globalAlpha = getAlpha(el,ctx);
  c.lineWidth = propPct(el,'w',s,ctx, 0.008*s);
  c.lineCap = getCap(el);

  // Rounded rect mode
  const cornerProp = el.props['corner'];
  if (cornerProp != null) {
    const cornerPct = resolveFloat(cornerProp, ctx);
    const cr = Math.max(0, Math.min(r, r * cornerPct / 100));
    for (let i = 0; i < count; i++) {
      c.strokeStyle = i < activeCount ? onColor : offColor;
      const angle = startAngle + sweepAngle * i / (count - 1);
      const pt = pointOnRoundedRect(cx, cy, r, cr, angle);
      c.beginPath();
      c.moveTo(pt.x + pt.nx * len, pt.y + pt.ny * len);
      c.lineTo(pt.x, pt.y);
      c.stroke();
    }
    c.restore();
    return;
  }

  for (let i = 0; i < count; i++) {
    c.strokeStyle = i < activeCount ? onColor : offColor;
    const angle = startAngle + sweepAngle * i / (count - 1);
    const rad = deg2rad(angle);
    c.beginPath();
    c.moveTo(cx+(r-len)*Math.cos(rad), cy+(r-len)*Math.sin(rad));
    c.lineTo(cx+r*Math.cos(rad), cy+r*Math.sin(rad));
    c.stroke();
  }
  c.restore();
}

// 19. DIGITAL_BAR
function drawDigitalBar(c, el, ctx, s) {
  if (el.args.length < 6) return;
  const x=pct(el.args[0],s,ctx), y=pct(el.args[1],s,ctx);
  const totalW=pct(el.args[2],s,ctx), h=pct(el.args[3],s,ctx);
  const count = parseInt(el.args[4]) || 0;
  const gap = pct(el.args[5],s,ctx);
  const corner = propPct(el,'corner',s,ctx);
  const fillColor = propColor(el,'fill',ctx, propColor(el,'color',ctx));
  const bgColor = propColor(el,'bg',ctx,'#222222');
  const segW = (totalW - gap*(count-1)) / count;
  const activeCount = Math.floor(count * Math.max(0, Math.min(1, ctx.progress)));
  c.save();
  c.globalAlpha = getAlpha(el,ctx);
  for (let i = 0; i < count; i++) {
    c.fillStyle = i < activeCount ? fillColor : bgColor;
    const sx = x + i*(segW+gap);
    if (corner > 0) { roundRect(c, sx, y, segW, h, corner); c.fill(); }
    else c.fillRect(sx, y, segW, h);
  }
  c.restore();
}
