/**
 * <folk-docs-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 { makeDraggableAll } from '../../../shared/draggable';
import { notebookSchema } from '../schemas';
import type { DocumentId } from '../../../shared/local-first/document';
import { getAccessToken } from '../../../shared/components/rstack-identity';
import { broadcastPresence as sharedBroadcastPresence, startPresenceHeartbeat } from '../../../shared/collab-presence';
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 { Markdown } from 'tiptap-markdown';
import { TourEngine } from '../../../shared/tour-engine';
import * as Y from 'yjs';
import { IndexeddbPersistence } from 'y-indexeddb';
import { ySyncPlugin, yUndoPlugin, yCursorPlugin } from '@tiptap/y-tiptap';
import { RSpaceYjsProvider } from '../yjs-ws-provider';
import { CommentMark } from './comment-mark';
import { SuggestionInsertMark, SuggestionDeleteMark } from './suggestion-marks';
import { createSuggestionPlugin, acceptSuggestion, rejectSuggestion } from './suggestion-plugin';
import './comment-panel';

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;
	source_ref?: { source: string; syncStatus?: string; lastSyncedAt?: number };
	sort_order?: number;
	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 FolkDocsApp extends HTMLElement {
	private shadow!: ShadowRoot;
	private space = "";
	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 = "";

	// Sidebar state
	private expandedNotebooks = new Set<string>();
	private notebookNotes = new Map<string, Note[]>();
	private sidebarOpen = true;
	private mobileEditing = false;
	private _resizeHandler: (() => void) | null = null;
	private _suggestionSyncTimer: any = null;

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

	// 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: '.sbt-nb-add', title: "Create a Note", message: "Each notebook has a '+' button to add new notes. Click it to create 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;

	// Yjs collaboration state
	private ydoc: Y.Doc | null = null;
	private yjsProvider: RSpaceYjsProvider | null = null;
	private yIndexedDb: IndexeddbPersistence | null = null;
	private yjsPlainTextTimer: ReturnType<typeof setInterval> | null = null;

	// Comments/suggestions state
	private suggestingMode = false;

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

	// Membrane — caller's role for visibility filtering
	private _myRole: string = 'viewer';

	// 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)[] = [];
	private _subscribedDocIds: string[] = [];

	// ── Presence indicators ──
	private _presencePeers: Map<string, {
		peerId: string;
		username: string;
		color: string;
		notebookId: string | null;
		noteId: string | null;
		lastSeen: number;
	}> = new Map();
	private _stopPresence: (() => void) | null = null;
	private _presenceUnsub: (() => void) | null = null;
	private _presenceGC: ReturnType<typeof setInterval> | null = null;

	// ── Demo data ──
	private demoNotebooks: (Notebook & { notes: Note[] })[] = [];
	private _demoThreads = new Map<string, Record<string, any>>();

	constructor() {
		super();
		this.shadow = this.attachShadow({ mode: "open", delegatesFocus: true });
		this._tour = new TourEngine(
			this.shadow,
			FolkDocsApp.TOUR_STEPS,
			"rdocs_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(); this.setupPresence(); this.loadMyRole(); }
		// Auto-start tour on first visit
		if (!localStorage.getItem("rdocs_tour_done")) {
			setTimeout(() => this._tour.start(), 1200);
		}

		// Mobile resize handler — sync mobile-editing state on viewport change
		this._resizeHandler = () => {
			if (window.innerWidth > 768) {
				// Switched to desktop — remove mobile-editing so both panels show
				this.setMobileEditing(false);
			} else if (this.selectedNote && this.editor) {
				// Went back to mobile with a note open — restore editor screen
				this.setMobileEditing(true);
			}
		};
		window.addEventListener('resize', this._resizeHandler);
	}

	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();

		const layout = document.createElement('div');
		layout.id = 'notes-layout';

		this.navZone = document.createElement('div');
		this.navZone.id = 'nav-zone';

		const rightCol = document.createElement('div');
		rightCol.className = 'notes-right-col';

		this.contentZone = document.createElement('div');
		this.contentZone.id = 'content-zone';
		this.metaZone = document.createElement('div');
		this.metaZone.id = 'meta-zone';

		rightCol.appendChild(this.contentZone);
		rightCol.appendChild(this.metaZone);

		// Sidebar reopen tab (lives on layout, outside navZone so it's visible when collapsed)
		const reopenBtn = document.createElement('button');
		reopenBtn.id = 'sidebar-reopen';
		reopenBtn.className = 'sidebar-reopen';
		reopenBtn.title = 'Show sidebar';
		reopenBtn.textContent = '\u203A';
		reopenBtn.addEventListener('click', () => this.toggleSidebar(true));

		layout.appendChild(this.navZone);
		layout.appendChild(reopenBtn);
		layout.appendChild(rightCol);

		this.shadow.appendChild(style);
		this.shadow.appendChild(layout);
	}

	// ── Demo data ──

	private static readonly DEMO_STORAGE_KEY = 'rdocs_demo_data';
	private _demoSaveTimer: ReturnType<typeof setTimeout> | null = null;

	/** Debounce-save demo notebooks to localStorage (500ms). */
	private saveDemoState() {
		if (this._demoSaveTimer) clearTimeout(this._demoSaveTimer);
		this._demoSaveTimer = setTimeout(() => {
			try {
				const serializable = this.demoNotebooks.map(nb => ({
					id: nb.id, title: nb.title, description: nb.description,
					cover_color: nb.cover_color, note_count: nb.note_count,
					updated_at: nb.updated_at, notes: nb.notes,
				}));
				localStorage.setItem(FolkDocsApp.DEMO_STORAGE_KEY, JSON.stringify(serializable));
			} catch { /* localStorage full or unavailable */ }
		}, 500);
	}

	private loadDemoData() {
		// Try restoring from localStorage first
		try {
			const saved = localStorage.getItem(FolkDocsApp.DEMO_STORAGE_KEY);
			if (saved) {
				const parsed = JSON.parse(saved);
				if (Array.isArray(parsed) && parsed.length > 0) {
					this.demoNotebooks = parsed;
					this.notebooks = this.demoNotebooks.map(({ notes, ...nb }: any) => nb as Notebook);
					for (const nb of this.demoNotebooks) {
						this.notebookNotes.set(nb.id, nb.notes);
					}
					if (this.demoNotebooks.length > 0) {
						this.expandedNotebooks.add(this.demoNotebooks[0].id);
					}
					this.loading = false;
					this.render();
					// Auto-open first note of first notebook
					this.autoOpenFirstNote();
					return;
				}
			}
		} catch { /* corrupt data — fall through to defaults */ }

		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(),
			},
		];

		// Starter notebook with a blank note (always first)
		const starterNote: Note = {
			id: "demo-note-starter", title: "Untitled", content: "", content_plain: "",
			content_format: 'tiptap-json',
			type: "NOTE", tags: null, is_pinned: false, sort_order: 0,
			created_at: new Date(now).toISOString(), updated_at: new Date(now).toISOString(),
		};

		this.demoNotebooks = [
			{
				id: "demo-nb-new", title: "New Documents", description: "",
				cover_color: "#3b82f6", note_count: "1", updated_at: new Date(now).toISOString(),
				notes: [starterNote],
			} as any,
			{
				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);
		// Populate sidebar note cache and expand first notebook
		for (const nb of this.demoNotebooks) {
			this.notebookNotes.set(nb.id, nb.notes);
		}
		if (this.demoNotebooks.length > 0) {
			this.expandedNotebooks.add(this.demoNotebooks[0].id);
		}
		this.loading = false;
		this.render();

		// Auto-open the blank starter note
		this.selectedNotebook = { ...this.demoNotebooks[0] };
		this.selectedNote = starterNote;
		this.renderNav();
		this.renderMeta();
		this.mountEditor(starterNote);

		this.saveDemoState(); // persist initial state
	}

	/** Open the first note of the first notebook (used on demo restore and fresh load). */
	private autoOpenFirstNote() {
		const firstNb = this.demoNotebooks[0];
		if (!firstNb?.notes?.length) return;
		this.selectedNotebook = { ...firstNb };
		this.selectedNote = firstNb.notes[0];
		this.renderNav();
		this.renderMeta();
		this.mountEditor(firstNb.notes[0]);
	}

	private demoSearchNotes(query: string) {
		if (!query.trim()) {
			this.searchResults = [];
			this.renderNav();
			return;
		}
		const q = query.toLowerCase();
		const results: Note[] = [];
		for (const nb of this.demoNotebooks) {
			for (const n of nb.notes) {
				if (n.title.toLowerCase().includes(q) ||
					n.content_plain.toLowerCase().includes(q) ||
					(n.tags && n.tags.some(t => t.toLowerCase().includes(q)))) {
					results.push(Object.assign({}, n, { notebook_id: nb.id }) as any);
				}
			}
		}
		this.searchResults = results;
		this.renderNav();
	}

	private demoLoadNote(id: string) {
		// Find the note and its parent notebook
		for (const nb of this.demoNotebooks) {
			const note = nb.notes.find(n => n.id === id);
			if (note) {
				this.selectedNote = note;
				this.selectedNotebook = { ...nb };
				if (!this.expandedNotebooks.has(nb.id)) {
					this.expandedNotebooks.add(nb.id);
				}
				this.renderNav();
				this.renderMeta();
				this.mountEditor(this.selectedNote);
				return;
			}
		}
	}

	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, sort_order: 0,
			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.notebookNotes.set(nbId, [newNote]);
		this.expandedNotebooks.add(nbId);
		this.selectedNotebook = { ...nb };
		// Auto-open the note for editing
		this.selectedNote = newNote;
		this.renderNav();
		this.renderMeta();
		this.mountEditor(newNote);
		this.saveDemoState();
	}

	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 || FolkDocsApp.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, sort_order: 0,
			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 nbId = this.selectedNotebook.id;
		const demoNb = this.demoNotebooks.find(n => n.id === nbId);
		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);
		// Update sidebar cache
		const cached = this.notebookNotes.get(nbId) || [];
		cached.unshift(newNote);
		this.notebookNotes.set(nbId, cached);
		const nbIdx = this.notebooks.findIndex(n => n.id === nbId);
		if (nbIdx >= 0) this.notebooks[nbIdx].note_count = String(cached.length);
		this.selectedNote = newNote;
		this.renderNav();
		this.renderMeta();
		this.mountEditor(newNote);
		this.saveDemoState();
	}

	// ── Mobile stack navigation ──

	private setMobileEditing(editing: boolean) {
		this.mobileEditing = editing;
		this.shadow.getElementById('notes-layout')?.classList.toggle('mobile-editing', editing);
	}

	private mobileGoBack() {
		this.setMobileEditing(false);
	}

	private mobileBackBarHtml(): string {
		const title = this.selectedNotebook?.title || 'Notes';
		return `<div class="mobile-back-bar"><button class="mobile-back-btn">&larr; ${this.esc(title)}</button></div>`;
	}

	private isMobile(): boolean {
		return window.innerWidth <= 768;
	}

	disconnectedCallback() {
		this.destroyEditor();
		this.cleanupPresence();
		if (this._resizeHandler) {
			window.removeEventListener('resize', this._resizeHandler);
			this._resizeHandler = null;
		}
		this._offlineUnsub?.();
		this._offlineUnsub = null;
		for (const unsub of this._offlineNotebookUnsubs) unsub();
		this._offlineNotebookUnsubs = [];
		const runtime = (window as any).__rspaceOfflineRuntime;
		if (runtime) {
			for (const id of this._subscribedDocIds) runtime.unsubscribe(id);
		}
		this._subscribedDocIds = [];
	}

	// ── Membrane ──

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

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

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

	private async subscribeNotebook(notebookId: string) {
		const runtime = (window as any).__rspaceOfflineRuntime;
		// Resolve scope: rdocs is globally-scoped, so use 'global' prefix
		const dataSpace = runtime?.isInitialized
			? (runtime.resolveDocSpace?.('rdocs') || 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._subscribedDocIds.push(docId);
				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 = [];
		// Clear viewId so overlay stops filtering cursors
		window.dispatchEvent(new CustomEvent('rspace-view-change', { detail: { viewId: null } }));
	}

	/** 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,
					sort_order: item.sortOrder || 0,
					created_at: item.createdAt ? new Date(item.createdAt).toISOString() : new Date().toISOString(),
					updated_at: item.updatedAt ? new Date(item.updatedAt).toISOString() : new Date().toISOString(),
				});
			}
		}

		this.sortNotes(notes);

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

		// Update sidebar note cache
		this.notebookNotes.set(nb.id, notes);
		const nbIdx = this.notebooks.findIndex(n => n.id === nb.id);
		if (nbIdx >= 0) this.notebooks[nbIdx].note_count = String(notes.length);

		// If editor is mounted for a note, update editor content from remote
		if (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,
					sort_order: noteItem.sortOrder || 0,
					created_at: noteItem.createdAt ? new Date(noteItem.createdAt).toISOString() : new Date().toISOString(),
					updated_at: noteItem.updatedAt ? new Date(noteItem.updatedAt).toISOString() : new Date().toISOString(),
				};

				// Skip content replacement when Yjs is active — Yjs handles content sync
				if (!this.ydoc) {
					// Legacy mode: 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 a note is selected but editor not mounted yet, update selectedNote
		if (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,
					sort_order: noteItem.sortOrder || 0,
					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 || FolkDocsApp.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, sort_order: 0,
			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.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();
			});
		}
	}

	/** Sort notes: pinned first, then by sort_order (if any are set), then by updated_at desc. */
	private sortNotes(notes: Note[]) {
		const hasSortOrder = notes.some(n => (n.sort_order || 0) > 0);
		notes.sort((a, b) => {
			if (a.is_pinned !== b.is_pinned) return a.is_pinned ? -1 : 1;
			if (hasSortOrder) {
				const sa = a.sort_order || 0;
				const sb = b.sort_order || 0;
				if (sa !== sb) return sa - sb;
			}
			return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
		});
	}

	/** Reorder a note within a notebook's sidebar list. */
	private reorderNote(noteId: string, notebookId: string, targetIndex: number) {
		const notes = this.notebookNotes.get(notebookId);
		if (!notes) return;

		const srcIdx = notes.findIndex(n => n.id === noteId);
		if (srcIdx < 0 || srcIdx === targetIndex) return;

		// Move in the local array
		const [note] = notes.splice(srcIdx, 1);
		notes.splice(targetIndex, 0, note);

		// Assign sort_order based on new positions
		notes.forEach((n, i) => { n.sort_order = i + 1; });
		this.notebookNotes.set(notebookId, notes);

		// Persist to Automerge
		const runtime = (window as any).__rspaceOfflineRuntime;
		const dataSpace = runtime?.isInitialized ? (runtime.resolveDocSpace?.('rdocs') || this.space) : this.space;
		const docId = `${dataSpace}:notes:notebooks:${notebookId}` as DocumentId;

		if (runtime?.isInitialized) {
			runtime.change(docId, `Reorder notes`, (d: NotebookDoc) => {
				for (const n of notes) {
					if (d.items[n.id]) d.items[n.id].sortOrder = n.sort_order!;
				}
			});
			this.doc = runtime.get(this.subscribedDocId as DocumentId);
		} else if (this.doc && this.subscribedDocId === docId) {
			this.doc = Automerge.change(this.doc, `Reorder notes`, (d: NotebookDoc) => {
				for (const n of notes) {
					if (d.items[n.id]) d.items[n.id].sortOrder = n.sort_order!;
				}
			});
		}

		// Also update selectedNotebook if it matches
		if (this.selectedNotebook?.id === notebookId) {
			this.selectedNotebook.notes = [...notes];
		}

		this.renderNav();
	}

	/** Move a note from one notebook to another via Automerge docs. */
	private async moveNoteToNotebook(noteId: string, sourceNotebookId: string, targetNotebookId: string) {
		if (sourceNotebookId === targetNotebookId) return;
		const runtime = (window as any).__rspaceOfflineRuntime;
		if (!runtime?.isInitialized) return;

		const dataSpace = runtime.resolveDocSpace?.('rdocs') || this.space;
		const sourceDocId = `${dataSpace}:notes:notebooks:${sourceNotebookId}` as DocumentId;
		const targetDocId = `${dataSpace}:notes:notebooks:${targetNotebookId}` as DocumentId;

		// Get the note data from source
		const sourceDoc = runtime.get(sourceDocId) as NotebookDoc | undefined;
		if (!sourceDoc?.items?.[noteId]) return;

		// Deep-clone the note item (plain object from Automerge)
		const noteItem = JSON.parse(JSON.stringify(sourceDoc.items[noteId]));
		noteItem.notebookId = targetNotebookId;
		noteItem.updatedAt = Date.now();

		// Subscribe to target doc if needed, add the note, then unsubscribe
		let targetDoc: NotebookDoc | undefined;
		try {
			targetDoc = await runtime.subscribe(targetDocId, notebookSchema);
		} catch {
			return; // target notebook not accessible
		}

		// Add to target
		runtime.change(targetDocId, `Move note ${noteId}`, (d: NotebookDoc) => {
			if (!d.items) (d as any).items = {};
			d.items[noteId] = noteItem;
		});

		// Remove from source
		runtime.change(sourceDocId, `Move note ${noteId} out`, (d: NotebookDoc) => {
			delete d.items[noteId];
		});

		// If we're viewing the source notebook, refresh
		if (this.subscribedDocId === sourceDocId) {
			this.doc = runtime.get(sourceDocId);
			this.renderFromDoc();
		}

		// Update sidebar counts
		const srcNb = this.notebooks.find(n => n.id === sourceNotebookId);
		const tgtNb = this.notebooks.find(n => n.id === targetNotebookId);
		if (srcNb) srcNb.note_count = String(Math.max(0, parseInt(srcNb.note_count) - 1));
		if (tgtNb) tgtNb.note_count = String(parseInt(tgtNb.note_count) + 1);

		// Refresh sidebar note cache for source
		const srcNotes = this.notebookNotes.get(sourceNotebookId);
		if (srcNotes) this.notebookNotes.set(sourceNotebookId, srcNotes.filter(n => n.id !== noteId));

		// If target is expanded, refresh its notes
		if (this.expandedNotebooks.has(targetNotebookId)) {
			const tgtDoc = runtime.get(targetDocId) as NotebookDoc | undefined;
			if (tgtDoc?.items) {
				const notes: Note[] = Object.values(tgtDoc.items).map((item: any) => ({
					id: item.id, title: item.title || 'Untitled', content: item.content || '',
					content_plain: item.contentPlain || '', 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,
					sort_order: item.sortOrder || 0,
					created_at: item.createdAt ? new Date(item.createdAt).toISOString() : new Date().toISOString(),
					updated_at: item.updatedAt ? new Date(item.updatedAt).toISOString() : new Date().toISOString(),
				}));
				this.notebookNotes.set(targetNotebookId, notes);
			}
		}

		// Unsubscribe from target if it's not the active notebook
		if (this.subscribedDocId !== targetDocId) {
			runtime.unsubscribe(targetDocId);
		}

		// Close editor if we were editing the moved note
		if (this.selectedNote?.id === noteId) {
			this.selectedNote = null;
			this.renderContent();
		}

		this.renderNav();
	}

	// ── 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(/^(\/[^/]+)?\/rdocs/);
		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();

		// Auto-scaffold a starter notebook with a blank note on first use
		if (this.notebooks.length === 0 && !this.error) {
			await this.scaffoldStarterNotebook();
		}
	}

	/** Create a "New Documents" notebook with one blank note for first-time users. */
	private async scaffoldStarterNotebook() {
		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: "New Documents", cover_color: "#3b82f6" }),
			});
			const nb = await res.json();
			if (!nb?.id) return;

			// Refresh notebook list so the new notebook appears
			const listRes = await fetch(`${base}/api/notebooks`, { headers: this.authHeaders() });
			const listData = await listRes.json();
			this.notebooks = listData.notebooks || [];
			this.render();

			// Subscribe and create a blank note inside it
			this.selectedNotebook = { ...nb, notes: [] };
			this.expandedNotebooks.add(nb.id);
			this.notebookNotes.set(nb.id, []);
			await this.loadNotebook(nb.id);
			this.createNoteViaSync();
		} catch {
			// Non-critical — user can still create manually
		}
	}

	private async loadNotebook(id: string) {
		this.unsubscribeNotebook();
		await this.subscribeNotebook(id);
		this.broadcastPresence();

		// REST fallback if Automerge doc is empty after 5s
		setTimeout(() => {
			if (this.subscribedDocId && (!this.doc?.items || Object.keys(this.doc.items).length === 0)) {
				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() });
			const data = await res.json();
			this.selectedNotebook = data;
			if (data?.notes) {
				this.sortNotes(data.notes);
				this.notebookNotes.set(id, data.notes);
			}
		} catch {
			this.error = "Failed to load notebook";
		}
		this.loading = false;
		this.render();
	}

	/** Fetch notes for a notebook (sidebar display). */
	private async fetchNotebookNotes(id: string) {
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/notebooks/${id}`, { headers: this.authHeaders() });
			const data = await res.json();
			if (data?.notes) {
				this.sortNotes(data.notes);
				this.notebookNotes.set(id, data.notes);
				const nbIdx = this.notebooks.findIndex(n => n.id === id);
				if (nbIdx >= 0) this.notebooks[nbIdx].note_count = String(data.notes.length);
			}
		} catch {
			// Silently fail — sidebar stays empty for this notebook
		}
		this.renderNav();
	}

	/** Toggle notebook expansion in sidebar. */
	private expandNotebook(id: string) {
		if (this.expandedNotebooks.has(id)) {
			this.expandedNotebooks.delete(id);
			this.renderNav();
			return;
		}
		this.expandedNotebooks.add(id);
		if (!this.notebookNotes.has(id)) {
			if (this.space === "demo") {
				const nb = this.demoNotebooks.find(n => n.id === id);
				if (nb) this.notebookNotes.set(id, nb.notes);
				this.renderNav();
			} else {
				this.fetchNotebookNotes(id);
			}
		} else {
			this.renderNav();
		}
	}

	/** Open a note for editing from the sidebar. */
	private async openNote(noteId: string, notebookId: string) {
		const isDemo = this.space === "demo";

		// Mobile: slide to editor screen
		if (this.isMobile()) {
			this.setMobileEditing(true);
		}

		// Expand notebook if not expanded
		if (!this.expandedNotebooks.has(notebookId)) {
			this.expandedNotebooks.add(notebookId);
		}

		if (isDemo) {
			this.demoLoadNote(noteId);
			return;
		}

		// Set selected notebook
		const nb = this.notebooks.find(n => n.id === notebookId);
		if (nb) {
			this.selectedNotebook = { ...nb, notes: this.notebookNotes.get(notebookId) || [] };
		}

		// Subscribe to Automerge if needed
		const needSubscribe = !this.subscribedDocId || !this.subscribedDocId.endsWith(`:${notebookId}`);
		if (needSubscribe) {
			await this.loadNotebook(notebookId);
		}

		this.loadNote(noteId);
	}

	/** Add a new note to a notebook via the sidebar '+' button. */
	private async addNoteToNotebook(notebookId: string) {
		const isDemo = this.space === "demo";

		// Ensure expanded
		if (!this.expandedNotebooks.has(notebookId)) {
			this.expandedNotebooks.add(notebookId);
		}

		// Set selected notebook
		const nb = this.notebooks.find(n => n.id === notebookId);
		if (!nb) return;
		this.selectedNotebook = { ...nb, notes: this.notebookNotes.get(notebookId) || [] };

		if (isDemo) {
			this.demoCreateNote();
			return;
		}

		// Ensure subscribed
		const needSubscribe = !this.subscribedDocId || !this.subscribedDocId.endsWith(`:${notebookId}`);
		if (needSubscribe) {
			await this.loadNotebook(notebookId);
		}
		this.createNoteViaSync();
	}

	/** Show a small dropdown menu near the "+" button with note creation options. */
	private showAddNoteMenu(nbId: string, anchorEl: HTMLElement) {
		// Remove any existing menu
		this.shadow.querySelector('.add-note-menu')?.remove();

		const menu = document.createElement('div');
		menu.className = 'add-note-menu';

		// Position near the anchor
		const hostRect = (this.shadow.host as HTMLElement).getBoundingClientRect();
		const anchorRect = anchorEl.getBoundingClientRect();
		menu.style.left = `${anchorRect.left - hostRect.left}px`;
		menu.style.top = `${anchorRect.bottom - hostRect.top + 4}px`;

		menu.innerHTML = `
			<button class="add-note-menu-item" data-action="note">New Note</button>
			<button class="add-note-menu-item" data-action="url">From URL</button>
			<button class="add-note-menu-item" data-action="upload">Upload File</button>
		`;

		this.shadow.appendChild(menu);

		const close = () => menu.remove();

		menu.querySelectorAll('.add-note-menu-item').forEach(btn => {
			btn.addEventListener('click', (e) => {
				e.stopPropagation();
				const action = (btn as HTMLElement).dataset.action;
				close();
				if (action === 'note') this.addNoteToNotebook(nbId);
				else if (action === 'url') this.createNoteFromUrl(nbId);
				else if (action === 'upload') this.createNoteFromFile(nbId);
			});
		});

		// Close on outside click
		const onOutside = (e: Event) => {
			if (!menu.contains(e.target as Node)) {
				close();
				this.shadow.removeEventListener('click', onOutside);
			}
		};
		requestAnimationFrame(() => this.shadow.addEventListener('click', onOutside));
	}

	/** Prompt for a URL and create a BOOKMARK note. */
	private async createNoteFromUrl(nbId: string) {
		const url = prompt('Enter URL:');
		if (!url) return;

		let title: string;
		try { title = new URL(url).hostname; } catch { title = url; }

		// Ensure notebook is selected and subscribed
		const nb = this.notebooks.find(n => n.id === nbId);
		if (!nb) return;
		this.selectedNotebook = { ...nb, notes: this.notebookNotes.get(nbId) || [] };
		if (!this.expandedNotebooks.has(nbId)) this.expandedNotebooks.add(nbId);

		if (this.space === 'demo') {
			this.demoCreateNote();
			return;
		}

		const needSubscribe = !this.subscribedDocId || !this.subscribedDocId.endsWith(`:${nbId}`);
		if (needSubscribe) await this.loadNotebook(nbId);

		this.createNoteViaSync({ type: 'BOOKMARK', url, title });
	}

	/** Open a file picker, upload the file, and create a FILE or IMAGE note. */
	private createNoteFromFile(nbId: string) {
		const input = document.createElement('input');
		input.type = 'file';
		input.accept = 'image/*,audio/*,video/*,.pdf,.doc,.docx,.txt,.md,.csv,.json';

		input.addEventListener('change', async () => {
			const file = input.files?.[0];
			if (!file) return;

			// Upload the file
			const base = this.getApiBase();
			const fd = new FormData();
			fd.append('file', file, file.name);

			try {
				const uploadRes = await fetch(`${base}/api/uploads`, {
					method: 'POST', headers: this.authHeaders(), body: fd,
				});
				if (!uploadRes.ok) throw new Error('Upload failed');
				const uploadData = await uploadRes.json();
				const fileUrl = uploadData.url || uploadData.path;

				// Determine note type
				const mime = file.type || '';
				const type: NoteType = mime.startsWith('image/') ? 'IMAGE'
					: mime.startsWith('audio/') ? 'AUDIO' : 'FILE';

				// Ensure notebook ready
				const nb = this.notebooks.find(n => n.id === nbId);
				if (!nb) return;
				this.selectedNotebook = { ...nb, notes: this.notebookNotes.get(nbId) || [] };
				if (!this.expandedNotebooks.has(nbId)) this.expandedNotebooks.add(nbId);

				if (this.space === 'demo') {
					this.demoCreateNote();
					return;
				}

				const needSubscribe = !this.subscribedDocId || !this.subscribedDocId.endsWith(`:${nbId}`);
				if (needSubscribe) await this.loadNotebook(nbId);

				this.createNoteViaSync({ type, fileUrl, mimeType: mime, title: file.name });
			} catch (err) {
				console.error('File upload failed:', err);
				alert('Failed to upload file. Please try again.');
			}
		});

		input.click();
	}

	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,
				sort_order: item.sortOrder || 0,
				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;
		}

		// Fallback: try sidebar note cache
		if (!this.selectedNote) {
			for (const [, notes] of this.notebookNotes) {
				const found = notes.find(n => n.id === id);
				if (found) { this.selectedNote = found; break; }
			}
		}

		if (this.selectedNote) {
			this.renderNav();
			this.renderMeta();
			this.mountEditor(this.selectedNote);
			this.broadcastPresence();
			// Notify overlay which sub-document we're viewing (scopes SVG cursors)
			window.dispatchEvent(new CustomEvent('rspace-view-change', {
				detail: { viewId: this.selectedNote.id, viewLabel: this.selectedNote.title },
			}));
		}
	}

	private async searchNotes(query: string) {
		if (!query.trim()) {
			this.searchResults = [];
			this.renderNav();
			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.renderNav();
	}

	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) {
				await this.loadNotebooks(); // Refresh list
				this.notebookNotes.set(nb.id, []);
				this.expandedNotebooks.add(nb.id);
				this.render();
			} 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;
		}

		// Mobile: inject back bar and slide to editor
		this.contentZone.insertAdjacentHTML('afterbegin', this.mobileBackBarHtml());
		this.contentZone.querySelector('.mobile-back-btn')?.addEventListener('click', () => this.mobileGoBack());
		if (this.isMobile()) this.setMobileEditing(true);
	}

	private mountTiptapEditor(note: Note, isEditable: boolean, isDemo: boolean) {
		const useYjs = !isDemo && isEditable;

		this.contentZone.innerHTML = `
			<div class="editor-with-comments">
				<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="collab-status-bar" id="collab-status-bar" style="display:none">
						<span class="collab-status-dot"></span>
						<span class="collab-status-text"></span>
					</div>
					<div class="tiptap-container" id="tiptap-container"></div>
				</div>
				<div class="comment-sidebar" id="comment-sidebar"></div>
			</div>
		`;

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

		if (useYjs) {
			this.mountTiptapWithYjs(note, container);
		} else {
			this.mountTiptapLegacy(note, isEditable, isDemo, container);
		}

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

		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();
		this.wireCommentHighlightClicks();
	}

	/** Mount TipTap with Yjs collaboration (real-time co-editing). */
	private mountTiptapWithYjs(note: Note, container: HTMLElement) {
		const runtime = (window as any).__rspaceOfflineRuntime;
		const spaceSlug = runtime?.space || this.space;
		const roomName = `rdocs:${spaceSlug}:${note.id}`;

		// Create Y.Doc
		this.ydoc = new Y.Doc();
		const fragment = this.ydoc.getXmlFragment('content');

		// IndexedDB persistence for offline
		this.yIndexedDb = new IndexeddbPersistence(roomName, this.ydoc);

		// Set awareness identity BEFORE connecting provider (avoids anonymous ghost)
		const sessionForAwareness = this.getSessionInfo();

		// Connect Yjs provider over rSpace WebSocket
		if (runtime?.isInitialized) {
			this.yjsProvider = new RSpaceYjsProvider(note.id, this.ydoc, runtime);
			// Pre-set user so the first awareness broadcast has the correct name
			this.yjsProvider.awareness.setLocalStateField('user', {
				name: sessionForAwareness.username || 'Anonymous',
				color: this.userColor(sessionForAwareness.userId || 'anon'),
			});
		}

		// Content migration: if Y.Doc fragment is empty and Automerge has content
		this.yIndexedDb.on('synced', () => {
			if (fragment.length === 0 && note.content) {
				// Migrate existing content into Yjs by creating a temp editor, setting content, then destroying
				let content: any = '';
				if (note.content_format === 'tiptap-json') {
					try { content = JSON.parse(note.content); } catch { content = note.content; }
				} else {
					content = note.content;
				}
				if (this.editor && content) {
					this.editor.commands.setContent(content, { emitUpdate: false });
				}
				// Mark as collab-enabled in Automerge
				if (this.doc?.items?.[note.id]) {
					this.updateNoteField(note.id, 'collabEnabled', 'true');
				}
			}
		});

		// Create editor with Yjs sync/undo plugins registered directly
		this.editor = new Editor({
			element: container,
			editable: true,
			extensions: [
				StarterKit.configure({
					codeBlock: false,
					heading: { levels: [1, 2, 3, 4] },
					undoRedo: false,  // Yjs has its own undo/redo
					link: false,
					underline: false,
				}),
				Link.configure({ openOnClick: false }),
				Image, TaskList, TaskItem.configure({ nested: true }),
				Placeholder.configure({ placeholder: 'Start writing, or type / for commands...' }),
				CodeBlockLowlight.configure({ lowlight }), Typography, Underline,
				Markdown.configure({ html: true, transformPastedText: true, transformCopiedText: true }),
				CommentMark,
				SuggestionInsertMark,
				SuggestionDeleteMark,
			],
			onSelectionUpdate: () => { this.updateToolbarState(); },
		});

		// Register Yjs sync and undo plugins
		this.editor.registerPlugin(ySyncPlugin(fragment));
		this.editor.registerPlugin(yUndoPlugin());

		// Register suggestion plugin for track-changes mode
		const suggestionPlugin = createSuggestionPlugin(
			() => this.suggestingMode,
			() => {
				const s = this.getSessionInfo();
				return { authorId: s.userId, authorName: s.username };
			},
		);
		this.editor.registerPlugin(suggestionPlugin);

		// Register cursor presence plugin
		if (this.yjsProvider) {
			const cursorPlugin = yCursorPlugin(
				this.yjsProvider.awareness,
				{ cursorBuilder: this.buildCollabCursor.bind(this) }
			);
			this.editor.registerPlugin(cursorPlugin);

			// Update collab status bar when peers change
			this.yjsProvider.awareness.on('update', () => {
				this.updatePeersIndicator();
			});
		}

		// Initial collab status bar update
		this.updatePeersIndicator();

		// Periodic plaintext sync to Automerge (for search indexing)
		this.yjsPlainTextTimer = setInterval(() => {
			if (!this.editor || !this.editorNoteId) return;
			const plain = this.editor.getText();
			this.updateNoteField(this.editorNoteId, 'contentPlain', plain);
		}, 5000);
	}

	/** Mount TipTap without Yjs (demo mode / read-only). */
	private mountTiptapLegacy(note: Note, isEditable: boolean, isDemo: boolean, container: HTMLElement) {
		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: false, underline: false }),
				Link.configure({ openOnClick: false }),
				Image, TaskList, TaskItem.configure({ nested: true }),
				Placeholder.configure({ placeholder: 'Start writing, or type / for commands...' }),
				CodeBlockLowlight.configure({ lowlight }), Typography, Underline,
				Markdown.configure({ html: true, transformPastedText: true, transformCopiedText: true }),
				CommentMark,
				SuggestionInsertMark,
				SuggestionDeleteMark,
			],
			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(); },
		});
	}

	/** Build a DOM element for remote collaborator cursor. */
	private buildCollabCursor(user: { name: string; color: string }) {
		const cursor = document.createElement('span');
		cursor.className = 'collab-cursor';
		cursor.style.borderLeftColor = user.color;

		const label = document.createElement('span');
		label.className = 'collab-cursor-label';
		label.style.backgroundColor = user.color;
		label.textContent = user.name;
		cursor.appendChild(label);

		return cursor;
	}

	/** Derive a stable color from a user ID string. */
	private userColor(id: string): string {
		let hash = 0;
		for (let i = 0; i < id.length; i++) {
			hash = ((hash << 5) - hash + id.charCodeAt(i)) | 0;
		}
		const hue = Math.abs(hash) % 360;
		return `hsl(${hue}, 70%, 50%)`;
	}

	/** Get session info for cursor display. */
	private getSessionInfo(): { username: string; userId: string } {
		try {
			const sess = JSON.parse(localStorage.getItem('encryptid_session') || '{}');
			const c = sess?.claims;
			return {
				username: c?.username || c?.displayName || sess?.username || 'Anonymous',
				userId: c?.sub || sess?.userId || 'anon',
			};
		} catch {
			return { username: 'Anonymous', userId: 'anon' };
		}
	}

	// ── Presence indicators ──

	/** Start presence broadcasting and listening. Called once when runtime is available. */
	private setupPresence() {
		const runtime = (window as any).__rspaceOfflineRuntime;
		if (this._presenceUnsub) return;
		if (!runtime?.isInitialized) {
			setTimeout(() => this.setupPresence(), 2000);
			return;
		}

		// Listen for presence messages from peers (for sidebar notebook/note dots)
		this._presenceUnsub = runtime.onCustomMessage('presence', (msg: any) => {
			if (msg.module !== 'rdocs' || !msg.peerId) return;
			this._presencePeers.set(msg.peerId, {
				peerId: msg.peerId,
				username: msg.username || 'Anonymous',
				color: msg.color || '#888',
				notebookId: msg.notebookId || null,
				noteId: msg.noteId || null,
				lastSeen: Date.now(),
			});
			this.renderPresenceIndicators();
		});

		// Use shared heartbeat for broadcasting
		this._stopPresence = startPresenceHeartbeat(() => ({
			module: 'rdocs',
			context: this.selectedNote
				? `${this.selectedNotebook?.title || 'Notebook'} > ${this.selectedNote.title}`
				: this.selectedNotebook?.title || '',
			notebookId: this.selectedNotebook?.id,
			noteId: this.selectedNote?.id,
		}));

		// GC: remove stale peers every 15s (for sidebar dots)
		this._presenceGC = setInterval(() => {
			const cutoff = Date.now() - 20_000;
			let changed = false;
			for (const [id, peer] of this._presencePeers) {
				if (peer.lastSeen < cutoff) {
					this._presencePeers.delete(id);
					changed = true;
				}
			}
			if (changed) this.renderPresenceIndicators();
		}, 15_000);
	}

	/** Broadcast current user position to peers. */
	private broadcastPresence() {
		sharedBroadcastPresence({
			module: 'rdocs',
			context: this.selectedNote
				? `${this.selectedNotebook?.title || 'Notebook'} > ${this.selectedNote.title}`
				: this.selectedNotebook?.title || '',
			notebookId: this.selectedNotebook?.id,
			noteId: this.selectedNote?.id,
		});
	}

	/** Patch presence dots onto sidebar notebook headers and note items. */
	private renderPresenceIndicators() {
		// Remove all existing presence dots
		this.shadow.querySelectorAll('.presence-dots').forEach(el => el.remove());

		// Notebook headers in sidebar
		this.shadow.querySelectorAll<HTMLElement>('.sbt-notebook-header[data-toggle-notebook]').forEach(header => {
			const nbId = header.dataset.toggleNotebook;
			const peers = Array.from(this._presencePeers.values()).filter(p => p.notebookId === nbId);
			if (peers.length === 0) return;
			header.appendChild(this.buildPresenceDots(peers));
		});

		// Note items in sidebar
		this.shadow.querySelectorAll<HTMLElement>('.sbt-note[data-note]').forEach(item => {
			const noteId = item.dataset.note;
			const peers = Array.from(this._presencePeers.values()).filter(p => p.noteId === noteId);
			if (peers.length === 0) return;
			item.appendChild(this.buildPresenceDots(peers));
		});
	}

	/** Build a presence-dots container for a set of peers. */
	private buildPresenceDots(peers: { username: string; color: string }[]): HTMLSpanElement {
		const container = document.createElement('span');
		container.className = 'presence-dots';
		const show = peers.slice(0, 3);
		for (const p of show) {
			const dot = document.createElement('span');
			dot.className = 'presence-dot';
			dot.style.background = p.color;
			dot.title = p.username;
			container.appendChild(dot);
		}
		if (peers.length > 3) {
			const more = document.createElement('span');
			more.className = 'presence-dot-more';
			more.textContent = `+${peers.length - 3}`;
			container.appendChild(more);
		}
		return container;
	}

	/** Tear down presence listeners and timers. */
	private cleanupPresence() {
		this._stopPresence?.();
		this._stopPresence = null;
		this._presenceUnsub?.();
		this._presenceUnsub = null;
		if (this._presenceGC) { clearInterval(this._presenceGC); this._presenceGC = null; }
		this._presencePeers.clear();
	}

	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: false, underline: false }),
				Link.configure({ openOnClick: false }), Image,
				Placeholder.configure({ placeholder: note.type === 'CLIP' ? 'Clipped content...' : 'Add notes about this bookmark...' }),
				Typography, Underline, Markdown.configure({ html: true, transformPastedText: true, transformCopiedText: true }),
			],
			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: false, underline: false }), Link.configure({ openOnClick: false }),
					Placeholder.configure({ placeholder: 'Add a caption or notes...' }), Typography, Underline,
					Markdown.configure({ html: true, transformPastedText: true, transformCopiedText: true }),
				],
				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: false, underline: false }), Link.configure({ openOnClick: false }),
					Placeholder.configure({ placeholder: 'Transcript will appear here...' }), Typography, Underline,
					Markdown.configure({ html: true, transformPastedText: true, transformCopiedText: true }),
				],
				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.yjsPlainTextTimer) {
			clearInterval(this.yjsPlainTextTimer);
			this.yjsPlainTextTimer = null;
		}
		if (this.yjsProvider) {
			this.yjsProvider.destroy();
			this.yjsProvider = null;
		}
		if (this.yIndexedDb) {
			this.yIndexedDb.destroy();
			this.yIndexedDb = null;
		}
		if (this.ydoc) {
			this.ydoc.destroy();
			this.ydoc = 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;
		this.suggestingMode = false;
		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 class="toolbar-sep"></div>
			<div class="toolbar-group collab-tools">
				<button class="toolbar-btn" data-cmd="comment" title="Add Comment">
					<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="M2 2.5h12a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H5l-3 3v-11a1 1 0 0 1 1-1z"/></svg>
				</button>
				<button class="toolbar-btn suggest-toggle" data-cmd="toggleSuggesting" title="Toggle Suggesting Mode">
					<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.5 1.5l3 3-9 9H2.5v-3z"/><path d="M9.5 3.5l3 3"/></svg>
				</button>
				<span class="collab-peers" id="collab-peers" title="Connected peers"></span>
			</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();
			if (window.innerWidth > 640) {
				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;
				case 'comment': this.addComment(); break;
				case 'toggleSuggesting': this.toggleSuggestingMode(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();
				}
			});
		}
	}

	/** Add a comment on the current selection. */
	private addComment() {
		if (!this.editor) return;
		const { from, to, empty } = this.editor.state.selection;
		if (empty) return; // Need selected text

		const threadId = `c_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
		const session = this.getSessionInfo();

		// Apply comment mark to the selection
		this.editor.chain().focus()
			.setMark('comment', { threadId, resolved: false })
			.run();

		// Create thread in Automerge or demo storage
		const noteId = this.editorNoteId;
		if (noteId && this.doc?.items?.[noteId] && this.subscribedDocId) {
			const runtime = (window as any).__rspaceOfflineRuntime;
			if (runtime?.isInitialized) {
				runtime.change(this.subscribedDocId as DocumentId, 'Add comment thread', (d: NotebookDoc) => {
					const item = d.items[noteId] as any;
					if (!item.comments) item.comments = {};
					item.comments[threadId] = {
						id: threadId,
						anchor: `${from}-${to}`,
						resolved: false,
						messages: [],
						createdAt: Date.now(),
					};
				});
				this.doc = runtime.get(this.subscribedDocId as DocumentId);
			}
		} else if (this.space === 'demo' && noteId) {
			if (!this._demoThreads.has(noteId)) this._demoThreads.set(noteId, {});
			this._demoThreads.get(noteId)![threadId] = {
				id: threadId,
				anchor: `${from}-${to}`,
				resolved: false,
				messages: [],
				createdAt: Date.now(),
			};
		}

		// Open comment panel
		this.showCommentPanel(threadId);
	}

	/** Toggle between editing and suggesting modes. */
	private toggleSuggestingMode(btn: HTMLElement) {
		this.suggestingMode = !this.suggestingMode;
		btn.classList.toggle('active', this.suggestingMode);
		btn.title = this.suggestingMode ? 'Switch to Editing Mode' : 'Switch to Suggesting Mode';

		// Update editor's editable state to reflect mode
		const container = this.shadow.getElementById('tiptap-container');
		if (container) {
			container.classList.toggle('suggesting-mode', this.suggestingMode);
		}

		// Show/hide suggestion review bar
		this.updateSuggestionReviewBar();
	}

	/** Show a review bar when there are pending suggestions. */
	private updateSuggestionReviewBar() {
		let bar = this.shadow.getElementById('suggestion-review-bar');
		if (!this.editor) {
			bar?.remove();
			return;
		}

		// Count suggestions
		const ids = new Set<string>();
		this.editor.state.doc.descendants((node: any) => {
			if (!node.isText) return;
			for (const mark of node.marks) {
				if (mark.type.name === 'suggestionInsert' || mark.type.name === 'suggestionDelete') {
					ids.add(mark.attrs.suggestionId);
				}
			}
		});

		if (ids.size === 0 && !this.suggestingMode) {
			bar?.remove();
			return;
		}

		if (!bar) {
			bar = document.createElement('div');
			bar.id = 'suggestion-review-bar';
			bar.className = 'suggestion-review-bar';
			// Insert after toolbar
			const toolbar = this.shadow.getElementById('editor-toolbar');
			if (toolbar?.parentNode) {
				toolbar.parentNode.insertBefore(bar, toolbar.nextSibling);
			}
		}

		bar.innerHTML = `
			<span class="srb-label">${this.suggestingMode ? 'Suggesting' : 'Editing'}</span>
			${ids.size > 0 ? `
				<span class="srb-count">${ids.size} suggestion${ids.size !== 1 ? 's' : ''} — review in sidebar</span>
			` : '<span class="srb-hint">Start typing to suggest changes</span>'}
		`;

		// Open sidebar to show suggestions when there are any
		if (ids.size > 0) this.showCommentPanel();
	}

	/** Show an accept/reject popover near a clicked suggestion mark. */
	private showSuggestionPopover(suggestionId: string, authorName: string, type: 'insert' | 'delete', rect: DOMRect) {
		// Remove any existing popover
		this.shadow.querySelector('.suggestion-popover')?.remove();

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

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

		pop.innerHTML = `
			<div class="sp-header">
				<span class="sp-author">${this.esc(authorName)}</span>
				<span class="sp-type">${type === 'insert' ? 'Added' : 'Deleted'}</span>
			</div>
			<div class="sp-actions">
				<button class="sp-btn sp-accept" title="Accept">&#10003;</button>
				<button class="sp-btn sp-reject" title="Reject">&#10005;</button>
			</div>
		`;

		pop.querySelector('.sp-accept')!.addEventListener('click', () => {
			if (this.editor) {
				acceptSuggestion(this.editor, suggestionId);
				this.updateSuggestionReviewBar();
				this.syncSuggestionsToPanel(true);
			}
			pop.remove();
		});
		pop.querySelector('.sp-reject')!.addEventListener('click', () => {
			if (this.editor) {
				rejectSuggestion(this.editor, suggestionId);
				this.updateSuggestionReviewBar();
				this.syncSuggestionsToPanel(true);
			}
			pop.remove();
		});

		this.shadow.appendChild(pop);

		// Close on click outside
		const close = (e: Event) => {
			if (!pop.contains((e as MouseEvent).target as Node)) {
				pop.remove();
				this.shadow.removeEventListener('click', close);
			}
		};
		setTimeout(() => this.shadow.addEventListener('click', close), 0);
	}

	/** Collect all pending suggestions from the editor doc. */
	private collectSuggestions(): { id: string; type: 'insert' | 'delete'; text: string; authorId: string; authorName: string; createdAt: number }[] {
		if (!this.editor) return [];
		const map = new Map<string, { id: string; type: 'insert' | 'delete'; text: string; authorId: string; authorName: string; createdAt: number }>();
		this.editor.state.doc.descendants((node: any) => {
			if (!node.isText) return;
			for (const mark of node.marks) {
				if (mark.type.name === 'suggestionInsert' || mark.type.name === 'suggestionDelete') {
					const id = mark.attrs.suggestionId;
					const existing = map.get(id);
					if (existing) {
						existing.text += node.text || '';
					} else {
						map.set(id, {
							id,
							type: mark.type.name === 'suggestionInsert' ? 'insert' : 'delete',
							text: node.text || '',
							authorId: mark.attrs.authorId || '',
							authorName: mark.attrs.authorName || 'Unknown',
							createdAt: mark.attrs.createdAt || Date.now(),
						});
					}
				}
			}
		});
		return Array.from(map.values());
	}

	/** Push current suggestions to the comment panel (debounced to avoid letter-by-letter flicker). */
	private syncSuggestionsToPanel(immediate = false) {
		clearTimeout(this._suggestionSyncTimer);
		const flush = () => {
			const panel = this.shadow.querySelector('notes-comment-panel') as any;
			if (!panel) return;
			const suggestions = this.collectSuggestions();
			panel.suggestions = suggestions;
			const sidebar = this.shadow.getElementById('comment-sidebar');
			if (sidebar && suggestions.length > 0) {
				sidebar.classList.add('has-comments');
			}
		};
		if (immediate) {
			flush();
		} else {
			this._suggestionSyncTimer = setTimeout(flush, 400);
		}
	}

	/** Show comment panel for a specific thread. */
	private showCommentPanel(threadId?: string) {
		const sidebar = this.shadow.getElementById('comment-sidebar');
		if (!sidebar) return;

		let panel = this.shadow.querySelector('notes-comment-panel') as any;
		if (!panel) {
			panel = document.createElement('notes-comment-panel');
			sidebar.appendChild(panel);
			// Listen for demo thread mutations from comment panel
			panel.addEventListener('comment-demo-mutation', (e: CustomEvent) => {
				const { noteId, threads } = e.detail;
				if (noteId) this._demoThreads.set(noteId, threads);
			});
			// Listen for suggestion accept/reject from comment panel
			panel.addEventListener('suggestion-accept', (e: CustomEvent) => {
				if (this.editor && e.detail?.suggestionId) {
					acceptSuggestion(this.editor, e.detail.suggestionId);
					this.updateSuggestionReviewBar();
					this.syncSuggestionsToPanel(true);
				}
			});
			panel.addEventListener('suggestion-reject', (e: CustomEvent) => {
				if (this.editor && e.detail?.suggestionId) {
					rejectSuggestion(this.editor, e.detail.suggestionId);
					this.updateSuggestionReviewBar();
					this.syncSuggestionsToPanel(true);
				}
			});
		}
		panel.noteId = this.editorNoteId;
		panel.doc = this.doc;
		panel.subscribedDocId = this.subscribedDocId;
		panel.activeThreadId = threadId || null;
		panel.editor = this.editor;
		panel.space = this.space;
		// Pass demo threads if in demo mode
		if (this.space === 'demo' && this.editorNoteId) {
			panel.demoThreads = this._demoThreads.get(this.editorNoteId) ?? null;
		} else {
			panel.demoThreads = null;
		}
		// Pass suggestions
		panel.suggestions = this.collectSuggestions();

		// Show sidebar when there are comments or suggestions
		sidebar.classList.add('has-comments');
	}

	/** Hide comment sidebar when no comments exist. */
	private hideCommentPanel() {
		const sidebar = this.shadow.getElementById('comment-sidebar');
		if (sidebar) sidebar.classList.remove('has-comments');
	}

	/** Wire click handling on comment highlights and suggestion marks in the editor. */
	private wireCommentHighlightClicks() {
		if (!this.editor) return;

		// On selection change, check if cursor is inside a comment mark
		this.editor.on('selectionUpdate', () => {
			if (!this.editor) return;
			const { $from } = this.editor.state.selection;
			const commentMark = $from.marks().find(m => m.type.name === 'comment');
			if (commentMark) {
				const threadId = commentMark.attrs.threadId;
				if (threadId) this.showCommentPanel(threadId);
			}
		});

		// On any change, update the suggestion review bar + sidebar panel (debounced)
		this.editor.on('update', () => {
			this.updateSuggestionReviewBar();
			this.syncSuggestionsToPanel(); // debounced — avoids letter-by-letter flicker
		});

		// Direct click on comment highlight or suggestion marks in the DOM
		const container = this.shadow.getElementById('tiptap-container');
		if (container) {
			container.addEventListener('click', (e) => {
				const target = e.target as HTMLElement;

				// Comment highlights
				const highlight = target.closest?.('.comment-highlight') as HTMLElement;
				if (highlight) {
					const threadId = highlight.getAttribute('data-thread-id');
					if (threadId) this.showCommentPanel(threadId);
					return;
				}

				// Suggestion marks — show accept/reject popover
				const suggestionEl = target.closest?.('.suggestion-insert, .suggestion-delete') as HTMLElement;
				if (suggestionEl) {
					const suggestionId = suggestionEl.getAttribute('data-suggestion-id');
					const authorName = suggestionEl.getAttribute('data-author-name') || 'Unknown';
					const type = suggestionEl.classList.contains('suggestion-insert') ? 'insert' : 'delete';
					if (suggestionId) {
						const rect = suggestionEl.getBoundingClientRect();
						this.showSuggestionPopover(suggestionId, authorName, type as 'insert' | 'delete', rect);
					}
					return;
				}
			});
		}
	}

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

		// Update comment button state (active when text is selected)
		const commentBtn = toolbar.querySelector('[data-cmd="comment"]') as HTMLElement;
		if (commentBtn) {
			commentBtn.classList.toggle('active', this.editor.isActive('comment'));
			const { empty } = this.editor.state.selection;
			commentBtn.style.opacity = empty ? '0.4' : '1';
		}

		// Update suggesting mode toggle
		const suggestBtn = toolbar.querySelector('[data-cmd="toggleSuggesting"]') as HTMLElement;
		if (suggestBtn) {
			suggestBtn.classList.toggle('active', this.suggestingMode);
		}

		// Update peers count
		this.updatePeersIndicator();
	}

	private updatePeersIndicator() {
		const peersEl = this.shadow.getElementById('collab-peers');
		const statusBar = this.shadow.getElementById('collab-status-bar');

		if (!peersEl || !this.yjsProvider) {
			if (peersEl) peersEl.style.display = 'none';
			if (statusBar) statusBar.style.display = 'none';
			return;
		}

		const connected = this.yjsProvider.isConnected;
		const states = this.yjsProvider.awareness.getStates();
		const peerCount = states.size - 1; // Exclude self

		// Toolbar peer dots
		if (peerCount > 0) {
			peersEl.style.display = 'inline-flex';
			peersEl.innerHTML = '';
			let shown = 0;
			for (const [clientId, state] of states) {
				if (clientId === this.ydoc?.clientID) continue;
				if (shown >= 3) break;
				const user = state.user || { name: '?', color: '#888' };
				const dot = document.createElement('span');
				dot.className = 'peer-dot';
				dot.style.backgroundColor = user.color;
				dot.title = user.name;
				peersEl.appendChild(dot);
				shown++;
			}
			if (peerCount > 3) {
				const more = document.createElement('span');
				more.className = 'peer-more';
				more.textContent = `+${peerCount - 3}`;
				peersEl.appendChild(more);
			}
		} else {
			peersEl.style.display = 'none';
		}

		// Collab status bar
		if (statusBar) {
			statusBar.style.display = 'flex';
			const dot = statusBar.querySelector('.collab-status-dot') as HTMLElement;
			const text = statusBar.querySelector('.collab-status-text') as HTMLElement;
			if (!dot || !text) return;

			if (!connected) {
				dot.className = 'collab-status-dot offline';
				text.textContent = 'Offline \u2014 changes will sync when reconnected';
			} else if (peerCount > 0) {
				dot.className = 'collab-status-dot live';
				const names: string[] = [];
				for (const [clientId, state] of states) {
					if (clientId === this.ydoc?.clientID) continue;
					names.push(state.user?.name || 'Anonymous');
				}
				text.textContent = `Live editing \u00B7 ${peerCount} collaborator${peerCount > 1 ? 's' : ''}`;
				text.title = names.join(', ');
			} else {
				dot.className = 'collab-status-dot synced';
				text.textContent = 'Live sync enabled';
				text.title = '';
			}
		}

		// Sidebar collab dot
		const sidebarDot = this.navZone.querySelector('.sidebar-collab-dot') as HTMLElement;
		if (sidebarDot) {
			sidebarDot.className = `sidebar-collab-dot ${connected ? 'connected' : 'disconnected'}`;
		}
	}

	// ── 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.selectedNote && this.editor && this.editorNoteId === this.selectedNote.id) {
			// Editor already mounted — don't touch contentZone
		} else {
			this.renderContent();
		}
		this.renderMeta();
		this.renderPresenceIndicators();
		this._tour.renderOverlay();
	}

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

	private renderNav() {
		const isSearching = this.searchQuery.trim().length > 0;

		let treeHtml = '';
		if (isSearching && this.searchResults.length > 0) {
			// Client-side membrane filter on search results
			const visibleResults = this.searchResults.filter(n => this._isVisibleTo((n as any).visibility, this._myRole));
			treeHtml = `<div class="sidebar-search-results">
				${visibleResults.map(n => {
					const nbId = (n as any).notebook_id || '';
					const nb = this.notebooks.find(x => x.id === nbId);
					return `<div class="sidebar-search-result" data-note="${n.id}" data-notebook="${nbId}">
						<span class="sbt-note-icon">${this.getNoteIcon(n.type)}</span>
						<span class="sbt-note-title">${(n as any).visibility && (n as any).visibility !== 'viewer' ? '<span class="membrane-lock" title="Restricted: ' + (n as any).visibility + ' only">&#128274;</span> ' : ''}${this.esc(n.title)}</span>
						${nb ? `<span class="sidebar-search-result-nb">${this.esc(nb.title)}</span>` : ''}
					</div>`;
				}).join('')}
			</div>`;
		} else if (isSearching) {
			treeHtml = '<div class="empty" style="padding:20px;font-size:13px">No results</div>';
		} else {
			treeHtml = this.notebooks.map(nb => {
				const isExpanded = this.expandedNotebooks.has(nb.id);
				const allNotes = this.notebookNotes.get(nb.id) || [];
				// Client-side membrane filter (defense-in-depth)
				const notes = allNotes.filter(n => this._isVisibleTo((n as any).visibility, this._myRole));
				return `
					<div class="sbt-notebook">
						<div class="sbt-notebook-header" data-toggle-notebook="${nb.id}">
							<span class="sbt-toggle${isExpanded ? ' expanded' : ''}">\u25B6</span>
							<span class="sbt-nb-dot" style="background:${nb.cover_color}"></span>
							<span class="sbt-nb-title">${this.esc(nb.title)}</span>
							<span class="sbt-nb-count">${nb.note_count}</span>
							<button class="sbt-nb-add" data-add-note="${nb.id}" title="Add note">+</button>
						</div>
						${isExpanded ? `<div class="sbt-notes">
							${notes.length > 0 ? notes.map(n => `
								<div class="sbt-note${this.selectedNote?.id === n.id ? ' active' : ''}" data-note="${n.id}" data-notebook="${nb.id}">
									<span class="sbt-note-icon">${this.getNoteIcon(n.type)}</span>
									<span class="sbt-note-title">${(n as any).visibility && (n as any).visibility !== 'viewer' ? '<span class="membrane-lock" title="Restricted: ' + (n as any).visibility + ' only">&#128274;</span> ' : ''}${this.esc(n.title)}</span>
									${n.is_pinned ? '<span class="sbt-note-pin">\u{1F4CC}</span>' : ''}
								</div>
							`).join('') : '<div class="empty" style="padding:8px 12px;font-size:12px">No notes</div>'}
						</div>` : ''}
					</div>
				`;
			}).join('');
		}

		// Preserve search input focus/cursor position
		const prevInput = this.navZone.querySelector('#search-input') as HTMLInputElement;
		const hadFocus = prevInput && (prevInput === this.shadow.activeElement);
		const selStart = prevInput?.selectionStart;
		const selEnd = prevInput?.selectionEnd;

		this.navZone.innerHTML = `
			<div class="notes-sidebar">
				<button class="sidebar-collapse" id="sidebar-collapse" title="Hide sidebar">\u2039</button>
				<div class="sidebar-header">
					<input class="sidebar-search" type="text" placeholder="Search notes..." id="search-input" value="${this.esc(this.searchQuery)}">
				</div>
				<button class="sidebar-btn-new-nb" id="create-notebook">+ New Notebook</button>
				<div class="sidebar-tree">
					${treeHtml}
				</div>
				<div class="sidebar-collab-info" title="All editors in this space see changes in real-time. Share the space link to collaborate.">
					<span class="sidebar-collab-dot ${this.yjsProvider?.isConnected ? 'connected' : 'disconnected'}"></span>
					<span>Live Collab</span>
				</div>
				<div class="sidebar-footer">
					<button class="sidebar-footer-btn" id="btn-import-export">Import / Export</button>
					<button class="sidebar-footer-btn" id="btn-web-clipper">Web Clipper</button>
					<button class="sidebar-footer-btn" id="btn-tour">Tour</button>
				</div>
			</div>
		`;

		// Apply collapsed state
		const layout = this.shadow.getElementById('notes-layout');
		if (layout) layout.classList.toggle('sidebar-collapsed', !this.sidebarOpen);

		// Restore search focus
		if (hadFocus) {
			const newInput = this.navZone.querySelector('#search-input') as HTMLInputElement;
			if (newInput) {
				newInput.focus();
				if (selStart !== null && selEnd !== null) {
					newInput.setSelectionRange(selStart, selEnd);
				}
			}
		}

		this.attachSidebarListeners();
	}

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

		// Empty state — no note selected
		this.contentZone.innerHTML = `
			<div class="editor-empty-state">
				<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5">
					<rect x="8" y="4" width="32" height="40" rx="3"/>
					<line x1="16" y1="14" x2="32" y2="14"/>
					<line x1="16" y1="22" x2="28" y2="22"/>
					<line x1="16" y1="30" x2="32" y2="30"/>
				</svg>
				<p>Select a note from the sidebar</p>
				${this.notebooks.length > 0
					? '<button class="rapp-nav__btn" id="btn-empty-new-note">+ Create a Note</button>'
					: '<button class="rapp-nav__btn" id="btn-empty-new-nb">+ Create a Notebook</button>'}
			</div>
		`;

		// Wire CTA button
		this.contentZone.querySelector('#btn-empty-new-note')?.addEventListener('click', () => {
			const nbId = this.selectedNotebook?.id || (this.notebooks.length > 0 ? this.notebooks[0].id : null);
			if (nbId) this.addNoteToNotebook(nbId);
		});
		this.contentZone.querySelector('#btn-empty-new-nb')?.addEventListener('click', () => {
			this.space === 'demo' ? this.demoCreateNotebook() : this.createNotebook();
		});
	}

	private renderMeta() {
		if (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 toggleSidebar(open?: boolean) {
		this.sidebarOpen = open !== undefined ? open : !this.sidebarOpen;
		const layout = this.shadow.getElementById('notes-layout');
		if (layout) layout.classList.toggle('sidebar-collapsed', !this.sidebarOpen);
	}

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

		// Sidebar collapse (reopen button is wired once in connectedCallback)
		this.shadow.getElementById('sidebar-collapse')?.addEventListener('click', () => this.toggleSidebar(false));

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

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

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

		// Web Clipper download
		this.shadow.getElementById("btn-web-clipper")?.addEventListener("click", () => {
			window.open(`${this.getApiBase()}/extension/download`, '_blank');
		});

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

		// Toggle notebooks
		this.shadow.querySelectorAll("[data-toggle-notebook]").forEach(el => {
			el.addEventListener("click", (e) => {
				if ((e.target as HTMLElement).closest('[data-add-note]')) return;
				const id = (el as HTMLElement).dataset.toggleNotebook!;
				this.expandNotebook(id);
			});
		});

		// Add note to notebook (context menu on +)
		this.shadow.querySelectorAll("[data-add-note]").forEach(el => {
			el.addEventListener("click", (e) => {
				e.stopPropagation();
				const nbId = (el as HTMLElement).dataset.addNote!;
				this.showAddNoteMenu(nbId, el as HTMLElement);
			});
		});

		// Click note in tree
		this.shadow.querySelectorAll(".sbt-note[data-note]").forEach(el => {
			el.addEventListener("click", () => {
				const noteId = (el as HTMLElement).dataset.note!;
				const nbId = (el as HTMLElement).dataset.notebook!;
				this.openNote(noteId, nbId);
			});
		});

		// Click search result
		this.shadow.querySelectorAll(".sidebar-search-result[data-note]").forEach(el => {
			el.addEventListener("click", () => {
				const noteId = (el as HTMLElement).dataset.note!;
				const nbId = (el as HTMLElement).dataset.notebook!;
				this.openNote(noteId, nbId);
			});
		});

		// Make sidebar notes draggable (cross-rApp + intra-sidebar)
		makeDraggableAll(this.shadow, ".sbt-note[data-note]", (el) => {
			const title = el.querySelector(".sbt-note-title")?.textContent || "";
			const id = el.dataset.note || "";
			return title ? { title, module: "rdocs", entityId: id, label: "Note", color: "#f59e0b" } : null;
		});

		// Also set native drag data for intra-sidebar notebook moves + cleanup on dragend
		this.shadow.querySelectorAll(".sbt-note[data-note]").forEach(el => {
			(el as HTMLElement).addEventListener("dragstart", (e) => {
				const noteId = (el as HTMLElement).dataset.note!;
				const nbId = (el as HTMLElement).dataset.notebook!;
				e.dataTransfer?.setData("application/x-rdocs-move", JSON.stringify({ noteId, sourceNotebookId: nbId }));
				(el as HTMLElement).style.opacity = "0.4";
			});
			(el as HTMLElement).addEventListener("dragend", () => {
				(el as HTMLElement).style.opacity = "";
				this.shadow.querySelectorAll('.sbt-note').forEach(n =>
					(n as HTMLElement).classList.remove('drag-above', 'drag-below'));
			});
		});

		// Notebook headers accept dropped notes (cross-notebook move)
		this.shadow.querySelectorAll(".sbt-notebook-header[data-toggle-notebook]").forEach(el => {
			(el as HTMLElement).addEventListener("dragover", (e) => {
				if (e.dataTransfer?.types.includes("application/x-rdocs-move")) {
					e.preventDefault();
					(el as HTMLElement).classList.add("drop-target");
				}
			});
			(el as HTMLElement).addEventListener("dragleave", () => {
				(el as HTMLElement).classList.remove("drop-target");
			});
			(el as HTMLElement).addEventListener("drop", (e) => {
				e.preventDefault();
				(el as HTMLElement).classList.remove("drop-target");
				const raw = e.dataTransfer?.getData("application/x-rdocs-move");
				if (!raw) return;
				try {
					const { noteId, sourceNotebookId } = JSON.parse(raw);
					const targetNotebookId = (el as HTMLElement).dataset.toggleNotebook!;
					if (noteId && sourceNotebookId && targetNotebookId) {
						this.moveNoteToNotebook(noteId, sourceNotebookId, targetNotebookId);
					}
				} catch {}
			});
		});

		// Note items accept drops for intra-notebook reordering
		this.shadow.querySelectorAll(".sbt-note[data-note]").forEach(el => {
			const noteEl = el as HTMLElement;
			noteEl.addEventListener("dragover", (e) => {
				if (!e.dataTransfer?.types.includes("application/x-rdocs-move")) return;
				e.preventDefault();
				e.stopPropagation();
				// Determine above/below based on cursor position
				const rect = noteEl.getBoundingClientRect();
				const midY = rect.top + rect.height / 2;
				// Clear all indicators in this container
				noteEl.closest('.sbt-notes')?.querySelectorAll('.sbt-note').forEach(n =>
					(n as HTMLElement).classList.remove('drag-above', 'drag-below'));
				noteEl.classList.add(e.clientY < midY ? 'drag-above' : 'drag-below');
			});
			noteEl.addEventListener("dragleave", (e) => {
				// Only clear if leaving the element entirely (not entering a child)
				if (noteEl.contains(e.relatedTarget as Node)) return;
				noteEl.classList.remove('drag-above', 'drag-below');
			});
			noteEl.addEventListener("drop", (e) => {
				e.preventDefault();
				e.stopPropagation();
				// Clear all indicators
				this.shadow.querySelectorAll('.sbt-note').forEach(n =>
					(n as HTMLElement).classList.remove('drag-above', 'drag-below'));
				const raw = e.dataTransfer?.getData("application/x-rdocs-move");
				if (!raw) return;
				try {
					const { noteId, sourceNotebookId } = JSON.parse(raw);
					const targetNbId = noteEl.dataset.notebook!;
					// Cross-notebook move — delegate to moveNoteToNotebook
					if (sourceNotebookId !== targetNbId) {
						this.moveNoteToNotebook(noteId, sourceNotebookId, targetNbId);
						return;
					}
					// Same notebook — reorder
					const notes = this.notebookNotes.get(targetNbId);
					if (!notes) return;
					const targetNoteId = noteEl.dataset.note!;
					const targetIdx = notes.findIndex(n => n.id === targetNoteId);
					if (targetIdx < 0) return;
					// Determine final index based on above/below
					const rect = noteEl.getBoundingClientRect();
					const insertAfter = e.clientY >= rect.top + rect.height / 2;
					const srcIdx = notes.findIndex(n => n.id === noteId);
					let finalIdx = insertAfter ? targetIdx + 1 : targetIdx;
					// Adjust if dragging from before the target
					if (srcIdx < finalIdx) finalIdx--;
					this.reorderNote(noteId, targetNbId, finalIdx);
				} catch {}
			});
		});
	}

	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();
			}
		}
		this.saveDemoState();
	}

	// ── Import/Export Dialog ──

	private importExportDialog: ImportExportDialog | null = null;

	private openImportExportDialog(tab: 'import' | 'export' | 'sync' = '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);

				const refreshAfterChange = () => {
					if (this.space === 'demo') {
						this.loadDemoData();
					} else {
						this.loadNotebooks();
						// Also refresh current notebook if one is open
						if (this.selectedNotebook) {
							this.loadNotebookREST(this.selectedNotebook.id);
						}
					}
				};

				this.importExportDialog.addEventListener('import-complete', refreshAfterChange);
				this.importExportDialog.addEventListener('sync-complete', refreshAfterChange);

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

	private showDialog(tab: 'import' | 'export' | 'sync') {
		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); -webkit-tap-highlight-color: transparent; }
			* { box-sizing: border-box; }
			button, a, input, select, textarea, [role="button"] { touch-action: manipulation; }

			/* ── Sidebar Layout ── */
			#notes-layout {
				display: grid;
				grid-template-columns: 260px 1fr;
				min-height: 400px;
				height: 100%;
				position: relative;
				transition: grid-template-columns 0.2s ease;
			}
			#notes-layout.sidebar-collapsed {
				grid-template-columns: 0px 1fr;
			}
			#notes-layout.sidebar-collapsed .notes-sidebar {
				opacity: 0;
				pointer-events: none;
			}
			#notes-layout.sidebar-collapsed .sidebar-reopen {
				opacity: 1;
				pointer-events: auto;
			}
			#nav-zone { overflow: visible; }
			#notes-layout.sidebar-collapsed #nav-zone { overflow: hidden; }
			.notes-sidebar {
				display: flex;
				flex-direction: column;
				border-right: 1px solid var(--rs-border-subtle);
				background: var(--rs-bg-surface);
				overflow: visible;
				height: 100%;
				position: relative;
				transition: opacity 0.15s ease;
			}
			/* Collapse button — right edge, vertically centered on sidebar */
			.sidebar-collapse {
				position: absolute; right: -12px; top: 50%; transform: translateY(-50%);
				width: 20px; height: 48px; z-index: 10;
				border: 1px solid var(--rs-border-subtle, #333);
				border-radius: 0 6px 6px 0;
				background: var(--rs-bg-surface, #1e1e2e);
				color: var(--rs-text-muted, #888);
				font-size: 16px; line-height: 1;
				cursor: pointer; display: flex; align-items: center; justify-content: center;
				transition: color 0.15s, border-color 0.15s, background 0.15s;
			}
			.sidebar-collapse:hover {
				color: var(--rs-text-primary);
				border-color: var(--rs-primary, #6366f1);
				background: var(--rs-bg-hover, #252538);
			}
			/* Reopen tab on left edge */
			.sidebar-reopen {
				position: absolute; left: 0; top: 50%; transform: translateY(-50%);
				width: 20px; height: 48px; z-index: 10;
				border: 1px solid var(--rs-border-subtle, #333);
				border-left: none;
				border-radius: 0 6px 6px 0;
				background: var(--rs-bg-surface, #1e1e2e);
				color: var(--rs-text-muted, #888);
				font-size: 18px; line-height: 1;
				cursor: pointer; display: flex; align-items: center; justify-content: center;
				opacity: 0; pointer-events: none;
				transition: opacity 0.15s ease, color 0.15s, background 0.15s;
			}
			.sidebar-reopen:hover {
				color: var(--rs-text-primary);
				background: var(--rs-bg-hover, #252538);
			}
			.sidebar-header { padding: 12px 12px 8px; }
			.sidebar-search {
				width: 100%; padding: 8px 12px; 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;
				transition: border-color 0.15s;
			}
			.sidebar-search:focus { border-color: var(--rs-primary); outline: none; }
			.sidebar-tree { flex: 1; overflow-y: auto; padding: 4px 0; }
			.sidebar-btn-new-nb {
				display: flex; align-items: center; justify-content: center; gap: 6px;
				width: calc(100% - 24px); margin: 0 12px 8px; padding: 7px;
				border-radius: 6px; border: 1px dashed var(--rs-border);
				background: transparent; color: var(--rs-text-secondary);
				font-size: 13px; font-family: inherit; cursor: pointer; transition: all 0.15s;
			}
			.sidebar-btn-new-nb:hover { border-color: var(--rs-primary); color: var(--rs-primary); background: rgba(99, 102, 241, 0.05); }

			/* Notebook tree */
			.sbt-notebook-header {
				display: flex; align-items: center; gap: 6px;
				padding: 6px 12px; cursor: pointer; user-select: none;
				transition: background 0.1s; font-size: 13px;
			}
			.sbt-notebook-header:hover { background: var(--rs-bg-hover); }
			.sbt-notebook-header.drop-target { background: rgba(99, 102, 241, 0.15); border: 1px dashed var(--rs-primary, #6366f1); border-radius: 4px; }
			.sbt-toggle {
				width: 16px; text-align: center; font-size: 10px;
				color: var(--rs-text-muted); flex-shrink: 0;
				transition: transform 0.15s;
			}
			.sbt-toggle.expanded { transform: rotate(90deg); }
			.sbt-nb-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
			.sbt-nb-title {
				flex: 1; font-weight: 500; color: var(--rs-text-primary);
				overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
			}
			.sbt-nb-count { font-size: 11px; color: var(--rs-text-muted); flex-shrink: 0; }
			.sbt-nb-add {
				opacity: 0; border: none; background: none;
				color: var(--rs-text-muted); cursor: pointer;
				font-size: 16px; line-height: 1; padding: 0 4px;
				border-radius: 3px; transition: all 0.15s; flex-shrink: 0;
			}
			.sbt-notebook-header:hover .sbt-nb-add { opacity: 1; }
			.sbt-nb-add:hover { color: var(--rs-primary); background: var(--rs-bg-surface-raised); }
			@media (pointer: coarse) {
				.sbt-nb-add { opacity: 0.6; }
				.sbt-nb-add:active { opacity: 1; color: var(--rs-primary); }
			}
			.sbt-notes { padding-left: 20px; }
			.sbt-note {
				display: flex; align-items: center; gap: 8px;
				padding: 5px 12px 5px 8px; cursor: pointer;
				font-size: 13px; color: var(--rs-text-secondary);
				border-radius: 4px; margin: 1px 8px 1px 0;
				transition: all 0.1s; overflow: hidden;
			}
			.sbt-note:hover { background: var(--rs-bg-hover); color: var(--rs-text-primary); }
			.sbt-note.active { background: var(--rs-primary); color: #fff; }
			.sbt-note-icon { font-size: 14px; flex-shrink: 0; }
			.sbt-note-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
			.sbt-note-pin { font-size: 10px; flex-shrink: 0; }
			.sbt-note.drag-above { box-shadow: 0 -2px 0 0 var(--rs-primary, #6366f1); }
			.sbt-note.drag-below { box-shadow: 0 2px 0 0 var(--rs-primary, #6366f1); }

			.sidebar-footer {
				padding: 8px 12px; border-top: 1px solid var(--rs-border-subtle);
				display: flex; gap: 6px; flex-wrap: wrap;
			}
			.sidebar-footer-btn {
				padding: 5px 10px; border-radius: 5px;
				border: 1px solid var(--rs-border); background: transparent;
				color: var(--rs-text-secondary); font-size: 11px;
				font-family: inherit; cursor: pointer; transition: all 0.15s;
			}
			.sidebar-footer-btn:hover { border-color: var(--rs-border-strong); color: var(--rs-text-primary); }

			/* Add-note context menu */
			.add-note-menu {
				position: absolute; z-index: 100;
				background: var(--rs-surface, #fff); border: 1px solid var(--rs-border);
				border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
				padding: 4px; min-width: 130px;
			}
			.add-note-menu-item {
				display: block; width: 100%; padding: 6px 10px;
				border: none; background: transparent; text-align: left;
				font-size: 12px; font-family: inherit; cursor: pointer;
				color: var(--rs-text-primary); border-radius: 4px;
			}
			.add-note-menu-item:hover { background: var(--rs-surface-hover, #f3f4f6); }

			/* Sidebar collab info */
			.sidebar-collab-info {
				display: flex; align-items: center; gap: 6px;
				padding: 6px 12px; font-size: 11px; color: var(--rs-text-muted);
				border-top: 1px solid var(--rs-border-subtle); cursor: default;
			}
			.sidebar-collab-dot {
				width: 7px; height: 7px; border-radius: 50%;
				flex-shrink: 0; background: #9ca3af;
			}
			.sidebar-collab-dot.connected { background: #22c55e; }
			.sidebar-collab-dot.disconnected { background: #9ca3af; }

			/* Sidebar search results */
			.sidebar-search-results { padding: 4px 0; }
			.sidebar-search-result {
				display: flex; align-items: center; gap: 8px;
				padding: 6px 12px; cursor: pointer; font-size: 13px;
				color: var(--rs-text-secondary); transition: background 0.1s;
			}
			.sidebar-search-result:hover { background: var(--rs-bg-hover); color: var(--rs-text-primary); }
			.sidebar-search-result-nb { font-size: 10px; color: var(--rs-text-muted); margin-left: auto; }

			/* Right column */
			.notes-right-col {
				display: flex; flex-direction: column; overflow: hidden; height: 100%;
			}
			.notes-right-col #content-zone { flex: 1; overflow-y: auto; padding: 20px; }
			.notes-right-col #meta-zone { padding: 0 20px 12px; }

			/* ── Google Docs-like comment sidebar layout ── */
			.editor-with-comments {
				display: flex;
				gap: 0;
				min-height: 100%;
			}
			.editor-with-comments > .editor-wrapper {
				flex: 1;
				min-width: 0;
			}
			.comment-sidebar {
				width: 0;
				overflow: hidden;
				transition: width 0.2s ease;
				flex-shrink: 0;
			}
			.comment-sidebar.has-comments {
				width: 280px;
				overflow-y: auto;
				border-left: 1px solid var(--rs-border, #e5e7eb);
			}
			@media (max-width: 768px) {
				/* Comment sidebar → bottom sheet on all mobile */
				.editor-with-comments { flex-direction: column; }
				.comment-sidebar.has-comments {
					width: 100%; border-left: none;
					border-top: 2px solid var(--rs-border, #e5e7eb);
					max-height: 250px; max-height: 40dvh;
					min-height: 120px; overflow-y: auto;
					border-radius: 12px 12px 0 0; padding-top: 4px;
				}
				.comment-sidebar.has-comments::before {
					content: ''; display: block; width: 32px; height: 4px;
					background: var(--rs-border-strong, #d1d5db); border-radius: 2px;
					margin: 0 auto 4px;
				}
			}

			/* Empty state */
			.editor-empty-state {
				display: flex; flex-direction: column; align-items: center;
				justify-content: center; height: 100%; min-height: 300px;
				color: var(--rs-text-muted); gap: 12px;
			}
			.editor-empty-state svg { width: 48px; height: 48px; opacity: 0.4; }
			.editor-empty-state p { font-size: 14px; }

			/* Mobile sidebar (legacy — hidden, replaced by stack nav) */
			.mobile-sidebar-toggle { display: none; }
			.sidebar-overlay { display: none; }

			/* ── Navigation ── */
			.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); }

			/* ── Presence Indicators ── */
			.presence-dots { display: inline-flex; gap: 2px; align-items: center; margin-left: auto; }
			.presence-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; border: 1px solid var(--rs-bg-surface, #fff); flex-shrink: 0; }
			.presence-dot-more { font-size: 10px; color: var(--rs-text-muted); margin-left: 2px; }

			/* ── 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: rgba(34, 197, 94, 0.15);
				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: rgba(239, 68, 68, 0.08); 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; }

			/* ── Mobile back bar (hidden on desktop) ── */
			.mobile-back-bar { display: none; }
			@media (max-width: 768px) {
				/* Two-screen horizontal stack: nav (100%) + editor (100%) side-by-side */
				#notes-layout {
					display: flex; overflow: hidden;
					grid-template-columns: unset;
				}
				#nav-zone, .notes-right-col {
					flex: 0 0 100%; min-width: 0;
					transition: transform 0.3s ease;
				}
				/* Slide both panels left when editing */
				#notes-layout.mobile-editing > #nav-zone,
				#notes-layout.mobile-editing > .notes-right-col {
					transform: translateX(-100%);
				}
				/* Sidebar fills screen width */
				.notes-sidebar { width: 100%; position: static; transform: none; box-shadow: none; }
				/* Hide old overlay FAB + desktop collapse on mobile */
				.mobile-sidebar-toggle, .sidebar-overlay, .sidebar-collapse, .sidebar-reopen { display: none !important; }
				/* Hide empty state on mobile — user sees doc list */
				.editor-empty-state { display: none; }
				/* Show back bar */
				.mobile-back-bar {
					display: flex; align-items: center;
					padding: 8px 12px; border-bottom: 1px solid var(--rs-border-subtle);
					background: var(--rs-bg-surface);
				}
				.mobile-back-btn {
					background: none; border: none; color: var(--rs-primary);
					font-size: 15px; font-weight: 600; cursor: pointer;
					padding: 4px 8px; border-radius: 6px; font-family: inherit;
				}
				.mobile-back-btn:hover { background: var(--rs-bg-surface-raised); }
				/* Tighten editor padding */
				.editor-wrapper .editable-title { padding: 12px 14px 0; }
				.tiptap-container .tiptap { padding: 14px 16px; }
				.sidebar-footer-btn { min-height: 36px; padding: 7px 12px; }
				/* Toolbar: scroll horizontally, bigger touch targets */
				.editor-toolbar { padding: 3px 4px; gap: 1px; overflow-x: auto; -webkit-overflow-scrolling: touch; flex-wrap: nowrap; }
				.toolbar-btn { width: 36px; height: 36px; }
				.toolbar-sep { display: none; }
				/* Collab tools first so comment/suggest are visible without scrolling */
				.collab-tools { order: -1; border-right: 1px solid var(--rs-toolbar-sep, #e5e7eb); padding-right: 6px; margin-right: 2px; }
				/* Suggestion review bar: allow wrapping */
				.suggestion-review-bar { flex-wrap: wrap; height: auto; min-height: 32px; padding: 4px 8px; gap: 6px; }
			}
			@media (max-width: 480px) {
				.rapp-nav__btn { padding: 5px 10px; font-size: 12px; }
				.editable-title { font-size: 18px; }
				.tiptap-container .tiptap { font-size: 14px; padding: 12px 14px; min-height: 200px; }
				.code-textarea { min-height: 200px; }
				.image-preview { max-height: 240px; }
				.note-actions-bar { flex-wrap: wrap; gap: 6px; }
				.note-action-btn { padding: 5px 10px; font-size: 11px; }
			}

			/* 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); }
			@media (max-width: 640px) {
				.url-popover {
					position: fixed; left: 8px !important; right: 8px !important;
					top: auto !important; bottom: max(env(safe-area-inset-bottom), 8px);
					min-width: 0; width: auto; border-radius: 12px 12px 0 0;
				}
			}

			/* ── 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;
			}
			@media (max-width: 480px) {
				.slash-menu { min-width: 200px; max-height: 260px; }
				.slash-menu-item { padding: 10px 12px; }
				.slash-menu-desc { display: none; }
				.slash-menu-hint { display: none; }
			}

			/* ── 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; }

			/* ── Collaboration: Remote Cursors ── */
			.collab-cursor {
				position: relative;
				border-left: 2px solid;
				margin-left: -1px;
				margin-right: -1px;
				pointer-events: none;
			}
			.collab-cursor-label {
				position: absolute;
				top: -1.4em;
				left: -1px;
				font-size: 10px;
				font-weight: 600;
				color: #fff;
				padding: 1px 5px;
				border-radius: 3px 3px 3px 0;
				white-space: nowrap;
				pointer-events: none;
				user-select: none;
				line-height: 1.4;
			}
			/* y-prosemirror remote selection highlight */
			.ProseMirror .yRemoteSelection {
				background-color: var(--user-color, rgba(99, 102, 241, 0.2));
			}
			.ProseMirror .yRemoteSelectionHead {
				position: absolute;
				border-left: 2px solid var(--user-color, #6366f1);
				height: 1.2em;
			}

			/* ── Collaboration: Status Bar ── */
			.collab-status-bar {
				display: none; align-items: center; gap: 8px;
				padding: 4px 16px; height: 28px;
				background: var(--rs-bg-hover, rgba(0,0,0,0.03));
				border-top: 1px solid var(--rs-border-subtle);
				border-bottom: 1px solid var(--rs-border-subtle);
				font-size: 12px; color: var(--rs-text-muted);
			}
			.collab-status-dot {
				width: 8px; height: 8px; border-radius: 50%;
				flex-shrink: 0;
				background: #9ca3af;
			}
			.collab-status-dot.live { background: #22c55e; }
			.collab-status-dot.synced { background: #3b82f6; }
			.collab-status-dot.offline { background: #9ca3af; }
			.collab-status-text { white-space: nowrap; }

			/* ── Collaboration: Peers Indicator ── */
			.collab-tools { display: flex; align-items: center; gap: 4px; }
			.collab-peers {
				display: none;
				align-items: center;
				gap: 2px;
				margin-left: 4px;
			}
			.peer-dot {
				width: 8px;
				height: 8px;
				border-radius: 50%;
				display: inline-block;
				border: 1px solid var(--rs-bg-surface, #fff);
			}
			.peer-more {
				font-size: 10px;
				color: var(--rs-text-muted);
				margin-left: 2px;
			}

			/* ── Collaboration: Comment Highlights (Google Docs style) ── */
			.tiptap-container .tiptap .comment-highlight {
				background: rgba(251, 188, 4, 0.2);
				border-bottom: 2px solid rgba(251, 188, 4, 0.5);
				cursor: pointer;
				transition: background 0.15s;
				border-radius: 1px;
			}
			.tiptap-container .tiptap .comment-highlight:hover {
				background: rgba(251, 188, 4, 0.35);
			}
			.tiptap-container .tiptap .comment-highlight.resolved {
				background: rgba(251, 188, 4, 0.06);
				border-bottom-color: rgba(251, 188, 4, 0.12);
			}

			/* ── Collaboration: Suggestions ── */
			.tiptap-container .tiptap .suggestion-insert {
				color: #137333;
				background: rgba(22, 163, 74, 0.1);
				border-bottom: 2px solid rgba(22, 163, 74, 0.4);
				text-decoration: none;
				cursor: pointer;
			}
			.tiptap-container .tiptap .suggestion-insert:hover {
				background: rgba(22, 163, 74, 0.18);
			}
			.tiptap-container .tiptap .suggestion-delete {
				color: #c5221f;
				background: rgba(220, 38, 38, 0.08);
				text-decoration: line-through;
				text-decoration-color: #c5221f;
				cursor: pointer;
			}
			.tiptap-container .tiptap .suggestion-delete:hover {
				background: rgba(220, 38, 38, 0.15);
			}
			.tiptap-container.suggesting-mode {
				border-left: 3px solid #f59e0b;
			}
			.suggest-toggle.active {
				background: #f59e0b !important;
				color: #fff !important;
			}

			/* ── Suggestion Review Bar ── */
			.suggestion-review-bar {
				display: flex; align-items: center; gap: 8px;
				padding: 4px 12px; height: 32px;
				background: color-mix(in srgb, #f59e0b 8%, var(--rs-bg-surface, #fff));
				border-bottom: 1px solid color-mix(in srgb, #f59e0b 20%, var(--rs-border, #e5e7eb));
				font-size: 12px; color: var(--rs-text-secondary, #666);
			}
			.srb-label { font-weight: 600; color: #b45309; }
			.srb-count { margin-left: auto; color: var(--rs-text-muted, #999); }
			.srb-hint { color: var(--rs-text-muted, #999); font-style: italic; }

			/* ── Suggestion Popover (accept/reject on click) ── */
			.suggestion-popover {
				position: absolute;
				z-index: 100;
				background: var(--rs-bg-surface, #fff);
				border: 1px solid var(--rs-border, #e5e7eb);
				border-radius: 8px;
				box-shadow: 0 2px 12px rgba(0,0,0,0.12);
				padding: 8px 10px;
				display: flex; flex-direction: column; gap: 6px;
				min-width: 140px;
				font-size: 12px;
			}
			.sp-header { display: flex; align-items: center; gap: 6px; }
			.sp-author { font-weight: 600; color: var(--rs-text-primary, #111); }
			.sp-type { color: var(--rs-text-muted, #999); font-size: 11px; }
			.sp-actions { display: flex; gap: 6px; }
			.sp-btn {
				flex: 1; padding: 4px 0; border: 1px solid var(--rs-border, #ddd);
				border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: 600;
				background: var(--rs-bg-surface, #fff); text-align: center;
			}
			.sp-btn:hover { background: var(--rs-bg-hover, #f5f5f5); }
			.sp-accept { color: #137333; border-color: #137333; }
			.sp-accept:hover { background: rgba(22, 163, 74, 0.08); }
			.sp-reject { color: #c5221f; border-color: #c5221f; }
			.sp-reject:hover { background: rgba(220, 38, 38, 0.08); }
		`;
	}
}

customElements.define("folk-docs-app", FolkDocsApp);
