/**
 * <folk-vote-dashboard> — conviction voting dashboard.
 *
 * Browse spaces, create/view proposals, cast votes (ranking + final).
 * Enhanced: inline voting, create proposal form, richer UI.
 */

import { proposalSchema, type ProposalDoc } from "../schemas";
import type { DocumentId } from "../../../shared/local-first/document";
import { TourEngine } from "../../../shared/tour-engine";
import { ViewHistory } from "../../../shared/view-history.js";
import { authFetch, requireAuth } from "../../../shared/auth-fetch";
import { startPresenceHeartbeat } from '../../../shared/collab-presence';

interface VoteSpace {
	slug: string;
	name: string;
	description: string;
	visibility: string;
	promotion_threshold: number;
	voting_period_days: number;
	credits_per_day: number;
	max_credits: number;
	starting_credits: number;
}

interface Proposal {
	id: string;
	title: string;
	description: string;
	status: string;
	score: number;
	vote_count: string;
	final_yes: number;
	final_no: number;
	final_abstain: number;
	elo: number;
	elo_comparisons: number;
	elo_wins: number;
	created_at: string;
	voting_ends_at: string | null;
}

/** Score history entry for trend chart */
interface ScoreSnapshot {
	time: number; // timestamp
	scores: Record<string, number>; // proposalId → score
}

class FolkVoteDashboard extends HTMLElement {
	private shadow: ShadowRoot;
	private space = "";
	private view: "spaces" | "proposals" | "proposal" | "rank" = "spaces";
	private spaces: VoteSpace[] = [];
	private _stopPresence: (() => void) | null = null;
	private selectedSpace: VoteSpace | null = null;
	private proposals: Proposal[] = [];
	private selectedProposal: Proposal | null = null;
	private loading = false;
	private error = "";
	private showCreateForm = false;
	private showTrendChart = true;
	private scoreHistory: ScoreSnapshot[] = [];
	private _offlineUnsubs: (() => void)[] = [];
	private _history = new ViewHistory<"spaces" | "proposals" | "proposal" | "rank">("spaces", "rvote");

	// Pairwise ranking state
	private rankPairA: Proposal | null = null;
	private rankPairB: Proposal | null = null;
	private rankCompareCount = 0;
	private rankResult: { winnerId: string; delta: number } | null = null;
	private rankLoading = false;

	// Guided tour
	private _tour!: TourEngine;
	private static readonly TOUR_STEPS = [
		{ target: '[data-space]', title: "Voting Spaces", message: "Each space has its own proposals and conviction parameters. Click a space to explore.", advanceOnClick: true },
		{ target: '[data-toggle-create]', title: "Create Proposal", message: "Submit a new proposal for the community to vote on with conviction-weighted ranking.", advanceOnClick: true },
		{ target: '[data-proposal]', title: "View Proposal", message: "Click a proposal to see its details, cast your vote, and watch conviction accumulate over time.", advanceOnClick: true },
		{ target: '[data-toggle-trend]', title: "Trend Chart", message: "Toggle the conviction trend chart to see how proposal scores evolve as votes accumulate.", advanceOnClick: false },
	];

	constructor() {
		super();
		this.shadow = this.attachShadow({ mode: "open" });
		this._tour = new TourEngine(
			this.shadow,
			FolkVoteDashboard.TOUR_STEPS,
			"rvote_tour_done",
			() => this.shadow.host as HTMLElement,
		);
	}

	connectedCallback() {
		this.space = this.getAttribute("space") || "demo";
		if (this.space === "demo") { this.loadDemoData(); }
		else { this.subscribeOffline(); this.loadSpaces(); }
		if (!localStorage.getItem("rvote_tour_done")) {
			setTimeout(() => this._tour.start(), 1200);
		}
		this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rvote', context: this.selectedProposal?.title || 'Voting' }));
		window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
	}

	disconnectedCallback() {
		this._history.destroy();
		window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener);
		for (const unsub of this._offlineUnsubs) unsub();
		this._offlineUnsubs = [];
		this._stopPresence?.();
	}

	private async subscribeOffline() {
		const runtime = (window as any).__rspaceOfflineRuntime;
		if (!runtime?.isInitialized) return;

		try {
			const docs = await runtime.subscribeModule('vote', 'proposals', proposalSchema);
			if (docs.size > 0 && this.proposals.length === 0) {
				const fromDocs: Proposal[] = [];
				for (const [docId, doc] of docs) {
					const d = doc as ProposalDoc;
					if (!d?.proposal) continue;
					fromDocs.push({
						id: d.proposal.id, title: d.proposal.title,
						description: d.proposal.description, status: d.proposal.status,
						score: d.proposal.score, vote_count: String(Object.keys(d.votes || {}).length),
						final_yes: d.proposal.finalYes, final_no: d.proposal.finalNo,
						final_abstain: d.proposal.finalAbstain,
						elo: d.pairwise?.elo ?? 1500, elo_comparisons: d.pairwise?.comparisons ?? 0, elo_wins: d.pairwise?.wins ?? 0,
						created_at: new Date(d.proposal.createdAt).toISOString(),
						voting_ends_at: d.proposal.votingEndsAt ? new Date(d.proposal.votingEndsAt).toISOString() : null,
					});
					this._offlineUnsubs.push(runtime.onChange(docId, () => {}));
				}
				if (fromDocs.length > 0) {
					this.proposals = fromDocs;
					this.view = "proposals";
					this.render();
				}
			}
		} catch { /* runtime unavailable */ }
	}

	private loadDemoData() {
		this.spaces = [
			{
				slug: "community",
				name: "Community Governance",
				description: "Proposals for the rSpace ecosystem",
				visibility: "public",
				promotion_threshold: 100,
				voting_period_days: 7,
				credits_per_day: 10,
				max_credits: 500,
				starting_credits: 50,
			},
		];
		this.selectedSpace = this.spaces[0];
		this.view = "proposals";

		const now = Date.now();
		const day = 86400000;
		this.proposals = [
			{
				id: "p1",
				title: "Implement real-time collaboration in rNotes",
				description: "Use Automerge CRDTs (already in the stack) to enable simultaneous editing of notes, similar to how rSpace canvas works.",
				status: "RANKING",
				score: 72,
				vote_count: "9",
				final_yes: 0, final_no: 0, final_abstain: 0, elo: 1500, elo_comparisons: 0, elo_wins: 0,
				created_at: new Date(now - 3 * day).toISOString(),
				voting_ends_at: null,
			},
			{
				id: "p2",
				title: "Add dark mode across all r* modules",
				description: "Implement a consistent dark theme with a toggle in shell.css. Use CSS custom properties for theming so each module inherits automatically.",
				status: "RANKING",
				score: 45,
				vote_count: "6",
				final_yes: 0, final_no: 0, final_abstain: 0, elo: 1500, elo_comparisons: 0, elo_wins: 0,
				created_at: new Date(now - 5 * day).toISOString(),
				voting_ends_at: null,
			},
			{
				id: "p3",
				title: "Adopt cosmolocal print-on-demand for all merch",
				description: "Route all merchandise orders through the provider registry to find the closest printer. Reduces shipping emissions and supports local economies.",
				status: "VOTING",
				score: 105,
				vote_count: "14",
				final_yes: 5, final_no: 2, final_abstain: 0, elo: 1568, elo_comparisons: 8, elo_wins: 5,
				created_at: new Date(now - 10 * day).toISOString(),
				voting_ends_at: new Date(now + 5 * day).toISOString(),
			},
			{
				id: "p4",
				title: "Use EncryptID passkeys for all authentication",
				description: "Standardize on WebAuthn passkeys via EncryptID across the entire r* ecosystem. One passkey, all apps.",
				status: "PASSED",
				score: 150,
				vote_count: "17",
				final_yes: 12, final_no: 3, final_abstain: 2, elo: 1612, elo_comparisons: 12, elo_wins: 9,
				created_at: new Date(now - 21 * day).toISOString(),
				voting_ends_at: new Date(now - 7 * day).toISOString(),
			},
			{
				id: "p5",
				title: "Switch from PostgreSQL to SQLite for simpler deployment",
				description: "Evaluate replacing PostgreSQL with SQLite for modules that don't need concurrent writes.",
				status: "FAILED",
				score: 30,
				vote_count: "11",
				final_yes: 2, final_no: 8, final_abstain: 1, elo: 1432, elo_comparisons: 10, elo_wins: 3,
				created_at: new Date(now - 18 * day).toISOString(),
				voting_ends_at: new Date(now - 4 * day).toISOString(),
			},
		];
		this.recordSnapshot();
		// Seed some historical snapshots for the demo trend chart
		this.seedDemoHistory();
		this.render();
	}

	/** Seed fake historical snapshots so the trend chart isn't empty */
	private seedDemoHistory() {
		const now = Date.now();
		const day = 86400000;
		const rankings = this.proposals.filter(p => p.status === "RANKING");
		// Simulate 7 days of score evolution
		const history: ScoreSnapshot[] = [];
		for (let d = 6; d >= 0; d--) {
			const snap: ScoreSnapshot = { time: now - d * day, scores: {} };
			for (const p of rankings) {
				// Work backwards: current score minus some random drift
				const drift = Math.round((Math.random() - 0.3) * 8 * (d + 1));
				snap.scores[p.id] = Math.max(0, p.score - drift);
			}
			history.push(snap);
		}
		this.scoreHistory = history;
	}

	/** Record current scores as a snapshot */
	private recordSnapshot() {
		const snap: ScoreSnapshot = { time: Date.now(), scores: {} };
		for (const p of this.proposals) {
			if (p.status === "RANKING") snap.scores[p.id] = p.score;
		}
		this.scoreHistory.push(snap);
		// Keep last 50 snapshots
		if (this.scoreHistory.length > 50) this.scoreHistory.shift();
	}

	private getApiBase(): string {
		const path = window.location.pathname;
		const match = path.match(/^(\/[^/]+)?\/rvote/);
		return match ? match[0] : "";
	}

	private async loadSpaces() {
		this.loading = true;
		this.render();
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/spaces`);
			const data = await res.json();
			this.spaces = data.spaces || [];
		} catch {
			this.error = "Failed to load spaces";
		}
		this.loading = false;
		this.render();
	}

	private async loadProposals(slug: string) {
		this.loading = true;
		this.render();
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/proposals?space_slug=${slug}`);
			const data = await res.json();
			this.proposals = data.proposals || [];
		} catch {
			this.error = "Failed to load proposals";
		}
		this.loading = false;
		this.render();
	}

	private async loadProposal(id: string) {
		this.loading = true;
		this.render();
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/proposals/${id}`);
			this.selectedProposal = await res.json();
		} catch {
			this.error = "Failed to load proposal";
		}
		this.loading = false;
		this.render();
	}

	private async castVote(proposalId: string, weight: number) {
		if (this.space === "demo") {
			const p = this.proposals.find(p => p.id === proposalId);
			if (p) {
				p.score += weight;
				p.vote_count = String(parseInt(p.vote_count) + 1);
				const threshold = this.selectedSpace?.promotion_threshold || 100;
				if (p.score >= threshold && p.status === "RANKING") {
					p.status = "VOTING";
					const votingDays = this.selectedSpace?.voting_period_days || 7;
					p.voting_ends_at = new Date(Date.now() + votingDays * 86400000).toISOString();
				}
				if (this.selectedProposal?.id === proposalId) this.selectedProposal = p;
			}
			this.recordSnapshot();
			this.render();
			return;
		}
		if (!requireAuth("cast vote")) return;
		try {
			const base = this.getApiBase();
			await authFetch(`${base}/api/proposals/${proposalId}/vote`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ weight }),
			});
			if (this.view === "proposal") await this.loadProposal(proposalId);
			else if (this.selectedSpace) await this.loadProposals(this.selectedSpace.slug);
		} catch {
			this.error = "Failed to cast vote";
			this.render();
		}
	}

	private async castFinalVote(proposalId: string, vote: string) {
		if (this.space === "demo") {
			const p = this.proposals.find(p => p.id === proposalId);
			if (p) {
				if (vote === "YES") p.final_yes++;
				else if (vote === "NO") p.final_no++;
				else if (vote === "ABSTAIN") p.final_abstain++;
				if (this.selectedProposal?.id === proposalId) this.selectedProposal = p;
			}
			this.render();
			return;
		}
		if (!requireAuth("cast final vote")) return;
		try {
			const base = this.getApiBase();
			await authFetch(`${base}/api/proposals/${proposalId}/final-vote`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ vote }),
			});
			if (this.view === "proposal") await this.loadProposal(proposalId);
			else if (this.selectedSpace) await this.loadProposals(this.selectedSpace.slug);
		} catch {
			this.error = "Failed to cast vote";
			this.render();
		}
	}

	private async createProposal(title: string, description: string) {
		if (this.space === "demo") {
			const now = Date.now();
			this.proposals.unshift({
				id: `p${now}`,
				title,
				description,
				status: "RANKING",
				score: 0,
				vote_count: "0",
				final_yes: 0, final_no: 0, final_abstain: 0, elo: 1500, elo_comparisons: 0, elo_wins: 0,
				created_at: new Date(now).toISOString(),
				voting_ends_at: null,
			});
			this.showCreateForm = false;
			this.render();
			return;
		}
		if (!requireAuth("create proposal")) return;
		try {
			const base = this.getApiBase();
			await authFetch(`${base}/api/proposals`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ space_slug: this.selectedSpace?.slug, title, description }),
			});
			this.showCreateForm = false;
			if (this.selectedSpace) await this.loadProposals(this.selectedSpace.slug);
		} catch {
			this.error = "Failed to create proposal";
			this.render();
		}
	}

	private getStatusColor(status: string): string {
		switch (status) {
			case "RANKING": return "#3b82f6";
			case "VOTING": return "#f59e0b";
			case "PASSED": return "#22c55e";
			case "FAILED": return "#ef4444";
			case "ARCHIVED": return "#6b7280";
			default: return "#888";
		}
	}

	private getStatusIcon(status: string): string {
		switch (status) {
			case "RANKING": return "📊";
			case "VOTING": return "🗳";
			case "PASSED": return "✅";
			case "FAILED": return "❌";
			case "ARCHIVED": return "📦";
			default: return "📄";
		}
	}

	private render() {
		this.shadow.innerHTML = `
		<style>
			:host { display: block; font-family: system-ui, -apple-system, sans-serif; color: var(--rs-text-primary); -webkit-tap-highlight-color: transparent; }
			* { box-sizing: border-box; }
			button, a, input, select, textarea, [role="button"] { touch-action: manipulation; }

			.header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; }
			.header-back { padding: 6px 12px; border-radius: 8px; border: 1px solid var(--rs-border); background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 13px; transition: all 0.15s; }
			.header-back:hover { color: var(--rs-text-primary); border-color: var(--rs-primary-hover); }
			.header-title { font-size: 18px; font-weight: 700; flex: 1; color: var(--rs-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
			.header-sub { font-size: 13px; color: var(--rs-text-muted); }

			/* Space cards */
			.space-card {
				background: var(--rs-bg-page); border: 1px solid var(--rs-border-subtle); border-radius: 12px;
				padding: 20px; margin-bottom: 12px; cursor: pointer; transition: all 0.2s;
			}
			.space-card:hover { border-color: var(--rs-primary-hover); transform: translateY(-1px); box-shadow: var(--rs-shadow-md); }
			.space-name { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
			.space-desc { font-size: 13px; color: var(--rs-text-secondary); margin-bottom: 12px; }
			.space-meta { display: flex; gap: 16px; flex-wrap: wrap; }
			.space-stat { display: flex; flex-direction: column; gap: 2px; }
			.space-stat-value { font-size: 15px; font-weight: 600; color: var(--rs-primary-hover); }
			.space-stat-label { font-size: 11px; color: var(--rs-text-muted); text-transform: uppercase; letter-spacing: 0.04em; }

			/* Proposal cards — Reddit layout */
			.proposal {
				background: var(--rs-bg-page); border: 1px solid var(--rs-border-subtle); border-radius: 12px;
				margin-bottom: 10px; transition: border-color 0.2s;
				display: flex; overflow: hidden;
			}
			.proposal:hover { border-color: var(--rs-border); }

			/* Reddit-style vote column */
			.vote-col {
				display: flex; flex-direction: column; align-items: center;
				padding: 12px 6px; gap: 2px; flex-shrink: 0; width: 56px;
				background: var(--rs-bg-surface); border-right: 1px solid var(--rs-border-subtle);
			}
			.vote-chevron {
				display: flex; align-items: center; justify-content: center;
				width: 32px; height: 28px; border: none; border-radius: 6px;
				background: transparent; cursor: pointer; color: var(--rs-text-muted);
				transition: all 0.15s; padding: 0;
			}
			.vote-chevron:hover { background: rgba(129,140,248,0.1); }
			.vote-chevron.up:hover { color: #f97316; }
			.vote-chevron.down:hover { color: #3b82f6; }
			.vote-chevron:active { transform: scale(0.9); }
			.vote-score {
				font-size: 16px; font-weight: 800; color: var(--rs-text-primary);
				font-variant-numeric: tabular-nums; line-height: 1;
				padding: 2px 0;
			}
			.vote-score.positive { color: #f97316; }
			.vote-score.negative { color: #3b82f6; }
			.qv-cost {
				font-size: 9px; color: var(--rs-text-muted); text-align: center;
				white-space: nowrap; margin-top: 2px;
			}

			/* Right content area */
			.proposal-body { flex: 1; padding: 14px 16px; min-width: 0; }
			.proposal-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 4px; }
			.proposal-title { font-size: 15px; font-weight: 600; color: var(--rs-text-primary); cursor: pointer; line-height: 1.4; }
			.proposal-title:hover { color: var(--rs-primary-hover); }
			.proposal-desc { font-size: 13px; color: var(--rs-text-muted); margin-bottom: 8px; line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

			/* Status badge */
			.badge {
				display: inline-flex; align-items: center; gap: 4px;
				padding: 3px 10px; border-radius: 6px;
				font-size: 11px; font-weight: 600; text-transform: uppercase;
				letter-spacing: 0.03em; white-space: nowrap; flex-shrink: 0;
			}

			/* Score / progress */
			.score-row { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
			.score-label { font-size: 13px; color: var(--rs-text-secondary); white-space: nowrap; }
			.score-value { font-weight: 700; font-variant-numeric: tabular-nums; }
			.score-bar { flex: 1; height: 8px; border-radius: 4px; background: var(--rs-bg-surface); overflow: hidden; min-width: 100px; }
			.score-fill { height: 100%; border-radius: 4px; transition: width 0.4s ease; }

			/* Vote buttons */
			.vote-row { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
			.vote-btn {
				padding: 6px 14px; border-radius: 8px; border: 1px solid var(--rs-border);
				background: var(--rs-bg-surface); color: var(--rs-text-secondary); cursor: pointer; font-size: 13px;
				font-weight: 500; transition: all 0.15s; white-space: nowrap;
			}
			.vote-btn:hover { border-color: var(--rs-primary-hover); color: var(--rs-text-primary); background: rgba(129,140,248,0.1); }
			.vote-btn:active { transform: scale(0.97); }
			.vote-btn.yes { border-color: rgba(34,197,94,0.4); color: #22c55e; }
			.vote-btn.yes:hover { background: rgba(34,197,94,0.1); border-color: #22c55e; }
			.vote-btn.no { border-color: rgba(239,68,68,0.4); color: #ef4444; }
			.vote-btn.no:hover { background: rgba(239,68,68,0.1); border-color: #ef4444; }
			.vote-btn.abstain { border-color: rgba(245,158,11,0.4); color: #f59e0b; }
			.vote-btn.abstain:hover { background: rgba(245,158,11,0.1); border-color: #f59e0b; }
			.vote-sep { color: var(--rs-border); font-size: 12px; }

			/* Tally */
			.tally { display: flex; gap: 20px; margin: 10px 0 8px; }
			.tally-item { text-align: center; }
			.tally-value { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; }
			.tally-label { font-size: 11px; color: var(--rs-text-muted); text-transform: uppercase; }
			.tally-bar { display: flex; height: 8px; border-radius: 4px; overflow: hidden; background: var(--rs-bg-surface); margin-top: 6px; }
			.tally-bar-yes { background: #22c55e; transition: width 0.3s; }
			.tally-bar-no { background: #ef4444; transition: width 0.3s; }
			.tally-bar-abstain { background: #f59e0b; transition: width 0.3s; }

			/* Meta row */
			.meta { display: flex; gap: 16px; font-size: 12px; color: var(--rs-text-muted); margin-top: 8px; flex-wrap: wrap; }
			.meta span { white-space: nowrap; }

			/* Create form */
			.create-form { background: var(--rs-bg-page); border: 2px solid var(--rs-primary-hover); border-radius: 12px; padding: 20px; margin-bottom: 16px; }
			.create-form h3 { margin: 0 0 12px; font-size: 15px; font-weight: 600; color: var(--rs-primary-hover); }
			.form-field { margin-bottom: 12px; }
			.form-label { display: block; font-size: 12px; color: var(--rs-text-secondary); margin-bottom: 4px; font-weight: 500; }
			.form-input, .form-textarea {
				width: 100%; padding: 10px 12px; border-radius: 8px;
				border: 1px solid var(--rs-input-border); background: var(--rs-input-bg); color: var(--rs-input-text);
				font-size: 14px; font-family: inherit; transition: border-color 0.15s;
			}
			.form-input:focus, .form-textarea:focus { outline: none; border-color: var(--rs-primary-hover); }
			.form-textarea { resize: vertical; min-height: 80px; }
			.form-actions { display: flex; gap: 8px; }
			.btn-primary {
				padding: 8px 20px; border-radius: 8px; border: none;
				background: linear-gradient(to right, var(--rs-primary-hover), var(--rs-primary)); color: white;
				font-size: 13px; font-weight: 600; cursor: pointer; transition: opacity 0.15s;
			}
			.btn-primary:hover { opacity: 0.9; }
			.btn-ghost {
				padding: 8px 16px; border-radius: 8px; border: 1px solid var(--rs-border);
				background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 13px;
			}
			.btn-ghost:hover { border-color: var(--rs-border-strong); color: var(--rs-text-primary); }
			.btn-new {
				padding: 8px 16px; border-radius: 8px; border: 1px dashed var(--rs-border);
				background: transparent; color: var(--rs-primary-hover); cursor: pointer; font-size: 13px;
				font-weight: 500; transition: all 0.15s;
			}
			.btn-new:hover { border-color: var(--rs-primary-hover); background: rgba(129,140,248,0.05); }

			/* Detail view */
			.detail { background: var(--rs-bg-page); border: 1px solid var(--rs-border-subtle); border-radius: 12px; padding: 24px; }
			.detail-title { font-size: 20px; font-weight: 700; margin-bottom: 8px; }
			.detail-desc { font-size: 14px; color: var(--rs-text-secondary); line-height: 1.6; margin-bottom: 16px; }
			.detail-section { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--rs-border-subtle); }
			.detail-section-title { font-size: 12px; font-weight: 600; color: var(--rs-text-muted); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 10px; }

			.empty { text-align: center; color: var(--rs-text-muted); padding: 48px 20px; }
			.empty-icon { font-size: 2.5rem; margin-bottom: 8px; }
			.empty-text { font-size: 14px; }
			.loading { text-align: center; color: var(--rs-text-muted); padding: 48px 20px; }
			.error { text-align: center; color: var(--rs-error); padding: 16px; background: rgba(239,68,68,0.05); border-radius: 8px; margin-bottom: 12px; font-size: 13px; }

			.demo-banner {
				background: linear-gradient(135deg, rgba(129,140,248,0.08), rgba(192,132,252,0.05));
				border: 1px solid rgba(129,140,248,0.2); border-radius: 10px;
				padding: 12px 16px; margin-bottom: 16px; display: flex; align-items: center; gap: 10px;
				font-size: 13px; color: var(--rs-text-secondary);
			}
			.demo-banner strong { color: var(--rs-primary-hover); }

			/* Quadratic weight picker (compact inline) */
			.qv-weights { display: flex; gap: 4px; margin-top: 6px; flex-wrap: wrap; }
			.qv-weight {
				padding: 3px 8px; border-radius: 6px; border: 1px solid var(--rs-border);
				background: var(--rs-bg-surface); color: var(--rs-text-secondary); cursor: pointer; font-size: 11px;
				font-weight: 600; transition: all 0.12s; white-space: nowrap;
			}
			.qv-weight:hover { border-color: #f97316; color: #fb923c; background: rgba(249,115,22,0.08); }
			.qv-weight:active { transform: scale(0.95); }
			.qv-weight.down { border-color: rgba(59,130,246,0.3); }
			.qv-weight.down:hover { border-color: #3b82f6; color: #60a5fa; background: rgba(59,130,246,0.08); }

			/* Trend chart */
			.trend-section {
				background: var(--rs-bg-page); border: 1px solid var(--rs-border-subtle); border-radius: 12px;
				padding: 16px 20px; margin-bottom: 16px;
			}
			.trend-header {
				display: flex; align-items: center; justify-content: space-between;
				margin-bottom: 12px;
			}
			.trend-title { font-size: 13px; font-weight: 600; color: var(--rs-text-secondary); text-transform: uppercase; letter-spacing: 0.04em; }
			.trend-toggle {
				font-size: 11px; color: var(--rs-text-muted); cursor: pointer; background: none;
				border: 1px solid var(--rs-border); border-radius: 6px; padding: 3px 8px;
			}
			.trend-toggle:hover { border-color: var(--rs-primary-hover); color: var(--rs-primary-hover); }
			.trend-chart { position: relative; }
			.trend-chart svg { display: block; width: 100%; }
			.trend-legend {
				display: flex; gap: 14px; flex-wrap: wrap; margin-top: 10px;
			}
			.trend-legend-item {
				display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--rs-text-secondary);
			}
			.trend-legend-dot {
				width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0;
			}

			/* Elo badge on proposal cards */
			.elo-badge {
				display: inline-flex; align-items: center; gap: 2px;
				padding: 1px 6px; border-radius: 4px;
				background: rgba(251,191,36,0.1); color: #fbbf24;
				font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
			}

			/* Rank button */
			.btn-rank {
				padding: 8px 16px; border-radius: 8px; border: 1px solid rgba(251,191,36,0.4);
				background: rgba(251,191,36,0.08); color: #fbbf24; cursor: pointer; font-size: 13px;
				font-weight: 600; transition: all 0.15s; white-space: nowrap;
			}
			.btn-rank:hover { border-color: #fbbf24; background: rgba(251,191,36,0.15); }

			/* Rank view — pairwise comparison */
			.rank-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
			.rank-pair {
				display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
				margin-bottom: 20px;
			}
			.rank-card {
				background: var(--rs-bg-page); border: 2px solid var(--rs-border-subtle); border-radius: 14px;
				padding: 20px; cursor: pointer; transition: all 0.2s; position: relative;
			}
			.rank-card:hover { border-color: var(--rs-primary-hover); transform: translateY(-2px); box-shadow: var(--rs-shadow-md); }
			.rank-card.winner {
				border-color: #22c55e; background: rgba(34,197,94,0.05);
				animation: rankPulse 0.5s ease-out;
			}
			.rank-card.loser { opacity: 0.5; }
			.rank-card-title { font-size: 16px; font-weight: 700; margin-bottom: 6px; color: var(--rs-text-primary); line-height: 1.4; }
			.rank-card-desc { font-size: 13px; color: var(--rs-text-muted); margin-bottom: 12px; line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
			.rank-card-stats { display: flex; gap: 12px; flex-wrap: wrap; }
			.rank-card-stat { font-size: 12px; color: var(--rs-text-secondary); }
			.rank-card-stat strong { font-weight: 700; }
			.rank-session-info {
				display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;
				font-size: 13px; color: var(--rs-text-muted);
			}
			.rank-skip {
				padding: 6px 14px; border-radius: 8px; border: 1px solid var(--rs-border);
				background: transparent; color: var(--rs-text-muted); cursor: pointer; font-size: 12px;
				transition: all 0.15s;
			}
			.rank-skip:hover { border-color: var(--rs-border-strong); color: var(--rs-text-secondary); }

			/* Elo Leaderboard */
			.elo-leaderboard { margin-top: 20px; }
			.elo-leaderboard-title {
				font-size: 12px; font-weight: 600; color: var(--rs-text-muted); text-transform: uppercase;
				letter-spacing: 0.05em; margin-bottom: 8px;
			}
			.elo-row {
				display: flex; align-items: center; gap: 10px; padding: 8px 12px;
				border-bottom: 1px solid var(--rs-border-subtle); font-size: 13px;
			}
			.elo-row:last-child { border-bottom: none; }
			.elo-rank-num { width: 24px; font-weight: 700; color: var(--rs-text-muted); text-align: center; font-variant-numeric: tabular-nums; }
			.elo-row-title { flex: 1; color: var(--rs-text-primary); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
			.elo-row-rating { font-weight: 700; font-variant-numeric: tabular-nums; color: #fbbf24; min-width: 48px; text-align: right; }
			.elo-row-meta { font-size: 11px; color: var(--rs-text-muted); min-width: 60px; text-align: right; }

			.rank-delta {
				position: absolute; top: 8px; right: 12px;
				font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums;
			}
			.rank-delta.positive { color: #22c55e; }
			.rank-delta.negative { color: #ef4444; }

			@keyframes rankPulse {
				0% { box-shadow: 0 0 0 0 rgba(34,197,94,0.4); }
				70% { box-shadow: 0 0 0 8px rgba(34,197,94,0); }
				100% { box-shadow: none; }
			}

			@media (max-width: 768px) {
				.tally { gap: 12px; }
				.detail { padding: 16px; }
				.detail-title { font-size: 17px; }
				.trend-section { padding: 12px 14px; }
			}
			@media (max-width: 640px) {
				.rank-pair { grid-template-columns: 1fr; }
				.tally { gap: 12px; }
				.tally-value { font-size: 20px; }
			}
			@media (max-width: 480px) {
				.header-title { font-size: 15px; }
				.space-card { padding: 14px; }
				.space-name { font-size: 16px; }
				.space-meta { gap: 10px; }
				.proposal-body { padding: 10px 12px; }
				.proposal-title { font-size: 14px; }
				.proposal-desc { font-size: 12px; }
				.vote-col { width: 44px; padding: 8px 4px; }
				.vote-chevron { width: 28px; height: 24px; }
				.vote-score { font-size: 14px; }
				.score-row { gap: 8px; flex-wrap: wrap; }
				.vote-row { gap: 4px; }
				.vote-btn { padding: 5px 10px; font-size: 12px; }
				.tally-value { font-size: 18px; }
				.meta { gap: 8px; font-size: 11px; }
				.create-form { padding: 14px; }
				.form-actions { flex-direction: column; }
				.qv-weights { gap: 3px; }
				.qv-weight { font-size: 10px; padding: 2px 6px; }
				.trend-legend { gap: 8px; }
			}
		</style>

		${this.error ? `<div class="error">${this.esc(this.error)}</div>` : ""}
		${this.loading ? '<div class="loading">Loading...</div>' : ""}

		${!this.loading ? this.renderView() : ""}
		`;

		this.attachListeners();
		this._tour.renderOverlay();
	}

	startTour() { this._tour.start(); }

	private renderView(): string {
		if (this.view === "rank" && this.selectedSpace) {
			return this.renderRank();
		}
		if (this.view === "proposal" && this.selectedProposal) {
			return this.renderProposal();
		}
		if (this.view === "proposals" && this.selectedSpace) {
			return this.renderProposals();
		}
		return this.renderSpaces();
	}

	private renderSpaces(): string {
		return `
			<div class="header">
				<span class="header-title">Voting Spaces</span>
				<button class="header-back" id="btn-tour">Tour</button>
			</div>
			${this.spaces.length === 0 ? '<div class="empty"><div class="empty-icon">🗳</div><div class="empty-text">No voting spaces yet. Create one to get started.</div></div>' : ""}
			${this.spaces.map((s) => `
				<div class="space-card" data-space="${s.slug}">
					<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
						<div class="space-name">${this.esc(s.name)}</div>
						<span class="badge" style="background:rgba(129,140,248,0.1);color:var(--rs-primary-hover)">
							${s.visibility === "public" ? "Public" : s.visibility === "permissioned" ? "Permissioned" : "Private"}
						</span>
					</div>
					<div class="space-desc">${this.esc(s.description || "")}</div>
					<div class="space-meta">
						<div class="space-stat"><span class="space-stat-value">${s.promotion_threshold}</span><span class="space-stat-label">Threshold</span></div>
						<div class="space-stat"><span class="space-stat-value">${s.voting_period_days}d</span><span class="space-stat-label">Vote Period</span></div>
						<div class="space-stat"><span class="space-stat-value">${s.credits_per_day}/day</span><span class="space-stat-label">Credits</span></div>
						<div class="space-stat"><span class="space-stat-value">${s.starting_credits}</span><span class="space-stat-label">Starting</span></div>
						<div class="space-stat"><span class="space-stat-value">${s.max_credits}</span><span class="space-stat-label">Max</span></div>
					</div>
				</div>
			`).join("")}
		`;
	}

	private relativeTime(dateStr: string): string {
		const daysAgo = Math.floor((Date.now() - new Date(dateStr).getTime()) / 86400000);
		if (daysAgo === 0) return "today";
		if (daysAgo === 1) return "yesterday";
		return `${daysAgo}d ago`;
	}

	private daysLeft(dateStr: string | null): string {
		if (!dateStr) return "";
		const diff = new Date(dateStr).getTime() - Date.now();
		if (diff <= 0) return "ended";
		const days = Math.ceil(diff / 86400000);
		return `${days}d left`;
	}

	private renderProposals(): string {
		const s = this.selectedSpace!;
		const threshold = s.promotion_threshold || 100;

		// Group proposals by status
		const ranking = this.proposals.filter(p => p.status === "RANKING");
		const voting = this.proposals.filter(p => p.status === "VOTING");
		const decided = this.proposals.filter(p => p.status === "PASSED" || p.status === "FAILED");

		return `
			<div class="header">
				${this._history.canGoBack && this.spaces.length > 1 ? '<button class="header-back" data-back="spaces">&larr;</button>' : ''}
				<span class="header-title">${this.esc(s.name)}</span>
				<span class="header-sub">${this.proposals.length} proposal${this.proposals.length !== 1 ? "s" : ""}</span>
				<button class="btn-rank" data-goto-rank>&#x1F3B2; Rank</button>
				<button class="btn-new" data-toggle-create>+ New Proposal</button>
			</div>

			${this.space === "demo" ? `
				<div class="demo-banner">
					<strong>Demo mode</strong> — votes modify local state only. Create a space for real governance.
				</div>
			` : ""}

			${this.showCreateForm ? this.renderCreateForm() : ""}

			${this.renderTrendChart()}

			${voting.length > 0 ? `
				<div style="font-size:12px;font-weight:600;color:#f59e0b;text-transform:uppercase;letter-spacing:0.05em;margin:16px 0 8px;display:flex;align-items:center;gap:6px">
					<span>🗳</span> Final Voting (${voting.length})
				</div>
				${voting.map(p => this.renderProposalCard(p, threshold)).join("")}
			` : ""}

			${ranking.length > 0 ? `
				<div style="font-size:12px;font-weight:600;color:#3b82f6;text-transform:uppercase;letter-spacing:0.05em;margin:16px 0 8px;display:flex;align-items:center;gap:6px">
					<span>📊</span> Ranking (${ranking.length})
				</div>
				${ranking.map(p => this.renderProposalCard(p, threshold)).join("")}
			` : ""}

			${decided.length > 0 ? `
				<div style="font-size:12px;font-weight:600;color:var(--rs-text-muted);text-transform:uppercase;letter-spacing:0.05em;margin:16px 0 8px;display:flex;align-items:center;gap:6px">
					<span>📋</span> Decided (${decided.length})
				</div>
				${decided.map(p => this.renderProposalCard(p, threshold)).join("")}
			` : ""}

			${this.proposals.length === 0 && !this.showCreateForm ? '<div class="empty"><div class="empty-icon">📝</div><div class="empty-text">No proposals yet. Create one to get started.</div></div>' : ""}
		`;
	}

	private renderProposalCard(p: Proposal, threshold: number): string {
		const statusColor = this.getStatusColor(p.status);
		const pct = Math.min(100, (p.score / threshold) * 100);
		const totalFinal = p.final_yes + p.final_no + p.final_abstain;
		const scoreClass = p.score > 0 ? 'positive' : p.score < 0 ? 'negative' : '';

		// SVG chevrons for Reddit-style voting
		const upChevron = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>`;
		const downChevron = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>`;

		return `
			<div class="proposal" data-pid="${p.id}" data-collab-id="proposal:${p.id}">
				${p.status === "RANKING" ? `
					<div class="vote-col">
						<button class="vote-chevron up" data-vote-weight="1" data-vote-id="${p.id}" title="Upvote (+1 credit)">${upChevron}</button>
						<span class="vote-score ${scoreClass}">${Math.round(p.score)}</span>
						<button class="vote-chevron down" data-vote-weight="-1" data-vote-id="${p.id}" title="Downvote (-1 credit)">${downChevron}</button>
						<div class="qv-cost">x&sup2; cost</div>
					</div>
				` : ""}
				<div class="proposal-body">
					<div class="proposal-top">
						<span class="proposal-title" data-proposal="${p.id}">${this.esc(p.title)}</span>
						<span class="badge" style="background:${statusColor}18;color:${statusColor}">${this.getStatusIcon(p.status)} ${p.status}</span>
					</div>
					<div class="proposal-desc">${this.esc(p.description || "")}</div>

					${p.status === "RANKING" ? `
						<div class="score-row">
							<span class="score-label"><span class="score-value" style="color:#3b82f6">${Math.round(p.score)}</span> / ${threshold}</span>
							<div class="score-bar"><div class="score-fill" style="width:${pct}%;background:linear-gradient(90deg,#f97316,#818cf8)"></div></div>
						</div>
						<div class="qv-weights">
							${[2, 3, 5].map(w => `<button class="qv-weight" data-vote-weight="${w}" data-vote-id="${p.id}">+${w} (${w*w}cr)</button>`).join("")}
							<button class="qv-weight down" data-vote-weight="-2" data-vote-id="${p.id}">-2</button>
						</div>
					` : ""}

					${p.status === "VOTING" ? `
						<div class="tally">
							<div class="tally-item"><div class="tally-value" style="color:#22c55e">${p.final_yes}</div><div class="tally-label">Yes</div></div>
							<div class="tally-item"><div class="tally-value" style="color:#ef4444">${p.final_no}</div><div class="tally-label">No</div></div>
							<div class="tally-item"><div class="tally-value" style="color:#f59e0b">${p.final_abstain}</div><div class="tally-label">Abstain</div></div>
						</div>
						${totalFinal > 0 ? `<div class="tally-bar">
							<div class="tally-bar-yes" style="width:${(p.final_yes/totalFinal)*100}%"></div>
							<div class="tally-bar-no" style="width:${(p.final_no/totalFinal)*100}%"></div>
							<div class="tally-bar-abstain" style="width:${(p.final_abstain/totalFinal)*100}%"></div>
						</div>` : ""}
						<div class="vote-row" style="margin-top:10px">
							<button class="vote-btn yes" data-final-vote="YES" data-vote-id="${p.id}">Vote Yes</button>
							<button class="vote-btn no" data-final-vote="NO" data-vote-id="${p.id}">Vote No</button>
							<button class="vote-btn abstain" data-final-vote="ABSTAIN" data-vote-id="${p.id}">Abstain</button>
						</div>
					` : ""}

					${p.status === "PASSED" || p.status === "FAILED" ? `
						<div class="tally">
							<div class="tally-item"><div class="tally-value" style="color:#22c55e">${p.final_yes}</div><div class="tally-label">Yes</div></div>
							<div class="tally-item"><div class="tally-value" style="color:#ef4444">${p.final_no}</div><div class="tally-label">No</div></div>
							<div class="tally-item"><div class="tally-value" style="color:#f59e0b">${p.final_abstain}</div><div class="tally-label">Abstain</div></div>
						</div>
						${totalFinal > 0 ? `<div class="tally-bar">
							<div class="tally-bar-yes" style="width:${(p.final_yes/totalFinal)*100}%"></div>
							<div class="tally-bar-no" style="width:${(p.final_no/totalFinal)*100}%"></div>
							<div class="tally-bar-abstain" style="width:${(p.final_abstain/totalFinal)*100}%"></div>
						</div>` : ""}
					` : ""}

					<div class="meta">
						<span>${p.vote_count} vote${p.vote_count !== "1" ? "s" : ""}</span>
						${(p.elo ?? 0) !== 1500 || (p.elo_comparisons ?? 0) > 0 ? `<span class="elo-badge" title="${p.elo_comparisons ?? 0} comparisons, ${p.elo_wins ?? 0} wins">&#x26A1; ${p.elo ?? 1500}</span>` : ''}
						<span>${this.relativeTime(p.created_at)}</span>
						${p.status === "VOTING" && p.voting_ends_at ? `<span style="color:#f59e0b">${this.daysLeft(p.voting_ends_at)}</span>` : ""}
						${p.status === "RANKING" ? `<span style="color:#3b82f6">${Math.round(threshold - p.score)} to advance</span>` : ""}
					</div>
				</div>
			</div>
		`;
	}

	/** Render an SVG line chart showing score trends over time */
	private renderTrendChart(): string {
		const ranking = this.proposals.filter(p => p.status === "RANKING");
		if (ranking.length === 0 || this.scoreHistory.length < 2) return "";

		const COLORS = ["#f97316", "#3b82f6", "#a855f7", "#22c55e", "#f43f5e", "#06b6d4", "#eab308"];
		const W = 500, H = 160, PAD_L = 36, PAD_R = 12, PAD_T = 8, PAD_B = 24;
		const plotW = W - PAD_L - PAD_R;
		const plotH = H - PAD_T - PAD_B;

		// Get all scores across history for scale
		let maxScore = 10;
		for (const snap of this.scoreHistory) {
			for (const s of Object.values(snap.scores)) {
				if (s > maxScore) maxScore = s;
			}
		}
		maxScore = Math.ceil(maxScore / 10) * 10; // round up to nearest 10

		const xScale = (i: number) => PAD_L + (i / (this.scoreHistory.length - 1)) * plotW;
		const yScale = (v: number) => PAD_T + plotH - (v / maxScore) * plotH;

		// Grid lines
		const gridLines: string[] = [];
		const gridSteps = 4;
		for (let i = 0; i <= gridSteps; i++) {
			const val = Math.round((maxScore / gridSteps) * i);
			const y = yScale(val);
			gridLines.push(`<line x1="${PAD_L}" y1="${y}" x2="${W - PAD_R}" y2="${y}" stroke="#1e293b" stroke-width="1"/>`);
			gridLines.push(`<text x="${PAD_L - 6}" y="${y + 4}" text-anchor="end" fill="#475569" font-size="10">${val}</text>`);
		}

		// Time labels
		const timeLabels: string[] = [];
		const firstTime = this.scoreHistory[0].time;
		const lastTime = this.scoreHistory[this.scoreHistory.length - 1].time;
		const dayDiff = Math.max(1, Math.round((lastTime - firstTime) / 86400000));
		const labelStep = Math.max(1, Math.floor(this.scoreHistory.length / 5));
		for (let i = 0; i < this.scoreHistory.length; i += labelStep) {
			const d = new Date(this.scoreHistory[i].time);
			const label = dayDiff > 1 ? `${d.getMonth() + 1}/${d.getDate()}` : `${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`;
			timeLabels.push(`<text x="${xScale(i)}" y="${H - 4}" text-anchor="middle" fill="#475569" font-size="10">${label}</text>`);
		}

		// Lines for each proposal
		const lines: string[] = [];
		const legend: string[] = [];
		ranking.forEach((p, pIdx) => {
			const color = COLORS[pIdx % COLORS.length];
			const points: string[] = [];
			for (let i = 0; i < this.scoreHistory.length; i++) {
				const score = this.scoreHistory[i].scores[p.id] ?? 0;
				points.push(`${xScale(i)},${yScale(score)}`);
			}
			lines.push(`<polyline points="${points.join(" ")}" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.9"/>`);
			// End dot
			const lastScore = this.scoreHistory[this.scoreHistory.length - 1].scores[p.id] ?? 0;
			const lastX = xScale(this.scoreHistory.length - 1);
			const lastY = yScale(lastScore);
			lines.push(`<circle cx="${lastX}" cy="${lastY}" r="4" fill="${color}" stroke="#0f172a" stroke-width="2"/>`);
			// Legend
			const shortTitle = p.title.length > 30 ? p.title.slice(0, 28) + '...' : p.title;
			legend.push(`<span class="trend-legend-item"><span class="trend-legend-dot" style="background:${color}"></span>${this.esc(shortTitle)}</span>`);
		});

		return `
			<div class="trend-section">
				<div class="trend-header">
					<span class="trend-title">Priority Trends</span>
					<button class="trend-toggle" data-toggle-trend>${this.showTrendChart ? 'Hide' : 'Show'}</button>
				</div>
				${this.showTrendChart ? `
					<div class="trend-chart">
						<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet">
							${gridLines.join("")}
							${timeLabels.join("")}
							${lines.join("")}
						</svg>
					</div>
					<div class="trend-legend">${legend.join("")}</div>
				` : ""}
			</div>
		`;
	}

	private async loadPair() {
		if (!this.selectedSpace) return;
		this.rankLoading = true;
		this.rankResult = null;
		this.render();
		try {
			if (this.space === "demo") {
				// Demo mode: pick two random RANKING proposals
				const ranking = this.proposals.filter(p => p.status === "RANKING");
				if (ranking.length < 2) {
					this.rankPairA = null;
					this.rankPairB = null;
				} else {
					const shuffled = [...ranking].sort(() => Math.random() - 0.5);
					this.rankPairA = shuffled[0];
					this.rankPairB = shuffled[1];
				}
			} else {
				const base = this.getApiBase();
				const res = await fetch(`${base}/api/proposals/pair?space_slug=${this.selectedSpace.slug}`);
				if (res.ok) {
					const data = await res.json();
					this.rankPairA = data.a;
					this.rankPairB = data.b;
				} else {
					this.rankPairA = null;
					this.rankPairB = null;
				}
			}
		} catch {
			this.rankPairA = null;
			this.rankPairB = null;
		}
		this.rankLoading = false;
		this.render();
	}

	private async submitComparison(winnerId: string, loserId: string) {
		this.rankResult = { winnerId, delta: 0 };
		this.render();

		if (this.space === "demo") {
			// Simple demo Elo update
			const winner = this.proposals.find(p => p.id === winnerId);
			const loser = this.proposals.find(p => p.id === loserId);
			if (winner && loser) {
				const wElo = winner.elo ?? 1500;
				const lElo = loser.elo ?? 1500;
				const expected = 1 / (1 + Math.pow(10, (lElo - wElo) / 400));
				const delta = Math.round(32 * (1 - expected));
				winner.elo = wElo + delta;
				winner.elo_comparisons = (winner.elo_comparisons ?? 0) + 1;
				winner.elo_wins = (winner.elo_wins ?? 0) + 1;
				loser.elo = lElo - delta;
				loser.elo_comparisons = (loser.elo_comparisons ?? 0) + 1;
				this.rankResult = { winnerId, delta };
				this.rankCompareCount++;
				this.render();
			}
			setTimeout(() => this.loadPair(), 500);
			return;
		}

		try {
			const base = this.getApiBase();
			const res = await authFetch(`${base}/api/proposals/compare`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ winnerId, loserId }),
			});
			if (res.ok) {
				const data = await res.json();
				this.rankResult = { winnerId, delta: data.winner.delta };
				this.rankCompareCount++;
			}
		} catch { /* fail silently */ }

		this.render();
		setTimeout(() => this.loadPair(), 500);
	}

	private renderRank(): string {
		const s = this.selectedSpace!;
		const ranking = this.proposals.filter(p => p.status === "RANKING")
			.sort((a, b) => (b.elo ?? 1500) - (a.elo ?? 1500));

		let pairHtml = '';
		if (this.rankLoading) {
			pairHtml = '<div class="loading">Loading pair...</div>';
		} else if (!this.rankPairA || !this.rankPairB) {
			pairHtml = `<div class="empty"><div class="empty-icon">&#x1F3B2;</div><div class="empty-text">Need at least 2 ranking proposals to compare.</div></div>`;
		} else {
			const a = this.rankPairA;
			const b = this.rankPairB;
			const aIsWinner = this.rankResult?.winnerId === a.id;
			const bIsWinner = this.rankResult?.winnerId === b.id;

			pairHtml = `
				<div class="rank-session-info">
					<span>Which proposal should be higher priority?</span>
					<button class="rank-skip" data-rank-skip>Skip pair</button>
				</div>
				<div class="rank-pair">
					<div class="rank-card${aIsWinner ? ' winner' : this.rankResult && !aIsWinner ? ' loser' : ''}" data-rank-pick="${a.id}" data-rank-lose="${b.id}">
						${this.rankResult && aIsWinner ? `<span class="rank-delta positive">+${this.rankResult.delta}</span>` : ''}
						${this.rankResult && !aIsWinner ? `<span class="rank-delta negative">-${this.rankResult.delta}</span>` : ''}
						<div class="rank-card-title">${this.esc(a.title)}</div>
						<div class="rank-card-desc">${this.esc(a.description || '')}</div>
						<div class="rank-card-stats">
							<span class="rank-card-stat">&#x26A1; <strong>${a.elo ?? 1500}</strong></span>
							<span class="rank-card-stat">${a.elo_comparisons ?? 0} comparisons</span>
							<span class="rank-card-stat">Score: ${Math.round(a.score)}</span>
						</div>
					</div>
					<div class="rank-card${bIsWinner ? ' winner' : this.rankResult && !bIsWinner ? ' loser' : ''}" data-rank-pick="${b.id}" data-rank-lose="${a.id}">
						${this.rankResult && bIsWinner ? `<span class="rank-delta positive">+${this.rankResult.delta}</span>` : ''}
						${this.rankResult && !bIsWinner ? `<span class="rank-delta negative">-${this.rankResult.delta}</span>` : ''}
						<div class="rank-card-title">${this.esc(b.title)}</div>
						<div class="rank-card-desc">${this.esc(b.description || '')}</div>
						<div class="rank-card-stats">
							<span class="rank-card-stat">&#x26A1; <strong>${b.elo ?? 1500}</strong></span>
							<span class="rank-card-stat">${b.elo_comparisons ?? 0} comparisons</span>
							<span class="rank-card-stat">Score: ${Math.round(b.score)}</span>
						</div>
					</div>
				</div>
			`;
		}

		// Elo leaderboard
		const leaderboard = ranking.map((p, i) => `
			<div class="elo-row">
				<span class="elo-rank-num">${i + 1}</span>
				<span class="elo-row-title">${this.esc(p.title)}</span>
				<span class="elo-row-rating">&#x26A1; ${p.elo ?? 1500}</span>
				<span class="elo-row-meta">${p.elo_comparisons ?? 0} cmp / ${p.elo_wins ?? 0}W</span>
			</div>
		`).join('');

		return `
			<div class="rank-header">
				<button class="header-back" data-back="proposals">&larr;</button>
				<span class="header-title">&#x1F3B2; Pairwise Ranking</span>
				<span class="header-sub">${this.rankCompareCount} comparisons this session</span>
			</div>

			${this.space === "demo" ? `
				<div class="demo-banner">
					<strong>Demo mode</strong> — Elo updates are local only. Create a space for persistent rankings.
				</div>
			` : ""}

			${pairHtml}

			<div class="elo-leaderboard">
				<div class="elo-leaderboard-title">Elo Leaderboard</div>
				${leaderboard || '<div class="empty"><div class="empty-text">No ranking proposals yet.</div></div>'}
			</div>
		`;
	}

	private renderCreateForm(): string {
		return `
			<div class="create-form">
				<h3>New Proposal</h3>
				<div class="form-field">
					<label class="form-label">Title</label>
					<input class="form-input" id="create-title" placeholder="What should we do?" autofocus>
				</div>
				<div class="form-field">
					<label class="form-label">Description</label>
					<textarea class="form-textarea" id="create-desc" placeholder="Explain the proposal and why it matters..."></textarea>
				</div>
				<div class="form-actions">
					<button class="btn-primary" data-submit-create>Submit Proposal</button>
					<button class="btn-ghost" data-cancel-create>Cancel</button>
				</div>
			</div>
		`;
	}

	private renderProposal(): string {
		const p = this.selectedProposal!;
		const threshold = this.selectedSpace?.promotion_threshold || 100;
		const pct = Math.min(100, (p.score / threshold) * 100);
		const totalFinal = p.final_yes + p.final_no + p.final_abstain;

		return `
			<div class="header">
				${this._history.canGoBack ? '<button class="header-back" data-back="proposals">&larr; Proposals</button>' : ''}
				<span class="header-title">${this.esc(p.title)}</span>
				<span class="badge" style="background:${this.getStatusColor(p.status)}18;color:${this.getStatusColor(p.status)}">${this.getStatusIcon(p.status)} ${p.status}</span>
			</div>
			<div class="detail">
				<div class="detail-desc">${this.esc(p.description || "No description provided.")}</div>
				<div class="meta" style="margin-top:0">
					<span>${p.vote_count} votes</span>
					<span>Created ${this.relativeTime(p.created_at)}</span>
					${p.voting_ends_at ? `<span>Voting ${this.daysLeft(p.voting_ends_at)}</span>` : ""}
				</div>

				${p.status === "RANKING" ? `
					<div class="detail-section">
						<div class="detail-section-title">Conviction Score</div>
						<div class="score-row">
							<span class="score-label"><span class="score-value" style="color:#3b82f6;font-size:24px">${Math.round(p.score)}</span> / ${threshold}</span>
							<div class="score-bar" style="height:10px"><div class="score-fill" style="width:${pct}%;background:linear-gradient(90deg,#3b82f6,#818cf8)"></div></div>
						</div>
						<div style="font-size:12px;color:var(--rs-text-muted);margin:8px 0 12px">
							Needs ${Math.max(0, Math.round(threshold - p.score))} more to advance to final vote. Cost: weight&sup2; credits.
						</div>
						<div class="vote-row">
							${[1, 2, 3, 5].map(w => `<button class="vote-btn" data-vote-weight="${w}">+${w} (${w * w} credits)</button>`).join("")}
							<span class="vote-sep">|</span>
							${[-1, -2].map(w => `<button class="vote-btn" data-vote-weight="${w}" style="border-color:rgba(239,68,68,0.3);color:#ef4444">${w}</button>`).join("")}
						</div>
					</div>
				` : ""}

				${p.status === "VOTING" ? `
					<div class="detail-section">
						<div class="detail-section-title">Final Vote — ${this.selectedSpace?.voting_period_days || 7}-day period</div>
						<div class="tally">
							<div class="tally-item"><div class="tally-value" style="color:#22c55e">${p.final_yes}</div><div class="tally-label">Yes</div></div>
							<div class="tally-item"><div class="tally-value" style="color:#ef4444">${p.final_no}</div><div class="tally-label">No</div></div>
							<div class="tally-item"><div class="tally-value" style="color:#f59e0b">${p.final_abstain}</div><div class="tally-label">Abstain</div></div>
						</div>
						${totalFinal > 0 ? `<div class="tally-bar" style="margin:8px 0">
							<div class="tally-bar-yes" style="width:${(p.final_yes/totalFinal)*100}%"></div>
							<div class="tally-bar-no" style="width:${(p.final_no/totalFinal)*100}%"></div>
							<div class="tally-bar-abstain" style="width:${(p.final_abstain/totalFinal)*100}%"></div>
						</div>` : ""}
						<div class="vote-row">
							<button class="vote-btn yes" data-final-vote="YES">Vote Yes</button>
							<button class="vote-btn no" data-final-vote="NO">Vote No</button>
							<button class="vote-btn abstain" data-final-vote="ABSTAIN">Abstain</button>
						</div>
					</div>
				` : ""}

				${p.status === "PASSED" || p.status === "FAILED" ? `
					<div class="detail-section">
						<div class="detail-section-title">Result: ${p.status}</div>
						<div class="tally">
							<div class="tally-item"><div class="tally-value" style="color:#22c55e">${p.final_yes}</div><div class="tally-label">Yes</div></div>
							<div class="tally-item"><div class="tally-value" style="color:#ef4444">${p.final_no}</div><div class="tally-label">No</div></div>
							<div class="tally-item"><div class="tally-value" style="color:#f59e0b">${p.final_abstain}</div><div class="tally-label">Abstain</div></div>
						</div>
						${totalFinal > 0 ? `<div class="tally-bar">
							<div class="tally-bar-yes" style="width:${(p.final_yes/totalFinal)*100}%"></div>
							<div class="tally-bar-no" style="width:${(p.final_no/totalFinal)*100}%"></div>
							<div class="tally-bar-abstain" style="width:${(p.final_abstain/totalFinal)*100}%"></div>
						</div>` : ""}
						<div style="font-size:13px;color:var(--rs-text-muted);margin-top:8px">
							Conviction score was <strong style="color:${this.getStatusColor(p.status)}">${Math.round(p.score)}</strong>
						</div>
					</div>
				` : ""}
			</div>
		`;
	}

	private attachListeners() {
		this.shadow.getElementById("btn-tour")?.addEventListener("click", () => this.startTour());
		// Space cards
		this.shadow.querySelectorAll("[data-space]").forEach((el) => {
			el.addEventListener("click", () => {
				const slug = (el as HTMLElement).dataset.space!;
				this._history.push(this.view);
				this._history.push("proposals", { space: slug });
				this.selectedSpace = this.spaces.find((s) => s.slug === slug) || null;
				this.view = "proposals";
				if (this.space === "demo") this.render();
				else this.loadProposals(slug);
			});
		});

		// Proposal title click → detail view
		this.shadow.querySelectorAll("[data-proposal]").forEach((el) => {
			el.addEventListener("click", (e) => {
				e.stopPropagation();
				const id = (el as HTMLElement).dataset.proposal!;
				this._history.push(this.view);
				this._history.push("proposal", { proposalId: id });
				this.view = "proposal";
				if (this.space === "demo") {
					this.selectedProposal = this.proposals.find((p) => p.id === id) || null;
					this.render();
				} else {
					this.loadProposal(id);
				}
			});
		});

		// Back buttons
		this.shadow.querySelectorAll("[data-back]").forEach((el) => {
			el.addEventListener("click", (e) => {
				e.stopPropagation();
				this.goBack();
			});
		});

		// Inline conviction vote buttons (on proposal cards)
		this.shadow.querySelectorAll("[data-vote-weight]").forEach((el) => {
			el.addEventListener("click", (e) => {
				e.stopPropagation();
				const weight = parseInt((el as HTMLElement).dataset.voteWeight!);
				const id = (el as HTMLElement).dataset.voteId || this.selectedProposal?.id;
				if (id) this.castVote(id, weight);
			});
		});

		// Final vote buttons
		this.shadow.querySelectorAll("[data-final-vote]").forEach((el) => {
			el.addEventListener("click", (e) => {
				e.stopPropagation();
				const vote = (el as HTMLElement).dataset.finalVote!;
				const id = (el as HTMLElement).dataset.voteId || this.selectedProposal?.id;
				if (id) this.castFinalVote(id, vote);
			});
		});

		// Rank view button
		this.shadow.querySelector("[data-goto-rank]")?.addEventListener("click", () => {
			this._history.push(this.view);
			this.view = "rank";
			this.loadPair();
		});

		// Rank pick (click a card to select winner)
		this.shadow.querySelectorAll("[data-rank-pick]").forEach(el => {
			el.addEventListener("click", () => {
				if (this.rankResult) return; // already picked
				const winnerId = (el as HTMLElement).dataset.rankPick!;
				const loserId = (el as HTMLElement).dataset.rankLose!;
				this.submitComparison(winnerId, loserId);
			});
		});

		// Rank skip
		this.shadow.querySelector("[data-rank-skip]")?.addEventListener("click", () => {
			this.loadPair();
		});

		// Toggle trend chart
		this.shadow.querySelector("[data-toggle-trend]")?.addEventListener("click", () => {
			this.showTrendChart = !this.showTrendChart;
			this.render();
		});

		// Toggle create form
		this.shadow.querySelector("[data-toggle-create]")?.addEventListener("click", () => {
			this.showCreateForm = !this.showCreateForm;
			this.render();
		});

		// Cancel create
		this.shadow.querySelector("[data-cancel-create]")?.addEventListener("click", () => {
			this.showCreateForm = false;
			this.render();
		});

		// Submit create
		this.shadow.querySelector("[data-submit-create]")?.addEventListener("click", () => {
			const titleInput = this.shadow.getElementById("create-title") as HTMLInputElement;
			const descInput = this.shadow.getElementById("create-desc") as HTMLTextAreaElement;
			const title = titleInput?.value?.trim();
			const desc = descInput?.value?.trim();
			if (!title) return;
			this.createProposal(title, desc || "");
		});
	}

	private _onViewRestored = (e: CustomEvent) => {
		if (e.detail?.moduleId !== 'rvote') return;
		this.view = e.detail.view;
		this.render();
	};

	private goBack() {
		const prev = this._history.back();
		if (!prev) return;
		this.view = prev.view;
		this.render();
	}

	private esc(s: string): string {
		const d = document.createElement("div");
		d.textContent = s || "";
		return d.innerHTML;
	}
}

customElements.define("folk-vote-dashboard", FolkVoteDashboard);
