// App shell — router + nav + tweaks
{
const { LENSES } = window.PL_DATA;
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"mode": "light",
"accent": "#c2410c",
"mapStyle": "tile",
"showLabels": "always",
"density": "comfy"
}/*EDITMODE-END*/;
function App() {
// intro = cinematic landing screen; atlas = full-bleed map experience
const [phase, setPhase] = React.useState(() => {
// Skip intro on subsequent visits in the same session
return sessionStorage.getItem("pl_introSeen") === "1" ? "atlas" : "intro";
});
const [page, setPage] = React.useState("landing");
const [lensId, setLensId] = React.useState("reproductive");
const [indexId, setIndexId] = React.useState("fascism");
const [t, setTweak] = window.useTweaks(TWEAK_DEFAULTS);
React.useEffect(() => {
document.documentElement.setAttribute("data-mode", t.mode);
document.documentElement.setAttribute("data-density", t.density);
document.documentElement.style.setProperty("--accent", t.accent);
}, [t.mode, t.density, t.accent]);
const enterAtlas = () => {
sessionStorage.setItem("pl_introSeen", "1");
setPhase("atlas");
};
const go = (p) => { setPage(p); window.scrollTo({ top: 0, behavior: "instant" }); };
// Intro is overlaid on top of the rendered atlas — that way when it fades
// out, the (light-themed) atlas is already painted underneath. No jarring
// theme flip.
return (
{phase === "intro" && }
{page === "landing" && (
)}
{page === "state" && (
)}
{page === "lens" && (
)}
{page === "indices" && (
)}
{page === "methodology" && }
{page === "specimen" && }
setTweak("mode", v)}
options={[{value:"light",label:"Light"},{value:"dark",label:"Dark"}]} />
setTweak("density", v)}
options={[{value:"comfy",label:"Comfy"},{value:"dense",label:"Dense"}]} />
setTweak("accent", v)}
options={["#c2410c", "#a87916", "#2a6b66", "#5e1e2c"]} />
setTweak("showLabels", v)}
options={[{value:"always",label:"Always"},{value:"hover",label:"Hover"},{value:"never",label:"Off"}]} />
);
}
// --- Cinematic intro screen ----------------------------------------------
const ROTATING_WORDS = [
"life", "work", "health", "voice", "freedom", "school",
"home", "body", "vote", "harm", "hope", "choice",
];
function Intro({ enter }) {
const [visible, setVisible] = React.useState(false);
const [exiting, setExiting] = React.useState(false);
const [wordIdx, setWordIdx] = React.useState(0);
React.useEffect(() => {
const t = setTimeout(() => setVisible(true), 60);
return () => clearTimeout(t);
}, []);
// Cycle the rotating word slowly, after the intro has appeared
React.useEffect(() => {
if (!visible) return;
const t = setInterval(() => setWordIdx(i => (i + 1) % ROTATING_WORDS.length), 2400);
return () => clearInterval(t);
}, [visible]);
// Click anywhere or press any key to enter
React.useEffect(() => {
const onKey = (e) => { if (e.key) handleEnter(); };
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
});
const handleEnter = () => {
if (exiting) return;
setExiting(true);
setTimeout(() => enter(), 480);
};
return (
POLICY LANDSCAPE · ATLAS OF LEGISLATIVE MOTION · 2025–26 SESSION
How law shapes{" "}
{ROTATING_WORDS[wordIdx]}
.
State by state. Bill by bill. With the receipts.
We score every U.S. legislature against an explicit framework of
human rights, public health, and democratic norms — and show our work.
);
}
// Set BUY_ME_A_COFFEE_URL to your buymeacoffee.com handle URL (e.g.,
// "https://buymeacoffee.com/your-handle") to activate the support link.
// Leave as empty string to hide the link entirely.
const BUY_ME_A_COFFEE_URL = "";
function Footer({ go }) {
return (
);
}
// Show a brief loading state while live data loads from the dashboard API,
// then mount the app. Avoids rendering with placeholder neutral scores.
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(