:root {
  --bg: #f7f7f5;
  --card: #ffffff;
  --text: #1a1a1a;
  --muted: #6b6b6b;
  /* Deep violet rather than the space theme's #7c4dff: it reads as the same
     family but holds 6.8:1 on white, so small text (chips, links) stays as
     legible as the blue it replaces. The bright gradient is for filled
     surfaces only, where the text sits on the colour instead of beside it. */
  --accent: #5b3fd1;
  /* The space screen's own #7c4dff -> #3f8cff is too light here: it sits on
     near-black there, but these fills carry white text, and #3f8cff only gives
     it 3.3:1. These deeper stops keep the violet-to-blue fade at ~6.2:1. */
  --accent-grad: linear-gradient(135deg, #6a3fd9, #3559c4);
  --border: #e2e2de;
  --danger: #b3372f;
  --chip: #eef2f8;
}

* { box-sizing: border-box; }

body {
  margin: 0 auto;
  max-width: 860px;
  padding: 2rem 1rem 4rem;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
  line-height: 1.5;
}

h1 { font-size: 1.6rem; margin: 0 0 1rem; }

.hidden { display: none !important; }

/* --- forms & inputs --- */

input, select, textarea, button {
  font: inherit;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.45rem 0.6rem;
  background: var(--card);
  color: var(--text);
}

input:focus, textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; }

button {
  cursor: pointer;
  background: var(--accent);
  color: #fff;
  border-color: var(--accent);
}
button:hover { filter: brightness(1.1); }
button:disabled { opacity: 0.6; cursor: wait; }
button.secondary {
  background: var(--card);
  color: var(--accent);
}

#add-form { display: flex; gap: 0.5rem; }
#add-input { flex: 1; }
/* Filled surfaces get the space theme's gradient: enough area for the fade to
   read, and white on it clears 4.8:1. Text-on-white keeps the solid --accent. */
#add-btn { background: var(--accent-grad); border-color: transparent; }

#manual-form {
  display: grid;
  gap: 0.5rem;
  margin-top: 0.75rem;
  padding: 0.9rem;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.manual-actions { display: flex; gap: 0.5rem; }

/* A toast, not a line in the header. It has to read on the light library and on
   the near-black latent field alike, so it carries its own dark surface rather
   than inheriting either background.

   z-index 400 puts it above the latent and Exploration layers (50), the
   settings dialog (100) and the confirm modal (300) — a failure raised by a
   dialog has to be visible over that dialog. Only the sign-in gate (1000) wins,
   which is right: nothing behind it should be talking to you. */
#message {
  position: fixed;
  z-index: 400;
  left: 50%;
  bottom: 1.4rem;
  transform: translateX(-50%);
  max-width: min(34rem, 92vw);
  margin: 0;
  padding: 0.6rem 1rem;
  border-radius: 999px;
  font-size: 0.9rem;
  text-align: center;
  background: rgba(22, 24, 45, 0.95);
  border: 1px solid rgba(255, 255, 255, 0.15);
  box-shadow: 0 6px 30px rgba(0, 0, 0, 0.35);
  color: #e9eaf5;
  /* The toast is feedback, never a target — clicking through it matters most
     in latent mode, where it floats over the field you are dragging. */
  pointer-events: none;
}
#message.success { color: #7ee0a0; }
#message.error { color: #ff8093; }

/* --- controls --- */

#controls {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
  margin: 0.4rem 0 1rem;
}

#add-form { margin-bottom: 0.4rem; }
#search { flex: 1; min-width: 220px; }

#tag-chips { display: flex; flex-wrap: wrap; gap: 0.35rem; width: 100%; }

.chip {
  background: var(--chip);
  color: var(--accent);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.15rem 0.65rem;
  font-size: 0.85rem;
}
.chip.active { background: var(--accent); color: #fff; }
.chip.small {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  cursor: default;
}
.chip-x {
  background: none;
  border: none;
  color: inherit;
  padding: 0;
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
}
.chip-x:hover { color: var(--danger); filter: none; }

/* --- cards --- */

.card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.9rem 1.1rem;
  margin-bottom: 0.8rem;
}
.card.flash {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(36, 86, 166, 0.2);
  transition: box-shadow 0.4s, border-color 0.4s;
}
/* Keyboard highlight while arrowing through search results. Deliberately not
   the .flash ring: that one is a fading "here it is" pulse, this one has to
   read as a steady cursor sitting on one card. A solid accent bar down the
   left edge plus a faint tint carries on the near-white card background. */
.card.keysel {
  border-color: var(--accent);
  border-left: 4px solid var(--accent);
  padding-left: calc(1.1rem - 3px);
  background: #f3f0fd;
}

.card-head {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
}
.card-head h2 { font-size: 1.05rem; margin: 0; flex: 1; font-weight: 600; }
.card-head a { color: var(--text); text-decoration: none; }
.card-head a:hover { color: var(--accent); text-decoration: underline; }
.card-head a { cursor: pointer; }
.card-head h2.title-toggle { cursor: pointer; }
.card-head h2.title-toggle:hover { color: var(--accent); }

.status { font-size: 0.85rem; padding: 0.2rem 0.4rem; }
.status-to_read { color: #8a6d1a; }
.status-reading { color: var(--accent); }
.status-read { color: #2c6e35; }

.meta { margin: 0.25rem 0 0.5rem; color: var(--muted); font-size: 0.9rem; }

.tag-row { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }
.tag-input {
  border: 1px dashed var(--border);
  border-radius: 999px;
  padding: 0.1rem 0.6rem;
  font-size: 0.85rem;
  width: 5.5rem;
  background: transparent;
}
.tag-input:focus { width: 10rem; }

.tag-edit {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  margin-bottom: 0.7rem;
}

.existing-toggle { cursor: pointer; border-style: dashed; background: transparent; }
.existing-toggle:hover { border-color: var(--accent); filter: none; }

.tag-picker {
  margin: -0.2rem 0 0.7rem;
  padding: 0.7rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
}
.tag-picker input { width: 100%; font-size: 0.88rem; }
.tag-picker-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-top: 0.55rem;
}
.tag-picker-list .chip { cursor: pointer; }
.picker-none { color: var(--muted); font-size: 0.85rem; }

/* --- folders --- */
#papers.folder-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 0.8rem;
}
.folder-tile {
  text-align: left; padding: 0.9rem 1.1rem; border-radius: 8px; cursor: pointer;
  background: var(--accent-grad); border-color: transparent;
}
/* opt out of the global button:hover brightness, like .init-annot does */
.folder-tile:hover { border-color: var(--accent); filter: none; }
.folder-tile .name { font-weight: 600; display: block; }
/* not --muted: dark grey on a coloured fill is ~1.1:1, effectively invisible */
.folder-tile .count { color: rgba(255, 255, 255, 0.8); font-size: 0.85rem; }
#papers.folder-grid .show-all { grid-column: 1 / -1; justify-self: start; font-size: 0.85rem; }

#folder-bar {
  display: flex; align-items: center; gap: 0.6rem;
  margin: 0.2rem 0 0.4rem; font-size: 0.9rem;
}
#folder-bar .crumb { color: var(--muted); }

.folder-row {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 0.35rem; margin-bottom: 0.7rem;
}
.folder-select { font-size: 0.85rem; padding: 0.1rem 0.4rem; }

.details { margin-top: 0.75rem; border-top: 1px solid var(--border); padding-top: 0.75rem; }
.abstract { font-size: 0.92rem; color: #333; margin: 0 0 0.75rem; }
.notes-label { font-size: 0.8rem; color: var(--muted); display: block; margin-bottom: 0.25rem; }
.notes { width: 100%; resize: vertical; font-size: 0.92rem; }

/* Rendered notes; click turns this back into the textarea above */
.notes-view {
  font-size: 0.92rem;
  white-space: pre-wrap;        /* notes keep their newlines */
  min-height: 2.4rem;
  padding: 0.35rem 0.5rem;
  border: 1px solid transparent;
  border-radius: 6px;
  cursor: text;
}
.notes-view:hover { border-color: var(--border); background: var(--bg); }
.notes-view:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.notes-view.empty { color: var(--muted); font-style: italic; }
.notes-view .katex-display { margin: 0.35em 0; }
/* markdown inside notes: real bullets rather than a literal "- " */
.notes-view ul, .notes-view ol { margin: 0.3em 0; padding-left: 1.35em; }
.notes-view li { margin: 0.15em 0; }
.notes-view p { margin: 0.3em 0; }
.notes-view > :first-child { margin-top: 0; }
.notes-view > :last-child { margin-bottom: 0; }
.notes-view code {
  background: var(--chip); border-radius: 4px; padding: 0.05em 0.3em; font-size: 0.9em;
}
.save-notes { margin-top: 0.4rem; font-size: 0.85rem; }

.pdf-badge {
  border: 1.5px solid var(--text);
  border-radius: 4px;
  padding: 0.05rem 0.4rem;
  color: var(--text) !important;
  text-decoration: none;
  font-weight: 600;
}
.pdf-badge:hover { background: var(--text); color: var(--card) !important; }
/* Draggable, so say so with the cursor rather than only in a tooltip. */
.pdf-badge[draggable="true"], .pdf-zone[draggable="true"] { cursor: grab; }
.pdf-badge[draggable="true"]:active, .pdf-zone[draggable="true"]:active { cursor: grabbing; }
/* The WebKit-only fallback: quieter than the PDF badge it sits beside. */
.pdf-badge.reveal { font-weight: 400; opacity: 0.65; margin-left: 0.3rem; }
.pdf-badge.reveal:hover { opacity: 1; }

.pdf-zone {
  margin-top: 0.7rem;
  border: 1px dashed var(--border);
  border-radius: 8px;
  padding: 0.6rem;
  text-align: center;
  color: var(--muted);
  font-size: 0.85rem;
}
.init-annot {
  display: block;
  margin-top: 0.5rem;
  width: 100%;
  background: var(--card);
  color: var(--accent);
  border: 1px solid var(--border);
  font-size: 0.85rem;
  padding: 0.45rem;
}
.init-annot:hover { border-color: var(--accent); filter: none; }
.init-annot.dragging { border-color: var(--accent); background: var(--chip); }
.init-annot:disabled { color: var(--muted); }

/* Once a PDF is attached, the drop box becomes a simple open button */
.pdf-zone.has-pdf {
  display: inline-block;
  margin-left: 0.8rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--card);
  color: var(--accent);
  cursor: pointer;
  padding: 0.35rem 0.8rem;
  font-weight: 500;
}
.pdf-zone.has-pdf:hover { border-color: var(--accent); background: var(--chip); }
.pdf-zone.dragging { border-color: var(--accent); background: var(--chip); }

.card-foot {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  margin-top: 0.6rem;
  font-size: 0.85rem;
}
.card-foot a { color: var(--accent); }
.spacer { flex: 1; }
.added { color: var(--muted); font-size: 0.8rem; }

.linklike {
  background: none;
  border: none;
  color: var(--accent);
  padding: 0;
}
.linklike:hover { text-decoration: underline; filter: none; }
.linklike.danger { color: var(--danger); }

.no-results, #empty-state { color: var(--muted); text-align: center; margin-top: 2rem; }

/* --- home / drop view --- */

#home-view {
  min-height: 80vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.home-center { text-align: center; width: 100%; max-width: 480px; }

/* The name is user-settable, and the space theme letterspaces it, so no fixed
   size fits every length. It stays on one line — a wrapped title reads as a
   bug — and fitLibraryTitle() in app.js shrinks it until it fits. The clamp is
   only what shows for the instant before that runs. */
.home-title {
  font-size: clamp(1.1rem, 5.5vw, 2rem);
  margin-bottom: 2rem;
  white-space: nowrap;
  /* the 11px floor in fitLibraryTitle() can still be too wide for a 60-char
     name in a narrow panel; truncate rather than spill out of the window */
  overflow: hidden;
  text-overflow: ellipsis;
  /* that overflow:hidden clips at the padding edge, and the logo dot's glow
     reaches ~1.2em past the dot itself — without this gutter a title long
     enough to fill the line would have its halo sliced off */
  padding: 0 0.4em;
}

.big-btn {
  display: block;
  width: 100%;
  padding: 1.1rem;
  font-size: 1.1rem;
  border-radius: 10px;
  background: var(--card);
  color: var(--text);
  border: 1px solid var(--border);
  text-decoration: none;
  text-align: center;
  cursor: pointer;
}
.big-btn:hover { border-color: var(--accent); color: var(--accent); filter: none; }

#drop-zone {
  border: 2px dashed var(--border);
  border-radius: 12px;
  padding: 2.5rem 1.5rem;
  background: var(--card);
  transition: border-color 0.15s, background 0.15s;
}
#drop-zone.dragging { border-color: var(--accent); background: var(--chip); }

.drop-hint { color: var(--muted); margin: 0 0 1.2rem; }

.drop-label {
  color: var(--muted);
  margin: 0 0 0.9rem;
  font-size: 0.85rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
body.space .drop-label { color: rgba(255, 255, 255, 0.55); }

/* --- quick search panel (home) --- */

#search-panel {
  margin-bottom: 1rem;
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1.3rem 1.5rem;
  background: var(--card);
}
#quick-search { width: 100%; }

#quick-results { text-align: left; margin-top: 0.6rem; }

.quick-item {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.8rem;
  padding: 0.45rem 0.2rem;
  text-decoration: none;
  border-radius: 6px;
}
.quick-item:hover, .quick-item.selected { background: var(--chip); }
.quick-title {
  color: var(--text);
  font-size: 0.92rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.quick-meta { color: var(--muted); font-size: 0.8rem; white-space: nowrap; }
.quick-none { color: var(--muted); font-size: 0.9rem; margin: 0.4rem 0 0; }

body.space #search-panel {
  background: rgba(255, 255, 255, 0.04);
  border-color: rgba(255, 255, 255, 0.2);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}
body.space .quick-item:hover, body.space .quick-item.selected { background: rgba(124, 77, 255, 0.15); }
body.space .quick-title { color: #fff; }
body.space .quick-meta { color: rgba(255, 255, 255, 0.5); }
body.space .quick-none { color: rgba(255, 255, 255, 0.5); }

#drop-form { display: flex; gap: 0.5rem; }
#drop-input { flex: 1; }

/* Explore, under the input: plain underlined text, not a button. The drop zone
   already has one filled button, and a second box competes with it — the
   underline is what carries "clickable" instead. The space rules sit here
   rather than in the theme block so both halves stay together; #drop-zone is
   only ever seen under body.space. */
/* Side by side. Wraps rather than squeezing, because the drop zone also has to
   survive the 560pt panel. */
.drop-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 1.1rem;
  margin-top: 0.6rem;
}
#drop-explore, #drop-manual {
  display: block;
  width: auto;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  font-size: 0.82rem;
  color: var(--muted);
  text-decoration: underline;
  cursor: pointer;
}
#drop-explore:hover, #drop-manual:hover { color: var(--accent); filter: none; }
body.space #drop-explore, body.space #drop-manual { color: rgba(255, 255, 255, 0.55); }
body.space #drop-explore:hover, body.space #drop-manual:hover { color: #fff; filter: none; }

#drop-msg { margin: 0.8rem 0 0; font-size: 0.9rem; }
#drop-msg a { color: var(--accent); }
body.space #drop-msg a { color: #a99cff; }

/* --- "paper saved" toast before auto-close --- */

#save-toast {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(5, 6, 15, 0.6);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  animation: toast-in 0.2s ease-out;
}
#save-toast span {
  padding: 1rem 2.2rem;
  border-radius: 999px;
  background: rgba(22, 24, 45, 0.95);
  border: 1px solid rgba(255, 255, 255, 0.15);
  box-shadow: 0 0 50px rgba(124, 77, 255, 0.3);
  color: #fff;
  font-size: 1.1rem;
  letter-spacing: 0.04em;
}
@keyframes toast-in {
  from { opacity: 0; transform: scale(0.96); }
  to   { opacity: 1; transform: scale(1); }
}

/* --- themed confirm modal --- */

#modal-overlay, #settings-overlay, #explore-overlay, #manual-overlay, #zotero-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;      /* vertically centered */
  justify-content: center;
  background: rgba(5, 6, 15, 0.55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

#modal, #settings-panel, #explore-panel, #manual-panel, #zotero-panel {
  width: min(420px, 90vw);
  /* Scroll rather than spill: the scrape pane is tall, and on a short window
     its buttons were simply off-screen with no way to reach them. */
  max-height: 86vh;
  overflow-y: auto;
  padding: 1.6rem 1.8rem;
  border-radius: 14px;
  background: rgba(22, 24, 45, 0.92);
  border: 1px solid rgba(255, 255, 255, 0.15);
  box-shadow: 0 0 60px rgba(124, 77, 255, 0.25);
  color: #e9eaf5;
  text-align: center;
}

#modal-text {
  margin: 0 0 1.3rem;
  white-space: pre-line;   /* keep the blank-line breaks in messages */
  font-size: 0.95rem;
}

/* smaller disclaimer line under the main confirm message */
#modal-sub {
  margin: -0.9rem 0 1.3rem;  /* pull up toward the main message */
  font-size: 0.8rem;
  color: var(--muted);
}

#modal-buttons, /* Wraps, because a row that silently overflows puts its buttons outside the
   panel where they cannot be clicked — which is what three of these did. */
.settings-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  justify-content: center;
  margin-top: 0.4rem;
}

#modal-buttons button, .settings-buttons button {
  min-width: 4.5rem;
  border-radius: 999px;
  padding: 0.45rem 0.85rem;
  background: rgba(255, 255, 255, 0.06);
  color: #e9eaf5;
  border: 1px solid rgba(255, 255, 255, 0.18);
}
#modal-buttons button:hover, .settings-buttons button:hover { border-color: #8f7bff; filter: none; }

#modal-buttons #modal-ok, .settings-buttons #settings-save, .settings-buttons #explore-go {
  background: linear-gradient(135deg, #7c4dff, #3f8cff);
  border-color: transparent;
  color: #fff;
}
#modal-buttons #modal-ok:hover, .settings-buttons #settings-save:hover,
.settings-buttons #explore-go:hover { filter: brightness(1.15); }

/* settings options menu */
.settings-option {
  display: block;
  width: 100%;
  padding: 0.85rem 1rem;
  margin-bottom: 0.6rem;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.15);
  color: #e9eaf5;
  text-align: left;
}
.settings-option:hover {
  border-color: rgba(150, 135, 255, 0.8);
  box-shadow: 0 0 20px rgba(124, 77, 255, 0.3);
  filter: none;
}

/* The confirm dialog must paint above the settings dialog */
#modal-overlay { z-index: 300; }

/* settings-specific bits */
#gn-decouple {
  background: rgba(255, 128, 147, 0.12);
  border: 1px solid rgba(255, 128, 147, 0.5);
  color: #ff8093;
  border-radius: 999px;
  padding: 0.45rem 1.1rem;
}
#gn-decouple:hover { border-color: #ff8093; filter: none; }
/* Decoupling reads the same whichever service it is, so it looks the same. */
#zot-decouple {
  background: rgba(255, 128, 147, 0.12);
  border: 1px solid rgba(255, 128, 147, 0.5);
  color: #ff8093;
  border-radius: 999px;
  padding: 0.45rem 1.1rem;
}
#zot-decouple:hover { border-color: #ff8093; filter: none; }
#settings-panel h2 { margin: 0 0 1rem; font-weight: 300; letter-spacing: 0.08em; }
#settings-panel h3 { margin: 0 0 0.5rem; font-size: 1rem; }
.settings-text { color: rgba(255, 255, 255, 0.65); font-size: 0.85rem; margin: 0 0 0.9rem; }

/* Latent framing sliders. The value sits in the label rather than beside the
   track: you are watching the field, not the number, and a figure that jumps
   around under the thumb pulls the eye back off it. */
/* The framing pane previews itself: it raises latent mode behind the dialog, so
   the overlay stops behaving like a modal. The scrim and the 6px blur both go —
   they turned a 126px shift of dim points into something that read as nothing
   happening — and the panel steps left, since the field sits right of centre. */
#settings-overlay.peek {
  background: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  justify-content: flex-start;
  padding-left: 2.5rem;
}
/* No scrim separating panel from field now, so the panel earns its own edge. */
#settings-overlay.peek #settings-panel {
  background: rgba(12, 13, 28, 0.96);
  box-shadow: 0 0 40px rgba(0, 0, 0, 0.6), 0 0 60px rgba(124, 77, 255, 0.25);
}

/* Two dropdowns in one pane, so each needs a label and a line saying what it
   actually drives — "Sparring agent" and "Read PDF authors" are not obviously
   different jobs from the model names alone. */
.ai-l {
  display: block;
  font-size: 0.8rem;
  color: rgba(255, 255, 255, 0.65);
  margin: 0.9rem 0 0.3rem;
}
.ai-note { margin: 0.35rem 0 0; font-size: 0.78rem; }
#index-coverage { margin: 0.6rem 0 0; font-size: 0.82rem; color: #7ee787; }
#index-coverage.warn { color: #ffd166; }

/* Topic checkboxes: two columns, so eight of them don't push the Scrape button
   off the bottom of a 560pt panel. */
.index-topics {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.15rem 0.6rem;
  text-align: left;
  margin-top: 0.2rem;
}
.index-topics label {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.8rem;
  color: rgba(255, 255, 255, 0.75);
  cursor: pointer;
}
.index-topics input { accent-color: #7c4dff; margin: 0; }
#index-progress { font-variant-numeric: tabular-nums; min-height: 1.2em; }
#index-msg { min-height: 1.2em; font-size: 0.85rem; color: #7ee787; margin: 0.4rem 0 0; }
#index-msg.error { color: #ff8093; }

/* The scrape's progress bar. Deliberately a view of the server's job rather
   than of this pane's visit — it fills from wherever the job actually is, so
   reopening the pane mid-scrape shows the truth, not a restart. */
#index-floor-box { margin-top: 0.9rem; }
#index-floor-note { min-height: 2.4em; }

#index-trim { margin-top: 0.9rem; }
#trim-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
  margin: 0.35rem 0 0.2rem;
}
#trim-table th {
  font-weight: 500;
  color: rgba(255, 255, 255, 0.45);
  text-align: right;
  padding: 0.1rem 0.4rem;
}
#trim-table th:first-child, #trim-table td:first-child { text-align: left; }
#trim-table td {
  text-align: right;
  padding: 0.14rem 0.4rem;
  color: rgba(255, 255, 255, 0.8);
  border-top: 1px solid rgba(255, 255, 255, 0.07);
}
/* The rows are the coarse picker, so they have to look pressable. */
.trim-row { cursor: pointer; }
.trim-row:hover td { background: rgba(124, 77, 255, 0.16); color: #fff; }
/* The chosen row is the cut, so it has to stay visible after the pointer leaves. */
.trim-row.selected td {
  background: rgba(124, 77, 255, 0.3);
  color: #fff;
  border-top-color: rgba(150, 135, 255, 0.5);
}
#trim-scale { font-size: 0.72rem; }
#index-covered { font-size: 0.78rem; margin: 0.1rem 0 0.5rem; }
#scan-scope { margin-top: -0.35rem; }
/* Same treatment as #scan-paper-search above it. `appearance: none` is the part
   that matters: without it macOS keeps drawing its own white control whatever
   background you give it — so the arrow has to be drawn here instead, inline so
   the strict no-external-requests rule still holds. */
#scan-window {
  width: 100%;
  max-width: var(--scan-field);
  font-size: 0.85rem;
  padding: 0.32rem 0.55rem;
  margin: 0.15rem 0 0.1rem;
  appearance: none;
  -webkit-appearance: none;
  background-color: rgba(255, 255, 255, 0.06);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6'><path d='M1 1l4 4 4-4' fill='none' stroke='rgba(255,255,255,0.45)' stroke-width='1.5' stroke-linecap='round'/></svg>");
  background-repeat: no-repeat;
  background-position: right 0.55rem center;
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: #fff;
  padding-right: 1.6rem;
  cursor: pointer;
}
#scan-window:hover { border-color: rgba(150, 135, 255, 0.55); }
/* Showing its own name rather than a choice — same grey as the search box's
   placeholder, so the two read as the same kind of empty. */
#scan-window.unset { color: rgba(255, 255, 255, 0.35); }
/* The open menu is drawn by the OS, so its own colours have to be set too. */
#scan-window option { background: #1b1b2b; color: #fff; }
#scan-window-note { margin: 0 0 0.5rem; font-variant-numeric: tabular-nums; }
#trim-note { min-height: 2.4em; }
#trim-go {
  font-size: 0.8rem;
  padding: 0.28rem 0.9rem;
  border-color: rgba(255, 128, 147, 0.45);
  color: #ff8093;
}
#trim-go:disabled { opacity: 0.35; border-color: rgba(255, 255, 255, 0.18); color: rgba(255, 255, 255, 0.4); }
.settings-text.warn { color: #ffc861; }

#index-bar {
  height: 4px;
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.12);
  overflow: hidden;
  margin: 0.6rem 0 0.35rem;
}
#index-bar i {
  display: block;
  height: 100%;
  width: 0;
  border-radius: 3px;
  background: linear-gradient(90deg, #7c4dff, #a98bff);
  transition: width 0.4s ease;
}
/* Waiting out a rate limit is not stalling, and should not look like it. */
#index-bar.waiting i {
  background: repeating-linear-gradient(115deg, #7c4dff 0 8px, #5a35c4 8px 16px);
}
#index-bar.failed i { background: #ff8093; }
#index-stop { color: #ff8093; }


.slider-l {
  display: block;
  font-size: 0.8rem;
  color: rgba(255, 255, 255, 0.65);
  margin: 0.7rem 0 0.25rem;
}
.slider-l span {
  color: #fff;
  font-variant-numeric: tabular-nums;   /* so 0.90 → 1.00 doesn't shift the label */
}
#settings-latent input[type="range"] {
  width: 100%;
  accent-color: #7c4dff;
  background: none;
  border: 0;
  padding: 0;
}
#lf-msg { min-height: 1.2em; font-size: 0.85rem; color: #7ee787; margin: 0.6rem 0 0; }
#lf-msg.error { color: #ff8093; }
/* Every text field in the settings panel. One rule rather than one per field:
   this was already copied twice, and the third field to be added arrived white
   because nobody remembered there was a copy to make. The browser's default
   input is light and the panel is not, so each of these has to say so — the
   rounded corners come from the base input rule and are left alone. */
#goodnotes-email,
#zotero-key,
#account-email {
  width: 100%;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: #fff;
}
#goodnotes-email::placeholder,
#zotero-key::placeholder,
#account-email::placeholder { color: rgba(255, 255, 255, 0.35); }
#zot-msg { min-height: 1.2em; font-size: 0.85rem; color: #7ee787; margin: 0.5rem 0 0.6rem; }
#zot-msg.error { color: #ff8093; }
/* The link out to zotero.org sits inside body text, where the panel's default
   link colour is too quiet to read as a link at all. */
#settings-zotero a { color: #a98bff; }

/* --- the Zotero import list ------------------------------------------------
   Grouped by folder, exactly as Zotero files it, and scrolled inside the panel
   rather than growing it: a library of forty papers would otherwise push the
   Decouple button and the footer off the bottom of the ⌥B panel entirely. */
/* Read-only is not a failure, so it is not styled as one — but it is the reason
   a push will refuse, so it does not read as ordinary prose either. */
#zot-access.warn { color: #ffcf8b; }

/* Wider than the 560px scrape panel because these rows are paper titles, and
   narrower than the window because it is still a dialog. The panel itself does
   not scroll — the list inside it does, so the header and Import all stay put
   instead of sliding away on the first flick. */
#zotero-panel {
  width: min(760px, 94vw);
  max-height: 86vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 1.3rem 1.4rem;
  text-align: left;
}
/* Everything above the list keeps its height; only the list gives. */
#zot-head, #zot-view-msg, #zot-report, #zot-progress { flex-shrink: 0; }

#zot-progress { margin: 0.7rem 0 0.2rem; }
#zot-progress-note { margin: 0 0 0.35rem; font-size: 0.75rem; color: rgba(255, 255, 255, 0.5); }
#zot-progress-count { margin: 0.3rem 0 0; font-size: 0.78rem; color: rgba(126, 231, 135, 0.9); }
#zot-bar {
  height: 6px;
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.12);
  overflow: hidden;
}
#zot-bar i {
  display: block;
  height: 100%;
  width: 0;
  border-radius: 3px;
  /* Green rather than the app's violet: this reports work already banked, and
     it is the same green the import messages use for the same reason. */
  background: linear-gradient(90deg, #4fbf6a, #7ee787);
  transition: width 0.3s ease;
}
#zot-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
#zot-head h2 { margin: 0; font-size: 1.2rem; font-weight: 500; }
#zot-head-sub { margin: 0.25rem 0 0; font-size: 0.85rem; color: rgba(255, 255, 255, 0.55); }
.zot-head-right { display: flex; align-items: center; gap: 0.9rem; }
#zot-view-msg { min-height: 1.2em; font-size: 0.85rem; color: #7ee787; margin: 0.6rem 0 0.5rem; }
#zot-view-msg.error { color: #ff8093; }
#zot-import-all {
  background: #7c4dff;
  border: 0;
  color: #fff;
  border-radius: 999px;
  padding: 0.4rem 1rem;
  font-size: 0.85rem;
}
/* Capped and scrolled: importing a whole library can name a dozen thin papers,
   and the list of what to import must not be pushed off the screen by the
   report of what already was. */
#zot-report {
  max-height: 22vh;
  overflow-y: auto;
  margin: 0 0 0.7rem;
  padding: 0.6rem 0.8rem;
  border-radius: 10px;
  border: 1px solid rgba(126, 231, 135, 0.35);
  background: rgba(126, 231, 135, 0.07);
  text-align: left;
}
.zot-report-head { font-size: 0.9rem; color: #b9f6c0; }
.zot-report-note { font-size: 0.8rem; color: rgba(255, 255, 255, 0.6); margin-top: 0.25rem; }
.zot-report-list {
  margin: 0.4rem 0 0;
  padding-left: 1.1rem;
  font-size: 0.8rem;
  color: rgba(255, 255, 255, 0.75);
}
.zot-report-list li { margin-bottom: 0.15rem; }
.zot-report-gap { color: rgba(255, 207, 139, 0.85); font-size: 0.72rem; }
.zot-report-got { color: rgba(126, 231, 135, 0.9); font-size: 0.72rem; }

#zot-list {
  flex: 1;
  min-height: 0;          /* or the flex child refuses to scroll and overflows */
  overflow-y: auto;
  text-align: left;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.03);
}
.zot-folder + .zot-folder { border-top: 1px solid rgba(255, 255, 255, 0.12); }
/* A group library is a shelf of its own, so it sits above the folder headings
   rather than beside them — and outranks them visually for the same reason. */
.zot-library {
  padding: 0.5rem 0.7rem 0.3rem;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: rgba(169, 139, 255, 0.85);
  border-top: 1px solid rgba(255, 255, 255, 0.12);
}
#zot-list > .zot-library:first-child { border-top: 0; }
/* Sticky so you always know which folder you are scrolling through — the whole
   point of grouping them is lost the moment the heading scrolls away. */
.zot-folder-head {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
  padding: 0.55rem 0.8rem;
  /* Opaque, and matched to the panel behind it: a translucent sticky header
     would show the rows sliding through it. */
  background: #1c1e32;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.zot-folder-name { font-size: 0.88rem; font-weight: 600; color: #e9eaf5; }
.zot-folder-name.unfiled { font-weight: 400; font-style: italic; color: rgba(255, 255, 255, 0.6); }
.zot-count { font-size: 0.72rem; color: rgba(255, 255, 255, 0.45); }
.zot-folder-import {
  margin-left: auto;
  font-size: 0.72rem;
  padding: 0.25rem 0.65rem;
  border-radius: 999px;
  background: rgba(124, 77, 255, 0.16);
  border: 1px solid rgba(150, 135, 255, 0.55);
  color: #c9b8ff;
}
.zot-row {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  padding: 0.55rem 0.8rem;
}
.zot-row:hover { background: rgba(255, 255, 255, 0.04); }
.zot-row + .zot-row { border-top: 1px solid rgba(255, 255, 255, 0.06); }
.zot-main { min-width: 0; flex: 1; }
.zot-title { font-size: 0.92rem; color: #e9eaf5; line-height: 1.35; }
.zot-meta { font-size: 0.72rem; color: rgba(255, 255, 255, 0.45); margin-top: 0.1rem; }
/* "Here already, but not filed here" — an offer, not a refusal, so it reads in
   the same amber as the incomplete-import notes rather than the grey of a row
   there is nothing to do about. */
.zot-gain { font-size: 0.72rem; color: rgba(255, 207, 139, 0.85); margin-top: 0.15rem; }
/* Imported and dismissed rows stay in place rather than vanishing, so the list
   does not jump under the cursor mid-scroll. */
.zot-row.done .zot-title, .zot-row.done .zot-meta { color: rgba(255, 255, 255, 0.35); }
.zot-state { font-size: 0.72rem; color: rgba(126, 231, 135, 0.75); white-space: nowrap; }
.zot-actions { display: flex; gap: 0.35rem; flex-shrink: 0; }
.zot-add, .zot-no {
  font-size: 0.72rem;
  padding: 0.25rem 0.6rem;
  border-radius: 999px;
  white-space: nowrap;
}
.zot-add { background: #7c4dff; border: 0; color: #fff; }
.zot-no {
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: rgba(255, 255, 255, 0.6);
}
/* In the ⌥B panel the row is ~430pt wide; below that the buttons drop under
   the title rather than squeezing it to two characters. */
@media (max-width: 460px) {
  .zot-row { flex-wrap: wrap; }
  .zot-actions { margin-left: auto; }
}
#settings-msg { min-height: 1.2em; font-size: 0.85rem; color: #7ee787; margin: 0.5rem 0 0.6rem; }
/* A touch larger than the 1.1rem it was, with a little padding for the hit
   area — still small enough to read as an accessory next to the title. */
#settings-btn { font-size: 1.35rem; line-height: 1; padding: 0.15rem 0.25rem; text-decoration: none; }
#settings-btn:hover { text-decoration: none; filter: brightness(1.3); }
#drop-msg.success { color: var(--muted); }
#drop-msg.error { color: var(--danger); }

/* --- manual add dialog -----------------------------------------------------
   A form on the same dark panel as settings and explore, so every field has to
   override the light-theme defaults the way #goodnotes-email does. One column
   at every width: this has to stay usable in the ~495pt ⌥B panel, where two
   columns would only ever be two half-columns. */

#manual-panel {
  width: min(520px, 92vw);
  text-align: left;
  max-height: 88vh;
  overflow-y: auto;
}
#manual-panel h2 { margin: 0 0 0.8rem; font-size: 1.15rem; }

/* Why the dialog opened by itself — only shown when it did */
#manual-why {
  margin: 0 0 0.9rem;
  padding: 0.6rem 0.7rem;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.14);
  font-size: 0.82rem;
  line-height: 1.45;
  color: rgba(255, 255, 255, 0.72);
}

.manual-l {
  display: block;
  margin: 0.85rem 0 0.3rem;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: rgba(255, 255, 255, 0.5);
}

#manual-panel input[type="text"],
#manual-panel input[type="number"],
#manual-panel select,
#manual-panel textarea {
  width: 100%;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: #fff;
}
#manual-panel ::placeholder { color: rgba(255, 255, 255, 0.35); }
#manual-panel textarea { resize: vertical; line-height: 1.45; }

.manual-row { display: flex; gap: 0.7rem; }
.manual-row > div { flex: 1; min-width: 0; }

#manual-drop {
  margin-top: 0.3rem;
  padding: 0.9rem;
  border: 1px dashed rgba(255, 255, 255, 0.3);
  border-radius: 10px;
  text-align: center;
  font-size: 0.85rem;
  color: rgba(255, 255, 255, 0.65);
  cursor: pointer;
}
#manual-drop:hover, #manual-drop.dragging {
  border-color: #8f7bff;
  background: rgba(124, 77, 255, 0.12);
  color: #fff;
}
#manual-drop.has-pdf {
  border-style: solid;
  border-color: rgba(126, 231, 135, 0.55);
  color: #b9f5c3;
}

#manual-embed {
  margin-top: 0.9rem;
  padding: 0.7rem 0.8rem;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.14);
}
.manual-check {
  display: flex; align-items: center; gap: 0.5rem;
  font-size: 0.88rem; cursor: pointer;
}
.manual-check input { width: auto; accent-color: #7c4dff; }
#manual-try-embed {
  margin-top: 0.6rem;
  border-radius: 999px;
  padding: 0.35rem 0.9rem;
  font-size: 0.82rem;
  background: rgba(255, 255, 255, 0.06);
  color: #e9eaf5;
  border: 1px solid rgba(255, 255, 255, 0.18);
}
#manual-try-embed:hover:not(:disabled) { border-color: #8f7bff; filter: none; }
#manual-try-embed:disabled { opacity: 0.4; cursor: not-allowed; }
#manual-embed-hint {
  margin: 0.55rem 0 0;
  font-size: 0.78rem;
  line-height: 1.4;
  color: rgba(255, 255, 255, 0.55);
}
/* The "it will be embedded from the title alone" warning — the one thing here
   worth colouring, since it is the difference between absent and wrong. */
#manual-embed-hint.warn { color: #ffc46b; }

#manual-msg {
  margin: 0.9rem 0 0;
  min-height: 1.15em;
  font-size: 0.83rem;
  color: rgba(255, 255, 255, 0.7);
}
#manual-msg.error { color: #ff8093; }

.settings-buttons #manual-save {
  background: linear-gradient(135deg, #7c4dff, #3f8cff);
  border-color: transparent;
  color: #fff;
}
.settings-buttons #manual-save:hover { filter: brightness(1.15); }

#drop-result {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 2rem 1.5rem;
}

.found-label { color: var(--muted); font-size: 0.85rem; margin: 0 0 0.3rem; }
/* The title is the way through to the paper itself. Underlined only on hover,
   because a permanently underlined heading reads as a form label. .nolink is
   for a paper that has no URL — a book, a manual entry — where it must look
   like the heading it has become rather than a link that does nothing. */
#found-title {
  display: block;
  font-size: 1.15rem;
  font-weight: 600;
  margin: 0 0 0.3rem;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}
#found-title:hover { text-decoration: underline; text-underline-offset: 3px; }
#found-title.nolink { cursor: default; text-decoration: none; }
#found-meta { color: var(--muted); font-size: 0.85rem; margin: 0 0 1.4rem; }
body.space #found-meta { color: rgba(255, 255, 255, 0.55); }
#found-meta:empty { display: none; }

/* whether a dragged-in PDF was attached — empty when none was dropped */

#drop-status-buttons {
  display: flex;
  gap: 0.5rem;
  justify-content: center;
  margin-bottom: 1.6rem;
}
.status-btn {
  background: var(--card);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.45rem 1.1rem;
}
.status-btn:hover { border-color: var(--accent); filter: none; }
.status-btn.selected {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent);
}

/* folder choice in the add pipeline */
#drop-folder-select { width: 100%; margin-bottom: 1rem; }
#drop-new-folder { width: 100%; margin-bottom: 1.4rem; }

/* Everything sits on one panel now, so the fields need to breathe and the
   labels need to sit against what they label rather than float mid-panel. */
#drop-result .found-label { margin-top: 1.2rem; }
#note-text { width: 100%; resize: vertical; text-align: left; }
#drop-result select, #drop-result input[type="text"] { width: 100%; }

/* The two ways out. Green keeps it, red bins it — the only place in the app
   that uses colour to mean the action rather than the state, because these are
   the two irreversible ones and they sit side by side. */
.drop-next { display: flex; flex-direction: column; gap: 0.6rem; margin-top: 1.6rem; }
.big-btn.add {
  background: #1f7a4d;
  border-color: #1f7a4d;
  color: #fff;
}
.big-btn.add:hover { background: #248f5a; border-color: #248f5a; color: #fff; }
.big-btn.remove {
  background: transparent;
  border-color: rgba(179, 55, 47, 0.55);
  color: var(--danger);
}
.big-btn.remove:hover {
  background: rgba(179, 55, 47, 0.1);
  border-color: var(--danger);
  color: var(--danger);
}
body.space .big-btn.add { background: #1e8f57; border-color: #1e8f57; }
body.space .big-btn.add:hover { background: #23a866; border-color: #23a866; }
body.space .big-btn.remove { color: #ff8093; border-color: rgba(255, 128, 147, 0.45); }
body.space .big-btn.remove:hover {
  background: rgba(255, 128, 147, 0.12);
  border-color: rgba(255, 128, 147, 0.7);
  color: #ff8093;
}

#tag-text { width: 100%; }
#recent-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  justify-content: center;
}
#recent-tags:empty { display: none; }
body.space #recent-tags .chip {
  background: rgba(255, 255, 255, 0.06);
  color: #e9eaf5;
  border-color: rgba(255, 255, 255, 0.18);
  cursor: pointer;
}
body.space #recent-tags .chip.active {
  background: linear-gradient(135deg, #7c4dff, #3f8cff);
  border-color: transparent;
  color: #fff;
}


.secondary-big { font-size: 0.95rem; padding: 0.7rem; }

/* --- the logo -------------------------------------------------------------
   One bright point, as if lifted out of the latent map: the library is a cloud
   of these, and the mark is a single one of them. Built entirely from
   gradients so it costs no request and no image asset, and sized in em so it
   tracks whatever type it sits beside — a 2rem home title and a 0.95rem button
   get proportionate dots from the same rule.

   It attaches two ways on purpose. As a ::before on the headings, because
   applyLibraryTitle() rewrites their textContent on every rename and a child
   element would not survive that; as an empty .logo-dot span inside buttons,
   whose labels are static markup. */
/* content: only on the pseudo-elements. Setting it on a real element replaces
   that element's children, which is a footgun waiting for the day a .logo-dot
   span is given a label. */
.lib-name::before,
#auth-brand::before { content: ""; }

.logo-dot,
.lib-name::before,
#auth-brand::before {
  display: inline-block;
  width: 0.62em;
  height: 0.62em;
  margin-right: 0.45em;
  border-radius: 50%;
  vertical-align: middle;
  flex: none;                    /* some hosts are flex rows; never let it squash */
  /* The off-centre white highlight is what makes this read as a lit sphere
     rather than a flat disc, and the dark outer stop gives it an edge to end
     on before the glow takes over. */
  background: radial-gradient(circle at 34% 30%,
              #ffffff 0%, #cdbcff 26%, #7c4dff 62%, #4a24b8 100%);
  /* Two shadows, tight then wide: the same falloff the map's points have, so
     the mark belongs to the picture it is a point of. Kept under ~1em because
     both headings clip at overflow:hidden (they need it for the ellipsis) and
     a halo wider than the line box would be sliced off with a hard edge. */
  box-shadow: 0 0 0.4em rgba(124, 77, 255, 0.85),
              0 0 0.95em rgba(124, 77, 255, 0.38);
}

/* Title links: the logo titles quietly link to the library */
.home-title a, .title-link {
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}
.home-title a:hover { text-shadow: 0 0 40px rgba(160, 140, 255, 0.9); }
.title-link:hover { color: var(--accent); }

/* text-shadow cannot reach the pseudo-element, so the dot brightens on its
   own — otherwise the hover glow would stop dead at the first letter. */
.home-title a:hover::before,
.title-link:hover::before {
  box-shadow: 0 0 0.5em rgba(124, 77, 255, 0.95),
              0 0 1em rgba(124, 77, 255, 0.55);
}

/* --- space theme (home view only) --- */

body.space {
  background: #05060f;
  color: #e9eaf5;
}

/* Nebula clouds: two fixed layers of soft radial gradients, drifting slowly */
body.space::before,
body.space::after {
  content: "";
  position: fixed;
  inset: -25%;
  z-index: -1;
  pointer-events: none;
  filter: blur(60px);
  background:
    radial-gradient(40% 35% at 30% 30%, rgba(124, 77, 255, 0.32), transparent 70%),
    radial-gradient(35% 30% at 72% 62%, rgba(0, 170, 255, 0.20), transparent 70%);
  animation: nebula-drift 50s ease-in-out infinite alternate;
}
body.space::after {
  background:
    radial-gradient(30% 25% at 68% 22%, rgba(255, 80, 190, 0.10), transparent 70%),
    radial-gradient(45% 40% at 22% 78%, rgba(70, 110, 255, 0.18), transparent 70%);
  animation-duration: 70s;
  animation-direction: alternate-reverse;
}

@keyframes nebula-drift {
  from { transform: translate3d(-4%, -2%, 0) scale(1); }
  to   { transform: translate3d(4%, 3%, 0) scale(1.18); }
}

@media (prefers-reduced-motion: reduce) {
  body.space::before, body.space::after { animation: none; }
}

body.space .home-title {
  color: #fff;
  font-weight: 200;
  letter-spacing: 0.12em;
  text-shadow: 0 0 32px rgba(140, 120, 255, 0.55);
}

body.space .big-btn {
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.15);
  color: #e9eaf5;
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.95rem;
  font-weight: 300;
}
body.space .big-btn:hover {
  border-color: rgba(150, 135, 255, 0.8);
  color: #fff;
  box-shadow: 0 0 26px rgba(124, 77, 255, 0.35);
}

body.space #drop-zone {
  background: rgba(255, 255, 255, 0.04);
  border-color: rgba(255, 255, 255, 0.2);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}
body.space #drop-zone.dragging {
  border-color: #8f7bff;
  background: rgba(124, 77, 255, 0.12);
  box-shadow: 0 0 40px rgba(124, 77, 255, 0.3);
}

body.space #drop-result {
  background: rgba(255, 255, 255, 0.05);
  border-color: rgba(255, 255, 255, 0.15);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}

body.space #home-view input,
body.space #home-view select,
body.space #home-view textarea {
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: #fff;
}
/* the dropdown's own list is drawn by the OS, so it needs a readable colour */
body.space #home-view select option { background: #1b1b2b; color: #fff; }
body.space #home-view input::placeholder,
body.space #home-view textarea::placeholder {
  color: rgba(255, 255, 255, 0.4);
}
body.space #home-view input:focus,
body.space #home-view textarea:focus {
  outline: 2px solid #8f7bff;
}

body.space #drop-submit {
  background: linear-gradient(135deg, #7c4dff, #3f8cff);
  border: none;
  color: #fff;
}

body.space .status-btn {
  background: rgba(255, 255, 255, 0.06);
  color: #e9eaf5;
  border-color: rgba(255, 255, 255, 0.18);
}
body.space .status-btn:hover { border-color: #8f7bff; }
body.space .status-btn.selected {
  background: linear-gradient(135deg, #7c4dff, #3f8cff);
  border-color: transparent;
  color: #fff;
}

body.space #found-title { color: #fff; }
body.space .found-label,
body.space .drop-hint { color: rgba(255, 255, 255, 0.55); }
body.space #drop-msg.success { color: rgba(255, 255, 255, 0.65); }
body.space #drop-msg.error { color: #ff8093; }

.lib-head { display: flex; align-items: baseline; gap: 1rem; }
/* min-width:0 lets a long name shrink rather than shove ⚙️ off the edge */
.lib-head h1 {
  flex: 1; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  /* Room for the logo dot's glow inside the overflow:hidden box, taken back
     with a matching negative margin so the title still starts flush with the
     controls below it. */
  padding-left: 0.4em; margin-left: -0.4em;
}
/* The latent toggle is a header-level action, not a filter, so it lives beside
   ⚙️ rather than in #controls. It keeps its natural width — only the title
   gives ground (and fitLibraryTitle() shrinks it to suit). */
.lib-head #latent-btn {
  flex: none;
  white-space: nowrap;
  font-size: 0.85rem;
  padding: 0.3rem 0.7rem;
}
.lib-head #latent-hint { flex: none; white-space: nowrap; }

/* Narrower than the 560pt ⌥B panel's neighbourhood the header has room for the
   name and ⚙️ and nothing else, and the hint would push the title down to the
   11px floor. Latent mode is far out of reach at that width anyway, so drop
   the explanation rather than the title. */
@media (max-width: 620px) {
  .lib-head #latent-hint { display: none; }
}

/* --- sparring session: full-screen split view --------------------------- */

/* Escape the 860px body cap; the nebula layers (z-index:-1) show through */
body.sparring { max-width: none; padding: 0; overflow: hidden; }

#spar-view {
  position: fixed;
  inset: 0;
  /* below the overlay ladder (100/200/300) so uiConfirm still paints on top */
  z-index: 50;
  display: grid;
  grid-template-columns: var(--spar-split, 55%) 6px 1fr;
  grid-template-rows: 100%;   /* pin the row, or a tall child grows the grid */
  overflow: hidden;
  /* Warm near-black, not #000 and not a cool blue-black. Pure black behind
     white text haloes during long reading; a little red and green in the
     ground takes that edge off without costing contrast. */
  background: #14120f;
}

/* Zoom widens the iframe's layout box rather than transforming it, so the PDF
   viewer re-renders the page at the larger size instead of scaling a bitmap.
   The page is loaded #view=FitH, so it fills whatever width the frame has;
   this pane then scrolls sideways to pan. Vertical scrolling stays the
   viewer's own, so there is never a second vertical scrollbar. */
#spar-left {
  position: relative; background: #1c1a17;
  --pdf-zoom: 1;
  overflow-x: auto; overflow-y: hidden;
}
#spar-pdf {
  width: calc(100% * var(--pdf-zoom));
  height: 100%; border: 0; display: block;
}

/* Split-screen mode: the real Preview holds the paper in its own window, so
   this one becomes chat-only and takes the right half of the screen. */
/* One column, not three zeroed ones: with the pane and divider display:none the
   chat becomes the first in-flow item and would be auto-placed into column 1. */
body.preview-split #spar-view { grid-template-columns: 1fr; }
body.preview-split #spar-left,
body.preview-split #spar-divider { display: none; }

/* Sits opposite the zoom cluster, always legible — this is the only route to
   marking the paper up, so it must not fade out like a decoration. */
#spar-annotate {
  position: absolute; bottom: 0.9rem; right: 0.9rem; z-index: 2;
  padding: 0.4rem 0.8rem; border-radius: 8px; font-size: 0.9rem;
  color: #fff; border: 1px solid rgba(150, 135, 255, 0.45);
  background: linear-gradient(135deg, rgba(124, 77, 255, 0.85), rgba(63, 140, 255, 0.85));
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
}
#spar-annotate:hover { filter: brightness(1.15); }
body.preview-split #spar-annotate { display: none; }

#spar-zoom {
  position: absolute; bottom: 0.9rem; left: 0.9rem; z-index: 2;
  display: flex; align-items: stretch; gap: 1px;
  border-radius: 8px; overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.15);
  background: rgba(20, 18, 34, 0.72);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  opacity: 0.35; transition: opacity 0.15s;
}
/* stays out of the way until wanted, but never fully invisible */
#spar-left:hover #spar-zoom, #spar-zoom:hover { opacity: 1; }
#spar-zoom button {
  background: none; border: 0; color: #e9eaf5;
  padding: 0.3rem 0.6rem; font-size: 0.9rem; line-height: 1.4;
}
#spar-zoom button:hover { background: rgba(255, 255, 255, 0.12); filter: none; }
#zoom-reset { min-width: 3.6rem; font-variant-numeric: tabular-nums; }

#spar-nopdf {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 1rem; padding: 2rem; text-align: center;
  color: rgba(255, 255, 255, 0.6);
}
#spar-nopdf .big-btn { max-width: 320px; }

#spar-divider {
  cursor: col-resize;
  background: rgba(255, 255, 255, 0.12);
  transition: background 0.15s;
}
#spar-divider:hover { background: rgba(143, 123, 255, 0.6); }

/* While dragging, the iframe must not swallow pointermove — without this the
   divider sticks the moment the cursor crosses the PDF. */
body.dragging-split #spar-pdf,
body.dragging-split iframe { pointer-events: none; }
body.dragging-split { cursor: col-resize; user-select: none; }

#spar-chat {
  /* one gutter for the header, the log and the composer, so their left edges
     line up down the whole pane */
  --spar-gutter: clamp(1rem, 7%, 3.5rem);
  display: flex; flex-direction: column;
  /* both min-*: 0 are load-bearing — a flex item defaults to min-height:auto,
     which stops the log shrinking, so overflow-y never engages */
  min-width: 0; min-height: 0;
  background: #1b1916;
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  color: #e9eaf5;
  position: relative;      /* anchor for the "add a paper" popup */
}

#spar-head {
  display: flex; align-items: center; gap: 0.75rem;
  padding: 0.85rem var(--spar-gutter);
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
}
#spar-title {
  flex: 1; min-width: 0; font-size: 0.9rem; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#spar-head .linklike { color: rgba(255, 255, 255, 0.7); font-size: 0.85rem; }
#spar-head .linklike:hover { color: #fff; }
#spar-add { flex: 0 0 auto; font-size: 0.95rem; line-height: 1; }
#spar-add:disabled { opacity: 0.35; cursor: default; }
#spar-add:disabled:hover { color: rgba(255, 255, 255, 0.7); }

/* --- extra papers pulled into the conversation ------------------------- */

#spar-attached {
  display: flex; gap: 0.35rem; flex: 0 1 auto; min-width: 0;
  overflow: hidden;
}
.spar-chip {
  max-width: 9rem;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  font-size: 0.72rem;
  background: rgba(124, 77, 255, 0.22);
  border: 1px solid rgba(150, 135, 255, 0.4);
  color: rgba(255, 255, 255, 0.85);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

#spar-add-pop {
  position: absolute;
  top: 3.1rem; left: 1rem; right: 1rem;
  z-index: 30;
  padding: 0.75rem;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(150, 135, 255, 0.35);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.5);
}
#spar-add-search {
  width: 100%;
  font-size: 0.9rem;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: #fff;
}
#spar-add-search::placeholder { color: rgba(255, 255, 255, 0.4); }
#spar-add-search:focus { outline: 2px solid #8f7bff; }

#spar-add-results { max-height: 15rem; overflow-y: auto; margin-top: 0.5rem; }
.spar-add-item {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 0.8rem;
  width: 100%;
  text-align: left;
  padding: 0.4rem 0.45rem;
  border: 1px solid transparent;
  border-radius: 8px;
  background: none;
  cursor: pointer;
}
.spar-add-item:hover, .spar-add-item.selected {
  background: rgba(124, 77, 255, 0.18);
  border-color: rgba(150, 135, 255, 0.4);
  filter: none;
}
.spar-add-title {
  color: #fff; font-size: 0.88rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.spar-add-meta {
  color: rgba(255, 255, 255, 0.5); font-size: 0.76rem; white-space: nowrap;
}
.spar-add-none {
  margin: 0.4rem 0.2rem 0;
  color: rgba(255, 255, 255, 0.5); font-size: 0.85rem;
}

#spar-log {
  flex: 1 1 0; min-height: 0; overflow-y: auto;
  /* A generous gutter that grows with the pane. On a wide monitor it pulls the
     column of text away from both edges, which is most of what makes a chat
     feel unhurried; on a narrow pane it collapses to something workable. */
  padding: 1.5rem var(--spar-gutter);
  display: flex; flex-direction: column; gap: 1.35rem;
}
/* messages must not be squeezed by the flex column either */
#spar-log > * { flex: 0 0 auto; }

.spar-msg { font-size: 0.92rem; line-height: 1.55; white-space: pre-wrap; }

/* Reading typography for Claude's replies — the part you actually read at
   length. Three changes, in order of how much they matter:
   1. a measure cap: full-pane text on a wide monitor runs to ~150 characters
      per line, and the eye loses its place returning to the left margin;
   2. ui-serif, which is New York on macOS — a proper reading face, already
      installed, no download and no licence to worry about;
   3. a little more size and leading than UI text wants.
   Switch `font-family` to `inherit` to go back to the interface sans. */
.spar-msg.assistant {
  font-family: ui-serif, "New York", Charter, Georgia, serif;
  font-size: 1.02rem;
  line-height: 1.68;
  max-width: 70ch;
  letter-spacing: 0.005em;
}
/* Code and maths stay in their own faces — a serif would misrepresent them. */
.spar-msg.assistant code, .spar-msg.assistant pre { font-family: ui-monospace, monospace; }
.spar-msg.assistant .katex { font-size: 1.04em; }
.spar-msg.user {
  align-self: flex-end; max-width: 85%;
  background: rgba(124, 77, 255, 0.25);
  border: 1px solid rgba(150, 135, 255, 0.4);
  border-radius: 12px 12px 2px 12px;
  padding: 0.55rem 0.8rem;
}
.spar-msg.assistant { align-self: stretch; }
.spar-msg.error { color: #ff8093; font-size: 0.85rem; }

/* Markdown inside a reply. setRichText consumes the blank lines between
   blocks, so the pre-wrap above only preserves soft breaks *within* a block. */
.spar-msg > :first-child { margin-top: 0; }
.spar-msg > :last-child { margin-bottom: 0; }
.spar-msg p { margin: 0 0 0.6em; }
.spar-msg ul, .spar-msg ol { margin: 0.35em 0 0.6em; padding-left: 1.5em; }
.spar-msg li { margin: 0.15em 0; }
.spar-msg h4, .spar-msg h5, .spar-msg h6 {
  margin: 0.9em 0 0.35em; font-size: 0.95rem; font-weight: 600;
}
.spar-msg blockquote {
  margin: 0.5em 0; padding-left: 0.7em;
  border-left: 2px solid rgba(255, 255, 255, 0.25);
  color: rgba(255, 255, 255, 0.75);
}
.spar-msg hr { border: 0; border-top: 1px solid rgba(255, 255, 255, 0.15); margin: 0.8em 0; }
.spar-msg a { color: #9db7ff; }
.spar-msg code {
  font-size: 0.88em; padding: 0.05em 0.3em; border-radius: 4px;
  background: rgba(255, 255, 255, 0.1);
}
.spar-msg pre {
  margin: 0.5em 0; padding: 0.55rem 0.7rem; border-radius: 8px;
  background: rgba(255, 255, 255, 0.07);
  white-space: pre; overflow-x: auto;      /* code must not be re-wrapped */
}
.spar-msg pre code { background: none; padding: 0; }
/* A wide equation scrolls inside its own block rather than widening the pane. */
.spar-msg .katex-display { margin: 0.5em 0; overflow-x: auto; overflow-y: hidden; }

.spar-thinking { color: rgba(255, 255, 255, 0.5); font-size: 0.85rem; font-style: italic; }
.spar-thinking::after {
  content: "…"; display: inline-block; width: 1.2em; text-align: left;
  animation: spar-dots 1.2s steps(4, end) infinite;
}
@keyframes spar-dots { 0% { content: "."; } 33% { content: ".."; } 66% { content: "…"; } }
@media (prefers-reduced-motion: reduce) { .spar-thinking::after { animation: none; } }

/* The composer is one box: the form draws the border and background, the
   textarea inside it is transparent and borderless, and the toolbar row sits
   within the same outline. That is why the controls read as "inside" the input
   without being typeable. */
/* Dragging a PDF over the composer. The whole box lights rather than a separate
   drop zone, because the box is what you are aiming at. */
#spar-form.dropping {
  border-color: rgba(150, 135, 255, 0.85);
  background: rgba(124, 77, 255, 0.14);
}

#spar-form {
  display: flex; flex-direction: column; gap: 0.5rem;
  margin: 0 var(--spar-gutter) 1.1rem;
  padding: 0.7rem 0.8rem 0.6rem;
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.14);
  transition: border-color 0.15s;
}
#spar-form:focus-within { border-color: rgba(150, 135, 255, 0.55); }

#spar-input {
  resize: none; min-height: 2.6rem; font-size: 0.92rem;
  /* Ceiling for the auto-grow (sparAutoGrow); past it the box scrolls. In `lh`
     so it is a whole number of lines whatever the type does. */
  max-height: calc(6lh + 0.9rem + 2px);
  background: none; border: 0; padding: 0.15rem 0.2rem;
  color: #fff;
}
#spar-input::placeholder { color: rgba(255, 255, 255, 0.4); }
/* no focus ring on the textarea — the box around it lights up instead */
#spar-input:focus { outline: none; }

/* Toolbar inside the box: ＋, web, model on the left; note and send on the right */
#spar-tools { display: flex; align-items: center; gap: 0.55rem; }
.spar-tools-gap { flex: 1; }
#spar-tools .linklike { color: rgba(255, 255, 255, 0.55); }
#spar-tools .linklike:hover:not(:disabled) { color: #fff; }

#spar-send {
  background: linear-gradient(135deg, #7c4dff, #3f8cff);
  border: none; color: #fff; padding: 0.35rem 0.95rem; font-size: 0.85rem;
}
#spar-send:disabled { opacity: 0.5; cursor: wait; }

/* --- "+ note" guided flow, inside the sparring chat --------------------- */

.spar-note-btn {
  padding: 0.45rem 0.5rem;
  white-space: nowrap;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.15);
  color: #e9eaf5;
  border-radius: 8px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.8rem;
  font-weight: 300;
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}
.spar-note-btn:hover {
  border-color: rgba(150, 135, 255, 0.8);
  color: #fff;
  box-shadow: 0 0 26px rgba(124, 77, 255, 0.35);
  filter: none;
}
.spar-note-btn:disabled { opacity: 0.55; cursor: wait; box-shadow: none; }

.spar-note-card {
  align-self: stretch;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(150, 135, 255, 0.35);
  border-radius: 12px;
  padding: 0.9rem 1rem;
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}
.spar-note-card.spar-note-done { border-color: rgba(120, 220, 170, 0.45); }

.spar-note-head {
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 0.7rem;
  color: rgba(255, 255, 255, 0.5);
  margin-bottom: 0.5rem;
}
.spar-note-topic { margin: 0 0 0.5rem; font-size: 0.95rem; }
.spar-note-hint {
  margin: 0 0 0.7rem;
  font-size: 0.8rem;
  color: rgba(255, 255, 255, 0.45);
}
.spar-note-text { margin: 0 0 0.7rem; font-size: 0.92rem; line-height: 1.5; }

/* selectable bullet boxes on the overview card */
.spar-pick {
  display: flex;
  align-items: flex-start;
  gap: 0.55rem;
  width: 100%;
  text-align: left;
  margin-bottom: 0.4rem;
  padding: 0.5rem 0.7rem;
  border-radius: 8px;
  font-size: 0.9rem;
  line-height: 1.45;
  cursor: pointer;
  background: rgba(255, 255, 255, 0.03);
  border: 1px dashed rgba(255, 255, 255, 0.22);
  color: rgba(255, 255, 255, 0.75);
}
/* opt out of the global button:hover brightness, like .init-annot does */
.spar-pick:hover { border-color: rgba(150, 135, 255, 0.7); filter: none; }
.spar-pick.selected {
  background: rgba(124, 77, 255, 0.14);
  border: 1px solid rgba(150, 135, 255, 0.75);
  color: #fff;
  box-shadow: 0 0 20px rgba(124, 77, 255, 0.3);
}
.spar-pick-tick { opacity: 0; color: #8f7bff; font-weight: 700; }
.spar-pick.selected .spar-pick-tick { opacity: 1; }
.spar-pick-text { flex: 1; }
.spar-pick:disabled { cursor: default; opacity: 0.75; box-shadow: none; }

/* Add / Don't add / Refine */
.spar-choice { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.spar-choice-btn {
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.2);
  color: #e9eaf5;
  border-radius: 999px;
  padding: 0.35rem 1rem;
  font-size: 0.85rem;
}
.spar-choice-btn:hover { border-color: rgba(150, 135, 255, 0.8); filter: none; }
.spar-choice-btn.primary {
  background: linear-gradient(135deg, #7c4dff, #3f8cff);
  border-color: transparent;
  color: #fff;
}
.spar-choice-btn:disabled { opacity: 0.35; cursor: default; box-shadow: none; }
/* the option actually taken stays legible once the card is frozen */
.spar-choice-btn.chosen {
  opacity: 1;
  border-color: rgba(150, 135, 255, 0.9);
  color: #fff;
  box-shadow: 0 0 0 2px rgba(150, 135, 255, 0.45);
}
/* A decided card stays clickable until the notes are written, so the choice
   has to read as *selected*, not merely bright: step the other option back. */
.spar-choice.decided .spar-choice-btn:not(.chosen):not(:disabled) {
  opacity: 0.5;
}

.spar-refine-row { display: flex; gap: 0.5rem; margin-top: 0.7rem; }
.spar-refine-input {
  flex: 1;
  font-size: 0.88rem;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: #fff;
}
.spar-refine-input::placeholder { color: rgba(255, 255, 255, 0.4); }
.spar-refine-input:focus { outline: 2px solid #8f7bff; }

/* Stop affordance: while something is running, a button keeps its shape but
   shows a filled square that aborts instead of repeating the action. */
.spar-stop {
  display: inline-block;
  width: 0.72rem;
  height: 0.72rem;
  border-radius: 2px;
  background: currentColor;
  opacity: 0.9;
}
#spar-send.is-stop,
.spar-note-btn.is-stop {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* the note button's label is uppercase+spaced; the square needs neither */
.spar-note-btn.is-stop { letter-spacing: normal; }
#spar-send.is-stop:hover,
.spar-note-btn.is-stop:hover { filter: brightness(1.15); }

/* Web-search toggle: one glyph that changes state, rather than two to parse.
   Off is the default — a slashed globe; on is a lit globe. */
#spar-web { position: relative; opacity: 0.5; font-size: 0.95rem; line-height: 1; }
#spar-web:hover:not(:disabled) { opacity: 0.85; }
#spar-web:disabled { opacity: 0.2; cursor: default; }
#spar-web.on {
  opacity: 1;
  filter: drop-shadow(0 0 6px rgba(124, 77, 255, 0.85));
}
/* the slash, drawn over the globe while it's off */
#spar-web.off::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 1.25em;
  height: 1.5px;
  background: currentColor;
  border-radius: 1px;
  transform: translate(-50%, -50%) rotate(-45deg);
  pointer-events: none;
}
#spar-web.off:disabled::after { opacity: 0.6; }

/* Option-C lands a passage while you are looking at Preview; the flash is the
   only signal that anything happened over here. */
@keyframes quote-landed {
  0%   { border-color: #8f7bff; box-shadow: 0 0 0 3px rgba(124, 77, 255, 0.45); }
  100% { border-color: inherit; box-shadow: 0 0 0 0 rgba(124, 77, 255, 0); }
}
#spar-input.quoted { animation: quote-landed 1.1s ease-out; }

/* sits inline with the chips, reads as a link rather than another tag */
.tag-more { font-size: 0.85rem; padding: 0.15rem 0.4rem; align-self: center; }

/* Model picker, bottom-right of the composer. Deliberately faint: it is a thing
   you set occasionally, not a control you should notice while reading. */
#spar-model-quick {
  background: none; border: 0; padding: 0.1rem 0.2rem;
  font-size: 0.8rem; color: rgba(255, 255, 255, 0.75);
  opacity: 0.45; transition: opacity 0.15s;
  cursor: pointer;
}
#spar-tools:hover #spar-model-quick,
#spar-model-quick:focus { opacity: 1; }
/* the note button is a peer of Send now, not a full-width stacked block */
.spar-note-btn { padding: 0.35rem 0.7rem; font-size: 0.8rem; }

/* ⌥N notes panel — floats over the conversation rather than replacing it, so
   you can still see what you were arguing about while you write. */
/* ⌥N notes panel — floats over the conversation rather than replacing it, so
   you can still see what you were arguing about while you write. Its height is
   content-driven: it grows with the notes and stops at half the pane, past
   which the body scrolls. */
#spar-notes-pop {
  position: absolute; z-index: 6;
  top: 3.2rem; left: var(--spar-gutter); right: var(--spar-gutter);
  max-height: 52%;
  display: flex; flex-direction: column; gap: 0.55rem;
  padding: 0.9rem 1rem 0.8rem;
  border-radius: 12px;
  border: 1px solid rgba(150, 135, 255, 0.35);
  background: rgba(32, 29, 25, 0.96);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.55);
}
#spar-notes-head {
  display: flex; align-items: baseline; gap: 0.6rem;
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.09em;
  color: rgba(255, 255, 255, 0.5);
}
#spar-notes-state { margin-left: auto; text-transform: none; letter-spacing: 0; }

/* the scroller: min-height:0 is load-bearing in a flex column, or it refuses
   to shrink and the panel grows past its max-height instead of scrolling */
#spar-notes-body { overflow-y: auto; min-height: 0; }

#spar-notes-view {
  font-size: 0.92rem; line-height: 1.6; color: #fff;
  padding: 0.4rem 0.5rem; border-radius: 8px;
  border: 1px solid transparent; cursor: text;
  white-space: pre-wrap;                 /* notes keep their line breaks */
}
#spar-notes-view:hover { border-color: rgba(255, 255, 255, 0.16); }
#spar-notes-view:focus { outline: 2px solid #8f7bff; }
#spar-notes-view.empty { color: rgba(255, 255, 255, 0.35); font-style: italic; }
#spar-notes-view ul, #spar-notes-view ol { margin: 0.3em 0; padding-left: 1.35em; }
#spar-notes-view li { margin: 0.15em 0; }
#spar-notes-view p { margin: 0.35em 0; }
#spar-notes-view > :first-child { margin-top: 0; }
#spar-notes-view > :last-child { margin-bottom: 0; }

#spar-notes-text {
  width: 100%; resize: none; display: block;
  font-size: 0.92rem; line-height: 1.6;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.16);
  color: #fff;
}
#spar-notes-text::placeholder { color: rgba(255, 255, 255, 0.35); }
#spar-notes-text:focus { outline: 2px solid #8f7bff; }

#spar-notes-actions { display: flex; align-items: center; gap: 0.6rem; }
#spar-notes-actions .linklike { color: rgba(255, 255, 255, 0.55); font-size: 0.8rem; }
#spar-notes-actions .linklike:hover { color: #fff; }
#spar-notes-save {
  margin-left: auto; font-size: 0.85rem; padding: 0.35rem 0.9rem;
  background: linear-gradient(135deg, #7c4dff, #3f8cff);
  border: 0; color: #fff; border-radius: 8px;
}

/* --- explore: find recent papers ------------------------------------------
   Lanes stack in one column at every width. That is what keeps this usable in
   the 560pt ⌥B panel without needing a native "open a wider window" action. */

#explore-btn { white-space: nowrap; }

/* Wider and left-aligned than the shared 420px centred panel it inherits from */
#explore-panel {
  width: min(560px, 92vw);
  text-align: left;
  max-height: 84vh;
  overflow-y: auto;
}
#explore-panel h2 { margin: 0 0 0.4rem; font-size: 1.15rem; }
#explore-panel .settings-text { text-align: left; }

.explore-h {
  margin: 1.1rem 0 0.45rem;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}
.explore-picks { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.explore-picks .chip { cursor: pointer; }
.explore-picks .chip:disabled { opacity: 0.3; cursor: not-allowed; }
/* var(--muted) is a light-theme grey and barely reads on this dark panel —
   and this line is the only place a search started from home can show its
   errors, since #message lives in the (hidden) library view. */
#explore-msg { margin: 0.9rem 0 0; font-size: 0.83rem; color: rgba(255, 255, 255, 0.65); min-height: 1.1em; }

/* While results are up, the normal library chrome would just be confusing */
body.exploring #controls,
body.exploring #folder-bar,
body.exploring #papers,
body.exploring #empty-state { display: none; }

.explore-head {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 1rem; margin-bottom: 1.2rem;
}
.explore-lane { margin-bottom: 1.8rem; }
.explore-lane h2 { margin: 0 0 0.15rem; font-size: 1.05rem; }
.explore-sub { margin: 0 0 0.5rem; color: var(--muted); font-size: 0.8rem; }

/* A result is the library's .card with the parts a candidate hasn't earned yet
   left off, so it takes its surface, title and abstract from the card rules
   above. Only the differences live here. */
.explore-card { margin-bottom: 0.7rem; }
/* The panel is ~495pt and the scrape pane narrower still, so the head has to be
   allowed to break rather than squeeze the title to a column of syllables. */
.explore-card .card-head { flex-wrap: wrap; }
.explore-card .card-head h2 { min-width: 9rem; overflow-wrap: anywhere; }
.explore-meta { margin: 0.25rem 0 0; }
.explore-why { margin: 0.2rem 0 0; font-size: 0.78rem; color: var(--muted); font-style: italic; }
.explore-card .abstract { margin: 0; }
.explore-no-abstract { color: var(--muted); font-style: italic; }
.explore-card .card-foot { margin-top: 0.5rem; }

/* Matching pills, side by side in the head — where a library card carries its
   status control. A row rather than a stack: stacked, they made the head twice
   as tall as a one-line title and opened a hole above the author line. */
.explore-actions {
  flex: none;
  display: flex; flex-wrap: wrap;
  gap: 0.35rem;
}
.explore-add, .explore-skip {
  border-radius: 999px;
  padding: 0.25rem 0.85rem;
  font-size: 0.85rem;
  white-space: nowrap;
  text-align: center;
  /* Fixed floor so the pair keeps its footprint — and the title beside it stays
     put — as the labels change from Add/Not for me to Delete/File… */
  min-width: 5.4rem;
}
.explore-add.done { opacity: 0.55; cursor: default; }
.explore-add.added {
  background: var(--danger);
  border-color: var(--danger);
  color: #fff;
}
.explore-add.added:hover { filter: brightness(1.1); }

/* Same pill as Add so the two read as one control group, but a neutral fill:
   it is the lesser action, and two accent-filled pills on every one of ten
   rows would fight each other. */
.explore-skip {
  background: var(--chip);
  color: var(--muted);
  border: 1px solid var(--border);
}
.explore-skip:hover { color: var(--danger); border-color: var(--danger); filter: none; }

/* Deliberately not a card: a dismissal collapses to one line, so the rows that
   are still worth reading keep the weight. */
.explore-dismissed {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 0.8rem; padding: 0.5rem 0.2rem; margin-bottom: 0.7rem;
  border-top: 1px solid var(--border);
}
.explore-dismissed-text {
  color: var(--muted); font-size: 0.82rem; font-style: italic;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* --- library map: PCA of the paper vectors, drawn as a star field ---------
   Full-screen and self-contained: it carries its own nebula rather than
   borrowing body.space, because it opens from the library view, which is the
   light theme. Sits at 200 — above #spar-view (50) and the settings dialog
   (100), below the confirm modal (300). */

#pca-view {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: #05060f;
  color: #e9eaf5;
}
#pca-view::before, #pca-view::after {
  content: "";
  position: absolute;
  inset: -25%;
  z-index: 0;
  pointer-events: none;
  filter: blur(60px);
  background:
    radial-gradient(40% 35% at 30% 30%, rgba(124, 77, 255, 0.32), transparent 70%),
    radial-gradient(35% 30% at 72% 62%, rgba(0, 170, 255, 0.20), transparent 70%);
  animation: nebula-drift 50s ease-in-out infinite alternate;
}
#pca-view::after {
  background:
    radial-gradient(30% 25% at 68% 22%, rgba(255, 80, 190, 0.10), transparent 70%),
    radial-gradient(45% 40% at 22% 78%, rgba(70, 110, 255, 0.18), transparent 70%);
  animation-duration: 70s;
  animation-direction: alternate-reverse;
}

#pca-head {
  position: relative; z-index: 1;
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 1rem; padding: 1.1rem 1.4rem 0.4rem;
}
#pca-title {
  margin: 0; font-size: 1.15rem; font-weight: 200;
  letter-spacing: 0.12em; text-transform: uppercase;
  text-shadow: 0 0 32px rgba(140, 120, 255, 0.55);
}
#pca-sub { margin: 0.25rem 0 0; font-size: 0.78rem; color: rgba(255, 255, 255, 0.5); }
#pca-head .linklike { color: rgba(255, 255, 255, 0.7); font-size: 1rem; }
#pca-head .linklike:hover { color: #fff; }

/* Identity is never colour alone: a legend is always up, and every star names
   itself on hover. */
#pca-legend {
  position: relative; z-index: 1;
  display: flex; flex-wrap: wrap; gap: 0.4rem 1rem;
  padding: 0.3rem 1.4rem 0.7rem;
  font-size: 0.78rem; color: rgba(255, 255, 255, 0.72);
}
/* Clickable: a key isolates its folder, and clicking it again clears. */
.pca-key {
  display: inline-flex; align-items: center; gap: 0.4rem;
  background: none; border: 0; padding: 0.1rem 0;
  font: inherit; font-size: 0.78rem; color: inherit; cursor: pointer;
  opacity: 0.92; transition: opacity 0.12s ease;
}
.pca-key:hover { opacity: 1; filter: none; }
.pca-key.on { opacity: 1; }
.pca-key.on b { color: #fff; text-decoration: underline; text-underline-offset: 3px; }
.pca-key.off { opacity: 0.4; }
.pca-note {
  font-size: 0.74rem; font-style: italic;
  color: rgba(255, 255, 255, 0.4);
}
.pca-key i {
  width: 9px; height: 9px; border-radius: 50%;
  display: inline-block; flex: none;
}
.pca-key b { font-weight: 500; }
.pca-key span { color: rgba(255, 255, 255, 0.45); }

#pca-canvas { position: relative; z-index: 1; flex: 1; min-height: 0; }

.pca-star { cursor: pointer; }
.pca-star .core { transition: r 0.12s ease; }
.pca-star:hover .halo { opacity: 0.42; }
.pca-star:hover .core { r: 6; }

#pca-tip {
  /* Above #latent-view (50) and #pca-view (200), below uiConfirm (300) */
  position: fixed; z-index: 250;
  max-width: 22rem; padding: 0.5rem 0.7rem;
  border-radius: 10px;
  background: rgba(12, 14, 30, 0.94);
  border: 1px solid rgba(255, 255, 255, 0.18);
  box-shadow: 0 6px 30px rgba(0, 0, 0, 0.5);
  pointer-events: none;
  font-size: 0.82rem; line-height: 1.35;
}
#pca-tip .t { color: #fff; display: block; }
#pca-tip .m { color: rgba(255, 255, 255, 0.55); font-size: 0.76rem; }

/* Tag highlighting: a dropdown that adds to a row of removable chips. Tags
   light papers up rather than filtering them, so this sits apart from the
   legend's folder filter. */
#pca-tagbar {
  position: relative; z-index: 1;
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem;
  padding: 0 1.4rem 0.8rem;
}
/* Recessive by default: this is a tool you reach for, not something competing
   with the legend. appearance:none is what strips the heavy native control —
   without it macOS draws its own bezel and arrows over anything set here. */
#pca-tag-select {
  appearance: none;
  -webkit-appearance: none;
  font-size: 0.74rem;
  padding: 0.05rem 0.95rem 0.05rem 0;
  border: 0;
  border-radius: 0;
  color: rgba(255, 255, 255, 0.38);
  cursor: pointer;
  background: transparent
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='7' height='4'%3E%3Cpath d='M0 0l3.5 4L7 0z' fill='%23ffffff' fill-opacity='.32'/%3E%3C/svg%3E")
    no-repeat right center;
}
#pca-tag-select:hover, #pca-tag-select:focus { color: rgba(255, 255, 255, 0.78); }
#pca-tag-select:focus { outline: none; }
#pca-tag-select:disabled { opacity: 0.3; cursor: default; }
/* the popup list is drawn by the OS, so keep its own text legible */
#pca-tag-select option { color: #1a1a1a; background: #fff; }
#pca-tagchips { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.pca-tagchip {
  display: inline-flex; align-items: center; gap: 0.35rem;
  border-radius: 999px;
  padding: 0.18rem 0.6rem;
  font-size: 0.76rem;
  background: rgba(255, 255, 255, 0.14);
  border: 1px solid rgba(255, 255, 255, 0.3);
  color: #fff;
}
.pca-tagchip:hover { border-color: #8f7bff; filter: none; }
.pca-tagchip span { opacity: 0.6; }

/* Rerun controls: same recessive weight as the tag select — they sit below the
   picture and should never pull attention from it. */
#pca-actions {
  position: relative; z-index: 1;
  display: flex; flex-wrap: wrap; align-items: center; gap: 1rem;
  padding: 0 1.4rem 0.8rem;
  font-size: 0.74rem;
}
#pca-actions button {
  background: none; border: 0; padding: 0;
  font: inherit; font-size: 0.74rem;
  color: rgba(255, 255, 255, 0.38);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}
#pca-actions button:hover:not(:disabled) { color: rgba(255, 255, 255, 0.85); filter: none; }
#pca-actions button:disabled { opacity: 0.32; cursor: default; text-decoration: none; }
#pca-err { color: #ff8093; }

/* 3D canvas fills the plot area; the cursor says it can be grabbed */
.pca-canvas3d { display: block; width: 100%; height: 100%; cursor: grab; }
.pca-canvas3d:active { cursor: grabbing; }

/* Paper search: same recessive weight as the tag select, with a small floating
   result list. Highlights rather than filters, exactly like the tags. */
#pca-paperbar {
  position: relative; z-index: 2;
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem;
  padding: 0 1.4rem 0.7rem;
}
#pca-paper-search {
  width: 11rem;
  font-size: 0.74rem;
  padding: 0.1rem 0;
  border: 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 0;
  background: transparent;
  color: rgba(255, 255, 255, 0.8);
}
#pca-paper-search::placeholder { color: rgba(255, 255, 255, 0.38); }
#pca-paper-search:focus {
  outline: none;
  border-bottom-color: rgba(150, 135, 255, 0.8);
}
#pca-paper-results {
  position: absolute; top: 1.5rem; left: 1.4rem; z-index: 3;
  width: min(26rem, 70vw);
  max-height: 15rem; overflow-y: auto;
  padding: 0.3rem;
  border-radius: 10px;
  background: rgba(12, 14, 30, 0.96);
  border: 1px solid rgba(255, 255, 255, 0.18);
  box-shadow: 0 8px 34px rgba(0, 0, 0, 0.55);
}
.pca-hit {
  display: block; width: 100%; text-align: left;
  padding: 0.32rem 0.45rem;
  border: 0; border-radius: 6px;
  background: none;
  color: rgba(255, 255, 255, 0.85);
  font: inherit; font-size: 0.78rem;
  cursor: pointer;
}
.pca-hit:hover, .pca-hit.selected { background: rgba(124, 77, 255, 0.25); filter: none; }
.pca-hit small { display: block; color: rgba(255, 255, 255, 0.45); font-size: 0.72rem; }
.pca-none { padding: 0.35rem 0.45rem; font-size: 0.76rem; color: rgba(255, 255, 255, 0.45); }
#pca-paperchips { display: flex; flex-wrap: wrap; gap: 0.35rem; }

/* --- latent mode: the library browsed inside its own latent space ----------
   Same shape as the sparring view — fixed grid, draggable divider — and the
   same warm near-black on both sides, so the divider is the only seam. */

body.latent { max-width: none; padding: 0; overflow: hidden; }

/* The visual scrape search. Same split geometry as latent mode — reading pane
   over a full-bleed field — because it is the same idea pointed at a different
   population, and a second layout would just be a second thing to keep in sync. */
#scan-view {
  --scan-field: 16rem;
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  grid-template-columns: var(--scan-split, 40%) 10px 1fr;
  grid-template-rows: 100%;
  overflow: hidden;
  background: #05060f;
  color: #e9eaf5;
}
#scan-left {
  position: relative;
  z-index: 2;
  min-width: 0;
  overflow-y: auto;
  padding: 1.2rem 1.4rem 2rem;
  background: linear-gradient(to right,
    rgba(11, 9, 16, 0.93) 0%, rgba(11, 9, 16, 0.86) 30%,
    rgba(11, 9, 16, 0.66) 62%, rgba(11, 9, 16, 0) 100%);
}
#scan-divider { position: relative; z-index: 3; background: none; cursor: col-resize; }
#scan-right { position: relative; z-index: 2; min-width: 0; pointer-events: none; }
#scan-canvas { position: absolute; inset: 0; z-index: 1; }
#scan-head { display: flex; align-items: baseline; justify-content: space-between; }
#scan-head .scan-head-right { display: flex; align-items: center; gap: 0.9rem; }
#scan-topup { font-size: 0.78rem; }
#scan-to-index { font-size: 0.78rem; color: #ffd166; margin-bottom: 0.5rem; }
#scan-settings {
  position: absolute;
  z-index: 3;
  top: 0.6rem;
  right: 0.9rem;
  pointer-events: auto;        /* #scan-right disables them for the field */
  font-size: 1.2rem;
  line-height: 1;
  padding: 0.15rem 0.25rem;
  opacity: 0.42;
}
#scan-settings:hover { opacity: 0.95; filter: none; }
#scan-topup-msg { font-variant-numeric: tabular-nums; }
/* Reserve no room until there is something to say. It only speaks during a
   rescrape, and an always-on min-height put ~30px of nothing between the
   subtitle and the first row of chips. */
#scan-topup-msg:empty { display: none; }
#scan-topup-msg.error { color: #ff8093; }
#scan-head h2 { margin: 0; font-weight: 300; letter-spacing: 0.06em; cursor: pointer; }
#scan-head h2:hover { color: #fff; }
.scan-note { color: rgba(255, 255, 255, 0.5); font-size: 0.8rem; margin: 0.2rem 0 0.55rem; }
.scan-picks { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-bottom: 0.45rem; }
.scan-modes { display: flex; flex-wrap: wrap; gap: 0.9rem; margin: 0.8rem 0 0.2rem; }
.scan-modes label {
  display: flex; align-items: center; gap: 0.35rem;
  font-size: 0.8rem; color: rgba(255, 255, 255, 0.75); cursor: pointer;
}
.scan-modes input { accent-color: #7c4dff; margin: 0; }
.scan-k { display: block; font-size: 0.8rem; color: rgba(255, 255, 255, 0.65); margin: 0.7rem 0 0.2rem; }
.scan-k input[type="number"] {
  width: 4.2rem;
  margin-left: 0.3rem;
  padding: 0.1rem 0.35rem;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 5px;
  color: #fff;
  font: inherit;
  font-variant-numeric: tabular-nums;
}
#scan-k { width: 100%; accent-color: #7c4dff; background: none; border: 0; padding: 0; }
#scan-paper-search {
  width: 100%;
  /* Capped rather than full-bleed: the pane is wide enough that a 100% field
     read as a page-width bar rather than a search box. The period select below
     it shares the cap, so the two line up. */
  max-width: var(--scan-field);
  font-size: 0.85rem;
  padding: 0.32rem 0.55rem;
  margin: 0.5rem 0 0.2rem;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: #fff;
}
#scan-paper-search::placeholder { color: rgba(255, 255, 255, 0.35); }
#scan-paper-results { display: flex; flex-direction: column; gap: 0.1rem; margin-bottom: 0.4rem; }
#scan-paper-results .quick-item { text-align: left; background: none; border: 0; cursor: pointer; }
/* The plain .quick-item.selected fill loses to the id above it, and the pane is
   dark — so say it again here, in the same purple the other dark views use. */
#scan-paper-results .quick-item:hover,
#scan-paper-results .quick-item.selected { background: rgba(124, 77, 255, 0.25); }

.scan-buttons { display: flex; gap: 0.6rem; margin: 0.9rem 0 0.4rem; }
#scan-msg { min-height: 1.2em; font-size: 0.82rem; color: #7ee787; margin: 0.2rem 0 0.6rem; }
#scan-msg.error { color: #ff8093; }
.scan-hit:hover { background: rgba(255, 209, 102, 0.10); }
.scan-hit { cursor: default; padding: 0.5rem 0.2rem; border-top: 1px solid rgba(255, 255, 255, 0.09); }
.scan-hit h3 { margin: 0; font-size: 0.9rem; font-weight: 500; }
.scan-hit p { margin: 0.15rem 0 0; font-size: 0.75rem; color: rgba(255, 255, 255, 0.45); }

#latent-view {
  position: fixed;
  inset: 0;
  z-index: 50;                 /* under the overlay ladder, like #spar-view */
  display: grid;
  grid-template-columns: var(--latent-split, 42%) 10px 1fr;
  grid-template-rows: 100%;    /* pin the row, or a tall child grows the grid */
  overflow: hidden;
  background: #05060f;
  color: #e9eaf5;
}

/* A scrim, not a wall: opaque enough at the far left to read against, thinning
   across the pane so stars drift visibly behind the titles. The text carries a
   shadow so a bright star passing under a line can't wash it out. */
#latent-left {
  position: relative;
  z-index: 2;
  min-width: 0;
  overflow-y: auto;
  padding: 1.2rem 1.4rem 2rem;
  background: linear-gradient(to right,
    rgba(11, 9, 16, 0.93) 0%,
    rgba(11, 9, 16, 0.86) 30%,
    rgba(11, 9, 16, 0.66) 62%,
    rgba(11, 9, 16, 0.30) 88%,
    rgba(11, 9, 16, 0) 100%);
  text-shadow: 0 1px 7px rgba(0, 0, 0, 0.95), 0 0 2px rgba(0, 0, 0, 0.9);
}

#latent-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; margin-bottom: 0.9rem;
}
#latent-title {
  margin: 0; font-size: 1.05rem; font-weight: 200;
  letter-spacing: 0.12em; text-transform: uppercase;
  text-shadow: 0 0 32px rgba(140, 120, 255, 0.55);
  cursor: pointer;
  transition: color 0.12s ease;
}
#latent-title:hover { color: #fff; text-shadow: 0 0 38px rgba(160, 140, 255, 0.8); }
#latent-head .linklike { color: rgba(255, 255, 255, 0.6); font-size: 0.78rem; }
#latent-head .linklike:hover { color: #fff; }

/* One row: the search gives up whatever the two controls need, rather than the
   controls wrapping onto a line of their own. */
#latent-controls { display: flex; gap: 0.4rem; align-items: center; }

#latent-search {
  flex: 1;
  min-width: 5rem;             /* shrink before wrapping, but stay usable */
  font-size: 0.85rem;
  padding: 0.4rem 0.6rem;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.16);
  color: #e9eaf5;
}

/* Same translucent glass as the search box. appearance:none is what lets that
   through — otherwise macOS paints its own opaque bezel over any background
   set here, which is what made the tag select look heavy earlier. */
#latent-status, #latent-sort {
  appearance: none;
  -webkit-appearance: none;
  flex: none;
  font-size: 0.78rem;
  padding: 0.4rem 1.4rem 0.4rem 0.6rem;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.06)
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='7' height='4'%3E%3Cpath d='M0 0l3.5 4L7 0z' fill='%23ffffff' fill-opacity='.45'/%3E%3C/svg%3E")
    no-repeat right 0.5rem center;
  border: 1px solid rgba(255, 255, 255, 0.16);
  color: rgba(255, 255, 255, 0.82);
  cursor: pointer;
}
#latent-status:hover, #latent-sort:hover { border-color: rgba(150, 135, 255, 0.7); color: #fff; }
#latent-status:focus, #latent-sort:focus { outline: none; border-color: rgba(150, 135, 255, 0.9); }
/* the popup list is drawn by the OS, so keep its own text legible */
#latent-status option, #latent-sort option { color: #1a1a1a; background: #fff; }
#latent-search::placeholder { color: rgba(255, 255, 255, 0.35); }
#latent-search:focus { outline: none; border-color: rgba(150, 135, 255, 0.8); }

#latent-folders {
  display: flex; flex-wrap: wrap; gap: 0.35rem;
  margin: 0.7rem 0 0.5rem;
}
.latent-chip {
  display: inline-flex; align-items: center; gap: 0.4rem;
  border-radius: 999px;
  padding: 0.2rem 0.7rem;
  font-size: 0.78rem;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.16);
  color: rgba(255, 255, 255, 0.75);
}
.latent-chip i { width: 9px; height: 9px; border-radius: 50%; display: inline-block; flex: none; }
.latent-chip:hover { border-color: rgba(150, 135, 255, 0.8); color: #fff; filter: none; }
.latent-chip.on { background: rgba(255, 255, 255, 0.16); color: #fff; border-color: rgba(255, 255, 255, 0.4); }

#latent-count, #latent-unplaced {
  margin: 0 0 0.6rem; font-size: 0.74rem; color: rgba(255, 255, 255, 0.4);
}
#latent-unplaced { font-style: italic; }

.latent-row {
  padding: 0.55rem 0.2rem;
  border-top: 1px solid rgba(255, 255, 255, 0.09);
  cursor: pointer;
}
.latent-row:hover { background: rgba(124, 77, 255, 0.12); }
/* The way back out of a soloed paper. Quiet — the paper is the subject here,
   and this is a door, not a control worth looking at. */
.latent-unsolo {
  display: block;
  width: auto;
  margin: 0 0 0.6rem;
  padding: 0;
  border: 0;
  background: none;
  font-size: 0.8rem;
  color: rgba(255, 255, 255, 0.5);
  cursor: pointer;
}
.latent-unsolo:hover { color: #fff; filter: none; }
/* No band behind an open paper: the card is already its own surface, and a
   tint around it just draws a second box around the first. The hover and
   keyboard-highlight tints have to be beaten too — an open row is usually also
   the selected one, so .keysel's fill and its left bar were what remained
   visible around the card. */
.latent-row.on,
.latent-row.on:hover,
.latent-row.on.keysel {
  background: none;
  box-shadow: none;
  padding: 0.35rem 0;
}
/* "Here it is", on arrival from a search elsewhere. The library's .card.flash
   is a blue ring on a near-white card and reads as nothing at all over the star
   field, so this is the same idea in the field's own accent. It fades rather
   than sits: .keysel is the steady cursor, this one announces and goes.
   Deliberately below .on — arriving always opens the paper too, so the row is
   .on as well, and the rule that blanks an open row's background is exactly
   what this has to outrank for the 1.6s it lasts. Equal specificity, so source
   order decides; moving this block above .on silently kills it. */
.latent-row.flash {
  background: rgba(124, 77, 255, 0.22);
  box-shadow: inset 0 0 0 1px rgba(124, 77, 255, 0.55);
  transition: background 0.5s, box-shadow 0.5s;
}
/* Direct children only: an open row now holds a whole .card, and a descendant
   selector here would outrank .meta and .notes-view p inside it. The row's own
   title and meta line are direct children, so nothing about them changes. */
.latent-row > h3 {
  margin: 0; font-size: 0.9rem; font-weight: 500; color: #fff;
}
.latent-row > p { margin: 0.15rem 0 0; font-size: 0.75rem; color: rgba(255, 255, 255, 0.45); }
.latent-abstract {
  margin: 0.5rem 0 0.2rem;
  font-size: 0.85rem; line-height: 1.5;
  color: rgba(255, 255, 255, 0.78);
}

/* --- the library card, dropped into a dark pane ---------------------------
   Opening a row builds the *same* .card the library builds, so not one of the
   card rules at the top of this file may change — they are only re-pointed at
   a dark palette for this subtree. Almost every card rule already reads a
   custom property, so redefining the properties on the card does most of the
   work; what follows the block is the handful of hard-coded colours.
   #scan-results rides along: "Retrieve details" builds explore cards into the
   scrape pane, which is the same near-black ground. */
#latent-left .card,
#scan-results .card {
  --card: #15121f;              /* the card surface itself */
  --bg: #100e1a;                /* recessed inside it: tag picker, notes hover */
  --text: #ecebf7;
  --muted: rgba(236, 235, 247, 0.55);
  --border: rgba(255, 255, 255, 0.15);
  --chip: rgba(255, 255, 255, 0.08);
  --accent: #b3a6ff;            /* light violet: it is text on dark here */
  --danger: #ff8f85;
  color: var(--text);
  /* Native menus, caret and scrollbars follow the surface they sit on */
  color-scheme: dark;
  /* A solid surface doesn't need the pane's anti-star shadow, and carrying it
     into small dark-on-light text only smears it. */
  text-shadow: none;
  margin: 0.6rem 0 0.2rem;
  /* auto, not default: undo the row's pointer without also taking the I-beam
     off the abstract — the card is something you read and edit, not a target */
  cursor: auto;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
}
/* The pane is 42% wide, so the head has to be allowed to break */
#latent-left .card-head { flex-wrap: wrap; }
/* Fields read as fields rather than as more card surface */
#latent-left .card input,
#latent-left .card select,
#latent-left .card textarea { background: rgba(255, 255, 255, 0.06); }
#latent-left .card .tag-input { background: transparent; }
/* Hard-coded in the light theme, all three unreadable on this ground */
#latent-left .card .abstract,
#scan-results .card .abstract { color: rgba(236, 235, 247, 0.82); }
#latent-left .card .status-to_read { color: #e6c15c; }
#latent-left .card .status-read { color: #7fd99a; }
/* The filled buttons in a card: white on light violet is ~2:1, so the text goes
   dark instead of the fill going pale. */
#latent-left .card .save-notes,
#scan-results .card .explore-add { color: #1b1730; border-color: transparent; }
/* Only the card is a solid surface; a dismissed row stays on the pane itself,
   where the light theme's grey would vanish into the starfield. */
#scan-results .explore-dismissed { border-top-color: rgba(255, 255, 255, 0.09); }
#scan-results .explore-dismissed-text { color: rgba(255, 255, 255, 0.45); }
#scan-results .explore-dismissed .linklike { color: #b3a6ff; }
/* The one thing the card can't do: jump to this paper in the text library */

/* No visible seam: the reading ground bleeds into the field instead. The
   column stays a real 10px drag target, but paints nothing until you point at
   it, when a 1px line appears to say it can be grabbed. */
#latent-divider {
  position: relative;
  z-index: 3;                 /* above #latent-canvas (1), so the fade covers it */
  background: none;
  cursor: col-resize;
}
#latent-divider::after {      /* the handle, only while pointed at */
  content: "";
  position: absolute;
  top: 0; bottom: 0; left: 50%;
  width: 1px; transform: translateX(-0.5px);
  background: rgba(150, 135, 255, 0.9);
  opacity: 0;
  transition: opacity 0.12s ease;
}
#latent-divider:hover::after,
body.dragging-split #latent-divider::after { opacity: 1; }

/* Only a positioning context for the two controls now — it must not intercept
   the drags and clicks meant for the canvas lying underneath it. */
#latent-right { position: relative; z-index: 2; min-width: 0; background: none; pointer-events: none; }
#latent-actions, #latent-play, #latent-settings { pointer-events: auto; }
#scan-play { pointer-events: auto; }
/* The gear takes the corner line to itself; the actions column starts below
   it, so no text ever runs alongside or under the icon. */
#latent-settings {
  position: absolute;
  z-index: 3;
  top: 0.6rem;
  right: 0.9rem;
  font-size: 1.2rem;
  line-height: 1;
  padding: 0.15rem 0.25rem;
  opacity: 0.42;
}
#latent-settings:hover { opacity: 0.95; filter: none; }
#latent-view::before, #latent-view::after {
  content: "";
  position: absolute; inset: -25%;
  z-index: 0; pointer-events: none;
  filter: blur(60px);
  background:
    radial-gradient(40% 35% at 30% 30%, rgba(124, 77, 255, 0.32), transparent 70%),
    radial-gradient(35% 30% at 72% 62%, rgba(0, 170, 255, 0.20), transparent 70%);
  animation: nebula-drift 50s ease-in-out infinite alternate;
}
#latent-view::after {
  background:
    radial-gradient(30% 25% at 68% 22%, rgba(255, 80, 190, 0.10), transparent 70%),
    radial-gradient(45% 40% at 22% 78%, rgba(70, 110, 255, 0.18), transparent 70%);
  animation-duration: 70s;
  animation-direction: alternate-reverse;
}
/* Full bleed, under both panes. Absolute takes it out of the grid flow. */
#latent-canvas { position: absolute; inset: 0; z-index: 1; }
/* Play/pause for the drift. Interacting with the field stops it — this is how
   you start it again without having to nudge it by hand. */
#latent-play, #scan-play {
  position: absolute; z-index: 4; right: 1.1rem; bottom: 0.9rem;
  width: 2rem; height: 2rem;
  display: flex; align-items: center; justify-content: center;
  padding: 0; border-radius: 50%;
  font-size: 0.72rem; line-height: 1;
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: rgba(255, 255, 255, 0.55);
}
#latent-play:hover, #scan-play:hover { color: #fff; border-color: rgba(150, 135, 255, 0.8); filter: none; }
#latent-play.on, #scan-play.on { color: #fff; border-color: rgba(255, 255, 255, 0.35); }

/* Shown instead of the toggle when the window is too narrow to split — better
   than a button vanishing with no explanation. */
#latent-hint { font-size: 0.78rem; color: var(--muted); font-style: italic; }

/* Rerun controls, floated over the top-right of the field. Same recessive
   weight as the map's own actions — they must not compete with the stars. */
#latent-actions {
  /* Clears the gear's own line (0.6rem top + 1.5rem tall) plus the column gap,
     so the icon sits alone above the first rerun instead of beside it. */
  position: absolute; z-index: 2; top: 2.35rem; right: 0.9rem;   /* same right edge as #latent-settings */
  display: flex; flex-direction: column; align-items: flex-end; gap: 0.25rem;
  text-align: right;
}
#latent-actions button {
  background: none; border: 0; padding: 0;
  font: inherit; font-size: 0.74rem;
  color: rgba(255, 255, 255, 0.42);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}
#latent-actions button:hover:not(:disabled) { color: #fff; filter: none; }
#latent-actions button:disabled { opacity: 0.3; cursor: default; text-decoration: none; }

/* Keyboard highlight in the latent list — distinct from .on (the opened paper)
   so arrowing past an open row is still legible. */
.latent-row.keysel {
  background: rgba(124, 77, 255, 0.16);
  box-shadow: inset 3px 0 0 rgba(150, 135, 255, 0.95);
}

/* Keyboard highlight in the latent list — distinct from .on (the opened paper)
   so arrowing past an open row stays legible. */
.latent-row.keysel {
  background: rgba(124, 77, 255, 0.16);
  box-shadow: inset 3px 0 0 rgba(150, 135, 255, 0.95);
}

/* Narrow windows: stop splitting proportionally. The reading column takes
   nearly all the width and the field becomes the layer behind it — cropped,
   with stars rotating out of frame, which is the graceful way for it to give
   ground. A saved divider position is deliberately ignored here; it was chosen
   at a width that no longer exists. */
@media (max-width: 860px) {
  #latent-view { grid-template-columns: 88% 10px 1fr; }
  /* The reruns would sit on top of the text at this width. Escape and the
     title still leave latent mode, so nothing here is the only way out. */
  #latent-rerun, #latent-rerun-all { display: none; }
}
@media (max-width: 620px) {
  #latent-view { grid-template-columns: 100% 0 0; }
  #latent-divider { display: none; }   /* nothing left to drag */
  #latent-left { padding: 1rem 1.1rem 2rem; }
}

/* Tag row: same chips as the folders, one step quieter — folders are the
   primary grouping here, tags are a refinement on top of them. */
#latent-tags { display: flex; flex-wrap: wrap; gap: 0.35rem; margin: 0 0 0.5rem; }

/* Below the width where a wrapped tag row would cost more than one line, keep
   it to exactly one line and scroll it instead. The vertical budget is what was
   ever scarce here — the tags themselves were never the problem. */
@media (max-width: 1000px) {
  #latent-tags {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;             /* the chips are the affordance */
    -webkit-overflow-scrolling: touch;
  }
  #latent-tags::-webkit-scrollbar { display: none; }
  #latent-tags .latent-chip { flex: none; }
}
#latent-tags .latent-chip { font-size: 0.74rem; padding: 0.15rem 0.6rem; }
.latent-more { border-style: dashed; }

/* A paper in the library but not in the field */
.latent-nostar {
  margin-left: 0.4rem;
  font-size: 0.85em;
  color: rgba(255, 255, 255, 0.4);
  cursor: help;
}

/* ---------------------------------------------------------------------------
   Sign in

   Opaque rather than the translucent wash the other overlays use, and above
   all of them (the confirm modal tops out at 300). Nothing of a library should
   be visible or reachable behind this.
   --------------------------------------------------------------------------- */
#auth-gate {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #0b0c16;
}

#auth-box {
  width: min(360px, 88vw);
  padding: 2rem 1.8rem;
  border-radius: 14px;
  background: rgba(22, 24, 45, 0.92);
  border: 1px solid rgba(255, 255, 255, 0.15);
  box-shadow: 0 0 60px rgba(124, 77, 255, 0.25);
  color: #e9eaf5;
  text-align: center;
}

#auth-brand {
  margin: 0;
  font-weight: 300;
  font-size: 1.35rem;
  letter-spacing: 0.06em;
}

/* The sign-in form rewrites its own line by id; the forgot / reset / email
   panels carry static text and share the styling by class. */
#auth-sub, .auth-sub {
  margin: 0.8rem 0 1.4rem;
  font-size: 0.85rem;
  color: rgba(255, 255, 255, 0.65);
}

.auth-field { display: block; text-align: left; margin-bottom: 0.9rem; }

.auth-field span {
  display: block;
  margin-bottom: 0.3rem;
  font-size: 0.72rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.55);
}

.auth-field input {
  width: 100%;
  padding: 0.55rem 0.7rem;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: #e9eaf5;
  font-size: 0.95rem;
}
.auth-field input:focus { outline: 2px solid #8f7bff; outline-offset: -1px; }

#auth-submit {
  width: 100%;
  margin-top: 0.4rem;
  padding: 0.55rem 1rem;
  border-radius: 999px;
  background: linear-gradient(135deg, #7c4dff, #3f8cff);
  border: 1px solid transparent;
  color: #fff;
  font-size: 0.95rem;
}
#auth-submit:hover { filter: brightness(1.15); }
#auth-submit:disabled { filter: grayscale(0.7) brightness(0.75); cursor: default; }

/* The two ways off the sign-in form, styled as one pair. Neither is the
   primary action — the button above is — so they match each other rather than
   compete, and .linklike's accent colour is overridden for that reason.
   Symmetric side margins, not a gap on one of them: "Forgot your password?" is
   hidden while creating an account, and a one-sided margin would quietly
   knock the remaining link off centre. */
#auth-toggle,
#auth-forgot {
  margin: 0.9rem 0.7rem 0;
  font-size: 0.85rem;
  color: rgba(255, 255, 255, 0.7);
  /* Underlined at rest, because once both are the same quiet white there is
     nothing else left to say they can be clicked. Faint enough not to shout. */
  text-decoration: underline;
  text-decoration-color: rgba(255, 255, 255, 0.28);
  text-underline-offset: 3px;
}
#auth-toggle:hover,
#auth-forgot:hover {
  color: #fff;
  text-decoration-color: rgba(255, 255, 255, 0.65);
}

/* Errors from the server are the only feedback here, so they sit above the
   button where the eye already is, not below the fold of a short box. */
#auth-error {
  margin: 0 0 0.9rem;
  font-size: 0.85rem;
  color: #ff8093;
}

#opt-logout { color: #ff8093; }
#opt-logout:hover { border-color: rgba(255, 128, 147, 0.8); box-shadow: 0 0 20px rgba(255, 128, 147, 0.25); }

/* ---------------------------------------------------------------------------
   AI, off by default

   The switch is the whole of Settings → AI until it is on; the model pickers
   below it are hidden rather than disabled, because a chooser for a feature you
   cannot reach is furniture.

   body.no-ai hides every entry point in one rule rather than a check inside
   each render path — the sparring button is built in renderCard(), which runs
   for every paper, and threading a flag through there would mean remembering it
   at each new call site. The server refuses these routes regardless; this is
   only so nothing offers what would be refused.
   --------------------------------------------------------------------------- */
.ai-toggle {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin: 0 0 0.6rem;
  cursor: pointer;
  font-size: 0.95rem;
}
.ai-toggle input { width: 1rem; height: 1rem; accent-color: #7c4dff; cursor: pointer; }

body.no-ai .init-annot.spar-entry,
body.no-ai #spar-annotate { display: none; }

/* The GoodNotes and Finder actions drive the desktop of whichever machine is
   hosting the library. When that is not your Mac they cannot work, so they are
   not offered — see native_host() in main.py. */
body.no-native .native-only { display: none; }

/* The optional welcome-email offer, revealed under Back/Save once an address
   has been saved. It needs real air above it: it appears after the fact, so
   without the gap it reads as a caption on the Save button rather than as a
   separate thing being offered. */
#email-verify { margin-top: 1.8rem; }
/* The one second of green before the panel closes. Same green as the drop
   card's Add, so "this worked" looks the same everywhere in the app. */
#email-verify-send.sent {
  background: #1e8f57;
  border-color: #1e8f57;
  color: #fff;
}
#email-verify .settings-text { margin-bottom: 1.1rem; }

/* The button a GoodNotes cover link was pointing at. Three pulses, not a
   permanent state: it says "here" on arrival and then gets out of the way. */
@keyframes wanted-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(120, 170, 255, 0); }
  50%      { box-shadow: 0 0 0 4px rgba(120, 170, 255, 0.35); }
}
.init-annot.wanted {
  animation: wanted-pulse 1.1s ease-in-out 3;
  border-color: rgba(120, 170, 255, 0.7);
}
