/**
 * <folk-notes-app> — notebook and note management.
 *
 * Browse notebooks, create/edit notes with rich text (Tiptap),
 * search, tag management.
 *
 * Notebook list: REST (GET /api/notebooks)
 * Notebook detail + notes: Automerge sync via WebSocket
 * Search: REST (GET /api/notes?q=...)
 */

import * as Automerge from '@automerge/automerge';
import { notebookSchema } from '../schemas';
import type { DocumentId } from '../../../shared/local-first/document';
import { getAccessToken } from '../../../shared/components/rstack-identity';
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import Link from '@tiptap/extension-link';
import Image from '@tiptap/extension-image';
import TaskList from '@tiptap/extension-task-list';
import TaskItem from '@tiptap/extension-task-item';
import Placeholder from '@tiptap/extension-placeholder';
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
import Typography from '@tiptap/extension-typography';
import Underline from '@tiptap/extension-underline';
import { common, createLowlight } from 'lowlight';
import { createSlashCommandPlugin } from './slash-command';
import type { ImportExportDialog } from './import-export-dialog';
import { SpeechDictation } from '../../../lib/speech-dictation';
import { TourEngine } from '../../../shared/tour-engine';
import { ViewHistory } from '../../../shared/view-history.js';

const lowlight = createLowlight(common);

/** Inline SVG icons for toolbar buttons (16×16, stroke-based, currentColor) */
const ICONS: Record<string, string> = {
	bold: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 2.5h5a2.5 2.5 0 0 1 0 5H4zM4 7.5h5.5a2.5 2.5 0 0 1 0 5H4z"/></svg>',
	italic: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><line x1="10" y1="2.5" x2="6" y2="13.5"/><line x1="6.5" y1="2.5" x2="11.5" y2="2.5"/><line x1="4.5" y1="13.5" x2="9.5" y2="13.5"/></svg>',
	underline: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 2.5v5a4 4 0 0 0 8 0v-5"/><line x1="3" y1="14" x2="13" y2="14"/></svg>',
	strike: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4.5c-.5-1-1.8-2-3.5-2C5.5 2.5 4 3.8 4 5.3c0 1 .5 1.7 1.5 2.2"/><line x1="3" y1="8" x2="13" y2="8"/><path d="M5 11c.5 1.2 1.8 2.5 3.5 2.5 2 0 3.5-1.3 3.5-2.8 0-.7-.3-1.3-.8-1.7"/></svg>',
	code: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="5.5 4 2 8 5.5 12"/><polyline points="10.5 4 14 8 10.5 12"/></svg>',
	bulletList: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="6" y1="4" x2="14" y2="4"/><line x1="6" y1="8" x2="14" y2="8"/><line x1="6" y1="12" x2="14" y2="12"/><circle cx="3" cy="4" r="1" fill="currentColor" stroke="none"/><circle cx="3" cy="8" r="1" fill="currentColor" stroke="none"/><circle cx="3" cy="12" r="1" fill="currentColor" stroke="none"/></svg>',
	orderedList: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="7" y1="4" x2="14" y2="4"/><line x1="7" y1="8" x2="14" y2="8"/><line x1="7" y1="12" x2="14" y2="12"/><text x="1.5" y="5.5" font-size="5" fill="currentColor" stroke="none" font-family="system-ui" font-weight="600">1</text><text x="1.5" y="9.5" font-size="5" fill="currentColor" stroke="none" font-family="system-ui" font-weight="600">2</text><text x="1.5" y="13.5" font-size="5" fill="currentColor" stroke="none" font-family="system-ui" font-weight="600">3</text></svg>',
	taskList: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="5" height="5" rx="1"/><polyline points="3.5 4.5 4.5 5.5 6 3.5"/><line x1="9" y1="4.5" x2="14" y2="4.5"/><rect x="2" y="9" width="5" height="5" rx="1"/><line x1="9" y1="11.5" x2="14" y2="11.5"/></svg>',
	blockquote: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="2" x2="3" y2="14"/><line x1="7" y1="4" x2="14" y2="4"/><line x1="7" y1="8" x2="14" y2="8"/><line x1="7" y1="12" x2="12" y2="12"/></svg>',
	codeBlock: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1.5" y="1.5" width="13" height="13" rx="2"/><polyline points="5 6 3.5 8 5 10"/><polyline points="11 6 12.5 8 11 10"/><line x1="9" y1="5" x2="7" y2="11"/></svg>',
	horizontalRule: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="2" y1="8" x2="14" y2="8"/><circle cx="4" cy="8" r="0.5" fill="currentColor"/><circle cx="8" cy="8" r="0.5" fill="currentColor"/><circle cx="12" cy="8" r="0.5" fill="currentColor"/></svg>',
	link: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6.5 9.5a3 3 0 0 0 4.2.3l2-2a3 3 0 0 0-4.2-4.3L7 4.8"/><path d="M9.5 6.5a3 3 0 0 0-4.2-.3l-2 2a3 3 0 0 0 4.2 4.3L9 11.2"/></svg>',
	image: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1.5" y="2.5" width="13" height="11" rx="2"/><circle cx="5.5" cy="6" r="1.5"/><path d="M14.5 10.5l-3.5-3.5-5 5"/></svg>',
	undo: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 6 2 8 4 10"/><path d="M2 8h8a4 4 0 0 1 0 8H8"/></svg>',
	redo: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="12 6 14 8 12 10"/><path d="M14 8H6a4 4 0 0 0 0 8h2"/></svg>',
	mic: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="1" width="6" height="9" rx="3"/><path d="M3 7v1a5 5 0 0 0 10 0V7"/><line x1="8" y1="13" x2="8" y2="15"/><line x1="5.5" y1="15" x2="10.5" y2="15"/></svg>',
	summarize: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 1l1.5 3 3.5.5-2.5 2.5.5 3.5L8 9l-3 1.5.5-3.5L3 4.5l3.5-.5z"/><line x1="4" y1="13" x2="12" y2="13"/><line x1="5" y1="15" x2="11" y2="15"/></svg>',
};

interface Notebook {
	id: string;
	title: string;
	description: string;
	cover_color: string;
	note_count: string;
	updated_at: string;
}

interface Note {
	id: string;
	title: string;
	content: string;
	content_plain: string;
	content_format?: 'html' | 'tiptap-json';
	type: string;
	tags: string[] | null;
	is_pinned: boolean;
	url?: string | null;
	language?: string | null;
	fileUrl?: string | null;
	mimeType?: string | null;
	duration?: number | null;
	created_at: string;
	updated_at: string;
}

type NoteType = 'NOTE' | 'CODE' | 'BOOKMARK' | 'CLIP' | 'IMAGE' | 'AUDIO' | 'FILE';

interface CreateNoteOpts {
	type?: NoteType;
	title?: string;
	url?: string;
	fileUrl?: string;
	mimeType?: string;
	duration?: number;
	language?: string;
	content?: string;
	tags?: string[];
}

/** Shape of Automerge notebook doc (matches PG→Automerge migration) */
interface NotebookDoc {
	meta: { module: string; collection: string; version: number; spaceSlug: string; createdAt: number };
	notebook: {
		id: string; title: string; slug: string; description: string;
		coverColor: string; isPublic: boolean; createdAt: number; updatedAt: number;
	};
	items: Record<string, {
		id: string; notebookId: string; title: string; content: string;
		contentPlain: string; contentFormat?: string; type: string; tags: string[]; isPinned: boolean;
		sortOrder: number; createdAt: number; updatedAt: number;
		url?: string | null; language?: string | null; fileUrl?: string | null;
		mimeType?: string | null; duration?: number | null;
		summary?: string; summaryModel?: string; openNotebookSourceId?: string;
	}>;
}

class FolkNotesApp extends HTMLElement {
	private shadow!: ShadowRoot;
	private space = "";
	private view: "notebooks" | "notebook" | "note" = "notebooks";
	private notebooks: Notebook[] = [];
	private selectedNotebook: (Notebook & { notes: Note[] }) | null = null;
	private selectedNote: Note | null = null;
	private searchQuery = "";
	private searchResults: Note[] = [];
	private typeFilter: NoteType | '' = '';
	private loading = false;
	private error = "";

	// Zone-based rendering
	private navZone!: HTMLDivElement;
	private contentZone!: HTMLDivElement;
	private metaZone!: HTMLDivElement;

	// Navigation history
	private _history = new ViewHistory<"notebooks" | "notebook" | "note">("notebooks");

	// Guided tour
	private _tour!: TourEngine;
	private static readonly TOUR_STEPS = [
		{ target: '#create-notebook', title: "Create a Notebook", message: "Notebooks organise your notes by topic. Click '+ New Notebook' to create one.", advanceOnClick: true },
		{ target: '#create-note', title: "Create a Note", message: "Inside a notebook you can add notes, links, tasks, and more. Click '+ New Note' to add one.", advanceOnClick: true },
		{ target: '#editor-toolbar', title: "Editor Toolbar", message: "Format text with the toolbar — bold, lists, code blocks, headings, and more. Click Next to continue.", advanceOnClick: false },
		{ target: '[data-cmd="mic"]', title: "Voice Notes", message: "Record voice notes with live transcription. Your words appear as you speak — no uploads needed.", advanceOnClick: false },
	];

	// Tiptap editor
	private editor: Editor | null = null;
	private editorNoteId: string | null = null;
	private isRemoteUpdate = false;
	private editorUpdateTimer: ReturnType<typeof setTimeout> | null = null;
	private dictation: SpeechDictation | null = null;

	// Audio recording (AUDIO note view)
	private audioRecorder: MediaRecorder | null = null;
	private audioSegments: { id: string; text: string; timestamp: number; isFinal: boolean }[] = [];
	private audioRecordingStart = 0;
	private audioRecordingTimer: ReturnType<typeof setInterval> | null = null;
	private audioRecordingDictation: SpeechDictation | null = null;

	// Automerge sync state (via shared runtime)
	private doc: Automerge.Doc<NotebookDoc> | null = null;
	private subscribedDocId: string | null = null;
	private syncConnected = false;
	private _offlineUnsub: (() => void) | null = null;
	private _offlineNotebookUnsubs: (() => void)[] = [];

	// ── Demo data ──
	private demoNotebooks: (Notebook & { notes: Note[] })[] = [];

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

	connectedCallback() {
		this.space = this.getAttribute("space") || "demo";
		this.setupShadow();
		if (this.space === "demo") { this.loadDemoData(); }
		else { this.subscribeOfflineRuntime(); this.loadNotebooks(); }
		// Auto-start tour on first visit
		if (!localStorage.getItem("rnotes_tour_done")) {
			setTimeout(() => this._tour.start(), 1200);
		}
	}

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

		try {
			// Discover all cached notebooks for this space
			const docs = await runtime.subscribeModule('notes', 'notebooks', notebookSchema);
			this.syncConnected = runtime.isOnline;

			// Listen for connection state changes
			this._offlineUnsub = runtime.onStatusChange((status: string) => {
				this.syncConnected = status === 'online';
			});

			// Populate notebook list from cached docs if REST hasn't loaded
			if (docs.size > 0 && this.notebooks.length === 0) {
				const fromDocs: Notebook[] = [];
				for (const [, doc] of docs) {
					const d = doc as NotebookDoc;
					if (!d?.notebook?.id) continue;
					fromDocs.push({
						id: d.notebook.id, title: d.notebook.title,
						description: d.notebook.description || '',
						cover_color: d.notebook.coverColor || '#3b82f6',
						note_count: String(Object.keys(d.items || {}).length),
						updated_at: d.notebook.updatedAt ? new Date(d.notebook.updatedAt).toISOString() : new Date().toISOString(),
					});
				}
				if (fromDocs.length > 0) {
					this.notebooks = fromDocs;
					this.renderNav();
				}
			}
		} catch {
			// Runtime unavailable — REST fallback handles data
		}
	}

	private setupShadow() {
		const style = document.createElement('style');
		style.textContent = this.getStyles();
		this.navZone = document.createElement('div');
		this.navZone.id = 'nav-zone';
		this.contentZone = document.createElement('div');
		this.contentZone.id = 'content-zone';
		this.metaZone = document.createElement('div');
		this.metaZone.id = 'meta-zone';
		this.shadow.appendChild(style);
		this.shadow.appendChild(this.navZone);
		this.shadow.appendChild(this.contentZone);
		this.shadow.appendChild(this.metaZone);
	}

	// ── Demo data ──

	private loadDemoData() {
		const now = Date.now();
		const hour = 3600000;
		const day = 86400000;

		const tripPlanningNotes: Note[] = [
			{
				id: "demo-note-1", title: "Pre-trip Preparation",
				content: `<h2>Pre-trip Preparation</h2><h3>Flights &amp; Transfers</h3><ul><li><strong>Jul 6</strong>: Fly Geneva, shuttle to Chamonix (~1.5h)</li><li><strong>Jul 14</strong>: Train Zermatt to Dolomites (Bernina Express, ~6h scenic route)</li><li><strong>Jul 20</strong>: Fly home from Innsbruck</li></ul><blockquote><p>Book the Aiguille du Midi cable car tickets at least 2 weeks in advance -- they sell out fast in July.</p></blockquote><h3>Travel Documents</h3><ol><li>Passports (valid 6+ months)</li><li>EU health insurance cards (EHIC)</li><li>Travel insurance policy (ref: WA-2026-7891)</li><li>Hut reservation confirmations (printed copies)</li><li>Drone registration for Italy</li></ol><h3>Budget Overview</h3><p>Total budget: <strong>EUR 4,000</strong> across 4 travelers.</p><pre><code>Transport:     EUR  800 (20%)
Accommodation: EUR 1200 (30%)
Activities:    EUR 1000 (25%)
Food:          EUR  600 (15%)
Gear:          EUR  400 (10%)</code></pre><p><em>Maya is tracking expenses in rFlows. Current spend: EUR 1,203.</em></p>`,
				content_plain: "Pre-trip preparation checklist covering flights, transfers, travel documents, and budget overview for the Alpine Explorer 2026 trip.",
				content_format: 'html',
				type: "NOTE", tags: ["planning", "budget", "transport"], is_pinned: true,
				created_at: new Date(now - 14 * day).toISOString(), updated_at: new Date(now - hour).toISOString(),
			},
			{
				id: "demo-note-2", title: "Accommodation Research",
				content: `<h2>Accommodation Research</h2><h3>Chamonix (Jul 6-10)</h3><ul><li><strong>Refuge du Lac Blanc</strong> -- Jul 7, 4 beds, conf #LB2026-234</li><li>Airbnb in town for other nights (~EUR 120/night for 4 pax)</li><li>Consider Hotel Le Morgane if Airbnb falls through</li></ul><h3>Zermatt (Jul 10-14)</h3><ul><li><strong>Hornlihutte</strong> (Matterhorn base) -- Waitlisted for Jul 12</li><li>Main accommodation: Apartment near Bahnhofstrasse</li><li>Car-free village, arrive by Glacier Express</li></ul><blockquote><p>Zermatt is expensive. Budget EUR 80-100pp/night minimum. The apartment saves us about 40% vs hotels.</p></blockquote><h3>Dolomites (Jul 14-20)</h3><ul><li><strong>Rifugio Locatelli</strong> -- Jul 15, 4 beds, conf #TRE2026-089</li><li>Val Gardena base: Ortisei area</li><li>Look for agriturismo options for authentic experience</li></ul>`,
				content_plain: "Accommodation research for all three destinations: Chamonix, Zermatt, and Dolomites. Includes confirmed bookings, waitlists, and budget estimates.",
				content_format: 'html',
				type: "NOTE", tags: ["accommodation", "budget"], is_pinned: false,
				created_at: new Date(now - 12 * day).toISOString(), updated_at: new Date(now - 3 * hour).toISOString(),
			},
			{
				id: "demo-note-3", title: "Activity Planning",
				content: `<h2>Activity Planning</h2><h3>Hiking Routes</h3><ul><li><strong>Lac Blanc</strong> (Jul 7) -- Acclimatization hike, ~6h round trip, 1000m elevation gain.</li><li><strong>Gornergrat Sunrise</strong> (Jul 11) -- Take the first train up at 7am, hike down.</li><li><strong>Matterhorn Base Camp</strong> (Jul 12) -- Full day trek to Hornlihutte. 1500m gain.</li><li><strong>Tre Cime di Lavaredo</strong> (Jul 15) -- Classic loop, ~4h.</li><li><strong>Seceda Ridgeline</strong> (Jul 17) -- Gondola up, ridge walk, hike down to Ortisei.</li></ul><h3>Adventure Activities</h3><ol><li><strong>Via Ferrata at Aiguille du Midi</strong> (Jul 8) -- Rent harness + lanyard + helmet, ~EUR 25/day</li><li><strong>Paragliding over Zermatt</strong> (Jul 13) -- Tandem flights ~EUR 180pp</li><li><strong>Kayaking at Lago di Braies</strong> (Jul 16) -- Turquoise glacial lake, ~EUR 15/hour</li></ol><h3>Rest Days</h3><ul><li>Jul 9: Explore Chamonix town, gear shopping</li><li>Jul 19: Free day before flying home, packing</li></ul>`,
				content_plain: "Detailed activity planning including hiking routes with difficulty ratings, adventure activities with costs, and rest day plans.",
				content_format: 'html',
				type: "NOTE", tags: ["hiking", "activities", "adventure"], is_pinned: false,
				created_at: new Date(now - 10 * day).toISOString(), updated_at: new Date(now - 5 * hour).toISOString(),
			},
			{
				id: "demo-note-4", title: "Gear Research",
				content: `<h2>Gear Research</h2><h3>Via Ferrata Kit</h3><p>Need harness + lanyard + helmet. Can rent in Chamonix for ~EUR 25/day per person.</p><h3>Camera &amp; Drone</h3><ul><li>Bring the <strong>DJI Mini 4 Pro</strong> for Tre Cime and Seceda</li><li>Check Italian drone regulations! Need ENAC registration for flights over 250g</li><li>ND filters for long exposure water shots at Lago di Braies</li><li>Extra batteries (3x) -- cold altitude drains them fast</li></ul><h3>Personal Gear Checklist</h3><ul><li>Hiking boots (broken in!)</li><li>Rain jacket (waterproof, not just resistant)</li><li>Headlamp + spare batteries</li><li>Trekking poles (collapsible for flights)</li><li>Sunscreen SPF 50 + lip balm</li><li>Wool base layers for hut nights</li></ul>`,
				content_plain: "Gear research including Via Ferrata rental, camera and drone regulations, shared group gear status, and personal gear checklist.",
				content_format: 'html',
				type: "NOTE", tags: ["gear", "equipment", "budget"], is_pinned: false,
				created_at: new Date(now - 8 * day).toISOString(), updated_at: new Date(now - 2 * hour).toISOString(),
			},
			{
				id: "demo-note-5", title: "Emergency Contacts & Safety",
				content: `<h2>Emergency Contacts &amp; Safety</h2><h3>Emergency Numbers</h3><ul><li><strong>France</strong>: 112 (EU general), PGHM Mountain Rescue: +33 4 50 53 16 89</li><li><strong>Switzerland</strong>: 1414 (REGA air rescue), 144 (ambulance)</li><li><strong>Italy</strong>: 118 (medical), 112 (general emergency)</li></ul><h3>Insurance</h3><ul><li>Policy #: <strong>WA-2026-7891</strong></li><li>Emergency line: +1-800-555-0199</li><li>Covers: mountain rescue, helicopter evacuation, medical repatriation</li></ul><h3>Altitude Sickness Protocol</h3><ol><li>Acclimatize in Chamonix (1,035m) for 2 days before going high</li><li>Stay hydrated -- minimum 3L water per day above 2,500m</li><li>Watch for symptoms: headache, nausea, dizziness</li><li>Descend immediately if symptoms worsen</li></ol>`,
				content_plain: "Emergency contacts for France, Switzerland, and Italy. Insurance details, altitude sickness protocol, weather contingency plans.",
				content_format: 'html',
				type: "NOTE", tags: ["safety", "emergency", "contacts"], is_pinned: false,
				created_at: new Date(now - 7 * day).toISOString(), updated_at: new Date(now - 6 * hour).toISOString(),
			},
			{
				id: "demo-note-6", title: "Photo Spots & Creative Plan",
				content: `<h2>Photo Spots &amp; Creative Plan</h2><h3>Must-Capture Locations</h3><ol><li><strong>Lac Blanc</strong> -- Reflection of Mont Blanc at sunrise. Arrive by 5:30am. Tripod essential.</li><li><strong>Gornergrat Panorama</strong> -- 360-degree view with Matterhorn. Golden hour is best.</li><li><strong>Tre Cime from Rifugio Locatelli</strong> -- The iconic three peaks at golden hour. Drone shots here.</li><li><strong>Seceda Ridgeline</strong> -- Dramatic Dolomite spires. Best drone footage location.</li><li><strong>Lago di Braies</strong> -- Turquoise water, use ND filters for long exposure reflections.</li></ol><h3>Zine Plan (Maya)</h3><p>We are making an <strong>Alpine Explorer Zine</strong> after the trip:</p><ul><li>Format: A5 risograph, 50 copies</li><li>Print at Chamonix Print Collective</li><li>Content: best photos, trail notes, hand-drawn maps</li><li>Price: EUR 12 per copy on rCart</li></ul>`,
				content_plain: "Photography and creative plan including must-capture locations, drone shot list, zine production details, and video plan.",
				content_format: 'html',
				type: "NOTE", tags: ["photography", "creative", "planning"], is_pinned: false,
				created_at: new Date(now - 5 * day).toISOString(), updated_at: new Date(now - 4 * hour).toISOString(),
			},
		];

		// Typed demo notes
		tripPlanningNotes.push(
			{
				id: "demo-note-code-1", title: "Expense Tracker Script",
				content: `const expenses = [\n  { item: "Flights", amount: 800, category: "transport" },\n  { item: "Lac Blanc Hut", amount: 120, category: "accommodation" },\n  { item: "Via Ferrata Rental", amount: 100, category: "activities" },\n];\n\nconst total = expenses.reduce((sum, e) => sum + e.amount, 0);\nconsole.log(\`Total: EUR \${total}\`);`,
				content_plain: "Expense tracker script for the trip budget",
				content_format: 'html',
				type: "CODE", tags: ["budget", "code"], is_pinned: false,
				language: "javascript",
				created_at: new Date(now - 2 * day).toISOString(), updated_at: new Date(now - 2 * hour).toISOString(),
			} as Note,
			{
				id: "demo-note-bookmark-1", title: "Chamonix Weather Forecast",
				content: "<p>Live weather forecast for the Chamonix valley. Check daily before hikes.</p>",
				content_plain: "Live weather forecast for the Chamonix valley",
				content_format: 'html',
				type: "BOOKMARK", tags: ["weather", "chamonix"], is_pinned: false,
				url: "https://www.chamonix.com/weather",
				created_at: new Date(now - 3 * day).toISOString(), updated_at: new Date(now - 4 * hour).toISOString(),
			} as Note,
		);

		const packingNotes: Note[] = [
			{
				id: "demo-note-7", title: "Packing Checklist",
				content: `<h2>Packing Checklist</h2><h3>Footwear</h3><ul data-type="taskList"><li data-type="taskItem" data-checked="true">Hiking boots (broken in!)</li><li data-type="taskItem" data-checked="false">Camp sandals / flip-flops</li><li data-type="taskItem" data-checked="false">Extra laces</li></ul><h3>Clothing</h3><ul data-type="taskList"><li data-type="taskItem" data-checked="true">Rain jacket (Gore-Tex)</li><li data-type="taskItem" data-checked="false">Down jacket for hut nights</li><li data-type="taskItem" data-checked="false">3x wool base layers</li><li data-type="taskItem" data-checked="false">2x hiking pants</li><li data-type="taskItem" data-checked="false">Sun hat + warm beanie</li></ul><h3>Gear</h3><ul data-type="taskList"><li data-type="taskItem" data-checked="true">Headlamp + spare batteries</li><li data-type="taskItem" data-checked="false">Trekking poles (collapsible)</li><li data-type="taskItem" data-checked="true">First aid kit</li><li data-type="taskItem" data-checked="false">Sunscreen SPF 50</li><li data-type="taskItem" data-checked="false">Water filter (Sawyer Squeeze)</li></ul>`,
				content_plain: "Complete packing checklist organized by category: footwear, clothing, gear, electronics, documents, and food.",
				content_format: 'html',
				type: "NOTE", tags: ["packing", "gear", "checklist"], is_pinned: true,
				created_at: new Date(now - 6 * day).toISOString(), updated_at: new Date(now - hour).toISOString(),
			},
			{
				id: "demo-note-8", title: "Food & Cooking Plan",
				content: `<h2>Food &amp; Cooking Plan</h2><h3>Hut Meals (Half-Board)</h3><p>Lac Blanc and Locatelli include dinner + breakfast. Budget EUR 0 for those nights.</p><h3>Self-Catering Days</h3><p>We have a kitchen in the Chamonix Airbnb and Zermatt apartment.</p><h3>Trail Lunches</h3><p>Pack these the night before each hike:</p><ul><li>Sandwiches (baguette + cheese + ham)</li><li>Energy bars (2 per person)</li><li>Nuts and dried fruit</li><li>Chocolate (the altitude calls for it)</li><li>1.5L water minimum</li></ul>`,
				content_plain: "Food and cooking plan covering hut meals, self-catering, trail lunches, special restaurant meals, and dietary notes.",
				content_format: 'html',
				type: "NOTE", tags: ["food", "planning", "budget"], is_pinned: false,
				created_at: new Date(now - 4 * day).toISOString(), updated_at: new Date(now - 8 * hour).toISOString(),
			},
			{
				id: "demo-note-9", title: "Transport & Logistics",
				content: `<h2>Transport &amp; Logistics</h2><h3>Getting There</h3><ul><li><strong>Jul 6</strong>: Fly to Geneva (everyone arrives by 14:00)</li><li>Geneva to Chamonix shuttle: EUR 186 for 4 pax</li></ul><h3>Between Destinations</h3><ul><li><strong>Jul 10</strong>: Chamonix to Zermatt -- Train via Martigny (~3.5h, scenic)</li><li><strong>Jul 14</strong>: Zermatt to Dolomites -- Bernina Express (6 hours but spectacular)</li></ul><h3>Local Transport</h3><ul><li><strong>Chamonix</strong>: Free local bus with guest card</li><li><strong>Zermatt</strong>: Car-free! Electric taxis + Gornergrat railway</li><li><strong>Dolomites</strong>: Need rental car or local bus (limited schedule)</li></ul>`,
				content_plain: "Transport and logistics plan covering flights, inter-city transfers, local transport options, return journey, and timetables.",
				content_format: 'html',
				type: "NOTE", tags: ["transport", "logistics"], is_pinned: false,
				created_at: new Date(now - 9 * day).toISOString(), updated_at: new Date(now - 5 * hour).toISOString(),
			},
		];

		const itineraryNotes: Note[] = [
			{
				id: "demo-note-10", title: "Full Itinerary -- Alpine Explorer 2026",
				content: `<h2>Full Itinerary -- Alpine Explorer 2026</h2><p><strong>Jul 6-20 | France, Switzerland, Italy</strong></p><h3>Week 1: Chamonix, France (Jul 6-10)</h3><ul><li>Jul 6: Fly Geneva, shuttle to Chamonix</li><li>Jul 7: Acclimatization hike -- Lac Blanc</li><li>Jul 8: Via Ferrata -- Aiguille du Midi</li><li>Jul 9: Rest day / Chamonix town</li><li>Jul 10: Train to Zermatt</li></ul><h3>Week 2: Zermatt, Switzerland (Jul 10-14)</h3><ul><li>Jul 10: Arrive Zermatt, settle in</li><li>Jul 11: Gornergrat sunrise hike</li><li>Jul 12: Matterhorn base camp trek</li><li>Jul 13: Paragliding over Zermatt</li><li>Jul 14: Transfer to Dolomites</li></ul><h3>Week 3: Dolomites, Italy (Jul 14-20)</h3><ul><li>Jul 14: Arrive Val Gardena</li><li>Jul 15: Tre Cime di Lavaredo loop</li><li>Jul 16: Lago di Braies kayaking</li><li>Jul 17: Seceda ridgeline hike</li><li>Jul 18: Cooking class in Bolzano</li><li>Jul 19: Free day -- shopping &amp; packing</li><li>Jul 20: Fly home from Innsbruck</li></ul>`,
				content_plain: "Complete day-by-day itinerary for the Alpine Explorer 2026 trip covering three weeks across Chamonix, Zermatt, and the Dolomites.",
				content_format: 'html',
				type: "NOTE", tags: ["itinerary", "planning"], is_pinned: true,
				created_at: new Date(now - 15 * day).toISOString(), updated_at: new Date(now - 2 * hour).toISOString(),
			},
			{
				id: "demo-note-11", title: "Mountain Hut Reservations",
				content: `<h2>Mountain Hut Reservations</h2><h3>Confirmed</h3><ul><li><strong>Refuge du Lac Blanc</strong> (Jul 7) -- 4 beds, half-board, conf #LB2026-234</li><li><strong>Rifugio Locatelli</strong> (Jul 15) -- 4 beds, half-board, conf #TRE2026-089</li></ul><h3>Waitlisted</h3><ul><li><strong>Hornlihutte</strong> (Matterhorn base, Jul 12) -- will know by Jul 1</li></ul><h3>Hut Etiquette Reminders</h3><ol><li>Arrive before 17:00 if possible</li><li>Remove boots at entrance (bring hut shoes or thick socks)</li><li>Lights out by 22:00</li><li>Pack out all trash</li><li>Tip is appreciated but not required</li></ol>`,
				content_plain: "Mountain hut reservations with confirmation numbers, check-in details, and hut etiquette reminders.",
				content_format: 'html',
				type: "NOTE", tags: ["accommodation", "hiking"], is_pinned: false,
				created_at: new Date(now - 11 * day).toISOString(), updated_at: new Date(now - day).toISOString(),
			},
			{
				id: "demo-note-12", title: "Group Decisions & Votes",
				content: `<h2>Group Decisions &amp; Votes</h2><h3>Decided</h3><ul><li><strong>Camera Gear</strong>: DJI Mini 4 Pro (Liam's decision matrix: 8.5/10)</li><li><strong>First Night Dinner in Zermatt</strong>: Fondue at Chez Vrony (won 5-4 over pizza)</li><li><strong>Day 5 Activity</strong>: Via Ferrata at Aiguille du Midi (won 7-3 over kayaking)</li></ul><h3>Active Votes (in rVote)</h3><ul><li><strong>Zermatt to Dolomites transfer</strong>: Train vs rental car -- Train leading 3-2</li></ul><h3>Pending Decisions</h3><ul><li>Val Gardena accommodation (agriturismo vs apartment)</li><li>Whether to rent the Starlink Mini (EUR 200, needs funding)</li><li>Trip zine print run size (50 vs 100 copies)</li></ul>`,
				content_plain: "Summary of group decisions made and active votes. Covers camera gear, dining, activities, and pending decisions.",
				content_format: 'html',
				type: "NOTE", tags: ["decisions", "planning"], is_pinned: false,
				created_at: new Date(now - 3 * day).toISOString(), updated_at: new Date(now - 3 * hour).toISOString(),
			},
		];

		this.demoNotebooks = [
			{
				id: "demo-nb-1", title: "Alpine Explorer Planning", description: "Shared knowledge base for our July 2026 trip across France, Switzerland, and Italy",
				cover_color: "#f59e0b", note_count: "8", updated_at: new Date(now - hour).toISOString(),
				notes: tripPlanningNotes,
			} as any,
			{
				id: "demo-nb-2", title: "Packing & Logistics", description: "Checklists, food plans, and transport details",
				cover_color: "#22c55e", note_count: "3", updated_at: new Date(now - hour).toISOString(),
				notes: packingNotes,
			} as any,
			{
				id: "demo-nb-3", title: "Itinerary & Decisions", description: "Day-by-day schedule, hut reservations, and group votes",
				cover_color: "#6366f1", note_count: "3", updated_at: new Date(now - 2 * hour).toISOString(),
				notes: itineraryNotes,
			} as any,
		];

		this.notebooks = this.demoNotebooks.map(({ notes, ...nb }) => nb as Notebook);
		this.loading = false;
		this.render();
	}

	private demoSearchNotes(query: string) {
		if (!query.trim()) {
			this.searchResults = [];
			this.render();
			return;
		}
		const q = query.toLowerCase();
		const all = this.demoNotebooks.flatMap(nb => nb.notes);
		this.searchResults = all.filter(n =>
			n.title.toLowerCase().includes(q) ||
			n.content_plain.toLowerCase().includes(q) ||
			(n.tags && n.tags.some(t => t.toLowerCase().includes(q)))
		);
		this.render();
	}

	private demoLoadNotebook(id: string) {
		const nb = this.demoNotebooks.find(n => n.id === id);
		if (nb) {
			this.selectedNotebook = { ...nb };
		} else {
			this.error = "Notebook not found";
		}
		this.loading = false;
		this.render();
	}

	private demoLoadNote(id: string) {
		const allNotes = this.demoNotebooks.flatMap(nb => nb.notes);
		this.selectedNote = allNotes.find(n => n.id === id) || null;
		if (this.selectedNote) {
			this.view = "note";
			this.renderNav();
			this.renderMeta();
			this.mountEditor(this.selectedNote);
		}
	}

	private demoCreateNotebook() {
		const now = Date.now();
		const nbId = `demo-nb-${now}`;
		const noteId = `demo-note-${now}`;
		const newNote: Note = {
			id: noteId, title: "Untitled Note", content: "", content_plain: "",
			content_format: 'tiptap-json',
			type: "NOTE", tags: null, is_pinned: false,
			created_at: new Date(now).toISOString(), updated_at: new Date(now).toISOString(),
		};
		const nb = {
			id: nbId, title: "Untitled Notebook", description: "",
			cover_color: "#8b5cf6", note_count: "1",
			updated_at: new Date(now).toISOString(), notes: [newNote],
		} as any;
		this.demoNotebooks.push(nb);
		this.notebooks = this.demoNotebooks.map(({ notes, ...rest }) => rest as Notebook);
		this.selectedNotebook = { ...nb };
		this.view = "notebook";
		this.render();
		// Auto-open the note for editing
		this.selectedNote = newNote;
		this.view = "note";
		this.renderNav();
		this.renderMeta();
		this.mountEditor(newNote);
	}

	private demoCreateNote(opts: CreateNoteOpts = {}) {
		if (!this.selectedNotebook) return;
		const now = Date.now();
		const noteId = `demo-note-${now}`;
		const type = opts.type || 'NOTE';
		const title = opts.title || FolkNotesApp.typeDefaultTitle(type);
		const newNote: Note = {
			id: noteId, title, content: opts.content || "", content_plain: "",
			content_format: type === 'CODE' ? 'html' : 'tiptap-json',
			type, tags: opts.tags || null, is_pinned: false,
			url: opts.url || null, language: opts.language || null,
			fileUrl: opts.fileUrl || null, mimeType: opts.mimeType || null,
			duration: opts.duration ?? null,
			created_at: new Date(now).toISOString(), updated_at: new Date(now).toISOString(),
		};
		const demoNb = this.demoNotebooks.find(n => n.id === this.selectedNotebook!.id);
		if (demoNb) {
			demoNb.notes.push(newNote);
			demoNb.note_count = String(demoNb.notes.length);
		}
		this.selectedNotebook.notes.push(newNote);
		this.selectedNotebook.note_count = String(this.selectedNotebook.notes.length);
		this.selectedNote = newNote;
		this.view = "note";
		this.renderNav();
		this.renderMeta();
		this.mountEditor(newNote);
	}

	disconnectedCallback() {
		this.destroyEditor();
		this._offlineUnsub?.();
		this._offlineUnsub = null;
		for (const unsub of this._offlineNotebookUnsubs) unsub();
		this._offlineNotebookUnsubs = [];
	}

	// ── Sync (via shared runtime) ──

	private async subscribeNotebook(notebookId: string) {
		const runtime = (window as any).__rspaceOfflineRuntime;
		// Resolve scope: rnotes is globally-scoped, so use 'global' prefix
		const dataSpace = runtime?.isInitialized
			? (runtime.resolveDocSpace?.('rnotes') || this.space)
			: this.space;
		this.subscribedDocId = `${dataSpace}:notes:notebooks:${notebookId}`;

		if (runtime?.isInitialized) {
			try {
				const docId = this.subscribedDocId as DocumentId;
				const doc = await runtime.subscribe(docId, notebookSchema);
				this.doc = doc;
				this.renderFromDoc();

				const unsub = runtime.onChange(docId, (updated: any) => {
					this.doc = updated;
					this.renderFromDoc();
				});
				this._offlineNotebookUnsubs.push(unsub);
			} catch {
				// Fallback: initialize empty doc for fresh notebook
				this.doc = Automerge.init<NotebookDoc>();
			}
		} else {
			// No runtime — initialize empty doc
			this.doc = Automerge.init<NotebookDoc>();
		}
	}

	private unsubscribeNotebook() {
		if (this.subscribedDocId) {
			const runtime = (window as any).__rspaceOfflineRuntime;
			if (runtime?.isInitialized) {
				runtime.unsubscribe(this.subscribedDocId as DocumentId);
			}
		}
		this.subscribedDocId = null;
		this.doc = null;
		for (const unsub of this._offlineNotebookUnsubs) unsub();
		this._offlineNotebookUnsubs = [];
	}

	/** Extract notebook + notes from Automerge doc into component state */
	private renderFromDoc() {
		if (!this.doc) return;

		const nb = this.doc.notebook;
		const items = this.doc.items;

		if (!nb) return;

		// Build notebook data from doc
		const notes: Note[] = [];
		if (items) {
			for (const [, item] of Object.entries(items)) {
				notes.push({
					id: item.id,
					title: item.title || "Untitled",
					content: item.content || "",
					content_plain: item.contentPlain || "",
					content_format: (item.contentFormat as Note['content_format']) || undefined,
					type: item.type || "NOTE",
					tags: item.tags?.length ? Array.from(item.tags) : null,
					is_pinned: item.isPinned || false,
					url: item.url || null,
					language: item.language || null,
					fileUrl: item.fileUrl || null,
					mimeType: item.mimeType || null,
					duration: item.duration ?? null,
					created_at: item.createdAt ? new Date(item.createdAt).toISOString() : new Date().toISOString(),
					updated_at: item.updatedAt ? new Date(item.updatedAt).toISOString() : new Date().toISOString(),
				});
			}
		}

		notes.sort((a, b) => {
			if (a.is_pinned !== b.is_pinned) return a.is_pinned ? -1 : 1;
			return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
		});

		this.selectedNotebook = {
			id: nb.id,
			title: nb.title,
			description: nb.description || "",
			cover_color: nb.coverColor || "#3b82f6",
			note_count: String(notes.length),
			updated_at: nb.updatedAt ? new Date(nb.updatedAt).toISOString() : new Date().toISOString(),
			notes,
		};

		// If viewing a note and editor is mounted, update editor content from remote
		if (this.view === "note" && this.selectedNote && this.editor && this.editorNoteId === this.selectedNote.id) {
			const noteItem = items?.[this.selectedNote.id];
			if (noteItem) {
				this.selectedNote = {
					id: noteItem.id,
					title: noteItem.title || "Untitled",
					content: noteItem.content || "",
					content_plain: noteItem.contentPlain || "",
					content_format: (noteItem.contentFormat as Note['content_format']) || undefined,
					type: noteItem.type || "NOTE",
					tags: noteItem.tags?.length ? Array.from(noteItem.tags) : null,
					is_pinned: noteItem.isPinned || false,
					url: noteItem.url || null,
					language: noteItem.language || null,
					fileUrl: noteItem.fileUrl || null,
					mimeType: noteItem.mimeType || null,
					duration: noteItem.duration ?? null,
					created_at: noteItem.createdAt ? new Date(noteItem.createdAt).toISOString() : new Date().toISOString(),
					updated_at: noteItem.updatedAt ? new Date(noteItem.updatedAt).toISOString() : new Date().toISOString(),
				};

				// Update editor content if different (remote change)
				const remoteContent = noteItem.content || "";
				const currentContent = noteItem.contentFormat === 'tiptap-json'
					? JSON.stringify(this.editor.getJSON())
					: this.editor.getHTML();

				if (remoteContent !== currentContent) {
					this.isRemoteUpdate = true;
					try {
						if (noteItem.contentFormat === 'tiptap-json') {
							try {
								this.editor.commands.setContent(JSON.parse(remoteContent), { emitUpdate: false });
							} catch {
								this.editor.commands.setContent(remoteContent, { emitUpdate: false });
							}
						} else {
							this.editor.commands.setContent(remoteContent, { emitUpdate: false });
						}
					} finally {
						this.isRemoteUpdate = false;
					}
				}

				// Update title input if it exists
				const titleInput = this.shadow.querySelector('#note-title-input') as HTMLInputElement;
				if (titleInput && document.activeElement !== titleInput && titleInput !== this.shadow.activeElement) {
					titleInput.value = noteItem.title || "Untitled";
				}

				// Only update nav/meta, skip contentZone
				this.renderNav();
				this.renderMeta();
				this.loading = false;
				return;
			}
		}

		// If viewing a specific note without editor mounted, update selectedNote
		if (this.view === "note" && this.selectedNote) {
			const noteItem = items?.[this.selectedNote.id];
			if (noteItem) {
				this.selectedNote = {
					id: noteItem.id,
					title: noteItem.title || "Untitled",
					content: noteItem.content || "",
					content_plain: noteItem.contentPlain || "",
					content_format: (noteItem.contentFormat as Note['content_format']) || undefined,
					type: noteItem.type || "NOTE",
					tags: noteItem.tags?.length ? Array.from(noteItem.tags) : null,
					is_pinned: noteItem.isPinned || false,
					url: noteItem.url || null,
					language: noteItem.language || null,
					fileUrl: noteItem.fileUrl || null,
					mimeType: noteItem.mimeType || null,
					duration: noteItem.duration ?? null,
					created_at: noteItem.createdAt ? new Date(noteItem.createdAt).toISOString() : new Date().toISOString(),
					updated_at: noteItem.updatedAt ? new Date(noteItem.updatedAt).toISOString() : new Date().toISOString(),
				};
			}
		}

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

	// ── Automerge mutations ──

	private static typeDefaultTitle(type: NoteType): string {
		switch (type) {
			case 'CODE': return 'Untitled Code Snippet';
			case 'BOOKMARK': return 'Untitled Bookmark';
			case 'CLIP': return 'Untitled Clip';
			case 'IMAGE': return 'Untitled Image';
			case 'AUDIO': return 'Voice Note';
			case 'FILE': return 'Untitled File';
			default: return 'Untitled Note';
		}
	}

	private createNoteViaSync(opts: CreateNoteOpts = {}) {
		if (!this.doc || !this.selectedNotebook || !this.subscribedDocId) return;

		const noteId = crypto.randomUUID();
		const now = Date.now();
		const notebookId = this.selectedNotebook.id;
		const type = opts.type || 'NOTE';
		const title = opts.title || FolkNotesApp.typeDefaultTitle(type);
		const contentFormat = type === 'CODE' ? 'html' : 'tiptap-json';

		const itemData: any = {
			id: noteId, notebookId, title,
			content: opts.content || "", contentPlain: "", contentFormat,
			type, tags: opts.tags || [], isPinned: false, sortOrder: 0,
			createdAt: now, updatedAt: now,
			url: opts.url || null, language: opts.language || null,
			fileUrl: opts.fileUrl || null, mimeType: opts.mimeType || null,
			duration: opts.duration ?? null,
		};

		const runtime = (window as any).__rspaceOfflineRuntime;
		if (runtime?.isInitialized) {
			runtime.change(this.subscribedDocId as DocumentId, "Create note", (d: NotebookDoc) => {
				if (!d.items) (d as any).items = {};
				d.items[noteId] = itemData;
			});
			this.doc = runtime.get(this.subscribedDocId as DocumentId);
		} else {
			this.doc = Automerge.change(this.doc, "Create note", (d: NotebookDoc) => {
				if (!d.items) (d as any).items = {};
				d.items[noteId] = itemData;
			});
		}

		this.renderFromDoc();

		// Open the new note
		this.selectedNote = {
			id: noteId, title, content: opts.content || "", content_plain: "",
			content_format: contentFormat,
			type, tags: opts.tags || null, is_pinned: false,
			url: opts.url || null, language: opts.language || null,
			fileUrl: opts.fileUrl || null, mimeType: opts.mimeType || null,
			duration: opts.duration ?? null,
			created_at: new Date(now).toISOString(), updated_at: new Date(now).toISOString(),
		};
		this.view = "note";
		this.renderNav();
		this.renderMeta();
		this.mountEditor(this.selectedNote);
	}

	private updateNoteField(noteId: string, field: string, value: string) {
		if (!this.doc || !this.doc.items?.[noteId] || !this.subscribedDocId) return;

		const runtime = (window as any).__rspaceOfflineRuntime;
		if (runtime?.isInitialized) {
			runtime.change(this.subscribedDocId as DocumentId, `Update ${field}`, (d: NotebookDoc) => {
				(d.items[noteId] as any)[field] = value;
				d.items[noteId].updatedAt = Date.now();
			});
			this.doc = runtime.get(this.subscribedDocId as DocumentId);
		} else {
			this.doc = Automerge.change(this.doc, `Update ${field}`, (d: NotebookDoc) => {
				(d.items[noteId] as any)[field] = value;
				d.items[noteId].updatedAt = Date.now();
			});
		}
	}

	// ── Note summarization ──

	private async summarizeNote(btn: HTMLElement) {
		const noteId = this.editorNoteId || this.selectedNote?.id;
		if (!noteId) return;

		// Get note content (plain text)
		const item = this.doc?.items?.[noteId];
		const content = item?.contentPlain || item?.content || this.selectedNote?.content_plain || '';
		if (!content?.trim()) return;

		// Show loading state on button
		btn.classList.add('active');
		btn.style.pointerEvents = 'none';

		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/notes/summarize`, {
				method: 'POST',
				headers: this.authHeaders({ 'Content-Type': 'application/json' }),
				body: JSON.stringify({ content, model: 'gemini-flash', length: 'medium' }),
			});

			if (!res.ok) {
				const err = await res.json().catch(() => ({}));
				console.error('[Notes] Summarize error:', err);
				return;
			}

			const data = await res.json() as { summary: string; model: string };

			// Save to Automerge doc
			if (this.space !== 'demo') {
				this.updateNoteField(noteId, 'summary', data.summary);
				this.updateNoteField(noteId, 'summaryModel', data.model);
			}

			// Update local selected note for immediate display
			if (this.selectedNote && this.selectedNote.id === noteId) {
				(this.selectedNote as any).summary = data.summary;
				(this.selectedNote as any).summaryModel = data.model;
			}

			this.renderMeta();
		} catch (err) {
			console.error('[Notes] Summarize failed:', err);
		} finally {
			btn.classList.remove('active');
			btn.style.pointerEvents = '';
		}
	}

	private async sendToOpenNotebook() {
		const noteId = this.editorNoteId || this.selectedNote?.id;
		if (!noteId) return;

		const item = this.doc?.items?.[noteId];
		const content = item?.contentPlain || item?.content || this.selectedNote?.content_plain || '';
		const title = item?.title || this.selectedNote?.title || 'Untitled';
		if (!content?.trim()) return;

		// Disable button during request
		const btn = this.metaZone.querySelector('[data-action="send-to-notebook"]') as HTMLButtonElement;
		if (btn) { btn.disabled = true; btn.textContent = 'Sending...'; }

		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/notes/send-to-notebook`, {
				method: 'POST',
				headers: this.authHeaders({ 'Content-Type': 'application/json' }),
				body: JSON.stringify({ noteId, title, content }),
			});

			if (!res.ok) {
				console.error('[Notes] Send to notebook error:', res.status);
				if (btn) { btn.disabled = false; btn.textContent = 'Send to Notebook'; }
				return;
			}

			const data = await res.json() as { sourceId: string };
			this.updateNoteField(noteId, 'openNotebookSourceId', data.sourceId);

			if (this.selectedNote && this.selectedNote.id === noteId) {
				(this.selectedNote as any).openNotebookSourceId = data.sourceId;
			}

			this.renderMeta();
		} catch (err) {
			console.error('[Notes] Send to notebook failed:', err);
			if (btn) { btn.disabled = false; btn.textContent = 'Send to Notebook'; }
		}
	}

	// ── REST (notebook list + search) ──

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

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

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

	private async loadNotebook(id: string) {
		this.loading = true;
		this.render();

		this.unsubscribeNotebook();
		this.subscribeNotebook(id);

		setTimeout(() => {
			if (this.loading && this.view === "notebook") {
				this.loadNotebookREST(id);
			}
		}, 5000);
	}

	private async loadNotebookREST(id: string) {
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/notebooks/${id}`, { headers: this.authHeaders() });
			this.selectedNotebook = await res.json();
		} catch {
			this.error = "Failed to load notebook";
		}
		this.loading = false;
		this.render();
	}

	private loadNote(id: string) {
		// Note is already in the Automerge doc
		if (this.doc?.items?.[id]) {
			const item = this.doc.items[id];
			this.selectedNote = {
				id: item.id,
				title: item.title || "Untitled",
				content: item.content || "",
				content_plain: item.contentPlain || "",
				content_format: (item.contentFormat as Note['content_format']) || undefined,
				type: item.type || "NOTE",
				tags: item.tags?.length ? Array.from(item.tags) : null,
				is_pinned: item.isPinned || false,
				url: item.url || null,
				language: item.language || null,
				fileUrl: item.fileUrl || null,
				mimeType: item.mimeType || null,
				duration: item.duration ?? null,
				created_at: item.createdAt ? new Date(item.createdAt).toISOString() : new Date().toISOString(),
				updated_at: item.updatedAt ? new Date(item.updatedAt).toISOString() : new Date().toISOString(),
			};
		} else if (this.selectedNotebook?.notes) {
			this.selectedNote = this.selectedNotebook.notes.find(n => n.id === id) || null;
		}

		if (this.selectedNote) {
			this.renderNav();
			this.renderMeta();
			this.mountEditor(this.selectedNote);
		}
	}

	private async searchNotes(query: string) {
		if (!query.trim()) {
			this.searchResults = [];
			this.render();
			return;
		}
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/notes?q=${encodeURIComponent(query)}`, { headers: this.authHeaders() });
			const data = await res.json();
			this.searchResults = data.notes || [];
		} catch {
			this.searchResults = [];
		}
		this.render();
	}

	private async createNotebook() {
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/notebooks`, {
				method: "POST",
				headers: this.authHeaders({ "Content-Type": "application/json" }),
				body: JSON.stringify({ title: "Untitled Notebook" }),
			});
			const nb = await res.json();
			if (nb?.id) {
				this.view = "notebook";
				await this.loadNotebook(nb.id);
			} else {
				await this.loadNotebooks();
			}
		} catch {
			this.error = "Failed to create notebook";
			this.render();
		}
	}

	// ── Tiptap Editor ──

	private mountEditor(note: Note) {
		this.destroyEditor();
		this.editorNoteId = note.id;

		const isDemo = this.space === "demo";
		const isAutomerge = !!(this.doc?.items?.[note.id]);
		const isEditable = isAutomerge || isDemo;

		// Branch on note type
		switch (note.type) {
			case 'CODE': this.mountCodeEditor(note, isEditable, isDemo); break;
			case 'BOOKMARK':
			case 'CLIP': this.mountBookmarkView(note, isEditable, isDemo); break;
			case 'IMAGE': this.mountImageView(note, isEditable, isDemo); break;
			case 'AUDIO': this.mountAudioView(note, isEditable, isDemo); break;
			default: this.mountTiptapEditor(note, isEditable, isDemo); break;
		}
	}

	private mountTiptapEditor(note: Note, isEditable: boolean, isDemo: boolean) {
		this.contentZone.innerHTML = `
			<div class="editor-wrapper">
				<input class="editable-title" id="note-title-input" value="${this.esc(note.title)}" placeholder="Note title...">
				${isEditable ? this.renderToolbar() : ''}
				<div class="tiptap-container" id="tiptap-container"></div>
			</div>
		`;

		const container = this.shadow.getElementById('tiptap-container');
		if (!container) return;

		let content: any = '';
		if (note.content) {
			if (note.content_format === 'tiptap-json') {
				try { content = JSON.parse(note.content); } catch { content = note.content; }
			} else {
				content = note.content;
			}
		}

		this.editor = new Editor({
			element: container,
			editable: isEditable,
			extensions: [
				StarterKit.configure({ codeBlock: false, heading: { levels: [1, 2, 3, 4] } }),
				Link.configure({ openOnClick: false }),
				Image, TaskList, TaskItem.configure({ nested: true }),
				Placeholder.configure({ placeholder: 'Start writing, or type / for commands...' }),
				CodeBlockLowlight.configure({ lowlight }), Typography, Underline,
			],
			content,
			onUpdate: ({ editor }) => {
				if (this.isRemoteUpdate) return;
				if (this.editorUpdateTimer) clearTimeout(this.editorUpdateTimer);
				this.editorUpdateTimer = setTimeout(() => {
					const json = JSON.stringify(editor.getJSON());
					const plain = editor.getText();
					const noteId = this.editorNoteId;
					if (!noteId) return;
					if (isDemo) {
						this.demoUpdateNoteField(noteId, "content", json);
						this.demoUpdateNoteField(noteId, "content_plain", plain);
						this.demoUpdateNoteField(noteId, "content_format", 'tiptap-json');
					} else {
						this.updateNoteField(noteId, "content", json);
						this.updateNoteField(noteId, "contentPlain", plain);
						this.updateNoteField(noteId, "contentFormat", 'tiptap-json');
					}
				}, 800);
			},
			onSelectionUpdate: () => { this.updateToolbarState(); },
		});

		this.editor.registerPlugin(createSlashCommandPlugin(this.editor, this.shadow));

		container.addEventListener('slash-insert-image', () => {
			if (!this.editor) return;
			const { from } = this.editor.view.state.selection;
			const coords = this.editor.view.coordsAtPos(from);
			const rect = new DOMRect(coords.left, coords.top, 0, coords.bottom - coords.top);
			this.showUrlPopover(rect, 'Enter image URL...').then(url => {
				if (url) this.editor!.chain().focus().setImage({ src: url }).run();
			});
		});

		// Listen for slash-create-typed-note events
		container.addEventListener('slash-create-typed-note', ((e: CustomEvent) => {
			const { type } = e.detail || {};
			if (type && this.selectedNotebook) {
				this.createNoteViaSync({ type });
			}
		}) as EventListener);

		this.wireTitleInput(note, isEditable, isDemo);
		this.attachToolbarListeners();
	}

	private mountCodeEditor(note: Note, isEditable: boolean, isDemo: boolean) {
		const languages = ['javascript', 'typescript', 'python', 'rust', 'go', 'html', 'css', 'json', 'sql', 'bash', 'c', 'cpp', 'java', 'ruby', 'php', 'markdown', 'yaml', 'toml', 'other'];
		const currentLang = note.language || 'javascript';

		this.contentZone.innerHTML = `
			<div class="editor-wrapper code-editor-wrapper">
				<input class="editable-title" id="note-title-input" value="${this.esc(note.title)}" placeholder="Code snippet title...">
				<div class="code-editor-controls">
					<select id="code-lang-select" class="toolbar-select"${!isEditable ? ' disabled' : ''}>
						${languages.map(l => `<option value="${l}"${l === currentLang ? ' selected' : ''}>${l}</option>`).join('')}
					</select>
				</div>
				<textarea id="code-textarea" class="code-textarea" placeholder="Paste or type code here..."${!isEditable ? ' readonly' : ''}>${this.esc(note.content || '')}</textarea>
			</div>
		`;

		const textarea = this.shadow.getElementById('code-textarea') as HTMLTextAreaElement;
		const langSelect = this.shadow.getElementById('code-lang-select') as HTMLSelectElement;

		if (textarea && isEditable) {
			let timer: any;
			textarea.addEventListener('input', () => {
				clearTimeout(timer);
				timer = setTimeout(() => {
					if (isDemo) {
						this.demoUpdateNoteField(note.id, "content", textarea.value);
						this.demoUpdateNoteField(note.id, "content_plain", textarea.value);
					} else {
						this.updateNoteField(note.id, "content", textarea.value);
						this.updateNoteField(note.id, "contentPlain", textarea.value);
					}
				}, 800);
			});
			// Tab inserts a tab character
			textarea.addEventListener('keydown', (e) => {
				if (e.key === 'Tab') {
					e.preventDefault();
					const start = textarea.selectionStart;
					textarea.value = textarea.value.substring(0, start) + '\t' + textarea.value.substring(textarea.selectionEnd);
					textarea.selectionStart = textarea.selectionEnd = start + 1;
					textarea.dispatchEvent(new Event('input'));
				}
			});
		}

		if (langSelect && isEditable) {
			langSelect.addEventListener('change', () => {
				if (isDemo) {
					this.demoUpdateNoteField(note.id, "language", langSelect.value);
				} else {
					this.updateNoteField(note.id, "language", langSelect.value);
				}
			});
		}

		this.wireTitleInput(note, isEditable, isDemo);
	}

	private mountBookmarkView(note: Note, isEditable: boolean, isDemo: boolean) {
		const hostname = note.url ? (() => { try { return new URL(note.url).hostname; } catch { return note.url; } })() : '';
		const favicon = note.url ? `https://www.google.com/s2/favicons?sz=32&domain=${hostname}` : '';

		this.contentZone.innerHTML = `
			<div class="editor-wrapper bookmark-wrapper">
				<input class="editable-title" id="note-title-input" value="${this.esc(note.title)}" placeholder="${note.type === 'CLIP' ? 'Clip title...' : 'Bookmark title...'}">
				<div class="bookmark-card">
					${favicon ? `<img src="${favicon}" class="bookmark-favicon" alt="" width="24" height="24">` : ''}
					<div class="bookmark-info">
						${note.url ? `<a href="${this.esc(note.url)}" target="_blank" rel="noopener" class="bookmark-url">${this.esc(hostname)}</a>` : ''}
						<div class="bookmark-url-input-row">
							<input id="bookmark-url-input" class="bookmark-url-input" value="${this.esc(note.url || '')}" placeholder="Enter URL..."${!isEditable ? ' readonly' : ''}>
						</div>
					</div>
				</div>
				<div class="tiptap-container" id="tiptap-container"></div>
			</div>
		`;

		// Mount tiptap for the excerpt/notes
		const container = this.shadow.getElementById('tiptap-container');
		if (!container) return;

		let content: any = '';
		if (note.content) {
			if (note.content_format === 'tiptap-json') {
				try { content = JSON.parse(note.content); } catch { content = note.content; }
			} else { content = note.content; }
		}

		this.editor = new Editor({
			element: container, editable: isEditable,
			extensions: [
				StarterKit.configure({ codeBlock: false, heading: { levels: [1, 2, 3] } }),
				Link.configure({ openOnClick: false }), Image,
				Placeholder.configure({ placeholder: note.type === 'CLIP' ? 'Clipped content...' : 'Add notes about this bookmark...' }),
				Typography, Underline,
			],
			content,
			onUpdate: ({ editor }) => {
				if (this.isRemoteUpdate) return;
				if (this.editorUpdateTimer) clearTimeout(this.editorUpdateTimer);
				this.editorUpdateTimer = setTimeout(() => {
					const json = JSON.stringify(editor.getJSON());
					const plain = editor.getText();
					const noteId = this.editorNoteId;
					if (!noteId) return;
					if (isDemo) {
						this.demoUpdateNoteField(noteId, "content", json);
						this.demoUpdateNoteField(noteId, "content_plain", plain);
					} else {
						this.updateNoteField(noteId, "content", json);
						this.updateNoteField(noteId, "contentPlain", plain);
					}
				}, 800);
			},
		});

		// Wire URL input
		const urlInput = this.shadow.getElementById('bookmark-url-input') as HTMLInputElement;
		if (urlInput && isEditable) {
			let timer: any;
			urlInput.addEventListener('input', () => {
				clearTimeout(timer);
				timer = setTimeout(() => {
					if (isDemo) {
						this.demoUpdateNoteField(note.id, "url", urlInput.value);
					} else {
						this.updateNoteField(note.id, "url", urlInput.value);
					}
				}, 500);
			});
		}

		this.wireTitleInput(note, isEditable, isDemo);
	}

	private mountImageView(note: Note, isEditable: boolean, isDemo: boolean) {
		this.contentZone.innerHTML = `
			<div class="editor-wrapper image-wrapper">
				<input class="editable-title" id="note-title-input" value="${this.esc(note.title)}" placeholder="Image title...">
				${note.fileUrl
					? `<div class="image-display"><img src="${this.esc(note.fileUrl)}" alt="${this.esc(note.title)}" class="image-preview"></div>`
					: `<div class="image-upload-placeholder">
						<input id="image-url-input" class="bookmark-url-input" value="${this.esc(note.fileUrl || '')}" placeholder="Enter image URL..."${!isEditable ? ' readonly' : ''}>
					</div>`
				}
				<div class="tiptap-container" id="tiptap-container"></div>
			</div>
		`;

		// Mount tiptap for caption/notes
		const container = this.shadow.getElementById('tiptap-container');
		if (container) {
			let content: any = '';
			if (note.content) {
				if (note.content_format === 'tiptap-json') {
					try { content = JSON.parse(note.content); } catch { content = note.content; }
				} else { content = note.content; }
			}
			this.editor = new Editor({
				element: container, editable: isEditable,
				extensions: [
					StarterKit.configure({ codeBlock: false }), Link.configure({ openOnClick: false }),
					Placeholder.configure({ placeholder: 'Add a caption or notes...' }), Typography, Underline,
				],
				content,
				onUpdate: ({ editor }) => {
					if (this.isRemoteUpdate) return;
					if (this.editorUpdateTimer) clearTimeout(this.editorUpdateTimer);
					this.editorUpdateTimer = setTimeout(() => {
						const json = JSON.stringify(editor.getJSON());
						const plain = editor.getText();
						const noteId = this.editorNoteId;
						if (!noteId) return;
						if (isDemo) { this.demoUpdateNoteField(noteId, "content", json); this.demoUpdateNoteField(noteId, "content_plain", plain); }
						else { this.updateNoteField(noteId, "content", json); this.updateNoteField(noteId, "contentPlain", plain); }
					}, 800);
				},
			});
		}

		// Wire image URL input
		const imgUrlInput = this.shadow.getElementById('image-url-input') as HTMLInputElement;
		if (imgUrlInput && isEditable) {
			let timer: any;
			imgUrlInput.addEventListener('input', () => {
				clearTimeout(timer);
				timer = setTimeout(() => {
					if (isDemo) { this.demoUpdateNoteField(note.id, "fileUrl", imgUrlInput.value); }
					else { this.updateNoteField(note.id, "fileUrl", imgUrlInput.value); }
				}, 500);
			});
		}

		this.wireTitleInput(note, isEditable, isDemo);
	}

	private mountAudioView(note: Note, isEditable: boolean, isDemo: boolean) {
		const durationStr = note.duration ? `${Math.floor(note.duration / 60)}:${String(note.duration % 60).padStart(2, '0')}` : '';

		this.contentZone.innerHTML = `
			<div class="editor-wrapper audio-wrapper">
				<input class="editable-title" id="note-title-input" value="${this.esc(note.title)}" placeholder="Voice note title...">
				${note.fileUrl
					? `<div class="audio-player-container">
						<audio controls src="${this.esc(note.fileUrl)}" class="audio-player"></audio>
						${durationStr ? `<span class="audio-duration">${durationStr}</span>` : ''}
					</div>`
					: `<div class="audio-record-placeholder">
						<button class="rapp-nav__btn" id="btn-start-recording">Record Voice Note</button>
					</div>`
				}
				<div class="audio-transcript-section">
					<div class="audio-transcript-label">Transcript</div>
					<div class="tiptap-container" id="tiptap-container"></div>
				</div>
			</div>
		`;

		// Mount tiptap for transcript
		const container = this.shadow.getElementById('tiptap-container');
		if (container) {
			let content: any = '';
			if (note.content) {
				if (note.content_format === 'tiptap-json') {
					try { content = JSON.parse(note.content); } catch { content = note.content; }
				} else { content = note.content; }
			}
			this.editor = new Editor({
				element: container, editable: isEditable,
				extensions: [
					StarterKit.configure({ codeBlock: false }), Link.configure({ openOnClick: false }),
					Placeholder.configure({ placeholder: 'Transcript will appear here...' }), Typography, Underline,
				],
				content,
				onUpdate: ({ editor }) => {
					if (this.isRemoteUpdate) return;
					if (this.editorUpdateTimer) clearTimeout(this.editorUpdateTimer);
					this.editorUpdateTimer = setTimeout(() => {
						const json = JSON.stringify(editor.getJSON());
						const plain = editor.getText();
						const noteId = this.editorNoteId;
						if (!noteId) return;
						if (isDemo) { this.demoUpdateNoteField(noteId, "content", json); this.demoUpdateNoteField(noteId, "content_plain", plain); }
						else { this.updateNoteField(noteId, "content", json); this.updateNoteField(noteId, "contentPlain", plain); }
					}, 800);
				},
			});
		}

		this.wireTitleInput(note, isEditable, isDemo);

		// Wire record button
		this.shadow.getElementById('btn-start-recording')?.addEventListener('click', () => {
			this.startAudioRecording(note, isDemo);
		});
	}

	private async startAudioRecording(note: Note, isDemo: boolean) {
		try {
			const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
			const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
				? 'audio/webm;codecs=opus'
				: MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : 'audio/mp4';

			const audioChunks: Blob[] = [];
			this.audioSegments = [];
			this.audioRecorder = new MediaRecorder(stream, { mimeType });

			this.audioRecorder.ondataavailable = (e) => {
				if (e.data.size > 0) audioChunks.push(e.data);
			};

			// Replace placeholder with recording UI
			const placeholder = this.shadow.querySelector('.audio-record-placeholder');
			if (placeholder) {
				placeholder.innerHTML = `
					<div class="audio-recording-ui">
						<div class="audio-recording-header">
							<div class="recording-pulse-sm"></div>
							<span class="audio-recording-timer">0:00</span>
							<button class="rapp-nav__btn" id="btn-stop-recording" style="background:var(--rs-error,#ef4444)">Stop</button>
						</div>
						<div class="live-transcript-segments audio-live-segments"></div>
					</div>
				`;
			}

			// Start timer
			this.audioRecordingStart = Date.now();
			let elapsed = 0;
			this.audioRecordingTimer = setInterval(() => {
				elapsed = Math.floor((Date.now() - this.audioRecordingStart) / 1000);
				const timerEl = this.shadow.querySelector('.audio-recording-timer');
				if (timerEl) timerEl.textContent = `${Math.floor(elapsed / 60)}:${String(elapsed % 60).padStart(2, '0')}`;
			}, 1000);

			// Start speech dictation with segment tracking
			if (SpeechDictation.isSupported()) {
				this.audioRecordingDictation = new SpeechDictation({
					onInterim: (text) => {
						const idx = this.audioSegments.findIndex(s => !s.isFinal);
						const ts = Math.floor((Date.now() - this.audioRecordingStart) / 1000);
						if (idx >= 0) {
							this.audioSegments[idx].text = text;
						} else {
							this.audioSegments.push({ id: crypto.randomUUID(), text, timestamp: ts, isFinal: false });
						}
						this.renderAudioSegments();
					},
					onFinal: (text) => {
						const idx = this.audioSegments.findIndex(s => !s.isFinal);
						const ts = Math.floor((Date.now() - this.audioRecordingStart) / 1000);
						if (idx >= 0) {
							this.audioSegments[idx] = { ...this.audioSegments[idx], text, isFinal: true };
						} else {
							this.audioSegments.push({ id: crypto.randomUUID(), text, timestamp: ts, isFinal: true });
						}
						this.renderAudioSegments();
					},
				});
				this.audioRecordingDictation.start();
			}

			this.audioRecorder.start(1000);

			// Wire stop button
			this.shadow.getElementById('btn-stop-recording')?.addEventListener('click', async () => {
				// Stop everything
				if (this.audioRecordingTimer) { clearInterval(this.audioRecordingTimer); this.audioRecordingTimer = null; }
				this.audioRecordingDictation?.stop();
				this.audioRecordingDictation?.destroy();
				this.audioRecordingDictation = null;

				this.audioRecorder!.onstop = async () => {
					stream.getTracks().forEach(t => t.stop());
					const blob = new Blob(audioChunks, { type: mimeType });
					const duration = Math.floor((Date.now() - this.audioRecordingStart) / 1000);

					// Upload audio
					let fileUrl = '';
					if (!isDemo) {
						try {
							const base = this.getApiBase();
							const fd = new FormData();
							fd.append('file', blob, 'recording.webm');
							const uploadRes = await fetch(`${base}/api/uploads`, {
								method: 'POST', headers: this.authHeaders(), body: fd,
							});
							if (uploadRes.ok) { fileUrl = (await uploadRes.json()).url; }
						} catch { /* continue without file */ }
					}

					// Convert segments to Tiptap JSON
					const finalSegments = this.audioSegments.filter(s => s.isFinal);
					const fmt = (s: number) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
					let tiptapContent: any = { type: 'doc', content: [{ type: 'paragraph' }] };
					if (finalSegments.length > 0) {
						tiptapContent = {
							type: 'doc',
							content: finalSegments.map(seg => ({
								type: 'paragraph',
								content: [
									{ type: 'text', marks: [{ type: 'code' }], text: `[${fmt(seg.timestamp)}]` },
									{ type: 'text', text: ` ${seg.text}` },
								],
							})),
						};
					}

					const contentJson = JSON.stringify(tiptapContent);
					const contentPlain = finalSegments.map(s => s.text).join(' ');

					// Update note fields
					const noteId = note.id;
					if (isDemo) {
						if (fileUrl) this.demoUpdateNoteField(noteId, 'fileUrl', fileUrl);
						this.demoUpdateNoteField(noteId, 'content', contentJson);
						this.demoUpdateNoteField(noteId, 'content_plain', contentPlain);
					} else {
						if (fileUrl) this.updateNoteField(noteId, 'fileUrl', fileUrl);
						this.updateNoteField(noteId, 'duration', String(duration));
						this.updateNoteField(noteId, 'content', contentJson);
						this.updateNoteField(noteId, 'contentPlain', contentPlain);
						this.updateNoteField(noteId, 'contentFormat', 'tiptap-json');
					}

					// Update local note for immediate display
					(note as any).fileUrl = fileUrl || note.fileUrl;
					(note as any).duration = duration;
					(note as any).content = contentJson;
					(note as any).content_plain = contentPlain;
					(note as any).content_format = 'tiptap-json';

					// Re-mount audio view
					this.audioRecorder = null;
					this.audioSegments = [];
					this.destroyEditor();
					this.editorNoteId = noteId;
					this.mountAudioView(note, true, isDemo);
				};

				if (this.audioRecorder?.state === 'recording') {
					this.audioRecorder.stop();
				}
			});
		} catch (err) {
			console.error('Failed to start audio recording:', err);
		}
	}

	private renderAudioSegments() {
		const container = this.shadow.querySelector('.audio-live-segments');
		if (!container) return;
		const esc = (s: string) => { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; };
		const fmt = (s: number) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;

		container.innerHTML = this.audioSegments.map(seg => `
			<div class="transcript-segment${seg.isFinal ? '' : ' interim'}">
				<span class="segment-time">[${fmt(seg.timestamp)}]</span>
				<span class="segment-text">${esc(seg.text)}</span>
			</div>
		`).join('');
		container.scrollTop = container.scrollHeight;
	}

	/** Shared title input wiring for all editor types */
	private wireTitleInput(note: Note, _isEditable: boolean, isDemo: boolean) {
		const titleInput = this.shadow.getElementById("note-title-input") as HTMLInputElement;
		if (titleInput) {
			let titleTimeout: any;
			titleInput.addEventListener("input", () => {
				clearTimeout(titleTimeout);
				titleTimeout = setTimeout(() => {
					if (isDemo) { this.demoUpdateNoteField(note.id, "title", titleInput.value); }
					else { this.updateNoteField(note.id, "title", titleInput.value); }
				}, 500);
			});
		}
	}

	private destroyEditor() {
		if (this.editorUpdateTimer) {
			clearTimeout(this.editorUpdateTimer);
			this.editorUpdateTimer = null;
		}
		if (this.dictation) {
			this.dictation.destroy();
			this.dictation = null;
		}
		if (this.audioRecordingTimer) {
			clearInterval(this.audioRecordingTimer);
			this.audioRecordingTimer = null;
		}
		if (this.audioRecordingDictation) {
			this.audioRecordingDictation.destroy();
			this.audioRecordingDictation = null;
		}
		if (this.audioRecorder?.state === 'recording') {
			this.audioRecorder.stop();
		}
		this.audioRecorder = null;
		if (this.editor) {
			this.editor.destroy();
			this.editor = null;
		}
		this.editorNoteId = null;
	}

	private renderToolbar(): string {
		const btn = (cmd: string, title: string) =>
			`<button class="toolbar-btn" data-cmd="${cmd}" title="${title}">${ICONS[cmd]}</button>`;
		return `
		<div class="editor-toolbar" id="editor-toolbar">
			<div class="toolbar-group">
				${btn('bold', 'Bold (Ctrl+B)')}
				${btn('italic', 'Italic (Ctrl+I)')}
				${btn('underline', 'Underline (Ctrl+U)')}
				${btn('strike', 'Strikethrough')}
				${btn('code', 'Inline Code')}
			</div>
			<div class="toolbar-sep"></div>
			<div class="toolbar-group">
				<select class="toolbar-select" data-cmd="heading" title="Block type">
					<option value="paragraph">Paragraph</option>
					<option value="1">Heading 1</option>
					<option value="2">Heading 2</option>
					<option value="3">Heading 3</option>
					<option value="4">Heading 4</option>
				</select>
			</div>
			<div class="toolbar-sep"></div>
			<div class="toolbar-group">
				${btn('bulletList', 'Bullet List')}
				${btn('orderedList', 'Numbered List')}
				${btn('taskList', 'Task List')}
			</div>
			<div class="toolbar-sep"></div>
			<div class="toolbar-group">
				${btn('blockquote', 'Blockquote')}
				${btn('codeBlock', 'Code Block')}
				${btn('horizontalRule', 'Divider')}
			</div>
			<div class="toolbar-sep"></div>
			<div class="toolbar-group">
				${btn('link', 'Insert Link')}
				${btn('image', 'Insert Image')}
			</div>
			<div class="toolbar-sep"></div>
			<div class="toolbar-group">
				${btn('undo', 'Undo (Ctrl+Z)')}
				${btn('redo', 'Redo (Ctrl+Y)')}
			</div>
			${SpeechDictation.isSupported() ? `
			<div class="toolbar-sep"></div>
			<div class="toolbar-group">
				${btn('mic', 'Voice Dictation')}
			</div>` : ''}
			<div class="toolbar-sep"></div>
			<div class="toolbar-group">
				${btn('summarize', 'Summarize Note')}
			</div>
		</div>`;
	}

	private showUrlPopover(anchorRect: DOMRect, placeholder: string): Promise<string | null> {
		return new Promise((resolve) => {
			this.shadow.querySelector('.url-popover')?.remove();

			const popover = document.createElement('div');
			popover.className = 'url-popover';

			const hostRect = (this.shadow.host as HTMLElement).getBoundingClientRect();
			popover.style.left = `${anchorRect.left - hostRect.left}px`;
			popover.style.top = `${anchorRect.bottom - hostRect.top + 4}px`;

			const input = document.createElement('input');
			input.type = 'url';
			input.placeholder = placeholder;
			input.className = 'url-popover__input';

			const insertBtn = document.createElement('button');
			insertBtn.textContent = 'Insert';
			insertBtn.className = 'url-popover__btn url-popover__btn--insert';

			const cancelBtn = document.createElement('button');
			cancelBtn.textContent = 'Cancel';
			cancelBtn.className = 'url-popover__btn url-popover__btn--cancel';

			const btnRow = document.createElement('div');
			btnRow.className = 'url-popover__actions';
			btnRow.append(cancelBtn, insertBtn);

			popover.append(input, btnRow);
			this.shadow.appendChild(popover);
			input.focus();

			const cleanup = (value: string | null) => {
				popover.remove();
				resolve(value);
			};

			insertBtn.addEventListener('click', () => {
				const val = input.value.trim();
				cleanup(val || null);
			});

			cancelBtn.addEventListener('click', () => cleanup(null));

			input.addEventListener('keydown', (e) => {
				if (e.key === 'Enter') {
					e.preventDefault();
					const val = input.value.trim();
					cleanup(val || null);
				}
				if (e.key === 'Escape') {
					e.preventDefault();
					cleanup(null);
				}
			});
		});
	}

	private attachToolbarListeners() {
		const toolbar = this.shadow.getElementById('editor-toolbar');
		if (!toolbar || !this.editor) return;

		// Button clicks via event delegation
		toolbar.addEventListener('click', (e) => {
			const btn = (e.target as HTMLElement).closest('[data-cmd]') as HTMLElement;
			if (!btn || btn.tagName === 'SELECT') return;
			e.preventDefault();
			const cmd = btn.dataset.cmd;
			if (!this.editor) return;

			switch (cmd) {
				case 'bold': this.editor.chain().focus().toggleBold().run(); break;
				case 'italic': this.editor.chain().focus().toggleItalic().run(); break;
				case 'underline': this.editor.chain().focus().toggleUnderline().run(); break;
				case 'strike': this.editor.chain().focus().toggleStrike().run(); break;
				case 'code': this.editor.chain().focus().toggleCode().run(); break;
				case 'bulletList': this.editor.chain().focus().toggleBulletList().run(); break;
				case 'orderedList': this.editor.chain().focus().toggleOrderedList().run(); break;
				case 'taskList': this.editor.chain().focus().toggleTaskList().run(); break;
				case 'blockquote': this.editor.chain().focus().toggleBlockquote().run(); break;
				case 'codeBlock': this.editor.chain().focus().toggleCodeBlock().run(); break;
				case 'horizontalRule': this.editor.chain().focus().setHorizontalRule().run(); break;
				case 'link': {
					const rect = btn.getBoundingClientRect();
					this.showUrlPopover(rect, 'Enter link URL...').then(url => {
						if (url) this.editor!.chain().focus().setLink({ href: url }).run();
						else this.editor!.chain().focus().run();
					});
					break;
				}
				case 'image': {
					const rect = btn.getBoundingClientRect();
					this.showUrlPopover(rect, 'Enter image URL...').then(url => {
						if (url) this.editor!.chain().focus().setImage({ src: url }).run();
						else this.editor!.chain().focus().run();
					});
					break;
				}
				case 'undo': this.editor.chain().focus().undo().run(); break;
				case 'redo': this.editor.chain().focus().redo().run(); break;
				case 'mic': this.toggleDictation(btn); break;
				case 'summarize': this.summarizeNote(btn); break;
			}
		});

		// Heading select
		const headingSelect = toolbar.querySelector('[data-cmd="heading"]') as HTMLSelectElement;
		if (headingSelect) {
			headingSelect.addEventListener('change', () => {
				if (!this.editor) return;
				const val = headingSelect.value;
				if (val === 'paragraph') {
					this.editor.chain().focus().setParagraph().run();
				} else {
					this.editor.chain().focus().setHeading({ level: parseInt(val) as 1 | 2 | 3 | 4 }).run();
				}
			});
		}
	}

	private toggleDictation(btn: HTMLElement) {
		if (this.dictation?.isRecording) {
			this.dictation.stop();
			btn.classList.remove('recording');
			this.removeDictationPreview();
			return;
		}
		if (!this.dictation) {
			this.dictation = new SpeechDictation({
				onInterim: (text) => {
					this.updateDictationPreview(text);
				},
				onFinal: (text) => {
					this.removeDictationPreview();
					if (this.editor) {
						this.editor.chain().focus().insertContent(text + ' ').run();
					}
				},
				onStateChange: (recording) => {
					btn.classList.toggle('recording', recording);
					if (!recording) this.removeDictationPreview();
				},
				onError: (err) => {
					console.warn('[Dictation]', err);
					btn.classList.remove('recording');
					this.removeDictationPreview();
				},
			});
		}
		this.dictation.start();
	}

	private updateDictationPreview(text: string) {
		let preview = this.shadow.getElementById('dictation-preview');
		if (!preview) {
			preview = document.createElement('div');
			preview.id = 'dictation-preview';
			preview.className = 'dictation-preview';
			const toolbar = this.shadow.getElementById('editor-toolbar');
			if (toolbar) toolbar.insertAdjacentElement('afterend', preview);
			else return;
		}
		preview.textContent = text;
	}

	private removeDictationPreview() {
		this.shadow.getElementById('dictation-preview')?.remove();
	}

	private updateToolbarState() {
		if (!this.editor) return;
		const toolbar = this.shadow.getElementById('editor-toolbar');
		if (!toolbar) return;

		// Toggle active class on buttons
		toolbar.querySelectorAll('.toolbar-btn[data-cmd]').forEach((btn) => {
			const cmd = (btn as HTMLElement).dataset.cmd!;
			let isActive = false;
			switch (cmd) {
				case 'bold': isActive = this.editor!.isActive('bold'); break;
				case 'italic': isActive = this.editor!.isActive('italic'); break;
				case 'underline': isActive = this.editor!.isActive('underline'); break;
				case 'strike': isActive = this.editor!.isActive('strike'); break;
				case 'code': isActive = this.editor!.isActive('code'); break;
				case 'bulletList': isActive = this.editor!.isActive('bulletList'); break;
				case 'orderedList': isActive = this.editor!.isActive('orderedList'); break;
				case 'taskList': isActive = this.editor!.isActive('taskList'); break;
				case 'blockquote': isActive = this.editor!.isActive('blockquote'); break;
				case 'codeBlock': isActive = this.editor!.isActive('codeBlock'); break;
			}
			btn.classList.toggle('active', isActive);
		});

		// Update heading select
		const headingSelect = toolbar.querySelector('[data-cmd="heading"]') as HTMLSelectElement;
		if (headingSelect) {
			if (this.editor.isActive('heading', { level: 1 })) headingSelect.value = '1';
			else if (this.editor.isActive('heading', { level: 2 })) headingSelect.value = '2';
			else if (this.editor.isActive('heading', { level: 3 })) headingSelect.value = '3';
			else if (this.editor.isActive('heading', { level: 4 })) headingSelect.value = '4';
			else headingSelect.value = 'paragraph';
		}
	}

	// ── Helpers ──

	private getNoteIcon(type: string): string {
		switch (type) {
			case "NOTE": return "\u{1F4DD}";
			case "CODE": return "\u{1F4BB}";
			case "BOOKMARK": return "\u{1F517}";
			case "IMAGE": return "\u{1F5BC}";
			case "AUDIO": return "\u{1F3A4}";
			case "FILE": return "\u{1F4CE}";
			case "CLIP": return "\u2702\uFE0F";
			default: return "\u{1F4C4}";
		}
	}

	private formatDate(dateStr: string): string {
		const d = new Date(dateStr);
		const now = new Date();
		const diffMs = now.getTime() - d.getTime();
		const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
		if (diffDays === 0) return "Today";
		if (diffDays === 1) return "Yesterday";
		if (diffDays < 7) return `${diffDays}d ago`;
		return d.toLocaleDateString();
	}

	// ── Rendering ──

	private render() {
		this.renderNav();
		if (this.view === 'note' && this.editor && this.editorNoteId) {
			// Editor already mounted — don't touch contentZone
		} else {
			this.renderContent();
		}
		this.renderMeta();
		this.attachListeners();
		this._tour.renderOverlay();
	}

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

	private renderNav() {
		const isDemo = this.space === "demo";

		if (this.view === "note" && this.selectedNote) {
			// Nav is handled by mountEditor's content, or we just show back button
			this.navZone.innerHTML = `
				<div class="rapp-nav">
					${this._history.canGoBack ? `<button class="rapp-nav__back" data-back="prev">
						\u2190 ${this.selectedNotebook ? this.esc(this.selectedNotebook.title) : "Notebooks"}
					</button>` : ''}
					<span style="flex:1"></span>
				</div>`;
			// Re-attach back listener
			this.navZone.querySelectorAll('[data-back]').forEach((el) => {
				el.addEventListener('click', (e) => {
					e.stopPropagation();
					this.goBack();
				});
			});
			return;
		}

		if (this.view === "notebook" && this.selectedNotebook) {
			const nb = this.selectedNotebook;
			const syncBadge = this.subscribedDocId
				? `<span class="sync-badge ${this.syncConnected ? "connected" : "disconnected"}" title="${this.syncConnected ? "Live sync" : "Reconnecting..."}"></span>`
				: "";
			const filterTypes: { label: string; value: NoteType | '' }[] = [
				{ label: 'All', value: '' },
				{ label: 'Notes', value: 'NOTE' },
				{ label: 'Code', value: 'CODE' },
				{ label: 'Bookmarks', value: 'BOOKMARK' },
				{ label: 'Clips', value: 'CLIP' },
				{ label: 'Audio', value: 'AUDIO' },
			];
			this.navZone.innerHTML = `
				<div class="rapp-nav">
					${this._history.canGoBack ? '<button class="rapp-nav__back" data-back="notebooks">\u2190 Notebooks</button>' : ''}
					<span class="rapp-nav__title">${this.esc(nb.title)}${syncBadge}</span>
					<button class="rapp-nav__btn rapp-nav__btn--secondary" id="btn-export-notebook" title="Export this notebook">
						<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 10V2M5 5l3-3 3 3"/><path d="M2 12v2h12v-2"/></svg>
						Export
					</button>
					<div class="new-note-split">
						<button class="rapp-nav__btn" id="create-note">+ New Note</button>
						<button class="rapp-nav__btn new-note-dropdown-btn" id="new-note-dropdown-toggle">\u25BE</button>
						<div class="new-note-dropdown" id="new-note-dropdown" style="display:none">
							<div class="new-note-dropdown-item" data-create-type="CODE">\u{1F4BB} Code Snippet</div>
							<div class="new-note-dropdown-item" data-create-type="BOOKMARK">\u{1F517} Bookmark</div>
							<div class="new-note-dropdown-item" data-create-type="CLIP">\u2702\uFE0F Clip</div>
							<div class="new-note-dropdown-item" data-create-type="AUDIO">\u{1F3A4} Voice Note</div>
							<div class="new-note-dropdown-item" data-create-type="IMAGE">\u{1F5BC}\uFE0F Image</div>
						</div>
					</div>
				</div>
				<div class="type-filter-bar">
					${filterTypes.map(f => `<button class="type-filter-pill${this.typeFilter === f.value ? ' active' : ''}" data-type-filter="${f.value}">${f.label}</button>`).join('')}
				</div>`;

			// Wire type filter pills
			this.navZone.querySelectorAll('[data-type-filter]').forEach(el => {
				el.addEventListener('click', () => {
					this.typeFilter = ((el as HTMLElement).dataset.typeFilter || '') as NoteType | '';
					this.renderNav();
					this.renderContent();
					this.attachListeners();
				});
			});

			// Wire new note dropdown
			const toggleBtn = this.navZone.querySelector('#new-note-dropdown-toggle');
			const dropdown = this.navZone.querySelector('#new-note-dropdown') as HTMLElement;
			if (toggleBtn && dropdown) {
				toggleBtn.addEventListener('click', (e) => {
					e.stopPropagation();
					dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
				});
				dropdown.querySelectorAll('[data-create-type]').forEach(el => {
					el.addEventListener('click', () => {
						const type = (el as HTMLElement).dataset.createType as NoteType;
						dropdown.style.display = 'none';
						if (this.space === 'demo') {
							this.demoCreateNote({ type });
						} else {
							this.createNoteViaSync({ type });
						}
					});
				});
				// Close dropdown on outside click
				document.addEventListener('click', () => { dropdown.style.display = 'none'; }, { once: true });
			}

			return;
		}

		// Notebooks view
		this.navZone.innerHTML = `
			<div class="rapp-nav">
				<span class="rapp-nav__title">Notebooks</span>
				<button class="rapp-nav__btn rapp-nav__btn--secondary" id="btn-import-export" title="Import / Export">
					<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v8M5 7l3 3 3-3"/><path d="M2 12v2h12v-2"/></svg>
					Import / Export
				</button>
				<button class="rapp-nav__btn" id="create-notebook">+ New Notebook</button>
				<button class="rapp-nav__btn rapp-nav__btn--secondary" id="btn-tour" title="Guided Tour">Tour</button>
			</div>
			<input class="search-bar" type="text" placeholder="Search notes..." id="search-input" value="${this.esc(this.searchQuery)}">`;
	}

	private renderContent() {
		if (this.error) {
			this.contentZone.innerHTML = `<div class="error">${this.esc(this.error)}</div>`;
			return;
		}
		if (this.loading) {
			this.contentZone.innerHTML = '<div class="loading">Loading...</div>';
			return;
		}

		if (this.view === "notebook" && this.selectedNotebook) {
			const nb = this.selectedNotebook;
			const filtered = this.typeFilter
				? nb.notes.filter(n => n.type === this.typeFilter)
				: nb.notes;
			this.contentZone.innerHTML = filtered.length > 0
				? filtered.map((n) => this.renderNoteItem(n)).join("")
				: `<div class="empty">${this.typeFilter ? `No ${this.typeFilter.toLowerCase()} notes in this notebook.` : 'No notes in this notebook.'}</div>`;
			return;
		}

		// Notebooks view
		let html = '';
		if (this.searchQuery && this.searchResults.length > 0) {
			html += `<div class="search-results-info">${this.searchResults.length} results for "${this.esc(this.searchQuery)}"</div>`;
			html += this.searchResults.map((n) => this.renderNoteItem(n)).join("");
		}
		if (!this.searchQuery) {
			html += `<div class="grid">${this.notebooks.map((nb) => `
				<div class="notebook-card" data-notebook="${nb.id}">
					<div class="notebook-card__accent" style="background:${nb.cover_color}"></div>
					<div class="notebook-card__body">
						<div class="notebook-card__title">${this.esc(nb.title)}</div>
						<div class="notebook-card__desc">${this.esc(nb.description || "")}</div>
					</div>
					<div class="notebook-card__footer">
						<span>${nb.note_count} notes</span>
						<span>${this.formatDate(nb.updated_at)}</span>
					</div>
				</div>
			`).join("")}</div>`;
			if (this.notebooks.length === 0) {
				html += '<div class="empty">No notebooks yet. Create one to get started.</div>';
			}
		}
		this.contentZone.innerHTML = html;
	}

	private renderMeta() {
		if (this.view === "note" && this.selectedNote) {
			const n = this.selectedNote;
			const isAutomerge = !!(this.doc?.items?.[n.id]);
			const isDemo = this.space === "demo";

			// Get summary from Automerge doc or local note object
			const item = this.doc?.items?.[n.id];
			const summary = item?.summary || (n as any).summary || '';
			const summaryModel = item?.summaryModel || (n as any).summaryModel || '';
			const openNotebookSourceId = item?.openNotebookSourceId || (n as any).openNotebookSourceId || '';

			this.metaZone.innerHTML = `
				<div class="note-meta-bar">
					<span>Type: ${n.type}</span>
					<span>Created: ${this.formatDate(n.created_at)}</span>
					<span>Updated: ${this.formatDate(n.updated_at)}</span>
					${n.tags ? n.tags.map((t) => `<span class="tag">${this.esc(t)}</span>`).join("") : ""}
					${isAutomerge ? '<span class="meta-live">Live</span>' : ""}
					${isDemo ? '<span class="meta-demo">Demo</span>' : ""}
				</div>
				${summary ? `
				<div class="note-summary-panel">
					<div class="note-summary-header" data-action="toggle-summary">
						<span class="note-summary-icon">${ICONS.summarize}</span>
						<span>Summary</span>
						<span class="note-summary-model">${this.esc(summaryModel)}</span>
						<button class="note-summary-regen" data-action="regenerate-summary" title="Regenerate summary">\u{21BB}</button>
						<span class="note-summary-chevron">\u{25BC}</span>
					</div>
					<div class="note-summary-body">${this.esc(summary)}</div>
				</div>` : ''}
				${!isDemo ? `
				<div class="note-actions-bar">
					<button class="note-action-btn" data-action="send-to-notebook" ${openNotebookSourceId ? 'disabled' : ''}>
						<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v8M5 7l3-3 3 3"/><path d="M2 12v2h12v-2"/></svg>
						${openNotebookSourceId ? 'Sent to Notebook' : 'Send to Notebook'}
					</button>
					${openNotebookSourceId ? '<span class="note-action-badge">Indexed</span>' : ''}
				</div>` : ''}`;

			// Attach summary panel event listeners
			if (summary) {
				const header = this.metaZone.querySelector('[data-action="toggle-summary"]');
				header?.addEventListener('click', (e) => {
					if ((e.target as HTMLElement).closest('[data-action="regenerate-summary"]')) return;
					const panel = this.metaZone.querySelector('.note-summary-panel');
					panel?.classList.toggle('collapsed');
				});
				const regen = this.metaZone.querySelector('[data-action="regenerate-summary"]');
				regen?.addEventListener('click', () => {
					const btn = this.shadow.querySelector('[data-cmd="summarize"]') as HTMLElement;
					if (btn) this.summarizeNote(btn);
				});
			}

			// Send to Notebook button
			const sendBtn = this.metaZone.querySelector('[data-action="send-to-notebook"]') as HTMLElement;
			if (sendBtn && !openNotebookSourceId) {
				sendBtn.addEventListener('click', () => this.sendToOpenNotebook());
			}
		} else {
			this.metaZone.innerHTML = '';
		}
	}

	private renderNoteItem(n: Note): string {
		let typeDetail = '';
		switch (n.type) {
			case 'BOOKMARK':
			case 'CLIP': {
				if (n.url) {
					try { typeDetail = `<span class="note-item__badge badge-url">${new URL(n.url).hostname}</span>`; } catch { typeDetail = ''; }
				}
				break;
			}
			case 'CODE':
				if (n.language) typeDetail = `<span class="note-item__badge badge-lang">${this.esc(n.language)}</span>`;
				break;
			case 'AUDIO':
				if (n.duration) {
					const m = Math.floor(n.duration / 60);
					const s = String(n.duration % 60).padStart(2, '0');
					typeDetail = `<span class="note-item__badge badge-duration">${m}:${s}</span>`;
				}
				break;
		}

		const typeBorder = this.getTypeBorderColor(n.type);

		return `
			<div class="note-item" data-note="${n.id}" data-collab-id="note:${n.id}" style="border-left: 3px solid ${typeBorder}">
				<span class="note-item__icon">${this.getNoteIcon(n.type)}</span>
				<div class="note-item__body">
					<div class="note-item__title">${n.is_pinned ? '<span class="note-item__pin">\u{1F4CC}</span> ' : ""}${this.esc(n.title)}</div>
					<div class="note-item__preview">${this.esc(n.content_plain || "")}</div>
					<div class="note-item__meta">
						<span>${this.formatDate(n.updated_at)}</span>
						${typeDetail}
						<span>${n.type}</span>
						${n.tags ? n.tags.map((t) => `<span class="tag">${this.esc(t)}</span>`).join("") : ""}
					</div>
				</div>
			</div>
		`;
	}

	private getTypeBorderColor(type: string): string {
		switch (type) {
			case 'NOTE': return 'var(--rs-primary, #6366f1)';
			case 'CODE': return '#10b981';
			case 'BOOKMARK': return '#f59e0b';
			case 'CLIP': return '#8b5cf6';
			case 'IMAGE': return '#ec4899';
			case 'AUDIO': return '#ef4444';
			case 'FILE': return '#6b7280';
			default: return 'var(--rs-border, #e5e7eb)';
		}
	}

	private attachListeners() {
		const isDemo = this.space === "demo";

		// Import / Export button
		this.shadow.getElementById("btn-import-export")?.addEventListener("click", () => {
			this.openImportExportDialog();
		});

		// Create notebook
		this.shadow.getElementById("create-notebook")?.addEventListener("click", () => {
			isDemo ? this.demoCreateNotebook() : this.createNotebook();
		});

		// Tour button
		this.shadow.getElementById("btn-tour")?.addEventListener("click", () => this.startTour());

		// Export notebook button (in notebook detail view)
		this.shadow.getElementById("btn-export-notebook")?.addEventListener("click", () => {
			this.openImportExportDialog('export');
		});

		// Create note
		this.shadow.getElementById("create-note")?.addEventListener("click", () => {
			isDemo ? this.demoCreateNote() : this.createNoteViaSync();
		});

		// Search
		const searchInput = this.shadow.getElementById("search-input") as HTMLInputElement;
		let searchTimeout: any;
		searchInput?.addEventListener("input", () => {
			clearTimeout(searchTimeout);
			this.searchQuery = searchInput.value;
			searchTimeout = setTimeout(() => {
				isDemo ? this.demoSearchNotes(this.searchQuery) : this.searchNotes(this.searchQuery);
			}, 300);
		});

		// Notebook cards
		this.shadow.querySelectorAll("[data-notebook]").forEach((el) => {
			el.addEventListener("click", () => {
				const id = (el as HTMLElement).dataset.notebook!;
				this._history.push(this.view);
				this._history.push("notebook", { notebookId: id });
				this.view = "notebook";
				isDemo ? this.demoLoadNotebook(id) : this.loadNotebook(id);
			});
		});

		// Note items
		this.shadow.querySelectorAll("[data-note]").forEach((el) => {
			el.addEventListener("click", () => {
				const id = (el as HTMLElement).dataset.note!;
				this._history.push(this.view);
				this._history.push("note", { noteId: id });
				this.view = "note";
				isDemo ? this.demoLoadNote(id) : this.loadNote(id);
			});
		});

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

	private goBack() {
		this.destroyEditor();
		const prev = this._history.back();
		if (!prev) return;
		this.view = prev.view;
		if (prev.view === "notebooks") {
			if (this.space !== "demo") this.unsubscribeNotebook();
			this.selectedNotebook = null;
			this.selectedNote = null;
		} else if (prev.view === "notebook") {
			this.selectedNote = null;
		}
		this.render();
	}

	private demoUpdateNoteField(noteId: string, field: string, value: string) {
		if (this.selectedNote && this.selectedNote.id === noteId) {
			(this.selectedNote as any)[field] = value;
			this.selectedNote.updated_at = new Date().toISOString();
		}
		for (const nb of this.demoNotebooks) {
			const note = nb.notes.find(n => n.id === noteId);
			if (note) {
				(note as any)[field] = value;
				note.updated_at = new Date().toISOString();
				break;
			}
		}
		if (this.selectedNotebook?.notes) {
			const note = this.selectedNotebook.notes.find(n => n.id === noteId);
			if (note) {
				(note as any)[field] = value;
				note.updated_at = new Date().toISOString();
			}
		}
	}

	// ── Import/Export Dialog ──

	private importExportDialog: ImportExportDialog | null = null;

	private openImportExportDialog(tab: 'import' | 'export' = 'import') {
		if (!this.importExportDialog) {
			// Dynamically import the dialog component
			import('./import-export-dialog').then(() => {
				this.importExportDialog = document.createElement('import-export-dialog') as unknown as ImportExportDialog;
				this.importExportDialog.setAttribute('space', this.space);
				this.shadow.appendChild(this.importExportDialog);

				this.importExportDialog.addEventListener('import-complete', () => {
					// Refresh notebooks list after import
					if (this.space === 'demo') {
						this.loadDemoData();
					} else {
						this.loadNotebooks();
					}
				});

				this.showDialog(tab);
			});
		} else {
			this.showDialog(tab);
		}
	}

	private showDialog(tab: 'import' | 'export') {
		if (!this.importExportDialog) return;

		// Gather notebook list for the dialog
		const notebooks = this.notebooks.map(nb => ({
			id: nb.id,
			title: nb.title,
		}));

		this.importExportDialog.open(notebooks, tab);
	}

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

	private getStyles(): string {
		return `
			:host { display: block; font-family: system-ui, -apple-system, sans-serif; color: var(--rs-text-primary); }
			* { box-sizing: border-box; }

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

			/* ── Search ── */
			.search-bar {
				width: 100%; padding: 10px 14px; border-radius: 8px;
				border: 1px solid var(--rs-input-border); background: var(--rs-input-bg); color: var(--rs-input-text);
				font-size: 14px; margin-bottom: 16px; transition: border-color 0.15s;
			}
			.search-bar:focus { border-color: var(--rs-primary); outline: none; }
			.search-results-info { margin-bottom: 12px; font-size: 13px; color: var(--rs-text-secondary); }

			/* ── Notebook Grid ── */
			.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
			.notebook-card {
				position: relative; overflow: hidden;
				border-radius: 12px; padding: 16px 16px 16px 20px; cursor: pointer;
				border: 1px solid var(--rs-card-border); background: var(--rs-card-bg);
				transition: border-color 0.2s, box-shadow 0.2s, transform 0.15s;
				min-height: 120px; display: flex; flex-direction: column; justify-content: space-between;
			}
			.notebook-card:hover { border-color: var(--rs-border); box-shadow: var(--rs-shadow-sm); transform: translateY(-1px); }
			.notebook-card__accent { position: absolute; top: 0; left: 0; width: 4px; height: 100%; border-radius: 12px 0 0 12px; }
			.notebook-card__body { flex: 1; }
			.notebook-card__title { font-size: 15px; font-weight: 600; margin-bottom: 4px; color: var(--rs-text-primary); }
			.notebook-card__desc { font-size: 12px; color: var(--rs-text-muted); line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
			.notebook-card__footer { display: flex; justify-content: space-between; font-size: 11px; color: var(--rs-text-muted); margin-top: 8px; }

			/* ── Note Items ── */
			.note-item {
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border-subtle); border-radius: 10px;
				padding: 14px 16px; margin-bottom: 8px; cursor: pointer;
				transition: border-color 0.15s, box-shadow 0.15s;
				display: flex; gap: 12px; align-items: flex-start;
			}
			.note-item:hover { border-color: var(--rs-border); box-shadow: var(--rs-shadow-sm); }
			.note-item__icon {
				font-size: 18px; flex-shrink: 0; width: 32px; height: 32px;
				display: flex; align-items: center; justify-content: center;
				background: var(--rs-bg-surface-raised); border-radius: 8px;
			}
			.note-item__body { flex: 1; min-width: 0; }
			.note-item__title { font-size: 14px; font-weight: 600; color: var(--rs-text-primary); }
			.note-item__pin { color: var(--rs-warning); }
			.note-item__preview {
				font-size: 12px; color: var(--rs-text-muted); margin-top: 3px; line-height: 1.4;
				overflow: hidden; text-overflow: ellipsis;
				display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
			}
			.note-item__meta { font-size: 11px; color: var(--rs-text-muted); margin-top: 6px; display: flex; gap: 8px; align-items: center; }
			.tag { display: inline-block; padding: 1px 6px; border-radius: 3px; background: var(--rs-bg-surface-raised); color: var(--rs-text-secondary); font-size: 10px; }

			/* ── Type Filter Bar ── */
			.type-filter-bar { display: flex; gap: 6px; margin-bottom: 12px; flex-wrap: wrap; }
			.type-filter-pill {
				padding: 4px 12px; border-radius: 16px; border: 1px solid var(--rs-border);
				background: transparent; color: var(--rs-text-secondary); font-size: 12px;
				cursor: pointer; transition: all 0.15s; font-family: inherit;
			}
			.type-filter-pill:hover { border-color: var(--rs-border-strong); color: var(--rs-text-primary); }
			.type-filter-pill.active { background: var(--rs-primary); color: #fff; border-color: var(--rs-primary); }

			/* ── New Note Split Button ── */
			.new-note-split { display: flex; position: relative; }
			.new-note-split .rapp-nav__btn:first-child { border-radius: 6px 0 0 6px; }
			.new-note-dropdown-btn {
				border-radius: 0 6px 6px 0 !important; padding: 6px 8px !important;
				border-left: 1px solid rgba(255,255,255,0.2) !important; min-width: 28px;
			}
			.new-note-dropdown {
				position: absolute; top: 100%; right: 0; margin-top: 4px; z-index: 50;
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border);
				border-radius: 8px; box-shadow: var(--rs-shadow-md); min-width: 180px;
				overflow: hidden;
			}
			.new-note-dropdown-item {
				padding: 8px 14px; cursor: pointer; font-size: 13px;
				color: var(--rs-text-primary); transition: background 0.1s;
			}
			.new-note-dropdown-item:hover { background: var(--rs-bg-hover); }

			/* ── Note Item Type Badges ── */
			.note-item__badge {
				display: inline-block; padding: 1px 6px; border-radius: 3px;
				font-size: 10px; font-weight: 500;
			}
			.badge-url { background: rgba(245, 158, 11, 0.15); color: #d97706; }
			.badge-lang { background: rgba(16, 185, 129, 0.15); color: #059669; }
			.badge-duration { background: rgba(239, 68, 68, 0.15); color: #dc2626; }

			/* ── Code Editor ── */
			.code-editor-controls { padding: 4px 12px; display: flex; gap: 8px; align-items: center; }
			.code-textarea {
				width: 100%; min-height: 400px; padding: 16px 20px; border: none; outline: none;
				font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
				font-size: 13px; line-height: 1.6; tab-size: 4; resize: vertical;
				background: var(--rs-bg-surface-sunken); color: var(--rs-text-primary);
			}

			/* ── Bookmark / Clip View ── */
			.bookmark-card {
				display: flex; gap: 12px; align-items: center;
				padding: 12px 16px; margin: 0 12px 8px;
				background: var(--rs-bg-surface-raised); border-radius: 8px;
			}
			.bookmark-favicon { border-radius: 4px; flex-shrink: 0; }
			.bookmark-info { flex: 1; min-width: 0; }
			.bookmark-url { color: var(--rs-primary); font-size: 13px; text-decoration: none; }
			.bookmark-url:hover { text-decoration: underline; }
			.bookmark-url-input-row { margin-top: 4px; }
			.bookmark-url-input {
				width: 100%; padding: 6px 10px; border-radius: 6px;
				border: 1px solid var(--rs-input-border); background: var(--rs-input-bg);
				color: var(--rs-input-text); font-size: 12px; font-family: inherit;
			}

			/* ── Image View ── */
			.image-display { padding: 12px 16px; text-align: center; }
			.image-preview { max-width: 100%; max-height: 500px; border-radius: 8px; border: 1px solid var(--rs-border-subtle); }
			.image-upload-placeholder { padding: 16px; }

			/* ── Audio View ── */
			.audio-player-container {
				display: flex; gap: 12px; align-items: center;
				padding: 12px 16px; margin: 0 12px;
			}
			.audio-player { flex: 1; max-width: 100%; height: 40px; }
			.audio-duration { font-size: 13px; color: var(--rs-text-muted); font-weight: 500; }
			.audio-record-placeholder { padding: 24px; text-align: center; }
			.audio-recording-ui { text-align: left; }
			.audio-recording-header {
				display: flex; align-items: center; gap: 12px; margin-bottom: 8px;
			}
			.recording-pulse-sm {
				width: 12px; height: 12px; border-radius: 50%;
				background: var(--rs-error, #ef4444); animation: pulse-recording 1.5s infinite;
			}
			.audio-recording-timer {
				font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums;
				color: var(--rs-text-primary);
			}
			.audio-live-segments {
				max-height: 200px; overflow-y: auto; padding: 4px 0;
			}
			.transcript-segment {
				display: flex; gap: 8px; padding: 3px 0; font-size: 14px; line-height: 1.6;
			}
			.transcript-segment.interim { font-style: italic; color: var(--rs-text-muted); }
			.segment-time {
				flex-shrink: 0; font-family: 'JetBrains Mono', 'Fira Code', monospace;
				font-size: 12px; color: var(--rs-text-muted); padding-top: 2px;
			}
			.segment-text { flex: 1; }
			.audio-transcript-section { padding: 0 4px; }
			.audio-transcript-label {
				font-size: 12px; font-weight: 600; text-transform: uppercase;
				letter-spacing: 0.05em; color: var(--rs-text-muted);
				padding: 8px 16px 0;
			}

			/* ── Editor Title ── */
			.editable-title {
				background: transparent; border: none; border-bottom: 2px solid transparent;
				color: var(--rs-text-primary); font-family: inherit;
				font-size: 22px; font-weight: 700; width: 100%; outline: none;
				padding: 8px 0; margin-bottom: 4px; transition: border-color 0.15s;
			}
			.editable-title:focus { border-bottom-color: var(--rs-primary); }
			.editable-title::placeholder { color: var(--rs-text-muted); }

			/* ── Sync Badge ── */
			.sync-badge {
				display: inline-block; width: 8px; height: 8px; border-radius: 50%;
				margin-left: 8px; vertical-align: middle;
			}
			.sync-badge.connected { background: var(--rs-success); }
			.sync-badge.disconnected { background: var(--rs-error); }

			/* ── State Messages ── */
			.empty { text-align: center; color: var(--rs-text-muted); padding: 40px; }
			.loading { text-align: center; color: var(--rs-text-secondary); padding: 40px; }
			.error { text-align: center; color: var(--rs-error); padding: 20px; }

			/* ── Meta Bar ── */
			.note-meta-bar {
				margin-top: 12px; font-size: 12px; color: var(--rs-text-muted);
				display: flex; gap: 12px; padding: 8px 0; align-items: center;
			}
			.meta-live { color: var(--rs-success); font-weight: 500; }
			.meta-demo { color: var(--rs-warning); font-weight: 500; }

			/* ── Summary Panel ── */
			.note-summary-panel {
				margin-top: 8px; border: 1px solid var(--rs-border-subtle);
				border-radius: 8px; background: var(--rs-bg-surface); overflow: hidden;
			}
			.note-summary-header {
				display: flex; align-items: center; gap: 6px; padding: 8px 12px;
				font-size: 12px; font-weight: 500; color: var(--rs-text-secondary);
				cursor: pointer; user-select: none;
			}
			.note-summary-header:hover { background: var(--rs-bg-surface-raised); }
			.note-summary-icon { display: flex; align-items: center; }
			.note-summary-icon svg { width: 14px; height: 14px; }
			.note-summary-model {
				font-size: 10px; color: var(--rs-text-muted); background: var(--rs-bg-surface-raised);
				padding: 1px 6px; border-radius: 3px; margin-left: auto;
			}
			.note-summary-regen {
				background: none; border: none; color: var(--rs-text-muted); cursor: pointer;
				font-size: 14px; padding: 0 4px; border-radius: 3px; transition: all 0.15s;
			}
			.note-summary-regen:hover { color: var(--rs-primary); background: var(--rs-bg-surface-raised); }
			.note-summary-chevron { font-size: 8px; color: var(--rs-text-muted); transition: transform 0.2s; }
			.note-summary-body {
				padding: 8px 12px 12px; font-size: 13px; line-height: 1.6;
				color: var(--rs-text-primary); border-top: 1px solid var(--rs-border-subtle);
				white-space: pre-wrap;
			}
			.note-summary-panel.collapsed .note-summary-body { display: none; }
			.note-summary-panel.collapsed .note-summary-chevron { transform: rotate(-90deg); }

			/* ── Note Actions Bar ── */
			.note-actions-bar {
				display: flex; align-items: center; gap: 8px; margin-top: 8px;
			}
			.note-action-btn {
				display: flex; align-items: center; gap: 6px;
				padding: 6px 14px; border-radius: 6px; border: 1px solid var(--rs-border);
				background: transparent; color: var(--rs-text-secondary); font-size: 12px;
				font-weight: 500; cursor: pointer; transition: all 0.15s; font-family: inherit;
			}
			.note-action-btn:hover:not(:disabled) { border-color: var(--rs-primary); color: var(--rs-primary); }
			.note-action-btn:disabled { opacity: 0.6; cursor: default; }
			.note-action-btn svg { flex-shrink: 0; }
			.note-action-badge {
				display: inline-flex; align-items: center; gap: 4px;
				padding: 3px 10px; border-radius: 12px;
				background: color-mix(in srgb, var(--rs-success, #22c55e) 15%, transparent);
				color: var(--rs-success, #22c55e); font-size: 11px; font-weight: 600;
			}

			/* ── Editor Toolbar ── */
			.editor-toolbar {
				display: flex; flex-wrap: wrap; gap: 2px; align-items: center;
				background: var(--rs-toolbar-bg); border: 1px solid var(--rs-toolbar-panel-border);
				border-radius: 8px; padding: 4px 6px; margin-bottom: 2px;
			}
			.toolbar-group { display: flex; gap: 1px; }
			.toolbar-sep { width: 1px; height: 20px; background: var(--rs-toolbar-sep); margin: 0 4px; }
			.toolbar-btn {
				display: flex; align-items: center; justify-content: center;
				width: 30px; height: 28px; border: none; border-radius: 4px;
				background: transparent; color: var(--rs-text-secondary); cursor: pointer;
				font-size: 13px; font-family: inherit; transition: all 0.15s;
			}
			.toolbar-btn svg { width: 16px; height: 16px; flex-shrink: 0; }
			.toolbar-btn:hover { background: var(--rs-toolbar-btn-hover); color: var(--rs-toolbar-btn-text); }
			.toolbar-btn.active { background: var(--rs-primary); color: #fff; }
			.toolbar-btn.recording { background: var(--rs-error, #ef4444); color: #fff; animation: pulse-recording 1.5s infinite; }
			@keyframes pulse-recording { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }

			/* ── Dictation Preview ── */
			.dictation-preview {
				padding: 6px 12px; margin: 0 8px 2px; border-radius: 6px;
				background: var(--rs-bg-surface-raised); color: var(--rs-text-muted);
				font-size: 13px; font-style: italic; line-height: 1.5;
				animation: dictation-pulse 2s ease-in-out infinite;
				border-left: 3px solid var(--rs-error, #ef4444);
			}
			@keyframes dictation-pulse {
				0%, 100% { background: var(--rs-bg-surface-raised); }
				50% { background: color-mix(in srgb, var(--rs-error, #ef4444) 8%, var(--rs-bg-surface-raised)); }
			}
			.toolbar-select {
				padding: 2px 4px; border-radius: 4px; border: 1px solid var(--rs-toolbar-panel-border);
				background: var(--rs-toolbar-bg); color: var(--rs-text-secondary); font-size: 12px; cursor: pointer;
				font-family: inherit; transition: border-color 0.15s;
			}
			.toolbar-select:focus { outline: none; border-color: var(--rs-primary); }

			/* ── Tiptap Editor ── */
			.editor-wrapper {
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border-subtle);
				border-radius: 10px; overflow: hidden;
			}
			.editor-wrapper .editable-title { padding: 16px 20px 0; }
			.editor-wrapper .editor-toolbar { margin: 4px 8px; border-radius: 6px; }

			.tiptap-container .tiptap {
				min-height: 300px; padding: 20px 24px; outline: none;
				font-size: 15px; line-height: 1.75; color: var(--rs-text-primary);
			}
			.tiptap-container .tiptap:focus { outline: none; }

			/* ── Prose Styles ── */
			.tiptap-container .tiptap h1 {
				font-size: 1.75em; font-weight: 700; margin: 1.2em 0 0.5em; color: var(--rs-text-primary);
				padding-bottom: 0.3em; border-bottom: 1px solid var(--rs-border-subtle);
			}
			.tiptap-container .tiptap h2 { font-size: 1.35em; font-weight: 600; margin: 1em 0 0.4em; color: var(--rs-text-primary); }
			.tiptap-container .tiptap h3 { font-size: 1.1em; font-weight: 600; margin: 0.8em 0 0.3em; color: var(--rs-text-secondary); }
			.tiptap-container .tiptap h4 {
				font-size: 0.95em; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
				margin: 0.7em 0 0.25em; color: var(--rs-text-muted);
			}
			.tiptap-container .tiptap p { margin: 0.5em 0; }
			.tiptap-container .tiptap blockquote {
				border-left: 3px solid var(--rs-primary); padding: 4px 0 4px 16px; margin: 0.8em 0;
				color: var(--rs-text-secondary); font-style: italic;
				background: var(--rs-bg-surface-raised); border-radius: 0 6px 6px 0;
			}
			.tiptap-container .tiptap code {
				background: var(--rs-bg-surface-raised); padding: 2px 6px; border-radius: 4px;
				font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 0.88em; color: var(--rs-accent);
			}
			.tiptap-container .tiptap pre {
				background: var(--rs-bg-surface-sunken); border: 1px solid var(--rs-border-subtle);
				border-radius: 8px; padding: 14px 16px; margin: 1em 0; overflow-x: auto;
			}
			.tiptap-container .tiptap pre code {
				background: none; padding: 0; border-radius: 0; color: var(--rs-text-primary);
				font-size: 13px; line-height: 1.6;
			}
			.tiptap-container .tiptap ul, .tiptap-container .tiptap ol { padding-left: 24px; margin: 0.5em 0; }
			.tiptap-container .tiptap li { margin: 0.2em 0; }
			.tiptap-container .tiptap li p { margin: 0.15em 0; }
			.tiptap-container .tiptap li::marker { color: var(--rs-text-muted); }

			/* Task list */
			.tiptap-container .tiptap ul[data-type="taskList"] { list-style: none; padding-left: 4px; }
			.tiptap-container .tiptap ul[data-type="taskList"] li { display: flex; align-items: flex-start; gap: 8px; }
			.tiptap-container .tiptap ul[data-type="taskList"] li label { margin-top: 3px; }
			.tiptap-container .tiptap ul[data-type="taskList"] li[data-checked="true"] > div > p {
				text-decoration: line-through; color: var(--rs-text-muted);
			}
			.tiptap-container .tiptap ul[data-type="taskList"] li label input[type="checkbox"] {
				accent-color: var(--rs-primary); width: 15px; height: 15px;
			}

			.tiptap-container .tiptap img {
				max-width: 100%; border-radius: 8px; margin: 0.75em 0;
				border: 1px solid var(--rs-border-subtle);
			}
			.tiptap-container .tiptap a {
				color: var(--rs-primary-hover); text-decoration: underline;
				text-underline-offset: 2px; text-decoration-color: rgba(99, 102, 241, 0.4);
			}
			.tiptap-container .tiptap a:hover { text-decoration-color: var(--rs-primary-hover); }
			.tiptap-container .tiptap hr { border: none; border-top: 1px solid var(--rs-border); margin: 1.5em 0; }
			.tiptap-container .tiptap strong { color: var(--rs-text-primary); font-weight: 600; }
			.tiptap-container .tiptap em { color: inherit; }
			.tiptap-container .tiptap s { color: var(--rs-text-muted); }
			.tiptap-container .tiptap u { text-underline-offset: 3px; }

			/* Placeholder */
			.tiptap-container .tiptap p.is-editor-empty:first-child::before {
				content: attr(data-placeholder);
				float: left; color: var(--rs-text-muted); pointer-events: none; height: 0;
				font-style: italic;
			}

			/* ── URL Popover ── */
			.url-popover {
				position: absolute; z-index: 110;
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border);
				border-radius: 10px; box-shadow: var(--rs-shadow-md);
				padding: 8px; min-width: 300px;
				animation: popover-in 0.15s ease-out;
			}
			@keyframes popover-in {
				from { opacity: 0; transform: translateY(-4px); }
				to { opacity: 1; transform: translateY(0); }
			}
			.url-popover__input {
				width: 100%; padding: 8px 10px; border-radius: 6px;
				border: 1px solid var(--rs-input-border); background: var(--rs-input-bg);
				color: var(--rs-input-text); font-size: 13px; font-family: inherit;
				outline: none; margin-bottom: 6px; transition: border-color 0.15s;
			}
			.url-popover__input:focus { border-color: var(--rs-primary); }
			.url-popover__actions { display: flex; gap: 6px; justify-content: flex-end; }
			.url-popover__btn {
				padding: 5px 12px; border-radius: 6px; font-size: 12px; font-weight: 600;
				cursor: pointer; border: none; transition: all 0.15s;
			}
			.url-popover__btn--insert { background: var(--rs-primary); color: #fff; }
			.url-popover__btn--insert:hover { background: var(--rs-primary-hover); }
			.url-popover__btn--cancel {
				background: transparent; color: var(--rs-text-secondary);
				border: 1px solid var(--rs-border);
			}
			.url-popover__btn--cancel:hover { color: var(--rs-text-primary); border-color: var(--rs-border-strong); }

			/* ── Slash Menu ── */
			.slash-menu {
				position: absolute; z-index: 100;
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border);
				border-radius: 10px; box-shadow: var(--rs-shadow-lg);
				max-height: 360px; overflow-y: auto; min-width: 240px;
				display: none;
			}
			.slash-menu__header {
				padding: 8px 12px 6px; font-size: 11px; font-weight: 600;
				text-transform: uppercase; letter-spacing: 0.05em;
				color: var(--rs-text-muted); border-bottom: 1px solid var(--rs-border-subtle);
			}
			.slash-menu-item {
				display: flex; align-items: center; gap: 10px;
				padding: 8px 12px; cursor: pointer; transition: background 0.1s;
			}
			.slash-menu-item:last-child { border-radius: 0 0 10px 10px; }
			.slash-menu-item:hover, .slash-menu-item.selected { background: var(--rs-bg-hover); }
			.slash-menu-icon {
				width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
				background: var(--rs-bg-surface-raised); border-radius: 6px;
				font-size: 13px; font-weight: 600; color: var(--rs-primary);
				flex-shrink: 0;
			}
			.slash-menu-icon svg { width: 16px; height: 16px; }
			.slash-menu-text { flex: 1; }
			.slash-menu-title { font-size: 13px; font-weight: 500; color: var(--rs-text-primary); }
			.slash-menu-desc { font-size: 11px; color: var(--rs-text-muted); }
			.slash-menu-hint {
				font-size: 10px; color: var(--rs-text-muted); padding: 1px 6px;
				background: var(--rs-bg-surface-raised); border-radius: 3px; margin-left: auto;
			}

			/* ── Code highlighting (lowlight) ── */
			.tiptap-container .tiptap .hljs-keyword { color: #c792ea; }
			.tiptap-container .tiptap .hljs-string { color: #c3e88d; }
			.tiptap-container .tiptap .hljs-number { color: #f78c6c; }
			.tiptap-container .tiptap .hljs-comment { color: #546e7a; font-style: italic; }
			.tiptap-container .tiptap .hljs-built_in { color: #82aaff; }
			.tiptap-container .tiptap .hljs-function { color: #82aaff; }
			.tiptap-container .tiptap .hljs-title { color: #82aaff; }
			.tiptap-container .tiptap .hljs-attr { color: #ffcb6b; }
			.tiptap-container .tiptap .hljs-tag { color: #f07178; }
			.tiptap-container .tiptap .hljs-type { color: #ffcb6b; }
		`;
	}
}

customElements.define("folk-notes-app", FolkNotesApp);
