// Updates — native rebuild of the 10-week insight series.
// All charts compute live from data/questions.json + summary.json, so figures
// always reflect the current (corrected) data rather than the original posts.

const LOW_SAMPLE = { LEBANON: 'Low sample size — interpret with caution.' };

function uFmt(v, d) { return v == null ? '—' : (v * 100).toFixed(d == null ? 0 : d) + '%'; }
function uTitle(c) { return titleCase(c); }

// ---- compute helpers -------------------------------------------------------
function UQ(data, id) { return data.questions.questions.find(q => q.id === id); }
function uCI(data) { const m = {}; data.questions.countries.forEach((c, i) => m[c] = i); return m; }
function uN(data) { const m = {}; data.summary.perCountry.forEach(r => m[r.name] = r.n); return m; }

// sum of selected answers for one country
function uVal(item, ci, country, match) {
  if (!item) return null;
  let t = 0, hit = false;
  item.answers.forEach(a => { if (match(a.label)) { t += (a.values[ci[country]] || 0); hit = true; } });
  return hit ? t : null;
}
// weighted global value
function uGlobal(item, ci, ns, countries, match) {
  let num = 0, den = 0;
  countries.forEach(c => { const v = uVal(item, ci, c, match); if (v != null) { num += v * (ns[c] || 0); den += (ns[c] || 0); } });
  return den ? num / den : null;
}
// mean of a numeric scale (labels beginning with a number)
function uMean(item, ci, country) {
  if (!item) return null;
  let t = 0, w = 0;
  item.answers.forEach(a => {
    const m = a.label.match(/^(\d+)/); if (!m) return;
    const v = a.values[ci[country]] || 0; t += parseInt(m[1], 10) * v; w += v;
  });
  return w ? t / w : null;
}

const U_MATCH = {
  yes: l => /^yes/i.test(l),
  no: l => /^no/i.test(l),
  agree: l => /^([567])/.test(l),                 // past neutral on 1–7
  strong: l => /^([67])/.test(l),
  high: l => /^([6789]|10)/.test(l),              // past neutral on 0–10
};
function uMatcher(spec) {
  if (typeof spec === 'string' && U_MATCH[spec]) return U_MATCH[spec];
  if (Array.isArray(spec)) return l => spec.some(s => l.toLowerCase().indexOf(String(s).toLowerCase()) === 0 || l === s);
  if (spec && spec.re) return l => new RegExp(spec.re, 'i').test(l);
  return () => true;
}

// ---- chart shells ----------------------------------------------------------
function UChartCard({ title, sub, footnote, filename, children }) {
  const ref = useRef(null);
  return (
    <figure style={{ margin: '30px 0' }}>
      <div className="chart-card" ref={ref}>
        <div className="chart-head">
          <div>
            <h3 style={{ fontFamily: 'Source Serif 4, serif', fontSize: 19, fontWeight: 500, letterSpacing: '-0.01em', textTransform: 'none' }}>{title}</h3>
            {sub && <div className="src" style={{ marginTop: 4 }}>{sub}</div>}
          </div>
          <div data-noexport="1"><ExportButton targetRef={ref} filename={filename || 'cbai-chart.png'} /></div>
        </div>
        {children}
        {footnote && <div className="src" style={{ marginTop: 14, paddingTop: 10, borderTop: '1px solid var(--rule)', lineHeight: 1.5 }}>{footnote}</div>}
      </div>
    </figure>);
}

// Horizontal ranked bars, one row per country
function URankBar({ rows, max, fmt, accent, note }) {
  const hi = max || Math.max(...rows.map(r => r.v || 0)) * 1.02;
  return (
    <div data-keep-cols className="uchart-rows" style={{ display: 'grid', gridTemplateColumns: '150px 1fr 54px', gap: '6px 10px', alignItems: 'center' }}>
      {rows.map((r, i) => (
        <React.Fragment key={r.c}>
          <div className="sans" style={{ fontFamily: 'Inter Tight, sans-serif', fontSize: 12.5, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
            <span className="mono" style={{ color: 'var(--muted)', fontSize: 10, marginRight: 6 }}>{i + 1}</span>
            <span style={{ marginRight: 5 }}>{flagEmoji(COUNTRY_META[r.c]?.iso || '')}</span>{uTitle(r.c)}
            {LOW_SAMPLE[r.c] && <sup style={{ color: 'var(--brand)', marginLeft: 3 }}>†</sup>}
          </div>
          <div style={{ height: 13, background: 'var(--surface-2)' }}>
            <div style={{ height: '100%', width: Math.max(0, (r.v / hi) * 100) + '%', background: r.hl ? 'var(--brand)' : (accent || 'var(--accent-data)') }} />
          </div>
          <div className="mono" style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 11.5, textAlign: 'right', fontVariantNumeric: 'tabular-nums' }}>{fmt ? fmt(r.v) : uFmt(r.v)}</div>
        </React.Fragment>
      ))}
      {note && <div className="src" style={{ gridColumn: '1 / -1', marginTop: 8 }}>{note}</div>}
    </div>);
}

// Global distribution across a question's answers
function UAnswerBars({ rows, fmt, accent, colors }) {
  const hi = Math.max(...rows.map(r => r.v)) * 1.02;
  return (
    <div data-keep-cols className="uchart-rows uchart-rows--wide" style={{ display: 'grid', gridTemplateColumns: 'minmax(160px, 300px) 1fr 54px', gap: '7px 12px', alignItems: 'center' }}>
      {rows.map(r => (
        <React.Fragment key={r.l}>
          <div className="sans" style={{ fontFamily: 'Inter Tight, sans-serif', fontSize: 12.5, lineHeight: 1.3 }}>{r.l}</div>
          <div style={{ height: 14, background: 'var(--surface-2)' }}>
            <div style={{ height: '100%', width: (r.v / hi * 100) + '%', background: r.hl ? 'var(--brand)' : ((colors && colors[r.l]) || accent || 'var(--accent-data)') }} />
          </div>
          <div className="mono" style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 11.5, textAlign: 'right' }}>{fmt ? fmt(r.v) : uFmt(r.v)}</div>
        </React.Fragment>
      ))}
    </div>);
}

// Two-series dot/dumbbell comparison per country
function UCompare({ rows, labels, fmt, max }) {
  const hi = max || Math.max(...rows.flatMap(r => [r.a || 0, r.b || 0])) * 1.05;
  return (
    <div>
      <div className="sans" style={{ display: 'flex', gap: 18, marginBottom: 12, fontFamily: 'Inter Tight, sans-serif', fontSize: 11.5, color: 'var(--muted)' }}>
        <span><span style={{ display: 'inline-block', width: 9, height: 9, background: 'var(--accent-data)', marginRight: 6 }} />{labels[0]}</span>
        <span><span style={{ display: 'inline-block', width: 9, height: 9, background: 'var(--brand)', marginRight: 6 }} />{labels[1]}</span>
      </div>
      <div data-keep-cols className="uchart-rows uchart-rows--cmp" style={{ display: 'grid', gridTemplateColumns: '150px 1fr 92px', gap: '6px 10px', alignItems: 'center' }}>
        {rows.map(r => (
          <React.Fragment key={r.c}>
            <div className="sans" style={{ fontFamily: 'Inter Tight, sans-serif', fontSize: 12.5, whiteSpace: 'nowrap' }}>
              <span style={{ marginRight: 5 }}>{flagEmoji(COUNTRY_META[r.c]?.iso || '')}</span>{uTitle(r.c)}
              {LOW_SAMPLE[r.c] && <sup style={{ color: 'var(--brand)', marginLeft: 3 }}>†</sup>}
            </div>
            <div style={{ position: 'relative', height: 14 }}>
              <div style={{ position: 'absolute', top: 6.5, left: 0, right: 0, height: 1, background: 'var(--rule)' }} />
              <div style={{ position: 'absolute', top: 4, left: Math.min(99, (r.a / hi) * 100) + '%', width: 7, height: 7, background: 'var(--accent-data)', transform: 'translateX(-3.5px)' }} />
              <div style={{ position: 'absolute', top: 4, left: Math.min(99, (r.b / hi) * 100) + '%', width: 7, height: 7, background: 'var(--brand)', transform: 'translateX(-3.5px)' }} />
            </div>
            <div className="mono" style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 11, textAlign: 'right', color: 'var(--ink-2)' }}>
              {fmt ? fmt(r.a) : uFmt(r.a)} / {fmt ? fmt(r.b) : uFmt(r.b)}
            </div>
          </React.Fragment>
        ))}
      </div>
    </div>);
}

// Stacked distribution per country (all answers of a question)
function UStack({ item, ci, countries, palette }) {
  const cols = palette || ['var(--accent-data)', 'var(--accent-data-2)', 'var(--brand)', 'var(--accent-neutral)', 'var(--accent-green)', 'var(--cornell-orange)', 'var(--cornell-sea-gray)'];
  return (
    <div>
      <div className="sans" style={{ display: 'flex', flexWrap: 'wrap', gap: '6px 16px', marginBottom: 14, fontFamily: 'Inter Tight, sans-serif', fontSize: 11.5, color: 'var(--muted)' }}>
        {item.answers.map((a, i) => (
          <span key={a.label}><span style={{ display: 'inline-block', width: 9, height: 9, background: cols[i % cols.length], marginRight: 6 }} />{a.label}</span>
        ))}
      </div>
      <div data-keep-cols className="uchart-rows uchart-rows--stack" style={{ display: 'grid', gridTemplateColumns: '150px 1fr', gap: '5px 10px', alignItems: 'center' }}>
        {countries.map(c => {
          const vals = item.answers.map(a => a.values[ci[c]] || 0);
          const tot = vals.reduce((x, y) => x + y, 0) || 1;
          return (
            <React.Fragment key={c}>
              <div className="sans" style={{ fontFamily: 'Inter Tight, sans-serif', fontSize: 12.5, whiteSpace: 'nowrap' }}>
                <span style={{ marginRight: 5 }}>{flagEmoji(COUNTRY_META[c]?.iso || '')}</span>{uTitle(c)}
                {LOW_SAMPLE[c] && <sup style={{ color: 'var(--brand)', marginLeft: 3 }}>†</sup>}
              </div>
              <div style={{ display: 'flex', height: 15 }}>
                {vals.map((v, i) => v <= 0 ? null : (
                  <div key={i} title={item.answers[i].label + ' · ' + uFmt(v / tot, 1)} style={{ width: (v / tot * 100) + '%', background: cols[i % cols.length] }} />
                ))}
              </div>
            </React.Fragment>);
        })}
      </div>
    </div>);
}

// 100% stacked Agree / Neutral / Disagree bands from a 1–7 item (matches the
// original release charts: Agree 6–7, Neutral 3–5, Disagree 1–2)
const BAND_DEFS = [
  { k: 'agree', l: 'Agree: 6–7', re: /^[67]/, c: 'var(--brand)' },
  { k: 'neutral', l: 'Neutral: 3–5', re: /^[345]/, c: 'var(--cornell-warm-gray)' },
  { k: 'disagree', l: 'Disagree: 1–2', re: /^[12]/, c: 'var(--cornell-navy)' },
];
function uBandsOf(item, ci, ns, countries, defs) {
  const out = {};
  (defs || BAND_DEFS).forEach(b => {
    let num = 0, den = 0;
    countries.forEach(c => {
      let t = 0;
      const re = b.re instanceof RegExp ? b.re : new RegExp(b.re, 'i');
      item.answers.forEach(a => { if (re.test(a.label)) t += (a.values[ci[c]] || 0); });
      num += t * (ns[c] || 0); den += (ns[c] || 0);
    });
    out[b.k || ['agree', 'neutral', 'disagree'][(defs || BAND_DEFS).indexOf(b)]] = den ? num / den : 0;
  });
  const keys = (defs || BAND_DEFS).map((b, i) => b.k || ['agree', 'neutral', 'disagree'][i]);
  const tot = keys.reduce((s, k) => s + (out[k] || 0), 0) || 1;
  keys.forEach(k => out[k] = (out[k] || 0) / tot);
  return out;
}
function UAxis({ vertical }) {
  const ticks = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
  if (vertical) return null;
  return (
    <div className="mono" style={{ display: 'flex', justifyContent: 'space-between', marginTop: 6, fontFamily: 'JetBrains Mono, monospace', fontSize: 10, color: 'var(--muted)' }}>
      {ticks.map(t => <span key={t}>{t}%</span>)}
    </div>);
}
function UBandLegend({ defs }) {
  const BAND_DEFS = defs || window.BAND_DEFS_DEFAULT;
  return (
    <div className="sans" style={{ display: 'flex', flexWrap: 'wrap', gap: '6px 22px', marginBottom: 18, fontFamily: 'Inter Tight, sans-serif', fontSize: 11.5, letterSpacing: '.04em', color: 'var(--ink-2)' }}>
      {BAND_DEFS.map(b => <span key={b.k}><span style={{ display: 'inline-block', width: 12, height: 12, background: b.c, marginRight: 7, transform: 'translateY(1px)' }} />{b.l}</span>)}
    </div>);
}
function UBands({ rows, vertical, defs }) {
  const B = defs || BAND_DEFS;
  if (vertical) return (
    <div>
      <UBandLegend defs={B} />
      <div data-keep-cols className="uchart-rows--cols" style={{ display: 'grid', gridTemplateColumns: '34px repeat(' + rows.length + ', 1fr)', gridTemplateRows: '300px auto', gap: '10px 14px', alignItems: 'end' }}>
        <div className="mono" style={{ position: 'relative', height: '100%', fontFamily: 'JetBrains Mono, monospace', fontSize: 9.5, color: 'var(--muted)' }}>
          {[0, 20, 40, 60, 80, 100].map(t => (
            <span key={t} style={{ position: 'absolute', right: 2, bottom: t + '%', transform: 'translateY(50%)' }}>{t}%</span>))}
        </div>
        {rows.map(r => (
          <div key={r.l} style={{ height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'flex-end' }}>
            <div style={{ display: 'flex', flexDirection: 'column', height: '100%', border: '1px solid var(--rule)' }}>
              <div style={{ height: (r.disagree * 100) + '%', background: B[2].c }} />
              <div style={{ height: (r.neutral * 100) + '%', background: B[1].c }} />
              <div style={{ height: (r.agree * 100) + '%', background: B[0].c }} />
            </div>
          </div>))}
        <div />
        {rows.map(r => (
          <div key={r.l + 'lbl'} className="sans" style={{ fontFamily: 'Inter Tight, sans-serif', fontSize: 11, lineHeight: 1.25, textAlign: 'center', paddingTop: 8, letterSpacing: '.02em' }}>{r.l}</div>))}
      </div>
    </div>);
  return (
    <div>
      <UBandLegend defs={B} />
      <div data-keep-cols className="uchart-rows--bands" style={{ display: 'grid', gridTemplateColumns: 'minmax(150px, 300px) 1fr', gap: '16px 14px', alignItems: 'center' }}>
        {rows.map(r => (
          <React.Fragment key={r.l}>
            <div className="sans" style={{ fontFamily: 'Inter Tight, sans-serif', fontSize: 12.5, lineHeight: 1.3, textAlign: 'right', letterSpacing: '.02em' }}>{r.l}</div>
            <div>
              <div style={{ display: 'flex', height: 26, border: '1px solid var(--rule)' }}>
                <div title={B[0].l + ' ' + uFmt(r.agree, 1)} style={{ width: (r.agree * 100) + '%', background: B[0].c }} />
                <div title={B[1].l + ' ' + uFmt(r.neutral, 1)} style={{ width: (r.neutral * 100) + '%', background: B[1].c }} />
                <div title={B[2].l + ' ' + uFmt(r.disagree, 1)} style={{ width: (r.disagree * 100) + '%', background: B[2].c }} />
              </div>
            </div>
          </React.Fragment>))}
        <div />
        <UAxis />
      </div>
    </div>);
}

// Two-series line chart across countries (matches the trust-vs-government chart)
function ULines({ rows, labels, colors }) {
  const W = 900, H = 376, padL = 38, padB = 120, padT = 10;
  const cw = (W - padL - 8) / Math.max(1, rows.length - 1);
  const y = v => padT + (1 - v) * (H - padT - padB);
  const pt = (i, v) => [padL + i * cw, y(v)];
  const path = key => rows.map((r, i) => (i ? 'L' : 'M') + pt(i, r[key]).join(' ')).join(' ');
  const cols = colors || ['var(--brand)', 'var(--fg)'];
  return (
    <div>
      <div className="sans" style={{ display: 'flex', gap: 22, marginBottom: 14, fontFamily: 'Inter Tight, sans-serif', fontSize: 11.5, letterSpacing: '.04em' }}>
        {labels.map((l, i) => <span key={l}><span style={{ display: 'inline-block', width: 12, height: 12, background: cols[i], marginRight: 7, transform: 'translateY(1px)' }} />{l}</span>)}
      </div>
      <div className="uchart-scroll"><svg viewBox={`0 0 ${W} ${H}`} className="uchart-svg" style={{ width: '100%', height: 'auto', overflow: 'visible' }}>
        {[0, .2, .4, .6, .8, 1].map(t => (
          <g key={t}>
            <line x1={padL} x2={W - 8} y1={y(t)} y2={y(t)} stroke="var(--rule)" strokeWidth="1" />
            <text x={padL - 6} y={y(t) + 3} textAnchor="end" fontSize="11" fill="var(--muted)" fontFamily="JetBrains Mono, monospace">{Math.round(t * 100)}%</text>
          </g>))}
        <path d={path('b')} fill="none" stroke={cols[1]} strokeWidth="2.4" />
        <path d={path('a')} fill="none" stroke={cols[0]} strokeWidth="2.8" />
        {rows.map((r, i) => <circle key={'b' + r.c} cx={pt(i, r.b)[0]} cy={pt(i, r.b)[1]} r="3.4" fill={cols[1]} />)}
        {rows.map((r, i) => <circle key={'a' + r.c} cx={pt(i, r.a)[0]} cy={pt(i, r.a)[1]} r="3.4" fill={cols[0]} />)}
        {rows.map((r, i) => (
          <text key={'t' + r.c} x={pt(i, 0)[0]} y={H - padB + 12} fontSize="13" fill="var(--ink-2)" fontFamily="Inter Tight, sans-serif"
            transform={`rotate(-60 ${pt(i, 0)[0]} ${H - padB + 12})`} textAnchor="end">{uTitle(r.c)}</text>))}
      </svg></div>
    </div>);
}

// Per-country vertical stacked Agree/Neutral/Disagree columns, optional line overlay
function UBandCols({ rows, overlayLabel, legend, colors }) {
  const C = colors || [BAND_DEFS[0].c, BAND_DEFS[1].c, BAND_DEFS[2].c];
  const W = 980, H = 366, padL = 38, padB = 128, padT = 8;
  const n = rows.length, slot = (W - padL - 8) / n, bw = Math.min(26, slot * 0.62);
  const y = v => padT + (1 - v) * (H - padT - padB);
  const cx = i => padL + slot * i + slot / 2;
  const overlay = rows.some(r => r.o != null);
  return (
    <div>
      <div className="sans" style={{ display: 'flex', flexWrap: 'wrap', gap: '6px 20px', marginBottom: 14, fontFamily: 'Inter Tight, sans-serif', fontSize: 11.5, letterSpacing: '.04em' }}>
        {(legend ? legend.map((l, i) => Array.isArray(l) ? l : [l, C[i]]) : [['Yes', C[0]], ['Neutral', C[1]], ['No', C[2]]]).map(([l, c]) => (
          <span key={l}><span style={{ display: 'inline-block', width: 12, height: 12, background: c, marginRight: 7, transform: 'translateY(1px)' }} />{l}</span>))}
        {overlay && <span><span style={{ display: 'inline-block', width: 12, height: 3, background: 'var(--fg)', marginRight: 7, transform: 'translateY(-3px)' }} />{overlayLabel || 'Never owned'}</span>}
      </div>
      <div className="uchart-scroll"><svg viewBox={`0 0 ${W} ${H}`} className="uchart-svg" style={{ width: '100%', height: 'auto', overflow: 'visible' }}>
        {[0, .2, .4, .6, .8, 1].map(t => (
          <g key={t}>
            <line x1={padL} x2={W - 8} y1={y(t)} y2={y(t)} stroke="var(--rule)" strokeWidth="1" />
            <text x={padL - 6} y={y(t) + 3} textAnchor="end" fontSize="11" fill="var(--muted)" fontFamily="JetBrains Mono, monospace">{Math.round(t * 100)}%</text>
          </g>))}
        {rows.map((r, i) => {
          const a = r.agree, ne = r.neutral, d = r.disagree;
          const x = cx(i) - bw / 2;
          return (
            <g key={r.c}>
              <rect x={x} y={y(a)} width={bw} height={(H - padT - padB) * a} fill={C[0]} />
              <rect x={x} y={y(a + ne)} width={bw} height={(H - padT - padB) * ne} fill={C[1]} />
              <rect x={x} y={y(a + ne + d)} width={bw} height={(H - padT - padB) * d} fill={C[2]} />
              <text x={cx(i)} y={H - padB + 13} fontSize="13" fill="var(--ink-2)" fontFamily="Inter Tight, sans-serif"
                transform={`rotate(-60 ${cx(i)} ${H - padB + 12})`} textAnchor="end">{uTitle(r.c)}</text>
            </g>);
        })}
        {overlay && <path d={rows.map((r, i) => (i ? 'L' : 'M') + cx(i) + ' ' + y(r.o)).join(' ')} fill="none" stroke="var(--fg)" strokeWidth="2.2" />}
      </svg></div>
    </div>);
}

// Per-country horizontal 100% stacked bands (matches the W10 intent charts)
function UBandRows({ rows, legend, colors }) {
  const C = colors || [BAND_DEFS[0].c, BAND_DEFS[1].c, BAND_DEFS[2].c];
  return (
    <div>
      <div className="sans" style={{ display: 'flex', flexWrap: 'wrap', gap: '6px 20px', marginBottom: 16, fontFamily: 'Inter Tight, sans-serif', fontSize: 11.5, letterSpacing: '.04em' }}>
        {(legend || ['Agree', 'Neutral', 'Disagree']).map((l, i) => (
          <span key={l}><span style={{ display: 'inline-block', width: 12, height: 12, background: C[i], marginRight: 7, transform: 'translateY(1px)' }} />{l}</span>))}
      </div>
      <div data-keep-cols className="uchart-rows uchart-rows--wide" style={{ display: 'grid', gridTemplateColumns: 'minmax(120px, 168px) 1fr', gap: '5px 12px', alignItems: 'center' }}>
        {rows.map(r => (
          <React.Fragment key={r.c}>
            <div className="sans" style={{ fontFamily: 'Inter Tight, sans-serif', fontSize: 12.5, textAlign: 'right', whiteSpace: 'nowrap' }}>
              {uTitle(r.c)}{LOW_SAMPLE[r.c] && <sup style={{ color: 'var(--brand)', marginLeft: 3 }}>†</sup>}
            </div>
            <div style={{ display: 'flex', height: 18, border: '1px solid var(--rule)' }}>
              <div title={'Likely ' + uFmt(r.agree, 1)} style={{ width: (r.agree * 100) + '%', background: C[0] }} />
              <div title={'Neutral ' + uFmt(r.neutral, 1)} style={{ width: (r.neutral * 100) + '%', background: C[1] }} />
              <div title={'Unlikely ' + uFmt(r.disagree, 1)} style={{ width: (r.disagree * 100) + '%', background: C[2] }} />
            </div>
          </React.Fragment>))}
        <div />
        <UAxis />
      </div>
    </div>);
}

// N-series line chart across countries
function ULinesN({ rows, labels, colors }) {
  const W = 980, H = 376, padL = 38, padB = 128, padT = 8;
  const cw = (W - padL - 8) / Math.max(1, rows.length - 1);
  const y = v => padT + (1 - v) * (H - padT - padB);
  const cols = colors || ['var(--cornell-navy)', 'var(--brand)', 'var(--cornell-warm-gray)'];
  return (
    <div>
      <div className="sans" style={{ display: 'flex', flexWrap: 'wrap', gap: '6px 20px', marginBottom: 14, fontFamily: 'Inter Tight, sans-serif', fontSize: 11.5, letterSpacing: '.04em' }}>
        {labels.map((l, i) => <span key={l}><span style={{ display: 'inline-block', width: 12, height: 12, background: cols[i], marginRight: 7, transform: 'translateY(1px)' }} />{l}</span>)}
      </div>
      <div className="uchart-scroll"><svg viewBox={`0 0 ${W} ${H}`} className="uchart-svg" style={{ width: '100%', height: 'auto', overflow: 'visible' }}>
        {[0, .2, .4, .6, .8, 1].map(t => (
          <g key={t}>
            <line x1={padL} x2={W - 8} y1={y(t)} y2={y(t)} stroke="var(--rule)" strokeWidth="1" />
            <text x={padL - 6} y={y(t) + 3} textAnchor="end" fontSize="11" fill="var(--muted)" fontFamily="JetBrains Mono, monospace">{Math.round(t * 100)}%</text>
          </g>))}
        {labels.map((l, s) => (
          <path key={l} d={rows.map((r, i) => (i ? 'L' : 'M') + (padL + i * cw) + ' ' + y(r.v[s])).join(' ')} fill="none" stroke={cols[s]} strokeWidth="2.4" />))}
        {labels.map((l, s) => rows.map((r, i) => <circle key={l + r.c} cx={padL + i * cw} cy={y(r.v[s])} r="3" fill={cols[s]} />))}
        {rows.map((r, i) => (
          <text key={r.c} x={padL + i * cw} y={H - padB + 12} fontSize="13" fill="var(--ink-2)" fontFamily="Inter Tight, sans-serif"
            transform={`rotate(-60 ${padL + i * cw} ${H - padB + 12})`} textAnchor="end">{uTitle(r.c)}</text>))}
      </svg></div>
    </div>);
}

// Grouped vertical columns, N series over categories or countries
function UGroupCols({ rows, labels, colors, height, valueLabels }) {
  const W = 980, H = height || 330, padL = 34, padB = 118, padT = 8;
  const S = labels.length;
  const n = rows.length, slot = (W - padL - 8) / n, bw = Math.max(4, Math.min(18, (slot * 0.74) / S));
  const vals = rows.flatMap(r => r.v || [r.a, r.b]).filter(v => v != null);
  const hi = Math.max(...vals) * 1.08 || 1;
  const y = v => padT + (1 - v / hi) * (H - padT - padB);
  const cols = colors || ['var(--cornell-warm-gray)', 'var(--brand)', '#4a4a4a', '#c9c6c0'];
  return (
    <div>
      <div className="sans" style={{ display: 'flex', flexWrap: 'wrap', gap: '6px 20px', marginBottom: 14, fontFamily: 'Inter Tight, sans-serif', fontSize: 11.5, letterSpacing: '.04em' }}>
        {labels.map((l, i) => <span key={l}><span style={{ display: 'inline-block', width: 12, height: 12, background: cols[i], marginRight: 7, transform: 'translateY(1px)' }} />{l}</span>)}
      </div>
      <div className="uchart-scroll"><svg viewBox={`0 0 ${W} ${H}`} className="uchart-svg" style={{ width: '100%', height: 'auto', overflow: 'visible' }}>
        {[0, .2, .4, .6, .8, 1].map(f => hi * f).map(t => (
          <g key={t}>
            <line x1={padL} x2={W - 8} y1={y(t)} y2={y(t)} stroke="var(--rule)" strokeWidth="1" />
            <text x={padL - 6} y={y(t) + 3} textAnchor="end" fontSize="11" fill="var(--muted)" fontFamily="JetBrains Mono, monospace">{Math.round(t * 100)}%</text>
          </g>))}
        {rows.map((r, i) => {
          const c = padL + slot * i + slot / 2;
          const series = r.v || [r.a, r.b];
          const total = bw * S + 2 * (S - 1);
          return (
            <g key={r.l}>
              {series.map((v, s) => v == null ? null : (
                <g key={s}>
                  <rect x={c - total / 2 + s * (bw + 2)} y={y(v)} width={bw} height={Math.max(0, (H - padT - padB) - (y(v) - padT))} fill={cols[s % cols.length]}>
                    <title>{labels[s] + ' · ' + uFmt(v, 1)}</title></rect>
                  {valueLabels && <text x={c - total / 2 + s * (bw + 2) + bw / 2} y={y(v) + 15} fontSize="11" fontWeight="700" fill="#fff" textAnchor="middle" fontFamily="Inter Tight, sans-serif">{uFmt(v, 0)}</text>}
                </g>))}
              <text x={c} y={H - padB + 12} fontSize="13" fill="var(--ink-2)" fontFamily="Inter Tight, sans-serif"
                transform={`rotate(-60 ${c} ${H - padB + 12})`} textAnchor="end">{r.l}</text>
            </g>);
        })}
      </svg></div>
    </div>);
}

// Stacked area: series a behind series b (awareness vs knowledge)
function UArea({ rows, labels, colors }) {
  const W = 980, H = 366, padL = 38, padB = 128, padT = 8;
  const cw = (W - padL - 8) / Math.max(1, rows.length - 1);
  const hi = Math.max(...rows.map(r => r.a)) * 1.12 || 1;
  const y = v => padT + (1 - v / hi) * (H - padT - padB);
  const x = i => padL + i * cw;
  const cols = colors || ['var(--cornell-warm-gray)', 'var(--cornell-navy)'];
  const areaOf = key => rows.map((r, i) => (i ? 'L' : 'M') + x(i) + ' ' + y(r[key])).join(' ')
    + ` L${x(rows.length - 1)} ${y(0)} L${x(0)} ${y(0)} Z`;
  return (
    <div>
      <div className="sans" style={{ display: 'flex', gap: 22, marginBottom: 14, fontFamily: 'Inter Tight, sans-serif', fontSize: 11.5, letterSpacing: '.04em' }}>
        {labels.map((l, i) => <span key={l}><span style={{ display: 'inline-block', width: 12, height: 12, background: cols[i], marginRight: 7, transform: 'translateY(1px)' }} />{l}</span>)}
      </div>
      <div className="uchart-scroll"><svg viewBox={`0 0 ${W} ${H}`} className="uchart-svg" style={{ width: '100%', height: 'auto', overflow: 'visible' }}>
        {[0, .25, .5, .75, 1].map(f => hi * f).map(t => (
          <g key={t}>
            <line x1={padL} x2={W - 8} y1={y(t)} y2={y(t)} stroke="var(--rule)" strokeWidth="1" />
            <text x={padL - 6} y={y(t) + 3} textAnchor="end" fontSize="11" fill="var(--muted)" fontFamily="JetBrains Mono, monospace">{Math.round(t * 100)}%</text>
          </g>))}
        <path d={areaOf('a')} fill={cols[0]} />
        <path d={areaOf('b')} fill={cols[1]} />
        {rows.map((r, i) => (
          <text key={r.c} x={x(i)} y={H - padB + 12} fontSize="13" fill="var(--ink-2)" fontFamily="Inter Tight, sans-serif"
            transform={`rotate(-60 ${x(i)} ${H - padB + 12})`} textAnchor="end">{uTitle(r.c)}</text>))}
      </svg></div>
    </div>);
}

// "Olympics" medal counts: how many countries rank each option 1st / 2nd / 3rd
const MEDALS = [['Gold', '#F2A900'], ['Silver', '#8E8E8E'], ['Bronze', '#E8730C']];
function UMedals({ rows }) {
  const hi = Math.max(...rows.map(r => r.g + r.s + r.b)) || 1;
  return (
    <div>
      <div className="sans" style={{ display: 'flex', gap: 22, marginBottom: 16, fontFamily: 'Inter Tight, sans-serif', fontSize: 11.5, letterSpacing: '.04em' }}>
        {MEDALS.map(([l, c]) => <span key={l}><span style={{ display: 'inline-block', width: 12, height: 12, background: c, marginRight: 7, transform: 'translateY(1px)' }} />{l}</span>)}
      </div>
      <div data-keep-cols className="uchart-rows uchart-rows--wide" style={{ display: 'grid', gridTemplateColumns: 'minmax(150px, 260px) 1fr 40px', gap: '12px 14px', alignItems: 'center' }}>
        {rows.map(r => (
          <React.Fragment key={r.l}>
            <div className="sans" style={{ fontFamily: 'Inter Tight, sans-serif', fontSize: 12.5, lineHeight: 1.3, textAlign: 'right', letterSpacing: '.02em' }}>{r.l}</div>
            <div style={{ display: 'flex', height: 20 }}>
              {[['g', 0], ['s', 1], ['b', 2]].map(([k, i]) => r[k] ? (
                <div key={k} title={MEDALS[i][0] + ': ' + r[k]} style={{ width: (r[k] / hi * 100) + '%', background: MEDALS[i][1] }} />) : null)}
            </div>
            <div className="mono" style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 11.5, textAlign: 'right' }}>{r.g + r.s + r.b}</div>
          </React.Fragment>))}
      </div>
    </div>);
}

// Top-cited option per country, bar colored by which option won
function UTopReason({ rows, legend }) {
  const hi = Math.max(...rows.map(r => r.v)) * 1.04;
  return (
    <div>
      <div className="sans" style={{ display: 'flex', flexWrap: 'wrap', gap: '8px 18px', marginBottom: 16, fontFamily: 'Inter Tight, sans-serif', fontSize: 11, letterSpacing: '.03em' }}>
        {legend.map(([l, c]) => <span key={l}><span style={{ display: 'inline-block', width: 12, height: 12, background: c, marginRight: 7, transform: 'translateY(1px)' }} />{l}</span>)}
      </div>
      <div data-keep-cols className="uchart-rows" style={{ display: 'grid', gridTemplateColumns: '150px 1fr 50px', gap: '5px 10px', alignItems: 'center' }}>
        {rows.map(r => (
          <React.Fragment key={r.c}>
            <div className="sans" style={{ fontFamily: 'Inter Tight, sans-serif', fontSize: 12.5, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
              <span style={{ marginRight: 5 }}>{flagEmoji(COUNTRY_META[r.c]?.iso || '')}</span>{uTitle(r.c)}
              {LOW_SAMPLE[r.c] && <sup style={{ color: 'var(--brand)', marginLeft: 3 }}>†</sup>}
            </div>
            <div style={{ height: 13, background: 'var(--surface-2)' }} title={r.l}>
              <div style={{ height: '100%', width: (r.v / hi * 100) + '%', background: r.color }} />
            </div>
            <div className="mono" style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 11.5, textAlign: 'right' }}>{uFmt(r.v)}</div>
          </React.Fragment>))}
      </div>
    </div>);
}

// Treemap of option shares
function UTreemap({ items, palette }) {
  const cols = palette || ['var(--brand)', 'var(--cornell-navy)', '#4a4a4a', '#8E8E8E', 'var(--cornell-sea-gray)', 'var(--cornell-warm-gray)', 'var(--fg)'];
  const total = items.reduce((s, i) => s + i.v, 0) || 1;
  // simple squarified rows: pack into rows of ~equal area
  const rows = [];
  let row = [], acc = 0;
  const target = total / 3.2;
  items.forEach(it => {
    row.push(it); acc += it.v;
    if (acc >= target) { rows.push(row); row = []; acc = 0; }
  });
  if (row.length) rows.push(row);
  return (
    <div style={{ display: 'grid', gap: 3 }}>
      {rows.map((r, ri) => {
        const rsum = r.reduce((s, i) => s + i.v, 0) || 1;
        const share = rsum / total;
        return (
          <div key={ri} style={{ display: 'flex', gap: 3, height: Math.max(64, share * 420) }}>
            {r.map((it, i) => (
              <div key={it.l} title={it.l + ' · ' + uFmt(it.v / total, 1)}
                style={{ flex: it.v, background: cols[(ri * 3 + i) % cols.length], color: '#fff', display: 'flex', alignItems: 'flex-end', padding: '10px 12px', overflow: 'hidden' }}>
                <span className="sans" style={{ fontFamily: 'Inter Tight, sans-serif', fontSize: Math.max(10, Math.min(14, (it.v / total) * 130)), fontWeight: 600, letterSpacing: '.02em', lineHeight: 1.2 }}>{it.l}</span>
              </div>))}
          </div>);
      })}
    </div>);
}

// Radar / spider chart of option shares
function URadar({ items, max }) {
  const S = 560, cx = S / 2, cy = S / 2, R = S * 0.34;
  const hi = max || 1;
  const n = items.length;
  const ang = i => (i / n) * Math.PI * 2 - Math.PI / 2;
  const pt = (i, v) => [cx + Math.cos(ang(i)) * R * (v / hi), cy + Math.sin(ang(i)) * R * (v / hi)];
  const ring = f => Array.from({ length: n }, (_, i) => pt(i, hi * f).join(' ')).join(' L');
  return (
    <svg viewBox={`0 0 ${S} ${S}`} style={{ width: '100%', maxWidth: 620, height: 'auto', display: 'block', margin: '0 auto', overflow: 'visible' }}>
      {[0.2, 0.4, 0.6, 0.8, 1].map(f => (
        <path key={f} d={'M' + ring(f) + ' Z'} fill="none" stroke="var(--rule)" strokeWidth="1" />))}
      {items.map((_, i) => <line key={i} x1={cx} y1={cy} x2={pt(i, hi)[0]} y2={pt(i, hi)[1]} stroke="var(--rule)" strokeWidth="1" />)}
      {[0.4, 0.8].map(f => (
        <text key={f} x={cx + 6} y={cy - R * f} fontSize="11" fontWeight="600" fill="var(--muted)" fontFamily="Inter Tight, sans-serif">{Math.round(hi * f * 100)}%</text>))}
      <path d={'M' + items.map((it, i) => pt(i, it.v).join(' ')).join(' L') + ' Z'} fill="none" stroke="var(--brand)" strokeWidth="3.5" strokeLinejoin="round" />
      {items.map((it, i) => <circle key={it.l} cx={pt(i, it.v)[0]} cy={pt(i, it.v)[1]} r="4.5" fill="var(--brand)" />)}
      {items.map((it, i) => {
        const [x, y] = pt(i, hi * 1.17);
        const a = ang(i), right = Math.cos(a) > 0.15, left = Math.cos(a) < -0.15;
        return (
          <text key={it.l + 'l'} x={x} y={y} fontSize="11.5" fontWeight="600" fill="var(--fg)" fontFamily="Inter Tight, sans-serif"
            textAnchor={right ? 'start' : left ? 'end' : 'middle'} dominantBaseline="middle" style={{ letterSpacing: '.02em' }}>
            {it.l.length > 26 ? it.l.slice(0, 24) + '…' : it.l}
          </text>);
      })}
    </svg>);
}

// Per-country 100% stacked columns with arbitrary answer groups
function UStackCols({ rows, groups, overlayLabel }) {
  const W = 980, H = 340, padL = 34, padB = 108, padT = 8;
  const n = rows.length, slot = (W - padL - 8) / n, bw = Math.min(24, slot * 0.6);
  const y = v => padT + (1 - v) * (H - padT - padB);
  const cx = i => padL + slot * i + slot / 2;
  const overlay = rows.some(r => r.o != null);
  return (
    <div>
      <div className="sans" style={{ display: 'flex', flexWrap: 'wrap', gap: '6px 18px', marginBottom: 14, fontFamily: 'Inter Tight, sans-serif', fontSize: 11.5, letterSpacing: '.04em' }}>
        {groups.map(g => <span key={g.l}><span style={{ display: 'inline-block', width: 12, height: 12, background: g.c, marginRight: 7, transform: 'translateY(1px)' }} />{g.l}</span>)}
        {overlay && <span><span style={{ display: 'inline-block', width: 12, height: 3, background: 'var(--fg)', marginRight: 7, transform: 'translateY(-3px)' }} />{overlayLabel}</span>}
      </div>
      <div className="uchart-scroll"><svg viewBox={`0 0 ${W} ${H}`} className="uchart-svg" style={{ width: '100%', height: 'auto', overflow: 'visible' }}>
        {[0, .2, .4, .6, .8, 1].map(t => (
          <g key={t}>
            <line x1={padL} x2={W - 8} y1={y(t)} y2={y(t)} stroke="var(--rule)" strokeWidth="1" />
            <text x={padL - 6} y={y(t) + 3} textAnchor="end" fontSize="11" fill="var(--muted)" fontFamily="JetBrains Mono, monospace">{Math.round(t * 100)}%</text>
          </g>))}
        {rows.map((r, i) => {
          let acc = 0;
          return (
            <g key={r.c}>
              {r.v.map((v, gi) => {
                const yy = y(acc + v), h = (H - padT - padB) * v;
                acc += v;
                return <rect key={gi} x={cx(i) - bw / 2} y={yy} width={bw} height={Math.max(0, h)} fill={groups[gi].c}>
                  <title>{groups[gi].l + ' · ' + uFmt(v, 1)}</title></rect>;
              })}
              <text x={cx(i)} y={H - padB + 12} fontSize="13" fill="var(--ink-2)" fontFamily="Inter Tight, sans-serif"
                transform={`rotate(-60 ${cx(i)} ${H - padB + 12})`} textAnchor="end">{uTitle(r.c)}</text>
            </g>);
        })}
        {overlay && <path d={rows.map((r, i) => (i ? 'L' : 'M') + cx(i) + ' ' + y(r.o)).join(' ')} fill="none" stroke="var(--fg)" strokeWidth="2.2" />}
      </svg></div>
    </div>);
}

// Side-by-side pie panels
function UPies({ panels }) {
  const arc = (items, R) => {
    const tot = items.reduce((s, i) => s + i.v, 0) || 1;
    let a0 = -Math.PI / 2;
    return items.map(it => {
      const a1 = a0 + (it.v / tot) * Math.PI * 2;
      const large = a1 - a0 > Math.PI ? 1 : 0;
      const d = `M0 0 L${Math.cos(a0) * R} ${Math.sin(a0) * R} A${R} ${R} 0 ${large} 1 ${Math.cos(a1) * R} ${Math.sin(a1) * R} Z`;
      const mid = (a0 + a1) / 2;
      const lp = [Math.cos(mid) * R * 1.2, Math.sin(mid) * R * 1.2];
      a0 = a1;
      return { d, c: it.c, l: it.l, v: it.v / tot, lp };
    });
  };
  return (
    <div data-keep-cols className="upies" style={{ display: 'grid', gridTemplateColumns: `repeat(${panels.length}, 1fr)`, gap: 1, background: 'var(--rule)' }}>
      {panels.map(p => {
        const segs = arc(p.items, 74);
        return (
          <div key={p.title} style={{ background: 'var(--surface)', padding: '16px 14px 18px' }}>
            <div className="sans" style={{ fontFamily: 'Inter Tight, sans-serif', fontSize: 12, letterSpacing: '.04em', textAlign: 'center', marginBottom: 10, minHeight: 30, color: 'var(--ink-2)' }}>{p.title}</div>
            <svg viewBox="-110 -100 220 200" style={{ width: '100%', height: 'auto', overflow: 'visible' }}>
              {segs.map(s => <path key={s.l} d={s.d} fill={s.c} stroke="var(--surface)" strokeWidth="1"><title>{s.l + ' · ' + uFmt(s.v, 0)}</title></path>)}
              {segs.filter(s => s.v > 0.03).map(s => (
                <text key={s.l + 'v'} x={s.lp[0]} y={s.lp[1]} fontSize="11" fontWeight="700" fill="var(--fg)" fontFamily="Inter Tight, sans-serif"
                  textAnchor={s.lp[0] > 6 ? 'start' : s.lp[0] < -6 ? 'end' : 'middle'} dominantBaseline="middle">{uFmt(s.v, 0)}</text>))}
            </svg>
            <div className="sans" style={{ display: 'grid', gap: 4, marginTop: 14, fontFamily: 'Inter Tight, sans-serif', fontSize: 10.5, letterSpacing: '.03em' }}>
              {p.items.map(it => <span key={it.l}><span style={{ display: 'inline-block', width: 10, height: 10, background: it.c, marginRight: 6, transform: 'translateY(1px)' }} />{it.l}</span>)}
            </div>
          </div>);
      })}
    </div>);
}

window.BAND_DEFS_DEFAULT = BAND_DEFS;
Object.assign(window, { UChartCard, URankBar, UAnswerBars, UCompare, UStack, UBands, UBandCols, UBandRows, UStackCols, UPies, ULines, ULinesN, UGroupCols, UArea, UMedals, UTopReason, UTreemap, URadar, uBandsOf, BAND_DEFS, UQ, uCI, uN, uVal, uGlobal, uMean, uMatcher, uFmt, uTitle, LOW_SAMPLE });
