/**
 * <folk-choices-dashboard> — lists choice shapes (polls, rankings, spider charts)
 * from the current space and links to the canvas to create/interact with them.
 *
 * Multiplayer: uses ChoicesLocalFirstClient for real-time voting session sync.
 */

import { TourEngine } from "../../../shared/tour-engine";
import { ChoicesLocalFirstClient } from "../local-first-client";
import type { ChoicesDoc, ChoiceSession, ChoiceVote } from "../schemas";
import { choicesSchema, choicesDocId } from "../schemas";
import { startPresenceHeartbeat } from '../../../shared/collab-presence';
import { getModuleApiBase, rspaceNavUrl, getCurrentSpace } from "../../../shared/url-helpers";
import type { DocumentId } from "../../../shared/local-first/document";

// ── CrowdSurf types ──
interface CrowdSurfOption {
	optionId: string;
	label: string;
	color: string;
	sessionId: string;
	sessionTitle: string;
	sessionType: 'vote' | 'rank' | 'score';
}

// ── Auth helpers ──
function getSession(): { accessToken: string; claims: { sub: string; did?: string; username?: string } } | null {
	try {
		const raw = localStorage.getItem("encryptid_session");
		if (!raw) return null;
		const s = JSON.parse(raw);
		return s?.accessToken ? s : null;
	} catch { return null; }
}
function getMyDid(): string | null {
	const s = getSession();
	if (!s) return null;
	return (s.claims as any).did || s.claims.sub;
}

class FolkChoicesDashboard extends HTMLElement {
	private shadow: ShadowRoot;
	private choices: any[] = [];
	private loading = true;
	private space: string;

	/* Demo state */
	private demoTab: "spider" | "ranking" | "voting" | "crowdsurf" = "spider";
	private hoveredPerson: string | null = null;
	private rankItems: { id: number; name: string; emoji: string }[] = [];
	private rankDragging: number | null = null;
	private voteOptions: { id: string; name: string; color: string; votes: number }[] = [];
	private voted = false;
	private votedId: string | null = null;
	private simTimer: number | null = null;

	/* Multiplayer state */
	private lfClient: ChoicesLocalFirstClient | null = null;
	private _lfcUnsub: (() => void) | null = null;
	private _subscribedDocIds: string[] = [];
	private _stopPresence: (() => void) | null = null;
	private sessions: ChoiceSession[] = [];
	private activeSessionId: string | null = null;
	private sessionVotes: Map<string, ChoiceVote[]> = new Map();

	/* CrowdSurf inline state */
	private csOptions: CrowdSurfOption[] = [];
	private csCurrentIndex = 0;
	private csSwipedMap: Map<string, 'right' | 'left'> = new Map();
	private csIsDragging = false;
	private csStartX = 0;
	private csCurrentX = 0;
	private csIsAnimating = false;
	private _csTransitionTimer: number | null = null;

	// Guided tour
	private _tour!: TourEngine;
	private static readonly TOUR_STEPS = [
		{ target: '.demo-content', title: "rChoices", message: "Explore spider charts, rankings, live voting, and CrowdSurf swipe cards. Use the sub-nav above to switch between modes.", advanceOnClick: true },
	];

	constructor() {
		super();
		this.shadow = this.attachShadow({ mode: "open" });
		this.space = this.getAttribute("space") || "demo";
		const tabAttr = this.getAttribute("tab") as "spider" | "ranking" | "voting" | "crowdsurf" | null;
		if (tabAttr && ["spider", "ranking", "voting", "crowdsurf"].includes(tabAttr)) {
			this.demoTab = tabAttr;
		}
		this._tour = new TourEngine(
			this.shadow,
			FolkChoicesDashboard.TOUR_STEPS,
			"rchoices_tour_done",
			() => this.shadow.host as HTMLElement,
		);
	}

	connectedCallback() {
		if (this.space === "demo") {
			this.loadDemoData();
		} else {
			this.initMultiplayer();
		}
		if (!localStorage.getItem("rchoices_tour_done")) {
			setTimeout(() => this._tour.start(), 1200);
		}
		this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rchoices', context: this.sessions.find(s => s.id === this.activeSessionId)?.title || 'Choices' }));
	}

	disconnectedCallback() {
		this._stopPresence?.();
		if (this.simTimer !== null) {
			clearInterval(this.simTimer);
			this.simTimer = null;
		}
		if (this._csTransitionTimer !== null) {
			clearTimeout(this._csTransitionTimer);
			this._csTransitionTimer = null;
		}
		this._lfcUnsub?.();
		this._lfcUnsub = null;
		this.lfClient?.disconnect();
		const runtime = (window as any).__rspaceOfflineRuntime;
		if (runtime) {
			for (const id of this._subscribedDocIds) runtime.unsubscribe(id);
		}
		this._subscribedDocIds = [];
	}

	private async initMultiplayer() {
		this.loading = true;
		this.render();

		try {
			this.lfClient = new ChoicesLocalFirstClient(this.space);
			await this.lfClient.init();
			await this.lfClient.subscribe();

			this._lfcUnsub = this.lfClient.onChange((doc) => {
				this.extractSessions(doc);
				this.render();
				this.bindLiveEvents();
			});

			const doc = this.lfClient.getDoc();
			if (doc) this.extractSessions(doc);
		} catch (err) {
			console.warn('[rChoices] Local-first init failed, falling back to API:', err);
		}
		this.subscribeCollabOverlay();

		// Also load canvas-based choices
		await this.loadChoices();

		this.loading = false;
		this.render();
		this.bindLiveEvents();
	}

	private async subscribeCollabOverlay() {
		const runtime = (window as any).__rspaceOfflineRuntime;
		if (!runtime?.isInitialized) return;
		try {
			const docId = choicesDocId(this.space) as DocumentId;
			await runtime.subscribe(docId, choicesSchema);
			this._subscribedDocIds.push(docId);
		} catch { /* runtime unavailable */ }
	}

	private extractSessions(doc: ChoicesDoc) {
		this.sessions = doc.sessions ? Object.values(doc.sessions).sort((a, b) => b.createdAt - a.createdAt) : [];
		// Pre-compute votes per session
		this.sessionVotes.clear();
		if (doc.votes) {
			for (const [key, vote] of Object.entries(doc.votes)) {
				const sessionId = key.split(':')[0];
				if (!this.sessionVotes.has(sessionId)) this.sessionVotes.set(sessionId, []);
				this.sessionVotes.get(sessionId)!.push(vote);
			}
		}
	}

	private createSession() {
		const title = prompt('Poll title:');
		if (!title || !this.lfClient) return;

		const optionsRaw = prompt('Options (comma-separated):');
		if (!optionsRaw) return;

		const colors = ['#3b82f6', '#ef4444', '#f59e0b', '#10b981', '#8b5cf6', '#06b6d4', '#ec4899', '#f97316'];
		const options = optionsRaw.split(',').map((label, i) => ({
			id: crypto.randomUUID(),
			label: label.trim(),
			color: colors[i % colors.length],
		})).filter(o => o.label);

		const session: ChoiceSession = {
			id: crypto.randomUUID(),
			title,
			type: 'vote',
			mode: 'single',
			options,
			createdBy: getMyDid(),
			createdAt: Date.now(),
			closed: false,
		};

		this.lfClient.createSession(session);
		this.activeSessionId = session.id;
	}

	private castVoteOnSession(sessionId: string, optionId: string) {
		const myDid = getMyDid();
		if (!myDid || !this.lfClient) return;
		this.lfClient.castVote(sessionId, myDid, { [optionId]: 1 });
	}

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

	private async loadChoices() {
		this.loading = true;
		this.render();
		try {
			const res = await fetch(`${this.getApiBase()}/api/choices`);
			const data = await res.json();
			this.choices = data.choices || [];
		} catch (e) {
			console.error("Failed to load choices:", e);
		}
		this.loading = false;
		this.render();
	}

	private render() {
		const typeIcons: Record<string, string> = {
			"folk-choice-vote": "☑",
			"folk-choice-rank": "📊",
			"folk-choice-spider": "🕸",
		};
		const typeLabels: Record<string, string> = {
			"folk-choice-vote": "Poll",
			"folk-choice-rank": "Ranking",
			"folk-choice-spider": "Spider Chart",
		};

		const isLive = this.lfClient?.isConnected ?? false;

		this.shadow.innerHTML = `
		<style>
			:host { display: block; padding: 1.5rem; -webkit-tap-highlight-color: transparent; }
			button, a, input, select, textarea, [role="button"] { touch-action: manipulation; }
			.rapp-nav { display: flex; gap: 8px; align-items: center; margin-bottom: 1rem; min-height: 36px; }
			.rapp-nav__title { font-size: 15px; font-weight: 600; flex: 1; color: var(--rs-text-primary); }
			.create-btns { display: flex; gap: 0.5rem; }
			.create-btn { padding: 0.5rem 1rem; border-radius: 8px; border: 1px solid var(--rs-border); background: var(--rs-bg-surface); color: var(--rs-text-secondary); cursor: pointer; font-size: 0.875rem; text-decoration: none; font-family: inherit; }
			.create-btn:hover { border-color: var(--rs-primary); color: var(--rs-text-primary); }
			.live-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #22c55e; margin-right: 4px; animation: pulse-dot 2s infinite; }
			@keyframes pulse-dot { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
			.live-badge { font-size: 0.7rem; padding: 2px 8px; border-radius: 999px; background: rgba(34,197,94,0.15); color: #22c55e; font-weight: 500; display: flex; align-items: center; gap: 2px; }

			/* Sessions */
			.section-label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--rs-text-muted); margin-bottom: 0.75rem; font-weight: 600; }
			.sessions-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 0.75rem; margin-bottom: 1.5rem; }
			.session-card { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 10px; padding: 1rem; cursor: pointer; transition: border-color 0.15s; }
			.session-card:hover { border-color: var(--rs-primary); }
			.session-card.closed { opacity: 0.6; }
			.session-title { font-weight: 600; font-size: 0.95rem; color: var(--rs-text-primary); margin-bottom: 4px; }
			.session-meta { font-size: 0.8rem; color: var(--rs-text-secondary); display: flex; gap: 0.75rem; }
			.session-status { font-size: 0.65rem; padding: 2px 6px; border-radius: 999px; font-weight: 500; }
			.session-status.open { background: rgba(34,197,94,0.15); color: #22c55e; }
			.session-status.closed { background: rgba(239,68,68,0.15); color: #ef4444; }

			/* Active session */
			.active-session { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
			.active-header { display: flex; align-items: center; gap: 8px; margin-bottom: 1rem; }
			.active-title { font-size: 1.1rem; font-weight: 700; color: var(--rs-text-primary); flex: 1; }
			.back-btn { padding: 4px 10px; border-radius: 6px; border: 1px solid var(--rs-border); background: var(--rs-bg-surface); color: var(--rs-text-secondary); cursor: pointer; font-size: 0.8rem; font-family: inherit; }
			.back-btn:hover { border-color: var(--rs-primary); color: var(--rs-text-primary); }
			.session-actions { display: flex; gap: 6px; }
			.session-action-btn { padding: 4px 10px; border-radius: 6px; border: 1px solid var(--rs-border); background: var(--rs-bg-surface); color: var(--rs-text-secondary); cursor: pointer; font-size: 0.75rem; font-family: inherit; }
			.session-action-btn:hover { border-color: var(--rs-primary); color: var(--rs-text-primary); }
			.session-action-btn.danger:hover { border-color: var(--rs-error, #ef4444); color: #fca5a5; }

			/* Vote bars */
			.vote-bar-row { display: flex; align-items: center; gap: 10px; padding: 0.6rem 0.75rem; margin-bottom: 6px; background: var(--rs-bg-page); border: 1px solid var(--rs-border); border-radius: 8px; cursor: pointer; position: relative; overflow: hidden; transition: border-color 0.15s; }
			.vote-bar-row:hover { border-color: var(--rs-primary); }
			.vote-bar-row.my-vote { border-color: var(--rs-primary); }
			.vote-bar-fill { position: absolute; left: 0; top: 0; bottom: 0; opacity: 0.12; transition: width 0.5s ease-out; pointer-events: none; }
			.vote-bar-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; position: relative; z-index: 1; }
			.vote-bar-label { flex: 1; font-weight: 600; font-size: 0.9rem; color: var(--rs-text-primary); position: relative; z-index: 1; }
			.vote-bar-count { font-size: 0.8rem; color: var(--rs-text-secondary); position: relative; z-index: 1; min-width: 20px; text-align: right; font-variant-numeric: tabular-nums; }
			.vote-bar-pct { font-size: 0.8rem; font-weight: 600; position: relative; z-index: 1; min-width: 40px; text-align: right; font-variant-numeric: tabular-nums; }
			.vote-bar-check { position: relative; z-index: 1; font-size: 0.75rem; }
			.vote-summary { text-align: center; font-size: 0.8rem; color: var(--rs-text-muted); margin-top: 0.75rem; }

			.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem; }
			.card { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 12px; padding: 1.25rem; cursor: pointer; text-decoration: none; display: block; }
			.card:hover { border-color: var(--rs-primary); }
			.card-icon { font-size: 1.5rem; margin-bottom: 0.5rem; }
			.card-title { color: var(--rs-text-primary); font-weight: 600; font-size: 1rem; margin: 0 0 0.25rem; }
			.card-type { color: var(--rs-primary-hover); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; }
			.card-meta { color: var(--rs-text-secondary); font-size: 0.8125rem; }
			.stat { display: inline-block; margin-right: 1rem; }
			.empty { text-align: center; padding: 3rem; color: var(--rs-text-muted); }
			.empty-icon { font-size: 3rem; margin-bottom: 1rem; }
			.empty p { margin: 0.5rem 0; font-size: 0.875rem; }
			.loading { text-align: center; padding: 3rem; color: var(--rs-text-secondary); }
			.info { background: rgba(99,102,241,0.1); border: 1px solid rgba(99,102,241,0.2); border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem; color: #a5b4fc; font-size: 0.8125rem; }
			.divider { border: none; border-top: 1px solid var(--rs-border); margin: 1.5rem 0; }
		</style>

		<div class="rapp-nav">
			<span class="rapp-nav__title">Choices</span>
			${isLive ? `<span class="live-badge"><span class="live-dot"></span>LIVE</span>` : ''}
			<div class="create-btns">
				${this.lfClient ? `<button class="create-btn" data-action="new-session">+ New Poll</button>` : ''}
				<a class="create-btn" href="${rspaceNavUrl(this.space, "rspace")}" title="Open canvas to create choices">+ Canvas</a>
			</div>
		</div>

		${this.loading ? `<div class="loading">Loading...</div>` : this.renderLiveContent(typeIcons, typeLabels)}
		`;

		this.bindLiveEvents();
	}

	private renderLiveContent(typeIcons: Record<string, string>, typeLabels: Record<string, string>): string {
		let html = '';

		// Active session view
		if (this.activeSessionId) {
			const session = this.sessions.find(s => s.id === this.activeSessionId);
			if (session) {
				html += this.renderActiveSession(session);
				return html;
			}
			this.activeSessionId = null;
		}

		// Session cards
		if (this.sessions.length > 0) {
			html += `<div class="section-label">Live Polls</div>`;
			html += this.renderSessionsList();
		}

		// Canvas choices
		if (this.choices.length > 0) {
			if (this.sessions.length > 0) html += `<hr class="divider">`;
			html += `<div class="section-label">Canvas Choices</div>`;
			html += this.renderGrid(typeIcons, typeLabels);
		}

		if (this.sessions.length === 0 && this.choices.length === 0) {
			html += this.renderEmpty();
		}

		return html;
	}

	private renderSessionsList(): string {
		const cards = this.sessions.map(session => {
			const votes = this.sessionVotes.get(session.id) || [];
			const status = session.closed ? 'closed' : 'open';
			const timeAgo = this.timeAgo(session.createdAt);
			return `<div class="session-card${session.closed ? ' closed' : ''}" data-session-id="${session.id}">
				<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px">
					<span class="session-status ${status}">${status}</span>
					<span class="session-title">${this.esc(session.title)}</span>
				</div>
				<div class="session-meta">
					<span>${session.options.length} options</span>
					<span>${votes.length} vote${votes.length !== 1 ? 's' : ''}</span>
					<span>${timeAgo}</span>
				</div>
			</div>`;
		}).join('');
		return `<div class="sessions-grid">${cards}</div>`;
	}

	private renderActiveSession(session: ChoiceSession): string {
		const myDid = getMyDid();
		const votes = this.sessionVotes.get(session.id) || [];
		const myVote = myDid ? this.lfClient?.getMyVote(session.id, myDid) ?? null : null;
		const totalVotes = votes.length;

		// Tally votes per option
		const tally: Record<string, number> = {};
		for (const opt of session.options) tally[opt.id] = 0;
		for (const vote of votes) {
			for (const [optId, val] of Object.entries(vote.choices)) {
				if (val > 0) tally[optId] = (tally[optId] || 0) + 1;
			}
		}
		const bars = session.options.map(opt => {
			const count = tally[opt.id] || 0;
			const pct = totalVotes > 0 ? (count / totalVotes) * 100 : 0;
			const isMyVote = myVote?.choices?.[opt.id] && myVote.choices[opt.id] > 0;
			return `<div class="vote-bar-row${isMyVote ? ' my-vote' : ''}" data-vote-option="${opt.id}" data-vote-session="${session.id}">
				<div class="vote-bar-fill" style="width:${pct}%;background:${opt.color}"></div>
				<span class="vote-bar-dot" style="background:${opt.color}"></span>
				<span class="vote-bar-label">${this.esc(opt.label)}</span>
				${isMyVote ? `<span class="vote-bar-check">✓</span>` : ''}
				<span class="vote-bar-count">${count}</span>
				<span class="vote-bar-pct" style="color:${opt.color}">${pct.toFixed(0)}%</span>
			</div>`;
		}).join('');

		const isOwner = myDid && session.createdBy === myDid;

		return `<div class="active-session">
			<div class="active-header">
				<button class="back-btn" data-action="back-to-list">&larr; Back</button>
				<span class="active-title">${this.esc(session.title)}</span>
				${session.closed ? `<span class="session-status closed">closed</span>` : `<span class="session-status open">open</span>`}
			</div>
			${!session.closed && !myVote ? `<div class="vote-summary">Tap an option to vote</div>` : ''}
			${bars}
			<div class="vote-summary">${totalVotes} vote${totalVotes !== 1 ? 's' : ''} total</div>
			${isOwner ? `<div class="session-actions" style="margin-top:0.75rem;justify-content:flex-end">
				${!session.closed ? `<button class="session-action-btn" data-action="close-session" data-session-id="${session.id}">Close Poll</button>` : ''}
				<button class="session-action-btn danger" data-action="delete-session" data-session-id="${session.id}">Delete</button>
			</div>` : ''}
		</div>`;
	}

	private bindLiveEvents() {
		// New session button
		this.shadow.querySelector('[data-action="new-session"]')?.addEventListener('click', () => this.createSession());

		// Session card clicks
		this.shadow.querySelectorAll<HTMLElement>('.session-card').forEach(el => {
			el.addEventListener('click', () => {
				const sid = el.dataset.sessionId;
				if (sid) { this.activeSessionId = sid; this.render(); }
			});
		});

		// Back to list
		this.shadow.querySelector('[data-action="back-to-list"]')?.addEventListener('click', () => {
			this.activeSessionId = null;
			this.render();
		});

		// Vote option clicks
		this.shadow.querySelectorAll<HTMLElement>('.vote-bar-row').forEach(el => {
			el.addEventListener('click', () => {
				const sessionId = el.dataset.voteSession;
				const optionId = el.dataset.voteOption;
				if (!sessionId || !optionId) return;
				const session = this.sessions.find(s => s.id === sessionId);
				if (session?.closed) return;
				const myDid = getMyDid();
				if (!myDid) return;
				const existing = this.lfClient?.getMyVote(sessionId, myDid);
				if (existing) return; // Already voted
				this.castVoteOnSession(sessionId, optionId);
			});
		});

		// Close session
		this.shadow.querySelector('[data-action="close-session"]')?.addEventListener('click', (e) => {
			const sid = (e.currentTarget as HTMLElement).dataset.sessionId;
			if (sid && this.lfClient) this.lfClient.closeSession(sid);
		});

		// Delete session
		this.shadow.querySelector('[data-action="delete-session"]')?.addEventListener('click', (e) => {
			const sid = (e.currentTarget as HTMLElement).dataset.sessionId;
			if (sid && this.lfClient && confirm('Delete this poll?')) {
				this.lfClient.deleteSession(sid);
				this.activeSessionId = null;
			}
		});
	}

	private timeAgo(ts: number): string {
		const diff = Date.now() - ts;
		const mins = Math.floor(diff / 60000);
		if (mins < 1) return 'just now';
		if (mins < 60) return `${mins}m ago`;
		const hrs = Math.floor(mins / 60);
		if (hrs < 24) return `${hrs}h ago`;
		const days = Math.floor(hrs / 24);
		return `${days}d ago`;
	}

	private renderEmpty(): string {
		return `<div class="empty">
			<div class="empty-icon">☑</div>
			<p>No choices in this space yet.</p>
			<p>Open the <a href="${rspaceNavUrl(this.space, "rspace")}" style="color:#818cf8">canvas</a> and use the Poll, Rank, or Spider buttons to create one.</p>
		</div>`;
	}

	private renderGrid(icons: Record<string, string>, labels: Record<string, string>): string {
		return `<div class="grid">
			${this.choices.map((ch) => `
			<a class="card" data-collab-id="choice:${ch.id}" href="${rspaceNavUrl(this.space, "rspace")}">
				<div class="card-icon">${icons[ch.type] || "☑"}</div>
				<div class="card-type">${labels[ch.type] || ch.type}</div>
				<h3 class="card-title">${this.esc(ch.title)}</h3>
				<div class="card-meta">
					<span class="stat">${ch.optionCount} options</span>
					<span class="stat">${ch.voteCount} responses</span>
				</div>
			</a>
			`).join("")}
		</div>`;
	}

	/* ===== Demo mode ===== */

	private loadDemoData() {
		this.rankItems = [
			{ id: 1, name: "Thai Place", emoji: "🍜" },
			{ id: 2, name: "Pizza", emoji: "🍕" },
			{ id: 3, name: "Sushi Bar", emoji: "🍣" },
			{ id: 4, name: "Tacos", emoji: "🌮" },
			{ id: 5, name: "Burgers", emoji: "🍔" },
		];
		this.voteOptions = [
			{ id: "action", name: "Action Movie", color: "#ef4444", votes: 2 },
			{ id: "comedy", name: "Comedy", color: "#f59e0b", votes: 3 },
			{ id: "horror", name: "Horror", color: "#8b5cf6", votes: 1 },
			{ id: "scifi", name: "Sci-Fi", color: "#06b6d4", votes: 2 },
		];
		this.voted = false;
		this.votedId = null;
		this.startVoteSim();
		this.renderDemo();
	}

	private startVoteSim() {
		if (this.simTimer !== null) clearInterval(this.simTimer);
		const tick = () => {
			if (this.voted) return;
			const idx = Math.floor(Math.random() * this.voteOptions.length);
			this.voteOptions[idx].votes += 1;
			if (this.demoTab === "voting") this.renderDemo();
		};
		const scheduleNext = () => {
			const delay = 1200 + Math.random() * 2000;
			this.simTimer = window.setTimeout(() => {
				tick();
				scheduleNext();
			}, delay) as unknown as number;
		};
		scheduleNext();
	}

	private renderDemo() {
		let content = "";
		if (this.demoTab === "spider") content = this.renderSpider();
		else if (this.demoTab === "ranking") content = this.renderRanking();
		else if (this.demoTab === "crowdsurf") content = this.renderCrowdSurf();
		else content = this.renderVoting();

		this.shadow.innerHTML = `
		<style>
			:host { display: block; padding: 1.5rem; -webkit-tap-highlight-color: transparent; }
			button, a, input, select, textarea, [role="button"] { touch-action: manipulation; }
			.rapp-nav { display: flex; gap: 8px; align-items: center; margin-bottom: 1rem; min-height: 36px; }
			.rapp-nav__title { font-size: 15px; font-weight: 600; flex: 1; color: var(--rs-text-primary); }
			.demo-badge { font-size: 0.7rem; padding: 2px 8px; border-radius: 999px; background: var(--rs-primary); color: #fff; font-weight: 500; }

			/* Spider chart */
			.spider-wrap { display: flex; flex-direction: column; align-items: center; }
			.spider-svg { width: 100%; max-width: 420px; }
			.spider-legend { display: flex; gap: 1.25rem; margin-top: 1rem; justify-content: center; flex-wrap: wrap; }
			.spider-legend-item { display: flex; align-items: center; gap: 6px; cursor: pointer; padding: 4px 10px; border-radius: 6px; transition: background 0.15s; font-size: 0.875rem; color: var(--rs-text-primary); }
			.spider-legend-item:hover { background: var(--rs-bg-hover); }
			.spider-legend-dot { width: 10px; height: 10px; border-radius: 50%; }
			.spider-axis-label { fill: var(--rs-text-secondary); font-size: 13px; font-family: inherit; }

			/* Ranking */
			.rank-list { list-style: none; padding: 0; margin: 0; max-width: 440px; margin-inline: auto; }
			.rank-item { display: flex; align-items: center; gap: 12px; padding: 0.75rem 1rem; margin-bottom: 6px; background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 10px; cursor: grab; transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s; user-select: none; }
			.rank-item:active { cursor: grabbing; }
			.rank-item.dragging { opacity: 0.4; transform: scale(0.97); }
			.rank-item.drag-over { border-color: var(--rs-primary); box-shadow: 0 0 0 2px rgba(99,102,241,0.3); }
			.rank-pos { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.8rem; color: var(--rs-bg-page); flex-shrink: 0; }
			.rank-pos.gold { background: #f59e0b; }
			.rank-pos.silver { background: #94a3b8; }
			.rank-pos.bronze { background: #cd7f32; }
			.rank-pos.plain { background: var(--rs-bg-surface-raised); color: var(--rs-text-secondary); }
			.rank-emoji { font-size: 1.5rem; flex-shrink: 0; }
			.rank-name { flex: 1; color: var(--rs-text-primary); font-weight: 600; font-size: 1rem; }
			.rank-grip { color: var(--rs-text-secondary); font-size: 1.1rem; flex-shrink: 0; letter-spacing: 2px; }

			/* Voting */
			.vote-wrap { max-width: 480px; margin-inline: auto; }
			.vote-option { display: flex; align-items: center; gap: 12px; padding: 0.75rem 1rem; margin-bottom: 8px; background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 10px; cursor: pointer; position: relative; overflow: hidden; transition: border-color 0.15s; }
			.vote-option:hover { border-color: var(--rs-primary); }
			.vote-option.voted { border-color: var(--rs-primary); }
			.vote-fill { position: absolute; left: 0; top: 0; bottom: 0; opacity: 0.12; transition: width 0.7s ease-out; pointer-events: none; }
			.vote-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; position: relative; z-index: 1; }
			.vote-name { flex: 1; color: var(--rs-text-primary); font-weight: 600; font-size: 1rem; position: relative; z-index: 1; }
			.vote-count { color: var(--rs-text-secondary); font-weight: 400; font-size: 0.8rem; min-width: 24px; text-align: right; position: relative; z-index: 1; font-variant-numeric: tabular-nums; }
			.vote-pct { font-weight: 600; font-size: 0.8rem; min-width: 40px; text-align: right; position: relative; z-index: 1; font-variant-numeric: tabular-nums; }
			.vote-badge { font-size: 0.625rem; padding: 2px 6px; border-radius: 999px; background: rgba(255,255,255,0.05); color: var(--rs-text-secondary); margin-left: 6px; position: relative; z-index: 1; font-weight: 400; }
			.vote-actions { display: flex; justify-content: center; margin-top: 1rem; }
			.vote-reset { padding: 0.5rem 1.25rem; border-radius: 8px; border: 1px solid var(--rs-border); background: var(--rs-bg-surface); color: var(--rs-text-secondary); cursor: pointer; font-size: 0.875rem; font-family: inherit; transition: all 0.15s; }
			.vote-reset:hover { border-color: var(--rs-error); color: #fca5a5; }
			.vote-status { text-align: center; margin-bottom: 1rem; font-size: 0.8rem; color: var(--rs-text-muted); }

			/* CrowdSurf inline */
			.cs-inline { max-width: 420px; margin-inline: auto; }
			.cs-progress-header { display: flex; justify-content: space-between; margin-bottom: 0.75rem; }
			.cs-card-stack { display: flex; flex-direction: column; align-items: center; min-height: 240px; justify-content: center; }
			.cs-card { position: relative; width: 100%; background: linear-gradient(135deg, var(--rs-bg-surface) 0%, var(--rs-bg-surface-raised, var(--rs-bg-surface)) 100%); border: 1px solid var(--rs-border); border-radius: 16px; padding: 1.5rem; cursor: grab; user-select: none; touch-action: pan-y; }
			.cs-card:active { cursor: grabbing; }
			.cs-card-body { display: flex; flex-direction: column; gap: 0.75rem; }
			.cs-type-badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; width: fit-content; }
			.cs-card-session { font-size: 0.85rem; color: var(--rs-text-secondary); }
			.cs-card-option { font-size: 1.3rem; font-weight: 700; color: var(--rs-text-primary); display: flex; align-items: center; gap: 10px; }
			.cs-color-dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
			.cs-swipe-indicator { position: absolute; top: 50%; transform: translateY(-50%); font-size: 1.1rem; font-weight: 700; padding: 6px 14px; border-radius: 8px; opacity: 0; transition: opacity 0.15s; pointer-events: none; z-index: 2; }
			.cs-swipe-left { left: 12px; color: #ef4444; background: rgba(239,68,68,0.15); }
			.cs-swipe-right { right: 12px; color: #22c55e; background: rgba(34,197,94,0.15); }
			.cs-swipe-indicator.show { opacity: 1; }
			.cs-swipe-buttons { display: flex; justify-content: center; gap: 2rem; margin-top: 1.25rem; }
			.cs-btn-skip, .cs-btn-approve { width: 52px; height: 52px; border-radius: 50%; border: 2px solid; font-size: 1.3rem; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.15s; background: var(--rs-bg-surface); font-family: inherit; }
			.cs-btn-skip { border-color: #ef4444; color: #ef4444; }
			.cs-btn-skip:hover { background: rgba(239,68,68,0.15); }
			.cs-btn-approve { border-color: #22c55e; color: #22c55e; }
			.cs-btn-approve:hover { background: rgba(34,197,94,0.15); }
			.cs-btn-reset { padding: 0.5rem 1.25rem; border-radius: 8px; border: 1px solid var(--rs-border); background: var(--rs-bg-surface); color: var(--rs-text-secondary); cursor: pointer; font-size: 0.85rem; font-family: inherit; transition: all 0.15s; margin-top: 0.75rem; }
			.cs-btn-reset:hover { border-color: var(--rs-primary); color: var(--rs-text-primary); }

			@media (max-width: 768px) {
				.grid { grid-template-columns: 1fr; }
			}
			@media (max-width: 480px) {
				:host { padding: 1rem; }
				.rapp-nav { gap: 4px; }
				.create-btn { padding: 0.375rem 0.75rem; font-size: 0.8125rem; }
				.rank-item { padding: 0.625rem 0.75rem; gap: 8px; }
				.rank-name { font-size: 0.875rem; }
				.vote-option { padding: 0.625rem 0.75rem; }
				.spider-svg { max-width: 300px; }
				.cs-card { padding: 1.25rem; border-radius: 12px; }
				.cs-card-option { font-size: 1.1rem; }
				.cs-btn-skip, .cs-btn-approve { width: 46px; height: 46px; font-size: 1.1rem; }
			}
		</style>

		<div class="rapp-nav">
			<span class="rapp-nav__title">Choices</span>
			<span class="demo-badge">DEMO</span>
			<button style="margin-left:auto;padding:4px 10px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);cursor:pointer;font-size:0.78rem;font-family:inherit;" id="btn-tour">Tour</button>
		</div>

		<div class="demo-content">
			${content}
		</div>
		`;

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

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

	/* -- Spider Chart -- */

	private polarToXY(cx: number, cy: number, radius: number, angleDeg: number): { x: number; y: number } {
		const rad = ((angleDeg - 90) * Math.PI) / 180;
		return { x: cx + radius * Math.cos(rad), y: cy + radius * Math.sin(rad) };
	}

	private renderSpider(): string {
		const cx = 200, cy = 200, maxR = 150;
		const axes = ["Taste", "Price", "Speed", "Healthy", "Distance"];
		const people: { name: string; color: string; values: number[] }[] = [
			{ name: "Alice", color: "#7c5bf5", values: [0.9, 0.6, 0.8, 0.4, 0.7] },
			{ name: "Bob", color: "#f59e0b", values: [0.5, 0.9, 0.6, 0.7, 0.8] },
			{ name: "Carol", color: "#10b981", values: [0.7, 0.4, 0.9, 0.8, 0.3] },
		];
		const angleStep = 360 / axes.length;

		// Grid rings
		let gridLines = "";
		for (let ring = 1; ring <= 5; ring++) {
			const r = (ring / 5) * maxR;
			const pts = axes.map((_, i) => {
				const p = this.polarToXY(cx, cy, r, i * angleStep);
				return `${p.x},${p.y}`;
			}).join(" ");
			gridLines += `<polygon points="${pts}" fill="none" stroke="#334155" stroke-width="1"/>`;
		}

		// Axis lines + labels
		let axisLines = "";
		const labelOffset = 18;
		axes.forEach((label, i) => {
			const angle = i * angleStep;
			const tip = this.polarToXY(cx, cy, maxR, angle);
			axisLines += `<line x1="${cx}" y1="${cy}" x2="${tip.x}" y2="${tip.y}" stroke="#334155" stroke-width="1"/>`;
			const lp = this.polarToXY(cx, cy, maxR + labelOffset, angle);
			axisLines += `<text x="${lp.x}" y="${lp.y}" text-anchor="middle" dominant-baseline="central" class="spider-axis-label">${this.esc(label)}</text>`;
		});

		// Data polygons
		let polygons = "";
		people.forEach((person) => {
			const dimmed = this.hoveredPerson !== null && this.hoveredPerson !== person.name;
			const opacity = dimmed ? 0.12 : 0.25;
			const strokeOpacity = dimmed ? 0.2 : 1;
			const strokeWidth = dimmed ? 1 : 2;
			const pts = person.values.map((v, i) => {
				const p = this.polarToXY(cx, cy, v * maxR, i * angleStep);
				return `${p.x},${p.y}`;
			}).join(" ");
			polygons += `<polygon points="${pts}" fill="${person.color}" fill-opacity="${opacity}" stroke="${person.color}" stroke-opacity="${strokeOpacity}" stroke-width="${strokeWidth}" stroke-linejoin="round"/>`;

			// Dots at each vertex
			person.values.forEach((v, i) => {
				const p = this.polarToXY(cx, cy, v * maxR, i * angleStep);
				const dotOpacity = dimmed ? 0.2 : 1;
				polygons += `<circle cx="${p.x}" cy="${p.y}" r="4" fill="${person.color}" opacity="${dotOpacity}"/>`;
			});
		});

		const legend = people.map((p) =>
			`<div class="spider-legend-item" data-person="${this.esc(p.name)}"><span class="spider-legend-dot" style="background:${p.color}"></span>${this.esc(p.name)}</div>`
		).join("");

		return `<div class="spider-wrap">
			<svg class="spider-svg" viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg">
				${gridLines}
				${axisLines}
				${polygons}
			</svg>
			<div class="spider-legend">${legend}</div>
		</div>`;
	}

	/* -- Ranking -- */

	private renderRanking(): string {
		const medalClass = (i: number) => i === 0 ? "gold" : i === 1 ? "silver" : i === 2 ? "bronze" : "plain";
		const items = this.rankItems.map((item, i) =>
			`<li class="rank-item" draggable="true" data-rank-id="${item.id}">
				<span class="rank-pos ${medalClass(i)}">${i + 1}</span>
				<span class="rank-emoji">${item.emoji}</span>
				<span class="rank-name">${this.esc(item.name)}</span>
				<span class="rank-grip">⠿</span>
			</li>`
		).join("");
		return `<ul class="rank-list">${items}</ul>`;
	}

	/* -- Live Voting -- */

	private renderVoting(): string {
		const sorted = [...this.voteOptions].sort((a, b) => b.votes - a.votes);
		const total = sorted.reduce((s, o) => s + o.votes, 0);
		const maxVotes = Math.max(...sorted.map((o) => o.votes), 1);

		const items = sorted.map((opt) => {
			const pct = total > 0 ? (opt.votes / total) * 100 : 0;
			const isLeader = opt.votes === maxVotes && total > 4;
			return `<div class="vote-option${this.voted ? " voted" : ""}" data-vote-id="${opt.id}" style="border-color:${this.voted === true ? (this.votedId === opt.id ? opt.color : '#334155') : '#334155'}">
				<div class="vote-fill" style="width:${pct}%;background:${opt.color}"></div>
				<span class="vote-dot" style="background:${opt.color}"></span>
				<span class="vote-name">${this.esc(opt.name)}${isLeader ? `<span class="vote-badge">leading</span>` : ""}</span>
				<span class="vote-count">${opt.votes}</span>
				<span class="vote-pct" style="color:${opt.color}">${pct.toFixed(0)}%</span>
			</div>`;
		}).join("");

		const status = this.voted
			? "Results are in!"
			: "Pick a movie \u2014 votes update live";

		return `<div class="vote-wrap">
			<div class="vote-status">${status}</div>
			${items}
			${this.voted ? `<div class="vote-actions"><button class="vote-reset">Reset demo</button></div>` : ""}
		</div>`;
	}

	/* -- CrowdSurf helpers -- */

	private static mulberry32(seed: number): () => number {
		let s = seed | 0;
		return () => {
			s = (s + 0x6D2B79F5) | 0;
			let t = Math.imul(s ^ (s >>> 15), 1 | s);
			t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
			return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
		};
	}

	private static hashString(str: string): number {
		let hash = 5381;
		for (let i = 0; i < str.length; i++) {
			hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0;
		}
		return hash >>> 0;
	}

	private buildCrowdSurfOptions() {
		const myDid = getMyDid() || 'anon';
		const lsKey = `cs_swiped:${this.space}:${myDid}`;

		// Restore persisted swipes
		try {
			const saved = localStorage.getItem(lsKey);
			if (saved) {
				const entries: [string, 'right' | 'left'][] = JSON.parse(saved);
				this.csSwipedMap = new Map(entries);
			}
		} catch { /* ignore */ }

		// Build pool from live sessions or demo data
		let pool: CrowdSurfOption[] = [];

		// Try live sessions first
		const openSessions = this.sessions.filter(s => !s.closed);
		for (const session of openSessions) {
			const sType: 'vote' | 'rank' | 'score' = session.type === 'rank' ? 'rank' : session.type === 'score' ? 'score' : 'vote';
			for (const opt of session.options) {
				pool.push({
					optionId: opt.id,
					label: opt.label,
					color: opt.color,
					sessionId: session.id,
					sessionTitle: session.title,
					sessionType: sType,
				});
			}
		}

		// Demo mode fallback
		if (this.space === 'demo' && pool.length === 0) {
			for (const opt of this.voteOptions) {
				pool.push({
					optionId: opt.id,
					label: opt.name,
					color: opt.color,
					sessionId: 'demo-vote',
					sessionTitle: 'Movie Night',
					sessionType: 'vote',
				});
			}
			for (const item of this.rankItems) {
				pool.push({
					optionId: String(item.id),
					label: item.name,
					color: '#f59e0b',
					sessionId: 'demo-rank',
					sessionTitle: 'Lunch Spot',
					sessionType: 'rank',
				});
			}
		}

		// Filter already swiped
		pool = pool.filter(o => !this.csSwipedMap.has(`${o.sessionId}:${o.optionId}`));

		// Seeded shuffle (Fisher-Yates)
		const today = new Date().toISOString().slice(0, 10);
		const seed = FolkChoicesDashboard.hashString(`${myDid}:${this.space}:${today}`);
		const rng = FolkChoicesDashboard.mulberry32(seed);
		for (let i = pool.length - 1; i > 0; i--) {
			const j = Math.floor(rng() * (i + 1));
			[pool[i], pool[j]] = [pool[j], pool[i]];
		}

		this.csOptions = pool.slice(0, Math.min(10, pool.length));
		this.csCurrentIndex = 0;
	}

	private renderCrowdSurf(): string {
		// Build options if empty or all consumed
		if (this.csOptions.length === 0 || this.csCurrentIndex >= this.csOptions.length) {
			this.buildCrowdSurfOptions();
		}

		// No options at all
		if (this.csOptions.length === 0) {
			return `<div class="cs-inline">
				<div style="text-align:center;padding:2rem 0;">
					<div style="font-size:2.5rem;margin-bottom:0.75rem;">🏄</div>
					<p style="color:var(--rs-text-secondary);margin:0 0 1rem;">No open polls to surf yet.</p>
					<p style="color:var(--rs-text-muted);font-size:0.8rem;margin:0 0 1rem;">Create a poll in the Voting tab, then come back to swipe!</p>
					${this.csSwipedMap.size > 0 ? `<button class="cs-btn-reset" data-cs-action="reset">Start Over</button>` : ''}
				</div>
			</div>`;
		}

		// All swiped — show summary
		if (this.csCurrentIndex >= this.csOptions.length) {
			return this.renderCrowdSurfSummary();
		}

		// Active card
		const opt = this.csOptions[this.csCurrentIndex];
		const approved = Array.from(this.csSwipedMap.values()).filter(v => v === 'right').length;
		const typeBadgeColors: Record<string, string> = { vote: '#3b82f6', rank: '#f59e0b', score: '#10b981' };
		const badgeColor = typeBadgeColors[opt.sessionType] || '#3b82f6';

		return `<div class="cs-inline">
			<div class="cs-progress-header">
				<span style="color:var(--rs-text-muted);font-size:0.8rem;">${this.csCurrentIndex + 1} of ${this.csOptions.length}</span>
				<span style="color:var(--rs-text-muted);font-size:0.8rem;">${approved} approved</span>
			</div>
			<div class="cs-card-stack">
				<div class="cs-card" id="cs-inline-card">
					<div class="cs-swipe-indicator cs-swipe-left">&#10007; Skip</div>
					<div class="cs-swipe-indicator cs-swipe-right">&#10003; Approve</div>
					<div class="cs-card-body">
						<div class="cs-type-badge" style="background:${badgeColor}20;color:${badgeColor}">${opt.sessionType}</div>
						<div class="cs-card-session">${this.esc(opt.sessionTitle)}</div>
						<div class="cs-card-option">
							<span class="cs-color-dot" style="background:${opt.color}"></span>
							${this.esc(opt.label)}
						</div>
					</div>
				</div>
			</div>
			<div class="cs-swipe-buttons">
				<button class="cs-btn-skip" data-cs-action="skip" title="Skip">&#10007;</button>
				<button class="cs-btn-approve" data-cs-action="approve" title="Approve">&#10003;</button>
			</div>
		</div>`;
	}

	private renderCrowdSurfSummary(): string {
		const approved: CrowdSurfOption[] = [];
		this.csSwipedMap.forEach((dir, key) => {
			if (dir !== 'right') return;
			const [sessionId, optionId] = key.split(':');
			const opt = this.csOptions.find(o => o.sessionId === sessionId && o.optionId === optionId);
			if (opt) approved.push(opt);
		});

		// Group by session
		const grouped = new Map<string, CrowdSurfOption[]>();
		for (const opt of approved) {
			const list = grouped.get(opt.sessionId) || [];
			list.push(opt);
			grouped.set(opt.sessionId, list);
		}

		let groupHtml = '';
		grouped.forEach((opts) => {
			const title = opts[0].sessionTitle;
			const items = opts.map(o =>
				`<div style="display:flex;align-items:center;gap:6px;padding:4px 0;">
					<span class="cs-color-dot" style="background:${o.color}"></span>
					<span style="color:var(--rs-text-primary);font-size:0.9rem;">${this.esc(o.label)}</span>
				</div>`
			).join('');
			groupHtml += `<div style="margin-bottom:1rem;">
				<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:0.05em;color:var(--rs-text-muted);margin-bottom:4px;">${this.esc(title)}</div>
				${items}
			</div>`;
		});

		return `<div class="cs-inline">
			<div style="text-align:center;padding:1.5rem 0;">
				<div style="font-size:2rem;margin-bottom:0.5rem;">&#10003;</div>
				<h3 style="color:var(--rs-text-primary);margin:0 0 0.5rem;font-size:1.1rem;">All done!</h3>
				<p style="color:var(--rs-text-secondary);margin:0 0 1.25rem;font-size:0.85rem;">
					You approved ${approved.length} of ${this.csSwipedMap.size} options
				</p>
				${groupHtml || `<p style="color:var(--rs-text-muted);font-size:0.85rem;">No approvals this round.</p>`}
				<button class="cs-btn-reset" data-cs-action="reset">Start Over</button>
			</div>
		</div>`;
	}

	private setupCrowdSurfSwipe() {
		const card = this.shadow.getElementById('cs-inline-card') as HTMLElement | null;
		if (!card) return;

		// Clear any pending transition timer from previous card
		if (this._csTransitionTimer !== null) {
			clearTimeout(this._csTransitionTimer);
			this._csTransitionTimer = null;
		}

		const handleStart = (clientX: number) => {
			if (this.csIsAnimating) return;
			// Clear any lingering transition
			if (this._csTransitionTimer !== null) {
				clearTimeout(this._csTransitionTimer);
				this._csTransitionTimer = null;
			}
			card.style.transition = '';
			this.csStartX = clientX;
			this.csCurrentX = clientX;
			this.csIsDragging = true;
		};

		const handleMove = (clientX: number) => {
			if (!this.csIsDragging || this.csIsAnimating) return;
			this.csCurrentX = clientX;
			const diffX = this.csCurrentX - this.csStartX;
			const rotation = diffX * 0.1;
			card.style.transform = `translateX(${diffX}px) rotate(${rotation}deg)`;

			const leftInd = card.querySelector('.cs-swipe-left') as HTMLElement;
			const rightInd = card.querySelector('.cs-swipe-right') as HTMLElement;

			if (diffX < -50) {
				leftInd?.classList.add('show');
				rightInd?.classList.remove('show');
			} else if (diffX > 50) {
				rightInd?.classList.add('show');
				leftInd?.classList.remove('show');
			} else {
				leftInd?.classList.remove('show');
				rightInd?.classList.remove('show');
			}
		};

		const handleEnd = () => {
			if (!this.csIsDragging || this.csIsAnimating) return;
			this.csIsDragging = false;
			const diffX = this.csCurrentX - this.csStartX;

			card.querySelector('.cs-swipe-left')?.classList.remove('show');
			card.querySelector('.cs-swipe-right')?.classList.remove('show');

			if (Math.abs(diffX) > 100) {
				const direction = diffX > 0 ? 1 : -1;
				this.csIsAnimating = true;
				card.style.transition = 'transform 0.3s ease-out, opacity 0.3s ease-out';
				card.style.transform = `translateX(${direction * 500}px) rotate(${direction * 30}deg)`;
				card.style.opacity = '0';
				this._csTransitionTimer = window.setTimeout(() => {
					card.style.transform = '';
					card.style.opacity = '';
					card.style.transition = '';
					this._csTransitionTimer = null;
					this.handleCrowdSurfSwipe(diffX > 0 ? 'right' : 'left');
				}, 300);
			} else {
				card.style.transition = 'transform 0.2s ease-out';
				card.style.transform = '';
				this._csTransitionTimer = window.setTimeout(() => {
					card.style.transition = '';
					this._csTransitionTimer = null;
				}, 200);
			}
		};

		card.addEventListener('pointerdown', (e: PointerEvent) => {
			if (e.button !== 0) return;
			card.setPointerCapture(e.pointerId);
			card.style.touchAction = 'none';
			handleStart(e.clientX);
		});
		card.addEventListener('pointermove', (e: PointerEvent) => {
			e.preventDefault();
			handleMove(e.clientX);
		});
		card.addEventListener('pointerup', () => handleEnd());
		card.addEventListener('pointercancel', () => {
			this.csIsDragging = false;
			card.style.transform = '';
			card.style.transition = '';
			card.style.opacity = '';
			card.style.touchAction = '';
		});
	}

	private handleCrowdSurfSwipe(direction: 'right' | 'left') {
		this.csIsAnimating = false;
		if (this.csCurrentIndex >= this.csOptions.length) return;

		const opt = this.csOptions[this.csCurrentIndex];
		const swipeKey = `${opt.sessionId}:${opt.optionId}`;
		this.csSwipedMap.set(swipeKey, direction);

		// Persist to localStorage
		const myDid = getMyDid() || 'anon';
		const lsKey = `cs_swiped:${this.space}:${myDid}`;
		try {
			localStorage.setItem(lsKey, JSON.stringify(Array.from(this.csSwipedMap.entries())));
		} catch { /* quota */ }

		// Cast vote on right swipe (live mode)
		if (direction === 'right' && this.lfClient && opt.sessionId !== 'demo-vote' && opt.sessionId !== 'demo-rank') {
			const did = getMyDid();
			if (did) {
				const existing = this.lfClient.getMyVote(opt.sessionId, did);
				const newChoices = { ...(existing?.choices || {}), [opt.optionId]: 1 };
				this.lfClient.castVote(opt.sessionId, did, newChoices);
			}
		}

		this.csCurrentIndex++;
		this.renderDemo();
		this.bindDemoEvents();
	}

	/* -- Demo event binding -- */

	private bindDemoEvents() {
		this.shadow.getElementById("btn-tour")?.addEventListener("click", () => this.startTour());

		// Spider legend hover
		this.shadow.querySelectorAll<HTMLElement>(".spider-legend-item").forEach((el) => {
			el.addEventListener("pointerenter", () => {
				this.hoveredPerson = el.dataset.person || null;
				this.renderDemo();
			});
			el.addEventListener("pointerleave", () => {
				this.hoveredPerson = null;
				this.renderDemo();
			});
			el.addEventListener("click", () => {
				this.hoveredPerson = this.hoveredPerson === (el.dataset.person || null) ? null : (el.dataset.person || null);
				this.renderDemo();
			});
		});

		// Ranking drag-and-drop (pointer events — works with touch, pen, mouse)
		const rankList = this.shadow.querySelector(".rank-list");
		if (rankList) {
			const items = rankList.querySelectorAll<HTMLLIElement>(".rank-item");
			items.forEach((li) => {
				li.removeAttribute("draggable");
				li.addEventListener("pointerdown", (e: PointerEvent) => {
					if (e.button !== 0) return;
					const id = parseInt(li.dataset.rankId || "0", 10);
					this.rankDragging = id;
					li.classList.add("dragging");
					li.setPointerCapture(e.pointerId);
					li.style.touchAction = "none";
				});

				li.addEventListener("pointermove", (e: PointerEvent) => {
					if (this.rankDragging === null) return;
					e.preventDefault();
					// Find target item under pointer
					const allItems = rankList.querySelectorAll<HTMLLIElement>(".rank-item");
					allItems.forEach(item => item.classList.remove("drag-over"));
					const target = this.shadow.elementFromPoint(e.clientX, e.clientY) as HTMLElement | null;
					const targetLi = target?.closest?.(".rank-item") as HTMLLIElement | null;
					if (targetLi && targetLi !== li) targetLi.classList.add("drag-over");
				});

				li.addEventListener("pointerup", (e: PointerEvent) => {
					if (this.rankDragging === null) return;
					const allItems = rankList.querySelectorAll<HTMLLIElement>(".rank-item");
					allItems.forEach(item => { item.classList.remove("drag-over"); item.classList.remove("dragging"); });
					li.style.touchAction = "";
					const target = this.shadow.elementFromPoint(e.clientX, e.clientY) as HTMLElement | null;
					const targetLi = target?.closest?.(".rank-item") as HTMLLIElement | null;
					if (targetLi) {
						const targetId = parseInt(targetLi.dataset.rankId || "0", 10);
						if (this.rankDragging !== targetId) {
							const fromIdx = this.rankItems.findIndex((r) => r.id === this.rankDragging);
							const toIdx = this.rankItems.findIndex((r) => r.id === targetId);
							if (fromIdx !== -1 && toIdx !== -1) {
								const [moved] = this.rankItems.splice(fromIdx, 1);
								this.rankItems.splice(toIdx, 0, moved);
							}
						}
					}
					this.rankDragging = null;
					this.renderDemo();
				});

				li.addEventListener("pointercancel", () => {
					const allItems = rankList.querySelectorAll<HTMLLIElement>(".rank-item");
					allItems.forEach(item => { item.classList.remove("drag-over"); item.classList.remove("dragging"); });
					li.style.touchAction = "";
					this.rankDragging = null;
				});
			});
		}

		// Voting click
		this.shadow.querySelectorAll<HTMLElement>(".vote-option").forEach((el) => {
			el.addEventListener("click", () => {
				if (this.voted) return;
				const id = el.dataset.voteId || "";
				const opt = this.voteOptions.find((o) => o.id === id);
				if (opt) {
					opt.votes += 1;
					this.voted = true;
					this.votedId = id;
					this.renderDemo();
				}
			});
		});

		// Vote reset
		const resetBtn = this.shadow.querySelector<HTMLButtonElement>(".vote-reset");
		if (resetBtn) {
			resetBtn.addEventListener("click", () => {
				this.voteOptions = [
					{ id: "action", name: "Action Movie", color: "#ef4444", votes: 2 },
					{ id: "comedy", name: "Comedy", color: "#f59e0b", votes: 3 },
					{ id: "horror", name: "Horror", color: "#8b5cf6", votes: 1 },
					{ id: "scifi", name: "Sci-Fi", color: "#06b6d4", votes: 2 },
				];
				this.voted = false;
				this.votedId = null;
				this.startVoteSim();
				this.renderDemo();
			});
		}

		// CrowdSurf swipe + buttons
		this.setupCrowdSurfSwipe();
		this.shadow.querySelector('[data-cs-action="skip"]')?.addEventListener('click', () => {
			if (this.csIsAnimating) return;
			const card = this.shadow.getElementById('cs-inline-card') as HTMLElement | null;
			if (card) {
				this.csIsAnimating = true;
				card.style.transition = 'transform 0.3s ease-out, opacity 0.3s ease-out';
				card.style.transform = 'translateX(-500px) rotate(-30deg)';
				card.style.opacity = '0';
				setTimeout(() => this.handleCrowdSurfSwipe('left'), 300);
			} else {
				this.handleCrowdSurfSwipe('left');
			}
		});
		this.shadow.querySelector('[data-cs-action="approve"]')?.addEventListener('click', () => {
			if (this.csIsAnimating) return;
			const card = this.shadow.getElementById('cs-inline-card') as HTMLElement | null;
			if (card) {
				this.csIsAnimating = true;
				card.style.transition = 'transform 0.3s ease-out, opacity 0.3s ease-out';
				card.style.transform = 'translateX(500px) rotate(30deg)';
				card.style.opacity = '0';
				setTimeout(() => this.handleCrowdSurfSwipe('right'), 300);
			} else {
				this.handleCrowdSurfSwipe('right');
			}
		});
		this.shadow.querySelector('[data-cs-action="reset"]')?.addEventListener('click', () => {
			const myDid = getMyDid() || 'anon';
			const lsKey = `cs_swiped:${this.space}:${myDid}`;
			localStorage.removeItem(lsKey);
			this.csSwipedMap.clear();
			this.csOptions = [];
			this.csCurrentIndex = 0;
			this.renderDemo();
			this.bindDemoEvents();
		});
	}

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

customElements.define("folk-choices-dashboard", FolkChoicesDashboard);
