/**
 * <folk-thread-builder> — Thread compose/preview/readonly web component.
 *
 * Attributes:
 *   space      — space slug
 *   thread-id  — existing thread ID (for edit/readonly)
 *   mode       — "new" | "edit" | "readonly"
 *
 * In new/edit mode: compose pane + live preview, auto-save to Automerge,
 * draft management, image ops, export to multiple platforms.
 *
 * In readonly mode: display thread with share/copy/export actions.
 */

import { socialsSchema, socialsDocId } from '../schemas';
import type { SocialsDoc, ThreadData } from '../schemas';
import type { DocumentId } from '../../../shared/local-first/document';
import { PLATFORM_LIMITS } from '../lib/types';

function generateThreadId(): string {
	const random = Math.random().toString(36).substring(2, 8);
	return `t-${Date.now()}-${random}`;
}

export class FolkThreadBuilder extends HTMLElement {
	private _space = 'demo';
	private _threadId: string | null = null;
	private _mode: 'new' | 'edit' | 'readonly' = 'new';
	private _thread: ThreadData | null = null;
	private _tweetImages: Record<string, string> = {};
	private _autoSaveTimer: ReturnType<typeof setTimeout> | null = null;
	private _offlineUnsub: (() => void) | null = null;
	private _subscribedDocIds: string[] = [];
	private _offlineReady: Promise<void> | null = null;
	private _tweetImageUploadIdx: string | null = null;
	private _linkPreviewCache: Map<string, { title: string; description: string; image: string | null; domain: string } | null> = new Map();

	// SVG icons
	private svgReply = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>';
	private svgRetweet = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M17 1l4 4-4 4"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><path d="M7 23l-4-4 4-4"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>';
	private svgHeart = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>';
	private svgShare = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>';
	private svgUpload = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>';
	private svgSparkle = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z"/></svg>';
	private svgCamera = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>';

	static get observedAttributes() { return ['space', 'thread-id', 'mode']; }

	connectedCallback() {
		if (!this.shadowRoot) {
			this.attachShadow({ mode: 'open' });
		}
		this._space = this.getAttribute('space') || 'demo';
		this._threadId = this.getAttribute('thread-id') || null;
		this._mode = (this.getAttribute('mode') as any) || 'new';

		// Check for server-hydrated data
		if ((window as any).__THREAD_DATA__) {
			const data = (window as any).__THREAD_DATA__;
			this._thread = data;
			this._threadId = data.id;
			this._tweetImages = data.tweetImages || {};
		}

		this.render();

		if (this._space !== 'demo') {
			this._offlineReady = this.subscribeOffline();
		}
	}

	disconnectedCallback() {
		this._offlineUnsub?.();
		this._offlineUnsub = null;
		if (this._autoSaveTimer) clearTimeout(this._autoSaveTimer);
		const runtime = (window as any).__rspaceOfflineRuntime;
		if (runtime) {
			for (const id of this._subscribedDocIds) runtime.unsubscribe(id);
		}
		this._subscribedDocIds = [];
	}

	attributeChangedCallback(name: string, _old: string, val: string) {
		if (name === 'space') this._space = val;
		else if (name === 'thread-id') this._threadId = val;
		else if (name === 'mode') this._mode = val as any;
	}

	private get basePath() {
		// On subdomain routing (e.g. jeff.rspace.online), the space is the subdomain
		// and paths should be /rsocials/... not /jeff/rsocials/...
		const host = window.location.hostname;
		if (host.endsWith('.rspace.online') || host.endsWith('.rsocials.online')) {
			return '/rsocials/';
		}
		return `/${this._space}/rsocials/`;
	}

	private async subscribeOffline() {
		// Wait for the runtime to be available and initialized
		let runtime = (window as any).__rspaceOfflineRuntime;

		// The runtime may not be created yet if shell.ts hasn't run init()
		if (!runtime) {
			// Wait briefly for shell.ts to create it
			await new Promise(r => setTimeout(r, 200));
			runtime = (window as any).__rspaceOfflineRuntime;
		}
		if (!runtime) return;

		// Wait for init() to complete if still in progress
		if (!runtime.isInitialized && runtime.init) {
			try { await runtime.init(); } catch { /* init may have been called already */ }
		}
		if (!runtime.isInitialized) return;

		try {
			const docId = socialsDocId(this._space) as DocumentId;
			const doc = await runtime.subscribe(docId, socialsSchema);
			this._subscribedDocIds.push(docId);

			if (this._threadId && doc?.threads?.[this._threadId] && !this._thread) {
				// Deep-clone to get a plain object (not an Automerge proxy)
				this._thread = JSON.parse(JSON.stringify(doc.threads[this._threadId]));
				this._tweetImages = this._thread?.tweetImages || {};
				this.render();
			}

			this._offlineUnsub = runtime.onChange(docId, (updated: SocialsDoc) => {
				if (this._threadId && updated?.threads?.[this._threadId]) {
					const synced = JSON.parse(JSON.stringify(updated.threads[this._threadId])) as ThreadData;
					// In readonly mode, always take the synced version
					if (this._mode === 'readonly') {
						this._thread = synced;
						this._tweetImages = synced.tweetImages || {};
						this.render();
					}
					// In edit mode, only update thread metadata (not images)
					// to avoid overwriting in-flight local changes
				}
			});
		} catch (e: any) {
			console.warn('[thread-builder] Offline subscribe failed:', e.message);
		}
	}

	private esc(s: string): string {
		return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
	}

	private getRuntime() {
		return (window as any).__rspaceOfflineRuntime;
	}

	private async saveToAutomerge(thread: ThreadData) {
		const runtime = this.getRuntime();
		if (!runtime?.isInitialized) return;

		// Ensure the document is open before writing
		if (this._offlineReady) {
			await this._offlineReady;
		}

		try {
			const docId = socialsDocId(this._space) as DocumentId;
			// Deep clone to avoid "Cannot create a reference to an existing document object"
			// when thread contains Automerge proxy objects from a previous read
			const plain = JSON.parse(JSON.stringify(thread)) as ThreadData;
			plain.updatedAt = Date.now();
			// Strip null fields that Automerge stores as absent
			if (plain.imageUrl === null) delete (plain as any).imageUrl;
			if (plain.tweetImages === null) delete (plain as any).tweetImages;
			runtime.change(docId, `Save thread ${plain.title || plain.id}`, (d: SocialsDoc) => {
				if (!d.threads) d.threads = {} as any;
				d.threads[plain.id] = plain;
			});
		} catch (e: any) {
			console.error('[thread-builder] Failed to save:', e.message);
		}
	}

	private async deleteFromAutomerge(id: string) {
		const runtime = this.getRuntime();
		if (!runtime?.isInitialized) return;

		if (this._offlineReady) {
			await this._offlineReady;
		}

		const docId = socialsDocId(this._space) as DocumentId;
		runtime.change(docId, `Delete thread ${id}`, (d: SocialsDoc) => {
			if (d.threads?.[id]) delete d.threads[id];
		});
	}

	private getDoc(): SocialsDoc | undefined {
		const runtime = this.getRuntime();
		if (!runtime?.isInitialized) return undefined;
		const docId = socialsDocId(this._space) as DocumentId;
		return runtime.get(docId);
	}

	private listThreads(): ThreadData[] {
		const doc = this.getDoc();
		if (!doc?.threads) return [];
		return Object.values(doc.threads).sort((a: ThreadData, b: ThreadData) => b.updatedAt - a.updatedAt);
	}

	// ── Rendering ──

	private render() {
		if (!this.shadowRoot) return;

		if (this._mode === 'readonly') {
			this.renderReadonly();
		} else {
			this.renderEditor();
		}
	}

	private renderReadonly() {
		if (!this.shadowRoot || !this._thread) return;
		const t = this._thread;
		const name = this.esc(t.name || 'Anonymous');
		const handle = this.esc(t.handle || '@anonymous');
		const initial = name.charAt(0).toUpperCase();
		const total = t.tweets.length;
		const dateStr = new Date(t.createdAt).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });

		const tweetCards = t.tweets.map((text, i) => {
			const len = text.length;
			const connector = i > 0 ? '<div class="connector"></div>' : '';
			const tweetImgUrl = t.tweetImages?.[String(i)];
			const tweetImgHtml = tweetImgUrl
				? `<div class="attached-image"><img src="${this.esc(tweetImgUrl)}" alt="Tweet image"></div>`
				: '';
			const linkCards = this.extractLinkCards(text);
			return `<div class="tweet-card">
				${connector}
				<div class="tc-header">
					<div class="tc-avatar">${this.esc(initial)}</div>
					<span class="tc-name">${name}</span>
					<span class="tc-handle">${handle}</span>
					<span class="tc-dot">&#183;</span>
					<span class="tc-time">${this.esc(dateStr)}</span>
				</div>
				<p class="tc-content">${this.renderTweetContent(text)}</p>
				${tweetImgHtml}
				${linkCards}
				<div class="tc-footer">
					<div class="tc-actions">
						<span class="tc-action">${this.svgReply}</span>
						<span class="tc-action">${this.svgRetweet}</span>
						<span class="tc-action">${this.svgHeart}</span>
						<span class="tc-action">${this.svgShare}</span>
					</div>
					<div class="tc-meta">
						<span class="tc-chars${len > 280 ? ' tc-chars--over' : ''}">${len}/280</span>
						<span class="tc-thread-num">${i + 1}/${total}</span>
					</div>
				</div>
			</div>`;
		}).join('\n');

		this.shadowRoot.innerHTML = `
			<style>${this.getBaseStyles()}${this.getReadonlyStyles()}</style>
			<div class="thread-ro">
				<div class="ro-header">
					<div class="ro-author">
						<div class="tc-avatar" style="width:48px;height:48px;font-size:1.2rem">${this.esc(initial)}</div>
						<div>
							<div class="ro-name">${name}</div>
							<div class="ro-handle">${handle}</div>
						</div>
					</div>
					<div class="ro-meta">
						<span>${total} tweet${total === 1 ? '' : 's'}</span>
						<span>&#183;</span>
						<span>${this.esc(dateStr)}</span>
					</div>
				</div>
				<div class="preview ro-cards">${tweetCards}</div>
				<div class="ro-actions">
					<a href="${this.basePath}thread-editor/${this.esc(t.id)}/edit" class="btn btn--primary">Edit Thread</a>
					<button class="btn btn--outline" id="ro-copy-thread">Copy Thread</button>
					<button class="btn btn--outline" id="ro-copy-link">Copy Link</button>
					<div class="export-dropdown">
						<button class="btn btn--outline" id="ro-export-btn">Export &#9662;</button>
						<div class="export-menu" id="ro-export-menu" hidden>
							<button data-platform="twitter">𝕏 Twitter (280)</button>
							<button data-platform="bluesky">🦋 Bluesky (300)</button>
							<button data-platform="mastodon">🐘 Mastodon (500)</button>
							<button data-platform="linkedin">💼 LinkedIn</button>
							<button data-platform="plain">📄 Plain Text</button>
						</div>
					</div>
					<button class="btn btn--postiz" id="ro-send-postiz">Send to Postiz</button>
				</div>
				<div class="ro-cta">
					<a href="${this.basePath}thread-editor" class="btn btn--success">Create Your Own Thread</a>
					<a href="${this.basePath}threads" class="btn btn--outline">Browse All Threads</a>
				</div>
			</div>
			<div class="toast" id="export-toast" hidden></div>
		`;

		this.bindReadonlyEvents();
	}

	private renderEditor() {
		if (!this.shadowRoot) return;
		const t = this._thread;

		this.shadowRoot.innerHTML = `
			<style>${this.getBaseStyles()}${this.getEditorStyles()}</style>
			<div class="thread-page">
				<div class="page-header">
					<h1>Thread Editor</h1>
					<div class="page-actions">
						<button class="btn btn--primary" id="thread-save">Save Draft</button>
						<button class="btn btn--success" id="thread-share">Share</button>
						<button class="btn btn--outline" id="thread-copy">Copy Thread</button>
						<div class="export-dropdown">
							<button class="btn btn--outline" id="thread-export-btn">Export &#9662;</button>
							<div class="export-menu" id="thread-export-menu" hidden>
								<button data-platform="twitter">𝕏 Twitter (280)</button>
								<button data-platform="bluesky">🦋 Bluesky (300)</button>
								<button data-platform="mastodon">🐘 Mastodon (500)</button>
								<button data-platform="linkedin">💼 LinkedIn</button>
								<button data-platform="plain">📄 Plain Text</button>
							</div>
						</div>
						<button class="btn btn--postiz" id="thread-send-postiz">Send to Postiz</button>
					</div>
				</div>
				<div class="drafts-area">
					<button class="btn btn--outline drafts-toggle" id="toggle-drafts">Saved Drafts &#9662;</button>
					<div class="drafts-list" id="drafts-list" hidden></div>
				</div>
				<div id="share-link-area"></div>
				<div class="compose">
					<div class="compose-fields">
						<input class="compose-input" id="thread-name" placeholder="Display name" value="${this.esc(t?.name || 'Your Name')}">
						<input class="compose-input" id="thread-handle" placeholder="@handle" value="${this.esc(t?.handle || '@yourhandle')}">
					</div>
					<textarea class="compose-textarea" id="thread-input" placeholder="Write your tweets here, separated by ---\n\nExample:\nFirst tweet goes here\n---\nSecond tweet\n---\nThird tweet">${t ? this.esc(t.tweets.join('\n---\n')) : ''}</textarea>
				</div>
				<div class="preview" id="thread-preview">
					<div class="preview-empty">Your tweet thread preview will appear here</div>
				</div>
				<input type="file" id="tweet-image-input" accept="image/png,image/jpeg,image/webp,image/gif" hidden>
			</div>
		`;

		if (t) {
			this._tweetImages = t.tweetImages || {};
		}

		this.bindEditorEvents();
		if (t) this.renderPreview();
	}

	// ── Preview rendering ──

	private renderPreview() {
		const sr = this.shadowRoot;
		if (!sr) return;
		const textarea = sr.getElementById('thread-input') as HTMLTextAreaElement;
		const preview = sr.getElementById('thread-preview');
		const nameInput = sr.getElementById('thread-name') as HTMLInputElement;
		const handleInput = sr.getElementById('thread-handle') as HTMLInputElement;
		if (!textarea || !preview || !nameInput || !handleInput) return;

		const raw = textarea.value;
		const tweets = raw.split(/\n---\n/).map(t => t.trim()).filter(Boolean);
		const name = nameInput.value || 'Your Name';
		const handle = handleInput.value || '@yourhandle';
		const initial = name.charAt(0).toUpperCase();
		const total = tweets.length;

		if (!total) {
			preview.innerHTML = '<div class="preview-empty">Your tweet thread preview will appear here</div>';
			return;
		}

		preview.innerHTML = tweets.map((text, i) => {
			const len = text.length;
			const overClass = len > 280 ? ' tc-chars--over' : '';
			const connector = i > 0 ? '<div class="connector"></div>' : '';
			const imgUrl = this._tweetImages[String(i)];
			const imgHtml = imgUrl
				? `<div class="attached-image">
					<img src="${this.esc(imgUrl)}" alt="Tweet image">
					<button class="image-remove" data-remove-idx="${i}" title="Remove image">&#215;</button>
				</div>`
				: '';
			const photoBtn = !imgUrl
				? `<button class="photo-btn" data-photo-idx="${i}" title="Add image">${this.svgCamera}<span class="photo-plus">+</span></button>
				<div class="photo-menu" hidden data-menu-idx="${i}">
					<button data-upload-idx="${i}">${this.svgUpload} Upload</button>
					<button data-generate-idx="${i}">${this.svgSparkle} Generate with AI</button>
				</div>`
				: '';
			const linkCards = this.extractLinkCards(text);
			return `<div class="tweet-card">
				${connector}
				${photoBtn}
				<div class="tc-header">
					<div class="tc-avatar">${this.esc(initial)}</div>
					<span class="tc-name">${this.esc(name)}</span>
					<span class="tc-handle">${this.esc(handle)}</span>
					<span class="tc-dot">&#183;</span>
					<span class="tc-time">now</span>
				</div>
				<p class="tc-content">${this.renderTweetContent(text)}</p>
				${imgHtml}
				${linkCards}
				<div class="tc-footer">
					<div class="tc-actions">
						<span class="tc-action">${this.svgReply}</span>
						<span class="tc-action">${this.svgRetweet}</span>
						<span class="tc-action">${this.svgHeart}</span>
						<span class="tc-action">${this.svgShare}</span>
					</div>
					<div class="tc-meta">
						<span class="tc-chars${overClass}">${len}/280</span>
						<span class="tc-thread-num">${i + 1}/${total}</span>
					</div>
				</div>
			</div>`;
		}).join('');
	}

	// ── Link preview helpers ──

	private static URL_REGEX = /https?:\/\/[^\s<>"')\]]+/gi;

	private extractUrls(text: string): string[] {
		return [...text.matchAll(FolkThreadBuilder.URL_REGEX)].map(m => m[0]);
	}

	private renderTweetContent(text: string): string {
		// Escape first, then linkify URLs
		let html = this.esc(text);
		html = html.replace(/https?:\/\/[^\s&lt;&quot;&#39;\]\)]+/gi, (url) => {
			// Decode the escaped URL back for href
			const decoded = url.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"');
			return `<a href="${url}" class="tc-link" target="_blank" rel="noopener">${url}</a>`;
		});
		return html;
	}

	private extractLinkCards(text: string): string {
		const urls = this.extractUrls(text);
		if (!urls.length) return '';
		return urls.map(url => {
			const cached = this._linkPreviewCache.get(url);
			if (cached === undefined) {
				// Not fetched yet — start fetching and show placeholder
				this.fetchLinkPreview(url);
				const domain = this.getDomain(url);
				return `<a href="${this.esc(url)}" class="link-card link-card--loading" target="_blank" rel="noopener">
					<div class="link-card__body">
						<div class="link-card__domain">${this.esc(domain)}</div>
						<div class="link-card__title">Loading preview...</div>
					</div>
				</a>`;
			}
			if (cached === null) {
				// Fetch failed — show simple link card
				const domain = this.getDomain(url);
				return `<a href="${this.esc(url)}" class="link-card" target="_blank" rel="noopener">
					<div class="link-card__body">
						<div class="link-card__domain">${this.esc(domain)}</div>
						<div class="link-card__title">${this.esc(url)}</div>
					</div>
				</a>`;
			}
			// Full preview card
			const imgHtml = cached.image
				? `<div class="link-card__image"><img src="${this.esc(cached.image)}" alt="" loading="lazy"></div>`
				: '';
			return `<a href="${this.esc(url)}" class="link-card" target="_blank" rel="noopener">
				${imgHtml}
				<div class="link-card__body">
					<div class="link-card__domain">${this.esc(cached.domain)}</div>
					<div class="link-card__title">${this.esc(cached.title)}</div>
					${cached.description ? `<div class="link-card__desc">${this.esc(cached.description.substring(0, 120))}</div>` : ''}
				</div>
			</a>`;
		}).join('');
	}

	private getDomain(url: string): string {
		try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return url; }
	}

	private async fetchLinkPreview(url: string) {
		if (this._linkPreviewCache.has(url)) return;
		this._linkPreviewCache.set(url, undefined as any); // Mark as in-progress
		try {
			const res = await fetch('/api/link-preview?url=' + encodeURIComponent(url));
			if (!res.ok) throw new Error('fetch failed');
			const data = await res.json();
			this._linkPreviewCache.set(url, {
				title: data.title || url,
				description: data.description || '',
				image: data.image || null,
				domain: data.domain || this.getDomain(url),
			});
		} catch {
			this._linkPreviewCache.set(url, null);
		}
		// Re-render to show fetched preview
		if (this._mode === 'readonly') {
			this.renderReadonly();
		} else {
			this.renderPreview();
		}
	}

	// ── Draft management (Automerge) ──

	private async saveDraft() {
		const sr = this.shadowRoot;
		if (!sr) return;
		const textarea = sr.getElementById('thread-input') as HTMLTextAreaElement;
		const nameInput = sr.getElementById('thread-name') as HTMLInputElement;
		const handleInput = sr.getElementById('thread-handle') as HTMLInputElement;
		const saveBtn = sr.getElementById('thread-save') as HTMLButtonElement;

		const tweets = textarea.value.split(/\n---\n/).map(t => t.trim()).filter(Boolean);
		if (!tweets.length) return;

		if (!this._threadId) {
			this._threadId = generateThreadId();
		}

		const thread: ThreadData = {
			id: this._threadId,
			name: nameInput.value || 'Your Name',
			handle: handleInput.value || '@yourhandle',
			title: tweets[0].substring(0, 60),
			tweets,
			tweetImages: Object.keys(this._tweetImages).length ? { ...this._tweetImages } : null,
			imageUrl: this._thread?.imageUrl || null,
			createdAt: this._thread?.createdAt || Date.now(),
			updatedAt: Date.now(),
		};

		this._thread = thread;

		// Save via Automerge (await ensures doc is open first)
		await this.saveToAutomerge(thread);

		// Update URL
		history.replaceState(null, '', this.basePath + 'thread/' + this._threadId + '/edit');

		if (saveBtn) {
			saveBtn.textContent = 'Saved!';
			setTimeout(() => { saveBtn.textContent = 'Save Draft'; }, 2000);
		}

		this.loadDraftList();
	}

	private loadDraft(id: string) {
		const doc = this.getDoc();
		const thread = doc?.threads?.[id];
		if (!thread) return;

		// Deep-clone to get a plain object (Automerge proxies reject direct writes)
		const plain = JSON.parse(JSON.stringify(thread)) as ThreadData;
		this._threadId = plain.id;
		this._thread = plain;
		this._tweetImages = plain.tweetImages || {};

		const sr = this.shadowRoot;
		if (!sr) return;
		const textarea = sr.getElementById('thread-input') as HTMLTextAreaElement;
		const nameInput = sr.getElementById('thread-name') as HTMLInputElement;
		const handleInput = sr.getElementById('thread-handle') as HTMLInputElement;

		textarea.value = thread.tweets.join('\n---\n');
		nameInput.value = thread.name || '';
		handleInput.value = thread.handle || '';

		history.replaceState(null, '', this.basePath + 'thread/' + thread.id + '/edit');
		this.renderPreview();
		this.loadDraftList();
	}

	private async deleteDraft(id: string) {
		if (!confirm('Delete this draft?')) return;

		// Delete images on server
		try {
			await fetch(this.basePath + 'api/threads/' + id + '/images', { method: 'DELETE' });
		} catch { /* ignore */ }

		// Remove from Automerge
		this.deleteFromAutomerge(id);

		if (this._threadId === id) {
			this._threadId = null;
			this._thread = null;
			this._tweetImages = {};
			history.replaceState(null, '', this.basePath + 'thread');

			const sr = this.shadowRoot;
			if (sr) {
				const shareLinkArea = sr.getElementById('share-link-area') as HTMLElement;
				if (shareLinkArea) shareLinkArea.innerHTML = '';
			}
		}

		this.loadDraftList();
	}

	private loadDraftList() {
		const sr = this.shadowRoot;
		if (!sr) return;
		const draftsList = sr.getElementById('drafts-list');
		if (!draftsList) return;

		const threads = this.listThreads();

		if (!threads.length) {
			draftsList.innerHTML = '<div class="drafts-empty">No saved drafts</div>';
			return;
		}

		draftsList.innerHTML = threads.map(t => {
			const date = new Date(t.updatedAt);
			const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
			const active = t.id === this._threadId ? ' draft-item--active' : '';
			return `<div class="draft-item${active}">
				<div class="draft-item__info" data-load-id="${this.esc(t.id)}">
					<strong>${this.esc(t.title || 'Untitled')}</strong>
					<span>${t.tweets.length} tweets &#183; ${dateStr}</span>
				</div>
				<button class="draft-item__delete" data-delete-id="${this.esc(t.id)}">&#215;</button>
			</div>`;
		}).join('');

		// Attach events
		draftsList.querySelectorAll('[data-load-id]').forEach(el => {
			el.addEventListener('click', () => this.loadDraft((el as HTMLElement).dataset.loadId!));
		});
		draftsList.querySelectorAll('[data-delete-id]').forEach(el => {
			el.addEventListener('click', (e) => { e.stopPropagation(); this.deleteDraft((el as HTMLElement).dataset.deleteId!); });
		});
	}

	// ── Image operations (server API) ──

	private async uploadTweetImage(index: string, file: File) {
		if (!this._threadId) {
			await this.saveDraft();
			if (!this._threadId) return;
		}

		try {
			const form = new FormData();
			form.append('file', file);
			const res = await fetch(this.basePath + 'api/threads/' + this._threadId + '/tweet/' + index + '/upload-image', { method: 'POST', body: form });
			const data = await res.json();
			if (data.imageUrl) {
				// Bust browser cache with timestamp
				this._tweetImages[index] = data.imageUrl + '?t=' + Date.now();
				if (this._thread) {
					this._thread.tweetImages = { ...this._tweetImages };
					await this.saveToAutomerge(this._thread);
				}
				this.renderPreview();
			}
		} catch (e) { console.error('Tweet image upload failed:', e); }
	}

	private async generateTweetImage(index: string) {
		if (!this._threadId) {
			await this.saveDraft();
			if (!this._threadId) return;
		}

		const sr = this.shadowRoot;
		const btn = sr?.querySelector(`[data-generate-idx="${index}"]`) as HTMLButtonElement;
		if (btn) { btn.textContent = 'Generating...'; btn.disabled = true; }

		try {
			const res = await fetch(this.basePath + 'api/threads/' + this._threadId + '/tweet/' + index + '/image', { method: 'POST' });
			const data = await res.json();
			if (data.imageUrl) {
				// Bust browser cache with timestamp
				this._tweetImages[index] = data.imageUrl + '?t=' + Date.now();
				if (this._thread) {
					this._thread.tweetImages = { ...this._tweetImages };
					await this.saveToAutomerge(this._thread);
				}
				this.renderPreview();
			} else if (btn) {
				btn.textContent = 'Failed';
				setTimeout(() => this.renderPreview(), 2000);
			}
		} catch {
			if (btn) { btn.textContent = 'Failed'; setTimeout(() => this.renderPreview(), 2000); }
		}
	}

	private async removeTweetImage(index: string) {
		if (!this._threadId) return;
		try {
			await fetch(this.basePath + 'api/threads/' + this._threadId + '/tweet/' + index + '/image', { method: 'DELETE' });
			delete this._tweetImages[index];
			if (this._thread) {
				this._thread.tweetImages = Object.keys(this._tweetImages).length ? { ...this._tweetImages } : null;
				await this.saveToAutomerge(this._thread);
			}
			this.renderPreview();
		} catch (e) { console.error('Tweet image removal failed:', e); }
	}

	// ── Export ──

	private formatForPlatform(platform: string): { text: string; warnings: string[] } {
		const sr = this.shadowRoot;
		if (!sr) return { text: '', warnings: [] };
		const textarea = sr.getElementById('thread-input') as HTMLTextAreaElement;
		const titleInput = sr.getElementById('thread-title') as HTMLInputElement;

		const limit = PLATFORM_LIMITS[platform] || 280;
		const tweets = (this._mode === 'readonly' && this._thread)
			? this._thread.tweets
			: textarea.value.split(/\n---\n/).map(t => t.trim()).filter(Boolean);
		const total = tweets.length;
		const title = (this._mode === 'readonly' && this._thread) ? this._thread.title : titleInput?.value || '';
		const warnings: string[] = [];

		if (platform === 'linkedin') {
			let text = '';
			if (title) text += title + '\n\n';
			text += tweets.join('\n\n');
			text += '\n\n---\nOriginally composed as a ' + total + '-tweet thread.';
			return { text, warnings: text.length > limit ? ['Content exceeds LinkedIn\'s ' + limit + ' char limit (' + text.length + ' chars)'] : [] };
		}

		const parts = tweets.map((t, i) => {
			const prefix = total > 1 ? (i + 1) + '/' + total + ' ' : '';
			const full = prefix + t;
			if (full.length > limit) warnings.push('Tweet ' + (i + 1) + ' exceeds ' + limit + ' chars (' + full.length + ')');
			return full;
		});

		return { text: parts.join('\n\n'), warnings };
	}

	// ── Auto-save ──

	private scheduleAutoSave() {
		if (!this._threadId) return;
		if (this._autoSaveTimer) clearTimeout(this._autoSaveTimer);
		this._autoSaveTimer = setTimeout(() => this.saveDraft(), 1500);
	}

	// ── Event binding ──

	private bindEditorEvents() {
		const sr = this.shadowRoot;
		if (!sr) return;

		const textarea = sr.getElementById('thread-input') as HTMLTextAreaElement;
		const nameInput = sr.getElementById('thread-name') as HTMLInputElement;
		const handleInput = sr.getElementById('thread-handle') as HTMLInputElement;
		const saveBtn = sr.getElementById('thread-save');
		const shareBtn = sr.getElementById('thread-share');
		const copyBtn = sr.getElementById('thread-copy');
		const toggleDraftsBtn = sr.getElementById('toggle-drafts');
		const draftsList = sr.getElementById('drafts-list') as HTMLElement;
		const preview = sr.getElementById('thread-preview') as HTMLElement;
		const tweetImageInput = sr.getElementById('tweet-image-input') as HTMLInputElement;

		// Preview updates
		textarea?.addEventListener('input', () => this.renderPreview());
		nameInput?.addEventListener('input', () => this.renderPreview());
		handleInput?.addEventListener('input', () => this.renderPreview());

		// Auto-save on blur
		textarea?.addEventListener('blur', () => this.scheduleAutoSave());
		nameInput?.addEventListener('blur', () => this.scheduleAutoSave());
		handleInput?.addEventListener('blur', () => this.scheduleAutoSave());

		// Buttons
		saveBtn?.addEventListener('click', () => this.saveDraft());
		shareBtn?.addEventListener('click', () => this.shareThread());

		// Drafts toggle
		toggleDraftsBtn?.addEventListener('click', () => {
			draftsList.hidden = !draftsList.hidden;
			toggleDraftsBtn.innerHTML = draftsList.hidden ? 'Saved Drafts &#9662;' : 'Saved Drafts &#9652;';
		});

		// Copy thread
		copyBtn?.addEventListener('click', async () => {
			const tweets = textarea.value.split(/\n---\n/).map(t => t.trim()).filter(Boolean);
			if (!tweets.length) return;
			const total = tweets.length;
			const text = tweets.map((t, i) => (i + 1) + '/' + total + '\n' + t).join('\n\n');
			try {
				await navigator.clipboard.writeText(text);
				copyBtn.textContent = 'Copied!';
				setTimeout(() => { copyBtn.textContent = 'Copy Thread'; }, 2000);
			} catch {
				copyBtn.textContent = 'Failed';
				setTimeout(() => { copyBtn.textContent = 'Copy Thread'; }, 2000);
			}
		});

		// Export dropdown
		const exportBtn = sr.getElementById('thread-export-btn');
		const exportMenu = sr.getElementById('thread-export-menu');
		exportBtn?.addEventListener('click', () => { if (exportMenu) exportMenu.hidden = !exportMenu.hidden; });
		sr.addEventListener('click', (e) => {
			if (!exportBtn?.contains(e.target as Node) && !exportMenu?.contains(e.target as Node)) {
				if (exportMenu) exportMenu.hidden = true;
			}
		});

		exportMenu?.querySelectorAll('button[data-platform]').forEach(btn => {
			btn.addEventListener('click', async () => {
				const platform = (btn as HTMLElement).dataset.platform!;
				const tweets = textarea.value.split(/\n---\n/).map(t => t.trim()).filter(Boolean);
				if (!tweets.length) return;
				const { text, warnings } = this.formatForPlatform(platform);
				try {
					await navigator.clipboard.writeText(text);
					const label = warnings.length
						? 'Copied with warnings: ' + warnings.join('; ')
						: 'Copied for ' + platform + '!';
					if (copyBtn) {
						copyBtn.textContent = label;
						setTimeout(() => { copyBtn.textContent = 'Copy Thread'; }, 3000);
					}
				} catch {
					if (copyBtn) { copyBtn.textContent = 'Failed'; setTimeout(() => { copyBtn.textContent = 'Copy Thread'; }, 2000); }
				}
				if (exportMenu) exportMenu.hidden = true;
			});
		});

		// Send to Postiz
		const postizBtn = sr.getElementById('thread-send-postiz');
		postizBtn?.addEventListener('click', async () => {
			const tweets = textarea.value.split(/\n---\n/).map(t => t.trim()).filter(Boolean);
			if (!tweets.length) { postizBtn.textContent = 'No content'; setTimeout(() => { postizBtn.textContent = 'Send to Postiz'; }, 2000); return; }
			postizBtn.textContent = 'Sending...';
			(postizBtn as HTMLButtonElement).disabled = true;
			try {
				const res = await fetch(`${this.basePath}api/postiz/threads`, {
					method: 'POST',
					headers: { 'Content-Type': 'application/json' },
					body: JSON.stringify({ tweets, type: 'draft' }),
				});
				const data = await res.json();
				if (!res.ok) throw new Error(data.error || 'Failed');
				postizBtn.textContent = 'Sent!';
				setTimeout(() => { postizBtn.textContent = 'Send to Postiz'; (postizBtn as HTMLButtonElement).disabled = false; }, 3000);
			} catch (err: any) {
				postizBtn.textContent = err.message || 'Error';
				setTimeout(() => { postizBtn.textContent = 'Send to Postiz'; (postizBtn as HTMLButtonElement).disabled = false; }, 3000);
			}
		});

		// Per-tweet image operations (event delegation)
		preview?.addEventListener('click', (e) => {
			const target = e.target as HTMLElement;

			const photoBtn = target.closest('[data-photo-idx]') as HTMLElement;
			if (photoBtn) {
				const idx = photoBtn.dataset.photoIdx!;
				const menu = preview.querySelector(`[data-menu-idx="${idx}"]`) as HTMLElement;
				if (menu) {
					const wasHidden = menu.hidden;
					preview.querySelectorAll('.photo-menu').forEach(m => (m as HTMLElement).hidden = true);
					menu.hidden = !wasHidden;
				}
				return;
			}

			const uploadTweetBtn = target.closest('[data-upload-idx]') as HTMLElement;
			if (uploadTweetBtn) {
				preview.querySelectorAll('.photo-menu').forEach(m => (m as HTMLElement).hidden = true);
				this._tweetImageUploadIdx = uploadTweetBtn.dataset.uploadIdx!;
				tweetImageInput?.click();
				return;
			}

			const genTweetBtn = target.closest('[data-generate-idx]') as HTMLElement;
			if (genTweetBtn) {
				preview.querySelectorAll('.photo-menu').forEach(m => (m as HTMLElement).hidden = true);
				this.generateTweetImage(genTweetBtn.dataset.generateIdx!);
				return;
			}

			const removeBtn = target.closest('[data-remove-idx]') as HTMLElement;
			if (removeBtn) {
				this.removeTweetImage(removeBtn.dataset.removeIdx!);
				return;
			}
		});

		// Close photo menus on outside click
		sr.addEventListener('click', (e) => {
			const target = e.target as HTMLElement;
			if (!target.closest('.photo-btn') && !target.closest('.photo-menu')) {
				preview?.querySelectorAll('.photo-menu').forEach(m => (m as HTMLElement).hidden = true);
			}
		});

		// Tweet image file input
		tweetImageInput?.addEventListener('change', () => {
			const file = tweetImageInput.files?.[0];
			if (file && this._tweetImageUploadIdx !== null) {
				this.uploadTweetImage(this._tweetImageUploadIdx, file);
			}
			tweetImageInput.value = '';
			this._tweetImageUploadIdx = null;
		});

		// Load initial draft list
		this.loadDraftList();
	}

	private async shareThread() {
		const sr = this.shadowRoot;
		if (!sr) return;
		const shareBtn = sr.getElementById('thread-share') as HTMLButtonElement;
		const shareLinkArea = sr.getElementById('share-link-area') as HTMLElement;

		shareBtn.textContent = 'Saving...';
		shareBtn.disabled = true;

		try {
			await this.saveDraft();
			if (!this._threadId) { shareBtn.textContent = 'Share'; shareBtn.disabled = false; return; }

			const url = window.location.origin + this.basePath + 'thread/' + this._threadId;
			try {
				await navigator.clipboard.writeText(url);
				shareBtn.textContent = 'Link Copied!';
			} catch {
				shareBtn.textContent = 'Shared!';
			}

			shareLinkArea.innerHTML = `<div class="share-link">
				<code>${this.esc(url)}</code>
				<button id="copy-share-link">Copy</button>
			</div>`;
			sr.getElementById('copy-share-link')?.addEventListener('click', () => {
				navigator.clipboard.writeText(url);
			});
		} catch {
			shareBtn.textContent = 'Error';
		}
		setTimeout(() => { shareBtn.textContent = 'Share'; shareBtn.disabled = false; }, 3000);
	}

	private bindReadonlyEvents() {
		const sr = this.shadowRoot;
		if (!sr || !this._thread) return;

		const t = this._thread;

		sr.getElementById('ro-copy-thread')?.addEventListener('click', async () => {
			const text = t.tweets.map((tw, i) => (i + 1) + '/' + t.tweets.length + '\n' + tw).join('\n\n');
			try { await navigator.clipboard.writeText(text); this.showToast('Thread copied!'); }
			catch { this.showToast('Failed to copy'); }
		});

		sr.getElementById('ro-copy-link')?.addEventListener('click', async () => {
			try { await navigator.clipboard.writeText(window.location.href); this.showToast('Link copied!'); }
			catch { this.showToast('Failed to copy'); }
		});

		const exportBtn = sr.getElementById('ro-export-btn');
		const exportMenu = sr.getElementById('ro-export-menu');
		exportBtn?.addEventListener('click', () => { if (exportMenu) exportMenu.hidden = !exportMenu.hidden; });
		sr.addEventListener('click', (e) => {
			if (!exportBtn?.contains(e.target as Node) && !exportMenu?.contains(e.target as Node)) {
				if (exportMenu) exportMenu.hidden = true;
			}
		});

		exportMenu?.querySelectorAll('button[data-platform]').forEach(btn => {
			btn.addEventListener('click', async () => {
				const platform = (btn as HTMLElement).dataset.platform!;
				const { text, warnings } = this.formatForPlatform(platform);
				try {
					await navigator.clipboard.writeText(text);
					this.showToast(warnings.length ? 'Copied with warnings: ' + warnings.join('; ') : 'Copied for ' + platform + '!');
				} catch { this.showToast('Failed to copy'); }
				if (exportMenu) exportMenu.hidden = true;
			});
		});

		// Send to Postiz (readonly)
		const roPostizBtn = sr.getElementById('ro-send-postiz');
		roPostizBtn?.addEventListener('click', async () => {
			if (!t) return;
			roPostizBtn.textContent = 'Sending...';
			(roPostizBtn as HTMLButtonElement).disabled = true;
			try {
				const res = await fetch(`${this.basePath}api/postiz/threads`, {
					method: 'POST',
					headers: { 'Content-Type': 'application/json' },
					body: JSON.stringify({ tweets: t.tweets, type: 'draft' }),
				});
				const data = await res.json();
				if (!res.ok) throw new Error(data.error || 'Failed');
				this.showToast('Thread sent to Postiz as draft!');
				roPostizBtn.textContent = 'Sent!';
				setTimeout(() => { roPostizBtn.textContent = 'Send to Postiz'; (roPostizBtn as HTMLButtonElement).disabled = false; }, 3000);
			} catch (err: any) {
				this.showToast(err.message || 'Failed to send');
				roPostizBtn.textContent = 'Error';
				setTimeout(() => { roPostizBtn.textContent = 'Send to Postiz'; (roPostizBtn as HTMLButtonElement).disabled = false; }, 3000);
			}
		});
	}

	private showToast(msg: string) {
		const toast = this.shadowRoot?.getElementById('export-toast');
		if (!toast) return;
		toast.textContent = msg;
		toast.hidden = false;
		setTimeout(() => { toast.hidden = true; }, 2500);
	}

	// ── Styles ──

	private getBaseStyles(): string {
		return `
			:host { display: block; }
			.btn { padding: 0.5rem 1rem; border-radius: 8px; border: none; font-size: 0.85rem; font-weight: 600; cursor: pointer; transition: all 0.15s; text-decoration: none; display: inline-flex; align-items: center; }
			.btn--primary { background: var(--rs-primary, #6366f1); color: white; }
			.btn--primary:hover { background: var(--rs-primary-hover, #818cf8); }
			.btn--outline { background: transparent; color: var(--rs-text-secondary, #94a3b8); border: 1px solid var(--rs-input-border, #334155); }
			.btn--outline:hover { border-color: var(--rs-primary, #6366f1); color: var(--rs-primary-hover, #c4b5fd); }
			.btn--success { background: var(--rs-success, #10b981); color: white; }
			.btn--success:hover { opacity: 0.85; }
			.btn--postiz { background: #f97316; color: white; }
			.btn--postiz:hover { background: #fb923c; }
			.btn:disabled { opacity: 0.5; cursor: not-allowed; }

			.tweet-card {
				position: relative; background: var(--rs-bg-surface, #1e293b); border: 1px solid var(--rs-input-border, #334155); border-radius: 0.75rem;
				padding: 1rem; margin-bottom: 0;
			}
			.tweet-card + .tweet-card { border-top-left-radius: 0; border-top-right-radius: 0; margin-top: -1px; }
			.tweet-card:has(+ .tweet-card) { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
			.connector { position: absolute; left: 29px; top: -1px; width: 2px; height: 1rem; background: var(--rs-input-border, #334155); z-index: 1; }
			.tc-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
			.tc-avatar { width: 40px; height: 40px; border-radius: 50%; background: var(--rs-primary, #6366f1); display: flex; align-items: center; justify-content: center; color: white; font-weight: 700; font-size: 1rem; flex-shrink: 0; }
			.tc-name { font-weight: 700; color: var(--rs-text-primary, #f1f5f9); font-size: 0.9rem; }
			.tc-handle { color: var(--rs-text-muted, #64748b); font-size: 0.85rem; }
			.tc-dot { color: var(--rs-text-muted, #64748b); font-size: 0.85rem; }
			.tc-time { color: var(--rs-text-muted, #64748b); font-size: 0.85rem; }
			.tc-content { color: var(--rs-text-primary, #f1f5f9); font-size: 0.95rem; line-height: 1.6; margin: 0 0 0.75rem; white-space: pre-wrap; word-break: break-word; }
			.tc-footer { display: flex; align-items: center; justify-content: space-between; }
			.tc-actions { display: flex; gap: 1.25rem; }
			.tc-action { display: flex; align-items: center; gap: 0.3rem; color: var(--rs-text-muted, #64748b); font-size: 0.8rem; cursor: default; }
			.tc-action svg { width: 16px; height: 16px; }
			.tc-meta { display: flex; align-items: center; gap: 0.75rem; font-size: 0.75rem; color: var(--rs-text-muted, #64748b); }
			.tc-chars { font-variant-numeric: tabular-nums; }
			.tc-chars--over { color: var(--rs-error, #ef4444); font-weight: 600; }
			.tc-thread-num { color: var(--rs-primary, #6366f1); font-weight: 600; }
			.attached-image { position: relative; margin-top: 0.5rem; border-radius: 8px; overflow: hidden; border: 1px solid var(--rs-input-border, #334155); }
			.attached-image img { display: block; width: 100%; height: auto; }
			.image-remove {
				position: absolute; top: 6px; right: 6px; width: 22px; height: 22px;
				border-radius: 50%; background: rgba(0,0,0,0.7); color: white; border: none;
				font-size: 0.8rem; cursor: pointer; display: flex; align-items: center;
				justify-content: center; line-height: 1; transition: background 0.15s;
			}
			.image-remove:hover { background: var(--rs-error, #ef4444); }

			.export-dropdown { position: relative; }
			.export-menu {
				position: absolute; top: calc(100% + 4px); right: 0; z-index: 100;
				background: var(--rs-bg-surface, #1e293b); border: 1px solid var(--rs-input-border, #334155); border-radius: 8px;
				min-width: 180px; overflow: hidden; box-shadow: 0 8px 24px var(--rs-shadow-lg, rgba(0,0,0,0.3));
			}
			.export-menu[hidden] { display: none; }
			.export-menu button {
				display: block; width: 100%; padding: 0.6rem 0.75rem; border: none;
				background: transparent; color: var(--rs-text-primary, #f1f5f9); font-size: 0.85rem;
				text-align: left; cursor: pointer; transition: background 0.1s;
			}
			.export-menu button:hover { background: var(--rs-bg-hover, rgba(99,102,241,0.15)); }
			.export-menu button + button { border-top: 1px solid var(--rs-bg-hover, #334155); }

			.preview { display: flex; flex-direction: column; gap: 0; }
			.preview-empty { color: var(--rs-text-muted, #64748b); text-align: center; padding: 3rem 1rem; font-size: 0.9rem; }
		`;
	}

	private getEditorStyles(): string {
		return `
			.thread-page { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; max-width: 1100px; margin: 0 auto; padding: 2rem 1rem; min-height: 80vh; }
			.page-header { grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
			.page-header h1 { margin: 0; font-size: 1.5rem; color: var(--rs-text-primary, #f1f5f9); background: linear-gradient(135deg, #7dd3fc, #c4b5fd); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
			.page-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
			.compose { position: sticky; top: 1rem; align-self: start; display: flex; flex-direction: column; gap: 1rem; }
			.compose-textarea {
				width: 100%; min-height: 320px; background: var(--rs-bg-surface, #1e293b); color: var(--rs-text-primary, #f1f5f9); border: 1px solid var(--rs-input-border, #334155);
				border-radius: 0.75rem; padding: 1rem; font-family: inherit; font-size: 0.9rem; resize: vertical;
				line-height: 1.6; box-sizing: border-box;
			}
			.compose-textarea:focus { outline: none; border-color: var(--rs-primary, #6366f1); }
			.compose-textarea::placeholder { color: var(--rs-text-muted, #64748b); }
			.compose-fields { display: flex; gap: 0.75rem; }
			.compose-input {
				flex: 1; background: var(--rs-bg-surface, #1e293b); color: var(--rs-text-primary, #f1f5f9); border: 1px solid var(--rs-input-border, #334155);
				border-radius: 8px; padding: 0.5rem 0.75rem; font-size: 0.85rem; box-sizing: border-box;
			}
			.compose-input:focus { outline: none; border-color: var(--rs-primary, #6366f1); }
			.compose-input::placeholder { color: var(--rs-text-muted, #64748b); }
			.compose-title {
				width: 100%; background: var(--rs-bg-surface, #1e293b); color: var(--rs-text-primary, #f1f5f9); border: 1px solid var(--rs-input-border, #334155);
				border-radius: 8px; padding: 0.5rem 0.75rem; font-size: 0.85rem; box-sizing: border-box;
			}
			.compose-title:focus { outline: none; border-color: var(--rs-primary, #6366f1); }
			.compose-title::placeholder { color: var(--rs-text-muted, #64748b); }

			.drafts-area { grid-column: 1 / -1; }
			.drafts-toggle { cursor: pointer; user-select: none; }
			.drafts-list {
				display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 0.5rem;
				margin-top: 0.75rem;
			}
			.drafts-list[hidden] { display: none; }
			.drafts-empty { color: var(--rs-text-muted, #64748b); font-size: 0.8rem; padding: 0.5rem 0; }
			.draft-item {
				display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem;
				background: var(--rs-bg-surface, #1e293b); border: 1px solid var(--rs-input-border, #334155); border-radius: 8px;
				transition: border-color 0.15s; cursor: pointer;
			}
			.draft-item:hover { border-color: var(--rs-primary, #6366f1); }
			.draft-item--active { border-color: var(--rs-primary, #6366f1); background: rgba(99,102,241,0.1); }
			.draft-item__info { flex: 1; min-width: 0; }
			.draft-item__info strong { display: block; font-size: 0.8rem; color: var(--rs-text-primary, #f1f5f9); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
			.draft-item__info span { font-size: 0.7rem; color: var(--rs-text-muted, #64748b); }
			.draft-item__delete {
				background: none; border: none; color: var(--rs-text-muted, #64748b); font-size: 1.2rem; cursor: pointer;
				padding: 0 4px; line-height: 1; flex-shrink: 0;
			}
			.draft-item__delete:hover { color: var(--rs-error, #ef4444); }

			#share-link-area { grid-column: 1 / -1; }
			.share-link {
				display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.75rem;
				background: rgba(99,102,241,0.1); border: 1px solid var(--rs-primary, #6366f1); border-radius: 8px;
				font-size: 0.8rem; color: var(--rs-primary-hover, #c4b5fd);
			}
			.share-link code { font-size: 0.75rem; color: var(--rs-accent, #7dd3fc); }
			.share-link button {
				background: none; border: none; color: var(--rs-text-secondary, #94a3b8); cursor: pointer; font-size: 0.75rem; padding: 2px 6px;
			}
			.share-link button:hover { color: var(--rs-text-primary, #f1f5f9); }

			.photo-btn {
				position: absolute; top: 8px; right: 8px; z-index: 5;
				width: 28px; height: 28px; border-radius: 50%; border: 1px solid var(--rs-input-border, #334155);
				background: var(--rs-bg-surface, #1e293b); color: var(--rs-text-muted, #64748b); cursor: pointer;
				display: flex; align-items: center; justify-content: center;
				opacity: 0; transition: opacity 0.15s, border-color 0.15s, color 0.15s;
			}
			.tweet-card:hover .photo-btn { opacity: 1; }
			.photo-btn:hover { border-color: var(--rs-primary, #6366f1); color: var(--rs-primary-hover, #c4b5fd); }
			.photo-btn svg { width: 14px; height: 14px; }
			.photo-plus {
				position: absolute; bottom: -1px; right: -3px; font-size: 10px; font-weight: 700;
				color: var(--rs-primary, #6366f1); line-height: 1;
			}
			.photo-menu {
				position: absolute; top: 38px; right: 8px; z-index: 10;
				background: var(--rs-bg-surface, #1e293b); border: 1px solid var(--rs-input-border, #334155); border-radius: 8px;
				min-width: 160px; overflow: hidden; box-shadow: 0 8px 24px var(--rs-shadow-lg, rgba(0,0,0,0.3));
			}
			.photo-menu[hidden] { display: none; }
			.photo-menu button {
				display: flex; align-items: center; gap: 0.4rem; width: 100%;
				padding: 0.5rem 0.7rem; border: none; background: transparent;
				color: var(--rs-text-primary, #f1f5f9); font-size: 0.8rem; cursor: pointer; transition: background 0.1s;
			}
			.photo-menu button:hover { background: var(--rs-bg-hover, rgba(99,102,241,0.15)); }
			.photo-menu button + button { border-top: 1px solid var(--rs-bg-hover, #334155); }
			.photo-menu button svg { width: 14px; height: 14px; }

			.link-card {
				display: flex; overflow: hidden; border: 1px solid var(--rs-input-border, #334155);
				border-radius: 12px; text-decoration: none; color: inherit; margin-top: 0.5rem;
				transition: border-color 0.15s;
			}
			.link-card:hover { border-color: var(--rs-primary, #6366f1); }
			.link-card--loading { opacity: 0.6; }
			.link-card__image { width: 120px; min-height: 80px; flex-shrink: 0; overflow: hidden; background: var(--rs-bg-hover, #334155); }
			.link-card__image img { width: 100%; height: 100%; object-fit: cover; display: block; }
			.link-card__body { padding: 0.5rem 0.75rem; min-width: 0; flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 0.15rem; }
			.link-card__domain { font-size: 0.7rem; color: var(--rs-text-muted, #64748b); text-transform: lowercase; }
			.link-card__title { font-size: 0.82rem; font-weight: 600; color: var(--rs-text-primary, #f1f5f9); line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
			.link-card__desc { font-size: 0.75rem; color: var(--rs-text-secondary, #94a3b8); line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
			.tc-link { color: var(--rs-accent, #7dd3fc); text-decoration: none; }
			.tc-link:hover { text-decoration: underline; }

			@media (max-width: 700px) {
				.thread-page { grid-template-columns: 1fr; }
				.compose { position: static; }
			}
		`;
	}

	private getReadonlyStyles(): string {
		return `
			.thread-ro { max-width: 640px; margin: 0 auto; padding: 2rem 1rem; }
			.ro-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem; }
			.ro-author { display: flex; align-items: center; gap: 0.75rem; }
			.ro-name { font-weight: 700; color: var(--rs-text-primary, #f1f5f9); font-size: 1.1rem; }
			.ro-handle { color: var(--rs-text-muted, #64748b); font-size: 0.9rem; }
			.ro-meta { display: flex; align-items: center; gap: 0.5rem; color: var(--rs-text-muted, #64748b); font-size: 0.85rem; }
			.ro-cards { margin-bottom: 1.5rem; }
			.ro-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 2rem; padding-bottom: 2rem; border-bottom: 1px solid var(--rs-input-border, #334155); }
			.ro-cta { display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap; }
			.toast {
				position: fixed; bottom: 1.5rem; left: 50%; transform: translateX(-50%);
				background: var(--rs-bg-surface, #1e293b); border: 1px solid var(--rs-primary, #6366f1); color: var(--rs-primary-hover, #c4b5fd);
				padding: 0.6rem 1.25rem; border-radius: 8px; font-size: 0.85rem;
				box-shadow: 0 4px 16px var(--rs-shadow-lg, rgba(0,0,0,0.3)); z-index: 1000;
			}
			.toast[hidden] { display: none; }
		`;
	}
}

customElements.define('folk-thread-builder', FolkThreadBuilder);
