← Notes results

Notes

Ornith 1.5 35B A3B · full

Initial view of Notes

Browser check: Artifact runtime error. This is a technical check, not a quality review.

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 80,242 bytes · SHA-256 a88054dcf5d0
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Notes</title>
<style>
/* =========================================================================
   TUNABLE CONSTANTS — colors, fonts, autosave debounce, snippet length
   ========================================================================= */
:root{
  /* Light theme (warm) */
  --bg: #f9f6f1;
  --sidebar-bg: #f2ede5;
  --list-bg: #faf8f4;
  --editor-bg: #fffdf9;
  --text: #2c2722;
  --text-muted: #8a8077;
  --text-faint: #b3aaa0;
  --border: #e7e0d5;
  --border-strong: #ddd4c6;
  --accent: #b8562f;
  --accent-hover: #a64a27;
  --accent-soft: #f4e7dd;
  --accent-text: #a64a27;
  --hover: #efe9df;
  --active: #ece4d8;
  --danger: #c0392b;
  --danger-soft: #f6e4e1;
  --highlight: #ffe19a;
  --shadow: rgba(60, 45, 30, 0.10);
  --serif: 'Iowan Old Style','Palatino Linotype','Palatino',Georgia,'Times New Roman',serif;
  --sans: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji';
  --radius: 8px;
  --radius-sm: 5px;
  --sidebar-w: 250px;
  --list-w: 300px;
  --toolbar-h: 46px;
  --transition: 160ms cubic-bezier(.4,0,.2,1);
}
[data-theme="dark"]{
  --bg: #1c1a17;
  --sidebar-bg: #232019;
  --list-bg: #201d18;
  --editor-bg: #191713;
  --text: #eae3d8;
  --text-muted: #9a9188;
  --text-faint: #6f685f;
  --border: #34302a;
  --border-strong: #403a33;
  --accent: #e0895f;
  --accent-hover: #ef9c73;
  --accent-soft: #3a2c22;
  --accent-text: #e8a079;
  --hover: #2b2720;
  --active: #322d25;
  --danger: #e5776a;
  --danger-soft: #3d2622;
  --highlight: #8a6a2e;
  --shadow: rgba(0, 0, 0, 0.4);
}

* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
  font-family: var(--sans);
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
  font-size: 14px;
}
::selection { background: var(--accent-soft); }

/* ---------- Layout shell ---------- */
#app {
  display: grid;
  grid-template-columns: var(--sidebar-w) var(--list-w) 1fr;
  grid-template-rows: 1fr;
  height: 100vh;
  width: 100vw;
  transition: grid-template-columns var(--transition);
}
#app.sidebar-collapsed { grid-template-columns: 0 var(--list-w) 1fr; }
.pane { overflow: hidden; min-width: 0; height: 100%; }
#sidebar   { background: var(--sidebar-bg); border-right: 1px solid var(--border); }
#noteList  { background: var(--list-bg);   border-right: 1px solid var(--border); }
#editorPane{ background: var(--editor-bg); position: relative; }
#app.sidebar-collapsed #sidebar { border-right: 0; }

/* ---------- Resize handles ---------- */
.handle {
  width: 4px; cursor: col-resize; background: transparent;
  position: relative; z-index: 20; flex-shrink: 0;
  transition: background var(--transition);
}
.handle:hover, .handle.active { background: var(--accent); }
.handle-h { height: 4px; width: 100%; cursor: row-resize; }

/* ---------- Sidebar ---------- */
.sidebar-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 16px 16px 10px; 
}
.brand { font-weight: 700; font-size: 17px; letter-spacing: -.2px; display:flex; align-items:center; gap:8px; }
.brand .dot { width: 9px; height: 9px; border-radius: 50%; background: var(--accent); }
.sidebar-actions { display:flex; gap:4px; }
.icon-btn {
  border: 0; background: transparent; color: var(--text-muted); cursor: pointer;
  width: 28px; height: 28px; border-radius: 6px; display:grid; place-items:center;
  font-size: 17px; line-height: 1; transition: all var(--transition);
}
.icon-btn:hover { background: var(--hover); color: var(--text); }
.icon-btn:active { transform: scale(.92); }

.search-box { padding: 0 12px 12px; }
.search-input-wrap {
  display:flex; align-items:center; gap:8px; background: var(--bg);
  border: 1px solid var(--border); border-radius: 8px; padding: 7px 10px;
  transition: border-color var(--transition), box-shadow var(--transition);
}
.search-input-wrap:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.search-input-wrap svg { color: var(--text-faint); flex-shrink:0; }
#searchInput {
  flex: 1; border: 0; background: transparent; color: var(--text);
  font-family: var(--sans); font-size: 13px; outline: none; min-width: 0;
}
#searchInput::placeholder { color: var(--text-faint); }

.tree { overflow-y: auto; padding: 4px 8px 16px; }
.tree::-webkit-scrollbar { width: 8px; }
.tree::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 8px; }

.tree-item {
  border-radius: 7px; margin: 1px 0;
}
.tree-row {
  display: flex; align-items: center; gap: 6px; cursor: pointer;
  padding: 6px 8px; border-radius: 7px; color: var(--text);
  font-size: 13.4px; user-select: none; position: relative;
  transition: background var(--transition);
}
.tree-row:hover { background: var(--hover); }
.tree-row.active { background: var(--active); }
.tree-item.all-notes > .tree-row.active { background: var(--accent-soft); color: var(--accent-text); }
.tree-row.active.all-notes-row { background: var(--accent-soft); color: var(--accent-text); }
.tree-row.trash-row.active { background: var(--danger-soft); color: var(--danger); }
.tree-row .chevron { width: 14px; color: var(--text-faint); transition: transform var(--transition); flex-shrink:0; }
.tree-item.collapsed .chevron { transform: rotate(-90deg); }
.tree-item.has-children > .tree-row .chevron { visibility: visible; }
.tree-item:not(.has-children) > .tree-row .chevron { visibility: hidden; }
.tree-row .label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
.tree-row .count { color: var(--text-faint); font-size: 11.5px; font-variant-numeric: tabular-nums; }
.tree-row .row-actions { display:none; gap:2px; }
.tree-row:hover .row-actions { display:flex; }
.tree-row .row-actions .icon-btn { width: 22px; height: 22px; font-size: 13px; }
.tree-children { padding-left: 12px; overflow: hidden; }
.tree-item.collapsed > .tree-children { display: none; }
.drag-over > .tree-row { box-shadow: inset 0 0 0 1.5px var(--accent); background: var(--accent-soft); }

/* ---------- Note list ---------- */
.list-head {
  padding: 16px 16px 10px; border-bottom: 1px solid var(--border);
  display:flex; align-items:center; justify-content:space-between; gap:8px;
  background: var(--list-bg);
}
.list-title { font-weight: 700; font-size: 15px; letter-spacing: -.2px; }
.list-sub { color: var(--text-faint); font-size: 11.5px; font-weight: 500; }
.sort-wrap { display:flex; align-items:center; gap:4px; }
.sort-btn {
  border:0; background: transparent; color: var(--text-muted); cursor:pointer;
  font-family: var(--sans); font-size: 11.5px; padding: 5px 8px; border-radius: 6px;
  display:flex; align-items:center; gap:4px; transition: all var(--transition);
}
.sort-btn:hover { background: var(--hover); color: var(--text); }
.sort-btn.active { background: var(--accent-soft); color: var(--accent-text); font-weight:600; }

.tag-filter-bar { padding: 8px 14px; display:none; }
.tag-filter-bar.show { display:flex; align-items:center; gap:8px; padding-bottom:10px; }
.active-filter-pill {
  display:inline-flex; align-items:center; gap:6px; background: var(--accent-soft);
  color: var(--accent-text); border-radius: 20px; padding: 3px 6px 3px 10px; font-size: 12px; font-weight:600;
}
.active-filter-pill .x { border:0; background:transparent; color:inherit; cursor:pointer; font-size:13px; line-height:1; padding:1px 3px; border-radius:50%; }
.active-filter-pill .x:hover { background: rgba(0,0,0,.08); }

.note-list { overflow-y: auto; padding: 8px; }
.note-list::-webkit-scrollbar { width: 8px; }
.note-list::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 8px; }

.note-card {
  padding: 11px 13px; border-radius: 10px; cursor: pointer; margin-bottom: 3px;
  border: 1px solid transparent; transition: all var(--transition); position: relative;
}
.note-card:hover { background: var(--hover); }
.note-card.active { background: var(--accent-soft); border-color: rgba(184,86,47,.25); }
.note-card .nc-top { display:flex; align-items:baseline; gap:8px; margin-bottom:3px; }
.note-card .nc-title { font-weight: 600; font-size: 13.6px; color: var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.note-card .pin-icon { color: var(--accent); font-size: 11px; flex-shrink:0; }
.note-card .nc-meta { display:flex; align-items:center; gap:8px; }
.note-card .nc-snippet { color: var(--text-muted); font-size: 12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1; line-height:1.35; }
.note-card .nc-time { color: var(--text-faint); font-size: 10.5px; flex-shrink:0; font-variant-numeric: tabular-nums; }
.note-card .nc-tags { display:flex; gap:4px; margin-top:6px; flex-wrap:wrap; }
.note-card .tag-chip {
  font-size: 10.5px; color: var(--text-muted); background: var(--bg);
  border: 1px solid var(--border); border-radius: 5px; padding: 1px 6px; cursor:pointer; transition: all var(--transition);
}
.note-card .tag-chip:hover { color: var(--accent-text); border-color: var(--accent); background: var(--accent-soft); }
.note-card .card-actions {
  position:absolute; top:8px; right:8px; display:none; gap:2px;
}
.note-card:hover .card-actions { display:flex; }
.note-card.active .card-actions { display:flex; }
.card-actions .icon-btn { width:24px; height:24px; font-size:12px; background: var(--editor-bg); border:1px solid var(--border); }
.card-actions .icon-btn:hover { background: var(--hover); border-color: var(--border-strong); }
.note-card .pin-toggle.active { color: var(--accent); background: var(--accent-soft); border-color: transparent; }

.note-card .nc-snippet .mark, .note-title-cell .mark { background: var(--highlight); color: inherit; border-radius: 2px; padding: 0 1px; }

/* Empty note list */
.empty-list {
  height:100%; display:flex; flex-direction:column; align-items:center; justify-content:center;
  color: var(--text-faint); text-align:center; padding: 30px; gap:10px;
}
.empty-list .empty-ico { font-size: 34px; opacity:.5; }

/* ---------- Editor ---------- */
.editor-head {
  height: var(--toolbar-h); border-bottom: 1px solid var(--border);
  display:flex; align-items:center; gap:6px; padding: 0 12px;
  background: var(--editor-bg);
}
.editor-head .spacer { flex:1; }
.saved-indicator { color: var(--text-faint); font-size: 11.5px; font-weight:500; display:flex; align-items:center; gap:5px; white-space:nowrap; }
.saved-indicator.saving { color: var(--accent); }
.saved-indicator .pulse { width:7px; height:7px; border-radius:50%; background: currentColor; opacity:.4; }
.saved-indicator.saving .pulse { animation: pulse 1s ease-in-out infinite; }
@keyframes pulse { 0%,100%{opacity:.25} 50%{opacity:1} }

/* Toolbar */
.toolbar {
  display:flex; align-items:center; gap:3px; flex-wrap:wrap;
  padding: 6px 12px; border-bottom: 1px solid var(--border);
  background: var(--editor-bg); position: sticky; top:0; z-index:5;
}
.toolbar .sep { width:1px; height:18px; background: var(--border); margin: 0 3px; }
.tb {
  border:0; background: transparent; color: var(--text-muted); cursor:pointer;
  min-width: 28px; height: 30px; padding: 0 7px; border-radius: 6px;
  font-family: var(--sans); font-size: 13px; font-weight:600; display:inline-flex;
  align-items:center; justify-content:center; gap:4px; transition: all var(--transition);
}
.tb:hover { background: var(--hover); color: var(--text); }
.tb:active { transform: scale(.93); }
.tb.active { background: var(--accent-soft); color: var(--accent-text); }
.tb .lbl { font-size: 12px; }
.tb[data-cmd="code"].active, .tb[data-cmd="code"]:hover { font-family: var(--serif); }
.tb svg { width:16px; height:16px; }

/* Editor body */
.editor-scroll { height: 100%; overflow-y: auto; }
.editor-inner { max-width: 780px; margin: 0 auto; padding: 34px 56px 120px; }
.editor-meta {
  display:flex; align-items:center; gap:8px; padding: 0 56px 14px; flex-wrap:wrap;
}
.editor-meta .note-tag-input-wrap { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
.editor-meta .tag {
  display:inline-flex; align-items:center; gap:5px; background: var(--accent-soft);
  color: var(--accent-text); border-radius: 6px; padding: 3px 5px 3px 9px; font-size: 12px; font-weight:500;
}
.editor-meta .tag .x { border:0; background:transparent; color:inherit; cursor:pointer; font-size:13px; line-height:1; padding:0 2px; opacity:.7; }
.editor-meta .tag .x:hover { opacity:1; }
.tag-input {
  border:0; background:transparent; color: var(--text); font-family: var(--sans);
  font-size: 12.5px; outline:none; min-width: 110px; height: 24px;
}
.tag-input::placeholder { color: var(--text-faint); }

#noteTitle {
  width: 100%; border: 0; outline: none; font-family: var(--serif);
  font-size: 30px; font-weight: 700; line-height: 1.2; color: var(--text);
  margin-bottom: 4px; letter-spacing: -.4px;
}
#noteTitle::placeholder { color: var(--text-faint); }

#editor {
  font-family: var(--serif);
  font-size: 17px; line-height: 1.7; color: var(--text);
  outline: none; min-height: 60vh; letter-spacing: .01em;
}
#editor:focus { outline: none; }
#editor :first-child { margin-top: 0; }

#editor p { margin: 0 0 12px; }
#editor h1 { font-size: 26px; font-weight: 700; margin: 22px 0 12px; line-height: 1.25; letter-spacing: -.3px; }
#editor h2 { font-size: 21px; font-weight: 700; margin: 20px 0 10px; line-height: 1.3; letter-spacing: -.2px; }
#editor h3 { font-size: 18px; font-weight: 700; margin: 18px 0 8px; line-height: 1.35; }
#editor blockquote {
  border-left: 3px solid var(--accent); margin: 10px 0; padding: 4px 16px;
  color: var(--text-muted); background: var(--hover); border-radius: 0 6px 6px 0;
}
#editor ul, #editor ol { margin: 8px 0 12px; padding-left: 26px; }
#editor li { margin: 3px 0; }
#editor code {
  font-family: 'SF Mono',ui-monospace,Menlo,Consolas,monospace; font-size: 88%;
  background: var(--accent-soft); color: var(--accent-text); padding: 2px 5px; border-radius: 4px;
}
#editor pre {
  font-family: 'SF Mono',ui-monospace,Menlo,Consolas,monospace; font-size: 13.5px;
  background: var(--hover); padding: 14px 16px; border-radius: 8px; overflow-x: auto; margin: 10px 0;
}
#editor hr { border: 0; border-top: 1px solid var(--border); margin: 22px 0; }
#editor a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }

/* Checklist */
#editor li.check { list-style: none; position: relative; margin: 5px 0; }
#editor li.check .ckb {
  position: absolute; left: -22px; top: 5px; width: 16px; height: 16px; accent-color: var(--accent);
  cursor: pointer; flex-shrink: 0;
}
#editor li.check .ck-body { margin-left: 4px; }
#editor li.check .ck-body:empty::before { content:'List item'; color: var(--text-faint); }
#editor li.check[data-checked="true"] .ck-body,
#editor li.check .ckb:checked + .ck-body { text-decoration: line-through; color: var(--text-faint); opacity:.75; }
#editor li.check[data-checked="true"] .ck-body:empty::before { content:''; }

/* Placeholder when no note selected */
.no-note {
  height:100%; display:flex; flex-direction:column; align-items:center; justify-content:center;
  color: var(--text-faint); gap:12px; text-align:center; padding: 40px;
}
.no-note .big { font-size: 52px; opacity:.4; }
.no-note .msg { font-size: 16px; font-weight: 500; color: var(--text-muted); }
.no-note .hint { font-size: 12.5px; }
.no-note kbd {
  background: var(--hover); border:1px solid var(--border); border-bottom-width:2px;
  border-radius:5px; padding:1px 6px; font-family: var(--sans); font-size:11px; color: var(--text-muted);
}

/* Dropdown menu */
.menu { position: absolute; min-width: 180px; background: var(--editor-bg); border:1px solid var(--border);
  border-radius: 10px; box-shadow: 0 10px 30px var(--shadow); padding: 5px; z-index: 100;
  animation: menuIn 120ms ease; }
@keyframes menuIn { from{opacity:0; transform: translateY(-4px);} to{opacity:1; transform:none;} }
.menu-item {
  display:flex; align-items:center; gap:10px; width:100%; border:0; background:transparent;
  color: var(--text); font-family: var(--sans); font-size: 13px; padding: 8px 10px; border-radius: 7px;
  cursor:pointer; text-align:left;
}
.menu-item:hover { background: var(--hover); }
.menu-item.danger { color: var(--danger); }
.menu-item.danger:hover { background: var(--danger-soft); }
.menu-item .menu-kbd { margin-left:auto; color: var(--text-faint); font-size:11px; }
.menu-sep { height:1px; background: var(--border); margin: 5px 3px; }

/* Modal / confirm */
.modal-backdrop { position:fixed; inset:0; background: rgba(30,25,18,.45); z-index: 200;
  display:flex; align-items:center; justify-content:center; animation: fadeIn 120ms ease; }
@keyframes fadeIn { from{opacity:0;} to{opacity:1;} }
.modal { background: var(--editor-bg); border-radius: 12px; padding: 22px; width: 340px;
  box-shadow: 0 20px 60px var(--shadow); }
.modal h3 { margin: 0 0 12px; font-size: 16px; }
.modal input[type=text] { width:100%; border:1px solid var(--border); background: var(--bg); color:var(--text);
  border-radius: 8px; padding: 10px 12px; font-family: var(--sans); font-size: 14px; outline:none; }
.modal input[type=text]:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.modal-actions { display:flex; justify-content:flex-end; gap:8px; margin-top:18px; }
.modal-actions button { border:0; border-radius: 8px; padding: 8px 16px; font-family: var(--sans); font-size:13px; cursor:pointer; font-weight:600; }
.btn-primary { background: var(--accent); color:#fff; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-ghost { background: transparent; color: var(--text-muted); }
.btn-ghost:hover { background: var(--hover); color: var(--text); }

.new-note-btn {
  margin: 4px 8px 10px; display:flex; align-items:center; justify-content:center; gap:7px;
  width: calc(100% - 24px); border:0; background: var(--accent); color:#fff; cursor:pointer;
  padding: 9px; border-radius: 8px; font-family: var(--sans); font-size: 13px; font-weight:600;
  transition: all var(--transition);
}
.new-note-btn:hover { background: var(--accent-hover); }
.new-note-btn:active { transform: scale(.98); }

/* Toast */
.toast { position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%) translateY(20px);
  background: var(--text); color: var(--editor-bg); padding: 10px 18px; border-radius: 10px;
  font-size: 13px; font-weight:500; z-index: 300; box-shadow: 0 8px 24px var(--shadow);
  opacity: 0; transition: all 220ms ease; pointer-events:none; }
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }

</style>
</head>
<body>
<div id="app">
  <!-- ===================== SIDEBAR ===================== -->
  <section class="pane" id="sidebar">
    <div class="sidebar-head">
      <div class="brand"><span class="dot"></span> Notes</div>
      <div class="sidebar-actions">
        <button class="icon-btn" id="themeToggle" title="Toggle theme (Ctrl+Shift+D)" aria-label="Toggle theme">◐</button>
      </div>
    </div>
    <div class="search-box">
      <div class="search-input-wrap">
        <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
        <input id="searchInput" type="text" placeholder="Search notes…" aria-label="Search notes">
      </div>
    </div>
    <div class="tree" id="notebookTree"></div>
  </section>
  <div class="handle" id="handleSidebar" title="Resize sidebar"></div>

  <!-- ===================== NOTE LIST ===================== -->
  <section class="pane" id="noteList">
    <div class="list-head">
      <div>
        <div class="list-title" id="listTitle">All Notes</div>
        <div class="list-sub" id="listCount"></div>
      </div>
      <div class="sort-wrap" id="sortWrap">
        <button class="sort-btn" data-sort="modified" title="Sort by last modified">Modified ▾</button>
      </div>
    </div>
    <div class="tag-filter-bar" id="tagFilterBar">
      <span style="color:var(--text-muted);font-size:12px;font-weight:600;">Tag:</span>
      <span class="active-filter-pill" id="activeFilterPill">
        <span id="activeFilterText"></span>
        <button class="x" id="clearFilter" aria-label="Clear filter">×</button>
      </span>
    </div>
    <div class="note-list" id="noteListEl"></div>
  </section>
  <div class="handle" id="handleList" title="Resize note list"></div>

  <!-- ===================== EDITOR ===================== -->
  <section class="pane" id="editorPane">
    <div id="editorApp"><!-- injected by JS --></div>
  </section>
</div>

<div id="toast" class="toast"></div>

<script>
/* =========================================================================
   NOTES APPLICATION — vanilla JS
   Sections: Constants → Utils → Sanitizer → Data model/persistence →
             Editor (formatting, toolbar, markdown shortcuts, undo/redo) →
             Rendering (sidebar, list, editor) → Interactions → Init
   ========================================================================= */
(function(){
'use strict';

/* ---------- Tunable constants ---------- */
const CFG = {
  STORAGE_KEY: 'notesapp.v1',
  AUTOSAVE_DEBOUNCE: 450,        // ms
  SNIPPET_LENGTH: 110,
  HISTORY_LIMIT: 200,
  NOTE_DATE_FORMAT: 'short'
};

/* ---------- Tiny DOM helpers ---------- */
const $  = (s, el=document) => el.querySelector(s);
const $$ = (s, el=document) => Array.from(el.querySelectorAll(s));
const el = (tag, attrs={}, ...children) => {
  const e = document.createElement(tag);
  for (const [k,v] of Object.entries(attrs)) {
    if (k === 'class') e.className = v;
    else if (k === 'html') e.innerHTML = v;
    else if (k.startsWith('on') && typeof v === 'function') e.addEventListener(k.slice(2), v);
    else if (v !== false && v != null) e.setAttribute(k, v === true ? '' : v);
  }
  for (const c of children) if (c != null && c !== false) e.append(c.nodeType ? c : document.createTextNode(String(c)));
  return e;
};
let _uid = 0;
const uid = () => 'id_' + Date.now().toString(36) + (++_uid).toString(36) + Math.random().toString(36).slice(2,6);

/* ---------- HTML escaping ---------- */
const esc = (s) => String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));

/* =========================================================================
   SANITIZER — drop foreign markup/scripts, keep supported subset
   ========================================================================= */
const ALLOWED_TAGS = new Set(['b','i','u','s','strike','em','strong','code','pre','h1','h2','h3',
  'p','br','hr','ul','ol','li','blockquote','a','span','input','img']);
const ALLOWED_STYLE = new Set(['color','background-color','font-weight','font-style',
  'text-decoration','font-family','font-size','text-align','line-height','margin','padding']);
const FORBID = ['script','style','link','meta','title','object','embed','iframe','base','form','input[type!=checkbox]','button','table','video','audio'];

function sanitizeHtml(html) {
  if (!html) return '';
  const tpl = document.createElement('template');
  tpl.innerHTML = html;
  cleanNode(tpl.content);
  return tpl.innerHTML.trim();
}
function isAllowed(tag) {
  if (tag === 'input') return true; // checkbox inputs allowed
  return ALLOWED_TAGS.has(tag);
}
function cleanNode(node) {
  // Remove forbidden block/element types first (recursively so their subtree dies)
  node.querySelectorAll('script,style,link,meta,title,object,embed,iframe,base,form,button,table,video,audio').forEach(n => n.remove());
  const unwrap = [];
  node.querySelectorAll('*').forEach(child => {
    if (child.tagName === 'INPUT' && (child.getAttribute('type')||'').toLowerCase() !== 'checkbox') unwrap.push(child);
    else if (!isAllowed(child.tagName.toLowerCase())) unwrap.push(child);
  });
  unwrap.forEach(n => {
    while (n.firstChild) node.insertBefore(n.firstChild, n);
    node.removeChild(n);
  });
  // Now clean attributes on remaining elements
  node.querySelectorAll('*').forEach(cleanAttrs);
}
function cleanAttrs(el) {
  const tag = el.tagName.toLowerCase();
  const remove = [];
  [...el.attributes].forEach(a => {
    const name = a.name.toLowerCase();
    if (name.startsWith('on')) { remove.push(name); return; }
    if (name === 'style') {
      const clean = cleanStyle(el.getAttribute('style'));
      if (clean) el.setAttribute('style', clean); else remove.push('style');
      return;
    }
    if (name === 'href' || name === 'src') {
      const u = sanitizeUrl(el.getAttribute(name));
      if (u == null) remove.push(name);
      else el.setAttribute(name, u);
      return;
    }
    if (tag === 'input') {
      if (!['type','checked','class','id','data-checked','aria-label','role'].includes(name)) remove.push(name);
      return;
    }
    if (!['class','id','dir','lang','title'].includes(name)) remove.push(name);
  });
  remove.forEach(n => el.removeAttribute(n));
}
function cleanStyle(css) {
  const out = [];
  css.split(';').forEach(rule => {
    const i = rule.indexOf(':');
    if (i < 0) return;
    const prop = rule.slice(0,i).trim().toLowerCase();
    const val = rule.slice(i+1).trim();
    if (!prop || !val) return;
    if (ALLOWED_STYLE.has(prop) && !/expression|javascript:|behavior/i.test(val)) out.push(prop + ': ' + val);
  });
  return out.join('; ');
}
function sanitizeUrl(u) {
  if (!u) return null;
  const s = u.trim().replace(/\s+/g, ' ').toLowerCase();
  if (/^(https?|mailto|tel|ftp):/i.test(u.trim())) return u.trim();
  if (s.startsWith('data:image/')) return u.trim();
  if (s.startsWith('javascript:') || s.startsWith('data:') || s.startsWith('vbscript:') || s.startsWith('file:')) return null;
  return u.trim(); // relative URLs allowed
}

/* ---------- Text extraction (for snippets + search) ---------- */
function extractText(htmlOrNode) {
  let node;
  if (typeof htmlOrNode === 'string') {
    const t = document.createElement('template'); t.innerHTML = sanitizeHtml(htmlOrNode);
    node = t.content;
  } else node = htmlOrNode;
  let txt = node.textContent || '';
  // Unwrap checkbox labels for checklists so they don't pollute text
  txt = txt.replace(/✓/g, '');
  return txt.replace(/\s+/g, ' ').trim();
}
function snippet(html, len = CFG.SNIPPET_LENGTH) {
  const t = extractText(html);
  if (t.length <= len) return t;
  return t.slice(0, len).replace(/\S*$/,'').trimEnd() + '…';
}

/* ---------- Time formatting ---------- */
function fmtTime(ts) {
  const d = new Date(ts), now = new Date();
  const sameDay = d.getFullYear()===now.getFullYear()&&d.getMonth()===now.getMonth()&&d.getDate()===now.getDate();
  const y = new Date(now); y.setDate(now.getDate()-1);
  const isYest = d.getFullYear()===y.getFullYear()&&d.getMonth()===y.getMonth()&&d.getDate()===y.getDate();
  const time = d.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
  if (sameDay) return 'Today ' + time;
  if (isYest) return 'Yesterday';
  const wk = new Date(now); wk.setDate(now.getDate()-7);
  if (d >= wk) return d.toLocaleDateString([], {weekday:'short'});
  return d.toLocaleDateString([], {month:'short', day:'numeric', year: d.getFullYear()!==now.getFullYear()?'numeric':undefined});
}
function fmtDateFull(ts) {
  return new Date(ts).toLocaleString([], {weekday:'long', year:'numeric', month:'long', day:'numeric', hour:'2-digit', minute:'2-digit'});
}

/* =========================================================================
   DATA MODEL + PERSISTENCE
   ========================================================================= */
const defaultData = () => ({
  version: 1,
  settings: { theme: 'light', layout: { sidebarW: 250, listW: 300 } },
  notebooks: [],   // {id, name, collapsed, parentId}
  notes: [],       // {id, notebookId, title, html, tags:[], pinned, createdAt, updatedAt, trashed, trashedAt}
  ui: { view: 'all', sort: 'modified', activeTag: null, selectedNote: null }
});

let state = null;
let saveTimer = null;
function load() {
  try {
    const raw = localStorage.getItem(CFG.STORAGE_KEY);
    if (raw) {
      const d = JSON.parse(raw);
      state = Object.assign(defaultData(), d, {
        settings: Object.assign(defaultData().settings, d.settings||{}),
        ui: Object.assign(defaultData().ui, d.ui||{})
      });
      if (!Array.isArray(d.notebooks)) state.notebooks = d.notebooks||[];
      if (!Array.isArray(d.notes)) state.notes = d.notes||[];
    } else {
      state = seedData();
    }
  } catch(e) { state = seedData(); }
  applyTheme();
  applyLayout();
}
function save() {
  clearTimeout(saveTimer);
  saveTimer = setTimeout(() => {
    try { localStorage.setItem(CFG.STORAGE_KEY, JSON.stringify(state)); }
    catch(e) { toast('Storage full — could not save'); }
  }, 80);
}
function markDirty() { save(); }

function seedData() {
  const d = defaultData();
  const sys = mkNotebook('System');
  const personal = mkNotebook('Personal');
  const work = mkNotebook('Work');
  const n1 = mkNote({ notebookId: personal.id, title: 'Welcome to Notes',
    html: '<h1>Welcome 👋</h1><p>This is a <b>rich-text</b> note. Try selecting this word and press <i>Ctrl+B</i> to make it bold.</p><blockquote>A quick way to capture ideas.</blockquote><ul><li>Bulleted lists like this one</li><li>And numbered lists</li></ul><ol><li>First</li><li>Second</li></ol><p>Type <code>#</code> at line start, or <code>- </code> for a list, or <code>[] </code> for a checklist.</p><p>Check out the <b>checkbox</b> below — toggle it and reload to see it persist:</p><ul><li class="check" data-checked="true"><input type="checkbox" class="ckb"><div class="ck-body">Checklist items persist their state</div></li></ul><hr><p>Your notes are saved locally and automatically as you type.</p>',
    tags: ['getting-started'], pinned: true,
    created: Date.now()-3600e3, updated: Date.now()-1200e3 });
  const n2 = mkNote({ notebookId: work.id, title: 'Project Ideas',
    html: '<h2>Project Ideas</h2><p><b>Brainstorm:</b></p><ul><li>A notes app like this one</li><li>Something with 3D graphics</li></ul><p>Remember to <u>ship it</u>.</p>',
    tags: ['ideas','work'], created: Date.now()-86400e3, updated: Date.now()-86400e3+1000 });
  const n3 = mkNote({ notebookId: personal.id, title: 'Shopping List',
    html: '<p><span class="check" >Check off items as you go:</span></p><ul><li class="check"><input type="checkbox" class="ckb"><div class="ck-body">Coffee beans</div></li><li class="check"><input type="checkbox" class="ckb"><div class="ck-body">Oat milk</div></li><li class="check"><input type="checkbox" class="ckb"><div class="ck-body">Sourdough</div></li></ul>',
    tags: [], created: Date.now()-172800e3, updated: Date.now()-172800e3+5 });
  d.notebooks.push(sys, personal, work);
  d.notes.push(n1, n2, n3);
  return d;
}
function mkNotebook(name) {
  const nb = { id: uid(), name: name || 'New Notebook', collapsed: false, parentId: null };
  state.notebooks.push(nb); return nb;
}
function mkNote(opts = {}) {
  const now = Date.now();
  const note = {
    id: uid(),
    notebookId: opts.notebookId || null,
    title: opts.title || 'Untitled',
    html: opts.html || '<p><br></p>',
    tags: opts.tags || [],
    pinned: !!opts.pinned,
    createdAt: opts.created || now,
    updatedAt: opts.updated || (opts.created || now),
    trashed: !!opts.trashed,
    trashedAt: opts.trashedAt || null
  };
  state.notes.push(note); return note;
}

/* =========================================================================
   NOTIFICATIONS / TOAST
   ========================================================================= */
let toastTimer = null;
function toast(msg) {
  const t = $('#toast');
  t.textContent = msg; t.classList.add('show');
  clearTimeout(toastTimer);
  toastTimer = setTimeout(() => t.classList.remove('show'), 2200);
}

/* =========================================================================
   EDITOR — formatting, toolbar, markdown shortcuts, undo/redo, paste
   ========================================================================= */
let editor = null, titleEl = null, savedIndicator = null;
let hist = [], histIndex = -1, isRestoring = false;
let autosaveTimer = null, lastContentKey = '';

function initEditor() {
  editor = $('#editor');
  titleEl = $('#noteTitle');
  savedIndicator = $('#savedIndicator');

  editor.addEventListener('input', onEditorInput);
  editor.addEventListener('keydown', onEditorKeydown);
  editor.addEventListener('keyup', () => updateToolbar());
  editor.addEventListener('mouseup', () => updateToolbar());
  editor.addEventListener('paste', onPaste);
  editor.addEventListener('blur', () => { updateToolbar(); });
}

function openNote(note) {
  rebuildEditorDOM();
  if (!note) {
    showNoNote(); return;
  }
  hist = []; histIndex = -1; isRestoring = false;
  titleEl.value = note.title;
  editor.innerHTML = note.html;
  histIndex = -1; // will push on first change / now
  pushHistory(true);
  updateSaved('saved');
  updateToolbar();
  restoreEditorSelection();
}
function rebuildEditorDOM() {
  const host = $('#editorApp');
  if (host.querySelector('#editorAppShell')) return; // already built
  host.innerHTML = '';
  host.append(buildEditorShell());
  editor = $('#editor'); titleEl = $('#noteTitle'); savedIndicator = $('#savedIndicator');
  editor.addEventListener('input', onEditorInput);
  editor.addEventListener('keydown', onEditorKeydown);
  editor.addEventListener('keyup', () => updateToolbar());
  editor.addEventListener('mouseup', () => updateToolbar());
  editor.addEventListener('paste', onPaste);
  editor.addEventListener('blur', () => updateToolbar());
}
function showNoNote() {
  const host = $('#editorApp');
  host.innerHTML = '';
  host.append(el('div', {className:'no-note'},
    el('div', {className:'big'}, '📝'),
    el('div', {className:'msg'}, 'Select or create a note'),
    el('div', {className:'hint'}, 'Press <kbd>Ctrl+N</kbd> for a new note, or pick one from the list.')));
}

/* Build the editor shell (header + toolbar + body). Rebuilt when note changes. */
function buildEditorShell() {
  const shell = el('div', {id:'editorAppShell'});
  shell.append(buildEditorHead());
  shell.append(buildToolbar());
  const scroll = el('div', {className:'editor-scroll'});
  const inner = el('div', {className:'editor-inner'});
  const meta = el('div', {className:'editor-meta'});
  inner.append(meta, titleEl2(), editorWrap());
  scroll.append(inner);
  shell.append(scroll);
  return shell;
}
function buildEditorHead() {
  const head = el('div', {className:'editor-head'});
  head.append(
    el('button', {className:'icon-btn', title:'Back to notes (Esc)', onclick: goBack}, arrowIcon()),
    el('span', {className:'spacer'}),
    el('div', {className:'saved-indicator', id:'savedIndicator'},
      el('span', {className:'pulse'}), 'Saved'),
    el('button', {className:'icon-btn', title:'More', id:'moreMenuBtn'}, dotMenuIcon())
  );
  $('#moreMenuBtn').addEventListener('click', (e)=>{ e.stopPropagation(); toggleMenu('moreMenu', moreMenuItems()); });
  return head;
}
function titleEl2(){ titleEl = el('input',{id:'noteTitle', type:'text', placeholder:'Untitled'});
  titleEl.addEventListener('input', onTitleInput); return titleEl; }
function editorWrap(){ return el('div',{id:'editorWrap'}, el('div',{id:'editor'})); }

function onTitleInput(e){
  const note = currentNote(); if(!note) return;
  note.title = e.target.value.trim() || 'Untitled';
  note.updatedAt = Date.now();
  debounceAutosave();
}

/* ---------- Formatting commands ---------- */
function format(cmd, arg) {
  editor.focus();
  if (cmd === 'undo') { doUndo(); return; }
  if (cmd === 'redo') { doRedo(); return; }
  if (cmd === 'code') { toggleCode(); return; }
  if (cmd === 'h1' || cmd === 'h2' || cmd === 'h3' || cmd === 'quote' || cmd === 'hr' || cmd === 'removeFormat') {
    runBlockFormat(cmd, arg); return;
  }
  if (cmd === 'bullet' || cmd === 'number') { toggleList(cmd); return; }
  if (cmd === 'checklist') { toggleChecklist(); return; }
  document.execCommand(cmd, false, arg || null);
  pushHistory();
}
function runBlockFormat(cmd, arg) {
  if (cmd === 'hr') {
    document.execCommand('insertHorizontalRule'); return;
  }
  let tag = {h1:'h1',h2:'h2',h3:'h3',quote:'blockquote'}[cmd];
  document.execCommand('formatBlock', false, tag);
}
function toggleList(kind) {
  const active = window.getSelection();
  const li = active.anchorNode && active.anchorNode.closest ? active.anchorNode.closest('li') : null;
  // If a checklist item is active, convert it to a normal list first
  if (li && li.classList.contains('check')) {
    li.classList.remove('check'); li.removeAttribute('data-checked');
    const inp = li.querySelector('.ckb'); if (inp) inp.remove();
    const body = li.querySelector('.ck-body');
    if (body) { while(body.firstChild) li.appendChild(body.firstChild); body.remove(); }
  }
  document.execCommand(kind === 'bullet' ? 'insertUnorderedList' : 'insertOrderedList');
  pushHistory();
}
function toggleCode() {
  const sel = window.getSelection();
  if (!sel.rangeCount) { document.execCommand('insertText', false, '`'); return; }
  const anchor = sel.anchorNode && sel.anchorNode.closest ? sel.anchorNode.closest('code') : null;
  if (anchor) {
    const txt = anchor.textContent;
    anchor.replaceWith(document.createTextNode(txt));
  } else {
    document.execCommand('insertText', false, '``');
    // select between the backticks
  }
  pushHistory();
}
function toggleChecklist() {
  const sel = window.getSelection();
  const li = sel.anchorNode && sel.anchorNode.closest ? sel.anchorNode.closest('li') : null;
  if (li && li.closest('#editor')) {
    if (li.classList.contains('check')) { checklistToLi(li); }
    else { liToChecklist(li); }
    pushHistory(); return;
  }
  // Not in a list: create one
  document.execCommand('insertUnorderedList');
  const newLi = window.getSelection().anchorNode?.closest?.('li');
  if (newLi && newLi.closest('#editor')) liToChecklist(newLi);
}
function liToChecklist(li) {
  const body = el('div', {className:'ck-body'});
  while (li.firstChild) body.appendChild(li.firstChild);
  const inp = el('input', {type:'checkbox', className:'ckb', role:'checkbox', 'aria-label':'checklist item'});
  inp.addEventListener('change', onCheckChange);
  li.prepend(inp); li.appendChild(body);
  li.classList.add('check'); li.setAttribute('data-checked','false');
}
function checklistToLi(li) {
  const inp = li.querySelector('.ckb'); if (inp) inp.remove();
  const body = li.querySelector('.ck-body');
  if (body) { while(body.firstChild) li.appendChild(body.firstChild); body.remove(); }
  li.classList.remove('check'); li.removeAttribute('data-checked');
}
function onCheckChange(e) {
  const li = e.target.closest('li.check');
  li.setAttribute('data-checked', e.target.checked ? 'true' : 'false');
  debounceAutosave();
}

/* ---------- Paste sanitize ---------- */
function onPaste(e) {
  const clip = e.clipboardData;
  if (!clip) return;
  const html = clip.getData('text/html');
  const plain = clip.getData('text/plain');
  e.preventDefault();
  let out;
  if (html) {
    out = sanitizeHtml(html);
    if (!out.trim()) out = esc(plain) || esc(clip.getData('text/rtf')||'');
  } else {
    out = esc(plain);
  }
  document.execCommand('insertHTML', false, out);
  pushHistory();
}

/* ---------- Markdown input shortcuts ---------- */
function onEditorKeydown(e) {
  // Keyboard shortcuts
  const mod = e.ctrlKey || e.metaKey;
  if (mod && !e.altKey) {
    const k = e.key.toLowerCase();
    if (k === 'z') { e.preventDefault(); e.shiftKey ? doRedo() : doUndo(); return; }
    if (k === 'y') { e.preventDefault(); doRedo(); return; }
    if (k === 'b') { e.preventDefault(); format('bold'); return; }
    if (k === 'i') { e.preventDefault(); format('italic'); return; }
    if (k === 'u') { e.preventDefault(); format('underline'); return; }
  }
  if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey) {
    handleEnter(e);
  }
  if (e.key === 'Escape') {
    closeMenus();
    const li = window.getSelection().anchorNode?.closest?.('li');
    if (li && li.closest('#editor') && isListItemEmpty(li)) {
      e.preventDefault();
      document.execCommand('formatBlock', false, 'p');
      pushHistory();
    }
  }
}

// Detect a markdown prefix at the START of the current block and convert on space.
function onSpaceTrigger() {
  const sel = window.getSelection();
  if (!sel.rangeCount) return;
  const block = sel.anchorNode?.closest?.('p,h1,h2,h3,li,blockquote,pre');
  if (!block || !block.closest('#editor')) return;
  const text = block.textContent;
  // Only trigger if the prefix is exactly the trigger (nothing typed after it yet)
  const m = text.match(/^\s*(#{1,3}\s|[-*]\s|\d+\.\s|\[\s*\]\s|>\s|---\s|~~~\s)$/);
  if (!m) return;
  const prefix = m[1];
  const trigger = prefix.trim();
  // Determine target type
  let type = null;
  if (/^#{1,3}$/.test(trigger)) type = 'heading';
  else if (/^[-*]$/.test(trigger)) type = 'bullet';
  else if (/^\d+\.$/.test(trigger)) type = 'number';
  else if (/^\[$/.test(trigger) || /^\]\s?$/.test(trigger)) type = 'checklist';
  else if (/^>$/.test(trigger)) type = 'quote';
  else if (/^---$/.test(trigger)) type = 'hr';
  else if (/^~~~$/.test(trigger)) type = 'pre';
  if (!type) return;
  // Convert: remove prefix, apply format.
  document.execCommand('selectAll');
  const range = document.createRange();
  const sel2 = window.getSelection();
  // Rebuild: select from block start to caret, delete prefix
  const carets = sel2.getRangeAt(0);
  range.selectNodeContents(block);
  range.setEnd(carets.startContainer, carets.startOffset);
  // But caret is at prefix end (space not inserted since we're in keydown). Actually space not yet inserted.
  // We'll instead just delete the prefix text nodes.
  range.collapse(true);
  sel2.removeAllRanges();
  sel2.addRange(carets);
  // Simplest robust approach: clear block then set new content
  applyMarkdown(type, block, prefix.trim());
}
function applyMarkdown(type, block, trigger) {
  // Remove any leading spaces and the trigger from the block, keeping content
  const content = block.textContent.replace(/^\s*#{1,3}\s?/, '').replace(/^\s*[-*]\s?/, '').replace(/^\s*\d+\.\s?/, '')
    .replace(/^\s*\[\s*\]\s?/, '').replace(/^\s*>\s?/, '').replace(/^\s*---\s?/, '').replace(/^\s*~~~\s?/, '');
  const carets = window.getSelection().getRangeAt(0);
  const newEl = document.createElement(type === 'hr' ? 'hr' : type === 'pre' ? 'pre' :
    type === 'quote' ? 'blockquote' : type === 'heading' ? ('h'+(content?3:1)) : 'ul');
  if (type === 'hr') {
    carets.deleteContents();
    carets.insertEnd ? null : null;
    document.execCommand('insertHorizontalRule');
    return;
  }
  if (type === 'bullet' || type === 'number') {
    const listType = type === 'bullet' ? 'ul' : 'ol';
    const li = document.createElement('li');
    const text = document.createTextNode(content);
    li.appendChild(text);
    newEl.appendChild(li);
    carets.deleteContents();
    carets.insertNode(newEl);
    // place caret at end of li text
    const r = document.createRange();
    r.setStart(text, text.nodeValue.length); r.collapse(true);
    const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(r);
  } else if (type === 'checklist') {
    const ul = document.createElement('ul');
    const li = document.createElement('li');
    li.className = 'check'; li.setAttribute('data-checked','false');
    const inp = document.createElement('input');
    inp.type = 'checkbox'; inp.className = 'ckb'; inp.setAttribute('role','checkbox');
    inp.addEventListener('change', onCheckChange);
    const body = document.createElement('div'); body.className = 'ck-body';
    body.appendChild(document.createTextNode(content));
    li.appendChild(inp); li.appendChild(body); ul.appendChild(li);
    carets.deleteContents(); carets.insertNode(ul);
    const r = document.createRange();
    r.setStart(body, 0); r.collapse(true);
    const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(r);
  } else {
    // heading / quote / pre
    if (type === 'heading') newEl.tagName = 'H'+(content?3:1);
    newEl.textContent = content;
    carets.deleteContents(); carets.insertNode(newEl);
    const r = document.createRange();
    r.selectNodeContents(newEl); r.collapse(false);
    const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(r);
  }
  pushHistory();
}

// Simpler, reliable markdown shortcut handler using keydown on space
function handleMarkdownShortcut(e) {
  if (e.key !== ' ' && e.key !== 'Spacebar') return;
  const sel = window.getSelection();
  if (!sel.rangeCount) return;
  const block = sel.anchorNode?.closest?.('p,h1,h2,h3,li,blockquote,pre');
  if (!block || !block.closest('#editor')) return;
  const text = block.textContent;
  const m = text.match(/^\s*(#{1,3}\s|[-*]\s|\d+\.\s|\[\s*\]\s|>\s|---\s|~~~\s)$/);
  if (!m) return;
  e.preventDefault();
  const trigger = m[1].trim();
  let type = 'heading';
  if (/^#{1,3}$/.test(trigger)) type = 'heading';
  else if (/^[-*]$/.test(trigger)) type = 'bullet';
  else if (/^\d+\.$/.test(trigger)) type = 'number';
  else if (/^\[$/.test(trigger) || /^\]\s?$/.test(trigger)) type = 'checklist';
  else if (/^>$/.test(trigger)) type = 'quote';
  else if (/^---$/.test(trigger)) type = 'hr';
  else if (/^~~~$/.test(trigger)) type = 'pre';
  applyMarkdown(type, block, trigger);
}

function handleEnter(e) {
  const sel = window.getSelection();
  const li = sel.anchorNode?.closest?.('li');
  if (li && li.closest('#editor')) {
    const atEnd = caretAtEnd(li);
    const isEmpty = isListItemEmpty(li);
    if (isEmpty) {
      // Enter on empty item → exit list (let browser do formatBlock to p)
      return; // allow default
    }
    if (atEnd) {
      // Let default create a new list item (continues list). Allow default.
      return;
    }
    return; // allow default line break within item
  }
  // At block end (non-list) → allow default (new paragraph via block)
}

function caretAtEnd(node) {
  const sel = window.getSelection();
  if (!sel.rangeCount) return false;
  const r = sel.getRangeAt(0);
  if (r.collapsed) {
    const after = document.createRange();
    after.setStart(r.endContainer, r.endOffset);
    after.setEnd(node, node.childNodes.length);
    return after.toString().trim() === '';
  }
  return false;
}
function isListItemEmpty(li) {
  const sel = window.getSelection();
  if (!sel.rangeCount) return false;
  const r = sel.getRangeAt(0);
  if (!r.collapsed) return false;
  // Caret must be at the very start of the li's content
  const before = document.createRange();
  before.setStart(li, 0); before.setEnd(r.startContainer, r.startOffset);
  const inp = li.querySelector('.ckb');
  let inpLen = inp ? 1 : 0;
  return before.toString().replace(/\u00a0/g,'').length <= inpLen;
}

/* ---------- Undo / Redo (custom snapshot history) ---------- */
function getEditorHtml() { return sanitizeHtml(editor.innerHTML); }
function pushHistory(silent) {
  if (isRestoring) return;
  const html = getEditorHtml();
  const key = html + '||' + titleEl.value;
  if (key === lastContentKey && histIndex >= 0) {
    // same content (e.g., formatting no-op); update top snapshot in place
    if (hist[histIndex]) { hist[histIndex].sel = saveSelection(); }
    return;
  }
  hist = hist.slice(0, histIndex + 1);
  hist.push({ html, title: titleEl.value, sel: saveSelection() });
  if (hist.length > CFG.HISTORY_LIMIT) hist.shift(), histIndex--;
  histIndex = hist.length - 1;
  lastContentKey = key;
  if (!silent) debounceAutosave();
}
function doUndo() {
  if (histIndex > 0) {
    histIndex--;
    restoreSnapshot(hist[histIndex]);
  }
}
function doRedo() {
  if (histIndex < hist.length - 1) {
    histIndex++;
    restoreSnapshot(hist[histIndex]);
  }
}
function restoreSnapshot(snap) {
  isRestoring = true;
  titleEl.value = snap.title || 'Untitled';
  editor.innerHTML = snap.html;
  restoreSelection(snap.sel);
  lastContentKey = snap.html + '||' + snap.title;
  histIndex = histIndex; // keep
  isRestoring = false;
  autosaveImmediately();
  updateToolbar();
}
function debounceAutosave() {
  clearTimeout(autosaveTimer);
  autosaveTimer = setTimeout(autosaveImmediately, CFG.AUTOSAVE_DEBOUNCE);
  updateSaved('saving');
}
function autosaveImmediately() {
  const note = currentNote();
  if (!note) return;
  const html = getEditorHtml();
  const title = (titleEl.value || '').trim() || 'Untitled';
  if (note.html === html && note.title === title) { updateSaved('saved'); return; }
  note.html = html; note.title = title; note.updatedAt = Date.now();
  markDirty();
  renderNoteList();
  updateSaved('saved');
}
function onEditorInput() {
  pushHistory();
}

/* ---------- Selection save/restore by text offset ---------- */
function saveSelection() {
  const sel = window.getSelection();
  if (!sel.rangeCount || !editor.contains(sel.anchorNode)) return {start:0,end:0};
  const s = textOffset(sel.anchorNode, sel.anchorOffset);
  const en = textOffset(sel.focusNode, sel.focusOffset);
  return { start: Math.min(s,en), end: Math.max(s,en) };
}
function textOffset(node, offset) {
  if (node.nodeType === 3) return offset;
  let count = 0;
  for (let i=0;i<node.childNodes.length;i++){
    const c = node.childNodes[i];
    if (c.nodeName === 'INPUT') { /* checkbox = 0 width in our model */ continue; }
    if (i < offset) count += c.nodeType===3 ? c.nodeLength : textOffset(c, c.nodeType===3?c.nodeLength:0);
    else if (i === offset) { count += textOffset(c, 0); break; }
  }
  return count;
}
function restoreSelection(s) {
  if (!s) return;
  const target = {node:null}, off = {v:0};
  let start = Math.max(0, Math.min(s.start||0, textLen(editor)));
  (function walk(node){
    if (target.node) return;
    if (node.nodeType === 3) {
      const len = node.nodeLength;
      if (start < off.v + len) { target.node = node; off.v = start - off.v; }
      else off.v += len;
    } else {
      for (const c of node.childNodes) { if (c.nodeName==='INPUT') continue; walk(c); if (target.node) return; }
    }
  })(editor);
  if (!target.node) {
    const tn = lastTextNode(editor);
    if (tn) { target.node = tn; off.v = Math.min(start, tn.nodeLength); }
    else return;
  }
  const range = document.createRange();
  range.setStart(target.node, Math.min(off.v, target.node.nodeLength));
  range.collapse(true);
  const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range);
}
function textLen(node){ let n=0; (function w(x){ for(const c of x.childNodes){ if(c.nodeName==='INPUT')continue; if(c.nodeType===3)n+=c.nodeLength; else w(c);} })(node); return n; }
function lastTextNode(node){ let last=null; (function w(x){ for(const c of x.childNodes){ if(c.nodeName==='INPUT')continue; if(c.nodeType===3)last=c; else w(c); if(last)return;} })(node); return last; }

/* ---------- Toolbar state ---------- */
function updateToolbar() {
  if (!editor) return;
  const sel = window.getSelection();
  const inEditor = sel.anchorNode && editor.contains(sel.anchorNode);
  const set = (cmd, on) => { const b = $('[data-cmd="'+cmd+'"]', editor.closest('#editorApp')); if(b) b.classList.toggle('active', on); };
  if (!inEditor) { ['bold','italic','underline','strike','code','h1','h2','h3','bullet','number','checklist','quote'].forEach(c=>set(c,false)); return; }
  set('bold', document.queryCommandState('bold'));
  set('italic', document.queryCommandState('italic'));
  set('underline', document.queryCommandState('underline'));
  set('strike', document.queryCommandState('strikeThrough') || document.queryCommandState('strikethrough'));
  const anchor = sel.anchorNode;
  set('code', !!(anchor && anchor.closest && anchor.closest('code')));
  const block = anchor && anchor.closest ? anchor.closest('h1,h2,h3') : null;
  set('h1', block && block.tagName === 'H1');
  set('h2', block && block.tagName === 'H2');
  set('h3', block && block.tagName === 'H3');
  set('bullet', document.queryCommandState('insertUnorderedList'));
  set('number', document.queryCommandState('insertOrderedList'));
  set('checklist', !!(anchor && anchor.closest && anchor.closest('li.check')));
  set('quote', !!(anchor && anchor.closest && anchor.closest('blockquote')));
}

function updateSaved(state) {
  if (!savedIndicator) return;
  const pulse = $('.pulse', savedIndicator);
  if (state === 'saving') { savedIndicator.classList.add('saving'); savedIndicator.lastChild.textContent = ' Saving…'; }
  else { savedIndicator.classList.remove('saving'); savedIndicator.lastChild.textContent = ' Saved'; }
}

/* =========================================================================
   NOTE OPERATIONS (single source of truth mutations)
   ========================================================================= */
function currentNote() {
  const id = state.ui.selectedNote;
  if (!id) return null;
  return state.notes.find(n => n.id === id);
}
function getNotebooks() { return state.notebooks; }
function childrenOf(pid) { return state.notebooks.filter(nb => nb.parentId === pid); }
function findNotebook(id) { return state.notebooks.find(nb => nb.id === id); }

function createNote() {
  let notebookId = null;
  if (state.ui.view === 'notebook') notebookId = state.ui.viewId;
  else if (state.ui.view === 'tag') notebookId = null;
  else if (state.ui.view === 'trash') notebookId = null;
  const note = mkNote({ notebookId, title: '', created: Date.now(), updated: Date.now() });
  state.ui.selectedNote = note.id;
  state.ui.view = 'notebook'; state.ui.viewId = note.notebookId;
  markDirty();
  renderAll();
  setTimeout(()=>{ if(titleEl) titleEl.focus(); }, 30);
}
function selectNote(id) {
  state.ui.selectedNote = id;
  // If the note is in trash, view it in trash context
  const note = state.notes.find(n=>n.id===id);
  if (note && note.trashed) { state.ui.view='trash'; state.ui.viewId=null; }
  markDirty();
  renderAll();
}
function selectNotebook(id) {
  state.ui.view = 'notebook'; state.ui.viewId = id; state.ui.activeTag = null;
  markDirty(); renderAll();
}
function viewAllNotes() { state.ui.view='all'; state.ui.viewId=null; state.ui.activeTag=null; markDirty(); renderAll(); }
function viewTrash() { state.ui.view='trash'; state.ui.viewId=null; state.ui.activeTag=null; markDirty(); renderAll(); }
function setSort(s) { state.ui.sort = s; markDirty(); renderAll(); }
function setActiveTag(t) {
  state.ui.activeTag = (state.ui.activeTag === t) ? null : t;
  markDirty(); renderAll();
}
function clearTagFilter() { state.ui.activeTag = null; markDirty(); renderAll(); }

function togglePin(note) {
  note.pinned = !note.pinned; note.updatedAt = Date.now();
  markDirty(); renderAll();
}
function duplicateNote(note) {
  const copy = mkNote({
    notebookId: note.notebookId, title: note.title + ' copy',
    html: note.html, tags: [...note.tags], pinned:false,
    created: Date.now(), updated: Date.now()
  });
  state.ui.selectedNote = copy.id;
  markDirty(); renderAll();
  toast('Note duplicated');
}
function deleteNote(note) {
  if (note.trashed) {
    // permanent delete
    state.notes = state.notes.filter(n => n.id !== note.id);
    if (state.ui.selectedNote === note.id) state.ui.selectedNote = null;
    markDirty(); renderAll();
    toast('Deleted permanently');
  } else {
    note.trashed = true; note.trashedAt = Date.now();
    if (state.ui.selectedNote === note.id) state.ui.selectedNote = null;
    markDirty(); renderAll();
    toast('Moved to Trash');
  }
}
function restoreNote(note) {
  note.trashed = false; note.trashedAt = null; note.updatedAt = Date.now();
  markDirty(); renderAll();
  toast('Restored from Trash');
}
function moveNote(note, notebookId) {
  note.notebookId = notebookId; note.updatedAt = Date.now();
  markDirty(); renderAll();
  toast(notebookId ? 'Moved to notebook' : 'Moved to All Notes');
}
function renameNotebook(nb) { promptRename(nb); }
function deleteNotebook(nb) {
  confirmModal('Delete notebook?', `"${nb.name}" and its notes will be moved to Trash.`, 'Delete', ()=>{
    const kids = childrenOf(nb.id);
    kids.forEach(k => deleteNotebookRecursive(k));
    // Move notes directly under this notebook to trash? Spec: move notes to trash.
    state.notes.filter(n => n.notebookId === nb.id).forEach(n => { n.trashed=true; n.trashedAt=Date.now(); });
    state.notebooks = state.notebooks.filter(x => x.id !== nb.id);
    if (state.ui.view==='notebook' && state.ui.viewId===nb.id) viewAllNotes();
    markDirty(); renderAll();
    toast('Notebook deleted');
  });
}
function deleteNotebookRecursive(nb) {
  childrenOf(nb.id).forEach(k => deleteNotebookRecursive(k));
  state.notebooks = state.notebooks.filter(x => x.id !== nb.id);
}
function addNotebook() {
  const nb = mkNotebook('New Notebook');
  nb.collapsed = false;
  markDirty(); renderAll();
  setTimeout(()=>promptRename(nb), 40);
}

function promptRename(nb) {
  modalInput('Rename', nb.name || 'New Notebook', (val)=>{
    const t = (val||'').trim();
    if (t) { nb.name = t; markDirty(); renderAll(); }
  });
}

/* =========================================================================
   RENDERING — SIDEBAR
   ========================================================================= */
function renderSidebar() {
  const tree = $('#notebookTree');
  tree.innerHTML = '';
  // New note button
  // (placed in list pane actually)
  // All Notes
  const all = el('div', {className:'tree-item all-notes'+(state.ui.view==='all'?' active':'')});
  all.append(el('div', {className:'tree-row all-notes-row'+(state.ui.view==='all'?' active':'')},
    el('span',{className:'chevron'}, chevronIcon()),
    el('span',{className:'label'}, 'All Notes'),
    el('span',{className:'count'}, String(countNotes(false)))
  ));
  all.querySelector('.tree-row').addEventListener('click', viewAllNotes);
  tree.append(all);

  // Root notebooks (parentId === null)
  const roots = state.notebooks.filter(nb => !nb.parentId);
  roots.forEach(nb => tree.append(renderNotebookItem(nb)));

  // Trash
  const trashCount = state.notes.filter(n=>n.trashed).length;
  const trash = el('div', {className:'tree-item trash'+(state.ui.view==='trash'?' active':'')});
  trash.append(el('div', {className:'tree-row trash-row'+(state.ui.view==='trash'?' active':'')},
    el('span',{className:'label'}, '🗑 Trash'),
    el('span',{className:'count'}, trashCount?String(trashCount):'')
  ));
  trash.querySelector('.tree-row').addEventListener('click', viewTrash);
  tree.append(trash);
}
function renderNotebookItem(nb) {
  const item = el('div', {className:'tree-item'+(nb.collapsed?' collapsed':'')+(childrenOf(nb.id).length?' has-children':'')});
  item.dataset.id = nb.id;
  const count = countNotesForNotebook(nb.id);
  const row = el('div', {className:'tree-row'+(state.ui.view==='notebook' && state.ui.viewId===nb.id?' active':'')});
  row.append(
    el('span',{className:'chevron'}, chevronIcon()),
    el('span',{className:'label'}, nb.name),
    el('span',{className:'count'}, count?String(count):'')
  );
  // actions
  const actions = el('div',{className:'row-actions'});
  const ren = el('button',{className:'icon-btn', title:'Rename'}, '✎');
  ren.addEventListener('click', (e)=>{ e.stopPropagation(); renameNotebook(nb); });
  const del = el('button',{className:'icon-btn', title:'Delete'}, '🗑');
  del.addEventListener('click', (e)=>{ e.stopPropagation(); deleteNotebook(nb); });
  actions.append(ren, del);
  row.append(actions);

  row.addEventListener('click', ()=> selectNotebook(nb.id));
  row.addEventListener('dblclick', ()=> promptRename(nb));
  // collapse toggle
  row.addEventListener('click', (e)=>{ if(e.target.closest('.row-actions')) return; nb.collapsed=!nb.collapsed; renderAll(); });

  const kids = childrenOf(nb.id);
  const wrap = el('div',{className:'tree-children'});
  kids.forEach(k => wrap.append(renderNotebookItem(k)));
  item.append(row, wrap);

  // drag & drop to move notes into this notebook
  item.addEventListener('dragover', (e)=>{ if(isNoteDrag(e)){ e.preventDefault(); item.classList.add('drag-over'); }});
  item.addEventListener('dragleave', ()=> item.classList.remove('drag-over'));
  item.addEventListener('drop', (e)=>{ if(isNoteDrag(e)){ e.preventDefault(); item.classList.remove('drag-over'); const nid=dragNoteId; if(nid){ const n=state.notes.find(x=>x.id===nid); if(n&&!n.trashed) moveNote(n, nb.id);} }});
  return item;
}
function countNotes(trashedOnly) {
  return state.notes.filter(n => trashedOnly ? n.trashed : !n.trashed).length;
}
function countNotesForNotebook(nbId) {
  return state.notes.filter(n => !n.trashed && n.notebookId === nbId).length;
}

/* =========================================================================
   RENDERING — NOTE LIST
   ========================================================================= */
function visibleNotes() {
  let list;
  const q = ($('#searchInput').value||'').trim().toLowerCase();
  if (state.ui.view === 'trash') {
    list = state.notes.filter(n => n.trashed);
  } else if (state.ui.view === 'tag') {
    list = state.notes.filter(n => !n.trashed && n.tags.includes(state.ui.activeTag));
  } else if (state.ui.view === 'notebook') {
    list = state.notes.filter(n => !n.trashed && n.notebookId === state.ui.viewId);
  } else { // all
    list = state.notes.filter(n => !n.trashed);
  }
  // tag filter
  if (state.ui.activeTag && state.ui.view !== 'tag') {
    list = list.filter(n => n.tags.includes(state.ui.activeTag));
  }
  // search
  if (q) {
    list = list.filter(n => (n.title||'').toLowerCase().includes(q) || (n._searchText||'').includes(q));
  }
  // sort
  const sort = state.ui.sort;
  list.sort((a,b)=>{
    if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
    if (sort === 'created') return b.createdAt - a.createdAt;
    if (sort === 'alpha') return (a.title||'').localeCompare(b.title||'');
    return b.updatedAt - a.updatedAt; // modified
  });
  return list;
}
function renderNoteList() {
  const el0 = $('#noteListEl');
  const titleEl2 = $('#listTitle');
  const countEl = $('#listCount');
  const sortWrap = $('#sortWrap');
  const tagBar = $('#tagFilterBar');

  // Header title
  if (state.ui.view === 'trash') { titleEl2.textContent = 'Trash'; }
  else if (state.ui.view === 'tag') { titleEl2.textContent = 'Tag: ' + state.ui.activeTag; }
  else if (state.ui.view === 'notebook') { const nb = findNotebook(state.ui.viewId); titleEl2.textContent = nb ? nb.name : 'Notebook'; }
  else { titleEl2.textContent = 'All Notes'; }

  // Sort button label
  const sortLabel = {modified:'Modified', created:'Created', alpha:'A–Z'}[state.ui.sort] || 'Modified';
  sortWrap.innerHTML = '';
  sortWrap.append(el('button',{className:'sort-btn'+(state.ui.sort==='modified'?' active':''), 'data-sort':'modified', title:'Sort by last modified'}, 'Modified ▾'));
  sortWrap.append(el('button',{className:'sort-btn'+(state.ui.sort==='created'?' active':''), 'data-sort':'created'}, 'Created'));
  sortWrap.append(el('button',{className:'sort-btn'+(state.ui.sort==='alpha'?' active':''), 'data-sort':'alpha'}, 'A–Z'));
  $$('.sort-btn', sortWrap).forEach(b => b.addEventListener('click', ()=> setSort(b.dataset.sort)));

  // Tag filter bar
  if (state.ui.activeTag) {
    tagBar.classList.add('show');
    $('#activeFilterText').textContent = state.ui.activeTag;
  } else {
    tagBar.classList.remove('show');
  }

  const notes = visibleNotes();
  countEl.textContent = notes.length + (notes.length===1?' note':' notes');

  el0.innerHTML = '';
  if (notes.length === 0) {
    el0.append(el('div',{className:'empty-list'},
      el('div',{className:'empty-ico'}, state.ui.view==='trash'?'🗑':'🔍'),
      el('div',{className:'msg'}, state.ui.view==='trash'?'Trash is empty':'No notes here')
    ));
    return;
  }
  const q = ($('#searchInput').value||'').trim().toLowerCase();
  notes.forEach(note => {
    el0.append(renderNoteCard(note, q));
  });
}
function renderNoteCard(note, q) {
  const card = el('div',{className:'note-card'+(state.ui.selectedNote===note.id?' active':'')});
  const snippetText = snippet(note.html);
  const time = fmtTime(note.updatedAt);
  const titleHtml = highlight(esc(note.title), q);
  const snippetHtml = highlight(esc(snippetText), q);
  card.append(
    el('div',{className:'nc-top'},
      note.pinned ? el('span',{className:'pin-icon'}, '📌') : el('span'),
      el('span',{className:'nc-title'}, html2node(titleHtml))
    ),
    el('div',{className:'nc-meta'},
      el('span',{className:'nc-snippet'}, html2node(snippetHtml)),
      el('span',{className:'nc-time'}, time)
    )
  );
  if (note.tags.length) {
    const tags = el('div',{className:'nc-tags'});
    note.tags.slice(0,4).forEach(t => tags.append(el('span',{className:'tag-chip'}, '#'+t)));
    card.append(tags);
  }
  // Actions
  const actions = el('div',{className:'card-actions'});
  const pin = el('button',{className:'icon-btn pin-toggle'+(note.pinned?' active':''), title: note.pinned?'Unpin':'Pin'}, '📌');
  pin.addEventListener('click', (e)=>{ e.stopPropagation(); togglePin(note); });
  const more = el('button',{className:'icon-btn', title:'More'}, '•••');
  more.addEventListener('click', (e)=>{ e.stopPropagation(); toggleMenu('card-'+note.id, [
    {label:'Duplicate', onclick:()=>{ duplicateNote(note); closeMenus(); }},
    {label:'Move to…', submenu: moveMenuItems(note)},
    {sep:true},
    {label: state.ui.view==='trash'?'Restore from Trash':'Delete', danger: state.ui.view!=='trash', onclick:()=>{ deleteNote(note); closeMenus(); }}
  ]);
  actions.append(pin, more);
  card.append(actions);

  // Click to select
  card.addEventListener('click', ()=> selectNote(note.id));
  // Tag click
  $$('.tag-chip', card).forEach(chip => chip.addEventListener('click', (e)=>{ e.stopPropagation(); setActiveTag(chip.textContent.replace('#','')); }));
  // Drag to move
  card.draggable = true;
  card.addEventListener('dragstart', (e)=>{ dragNoteId = note.id; e.dataTransfer.effectAllowed='move'; e.dataTransfer.setData('text/plain', note.id); });
  card.addEventListener('dragend', ()=>{ dragNoteId=null; });
  return card;
}
function html2node(html){ const t=document.createElement('span'); t.innerHTML=html; return t; }
function highlight(text, q) {
  if (!q) return text;
  const idx = text.toLowerCase().indexOf(q);
  if (idx < 0) return text;
  return text.slice(0,idx) + '<mark class="mark">'+text.slice(idx, idx+q.length)+'</mark>' + text.slice(idx+q.length);
}

/* =========================================================================
   RENDERING — EDITOR
   ========================================================================= */
function renderEditor() {
  const host = $('#editorApp');
  const note = currentNote();
  if (!state.ui.selectedNote) { showNoNote(); return; }
  if (!note) { showNoNote(); return; }
  rebuildEditorDOM();
  // meta bar (tags input)
  const meta = $('.editor-meta');
  meta.innerHTML = '';
  meta.append(el('div',{className:'note-tag-input-wrap'}, tagChips(note)));
  // tag input
  const inp = el('input',{className:'tag-input', type:'text', placeholder:'+ Add tag'});
  inp.addEventListener('keydown', (e)=>{
    if (e.key==='Enter') {
      e.preventDefault();
      const val = inp.value.trim().toLowerCase().replace(/^#/,'');
      if (val && !note.tags.includes(val)) { note.tags.push(val); note.updatedAt=Date.now(); markDirty(); renderEditor(); renderNoteList(); }
    } else if (e.key==='Backspace' && inp.value==='' && note.tags.length) {
      note.tags.pop(); note.updatedAt=Date.now(); markDirty(); renderEditor(); renderNoteList();
    }
  });
  meta.append(inp);
  openNote(note);
}
function tagChips(note) {
  const wrap = el('div',{className:'tags-row'});
  note.tags.forEach(t => {
    const chip = el('span',{className:'tag'}, t, el('button',{className:'x', type:'button', title:'Remove tag'}, '×'));
    chip.querySelector('.x').addEventListener('click', ()=>{ note.tags = note.tags.filter(x=>x!==t); note.updatedAt=Date.now(); markDirty(); renderEditor(); renderNoteList(); });
    chip.addEventListener('click', ()=> setActiveTag(t));
    wrap.append(chip);
  });
  return wrap;
}

/* =========================================================================
   MENUS (dropdowns)
   ========================================================================= */
let openMenu = null;
function closeMenus() { if (openMenu) { openMenu.remove(); openMenu=null; } }
function toggleMenu(id, items) {
  if (openMenu) { openMenu.remove(); openMenu=null; }
  if (id==='moreMenu') {
    // build from moreMenuItems
    items = moreMenuItems();
  }
  const menu = el('div',{className:'menu', 'data-menu':id});
  items.forEach(it => {
    if (it.sep) { menu.append(el('div',{className:'menu-sep'})); return; }
    if (it.submenu) {
      const mi = el('button',{className:'menu-item', type:'button'}, it.label, el('span',{className:'menu-kbd'}, '›'));
      mi.addEventListener('mouseenter', ()=>{ closeSub(); const subWrap=el('div'); const sub=toggleMenu('sub', it.submenu); subWrap.append(sub); subWrap.style.position='absolute'; subWrap.style.left='100%'; subWrap.style.top=0; mi.append(subWrap); });
      menu.append(mi);
    } else {
      const mi = el('button',{className:'menu-item'+(it.danger?' danger':''), type:'button'}, it.label, it.kbd?el('span',{className:'menu-kbd'}, it.kbd):null);
      mi.addEventListener('click', ()=>{ it.onclick&&it.onclick(); closeMenus(); });
      menu.append(mi);
    }
  });
  document.body.append(menu);
  const btn = $('#moreMenuBtn');
  const r = btn.getBoundingClientRect();
  menu.style.top = (r.bottom + 4) + 'px';
  menu.style.right = '12px';
  openMenu = menu;
  setTimeout(()=> document.addEventListener('click', closeMenusOuter), 0);
}
function closeMenusOuter(e) { if (openMenu && !openMenu.contains(e.target)) closeMenus(); }
function closeSub() {}
function moreMenuItems(note) {
  note = note || currentNote();
  if (!note) return [];
  const items = [
    {label:'Duplicate', onclick:()=>{ duplicateNote(note); }},
    {label:'Move to…', submenu: moveMenuItems(note)},
  ];
  if (state.ui.view === 'trash') {
    items.push({sep:true}, {label:'Restore', onclick:()=>{ restoreNote(note); }},
      {label:'Delete permanently', danger:true, onclick:()=>{ deleteNote(note); }});
  } else {
    items.push({sep:true}, {label:'Move to Trash', onclick:()=>{ deleteNote(note); }},
      {label:'Delete permanently', danger:true, onclick:()=>{ deleteNote(note); toast('Delete permanently requires opening Trash'); }});
  }
  return items;
}
function moveMenuItems(note) {
  const items = [{label:'All Notes', onclick:()=>{ moveNote(note, null); }}, {sep:true}];
  state.notebooks.forEach(nb => items.push({label: nb.name, onclick:()=>{ moveNote(note, nb.id); }}));
  if (state.notebooks.length===0) items.push({label:'(no notebooks)', onclick:()=>{}});
  return items;
}

/* =========================================================================
   DRAG & DROP (notes)
   ========================================================================= */
let dragNoteId = null;
function isNoteDrag(e) { return e.dataTransfer && e.dataTransfer.types && e.dataTransfer.types.includes('text/plain'); }

/* =========================================================================
   SEARCH
   ========================================================================= */
function precomputeSearchText() {
  state.notes.forEach(n => { n._searchText = extractText(n.html).toLowerCase(); });
}

/* =========================================================================
   THEME + LAYOUT
   ========================================================================= */
function applyTheme() { document.documentElement.setAttribute('data-theme', state.settings.theme); }
function toggleTheme() { state.settings.theme = state.settings.theme==='light'?'dark':'light'; applyTheme(); markDirty(); }
function applyLayout() {
  const l = state.settings.layout;
  document.documentElement.style.setProperty('--sidebar-w', (l.sidebarW||250)+'px');
  document.documentElement.style.setProperty('--list-w', (l.listW||300)+'px');
  const app = $('#app');
  if (state.settings.sidebarCollapsed) app.classList.add('sidebar-collapsed');
  else app.classList.remove('sidebar-collapsed');
}

/* =========================================================================
   RESIZABLE PANES
   ========================================================================= */
function initResize() {
  makeResize('#handleSidebar', 'x', (delta)=>{
    const l = state.settings.layout;
    l.sidebarW = Math.max(180, Math.min(460, (l.sidebarW||250)+delta));
    applyLayout();
  });
  makeResize('#handleList', 'x', (delta)=>{
    const l = state.settings.layout;
    l.listW = Math.max(220, Math.min(560, (l.listW||300)+delta));
    applyLayout();
  });
  // Sidebar collapse via brand double-click or button
  const collapseBtn = el('button',{className:'icon-btn', id:'collapseSidebar', title:'Collapse sidebar'}, '«');
  // attach to sidebar head actions
  const sha = $('.sidebar-actions');
  sha.insertBefore(collapseBtn, sha.firstChild);
  collapseBtn.addEventListener('click', ()=>{ state.settings.sidebarCollapsed = !state.settings.sidebarCollapsed; applyLayout(); });
}
function makeResize(selector, dir, onDelta) {
  const handle = $(selector);
  let start = 0, base = 0, active = false;
  handle.addEventListener('mousedown', (e)=>{
    e.preventDefault();
    active = true; start = e.clientX;
    const l = state.settings.layout;
    base = dir==='x' ? (l.sidebarW||250) : (l.listW||300);
    handle.classList.add('active');
    document.body.style.userSelect='none';
    document.addEventListener('mousemove', onMouse);
    document.addEventListener('mouseup', ()=>{
      active=false; handle.classList.remove('active');
      document.body.style.userSelect='';
      document.removeEventListener('mousemove', onMouse);
      markDirty();
    });
    function onMouse(ev){ if(!active) return; onDelta(ev.clientX - start); }
  });
}

/* =========================================================================
   ICONS (inline SVG)
   ========================================================================= */
function chevronIcon(){ return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>'; }
function arrowIcon(){ return '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>'; }
function dotMenuIcon(){ return '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg>'; }

/* =========================================================================
   MODALS
   ========================================================================= */
function modalInput(title, defVal, onOk) {
  const bd = el('div',{className:'modal-backdrop'});
  const m = el('div',{className:'modal'});
  m.append(el('h3',{},title), el('input',{type:'text', id:'modalInput', value:defVal}),
    el('div',{className:'modal-actions'},
      el('button',{className:'btn-ghost', id:'modalCancel'}, 'Cancel'),
      el('button',{className:'btn-primary', id:'modalOk'}, 'Save')
    ));
  bd.append(m); document.body.append(bd);
  const input = $('#modalInput'); input.focus(); input.select();
  const close = (ok)=>{ bd.remove(); if(ok) onOk(input.value); };
  $('#modalCancel', m).addEventListener('click', ()=> close(false));
  $('#modalOk', m).addEventListener('click', ()=> close(true));
  input.addEventListener('keydown', (e)=>{ if(e.key==='Enter') close(true); if(e.key==='Escape') bd.remove(); });
  bd.addEventListener('click', (e)=>{ if(e.target===bd) bd.remove(); });
}
function confirmModal(title, msg, btnLabel, onOk) {
  const bd = el('div',{className:'modal-backdrop'});
  const m = el('div',{className:'modal'});
  m.append(el('h3',{},title), el('p',{style:'margin:0 0 4px;color:var(--text-muted);font-size:13px;line-height:1.4'}, msg),
    el('div',{className:'modal-actions'},
      el('button',{className:'btn-ghost', id:'modalCancel'}, 'Cancel'),
      el('button',{className:'btn-primary', id:'modalOk', style: btnLabel==='Delete'||btnLabel==='Delete permanently'?'background:var(--danger);':''}, btnLabel)
    ));
  bd.append(m); document.body.append(bd);
  const close=(ok)=>{ bd.remove(); if(ok) onOk(); };
  $('#modalCancel', m).addEventListener('click', ()=> close(false));
  $('#modalOk', m).addEventListener('click', ()=> close(true));
  input=null;
  bd.addEventListener('click', (e)=>{ if(e.target===bd) bd.remove(); });
  document.addEventListener('keydown', function once(e){ if(e.key==='Escape'){ bd.remove(); document.removeEventListener('keydown', once);} });
}

/* =========================================================================
   NAV / BACK
   ========================================================================= */
function goBack() { viewAllNotes(); }

/* =========================================================================
   GLOBAL EVENTS
   ========================================================================= */
function initGlobal() {
  $('#themeToggle').addEventListener('click', toggleTheme);
  $('#clearFilter').addEventListener('click', clearTagFilter);
  $('#searchInput').addEventListener('input', ()=>{ precomputeSearchText(); renderNoteList(); });

  // Global keyboard shortcuts
  document.addEventListener('keydown', (e)=>{
    const mod = e.ctrlKey || e.metaKey;
    if (mod && e.key.toLowerCase()==='n') { e.preventDefault(); createNote(); }
    if (mod && e.shiftKey && (e.key.toLowerCase()==='z'||e.key.toLowerCase()==='y')) { e.preventDefault(); const note=currentNote(); if(note){ editor.focus(); doRedo(); } }
    if (mod && e.key.toLowerCase()==='d') { e.preventDefault(); toggleTheme(); }
    if (e.key==='Escape') { closeMenus(); }
    if (e.key==='Enter' && e.key.toLowerCase()==='n' && mod) {}
  });
  // Click outside to close menus
  document.addEventListener('click', (e)=>{ if(openMenu && !openMenu.contains(e.target)) closeMenus(); });
  // New note button
  document.body.insertAdjacentHTML('beforeend','');
}

/* =========================================================================
   RENDER ALL
   ========================================================================= */
function renderAll() {
  precomputeSearchText();
  renderSidebar();
  renderNoteList();
  renderEditor();
}

/* =========================================================================
   INIT
   ========================================================================= */
function init() {
  load();
  initEditor();
  initResize();
  initGlobal();
  // New note button in sidebar area (append into list head area is not ideal; use a dedicated button)
  // We'll inject a "New note" button into the list head.
  const listHead = $('.list-head');
  const newBtn = el('button',{className:'new-note-btn', id:'newNoteBtn'}, '+ New Note');
  newBtn.addEventListener('click', createNote);
  listHead.append(newBtn);

  precomputeSearchText();
  // Restore selection
  if (state.ui.selectedNote) {
    const note = state.notes.find(n=>n.id===state.ui.selectedNote);
    if (!note || note.trashed) state.ui.selectedNote = state.notes.find(n=>!n.trashed)?.id || null;
  }
  renderAll();

  // Markdown shortcut: space triggers conversion
  editor && editor.addEventListener('keydown', (e)=>{ if(e.key===' '||e.key==='Spacebar') handleMarkdownShortcut(e); });
}

document.addEventListener('DOMContentLoaded', init);
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"bartowski/Ornith-1.5-35B-A3B","provider":"brigid","persona":"full","sessionId":"6afab0b1-711e-4771-aa4b-6305d9d0af90","tokensIn":8209354,"tokensOut":114547,"tokensTotal":8323901,"cost":0,"turns":110,"toolCalls":109,"failedToolCalls":6,"timestamp":"2026-08-23T20:53:27.164Z"} -->