/* ai-haystack assistant — a precision instrument, not a room.
   Near-black flat surfaces, hairline rules, tabular numerals, mono uppercase micro-labels,
   and one acid accent that means something: lime while the generator is in the request path,
   cool cyan when it has been switched out. Fraunces survives as the single brand artifact —
   the wordmark only; everything else is sans/mono. */

:root {
  color-scheme: light dark;

  --ground:  #0B0C0E;
  --surface: #111318;
  --raise:   #181B21;
  --rule:    #23262E;
  --text:    #E7E9EE;
  --muted:   #7E8592;
  /* Contrast-floored against --on-accent in both palettes so the 11px uppercase label on a
     solid accent fill clears the 4.5 floor: lime on #0B0C0E is 15.1:1, cyan 11.3:1; in light,
     white on #537709 is 5.24:1 and on the cyan 4.81:1. */
  --accent:  #C6F24E;
  --accent-search: #67D4E7;
  --ok:      #7BE0A2;
  --warn:    #E8B84B;
  --danger:  #F0644F;
  --on-accent: #0B0C0E;

  --mono: ui-monospace, "JetBrains Mono", "IBM Plex Mono", SFMono-Regular, Menlo, Consolas, monospace;
  --sans: ui-sans-serif, system-ui, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  --serif: "Fraunces", "Iowan Old Style", Georgia, serif;

  --step: 0.5rem;
  --radius: 6px;
  --radius-lg: 10px;
  /* The admin views (corpus, ingest, users, connectors) sit in a measure of their own; the
     thread has the 72ch reading column instead. */
  --view-max: 1180px;
  /* The thread's reading column, in rem rather than ch so the question, the answer and the
     composer — three different type sizes — agree on one width. Fluid: 680px on a laptop,
     growing with the viewport up to 1024px so a wide monitor is not two-thirds empty. */
  --measure: clamp(42.5rem, 60vw, 64rem);
}

/* The device prefers light and the user has not pinned a choice: ink-on-paper values with
   the same geometry. Kept in pair with the explicit light block below — both must declare
   the same light palette tokens. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    --ground:  #F5F6F7;
    --surface: #FFFFFF;
    --raise:   #EDEFF2;
    --rule:    #D9DCE1;
    --text:    #141519;
    --muted:   #5C6470;

    --accent:        #537709;
    --accent-search: #0E7C9A;
    --ok:            #1E7A46;
    --warn:          #8A5A00;
    --danger:        #B93A22;
    --on-accent: #FFFFFF;
  }
}

/* Explicit dark choice overrides an OS set to light. Both dark reads must agree. */
:root[data-theme="dark"] {
  --ground:  #0B0C0E;
  --surface: #111318;
  --raise:   #181B21;
  --rule:    #23262E;
  --text:    #E7E9EE;
  --muted:   #7E8592;

  --accent:  #C6F24E;
  --accent-search: #67D4E7;
  --ok:      #7BE0A2;
  --warn:    #E8B84B;
  --danger:  #F0644F;
  --on-accent: #0B0C0E;
}

:root[data-theme="light"] {
  color-scheme: light;
  --ground:  #F5F6F7;
  --surface: #FFFFFF;
  --raise:   #EDEFF2;
  --rule:    #D9DCE1;
  --text:    #141519;
  --muted:   #5C6470;

  --accent:        #537709;
  --accent-search: #0E7C9A;
  --ok:            #1E7A46;
  --warn:          #8A5A00;
  --danger:        #B93A22;
  --on-accent: #FFFFFF;
}
:root[data-theme="dark"] { color-scheme: dark; }

/* Retrieval only: the whole page cools when the model leaves the path. Same hook as the
   old --sage swap; every component that follows --accent picks it up. */
body[data-mode="search"] {
  --accent: var(--accent-search);
}

/* Citation chips: accent text on an accent tint in dark; in light that tint left the lime at
   4.25:1 and the cyan under 4, so there the chip fills solid and takes the on-accent text.
   On body rather than :root so the search-mode --accent above reaches into the mix. */
body {
  --cite-bg: color-mix(in srgb, var(--accent) 10%, transparent);
  --cite-fg: var(--accent);
  --cite-hover: color-mix(in srgb, var(--accent) 22%, transparent);
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme]) body {
    --cite-bg: var(--accent);
    --cite-fg: var(--on-accent);
    --cite-hover: color-mix(in srgb, var(--accent) 85%, black);
  }
}

:root[data-theme="light"] body {
  --cite-bg: var(--accent);
  --cite-fg: var(--on-accent);
  --cite-hover: color-mix(in srgb, var(--accent) 85%, black);
}

/* Vendored, not fetched from a CDN (same rule as vendor/marked.min.js). Variable files,
   latin subset only — the UI and its corpora are Latin-script. */
@font-face {
  font-family: "Fraunces";
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("vendor/fonts/Fraunces-var.woff2") format("woff2");
}

@font-face {
  font-family: "Fraunces";
  font-style: italic;
  font-weight: 400 600;
  font-display: swap;
  src: url("vendor/fonts/Fraunces-italic-var.woff2") format("woff2");
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
}

body {
  margin: 0;
  display: grid;
  grid-template-rows: auto auto 1fr; /* masthead · collection-bar · shell */
  min-height: 100%;
  background: var(--ground);
  color: var(--text);
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1.65;
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
}

/* Data voice: filenames, scores, dates, stats, status lines, compact buttons and the
   masthead labels. Mono uppercase micro-labels — never prose. */
.label,
#path .node,
#conversation-list .meta,
.rewrite,
.hit .score,
.hit .name,
.sources summary,
.banner,
#views button,
#corpus-stats,
#corpus-range,
#source-list .meta,
.confirm span,
.chunks .rank,
.chunks .chars,
.rail-head .label,
#grant-list .level {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}

/* The UA's own `[hidden] { display: none }` is an author-weight rule this stylesheet
   outranks: anything given a `display` here — the confirm row, the pager — stayed on screen
   with its `hidden` property set, so every row asked to be deleted at once. */
[hidden] {
  display: none !important;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}

/* --- primitives ---------------------------------------------------------------------- */
/* The admin views share four pieces of chrome, and nothing else: a view head, a button in
   three tiers, a panel, and a pill. Everything below the masthead is built from them, so
   the corpus, the users list and a connector form read as the same instrument. */

/* One title per view, in the body sans: the mono micro-label is for sub-sections, and a
   page that opens on one had nothing to lead with. */
.view-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.75rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--rule);
}

.view-title {
  flex: 1 1 16rem;
  min-width: 0;
  margin-right: auto;
}

.view-title h2 {
  margin: 0;
  font-size: 21px;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.012em;
}

.view-title p,
.view-title > span {
  display: block;
  margin: 0.2rem 0 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted);
}

.view-head [role="status"] {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted);
}

.view-head [role="status"]:empty {
  display: none;
}

/* Buttons. Outlined is the default; .primary is the one action a view exists for, and
   there is at most one on screen; .ghost is for row controls and anything that repeats;
   .danger is outlined red and fills on hover, so the colour is not the only warning. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  line-height: 1.2;
  padding: 0.45rem 0.75rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--muted);
  white-space: nowrap;
  transition: color 120ms ease-out, border-color 120ms ease-out, background 120ms ease-out;
}

.btn:hover:not(:disabled) {
  border-color: var(--accent);
  color: var(--text);
}

.btn:disabled {
  opacity: 0.45;
  cursor: default;
}

.btn.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
}

.btn.primary:hover:not(:disabled) {
  color: var(--on-accent);
  filter: brightness(1.07);
}

.btn.ghost {
  border-color: transparent;
  background: transparent;
}

.btn.ghost:hover:not(:disabled) {
  border-color: transparent;
  background: var(--raise);
}

.btn.danger {
  border-color: color-mix(in srgb, var(--danger) 55%, var(--rule));
  color: var(--danger);
}

.btn.danger:hover:not(:disabled) {
  background: var(--danger);
  border-color: var(--danger);
  color: var(--on-accent);
}

/* A panel is a bounded thing with a head: a list of grants, the jobs in flight, a form.
   Lists inside one rule their rows with hairlines and carry the panel's inset themselves. */
.panel {
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
}

.panel-head {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 0.9rem;
  border-bottom: 1px solid var(--rule);
}

.panel-head .label {
  margin: 0 auto 0 0;
  color: var(--muted);
}

.panel-head [role="status"] {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted);
}

.panel-head [role="status"]:empty {
  display: none;
}

.panel-foot {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 0.9rem;
  border-top: 1px solid var(--rule);
}

.panel > ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

.panel > ul > li {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.9rem;
  font-size: 13px;
}

.panel > ul > li + li {
  border-top: 1px solid var(--rule);
}

/* A pill is a state: a run's outcome, a user's flag, whether a connector answers. The dot
   carries the colour so the word is never the only channel, and the word is never the
   colour's only channel either. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.02em;
  line-height: 1.5;
  padding: 0.1rem 0.55rem 0.1rem 0.5rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  color: var(--muted);
  white-space: nowrap;
  vertical-align: middle;
}

.pill::before {
  content: "";
  flex: none;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.85;
}

.pill.ok     { color: var(--ok);     border-color: color-mix(in srgb, var(--ok) 45%, var(--rule)); }
.pill.warn   { color: var(--warn);   border-color: color-mix(in srgb, var(--warn) 45%, var(--rule)); }
.pill.danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 45%, var(--rule)); }
.pill.accent { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, var(--rule)); }

/* Still moving: the dot blinks with the same cadence as the streaming cursor. */
.pill.live { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, var(--rule)); }
.pill.live::before { animation: blink 1s steps(2, start) infinite; }

/* A kind, not a state: no dot, a filled ground. */
.pill.type {
  background: var(--raise);
  color: var(--text);
}

.pill.type::before { display: none; }

/* Form fields in the admin views: caption over control. The auth screen has its own copy
   of this scoped to #auth-form, with the wordmark's spacing. */
.field {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  min-width: 0;
  font-size: 13px;
}

.field > span:first-child {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
}

/* A checkbox is the one control that reads better beside its caption than under it. */
.field:has(> input[type="checkbox"]) {
  flex-direction: row-reverse;
  justify-content: flex-end;
  align-items: center;
  gap: 0.5rem;
}

.field-unit {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  align-self: flex-end;
}

.field-unit input {
  flex: 1;
  min-width: 0;
}

/* The number takes the slack; the unit dropdown stays at its content width. */
.field-unit select {
  flex: 0 0 auto;
}

/* The "at HH:MM" pair inside the schedule picker row. */
.field-unit label {
  display: flex;
  align-items: center;
  gap: 0.35rem;
}

/* The server zone after a schedule time: the picker's "at" and the table's header. */
#source-schedule-at-tz,
.th-tz {
  color: var(--muted);
  text-transform: none;
  letter-spacing: 0;
}

/* One input, everywhere outside the composer and the rail. The stylesheet used to give
   each form its own copy, and the connector form got none at all. The :is() carries the
   view ids, so this is (1,0,0): an override has to carry an id of its own, as the per-form
   `#user-create input` rules do — a class-only rule inside these views loses to it. */
:is(#corpus, #ingest, #users, #connectors) :where(input:not([type="checkbox"]):not([type="file"]), select, textarea) {
  font: inherit;
  font-size: 13px;
  line-height: 1.4;
  color: var(--text);
  background: var(--ground);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 0.4rem 0.6rem;
  min-width: 0;
}

:is(#corpus, #ingest, #users, #connectors) :where(input, select, textarea):focus {
  border-color: var(--accent);
}

:is(#corpus, #ingest, #users, #connectors) textarea {
  min-height: 4.5rem;
  resize: vertical;
  font-family: var(--mono);
  font-size: 12px;
}

:is(#corpus, #ingest, #users, #connectors) input::placeholder {
  color: color-mix(in srgb, var(--muted) 70%, transparent);
}

/* A table in a panel. No overflow here at desktop widths: a scroll container would clip
   the row menus and delete questions that hang off the last row. Narrow viewports trade
   that for a sideways scroll and lay those popovers inline instead (see the media block).
   Disappears with the table when app.js hides it. */
.table-wrap {
  flex: none;
  min-width: 0;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
}

.table-wrap:has(> table[hidden]) {
  display: none;
}

.table-wrap table {
  border-collapse: collapse;
  width: 100%;
}

.table-wrap th,
.table-wrap td {
  text-align: left;
  padding: 0.6rem 0.9rem;
  border-bottom: 1px solid var(--rule);
  vertical-align: middle;
}

.table-wrap th {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 500;
  white-space: nowrap;
}

.table-wrap td {
  font-size: 13px;
}

.table-wrap tbody tr:last-child td {
  border-bottom: 0;
}

.table-wrap tbody tr:hover td {
  background: color-mix(in srgb, var(--raise) 55%, transparent);
}

.table-wrap tr.disabled td.who {
  color: var(--muted);
}

/* The row's subject: the one column in the text colour and the semibold. */
.table-wrap td.who {
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
}

.table-wrap .th-actions {
  text-align: right;
}

/* Stays a table cell (a flex td would stop stretching to the row's height and break the
   hover wash); the controls sit inline, and the menu and the delete question hang off it. */
.actions {
  position: relative;
  text-align: right;
  white-space: nowrap;
}

.actions > .btn,
.actions > .menu-toggle {
  display: inline-flex;
  vertical-align: middle;
}

.actions > * + * {
  margin-left: 0.3rem;
}

.actions .btn {
  padding: 0.3rem 0.6rem;
}

/* The "…" and its menu, as in the thread rail. The anchor span is the positioning
   context, so the menu hangs off the toggle however tall the row grows. */
.actions .menu-anchor {
  position: relative;
  display: inline-flex;
  align-items: center;
  vertical-align: middle;
}

.actions .menu-toggle {
  padding: 0.2rem 0.5rem;
  border-radius: var(--radius);
  color: var(--muted);
  font-size: 14px;
  line-height: 1;
}

.actions .menu-toggle:hover,
.actions .menu-toggle[aria-expanded="true"] {
  background: var(--raise);
  color: var(--text);
}

.actions .menu {
  position: absolute;
  top: calc(100% + 0.3rem);
  right: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  min-width: 9.5rem;
  padding: 0.3rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.28);
}

.actions .menu button {
  text-align: left;
  font-size: 12.5px;
  padding: 0.4rem 0.6rem;
  border-radius: var(--radius);
  color: var(--text);
}

.actions .menu button:hover {
  background: var(--raise);
}

.actions .menu button.danger {
  color: var(--danger);
}

/* The delete question hangs under the row's controls, where the menu that asked it was. */
.actions .confirm {
  position: absolute;
  top: calc(100% + 0.3rem);
  right: 0;
  z-index: 5;
  margin: 0;
  padding: 0.5rem 0.7rem;
  border: 1px solid color-mix(in srgb, var(--danger) 45%, var(--rule));
  border-radius: 8px;
  background: var(--surface);
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.28);
  white-space: nowrap;
}

/* --- masthead ------------------------------------------------------------------------ */

#masthead {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--step);
  padding: 0 0.75rem;
  height: 48px;
  background: var(--surface);
  border-bottom: 1px solid var(--rule);
}

/* The wordmark is the one piece of brand voice on the page: Fraunces italic, sentence
   case, no tracking. DOM order already matches visual order (toggle · wordmark · views ·
   session); the auto margins on #views centre it and push the session to the right edge. */
#masthead h1 {
  font-family: var(--serif);
  font-style: italic;
  font-weight: 500;
  font-size: 18px;
  letter-spacing: 0;
  text-transform: none;
  margin: 0;
}

#threads-toggle {
  display: none;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}

#threads-toggle::before {
  content: "☰ ";
}

/* The view switcher: one segmented control rather than loose buttons, so which view is
   showing reads off the shape as well as off the accent bar under the live segment. */
#views {
  display: flex;
  margin: 0 auto;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  overflow: hidden;
}

#views button {
  padding: 0.35rem 0.7rem;
  color: var(--muted);
  transition: color 120ms ease-out, background 120ms ease-out;
}

#views button + button {
  border-left: 1px solid var(--rule);
}

/* The container clips at its rounded edge, so the page's outset focus ring on a segment is
   cut down to a sliver on the segment's own border. Inset it: the ring stays inside the
   clip, and this is the app's primary navigation to reach by keyboard. */
#views button:focus-visible {
  outline-offset: -2px;
}

#views button:hover {
  color: var(--text);
}

/* The bar follows --accent, so it cools with the rest of the page when GENERATE leaves
   the request path. */
#views button[aria-current="page"] {
  background: var(--raise);
  color: var(--text);
  box-shadow: inset 0 -2px 0 var(--accent);
}

/* --- ingest -------------------------------------------------------------------------- */

body:not([data-view="ingest"]) #ingest {
  display: none;
}

/* The four admin views share one measure and one inset; the thread has its own column. */
#ingest,
#corpus,
#users,
#connectors {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  width: 100%;
  max-width: var(--view-max);
  min-width: 0;
  min-height: 0;
  margin: 0 auto;
  padding: 1.25rem 1.5rem 2rem;
  overflow-y: auto;
}

/* Under the view title, which mutes its paragraphs; the collection name is the point. */
.view-title .ingest-target {
  display: flex;
  gap: 0.5rem;
  align-items: baseline;
  color: var(--text);
}

.ingest-target .label {
  color: var(--muted);
}

/* Drop zone and stage on the left; what happened to earlier uploads, and what the indexer
   is still doing, on the right. One column below tablet width. */
.ingest-grid {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(280px, 2fr);
  gap: 1.25rem;
  align-items: start;
}

.ingest-main,
.ingest-side {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  min-width: 0;
}

#ingest-form {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

/* The panel is the target; the dashed rule says "drop here" without an icon. */
#ingest-drop {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  padding: 3.25rem 1rem;
  border: 1.5px dashed var(--rule);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--muted);
  font-size: 14px;
  transition: border-color 150ms ease-out, background 150ms ease-out;
}

#ingest-drop.dropping {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 6%, var(--surface));
}

/* The native file input is unstyleable; the label is the control. */
#ingest-drop .file input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

#ingest-drop .file span {
  display: inline-block;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text);
  padding: 0.4rem 0.7rem;
  border: 1px solid color-mix(in srgb, var(--accent) 50%, var(--rule));
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
}

#ingest-drop .file input:focus-visible + span {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

#ingest-drop .file span:hover {
  border-color: var(--accent);
}

/* The stage's head is also its toolbar: what is staged on the left, Index on the right. */
.stage-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.stage-head .label {
  margin: 0 auto 0 0;
  color: var(--muted);
}

#stage-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

#stage-list:empty {
  display: none;
}

#stage-list li {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.45rem 0.9rem;
  background: var(--surface);
  border: 1px solid var(--rule);
}

#stage-list li:first-child { border-radius: var(--radius-lg) var(--radius-lg) 0 0; }
#stage-list li:last-child { border-radius: 0 0 var(--radius-lg) var(--radius-lg); }
#stage-list li + li { border-top: 0; }

#stage-list .name,
#upload-list .name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--mono);
  font-size: 12px;
}

#stage-list .meta,
#upload-list .meta,
#upload-list .time {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted);
  white-space: nowrap;
}

#stage-list .meta { margin-left: auto; }

#stage-list button {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted);
  padding: 0.15rem 0.4rem;
  border-radius: var(--radius);
}

#stage-list button:hover {
  color: var(--danger);
  background: var(--raise);
}

/* The right column's three panels. Uploads is always there and says so when empty; the
   other two appear only while they have something to show. */
.panel-note {
  margin: 0;
  padding: 0.6rem 0.9rem;
  font-size: 13px;
  color: var(--muted);
}

#upload-list .name { flex: 1; }
#upload-list .meta { margin-left: auto; }

#job-list .name,
#summary-list .name {
  font-family: var(--mono);
  font-size: 12px;
}

#summary-list .name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#summary-list .meta {
  margin-left: auto;
  max-width: 45%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted);
}

.upload-retry,
.summary-retry {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  padding: 0.25rem 0.55rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  color: var(--muted);
}

.upload-retry:hover,
.summary-retry:hover:not(:disabled) {
  border-color: var(--accent);
  color: var(--text);
}

/* --- shell --------------------------------------------------------------------------- */

#shell {
  position: relative;
  display: grid;
  grid-template-columns: 244px minmax(0, 1fr);
  min-height: 0;
}

/* --- threads rail -------------------------------------------------------------------- */

#conversations {
  display: flex;
  flex-direction: column;
  gap: var(--step);
  min-width: 0;
  padding: 0.75rem;
  background: var(--surface);
  border-right: 1px solid var(--rule);
  overflow-y: auto;
}

.rail-head {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.rail-head .label {
  margin: 0;
  color: var(--muted);
}

#conversation-status {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--danger);
}

#conversation-status:empty {
  display: none;
}

/* Scoped to the id, NOT to `#conversations button`: a descendant selector here also hit the
   per-row title and delete buttons, giving the delete a full-row flex basis and collapsing
   the title to nothing — a row that rendered as a stray "x". */
#new-chat {
  width: 100%;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  text-align: left;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  color: var(--muted);
}

#new-chat {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--rule));
  color: var(--text);
}

#new-chat:hover {
  border-color: var(--accent);
}

#conversation-list {
  list-style: none;
  margin: 0;
  padding: 0;
  min-width: 0;
}

#conversation-list li {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  gap: 0.25rem;
  min-width: 0;
  border-radius: var(--radius);
}

#conversation-list li + li {
  margin-top: 2px;
}

#conversation-list li:hover {
  background: var(--raise);
}

#conversation-list .title {
  min-width: 0;
  text-align: left;
  padding: 0.5rem 0.1rem 0.5rem 0.6rem;
  color: var(--text);
}

#conversation-list .title .name {
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: 13px;
  line-height: 1.35;
}

#conversation-list .meta {
  display: block;
  margin-top: 0.25rem;
  color: var(--muted);
}

/* The active thread is marked structurally, not by weight alone. */
#conversation-list li.active {
  background: var(--raise);
}

#conversation-list li.active::before {
  content: "";
  position: absolute;
  inset: 0.35rem auto 0.35rem 0;
  width: 2px;
  border-radius: 2px;
  background: var(--accent);
}

#conversation-list li.active .title .name {
  color: var(--text);
  font-weight: 600;
}

.rail-empty {
  margin: 0;
  padding: 0.5rem 0.1rem;
  font-size: 13px;
  color: var(--muted);
}

/* --- folders, search, menus ------------------------------------------------------------ */

#thread-search,
#folder-name {
  width: 100%;
  box-sizing: border-box;
  padding: 0.4rem 0.6rem;
  font: inherit;
  font-size: 13px;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
}

#viewing-wrap {
  display: flex;
  gap: 0.4rem;
  align-items: center;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
}

#viewing {
  flex: 1;
  min-width: 0;
  font: inherit;
  padding: 0.25rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
}

#folder-new {
  align-self: flex-start;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 0.25rem 0.1rem;
}

#folder-new:hover { color: var(--text); }

#folder-create-status {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--danger);
}

/* A folder section is a header over its own list; the grid rule for rows must not apply. */
#conversation-list li.folder {
  display: block;
  background: none;
}

.folder-head {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.35rem 0.1rem 0.2rem 0.4rem;
}

.folder-head .folder-name {
  flex: 1;
  min-width: 0;
  text-align: left;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.folder-head .folder-name[aria-expanded="false"]::before { content: "▸ "; }
.folder-head .folder-name[aria-expanded="true"]::before { content: "▾ "; }

.folder-head .folder-rename,
.folder-head .folder-delete {
  font-size: 11px;
  color: var(--muted);
  opacity: 0;
  transition: opacity 120ms ease-out;
}

.folder-head:hover .folder-rename,
.folder-head:hover .folder-delete,
.folder-head .folder-rename:focus-visible,
.folder-head .folder-delete:focus-visible {
  opacity: 1;
}

.folder-head .folder-delete:hover { color: var(--danger); }

.folder-threads {
  list-style: none;
  margin: 0;
  padding: 0 0 0 0.35rem;
}

#conversation-list .menu-toggle {
  align-self: center;
  padding: 0.35rem 0.5rem;
  color: var(--muted);
  opacity: 0;
  transition: opacity 120ms ease-out;
}

#conversation-list li:hover .menu-toggle,
#conversation-list .menu-toggle:focus-visible,
#conversation-list .menu-toggle[aria-expanded="true"] {
  opacity: 1;
}

#conversation-list .menu {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
  padding: 0.25rem 0.4rem 0.4rem;
}

#conversation-list .menu button {
  text-align: left;
  font-size: 12px;
  padding: 0.3rem 0.5rem;
  color: var(--text);
  border-radius: var(--radius);
}

#conversation-list .menu button:hover { background: var(--surface); }
#conversation-list .menu .delete:hover { color: var(--danger); }

#conversation-list .submenu {
  display: flex;
  flex-direction: column;
  padding-left: 0.75rem;
}

#conversation-list .rename {
  grid-column: 1 / -1;
  margin: 0.3rem;
  padding: 0.35rem 0.5rem;
  font: inherit;
  font-size: 13px;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
}

#conversation-list .snippet {
  display: block;
  margin-top: 0.25rem;
  font-size: 12px;
  line-height: 1.35;
  color: var(--muted);
}

#conversation-list .snippet b { color: var(--text); }

@media (hover: none) {
  #conversation-list .menu-toggle,
  .folder-head .folder-rename,
  .folder-head .folder-delete { opacity: 1; }
}

#readonly-note {
  margin: 0;
  padding: 0.75rem 1rem;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.05em;
  color: var(--muted);
  border-top: 1px solid var(--rule);
}

/* --- thread -------------------------------------------------------------------------- */

#thread {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  min-width: 0;
  min-height: 0;
}

#output {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 1.75rem 1.5rem 2.5rem;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* One reading column, centred; the composer below takes the same measure. */
#output > * {
  max-width: var(--measure);
  width: 100%;
  margin: 0 auto;
}

/* The user's own words are the one voice the page echoes back — in the body sans, but
   marked with a 2px lime gutter so "your" words still read distinct from the answer
   without a second typeface. JS emits a bare <p class="question"> — no extra DOM. */
.question {
  font-family: var(--sans);
  font-size: 16.5px;
  font-weight: 500;
  line-height: 1.45;
  padding: 0.75rem 1rem 0.75rem 0.95rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-left: 3px solid var(--accent);
  border-radius: 0 8px 8px 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Each exchange after the first opens on air, so the turns read as turns. */
#output > .question:not(:first-child) {
  margin-top: 1.75rem;
}

/* Rendered Markdown, so no pre-wrap: the parser already turned blank lines into blocks,
   and preserving them too would double every gap. */
.answer {
  font-size: 15px;
  line-height: 1.7;
  padding: 0 0.25rem;
  overflow-wrap: anywhere;
}

.answer > :first-child { margin-top: 0; }
.answer > :last-child { margin-bottom: 0; }
.answer p, .answer ul, .answer ol, .answer pre, .answer blockquote { margin: 0.6em 0; }
.answer ul, .answer ol { padding-left: 1.4em; }
.answer h1, .answer h2, .answer h3, .answer h4, .answer h5, .answer h6 { font-size: 1em; margin: 0.8em 0 0.3em; }
.answer hr { border: 0; border-top: 1px solid var(--rule); margin: 0.8em 0; }
.answer code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.9em;
  background: var(--raise);
  border: 1px solid var(--rule);
  padding: 0.1em 0.3em;
  border-radius: 3px;
}
.answer pre {
  overflow-x: auto;
  padding: 0.7em 0.9em;
  background: var(--raise);
  border: 1px solid var(--rule);
  border-radius: 8px;
}
.answer pre code { background: none; border: 0; padding: 0; }
.answer blockquote {
  border-left: 2px solid var(--accent);
  padding-left: 0.8em;
  margin-left: 0;
  color: var(--muted);
}
.answer table { border-collapse: collapse; margin: 0.6em 0; }
.answer th, .answer td { border: 1px solid var(--rule); padding: 0.25em 0.6em; }

/* Streaming leaves this empty until the first token lands; a bare gap reads as a stall. */
.answer:empty::after {
  content: "";
  display: inline-block;
  width: 2px;
  height: 1em;
  vertical-align: -0.15em;
  background: var(--accent);
  animation: blink 1s steps(2, start) infinite;
}

@keyframes blink { 50% { opacity: 0; } }

/* What was actually sent to retrieval, when the rewriter changed it. */
.rewrite {
  color: var(--muted);
  margin-top: -0.75rem !important;
}

.error {
  font-size: 13px;
  color: var(--danger);
  border-left: 2px solid var(--danger);
  padding-left: 0.6rem;
}

.banner {
  color: var(--muted);
  border-left: 2px solid var(--accent);
  padding-left: 0.6rem;
}

.empty {
  margin: auto 0 !important;
  color: var(--muted);
}

/* The chat placeholder is the one .empty inside a full-width scroller: keep it vertically
   centred, but on the reading column the answers and the composer share — the shared
   `auto 0` would pin it to the left edge. */
#output > .empty {
  margin: auto !important;
}

.empty strong {
  display: block;
  font-family: var(--sans);
  font-size: 17px;
  font-weight: 600;
  color: var(--text);
  margin-bottom: 0.35rem;
}

/* --- retrieved documents ------------------------------------------------------------- */

/* The answer's footer: what was cited and how sure the model was, on one quiet strip that
   opens into the reading list. */
.sources {
  margin-top: -0.35rem !important;
  padding: 0.3rem 0.75rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 8px;
}

.sources summary {
  color: var(--muted);
  cursor: pointer;
  padding: 0.25rem 0;
  list-style-position: inside;
}

.sources[open] summary {
  border-bottom: 1px solid var(--rule);
  padding-bottom: 0.4rem;
}

.sources .hits {
  border-top: 0;
  margin-bottom: 0.3rem;
}

.sources summary:hover {
  color: var(--text);
}

.agent-steps {
  margin: 0.25rem 0 0.5rem;
  font-size: 0.85em;
  color: var(--muted);
}
.agent-steps summary {
  cursor: pointer;
}
.agent-step {
  display: flex;
  gap: 0.75rem;
  padding: 0.15rem 0 0.15rem 1rem;
}
.agent-step .tool {
  font-family: var(--mono);
  white-space: nowrap;
}
.agent-step .outcome {
  overflow-wrap: anywhere;
}

.sources li.uncited {
  opacity: .55;
}

.confidence {
  display: inline-block;
  font: inherit;
  font-family: var(--mono);
  font-size: 0.75rem;
  padding: 0.1rem 0.55rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: transparent;
  color: var(--muted);
  vertical-align: middle;
}

/* Traffic light, outline only. The badge also spells the level out in its text, so the
   colour is a second channel and not the only one. */
.confidence.high {
  border-color: var(--ok);
  color: var(--ok);
}

.confidence.medium {
  border-color: var(--warn);
  color: var(--warn);
}

.confidence.low {
  border-color: var(--danger);
  color: var(--danger);
}

/* Ruled rows, not boxed cards: a reading list, separated by hairlines. */
.hits {
  list-style: none;
  margin: 0.35rem 0 0;
  padding: 0;
  border-top: 1px solid var(--rule);
}

.hit {
  display: grid;
  grid-template-columns: 2.25rem minmax(0, 1fr);
  gap: 0 0.5rem;
  padding: 0.6rem 0.1rem;
  border-bottom: 1px solid var(--rule);
}

.hit .rank {
  font-family: var(--mono);
  font-size: 12px;
  color: var(--muted);
  padding-top: 1px;
  font-variant-numeric: tabular-nums;
}

.hit .head {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  min-width: 0;
}

.hit .name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-transform: none;
  letter-spacing: 0.02em;
  font-size: 12px;
  color: var(--text);
  padding: 0;
  text-align: left;
}

.hit button.name:hover {
  text-decoration: underline;
  color: var(--accent);
}

.hit .collection {
  font-family: var(--mono);
  font-size: 0.7rem;
  padding: 0.05rem 0.45rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  color: var(--muted);
}

/* Sized against the best hit in this result set, not against 1.0: the score is a reranker
   relevance only when a reranker is configured, and the joiner's fused RRF score otherwise
   — which would draw every bar as an empty sliver. The printed number is the raw value. */
.meter {
  flex: 0 0 56px;
  height: 2px;
  background: var(--raise);
  overflow: hidden;
}

.meter i {
  display: block;
  height: 100%;
  background: var(--accent);
  transition: width 180ms ease-out;
}

.hit .score {
  color: var(--muted);
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}

/* Clamped, the excerpt is a preview and reads best as continuous prose — the chunk's own
   blank lines would otherwise spend the three lines on nothing. Expanded, it is the document
   again and keeps its real shape. */
.excerpt {
  margin: 0.4rem 0 0;
  font-size: 13px;
  line-height: 1.55;
  color: var(--muted);
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  cursor: pointer;
}

.excerpt.open {
  display: block;
  white-space: pre-wrap;
  color: var(--text);
}

/* The hit's own grid puts the rank in column 1; the excerpt belongs under the head. Without
   this the image preview auto-places into the narrow rank column instead — it is appended as
   a plain sibling, so the grid has no other way to know it belongs under the head too. */
.hit .excerpt,
.hit .image-preview {
  grid-column: 2;
}

/* --- citations and popover ----------------------------------------------------------- */

a.cite {
  display: inline;
  font-family: var(--mono);
  font-size: .72em;
  font-weight: 600;
  line-height: 1;
  padding: 0.1em .45em;
  white-space: nowrap;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--rule));
  background: var(--cite-bg);
  text-decoration: none;
  vertical-align: super;
  color: var(--cite-fg);
}

a.cite:hover {
  background: var(--cite-hover);
}

/* A chip attribution placed, not the model: same affordance, dashed so the reader can
   tell the two apart at a glance. */
a.cite.suggested {
  border-style: dashed;
  color: var(--muted);
}

#cite-popover .suggested-note {
  display: block;
  margin-top: 0.3rem;
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--muted);
}

#cite-popover {
  position: fixed;
  max-width: 28rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: 0 4px 20px rgb(0 0 0 / 0.24);
  padding: 0.6rem 0.8rem;
  z-index: 20;
}

#cite-popover p {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin: 0.4rem 0;
  font-size: 13px;
  line-height: 1.55;
  color: var(--muted);
}

#cite-popover strong {
  font-size: 12px;
  color: var(--text);
}

#cite-popover .via,
#cite-popover .score {
  font-size: 11px;
  color: var(--muted);
}

/* --- corpus -------------------------------------------------------------------------- */

/* One page, three views. The rail belongs to the thread, so the corpus and ingest views
   take the whole shell — a list of filenames has nothing to do with a list of threads. */
body:not([data-view="chat"]) #thread,
body:not([data-view="chat"]) #conversations {
  display: none;
}

body:not([data-view="corpus"]) #corpus {
  display: none;
}

body:not([data-view="chat"]) #shell {
  grid-template-columns: minmax(0, 1fr);
}

.corpus-head #corpus-stats {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: none;
}

/* The trail back from a cross-page jump carries a source name, so it is bounded rather
   than letting a long filename push the rest of the head around. */
#corpus-back {
  display: inline-block;
  max-width: 16rem;
  overflow: hidden;
  text-overflow: ellipsis;
  text-transform: none;
  letter-spacing: 0.02em;
}

#corpus-status {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--danger);
}

#corpus-status:empty {
  display: none;
}

/* One bounded list, hairline-ruled rows: a file-type badge, the title over the source name,
   the chunk count in the data voice. */
#source-list {
  list-style: none;
  margin: 0;
  padding: 0;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
}

#source-list > li + li {
  border-top: 1px solid var(--rule);
}

#source-list > li:first-child { border-radius: var(--radius-lg) var(--radius-lg) 0 0; }
#source-list > li:last-child { border-radius: 0 0 var(--radius-lg) var(--radius-lg); }

/* A jump lands the user on a page they did not ask for, so the row it landed on says so for
   a moment. Neutralised by the prefers-reduced-motion block at the foot of this file. */
#source-list > li.jumped {
  animation: jumped 1.5s ease-out;
}

/* color-mix rather than a new palette entry: one tint in one place, and it follows --accent
   through both palettes and the search-mode swap without a second variable to keep in step. */
@keyframes jumped {
  from { background: color-mix(in srgb, var(--accent) 22%, transparent); }
  to { background: transparent; }
}

#source-list .head {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  min-width: 0;
  padding: 0 0.5rem 0 0.9rem;
}

#source-list .title {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas:
    "ext name meta"
    "ext source meta";
  column-gap: 0.75rem;
  row-gap: 0.1rem;
  align-items: center;
  flex: 1;
  min-width: 0;
  text-align: left;
  padding: 0.6rem 0;
}

/* The file type, from the data-ext app.js sets. A source with no extension keeps the
   badge's width so the titles still line up. */
#source-list .title::before {
  content: attr(data-ext) / "";
  grid-area: ext;
  align-self: start;
  min-width: 2.6rem;
  padding: 0.15rem 0.3rem;
  text-align: center;
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.4;
  color: var(--muted);
  background: var(--raise);
  border: 1px solid var(--rule);
  border-radius: 4px;
}

#source-list .title[data-ext=""]::before {
  visibility: hidden;
}

#source-list .name {
  grid-area: name;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--sans);
  font-size: 13.5px;
  font-weight: 500;
  color: var(--text);
}

#source-list .title:hover .name {
  color: var(--accent);
}

#source-list .meta {
  grid-area: meta;
  color: var(--muted);
  white-space: nowrap;
}

/* An attachment row sits under its parent, one indent in, with the corner to say so. */
#source-list > li.attachment .title {
  padding-left: 1.5rem;
  position: relative;
}

#source-list > li.attachment .title::after {
  content: "↳";
  position: absolute;
  left: 0.2rem;
  top: 0.55rem;
  font-size: 12px;
  color: var(--muted);
}

/* The source name under a catalog title that differs from it. */
#source-list .source {
  grid-area: source;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 11px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

#source-list .delete {
  padding: 0.35rem 0.6rem;
  color: var(--muted);
  opacity: 0;
  transition: opacity 120ms ease-out;
}

/* Revealed on hover, but never unreachable by keyboard — and always present where there is
   no hover at all. */
#source-list > li:hover .delete,
#source-list .delete:focus-visible {
  opacity: 1;
}

#source-list .delete:hover {
  color: var(--danger);
}

/* Revealing on hover hides the control completely on a touch screen, where there is no
   hover and a tap does not match :focus-visible either. */
@media (hover: none) {
  #source-list .delete { opacity: 1; }
}

#source-list .head .download,
#source-list .head .open,
.hits .head .download,
.hits .head .open {
  font-family: var(--mono);
  font-size: 11px;
  padding: 0.35rem 0.6rem;
  color: var(--muted);
  white-space: nowrap;
}

#source-list .head .download:hover,
.hits .head .download:hover {
  color: var(--text);
}

#source-list .head .open,
.hits .head .open {
  text-decoration: underline;
}

#source-list .head .open:hover,
.hits .head .open:hover {
  color: var(--accent);
}

/* Deleting a source is irreversible, so the row asks before it does it. */
.confirm {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.1rem;
  border-top: 1px dashed var(--rule);
}

.confirm span {
  margin-right: auto;
  color: var(--danger);
}

.confirm button {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  color: var(--muted);
}

.confirm button.danger {
  border-color: var(--danger);
  color: var(--danger);
}

.confirm button.danger:hover {
  background: var(--danger);
  color: var(--surface);
}

#source-list .preview {
  margin: 0 0.9rem;
  padding: 0.1rem 0.1rem 0.6rem;
  border-top: 1px dashed var(--rule);
}

#source-list > li > .confirm {
  margin: 0 0.9rem;
}

.chunks {
  list-style: none;
  margin: 0;
  padding: 0;
}

.chunks li {
  padding: 0.5rem 0;
}

.chunks li + li {
  border-top: 1px solid var(--rule);
}

.chunks .head {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  color: var(--muted);
}

.chunks .chars {
  font-variant-numeric: tabular-nums;
}

/* Status lines inside the preview: loading, truncation, a failed read. */
.note {
  margin: 0.5rem 0 0;
  font-size: 12px;
  color: var(--muted);
}

.note.error {
  color: var(--danger);
}

#source-list .more {
  margin: 0.5rem 0 0;
  padding: 0;
  font-size: 12px;
  color: var(--accent);
  text-decoration: underline;
}

#source-list .mode {
  display: flex;
  gap: 0.25rem;
  margin: 0.5rem 0 0.25rem;
}

#source-list .mode button {
  padding: 0.1rem 0.5rem;
  font-size: 12px;
  border: 1px solid var(--rule);
  border-radius: 3px;
  color: var(--muted);
}

#source-list .mode button[aria-pressed="true"] {
  color: var(--text);
  border-color: var(--text);
}

#source-list .reading {
  max-width: 70ch;
  font-size: 14px;
  line-height: 1.6;
}

#source-list .reading p {
  margin: 0 0 0.8em;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

#source-list .reading p.cited {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  outline: 2px solid var(--accent);
}

/* A markdown document rendered whole: the paragraph rule above keeps newlines for plain
   text, which would double-space markup; a document keeps its heading hierarchy, unlike
   an answer, which flattens it. The rest mirrors the .answer rules. */
#source-list .reading.markdown { overflow-wrap: anywhere; }
#source-list .reading.markdown p { white-space: normal; }
#source-list .reading.markdown > :first-child { margin-top: 0; }
#source-list .reading.markdown ul, #source-list .reading.markdown ol, #source-list .reading.markdown pre,
#source-list .reading.markdown blockquote, #source-list .reading.markdown table { margin: 0.6em 0 0.8em; }
#source-list .reading.markdown ul, #source-list .reading.markdown ol { padding-left: 1.4em; }
#source-list .reading.markdown h1, #source-list .reading.markdown h2, #source-list .reading.markdown h3,
#source-list .reading.markdown h4, #source-list .reading.markdown h5, #source-list .reading.markdown h6 {
  margin: 1.2em 0 0.4em;
  line-height: 1.3;
}
#source-list .reading.markdown h1 { font-size: 1.4em; }
#source-list .reading.markdown h2 { font-size: 1.25em; }
#source-list .reading.markdown h3 { font-size: 1.1em; }
#source-list .reading.markdown h4, #source-list .reading.markdown h5, #source-list .reading.markdown h6 { font-size: 1em; }
#source-list .reading.markdown hr { border: 0; border-top: 1px solid var(--rule); margin: 0.8em 0; }
#source-list .reading.markdown code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.9em;
  background: var(--raise);
  border: 1px solid var(--rule);
  padding: 0.1em 0.3em;
  border-radius: 3px;
}
#source-list .reading.markdown pre {
  overflow-x: auto;
  padding: 0.7em 0.9em;
  background: var(--raise);
  border: 1px solid var(--rule);
  border-radius: 8px;
}
#source-list .reading.markdown pre code { background: none; border: 0; padding: 0; }
#source-list .reading.markdown blockquote {
  border-left: 2px solid var(--accent);
  padding-left: 0.8em;
  margin-left: 0;
  color: var(--muted);
}
#source-list .reading.markdown table { border-collapse: collapse; }
#source-list .reading.markdown th, #source-list .reading.markdown td { border: 1px solid var(--rule); padding: 0.25em 0.6em; }

/* The catalog half of a preview: summary, then attachments / links / linked-from. */
#source-list .detail {
  padding: 0.4rem 0 0.2rem;
}

#source-list .detail .summary {
  margin: 0 0 0.4rem;
  font-size: 13px;
}

#source-list .detail .label {
  margin: 0.5rem 0 0.15rem;
}

.edges {
  list-style: none;
  margin: 0;
  padding: 0;
}

.edges li {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  font-size: 12.5px;
}

/* A resolved target is a row on this page, so it looks like the link it is. */
.edges .jump {
  padding: 0;
  color: var(--accent);
  text-decoration: underline;
  text-align: left;
}

#corpus-pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
}

#corpus-range {
  color: var(--muted);
}

/* --- composer ------------------------------------------------------------------------ */

/* A floating card inset from the viewport edges; the signal line sits inside it, above the
   input. Squared corners, one hairline, a deeper shadow in dark. */
#chat-form {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  width: min(100% - 2rem, var(--measure));
  margin: 0 auto 1rem;
  padding: 0.6rem 0.9rem 0.8rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 8px;
  box-shadow: 0 4px 20px rgb(0 0 0 / 0.24);
}

/* The request pipeline as a 2px signal line with tick nodes. GENERATE is the only stage
   that can leave it; lime while it is in the path, the whole page — ticks, links, send
   button — goes cyan and the connector breaks into dashes when it isn't. All follow
   --accent, so the swap needs no per-stage rules of its own. */
#path {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

#path .node {
  color: var(--muted);
  padding: 0.2rem 0;
  transition: color 120ms ease-out;
}

#path .node:not(#generate-toggle) {
  color: var(--accent);
}

/* A tick on the line marks each stage that is in the path. */
#path .node:not(#generate-toggle)::before {
  content: "";
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-right: 0.35rem;
  vertical-align: 1px;
  border-radius: 50%;
  background: var(--accent);
}

#generate-toggle::before {
  content: "";
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-right: 0.35rem;
  vertical-align: 1px;
  border-radius: 50%;
  background: currentColor;
  transition: background 120ms ease-out;
}

#path .link {
  width: 1.5rem;
  height: 2px;
  background: var(--accent);
}

#generate-toggle.node {
  color: var(--accent);
}

#generate-toggle[aria-checked="false"] {
  color: var(--muted);
  opacity: 0.65;
}

#generate-toggle[aria-checked="false"]::before {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--muted);
}

/* The rewrite stage is never out of the path; the switch is only about confirming it, so
   it keeps the fixed-stage look and takes the button reset the generate switch has. */
#rewrite-toggle.node {
  color: var(--accent);
}

/* The proposal card: the rewrite, shown for approval where the answer will appear. */
.proposal {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  border-left: 2px solid var(--accent);
  padding-left: 0.6rem;
}

.proposal label {
  color: var(--muted);
}

.proposal input {
  flex: 1 1 16rem;
}

/* The connector into a stage that is no longer in the path. */
#path-link.broken {
  background: repeating-linear-gradient(
    to right, var(--muted) 0 2px, transparent 2px 5px
  );
  opacity: 0.55;
}

.compose {
  display: flex;
  gap: var(--step);
}

#question {
  flex: 1;
  min-width: 0;
  font: inherit;
  color: var(--text);
  background: var(--ground);
  padding: 0.55rem 0.75rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}

/* No `outline: none` here: an id + :focus outranks the page's :focus-visible rule, so
   suppressing it left this one control with a weaker focus indicator than every other. */
#question:focus {
  border-color: var(--accent);
}

#send {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  padding: 0.6rem 1.25rem;
  border-radius: var(--radius);
  /* --accent, not a fixed lime: the fill goes cyan with the page when GENERATE is switched
     out of the path, so the button's colour says which endpoint it will hit. */
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
}

#send:disabled {
  opacity: 0.5;
  cursor: default;
}

/* --- narrow viewports ---------------------------------------------------------------- */

@media (max-width: 820px) {
  #threads-toggle { display: block; }

  /* The toggle is meaningless in the corpus view — the rail it opens is hidden there. */
  body:not([data-view="chat"]) #threads-toggle { display: none; }

  #shell { grid-template-columns: minmax(0, 1fr); }

  /* Absolute inside #shell rather than fixed below a hardcoded 48px: the masthead wraps to
     two rows further down, and a magic number here would have left a gap or an overlap. */
  #conversations {
    position: absolute;
    z-index: 2;
    inset: 0 auto 0 0;
    width: min(280px, 82vw);
    transform: translateX(-102%);
    /* visibility, not transform alone: a rail moved off-canvas is still in the tab order and
       still in the accessibility tree, so the closed drawer's buttons could be focused and
       read out. Delayed so it does not cut the slide short on the way out. */
    visibility: hidden;
    transition: transform 160ms ease-out, visibility 0s linear 160ms;
  }

  /* The scrim belongs to the open state. Spread over 100vmax it reaches the whole viewport
     from wherever the rail is, so leaving it on the base rule dimmed the entire page behind
     a drawer that was shut. */
  #conversations.open {
    transform: none;
    visibility: visible;
    box-shadow: 0 0 0 100vmax rgb(0 0 0 / 0.45);
    transition: transform 160ms ease-out;
  }

  #output { padding: 1rem 1rem 1.5rem; }
  #chat-form { width: calc(100% - 1.5rem); margin: 0 auto 0.75rem; padding: 0.5rem 0.75rem 0.7rem; }
  #corpus, #ingest, #users, #connectors { padding: 1rem 1rem 1.5rem; }
  .ingest-grid { grid-template-columns: minmax(0, 1fr); }
  .form-grid { grid-template-columns: minmax(0, 1fr); }
  .manage-row { grid-template-columns: minmax(0, 1fr); }

  /* The table scrolls sideways here, which clips anything hanging off a cell, so the row
     menu and the delete question sit inside the cell instead of floating over the page. */
  .table-wrap { overflow-x: auto; }
  .actions .menu-anchor {
    flex-wrap: wrap;
    justify-content: flex-end;
  }
  .actions .menu,
  .actions .confirm {
    position: static;
    display: flex;
    flex-basis: 100%;
    margin-top: 0.4rem;
    box-shadow: none;
  }
  .actions .confirm { justify-content: flex-end; }
}

/* Below this the wordmark and the nav no longer share one line. */
@media (max-width: 620px) {
  #masthead {
    height: auto;
    min-height: 48px;
    flex-wrap: wrap;
    padding: 0.5rem 0.75rem;
    row-gap: 0.5rem;
  }

  #masthead h1 { white-space: nowrap; }

  #session span { display: none; }

  /* A corpus row on a phone: badge and title on the first line, path under, count under. */
  #source-list .title {
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      "ext name"
      "ext source"
      "ext meta";
  }

  .view-title h2 { font-size: 19px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

#collection-new,
#collection-form button {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  padding: 0.3rem 0.6rem;
  color: var(--muted);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}

#collection-new:hover,
#collection-form button:hover {
  color: var(--text);
}

#collection-form {
  display: flex;
  align-items: center;
  gap: var(--step);
  flex-wrap: wrap;
}

#collection-form input {
  font-family: var(--mono);
  font-size: 11px;
  padding: 0.3rem 0.4rem;
  width: 14ch;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}

#collection-status {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The scope row, merged under the masthead as a second hairline. Choosing the collection
   is the first decision on the page, so it keeps its own row rather than a masthead widget. */
#collection-bar {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--rule);
  background: var(--surface);
}
#collections,
#also-search {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
}
#collection-bar .label {
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.7;
  min-width: 7ch;
}
#collection {
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: 0.85rem;
  padding: 0.35rem 0.6rem;
  min-width: 16rem;
}

/* --accent, not a fixed lime: otherwise this ring stays lime in search mode while the
   page's own :focus-visible ring next to it goes cyan. */
#collection:focus {
  border-color: var(--accent);
}

#also-search-chips {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
}
.chip {
  font: inherit;
  font-family: var(--mono);
  font-size: 0.75rem;
  padding: 0.2rem 0.7rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

/* A dot marks the chip that is on, instead of a fill. */
.chip::before {
  content: "";
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-right: 0.35rem;
  vertical-align: 1px;
  border-radius: 50%;
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--rule);
}

.chip[aria-checked="true"] {
  border-color: var(--accent);
  color: var(--text);
}

.chip[aria-checked="true"]::before {
  background: var(--accent);
  box-shadow: none;
}

/* Available to a screen reader, off screen for everyone else. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The manage-collection forms keep their disabled look while no collection is selected; the
   layout and the buttons are .manage-row and .btn in the corpus section. */
#collection-empty-form input:disabled,
#collection-delete-form input:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

body[data-no-collection] #corpus-empty,
body[data-no-collection] #ingest-empty {
  display: block;
}

body[data-no-collection] #source-list,
body[data-no-collection] #ingest-form {
  display: none;
}

/* --- auth screen ----------------------------------------------------------------------- */

/* The screen replaces the shell rather than covering it: while there is no session there is
   nothing behind it that should be reachable, and hiding the shell means no request is
   made on behalf of nobody. */
body[data-auth] #masthead,
body[data-auth] #collection-bar,
body[data-auth] #shell {
  display: none;
}

body:not([data-auth]) #auth-screen {
  display: none;
}

#auth-screen {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: calc(var(--step) * 2);
  min-height: 100vh;
  padding: var(--step);
}

#auth-screen h1 {
  font-family: var(--serif);
  font-style: italic;
  font-weight: 500;
  font-size: 26px;
  letter-spacing: 0;
  text-transform: none;
  margin: 0;
}

#auth-form {
  display: flex;
  flex-direction: column;
  gap: var(--step);
  width: min(100%, 22rem);
  padding: calc(var(--step) * 2);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 12px;
  box-shadow: 0 4px 20px rgb(0 0 0 / 0.3);
}

#auth-form .field {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

#auth-form .field span {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
}

#auth-form input {
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--ground);
  color: var(--text);
}

#auth-form input:focus {
  border-color: var(--accent);
}

.auth-actions {
  display: flex;
  gap: var(--step);
}

#auth-submit {
  flex: 1;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  font-weight: 600;
  padding: 0.55rem 0.6rem;
  border-radius: var(--radius);
  /* --accent, not a fixed lime: the submit stays consistent with whatever accent the page
     is in, and the search-mode cyan read the same on the auth card. */
  background: var(--accent);
  color: var(--on-accent);
}

#auth-cancel {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  padding: 0.55rem 0.6rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  color: var(--muted);
}

#auth-status {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--danger);
}

#auth-status:empty {
  display: none;
}

/* Which fields a mode shows. The password field is common to all three. */
#auth-screen:not([data-mode="setup"]) .auth-setup:not(.auth-login):not(.auth-password),
#auth-screen[data-mode="password"] .auth-login:not(.auth-password),
#auth-screen:not([data-mode="password"]) .auth-password:not(.auth-setup),
#auth-screen[data-mode="login"] .auth-setup.auth-password {
  display: none;
}

/* --- session ---------------------------------------------------------------------------- */

#session {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
}

#session-user {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--text);
}

/* A live session is signalled by the lime dot, matching the "signal is on" language of the
   preset chips and the pipeline ticks. */
#session-user::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent);
  flex: none;
}

#session button {
  padding: 0.35rem 0.5rem;
  border-radius: var(--radius);
  color: var(--muted);
  transition: color 120ms ease-out, background 120ms ease-out;
}

#session button:hover {
  color: var(--text);
  background: var(--raise);
}

/* --- per-level gating --------------------------------------------------------------------- */

/* A reader's ingest view: the form is not disabled, it is not there. A disabled drop zone
   would invite a drop that goes nowhere. */
body[data-level="read"] #ingest-form {
  display: none;
}

body[data-level="read"] #ingest-readonly {
  display: block;
}

/* --- access panel ---------------------------------------------------------------------------- */

#access[hidden] {
  display: none;
}

#grant-list li > span:first-child {
  font-weight: 500;
  color: var(--text);
}

#grant-list .level {
  display: inline-flex;
  align-items: center;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.02em;
  line-height: 1.5;
  padding: 0.05rem 0.5rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--raise);
  color: var(--text);
}

#grant-list li > button {
  margin-left: auto;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  padding: 0.25rem 0.5rem;
  border-radius: var(--radius);
  color: var(--muted);
}

#grant-list li > button:hover {
  background: var(--raise);
  color: var(--danger);
}

#grant-form input {
  flex: 1 1 10rem;
}

/* --- manage collection ------------------------------------------------------------------- */

/* Folded shut until asked for; applyLevel() in app.js hides it below admin, alongside the
   three forms inside it. */
#collection-manage summary {
  list-style: none;
  cursor: pointer;
  user-select: none;
}

#collection-manage summary::-webkit-details-marker {
  display: none;
}

#collection-manage summary .label::before {
  content: "▸";
  display: inline-block;
  width: 1rem;
}

#collection-manage[open] summary .label::before {
  content: "▾";
}

#collection-manage:not([open]) summary {
  border-bottom: 0;
}

.manage-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 15rem auto;
  gap: 0.75rem;
  align-items: center;
  padding: 0.7rem 0.9rem;
}

.manage-row + .manage-row {
  border-top: 1px solid var(--rule);
}

.manage-what {
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted);
}

/* --- users ------------------------------------------------------------------------------ */

body:not([data-view="users"]) #users {
  display: none;
}

#user-create {
  display: flex;
  gap: 0.5rem;
}

#user-create input {
  width: 13rem;
  font-family: var(--mono);
  font-size: 12.5px;
}

/* The one-time password: shown once, in the accent's frame, with Copy as the panel's job. */
#temp-password {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem 0.9rem;
  padding: 0.7rem 0.9rem;
  border-color: color-mix(in srgb, var(--accent) 45%, var(--rule));
}

#temp-password[hidden] {
  display: none;
}

#temp-password .label {
  margin: 0;
  color: var(--muted);
}

#temp-password .label strong {
  color: var(--text);
}

#temp-password code {
  font-family: var(--mono);
  font-size: 13.5px;
  padding: 0.25rem 0.6rem;
  background: var(--ground);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  user-select: all;
}

#temp-password .btn:first-of-type {
  margin-left: auto;
}

/* The monogram before a username, from the data-initials app.js sets. */
#user-table td.who::before {
  content: attr(data-initials) / "";
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  margin-right: 0.6rem;
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  background: var(--raise);
  border: 1px solid var(--rule);
  border-radius: 50%;
  vertical-align: middle;
}

#user-table tr.disabled td.who::before {
  opacity: 0.5;
}

#user-table td:nth-child(2) .pill {
  margin: 0.1rem 0.3rem 0.1rem 0;
}

#user-table td:nth-child(3) {
  font-family: var(--mono);
  font-size: 12px;
  color: var(--muted);
  white-space: nowrap;
}

/* --- connectors ------------------------------------------------------------------------- */

body:not([data-view="connectors"]) #connectors {
  display: none;
}

/* One card per deployed connector type. The mark is the type's initial in the wordmark's
   face: the only other place Fraunces appears, and the one thing here that is ours alone. */
#connector-health {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 0.6rem;
  margin: 0;
  padding: 0;
}

#connector-health li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas:
    "glyph type version"
    "glyph state state";
  column-gap: 0.75rem;
  row-gap: 0.25rem;
  align-items: center;
  padding: 0.65rem 0.8rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
}

#connector-health li::before {
  content: attr(data-glyph) / "";
  grid-area: glyph;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  font-family: var(--serif);
  font-style: italic;
  font-weight: 500;
  font-size: 20px;
  line-height: 1;
  color: var(--accent);
  background: var(--raise);
  border: 1px solid var(--rule);
  border-radius: 8px;
}

#connector-health li.unhealthy::before {
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 45%, var(--rule));
}

#connector-health .type {
  grid-area: type;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#connector-health .version {
  grid-area: version;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted);
}

#connector-health .pill {
  grid-area: state;
  justify-self: start;
}

/* Last run: the status pill, then the counts or the error in the data voice. */
.run {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
}

.run-text {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--muted);
}

.run-text:empty {
  display: none;
}

#source-table td:nth-child(3),
#source-table td:nth-child(5),
#source-table td:nth-child(6) {
  font-family: var(--mono);
  font-size: 12px;
  color: var(--muted);
  white-space: nowrap;
}

.runs-list {
  list-style: none;
  margin: 0.5rem 0 0 0;
  padding: 0.4rem 0 0 0;
  border-top: 1px dashed var(--rule);
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted);
}

.runs-list li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.15rem 0;
}

.runs-list .pill {
  font-size: 10px;
  padding-block: 0;
}

/* While a row's runs list is open the LAST RUN cell is tall; the rest of the row stays
   at the top so the source name, schedule and actions line up with the latest run. */
#source-table tr:has(.runs-list:not([hidden])) td {
  vertical-align: top;
}

/* The add/edit form: a panel with the fields in two columns, long ones spanning both. */
#source-form[hidden] {
  display: none;
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.9rem 1.25rem;
  padding: 1rem 0.9rem;
}

/* The connector-specific fields are appended inside #source-fields; contents lets them
   take grid cells of their own. */
#source-fields {
  display: contents;
}

.form-grid .field:has(textarea) {
  grid-column: 1 / -1;
}

.source-actions {
  justify-content: flex-end;
}

/* Image sources: a thumbnail of the original (web/app.js imagePreview). */
.image-preview {
  display: block;
  max-width: min(100%, 360px);
  max-height: 240px;
  margin: 0.4rem 0;
  border: 1px solid var(--rule);
  border-radius: 4px;
  cursor: zoom-in;
  background: #fff; /* transparent PNGs and SVGs are drawn for a white page */
}
.image-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #000c;
  cursor: zoom-out;
}
.image-overlay img {
  max-width: 95vw;
  max-height: 95vh;
  background: #fff;
}
