/* ── Theme ──
   Color custom properties used throughout this file's CSS. data-theme is
   set on <html> by the inline script below (before first paint) and by
   applyTheme() (see the JS further down). */
:root {
    --color-bg: #fff;
    --color-bg-alt: #f0f0f0;
    --color-bg-active: #e4edfa;
    --color-bg-active-hover: #dae5f6;
    --color-text: #2e2e2e;
    --color-text-secondary: #888;
    --color-text-faint: #aaa;
    --color-border: #e6e6e6;
    --color-input-bg: #fafcff;
    --color-input-border: #d8e2ec;
    --color-input-border-focus: #6e8faa;
    --color-brand-bg: #1d2d45;
    --color-brand-bg-hover: #263d5e;
    --color-brand-text: #1d2d45;
    --color-brand-tint: #f0f4fa;
    --color-brand-overlay: rgba(29,45,69,0.85);
    --color-on-brand: #fff;
    --color-shadow: rgba(0,0,0,0.25);
    --color-backdrop: rgba(0,0,0,0.35);
}
:root[data-theme="dark"] {
    --color-bg: #1c1f26;
    --color-bg-alt: #262a33;
    --color-bg-active: #2d3e56;
    --color-bg-active-hover: #354863;
    --color-text: #e6e6e6;
    --color-text-secondary: #9099a6;
    --color-text-faint: #6b7280;
    --color-border: #333844;
    --color-input-bg: #20242c;
    --color-input-border: #3a4150;
    --color-input-border-focus: #7f9fc0;
    --color-brand-bg: #2f4a75;
    --color-brand-bg-hover: #3c5c8f;
    --color-brand-text: #7fa8e0;
    --color-brand-tint: #232f42;
    --color-brand-overlay: rgba(63,99,153,0.85);
    --color-on-brand: #fff;
    --color-shadow: rgba(0,0,0,0.5);
    --color-backdrop: rgba(0,0,0,0.55);
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; overflow: hidden; }
body {
    font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
    font-size: 14px;
    color: var(--color-text);
    background: var(--color-bg);
    line-height: 1.5;
}

/* ── Boot loader ──
   Covers the static empty #standalone-root while the module script below
   loads, syncs, and mounts the app view — without it, that gap briefly
   shows raw unstyled/empty markup. Present by default in the static HTML
   (no JS needed to show it); boot() adds .hide once the mounted view's
   first render is actually on screen. */
#boot-loader {
    position: fixed;
    inset: 0;
    z-index: 500;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 18px;
    background: #1d2d45;
    color: #fff;
    transition: opacity 0.15s ease;
}
#boot-loader.hide { opacity: 0; pointer-events: none; }
.boot-loader-logo {
    font-size: 22px;
    font-weight: 700;
    letter-spacing: -0.02em;
}
.boot-loader-spinner {
    width: 26px;
    height: 26px;
    border: 3px solid rgba(255,255,255,0.25);
    border-top-color: #fff;
    border-radius: 50%;
    animation: boot-loader-spin 0.8s linear infinite;
}
@keyframes boot-loader-spin { to { transform: rotate(360deg); } }
.boot-loader-force-refresh {
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s ease;
    background: rgba(255,255,255,0.12);
    border: 1px solid rgba(255,255,255,0.35);
    color: #fff;
    font-size: 14px;
    padding: 8px 16px;
    border-radius: 8px;
    cursor: pointer;
}
.boot-loader-force-refresh.show { opacity: 1; pointer-events: auto; }
.boot-loader-force-refresh:hover { background: rgba(255,255,255,0.2); }

/* ── Standalone mount point ── the app view owns the whole viewport. */
#standalone-root {
    display: flex;
    flex-direction: column;
    height: 100vh;
    height: 100dvh;
    height: var(--app-height, 100dvh);
    overflow: hidden;
    padding-bottom: env(safe-area-inset-bottom, 0px);
}

/* ── Sync toast ── */
#sync-toast {
    position: fixed;
    bottom: calc(20px + env(safe-area-inset-bottom, 0px));
    right: 20px;
    background: #2a2a2a;
    color: #fff;
    padding: 7px 14px;
    border-radius: 6px;
    font-size: 12px;
    opacity: 0;
    transition: opacity 0.2s;
    pointer-events: none;
}
#sync-toast.show { opacity: 1; }
#sync-toast.success { background: #1e7e34; }

/* ── Mobile breakpoint ── */
@media (max-width: 640px) {
    body { font-size: 15px; }
}

/* Styling for this file's shared utilities/components (format/storage/dialog
   helpers below). Anything built using their conventions (.note, .dialog)
   is styled here. */

.placeholder {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--color-text-faint);
}

/* ── View header/entries chrome (shared container conventions) ── */
.view-header {
    border-bottom: 1px solid var(--color-border);
    flex-shrink: 0;
}
.view-entries {
    flex: 1;
    overflow-y: auto;
    overflow-x: hidden;
    padding-bottom: 32px;
    display: flex;
    flex-direction: column;
}

.header-simple {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 11px 20px;
    background: var(--color-bg);
}
.header-left { display: flex; align-items: center; gap: 6px; }
.header-right { display: flex; align-items: center; gap: 6px; }
.header-title { font-weight: 600; font-size: 15px; }

/* Icon-only header button (e.g. the settings/⚙ trigger — see the Dialog class below) */
.settings-btn {
    background: none;
    border: none;
    cursor: pointer;
    padding: 4px 6px;
    font-size: 17px;
    line-height: 1;
    color: var(--color-text-secondary);
    border-radius: 5px;
}
.settings-btn:hover { background: var(--color-bg-alt); }

/* Page contents (markdown-rendered) */
.page-tab-contents {
    padding: 20px 20px 20px 74px;
    max-width: 760px;
    margin: 0 auto;
    line-height: 1.7;
    overflow-wrap: break-word;
    min-width: 0;
}
.page-tab-contents h1, .page-tab-contents h2, .page-tab-contents h3 {
    font-weight: 600;
    margin-top: 1.2em;
    margin-bottom: 0.3em;
}
.page-tab-contents h1 { font-size: 18px; }
.page-tab-contents h2 { font-size: 15px; }
.page-tab-contents h3 { font-size: 14px; }
.page-tab-contents p { margin-bottom: 0.6em; }
.page-tab-contents p:last-child { margin-bottom: 0; }
.page-tab-contents ul, .page-tab-contents ol { padding-left: 1.4em; margin-bottom: 0.6em; }
.page-tab-contents ul { list-style: disc; }
.page-tab-contents ol { list-style: decimal; }
.page-tab-contents li + li { margin-top: 4px; }
.page-tab-contents code {
    font-family: 'SF Mono', 'Fira Code', ui-monospace, monospace;
    font-size: 12px;
    background: var(--color-bg-alt);
    padding: 1px 4px;
    border-radius: 3px;
}
.page-tab-contents pre {
    background: var(--color-bg-alt);
    padding: 10px 12px;
    border-radius: 5px;
    overflow-x: auto;
    max-width: 100%;
    margin-bottom: 0.6em;
}
.page-tab-contents pre code {
    background: none;
    padding: 0;
    white-space: pre;
}
.page-tab-contents img, .page-tab-contents table { max-width: 100%; }
.page-tab-contents table { display: block; overflow-x: auto; }

/* ── Notes / blocks — full-width "comment" rows (Zulip-like), not chat bubbles: no
   right-alignment, no per-message background/border. A same-topic run's messages get a shared
   left border instead, at the wrapper level — see .app-journal-topic-run below — so the grouping
   reads at a glance without outlining every individual message. */
.note {
    display: flex;
    gap: 14px;
    padding: 3px 20px;
    margin: 0 2px;
    border-radius: 4px;
}
.note:hover { background: var(--color-bg-alt); }
/* Touch devices open the edit menu via long-press (see shared/longpress.js) — without this, the
   same long-press also triggers the OS's native text-selection/callout ("Copy", "Look Up", …),
   which visibly fights the custom menu for the gesture. Mouse users are unaffected (they get the
   menu via right-click/contextmenu instead) and keep normal text selection. */
@media (pointer: coarse) {
    .note { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }
}
.note-attrs {
    width: 38px;
    flex-shrink: 0;
    text-align: right;
    padding-top: 3px;
}
.note:hover .note-link-btn { color: var(--color-text-secondary); }

/* Topicless Journal entry (no page reference, see getJournalFeedForGraph's `topics` field) — never
   wrapped in an .app-journal-topic-run (see _prependJournalBatch), so it has no border of its own
   to lean on; muted text is the only cue left that it reads as "not filed under any topic". */
.note-topicless .note-body { color: var(--color-text-secondary); }

.note-body { flex: 1; overflow-wrap: break-word; min-width: 0; }
.note-body strong { font-weight: 600; }
.note-body em { font-style: italic; }
.note-body code {
    font-family: 'SF Mono', 'Fira Code', ui-monospace, monospace;
    font-size: 12px;
    background: var(--color-bg-alt);
    padding: 1px 4px;
    border-radius: 3px;
}
.note-body pre {
    background: var(--color-bg-alt);
    padding: 8px 10px;
    border-radius: 5px;
    overflow-x: auto;
    max-width: 100%;
    margin-bottom: 0.4em;
}
.note-body pre code {
    background: none;
    padding: 0;
    white-space: pre;
}
.note-body img, .note-body table { max-width: 100%; }
.note-body table { display: block; overflow-x: auto; }
.note-body h1, .note-body h2, .note-body h3 {
    font-weight: 600;
    line-height: 1.3;
    margin-top: 2px;
}
.note-body h1 { font-size: 16px; }
.note-body h2 { font-size: 14px; }
.note-body h3 { font-size: 14px; }
.note-body ul, .note-body ol { padding-left: 1.4em; }
.note-body ul { list-style: disc; }
.note-body ol { list-style: decimal; }
.note-body li + li { margin-top: 2px; }
.note-body p { margin-bottom: 0.4em; }
.note-body p:last-child { margin-bottom: 0; }

/* ── Search bar (the Topics list) ── */
.browse-search {
    padding: 16px 20px 8px;
}
.browse-search input {
    width: 100%;
    padding: 8px 12px;
    border: 1px solid var(--color-border);
    border-radius: 6px;
    font-size: 13px;
    color: var(--color-text);
    background: var(--color-input-bg);
    outline: none;
    font-family: inherit;
}
.browse-search input:focus { border-color: var(--color-input-border-focus); background: var(--color-bg); }

/* ── Primary filled button — used by dialog forms (e.g. "Add graph" in the
   Manage graphs dialog). ── */
.compose-btn {
    padding: 5px 16px;
    background: var(--color-brand-bg);
    color: var(--color-on-brand);
    border: none;
    border-radius: 5px;
    font-size: 12px;
    font-family: inherit;
    font-weight: 600;
    cursor: pointer;
}
.compose-btn:hover:not(:disabled) { background: var(--color-brand-bg-hover); }
.compose-btn:disabled { opacity: 0.5; cursor: default; }

/* ── Generic centered dialog (the Dialog class below) — e.g. the settings menu ── */
.dialog-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 300;
    background: var(--color-backdrop);
    align-items: center;
    justify-content: center;
}
.dialog-backdrop.show { display: flex; }
.dialog {
    background: var(--color-bg);
    border-radius: 10px;
    width: 90%;
    max-width: 300px;
    box-shadow: 0 8px 30px var(--color-shadow);
    overflow: hidden;
}
.dialog-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 8px 12px 16px;
    border-bottom: 1px solid var(--color-border);
}
.dialog-title { font-weight: 600; font-size: 14px; }
.dialog-close-btn {
    background: none;
    border: none;
    cursor: pointer;
    padding: 2px 8px;
    font-size: 20px;
    line-height: 1;
    color: var(--color-text-secondary);
    border-radius: 5px;
}
.dialog-close-btn:hover { background: var(--color-bg-alt); color: var(--color-text); }
.dialog-body { padding: 6px; }
.dialog-item {
    display: block;
    width: 100%;
    text-align: left;
    padding: 9px 12px;
    border: none;
    background: none;
    font-size: 13px;
    font-family: inherit;
    color: var(--color-text);
    cursor: pointer;
    border-radius: 6px;
}
.dialog-item:hover { background: var(--color-brand-tint); }
.dialog-item.active { font-weight: 600; color: var(--color-brand-text); }
.dialog-item-disabled {
    color: var(--color-text-faint);
    cursor: default;
    pointer-events: none;
}
.dialog-divider { height: 1px; background: var(--color-border); margin: 6px 4px; }
.dialog-group summary {
    display: block;
    width: 100%;
    padding: 9px 12px;
    font-size: 13px;
    font-family: inherit;
    color: var(--color-text-secondary);
    cursor: pointer;
    border-radius: 6px;
    list-style: none;
}
.dialog-group summary::-webkit-details-marker { display: none; }
.dialog-group summary:hover { background: var(--color-brand-tint); }
.dialog-group summary::before { content: '▸  '; }
.dialog-group[open] summary::before { content: '▾  '; }
.dialog-group[open] summary { color: var(--color-text); }
.dialog-group .dialog-item { padding-left: 24px; }
.dialog-footer {
    padding: 9px 12px 4px;
    font-size: 12px;
    color: var(--color-text-secondary);
    text-align: center;
}

/* ── Mobile breakpoint ── */
@media (max-width: 640px) {
    .header-simple { padding: 10px 14px; }
    .page-tab-contents { padding: 14px 14px 24px; }
    .note { padding: 3px 10px; }
    .browse-search { padding: 12px 14px 8px; }

    /* iOS Safari zooms in on focus when an input's font-size is under 16px */
    .browse-search input { font-size: 16px; }
}

/* header/entries/note chrome is covered by the shared-utilities CSS above
   (.view-header, .view-entries, .placeholder, .header-simple, .header-left,
   .header-right, .header-title, .note, .settings-btn). Rules
   below are specific to the two-pane layout, its conversation-list rows,
   and its back button. */

/* Mounts as a bare custom element inside #standalone-root's flex column
   (see the #standalone-root rule above) — without this it renders inline
   with no fixed height, so .app-layout's children have nothing to size
   themselves against. */
/* touch-action: manipulation drops the double-tap-to-zoom gesture (and the
   ~300ms tap delay with it) — paired with the no-zoom viewport meta that
   AppView's connectedCallback sets, so this feels like a native messenger
   rather than a zoomable web page. */
qorgi-app { display: flex; flex-direction: column; flex: 1; overflow: hidden; touch-action: manipulation; }

/* ── Two-pane layout ── Below DESKTOP_BP (900px), only one of the two panes
   is shown at a time — full width, chosen by which of .route-list/
   .route-topic is on .app-layout (set by handleRoute) — reproducing the
   original single-column phone UI (tap a topic, back returns to the list).
   At/above DESKTOP_BP both panes show together, list pinned to the left at
   a fixed width like a desktop messenger; .app-back-btn disappears since
   the list is always reachable right there. The thread header's topic-menu
   button (.app-topic-menu-btn) stays visible at every breakpoint — unlike
   the old duplicate settings button it used to hold, it's the only place
   topic actions live, so hiding it on desktop would remove them entirely. */
.app-layout {
    display: flex;
    flex: 1;
    min-height: 0;
    overflow: hidden;
}
.app-list-pane, .app-thread-pane {
    display: flex;
    flex-direction: column;
    flex: 1 1 100%;
    min-width: 0;
    min-height: 0;
}
@media (max-width: 899px) {
    .app-layout.route-list .app-thread-pane { display: none; }
    .app-layout.route-topic .app-list-pane { display: none; }
}
@media (min-width: 900px) {
    .app-list-pane {
        flex: 0 0 320px;
        border-right: 1px solid var(--color-border);
    }
    .app-thread-pane { flex: 1; }
    .app-back-btn { display: none; }
}

.app-back-btn {
    background: none;
    border: none;
    cursor: pointer;
    padding: 0 4px;
    font-size: 22px;
    line-height: 1;
    color: var(--color-brand-text);
}

/* ── Conversation list — rounded hover highlight instead of hard row
   dividers, closer to Signal/other mobile chat apps' list feel ── */
.app-item {
    margin: 2px 10px;
    padding: 10px 10px;
    border-radius: 10px;
    cursor: pointer;
}
.app-item:hover { background: var(--color-brand-tint); }
/* Only meaningful once the list pane can be visible alongside an open
   thread (desktop, see the two-pane layout section above) — harmless below
   that breakpoint since the list isn't shown at the same time as a thread. */
.app-item.active { background: var(--color-bg-active); }
.app-item.active:hover { background: var(--color-bg-active-hover); }
.app-item-top {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
}
.app-item-title {
    font-weight: 600;
    min-width: 0;
    flex: 1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.app-item-time {
    flex-shrink: 0;
    font-size: 11px;
    color: var(--color-text-secondary);
    font-variant-numeric: tabular-nums;
}
/* Unsent composer draft for this topic — see shared/storage.js's getAllDrafts. */
.app-item-draft-dot {
    flex-shrink: 0;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--color-brand-text);
}
.app-item-preview {
    margin-top: 2px;
    color: var(--color-text-secondary);
    font-size: 13px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.app-item-preview em { font-style: normal; color: var(--color-text-faint); }

/* ── Graph/space switcher — pinned under the list pane's title row, first
   thing shown on boot (see switcher.js); sets which single registered graph
   the list/search/journal feed below scope to. Renders as a plain label
   (.app-switcher-single) rather than a <select> when there's only one graph
   registered — nothing to switch between yet. ── */
.app-switcher {
    padding: 0 20px 10px;
    background: var(--color-bg);
}
.app-switcher-empty { display: none; } /* no graphs yet — Manage graphs dialog is the way in */
.app-switcher-label {
    font-size: 12px;
    color: var(--color-text-secondary);
    font-weight: 600;
}
.app-switcher-select {
    width: 100%;
    padding: 6px 10px;
    border: 1px solid var(--color-input-border);
    border-radius: 6px;
    background: var(--color-input-bg);
    color: var(--color-text);
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
}
.app-switcher-select:focus { border-color: var(--color-input-border-focus); }

/* ── Journal pseudo-topic — pinned above the (scrollable) topic list, not part
   of it, so it stays put regardless of scroll position or search filtering;
   visually set apart from ordinary .app-item rows via a tinted background and
   accent border rather than the plain hover-highlight look. ── */
.app-item-journal {
    margin: 8px 10px;
    border-radius: 10px;
    background: var(--color-brand-tint);
    border: 1px solid var(--color-input-border);
}
.app-item-journal:hover { background: var(--color-bg-active); }
.app-item-journal.active { background: var(--color-bg-active); border-color: var(--color-input-border-focus); }
.app-item-journal .app-item-title { color: var(--color-brand-text); }

@media (max-width: 640px) {
    .app-item { margin: 1px 6px; padding: 10px; }
}

/* Search box pinned to the bottom of the Topics list — a sibling of
   .view-entries, not part of the scrolling list itself. */
.app-search-bar {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-shrink: 0;
    border-top: 1px solid var(--color-border);
    padding: 10px 14px;
    background: var(--color-bg);
}
.app-search-bar input { flex: 1; min-width: 0; width: auto; }

/* ── Generic dialog form body (e.g. Manage graphs) ── */
.dialog-form {
    display: flex;
    flex-direction: column;
    gap: 10px;
}
/* :not([type="checkbox"]) etc. - this is a text-field reset (full-width box, border, background);
   without the exclusion it also clobbers the composer's "keep original quality/filename"
   checkboxes (they live in a .dialog-form too), stretching each into a giant bordered text-input
   look-alike instead of a small checkbox — see .composer-checkbox-row below for their own styling. */
.dialog-form input:not([type="checkbox"]):not([type="radio"]) {
    width: 100%;
    padding: 7px 10px;
    border: 1px solid var(--color-input-border);
    border-radius: 6px;
    font-family: inherit;
    outline: none;
    background: var(--color-input-bg);
    color: var(--color-text);
    /* 16px, unconditionally — avoids iOS Safari zooming in on focus; this
       dialog isn't gated behind the 640px mobile breakpoint elsewhere. */
    font-size: 16px;
}
.dialog-form input:not([type="checkbox"]):not([type="radio"]):focus {
    border-color: var(--color-input-border-focus);
    background: var(--color-bg);
}

/* ── Manage graphs dialog body ── */
.manage-graphs-list { display: flex; flex-direction: column; gap: 8px; }
.manage-graphs-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 8px 10px;
    border: 1px solid var(--color-border);
    border-radius: 6px;
}
.manage-graphs-row-main { min-width: 0; }
.manage-graphs-path { color: var(--color-text-secondary); font-size: 12px; }
.manage-graphs-status { font-size: 12px; color: var(--color-text-secondary); }
.manage-graphs-status-error { color: #d64545; }
/* "⚠ No access" status, when clickable (opens the access-token dialog — see app-view.js's
   onAuthNeeded wiring) rather than a plain, non-actionable error message. */
.manage-graphs-status-btn {
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    font-size: 12px;
    color: #d64545;
    text-decoration: underline;
    cursor: pointer;
}
.manage-graphs-add { display: flex; flex-direction: column; gap: 8px; }

/* ── Access token dialog body ── */
.auth-dialog-hint { margin: 0; color: var(--color-text-secondary); font-size: 13px; }
.auth-dialog-input-row { display: flex; align-items: center; gap: 6px; }
.auth-dialog-input-row input { flex: 1; min-width: 0; }
.auth-dialog-error { color: #d64545; font-size: 13px; }

/* ── Scan setup code dialog body ── */
.qr-setup-hint { margin: 0; color: var(--color-text-secondary); font-size: 13px; }
.qr-setup-video {
    display: block;
    width: 100%;
    aspect-ratio: 1 / 1;
    max-height: 60vh;
    object-fit: cover;
    background: #000;
    border-radius: 6px;
}
.qr-setup-video[hidden] { display: none; }
.qr-setup-camera-status { margin: 0; color: var(--color-text-secondary); font-size: 13px; }
.qr-setup-camera-status:empty { display: none; }
.qr-setup-paste-row { display: flex; align-items: center; gap: 6px; }
.qr-setup-paste-row input { flex: 1; min-width: 0; }
.qr-setup-error { color: #d64545; font-size: 13px; }
.qr-setup-result { color: var(--color-text-secondary); font-size: 13px; }

/* Topic Page rendered above the messages thread — plain block, not a bubble */
.app-page-contents {
    padding: 16px 20px 20px;
    border-bottom: 1px solid var(--color-border);
    margin-bottom: 6px;
}

/* ── Logseq block outline (page contents + individual messages) ── */
/* `ul.lq-outline` (type+class) matches .note-body's own `.note-body ul` list-style/padding
   reset in specificity; being declared later here, it wins the tie and actually takes effect,
   instead of just losing silently to the more generic message-body list styling above. Real
   `list-style` markers are used rather than a hand-rolled ::before: marked.parse() wraps even a
   plain one-line block's text in a <p>, and an inline ::before bullet placed before that
   block-level <p> gets shoved onto its own line by the CSS anonymous-box rules instead of sitting
   next to the text. Native markers don't have that problem, at any nesting depth — each level's
   own padding-left simply stacks on its parent's, so deeper outlines indent further for free. A
   message's own root body is never wrapped in an <li> at all (see renderBlockTreeHTML), so it
   never gets a marker and needs no separate "root" exception. */
ul.lq-outline { list-style: disc; margin: 2px 0 0; padding-left: 1.2em; }
.lq-block::marker { color: var(--color-text-faint); }
/* Only used as a fallback when an embed's filename couldn't be resolved or there's no graph
   urlPrefix to build a URL from (see render.js's renderAssetHTML) — the normal case renders a
   real <img>/download link instead, styled below. */
.lq-asset-ref { color: var(--color-text-faint); font-style: italic; }

/* Real asset embeds (![[file]] / ![alt](path), see render.js) — capped height/width so a large
   photo doesn't blow out the message/page layout; `.note-body img`'s own `max-width: 100%` rule
   (above) still applies too, this just adds the height cap and a bit of visual polish on top. */
.lq-asset-img {
    display: block;
    max-width: 100%;
    max-height: 320px;
    border-radius: 8px;
    margin: 4px 0;
    object-fit: contain;
    cursor: zoom-in; /* opens full-screen with zoom on click/tap — see ui/image-lightbox.js */
}
.lq-asset-link {
    display: inline-block;
    margin: 2px 0;
    color: inherit;
    text-decoration: underline;
}

/* GPX route-preview map (ui/gpx-map.js builds the actual Leaflet map into `.lq-gpx-map-canvas`
   once this scrolls into view; see render.js's renderAssetHTML for the markup). Fixed canvas
   height, unlike `.lq-asset-img`'s max-height cap, since a map needs a real size up front to lay
   tiles into — Leaflet fits the *view* (via fitBounds) to the track, not the box to the view. */
.lq-gpx-map {
    display: block;
    margin: 4px 0;
    border: 1px solid var(--color-border);
    border-radius: 8px;
    overflow: hidden;
    cursor: zoom-in; /* opens full-screen interactive map on click/tap — see ui/gpx-map.js */
}
.lq-gpx-map-canvas {
    width: 100%;
    height: 160px;
    background: var(--color-bg-alt);
}
.lq-gpx-map-label {
    padding: 4px 8px;
    font-size: 12px;
    color: var(--color-text-faint);
}
.lq-gpx-map-error { cursor: default; }
.lq-gpx-map-error .lq-gpx-map-canvas { display: none; }

/* Drawer summary (:LOGBOOK:/:END: etc.) — raw drawer lines never render at all (diverted out
   during parsing, see blocks.js); just a quiet one-line stand-in, same "deliberately inert,
   quietly present" register as .lq-asset-ref above. */
.lq-drawer { display: block; margin-top: 2px; color: var(--color-text-faint); font-size: 12px; }

/* Task marker badge (TODO/DOING/NOW/LATER/DONE/WAITING/CANCELED) + optional [#A/B/C] priority
   cookie — a small neutral badge, not full Logseq keyword coloring. SCHEDULED/DEADLINE render as
   their own small line with an emoji, entirely outside the marked/masking pipeline. */
.lq-marker {
    display: inline-block;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.02em;
    padding: 1px 6px;
    margin: 0 6px 2px 0;
    border-radius: 4px;
    background: var(--color-bg-alt);
    color: var(--color-text-secondary);
    vertical-align: middle;
}
.lq-marker-priority { opacity: 0.7; font-weight: 600; }
.lq-schedule { display: block; margin-top: 2px; font-size: 12px; color: var(--color-text-secondary); }
/* Cheap "this is settled" cue for a DONE/CANCELED block's own marker/text/drawer-summary — not
   its children, which render as a sibling <ul class="lq-outline"> outside this div. */
.lq-block-body.lq-block-done { opacity: 0.6; }

/* Trailing signifier on a message that carries an edited:: true property (see main.go's PUT
   handler + topics.js's `edited` field) — appended after .note-body's message.html in
   app-view.js, deliberately outside render.js's own sanitized/purified markup, so it's never a
   sibling the DOMPurify pipeline has to reason about. Same quiet-metadata register as
   .lq-schedule/.lq-drawer above. */
.note-edited { font-size: 12px; color: var(--color-text-faint); }

/* ── Day divider between journal-derived messages spanning multiple days ── */
.app-day-divider {
    text-align: center;
    color: var(--color-text-faint);
    font-size: 12px;
    font-weight: 600;
    margin: 10px 0 4px;
}

/* ── Zulip-style topic header above a run of consecutive same-topic Journal entries (see
   app-view.js's _buildJournalTopicHeader) — clickable, links to that topic's own thread. Lives as
   the first child of .app-journal-topic-run below, not free-standing. */
.app-journal-topic-header {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    padding: 8px 20px 2px;
}
.app-journal-topic-pill {
    display: inline-block;
    padding: 2px 10px;
    border-radius: 10px;
    background: var(--color-brand-tint);
    color: var(--color-brand-text);
    font-size: 12px;
    font-weight: 600;
    text-decoration: none;
}
.app-journal-topic-pill:hover { background: var(--color-bg-active); }

/* ── Wrapper around one topic header + the run of consecutive same-topic(s) messages below it
   (see app-view.js's _prependJournalBatch) — a single shared left border down the whole run
   instead of outlining each message individually, so it reads as one group at a glance without
   the Signal/Zulip "chat bubble" look. A topicless entry (no page reference) isn't part of any
   run — see _prependJournalBatch — and renders as a plain .note with no wrapper/border. */
.app-journal-topic-run {
    border-left: 3px solid var(--color-brand-bg);
    border-radius: 2px;
    margin: 10px 0;
}

/* ── Floating scroll-to-top/bottom buttons — fade in/out (Signal-style) ── */
.app-scroll-btn {
    position: fixed;
    right: 20px;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    border: none;
    background: var(--color-brand-overlay);
    color: var(--color-on-brand);
    font-size: 16px;
    cursor: pointer;
    opacity: 0;
    pointer-events: none;
    transform: translateY(4px);
    transition: opacity 0.15s ease, transform 0.15s ease;
    z-index: 20;
}
.app-scroll-btn.visible { opacity: 1; pointer-events: auto; transform: translateY(0); }
.app-scroll-top { top: 64px; }
.app-scroll-bottom { bottom: 100px; }

/* Topics list's own scroll-to-top/bottom buttons — pinned to the left
   instead of the thread pane's right, so on desktop (both panes visible
   side by side, list pinned left) the two button pairs never overlap. */
.app-list-scroll-btn { left: 20px; right: auto; }
.app-list-scroll-top { top: 64px; }
.app-list-scroll-bottom { bottom: 90px; }

/* ── Deep-linkable messages: tap the ¶ to copy a link to it ── */
.note-link-btn {
    background: none;
    border: none;
    cursor: pointer;
    color: var(--color-text-faint);
    font-size: 12px;
    padding: 0 2px;
}
.note-link-btn:hover { color: var(--color-text-secondary); }

@keyframes note-highlight-flash {
    0%, 100% { background: transparent; }
    30% { background: var(--color-brand-tint); }
}
.note-highlight { animation: note-highlight-flash 2s ease; }

/* ── Bottom "compose" bar (topic thread pane + Journal feed) — chat-app-style entry point that
   replaces the old ✎ header icon button. Never a real input (see app-view.js's
   _buildComposerBar) — clicking it opens the Composer dialog, prefilled/locked for an open topic
   or, on the Journal feed, the same plain dialog the list pane's "+" button opens; shows the
   relevant draft text in place of a placeholder when one exists (.has-draft). Hidden only when
   neither a topic nor the Journal feed is open (see _updateComposerBar) — the explicit [hidden]
   override below is required because `display: block` on the plain class would otherwise beat the
   UA stylesheet's `[hidden] { display: none }` (same specificity, and author styles always win
   over UA styles at equal specificity), silently defeating the `hidden` attribute. ── */
.app-composer-bar {
    display: block;
    width: 100%;
    flex-shrink: 0;
    border: none;
    border-top: 1px solid var(--color-border);
    background: var(--color-bg);
    padding: 12px 20px;
    text-align: left;
    font-family: inherit;
    font-size: 14px;
    color: var(--color-text-faint);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    cursor: pointer;
}
.app-composer-bar[hidden] { display: none; }
.app-composer-bar:hover { background: var(--color-bg-alt); }
.app-composer-bar.has-draft { color: var(--color-text); }

/* ── Composer dialog ── */
/* Wider than the default 300px .dialog (see shared/dialog.js) — a textarea + file picker need
   more room than a settings-style button list/short form. */
.dialog.composer-dialog { max-width: 460px; }
.composer-form { gap: 14px; }
.composer-field { display: flex; flex-direction: column; gap: 4px; }
.composer-label { font-size: 12px; font-weight: 600; color: var(--color-text-secondary); }
/* Locked topic (prefilled opens — see composer.js) — deliberately styled as inert text, not an
   input, since it can't be edited: no border/background implying it's interactive. */
.composer-topic-locked {
    padding: 7px 0;
    font-size: 15px;
    font-weight: 600;
    color: var(--color-text);
}
.composer-textarea {
    width: 100%;
    padding: 8px 10px;
    border: 1px solid var(--color-input-border);
    border-radius: 6px;
    font-family: inherit;
    font-size: 16px; /* avoids iOS Safari zoom-on-focus, same reasoning as .dialog-form input */
    outline: none;
    resize: vertical;
    background: var(--color-input-bg);
    color: var(--color-text);
}
.composer-textarea:focus { border-color: var(--color-input-border-focus); background: var(--color-bg); }
/* Attachments is a <details> disclosure (see composer.js), collapsed by default — same native
   triangle-toggle convention as .dialog-group summary (above), just re-sized to match
   .composer-label's own font rather than reusing .dialog-group verbatim. */
.composer-attachments-summary {
    cursor: pointer;
    list-style: none;
    padding: 2px 4px;
    margin: 0 -4px;
    border-radius: 5px;
}
.composer-attachments-summary::-webkit-details-marker { display: none; }
.composer-attachments-summary:hover { background: var(--color-brand-tint); }
.composer-attachments-summary::before { content: '▸ '; }
.composer-attachments[open] > .composer-attachments-summary::before { content: '▾ '; }
/* Same 4px label-to-content gap .composer-field's flex layout gives every other field — lives on
   this inner wrapper, not on <details> itself (see composer.js's doc comment on why). */
.composer-attachments-body { margin-top: 4px; }
.composer-file-input { font-size: 13px; color: var(--color-text-secondary); }
.composer-file-list { display: flex; flex-direction: column; gap: 4px; }
.composer-file-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 4px 8px;
    background: var(--color-bg-alt);
    border-radius: 5px;
    font-size: 12px;
}
.composer-file-name { overflow-wrap: anywhere; min-width: 0; }
.composer-file-remove {
    flex-shrink: 0;
    background: none;
    border: none;
    cursor: pointer;
    font-size: 15px;
    line-height: 1;
    color: var(--color-text-secondary);
    padding: 0 2px;
}
.composer-file-remove:hover { color: #d64545; }
/* "Keep original quality"/"Keep original filename" — only shown once at least one attached file
   is an image (see composer.js's renderFileList; both options are no-ops for non-image files). */
.composer-file-options { display: flex; flex-direction: column; gap: 2px; margin-top: 4px; }
.composer-checkbox-row {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    padding: 6px 4px;
    margin: 0 -4px; /* offsets the padding above so the row's *text* still lines up with its
                       siblings (file list, error text) instead of looking indented */
    border-radius: 6px;
    font-size: 13px;
    line-height: 20px; /* matches the checkbox's own height below, so single- and multi-line
                           labels both stay vertically centered against it */
    color: var(--color-text);
    cursor: pointer;
}
.composer-checkbox-row:hover { background: var(--color-bg-alt); }
.composer-checkbox-row input[type="checkbox"] {
    flex-shrink: 0; /* never let a wrapping label line squeeze the box itself down */
    width: 20px;
    height: 20px;
    margin: 0;
    accent-color: var(--color-brand-bg);
    cursor: pointer;
}
.composer-error { color: #d64545; font-size: 13px; }
.composer-submit-btn { align-self: flex-start; padding: 7px 20px; font-size: 13px; }

/* ── Topic combobox (composer's topic field) ── */
.topic-combobox { position: relative; }
.topic-combobox-input {
    width: 100%;
    padding: 7px 10px;
    border: 1px solid var(--color-input-border);
    border-radius: 6px;
    font-family: inherit;
    font-size: 16px; /* avoids iOS Safari zoom-on-focus */
    outline: none;
    background: var(--color-input-bg);
    color: var(--color-text);
}
.topic-combobox-input:focus { border-color: var(--color-input-border-focus); background: var(--color-bg); }
.topic-combobox-dropdown {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    max-height: 180px;
    overflow-y: auto;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: 6px;
    box-shadow: 0 6px 20px var(--color-shadow);
    z-index: 10;
}
.topic-combobox-option {
    display: block;
    width: 100%;
    text-align: left;
    padding: 7px 10px;
    border: none;
    background: none;
    font-size: 13px;
    font-family: inherit;
    color: var(--color-text);
    cursor: pointer;
}
.topic-combobox-option:hover { background: var(--color-brand-tint); }

/* ── Image lightbox (ui/image-lightbox.js) ── */
/* Full-bleed viewer, deliberately separate from .dialog-backdrop/.dialog (shared/dialog.js) —
   that's a centered, bounded-width panel meant for menus/forms; this needs the whole viewport. */
.lq-lightbox-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 1000;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.92);
}
.lq-lightbox-backdrop.show { display: flex; }
.lq-lightbox-img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    touch-action: none; /* pointer events drive pan/zoom ourselves — see image-lightbox.js */
    user-select: none;
    -webkit-user-drag: none;
}
/* Only eased while settling after a wheel/double-tap zoom — not during an active drag/pinch,
   where a transition would lag behind the pointer instead of tracking it 1:1. */
.lq-lightbox-img-snap { transition: transform 0.15s ease-out; }
.lq-lightbox-close {
    position: absolute;
    top: max(12px, env(safe-area-inset-top));
    right: max(12px, env(safe-area-inset-right));
    width: 40px;
    height: 40px;
    border: none;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.15);
    color: #fff;
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
}
.lq-lightbox-close:hover { background: rgba(255, 255, 255, 0.28); }
/* Suppress the app's own scroll while the lightbox is open (set/cleared by
   ui/image-lightbox.js's open()/close(), and also shared by the GPX lightbox below). */
html.lq-lightbox-open, html.lq-lightbox-open body { overflow: hidden; }

/* ── GPX lightbox (ui/gpx-map.js) ── */
/* Same full-bleed-overlay shape as the image lightbox above, but a header bar replaces the single
   corner close button since the map itself (unlike the image) fills the entire remaining space
   edge-to-edge, leaving nowhere else to show which file is open. */
.lq-gpx-lightbox-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 1000;
    flex-direction: column;
    background: var(--color-bg);
}
.lq-gpx-lightbox-backdrop.show { display: flex; }
.lq-gpx-lightbox-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: max(10px, env(safe-area-inset-top)) 12px 10px;
    background: var(--color-brand-bg);
    color: var(--color-on-brand);
}
.lq-gpx-lightbox-title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 14px;
}
.lq-gpx-lightbox-close {
    flex: none;
    width: 32px;
    height: 32px;
    border: none;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.15);
    color: inherit;
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
}
.lq-gpx-lightbox-close:hover { background: rgba(255, 255, 255, 0.28); }
.lq-gpx-lightbox-map { flex: 1; }
