← Notes results

Notes

Qwen 3.8 · full

Initial view of Notes

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 86,777 bytes · SHA-256 1a066a4db98e
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Quill — Notes</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Crect%20width='24'%20height='24'%20rx='6'%20fill='%23b45309'/%3E%3Cpath%20d='M17.5%206.5a4.24%204.24%200%200%200-6-6L5%207v8h8z'%20fill='none'%20stroke='%23fff'%20stroke-width='1.7'%20stroke-linecap='round'%20stroke-linejoin='round'/%3E%3Cpath%20d='M13%208%204%2017'%20stroke='%23fff'%20stroke-width='1.7'%20stroke-linecap='round'/%3E%3C/svg%3E">
<style>
/* ============================================================
   1. THEME VARIABLES & RESET
   ============================================================ */
:root{
  --bg:#f2ede5;
  --panel:#f8f5f0;
  --panel-hi:#fcfbf8;
  --panel-2:#eae3d8;
  --text:#2c2620;
  --text-2:#6e6558;
  --text-3:#9b9184;
  --border:#e4ddd1;
  --border-2:#d5ccbd;
  --accent:#b45309;
  --accent-ink:#ffffff;
  --accent-soft:rgba(180,83,9,.10);
  --accent-soft-2:rgba(180,83,9,.20);
  --mark:#f5d688;
  --hover:rgba(60,45,20,.05);
  --hover-2:rgba(60,45,20,.10);
  --danger:#c73e33;
  --danger-soft:rgba(199,62,51,.09);
  --check-ink:#ffffff;
  --shadow:0 6px 24px rgba(70,55,30,.14),0 1px 3px rgba(70,55,30,.08);
  --font-ui:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
  --font-editor:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,"Times New Roman",serif;
  --font-mono:ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;
}
[data-theme="dark"]{
  --bg:#161310;
  --panel:#1d1a15;
  --panel-hi:#242019;
  --panel-2:#2d2820;
  --text:#ece4d6;
  --text-2:#a89d8b;
  --text-3:#776c5c;
  --border:#322c23;
  --border-2:#443b2e;
  --accent:#e2a44f;
  --accent-ink:#241a0a;
  --accent-soft:rgba(226,164,79,.13);
  --accent-soft-2:rgba(226,164,79,.26);
  --mark:rgba(226,164,79,.42);
  --hover:rgba(255,245,225,.055);
  --hover-2:rgba(255,245,225,.11);
  --danger:#e0685c;
  --danger-soft:rgba(224,104,92,.13);
  --check-ink:#241a0a;
  --shadow:0 6px 24px rgba(0,0,0,.5),0 1px 3px rgba(0,0,0,.4);
  color-scheme:dark;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
  font-family:var(--font-ui);
  font-size:14px;
  background:var(--bg);
  color:var(--text);
  overflow:hidden;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}
button{font-family:inherit}
.hidden{display:none!important}

/* ============================================================
   2. APP LAYOUT
   ============================================================ */
#app{display:flex;flex-direction:column;height:100vh;--sbw:264px;--listw:320px}
#panes{display:flex;flex:1;min-height:0}
.handle{width:6px;flex-shrink:0;cursor:col-resize;position:relative;z-index:5}
.handle::after{content:'';position:absolute;top:0;bottom:0;left:2px;width:2px;background:var(--border);transition:background .15s}
.handle:hover::after,.handle.active::after{background:var(--accent)}
body.resizing{cursor:col-resize;user-select:none}
body.resizing #sidebar,body.resizing #listpane{transition:none}
#app.sb-closed #sidebar{width:0!important;border-right-width:0}
#app.sb-closed #handle-sb{display:none}

/* ============================================================
   3. TOP BAR
   ============================================================ */
#topbar{
  height:46px;flex-shrink:0;display:flex;align-items:center;justify-content:space-between;
  padding:0 10px 0 6px;background:var(--panel);border-bottom:1px solid var(--border);gap:10px;
}
.tb-left,.tb-right{display:flex;align-items:center;gap:8px}
.brand{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:700;letter-spacing:.01em;padding-left:4px;user-select:none}
.brand-mark{width:24px;height:24px;border-radius:7px;background:var(--accent);color:var(--accent-ink);display:grid;place-items:center}
.save-ind{display:flex;align-items:center;gap:5px;font-size:12px;color:var(--text-3);opacity:0;transition:opacity .35s;padding:4px 8px}
.save-ind.show{opacity:1}
.save-ind svg{color:var(--accent)}

/* ============================================================
   4. SIDEBAR
   ============================================================ */
#sidebar{
  background:var(--bg);border-right:1px solid var(--border);display:flex;flex-direction:column;
  width:var(--sbw);min-width:0;overflow:hidden;transition:width .18s ease;
}
.sb-head{display:flex;align-items:center;justify-content:space-between;padding:12px 10px 8px 14px;flex-shrink:0}
.sb-title{font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--text-3)}
.sb-head-actions{display:flex;gap:2px}
#sb-body{flex:1;overflow-y:auto;overflow-x:hidden;padding:0 0 14px}
.sb-item{
  display:flex;align-items:center;gap:8px;padding:6px 8px;margin:1px 8px;border-radius:8px;
  cursor:pointer;color:var(--text-2);font-size:13px;user-select:none;position:relative;
  transition:background .12s;
}
.sb-item:hover{background:var(--hover);color:var(--text)}
.sb-item.selected{background:var(--accent-soft);color:var(--text);font-weight:600}
.sb-item.selected .sb-ic{color:var(--accent)}
.sb-ic{display:grid;place-items:center;color:var(--text-3);flex-shrink:0}
.sb-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.sb-count{font-size:11px;color:var(--text-3);flex-shrink:0}
.sb-chev{background:none;border:0;cursor:pointer;color:var(--text-3);display:grid;place-items:center;width:18px;height:18px;border-radius:4px;padding:0;flex-shrink:0}
.sb-chev:hover{background:var(--hover-2);color:var(--text)}
.sb-chev svg{transition:transform .15s}
.nb.collapsed .sb-chev svg{transform:rotate(-90deg)}
.sb-kebab{opacity:0;background:none;border:0;cursor:pointer;color:var(--text-3);width:20px;height:20px;border-radius:5px;display:grid;place-items:center;padding:0;flex-shrink:0;transition:opacity .12s}
.sb-item:hover .sb-kebab{opacity:1}
.sb-kebab:hover{background:var(--hover-2);color:var(--text)}
.sb-sep{height:1px;background:var(--border);margin:8px 14px}
.sb-notes{margin:2px 8px 6px 24px;border-left:1px solid var(--border);padding-left:4px}
.sb-note{padding:4px 10px;font-size:12.5px;color:var(--text-2);border-radius:6px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:background .12s}
.sb-note:hover{background:var(--hover);color:var(--text)}
.sb-note.selected{background:var(--accent-soft);color:var(--accent);font-weight:600}
.sb-note-add{display:flex;align-items:center;gap:6px;color:var(--text-3);margin-top:2px}
.sb-note-add:hover{color:var(--accent)}
.drop-nb.drop-here{background:var(--accent-soft);outline:1.5px dashed var(--accent);outline-offset:-1.5px}
.inline-rename{flex:1;font-size:13px;font-family:inherit;padding:3px 7px;border-radius:5px;border:1px solid var(--accent);background:var(--panel-hi);color:var(--text);outline:none;min-width:0}

/* ============================================================
   5. NOTE LIST PANE
   ============================================================ */
#listpane{background:var(--panel);border-right:1px solid var(--border);display:flex;flex-direction:column;width:var(--listw);min-width:0}
.lp-head{padding:12px 12px 10px;border-bottom:1px solid var(--border);flex-shrink:0}
.lp-title-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px}
#lp-title{font-size:14.5px;font-weight:650;letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0}
.lp-actions{display:flex;gap:6px;flex-shrink:0}
.chip-btn{
  display:inline-flex;align-items:center;gap:5px;height:26px;padding:0 9px;border-radius:7px;
  border:1px solid var(--border-2);background:var(--panel-hi);color:var(--text-2);
  font-size:12px;font-weight:550;cursor:pointer;transition:all .12s;white-space:nowrap;
}
.chip-btn:hover{border-color:var(--text-3);color:var(--text)}
.chip-btn.primary{background:var(--accent);border-color:var(--accent);color:var(--accent-ink)}
.chip-btn.primary:hover{filter:brightness(1.07);color:var(--accent-ink)}
.lp-search-row{display:flex;gap:6px;align-items:center}
.search-box{
  flex:1;display:flex;align-items:center;gap:7px;height:30px;padding:0 10px;border-radius:8px;
  background:var(--panel-2);border:1px solid transparent;color:var(--text-3);transition:all .15s;min-width:0;
}
.search-box:focus-within{background:var(--panel-hi);border-color:var(--accent);color:var(--accent)}
.search-box input{flex:1;border:0;background:none;outline:none;font-size:13px;color:var(--text);font-family:inherit;min-width:0}
.search-box input::placeholder{color:var(--text-3)}
.tag-chip{
  display:inline-flex;align-items:center;gap:4px;height:26px;padding:0 9px;border-radius:999px;
  background:var(--accent-soft);color:var(--accent);font-size:12px;font-weight:600;border:0;cursor:pointer;
}
.tag-chip:hover{background:var(--accent-soft-2)}
#note-list{flex:1;overflow-y:auto;padding:8px 8px 16px}
.note-card{
  padding:10px 12px 9px;border-radius:10px;cursor:pointer;border:1px solid transparent;
  transition:background .12s,border-color .12s;user-select:none;
}
.note-card + .note-card{margin-top:3px}
.note-card:hover{background:var(--hover)}
.note-card.selected{background:var(--accent-soft);border-color:var(--accent-soft-2);box-shadow:inset 3px 0 0 var(--accent)}
.note-card.dragging{opacity:.45}
.nc-line1{display:flex;align-items:center;gap:6px}
.nc-title{flex:1;font-size:13.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;letter-spacing:-.005em;min-width:0}
.nc-pin{color:var(--accent);display:grid;place-items:center;flex-shrink:0}
.nc-kebab{opacity:0;background:none;border:0;color:var(--text-3);width:20px;height:20px;border-radius:5px;display:grid;place-items:center;cursor:pointer;padding:0;flex-shrink:0;transition:opacity .12s}
.note-card:hover .nc-kebab,.note-card.selected .nc-kebab{opacity:1}
.nc-kebab:hover{background:var(--hover-2);color:var(--text)}
.nc-snippet{font-size:12.5px;color:var(--text-2);line-height:1.45;margin:3px 0 6px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.nc-snippet .dim{color:var(--text-3);font-style:italic}
.nc-line2{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--text-3)}
.nc-nb{background:var(--panel-2);padding:1px 6px;border-radius:4px;font-size:10.5px;white-space:nowrap}
.nc-tags{display:flex;gap:4px;margin-left:auto;flex-wrap:wrap;justify-content:flex-end}
.nc-tag{border:0;background:var(--accent-soft);color:var(--accent);font-size:10.5px;font-weight:600;padding:1.5px 7px;border-radius:999px;cursor:pointer;transition:background .12s}
.nc-tag:hover{background:var(--accent-soft-2)}
mark{background:var(--mark);color:inherit;border-radius:3px;padding:0 1px}
.list-empty{display:flex;flex-direction:column;align-items:center;gap:6px;padding:48px 20px;text-align:center;color:var(--text-3)}
.le-ic{opacity:.5;margin-bottom:4px}
.le-t{font-size:13.5px;font-weight:600;color:var(--text-2)}
.le-s{font-size:12px}
.list-empty .btn-primary{margin-top:10px}

/* ============================================================
   6. EDITOR PANE — CHROME
   ============================================================ */
#editorpane{flex:1;min-width:0;display:flex;flex-direction:column;background:var(--panel-hi)}
#editor-head{flex-shrink:0}
.ed-head-inner{max-width:780px;margin:0 auto;padding:18px 44px 0}
.eh-title-row{display:flex;align-items:center;gap:10px}
#note-title{
  flex:1;border:0;background:none;outline:none;font-family:var(--font-editor);
  font-size:27px;font-weight:600;color:var(--text);letter-spacing:-.015em;padding:2px 0;min-width:0;
}
#note-title::placeholder{color:var(--text-3);font-weight:500}
.eh-actions{display:flex;gap:4px;flex-shrink:0}
.eh-meta{display:flex;align-items:center;gap:10px;margin:4px 0 10px;font-size:12px;color:var(--text-3);flex-wrap:wrap}
#note-updated{white-space:nowrap}
#tag-row{display:flex;align-items:center;gap:5px;flex-wrap:wrap;flex:1;min-width:0}
.tag-pill{display:inline-flex;align-items:center;gap:3px;background:var(--accent-soft);color:var(--accent);font-size:12px;font-weight:600;padding:2px 4px 2px 9px;border-radius:999px}
.tag-x{border:0;background:none;color:inherit;opacity:.55;cursor:pointer;font-size:13px;line-height:1;padding:2px 5px;border-radius:999px}
.tag-x:hover{opacity:1;background:var(--accent-soft-2)}
.tag-add{border:0;background:none;outline:none;font-size:12px;color:var(--text-2);width:64px;padding:2px 0}
.tag-add::placeholder{color:var(--text-3)}

/* toolbar */
#toolbar{display:flex;align-items:center;gap:1px;flex-wrap:wrap;padding:8px 0 12px;border-bottom:1px solid var(--border)}
.tb-btn{
  display:grid;place-items:center;min-width:28px;height:28px;padding:0 5px;border-radius:6px;
  border:0;background:none;color:var(--text-2);cursor:pointer;transition:background .1s,color .1s;
}
.tb-btn:hover{background:var(--hover);color:var(--text)}
.tb-btn.on{background:var(--accent-soft);color:var(--accent)}
.tb-label{font-size:11.5px;font-weight:700;letter-spacing:.02em;font-family:var(--font-ui)}
.tb-sep{width:1px;height:18px;background:var(--border);margin:0 5px}

/* buttons */
.icon-btn{
  display:grid;place-items:center;width:28px;height:28px;border-radius:7px;border:0;background:none;
  color:var(--text-2);cursor:pointer;transition:background .12s,color .12s;padding:0;
}
.icon-btn:hover{background:var(--hover);color:var(--text)}
.icon-btn.on{background:var(--accent-soft);color:var(--accent)}
.icon-btn.sm{width:24px;height:24px;border-radius:6px}
.btn-primary{
  background:var(--accent);color:var(--accent-ink);border:0;padding:8px 16px;border-radius:9px;
  font-size:13px;font-weight:650;cursor:pointer;transition:filter .12s;box-shadow:0 1px 2px rgba(0,0,0,.08);
}
.btn-primary:hover{filter:brightness(1.07)}

/* ============================================================
   7. EDITOR PANE — CONTENT TYPOGRAPHY
   ============================================================ */
#editor-scroll{flex:1;overflow-y:auto}
#editor{
  max-width:780px;margin:0 auto;padding:18px 44px 140px;outline:none;min-height:50vh;
  font-family:var(--font-editor);font-size:16.5px;line-height:1.75;color:var(--text);caret-color:var(--accent);
}
#editor ::selection{background:var(--accent-soft-2)}
#editor > :first-child{margin-top:0}
#editor h1,#editor h2,#editor h3{font-weight:700;letter-spacing:-.015em;line-height:1.3}
#editor h1{font-size:1.8em;margin:.55em 0 .4em}
#editor h2{font-size:1.42em;margin:.55em 0 .35em}
#editor h3{font-size:1.16em;margin:.5em 0 .3em}
#editor p{margin:.45em 0}
#editor ul,#editor ol{margin:.45em 0;padding-left:1.55em}
#editor li{margin:.14em 0}
#editor li::marker{color:var(--text-3)}
#editor blockquote{margin:.7em 0;padding:.15em 0 .15em 1.05em;border-left:3px solid var(--accent);color:var(--text-2);font-style:italic}
#editor blockquote p{margin:.2em 0}
#editor hr{border:0;height:1px;background:var(--border-2);margin:1.5em 0}
#editor code{
  font-family:var(--font-mono);font-size:.84em;background:var(--panel-2);
  border:1px solid var(--border);border-radius:5px;padding:.12em .4em;
}
#editor b,#editor strong{font-weight:700}
#editor a{color:var(--accent)}

/* checklists */
#editor ul.ck{list-style:none;padding-left:.15em;margin:.35em 0}
#editor ul.ck li{position:relative;padding-left:30px;margin:.18em 0}
.ck-box{
  position:absolute;left:2px;top:6px;width:16px;height:16px;border:1.5px solid var(--border-2);
  border-radius:5px;cursor:pointer;transition:background .12s,border-color .12s;
}
.ck-box:hover{border-color:var(--accent)}
#editor ul.ck li[data-checked="true"] .ck-box{background:var(--accent);border-color:var(--accent)}
#editor ul.ck li[data-checked="true"] .ck-box::after{
  content:'';position:absolute;left:4.5px;top:1px;width:4px;height:8.5px;
  border:solid var(--check-ink);border-width:0 2px 2px 0;transform:rotate(45deg);
}
#editor ul.ck li[data-checked="true"]{color:var(--text-3)}

/* ============================================================
   8. MENUS
   ============================================================ */
.menu{
  position:fixed;z-index:200;min-width:190px;max-width:270px;background:var(--panel-hi);
  border:1px solid var(--border-2);border-radius:11px;box-shadow:var(--shadow);padding:5px;
  animation:menuIn .13s ease;
}
@keyframes menuIn{from{opacity:0;transform:scale(.97) translateY(-2px)}to{opacity:1;transform:none}}
.menu-item{
  display:flex;align-items:center;gap:9px;width:100%;padding:6.5px 9px;border:0;background:none;
  border-radius:7px;font-size:13px;color:var(--text);cursor:pointer;text-align:left;
}
.menu-item:hover{background:var(--hover)}
.menu-item svg{color:var(--text-3);flex-shrink:0}
.menu-item.danger{color:var(--danger)}
.menu-item.danger svg{color:var(--danger)}
.menu-item.danger:hover{background:var(--danger-soft)}
.menu-sep{height:1px;background:var(--border);margin:5px 8px}

/* ============================================================
   9. EMPTY STATE
   ============================================================ */
#empty-state{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;text-align:center;padding:40px}
.es-icon{width:60px;height:60px;border-radius:18px;background:var(--panel-2);color:var(--text-3);display:grid;place-items:center;margin-bottom:6px}
.es-title{font-family:var(--font-editor);font-size:20px;font-weight:600;color:var(--text-2)}
.es-sub{font-size:13px;color:var(--text-3);margin-bottom:10px}

/* ============================================================
   10. TOAST
   ============================================================ */
#toast{
  position:fixed;left:50%;bottom:22px;transform:translateX(-50%) translateY(12px);
  display:flex;align-items:center;gap:12px;background:var(--panel-hi);color:var(--text);
  border:1px solid var(--border-2);border-radius:11px;box-shadow:var(--shadow);
  padding:9px 10px 9px 14px;font-size:13px;z-index:300;opacity:0;pointer-events:none;
  transition:opacity .18s,transform .18s;max-width:min(440px,80vw);
}
#toast.show{opacity:1;transform:translateX(-50%) translateY(0);pointer-events:auto}
#toast .toast-msg{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#toast .toast-action{
  border:0;background:none;color:var(--accent);font-weight:700;font-size:13px;cursor:pointer;
  padding:4px 10px;border-radius:7px;
}
#toast .toast-action:hover{background:var(--accent-soft)}

/* ============================================================
   11. SCROLLBARS, FOCUS, MISC
   ============================================================ */
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--border-2);border-radius:8px;border:3px solid transparent;background-clip:content-box}
::-webkit-scrollbar-thumb:hover{background:var(--text-3);border:3px solid transparent;background-clip:content-box}
:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
button:focus:not(:focus-visible){outline:none}
.note-card:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
</style>
</head>
<body>
<div id="app">
  <header id="topbar">
    <div class="tb-left">
      <button id="btn-sidebar" class="icon-btn" title="Toggle sidebar"></button>
      <div class="brand"><span class="brand-mark" id="brand-mark"></span>Quill</div>
    </div>
    <div class="tb-right">
      <div id="save-ind" class="save-ind"><span id="save-ind-ic"></span><span id="save-ind-text">Saved</span></div>
      <button id="btn-theme" class="icon-btn" title="Toggle dark mode"></button>
    </div>
  </header>
  <div id="panes">
    <aside id="sidebar">
      <div class="sb-head">
        <span class="sb-title">Notebooks</span>
        <div class="sb-head-actions">
          <button id="btn-add-notebook" class="icon-btn sm" title="New notebook"></button>
        </div>
      </div>
      <div id="sb-body"></div>
    </aside>
    <div id="handle-sb" class="handle"></div>
    <section id="listpane">
      <div class="lp-head">
        <div class="lp-title-row">
          <h1 id="lp-title">All Notes</h1>
          <div class="lp-actions">
            <button id="btn-sort" class="chip-btn" title="Sort order"></button>
            <button id="btn-new-note" class="chip-btn primary" title="Create a new note"></button>
          </div>
        </div>
        <div class="lp-search-row">
          <div class="search-box"><span id="search-ic"></span><input id="search" type="text" placeholder="Search notes…  ( / )" spellcheck="false" autocomplete="off"></div>
          <button id="tag-chip" class="tag-chip hidden" title="Clear tag filter"></button>
        </div>
      </div>
      <div id="note-list"></div>
    </section>
    <div id="handle-list" class="handle"></div>
    <main id="editorpane">
      <div id="editor-head">
        <div class="ed-head-inner">
          <div class="eh-title-row">
            <input id="note-title" placeholder="Untitled note" spellcheck="false" autocomplete="off">
            <div class="eh-actions">
              <button id="btn-pin" class="icon-btn" title="Pin note to top"></button>
              <button id="btn-note-menu" class="icon-btn" title="Note options"></button>
            </div>
          </div>
          <div class="eh-meta">
            <span id="note-updated"></span>
            <div id="tag-row"></div>
          </div>
          <div id="toolbar"></div>
        </div>
      </div>
      <div id="editor-scroll">
        <div id="editor" contenteditable="true" spellcheck="true"></div>
      </div>
      <div id="empty-state">
        <div class="es-icon" id="es-icon"></div>
        <div class="es-title">Select or create a note</div>
        <div class="es-sub">Choose a note from the list, or start a new one.</div>
        <button id="btn-empty-new" class="btn-primary">New note</button>
      </div>
    </main>
  </div>
</div>
<script>
'use strict';
/* ============================================================
   1. TUNABLE CONSTANTS
   ============================================================ */
const CONFIG = {
  STORAGE_KEY: 'quill.notes.v1',
  AUTOSAVE_MS: 500,          // debounce for localStorage writes
  SNIPPET_LEN: 150,          // approx. chars in list snippets
  UNDO_LIMIT: 200,           // max undo steps kept per note
  LIST_REFRESH_MS: 350,      // debounce for re-rendering list while typing
  MIN_SIDEBAR: 200, MAX_SIDEBAR: 420,
  MIN_LIST: 250,    MAX_LIST: 520,
};

/* ============================================================
   2. ICONS (inline SVG, stroke-based)
   ============================================================ */
const ICONS = {
  plus:      '<path d="M12 5v14M5 12h14"/>',
  search:    '<circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/>',
  x:         '<path d="M18 6 6 18M6 6l12 12"/>',
  check:     '<path d="M20 6 9 17l-5-5"/>',
  chevron:   '<path d="m6 9 6 6 6-6"/>',
  pin:       '<path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1z"/>',
  trash:     '<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>',
  restore:   '<path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>',
  kebab:     '<circle cx="12" cy="5" r="1.6" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="12" cy="19" r="1.6" fill="currentColor" stroke="none"/>',
  moon:      '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>',
  sun:       '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>',
  bold:      '<path d="M6 4h8a4 4 0 0 1 0 8H6z"/><path d="M6 12h9a4 4 0 0 1 0 8H6z"/>',
  italic:    '<path d="M19 4h-9M14 20H5M15 4 9 20"/>',
  underline: '<path d="M6 4v6a6 6 0 0 0 12 0V4"/><path d="M4 20h16"/>',
  strike:    '<path d="M16 4H9a3 3 0 0 0-2.83 4"/><path d="M14 12a4 4 0 0 1 0 8H6"/><path d="M4 12h16"/>',
  code:      '<path d="m16 18 6-6-6-6M8 6l-6 6 6 6"/>',
  list:      '<path d="M8 6h13M8 12h13M8 18h13"/><circle cx="3.5" cy="6" r="1.3" fill="currentColor" stroke="none"/><circle cx="3.5" cy="12" r="1.3" fill="currentColor" stroke="none"/><circle cx="3.5" cy="18" r="1.3" fill="currentColor" stroke="none"/>',
  listol:    '<path d="M10 6h11M10 12h11M10 18h11"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/>',
  checklist: '<rect x="3" y="4" width="7" height="7" rx="1.5"/><path d="m4.5 7.5 1.5 1.5 2.5-2.5"/><path d="M14 8h7"/><rect x="3" y="13" width="7" height="7" rx="1.5"/><path d="M14 16h7"/>',
  quote:     '<path d="M10 11H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v6c0 2-1 3.5-3 4"/><path d="M19 11h-4a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v6c0 2-1 3.5-3 4"/>',
  hr:        '<path d="M4 12h16"/><path d="M8 8h8" opacity=".4"/><path d="M8 16h8" opacity=".4"/>',
  note:      '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M9 13h6M9 17h4"/>',
  folder:    '<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>',
  inbox:     '<path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>',
  tag:       '<path d="M20.59 13.41 12 22l-9-9V4a1 1 0 0 1 1-1h9z"/><circle cx="7.5" cy="7.5" r="1" fill="currentColor" stroke="none"/>',
  copy:      '<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>',
  sort:      '<path d="M3 6h13M3 12h9M3 18h5"/><path d="m17 14 3 3 3-3M20 17V4"/>',
  sidebar:   '<rect x="3" y="3" width="18" height="18" rx="2.5"/><path d="M9 3v18"/>',
  edit:      '<path d="M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5z"/>',
  feather:   '<path d="M20.24 12.24a6 6 0 0 0-8.49-8.49L5 10.5V19h8.5z"/><path d="M16 8 2 22"/><path d="M17.5 15H9"/>',
  corner:    '<path d="M15 14 20 9l-5-5"/><path d="M4 20v-7a4 4 0 0 1 4-4h12"/>',
};
function icon(name, size){
  size = size || 16;
  return '<svg width="'+size+'" height="'+size+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'+(ICONS[name]||'')+'</svg>';
}

/* ============================================================
   3. SMALL UTILITIES
   ============================================================ */
function $(sel){ return document.querySelector(sel); }
function div(cls){ const d = document.createElement('div'); if (cls) d.className = cls; return d; }
function escapeHtml(s){
  return String(s).replace(/[&<>"']/g, function(c){
    return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];
  });
}
function clamp(v, a, b){ return Math.max(a, Math.min(b, v)); }
function uid(){ return Date.now().toString(36) + Math.random().toString(36).slice(2, 8); }
function fmtTime(ts){
  const d = new Date(ts), now = new Date();
  const hm = String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0');
  if (d.toDateString() === now.toDateString()) return hm;
  const mon = d.toLocaleString('en-US', { month:'short', day:'numeric' });
  return d.getFullYear() === now.getFullYear() ? mon : mon + ', ' + d.getFullYear();
}

/* ============================================================
   4. DATA MODEL & PERSISTENCE
   Single source of truth: `state`. The DOM is a projection.
   ============================================================ */
let state = null;

function defaultUi(){
  return {
    scope: 'all',          // 'all' | 'trash' | notebookId
    noteId: null,
    sort: 'modified',      // 'modified' | 'created' | 'alpha'
    theme: 'light',
    sidebarW: 264,
    listW: 320,
    sidebarOpen: true,
    tagFilter: null,
    search: '',
  };
}

function seedState(){
  const now = Date.now(), H = 3600e3, D = 24*H;
  const nbGeneral = { id: uid(), name: 'General', collapsed: false };
  const nbWork    = { id: uid(), name: 'Work',    collapsed: false };
  const nbReading = { id: uid(), name: 'Reading', collapsed: true };
  const welcome = {
    id: uid(), notebookId: nbGeneral.id, title: 'Welcome to Quill',
    html: '<h2>Welcome to Quill</h2><p>Quill is a quiet place for your notes. Everything you type is <b>saved automatically</b> in this browser — reload the page and it is all still here.</p><h3>Things to try</h3><ul class="ck"><li data-checked="true"><span class="ck-box"></span>Open a note and start typing</li><li data-checked="false"><span class="ck-box"></span>Type <code># </code> at the start of a line to make a heading</li><li data-checked="false"><span class="ck-box"></span>Drag this note onto another notebook in the sidebar</li><li data-checked="false"><span class="ck-box"></span>Press <code>Ctrl + Z</code> — undo understands your formatting</li></ul><blockquote>Tip: the search box finds anything, even words hidden inside <b>bold</b> or <i>italic</i> text.</blockquote>',
    tags: ['getting-started'], pinned: true, createdAt: now - 2*D, updatedAt: now - 2*H, deletedAt: null,
  };
  const notes = [
    welcome,
    {
      id: uid(), notebookId: nbGeneral.id, title: 'Formatting cheat sheet',
      html: '<p>Shortcuts and gestures worth knowing:</p><ul><li><b>Bold</b> — <code>Ctrl/⌘ B</code></li><li><i>Italic</i> — <code>Ctrl/⌘ I</code></li><li><u>Underline</u> — <code>Ctrl/⌘ U</code></li><li><code>Inline code</code> — toolbar</li><li>Bullets — type <code>- </code> at line start</li><li>Checklist — type <code>[] </code> at line start</li><li>Quote — type <code>&gt; </code> at line start</li></ul><hr><p>Headings, numbered lists and horizontal rules live in the toolbar above.</p>',
      tags: ['reference'], pinned: false, createdAt: now - 2*D, updatedAt: now - 5*H, deletedAt: null,
    },
    {
      id: uid(), notebookId: nbWork.id, title: 'Project kickoff — draft',
      html: '<p>Goals for the next sprint. <b>Decision needed:</b> firm up the shipping date.</p><ul class="ck"><li data-checked="false"><span class="ck-box"></span>Outline the onboarding flow</li><li data-checked="false"><span class="ck-box"></span>Interview three beta users</li><li data-checked="true"><span class="ck-box"></span>Book the demo room</li></ul>',
      tags: ['work','q3'], pinned: false, createdAt: now - D, updatedAt: now - 30*60e3, deletedAt: null,
    },
    {
      id: uid(), notebookId: nbReading.id, title: 'To read',
      html: '<p>Books and papers queued for the next month.</p><ul><li>Deep Work — Cal Newport</li><li>The Design of Everyday Things</li></ul>',
      tags: [], pinned: false, createdAt: now - 6*D, updatedAt: now - 2*D, deletedAt: null,
    },
  ];
  const ui = defaultUi();
  ui.noteId = welcome.id;
  return { notebooks: [nbGeneral, nbWork, nbReading], notes: notes, ui: ui };
}

function loadState(){
  let raw = null;
  try { raw = localStorage.getItem(CONFIG.STORAGE_KEY); } catch(e){}
  if (raw){
    try {
      const s = JSON.parse(raw);
      if (s && Array.isArray(s.notebooks) && Array.isArray(s.notes) && s.ui){
        s.ui = Object.assign(defaultUi(), s.ui);
        state = s;
        return;
      }
    } catch(e){ /* corrupted — reseed */ }
  }
  state = seedState();
  persist();
}

function persist(){
  try { localStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify(state)); } catch(e){}
}

/* saved indicator */
let saveTimer = null, fadeTimer = null;
let saveInd, saveIndText, saveIndIc;
function setSaveInd(text, show){
  saveIndText.textContent = text;
  saveIndIc.innerHTML = icon('check', 13);
  saveInd.classList.add('show');
  clearTimeout(fadeTimer);
  if (text === 'Saved') fadeTimer = setTimeout(function(){ saveInd.classList.remove('show'); }, 1500);
}
function scheduleSave(){
  setSaveInd('Saving…', true);
  clearTimeout(saveTimer);
  saveTimer = setTimeout(function(){ persist(); setSaveInd('Saved', true); }, CONFIG.AUTOSAVE_MS);
}
function flushSave(){
  clearTimeout(saveTimer);
  persist();
}

function getNote(id){ return state.notes.find(function(n){ return n.id === id; }) || null; }
function getNotebook(id){ return state.notebooks.find(function(n){ return n.id === id; }) || null; }
function firstNotebookId(){ return state.notebooks.length ? state.notebooks[0].id : null; }

/* ============================================================
   5. TEXT EXTRACTION & SNIPPETS
   Pure functions over stored HTML — feed search and previews.
   Never matches markup: tags are stripped before matching.
   ============================================================ */
function htmlToText(html){
  const host = document.createElement('div');
  host.innerHTML = html || '';
  host.querySelectorAll('script,style,iframe,object,embed').forEach(function(n){ n.remove(); });
  const lines = [];
  const norm = function(s){ return String(s).replace(/\s+/g, ' ').trim(); };
  const visit = function(node){
    if (node.nodeType === 3){ lines.push(node.nodeValue); return; }
    if (node.nodeType !== 1) return;
    const tag = node.tagName;
    if (tag === 'BR'){ lines.push('\n'); return; }
    if (tag === 'LI'){
      const t = norm(node.textContent);
      if (t) lines.push(t + '\n');
      return;
    }
    if (/^(H1|H2|H3|P|BLOCKQUOTE|UL|OL|DIV|PRE|TABLE)$/.test(tag)){
      for (const c of node.childNodes) visit(c);
      lines.push('\n');
      return;
    }
    for (const c of node.childNodes) visit(c);
  };
  for (const c of host.childNodes) visit(c);
  return lines.join('').split('\n').map(norm).filter(Boolean).join('\n');
}

function makeSnippet(text, q){
  if (!q) {
    const t = text.slice(0, CONFIG.SNIPPET_LEN);
    return escapeHtml(t) + (text.length > CONFIG.SNIPPET_LEN ? '…' : '');
  }
  const idx = text.toLowerCase().indexOf(q.toLowerCase());
  if (idx < 0) return escapeHtml(text.slice(0, CONFIG.SNIPPET_LEN));
  const start = Math.max(0, idx - 40);
  const end = Math.min(text.length, idx + q.length + 110);
  const pre = text.slice(start, idx);
  const match = text.slice(idx, idx + q.length);
  const post = text.slice(idx + q.length, end);
  return (start > 0 ? '…' : '') + escapeHtml(pre) + '<mark>' + escapeHtml(match) + '</mark>' + escapeHtml(post) + (end < text.length ? '…' : '');
}

function highlightText(text, q){
  const i = text.toLowerCase().indexOf(q.toLowerCase());
  if (i < 0) return escapeHtml(text);
  return escapeHtml(text.slice(0, i)) + '<mark>' + escapeHtml(text.slice(i, i + q.length)) + '</mark>' + escapeHtml(text.slice(i + q.length));
}

/* ============================================================
   6. HTML SANITIZATION & CONTENT NORMALIZATION
   Whitelist-based: only supported markup survives paste/load.
   ============================================================ */
const editor = $('#editor');
const ALLOWED_TAGS = new Set(['P','BR','B','I','U','S','STRONG','EM','SUB','SUP','CODE','PRE','H1','H2','H3','H4','UL','OL','LI','BLOCKQUOTE','HR','A','SPAN','DIV']);
const STRIP_TAGS = new Set(['SCRIPT','STYLE','IFRAME','OBJECT','EMBED','LINK','META','BASE','FORM','INPUT','TEXTAREA','BUTTON','VIDEO','AUDIO','CANVAS','SVG','TEMPLATE','NOSCRIPT','IMG','FRAME','FRAMESET']);

function safeHref(v){ return /^(https?:|mailto:|#|\/)/i.test(String(v).trim()); }

function sanitizeHtml(html){
  const host = document.createElement('div');
  host.innerHTML = html;
  cleanNode(host);
  normalizeContent(host);
  return host.innerHTML;
}

function cleanNode(node){
  for (const child of Array.from(node.childNodes)){
    if (child.nodeType !== 1){
      if (child.nodeType !== 3) child.remove();
      continue;
    }
    const tag = child.tagName;
    if (STRIP_TAGS.has(tag)){ child.remove(); continue; }
    if (!ALLOWED_TAGS.has(tag)){
      while (child.firstChild) node.insertBefore(child.firstChild, child);
      child.remove();
      cleanNode(node);
      continue;
    }
    for (const attr of Array.from(child.attributes)){
      const name = attr.name, value = attr.value;
      if (name === 'href' && tag === 'A' && safeHref(value)) continue;
      if (name === 'data-checked' && tag === 'LI' && (value === 'true' || value === 'false')) continue;
      if (name === 'class' && tag === 'UL' && value === 'ck') continue;
      child.removeAttribute(name);
    }
    cleanNode(child);
  }
}

/* Keeps document structure consistent: checklist boxes, block wrapping,
   trailing editable block, no stray empty paragraphs. */
function normalizeContent(host){
  host.querySelectorAll('script,style,iframe,object,embed').forEach(function(n){ n.remove(); });
  host.querySelectorAll('li').forEach(function(li){
    const parent = li.parentElement;
    const isCk = parent && parent.tagName === 'UL' && parent.classList.contains('ck');
    const box = li.querySelector(':scope > .ck-box');
    if (isCk && !box){
      const b = document.createElement('span');
      b.className = 'ck-box';
      li.insertBefore(b, li.firstChild);
    } else if (!isCk && box){
      box.remove();
    }
    if (isCk && li.hasAttribute('data-checked') && li.getAttribute('data-checked') !== 'true'){
      li.setAttribute('data-checked', 'false');
    }
  });
  host.querySelectorAll('div').forEach(function(d){
    const hasBlock = Array.from(d.children).some(function(c){
      return /^(P|H1|H2|H3|H4|UL|OL|BLOCKQUOTE|HR|PRE|DIV)$/.test(c.tagName);
    });
    if (hasBlock){
      while (d.firstChild) d.parentNode.insertBefore(d.firstChild, d);
      d.remove();
    } else {
      const p = document.createElement('p');
      while (d.firstChild) p.appendChild(d.firstChild);
      d.replaceWith(p);
    }
  });
  // unwrap nested blocks (e.g. <p><p>x</p></p> from pasting)
  host.querySelectorAll('p, h1, h2, h3, h4').forEach(function(b){
    const blockChild = Array.from(b.children).some(function(c){
      return /^(P|H1|H2|H3|H4|UL|OL|BLOCKQUOTE|HR|PRE)$/.test(c.tagName);
    });
    if (blockChild){
      while (b.firstChild) b.parentNode.insertBefore(b.firstChild, b);
      b.remove();
    }
  });
  // wrap leading inline content in a paragraph
  let node = host.firstChild;
  const inline = [];
  while (node && (node.nodeType === 3 || (node.nodeType === 1 && !/^(P|H1|H2|H3|H4|UL|OL|BLOCKQUOTE|HR|PRE)$/.test(node.tagName)))){
    inline.push(node);
    node = node.nextSibling;
  }
  if (inline.length){
    const p = document.createElement('p');
    inline.forEach(function(n){ p.appendChild(n); });
    if (node) node.parentNode.insertBefore(p, node);
    else host.appendChild(p);
  }
  // drop empty paragraphs, keep the last one
  host.querySelectorAll('p').forEach(function(p){
    if (!p.textContent.trim() && !p.querySelector('br') && p !== host.lastChild) p.remove();
  });
  if (!host.childNodes.length){ host.innerHTML = '<p><br></p>'; return; }
  const last = host.lastChild;
  if (last.nodeType === 3 || (last.nodeType === 1 && !/^(P|H1|H2|H3|H4|UL|OL|BLOCKQUOTE|PRE)$/.test(last.tagName))){
    const p = document.createElement('p');
    p.innerHTML = '<br>';
    host.appendChild(p);
  }
}

/* ============================================================
   7. SELECTION & UNDO / REDO
   Snapshot-based: every change records pre-state + caret offsets.
   Typing bursts coalesce; structural ops always snapshot.
   ============================================================ */
const undoStack = [], redoStack = [];

function editorSel(){
  const s = window.getSelection();
  if (!s.rangeCount) return null;
  const r = s.getRangeAt(0);
  return editor.contains(r.commonAncestorContainer) ? r : null;
}

function caretOffsets(range){
  const r = document.createRange();
  r.selectNodeContents(editor);
  r.setEnd(range.startContainer, range.startOffset);
  const d = document.createElement('div');
  d.appendChild(r.cloneContents());
  const start = d.textContent.length;
  d.innerHTML = '';
  r.setEnd(range.endContainer, range.endOffset);
  d.appendChild(r.cloneContents());
  return { start: start, end: d.textContent.length };
}

function rangeFromOffsets(start, end){
  const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
  let rem = start, node, range = null;
  while ((node = walker.nextNode())){
    const len = node.textContent.length;
    if (rem <= len){ range = document.createRange(); range.setStart(node, rem); break; }
    rem -= len;
  }
  if (!range){
    range = document.createRange();
    range.selectNodeContents(editor);
    range.collapse(false);
    return range;
  }
  let rem2 = end, node2, endSet = false;
  const w2 = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
  while ((node2 = w2.nextNode())){
    const len = node2.textContent.length;
    if (rem2 <= len){ range.setEnd(node2, rem2); endSet = true; break; }
    rem2 -= len;
  }
  if (!endSet){ range.setEnd(range.startContainer, range.startOffset); }
  return range;
}

function setSelectionRange(range){
  const s = window.getSelection();
  s.removeAllRanges();
  s.addRange(range);
}

function placeCaret(node, collapseToStart){
  const r = document.createRange();
  r.selectNodeContents(node);
  r.collapse(!!collapseToStart);
  setSelectionRange(r);
}
function placeCaretEnd(node){
  const r = document.createRange();
  r.selectNodeContents(node);
  r.collapse(false);
  setSelectionRange(r);
}
function focusEditorEnd(){
  const r = document.createRange();
  r.selectNodeContents(editor);
  r.collapse(false);
  setSelectionRange(r);
  editor.focus();
}

function pushSnapshot(kind){
  const range = editorSel();
  const off = range ? caretOffsets(range) : { start: 0, end: 0 };
  const snap = { html: editor.innerHTML, start: off.start, end: off.end, kind: kind, ts: Date.now() };
  const top = undoStack[undoStack.length - 1];
  if (kind === 'typing' && top && top.kind === 'typing' && (snap.ts - top.ts) < 900){
    top.ts = snap.ts; // coalesce the burst; keep the pre-burst html
  } else {
    undoStack.push(snap);
    if (undoStack.length > CONFIG.UNDO_LIMIT) undoStack.shift();
  }
  redoStack.length = 0;
}
function pushOp(){ pushSnapshot('op'); }

function currentSnap(){
  const range = editorSel();
  const off = range ? caretOffsets(range) : { start: 0, end: 0 };
  return { html: editor.innerHTML, start: off.start, end: off.end, kind: 'op', ts: Date.now() };
}

function applyHtmlWithCaret(snap){
  editor.innerHTML = snap.html;
  normalizeContent(editor);
  setSelectionRange(rangeFromOffsets(snap.start, snap.end));
}

function undo(){
  const snap = undoStack.pop();
  if (!snap || !state.ui.noteId) return;
  redoStack.push(currentSnap());
  applyHtmlWithCaret(snap);
  afterEditorChange();
}
function redo(){
  const snap = redoStack.pop();
  if (!snap) return;
  undoStack.push(currentSnap());
  applyHtmlWithCaret(snap);
  afterEditorChange();
}

/* ============================================================
   8. EDITOR EVENTS — markdown shortcuts, lists, paste
   ============================================================ */
function currentBlock(){
  const range = editorSel();
  if (!range) return null;
  let n = range.startContainer;
  if (n.nodeType === 3) n = n.parentElement;
  if (!n || !editor.contains(n)) return null;
  return n.closest('p, li, h1, h2, h3, blockquote');
}

/* `# ` `## ` `### ` `- ` `[] ` `> ` `1. ` at line start → block conversion */
function handleMarkdownShortcut(e){
  if (e.key !== ' ' || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return;
  const range = editorSel();
  if (!range) return;
  let n = range.startContainer;
  if (n.nodeType === 3) n = n.parentElement;
  const block = n && n.closest('li, p');
  if (!block || !editor.contains(block)) return;
  const r = document.createRange();
  r.selectNodeContents(block);
  r.setEnd(range.startContainer, range.startOffset);
  const before = r.toString();
  const m = before.match(/^(#{1,3}|\[ ?\]|[-*]|\d+\.|>)$/);
  if (!m) return;
  e.preventDefault();
  pushOp();
  const marker = m[1];
  r.deleteContents(); // strip the marker text
  const inner = block.innerHTML;
  let newBlock;
  if (/^#/.test(marker)){
    newBlock = document.createElement('h' + marker.length);
    newBlock.innerHTML = inner;
  } else if (marker === '-' || marker === '*'){
    newBlock = document.createElement('ul');
    const li = document.createElement('li');
    li.innerHTML = inner;
    newBlock.appendChild(li);
  } else if (marker === '[]' || marker === '[ ]'){
    newBlock = document.createElement('ul');
    newBlock.classList.add('ck');
    const li = document.createElement('li');
    li.setAttribute('data-checked', 'false');
    li.innerHTML = inner;
    newBlock.appendChild(li);
  } else if (marker === '>'){
    newBlock = document.createElement('blockquote');
    const p = document.createElement('p');
    p.innerHTML = inner;
    newBlock.appendChild(p);
  } else {
    newBlock = document.createElement('ol');
    const li = document.createElement('li');
    li.innerHTML = inner;
    newBlock.appendChild(li);
  }
  block.replaceWith(newBlock);
  normalizeContent(editor);
  placeCaret(newBlock, true);
  afterEditorChange();
}

/* Enter: continue list, exit empty item */
function handleEnter(e){
  if (e.key !== 'Enter' || e.shiftKey || e.ctrlKey || e.metaKey) return;
  const range = editorSel();
  if (!range) return;
  let n = range.startContainer;
  if (n.nodeType === 3) n = n.parentElement;
  const li = n && n.closest('li');
  if (!li || !editor.contains(li)) return;
  e.preventDefault();
  pushOp();
  if (!li.textContent.trim()){
    const list = li.parentElement;
    const prevLi = li.previousElementSibling;
    li.remove();
    if (!list.children.length){
      const p = document.createElement('p');
      p.innerHTML = '<br>';
      list.replaceWith(p);
      placeCaret(p, true);
    } else if (prevLi){
      placeCaretEnd(prevLi);
    } else {
      placeCaret(list, true);
    }
  } else {
    const r = document.createRange();
    r.setStart(range.startContainer, range.startOffset);
    r.setEnd(range.endContainer, range.endOffset);
    const frag = r.extractContents();
    const newLi = document.createElement('li');
    if (li.parentElement && li.parentElement.classList.contains('ck')) newLi.setAttribute('data-checked', 'false');
    newLi.appendChild(frag);
    if (!newLi.textContent.trim() && !newLi.querySelector('br,img')) newLi.innerHTML = '<br>';
    li.after(newLi);
    placeCaret(newLi, true);
  }
  normalizeContent(editor);
  afterEditorChange();
}

/* Tab / Shift+Tab: indent & outdent list items */
function handleTab(e){
  if (e.key !== 'Tab') return;
  const range = editorSel();
  if (!range) return;
  let n = range.startContainer;
  if (n.nodeType === 3) n = n.parentElement;
  const li = n && n.closest('li');
  if (!li || !editor.contains(li)) return;
  e.preventDefault();
  pushOp();
  if (e.shiftKey) outdentLi(li); else indentLi(li);
  normalizeContent(editor);
  placeCaretEnd(li);
  afterEditorChange();
}
function indentLi(li){
  const prev = li.previousElementSibling;
  if (!prev) return;
  const isCk = li.parentElement.classList.contains('ck');
  let sub = prev.querySelector(':scope > ul, :scope > ol');
  if (!sub){
    sub = document.createElement(isCk ? 'ul' : 'ul');
    if (isCk) sub.classList.add('ck');
    prev.appendChild(sub);
  }
  sub.appendChild(li);
}
function outdentLi(li){
  const list = li.parentElement;
  if (list && list.parentElement && list.parentElement.tagName === 'LI'){
    const parentLi = list.parentElement;
    parentLi.after(li);
    if (!list.children.length) list.remove();
  }
}

/* Backspace at the very start of a list item: merge / de-list */
function handleBackspace(e){
  if (e.key !== 'Backspace') return;
  const range = editorSel();
  if (!range || !range.collapsed) return;
  let n = range.startContainer;
  if (n.nodeType === 3) n = n.parentElement;
  const li = n && n.closest('li');
  if (!li || !editor.contains(li)) return;
  const r = document.createRange();
  r.selectNodeContents(li);
  r.setEnd(range.startContainer, range.startOffset);
  if (r.toString().length > 0) return;
  e.preventDefault();
  pushOp();
  const list = li.parentElement;
  const prevLi = li.previousElementSibling;
  if (prevLi){
    const frag = document.createRange().createContextualFragment(li.innerHTML);
    frag.querySelectorAll('.ck-box').forEach(function(b){ b.remove(); });
    const first = frag.firstChild;
    prevLi.appendChild(frag);
    li.remove();
    const r2 = document.createRange();
    if (first){
      if (first.nodeType === 3) r2.setStart(first, 0);
      else r2.setStartBefore(first);
      r2.collapse(true);
    } else {
      r2.selectNodeContents(prevLi);
      r2.collapse(false);
    }
    setSelectionRange(r2);
  } else {
    const p = document.createElement('p');
    p.innerHTML = li.innerHTML;
    li.remove();
    if (list.previousElementSibling){
      list.insertBefore(p, list);
      if (!list.children.length) list.remove();
    } else {
      list.replaceWith(p);
    }
    placeCaret(p, true);
  }
  normalizeContent(editor);
  afterEditorChange();
}

/* Paste: sanitize to the supported subset */
function handlePaste(e){
  e.preventDefault();
  const html = e.clipboardData.getData('text/html');
  const text = e.clipboardData.getData('text/plain');
  let range = editorSel();
  if (!range) return;
  pushOp();
  const host = document.createElement('div');
  if (html){
    host.innerHTML = sanitizeHtml(html);
  } else if (text){
    const lines = text.replace(/\r/g, '').split('\n');
    if (lines.length === 1){
      host.appendChild(document.createTextNode(lines[0]));
    } else {
      lines.forEach(function(line){
        const p = document.createElement('p');
        if (line) p.textContent = line;
        else p.innerHTML = '<br>';
        host.appendChild(p);
      });
    }
  } else return;
  range.deleteContents();
  range.collapse(true);
  const nodes = Array.from(host.childNodes);
  let last = null;
  nodes.forEach(function(n){
    range.insertNode(n);
    const r2 = document.createRange();
    r2.setStartAfter(n);
    r2.collapse(true);
    setSelectionRange(r2);
    range = r2;
    last = n;
  });
  if (nodes.length) placeCaretEnd(last);
  normalizeContent(editor);
  afterEditorChange();
}

/* ============================================================
   9. FORMATTING OPERATIONS (toolbar actions)
   ============================================================ */
function isRangeInside(range, el){
  const r = document.createRange();
  r.selectNodeContents(el);
  return range.compareBoundaryPoints(Range.END_TO_START, r) >= 0 &&
         range.compareBoundaryPoints(Range.START_TO_END, r) <= 0;
}

function unwrapEl(el){
  const parent = el.parentNode;
  if (!parent) return;
  const frag = document.createRange().createContextualFragment(el.innerHTML);
  const idx = Array.from(parent.childNodes).indexOf(el);
  parent.replaceChild(frag, el);
  const r = document.createRange();
  const ref = parent.childNodes[Math.min(idx, parent.childNodes.length - 1)];
  r.setStartBefore(ref);
  r.collapse(true);
  setSelectionRange(r);
}

function wrapSelection(tag){
  const range = editorSel();
  if (!range) return;
  if (range.collapsed){
    const el = document.createElement(tag);
    el.innerHTML = '&nbsp;';
    range.insertNode(el);
    placeCaret(el, true);
    return;
  }
  const el = document.createElement(tag);
  try {
    range.surroundContents(el);
  } catch(err){
    const frag = range.extractContents();
    el.appendChild(frag);
    range.insertNode(el);
  }
  placeCaret(el, true);
}

function applyInline(tag){
  const range = editorSel();
  if (!range) return;
  const anc = range.commonAncestorContainer;
  const ancEl = anc.nodeType === 1 ? anc : (anc.parentElement || null);
  const holder = ancEl ? ancEl.closest(tag) : null;
  if (holder && isRangeInside(range, holder)){
    pushOp();
    unwrapEl(holder);
    afterEditorChange();
    return;
  }
  pushOp();
  if (tag === 'code'){
    wrapSelection('code');
  } else {
    const cmd = { b: 'bold', i: 'italic', u: 'underline', s: 'strikeThrough' }[tag];
    let ok = false;
    try { ok = document.execCommand(cmd); } catch(err){ ok = false; }
    if (!ok) wrapSelection(tag);
  }
  afterEditorChange();
}

function applyBlock(tag){
  const block = currentBlock();
  if (!block) return;
  pushOp();
  const nextTag = block.tagName === tag ? 'P' : tag;
  const el = document.createElement(nextTag);
  el.innerHTML = block.innerHTML;
  block.replaceWith(el);
  normalizeContent(editor);
  placeCaret(el, true);
  afterEditorChange();
}

function toggleList(type){
  const block = currentBlock();
  if (!block) return;
  pushOp();
  if (block.tagName === 'LI'){
    const list = block.parentElement;
    const isCk = list.classList.contains('ck');
    const isOl = list.tagName === 'OL';
    const same = (type === 'ul' && !isCk && !isOl) || (type === 'ol' && !isCk && isOl) || (type === 'ck' && isCk);
    if (same){
      const p = document.createElement('p');
      p.innerHTML = block.innerHTML;
      block.replaceWith(p);
      if (!list.children.length) list.remove();
      normalizeContent(editor);
      placeCaret(p, true);
      afterEditorChange();
      return;
    }
    const newList = document.createElement(type === 'ol' ? 'ol' : 'ul');
    if (type === 'ck') newList.classList.add('ck');
    while (list.firstChild) newList.appendChild(list.firstChild);
    list.replaceWith(newList);
    if (type !== 'ck') newList.querySelectorAll(':scope > li > .ck-box').forEach(function(b){ b.remove(); });
    normalizeContent(editor);
    placeCaret(block, true);
    afterEditorChange();
    return;
  }
  if (block.tagName === 'P'){
    const list = document.createElement(type === 'ol' ? 'ol' : 'ul');
    if (type === 'ck') list.classList.add('ck');
    const li = document.createElement('li');
    if (type === 'ck') li.setAttribute('data-checked', 'false');
    li.innerHTML = block.innerHTML;
    list.appendChild(li);
    block.replaceWith(list);
    normalizeContent(editor);
    placeCaret(li, true);
    afterEditorChange();
  }
}

function toggleQuote(){
  const block = currentBlock();
  if (!block) return;
  pushOp();
  const bq = block.closest('blockquote');
  if (bq){
    while (bq.firstChild) bq.parentNode.insertBefore(bq.firstChild, bq);
    bq.remove();
    normalizeContent(editor);
    placeCaret(block, true);
  } else {
    const bq2 = document.createElement('blockquote');
    const p = document.createElement('p');
    p.innerHTML = block.innerHTML;
    bq2.appendChild(p);
    block.replaceWith(bq2);
    normalizeContent(editor);
    placeCaret(p, true);
  }
  afterEditorChange();
}

function insertHr(){
  const block = currentBlock();
  if (!block) return;
  pushOp();
  const hr = document.createElement('hr');
  const p = document.createElement('p');
  p.innerHTML = '<br>';
  block.after(hr);
  hr.after(p);
  normalizeContent(editor);
  placeCaret(p, true);
  afterEditorChange();
}

/* toolbar state at caret */
const toolbarBtns = {};
function updateToolbarState(){
  const range = editorSel();
  const anc = range ? range.commonAncestorContainer : null;
  const ancEl = anc ? (anc.nodeType === 1 ? anc : anc.parentElement) : null;
  const setOn = function(key, on){
    const b = toolbarBtns[key];
    if (b) b.classList.toggle('on', !!on);
  };
  if (!ancEl || !editor.contains(ancEl)){
    Object.keys(toolbarBtns).forEach(function(k){ setOn(k, false); });
    return;
  }
  setOn('b', !!ancEl.closest('b, strong'));
  setOn('i', !!ancEl.closest('i, em'));
  setOn('u', !!ancEl.closest('u'));
  setOn('s', !!ancEl.closest('s, strike'));
  setOn('code', !!ancEl.closest('code'));
  const block = ancEl.closest('p, li, h1, h2, h3, blockquote');
  setOn('h1', !!block && block.tagName === 'H1');
  setOn('h2', !!block && block.tagName === 'H2');
  setOn('h3', !!block && block.tagName === 'H3');
  const list = block && block.tagName === 'LI' ? block.parentElement : (block ? block.closest('ul, ol') : null);
  const isCk = !!(list && list.classList && list.classList.contains('ck'));
  setOn('ul', !!list && list.tagName === 'UL' && !isCk);
  setOn('ol', !!list && list.tagName === 'OL');
  setOn('ck', isCk);
  setOn('quote', !!ancEl.closest('blockquote'));
}

function buildToolbar(){
  const tb = $('#toolbar');
  const defs = [
    { key: 'b', icon: 'bold', title: 'Bold (Ctrl+B)', fn: function(){ applyInline('b'); } },
    { key: 'i', icon: 'italic', title: 'Italic (Ctrl+I)', fn: function(){ applyInline('i'); } },
    { key: 'u', icon: 'underline', title: 'Underline (Ctrl+U)', fn: function(){ applyInline('u'); } },
    { key: 's', icon: 'strike', title: 'Strikethrough', fn: function(){ applyInline('s'); } },
    { key: 'code', icon: 'code', title: 'Inline code', fn: function(){ applyInline('code'); } },
    { sep: true },
    { key: 'h1', label: 'H1', title: 'Heading 1', fn: function(){ applyBlock('H1'); } },
    { key: 'h2', label: 'H2', title: 'Heading 2', fn: function(){ applyBlock('H2'); } },
    { key: 'h3', label: 'H3', title: 'Heading 3', fn: function(){ applyBlock('H3'); } },
    { sep: true },
    { key: 'ul', icon: 'list', title: 'Bulleted list', fn: function(){ toggleList('ul'); } },
    { key: 'ol', icon: 'listol', title: 'Numbered list', fn: function(){ toggleList('ol'); } },
    { key: 'ck', icon: 'checklist', title: 'Checklist', fn: function(){ toggleList('ck'); } },
    { key: 'quote', icon: 'quote', title: 'Blockquote', fn: toggleQuote },
    { key: 'hr', icon: 'hr', title: 'Horizontal rule', fn: insertHr },
  ];
  defs.forEach(function(d){
    if (d.sep){ tb.appendChild(div('tb-sep')); return; }
    const b = document.createElement('button');
    b.type = 'button';
    b.className = 'tb-btn';
    b.title = d.title;
    b.innerHTML = d.label ? '<span class="tb-label">' + d.label + '</span>' : icon(d.icon, 16);
    b.addEventListener('mousedown', function(e){ e.preventDefault(); }); // keep editor selection
    b.addEventListener('click', function(){
      d.fn();
      editor.focus();
      updateToolbarState();
    });
    toolbarBtns[d.key] = b;
    tb.appendChild(b);
  });
}

/* ============================================================
   10. MODEL MUTATIONS (notes & notebooks)
   ============================================================ */
function createNote(notebookId){
  let nbId = notebookId || (state.ui.scope !== 'all' && state.ui.scope !== 'trash' ? state.ui.scope : null) || firstNotebookId();
  if (!nbId){
    const nb = { id: uid(), name: 'General', collapsed: false };
    state.notebooks.push(nb);
    nbId = nb.id;
  }
  const now = Date.now();
  const note = {
    id: uid(), notebookId: nbId, title: '', html: '<p><br></p>', tags: [],
    pinned: false, createdAt: now, updatedAt: now, deletedAt: null,
  };
  state.notes.push(note);
  state.ui.noteId = note.id;
  scheduleSave();
  renderAll();
  const ti = $('#note-title');
  ti.focus();
}

function selectNote(id, opts){
  state.ui.noteId = id;
  scheduleSave();
  renderList();
  renderSidebar();
  renderEditor();
  if (opts && opts.focus){
    editor.focus();
    focusEditorEnd();
  }
}

function moveNote(id, nbId){
  const note = getNote(id);
  if (!note) return;
  note.notebookId = nbId;
  note.deletedAt = null;
  scheduleSave();
  renderAll();
}

function togglePin(id){
  const note = getNote(id);
  if (!note) return;
  note.pinned = !note.pinned;
  scheduleSave();
  renderList();
  renderEditor();
}

function duplicateNote(id){
  const note = getNote(id);
  if (!note) return;
  const now = Date.now();
  const idx = state.notes.findIndex(function(n){ return n.id === id; });
  const copy = {
    id: uid(), notebookId: note.notebookId,
    title: (note.title || 'Untitled') + ' (copy)',
    html: note.html, tags: (note.tags || []).slice(),
    pinned: false, createdAt: now, updatedAt: now, deletedAt: null,
  };
  state.notes.splice(idx + 1, 0, copy);
  state.ui.noteId = copy.id;
  scheduleSave();
  renderAll();
}

function deleteNote(id){
  const note = getNote(id);
  if (!note) return;
  const title = note.title || 'Untitled note';
  note.deletedAt = Date.now();
  if (state.ui.noteId === id) state.ui.noteId = null;
  scheduleSave();
  renderAll();
  showToast('“' + title + '” moved to Trash', 'Undo', function(){ restoreNote(id); });
}

function restoreNote(id){
  const note = getNote(id);
  if (!note) return;
  note.deletedAt = null;
  scheduleSave();
  renderAll();
}

function destroyNote(id){
  const idx = state.notes.findIndex(function(n){ return n.id === id; });
  if (idx < 0) return;
  state.notes.splice(idx, 1);
  if (state.ui.noteId === id) state.ui.noteId = null;
  scheduleSave();
  renderAll();
}

function addTag(noteId, tag){
  const note = getNote(noteId);
  if (!note) return;
  tag = tag.trim().replace(/^#+/, '');
  if (!tag) return;
  note.tags = note.tags || [];
  if (note.tags.some(function(t){ return t.toLowerCase() === tag.toLowerCase(); })) return;
  note.tags.push(tag);
  scheduleSave();
  renderTags(note);
  renderList();
}

function removeTag(noteId, tag){
  const note = getNote(noteId);
  if (!note) return;
  note.tags = (note.tags || []).filter(function(t){ return t !== tag; });
  scheduleSave();
  renderTags(note);
  renderList();
}

function setScope(scope){
  state.ui.scope = scope;
  state.ui.tagFilter = null;
  scheduleSave();
  renderSidebar();
  renderList();
}

function createNotebook(){
  const nb = { id: uid(), name: 'New Notebook', collapsed: false };
  state.notebooks.push(nb);
  scheduleSave();
  renderSidebar();
  const item = document.querySelector('#sb-body .sb-item[data-nb="' + nb.id + '"]');
  if (item) startRenameNotebook(nb, item);
}

function deleteNotebook(nb){
  let target = state.notebooks.find(function(n){ return n.id !== nb.id; }) || null;
  if (!target){
    target = { id: uid(), name: 'General', collapsed: false };
    state.notebooks.push(target);
  }
  state.notes.forEach(function(n){
    if (n.notebookId === nb.id) n.notebookId = target.id;
  });
  state.notebooks = state.notebooks.filter(function(n){ return n.id !== nb.id; });
  if (state.ui.scope === nb.id) state.ui.scope = 'all';
  scheduleSave();
  renderAll();
}

function startRenameNotebook(nb, item){
  const lab = item.querySelector('.sb-label');
  if (!lab || lab.querySelector('input')) return;
  const input = document.createElement('input');
  input.className = 'inline-rename';
  input.value = nb.name;
  lab.textContent = '';
  lab.appendChild(input);
  input.focus();
  input.select();
  let done = false;
  const commit = function(ok){
    if (done) return;
    done = true;
    const v = input.value.trim();
    if (ok && v) nb.name = v;
    scheduleSave();
    renderSidebar();
  };
  input.addEventListener('keydown', function(e){
    e.stopPropagation();
    if (e.key === 'Enter') commit(true);
    if (e.key === 'Escape') commit(false);
  });
  input.addEventListener('blur', function(){ commit(true); });
}

/* ============================================================
   11. RENDERING — sidebar, note list, editor, tags
   The DOM is a projection of `state`.
   ============================================================ */
let dragNoteId = null;
let listTimer = null;

function countFor(scope){
  if (scope === 'trash') return state.notes.filter(function(n){ return n.deletedAt; }).length;
  if (scope === 'all') return state.notes.filter(function(n){ return !n.deletedAt; }).length;
  return state.notes.filter(function(n){ return !n.deletedAt && n.notebookId === scope; }).length;
}

function sbItem(iconName, label, count, selected, onClick, opts){
  opts = opts || {};
  const item = div('sb-item' + (selected ? ' selected' : ''));
  if (opts.nb) item.dataset.nb = opts.nb.id;
  const ic = div('sb-ic');
  ic.innerHTML = icon(iconName, 15);
  item.appendChild(ic);
  const lab = div('sb-label');
  lab.textContent = label;
  item.appendChild(lab);
  const c = div('sb-count');
  c.textContent = count;
  item.appendChild(c);
  if (opts.nb){
    const chev = document.createElement('button');
    chev.className = 'sb-chev';
    chev.title = opts.nb.collapsed ? 'Expand' : 'Collapse';
    chev.innerHTML = icon('chevron', 13);
    chev.addEventListener('click', function(e){
      e.stopPropagation();
      opts.nb.collapsed = !opts.nb.collapsed;
      scheduleSave();
      renderSidebar();
    });
    item.appendChild(chev);
    const kebab = document.createElement('button');
    kebab.className = 'sb-kebab';
    kebab.title = 'Notebook options';
    kebab.innerHTML = icon('kebab', 14);
    kebab.addEventListener('click', function(e){
      e.stopPropagation();
      openNotebookMenu(kebab, opts.nb);
    });
    item.appendChild(kebab);
    item.addEventListener('dblclick', function(){ startRenameNotebook(opts.nb, item); });
    item.addEventListener('dragover', function(e){
      if (dragNoteId){ e.preventDefault(); item.classList.add('drop-here'); }
    });
    item.addEventListener('dragleave', function(){ item.classList.remove('drop-here'); });
    item.addEventListener('drop', function(e){
      if (!dragNoteId) return;
      e.preventDefault();
      item.classList.remove('drop-here');
      moveNote(dragNoteId, opts.nb.id);
    });
  }
  if (opts.trash){
    item.addEventListener('dragover', function(e){
      if (dragNoteId){ e.preventDefault(); item.classList.add('drop-here'); }
    });
    item.addEventListener('dragleave', function(){ item.classList.remove('drop-here'); });
    item.addEventListener('drop', function(e){
      if (!dragNoteId) return;
      e.preventDefault();
      item.classList.remove('drop-here');
      deleteNote(dragNoteId);
    });
  }
  item.addEventListener('click', onClick);
  return item;
}

function renderSidebar(){
  const body = $('#sb-body');
  body.innerHTML = '';
  body.appendChild(sbItem('inbox', 'All Notes', countFor('all'), state.ui.scope === 'all', function(){ setScope('all'); }));
  state.notebooks.forEach(function(nb){
    body.appendChild(sbItem('folder', nb.name, countFor(nb.id), state.ui.scope === nb.id, function(){ setScope(nb.id); }, { nb: nb }));
    const notes = state.notes.filter(function(n){ return !n.deletedAt && n.notebookId === nb.id; });
    if (!nb.collapsed && notes.length){
      const wrap = div('sb-notes');
      notes.forEach(function(n){
        const row = div('sb-note' + (state.ui.noteId === n.id ? ' selected' : ''));
        row.textContent = n.title || 'Untitled note';
        row.title = n.title || 'Untitled note';
        row.addEventListener('click', function(){ selectNote(n.id, { focus: false }); });
        wrap.appendChild(row);
      });
      const add = div('sb-note sb-note-add');
      add.innerHTML = icon('plus', 13) + '<span>New note</span>';
      add.addEventListener('click', function(){ createNote(nb.id); });
      wrap.appendChild(add);
      body.appendChild(wrap);
    }
  });
  body.appendChild(div('sb-sep'));
  const trashed = state.notes.filter(function(n){ return n.deletedAt; });
  body.appendChild(sbItem('trash', 'Trash', trashed.length, state.ui.scope === 'trash', function(){ setScope('trash'); }, { trash: true }));
  if (state.ui.scope === 'trash' && trashed.length){
    const wrap = div('sb-notes');
    trashed.forEach(function(n){
      const row = div('sb-note' + (state.ui.noteId === n.id ? ' selected' : ''));
      row.textContent = n.title || 'Untitled note';
      row.addEventListener('click', function(){ selectNote(n.id, { focus: false }); });
      wrap.appendChild(row);
    });
    body.appendChild(wrap);
  }
}

const SORT_LABELS = { modified: 'Modified', created: 'Created', alpha: 'A–Z' };
const SORT_ORDER = ['modified', 'created', 'alpha'];

function visibleNotes(){
  const q = state.ui.search.trim().toLowerCase();
  let notes = state.notes.slice();
  if (state.ui.scope === 'trash') notes = notes.filter(function(n){ return n.deletedAt; });
  else notes = notes.filter(function(n){
    return !n.deletedAt && (state.ui.scope === 'all' || n.notebookId === state.ui.scope);
  });
  if (state.ui.tagFilter) notes = notes.filter(function(n){ return (n.tags || []).indexOf(state.ui.tagFilter) >= 0; });
  if (q) notes = notes.filter(function(n){
    return (n.title || '').toLowerCase().indexOf(q) >= 0 || htmlToText(n.html).toLowerCase().indexOf(q) >= 0;
  });
  const sortFn = {
    modified: function(a, b){ return b.updatedAt - a.updatedAt; },
    created: function(a, b){ return b.createdAt - a.createdAt; },
    alpha: function(a, b){ return (a.title || '').localeCompare(b.title || '', undefined, { sensitivity: 'base' }); },
  }[state.ui.sort] || function(a, b){ return b.updatedAt - a.updatedAt; };
  notes.sort(sortFn);
  if (state.ui.scope !== 'trash'){
    notes.sort(function(a, b){ return (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0); });
  }
  return notes;
}

function noteCard(n){
  const q = state.ui.search.trim();
  const card = div('note-card' + (state.ui.noteId === n.id ? ' selected' : ''));
  card.draggable = true;
  const l1 = div('nc-line1');
  const t = div('nc-title');
  t.innerHTML = n.title ? highlightText(n.title, q) : '<span class="dim">Untitled note</span>';
  l1.appendChild(t);
  if (n.pinned && state.ui.scope !== 'trash'){
    const pin = div('nc-pin');
    pin.innerHTML = icon('pin', 12);
    pin.title = 'Pinned';
    l1.appendChild(pin);
  }
  const kebab = document.createElement('button');
  kebab.className = 'nc-kebab';
  kebab.title = 'Note options';
  kebab.innerHTML = icon('kebab', 14);
  kebab.addEventListener('click', function(e){
    e.stopPropagation();
    openNoteMenu(kebab, n);
  });
  l1.appendChild(kebab);
  card.appendChild(l1);
  const snip = div('nc-snippet');
  const text = htmlToText(n.html);
  snip.innerHTML = text ? makeSnippet(text, q) : '<span class="dim">No additional text</span>';
  card.appendChild(snip);
  const l2 = div('nc-line2');
  const time = document.createElement('span');
  time.textContent = fmtTime(n.updatedAt);
  l2.appendChild(time);
  if (state.ui.scope === 'all' && !n.deletedAt){
    const nb = getNotebook(n.notebookId);
    if (nb){
      const el = div('nc-nb');
      el.textContent = nb.name;
      l2.appendChild(el);
    }
  }
  if ((n.tags || []).length){
    const tags = div('nc-tags');
    n.tags.forEach(function(tag){
      const b = document.createElement('button');
      b.className = 'nc-tag';
      b.textContent = tag;
      b.title = 'Filter by #' + tag;
      b.addEventListener('click', function(e){
        e.stopPropagation();
        state.ui.tagFilter = tag;
        scheduleSave();
        renderList();
      });
      tags.appendChild(b);
    });
    l2.appendChild(tags);
  }
  card.appendChild(l2);
  card.addEventListener('click', function(){ selectNote(n.id, { focus: false }); });
  card.addEventListener('dblclick', function(){
    selectNote(n.id, { focus: false });
    const ti = $('#note-title');
    ti.focus();
    ti.select();
  });
  card.addEventListener('dragstart', function(e){
    dragNoteId = n.id;
    e.dataTransfer.effectAllowed = 'move';
    e.dataTransfer.setData('text/plain', n.id);
    card.classList.add('dragging');
  });
  card.addEventListener('dragend', function(){
    card.classList.remove('dragging');
    dragNoteId = null;
  });
  return card;
}

function renderList(){
  const titleEl = $('#lp-title');
  titleEl.textContent = state.ui.scope === 'all' ? 'All Notes' : state.ui.scope === 'trash' ? 'Trash' : (getNotebook(state.ui.scope) || { name: '' }).name;
  $('#btn-sort').innerHTML = icon('sort', 13) + SORT_LABELS[state.ui.sort];
  const chip = $('#tag-chip');
  if (state.ui.tagFilter){
    chip.classList.remove('hidden');
    chip.innerHTML = icon('tag', 12) + escapeHtml(state.ui.tagFilter) + '<span>×</span>';
  } else {
    chip.classList.add('hidden');
  }
  const list = $('#note-list');
  list.innerHTML = '';
  const notes = visibleNotes();
  if (!notes.length){
    const empty = div('list-empty');
    if (state.ui.search.trim()){
      empty.innerHTML = icon('search', 26) + '<div class="le-t">No results</div><div class="le-s">Nothing matches “' + escapeHtml(state.ui.search.trim()) + '”.</div>';
    } else if (state.ui.scope === 'trash'){
      empty.innerHTML = icon('trash', 26) + '<div class="le-t">Trash is empty</div><div class="le-s">Deleted notes land here.</div>';
    } else {
      empty.innerHTML = icon('note', 26) + '<div class="le-t">No notes here yet</div><div class="le-s">Capture your first thought.</div>';
      const b = document.createElement('button');
      b.className = 'btn-primary';
      b.textContent = 'New note';
      b.addEventListener('click', function(){ createNote(); });
      empty.appendChild(b);
    }
    list.appendChild(empty);
    return;
  }
  notes.forEach(function(n){ list.appendChild(noteCard(n)); });
}

let lastRenderedNoteId = null;

function renderEditor(){
  const note = getNote(state.ui.noteId);
  const head = $('#editor-head');
  const scroll = $('#editor-scroll');
  const empty = $('#empty-state');
  if (!note){
    head.classList.add('hidden');
    scroll.classList.add('hidden');
    empty.classList.remove('hidden');
    lastRenderedNoteId = null;
    return;
  }
  head.classList.remove('hidden');
  scroll.classList.remove('hidden');
  empty.classList.add('hidden');
  if (note.id !== lastRenderedNoteId){
    undoStack.length = 0;
    redoStack.length = 0;
    lastRenderedNoteId = note.id;
    editor.innerHTML = note.html || '<p><br></p>';
    normalizeContent(editor);
  }
  const ti = $('#note-title');
  if (document.activeElement !== ti) ti.value = note.title || '';
  $('#note-updated').textContent = 'Edited ' + fmtTime(note.updatedAt);
  $('#btn-pin').classList.toggle('on', !!note.pinned);
  renderTags(note);
  updateToolbarState();
}

function renderTags(note){
  const row = $('#tag-row');
  row.innerHTML = '';
  (note.tags || []).forEach(function(tag){
    const pill = div('tag-pill');
    pill.innerHTML = escapeHtml(tag);
    const x = document.createElement('button');
    x.className = 'tag-x';
    x.innerHTML = '×';
    x.title = 'Remove tag';
    x.addEventListener('click', function(){ removeTag(note.id, tag); });
    pill.appendChild(x);
    row.appendChild(pill);
  });
  const input = document.createElement('input');
  input.className = 'tag-add';
  input.placeholder = 'Add tag…';
  input.spellcheck = false;
  input.addEventListener('keydown', function(e){
    if (e.key === 'Enter'){
      const v = input.value.trim();
      if (v) addTag(note.id, v);
      input.value = '';
    } else if (e.key === 'Backspace' && !input.value){
      const tags = note.tags || [];
      if (tags.length) removeTag(note.id, tags[tags.length - 1]);
    }
  });
  row.appendChild(input);
}

function updateMeta(note){
  $('#note-updated').textContent = 'Edited ' + fmtTime(note.updatedAt);
}

function renderAll(){
  renderSidebar();
  renderList();
  renderEditor();
}

/* content → model sync (called after every editor change) */
function afterEditorChange(){
  const note = getNote(state.ui.noteId);
  if (!note) return;
  note.html = editor.innerHTML;
  note.updatedAt = Date.now();
  scheduleSave();
  clearTimeout(listTimer);
  listTimer = setTimeout(function(){
    renderList();
    updateMeta(note);
  }, CONFIG.LIST_REFRESH_MS);
  updateToolbarState();
}

/* ============================================================
   12. TOAST
   ============================================================ */
let toastTimer = null;
function showToast(message, actionLabel, actionFn){
  let toast = document.getElementById('toast');
  if (!toast){
    toast = document.createElement('div');
    toast.id = 'toast';
    document.body.appendChild(toast);
  }
  toast.innerHTML = '';
  const msg = document.createElement('span');
  msg.className = 'toast-msg';
  msg.textContent = message;
  toast.appendChild(msg);
  if (actionLabel){
    const b = document.createElement('button');
    b.className = 'toast-action';
    b.textContent = actionLabel;
    b.addEventListener('click', function(){
      hideToast();
      actionFn();
    });
    toast.appendChild(b);
  }
  toast.classList.add('show');
  clearTimeout(toastTimer);
  toastTimer = setTimeout(hideToast, 4500);
}
function hideToast(){
  const toast = document.getElementById('toast');
  if (toast) toast.classList.remove('show');
}

/* ============================================================
   13. MENUS
   ============================================================ */
let activeMenu = null;

function closeMenu(){
  if (activeMenu){
    activeMenu.remove();
    activeMenu = null;
  }
  document.removeEventListener('mousedown', menuOutsideHandler);
}

function menuOutsideHandler(e){
  if (activeMenu && !activeMenu.contains(e.target)) closeMenu();
}

function openMenu(anchor, items){
  closeMenu();
  const menu = div('menu');
  items.forEach(function(it){
    if (it === 'sep'){ menu.appendChild(div('menu-sep')); return; }
    const b = document.createElement('button');
    b.className = 'menu-item' + (it.danger ? ' danger' : '');
    b.innerHTML = (it.icon ? icon(it.icon, 14) : '') + '<span>' + escapeHtml(it.label) + '</span>';
    b.addEventListener('click', function(){
      closeMenu();
      it.onClick();
    });
    menu.appendChild(b);
  });
  document.body.appendChild(menu);
  const r = anchor.getBoundingClientRect();
  const mw = menu.offsetWidth;
  const mh = menu.offsetHeight;
  let left = r.left;
  let top = r.bottom + 4;
  if (left + mw > window.innerWidth - 8) left = window.innerWidth - mw - 8;
  if (top + mh > window.innerHeight - 8) top = Math.max(8, r.top - mh - 4);
  menu.style.left = Math.max(8, left) + 'px';
  menu.style.top = top + 'px';
  activeMenu = menu;
  setTimeout(function(){
    document.addEventListener('mousedown', menuOutsideHandler);
    window.addEventListener('scroll', closeMenu, true);
  }, 0);
}

function openNoteMenu(anchor, note){
  const inTrash = state.ui.scope === 'trash' || !!note.deletedAt;
  const items = [];
  items.push({ label: note.pinned ? 'Unpin' : 'Pin to top', icon: 'pin', onClick: function(){ togglePin(note.id); } });
  state.notebooks.forEach(function(nb){
    if (!inTrash && nb.id === note.notebookId) return;
    items.push({
      label: (inTrash ? 'Restore to ' : 'Move to ') + nb.name,
      icon: 'corner',
      onClick: function(){ moveNote(note.id, nb.id); },
    });
  });
  items.push('sep');
  items.push({ label: 'Duplicate', icon: 'copy', onClick: function(){ duplicateNote(note.id); } });
  items.push('sep');
  if (inTrash){
    items.push({ label: 'Restore', icon: 'restore', onClick: function(){ restoreNote(note.id); } });
    items.push({ label: 'Delete forever', icon: 'trash', danger: true, onClick: function(){ destroyNote(note.id); } });
  } else {
    items.push({ label: 'Delete', icon: 'trash', danger: true, onClick: function(){ deleteNote(note.id); } });
  }
  openMenu(anchor, items);
}

function openNotebookMenu(anchor, nb){
  openMenu(anchor, [
    { label: 'Rename', icon: 'edit', onClick: function(){
      const item = document.querySelector('#sb-body .sb-item[data-nb="' + nb.id + '"]');
      if (item) startRenameNotebook(nb, item);
    } },
    { label: 'Delete notebook', icon: 'trash', danger: true, onClick: function(){ deleteNotebook(nb); } },
  ]);
}

/* ============================================================
   13. WIRING — topbar, list, editor, keys, resize, theme
   ============================================================ */
function wireTopbar(){
  $('#btn-sidebar').innerHTML = icon('sidebar', 16);
  $('#btn-sidebar').addEventListener('click', function(){
    state.ui.sidebarOpen = !state.ui.sidebarOpen;
    applySidebarOpen();
    scheduleSave();
  });
  $('#btn-theme').addEventListener('click', function(){
    state.ui.theme = state.ui.theme === 'dark' ? 'light' : 'dark';
    applyTheme();
    scheduleSave();
  });
  $('#btn-add-notebook').innerHTML = icon('plus', 15);
  $('#btn-add-notebook').title = 'New notebook';
  $('#btn-add-notebook').addEventListener('click', createNotebook);
  $('#brand-mark').innerHTML = icon('feather', 14);
  $('#es-icon').innerHTML = icon('feather', 26);
  $('#search-ic').innerHTML = icon('search', 14);
  $('#btn-pin').innerHTML = icon('pin', 15);
  $('#btn-note-menu').innerHTML = icon('kebab', 16);
  $('#btn-new-note').innerHTML = icon('plus', 13) + 'New note';
}

function wireList(){
  $('#btn-sort').addEventListener('click', function(){
    const i = SORT_ORDER.indexOf(state.ui.sort);
    state.ui.sort = SORT_ORDER[(i + 1) % SORT_ORDER.length];
    scheduleSave();
    renderList();
  });
  $('#btn-new-note').addEventListener('click', function(){ createNote(); });
  $('#btn-empty-new').addEventListener('click', function(){ createNote(); });
  $('#search').addEventListener('input', function(e){
    state.ui.search = e.target.value;
    scheduleSave();
    renderList();
  });
  $('#tag-chip').addEventListener('click', function(){
    state.ui.tagFilter = null;
    scheduleSave();
    renderList();
  });
  $('#btn-pin').addEventListener('click', function(){
    if (state.ui.noteId) togglePin(state.ui.noteId);
  });
  $('#btn-note-menu').addEventListener('click', function(e){
    const note = getNote(state.ui.noteId);
    if (note) openNoteMenu(e.currentTarget, note);
  });
}

function wireTitle(){
  const ti = $('#note-title');
  ti.addEventListener('input', function(){
    const note = getNote(state.ui.noteId);
    if (!note) return;
    note.title = ti.value;
    note.updatedAt = Date.now();
    scheduleSave();
    clearTimeout(listTimer);
    listTimer = setTimeout(function(){
      renderList();
      updateMeta(note);
    }, CONFIG.LIST_REFRESH_MS);
  });
  ti.addEventListener('keydown', function(e){
    if (e.key === 'Enter'){ e.preventDefault(); editor.focus(); focusEditorEnd(); }
    if (e.key === 'Escape'){ ti.blur(); }
  });
}

function wireEditor(){
  editor.addEventListener('beforeinput', function(e){
    if (e.isComposing) return;
    const t = e.inputType || '';
    if (t.indexOf('insert') === 0 || t.indexOf('delete') === 0) pushSnapshot('typing');
  });
  editor.addEventListener('input', function(){ afterEditorChange(); });
  editor.addEventListener('keydown', function(e){
    const mod = e.ctrlKey || e.metaKey;
    const k = (e.key || '').toLowerCase();
    if (mod){
      if (k === 'z' && !e.shiftKey){ e.preventDefault(); e.stopPropagation(); undo(); return; }
      if (k === 'z' && e.shiftKey){ e.preventDefault(); e.stopPropagation(); redo(); return; }
      if (k === 'y' && e.shiftKey){ e.preventDefault(); e.stopPropagation(); redo(); return; }
      if (k === 'b'){ e.preventDefault(); e.stopPropagation(); applyInline('b'); return; }
      if (k === 'i'){ e.preventDefault(); e.stopPropagation(); applyInline('i'); return; }
      if (k === 'u'){ e.preventDefault(); e.stopPropagation(); applyInline('u'); return; }
    }
    if (e.key === ' '){ handleMarkdownShortcut(e); return; }
    if (e.key === 'Enter'){ handleEnter(e); return; }
    if (e.key === 'Tab'){ handleTab(e); return; }
    if (e.key === 'Backspace'){ handleBackspace(e); return; }
  });
  editor.addEventListener('click', function(e){
    const box = e.target.closest('.ck-box');
    if (!box) return;
    e.preventDefault();
    const li = box.closest('li');
    if (!li) return;
    pushOp();
    const checked = li.getAttribute('data-checked') === 'true';
    li.setAttribute('data-checked', checked ? 'false' : 'true');
    afterEditorChange();
  });
  editor.addEventListener('paste', handlePaste);
  document.addEventListener('selectionchange', function(){
    if (editorSel()) updateToolbarState();
  });
}

function wireKeys(){
  document.addEventListener('keydown', function(e){
    if (e.key === 'Escape'){
      closeMenu();
      const s = $('#search');
      if (document.activeElement === s){
        s.value = '';
        state.ui.search = '';
        scheduleSave();
        renderList();
        s.blur();
      }
      return;
    }
    if (e.key === '/' && !(e.ctrlKey || e.metaKey || e.altKey)){
      const t = e.target;
      const inField = t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable);
      if (!inField){
        e.preventDefault();
        $('#search').focus();
      }
    }
  });
}

function initResize(){
  const app = $('#app');
  bindHandle($('#handle-sb'), function(x){
    const w = clamp(x - app.getBoundingClientRect().left, CONFIG.MIN_SIDEBAR, CONFIG.MAX_SIDEBAR);
    app.style.setProperty('--sbw', w + 'px');
    state.ui.sidebarW = w;
  });
  bindHandle($('#handle-list'), function(x){
    const rect = app.getBoundingClientRect();
    const sbw = state.ui.sidebarOpen ? state.ui.sidebarW : 0;
    const w = clamp(x - rect.left - sbw - 6, CONFIG.MIN_LIST, CONFIG.MAX_LIST);
    app.style.setProperty('--listw', w + 'px');
    state.ui.listW = w;
  });
}

function bindHandle(el, onMove){
  el.addEventListener('mousedown', function(e){
    e.preventDefault();
    el.classList.add('active');
    document.body.classList.add('resizing');
    const move = function(ev){ onMove(ev.clientX); };
    const up = function(){
      el.classList.remove('active');
      document.body.classList.remove('resizing');
      document.removeEventListener('mousemove', move);
      document.removeEventListener('mouseup', up);
      flushSave();
    };
    document.addEventListener('mousemove', move);
    document.addEventListener('mouseup', up);
  });
}

function applyTheme(){
  document.documentElement.setAttribute('data-theme', state.ui.theme);
  $('#btn-theme').innerHTML = icon(state.ui.theme === 'dark' ? 'sun' : 'moon', 16);
}

function applySidebarOpen(){
  $('#app').classList.toggle('sb-closed', !state.ui.sidebarOpen);
}

/* ============================================================
   14. INIT
   ============================================================ */
function init(){
  loadState();
  saveInd = $('#save-ind');
  saveIndText = $('#save-ind-text');
  saveIndIc = $('#save-ind-ic');
  applyTheme();
  const app = $('#app');
  app.style.setProperty('--sbw', state.ui.sidebarW + 'px');
  app.style.setProperty('--listw', state.ui.listW + 'px');
  applySidebarOpen();
  buildToolbar();
  wireTopbar();
  wireList();
  wireTitle();
  wireEditor();
  wireKeys();
  initResize();
  renderAll();
  window.addEventListener('beforeunload', flushSave);
}

init();
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.8","provider":"brigid","persona":"full","sessionId":"77fccd33-c8ad-4cc7-975f-47be441b404e","tokensIn":5785434,"tokensOut":161889,"tokensTotal":5947323,"cost":0,"turns":68,"toolCalls":67,"failedToolCalls":0,"timestamp":"2026-09-01T21:56:39.341Z"} -->