// State Detail page — human-first reframe. // Hero: state name (moderate scale) + ONE editorial finding. // Summary card: visual at-a-glance — top lenses + party split + count. // Lens spectrum: ALL lenses with data, no overflow. // Featured bills: cards with evidence. { const { LENSES, STATES, scoreFor, lensMetaFor, billsFor, billCountFor } = window.PL_DATA; function StatePage({ go, setLensId }) { const code = window.__plSelectedState || "TX"; const st = STATES.find(s => s[0] === code); if (!st) return
Missing state.
; const [, name] = st; // Lenses where this state has actually-scored data const lensRows = LENSES .map(l => { const meta = lensMetaFor(code, l.id); const passed = scoreFor(code, l.id, "passed"); const pressure = scoreFor(code, l.id, "pressure"); return { ...l, passed, pressure, meta, billCount: meta?.bill_count || 0, nPassed: meta?.n_passed || 0 }; }) .filter(l => l.billCount > 0) .sort((a, b) => b.billCount - a.billCount); const totalBills = lensRows.reduce((s, l) => s + l.billCount, 0); const totalPassed = lensRows.reduce((s, l) => s + l.nPassed, 0); // Top 3 most-active lenses, used to drive the headline finding const topLenses = lensRows.slice(0, 3); // Default active lens = the most-scored one const [activeLens, setActiveLens] = React.useState(topLenses[0]?.id || "reproductive"); const activeRow = lensRows.find(l => l.id === activeLens) || topLenses[0]; // Editorial finding driven by the data const finding = buildFinding(name, lensRows); return (
{/* HERO — state name + editorial finding */}

{name}

{code} · 2025–26 SESSION

{finding}

{/* Summary card — compact visual at-a-glance */}
{/* LENS SPECTRUM — every lens with data, sorted by activity */}
The {name} lens spectrum

Each row is one policy lens with at least one scored bill in this state. Filled bar = bills that became law. Dashed outline = all bills introduced (the pressure). Click a row to see its bills below.

{lensRows.length === 0 ? (
No bills scored for this state yet.
) : (
{lensRows.map(l => ( setActiveLens(l.id)} /> ))}
)}
{/* BILLS for the active lens */} {activeRow && ( )}
); } // --- Summary card — visual data at-a-glance ------------------------------ function SummaryCard({ code, name, totalBills, totalPassed, topLenses }) { // Compute aggregate party split across top 3 lenses const partyTotals = { D: 0, R: 0, other: 0 }; let directionRestrict = 0; let directionExpand = 0; topLenses.forEach(l => { const sp = l.meta?.sponsor_party_by_direction || {}; Object.entries(sp).forEach(([dir, parties]) => { Object.entries(parties).forEach(([p, n]) => { if (p === "D") partyTotals.D += n; else if (p === "R") partyTotals.R += n; else partyTotals.other += n; }); if (dir === "expand") directionExpand += Object.values(parties).reduce((s,n)=>s+n,0); if (dir === "restrict") directionRestrict += Object.values(parties).reduce((s,n)=>s+n,0); }); }); const partyTotal = partyTotals.D + partyTotals.R + partyTotals.other; const dPct = partyTotal ? Math.round(100 * partyTotals.D / partyTotal) : 0; const rPct = partyTotal ? Math.round(100 * partyTotals.R / partyTotal) : 0; const oPct = Math.max(0, 100 - dPct - rPct); return (
Bills tracked
{totalBills.toLocaleString()}
Became law
{totalPassed.toLocaleString()}
{topLenses.length > 0 && (
Most-active lenses
{topLenses.map(l => { const color = window.PL_COLOR(l.passed).color; return (
{l.label}
{l.billCount} bills · {l.nPassed} passed
); })}
)} {partyTotal > 0 && (
Bill sponsorship · top lenses
{dPct > 0 &&
} {rPct > 0 &&
} {oPct > 0 &&
}
D {dPct}% R {rPct}% {oPct > 0 && OTHER {oPct}%}
)}
); } // --- Single lens spectrum row -------------------------------------------- function LensRow({ lens, isActive, onSelect }) { const passedColor = window.PL_COLOR(lens.passed).color; return ( ); } // --- Featured bills for a lens (real data when available) ---------------- function BillsForLens({ code, lens, go, setLensId }) { const [bills, setBills] = React.useState(null); React.useEffect(() => { setBills(null); billsFor(code, lens.id, { limit: 20 }).then(setBills); }, [code, lens.id]); return (
{code} · {lens.label}

{lens.billCount} bills · {lens.nPassed} became law

{bills === null &&
Loading bills…
} {bills && bills.length === 0 && (
No bills available for this lens yet.
)} {bills && bills.length > 0 && (
{bills.slice(0, 12).map(b => )}
)}
); } function BillCard({ bill }) { const lensScores = bill.lens_scores || []; const primaryDirection = lensScores[0]?.direction || "neutral"; const dirColor = primaryDirection === "expand" ? "#2a6b66" : primaryDirection === "restrict" ? "#b85a4a" : "var(--ink-4)"; return (
{bill.bill_number} {bill.status_label} {bill.legislative_character && ( {bill.legislative_character.replace(/_/g, " ")} )}
{bill.title}
{bill.summary && (
{bill.summary}
)} {lensScores.length > 0 && (
{lensScores.slice(0, 4).map((ls, i) => ( {ls.lens.replace(/_/g, " ")} {ls.direction === "expand" ? "↑" : ls.direction === "restrict" ? "↓" : "·"} {(ls.magnitude || "")[0]?.toUpperCase() || ""} ))}
)}
); } // --- Editorial finding builder (data-driven, simple) --------------------- function buildFinding(stateName, lensRows) { if (lensRows.length === 0) { return `${stateName} has no scored bills yet for the lenses we track. Pipeline is filling in — check back as more states are added.`; } const top = lensRows[0]; const score = top.passed; const direction = score >= 60 ? "expanding" : score < 40 ? "restricting" : "evenly split on"; const verb = top.nPassed === 0 ? "introduced" : "passed"; return `${stateName}'s most-active lens this session is ${top.label.toLowerCase()} — ${top.billCount} bills tracked, ${top.nPassed} became law. The lawmaking is ${direction} ${top.label.toLowerCase()}.`; } window.PL_StatePage = StatePage; }