-
-
-
-
-
-
- 2026 State Legislative Tracker
-
-
- PolicyEngine State Tax Research
-
-
+
+
+ 2026 State Legislative Tracker
+
+
+ PolicyEngine State Tax Research
+
@@ -338,44 +331,8 @@ function App() {
)}
- {/* Footer */}
-
+ {/* PE org footer */}
+
);
}
@@ -536,27 +493,4 @@ function QuickLinkCard({ href, title, description }) {
);
}
-// Footer Link Component
-function FooterLink({ href, children }) {
- return (
-
e.currentTarget.style.color = colors.primary[300]}
- onMouseLeave={(e) => e.currentTarget.style.color = colors.gray[400]}
- >
- {children}
-
- );
-}
-
export default App;
diff --git a/src/components/PolicyEngineFooter.jsx b/src/components/PolicyEngineFooter.jsx
new file mode 100644
index 0000000..4432060
--- /dev/null
+++ b/src/components/PolicyEngineFooter.jsx
@@ -0,0 +1,206 @@
+import { colors, typography } from '../designTokens';
+
+/**
+ * PolicyEngine org footer — matches policyengine.org production footer.
+ * Adapted from keep-your-pay-act Footer.tsx.
+ *
+ * TODO: Replace this entire file with `import { Footer } from "@policyengine/ui-kit"`
+ * once ui-kit 0.4.0 is published to npm. See:
+ * - https://github.com/PolicyEngine/policyengine-ui-kit/issues/18 (build OOM blocking publish)
+ * - https://github.com/PolicyEngine/policyengine-ui-kit/issues/16 (changelog/publish pipeline)
+ */
+
+const COLORS = {
+ primary500: colors.primary[500],
+ primary600: colors.primary[600],
+ primary800: colors.primary[800],
+};
+
+const FONT = typography.fontFamily.primary;
+
+const NAV_LINKS = [
+ { href: 'https://policyengine.org/us/team', text: 'About us' },
+ { href: 'https://policyengine.org/us/donate', text: 'Donate' },
+ { href: 'https://policyengine.org/us/privacy', text: 'Privacy policy' },
+ { href: 'https://policyengine.org/us/terms', text: 'Terms and conditions' },
+];
+
+const SOCIAL_LINKS = [
+ {
+ label: 'Email',
+ href: 'mailto:hello@policyengine.org',
+ icon: (
+
+ ),
+ },
+ {
+ label: 'Twitter',
+ href: 'https://twitter.com/ThePolicyEngine',
+ icon: (
+
+ ),
+ },
+ {
+ label: 'Facebook',
+ href: 'https://www.facebook.com/PolicyEngine',
+ icon: (
+
+ ),
+ },
+ {
+ label: 'LinkedIn',
+ href: 'https://www.linkedin.com/company/thepolicyengine',
+ icon: (
+
+ ),
+ },
+ {
+ label: 'YouTube',
+ href: 'https://www.youtube.com/@policyengine',
+ icon: (
+
+ ),
+ },
+ {
+ label: 'Instagram',
+ href: 'https://www.instagram.com/PolicyEngine/',
+ icon: (
+
+ ),
+ },
+ {
+ label: 'GitHub',
+ href: 'https://github.com/PolicyEngine',
+ icon: (
+
+ ),
+ },
+];
+
+export default function PolicyEngineFooter() {
+ return (
+
+ );
+}
diff --git a/src/components/PolicyEngineHeader.jsx b/src/components/PolicyEngineHeader.jsx
new file mode 100644
index 0000000..93c5090
--- /dev/null
+++ b/src/components/PolicyEngineHeader.jsx
@@ -0,0 +1,499 @@
+import { useCallback, useState, useRef, useEffect } from 'react';
+import { colors, typography } from '../designTokens';
+
+/**
+ * PolicyEngine org header — matches policyengine.org production header.
+ * Adapted from keep-your-pay-act Header.tsx with added API and Citations nav items.
+ *
+ * TODO: Replace this entire file with `import { Header } from "@policyengine/ui-kit"`
+ * once ui-kit 0.4.0 is published to npm. See:
+ * - https://github.com/PolicyEngine/policyengine-ui-kit/issues/18 (build OOM blocking publish)
+ * - https://github.com/PolicyEngine/policyengine-ui-kit/issues/16 (changelog/publish pipeline)
+ */
+
+const COLORS = {
+ primary500: colors.primary[500],
+ primary600: colors.primary[600],
+ primary700: colors.primary[700],
+ primary800: colors.primary[800],
+ textInverse: colors.text.inverse,
+ shadowLight: 'rgba(16, 24, 40, 0.05)',
+ shadowMedium: 'rgba(16, 24, 40, 0.1)',
+};
+
+const FONT = typography.fontFamily.primary;
+
+const NAV_ITEMS = [
+ { label: 'Research', href: 'https://policyengine.org/us/research' },
+ { label: 'Model', href: 'https://policyengine.org/us/model' },
+ { label: 'API', href: 'https://policyengine.org/us/api' },
+ {
+ label: 'About',
+ hasDropdown: true,
+ items: [
+ { label: 'Team', href: 'https://policyengine.org/us/team' },
+ { label: 'Supporters', href: 'https://policyengine.org/us/supporters' },
+ { label: 'Citations', href: 'https://policyengine.org/us/citations' },
+ ],
+ },
+ { label: 'Donate', href: 'https://policyengine.org/us/donate' },
+];
+
+const COUNTRIES = [
+ { id: 'us', label: 'United States' },
+ { id: 'uk', label: 'United Kingdom' },
+];
+
+const navItemStyle = {
+ color: COLORS.textInverse,
+ fontWeight: 500,
+ fontSize: '15px',
+ fontFamily: FONT,
+ textDecoration: 'none',
+ padding: '6px 14px',
+ borderRadius: '6px',
+ transition: 'background-color 0.15s ease',
+ letterSpacing: '0.01em',
+};
+
+const hoverHandlers = {
+ onMouseEnter: (e) => {
+ e.currentTarget.style.backgroundColor = 'rgba(255, 255, 255, 0.12)';
+ },
+ onMouseLeave: (e) => {
+ e.currentTarget.style.backgroundColor = 'transparent';
+ },
+};
+
+function AppleDropdown({ items, open, onClose, align = 'center' }) {
+ const contentRef = useRef(null);
+ const [contentHeight, setContentHeight] = useState(0);
+ const [visible, setVisible] = useState(false);
+
+ useEffect(() => {
+ if (open && contentRef.current) {
+ setContentHeight(contentRef.current.scrollHeight);
+ requestAnimationFrame(() => setVisible(true));
+ } else {
+ setVisible(false);
+ const timer = setTimeout(() => setContentHeight(0), 250);
+ return () => clearTimeout(timer);
+ }
+ }, [open]);
+
+ if (!open && contentHeight === 0) return null;
+
+ const positionStyle =
+ align === 'right'
+ ? { right: 0, transform: visible ? 'translateY(0)' : 'translateY(-8px)' }
+ : {
+ left: '50%',
+ transform: visible
+ ? 'translateX(-50%) translateY(0)'
+ : 'translateX(-50%) translateY(-8px)',
+ };
+
+ return (
+ <>
+
+
+
+ {items.map((item, i) => (
+
onClose()}
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ width: '100%',
+ textAlign: 'left',
+ padding: '11px 16px',
+ borderRadius: '10px',
+ border: 'none',
+ background: 'transparent',
+ cursor: 'pointer',
+ fontSize: '14px',
+ fontFamily: FONT,
+ fontWeight: 600,
+ color: COLORS.primary800,
+ textDecoration: 'none',
+ transition: 'background-color 0.12s ease, color 0.12s ease, opacity 0.3s ease',
+ transitionDelay: visible ? `${i * 50}ms` : '0ms',
+ opacity: visible ? 1 : 0,
+ lineHeight: '1.3',
+ letterSpacing: '-0.01em',
+ }}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.backgroundColor = COLORS.primary500;
+ e.currentTarget.style.color = COLORS.textInverse;
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.backgroundColor = 'transparent';
+ e.currentTarget.style.color = COLORS.primary800;
+ }}
+ >
+ {item.label}
+
+ ))}
+
+
+ >
+ );
+}
+
+function CountrySelector() {
+ const [open, setOpen] = useState(false);
+ const containerRef = useRef(null);
+ const contentRef = useRef(null);
+ const [contentHeight, setContentHeight] = useState(0);
+ const [visible, setVisible] = useState(false);
+
+ useEffect(() => {
+ if (open && contentRef.current) {
+ setContentHeight(contentRef.current.scrollHeight);
+ requestAnimationFrame(() => setVisible(true));
+ } else {
+ setVisible(false);
+ const timer = setTimeout(() => setContentHeight(0), 250);
+ return () => clearTimeout(timer);
+ }
+ }, [open]);
+
+ useEffect(() => {
+ if (!open) return;
+ function handleClick(e) {
+ if (containerRef.current && !containerRef.current.contains(e.target)) setOpen(false);
+ }
+ function handleKey(e) {
+ if (e.key === 'Escape') setOpen(false);
+ }
+ document.addEventListener('mousedown', handleClick);
+ document.addEventListener('keydown', handleKey);
+ return () => {
+ document.removeEventListener('mousedown', handleClick);
+ document.removeEventListener('keydown', handleKey);
+ };
+ }, [open]);
+
+ return (
+
+
+
+ {(open || contentHeight > 0) && (
+ <>
+
setOpen(false)}
+ style={{ position: 'fixed', inset: 0, zIndex: 999, cursor: 'default' }}
+ />
+
+ >
+ )}
+
+ );
+}
+
+export default function PolicyEngineHeader() {
+ const [aboutOpen, setAboutOpen] = useState(false);
+ const [mobileOpen, setMobileOpen] = useState(false);
+ const aboutRef = useRef(null);
+
+ useEffect(() => {
+ if (!aboutOpen) return;
+ function handleClick(e) {
+ if (aboutRef.current && !aboutRef.current.contains(e.target)) setAboutOpen(false);
+ }
+ function handleKey(e) {
+ if (e.key === 'Escape') setAboutOpen(false);
+ }
+ document.addEventListener('mousedown', handleClick);
+ document.addEventListener('keydown', handleKey);
+ return () => {
+ document.removeEventListener('mousedown', handleClick);
+ document.removeEventListener('keydown', handleKey);
+ };
+ }, [aboutOpen]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {mobileOpen && (
+ <>
+ setMobileOpen(false)}
+ />
+
+
+ Menu
+
+
+
+ {NAV_ITEMS.map((item) =>
+ item.hasDropdown ? (
+
+ ) : (
+
+ {item.label}
+
+ ),
+ )}
+
+
+ >
+ )}
+
+ );
+}
diff --git a/src/components/reform/ChartExportWrapper.jsx b/src/components/reform/ChartExportWrapper.jsx
index c4cd8ae..38c922f 100644
--- a/src/components/reform/ChartExportWrapper.jsx
+++ b/src/components/reform/ChartExportWrapper.jsx
@@ -2,7 +2,7 @@ import { useRef, useCallback } from "react";
import { toPng } from "html-to-image";
import { colors, typography, spacing } from "../../designTokens";
-const PE_LOGO_URL = "/policyengine-logo.svg";
+const PE_LOGO_URL = "https://www.policyengine.org/assets/logos/policyengine/teal.svg";
const DownloadIcon = () => (