← back to Patty

components/orbit/OrbitTab.tsx

2383 lines

'use client';

import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import {
  Globe, RefreshCw, Loader2, ExternalLink, Copy, Check,
  TrendingUp, Link2, Rss, BarChart3, Sparkles, X,
  Send, Mail, ChevronDown, ChevronUp, FileText, Zap,
  Eye, Target, Clock, AlertCircle, Newspaper,
  ZoomIn, ZoomOut, Maximize2, Activity, Camera, Film,
  Gauge, Triangle,
} from 'lucide-react';

/* ═══════════════════════════════════════════════════════════════════════════
   Types
   ═══════════════════════════════════════════════════════════════════════════ */

interface KalshiMarket {
  id: string;
  title: string;
  ticker: string;
  category: string;
  yes_price: number;
  no_price: number;
  volume: number;
  status: string;
  close_time: string | null;
  // DB fields (optional, used for mapping)
  yes_bid?: number;
  yes_ask?: number;
  no_bid?: number;
  no_ask?: number;
  last_price?: number;
}

interface Petition {
  id: string;
  title: string;
  summary: string;
  status: 'draft' | 'active' | 'closed';
  signature_count: number;
  signature_goal: number;
  category: string;
  created_at: string;
}

interface OrbitLink {
  id: string;
  market_id: string;
  petition_id: string;
  link_type: 'influence' | 'counter' | 'support' | 'alliance';
  strength: number;
  ai_reasoning: string;
}

interface RSSArticle {
  id: string;
  source: string;
  feed_name?: string;
  title: string;
  url: string;
  published_at: string;
  sentiment: 'positive' | 'negative' | 'neutral';
  matched_tickers?: string[] | null;
  matched_markets?: string[] | null;
  feed_category: 'national' | 'local' | 'wire';
}

interface OrbitStats {
  total_markets: number;
  linked_petitions: number;
  active_feeds: number;
  articles_today: number;
  avg_sentiment: number;
}

interface TrendingTopic {
  id: string;
  title: string;
  content?: string;
  category: string;
  tags?: string[];
  engagement_score: number;
  source_url?: string;
  created_at: string;
}

interface AgentInfo {
  name: string;
  role: string;
  port: number;
  status: 'online' | 'offline' | 'error';
  uptime?: number;
}

interface GeneratedPetition {
  title: string;
  summary: string;
  body_html: string;
  body_text: string;
  suggested_target: string;
  suggested_tags: string[];
  suggested_goal: number;
  // Alliance fields (optional — present when generated via alliance endpoint)
  alliance_insight?: string;
  group_a_appeal?: string;
  group_b_appeal?: string;
  impact_multiplier?: string;
}

/* ═══════════════════════════════════════════════════════════════════════════
   Helpers
   ═══════════════════════════════════════════════════════════════════════════ */

function decodeEntities(str: string): string {
  return str
    .replace(/&/g, '&')
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/&quot;/g, '"')
    .replace(/&#0?39;/g, "'")
    .replace(/&apos;/g, "'");
}

/* ═══════════════════════════════════════════════════════════════════════════
   Constants
   ═══════════════════════════════════════════════════════════════════════════ */

const CATEGORY_COLORS: Record<string, string> = {
  Politics: '#3b82f6',
  Economics: '#22c55e',
  Climate: '#f97316',
  Science: '#8b5cf6',
  Sports: '#ef4444',
  World: '#06b6d4',
  Entertainment: '#ec4899',
  default: '#71717a',
};

const TRENDING_COLOR = '#10b981'; // emerald-500

const LINK_COLORS: Record<string, string> = {
  influence: '#7c3aed',
  counter: '#ef4444',
  support: '#22c55e',
  alliance: '#f59e0b',
};

const SOURCE_COLORS: Record<string, string> = {
  'CNN': '#cc0000',
  'Fox News': '#003366',
  'NBC': '#6a5acd',
  'Reuters': '#ff8c00',
  'AP': '#ff0000',
  'NPR': '#2b9cd8',
  'BBC': '#bb1919',
  'Politico': '#ff3333',
  'The Hill': '#1a5276',
  'Washington Post': '#231f20',
  default: '#71717a',
};

const PLATFORMS: Record<string, { name: string; url: string; color: string }> = {
  moveon: { name: 'MoveOn', url: 'https://sign.moveon.org/petitions/new', color: '#e53e3e' },
  change_org: { name: 'Change.org', url: 'https://www.change.org/start-a-petition', color: '#e53e3e' },
  we_the_people: { name: 'We The People', url: 'https://petitions.whitehouse.gov', color: '#1d4ed8' },
};

const STATUS_STYLES: Record<string, { bg: string; text: string; border: string }> = {
  draft: { bg: 'rgba(161,161,170,0.15)', text: '#a1a1aa', border: 'rgba(161,161,170,0.3)' },
  active: { bg: 'rgba(34,197,94,0.15)', text: '#22c55e', border: 'rgba(34,197,94,0.3)' },
  closed: { bg: 'rgba(239,68,68,0.15)', text: '#ef4444', border: 'rgba(239,68,68,0.3)' },
};

/* ─── Mock data for graceful display when API not ready ──────────────── */
const MOCK_MARKETS: KalshiMarket[] = [
  { id: 'm1', title: 'Will Congress pass student debt relief by Q3?', ticker: 'STUDEBT-Q3', category: 'Politics', yes_price: 0.34, no_price: 0.66, volume: 245000, status: 'open', close_time: '2026-09-30T23:59:00Z' },
  { id: 'm2', title: 'Fed rate cut before July 2026?', ticker: 'FEDCUT-JUL26', category: 'Economics', yes_price: 0.58, no_price: 0.42, volume: 892000, status: 'open', close_time: '2026-07-01T23:59:00Z' },
  { id: 'm3', title: 'US rejoins Paris Climate Agreement by 2027?', ticker: 'PARIS-27', category: 'Climate', yes_price: 0.22, no_price: 0.78, volume: 167000, status: 'open', close_time: '2027-01-01T23:59:00Z' },
  { id: 'm4', title: 'Supreme Court rules on social media regulation?', ticker: 'SCOTUS-SM', category: 'Politics', yes_price: 0.71, no_price: 0.29, volume: 534000, status: 'open', close_time: '2026-06-30T23:59:00Z' },
  { id: 'm5', title: 'Unemployment stays below 5% through 2026?', ticker: 'UNEMP-5PCT', category: 'Economics', yes_price: 0.62, no_price: 0.38, volume: 312000, status: 'open', close_time: '2026-12-31T23:59:00Z' },
  { id: 'm6', title: 'AI regulation bill passes Senate?', ticker: 'AIREG-SEN', category: 'Science', yes_price: 0.45, no_price: 0.55, volume: 423000, status: 'open', close_time: '2026-12-31T23:59:00Z' },
];

const MOCK_PETITIONS: Petition[] = [
  { id: 'p1', title: 'Cancel Student Loan Interest', summary: 'Demand Congress eliminate interest on federal student loans', status: 'active', signature_count: 12450, signature_goal: 25000, category: 'debt_relief', created_at: '2026-02-15T10:00:00Z' },
  { id: 'p2', title: 'Protect Social Media Free Speech', summary: 'Oppose government overreach in social media regulation', status: 'active', signature_count: 8320, signature_goal: 15000, category: 'policy', created_at: '2026-02-10T08:00:00Z' },
  { id: 'p3', title: 'Support AI Worker Protections', summary: 'Require AI impact assessments for workforce changes', status: 'draft', signature_count: 0, signature_goal: 10000, category: 'policy', created_at: '2026-02-20T14:00:00Z' },
  { id: 'p4', title: 'Restore Climate Commitments', summary: 'Push for renewed Paris Agreement participation', status: 'active', signature_count: 34210, signature_goal: 50000, category: 'policy', created_at: '2026-01-28T09:00:00Z' },
  { id: 'p5', title: 'Lower Prescription Drug Costs', summary: 'Support Medicare negotiation for all drug prices', status: 'active', signature_count: 19800, signature_goal: 30000, category: 'consumer_protection', created_at: '2026-02-01T11:00:00Z' },
];

const MOCK_LINKS: OrbitLink[] = [
  { id: 'l1', market_id: 'm1', petition_id: 'p1', link_type: 'influence', strength: 0.85, ai_reasoning: 'Market probability directly tracks legislative momentum on student debt' },
  { id: 'l2', market_id: 'm4', petition_id: 'p2', link_type: 'counter', strength: 0.72, ai_reasoning: 'SCOTUS ruling could override petition goals on social media regulation' },
  { id: 'l3', market_id: 'm6', petition_id: 'p3', link_type: 'support', strength: 0.68, ai_reasoning: 'AI regulation bill aligns with worker protection petition demands' },
  { id: 'l4', market_id: 'm3', petition_id: 'p4', link_type: 'influence', strength: 0.91, ai_reasoning: 'Paris Agreement market directly predicts petition outcome likelihood' },
  { id: 'l5', market_id: 'm2', petition_id: 'p5', link_type: 'support', strength: 0.45, ai_reasoning: 'Fed policy indirectly affects healthcare cost dynamics' },
];

const MOCK_ARTICLES: RSSArticle[] = [
  { id: 'a1', source: 'CNN', title: 'Student debt relief bill gains bipartisan support in Senate committee', url: '#', published_at: new Date(Date.now() - 2 * 3600000).toISOString(), sentiment: 'positive', matched_tickers: ['STUDEBT-Q3'], feed_category: 'national' },
  { id: 'a2', source: 'Reuters', title: 'Federal Reserve signals potential rate adjustment in upcoming meeting', url: '#', published_at: new Date(Date.now() - 4 * 3600000).toISOString(), sentiment: 'neutral', matched_tickers: ['FEDCUT-JUL26'], feed_category: 'wire' },
  { id: 'a3', source: 'Politico', title: 'Supreme Court to hear oral arguments on social media liability case', url: '#', published_at: new Date(Date.now() - 5 * 3600000).toISOString(), sentiment: 'negative', matched_tickers: ['SCOTUS-SM'], feed_category: 'national' },
  { id: 'a4', source: 'NPR', title: 'Climate activists renew push for international cooperation on emissions', url: '#', published_at: new Date(Date.now() - 7 * 3600000).toISOString(), sentiment: 'positive', matched_tickers: ['PARIS-27'], feed_category: 'national' },
  { id: 'a5', source: 'AP', title: 'Tech companies lobby against proposed AI oversight legislation', url: '#', published_at: new Date(Date.now() - 8 * 3600000).toISOString(), sentiment: 'negative', matched_tickers: ['AIREG-SEN'], feed_category: 'wire' },
  { id: 'a6', source: 'The Hill', title: 'Prescription drug pricing reform advances through committee markup', url: '#', published_at: new Date(Date.now() - 10 * 3600000).toISOString(), sentiment: 'positive', matched_tickers: ['UNEMP-5PCT'], feed_category: 'national' },
  { id: 'a7', source: 'BBC', title: 'Global unemployment trends show mixed signals across economies', url: '#', published_at: new Date(Date.now() - 12 * 3600000).toISOString(), sentiment: 'neutral', matched_tickers: ['UNEMP-5PCT'], feed_category: 'wire' },
];

const MOCK_STATS: OrbitStats = {
  total_markets: 6,
  linked_petitions: 5,
  active_feeds: 7,
  articles_today: 23,
  avg_sentiment: 0.42,
};

/* ═══════════════════════════════════════════════════════════════════════════
   Helpers
   ═══════════════════════════════════════════════════════════════════════════ */

function relativeTime(dateStr: string): string {
  const diff = Date.now() - new Date(dateStr).getTime();
  const minutes = Math.floor(diff / 60000);
  if (minutes < 1) return 'just now';
  if (minutes < 60) return `${minutes}m ago`;
  const hours = Math.floor(minutes / 60);
  if (hours < 24) return `${hours}h ago`;
  const days = Math.floor(hours / 24);
  return `${days}d ago`;
}

function truncate(str: string, len: number): string {
  return str.length > len ? str.slice(0, len) + '...' : str;
}

function sentimentColor(s: string): string {
  if (s === 'positive') return '#22c55e';
  if (s === 'negative') return '#ef4444';
  return '#71717a';
}

function sentimentLabel(score: number): { label: string; color: string } {
  if (score > 0.6) return { label: 'Positive', color: '#22c55e' };
  if (score > 0.4) return { label: 'Mixed', color: '#f59e0b' };
  return { label: 'Negative', color: '#ef4444' };
}

/* ═══════════════════════════════════════════════════════════════════════════
   SVG Mind Map Sub-Component
   ═══════════════════════════════════════════════════════════════════════════ */

interface MindMapProps {
  markets: KalshiMarket[];
  petitions: Petition[];
  links: OrbitLink[];
  trendingTopics: TrendingTopic[];
  hoveredMarket: string | null;
  hoveredPetition: string | null;
  selectedMarkets: string[];
  selectedPetition: string | null;
  onHoverMarket: (id: string | null) => void;
  onHoverPetition: (id: string | null) => void;
  onSelectMarket: (id: string | null, shift?: boolean) => void;
  onSelectPetition: (id: string | null) => void;
  onSelectTrending: (id: string | null) => void;
}

function MindMap({
  markets, petitions, links, trendingTopics,
  hoveredMarket, hoveredPetition,
  selectedMarkets, selectedPetition,
  onHoverMarket, onHoverPetition,
  onSelectMarket, onSelectPetition, onSelectTrending,
}: MindMapProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const svgRef = useRef<SVGSVGElement>(null);
  const [dims, setDims] = useState({ w: 800, h: 600 });

  // Zoom system
  const [zoom, setZoom] = useState(1);
  const [panOffset, setPanOffset] = useState({ x: 0, y: 0 });
  const handleZoomIn = useCallback(() => setZoom(z => Math.min(z + 0.2, 3)), []);
  const handleZoomOut = useCallback(() => setZoom(z => Math.max(z - 0.2, 0.4)), []);
  const handleZoomReset = useCallback(() => { setZoom(1); setPanOffset({ x: 0, y: 0 }); }, []);

  // Drag system
  const [posOverrides, setPosOverrides] = useState<Record<string, { x: number; y: number }>>({});
  const dragRef = useRef<{
    id: string; type: 'market' | 'petition' | 'trending';
    offsetX: number; offsetY: number;
    startX: number; startY: number;
    dragged: boolean;
  } | null>(null);
  const [isDragging, setIsDragging] = useState(false);

  // Hovered trending topic
  const [hoveredTrending, setHoveredTrending] = useState<string | null>(null);

  useEffect(() => {
    const el = containerRef.current;
    if (!el) return;
    const ro = new ResizeObserver(entries => {
      for (const entry of entries) {
        setDims({ w: entry.contentRect.width, h: entry.contentRect.height });
      }
    });
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  const centerX = dims.w / 2;
  const centerY = dims.h / 2;
  const radius = Math.min(dims.w, dims.h) * 0.36;

  // Compute positions with drag overrides
  const marketPositions = useMemo(() => {
    return markets.map((m, i) => {
      const angle = (i / Math.max(markets.length, 1)) * 2 * Math.PI - Math.PI / 2;
      const baseX = centerX - radius * 0.55 + radius * 0.85 * Math.cos(angle);
      const baseY = centerY + radius * 0.85 * Math.sin(angle);
      const override = posOverrides[`m-${m.id}`];
      return { ...m, x: override?.x ?? baseX, y: override?.y ?? baseY };
    });
  }, [markets, centerX, centerY, radius, posOverrides]);

  const petitionPositions = useMemo(() => {
    return petitions.map((p, i) => {
      const angle = (i / Math.max(petitions.length, 1)) * 2 * Math.PI - Math.PI / 2;
      const baseX = centerX + radius * 0.55 + radius * 0.85 * Math.cos(angle);
      const baseY = centerY + radius * 0.85 * Math.sin(angle);
      const override = posOverrides[`p-${p.id}`];
      return { ...p, x: override?.x ?? baseX, y: override?.y ?? baseY };
    });
  }, [petitions, centerX, centerY, radius, posOverrides]);

  // Trending topic positions (bottom arc)
  const trendingPositions = useMemo(() => {
    return trendingTopics.map((t, i) => {
      const spread = Math.PI * 0.6; // 108-degree arc at the bottom
      const startAngle = Math.PI / 2 - spread / 2;
      const angle = startAngle + (i / Math.max(trendingTopics.length - 1, 1)) * spread;
      const baseX = centerX + radius * 0.95 * Math.cos(angle);
      const baseY = centerY + radius * 0.95 * Math.sin(angle);
      const override = posOverrides[`t-${t.id}`];
      return { ...t, x: override?.x ?? baseX, y: override?.y ?? baseY };
    });
  }, [trendingTopics, centerX, centerY, radius, posOverrides]);

  // SVG drag handlers
  const handleSvgMouseMove = useCallback((e: React.MouseEvent<SVGSVGElement>) => {
    if (!dragRef.current || !svgRef.current) return;
    const rect = svgRef.current.getBoundingClientRect();
    const x = e.clientX - rect.left - dragRef.current.offsetX;
    const y = e.clientY - rect.top - dragRef.current.offsetY;
    const dx = e.clientX - dragRef.current.startX;
    const dy = e.clientY - dragRef.current.startY;
    if (Math.abs(dx) > 4 || Math.abs(dy) > 4) dragRef.current.dragged = true;
    const prefix = dragRef.current.type === 'market' ? 'm' : dragRef.current.type === 'petition' ? 'p' : 't';
    const key = `${prefix}-${dragRef.current.id}`;
    setPosOverrides(prev => ({ ...prev, [key]: { x, y } }));
  }, []);

  const handleSvgMouseUp = useCallback(() => {
    if (dragRef.current) {
      if (!dragRef.current.dragged) {
        const { id, type } = dragRef.current;
        if (type === 'market') onSelectMarket(id);
        else if (type === 'petition') onSelectPetition(selectedPetition === id ? null : id);
        else onSelectTrending(id);
      }
      dragRef.current = null;
      setIsDragging(false);
    }
  }, [onSelectMarket, onSelectPetition, onSelectTrending, selectedPetition]);

  const handleNodeMouseDown = useCallback((e: React.MouseEvent, id: string, type: 'market' | 'petition' | 'trending', nodeX: number, nodeY: number) => {
    e.preventDefault();
    if (!svgRef.current) return;
    const rect = svgRef.current.getBoundingClientRect();
    const svgX = e.clientX - rect.left;
    const svgY = e.clientY - rect.top;
    dragRef.current = {
      id, type,
      offsetX: svgX - nodeX,
      offsetY: svgY - nodeY,
      startX: e.clientX,
      startY: e.clientY,
      dragged: false,
    };
    setIsDragging(true);
  }, []);

  // Which nodes are highlighted (supports multi-select)
  const highlightedMarkets = useMemo(() => {
    const s = new Set<string>();
    if (hoveredMarket) s.add(hoveredMarket);
    selectedMarkets.forEach(id => s.add(id));
    if (hoveredPetition || selectedPetition) {
      const pid = hoveredPetition || selectedPetition;
      links.filter(l => l.petition_id === pid).forEach(l => s.add(l.market_id));
    }
    return s;
  }, [hoveredMarket, selectedMarkets, hoveredPetition, selectedPetition, links]);

  const highlightedPetitions = useMemo(() => {
    const s = new Set<string>();
    if (hoveredPetition) s.add(hoveredPetition);
    if (selectedPetition) s.add(selectedPetition);
    if (hoveredMarket || selectedMarkets.length > 0) {
      const mids = hoveredMarket ? [hoveredMarket] : selectedMarkets;
      mids.forEach(mid => links.filter(l => l.market_id === mid).forEach(l => s.add(l.petition_id)));
    }
    return s;
  }, [hoveredPetition, selectedPetition, hoveredMarket, selectedMarkets, links]);

  const highlightedLinks = useMemo(() => {
    const s = new Set<string>();
    const mids = hoveredMarket ? [hoveredMarket] : selectedMarkets;
    const pid = hoveredPetition || selectedPetition;
    links.forEach(l => {
      if (mids.some(mid => l.market_id === mid)) s.add(l.id);
      if (pid && l.petition_id === pid) s.add(l.id);
    });
    return s;
  }, [hoveredMarket, selectedMarkets, hoveredPetition, selectedPetition, links]);

  const anyHighlight = highlightedMarkets.size > 0 || highlightedPetitions.size > 0;

  // Max volume for sizing
  const maxVol = Math.max(...markets.map(m => m.volume), 1);

  return (
    <div ref={containerRef} style={{ position: 'absolute', inset: 0, overflow: 'hidden' }}>
      <svg
        ref={svgRef}
        width={dims.w} height={dims.h}
        viewBox={`${panOffset.x + (dims.w - dims.w / zoom) / 2} ${panOffset.y + (dims.h - dims.h / zoom) / 2} ${dims.w / zoom} ${dims.h / zoom}`}
        style={{ display: 'block', cursor: isDragging ? 'grabbing' : 'default' }}
        onMouseMove={handleSvgMouseMove}
        onMouseUp={handleSvgMouseUp}
        onMouseLeave={handleSvgMouseUp}
        onWheel={(e) => {
          e.preventDefault();
          if (e.deltaY < 0) handleZoomIn();
          else handleZoomOut();
        }}
      >
        <defs>
          {/* Animated dash pattern */}
          <style>{`
            @keyframes orbitDash {
              to { stroke-dashoffset: -40; }
            }
            @keyframes orbitPulse {
              0%, 100% { r: 22; opacity: 0.7; }
              50% { r: 26; opacity: 1; }
            }
            @keyframes orbitGlow {
              0%, 100% { opacity: 0.3; }
              50% { opacity: 0.6; }
            }
          `}</style>
          <radialGradient id="hubGradient" cx="50%" cy="50%" r="50%">
            <stop offset="0%" stopColor="#a78bfa" />
            <stop offset="100%" stopColor="#7c3aed" />
          </radialGradient>
          <filter id="glow">
            <feGaussianBlur stdDeviation="3" result="coloredBlur" />
            <feMerge>
              <feMergeNode in="coloredBlur" />
              <feMergeNode in="SourceGraphic" />
            </feMerge>
          </filter>
        </defs>

        {/* Connection lines */}
        {links.map(link => {
          const mp = marketPositions.find(m => m.id === link.market_id);
          const pp = petitionPositions.find(p => p.id === link.petition_id);
          if (!mp || !pp) return null;

          const isHl = highlightedLinks.has(link.id);
          const opacity = anyHighlight ? (isHl ? 1 : 0.08) : 0.45 + link.strength * 0.3;
          const strokeWidth = 1.5 + link.strength * 3;
          const color = LINK_COLORS[link.link_type] || '#7c3aed';

          // Deterministic curve offset from link ID (no Math.random flickering)
          const hash = link.id.split('').reduce((a, c) => a + c.charCodeAt(0), 0);
          const midX = centerX + ((hash % 40) - 20);
          const midY = centerY + (((hash * 7) % 40) - 20);

          return (
            <g key={link.id} filter={isHl ? 'url(#glow)' : undefined}>
              {/* Glow underlay for visibility on dark bg */}
              <path
                d={`M ${mp.x} ${mp.y} Q ${midX} ${midY} ${pp.x} ${pp.y}`}
                fill="none"
                stroke={color}
                strokeWidth={strokeWidth + 2}
                opacity={opacity * 0.3}
                strokeLinecap="round"
              />
              {/* Main connection line */}
              <path
                d={`M ${mp.x} ${mp.y} Q ${midX} ${midY} ${pp.x} ${pp.y}`}
                fill="none"
                stroke={color}
                strokeWidth={strokeWidth}
                opacity={opacity}
                strokeDasharray={isHl ? '8 6' : 'none'}
                strokeLinecap="round"
                style={isHl ? { animation: 'orbitDash 1.5s linear infinite' } : {}}
              />
              {/* Endpoint dots */}
              <circle cx={mp.x} cy={mp.y} r={isHl ? 3 : 2} fill={color} opacity={opacity * 0.8} />
              <circle cx={pp.x} cy={pp.y} r={isHl ? 3 : 2} fill={color} opacity={opacity * 0.8} />
              {/* Label on hover */}
              {isHl && (
                <text
                  x={(mp.x + pp.x) / 2}
                  y={(mp.y + pp.y) / 2 - 10}
                  textAnchor="middle"
                  fontSize={9}
                  fill={color}
                  opacity={0.9}
                  fontWeight={600}
                >
                  {truncate(link.ai_reasoning, 50)}
                </text>
              )}
            </g>
          );
        })}

        {/* Center hub */}
        <circle
          cx={centerX}
          cy={centerY}
          r={24}
          fill="url(#hubGradient)"
          filter="url(#glow)"
          style={{ animation: 'orbitPulse 3s ease-in-out infinite' }}
        />
        <circle
          cx={centerX}
          cy={centerY}
          r={34}
          fill="none"
          stroke="#7c3aed"
          strokeWidth={1}
          opacity={0.2}
          style={{ animation: 'orbitGlow 3s ease-in-out infinite' }}
        />
        <text
          x={centerX}
          y={centerY + 1}
          textAnchor="middle"
          dominantBaseline="middle"
          fontSize={10}
          fontWeight={800}
          fill="#fff"
          letterSpacing="0.1em"
        >
          ORBIT
        </text>

        {/* Market nodes (left arc) */}
        {marketPositions.map(m => {
          const catColor = CATEGORY_COLORS[m.category] || CATEGORY_COLORS.default;
          const nodeR = 14 + (m.volume / maxVol) * 16;
          const isHl = highlightedMarkets.has(m.id);
          const isSelected = selectedMarkets.includes(m.id);
          const opacity = anyHighlight ? (isHl ? 1 : 0.25) : 0.85;

          return (
            <g
              key={m.id}
              style={{ cursor: isDragging ? 'grabbing' : 'grab', transition: 'opacity 150ms ease' }}
              opacity={opacity}
              onMouseEnter={() => !isDragging && onHoverMarket(m.id)}
              onMouseLeave={() => !isDragging && onHoverMarket(null)}
              onMouseDown={(e) => {
                // Shift-click handled inside handleSvgMouseUp via onSelectMarket
                if (e.shiftKey) {
                  e.preventDefault();
                  onSelectMarket(m.id, true);
                  return;
                }
                handleNodeMouseDown(e, m.id, 'market', m.x, m.y);
              }}
            >
              {/* Glow ring on selection */}
              {isSelected && (
                <circle
                  cx={m.x} cy={m.y} r={nodeR + 6}
                  fill="none" stroke={catColor} strokeWidth={2}
                  opacity={0.5}
                  strokeDasharray="4 3"
                  style={{ animation: 'orbitDash 2s linear infinite' }}
                />
              )}
              <circle
                cx={m.x} cy={m.y} r={nodeR}
                fill={`${catColor}18`}
                stroke={catColor}
                strokeWidth={isHl ? 2.5 : 1.5}
                filter={isSelected ? 'url(#glow)' : undefined}
              />
              {/* Yes probability arc */}
              <circle
                cx={m.x} cy={m.y} r={nodeR - 3}
                fill="none"
                stroke={catColor}
                strokeWidth={3}
                strokeDasharray={`${m.yes_price * 2 * Math.PI * (nodeR - 3)} ${2 * Math.PI * (nodeR - 3)}`}
                strokeDashoffset={0}
                transform={`rotate(-90, ${m.x}, ${m.y})`}
                opacity={0.75}
              />
              {/* Probability text */}
              <text
                x={m.x} y={m.y - 1}
                textAnchor="middle" dominantBaseline="middle"
                fontSize={nodeR > 20 ? 12 : 10} fontWeight={800}
                fill={catColor}
              >
                {Math.round(m.yes_price * 100)}%
              </text>
              {/* Title below with background for readability */}
              <text
                x={m.x} y={m.y + nodeR + 12}
                textAnchor="middle"
                fontSize={9}
                fill="var(--color-text)"
                fontWeight={600}
                style={{ filter: 'drop-shadow(0 0 3px rgba(10,10,18,0.9)) drop-shadow(0 0 6px rgba(10,10,18,0.7))' }}
              >
                {truncate(m.title, 22)}
              </text>
            </g>
          );
        })}

        {/* Petition nodes (right arc) */}
        {petitionPositions.map(p => {
          const sty = STATUS_STYLES[p.status] || STATUS_STYLES.draft;
          const progress = p.signature_goal > 0 ? p.signature_count / p.signature_goal : 0;
          const isHl = highlightedPetitions.has(p.id);
          const isSelected = selectedPetition === p.id;
          const opacity = anyHighlight ? (isHl ? 1 : 0.25) : 0.85;
          const rectW = 130;
          const rectH = 44;

          return (
            <g
              key={p.id}
              style={{ cursor: isDragging ? 'grabbing' : 'grab', transition: 'opacity 150ms ease' }}
              opacity={opacity}
              onMouseEnter={() => !isDragging && onHoverPetition(p.id)}
              onMouseLeave={() => !isDragging && onHoverPetition(null)}
              onMouseDown={(e) => handleNodeMouseDown(e, p.id, 'petition', p.x, p.y)}
            >
              {/* Selection ring */}
              {isSelected && (
                <rect
                  x={p.x - rectW / 2 - 4} y={p.y - rectH / 2 - 4}
                  width={rectW + 8} height={rectH + 8}
                  rx={14} ry={14}
                  fill="none" stroke="#a78bfa" strokeWidth={2}
                  opacity={0.5}
                  strokeDasharray="4 3"
                  style={{ animation: 'orbitDash 2s linear infinite' }}
                />
              )}
              <rect
                x={p.x - rectW / 2} y={p.y - rectH / 2}
                width={rectW} height={rectH}
                rx={10} ry={10}
                fill="rgba(30, 30, 50, 0.9)"
                stroke={isHl ? '#a78bfa' : 'rgba(100, 100, 140, 0.4)'}
                strokeWidth={isHl ? 2 : 1}
              />
              {/* Left accent bar */}
              <rect
                x={p.x - rectW / 2 + 1} y={p.y - rectH / 2 + 6}
                width={3} height={rectH - 12}
                rx={1.5} ry={1.5}
                fill={sty.text}
              />
              {/* Title */}
              <text
                x={p.x + 2} y={p.y - 8}
                textAnchor="middle" dominantBaseline="middle"
                fontSize={9} fontWeight={700}
                fill="var(--color-text)"
              >
                {truncate(p.title, 20)}
              </text>
              {/* Progress bar */}
              <rect
                x={p.x - rectW / 2 + 8} y={p.y + 4}
                width={rectW - 16} height={4}
                rx={2} ry={2}
                fill="var(--color-bg)"
              />
              <rect
                x={p.x - rectW / 2 + 8} y={p.y + 4}
                width={(rectW - 16) * Math.min(progress, 1)} height={4}
                rx={2} ry={2}
                fill={sty.text}
              />
              {/* Sig count */}
              <text
                x={p.x} y={p.y + 16}
                textAnchor="middle" dominantBaseline="middle"
                fontSize={7}
                fill="var(--color-text-muted)"
              >
                {((p.signature_count || 0) / 1000).toFixed(1)}k / {((p.signature_goal || 1) / 1000).toFixed(0)}k
              </text>
            </g>
          );
        })}

        {/* Trending topic nodes (bottom arc — green diamonds) */}
        {trendingPositions.map(t => {
          const maxScore = Math.max(...trendingTopics.map(tt => tt.engagement_score), 1);
          const size = 10 + (t.engagement_score / maxScore) * 10;
          const isHovered = hoveredTrending === t.id;
          const opacity = isHovered ? 1 : 0.8;

          return (
            <g
              key={`trending-${t.id}`}
              style={{ cursor: isDragging ? 'grabbing' : 'grab', transition: 'opacity 150ms ease' }}
              opacity={opacity}
              onMouseEnter={() => { if (!isDragging) setHoveredTrending(t.id); }}
              onMouseLeave={() => { if (!isDragging) setHoveredTrending(null); }}
              onMouseDown={(e) => handleNodeMouseDown(e, t.id, 'trending', t.x, t.y)}
            >
              {/* Diamond shape (rotated square) */}
              <rect
                x={t.x - size / 2} y={t.y - size / 2}
                width={size} height={size}
                rx={3} ry={3}
                transform={`rotate(45, ${t.x}, ${t.y})`}
                fill={`${TRENDING_COLOR}25`}
                stroke={TRENDING_COLOR}
                strokeWidth={isHovered ? 2.5 : 1.5}
              />
              {/* Engagement score inside */}
              <text
                x={t.x} y={t.y + 1}
                textAnchor="middle" dominantBaseline="middle"
                fontSize={7} fontWeight={700}
                fill={TRENDING_COLOR}
              >
                {t.engagement_score}
              </text>
              {/* Title below — only show on hover to reduce clutter */}
              {isHovered && (
                <text
                  x={t.x} y={t.y + size / 2 + 14}
                  textAnchor="middle"
                  fontSize={8}
                  fill="var(--color-text)"
                  fontWeight={600}
                >
                  {truncate(t.title, 28)}
                </text>
              )}
              {/* Tooltip on hover */}
              {isHovered && t.content && (
                <g>
                  <rect
                    x={t.x - 90} y={t.y - size / 2 - 32}
                    width={180} height={22}
                    rx={6} fill="var(--color-surface-el)"
                    stroke="var(--color-border)" strokeWidth={0.5}
                  />
                  <text
                    x={t.x} y={t.y - size / 2 - 18}
                    textAnchor="middle" dominantBaseline="middle"
                    fontSize={7} fill="var(--color-text-muted)"
                  >
                    {truncate(t.content, 50)}
                  </text>
                </g>
              )}
            </g>
          );
        })}

        {/* Alliance line between 2 selected markets */}
        {selectedMarkets.length === 2 && (() => {
          const m1 = marketPositions.find(m => m.id === selectedMarkets[0]);
          const m2 = marketPositions.find(m => m.id === selectedMarkets[1]);
          if (!m1 || !m2) return null;
          const midX = (m1.x + m2.x) / 2;
          const midY = (m1.y + m2.y) / 2;
          return (
            <g>
              <line
                x1={m1.x} y1={m1.y} x2={m2.x} y2={m2.y}
                stroke="#f59e0b" strokeWidth={2.5}
                strokeDasharray="6 4" opacity={0.8}
                style={{ animation: 'orbitDash 1.5s linear infinite' }}
              />
              <rect x={midX - 36} y={midY - 10} width={72} height={18} rx={9} fill="#f59e0b" opacity={0.9} />
              <text x={midX} y={midY + 2} textAnchor="middle" dominantBaseline="middle"
                fontSize={8} fontWeight={700} fill="#000" letterSpacing="0.03em">
                ALLIANCE
              </text>
            </g>
          );
        })()}

        {/* Legend */}
        <g transform={`translate(12, ${dims.h - 80})`}>
          <text fontSize={9} fill="var(--color-text-secondary)" fontWeight={700} letterSpacing="0.05em">LINK TYPES</text>
          {Object.entries(LINK_COLORS).map(([type, color], i) => (
            <g key={type} transform={`translate(0, ${16 + i * 15})`}>
              <line x1={0} y1={0} x2={18} y2={0} stroke={color} strokeWidth={2.5} />
              <text x={22} y={3} fontSize={8} fill="var(--color-text-muted)" fontWeight={500}>{type.charAt(0).toUpperCase() + type.slice(1)}</text>
            </g>
          ))}
        </g>

        {/* Category legend */}
        <g transform={`translate(12, 12)`}>
          <text fontSize={9} fill="var(--color-text-secondary)" fontWeight={700} letterSpacing="0.05em">CATEGORIES</text>
          {Object.entries(CATEGORY_COLORS).filter(([k]) => k !== 'default').slice(0, 5).map(([cat, color], i) => (
            <g key={cat} transform={`translate(${i * 68}, 16)`}>
              <circle cx={5} cy={0} r={4} fill={color} />
              <text x={12} y={3} fontSize={8} fill="var(--color-text-muted)" fontWeight={500}>{cat}</text>
            </g>
          ))}
        </g>

        {/* Node type legend (top right) */}
        <g transform={`translate(${dims.w - 130}, 12)`}>
          <text fontSize={9} fill="var(--color-text-secondary)" fontWeight={700} letterSpacing="0.05em">NODES</text>
          <g transform="translate(0, 14)">
            <circle cx={5} cy={0} r={4} fill="#3b82f622" stroke="#3b82f6" strokeWidth={1} />
            <text x={14} y={3} fontSize={7} fill="var(--color-text-muted)">Market (circle)</text>
          </g>
          <g transform="translate(0, 28)">
            <rect x={1} y={-4} width={8} height={8} rx={2} fill="var(--color-surface-el)" stroke="var(--color-border)" strokeWidth={1} />
            <text x={14} y={3} fontSize={7} fill="var(--color-text-muted)">Petition (rect)</text>
          </g>
          <g transform="translate(0, 42)">
            <rect x={2} y={-3} width={6} height={6} rx={1} fill={`${TRENDING_COLOR}25`} stroke={TRENDING_COLOR} strokeWidth={1}
              transform="rotate(45, 5, 0)" />
            <text x={14} y={3} fontSize={7} fill="var(--color-text-muted)">Trending (diamond)</text>
          </g>
        </g>

        {/* Shift+click hint */}
        <text x={dims.w - 12} y={dims.h - 8} textAnchor="end"
          fontSize={8} fill="var(--color-text-muted)" opacity={0.5}>
          Drag to move | Scroll to zoom | Shift+click for Alliance
        </text>
      </svg>

      {/* Zoom controls (top-left overlay) */}
      <div style={{
        position: 'absolute', top: 8, left: 8, zIndex: 10,
        display: 'flex', flexDirection: 'column', gap: 2,
      }}>
        {[
          { icon: <ZoomIn size={13} />, action: handleZoomIn, title: 'Zoom In' },
          { icon: <ZoomOut size={13} />, action: handleZoomOut, title: 'Zoom Out' },
          { icon: <Maximize2 size={13} />, action: handleZoomReset, title: 'Reset View' },
        ].map(({ icon, action, title }) => (
          <button
            key={title}
            onClick={action}
            title={title}
            style={{
              width: 28, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center',
              borderRadius: 6, border: '1px solid var(--color-border)',
              backgroundColor: 'var(--color-surface-el)',
              color: 'var(--color-text-muted)', cursor: 'pointer',
              transition: 'all 150ms ease',
            }}
            onMouseEnter={e => { e.currentTarget.style.borderColor = '#a78bfa'; e.currentTarget.style.color = '#a78bfa'; }}
            onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--color-border)'; e.currentTarget.style.color = 'var(--color-text-muted)'; }}
          >
            {icon}
          </button>
        ))}
      </div>

      {/* Zoom level indicator */}
      {zoom !== 1 && (
        <div style={{
          position: 'absolute', top: 8, left: 42,
          padding: '4px 8px', borderRadius: 6,
          backgroundColor: 'var(--color-surface-el)',
          border: '1px solid var(--color-border)',
          fontSize: 9, fontWeight: 600, color: '#a78bfa',
        }}>
          {Math.round(zoom * 100)}%
        </div>
      )}

      {/* Reset Layout button */}
      {Object.keys(posOverrides).length > 0 && (
        <button
          onClick={() => setPosOverrides({})}
          style={{
            position: 'absolute', bottom: 8, left: 12,
            padding: '3px 10px', borderRadius: 6,
            border: '1px solid var(--color-border)',
            backgroundColor: 'var(--color-surface-el)',
            color: 'var(--color-text-muted)',
            fontSize: 9, fontWeight: 600, cursor: 'pointer',
            zIndex: 10, transition: 'all 150ms ease',
          }}
          onMouseEnter={e => { e.currentTarget.style.borderColor = '#a78bfa'; e.currentTarget.style.color = '#a78bfa'; }}
          onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--color-border)'; e.currentTarget.style.color = 'var(--color-text-muted)'; }}
        >
          Reset Layout
        </button>
      )}
    </div>
  );
}

/* ═══════════════════════════════════════════════════════════════════════════
   Pulse Panel Sub-Component
   ═══════════════════════════════════════════════════════════════════════════ */

interface PulsePanelProps {
  articles: RSSArticle[];
  loading: boolean;
  onRefresh: () => void;
  refreshing: boolean;
}

function PulsePanel({ articles, loading, onRefresh, refreshing }: PulsePanelProps) {
  const [feedFilter, setFeedFilter] = useState<string>('all');
  const filters = ['all', 'national', 'local', 'wire'];

  const filtered = useMemo(() => {
    if (feedFilter === 'all') return articles;
    return articles.filter(a => a.feed_category === feedFilter);
  }, [articles, feedFilter]);

  return (
    <div style={{
      display: 'flex', flexDirection: 'column', height: '100%',
      backgroundColor: 'var(--color-surface)',
      border: '1px solid var(--color-border)',
      borderRadius: 12, overflow: 'hidden',
    }}>
      {/* Header */}
      <div style={{
        padding: '10px 14px', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        borderBottom: '1px solid var(--color-border)', flexShrink: 0,
      }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <Rss size={14} style={{ color: '#a78bfa' }} />
          <span style={{ fontSize: 12, fontWeight: 700, color: 'var(--color-text)' }}>Pulse Feed</span>
          <span style={{
            fontSize: 9, padding: '1px 6px', borderRadius: 99,
            backgroundColor: 'rgba(124, 58, 237, 0.15)', color: '#a78bfa',
            fontWeight: 600,
          }}>
            {articles.length}
          </span>
        </div>
        <button
          onClick={onRefresh}
          disabled={refreshing}
          style={{
            display: 'flex', alignItems: 'center', gap: 4, padding: '4px 8px',
            borderRadius: 6, border: '1px solid var(--color-border)',
            backgroundColor: 'transparent', color: 'var(--color-text-secondary)',
            cursor: 'pointer', fontSize: 10, fontWeight: 600,
            transition: 'all 150ms ease',
          }}
          onMouseEnter={e => { e.currentTarget.style.backgroundColor = 'var(--color-surface-el)'; }}
          onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'transparent'; }}
        >
          {refreshing
            ? <Loader2 size={11} style={{ animation: 'spin 1s linear infinite' }} />
            : <RefreshCw size={11} />
          }
          Refresh
        </button>
      </div>

      {/* Filter chips */}
      <div style={{
        padding: '8px 14px', display: 'flex', gap: 4, flexShrink: 0,
        borderBottom: '1px solid var(--color-border)',
      }}>
        {filters.map(f => (
          <button
            key={f}
            onClick={() => setFeedFilter(f)}
            style={{
              padding: '3px 10px', borderRadius: 99, border: 'none',
              fontSize: 10, fontWeight: 600, cursor: 'pointer',
              backgroundColor: feedFilter === f ? 'rgba(124, 58, 237, 0.2)' : 'var(--color-surface-el)',
              color: feedFilter === f ? '#a78bfa' : 'var(--color-text-muted)',
              transition: 'all 150ms ease',
              textTransform: 'capitalize',
            }}
          >
            {f}
          </button>
        ))}
      </div>

      {/* Articles list */}
      <div style={{ flex: 1, overflowY: 'auto', padding: '6px 10px' }}>
        {loading && (
          <div style={{ padding: 20, textAlign: 'center' }}>
            <Loader2 size={18} style={{ color: 'var(--color-text-muted)', animation: 'spin 1s linear infinite' }} />
          </div>
        )}
        {!loading && filtered.length === 0 && (
          <div style={{ padding: 20, textAlign: 'center', color: 'var(--color-text-muted)', fontSize: 11 }}>
            No articles found for this filter.
          </div>
        )}
        {!loading && filtered.map(article => {
          const srcName = article.source || article.feed_name || 'Unknown';
          const srcColor = SOURCE_COLORS[srcName] || SOURCE_COLORS.default;
          const sentColor = sentimentColor(article.sentiment || 'neutral');
          return (
            <div
              key={article.id}
              style={{
                padding: '8px 10px 8px 12px', borderRadius: 8, marginBottom: 4,
                borderLeft: `2px solid ${srcColor}40`,
                transition: 'all 150ms ease', cursor: 'default',
              }}
              onMouseEnter={e => { e.currentTarget.style.backgroundColor = 'var(--color-surface-el)'; }}
              onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'transparent'; }}
            >
              <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
                {/* Source badge */}
                <span style={{
                  fontSize: 8, fontWeight: 700, padding: '1px 5px', borderRadius: 4,
                  backgroundColor: `${srcColor}20`, color: srcColor,
                  border: `1px solid ${srcColor}40`,
                  textTransform: 'uppercase', letterSpacing: '0.03em',
                }}>
                  {srcName}
                </span>
                {/* Time */}
                <span style={{ fontSize: 9, color: 'var(--color-text-muted)', marginLeft: 'auto' }}>
                  {relativeTime(article.published_at)}
                </span>
              </div>
              {/* Title */}
              <a
                href={article.url}
                target="_blank"
                rel="noopener noreferrer"
                style={{
                  fontSize: 11, fontWeight: 600, color: 'var(--color-text)',
                  textDecoration: 'none', lineHeight: 1.35,
                  display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical',
                  overflow: 'hidden',
                }}
              >
                {decodeEntities(article.title)}
              </a>
              {/* Bottom row: sentiment (only if non-neutral) + tickers */}
              <div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 4, flexWrap: 'wrap' }}>
                {article.sentiment && article.sentiment !== 'neutral' && (
                  <span style={{
                    fontSize: 8, fontWeight: 600, padding: '1px 5px', borderRadius: 4,
                    backgroundColor: `${sentColor}20`, color: sentColor,
                    textTransform: 'capitalize',
                  }}>
                    {article.sentiment}
                  </span>
                )}
                {(article.matched_tickers || article.matched_markets || []).map(t => (
                  <span key={t} style={{
                    fontSize: 8, fontWeight: 700, padding: '2px 6px', borderRadius: 4,
                    backgroundColor: 'rgba(124, 58, 237, 0.12)', color: '#a78bfa',
                    letterSpacing: '0.02em',
                  }}>
                    {t}
                  </span>
                ))}
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

/* ═══════════════════════════════════════════════════════════════════════════
   Actions Panel Sub-Component
   ═══════════════════════════════════════════════════════════════════════════ */

interface ActionsPanelProps {
  selectedMarkets: KalshiMarket[];
  selectedPetition: Petition | null;
  onClearSelection: () => void;
  onRefresh: () => void;
}

function ActionsPanel({ selectedMarkets, selectedPetition, onClearSelection, onRefresh }: ActionsPanelProps) {
  const [tone, setTone] = useState('urgent');
  const [generating, setGenerating] = useState(false);
  const [generated, setGenerated] = useState<GeneratedPetition | null>(null);
  const [copiedPlatform, setCopiedPlatform] = useState<string | null>(null);
  const [actionsExpanded, setActionsExpanded] = useState(true);
  const [toast, setToast] = useState<{ msg: string; type: 'success' | 'error' | 'info' } | null>(null);
  const [saving, setSaving] = useState(false);
  const [batchLinking, setBatchLinking] = useState(false);

  function showToast(msg: string, type: 'success' | 'error' | 'info' = 'info') {
    setToast({ msg, type });
    setTimeout(() => setToast(null), 3000);
  }

  // Derived: first selected market (for single-market mode)
  const selectedMarket = selectedMarkets[0] || null;
  // Alliance mode: 2 markets from different categories
  const isAllianceMode = selectedMarkets.length === 2 &&
    selectedMarkets[0].category !== selectedMarkets[1].category;

  async function handleGenerate() {
    if (!selectedMarket) return;
    setGenerating(true);
    setGenerated(null);
    try {
      const res = await fetch('/api/orbit/generate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          market_id: selectedMarket.id,
          market_title: selectedMarket.title,
          market_ticker: selectedMarket.ticker,
          market_yes_price: selectedMarket.yes_price,
          tone,
        }),
      });
      if (res.ok) {
        const data = await res.json();
        setGenerated(data.petition || data);
      } else {
        setGenerated({
          title: `Action Needed: ${selectedMarket.title}`,
          summary: `Based on Kalshi market ${selectedMarket.ticker} showing ${Math.round(selectedMarket.yes_price * 100)}% probability, we urge immediate action on this critical issue.`,
          body_html: `<p>We, the undersigned, call for immediate attention to the issue tracked by prediction market ${selectedMarket.ticker}.</p>`,
          body_text: `We, the undersigned, call for immediate attention to the issue tracked by prediction market ${selectedMarket.ticker}.`,
          suggested_target: 'U.S. Congress',
          suggested_tags: [(selectedMarket.category || 'Other').toLowerCase(), 'prediction-market', 'action'],
          suggested_goal: 10000,
        });
      }
    } catch {
      setGenerated({
        title: `Action Needed: ${selectedMarket.title}`,
        summary: `Based on Kalshi market ${selectedMarket.ticker} showing ${Math.round(selectedMarket.yes_price * 100)}% probability, we urge immediate action.`,
        body_html: `<p>We call for immediate attention to this issue.</p>`,
        body_text: `We call for immediate attention to this issue.`,
        suggested_target: 'U.S. Congress',
        suggested_tags: [(selectedMarket.category || 'Other').toLowerCase()],
        suggested_goal: 10000,
      });
    } finally {
      setGenerating(false);
    }
  }

  async function handleAllianceGenerate() {
    if (selectedMarkets.length !== 2) return;
    setGenerating(true);
    setGenerated(null);
    try {
      const res = await fetch('/api/orbit/suggest-alliance', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          market_tickers: [selectedMarkets[0].ticker, selectedMarkets[1].ticker],
          tone,
        }),
      });
      if (res.ok) {
        const data = await res.json();
        setGenerated(data.petition || data);
      } else {
        // Fallback alliance mock
        setGenerated({
          title: `United Action: ${selectedMarkets[0].category} + ${selectedMarkets[1].category}`,
          summary: `An unlikely alliance petition bridging ${selectedMarkets[0].title} and ${selectedMarkets[1].title} for maximum political impact.`,
          body_html: `<p>We, a coalition spanning ${selectedMarkets[0].category} and ${selectedMarkets[1].category} advocates, demand unified action.</p>`,
          body_text: `We, a coalition spanning ${selectedMarkets[0].category} and ${selectedMarkets[1].category} advocates, demand unified action.`,
          suggested_target: 'U.S. Congress',
          suggested_tags: [selectedMarkets[0].category.toLowerCase(), selectedMarkets[1].category.toLowerCase(), 'alliance'],
          suggested_goal: 25000,
          alliance_insight: `This alliance between ${selectedMarkets[0].category} and ${selectedMarkets[1].category} groups creates cross-partisan momentum that neither group could achieve alone.`,
          group_a_appeal: `${selectedMarkets[0].category} supporters care because it directly impacts ${selectedMarkets[0].title}.`,
          group_b_appeal: `${selectedMarkets[1].category} supporters care because it connects to ${selectedMarkets[1].title}.`,
          impact_multiplier: `Cross-partisan petitions historically achieve 3-5x higher engagement and are more likely to receive legislative attention.`,
        });
      }
    } catch {
      setGenerated({
        title: `United Action: ${selectedMarkets[0].category} + ${selectedMarkets[1].category}`,
        summary: `Alliance petition bridging two issue groups.`,
        body_html: `<p>Coalition petition spanning multiple constituencies.</p>`,
        body_text: `Coalition petition spanning multiple constituencies.`,
        suggested_target: 'U.S. Congress',
        suggested_tags: ['alliance'],
        suggested_goal: 25000,
      });
    } finally {
      setGenerating(false);
    }
  }

  function handleCopyAndOpen(platformKey: string) {
    const platform = PLATFORMS[platformKey];
    if (!platform || !generated) return;
    const text = `${generated.title}\n\n${generated.body_text || generated.summary}`;
    // Clipboard with fallback for HTTP contexts
    const doCopy = () => {
      if (navigator.clipboard?.writeText) {
        return navigator.clipboard.writeText(text);
      }
      // Fallback: textarea copy
      const ta = document.createElement('textarea');
      ta.value = text;
      ta.style.position = 'fixed';
      ta.style.left = '-9999px';
      document.body.appendChild(ta);
      ta.select();
      document.execCommand('copy');
      document.body.removeChild(ta);
      return Promise.resolve();
    };
    doCopy().then(() => {
      setCopiedPlatform(platformKey);
      showToast(`Copied to clipboard! Opening ${platform.name}...`, 'success');
      setTimeout(() => setCopiedPlatform(null), 2000);
      window.open(platform.url, '_blank');
    }).catch(() => {
      showToast('Copy failed — opening platform anyway', 'error');
      window.open(platform.url, '_blank');
    });
  }

  async function handleSaveDraft() {
    if (!generated) return;
    setSaving(true);
    try {
      const res = await fetch('/api/petitions', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          title: generated.title,
          summary: generated.summary,
          body_html: generated.body_html || `<p>${generated.summary}</p>`,
          body_text: generated.body_text || generated.summary,
          target: generated.suggested_target,
          tags: generated.suggested_tags,
          signature_goal: generated.suggested_goal || 10000,
          status: 'draft',
        }),
      });
      if (res.ok) {
        showToast('Petition saved as draft!', 'success');
        setGenerated(null);
        onClearSelection();
        onRefresh();
      } else {
        const err = await res.json().catch(() => ({ error: 'Save failed' }));
        showToast(err.error || 'Failed to save petition', 'error');
      }
    } catch {
      showToast('Network error — petition not saved', 'error');
    } finally {
      setSaving(false);
    }
  }

  async function handleBatchLink() {
    setBatchLinking(true);
    try {
      const res = await fetch('/api/orbit/batch-link', { method: 'POST' });
      if (res.ok) {
        const data = await res.json();
        showToast(`AI linked ${data.links_inserted} connections, created ${data.petitions_created} petitions`, 'success');
      } else {
        showToast('AI linking failed — try again', 'error');
      }
    } catch {
      showToast('Network error during AI linking', 'error');
    } finally {
      setBatchLinking(false);
    }
  }

  async function handleActivatePetition(petitionId: string) {
    try {
      const res = await fetch('/api/petitions', {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ id: petitionId, status: 'active' }),
      });
      if (res.ok) { showToast('Petition activated!', 'success'); onRefresh(); }
      else showToast('Failed to activate', 'error');
    } catch {
      showToast('Network error', 'error');
    }
  }

  async function handleArchivePetition(petitionId: string) {
    try {
      const res = await fetch('/api/petitions', {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ id: petitionId, status: 'closed' }),
      });
      if (res.ok) {
        showToast('Petition archived', 'info');
        onClearSelection();
        onRefresh();
      } else showToast('Failed to archive', 'error');
    } catch {
      showToast('Network error', 'error');
    }
  }

  return (
    <div style={{
      display: 'flex', flexDirection: 'column', height: '100%',
      backgroundColor: 'var(--color-surface)',
      border: '1px solid var(--color-border)',
      borderRadius: 12, overflow: 'hidden',
      position: 'relative',
    }}>
      {/* Header */}
      <div
        style={{
          padding: '10px 14px', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          borderBottom: '1px solid var(--color-border)', flexShrink: 0, cursor: 'pointer',
        }}
        onClick={() => setActionsExpanded(!actionsExpanded)}
      >
        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <Zap size={14} style={{ color: '#a78bfa' }} />
          <span style={{ fontSize: 12, fontWeight: 700, color: 'var(--color-text)' }}>Actions</span>
        </div>
        {actionsExpanded ? <ChevronUp size={14} style={{ color: 'var(--color-text-muted)' }} /> : <ChevronDown size={14} style={{ color: 'var(--color-text-muted)' }} />}
      </div>

      {/* Toast notification */}
      {toast && (
        <div style={{
          position: 'absolute', bottom: 12, left: '50%', transform: 'translateX(-50%)',
          zIndex: 100, padding: '8px 16px', borderRadius: 8,
          backgroundColor: toast.type === 'success' ? 'rgba(34, 197, 94, 0.9)' : toast.type === 'error' ? 'rgba(239, 68, 68, 0.9)' : 'rgba(124, 58, 237, 0.9)',
          color: '#fff', fontSize: 11, fontWeight: 600, whiteSpace: 'nowrap',
          boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
          animation: 'fadeInUp 200ms ease',
        }}>
          {toast.msg}
        </div>
      )}

      {actionsExpanded && (
        <div style={{ flex: 1, overflowY: 'auto', padding: '10px 14px' }}>
          {/* No selection — Quick Actions */}
          {selectedMarkets.length === 0 && !selectedPetition && !generated && (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
              <div style={{ textAlign: 'center', padding: '12px 10px' }}>
                <div style={{
                  width: 40, height: 40, borderRadius: 12, margin: '0 auto 10px',
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  background: 'linear-gradient(135deg, rgba(124, 58, 237, 0.15), rgba(167, 139, 250, 0.1))',
                  border: '1px solid rgba(124, 58, 237, 0.2)',
                }}>
                  <Target size={18} style={{ color: '#a78bfa' }} />
                </div>
                <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text)', marginBottom: 4 }}>
                  Select a Node
                </div>
                <div style={{ fontSize: 10, color: 'var(--color-text-muted)', lineHeight: 1.4 }}>
                  Click a market to generate a petition. Shift+click two from different categories for Alliance mode.
                </div>
              </div>

              {/* Quick Action Buttons */}
              <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                <div style={{ fontSize: 9, fontWeight: 700, color: 'var(--color-text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>
                  Quick Actions
                </div>
                <button
                  onClick={handleBatchLink}
                  disabled={batchLinking}
                  style={{
                    display: 'flex', alignItems: 'center', gap: 8,
                    padding: '8px 10px', borderRadius: 8,
                    border: '1px solid rgba(167, 139, 250, 0.3)',
                    backgroundColor: 'rgba(167, 139, 250, 0.06)',
                    color: '#a78bfa', fontSize: 10, fontWeight: 700, cursor: 'pointer',
                    transition: 'all 150ms ease', textAlign: 'left',
                    opacity: batchLinking ? 0.7 : 1,
                  }}
                  onMouseEnter={e => { if (!batchLinking) e.currentTarget.style.backgroundColor = 'rgba(167, 139, 250, 0.12)'; }}
                  onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'rgba(167, 139, 250, 0.06)'; }}
                >
                  {batchLinking
                    ? <Loader2 size={13} style={{ animation: 'spin 1s linear infinite', flexShrink: 0 }} />
                    : <Sparkles size={13} style={{ flexShrink: 0 }} />}
                  <span style={{ flex: 1 }}>AI Auto-Link Markets & Petitions</span>
                </button>
                <button
                  onClick={async () => {
                    showToast('Discovering trending topics...', 'info');
                    try {
                      const res = await fetch('/api/trending/discover', { method: 'POST' });
                      if (res.ok) {
                        const data = await res.json();
                        showToast(`Discovered ${data.count || 0} trending topics!`, 'success');
                      } else showToast('Discovery failed', 'error');
                    } catch { showToast('Network error', 'error'); }
                  }}
                  style={{
                    display: 'flex', alignItems: 'center', gap: 8,
                    padding: '8px 10px', borderRadius: 8,
                    border: '1px solid rgba(34, 197, 94, 0.3)',
                    backgroundColor: 'rgba(34, 197, 94, 0.06)',
                    color: '#22c55e', fontSize: 10, fontWeight: 700, cursor: 'pointer',
                    transition: 'all 150ms ease', textAlign: 'left',
                  }}
                  onMouseEnter={e => { e.currentTarget.style.backgroundColor = 'rgba(34, 197, 94, 0.12)'; }}
                  onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'rgba(34, 197, 94, 0.06)'; }}
                >
                  <TrendingUp size={13} style={{ flexShrink: 0 }} />
                  <span style={{ flex: 1 }}>Discover Trending Topics</span>
                </button>
                <button
                  onClick={() => {
                    showToast('Opening Pulse Agent...', 'info');
                    window.open('http://45.61.58.125:9845/', '_blank');
                  }}
                  style={{
                    display: 'flex', alignItems: 'center', gap: 8,
                    padding: '8px 10px', borderRadius: 8,
                    border: '1px solid rgba(6, 182, 212, 0.3)',
                    backgroundColor: 'rgba(6, 182, 212, 0.06)',
                    color: '#06b6d4', fontSize: 10, fontWeight: 700, cursor: 'pointer',
                    transition: 'all 150ms ease', textAlign: 'left',
                  }}
                  onMouseEnter={e => { e.currentTarget.style.backgroundColor = 'rgba(6, 182, 212, 0.12)'; }}
                  onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'rgba(6, 182, 212, 0.06)'; }}
                >
                  <Globe size={13} style={{ flexShrink: 0 }} />
                  <span style={{ flex: 1 }}>Open Pulse of America</span>
                </button>
              </div>
            </div>
          )}

          {/* Alliance mode — 2 markets from different categories */}
          {isAllianceMode && !generated && (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
              {/* Alliance badge */}
              <div style={{
                display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
                padding: '6px 12px', borderRadius: 8,
                background: 'linear-gradient(135deg, rgba(245, 158, 11, 0.15), rgba(245, 158, 11, 0.05))',
                border: '1px solid rgba(245, 158, 11, 0.3)',
              }}>
                <Zap size={12} style={{ color: '#f59e0b' }} />
                <span style={{ fontSize: 10, fontWeight: 700, color: '#f59e0b' }}>
                  Alliance Detected: {selectedMarkets[0].category} + {selectedMarkets[1].category}
                </span>
              </div>

              {/* Both markets stacked */}
              {selectedMarkets.map((m, idx) => (
                <div key={m.id} style={{
                  padding: '8px 12px', borderRadius: 8,
                  backgroundColor: 'var(--color-surface-el)',
                  border: `1px solid ${CATEGORY_COLORS[m.category] || 'var(--color-border)'}33`,
                }}>
                  <div style={{ fontSize: 9, fontWeight: 600, color: CATEGORY_COLORS[m.category] || '#71717a', marginBottom: 2, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
                    Group {String.fromCharCode(65 + idx)}: {m.category}
                  </div>
                  <div style={{ fontSize: 10, fontWeight: 700, color: 'var(--color-text)', lineHeight: 1.3 }}>
                    {m.title}
                  </div>
                  <div style={{ fontSize: 9, color: '#22c55e', fontWeight: 600, marginTop: 2 }}>
                    {Math.round((m.yes_price || 0) * 100)}% probability
                  </div>
                </div>
              ))}

              {/* Tone selector */}
              <div>
                <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--color-text-secondary)', marginBottom: 4 }}>Tone</div>
                <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
                  {['formal', 'urgent', 'passionate', 'grassroots'].map(t => (
                    <button key={t} onClick={() => setTone(t)}
                      style={{
                        padding: '3px 10px', borderRadius: 99, border: 'none',
                        fontSize: 10, fontWeight: 600, cursor: 'pointer',
                        backgroundColor: tone === t ? 'rgba(245, 158, 11, 0.2)' : 'var(--color-surface-el)',
                        color: tone === t ? '#f59e0b' : 'var(--color-text-muted)',
                        textTransform: 'capitalize', transition: 'all 150ms ease',
                      }}
                    >{t}</button>
                  ))}
                </div>
              </div>

              <button
                onClick={handleAllianceGenerate}
                disabled={generating}
                style={{
                  display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
                  padding: '8px 12px', borderRadius: 8, border: 'none',
                  background: 'linear-gradient(135deg, #f59e0b, #d97706)',
                  color: '#000', fontSize: 11, fontWeight: 700,
                  cursor: generating ? 'wait' : 'pointer',
                  opacity: generating ? 0.7 : 1,
                  transition: 'opacity 150ms ease',
                }}
              >
                {generating
                  ? <><Loader2 size={13} style={{ animation: 'spin 1s linear infinite' }} /> Generating Alliance...</>
                  : <><Zap size={13} /> Generate Alliance Petition</>
                }
              </button>
            </div>
          )}

          {/* Single market selected — Generate petition */}
          {selectedMarkets.length >= 1 && !isAllianceMode && !generated && (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
              <div style={{
                padding: '10px 12px', borderRadius: 8,
                backgroundColor: 'var(--color-surface-el)',
                border: '1px solid var(--color-border)',
              }}>
                <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--color-text-muted)', marginBottom: 4, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
                  Selected Market
                </div>
                <div style={{ fontSize: 11, fontWeight: 700, color: 'var(--color-text)', marginBottom: 6, lineHeight: 1.3 }}>
                  {selectedMarket!.title}
                </div>
                <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
                  <span style={{ fontSize: 9, color: '#22c55e', fontWeight: 700 }}>
                    Yes: {Math.round((selectedMarket!.yes_price || 0) * 100)}%
                  </span>
                  <span style={{ fontSize: 9, color: '#ef4444', fontWeight: 700 }}>
                    No: {Math.round((selectedMarket!.no_price || 0) * 100)}%
                  </span>
                  <span style={{ fontSize: 9, color: 'var(--color-text-muted)' }}>
                    Vol: {((selectedMarket!.volume || 0) / 1000).toFixed(0)}k
                  </span>
                </div>
              </div>

              {/* Tone selector */}
              <div>
                <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--color-text-secondary)', marginBottom: 4 }}>Tone</div>
                <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
                  {['formal', 'urgent', 'passionate', 'grassroots'].map(t => (
                    <button key={t} onClick={() => setTone(t)}
                      style={{
                        padding: '3px 10px', borderRadius: 99, border: 'none',
                        fontSize: 10, fontWeight: 600, cursor: 'pointer',
                        backgroundColor: tone === t ? 'rgba(124, 58, 237, 0.2)' : 'var(--color-surface-el)',
                        color: tone === t ? '#a78bfa' : 'var(--color-text-muted)',
                        textTransform: 'capitalize', transition: 'all 150ms ease',
                      }}
                    >{t}</button>
                  ))}
                </div>
              </div>

              <button
                onClick={handleGenerate}
                disabled={generating}
                style={{
                  display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
                  padding: '8px 12px', borderRadius: 8, border: 'none',
                  background: 'linear-gradient(135deg, #7c3aed, #6d28d9)',
                  color: '#fff', fontSize: 11, fontWeight: 700,
                  cursor: generating ? 'wait' : 'pointer',
                  opacity: generating ? 0.7 : 1,
                  transition: 'opacity 150ms ease',
                }}
              >
                {generating
                  ? <><Loader2 size={13} style={{ animation: 'spin 1s linear infinite' }} /> Generating...</>
                  : <><Sparkles size={13} /> Generate Petition</>
                }
              </button>
            </div>
          )}

          {/* Generated petition preview */}
          {generated && (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
              <div style={{
                padding: '10px 12px', borderRadius: 8,
                backgroundColor: 'var(--color-surface-el)',
                border: `1px solid ${generated.alliance_insight ? 'rgba(245, 158, 11, 0.3)' : 'rgba(124, 58, 237, 0.2)'}`,
              }}>
                <div style={{ fontSize: 10, fontWeight: 600, color: generated.alliance_insight ? '#f59e0b' : '#a78bfa', marginBottom: 4, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
                  {generated.alliance_insight ? 'Alliance Petition' : 'Generated Preview'}
                </div>
                <div style={{ fontSize: 11, fontWeight: 700, color: 'var(--color-text)', marginBottom: 4, lineHeight: 1.3 }}>
                  {generated.title}
                </div>
                <div style={{ fontSize: 10, color: 'var(--color-text-muted)', lineHeight: 1.4, marginBottom: 6 }}>
                  {generated.summary}
                </div>
                <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
                  {(generated.suggested_tags || []).map(tag => (
                    <span key={tag} style={{
                      fontSize: 8, padding: '1px 6px', borderRadius: 99,
                      backgroundColor: generated.alliance_insight ? 'rgba(245, 158, 11, 0.1)' : 'rgba(124, 58, 237, 0.1)',
                      color: generated.alliance_insight ? '#f59e0b' : '#a78bfa',
                    }}>
                      {tag}
                    </span>
                  ))}
                </div>
              </div>

              {/* Alliance insight blocks */}
              {generated.alliance_insight && (
                <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                  <div style={{
                    padding: '8px 10px', borderRadius: 8,
                    backgroundColor: 'rgba(245, 158, 11, 0.06)',
                    border: '1px solid rgba(245, 158, 11, 0.15)',
                  }}>
                    <div style={{ fontSize: 9, fontWeight: 700, color: '#f59e0b', marginBottom: 3, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
                      Why This Alliance Works
                    </div>
                    <div style={{ fontSize: 10, color: 'var(--color-text-muted)', lineHeight: 1.4 }}>
                      {generated.alliance_insight}
                    </div>
                  </div>
                  <div style={{ display: 'flex', gap: 6 }}>
                    {generated.group_a_appeal && (
                      <div style={{ flex: 1, padding: '6px 8px', borderRadius: 6, backgroundColor: 'var(--color-surface-el)', border: '1px solid var(--color-border)' }}>
                        <div style={{ fontSize: 8, fontWeight: 700, color: CATEGORY_COLORS[selectedMarkets[0]?.category] || '#71717a', marginBottom: 2 }}>GROUP A</div>
                        <div style={{ fontSize: 9, color: 'var(--color-text-muted)', lineHeight: 1.3 }}>{generated.group_a_appeal}</div>
                      </div>
                    )}
                    {generated.group_b_appeal && (
                      <div style={{ flex: 1, padding: '6px 8px', borderRadius: 6, backgroundColor: 'var(--color-surface-el)', border: '1px solid var(--color-border)' }}>
                        <div style={{ fontSize: 8, fontWeight: 700, color: CATEGORY_COLORS[selectedMarkets[1]?.category] || '#71717a', marginBottom: 2 }}>GROUP B</div>
                        <div style={{ fontSize: 9, color: 'var(--color-text-muted)', lineHeight: 1.3 }}>{generated.group_b_appeal}</div>
                      </div>
                    )}
                  </div>
                  {generated.impact_multiplier && (
                    <div style={{
                      padding: '6px 10px', borderRadius: 6,
                      backgroundColor: 'rgba(34, 197, 94, 0.06)',
                      border: '1px solid rgba(34, 197, 94, 0.15)',
                    }}>
                      <div style={{ fontSize: 9, fontWeight: 700, color: '#22c55e', marginBottom: 2 }}>IMPACT MULTIPLIER</div>
                      <div style={{ fontSize: 9, color: 'var(--color-text-muted)', lineHeight: 1.3 }}>{generated.impact_multiplier}</div>
                    </div>
                  )}
                </div>
              )}

              {/* Save / Regenerate / Activate */}
              <div style={{ display: 'flex', gap: 6 }}>
                <button
                  onClick={handleSaveDraft}
                  disabled={saving}
                  style={{
                    flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4,
                    padding: '7px 10px', borderRadius: 8, border: 'none',
                    background: saving ? '#555' : 'linear-gradient(135deg, #7c3aed, #6d28d9)',
                    color: '#fff', fontSize: 10, fontWeight: 700,
                    cursor: saving ? 'wait' : 'pointer',
                    opacity: saving ? 0.7 : 1,
                    transition: 'all 150ms ease',
                  }}
                >
                  {saving
                    ? <><Loader2 size={12} style={{ animation: 'spin 1s linear infinite' }} /> Saving...</>
                    : <><FileText size={12} /> Save as Draft</>
                  }
                </button>
                <button
                  onClick={() => setGenerated(null)}
                  style={{
                    padding: '7px 10px', borderRadius: 8,
                    border: '1px solid var(--color-border)',
                    backgroundColor: 'transparent', color: 'var(--color-text-secondary)',
                    fontSize: 10, fontWeight: 600, cursor: 'pointer',
                    transition: 'all 150ms ease',
                  }}
                  onMouseEnter={e => { e.currentTarget.style.backgroundColor = 'var(--color-surface-el)'; }}
                  onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'transparent'; }}
                >
                  Redo
                </button>
              </div>

              {/* Cross-Platform Posting */}
              <div>
                <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--color-text-secondary)', marginBottom: 6, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
                  Cross-Platform Post
                </div>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                  {Object.entries(PLATFORMS).map(([key, platform]) => (
                    <button
                      key={key}
                      onClick={() => handleCopyAndOpen(key)}
                      style={{
                        display: 'flex', alignItems: 'center', gap: 8,
                        padding: '7px 10px', borderRadius: 8,
                        border: '1px solid var(--color-border)',
                        backgroundColor: 'transparent', color: 'var(--color-text)',
                        fontSize: 10, fontWeight: 600, cursor: 'pointer',
                        transition: 'all 150ms ease', textAlign: 'left',
                      }}
                      onMouseEnter={e => { e.currentTarget.style.backgroundColor = 'var(--color-surface-el)'; e.currentTarget.style.borderColor = platform.color; }}
                      onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'transparent'; e.currentTarget.style.borderColor = 'var(--color-border)'; }}
                    >
                      <div style={{
                        width: 22, height: 22, borderRadius: 6, flexShrink: 0,
                        backgroundColor: `${platform.color}20`,
                        display: 'flex', alignItems: 'center', justifyContent: 'center',
                      }}>
                        <ExternalLink size={11} style={{ color: platform.color }} />
                      </div>
                      <span style={{ flex: 1 }}>{platform.name}</span>
                      {copiedPlatform === key
                        ? <Check size={12} style={{ color: '#22c55e' }} />
                        : <Copy size={12} style={{ color: 'var(--color-text-muted)' }} />
                      }
                    </button>
                  ))}
                </div>
              </div>

              {/* Email Campaign Link */}
              <button
                onClick={async () => {
                  if (!generated) return;
                  // Save petition first, then redirect to campaigns
                  setSaving(true);
                  try {
                    const res = await fetch('/api/petitions', {
                      method: 'POST',
                      headers: { 'Content-Type': 'application/json' },
                      body: JSON.stringify({
                        title: generated.title,
                        summary: generated.summary,
                        body_html: generated.body_html || `<p>${generated.summary}</p>`,
                        body_text: generated.body_text || generated.summary,
                        target: generated.suggested_target,
                        tags: generated.suggested_tags,
                        signature_goal: generated.suggested_goal || 10000,
                        status: 'active',
                      }),
                    });
                    if (res.ok) {
                      showToast('Petition saved & ready for campaign!', 'success');
                      // Navigate to campaigns tab after short delay
                      setTimeout(() => {
                        const event = new CustomEvent('patty-navigate', { detail: { tab: 'campaigns' } });
                        window.dispatchEvent(event);
                      }, 500);
                    } else {
                      showToast('Failed to save petition for campaign', 'error');
                    }
                  } catch {
                    showToast('Network error', 'error');
                  } finally {
                    setSaving(false);
                  }
                }}
                disabled={saving}
                style={{
                  display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
                  padding: '7px 10px', borderRadius: 8,
                  border: '1px solid rgba(124, 58, 237, 0.3)',
                  backgroundColor: 'rgba(124, 58, 237, 0.08)',
                  color: '#a78bfa', fontSize: 10, fontWeight: 700, cursor: 'pointer',
                  transition: 'all 150ms ease',
                }}
                onMouseEnter={e => { e.currentTarget.style.backgroundColor = 'rgba(124, 58, 237, 0.15)'; }}
                onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'rgba(124, 58, 237, 0.08)'; }}
              >
                <Mail size={12} /> Create Email Campaign
              </button>
            </div>
          )}

          {/* Petition selected — show linked markets + cross-post */}
          {selectedPetition && selectedMarkets.length === 0 && !generated && (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
              <div style={{
                padding: '10px 12px', borderRadius: 8,
                backgroundColor: 'var(--color-surface-el)',
                border: '1px solid var(--color-border)',
              }}>
                <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--color-text-muted)', marginBottom: 4, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
                  Selected Petition
                </div>
                <div style={{ fontSize: 11, fontWeight: 700, color: 'var(--color-text)', marginBottom: 4, lineHeight: 1.3 }}>
                  {selectedPetition.title}
                </div>
                <div style={{ fontSize: 10, color: 'var(--color-text-muted)', lineHeight: 1.4, marginBottom: 6 }}>
                  {selectedPetition.summary}
                </div>
                <div style={{ display: 'flex', gap: 8 }}>
                  <span style={{
                    fontSize: 9, padding: '2px 6px', borderRadius: 4,
                    backgroundColor: STATUS_STYLES[selectedPetition.status]?.bg,
                    color: STATUS_STYLES[selectedPetition.status]?.text,
                    fontWeight: 600, textTransform: 'capitalize',
                  }}>
                    {selectedPetition.status}
                  </span>
                  <span style={{ fontSize: 9, color: 'var(--color-text-muted)' }}>
                    {(selectedPetition.signature_count ?? 0).toLocaleString()} / {(selectedPetition.signature_goal ?? 1).toLocaleString()} sigs
                  </span>
                </div>
              </div>

              {/* Cross-Platform Posting for existing petition */}
              <div>
                <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--color-text-secondary)', marginBottom: 6, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
                  Post to Platform
                </div>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                  {Object.entries(PLATFORMS).map(([key, platform]) => (
                    <button
                      key={key}
                      onClick={() => {
                        const text = `${selectedPetition.title}\n\n${selectedPetition.summary}`;
                        navigator.clipboard.writeText(text).then(() => {
                          setCopiedPlatform(key);
                          setTimeout(() => setCopiedPlatform(null), 2000);
                          window.open(platform.url, '_blank');
                        });
                      }}
                      style={{
                        display: 'flex', alignItems: 'center', gap: 8,
                        padding: '7px 10px', borderRadius: 8,
                        border: '1px solid var(--color-border)',
                        backgroundColor: 'transparent', color: 'var(--color-text)',
                        fontSize: 10, fontWeight: 600, cursor: 'pointer',
                        transition: 'all 150ms ease', textAlign: 'left',
                      }}
                      onMouseEnter={e => { e.currentTarget.style.backgroundColor = 'var(--color-surface-el)'; }}
                      onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'transparent'; }}
                    >
                      <div style={{
                        width: 22, height: 22, borderRadius: 6, flexShrink: 0,
                        backgroundColor: `${platform.color}20`,
                        display: 'flex', alignItems: 'center', justifyContent: 'center',
                      }}>
                        <ExternalLink size={11} style={{ color: platform.color }} />
                      </div>
                      <span style={{ flex: 1 }}>Copy & Open {platform.name}</span>
                      {copiedPlatform === key
                        ? <Check size={12} style={{ color: '#22c55e' }} />
                        : <Copy size={12} style={{ color: 'var(--color-text-muted)' }} />
                      }
                    </button>
                  ))}
                </div>
              </div>

              {/* Activate / Archive buttons */}
              <div style={{ display: 'flex', gap: 6 }}>
                {selectedPetition.status === 'draft' && (
                  <button
                    onClick={() => handleActivatePetition(selectedPetition.id)}
                    style={{
                      flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4,
                      padding: '7px 10px', borderRadius: 8, border: 'none',
                      background: 'linear-gradient(135deg, #22c55e, #16a34a)',
                      color: '#fff', fontSize: 10, fontWeight: 700, cursor: 'pointer',
                      transition: 'all 150ms ease',
                    }}
                  >
                    <Zap size={11} /> Activate
                  </button>
                )}
                <button
                  onClick={() => handleArchivePetition(selectedPetition.id)}
                  style={{
                    padding: '7px 10px', borderRadius: 8,
                    border: '1px solid rgba(239, 68, 68, 0.3)',
                    backgroundColor: 'rgba(239, 68, 68, 0.06)',
                    color: '#ef4444', fontSize: 10, fontWeight: 600, cursor: 'pointer',
                    transition: 'all 150ms ease',
                  }}
                  onMouseEnter={e => { e.currentTarget.style.backgroundColor = 'rgba(239, 68, 68, 0.12)'; }}
                  onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'rgba(239, 68, 68, 0.06)'; }}
                >
                  <X size={11} /> Archive
                </button>
              </div>

              <button
                onClick={() => {
                  showToast('Opening campaign builder...', 'info');
                  const event = new CustomEvent('patty-navigate', { detail: { tab: 'campaigns' } });
                  window.dispatchEvent(event);
                }}
                style={{
                  display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
                  padding: '7px 10px', borderRadius: 8,
                  border: '1px solid rgba(124, 58, 237, 0.3)',
                  backgroundColor: 'rgba(124, 58, 237, 0.08)',
                  color: '#a78bfa', fontSize: 10, fontWeight: 700, cursor: 'pointer',
                  transition: 'all 150ms ease',
                }}
                onMouseEnter={e => { e.currentTarget.style.backgroundColor = 'rgba(124, 58, 237, 0.15)'; }}
                onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'rgba(124, 58, 237, 0.08)'; }}
              >
                <Mail size={12} /> Create Campaign from Petition
              </button>

              {/* Share Link */}
              <button
                onClick={() => {
                  const shareUrl = `${window.location.origin}/petition/${selectedPetition.id}`;
                  const doCopy = () => {
                    if (navigator.clipboard?.writeText) return navigator.clipboard.writeText(shareUrl);
                    const ta = document.createElement('textarea');
                    ta.value = shareUrl;
                    ta.style.position = 'fixed';
                    ta.style.left = '-9999px';
                    document.body.appendChild(ta);
                    ta.select();
                    document.execCommand('copy');
                    document.body.removeChild(ta);
                    return Promise.resolve();
                  };
                  doCopy().then(() => showToast('Petition link copied!', 'success'))
                    .catch(() => showToast('Failed to copy link', 'error'));
                }}
                style={{
                  display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
                  padding: '7px 10px', borderRadius: 8,
                  border: '1px solid var(--color-border)',
                  backgroundColor: 'transparent',
                  color: 'var(--color-text-secondary)', fontSize: 10, fontWeight: 600, cursor: 'pointer',
                  transition: 'all 150ms ease',
                }}
                onMouseEnter={e => { e.currentTarget.style.backgroundColor = 'var(--color-surface-el)'; }}
                onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'transparent'; }}
              >
                <Link2 size={11} /> Share Petition Link
              </button>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

/* ═══════════════════════════════════════════════════════════════════════════
   Main OrbitTab Component
   ═══════════════════════════════════════════════════════════════════════════ */

interface OrbitTabProps {
  pulseTopic?: string | null;
  onPulseTopicConsumed?: () => void;
}

export default function OrbitTab({ pulseTopic, onPulseTopicConsumed }: OrbitTabProps = {}) {
  /* ── Data state ───────────────────────────────────────────────────────── */
  const [markets, setMarkets] = useState<KalshiMarket[]>(MOCK_MARKETS);
  const [petitions, setPetitions] = useState<Petition[]>(MOCK_PETITIONS);
  const [links, setLinks] = useState<OrbitLink[]>(MOCK_LINKS);
  const [articles, setArticles] = useState<RSSArticle[]>(MOCK_ARTICLES);
  const [stats, setStats] = useState<OrbitStats>(MOCK_STATS);
  const [trendingTopics, setTrendingTopics] = useState<TrendingTopic[]>([]);
  const [agents, setAgents] = useState<AgentInfo[]>([]);
  const [loading, setLoading] = useState(false);
  const [rssLoading, setRssLoading] = useState(false);
  const [pulseGenerating, setPulseGenerating] = useState(false);
  const [pulseResult, setPulseResult] = useState<{ petition: any; influence_analysis: string } | null>(null);
  const [refreshing, setRefreshing] = useState(false);
  const [usingMock, setUsingMock] = useState(true);

  /* ── Interaction state ────────────────────────────────────────────────── */
  const [hoveredMarket, setHoveredMarket] = useState<string | null>(null);
  const [hoveredPetition, setHoveredPetition] = useState<string | null>(null);
  const [selectedMarkets, setSelectedMarkets] = useState<string[]>([]);
  const [selectedPetition, setSelectedPetition] = useState<string | null>(null);

  // Market selection handler: normal click = single select, shift = multi (max 2)
  const handleSelectMarket = useCallback((id: string | null, shift?: boolean) => {
    if (!id) {
      setSelectedMarkets([]);
      return;
    }
    setSelectedMarkets(prev => {
      // Deselect if already selected
      if (prev.includes(id)) return prev.filter(x => x !== id);
      // Shift+click: add to selection (max 2)
      if (shift && prev.length < 2) return [...prev, id];
      if (shift && prev.length >= 2) return [prev[1], id]; // drop oldest
      // Normal click: replace
      return [id];
    });
    setSelectedPetition(null);
  }, []);

  /* ── Fetch orbit data ─────────────────────────────────────────────────── */
  const fetchOrbitData = useCallback(async () => {
    setLoading(true);
    try {
      const res = await fetch('/api/orbit');
      if (res.ok) {
        const data = await res.json();
        if (data.markets?.length > 0) {
          // Map DB fields to client-expected fields
          const mappedMarkets = data.markets.map((m: any) => {
            // DB stores prices as integers 0-100; client expects floats 0-1
            const rawYes = m.yes_price ?? m.last_price ?? m.yes_bid ?? 0;
            const yesPrice = rawYes > 1 ? rawYes / 100 : rawYes;
            const rawNo = m.no_price ?? m.no_bid ?? (rawYes != null ? (rawYes > 1 ? 100 : 1) - rawYes : 0);
            const noPrice = rawNo > 1 ? rawNo / 100 : rawNo;
            return {
              ...m,
              yes_price: yesPrice,
              no_price: noPrice,
              volume: m.volume ?? 0,
              category: m.category || 'Other',
            };
          });
          setMarkets(mappedMarkets);
          setPetitions(data.petitions || []);
          setLinks(data.links || []);
          if (data.trending?.length > 0) setTrendingTopics(data.trending);
          setStats({
            ...MOCK_STATS,
            ...data.stats,
            articles_today: data.stats?.articles_today ?? data.stats?.recent_articles ?? MOCK_STATS.articles_today,
            avg_sentiment: data.stats?.avg_sentiment ?? MOCK_STATS.avg_sentiment,
          });
          setUsingMock(false);
        }
      }
    } catch {
      // Keep mock data on failure
    } finally {
      setLoading(false);
    }
  }, []);

  /* ── Fetch RSS data ───────────────────────────────────────────────────── */
  const fetchRSSData = useCallback(async () => {
    setRssLoading(true);
    try {
      const res = await fetch('/api/orbit/rss');
      if (res.ok) {
        const data = await res.json();
        if (data.articles?.length > 0) {
          setArticles(data.articles);
          if (data.stats) {
            setStats(prev => ({
              ...prev,
              active_feeds: data.stats.active_feeds ?? prev.active_feeds,
              articles_today: data.stats.articles_today ?? prev.articles_today,
              avg_sentiment: data.stats.avg_sentiment ?? prev.avg_sentiment,
            }));
          }
          setUsingMock(false);
        }
      }
    } catch {
      // Keep mock data
    } finally {
      setRssLoading(false);
    }
  }, []);

  /* ── Fetch agent status ──────────────────────────────────────────────── */
  const fetchAgentStatus = useCallback(async () => {
    try {
      const res = await fetch('/api/orbit/agents');
      if (res.ok) {
        const data = await res.json();
        setAgents(data.agents || []);
      }
    } catch { /* ignore */ }
  }, []);

  const handleRefreshFeeds = useCallback(async () => {
    setRefreshing(true);
    await fetchRSSData();
    setRefreshing(false);
  }, [fetchRSSData]);

  useEffect(() => {
    fetchOrbitData();
    fetchRSSData();
    fetchAgentStatus();
  }, [fetchOrbitData, fetchRSSData, fetchAgentStatus]);

  /* ── Auto-generate petition from Pulse topic ───────────────────────── */
  useEffect(() => {
    if (!pulseTopic) return;
    setPulseGenerating(true);
    setPulseResult(null);
    fetch('/api/orbit/generate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ topic_title: pulseTopic, tone: 'urgent' }),
    })
      .then(res => res.ok ? res.json() : Promise.reject(res))
      .then(data => {
        setPulseResult({ petition: data.petition, influence_analysis: data.influence_analysis || '' });
        // Refresh data to show new petition in mind map
        fetchOrbitData();
      })
      .catch(err => {
        console.error('[OrbitTab] Pulse petition generation failed:', err);
        setPulseResult(null);
      })
      .finally(() => {
        setPulseGenerating(false);
        onPulseTopicConsumed?.();
      });
  }, [pulseTopic, onPulseTopicConsumed, fetchOrbitData]);

  /* ── Derived selection objects ─────────────────────────────────────────── */
  const selectedMarketObjs = selectedMarkets.map(id => markets.find(m => m.id === id)).filter(Boolean) as KalshiMarket[];
  const selectedPetitionObj = selectedPetition ? petitions.find(p => p.id === selectedPetition) || null : null;

  const sentimentInfo = sentimentLabel(stats.avg_sentiment);

  /* ─── Render ──────────────────────────────────────────────────────────── */
  return (
    <div style={{ padding: 16, height: '100%', display: 'flex', flexDirection: 'column', gap: 12, overflow: 'hidden' }}>

      {/* ── Stats Bar ────────────────────────────────────────────────────── */}
      <div style={{
        display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap',
        padding: '10px 16px', borderRadius: 12,
        background: 'linear-gradient(135deg, var(--color-surface), var(--color-surface-el))',
        border: '1px solid var(--color-border)',
        flexShrink: 0,
      }}>
        {usingMock && (
          <div style={{
            display: 'flex', alignItems: 'center', gap: 4,
            padding: '3px 10px', borderRadius: 99,
            backgroundColor: 'rgba(245, 158, 11, 0.15)',
            border: '1px solid rgba(245, 158, 11, 0.3)',
          }}>
            <AlertCircle size={11} style={{ color: '#f59e0b' }} />
            <span style={{ fontSize: 9, fontWeight: 600, color: '#f59e0b' }}>Demo Data</span>
          </div>
        )}
        <StatPill icon={<BarChart3 size={12} />} label="Markets" value={stats.total_markets} color="#3b82f6" />
        <StatPill icon={<Link2 size={12} />} label="Linked" value={stats.linked_petitions} color="#a78bfa" />
        <StatPill icon={<Rss size={12} />} label="Feeds" value={stats.active_feeds} color="#22c55e" />
        <StatPill icon={<Newspaper size={12} />} label="Today" value={stats.articles_today} color="#06b6d4" />
        <StatPill icon={<TrendingUp size={12} />} label="Trending" value={trendingTopics.length} color={TRENDING_COLOR} />
        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <span style={{ fontSize: 10, color: 'var(--color-text-muted)', fontWeight: 600 }}>Sentiment</span>
          <span style={{
            fontSize: 10, fontWeight: 700, padding: '2px 8px', borderRadius: 99,
            backgroundColor: `${sentimentInfo.color}20`, color: sentimentInfo.color,
          }}>
            {sentimentInfo.label} ({(stats.avg_sentiment ?? 0).toFixed(2)})
          </span>
        </div>
        {/* Agent Status Indicators */}
        {agents.length > 0 && (
          <div style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '2px 8px', borderRadius: 99,
            backgroundColor: 'rgba(124,58,237,0.08)', border: '1px solid rgba(124,58,237,0.15)' }}
            title={agents.map(a => `${a.name}: ${a.status}`).join(', ')}
          >
            <Activity size={10} style={{ color: '#a78bfa' }} />
            <span style={{ fontSize: 9, fontWeight: 600, color: 'var(--color-text-muted)' }}>Agents</span>
            {agents.map(a => (
              <div key={a.name} title={`${a.name} (${a.role}) — ${a.status}`}
                style={{
                  width: 7, height: 7, borderRadius: '50%',
                  backgroundColor: a.status === 'online' ? '#22c55e' : a.status === 'error' ? '#f59e0b' : '#ef4444',
                  border: '1px solid rgba(255,255,255,0.15)',
                  transition: 'background-color 300ms ease',
                }}
              />
            ))}
            <span style={{ fontSize: 9, fontWeight: 700, color: '#22c55e' }}>
              {agents.filter(a => a.status === 'online').length}/{agents.length}
            </span>
          </div>
        )}

        <div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 4 }}>
          {loading && <Loader2 size={13} style={{ color: 'var(--color-text-muted)', animation: 'spin 1s linear infinite' }} />}
          <button
            onClick={() => { fetchOrbitData(); fetchRSSData(); fetchAgentStatus(); }}
            style={{
              display: 'flex', alignItems: 'center', gap: 4, padding: '4px 10px',
              borderRadius: 6, border: '1px solid var(--color-border)',
              backgroundColor: 'transparent', color: 'var(--color-text-secondary)',
              cursor: 'pointer', fontSize: 10, fontWeight: 600,
              transition: 'all 150ms ease',
            }}
            onMouseEnter={e => { e.currentTarget.style.backgroundColor = 'var(--color-surface-el)'; }}
            onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'transparent'; }}
          >
            <RefreshCw size={11} /> Sync
          </button>
        </div>
      </div>

      {/* ── Pulse Topic Banner ──────────────────────────────────────────── */}
      {(pulseGenerating || pulseResult) && (
        <div style={{
          display: 'flex', alignItems: 'center', gap: 10,
          padding: '10px 16px', borderRadius: 10,
          background: pulseGenerating
            ? 'linear-gradient(135deg, rgba(124,58,237,0.15), rgba(59,130,246,0.15))'
            : 'linear-gradient(135deg, rgba(34,197,94,0.12), rgba(124,58,237,0.12))',
          border: `1px solid ${pulseGenerating ? 'rgba(124,58,237,0.3)' : 'rgba(34,197,94,0.3)'}`,
          flexShrink: 0,
        }}>
          {pulseGenerating ? (
            <>
              <Loader2 size={16} style={{ color: '#a78bfa', animation: 'spin 1s linear infinite' }} />
              <span style={{ fontSize: 12, fontWeight: 600, color: '#a78bfa' }}>
                Generating petition from Pulse topic...
              </span>
            </>
          ) : pulseResult ? (
            <>
              <Sparkles size={16} style={{ color: '#22c55e' }} />
              <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text)' }}>
                Petition created: &ldquo;{pulseResult.petition?.title}&rdquo;
              </span>
              <span style={{ fontSize: 10, color: 'var(--color-text-muted)', marginLeft: 4 }}>
                {pulseResult.influence_analysis?.slice(0, 120)}...
              </span>
              <button
                onClick={() => setPulseResult(null)}
                style={{
                  marginLeft: 'auto', padding: '2px 8px', borderRadius: 4,
                  border: '1px solid var(--color-border)', backgroundColor: 'transparent',
                  color: 'var(--color-text-muted)', fontSize: 10, cursor: 'pointer',
                }}
              >
                <X size={12} />
              </button>
            </>
          ) : null}
        </div>
      )}

      {/* ── Main Layout: Mind Map | Right Panels ─────────────────────────── */}
      <div style={{ flex: 1, display: 'flex', gap: 12, minHeight: 0 }}>
        {/* Mind Map (left, 60%) */}
        <div style={{
          flex: '0 0 60%', minWidth: 0,
          height: 'calc(100vh - 140px)',
          backgroundColor: 'var(--color-surface)',
          border: '1px solid var(--color-border)',
          borderRadius: 12, overflow: 'hidden', position: 'relative',
        }}>
          {loading && !usingMock ? (
            <div style={{
              display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%',
              flexDirection: 'column', gap: 8,
            }}>
              <Loader2 size={24} style={{ color: '#a78bfa', animation: 'spin 1s linear infinite' }} />
              <span style={{ fontSize: 11, color: 'var(--color-text-muted)' }}>Loading orbit data...</span>
            </div>
          ) : (
            <MindMap
              markets={markets.slice(0, 14)}
              petitions={petitions.slice(0, 6)}
              links={links}
              trendingTopics={trendingTopics.slice(0, 6)}
              hoveredMarket={hoveredMarket}
              hoveredPetition={hoveredPetition}
              selectedMarkets={selectedMarkets}
              selectedPetition={selectedPetition}
              onHoverMarket={setHoveredMarket}
              onHoverPetition={setHoveredPetition}
              onSelectMarket={handleSelectMarket}
              onSelectPetition={(id) => { setSelectedPetition(id); if (id) setSelectedMarkets([]); }}
              onSelectTrending={(id) => {
                // When a trending topic is clicked, auto-generate a petition from it
                if (id) {
                  const topic = trendingTopics.find(t => t.id === id);
                  if (topic) {
                    window.dispatchEvent(new CustomEvent('patty-navigate', { detail: { tab: 'trending' } }));
                  }
                }
              }}
            />
          )}

          {/* Watermark */}
          <div style={{
            position: 'absolute', bottom: 8, right: 12,
            fontSize: 9, color: 'var(--color-text-muted)', opacity: 0.5,
            fontWeight: 600, letterSpacing: '0.04em',
          }}>
            ORBIT MIND MAP
          </div>
        </div>

        {/* Right panels (40%, stacked) */}
        <div style={{ flex: '0 0 40%', minWidth: 0, display: 'flex', flexDirection: 'column', gap: 12, height: 'calc(100vh - 140px)' }}>
          {/* Pulse panel (top 55%) */}
          <div style={{ flex: '0 0 55%', minHeight: 0 }}>
            <PulsePanel
              articles={articles}
              loading={rssLoading}
              onRefresh={handleRefreshFeeds}
              refreshing={refreshing}
            />
          </div>

          {/* Actions panel (bottom 45%) */}
          <div style={{ flex: 1, minHeight: 0 }}>
            <ActionsPanel
              selectedMarkets={selectedMarketObjs}
              selectedPetition={selectedPetitionObj}
              onClearSelection={() => { setSelectedMarkets([]); setSelectedPetition(null); }}
              onRefresh={fetchOrbitData}
            />
          </div>
        </div>
      </div>

      {/* Animation keyframes */}
      <style>{`
        @keyframes spin {
          from { transform: rotate(0deg); }
          to { transform: rotate(360deg); }
        }
        @keyframes fadeInUp {
          from { opacity: 0; transform: translateX(-50%) translateY(8px); }
          to { opacity: 1; transform: translateX(-50%) translateY(0); }
        }
      `}</style>
    </div>
  );
}

/* ═══════════════════════════════════════════════════════════════════════════
   StatPill Utility
   ═══════════════════════════════════════════════════════════════════════════ */

function StatPill({ icon, label, value, color }: { icon: React.ReactNode; label: string; value: number; color: string }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
      <div style={{ color, display: 'flex', alignItems: 'center' }}>{icon}</div>
      <span style={{ fontSize: 10, color: 'var(--color-text-muted)', fontWeight: 600 }}>{label}</span>
      <span style={{ fontSize: 11, fontWeight: 800, color: 'var(--color-text)' }}>{value}</span>
    </div>
  );
}