/* Task 36: every color in the file lives here (or in the dark override
   right below) as a token — components below only ever reference var(...),
   never a literal. `color-scheme: light dark` alone (already declared
   before this task) wasn't enough to stop Android/Chrome's forced-dark
   heuristic: that heuristic backs off once it detects a page with *real*
   `prefers-color-scheme: dark` styling, not just the meta declaration —
   without it, the browser was auto-inverting individual elements (the
   real bug: "+ Neuer Chat" and the sidebar toggle went white-on-white,
   both `background: #fff` with no matching dark counterpart). Native form
   controls (bare `<input>`/`<textarea>`/`<select>` with no explicit
   background) and `<dialog>` itself are deliberately left alone — with
   `color-scheme` declared, the browser already themes those correctly via
   the `canvas`/`CanvasText` system colors, and overriding them here would
   just be redoing what the platform already does right. */
:root {
  color-scheme: light dark;
  --bg: #fafafa;
  --text: #1a1a1a;
  --surface: #fff;
  --surface-hover: #e4e4ea;
  --surface-hover-strong: #d5d5dd;
  --bg-subtle: #f2f2f5;
  --accent-bg: #eef2ff;
  --border: #d0d0d5;
  --bubble-user: #2563eb;
  --bubble-user-text: #fff;
  --bubble-assistant: #f0f0f3;
  --bubble-assistant-text: #1a1a1a;
  --bubble-error: #fde2e2;
  --bubble-error-text: #7a1f1f;
  --bubble-success: #e3f5e6;
  --bubble-success-text: #1f6b30;
  --muted: #6b6b76;
  /* Task 25: deliberately more saturated than --bubble-error above — that's
     a per-message error bubble among many other messages, this is a
     system-level operational alarm that must not blend in with anything
     else on the page. */
  --notice-error-bg: #dc2626;
  --notice-error-text: #fff;
  --notice-warning-bg: #fef3c7;
  --notice-warning-text: #7a4a00;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #16171b;
    --text: #eaeaef;
    --surface: #212227;
    --surface-hover: #2b2d33;
    --surface-hover-strong: #35373f;
    --bg-subtle: #1b1c21;
    --accent-bg: #1f2a44;
    --border: #3a3b42;
    --bubble-user: #3b82f6;
    --bubble-user-text: #fff;
    --bubble-assistant: #26272d;
    --bubble-assistant-text: #eaeaef;
    --bubble-error: #3a1616;
    --bubble-error-text: #ff9c96;
    --bubble-success: #16341f;
    --bubble-success-text: #7be0a0;
    --muted: #9a9ba6;
    /* Same "louder than the per-message bubble" contrast the light note
       above describes — kept vivid rather than muted-down for dark, an
       operational alarm has to read as one in either scheme. */
    --notice-error-bg: #dc2626;
    --notice-error-text: #fff;
    --notice-warning-bg: #4a3900;
    --notice-warning-text: #ffd166;
  }
}

* {
  box-sizing: border-box;
}

/* Task 34a: only #messages below ever scrolls (overflow-y:auto there) — html
   and body must never grow their own scrollbar, otherwise a mobile browser's
   dynamic toolbar (real viewport shorter than the `100vh` #app was laid out
   against) lets the whole page rubber-band, dragging the header off-screen
   with it. */
html,
body {
  height: 100%;
  overflow: hidden;
}

body {
  margin: 0;
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  background: var(--bg);
  color: var(--text);
}

#app {
  height: 100vh;
  height: 100dvh; /* real visible height on mobile, where 100vh overshoots behind the address bar */
  display: flex;
  flex-direction: row;
}

/* Sidebar (task 11): hidden unless #app has "sidebar-open" — app.js sets the
   initial state based on viewport width and the toggle button flips it. */
#sidebar {
  display: none;
  width: 260px;
  flex-shrink: 0;
  flex-direction: column;
  border-right: 1px solid var(--border);
  background: var(--bg-subtle);
  overflow-y: auto;
}

#app.sidebar-open #sidebar {
  display: flex;
}

@media (max-width: 700px) {
  #app.sidebar-open #sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    width: 80vw;
    max-width: 300px;
    z-index: 20;
    box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2);
  }

  /* Task 34b's own mobile #sidebar-toggle left-offset override lives further
     down, right after the shared desktop rule (same selector, same
     specificity — source order is what decides the tie, and it must come
     after that rule to actually win here; see the note there). Supersedes
     task 33's old position:relative bump (that was only needed because the
     toggle used to sit inline in the header, which the fixed sidebar could
     cover); #sidebar-toggle is position:fixed with its own z-index
     everywhere now, so no mobile-only stacking fix is needed here anymore. */

  /* Backdrop behind the sidebar, above the (now-hidden-under-it) chat —
     tap-to-close is the mobile-standard escape hatch alongside the toggle
     button itself. Doesn't exist/isn't clickable at all outside this state:
     display:none by default below, and the sidebar itself is never fixed
     above 700px, so there's nothing to dim there. */
  #app.sidebar-open #sidebar-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.4);
    z-index: 15;
  }
}

#sidebar-backdrop {
  display: none;
}

#sidebar-header {
  padding: 0.75rem;
  border-bottom: 1px solid var(--border);
}

#new-chat-button {
  width: 100%;
  font: inherit;
  padding: 0.5rem;
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  background: var(--surface);
  cursor: pointer;
}

#session-list {
  list-style: none;
  margin: 0;
  padding: 0.4rem;
  overflow-y: auto;
  flex: 1;
}

.session-item {
  display: flex;
  align-items: center;
  border-radius: 0.4rem;
}

.session-item:hover,
.session-item--active {
  background: var(--surface-hover);
}

.session-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
  font: inherit;
  font-size: 0.85rem;
  padding: 0.45rem 0.3rem;
  border: none;
  background: none;
  cursor: pointer;
  color: inherit;
}

.session-item--active .session-title {
  font-weight: 600;
}

.session-action {
  flex-shrink: 0;
  border: none;
  background: none;
  cursor: pointer;
  font-size: 0.85rem;
  padding: 0.3rem;
  border-radius: 0.4rem;
  color: var(--muted);
}

.session-action:hover {
  background: var(--surface-hover-strong);
}

/* Task 18: max-width caps the chat column's own rendered width, and — as a
   flex item with flex:1 and margin:0 auto — any space #main-panel is
   allocated beyond that cap becomes auto-margin on either side rather than
   extra content width. That's what already makes the gutters (not the chat
   text) the first thing to shrink when the task-15 preview panel opens and
   eats into the row's available space — no separate mechanism needed for
   that "proportional split", just a sane cap. 720px was too conservative on
   a real wide screen (~1920px): real multi-paragraph answers left large
   empty margins on both sides even with the panel closed, direct user
   feedback. Raised into the ~900-1100px sweet spot the task calls out —
   bullet-heavy prose (the common real case here) reads fine at this width,
   unbounded/full-bleed would hurt readability instead.
*/
#main-panel {
  flex: 1;
  min-width: 0;
  max-width: 1000px;
  width: 100%;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
}

/* Task 15: source previews (video player, PDF page) used to stack inline
   below the message; they now render here instead, next to the chat rather
   than under it. Hidden unless #app has "preview-open" (set by app.js when a
   source chip is clicked), same on/off pattern as #sidebar above. */
#preview-panel {
  display: none;
  width: 380px;
  flex-shrink: 0;
  flex-direction: column;
  border-left: 1px solid var(--border);
  background: var(--surface);
  overflow-y: auto;
}

#app.preview-open #preview-panel {
  display: flex;
}

/* Below this width there isn't room for chat + preview side by side without
   squeezing the chat unusably narrow — the panel becomes a full-screen
   overlay instead of a column, closed the same way (the panel's own close
   button), mirroring how #sidebar degrades on narrow viewports. */
@media (max-width: 900px) {
  #app.preview-open #preview-panel {
    position: fixed;
    inset: 0;
    width: 100%;
    max-width: 100%;
    z-index: 25;
  }
}

/* Task 20: draggable divider, side-by-side layout only (>900px — below that
   task 15's full-screen overlay applies and a column split doesn't exist).
   Hidden whenever the panel itself is hidden, same on/off pattern as the
   panel. app.js sets each side's actual share via inline flex-grow
   (applySplit()) — the flex-grow:1/1 here is only the fallback for "no
   drag/persisted split yet", giving the task's own "~50/50 initial" for
   free through the cascade rather than needing JS to compute it.
   min-width on both sides is what actually enforces "can't drag into
   uselessness" — flexbox clamps to it automatically regardless of what
   ratio JS asks for, so the drag handler itself doesn't need to duplicate
   that math. */
#panel-divider {
  display: none;
  width: 6px;
  flex-shrink: 0;
  cursor: col-resize;
  position: relative;
  touch-action: none;
}

#panel-divider::after {
  content: "";
  position: absolute;
  inset: 0 2px;
  background: var(--border);
  border-radius: 2px;
}

#panel-divider:hover::after,
#panel-divider.dragging::after {
  background: var(--bubble-user);
}

@media (min-width: 901px) {
  #app.preview-open #panel-divider {
    display: block;
  }

  #app.preview-open #main-panel {
    max-width: none;
    margin: 0;
    min-width: 420px;
    flex-grow: 1;
  }

  #app.preview-open #preview-panel {
    width: auto;
    min-width: 320px;
    flex: 1 1 0;
  }
}

/* Dragging can otherwise select chat text / highlight the panel behind the
   cursor as it crosses over — same annoyance a resizable split always has
   without this. */
body.resizing-panels {
  user-select: none;
}

#preview-panel-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.6rem;
  padding: 0.9rem 1rem;
  border-bottom: 1px solid var(--border);
}

#preview-panel-title {
  margin: 0;
  font-size: 0.85rem;
  font-weight: 600;
  word-break: break-word;
}

#preview-panel-close {
  flex-shrink: 0;
  font: inherit;
  border: 1px solid var(--border);
  background: var(--surface);
  border-radius: 0.4rem;
  padding: 0.2rem 0.5rem;
  cursor: pointer;
  color: var(--muted);
}

#preview-panel-content {
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

#preview-panel-content img,
#preview-panel-content video,
#preview-panel-content audio {
  max-width: 100%;
  max-height: 70vh;
  border-radius: 0.4rem;
}

/* Task 19: playback speed, video and audio alike — sits right under the
   player, above the "Original öffnen" link. */
.preview-speed-control {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.8rem;
  color: var(--muted);
}

.preview-speed-control select {
  font: inherit;
  padding: 0.2rem 0.4rem;
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  background: var(--surface);
}

/* Tasks 42/43: client document viewers (pdf.js / epub.js / text) share this
   container, appended synchronously so their async-rendered content lands
   between the source path and the "Original öffnen" link. min-height:0 lets a
   flex child actually shrink so the inner scroll boxes below can bound
   themselves instead of stretching the whole panel. */
.viewer-container {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  min-height: 0;
}

.viewer-loading {
  font-size: 0.85rem;
  color: var(--muted);
  margin: 0;
}

.viewer-error {
  font-size: 0.85rem;
  color: var(--bubble-error-text);
  margin: 0;
}

/* PDF (task 43, pdf.js): a toolbar (zoom + page indicator + jump-to-cited)
   over a self-scrolling column of lazily-rendered page canvases. */
.pdf-toolbar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.8rem;
  color: var(--muted);
  flex-wrap: wrap;
}

.pdf-toolbar-button {
  font: inherit;
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  background: var(--surface);
  padding: 0.15rem 0.6rem;
  cursor: pointer;
  line-height: 1.2;
}

.pdf-zoom-label {
  min-width: 3rem;
  text-align: center;
}

.pdf-page-info {
  margin-left: 0.3rem;
}

.pdf-scroll {
  max-height: 78vh;
  overflow: auto;
  background: var(--bg-subtle);
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  padding: 0.5rem;
}

/* Explicit pixel width set by JS (fit-to-width × zoom). margin:0 auto centers
   the page when it's narrower than the scroll box and collapses to 0 when
   it's wider than the box (zoomed in), so horizontal panning can reach both
   edges — a flex/align-items:center parent would strand the left edge. */
.pdf-page {
  margin: 0 auto 0.5rem;
  background: #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
}

.pdf-page:last-child {
  margin-bottom: 0;
}

.pdf-page-slot {
  width: 100%;
}

.pdf-page-canvas {
  display: block;
  width: 100%;
  height: auto;
}

.pdf-page-back {
  font: inherit;
  font-size: 0.75rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  padding: 0.2rem 0.6rem;
  cursor: pointer;
  color: var(--muted);
  margin-left: auto;
}

/* EPUB (task 42, epub.js): prev/next over a fixed-height paginated frame.
   The book renders as an iframe with its own document styling — kept on a
   white surface (theme-invariant, like the task-36 lightbox) so a book that
   assumes a light page stays legible in dark mode. */
.epub-nav {
  display: flex;
  gap: 0.6rem;
}

.epub-nav-button {
  font: inherit;
  font-size: 0.8rem;
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  background: var(--surface);
  padding: 0.25rem 0.7rem;
  cursor: pointer;
}

.epub-viewer {
  height: 70vh;
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  background: #fff;
  overflow: hidden;
}

/* text (task 42): .md through markdown.js, .txt as pre — both self-scroll so
   a long file doesn't stretch the panel. */
.text-viewer {
  max-height: 78vh;
  overflow: auto;
}

.text-viewer-plain {
  white-space: pre-wrap;
  word-break: break-word;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.85rem;
  line-height: 1.5;
  margin: 0;
  padding: 0.75rem;
  background: var(--bg-subtle);
  border: 1px solid var(--border);
  border-radius: 0.4rem;
}

/* Task 34b left #sidebar-toggle's old flex slot behind when it went
   position:fixed — without a reserve, its own left-edge dock (see its rule
   below) sits directly under this header's first line of text on any
   viewport narrow enough that #main-panel isn't centered with its own
   gutter (task 18's auto-margin only appears once there's spare width
   beyond max-width:1000px). padding-left restores that slot statically
   instead, roughly matching the toggle's own rendered width + the header's
   old inter-item gap, so h1 starts exactly where it always visually did. */
header {
  padding: 1rem 1rem 1rem 3.4rem;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

header h1 {
  font-size: 1.1rem;
  margin: 0;
  flex: 1;
}

/* Task 34c: active session name, next to the settings button. Bounded and
   ellipsized rather than flex:1 like h1 above — a long custom title (task
   11's rename has no length cap) must never be what pushes the settings
   button out of the header. */
#active-session-name {
  flex-shrink: 1;
  min-width: 0;
  max-width: 40vw;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.8rem;
  color: var(--muted);
}

@media (max-width: 700px) {
  #active-session-name {
    max-width: 24vw;
    font-size: 0.72rem;
  }
}

/* Header action buttons (⚙️ settings, and whatever icon buttons a mounted
   skill surface adds). Was `#settings-open` + an identical `#breeds-open`
   rule; a shared class is what lets a skill built in JS look like a native
   part of the header without the core stylesheet knowing that skill's id. */
.header-action {
  flex-shrink: 0;
  font-size: 1.1rem;
  border: 1px solid var(--border);
  background: var(--surface);
  border-radius: 0.4rem;
  padding: 0.3rem 0.6rem;
  cursor: pointer;
}

/* Optional theme wordmark in front of the header title (theming.js's
   `brand.logoUrl`) — capped to the title's own line height so a theme
   can't change the header's geometry by shipping a large image. */
.app-logo {
  height: 1.4em;
  width: auto;
  vertical-align: -0.25em;
  margin-right: 0.4rem;
}

/* Task 25: operational banner — top of the chat area, above #messages, so
   it's visible without scrolling. Empty when api sends no notices (or an
   older api sends no system_notices field at all): no element rendered
   inside, so #system-notices itself just collapses to zero height, no
   `display` toggling needed on the container (same reasoning as task 23's
   #logging-notice — nothing to fight [hidden] here since it's never given
   the attribute at all, just left empty). */
#system-notices {
  display: flex;
  flex-direction: column;
}

.system-notice {
  padding: 0.6rem 1rem;
  font-size: 0.85rem;
  text-align: center;
}

/* Error: "unübersehbar" — deliberately loud, no dismiss control. */
.system-notice--error {
  background: var(--notice-error-bg);
  color: var(--notice-error-text);
  font-weight: 600;
}

/* Warning: visible but genuinely less alarming than error — smaller
   presence, no bold, amber rather than red. */
.system-notice--warning {
  background: var(--notice-warning-bg);
  color: var(--notice-warning-text);
}

/* Task 34b: floats independently of the header's own flow instead of
   sitting inline in it, so it can dock at the sidebar's actual edge (open)
   or the window's edge (closed) — positions that don't correspond to any
   fixed point *within* the header once #main-panel is off-center (task 18's
   centered/capped column on wide screens). `top` matches header's own
   padding-top so it still lines up with the h1/settings row visually. */
#sidebar-toggle {
  position: fixed;
  top: 1rem;
  left: 0.6rem;
  z-index: 21;
  font: inherit;
  border: 1px solid var(--border);
  background: var(--surface);
  border-radius: 0.4rem;
  padding: 0.3rem 0.6rem;
  cursor: pointer;
  transition: left 0.2s ease;
}

#app.sidebar-open #sidebar-toggle {
  left: calc(260px + 0.6rem);
}

/* Same selector/specificity as the desktop rule above — must stay declared
   after it so the cascade's source-order tiebreak actually picks this one
   up on narrow viewports (an earlier version of this rule sat up near
   #sidebar's own breakpoint instead and silently lost every tie to the
   later desktop rule; caught by an automated bounding-box check, not
   visually). #sidebar's own width formula (80vw, max-width 300px) mirrored
   exactly so the button docks flush against its real edge. */
@media (max-width: 700px) {
  #app.sidebar-open #sidebar-toggle {
    left: calc(min(80vw, 300px) + 0.6rem);
  }
}

@media (prefers-reduced-motion: reduce) {
  #sidebar-toggle {
    transition: none;
  }
}

#messages {
  flex: 1;
  overflow-y: auto;
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.message {
  max-width: 80%;
  padding: 0.6rem 0.9rem;
  border-radius: 0.9rem;
  white-space: pre-wrap;
  line-height: 1.4;
}

.message.user {
  align-self: flex-end;
  background: var(--bubble-user);
  color: var(--bubble-user-text);
  border-bottom-right-radius: 0.2rem;
}

.message.assistant {
  align-self: flex-start;
  background: var(--bubble-assistant);
  color: var(--bubble-assistant-text);
  border-bottom-left-radius: 0.2rem;
}

.message.pending {
  align-self: flex-start;
  color: var(--muted);
  font-style: italic;
  /* Task 27: the wait words vary in length — this guards against a visible
     height flicker between the shortest/longest ones without forcing
     nowrap, which would risk overflowing the bubble on a narrow viewport
     for the longer phrases instead. */
  min-height: 1.4em;
}

.message.error {
  align-self: flex-start;
  background: var(--bubble-error);
  color: var(--bubble-error-text);
}

/* Task 32: manual retry after api.js's own automatic retries are exhausted.
   Plain underlined text-button (like .job-download) rather than a boxed
   button — it sits inside the already-colored error bubble, no extra chrome
   needed. */
.error-resend {
  display: block;
  margin-top: 0.4rem;
  font: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  color: inherit;
  text-decoration: underline;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}

.error-resend:disabled {
  cursor: default;
  opacity: 0.6;
}

/* Task 2: async skill jobs (currently flashcards, INTERFACES.md §6/§7). */
.message.job {
  align-self: flex-start;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.4rem;
  background: var(--bubble-assistant);
  color: var(--bubble-assistant-text);
}

.message.job-pending {
  font-style: italic;
  color: var(--muted);
}

.message.job-done {
  background: var(--bubble-success);
  color: var(--bubble-success-text);
  font-style: normal;
}

.message.job-error {
  background: var(--bubble-error);
  color: var(--bubble-error-text);
  font-style: normal;
}

.job-label {
  margin: 0;
}

.job-download {
  font-size: 0.85rem;
  font-weight: 600;
  color: inherit;
  text-decoration: underline;
}

.job-refresh {
  font: inherit;
  font-size: 0.75rem;
  padding: 0.25rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--muted);
  cursor: pointer;
}

/* Task 57 (api task 73): choice-prompt picker — an assistant-side bubble that
   carries an interactive form (radios/checkboxes + optional free text). Uses
   the same tokens as everything else, so it's theme-aware for free. */
.message.prompt {
  align-self: flex-start;
  background: var(--bubble-assistant);
  color: var(--bubble-assistant-text);
  border-bottom-left-radius: 0.2rem;
  white-space: normal;
}

.prompt-form {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 0.6rem;
}

.prompt-options {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.prompt-option {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  background: var(--surface);
  cursor: pointer;
}

.prompt-option:hover {
  background: var(--surface-hover);
}

.prompt-option input {
  margin-top: 0.15rem;
  flex-shrink: 0;
}

.prompt-option input:disabled {
  cursor: default;
}

.prompt-option-body {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}

.prompt-option-label {
  font-weight: 600;
}

.prompt-option-description {
  font-size: 0.85rem;
  color: var(--muted);
}

.prompt-freetext {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.prompt-freetext-label {
  font-size: 0.85rem;
  color: var(--muted);
}

.prompt-freetext-input {
  font: inherit;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  background: var(--surface);
  color: var(--text);
  resize: vertical;
}

.prompt-hint {
  margin: 0;
  font-size: 0.85rem;
  color: var(--bubble-error-text, #b00);
}

.prompt-submit {
  align-self: flex-start;
  font: inherit;
  padding: 0.35rem 1.1rem;
  border: none;
  border-radius: 0.6rem;
  background: var(--bubble-user);
  color: var(--bubble-user-text);
  cursor: pointer;
}

.prompt-submit:disabled {
  opacity: 0.5;
  cursor: default;
}

.prompt-sent-note {
  margin: 0;
  font-size: 0.85rem;
  color: var(--muted);
  font-style: italic;
}

/* Task 60: A/B comparison — two full answers side by side (stacked when
   narrow) plus the judging form, which reuses the .message.prompt/.prompt-form
   styling above. The wrapper is not a .message, so the pair isn't squeezed
   into one bubble's max-width. */
.comparison-turn {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.comparison-variants {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.75rem;
  align-items: start;
}

@media (max-width: 800px) {
  .comparison-variants {
    grid-template-columns: minmax(0, 1fr);
  }
}

.comparison-variant {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  min-width: 0;
}

.comparison-variant-label {
  margin: 0;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The variant answers fill their own column instead of the 80% a free-floating
   bubble gets, and their sources stay inside it. */
.comparison-variant > .message,
.comparison-variant > .sources {
  max-width: 100%;
  align-self: stretch;
}

.comparison-choice {
  align-self: flex-start;
}

/* Task 40: a staged/async answer's turn. Its finished pieces (reasoning,
   bubble, feedback, sources) render into this wrapper so they replace the
   in-progress bubble in place rather than being re-appended to the bottom.
   Mirrors #messages' own column layout + gap so the inner elements sit exactly
   as they would as direct children of the list. */
.staged-turn {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

/* The dezent "schreibt weiter …" state under the provisional answer once the
   first section has arrived — wait words only run up to that point (task 40). */
.staged-writing {
  align-self: flex-start;
  margin-top: -0.35rem;
  color: var(--muted);
  font-style: italic;
  font-size: 0.85rem;
}

/* Task 29: thumbs-up/-down per answer (api task 39). Hidden unless #app
   carries "feedback-enabled" (app.js's /config feature detection — an older
   api without POST /feedback means the whole surface stays invisible, same
   pattern as the sidebar's class toggle above). The bar sits between the
   answer bubble and its sources, deliberately small and muted — feedback is
   an offer, not a prompt. */
.feedback-bar,
.feedback-comment {
  display: none;
}

#app.feedback-enabled .feedback-bar {
  align-self: flex-start;
  display: flex;
  align-items: center;
  gap: 0.35rem;
  margin-top: -0.35rem;
}

.feedback-button {
  font: inherit;
  font-size: 0.8rem;
  line-height: 1;
  padding: 0.25rem 0.45rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  cursor: pointer;
  /* The emoji glyphs read as washed-out at full color next to muted chrome —
     dimmed until hover/chosen so the resting state stays dezent. */
  filter: grayscale(1);
  opacity: 0.75;
}

.feedback-button:hover:not(:disabled) {
  filter: none;
  opacity: 1;
  border-color: var(--bubble-user);
}

.feedback-button:disabled {
  cursor: default;
}

.feedback-button--chosen {
  filter: none;
  opacity: 1;
  border-color: var(--bubble-user);
  background: var(--accent-bg);
}

.feedback-status {
  font-size: 0.75rem;
  color: var(--muted);
}

#app.feedback-enabled .feedback-comment {
  align-self: flex-start;
  display: flex;
  align-items: flex-end;
  gap: 0.4rem;
  max-width: 80%;
  margin-top: -0.15rem;
}

.feedback-comment-input {
  font: inherit;
  font-size: 0.8rem;
  width: 18rem;
  max-width: 100%;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  background: var(--surface);
  resize: vertical;
}

.feedback-comment-send {
  font: inherit;
  font-size: 0.75rem;
  padding: 0.25rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--muted);
  cursor: pointer;
}

.sources {
  align-self: flex-start;
  max-width: 80%;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin-top: -0.35rem;
}

/* Task 30: <summary> now (was <p>) — same ▸/▾ marker treatment as
   .reasoning above, so a collapsed/expandable section reads consistently
   across the app. */
.sources-heading {
  margin: 0;
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--muted);
  cursor: pointer;
  list-style: none;
}

.sources-heading::-webkit-details-marker {
  display: none;
}

.sources-heading::before {
  content: "▸ ";
}

.sources[open] > .sources-heading::before {
  content: "▾ ";
}

.source-entry {
  border-radius: 0.6rem;
  overflow: hidden;
}

/* Task 15: a chip used to be a <summary> that expanded its preview inline
   below the message; it's now a plain button that opens/updates the
   right-hand preview panel instead, so it no longer needs the
   details-marker/open-state styling that used to live here. */
.source-chip {
  display: flex;
  align-items: baseline;
  width: 100%;
  font: inherit;
  border: 1px solid var(--border);
  border-radius: 0.6rem;
  background: var(--surface);
  font-size: 0.75rem;
  color: var(--muted);
  padding: 0.35rem 0.7rem;
  cursor: pointer;
  white-space: nowrap;
  text-align: left;
}

/* Only the folder segment shrinks/ellipsizes — the citation number and
   "filename · location" (chip-prefix/chip-tail) always stay fully visible,
   see app.js#buildChipLabel for why. */
.chip-prefix,
.chip-tail {
  flex-shrink: 0;
  white-space: nowrap;
}

.chip-folder {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Task 38: dezentes Badge für Quellen aus dem kuratierten Recherche/-Bereich
   (externe Studien statt Kursmaterial) — same shrink-resistant treatment as
   chip-prefix/chip-tail above so it never gets squeezed out. */
.chip-badge {
  flex-shrink: 0;
  white-space: nowrap;
  font-size: 0.65rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  margin-left: 0.4rem;
}

.chip-badge--research {
  background: var(--accent-bg);
  color: var(--bubble-user);
}

.source-chip:hover {
  color: var(--bubble-user);
}

.source-chip--active {
  color: var(--bubble-user);
  border-color: var(--bubble-user);
  background: var(--accent-bg);
}

/* Task 7: a retrieval hit the model didn't actually cite — kept in the list
   (api marks rather than filters), de-emphasized here rather than styled
   identically to a real citation. */
.source-entry--uncited {
  opacity: 0.55;
}

.source-entry--uncited:hover {
  opacity: 0.85;
}

/* Task 26 (api task 37): one row per document for uncited entries, instead
   of one row per page/chunk — the header (folder/filename · ohne Zitat)
   isn't itself clickable, only the individual position/range buttons below
   it are, so it gets a bordered card treatment instead of the plain
   .source-chip button look (no hover-blue, default cursor). */
.source-entry--grouped {
  border: 1px solid var(--border);
  border-radius: 0.6rem;
  background: var(--surface);
  overflow: hidden;
}

.source-entry--grouped .source-chip--grouped {
  border: none;
  border-radius: 0;
  cursor: default;
}

.source-chip--grouped:hover {
  color: var(--muted);
}

.source-positions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  padding: 0 0.7rem 0.6rem;
}

.source-position-link {
  font: inherit;
  font-size: 0.72rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--muted);
  padding: 0.15rem 0.55rem;
  cursor: pointer;
}

.source-position-link:hover {
  color: var(--bubble-user);
  border-color: var(--bubble-user);
  background: var(--accent-bg);
}

/* Task 28 (api task 38): thumbnail gallery of display-only example images on
   a cited source — small squares below the chip, click opens #media-lightbox. */
.source-media {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  padding: 0.35rem 0 0.1rem;
}

.source-media-thumb {
  width: 3.5rem;
  height: 3.5rem;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  background: var(--surface);
  overflow: hidden;
  cursor: pointer;
  flex-shrink: 0;
}

.source-media-thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.source-media-thumb:hover {
  border-color: var(--bubble-user);
}

.source-no-preview {
  margin: 0;
  font-size: 0.8rem;
  color: var(--muted);
  font-style: italic;
}

.source-full-path {
  margin: 0;
  font-size: 0.7rem;
  color: var(--muted);
  word-break: break-word;
}

.source-original {
  align-self: flex-start;
  font-size: 0.75rem;
  color: var(--muted);
  text-decoration: underline;
}

.reasoning {
  align-self: flex-start;
  max-width: 80%;
  font-size: 0.78rem;
  color: var(--muted);
  border-left: 2px solid var(--border);
  padding-left: 0.6rem;
}

.reasoning summary {
  cursor: pointer;
  list-style: none;
}

.reasoning summary::-webkit-details-marker {
  display: none;
}

.reasoning summary::before {
  content: "▸ ";
}

.reasoning[open] summary::before {
  content: "▾ ";
}

.reasoning-body {
  white-space: pre-wrap;
  margin-top: 0.3rem;
  font-style: italic;
}

/* Task 55: per-answer token cost (api task 68's `usage`). Closes the turn,
   quieter than everything above it — smaller and more muted than .reasoning,
   no border, dimmed until hovered/opened. It is a footnote about the request,
   not part of the answer, and must never compete with it for attention. */
.usage {
  align-self: flex-start;
  margin-top: -0.35rem;
  font-size: 0.72rem;
  color: var(--muted);
  opacity: 0.75;
}

.usage summary {
  cursor: pointer;
  list-style: none;
}

.usage summary::-webkit-details-marker {
  display: none;
}

/* Same ▸/▾ affordance as .reasoning — without it the line reads as static text
   and nobody discovers the Prompt/Antwort split. */
.usage summary::before {
  content: "▸ ";
}

.usage[open] summary::before {
  content: "▾ ";
}

.usage[open],
.usage:hover {
  opacity: 1;
}

.usage-body {
  margin-top: 0.15rem;
  font-variant-numeric: tabular-nums;
}

/* Task 12: rendered Markdown subset (markdown.js) for assistant replies and
   the reasoning trace. Plain prose with nothing to format never gets these
   elements at all (bare text node instead), so this only affects answers
   that actually use bold/lists/headings. */
.markdown-content p {
  margin: 0;
}

.markdown-content p + p,
.markdown-content ul,
.markdown-content ol,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
  margin-top: 0.6em;
}

.markdown-content ul,
.markdown-content ol {
  margin-bottom: 0;
  padding-left: 1.4em;
}

.markdown-content li {
  margin: 0.2em 0;
}

.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
  margin: 0.6em 0 0.3em;
  font-size: 1em;
}

/* Task 14: shown above #chat-form whenever a source-picker selection is
   active — the always-visible "referencing: X, Y" indicator that's the
   whole reason the selection is allowed to survive a new chat/session
   switch instead of silently resetting (see app.js's own note on that). */
#source-selection-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  padding: 0.5rem 1rem;
  border-top: 1px solid var(--border);
  background: var(--accent-bg);
  font-size: 0.8rem;
}

/* The [hidden] attribute selector loses to the id selector above on
   specificity alone, so app.js's `.hidden = true` would otherwise be
   silently ignored — caught live during task 14 verification (the bar
   stayed visible after clearing the selection). */
#source-selection-bar[hidden] {
  display: none;
}

#source-selection-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#source-selection-clear {
  flex-shrink: 0;
  font: inherit;
  font-size: 0.75rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  padding: 0.2rem 0.6rem;
  cursor: pointer;
  color: var(--muted);
}

/* Task 59 (api task 77): Tutor-Modus indicator — a persistent 🎓 badge + exit
   affordance shown while api signals `tutor.active` on the latest answer. Same
   bar shape/placement as #source-selection-bar (above the input), and the same
   id-vs-[hidden] specificity fix that bar needed, so app.js's `.hidden = true`
   actually hides it. */
#tutor-indicator {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  padding: 0.5rem 1rem;
  border-top: 1px solid var(--border);
  background: var(--accent-bg);
  font-size: 0.8rem;
}

#tutor-indicator[hidden] {
  display: none;
}

#tutor-indicator-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}

#tutor-indicator-exit {
  flex-shrink: 0;
  font: inherit;
  font-size: 0.75rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  padding: 0.2rem 0.6rem;
  cursor: pointer;
  color: var(--muted);
}

/* Task 23: permanent (not dismissable), unobtrusive transparency notice —
   shown only when api's GET /config says logging is on. Deliberately no
   `display` declared here: leaving that to the browser's own default (block,
   same as any <p>) means the [hidden] attribute's UA-stylesheet
   `display:none` just works untouched, instead of needing the same
   [hidden]-override fix tasks 14/15 already needed twice for elements that
   *did* set their own unconditional display. */
#logging-notice {
  margin: 0;
  padding: 0.35rem 1rem 0;
  font-size: 0.72rem;
  color: var(--muted);
}

#settings-logging-notice {
  margin: 0;
  padding-top: 0.2rem;
  font-size: 0.78rem;
  color: var(--muted);
}

#chat-form {
  display: flex;
  gap: 0.5rem;
  padding: 1rem;
  border-top: 1px solid var(--border);
}

#source-picker-open,
#image-attach-open {
  flex-shrink: 0;
  font-size: 1.1rem;
  border: 1px solid var(--border);
  border-radius: 0.6rem;
  background: var(--surface);
  cursor: pointer;
  padding: 0 0.7rem;
}

/* Task 70: pending photo attachment — same bar shape/placement as
   #source-selection-bar above, and the same id-vs-[hidden] specificity fix
   that bar and the tutor indicator both needed. */
#image-attachment-bar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 1rem;
  border-top: 1px solid var(--border);
  background: var(--accent-bg);
  font-size: 0.8rem;
}

#image-attachment-bar[hidden] {
  display: none;
}

#image-attachment-bar.attachment-error {
  background: var(--bubble-error);
  color: var(--bubble-error-text);
}

#image-attachment-preview {
  width: 2.4rem;
  height: 2.4rem;
  object-fit: cover;
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  flex-shrink: 0;
}

#image-attachment-text {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#image-attachment-clear {
  flex-shrink: 0;
  font: inherit;
  font-size: 0.75rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  padding: 0.2rem 0.6rem;
  cursor: pointer;
  color: var(--muted);
}

/* Task 70: the sent photo's thumbnail inside the user bubble. Reuses
   .source-media-thumb's box/img/hover look (task 28); block + margin so it
   sits on its own line above the question text, and a bit larger — it's the
   user's own photo, recognizability matters more than gallery density. */
.user-image-thumb {
  display: block;
  width: 5.5rem;
  height: 5.5rem;
  margin-bottom: 0.4rem;
}

/* Task 65: a backend-restored turn only knows image_attached (§6 stores no
   base64) — on a device without the cached photo this muted hint stands in
   for the thumbnail. */
.user-image-placeholder {
  font-size: 0.8rem;
  opacity: 0.75;
  margin-bottom: 0.4rem;
}

#chat-input {
  flex: 1;
  resize: none;
  font: inherit;
  padding: 0.6rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: 0.6rem;
}

#send-button {
  font: inherit;
  padding: 0 1.2rem;
  border: none;
  border-radius: 0.6rem;
  background: var(--bubble-user);
  color: var(--bubble-user-text);
  cursor: pointer;
}

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

/* Task 14: the source picker itself — a native <dialog> (built-in modal
   behavior, backdrop, Escape-to-close) rather than a hand-rolled overlay. */
/* display:flex only applies while [open] is present — a bare unconditional
   rule here would out-specificity the UA stylesheet's own
   `dialog:not([open]) { display: none }` and keep the dialog visibly
   rendered even after .close() removes [open] (same class of bug just fixed
   on #source-selection-bar's [hidden], caught the same way — a screenshot
   after clicking the picker's own close button still showed it open). */
#source-picker-modal {
  width: min(560px, 92vw);
  max-height: 80vh;
  padding: 0;
  border: none;
  border-radius: 0.8rem;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.25);
  flex-direction: column;
}

#source-picker-modal[open] {
  display: flex;
}

#source-picker-modal::backdrop {
  background: rgba(0, 0, 0, 0.4);
}

#source-picker-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  padding: 0.9rem 1rem;
  border-bottom: 1px solid var(--border);
}

#source-picker-header h2 {
  margin: 0;
  font-size: 1rem;
}

#source-picker-close {
  flex-shrink: 0;
  font: inherit;
  border: 1px solid var(--border);
  background: var(--surface);
  border-radius: 0.4rem;
  padding: 0.2rem 0.5rem;
  cursor: pointer;
  color: var(--muted);
}

#source-picker-filter {
  margin: 0.8rem 1rem 0;
  font: inherit;
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 0.5rem;
}

#source-picker-list {
  flex: 1;
  overflow-y: auto;
  padding: 0.6rem 1rem;
}

.source-picker-group summary {
  cursor: pointer;
  font-size: 0.85rem;
  font-weight: 600;
  padding: 0.3rem 0;
}

.source-picker-items {
  list-style: none;
  margin: 0.2rem 0 0.6rem;
  padding: 0;
}

.source-picker-items li {
  padding: 0.15rem 0;
}

/* Task 37: one indent step per nesting level — a plain margin-left on any
   .source-picker-group that's itself inside a .source-picker-items compounds
   naturally through the DOM nesting, no depth-tracking needed in app.js. */
.source-picker-items .source-picker-group {
  margin-left: 0.9rem;
}

.source-picker-items label {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-size: 0.82rem;
  cursor: pointer;
}

.source-picker-empty {
  color: var(--muted);
  font-size: 0.85rem;
  font-style: italic;
}

#source-picker-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  padding: 0.7rem 1rem;
  border-top: 1px solid var(--border);
  font-size: 0.8rem;
  color: var(--muted);
}

#source-picker-clear {
  font: inherit;
  font-size: 0.75rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  padding: 0.25rem 0.6rem;
  cursor: pointer;
  color: var(--muted);
}

/* Task 35's breed directory used to live here. It moved to the breed skill's
   own stylesheet (`skills/breeds.css`, injected when the surface mounts) when
   the edition layer was introduced — app.css is the edition-neutral core and
   must not carry a single edition's feature. See README.md "Ein neues
   Edition-Theme anlegen". */

/* Task 28: image lightbox — same native-<dialog> pattern as the picker
   above (including the [open]-specificity gotcha, see that note). Sized by
   its image, capped to the viewport; the close button floats over the
   image's top-right corner so the layout never jumps with image size.
   Task 36: its near-black canvas is intentionally the same in both color
   schemes (a photo viewer, not a themed UI surface) — left as a literal,
   not a token, on purpose. */
#media-lightbox {
  position: relative;
  padding: 0;
  border: none;
  border-radius: 0.8rem;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.25);
  background: #1a1a1a;
  overflow: hidden;
}

#media-lightbox[open] {
  display: flex;
  flex-direction: column;
}

#media-lightbox::backdrop {
  background: rgba(0, 0, 0, 0.6);
}

#media-lightbox img {
  display: block;
  max-width: min(1100px, 90vw);
  max-height: 82vh;
  object-fit: contain;
}

#media-lightbox-close {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  font: inherit;
  border: none;
  border-radius: 0.4rem;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  padding: 0.2rem 0.5rem;
  cursor: pointer;
}

#media-lightbox-caption {
  margin: 0;
  padding: 0.5rem 0.8rem;
  font-size: 0.75rem;
  color: #d5d5d5;
  overflow-wrap: anywhere;
}

/* Task 19: the app's first settings dialog — same native-<dialog> pattern
   (and the same [open]-specificity gotcha, see the source-picker-modal note
   above) as task 14's picker. */
#settings-modal {
  width: min(420px, 92vw);
  padding: 0;
  border: none;
  border-radius: 0.8rem;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.25);
  flex-direction: column;
}

#settings-modal[open] {
  display: flex;
}

#settings-modal::backdrop {
  background: rgba(0, 0, 0, 0.4);
}

#settings-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  padding: 0.9rem 1rem;
  border-bottom: 1px solid var(--border);
}

#settings-header h2 {
  margin: 0;
  font-size: 1rem;
}

#settings-close {
  flex-shrink: 0;
  font: inherit;
  border: 1px solid var(--border);
  background: var(--surface);
  border-radius: 0.4rem;
  padding: 0.2rem 0.5rem;
  cursor: pointer;
  color: var(--muted);
}

#settings-body {
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

.settings-field {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.8rem;
  font-size: 0.85rem;
}

.settings-field select {
  font: inherit;
  padding: 0.3rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  background: var(--surface);
}

/* ---------------------------------------------------------------------------
   Task 47 (Multi-User Phase 1): auth controls, login overlay, admin dialog.
   Reuses the header-button look (.header-action) and the native-<dialog>
   pattern (settings/picker) so the login/admin surfaces sit in the
   existing visual system rather than a new one. All colors via the task-36
   tokens, so light/dark come for free.
   --------------------------------------------------------------------------- */

/* Header auth cluster — same flex-shrink:0 pill/button shape as .header-action,
   so nothing here can push the header action buttons out of the header. */
#auth-status {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.8rem;
  color: var(--muted);
  min-width: 0;
}

/* The [hidden] attribute selector loses to the id selector above — the same
   specificity trap #source-selection-bar/#tutor-indicator/#image-attachment-bar
   already hit. So auth.js's logged-out path *did* set `statusEl.hidden = true`,
   it just had no visual effect: "Abmelden" stayed on screen right next to
   "Anmelden", i.e. both header states at once (found live). It also made the
   logout button visible before /auth/me had answered anything at all, and on an
   older api without /auth/* (where no auth control may appear). One state at a
   time again. */
#auth-status[hidden] {
  display: none;
}

#auth-username {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 24vw;
}

#auth-login-button,
#auth-logout-button,
#auth-admin-button {
  flex-shrink: 0;
  font: inherit;
  font-size: 0.8rem;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  border-radius: 0.4rem;
  padding: 0.3rem 0.6rem;
  cursor: pointer;
}

@media (max-width: 700px) {
  #auth-username {
    max-width: 18vw;
  }
}

/* Task 50: login as a real full page — an OPAQUE full-viewport surface (the old
   translucent scrim let the chat show through, which the user flagged). Not a
   <dialog>, so it can be shown undismissable when an enforced 401 forces a login
   (the app underneath must not be usable). Sits above everything, incl. native
   dialog backdrops and the admin page. */
#auth-login-page {
  position: fixed;
  inset: 0;
  z-index: 1100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: var(--bg);
}

#auth-login-page[hidden] {
  display: none;
}

/* Boot gate (see index.html's <html class="auth-boot">). The chat shell must
   never be painted for a caller who may not see it — not for one frame. Hiding
   the shell rather than showing an overlay over it is the difference between
   "covered" and "never rendered".
   `visibility`, not `display: none`: the shell keeps its real layout while
   hidden, so app.js's boot-time measuring (textarea autosize, panel widths, the
   dvh height dance) sees the same geometry it always did and nothing reflows
   when the gate lifts — and a `visibility: hidden` subtree is painted nowhere
   and exposed to no accessibility tree, which is the whole requirement. */
html.auth-boot #app {
  visibility: hidden;
}

/* The remembered-gate replay: paint the login page before any script has run.
   Beats the `[hidden]` rule above on specificity, and auth.js takes over
   authoritatively (same `display: flex`, so the handover is invisible) the
   moment /auth/me has actually answered. */
html.auth-boot-gated #auth-login-page[hidden] {
  display: flex;
}

.auth-login-brand {
  margin: 0 0 0.2rem;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

#auth-login-form {
  width: min(360px, 100%);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 0.8rem;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
  padding: 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

#auth-login-form h2 {
  margin: 0;
  font-size: 1.15rem;
}

.auth-field {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  font-size: 0.85rem;
  color: var(--muted);
}

.auth-field input {
  font: inherit;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  background: var(--surface);
  color: var(--text);
}

.auth-error {
  margin: 0;
  padding: 0.5rem 0.7rem;
  border-radius: 0.4rem;
  background: var(--bubble-error);
  color: var(--bubble-error-text);
  font-size: 0.85rem;
}

.auth-login-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.6rem;
}

.auth-login-actions button {
  font: inherit;
  padding: 0.5rem 1rem;
  border-radius: 0.4rem;
  cursor: pointer;
}

#auth-login-cancel {
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
}

#auth-login-submit {
  border: 1px solid var(--bubble-user);
  background: var(--bubble-user);
  color: var(--bubble-user-text);
}

#auth-login-submit:disabled {
  opacity: 0.6;
  cursor: default;
}

/* --- Task 50: the admin page ------------------------------------------------
   ONE full-viewport, opaque page (chat NOT showing through) replacing the two
   former dialogs. A header bar, an internal tab nav (Rollen · Benutzer ·
   Verbrauch), and a scrolling body that shows one section at a time with real
   width for the folder tree. */
#admin-page {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
}

#admin-page[hidden] {
  display: none;
}

#admin-page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  padding: 0.9rem 1.2rem;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}

#admin-page-header h2 {
  margin: 0;
  font-size: 1.15rem;
}

#admin-page-close {
  flex-shrink: 0;
  font: inherit;
  font-size: 0.85rem;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  border-radius: 0.4rem;
  padding: 0.35rem 0.8rem;
  cursor: pointer;
}

#admin-nav {
  display: flex;
  gap: 0.3rem;
  padding: 0.6rem 1.2rem 0;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
  overflow-x: auto;
}

.admin-nav-tab {
  font: inherit;
  font-size: 0.9rem;
  border: none;
  background: none;
  color: var(--muted);
  padding: 0.5rem 0.9rem;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  cursor: pointer;
  white-space: nowrap;
}

.admin-nav-tab:hover {
  color: var(--text);
}

.admin-nav-tab.admin-nav-tab--active {
  color: var(--text);
  font-weight: 600;
  border-bottom-color: var(--bubble-user);
}

#admin-page-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 1.2rem;
}

/* One section visible at a time; the active one gets a comfortable reading
   width but the folder tree inside Rollen may use the full column. */
.admin-section {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  max-width: 900px;
  margin: 0 auto;
}

.admin-section[hidden] {
  display: none;
}

#auth-admin-create-form {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--border);
}

#auth-admin-create-form h3 {
  margin: 0;
  font-size: 0.9rem;
}

.auth-admin-create-fields {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.auth-admin-create-fields input,
.auth-admin-create-fields select {
  font: inherit;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  background: var(--surface);
  color: var(--text);
  flex: 1 1 8rem;
  min-width: 0;
}

.auth-admin-create-fields button {
  font: inherit;
  padding: 0.4rem 0.9rem;
  border: 1px solid var(--bubble-user);
  background: var(--bubble-user);
  color: var(--bubble-user-text);
  border-radius: 0.4rem;
  cursor: pointer;
  flex: 0 0 auto;
}

#auth-admin-list {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.auth-user-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.45rem 0.55rem;
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  background: var(--bg-subtle);
}

.auth-user-row--inactive {
  opacity: 0.6;
}

.auth-user-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.9rem;
}

.auth-user-you {
  color: var(--muted);
  font-size: 0.8rem;
}

.auth-user-role {
  font: inherit;
  font-size: 0.8rem;
  padding: 0.25rem 0.4rem;
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  background: var(--surface);
  color: var(--text);
  flex-shrink: 0;
}

.auth-user-active {
  font: inherit;
  font-size: 0.78rem;
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  cursor: pointer;
  flex-shrink: 0;
  background: var(--bubble-success);
  color: var(--bubble-success-text);
}

.auth-user-active[aria-pressed="false"] {
  background: var(--bubble-error);
  color: var(--bubble-error-text);
}

/* --- Task 48/50: role & knowledge-area management --------------------------
   Now lives in the Rollen section of the admin page (was a dialog). */
#auth-roles-create-form {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

#auth-roles-create-form h3 {
  margin: 0;
  font-size: 0.9rem;
}

.auth-roles-create-fields {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.auth-roles-create-fields input {
  font: inherit;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  background: var(--surface);
  color: var(--text);
  flex: 1 1 10rem;
  min-width: 0;
}

.auth-roles-create-fields button {
  font: inherit;
  padding: 0.4rem 0.9rem;
  border: 1px solid var(--bubble-user);
  background: var(--bubble-user);
  color: var(--bubble-user-text);
  border-radius: 0.4rem;
  cursor: pointer;
  flex: 0 0 auto;
}

.auth-roles-hint {
  margin: 0;
  font-size: 0.8rem;
  color: var(--muted);
}

#auth-roles-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.auth-role-row {
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  background: var(--bg-subtle);
  overflow: hidden;
}

.auth-role-head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.6rem;
}

.auth-role-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.9rem;
  font-weight: 600;
}

.auth-role-badge {
  font-size: 0.7rem;
  font-weight: 400;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 0.3rem;
  padding: 0.05rem 0.3rem;
  vertical-align: middle;
}

.auth-role-summary {
  flex-shrink: 0;
  font-size: 0.78rem;
  color: var(--muted);
}

.auth-role-edit,
.auth-role-delete {
  flex-shrink: 0;
  font: inherit;
  font-size: 0.78rem;
  padding: 0.3rem 0.55rem;
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
}

.auth-role-delete {
  background: var(--bubble-error);
  color: var(--bubble-error-text);
  border-color: var(--bubble-error);
}

.auth-role-editor {
  padding: 0.4rem 0.6rem 0.7rem;
  border-top: 1px solid var(--border);
}

.auth-role-editor-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 0.6rem;
}

.auth-role-save,
.auth-role-cancel {
  font: inherit;
  font-size: 0.82rem;
  padding: 0.35rem 0.85rem;
  border-radius: 0.4rem;
  cursor: pointer;
}

.auth-role-cancel {
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
}

.auth-role-save {
  border: 1px solid var(--bubble-user);
  background: var(--bubble-user);
  color: var(--bubble-user-text);
}

.auth-role-save:disabled {
  opacity: 0.6;
  cursor: default;
}

/* The grantable folder tree — same collapsible look as the source picker
   (task 37), scoped to its own classes so the two never interfere. */
.auth-area-tree,
.auth-area-items {
  list-style: none;
  margin: 0;
  padding: 0;
}

.auth-area-items {
  padding-left: 1rem;
  border-left: 1px solid var(--border);
  margin-left: 0.35rem;
}

.auth-area-leaf {
  padding: 0.15rem 0;
}

.auth-area-group > summary {
  cursor: pointer;
  padding: 0.15rem 0;
  list-style-position: inside;
}

.auth-area-label {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.85rem;
  cursor: pointer;
}

.auth-area-label input {
  flex-shrink: 0;
}

.auth-area-name {
  font-size: 0.85rem;
  color: var(--muted);
}

.auth-roles-empty {
  font-size: 0.85rem;
  color: var(--muted);
  padding: 0.3rem 0;
}

/* --- Task 50: role rename (api 63 PATCH), inline in the role head. --------- */
.auth-role-rename {
  flex-shrink: 0;
  font: inherit;
  font-size: 0.78rem;
  padding: 0.3rem 0.55rem;
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
}

.auth-role-rename-form {
  display: flex;
  flex: 1;
  gap: 0.4rem;
  align-items: center;
}

.auth-role-rename-input {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-size: 0.9rem;
  padding: 0.25rem 0.4rem;
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  background: var(--surface);
  color: var(--text);
}

/* --- Task 50: "Original-Quellen öffnen erlaubt" per-role toggle (api 63
   can_open_sources). A full-width row under the role head, shown only when the
   api carries the field. --------------------------------------------------- */
.auth-role-opensrc {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.4rem 0.6rem;
  border-top: 1px solid var(--border);
  font-size: 0.85rem;
  color: var(--text);
  cursor: pointer;
}

.auth-role-opensrc input {
  flex-shrink: 0;
}

.auth-role-opensrc-hint {
  color: var(--muted);
  font-size: 0.78rem;
}

/* Give the grant editor's folder tree the full section width (the old dialog
   scroll-box was cramped — the whole point of the page rework). */
.auth-role-editor .auth-area-tree {
  max-height: none;
}

/* --- Task 50: token-usage views (api 62) ----------------------------------
   Admin "Verbrauch" table + the per-user "mein Verbrauch" block in settings. */
#admin-usage-content {
  overflow-x: auto;
}

.usage-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
}

.usage-table th,
.usage-table td {
  text-align: left;
  padding: 0.45rem 0.6rem;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

.usage-table th {
  color: var(--muted);
  font-weight: 600;
}

.usage-table td.usage-num,
.usage-table th.usage-num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.usage-model-row td {
  color: var(--muted);
  font-size: 0.8rem;
}

.usage-model-row .usage-model-name {
  padding-left: 1.4rem;
}

.usage-empty,
.usage-note {
  font-size: 0.85rem;
  color: var(--muted);
  margin: 0;
}

.settings-usage {
  border-top: 1px solid var(--border);
  padding-top: 0.8rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.settings-usage h3 {
  margin: 0;
  font-size: 0.9rem;
}

.settings-usage-figures {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.2rem;
  font-size: 0.85rem;
}

.settings-usage-figures span strong {
  font-variant-numeric: tabular-nums;
}

/* --- Task 50: a source whose original/preview the api withheld (a role
   without can_open_sources — download_url/preview_url stripped). The chip is
   rendered non-interactive: it still names the source (attribution/citation)
   but offers no open/download affordance. */
.source-chip--locked {
  cursor: default;
  opacity: 0.85;
}

.source-locked-note {
  font-size: 0.78rem;
  color: var(--muted);
  margin: 0.3rem 0 0;
}

/* --- Task 61: Karten-Review (api task 87, DESIGN.md §12) -------------------
   A review surface whose bottleneck is the user's time, so the layout is built
   for speed of reading rather than density: one card at a time, the judgement
   row always in the same place, the key legend permanently visible. All colours
   come from the existing tokens, so light/dark (task 36) work for free. */
.cards-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.cards-stat {
  display: flex;
  align-items: baseline;
  gap: 0.35rem;
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  background: var(--surface);
  padding: 0.3rem 0.65rem;
}

.cards-stat-label {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--muted);
}

.cards-stat-value {
  font-size: 0.9rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.card-review {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  border: 1px solid var(--border);
  border-radius: 0.7rem;
  background: var(--surface);
  padding: 1.1rem;
}

.card-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.card-meta-tag {
  font-size: 0.68rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--bubble-user);
  background: var(--accent-bg);
  border-radius: 999px;
  padding: 0.1rem 0.5rem;
}

.card-block-heading {
  margin: 0 0 0.35rem;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--muted);
}

.card-block-body {
  font-size: 0.95rem;
  line-height: 1.5;
}

.card-block-body > :first-child { margin-top: 0; }
.card-block-body > :last-child { margin-bottom: 0; }

.card-options {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.card-option {
  display: flex;
  gap: 0.5rem;
  align-items: baseline;
  border: 1px solid var(--border);
  border-radius: 0.45rem;
  padding: 0.35rem 0.6rem;
  font-size: 0.9rem;
}

/* The correct options are marked outright — this is a review of the card, not a
   quiz for the reviewer, so hiding the solution would only slow judging down. */
.card-option--correct {
  border-color: var(--bubble-success-text);
  background: var(--bubble-success);
  color: var(--bubble-success-text);
}

.card-option-input {
  flex-shrink: 0;
  margin: 0;
}

/* The disabled input already shows the correct options; this spells it out in
   words too, so the answer key does not depend on spotting a greyed-out tick. */
.card-option-marker {
  flex-shrink: 0;
  margin-left: auto;
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

.card-option-text {
  flex: 1 1 auto;
  min-width: 0;
}

.card-source-chip {
  width: auto;
  max-width: 100%;
  text-decoration: none;
}

.card-source-missing {
  margin: 0;
  font-size: 0.85rem;
  color: var(--muted);
}

.card-actions {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  border-top: 1px solid var(--border);
  padding-top: 0.9rem;
}

.card-actions-primary,
.card-reasons {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
}

.card-reasons-label {
  font-size: 0.75rem;
  color: var(--muted);
  margin-right: 0.15rem;
}

.card-action {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font: inherit;
  font-size: 0.85rem;
  border: 1px solid var(--border);
  border-radius: 0.45rem;
  background: var(--surface);
  color: var(--text);
  padding: 0.4rem 0.75rem;
  cursor: pointer;
}

.card-action:hover {
  background: var(--surface-hover);
}

.card-action--approve {
  border-color: var(--bubble-success-text);
  color: var(--bubble-success-text);
}

.card-action--reject {
  border-color: var(--bubble-error-text);
  color: var(--bubble-error-text);
}

.card-reason {
  font-size: 0.78rem;
  padding: 0.28rem 0.55rem;
  color: var(--muted);
}

.card-reason:hover {
  color: var(--text);
}

/* Shortcut badges on the buttons and in the legend are the same object, so they
   read as one system — the button teaches the key, the legend recalls it. */
.card-action kbd,
.cards-legend kbd {
  font-family: inherit;
  font-size: 0.7rem;
  font-weight: 700;
  min-width: 1.1rem;
  text-align: center;
  border: 1px solid var(--border);
  border-bottom-width: 2px;
  border-radius: 0.25rem;
  background: var(--bg-subtle);
  color: var(--muted);
  padding: 0.05rem 0.3rem;
}

.cards-legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  font-size: 0.78rem;
  color: var(--muted);
  border-top: 1px solid var(--border);
  padding-top: 0.7rem;
}

.cards-legend[hidden] {
  display: none;
}

.cards-legend-title {
  font-weight: 600;
  text-transform: uppercase;
  font-size: 0.68rem;
  letter-spacing: 0.03em;
}

.cards-legend span {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
}

.cards-empty {
  margin: 0;
  padding: 2rem 0;
  text-align: center;
  color: var(--muted);
}

.card-edit {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  border: 1px solid var(--bubble-user);
  border-radius: 0.7rem;
  background: var(--surface);
  padding: 1.1rem;
}

.card-edit-hint {
  margin: 0;
  font-size: 0.8rem;
  color: var(--muted);
}

.card-edit-label {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: 0.75rem;
  color: var(--muted);
}

.card-edit-field {
  font: inherit;
  font-size: 0.9rem;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  padding: 0.45rem 0.6rem;
  resize: vertical;
}

.card-edit-actions {
  display: flex;
  gap: 0.4rem;
}
