// Public-site Layer 2 Indices page — per-index methodology + state ranking.
// Loads state_indices.json on first visit, caches in window.PL_INDICES.
{
const { STATES, STATE_POPULATION } = window.PL_DATA;

// Indices where higher = bad (rust palette). Low = teal palette.
const RESTRICTIVE = new Set(["fascism", "greed", "religious_nationalism", "punishment"]);

// Canon files we expect to load lazily (must match rubrics/index_canons/{id}.md).
const KNOWN_INDICES = [
  "fascism", "greed", "religious_nationalism", "wellbeing",
  "punishment", "equity", "climate_resilience", "aging"
];

// Hex helpers — diverging palette matching the atlas styling.
function indexColor(score, restrictive) {
  if (score === null || score === undefined) return "#9ca29a";
  const polarity = restrictive ? score : (100 - score);
  const distance = (polarity - 50) / 50;  // -1..1
  const t = Math.min(Math.abs(distance), 1);
  if (distance >= 0) {
    if (t < 0.5) {
      return `rgb(${Math.round(125 + 65 * t * 2)}, ${Math.round(105 - 15 * t * 2)}, ${Math.round(95 - 25 * t * 2)})`;
    }
    return `rgb(${Math.round(190 - 80 * (t - 0.5) * 2)}, ${Math.round(90 - 30 * (t - 0.5) * 2)}, ${Math.round(70 - 20 * (t - 0.5) * 2)})`;
  }
  if (t < 0.5) {
    return `rgb(${Math.round(125 - 30 * t * 2)}, ${Math.round(150 + 5 * t * 2)}, ${Math.round(140 + 8 * t * 2)})`;
  }
  return `rgb(${Math.round(95 - 30 * (t - 0.5) * 2)}, ${Math.round(155 - 35 * (t - 0.5) * 2)}, ${Math.round(148 - 30 * (t - 0.5) * 2)})`;
}

const MODES = [
  { id: "all",      label: "All activity" },
  { id: "passed",   label: "Passed (became law)" },
  { id: "pressure", label: "Pressure (introduced)" },
];

// Tiny markdown renderer — handles the subset we use in canon files
// (headings, paragraphs, bullets, simple bold/italic, code spans). No fenced
// code blocks — we don't need them here.
function renderMarkdown(md) {
  if (!md) return null;
  const lines = md.split("\n");
  const out = [];
  let i = 0;
  let key = 0;
  while (i < lines.length) {
    const line = lines[i];
    if (/^#\s/.test(line)) {
      out.push(<h1 key={key++} style={{ fontSize: 28, fontWeight: 600, margin: "24px 0 12px", letterSpacing: "-0.01em" }}>{line.replace(/^#\s/, "")}</h1>);
    } else if (/^##\s/.test(line)) {
      out.push(<h2 key={key++} style={{ fontSize: 20, fontWeight: 600, margin: "20px 0 10px", letterSpacing: "-0.005em" }}>{line.replace(/^##\s/, "")}</h2>);
    } else if (/^###\s/.test(line)) {
      out.push(<h3 key={key++} style={{ fontSize: 15, fontWeight: 600, margin: "16px 0 8px" }}>{line.replace(/^###\s/, "")}</h3>);
    } else if (/^\|/.test(line)) {
      // Table — gobble all subsequent table lines
      const tableLines = [];
      while (i < lines.length && /^\|/.test(lines[i])) {
        tableLines.push(lines[i]);
        i++;
      }
      out.push(renderTable(tableLines, key++));
      continue;
    } else if (/^-\s/.test(line)) {
      // List — gobble bullets
      const items = [];
      while (i < lines.length && /^-\s/.test(lines[i])) {
        items.push(lines[i].replace(/^-\s/, ""));
        i++;
      }
      out.push(
        <ul key={key++} style={{ paddingLeft: 22, margin: "8px 0 14px" }}>
          {items.map((item, idx) => <li key={idx} style={{ margin: "4px 0", lineHeight: 1.55 }}>{renderInline(item)}</li>)}
        </ul>
      );
      continue;
    } else if (line.trim() === "") {
      // blank line — paragraph break, no DOM
    } else {
      out.push(<p key={key++} style={{ margin: "8px 0", lineHeight: 1.6 }}>{renderInline(line)}</p>);
    }
    i++;
  }
  return out;
}

function renderInline(text) {
  // Handle **bold**, *italic*, `code` minimally
  const parts = [];
  let key = 0;
  let cursor = 0;
  const re = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g;
  let m;
  while ((m = re.exec(text)) !== null) {
    if (m.index > cursor) parts.push(text.slice(cursor, m.index));
    const tok = m[0];
    if (tok.startsWith("**")) parts.push(<strong key={key++}>{tok.slice(2, -2)}</strong>);
    else if (tok.startsWith("`")) parts.push(<code key={key++} style={{ fontFamily: "var(--mono)", background: "rgba(0,0,0,0.05)", padding: "1px 5px", borderRadius: 3, fontSize: "0.92em" }}>{tok.slice(1, -1)}</code>);
    else parts.push(<em key={key++}>{tok.slice(1, -1)}</em>);
    cursor = m.index + tok.length;
  }
  if (cursor < text.length) parts.push(text.slice(cursor));
  return parts;
}

function renderTable(lines, k) {
  // Skip separator row (--- | --- | ...)
  const rows = lines.filter(l => !/^\|\s*[-:]+/.test(l)).map(l =>
    l.split("|").slice(1, -1).map(cell => cell.trim())
  );
  if (!rows.length) return null;
  const [header, ...body] = rows;
  return (
    <table key={k} style={{ width: "100%", borderCollapse: "collapse", margin: "12px 0" }}>
      <thead>
        <tr style={{ borderBottom: "2px solid var(--rule, #d6d2c8)" }}>
          {header.map((h, i) => <th key={i} style={{ textAlign: "left", padding: "8px 10px", fontSize: 12, fontWeight: 600 }}>{renderInline(h)}</th>)}
        </tr>
      </thead>
      <tbody>
        {body.map((row, i) => (
          <tr key={i} style={{ borderBottom: "1px solid var(--rule, #e8e4d8)" }}>
            {row.map((cell, j) => <td key={j} style={{ padding: "6px 10px", fontSize: 13, lineHeight: 1.5 }}>{renderInline(cell)}</td>)}
          </tr>
        ))}
      </tbody>
    </table>
  );
}


function IndicesPage({ go, indexId, setIndexId }) {
  const [indicesData, setIndicesData] = React.useState(window.PL_INDICES || null);
  const [externalData, setExternalData] = React.useState(window.PL_INDICES_EXTERNAL || null);
  const [canonText, setCanonText] = React.useState("");
  const [mode, setMode] = React.useState("all");
  const [latestOnly, setLatestOnly] = React.useState(true);
  const [loadError, setLoadError] = React.useState(null);

  // Lazy-load state_indices.json (~2.4 MB) on first mount.
  React.useEffect(() => {
    if (indicesData) return;
    fetch("./state_indices.json")
      .then(r => r.ok ? r.json() : Promise.reject(`HTTP ${r.status}`))
      .then(data => {
        window.PL_INDICES = data;
        setIndicesData(data);
      })
      .catch(err => setLoadError(String(err)));
  }, [indicesData]);

  // Lazy-load indices_external.json (small, cheap). Used for the triangulation
  // panel — when populated, surfaces side-by-side comparison with established
  // external indices (V-Dem, MAP, AARP LTSS, Commonwealth Fund, etc.).
  // Schema doc: rubrics/index_canons/EXTERNAL_DATA_HOWTO.md
  React.useEffect(() => {
    if (externalData) return;
    fetch("./indices_external.json")
      .then(r => r.ok ? r.json() : Promise.reject(`HTTP ${r.status}`))
      .then(data => {
        window.PL_INDICES_EXTERNAL = data;
        setExternalData(data);
      })
      .catch(() => setExternalData({})); // silent fail — panel just hides
  }, [externalData]);

  // Default to first known index when nothing selected
  React.useEffect(() => {
    if (!indexId && KNOWN_INDICES.length) setIndexId(KNOWN_INDICES[0]);
  }, [indexId, setIndexId]);

  // Load canon markdown lazily as user switches indices.
  React.useEffect(() => {
    if (!indexId) return;
    fetch(`./index_canons/${indexId}.md`)
      .then(r => r.ok ? r.text() : Promise.reject(`HTTP ${r.status}`))
      .then(setCanonText)
      .catch(() => setCanonText("Canon file not loaded."));
  }, [indexId]);

  if (loadError) {
    return (
      <div className="pl-page" style={{ padding: "48px clamp(24px, 4vw, 64px)", maxWidth: 800 }}>
        <h1 style={{ fontSize: 32, fontWeight: 600 }}>Indices</h1>
        <p style={{ color: "var(--ink-3)", marginTop: 8 }}>Failed to load indices data: {loadError}</p>
      </div>
    );
  }

  if (!indicesData || !indexId) {
    return (
      <div className="pl-page" style={{ padding: "48px clamp(24px, 4vw, 64px)" }}>
        <div style={{ color: "var(--ink-3)", fontSize: 14 }}>Loading indices…</div>
      </div>
    );
  }

  const restrictive = RESTRICTIVE.has(indexId);

  // Build ranking for the current index + mode
  const rows = [];
  Object.entries(indicesData).forEach(([ss, byIdx]) => {
    const cell = byIdx[indexId]?.modes?.[mode];
    if (!cell || cell.score === undefined) return;
    const [state, sessionId] = ss.split("_");
    rows.push({
      state_session: ss,
      state,
      session_id: sessionId,
      score: cell.score,
      coverage: cell.coverage,
      contributing: cell.contributing,
    });
  });

  // Dedupe to latest session per state
  let displayRows = rows;
  if (latestOnly) {
    const byState = {};
    rows.forEach(r => {
      const sid = parseInt(r.session_id) || 0;
      const ex = byState[r.state];
      if (!ex || sid > (parseInt(ex.session_id) || 0)) byState[r.state] = r;
    });
    displayRows = Object.values(byState);
  }
  displayRows.sort((a, b) => b.score - a.score);

  // Index meta from the first state-session (all states carry the same meta)
  const firstSS = Object.keys(indicesData)[0];
  const meta = indicesData[firstSS]?.[indexId] || {};

  return (
    <div className="pl-page" style={{ padding: "32px clamp(24px, 4vw, 64px) 80px", maxWidth: 1400, margin: "0 auto" }}>
      {/* Page header */}
      <div className="eyebrow" style={{ marginBottom: 8 }}>Layer 2 — Editorial Indices</div>
      <h1 style={{ fontSize: "clamp(32px, 5vw, 56px)", fontWeight: 600, letterSpacing: "-0.02em", lineHeight: 1.05, margin: "0 0 12px" }}>
        {meta.name || indexId}
      </h1>
      <p style={{ fontSize: 16, color: "var(--ink-2)", maxWidth: 760, lineHeight: 1.55, margin: 0 }}>
        {meta.description}
      </p>

      {/* Index tabs */}
      <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginTop: 28, marginBottom: 28, borderBottom: "1px solid var(--rule)", paddingBottom: 12 }}>
        {KNOWN_INDICES.map(id => (
          <button key={id}
            onClick={() => setIndexId(id)}
            style={{
              padding: "7px 13px", fontSize: 13,
              background: id === indexId ? "var(--ink)" : "transparent",
              color: id === indexId ? "var(--bg)" : "var(--ink-2)",
              border: "1px solid var(--rule)",
              borderRadius: 4,
              cursor: "pointer",
              fontWeight: id === indexId ? 600 : 400,
            }}>
            {indicesData[firstSS]?.[id]?.name || id}
          </button>
        ))}
      </div>

      {/* Controls */}
      <div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 18, marginBottom: 20 }}>
        <div style={{ display: "inline-flex", border: "1px solid var(--rule)", borderRadius: 4 }}>
          {MODES.map(m => (
            <button key={m.id}
              onClick={() => setMode(m.id)}
              style={{
                padding: "6px 12px", fontSize: 12,
                background: mode === m.id ? "var(--ink)" : "transparent",
                color: mode === m.id ? "var(--bg)" : "var(--ink-2)",
                border: "none", cursor: "pointer", fontWeight: mode === m.id ? 600 : 400,
              }}>{m.label}</button>
          ))}
        </div>
        <label style={{ display: "inline-flex", alignItems: "center", gap: 6, fontSize: 12, color: "var(--ink-3)", cursor: "pointer" }}>
          <input type="checkbox" checked={latestOnly} onChange={e => setLatestOnly(e.target.checked)} />
          <span>Most recent session per state</span>
        </label>
      </div>

      {/* Two-column: ranking + breakdown */}
      <div style={{ display: "grid", gridTemplateColumns: "minmax(320px, 1fr) minmax(320px, 1fr)", gap: 32, marginBottom: 40 }}>
        {/* Ranked list */}
        <div>
          <div className="eyebrow" style={{ marginBottom: 10 }}>Ranked states · {displayRows.length}</div>
          <div style={{ background: "var(--card-bg, #faf8f3)", border: "1px solid var(--rule)", borderRadius: 6, padding: "8px 0", maxHeight: 600, overflowY: "auto" }}>
            {displayRows.map((row, i) => (
              <div key={`${row.state_session}-${i}`}
                style={{
                  display: "flex", alignItems: "center", justifyContent: "space-between",
                  padding: "7px 16px",
                  borderBottom: i < displayRows.length - 1 ? "1px solid var(--rule, #e8e4d8)" : "none",
                }}>
                <span style={{ fontFamily: "var(--mono)", color: "var(--ink)" }}>
                  {row.state}
                  {!latestOnly && <span style={{ color: "var(--ink-3)", marginLeft: 8, fontSize: 11 }}>{row.session_id}</span>}
                </span>
                <span style={{ fontFamily: "var(--mono)", fontWeight: 600, color: indexColor(row.score, restrictive) }}>
                  {row.score.toFixed(1)}
                </span>
              </div>
            ))}
          </div>
        </div>

        {/* Breakdown for top state */}
        <div>
          <div className="eyebrow" style={{ marginBottom: 10 }}>Top contributors · {displayRows[0]?.state || "—"}</div>
          <div style={{ background: "var(--card-bg, #faf8f3)", border: "1px solid var(--rule)", borderRadius: 6, padding: 16 }}>
            {displayRows[0] ? (
              <>
                <div style={{ fontSize: 14, fontWeight: 600, color: "var(--ink)", marginBottom: 12 }}>
                  {displayRows[0].state} → {displayRows[0].score.toFixed(1)}
                  <span style={{ fontSize: 11, color: "var(--ink-3)", marginLeft: 8, fontWeight: 400 }}>
                    coverage {(displayRows[0].coverage * 100).toFixed(0)}%
                  </span>
                </div>
                <table style={{ width: "100%", fontSize: 12 }}>
                  <thead>
                    <tr style={{ color: "var(--ink-3)", textAlign: "left", borderBottom: "1px solid var(--rule)" }}>
                      <th style={{ padding: "5px 0", fontWeight: 600 }}>Lens</th>
                      <th style={{ padding: "5px 0", textAlign: "right", fontWeight: 600 }}>Wt</th>
                      <th style={{ padding: "5px 0", textAlign: "right", fontWeight: 600 }}>Raw</th>
                      <th style={{ padding: "5px 0", textAlign: "right", fontWeight: 600 }}>Used</th>
                      <th style={{ padding: "5px 0", textAlign: "center", fontWeight: 600 }}>↕</th>
                    </tr>
                  </thead>
                  <tbody>
                    {displayRows[0].contributing.map(c => (
                      <tr key={c.lens} style={{ borderBottom: "1px solid var(--rule, #e8e4d8)" }}>
                        <td style={{ padding: "4px 0", color: "var(--ink)" }}>{c.lens.replace(/_/g, " ")}</td>
                        <td style={{ padding: "4px 0", textAlign: "right", fontFamily: "var(--mono)" }}>{(c.weight * 100).toFixed(0)}%</td>
                        <td style={{ padding: "4px 0", textAlign: "right", fontFamily: "var(--mono)", color: "var(--ink-3)" }}>{c.raw}</td>
                        <td style={{ padding: "4px 0", textAlign: "right", fontFamily: "var(--mono)" }}>{c.used}</td>
                        <td style={{ padding: "4px 0", textAlign: "center", color: c.invert ? "var(--accent)" : "var(--ink-3)" }}>
                          {c.invert ? "⇅" : ""}
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
                <div style={{ marginTop: 10, fontSize: 11, color: "var(--ink-3)", lineHeight: 1.5 }}>
                  ⇅ means inverted: we use (100 − lens_score) so the contribution aligns with the index's editorial direction.
                </div>
              </>
            ) : (
              <div style={{ color: "var(--ink-3)" }}>No data.</div>
            )}
          </div>
        </div>
      </div>

      {/* External comparison / triangulation */}
      <ExternalComparison
        indexId={indexId}
        externalData={externalData}
        displayRows={displayRows}
        restrictive={restrictive}
      />

      {/* Full canon */}
      <div className="eyebrow" style={{ marginBottom: 10 }}>Source canon · methodology · limitations</div>
      <div style={{ background: "var(--card-bg, #faf8f3)", border: "1px solid var(--rule)", borderRadius: 6, padding: "20px 28px", fontSize: 14, color: "var(--ink)" }}>
        {canonText ? renderMarkdown(canonText) : <span style={{ color: "var(--ink-3)" }}>Loading canon…</span>}
      </div>

      {/* Navigation back */}
      <div style={{ marginTop: 32, paddingTop: 20, borderTop: "1px solid var(--rule)" }}>
        <a onClick={() => go("landing")} style={{ color: "var(--accent)", cursor: "pointer", fontSize: 13, textDecoration: "underline" }}>
          ← Back to atlas
        </a>
      </div>
    </div>
  );
}

// ── External comparison panel ───────────────────────────────────────────────
// Shows side-by-side comparison with established external indices (V-Dem,
// MAP, AARP LTSS, etc.). Renders gracefully whether or not the operator has
// populated values in indices_external.json — empty sources show a "not yet
// compiled" note so readers know we plan to triangulate.
// Schema + how-to: rubrics/index_canons/EXTERNAL_DATA_HOWTO.md

function ExternalComparison({ indexId, externalData, displayRows, restrictive }) {
  if (!externalData) return null;
  const indexBlock = externalData[indexId];
  if (!indexBlock || !indexBlock.sources) return null;

  const sources = Object.entries(indexBlock.sources);
  if (sources.length === 0) return null;

  // Build state → our-score map for joining
  const ourByState = {};
  displayRows.forEach(r => { ourByState[r.state] = r.score; });

  return (
    <div style={{ marginTop: 32, marginBottom: 32 }}>
      <div className="eyebrow" style={{ marginBottom: 10 }}>
        External comparison · triangulation
      </div>
      <div style={{ fontSize: 13, color: "var(--ink-3)", marginBottom: 16, maxWidth: 720, lineHeight: 1.55 }}>
        Established indices that measure adjacent constructs. When our score sharply
        disagrees with one of these for a state, that's a signal worth investigating —
        could be a methodological difference, a filter gap, or a real divergence.
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(360px, 1fr))", gap: 16 }}>
        {sources.map(([sourceId, source]) => (
          <ExternalSourceCard
            key={sourceId}
            source={source}
            ourByState={ourByState}
            restrictive={restrictive}
          />
        ))}
      </div>

      {indexBlock._note && (
        <div style={{ marginTop: 12, fontSize: 11, color: "var(--ink-3)", fontStyle: "italic" }}>
          {indexBlock._note}
        </div>
      )}
    </div>
  );
}

function ExternalSourceCard({ source, ourByState, restrictive }) {
  const values = source.values || {};
  const stateEntries = Object.entries(values);
  const hasData = stateEntries.length > 0;

  // Polarity alignment: if both indices "high = same thing" then we say "aligned",
  // else "inverted." Used to interpret divergences.
  const polarityAligns = source.polarity_aligns_with_our_index;
  const polarityLabel =
    polarityAligns === true  ? "polarity aligned"
  : polarityAligns === false ? "polarity inverted"
  : "polarity unspecified";

  return (
    <div style={{
      background: "var(--card-bg, #faf8f3)",
      border: "1px solid var(--rule)",
      borderRadius: 6,
      padding: "14px 16px",
    }}>
      {/* Header */}
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 8 }}>
        <div style={{ fontSize: 14, fontWeight: 600, color: "var(--ink)", lineHeight: 1.3 }}>
          {source.name}
        </div>
        {source.as_of && (
          <div style={{ fontSize: 11, color: "var(--ink-3)", fontFamily: "var(--mono)", whiteSpace: "nowrap" }}>
            as of {source.as_of}
          </div>
        )}
      </div>

      {/* Metadata strip */}
      <div style={{ marginTop: 6, fontSize: 11, color: "var(--ink-3)", lineHeight: 1.5 }}>
        {source.url && (
          <>
            <a href={source.url} target="_blank" rel="noopener noreferrer"
               style={{ color: "var(--accent)", textDecoration: "underline" }}>
              source ↗
            </a>
            <span style={{ margin: "0 6px" }}>·</span>
          </>
        )}
        {source.higher_is && (
          <>
            <span>higher = {source.higher_is}</span>
            <span style={{ margin: "0 6px" }}>·</span>
          </>
        )}
        <span>{polarityLabel}</span>
      </div>

      {/* Body */}
      <div style={{ marginTop: 12 }}>
        {hasData ? (
          <ComparisonTable
            stateEntries={stateEntries}
            ourByState={ourByState}
            polarityAligns={polarityAligns}
            restrictive={restrictive}
          />
        ) : (
          <div style={{
            padding: "12px 0",
            fontSize: 12,
            color: "var(--ink-3)",
            fontStyle: "italic",
            lineHeight: 1.55,
          }}>
            Not yet compiled. State-level values are published in the source
            report; comparison will appear here once values are populated in
            <code style={{ fontFamily: "var(--mono)", padding: "0 4px", fontStyle: "normal" }}>indices_external.json</code>.
          </div>
        )}
      </div>
    </div>
  );
}

function ComparisonTable({ stateEntries, ourByState, polarityAligns, restrictive }) {
  // Build joined rows where BOTH our score and their value exist.
  // Sort by our score descending (matches the main ranking).
  const joined = stateEntries
    .filter(([state]) => ourByState[state] !== undefined)
    .map(([state, theirValue]) => ({
      state,
      ours: ourByState[state],
      theirs: theirValue,
    }))
    .sort((a, b) => b.ours - a.ours);

  if (joined.length === 0) {
    return (
      <div style={{ fontSize: 12, color: "var(--ink-3)", fontStyle: "italic" }}>
        No state overlap between this source and our scored states.
      </div>
    );
  }

  // Show top 5 + bottom 5 when there's enough data; else show all.
  const showSplit = joined.length > 12;
  const topN = showSplit ? joined.slice(0, 5) : joined;
  const botN = showSplit ? joined.slice(-5) : [];

  // Compute correlation-ish divergence flag for each row.
  // If polarityAligns: rows where ours is HIGH and theirs is LOW = divergence.
  // If !polarityAligns: rows where ours is HIGH and theirs is HIGH = divergence.
  const theirValues = joined.map(j => j.theirs).filter(v => typeof v === "number").sort((a, b) => a - b);
  const theirMedian = theirValues.length ? theirValues[Math.floor(theirValues.length / 2)] : null;

  const renderRow = (row) => {
    const ours = row.ours;
    const theirs = row.theirs;
    const ourHigh = ours >= 50;
    let divergent = false;
    if (theirMedian !== null && typeof theirs === "number") {
      const theirHigh = theirs >= theirMedian;
      // If polarity aligned: expect ourHigh == theirHigh. Divergent when opposite.
      // If inverted: expect ourHigh != theirHigh. Divergent when same.
      divergent = polarityAligns ? (ourHigh !== theirHigh) : (ourHigh === theirHigh);
    }
    return (
      <tr key={row.state} style={{ borderBottom: "1px solid var(--rule, #e8e4d8)" }}>
        <td style={{ padding: "4px 0", fontFamily: "var(--mono)", fontSize: 12, color: "var(--ink)" }}>{row.state}</td>
        <td style={{ padding: "4px 8px", textAlign: "right", fontFamily: "var(--mono)", fontSize: 12, color: "var(--ink)" }}>
          {ours.toFixed(1)}
        </td>
        <td style={{ padding: "4px 8px", textAlign: "right", fontFamily: "var(--mono)", fontSize: 12, color: "var(--ink-3)" }}>
          {typeof theirs === "number" ? theirs.toLocaleString() : String(theirs)}
        </td>
        <td style={{ padding: "4px 0", textAlign: "center", fontSize: 11, color: divergent ? "var(--accent)" : "var(--ink-3)" }}>
          {divergent ? "⚠" : "✓"}
        </td>
      </tr>
    );
  };

  return (
    <>
      <table style={{ width: "100%", fontSize: 12 }}>
        <thead>
          <tr style={{ color: "var(--ink-3)", textAlign: "left", borderBottom: "1px solid var(--rule)" }}>
            <th style={{ padding: "5px 0", fontWeight: 600 }}>State</th>
            <th style={{ padding: "5px 8px", textAlign: "right", fontWeight: 600 }}>Ours</th>
            <th style={{ padding: "5px 8px", textAlign: "right", fontWeight: 600 }}>Theirs</th>
            <th style={{ padding: "5px 0", textAlign: "center", fontWeight: 600, width: 28 }}>≈</th>
          </tr>
        </thead>
        <tbody>
          {topN.map(renderRow)}
          {showSplit && (
            <tr>
              <td colSpan={4} style={{ padding: "6px 0", textAlign: "center", color: "var(--ink-3)", fontSize: 10, letterSpacing: "0.2em" }}>
                ··· {joined.length - 10} STATES ···
              </td>
            </tr>
          )}
          {botN.map(renderRow)}
        </tbody>
      </table>
      <div style={{ marginTop: 8, fontSize: 10, color: "var(--ink-3)", lineHeight: 1.5 }}>
        ✓ = directional agreement (both above/below median per their polarity). ⚠ = divergence worth investigating.
      </div>
    </>
  );
}


window.PL_IndicesPage = IndicesPage;
window.PL_KNOWN_INDICES = KNOWN_INDICES;
}
