/**
 * <folk-tasks-board> — kanban board for per-space task management.
 *
 * Single board per space, loaded directly on page load.
 * Supports task creation, status changes, and priority labels.
 */

import { boardSchema, type BoardDoc } from "../schemas";
import { TourEngine } from "../../../shared/tour-engine";
import { startPresenceHeartbeat } from '../../../shared/collab-presence';

class FolkTasksBoard extends HTMLElement {
	private shadow: ShadowRoot;
	private space = "";
	private workspaceSlug = "";
	private tasks: any[] = [];
	private statuses: string[] = ["TODO", "IN_PROGRESS", "REVIEW", "DONE"];
	private loading = false;
	private error = "";
	private isDemo = false;
	private dragTaskId: string | null = null;
	private editingTaskId: string | null = null;
	private showCreateForm: string | false = false;
	private boardView: "board" | "checklist" = "board";
	private dragOverStatus: string | null = null;
	private dragOverIndex = -1;
	private dragSourceStatus: string | null = null;
	private priorities = ["LOW", "MEDIUM", "HIGH", "URGENT"];
	private _offlineUnsubs: (() => void)[] = [];
	private _backlogTaskId: string | null = null;
	private _stopPresence: (() => void) | null = null;
	// Detail panel
	private detailTaskId: string | null = null;
	// Search & filter
	private _searchQuery = '';
	private _filterPriority = '';
	private _filterLabel = '';
	// Column editor
	private _showColumnEditor = false;
	// Board labels (from server)
	private _boardLabels: string[] = [];
	// Inline delete confirmation
	private _confirmDeleteId: string | null = null;
	// Space members for assignee dropdown
	private _spaceMembers: { did: string; displayName?: string; role: string; isOwner: boolean }[] = [];
	// Membrane — caller's role for visibility filtering
	private _myRole: string = 'viewer';
	// Drag guard — prevents column click from firing after drag
	private _justDragged = false;
	// ClickUp integration state
	private _cuConnected = false;
	private _cuTeamName = '';
	private _cuPendingTasks = 0;
	private _cuShowPanel = false;
	private _cuStep: 'token' | 'workspace' | 'list' | 'config' | 'importing' | 'done' = 'token';
	private _cuWorkspaces: any[] = [];
	private _cuSpaces: any[] = [];
	private _cuLists: any[] = [];
	private _cuSelectedTeam = '';
	private _cuSelectedSpace = '';
	private _cuSelectedList = '';
	private _cuEnableSync = true;
	private _cuImportResult: { boardId: string; taskCount: number } | null = null;
	private _tour!: TourEngine;
	private static readonly TOUR_STEPS = [
		{ target: '#create-task', title: "New Task", message: "Create a new task with title, priority, and description.", advanceOnClick: false },
		{ target: '.board', title: "Kanban Board", message: "Drag tasks between columns — TODO, In Progress, Review, Done.", advanceOnClick: false },
		{ target: '.badge.clickable', title: "Priority", message: "Click the priority badge on a task to cycle through levels.", advanceOnClick: false },
	];

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

	connectedCallback() {
		this.space = this.getAttribute("space") || "demo";
		this.workspaceSlug = this.space;
		// Check for ?backlog= deep-link from email checklist
		const params = new URLSearchParams(window.location.search);
		this._backlogTaskId = params.get("backlog");
		if (this.space === "demo") { this.loadDemoData(); }
		else {
			this.subscribeOffline();
			this.loadTasks();
			this.loadClickUpStatus();
			this.loadSpaceMembers();
			this.loadMyRole();
			this.render();
		}
		if (!localStorage.getItem("rtasks_tour_done")) {
			setTimeout(() => this._tour.start(), 1200);
		}
		this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rtasks', context: this.workspaceSlug || 'Workspaces' }));
		this._escHandler = (e: KeyboardEvent) => {
			if (e.key === 'Escape') {
				if (this.detailTaskId) { this.detailTaskId = null; this.render(); }
				else if (this._showColumnEditor) { this._showColumnEditor = false; this.render(); }
			}
		};
		document.addEventListener('keydown', this._escHandler);
	}

	private _escHandler: ((e: KeyboardEvent) => void) | null = null;

	private _isVisibleTo(vis: string | undefined | null, role: string): boolean {
		const levels: Record<string, number> = { viewer: 0, member: 1, moderator: 2, admin: 3 };
		return (levels[role] ?? 0) >= (levels[vis ?? 'viewer'] ?? 0);
	}

	private getFilteredTasks(): any[] {
		// Client-side membrane filter (defense-in-depth — server already filters API responses)
		let filtered = this.tasks.filter(t => this._isVisibleTo(t.visibility, this._myRole));
		if (this._searchQuery) {
			const q = this._searchQuery.toLowerCase();
			filtered = filtered.filter(t => t.title?.toLowerCase().includes(q) || t.description?.toLowerCase().includes(q));
		}
		if (this._filterPriority) {
			filtered = filtered.filter(t => t.priority === this._filterPriority);
		}
		if (this._filterLabel) {
			filtered = filtered.filter(t => (t.labels || []).includes(this._filterLabel));
		}
		return filtered;
	}

	disconnectedCallback() {
		for (const unsub of this._offlineUnsubs) unsub();
		this._offlineUnsubs = [];
		this._stopPresence?.();
		if (this._escHandler) document.removeEventListener('keydown', this._escHandler);
	}

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

		try {
			const docs = await runtime.subscribeModule('tasks', 'boards', boardSchema);
			// Subscribe to changes for the current board
			if (docs.size > 0) {
				for (const [docId] of docs) {
					this._offlineUnsubs.push(runtime.onChange(docId, () => this.refreshFromDocs()));
				}
				this.refreshFromDocs();
			}
		} catch { /* runtime unavailable */ }
	}

	private refreshFromDocs() {
		const runtime = (window as any).__rspaceOfflineRuntime;
		if (!runtime || !this.workspaceSlug) return;
		// Reload tasks for current board from runtime
		const docId = runtime.makeDocId('tasks', 'boards', this.workspaceSlug);
		const doc = runtime.get(docId) as BoardDoc | undefined;
		if (doc?.tasks && Object.keys(doc.tasks).length > 0) {
			this.tasks = Object.values(doc.tasks).map(t => ({
				id: t.id, title: t.title, description: t.description,
				status: t.status, priority: t.priority, labels: t.labels,
				assignee: t.assigneeId, sort_order: t.sortOrder ?? 0,
			}));
			this.render();
		}
	}

	private loadDemoData() {
		this.isDemo = true;
		this.workspaceSlug = "rspace-dev";
		this.tasks = [
			{ id: "d1", title: "Add dark mode toggle to settings page", status: "TODO", priority: "MEDIUM", labels: ["feature"], assignee: "Alice", sort_order: 0 },
			{ id: "d2", title: "Write API documentation for rPubs endpoints", status: "TODO", priority: "LOW", labels: ["docs"], assignee: "Bob", sort_order: 1000 },
			{ id: "d3", title: "Investigate slow PDF generation on large documents", status: "TODO", priority: "HIGH", labels: ["bug"], assignee: "Alice", sort_order: 2000 },
			{ id: "d4", title: "Implement file search and filtering in rFiles", status: "IN_PROGRESS", priority: "HIGH", labels: ["feature"], assignee: "Alice", sort_order: 0 },
			{ id: "d5", title: "Set up SMTP relay for transactional notifications", status: "IN_PROGRESS", priority: "MEDIUM", labels: ["chore"], assignee: "Bob", sort_order: 1000 },
			{ id: "d6", title: "Add PDF export to rNotes notebooks", status: "REVIEW", priority: "MEDIUM", labels: ["feature"], assignee: "Bob", sort_order: 0 },
			{ id: "d7", title: "Fix conviction score decay calculation in rVote", status: "REVIEW", priority: "HIGH", labels: ["bug"], assignee: "Alice", sort_order: 1000 },
			{ id: "d8", title: "Deploy EncryptID passkey authentication", status: "DONE", priority: "URGENT", labels: ["feature"], assignee: "Alice", sort_order: 0 },
			{ id: "d9", title: "Set up Cloudflare tunnel for all r* domains", status: "DONE", priority: "HIGH", labels: ["chore"], assignee: "Bob", sort_order: 1000 },
			{ id: "d10", title: "Create cosmolocal provider directory with 6 printers", status: "DONE", priority: "MEDIUM", labels: ["feature"], assignee: "Alice", sort_order: 2000 },
			{ id: "d11", title: "Migrate email from Resend to self-hosted Mailcow", status: "DONE", priority: "MEDIUM", labels: ["chore"], assignee: "Bob", sort_order: 3000 },
		];
		this.render();
	}

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

	private authHeaders(extra?: Record<string, string>): Record<string, string> {
		const h: Record<string, string> = { ...extra };
		const token = localStorage.getItem("encryptid-token");
		if (token) h["Authorization"] = `Bearer ${token}`;
		return h;
	}

	private async loadTasks() {
		if (!this.workspaceSlug) return;
		try {
			const base = this.getApiBase();
			let res = await fetch(`${base}/api/spaces/${this.workspaceSlug}/tasks`, { headers: this.authHeaders() });
			if (res.status === 401) res = await fetch(`${base}/api/spaces/${this.workspaceSlug}/tasks`);
			if (res.ok) this.tasks = await res.json();

			const spaceRes = await fetch(`${base}/api/spaces/${this.workspaceSlug}`, { headers: this.authHeaders() });
			if (spaceRes.ok) {
				const space = await spaceRes.json();
				if (space.statuses?.length) this.statuses = space.statuses;
				this._boardLabels = space.labels || [];
				this._boardClickup = space.clickup || null;
			}
		} catch { this.tasks = []; }
		this.render();
	}

	private async loadSpaceMembers() {
		if (this.isDemo) return;
		try {
			// Members API is at /api/spaces/:slug/members (global, not module-scoped)
			const res = await fetch(`/api/spaces/${this.space}/members`, { headers: this.authHeaders() });
			if (res.ok) {
				const data = await res.json();
				this._spaceMembers = data.members || [];
			}
		} catch { /* not authenticated or no members — fallback to text input */ }
	}

	private async loadMyRole() {
		if (this.isDemo) return;
		const token = localStorage.getItem('encryptid-token');
		if (!token) return;
		try {
			const res = await fetch(`/api/space-access/${encodeURIComponent(this.space)}`, {
				headers: { Authorization: `Bearer ${token}` },
			});
			if (res.ok) {
				const data = await res.json();
				this._myRole = data.role || 'viewer';
			}
		} catch {}
	}

	private async loadClickUpStatus() {
		if (this.isDemo) return;
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/clickup/status`, { headers: this.authHeaders() });
			if (res.ok) {
				const data = await res.json();
				this._cuConnected = data.connected;
				this._cuTeamName = data.teamName || '';
				this._cuPendingTasks = data.pendingTasks || 0;
			}
		} catch {}
	}

	private async connectClickUpToken(token: string) {
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/clickup/connect-token`, {
				method: 'POST',
				headers: this.authHeaders({ 'Content-Type': 'application/json' }),
				body: JSON.stringify({ token }),
			});
			if (res.ok) {
				const data = await res.json();
				this._cuConnected = true;
				this._cuTeamName = data.teamName;
				this._cuStep = 'workspace';
				await this.loadClickUpWorkspaces();
			} else {
				this.error = 'Invalid ClickUp API token';
			}
		} catch {
			this.error = 'Failed to connect to ClickUp';
		}
		this.render();
	}

	private async loadClickUpWorkspaces() {
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/clickup/workspaces`, { headers: this.authHeaders() });
			if (res.ok) this._cuWorkspaces = await res.json();
		} catch { this._cuWorkspaces = []; }
	}

	private async loadClickUpSpaces(teamId: string) {
		this._cuSelectedTeam = teamId;
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/clickup/spaces/${teamId}`, { headers: this.authHeaders() });
			if (res.ok) this._cuSpaces = await res.json();
		} catch { this._cuSpaces = []; }
		this.render();
	}

	private async loadClickUpLists(spaceId: string) {
		this._cuSelectedSpace = spaceId;
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/clickup/lists/${spaceId}`, { headers: this.authHeaders() });
			if (res.ok) this._cuLists = await res.json();
		} catch { this._cuLists = []; }
		this._cuStep = 'list';
		this.render();
	}

	private async importClickUpList() {
		if (!this._cuSelectedList) return;
		this._cuStep = 'importing';
		this.render();
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/clickup/import`, {
				method: 'POST',
				headers: this.authHeaders({ 'Content-Type': 'application/json' }),
				body: JSON.stringify({
					listId: this._cuSelectedList,
					enableSync: this._cuEnableSync,
				}),
			});
			if (res.ok) {
				this._cuImportResult = await res.json();
				this._cuStep = 'done';
			} else {
				this.error = 'Import failed';
				this._cuStep = 'list';
			}
		} catch {
			this.error = 'Import failed';
			this._cuStep = 'list';
		}
		this.loadTasks();
		this.render();
	}

	private async disconnectClickUp() {
		try {
			const base = this.getApiBase();
			await fetch(`${base}/api/clickup/disconnect`, { method: 'POST', headers: this.authHeaders() });
			this._cuConnected = false;
			this._cuTeamName = '';
			this._cuShowPanel = false;
		} catch {}
		this.render();
	}

	private renderClickUpPanel(): string {
		if (!this._cuShowPanel) return '';

		if (this._cuStep === 'token' && !this._cuConnected) {
			return `
				<div class="cu-panel">
					<h3>Connect ClickUp</h3>
					<p style="font-size:12px;color:var(--rs-text-muted);margin:0 0 10px">Enter your ClickUp Personal API Token (Settings &gt; Apps)</p>
					<input type="password" id="cu-token-input" placeholder="pk_..." autocomplete="off">
					<div class="cu-panel-actions">
						<button class="cu-btn-primary" id="cu-connect-btn">Connect</button>
						<button class="cu-btn-secondary" id="cu-cancel-btn">Cancel</button>
					</div>
				</div>`;
		}

		if (this._cuStep === 'workspace') {
			return `
				<div class="cu-panel">
					<h3>Select Workspace</h3>
					${this._cuWorkspaces.map(w => `
						<div class="cu-list-item" data-cu-team="${w.id}">
							<span class="cu-list-name">${this.esc(w.name)}</span>
							<span class="cu-list-meta">${w.members || 0} members</span>
						</div>
					`).join('')}
					${this._cuSpaces.length > 0 ? `
						<h3 style="margin-top:12px">Select Space</h3>
						${this._cuSpaces.map(s => `
							<div class="cu-list-item" data-cu-space="${s.id}">
								<span class="cu-list-name">${this.esc(s.name)}</span>
							</div>
						`).join('')}
					` : ''}
					<div class="cu-panel-actions">
						<button class="cu-btn-secondary" id="cu-cancel-btn">Cancel</button>
					</div>
				</div>`;
		}

		if (this._cuStep === 'list') {
			return `
				<div class="cu-panel">
					<h3>Select List to Import</h3>
					${this._cuLists.map(l => `
						<div class="cu-list-item ${this._cuSelectedList === l.id ? 'selected' : ''}" data-cu-list="${l.id}">
							<div>
								<span class="cu-list-name">${this.esc(l.name)}</span>
								${l.folder ? `<span class="cu-list-meta"> in ${this.esc(l.folder)}</span>` : ''}
							</div>
							<span class="cu-list-meta">${l.taskCount} tasks</span>
						</div>
					`).join('')}
					<div style="margin-top:10px">
						<label style="font-size:12px;display:flex;align-items:center;gap:6px;color:var(--rs-text-secondary)">
							<input type="checkbox" id="cu-sync-toggle" ${this._cuEnableSync ? 'checked' : ''}> Enable two-way sync
						</label>
					</div>
					<div class="cu-panel-actions">
						<button class="cu-btn-primary" id="cu-import-btn" ${!this._cuSelectedList ? 'disabled' : ''}>Import</button>
						<button class="cu-btn-secondary" id="cu-back-btn">Back</button>
						<button class="cu-btn-secondary" id="cu-cancel-btn">Cancel</button>
					</div>
				</div>`;
		}

		if (this._cuStep === 'importing') {
			return `<div class="cu-panel"><div class="cu-progress">Importing tasks from ClickUp...</div></div>`;
		}

		if (this._cuStep === 'done' && this._cuImportResult) {
			return `
				<div class="cu-panel">
					<h3>Import Complete</h3>
					<p style="font-size:13px;color:var(--rs-text-secondary);margin:0 0 8px">
						Imported <strong>${this._cuImportResult.taskCount}</strong> tasks into board <strong>${this.esc(this._cuImportResult.boardId)}</strong>
					</p>
					<div class="cu-panel-actions">
						<button class="cu-btn-primary" id="cu-open-board" data-cu-board="${this._cuImportResult.boardId}">Open Board</button>
						<button class="cu-btn-secondary" id="cu-cancel-btn">Close</button>
					</div>
				</div>`;
		}

		// Connected state — show disconnect option
		if (this._cuConnected) {
			return `
				<div class="cu-panel">
					<h3>ClickUp Connected</h3>
					<p style="font-size:12px;color:var(--rs-text-muted);margin:0 0 8px">Workspace: ${this.esc(this._cuTeamName)}</p>
					<div class="cu-panel-actions">
						<button class="cu-btn-primary" id="cu-import-start">Import List</button>
						<button class="cu-btn-secondary" id="cu-disconnect-btn" style="color:#f87171;border-color:#f87171 !important">Disconnect</button>
						<button class="cu-btn-secondary" id="cu-cancel-btn">Close</button>
					</div>
				</div>`;
		}

		return '';
	}

	private async submitCreateTask(title: string, priority: string, description: string, opts?: { dueDate?: string; status?: string; assignee?: string; visibility?: string }) {
		if (!title.trim()) return;
		const taskStatus = opts?.status || this.statuses[0] || "TODO";
		if (this.isDemo) {
			this.tasks.push({ id: `d${Date.now()}`, title: title.trim(), status: taskStatus, priority, labels: [], description: description.trim() || undefined, due_date: opts?.dueDate || null, assignee_id: opts?.assignee || null, sort_order: 0 });
			this.showCreateForm = false;
			this.render();
			return;
		}
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/spaces/${this.workspaceSlug}/tasks`, {
				method: "POST",
				headers: this.authHeaders({ "Content-Type": "application/json" }),
				body: JSON.stringify({ title: title.trim(), priority, status: taskStatus, description: description.trim() || undefined, due_date: opts?.dueDate || undefined, assignee_id: opts?.assignee || undefined, visibility: (opts?.visibility && opts.visibility !== 'viewer') ? opts.visibility : undefined }),
			});
			if (!res.ok) {
				const data = await res.json().catch(() => ({}));
				this.error = data.error || `Failed to create task (${res.status})`;
				this.render();
				return;
			}
			this.showCreateForm = false;
			this.loadTasks();
		} catch { this.error = "Failed to create task"; this.render(); }
	}

	private async updateTask(taskId: string, fields: Record<string, any>) {
		if (this.isDemo) {
			const task = this.tasks.find(t => t.id === taskId);
			if (task) Object.assign(task, fields);
			this.editingTaskId = null;
			this.render();
			return;
		}
		try {
			const base = this.getApiBase();
			await fetch(`${base}/api/tasks/${taskId}`, {
				method: "PATCH",
				headers: this.authHeaders({ "Content-Type": "application/json" }),
				body: JSON.stringify(fields),
			});
			this.editingTaskId = null;
			this.loadTasks();
		} catch { this.error = "Failed to update task"; this.render(); }
	}

	private confirmDelete(taskId: string) {
		this._confirmDeleteId = taskId;
		this.render();
	}

	private cancelDelete() {
		this._confirmDeleteId = null;
		this.render();
	}

	private async deleteTask(taskId: string) {
		this._confirmDeleteId = null;
		if (this.isDemo) {
			this.tasks = this.tasks.filter(t => t.id !== taskId);
			if (this.detailTaskId === taskId) this.detailTaskId = null;
			this.render();
			return;
		}
		try {
			const base = this.getApiBase();
			await fetch(`${base}/api/tasks/${taskId}`, { method: "DELETE", headers: this.authHeaders() });
			if (this.detailTaskId === taskId) this.detailTaskId = null;
			this.loadTasks();
		} catch { this.error = "Failed to delete task"; this.render(); }
	}

	private async updateBoardMeta(fields: Record<string, any>) {
		if (this.isDemo) {
			if (fields.statuses) this.statuses = fields.statuses;
			if (fields.labels) this._boardLabels = fields.labels;
			this.render();
			return;
		}
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/spaces/${this.workspaceSlug}`, {
				method: "PATCH",
				headers: this.authHeaders({ "Content-Type": "application/json" }),
				body: JSON.stringify(fields),
			});
			if (res.ok) {
				const data = await res.json();
				if (data.statuses) this.statuses = data.statuses;
				if (data.labels) this._boardLabels = data.labels;
			}
		} catch { this.error = "Failed to update board"; }
		this.render();
	}

	private cyclePriority(taskId: string) {
		const task = this.tasks.find(t => t.id === taskId);
		if (!task) return;
		const idx = this.priorities.indexOf(task.priority || "MEDIUM");
		const next = this.priorities[(idx + 1) % this.priorities.length];
		this.updateTask(taskId, { priority: next });
	}

	private async moveTask(taskId: string, newStatus: string, sortOrder?: number) {
		if (this.isDemo) {
			const task = this.tasks.find(t => t.id === taskId);
			if (task) {
				task.status = newStatus;
				if (sortOrder !== undefined) task.sort_order = sortOrder;
				this.render();
			}
			return;
		}
		try {
			const base = this.getApiBase();
			const body: Record<string, any> = { status: newStatus };
			if (sortOrder !== undefined) body.sort_order = sortOrder;
			await fetch(`${base}/api/tasks/${taskId}`, {
				method: "PATCH",
				headers: this.authHeaders({ "Content-Type": "application/json" }),
				body: JSON.stringify(body),
			});
			this.loadTasks();
		} catch { this.error = "Failed to move task"; this.render(); }
	}

	/** Optimistic drop — update local state immediately, persist in background */
	private persistDrop(taskId: string, newStatus: string, sortOrder: number, rebalanceUpdates: { id: string; sort_order: number }[]) {
		// Update local tasks array immediately
		const task = this.tasks.find(t => t.id === taskId);
		if (!task) return;
		task.status = newStatus;
		task.sort_order = sortOrder;
		// Apply rebalance updates to local state
		for (const u of rebalanceUpdates) {
			const t = this.tasks.find(t => t.id === u.id);
			if (t) t.sort_order = u.sort_order;
		}
		this.render();

		if (this.isDemo) return;

		// Fire-and-forget API calls
		const base = this.getApiBase();
		fetch(`${base}/api/tasks/${taskId}`, {
			method: "PATCH",
			headers: this.authHeaders({ "Content-Type": "application/json" }),
			body: JSON.stringify({ status: newStatus, sort_order: sortOrder }),
		}).catch(() => { this.error = "Failed to save task move"; this.render(); });

		if (rebalanceUpdates.length > 0) {
			fetch(`${base}/api/tasks/bulk-sort-order`, {
				method: "PATCH",
				headers: this.authHeaders({ "Content-Type": "application/json" }),
				body: JSON.stringify({ updates: rebalanceUpdates }),
			}).catch(() => {}); // non-critical
		}
	}

	// ── Ordinal algorithm (ported from backlog-md) ──

	private static readonly ORDINAL_STEP = 1000;
	private static readonly ORDINAL_EPSILON = 1e-6;

	private calculateNewOrdinal(
		prev: { sort_order: number } | null,
		next: { sort_order: number } | null,
	): { ordinal: number; requiresRebalance: boolean } {
		const STEP = FolkTasksBoard.ORDINAL_STEP;
		const EPS = FolkTasksBoard.ORDINAL_EPSILON;
		const p = prev?.sort_order;
		const n = next?.sort_order;

		if (p === undefined && n === undefined) return { ordinal: STEP, requiresRebalance: false };
		if (p === undefined || p === null) {
			const candidate = (n ?? STEP) / 2;
			return { ordinal: candidate, requiresRebalance: !Number.isFinite(candidate) || candidate <= 0 || candidate >= (n ?? STEP) - EPS };
		}
		if (n === undefined || n === null) {
			const candidate = p + STEP;
			return { ordinal: candidate, requiresRebalance: !Number.isFinite(candidate) };
		}
		const gap = n - p;
		if (gap <= EPS) return { ordinal: p + STEP, requiresRebalance: true };
		const candidate = p + gap / 2;
		return { ordinal: candidate, requiresRebalance: candidate <= p + EPS || candidate >= n - EPS };
	}

	private resolveOrdinalConflicts(tasks: any[]): { id: string; sort_order: number }[] {
		const STEP = FolkTasksBoard.ORDINAL_STEP;
		const updates: { id: string; sort_order: number }[] = [];
		let last: number | undefined;
		for (let i = 0; i < tasks.length; i++) {
			const t = tasks[i];
			const assigned = i === 0 ? STEP : (last ?? STEP) + STEP;
			if (assigned !== (t.sort_order ?? 0)) updates.push({ id: t.id, sort_order: assigned });
			t.sort_order = assigned;
			last = assigned;
		}
		return updates;
	}

	private computeSortOrder(targetStatus: string, insertIndex: number): { sortOrder: number; rebalanceUpdates: { id: string; sort_order: number }[] } {
		const columnTasks = this.tasks
			.filter(t => t.status === targetStatus && t.id !== this.dragTaskId)
			.sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0));

		const prev = insertIndex > 0 ? columnTasks[insertIndex - 1] : null;
		const next = insertIndex < columnTasks.length ? columnTasks[insertIndex] : null;
		const { ordinal, requiresRebalance } = this.calculateNewOrdinal(prev, next);

		let rebalanceUpdates: { id: string; sort_order: number }[] = [];
		if (requiresRebalance) {
			// Insert dragged task at correct position for rebalance
			const all = [...columnTasks];
			all.splice(insertIndex, 0, { id: this.dragTaskId, sort_order: ordinal });
			rebalanceUpdates = this.resolveOrdinalConflicts(all);
		}

		return { sortOrder: ordinal, rebalanceUpdates };
	}

	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; }

			.rapp-nav { display: flex; gap: 8px; margin-bottom: 16px; align-items: center; min-height: 36px; }
			.rapp-nav__back { padding: 4px 10px; border-radius: 6px; border: 1px solid var(--rs-border-subtle); background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 13px; text-decoration: none; }
			.rapp-nav__back:hover { color: var(--rs-text-primary); border-color: var(--rs-border-strong); }
			.rapp-nav__title { font-size: 15px; font-weight: 600; flex: 1; color: var(--rs-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
			.rapp-nav__btn { padding: 6px 14px; border-radius: 6px; border: none; background: var(--rs-primary); color: #fff; font-weight: 600; cursor: pointer; font-size: 13px; }
			.rapp-nav__btn:hover { background: var(--rs-primary-hover); }

			.board { display: flex; gap: 12px; overflow-x: auto; min-height: 400px; padding-bottom: 12px; }
			.column {
				min-width: 240px; max-width: 280px; flex-shrink: 0;
				background: var(--rs-bg-surface-sunken); border: 1px solid var(--rs-border); border-radius: 10px; padding: 12px;
				cursor: pointer;
			}
			.col-header { font-size: 13px; font-weight: 600; text-transform: uppercase; color: var(--rs-text-muted); margin-bottom: 10px; display: flex; justify-content: space-between; }
			.col-count { background: var(--rs-border); border-radius: 10px; padding: 0 8px; font-size: 11px; }

			.column.drag-over { background: var(--rs-bg-surface); border-color: var(--rs-primary); }
			.column.drag-source { opacity: 0.55; }

			.task-card {
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border-strong); border-radius: 8px;
				padding: 10px 12px; margin-bottom: 8px; cursor: grab;
				transition: opacity 0.2s, transform 0.15s;
				position: relative;
			}
			.task-card.dragging { opacity: 0.4; transform: rotate(2deg) scale(1.05); }
			.task-card:hover { border-color: var(--rs-border-strong); }
			.task-card:hover .task-delete-btn { opacity: 1; }
			.task-delete-btn { position: absolute; top: 4px; right: 4px; width: 20px; height: 20px; border: none; background: var(--rs-bg-surface-sunken); color: var(--rs-text-muted); border-radius: 4px; cursor: pointer; font-size: 12px; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.15s; z-index: 1; }
			.task-delete-btn:hover { background: #3b1111; color: #f87171; }
			.task-title { font-size: 13px; font-weight: 500; margin-bottom: 4px; }
			.task-desc-preview { font-size: 11px; color: var(--rs-text-muted); margin-bottom: 4px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.3; }
			.task-due { font-size: 10px; padding: 2px 6px; border-radius: 4px; background: var(--rs-border); color: var(--rs-text-muted); }
			.task-due.overdue { background: #3b1111; color: #f87171; }
			.task-meta { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
			.badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; background: var(--rs-border); color: var(--rs-text-muted); }
			.badge-urgent { background: #3b1111; color: #f87171; }
			.badge-high { background: #3b2611; color: #fb923c; }
			.badge-medium { background: #3b3511; color: #facc15; }
			.badge-low { background: #112a3b; color: #60a5fa; }
			.badge-assignee { background: #1a2332; color: #93c5fd; font-style: italic; }
			.membrane-lock { font-size: 11px; opacity: 0.6; cursor: help; }

			.move-btns { display: flex; gap: 4px; margin-top: 6px; }
			.move-btn { font-size: 11px; padding: 6px 10px; min-height: 36px; border-radius: 4px; border: 1px solid var(--rs-border-strong); background: var(--rs-bg-surface-sunken); color: var(--rs-text-muted); cursor: pointer; }
			.move-btn:hover, .move-btn:active { border-color: var(--rs-border-strong); color: var(--rs-text-secondary); }

			.create-form { background: var(--rs-bg-surface); border: 1px solid var(--rs-primary); border-radius: 8px; padding: 10px; margin-bottom: 10px; }
			.create-form input, .create-form select, .create-form textarea {
				width: 100%; padding: 6px 8px; border-radius: 6px; border: 1px solid var(--rs-border-strong);
				background: var(--rs-bg-surface-sunken); color: var(--rs-text-primary); font-size: 13px; margin-bottom: 6px; outline: none; font-family: inherit;
			}
			.create-form input:focus, .create-form select:focus, .create-form textarea:focus { border-color: var(--rs-primary-hover); }
			.create-form textarea { resize: vertical; min-height: 40px; }
			.create-form-actions { display: flex; gap: 6px; }
			.create-form-actions button { padding: 4px 12px; border-radius: 6px; border: none; font-size: 12px; cursor: pointer; font-weight: 600; }
			.cf-submit { background: var(--rs-primary); color: #fff; }
			.cf-cancel { background: transparent; color: var(--rs-text-muted); border: 1px solid var(--rs-border-strong) !important; }

			.task-title-input {
				width: 100%; padding: 4px 6px; border-radius: 4px; border: 1px solid var(--rs-primary-hover);
				background: var(--rs-bg-surface-sunken); color: var(--rs-text-primary); font-size: 13px; font-weight: 500; outline: none; font-family: inherit;
			}
			.backlog-banner { display:flex;align-items:center;justify-content:space-between;background:#6366f1;color:#fff;padding:10px 16px;border-radius:8px;margin-bottom:12px;font-size:14px }
			.backlog-dismiss { background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px;opacity:0.8 }
			.backlog-dismiss:hover { opacity:1 }
			.badge.clickable { cursor: pointer; transition: all 0.15s; padding: 4px 10px; min-height: 28px; }
			.badge.clickable:hover, .badge.clickable:active { filter: brightness(1.3); transform: scale(1.1); }

			.empty { text-align: center; color: var(--rs-text-muted); padding: 40px; }

			/* View toggle */
			.view-toggle { display: flex; gap: 2px; background: var(--rs-bg-surface-sunken); border-radius: 6px; padding: 2px; }
			.view-toggle__btn { padding: 4px 12px; border-radius: 5px; border: none; background: transparent; color: var(--rs-text-muted); cursor: pointer; font-size: 12px; font-weight: 600; transition: all 0.15s; }
			.view-toggle__btn.active { background: var(--rs-bg-surface); color: var(--rs-text-primary); box-shadow: 0 1px 2px rgba(0,0,0,0.2); }
			.view-toggle__btn:hover:not(.active) { color: var(--rs-text-secondary); }

			/* Drop indicator */
			.drop-indicator { height: 3px; background: var(--rs-primary); border-radius: 2px; margin: 2px 0; pointer-events: none; animation: pulse-indicator 1.5s ease-in-out infinite; }
			@keyframes pulse-indicator { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; box-shadow: 0 0 8px var(--rs-primary); } }

			/* Empty column drop zone */
			.empty-drop-zone { border: 2px dashed var(--rs-border); border-radius: 8px; padding: 24px 12px; text-align: center; color: var(--rs-text-muted); font-size: 12px; font-weight: 600; cursor: pointer; opacity: 0.4; transition: all 0.15s; }
			.empty-drop-zone:hover { opacity: 0.7; border-color: var(--rs-primary); color: var(--rs-primary); }
			.column.drag-over .empty-drop-zone { opacity: 1; border-color: #22c55e; color: #22c55e; background: rgba(34,197,94,0.05); }

			/* Checklist view */
			.checklist { max-width: 720px; }
			.checklist-group { margin-bottom: 16px; }
			.checklist-header { font-size: 12px; font-weight: 700; text-transform: uppercase; color: var(--rs-text-muted); margin-bottom: 6px; display: flex; align-items: center; gap: 8px; }
			.checklist-row {
				display: flex; align-items: center; gap: 10px; padding: 8px 12px;
				border-left: 3px solid var(--rs-border); border-radius: 4px;
				background: var(--rs-bg-surface); margin-bottom: 2px; min-height: 38px;
				transition: background 0.1s;
			}
			.checklist-row:hover { background: var(--rs-bg-surface-sunken); }
			.checklist-row.done .checklist-title { text-decoration: line-through; opacity: 0.5; }
			.checklist-row.border-urgent { border-left-color: #f87171; }
			.checklist-row.border-high { border-left-color: #fb923c; }
			.checklist-row.border-medium { border-left-color: #facc15; }
			.checklist-row.border-low { border-left-color: #60a5fa; }
			.checklist-check { width: 16px; height: 16px; cursor: pointer; accent-color: var(--rs-primary); flex-shrink: 0; }
			.checklist-title { font-size: 13px; font-weight: 500; flex: 1; cursor: text; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
			.checklist-badges { display: flex; gap: 4px; flex-shrink: 0; }

			/* ClickUp integration */
			.cu-badge { display: inline-flex; align-items: center; gap: 3px; font-size: 9px; padding: 1px 5px; border-radius: 3px; background: #7b68ee; color: #fff; text-decoration: none; font-weight: 600; vertical-align: middle; }
			.cu-badge:hover { background: #6c5ce7; }
			.cu-sync-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
			.cu-sync-dot.synced { background: #22c55e; }
			.cu-sync-dot.pending-push { background: #facc15; }
			.cu-sync-dot.conflict { background: #f87171; }
			.cu-sync-dot.push-failed { background: #ef4444; }
			.cu-board-status { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--rs-text-muted); padding: 2px 8px; border-radius: 4px; background: var(--rs-bg-surface-sunken); }
			.cu-panel { background: var(--rs-bg-surface); border: 1px solid var(--rs-border-strong); border-radius: 10px; padding: 16px; margin-bottom: 16px; }
			.cu-panel h3 { font-size: 14px; font-weight: 600; margin: 0 0 12px; }
			.cu-panel input[type="text"], .cu-panel input[type="password"] {
				width: 100%; padding: 8px 10px; border-radius: 6px; border: 1px solid var(--rs-border-strong);
				background: var(--rs-bg-surface-sunken); color: var(--rs-text-primary); font-size: 13px; margin-bottom: 8px; outline: none; font-family: inherit;
			}
			.cu-panel input:focus { border-color: var(--rs-primary-hover); }
			.cu-panel-actions { display: flex; gap: 8px; margin-top: 8px; }
			.cu-panel-actions button { padding: 6px 14px; border-radius: 6px; border: none; font-size: 12px; cursor: pointer; font-weight: 600; }
			.cu-btn-primary { background: #7b68ee; color: #fff; }
			.cu-btn-primary:hover { background: #6c5ce7; }
			.cu-btn-secondary { background: transparent; color: var(--rs-text-muted); border: 1px solid var(--rs-border-strong) !important; }
			.cu-list-item { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-radius: 6px; cursor: pointer; transition: background 0.1s; }
			.cu-list-item:hover { background: var(--rs-bg-surface-sunken); }
			.cu-list-item.selected { background: rgba(123, 104, 238, 0.15); border: 1px solid #7b68ee; }
			.cu-list-name { font-size: 13px; font-weight: 500; }
			.cu-list-meta { font-size: 11px; color: var(--rs-text-muted); }
			.cu-progress { font-size: 13px; color: var(--rs-text-muted); padding: 20px; text-align: center; }
			.cu-connect-btn { padding: 4px 10px; border-radius: 6px; border: 1px solid #7b68ee; background: transparent; color: #7b68ee; cursor: pointer; font-size: 12px; font-weight: 600; }
			.cu-connect-btn:hover { background: rgba(123, 104, 238, 0.1); }

			/* Detail panel slide-out */
			.detail-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.4); z-index: 100; }
			.detail-panel { position: fixed; top: 0; right: 0; bottom: 0; width: 420px; max-width: 100vw; background: var(--rs-bg-surface); border-left: 1px solid var(--rs-border-strong); z-index: 101; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 12px; animation: slideIn 0.2s ease-out; }
			@keyframes slideIn { from { transform: translateX(100%); } to { transform: translateX(0); } }
			.detail-panel__close { position: absolute; top: 12px; right: 12px; width: 28px; height: 28px; border: none; background: var(--rs-bg-surface-sunken); color: var(--rs-text-muted); border-radius: 6px; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; }
			.detail-panel__close:hover { color: var(--rs-text-primary); }
			.detail-field { display: flex; flex-direction: column; gap: 4px; }
			.detail-field label { font-size: 11px; font-weight: 600; text-transform: uppercase; color: var(--rs-text-muted); }
			.detail-field input, .detail-field select, .detail-field textarea {
				padding: 8px 10px; border-radius: 6px; border: 1px solid var(--rs-border-strong);
				background: var(--rs-bg-surface-sunken); color: var(--rs-text-primary); font-size: 13px; outline: none; font-family: inherit;
			}
			.detail-field input:focus, .detail-field select:focus, .detail-field textarea:focus { border-color: var(--rs-primary-hover); }
			.detail-field textarea { resize: vertical; min-height: 80px; }
			.detail-field .readonly { font-size: 12px; color: var(--rs-text-muted); padding: 4px 0; }
			.detail-labels { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
			.detail-label-chip { display: inline-flex; align-items: center; gap: 3px; font-size: 11px; padding: 3px 8px; border-radius: 4px; background: var(--rs-border); color: var(--rs-text-secondary); }
			.detail-label-chip button { border: none; background: none; color: var(--rs-text-muted); cursor: pointer; font-size: 12px; padding: 0 2px; }
			.detail-label-chip button:hover { color: #f87171; }
			.detail-delete { padding: 8px 16px; border-radius: 6px; border: 1px solid #f87171; background: transparent; color: #f87171; cursor: pointer; font-size: 13px; font-weight: 600; margin-top: 8px; }
			.detail-delete:hover { background: #3b1111; }

			/* Search & filter bar */
			.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; flex-wrap: wrap; }
			.filter-bar input { flex: 1; min-width: 160px; padding: 6px 10px; border-radius: 6px; border: 1px solid var(--rs-border-strong); background: var(--rs-bg-surface-sunken); color: var(--rs-text-primary); font-size: 13px; outline: none; }
			.filter-bar input:focus { border-color: var(--rs-primary-hover); }
			.filter-bar select { padding: 6px 10px; border-radius: 6px; border: 1px solid var(--rs-border-strong); background: var(--rs-bg-surface-sunken); color: var(--rs-text-primary); font-size: 12px; outline: none; }
			.filter-bar__count { font-size: 12px; color: var(--rs-text-muted); white-space: nowrap; }
			.filter-bar__clear { padding: 4px 10px; border-radius: 6px; border: 1px solid var(--rs-border-strong); background: transparent; color: var(--rs-text-muted); cursor: pointer; font-size: 11px; }
			.filter-bar__clear:hover { color: var(--rs-text-primary); border-color: var(--rs-border-strong); }
			.badge.filter-active { outline: 2px solid var(--rs-primary); outline-offset: 1px; }

			/* Column editor */
			.col-editor { background: var(--rs-bg-surface); border: 1px solid var(--rs-border-strong); border-radius: 10px; padding: 16px; margin-bottom: 16px; }
			.col-editor h3 { font-size: 14px; font-weight: 600; margin: 0 0 12px; }
			.col-editor__item { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 6px; margin-bottom: 4px; background: var(--rs-bg-surface-sunken); }
			.col-editor__item input { flex: 1; padding: 4px 6px; border-radius: 4px; border: 1px solid var(--rs-border-strong); background: var(--rs-bg-surface); color: var(--rs-text-primary); font-size: 12px; outline: none; }
			.col-editor__btn { padding: 3px 8px; border-radius: 4px; border: 1px solid var(--rs-border-strong); background: transparent; color: var(--rs-text-muted); cursor: pointer; font-size: 11px; }
			.col-editor__btn:hover { color: var(--rs-text-primary); }
			.col-editor__btn.danger { color: #f87171; border-color: #f87171; }
			.col-editor__btn.danger:hover { background: #3b1111; }
			.col-editor__add { display: flex; gap: 8px; margin-top: 8px; }
			.col-editor__add input { flex: 1; padding: 6px 8px; border-radius: 6px; border: 1px solid var(--rs-border-strong); background: var(--rs-bg-surface-sunken); color: var(--rs-text-primary); font-size: 12px; outline: none; }
			.col-editor__add button { padding: 6px 14px; border-radius: 6px; border: none; background: var(--rs-primary); color: #fff; cursor: pointer; font-size: 12px; font-weight: 600; }

			/* Inline delete confirmation */
			.confirm-delete { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: #3b1111; border-radius: 6px; margin-top: 6px; }
			.confirm-delete span { font-size: 12px; color: #f87171; }
			.confirm-delete button { padding: 4px 10px; border-radius: 4px; border: none; font-size: 11px; cursor: pointer; font-weight: 600; }
			.confirm-yes { background: #f87171; color: #fff; }
			.confirm-no { background: transparent; color: var(--rs-text-muted); border: 1px solid var(--rs-border-strong) !important; }

			/* Gear icon */
			.gear-btn { padding: 4px 8px; border-radius: 6px; border: 1px solid var(--rs-border-subtle); background: transparent; color: var(--rs-text-muted); cursor: pointer; font-size: 14px; }
			.gear-btn:hover { color: var(--rs-text-primary); border-color: var(--rs-border-strong); }

			@media (max-width: 768px) {
				.board { flex-direction: column; overflow-x: visible; }
				.column { min-width: 100%; max-width: 100%; }
				.detail-panel { width: 100vw; }
			}
			@media (max-width: 480px) {
				.rapp-nav { gap: 4px; }
				.rapp-nav__title { font-size: 14px; }
				.rapp-nav__btn { padding: 4px 10px; font-size: 12px; }
				.column-header { padding: 8px; font-size: 12px; }
				.card { padding: 10px; }
				.card-title { font-size: 13px; }
				.card-meta { font-size: 10px; }
				.create-form input, .create-form select, .create-form textarea { font-size: 16px; }
				.create-form-actions button { min-height: 36px; padding: 6px 14px; }
			}
		</style>

		${this.error ? `<div style="color:var(--rs-error);text-align:center;padding:8px">${this.esc(this.error)}</div>` : ""}
		${this._backlogTaskId ? `<div class="backlog-banner">
			<span>📋 Backlog task: <strong>${this.esc(this._backlogTaskId)}</strong></span>
			<button class="backlog-dismiss" data-dismiss-backlog>✕</button>
		</div>` : ""}
		${this.renderBoard()}
		`;
		this.attachListeners();
		this._tour.renderOverlay();
	}

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

	private renderCreateForm(forStatus: string): string {
		if (this.showCreateForm !== forStatus) return "";
		return `
			<div class="create-form" id="create-form">
				<input type="text" id="cf-title" placeholder="Task title..." autofocus>
				<input type="hidden" id="cf-status" value="${this.esc(forStatus)}">
				<select id="cf-priority">
					<option value="LOW">Low</option>
					<option value="MEDIUM" selected>Medium</option>
					<option value="HIGH">High</option>
					<option value="URGENT">Urgent</option>
				</select>
				${this._spaceMembers.length > 0 ? `
				<select id="cf-assignee">
					<option value="">Unassigned</option>
					${this._spaceMembers.map(m => `<option value="${this.esc(m.did)}">${this.esc(m.displayName || m.did.slice(0, 12))}${m.isOwner ? ' (owner)' : ''}</option>`).join('')}
				</select>` : ''}
				<textarea id="cf-desc" placeholder="Description (optional)" rows="2"></textarea>
				${this._isVisibleTo('moderator', this._myRole) ? `
				<select id="cf-visibility">
					<option value="viewer">Everyone</option>
					<option value="member">Members only</option>
					<option value="moderator">Moderators only</option>
					<option value="admin">Admins only</option>
				</select>` : ''}
				<div class="create-form-actions">
					<button class="cf-submit" id="cf-submit">Add Task</button>
					<button class="cf-cancel" id="cf-cancel">Cancel</button>
				</div>
			</div>`;
	}

	private _boardClickup: { listName?: string; syncEnabled?: boolean } | null = null;

	private renderBoard(): string {
		const cuSyncInfo = this._boardClickup?.syncEnabled
			? `<span class="cu-board-status"><span class="cu-sync-dot synced"></span> CU ${this.esc(this._boardClickup.listName || '')}</span>`
			: '';
		const filtered = this.getFilteredTasks();
		const hasFilters = !!(this._searchQuery || this._filterPriority || this._filterLabel);
		return `
			<div class="rapp-nav">
				<span class="rapp-nav__title">${this.esc(this.workspaceSlug)}</span>
				${cuSyncInfo}
				${!this.isDemo ? `<button class="cu-connect-btn" id="cu-toggle-panel">${this._cuConnected ? 'CU Connected' : 'Connect ClickUp'}</button>` : ''}
				<button class="gear-btn" id="toggle-col-editor" title="Manage columns">&#9881;</button>
				<div class="view-toggle">
					<button class="view-toggle__btn ${this.boardView === 'board' ? 'active' : ''}" data-set-view="board">Board</button>
					<button class="view-toggle__btn ${this.boardView === 'checklist' ? 'active' : ''}" data-set-view="checklist">List</button>
				</div>
				<button class="rapp-nav__btn" id="create-task">+ New Task</button>
			</div>
			<div class="filter-bar">
				<input type="text" id="filter-search" placeholder="Search tasks..." value="${this.esc(this._searchQuery)}">
				<select id="filter-priority">
					<option value="">All priorities</option>
					${this.priorities.map(p => `<option value="${p}" ${this._filterPriority === p ? 'selected' : ''}>${p.toLowerCase()}</option>`).join('')}
				</select>
				${hasFilters ? '<button class="filter-bar__clear" id="filter-clear">Clear</button>' : ''}
				<span class="filter-bar__count">${filtered.length} of ${this.tasks.length} tasks</span>
			</div>
			${this.renderClickUpPanel()}
			${this._showColumnEditor ? this.renderColumnEditor() : ''}
			${this.boardView === "checklist" ? this.renderChecklist() : this.renderKanban()}
			${this.detailTaskId ? this.renderDetailPanel() : ''}
		`;
	}

	private renderKanban(): string {
		const filtered = this.getFilteredTasks();
		return `
			<div class="board">
				${this.statuses.map(status => {
					const columnTasks = filtered
						.filter(t => t.status === status)
						.sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0));
					const isDragSource = this.dragTaskId && this.dragSourceStatus === status;
					return `
						<div class="column${isDragSource ? ' drag-source' : ''}" data-status="${status}" data-col-click="${status}">
							<div class="col-header">
								<span>${this.esc(status.replace(/_/g, " "))}</span>
								<span class="col-count">${columnTasks.length}</span>
							</div>
							${this.renderCreateForm(status)}
							${columnTasks.map(t => this.renderTaskCard(t, status)).join("")}
							${columnTasks.length === 0 && this.showCreateForm !== status ? '<div class="empty-drop-zone" data-col-click="' + this.esc(status) + '">+ Add task</div>' : ''}
						</div>
					`;
				}).join("")}
			</div>
		`;
	}

	private renderChecklist(): string {
		const filtered = this.getFilteredTasks();
		return `
			<div class="checklist">
				${this.statuses.map(status => {
					const columnTasks = filtered
						.filter(t => t.status === status)
						.sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0));
					if (columnTasks.length === 0) return '';
					const isDone = status === "DONE";
					return `
						<div class="checklist-group">
							<div class="checklist-header">${this.esc(status.replace(/_/g, " "))} <span class="col-count">${columnTasks.length}</span></div>
							${columnTasks.map(t => {
								const pClass = `border-${(t.priority || 'medium').toLowerCase()}`;
								return `
									<div class="checklist-row ${pClass} ${isDone ? 'done' : ''}">
										<input type="checkbox" class="checklist-check" data-check-task="${t.id}" ${isDone ? 'checked' : ''}>
										<span class="checklist-title" data-start-edit="${t.id}">${this.esc(t.title)}</span>
										<div class="checklist-badges">
											<span class="badge clickable badge-${(t.priority || 'medium').toLowerCase()}" data-cycle-priority="${t.id}">${this.esc((t.priority || '').toLowerCase())}</span>
											${(t.labels || []).map((l: string) => `<span class="badge">${this.esc(l)}</span>`).join("")}
										</div>
									</div>
								`;
							}).join("")}
						</div>
					`;
				}).join("")}
			</div>
		`;
	}

	private renderTaskCard(task: any, currentStatus: string): string {
		const isEditing = this.editingTaskId === task.id;
		const priorityBadge = (p: string) => {
			const map: Record<string, string> = { URGENT: "badge-urgent", HIGH: "badge-high", MEDIUM: "badge-medium", LOW: "badge-low" };
			return map[p] ? `<span class="badge clickable ${map[p]}" data-cycle-priority="${task.id}">${this.esc(p.toLowerCase())}</span>` : "";
		};
		const cuBadge = task.clickup
			? `<a class="cu-badge" href="${this.esc(task.clickup.url)}" target="_blank" rel="noopener" title="Open in ClickUp">CU <span class="cu-sync-dot ${task.clickup.syncStatus || 'synced'}"></span></a>`
			: '';
		const descPreview = task.description
			? `<div class="task-desc-preview">${this.esc(task.description)}</div>`
			: '';
		const dueDateBadge = (() => {
			const raw = task.due_date || task.dueDate;
			if (!raw) return '';
			const d = new Date(raw);
			const overdue = d.getTime() < Date.now() && currentStatus !== 'DONE';
			const fmt = `${d.getMonth()+1}/${d.getDate()}`;
			return `<span class="task-due${overdue ? ' overdue' : ''}">${fmt}</span>`;
		})();
		const labelFilter = this._filterLabel;
		const confirmingDelete = this._confirmDeleteId === task.id;
		const assigneeName = (() => {
			const aid = task.assignee_id || task.assignee;
			if (!aid) return '';
			const member = this._spaceMembers.find(m => m.did === aid);
			return member?.displayName || aid.slice(0, 10);
		})();
		return `
			<div class="task-card" draggable="${isEditing ? "false" : "true"}" data-task-id="${task.id}" data-collab-id="task:${task.id}">
				<button class="task-delete-btn" data-quick-delete="${task.id}" title="Delete">&times;</button>
				${isEditing
					? `<input class="task-title-input" data-edit-title="${task.id}" value="${this.esc(task.title)}">`
					: `<div class="task-title" data-open-detail="${task.id}" style="cursor:pointer">${task.visibility && task.visibility !== 'viewer' ? '<span class="membrane-lock" title="Restricted: ' + task.visibility + ' only">&#128274;</span> ' : ''}${this.esc(task.title)} ${cuBadge}</div>`}
				${descPreview}
				<div class="task-meta">
					${priorityBadge(task.priority || "")}
					${dueDateBadge}
					${assigneeName ? `<span class="badge badge-assignee">${this.esc(assigneeName)}</span>` : ''}
					${(task.labels || []).map((l: string) => `<span class="badge clickable${labelFilter === l ? ' filter-active' : ''}" data-filter-label="${l}">${this.esc(l)}</span>`).join("")}
				</div>
				${confirmingDelete ? `<div class="confirm-delete"><span>Delete?</span><button class="confirm-yes" data-confirm-yes="${task.id}">Yes</button><button class="confirm-no" data-confirm-no="${task.id}">No</button></div>` : ''}
			</div>
		`;
	}

	private renderDetailPanel(): string {
		const task = this.tasks.find(t => t.id === this.detailTaskId);
		if (!task) return '';
		const dueDateVal = (() => {
			const raw = task.due_date || task.dueDate;
			if (!raw) return '';
			const d = new Date(raw);
			return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
		})();
		return `
			<div class="detail-overlay" id="detail-overlay"></div>
			<div class="detail-panel">
				<button class="detail-panel__close" id="detail-close">&times;</button>
				<div class="detail-field">
					<label>Title</label>
					<input type="text" id="detail-title" value="${this.esc(task.title)}">
				</div>
				<div class="detail-field">
					<label>Description</label>
					<textarea id="detail-desc" rows="4">${this.esc(task.description || '')}</textarea>
				</div>
				<div class="detail-field">
					<label>Status</label>
					<select id="detail-status">
						${this.statuses.map(s => `<option value="${s}" ${task.status === s ? 'selected' : ''}>${this.esc(s.replace(/_/g, ' '))}</option>`).join('')}
					</select>
				</div>
				<div class="detail-field">
					<label>Priority</label>
					<select id="detail-priority">
						<option value="">None</option>
						${this.priorities.map(p => `<option value="${p}" ${task.priority === p ? 'selected' : ''}>${p.toLowerCase()}</option>`).join('')}
					</select>
				</div>
				<div class="detail-field">
					<label>Labels</label>
					<div class="detail-labels">
						${(task.labels || []).map((l: string) => `<span class="detail-label-chip">${this.esc(l)}<button data-remove-label="${l}">&times;</button></span>`).join('')}
						<input type="text" id="detail-add-label" placeholder="Add label..." style="flex:1;min-width:80px;padding:4px 6px;font-size:12px;">
					</div>
				</div>
				<div class="detail-field">
					<label>Assignee</label>
					${this._spaceMembers.length > 0 ? `
						<select id="detail-assignee">
							<option value="">Unassigned</option>
							${this._spaceMembers.map(m => {
								const name = m.displayName || m.did.slice(0, 12);
								const val = m.did;
								const selected = (task.assignee_id || task.assignee || '') === val;
								return `<option value="${this.esc(val)}" ${selected ? 'selected' : ''}>${this.esc(name)}${m.isOwner ? ' (owner)' : ''}</option>`;
							}).join('')}
						</select>
					` : `<input type="text" id="detail-assignee" value="${this.esc(task.assignee_id || task.assignee || '')}" placeholder="Assignee...">`}
				</div>
				<div class="detail-field">
					<label>Due Date</label>
					<input type="date" id="detail-due" value="${dueDateVal}">
				</div>
				${this._isVisibleTo('moderator', this._myRole) ? `
				<div class="detail-field">
					<label>Visibility</label>
					<select id="detail-visibility">
						<option value="viewer" ${(!task.visibility || task.visibility === 'viewer') ? 'selected' : ''}>Everyone</option>
						<option value="member" ${task.visibility === 'member' ? 'selected' : ''}>Members only</option>
						<option value="moderator" ${task.visibility === 'moderator' ? 'selected' : ''}>Moderators only</option>
						<option value="admin" ${task.visibility === 'admin' ? 'selected' : ''}>Admins only</option>
					</select>
				</div>` : ''}
				<div class="detail-field">
					<label>Created</label>
					<span class="readonly">${task.created_at ? new Date(task.created_at).toLocaleString() : new Date(task.createdAt || Date.now()).toLocaleString()}</span>
				</div>
				<div class="detail-field">
					<label>Updated</label>
					<span class="readonly">${task.updated_at ? new Date(task.updated_at).toLocaleString() : new Date(task.updatedAt || Date.now()).toLocaleString()}</span>
				</div>
				${this._confirmDeleteId === task.id
					? `<div class="confirm-delete"><span>Delete this task?</span><button class="confirm-yes" data-confirm-yes="${task.id}">Yes, delete</button><button class="confirm-no" data-confirm-no="${task.id}">Cancel</button></div>`
					: `<button class="detail-delete" id="detail-delete">Delete Task</button>`}
			</div>
		`;
	}

	private renderColumnEditor(): string {
		return `
			<div class="col-editor">
				<h3>Manage Columns</h3>
				${this.statuses.map((s, i) => {
					const count = this.tasks.filter(t => t.status === s).length;
					return `
						<div class="col-editor__item">
							<input type="text" value="${this.esc(s)}" data-rename-col="${i}">
							${i > 0 ? `<button class="col-editor__btn" data-move-col-up="${i}">&uarr;</button>` : ''}
							${i < this.statuses.length - 1 ? `<button class="col-editor__btn" data-move-col-down="${i}">&darr;</button>` : ''}
							<button class="col-editor__btn danger" data-remove-col="${i}" ${count > 0 ? 'disabled title="Column has tasks"' : ''}>&times;</button>
						</div>`;
				}).join('')}
				<div class="col-editor__add">
					<input type="text" id="col-add-input" placeholder="New status name...">
					<button id="col-add-btn">Add</button>
				</div>
			</div>
		`;
	}

	private attachListeners() {
		this.shadow.querySelector("[data-dismiss-backlog]")?.addEventListener("click", () => {
			this._backlogTaskId = null;
			const url = new URL(window.location.href);
			url.searchParams.delete("backlog");
			window.history.replaceState({}, "", url.toString());
			this.render();
		});
		this.shadow.getElementById("create-task")?.addEventListener("click", () => {
			const firstStatus = this.statuses[0] || "TODO";
			this.showCreateForm = this.showCreateForm ? false : firstStatus;
			this.render();
			if (this.showCreateForm) {
				setTimeout(() => this.shadow.getElementById("cf-title")?.focus(), 0);
			}
		});

		// Column click to create task in that column
		this.shadow.querySelectorAll("[data-col-click]").forEach(el => {
			el.addEventListener("click", (e) => {
				// Don't trigger if a drag just ended
				if (this._justDragged) return;
				// Don't trigger if clicking on a card, button, or input
				const target = e.target as HTMLElement;
				if (target.closest('.task-card, button, input, select, textarea, .create-form')) return;
				const status = (el as HTMLElement).dataset.colClick!;
				if (this.showCreateForm === status) return;
				this.showCreateForm = status;
				this.render();
				setTimeout(() => this.shadow.getElementById("cf-title")?.focus(), 0);
			});
		});

		// Create form handlers
		const getCreateFormValues = () => ({
			title: (this.shadow.getElementById("cf-title") as HTMLInputElement)?.value || "",
			priority: (this.shadow.getElementById("cf-priority") as HTMLSelectElement)?.value || "MEDIUM",
			desc: (this.shadow.getElementById("cf-desc") as HTMLTextAreaElement)?.value || "",
			status: (this.shadow.getElementById("cf-status") as HTMLInputElement)?.value || "",
			assignee: (this.shadow.getElementById("cf-assignee") as HTMLSelectElement)?.value || "",
			visibility: (this.shadow.getElementById("cf-visibility") as HTMLSelectElement)?.value || "",
		});
		this.shadow.getElementById("cf-submit")?.addEventListener("click", () => {
			const v = getCreateFormValues();
			this.submitCreateTask(v.title, v.priority, v.desc, { status: v.status, assignee: v.assignee, visibility: v.visibility });
		});
		this.shadow.getElementById("cf-cancel")?.addEventListener("click", () => {
			this.showCreateForm = false; this.render();
		});
		this.shadow.getElementById("cf-title")?.addEventListener("keydown", (e) => {
			if ((e as KeyboardEvent).key === "Enter") {
				const v = getCreateFormValues();
				this.submitCreateTask(v.title, v.priority, v.desc, { status: v.status, assignee: v.assignee, visibility: v.visibility });
			}
		});

		// Inline title editing
		this.shadow.querySelectorAll("[data-start-edit]").forEach(el => {
			el.addEventListener("click", (e) => {
				e.stopPropagation();
				this.editingTaskId = (el as HTMLElement).dataset.startEdit!;
				this.render();
				setTimeout(() => {
					const input = this.shadow.querySelector(`[data-edit-title="${this.editingTaskId}"]`) as HTMLInputElement;
					if (input) { input.focus(); input.select(); }
				}, 0);
			});
		});
		this.shadow.querySelectorAll("[data-edit-title]").forEach(el => {
			const handler = () => {
				const taskId = (el as HTMLElement).dataset.editTitle!;
				const val = (el as HTMLInputElement).value;
				if (val.trim()) this.updateTask(taskId, { title: val.trim() });
				else { this.editingTaskId = null; this.render(); }
			};
			el.addEventListener("blur", handler);
			el.addEventListener("keydown", (e) => {
				if ((e as KeyboardEvent).key === "Enter") handler();
				if ((e as KeyboardEvent).key === "Escape") { this.editingTaskId = null; this.render(); }
			});
		});

		// Priority cycling
		this.shadow.querySelectorAll("[data-cycle-priority]").forEach(el => {
			el.addEventListener("click", (e) => {
				e.stopPropagation();
				this.cyclePriority((el as HTMLElement).dataset.cyclePriority!);
			});
		});

		this.shadow.querySelectorAll("[data-move]").forEach(el => {
			el.addEventListener("click", (e) => {
				e.stopPropagation();
				const btn = el as HTMLElement;
				this.moveTask(btn.dataset.move!, btn.dataset.to!);
			});
		});

		// View toggle
		this.shadow.querySelectorAll("[data-set-view]").forEach(el => {
			el.addEventListener("click", () => {
				this.boardView = (el as HTMLElement).dataset.setView as "board" | "checklist";
				this.render();
			});
		});

		// Checklist checkboxes
		this.shadow.querySelectorAll(".checklist-check").forEach(el => {
			el.addEventListener("change", () => {
				const taskId = (el as HTMLElement).dataset.checkTask!;
				const checked = (el as HTMLInputElement).checked;
				this.moveTask(taskId, checked ? "DONE" : "TODO");
			});
		});

		// ── Detail panel ──
		this.shadow.querySelectorAll("[data-open-detail]").forEach(el => {
			el.addEventListener("click", (e) => {
				e.stopPropagation();
				this.detailTaskId = (el as HTMLElement).dataset.openDetail!;
				this.render();
			});
		});
		this.shadow.getElementById("detail-overlay")?.addEventListener("click", () => { this.detailTaskId = null; this.render(); });
		this.shadow.getElementById("detail-close")?.addEventListener("click", () => { this.detailTaskId = null; this.render(); });
		// Detail field auto-save — save individual fields
		const detailFieldSave = (field: string, value: any) => {
			if (!this.detailTaskId) return;
			// Update local task data immediately
			const task = this.tasks.find(t => t.id === this.detailTaskId);
			if (task) {
				if (field === 'due_date') task.due_date = value;
				else if (field === 'assignee_id') { task.assignee_id = value; task.assignee = value; }
				else task[field] = value;
			}
			// Persist to server (non-blocking, don't re-render)
			if (!this.isDemo) {
				const base = this.getApiBase();
				fetch(`${base}/api/tasks/${this.detailTaskId}`, {
					method: "PATCH",
					headers: this.authHeaders({ "Content-Type": "application/json" }),
					body: JSON.stringify({ [field]: value }),
				}).catch(() => { this.error = "Failed to save"; this.render(); });
			}
		};
		this.shadow.getElementById("detail-title")?.addEventListener("blur", (e) => {
			const v = (e.target as HTMLInputElement).value.trim();
			if (v) detailFieldSave('title', v);
		});
		this.shadow.getElementById("detail-desc")?.addEventListener("blur", (e) => {
			detailFieldSave('description', (e.target as HTMLTextAreaElement).value);
		});
		this.shadow.getElementById("detail-status")?.addEventListener("change", (e) => {
			detailFieldSave('status', (e.target as HTMLSelectElement).value);
		});
		this.shadow.getElementById("detail-priority")?.addEventListener("change", (e) => {
			detailFieldSave('priority', (e.target as HTMLSelectElement).value || null);
		});
		const assigneeEl = this.shadow.getElementById("detail-assignee");
		if (assigneeEl) {
			const event = assigneeEl.tagName === 'SELECT' ? 'change' : 'blur';
			assigneeEl.addEventListener(event, (e) => {
				detailFieldSave('assignee_id', (e.target as HTMLInputElement | HTMLSelectElement).value.trim() || null);
			});
		}
		this.shadow.getElementById("detail-due")?.addEventListener("change", (e) => {
			const v = (e.target as HTMLInputElement).value;
			detailFieldSave('due_date', v ? new Date(v).toISOString() : null);
		});
		this.shadow.getElementById("detail-visibility")?.addEventListener("change", (e) => {
			const v = (e.target as HTMLSelectElement).value;
			detailFieldSave('visibility', v === 'viewer' ? null : v);
		});
		// Detail label add
		this.shadow.getElementById("detail-add-label")?.addEventListener("keydown", (e) => {
			if ((e as KeyboardEvent).key === "Enter") {
				const input = e.target as HTMLInputElement;
				const val = input.value.trim();
				if (!val || !this.detailTaskId) return;
				const task = this.tasks.find(t => t.id === this.detailTaskId);
				if (!task) return;
				const labels = [...(task.labels || [])];
				if (!labels.includes(val)) labels.push(val);
				this.updateTask(this.detailTaskId, { labels });
				this.detailTaskId = this.detailTaskId; // keep open
			}
		});
		// Detail label remove
		this.shadow.querySelectorAll("[data-remove-label]").forEach(el => {
			el.addEventListener("click", () => {
				if (!this.detailTaskId) return;
				const label = (el as HTMLElement).dataset.removeLabel!;
				const task = this.tasks.find(t => t.id === this.detailTaskId);
				if (!task) return;
				const labels = (task.labels || []).filter((l: string) => l !== label);
				this.updateTask(this.detailTaskId, { labels });
				this.detailTaskId = this.detailTaskId;
			});
		});
		// Detail delete
		this.shadow.getElementById("detail-delete")?.addEventListener("click", () => {
			if (this.detailTaskId) this.confirmDelete(this.detailTaskId);
		});

		// ── Quick delete on card hover ──
		this.shadow.querySelectorAll("[data-quick-delete]").forEach(el => {
			el.addEventListener("click", (e) => {
				e.stopPropagation();
				const taskId = (el as HTMLElement).dataset.quickDelete!;
				this.confirmDelete(taskId);
			});
		});

		// ── Inline delete confirmation ──
		this.shadow.querySelectorAll("[data-confirm-yes]").forEach(el => {
			el.addEventListener("click", (e) => {
				e.stopPropagation();
				const taskId = (el as HTMLElement).dataset.confirmYes!;
				this.deleteTask(taskId);
			});
		});
		this.shadow.querySelectorAll("[data-confirm-no]").forEach(el => {
			el.addEventListener("click", (e) => {
				e.stopPropagation();
				this.cancelDelete();
			});
		});

		// ── Label filter on click ──
		this.shadow.querySelectorAll("[data-filter-label]").forEach(el => {
			el.addEventListener("click", (e) => {
				e.stopPropagation();
				const label = (el as HTMLElement).dataset.filterLabel!;
				this._filterLabel = this._filterLabel === label ? '' : label;
				this.render();
			});
		});

		// ── Search & filter bar ──
		this.shadow.getElementById("filter-search")?.addEventListener("input", (e) => {
			this._searchQuery = (e.target as HTMLInputElement).value;
			this.render();
			// Restore focus & cursor
			setTimeout(() => {
				const el = this.shadow.getElementById("filter-search") as HTMLInputElement;
				if (el) { el.focus(); el.selectionStart = el.selectionEnd = el.value.length; }
			}, 0);
		});
		this.shadow.getElementById("filter-priority")?.addEventListener("change", (e) => {
			this._filterPriority = (e.target as HTMLSelectElement).value;
			this.render();
		});
		this.shadow.getElementById("filter-clear")?.addEventListener("click", () => {
			this._searchQuery = '';
			this._filterPriority = '';
			this._filterLabel = '';
			this.render();
		});

		// ── Column editor ──
		this.shadow.getElementById("toggle-col-editor")?.addEventListener("click", () => {
			this._showColumnEditor = !this._showColumnEditor;
			this.render();
		});
		this.shadow.getElementById("col-add-btn")?.addEventListener("click", () => {
			const input = this.shadow.getElementById("col-add-input") as HTMLInputElement;
			const val = input?.value?.trim().toUpperCase().replace(/\s+/g, '_');
			if (!val || this.statuses.includes(val)) return;
			this.updateBoardMeta({ statuses: [...this.statuses, val] });
		});
		this.shadow.getElementById("col-add-input")?.addEventListener("keydown", (e) => {
			if ((e as KeyboardEvent).key === "Enter") {
				this.shadow.getElementById("col-add-btn")?.click();
			}
		});
		this.shadow.querySelectorAll("[data-rename-col]").forEach(el => {
			el.addEventListener("blur", () => {
				const idx = parseInt((el as HTMLElement).dataset.renameCol!);
				const val = (el as HTMLInputElement).value.trim().toUpperCase().replace(/\s+/g, '_');
				if (!val || val === this.statuses[idx]) return;
				// Rename status on all tasks in this column
				const oldStatus = this.statuses[idx];
				const newStatuses = [...this.statuses];
				newStatuses[idx] = val;
				// Update tasks with the old status
				const tasksToUpdate = this.tasks.filter(t => t.status === oldStatus);
				for (const t of tasksToUpdate) {
					this.updateTask(t.id, { status: val });
				}
				this.updateBoardMeta({ statuses: newStatuses });
			});
		});
		this.shadow.querySelectorAll("[data-move-col-up]").forEach(el => {
			el.addEventListener("click", () => {
				const idx = parseInt((el as HTMLElement).dataset.moveColUp!);
				if (idx <= 0) return;
				const newStatuses = [...this.statuses];
				[newStatuses[idx - 1], newStatuses[idx]] = [newStatuses[idx], newStatuses[idx - 1]];
				this.updateBoardMeta({ statuses: newStatuses });
			});
		});
		this.shadow.querySelectorAll("[data-move-col-down]").forEach(el => {
			el.addEventListener("click", () => {
				const idx = parseInt((el as HTMLElement).dataset.moveColDown!);
				if (idx >= this.statuses.length - 1) return;
				const newStatuses = [...this.statuses];
				[newStatuses[idx], newStatuses[idx + 1]] = [newStatuses[idx + 1], newStatuses[idx]];
				this.updateBoardMeta({ statuses: newStatuses });
			});
		});
		this.shadow.querySelectorAll("[data-remove-col]").forEach(el => {
			el.addEventListener("click", () => {
				const idx = parseInt((el as HTMLElement).dataset.removeCol!);
				const count = this.tasks.filter(t => t.status === this.statuses[idx]).length;
				if (count > 0) return;
				const newStatuses = this.statuses.filter((_, i) => i !== idx);
				this.updateBoardMeta({ statuses: newStatuses });
			});
		});

		// ClickUp panel listeners
		this.shadow.getElementById("cu-toggle-panel")?.addEventListener("click", () => {
			this._cuShowPanel = !this._cuShowPanel;
			if (this._cuShowPanel && !this._cuConnected) this._cuStep = 'token';
			this.render();
		});
		this.shadow.getElementById("cu-connect-btn")?.addEventListener("click", () => {
			const input = this.shadow.getElementById("cu-token-input") as HTMLInputElement;
			if (input?.value?.trim()) this.connectClickUpToken(input.value.trim());
		});
		this.shadow.getElementById("cu-token-input")?.addEventListener("keydown", (e) => {
			if ((e as KeyboardEvent).key === "Enter") {
				const input = e.target as HTMLInputElement;
				if (input.value?.trim()) this.connectClickUpToken(input.value.trim());
			}
		});
		this.shadow.getElementById("cu-cancel-btn")?.addEventListener("click", () => {
			this._cuShowPanel = false;
			this._cuStep = 'token';
			this.render();
		});
		this.shadow.getElementById("cu-disconnect-btn")?.addEventListener("click", () => this.disconnectClickUp());
		this.shadow.getElementById("cu-import-start")?.addEventListener("click", () => {
			this._cuStep = 'workspace';
			this.loadClickUpWorkspaces().then(() => this.render());
		});
		this.shadow.getElementById("cu-import-btn")?.addEventListener("click", () => this.importClickUpList());
		this.shadow.getElementById("cu-back-btn")?.addEventListener("click", () => {
			this._cuStep = 'workspace';
			this.render();
		});
		this.shadow.querySelectorAll("[data-cu-team]").forEach(el => {
			el.addEventListener("click", () => this.loadClickUpSpaces((el as HTMLElement).dataset.cuTeam!));
		});
		this.shadow.querySelectorAll("[data-cu-space]").forEach(el => {
			el.addEventListener("click", () => this.loadClickUpLists((el as HTMLElement).dataset.cuSpace!));
		});
		this.shadow.querySelectorAll("[data-cu-list]").forEach(el => {
			el.addEventListener("click", () => {
				this._cuSelectedList = (el as HTMLElement).dataset.cuList!;
				this.render();
			});
		});
		this.shadow.getElementById("cu-sync-toggle")?.addEventListener("change", (e) => {
			this._cuEnableSync = (e.target as HTMLInputElement).checked;
		});
		this.shadow.getElementById("cu-open-board")?.addEventListener("click", () => {
			this._cuShowPanel = false;
			this._cuStep = 'token';
			this._cuImportResult = null;
			this.loadTasks();
		});

		// Pointer events drag-and-drop on task cards (works with touch, pen, mouse)
		this.shadow.querySelectorAll(".task-card[draggable]").forEach(card => {
			const el = card as HTMLElement;
			el.addEventListener("pointerdown", (e) => {
				const pe = e as PointerEvent;
				if (pe.button !== 0) return;
				if (el.getAttribute("draggable") === "false") return;
				this.dragTaskId = el.dataset.taskId || null;
				// Track source column for dimming
				const task = this.tasks.find(t => t.id === this.dragTaskId);
				this.dragSourceStatus = task?.status || null;
				el.classList.add("dragging");
				el.setPointerCapture(pe.pointerId);
				el.style.touchAction = "none";
			});
			el.addEventListener("pointermove", (e) => {
				if (!this.dragTaskId) return;
				const pe = e as PointerEvent;
				pe.preventDefault();
				// Clear previous state
				this.shadow.querySelectorAll(".column[data-status]").forEach(c => {
					(c as HTMLElement).classList.remove("drag-over");
					// Re-apply drag-source class since we're clearing
					if (this.dragSourceStatus && (c as HTMLElement).dataset.status === this.dragSourceStatus) {
						(c as HTMLElement).classList.add("drag-source");
					}
				});
				this.shadow.querySelector('.drop-indicator')?.remove();
				// Find target column via bounding-box hit test (fixes Shadow DOM elementFromPoint issues)
				let targetCol: HTMLElement | null = null;
				const columns = this.shadow.querySelectorAll(".column[data-status]");
				for (const col of columns) {
					const rect = col.getBoundingClientRect();
					if (pe.clientX >= rect.left && pe.clientX <= rect.right && pe.clientY >= rect.top && pe.clientY <= rect.bottom) {
						targetCol = col as HTMLElement;
						break;
					}
				}
				if (targetCol) {
					// Don't highlight source column green — it's already dimmed
					if (targetCol.dataset.status !== this.dragSourceStatus) {
						targetCol.classList.add("drag-over");
					}
					// Calculate insert position among non-dragging cards
					const cards = Array.from(targetCol.querySelectorAll('.task-card:not(.dragging)'));
					let insertIndex = cards.length;
					for (let i = 0; i < cards.length; i++) {
						const rect = cards[i].getBoundingClientRect();
						if (pe.clientY < rect.top + rect.height / 2) { insertIndex = i; break; }
					}
					this.dragOverStatus = targetCol.dataset.status || null;
					this.dragOverIndex = insertIndex;
					// Show drop indicator line
					const indicator = document.createElement('div');
					indicator.className = 'drop-indicator';
					if (insertIndex < cards.length) {
						cards[insertIndex].before(indicator);
					} else if (cards.length > 0) {
						// After last card
						cards[cards.length - 1].after(indicator);
					}
					// For empty columns the empty-drop-zone CSS handles the visual
				} else {
					this.dragOverStatus = null;
					this.dragOverIndex = -1;
				}
			});
			el.addEventListener("pointerup", (e) => {
				if (!this.dragTaskId) return;
				// Prevent column click handler from firing after drag
				this._justDragged = true;
				setTimeout(() => { this._justDragged = false; }, 100);
				el.classList.remove("dragging");
				el.style.touchAction = "";
				this.shadow.querySelectorAll(".column[data-status]").forEach(c => {
					(c as HTMLElement).classList.remove("drag-over", "drag-source");
				});
				this.shadow.querySelector('.drop-indicator')?.remove();
				if (this.dragOverStatus && this.dragOverIndex >= 0) {
					const task = this.tasks.find(t => t.id === this.dragTaskId);
					if (task) {
						// No-op detection: same column, same position → skip
						const colTasks = this.tasks
							.filter(t => t.status === this.dragOverStatus && t.id !== this.dragTaskId)
							.sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0));
						const currentIdx = this.tasks
							.filter(t => t.status === task.status)
							.sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0))
							.findIndex(t => t.id === task.id);
						const sameColumn = task.status === this.dragOverStatus;
						const samePosition = sameColumn && (this.dragOverIndex === currentIdx || this.dragOverIndex === currentIdx + 1);

						if (!samePosition) {
							const { sortOrder, rebalanceUpdates } = this.computeSortOrder(this.dragOverStatus!, this.dragOverIndex);
							this.persistDrop(this.dragTaskId!, this.dragOverStatus!, sortOrder, rebalanceUpdates);
						}
					}
				}
				this.dragTaskId = null;
				this.dragSourceStatus = null;
				this.dragOverStatus = null;
				this.dragOverIndex = -1;
			});
			el.addEventListener("pointercancel", () => {
				this._justDragged = true;
				setTimeout(() => { this._justDragged = false; }, 100);
				el.classList.remove("dragging");
				el.style.touchAction = "";
				this.shadow.querySelectorAll(".column[data-status]").forEach(c => {
					(c as HTMLElement).classList.remove("drag-over", "drag-source");
				});
				this.shadow.querySelector('.drop-indicator')?.remove();
				this.dragTaskId = null;
				this.dragSourceStatus = null;
				this.dragOverStatus = null;
				this.dragOverIndex = -1;
			});
		});

		// Make task cards draggable for calendar reminders (cross-module)
		this.shadow.querySelectorAll<HTMLElement>(".task-card[data-task-id]").forEach((el) => {
			el.addEventListener("dragstart", (e) => {
				const de = e as DragEvent;
				if (!de.dataTransfer) return;
				const title = el.querySelector(".task-title")?.textContent || "";
				const id = el.dataset.taskId || "";
				if (!title) return;
				de.dataTransfer.setData("application/rspace-item", JSON.stringify({
					title, module: "rtasks", entityId: id, label: "Task", color: "#3b82f6",
				}));
				de.dataTransfer.setData("text/plain", title);
				de.dataTransfer.effectAllowed = "copyMove";
			});
		});
	}

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

customElements.define("folk-tasks-board", FolkTasksBoard);
