// SQSEO — reusable chart-primitive kit.
// Pure SVG, monochrome + --viz-* palette, animated draw-in, reduced-motion aware,
// tasteful tooltips. All register on window.LTQ. House style matches charts.jsx.
(function init(){
if(!window.LongtailIQDesignSystem_ae8f12){return setTimeout(init,30);}
const React = window.React;
const t = (k, v) => (window.LTQ && window.LTQ.t ? window.LTQ.t(k, v) : k);

const RM = () => typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const uidGen = (p) => p + Math.random().toString(36).slice(2, 7);
// Categorical series palette (from the design tokens' data-viz ramp).
const VIZ = ["var(--viz-blue)", "var(--viz-violet)", "var(--viz-cyan)", "var(--viz-green)", "var(--viz-amber)", "var(--viz-red)"];
const pick = (i, color) => color || VIZ[i % VIZ.length];
const fmtC = (n) => (window.LTQ && window.LTQ.fmtCompact ? window.LTQ.fmtCompact(n) : String(Math.round(n)));

// useDrawn — flips false→true one frame after mount to trigger CSS transitions
// (respects reduced motion by starting already-drawn).
function useDrawn(deps) {
  const reduce = React.useMemo(RM, []);
  const [drawn, setDrawn] = React.useState(reduce);
  React.useEffect(() => { if (reduce) return; setDrawn(false); const t = setTimeout(() => setDrawn(true), 40); return () => clearTimeout(t); }, deps || []);
  return drawn;
}

// ── Sparkline ────────────────────────────────────────────────────────────────
// Tiny trend line + soft area. points: number[]. The y scale's hi/lo always ride
// on the right (owner rule 2026-07-17: no chart without axes); pass a `span`
// [x0, x1] to print the period underneath.
function Sparkline({ points, color = "var(--viz-blue)", width = 120, height = 32, strokeWidth = 1.75, area = true, dot = true, span = null, format = fmtC }) {
  const pts = (points || []).map(Number).filter((v) => isFinite(v));
  const uid = React.useRef(uidGen("sp")).current;
  const drawn = useDrawn([pts.length]);
  if (pts.length < 2) return <svg width={width} height={height} />;
  const pad = 2, n = pts.length;
  const max = Math.max(...pts), min = Math.min(...pts), rng = (max - min) || 1;
  const x = (i) => pad + (i / (n - 1)) * (width - pad * 2);
  const y = (v) => pad + (1 - (v - min) / rng) * (height - pad * 2);
  const d = pts.map((v, i) => `${i ? "L" : "M"}${x(i).toFixed(1)} ${y(v).toFixed(1)}`).join(" ");
  const areaD = d + ` L ${x(n - 1).toFixed(1)} ${height - pad} L ${x(0).toFixed(1)} ${height - pad} Z`;
  const axl = { fontSize: 8.5, fontFamily: "var(--font-mono)", color: "var(--text-faint)", lineHeight: 1 };
  return (
    <span style={{ display: "inline-block" }}>
      <span style={{ display: "inline-grid", gridTemplateColumns: "auto auto", gap: 5, alignItems: "stretch" }}>
        <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} style={{ display: "block", overflow: "visible" }}>
          <defs><linearGradient id={uid} x1="0" y1="0" x2="0" y2="1"><stop offset="0" stopColor={color} stopOpacity="0.20" /><stop offset="1" stopColor={color} stopOpacity="0" /></linearGradient></defs>
          {area && <path d={areaD} fill={`url(#${uid})`} style={{ opacity: drawn ? 1 : 0, transition: "opacity .6s ease .3s" }} />}
          <path d={d} fill="none" stroke={color} strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round"
            style={{ strokeDasharray: 600, strokeDashoffset: drawn ? 0 : 600, transition: "stroke-dashoffset .9s var(--ease-out)" }} />
          {dot && <circle cx={x(n - 1)} cy={y(pts[n - 1])} r="2.4" fill={color} style={{ opacity: drawn ? 1 : 0, transition: "opacity .3s ease .8s" }} />}
        </svg>
        <span style={{ display: "flex", flexDirection: "column", justifyContent: "space-between", textAlign: "left", padding: "1px 0" }}>
          <span className="lt-num" style={axl}>{format(max)}</span><span className="lt-num" style={axl}>{format(min)}</span>
        </span>
      </span>
      {span && <span style={{ display: "flex", justifyContent: "space-between", marginTop: 2 }}><span className="lt-num" style={axl}>{span[0]}</span><span className="lt-num" style={axl}>{span[1]}</span></span>}
    </span>
  );
}

// ── RankedBars ───────────────────────────────────────────────────────────────
// Horizontal ranked bars. items:[{label, value, sub?, color?, href?, onClick?}].
function RankedBars({ items = [], max, format = fmtC, barHeight = 26, gap = 10, showValue = true, accent }) {
  const drawn = useDrawn([items.length]);
  const hi = max || Math.max(1, ...items.map((it) => Math.abs(Number(it.value) || 0)));
  return (
    <div style={{ display: "flex", flexDirection: "column", gap }}>
      {items.map((it, i) => {
        const v = Number(it.value) || 0, w = Math.max(0, Math.min(1, Math.abs(v) / hi)) * 100;
        const col = pick(i, it.color || accent);
        const Row = it.href ? "a" : "div";
        return (
          <Row key={i} href={it.href} onClick={it.onClick} style={{ display: "grid", gridTemplateColumns: "1fr auto", gap: 4, textDecoration: "none", color: "inherit", cursor: it.href || it.onClick ? "pointer" : "default" }}>
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 10, gridColumn: "1 / -1" }}>
              <span style={{ fontSize: 12.5, color: "var(--text-body)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{it.label}</span>
              {showValue && <span className="lt-num" style={{ fontFamily: "var(--font-mono)", fontSize: 12.5, fontWeight: 700, color: "var(--text-strong)", flex: "none" }}>{format(v)}{it.suffix || ""}</span>}
            </div>
            <div style={{ gridColumn: "1 / -1", height: 8, borderRadius: 999, background: "var(--ink-100)", overflow: "hidden", marginTop: 1 }}>
              <div style={{ height: "100%", width: (drawn ? w : 0) + "%", background: col, borderRadius: 999, transition: `width .9s var(--ease-out) ${i * 60}ms` }} title={it.sub || ""} />
            </div>
          </Row>
        );
      })}
    </div>
  );
}

// ── StackedBar ───────────────────────────────────────────────────────────────
// One 100%-proportional horizontal bar. segments:[{label, value, color}].
function StackedBar({ segments = [], height = 14, radius = 999, legend = true, format = fmtC }) {
  const drawn = useDrawn([segments.length]);
  const total = segments.reduce((s, x) => s + (Number(x.value) || 0), 0) || 1;
  return (
    <div>
      <div style={{ display: "flex", height, borderRadius: radius, overflow: "hidden", background: "var(--ink-100)" }}>
        {segments.map((s, i) => {
          const w = ((Number(s.value) || 0) / total) * 100, col = pick(i, s.color);
          return <div key={i} title={`${s.label}: ${format(s.value)} (${w.toFixed(1)}%)`}
            style={{ width: (drawn ? w : 0) + "%", background: col, transition: `width .9s var(--ease-out) ${i * 80}ms`, borderRight: i < segments.length - 1 ? "1.5px solid var(--paper)" : "none" }} />;
        })}
      </div>
      {legend && (
        <div style={{ display: "flex", flexWrap: "wrap", gap: "6px 16px", marginTop: 12 }}>
          {segments.map((s, i) => {
            const w = ((Number(s.value) || 0) / total) * 100;
            return (
              <div key={i} style={{ display: "flex", alignItems: "center", gap: 7, fontSize: 12 }}>
                <span style={{ width: 9, height: 9, borderRadius: 3, background: pick(i, s.color), flex: "none" }} />
                <span style={{ color: "var(--text-muted)" }}>{s.label}</span>
                <span className="lt-num" style={{ fontFamily: "var(--font-mono)", fontWeight: 700, color: "var(--text-strong)" }}>{w.toFixed(0)}%</span>
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

// ── Donut / Ring ─────────────────────────────────────────────────────────────
// segments:[{label, value, color}]. Center shows top/sub. Interactive legend.
function Donut({ segments = [], size = 168, thickness = 22, centerTop, centerSub, legend = true, format = fmtC }) {
  const drawn = useDrawn([segments.length]);
  const [hi, setHi] = React.useState(null);
  const total = segments.reduce((s, x) => s + (Number(x.value) || 0), 0) || 1;
  const r = (size - thickness) / 2, cx = size / 2, cy = size / 2, C = 2 * Math.PI * r;
  let acc = 0;
  const arcs = segments.map((s, i) => {
    const frac = (Number(s.value) || 0) / total, len = frac * C, off = acc * C; acc += frac;
    return { s, i, frac, len, off, col: pick(i, s.color) };
  });
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 22, flexWrap: "wrap" }}>
      <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{ flex: "none" }}>
        <circle cx={cx} cy={cy} r={r} fill="none" stroke="var(--ink-100)" strokeWidth={thickness} />
        <g transform={`rotate(-90 ${cx} ${cy})`}>
          {arcs.map((a) => (
            <circle key={a.i} cx={cx} cy={cy} r={r} fill="none" stroke={a.col} strokeWidth={hi === a.i ? thickness + 3 : thickness}
              strokeDasharray={`${a.len} ${C - a.len}`} strokeDashoffset={drawn ? -a.off : -C} strokeLinecap="butt"
              onMouseEnter={() => setHi(a.i)} onMouseLeave={() => setHi(null)}
              style={{ transition: `stroke-dashoffset 1s var(--ease-out) ${a.i * 90}ms, stroke-width .15s ease`, cursor: "default", opacity: hi == null || hi === a.i ? 1 : 0.4 }}>
              <title>{`${a.s.label}: ${format(a.s.value)} (${(a.frac * 100).toFixed(1)}%)`}</title>
            </circle>
          ))}
        </g>
        <text x={cx} y={cy - 2} textAnchor="middle" style={{ fontFamily: "var(--font-mono)", fontWeight: 700, fontSize: 24, fill: "var(--text-strong)" }}>
          {hi != null ? `${(arcs[hi].frac * 100).toFixed(0)}%` : (centerTop != null ? centerTop : segments.length)}
        </text>
        <text x={cx} y={cy + 18} textAnchor="middle" style={{ fontSize: 11, fill: "var(--text-muted)" }}>
          {hi != null ? segments[hi].label : (centerSub || t("charts.segments"))}
        </text>
      </svg>
      {legend && (
        <div style={{ display: "flex", flexDirection: "column", gap: 8, minWidth: 140 }}>
          {arcs.map((a) => (
            <div key={a.i} onMouseEnter={() => setHi(a.i)} onMouseLeave={() => setHi(null)}
              style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 12.5, opacity: hi == null || hi === a.i ? 1 : 0.5, transition: "opacity .15s" }}>
              <span style={{ width: 9, height: 9, borderRadius: 3, background: a.col, flex: "none" }} />
              <span style={{ color: "var(--text-body)", flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{a.s.label}</span>
              <span className="lt-num" style={{ fontFamily: "var(--font-mono)", fontWeight: 700, color: "var(--text-strong)" }}>{(a.frac * 100).toFixed(0)}%</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ── Gauge ────────────────────────────────────────────────────────────────────
// 270° arc gauge. value/max, big center read-out. Generalizes AiGauge.
function Gauge({ value = 0, max = 100, size = 150, thickness = 12, color = "var(--viz-green)", label, format = (v) => Math.round(v) }) {
  const drawn = useDrawn([value]);
  const frac = Math.max(0, Math.min(1, (Number(value) || 0) / (max || 1)));
  const START = 135, SWEEP = 270; // degrees; open at the bottom
  const r = (size - thickness) / 2, cx = size / 2, cy = size / 2;
  const polar = (deg) => { const a = (deg * Math.PI) / 180; return [cx + r * Math.cos(a), cy + r * Math.sin(a)]; };
  const arcPath = (fromDeg, toDeg) => {
    const [x1, y1] = polar(fromDeg), [x2, y2] = polar(toDeg);
    const large = Math.abs(toDeg - fromDeg) > 180 ? 1 : 0;
    return `M ${x1.toFixed(1)} ${y1.toFixed(1)} A ${r} ${r} 0 ${large} 1 ${x2.toFixed(1)} ${y2.toFixed(1)}`;
  };
  const end = START + SWEEP * (drawn ? frac : 0);
  const uid = React.useRef(uidGen("ga")).current;
  return (
    <div style={{ position: "relative", width: size, height: size }}>
      <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
        <defs><filter id={uid} x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="2.5" /></filter></defs>
        <path d={arcPath(START, START + SWEEP)} fill="none" stroke="var(--ink-100)" strokeWidth={thickness} strokeLinecap="round" />
        <path d={arcPath(START, end)} fill="none" stroke={color} strokeWidth={thickness} strokeLinecap="round"
          style={{ transition: "d 1s var(--ease-out)" }} opacity="0.35" filter={`url(#${uid})`} />
        <path d={arcPath(START, end)} fill="none" stroke={color} strokeWidth={thickness} strokeLinecap="round"
          style={{ transition: "d 1s var(--ease-out)" }} />
      </svg>
      <div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center", textAlign: "center" }}>
        <div>
          <div className="lt-num" style={{ fontFamily: "var(--font-mono)", fontWeight: 700, fontSize: size * 0.24, lineHeight: 1, color: "var(--text-strong)" }}>{format(value)}</div>
          {label && <div style={{ fontSize: 11, color: "var(--text-muted)", marginTop: 4 }}>{label}</div>}
        </div>
      </div>
    </div>
  );
}

// ── Scatter ──────────────────────────────────────────────────────────────────
// points:[{x, y, label?, color?, r?}]. Optional reference line (fn or points),
// axis labels, quadrant guides. Hover tooltip.
function Scatter({ points = [], width = 720, height = 300, xLabel, yLabel, xDomain, yDomain, refLine, refLabel, invertY = false, format = fmtC, xFormat }) {
  const drawn = useDrawn([points.length]);
  const [hi, setHi] = React.useState(null);
  const padL = 44, padR = 14, padT = 14, padB = 34;
  const xs = points.map((p) => Number(p.x) || 0), ys = points.map((p) => Number(p.y) || 0);
  const xdom = xDomain || [Math.min(0, ...xs), Math.max(1, ...xs)];
  const ydom = yDomain || [Math.min(0, ...ys), Math.max(1, ...ys)];
  const X = (v) => padL + ((v - xdom[0]) / ((xdom[1] - xdom[0]) || 1)) * (width - padL - padR);
  const Y = (v) => { const t = (v - ydom[0]) / ((ydom[1] - ydom[0]) || 1); return padT + (invertY ? t : 1 - t) * (height - padT - padB); };
  const xf = xFormat || format;
  const gridY = [0, 0.25, 0.5, 0.75, 1];
  let refD = null;
  if (typeof refLine === "function") {
    const N = 40; const seg = [];
    for (let i = 0; i <= N; i++) { const xv = xdom[0] + (i / N) * (xdom[1] - xdom[0]); seg.push(`${i ? "L" : "M"}${X(xv).toFixed(1)} ${Y(refLine(xv)).toFixed(1)}`); }
    refD = seg.join(" ");
  } else if (Array.isArray(refLine)) {
    refD = refLine.map((p, i) => `${i ? "L" : "M"}${X(p.x).toFixed(1)} ${Y(p.y).toFixed(1)}`).join(" ");
  }
  return (
    <div style={{ position: "relative", width: "100%" }}>
      <svg viewBox={`0 0 ${width} ${height}`} style={{ width: "100%", height, display: "block", overflow: "visible" }}>
        {gridY.map((g, i) => { const yy = padT + g * (height - padT - padB); const val = ydom[invertY ? 0 : 1] + (invertY ? 1 : -1) * g * (ydom[1] - ydom[0]); return (
          <g key={i}><line x1={padL} x2={width - padR} y1={yy} y2={yy} stroke="var(--border-subtle)" strokeWidth="1" opacity={i === 0 || i === 4 ? 0.9 : 0.5} />
          <text x={padL - 8} y={yy + 3} textAnchor="end" fontSize="10" fontFamily="var(--font-mono)" fill="var(--text-faint)">{format(val)}</text></g>
        ); })}
        {refD && <path d={refD} fill="none" stroke="var(--ink-400)" strokeWidth="1.6" strokeDasharray="5 4" style={{ opacity: drawn ? 0.8 : 0, transition: "opacity .6s ease .4s" }} />}
        {points.map((p, i) => (
          <circle key={i} cx={X(Number(p.x) || 0)} cy={Y(Number(p.y) || 0)} r={hi === i ? (p.r || 5) + 2 : (p.r || 5)}
            fill={p.color || "var(--viz-blue)"} fillOpacity={hi == null || hi === i ? 0.85 : 0.4} stroke="var(--paper)" strokeWidth="1.5"
            onMouseEnter={() => setHi(i)} onMouseLeave={() => setHi(null)}
            style={{ transition: `opacity .3s ease, r .12s ease`, opacity: drawn ? 1 : 0, transitionDelay: `${Math.min(600, i * 8)}ms`, cursor: "pointer" }} />
        ))}
        {/* numeric x ticks at start / middle / end of the domain */}
        {[0, 0.5, 1].map((g, i) => (
          <text key={"xt" + i} x={X(xdom[0] + g * (xdom[1] - xdom[0]))} y={height - padB + 14} textAnchor={i === 0 ? "start" : i === 2 ? "end" : "middle"} fontSize="10" fontFamily="var(--font-mono)" fill="var(--text-faint)">{xf(xdom[0] + g * (xdom[1] - xdom[0]))}</text>
        ))}
        {xLabel && <text x={(padL + width - padR) / 2} y={height - 4} textAnchor="middle" fontSize="10.5" fill="var(--text-muted)">{xLabel}</text>}
        {yLabel && <text transform={`rotate(-90 12 ${(padT + height - padB) / 2})`} x={12} y={(padT + height - padB) / 2} textAnchor="middle" fontSize="10.5" fill="var(--text-muted)">{yLabel}</text>}
        {refLabel && refD && <text x={width - padR} y={padT + 10} textAnchor="end" fontSize="10" fontFamily="var(--font-mono)" fill="var(--text-faint)">{refLabel}</text>}
      </svg>
      {hi != null && points[hi] && (
        <div style={{ position: "absolute", left: `${(X(points[hi].x) / width) * 100}%`, top: Y(points[hi].y) - 6, transform: "translate(-50%,-100%)",
          background: "var(--ink-900)", color: "#fff", borderRadius: 9, padding: "7px 10px", pointerEvents: "none", boxShadow: "var(--shadow-lg)", whiteSpace: "nowrap", zIndex: 5 }}>
          {points[hi].label && <div style={{ fontSize: 11, color: "rgba(255,255,255,0.7)", marginBottom: 2, maxWidth: 220, overflow: "hidden", textOverflow: "ellipsis" }}>{points[hi].label}</div>}
          <div className="lt-num" style={{ fontFamily: "var(--font-mono)", fontSize: 12, fontWeight: 700 }}>{xLabel ? xLabel + ": " : ""}{xf(points[hi].x)} · {yLabel ? yLabel + ": " : ""}{format(points[hi].y)}</div>
        </div>
      )}
    </div>
  );
}

// ── Histogram ────────────────────────────────────────────────────────────────
// Vertical bars. bins:[{label, value, color?}].
function Histogram({ bins = [], height = 180, color = "var(--viz-blue)", format = fmtC }) {
  const drawn = useDrawn([bins.length]);
  const hi = Math.max(1, ...bins.map((b) => Number(b.value) || 0));
  return (
    <div style={{ display: "flex", alignItems: "flex-end", gap: 8, height, paddingTop: 18 }}>
      {bins.map((b, i) => {
        const h = (Math.max(0, Number(b.value) || 0) / hi) * 100;
        return (
          <div key={i} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", height: "100%", justifyContent: "flex-end" }} title={`${b.label}: ${format(b.value)}`}>
            <span className="lt-num" style={{ fontFamily: "var(--font-mono)", fontSize: 11, fontWeight: 700, color: "var(--text-strong)", marginBottom: 4, opacity: drawn ? 1 : 0, transition: "opacity .4s ease .5s" }}>{format(b.value)}</span>
            <div style={{ width: "100%", maxWidth: 56, height: (drawn ? h : 0) + "%", minHeight: 2, background: b.color || color, borderRadius: "5px 5px 2px 2px", transition: `height .8s var(--ease-out) ${i * 60}ms` }} />
            <span style={{ fontSize: 10.5, color: "var(--text-muted)", marginTop: 6, textAlign: "center", lineHeight: 1.2 }}>{b.label}</span>
          </div>
        );
      })}
    </div>
  );
}

// ── CalendarHeatmap ──────────────────────────────────────────────────────────
// GitHub-style. days:[{date:'YYYY-MM-DD', value}]. Columns = ISO weeks (Mon top).
function CalendarHeatmap({ days = [], weeks = 26, cell = 13, gap = 3, color = "var(--viz-green)", emptyLabel = "publishing" }) {
  const byDate = React.useMemo(() => { const m = new Map(); (days || []).forEach((d) => m.set(d.date, (m.get(d.date) || 0) + (Number(d.value) || 0))); return m; }, [days]);
  const maxV = Math.max(1, ...Array.from(byDate.values()));
  const today = new Date(); today.setHours(0, 0, 0, 0);
  const dow = (today.getDay() + 6) % 7; // Mon=0
  const end = new Date(today); end.setDate(end.getDate() + (6 - dow)); // end of this ISO week
  const totalDays = weeks * 7;
  const cells = [];
  for (let i = totalDays - 1; i >= 0; i--) { const d = new Date(end); d.setDate(end.getDate() - i); cells.push(d); }
  const iso = (d) => d.toISOString().slice(0, 10);
  const shade = (v) => { if (!v) return "var(--ink-100)"; const t = 0.2 + 0.8 * Math.min(1, v / maxV); return `color-mix(in srgb, ${color} ${Math.round(t * 100)}%, var(--paper))`; };
  const cols = weeks, W = cols * (cell + gap), H = 7 * (cell + gap);
  const monthTicks = [];
  for (let w = 0; w < cols; w++) { const d = cells[w * 7]; if (d && d.getDate() <= 7) monthTicks.push({ w, m: window.LTQ.fmtDate ? window.LTQ.fmtDate(d, { month: "short" }) : d.toLocaleString("en", { month: "short" }) }); }
  return (
    <div style={{ overflowX: "auto" }} className="lt-scroll">
      <svg width={W + 30} height={H + 18} viewBox={`0 0 ${W + 30} ${H + 18}`} style={{ display: "block" }}>
        {monthTicks.map((t, i) => <text key={i} x={t.w * (cell + gap)} y={9} fontSize="9.5" fontFamily="var(--font-mono)" fill="var(--text-faint)">{t.m}</text>)}
        {["Mon", "", "Wed", "", "Fri", "", ""].map((lbl, r) => lbl ? <text key={r} x={W + 4} y={16 + r * (cell + gap) + cell} fontSize="9" fill="var(--text-faint)">{lbl}</text> : null)}
        {cells.map((d, i) => { const col = Math.floor(i / 7), row = i % 7; const v = byDate.get(iso(d)) || 0; const future = d > today;
          return <rect key={i} x={col * (cell + gap)} y={14 + row * (cell + gap)} width={cell} height={cell} rx="3"
            fill={future ? "transparent" : shade(v)} stroke={future ? "transparent" : "var(--border-subtle)"} strokeWidth="0.5">
            <title>{`${iso(d)} — ${v ? v + " " + emptyLabel : "none"}`}</title></rect>;
        })}
      </svg>
    </div>
  );
}

// ── BumpChart — rank / position over time (lower = better, drawn higher) ───────
// points:[{date, rank}]. maxRank clamps the axis. Inverted y so #1 sits at top.
function BumpChart({ points = [], height = 130, color = "var(--viz-violet)", maxRank, goodBelow }) {
  const drawn = useDrawn([points.length]);
  const [hi, setHi] = React.useState(null);
  const W = 620, H = height, padL = 8, padR = 8, padT = 16, padB = 22;
  const rows = (points || []).filter((p) => p && isFinite(Number(p.rank)));
  if (rows.length < 2) return <div style={{ height, display: "grid", placeItems: "center", color: "var(--text-faint)", fontSize: 12 }}>{t("charts.no_history")}</div>;
  const n = rows.length;
  const ranks = rows.map((p) => Number(p.rank));
  const top = 1, bottom = maxRank || Math.max(10, Math.ceil(Math.max(...ranks) + 1));
  const x = (i) => padL + (i / (n - 1)) * (W - padL - padR);
  const y = (r) => padT + ((Math.min(bottom, Math.max(top, r)) - top) / ((bottom - top) || 1)) * (H - padT - padB);
  const d = ranks.map((r, i) => `${i ? "L" : "M"}${x(i).toFixed(1)} ${y(r).toFixed(1)}`).join(" ");
  const ix = hi == null ? n - 1 : hi;
  return (
    <div style={{ position: "relative", width: "100%" }} onMouseLeave={() => setHi(null)}
      onMouseMove={(e) => { const r = e.currentTarget.getBoundingClientRect(); setHi(Math.max(0, Math.min(n - 1, Math.round(((e.clientX - r.left) / r.width) * (n - 1))))); }}>
      <svg viewBox={`0 0 ${W} ${H}`} style={{ width: "100%", height: H, display: "block", overflow: "visible" }}>
        {goodBelow != null && goodBelow <= bottom && (
          <g><rect x={padL} y={padT} width={W - padL - padR} height={y(goodBelow) - padT} fill="var(--viz-green)" opacity="0.06" />
          <line x1={padL} x2={W - padR} y1={y(goodBelow)} y2={y(goodBelow)} stroke="var(--viz-green)" strokeWidth="1" strokeDasharray="3 3" opacity="0.5" /></g>
        )}
        {[top, Math.round((top + bottom) / 2), bottom].map((r, i) => (
          <text key={i} x={padL} y={y(r) - 2} fontSize="9.5" fontFamily="var(--font-mono)" fill="var(--text-faint)">#{r}</text>
        ))}
        {/* x axis: first and last dates of the rank history */}
        <text x={padL} y={H - 6} textAnchor="start" fontSize="9.5" fontFamily="var(--font-mono)" fill="var(--text-faint)">{String(rows[0].date || "").slice(5)}</text>
        <text x={W - padR} y={H - 6} textAnchor="end" fontSize="9.5" fontFamily="var(--font-mono)" fill="var(--text-faint)">{String(rows[n - 1].date || "").slice(5)}</text>
        <path d={d} fill="none" stroke={color} strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"
          style={{ strokeDasharray: 2000, strokeDashoffset: drawn ? 0 : 2000, transition: "stroke-dashoffset 1.1s var(--ease-out)" }} />
        <circle cx={x(ix)} cy={y(ranks[ix])} r="4" fill={color} stroke="var(--paper)" strokeWidth="2" style={{ opacity: drawn ? 1 : 0 }} />
      </svg>
      {hi != null && rows[ix] && (
        <div style={{ position: "absolute", left: `${(x(ix) / W) * 100}%`, top: 2, transform: "translateX(-50%)", background: "var(--ink-900)", color: "#fff", borderRadius: 8, padding: "6px 9px", pointerEvents: "none", boxShadow: "var(--shadow-lg)", whiteSpace: "nowrap" }}>
          <span style={{ fontSize: 10.5, color: "rgba(255,255,255,0.6)" }}>{rows[ix].date} · </span>
          <span className="lt-num" style={{ fontFamily: "var(--font-mono)", fontWeight: 700 }}>#{ranks[ix].toFixed(1)}</span>
        </div>
      )}
    </div>
  );
}

// ── Funnel — narrowing centered bars with step conversion (Phase 12) ───────────
// stages:[{label, value, color?, sub?}]. Width ∝ value; "↓ N%" between stages.
function Funnel({ stages = [], barHeight = 46, gap = 4, color = "var(--viz-blue)", format = fmtC }) {
  const drawn = useDrawn([stages.length]);
  const max = Math.max(1, ...stages.map((s) => Math.abs(Number(s.value) || 0)));
  return (
    <div style={{ display: "flex", flexDirection: "column", gap }}>
      {stages.map((s, i) => {
        const v = Number(s.value) || 0;
        const w = Math.max(9, (Math.abs(v) / max) * 100);
        const next = i < stages.length - 1 ? (Number(stages[i + 1].value) || 0) : null;
        const conv = next != null && v ? Math.round((next / v) * 100) : null;
        const col = s.color || color;
        return (
          <div key={i} style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
            <div style={{ width: (drawn ? w : 8) + "%", minWidth: 108, height: barHeight, borderRadius: 9, transition: `width .85s var(--ease-out) ${i * 90}ms`,
              background: `linear-gradient(180deg, ${col}, color-mix(in srgb, ${col} 82%, black))`, boxShadow: "var(--shadow-sm)",
              display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", color: "#fff", lineHeight: 1.15 }}>
              <span className="lt-num" style={{ fontFamily: "var(--font-mono)", fontWeight: 700, fontSize: 15 }}>{format(v)}</span>
              <span style={{ fontSize: 10.5, opacity: 0.9 }}>{s.label}</span>
            </div>
            {conv != null && <span style={{ fontSize: 10.5, color: "var(--text-faint)", fontFamily: "var(--font-mono)", margin: "3px 0 0" }}>↓ {conv}%</span>}
          </div>
        );
      })}
    </div>
  );
}

window.LTQ = window.LTQ || {};
Object.assign(window.LTQ, { Sparkline, RankedBars, StackedBar, Donut, Gauge, Scatter, Histogram, CalendarHeatmap, BumpChart, Funnel, VIZ_PALETTE: VIZ });
})();
