/* global React, window */
/* =========================================================================
   Production chart components, ported.

   Faithful ports of MakerSightsClient's own chart components (origin/main
   c728bf9c88), for a static Babel page that cannot import the TS/Mantine
   originals. Same structure, geometry, type, tokens and colors, read from
   source; see the mapping table in Design Decisions.md (2026-10-23). Each
   component names the production file it ports.

   Shared conventions taken from the production theme:
     spacing getSpacing(n) = 4n px; font sizes sm 12 / md 14 / lg 16;
     Text md 14/24, sm 12/18, weight 400 (600 bold); icons Tabler 20px.
   ========================================================================= */
const { useState: msS, useRef: msR, useEffect: msE, useLayoutEffect: msLE } = React;

/* Container width, for the charts production sizes with visx ParentSize. */
function useMsWidth(ref, fallback) {
  const [w, setW] = msS(fallback || 720);
  msLE(() => {
    const el = ref.current;
    if (!el) return undefined;
    const read = () => setW(el.clientWidth || fallback || 720);
    read();
    if (typeof ResizeObserver === "undefined") return undefined;
    const ro = new ResizeObserver(read);
    ro.observe(el);
    return () => ro.disconnect();
  }, []);
  return w;
}

/* d3-scale's tickIncrement / nice / ticks, which visx's scaleLinear uses. */
function msTickIncrement(start, stop, count) {
  const step = (stop - start) / Math.max(0, count);
  const power = Math.floor(Math.log10(step));
  const error = step / Math.pow(10, power);
  const f = error >= Math.sqrt(50) ? 10 : error >= Math.sqrt(10) ? 5 : error >= Math.sqrt(2) ? 2 : 1;
  return power >= 0 ? f * Math.pow(10, power) : -Math.pow(10, -power) / f;
}
function msNice(lo, hi, count) {
  let prev;
  for (let i = 0; i < 10; i++) {
    const step = msTickIncrement(lo, hi, count);
    if (step === prev) break;
    prev = step;
    if (step > 0) { lo = Math.floor(lo / step) * step; hi = Math.ceil(hi / step) * step; }
    else { break; }
  }
  return [lo, hi];
}
function msTicks(lo, hi, count) {
  const step = msTickIncrement(lo, hi, count);
  const out = [];
  for (let v = Math.ceil(lo / step) * step; v <= hi + 1e-9; v += step) out.push(Math.round(v * 1e6) / 1e6);
  return out;
}

/* Chart.constants.ts — CHART_GROUP_COLORS_DEFAULT order and the yellow text pairing. */
const MS_GROUP_COLORS = ["#653bd9", "#3a8518", "#697687", "#e7cb38", "#d63f2b", "#a188ea", "#a5cf8e", "#b2bbc6", "#f1e088", "#e89d98",
  "#4921bb", "#395c27", "#586370", "#bda62e", "#aa3222", "#8a6ae5", "#82bc62", "#98a4b3", "#ecd560", "#e1756b"];
const MS_YELLOWS = ["#e7cb38", "#ecd560", "#f1e088", "#f5eaaf", "#faf5d7", "#d4ba33", "#bda62e", "#a89329", "#847320"];
/* Pastel fills for the comparative (grouped) bar chart ONLY: production's own
   tint tokens (BaseCharts*T40) stand in for the *Main hexes, and its value
   labels go to ink. Every other chart keeps the platform's original colors. */
const MS_SOFT = { "#3a8518": "#a5cf8e", "#653bd9": "#a188ea", "#697687": "#b2bbc6", "#e7cb38": "#f1e088", "#d63f2b": "#e89d98" };
const MS_CHART_VARS = { "var(--chart-green)": "#3a8518", "var(--chart-purple)": "#653bd9", "var(--chart-gray)": "#697687", "var(--chart-yellow)": "#e7cb38", "var(--chart-red)": "#d63f2b" };
const msSoft = (c) => MS_SOFT[MS_CHART_VARS[String(c)] || String(c).toLowerCase()] || c;
const msTextOn = () => "#050301";

/* ---------- Badge (packages/common/components/Badge) ---------- */
function MsBadge({ color = "gray", variant = "solid", icon, children, className, title }) {
  return (
    <span className={"ms-badge ms-badge--" + color + (variant === "outlined" ? " ms-badge--outlined" : "") + (className ? " " + className : "")} title={title}>
      {icon && <i className={"ti ti-" + icon + " ms-badge__ic"} aria-hidden="true"></i>}
      <span className="ms-badge__inner">{children}</span>
    </span>
  );
}

/* ---------- ChartLegend (Charts/ChartLegend) ---------- */
function MsChartLegend({ items, glyph = [32, 16], soft = false }) {
  return (
    <div className="ms-legend">
      {items.map((it) => (
        <span className="ms-legend__item" key={it.label}>
          <svg width={glyph[0]} height={glyph[1]} aria-hidden="true"><rect width={glyph[0]} height={glyph[1]} rx="4" fill={soft ? msSoft(it.color) : it.color} /></svg>
          <span className="ms-legend__label">{it.label}</span>
        </span>
      ))}
    </div>
  );
}

/* ---------- FindingChartFrame (ObjectiveInsights/.../FindingChartFrame) ----------
   Title, the gray segment badge, the chart, then a right-aligned footer with
   "Explore the data" and the outlined full-screen trigger.
   MsChromeContext false (a Report slide thumbnail, itself a button) drops the
   footer's controls, so no button is ever rendered inside a button. */
const MsChromeContext = React.createContext(true);
function MsFindingChartFrame({ title, segment, onExplore, onFullScreen, isFullScreen, footExtra, children }) {
  const seg = segment || { value: "All respondents" };
  if (!React.useContext(MsChromeContext)) { onExplore = null; onFullScreen = null; footExtra = null; }
  return (
    <div className="ms-fc" data-testid="finding-chart">
      <div className="ms-fc__head">
        {!isFullScreen && title && <div className="ms-fc__title">{title}</div>}
        <MsBadge color="gray" icon="filter" className="ms-fc__seg" title={seg.tip || [seg.dimension, seg.value].filter(Boolean).join(": ")}>
          {seg.dimension && <React.Fragment><span className="ms-fc__segdim">{seg.dimension}</span><i className="ti ti-chevron-right ms-fc__segsep" aria-hidden="true"></i></React.Fragment>}
          <span className="ms-fc__segval">{seg.value}</span>
        </MsBadge>
      </div>
      {children}
      {(onExplore || onFullScreen || footExtra) && (
        <div className="ms-fc__foot">
          {footExtra}
          {onExplore && (
            <button type="button" className="ms-link-subtle" onClick={onExplore} aria-label={"Explore the data: " + (title || "chart")}>
              <i className="ti ti-device-desktop-analytics" aria-hidden="true"></i>Explore the data
            </button>
          )}
          {onFullScreen && !isFullScreen && (
            <button type="button" className="ms-iconbtn-outlined" onClick={onFullScreen} title="Full screen" aria-label={"View " + (title || "chart") + " full screen"}>
              <i className="ti ti-arrows-diagonal" aria-hidden="true"></i>
            </button>
          )}
        </div>
      )}
    </div>
  );
}

/* ---------- FindingChartFullScreen (Modal fullScreen) ---------- */
function MsFullScreen({ title, onClose, children }) {
  msE(() => {
    const k = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", k);
    return () => document.removeEventListener("keydown", k);
  }, [onClose]);
  return (
    <div className="ms-fs" role="dialog" aria-modal="true" aria-label={title || "Chart"}>
      <div className="ms-fs__head">
        <div className="ms-fs__title">{title || "Chart"}</div>
        <button type="button" className="ms-iconbtn-outlined" onClick={onClose} aria-label="Close full screen"><i className="ti ti-x" aria-hidden="true"></i></button>
      </div>
      <div className="ms-fs__body">{children}</div>
    </div>
  );
}

/* ---------- ChartTooltip (light) ---------- */
function MsTip({ tip }) {
  if (!tip) return null;
  return <div className="ms-tip ms-tip--light" style={{ left: tip.x, top: tip.y }}>{tip.body}</div>;
}

/* ---------- VerticalBarChart (Charts/VerticalBarChart), as FindingBucketChart passes it ----------
   barWidth 48, gapBetweenGroup 36, height 300, maxScaleLimit 100, one series
   in brand green. Bars rx 4 (all corners); value label inside in white when the
   bar is at least 25px tall, otherwise above it in ink. */
/* getAccessibleTextColor: white or ink, whichever reads better on the fill. */
function msLum(hex) {
  const h = hex.replace("#", "");
  const c = [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255).map((v) => (v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)));
  return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
}
function msAccessibleText(hex) {
  const L = msLum(hex);
  return (1.05 / (L + 0.05)) >= ((L + 0.05) / (msLum("#050301") + 0.05)) ? "#ffffff" : "#050301";
}
/* Optional, as QuestionReportVerticalSentimentChart passes it: colorOf (bar
   color per row), interval (fixed 0-100 y scale), imgOf + imageSize (tick
   image above the label). */
function MsVerticalBarChart({ rows, height = 300, barWidth = 48, gap = 36, color = "#3a8518", format = (v) => (v || 0) + "%", maxScale = 100,
  colorOf, interval, imgOf, imageSize = 56 }) {
  const ref = msR(null);
  const parentW = useMsWidth(ref, 720);
  const [tip, setTip] = msS(null);
  const n = rows.length;
  const groupW = barWidth * 2;
  const contentW = groupW * n + gap * Math.max(0, n - 1);
  const ext = Math.max(0, gap - 10) / 2;
  /* Two-line estimate: chars per line from the tick width at 8.5px a glyph. */
  const tickW = groupW + gap / 2 - 8;
  const perLine = Math.floor(tickW / 8.5);
  const lines = (s) => {
    const words = String(s).split(/[\s/]+/);
    let ln = 1, cur = 0;
    words.forEach((w) => { if (cur && cur + 1 + w.length > perLine) { ln++; cur = w.length; } else cur += (cur ? 1 : 0) + w.length; });
    return ln;
  };
  const imgH = imgOf ? imageSize + 4 : 0;
  const tickH = (rows.some((r) => lines(r.label) > 2) ? 68 : 36) + imgH;
  const highest = Math.max(0, ...rows.map((r) => r.value || 0));
  const maxDomain = interval ? maxScale : Math.min(Math.ceil(highest / 10) * 10 + 11, maxScale);
  const [, niceMax] = interval ? [0, maxScale] : msNice(0, maxDomain, 10);
  const top = 24, right = 40, bottom = 44 + (tickH - 36);
  const left = 44 + Math.max(0, String(format(maxDomain)).length - 3) * 8.5;
  const yMax = height - top - bottom;
  const y = (v) => yMax - (v / niceMax) * yMax;
  const ticks = interval ? Array.from({ length: Math.floor(maxScale / interval) + 1 }, (_, i) => i * interval) : msTicks(0, niceMax, 5);
  const svgW = Math.max(contentW + left + right + ext * 2, parentW);
  const x0 = left + ext;
  const H = height + imgH;
  return (
    <div className="ms-vbc" ref={ref}>
      <div className="ms-vbc__scroll" style={{ height: H }}>
        <svg role="figure" width={svgW} height={H} className="ms-vbc__svg">
          <g transform={`translate(${left},${top})`}>
            {ticks.map((t) => <line key={"g" + t} x1={0} x2={svgW - left} y1={Math.round(y(t))} y2={Math.round(y(t))} stroke="var(--visx-grid-stroke)" strokeWidth="1" />)}
            {ticks.map((t) => <text key={"t" + t} x={-8} dx="-1em" dy="0.25em" y={Math.round(y(t))} className="ms-axis-label" textAnchor="end">{format(t)}</text>)}
          </g>
          <g transform={`translate(${x0},${top})`}>
            {rows.map((r, i) => {
              const fill = colorOf ? colorOf(r) : color;
              const v = r.value || 0;
              const gx = i * (groupW + gap);
              const h = Math.max(1, yMax - y(v));
              const by = yMax - h;
              const inside = h >= 25;
              return (
                <g key={r.label + i}
                  onMouseMove={(e) => setTip({ x: e.nativeEvent.offsetX + 12, y: e.nativeEvent.offsetY + 12,
                    body: <React.Fragment><div className="ms-tip__t">{r.label}</div><div>{format(v)}</div>{r.basis && <div className="ms-tip__s">{r.basis}</div>}</React.Fragment> })}
                  onMouseLeave={() => setTip(null)}>
                  <rect x={gx} y={0} width={groupW} height={yMax} fill="transparent" />
                  <rect x={gx + (groupW - barWidth) / 2} y={by} width={barWidth} height={h} rx="4" fill={fill} />
                  <text x={gx + groupW / 2} y={inside ? by + 8 : by - 16} dy="0.71em" textAnchor="middle" className="ms-bar-val"
                    fill={inside ? msAccessibleText(fill) : "#050301"}>{format(v)}</text>
                </g>
              );
            })}
          </g>
          <g transform={`translate(${x0},${top + yMax})`}>
            {rows.map((r, i) => {
              const cx = i * (groupW + gap) + groupW / 2;
              const fw = groupW + gap - 8;
              return (
                <foreignObject key={r.label + i} x={cx - fw / 2} y={8} width={fw} height={tickH} style={{ overflow: "visible" }}>
                  <div className="ms-vbc__tick">
                    {imgOf && <span className="ms-vbc__tickimg" style={{ width: imageSize, height: imageSize }}>{imgOf(r) ? <img src={imgOf(r)} alt="" loading="lazy" /> : null}</span>}
                    <span className="ms-vbc__ticklab" title={r.label}>{r.label}</span>
                  </div>
                </foreignObject>
              );
            })}
          </g>
        </svg>
        <MsTip tip={tip} />
      </div>
    </div>
  );
}

/* ---------- HorizontalBarChart, group mode (Charts/HorizontalBarChart) ----------
   Bars 24px (<=2 series) or 16px (>2), 4px between bars of a group, 48/20/16px
   between groups for 1/2/3+ series. Left label column = 25% of the width
   minus 32. Value label 12px/600 at the bar end + 4, in the bar's color. */
function MsHorizontalBarChart({ groups, series, domain = [0, 100], interval = 20, format = (v) => String(v) }) {
  const ref = msR(null);
  const W = useMsWidth(ref, 720);
  const [tip, setTip] = msS(null);
  const k = series.length;
  const bh = k <= 2 ? 24 : 16;
  const gb = 4;
  const gg = k === 1 ? 48 : k === 2 ? 20 : 16;
  const groupH = bh * k + gb * (k - 1);
  const rowH = gg + groupH;
  const margin = { top: 0, right: 46, bottom: 12, left: 32 };
  const TICK_H = 18;
  const height = rowH * groups.length + margin.top + margin.bottom + TICK_H + 12;
  const ltw = Math.max(80, 0.25 * W - margin.left);
  const xMax = Math.max(40, W - margin.left - margin.right - ltw);
  const x0 = margin.left + ltw;
  const [lo, hi] = domain;
  const x = (v) => ((Math.max(lo, Math.min(hi, v)) - lo) / (hi - lo)) * xMax;
  const ticks = [];
  for (let t = lo; t <= hi; t += interval) ticks.push(t);
  const plotBottom = height - margin.bottom - TICK_H;
  return (
    <div className="ms-hbc" ref={ref} style={{ height }}>
      <svg role="figure" width={W} height={height} className="ms-hbc__svg">
        {groups.map((g, i) => <line key={"r" + i} x1={0} x2={W} y1={margin.top + i * rowH + rowH / 2} y2={margin.top + i * rowH + rowH / 2} stroke="var(--visx-grid-stroke)" strokeWidth="1" />)}
        {ticks.map((t) => <line key={"c" + t} x1={x0 + x(t)} x2={x0 + x(t)} y1={margin.top} y2={plotBottom} stroke="var(--visx-grid-stroke)" strokeWidth="1" />)}
        {groups.map((g, i) => (
          <foreignObject key={"l" + i} x={margin.left - 8} y={margin.top + i * rowH} width={ltw} height={rowH}>
            <div className="ms-hbc__lab"><span className="ms-hbc__labtxt" title={g.label}>{g.label}</span></div>
          </foreignObject>
        ))}
        <g transform={`translate(${x0},${margin.top})`}>
          {groups.map((g, i) => {
            const gy = i * rowH + (rowH - groupH) / 2;
            return series.map((s, j) => {
              const v = g.values[s.key];
              if (typeof v !== "number") return null;
              const w = Math.max(1, x(v));
              const by = gy + j * (bh + gb);
              return (
                <g key={i + "-" + s.key}
                  onMouseMove={(e) => setTip({ x: e.nativeEvent.offsetX + 12, y: e.nativeEvent.offsetY + 12,
                    body: <React.Fragment><div className="ms-tip__t">{g.label}</div><div>{s.label}: {format(v)}</div></React.Fragment> })}
                  onMouseLeave={() => setTip(null)}>
                  <rect x={0} y={by} width={w} height={bh} rx="4" ry="4" fill={msSoft(s.color)} />
                  <text x={w + 4} y={by + bh / 2} dy="0.33em" className="ms-bar-val" fill={msTextOn(s.color)}>{format(v)}</text>
                </g>
              );
            });
          })}
        </g>
        <g transform={`translate(${x0},${plotBottom})`}>
          {ticks.map((t) => <text key={"x" + t} x={x(t)} y={TICK_H - 4} textAnchor="middle" className="ms-axis-label ms-axis-label--x">{format(t)}</text>)}
        </g>
      </svg>
      <MsTip tip={tip} />
    </div>
  );
}

/* ---------- SimpleHorizontalBarChart (Charts/SimpleHorizontalBarChart) ----------
   Row padding 8px 24px; label Text md; Mantine Progress 16px, radius 2, track
   gray[2]; value label inside the bar end in white, outside in green under 5. */
function MsSimpleHorizontalBarChart({ rows, color = "#3a8518", barHeight = 16, onRowClick }) {
  return (
    <div className="ms-shbc">
      {rows.map((r, i) => {
        const v = Math.max(0, Math.min(100, r.value || 0));
        const out = (r.value || 0) < 5;
        return (
          <div className={"ms-shbc__row" + (r.highlighted ? " is-hl" : "") + (onRowClick ? " is-click" : "")} key={r.label + i}
            onClick={onRowClick ? () => onRowClick(r) : undefined}>
            <div className="ms-shbc__line">
              <span className="ms-shbc__left">{r.left}<span className="ms-shbc__label" title={r.label}>{r.label}</span></span>
              {r.right && <span className="ms-shbc__right">{r.right}</span>}
            </div>
            <div className="ms-progress" style={{ height: barHeight }}>
              <div className="ms-progress__bar" style={{ width: v + "%", background: r.color || color }}>
                {!out && <span className="ms-progress__lab">{r.valueLabel}</span>}
                {out && <span className="ms-progress__lab ms-progress__lab--out">{r.valueLabel}</span>}
              </div>
            </div>
          </div>
        );
      })}
    </div>
  );
}

/* ---------- Heatmap (Charts/Heatmap + HeatmapValueCell) ----------
   CSS grid, no gaps or borders. Cell alpha 0.08 -> 0.95 across the matrix's
   own min..max; white text when the blended luma is under 160 (210 for the
   yellow base). Compact: 36px rows. */
const MS_HEAT_MIN = 0.08, MS_HEAT_MAX = 0.95;
function msHeatAlpha(v, min, max) {
  const r = max - min;
  if (r <= 0) return MS_HEAT_MIN;
  const t = Math.max(0, Math.min(1, (v - min) / r));
  return MS_HEAT_MIN + t * (MS_HEAT_MAX - MS_HEAT_MIN);
}
function msHeatText(base, a) {
  const h = base.replace("#", "");
  const ch = [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16)).map((c) => 255 * (1 - a) + c * a);
  const luma = (299 * ch[0] + 587 * ch[1] + 114 * ch[2]) / 1000;
  const threshold = base.toLowerCase() === "#e7cb38" ? 210 : 160;
  return luma < threshold ? "#ffffff" : "#050301";
}
function msRgba(base, a) {
  const h = base.replace("#", "");
  return "rgba(" + [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16)).join(",") + "," + a.toFixed(3) + ")";
}
/* cols: [{key,label,kind}] — kind "value" (heat), "text", "call", "plain".
   rows: [{label, cells:[{v}]}]. base: the heat color (green, or yellow for
   negative measures, as production's detractor heatmap). */
function MsHeatmap({ cols, rows, base = "#3a8518", format = (v) => String(v), compact = true, rowLabel }) {
  const rowH = compact ? 36 : 56;
  /* min..max is taken over the matrix's value cells; a table that carries two
     bases (a share column beside a share-of-passes column) scales each base
     over its own cells, as two production heatmaps would. */
  const baseOf = (j) => cols[j].base || base;
  const range = {};
  rows.forEach((r) => r.cells.forEach((c, j) => {
    if (cols[j].kind !== "value" || typeof c.v !== "number") return;
    const b = baseOf(j);
    const rg = range[b] || (range[b] = { min: Infinity, max: -Infinity });
    rg.min = Math.min(rg.min, c.v); rg.max = Math.max(rg.max, c.v);
  }));
  const hasText = cols.some((c) => c.kind === "text");
  // With text columns the grid fits the card, where fit-content would let the
  // label column collapse to min-content; the label itself caps at 200px.
  // A text column whose longest cell is a few words ("Print", "Customers") sizes to its
  // content, so it doesn't sit in a wide empty cell; long prose (a "Why" column) keeps
  // the 2fr share of the spare width.
  const isShortText = (j) => cols[j].kind === "text" && rows.every((r) => String((r.cells[j] || {}).v == null ? "" : r.cells[j].v).length <= 28);
  const tmpl = [hasText ? "max-content" : "fit-content(200px)"].concat(cols.map((c, j) => (c.kind === "text" ? (isShortText(j) ? "max-content" : "minmax(160px, 2fr)") : c.kind === "call" ? "minmax(120px, max-content)" : "minmax(120px, 1fr)"))).join(" ");
  return (
    <div className="ms-heat__scroll">
      <div className={"ms-heat" + (cols.some((c) => c.kind === "text") ? " has-text" : "")} style={{ gridTemplateColumns: tmpl }} role="table">
        <div className="ms-heat__corner" role="columnheader">{rowLabel ? <span className="ms-heat__cornerlab">{rowLabel}</span> : null}</div>
        {cols.map((c) => <div key={c.key} className={"ms-heat__col" + (c.kind === "text" ? " is-text" : "")} role="columnheader"><span className="ms-heat__collab">{c.label}</span></div>)}
        {rows.map((r, i) => (
          <React.Fragment key={r.label + i}>
            <div className="ms-heat__rowlab" role="rowheader" style={{ minHeight: rowH }}><span className="ms-heat__rowlabtxt">{r.label}</span></div>
            {r.cells.map((c, j) => {
              const kind = cols[j].kind;
              if (kind === "value" && typeof c.v === "number") {
                const b = baseOf(j);
                const a = msHeatAlpha(c.v, range[b].min, range[b].max);
                return <div key={j} className="ms-heat__cell" role="cell" style={{ minHeight: rowH, background: msRgba(b, a), color: msHeatText(b, a) }}>{format(c.v)}</div>;
              }
              if (kind === "call") return <div key={j} className="ms-heat__cell ms-heat__cell--call" role="cell" style={{ minHeight: rowH }}><MsCallBadge v={c.v} /></div>;
              if (kind === "text") return <div key={j} className="ms-heat__cell ms-heat__cell--text" role="cell" style={{ minHeight: rowH }}>{c.v}</div>;
              return <div key={j} className="ms-heat__cell ms-heat__cell--plain" role="cell" style={{ minHeight: rowH }}>{c.v}</div>;
            })}
          </React.Fragment>
        ))}
      </div>
    </div>
  );
}

/* Keep / change / drop, as production Badges (solid): primary, yellow, gray.
   Production has no red badge. Icon + label, never color alone. */
const MS_CALL = { keep: ["primary", "check", "Keep"], change: ["yellow", "refresh", "Change"], drop: ["gray", "x", "Drop"] };
function MsCallBadge({ v }) {
  const c = MS_CALL[String(v || "").toLowerCase()];
  if (!c) return <span>{v}</span>;
  return <MsBadge color={c[0]} icon={c[1]} className="ms-badge--call">{c[2]}</MsBadge>;
}

/* ---------- ProductGrid + Badge (FindingSentimentScoreChart) ----------
   imageSize 90: 130px columns, cell padding 6px 16px, "#N" rank, outlined
   badge at the image's top-right (inside the corner in fill mode), bold 12px
   name that wraps. Badge color comes from the benchmark position, as
   getBenchmarkBadgeColor maps it: AboveTop primary, Between gray,
   BelowBottom yellow. */
const MS_POSITION_BADGE = { AboveTop: "primary", Between: "gray", BelowBottom: "yellow" };
function MsProductGrid({ items, fill, imgOf }) {
  return (
    <div className={"ms-pgrid" + (fill ? " is-fill" : "")}>
      {items.map((it, i) => {
        const img = imgOf ? imgOf(it.id) : null;
        return (
          <div className="ms-pgrid__cell" key={it.label + i}>
            <span className="ms-pgrid__rank">#{i + 1}</span>
            <div className="ms-pgrid__imgwrap">
              <div className="ms-pgrid__img">{img ? <img src={img} alt={it.label} loading="lazy" /> : <i className="ti ti-shirt" aria-hidden="true"></i>}</div>
              <MsBadge color={MS_POSITION_BADGE[it.position] || "gray"} variant="outlined" className="ms-pgrid__badge">{Math.round(it.score)}</MsBadge>
            </div>
            <div className="ms-pgrid__name">{it.label}</div>
          </div>
        );
      })}
    </div>
  );
}
/* FindingSentimentScoreLegend: 12x12 rx 4 glyphs, 14px labels. */
function MsBenchmarkLegend({ labels }) {
  const L = labels || { AboveTop: "Top 25%", Between: "Mid", BelowBottom: "Lower 25%" };
  return <MsChartLegend glyph={[12, 12]} items={[{ label: L.AboveTop, color: "#3a8518" }, { label: L.Between, color: "#eef2f6" }, { label: L.BelowBottom, color: "#d69d2b" }]} />;
}

/* ---------- ScoreBoard (workspace/app/components/ScoreBoard) ---------- */
function MsScoreBoard({ icon = "users", title, value, captionTitle, captionValue }) {
  return (
    <div className="ms-score">
      <div className="ms-score__row">
        <span className="ms-score__l"><i className={"ti ti-" + icon} aria-hidden="true"></i><span className="ms-score__t">{title}</span></span>
        <span className="ms-score__v">{value}</span>
      </div>
      {captionTitle != null && captionValue != null && (
        <div className="ms-score__cap"><span>{captionTitle}</span><span>{captionValue}</span></div>
      )}
    </div>
  );
}

/* ---------- LineEfficiencyChart (incremental VerticalBarChart) ----------
   barWidth 44, gapBetweenGroup 28, height 341 + 48 image row, y 0-100 every
   25 with no "%". Cumulative bar gray, incremental bar purple floating on
   it; dashed purple connector; product image 40px under each column. */
function MsLineEfficiencyChart({ rows, imgOf }) {
  const ref = msR(null);
  const parentW = useMsWidth(ref, 720);
  const [hover, setHover] = msS(null);
  const barW = 44, gap = 28, H = 341 + 48, groupW = barW * 2;
  const ext = (gap - 10) / 2;
  const tickH = 36;
  const margin = { top: 24, right: 40, bottom: 44 + 48 + (tickH - 36), left: 44 };
  const yMax = H - margin.top - margin.bottom;
  const y = (v) => yMax - (v / 100) * yMax;
  const ticks = [0, 25, 50, 75, 100];
  const n = rows.length;
  const contentW = groupW * n + gap * Math.max(0, n - 1);
  const svgW = Math.max(contentW + margin.left + margin.right + ext * 2, parentW);
  const x0 = margin.left + ext;
  /* rows carry each product's incremental reach (`val`), in the order the
     greedy pass added them; the cumulative bar is the running total before it. */
  let run = 0;
  const bars = rows.map((r) => {
    const cum = run;
    run += r.val || 0;
    return { ...r, cum, rel: r.val || 0 };
  });
  return (
    <div className="ms-vbc" ref={ref}>
      <div className="ms-le__legend">
        <MsChartLegend glyph={[12, 12]} items={[{ label: "Incremental reach", color: "#653bd9" }, { label: "Cumulative incremental reach", color: "#697687" }]} />
      </div>
      <div className="ms-vbc__scroll" style={{ height: H }}>
        <svg role="figure" width={svgW} height={H} className="ms-vbc__svg">
          <g transform={`translate(${margin.left},${margin.top})`}>
            {ticks.map((t) => <line key={"g" + t} x1={0} x2={svgW - margin.left} y1={Math.round(y(t))} y2={Math.round(y(t))} stroke="var(--visx-grid-stroke)" strokeWidth="1" />)}
            {ticks.map((t) => <text key={"t" + t} x={-8} dx="-1em" dy="0.25em" y={Math.round(y(t))} className="ms-axis-label" textAnchor="end">{t}</text>)}
          </g>
          <g transform={`translate(${x0},${margin.top})`}>
            {bars.map((b, i) => {
              const gx = i * (groupW + gap);
              const dim = hover != null && i > hover;
              const cumH = Math.max(0, yMax - y(b.cum));
              const relTop = y(b.cum + b.rel);
              const relH = Math.max(1, y(b.cum) - relTop);
              const relX = i === 0 ? gx + groupW / 2 - barW / 2 : gx + barW;
              return (
                <g key={b.label + i} opacity={dim ? 0.5 : 1} onMouseEnter={() => setHover(i)} onMouseLeave={() => setHover(null)}>
                  <rect x={gx - ext} y={0} width={groupW + ext * 2} height={yMax} fill="transparent" />
                  {i > 0 && cumH > 0 && <rect x={gx} y={yMax - cumH} width={barW} height={cumH} rx="4" fill="#697687" />}
                  {i > 0 && cumH > 0 && <text x={gx + barW / 2} y={cumH >= 25 ? yMax - cumH + 8 : yMax - cumH - 16} dy="0.71em" textAnchor="middle" className="ms-bar-val" fill={cumH >= 25 ? "#ffffff" : "#653bd9"}>{Math.round(b.cum)}</text>}
                  <rect x={relX} y={relTop} width={barW} height={relH} rx="4" fill="#653bd9" />
                  <text x={relX + barW / 2} y={relTop - 16} dy="0.71em" textAnchor="middle" className="ms-bar-val" fill="#653bd9">{Math.round(b.rel)}</text>
                  {i < n - 1 && (hover == null || i < hover) && (
                    <line x1={i === 0 ? relX + barW : gx + groupW} x2={gx + groupW + gap} y1={relTop + 0.5} y2={relTop + 0.5} stroke="#653bd9" strokeWidth="1" strokeDasharray="2,1" />
                  )}
                </g>
              );
            })}
          </g>
          <g transform={`translate(${x0},${margin.top + yMax})`}>
            {bars.map((b, i) => {
              const cx = i * (groupW + gap) + groupW / 2;
              const fw = groupW + gap - 8;
              const img = imgOf ? imgOf(b.id) : null;
              return (
                <foreignObject key={b.label + i} x={cx - fw / 2} y={8} width={fw} height={tickH + 48} style={{ overflow: "visible" }}
                  opacity={hover != null && i > hover ? 0.5 : 1}>
                  <div className="ms-vbc__tick">
                    <span className="ms-le__img">{img ? <img src={img} alt="" loading="lazy" /> : null}</span>
                    <span className="ms-vbc__ticklab" title={b.label}>{b.label}</span>
                  </div>
                </foreignObject>
              );
            })}
          </g>
        </svg>
      </div>
    </div>
  );
}

/* ---------- PieChart (Charts/PieChart + PieLegend) ----------
   320px donut, inner radius 78, pad angle 0.5 deg, colors in the default
   group order; legend glyph 32x16, bold value in its paired dark shade. */
const MS_PIE_TEXT = { "#653bd9": "#321780", "#a188ea": "#321780", "#3a8518": "#213516", "#a5cf8e": "#213516",
  "#697687": "#424952", "#b2bbc6": "#424952", "#d63f2b": "#802b21", "#e89d98": "#802b21" };
function MsPieChart({ rows }) {
  const data = rows.filter((r) => r.value > 0);
  const total = data.reduce((t, r) => t + r.value, 0);
  const R = 158, RI = 78, C = 160, pad = Math.PI / 360;
  let a0 = 0;
  const arc = (s, e) => {
    const p = (r, a) => [C + r * Math.sin(a), C - r * Math.cos(a)];
    const [x0, y0] = p(R, s), [x1, y1] = p(R, e), [x2, y2] = p(RI, e), [x3, y3] = p(RI, s);
    const large = e - s > Math.PI ? 1 : 0;
    return `M${x0},${y0}A${R},${R} 0 ${large} 1 ${x1},${y1}L${x2},${y2}A${RI},${RI} 0 ${large} 0 ${x3},${y3}Z`;
  };
  return (
    <div className="ms-pie">
      <svg className="ms-pie__svg" width="320" height="320" role="figure">
        {data.map((r, i) => {
          const sweep = total ? (r.value / total) * Math.PI * 2 : 0;
          const s = a0 + pad / 2, e = a0 + sweep - pad / 2;
          a0 += sweep;
          return <path key={r.label} d={arc(s, Math.max(s + 0.001, e))} fill={MS_GROUP_COLORS[i % MS_GROUP_COLORS.length]}><title>{r.label + ": " + r.valueLabel}</title></path>;
        })}
      </svg>
      <div className="ms-pie__legend">
        {data.map((r, i) => {
          const c = MS_GROUP_COLORS[i % MS_GROUP_COLORS.length];
          return (
            <div className="ms-pie__item" key={r.label}>
              <svg width="32" height="16" aria-hidden="true"><rect width="32" height="16" rx="4" fill={c} /></svg>
              <span className="ms-pie__vals">
                <span className="ms-pie__val" style={{ color: MS_PIE_TEXT[c] || (MS_YELLOWS.indexOf(c) >= 0 ? "#847320" : "#050301") }}>{r.valueLabel}</span>
                <span className="ms-pie__lab">{r.label}</span>
              </span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

/* ---------- Text responses (QuestionReportPaginatedText rows) ----------
   Row padding 12px 24px, hairline under each; index 14px gray, the response
   14/24 clamped to 3 lines; right side bold green (production: the count). */
function MsTextResponses({ rows }) {
  return (
    <div className="ms-texts">
      {rows.map((r, i) => (
        <div className="ms-texts__row" key={i}>
          <span className="ms-texts__l">
            <span className="ms-texts__n">{i + 1}</span>
            {r.id && r.img ? <span className="ms-texts__img"><img src={r.img} alt="" loading="lazy" /></span> : null}
            <span className="ms-texts__t" title={r.text}>{r.text}</span>
          </span>
          {r.right != null && <span className={"ms-texts__r" + (r.rightMuted ? " is-muted" : "")}>{r.right}</span>}
        </div>
      ))}
    </div>
  );
}

/* ---------- QuestionCard (SurveyReport QuestionReport) ----------
   Paper radius 12, hairline border; header 12px 24px, min-height 64; Heading
   h5 16/20 semibold; the type as a gray pill with its icon; sort IconButton;
   the respondent count as a gray Badge with IconUsers; content under a
   divider; footer with icon-only chart toggles and Options / full screen. */
const MS_QTYPE = {
  single: ["circle-check", "Single select"], multi: ["checks", "Multi select"], scale: ["star", "Star rating"],
  likert: ["circle-number-1", "Likert rating"], ranking: ["sort-ascending-numbers", "Ranking"], text: ["letter-case-toggle", "Long text"],
  sentiment: ["heart", "Sentiment score"], age: ["rating-18-plus", "Age"], gender: ["gender-transgender", "Gender"]
};
const MS_TOGGLES = { hbar: ["chart-arrows", "Horizontal bar chart"], vbar: ["chart-bar", "Vertical bar chart"], pie: ["chart-pie-2", "Pie chart"], heat: ["table-alias", "Heatmap"], text: ["letter-case-toggle", "Free text"],
  /* Not a production toggle: the demo's product-tile view of the sentiment chart. */
  grid: ["layout-grid", "Product tiles"] };
function MsQuestionCard({ title, subtitle, type, n, toggles, view, onView, onSort, sortMenu, children }) {
  const t = MS_QTYPE[type] || MS_QTYPE.single;
  return (
    <article className="ms-qcard">
      <div className="ms-qcard__head">
        <div className="ms-qcard__title">
          <h5 className="ms-qcard__h">{title}</h5>
          {subtitle && <div className="ms-qcard__sub">{subtitle}</div>}
        </div>
        <div className="ms-qcard__meta">
          <span className="ms-qcard__type"><i className={"ti ti-" + t[0]} aria-hidden="true"></i>{t[1]}</span>
          {onSort && (
            <span className="ms-qcard__sortwrap">
              <button type="button" className="ms-iconbtn-subtle" onClick={onSort} aria-label="Sort"><i className="ti ti-sort-descending" aria-hidden="true"></i></button>
              {sortMenu}
            </span>
          )}
          <MsBadge color="gray" icon="users">{(n || 0).toLocaleString()}</MsBadge>
        </div>
      </div>
      <div className="ms-qcard__content">{children}</div>
      {toggles && toggles.length > 0 && (
        <div className="ms-qcard__foot">
          <div className="ms-qcard__toggles" role="tablist">
            {toggles.map((k) => (
              <button key={k} type="button" role="tab" aria-selected={view === k} className={"ms-qcard__tog" + (view === k ? " is-on" : "")}
                title={MS_TOGGLES[k][1]} aria-label={MS_TOGGLES[k][1]} onClick={() => onView && onView(k)}>
                <i className={"ti ti-" + MS_TOGGLES[k][0]} aria-hidden="true"></i>
              </button>
            ))}
          </div>
          <div className="ms-qcard__acts">
            <button type="button" className="ms-btn-subtle"><i className="ti ti-adjustments-horizontal" aria-hidden="true"></i>Options</button>
            <button type="button" className="ms-iconbtn-outlined" title="Toggle full screen" aria-label="Toggle full screen"><i className="ti ti-arrows-diagonal" aria-hidden="true"></i></button>
          </div>
        </div>
      )}
    </article>
  );
}

Object.assign(window, {
  MsBadge, MsChartLegend, MsFindingChartFrame, MsFullScreen, MsVerticalBarChart, MsHorizontalBarChart,
  MsSimpleHorizontalBarChart, MsHeatmap, MsCallBadge, MsProductGrid, MsBenchmarkLegend, MsScoreBoard,
  MsLineEfficiencyChart, MsPieChart, MsTextResponses, MsQuestionCard, MS_GROUP_COLORS, MsChromeContext
});
