/* The theme contract (colour tokens, six palettes) lives in theme.css and is
   pulled in HERE, not via a <link> in each page: index.html, web.html and four
   test harnesses all load styles.css, and a separate link is something every new
   page has to remember. It was forgotten immediately — the Settings palette
   swatches rendered colourless in the e2e harness. One import, no drift.
   (quick-note.html links theme.css directly; it does not load styles.css.) */
@import url("theme.b4f9d47d.css");

/* iOS/WebKit inflates text inside a WebView when it judges a block "too narrow
   to read", which is why every label on the phone rendered a step or two above
   what `Interface size` asks for while desktop looked correct. Pinning this to
   100% makes the app's own type scale authoritative on both platforms. It is a
   no-op on desktop (100% means "leave it alone"), so it is safe outside the
   phone media query. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

/* styles.css — Inkdrop-inspired three-pane theme with light/dark support.
   Variables drive theming; .theme-dark on <body> swaps the palette. */

/* Bundled UI/reading sans — Inter (SIL OFL, Google Fonts). The screen sans the
   modern note apps standardized on: Notion's default, Obsidian's interface font,
   Evernote's UI since 2024. A note app reads and writes prose, so it wants a
   clean neo-grotesque, not a newsprint serif or a code face. Latin subset,
   variable 400–700 (~48 KB); CJK falls through to the system sans (PingFang /
   YaHei) via --font-ui. This is the ONE face we ship. */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("fonts/inter-latin.b4f9d47d.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}


* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
  font-family: var(--font-ui);
  color: var(--text);
  background: var(--editor-bg);
  /* Interface text size (Preferences → Appearance). The three editing surfaces
     keep their own sizes — this is chrome: sidebar, note list, buttons, dialogs. */
  font-size: var(--ui-font-size);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow: hidden;
}

/* cross-platform scrollbars (WebKit/WebView2) */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: rgba(128, 128, 128, 0.35); border-radius: 5px; }
::-webkit-scrollbar-thumb:hover { background: rgba(128, 128, 128, 0.55); }
::-webkit-scrollbar-track { background: transparent; }

#app { display: grid; grid-template-columns: var(--col-sidebar, 232px) var(--col-notelist, 312px) 1fr; height: 100vh; position: relative; }
.col-resizer { position: absolute; top: 0; bottom: 0; width: 7px; margin-left: -3px;
  cursor: col-resize; z-index: 50; }
.col-resizer::after { content: ""; position: absolute; left: 3px; top: 0; bottom: 0; width: 1px; background: transparent; }
.col-resizer:hover::after, .col-resizer.dragging::after { background: var(--accent); }
/* sidebar toggled off (⌘S): collapse its column and hide its divider */
body.sidebar-collapsed #sidebar { display: none; }
body.sidebar-collapsed #resizer-1 { display: none; }
body.sidebar-collapsed #app { grid-template-columns: var(--col-notelist, 312px) 1fr; }

/* ---- sidebar ---------------------------------------------------------------- */
/* -webkit- prefix as well as the standard property: this app ships in
   WKWebView, where the unprefixed `user-select` did not stop a press-and-drag
   from painting a native text selection across sidebar rows (the stray blue
   block). The notebook rows only looked immune because beginBookDrag
   suppresses `selectstart` in JS; rows without a drag handler had nothing. */
#sidebar { background: var(--sidebar-bg); color: var(--sidebar-fg); padding: 12px 10px; overflow: hidden;
  user-select: none; -webkit-user-select: none; display: flex; flex-direction: column; }
.sidebar-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.sidebar-footer { flex: 0 0 auto; margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--sidebar-border); display: flex; align-items: center; gap: 6px; }
/* Dev/test build id next to Settings — small, muted, monospace; never wraps. Hidden
   in release (the build_id command returns "" and #build-id keeps its [hidden]). */
.build-id { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--font-mono); font-size: 10.5px; color: var(--text-muted); opacity: 0.7; user-select: text; }
.sidebar-title { display: flex; justify-content: space-between; align-items: center; font-weight: 700; font-size: 15px; padding: 4px 6px 12px; color: var(--sidebar-fg-strong); }
/* Match the AI chat rail's wordmark (.ls-head h1): 19px / 600 / -0.02em.
   Truncate (don't push the buttons out) so the tight-width behaviour matches the
   chat rail: wordmark ellipsizes first, the flip pill never gets swallowed. */
.app-name { font-size: 19px; font-weight: 600; letter-spacing: -0.02em; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-title-actions { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
/* Explicit Notes→Chat button — same fixed geometry (width/height/right inset) as
   the chat rail's "Notes" pill so the two pin exactly over each other on flip,
   but a violet "AI" tint (vs the rail's blue) so they're identifiable. */
.ai-chat-btn { display: inline-flex; align-items: center; justify-content: center; gap: 5px; flex: 0 0 auto; min-width: 100px; background: linear-gradient(135deg, color-mix(in oklch, var(--brand) 78%, var(--editor-bg)), var(--brand)); color: var(--accent-ink); border: none; cursor: pointer; font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 8px; line-height: 1.4; }
.ai-chat-btn:hover { filter: brightness(1.08); }
.sidebar-section { display: flex; justify-content: space-between; align-items: center; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--sidebar-muted); padding: 14px 8px 5px; }
/* The section title is the fold control. It has to LOOK like the heading it
   replaced — a button here must not read as chrome — so it inherits everything
   and only gains a caret and a hover tint. */
.section-toggle { display: flex; align-items: center; gap: 3px; background: none; border: 0; padding: 0; margin: 0;
  font: inherit; letter-spacing: inherit; text-transform: inherit; color: inherit; cursor: pointer; }
.section-toggle:hover { color: var(--sidebar-fg); }
.section-caret { display: inline-flex; width: 11px; opacity: 0.8; }
/* Status has no fold (nothing to fold — it is a fixed four-row list), so it has
   no caret either. Pad it by exactly the caret's width so the three section
   titles still start on the same x. */
.sidebar-section:not(:has(.section-toggle)) { padding-left: 22px; }
/* A folded section keeps its header — that is what unfolds it — and drops its list. */
.section-folded { display: none; }
.btn-gone { display: none; }
.nav-item, .tree-item { display: flex; align-items: center; gap: 7px; padding: 5px 8px; border-radius: 6px; cursor: pointer; white-space: nowrap; }
.nav-item:hover, .tree-item:hover { background: var(--sidebar-hover); }
/* Active row, Slack-style: accent fill + the palette's on-accent ink. NOT a
   hardcoded #fff — the yellow palette's fill is light, so white ink on it is
   unreadable; --accent-ink is picked per palette to clear 4.5:1 on the fill. */
.nav-item.active, .tree-item.active { background: var(--accent); color: var(--accent-ink); }
.nav-item.active .nav-icon, .tree-item.active .tree-icon,
.nav-item.active .tree-toggle, .tree-item.active .tree-toggle { color: var(--accent-ink); }
.nav-item.active .nav-count { background: color-mix(in srgb, var(--accent-ink) 22%, transparent); color: var(--accent-ink); }
.nav-icon, .tree-icon { width: 16px; display: inline-flex; align-items: center; justify-content: center; color: var(--sidebar-muted); }
/* Lucide inline icons: stroke = currentColor, so they take the tone of wherever
   they sit — muted in resting sidebar rows, lifted with the row text on
   hover/active, and correct in both themes with zero per-theme rules. */
.lucide { flex: none; display: block; }
.femoji { flex: none; display: block; }
.lucide-spin { animation: lucide-spin 1s linear infinite; }
@keyframes lucide-spin { to { transform: rotate(360deg); } }
/* icons inside text-flow buttons (format bar, chat, conflict rows) sit on the baseline */
.tb-btn .lucide, .ai-chat-btn .lucide, .ai-assist-btn .lucide,
.cd-open .lucide, .note-item-snippet .lucide { display: inline-block; vertical-align: -2px; }
.note-pin .lucide { display: inline-block; vertical-align: -1px; color: var(--accent-text); }
.nav-item:hover .nav-icon, .nav-item.active .nav-icon,
.tree-item:hover .tree-icon, .tree-item.active .tree-icon { color: inherit; }
/* nested notebook tree: tighter spacing than nav rows so labels get more room */
.tree-item { gap: 2px; }
.tree-toggle { flex: 0 0 auto; width: 11px; text-align: right; font-size: 10px; line-height: 1; opacity: 0.8; color: var(--sidebar-muted); cursor: pointer; user-select: none; transition: opacity 0.1s, color 0.1s; }
.tree-toggle:hover { opacity: 1; color: var(--accent-text); }
.tree-toggle.empty { visibility: hidden; cursor: default; pointer-events: none; }
.nav-label, .tree-label { flex: 1; overflow: hidden; text-overflow: ellipsis; }
.nav-count { font-size: 10.5px; color: var(--sidebar-muted); min-width: 18px; text-align: center;
  background: var(--sidebar-hover); border-radius: 99px; padding: 1px 6px; line-height: 1.5; }
.tree { list-style: none; margin: 0; padding: 0; }
.tag-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); display: inline-block; }
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex: 0 0 auto; }
.status-active { background: var(--status-active); }
.status-onHold { background: var(--status-onHold); }
.status-completed { background: var(--status-completed); }
.status-dropped { background: var(--status-dropped); }
.section-actions { display: flex; align-items: center; gap: 2px; }
#sort-books { font-size: 12px; font-weight: 600; }
.icon-btn { background: none; border: none; color: var(--sidebar-muted); cursor: pointer; font-size: 15px; line-height: 1; padding: 2px 6px; border-radius: 4px; }
.icon-btn:hover { background: var(--sidebar-icon-hover); color: var(--sidebar-fg-strong); }
.icon-btn:disabled { opacity: 0.28; cursor: default; pointer-events: none; }
/* Sidebar settings gear: square target + crisp SVG, sized/colored to match the
   chat rail's gear (.ls-ico) so the two left bars read identically. Brighter than
   --sidebar-muted so it's an obvious, findable control next to the AI Chat pill. */
/* Bottom-left "Settings" row — shared by the Notes sidebar (#settings-btn) and
   the chat rail (.ls-settings), so both panels look the same. */
.settings-row { display: flex; align-items: center; gap: 9px; width: 100%; padding: 8px; border: none; background: none; cursor: pointer; border-radius: 6px; color: var(--sidebar-fg); font-size: 13px; font-family: inherit; }
/* In the footer flex row, let Settings take the remaining space so the build id can
   sit beside it (instead of width:100% pushing the id out). */
.sidebar-footer .settings-row { flex: 1 1 auto; width: auto; min-width: 0; }
.settings-row:hover { background: var(--sidebar-icon-hover); color: var(--sidebar-fg-strong); }
.settings-row svg { display: block; flex: 0 0 auto; }

/* ---- note list -------------------------------------------------------------- */
#note-list-pane { background: var(--list-bg); border-right: 1px solid var(--list-border); display: flex; flex-direction: column; overflow: hidden; }
/* No divider under the toolbar — the mock lets the list surface run straight
   through; the sub-bar's whitespace does the separating. */
.list-toolbar { display: flex; gap: 8px; padding: 12px 12px 8px; position: relative; z-index: 60; }
.search-wrap { flex: 1; min-width: 0; position: relative; display: flex; }
.search { flex: 1; min-width: 0; border: 1px solid var(--list-border); border-radius: 8px; padding: 6px 10px; font-size: 13px; background: var(--input-bg); color: var(--text); }
.search:focus { outline: none; border-color: var(--accent-text); }
/* Empty box → room for the keycap so a query can never run underneath it.
   Driven off :placeholder-shown rather than a JS class: the search value is also
   set programmatically (history restore, view switch, note-moved), and a class
   would have to be re-synced at every one of those call sites. */
.search-wrap .search:placeholder-shown { padding-right: 42px; }
/* Same shape as the <kbd> the keymap pane and Markdown preview already use
   (--chip-bg, 2px bottom border) — one keycap look in the app, not a second. */
.search-kbd {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  background: var(--chip-bg); border: 1px solid var(--list-border); border-bottom-width: 2px;
  border-radius: 4px; padding: 1px 5px; font-family: var(--font-mono); font-size: 10px;
  line-height: 1.5; color: var(--text-muted); cursor: pointer; white-space: nowrap;
}
/* Touch shells: app.js swaps the label for a glyph (no ⌘ and no Ctrl to press),
   so the raised-keycap skin has to go with it — it was promising a shortcut
   that cannot be typed. The button and its job survive: this is still the only
   entry point to searching everything. NOT inside the phone media query — the
   class is applied on `pointer: coarse`, and a width rule would miss a tablet
   and wrongly strip the keycap off a narrow desktop window that still has ⌘P. */
.search-kbd-touch {
  background: none; border: none; padding: 4px;
  display: inline-flex; align-items: center; color: var(--text-muted);
}
.search-kbd:hover { color: var(--text); }
.search-kbd:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 1px; }
/* Typing hands the box back to plain note search — the hint has done its job. */
.search-kbd { display: none; }
.search-wrap .search:placeholder-shown ~ .search-kbd { display: block; }
.primary-btn { background: var(--accent); color: var(--accent-ink); border: none; border-radius: 8px; padding: 6px 12px; cursor: pointer; font-weight: 600; white-space: nowrap; }
.primary-btn:hover { filter: brightness(1.06); }
/* The title is the ONLY thing in this row allowed to give up space. A notebook
   name is arbitrarily long, and with the flex defaults it took the whole row and
   wrapped to two lines while the two controls beside it — which have exactly one
   right size — were squeezed instead: "Updated" rendered as a lone "U". So the
   title truncates (min-width:0 is what makes text-overflow work on a flex child)
   and the controls are flex:none. */
.list-subbar { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 8px 14px 4px; }
.list-title { min-width: 0; flex: 0 1 auto; display: flex; align-items: baseline; gap: 4px; overflow: hidden;
  font-size: 13px; font-weight: 650; color: var(--text); }
.list-title-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Never truncated: the count is the one part of this label that is the same
   three characters whatever the notebook is called. */
.list-title-count { flex: none; color: var(--text-muted); font-weight: 500; }
.mini-select { flex: none; font-size: 12px; border: none; background: transparent; color: var(--text-muted); padding: 2px 4px; }
/* The scope checkbox sits to the RIGHT of the title, next to Sort, as its peer.
   Toned to the sub-bar (muted, 12px) so it reads as a view option rather than an
   action; the accent only appears once it is ON, because the list is then
   showing more than its title says. */
/* Just the words. No border, no box, no switch — the accent alone says whether
   the scope is on, and the label never changes to say it a second way. */
.mini-link { flex: none; margin-left: auto; background: none; border: 0; padding: 0; font: inherit; font-size: 12px;
  color: var(--text-muted); cursor: pointer; white-space: nowrap; user-select: none; }
.mini-link:hover { color: var(--text); }
.mini-link.on { color: var(--accent-text); font-weight: 600; }
#scope-toggle.btn-gone { display: none; }
/* No text selection in the list. Rows are drag handles, not prose: without this
   a press-and-drag on a note began a native selection (the stray blue block) —
   the sidebar has said `user-select: none` for the same reason all along. */
.note-list { list-style: none; margin: 0; padding: 0; overflow-y: auto; flex: 1; user-select: none; }
/* The swipe-to-trash surface (mobile.js) is mounted on every shell, because the
   viewport can cross the 700px line at runtime. Everything that makes it a red
   panel lives in the phone media query — so it needs to be NOTHING here, or a
   desktop window grows a stray "Trash" block in the note-list pane. */
.swipe-action { display: none; }
/* Same rule, same reason, for the phone tab bar: it is in the markup on every
   shell (the viewport can cross the 700px line at runtime) and everything that
   makes it a tab bar lives in the phone media query. */
.mobile-tabs { display: none; }
/* THE note-row height, defined once. app.js reads this custom property (`rowH()`)
   instead of carrying its own copy: the number drives the spacer math for
   windowed/remote scrolling, so a JS constant that disagreed with the CSS would
   leave the scrollbar right and everything under it wrong. The phone layout
   overrides it — a shorter row, since it shows one snippet line, not two.
   Sized here for title + a TWO-line snippet + the meta row. */
:root { --note-row-h: 96px; }
.note-item { height: var(--note-row-h); box-sizing: border-box; padding: 10px 14px 11px; border-bottom: 1px solid var(--list-border); cursor: pointer; overflow: hidden; }
/* Remote (Web) virtualization: off-screen spacer <li>s reserve scroll height; skeleton
   rows stand in for not-yet-fetched notes so a fast scroll never shows a blank, and the
   placeholder is unclickable (pointer-events:none) so it's never a dead click target. */
.note-vspacer { list-style: none; padding: 0; margin: 0; }
.note-skeleton { cursor: default; pointer-events: none; display: flex; flex-direction: column; justify-content: center; gap: 8px; }
.note-skeleton .skel-bar { height: 11px; border-radius: 4px; background: linear-gradient(90deg, var(--list-border) 25%, var(--list-hover) 37%, var(--list-border) 63%); background-size: 400% 100%; animation: skel-shimmer 1.4s ease infinite; }
.note-skeleton .skel-title { width: 62%; }
.note-skeleton .skel-snip { width: 88%; height: 9px; }
@keyframes skel-shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }
/* Conflicts list items + the side-by-side diff modal */
/* Conflict rows carry two action buttons, so they can't use the fixed 78px note-row
   height — let them grow so a wrapped "Keep incoming" sits on its own row, not clipped. */
.note-item.conflict-item { height: auto; overflow: visible; }
.conflict-item .conflict-acts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
/* keep full labels readable: never break "Keep yours" mid-word; when the column is
   too narrow for both buttons side-by-side, the second wraps onto its own row. */
.conflict-item .conflict-acts .conflict-btn { flex: 1 1 auto; white-space: nowrap; text-align: center; }
.conflict-btn { padding: 5px 10px; border: 1px solid var(--list-border); border-radius: 6px; background: var(--list-bg); color: var(--text); cursor: pointer; font-size: 12px; }
.conflict-btn.mine { border-color: var(--accent-text); color: var(--accent-text); font-weight: 600; }
.conflict-btn:hover { background: var(--list-hover); }
/* conflict detail — fills the editor pane (right column) */
#conflict-detail { position: absolute; inset: 0; z-index: 6; background: var(--editor-bg); display: flex; flex-direction: column; overflow: hidden; }
#conflict-detail.hidden { display: none; } /* id+class beats the #conflict-detail display:flex above */
.cd-head { padding: 14px 16px 10px; border-bottom: 1px solid var(--list-border); }
.cd-title { font-weight: 700; font-size: 16px; }
.cd-sub { font-size: 12px; color: var(--text-muted); margin-top: 3px; }
.cd-meta { display: flex; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--list-border); }
.cd-col { flex: 1; min-width: 0; border: 1px solid var(--list-border); border-radius: 8px; padding: 10px 12px; }
.cd-col.mine { border-color: var(--accent-text); }
.cd-col-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); margin-bottom: 8px; }
.cd-row { display: flex; gap: 8px; font-size: 12.5px; padding: 2px 0; }
.cd-k { color: var(--text-muted); min-width: 72px; flex: 0 0 72px; }
.cd-v { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cd-open { display: inline-flex; align-items: center; gap: 5px; margin-top: 10px; padding: 5px 10px;
  font-size: 12px; color: var(--text); background: var(--list-bg); border: 1px solid var(--list-border);
  border-radius: 6px; cursor: pointer; transition: border-color 0.12s, color 0.12s; }
.cd-open:hover { border-color: var(--accent-text); color: var(--accent-text); }
.cd-col.mine .cd-open:hover { border-color: var(--accent-text); }
.cd-merge { flex: 1; min-height: 0; overflow: auto; }
.cd-merge .cm-mergeView, .cd-merge .cm-editor { height: auto; }
.cd-acts { display: flex; gap: 8px; justify-content: flex-end; padding: 12px 16px; border-top: 1px solid var(--list-border); }
.cd-acts .conflict-btn { padding: 8px 18px; font-size: 13px; }
.note-item:hover { background: var(--list-hover); }
/* Selection wash alone marks the active row (the old inset accent bar read as
   heavy chrome against the soft June surfaces). */
.note-item.active { background: var(--selection); }
.note-item-top { display: flex; align-items: center; gap: 6px; }
.note-item-title { font-weight: 600; font-size: 13.5px; line-height: 1.35; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Pinned marker: a stroke pin icon in the accent tone — no filled badge, which
   used to blend into the accent-tinted selected/pinned row backgrounds. */
.note-pin { flex: 0 0 auto; display: inline-flex; align-items: center; color: var(--accent-text); }
/* Two-line snippet (mock-style): line-clamp inside the fixed row height. */
.note-item-snippet { color: var(--text-muted); font-size: 12.5px; line-height: 1.45; margin-top: 3px; height: 36px;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
/* The meta row is fixed-height by construction (the row is a fixed 96px), so
   NOTHING in it may wrap. Two flex defaults conspired against that: a flex item's
   `min-width: auto` refuses to shrink below its content, so the tags' ellipsis
   never engaged and they blew the row out instead — and the date, being
   shrinkable, absorbed the overflow by breaking "12 hours ago" across two lines,
   which then pushed the snippet out of the clipped row. Hence min-width:0 on the
   part that may truncate, and no-shrink + nowrap on the parts that may not. */
.note-item-meta { display: flex; align-items: center; gap: 8px; margin-top: 5px; }
.note-item-date { color: var(--text-muted); opacity: 0.8; font-size: 11px; flex: 0 0 auto; white-space: nowrap; }
.note-item-tags { color: var(--accent-text); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  min-width: 0; flex: 0 1 auto; }
.list-empty { padding: 20px 14px; color: var(--text-muted); }
/* matched-term highlight — an accent wash, so it follows the selected palette
   instead of the fixed green it used to be (the same off-theme green that made
   markdown markers stand out; see .tok-md-mark). One rule, both themes: the mix
   is translucent, so it lands correctly on the light and dark list background. */
.note-item mark { background: color-mix(in oklch, var(--accent) 22%, transparent);
  color: inherit; border-radius: 2px; padding: 0 1px; }

.keymap-keys kbd { background: var(--chip-bg); border: 1px solid var(--list-border); border-bottom-width: 2px; border-radius: 4px; padding: 1px 6px; font-family: var(--font-mono); font-size: 11px; margin-left: 4px; }
/* small-caps section header grouping the keymap list */
.keymap-subhead { margin: 18px 0 4px; font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: .05em; }
.keymap-subhead:first-child { margin-top: 2px; }
.task-badge { font-size: 10px; background: var(--chip-bg); color: var(--text-muted); border-radius: 8px; padding: 1px 6px; white-space: nowrap; flex: 0 0 auto; }
.task-badge.done { background: rgba(33, 186, 69, 0.16); color: var(--status-active); }

/* GFM task lists in preview (micromark output, made clickable) */
.markdown-body li.task-item { list-style: none; display: flex; align-items: flex-start; gap: 8px; margin: 3px 0; margin-left: -1.1em; }
.markdown-body .task-checkbox { margin-top: 4px; cursor: pointer; accent-color: var(--accent-text); }
/* GFM tables */
.markdown-body table { border-collapse: collapse; margin: 0.7em 0; font-size: 0.95em; }
.markdown-body th, .markdown-body td { border: 1px solid var(--list-border); padding: 5px 10px; text-align: left; }
.markdown-body th { background: var(--chip-bg); font-weight: 600; }
.markdown-body del { opacity: 0.6; }
.markdown-body .footnotes { margin-top: 1.4em; padding-top: 0.6em; border-top: 1px solid var(--list-border);
  font-size: 0.88em; color: var(--text-muted); }
.markdown-body .footnotes ol { padding-left: 1.4em; }
.markdown-body [data-footnote-ref] { font-size: 0.8em; padding: 0 1px; }
.markdown-body .footnote-backref { text-decoration: none; margin-left: 4px; }
.markdown-body .katex-display { overflow-x: auto; overflow-y: hidden; padding: 2px 0; }
.markdown-body .math-error { color: #c0392b; background: rgba(192,57,43,0.08); }
.markdown-body .mermaid-diagram { margin: 0.8em 0; text-align: center; overflow-x: auto; }
.markdown-body .mermaid-diagram svg { max-width: 100%; height: auto; }
.markdown-body .mermaid-error { color: #c0392b; background: rgba(192,57,43,0.08); white-space: pre-wrap; }

/* ---- editor ----------------------------------------------------------------- */
#editor-pane { background: var(--editor-bg); display: flex; flex-direction: column; position: relative; overflow: hidden; }
/* flex-wrap lets the tool icons drop to a second row instead of overflowing the
   pane's right edge (where #editor-pane{overflow:hidden} would clip them) once the
   window/pane gets narrow. Verified across widths with _uitest/resize.mjs. */
#editor-header { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 11px 16px 8px; border-bottom: 1px solid var(--list-border); }
.drag-handle { cursor: grab; color: var(--text-muted); font-size: 14px; user-select: none; padding: 2px 4px; border-radius: 4px; flex: 0 0 auto; }
.drag-handle:hover { background: var(--list-hover); color: var(--text); }
.drag-handle:active { cursor: grabbing; }
.date-info { flex: 0 0 auto; color: var(--text-muted); font-size: 11px; white-space: nowrap; padding-left: 8px; }
/* min-width:0 lets the title fully yield so the toolbar stays on row 1 as long as it
   fits, and only wraps to row 2 when genuinely out of room (not prematurely). */
.title-input { flex: 1; min-width: 0; border: none; background: none; font-family: var(--font-ui); font-size: 18px; font-weight: 650; letter-spacing: -0.01em; padding: 4px 0; color: var(--text); }
.title-input:focus { outline: none; }
/* flex-shrink:0 keeps each icon full-size (the title yields, not the toolbar);
   max-width:100% caps the group to the row width so its own flex-wrap kicks in and the
   icons wrap across rows instead of overflowing the pane; margin-left:auto right-aligns
   the group, including when it wraps onto its own row. */
.editor-actions { display: flex; gap: 4px; align-items: center; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; margin-left: auto; max-width: 100%; }
.outline-panel {
  position: absolute; top: 48px; right: 12px; z-index: 40;
  width: 240px; max-height: 60%; overflow-y: auto;
  background: var(--editor-bg); border: 1px solid var(--list-border);
  border-radius: 8px; box-shadow: 0 8px 28px rgba(0,0,0,0.18);
  padding: 6px 0; font-size: 13px;
}
.outline-panel.hidden { display: none; }
.outline-item { padding: 4px 12px; cursor: pointer; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.outline-item:hover { background: var(--list-hover); }
.outline-item.active { background: var(--accent); color: var(--accent-ink); }
.outline-item.active.outline-h4, .outline-item.active.outline-h5, .outline-item.active.outline-h6 { color: var(--accent-ink); }
.outline-h1 { font-weight: 600; }
.outline-h2 { padding-left: 22px; }
.outline-h3 { padding-left: 32px; }
.outline-h4 { padding-left: 42px; color: var(--text-muted); }
.outline-h5, .outline-h6 { padding-left: 52px; color: var(--text-muted); }
.outline-empty { padding: 8px 12px; color: var(--text-muted); }
.ac-menu {
  position: fixed; z-index: 60; min-width: 180px; max-width: 320px; max-height: 220px;
  overflow-y: auto; background: var(--editor-bg); border: 1px solid var(--list-border);
  border-radius: 8px; box-shadow: 0 8px 28px rgba(0,0,0,0.18); padding: 4px 0; font-size: 13px;
}
.ac-item { display: flex; align-items: baseline; gap: 10px; padding: 5px 12px; cursor: pointer;
  color: var(--text); }
.ac-label { flex: 1 1 auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ac-hint { flex: 0 0 auto; font-size: 11px; color: var(--text-muted); font-family: var(--mono, monospace); }
.ac-item.active, .ac-item:hover { background: var(--accent); color: var(--accent-ink); }
.ac-item.active .ac-hint, .ac-item:hover .ac-hint { color: rgba(255,255,255,0.7); }
.greplace-modal .modal-input { display: block; width: 100%; margin: 8px 0; }
.greplace-case { display: flex; align-items: center; gap: 6px; font-size: 13px;
  color: var(--text-muted); margin: 4px 0 2px; cursor: pointer; }
.storage-pane .setting-label { display: block; font-weight: 600; margin: 14px 0 6px; }
.storage-path-row { display: flex; align-items: center; gap: 8px; }
.storage-path { flex: 1; overflow-x: auto; white-space: nowrap; background: var(--chip-bg);
  border: 1px solid var(--list-border); border-radius: 6px; padding: 5px 8px; font-size: 12px; }
.storage-path-actions { display: flex; gap: 6px; flex-shrink: 0; }
.storage-path-actions .ghost-btn { padding: 4px 10px; font-size: 12px; }
.backlinks { border-top: 1px solid var(--list-border); padding: 8px 14px; max-height: 22%;
  overflow-y: auto; background: var(--editor-bg); flex-shrink: 0; }
.backlinks.hidden { display: none; }
.backlinks-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-muted); margin-bottom: 5px; }
.backlinks-item { padding: 3px 6px; border-radius: 5px; cursor: pointer; font-size: 13px;
  color: var(--accent-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.backlinks-item:hover { background: var(--list-hover); }
/* history mode: banner + revisions side panel (content shows in the real editor) */
.history-bar { display: none; align-items: center; justify-content: space-between;
  gap: 10px; padding: 7px 14px; background: var(--chip-bg);
  border-bottom: 1px solid var(--list-border); font-size: 13px; color: var(--text); }
.history-bar-actions { display: flex; gap: 8px; }
.history-bar .primary-btn, .history-bar .ghost-btn { padding: 4px 12px; font-size: 12px; }
.history-panel { position: absolute; top: 48px; right: 12px; z-index: 45;
  width: 250px; max-height: 70%; overflow-y: auto; background: var(--editor-bg);
  border: 1px solid var(--list-border); border-radius: 8px;
  box-shadow: 0 8px 28px rgba(0,0,0,0.18); padding: 4px 0; }
.history-panel.hidden { display: none; }
.history-panel-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-muted); padding: 8px 12px 4px; }
.history-item { padding: 7px 12px; cursor: pointer; border-top: 1px solid var(--list-border); }
.history-item:hover { background: var(--list-hover); }
.history-item.active { background: var(--accent); color: var(--accent-ink); }
.history-item.active .history-when, .history-item.active .history-meta { color: var(--accent-ink); }
.history-current { border-bottom: 1px solid var(--list-border); }
.history-current .history-meta { color: var(--accent-text); }
.history-when { font-size: 13px; color: var(--text); }
.history-meta { font-size: 11px; color: var(--text-muted); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }
#editor-pane .icon-btn { color: var(--text-muted); }
#editor-pane .icon-btn:hover { background: var(--list-hover); color: var(--text); }
#editor-pane .icon-btn.active { color: var(--accent-text); }
/* pinned: fill the button so state is obvious (emoji ignore text color) */
#pin-btn.active { background: var(--accent); color: var(--accent-ink); border-radius: 6px; }
#pin-btn.active:hover { background: var(--accent); }
/* Pinned: light pin on the accent fill (higher specificity than the generic
   .icon-btn.active accent color, which would be accent-on-accent = invisible). */
#editor-pane #pin-btn.active { color: var(--accent-ink); }
/* flex-wrap + max-width let the segmented control wrap its buttons within the group
   once the pane is narrower than the three buttons side by side — otherwise the group
   is one unshrinkable flex item that overflows the pane's right edge and gets clipped
   by #editor-pane{overflow:hidden}. Verified with _uitest/resize.mjs down to 640px. */
.viewmode-group { display: flex; flex-wrap: wrap; max-width: 100%; border: 1px solid var(--list-border); border-radius: 6px; overflow: hidden; margin-right: 4px; }
/* Preferences → Experiments → "Edit in rendered Markdown": one view, no switcher. */
.viewmode-group.hidden { display: none; }
.vm-btn { background: none; border: none; padding: 4px 8px; cursor: pointer; color: var(--text-muted); font-size: 12px; }
.vm-btn:hover { background: var(--list-hover); }
.vm-btn.active { background: var(--accent); color: var(--accent-ink); }

#editor-meta { display: flex; align-items: center; gap: 8px; padding: 7px 14px; border-bottom: 1px solid var(--list-border); flex-wrap: wrap; }
/* Chip-styled selects (mock treatment): soft chip fill, no hard border.
   appearance:none or the native select chrome overrides all of it. */
.meta-select { appearance: none; -webkit-appearance: none; border: none; border-radius: 6px; padding: 4px 20px 4px 9px; font-size: 12px; background: var(--chip-bg); color: var(--text);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'><path d='M1 1l3 3 3-3' fill='none' stroke='%23888' stroke-width='1.4' stroke-linecap='round'/></svg>");
  background-repeat: no-repeat; background-position: right 7px center; }

/* Chip select (ui.js chipSelect) — the custom dropdown that replaced the native
   meta-bar <select>s: same chip skin on the button, tag-suggest-style menu. */
.vn-select { position: relative; }
.vn-select-btn {
  display: inline-flex; align-items: center; gap: 6px;
  border: none; border-radius: 6px; padding: 4px 8px 4px 9px;
  font-size: 12px; background: var(--chip-bg); color: var(--text); cursor: pointer; }
.vn-select-btn:hover { background: var(--list-hover); }
.vn-select-btn > svg { color: var(--text-muted); flex: none; }
.vn-select-label { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.vn-select-label svg { color: var(--text-muted); flex: none; }
.vn-select-text { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vn-select-menu {
  position: absolute; top: 100%; left: 0; margin-top: 4px; z-index: 60;
  min-width: 190px; max-width: 300px; max-height: 260px; overflow-y: auto;
  background: var(--modal-bg); border: 1px solid var(--list-border); border-radius: 6px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2); padding: 4px; }
.vn-select-option {
  display: flex; align-items: center; gap: 7px;
  padding: 5px 10px; border-radius: 4px; font-size: 12px; color: var(--text); cursor: pointer; }
.vn-select-option svg { color: var(--text-muted); flex: none; }
.vn-select-option-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vn-select-option-count { color: var(--text-muted); font-size: 11px; }
.vn-select-option.active { background: var(--list-hover); }
.vn-select-option.selected .vn-select-option-label { color: var(--accent-text); font-weight: 600; }
.vn-select-empty { padding: 8px 10px; font-size: 12px; color: var(--text-muted); }
/* hollow dot for the "No status" row (filled dots: .status-active & friends) */
.status-dot.status-none { background: transparent; border: 1.5px solid var(--text-muted); box-sizing: border-box; }
.tag-editor { display: flex; align-items: center; gap: 5px; flex: 1; flex-wrap: wrap; position: relative; }
.tag-suggest { position: absolute; top: 100%; left: 0; margin-top: 4px; min-width: 180px; max-height: 220px; overflow-y: auto; background: var(--modal-bg); border: 1px solid var(--list-border); border-radius: 6px; box-shadow: 0 6px 20px rgba(0,0,0,0.2); z-index: 60; }
/* Rows keep each tag's CUSTOM color (inline, from tagColor in tag-suggest.js),
   so hover/active highlight with a neutral wash — an accent-filled row would
   fight the per-tag colors. Matches the vn-select menu treatment. */
.tag-suggest-item { display: flex; justify-content: space-between; gap: 12px; padding: 6px 10px; cursor: pointer; font-size: 12px; color: var(--text); }
.tag-suggest-item:hover, .tag-suggest-item.active { background: var(--list-hover); }
.tag-suggest-count { color: var(--text-muted); }
/* `display: contents` so the chips are flex items of .tag-editor ITSELF. As a
   nested flex box they wrapped as one unit, so the moment a single chip moved to
   a second line the "+ tag" input was pushed onto a third line of its own. Flowed
   together, a line is spent only when the chips actually fill one. */
.tag-chips { display: contents; }
.tag-chip { background: var(--chip-bg); color: var(--accent-text); border-radius: 10px; padding: 2px 8px; font-size: 11px; display: inline-flex; align-items: center; gap: 4px; }
.tag-chip-x { background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 13px; line-height: 1; padding: 0; }
.tag-chip-x:hover { color: var(--status-dropped); }
/* `flex: 1 1 44px` — an <input>'s intrinsic width is ~170px and a flex item wraps
   on its BASE size, so the input dropped to a line of its own whenever the chip
   line had less than that left, however little room "+ tag" actually needs.
   Basing it at 44px (the placeholder measures ~32px, so it is never clipped)
   lets it join any line with room and grow into whatever is left over. */
.tag-input { border: none; background: none; font-size: 12px; color: var(--text); flex: 1 1 44px; min-width: 44px; }
.tag-input:focus { outline: none; }

.trash-bar { display: none; align-items: center; gap: 10px; padding: 8px 14px; background: rgba(219, 40, 40, 0.08); color: var(--text-muted); font-size: 12px; border-bottom: 1px solid var(--list-border); }

#editor-body { flex: 1; display: grid; overflow: hidden; position: relative; }
#editor-body.mode-side { grid-template-columns: 1fr 1fr; }
#editor-body.mode-editor, #editor-body.mode-live { grid-template-columns: 1fr 0; }
/* read-only Live preview: the rendered preview pane full-width (editor hidden) */
#editor-body.mode-preview { grid-template-columns: 0 1fr; }
/* Explicit grid placement for all three children. #preview-find-host (the find-in-
   preview widget's overlay, frontend/preview-find.js) shares the preview's column/row
   so it always tracks the preview pane's width in mode-side (50%) AND mode-preview
   (100%), and collapses to 0 in editor-only modes. Without explicit placement the
   auto-placer would push #preview to a second row once a third child is added. */
#editor-body > .cm-host { grid-column: 1; grid-row: 1; }
#editor-body > #preview { grid-column: 2; grid-row: 1; }
#preview-find-host {
  grid-column: 2; grid-row: 1; align-self: start; justify-self: stretch;
  position: relative; overflow: hidden; pointer-events: none; z-index: 30; }
.cm-host { overflow: hidden; border-right: 1px solid var(--list-border); }
#editor-body.mode-editor .cm-host, #editor-body.mode-live .cm-host { border-right: none; }
/* No padding on the editor element: the CM6 view drops the shim-added `CodeMirror`
   class the moment it gains focus (it rewrites className to add `cm-focused`), so a
   `.CodeMirror`-scoped padding is present before focus and gone after — a 16px width
   swing that shifted .cm-content ~8px on click. Insets live on .cm-content (below),
   which CM6 keeps stable. */
.cm-host .CodeMirror { height: 100%; font-family: var(--editor-font, var(--font-mono)); font-size: inherit; background: var(--editor-bg); color: var(--text); padding: 0; }
body.theme-dark .cm-host .CodeMirror { color: var(--text); }
body.theme-dark .CodeMirror-cursor { border-left-color: var(--text); }
body.theme-dark .CodeMirror-selected { background: var(--selection) !important; }
body.theme-dark .CodeMirror-gutters { background: var(--editor-bg); border-right: 1px solid var(--list-border); }
body.theme-dark .CodeMirror-linenumber { color: var(--text-muted); }

/* CodeMirror 6 (cm6-bundle) — the new engine. Mirrors the CM5 rules above using
   the same variables, so light/dark and the editor chrome match. The adapter adds
   the .CodeMirror class to .cm-editor, so container rules above still apply. */
.cm-host .cm-editor { height: 100%; background: var(--editor-bg); }
.cm-host .cm-editor.cm-focused { outline: none; }
/* File drag-and-drop target cue (see handleEditorDragOver in app.js). */
.cm-host .cm-editor.cm-drop-active { outline: 2px dashed var(--accent); outline-offset: -3px; background: var(--selection); }
/* ONE content geometry across all three view modes (2026-07, matched to the
   Inkdrop demo's default): FULL-WIDTH, left-aligned, with the SAME fixed insets
   on the editor's .cm-content and the preview's .markdown-body — so toggling
   Editor / Live / Side-by-side never shifts the text. Inkdrop centers into a
   ~740px column only under an opt-in "limit max width" preference; the default
   (and ours) is uncentered. Insets stay on .cm-content (not .CodeMirror) so they
   don't swing when CM6 drops the shim class on focus; scrollbar-gutter: stable
   keeps the right edge from reflowing when the vertical scrollbar toggles. The
   ⌘K preview pane (.velocity-preview-pane, later rule) keeps tighter insets. */
/* --editor-font resolves to --font-mono (default) or --font-ui (the "sans" editor-font
   preference); set on documentElement by applyEditorFont() in app.js. Falls back to
   --font-mono so a build without the pref still renders monospace. */
.cm-host .cm-scroller { font-family: var(--editor-font, var(--font-mono)); font-size: inherit; line-height: 1.5; scrollbar-gutter: stable; }
.cm-host .cm-content { padding: 18px 22px 32px; color: var(--text); caret-color: var(--text); }
.cm-host .cm-line { padding: 0; }
.cm-host .cm-gutters { background: var(--editor-bg); border: none; color: var(--text-muted); }
.cm-host .cm-activeLineGutter, .cm-host .cm-activeLine { background: transparent; }
.cm-cursor, .cm-dropCursor { border-left-color: var(--text); }
.cm-selectionBackground, .cm-focused .cm-selectionBackground,
.cm-content ::selection { background: var(--selection) !important; }
.cm-host .cm-placeholder { color: var(--text-muted); }
/* in-editor find/replace panel (CM6 @codemirror/search) — custom VSCode-style
   panel built in frontend/editor/cm6.js (SearchPanel): a live match counter, and
   compact icon option-toggles (Aa / ab / .*) tucked inside the find field. */
.cm-panels { background: var(--editor-bg); color: var(--text); border-bottom: 1px solid var(--list-border); }
/* CM6's base theme parks the find/replace panel at z-index:300, which covers the
   app's own context menus (200), AI menu, and outline/history panels. It only needs
   to sit above the editor text (z-index:0) — pin it low. Two-class selector to beat
   CM6's single-class `.cm-panels` rule regardless of stylesheet order. */
.cm-editor .cm-panels { z-index: 5; }
.cm-panels.cm-panels-bottom { border-top: 1px solid var(--list-border); border-bottom: none; }
/* ============================================================================
   Find / replace — a 1:1 recreation of VS Code's find widget: a floating rounded
   panel that hovers over the editor's top-right, with VS Code's own colors,
   @vscode/codicons, borderless icon buttons, blue "active" toggles, and the left
   "Toggle Replace" chevron. Colors are VS Code's exact Dark/Light Modern token
   values, hardcoded so the widget looks like VS Code regardless of the app theme.
   ========================================================================= */

/* Float the top panel over the editor (VS Code positions the widget, it isn't a
   docked bar). The container is click-through; only the widget itself catches events.
   The find-in-preview widget (#preview-find-host, frontend/preview-find.js) reuses the
   exact `.cm-panel.cm-search` rules below — one source of truth styles both widgets. */
.cm-editor .cm-panels.cm-panels-top {
  position: absolute; top: 0; left: 0; right: 0; z-index: 5;
  background: transparent; border: none; pointer-events: none; }

/* Make each pane that hosts the widget a size query container, so the widget can drop
   its least-essential controls (VS Code's reduced/narrow/collapsed-find-widget) as the
   pane narrows — keeping its min-content under the pane width so it can never overflow
   and clip (the side-by-side / narrow-window bug). */
.cm-host, #preview-find-host { container-type: inline-size; }

.cm-panel.cm-search {
  /* VS Code Light Modern tokens */
  --vs-bg: #f8f8f8; --vs-border: #e5e5e5; --vs-shadow: rgba(0,0,0,.16);
  --vs-input-bg: #ffffff; --vs-input-border: #cecece; --vs-fg: #3b3b3b;
  --vs-icon: #3b3b3b; --vs-focus: #005fb8;
  --vs-opt-bg: #bed6ed; --vs-opt-border: #005fb8; --vs-opt-fg: #000000;
  --vs-hover: rgba(184,184,184,.31); --vs-error: #f85149;

  pointer-events: auto; position: relative;
  display: flex; align-items: flex-start; gap: 0;
  /* Right-anchored, hangs from the top edge (VS Code). max-width clamps to the pane
     (minus an 18px right gap + ~10px breathing room) so it never spills leftward. */
  width: fit-content; max-width: min(419px, calc(100% - 28px));
  margin: 0 18px 0 auto; padding: 4px;
  font-size: 13px; line-height: 1;
  color: var(--vs-fg); background: var(--vs-bg);
  border: 1px solid var(--vs-border); border-top: none; border-radius: 0 0 4px 4px;
  box-shadow: 0 2px 8px var(--vs-shadow);
  animation: cm-search-in .15s ease-out; }
/* The preview widget hides via the `hidden` attribute; the base rule's `display:flex`
   (higher specificity than the UA `[hidden]{display:none}`) would otherwise keep it
   visible — so the X button / Esc / note-switch could never actually close it. */
.cm-panel.cm-search[hidden] { display: none; }
/* Subtle drop-in on open. A full VS Code translateY(-100%) slide would be clipped by
   the host's overflow:hidden (the horizontal clip guard), so keep it within bounds. */
@keyframes cm-search-in { from { opacity: 0; transform: translateY(-8px); } }
@media (prefers-reduced-motion: reduce) { .cm-panel.cm-search { animation: none; } }

body.theme-dark .cm-panel.cm-search {
  --vs-bg: #202020; --vs-border: #313131; --vs-shadow: rgba(0,0,0,.36);
  --vs-input-bg: #313131; --vs-input-border: #3c3c3c; --vs-fg: #cccccc;
  --vs-icon: #cccccc; --vs-focus: #0078d4;
  --vs-opt-bg: rgba(36,137,219,.51); --vs-opt-border: #2488db; --vs-opt-fg: #ffffff;
  --vs-hover: rgba(90,93,94,.31); --vs-error: #f85149; }

.cm-panel.cm-search input, .cm-panel.cm-search button {
  font-size: 13px; color: inherit; vertical-align: middle; margin: 0; box-sizing: border-box; }
/* reserve space at the right for the absolutely-positioned close (VS Code does this
   with a 25px right margin on the parts) so the ↑ ↓ actions never slide under it */
.cm-panel.cm-search .cm-search-fields { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; margin-right: 24px; }
.cm-panel.cm-search .cm-search-row { display: flex; align-items: center; gap: 0; }

/* left "Toggle Replace" chevron — a tall button spanning both rows (VS Code) */
.cm-panel.cm-search .cm-search-toggle {
  flex: none; align-self: stretch; width: 20px; margin-right: 4px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; border-radius: 3px; color: var(--vs-icon); cursor: pointer; }
.cm-panel.cm-search .cm-search-toggle:hover { background: var(--vs-hover); }
.cm-panel.cm-search.cm-replace-collapsed .cm-replace-part { display: none; }

/* the text field + its inline toggles share one bordered input shell. A low min-width
   lets it (and the whole widget) shrink in a narrow pane instead of forcing an overflow;
   the inline toggles keep it from collapsing to nothing. */
.cm-panel.cm-search .cm-search-input {
  position: relative; display: flex; align-items: center;
  flex: 1 1 220px; min-width: 92px; max-width: 320px;
  background: var(--vs-input-bg); border: 1px solid var(--vs-input-border); border-radius: 2px; }
.cm-panel.cm-search .cm-search-input:focus-within {
  border-color: var(--vs-focus); outline: 1px solid var(--vs-focus); outline-offset: -1px; }
.cm-panel.cm-search .cm-textfield {
  flex: 1; min-width: 0; background: transparent; color: var(--vs-fg);
  border: none; outline: none; padding: 2px 6px; height: 24px; }
.cm-panel.cm-search .cm-textfield::placeholder { color: var(--vs-fg); opacity: .5; }
.cm-panel.cm-search .cm-search-input:has(.cm-search-toggles) .cm-textfield { padding-right: 2px; }

/* inline case / word / regex codicon toggles — blue "active" state (VS Code) */
.cm-panel.cm-search .cm-search-toggles { display: flex; align-items: center; gap: 2px; padding: 0 2px; }
.cm-panel.cm-search .cm-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; padding: 2px; border: 1px solid transparent; border-radius: 3px;
  background: transparent; color: var(--vs-icon); cursor: pointer; }
.cm-panel.cm-search .cm-toggle svg { display: block; width: 16px; height: 16px; }
.cm-panel.cm-search .cm-toggle:hover { background: var(--vs-hover); }
.cm-panel.cm-search .cm-toggle[aria-pressed="true"] {
  background: var(--vs-opt-bg); border-color: var(--vs-opt-border); color: var(--vs-opt-fg); }

/* live "N of M" / "No results" counter beside the find field */
.cm-panel.cm-search .cm-search-count {
  min-width: 52px; padding: 0 4px 0 8px; color: var(--vs-fg);
  font-variant-numeric: tabular-nums; white-space: nowrap; }
.cm-panel.cm-search .cm-search-count-none { color: var(--vs-error); }

/* Responsive collapse (VS Code reduced-/collapsed-find-widget): as the hosting pane
   narrows, shed the least-essential controls so the widget's min-content stays below
   the pane width and never clips. Enter / Shift-Enter still navigate when the arrow
   buttons are hidden. Keyed on the pane query container above. */
@container (max-width: 360px) {
  .cm-panel.cm-search .cm-search-count { display: none; }
}
@container (max-width: 280px) {
  .cm-panel.cm-search .cm-button[name="prev"],
  .cm-panel.cm-search .cm-button[name="next"] { display: none; }
}

/* prev / next / replace / replace-all / close — borderless codicon buttons,
   transparent with a subtle hover background (VS Code's `.find-widget .button`). */
.cm-panel.cm-search .cm-button {
  display: inline-flex; align-items: center; justify-content: center;
  flex: none; width: 22px; height: 22px; padding: 3px; margin-left: 3px;
  background: none; border: none; border-radius: 5px; background-image: none;
  color: var(--vs-icon); cursor: pointer; }
.cm-panel.cm-search .cm-button svg { display: block; width: 16px; height: 16px; }
.cm-panel.cm-search .cm-button:hover { background: var(--vs-hover); }
/* close sits absolutely at the widget's top-right (VS Code). Higher specificity than
   CM6's own `.cm-panel.cm-search [name=close]` so our offsets win. Both widgets. */
.cm-editor .cm-panel.cm-search button[name="close"],
#preview-find-host .cm-panel.cm-search button[name="close"] {
  position: absolute; top: 4px; right: 4px; margin: 0; align-self: auto; }

/* VS Code find-match colors: editor.findMatchHighlightBackground for every match,
   editor.findMatchBackground for the current one. The `.cm-searchMatch*` rules (CM6
   editor) and the `::highlight()` rules (preview widget, frontend/preview-find.js) are
   kept as SEPARATE rules on purpose: a webview without the CSS Custom Highlight API
   drops only the ::highlight rules, never the editor's match styling. */
.cm-searchMatch { background: #ea5c0055; }
.cm-searchMatch-selected { background: #a8ac94; }
body.theme-dark .cm-searchMatch-selected { background: #515c6a; }
::highlight(preview-find) { background-color: #ea5c0055; }
::highlight(preview-find-current) { background-color: #a8ac94; }
body.theme-dark ::highlight(preview-find-current) { background-color: #515c6a; }
/* Live Preview (decoration WYSIWYG) — frontend/editor/live-preview.js */
.cm-host .cm-lp-strong { font-weight: 700; }
.cm-host .cm-lp-em { font-style: italic; }
.cm-host .cm-lp-strike { text-decoration: line-through; color: var(--text-muted); }
.cm-host .cm-lp-code { font-family: var(--font-mono); background: var(--chip-bg); border-radius: 4px; padding: 0 3px; }
.cm-host .cm-line.cm-lp-h1 { font-size: 1.6em; font-weight: 700; line-height: 1.3; }
.cm-host .cm-line.cm-lp-h2 { font-size: 1.35em; font-weight: 700; line-height: 1.3; }
.cm-host .cm-line.cm-lp-h3 { font-size: 1.2em; font-weight: 700; }
.cm-host .cm-line.cm-lp-h4 { font-size: 1.08em; font-weight: 700; }
.cm-host .cm-line.cm-lp-h5, .cm-host .cm-line.cm-lp-h6 { font-weight: 700; color: var(--text-muted); }
.cm-host input.cm-lp-task { vertical-align: middle; margin: 0 4px 0 0; cursor: pointer; accent-color: var(--accent-text); }
.cm-host .cm-lp-link { color: var(--accent-text); text-decoration: none; }
.cm-host .cm-line.cm-lp-quote { border-left: 3px solid var(--list-border); padding-left: 10px; color: var(--text-muted); }
/* Footnotes. The reference raises like the preview's <sup>; the definition line
   is set back the way the preview's footnotes section is. Tokens only — see
   THEME.md; a hex here would be a bug. */
.cm-host .cm-lp-footnote-ref { font-size: 0.8em; vertical-align: super; line-height: 0; color: var(--accent); }
.cm-host .cm-line.cm-lp-footnote-def { color: var(--text-muted); font-size: 0.92em; }
.cm-host .cm-lp-bullet { color: var(--text-muted); }
.cm-host .cm-lp-hr { display: inline-block; width: 100%; border-top: 1px solid var(--list-border); vertical-align: middle; }
.cm-host img.cm-lp-img { max-width: 100%; display: block; margin: 4px 0; border-radius: 6px; }
/* Inline image widget with a drag-resize handle (ImageWidget). The wrapper hugs
   the image so the handle sits on its bottom-right corner. */
.cm-host .cm-lp-img-wrap { position: relative; display: inline-block; max-width: 100%; }
.cm-host .cm-lp-img-wrap .cm-lp-img { margin: 4px 0; }
.cm-host .cm-lp-img-handle {
  position: absolute; right: 2px; bottom: 6px; width: 14px; height: 14px;
  border-right: 3px solid var(--accent); border-bottom: 3px solid var(--accent);
  border-radius: 0 0 4px 0; cursor: nwse-resize;
  opacity: 0; transition: opacity 0.12s ease; }
.cm-host .cm-lp-img-wrap:hover .cm-lp-img-handle,
.cm-host .cm-lp-img-wrap.resizing .cm-lp-img-handle { opacity: 0.9; }
.cm-host .cm-lp-img-size {
  position: absolute; right: 6px; top: 10px; padding: 2px 7px;
  border-radius: 4px; background: rgba(0, 0, 0, 0.65); color: #fff;
  font-size: 11px; font-family: var(--font-mono); pointer-events: none; }
.cm-host .cm-lp-img-size:empty { display: none; }
/* GFM tables — rendered inline, matching the preview pane */
.cm-host .cm-lp-table { margin: 6px 0; overflow-x: auto; }
.cm-host .cm-lp-table table { border-collapse: collapse; }
.cm-host .cm-lp-table th, .cm-host .cm-lp-table td { border: 1px solid var(--list-border); padding: 4px 10px; }
.cm-host .cm-lp-table th { background: var(--chip-bg); font-weight: 700; }
/* KaTeX math */
.cm-host .cm-lp-math-display { display: block; text-align: center; margin: 6px 0; }
/* alert callouts (> [!NOTE] …) — left bar + tint per type, matches the preview */
.cm-host .cm-line.cm-lp-alert { padding-left: 12px; border-left: 3px solid var(--list-border); }
.cm-host .cm-line.cm-lp-alert-note { border-left-color: #2185d0; background: rgba(33, 133, 208, 0.06); }
.cm-host .cm-line.cm-lp-alert-tip { border-left-color: #21ba45; background: rgba(33, 186, 69, 0.06); }
.cm-host .cm-line.cm-lp-alert-important { border-left-color: #a333c8; background: rgba(163, 51, 200, 0.06); }
.cm-host .cm-line.cm-lp-alert-warning { border-left-color: #fbbd08; background: rgba(251, 189, 8, 0.08); }
.cm-host .cm-line.cm-lp-alert-caution { border-left-color: #db2828; background: rgba(219, 40, 40, 0.06); }
.cm-host .cm-lp-alert-title { font-weight: 700; }
/* fenced code blocks — a monospace box; ``` fences dimmed */
.cm-host .cm-line.cm-lp-codeblock { background: var(--chip-bg); font-family: var(--font-mono); }
.cm-host .cm-lp-fence { color: var(--text-muted); opacity: 0.6; }
.cm-host .cm-lp-fence-info { color: var(--text-muted); font-style: italic; }
/* copy button on the opening fence line (live-preview CopyCodeWidget) —
   faint until the line is hovered, so the editing surface stays quiet */
.cm-host .cm-lp-copy {
  float: right; margin-left: 8px; padding: 0 6px;
  border: none; border-radius: 4px; background: none;
  color: var(--text-muted); font-size: 12px; line-height: 1.5; cursor: pointer;
  opacity: 0.25; transition: opacity 0.12s ease; }
.cm-host .cm-line:hover .cm-lp-copy, .cm-host .cm-lp-copy:focus-visible { opacity: 1; }
.cm-host .cm-lp-copy:hover { background: var(--list-hover); color: var(--text); }
.cm-host .cm-lp-copy.copied { opacity: 1; color: var(--status-active); }
/* ```math fenced block rendered as KaTeX display (MathBlockWidget). The raw-LaTeX
   fallback (before KaTeX lazy-loads) shows as readable mono text. */
.cm-host .cm-lp-math-block {
  padding: 8px 12px; margin: 4px 0; border-radius: 6px;
  background: var(--chip-bg); text-align: center; overflow-x: auto;
  font-family: var(--font-mono); white-space: pre-wrap; }
.cm-host .cm-lp-math-block .katex-display { margin: 0; }

/* Code-token colors inside fenced blocks (cm6.js codeHighlight → tok-* classes).
   Same GitHub light/dark palettes as the preview's vendored hljs themes
   (vendor/highlight/github.css / github-dark.css), so editor and preview code
   look identical — and tokens stay readable on theme-dark, where CM6's
   defaultHighlightStyle inline colors were near-invisible. */
.cm-editor .tok-kw { color: #d73a49; }
.cm-editor .tok-str { color: #032f62; }
.cm-editor .tok-const { color: #005cc5; }
.cm-editor .tok-fn { color: #6f42c1; }
.cm-editor .tok-type { color: #e36209; }
.cm-editor .tok-prop { color: #005cc5; }
.cm-editor .tok-comment { color: #6a737d; font-style: italic; }
.cm-editor .tok-meta { color: #22863a; }
body.theme-dark .cm-editor .tok-kw { color: #ff7b72; }
body.theme-dark .cm-editor .tok-str { color: #a5d6ff; }
body.theme-dark .cm-editor .tok-const { color: #79c0ff; }
body.theme-dark .cm-editor .tok-fn { color: #d2a8ff; }
body.theme-dark .cm-editor .tok-type { color: #ffa657; }
body.theme-dark .cm-editor .tok-prop { color: #79c0ff; }
body.theme-dark .cm-editor .tok-comment { color: #8b949e; }
body.theme-dark .cm-editor .tok-meta { color: #7ee787; }

/* Markdown structural markers (`##`, `**`, backticks, `>`, list bullets, link
   brackets — cm6.js maps tags.processingInstruction here). These are note syntax,
   not code tokens: they must follow the SELECTED theme/palette, so they take the
   achromatic muted text token instead of a fixed code-palette color (they used to
   share tok-meta and rendered green in every theme). One rule, both themes — the
   token already flips with body.theme-dark / data-palette. */
.cm-editor .tok-md-mark { color: var(--text-muted); }

/* search-palette preview instance on CM6 */
.velocity-preview-pane .cm-editor { height: 100%; background: transparent; font-size: 13px; color: var(--text); }
.velocity-preview-pane .cm-content { padding: 8px 10px; }

/* Full-width, same insets as the editor's .cm-content (see the view-mode
   alignment note above) — no centered column, matching Inkdrop's default.
   line-height 1.5 = the editor's (and Inkdrop's --mde-preview-line-height),
   so side-by-side lines don't drift apart. */
.markdown-body { padding: 18px 22px 32px; overflow-y: auto; line-height: 1.5; color: var(--text); }
/* First rendered element starts AT the pane padding (Inkdrop's h*:first-child /
   p:first-child rules) — its own top margin pushed the preview's first line
   ~15px below the editor's, the "slightly off" mode-switch jump. */
.markdown-body > :first-child { margin-top: 0; }
.markdown-body h1, .markdown-body h2, .markdown-body h3 { margin: 0.6em 0 0.3em; line-height: 1.3; }
.markdown-body h1 { font-weight: 700; letter-spacing: -0.02em; font-size: 1.7em; border-bottom: 1px solid var(--list-border); padding-bottom: 0.2em; }
.markdown-body h2 { font-size: 1.3em; }
.markdown-body p { margin: 0.5em 0; }
.markdown-body code { background: var(--chip-bg); padding: 1px 5px; border-radius: 4px; font-family: var(--font-mono); font-size: 0.9em; }
.markdown-body pre { background: var(--chip-bg); padding: 12px 14px; border-radius: 8px; overflow-x: auto; }
.markdown-body pre code { background: none; padding: 0; }

/* Fenced-code chrome (markdown.js decorateCodeBlocks): hover copy button on
   every block; ```js title=src/app.js adds a filename header + language badge. */
.md-codeblock { position: relative; }
.md-codeblock-copy {
  position: absolute; top: 7px; right: 7px; z-index: 1;
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0;
  border: 1px solid var(--list-border); border-radius: 6px;
  background: var(--editor-bg); color: var(--text-muted); cursor: pointer;
  opacity: 0; transition: opacity 0.12s ease; }
.md-codeblock:hover .md-codeblock-copy, .md-codeblock-copy:focus-visible { opacity: 1; }
.md-codeblock-copy:hover { color: var(--text); background: var(--list-hover); }
.md-codeblock-copy svg { display: block; }
.md-codeblock-copy .md-copy-check { display: none; }
.md-codeblock-copy.copied { opacity: 1; color: var(--status-active); }
.md-codeblock-copy.copied .md-copy-icon { display: none; }
.md-codeblock-copy.copied .md-copy-check { display: block; }
/* the header sits inside the block's 1em top margin slot; the pre docks under it */
.md-codeblock-header {
  display: flex; align-items: center; gap: 8px;
  margin: 1em 0 0; padding: 6px 12px; padding-right: 40px; /* room for the copy button */
  background: var(--chip-bg); border-bottom: 1px solid var(--list-border);
  border-radius: 8px 8px 0 0; }
.md-codeblock-titled pre { margin-top: 0; border-top-left-radius: 0; border-top-right-radius: 0; }
/* in titled blocks the button lives inside the (~29px) header row — shrink to fit */
.md-codeblock-titled .md-codeblock-copy { top: 4px; right: 6px; width: 22px; height: 22px; border: none; background: none; }
.md-codeblock-titled .md-codeblock-copy:hover { background: var(--list-hover); }
.md-codeblock-file {
  display: inline-flex; align-items: center; gap: 6px; min-width: 0;
  font-family: var(--font-mono); font-size: 12px; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.md-codeblock-file svg { flex: none; opacity: 0.6; }
.md-codeblock-lang {
  margin-left: auto; flex: none;
  font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-muted); }
.markdown-body blockquote { margin: 0.5em 0; padding: 0.2em 1em; border-left: 3px solid var(--list-border); color: var(--text-muted); }
.markdown-body ul, .markdown-body ol { padding-left: 1.4em; }
.markdown-body a { color: var(--accent-text); text-decoration: none; }
.markdown-body a:hover { text-decoration: underline; }
.markdown-body hr { border: none; border-top: 1px solid var(--list-border); margin: 1em 0; }

.status-bar { display: flex; justify-content: space-between; padding: 5px 14px; border-top: 1px solid var(--list-border); font-size: 11px; color: var(--text-muted); }

/* formatting toolbar */
.editor-toolbar { display: flex; align-items: center; gap: 5px 0; padding: 5px 10px; border-bottom: 1px solid var(--list-border); flex-wrap: wrap; }
/* a section of buttons that stays together — wraps to the next line as a unit.
   The separator is a left border on the group, so it travels with the section
   when it wraps instead of dangling at a line end like a standalone divider. */
/* Group and button spacing is tight on purpose: 18 buttons at the old metrics
   needed ~620px of editor pane to stay on one line, so a normally-sized window
   spent a second row on the last three icons. The saved ~90px moves that
   threshold to ~520px. Wrapping is still what happens below it — nothing here
   clips or scrolls the toolbar away. */
.tb-group { display: flex; align-items: center; gap: 1px; flex: 0 0 auto; margin-left: 4px; padding-left: 5px; border-left: 1px solid var(--list-border); }
.tb-group:first-child { margin-left: 0; padding-left: 0; border-left: none; }
.tb-btn { background: none; border: none; cursor: pointer; color: var(--text-muted); font-size: 13px; min-width: 26px; height: 26px; border-radius: 5px; padding: 0 4px; }
.tb-btn:hover { background: var(--list-hover); color: var(--text); }
/* icon variant: center the stroke SVG (link / image / attach / alert) so it
   aligns with the typographic buttons and inherits the muted → text hover tone */
.tb-btn.tb-icon { display: inline-flex; align-items: center; justify-content: center; }

/* single-note window (opened via ?note=ID) */
body.single-note #app { grid-template-columns: 1fr; }
body.single-note #sidebar, body.single-note #note-list-pane, body.single-note #popout-btn,
body.single-note #resizer-1, body.single-note #resizer-2 { display: none; }

.empty-state { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 14px; color: var(--text-muted); }

/* ---- settings modal --------------------------------------------------------- */
.overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.4); display: flex; align-items: center; justify-content: center; z-index: 100; }
.overlay.hidden { display: none; }

/* Launch login gate — a small Evernote-style sign-in card shown over the whole
   app until a credential is stored. Sits above everything (settings = z 100). */
.login-gate { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; background: var(--editor-bg); }
.login-card { width: 320px; max-width: calc(100vw - 40px); background: var(--modal-bg); border: 1px solid var(--list-border); border-radius: 14px; box-shadow: 0 18px 50px rgba(0, 0, 0, 0.18); padding: 32px 28px; text-align: center; }
/* The first thing a new user sees was a stand-in 📝 emoji, not this app's mark.
   `icon.b4f9d47d.svg` IS the app icon — stage-frontend.sh copies the one canonical file
   (src-tauri/icons/source/icon.svg) in at build time, the same file the favicon
   and the OAuth landing page use, so there is no second copy to drift.
   The centring is part of the fix: .femoji is `display: block` for the sidebar's
   flex rows, and a block image ignores the card's text-align, which is why the
   old glyph sat off to the left of a centred card. */
.login-logo { font-size: 34px; line-height: 1; }
.login-logo .femoji { margin: 0 auto; }
.login-mark { display: inline-block; width: 56px; height: 56px; }
.login-title { font-size: 22px; font-weight: 700; margin: 10px 0 4px; color: var(--text); }
.login-sub { font-size: 13px; color: var(--text-muted); margin: 0 0 22px; line-height: 1.45; }
.login-btn { display: inline-flex; align-items: center; justify-content: center; gap: 10px; width: 100%; padding: 10px 14px; font-size: 14px; font-weight: 600; color: var(--text); background: var(--input-bg); border: 1px solid var(--list-border); border-radius: 8px; cursor: pointer; transition: border-color .15s, background .15s; }
.login-btn:hover:not(:disabled) { border-color: var(--accent-text); background: var(--list-hover); }
.login-btn:disabled { opacity: 0.6; cursor: default; }
.login-btn svg { width: 18px; height: 18px; flex: 0 0 18px; }
/* Sign in with Apple — Apple's HIG mandates the black (light mode) / white (dark
   mode) button with logo+label in the opposite colour; brand colours, not theme
   tokens (see THEME.md "Deliberately NOT themed"). */
.login-btn-apple { margin-top: 8px; background: #000; color: #fff; border-color: #000; }
.login-btn-apple:hover:not(:disabled) { background: #1c1c1e; border-color: #1c1c1e; }
body.theme-dark .login-btn-apple { background: #fff; color: #000; border-color: #fff; }
body.theme-dark .login-btn-apple:hover:not(:disabled) { background: #e8e8ed; border-color: #e8e8ed; }
.login-err { min-height: 16px; margin: 12px 0 0; font-size: 12px; color: var(--status-dropped); }
.login-foot { margin: 18px 0 0; font-size: 11px; color: var(--text-muted); }
/* Secondary text actions on the login/lock card (recheck, switch account). */
.login-link { display: block; width: 100%; margin: 10px 0 0; padding: 4px; background: none; border: none; cursor: pointer;
  font-size: 12px; color: var(--text-muted); text-decoration: underline; text-underline-offset: 2px; }
.login-link:hover { color: var(--accent-text); }

.settings-modal { width: 640px; max-width: 90vw; height: 560px; max-height: 88vh; background: var(--modal-bg); border-radius: 12px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); display: flex; flex-direction: column; overflow: hidden; }
.settings-titlebar { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid var(--list-border); font-weight: 600; }
.settings-titlebar .icon-btn { color: var(--text-muted); }
.settings-titlebar .icon-btn:hover { background: var(--list-hover); color: var(--text); }
.settings-body { display: grid; grid-template-columns: 160px 1fr; flex: 1; overflow: hidden; }
.settings-nav { background: var(--list-bg); padding: 10px 8px; border-right: 1px solid var(--list-border); overflow-y: auto; min-height: 0; }
.settings-nav-group { margin-top: 8px; padding: 10px 12px 4px; border-top: 1px solid var(--list-border); font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; color: var(--text-muted); }
.settings-nav-group:first-child { margin-top: 0; padding-top: 2px; border-top: none; }
.settings-nav-item { padding: 8px 12px; border-radius: 6px; cursor: pointer; color: var(--text); display: flex; align-items: center; gap: 8px; }
.settings-nav-item .lucide { color: var(--text-muted); }
.settings-nav-item.active .lucide, .settings-nav-item:hover .lucide { color: currentColor; }
.settings-nav-item:hover { background: var(--list-hover); }
.settings-nav-item.active { background: var(--accent); color: var(--accent-ink); }
.settings-pane { padding: 18px 22px; overflow-y: auto; min-height: 0; }
.setting-row { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid var(--list-border); }
.setting-label { color: var(--text); font-size: 13px; }
.setting-input { border: 1px solid var(--list-border); border-radius: 6px; padding: 4px 8px; background: var(--input-bg); color: var(--text); min-width: 120px; }
/* Theme segmented control + palette swatches (Preferences → Appearance). */
.seg-control { display: inline-flex; gap: 2px; padding: 2px; background: var(--chip-bg); border-radius: 8px; }
.seg-control .seg-btn { border: none; background: none; color: var(--text-muted); font: inherit; font-size: 12px;
  padding: 4px 12px; border-radius: 6px; cursor: pointer; }
.seg-control .seg-btn:hover { color: var(--text); }
.seg-control .seg-btn[aria-checked="true"] { background: var(--accent); color: var(--accent-ink); font-weight: 600; }
.seg-control .seg-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.palette-control { display: inline-flex; gap: 6px; }
.palette-swatch { display: inline-flex; align-items: center; gap: 7px; cursor: pointer; font: inherit; font-size: 12px;
  color: var(--text-muted); background: var(--input-bg); border: 1px solid var(--list-border);
  border-radius: 8px; padding: 4px 10px 4px 7px; }
/* Split chip: the palette's light accent on one half, its dark accent on the
   other, so the swatch reads correctly in whichever theme is showing. */
.palette-swatch::before { content: ""; width: 14px; height: 14px; border-radius: 4px; flex: none;
  background: linear-gradient(135deg, var(--sw-light) 0 50%, var(--sw-dark) 50% 100%);
  box-shadow: inset 0 0 0 1px rgba(128, 128, 128, 0.3); }
.palette-swatch:hover { border-color: var(--accent); }
.palette-swatch[aria-checked="true"] { border-color: var(--accent); color: var(--text); font-weight: 600;
  box-shadow: inset 0 0 0 1px var(--accent); }
.palette-swatch:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.setting-note { color: var(--text-muted); font-size: 12px; margin-top: 16px; line-height: 1.5; }
.setting-value { color: var(--text-muted); font-size: 12px; }

/* Membership (Preferences → Account). The note meter is drawn ONLY on the
   capped plan — see settings.js membershipSection. Its fill uses the semantic
   status tokens rather than the accent: "nearly full" is a state, not a brand
   moment, and painting a warning in the accent makes it look like a feature.
   Every colour here is a theme.css token; no literals (THEME.md). */
.member-term { font-size: 10.5px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
  padding: 2px 7px; border-radius: 999px; background: var(--chip-bg); color: var(--text-muted); white-space: nowrap; }
.member-quota { display: flex; flex-direction: column; gap: 6px; padding: 12px 0; border-bottom: 1px solid var(--list-border); }
.member-quota-top { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.member-quota-count { font-size: 12px; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.member-track { height: 6px; border-radius: 999px; background: var(--chip-bg); overflow: hidden; }
.member-fill { height: 100%; border-radius: 999px; background: var(--accent); transition: width .2s ease; }
.member-fill.warn { background: var(--status-onHold); }
.member-fill.full { background: var(--status-dropped); }
.member-quota-msg { font-size: 12px; color: var(--text-muted); }
.member-quota-msg.warn { color: var(--status-onHold); }
.member-quota-msg.full { color: var(--status-dropped); }
@media (prefers-reduced-motion: reduce) { .member-fill { transition: none; } }
.sync-conflict { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--list-border); }
.sync-conflict-title { flex: 1; font-size: 13px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sync-conflict-acts { display: flex; gap: 6px; }

/* ---- Sync pane — role-centric state machine (Local / Cloud / Peer / Server) ---- */
/* One status header per role. Server is deliberately a DISTINCT tone (violet), never
   the green "synced" — a server is the source, not caught up to anything. */
.role-status { display: flex; align-items: center; gap: 11px; padding: 13px 15px; border-radius: 11px; margin-bottom: 4px; }
.role-status .rs-big { font-weight: 700; font-size: 15px; color: var(--text); }
.role-status .rs-small { font-size: 12.5px; color: var(--text-muted); margin-top: 1px; }
.role-status .rs-dot { width: 10px; height: 10px; border-radius: 50%; flex: 0 0 auto; }
.role-status.local  { background: var(--list-bg); }
.role-status.local  .rs-dot { background: #b9b0a6; }
.role-status.cloud  { background: color-mix(in oklch, var(--status-active) 12%, transparent); }
.role-status.cloud  .rs-dot { background: var(--status-active); }
.role-status.peer   { background: color-mix(in oklch, var(--status-completed) 12%, transparent); }
.role-status.peer   .rs-dot { background: var(--status-completed); }
.role-status.server { background: color-mix(in oklch, #7c5cc4 16%, transparent); }
.role-status.server .rs-dot { background: #7c5cc4; }

/* The role chooser: the ONE place a device's role changes, gated per target. */
.role-choice { border: 1px solid var(--list-border); border-radius: 10px; padding: 12px 14px; margin-top: 8px; display: flex; align-items: flex-start; gap: 12px; }
.role-choice.current { border-color: var(--accent-text); background: color-mix(in oklch, var(--accent) 5%, transparent); }
.role-choice .rc-body { flex: 1 1 auto; min-width: 0; }
.role-choice .rc-title { font-weight: 600; font-size: 13.5px; color: var(--text); }
.role-choice .rc-desc { font-size: 12px; color: var(--text-muted); margin-top: 2px; line-height: 1.45; }
.role-choice .rc-act { flex: 0 0 auto; align-self: center; }
.role-pill { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 600; padding: 3px 9px; border-radius: 999px; }
.role-pill.here { background: color-mix(in oklch, var(--status-active) 16%, transparent); color: var(--status-active); }
.role-pill.serve { background: color-mix(in oklch, #7c5cc4 18%, transparent); color: #6a49b8; }
/* Server's own web link / broadcast callout tone. */
.serve-note { color: #6a49b8; }

/* Server pane: connect-another-device + serving options, folded away by default. */
.sync-advanced { margin-top: 8px; border: 1px solid var(--list-border); border-radius: 10px; overflow: hidden; }
.sync-advanced > summary { list-style: none; cursor: pointer; padding: 11px 14px; font-size: 13px; font-weight: 600; color: var(--text); }
.sync-advanced > summary::-webkit-details-marker { display: none; }
.sync-advanced > summary::before { content: "▸ "; color: var(--text-muted); }
.sync-advanced[open] > summary { border-bottom: 1px solid var(--list-border); background: var(--list-bg); }
.sync-advanced[open] > summary::before { content: "▾ "; }
.sync-advanced-inner { padding: 6px 14px 14px; }
.peer-follow-form .setting-row:last-of-type { border-bottom: none; }
/* Manual connect-link fallback, folded under the fleet discovery list. */
.peer-manual > summary { list-style: none; cursor: pointer; padding: 8px 0; font-size: 12.5px; color: var(--text-muted); }
.peer-manual > summary::-webkit-details-marker { display: none; }
.peer-manual > summary::before { content: "▸ "; }
.peer-manual[open] > summary::before { content: "▾ "; }

/* ---- AI usage board --------------------------------------------------------- */
.usage-board { width: 100%; border-collapse: collapse; margin: 2px 0 14px; }
.usage-board th, .usage-board td { text-align: left; padding: 9px 10px; border-bottom: 1px solid var(--list-border); }
.usage-board thead th { font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: .04em; }
.usage-board th.usage-kind { font-size: 13px; font-weight: 600; color: var(--text); white-space: nowrap; }
.usage-cell .usage-tokens { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--text); }
.usage-cell .usage-calls { font-size: 12px; color: var(--text-muted); margin-top: 1px; }

/* ---- AI / semantic-search pane ---------------------------------------------- */
.setting-input[type="text"], .setting-input[type="password"] { min-width: 240px; }
.ai-pane .storage-path-actions { margin-top: 12px; }
.ai-key-wrap, .ai-model-wrap { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.ai-key-wrap .setting-input, .ai-model-wrap .setting-input, .ai-model-wrap select.setting-input { min-width: 200px; }
.ai-key-wrap .ghost-btn, .ai-model-refresh { padding: 4px 10px; font-size: 12px; }
.ai-model-note { flex-basis: 100%; text-align: right; margin-top: 4px; }

/* ---- MCP + CLI panes -------------------------------------------------------- */
.localapi-pane .ai-key-wrap { margin-top: 4px; }
.localapi-pane .ai-key-wrap .setting-input { min-width: 260px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
/* Ruled section headings: each pane is a short sequence of steps, and the rules
   keep the copy/paste boxes from running together into one wall. */
.mcp-pane .setting-subhead,
.cli-pane .setting-subhead { margin: 26px 0 6px; padding-top: 16px; border-top: 1px solid var(--list-border);
  font-size: 13px; font-weight: 600; color: var(--text); }
/* One command per row, each independently copyable. A single multi-line textarea
   could only be selected as a whole, so "copy" couldn't mean one command. */
.cmd-row { margin: 12px 0 16px; }
/* Click anywhere on the command to copy it — no separate button, matching the
   other snippet boxes. `pointer` + the hover tint advertise that it's clickable. */
.cmd-text { display: block; padding: 7px 9px; border-radius: 6px;
  background: var(--chip-bg); border: 1px solid var(--list-border);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px;
  line-height: 1.5; color: var(--text); white-space: pre-wrap; overflow-wrap: anywhere;
  cursor: pointer; transition: background 0.12s, border-color 0.12s; }
.cmd-text:hover { border-color: var(--accent-text); }
.cmd-row.copied .cmd-text { color: var(--accent-text); font-weight: 600; }
.cmd-caption { margin: 5px 0 0; font-size: 12px; line-height: 1.5; color: var(--text-muted); }

/* ---- Import pane (Preferences → Import) ------------------------------------- */
/* Direct children only: the section HEADINGS. A descendant selector also hits
   the labels inside .setting-row, bolding them unlike every other pane. */
.import-pane > .setting-label { display: block; font-weight: 600; margin: 14px 0 6px; }
/* Two native cards today; auto-fit so adding a third needs no CSS change. */
.import-sources { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px; }
.import-card { display: flex; flex-direction: column; gap: 3px; align-items: flex-start;
  text-align: left; padding: 10px 12px; border: 1px solid var(--list-border);
  border-radius: 8px; background: var(--chip-bg); color: var(--text); cursor: pointer; }
.import-card:hover { border-color: var(--accent-text); }
.import-card-title { font-size: 13px; font-weight: 600; }
.import-card-sub { font-size: 11px; color: var(--text-muted); }

.import-summary { display: flex; gap: 18px; margin: 12px 0 4px; }
.import-stat { display: flex; flex-direction: column; }
.import-stat b { font-size: 17px; }
.import-stat span { font-size: 11px; color: var(--text-muted); }
.import-stat-warn b { color: var(--status-onHold); }

/* Scrollable body with a sticky header: --modal-bg (the settings modal's own
   backdrop) keeps the header opaque over rows scrolling under it. */
.import-table { width: 100%; border-collapse: collapse; font-size: 12px; margin-top: 10px;
  display: block; max-height: 260px; overflow: auto; }
.import-table th { text-align: left; font-weight: 600; color: var(--text-muted);
  padding: 4px 8px 4px 0; position: sticky; top: 0; background: var(--modal-bg); }
.import-table td { padding: 4px 8px 4px 0; border-top: 1px solid var(--list-border);
  vertical-align: top; }
.import-table code { font-size: 11px; color: var(--text-muted); }

/* Disclosure shape borrowed from .mcp-stdio above (same pattern, same feel). */
.import-warnings { margin-top: 14px; font-size: 12px; color: var(--text-muted); }
.import-warnings > summary { cursor: pointer; user-select: none; color: var(--status-onHold); }
.import-warnings ul { margin: 6px 0 0; padding-left: 18px; line-height: 1.6; }

/* One progress bar for the app (import, backup) — not one per feature. */
.progress-bar { height: 8px; border-radius: 4px; background: var(--list-border);
  overflow: hidden; margin: 10px 0 4px; }
.progress-bar > div { height: 100%; background: var(--accent); transition: width .3s; }
/* Phases with no measurable length (a WAL checkpoint, a `tar` we shell out to) show
   motion instead of a fake 0% — the whole complaint was that a long silent step is
   indistinguishable from a hang. */
.progress-bar.indeterminate > div { width: 40%; animation: progress-slide 1.1s ease-in-out infinite; }
@keyframes progress-slide { 0% { margin-left: -40%; } 100% { margin-left: 100%; } }
@media (prefers-reduced-motion: reduce) {
  .progress-bar.indeterminate > div { animation: none; width: 100%; opacity: .45; }
}

.import-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
textarea.mcp-cmd { width: 100%; margin-top: 8px; resize: vertical; white-space: pre; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.5; }
/* Collapsed stdio-bridge fallback: HTTP is the recommended path (no build), so
   the bridge config is tucked behind a disclosure instead of shown by default. */
.mcp-stdio { margin-top: 14px; border-top: 1px solid var(--list-border); padding-top: 10px; }
.mcp-stdio > summary { cursor: pointer; user-select: none; }
.mcp-stdio > summary:hover { color: var(--text); }

/* ---- generic modal (prompt / confirm / picker) ------------------------------ */
.modal-box { background: var(--modal-bg); border-radius: 10px; box-shadow: 0 16px 48px rgba(0,0,0,0.3); width: 360px; max-width: 88vw; padding: 18px; }
.modal-title { font-size: 14px; font-weight: 600; margin-bottom: 12px; color: var(--text); }
.modal-detail { font-size: 12.5px; line-height: 1.5; color: var(--text-muted); margin: -4px 0 4px; white-space: pre-wrap; word-break: break-word; }
.modal-input { width: 100%; border: 1px solid var(--list-border); border-radius: 6px; padding: 7px 10px; font-size: 14px; background: var(--input-bg); color: var(--text); }
.modal-input:focus { outline: none; border-color: var(--accent-text); }
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; flex-wrap: wrap; }

/* AI assist toolbar button — deliberately prominent (accent pill) */
.ai-assist-btn { display: inline-flex; align-items: center; gap: 5px; background: var(--chip-bg); color: var(--text-muted);
  border: 1px solid var(--list-border); border-radius: 7px; padding: 6px 14px; font-size: 14px; font-weight: 700; cursor: pointer;
  line-height: 1.4; white-space: nowrap; letter-spacing: 0.01em; }
.ai-assist-btn:hover { background: var(--list-hover); color: var(--text); }
.ai-assist-btn.working { opacity: 0.85; }
.ai-assist-btn.active { border-color: var(--accent-text); color: var(--text); }

/* Per-note AI progress badge in the middle list */
@keyframes ai-spin { to { transform: rotate(360deg); } }
.ai-badge { flex: 0 0 auto; margin-left: 4px; font-size: 11px; line-height: 1; }
.ai-badge-running { color: var(--accent-text); display: inline-block; animation: ai-spin 1.2s linear infinite; }
.ai-badge-done { color: var(--accent-text); }
.ai-badge-error { color: #c0392b; }

/* Fix before/after bar: segmented Current | AI suggestion toggle */
.ai-fix-left { display: inline-flex; align-items: center; gap: 8px; }
.ai-fix-tag { font-size: 12px; font-weight: 600; color: var(--accent-text); }
.ai-fix-toggle { display: inline-flex; border: 1px solid var(--list-border); border-radius: 6px; overflow: hidden; }
.ai-fix-toggle .seg-btn { background: none; border: none; color: var(--text-muted); padding: 4px 10px;
  font-size: 12px; cursor: pointer; line-height: 1.4; }
.ai-fix-toggle .seg-btn:hover { background: var(--list-hover); }
.ai-fix-toggle .seg-btn.active { background: var(--accent); color: var(--accent-ink); }

/* ⌘P Velocity Mode — NV-style command palette (input · results · preview) */
.velocity-modal { width: 860px; max-width: 94vw; padding: 0; overflow: hidden;
  display: flex; flex-direction: column; }
.velocity-input { width: 100%; box-sizing: border-box; border: none; outline: none;
  background: transparent; color: var(--text); font-size: 17px; padding: 15px 18px; }
.velocity-body { display: flex; min-height: 0; height: 56vh;
  border-top: 1px solid var(--list-border); }
.velocity-list { list-style: none; margin: 0; padding: 0; flex: 0 0 320px;
  overflow-y: auto; border-right: 1px solid var(--list-border); }
.velocity-item { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 9px 16px; cursor: pointer; }
.velocity-item.sel { background: var(--selection); box-shadow: inset 3px 0 0 var(--accent); }
.velocity-title { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; color: var(--text); }
.velocity-kind { flex: 0 0 auto; font-size: 11px; color: var(--text-muted); }
.velocity-snippet { flex: 1 0 100%; font-size: 12px; color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* matched-term highlight in the palette rows (mirrors .note-item mark) */
.velocity-item mark { background: color-mix(in oklch, var(--accent) 22%, transparent);
  color: inherit; border-radius: 2px; padding: 0 1px; }
.velocity-preview-pane { flex: 1 1 auto; min-width: 0; overflow: hidden; position: relative;
  display: flex; flex-direction: column; }
/* the CodeMirror preview sits UNDER the metadata header and takes the rest */
.velocity-preview-body { flex: 1 1 auto; min-height: 0; overflow: hidden; position: relative; }
/* metadata header: what the one-line palette row cannot carry (tags, status, dates) */
.velocity-meta { flex: 0 0 auto; padding: 10px 12px 8px; border-bottom: 1px solid var(--list-border); }
.velocity-meta.empty { display: none; }
.velocity-meta-title { font-size: 13px; font-weight: 600; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.velocity-meta-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.velocity-meta-chip { display: inline-flex; align-items: center; gap: 4px;
  font-size: 11px; color: var(--text-muted); background: var(--chip-bg);
  border-radius: 10px; padding: 1px 8px; max-width: 220px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.velocity-meta-dates { margin-top: 6px; font-size: 11px; color: var(--text-muted); }
.velocity-preview-pane .CodeMirror { height: 100%; background: transparent;
  font-size: 13px; color: var(--text); }
/* term hits inside the preview — stronger than the chunk wash below, same accent
   family (was a fixed green that ignored the palette) */
.velocity-preview-pane .cm-velocity-match { background: color-mix(in oklch, var(--accent) 32%, transparent); border-radius: 2px; }
/* the semantically-matched chunk (markText) — a soft accent wash behind the term hits */
.velocity-preview-pane .velocity-chunk-match { background: color-mix(in srgb, var(--accent) 16%, transparent); border-radius: 2px; }
.velocity-body.no-preview .velocity-preview-pane { display: none; }
.velocity-body.no-preview .velocity-list { flex: 1 1 auto; }
.velocity-footer { display: flex; align-items: center; gap: 12px; padding: 8px 16px;
  border-top: 1px solid var(--list-border); }
.velocity-hint { flex: 1 1 auto; text-align: right; font-size: 12px; color: var(--text-muted); }
/* keyboard-selectable "create in <notebook>" rows. The row label is the NOTEBOOK
   (that's what varies down the group); the query appears once, in the group
   header — which is data-createLabel, not a fixed string, so it names the note
   being created. */
.velocity-create .velocity-title { color: var(--accent-text); }
.velocity-create .velocity-kind { color: var(--accent-text); opacity: 0.85; }
.velocity-create-first { border-top: 1px solid var(--list-border); }
.velocity-create-first::before { content: attr(data-create-label); flex: 1 0 100%;
  order: -1; font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--text-muted); padding-bottom: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.primary-btn.danger { background: var(--status-dropped); }
.danger-btn { display: inline-block; color: var(--status-dropped); border: 1px solid var(--status-dropped); border-radius: 6px; padding: 6px 12px; background: transparent; cursor: pointer; font-size: 13px; margin-top: 16px; }
.danger-btn:hover { background: var(--status-dropped); color: #fff; }
/* Notebook picker (notebook.js notebookList) — the one notebook list widget,
   shared by the move-note picker and the Import pane. Rows carry an icon, the
   name (or the full path once filtered), and the sidebar's count roll-up. */
.picker-list { max-height: 300px; overflow-y: auto; margin-top: 4px; }
.picker-list.busy { opacity: 0.6; pointer-events: none; }
.picker-item { display: flex; align-items: center; gap: 8px;
  padding: 7px 10px; border-radius: 6px; cursor: pointer; font-size: 13px; color: var(--text); }
.picker-item svg { color: var(--text-muted); flex: none; }
.picker-item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.picker-item-count { flex: none; font-size: 11px; color: var(--text-muted); }
.picker-item:hover, .picker-item.active { background: var(--list-hover); }
.picker-item.picker-create { color: var(--accent-text); }
.picker-item.picker-create svg { color: var(--accent-text); }
.picker-empty { padding: 8px 10px; color: var(--text-muted); font-size: 13px; }
.picker-hint { font-size: 11px; color: var(--text-muted); text-align: right; padding: 6px 2px 0; min-height: 14px; }
.bookpick-modal { width: 420px; }
.bookpick-modal .modal-input { display: block; width: 100%; margin: 8px 0 4px; box-sizing: border-box; }

/* Import pane: "Import into" opens the notebook picker, so it's a button that
   shows the current target — not a free-text field (a typo silently made a
   second notebook). */
.import-book-btn { display: inline-flex; align-items: center; gap: 6px; max-width: 260px; }
.import-book-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.import-book-caret { color: var(--text-muted); font-size: 10px; flex: none; }

/* ---- context menu ----------------------------------------------------------- */
/* A menu row's label is plain text, so pressing on it and moving even slightly
   made WKWebView start a TEXT DRAG and paint a translucent copy of the row next
   to the cursor — a second floating "Paste" hovering over the real one, which
   reads as a duplicated menu item. `user-select: none` (both spellings — the
   unprefixed one alone does not stop the press-and-drag in WKWebView, same as
   the sidebar above) plus `-webkit-user-drag: none` kills the ghost; nothing in
   a context menu was ever meant to be selected or dragged. */
.context-menu { position: fixed; z-index: 200; background: var(--modal-bg); border: 1px solid var(--list-border); border-radius: 8px; box-shadow: 0 8px 28px rgba(0,0,0,0.25); padding: 5px; min-width: 180px;
  user-select: none; -webkit-user-select: none; -webkit-user-drag: none; }
.context-item { display: flex; align-items: center; gap: 10px; padding: 7px 12px; border-radius: 5px; cursor: pointer; font-size: 13px; color: var(--text); white-space: nowrap; }
.context-item:hover { background: var(--accent); color: var(--accent-ink); }
.context-item.danger { color: var(--status-dropped); }
.context-item.danger:hover { background: var(--status-dropped); color: #fff; }
.context-item.disabled { color: var(--text-muted); cursor: default; }
.context-item.disabled:hover { background: none; color: var(--text-muted); }
.context-item .ctx-label { flex: 1; }
.context-item .ctx-shortcut { color: var(--text-muted); font-size: 11px; opacity: 0.8; }
.context-item:hover .ctx-shortcut { color: rgba(255,255,255,0.65); opacity: 1; }
.context-item.disabled .ctx-shortcut { opacity: 0.4; }
.submenu-arrow { margin-left: auto; opacity: 0.6; font-size: 15px; }
.context-divider { height: 1px; background: var(--list-border); margin: 4px 0; }

/* the open note's notebook / status, marked in the sidebar */
.tree-item.note-here .tree-label, .nav-item.note-here .nav-label { color: var(--accent-text); font-weight: 600; }
.tree-item.note-here.active .tree-label, .nav-item.note-here.active .nav-label { color: var(--accent-ink); }
.tree-item.note-here, .nav-item.note-here { box-shadow: inset 2px 0 0 var(--accent); }

/* ---- drag-and-drop ---------------------------------------------------------- */
/* The wash was a hardcoded `rgba(33,133,208,.12)` — a fixed blue that ignored the
   palette entirely (THEME.md: a colour literal in this file is almost always a
   bug). It is the accent now, mixed down so it reads as a wash in both themes. */
.drop-target { outline: 2px dashed var(--accent); outline-offset: -2px;
  background: color-mix(in oklch, var(--accent) 12%, transparent) !important; }
body.dragging { cursor: grabbing; user-select: none; }
.drag-source { opacity: 0.4; }
/* reorder: a clear insertion line above (before) or below (after) the target */
.tree-item.drop-before { box-shadow: inset 0 3px 0 var(--accent); }
.tree-item.drop-after { box-shadow: inset 0 -3px 0 var(--accent); }
/* nest: highlight the whole row + outline so it's obviously "into this notebook" */
.tree-item.drop-target { background: var(--sidebar-active); box-shadow: inset 0 0 0 2px var(--accent); }
/* floating label that follows the cursor while dragging a notebook */
.drag-ghost { position: fixed; z-index: 9999; pointer-events: none; background: var(--accent); color: var(--accent-ink);
  font-size: 12px; line-height: 1; padding: 4px 8px; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.4); opacity: 0.95; }
.note-item[draggable="true"] { cursor: default; }

/* ---- GitHub-style alerts ---------------------------------------------------- */
.md-alert { border-left: 4px solid var(--text-muted); border-radius: 6px; padding: 8px 14px; margin: 0.7em 0; background: var(--chip-bg); }
.md-alert-title { font-weight: 600; font-size: 0.92em; margin-bottom: 2px; }
.md-alert-body p { margin: 0.2em 0; }
.md-alert-note { border-color: var(--status-completed); } .md-alert-note .md-alert-title { color: var(--status-completed); }
.md-alert-tip { border-color: var(--status-active); } .md-alert-tip .md-alert-title { color: var(--status-active); }
.md-alert-important { border-color: #a333c8; } .md-alert-important .md-alert-title { color: #a333c8; }
.md-alert-warning { border-color: var(--status-onHold); } .md-alert-warning .md-alert-title { color: var(--status-onHold); }
.md-alert-caution { border-color: var(--status-dropped); } .md-alert-caution .md-alert-title { color: var(--status-dropped); }

/* ---- <kbd> + highlight.js --------------------------------------------------- */
.markdown-body kbd { background: var(--chip-bg); border: 1px solid var(--list-border); border-bottom-width: 2px; border-radius: 4px; padding: 1px 5px; font-family: var(--font-mono); font-size: 0.85em; }
.markdown-body sub, .markdown-body sup { font-size: 0.75em; }
.markdown-body pre code.hljs { background: transparent; padding: 0; }
.markdown-body img { max-width: 100%; border-radius: 6px; }
/* Preview-pane image resize (decoratePreviewImages in app.js) — same corner
   handle + live size badge as the editor's live-preview widget. */
.markdown-body .pv-img-wrap { position: relative; display: inline-block; max-width: 100%; }
.markdown-body .pv-img-handle {
  position: absolute; right: 2px; bottom: 2px; width: 14px; height: 14px;
  border-right: 3px solid var(--accent); border-bottom: 3px solid var(--accent);
  border-radius: 0 0 4px 0; cursor: nwse-resize;
  opacity: 0; transition: opacity 0.12s ease; }
.markdown-body .pv-img-wrap:hover .pv-img-handle,
.markdown-body .pv-img-wrap.resizing .pv-img-handle { opacity: 0.9; }
.markdown-body .pv-img-size {
  position: absolute; right: 6px; top: 6px; padding: 2px 7px;
  border-radius: 4px; background: rgba(0, 0, 0, 0.65); color: #fff;
  font-size: 11px; font-family: var(--font-mono); pointer-events: none; }
.markdown-body .pv-img-size:empty { display: none; }

/* CodeMirror find/replace dialog — dark theme */
body.theme-dark .CodeMirror-dialog { background: var(--editor-bg); color: var(--text); border-color: var(--list-border); }
body.theme-dark .CodeMirror-dialog input { background: var(--input-bg); color: var(--text); border: 1px solid var(--list-border); }

/* tag color swatch in menus */
.ctx-swatch { width: 11px; height: 11px; border-radius: 50%; flex: 0 0 auto; }

/* PDF export via window.print() — show only the rendered preview */
@media print {
  body { overflow: visible; }
  #sidebar, #note-list-pane, #editor-header, #editor-meta, #editor-toolbar,
  #editor-status-bar, .cm-host, #empty-state, #trash-bar, .overlay, .context-menu { display: none !important; }
  #app { display: block !important; height: auto !important; }
  #editor-pane, #editor-body { display: block !important; overflow: visible !important; }
  #preview { display: block !important; overflow: visible !important; height: auto !important; max-width: 720px; margin: 0 auto; }
}

/* multi-select + batch bar */
.note-item.multi-selected { background: var(--selection); }
/* Six actions in a 300px column wrap, and that is fine — what was NOT fine was
   the dismiss ✕ riding `margin-left: auto` onto the end of whichever row it
   landed on, so the bar looked broken rather than wrapped. It is pinned to the
   top-right corner now, and the rows below it are evenly spaced. */
.batch-bar { display: none; align-items: center; gap: 6px 5px; padding: 7px 30px 7px 12px;
  border-bottom: 1px solid var(--list-border); background: var(--list-hover);
  flex-wrap: wrap; position: relative; }
.batch-count { font-size: 12px; font-weight: 600; color: var(--text); margin-right: 2px; }
.batch-btn { background: var(--input-bg); border: 1px solid var(--list-border); border-radius: 6px;
  padding: 3px 8px; font-size: 12px; line-height: 1.5; cursor: pointer; color: var(--text); white-space: nowrap; }
.batch-btn:hover { border-color: var(--accent-text); color: var(--accent-text); }
.batch-btn.batch-clear { position: absolute; top: 5px; right: 8px; margin: 0;
  border: none; background: none; font-size: 14px; padding: 2px 4px; color: var(--text-muted); }
.batch-btn.batch-clear:hover { color: var(--text); }

/* Auth-expired banner — shown when cloud sync fails with HTTP 401 (re-login). */
.auth-banner {
  position: fixed; top: 0; left: 0; right: 0; z-index: 9999;
  display: flex; align-items: center; gap: 12px;
  padding: 8px 14px;
  background: #b42318; color: #fff;
  font-size: 13px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
}
.auth-banner-msg { flex: 1 1 auto; }
.auth-banner-btn {
  flex: 0 0 auto; border: 0; border-radius: 6px;
  padding: 4px 12px; cursor: pointer;
  background: #fff; color: #b42318; font-weight: 600;
}
.auth-banner-x {
  flex: 0 0 auto; border: 0; background: transparent; color: #fff;
  cursor: pointer; font-size: 14px; opacity: 0.85; padding: 2px 6px;
}
.auth-banner-x:hover { opacity: 1; }

/* ═══════════════════════════════════════════════════════════════════════════
   June flavor — components only. The panes stay FLUSH and fill the window; an
   earlier floating "inset card" wasted top/bottom space and read as a box-in-a-
   box because notedrop keeps the native titlebar (June hides it for a tab strip).
   So we keep the warm surfaces + the brand mark, segmented pill, and softened
   chrome, and let the three panes fill edge-to-edge like before. */

/* Brand mark — a clay tile with an "n" (notedrop) seated before the wordmark. */
.brand-mark { display: inline-flex; flex: 0 0 auto; margin-right: 8px; }
.brand-mark svg { display: block; }
.app-name { font-size: 16px; font-weight: 600; }

/* Segmented pill (view-mode switch): a rail with the active segment as a raised
   chip — June's segmented-control treatment, not a solid accent fill. */
.viewmode-group { border: 1px solid var(--list-border); border-radius: 999px; background: var(--chip-bg); padding: 2px; gap: 2px; overflow: visible; margin-right: 6px; }
.vm-btn { border-radius: 999px; padding: 4px 9px; line-height: 1; }
.vm-btn:hover { background: transparent; color: var(--text); }
.vm-btn.active { background: var(--editor-bg); color: var(--accent-text); box-shadow: 0 1px 2px oklch(24% 0.01 60 / 18%); }
body.theme-dark .vm-btn.active { background: var(--list-hover); }

/* Softer chrome, mock-calibrated: 7px rounded rects (the full pills read as
   too round against the squarer note cards), roomier nav rows. */
.search { border-radius: 7px; padding: 6px 12px; }
.nav-item, .tree-item { border-radius: 8px; }
.primary-btn { border-radius: 7px; padding: 6px 13px; }

/* ---- Voice module -------------------------------------------------------- */
#voice-btn.recording,
#editor-pane #voice-btn.recording:hover {   /* keep the red on hover (override .icon-btn:hover) */
  color: #fff;
  background: var(--danger, #e5484d);
  animation: voice-pulse 1.2s ease-in-out infinite;
}
@keyframes voice-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(229, 72, 77, 0.5); }
  50% { box-shadow: 0 0 0 4px rgba(229, 72, 77, 0); }
}
#voice-btn.transcribing { opacity: 0.6; pointer-events: none; }
#voice-btn.transcribing svg { animation: voice-spin 0.9s linear infinite; }
@keyframes voice-spin { to { transform: rotate(360deg); } }

/* Audio player card — [name.wav](filelink://…) in both the editor live-preview
   (AudioWidget) and the rendered preview pane. Matches the voice editor mock
   exactly: ▶ · (filename over self-drawn waveform) · mm:ss / mm:ss · [转写]. */
.voice-audio-card {
  display: flex; align-items: center; gap: 12px;
  background: var(--chip-bg); border: none;   /* single soft surface — no border box */
  border-radius: 10px; padding: 10px 14px; margin: 3px 0 5px;
  max-width: 100%; box-sizing: border-box; overflow: hidden;
}
.voice-audio-card .aw-play {
  flex: none; width: 34px; height: 34px; border-radius: 50%; border: none;
  background: var(--brand); color: var(--accent-ink); cursor: pointer;
  display: grid; place-items: center; padding: 0;
}
.voice-audio-card .aw-play svg { margin-left: 2px; }
.voice-audio-card .aw-mid { flex: 1; min-width: 0; }
.voice-audio-card .aw-name {
  display: flex; gap: 8px; font-size: 12px; color: var(--text-muted);
  margin-bottom: 5px; min-width: 0;
}
.voice-audio-card .aw-name b { color: var(--text); font-weight: 600; flex: none; }
.voice-audio-card .aw-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; opacity: 0.8; }
.voice-audio-card .aw-wave {
  display: flex; align-items: center; gap: 1.5px; height: 26px; cursor: pointer;
  overflow: hidden; width: 100%;   /* never spill past the card */
}
.voice-audio-card .aw-wave i {
  flex: 1 1 0; min-width: 0; border-radius: 1px;
  background: var(--wave-rest, color-mix(in srgb, var(--text-muted) 45%, transparent));
  transition: background 0.08s;
}
.voice-audio-card .aw-wave i.played { background: var(--brand); }
.voice-audio-card .aw-time {
  flex: none; font-family: var(--font-mono); font-size: 11.5px;
  color: var(--text-muted); font-variant-numeric: tabular-nums;
}
.voice-audio-card .aw-time b { color: var(--text); font-weight: 600; }
.voice-audio-card audio { display: none; }   /* hidden; the custom UI drives it */
.voice-audio-card .aw-transcribe {
  flex: none; font-size: 12px; font-weight: 600; color: var(--accent-text);
  background: none; border: 1px solid color-mix(in srgb, var(--brand) 45%, transparent);
  border-radius: 6px; padding: 3px 10px; cursor: pointer;
}
.voice-audio-card .aw-transcribe:hover { background: color-mix(in srgb, var(--brand) 10%, transparent); }
/* CM6 live-preview: the widget replaces an inline link, so make it fill the line */
.cm-line .voice-audio-card { display: inline-flex; width: 100%; vertical-align: top; margin: 4px 0; }

/* Timestamp pill — transcript phrases start with **[mm:ss]**; the preview renders
   .voice-ts (markdown.js), the editor a TimestampWidget. Click seeks the player. */
.voice-ts {
  font-family: var(--font-mono); font-size: 12px; font-weight: 600;
  color: var(--accent-text); background: color-mix(in srgb, var(--brand) 10%, transparent);
  border-radius: 5px; padding: 1px 6px; margin-right: 8px; cursor: pointer;
}
.voice-ts:hover { background: color-mix(in srgb, var(--brand) 18%, transparent); }

/* Recording bar — ONE line, IN FLOW directly above #editor-body, exactly like
   #history-bar / #ai-fix-bar. It is not an overlay: it pushes the editor down instead
   of covering it. That matters now that the transcript is written into the note as you
   speak — the old floating card sat squarely on the lines being dictated (seen in the
   real app: the opening phrases stayed hidden behind it for the whole take).
   Recording dot · name · waveform · timer · mic switcher · short status. NO transcript
   (that's in the note — see voice.js), so this never grows past one line. */
.voice-live-bar {
  display: flex; align-items: center; gap: 10px;
  padding: 6px 14px; background: var(--chip-bg);
  border-bottom: 1px solid color-mix(in srgb, var(--brand) 35%, var(--list-border));
  font-size: 13px; color: var(--text);
}
.vlc-dot {
  flex: none; width: 11px; height: 11px; border-radius: 50%; background: var(--danger, #e5484d);
  animation: voice-pulse 1.2s ease-in-out infinite;
}
.vlc-name { flex: none; font-size: 12px; font-weight: 600; color: var(--text); }
.vlc-wave {
  flex: 1; display: flex; align-items: center; gap: 2px; height: 22px;
  min-width: 0; overflow: hidden;
}
/* Bars are driven by the REAL input level pushed from the recording thread
   (`voice:level` -> voice.js drawWave). Deliberately NOT a CSS keyframe: an
   animation dances exactly the same when the mic is dead, which is precisely the
   "is this even working?" question the meter exists to answer. */
.vlc-wave i {
  flex: 1 1 0; min-width: 0; height: 100%; border-radius: 1px; transform: scaleY(0.06);
  transform-origin: center; background: color-mix(in srgb, var(--brand) 55%, transparent);
  transition: transform 90ms linear;
}
.vlc-timer {
  flex: none; font-family: var(--font-mono); font-size: 12.5px; font-weight: 600;
  color: var(--accent-text); font-variant-numeric: tabular-nums;
}
/* Which mic is being captured + inline switcher, inline in the bar. Muted until hover —
   informational, but reachable the moment nothing is landing in the note. */
.vlc-device {
  flex: 0 1 auto; min-width: 0; display: flex; align-items: center; gap: 4px; color: var(--text-muted);
}
.vlc-device .lucide { flex: none; }
.vlc-mic {
  min-width: 0; max-width: 180px; font: inherit; font-size: 12px; color: var(--text-muted);
  background: transparent; border: 1px solid transparent; border-radius: 6px;
  padding: 1px 4px; cursor: pointer; text-overflow: ellipsis;
}
.vlc-mic:hover, .vlc-mic:focus {
  color: var(--text); border-color: color-mix(in srgb, var(--brand) 35%, transparent);
  background: var(--list-hover);
}
/* Trailing status slot — "Listening…" or a SHORT warning, never phrases (those are in
   the note). Empty once text is landing, so the bar is just the recording indicator. */
.vlc-transcript {
  flex: none; font-size: 12px; color: var(--text); white-space: nowrap;
}
.vlc-transcript.vlc-empty { color: var(--text-muted); font-style: italic; }
/* No audio arriving (denied mic permission / dead device) — the full explanation is in
   the dismissible top banner; this is the always-visible short form. */
.vlc-transcript.vlc-warn { color: #b54708; font-style: normal; font-weight: 600; }
/* Amber tint so the "no audio" banner reads as a device warning, not an auth error. */
.voice-warn-banner { background: #b54708; }
.voice-warn-banner .auth-banner-btn { color: #b54708; }
/* The meter reflects real input, so it stays under reduced-motion — but drop the
   easing so it steps rather than glides. */
@media (prefers-reduced-motion: reduce) { .vlc-wave i { transition: none; } }
/* Jump-back pill reuses the pulsing dot. */
.voice-live-dot {
  width: 12px; height: 12px; border-radius: 50%; background: var(--danger, #e5484d);
  animation: voice-pulse 1.2s ease-in-out infinite; flex: none;
}

/* Recording block container — groups [audio card + meta + transcript] into one
   unit, delimited from the rest of the note. Preview pane wraps it in .voice-block;
   the editor gives each block line a shared .cm-voice-block background. */
.voice-block {
  border: 1px solid var(--list-border); border-radius: 12px;
  background: color-mix(in srgb, var(--brand) 3.5%, transparent);
  padding: 6px 14px 10px; margin: 12px 0;
}
.voice-block > p:first-child { margin-top: 8px; }
.voice-block .voice-audio-card { background: var(--chip-bg); margin-top: 6px; }

.cm-editor .cm-voice-block {
  background: color-mix(in srgb, var(--brand) 3.5%, transparent);
  padding-left: 14px; padding-right: 14px;
  box-shadow: 1px 0 0 var(--list-border) inset, -1px 0 0 var(--list-border) inset;
}
.cm-editor .cm-voice-block-first { border-top: 1px solid var(--list-border); border-radius: 12px 12px 0 0; padding-top: 6px; }
.cm-editor .cm-voice-block-last { border-bottom: 1px solid var(--list-border); border-radius: 0 0 12px 12px; padding-bottom: 8px; }

/* Floating "jump back to recording" pill — shown while recording when you've
   navigated away from the recording note. Click returns to it. */
.voice-rec-pill {
  position: fixed; z-index: 70; bottom: 20px; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 8px;
  background: var(--modal-bg, var(--chip-bg)); color: var(--text);
  border: 1px solid var(--brand); border-radius: 999px;
  padding: 7px 14px; font-size: 13px; cursor: pointer;
  box-shadow: 0 6px 24px rgba(30, 20, 12, 0.22);
}
.voice-rec-pill .voice-pill-time { font-family: var(--font-mono); font-weight: 600; color: var(--accent-text); font-variant-numeric: tabular-nums; }
.voice-rec-pill .voice-pill-title { max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-muted); }
.voice-rec-pill .voice-pill-go { color: var(--accent-text); font-weight: 700; }
.voice-rec-pill:hover { border-color: var(--accent-text); background: var(--list-hover); }

/* Note-list duration pill (voice notes) — mock's .nc-dur. */
.note-dur-pill {
  display: inline-flex; align-items: center; gap: 4px;
  background: var(--chip-bg); border-radius: 999px; padding: 1.5px 8px;
  font-variant-numeric: tabular-nums;
  /* Sits in .note-item-meta, where only the tags may give ground (see there). */
  flex: 0 0 auto; white-space: nowrap;
  /* The row is a fixed 96px and its parts already add up to exactly that, so the
     pill must not be taller than the meta line it sits on: at the old 17px
     line-height + vertical padding it stood 20px tall and pushed every VOICE
     note's row 7px over, silently clipping the bottom of the snippet. Match the
     11px/13px of the date beside it. */
  font-size: 11px; line-height: 13px; padding: 0 7px;
}
.note-dur-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--brand); }

.voice-pane .setting-subhead { margin: 18px 0 6px; font-size: 13px; font-weight: 600; opacity: 0.85; }
.voice-model-row { display: flex; align-items: flex-start; gap: 10px; padding: 8px 0; }
.voice-model-info { flex: 1; min-width: 0; }
.voice-model-name { font-weight: 500; }
.voice-model-action { flex-shrink: 0; }
.voice-dl-bar { height: 4px; border-radius: 2px; background: var(--border, #d0d7de); margin-top: 6px; overflow: hidden; }
.voice-dl-fill { height: 100%; width: 0; background: var(--accent, #4c6ef5); transition: width 0.2s; }

/* Fleet discovery list (fleet-ui.js / FLEET.md) */
.fleet-section { margin-top: 18px; }
.fleet-list { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
.fleet-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border: 1px solid var(--divider, rgba(128,128,128,0.25)); border-radius: 8px; }
.fleet-dot { width: 8px; height: 8px; border-radius: 50%; background: #b9c0bd; flex: 0 0 auto; }
.fleet-dot.on { background: #12b76a; }
.fleet-main { flex: 1 1 auto; min-width: 0; }
.fleet-name { font-weight: 600; font-size: 14px; }
.fleet-meta { font-size: 12px; opacity: 0.7; }
.fleet-btn { flex: 0 0 auto; }
.fleet-tag { font-size: 12px; padding: 2px 8px; border-radius: 999px; background: rgba(18,183,106,0.14); color: #0f9a5a; font-weight: 600; flex: 0 0 auto; }
.fleet-warn { color: #b54708; font-weight: 600; font-size: 13px; margin: 6px 0; }

/* ===================== Phone layout =====================
   One pane on screen at a time — the note list by default, body.mobile-view-nav
   for the sidebar, body.mobile-view-editor for the editor. frontend/mobile.js
   switches the classes and matches on the SAME query string (see below).
   Desktop is untouched: everything here lives behind the media query. */
#mobile-nav-btn, #mobile-back-btn, #note-more-btn { display: none; }

/* ── PHONE LAYOUT — the condition, and why it is not just a width ────────────
   Width alone asked "is this window narrow", which is not the same question as
   "is this a phone", and it got phones wrong the moment one was turned
   sideways: an iPhone in LANDSCAPE is 844–956px, so it sailed past a 700px
   query and rendered the full three-pane desktop layout — three ~280px columns
   on a handset, with no tab bar. That was true in mobile Safari AND in the iOS
   app, since both are laid out by this file.

   So the query is a two-term OR, and each term answers a different real
   question:

     (max-width: 700px)                      any window too narrow for three
                                             panes — including a desktop window
                                             dragged small, which still deserves
                                             the one-pane layout.

     (pointer: coarse) and (max-width: 1000px)
                                             a touch device that is not a large
                                             tablet. `pointer: coarse` is the
                                             DEVICE talking, not a guess from
                                             its size. 1000px clears every
                                             iPhone in landscape (the widest,
                                             the 16 Pro Max, is 956) and iPad
                                             portrait (744–834, genuinely too
                                             narrow for three columns) while
                                             leaving iPad landscape (1024+) on
                                             the wide layout.

   `mobile.b4f9d47d.js` matches on this SAME string — keep the two in step. And note
   this is still the LAYOUT axis: what the shell can DO remains features.js,
   declared by the build (FEATURES.md §1.1). A touch screen never grants or
   removes a capability. */
@media (max-width: 700px), (pointer: coarse) and (max-width: 1000px) {
  #mobile-nav-btn, #mobile-back-btn { display: inline-flex; align-items: center; flex-shrink: 0; }

  /* The grid becomes a COLUMN: one full-screen pane with the tab bar under it.
     (It was `display: block` with `height: 100%` panes; the tab bar has to take
     real height off them, so the pane sizes with flex instead of a percentage
     that would run under the bar.) dvh tracks the collapsing browser chrome on
     phones; vh is the fallback for older engines. */
  #app { display: flex; flex-direction: column; height: 100vh; height: 100dvh; }
  .col-resizer { display: none; }
  #sidebar, #note-list-pane, #editor-pane { display: none; width: 100%; flex: 1; min-height: 0; height: auto; }
  body:not(.mobile-view-nav):not(.mobile-view-editor) #note-list-pane { display: flex; }
  body.mobile-view-nav #sidebar { display: flex; } /* beats body.sidebar-collapsed #sidebar: equal specificity, later in file */
  body.mobile-view-editor #editor-pane { display: flex; }
  #note-list-pane { border-right: none; }

  /* ---- The bottom tab bar ------------------------------------------------
     Four fixed destinations, each a thumb-tap away. It replaces a hamburger
     that had all of them behind a drawer — the single biggest reason the phone
     build read as a desktop app squeezed narrow. Labels stay under the icons:
     an icon-only bar makes the user learn four glyphs before they can navigate,
     and there is width for both.
     The hamburger stays: "Books" opens the same drawer, which still carries
     status, tags and quick access. Nothing was removed to make room. */
  .mobile-tabs {
    display: flex; flex: none;
    border-top: 1px solid var(--list-border);
    background: var(--sidebar-bg);
    padding-bottom: env(safe-area-inset-bottom);   /* clear of the home indicator */
  }
  .mtab {
    flex: 1; min-width: 0;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 3px; padding: 7px 2px 6px;
    border: none; background: none; cursor: pointer;
    color: var(--text-muted); font-size: 10.5px; font-family: inherit;
    -webkit-tap-highlight-color: transparent;
  }
  .mtab svg { width: 21px; height: 21px; }
  .mtab.on { color: var(--accent-text); font-weight: 600; }
  .mtab:active { background: var(--list-hover); }

  /* side-by-side cannot exist at phone width — the editor half gets the screen
     (mobile boot also defaults the mode to live preview, app.js) */
  #editor-body.mode-side { grid-template-columns: 1fr 0; }

  /* iOS Safari zooms the whole page when a focused input is under 16px */
  .search, .title-input, .tag-input { font-size: 16px; }


  /* the editor header wraps into two rows — nav + title, then the action
     icons as a sideways-scrolling strip (they'd otherwise squeeze the title
     input to zero width at 390px) */
  .title-input { min-width: 120px; }
  .editor-actions { flex: 1 0 100%; flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; justify-content: flex-start; }
  .editor-toolbar { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }

  /* ---- Swipe a row left to trash it (mobile.js) -------------------------
     `pan-y` tells the engine up front that these rows never pan horizontally,
     so it commits the gesture to a vertical scroll or to us — without it iOS
     spends the first frames deciding and the swipe starts with a stutter.
     The PANE (not the scrolling list) is the positioning context for the one
     delete surface — see mobile.js for why it cannot live inside the list. */
  #note-list-pane { position: relative; }
  .note-item { touch-action: pan-y; }
  /* Sliding the row exposes whatever is behind it, so it needs a ground of its
     own for exactly as long as it is moving. */
  .note-item.swiping { background: var(--list-bg); position: relative; z-index: 1; }
  .swipe-action {
    position: absolute; left: 0; right: 0; z-index: 0;
    display: none; align-items: center; justify-content: flex-end;
    padding-right: 22px;
    background: var(--status-dropped);
    color: var(--status-dropped-ink);
    font-size: 13px; font-weight: 600;
  }
  .swipe-action.shown { display: flex; }
  /* Past the commit point the label brightens — the row tells you it will act
     on release, rather than making you guess how far is far enough. */
  .swipe-action-label { opacity: 0.55; transition: opacity 0.12s ease; }
  .swipe-action.armed .swipe-action-label { opacity: 1; }

  /* ---- The header's actions become ONE "⋮" -------------------------------
     Eight icons in a sideways-scrolling strip left the title fighting for room
     and every target under 30px. They fold into an overflow menu that app.js
     builds by DELEGATING to these same buttons, so nothing here is a second
     implementation of what a note can do — which is also why they are hidden
     rather than removed: `.click()` still needs them in the DOM.

     `#popout-btn` is additionally gone from the DOM on iOS, where Tauri has no
     second window (the `multiWindow` key in features.js). That is a SHELL fact
     and this is a SCREEN one: mobile web genuinely can open a window, and a
     popup is still meaningless on a 390px display. Two reasons, both true. */
  .hdr-overflow { display: none; }
  #note-more-btn { display: inline-flex; align-items: center; }

  /* ---- The note row: a title, a time, and one line ----------------------
     Four things were stacked in 96px at desktop type sizes — title, TWO snippet
     lines, a date, and tags — so the row was dense and small at the same time.
     Every list app people actually read on a phone (Mail, Bear, WeChat) puts
     the timestamp on the TITLE's line, right-aligned, and gives the remaining
     space to one line of preview at a size you can read at arm's length.

     One snippet line instead of two means the row can lose height AND gain type
     size at once. `--note-row-h` is the single definition of that height —
     app.js reads it rather than holding a copy, because the number also drives
     the spacer math behind windowed scrolling, and two copies that disagree
     leave the scrollbar correct and the content under it wrong. */
  :root { --note-row-h: 74px; }
  .note-item { position: relative; padding: 12px 16px; }
  /* The meta row carries the date; lift it to the top-right corner so it sits on
     the title's line. Absolute rather than reordered flex, because the date is
     nested inside `.note-item-meta` and cannot become a sibling of the title
     without rebuilding the row in JS — which desktop shares verbatim. */
  .note-item-meta { position: absolute; top: 12px; right: 16px; margin: 0; gap: 6px; }
  .note-item-date { font-size: 12px; opacity: 1; }
  /* Tags are the item that crowds a phone row: they compete with the title for
     the one line that has to be readable. They stay on desktop, and on a phone
     the notebook/tag they belong to is usually how you navigated here anyway. */
  .note-item-tags { display: none; }
  /* Reserve the corner so a long title runs into the time instead of under it. */
  .note-item-top { padding-right: 62px; }
  .note-item-title { font-size: 15.5px; }
  .note-item-snippet {
    font-size: 13.5px;
    -webkit-line-clamp: 1;
    height: auto;
    margin-top: 5px;
  }

  /* ---- Settings goes to the sidebar's top-right ------------------------
     Pinned to the BOTTOM of the sidebar it now sits directly above the tab
     bar — two rows of chrome stacked in the corner your thumb rests on, and
     the one you are more likely to hit is the one you did not mean. The gear
     moves to the header's top-right, which is where a phone puts it and is
     nowhere near the tab bar.

     Positioned rather than reparented: moving the node into
     `.sidebar-title-actions` would be a DOM mutation keyed on layout, and the
     layout can change under it at runtime (a rotate crosses the phone-layout
     line). This way one stylesheet owns the whole question.

     The label and the dev build id go with it — a 24px gear in a corner does
     not have room for either, and the build id is still in Preferences. */
  #sidebar { position: relative; }
  .sidebar-footer {
    position: absolute; top: 2px; right: 6px; z-index: 2;
    margin: 0; padding: 0; border-top: none; width: auto;
  }
  .sidebar-footer .settings-row { flex: 0 0 auto; padding: 8px; border-radius: 8px; }
  .sidebar-footer .settings-row span { display: none; }
  .sidebar-footer .build-id { display: none; }
  /* The title keeps clear of it, so a long app name cannot slide underneath. */
  .sidebar-title { padding-right: 44px; }

  /* comfortable touch targets */
  .icon-btn { padding: 6px 8px; }
  .nav-item, .tree-item { padding-top: 8px; padding-bottom: 8px; }

  /* status bar stays one line (the word count was wrapping into three) */
  .status-bar { gap: 10px; }
  .status-bar > span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

  /* Preferences. A 110px rail clipped every label it carried ("Appearanc",
     "Experimen") and still left the pane too narrow to lay out a row. Stack it
     the way phone settings actually work: the sections become a horizontal,
     swipeable tab strip above one full-width pane. */
  .settings-modal { width: 100vw; max-width: 100vw; height: 100vh; height: 100dvh; max-height: 100dvh; border-radius: 0; }
  .settings-body { grid-template-columns: 1fr; grid-template-rows: auto 1fr; min-height: 0; }
  .settings-nav {
    display: flex; flex-direction: row; gap: 4px;
    overflow-x: auto; overflow-y: hidden; scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    border-right: none; border-bottom: 1px solid var(--list-border);
    padding: 6px 10px;
  }
  .settings-nav::-webkit-scrollbar { display: none; }
  /* "GENERAL" / "YOUR NOTES" are vertical-list furniture; between tabs they are
     noise, and their border-top draws a stray vertical rule. */
  .settings-nav-group { display: none; }
  .settings-nav-item { flex: 0 0 auto; white-space: nowrap; border-radius: 999px; padding: 7px 13px; }

  /* ---- Preferences as a GROUPED LIST ------------------------------------
     A phone settings screen is a stack of cards on a deeper ground, not a flat
     run of hairlines — that difference is most of why this pane read as rough.
     The pane takes the deepest ground and consecutive `.setting-row`s collect
     into one rounded card, so the grouping already implied by the pane's order
     (a `.setting-note` or a heading between two rows ends one card and starts
     the next) becomes visible without settings.js growing a wrapper element. */
  .settings-pane { background: var(--sidebar-bg); padding: 14px 12px 30px; }
  .setting-row {
    background: var(--modal-bg);
    border-bottom: none;
    border-radius: 11px;
    padding: 10px 14px;
    gap: 12px;
    min-height: 44px;                       /* the iOS touch-target floor */
  }
  /* Corner rounding belongs to the CARD, not to each row: a row with a row
     above it loses its top corners, a row with a row below it loses its
     bottom ones, and a lone row keeps all four. */
  .setting-row + .setting-row {
    border-top-left-radius: 0; border-top-right-radius: 0;
    box-shadow: inset 0 1px 0 var(--list-border);   /* the separator BETWEEN rows */
  }
  .setting-row:has(+ .setting-row) { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
  .setting-label { flex: 1 1 auto; min-width: 0; }

  /* Values sit at the END of their row, iOS-style, and only take the width
     they need — a "14" used to be handed the same full 350px as a font menu,
     which is what made the pane look like a form rather than a settings list.
     Controls that genuinely need the width still get their own line below. */
  .setting-row > select,
  .setting-row > input[type="text"],
  .setting-row > input[type="password"],
  .setting-row > input[type="number"] { flex: 0 1 auto; min-width: 0; }
  .setting-row > input[type="number"] { width: 68px; text-align: right; }
  .setting-row > select { max-width: 58%; }

  /* Wide controls stack: a segmented control, a palette strip or a free-text
     field (an API key, a URL) is unreadable squeezed into half a row. */
  .setting-row:has(> .seg-control),
  .setting-row:has(> .palette-control),
  .setting-row:has(> input[type="text"]),
  .setting-row:has(> input[type="password"]) {
    flex-direction: column; align-items: stretch; gap: 7px;
  }
  .setting-row:has(> input[type="text"]) > input,
  .setting-row:has(> input[type="password"]) > input { width: 100%; }

  /* A note explains the card above it — indent it to the card's text, and give
     it the ground's own colour rather than a card's. */
  .setting-note { padding: 0 4px; margin-top: 12px; margin-bottom: 4px; }

  /* A toggle row carries a SWITCH, not a tick box. The native control renders
     at ~13px (below the touch floor) and paints itself the OS blue, so on a
     green or yellow palette every toggle in Preferences was the one element on
     screen ignoring the accent. Built from the same tokens as everything else,
     so it follows both themes and all three palettes with no rule of its own. */
  .setting-row > input[type="checkbox"] {
    appearance: none; -webkit-appearance: none;
    flex: 0 0 auto; position: relative; margin: 0;
    width: 46px; height: 28px; border-radius: 999px;
    background: var(--chip-bg); border: 1px solid var(--list-border);
    transition: background 0.15s ease, border-color 0.15s ease;
    cursor: pointer;
  }
  .setting-row > input[type="checkbox"]::after {
    content: ""; position: absolute; top: 2px; left: 2px;
    width: 22px; height: 22px; border-radius: 50%;
    background: var(--switch-knob);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.28);
    transition: transform 0.15s ease;
  }
  .setting-row > input[type="checkbox"]:checked {
    background: var(--accent); border-color: var(--accent);
  }
  .setting-row > input[type="checkbox"]:checked::after { transform: translateX(18px); }
  .setting-row > input[type="checkbox"]:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 2px; }

  /* Segmented + palette controls scroll instead of overflowing the viewport. */
  .seg-control, .palette-control {
    display: flex; overflow-x: auto; scrollbar-width: none; max-width: 100%;
  }
  .seg-control::-webkit-scrollbar, .palette-control::-webkit-scrollbar { display: none; }
  .seg-control > *, .palette-control > * { flex: 0 0 auto; }

  /* Side-by-side is not a phone layout — two ~195px columns show nothing
     usable. app.js already coerces the mode at boot and the grid collapse above
     is a guard; this removes the CONTROL, so it can no longer be chosen and
     then silently ignored. Editor-only and Live preview remain. */
  .vm-btn[data-mode="side"] { display: none; }

  /* ---- Reclaiming vertical space above the note -------------------------
     On a 390x844 screen the header, meta bar and toolbar together ate the top
     third before a single word of the note appeared. Bear and Evernote both put
     the note first and the machinery down by the keyboard. */

  /* Notebook, status and tags share ONE scrolling row instead of wrapping into
     three. `flex-wrap: wrap` is what let the meta bar grow; on a phone it
     scrolls sideways like a filter-chip strip. */
  #editor-meta {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    padding: 5px 12px;
    gap: 6px;
  }
  #editor-meta::-webkit-scrollbar { display: none; }
  #editor-meta > * { flex: 0 0 auto; }
  .tag-chips { display: inline-flex; gap: 6px; flex-wrap: nowrap; }

  /* "+ tag" is a desktop affordance — a permanently-visible empty input whose
     only job is to be clicked. On a phone it spends a row advertising an action
     the tag chips and the palette already reach, so it collapses until the tag
     editor actually takes focus. */
  .tag-input { flex: 0 0 auto; width: 0; min-width: 0; padding: 0; }
  .tag-editor:focus-within .tag-input { width: auto; min-width: 90px; padding: 0 4px; }

  /* ---- Pickers have to ESCAPE the meta bar's scroller --------------------
     This is the whole of "all the drop-down lists are broken", and the
     single-row meta bar above is what caused it: `overflow-x: auto` makes
     #editor-meta a clipping container, and a clipping ancestor clips an
     ABSOLUTELY positioned descendant. The notebook / status / tag menus were
     therefore being drawn inside a 30px-tall strip — one sliver of one row
     visible, every other row unreachable. Nothing was wrong with the menus.

     `position: fixed` leaves that clip behind (no ancestor here sets
     transform/filter/contain, which are what would re-trap a fixed element).
     And once a picker is no longer anchored to its button, the phone answer is
     a bottom sheet — what Apple Notes and Notion both do: it lands under the
     thumb instead of at the top of a 6.3-inch screen, the rows get a real
     touch target, and there is no positioning math against an anchor that can
     be scrolled halfway off the meta bar. Below the modal overlay (z 100) on
     purpose, so a modal still covers it. */
  .vn-select-menu, .tag-suggest {
    position: fixed;
    inset: auto 0 0 0;
    margin: 0;
    min-width: 0;
    max-width: none;
    max-height: 55vh;
    max-height: 55dvh;
    z-index: 90;
    padding: 6px 8px calc(8px + env(safe-area-inset-bottom));
    border: none;
    border-top: 1px solid var(--list-border);
    border-radius: 14px 14px 0 0;
    box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.28);
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }
  .vn-select-option, .tag-suggest-item {
    padding: 11px 12px;
    font-size: 14px;
    border-radius: 8px;
  }
  /* (Notebook depth still indents: ui.js writes `padding-left` INLINE per row,
     and an inline declaration beats the shorthand above.) */

  /* The format toolbar docks at the BOTTOM, where the keyboard rises — what
     Evernote and Apple Notes both do. It puts the marks under the thumb instead
     of at the far top of a 6.3-inch screen, and gives the note the space back. */
  /* flex-direction ONLY — never `display` here. `#editor-pane` is one of the
     three panes the one-pane rule at the top of this block hides with
     `display: none`, and it is un-hidden by `body.mobile-view-editor
     #editor-pane`. Re-declaring `display: flex` on the bare id (same
     specificity, later in the file) beat that `none` unconditionally, so the
     editor sat on top of the note list on EVERY screen — which is what "the
     listing is broken / huge white space" was. `10-mobile.spec.js` catches it. */
  #editor-pane { flex-direction: column; }
  #editor-header { order: 0; }
  #editor-meta { order: 1; }
  #editor-body { order: 2; flex: 1; min-height: 0; }
  #backlinks { order: 3; }
  /* ---- The format bar is a keyboard ACCESSORY, not a squeezed desktop toolbar
     It reads as a strip of 26px targets with hairline dividers between little
     groups, because that is what it is on desktop — where it sits under a mouse
     and never scrolls. On a phone it is the row that rides above the keyboard
     and gets hit with a thumb, so it wants the opposite: no dividers, one
     rhythm, and targets at the 44px iOS floor.

     All seventeen buttons stay and the row scrolls. That is deliberate: the
     complaint was that it looked bad, not that it did too much, and dropping
     marks would take away the only way to reach them while typing — the ⌘
     shortcuts they are labelled with cannot be pressed here. */
  #editor-toolbar {
    order: 4;
    border-top: 1px solid var(--list-border);
    border-bottom: none;
    padding: 4px 6px calc(4px + env(safe-area-inset-bottom));
    background: var(--list-bg);
    gap: 0;
  }
  /* The groups exist to keep sections together when the DESKTOP bar wraps. This
     bar never wraps — it scrolls — so their dividers and indents are decoration
     that breaks the rhythm. Flatten them into one even run. */
  #editor-toolbar .tb-group {
    margin-left: 0; padding-left: 0; border-left: none; gap: 0;
  }
  #editor-toolbar .tb-btn {
    min-width: 44px; height: 44px;
    font-size: 15px;
    padding: 0 6px;
    color: var(--text);
    border-radius: 10px;
  }
  #editor-toolbar .tb-btn .lucide { width: 19px; height: 19px; }
  /* Hover is a mouse idea; on touch the only feedback that exists is the press,
     and without it a tap on a transparent button looks like nothing happened. */
  #editor-toolbar .tb-btn:hover { background: none; color: var(--text); }
  #editor-toolbar .tb-btn:active { background: var(--chip-bg); }

  .status-bar { order: 5; }

  /* The tab bar stays on EVERY screen, the editor included. It was hidden here
     for a while on the theory that a note is a drill-down rather than a
     destination — but the whole point of the bar is that any of the four is
     always one thumb-tap away, and a bar that vanishes on the screen you spend
     the most time on is not that. It costs the editor a row; that is the price
     of the guarantee, and it is the guarantee that was asked for. */

  /* Linked references are a research affordance, not a reading one — collapsed
     to its header by default, tapped to open. The header stays rather than the
     whole strip hiding: the count is the useful part, and it is how you know
     there is anything to open at all. */
  .backlinks:not(.expanded) { max-height: none; padding: 6px 12px; }
  .backlinks:not(.expanded) .backlinks-item { display: none; }
  .backlinks-title { cursor: pointer; user-select: none; }
  .backlinks:not(.expanded) .backlinks-title::after { content: " ›"; }
  .backlinks.expanded .backlinks-title::after { content: " ⌄"; }

  /* The status bar carried "72 words · Created … · Updated …" on one 390px line,
     so it clipped mid-word at both ends and still cost a full row. Created and
     Updated are reference facts, not reading ones — they go behind an info
     toggle. Word count and sync state stay: those change while you work. */
  .status-bar { gap: 8px; align-items: center; }
  .status-bar .date-info { display: none; }
  .status-bar.info-open { flex-wrap: wrap; }
  .status-bar.info-open .date-info { display: block; flex: 1 0 100%; order: 2; padding-top: 3px; }
  .status-info-btn {
    order: 1; margin-left: auto; border: none; background: none; padding: 2px 6px;
    color: var(--text-muted); font-size: 12px; line-height: 1; cursor: pointer;
    border-radius: 999px;
  }
  .status-bar.info-open .status-info-btn { background: var(--list-hover); color: var(--text); }
}

/* A synthetic tag group — "work" exists only because "work/2026" names it, so
   there is no tag doc to filter by, rename or recolor. Muted so it reads as a
   grouping row rather than a tag that mysteriously does nothing. */
.tree-item-virtual .tree-label { opacity: 0.72; font-style: italic; }

/* Quick access: a shortcut row's remove button, revealed on hover so the list
   stays quiet until you reach for it. */
.quick-remove { margin-left: auto; background: none; border: 0; padding: 0 2px; cursor: pointer;
  color: var(--sidebar-muted); font-size: 11px; line-height: 1; opacity: 0; transition: opacity 0.1s; }
.tree-item:hover .quick-remove { opacity: 0.8; }
.quick-remove:hover { opacity: 1; color: var(--status-dropped); }
/* The empty state says what the section is for, rather than hiding (nothing to
   drop the first shortcut onto) or appearing mid-drag (which would shift every
   row below it while the pointer is moving). */
.quick-hint { color: var(--sidebar-muted); font-style: italic; opacity: 0.7; cursor: default; font-size: 11.5px; }

/* Link issues (frontend/link-issues.js) — a report, so the rows are readable
   rather than the fixed-height note rows: one source note, then each of its
   links that lands nowhere definite. */
.link-issue-head { padding: 10px 14px; border-bottom: 1px solid var(--list-border); background: var(--list-hover);
  display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }
.link-issue-head-text { font-size: 12px; color: var(--text-muted); line-height: 1.45; }
.note-item.link-issue-item { height: auto; overflow: visible; }
.link-issue-line { display: flex; align-items: center; gap: 7px; margin-top: 5px; font-size: 12px; }
.link-issue-kind { flex: 0 0 auto; font-size: 10.5px; border-radius: 8px; padding: 1px 7px; white-space: nowrap; }
.link-issue-dangling { background: color-mix(in oklch, var(--status-dropped) 16%, transparent); color: var(--status-dropped); }
.link-issue-ambiguous { background: color-mix(in oklch, var(--status-onHold) 20%, transparent); color: var(--status-onHold); }
.link-issue-target { font-family: var(--font-mono); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* One identifying glyph on a section heading (Tags). The ROWS show tag colours,
   so repeating an icon on each of them would just crowd out the thing that
   actually distinguishes them. */
.section-glyph { display: inline-flex; margin-right: 3px; opacity: 0.75; }
/* A disabled batch action says "not now", not "gone". */
.batch-btn:disabled { opacity: 0.45; cursor: default; }
.batch-btn:disabled:hover { border-color: var(--list-border); color: var(--text); }
/* A settings note that reports success — green enough to read as an outcome,
   not so loud it competes with the pane's own headings. */
.setting-note.setting-ok { color: var(--accent-text); white-space: pre-wrap; }

/* "DEV" badge beside the build version (Preferences → Updates / Account).
   Amber via the semantic status token — theme.css defines it for light AND
   dark, so this needs no per-mode rule and no hex literal of its own. */
.build-dev-badge {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 6px;
  border: 1px solid var(--status-onHold);
  border-radius: 4px;
  color: var(--status-onHold);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  vertical-align: 1px;
}
