/**
 * <folk-pubs-publish-panel> — Unified sidebar for sharing, DIY printing, and ordering.
 *
 * Three tabs: Share, DIY Print, Order.
 * Replaces the old bare "Download PDF" link.
 *
 * Attributes:
 *   pdf-url     — Blob URL of the generated PDF
 *   format-id   — Selected format (a7, a6, quarter-letter, digest)
 *   format-name — Display name of the format
 *   page-count  — Number of pages in the generated PDF
 *   space-slug  — Current space slug for API calls
 */

import { getModuleApiBase } from "../../../shared/url-helpers";

export class FolkPubsPublishPanel extends HTMLElement {
	private _pdfUrl = "";
	private _formatId = "";
	private _formatName = "";
	private _pageCount = 0;
	private _spaceSlug = "personal";
	private _activeTab: "share" | "diy" | "order" = "share";
	private _printers: any[] = [];
	private _printersLoading = false;
	private _printersError: string | null = null;
	private _selectedProvider: any = null;
	private _emailSending = false;
	private _emailSent = false;
	private _emailError: string | null = null;
	private _impositionLoading = false;
	private _epubReflowLoading = false;
	private _epubFixedLoading = false;
	private _epubError: string | null = null;
	private _publishing = false;
	private _publishResult: { hosted_url: string; pdf_url: string; epub_url: string; epub_fixed_url: string | null } | null = null;
	private _publishError: string | null = null;
	private _publishIncludeFixed = false;
	private _orderStatus: string | null = null;
	private _batchStatus: any = null;

	private static readonly ICONS = {
		download: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`,
		copy: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`,
		mail: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>`,
		book: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>`,
		scissors: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><line x1="20" y1="4" x2="8.12" y2="15.88"/><line x1="14.47" y1="14.48" x2="20" y2="20"/><line x1="8.12" y1="8.12" x2="12" y2="12"/></svg>`,
		printer: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>`,
		location: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>`,
		users: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>`,
		check: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`,
		send: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>`,
		share: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>`,
		info: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`,
		phone: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/></svg>`,
		globe: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>`,
		verified: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>`,
	};

	static get observedAttributes() {
		return ["pdf-url", "format-id", "format-name", "page-count", "space-slug"];
	}

	attributeChangedCallback(name: string, _old: string, val: string) {
		if (name === "pdf-url") this._pdfUrl = val;
		else if (name === "format-id") this._formatId = val;
		else if (name === "format-name") this._formatName = val;
		else if (name === "page-count") this._pageCount = parseInt(val) || 0;
		else if (name === "space-slug") this._spaceSlug = val;
		if (this.shadowRoot) this.render();
	}

	connectedCallback() {
		this._pdfUrl = this.getAttribute("pdf-url") || "";
		this._formatId = this.getAttribute("format-id") || "";
		this._formatName = this.getAttribute("format-name") || "";
		this._pageCount = parseInt(this.getAttribute("page-count") || "0") || 0;
		this._spaceSlug = this.getAttribute("space-slug") || "personal";
		if (!this.shadowRoot) this.attachShadow({ mode: "open" });
		this.render();
	}

	private render() {
		if (!this.shadowRoot) return;
		this.shadowRoot.innerHTML = `
			${this.getStyles()}
			<div class="panel">
				<div class="tabs">
					<button class="tab ${this._activeTab === 'share' ? 'active' : ''}" data-tab="share">
						${FolkPubsPublishPanel.ICONS.share}
						<span>Share</span>
					</button>
					<button class="tab ${this._activeTab === 'diy' ? 'active' : ''}" data-tab="diy">
						${FolkPubsPublishPanel.ICONS.scissors}
						<span>DIY Print</span>
					</button>
					<button class="tab ${this._activeTab === 'order' ? 'active' : ''}" data-tab="order">
						${FolkPubsPublishPanel.ICONS.printer}
						<span>Order</span>
					</button>
				</div>
				<div class="tab-content">
					${this._activeTab === 'share' ? this.renderShareTab() : ''}
					${this._activeTab === 'diy' ? this.renderDiyTab() : ''}
					${this._activeTab === 'order' ? this.renderOrderTab() : ''}
				</div>
			</div>
		`;
		this.bindEvents();
	}

	private renderShareTab(): string {
		return `
			<div class="section">
				<div class="info-card">
					${FolkPubsPublishPanel.ICONS.book}
					<div class="info-card-text">
						<span class="info-card-title">${this.esc(this._formatName)}</span>
						<span class="info-card-detail">${this._pageCount} pages</span>
					</div>
				</div>

				${this.renderPublishSection()}

				<div class="divider-row"><span class="divider-line"></span><span class="divider-text">downloads</span><span class="divider-line"></span></div>

				<a class="action-btn secondary" href="${this._pdfUrl}" download>
					${FolkPubsPublishPanel.ICONS.download}
					<span>Download PDF</span>
				</a>

				<div class="epub-row">
					<button class="action-btn secondary epub-btn" data-action="download-epub-reflow" ${this._epubReflowLoading ? 'disabled' : ''}>
						${this._epubReflowLoading ? '<span class="spinner"></span>' : FolkPubsPublishPanel.ICONS.book}
						<span>${this._epubReflowLoading ? 'Building...' : 'EPUB · reflowable'}</span>
					</button>
					<button class="action-btn secondary epub-btn" data-action="download-epub-fixed" ${this._epubFixedLoading ? 'disabled' : ''}>
						${this._epubFixedLoading ? '<span class="spinner"></span>' : FolkPubsPublishPanel.ICONS.book}
						<span>${this._epubFixedLoading ? 'Building...' : 'EPUB · fixed layout'}</span>
					</button>
				</div>
				<p class="hint">Reflowable reads on any e-reader. Fixed layout preserves page design.</p>
				${this._epubError ? `<div class="msg error">${this.esc(this._epubError)}</div>` : ''}

				<div class="divider-row"><span class="divider-line"></span><span class="divider-text">or send by email</span><span class="divider-line"></span></div>

				<div class="email-row">
					<div class="email-input-wrap">
						${FolkPubsPublishPanel.ICONS.mail}
						<input type="email" class="email-input" placeholder="recipient@email.com" />
					</div>
					<button class="action-btn accent send-btn" data-action="email-pdf" ${this._emailSending ? 'disabled' : ''}>
						${this._emailSending
							? '<span class="spinner"></span>'
							: FolkPubsPublishPanel.ICONS.send}
						<span>${this._emailSending ? 'Sending...' : 'Send'}</span>
					</button>
				</div>
				${this._emailSent ? `<div class="msg success">${FolkPubsPublishPanel.ICONS.check} PDF sent!</div>` : ''}
				${this._emailError ? `<div class="msg error">${this.esc(this._emailError)}</div>` : ''}
			</div>
		`;
	}

	private renderPublishSection(): string {
		if (this._publishResult) {
			return `
				<div class="publish-success">
					<div class="publish-success-head">
						${FolkPubsPublishPanel.ICONS.check}
						<span>Published to ${this.esc(this._spaceSlug)}</span>
					</div>
					<div class="publish-url-row">
						<input type="text" class="publish-url" readonly value="${this.esc(this._publishResult.hosted_url)}" />
						<button class="action-btn secondary publish-copy-btn" data-action="copy-published-link">${FolkPubsPublishPanel.ICONS.copy}<span>Copy</span></button>
					</div>
					<div class="publish-actions-row">
						<a class="publish-link" href="${this.esc(this._publishResult.hosted_url)}" target="_blank" rel="noopener">Open page</a>
						<button class="publish-link-btn" data-action="publish-again">Publish again</button>
					</div>
				</div>
			`;
		}

		return `
			<button class="action-btn primary publish-btn" data-action="publish-to-space" ${this._publishing ? 'disabled' : ''}>
				${this._publishing ? '<span class="spinner"></span>' : FolkPubsPublishPanel.ICONS.share}
				<span>${this._publishing ? 'Publishing…' : `Publish to ${this._spaceSlug}`}</span>
			</button>
			<label class="publish-option">
				<input type="checkbox" data-action="toggle-include-fixed" ${this._publishIncludeFixed ? 'checked' : ''} />
				<span>Also build fixed-layout EPUB (slower, preserves page design)</span>
			</label>
			<p class="hint">Saves PDF + EPUB to your space and returns a public URL anyone can visit.</p>
			${this._publishError ? `<div class="msg error">${this.esc(this._publishError)}</div>` : ''}
		`;
	}

	private renderDiyTab(): string {
		return `
			<div class="section">
				<div class="section-header">
					${FolkPubsPublishPanel.ICONS.scissors}
					<div>
						<div class="section-title">DIY Printing Guide</div>
						<div class="section-subtitle">Print, fold &amp; bind at home</div>
					</div>
				</div>

				<button class="action-btn primary" data-action="download-imposition" ${this._impositionLoading ? 'disabled' : ''}>
					${this._impositionLoading
						? '<span class="spinner"></span>'
						: FolkPubsPublishPanel.ICONS.download}
					<span>${this._impositionLoading ? 'Generating...' : 'Download Imposition PDF'}</span>
				</button>
				<p class="hint">Pre-arranged pages for double-sided printing &amp; folding.</p>
				<div class="guide-placeholder" data-guide-target></div>
			</div>
		`;
	}

	private renderOrderTab(): string {
		if (this._selectedProvider) {
			return this.renderProviderDetail();
		}

		return `
			<div class="section">
				<div class="pricing-tiers">
					<div class="tier-card">
						<div class="tier-qty">25+</div>
						<div class="tier-binding">Saddle stitch</div>
						<div class="tier-price">~$1.20/ea</div>
					</div>
					<div class="tier-card popular">
						<div class="tier-badge">popular</div>
						<div class="tier-qty">50+</div>
						<div class="tier-binding">Perfect bind</div>
						<div class="tier-price">~$0.85/ea</div>
					</div>
					<div class="tier-card">
						<div class="tier-qty">100+</div>
						<div class="tier-binding">Perfect bind</div>
						<div class="tier-price">~$0.60/ea</div>
					</div>
				</div>

				<button class="action-btn primary" data-action="find-printers" ${this._printersLoading ? 'disabled' : ''}>
					${this._printersLoading
						? '<span class="spinner"></span>'
						: FolkPubsPublishPanel.ICONS.location}
					<span>${this._printersLoading ? 'Searching...' : 'Find Nearby Printers'}</span>
				</button>
				${this._printersError ? `<div class="msg error">${this.esc(this._printersError)}</div>` : ''}
				${this._printers.length > 0 ? this.renderPrinterList() : ''}
			</div>
		`;
	}

	private renderPrinterList(): string {
		return `
			<div class="printer-list">
				${this._printers.map((p) => `
					<button class="printer-card" data-provider-id="${this.esc(p.id)}">
						<div class="printer-header">
							<span class="printer-name">${this.esc(p.name)}</span>
							<span class="printer-distance">${p.distance_km} km</span>
						</div>
						<div class="printer-meta">
							${this.esc(p.city)}
							${p.source === 'curated' ? `<span class="verified-badge">${FolkPubsPublishPanel.ICONS.verified} verified</span>` : ''}
						</div>
						${p.tags?.length ? `<div class="printer-tags">${p.tags.map((t: string) => `<span class="tag">${this.esc(t)}</span>`).join('')}</div>` : ''}
						${p.capabilities?.length ? `<div class="printer-caps">${p.capabilities.join(', ')}</div>` : ''}
					</button>
				`).join('')}
			</div>
		`;
	}

	private renderProviderDetail(): string {
		const p = this._selectedProvider;
		return `
			<div class="section">
				<button class="back-btn" data-action="back-to-list">&larr; Back to results</button>
				<div class="provider-detail">
					<div class="provider-card-header">
						${FolkPubsPublishPanel.ICONS.printer}
						<h4>${this.esc(p.name)}</h4>
					</div>
					<div class="provider-info-grid">
						${p.address ? `<div class="provider-info-row">${FolkPubsPublishPanel.ICONS.location} <span>${this.esc(p.address)}</span></div>` : ''}
						${p.phone ? `<div class="provider-info-row">${FolkPubsPublishPanel.ICONS.phone} <span>${this.esc(p.phone)}</span></div>` : ''}
						${p.website ? `<div class="provider-info-row">${FolkPubsPublishPanel.ICONS.globe} <a href="${this.esc(p.website)}" target="_blank" rel="noopener">${this.esc(p.website)}</a></div>` : ''}
						${p.email ? `<div class="provider-info-row">${FolkPubsPublishPanel.ICONS.mail} <span>${this.esc(p.email)}</span></div>` : ''}
					</div>
					${p.description ? `<div class="provider-desc">${this.esc(p.description)}</div>` : ''}

					<button class="action-btn primary" data-action="place-order">
						${FolkPubsPublishPanel.ICONS.printer}
						<span>Place Order</span>
					</button>
					<button class="action-btn secondary" data-action="join-batch">
						${FolkPubsPublishPanel.ICONS.users}
						<span>Join Group Buy</span>
					</button>

					${this._orderStatus ? `<div class="msg success">${FolkPubsPublishPanel.ICONS.check} ${this.esc(this._orderStatus)}</div>` : ''}
					${this._batchStatus ? this.renderBatchProgress() : ''}
				</div>
			</div>
		`;
	}

	private renderBatchProgress(): string {
		const b = this._batchStatus;
		if (b.action === 'error') return `<div class="msg error">${this.esc(b.error)}</div>`;

		const participants = b.participants || 1;
		const threshold = 25;
		const pct = Math.min(Math.round((participants / threshold) * 100), 100);

		return `
			<div class="batch-card">
				<div class="batch-header">
					${FolkPubsPublishPanel.ICONS.users}
					<span>Group Buy Progress</span>
				</div>
				<div class="batch-bar-wrap">
					<div class="batch-bar">
						<div class="batch-bar-fill" style="width:${pct}%"></div>
					</div>
					<div class="batch-bar-label">${participants} / ${threshold} participants</div>
				</div>
				<div class="batch-tiers">
					<span class="${participants >= 25 ? 'tier-unlocked' : 'tier-locked'}">25+ unlocked</span>
					<span class="${participants >= 50 ? 'tier-unlocked' : 'tier-locked'}">50+ ${participants >= 50 ? 'unlocked' : 'locked'}</span>
				</div>
			</div>
		`;
	}

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

		// Tab switching
		this.shadowRoot.querySelectorAll(".tab").forEach((btn) => {
			btn.addEventListener("click", () => {
				this._activeTab = (btn as HTMLElement).dataset.tab as any;
				this.render();
			});
		});

		// Share tab actions
		this.shadowRoot.querySelector('[data-action="copy-link"]')?.addEventListener("click", () => {
			const url = `${window.location.origin}${window.location.pathname}`;
			navigator.clipboard.writeText(url).then(() => {
				const btn = this.shadowRoot!.querySelector('[data-action="copy-link"]')!;
				const span = btn.querySelector('span');
				if (span) span.textContent = "Copied!";
				btn.innerHTML = `${FolkPubsPublishPanel.ICONS.check}<span>Copied!</span>`;
				setTimeout(() => {
					btn.innerHTML = `${FolkPubsPublishPanel.ICONS.copy}<span>Copy Flipbook Link</span>`;
				}, 2000);
			});
		});

		this.shadowRoot.querySelector('[data-action="email-pdf"]')?.addEventListener("click", () => {
			this.sendEmailPdf();
		});

		this.shadowRoot.querySelector('[data-action="download-epub-reflow"]')?.addEventListener("click", () => {
			this.downloadEpub("reflowable");
		});

		this.shadowRoot.querySelector('[data-action="download-epub-fixed"]')?.addEventListener("click", () => {
			this.downloadEpub("fixed");
		});

		this.shadowRoot.querySelector('[data-action="publish-to-space"]')?.addEventListener("click", () => {
			this.publishToSpace();
		});

		this.shadowRoot.querySelector('[data-action="toggle-include-fixed"]')?.addEventListener("change", (e) => {
			this._publishIncludeFixed = (e.target as HTMLInputElement).checked;
		});

		this.shadowRoot.querySelector('[data-action="copy-published-link"]')?.addEventListener("click", () => {
			if (!this._publishResult) return;
			navigator.clipboard.writeText(this._publishResult.hosted_url).then(() => {
				const btn = this.shadowRoot!.querySelector('[data-action="copy-published-link"] span');
				if (btn) {
					const orig = btn.textContent;
					btn.textContent = "Copied!";
					setTimeout(() => { btn.textContent = orig; }, 1500);
				}
			});
		});

		this.shadowRoot.querySelector('[data-action="publish-again"]')?.addEventListener("click", () => {
			this._publishResult = null;
			this._publishError = null;
			this.render();
		});

		// DIY tab
		this.shadowRoot.querySelector('[data-action="download-imposition"]')?.addEventListener("click", () => {
			this.downloadImposition();
		});

		// Load guide content
		const guideTarget = this.shadowRoot.querySelector('[data-guide-target]');
		if (guideTarget && this._activeTab === 'diy') {
			this.loadGuide(guideTarget as HTMLElement);
		}

		// Order tab
		this.shadowRoot.querySelector('[data-action="find-printers"]')?.addEventListener("click", () => {
			this.findPrinters();
		});

		this.shadowRoot.querySelectorAll(".printer-card").forEach((card) => {
			card.addEventListener("click", () => {
				const id = (card as HTMLElement).dataset.providerId;
				this._selectedProvider = this._printers.find((p) => p.id === id) || null;
				this.render();
			});
		});

		this.shadowRoot.querySelector('[data-action="back-to-list"]')?.addEventListener("click", () => {
			this._selectedProvider = null;
			this._orderStatus = null;
			this._batchStatus = null;
			this.render();
		});

		this.shadowRoot.querySelector('[data-action="place-order"]')?.addEventListener("click", () => {
			this.placeOrder();
		});

		this.shadowRoot.querySelector('[data-action="join-batch"]')?.addEventListener("click", () => {
			this.joinBatch();
		});
	}

	private async sendEmailPdf() {
		const input = this.shadowRoot?.querySelector(".email-input") as HTMLInputElement;
		const email = input?.value?.trim();
		if (!email || !email.includes("@")) {
			this._emailError = "Enter a valid email address";
			this.render();
			return;
		}

		this._emailSending = true;
		this._emailError = null;
		this._emailSent = false;
		this.render();

		try {
			const res = await fetch(`${getModuleApiBase("rpubs")}/api/email-pdf`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({
					email,
					format: this._formatId,
					// Content will be re-read from the editor via a custom event
					...this.getEditorContent(),
				}),
			});

			if (!res.ok) {
				const err = await res.json();
				throw new Error(err.error || "Failed to send email");
			}

			this._emailSent = true;
		} catch (e: any) {
			this._emailError = e.message;
		} finally {
			this._emailSending = false;
			this.render();
		}
	}

	private async publishToSpace() {
		this._publishing = true;
		this._publishError = null;
		this.render();

		try {
			const res = await fetch(`${getModuleApiBase("rpubs")}/api/publish`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({
					format: this._formatId,
					include_fixed_epub: this._publishIncludeFixed,
					...this.getEditorContent(),
				}),
			});

			if (!res.ok) {
				const err = await res.json().catch(() => ({}));
				throw new Error((err as any).error || "Publish failed");
			}

			const data = await res.json();
			this._publishResult = {
				hosted_url: data.hosted_url,
				pdf_url: data.pdf_url,
				epub_url: data.epub_url,
				epub_fixed_url: data.epub_fixed_url,
			};
		} catch (e: any) {
			this._publishError = e.message;
			console.error("[rpubs] Publish error:", e);
		} finally {
			this._publishing = false;
			this.render();
		}
	}

	private async downloadEpub(mode: "reflowable" | "fixed") {
		this._epubError = null;
		if (mode === "reflowable") this._epubReflowLoading = true;
		else this._epubFixedLoading = true;
		this.render();

		try {
			const res = await fetch(`${getModuleApiBase("rpubs")}/api/generate-epub`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({
					format: this._formatId,
					mode,
					...this.getEditorContent(),
				}),
			});

			if (!res.ok) {
				const err = await res.json().catch(() => ({}));
				throw new Error((err as any).error || "EPUB generation failed");
			}

			const blob = await res.blob();
			const url = URL.createObjectURL(blob);
			const a = document.createElement("a");
			a.href = url;
			const suffix = mode === "fixed" ? `-${this._formatId}-fixed` : "";
			a.download = `publication${suffix}.epub`;
			a.click();
			URL.revokeObjectURL(url);
		} catch (e: any) {
			this._epubError = e.message;
			console.error("[rpubs] EPUB error:", e);
		} finally {
			if (mode === "reflowable") this._epubReflowLoading = false;
			else this._epubFixedLoading = false;
			this.render();
		}
	}

	private async downloadImposition() {
		this._impositionLoading = true;
		this.render();

		try {
			const res = await fetch(`${getModuleApiBase("rpubs")}/api/imposition`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({
					format: this._formatId,
					...this.getEditorContent(),
				}),
			});

			if (!res.ok) {
				const err = await res.json();
				throw new Error(err.error || "Imposition generation failed");
			}

			const blob = await res.blob();
			const url = URL.createObjectURL(blob);
			const a = document.createElement("a");
			a.href = url;
			a.download = `imposition-${this._formatId}.pdf`;
			a.click();
			URL.revokeObjectURL(url);
		} catch (e: any) {
			console.error("[rpubs] Imposition error:", e);
		} finally {
			this._impositionLoading = false;
			this.render();
		}
	}

	private async loadGuide(target: HTMLElement) {
		if (!this._formatId) return;

		// Dynamically import the guide
		const { getGuide, recommendedBinding, paddedPageCount } = await import("../print-guides");
		const guide = getGuide(this._formatId);
		if (!guide) { target.innerHTML = '<p class="hint">No guide available for this format.</p>'; return; }

		const binding = recommendedBinding(this._formatId, this._pageCount);
		const padded = paddedPageCount(this._pageCount);
		const sheets = Math.ceil(padded / guide.pagesPerSheet);

		target.innerHTML = `
			<div class="guide">
				<div class="guide-card">
					<div class="guide-stats">
						<div class="guide-stat-col">
							<span class="guide-stat-value">${sheets}</span>
							<span class="guide-stat-label">Sheets</span>
						</div>
						<div class="guide-stat-col">
							<span class="guide-stat-value">${guide.parentSheet}</span>
							<span class="guide-stat-label">Paper</span>
						</div>
						<div class="guide-stat-col">
							<span class="guide-stat-value">${binding}</span>
							<span class="guide-stat-label">Binding</span>
						</div>
					</div>

					<div class="guide-info-banner">
						${FolkPubsPublishPanel.ICONS.info}
						<span>${guide.paperRecommendation}</span>
					</div>

					<div class="guide-section">
						<div class="guide-section-title">Tools needed</div>
						<ul class="guide-list">${guide.tools.map((t: string) => `<li>${t}</li>`).join('')}</ul>
					</div>

					<div class="guide-section">
						<div class="guide-section-title">Folding</div>
						<div class="guide-numbered-steps">
							${guide.foldInstructions.map((s: string, i: number) => `
								<div class="guide-step">
									<span class="guide-step-num">${i + 1}</span>
									<span>${s}</span>
								</div>
							`).join('')}
						</div>
					</div>

					<div class="guide-section">
						<div class="guide-section-title">Binding</div>
						<div class="guide-numbered-steps">
							${guide.bindingInstructions.filter((s: string) => s).map((s: string, i: number) => `
								<div class="guide-step">
									<span class="guide-step-num">${i + 1}</span>
									<span>${s.replace(/^\s+/, '')}</span>
								</div>
							`).join('')}
						</div>
					</div>

					<div class="guide-section">
						<div class="guide-section-title">Tips</div>
						<ul class="guide-tips">${guide.tips.map((t: string) => `<li>\u2605 ${t}</li>`).join('')}</ul>
					</div>
				</div>
			</div>
		`;
	}

	private async findPrinters() {
		this._printersLoading = true;
		this._printersError = null;
		this.render();

		try {
			const pos = await new Promise<GeolocationPosition>((resolve, reject) => {
				navigator.geolocation.getCurrentPosition(resolve, reject, { timeout: 10000 });
			});

			const { latitude: lat, longitude: lng } = pos.coords;
			const res = await fetch(
				`${getModuleApiBase("rpubs")}/api/printers?lat=${lat}&lng=${lng}&radius=100&format=${this._formatId}`,
			);

			if (!res.ok) throw new Error("Failed to search printers");
			const data = await res.json();
			this._printers = data.providers || [];
		} catch (e: any) {
			if (e.code === 1) {
				this._printersError = "Location access denied. Enable location to find nearby printers.";
			} else {
				this._printersError = e.message || "Search failed";
			}
		} finally {
			this._printersLoading = false;
			this.render();
		}
	}

	private async placeOrder() {
		if (!this._selectedProvider) return;
		try {
			const res = await fetch(`${getModuleApiBase("rpubs")}/api/order`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({
					provider_id: this._selectedProvider.id,
					provider_name: this._selectedProvider.name,
					provider_distance_km: this._selectedProvider.distance_km,
					total_price: 0,
					currency: "USD",
					format: this._formatId,
					...this.getEditorContent(),
				}),
			});

			if (!res.ok) {
				const err = await res.json();
				throw new Error(err.error || "Order failed");
			}

			const order = await res.json();
			this._orderStatus = `Order created: ${order.id || 'confirmed'}`;
			this.render();
		} catch (e: any) {
			this._orderStatus = `Error: ${e.message}`;
			this.render();
		}
	}

	private async joinBatch() {
		if (!this._selectedProvider) return;
		try {
			const res = await fetch(`${getModuleApiBase("rpubs")}/api/batch`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({
					provider_id: this._selectedProvider.id,
					provider_name: this._selectedProvider.name,
					format: this._formatId,
					...this.getEditorContent(),
				}),
			});

			if (!res.ok) {
				const err = await res.json();
				throw new Error(err.error || "Batch operation failed");
			}

			this._batchStatus = await res.json();
			this.render();
		} catch (e: any) {
			this._batchStatus = { action: "error", error: e.message };
			this.render();
		}
	}

	/** Get content from the parent editor — uses cached values since textarea may not be in DOM during publish step */
	private getEditorContent(): { content: string; title?: string; author?: string } {
		const editor = this.closest("folk-pubs-editor") || document.querySelector("folk-pubs-editor");
		if (!editor) return { content: "" };

		// Try cached content first (always available after PDF generation)
		const cached = (editor as any).cachedContent;
		if (cached?.content) {
			return {
				content: cached.content,
				title: cached.title || undefined,
				author: cached.author || undefined,
			};
		}

		// Fallback: try reading from shadow DOM (only works during write step)
		if (!editor.shadowRoot) return { content: "" };
		const textarea = editor.shadowRoot.querySelector(".content-area") as HTMLTextAreaElement;
		const titleInput = editor.shadowRoot.querySelector(".title-input") as HTMLInputElement;
		const authorInput = editor.shadowRoot.querySelector(".author-input") as HTMLInputElement;

		return {
			content: textarea?.value || "",
			title: titleInput?.value?.trim() || undefined,
			author: authorInput?.value?.trim() || undefined,
		};
	}

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

	private getStyles(): string {
		return `<style>
			:host { display: block; max-width: 32rem; margin: 0 auto; }

			.panel {
				display: flex;
				flex-direction: column;
				background: var(--rs-bg-surface, #1e1e2e);
				border: 1px solid var(--rs-border, #333);
				border-radius: 0.75rem;
				box-shadow: var(--rs-shadow-md, 0 4px 16px rgba(0,0,0,0.25));
				overflow: hidden;
			}

			/* ── Tabs ── */

			.tabs {
				display: flex;
				gap: 0;
				border-bottom: 1px solid var(--rs-border-subtle, #333);
				background: var(--rs-bg-page, #181825);
			}
			.tab {
				flex: 1;
				padding: 0.625rem 0.5rem;
				border: none;
				border-bottom: 2px solid transparent;
				background: transparent;
				color: var(--rs-text-secondary, #aaa);
				font-size: 0.78rem;
				font-weight: 500;
				cursor: pointer;
				transition: all 0.15s;
				display: inline-flex;
				align-items: center;
				justify-content: center;
				gap: 0.375rem;
			}
			.tab:hover { color: var(--rs-text-primary, #eee); }
			.tab.active {
				color: var(--rs-accent, #14b8a6);
				border-bottom-color: var(--rs-accent, #14b8a6);
			}
			.tab svg { opacity: 0.7; }
			.tab.active svg { opacity: 1; }

			/* ── Tab Content ── */

			.tab-content { padding: 1rem; }
			.section { display: flex; flex-direction: column; gap: 0.75rem; }

			/* ── Info Card ── */

			.info-card {
				display: flex;
				align-items: center;
				gap: 0.625rem;
				padding: 0.625rem 0.75rem;
				background: var(--rs-card-bg, #232336);
				border: 1px solid var(--rs-card-border, #2a2a3e);
				border-radius: 0.5rem;
			}
			.info-card svg { color: var(--rs-accent, #14b8a6); flex-shrink: 0; }
			.info-card-text { display: flex; flex-direction: column; gap: 0.125rem; }
			.info-card-title { font-size: 0.8rem; font-weight: 600; color: var(--rs-text-primary, #eee); }
			.info-card-detail { font-size: 0.7rem; color: var(--rs-text-muted, #666); }

			/* ── Section Header ── */

			.section-header {
				display: flex;
				align-items: center;
				gap: 0.625rem;
				margin-bottom: 0.25rem;
			}
			.section-header svg { color: var(--rs-accent, #14b8a6); flex-shrink: 0; }
			.section-title { font-size: 0.85rem; font-weight: 600; color: var(--rs-text-primary, #eee); }
			.section-subtitle { font-size: 0.7rem; color: var(--rs-text-muted, #666); }

			/* ── Action Buttons ── */

			.action-btn {
				display: inline-flex;
				align-items: center;
				justify-content: center;
				gap: 0.375rem;
				width: 100%;
				text-align: center;
				padding: 0.5rem 0.75rem;
				border-radius: 0.5rem;
				border: 1px solid var(--rs-border, #444);
				background: var(--rs-bg-surface, #2a2a2a);
				color: var(--rs-text-primary, #eee);
				font-size: 0.8rem;
				cursor: pointer;
				text-decoration: none;
				transition: all 0.15s;
			}
			.action-btn:hover { border-color: var(--rs-accent, #14b8a6); }
			.action-btn:disabled { opacity: 0.5; cursor: not-allowed; }
			.action-btn svg { flex-shrink: 0; }

			.action-btn.primary {
				background: var(--rs-accent, #14b8a6);
				border-color: var(--rs-accent, #14b8a6);
				color: #fff;
				font-weight: 600;
			}
			.action-btn.primary:hover { background: var(--rs-accent-hover, #0d9488); }

			.action-btn.secondary {
				background: transparent;
				border: 1px solid var(--rs-border, #444);
			}
			.action-btn.secondary:hover { border-color: var(--rs-accent, #14b8a6); color: var(--rs-accent, #14b8a6); }

			.action-btn.accent {
				background: var(--rs-accent, #14b8a6);
				border-color: var(--rs-accent, #14b8a6);
				color: #fff;
				font-weight: 600;
			}
			.action-btn.accent:hover { background: var(--rs-accent-hover, #0d9488); }

			.send-btn { width: auto; flex-shrink: 0; padding: 0.5rem 0.875rem; }

			.epub-row {
				display: grid;
				grid-template-columns: 1fr 1fr;
				gap: 0.5rem;
			}
			.epub-row .epub-btn { font-size: 0.8125rem; padding: 0.5rem 0.5rem; }

			.publish-btn { font-size: 0.95rem; padding: 0.7rem 1rem; }
			.publish-option {
				display: flex; align-items: flex-start; gap: 0.5rem;
				margin: 0.5rem 0 0; font-size: 0.8125rem;
				color: var(--rs-text-muted, #94a3b8); cursor: pointer;
			}
			.publish-option input { margin-top: 0.15rem; flex-shrink: 0; }

			.publish-success {
				background: var(--rs-surface-alt, #0f172a);
				border: 1px solid color-mix(in srgb, var(--rs-accent, #14b8a6) 30%, transparent);
				border-radius: 8px;
				padding: 0.75rem;
				display: flex; flex-direction: column; gap: 0.5rem;
			}
			.publish-success-head {
				display: flex; align-items: center; gap: 0.4rem;
				font-weight: 600; font-size: 0.875rem;
				color: var(--rs-accent, #14b8a6);
			}
			.publish-url-row { display: flex; gap: 0.4rem; align-items: center; }
			.publish-url {
				flex: 1; min-width: 0; padding: 0.4rem 0.5rem;
				background: var(--rs-bg-page, #020617);
				color: inherit; border: 1px solid var(--rs-border, #334155);
				border-radius: 4px; font-family: inherit; font-size: 0.78rem;
			}
			.publish-copy-btn { width: auto; flex-shrink: 0; padding: 0.4rem 0.6rem; font-size: 0.78rem; }
			.publish-actions-row {
				display: flex; justify-content: space-between; align-items: center;
				font-size: 0.8125rem;
			}
			.publish-link, .publish-link-btn {
				background: none; border: none; color: var(--rs-accent, #14b8a6);
				font: inherit; padding: 0; cursor: pointer; text-decoration: underline;
			}
			.publish-link:hover, .publish-link-btn:hover { opacity: 0.8; }

			/* ── Spinner ── */

			.spinner {
				display: inline-block;
				width: 14px; height: 14px;
				border: 2px solid rgba(255,255,255,0.3);
				border-top-color: #fff;
				border-radius: 50%;
				animation: spin 0.6s linear infinite;
			}
			@keyframes spin { to { transform: rotate(360deg); } }

			/* ── Divider ── */

			.divider-row {
				display: flex;
				align-items: center;
				gap: 0.625rem;
				margin: 0.25rem 0;
			}
			.divider-line { flex: 1; height: 1px; background: var(--rs-border-subtle, #333); }
			.divider-text { font-size: 0.7rem; color: var(--rs-text-muted, #666); white-space: nowrap; }

			/* ── Email ── */

			.email-row { display: flex; gap: 0.375rem; }
			.email-input-wrap {
				flex: 1;
				display: flex;
				align-items: center;
				gap: 0.375rem;
				padding: 0 0.5rem;
				border: 1px solid var(--rs-input-border, #444);
				border-radius: 0.5rem;
				background: var(--rs-input-bg, #1a1a2e);
				transition: border-color 0.15s;
			}
			.email-input-wrap:focus-within { border-color: var(--rs-accent, #14b8a6); }
			.email-input-wrap svg { color: var(--rs-text-muted, #666); flex-shrink: 0; }
			.email-input {
				flex: 1;
				padding: 0.5rem 0;
				border: none;
				background: transparent;
				color: var(--rs-input-text, #eee);
				font-size: 0.8rem;
				outline: none;
			}
			.email-input::placeholder { color: var(--rs-text-muted, #666); }

			/* ── Messages ── */

			.msg {
				font-size: 0.75rem;
				padding: 0.5rem 0.625rem;
				border-radius: 0.5rem;
				display: inline-flex;
				align-items: center;
				gap: 0.375rem;
			}
			.msg.success {
				background: rgba(20, 184, 166, 0.1);
				border: 1px solid rgba(20, 184, 166, 0.2);
				color: var(--rs-accent, #14b8a6);
			}
			.msg.error {
				background: rgba(248, 113, 113, 0.1);
				border: 1px solid rgba(248, 113, 113, 0.2);
				color: #f87171;
			}
			.msg.info {
				background: rgba(20, 184, 166, 0.08);
				border: 1px solid rgba(20, 184, 166, 0.15);
				color: var(--rs-accent, #14b8a6);
			}

			.hint { font-size: 0.7rem; color: var(--rs-text-muted, #666); margin: 0; }

			/* ── DIY Guide ── */

			.guide-card {
				background: var(--rs-card-bg, #232336);
				border: 1px solid var(--rs-card-border, #2a2a3e);
				border-radius: 0.5rem;
				padding: 0.875rem;
				display: flex;
				flex-direction: column;
				gap: 0.75rem;
			}

			.guide-stats {
				display: grid;
				grid-template-columns: 1fr 1fr 1fr;
				gap: 0.5rem;
				text-align: center;
			}
			.guide-stat-col { display: flex; flex-direction: column; gap: 0.125rem; }
			.guide-stat-value { font-size: 0.85rem; font-weight: 700; color: var(--rs-accent, #14b8a6); }
			.guide-stat-label { font-size: 0.65rem; color: var(--rs-text-muted, #666); text-transform: uppercase; letter-spacing: 0.04em; }

			.guide-info-banner {
				display: flex;
				align-items: flex-start;
				gap: 0.5rem;
				padding: 0.5rem 0.625rem;
				background: rgba(20, 184, 166, 0.08);
				border: 1px solid rgba(20, 184, 166, 0.15);
				border-radius: 0.375rem;
				font-size: 0.72rem;
				color: var(--rs-text-secondary, #aaa);
				line-height: 1.4;
			}
			.guide-info-banner svg { color: var(--rs-accent, #14b8a6); flex-shrink: 0; margin-top: 0.05rem; }

			.guide-section { display: flex; flex-direction: column; gap: 0.375rem; }
			.guide-section-title {
				font-size: 0.72rem;
				font-weight: 600;
				color: var(--rs-text-secondary, #aaa);
				text-transform: uppercase;
				letter-spacing: 0.04em;
			}

			.guide-list {
				margin: 0; padding-left: 1.25rem;
				font-size: 0.75rem; color: var(--rs-text-primary, #ddd); line-height: 1.5;
			}
			.guide-list li { margin-bottom: 0.2rem; }

			.guide-numbered-steps { display: flex; flex-direction: column; gap: 0.375rem; }
			.guide-step {
				display: flex;
				align-items: flex-start;
				gap: 0.5rem;
				font-size: 0.75rem;
				color: var(--rs-text-primary, #ddd);
				line-height: 1.4;
			}
			.guide-step-num {
				display: inline-flex;
				align-items: center;
				justify-content: center;
				width: 20px; height: 20px;
				border-radius: 50%;
				background: var(--rs-accent, #14b8a6);
				color: #fff;
				font-size: 0.65rem;
				font-weight: 700;
				flex-shrink: 0;
				margin-top: 0.05rem;
			}

			.guide-tips {
				margin: 0; padding: 0; list-style: none;
				font-size: 0.75rem; color: var(--rs-text-primary, #ddd); line-height: 1.5;
			}
			.guide-tips li { margin-bottom: 0.2rem; }

			/* ── Pricing Tiers ── */

			.pricing-tiers {
				display: grid;
				grid-template-columns: 1fr 1fr 1fr;
				gap: 0.5rem;
				margin-bottom: 0.25rem;
			}
			.tier-card {
				display: flex;
				flex-direction: column;
				align-items: center;
				gap: 0.125rem;
				padding: 0.625rem 0.5rem;
				border: 1px solid var(--rs-card-border, #2a2a3e);
				border-radius: 0.5rem;
				background: var(--rs-card-bg, #232336);
				text-align: center;
				position: relative;
			}
			.tier-card.popular {
				border-color: var(--rs-accent, #14b8a6);
				background: rgba(20, 184, 166, 0.06);
			}
			.tier-badge {
				position: absolute;
				top: -8px;
				font-size: 0.55rem;
				font-weight: 600;
				text-transform: uppercase;
				letter-spacing: 0.05em;
				padding: 0.1rem 0.5rem;
				border-radius: 1rem;
				background: var(--rs-accent, #14b8a6);
				color: #fff;
			}
			.tier-qty { font-size: 0.9rem; font-weight: 700; color: var(--rs-accent, #14b8a6); }
			.tier-binding { font-size: 0.65rem; color: var(--rs-text-muted, #666); }
			.tier-price { font-size: 0.75rem; font-weight: 600; color: var(--rs-text-primary, #eee); }

			/* ── Printer List ── */

			.printer-list { display: flex; flex-direction: column; gap: 0.375rem; max-height: 300px; overflow-y: auto; }
			.printer-card {
				text-align: left;
				padding: 0.625rem 0.75rem;
				border: 1px solid var(--rs-card-border, #2a2a3e);
				border-radius: 0.5rem;
				background: var(--rs-card-bg, #232336);
				color: var(--rs-text-primary, #eee);
				cursor: pointer;
				transition: all 0.15s;
			}
			.printer-card:hover {
				border-color: var(--rs-accent, #14b8a6);
				background: rgba(20, 184, 166, 0.04);
			}
			.printer-header {
				display: flex;
				justify-content: space-between;
				align-items: center;
			}
			.printer-name { font-size: 0.8rem; font-weight: 600; }
			.printer-distance { font-size: 0.7rem; color: var(--rs-text-muted, #666); }
			.printer-meta {
				font-size: 0.7rem;
				color: var(--rs-text-secondary, #aaa);
				display: flex;
				align-items: center;
				gap: 0.375rem;
				margin-top: 0.125rem;
			}
			.verified-badge {
				display: inline-flex;
				align-items: center;
				gap: 0.2rem;
				font-size: 0.6rem;
				color: var(--rs-accent, #14b8a6);
			}
			.printer-tags { display: flex; gap: 0.25rem; flex-wrap: wrap; margin-top: 0.375rem; }
			.tag {
				font-size: 0.6rem;
				padding: 0.15rem 0.5rem;
				border-radius: 1rem;
				background: rgba(20, 184, 166, 0.1);
				color: var(--rs-accent, #14b8a6);
			}
			.printer-caps { font-size: 0.65rem; color: var(--rs-text-muted, #666); margin-top: 0.25rem; }

			/* ── Provider Detail ── */

			.back-btn {
				background: none; border: none;
				color: var(--rs-text-secondary, #aaa);
				font-size: 0.75rem; cursor: pointer;
				padding: 0; text-align: left;
			}
			.back-btn:hover { color: var(--rs-text-primary, #eee); }

			.provider-detail {
				display: flex;
				flex-direction: column;
				gap: 0.625rem;
			}
			.provider-card-header {
				display: flex;
				align-items: center;
				gap: 0.5rem;
			}
			.provider-card-header svg { color: var(--rs-accent, #14b8a6); }
			.provider-detail h4 { margin: 0; font-size: 0.9rem; }

			.provider-info-grid {
				display: flex;
				flex-direction: column;
				gap: 0.375rem;
				padding: 0.625rem 0.75rem;
				background: var(--rs-card-bg, #232336);
				border: 1px solid var(--rs-card-border, #2a2a3e);
				border-radius: 0.5rem;
			}
			.provider-info-row {
				display: flex;
				align-items: center;
				gap: 0.5rem;
				font-size: 0.75rem;
				color: var(--rs-text-secondary, #aaa);
			}
			.provider-info-row svg { color: var(--rs-text-muted, #666); flex-shrink: 0; }
			.provider-info-row a { color: var(--rs-accent, #14b8a6); text-decoration: none; }
			.provider-info-row a:hover { text-decoration: underline; }

			.provider-desc { font-size: 0.72rem; color: var(--rs-text-muted, #666); line-height: 1.4; }

			/* ── Batch / Group Buy ── */

			.batch-card {
				display: flex;
				flex-direction: column;
				gap: 0.5rem;
				padding: 0.75rem;
				background: var(--rs-card-bg, #232336);
				border: 1px solid var(--rs-card-border, #2a2a3e);
				border-radius: 0.5rem;
			}
			.batch-header {
				display: flex;
				align-items: center;
				gap: 0.375rem;
				font-size: 0.78rem;
				font-weight: 600;
				color: var(--rs-text-primary, #eee);
			}
			.batch-header svg { color: var(--rs-accent, #14b8a6); }
			.batch-bar-wrap { display: flex; flex-direction: column; gap: 0.25rem; }
			.batch-bar {
				height: 6px;
				border-radius: 3px;
				background: var(--rs-border-subtle, #333);
				overflow: hidden;
			}
			.batch-bar-fill {
				height: 100%;
				border-radius: 3px;
				background: linear-gradient(90deg, var(--rs-accent, #14b8a6), var(--rs-accent-hover, #0d9488));
				transition: width 0.4s ease;
			}
			.batch-bar-label { font-size: 0.68rem; color: var(--rs-text-muted, #666); }
			.batch-tiers {
				display: flex;
				gap: 0.75rem;
				font-size: 0.68rem;
			}
			.tier-unlocked { color: var(--rs-accent, #14b8a6); font-weight: 500; }
			.tier-locked { color: var(--rs-text-muted, #666); }

			/* ── Responsive ── */

			@media (max-width: 480px) {
				.tab-content { padding: 0.75rem; }
				.pricing-tiers { grid-template-columns: 1fr; }
				.guide-stats { grid-template-columns: 1fr; }
			}
		</style>`;
	}
}

customElements.define("folk-pubs-publish-panel", FolkPubsPublishPanel);
