// app.jsx — main application

const { useState, useEffect, useRef, useMemo, useCallback, useLayoutEffect } = React;

// ── i18n ────────────────────────────────────────────────────────────────────
const I18N = {
  zh: {
    app_name: 'Inkstone',
    mode_edit: '编辑', mode_preview: '预览', mode_split: '分栏',
    tab_files: '文件', tab_outline: '大纲',
    documents: '文档', new_doc: '新建', search_placeholder: '搜索文档…',
    no_matches: '没有匹配的文档', no_docs: '暂无文档', no_headings: '此文档暂无标题',
    menu_rename: '重命名', menu_duplicate: '创建副本', menu_delete: '删除',
    saving: '保存中…', saved: '已保存',
    words: '字数', chars: '字符', reading_time: '阅读', min: '分钟',
    focus_mode: '专注模式',
    sidebar_toggle: '侧边栏', theme_toggle: '主题', language: '语言', export: '导出', tweaks: '调整', settings: '设置',
    export_pdf: '导出 PDF…', export_html: '导出 HTML', export_md: '导出 Markdown',
    pdf_title: 'PDF 预览', pdf_subtitle: '可下载或打印', pdf_hint: '点击下载 PDF 将打开打印对话框，选择"另存为 PDF"',
    pdf_download: '下载 PDF', cancel: '取消',
    empty_title: '开始书写', empty_body: '从左侧选择一个文档，或新建一个开始书写。',
    light_theme: '浅色', dark_theme: '深色',
    tw_typography: '排版', tw_theme: '主题', tw_layout: '布局', tw_font_family: '字体族',
    tw_font_size: '正文字号', tw_editor_size: '编辑器字号', tw_line_height: '行高',
    tw_page_width: '页面宽度', tw_accent: '主题色', tw_dark: '深色模式', tw_split_ratio: '编辑/预览比例',
    font_serif: '衬线 (Source Serif)', font_sans: '无衬线 (Inter)', font_lora: '柔和衬线 (Lora)',
    font_plex: 'IBM Plex', font_cn_serif: '中文衬线 (思源宋体)', font_cn_sans: '中文黑体 (思源黑体)',
    accent_indigo: '靛蓝', accent_emerald: '翠绿', accent_rose: '玫瑰', accent_amber: '琥珀', accent_violet: '紫罗兰', accent_slate: '石板',
  },
  en: {
    app_name: 'Inkstone',
    mode_edit: 'Edit', mode_preview: 'Preview', mode_split: 'Split',
    tab_files: 'Files', tab_outline: 'Outline',
    documents: 'Documents', new_doc: 'New', search_placeholder: 'Search documents…',
    no_matches: 'No matching documents', no_docs: 'No documents yet', no_headings: 'No headings in this document',
    menu_rename: 'Rename', menu_duplicate: 'Duplicate', menu_delete: 'Delete',
    saving: 'Saving…', saved: 'Saved',
    words: 'Words', chars: 'Chars', reading_time: 'Read', min: 'min',
    focus_mode: 'Focus mode',
    sidebar_toggle: 'Sidebar', theme_toggle: 'Theme', language: 'Language', export: 'Export', tweaks: 'Tweaks', settings: 'Settings',
    export_pdf: 'Export PDF…', export_html: 'Export HTML', export_md: 'Export Markdown',
    pdf_title: 'PDF Preview', pdf_subtitle: 'Download or print', pdf_hint: 'Download opens the print dialog — choose "Save as PDF"',
    pdf_download: 'Download PDF', cancel: 'Cancel',
    empty_title: 'Start writing', empty_body: 'Select a document on the left, or create a new one to begin.',
    light_theme: 'Light', dark_theme: 'Dark',
    tw_typography: 'Typography', tw_theme: 'Theme', tw_layout: 'Layout', tw_font_family: 'Font family',
    tw_font_size: 'Prose size', tw_editor_size: 'Editor size', tw_line_height: 'Line height',
    tw_page_width: 'Page width', tw_accent: 'Accent', tw_dark: 'Dark mode', tw_split_ratio: 'Split ratio',
    font_serif: 'Serif (Source Serif)', font_sans: 'Sans (Inter)', font_lora: 'Soft serif (Lora)',
    font_plex: 'IBM Plex', font_cn_serif: 'CN Serif (Noto Serif SC)', font_cn_sans: 'CN Sans (Noto Sans SC)',
    accent_indigo: 'Indigo', accent_emerald: 'Emerald', accent_rose: 'Rose', accent_amber: 'Amber', accent_violet: 'Violet', accent_slate: 'Slate',
  },
};

const FONT_PRESETS = {
  serif:    { prose: "'Source Serif 4', 'Noto Serif SC', Georgia, serif", ui: "'Inter', system-ui, sans-serif" },
  sans:     { prose: "'Inter', 'Noto Sans SC', system-ui, sans-serif",     ui: "'Inter', system-ui, sans-serif" },
  lora:     { prose: "'Lora', 'Noto Serif SC', Georgia, serif",            ui: "'Inter', system-ui, sans-serif" },
  plex:     { prose: "'IBM Plex Serif', 'Noto Serif SC', serif",           ui: "'IBM Plex Sans', 'Noto Sans SC', system-ui, sans-serif" },
  cn_serif: { prose: "'Noto Serif SC', 'Source Serif 4', serif",           ui: "'Noto Sans SC', 'Inter', system-ui, sans-serif" },
  cn_sans:  { prose: "'Noto Sans SC', 'Inter', system-ui, sans-serif",     ui: "'Noto Sans SC', 'Inter', system-ui, sans-serif" },
};

const ACCENT_PRESETS = {
  indigo:   'oklch(58% 0.13 268)',
  emerald:  'oklch(58% 0.13 158)',
  rose:     'oklch(62% 0.16 18)',
  amber:    'oklch(68% 0.13 70)',
  violet:   'oklch(58% 0.16 305)',
  slate:    'oklch(48% 0.02 260)',
};

// ── Tweak defaults ──────────────────────────────────────────────────────────
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "fontPreset": "serif",
  "proseFontSize": 17,
  "editorFontSize": 14,
  "lineHeight": 1.75,
  "pageWidth": 720,
  "accent": "indigo",
  "dark": false,
  "splitRatio": 50
}/*EDITMODE-END*/;

// ── Storage ────────────────────────────────────────────────────────────────
const STORAGE_KEY = 'inkstone.docs.v1';
const STATE_KEY = 'inkstone.state.v1';

function loadDocs() {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (raw) return JSON.parse(raw);
  } catch (_) {}
  const seed = JSON.parse(document.getElementById('seed-docs').textContent);
  return seed;
}
function saveDocs(docs) {
  try { localStorage.setItem(STORAGE_KEY, JSON.stringify(docs)); } catch (_) {}
}
function loadState() {
  try {
    const raw = localStorage.getItem(STATE_KEY);
    if (raw) return JSON.parse(raw);
  } catch (_) {}
  return {};
}
function persistState(s) {
  try { localStorage.setItem(STATE_KEY, JSON.stringify(s)); } catch (_) {}
}

// ── Toolbar ────────────────────────────────────────────────────────────────
function Toolbar({
  mode, setMode, sidebarOpen, setSidebarOpen,
  dark, setDark, lang, setLang, t,
  onExportClick, exportAnchor, exportOpen, onExportClose,
  onMenuClick, menuAnchor, menuOpen, onMenuClose, onFocus, onSettings,
}) {
  return (
    <div className="topbar">
      <div className="brand">
        <BrandMark />
        <span>{t.app_name}</span>
      </div>
      <div className="topbar-divider" />
      <button className={`icon-btn ${sidebarOpen ? 'active' : ''}`} onClick={() => setSidebarOpen(!sidebarOpen)} title={t.sidebar_toggle}>
        <Icon name="sidebar" />
      </button>
      <div className="topbar-spacer" />
      <ModeToggle value={mode} onChange={setMode} t={t} />
      <div className="topbar-spacer" />
      <button className="icon-btn" onClick={() => setDark(!dark)} title={dark ? t.light_theme : t.dark_theme}>
        <Icon name={dark ? 'sun' : 'moon'} />
      </button>
      <button className="text-btn" onClick={(e) => { onMenuClick(e.currentTarget, 'lang'); }} title={t.language}>
        <Icon name="globe" />{lang === 'zh' ? '中' : 'EN'}
      </button>
      <div className="topbar-divider" />
      <button className="text-btn" onClick={(e) => onExportClick(e.currentTarget)}>
        <Icon name="download" />{t.export}<Icon name="chev-down" size={12} />
      </button>
      <button className="icon-btn" onClick={onSettings} title={t.settings}>
        <Icon name="tweaks" />
      </button>
      <button className="icon-btn" onClick={onFocus} title={t.focus_mode}>
        <Icon name="maximize" />
      </button>
    </div>
  );
}

// ── App root ────────────────────────────────────────────────────────────────
function App() {
  const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [docs, setDocs] = useState(() => loadDocs());
  const [activeId, setActiveId] = useState(() => loadState().activeId || (loadDocs()[0]?.id));
  const [mode, setMode] = useState(() => loadState().mode || 'split');
  const [sidebarOpen, setSidebarOpen] = useState(() => loadState().sidebarOpen ?? true);
  const [lang, setLang] = useState(() => loadState().lang || 'zh');
  const [focus, setFocus] = useState(false);
  const [saveState, setSaveState] = useState('saved');
  const [lastSaved, setLastSaved] = useState('');
  const [pdfOpen, setPdfOpen] = useState(false);
  const [exportAnchor, setExportAnchor] = useState(null);
  const [langAnchor, setLangAnchor] = useState(null);
  const [activeHeading, setActiveHeading] = useState(null);
  const [settingsOpen, setSettingsOpen] = useState(false);
  const textareaRef = useRef(null);

  const t = I18N[lang];
  const active = docs.find(d => d.id === activeId);

  // Persist state
  useEffect(() => { persistState({ activeId, mode, sidebarOpen, lang }); }, [activeId, mode, sidebarOpen, lang]);

  // Apply theme + tweaks to document
  useEffect(() => {
    const root = document.documentElement;
    root.setAttribute('data-theme', tweaks.dark ? 'dark' : 'light');
    root.setAttribute('lang', lang === 'zh' ? 'zh-CN' : 'en');
    document.getElementById('hljs-light').disabled = tweaks.dark;
    document.getElementById('hljs-dark').disabled = !tweaks.dark;
    const fp = FONT_PRESETS[tweaks.fontPreset] || FONT_PRESETS.serif;
    root.style.setProperty('--font-prose', fp.prose);
    root.style.setProperty('--font-ui', fp.ui);
    root.style.setProperty('--prose-fs', tweaks.proseFontSize + 'px');
    root.style.setProperty('--editor-fs', tweaks.editorFontSize + 'px');
    root.style.setProperty('--prose-lh', String(tweaks.lineHeight));
    root.style.setProperty('--editor-lh', String(Math.min(tweaks.lineHeight, 1.7)));
    root.style.setProperty('--content-width', tweaks.pageWidth + 'px');
    root.style.setProperty('--accent', ACCENT_PRESETS[tweaks.accent] || ACCENT_PRESETS.indigo);
  }, [tweaks, lang]);

  // Render markdown
  const html = useMemo(() => {
    if (!active) return '';
    return window.MD.render(active.content || '');
  }, [active?.content]);

  // Headings for TOC
  const headings = useMemo(() => {
    if (!active) return [];
    return window.MD.extractHeadings(active.content || '');
  }, [active?.content]);

  // Stats
  const stats = useMemo(() => {
    if (!active) return { words: 0, chars: 0, reading: 0 };
    const c = window.MD.countWords(active.content || '');
    return { words: c.words, chars: c.charsNoSpace, reading: window.MD.readingTime(c.words) };
  }, [active?.content]);

  // Autosave
  const saveTimerRef = useRef(null);
  const updateContent = useCallback((newContent) => {
    setDocs(prev => prev.map(d => d.id === activeId ? { ...d, content: newContent, dirty: true } : d));
    setSaveState('saving');
    if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
    saveTimerRef.current = setTimeout(() => {
      setDocs(prev => {
        const next = prev.map(d => d.id === activeId ? { ...d, dirty: false } : d);
        saveDocs(next);
        return next;
      });
      setSaveState('saved');
      const now = new Date();
      setLastSaved(now.toLocaleTimeString(lang === 'zh' ? 'zh-CN' : 'en-US', { hour: '2-digit', minute: '2-digit' }));
    }, 700);
  }, [activeId, lang]);

  // Doc CRUD
  const createDoc = () => {
    const id = 'd' + Date.now();
    const name = lang === 'zh' ? '未命名.md' : 'Untitled.md';
    const newDoc = { id, name, content: `# ${name.replace(/\.md$/, '')}\n\n` };
    setDocs(prev => { const next = [newDoc, ...prev]; saveDocs(next); return next; });
    setActiveId(id);
  };
  const renameDoc = (id, newName) => {
    setDocs(prev => {
      const next = prev.map(d => d.id === id ? { ...d, name: newName.endsWith('.md') ? newName : newName + '.md' } : d);
      saveDocs(next); return next;
    });
  };
  const deleteDoc = (id) => {
    setDocs(prev => {
      const next = prev.filter(d => d.id !== id);
      saveDocs(next);
      if (id === activeId) setActiveId(next[0]?.id);
      return next;
    });
  };
  const duplicateDoc = (id) => {
    const src = docs.find(d => d.id === id);
    if (!src) return;
    const newDoc = { ...src, id: 'd' + Date.now(), name: src.name.replace(/\.md$/, '') + ' (copy).md' };
    setDocs(prev => { const next = [newDoc, ...prev]; saveDocs(next); return next; });
    setActiveId(newDoc.id);
  };

  // Sync scroll between editor textarea and preview pane (split mode).
  // Time-based source-lock: when user scrolls source, we lock for ~150ms so
  // the programmatic scroll on dst doesn't bounce back. Works with scrollbar
  // drag, wheel, and trackpad inertia (all of which keep firing scroll events).
  const editorRef = useRef(null);
  const previewRef = useRef(null);
  const syncLockRef = useRef({ source: null, until: 0 });
  const syncRafRef = useRef(null);

  const scheduleSync = (source) => {
    if (mode !== 'split') return;
    const now = performance.now();
    const lock = syncLockRef.current;
    // If a different source recently took control, ignore this scroll — it's
    // the bounce from our own programmatic write.
    if (lock.source && lock.source !== source && now < lock.until) return;
    lock.source = source;
    lock.until = now + 160;
    if (syncRafRef.current) return;
    syncRafRef.current = requestAnimationFrame(() => {
      syncRafRef.current = null;
      const ed = textareaRef.current;
      const pv = previewRef.current;
      if (!ed || !pv) return;
      const src = source === 'editor' ? ed : pv;
      const dst = source === 'editor' ? pv : ed;
      const srcMax = src.scrollHeight - src.clientHeight;
      const dstMax = dst.scrollHeight - dst.clientHeight;
      if (srcMax <= 0 || dstMax <= 0) return;
      const frac = src.scrollTop / srcMax;
      const target = frac * dstMax;
      // Skip if dst is already where it should be (avoid an unneeded write)
      if (Math.abs(dst.scrollTop - target) < 0.5) return;
      dst.scrollTop = target;
    });
  };

  // Active heading tracking on preview scroll
  useEffect(() => {
    if (mode === 'edit') return;
    const root = previewRef.current;
    if (!root) return;
    let raf;
    const onScroll = () => {
      if (raf) return;
      raf = requestAnimationFrame(() => {
        raf = null;
        const els = root.querySelectorAll('h1, h2, h3, h4, h5, h6');
        let current = null;
        for (const el of els) {
          if (el.getBoundingClientRect().top - root.getBoundingClientRect().top < 80) current = el.id;
        }
        setActiveHeading(current);
      });
    };
    root.addEventListener('scroll', onScroll);
    onScroll();
    return () => { root.removeEventListener('scroll', onScroll); if (raf) cancelAnimationFrame(raf); };
  }, [mode, html]);

  // Render mermaid after preview updates
  useEffect(() => {
    const root = previewRef.current;
    if (!root || mode === 'edit') return;
    const id = setTimeout(() => {
      window.MD.renderMermaidIn(root, tweaks.dark ? 'dark' : 'light');
    }, 50);
    return () => clearTimeout(id);
  }, [html, mode, tweaks.dark]);

  // Jump to heading
  const jumpHeading = (h) => {
    const root = previewRef.current;
    if (!root) return;
    const el = root.querySelector(`#${CSS.escape(h.id)}`);
    if (el) {
      const rootRect = root.getBoundingClientRect();
      const elRect = el.getBoundingClientRect();
      root.scrollTo({ top: root.scrollTop + (elRect.top - rootRect.top) - 24, behavior: 'smooth' });
      el.classList.add('flash');
      setTimeout(() => el.classList.remove('flash'), 800);
    }
  };

  // Keyboard shortcuts
  useEffect(() => {
    const onKey = (e) => {
      const meta = e.metaKey || e.ctrlKey;
      if (meta && e.key === 's') { e.preventDefault(); /* autosave runs already */ }
      if (meta && e.shiftKey && e.key === 'P') { e.preventDefault(); setMode('preview'); }
      if (meta && e.shiftKey && e.key === 'E') { e.preventDefault(); setMode('edit'); }
      if (meta && e.shiftKey && e.key === 'S') { e.preventDefault(); setMode('split'); }
      if (meta && e.key === 'p') { e.preventDefault(); setPdfOpen(true); }
      if (e.key === 'Escape' && focus) setFocus(false);
      if (meta && e.key === '\\') { e.preventDefault(); setSidebarOpen(s => !s); }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [focus]);

  // Export menu
  const exportItems = [
    { section: t.export },
    { label: t.export_pdf, icon: 'pdf', shortcut: '⌘P', onClick: () => setPdfOpen(true) },
    { label: t.export_html, icon: 'html', onClick: () => doExportHtml(active, html) },
    { label: t.export_md, icon: 'file', onClick: () => doExportMd(active) },
  ];
  const langItems = [
    { section: t.language },
    { label: '中文', onClick: () => setLang('zh'), checked: lang === 'zh' },
    { label: 'English', onClick: () => setLang('en'), checked: lang === 'en' },
  ];

  const handlePrintPdf = () => {
    setPdfOpen(false);
    setTimeout(() => window.print(), 200);
  };

  // Editor with tab handling
  const onEditorChange = (e) => updateContent(e.target.value);
  const onEditorKeyDown = (e) => {
    if (e.key === 'Tab') {
      e.preventDefault();
      const ta = e.target;
      const { selectionStart: s, selectionEnd: en, value } = ta;
      const insert = '  ';
      ta.value = value.slice(0, s) + insert + value.slice(en);
      ta.selectionStart = ta.selectionEnd = s + insert.length;
      updateContent(ta.value);
    }
  };

  const splitL = mode === 'split' ? `${tweaks.splitRatio}%` : null;
  const splitR = mode === 'split' ? `${100 - tweaks.splitRatio}%` : null;

  return (
    <>
      <div className={`app ${focus ? 'focus' : ''}`}>
        <Toolbar
          mode={mode} setMode={setMode}
          sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen}
          dark={tweaks.dark} setDark={(v) => setTweak('dark', v)}
          lang={lang} setLang={setLang}
          t={t}
          onExportClick={(el) => setExportAnchor(el)}
          onMenuClick={(el, kind) => { if (kind === 'lang') setLangAnchor(el); }}
          onFocus={() => setFocus(true)}
          onSettings={() => setSettingsOpen(true)}
        />

        <div className={`main ${sidebarOpen ? '' : 'no-sidebar'}`}>
          <Sidebar
            docs={docs}
            activeId={activeId}
            onSelect={setActiveId}
            onCreate={createDoc}
            onRename={renameDoc}
            onDelete={deleteDoc}
            onDuplicate={duplicateDoc}
            headings={headings}
            activeHeading={activeHeading}
            onJumpHeading={jumpHeading}
            t={t}
          />

          {!active ? (
            <EmptyDoc onCreate={createDoc} t={t} />
          ) : (
            <div className={`canvas ${mode}`} style={mode === 'split' ? { gridTemplateColumns: `${splitL} ${splitR}` } : undefined}>
              {(mode === 'edit' || mode === 'split') && (
                <div className="pane editor-pane">
                  <div className="pane-header">
                    <span><Icon name="edit" size={11} style={{ marginRight: 6, verticalAlign: -1 }}/>{t.mode_edit}</span>
                    <span style={{ fontSize: 10, color: 'var(--fg-4)', fontFamily: 'var(--font-mono)', textTransform: 'none', letterSpacing: 0 }}>
                      {active.name}
                    </span>
                  </div>
                  <div className="pane-body" ref={editorRef}>
                    <div className="editor-wrap">
                      <textarea
                        className="editor"
                        value={active.content}
                        onChange={onEditorChange}
                        onKeyDown={onEditorKeyDown}
                        onScroll={() => scheduleSync('editor')}
                        spellCheck={false}
                        placeholder={lang === 'zh' ? '从这里开始书写…' : 'Start writing…'}
                        ref={textareaRef}
                      />
                      <FormatToolbar
                        textareaRef={textareaRef}
                        scrollRef={textareaRef}
                        value={active.content}
                        onChange={updateContent}
                      />
                    </div>
                  </div>
                </div>
              )}
              {(mode === 'preview' || mode === 'split') && (
                <div className="pane">
                  <div className="pane-header">
                    <span><Icon name="eye" size={11} style={{ marginRight: 6, verticalAlign: -1 }}/>{t.mode_preview}</span>
                    <span style={{ fontSize: 10, color: 'var(--fg-4)', textTransform: 'none', letterSpacing: 0 }}>
                      {stats.words.toLocaleString()} {t.words} · {stats.reading} {t.min}
                    </span>
                  </div>
                  <div className="pane-body" ref={previewRef}
                       onScroll={() => scheduleSync('preview')}>
                    <div className="prose" dangerouslySetInnerHTML={{ __html: html }} />
                  </div>
                </div>
              )}
            </div>
          )}
        </div>

        <StatusBar stats={stats} saveState={saveState} lastSaved={lastSaved}
                   mode={mode} onMode={setMode} onFocus={() => setFocus(true)} t={t} lang={lang} />
      </div>

      {focus && (
        <button
          className="focus-exit"
          onClick={() => setFocus(false)}
          title={lang === 'zh' ? '退出专注模式 (Esc)' : 'Exit focus mode (Esc)'}
        >
          <Icon name="x" size={14} />
          <span>{lang === 'zh' ? '退出专注' : 'Exit focus'}</span>
          <kbd>Esc</kbd>
        </button>
      )}

      {exportAnchor && (
        <Menu anchor={exportAnchor} items={exportItems} onClose={() => setExportAnchor(null)} />
      )}
      {langAnchor && (
        <Menu anchor={langAnchor} items={langItems} onClose={() => setLangAnchor(null)} />
      )}

      {pdfOpen && active && (
        <PdfPreviewModal html={html} title={active.name} onClose={() => setPdfOpen(false)} onPrint={handlePrintPdf} t={t} />
      )}

      <SettingsDrawer
        open={settingsOpen}
        onClose={() => setSettingsOpen(false)}
        settings={{ ...tweaks, lang }}
        setSetting={(k, v) => {
          if (k === 'lang') setLang(v);
          else setTweak(k, v);
        }}
        t={t}
      />
    </>
  );
}

// ── Export helpers ──────────────────────────────────────────────────────────
function doExportHtml(doc, html) {
  if (!doc) return;
  const accent = getComputedStyle(document.documentElement).getPropertyValue('--accent');
  const proseFont = getComputedStyle(document.documentElement).getPropertyValue('--font-prose');
  const monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono');
  const out = `<!doctype html>
<html><head><meta charset="utf-8"><title>${escapeAttr(doc.name)}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<style>
  body{max-width:720px;margin:48px auto;padding:0 24px;font-family:${proseFont};font-size:17px;line-height:1.75;color:#1a1a1a;background:#fff}
  h1,h2,h3,h4{font-family:'Inter',system-ui,sans-serif;letter-spacing:-.015em}
  h1{font-size:2em;border-bottom:1px solid #eee;padding-bottom:.3em}
  h2{font-size:1.5em;border-bottom:1px solid #eee;padding-bottom:.3em}
  a{color:${accent.trim() || '#5856d6'}}
  code{font-family:${monoFont};font-size:.86em;padding:.15em .4em;background:#f5f5f3;border-radius:4px}
  pre{background:#f5f5f3;padding:16px;border-radius:8px;overflow-x:auto}
  pre code{background:none;padding:0}
  blockquote{border-left:3px solid ${accent.trim() || '#5856d6'};padding:.4em 1.2em;color:#555;background:#f9f8f3;margin:1.2em 0;border-radius:0 6px 6px 0}
  table{border-collapse:collapse;width:100%}
  th,td{border-bottom:1px solid #eee;padding:8px 12px;text-align:left}
  th{background:#f5f5f3}
</style></head>
<body>${html}</body></html>`;
  const blob = new Blob([out], { type: 'text/html' });
  triggerDownload(blob, doc.name.replace(/\.md$/, '') + '.html');
}
function doExportMd(doc) {
  if (!doc) return;
  const blob = new Blob([doc.content], { type: 'text/markdown' });
  triggerDownload(blob, doc.name);
}
function triggerDownload(blob, name) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = name;
  document.body.appendChild(a); a.click(); a.remove();
  setTimeout(() => URL.revokeObjectURL(a.href), 1000);
}
function escapeAttr(s) { return String(s).replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c])); }

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
