/* global React, PinContext */
// =========================================================
// Tecovas Women's Apparel — Insights (client view).
// Reuses the Gola client-view shell (left index + sticky-headed
// reading panel) but renders the apparel insight model:
//   Objective → Resolution (section header)
//             → Arguments  (cards)
//             → Findings   (data viz: line efficiency, advocate/
//                           detractor, sentiment tiers, single stat)
// Every finding card traces back to "Explore the data" via the
// shared navigateToData intent.
// =========================================================

function apImg(id) {
  return (window.PRODUCT_IMAGES && id) ? window.PRODUCT_IMAGES.url(id) : null;
}
// productId -> cross-product sentiment score, rebuilt per ApparelInsights render
let apSentiment = {};
// Live per-product sentiment score for ALL products (from the loaded study),
// so gallery badges aren't limited to the top-N encoded in the ranking findings.
function useProductScores() {
  const { study } = window.useFindingsStudyBench ? window.useFindingsStudyBench() : { study: null };
  return React.useMemo(() => {
    const m = {};
    if (study && window.StudyData && window.csScore) {
      const arr = window.StudyData.aggregateAllProducts(study.respondents).filter((p) => p.n > 0)
        .map((p) => ({ id: p.id, score: window.csScore(p.meanRating) }))
        .sort((a, b) => b.score - a.score);
      const n = arr.length, top = Math.ceil(n / 3), bot = Math.floor(n / 3);
      arr.forEach((p, i) => { m[p.id] = { score: p.score, tier: i < top ? "top" : (i >= n - bot ? "low" : "mid") }; });
    }
    return m;
  }, [study]);
}
// ---- Trace-to-data link (mirrors real-findings TraceToData) ----
function ApTrace({ link, from }) {
  const store = React.useContext(PinContext);
  if (!link || !store || !store.navigateToData) return null;
  return (
    <button className="gl-rf-trace ap-trace" onClick={() => store.navigateToData(from ? { ...link, fromId: from } : link)}
      title={"Open in Explore the data — " + link.sourceLabel + " · " + link.cutLabel}>
      <span className="gl-rf-trace__txt">Explore the data ↗</span>
    </button>
  );
}

// =========================================================
// Segment / filtration UI — standalone "who was cut" badge that
// sits UNDER the finding title (title = description only). Right-
// side header metadata is ALWAYS the sample size (n).
// =========================================================
// Condensed dimension + value labels so the badge stays digestible.
const SEG_DIM_SHORT = {
  "Age group": "Age",
  "Relationship to Tecovas": "Relationship",
};
const SEG_VAL_SHORT = {
  "I am not a current customer, but would be open to shopping": "Non-customer · open to shop",
  "Tecovas is one of my favorite and most frequently shopped brands": "Favorite brand",
  "I regularly shop for Tecovas products": "Regular shopper",
  "I have shopped Tecovas before but not regularly": "Lapsed / occasional",
};
function segCondense(v) {
  if (SEG_VAL_SHORT[v]) return SEG_VAL_SHORT[v];
  return (typeof v === "string" && v.length > 34) ? v.slice(0, 32).trim() + "…" : v;
}

// Compute a segment's live respondent count by replaying its Explore-the-data
// filter against the loaded study (arrays → Sets for StudyData.filterRespondents).
function useSegN(link) {
  const { study } = window.useFindingsStudyBench
    ? window.useFindingsStudyBench()
    : { study: null };
  return React.useMemo(() => {
    if (!link || !link.filter || !window.StudyData || !study) return null;
    const toSet = (o) => Object.fromEntries(
      Object.entries(o || {}).map(([k, v]) => [k, new Set(Array.isArray(v) ? v : [v])])
    );
    const filters = { demo: toSet(link.filter.demo), answers: toSet(link.filter.answers) };
    const rows = window.StudyData.filterRespondents(filters);
    return rows ? rows.length : null;
  }, [link, study]);
}

// One segment chip. Variants:
//   all  — no cut applied (all respondents)
//   role — behavioral cut (advocators / detractors), colored by polarity
//   seg  — demographic / answer-option cut: DIMENSION › value (filter-like)
function ApSegChip({ s }) {
  if (s.all) {
    return (
      <span className="ap-seg__chip ap-seg__chip--all">
        <i className="ti ti-users-group ap-seg__ic"></i>
        <span className="ap-seg__val">All respondents</span>
      </span>
    );
  }
  if (s.role) {
    return (
      <span className={"ap-seg__chip ap-seg__chip--role ap-seg__chip--" + (s.pos ? "pos" : "neg")}>
        <span className="ap-seg__dim">{s.label}</span>
        {s.count != null && (
          <span className="ap-seg__val">{s.count}{s.pct != null ? " · " + s.pct + "%" : ""}</span>
        )}
      </span>
    );
  }
  return (
    <span className="ap-seg__chip ap-seg__chip--seg" title={s.full || (s.dim + " · " + s.value)}>
      <i className="ti ti-filter ap-seg__ic"></i>
      <span className="ap-seg__dim">{s.dim}</span>
      <span className="ap-seg__sep" aria-hidden="true"></span>
      <span className="ap-seg__val">{segCondense(s.value)}</span>
    </span>
  );
}
// Chart tools (sort / show) are owned by the argument card and consumed by
// every segment bar + heatmap inside it.
const ApToolsCtx = React.createContext(null);
// Sample-size badge — MakerSights design-system badge (outlined / secondary,
// squared). Rides at the LEFT of the segment bar rather than the header.
function ApNBadge({ n }) {
  if (n == null) return null;
  const sp = apSrcSplit(n);
  if (!sp) return (
    <span className="bdg outlined secondary squared ap-nbadge">
      <i className="ti ti-users ap-nbadge__ic"></i>
      <span className="ap-nbadge__v">{n}</span>
    </span>
  );
  return (
    <span className="ap-nwrap" tabIndex={0}>
      <span className="bdg outlined secondary squared ap-nbadge ap-nbadge--pop">
        <i className="ti ti-users ap-nbadge__ic"></i>
        <span className="ap-nbadge__v">{n}</span>
        <i className="ti ti-chevron-down ap-nbadge__cv"></i>
      </span>
      {window.ApSamplePop ? <window.ApSamplePop n={n} sp={sp} /> : null}
    </span>
  );
}
// Respondent-source split (humans vs digital twins). Renders only when the
// hosting page opts in via window.__TWIN_SPLIT = { share } (share = twin ratio).
function apSrcSplit(n) {
  const s = window.__TWIN_SPLIT;
  if (!s || n == null) return null;
  const t = Math.round(n * (s.share || 0.667));
  return { t, h: n - t };
}
// Composition popover shown on hover/focus of the respondent-count badge.
// Deliberately monochrome — no chart hues — so it never reads as a series.
function ApSamplePop({ n, sp }) {
  const pct = (v) => Math.round((v / n) * 100);
  const anchor = React.useRef(null);
  const [pos, setPos] = React.useState(null);
  React.useEffect(() => {
    const wrap = anchor.current && anchor.current.parentElement;
    if (!wrap) return;
    const W = 252;
    const open = () => {
      const r = wrap.getBoundingClientRect();
      const left = Math.max(10, Math.min(r.left, window.innerWidth - W - 10));
      setPos({ left, top: r.bottom + 7, ax: Math.max(8, Math.min(W - 18, r.left - left + 10)) });
    };
    const close = () => setPos(null);
    wrap.addEventListener("mouseenter", open);
    wrap.addEventListener("mouseleave", close);
    wrap.addEventListener("focusin", open);
    wrap.addEventListener("focusout", close);
    window.addEventListener("scroll", close, true);
    return () => {
      wrap.removeEventListener("mouseenter", open);
      wrap.removeEventListener("mouseleave", close);
      wrap.removeEventListener("focusin", open);
      wrap.removeEventListener("focusout", close);
      window.removeEventListener("scroll", close, true);
    };
  }, []);
  return (
    <React.Fragment>
      <i className="ap-pop__anchor" ref={anchor} aria-hidden="true"></i>
      {pos && (
    <span className="ap-pop" role="tooltip" style={{ left: pos.left, top: pos.top, "--ax": pos.ax + "px" }}>
      <span className="ap-pop__hd"><b>{n.toLocaleString()}</b> respondents in this read</span>
      <span className="ap-pop__bar">
        <i className="ap-pop__seg ap-pop__seg--t" style={{ width: pct(sp.t) + "%" }}></i>
        <i className="ap-pop__seg ap-pop__seg--h" style={{ width: pct(sp.h) + "%" }}></i>
      </span>
      <span className="ap-pop__r">
        <i className="ap-pop__key ap-pop__key--t"></i>
        <span className="ap-pop__l">Digital Twins</span>
        <b>{sp.t.toLocaleString()}</b><em>{pct(sp.t)}%</em>
      </span>
      <span className="ap-pop__r">
        <i className="ap-pop__key ap-pop__key--h"></i>
        <span className="ap-pop__l">Human respondents</span>
        <b>{sp.h.toLocaleString()}</b><em>{pct(sp.h)}%</em>
      </span>
      <span className="ap-pop__ft">Twins are calibrated on verified customer interviews and validated against the human cell.</span>
    </span>
      )}
    </React.Fragment>
  );
}
window.ApSamplePop = ApSamplePop;
function ApSrcChip() { return null; }
// Credibility method rail (opt-in via window.__METHOD_RAIL) — Insights only.
function ApMethodRail() {
  const rows = [
    ["chart-scatter", "40,412 data cuts", "8 data-science agents tested every segment and split"],
    ["flask", "312 significance checks", "q < 0.001 after multiple-comparison correction"],
    ["books", "176M consumer responses", "the MakerSights data foundation every read is benchmarked against"],
    ["hanger", "91,000+ products and concepts", "analyzed across MakerSights studies"],
  ];
  const [open, setOpen] = React.useState(true);
  return (
    <aside className={"ap-mrail" + (open ? "" : " is-collapsed")}>
      <div className="ap-mrail__hd">
        <button className="ap-mrail__collapse" onClick={() => setOpen(v => !v)} aria-expanded={open} aria-label={open ? "Collapse method panel" : "Show method panel"} title={open ? "Collapse" : "Behind these insights"}><i className="ti ti-layout-sidebar-right"></i></button>
        {open && <span className="ap-mrail__h">Behind these insights</span>}
      </div>
      {open && (
        <div className="ap-mrail__body">
          <p className="ap-mrail__s">Every finding on this page passed the same pipeline before you saw it.</p>
          {rows.map(([ic, t, s]) => (
            <div className="ap-mrail__r" key={t}><i className={"ti ti-" + ic}></i><span><b>{t}</b>{s}</span></div>
          ))}
          <button type="button" className="ap-mrail__link">Read the full method<i className="ti ti-arrow-right"></i></button>
        </div>
      )}
    </aside>
  );
}
function ApSegBar({ segs, n, link, from, products, verdict }) {
  const hasSegs = !!(segs && segs.length);
  if (!hasSegs && n == null && !link && products == null && !verdict) return null;
  return (
    <div className="ap-seg">
      {verdict && <VerdictChip verdict={verdict} />}
      <ApNBadge n={n} />
      <ApSrcChip n={n} />
      {hasSegs && segs.map((s, i) => <ApSegChip key={i} s={s} />)}
      {products != null && (
        <span className="bdg outlined secondary squared ap-nbadge ap-prodbadge">
          <i className="ti ti-shirt ap-nbadge__ic"></i>
          <span className="ap-nbadge__v">{products}</span>
          <span className="ap-nbadge__k">{products === 1 ? "product" : "products"}</span>
        </span>
      )}
      <ApSegTools />
      {link && <div className="ap-seg__trace"><ApTrace link={link} from={from} /></div>}
    </div>
  );
}

// =========================================================
// Finding viz — one component per source type.
// =========================================================

// Line efficiency: TURF waterfall.
//   gray  bar = cumulative incremental reach (running total)
//   purple bar = the incremental reach this product adds, floating from the
//                prior cumulative to the new cumulative.
// X-axis pairs each column with the product image, name and sentiment score.
const LE_PLOT_H = 226;          // px height of the plotting region
const LE_TICKS = [100, 75, 50, 25, 0];

function VizLineEfficiency({ f, hl }) {
  const { study, bench } = window.useFindingsStudyBench
    ? window.useFindingsStudyBench()
    : { study: null, bench: null };

  // per-product sentiment score (0–100), keyed by product id
  const scoreById = React.useMemo(() => {
    const m = {};
    if (study && window.StudyData && window.csScore) {
      window.StudyData.aggregateAllProducts(study.respondents)
        .filter((p) => p.n > 0)
        .forEach((p) => { m[p.id] = window.csScore(p.meanRating); });
    }
    return m;
  }, [study]);

  // running cumulative; scale tops out at 100
  let run = 0;
  const cols = f.rows.map((r) => {
    const prev = run;
    run = Math.min(100, run + r.val);
    return { ...r, incr: r.val, prevCum: prev, cum: run };
  });
  const yPct = (v) => (v / 100) * LE_PLOT_H;
  const greenCut = bench ? bench.mean : 52;

  return (
    <div className="ap-find ap-find--le">
      <div className="ap-find__head">
        <span className="ap-find__title">Incremental reach across the full line</span>
      </div>
      <ApSegBar segs={[{ all: true }]} n={f.n} link={f.link} from={f.rawId} products={f.rows ? f.rows.length : null} verdict={f.verdict} />

      <div className="le-legend">
        <span className="le-legend__item"><i className="le-legend__sw le-legend__sw--incr"></i>Incremental reach</span>
        <span className="le-legend__item"><i className="le-legend__sw le-legend__sw--cum"></i>Cumulative incremental reach</span>
      </div>

      <div className="le-plotwrap">
        <div className="le-yaxis" style={{ height: LE_PLOT_H }}>
          {LE_TICKS.map((t) => <span key={t} className="le-ytick" style={{ top: (LE_PLOT_H * (1 - t / 100)) + "px" }}>{t}</span>)}
        </div>
        <div className="le-scroll">
          <div className="le-cols">
            <div className="le-gridlines" style={{ height: LE_PLOT_H }}>
              {LE_TICKS.map((t) => <span key={t} className="le-gridline" style={{ top: (LE_PLOT_H * (1 - t / 100)) + "px" }}></span>)}
            </div>
            {cols.map((c, i) => {
              const score = scoreById[c.id];
              const hi = score != null && score >= greenCut;
              const img = apImg(c.id);
              const showGray = i > 0;
              const connect = i < cols.length - 1;
              return (
                <div className="le-col" key={c.label}>
                  <div className="le-plot" style={{ height: LE_PLOT_H }}>
                    {showGray && (
                      <div className="le-bar le-bar--gray" style={{ height: yPct(c.cum) + "px" }}>
                        <span className="le-bar__cum">{Math.round(c.cum)}</span>
                      </div>
                    )}
                    <div className="le-bar le-bar--purple" style={{ bottom: yPct(c.prevCum) + "px", height: Math.max(6, yPct(c.incr)) + "px" }}></div>
                    <span className="le-incr" style={{ bottom: (yPct(c.cum) + 6) + "px" }}>{c.incr}</span>
                    {connect && <span className="le-connect" style={{ bottom: yPct(c.cum) + "px" }}></span>}
                  </div>
                  <div className="le-foot">
                    <div className="le-foot__row">
                      <span className="le-foot__thumb">
                        {img ? <img src={img} alt={c.label} loading="lazy" /> : <span className="le-foot__x">{c.label.slice(0, 2)}</span>}
                        {score != null && <span className={"le-score" + (hi ? " le-score--hi" : "")}>{score}</span>}
                      </span>
                    </div>
                    <div className="le-foot__name" title={c.label}>{apHi(c.label, hl)}</div>
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      </div>

    </div>
  );
}

// highlight matched search terms inside any viz text node (search active only)
function apHi(text, terms) {
  if (!terms || !terms.length || text == null) return text;
  const str = String(text), lower = str.toLowerCase();
  let ranges = [];
  terms.forEach((t) => { if (!t) return; const tl = String(t).toLowerCase(); let from = 0, i; while ((i = lower.indexOf(tl, from)) >= 0) { ranges.push([i, i + tl.length]); from = i + tl.length; } });
  if (!ranges.length) return str;
  ranges.sort((a, b) => a[0] - b[0]);
  const merged = [ranges[0]];
  for (let k = 1; k < ranges.length; k++) { const last = merged[merged.length - 1]; if (ranges[k][0] <= last[1]) last[1] = Math.max(last[1], ranges[k][1]); else merged.push(ranges[k]); }
  const out = []; let pos = 0;
  merged.forEach(([s, e], k) => { if (s > pos) out.push(str.slice(pos, s)); out.push(<mark key={k} className="apx-mk">{str.slice(s, e)}</mark>); pos = e; });
  if (pos < str.length) out.push(str.slice(pos));
  return out;
}

// Advocate / detractor: attribute drivers of like / dislike for one product.
function VizAdvDet({ f, hl }) {
  const pos = f.polarity === "positive";
  const img = apImg(f.productId);
  const max = Math.max(...f.rows.map((r) => r.pct), 1);
  return (
    <div className="ap-find">
      <div className="ap-find__head ap-find__head--prod">
        <span className="ap-find__thumb">
          {img ? <img src={img} alt={f.product} loading="lazy" /> : <span className="ap-find__thumb-x">{(f.product || "?").slice(0, 2)}</span>}
        </span>
        <span className="ap-find__prodmeta">
          <span className="ap-find__title">{apHi(f.product, hl)}</span>
          <span className={"ap-find__desc ap-find__desc--" + (pos ? "pos" : "neg")}>{pos ? "Liked most" : "Disliked most"}</span>
        </span>
      </div>
      <ApSegBar segs={[{ all: true }]} n={f.totalN} link={f.link} from={f.rawId} products={1} />
      <div className="ap-bars ap-bars--attr">
        {f.rows.map((r, i) => (
          <div className="ap-bar" key={r.label}>
            <span className="ap-bar__label ap-bar__label--attr" title={r.label}>{apHi(r.label, hl)}</span>
            <span className="ap-bar__track">
              <span className={"ap-bar__fill ap-bar__fill--" + (pos ? (i === 0 ? "pos-lead" : "pos") : (i === 0 ? "neg-lead" : "neg"))}
                style={{ width: Math.max(1.5, (r.pct / max) * 100) + "%" }}></span>
            </span>
            <span className="ap-bar__val">{r.pct}%<span className="ap-bar__count"> ({r.count})</span></span>
          </div>
        ))}
      </div>
    </div>
  );
}

const TIER_META = {
  AboveTop:    { label: "Top tier", cls: "top" },
  Between:     { label: "Mid",      cls: "mid" },
  BelowBottom: { label: "Bottom tier", cls: "low" },
};

// Sentiment: products ranked into tiers for one segment.
function VizSentiment({ f, hl }) {
  const segN = useSegN(f.link);
  return (
    <div className="ap-find">
      <div className="ap-find__head">
        <span className="ap-find__title">Product sentiment ranking</span>
      </div>
      <ApSegBar segs={[{ dim: SEG_DIM_SHORT[f.field] || f.field, value: f.segValue, full: f.field + " · " + f.segValue }]} n={segN} verdict={f.verdict} link={f.link} from={f.rawId} products={f.tiers.reduce((s, t) => s + (t.items ? t.items.length : 0), 0)} />
      <div className="ap-tiers">
        {f.tiers.map((t) => {
          const meta = TIER_META[t.tier] || { label: t.tier, cls: "mid" };
          return (
            <div className="ap-tier" key={t.tier}>
              <div className={"ap-tier__label ap-tier__label--" + meta.cls}>{meta.label}</div>
              <div className="ap-tiles">
                {t.items.map((it) => {
                  const img = apImg(it.id);
                  return (
                    <div className="ap-tile" key={it.label}>
                      <div className="ap-tile__card">
                        {img ? <img src={img} alt={it.label} loading="lazy" /> : <span className="ap-tile__x">{it.label.slice(0, 2)}</span>}
                        <span className={"ap-tile__badge ap-tile__badge--" + meta.cls}>{Math.round(it.score)}</span>
                      </div>
                      <div className="ap-tile__name">{apHi(it.label, hl)}</div>
                      <div className="ap-tile__pop" role="tooltip">
                        <div className="ap-tile__pop-img">
                          {img ? <img src={img} alt={it.label} loading="lazy" /> : <span className="ap-tile__x">{it.label.slice(0, 2)}</span>}
                          <span className={"ap-tile__badge ap-tile__badge--" + meta.cls}>{Math.round(it.score)}</span>
                        </div>
                        <div className="ap-tile__pop-name">{it.label}</div>
                      </div>
                    </div>
                  );
                })}
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// Single stat (e.g. household income share).
function VizStat({ f, hl }) {
  return (
    <div className="ap-find">
      <div className="ap-find__head">
        <span className="ap-find__title">{apHi(f.question, hl)}</span>
      </div>
      <ApSegBar segs={[{ all: true }]} n={f.n} link={f.link} from={f.rawId} verdict={f.verdict} />
      <div className="ap-stats">
        {f.rows.map((r) => (
          <div className="ap-stat" key={r.label}>
            <span className="ap-stat__val">{r.pct}%</span>
            <span className="ap-stat__label">{apHi(r.label, hl)}</span>
            <span className="ap-stat__count">{r.count} respondents</span>
          </div>
        ))}
      </div>
    </div>
  );
}

// Bucket distribution (source=bucket, scope=question): horizontal bar chart.
// Each track is the full 100%; brand-green fill = the option's share.
function apVerdictKind(v) {
  if (!v) return null;
  const s = String(v).toLowerCase();
  if (s.indexOf("did not support") >= 0) return { k: "incon", label: "Inconclusive" };
  if (s.indexOf("descriptive") >= 0) return { k: "desc", label: "Descriptive" };
  if (s.indexOf("equivalent") >= 0 || s.indexOf("no meaningful difference") >= 0 || s.indexOf("no difference") >= 0) return { k: "equiv", label: "No difference" };
  if (s.indexOf("strong effect") >= 0 || s.indexOf("q<0.001") >= 0) return { k: "strong", label: "Strong effect" };
  if (s.indexOf("meaningful difference") >= 0 || s.indexOf("effect") >= 0) return { k: "effect", label: "Meaningful" };
  return { k: "effect", label: "Tested" };
}
function VerdictChip({ verdict }) {
  const t = apVerdictKind(verdict);
  if (!t) return null;
  return <span className={"ap-vchip ap-vchip--" + t.k} title={verdict} data-tip={verdict} tabIndex={0}><i className="ti ti-flag-3-filled"></i>{t.label}</span>;
}
function VizBarList({ f, hl }) {
  const rows = f.rows || [];
  return (
    <div className="ap-find">
      <div className="ap-find__head"><span className="ap-find__title">{apHi(f.question, hl)}</span></div>
      <ApSegBar segs={[{ all: true }]} n={f.n} link={f.link} from={f.rawId} verdict={f.verdict} />
      <div className="ap-bl">
        {rows.map((r) => (
          <div className="ap-bl__row" key={r.label}>
            <div className="ap-bl__lab" title={r.label}>{apHi(r.label, hl)}</div>
            <div className="ap-bl__track">
              <div className="ap-bl__fill" style={{ width: Math.max(0, r.pct) + "%" }}>
                {r.pct >= 7 && <span className="ap-bl__val">{r.pct}%</span>}
              </div>
              {r.pct < 7 && <span className="ap-bl__val ap-bl__val--out" style={{ left: "calc(" + r.pct + "% + 7px)" }}>{r.pct}%</span>}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// =========================================================
// Matrix — the range-plan and channel tables. One cell = one segment score,
// toned green / slate / yellow against the table's own distribution so the
// colour pattern carries the argument before any number is read.
// =========================================================
function VizMatrix({ f, hl }) {
  const cols = f.cols || [];
  const toned = (f.rows || []).some((r) => (r.cells || []).some((c) => c.tone && c.tone !== "text"));
  return (
    <div className="ap-find">
      <div className="ap-find__head"><span className="ap-find__title">{apHi(f.question, hl)}</span></div>
      <ApSegBar segs={[{ all: true }]} n={f.n} link={f.link} from={f.rawId} verdict={f.verdict} />
      <div className="ap-mx">
        {f.sub && <p className="ap-mx__sub">{f.sub}</p>}
        <div className="ap-mx__scroll">
          <table>
            <thead>
              <tr>
                <th className="ap-mx__rowhead">{f.rowLabel || "Colorway"}</th>
                {cols.map((c) => <th key={c.key} style={c.w ? { width: c.w } : null}>{c.label}</th>)}
              </tr>
            </thead>
            <tbody>
              {(f.rows || []).map((r, i) => (
                <tr key={r.label + i}>
                  <td className="ap-mx__rowhead">
                    <span className="ap-mx__cell">
                      {r.id && apImg(r.id)
                        ? <span className="ap-mx__thumb"><img src={apImg(r.id)} alt="" loading="lazy" /></span>
                        : null}
                      <span className="ap-mx__lab">{apHi(r.label, hl)}</span>
                    </span>
                  </td>
                  {(r.cells || []).map((c, j) => {
                    const isText = c.kind === "note" || c.kind === "slot" || typeof c.v === "string" && c.kind === "product";
                    if (c.kind === "product") {
                      return (
                        <td key={j} className="t-text">
                          <span className="ap-mx__cell">
                            {apImg(c.id) ? <span className="ap-mx__thumb"><img src={apImg(c.id)} alt="" loading="lazy" /></span> : null}
                            <span className="ap-mx__slot">{c.v}</span>
                          </span>
                        </td>
                      );
                    }
                    if (isText) return <td key={j} className={"t-text" + (c.kind === "slot" ? " is-slot" : "")}>{c.v}</td>;
                    return <td key={j} className={"t-" + (c.tone || "mid")}>{c.v}</td>;
                  })}
                </tr>
              ))}
            </tbody>
          </table>
        </div>
        {toned && (
          <div className="ap-mx__legend">
            <b><i className="ap-mx__sw ap-mx__sw--good"></i>Strong fit</b>
            <b><i className="ap-mx__sw ap-mx__sw--mid"></i>Moderate</b>
            <b><i className="ap-mx__sw ap-mx__sw--weak"></i>Weak fit</b>
          </div>
        )}
        {f.note && <div className="ap-mx__note">{f.note}</div>}
      </div>
    </div>
  );
}

function FindingViz({ f, hl }) {
  if (f.kind === "matrix") return <VizMatrix f={f} hl={hl} />;
  if (f.kind === "line_efficiency") return <VizLineEfficiency f={f} hl={hl} />;
  if (f.kind === "advdet") return <VizAdvDet f={f} hl={hl} />;
  if (f.kind === "sentiment") return <VizSentiment f={f} hl={hl} />;
  if (f.kind === "barlist") return <VizBarList f={f} hl={hl} />;
  if (f.kind === "stat") return <VizStat f={f} hl={hl} />;
  return null;
}

// =========================================================
// Advocate / detractor HEATMAP — condenses a run of per-product
// like-most / dislike-most findings into one table:
//   header row = each product (image · name · sentiment score)
//   left column = attributes; cells = % shaded as a heatmap.
// =========================================================
const ATTR_ORDER = [
  "Design Details", "Material", "Color", "Silhouette/Shape",
  "Print/Pattern", "Versatility (can wear for multiple occasions)", "Fit", "Price",
];
const ATTR_SHORT = { "Versatility (can wear for multiple occasions)": "Versatility" };

function AdvDetHeatmap({ findings, polarity }) {
  const store = React.useContext(PinContext);
  const tools = React.useContext(ApToolsCtx);
  const sort = (tools && tools.sort) || "rank";
  const cap = (tools && tools.cap) || "all";
  const { study, bench } = window.useFindingsStudyBench
    ? window.useFindingsStudyBench()
    : { study: null, bench: null };

  const scoreById = React.useMemo(() => {
    const m = {};
    if (study && window.StudyData && window.csScore) {
      window.StudyData.aggregateAllProducts(study.respondents).filter((p) => p.n > 0)
        .forEach((p) => { m[p.id] = window.csScore(p.meanRating); });
    }
    return m;
  }, [study]);
  const greenCut = bench ? bench.mean : 52;
  const pos = polarity === "positive";

  // attribute rows present across the findings, in canonical order
  const present = new Set();
  findings.forEach((f) => f.rows.forEach((r) => present.add(r.label)));
  const avgOf = (a) => {
    const vals = findings.map((f) => { const r = f.rows.find((x) => x.label === a); return r ? r.pct : null; }).filter((v) => v != null);
    return vals.length ? vals.reduce((s, v) => s + v, 0) / vals.length : 0;
  };
  const attrs = (() => {
    const list = [...present].sort((a, b) => sort === "az"
      ? String(ATTR_SHORT[a] || a).localeCompare(String(ATTR_SHORT[b] || b))
      : sort === "asc" ? avgOf(a) - avgOf(b) : avgOf(b) - avgOf(a));
    return cap === "top5" ? list.slice(0, 5) : list;
  })();

  // value lookup product→attr→pct
  const val = (f, attr) => { const r = f.rows.find((x) => x.label === attr); return r ? r.pct : null; };
  const maxPct = Math.max(1, ...findings.flatMap((f) => f.rows.map((r) => r.pct)));

  const displayFindings = findings;

  function cellStyle(pct) {
    if (pct == null) return { background: "var(--ms-white)" };
    const a = 0.12 + (pct / maxPct) * 0.78;
    if (pos) {
      return { background: "rgba(74,138,46," + a.toFixed(3) + ")", color: a > 0.55 ? "#fff" : "var(--ms-ink-2)" };
    }
    // negative → design-system yellow (warm) rather than red; keep dark text for legibility
    return { background: "rgba(214,157,43," + a.toFixed(3) + ")", color: a > 0.72 ? "var(--ms-clay-deep)" : "var(--ms-ink-2)" };
  }

  return (
    <div className="ap-find ap-find--heat" style={{ position: "relative" }}>
      <div className="ap-find__head">
        <span className="ap-find__title">{pos ? "Liked most" : "Disliked most"}</span>
        <span className="ap-find__desc">Drivers by attribute · {findings.length} {findings.length === 1 ? "product" : "products"}</span>
      </div>
      <ApSegBar segs={[{ all: true }]} products={findings.length} link={findings[0] && {
        section: "product",
        productId: findings[0].productId,
        pqTitle: findings[0].question,
        filter: { demo: {}, answers: {} },
        sourceLabel: "Product section",
        cutLabel: findings[0].question,
        statement: findings[0].question,
      }} from={findings[0] && findings[0].rawId} />
      <div className="ap-heatscroll">
        <table className="ap-heat">
          <thead>
            <tr>
              <th className="ap-heat__corner"></th>
              {displayFindings.map((f, i) => {
                const img = apImg(f.productId);
                const score = scoreById[f.productId];
                const hi = score != null && score >= greenCut;
                return (
                  <th key={f.productId} className="ap-heat__prodh">
                    <button className="ap-heat__prod" onClick={() => store && store.navigateToData && f.link && store.navigateToData(f.link)}
                      title={"Explore the data — " + f.product}>
                      <span className="ap-heat__thumb">
                        {img ? <img src={img} alt={f.product} loading="lazy" /> : <span className="le-foot__x">{(f.product || "?").slice(0, 2)}</span>}
                        {score != null && <span className={"le-score" + (hi ? " le-score--hi" : "")}>{score}</span>}
                      </span>
                      <span className="ap-heat__pname">{f.product}</span>
                    </button>
                  </th>
                );
              })}
            </tr>
          </thead>
          <tbody>
            {attrs.map((attr) => (
              <tr key={attr}>
                <th className="ap-heat__attr" scope="row">{ATTR_SHORT[attr] || attr}</th>
                {displayFindings.map((f, i) => {
                  const pct = val(f, attr);
                  return (
                    <td key={f.productId} className="ap-heat__cell" style={cellStyle(pct)}>
                      {pct == null ? <span className="ap-heat__na">·</span> : pct + "%"}
                    </td>
                  );
                })}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

// =========================================================
// Product gallery — for findings scoped to a single product. The product
// IMAGE is the dominant visual; the finding's bars/stats sit underneath as
// supporting detail. Consecutive product-scoped findings lay out as a grid.
// =========================================================
function VizProductCard({ card }) {
  const img = apImg(card.productId);
  const liveScores = useProductScores();
  const entry = liveScores[card.productId];
  const raw = entry ? entry.score : apSentiment[card.productId];
  const score = raw != null ? Math.round(raw) : null;
  const tone = entry ? entry.tier : "mid";
  return (
    <div className="ap-gcard">
      <div className="ap-gcard__img">
        {img ? <img src={img} alt={card.product} loading="lazy" /> : <span className="ap-gcard__x">{(card.product || "?").slice(0, 2)}</span>}
        {score != null && <span className={"ap-gcard__score ap-gcard__score--" + tone} title="Purchase-intent sentiment score">{score}</span>}
      </div>
      <div className="ap-gcard__body">
        <div className="ap-gcard__name">{card.product}</div>
        {card.items.map((f, i) => (
          <div className="ap-gcard__sec" key={i}>
            {(f.sectionLabel || f.question) && !f.pctl && <div className="ap-gcard__seclab">{f.sectionLabel || f.question}</div>}
            {f.pctl
              ? (() => {
                  const pos = (f.rows[0] && f.rows[0].label) || "", pctl = (f.rows[0] && f.rows[0].pct) || 0;
                  const l = pos.toLowerCase();
                  const tone = l.indexOf("above") >= 0 ? "above" : l.indexOf("below") >= 0 ? "below" : "within";
                  const v = pctl % 100, suf = (v >= 11 && v <= 13) ? "th" : ["th", "st", "nd", "rd"][pctl % 10] || "th";
                  return (
                    <div className="ap-pctl-row">
                      <span className="ap-pctl__pos">{pos}</span>
                      <span className={"ap-pctl ap-pctl--" + tone}><strong className="ap-pctl__n">{pctl}<sup>{suf}</sup></strong> percentile</span>
                    </div>
                  );
                })()
              : f.split
              ? (() => {
                  const adv = (f.rows[0] && f.rows[0].pct) || 0, det = (f.rows[1] && f.rows[1].pct) || 0;
                  return (
                    <div className="ap-sp">
                      <div className="ap-sp__track">
                        <div className="ap-sp__seg ap-sp__seg--adv" style={{ width: adv + "%" }}>{adv >= 12 && <span>{adv}%</span>}</div>
                        <div className="ap-sp__seg ap-sp__seg--det" style={{ width: det + "%" }}>{det >= 12 && <span>{det}%</span>}</div>
                      </div>
                    </div>
                  );
                })()
              : (() => {
                  const lab = (f.sectionLabel || "").toLowerCase();
                  const pol = /detractor/.test(lab) ? "det" : /advocate/.test(lab) ? "adv" : "bucket";
                  return (
                    <div className={"ap-bars ap-bars--attr ap-gcard__bars ap-bars--" + pol}>
                      {(f.rows || []).map((r) => (
                        <div className="ap-bar" key={r.label}>
                          <span className="ap-bar__label ap-bar__label--attr" title={r.label}>{r.label}</span>
                          <span className="ap-bar__track"><span className={"ap-bar__fill ap-bar__fill--" + pol} style={{ width: Math.max(1.5, r.pct) + "%" }}></span></span>
                          <span className="ap-bar__val">{r.pct}%</span>
                        </div>
                      ))}
                    </div>
                  );
                })()}
            {f.verdict && <div className="ap-gcard__verdict"><VerdictChip verdict={f.verdict} /></div>}
          </div>
        ))}
      </div>
    </div>
  );
}
function VizGallery({ findings }) {
  const order = [], byId = {};
  (findings || []).forEach((f) => { const k = f.productId; if (!byId[k]) { byId[k] = { product: f.product, productId: k, items: [] }; order.push(k); } byId[k].items.push(f); });
  const cards = order.map((k) => byId[k]);
  // Column count per finding: keep a finding's products together and never leave
  // a lone orphan in the bottom row. 1–3 products => that many columns (3 always
  // share one row); 4+ => most columns (max 4) whose remainder is 0 or >=2.
  const n = cards.length;
  const cols = n <= 3 ? n : ([3, 2].find((c) => n % c === 0 || n % c >= 2) || 3);
  const labels = [...new Set((findings || []).map((f) => f.sectionLabel || f.question).filter(Boolean))];
  const head = findings && findings[0];
  return (
    <div className="ap-find ap-find--gallery">
      <div className="ap-find__head">
        <span className="ap-find__title">{labels.join(" · ")}</span>
        <span className="ap-find__desc">{cards.length} {cards.length === 1 ? "colorway" : "colorways"}</span>
      </div>
      <ApSegBar segs={[{ all: true }]} n={head && head.totalN} link={head && head.link} from={head && head.rawId} products={cards.length} />
      <div className="ap-gallery" style={{ "--ap-gal-cols": String(cols) }}>
        {cards.map((c) => <VizProductCard key={c.productId} card={c} />)}
      </div>
    </div>
  );
}

// Walk a findings list, batching consecutive product-scoped findings into a
// gallery block (image-dominant cards), and consecutive advocate/detractor
// findings of the same polarity into a heatmap block (non-product studies).
function groupFindings(findings) {
  const blocks = [];
  let heat = null, gal = null;
  findings.forEach((f) => {
    if (f.scope === "product" && f.productId) {
      heat = null;
      if (gal) gal.findings.push(f);
      else { gal = { type: "gallery", findings: [f] }; blocks.push(gal); }
      return;
    }
    gal = null;
    if (f.kind === "advdet") {
      if (heat && heat.polarity === f.polarity) heat.findings.push(f);
      else { heat = { type: "heat", polarity: f.polarity, findings: [f] }; blocks.push(heat); }
    } else { heat = null; blocks.push({ type: "finding", f }); }
  });
  return blocks;
}

// =========================================================
// Finding curation (Direction A) — CX surface, in-session.
// Pin the dominant finding (folded default), hide off-base ones
// (sink into a dimmed "Hidden from client" group). Heatmap hide
// opens a per-product dropdown. Replaces relevant/off-base rating.
// =========================================================
const FCX_PIN = <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 4v6l-2 4v2h10v-2l-2-4V4"/><path d="M12 16v5"/><path d="M8 4h8"/></svg>;

function useArgCuration(items) {
  const [pinned, setPinned] = React.useState(items[0] ? items[0].key : null);
  const [hidden, setHidden] = React.useState(() => new Set());
  const [hiddenProd, setHiddenProd] = React.useState(() => new Set());
  const pin = (k) => setPinned(k);
  const toggleHide = (k) => setHidden((prev) => {
    const n = new Set(prev);
    if (n.has(k)) n.delete(k);
    else { n.add(k); if (k === pinned) { const nv = items.find((it) => it.key !== k && !n.has(it.key)); if (nv) setPinned(nv.key); } }
    return n;
  });
  const toggleProd = (id) => setHiddenProd((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; });
  return { pinned, hidden, hiddenProd, pin, toggleHide, toggleProd };
}

function CurationControls({ item, cur }) {
  const [open, setOpen] = React.useState(false);
  React.useEffect(() => {
    if (!open) return;
    const close = (e) => { if (!e.target.closest(".fc-ctrls")) setOpen(false); };
    document.addEventListener("mousedown", close);
    return () => document.removeEventListener("mousedown", close);
  }, [open]);
  const pinned = item.key === cur.pinned;
  const hidden = cur.hidden.has(item.key);
  const isHeat = item.kind === "heat";
  return (
    <div className="fc-ctrls">
      {!pinned && <button className="fc-ib" data-tip="Pin to top" onClick={() => cur.pin(item.key)}>{FCX_PIN}</button>}
      <button className={"fc-ib fc-ib--danger" + (hidden ? " is-hidden" : "")}
        data-tip={isHeat ? "Hide products" : (hidden ? "Restore for client" : "Hide from client")}
        onClick={() => (isHeat ? setOpen((o) => !o) : cur.toggleHide(item.key))}>
        <i className={"ti " + (hidden ? "ti-eye" : "ti-eye-off")}></i></button>
      {isHeat && open && (
        <div className="fc-menu">
          <button className="fc-menu__item fc-menu__item--danger" onClick={() => { cur.toggleHide(item.key); setOpen(false); }}>
            <i className={"ti " + (hidden ? "ti-eye" : "ti-eye-off")}></i>{hidden ? "Restore whole chart" : "Hide whole chart"}
          </button>
          <div className="fc-menu__sub">Or hide a single product</div>
          {item.block.findings.map((f) => {
            const off = cur.hiddenProd.has(f.productId);
            const img = apImg(f.productId);
            return (
              <button key={f.productId} className="fc-menu__item" onClick={() => cur.toggleProd(f.productId)}>
                <span className="fc-menu__prod">{img ? <img src={img} alt="" /> : <i className="ti ti-package"></i>}{f.product}</span>
                {off && <i className="ti ti-eye-off fc-menu__check" style={{ color: "var(--chart-red)" }}></i>}
              </button>
            );
          })}
        </div>
      )}
    </div>
  );
}

// one finding block — measures its segment bar so the controls align
// onto that line, just left of "Explore the data".
function CurationBlock({ item, cur, hidden, findId, view }) {
  const ref = React.useRef(null);
  React.useLayoutEffect(() => {
    const el = ref.current; if (!el) return;
    const seg = el.querySelector(".ap-seg");
    if (seg) { const s = seg.getBoundingClientRect(), e = el.getBoundingClientRect(); el.style.setProperty("--fc-seg-top", (s.top - e.top + s.height / 2 - 16) + "px"); }
    const trace = el.querySelector(".ap-seg__trace");
    if (trace) {
      const br = el.getBoundingClientRect(), tr = trace.getBoundingClientRect();
      el.style.setProperty("--fc-ctrls-right", (br.right - tr.left + 2) + "px");
    }
  });
  const b = item.block;
  const heatFinds = item.kind === "heat" ? b.findings.filter((f) => !cur.hiddenProd.has(f.productId)) : null;
  return (
    <div ref={ref} id={findId ? "find-" + findId : undefined} className={"ap-findblock" + (hidden ? " fc-block--hidden" : "")}>
      {view !== "client" && <CurationControls item={item} cur={cur} />}
      {item.kind === "heat"
        ? <AdvDetHeatmap findings={heatFinds} polarity={b.polarity} />
        : item.kind === "gallery"
        ? <VizGallery findings={b.findings} />
        : <FindingViz f={b.f} />}
    </div>
  );
}

// ---- Claim-level hover affordances (confidence + report) ----
// A confidence badge (three tiers) plus a chrome-free report action, both
// revealed on hover of the claim they belong to: a key-takeaways block or an
// argument card. Follow-up interactions land later — clicks are inert for now.
const AP_CONF_TIERS = {
  high: { label: "High confidence", short: "High confidence" },
  mid:  { label: "Solid", short: "Solid" },
  low:  { label: "Directional", short: "Directional" },
};
// Placeholder tiering fallback (unused once signals derive the tier).
function apConfTier(id) {
  let h = 0;
  for (let i = 0; i < String(id).length; i++) h = (h * 31 + String(id).charCodeAt(i)) % 997;
  return h % 5 === 0 ? "low" : h % 3 === 0 ? "mid" : "high";
}
// The two eval signals behind the badge. Faithfulness = is the generated claim
// grounded in the raw data (higher is better). Tension = how much the evidence
// underneath disagrees (lower is better). The tier is DERIVED: both signals in
// the good band = high, one = moderate, neither = low.
// Placeholder data, but assigned in render order so the mock shows a clean
// spread of all three tiers.
const AP_FAITH_GOOD = 85;   // >= is green
const AP_TENSION_GOOD = 30; // <= is green
const AP_TIER_PATTERN = ["high", "mid", "low", "high", "high", "low", "mid", "high", "mid", "low", "high", "mid"];
const apClaimSlot = new Map();
function apSlotOf(id) {
  if (!apClaimSlot.has(id)) apClaimSlot.set(id, apClaimSlot.size);
  return apClaimSlot.get(id);
}
function apConfSignals(id) {
  const slot = apSlotOf(id);
  const want = AP_TIER_PATTERN[slot % AP_TIER_PATTERN.length];
  let h = 7;
  for (let i = 0; i < String(id).length; i++) h = (h * 37 + String(id).charCodeAt(i)) % 9973;
  const pick = (lo, hi, salt) => lo + ((h + salt) % (hi - lo + 1));
  let faith, tension;
  if (want === "high") { faith = pick(88, 97, 0); tension = pick(7, 24, 11); }
  else if (want === "low") { faith = pick(63, 80, 3); tension = pick(44, 68, 17); }
  else if (slot % 2 === 0) { faith = pick(88, 96, 5); tension = pick(41, 58, 23); }
  else { faith = pick(71, 82, 9); tension = pick(11, 27, 29); }
  const faithGood = faith >= AP_FAITH_GOOD;
  const tensionGood = tension <= AP_TENSION_GOOD;
  const n = (faithGood ? 1 : 0) + (tensionGood ? 1 : 0);
  return { faith, tension, faithGood, tensionGood, tier: n === 2 ? "high" : n === 1 ? "mid" : "low" };
}
const AP_CONF_SAY = {
  high: { icon: "mood-happy", head: "You can run with this", body: "We'd get this same answer if we asked another 1,000 shoppers. The people who said it agreed with each other, and it's backed by plenty of them.", foot: "Safe to put in front of the room." },
  mid: { icon: "mood-neutral", head: "Right direction, worth a second look", body: "The pattern is real, but shoppers were a little split — or fewer of them weighed in. Good enough to shape a point of view, not to bet the line on." , foot: "Sanity-check it against what you're seeing in store." },
  low: { icon: "mood-look-down", head: "Treat this as a hunch", body: "Not many shoppers landed here, and they didn't fully agree. It's a lead worth chasing, not a conclusion.", foot: "Ask us to dig deeper before you act on it." }
};
function ConfidenceTip({ tier, label, sig }) {
  const say = AP_CONF_SAY[tier] || AP_CONF_SAY.high;
  return (
    <div className="ap-conftip ap-conftip--plain" role="tooltip">
      <div className={"ap-conftip__hd ap-conftip__hd--" + tier}>{say.head}</div>
      <div className="ap-conftip__body">{say.body}</div>
      <div className="ap-conftip__foot">{say.foot}</div>
    </div>
  );
}
// "badge" = the three tiered pills; "icon" = one neutral stethoscope that only
// reveals the two signals on hover. Set per render by ApparelInsights.
let apConfMode = "badge";
function ClaimActions({ scope, id, tier }) {
  const stop = (e) => { e.preventDefault(); e.stopPropagation(); };
  const sig = apConfSignals(scope + "::" + id);
  const t = tier || sig.tier;
  const meta = AP_CONF_TIERS[t] || AP_CONF_TIERS.high;
  const [open, setOpen] = React.useState(false);
  const [verdict, setVerdict] = React.useState(null);
  const [note, setNote] = React.useState("");
  const [sent, setSent] = React.useState(false);
  const wrapRef = React.useRef(null);
  // close on outside click / Escape; reset the form when it closes
  React.useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); };
    const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
    document.addEventListener("mousedown", onDoc);
    document.addEventListener("keydown", onKey);
    return () => { document.removeEventListener("mousedown", onDoc); document.removeEventListener("keydown", onKey); };
  }, [open]);
  React.useEffect(() => {
    if (open) return;
    const t2 = setTimeout(() => { setVerdict(null); setNote(""); setSent(false); }, 180);
    return () => clearTimeout(t2);
  }, [open]);
  function submit(e) {
    stop(e);
    setSent(true);
    setTimeout(() => setOpen(false), 1900);
  }
  return (
    <span className={"ap-claimacts" + (open ? " is-open" : "")} data-scope={scope} data-claim={id} ref={wrapRef}>
      <span className="ap-confwrap">
        {apConfMode === "icon" ? (
          <button type="button" className="ap-claimact ap-claimact--conf" onClick={stop}
            aria-label="Evidence check" title="">
            <i className="ti ti-stethoscope"></i>
          </button>
        ) : (
          <button type="button" className={"ap-confbadge ap-confbadge--" + t} onClick={stop}
            aria-label={meta.label} title="">
            <span className="ap-confbadge__txt">{meta.short}</span>
          </button>
        )}
        <ConfidenceTip tier={t} label={meta.label} sig={sig} />
      </span>
      <span className="ap-fbthumbs">
        <button type="button" className={"ap-fbthumb" + (open && verdict === "up" ? " is-on" : "")}
          onClick={(e) => { stop(e); setVerdict("up"); setOpen(true); }}
          aria-label="This looks right" title="This looks right">
          <i className="ti ti-thumb-up"></i>
        </button>
        <button type="button" className={"ap-fbthumb" + (open && verdict === "down" ? " is-on" : "")}
          onClick={(e) => { stop(e); setVerdict("down"); setOpen(true); }}
          aria-label="Something's off" title="Something's off">
          <i className="ti ti-thumb-down"></i>
        </button>
      </span>
      {open && (
        <div className="ap-fbpop" onClick={stop} onMouseDown={(e) => e.stopPropagation()}>
          {sent ? (
            <div className="ap-fbpop__done">
              <i className="ti ti-circle-check ap-fbpop__done-ic"></i>
              <div>
                <div className="ap-fbpop__done-t">Feedback submitted</div>
                <div className="ap-fbpop__done-s">Thanks — this goes to the analyst on this study.</div>
              </div>
            </div>
          ) : (
            <React.Fragment>
              <div className="ap-fbpop__head">
                <div className="ap-fbpop__lab">How do you like this {scope === "takeaways" ? "takeaway" : "headline"}?</div>
                <div className="ap-fbpop__rate">
                  <button type="button" className={"ap-fbpop__thumb ap-fbpop__thumb--up" + (verdict === "up" ? " is-on" : "")}
                    onClick={() => setVerdict(verdict === "up" ? null : "up")} aria-pressed={verdict === "up"}
                    aria-label="Looks right" title="Looks right">
                    <i className="ti ti-thumb-up"></i>
                  </button>
                  <button type="button" className={"ap-fbpop__thumb ap-fbpop__thumb--down" + (verdict === "down" ? " is-on" : "")}
                    onClick={() => setVerdict(verdict === "down" ? null : "down")} aria-pressed={verdict === "down"}
                    aria-label="Something's off" title="Something's off">
                    <i className="ti ti-thumb-down"></i>
                  </button>
                </div>
              </div>
              <textarea className="ap-fbpop__note" value={note} onChange={(e) => setNote(e.target.value)}
                placeholder={verdict === "down"
                  ? "What's off? e.g. the evidence doesn't support this for the UK sample."
                  : "Why? Optional, but it's what makes the feedback useful."} />
              <div className="ap-fbpop__foot">
                <button type="button" className="ap-fbpop__cancel" onClick={() => setOpen(false)}>Cancel</button>
                <button type="button" className="ap-fbpop__submit" disabled={!verdict} onClick={submit}>Submit</button>
              </div>
            </React.Fragment>
          )}
        </div>
      )}
    </span>
  );
}

function apSortArg(arg, sort, cap) {
  const fs = (arg.findings || []).map((f) => {
    if (f.kind === "line_efficiency" && f.rows) {
      let rows = f.rows.slice().sort((a, b) => sort === "asc" ? a.val - b.val : sort === "az" ? String(a.label).localeCompare(String(b.label)) : b.val - a.val);
      if (cap === "top5") rows = rows.slice(0, 5);
      return { ...f, rows };
    }
    if (f.kind === "sentiment" && f.tiers) {
      return { ...f, tiers: f.tiers.map((t) => {
        let items = (t.items || []).slice().sort((a, b) => sort === "asc" ? a.score - b.score : sort === "az" ? String(a.label).localeCompare(String(b.label)) : b.score - a.score);
        if (cap === "top5") items = items.slice(0, 5);
        return { ...t, items };
      }) };
    }
    if (f.kind === "advdet" && f.rows) {
      let rows = f.rows.slice().sort((a, b) => sort === "asc" ? a.pct - b.pct : sort === "az" ? String(a.label).localeCompare(String(b.label)) : b.pct - a.pct);
      if (cap === "top5") rows = rows.slice(0, 5);
      return { ...f, rows };
    }
    return f;
  });
  return { ...arg, findings: fs };
}
const AP_SORTS = [{ id: "rank", icon: "sort-descending", label: "Highest" }, { id: "asc", icon: "sort-ascending", label: "Lowest" }, { id: "az", icon: "sort-a-z", label: "A–Z" }];
const AP_CAPS = [{ id: "all", icon: "list", label: "All" }, { id: "top5", icon: "filter", label: "Top 5" }];
function ApDrop({ icon, prefix, value, options, onPick, isDefault }) {
  const [open, setOpen] = React.useState(false);
  const cur = options.find((o) => o.id === value) || options[0];
  return (
    <div className="ap-drop">
      <button className={"ap-tool ap-drop__btn" + (open ? " is-open" : "") + (isDefault ? "" : " on")} onClick={() => setOpen((v) => !v)} aria-haspopup="menu" aria-expanded={open}>
        <i className={"ti ti-" + icon}></i>
        {prefix && <span className="ap-drop__pre">{prefix}</span>}
        <span className="ap-drop__val">{cur.label}</span>
        <i className="ti ti-chevron-down ap-drop__chev" aria-hidden="true"></i>
      </button>
      {open && (
        <React.Fragment>
          <div className="ap-drop__scrim" onClick={() => setOpen(false)}></div>
          <div className="ap-drop__menu" role="menu">
            {options.map((o) => (
              <button key={o.id} className={"ap-drop__item" + (o.id === cur.id ? " is-sel" : "")} role="menuitemradio" aria-checked={o.id === cur.id}
                onClick={() => { onPick(o.id); setOpen(false); }}>
                <i className={"ti ti-" + o.icon + " ap-drop__item-ic"}></i>
                <span className="ap-drop__item-lab">{o.label}</span>
                {o.id === cur.id && <i className="ti ti-check ap-drop__check"></i>}
              </button>
            ))}
          </div>
        </React.Fragment>
      )}
    </div>
  );
}
function ApChartTools({ sort, setSort, cap, setCap }) {
  return (
    <div className="ap-tools">
      <ApDrop icon="arrows-sort" value={sort} options={AP_SORTS} onPick={setSort} isDefault={sort === "rank"} />
      <ApDrop icon="filter" value={cap} options={AP_CAPS} onPick={setCap} isDefault={cap === "all"} />
    </div>
  );
}
// The same two controls, rendered inline on a finding's segment bar.
function ApSegTools() {
  const tools = React.useContext(ApToolsCtx);
  if (!tools) return null;
  return <ApChartTools sort={tools.sort} setSort={tools.setSort} cap={tools.cap} setCap={tools.setCap} />;
}

// ---- Argument card: claim + curated findings ----
function ArgumentCard({ arg: argIn, accent, idx, cardRef, argKey, view = "cx", assignments = {}, apxById = {} }) {
  const [expanded, setExpanded] = React.useState(false);
  const [sort, setSort] = React.useState("rank");
  const [cap, setCap] = React.useState("all");
  const arg = apSortArg(argIn, sort, cap);
  // an argument shows a native finding only while it's still linked here — so
  // un-checking a baseline anchoring in the appendix removes it immediately.
  const isLinked = (rawId) => (assignments[rawId] || []).some((t) => t.slug + "::" + t.argId === argKey);
  const liveFindings = (arg.findings || []).filter((f) => !f.rawId || isLinked(f.rawId));
  const blocks = groupFindings(liveFindings);
  const baseItems = blocks.map((b, i) => ({
    block: b,
    key: b.type === "heat" ? ("heat" + i) : b.type === "gallery" ? ("gal" + i) : ((b.f && b.f.rawId) || ("blk" + i)),
    kind: b.type === "heat" ? "heat" : b.type === "gallery" ? "gallery" : "single",
  }));
  // appendix findings linked here but NOT native to the argument (deduped)
  const nativeIds = new Set();
  liveFindings.forEach((f) => { if (f.rawId) nativeIds.add(f.rawId); });
  const assignedItems = Object.keys(assignments)
    .filter((id) => !nativeIds.has(id) && (assignments[id] || []).some((t) => t.slug + "::" + t.argId === argKey))
    .map((id) => apxById[id]).filter(Boolean)
    .map((f) => ({
    block: { type: "single", f },
    key: "asg::" + f.rawId,
    kind: "single",
    assignedId: f.rawId,
  }));
  const items = [...baseItems, ...assignedItems];
  const cur = useArgCuration(items);
  const isClient = view === "client";
  const findIdOf = (it) => (it.kind === "heat" || it.kind === "gallery") ? (it.block.findings[0] && it.block.findings[0].rawId) : (it.block.f && it.block.f.rawId);
  const heatVisN = (it) => it.kind === "heat" ? it.block.findings.filter((f) => !cur.hiddenProd.has(f.productId)).length : it.kind === "gallery" ? it.block.findings.length : 1;

  if (!items[0]) {
    return (
      <div className="ap-argcard" ref={cardRef} style={{ "--ap-accent": accent }}>
        <div className="ap-argcard__head"><div className="ap-argcard__claim">{arg.argument.map((s, i) => i === 0 ? <p key={i}>{s}</p> : <p key={i} className="ap-argcard__sub">{s}</p>)}</div><ClaimActions scope="argument" id={argKey} /></div>
      </div>
    );
  }

  // client view drops hidden findings entirely (and empty heatmaps); CX keeps them dimmed
  const visible = items.filter((it) => !cur.hidden.has(it.key) && !(isClient && it.kind === "heat" && heatVisN(it) === 0));
  const hiddenItems = isClient ? [] : items.filter((it) => cur.hidden.has(it.key));
  const ordered = [...visible.filter((it) => it.key === cur.pinned), ...visible.filter((it) => it.key !== cur.pinned)];
  const hero = ordered[0];
  const rest = ordered.slice(1);
  const moreN = rest.reduce((s, it) => s + heatVisN(it), 0);

  return (
    <div className={"ap-argcard" + (expanded ? " is-expanded" : "")} ref={cardRef} style={{ "--ap-accent": accent }}>
      <div className="ap-argcard__head"><span className="ap-argcard__n">{idx}</span><div className="ap-argcard__claim">{arg.argument.map((s, i) => i === 0 ? <p key={i} contentEditable suppressContentEditableWarning spellCheck={false}>{s}</p> : <p key={i} className="ap-argcard__sub" contentEditable suppressContentEditableWarning spellCheck={false}>{s}</p>)}</div><ClaimActions scope="argument" id={argKey} /></div>
      <ApToolsCtx.Provider value={{ sort, setSort, cap, setCap }}>
      <div className="ap-argcard__findings">
        {hero && <CurationBlock item={hero} cur={cur} hidden={false} findId={findIdOf(hero)} view={view} />}
        {expanded && rest.length > 0 && (
          <div className="ap-argcard__supporting">
            {rest.map((it) => <CurationBlock key={it.key} item={it} cur={cur} hidden={false} findId={findIdOf(it)} view={view} />)}
          </div>
        )}
        {expanded && hiddenItems.length > 0 && (
          <div className="fc-cxgroup">
            {hiddenItems.map((it) => <CurationBlock key={it.key} item={it} cur={cur} hidden={true} findId={findIdOf(it)} view={view} />)}
          </div>
        )}
      </div>
      </ApToolsCtx.Provider>
      {(rest.length > 0 || hiddenItems.length > 0) && (
        <button className="ap-argcard__more" onClick={() => setExpanded((v) => !v)} aria-expanded={expanded}>
          <span className="ap-argcard__more-txt">
            {expanded
              ? "Hide findings"
              : moreN > 0
              ? "Show " + moreN + " more " + (moreN === 1 ? "finding" : "findings")
              : "Show " + hiddenItems.length + " hidden " + (hiddenItems.length === 1 ? "finding" : "findings")}
          </span>
          <i className={"ti ap-argcard__more-ic " + (expanded ? "ti-chevron-up" : "ti-chevron-down")}></i>
        </button>
      )}
    </div>
  );
}

// =========================================================
// Main view.
// =========================================================
function ApparelInsights({ viewMode = "cx", claimDivider = false, confMode = "badge", railMode = "full", confAlways = false, data, appendix, withAppendix = true }) {
  const DATA = data || window.APPAREL_INSIGHTS;
  apConfMode = confMode;
  apSentiment = {};
  if (DATA) DATA.objectives.forEach((o) => o.arguments.forEach((a) => (a.findings || []).forEach((f) => {
    if (f.kind === "sentiment" && f.tiers) f.tiers.forEach((t) => (t.items || []).forEach((it) => { if (it.id != null) apSentiment[it.id] = it.score; }));
  })));
  const readRef = React.useRef(null);
  const secRefs = React.useRef({});
  const argRefs = React.useRef({});
  const [openFind, setOpenFind] = React.useState({});
  const store = React.useContext(PinContext);
  const [activeObj, setActiveObj] = React.useState(DATA ? DATA.objectives[0].slug : null);
  const [activeArg, setActiveArg] = React.useState(null);
  const [view, setView] = React.useState("objectives"); // "objectives" | "appendix"
  const [apxCat, setApxCat] = React.useState("all");
  // ---- appendix → argument assignment store (multi-select) ----
  // Seeded from the system's existing anchoring: every finding already used
  // in an argument (matched by rawId) starts "linked" there, so the cascade
  // reflects the baseline on every load. User toggles layer on top (in-session).
  const [assignments, setAssignments] = React.useState(() => {
    const b = {}; const D = data || window.APPAREL_INSIGHTS;
    if (D) D.objectives.forEach((o) => o.arguments.forEach((a) => (a.findings || []).forEach((f) => {
      if (!f.rawId) return;
      (b[f.rawId] = b[f.rawId] || []).push({ slug: o.slug, argId: a.id, objNum: o.num, label: a.argument[0] });
    })));
    return b;
  });
  const toggleAssign = React.useCallback((id, t) => setAssignments((m) => {
    const arr = m[id] ? m[id].slice() : [];
    const i = arr.findIndex((x) => x.slug === t.slug && x.argId === t.argId);
    if (i >= 0) arr.splice(i, 1); else arr.push(t);
    const n = { ...m }; if (arr.length) n[id] = arr; else delete n[id]; return n;
  }), []);
  const apxById = React.useMemo(() => {
    const m = {}; const D = withAppendix ? (appendix || window.APPENDIX_FINDINGS) : null;
    if (D) D.groups.forEach((g) => g.findings.forEach((f) => { if (f.rawId) m[f.rawId] = f; }));
    return m;
  }, []);
  const assignedFor = React.useCallback((argKey) => {
    return Object.keys(assignments)
      .filter((id) => (assignments[id] || []).some((t) => t.slug + "::" + t.argId === argKey))
      .map((id) => apxById[id]).filter(Boolean);
  }, [assignments, apxById]);
  const apxCounts = React.useMemo(() => {
    const m = {}; const D = withAppendix ? (appendix || window.APPENDIX_FINDINGS) : null; const catOf = window.apxCatOf;
    if (D && catOf) D.groups.forEach((g) => g.findings.forEach((f) => { const k = catOf(f); m[k] = (m[k] || 0) + 1; }));
    return m;
  }, []);

  // Return-from-Explore: scroll back to the finding the user traced from.
  const retId = store && store.returnToFinding;
  React.useEffect(() => {
    if (!retId) return;
    const sc = readRef.current;
    let n = 0;
    const tick = () => {
      const el = document.getElementById("find-" + retId);
      if (el && sc) {
        const top = el.getBoundingClientRect().top - sc.getBoundingClientRect().top + sc.scrollTop - 24;
        sc.scrollTo({ top, behavior: n === 0 ? "auto" : "smooth" });
        el.classList.add("ap-findblock--flash");
        setTimeout(() => el.classList.remove("ap-findblock--flash"), 1600);
        store.setReturnToFinding(null);
        return;
      }
      n += 1;
      if (n < 12) setTimeout(tick, 120);
      else store.setReturnToFinding(null);
    };
    setTimeout(tick, 80);
  }, [retId]);

  function showAppendix(catKey) {
    setView("appendix");
    setActiveArg("objective-3::appendix");
    if (typeof catKey === "string") setApxCat(catKey);
    const sc = readRef.current;
    if (sc) requestAnimationFrame(() => sc.scrollTo({ top: 0 }));
  }

  function scrollToSec(slug) {
    const go = () => {
      const sc = readRef.current || document.querySelector(".ap-read");
      const el = secRefs.current[slug] || (sc && sc.querySelector('[data-sec="' + slug + '"]'));
      if (!el || !sc) return;
      const top = el.getBoundingClientRect().top - sc.getBoundingClientRect().top + sc.scrollTop - 6;
      sc.scrollTo({ top, behavior: view === "appendix" ? "auto" : "smooth" });
    };
    setActiveObj(slug); setActiveArg(null);
    if (view === "appendix") { setView("objectives"); requestAnimationFrame(() => requestAnimationFrame(go)); }
    else go();
  }
  function scrollToArg(slug, argId) {
    const key = slug + "::" + argId;
    const go = () => {
      const sc = readRef.current || document.querySelector(".ap-read");
      const el = argRefs.current[key] || (sc && sc.querySelector('[data-arg="' + key + '"]'));
      if (!el || !sc) return;
      const top = el.getBoundingClientRect().top - sc.getBoundingClientRect().top + sc.scrollTop - 14;
      sc.scrollTo({ top, behavior: view === "appendix" ? "auto" : "smooth" });
    };
    setActiveObj(slug); setActiveArg(key);
    if (view === "appendix") { setView("objectives"); requestAnimationFrame(() => requestAnimationFrame(go)); }
    else go();
  }

  // scroll-spy
  React.useEffect(() => {
    const sc = readRef.current;
    if (!sc || view === "appendix") return;
    const secs = new Map(), args = new Map();
    const apply = () => {
      let topSec = null, y0 = -Infinity;
      secs.forEach((y, id) => { if (y > y0) { y0 = y; topSec = id; } });
      let topArg = null, y1 = -Infinity;
      args.forEach((y, key) => {
        if (topSec && key.indexOf(topSec + "::") !== 0) return;
        if (y > y1) { y1 = y; topArg = key; }
      });
      if (topSec) setActiveObj(topSec);
      setActiveArg(topArg);
    };
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        const a = e.target.getAttribute("data-arg");
        const s = e.target.getAttribute("data-sec");
        if (a != null) { e.isIntersecting ? args.set(a, e.boundingClientRect.top) : args.delete(a); }
        if (s != null) { e.isIntersecting ? secs.set(s, e.boundingClientRect.top) : secs.delete(s); }
      });
      apply();
    }, { root: sc, rootMargin: "-18% 0px -62% 0px", threshold: 0 });
    Object.values(secRefs.current).forEach((el) => el && io.observe(el));
    Object.values(argRefs.current).forEach((el) => el && io.observe(el));
    return () => io.disconnect();
  }, [DATA, view]);

  if (!DATA) return <div style={{ padding: 24, color: "var(--ms-fg-muted)" }}>Loading insights…</div>;

  return (
    <div className={"gl gl-insx2 ap-ins" + (claimDivider ? " ap-ins--claimdiv" : "") + (railMode === "dashes" || railMode === "none" ? " ap-ins--dashrail" : "") + (railMode === "none" ? " ap-ins--norail" : "") + (confAlways ? " ap-ins--confon" : "") + (window.__METHOD_RAIL ? " ap-ins--mrail" : "")}>
      {/* ---- Left index: dash gutter (B1) or full outline ---- */}
      {railMode === "none" ? null : railMode === "dashes" ? (
        <aside className="ap-dash">
          <div className="ap-dash__in">
            {DATA.objectives.map((o) => (
              <div className="ap-dash__grp" key={o.slug}>
                <button className={"ap-dash__d ap-dash__d--o" + (activeObj === o.slug ? " on" : "")}
                  onClick={() => scrollToSec(o.slug)}>
                  <span className="ap-dash__line"></span>
                  <span className="ap-dash__lbl"><b>{o.num}</b>{o.objective}</span>
                </button>
                {o.arguments.map((a, i) => {
                  const key = o.slug + "::" + a.id;
                  return (
                    <button key={key} className={"ap-dash__d" + (activeArg === key ? " on" : "")}
                      onClick={() => scrollToArg(o.slug, a.id)}>
                      <span className="ap-dash__line"></span>
                      <span className="ap-dash__lbl">{a.argument[0]}</span>
                    </button>
                  );
                })}
              </div>
            ))}
            {withAppendix && window.AppendixPage && (
              <div className="ap-dash__grp">
                <button className={"ap-dash__d ap-dash__d--o" + (view === "appendix" ? " on" : "")} onClick={() => showAppendix("all")}>
                  <span className="ap-dash__line"></span>
                  <span className="ap-dash__lbl">All findings</span>
                </button>
              </div>
            )}
          </div>
        </aside>
      ) : (
      <aside className="gl-bx-index">
        <div className="gl-bx-index__scroll">
          {DATA.objectives.map((o) => (
            <div className="gl-bx-grp" key={o.slug}>
              <button className={"gl-bx-grp__head" + (activeObj === o.slug && !activeArg ? " is-active" : "")}
                onClick={() => scrollToSec(o.slug)}>
                <span className="gl-bx-grp__num">{o.num}</span>
                <span className="gl-bx-grp__title">{o.objective}</span>
              </button>
              <button className={"gl-bx-item ap-idxarg ap-idxtk" + (activeArg === o.slug + "::tk" ? " is-sel" : "")}
                onClick={() => { scrollToSec(o.slug); setActiveArg(o.slug + "::tk"); }}>
                <i className="ti ti-sparkles ap-idxtk__ic"></i>
                <span className="gl-bx-item__txt">Key takeaways</span>
              </button>
              <div className="gl-bx-item ap-idxarg ap-idxfind ap-idxfind--static">
                <span className="gl-bx-item__txt">Findings</span>
                <span className="ap-idxfind__n">{o.arguments.length}</span>
              </div>
              <div className="ap-idxargs">
              {o.arguments.map((a, i) => {
                const key = o.slug + "::" + a.id;
                return (
                  <button key={key} className={"gl-bx-item ap-idxarg" + (activeArg === key ? " is-sel" : "")}
                    onClick={() => scrollToArg(o.slug, a.id)}>
                    <span className="ap-idxarg__n">{i + 1}</span>
                    <span className="gl-bx-item__txt">{a.argument[0]}</span>
                  </button>
                );
              })}
              </div>
            </div>
          ))}
          {withAppendix && window.AppendixPage && (
            <div className="gl-bx-grp apx-idxgrp">
              <button className={"gl-bx-grp__head apx-idxhead" + (view === "appendix" && apxCat === "all" ? " is-active" : "")}
                onClick={() => showAppendix("all")}>
                <span className="gl-bx-grp__title">All findings</span>
              </button>
              {(window.APX_CATS || []).filter((c) => c.key !== "all").map((c) => (
                <button key={c.key}
                  className={"gl-bx-item ap-idxarg apx-idxcat" + (view === "appendix" && apxCat === c.key ? " is-sel" : "")}
                  onClick={() => showAppendix(c.key)}>
                  <span className="gl-bx-item__txt">{c.label}</span>
                  {apxCounts[c.key] != null && <span className="apx-idxcat__n">{apxCounts[c.key]}</span>}
                </button>
              ))}
            </div>
          )}
        </div>
      </aside>
      )}

      {/* ---- Reading panel ---- */}
      {view === "appendix" && withAppendix && window.AppendixPage ? (
        <window.AppendixPage cat={apxCat} setCat={setApxCat}
          assign={toggleAssign} assignments={assignments} objectives={DATA.objectives} />
      ) : (
      <div className="gl-bx-read ap-read" ref={readRef}>
        {DATA.objectives.map((o) => (
          <section className="gl-bx-sec ap-sec" key={o.slug} data-sec={o.slug}
            data-screen-label={"Objective " + o.num}
            ref={(el) => { secRefs.current[o.slug] = el; }} style={{ "--ap-accent": o.accent }}>
            <div className="gl-bx-sticky ap-sticky">
              <div className="gl-bx-sticky__inner">
                <div className="ap-objhead">
                  <span className="ap-objhead__num">Objective {o.num}</span>
                  <h2 className="ap-objtitle">{o.objective}</h2>
                </div>
                <div className="ap-takeaways">
                  <div className="ap-takeaways__head">
                    <div className="ap-takeaways__label"><i className="ti ti-sparkles"></i>Key takeaways</div>
                    <ClaimActions scope="takeaways" id={o.slug} />
                  </div>
                  <ul className="ap-takeaways__list">
                    {o.keyTakeaways.map((r, i) => {
                      const short = (((DATA && DATA.takeawaysShort) || {})[o.slug] || [])[i];
                      const a = o.arguments[i];
                      return (
                        <li key={i}>
                          <span className="ap-tk__txt" contentEditable suppressContentEditableWarning spellCheck={false}>{short || r}</span>
                          {a && (
                            <button className="ap-cite" title={a.argument[0]} onClick={() => scrollToArg(o.slug, a.id)}>{i + 1}</button>
                          )}
                        </li>
                      );
                    })}
                  </ul>
                </div>
              </div>
            </div>
            <div className="gl-bx-sec__body ap-body">
              {o.arguments.map((a, i) => {
                const key = o.slug + "::" + a.id;
                return (
                  <div key={key} data-arg={key} className="ap-argwrap"
                    ref={(el) => { argRefs.current[key] = el; }}>
                    <ArgumentCard arg={a} accent={o.accent} idx={i + 1} argKey={key} view={viewMode} assignments={assignments} apxById={apxById} />
                  </div>
                );
              })}
            </div>
          </section>
        ))}
      </div>
      )}
      {window.__METHOD_RAIL && view !== "appendix" ? <ApMethodRail /> : null}
    </div>
  );
}

// Export the leaf viz so the feedback-exploration view (v6) can reuse the
// exact same charts and just layer feedback affordances on top.
Object.assign(window, {
  ApparelInsights,
  ApVizLineEfficiency: VizLineEfficiency,
  ApVizAdvDet: VizAdvDet,
  ApVizSentiment: VizSentiment,
  ApVizStat: VizStat,
  ApVizBarList: VizBarList,
  ApFindingViz: FindingViz,
  ApAdvDetHeatmap: AdvDetHeatmap,
  apImg,
});
