← back to Norma

components/congress/CongressTab.tsx

1404 lines

'use client';

import { useState, useEffect, useCallback, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
  Search,
  RefreshCw,
  Phone,
  Globe,
  Twitter,
  Youtube,
  Facebook,
  ChevronDown,
  ChevronUp,
  Users,
  MapPin,
  School,
  Inbox,
  ExternalLink,
  Linkedin,
  ArrowUpDown,
  AlertTriangle,
  Star,
  TrendingUp,
  Map,
  BarChart3,
} from 'lucide-react';

/* ─── Types ──────────────────────────────────────────────────────────────── */

interface SocialMedia {
  twitter?: string | null;
  facebook?: string | null;
  youtube?: string | null;
}

interface Staffer {
  full_name: string;
  title: string;
  role_category: string;
  email: string | null;
  phone: string | null;
  office_location: string | null;
  is_dc_office: boolean;
  linkedin_url: string | null;
  twitter_handle: string | null;
  handles_education: boolean;
  handles_finance: boolean;
  is_key_contact: boolean;
}

interface CommunityCollege {
  school_name: string;
  city: string;
  state: string;
  zip: string;
  enrollment: number | null;
  median_debt: number | null;
  pell_grant_rate: number | null;
  completion_rate: number | null;
  retention_rate: number | null;
  tuition_in_state: number | null;
  avg_net_price: number | null;
  median_earnings_6yr: number | null;
  school_url: string | null;
}

interface CongressMember {
  name: string;
  title: string;
  party: string;
  state: string;
  district: string | null;
  congressional_district: string | null;
  phone: string | null;
  website: string | null;
  email: string | null;
  social_media: SocialMedia | null;
  photo_url: string | null;
  bioguide_id: string;
  votes_with_party_pct: number | null;
  missed_votes_pct: number | null;
  committees: string[] | null;
  position_student_debt: string | null;
  zip_count: number | null;
  community_college_count: number | null;
  total_cc_enrollment: number | null;
  avg_cc_median_debt: number | null;
  avg_cc_pell_rate: number | null;
  highest_debt_college: string | null;
  total_complaints: number | null;
  avg_distress_score: number | null;
  opportunity_score: number | null;
  opportunity_factors: Record<string, unknown> | null;
  staffers: Staffer[];
  community_colleges: CommunityCollege[];
}

interface ApiStats {
  total_members: number;
  avg_opportunity_score: number | null;
  total_cc_enrollment: number;
  high_priority_count: number;
}

interface ApiResponse {
  members: CongressMember[];
  total: number;
  stats?: ApiStats;
  limit: number;
  offset: number;
}

/* ─── Constants ──────────────────────────────────────────────────────────── */

const US_STATES = [
  { value: '', label: 'All States' },
  { value: 'AL', label: 'Alabama' }, { value: 'AK', label: 'Alaska' },
  { value: 'AZ', label: 'Arizona' }, { value: 'AR', label: 'Arkansas' },
  { value: 'CA', label: 'California' }, { value: 'CO', label: 'Colorado' },
  { value: 'CT', label: 'Connecticut' }, { value: 'DE', label: 'Delaware' },
  { value: 'FL', label: 'Florida' }, { value: 'GA', label: 'Georgia' },
  { value: 'HI', label: 'Hawaii' }, { value: 'ID', label: 'Idaho' },
  { value: 'IL', label: 'Illinois' }, { value: 'IN', label: 'Indiana' },
  { value: 'IA', label: 'Iowa' }, { value: 'KS', label: 'Kansas' },
  { value: 'KY', label: 'Kentucky' }, { value: 'LA', label: 'Louisiana' },
  { value: 'ME', label: 'Maine' }, { value: 'MD', label: 'Maryland' },
  { value: 'MA', label: 'Massachusetts' }, { value: 'MI', label: 'Michigan' },
  { value: 'MN', label: 'Minnesota' }, { value: 'MS', label: 'Mississippi' },
  { value: 'MO', label: 'Missouri' }, { value: 'MT', label: 'Montana' },
  { value: 'NE', label: 'Nebraska' }, { value: 'NV', label: 'Nevada' },
  { value: 'NH', label: 'New Hampshire' }, { value: 'NJ', label: 'New Jersey' },
  { value: 'NM', label: 'New Mexico' }, { value: 'NY', label: 'New York' },
  { value: 'NC', label: 'North Carolina' }, { value: 'ND', label: 'North Dakota' },
  { value: 'OH', label: 'Ohio' }, { value: 'OK', label: 'Oklahoma' },
  { value: 'OR', label: 'Oregon' }, { value: 'PA', label: 'Pennsylvania' },
  { value: 'RI', label: 'Rhode Island' }, { value: 'SC', label: 'South Carolina' },
  { value: 'SD', label: 'South Dakota' }, { value: 'TN', label: 'Tennessee' },
  { value: 'TX', label: 'Texas' }, { value: 'UT', label: 'Utah' },
  { value: 'VT', label: 'Vermont' }, { value: 'VA', label: 'Virginia' },
  { value: 'WA', label: 'Washington' }, { value: 'WV', label: 'West Virginia' },
  { value: 'WI', label: 'Wisconsin' }, { value: 'WY', label: 'Wyoming' },
  { value: 'DC', label: 'D.C.' },
];

const HIGH_DEBT_THRESHOLD = 30000;

type CollegeSort = {
  key: keyof CommunityCollege;
  dir: 'asc' | 'desc';
};

/* ─── Helpers ────────────────────────────────────────────────────────────── */

function fmt$(n: number | null | undefined): string {
  if (n == null) return '—';
  return '$' + n.toLocaleString('en-US', { maximumFractionDigits: 0 });
}

function fmtPct(n: number | null | undefined): string {
  if (n == null) return '—';
  return (n * 100).toFixed(1) + '%';
}

function fmtNum(n: number | null | undefined): string {
  if (n == null) return '—';
  return n.toLocaleString('en-US');
}

/** Returns party colors: background tint, text color, border color */
function partyColors(party: string): { bg: string; text: string; border: string; leftBorder: string } {
  switch (party?.toUpperCase()) {
    case 'D':
      return {
        bg: 'rgba(37,99,235,0.06)',
        text: '#60a5fa',
        border: 'rgba(37,99,235,0.25)',
        leftBorder: '#2563eb',
      };
    case 'R':
      return {
        bg: 'rgba(239,68,68,0.06)',
        text: '#f87171',
        border: 'rgba(239,68,68,0.25)',
        leftBorder: '#ef4444',
      };
    default:
      return {
        bg: 'rgba(168,85,247,0.06)',
        text: '#c084fc',
        border: 'rgba(168,85,247,0.25)',
        leftBorder: '#a855f7',
      };
  }
}

function partyLabel(party: string): string {
  switch (party?.toUpperCase()) {
    case 'D': return 'Democrat';
    case 'R': return 'Republican';
    case 'I': return 'Independent';
    default: return party ?? '—';
  }
}

/**
 * Opportunity score gauge: 0-100 where higher = higher priority (red).
 * Returns gradient stop color from green (low) to red (high).
 */
function scoreColor(score: number | null): string {
  if (score == null) return 'var(--color-text-muted)';
  if (score >= 75) return '#ef4444';
  if (score >= 50) return '#f59e0b';
  if (score >= 25) return '#eab308';
  return '#22c55e';
}

function scoreBg(score: number | null): string {
  if (score == null) return 'rgba(113,113,122,0.15)';
  if (score >= 75) return 'rgba(239,68,68,0.12)';
  if (score >= 50) return 'rgba(245,158,11,0.12)';
  if (score >= 25) return 'rgba(234,179,8,0.12)';
  return 'rgba(34,197,94,0.12)';
}

/* ─── OpportunityGauge ───────────────────────────────────────────────────── */

function OpportunityGauge({ score }: { score: number | null }) {
  const pct = score ?? 0;
  const color = scoreColor(score);
  const bg = scoreBg(score);
  const label = score == null ? '—' : score >= 75 ? 'High' : score >= 50 ? 'Med' : score >= 25 ? 'Low' : 'Min';

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
        <span style={{ fontSize: '0.65rem', color: 'var(--color-text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
          Priority
        </span>
        <span
          className="badge"
          style={{
            backgroundColor: bg,
            color,
            border: `1px solid ${color}40`,
            fontSize: '0.6rem',
            padding: '1px 6px',
          }}
        >
          {label} {score != null ? score : ''}
        </span>
      </div>
      {/* Gauge bar */}
      <div
        style={{
          height: 4,
          borderRadius: 2,
          backgroundColor: 'var(--color-surface-el)',
          overflow: 'hidden',
          border: '1px solid var(--color-border)',
        }}
        role="progressbar"
        aria-valuenow={pct}
        aria-valuemin={0}
        aria-valuemax={100}
        aria-label={`Opportunity score: ${pct}`}
      >
        <motion.div
          initial={{ width: 0 }}
          animate={{ width: `${pct}%` }}
          transition={{ duration: 0.6, ease: 'easeOut' }}
          style={{ height: '100%', backgroundColor: color, borderRadius: 2 }}
        />
      </div>
    </div>
  );
}

/* ─── StafferRow ─────────────────────────────────────────────────────────── */

function StafferRow({ staffer }: { staffer: Staffer }) {
  return (
    <tr
      style={{
        borderBottom: '1px solid var(--color-border)',
        backgroundColor: staffer.is_key_contact ? 'rgba(37,99,235,0.04)' : 'transparent',
      }}
    >
      <td style={{ padding: '8px 10px', verticalAlign: 'top' }}>
        <div style={{ display: 'flex', alignItems: 'flex-start', gap: 6 }}>
          <div>
            <div style={{ fontSize: '0.8125rem', fontWeight: 600, color: 'var(--color-text)', lineHeight: 1.3 }}>
              {staffer.full_name}
            </div>
            <div style={{ fontSize: '0.7rem', color: 'var(--color-text-muted)', marginTop: 2 }}>
              {staffer.title}
            </div>
          </div>
          {staffer.is_key_contact && (
            <Star size={10} style={{ color: '#eab308', flexShrink: 0, marginTop: 3 }} aria-label="Key contact" />
          )}
        </div>
      </td>
      <td style={{ padding: '8px 10px', verticalAlign: 'top' }}>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
          {staffer.handles_education && (
            <span
              className="badge"
              style={{ backgroundColor: 'rgba(34,197,94,0.1)', color: '#4ade80', border: '1px solid rgba(34,197,94,0.25)', fontSize: '0.6rem' }}
            >
              Education
            </span>
          )}
          {staffer.handles_finance && (
            <span
              className="badge"
              style={{ backgroundColor: 'rgba(234,179,8,0.1)', color: '#fbbf24', border: '1px solid rgba(234,179,8,0.25)', fontSize: '0.6rem' }}
            >
              Finance
            </span>
          )}
          {staffer.office_location && (
            <span style={{ fontSize: '0.65rem', color: 'var(--color-text-muted)' }}>
              {staffer.is_dc_office ? 'DC' : staffer.office_location}
            </span>
          )}
        </div>
      </td>
      <td style={{ padding: '8px 10px', verticalAlign: 'top' }}>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
          {staffer.email && (
            <a
              href={`mailto:${staffer.email}`}
              style={{ fontSize: '0.7rem', color: 'var(--color-primary)', textDecoration: 'none' }}
              title={staffer.email}
            >
              {staffer.email.length > 28 ? staffer.email.slice(0, 25) + '…' : staffer.email}
            </a>
          )}
          {staffer.phone && (
            <a
              href={`tel:${staffer.phone}`}
              style={{ fontSize: '0.7rem', color: 'var(--color-text-secondary)', textDecoration: 'none' }}
            >
              {staffer.phone}
            </a>
          )}
        </div>
      </td>
      <td style={{ padding: '8px 10px', verticalAlign: 'middle' }}>
        <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
          {staffer.linkedin_url && (
            <a
              href={staffer.linkedin_url}
              target="_blank"
              rel="noopener noreferrer"
              aria-label={`${staffer.full_name} on LinkedIn`}
              style={{ color: '#60a5fa', display: 'flex' }}
            >
              <Linkedin size={13} />
            </a>
          )}
          {staffer.twitter_handle && (
            <a
              href={`https://twitter.com/${staffer.twitter_handle.replace(/^@/, '')}`}
              target="_blank"
              rel="noopener noreferrer"
              aria-label={`${staffer.full_name} on Twitter`}
              style={{ color: '#60a5fa', display: 'flex' }}
            >
              <Twitter size={13} />
            </a>
          )}
        </div>
      </td>
    </tr>
  );
}

/* ─── CollegeRow ─────────────────────────────────────────────────────────── */

function CollegeRow({ college }: { college: CommunityCollege }) {
  const isHighDebt = (college.median_debt ?? 0) >= HIGH_DEBT_THRESHOLD;

  return (
    <tr
      style={{
        borderBottom: '1px solid var(--color-border)',
        backgroundColor: isHighDebt ? 'rgba(245,158,11,0.05)' : 'transparent',
      }}
    >
      <td style={{ padding: '8px 10px', verticalAlign: 'middle' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
          {isHighDebt && (
            <AlertTriangle size={11} style={{ color: '#f59e0b', flexShrink: 0 }} aria-label="High debt school" />
          )}
          <div>
            <div style={{ fontSize: '0.8rem', fontWeight: 600, color: isHighDebt ? '#fbbf24' : 'var(--color-text)', lineHeight: 1.3 }}>
              {college.school_name}
            </div>
            <div style={{ fontSize: '0.65rem', color: 'var(--color-text-muted)' }}>
              {college.city}, {college.state}
            </div>
          </div>
        </div>
      </td>
      <td style={{ padding: '8px 10px', textAlign: 'right', fontSize: '0.8rem', color: 'var(--color-text-secondary)' }}>
        {fmtNum(college.enrollment)}
      </td>
      <td style={{ padding: '8px 10px', textAlign: 'right', fontSize: '0.8rem', color: isHighDebt ? '#f59e0b' : 'var(--color-text-secondary)', fontWeight: isHighDebt ? 600 : 400 }}>
        {fmt$(college.median_debt)}
      </td>
      <td style={{ padding: '8px 10px', textAlign: 'right', fontSize: '0.8rem', color: 'var(--color-text-secondary)' }}>
        {fmtPct(college.pell_grant_rate)}
      </td>
      <td style={{ padding: '8px 10px', textAlign: 'right', fontSize: '0.8rem', color: 'var(--color-text-secondary)' }}>
        {fmtPct(college.completion_rate)}
      </td>
      <td style={{ padding: '8px 10px', textAlign: 'right', fontSize: '0.8rem', color: 'var(--color-text-secondary)' }}>
        {fmt$(college.tuition_in_state)}
      </td>
      <td style={{ padding: '8px 10px', textAlign: 'right', fontSize: '0.8rem', color: 'var(--color-text-secondary)' }}>
        {fmt$(college.median_earnings_6yr)}
      </td>
    </tr>
  );
}

/* ─── CollegeTable ───────────────────────────────────────────────────────── */

function CollegeTable({ colleges }: { colleges: CommunityCollege[] }) {
  const [sort, setSort] = useState<CollegeSort>({ key: 'median_debt', dir: 'desc' });

  const sorted = useMemo(() => {
    const arr = [...colleges];
    arr.sort((a, b) => {
      const av = a[sort.key];
      const bv = b[sort.key];
      if (av == null && bv == null) return 0;
      if (av == null) return 1;
      if (bv == null) return -1;
      const cmp = av < bv ? -1 : av > bv ? 1 : 0;
      return sort.dir === 'asc' ? cmp : -cmp;
    });
    return arr;
  }, [colleges, sort]);

  function toggleSort(key: keyof CommunityCollege) {
    setSort((prev) =>
      prev.key === key
        ? { key, dir: prev.dir === 'asc' ? 'desc' : 'asc' }
        : { key, dir: 'desc' }
    );
  }

  function SortTh({
    col,
    children,
    align = 'right',
  }: {
    col: keyof CommunityCollege;
    children: React.ReactNode;
    align?: 'left' | 'right';
  }) {
    const active = sort.key === col;
    return (
      <th
        onClick={() => toggleSort(col)}
        style={{
          padding: '8px 10px',
          fontSize: '0.68rem',
          fontWeight: 600,
          color: active ? 'var(--color-primary)' : 'var(--color-text-muted)',
          textTransform: 'uppercase',
          letterSpacing: '0.05em',
          cursor: 'pointer',
          userSelect: 'none',
          textAlign: align,
          whiteSpace: 'nowrap',
          borderBottom: '1px solid var(--color-border)',
        }}
      >
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 3 }}>
          {children}
          <ArrowUpDown size={10} style={{ opacity: active ? 1 : 0.4 }} />
        </span>
      </th>
    );
  }

  return (
    <div style={{ overflowX: 'auto' }}>
      <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8rem' }}>
        <thead>
          <tr>
            <SortTh col="school_name" align="left">School</SortTh>
            <SortTh col="enrollment">Enrollment</SortTh>
            <SortTh col="median_debt">Med. Debt</SortTh>
            <SortTh col="pell_grant_rate">Pell %</SortTh>
            <SortTh col="completion_rate">Completion</SortTh>
            <SortTh col="tuition_in_state">Tuition</SortTh>
            <SortTh col="median_earnings_6yr">Earnings 6yr</SortTh>
          </tr>
        </thead>
        <tbody>
          {sorted.map((c, i) => (
            <CollegeRow key={`${c.school_name}-${c.zip}-${i}`} college={c} />
          ))}
        </tbody>
      </table>
    </div>
  );
}

/* ─── MemberCard ─────────────────────────────────────────────────────────── */

function MemberCard({ member, index }: { member: CongressMember; index: number }) {
  const [staffersOpen, setStaffersOpen] = useState(false);
  const [collegesOpen, setCollegesOpen] = useState(false);
  const colors = partyColors(member.party);
  const social = member.social_media ?? {};

  const districtLabel = member.title?.toLowerCase().includes('sen')
    ? `Senator — ${member.state}`
    : `${member.state}${member.congressional_district ? `-${member.congressional_district}` : member.district ? ` Dist. ${member.district}` : ''}`;

  return (
    <motion.article
      initial={{ opacity: 0, y: 10 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.18, delay: Math.min(index * 0.025, 0.4) }}
      className="card"
      style={{
        borderColor: colors.border,
        borderLeftWidth: 3,
        borderLeftColor: colors.leftBorder,
        backgroundColor: colors.bg,
        padding: 0,
        overflow: 'hidden',
      }}
      aria-label={`${member.name}, ${partyLabel(member.party)}, ${districtLabel}`}
    >
      {/* ── Card Header ───────────────────────────────────────────────── */}
      <div style={{ padding: '14px 16px', display: 'flex', gap: 12, alignItems: 'flex-start' }}>
        {/* Photo */}
        <div style={{ flexShrink: 0 }}>
          {member.photo_url ? (
            <img
              src={member.photo_url}
              alt={member.name}
              width={52}
              height={52}
              style={{
                borderRadius: 'var(--radius-md)',
                objectFit: 'cover',
                border: `2px solid ${colors.leftBorder}40`,
              }}
              loading="lazy"
              onError={(e) => {
                (e.currentTarget as HTMLImageElement).style.display = 'none';
              }}
            />
          ) : (
            <div
              style={{
                width: 52,
                height: 52,
                borderRadius: 'var(--radius-md)',
                backgroundColor: 'var(--color-surface-el)',
                border: `2px solid ${colors.leftBorder}30`,
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center',
              }}
              aria-hidden="true"
            >
              <Users size={22} style={{ color: 'var(--color-text-muted)' }} />
            </div>
          )}
        </div>

        {/* Name / meta */}
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' }}>
            <div>
              <h3 style={{ fontSize: '0.9375rem', fontWeight: 700, color: 'var(--color-text)', lineHeight: 1.3, margin: 0 }}>
                {member.name}
              </h3>
              <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 4, flexWrap: 'wrap' }}>
                <span
                  className="badge"
                  style={{
                    backgroundColor: colors.bg,
                    color: colors.text,
                    border: `1px solid ${colors.border}`,
                    fontSize: '0.65rem',
                  }}
                >
                  {partyLabel(member.party)}
                </span>
                <span style={{ display: 'flex', alignItems: 'center', gap: 3, fontSize: '0.75rem', color: 'var(--color-text-secondary)' }}>
                  <MapPin size={11} aria-hidden="true" />
                  {districtLabel}
                </span>
              </div>
            </div>

            {/* Opportunity score */}
            <div style={{ minWidth: 110 }}>
              <OpportunityGauge score={member.opportunity_score} />
            </div>
          </div>

          {/* District stats chips */}
          {(member.community_college_count || member.total_cc_enrollment || member.avg_cc_median_debt) && (
            <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginTop: 8 }}>
              {member.community_college_count != null && member.community_college_count > 0 && (
                <span style={{ fontSize: '0.65rem', color: 'var(--color-text-muted)', display: 'flex', alignItems: 'center', gap: 3 }}>
                  <School size={10} aria-hidden="true" />
                  {member.community_college_count} CCs
                </span>
              )}
              {member.total_cc_enrollment != null && member.total_cc_enrollment > 0 && (
                <span style={{ fontSize: '0.65rem', color: 'var(--color-text-muted)', display: 'flex', alignItems: 'center', gap: 3 }}>
                  <Users size={10} aria-hidden="true" />
                  {fmtNum(member.total_cc_enrollment)} enrolled
                </span>
              )}
              {member.avg_cc_median_debt != null && (
                <span style={{ fontSize: '0.65rem', color: (member.avg_cc_median_debt >= 15000 ? '#f87171' : 'var(--color-text-muted)'), display: 'flex', alignItems: 'center', gap: 3 }}>
                  Avg Debt: {fmt$(member.avg_cc_median_debt)}
                </span>
              )}
              {member.avg_cc_pell_rate != null && (
                <span style={{ fontSize: '0.65rem', color: 'var(--color-text-muted)' }}>
                  Pell: {fmtPct(member.avg_cc_pell_rate)}
                </span>
              )}
            </div>
          )}

          {/* Contact row */}
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 8, flexWrap: 'wrap' }}>
            {member.phone && (
              <a
                href={`tel:${member.phone}`}
                style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: '0.75rem', color: 'var(--color-text-secondary)', textDecoration: 'none' }}
                aria-label={`Call ${member.name}`}
              >
                <Phone size={12} aria-hidden="true" />
                {member.phone}
              </a>
            )}
            {member.website && (
              <a
                href={member.website}
                target="_blank"
                rel="noopener noreferrer"
                style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: '0.75rem', color: 'var(--color-primary)', textDecoration: 'none' }}
                aria-label={`${member.name} official website`}
              >
                <Globe size={12} aria-hidden="true" />
                Website
                <ExternalLink size={10} aria-hidden="true" />
              </a>
            )}
            {social.twitter && (
              <a
                href={`https://twitter.com/${social.twitter.replace(/^@/, '')}`}
                target="_blank"
                rel="noopener noreferrer"
                style={{ display: 'flex', alignItems: 'center', gap: 3, fontSize: '0.75rem', color: '#60a5fa', textDecoration: 'none' }}
                aria-label={`${member.name} on Twitter`}
              >
                <Twitter size={12} aria-hidden="true" />
                {social.twitter}
              </a>
            )}
            {social.facebook && (
              <a
                href={social.facebook}
                target="_blank"
                rel="noopener noreferrer"
                style={{ color: '#818cf8', display: 'flex', alignItems: 'center' }}
                aria-label={`${member.name} on Facebook`}
              >
                <Facebook size={14} aria-hidden="true" />
              </a>
            )}
            {social.youtube && (
              <a
                href={social.youtube}
                target="_blank"
                rel="noopener noreferrer"
                style={{ color: '#f87171', display: 'flex', alignItems: 'center' }}
                aria-label={`${member.name} on YouTube`}
              >
                <Youtube size={14} aria-hidden="true" />
              </a>
            )}
          </div>

          {/* Stats row */}
          <div style={{ display: 'flex', gap: 16, marginTop: 10, flexWrap: 'wrap' }}>
            {member.zip_count != null && (
              <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
                <MapPin size={11} style={{ color: 'var(--color-text-muted)' }} aria-hidden="true" />
                <span style={{ fontSize: '0.72rem', color: 'var(--color-text-muted)' }}>
                  <strong style={{ color: 'var(--color-text-secondary)' }}>{fmtNum(member.zip_count)}</strong> ZIPs
                </span>
              </div>
            )}
            {member.avg_distress_score != null && (
              <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
                <TrendingUp size={11} style={{ color: 'var(--color-text-muted)' }} aria-hidden="true" />
                <span style={{ fontSize: '0.72rem', color: 'var(--color-text-muted)' }}>
                  Distress <strong style={{ color: scoreColor(member.avg_distress_score) }}>
                    {member.avg_distress_score.toFixed(1)}
                  </strong>
                </span>
              </div>
            )}
            {member.total_complaints != null && (
              <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
                <AlertTriangle size={11} style={{ color: 'var(--color-text-muted)' }} aria-hidden="true" />
                <span style={{ fontSize: '0.72rem', color: 'var(--color-text-muted)' }}>
                  <strong style={{ color: 'var(--color-text-secondary)' }}>{fmtNum(member.total_complaints)}</strong> complaints
                </span>
              </div>
            )}
            {member.community_college_count != null && (
              <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
                <School size={11} style={{ color: 'var(--color-text-muted)' }} aria-hidden="true" />
                <span style={{ fontSize: '0.72rem', color: 'var(--color-text-muted)' }}>
                  <strong style={{ color: 'var(--color-text-secondary)' }}>{member.community_college_count}</strong> colleges
                </span>
              </div>
            )}
          </div>

          {/* Position on key issues */}
          {member.position_student_debt && (
            <div
              style={{
                marginTop: 10,
                padding: '5px 10px',
                borderRadius: 'var(--radius-sm)',
                backgroundColor: 'rgba(37,99,235,0.07)',
                border: '1px solid rgba(37,99,235,0.15)',
                fontSize: '0.72rem',
                color: 'var(--color-text-secondary)',
              }}
            >
              <strong style={{ color: '#93c5fd' }}>Position:</strong> {member.position_student_debt}
            </div>
          )}

          {/* Opportunity factor breakdown */}
          {member.opportunity_factors && typeof member.opportunity_factors === 'object' && (
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 8 }}>
              {(['debt_distress', 'college_density', 'zip_coverage', 'complaint_density', 'hotspot_concentration', 'advocacy_readiness'] as const).map((key) => {
                const factor = member.opportunity_factors?.[key] as { score?: number; raw?: number } | undefined;
                if (!factor?.score) return null;
                const labels: Record<string, string> = {
                  debt_distress: 'Distress', college_density: 'Colleges', zip_coverage: 'ZIPs',
                  complaint_density: 'Complaints', hotspot_concentration: 'Hotspots', advocacy_readiness: 'Advocacy',
                };
                const score = Math.round(factor.score);
                const clr = score >= 70 ? '#f87171' : score >= 40 ? '#fbbf24' : '#4ade80';
                return (
                  <span
                    key={key}
                    title={`${labels[key]}: ${score}/100 (raw: ${factor.raw})`}
                    style={{
                      fontSize: '0.6rem', padding: '2px 6px', borderRadius: 'var(--radius-sm)',
                      backgroundColor: `${clr}12`, border: `1px solid ${clr}33`, color: clr,
                    }}
                  >
                    {labels[key]} {score}
                  </span>
                );
              })}
              {(member.opportunity_factors as Record<string, unknown>).rank != null && (
                <span style={{ fontSize: '0.6rem', color: 'var(--color-text-muted)', alignSelf: 'center' }}>
                  Rank #{String((member.opportunity_factors as Record<string, unknown>).rank)}
                </span>
              )}
            </div>
          )}
          {/* Committees */}
          {member.committees && member.committees.length > 0 && (
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 8 }}>
              {member.committees.map((c) => (
                <span
                  key={c}
                  style={{
                    fontSize: '0.6rem', padding: '2px 6px', borderRadius: 'var(--radius-sm)',
                    backgroundColor: 'rgba(37,99,235,0.08)', border: '1px solid rgba(37,99,235,0.2)',
                    color: '#93c5fd',
                  }}
                >
                  {c}
                </span>
              ))}
            </div>
          )}
        </div>
      </div>

      {/* ── Divider ───────────────────────────────────────────────────── */}
      <hr className="divider" />

      {/* ── Collapsible: Staffers ─────────────────────────────────────── */}
      <section>
        <button
          type="button"
          onClick={() => setStaffersOpen((v) => !v)}
          style={{
            width: '100%',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'space-between',
            padding: '9px 16px',
            background: 'none',
            border: 'none',
            cursor: 'pointer',
            color: 'var(--color-text-secondary)',
            fontSize: '0.78rem',
            fontWeight: 600,
          }}
          aria-expanded={staffersOpen}
          aria-controls={`staffers-${member.bioguide_id}`}
        >
          <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
            <Users size={13} aria-hidden="true" />
            Staffers
            <span
              className="badge"
              style={{
                backgroundColor: 'var(--color-surface-el)',
                color: 'var(--color-text-muted)',
                border: '1px solid var(--color-border)',
                fontSize: '0.6rem',
              }}
            >
              {member.staffers?.length ?? 0}
            </span>
          </span>
          {staffersOpen
            ? <ChevronUp size={14} aria-hidden="true" />
            : <ChevronDown size={14} aria-hidden="true" />}
        </button>

        <AnimatePresence initial={false}>
          {staffersOpen && (
            <motion.div
              id={`staffers-${member.bioguide_id}`}
              key="staffers"
              initial={{ height: 0, opacity: 0 }}
              animate={{ height: 'auto', opacity: 1 }}
              exit={{ height: 0, opacity: 0 }}
              transition={{ duration: 0.22, ease: 'easeInOut' }}
              style={{ overflow: 'hidden' }}
            >
              <div
                style={{
                  borderTop: '1px solid var(--color-border)',
                  backgroundColor: 'var(--color-surface)',
                }}
              >
                {member.staffers && member.staffers.length > 0 ? (
                  <div style={{ overflowX: 'auto' }}>
                    <table style={{ width: '100%', borderCollapse: 'collapse' }}>
                      <thead>
                        <tr style={{ borderBottom: '1px solid var(--color-border)' }}>
                          {['Name & Title', 'Role / Flags', 'Contact', 'Links'].map((h) => (
                            <th
                              key={h}
                              style={{
                                padding: '7px 10px',
                                fontSize: '0.65rem',
                                fontWeight: 600,
                                color: 'var(--color-text-muted)',
                                textTransform: 'uppercase',
                                letterSpacing: '0.05em',
                                textAlign: 'left',
                                whiteSpace: 'nowrap',
                              }}
                            >
                              {h}
                            </th>
                          ))}
                        </tr>
                      </thead>
                      <tbody>
                        {member.staffers.map((s, i) => (
                          <StafferRow key={`${s.full_name}-${i}`} staffer={s} />
                        ))}
                      </tbody>
                    </table>
                  </div>
                ) : (
                  <p style={{ padding: '12px 16px', fontSize: '0.78rem', color: 'var(--color-text-muted)', margin: 0 }}>
                    No staffer data available.
                  </p>
                )}
              </div>
            </motion.div>
          )}
        </AnimatePresence>
      </section>

      {/* ── Collapsible: Community Colleges ──────────────────────────── */}
      <section>
        <button
          type="button"
          onClick={() => setCollegesOpen((v) => !v)}
          style={{
            width: '100%',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'space-between',
            padding: '9px 16px',
            background: 'none',
            border: 'none',
            borderTop: '1px solid var(--color-border)',
            cursor: 'pointer',
            color: 'var(--color-text-secondary)',
            fontSize: '0.78rem',
            fontWeight: 600,
          }}
          aria-expanded={collegesOpen}
          aria-controls={`colleges-${member.bioguide_id}`}
        >
          <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
            <School size={13} aria-hidden="true" />
            Community Colleges
            <span
              className="badge"
              style={{
                backgroundColor: 'var(--color-surface-el)',
                color: 'var(--color-text-muted)',
                border: '1px solid var(--color-border)',
                fontSize: '0.6rem',
              }}
            >
              {member.community_colleges?.length ?? 0}
            </span>
            {member.highest_debt_college && (
              <span style={{ fontSize: '0.65rem', color: '#f59e0b', fontWeight: 400 }}>
                — highest: {member.highest_debt_college}
              </span>
            )}
          </span>
          {collegesOpen
            ? <ChevronUp size={14} aria-hidden="true" />
            : <ChevronDown size={14} aria-hidden="true" />}
        </button>

        <AnimatePresence initial={false}>
          {collegesOpen && (
            <motion.div
              id={`colleges-${member.bioguide_id}`}
              key="colleges"
              initial={{ height: 0, opacity: 0 }}
              animate={{ height: 'auto', opacity: 1 }}
              exit={{ height: 0, opacity: 0 }}
              transition={{ duration: 0.22, ease: 'easeInOut' }}
              style={{ overflow: 'hidden' }}
            >
              <div
                style={{
                  borderTop: '1px solid var(--color-border)',
                  backgroundColor: 'var(--color-surface)',
                }}
              >
                {member.community_colleges && member.community_colleges.length > 0 ? (
                  <>
                    <div style={{ padding: '6px 16px 0', display: 'flex', alignItems: 'center', gap: 6 }}>
                      <AlertTriangle size={10} style={{ color: '#f59e0b' }} aria-hidden="true" />
                      <span style={{ fontSize: '0.65rem', color: '#f59e0b' }}>
                        Amber = median debt above {fmt$(HIGH_DEBT_THRESHOLD)}
                      </span>
                    </div>
                    <div style={{ padding: '4px 0 8px' }}>
                      <CollegeTable colleges={member.community_colleges} />
                    </div>
                  </>
                ) : (
                  <p style={{ padding: '12px 16px', fontSize: '0.78rem', color: 'var(--color-text-muted)', margin: 0 }}>
                    No community college data available.
                  </p>
                )}
              </div>
            </motion.div>
          )}
        </AnimatePresence>
      </section>
    </motion.article>
  );
}

/* ─── CongressTab ────────────────────────────────────────────────────────── */

export default function CongressTab({ onNavigate }: { onNavigate?: (tab: string) => void }) {
  const [members, setMembers] = useState<CongressMember[]>([]);
  const [total, setTotal] = useState(0);
  const [stats, setStats] = useState<ApiStats | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  // Filters
  const [stateFilter, setStateFilter] = useState('');
  const [partyFilter, setPartyFilter] = useState('');
  const [searchQuery, setSearchQuery] = useState('');
  const [debouncedSearch, setDebouncedSearch] = useState('');
  const [sortBy, setSortBy] = useState<'state' | 'opportunity' | 'name' | 'debt'>('state');
  const [oppLevel, setOppLevel] = useState('');

  // Debounce search input
  useEffect(() => {
    const t = setTimeout(() => setDebouncedSearch(searchQuery), 320);
    return () => clearTimeout(t);
  }, [searchQuery]);

  const fetchMembers = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const params = new URLSearchParams();
      if (stateFilter) params.set('state', stateFilter);
      if (partyFilter) params.set('party', partyFilter);
      if (debouncedSearch.trim()) params.set('search', debouncedSearch.trim());
      if (sortBy !== 'state') params.set('sort', sortBy);
      if (oppLevel) params.set('opp_level', oppLevel);

      const res = await fetch(`/api/congress?${params.toString()}`);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const data: ApiResponse = await res.json();
      setMembers(data.members ?? []);
      setTotal(data.total ?? 0);
      setStats(data.stats ?? null);
    } catch (err) {
      setError((err as Error).message || 'Failed to load congress members.');
    } finally {
      setLoading(false);
    }
  }, [stateFilter, partyFilter, debouncedSearch, sortBy, oppLevel]);

  useEffect(() => { fetchMembers(); }, [fetchMembers]);

  const partyOptions = [
    { value: '', label: 'All Parties' },
    { value: 'D', label: 'Democrat' },
    { value: 'R', label: 'Republican' },
    { value: 'I', label: 'Independent' },
  ];

  const selectStyle: React.CSSProperties = {
    background: 'var(--color-surface-el)',
    border: '1px solid var(--color-border)',
    borderRadius: 'var(--radius-md)',
    color: 'var(--color-text)',
    padding: '0.5rem 0.75rem',
    fontSize: '0.875rem',
    outline: 'none',
    cursor: 'pointer',
    minWidth: 0,
  };

  return (
    <div style={{ padding: '1rem 1.25rem', maxWidth: 1200, margin: '0 auto' }}>
      {/* ── Header ──────────────────────────────────────────────────────── */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16, flexWrap: 'wrap' }}>
        <div
          style={{
            width: 34,
            height: 34,
            borderRadius: 'var(--radius-md)',
            background: 'linear-gradient(135deg, rgba(37,99,235,0.25), rgba(168,85,247,0.15))',
            border: '1px solid rgba(37,99,235,0.35)',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            flexShrink: 0,
          }}
          aria-hidden="true"
        >
          <Users size={18} style={{ color: '#60a5fa' }} />
        </div>
        <div style={{ flex: 1 }}>
          <h1 style={{ fontSize: '1.125rem', fontWeight: 700, color: 'var(--color-text)', margin: 0 }}>
            Congress Members
          </h1>
          <p style={{ fontSize: '0.78rem', color: 'var(--color-text-muted)', margin: 0 }}>
            {loading
              ? 'Loading...'
              : `${total.toLocaleString()} member${total !== 1 ? 's' : ''}`}
          </p>
        </div>
        <button
          type="button"
          onClick={fetchMembers}
          disabled={loading}
          className="btn btn-ghost btn-sm"
          aria-label="Refresh congress members"
        >
          <RefreshCw
            size={14}
            aria-hidden="true"
            style={{ animation: loading ? 'spin 0.7s linear infinite' : 'none' }}
          />
        </button>
      </div>

      {/* ── Cross-reference navigation ────────────────────────────────── */}
      {onNavigate && (
        <div className="flex items-center gap-1.5 flex-wrap mb-4">
          <span className="text-xs" style={{ color: 'var(--color-text-muted)', marginRight: 2 }}>Related:</span>
          <button
            onClick={() => onNavigate('opportunity-map')}
            className="btn btn-ghost btn-sm"
            style={{ fontSize: 12, gap: 4 }}
          >
            <Map size={14} />
            Opportunity Map
          </button>
          <button
            onClick={() => onNavigate('geo-report')}
            className="btn btn-ghost btn-sm"
            style={{ fontSize: 12, gap: 4 }}
          >
            <BarChart3 size={14} />
            Geo Intelligence
          </button>
        </div>
      )}

      {/* ── Filter Bar ──────────────────────────────────────────────────── */}
      <div
        className="card-elevated"
        style={{
          marginBottom: 20,
          display: 'flex',
          gap: 10,
          alignItems: 'center',
          flexWrap: 'wrap',
          padding: '10px 14px',
        }}
      >
        {/* State dropdown */}
        <select
          value={stateFilter}
          onChange={(e) => setStateFilter(e.target.value)}
          style={selectStyle}
          aria-label="Filter by state"
        >
          {US_STATES.map((s) => (
            <option key={s.value} value={s.value}>{s.label}</option>
          ))}
        </select>

        {/* Party filter */}
        <div style={{ display: 'flex', gap: 4 }}>
          {partyOptions.map((p) => {
            const active = partyFilter === p.value;
            const pc = p.value ? partyColors(p.value) : null;
            return (
              <button
                key={p.value}
                type="button"
                onClick={() => setPartyFilter(p.value)}
                className="btn btn-sm"
                style={{
                  backgroundColor: active
                    ? pc ? pc.bg : 'var(--color-surface-el)'
                    : 'transparent',
                  color: active
                    ? pc ? pc.text : 'var(--color-text)'
                    : 'var(--color-text-muted)',
                  border: active
                    ? `1px solid ${pc ? pc.border : 'var(--color-border)'}`
                    : '1px solid transparent',
                  fontWeight: active ? 600 : 400,
                }}
                aria-pressed={active}
              >
                {p.label}
              </button>
            );
          })}
        </div>

        {/* Search */}
        <div style={{ position: 'relative', flex: 1, minWidth: 160 }}>
          <Search
            size={13}
            style={{
              position: 'absolute',
              left: 10,
              top: '50%',
              transform: 'translateY(-50%)',
              color: 'var(--color-text-muted)',
              pointerEvents: 'none',
            }}
            aria-hidden="true"
          />
          <input
            type="search"
            className="input"
            style={{ paddingLeft: '2rem', width: '100%' }}
            placeholder="Search by name..."
            value={searchQuery}
            onChange={(e) => setSearchQuery(e.target.value)}
            aria-label="Search congress members by name"
          />
        </div>

        {/* Sort dropdown */}
        <select
          value={sortBy}
          onChange={(e) => setSortBy(e.target.value as typeof sortBy)}
          style={selectStyle}
          aria-label="Sort by"
        >
          <option value="state">Sort: State</option>
          <option value="opportunity">Sort: Opportunity</option>
          <option value="debt">Sort: Avg Debt</option>
          <option value="name">Sort: Name</option>
        </select>

        {/* Opportunity level filter */}
        <div style={{ display: 'flex', gap: 3 }}>
          {[
            { v: '', label: 'All' },
            { v: 'high', label: 'High', color: '#ef4444' },
            { v: 'med', label: 'Med', color: '#f59e0b' },
            { v: 'low', label: 'Low', color: '#22c55e' },
          ].map((o) => {
            const active = oppLevel === o.v;
            return (
              <button
                key={o.v}
                type="button"
                onClick={() => setOppLevel(o.v)}
                className="btn btn-sm"
                style={{
                  backgroundColor: active ? (o.color ? `${o.color}18` : 'var(--color-surface-el)') : 'transparent',
                  color: active ? (o.color || 'var(--color-text)') : 'var(--color-text-muted)',
                  border: active ? `1px solid ${o.color || 'var(--color-border)'}40` : '1px solid transparent',
                  fontSize: '0.7rem', padding: '3px 8px',
                }}
                aria-pressed={active}
              >
                {o.label}
              </button>
            );
          })}
        </div>

        {/* Total badge */}
        {!loading && (
          <span
            className="badge"
            style={{
              backgroundColor: 'rgba(37,99,235,0.1)',
              color: '#93c5fd',
              border: '1px solid rgba(37,99,235,0.2)',
              whiteSpace: 'nowrap',
              flexShrink: 0,
            }}
          >
            {total.toLocaleString()} results
          </span>
        )}
      </div>

      {/* ── Stats Dashboard ──────────────────────────────────────────────── */}
      {!loading && stats && (
        <div
          style={{
            display: 'grid',
            gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))',
            gap: 8,
            marginBottom: 16,
          }}
        >
          {[
            { label: 'Members', value: stats.total_members.toLocaleString(), icon: '👥' },
            { label: 'Avg Opp Score', value: stats.avg_opportunity_score != null ? stats.avg_opportunity_score.toFixed(1) : '—', icon: '📊', color: scoreColor(stats.avg_opportunity_score) },
            { label: 'CC Enrollment', value: stats.total_cc_enrollment > 0 ? (stats.total_cc_enrollment / 1000).toFixed(0) + 'K' : '—', icon: '🎓' },
            { label: 'High Priority', value: String(stats.high_priority_count), icon: '🔴', color: '#ef4444' },
          ].map((s) => (
            <div
              key={s.label}
              className="card"
              style={{ padding: '10px 12px', textAlign: 'center' }}
            >
              <div style={{ fontSize: '1.125rem', fontWeight: 700, color: s.color || 'var(--color-text)' }}>
                {s.value}
              </div>
              <div style={{ fontSize: '0.65rem', color: 'var(--color-text-muted)', marginTop: 2 }}>
                {s.label}
              </div>
            </div>
          ))}
        </div>
      )}

      {/* ── Loading Skeletons ────────────────────────────────────────────── */}
      {loading && (
        <div
          style={{ display: 'grid', gap: 12, gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))' }}
          aria-live="polite"
          aria-busy="true"
        >
          {Array.from({ length: 6 }).map((_, i) => (
            <div
              key={i}
              className="skeleton"
              style={{ height: 160, borderRadius: 'var(--radius-lg)' }}
              aria-hidden="true"
            />
          ))}
        </div>
      )}

      {/* ── Error ───────────────────────────────────────────────────────── */}
      {!loading && error && (
        <div
          role="alert"
          style={{
            padding: '12px 16px',
            borderRadius: 'var(--radius-lg)',
            backgroundColor: 'rgba(239,68,68,0.05)',
            border: '1px solid rgba(239,68,68,0.3)',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'space-between',
            gap: 12,
          }}
        >
          <p style={{ fontSize: '0.875rem', color: 'var(--color-error)', margin: 0 }}>{error}</p>
          <button type="button" onClick={fetchMembers} className="btn btn-secondary btn-sm">
            <RefreshCw size={12} aria-hidden="true" /> Retry
          </button>
        </div>
      )}

      {/* ── Empty ───────────────────────────────────────────────────────── */}
      {!loading && !error && members.length === 0 && (
        <div
          style={{
            borderRadius: 'var(--radius-lg)',
            border: '1px dashed var(--color-border)',
            backgroundColor: 'var(--color-surface)',
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            justifyContent: 'center',
            padding: '4rem 2rem',
            gap: 12,
            textAlign: 'center',
          }}
          role="status"
        >
          <Inbox size={38} style={{ color: 'var(--color-text-muted)' }} aria-hidden="true" />
          <p style={{ fontSize: '1.0625rem', fontWeight: 600, color: 'var(--color-text)', margin: 0 }}>
            {searchQuery || stateFilter || partyFilter
              ? 'No members match your filters'
              : 'No congress members found'}
          </p>
          <p style={{ fontSize: '0.8125rem', color: 'var(--color-text-muted)', margin: 0, maxWidth: 300 }}>
            {searchQuery || stateFilter || partyFilter
              ? 'Try adjusting your search or filter criteria.'
              : 'Member data will appear here once the API is connected.'}
          </p>
        </div>
      )}

      {/* ── Card Grid ───────────────────────────────────────────────────── */}
      {!loading && !error && members.length > 0 && (
        <div
          style={{
            display: 'grid',
            gap: 14,
            gridTemplateColumns: 'repeat(auto-fill, minmax(360px, 1fr))',
          }}
          role="list"
          aria-label="Congress members"
        >
          <AnimatePresence initial={false}>
            {members.map((m, i) => (
              <div key={m.bioguide_id} role="listitem">
                <MemberCard member={m} index={i} />
              </div>
            ))}
          </AnimatePresence>
        </div>
      )}
    </div>
  );
}