const { useState, useMemo, useRef, useEffect } = React;

function VotingAnalysis() {
    const [numCandidates, setNumCandidates] = useState(3);
    // 1D positions
    const [c1, setC1] = useState(0.2);
    const [c2, setC2] = useState(0.5);
    const [c3, setC3] = useState(0.8);
    const [c4, setC4] = useState(0.95);
    // 2D positions (x, y)
    const [c1_2d, setC1_2d] = useState({ x: 0.2, y: 0.3 });
    const [c2_2d, setC2_2d] = useState({ x: 0.5, y: 0.7 });
    const [c3_2d, setC3_2d] = useState({ x: 0.8, y: 0.4 });
    const [c4_2d, setC4_2d] = useState({ x: 0.3, y: 0.8 });
    // Dimension mode: '1d' or '2d'
    const [dimensionMode, setDimensionMode] = useState('1d');
    const [dragging, setDragging] = useState(null);
    const svgRef = useRef(null);
    const svg2dRef = useRef(null);
    const hasInitializedRef = useRef(false);

    // Custom candidate labels
    const [label1, setLabel1] = useState('A');
    const [label2, setLabel2] = useState('B');
    const [label3, setLabel3] = useState('C');
    const [label4, setLabel4] = useState('D');

    // RCV max ranks allowed (for ballot exhaustion)
    const [maxRanksAllowed, setMaxRanksAllowed] = useState(4);

    // --- URL params handling: read initial params, respond to back/forward, and keep URL updated ---
    const parseAndApplyUrl = (replaceValues = true) => {
        try {
            const params = new URLSearchParams(window.location.search);

            const pn = params.has('n') ? parseInt(params.get('n')) : null;
            const p1 = params.has('c1') ? parseFloat(params.get('c1')) : null;
            const p2 = params.has('c2') ? parseFloat(params.get('c2')) : null;
            const p3 = params.has('c3') ? parseFloat(params.get('c3')) : null;
            const p4 = params.has('c4') ? parseFloat(params.get('c4')) : null;

            // 2D positions
            const dim = params.get('dim');
            const parse2d = (key) => {
                const val = params.get(key);
                if (!val) return null;
                const parts = val.split(',');
                if (parts.length !== 2) return null;
                const x = parseFloat(parts[0]);
                const y = parseFloat(parts[1]);
                if (Number.isNaN(x) || Number.isNaN(y)) return null;
                return { x: Math.max(0, Math.min(1, x)), y: Math.max(0, Math.min(1, y)) };
            };
            const p1_2d = parse2d('c1_2d');
            const p2_2d = parse2d('c2_2d');
            const p3_2d = parse2d('c3_2d');
            const p4_2d = parse2d('c4_2d');

            const l1 = params.get('l1');
            const l2 = params.get('l2');
            const l3 = params.get('l3');
            const l4 = params.get('l4');

            const st = params.has('st') ? parseFloat(params.get('st')) : null;
            const bs = params.has('bs') ? (params.get('bs') === '1' || params.get('bs') === 'true') : null;
            const strat = params.get('strat'); // Strategy type: 'threshold', 'basic', 'leaderRule'

            // Max ranks allowed for RCV
            const mr = params.has('mr') ? parseInt(params.get('mr')) : null;

            const clamp = (v) => {
                if (v === null || Number.isNaN(v)) return null;
                // round to 2 decimals, clamp to [0,1]
                return Math.round(Math.max(0, Math.min(1, v)) * 100) / 100;
            };

            let nn = pn !== null ? Math.max(2, Math.min(4, pn)) : numCandidates;
            let nc1 = clamp(p1);
            let nc2 = clamp(p2);
            let nc3 = clamp(p3);
            let nc4 = clamp(p4);

            // If any missing, leave as current state (so we need to read current state values)
            if (nc1 === null) nc1 = c1;
            if (nc2 === null) nc2 = c2;
            if (nc3 === null) nc3 = c3;
            if (nc4 === null) nc4 = c4;

            // No ordering enforcement - allow candidates to be in any order
            // The candidates useMemo will handle sorting for display

            if (replaceValues) {
                setNumCandidates(nn);
                setC1(nc1);
                setC2(nc2);
                setC3(nc3);
                setC4(nc4);
                // 2D positions
                if (dim === '2d') setDimensionMode('2d');
                else if (dim === '1d') setDimensionMode('1d');
                if (p1_2d) setC1_2d(p1_2d);
                if (p2_2d) setC2_2d(p2_2d);
                if (p3_2d) setC3_2d(p3_2d);
                if (p4_2d) setC4_2d(p4_2d);

                if (l1 !== null) setLabel1(l1);
                if (l2 !== null) setLabel2(l2);
                if (l3 !== null) setLabel3(l3);
                if (l4 !== null) setLabel4(l4);
                if (st !== null && !Number.isNaN(st)) {
                    setSincereThreshold(Math.max(0.01, Math.min(1.0, st)));
                }
                if (bs !== null) {
                    setUseBasicStrategy(Boolean(bs));
                }
                // Strategy type - map 'basic' to 'threshold' + useBasicStrategy for backwards compatibility
                if (strat !== null) {
                    if (strat === 'basic') {
                        setStrategyType('threshold');
                        setUseBasicStrategy(true);
                    } else if (['threshold', 'leaderRule'].includes(strat)) {
                        setStrategyType(strat);
                    }
                }

                // Async update rate
                const au = parseFloat(params.get('au'));
                if (au !== null && !Number.isNaN(au) && au >= 0.1 && au <= 1.0) {
                    setAsyncUpdateRate(au);
                }

                // Sincere voter proportion
                const sv = parseFloat(params.get('sv'));
                if (sv !== null && !Number.isNaN(sv) && sv >= 0.0 && sv <= 1.0) {
                    setSincereVoterProportion(sv);
                }

                // Max ranks allowed
                if (mr !== null && !Number.isNaN(mr) && mr >= 1) {
                    setMaxRanksAllowed(Math.min(mr, numCandidates));
                }
            }
        } catch (err) {
            console.warn('Error parsing URL params', err);
        }
    };

    // Helper to update a single URL param immediately (used by sliders)
    const updateUrlParam = (key, value) => {
        try {
            const params = new URLSearchParams(window.location.search);
            if (value === null || value === undefined) params.delete(key);
            else params.set(key, value);
            const newQuery = params.toString();
            const newUrl = window.location.pathname + (newQuery ? '?' + newQuery : '');
            window.history.replaceState({}, '', newUrl);
        } catch (err) {
            console.warn('Error updating single URL param', err);
        }
    };

    // Read initial params once and respond to back/forward navigation
    useEffect(() => {
        parseAndApplyUrl(true);
        hasInitializedRef.current = true;

        const onPop = () => parseAndApplyUrl(true);
        window.addEventListener('popstate', onPop);
        return () => window.removeEventListener('popstate', onPop);
        // Intentionally run only once on mount
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    // Update the URL whenever positions or labels change
    useEffect(() => {
        // Don't update URL on initial mount (let parseAndApplyUrl run first)
        if (!hasInitializedRef.current) return;

        try {
            const params = new URLSearchParams(window.location.search);
            params.set('n', numCandidates.toString());
            params.set('dim', dimensionMode);
            // 1D positions
            params.set('c1', c1.toFixed(3));
            params.set('c2', c2.toFixed(3));
            if (numCandidates >= 3) params.set('c3', c3.toFixed(3));
            else params.delete('c3');
            if (numCandidates >= 4) params.set('c4', c4.toFixed(3));
            else params.delete('c4');
            // 2D positions
            params.set('c1_2d', `${c1_2d.x.toFixed(3)},${c1_2d.y.toFixed(3)}`);
            params.set('c2_2d', `${c2_2d.x.toFixed(3)},${c2_2d.y.toFixed(3)}`);
            if (numCandidates >= 3) params.set('c3_2d', `${c3_2d.x.toFixed(3)},${c3_2d.y.toFixed(3)}`);
            else params.delete('c3_2d');
            if (numCandidates >= 4) params.set('c4_2d', `${c4_2d.x.toFixed(3)},${c4_2d.y.toFixed(3)}`);
            else params.delete('c4_2d');

            if (label1) params.set('l1', label1); else params.delete('l1');
            if (label2) params.set('l2', label2); else params.delete('l2');
            if (label3 && numCandidates >= 3) params.set('l3', label3); else params.delete('l3');
            if (label4 && numCandidates >= 4) params.set('l4', label4); else params.delete('l4');

            params.set('st', sincereThreshold.toFixed(3));
            params.set('bs', useBasicStrategy ? '1' : '0');
            params.set('strat', strategyType);
            params.set('au', asyncUpdateRate.toFixed(1));
            params.set('sv', sincereVoterProportion.toFixed(1));
            params.set('mr', maxRanksAllowed.toString());

            const newQuery = params.toString();
            const newUrl = window.location.pathname + (newQuery ? '?' + newQuery : '');
            // use replaceState so user history isn't flooded with tiny changes
            window.history.replaceState({}, '', newUrl);
        } catch (err) {
            console.warn('Error updating URL params', err);
        }
    }, [numCandidates, dimensionMode, c1, c2, c3, c4, c1_2d, c2_2d, c3_2d, c4_2d, label1, label2, label3, label4, sincereThreshold, useBasicStrategy, strategyType, asyncUpdateRate, sincereVoterProportion, maxRanksAllowed]);

    const handlePointerDown = (point) => (e) => {
        e.preventDefault();
        setDragging(point);
    };

    const handlePointerMove = (e) => {
        if (!dragging) return;

        // Handle 1D dragging
        if (dimensionMode === '1d' && svgRef.current) {
            const svg = svgRef.current;
            const rect = svg.getBoundingClientRect();

            // Handle both mouse and touch events
            const clientX = e.touches ? e.touches[0].clientX : e.clientX;
            const x = (clientX - rect.left) / rect.width;
            const clampedX = Math.max(0, Math.min(1, x));

            // Round to nearest 0.01 for cleaner values
            const roundedX = Math.round(clampedX * 100) / 100;

            // Allow candidates to be dragged freely across each other
            if (dragging === 'c1') {
                setC1(roundedX);
            } else if (dragging === 'c2') {
                setC2(roundedX);
            } else if (dragging === 'c3' && numCandidates >= 3) {
                setC3(roundedX);
            } else if (dragging === 'c4' && numCandidates >= 4) {
                setC4(roundedX);
            }
        }

        // Handle 2D dragging
        if (dimensionMode === '2d' && svg2dRef.current) {
            const svg = svg2dRef.current;
            const rect = svg.getBoundingClientRect();

            const clientX = e.touches ? e.touches[0].clientX : e.clientX;
            const clientY = e.touches ? e.touches[0].clientY : e.clientY;
            const x = (clientX - rect.left) / rect.width;
            const y = (clientY - rect.top) / rect.height;
            const clampedX = Math.max(0, Math.min(1, x));
            const clampedY = Math.max(0, Math.min(1, y));

            const roundedX = Math.round(clampedX * 100) / 100;
            const roundedY = Math.round(clampedY * 100) / 100;

            if (dragging === 'c1') {
                setC1_2d({ x: roundedX, y: roundedY });
            } else if (dragging === 'c2') {
                setC2_2d({ x: roundedX, y: roundedY });
            } else if (dragging === 'c3' && numCandidates >= 3) {
                setC3_2d({ x: roundedX, y: roundedY });
            } else if (dragging === 'c4' && numCandidates >= 4) {
                setC4_2d({ x: roundedX, y: roundedY });
            }
        }
    };

    const handlePointerUp = () => {
        setDragging(null);
    };

    useEffect(() => {
        if (dragging) {
            document.addEventListener('pointermove', handlePointerMove);
            document.addEventListener('pointerup', handlePointerUp);
            document.addEventListener('touchmove', handlePointerMove);
            document.addEventListener('touchend', handlePointerUp);
            return () => {
                document.removeEventListener('pointermove', handlePointerMove);
                document.removeEventListener('pointerup', handlePointerUp);
                document.removeEventListener('touchmove', handlePointerMove);
                document.removeEventListener('touchend', handlePointerUp);
            };
        }
    }, [dragging, dimensionMode, c1, c2, c3, c4, c1_2d, c2_2d, c3_2d, c4_2d, numCandidates]);

    // Candidate positions - unified structure for both 1D and 2D
    // In 1D mode: { C1: 0.2, C2: 0.5, ... } (number positions)
    // In 2D mode: { C1: {x: 0.2, y: 0.3}, C2: {x: 0.5, y: 0.7}, ... } (point positions)
    const candidates = useMemo(() => {
        if (dimensionMode === '1d') {
            const positions = [
                { id: 'C1', pos: c1 },
                { id: 'C2', pos: c2 }
            ];
            if (numCandidates >= 3) positions.push({ id: 'C3', pos: c3 });
            if (numCandidates >= 4) positions.push({ id: 'C4', pos: c4 });

            // Sort by position to maintain left-to-right ordering
            positions.sort((a, b) => a.pos - b.pos);

            const result = {};
            positions.forEach(p => result[p.id] = p.pos);
            return result;
        } else {
            // 2D mode - don't sort, just return positions
            const result = { C1: c1_2d, C2: c2_2d };
            if (numCandidates >= 3) result.C3 = c3_2d;
            if (numCandidates >= 4) result.C4 = c4_2d;
            return result;
        }
    }, [dimensionMode, c1, c2, c3, c4, c1_2d, c2_2d, c3_2d, c4_2d, numCandidates]);

    // Helper function to calculate distance between two points (works for both 1D and 2D)
    const distance = (pos1, pos2) => {
        if (dimensionMode === '1d') {
            return Math.abs(pos1 - pos2);
        } else {
            return Math.sqrt((pos1.x - pos2.x) ** 2 + (pos1.y - pos2.y) ** 2);
        }
    };

    // Calculate indifference points/lines (where voters are equidistant)
    // In 1D: points (midpoints between candidates)
    // In 2D: lines (perpendicular bisectors between candidates)
    const indifferencePoints = useMemo(() => {
        if (dimensionMode === '1d') {
            const points = {
                x12: (c1 + c2) / 2
            };
            if (numCandidates >= 3) {
                points.x23 = (c2 + c3) / 2;
                points.x13 = (c1 + c3) / 2;
            }
            if (numCandidates >= 4) {
                points.x34 = (c3 + c4) / 2;
                points.x14 = (c1 + c4) / 2;
                points.x24 = (c2 + c4) / 2;
            }
            return points;
        } else {
            // 2D mode: return perpendicular bisector lines
            // Each line is defined as { midpoint: {x, y}, slope: number or 'vertical' }
            const candList = [{ id: 'C1', pos: c1_2d }, { id: 'C2', pos: c2_2d }];
            if (numCandidates >= 3) candList.push({ id: 'C3', pos: c3_2d });
            if (numCandidates >= 4) candList.push({ id: 'C4', pos: c4_2d });

            const lines = {};
            for (let i = 0; i < candList.length; i++) {
                for (let j = i + 1; j < candList.length; j++) {
                    const p1 = candList[i].pos;
                    const p2 = candList[j].pos;
                    const midpoint = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };
                    // Perpendicular bisector has slope perpendicular to line connecting candidates
                    const dx = p2.x - p1.x;
                    const dy = p2.y - p1.y;
                    // Original slope is dy/dx, perpendicular slope is -dx/dy
                    const slope = Math.abs(dy) < 0.0001 ? 'vertical' : -dx / dy;
                    const key = `x${candList[i].id.slice(1)}${candList[j].id.slice(1)}`;
                    lines[key] = { midpoint, slope, c1: candList[i].id, c2: candList[j].id };
                }
            }
            return lines;
        }
    }, [dimensionMode, c1, c2, c3, c4, c1_2d, c2_2d, c3_2d, c4_2d, numCandidates]);

    // Calculate voter preferences based on distance
    const voterRankings = useMemo(() => {
        const candNames = Object.keys(candidates);
        const epsilon = 0.0001; // Threshold for considering candidates at the same position

        if (dimensionMode === '1d') {
            const candPositions = Object.values(candidates);

            // Find critical points where rankings change
            const points = [0, 1, ...candPositions];

            // Add midpoints between all pairs
            for (let i = 0; i < candPositions.length; i++) {
                for (let j = i + 1; j < candPositions.length; j++) {
                    points.push((candPositions[i] + candPositions[j]) / 2);
                }
            }

            points.sort((a, b) => a - b);

            // For each interval, determine ranking and track intervals
            const rankings = [];
            const rankingIntervals = {}; // Track intervals for each ranking

            for (let i = 0; i < points.length - 1; i++) {
                const voterPos = (points[i] + points[i + 1]) / 2;

                const dists = candNames.map(name => ({
                    name,
                    dist: Math.abs(voterPos - candidates[name])
                }));

                dists.sort((a, b) => a.dist - b.dist);

                // Group candidates by distance (handle ties)
                const groups = [];
                let currentGroup = [dists[0]];
                for (let j = 1; j < dists.length; j++) {
                    if (Math.abs(dists[j].dist - dists[j - 1].dist) < epsilon) {
                        currentGroup.push(dists[j]);
                    } else {
                        groups.push(currentGroup);
                        currentGroup = [dists[j]];
                    }
                }
                groups.push(currentGroup);

                // Create ranking with ties indicated by '=' between tied candidates
                const ranking = groups.map(group =>
                    group.map(d => d.name).sort().join('=')
                ).join('>');

                const proportion = points[i + 1] - points[i];

                // Track the interval for this ranking
                if (!rankingIntervals[ranking]) {
                    rankingIntervals[ranking] = [];
                }
                rankingIntervals[ranking].push({ start: points[i], end: points[i + 1] });

                const found = rankings.find(r => r.ranking === ranking);
                if (found) {
                    found.proportion += proportion;
                } else {
                    rankings.push({ ranking, proportion, intervals: [] });
                }
            }

            // Merge adjacent intervals for each ranking
            rankings.forEach(r => {
                const intervals = rankingIntervals[r.ranking];
                if (intervals && intervals.length > 0) {
                    // Sort intervals by start
                    intervals.sort((a, b) => a.start - b.start);

                    // Merge adjacent/overlapping intervals
                    const merged = [intervals[0]];
                    for (let i = 1; i < intervals.length; i++) {
                        const current = intervals[i];
                        const last = merged[merged.length - 1];

                        // If intervals are adjacent or overlap, merge them
                        if (Math.abs(current.start - last.end) < 0.0001) {
                            last.end = current.end;
                        } else {
                            merged.push(current);
                        }
                    }

                    r.intervals = merged;
                }
            });

            return rankings.filter(r => r.proportion > 0.0001);
        } else {
            // 2D mode: sample the grid uniformly to compute Voronoi proportions
            const gridSize = 100; // 100x100 grid for decent precision
            const rankings = [];
            const rankingCounts = {};

            for (let ix = 0; ix < gridSize; ix++) {
                for (let iy = 0; iy < gridSize; iy++) {
                    const voterPos = {
                        x: (ix + 0.5) / gridSize,
                        y: (iy + 0.5) / gridSize
                    };

                    const dists = candNames.map(name => ({
                        name,
                        dist: Math.sqrt(
                            (voterPos.x - candidates[name].x) ** 2 +
                            (voterPos.y - candidates[name].y) ** 2
                        )
                    }));

                    dists.sort((a, b) => a.dist - b.dist);

                    // Group candidates by distance (handle ties)
                    const groups = [];
                    let currentGroup = [dists[0]];
                    for (let j = 1; j < dists.length; j++) {
                        if (Math.abs(dists[j].dist - dists[j - 1].dist) < epsilon) {
                            currentGroup.push(dists[j]);
                        } else {
                            groups.push(currentGroup);
                            currentGroup = [dists[j]];
                        }
                    }
                    groups.push(currentGroup);

                    // Create ranking with ties indicated by '=' between tied candidates
                    const ranking = groups.map(group =>
                        group.map(d => d.name).sort().join('=')
                    ).join('>');

                    if (rankingCounts[ranking]) {
                        rankingCounts[ranking]++;
                    } else {
                        rankingCounts[ranking] = 1;
                    }
                }
            }

            const totalCells = gridSize * gridSize;
            for (const [ranking, count] of Object.entries(rankingCounts)) {
                rankings.push({ ranking, proportion: count / totalCells });
            }

            return rankings.filter(r => r.proportion > 0.0001);
        }
    }, [candidates, dimensionMode]);

    // Calculate ranking regions for visualization
    const rankingRegions = useMemo(() => {
        const candNames = Object.keys(candidates);

        if (dimensionMode === '1d') {
            const criticalPoints = [0, ...Object.values(indifferencePoints), 1].sort((a, b) => a - b);

            // Remove duplicates
            const uniquePoints = [...new Set(criticalPoints)];

            // For each interval, determine ranking
            const regions = [];
            for (let i = 0; i < uniquePoints.length - 1; i++) {
                const voterPos = (uniquePoints[i] + uniquePoints[i + 1]) / 2;

                const dists = candNames.map(name => ({
                    name,
                    dist: Math.abs(voterPos - candidates[name])
                }));

                dists.sort((a, b) => a.dist - b.dist);

                const ranking = dists.map(d => d.name).join('>');

                regions.push({
                    start: uniquePoints[i],
                    end: uniquePoints[i + 1],
                    ranking
                });
            }

            return regions;
        } else {
            // 2D mode: return grid of ranking data for visualization
            // We'll compute this at render time in the visualization component
            return [];
        }
    }, [candidates, indifferencePoints, dimensionMode]);

    // Helper function to parse rankings with ties
    // Returns array of groups where each group is an array of tied candidates
    // e.g., "C1=C2>C3" => [["C1", "C2"], ["C3"]]
    const parseRankingWithTies = (ranking) => {
        if (!ranking) return [];
        return ranking.split('>').map(group => group.split('='));
    };

    // Helper function to get the first choice(s) from a ranking, accounting for ties
    // Returns an array of candidates tied for first
    const getFirstChoices = (ranking, remaining = null) => {
        const groups = parseRankingWithTies(ranking);
        if (groups.length === 0) return [];

        // If remaining is specified, find the first group with at least one remaining candidate
        if (remaining) {
            for (const group of groups) {
                const validCandidates = group.filter(c => c !== '' && remaining.includes(c));
                if (validCandidates.length > 0) {
                    return validCandidates;
                }
            }
            return [];
        }

        // Otherwise return the first group
        return groups[0].filter(c => c !== '');
    };

    // Pairwise comparisons
    const pairwise = useMemo(() => {
        const results = {};
        const names = Object.keys(candidates);

        for (let i = 0; i < names.length; i++) {
            for (let j = i + 1; j < names.length; j++) {
                const c1 = names[i];
                const c2 = names[j];
                let c1Votes = 0;

                voterRankings.forEach(v => {
                    const groups = parseRankingWithTies(v.ranking);

                    // Find which group c1 and c2 are in
                    let c1Group = -1, c2Group = -1;
                    for (let g = 0; g < groups.length; g++) {
                        if (groups[g].includes(c1)) c1Group = g;
                        if (groups[g].includes(c2)) c2Group = g;
                    }

                    // Only count vote if they're in different groups
                    if (c1Group !== c2Group) {
                        if (c1Group < c2Group) {
                            c1Votes += v.proportion;
                        }
                    }
                    // If they're in the same group (tied), neither gets a vote
                });

                results[c1 + ' vs ' + c2] = {
                    winner: c1Votes > 0.5 ? c1 : c2,
                    score: (Math.max(c1Votes, 1 - c1Votes) * 100).toFixed(1) + '%'
                };
            }
        }

        return results;
    }, [voterRankings, candidates]);

    // Condorcet winner and wins count
    const condorcetInfo = useMemo(() => {
        const names = Object.keys(candidates);
        const wins = {};
        names.forEach(name => wins[name] = 0);

        Object.values(pairwise).forEach(r => wins[r.winner]++);
        const maxWins = Math.max(...Object.values(wins));

        let winner = 'None';
        for (let c of names) {
            if (wins[c] === names.length - 1) {
                winner = c;
                break;
            }
        }

        return { winner, wins };
    }, [pairwise, candidates]);

    // Group pairwise matchups by candidate (ordered by strength)
    const groupedPairwise = useMemo(() => {
        const candNames = Object.keys(candidates);
        const sortedCandidates = candNames.sort((a, b) =>
            condorcetInfo.wins[b] - condorcetInfo.wins[a]
        );

        const groups = [];
        sortedCandidates.forEach(cand => {
            const matchups = [];
            Object.entries(pairwise).forEach(([matchup, result]) => {
                if (matchup.startsWith(cand + ' vs ') || matchup.includes(' vs ' + cand)) {
                    matchups.push({ matchup, result });
                }
            });

            // Remove duplicates (each matchup appears twice)
            const uniqueMatchups = [];
            const seen = new Set();
            matchups.forEach(m => {
                const sorted = m.matchup.split(' vs ').sort().join(' vs ');
                if (!seen.has(sorted)) {
                    seen.add(sorted);
                    uniqueMatchups.push(m);
                }
            });

            groups.push({
                candidate: cand,
                wins: condorcetInfo.wins[cand],
                matchups: uniqueMatchups
            });
        });

        return groups;
    }, [pairwise, condorcetInfo, candidates]);

    // Calculate reverse Borda count (1 pt for 1st, 2 pts for 2nd, 3 pts for 3rd, etc.)
    // Higher score is worse, used for tiebreaking
    const bordaScores = useMemo(() => {
        const names = Object.keys(candidates);
        const scores = {};
        names.forEach(name => scores[name] = 0);

        voterRankings.forEach(v => {
            const groups = parseRankingWithTies(v.ranking);
            let currentRank = 1;

            groups.forEach(group => {
                // For tied candidates, give them the average score
                // e.g., if 2 candidates tied for 1st, they each get (1+2)/2 = 1.5 points
                const groupSize = group.filter(c => c !== '').length;
                const avgScore = (currentRank + currentRank + groupSize - 1) / 2;

                group.forEach(cand => {
                    if (cand !== '') {
                        scores[cand] += avgScore * v.proportion;
                    }
                });

                currentRank += groupSize;
            });
        });

        return scores;
    }, [voterRankings, candidates]);

    // Borda winner (lowest score wins)
    const bordaWinner = useMemo(() => {
        const sorted = Object.entries(bordaScores).sort((a, b) => a[1] - b[1]);
        return sorted[0][0];
    }, [bordaScores]);

    // Label mapping helper
    const getLabel = (candId) => {
        const labels = { C1: label1, C2: label2, C3: label3, C4: label4 };
        return labels[candId] || candId;
    };

    // Special regions for 3-candidate case (center winning regions)
    const specialRegions = useMemo(() => {
        if (numCandidates !== 3 || dimensionMode !== '1d') return null;

        // Sort candidates by position to find the middle one
        const positions = [
            { id: 'C1', pos: c1 },
            { id: 'C2', pos: c2 },
            { id: 'C3', pos: c3 }
        ].sort((a, b) => a.pos - b.pos);

        const left = positions[0];
        const middle = positions[1];
        const right = positions[2];

        const eps = right.pos - left.pos - 0.5;
        if (eps <= 0) return null;

        const regions = [];

        // Left candidate region: center at 0.5 - left.pos
        const leftCenter = 0.5 - left.pos;
        regions.push({
            center: leftCenter,
            start: Math.max(0, leftCenter - eps, left.pos),
            end: Math.min(1, leftCenter + eps),
            candId: left.id,
            label: getLabel(left.id)
        });

        // Right candidate region: center at 1.5 - right.pos
        const rightCenter = 1.5 - right.pos;
        regions.push({
            center: rightCenter,
            start: Math.max(0, rightCenter - eps),
            end: Math.min(1, rightCenter + eps, right.pos),
            candId: right.id,
            label: getLabel(right.id)
        });

        return {
            eps,
            regions,
            middleCandidate: middle.id,
            middleLabel: getLabel(middle.id),
            leftLabel: getLabel(left.id),
            rightLabel: getLabel(right.id)
        };
    }, [numCandidates, dimensionMode, c1, c2, c3, label1, label2, label3]);

    // Convert ranking string to use custom labels (e.g., "C1>C2>C3" → "Alice>Bob>Charlie")
    const formatRanking = (ranking) => {
        return ranking.split('>').map(group =>
            group.split('=').map(c => getLabel(c)).join('=')
        ).join('>');
    };

    // Convert ranking to use first letter only (for compact display in regions)
    const formatRankingCompact = (ranking) => {
        return ranking.split('>').map(group =>
            group.split('=').map(c => getLabel(c).charAt(0)).join('=')
        ).join('>');
    };

    // RCV rounds with reverse Borda tiebreaker and ballot exhaustion
    const rcv = useMemo(() => {
        const rounds = [];
        let remaining = Object.keys(candidates);
        let voters = voterRankings.map(v => ({ ...v }));
        let exhausted = 0;

        // Apply max ranks limit to initial voter rankings
        voters = voters.map(v => {
            const groups = parseRankingWithTies(v.ranking);
            const limitedRanking = groups.slice(0, maxRanksAllowed).map(g => g.join('=')).join('>');
            return { ranking: limitedRanking, proportion: v.proportion };
        });

        while (remaining.length > 1) {
            const votes = {};
            remaining.forEach(c => votes[c] = 0);
            let newExhausted = 0;

            voters.forEach(v => {
                const firstChoices = getFirstChoices(v.ranking, remaining);
                if (firstChoices.length > 0) {
                    // Split vote evenly among tied first choices
                    const voteShare = v.proportion / firstChoices.length;
                    firstChoices.forEach(choice => {
                        votes[choice] += voteShare;
                    });
                } else {
                    // Ballot is exhausted (no remaining candidates in their ranking)
                    newExhausted += v.proportion;
                }
            });

            exhausted += newExhausted;

            const round = {};
            remaining.forEach(c => round[c] = votes[c]);
            round.exhausted = exhausted;

            const sorted = Object.entries(votes).sort((a, b) => b[1] - a[1]);
            const totalNonExhausted = 1 - exhausted;

            // Check for majority among non-exhausted ballots
            if (totalNonExhausted > 0 && sorted.length > 0 && sorted[0][1] > totalNonExhausted / 2) {
                rounds.push({ votes: round, eliminated: null, exhausted });
                rounds.push({ winner: sorted[0][0], exhausted });
                break;
            }

            // Find minimum vote count
            const minVotes = sorted.length > 0 ? sorted[sorted.length - 1][1] : 0;

            // Get all candidates tied for last (with epsilon for floating point comparison)
            const epsilon = 0.0001;
            const tiedForLast = sorted.filter(([c, v]) => Math.abs(v - minVotes) < epsilon).map(([c, v]) => c);

            let eliminated;
            if (tiedForLast.length === 1) {
                eliminated = tiedForLast[0];
            } else if (tiedForLast.length > 1) {
                // Break tie by reverse Borda count (HIGHEST score gets eliminated)
                // Find the maximum Borda score among tied candidates
                const maxBorda = Math.max(...tiedForLast.map(c => bordaScores[c]));
                eliminated = tiedForLast.find(c => bordaScores[c] === maxBorda);
            }

            rounds.push({ votes: round, eliminated, exhausted });

            remaining = remaining.filter(c => c !== eliminated);
            voters = voters.map(v => {
                // Remove eliminated candidate from all tie groups
                const groups = parseRankingWithTies(v.ranking);
                const filteredGroups = groups.map(g => g.filter(c => c !== eliminated)).filter(g => g.length > 0);
                return {
                    ranking: filteredGroups.map(g => g.join('=')).join('>'),
                    proportion: v.proportion
                };
            });
        }

        return rounds;
    }, [voterRankings, bordaScores, candidates, maxRanksAllowed]);

    // AV critical profiles
    const avProfiles = useMemo(() => {
        const profiles = {};
        const names = Object.keys(candidates);

        names.forEach(target => {
            const approvals = {};
            names.forEach(name => approvals[name] = 0);

            voterRankings.forEach(v => {
                const groups = parseRankingWithTies(v.ranking);

                // Find which group the target is in
                let targetGroupIdx = -1;
                for (let i = 0; i < groups.length; i++) {
                    if (groups[i].includes(target)) {
                        targetGroupIdx = i;
                        break;
                    }
                }

                if (targetGroupIdx === -1) return; // target not in ranking

                if (targetGroupIdx === groups.length - 1) {
                    // Target is in the last group, approve only candidates in the first group
                    // Split vote equally among first-place candidates
                    const firstGroup = groups[0].filter(c => c !== '');
                    const voteShare = v.proportion / firstGroup.length;
                    firstGroup.forEach(c => {
                        approvals[c] += voteShare;
                    });
                } else {
                    // Approve target and everyone in groups at or above target
                    for (let i = 0; i <= targetGroupIdx; i++) {
                        groups[i].forEach(c => {
                            if (c !== '') {
                                approvals[c] += v.proportion;
                            }
                        });
                    }
                }
            });

            profiles[target] = approvals;
        });

        return profiles;
    }, [voterRankings, candidates]);

    const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444'];

    // Add/Remove candidates handler
    const addCandidate = () => {
        if (numCandidates < 4) {
            setNumCandidates(numCandidates + 1);
            // Adjust positions if needed to maintain spacing
            if (numCandidates === 2) {
                // Adding C3, make sure there's space
                if (c3 - c2 < 0.01) {
                    setC3(Math.min(0.99, c2 + 0.1));
                }
            } else if (numCandidates === 3) {
                // Adding C4, make sure there's space
                if (c4 - c3 < 0.01) {
                    setC4(Math.min(0.99, c3 + 0.05));
                }
            }
        }
    };

    const removeCandidate = () => {
        if (numCandidates > 2) {
            setNumCandidates(numCandidates - 1);
        }
    };

    // Monte Carlo simulation state
    const [numSimulations, setNumSimulations] = useState(1000);
    const [distributionType, setDistributionType] = useState('uniform'); // 'uniform' or 'normal'

    // Sincere threshold simulation state
    const [sincereThreshold, setSincereThreshold] = useState(0.15);
    const [numVoters, setNumVoters] = useState(10000);
    const [useBasicStrategy, setUseBasicStrategy] = useState(false);
    // Strategy type: 'threshold' (approval polls assumption), 'basic' (always approve closest, never furthest), 'leaderRule'
    const [strategyType, setStrategyType] = useState('threshold');
    const sincereMCSimulations = 100;
    const [sincereMCResults, setSincereMCResults] = useState(null);

    // Turn-based simulation state
    const [turnBasedResults, setTurnBasedResults] = useState(null);
    const [currentStep, setCurrentStep] = useState(0);
    const [voterSeed, setVoterSeed] = useState(0); // For redistributing voters
    const [asyncUpdateRate, setAsyncUpdateRate] = useState(1.0); // Fraction of voters that update per step
    const [sincereVoterProportion, setSincereVoterProportion] = useState(0.0); // Proportion of voters who never change strategy
    const [showInitialApprovals, setShowInitialApprovals] = useState(true); // Show initial vs final approvals for leader rule
    const showInitialFinalToggle = false; // Flag to control visibility of initial/final toggle checkbox

    // Monte Carlo simulation for approval voting
    const monteCarloResults = useMemo(() => {
        const results = { C1: 0, C2: 0, C3: 0, C4: 0 };

        for (let sim = 0; sim < numSimulations; sim++) {
            const approvals = { C1: 0, C2: 0, C3: 0, C4: 0 };

            // For each simulation, assign each voter bloc a probability of approving candidates
            voterRankings.forEach(v => {
                const groups = parseRankingWithTies(v.ranking);

                // Always approve top choice(s) - split equally among tied candidates
                const firstGroup = groups[0].filter(c => c !== '');
                const firstShare = v.proportion / firstGroup.length;
                firstGroup.forEach(c => {
                    approvals[c] += firstShare;
                });

                // For groups 1 through second-to-last, draw probabilities
                // Never approve the last-ranked group
                for (let i = 1; i < groups.length - 1; i++) {
                    let prob;
                    if (distributionType === 'uniform') {
                        prob = Math.random(); // uniform [0,1]
                    } else {
                        // normal distribution, mean=0.5, std=0.2, clamped to [0,1]
                        const u1 = Math.random();
                        const u2 = Math.random();
                        const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
                        prob = Math.max(0, Math.min(1, 0.5 + 0.2 * z));
                    }

                    // Apply this probability to all candidates in this group
                    groups[i].forEach(c => {
                        if (c !== '') {
                            approvals[c] += v.proportion * prob;
                        }
                    });
                }
            });

            // Find winner
            const sorted = Object.entries(approvals).sort((a, b) => b[1] - a[1]);
            results[sorted[0][0]]++;
        }

        return results;
    }, [voterRankings, numSimulations, distributionType]);

    // Copy-to-clipboard feedback state
    const [copyStatus, setCopyStatus] = useState('');

    // Sincere threshold simulation - generates voters and computes results
    const sincereThresholdResults = useMemo(() => {
        const candNames = Object.keys(candidates);

        // Simple seeded random number generator
        const seededRandom = (seed) => {
            let state = seed;
            return () => {
                state = (state * 1664525 + 1013904223) % 4294967296;
                return state / 4294967296;
            };
        };
        const rng = seededRandom(voterSeed);

        // Generate uniformly distributed voters
        const voters = [];
        for (let i = 0; i < numVoters; i++) {
            if (dimensionMode === '1d') {
                voters.push(rng());
            } else {
                // 2D: voters are {x, y} points
                voters.push({ x: rng(), y: rng() });
            }
        }

        // Helper to compute distance
        const computeDistance = (voterPos, candPos) => {
            if (dimensionMode === '1d') {
                return Math.abs(voterPos - candPos);
            } else {
                return Math.sqrt((voterPos.x - candPos.x) ** 2 + (voterPos.y - candPos.y) ** 2);
            }
        };

        // For leaderRule strategy, we need initial poll results first
        // Use threshold-based voting for the initial poll
        let initialPollResults = null;

        // Always store initial approval counts (sincere threshold voting)
        const initialApprovalCounts = {};
        candNames.forEach(name => initialApprovalCounts[name] = 0);

        const initialVotesPerVoter = []; // Track initial votes cast per voter

        voters.forEach(voterPos => {
            const distances = candNames.map(name => ({
                name,
                distance: computeDistance(voterPos, candidates[name])
            }));
            
            let initialApprovedCount = 0;
            
            // Use basic strategy if enabled for initial poll
            if (useBasicStrategy) {
                distances.sort((a, b) => a.distance - b.distance);
                initialApprovalCounts[distances[0].name]++;
                initialApprovedCount++;
                for (let i = 1; i < distances.length - 1; i++) {
                    if (distances[i].distance <= sincereThreshold) {
                        initialApprovalCounts[distances[i].name]++;
                        initialApprovedCount++;
                    }
                }
            } else {
                distances.forEach(d => {
                    if (d.distance <= sincereThreshold) {
                        initialApprovalCounts[d.name]++;
                        initialApprovedCount++;
                    }
                });
            }
            
            initialVotesPerVoter.push(initialApprovedCount);
        });

        // For leaderRule, identify leader and challenger from initial counts
        if (strategyType === 'leaderRule') {
            const sorted = Object.entries(initialApprovalCounts).sort((a, b) => b[1] - a[1]);
            initialPollResults = {
                leader: sorted[0][0],
                challenger: sorted[1][0]
            };
        }

        // Calculate approval votes for each voter
        const approvalCounts = {};
        candNames.forEach(name => approvalCounts[name] = 0);

        const votesPerVoter = []; // Track how many candidates each voter approves

        voters.forEach(voterPos => {
            // Calculate distances to all candidates
            const distances = candNames.map(name => ({
                name,
                distance: computeDistance(voterPos, candidates[name])
            }));

            // Sort by distance (closest first = most preferred)
            distances.sort((a, b) => a.distance - b.distance);

            let approvedCount = 0;

            // Determine which candidates to approve based on strategy
            if (strategyType === 'leaderRule' && initialPollResults) {
                // Leader Rule Strategy:
                // 1. Identify the current leader (x1) and challenger (x2)
                // 2. Approve everyone preferred STRICTLY to x1
                // 3. If voter prefers x1 > x2, also approve x1
                //    If voter prefers x2 > x1, do NOT approve x1

                const { leader, challenger } = initialPollResults;
                const leaderDist = computeDistance(voterPos, candidates[leader]);
                const challengerDist = computeDistance(voterPos, candidates[challenger]);
                const prefersLeader = leaderDist < challengerDist;

                // Basic strategy: always approve closest
                if (useBasicStrategy) {
                    approvalCounts[distances[0].name]++;
                    approvedCount++;
                }

                // Approve all candidates preferred strictly to the leader
                distances.forEach(d => {
                    // Skip if already approved (closest with basic strategy)
                    if (useBasicStrategy && d.name === distances[0].name) return;
                    // Never approve furthest if basic strategy
                    if (useBasicStrategy && d.name === distances[distances.length - 1].name) return;

                    if (d.distance < leaderDist) {
                        // Strictly preferred to leader
                        approvalCounts[d.name]++;
                        approvedCount++;
                    } else if (d.name === leader && prefersLeader) {
                        // Approve leader only if voter prefers leader to challenger
                        approvalCounts[d.name]++;
                        approvedCount++;
                    }
                });
            } else if (useBasicStrategy) {
                // Basic strategy: Always approve closest, never approve furthest
                approvalCounts[distances[0].name]++;
                approvedCount++;

                // Check middle candidates against threshold
                for (let i = 1; i < distances.length - 1; i++) {
                    if (distances[i].distance <= sincereThreshold) {
                        approvalCounts[distances[i].name]++;
                        approvedCount++;
                    }
                }
            } else {
                // Default: Approve all candidates within threshold (could be 0 or all)
                distances.forEach(d => {
                    if (d.distance <= sincereThreshold) {
                        approvalCounts[d.name]++;
                        approvedCount++;
                    }
                });
            }

            votesPerVoter.push(approvedCount);
        });

        // Find winner
        const sorted = Object.entries(approvalCounts).sort((a, b) => b[1] - a[1]);
        const winner = sorted[0][0];
        const maxVotes = sorted[0][1];

        // Count distribution of votes per voter
        const votesDistribution = {};
        for (let i = 0; i <= candNames.length; i++) {
            votesDistribution[i] = 0;
        }
        votesPerVoter.forEach(count => {
            votesDistribution[count]++;
        });

        // Count initial distribution of votes per voter
        const initialVotesDistribution = {};
        for (let i = 0; i <= candNames.length; i++) {
            initialVotesDistribution[i] = 0;
        }
        initialVotesPerVoter.forEach(count => {
            initialVotesDistribution[count]++;
        });

        return {
            approvalCounts,
            winner,
            maxVotes,
            votesDistribution,
            totalVoters: numVoters,
            voters, // Return voters array for turn-based simulation
            initialPollResults, // Return initial poll results for reference
            initialApprovalCounts, // Return initial approval counts for leader rule
            initialVotesDistribution // Return initial votes distribution
        };
    }, [candidates, dimensionMode, sincereThreshold, numVoters, useBasicStrategy, strategyType, voterSeed]);

    // Run Monte Carlo simulation for sincere threshold
    const runSincereMonteCarlo = () => {
        const candNames = Object.keys(candidates);
        const winCounts = {};
        candNames.forEach(name => winCounts[name] = 0);

        for (let sim = 0; sim < sincereMCSimulations; sim++) {
            // Generate uniformly distributed voters for this simulation
            const voters = [];
            for (let i = 0; i < numVoters; i++) {
                if (dimensionMode === '1d') {
                    voters.push(Math.random());
                } else {
                    voters.push({ x: Math.random(), y: Math.random() });
                }
            }

            // Helper to compute distance
            const computeDistance = (voterPos, candPos) => {
                if (dimensionMode === '1d') {
                    return Math.abs(voterPos - candPos);
                } else {
                    return Math.sqrt((voterPos.x - candPos.x) ** 2 + (voterPos.y - candPos.y) ** 2);
                }
            };

            // For leaderRule, compute initial poll results first
            let initialPollResults = null;
            if (strategyType === 'leaderRule') {
                const initialCounts = {};
                candNames.forEach(name => initialCounts[name] = 0);

                voters.forEach(voterPos => {
                    const distances = candNames.map(name => ({
                        name,
                        distance: computeDistance(voterPos, candidates[name])
                    }));
                    distances.forEach(d => {
                        if (d.distance <= sincereThreshold) {
                            initialCounts[d.name]++;
                        }
                    });
                });

                const sorted = Object.entries(initialCounts).sort((a, b) => b[1] - a[1]);
                initialPollResults = { leader: sorted[0][0], challenger: sorted[1][0] };
            }

            // Calculate approval votes for each voter
            const approvalCounts = {};
            candNames.forEach(name => approvalCounts[name] = 0);

            voters.forEach(voterPos => {
                // Calculate distances to all candidates
                const distances = candNames.map(name => ({
                    name,
                    distance: computeDistance(voterPos, candidates[name])
                }));

                // Sort by distance
                distances.sort((a, b) => a.distance - b.distance);

                if (strategyType === 'leaderRule' && initialPollResults) {
                    // Leader Rule Strategy
                    const { leader, challenger } = initialPollResults;
                    const leaderDist = computeDistance(voterPos, candidates[leader]);
                    const challengerDist = computeDistance(voterPos, candidates[challenger]);
                    const prefersLeader = leaderDist < challengerDist;

                    // Basic strategy: always approve closest
                    if (useBasicStrategy) {
                        approvalCounts[distances[0].name]++;
                    }

                    distances.forEach(d => {
                        if (useBasicStrategy && d.name === distances[0].name) return;
                        if (useBasicStrategy && d.name === distances[distances.length - 1].name) return;
                        if (d.distance < leaderDist) {
                            approvalCounts[d.name]++;
                        } else if (d.name === leader && prefersLeader) {
                            approvalCounts[d.name]++;
                        }
                    });
                } else if (useBasicStrategy) {
                    // Always approve closest, never approve furthest
                    approvalCounts[distances[0].name]++;

                    // Check middle candidates against threshold
                    for (let i = 1; i < distances.length - 1; i++) {
                        if (distances[i].distance <= sincereThreshold) {
                            approvalCounts[distances[i].name]++;
                        }
                    }
                } else {
                    // Default: Approve all candidates within threshold
                    distances.forEach(d => {
                        if (d.distance <= sincereThreshold) {
                            approvalCounts[d.name]++;
                        }
                    });
                }
            });

            // Find winner for this simulation
            const sorted = Object.entries(approvalCounts).sort((a, b) => b[1] - a[1]);
            winCounts[sorted[0][0]]++;
        }

        setSincereMCResults(winCounts);
    };

    // Run turn-based AV strategy simulation
    const runTurnBasedSimulation = () => {
        const candNames = Object.keys(candidates);
        const voters = sincereThresholdResults.voters;

        const maxSteps = 50;
        const epsilon = 0.001;
        const steps = [];

        // Helper to compute distance based on dimension mode
        const computeDistance = (voterPos, candPos) => {
            if (dimensionMode === '1d') {
                return Math.abs(voterPos - candPos);
            } else {
                return Math.sqrt((voterPos.x - candPos.x) ** 2 + (voterPos.y - candPos.y) ** 2);
            }
        };

        // Simple seeded random number generator (needed for async updates)
        const seededRandom = (seed) => {
            let state = seed;
            return () => {
                state = (state * 1664525 + 1013904223) % 4294967296;
                return state / 4294967296;
            };
        };

        // Step 0: Initial sincere voting with current threshold
        let currentThresholds = voters.map(() => sincereThreshold);

        // For leaderRule, we track the frontrunners each voter is responding to
        // This allows async updates where some voters update to new polls and some don't
        let voterFrontrunners = voters.map(() => null); // null = sincere voting (step 0)

        // Global frontrunners for reference
        let currentFrontrunners = null;

        // computeStep can accept either a single frontrunners object (all voters use same)
        // or an array of frontrunners per voter (for async leader rule updates)
        const computeStep = (thresholds, frontrunners = null, perVoterFrontrunners = null) => {
            const approvalCounts = {};
            candNames.forEach(name => approvalCounts[name] = 0);

            const votesPerVoter = [];
            const ballotCounts = {}; // Track unique ballots and their counts
            const voterBallots = []; // Track each voter's ballot

            voters.forEach((voterPos, voterIdx) => {
                const voterThreshold = thresholds[voterIdx];
                // Use per-voter frontrunners if provided, otherwise use global frontrunners
                const voterFR = perVoterFrontrunners ? perVoterFrontrunners[voterIdx] : frontrunners;

                // Calculate distances to all candidates
                const distances = candNames.map(name => ({
                    name,
                    distance: computeDistance(voterPos, candidates[name])
                }));

                distances.sort((a, b) => a.distance - b.distance);

                let approvedCount = 0;
                const approvedCandidates = [];

                // Determine which candidates to approve based on strategy
                if (strategyType === 'leaderRule' && voterFR) {
                    // Leader Rule Strategy:
                    // 1. Identify the current leader (x1) and challenger (x2) from frontrunners
                    // 2. Approve everyone preferred STRICTLY to x1
                    // 3. If voter prefers x1 > x2, also approve x1
                    //    If voter prefers x2 > x1, do NOT approve x1

                    const { leader, challenger } = voterFR;
                    const leaderDist = computeDistance(voterPos, candidates[leader]);
                    const challengerDist = computeDistance(voterPos, candidates[challenger]);
                    const prefersLeader = leaderDist < challengerDist;

                    // Basic strategy: always approve closest
                    if (useBasicStrategy) {
                        approvalCounts[distances[0].name]++;
                        approvedCandidates.push(distances[0].name);
                        approvedCount++;
                    }

                    // Approve all candidates preferred strictly to the leader (unless already approved)
                    distances.forEach(d => {
                        if (useBasicStrategy && d.name === distances[0].name) {
                            // Already approved above
                            return;
                        }
                        // Never approve furthest if basic strategy
                        if (useBasicStrategy && d.name === distances[distances.length - 1].name) {
                            return;
                        }
                        if (d.distance < leaderDist) {
                            // Strictly preferred to leader
                            approvalCounts[d.name]++;
                            approvedCandidates.push(d.name);
                            approvedCount++;
                        } else if (d.name === leader && prefersLeader) {
                            // Approve leader only if voter prefers leader to challenger
                            approvalCounts[d.name]++;
                            approvedCandidates.push(d.name);
                            approvedCount++;
                        }
                    });
                } else if (useBasicStrategy) {
                    // Basic strategy: Always approve closest, never approve furthest
                    approvalCounts[distances[0].name]++;
                    approvedCandidates.push(distances[0].name);
                    approvedCount++;

                    // Check middle candidates against threshold
                    for (let i = 1; i < distances.length - 1; i++) {
                        if (distances[i].distance <= voterThreshold) {
                            approvalCounts[distances[i].name]++;
                            approvedCandidates.push(distances[i].name);
                            approvedCount++;
                        }
                    }
                } else {
                    // Default (threshold / polls assumption): Approve all candidates within threshold
                    distances.forEach(d => {
                        if (d.distance <= voterThreshold) {
                            approvalCounts[d.name]++;
                            approvedCandidates.push(d.name);
                            approvedCount++;
                        }
                    });
                }

                votesPerVoter.push(approvedCount);

                // Create ballot key ordered by voter's preference (distances already sorted)
                const ballotKey = approvedCandidates.join(',');
                voterBallots.push(ballotKey);

                if (ballotKey) { // Only count non-empty ballots
                    ballotCounts[ballotKey] = (ballotCounts[ballotKey] || 0) + 1;
                }
            });

            // Find winner and viable candidates (within margin of error of 1st place)
            const sorted = Object.entries(approvalCounts).sort((a, b) => b[1] - a[1]);
            const winner = sorted[0][0];
            const firstPlaceVotes = sorted[0][1];
            const marginOfError = 0.03; // 3% margin

            // Include all candidates within MOE of first place
            const threshold = firstPlaceVotes * (1 - marginOfError);
            const viableCandidates = sorted
                .filter(([cand, votes]) => votes >= threshold)
                .map(([cand, votes]) => cand);

            // Count votes distribution
            const votesDistribution = {};
            for (let i = 0; i <= candNames.length; i++) {
                votesDistribution[i] = 0;
            }
            votesPerVoter.forEach(count => votesDistribution[count]++);

            // Calculate mean ballot size
            const meanBallotSize = votesPerVoter.reduce((sum, v) => sum + v, 0) / voters.length;

            // Track top 2 frontrunners (as object for leaderRule)
            const frontrunnersArr = sorted.slice(0, 2).map(([cand]) => cand);
            const frontrunnersObj = { leader: frontrunnersArr[0], challenger: frontrunnersArr[1] };

            return {
                approvalCounts,
                winner,
                viableCandidates,
                votesDistribution,
                meanBallotSize,
                ballotCounts,
                voterBallots,
                frontrunners: frontrunnersArr,
                frontrunnersObj
            };
        };

        // For leaderRule, first compute initial sincere votes to get frontrunners
        // This is used for step 1 onwards, not step 0
        let initialFrontrunners = null;
        if (strategyType === 'leaderRule') {
            // Compute initial sincere threshold results to get frontrunners
            const initialCounts = {};
            candNames.forEach(name => initialCounts[name] = 0);

            voters.forEach(voterPos => {
                const distances = candNames.map(name => ({
                    name,
                    distance: computeDistance(voterPos, candidates[name])
                }));
                // For initial poll, use basic strategy if enabled, otherwise pure threshold
                if (useBasicStrategy) {
                    distances.sort((a, b) => a.distance - b.distance);
                    initialCounts[distances[0].name]++;
                    for (let i = 1; i < distances.length - 1; i++) {
                        if (distances[i].distance <= sincereThreshold) {
                            initialCounts[distances[i].name]++;
                        }
                    }
                } else {
                    distances.forEach(d => {
                        if (d.distance <= sincereThreshold) {
                            initialCounts[d.name]++;
                        }
                    });
                }
            });

            const sorted = Object.entries(initialCounts).sort((a, b) => b[1] - a[1]);
            initialFrontrunners = { leader: sorted[0][0], challenger: sorted[1][0] };
        }

        // Compute initial step (step 0) - this is SINCERE voting for all strategies
        // For leaderRule, we don't pass frontrunners so it falls through to threshold-based voting
        const step0 = computeStep(currentThresholds, null, voterFrontrunners);
        steps.push({
            stepNumber: 0,
            ...step0,
            thresholds: [...currentThresholds],
            voterFrontrunners: [...voterFrontrunners]
        });

        // Set up frontrunners for step 1 based on step 0 results
        if (strategyType === 'leaderRule') {
            // Use step 0's results as the "poll" that voters see
            currentFrontrunners = step0.frontrunnersObj;
        }

        // Helper to determine what candidates a voter is currently approving
        const getCurrentApprovals = (voterPos, threshold, frontrunners = null) => {
            const distances = candNames.map(name => ({
                name,
                distance: computeDistance(voterPos, candidates[name])
            }));
            distances.sort((a, b) => a.distance - b.distance);

            const approved = new Set();

            if (strategyType === 'leaderRule' && frontrunners) {
                // Leader Rule Strategy
                const { leader, challenger } = frontrunners;
                const leaderDist = computeDistance(voterPos, candidates[leader]);
                const challengerDist = computeDistance(voterPos, candidates[challenger]);
                const prefersLeader = leaderDist < challengerDist;

                // Basic strategy: always approve closest
                if (useBasicStrategy) {
                    approved.add(distances[0].name);
                }

                distances.forEach(d => {
                    if (useBasicStrategy && d.name === distances[0].name) return;
                    if (useBasicStrategy && d.name === distances[distances.length - 1].name) return;
                    if (d.distance < leaderDist) {
                        approved.add(d.name);
                    } else if (d.name === leader && prefersLeader) {
                        approved.add(d.name);
                    }
                });
            } else if (useBasicStrategy) {
                // Always approve closest
                approved.add(distances[0].name);

                // Check middle candidates against threshold
                for (let i = 1; i < distances.length - 1; i++) {
                    if (distances[i].distance <= threshold) {
                        approved.add(distances[i].name);
                    }
                }
                // Never approve furthest (skip distances[distances.length - 1])
            } else {
                // Approve all candidates within threshold
                distances.forEach(d => {
                    if (d.distance <= threshold) {
                        approved.add(d.name);
                    }
                });
            }

            return approved;
        };

        // Iterate until convergence
        for (let stepNum = 1; stepNum <= maxSteps; stepNum++) {
            const prevStep = steps[steps.length - 1];

            // Each voter adjusts strategy based on the selected strategyType
            let newThresholds;
            let newFrontrunners = currentFrontrunners;

            if (strategyType === 'leaderRule') {
                // For leaderRule, voters respond to the current frontrunners from the previous step
                // Update frontrunners based on previous results - this is what voters "see" in the polls
                newFrontrunners = prevStep.frontrunnersObj;
                // Thresholds are still used for basic strategy constraint on middle candidates
                // But for async updates, we need to track which frontrunners each voter is responding to
                newThresholds = voters.map(() => sincereThreshold);
            } else {
                // Default: Approval Polls Assumption strategy
                newThresholds = voters.map((voterPos, voterIdx) => {
                    // Get voter's ranking by distance
                    const allDistances = candNames.map(candName => ({
                        name: candName,
                        distance: computeDistance(voterPos, candidates[candName])
                    }));
                    allDistances.sort((a, b) => a.distance - b.distance);

                    // Identify top two candidates from the election results
                    const sortedResults = Object.entries(prevStep.approvalCounts).sort((a, b) => b[1] - a[1]);
                    const topCandidate = sortedResults[0][0]; // Candidate A (top in election)
                    const secondCandidate = sortedResults[1][0]; // Candidate B (second in election)

                    // Determine which one the voter prefers
                    const topCandDist = computeDistance(voterPos, candidates[topCandidate]);
                    const secondCandDist = computeDistance(voterPos, candidates[secondCandidate]);

                    const preferredTopTwo = topCandDist <= secondCandDist ? topCandidate : secondCandidate;

                    // Check what this voter is currently approving
                    const currentThreshold = prevStep.thresholds[voterIdx];
                    const currentApprovals = getCurrentApprovals(voterPos, currentThreshold, currentFrontrunners);

                    const currentlyApprovingTop = currentApprovals.has(topCandidate);
                    const currentlyApprovingSecond = currentApprovals.has(secondCandidate);

                    // Rule 2: If currently approving exactly one of the top two, don't change
                    const approvingExactlyOne = (currentlyApprovingTop && !currentlyApprovingSecond) ||
                        (!currentlyApprovingTop && currentlyApprovingSecond);

                    if (approvingExactlyOne) {
                        // Keep current threshold
                        return currentThreshold;
                    }

                    // Rule 3: Approve the preferred top candidate and all those preferred to them
                    // Find distance to preferred top candidate
                    const preferredDist = computeDistance(voterPos, candidates[preferredTopTwo]);

                    // Set threshold to include preferred candidate and all closer ones
                    // Use epsilon to ensure we include the preferred candidate
                    return preferredDist + epsilon;
                });
            }

            // Apply asynchronous updates: only update a fraction of voters
            const updatedThresholds = voters.map((voterPos, voterIdx) => {
                // Check if this voter is a sincere voter (never changes strategy)
                const sincereRng = seededRandom(voterSeed * 10000 + voterIdx);
                const isSincereVoter = sincereRng() < sincereVoterProportion;

                if (isSincereVoter) {
                    // Sincere voters always use initial sincere threshold
                    return sincereThreshold;
                }

                // For strategic voters: randomly decide if this voter updates
                // (async rate applies only to strategic voters)
                const rng = seededRandom(voterSeed + stepNum * 1000 + voterIdx);
                if (rng() < asyncUpdateRate) {
                    return newThresholds[voterIdx];
                } else {
                    // Keep previous threshold
                    return prevStep.thresholds[voterIdx];
                }
            });

            // For leaderRule, also track per-voter frontrunners for async updates
            let updatedVoterFrontrunners = voterFrontrunners;
            if (strategyType === 'leaderRule') {
                updatedVoterFrontrunners = voters.map((voterPos, voterIdx) => {
                    // Check if this voter is a sincere voter (never changes strategy)
                    const sincereRng = seededRandom(voterSeed * 10000 + voterIdx);
                    const isSincereVoter = sincereRng() < sincereVoterProportion;

                    if (isSincereVoter) {
                        // Sincere voters don't update their frontrunner response
                        return prevStep.voterFrontrunners ? prevStep.voterFrontrunners[voterIdx] : null;
                    }

                    // For strategic voters: randomly decide if this voter updates
                    const rng = seededRandom(voterSeed + stepNum * 1000 + voterIdx);
                    if (rng() < asyncUpdateRate) {
                        // Update to respond to new poll results
                        return newFrontrunners;
                    } else {
                        // Keep responding to previous frontrunners
                        return prevStep.voterFrontrunners ? prevStep.voterFrontrunners[voterIdx] : null;
                    }
                });
                voterFrontrunners = updatedVoterFrontrunners;
            }

            // Update current frontrunners for leaderRule
            if (strategyType === 'leaderRule') {
                currentFrontrunners = newFrontrunners;
            }

            const currentStep = computeStep(updatedThresholds, currentFrontrunners, updatedVoterFrontrunners);

            // Update frontrunners for next iteration
            if (strategyType === 'leaderRule') {
                currentFrontrunners = currentStep.frontrunnersObj;
            }

            // Compute ballot transitions from previous step
            const transitions = {};
            let votersUnchanged = 0;

            voters.forEach((voterPos, voterIdx) => {
                const prevBallot = prevStep.voterBallots[voterIdx];
                const currBallot = currentStep.voterBallots[voterIdx];

                if (prevBallot === currBallot) {
                    votersUnchanged++;
                } else {
                    const transitionKey = `${prevBallot || '∅'}→${currBallot || '∅'}`;
                    transitions[transitionKey] = (transitions[transitionKey] || 0) + 1;
                }
            });

            steps.push({
                stepNumber: stepNum,
                ...currentStep,
                thresholds: [...updatedThresholds],
                voterFrontrunners: strategyType === 'leaderRule' ? [...updatedVoterFrontrunners] : null,
                transitions,
                votersUnchanged
            });

            // Check for convergence: no voter changed their ballot
            // Compare ballot counts between steps - if identical, no one changed
            const prevBallots = Object.keys(prevStep.ballotCounts).sort().join('|');
            const currBallots = Object.keys(currentStep.ballotCounts).sort().join('|');
            const ballotsMatch = prevBallots === currBallots &&
                Object.keys(prevStep.ballotCounts).every(
                    ballot => prevStep.ballotCounts[ballot] === currentStep.ballotCounts[ballot]
                );

            if (ballotsMatch) {
                break;
            }
        }

        setTurnBasedResults(steps);
        setCurrentStep(steps.length - 1); // Show final step by default
    };

    const redistributeVoters = () => {
        setVoterSeed(prev => prev + 1);
        setTurnBasedResults(null);
        setSincereMCResults(null);
        setCurrentStep(0);
    };

    const copyUrlToClipboard = async () => {
        try {
            const url = window.location.href;
            if (navigator.clipboard && navigator.clipboard.writeText) {
                await navigator.clipboard.writeText(url);
            } else {
                const ta = document.createElement('textarea');
                ta.value = url;
                ta.style.position = 'fixed';
                ta.style.left = '-9999px';
                document.body.appendChild(ta);
                ta.select();
                document.execCommand('copy');
                document.body.removeChild(ta);
            }
            setCopyStatus('Copied!');
            setTimeout(() => setCopyStatus(''), 2000);
        } catch (err) {
            console.warn('Copy failed', err);
            setCopyStatus('Failed');
            setTimeout(() => setCopyStatus(''), 2000);
        }
    };

    const resetToDefaults = () => {
        // Reset all state to defaults
        setNumCandidates(3);
        setDimensionMode('1d');
        // 1D positions
        setC1(0.2);
        setC2(0.5);
        setC3(0.8);
        setC4(0.95);
        // 2D positions
        setC1_2d({ x: 0.2, y: 0.3 });
        setC2_2d({ x: 0.5, y: 0.7 });
        setC3_2d({ x: 0.8, y: 0.4 });
        setC4_2d({ x: 0.3, y: 0.8 });
        // Labels
        setLabel1('A');
        setLabel2('B');
        setLabel3('C');
        setLabel4('D');
        setNumSimulations(1000);
        setDistributionType('uniform');
        setMaxRanksAllowed(4);
        // Strategy
        setStrategyType('threshold');
        setUseBasicStrategy(false);
        setSincereThreshold(0.15);
        setAsyncUpdateRate(0.4);
        setSincereVoterProportion(0.0);
        // Clear simulation results
        setTurnBasedResults(null);
        setSincereMCResults(null);

        // Clear URL parameters
        window.history.replaceState({}, '', window.location.pathname);

        setCopyStatus('Reset!');
        setTimeout(() => setCopyStatus(''), 2000);
    };

    return (
        <div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto', fontFamily: 'system-ui', backgroundColor: '#0f172a', color: '#e2e8f0', minHeight: '100vh' }}>
            <h1 style={{ fontSize: '24px', fontWeight: 'bold', marginBottom: '20px', color: '#f1f5f9' }}>AVLab - Approval Voting Strategy Analyzer</h1>

            <div style={{ marginBottom: '20px', backgroundColor: '#1e293b', padding: '15px', borderRadius: '8px', border: '1px solid #334155' }}>
                <div style={{ display: 'flex', gap: '10px', marginBottom: '15px', alignItems: 'center', flexWrap: 'wrap' }}>
                    <span style={{ fontWeight: '600', color: '#f1f5f9' }}>Number of Candidates: {numCandidates}</span>
                    <button
                        onClick={removeCandidate}
                        disabled={numCandidates <= 2}
                        style={{
                            padding: '6px 12px',
                            borderRadius: '4px',
                            backgroundColor: numCandidates <= 2 ? '#475569' : '#ef4444',
                            border: 'none',
                            color: '#fff',
                            fontWeight: '600',
                            cursor: numCandidates <= 2 ? 'not-allowed' : 'pointer',
                            opacity: numCandidates <= 2 ? 0.5 : 1
                        }}
                    >
                        Remove Candidate
                    </button>
                    <button
                        onClick={addCandidate}
                        disabled={numCandidates >= 4}
                        style={{
                            padding: '6px 12px',
                            borderRadius: '4px',
                            backgroundColor: numCandidates >= 4 ? '#475569' : '#10b981',
                            border: 'none',
                            color: '#fff',
                            fontWeight: '600',
                            cursor: numCandidates >= 4 ? 'not-allowed' : 'pointer',
                            opacity: numCandidates >= 4 ? 0.5 : 1
                        }}
                    >
                        Add Candidate
                    </button>
                    <div style={{ marginLeft: '20px', display: 'flex', gap: '8px', alignItems: 'center' }}>
                        <span style={{ fontWeight: '600', color: '#f1f5f9' }}>Space:</span>
                        <button
                            onClick={() => setDimensionMode('1d')}
                            style={{
                                padding: '6px 12px',
                                borderRadius: '4px',
                                backgroundColor: dimensionMode === '1d' ? '#3b82f6' : '#475569',
                                border: 'none',
                                color: '#fff',
                                fontWeight: '600',
                                cursor: 'pointer'
                            }}
                        >
                            1D Axis
                        </button>
                        <button
                            onClick={() => setDimensionMode('2d')}
                            style={{
                                padding: '6px 12px',
                                borderRadius: '4px',
                                backgroundColor: dimensionMode === '2d' ? '#3b82f6' : '#475569',
                                border: 'none',
                                color: '#fff',
                                fontWeight: '600',
                                cursor: 'pointer'
                            }}
                        >
                            2D Plane
                        </button>
                    </div>
                </div>
            </div>

            {/* 1D Controls and Visualization */}
            {dimensionMode === '1d' && (
                <>
                    <div style={{ marginBottom: '20px' }}>
                        <div style={{ marginBottom: '15px' }}>
                            <label style={{ display: 'block', fontWeight: '600', marginBottom: '5px', color: '#f1f5f9' }}>
                                C1 = {c1.toFixed(3)}
                            </label>
                            <input
                                type="range"
                                min="0.01"
                                max={Math.min(0.99, c2 - 0.01)}
                                step="0.01"
                                value={c1}
                                onChange={e => setC1(parseFloat(e.target.value))}
                                style={{ width: '100%' }}
                            />
                        </div>

                        <div style={{ marginBottom: '15px' }}>
                            <label style={{ display: 'block', fontWeight: '600', marginBottom: '5px', color: '#f1f5f9' }}>
                                C2 = {c2.toFixed(3)}
                            </label>
                            <input
                                type="range"
                                min={Math.max(0.01, c1 + 0.01)}
                                max={numCandidates >= 3 ? Math.min(0.99, c3 - 0.01) : 0.99}
                                step="0.01"
                                value={c2}
                                onChange={e => setC2(parseFloat(e.target.value))}
                                style={{ width: '100%' }}
                            />
                        </div>

                        {numCandidates >= 3 && (
                            <div style={{ marginBottom: '15px' }}>
                                <label style={{ display: 'block', fontWeight: '600', marginBottom: '5px', color: '#f1f5f9' }}>
                                    C3 = {c3.toFixed(3)}
                                </label>
                                <input
                                    type="range"
                                    min={Math.max(0.01, c2 + 0.01)}
                                    max={numCandidates >= 4 ? Math.min(0.99, c4 - 0.01) : 0.99}
                                    step="0.01"
                                    value={c3}
                                    onChange={e => setC3(parseFloat(e.target.value))}
                                    style={{ width: '100%' }}
                                />
                            </div>
                        )}

                        {numCandidates >= 4 && (
                            <div style={{ marginBottom: '15px' }}>
                                <label style={{ display: 'block', fontWeight: '600', marginBottom: '5px', color: '#f1f5f9' }}>
                                    C4 = {c4.toFixed(3)}
                                </label>
                                <input
                                    type="range"
                                    min={Math.max(0.01, c3 + 0.01)}
                                    max="0.99"
                                    step="0.01"
                                    value={c4}
                                    onChange={e => setC4(parseFloat(e.target.value))}
                                    style={{ width: '100%' }}
                                />
                            </div>
                        )}
                    </div>

                    <div style={{ backgroundColor: '#1e293b', padding: '20px', borderRadius: '8px', marginBottom: '20px', border: '1px solid #334155' }}>
                        <h2 style={{ fontSize: '18px', fontWeight: 'bold', marginBottom: '5px', color: '#f1f5f9' }}>Interval [0,1]</h2>
                        <p style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '5px' }}>
                            <strong>Drag candidates</strong> to change positions. Indifference points shown in purple.
                        </p>
                        <p>
                            Candidate positions: {Object.entries(candidates).map(([key, val], idx) => (
                                <span key={key} style={{ color: colors[parseInt(key.slice(1)) - 1] }}> {getLabel(key)}={val.toFixed(3)}{idx < Object.entries(candidates).length - 1 ? ', ' : ''}</span>
                            ))}
                        </p>
                        <p style={{ fontSize: '11px', color: '#94a3b8', marginBottom: '15px' }}>
                            Indifference points: {Object.entries(indifferencePoints).map(([key, val], idx) => (
                                <span key={key}>{idx > 0 ? ', ' : ''}{key.toUpperCase()}={val.toFixed(3)}</span>
                            ))}
                        </p>
                        {specialRegions && (
                            <p style={{ fontSize: '11px', color: '#fbbf24', marginBottom: '15px' }}>
                                <strong>Center winning regions</strong> (yellow): Where {specialRegions.middleLabel} can win.
                                ε = ({specialRegions.rightLabel} - {specialRegions.leftLabel} - 0.5) = {specialRegions.eps.toFixed(3)}:
                                {specialRegions.regions.map((r, i) => {
                                    const formula = i === 0 ? `0.5-${getLabel(r.candId)}` : `1.5-${getLabel(r.candId)}`;
                                    return (
                                        <div key={i} style={{ marginTop: '6px' }}>
                                            dist({specialRegions.middleLabel},{formula}) = dist({specialRegions.middleLabel}, {r.center.toFixed(3)}) ≤ ε
                                        </div>
                                    );
                                })}
                            </p>
                        )}
                        <svg ref={svgRef} width="100%" height="160" style={{ display: 'block', cursor: dragging ? 'grabbing' : 'default', touchAction: 'none', backgroundColor: '#0f172a' }}>
                            {/* Ranking regions - showing where each voter type is located */}
                            {rankingRegions.map((region, idx) => {
                                const width = (region.end - region.start) * 100;
                                const x = region.start * 100;
                                const regionColors = {
                                    'C1>C2>C3': '#1e3a8a',
                                    'C1>C3>C2': '#831843',
                                    'C2>C1>C3': '#14532d',
                                    'C2>C3>C1': '#713f12',
                                    'C3>C1>C2': '#7c2d12',
                                    'C3>C2>C1': '#3730a3'
                                };
                                return (
                                    <g key={idx}>
                                        <rect
                                            x={x + '%'}
                                            y="10"
                                            width={width + '%'}
                                            height="30"
                                            fill={regionColors[region.ranking] || '#374151'}
                                            stroke="#64748b"
                                            strokeWidth="0.5"
                                            opacity="0.8"
                                        />
                                        {width > 8 && (
                                            <text
                                                x={(x + width / 2) + '%'}
                                                y="28"
                                                fontSize="9"
                                                textAnchor="middle"
                                                fill="#e2e8f0"
                                                fontFamily="monospace"
                                                fontWeight="600"
                                            >
                                                {formatRankingCompact(region.ranking)}
                                            </text>
                                        )}
                                    </g>
                                );
                            })}

                            {/* Main interval line */}
                            <line x1="0" y1="60" x2="100%" y2="60" stroke="#cbd5e1" strokeWidth="2" />

                            {/* Endpoint markers */}
                            <line x1="0" y1="55" x2="0" y2="65" stroke="#cbd5e1" strokeWidth="2" />
                            <text x="0" y="75" fontSize="10" textAnchor="middle" fill="#e2e8f0">0</text>

                            <line x1="50%" y1="55" x2="50%" y2="65" stroke="#cbd5e1" strokeWidth="2" />
                            <text x="50%" y="75" fontSize="10" textAnchor="middle" fill="#e2e8f0">0.5</text>

                            <line x1="100%" y1="55" x2="100%" y2="65" stroke="#cbd5e1" strokeWidth="2" />
                            <text x="100%" y="75" fontSize="10" textAnchor="end" fill="#e2e8f0">1</text>

                            {/* Special regions for 3-candidate case */}
                            {specialRegions && specialRegions.regions.map((region, idx) => {
                                const width = (region.end - region.start) * 100;
                                const x = region.start * 100;
                                const centerX = region.center * 100;
                                return (
                                    <g key={`special-${idx}`}>
                                        <rect
                                            x={x + '%'}
                                            y="85"
                                            width={width + '%'}
                                            height="10"
                                            fill="#fbbf24"
                                            opacity="0.6"
                                            stroke="#f59e0b"
                                            strokeWidth="1"
                                        />
                                        <line
                                            x1={centerX + '%'}
                                            y1="83"
                                            x2={centerX + '%'}
                                            y2="97"
                                            stroke="#f59e0b"
                                            strokeWidth="1.5"
                                        />
                                        <text
                                            x={centerX + '%'}
                                            y="105"
                                            fontSize="8"
                                            fill="#fbbf24"
                                            textAnchor="middle"
                                        >
                                            {region.center.toFixed(3)}
                                        </text>
                                    </g>
                                );
                            })}

                            {/* Indifference points (not draggable) */}
                            {Object.entries(indifferencePoints).map(([key, value]) => {
                                const label = key.toUpperCase();
                                return (
                                    <g key={key}>
                                        <line x1={value * 100 + '%'} y1="55" x2={value * 100 + '%'} y2="65" stroke="#a78bfa" strokeWidth="1.5" strokeDasharray="2,2" />
                                        <text x={value * 100 + '%'} y="50" fontSize="8" fill="#a78bfa" textAnchor="middle">{label}</text>
                                    </g>
                                );
                            })}

                            {/* Candidates (draggable) */}
                            {Object.entries(candidates).map(([candId, position], idx) => {
                                const candKey = candId.toLowerCase();
                                return (
                                    <g
                                        key={candId}
                                        onPointerDown={handlePointerDown(candKey)}
                                        onTouchStart={handlePointerDown(candKey)}
                                        style={{ cursor: 'grab', touchAction: 'none' }}
                                    >
                                        <circle cx={position * 100 + '%'} cy="100" r="8" fill={colors[idx]} stroke="#1e293b" strokeWidth="2" opacity="0.9" />
                                        <text x={position * 100 + '%'} y="120" fontSize="11" fontWeight="bold" textAnchor="middle" fill="#e2e8f0">{getLabel(candId)}</text>
                                    </g>
                                );
                            })}

                            {/* Legend for ranking regions */}
                            <text x="0" y="145" fontSize="9" fill="#94a3b8">Voter Rankings by Location</text>
                        </svg>
                    </div>
                </>
            )}

            {/* 2D Controls and Visualization */}
            {dimensionMode === '2d' && (
                <div style={{ backgroundColor: '#1e293b', padding: '20px', borderRadius: '8px', marginBottom: '20px', border: '1px solid #334155' }}>
                    <h2 style={{ fontSize: '18px', fontWeight: 'bold', marginBottom: '5px', color: '#f1f5f9' }}>Plane [0,1]×[0,1]</h2>
                    <p style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '5px' }}>
                        <strong>Drag candidates</strong> to change positions. Colors show Voronoi regions (closest candidate). Indifference lines shown in purple.
                    </p>
                    <p style={{ fontSize: '11px', color: '#94a3b8', marginBottom: '15px' }}>
                        Candidate positions: {Object.entries(candidates).map(([id, pos], idx) => (
                            <span key={id}>{idx > 0 ? ', ' : ''}{getLabel(id)}=({pos.x.toFixed(2)},{pos.y.toFixed(2)})</span>
                        ))}
                    </p>
                    <svg ref={svg2dRef} width="100%" height="500" viewBox="0 0 500 500" style={{ display: 'block', cursor: dragging ? 'grabbing' : 'default', touchAction: 'none', backgroundColor: '#0f172a', border: '1px solid #334155' }}>
                        {/* Voronoi regions - render as colored pixels */}
                        {(() => {
                            const candNames = Object.keys(candidates);
                            const gridSize = 50; // 50x50 grid for performance
                            const cellSize = 500 / gridSize;
                            const regionColors = {
                                'C1': colors[0],
                                'C2': colors[1],
                                'C3': colors[2],
                                'C4': colors[3]
                            };
                            const cells = [];

                            for (let ix = 0; ix < gridSize; ix++) {
                                for (let iy = 0; iy < gridSize; iy++) {
                                    const px = (ix + 0.5) / gridSize;
                                    const py = (iy + 0.5) / gridSize;

                                    // Find closest candidate
                                    let minDist = Infinity;
                                    let closest = candNames[0];
                                    for (const name of candNames) {
                                        const pos = candidates[name];
                                        const dist = Math.sqrt((px - pos.x) ** 2 + (py - pos.y) ** 2);
                                        if (dist < minDist) {
                                            minDist = dist;
                                            closest = name;
                                        }
                                    }

                                    cells.push(
                                        <rect
                                            key={`${ix}-${iy}`}
                                            x={ix * cellSize}
                                            y={iy * cellSize}
                                            width={cellSize}
                                            height={cellSize}
                                            fill={regionColors[closest]}
                                            opacity="0.3"
                                        />
                                    );
                                }
                            }
                            return cells;
                        })()}

                        {/* Indifference lines (perpendicular bisectors) */}
                        {Object.entries(indifferencePoints).map(([key, line]) => {
                            const { midpoint, slope, c1: cand1, c2: cand2 } = line;
                            let x1, y1, x2, y2;

                            if (slope === 'vertical') {
                                x1 = midpoint.x * 500;
                                y1 = 0;
                                x2 = midpoint.x * 500;
                                y2 = 500;
                            } else {
                                // Line equation: y - my = slope * (x - mx)
                                // Find intersections with boundaries
                                const mx = midpoint.x * 500;
                                const my = midpoint.y * 500;
                                const m = slope;

                                // Calculate line endpoints extending to boundaries
                                // y = my + m * (x - mx)
                                // At x = 0: y = my - m * mx
                                // At x = 500: y = my + m * (500 - mx)
                                const y_at_0 = my + m * (0 - mx);
                                const y_at_500 = my + m * (500 - mx);

                                // Clamp to viewport
                                const points = [];
                                if (y_at_0 >= 0 && y_at_0 <= 500) points.push({ x: 0, y: y_at_0 });
                                if (y_at_500 >= 0 && y_at_500 <= 500) points.push({ x: 500, y: y_at_500 });

                                // At y = 0: x = mx - my/m
                                // At y = 500: x = mx + (500 - my)/m
                                if (Math.abs(m) > 0.001) {
                                    const x_at_0 = mx - my / m;
                                    const x_at_500 = mx + (500 - my) / m;
                                    if (x_at_0 >= 0 && x_at_0 <= 500) points.push({ x: x_at_0, y: 0 });
                                    if (x_at_500 >= 0 && x_at_500 <= 500) points.push({ x: x_at_500, y: 500 });
                                }

                                if (points.length >= 2) {
                                    x1 = points[0].x;
                                    y1 = points[0].y;
                                    x2 = points[1].x;
                                    y2 = points[1].y;
                                } else {
                                    return null;
                                }
                            }

                            return (
                                <g key={key}>
                                    <line
                                        x1={x1}
                                        y1={y1}
                                        x2={x2}
                                        y2={y2}
                                        stroke="#a78bfa"
                                        strokeWidth="2"
                                        strokeDasharray="4,4"
                                    />
                                    <text
                                        x={midpoint.x * 500}
                                        y={midpoint.y * 500 - 8}
                                        fontSize="10"
                                        fill="#a78bfa"
                                        textAnchor="middle"
                                    >
                                        {key.toUpperCase()}
                                    </text>
                                </g>
                            );
                        })}

                        {/* Grid lines */}
                        <line x1="250" y1="0" x2="250" y2="500" stroke="#475569" strokeWidth="1" strokeDasharray="2,2" />
                        <line x1="0" y1="250" x2="500" y2="250" stroke="#475569" strokeWidth="1" strokeDasharray="2,2" />

                        {/* Border */}
                        <rect x="0" y="0" width="500" height="500" fill="none" stroke="#64748b" strokeWidth="2" />

                        {/* Axis labels */}
                        <text x="250" y="495" fontSize="10" textAnchor="middle" fill="#94a3b8">0.5</text>
                        <text x="5" y="250" fontSize="10" textAnchor="start" fill="#94a3b8">0.5</text>
                        <text x="5" y="12" fontSize="10" textAnchor="start" fill="#94a3b8">0</text>
                        <text x="490" y="495" fontSize="10" textAnchor="end" fill="#94a3b8">1</text>
                        <text x="5" y="495" fontSize="10" textAnchor="start" fill="#94a3b8">1</text>

                        {/* Candidates (draggable) */}
                        {Object.entries(candidates).map(([candId, position], idx) => {
                            const candKey = candId.toLowerCase();
                            return (
                                <g
                                    key={candId}
                                    onPointerDown={handlePointerDown(candKey)}
                                    onTouchStart={handlePointerDown(candKey)}
                                    style={{ cursor: 'grab', touchAction: 'none' }}
                                >
                                    <circle cx={position.x * 500} cy={position.y * 500} r="12" fill={colors[idx]} stroke="#1e293b" strokeWidth="3" />
                                    <text x={position.x * 500} y={position.y * 500 + 25} fontSize="12" fontWeight="bold" textAnchor="middle" fill="#e2e8f0">{getLabel(candId)}</text>
                                </g>
                            );
                        })}
                    </svg>
                </div>
            )}

            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '15px', marginBottom: '20px' }}>
                <div style={{ backgroundColor: '#14532d', padding: '15px', borderRadius: '8px', border: '1px solid #16a34a' }}>
                    <h3 style={{ fontSize: '16px', fontWeight: 'bold', marginBottom: '5px', color: '#86efac' }}>
                        IRV Rounds
                        {condorcetInfo.winner !== 'None' && (
                            <span style={{ fontSize: '12px', fontWeight: 'normal', marginLeft: '8px', color: '#4ade80' }}>
                                (Condorcet Winner: {getLabel(condorcetInfo.winner)})
                            </span>
                        )}
                    </h3>
                    {dimensionMode === '1d' && (
                        <div style={{ fontSize: '11px', color: '#86efac', marginBottom: '8px' }}>
                            Positions: {Object.entries(candidates).map(([id, pos], idx) => (
                                <span key={id}>{idx > 0 ? ', ' : ''}{getLabel(id)}={pos.toFixed(3)}</span>
                            ))}
                        </div>
                    )}
                    {dimensionMode === '2d' && (
                        <div style={{ fontSize: '11px', color: '#86efac', marginBottom: '8px' }}>
                            Positions: {Object.entries(candidates).map(([id, pos], idx) => (
                                <span key={id}>{idx > 0 ? ', ' : ''}{getLabel(id)}=({pos.x.toFixed(2)},{pos.y.toFixed(2)})</span>
                            ))}
                        </div>
                    )}
                    <div style={{ marginBottom: '10px' }}>
                        <label style={{ display: 'block', fontSize: '12px', fontWeight: '600', marginBottom: '5px', color: '#86efac' }}>
                            Max Ranks Allowed: {maxRanksAllowed}
                        </label>
                        <input
                            type="range"
                            min="1"
                            max={Object.keys(candidates).length}
                            step="1"
                            value={maxRanksAllowed}
                            onChange={(e) => setMaxRanksAllowed(parseInt(e.target.value))}
                            style={{ width: '100%', cursor: 'pointer' }}
                        />
                    </div>
                    {rcv.map((round, i) => {
                        const totalNonExhausted = 1 - (round.exhausted || 0);
                        return (
                            <div key={i} style={{ fontSize: '13px', marginBottom: '8px', color: '#e2e8f0' }}>
                                {round.winner ? (
                                    <div>
                                        <div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '4px' }}>
                                            <span style={{ fontSize: '16px' }}>🥇</span>
                                            <strong style={{ color: '#4ade80' }}>Winner: {getLabel(round.winner)}</strong>
                                            {round.winner === condorcetInfo.winner && condorcetInfo.winner !== 'None' && (
                                                <span style={{ fontSize: '16px' }}>🏆</span>
                                            )}
                                        </div>
                                        {round.exhausted > 0 && (
                                            <div style={{ fontSize: '12px', color: '#cbd5e1', marginLeft: '28px' }}>
                                                Exhausted: {(round.exhausted * 100).toFixed(1)}%
                                            </div>
                                        )}
                                    </div>
                                ) : (
                                    <div>
                                        <div style={{ fontWeight: '600', marginBottom: '3px' }}>Round {i + 1}:</div>
                                        {Object.entries(round.votes)
                                            .filter(([c]) => c !== 'exhausted')
                                            .sort((a, b) => b[1] - a[1])
                                            .map(([c, v], idx) => {
                                                const pctTotal = (v * 100).toFixed(1);
                                                const pctNonExhausted = totalNonExhausted > 0 ? ((v / totalNonExhausted) * 100).toFixed(1) : '0.0';
                                                return (
                                                    <div key={c} style={{ display: 'flex', alignItems: 'center', gap: '8px', justifyContent: 'space-between' }}>
                                                        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
                                                            <div style={{ width: '32px', display: 'flex', justifyContent: 'flex-start', alignItems: 'center', gap: '2px' }}>
                                                                {round.eliminated === c && <span style={{ color: '#ef4444', fontWeight: 'bold', fontSize: '16px' }}>✗</span>}
                                                                {idx === 0 && !round.eliminated && <span style={{ fontSize: '14px' }}>⭐</span>}
                                                                {c === condorcetInfo.winner && condorcetInfo.winner !== 'None' && (
                                                                    <span style={{ fontSize: '14px' }}>🏆</span>
                                                                )}
                                                            </div>
                                                            <div style={{ minWidth: '60px' }}>{getLabel(c)}</div>
                                                            <div>
                                                                {pctTotal}%
                                                                {round.exhausted > 0 && (
                                                                    <span style={{ color: '#86efac', marginLeft: '4px' }}>({pctNonExhausted}%)</span>
                                                                )}
                                                            </div>
                                                        </div>
                                                        {round.eliminated === c && c === condorcetInfo.winner && condorcetInfo.winner !== 'None' && (
                                                            <div style={{ fontSize: '11px', color: '#fbbf24', fontStyle: 'italic' }}>(Condorcet Winner Eliminated)</div>
                                                        )}
                                                    </div>
                                                );
                                            })}
                                        {round.exhausted > 0 && (
                                            <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '4px', paddingLeft: '40px' }}>
                                                <div style={{ color: '#94a3b8', fontSize: '12px' }}>
                                                    Exhausted: {(round.exhausted * 100).toFixed(1)}%
                                                </div>
                                            </div>
                                        )}
                                    </div>
                                )}
                            </div>
                        );
                    })}
                </div>

                <div style={{ backgroundColor: '#1e3a5f', padding: '15px', borderRadius: '8px', border: '1px solid #2563eb' }}>
                    <h3 style={{ fontSize: '16px', fontWeight: 'bold', marginBottom: '10px', color: '#93c5fd' }}>
                        Condorcet Winner: {getLabel(condorcetInfo.winner)}
                    </h3>
                    {groupedPairwise.map((group, idx) => (
                        <div key={group.candidate}>
                            <div style={{ fontWeight: 'bold', fontSize: '14px', marginTop: idx > 0 ? '8px' : '0', marginBottom: '4px', color: group.candidate === condorcetInfo.winner ? '#60a5fa' : '#cbd5e1' }}>
                                {getLabel(group.candidate)} ({group.wins} wins)
                            </div>
                            {group.matchups.map(({ matchup, result }) => {
                                const parts = matchup.split(' vs ');
                                // Ensure current candidate is always first
                                const opponent = parts[0] === group.candidate ? parts[1] : parts[0];
                                const isWinner = result.winner === group.candidate;
                                return (
                                    <div key={matchup} style={{ fontSize: '13px', marginBottom: '3px', marginLeft: '8px', color: '#e2e8f0' }}>
                                        {isWinner ? (
                                            <span><strong>{getLabel(group.candidate)}</strong> vs {getLabel(opponent)}: <strong>{getLabel(result.winner)}</strong> ({result.score})</span>
                                        ) : (
                                            <span>{getLabel(group.candidate)} vs <strong>{getLabel(opponent)}</strong>: <strong>{getLabel(result.winner)}</strong> ({result.score})</span>
                                        )}
                                    </div>
                                );
                            })}
                            {idx < groupedPairwise.length - 1 && (
                                <div style={{ borderTop: '1px solid #3b82f6', marginTop: '6px' }}></div>
                            )}
                        </div>
                    ))}
                </div>
            </div>

            <div style={{ backgroundColor: '#78350f', padding: '15px', borderRadius: '8px', marginBottom: '20px', border: '1px solid #f59e0b' }}>
                <h3 style={{ fontSize: '16px', fontWeight: 'bold', marginBottom: '10px', color: '#fde68a' }}>Voter Rankings</h3>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '8px' }}>
                    {voterRankings.sort((a, b) => b.proportion - a.proportion).map(v => {
                        // Format intervals for display
                        const intervalsText = v.intervals && v.intervals.length > 0
                            ? v.intervals.map(interval => `(${interval.start.toFixed(2)},${interval.end.toFixed(2)})`).join(', ')
                            : null;

                        return (
                            <div key={v.ranking} style={{ fontSize: '13px', color: '#e2e8f0' }}>
                                <span style={{ fontFamily: 'monospace' }}>{formatRanking(v.ranking)}</span>: {(v.proportion * 100).toFixed(1)}%
                                {intervalsText && dimensionMode === '1d' && (
                                    <span style={{ color: '#fbbf24', marginLeft: '4px' }}>{intervalsText}</span>
                                )}
                            </div>
                        );
                    })}
                </div>
            </div>

            <div style={{ backgroundColor: '#581c87', padding: '15px', borderRadius: '8px', marginBottom: '20px', border: '1px solid #a855f7' }}>
                <h3 style={{ fontSize: '16px', fontWeight: 'bold', marginBottom: '5px', color: '#e9d5ff' }}>
                    Reverse Borda Scores - Winner: {getLabel(bordaWinner)} 🏆
                </h3>
                <p style={{ fontSize: '11px', color: '#d8b4fe', marginBottom: '10px' }}>
                    1 pt for 1st place, 2 pts for 2nd, 3 pts for 3rd. Lower = better (used for RCV tiebreaking).
                </p>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))', gap: '12px' }}>
                    {Object.entries(bordaScores)
                        .sort((a, b) => a[1] - b[1])
                        .map(([cand, score]) => (
                            <div key={cand} style={{ backgroundColor: '#4c1d95', padding: '12px', borderRadius: '6px', textAlign: 'center', border: cand === bordaWinner ? '2px solid #a855f7' : '1px solid #7c3aed' }}>
                                <div style={{ fontWeight: 'bold', fontSize: '14px', marginBottom: '4px', color: '#e9d5ff' }}>
                                    {getLabel(cand)}
                                    {cand === bordaWinner && <span style={{ marginLeft: '4px' }}>🏆</span>}
                                </div>
                                <div style={{ fontSize: '18px', fontWeight: 'bold', color: '#c084fc' }}>
                                    {score.toFixed(3)}
                                </div>
                            </div>
                        ))}
                </div>
            </div>

            <div style={{ backgroundColor: '#7c2d12', padding: '15px', borderRadius: '8px', border: '1px solid #f97316' }}>
                <h3 style={{ fontSize: '16px', fontWeight: 'bold', marginBottom: '10px', color: '#fed7aa' }}>AV Critical Profiles</h3>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: '15px' }}>
                    {Object.entries(avProfiles).map(([target, app]) => {
                        const sorted = Object.entries(app).sort((a, b) => b[1] - a[1]);
                        const winner = sorted[0][0];
                        return (
                            <div key={target} style={{ backgroundColor: '#431407', padding: '12px', borderRadius: '6px', border: '1px solid #ea580c' }}>
                                <div style={{ fontWeight: 'bold', marginBottom: '8px', color: '#fed7aa' }}>{getLabel(target)} Critical</div>
                                {sorted.map(([c, a]) => (
                                    <div key={c} style={{ display: 'flex', alignItems: 'center', marginBottom: '5px' }}>
                                        <span style={{ width: '50px', fontSize: '13px', fontWeight: c === winner ? 'bold' : 'normal', color: '#e2e8f0' }}>{getLabel(c)}:</span>
                                        <div style={{ flex: 1, height: '14px', backgroundColor: '#1e293b', borderRadius: '7px', overflow: 'hidden', marginRight: '8px' }}>
                                            <div style={{ width: (a * 100) + '%', height: '100%', backgroundColor: c === winner ? '#fb923c' : '#64748b' }}></div>
                                        </div>
                                        <span style={{ fontSize: '12px', width: '45px', fontWeight: c === winner ? 'bold' : 'normal', color: '#e2e8f0' }}>
                                            {(a * 100).toFixed(1)}%
                                        </span>
                                    </div>
                                ))}
                                <div style={{ marginTop: '8px', fontSize: '13px', fontWeight: 'bold', color: '#fed7aa' }}>Winner: {getLabel(winner)}</div>
                            </div>
                        );
                    })}
                </div>
            </div>



            <div style={{ backgroundColor: '#1e3a5f', padding: '15px', borderRadius: '8px', marginBottom: '20px', border: '1px solid #3b82f6' }}>
                <h3 style={{ fontSize: '16px', fontWeight: 'bold', marginBottom: '10px', color: '#93c5fd' }}>
                    Monte Carlo Simulation - Approval Voting
                </h3>
                <p style={{ fontSize: '12px', color: '#cbd5e1', marginBottom: '12px' }}>
                    Simulates approval voting where each voter bloc always approves their top choice,
                    never approves their last choice, and randomly decides whether to approve middle-ranked candidates.
                </p>

                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '15px', marginBottom: '15px' }}>
                    <div>
                        <label style={{ display: 'block', fontSize: '13px', fontWeight: '600', marginBottom: '5px', color: '#cbd5e1' }}>
                            Number of Simulations:
                        </label>
                        <input
                            type="number"
                            min="100"
                            max="10000"
                            step="100"
                            value={numSimulations}
                            onChange={(e) => setNumSimulations(parseInt(e.target.value))}
                            style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #475569', fontSize: '14px', backgroundColor: '#0f172a', color: '#e2e8f0' }}
                        />
                    </div>

                    <div>
                        <label style={{ display: 'block', fontSize: '13px', fontWeight: '600', marginBottom: '5px', color: '#cbd5e1' }}>
                            Probability Distribution:
                        </label>
                        <select
                            value={distributionType}
                            onChange={(e) => setDistributionType(e.target.value)}
                            style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #475569', fontSize: '14px', backgroundColor: '#0f172a', color: '#e2e8f0', cursor: 'pointer' }}
                        >
                            <option value="uniform">Uniform (0 to 1)</option>
                            <option value="normal">Normal (mean=0.5, std=0.2)</option>
                        </select>
                    </div>
                </div>

                <div style={{ backgroundColor: '#0f172a', padding: '15px', borderRadius: '6px', border: '1px solid #1e40af' }}>
                    <h4 style={{ fontSize: '14px', fontWeight: 'bold', marginBottom: '12px', color: '#93c5fd' }}>
                        Winner Frequency (out of {numSimulations} simulations)
                    </h4>
                    {Object.entries(monteCarloResults)
                        .filter(([cand]) => candidates.hasOwnProperty(cand))
                        .sort((a, b) => b[1] - a[1])
                        .map(([cand, wins]) => {
                            const percentage = (wins / numSimulations) * 100;
                            const maxWins = Math.max(...Object.values(monteCarloResults));
                            const isWinner = wins === maxWins;

                            return (
                                <div key={cand} style={{ marginBottom: '10px' }}>
                                    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
                                        <span style={{ fontSize: '14px', fontWeight: isWinner ? 'bold' : 'normal', color: '#e2e8f0' }}>
                                            {getLabel(cand)}
                                            {isWinner && <span style={{ marginLeft: '6px' }}>🏆</span>}
                                        </span>
                                        <span style={{ fontSize: '13px', fontWeight: isWinner ? 'bold' : 'normal', color: '#cbd5e1' }}>
                                            {wins} ({percentage.toFixed(1)}%)
                                        </span>
                                    </div>
                                    <div style={{ height: '24px', backgroundColor: '#1e293b', borderRadius: '4px', overflow: 'hidden', position: 'relative' }}>
                                        <div
                                            style={{
                                                width: percentage + '%',
                                                height: '100%',
                                                backgroundColor: isWinner ? '#3b82f6' : '#475569',
                                                transition: 'width 0.3s ease'
                                            }}
                                        ></div>
                                    </div>
                                </div>
                            );
                        })}
                </div>

                <p style={{ fontSize: '11px', color: '#94a3b8', marginTop: '12px', fontStyle: 'italic' }}>
                    {distributionType === 'uniform'
                        ? 'Uniform distribution: Each voter bloc is assigned a random approval probability (0-100%) for middle-ranked candidates in each simulation. Last-ranked candidates are never approved.'
                        : 'Normal distribution: Blocs tend to approve ~50% for middle-ranked candidates, with variation (std=0.2). Example: a bloc might approve 80% of their 2nd choice in one sim, 30% in another. Last-ranked candidates are never approved.'}
                </p>
            </div>

            <div style={{ backgroundColor: '#134e4a', padding: '15px', borderRadius: '8px', marginBottom: '20px', border: '1px solid #14b8a6' }}>
                <h3 style={{ fontSize: '16px', fontWeight: 'bold', marginBottom: '10px', color: '#5eead4' }}>
                    Sincere Threshold Simulation
                </h3>
                <p style={{ fontSize: '12px', color: '#cbd5e1', marginBottom: '12px' }}>
                    Simulates {numVoters} voters uniformly distributed on [0,1]. Each voter approves candidates within their sincere threshold distance.
                </p>

                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '15px', marginBottom: '15px' }}>
                    <div>
                        <label style={{ display: 'block', fontSize: '13px', fontWeight: '600', marginBottom: '5px', color: '#cbd5e1' }}>
                            Number of Voters: {numVoters}
                        </label>
                        <input
                            type="range"
                            min="10"
                            max="20000"
                            step="10"
                            value={numVoters}
                            onChange={(e) => setNumVoters(parseInt(e.target.value))}
                            style={{ width: '100%' }}
                        />
                    </div>

                    <div>
                        <label style={{ display: 'block', fontSize: '13px', fontWeight: '600', marginBottom: '5px', color: '#cbd5e1' }}>
                            Sincere Threshold: {sincereThreshold.toFixed(3)}
                        </label>
                        <input
                            type="range"
                            min="0.01"
                            max="1.0"
                            step="0.01"
                            value={sincereThreshold}
                            onChange={(e) => setSincereThreshold(parseFloat(e.target.value))}
                            style={{ width: '100%' }}
                        />
                    </div>
                </div>

                <div style={{ marginBottom: '15px' }}>
                    <label style={{ display: 'block', fontSize: '13px', fontWeight: '600', marginBottom: '5px', color: '#cbd5e1' }}>
                        Voter Strategy:
                    </label>
                    <select
                        value={strategyType}
                        onChange={(e) => {
                            setStrategyType(e.target.value);
                            // Reset turn-based results when strategy changes
                            setTurnBasedResults(null);
                            setSincereMCResults(null);
                        }}
                        style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #14b8a6', fontSize: '14px', backgroundColor: '#0f172a', color: '#e2e8f0', cursor: 'pointer' }}
                    >
                        <option value="threshold">Approval Polls Assumption</option>
                        <option value="leaderRule">Leader Rule</option>
                    </select>
                    <p style={{ fontSize: '11px', color: '#94a3b8', marginTop: '4px', fontStyle: 'italic' }}>
                        {strategyType === 'threshold' && 'Voters approve all candidates within their threshold distance. In turn-based mode, they strategically adjust to support their preferred frontrunner.'}
                        {strategyType === 'leaderRule' && 'Voters approve all candidates strictly preferred to the poll leader. If they prefer the leader over the challenger, they also approve the leader.'}
                    </p>
                </div>

                <div style={{ marginBottom: '15px' }}>
                    <label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', fontSize: '13px', color: '#cbd5e1' }}>
                        <input
                            type="checkbox"
                            checked={useBasicStrategy}
                            onChange={(e) => {
                                setUseBasicStrategy(e.target.checked);
                                setTurnBasedResults(null);
                                setSincereMCResults(null);
                            }}
                            style={{ width: '16px', height: '16px', cursor: 'pointer' }}
                        />
                        <span>Use basic strategy (always approve closest, never approve furthest)</span>
                    </label>
                    <p style={{ fontSize: '11px', color: '#94a3b8', marginTop: '4px', fontStyle: 'italic', marginLeft: '24px' }}>
                        When enabled, voters will always approve their closest candidate and never approve their furthest, regardless of the strategy above.
                    </p>
                </div>

                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '15px', marginBottom: '15px' }}>
                    <div>
                        <label style={{ display: 'block', fontSize: '13px', fontWeight: '600', marginBottom: '5px', color: '#cbd5e1' }}>
                            Async Update Rate: {asyncUpdateRate.toFixed(1)} ({(asyncUpdateRate * (1 - sincereVoterProportion) * 100).toFixed(1)}% of all voters update per step)
                        </label>
                        <input
                            type="range"
                            min="0.1"
                            max="1.0"
                            step="0.1"
                            value={asyncUpdateRate}
                            onChange={(e) => {
                                const v = parseFloat(e.target.value);
                                setAsyncUpdateRate(v);
                                updateUrlParam('au', v.toFixed(1));
                            }}
                            style={{ width: '100%' }}
                        />
                        <p style={{ fontSize: '11px', color: '#94a3b8', marginTop: '4px', fontStyle: 'italic' }}>
                            Applies only to strategic voters. Lower values create gradual convergence.
                        </p>
                    </div>

                    <div>
                        <label style={{ display: 'block', fontSize: '13px', fontWeight: '600', marginBottom: '5px', color: '#cbd5e1' }}>
                            Sincere Voter Proportion: {sincereVoterProportion.toFixed(1)} ({(sincereVoterProportion * 100).toFixed(0)}% never change strategy)
                        </label>
                        <input
                            type="range"
                            min="0.0"
                            max="1.0"
                            step="0.1"
                            value={sincereVoterProportion}
                            onChange={(e) => {
                                const v = parseFloat(e.target.value);
                                setSincereVoterProportion(v);
                                updateUrlParam('sv', v.toFixed(1));
                            }}
                            style={{ width: '100%' }}
                        />
                        <p style={{ fontSize: '11px', color: '#94a3b8', marginTop: '4px', fontStyle: 'italic' }}>
                            These voters always vote with their initial sincere threshold.
                        </p>
                    </div>
                </div>

                <div style={{ marginBottom: '15px', display: 'flex', gap: '10px', alignItems: 'center', flexWrap: 'wrap' }}>
                    <button
                        onClick={redistributeVoters}
                        style={{
                            padding: '8px 16px',
                            borderRadius: '6px',
                            backgroundColor: '#0891b2',
                            border: 'none',
                            color: '#fff',
                            fontWeight: '600',
                            cursor: 'pointer',
                            fontSize: '13px'
                        }}
                    >
                        Redistribute Voters
                    </button>
                    <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
                        <label style={{ fontSize: '13px', fontWeight: '600', color: '#cbd5e1' }}>
                            Monte Carlo Sims: {sincereMCSimulations}
                        </label>
                    </div>
                    <button
                        onClick={runSincereMonteCarlo}
                        style={{
                            padding: '8px 16px',
                            borderRadius: '6px',
                            backgroundColor: '#0d9488',
                            border: 'none',
                            color: '#fff',
                            fontWeight: '600',
                            cursor: 'pointer',
                            fontSize: '13px'
                        }}
                    >
                        Run Monte Carlo
                    </button>
                    <button
                        onClick={runTurnBasedSimulation}
                        style={{
                            padding: '8px 16px',
                            borderRadius: '6px',
                            backgroundColor: '#7c3aed',
                            border: 'none',
                            color: '#fff',
                            fontWeight: '600',
                            cursor: 'pointer',
                            fontSize: '13px'
                        }}
                    >
                        Run Turn-Based Simulation
                    </button>
                    {sincereMCResults && (
                        <button
                            onClick={() => setSincereMCResults(null)}
                            style={{
                                padding: '8px 16px',
                                borderRadius: '6px',
                                backgroundColor: '#475569',
                                border: 'none',
                                color: '#cbd5e1',
                                fontWeight: '600',
                                cursor: 'pointer',
                                fontSize: '13px'
                            }}
                        >
                            Clear Results
                        </button>
                    )}
                </div>

                {/* Show initial poll results for leader rule */}
                {strategyType === 'leaderRule' && sincereThresholdResults.initialPollResults && (
                    <div style={{ padding: '10px', backgroundColor: '#1e293b', borderRadius: '6px', border: '1px solid #6366f1', marginBottom: '10px', display: 'none' }}>
                        <p style={{ fontSize: '13px', color: '#a5b4fc', marginBottom: '0' }}>
                            <strong>Initial Poll Results:</strong> Leader = <span style={{ color: '#34d399' }}>{getLabel(sincereThresholdResults.initialPollResults.leader)}</span>,
                            Challenger = <span style={{ color: '#fbbf24' }}>{getLabel(sincereThresholdResults.initialPollResults.challenger)}</span>
                        </p>
                    </div>
                )}

                {/* Checkbox to toggle between initial and final approvals - only show when flag is true */}
                {showInitialFinalToggle && (
                    <div style={{ marginBottom: '15px' }}>
                        <label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', fontSize: '13px', color: '#cbd5e1' }}>
                            <input
                                type="checkbox"
                                checked={showInitialApprovals}
                                onChange={(e) => setShowInitialApprovals(e.target.checked)}
                                style={{ width: '16px', height: '16px', cursor: 'pointer' }}
                            />
                            <span>
                                Show initial approvals (before {strategyType === 'leaderRule' ? 'leader rule' : useBasicStrategy ? 'basic strategy' : 'strategy'} applied)
                            </span>
                        </label>
                    </div>
                )}

                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '15px' }}>
                    <div style={{ backgroundColor: '#0f172a', padding: '15px', borderRadius: '6px', border: '1px solid #0d9488' }}>
                        <h4 style={{ fontSize: '14px', fontWeight: 'bold', marginBottom: '12px', color: '#5eead4' }}>
                            Approval Votes by Candidate {showInitialApprovals && '(Initial)'}
                        </h4>
                        {/* Use initial or final approval counts based on toggle */}
                        {(() => {
                            const displayCounts = (showInitialApprovals && sincereThresholdResults.initialApprovalCounts)
                                ? sincereThresholdResults.initialApprovalCounts
                                : sincereThresholdResults.approvalCounts;

                            // Calculate winner from the displayed counts
                            const displayWinner = Object.entries(displayCounts)
                                .sort((a, b) => b[1] - a[1])[0][0];

                            return (
                                <>
                                    {/* Show count of voters who cast no approvals, if any */}
                                    {((sincereThresholdResults.votesDistribution && sincereThresholdResults.votesDistribution[0]) || 0) > 0 && (() => {
                                        const noVotes = sincereThresholdResults.votesDistribution[0] || 0;
                                        const percentage = (noVotes / sincereThresholdResults.totalVoters) * 100;
                                        return (
                                            <div key="no-vote" style={{ marginBottom: '10px' }}>
                                                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
                                                    <span style={{ fontSize: '14px', color: '#e2e8f0' }}>
                                                        No Vote
                                                    </span>
                                                    <span style={{ fontSize: '13px', color: '#cbd5e1' }}>
                                                        {noVotes} ({percentage.toFixed(1)}%)
                                                    </span>
                                                </div>
                                                <div style={{ height: '24px', backgroundColor: '#1e293b', borderRadius: '4px', overflow: 'hidden', position: 'relative' }}>
                                                    <div
                                                        style={{
                                                            width: percentage + '%',
                                                            height: '100%',
                                                            backgroundColor: '#6b7280',
                                                            transition: 'width 0.3s ease'
                                                        }}
                                                    ></div>
                                                </div>
                                            </div>
                                        );
                                    })()}

                                    {Object.entries(displayCounts)
                                        .sort((a, b) => b[1] - a[1])
                                        .map(([cand, votes]) => {
                                            const percentage = (votes / sincereThresholdResults.totalVoters) * 100;
                                            const isWinner = cand === displayWinner;

                                            return (
                                                <div key={cand} style={{ marginBottom: '10px' }}>
                                                    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
                                                        <span style={{ fontSize: '14px', fontWeight: isWinner ? 'bold' : 'normal', color: '#e2e8f0' }}>
                                                            {getLabel(cand)}
                                                            {isWinner && <span style={{ marginLeft: '6px' }}>🏆</span>}
                                                        </span>
                                                        <span style={{ fontSize: '13px', fontWeight: isWinner ? 'bold' : 'normal', color: '#cbd5e1' }}>
                                                            {votes} ({percentage.toFixed(1)}%)
                                                        </span>
                                                    </div>
                                                    <div style={{ height: '24px', backgroundColor: '#1e293b', borderRadius: '4px', overflow: 'hidden', position: 'relative' }}>
                                                        <div
                                                            style={{
                                                                width: percentage + '%',
                                                                height: '100%',
                                                                backgroundColor: isWinner ? '#14b8a6' : '#475569',
                                                                transition: 'width 0.3s ease'
                                                            }}
                                                        ></div>
                                                    </div>
                                                </div>
                                            );
                                        })}
                                </>
                            );
                        })()}

                        <div style={{ marginTop: '8px', fontSize: '12px', color: '#94a3b8' }}>
                            {condorcetInfo.winner && condorcetInfo.winner !== 'None' ? (
                                <span>Condorcet winner: <strong style={{ color: '#60a5fa' }}>{getLabel(condorcetInfo.winner)}</strong></span>
                            ) : (
                                <span>No Condorcet winner</span>
                            )}
                        </div>
                    </div>

                    <div style={{ backgroundColor: '#0f172a', padding: '15px', borderRadius: '6px', border: '1px solid #0d9488' }}>
                        <h4 style={{ fontSize: '14px', fontWeight: 'bold', marginBottom: '12px', color: '#5eead4' }}>
                            Votes Cast per Voter {showInitialApprovals && '(Initial)'}
                        </h4>
                        {(() => {
                            // Use initial or final votes distribution based on toggle
                            const displayVotesDistribution = (showInitialApprovals && sincereThresholdResults.initialVotesDistribution)
                                ? sincereThresholdResults.initialVotesDistribution
                                : sincereThresholdResults.votesDistribution;
                            
                            return Object.entries(displayVotesDistribution)
                                .filter(([count, voters]) => voters > 0)
                                .sort((a, b) => parseInt(a[0]) - parseInt(b[0]))
                                .map(([count, voters]) => {
                                    const percentage = (voters / sincereThresholdResults.totalVoters) * 100;
                                    const maxVoters = Math.max(...Object.values(displayVotesDistribution));
                                    const relativeWidth = (voters / maxVoters) * 100;

                                    return (
                                    <div key={count} style={{ marginBottom: '10px' }}>
                                        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
                                            <span style={{ fontSize: '14px', color: '#e2e8f0' }}>
                                                {count} {count === '1' ? 'candidate' : 'candidates'}
                                            </span>
                                            <span style={{ fontSize: '13px', color: '#cbd5e1' }}>
                                                {voters} ({percentage.toFixed(1)}%)
                                            </span>
                                        </div>
                                        <div style={{ height: '24px', backgroundColor: '#1e293b', borderRadius: '4px', overflow: 'hidden', position: 'relative' }}>
                                            <div
                                                style={{
                                                    width: relativeWidth + '%',
                                                    height: '100%',
                                                    backgroundColor: '#0d9488',
                                                    transition: 'width 0.3s ease'
                                                }}
                                            ></div>
                                        </div>
                                    </div>
                                );
                            });
                        })()}
                    </div>
                </div>

                <p style={{ fontSize: '11px', color: '#94a3b8', marginTop: '12px', fontStyle: 'italic' }}>
                    {useBasicStrategy
                        ? 'Basic strategy: Voters always approve their closest candidate and never approve their furthest candidate. Middle candidates are approved if within the threshold.'
                        : 'Without strategy: Voters approve all candidates within their sincere threshold. This could result in approving 0 or all candidates.'}
                </p>

                {sincereMCResults && (
                    <div style={{ backgroundColor: '#0f172a', padding: '15px', borderRadius: '6px', border: '1px solid #0d9488', marginTop: '15px' }}>
                        <h4 style={{ fontSize: '14px', fontWeight: 'bold', marginBottom: '12px', color: '#5eead4' }}>
                            Monte Carlo Results ({sincereMCSimulations} simulations)
                        </h4>
                        {Object.entries(sincereMCResults)
                            .sort((a, b) => b[1] - a[1])
                            .map(([cand, wins]) => {
                                const percentage = (wins / sincereMCSimulations) * 100;
                                const maxWins = Math.max(...Object.values(sincereMCResults));
                                const isWinner = wins === maxWins;

                                return (
                                    <div key={cand} style={{ marginBottom: '10px' }}>
                                        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
                                            <span style={{ fontSize: '14px', fontWeight: isWinner ? 'bold' : 'normal', color: '#e2e8f0' }}>
                                                {getLabel(cand)}
                                                {isWinner && <span style={{ marginLeft: '6px' }}>🏆</span>}
                                            </span>
                                            <span style={{ fontSize: '13px', fontWeight: isWinner ? 'bold' : 'normal', color: '#cbd5e1' }}>
                                                {wins} ({percentage.toFixed(1)}%)
                                            </span>
                                        </div>
                                        <div style={{ height: '24px', backgroundColor: '#1e293b', borderRadius: '4px', overflow: 'hidden', position: 'relative' }}>
                                            <div
                                                style={{
                                                    width: percentage + '%',
                                                    height: '100%',
                                                    backgroundColor: isWinner ? '#14b8a6' : '#475569',
                                                    transition: 'width 0.3s ease'
                                                }}
                                            ></div>
                                        </div>
                                    </div>
                                );
                            })}
                        <p style={{ fontSize: '11px', color: '#94a3b8', marginTop: '12px', fontStyle: 'italic' }}>
                            Each simulation randomly distributes {numVoters} voters across [0,1] and determines the winner.
                        </p>
                    </div>
                )}

                {turnBasedResults && (
                    <div style={{ backgroundColor: '#0f172a', padding: '15px', borderRadius: '6px', border: '1px solid #7c3aed', marginTop: '15px' }}>
                        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
                            <h4 style={{ fontSize: '14px', fontWeight: 'bold', color: '#c4b5fd' }}>
                                Turn-Based Simulation Results
                            </h4>
                            <button
                                onClick={() => setTurnBasedResults(null)}
                                style={{
                                    padding: '6px 12px',
                                    borderRadius: '4px',
                                    backgroundColor: '#475569',
                                    border: 'none',
                                    color: '#cbd5e1',
                                    fontWeight: '600',
                                    cursor: 'pointer',
                                    fontSize: '12px'
                                }}
                            >
                                Clear
                            </button>
                        </div>

                        <div style={{ backgroundColor: '#1e1b4b', padding: '12px', borderRadius: '4px', marginBottom: '12px' }}>
                            <div style={{ fontSize: '13px', color: '#e9d5ff', marginBottom: '6px' }}>
                                <strong>Initial Winner:</strong> {getLabel(turnBasedResults[0].winner)} (Step 0)
                            </div>
                            <div style={{ fontSize: '13px', color: '#e9d5ff', marginBottom: '6px' }}>
                                <strong>Final Winner:</strong> {getLabel(turnBasedResults[turnBasedResults.length - 1].winner)} (Step {turnBasedResults.length - 1})
                            </div>
                            {strategyType === 'leaderRule' && turnBasedResults[currentStep].frontrunnersObj && (
                                <div style={{ fontSize: '13px', color: '#e9d5ff', marginBottom: '6px' }}>
                                    <strong>Leader/Challenger (Step {currentStep}):</strong> {getLabel(turnBasedResults[currentStep].frontrunnersObj.leader)} / {getLabel(turnBasedResults[currentStep].frontrunnersObj.challenger)}
                                </div>
                            )}
                            <div style={{ fontSize: '13px', color: '#e9d5ff', marginBottom: '6px' }}>
                                <strong>Viable Candidates (Step {currentStep}):</strong> {turnBasedResults[currentStep].viableCandidates.map(c => getLabel(c)).join(', ')}
                            </div>
                            <div style={{ fontSize: '13px', color: '#e9d5ff', marginBottom: '6px' }}>
                                <strong>Mean Ballot Size:</strong> {turnBasedResults[0].meanBallotSize.toFixed(2)} → {turnBasedResults[turnBasedResults.length - 1].meanBallotSize.toFixed(2)} candidates
                            </div>
                            <div style={{ fontSize: '13px', color: '#e9d5ff' }}>
                                <strong>Converged in {turnBasedResults.length - 1} steps</strong>
                            </div>
                        </div>

                        <div style={{ marginBottom: '12px' }}>
                            <label style={{ display: 'block', fontSize: '13px', fontWeight: '600', marginBottom: '8px', color: '#cbd5e1' }}>
                                View Step: {currentStep} / {turnBasedResults.length - 1}
                            </label>
                            <input
                                type="range"
                                min="0"
                                max={turnBasedResults.length - 1}
                                step="1"
                                value={currentStep}
                                onChange={(e) => setCurrentStep(parseInt(e.target.value))}
                                style={{ width: '100%' }}
                            />
                        </div>

                        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '15px' }}>
                            <div>
                                <h5 style={{ fontSize: '13px', fontWeight: 'bold', marginBottom: '10px', color: '#c4b5fd' }}>
                                    Approval Votes by Candidate (Step {currentStep})
                                </h5>
                                {Object.entries(turnBasedResults[currentStep].approvalCounts)
                                    .sort((a, b) => b[1] - a[1])
                                    .map(([cand, votes]) => {
                                        const percentage = (votes / numVoters) * 100;
                                        const isWinner = cand === turnBasedResults[currentStep].winner;
                                        const isCondorcet = cand === condorcetInfo.winner && condorcetInfo.winner !== 'None';

                                        return (
                                            <div key={cand} style={{ marginBottom: '10px' }}>
                                                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
                                                    <span style={{ fontSize: '14px', fontWeight: isWinner ? 'bold' : 'normal', color: '#e2e8f0' }}>
                                                        {getLabel(cand)}
                                                        {isWinner && <span style={{ marginLeft: '6px' }}>🏆</span>}
                                                        {isCondorcet && <span style={{ marginLeft: '4px', fontSize: '11px', color: '#60a5fa' }}>(Condorcet)</span>}
                                                    </span>
                                                    <span style={{ fontSize: '13px', fontWeight: isWinner ? 'bold' : 'normal', color: '#cbd5e1' }}>
                                                        {votes} ({percentage.toFixed(1)}%)
                                                    </span>
                                                </div>
                                                <div style={{ height: '24px', backgroundColor: '#1e293b', borderRadius: '4px', overflow: 'hidden', position: 'relative' }}>
                                                    <div
                                                        style={{
                                                            width: percentage + '%',
                                                            height: '100%',
                                                            backgroundColor: isWinner ? '#7c3aed' : '#475569',
                                                            transition: 'width 0.3s ease'
                                                        }}
                                                    ></div>
                                                </div>
                                            </div>
                                        );
                                    })}
                            </div>

                            <div>
                                <h5 style={{ fontSize: '13px', fontWeight: 'bold', marginBottom: '10px', color: '#c4b5fd' }}>
                                    Votes Cast per Voter (Step {currentStep})
                                </h5>
                                {Object.entries(turnBasedResults[currentStep].votesDistribution)
                                    .filter(([count, voters]) => voters > 0)
                                    .sort((a, b) => parseInt(a[0]) - parseInt(b[0]))
                                    .map(([count, voters]) => {
                                        const percentage = (voters / numVoters) * 100;
                                        const maxVoters = Math.max(...Object.values(turnBasedResults[currentStep].votesDistribution));
                                        const relativeWidth = (voters / maxVoters) * 100;

                                        return (
                                            <div key={count} style={{ marginBottom: '10px' }}>
                                                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
                                                    <span style={{ fontSize: '14px', color: '#e2e8f0' }}>
                                                        {count} {count === '1' ? 'candidate' : 'candidates'}
                                                    </span>
                                                    <span style={{ fontSize: '13px', color: '#cbd5e1' }}>
                                                        {voters} ({percentage.toFixed(1)}%)
                                                    </span>
                                                </div>
                                                <div style={{ height: '24px', backgroundColor: '#1e293b', borderRadius: '4px', overflow: 'hidden', position: 'relative' }}>
                                                    <div
                                                        style={{
                                                            width: relativeWidth + '%',
                                                            height: '100%',
                                                            backgroundColor: '#7c3aed',
                                                            transition: 'width 0.3s ease'
                                                        }}
                                                    ></div>
                                                </div>
                                            </div>
                                        );
                                    })}
                            </div>
                        </div>

                        <div style={{ marginTop: '15px', backgroundColor: '#1e1b4b', padding: '12px', borderRadius: '4px' }}>
                            <h5 style={{ fontSize: '13px', fontWeight: 'bold', marginBottom: '10px', color: '#c4b5fd' }}>
                                Most Common Ballots (Step {currentStep})
                            </h5>
                            <div style={{ fontSize: '12px', color: '#e2e8f0' }}>
                                {Object.entries(turnBasedResults[currentStep].ballotCounts)
                                    .sort((a, b) => b[1] - a[1])
                                    .slice(0, 10)
                                    .map(([ballot, count]) => {
                                        const percentage = (count / numVoters) * 100;
                                        const candidateLabels = ballot.split(',').map(c => getLabel(c)).join(', ');
                                        return (
                                            <div key={ballot} style={{ marginBottom: '8px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                                                <span style={{ fontFamily: 'monospace', color: '#e9d5ff' }}>
                                                    [{candidateLabels}]
                                                </span>
                                                <span style={{ color: '#cbd5e1', fontSize: '11px' }}>
                                                    {count} voters ({percentage.toFixed(1)}%)
                                                </span>
                                            </div>
                                        );
                                    })}
                            </div>
                            <p style={{ fontSize: '10px', color: '#a78bfa', marginTop: '8px', fontStyle: 'italic' }}>
                                Ballots are ordered by each voter's preference (closest candidate first).
                            </p>

                            <div style={{ marginTop: '12px', paddingTop: '12px', borderTop: '1px solid #4c1d95' }}>
                                <h6 style={{ fontSize: '12px', fontWeight: 'bold', marginBottom: '8px', color: '#c4b5fd' }}>
                                    Plurality Vote (if only first choice counted):
                                </h6>
                                {(() => {
                                    // Calculate plurality - count first candidate on each ballot
                                    const pluralityVotes = {};
                                    Object.entries(turnBasedResults[currentStep].ballotCounts).forEach(([ballot, count]) => {
                                        const firstChoice = ballot.split(',')[0]; // First candidate is voter's favorite
                                        pluralityVotes[firstChoice] = (pluralityVotes[firstChoice] || 0) + count;
                                    });

                                    const sorted = Object.entries(pluralityVotes).sort((a, b) => b[1] - a[1]);
                                    const pluralityWinner = sorted[0][0];

                                    return sorted.map(([cand, votes]) => {
                                        const percentage = (votes / numVoters) * 100;
                                        const isWinner = cand === pluralityWinner;
                                        return (
                                            <div key={cand} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px', fontSize: '11px' }}>
                                                <span style={{ color: isWinner ? '#fbbf24' : '#cbd5e1', fontWeight: isWinner ? 'bold' : 'normal' }}>
                                                    {getLabel(cand)} {isWinner && '🥇'}
                                                </span>
                                                <span style={{ color: '#94a3b8' }}>
                                                    {votes} ({percentage.toFixed(1)}%)
                                                </span>
                                            </div>
                                        );
                                    });
                                })()}
                            </div>
                        </div>

                        {currentStep > 0 && turnBasedResults[currentStep].transitions && (
                            <div style={{ marginTop: '15px', backgroundColor: '#1e1b4b', padding: '12px', borderRadius: '4px' }}>
                                <h5 style={{ fontSize: '13px', fontWeight: 'bold', marginBottom: '10px', color: '#c4b5fd' }}>
                                    Ballot Changes from Step {currentStep - 1} to Step {currentStep}
                                </h5>

                                <div style={{ marginBottom: '12px', padding: '8px', backgroundColor: '#312e81', borderRadius: '4px', fontSize: '11px' }}>
                                    <div style={{ color: '#a78bfa', marginBottom: '4px', fontWeight: 'bold' }}>
                                        Frontrunners at Step {currentStep - 1}:
                                    </div>
                                    <div style={{ color: '#e9d5ff' }}>
                                        {turnBasedResults[currentStep - 1].frontrunners && turnBasedResults[currentStep - 1].frontrunners.length >= 2
                                            ? `${getLabel(turnBasedResults[currentStep - 1].frontrunners[0])} vs ${getLabel(turnBasedResults[currentStep - 1].frontrunners[1])}`
                                            : turnBasedResults[currentStep - 1].frontrunners && turnBasedResults[currentStep - 1].frontrunners.length === 1
                                                ? `${getLabel(turnBasedResults[currentStep - 1].frontrunners[0])}`
                                                : 'Unknown'}
                                    </div>
                                </div>

                                <div style={{ marginBottom: '12px', fontSize: '12px', color: '#e9d5ff' }}>
                                    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px', backgroundColor: '#312e81', borderRadius: '4px' }}>
                                        <span style={{ fontWeight: 'bold' }}>Voters Unchanged:</span>
                                        <span>{turnBasedResults[currentStep].votersUnchanged} ({((turnBasedResults[currentStep].votersUnchanged / numVoters) * 100).toFixed(1)}%)</span>
                                    </div>
                                </div>
                                {Object.keys(turnBasedResults[currentStep].transitions).length > 0 && (
                                    <>
                                        <div style={{ fontSize: '11px', color: '#c4b5fd', marginBottom: '6px', fontWeight: 'bold' }}>
                                            Ballot Transitions:
                                        </div>
                                        <div style={{ fontSize: '12px', color: '#e2e8f0', maxHeight: '300px', overflowY: 'auto' }}>
                                            {Object.entries(turnBasedResults[currentStep].transitions)
                                                .sort((a, b) => b[1] - a[1])
                                                .map(([transition, count]) => {
                                                    const [from, to] = transition.split('→');
                                                    const fromLabels = from === '∅' ? '∅' : from.split(',').map(c => getLabel(c)).join(', ');
                                                    const toLabels = to === '∅' ? '∅' : to.split(',').map(c => getLabel(c)).join(', ');
                                                    const percentage = (count / numVoters) * 100;

                                                    return (
                                                        <div key={transition} style={{ marginBottom: '6px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '4px 0' }}>
                                                            <span style={{ fontFamily: 'monospace', color: '#e9d5ff', fontSize: '11px' }}>
                                                                [{fromLabels}] → [{toLabels}]
                                                            </span>
                                                            <span style={{ color: '#cbd5e1', fontSize: '10px', marginLeft: '8px', whiteSpace: 'nowrap' }}>
                                                                {count} voters ({percentage.toFixed(1)}%)
                                                            </span>
                                                        </div>
                                                    );
                                                })}
                                        </div>
                                    </>
                                )}
                            </div>
                        )}

                        <p style={{ fontSize: '11px', color: '#94a3b8', marginTop: '12px', fontStyle: 'italic' }}>
                            Each step, voters adjust strategically: viable candidates are within 3% of first place. Strategic voters never approve their least favorite candidate. If multiple are viable, voters approve their closest viable. If only one is viable (clear frontrunner), voters who have the frontrunner within their sincere threshold approve up to and including the frontrunner, while those who don't approve only candidates closer than the frontrunner. {(sincereVoterProportion * 100).toFixed(0)}% of voters are sincere and always vote with their initial threshold. Of the remaining {((1 - sincereVoterProportion) * 100).toFixed(0)}% strategic voters, {(asyncUpdateRate * 100).toFixed(0)}% update each step (= {(asyncUpdateRate * (1 - sincereVoterProportion) * 100).toFixed(1)}% of all voters).
                        </p>
                    </div>
                )}
            </div>

            <div style={{ backgroundColor: '#1e293b', padding: '15px', borderRadius: '8px', marginTop: '20px', border: '1px solid #0ea5e9' }}>
                <h3 style={{ fontSize: '16px', fontWeight: 'bold', marginBottom: '10px', color: '#7dd3fc' }}>Custom Candidate Labels</h3>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '15px' }}>
                    <div>
                        <label style={{ display: 'block', fontSize: '13px', fontWeight: '600', marginBottom: '5px', color: '#cbd5e1' }}>
                            C1 Label:
                        </label>
                        <input
                            type="text"
                            value={label1}
                            onChange={(e) => setLabel1(e.target.value)}
                            placeholder="A"
                            style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #475569', fontSize: '14px', backgroundColor: '#0f172a', color: '#e2e8f0' }}
                        />
                    </div>
                    <div>
                        <label style={{ display: 'block', fontSize: '13px', fontWeight: '600', marginBottom: '5px', color: '#cbd5e1' }}>
                            C2 Label:
                        </label>
                        <input
                            type="text"
                            value={label2}
                            onChange={(e) => setLabel2(e.target.value)}
                            placeholder="B"
                            style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #475569', fontSize: '14px', backgroundColor: '#0f172a', color: '#e2e8f0' }}
                        />
                    </div>
                    {numCandidates >= 3 && (
                        <div>
                            <label style={{ display: 'block', fontSize: '13px', fontWeight: '600', marginBottom: '5px', color: '#cbd5e1' }}>
                                C3 Label:
                            </label>
                            <input
                                type="text"
                                value={label3}
                                onChange={(e) => setLabel3(e.target.value)}
                                placeholder="C"
                                style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #475569', fontSize: '14px', backgroundColor: '#0f172a', color: '#e2e8f0' }}
                            />
                        </div>
                    )}
                    {numCandidates >= 4 && (
                        <div>
                            <label style={{ display: 'block', fontSize: '13px', fontWeight: '600', marginBottom: '5px', color: '#cbd5e1' }}>
                                C4 Label:
                            </label>
                            <input
                                type="text"
                                value={label4}
                                onChange={(e) => setLabel4(e.target.value)}
                                placeholder="D"
                                style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #475569', fontSize: '14px', backgroundColor: '#0f172a', color: '#e2e8f0' }}
                            />
                        </div>
                    )}
                </div>
                <div style={{ marginTop: '12px', display: 'flex', gap: '8px', alignItems: 'center' }}>
                    <button onClick={copyUrlToClipboard} style={{ padding: '8px 12px', borderRadius: '6px', backgroundColor: '#0891b2', border: 'none', color: '#e6fffa', fontWeight: '600', cursor: 'pointer' }}>
                        Copy URL
                    </button>
                    <button onClick={resetToDefaults} style={{ padding: '8px 12px', borderRadius: '6px', backgroundColor: '#dc2626', border: 'none', color: '#fff', fontWeight: '600', cursor: 'pointer' }}>
                        Reset to Defaults
                    </button>
                    <span style={{ color: '#cbd5e1', fontSize: '13px' }}>{copyStatus}</span>
                </div>
            </div>
        </div>
    );
}

// Expose component to global scope so `index.html` can render it
window.VotingAnalysis = VotingAnalysis;