/* Exige Workshop — chat UI: dark, fast, grounded */
:root {
  --bg: #0e1116;
  --bg-elev: #161b22;
  --bg-elev-2: #1c232c;
  --bg-elev-3: #232b35;
  --border: #2a313c;
  --border-strong: #3a4250;
  --text: #e6edf3;
  --muted: #8b949e;
  /* Aliases used by the auth + user-widget styles (added for stage 2 auth) */
  --fg: var(--text);
  --fg-mute: var(--muted);
  --accent: #1f8f4d;
  --accent-2: #34c77b;
  --highlight: #ffd86b;
  --danger: #f85149;
  --radius: 10px;
  --radius-sm: 6px;
  --radius-lg: 14px;
  --shadow: 0 6px 24px rgba(0,0,0,.35);
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; height: 100%; }
body {
  font-family: var(--font);
  background: var(--bg);
  color: var(--text);
  font-size: 14.5px;
  line-height: 1.5;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

a { color: var(--accent-2); text-decoration: none; }
a:hover { text-decoration: underline; }

.muted { color: var(--muted); }
.small { font-size: 12.5px; }

button {
  font-family: var(--font);
  font-size: 14px;
  cursor: pointer;
}

/* ---------- Topbar ---------- */
.topbar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 18px;
  background: var(--bg-elev);
  border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 10;
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand .logo {
  height: 44px; width: auto; display: block;
  filter: drop-shadow(0 2px 4px rgba(0,0,0,.45));
}
.brand h1 { margin: 0; font-size: 16px; line-height: 1.2; }
.brand .sub { margin: 0; color: var(--muted); font-size: 12px; }
.topnav { display: flex; gap: 8px; }
.nav-btn {
  background: var(--bg-elev-2); color: var(--text);
  border: 1px solid var(--border); padding: 6px 12px;
  border-radius: var(--radius-sm);
}
.nav-btn:hover { background: var(--bg-elev-3); }

/* Messages button — has icon + label + badge cluster */
.nav-btn-messages {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.nav-btn-icon { font-size: 16px; }
.nav-btn-label { font-size: 13px; }
.nav-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  margin-left: 4px;
  border-radius: 9px;
  background: #c14242;
  color: #fff;
  font-size: 11px;
  font-weight: 600;
  line-height: 1;
}
/* Per-feedback-bar conversation link styling */
.fb-conv-link {
  color: var(--accent);
  text-decoration: underline;
  font-weight: 500;
}
.fb-conv-link:hover { text-decoration: none; }

/* Hamburger button — visible only on phone + tablet via @media blocks
 * below. Default hidden so desktop layout is byte-for-byte unchanged. */
.drawer-toggle {
  display: none;
  background: var(--bg-elev-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  width: 44px; height: 44px;
  font-size: 20px;
  align-items: center; justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
}
.drawer-toggle:hover { background: var(--bg-elev-3); }

/* User avatar circle (phone-only). Hidden on desktop+tablet; revealed
 * via @media (max-width: 640px) in the responsive scaffolding block. */
.user-avatar {
  /* Avatar is now visible on ALL viewports (was phone-only pre-refactor).
     Display flex when JS makes it visible after /api/whoami returns. */
  display: inline-flex;
  position: relative;
  background: var(--accent);
  color: #0c1018;
  width: 36px; height: 36px;
  border-radius: 50%;
  border: 2px solid transparent;
  font-weight: 700; font-size: 15px;
  align-items: center; justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
}
.user-avatar:hover { filter: brightness(1.1); }
/* Yellow ring for admin accounts. */
.user-avatar.is-admin { border-color: #d4a838; }
/* Gmail-iOS style unread badge in the top-right corner of the avatar.
   JS sets text + toggles hidden based on the summed unread count
   (messages + notifications). */
.user-avatar-badge {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 9px;
  background: #c14242;
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
  border: 2px solid var(--bg);
  pointer-events: none;
  box-sizing: border-box;
}

/* Avatar popover. Static HTML has the [hidden] attribute set so it's
   invisible on first paint. JS removes [hidden] to show, sets it back
   to hide. The [open] attribute mirrors hidden state for ARIA + tests
   (aria-expanded toggles in lockstep). Mobile @media block at the end
   of the file overrides position to top-right of the viewport. */
.user-popover {
  position: absolute;
  top: 56px;
  right: 16px;
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
  min-width: 240px;
  max-width: 320px;
  z-index: 50;
  padding: 6px 0;
}
.user-popover[hidden] { display: none; }
.user-popover-header {
  padding: 8px 14px;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.user-popover-name {
  font-weight: 600;
  font-size: 14px;
  color: var(--text);
  word-break: break-word;
  flex: 1;
}
.user-popover-item {
  display: flex;
  align-items: center;
  width: 100%;
  padding: 10px 14px;
  background: transparent;
  border: none;
  color: var(--text);
  text-align: left;
  font-size: 14px;
  cursor: pointer;
  text-decoration: none;
  font-family: inherit;
  box-sizing: border-box;
}
.user-popover-item:hover { background: var(--bg-elev-2); }
.user-popover-item-label { flex: 1; }
.user-popover-section-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 22px;
  padding: 0 7px;
  border-radius: 11px;
  background: #c14242;
  color: #fff;
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
}
.user-popover-divider {
  height: 1px;
  background: var(--border);
  margin: 4px 0;
}
.user-popover-form { display: block; margin: 0; }
.user-popover-form button { width: 100%; }
.user-popover-logout { color: var(--danger); }
.user-popover-logout:hover { background: rgba(255, 80, 80, 0.08); }
.user-popover .user-admin-badge {
  font-size: 10px;
  padding: 2px 6px;
  background: #d4a838;
  color: #0c1018;
  border-radius: 4px;
  font-weight: 700;
}

/* Phone-only kebab + popover for chat-head thread actions. */
.thread-kebab { display: none; }
.thread-actions-menu { display: none; }
.thread-actions-menu[hidden] { display: none; }

/* Phone+tablet only — the drawer-bottom Parts/Settings buttons.
 * Hidden by default on desktop where the topbar already has Parts +
 * Settings buttons. Revealed by the @media phone+tablet blocks. */
.drawer-tools { display: none; }

/* ---------- Buttons ---------- */
.primary {
  background: var(--accent); color: #fff; border: 0;
  padding: 8px 14px; border-radius: var(--radius-sm); font-weight: 600;
  display: inline-flex; align-items: center; gap: 8px;
}
.primary:hover { background: var(--accent-2); }
.primary:disabled, .primary.busy { opacity: .7; cursor: progress; }
.ghost {
  background: transparent; border: 1px solid var(--border);
  color: var(--text); padding: 6px 12px; border-radius: var(--radius-sm);
}
.ghost:hover { background: var(--bg-elev-2); }
.ghost.danger { color: var(--danger); border-color: rgba(248,81,73,.35); }
.ghost.danger:hover { background: rgba(248,81,73,.12); }
.small { font-size: 12.5px; padding: 4px 8px; }
.spinner {
  width: 14px; height: 14px; border-radius: 50%;
  border: 2px solid rgba(255,255,255,.3); border-top-color: #fff;
  display: none; animation: spin .8s linear infinite;
}
.busy .spinner { display: inline-block; }
.busy .btn-label { opacity: .7; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------- Layout shell ---------- */
.app-shell {
  flex: 1;
  display: grid;
  grid-template-columns: 280px 1fr;
  min-height: 0;
  height: calc(100vh - 57px);
}

/* ---------- Sidebar ---------- */
.sidebar {
  background: var(--bg-elev);
  border-right: 1px solid var(--border);
  display: flex; flex-direction: column;
  min-height: 0;
}
.new-thread { margin: 12px; }
.thread-filter {
  margin: 0 12px 8px;
  background: var(--bg-elev-2); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 6px 10px; font-size: 13px;
}
.thread-list { list-style: none; margin: 0; padding: 4px 8px; overflow-y: auto; flex: 1; }
.thread-list li {
  border-radius: var(--radius-sm);
  cursor: pointer; color: var(--text); font-size: 13.5px;
  border: 1px solid transparent;
  /* New structure for swipe-to-delete: li is a positioning context
   * for the .swipe-delete button which sits absolutely at the right
   * edge, hidden behind .swipe-content until revealed by swiping. */
  position: relative;
  overflow: hidden;
}
.thread-list .swipe-content {
  /* Holds the title + meta. On desktop it's a regular static block
   * (display: flex column); on touch it can translate left to reveal
   * the delete action below. */
  display: flex; flex-direction: column; gap: 2px;
  padding: 8px 10px;
  background: var(--bg-elev);  /* opaque so it covers the delete button */
  transition: transform 0.18s ease;
  position: relative; z-index: 1;
}
.thread-list li.active .swipe-content {
  background: var(--bg-elev-2);
}
.thread-list li:hover .swipe-content { background: var(--bg-elev-2); }
.thread-list li.active { background: var(--bg-elev-2); border-color: var(--border); }
.thread-list li.swipe-revealed .swipe-content {
  transform: translateX(-88px);
}
.thread-list .swipe-delete {
  /* Hidden on desktop; revealed on touch via @media. Sits absolutely
   * at the right edge of the li. */
  display: none;
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: 88px;
  background: var(--danger);
  color: #fff;
  border: none;
  font-weight: 600; font-size: 13px;
  cursor: pointer;
  z-index: 0;
}
.thread-list .swipe-delete:hover { filter: brightness(1.05); }
.thread-list .t-title { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.thread-list .t-meta { color: var(--muted); font-size: 11.5px; }
.sidebar-foot {
  border-top: 1px solid var(--border); padding: 8px 12px;
  display: flex; flex-direction: column; gap: 6px;
  /* Two-row stack: actions row (Report a bug + Clear all) on top,
     info row (manuals indexed + version) below. The bottom-strip
     alignment JS in app.js measures the resulting height and matches
     the composer's min-height accordingly. */
  min-height: 62px;
  box-sizing: border-box;
}
.sidebar-foot-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  /* Push Clear all to the far right so it sits away from Report a bug —
     reduces the chance of an accidental tap on the destructive action. */
  justify-content: space-between;
}
.sidebar-foot-actions button {
  white-space: nowrap;
}
.sidebar-foot-info {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
  font-size: 11.5px;
}
.sidebar-foot-info > * {
  white-space: nowrap;
}
.sidebar-foot .danger { color: var(--danger); }
.sidebar-foot .danger:hover { background: rgba(255,80,80,.12); }

/* ---------- Chat area ---------- */
.chat {
  display: flex; flex-direction: column;
  min-height: 0;
  position: relative;
}
.chat-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 20px; border-bottom: 1px solid var(--border);
  background: var(--bg-elev);
}
.chat-head h2 { margin: 0; font-size: 15px; font-weight: 600; cursor: pointer; }
.chat-head h2:hover { color: var(--accent-2); }
.head-actions { display: flex; gap: 6px; }

.messages {
  flex: 1; overflow-y: auto;
  padding: 20px 24px; padding-bottom: 12px;
  display: flex; flex-direction: column; gap: 18px;
}
.messages .empty {
  margin: auto; max-width: 480px; color: var(--muted); text-align: center;
}

/* Message bubbles */
.msg { display: flex; gap: 10px; max-width: 88%; position: relative; }
.msg.user { align-self: flex-end; flex-direction: row-reverse; }
.msg .avatar {
  width: 28px; height: 28px; border-radius: 50%;
  display: grid; place-items: center; font-size: 11px; font-weight: 700;
  color: #fff; flex-shrink: 0;
}
.msg.user .avatar { background: var(--bg-elev-3); color: var(--text); }
/* Per-message user-initial avatar (feat-chat-user-initial). Mirrors the
   topbar avatar styling — same accent fill + admin yellow ring. The
   circle in the chat is smaller (28px vs 36px) but uses the same colours
   so it visually ties to the avatar in the topbar. */
.msg.user .avatar.avatar-user-initial {
  background: var(--accent);
  color: #0c1018;
  border: 2px solid transparent;
}
.msg.user .avatar.avatar-user-initial.is-admin { border-color: #d4a838; }
.msg.assistant .avatar { background: var(--accent); }
.msg .bubble {
  background: var(--bg-elev); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 10px 14px;
  word-wrap: break-word; min-width: 0;
}
.msg.user .bubble {
  background: rgba(31,143,77,.12); border-color: rgba(31,143,77,.35);
}
.msg.assistant .bubble.streaming::after {
  content: "▍"; color: var(--accent-2); animation: blink 1s steps(1) infinite;
}

/* Delete-exchange affordance (feat-delete-exchange).
   ✕ icon top-right of assistant bubbles. Hidden by default; opacity:1 on
   hover (desktop) and on touch via JS long-press handler that opens the
   confirm UI directly. Hidden during streaming. */
.bubble-delete-btn {
  position: absolute;
  top: -8px; right: -8px;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--bg-elev-2);
  color: var(--muted);
  border: 1px solid var(--border);
  font-size: 11px;
  line-height: 1;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease;
  z-index: 2;
  padding: 0;
}
.msg.assistant:hover .bubble-delete-btn { opacity: 1; }
.bubble-delete-btn:hover {
  background: var(--danger);
  color: #fff;
  border-color: var(--danger);
}
/* Hide delete during streaming so the user can't ✕ a half-rendered answer */
.msg.assistant:has(.bubble.streaming) .bubble-delete-btn { display: none; }
/* On touch devices, show the ✕ permanently — long-press is the intent path
   but visible affordance is friendlier. Subtle opacity so it doesn't shout. */
@media (hover: none) and (pointer: coarse) {
  .bubble-delete-btn { opacity: 0.5; }
}

/* Inline confirm slot — fixed toast above the composer so it's always
   visible regardless of bubble length or scroll position. The previous
   absolute `bottom: -42px` placement put it at the END of long bubbles,
   forcing mobile users to scroll to find it after a long-press. */
.bubble-delete-confirm {
  position: fixed;
  bottom: calc(var(--composer-height, 80px) + var(--keyboard-offset, 0px) + 16px);
  left: 50%;
  transform: translateX(-50%);
  background: var(--bg-elev-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 8px 12px;
  display: flex;
  gap: 8px;
  align-items: center;
  font-size: 13px;
  z-index: 1000;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5);
  white-space: nowrap;
  max-width: calc(100vw - 24px);
}
.bubble-delete-confirm-text { color: var(--text); }
.bubble-delete-confirm button { padding: 3px 10px !important; font-size: 12px !important; }
.bubble-delete-error {
  margin-left: 8px;
  color: var(--danger);
  font-size: 11px;
}

@keyframes blink { 50% { opacity: 0; } }

/* Markdown bits inside bubbles */
.bubble p { margin: 0 0 8px; }
.bubble p:last-child { margin: 0; }
.bubble ul, .bubble ol { margin: 4px 0 8px; padding-left: 22px; }
.bubble li { margin: 2px 0; }
.bubble code {
  font-family: var(--mono); background: var(--bg-elev-2);
  padding: 1px 5px; border-radius: 4px; font-size: 12.5px;
}
.bubble strong { color: #fff; }

/* Citation superscripts */
.cite-sup {
  display: inline-block; background: var(--bg-elev-3);
  color: var(--accent-2); border: 1px solid var(--border-strong);
  border-radius: 5px; font-size: 10.5px; padding: 1px 5px; margin: 0 1px;
  cursor: pointer; line-height: 1.4; vertical-align: super;
  user-select: none; font-weight: 600;
  text-decoration: none;
}
.cite-sup:hover { background: var(--accent); color: #fff; text-decoration: none; }

/* Labour cards */
.labour-block { margin-top: 10px; padding: 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-elev-2); }
.labour-block h4 { margin: 0 0 6px; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.cost-card { padding: 6px 0; }
.cost-card + .cost-card { border-top: 1px dashed var(--border); }
.op-line strong { color: var(--accent-2); }
.cost-line { font-size: 12.5px; color: var(--muted); margin-top: 2px; }
.cost-line .num { color: var(--text); font-weight: 600; }

/* Inline parts cards */
.part-inline {
  margin-top: 10px; padding: 10px 12px;
  border: 1px solid var(--accent); border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm); background: var(--bg-elev-3);
  font-size: 13px;
}
.part-inline strong { color: var(--accent-2); font-family: var(--mono); font-size: 13.5px; }
.part-inline ul { list-style: none; margin: 6px 0 0; padding: 0; }
.part-inline li { display: flex; justify-content: space-between; gap: 10px; padding: 3px 0; }
.part-inline li a { color: var(--text); text-decoration: none; }
.part-inline li a:hover { text-decoration: underline; }
.part-inline li .muted { color: var(--accent-2); font-weight: 600; white-space: nowrap; }

/* Inline citation thumbnail strip below an assistant answer */
.cite-thumbs {
  display: flex; gap: 8px; margin-top: 12px; padding-top: 10px;
  border-top: 1px dashed var(--border);
  overflow-x: auto; scrollbar-width: thin;
}
.cite-thumb {
  flex: 0 0 auto; width: 110px; padding: 4px;
  background: var(--bg-elev-2); border: 1px solid var(--border);
  border-radius: var(--radius-sm); cursor: pointer;
  display: flex; flex-direction: column; gap: 4px; align-items: stretch;
  font: inherit; color: inherit; text-align: left;
  transition: border-color .15s, transform .15s;
}
.cite-thumb:hover { border-color: var(--accent); transform: translateY(-1px); }
.cite-thumb img {
  width: 100%; height: 130px; object-fit: cover; object-position: top;
  background: #fff; border-radius: 3px;
}
/* Caption shows only the ordinal pill [N] / [P1] — no page number.
 * Per the "no PDF artifacts in the UI" principle (see commit e2cba11),
 * the user shouldn't have to think in source-document-page terms. The
 * lightbox renders the actual page when clicked. */
.cite-thumb-label {
  font-size: 11px; color: var(--muted); display: flex; align-items: center; gap: 4px;
}
.cite-thumb-num {
  display: inline-block; min-width: 16px; padding: 0 4px; height: 16px; line-height: 16px;
  text-align: center; background: var(--accent); color: #fff; border-radius: 8px;
  font-size: 10px; font-weight: 600;
}

/* Prerequisite thumbnails strip — separate from .cite-thumbs so users can
 * tell at a glance which thumbnails are direct evidence and which are
 * prerequisite procedures. The "Prerequisites" label sits above the strip
 * to reinforce the distinction. Same hover/click affordances as cite-thumbs.
 * Caption pill colour differs (see .prereq-thumb-num) so the [P1] in the
 * answer text and the strip below are visually linked. */
.prereq-thumbs {
  margin-top: 12px; padding-top: 10px;
  border-top: 1px dashed var(--border);
}
.prereq-thumbs-label {
  font-size: 11px; color: var(--muted); margin-bottom: 6px;
  text-transform: uppercase; letter-spacing: .04em;
}
.prereq-thumbs-inner {
  display: flex; gap: 8px;
  overflow-x: auto; scrollbar-width: thin;
}
.prereq-thumb-num {
  display: inline-block; min-width: 18px; padding: 0 4px; height: 16px; line-height: 16px;
  text-align: center; background: #f4a020; color: #1a1f2a; border-radius: 8px;
  font-size: 10px; font-weight: 700;
}

/* ---------- Page lightbox ---------- */
.lightbox {
  position: fixed; inset: 0; z-index: 30; display: none;
  background: rgba(0,0,0,.85);
  flex-direction: column;
}
.lightbox[aria-hidden="false"] { display: flex; }
.lightbox-toolbar {
  display: flex; align-items: center; gap: 6px; padding: 6px 12px;
  background: var(--bg-elev); border-bottom: 1px solid var(--border);
}
.lightbox-title {
  flex: 1; color: var(--text); font-size: 13px;
  font-family: var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lightbox-position {
  color: var(--muted); font-size: 12px; font-family: var(--mono);
  padding: 0 8px; flex-shrink: 0;
}
.lightbox-toolbar button { min-width: 32px; padding: 4px 8px; }
.lightbox-stage {
  flex: 1; display: flex; align-items: center; justify-content: center;
  overflow: hidden; cursor: grab; user-select: none; position: relative;
}
.lightbox-stage:active { cursor: grabbing; }
.lightbox-stage img {
  max-width: 96vw; max-height: calc(100vh - 48px);
  background: #fff; box-shadow: 0 8px 30px rgba(0,0,0,.6);
  transform-origin: center center;
  transition: transform .05s linear;
  pointer-events: auto;
}

/* Edge-overlay nav arrows. Positioned by JS to butt right up against the
   left/right edges of the image (which can be much narrower than the stage
   when an A4 portrait page is height-constrained on a wide monitor). */
.lightbox-nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 96px; height: 180px;
  background: rgba(0,0,0,.4); color: #fff;
  border: 1px solid rgba(255,255,255,.22); border-radius: 12px;
  font: 600 64px/1 var(--font); cursor: pointer;
  opacity: .35; transition: opacity .15s, background .15s, transform .12s;
  display: flex; align-items: center; justify-content: center;
  z-index: 2;
}
.lightbox-nav:hover { opacity: 1; background: rgba(0,0,0,.7); transform: translateY(-50%) scale(1.04); }
.lightbox-nav:focus-visible { opacity: 1; outline: 2px solid var(--accent); }
.lightbox-nav:disabled { cursor: default; }
/* JS sets concrete left/right pixel positions on the buttons after each
   image loads. These initial values are just a safe default for the brief
   moment before the load handler fires. */
.lightbox-nav-prev { left: 8vw; }
.lightbox-nav-next { right: 8vw; }
.lightbox-nav[hidden] { display: none; }

.error { color: var(--danger); margin-top: 6px; font-size: 13px; }

/* ---------- Composer ---------- */
.composer {
  display: flex; gap: 10px;
  padding: 10px 16px 14px; border-top: 1px solid var(--border);
  background: var(--bg-elev);
}
.composer textarea {
  flex: 1; resize: none; max-height: 200px; min-height: 38px;
  background: var(--bg-elev-2); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 9px 12px; font-family: var(--font); font-size: 14px; line-height: 1.5;
}
.composer textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; }

/* ---------- Citation drawer ---------- */
.drawer {
  position: fixed; top: 57px; right: 0; bottom: 0; width: 380px;
  background: var(--bg-elev); border-left: 1px solid var(--border);
  transform: translateX(110%); transition: transform .25s ease;
  z-index: 9; display: flex; flex-direction: column; box-shadow: var(--shadow);
}
.drawer[aria-hidden="false"] { transform: translateX(0); }
.drawer-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 14px; border-bottom: 1px solid var(--border);
}
.drawer-head h3 { margin: 0; font-size: 14px; }
.drawer-body { flex: 1; overflow-y: auto; padding: 12px 14px; }
.drawer-card {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg-elev-2); padding: 10px; margin-bottom: 12px;
}
.drawer-card.flash { animation: flash 1.4s ease; }
@keyframes flash {
  0%   { box-shadow: 0 0 0 0 var(--accent); }
  30%  { box-shadow: 0 0 0 3px rgba(52,199,123,.5); }
  100% { box-shadow: 0 0 0 0 rgba(52,199,123,0); }
}
.drawer-card .num {
  display: inline-block; background: var(--accent); color: #fff;
  border-radius: 4px; padding: 1px 6px; font-size: 11px; font-weight: 700;
  margin-right: 6px;
}
.drawer-card .file { color: var(--accent-2); font-family: var(--mono); font-size: 12px; word-break: break-all; }
.drawer-card .heads { color: var(--muted); font-size: 12px; margin: 4px 0; }
.drawer-card img {
  width: 100%; max-height: 240px; object-fit: contain;
  background: #000; border-radius: 4px; margin-top: 6px;
  border: 1px solid var(--border);
}
.drawer-card a.open,
.drawer-card button.open {
  display: inline-block;
  margin-top: 6px;
  font-size: 12px;
  background: none;
  border: 0;
  padding: 0;
  color: var(--accent-2);
  cursor: pointer;
  font-family: inherit;
  text-align: left;
}
.drawer-card button.open:hover { text-decoration: underline; }

/* Buttons that should look and behave like inline text links (used by labour
   page references and parts-mention rows so a click opens the in-app
   lightbox instead of the PDF in a new tab). */
button.linklike {
  background: none;
  border: 0;
  padding: 0;
  color: var(--accent-2);
  cursor: pointer;
  font: inherit;
}
button.linklike:hover { text-decoration: underline; }

/* ---------- Modals ---------- */
.modal {
  position: fixed; inset: 0; z-index: 20;
  background: rgba(0,0,0,.55); display: none;
  align-items: center; justify-content: center; padding: 20px;
}
.modal[aria-hidden="false"] { display: flex; }
.modal-card {
  background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius-lg);
  width: min(720px, 100%); max-height: 90vh; overflow-y: auto;
  padding: 16px 18px; box-shadow: var(--shadow);
}
.modal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.modal-head h3 { margin: 0; font-size: 15px; }
.modal-sub { margin: 14px 0 6px; font-size: 13px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }

/* Search bars (in modals) */
.searchbar { display: flex; gap: 8px; align-items: stretch; margin-bottom: 4px; }
.searchbar input {
  flex: 1; background: var(--bg-elev-2); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 8px 12px; font-family: var(--font); font-size: 14px;
}
.searchbar input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }

/* Settings form */
.settings-form { display: grid; gap: 10px; grid-template-columns: 1fr 1fr; }
.settings-form label { display: flex; flex-direction: column; gap: 4px; font-size: 12.5px; color: var(--muted); }
.settings-form input {
  background: var(--bg-elev-2); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: 6px 10px; font-family: var(--font); font-size: 13.5px;
}
.settings-form .primary { grid-column: 1 / -1; justify-self: start; }
.settings-form .hint { grid-column: 1 / -1; color: var(--accent-2); font-size: 12px; }
.manual-list { list-style: none; margin: 0; padding: 0; }
.manual-list li { display: flex; justify-content: space-between; gap: 8px; padding: 4px 0; border-bottom: 1px solid var(--border); font-size: 13px; }

/* Parts grid */
.parts-grid { display: grid; gap: 12px; grid-template-columns: 1fr; margin-top: 12px; }
.supplier-card { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px; background: var(--bg-elev-2); }
.supplier-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
.supplier-head h3 { margin: 0; font-size: 13px; }
.supplier-list { list-style: none; margin: 0; padding: 0; }
.supplier-item { display: flex; justify-content: space-between; padding: 4px 0; font-size: 13px; gap: 10px; }
.supplier-item .price { color: var(--highlight); font-family: var(--mono); }
.supplier-foot { margin-top: 6px; font-size: 12px; }

.mentions { margin-top: 6px; font-size: 13px; }
.mentions ul { list-style: none; margin: 0; padding: 0; }
.mentions li { padding: 4px 0; border-bottom: 1px dashed var(--border); }

/* ===== Bug report modal + per-message report button ===== */
.bug-report-form {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.bug-report-label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 13px;
  color: var(--text);
}
.bug-report-label select,
.bug-report-label textarea {
  background: var(--bg-elev-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  font-family: var(--font);
  font-size: 14px;
  resize: vertical;
}
.bug-report-label textarea {
  min-height: 100px;
  max-height: 300px;
}
.bug-report-hint {
  margin-top: 2px;
}
.bug-report-context > summary {
  cursor: pointer;
  padding: 4px 0;
  list-style: none;
}
.bug-report-context > summary::before {
  content: "▸ ";
  display: inline-block;
  margin-right: 4px;
}
.bug-report-context[open] > summary::before {
  content: "▾ ";
}
.bug-report-context-list {
  margin: 8px 0 0;
  padding: 8px 12px;
  background: var(--bg-elev-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-family: var(--mono);
  font-size: 11.5px;
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 4px 12px;
}
.bug-report-context-list dt {
  color: var(--muted);
  font-weight: 600;
}
.bug-report-context-list dd {
  margin: 0;
  word-break: break-word;
  color: var(--text);
}
.bug-report-disclosure {
  margin: 0;
  padding: 8px 10px;
  background: rgba(255, 255, 255, 0.03);
  border-left: 3px solid var(--accent-2, #888);
  border-radius: 0 4px 4px 0;
  font-size: 12px;
  line-height: 1.4;
}
.bug-report-error {
  padding: 8px 10px;
  background: rgba(180, 60, 60, 0.15);
  border: 1px solid rgba(180, 60, 60, 0.4);
  border-radius: var(--radius-sm);
  color: #f5c2c2;
  font-size: 13px;
}
.bug-report-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 8px;
}

/* Per-message 🐞 button — sits in the existing feedback bar on each
   assistant bubble, alongside thumbs up/down. */
.msg-bug-report {
  background: transparent;
  border: 1px solid transparent;
  border-radius: 4px;
  padding: 2px 6px;
  cursor: pointer;
  font-size: 12px;
  color: var(--muted);
}
.msg-bug-report:hover {
  background: rgba(255, 255, 255, 0.06);
  color: var(--text);
  border-color: var(--border);
}

/* Admin bug-reports page styles */
.admin-filter-form {
  margin-bottom: 12px;
}
.admin-filter-form label {
  font-size: 13px;
  color: var(--muted);
}
.bug-desc {
  max-width: 400px;
}
.badge {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 10px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
}
.badge-ui            { background: #15263a; color: #cfe1f8; }
.badge-wrong_answer  { background: #3a1818; color: #f5c2c2; }
.badge-performance   { background: #3a2e15; color: #f3d8a8; }
.badge-other         { background: #2a2a2a; color: #cccccc; }
.badge-status-new       { background: #2a4870; color: #cfe1f8; }
.badge-status-triaged   { background: #846020; color: #f3d8a8; }
.badge-status-fixed     { background: #2a6e44; color: #c9e8d5; }
.badge-status-wont_fix  { background: #555555; color: #cccccc; }
.badge-status-duplicate { background: #4a2e5a; color: #d8c2e8; }
.bug-detail-dialog {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-elev);
  color: var(--text);
  padding: 20px;
  max-width: 800px;
  width: 90vw;
}
.bug-detail-dialog::backdrop {
  background: rgba(0, 0, 0, 0.6);
}
.bug-detail-pre {
  background: var(--bg-elev-2);
  padding: 8px 10px;
  border-radius: 4px;
  white-space: pre-wrap;
  word-break: break-word;
  font-family: var(--mono);
  font-size: 12px;
  max-height: 200px;
  overflow-y: auto;
}
.bug-detail-meta {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 4px 12px;
  font-size: 13px;
}
.bug-detail-meta dt { color: var(--muted); font-weight: 600; }
.bug-detail-meta dd { margin: 0; }
.bug-detail-update {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}
.bug-detail-update label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 13px;
  color: var(--muted);
}
.bug-detail-update select,
.bug-detail-update textarea {
  background: var(--bg-elev-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 6px 10px;
  font-family: var(--font);
  font-size: 13px;
}

/* ===== Messages page (feat-messages) ===== */
body.messages-body {
  display: flex;
  flex-direction: column;
  /* Tall layout — the composer is position:fixed on mobile so body height
     doesn't have to be exact. Just give it enough vertical space for the
     two-pane desktop layout. */
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  margin: 0;
  background: var(--bg);
  color: var(--text);
}
.brand-link { text-decoration: none; color: inherit; }
.brand-link:hover .brand-text h1 { color: var(--accent); }

.messages-shell {
  display: grid;
  grid-template-columns: 360px 1fr;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}

.messages-list {
  border-right: 1px solid var(--border);
  background: var(--bg-elev);
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}
.messages-list-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
  background: var(--bg-elev-2);
}
.messages-list-head h2 { margin: 0; font-size: 15px; }
.messages-list-body {
  flex: 1;
  overflow-y: auto;
}
.messages-empty { padding: 24px 16px; text-align: center; }

.messages-list-item {
  display: block;
  width: 100%;
  text-align: left;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--border);
  color: var(--text);
  font-family: inherit;
  /* Positioning context for the swipe-delete button which sits absolutely
     at the right edge, hidden behind .swipe-content until revealed by
     swiping (touch only). */
  position: relative;
  overflow: hidden;
  padding: 0;
}
.messages-list-item .swipe-content {
  display: block;
  width: 100%;
  text-align: left;
  background: var(--bg-elev);
  padding: 12px 14px;
  cursor: pointer;
  color: inherit;
  /* Slides left to reveal the delete action on swipe. */
  transition: transform 0.18s ease;
  position: relative; z-index: 1;
  box-sizing: border-box;
}
.messages-list-item .swipe-content:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.messages-list-item:hover .swipe-content { background: var(--bg-elev-2); }
.messages-list-item.is-current .swipe-content {
  background: var(--bg-elev-3);
  border-left: 3px solid var(--accent);
  padding-left: 11px;
}
.messages-list-item.swipe-revealed .swipe-content {
  transform: translateX(-88px);
}
.messages-list-item .swipe-delete {
  /* Hidden on desktop; revealed on touch via @media. Sits absolutely
     at the right edge of the item. */
  display: none;
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: 88px;
  background: var(--danger);
  color: #fff;
  border: none;
  font-weight: 600; font-size: 13px;
  cursor: pointer;
  z-index: 0;
}
.messages-list-item .swipe-delete:hover { filter: brightness(1.05); }
.messages-list-item.is-unread .messages-list-item-subject { font-weight: 700; }
.messages-list-item-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 4px;
}
.messages-list-item-subject {
  font-size: 14px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 260px;
}
.messages-list-item-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: #c14242;
  margin-left: 6px;
  flex-shrink: 0;
}
.messages-list-item-meta {
  display: flex;
  gap: 6px;
  align-items: center;
  font-size: 11px;
  margin-bottom: 4px;
  flex-wrap: wrap;
}
.messages-list-item-preview {
  font-size: 12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.messages-detail {
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  padding: 0;
}
.messages-detail-empty {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 24px;
}
.messages-detail-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 18px;
  border-bottom: 1px solid var(--border);
  background: var(--bg-elev);
  flex-shrink: 0;
}
.messages-detail-head h2 { margin: 0; font-size: 16px; }
.messages-back-btn { display: none; }  /* desktop hides it; mobile @media shows */
.messages-detail-actions { display: flex; gap: 8px; align-items: center; }
.messages-snapshot {
  border-bottom: 1px solid var(--border);
  padding: 8px 18px;
  background: var(--bg-elev-2);
}
.messages-snapshot summary { cursor: pointer; padding: 4px 0; }
.messages-snapshot-section h4 {
  margin: 8px 0 4px; font-size: 11px; text-transform: uppercase;
  color: var(--muted); letter-spacing: 0.04em;
}
.messages-snapshot-section pre {
  background: var(--bg-elev-3);
  padding: 8px 10px;
  border-radius: 4px;
  font-size: 12px;
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 150px;
  overflow-y: auto;
  margin: 0;
}

.messages-thread {
  flex: 1;
  overflow-y: auto;
  padding: 14px 18px;
}
.msg-row {
  display: flex;
  margin-bottom: 12px;
}
.msg-row-user { justify-content: flex-end; }
.msg-row-admin { justify-content: flex-start; }
.msg-row-system {
  justify-content: center;
  font-style: italic;
}
.msg-bubble {
  /* max-width is on .msg-bubble-wrap so the reactions-row sibling
     stays aligned to the bubble's edge (not the row's edge). */
  padding: 8px 12px;
  border-radius: 12px;
  background: var(--bg-elev-2);
  border: 1px solid var(--border);
}
.msg-bubble-wrap {
  display: flex;
  flex-direction: column;
  max-width: 70%;
}
.msg-row-user .msg-bubble-wrap { align-items: flex-end; }
.msg-row-admin .msg-bubble-wrap { align-items: flex-start; }
.msg-row-user .msg-bubble {
  background: var(--accent-soft, #2a4870);
  border-color: var(--accent, #5298d8);
}
.msg-row-admin .msg-bubble {
  background: var(--bg-elev-2);
}
.msg-bubble-meta {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-bottom: 4px;
  font-size: 11px;
}
.msg-bubble-sender { font-weight: 600; color: var(--text); }
.msg-bubble-body {
  font-size: 14px;
  line-height: 1.4;
  white-space: pre-wrap;
  word-break: break-word;
}

/* Reaction chips + picker (feat-message-emote-reactions) */
.msg-bubble {
  position: relative;
}
.msg-react-btn {
  /* Small chip that hangs off the bottom-right of the bubble — like
     WhatsApp / Slack. Subtle until hovered/focused. */
  position: absolute;
  bottom: -8px;
  right: -8px;
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 999px;
  width: 20px;
  height: 20px;
  font-size: 11px;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.12s;
  padding: 0;
  color: var(--text);
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.msg-bubble:hover .msg-react-btn,
.msg-react-btn:focus { opacity: 1; }
@media (hover: none) and (pointer: coarse) {
  /* Mobile — always visible (subtle) since no hover. */
  .msg-react-btn { opacity: 0.55; }
}
.reactions-row {
  /* Tucked under the bubble, slightly overlapping the bubble's bottom
     edge so they appear visually attached. Sits at the chip's natural
     compact height — not stretched to bubble height. */
  display: inline-flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: -6px;
  padding: 0 4px;
  position: relative;
  z-index: 1;
}
.msg-row-user .reactions-row { justify-content: flex-end; }
.reaction-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  background: var(--bg-elev-2);
  border: 1px solid var(--border);
  border-radius: 12px;
  cursor: pointer;
  font-size: 12px;
  color: var(--text);
  line-height: 1.2;
  user-select: none;
}
.reaction-chip:hover { background: var(--bg-elev-3); }
.reaction-chip.is-mine {
  background: var(--accent-soft, #2a4870);
  border-color: var(--accent, #5298d8);
}
.reaction-chip-emote { font-size: 14px; }
.reaction-chip-count { font-size: 11px; opacity: 0.8; }

.reaction-picker {
  position: fixed;
  z-index: 100;
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
  max-width: 260px;
}
.reaction-picker-row {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
}
.reaction-picker-emote {
  background: transparent;
  border: 0;
  font-size: 18px;
  width: 28px;
  height: 28px;
  border-radius: 6px;
  cursor: pointer;
  padding: 0;
  line-height: 1;
}
.reaction-picker-emote:hover { background: var(--bg-elev-2); }
.reaction-picker-more {
  margin-top: 6px;
  border-top: 1px solid var(--border);
  padding-top: 6px;
}
.reaction-picker-more summary {
  cursor: pointer;
  font-size: 11px;
  color: var(--muted);
  padding: 2px 4px;
}

.messages-composer {
  display: flex;
  gap: 8px;
  align-items: flex-end;
  padding: 12px 18px;
  /* Push the composer above the iOS home indicator. env(safe-area-inset-bottom)
     is 0 on devices without one (iPhone SE, Android), 34px on iPhone Pro. */
  padding-bottom: max(12px, env(safe-area-inset-bottom));
  border-top: 1px solid var(--border);
  background: var(--bg-elev);
  flex-shrink: 0;
}
.messages-composer textarea {
  flex: 1;
  background: var(--bg-elev-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: 10px 16px;
  font-family: inherit;
  font-size: 14px;
  resize: none;
  /* Single-line by default; auto-grows via JS as user types newlines (or
     manually via Shift+Enter). max-height clamps to 5 visual lines so the
     composer never eats the thread above it. */
  min-height: 40px;
  max-height: 140px;
  line-height: 1.4;
  overflow-y: auto;
}
.messages-composer button[type="submit"] {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  padding: 0;
  border-radius: 50%;
  background: var(--accent, #5298d8);
  color: #fff;
  border: none;
  font-size: 18px;
  font-weight: 600;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: filter 0.12s ease, transform 0.12s ease;
}
.messages-composer button[type="submit"]:hover { filter: brightness(1.1); }
.messages-composer button[type="submit"]:active { transform: scale(0.94); }
.messages-composer button[type="submit"]:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.messages-composer-locked {
  padding: 16px 18px;
  padding-bottom: max(16px, env(safe-area-inset-bottom));
  border-top: 1px solid var(--border);
  background: var(--bg-elev);
  text-align: center;
  flex-shrink: 0;
}

.msg-pill {
  display: inline-flex; align-items: center;
  padding: 2px 8px;
  border-radius: 10px;
  font-size: 11px;
  font-weight: 500;
}
.msg-pill-open    { background: #15263a; color: #cfe1f8; }
.msg-pill-closed  { background: #2a2a2a; color: #cccccc; }
.msg-pill-locked  { background: #3a2818; color: #f3c8a8; }
.msg-source-pill {
  display: inline-flex; align-items: center;
  padding: 2px 8px; border-radius: 10px;
  background: #1f2a3d;
  color: #c5d8ee;
  font-size: 11px;
}

/* Mobile messages page: list-then-detail (single column, hide list when detail shown) */

/* ===== Admin /admin/messages page (two-pane within admin layout) ===== */
.admin-messages-shell {
  display: grid;
  grid-template-columns: 320px 1fr;
  gap: 16px;
  height: calc(100vh - 160px);
  min-height: 500px;
}
.admin-messages-list {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-elev);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.admin-messages-list-head {
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  background: var(--bg-elev-2);
}
.admin-messages-list-head h2 { margin: 0 0 8px; font-size: 14px; }
.admin-messages-filters {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.admin-messages-filters select,
.admin-messages-filters input {
  background: var(--bg-elev-3);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 4px 8px;
  font-size: 12px;
}
.admin-messages-list-body {
  flex: 1;
  overflow-y: auto;
}
.admin-msg-item {
  display: block;
  width: 100%;
  text-align: left;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--border);
  padding: 10px 12px;
  cursor: pointer;
  color: var(--text);
  font-family: inherit;
}
.admin-msg-item:hover { background: var(--bg-elev-2); }
.admin-msg-item.is-current {
  background: var(--bg-elev-3);
  border-left: 3px solid var(--accent);
  padding-left: 9px;
}
.admin-msg-item.is-unread .admin-msg-item-user,
.admin-msg-item.is-unread .admin-msg-item-subject { font-weight: 700; }
.admin-msg-item-row1 {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 4px;
}
.admin-msg-item-user { font-size: 12px; flex: 1; }
.admin-msg-item-time { margin-left: auto; }
.admin-msg-item-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: #c14242;
}
.admin-msg-item-subject {
  font-size: 13px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-bottom: 4px;
}
.admin-msg-item-meta {
  display: flex;
  gap: 6px;
  align-items: center;
  font-size: 11px;
}
.admin-messages-detail {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-elev);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  min-height: 0;
}
.admin-msg-show-deleted {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  color: var(--muted);
  cursor: pointer;
}
.admin-msg-show-deleted input { margin: 0; }
.admin-msg-item.is-deleted { opacity: 0.55; }
.admin-msg-item.is-deleted .admin-msg-item-subject { text-decoration: line-through; }
.admin-msg-deleted-tag {
  display: inline-block;
  padding: 1px 6px;
  background: #2a2a2a;
  color: #aaa;
  font-size: 10px;
  border-radius: 8px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.admin-msg-deleted-banner {
  padding: 8px 14px;
  background: rgba(180, 60, 60, 0.12);
  border-bottom: 1px solid rgba(180, 60, 60, 0.3);
  border-radius: 0;
  color: #f5c2c2;
}

/* Mobile back-button shown ONLY on phone-class viewports when a
   conversation is open (admin-messages-mobile-ux todo). Hidden on
   desktop because the side-by-side layout doesn't need it. */
.admin-msg-back-btn {
  display: none;
  background: transparent;
  border: 0;
  color: var(--accent);
  font-size: 14px;
  padding: 4px 8px;
  cursor: pointer;
}
.admin-msg-back-btn::before { content: "← "; }

/* Mobile: list-then-detail single-column layout. Same pattern as the
   user-side /messages page. When the shell has data-detail-open="1",
   the list collapses + the detail expands fullwidth.

   Threshold matches the existing mobile media query at 800px so
   touch + small-laptop viewports both get the swap. */
@media (max-width: 800px) {
  .admin-messages-shell {
    grid-template-columns: 1fr;
    height: calc(100vh - 140px);
  }
  .admin-messages-shell[data-detail-open="1"] .admin-messages-list {
    display: none;
  }
  .admin-messages-shell:not([data-detail-open="1"]) .admin-messages-detail {
    display: none;
  }
  .admin-msg-back-btn {
    display: inline-block;
  }
  /* Tighter detail header on mobile so the actions wrap usefully */
  .admin-messages-detail .messages-detail-head {
    flex-wrap: wrap;
    gap: 6px;
  }
}

/* ===== Notifications page (bell-revival) ===== */
.notif-shell {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 16px 18px;
  max-width: 800px;
  margin: 0 auto;
  width: 100%;
  box-sizing: border-box;
}
.notif-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
  gap: 8px;
  flex-wrap: wrap;
}
.notif-head h2 {
  margin: 0;
  font-size: 18px;
}
.notif-head-actions {
  display: flex;
  gap: 6px;
  align-items: center;
}
.notif-list {
  flex: 1;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-bottom: max(12px, env(safe-area-inset-bottom));
}
.notif-empty {
  padding: 24px 16px;
  text-align: center;
}
.notif-item {
  /* Swipe-to-delete: positioning context for the .swipe-delete button
     which sits absolutely at the right edge, hidden behind .swipe-content
     until revealed by swiping (touch only). */
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--bg-elev);
  transition: opacity 0.15s ease;
}
.notif-item.is-read { opacity: 0.55; }
.notif-item .swipe-content {
  display: flex;
  gap: 10px;
  padding: 12px 14px;
  background: var(--bg-elev);
  align-items: flex-start;
  transition: transform 0.18s ease;
  position: relative;
  z-index: 1;
}
.notif-item.swipe-revealed .swipe-content {
  transform: translateX(-88px);
}
.notif-item .swipe-delete {
  /* Hidden on desktop (revealed via @media on touch). Sits absolutely
     at the right edge of the item. */
  display: none;
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: 88px;
  background: var(--danger);
  color: #fff;
  border: none;
  font-weight: 600; font-size: 13px;
  cursor: pointer;
  z-index: 0;
}
.notif-item .swipe-delete:hover { filter: brightness(1.05); }
.notif-item-stripe {
  width: 3px;
  align-self: stretch;
  border-radius: 2px;
  flex-shrink: 0;
}
.notif-item.notif-info     .notif-item-stripe { background: #2a4870; }
.notif-item.notif-success  .notif-item-stripe { background: #2a6e44; }
.notif-item.notif-warn     .notif-item-stripe { background: #846020; }
.notif-item.notif-critical .notif-item-stripe { background: #8a3030; }
.notif-item-content {
  flex: 1;
  min-width: 0;
}
.notif-item-body {
  font-size: 14px;
  line-height: 1.4;
  color: var(--text);
  word-wrap: break-word;
}
.notif-item-meta {
  display: flex;
  gap: 10px;
  align-items: center;
  font-size: 11px;
  margin-top: 6px;
}
.notif-item-actions {
  display: flex;
  gap: 4px;
  align-items: center;
  flex-shrink: 0;
  align-self: center;
}
.notif-item-mark {
  flex-shrink: 0;
}
.notif-item-delete {
  flex-shrink: 0;
  padding: 4px 10px !important;
  color: var(--danger);
  font-size: 14px !important;
}
.notif-item-delete:hover {
  background: rgba(255, 80, 80, 0.12);
}
.notif-pill {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 10px;
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
}
.notif-pill-critical { background: #3a1818; color: #f5c2c2; }

/* ---------- Responsive ---------- */
@media (max-width: 800px) {
  .app-shell { grid-template-columns: 1fr; }
  .sidebar { display: none; }
  .drawer { width: 100%; }
}


/* ---------- Ingest progress banner ---------- */
.ingest-banner {
  background: linear-gradient(90deg, rgba(184,49,47,.18), rgba(184,49,47,.06));
  border-bottom: 1px solid var(--border);
  padding: 8px 18px;
}
.ingest-banner-inner {
  display: flex; align-items: center; gap: 12px;
  font-size: 12.5px; color: var(--text);
}
.ingest-spinner {
  width: 12px; height: 12px; border-radius: 50%;
  border: 2px solid var(--accent);
  border-top-color: transparent;
  animation: spin 0.9s linear infinite;
  display: inline-block;
}
@keyframes spin { to { transform: rotate(360deg); } }
.ingest-banner .ingest-bar {
  flex: 1; height: 6px; background: var(--bg-elev-2);
  border-radius: 3px; overflow: hidden; max-width: 320px;
}
.ingest-banner .ingest-bar-fill {
  height: 100%; width: 0%; background: var(--accent);
  transition: width 0.3s ease;
}
.ingest-pct { color: var(--accent-2); font-weight: 600; min-width: 40px; text-align: right; }

/* ==========================================================================
   Auth pages (login.html, signup.html) — server-rendered by auth_routes.py.
   Stage 2 auth model: invite codes + bootstrap admin pin.
   ========================================================================== */

.auth-body {
  min-height: 100vh;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  color: var(--fg);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  padding: 24px;
}

.auth-card {
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 32px 28px 24px;
  max-width: 420px;
  width: 100%;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}

.auth-title {
  font-size: 1.15rem;
  font-weight: 600;
  margin: 0 0 4px;
  color: var(--accent);
  letter-spacing: 0.01em;
}

.auth-subtitle {
  font-size: 1.4rem;
  font-weight: 600;
  margin: 0 0 20px;
}

.auth-form {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.auth-form label {
  font-size: 0.85rem;
  color: var(--fg-mute);
  margin-top: 8px;
  font-weight: 500;
}

.auth-form input {
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 10px 12px;
  font-size: 0.95rem;
  font-family: inherit;
  width: 100%;
  box-sizing: border-box;
  transition: border-color 0.15s;
}

.auth-form input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(138, 180, 248, 0.18);
}

.auth-form button {
  margin-top: 18px;
  background: var(--accent);
  color: #0c1018;
  border: 0;
  border-radius: 6px;
  padding: 11px;
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
  transition: filter 0.15s;
}

.auth-form button:hover { filter: brightness(1.08); }
.auth-form button:active { filter: brightness(0.95); }

.auth-error {
  background: rgba(244, 67, 54, 0.12);
  border: 1px solid rgba(244, 67, 54, 0.4);
  color: #ff8a80;
  padding: 10px 12px;
  border-radius: 6px;
  margin-bottom: 14px;
  font-size: 0.9rem;
}

.auth-flash {
  background: rgba(76, 175, 80, 0.10);
  border: 1px solid rgba(76, 175, 80, 0.35);
  color: #a5d6a7;
  padding: 10px 12px;
  border-radius: 6px;
  margin-bottom: 14px;
  font-size: 0.9rem;
}

/* AI safety disclosure on signup. Amber/warning palette so it stands out
   from the form fields without screaming. The checkbox label resets a few
   .auth-form label inheritances (uppercase, font-size) that would otherwise
   make the wording hard to read. */
.auth-disclosure {
  background: rgba(255, 165, 0, 0.07);
  border: 1px solid rgba(255, 165, 0, 0.35);
  border-radius: 6px;
  padding: 12px 14px;
  margin: 18px 0 4px;
  font-size: 0.85rem;
  line-height: 1.45;
  color: var(--fg);
}
.auth-disclosure strong { color: #ffb74d; }
.auth-disclosure p { margin: 6px 0; }
.auth-disclosure-check {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin-top: 10px;
  font-weight: 500;
  cursor: pointer;
  font-size: 0.85rem;
  color: var(--fg);
  text-transform: none;
}
.auth-disclosure-check input[type="checkbox"] {
  margin-top: 2px;
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

/* Support-link prose ('Need help / WhatsApp →') on auth pages. Slightly
   muted but still discoverable; the embedded <a> uses the global a colour. */
.auth-alt.support-link {
  color: var(--fg-mute);
  font-size: 0.85rem;
  margin-top: 14px;
}

/* Compact AI safety banner shown at the top of an empty conversation in
   the chat UI. Disappears once the user sends their first message. */
.ai-safety-banner {
  margin: 0 0 16px;
  padding: 10px 14px;
  background: rgba(255, 165, 0, 0.08);
  border: 1px solid rgba(255, 165, 0, 0.35);
  border-radius: 8px;
  color: var(--text);
  font-size: 0.85rem;
  line-height: 1.45;
  display: flex;
  gap: 10px;
  align-items: flex-start;
}
.ai-safety-banner .icon {
  color: #ffb74d;
  font-size: 1rem;
  line-height: 1;
}

/* ---------- Empty-state onboarding (starter prompts + tips card) ---------- */
/* Sits inside .messages .empty when the thread has no messages.
   Goal: zero-friction first question, examples of good phrasing,
   discoverable /help. See app.js STARTER_PROMPTS + buildTipsCardHtml. */
.empty-headline {
  font-size: 1.1rem;
  font-weight: 600;
  color: var(--text);
  margin: 0 0 6px;
}
.empty-sub {
  margin: 0 0 18px;
  color: var(--muted);
  font-size: 0.92rem;
}
.starter-prompts {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 18px;
  text-align: left;
}
.starter-prompt {
  text-align: left;
  padding: 10px 14px;
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-family: inherit;
  font-size: 0.92rem;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}
.starter-prompt:hover {
  background: var(--bg-elev-2);
  border-color: var(--accent);
}
.starter-prompt:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.onboarding-tips {
  text-align: left;
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px 14px;
  margin-top: 4px;
  font-size: 0.88rem;
  color: var(--text);
  line-height: 1.5;
}
.onboarding-tips-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}
.onboarding-tips-icon {
  font-size: 1rem;
}
.onboarding-tips-dismiss {
  margin-left: auto;
  background: transparent;
  border: 0;
  color: var(--muted);
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
  padding: 2px 6px;
  border-radius: 4px;
}
.onboarding-tips-dismiss:hover {
  background: var(--bg-elev-2);
  color: var(--text);
}
.onboarding-tips ul {
  margin: 6px 0;
  padding-left: 20px;
}
.onboarding-tips li {
  margin: 4px 0;
}

/* ---------- Notifications: top banner (warn/critical) + corner toast (info/success) ---------- */
#notifBannerHost {
  position: sticky;
  top: 0;
  z-index: 30;
}
.notif-banner {
  width: 100%;
  border-bottom: 1px solid var(--border);
  font-size: 0.9rem;
  line-height: 1.4;
  animation: notif-slide-down 220ms ease-out;
}

/* Admin-only "Preview as user" mode banner.
   IMPORTANT (mobile layout safety, see PROJECT_CONTEXT.md):
   The element is a STATIC child of <body> in index.html so it exists at
   script-load time, before syncComposerHeight() runs. Visibility is
   controlled by the [hidden] HTML attribute (display: none → 0 layout
   space). When toggled, the JS re-fires syncComposerHeight + the snap
   loop so the brute-force messages.style.maxHeight cap is recomputed
   against the new messages.gBCR.top.
   Distinct amber palette so it can't be confused with admin-pushed
   warning notifications below it. */
.preview-banner {
  width: 100%;
  background: #f59e0b;
  color: #1f2937;
  border-bottom: 1px solid #b45309;
  font-size: 0.9rem;
  line-height: 1.4;
  /* NOT position: sticky — sticky created subtle layout interactions
     with the position:fixed body lockdown on iOS Chrome. Plain in-flow
     positioning is enough since we re-fire syncComposerHeight on toggle. */
  animation: notif-slide-down 220ms ease-out;
  flex-shrink: 0;  /* never collapse if body is tight on vertical space */
}
.preview-banner[hidden] { display: none; }
.preview-banner-inner {
  max-width: 1100px;
  margin: 0 auto;
  padding: 8px 18px;
  display: flex;
  align-items: center;
  gap: 14px;
}
.preview-banner-body { flex: 1 1 auto; }
.preview-banner-action {
  background: rgba(0, 0, 0, 0.15);
  color: inherit;
  border: 1px solid rgba(0, 0, 0, 0.3);
  border-radius: 6px;
  padding: 4px 12px;
  cursor: pointer;
  font-weight: 600;
  font-size: 0.85rem;
}
.preview-banner-action:hover { background: rgba(0, 0, 0, 0.25); }
/* Impersonation banner — feat-admin-impersonate-user.
   Distinct red palette so it CANNOT be confused with the preview-mode
   banner (amber). The admin must always see this when impersonating —
   it's the failsafe against the "forgot to stop" footgun.
   Same layout primitives as .preview-banner so the mobile composer
   syncComposerHeight() math handles the height shift identically. */
.impersonation-banner {
  width: 100%;
  background: #dc2626;
  color: #fff;
  border-bottom: 1px solid #7f1d1d;
  font-size: 0.92rem;
  line-height: 1.4;
  animation: notif-slide-down 220ms ease-out;
  flex-shrink: 0;
}
.impersonation-banner[hidden] { display: none; }
.impersonation-banner-inner {
  max-width: 1100px;
  margin: 0 auto;
  padding: 9px 18px;
  display: flex;
  align-items: center;
  gap: 14px;
}
.impersonation-banner-body { flex: 1 1 auto; }
.impersonation-banner-action {
  background: rgba(0, 0, 0, 0.25);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.5);
  border-radius: 6px;
  padding: 5px 14px;
  cursor: pointer;
  font-weight: 700;
  font-size: 0.88rem;
}
.impersonation-banner-action:hover { background: rgba(0, 0, 0, 0.4); }

/* Impersonation user-search modal (avatar-menu entry path).
   Reuses the .modal/.modal-card primitives where possible. */
#impersonateModal .impersonate-search-input {
  width: 100%;
  padding: 8px 12px;
  background: var(--bg-elev-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 0.95rem;
}
#impersonateModal .impersonate-results {
  max-height: 320px;
  overflow-y: auto;
  margin-top: 12px;
  border: 1px solid var(--border);
  border-radius: 6px;
}
#impersonateModal .impersonate-result-row {
  padding: 10px 12px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
}
#impersonateModal .impersonate-result-row:last-child { border-bottom: none; }
#impersonateModal .impersonate-result-row:hover { background: var(--bg-elev-2); }
#impersonateModal .impersonate-result-username { font-weight: 600; }
#impersonateModal .impersonate-result-email { color: var(--muted); font-size: 0.85rem; }
#impersonateModal .impersonate-empty {
  padding: 18px;
  color: var(--muted);
  text-align: center;
  font-size: 0.9rem;
}
.notif-banner-inner {
  max-width: 1100px;
  margin: 0 auto;
  padding: 10px 18px;
  display: flex;
  align-items: center;
  gap: 14px;
}
.notif-body {
  flex: 1 1 auto;
}
.notif-action {
  background: rgba(255, 255, 255, 0.18);
  color: inherit;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 6px;
  padding: 4px 12px;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  flex-shrink: 0;
}
.notif-action:hover { background: rgba(255, 255, 255, 0.28); }
.notif-dismiss {
  background: none;
  border: 0;
  color: inherit;
  font-size: 1.05rem;
  line-height: 1;
  cursor: pointer;
  opacity: 0.65;
  padding: 4px 6px;
  flex-shrink: 0;
}
.notif-dismiss:hover { opacity: 1; }

/* Corner toast stack */
#notifToastHost {
  position: fixed;
  bottom: 16px;
  right: 16px;
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-width: 380px;
  pointer-events: none;
}
#notifToastHost .notif-toast { pointer-events: auto; }
.notif-toast {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 14px;
  border-radius: 8px;
  font-size: 0.9rem;
  line-height: 1.4;
  box-shadow: 0 4px 16px rgba(0,0,0,0.4);
  animation: notif-slide-up 240ms ease-out;
  transition: opacity 600ms ease, transform 600ms ease;
}
.notif-toast.fade-out {
  opacity: 0;
  transform: translateY(8px);
}

/* Severity colour scheme — banner and toast share styles */
.notif-info     { background: #15263a; color: #cfe1f8; border: 1px solid #2a4870; }
.notif-success  { background: #163a23; color: #c9e8d5; border: 1px solid #2a6e44; }
.notif-warn     { background: #3a2e15; color: #f3d8a8; border: 1px solid #846020; }
.notif-critical { background: #3a1818; color: #f5c2c2; border: 1px solid #8a3030; }

@keyframes notif-slide-down {
  from { transform: translateY(-100%); opacity: 0.4; }
  to   { transform: translateY(0);     opacity: 1; }
}
@keyframes notif-slide-up {
  from { transform: translateY(20%); opacity: 0; }
  to   { transform: translateY(0);   opacity: 1; }
}

/* Admin notification form: checkbox rows + severity badges */
.checkbox-row {
  display: flex !important;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  text-transform: none !important;
  font-size: 0.9rem !important;
}
.checkbox-row input[type="checkbox"] {
  width: 18px;
  height: 18px;
  accent-color: var(--accent);
  cursor: pointer;
}
.sev {
  display: inline-block;
  padding: 1px 8px;
  border-radius: 10px;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
}
.sev-info     { background: #15263a; color: #cfe1f8; }
.sev-success  { background: #163a23; color: #c9e8d5; }
.sev-warn     { background: #3a2e15; color: #f3d8a8; }
.sev-critical { background: #3a1818; color: #f5c2c2; }

/* ---------- Feedback bar (👍 / 👎 under each assistant bubble) ---------- */
.feedback-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 12px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
  font-size: 0.85rem;
  color: var(--muted);
  flex-wrap: wrap;
}
.fb-btn {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 4px 10px;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  color: var(--text);
  transition: background 100ms, border-color 100ms;
}
.fb-btn:hover { background: var(--bg-elev-2); }
.fb-btn.active.fb-up   { background: rgba(76, 175, 80, 0.18); border-color: #2a6e44; }
.fb-btn.active.fb-down { background: rgba(244, 67, 54, 0.18); border-color: #8a3030; }
.fb-status {
  margin-left: 4px;
  color: var(--muted);
}
.fb-form {
  flex-basis: 100%;
  margin-top: 8px;
  padding: 10px;
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 6px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
/* The form's `display: flex` above overrides the HTML `hidden` attribute,
   so JS toggling `form.hidden = true` does nothing. Re-establish hidden
   semantics explicitly. */
.fb-form[hidden] { display: none; }
.fb-form-label {
  font-size: 0.85rem;
  color: var(--text);
  font-weight: 500;
}
.fb-reason, .fb-comment {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 6px 8px;
  font: inherit;
  font-size: 0.9rem;
}
.fb-comment {
  resize: vertical;
  min-height: 60px;
}
.fb-form-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}
.fb-cancel, .fb-submit {
  background: transparent;
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 5px 12px;
  font-size: 0.85rem;
  cursor: pointer;
}
.fb-submit.primary {
  background: var(--accent);
  color: #0c1018;
  border-color: var(--accent);
  font-weight: 600;
}
.fb-submit.primary:hover { filter: brightness(1.08); }

/* Admin feedback table — responsive cells */
.feedback-prompt, .feedback-response, .feedback-comment {
  max-width: 360px;
  white-space: pre-wrap;
  word-break: break-word;
  font-size: 0.85rem;
  line-height: 1.4;
}
.feedback-prompt { color: var(--text); font-weight: 500; }
.feedback-response { color: var(--muted); }
.feedback-comment { color: #f3d8a8; font-style: italic; }

.auth-alt {
  text-align: center;
  margin: 18px 0 0;
  font-size: 0.9rem;
}

.auth-alt a {
  color: var(--accent);
  text-decoration: none;
}

.auth-alt a:hover { text-decoration: underline; }

.auth-footer {
  margin-top: 24px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
  font-size: 0.8rem;
  color: var(--fg-mute);
  text-align: center;
  line-height: 1.5;
}

/* User widget — DEPRECATED after the avatar-as-hub refactor (the avatar
   popover replaces this element on all viewports). Hidden via display:none
   so any leftover JS that still mutates it doesn't paint visible chrome. */
.user-widget {
  display: none !important;
}

.user-widget .user-name {
  font-weight: 500;
  color: var(--fg);
}

.user-widget .user-admin-badge {
  background: var(--accent);
  color: #0c1018;
  font-size: 0.7rem;
  font-weight: 700;
  padding: 2px 6px;
  border-radius: 3px;
  letter-spacing: 0.04em;
}

.user-widget form {
  display: inline;
  margin: 0;
}

.user-widget button {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--fg-mute);
  padding: 4px 10px;
  border-radius: 4px;
  font-size: 0.8rem;
  cursor: pointer;
  transition: all 0.15s;
}

.user-widget button:hover {
  border-color: var(--accent);
  color: var(--fg);
}

/* Admin tables */
.admin-table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 12px;
  font-size: 0.9rem;
}
.admin-table th {
  text-align: left;
  font-weight: 600;
  color: var(--fg-mute);
  border-bottom: 1px solid var(--border);
  padding: 8px 6px;
}
/* Click-to-sort affordance for admin tables (wired by admin-sort.js).
   Only headers without data-no-sort get the cursor + hover styling. */
.admin-table th.admin-table-th-sortable {
  cursor: pointer;
  user-select: none;
}
.admin-table th.admin-table-th-sortable:hover {
  color: var(--text);
}
.admin-table th.admin-table-th-sortable:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.admin-table td {
  padding: 6px;
  border-bottom: 1px solid var(--bg-elev-2);
}
.admin-table code {
  font-family: ui-monospace, "Cascadia Mono", "Fira Code", Menlo, monospace;
  font-size: 0.85rem;
}

/* "Copy link" button on /admin/tokens. Compact, fits in admin-table row;
   gives temporary OK/err feedback when clicked. */
.copy-invite-btn {
  font-size: 0.85rem;
  padding: 4px 10px;
  border-radius: 4px;
  border: 1px solid var(--border);
  background: var(--bg-elev);
  color: var(--text);
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease;
}
.copy-invite-btn:hover {
  background: var(--bg-elev-2);
}
.copy-invite-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.copy-invite-btn-ok {
  background: #1f7a3a;
  border-color: #1f7a3a;
  color: #fff;
}
.copy-invite-btn-err {
  background: #a13b2e;
  border-color: #a13b2e;
  color: #fff;
}

/* ---------- Admin page layout ---------- */
/* Replaces the (cramped) login-card layout the admin used to borrow.
   Wide content area (data tables, mint form, audit log) on a centred
   1100px max-width. */
.admin-body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 14.5px;
  line-height: 1.5;
  min-height: 100vh;
  /* Override the global body { display: flex; flex-direction: column } from
     line ~28 — that rule was added for the chat layout but causes admin-main
     to shrink-to-content-width since it's a flex item without flex: 1. We
     want a normal block layout here. */
  display: block;
}
.admin-header {
  background: var(--bg-elev);
  border-bottom: 1px solid var(--border);
  padding: 14px 0;
}
.admin-header-inner {
  max-width: 1100px;
  margin: 0 auto;
  padding: 0 28px;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 18px;
  flex-wrap: wrap;
}
.admin-header h1 {
  margin: 0;
  font-size: 1.3rem;
  color: var(--accent-2);
  letter-spacing: 0.01em;
}
.admin-meta {
  color: var(--muted);
  font-size: 0.9rem;
}
.admin-meta a {
  color: var(--accent-2);
  text-decoration: none;
}
.admin-meta a:hover { text-decoration: underline; }
.admin-main {
  max-width: 1100px;
  margin: 0 auto;
  padding: 28px;
}
.admin-main h2 {
  font-size: 1.1rem;
  margin: 28px 0 6px;
  color: var(--text);
}
.admin-main h2:first-child {
  margin-top: 0;
}
/* The ad-hoc 'mint codes' form on /admin used to lean on .auth-form. Give
   it a self-contained look so it doesn't render with login-page font sizes. */
.admin-main form.auth-form {
  max-width: 380px;
  background: var(--bg-elev);
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: 8px;
  margin-top: 12px;
}

/* ---------- Admin tab strip ---------- */
.admin-tabs {
  background: var(--bg-elev);
  border-bottom: 1px solid var(--border);
}
.admin-tabs-inner {
  max-width: 1100px;
  margin: 0 auto;
  /* Tighter outer padding so 11 tabs fit. The last tab ("Usage")
     was being clipped because the row was just over 1100px and
     the hidden scrollbar made the overflow invisible. */
  padding: 0 16px;
  display: flex;
  gap: 4px;
  overflow-x: auto;
  /* Without this, browsers sometimes show a tiny vertical scrollbar when
     the row fits but `overflow-x: auto` implicitly enables both axes. */
  overflow-y: hidden;
  /* Hide the horizontal scrollbar visually — the row is still
     swipe/scroll-wheel scrollable when narrow. */
  scrollbar-width: none;            /* Firefox */
  -ms-overflow-style: none;          /* old Edge / IE */
}
.admin-tabs-inner::-webkit-scrollbar { display: none; }  /* Chrome/Safari */
.admin-tab {
  /* Was 10px 16px — tightened to 10px 12px so all 11 tabs
     (Overview .. Usage) fit within max-width: 1100px on desktop. */
  padding: 10px 12px;
  color: var(--muted);
  text-decoration: none;
  font-size: 0.92rem;
  font-weight: 500;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;            /* overlap header bottom border */
  white-space: nowrap;
  flex-shrink: 0;                 /* never let a tab squeeze its label */
  transition: color 0.15s, border-color 0.15s;
}
.admin-tab:hover {
  color: var(--text);
}
.admin-tab.active {
  color: var(--accent-2);
  border-bottom-color: var(--accent-2);
}

/* ---------- Overview stat-card grid ---------- */
.admin-stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 14px;
  margin-top: 4px;
}
.admin-stat-card {
  display: block;
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 14px 16px;
  color: var(--text);
  text-decoration: none;
  transition: border-color 0.15s, transform 0.15s;
}
.admin-stat-card:not(.no-link):hover {
  border-color: var(--accent);
  transform: translateY(-1px);
}
.admin-stat-card.no-link {
  cursor: default;
  opacity: 0.85;
}
.admin-stat-num {
  font-size: 1.6rem;
  font-weight: 700;
  color: var(--accent-2);
  line-height: 1.1;
}
.admin-stat-label {
  font-size: 0.82rem;
  color: var(--muted);
  margin-top: 4px;
  letter-spacing: 0.01em;
}

/* ---------- User-detail page ---------- */
.admin-link {
  color: var(--accent-2);
  text-decoration: none;
}
.admin-link:hover { text-decoration: underline; }
.admin-detail-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 14px;
  margin-top: 12px;
}
.admin-detail-grid > div {
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 10px 12px;
}
.admin-detail-grid .lbl {
  display: block;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  margin-bottom: 4px;
}
.admin-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 8px;
}
.admin-actions form { margin: 0; }
.admin-actions button {
  background: var(--bg-elev);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px 14px;
  font-size: 0.9rem;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.admin-actions button:hover:not(:disabled) {
  background: var(--bg-elev-2);
  border-color: var(--border-strong);
}
.admin-actions button.primary {
  background: var(--accent);
  color: #0c1018;
  border-color: var(--accent);
  font-weight: 600;
}
.admin-actions button.primary:hover { filter: brightness(1.08); }
.admin-actions button.danger {
  background: rgba(244, 67, 54, 0.12);
  border-color: #8a3030;
  color: #ff8a80;
}
.admin-actions button.danger:hover { background: rgba(244, 67, 54, 0.22); }
.admin-actions button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
.admin-token-box {
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 16px 18px;
  max-width: 640px;
}
.admin-token-box .lbl {
  display: block;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  margin-bottom: 4px;
}
.admin-token-box pre {
  background: var(--bg);
  padding: 8px 12px;
  border-radius: 4px;
  margin: 0;
  font-family: ui-monospace, "Cascadia Mono", "Fira Code", Menlo, monospace;
  font-size: 0.95rem;
  word-break: break-all;
  white-space: pre-wrap;
}
.admin-notice {
  padding: 10px 14px;
  background: rgba(76, 175, 80, 0.1);
  border: 1px solid rgba(76, 175, 80, 0.35);
  color: #c9e8d5;
  border-radius: 6px;
  margin-bottom: 14px;
  font-size: 0.9rem;
}

/* Inline two-step delete confirmation under the user-detail Actions row.
   Click 'Delete account' → reveals this panel with a hard-delete checkbox
   and a final 'Confirm delete' button. Default = soft delete; ticking the
   box switches to hard delete (CASCADEs feedback away). */
.admin-delete-wrap { display: inline-block; }
.admin-delete-panel {
  margin-top: 12px;
  padding: 14px;
  background: var(--bg-elev);
  border: 1px solid #8a3030;
  border-radius: 8px;
  max-width: 640px;
}
.admin-delete-warn {
  margin: 0 0 12px;
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--text);
}
.admin-delete-warn code {
  background: var(--bg);
  padding: 1px 6px;
  border-radius: 3px;
  font-size: 0.85rem;
}
.admin-delete-form .checkbox-row {
  align-items: flex-start !important;
  margin: 0 0 12px !important;
  font-size: 0.9rem !important;
  line-height: 1.5;
  font-weight: normal !important;
}
.admin-delete-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}
.admin-delete-actions button {
  background: var(--bg-elev-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 14px;
  font-size: 0.9rem;
  cursor: pointer;
}
.admin-delete-actions button.danger {
  background: rgba(244, 67, 54, 0.18);
  border-color: #8a3030;
  color: #ff8a80;
  font-weight: 600;
}
.admin-delete-actions button.danger:hover { background: rgba(244, 67, 54, 0.28); }

/* ===========================================================
 * Responsive scaffolding (added 2026-05-06 for mobile/tablet)
 * ===========================================================
 *
 * Three breakpoints (touch the existing desktop layer is OFF LIMITS;
 * any change to the touch experience must live INSIDE one of these
 * media queries so the desktop selectors stay byte-for-byte unchanged):
 *
 *   Phone   ≤ 640px    @media (max-width: 640px)
 *   Tablet  641-1024px @media (min-width: 641px) and (max-width: 1024px)
 *   Desktop ≥ 1025px   (no media query — the default layer above)
 *
 * Phone and tablet share the same drawer-style thread list (hamburger +
 * slide-in overlay). Tablet differs from phone mostly in larger spacing
 * and wider drawer width.
 *
 * Implementation rules:
 *   1. Every NEW rule for touch UX goes inside one of the two blocks
 *      below. Do not add it to the desktop layer.
 *   2. If a desktop rule MUST change to support touch, copy-and-shadow
 *      the rule inside a media query — never edit the desktop selector.
 *   3. The legacy `@media (max-width: 800px)` rule above is being
 *      replaced by these two; it stays during the migration so partial
 *      deploys don't break, then gets deleted in mobile-deploy.
 *   4. New mobile-only utility classes (e.g. `.is-drawer-open`) are
 *      fine to add to the desktop layer as long as they are unused on
 *      desktop — they are inert without the touch JS.
 *
 * Rationale for the breakpoints: 640px = iPhone Pro Max-class phones
 * in landscape; 1024px = iPad portrait. iPad landscape (1024px wide)
 * is borderline desktop and intentionally falls into the desktop layer
 * — it already has enough horizontal room for the existing three-pane
 * layout to feel right.
 */

/* ---------- Phone (≤ 640px) — bespoke redesign (phase 2.5) ---------- */
@media (max-width: 640px) {
  /* Use safe-area insets so content clears the Dynamic Island / notch /
   * home indicator on modern iPhones. The viewport-fit=cover meta tag
   * (added phase 1) is what makes env() return non-zero values here. */

  /* ===== Topbar — bulletproof at ANY viewport width =====
   * Three rigid columns: hamburger (40px) | brand (fills, shrinks)
   * | avatar (36px). overflow: hidden on the topbar so nothing can
   * leak out and break layout. min-width: 0 on every flex/grid child
   * that contains text so they can shrink down to nothing without
   * forcing horizontal scroll. Title gets ellipsis at any width.
   *
   * This survives:
   *   - iOS focus-zoom changing the visual viewport
   *   - Manual pinch-zoom
   *   - Address bar showing/hiding (changes available height)
   *   - Rotation
   *   - Notification banner / keyboard appearing
   *   - Long usernames or admin badges
   * Without re-flowing or breaking layout. */
  .topbar {
    display: grid;
    grid-template-columns: 40px minmax(0, 1fr) 36px;
    align-items: center;
    padding: 8px 10px;
    padding-top: max(8px, env(safe-area-inset-top));
    gap: 8px;
    min-height: 52px;
    overflow: hidden;
  }
  .drawer-toggle {
    display: inline-flex;
    width: 40px; height: 40px; font-size: 18px;
    padding: 0;
  }
  .brand {
    display: flex;
    align-items: center;
    min-width: 0;
    gap: 6px;
    justify-content: center;
    overflow: hidden;
  }
  .brand .logo { height: 28px; flex-shrink: 0; max-width: 100%; }
  .brand .brand-text {
    display: block;
    min-width: 0;
    overflow: hidden;
    flex-shrink: 1;
  }
  .brand h1 {
    font-size: 14px; line-height: 1.1;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    margin: 0;
  }
  .brand .sub { display: none; }

  /* Hide the full desktop user-widget completely on phone — the avatar
   * + popover replaces it. */
  .user-widget { display: none !important; }
  .topnav {
    /* No margin-left: auto here — that would force the avatar to push
     * to the right edge with brand right after the hamburger, which
     * created a big void between brand and avatar. With space-between
     * on the topbar instead, the avatar floats right naturally without
     * a margin-left override. */
    gap: 4px; min-width: 0; flex-shrink: 0;
  }
  .topnav .nav-btn { display: none; }

  /* Avatar circle. Renders the user's first initial. is-admin gets a
   * yellow ring so admins can see at a glance which account they're in.
   * Always rendered (not behind [hidden]) so it reserves layout space
   * immediately — JS toggles style.visibility once /api/whoami returns
   * to fade it in without shifting the topbar layout. */
  .user-avatar {
    display: inline-flex;
    align-items: center; justify-content: center;
    width: 36px; height: 36px;
    border-radius: 50%;
    background: var(--accent);
    color: #0c1018;
    font-weight: 700; font-size: 15px;
    border: 2px solid transparent;
    cursor: pointer;
    flex-shrink: 0;
  }
  .user-avatar.is-admin { border-color: var(--highlight); }
  .user-avatar:hover { filter: brightness(1.05); }

  /* User popover — anchored to the topbar's right edge, slides down
   * below the avatar. */
  .user-popover {
    position: fixed;
    top: calc(env(safe-area-inset-top) + 56px);
    right: 8px;
    min-width: 180px; max-width: calc(100vw - 16px);
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    z-index: 60;
    padding: 6px 0;
    display: flex; flex-direction: column;
  }
  .user-popover[hidden] { display: none; }
  .user-popover-header {
    display: flex; align-items: center; gap: 8px;
    padding: 8px 14px 10px;
    border-bottom: 1px solid var(--border);
    margin-bottom: 4px;
  }
  .user-popover-name {
    color: var(--text); font-weight: 600; font-size: 14px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .user-popover-item {
    display: block;
    padding: 12px 14px;
    color: var(--text);
    background: none; border: none;
    font-size: 14px; text-align: left; text-decoration: none;
    cursor: pointer; width: 100%;
    font-family: inherit;
  }
  .user-popover-item:hover,
  .user-popover-item:active { background: var(--bg-elev-2); }
  .user-popover-logout { color: var(--danger); }
  .user-popover-form { margin: 0; }

  /* ===== iOS Chrome scroll lockdown =====
   * iOS Chrome (and Safari) sometimes ignore body { overflow: hidden }
   * and let the body scroll anyway, especially when the body content
   * (or a fixed-position child) is taller than the viewport. The user
   * reported being able to "drag the page" to bring the composer into
   * view — that's body scroll happening despite overflow:hidden.
   *
   * The reliable iOS lockdown: position: fixed on html AND body, so
   * neither can be scrolled. All scrolling happens inside .messages
  /* ===== iOS scroll lockdown =====
   * Pin html AND body to position: fixed so the OUTER document never
   * scrolls — only the inner .messages region scrolls via overflow-y:
   * auto (with overscroll-behavior: contain to prevent rubber-band
   * propagation).
   *
   * NOTE: admin pages explicitly opt OUT of this lockdown via the
   * body.admin-body override later in this file, because they need
   * natural body scrolling (vertical for long user/code tables,
   * horizontal for wide tables). Without that override the admin
   * page would be completely un-scrollable on phones. */
  html {
    position: fixed;
    overflow: hidden;
    width: 100%;
    height: 100%;
  }
  body {
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    display: flex; flex-direction: column;
    width: 100%;
    height: 100%;
    overflow: hidden;
  }
  .topbar { flex-shrink: 0; }
  .app-shell {
    grid-template-columns: 1fr;
    flex: 1 1 0;
    min-height: 0;
    min-width: 0;
    max-width: 100vw;
    height: auto;
  }
  .app-shell .sidebar { display: none; }

  /* Chat column is a 3-row grid: chat-head (auto) | messages (minmax 0 1fr)
   * | composer (auto).
   *
   * CRITICAL: minmax(0, 1fr) NOT just 1fr. The default `1fr` is
   * equivalent to minmax(auto, 1fr), where `auto` means "min size =
   * content size". With a long thread (21000+px of messages), the
   * row grows to fit content instead of being constrained to leftover
   * space — pushing the composer (row 3) off-screen. minmax(0, 1fr)
   * forces the row to honour the available space and let overflow:auto
   * inside .messages handle the scrolling.
   *
   * This was the root cause of 'composer vanishes when loading existing
   * chat in new tab' on iOS Chrome — WebKit is stricter about
   * minmax(auto, 1fr) than Chromium emulation. */
  .chat {
    display: grid;
    /* Third row explicitly reserves space for the composer (which is
     * `position:fixed` on mobile and would otherwise be a 0-height
     * grid track on Chrome iOS, letting .messages overflow below the
     * composer where feedback bars get hidden). --composer-height is
     * set by syncComposerHeight(); 80px fallback for first paint. */
    grid-template-rows: auto minmax(0, 1fr) var(--composer-height, 80px);
    min-height: 0;
    min-width: 0;
    max-width: 100%;
    overflow: hidden;
    height: 100%;
  }
  .chat .thread-actions-menu { grid-row: auto; }
  .messages {
    min-height: 0;
    /* Prevent rubber-band scroll propagating to the body when the user
     * reaches the end of the message scroll. Without this, iOS scrolls
     * the body too, which can move the composer off-screen. */
    overscroll-behavior: contain;
  }
  /* ===== Composer — position:fixed with visualViewport handler =====
   * After many CSS-only attempts (flex column, grid auto/1fr/auto,
   * grid auto/minmax(0,1fr)/auto, position:fixed on html+body,
   * overflow:hidden on body, etc), iOS Chrome was still rendering
   * the composer below the visible viewport on first paint with an
   * existing chat. CSS-only solutions all hit some WebKit edge case.
   *
   * Going brute-force: position:fixed on the composer, anchored to
   * the bottom of the visual viewport (which excludes Chrome chrome
   * + soft keyboard). JS in app.js listens to visualViewport resize
   * and updates --keyboard-offset so the composer rides above the
   * keyboard when it opens. Same approach used by Slack iOS web,
   * WhatsApp Web, Discord mobile.
   *
   * .messages gets padding-bottom equal to the composer height so
   * its content can scroll past where the composer sits. */
  .composer {
    position: fixed;
    bottom: var(--keyboard-offset, env(safe-area-inset-bottom, 0));
    left: 0; right: 0;
    z-index: 100;
    background: var(--bg-elev);
    flex-shrink: 0;
    min-height: 56px;
  }
  .messages {
    padding-bottom: 12px;
  }

  /* ===== Modals (Parts + Settings) — full viewport on phone =====
   * Desktop modals are centered cards with max-height 90vh. On phone
   * we want them to fill the entire viewport so users get maximum
   * working space and the close button is always reachable. */
  .modal { padding: 0; align-items: stretch; justify-content: stretch; }
  .modal-card {
    width: 100%;
    max-width: none;
    max-height: none;
    height: 100dvh;
    border-radius: 0;
    border: none;
    padding: 0;
    overflow-y: auto;
    /* Reserve space for the iOS bottom safe area so content doesn't
     * sit under the home indicator. */
    padding-bottom: env(safe-area-inset-bottom);
    display: flex;
    flex-direction: column;
  }
  .modal-head {
    /* Sticky head so users can close at any scroll position. Larger
     * close button (44x44 = Apple HIG min tap target). */
    position: sticky;
    top: 0;
    z-index: 5;
    background: var(--bg-elev);
    padding: 12px 16px;
    padding-top: max(12px, env(safe-area-inset-top));
    margin-bottom: 0;
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
  }
  .modal-head h3 { font-size: 16px; }
  .modal-head .modal-close {
    width: 44px;
    height: 44px;
    font-size: 20px;
    padding: 0;
    border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    flex-shrink: 0;
  }
  .modal-card > *:not(.modal-head) {
    padding-left: 16px;
    padding-right: 16px;
  }
  .modal-card > .searchbar:first-of-type { margin-top: 12px; }
  /* Settings form: stack the two columns into one on phone. */
  .settings-form { grid-template-columns: 1fr; }

  /* ===== Lightbox — bottom-sheet on phone =====
   * Desktop lightbox is a full-screen modal. On phone, convert to a
   * bottom-sheet style: dark backdrop, image fills 90% width, drag
   * handle at the top suggests dismissibility, native pinch-zoom works
   * on the image itself.
   *
   * The full overlay is kept (not 90vh half-sheet) because manual page
   * images NEED the vertical space to be readable. Bottom-sheet visual
   * cue is just the drag handle and rounded top corners. */
  .lightbox {
    /* Add a small drag-handle bar at the top via ::before. */
  }
  .lightbox::before {
    content: "";
    position: absolute;
    top: 8px;
    left: 50%;
    transform: translateX(-50%);
    width: 40px; height: 4px;
    background: rgba(255, 255, 255, 0.4);
    border-radius: 2px;
    z-index: 10;
  }
  .lightbox-toolbar {
    padding: 18px 8px 6px;  /* room for the drag handle above */
    flex-wrap: wrap;
    row-gap: 4px;
  }
  .lightbox-title {
    font-size: 11.5px;
    flex-basis: 100%;
    order: 99;  /* push to bottom of toolbar wrap */
  }
  .lightbox-position {
    font-size: 11px; padding: 0 4px;
  }
  .lightbox-toolbar button {
    min-width: 36px; padding: 6px 8px; font-size: 13px;
  }
  /* Stage uses the FULL viewport on phone (minus toolbar) for max
   * image readability. */
  .lightbox-stage img {
    max-width: 100vw;
    max-height: calc(100dvh - 80px);
  }
  /* Messages area: clip horizontal overflow so long URLs / wide images
   * stay inside the viewport instead of pushing the body wider. */
  .messages {
    min-width: 0;
    overflow-x: hidden;
  }
  /* Bubble children that historically caused horizontal overflow:
   * inline images (citations, diagrams), <pre> code blocks, and
   * <table> elements. Force them to fit the bubble. */
  .bubble img,
  .bubble pre,
  .bubble table {
    max-width: 100% !important;
    box-sizing: border-box;
  }
  .bubble pre {
    overflow-x: auto;  /* still scrollable inside the bubble */
    word-break: normal;
  }

  /* ===== Drawer (slide-in thread list) ===== */
  body.drawer-open .app-shell .sidebar {
    display: flex;
    position: fixed;
    top: calc(env(safe-area-inset-top) + 52px);
    left: 0;
    /* Stop above the fixed composer (composer min-height 56 + 16 padding
     * + safe-area-inset-bottom) so the drawer's bottom items (Parts,
     * Settings) aren't obscured. */
    bottom: calc(var(--composer-height, 100px) + 8px);
    width: min(85vw, 320px);
    z-index: 50;
    background: var(--bg-elev);
    border-right: 1px solid var(--border);
    box-shadow: var(--shadow);
    overflow-y: auto;
    padding-bottom: 8px;
  }
  /* Backdrop dim — also stop above the composer so it doesn't cover
   * the composer either. */
  body.drawer-open .app-shell::before {
    content: "";
    position: fixed;
    top: calc(env(safe-area-inset-top) + 52px);
    left: 0; right: 0;
    bottom: calc(var(--composer-height, 100px) + 8px);
    background: rgba(0, 0, 0, 0.55);
    z-index: 49;
  }

  /* ===== Chat header — title + kebab ===== */
  .chat-head {
    padding: 8px 12px;
    gap: 8px;
  }
  .chat-head h2 {
    font-size: 14px; line-height: 1.25;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    flex: 1; min-width: 0;
  }
  /* Hide inline Rename + Delete; surface them via the kebab. */
  .head-actions .head-action-rename,
  .head-actions .head-action-delete { display: none; }
  .thread-kebab {
    display: inline-flex;
    align-items: center; justify-content: center;
    width: 36px; height: 36px;
    background: transparent; color: var(--muted);
    border: 1px solid var(--border); border-radius: var(--radius-sm);
    font-size: 20px; line-height: 1; cursor: pointer;
    flex-shrink: 0;
  }
  .thread-kebab:hover { background: var(--bg-elev-2); color: var(--text); }
  .thread-actions-menu {
    position: fixed;
    top: calc(env(safe-area-inset-top) + 100px);
    right: 12px;
    min-width: 160px;
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    z-index: 60;
    padding: 6px 0;
    display: flex; flex-direction: column;
  }
  .thread-actions-menu[hidden] { display: none; }
  .thread-actions-menu button {
    display: block;
    padding: 12px 14px;
    background: none; border: none; color: var(--text);
    font-size: 14px; text-align: left; cursor: pointer; width: 100%;
    font-family: inherit;
  }
  .thread-actions-menu button:hover { background: var(--bg-elev-2); }
  .thread-actions-menu button.danger { color: var(--danger); }

  /* ===== Messages area ===== */
  .messages {
    padding: 12px 10px;
    /* Reserve space for the position:fixed composer at the bottom.
     * Composer = min-height 56px + 8px top padding + 8px bottom padding
     * + safe-area-inset-bottom (up to ~34px on iPhones with home indicator)
     * = up to 106px. Use 110px + safe-area double-counted for extra
     * breathing room so feedback buttons + thumbnails clear it. */
    padding-bottom: 12px;
    gap: 14px;
  }
  .msg {
    max-width: 100%;
    gap: 6px;
  }
  .msg .avatar {
    width: 24px; height: 24px; font-size: 10px;
  }
  .msg .bubble {
    /* min-width: 0 is critical — without it, flex children can refuse
     * to shrink below their content width, causing horizontal overflow. */
    min-width: 0;
    flex: 1 1 auto;
    padding: 9px 12px;
    font-size: 14.5px;
    /* Force long unbroken strings (URLs, part numbers) to wrap rather
     * than push the bubble off-screen. */
    overflow-wrap: anywhere;
    word-break: normal;
  }
  .msg.user .bubble {
    /* User bubble gets a small left margin so it visibly sits inside
     * the right edge rather than butting against it. */
    margin-left: 32px;
  }
  .bubble code { font-size: 12px; }

  /* ===== Citation thumbnail strip ===== */
  .cite-thumbs {
    margin-top: 10px;
    /* The strip itself scrolls horizontally if there are too many; the
     * key fix is making sure the bubble's own min-width is 0 (above)
     * so this scroll works inside the bubble instead of pushing it. */
    -webkit-overflow-scrolling: touch;
    padding-bottom: 4px;
  }
  .cite-thumb {
    width: 88px;
  }
  .cite-thumb img { height: 110px; }

  /* ===== Composer =====
   * NOT position: fixed — the body uses display:flex column with
   * overflow:hidden, so .composer is the last flex sibling in the
   * .chat column and naturally sits at the bottom. .messages above
   * it has flex:1 + overflow-y:auto so its content scrolls within
   * itself, and the composer is always visible.
   *
   * Why not position:fixed? On iOS, position:fixed + soft keyboard +
   * 100dvh interacts badly: the keyboard opening shifts the layout
   * viewport, the fixed element ends up behind the keyboard, and
   * iOS pushes the whole page up to compensate — content overflows
   * to the right and the user has to pinch-zoom out to see it. */
  .composer {
    flex-shrink: 0;
    padding: 8px 10px;
    padding-bottom: max(8px, env(safe-area-inset-bottom));
    gap: 6px;
    align-items: flex-end;
    background: var(--bg-elev);
  }
  .composer textarea {
    font-size: 16px; /* iOS won't auto-zoom on focus if input >= 16px */
    min-height: 40px; max-height: 40dvh;
    padding: 9px 12px;
    border-radius: 18px;
  }
  /* Override the verbose desktop placeholder with a short touch hint.
   * Browsers don't expose ::placeholder text directly via CSS; the
   * actual text comes from index.html. We mitigate by shrinking + truncating
   * so the desktop hint fits in the available width. JS could swap it
   * but a CSS-only solution keeps phase 2.5 zero-JS-additions for the
   * composer specifically. */
  .composer textarea::placeholder {
    font-size: 14px;
  }
  /* Convert Send into a circular icon button. The .btn-label text is
   * hidden via visually-hidden trick; the ➤ is added via ::before. */
  .composer .primary {
    width: 44px; height: 44px;
    border-radius: 50%;
    padding: 0;
    flex-shrink: 0;
    position: relative;
    display: inline-flex; align-items: center; justify-content: center;
  }
  .composer .primary .btn-label {
    position: absolute; width: 1px; height: 1px;
    padding: 0; margin: -1px; overflow: hidden;
    clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
  }
  .composer .primary::before {
    content: "➤"; font-size: 18px; line-height: 1;
  }
  .composer .primary.busy::before { content: ""; }
  /* Stop button: also circular, danger-coloured. Only when not hidden —
   * the [hidden] attribute is meant to hide the element entirely; without
   * the :not exception my display: inline-flex would override the
   * browser's default `[hidden] { display: none }`. */
  .composer .ghost.danger:not([hidden]) {
    width: 44px; height: 44px;
    border-radius: 50%;
    padding: 0;
    flex-shrink: 0;
    display: inline-flex; align-items: center; justify-content: center;
  }
  /* Composer position is now handled by the body { display: flex }
   * layout above — composer is the last flex sibling in .chat which
   * is the last flex sibling in body, so it sits at the bottom by
   * design. No position:fixed needed (which would have iOS keyboard
   * interaction bugs). The .messages container above it has
   * overflow-y: auto so its content scrolls within itself, and the
   * composer is always visible without breaking layout when the
   * keyboard opens. */
  /* (No padding-bottom needed on .messages now — content can't sit
   * under a fixed composer because the composer isn't fixed.) */

  /* ===== Drawer footer tools (Parts + Messages + Settings) ===== */
  /* These buttons live in the thread drawer on phone+tablet because
   * the original Parts/Settings buttons in the topbar are hidden. They
   * proxy-click the originals via app.js. flex-shrink:0 + bottom padding
   * keeps them safely above the .sidebar-foot which holds Report-a-bug
   * + Clear-all (so they never get squished under each other when the
   * thread list is long). */
  .drawer-tools {
    display: flex; flex-direction: column; gap: 4px;
    padding: 12px 12px 12px;
    border-top: 1px solid var(--border);
    flex-shrink: 0;
  }
  .drawer-tool {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    padding: 12px 14px;
    background: var(--bg-elev-2);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-size: 14px;
    text-align: left;
    cursor: pointer;
    text-decoration: none;
    box-sizing: border-box;
  }
  .drawer-tool:hover { background: var(--bg-elev-3); }
  .drawer-tool-messages { gap: 8px; }
  /* Sidebar foot must also keep its height + wrap if narrow so buttons
   * don't overlap each other on small screens (Report a bug + Clear all
   * sit alongside the version label). */
  .sidebar-foot {
    flex-shrink: 0;
    flex-wrap: wrap;
    row-gap: 6px;
  }

  /* ===== Lightbox prev/next nav buttons ===== */
  /* Desktop is 96x180 — way too big on phone. Scale to a tappable
   * 48x64 sized for thumb-reach near screen edges. */
  .lightbox-nav {
    width: 48px; height: 64px;
    font-size: 32px;
    border-radius: 8px;
  }
  .lightbox-nav-prev { left: 8px; }
  .lightbox-nav-next { right: 8px; }
  /* Toolbar shrinks too — desktop has plenty of room for "100%" zoom
   * label, on phone it crowds. */
  .lightbox-toolbar { padding: 6px 8px; gap: 6px; }
  .lightbox-title { font-size: 12px; }
  .lightbox-toolbar button { min-width: 36px; padding: 6px 8px; font-size: 13px; }

  /* ===== iOS zoom-on-focus prevention =====
   * iOS Safari auto-zooms the viewport when focusing an input whose
   * computed font-size is < 16px. The zoom is unwanted because it
   * (a) scales the whole UI awkwardly, (b) doesn't always reset on
   * blur. Forcing every text-entry control to 16px disables it.
   * Applies to the composer textarea, thread filter, signup/login,
   * settings modal inputs, etc. */
  input[type="text"],
  input[type="search"],
  input[type="email"],
  input[type="password"],
  input[type="number"],
  input[type="tel"],
  input[type="url"],
  textarea,
  select {
    font-size: 16px !important;
  }

  /* ===== Notification banner / toast =====
   * On phone, multiple stacked info toasts (e.g. WhatsApp announcement +
   * feedback announcement) consume nearly half the screen, push the
   * composer to the bottom edge, and obscure the chat. Show only the
   * MOST RECENT toast at a time on phone — the others are still in the
   * DOM (so dismissal still works) but visually hidden. */
  .notif-banner-inner {
    padding: 10px 12px;
    gap: 8px; font-size: 13px;
  }
  #notifToastHost {
    bottom: calc(env(safe-area-inset-bottom) + 70px) !important; /* clear composer */
    left: 8px !important; right: 8px !important;
    max-width: none !important;
    /* Cap height so toasts can never take more than 1/3 the viewport. */
    max-height: 33vh;
    overflow: hidden;
  }
  /* Only show the most recent toast. The toast list is appended in
   * reverse order (newest first?) — to be safe, show the LAST child
   * which is the most recently inserted. */
  #notifToastHost .notif-toast { display: none; }
  #notifToastHost .notif-toast:last-child { display: flex; }
  .notif-toast {
    max-width: none;
    font-size: 13px;
  }

  /* ===== Ingest banner (compact) ===== */
  .ingest-banner { padding: 6px 12px; }
  .ingest-banner-inner { font-size: 11.5px; gap: 8px; }

  /* ===== Messages page mobile ===== */
  /* Single column: list shown by default; opening a conversation
     hides the list and shows the detail. Back button below restores. */
  .messages-shell {
    grid-template-columns: 1fr;
    position: relative;
  }
  .messages-list {
    border-right: none;
  }
  .messages-detail {
    position: absolute;
    inset: 0;
    background: var(--bg);
    z-index: 1;
    display: none;
  }
  /* When detail has content, show it instead of list */
  .messages-shell.detail-active .messages-list { display: none; }
  .messages-shell.detail-active .messages-detail { display: flex; }
  .messages-detail-head h2 {
    font-size: 14px;
    flex: 1;
    margin-left: 8px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .messages-back-btn { display: inline-flex; }
  .messages-list-item-subject { max-width: calc(100vw - 80px); }
  /* Compact compose / hide labels in messages topnav */
  /* On the /messages page mobile view, the topbar's only nav-btn is the
     "← Chat" back link — that one MUST stay visible (the chat page hides
     all topbar nav-btns because Parts/Settings move into the drawer, but
     /messages doesn't have a drawer to surface this link). Override the
     chat-page's grid layout (40 / 1fr / 36) which would otherwise squish
     the brand and bury the back link in the wrong column. Right-justify
     the back link to match desktop behaviour. */
  .messages-body .topbar {
    display: flex;
    grid-template-columns: none;
    justify-content: space-between;
    gap: 8px;
    overflow: visible;
  }
  .messages-body .topnav {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
  }
  .messages-body .topnav .nav-btn {
    display: inline-flex;
    align-items: center;
    padding: 6px 10px;
    font-size: 13px;
  }
  .messages-body .nav-btn-label { display: none; }
  .messages-body .nav-btn-icon { font-size: 18px; }
  /* On the /messages page, KEEP the brand text + sub visible on mobile —
     the chat page hides them to save space, but on /messages this is the
     only page header the user has so it's worth the few extra pixels.
     Compact the sub line to a smaller size to fit. */
  .messages-body .brand {
    min-width: 0;
    flex: 1 1 auto;
    overflow: hidden;
  }
  .messages-body .topnav { flex-shrink: 0; }
  .messages-body .brand .brand-text {
    display: block;
    min-width: 0;
    overflow: hidden;
  }
  .messages-body .brand .brand-text h1 {
    font-size: 16px;
    line-height: 1.1;
    margin: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .messages-body .brand .sub {
    display: block;
    font-size: 11px;
    line-height: 1.25;
    margin-top: 2px;
    color: var(--muted);
    /* Ellipsis if it doesn't fit */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Composer pinned to viewport bottom — flex/svh approaches lose against
     iOS Chrome's bottom toolbar (it overlays content rather than reserving
     space). position:fixed is anchored relative to the LAYOUT viewport,
     which iOS Chrome correctly excludes its own toolbar from. Mirrors the
     proven main chat page pattern (.composer in style.css). */
  .messages-composer,
  .messages-composer-locked {
    position: fixed;
    left: 0;
    right: 0;
    bottom: var(--keyboard-offset, 0);
    padding-bottom: max(12px, env(safe-area-inset-bottom));
    z-index: 5;
    box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.3);
  }
  /* Reserve space at the bottom of the thread so the last message isn't
     hidden behind the fixed composer. JS sets --messages-composer-h on
     resize; fallback ensures content clears even if JS hasn't run yet. */
  .messages-thread {
    padding-bottom: calc(var(--messages-composer-h, 100px) + env(safe-area-inset-bottom, 0px));
  }
}

/* ---------- Tablet (641-1024px) ---------- */
@media (min-width: 641px) and (max-width: 1024px) {
  /* iOS zoom-on-focus prevention applies on iPad too. Same rule as
   * the phone block — see comment there. */
  input[type="text"],
  input[type="search"],
  input[type="email"],
  input[type="password"],
  input[type="number"],
  input[type="tel"],
  input[type="url"],
  textarea,
  select {
    font-size: 16px !important;
  }

  /* Tablets keep the desktop topbar layout with the addition of a
   * hamburger to summon the thread drawer. */
  .drawer-toggle { display: inline-flex; }
  .topbar { padding: 10px 14px; gap: 12px; }
  .brand .logo { height: 40px; }
  .brand h1 { font-size: 15px; white-space: nowrap; }
  .brand .sub { display: none; }

  .app-shell { grid-template-columns: 1fr; }
  .app-shell .sidebar { display: none; }
  body.drawer-open .app-shell .sidebar {
    display: flex;
    position: fixed;
    top: 57px; left: 0;
    /* Stop above the composer so its bottom items (Parts, Settings)
     * aren't obscured. On tablet the composer is part of the layout
     * flow (not position:fixed like phone) but the same bottom-cap
     * pattern keeps the composer visible alongside the open drawer. */
    bottom: 80px;
    width: min(60vw, 360px);
    z-index: 50;
    background: var(--bg-elev);
    border-right: 1px solid var(--border);
    box-shadow: var(--shadow);
    overflow-y: auto;
  }
  body.drawer-open .app-shell::before {
    content: "";
    position: fixed;
    top: 57px; left: 0; right: 0;
    bottom: 80px;
    background: rgba(0, 0, 0, 0.45);
    z-index: 49;
  }
  /* Drawer-bottom Parts + Settings — same idea as phone, since the
   * topbar Parts/Settings buttons are still visible on tablet but
   * exposing them in the drawer too is harmless and matches phone. */
  .drawer-tools { display: flex; flex-direction: column; gap: 4px; padding: 12px 12px 0; border-top: 1px solid var(--border); }
  .drawer-tool {
    display: block; width: 100%; padding: 12px 14px;
    background: var(--bg-elev-2); color: var(--text);
    border: 1px solid var(--border); border-radius: var(--radius-sm);
    font-size: 14px; text-align: left; cursor: pointer;
  }
  .drawer-tool:hover { background: var(--bg-elev-3); }
}

/* ---------- Touch-shared (≤ 1024px) — overflow protection ----------
 * Applies to BOTH phone and tablet. Wide message content (long URLs,
 * pre/code blocks, images, tables) historically pushed the body wider
 * than the viewport, shifting the topbar and breaking layout.
 *
 * Admin pages explicitly opt OUT of the body-level overflow clamp via
 * the body.admin-body override in the admin block lower down — wide
 * admin tables (users, codes, audit, feedback) need horizontal scroll
 * on mobile, otherwise right-hand columns are unreachable. */
@media (max-width: 1024px) {
  /* The phone @media block above pins html and body to position:fixed
   * for iOS scroll lockdown — that already covers width/overflow. The
   * tablet block here still needs the body width clamp. */
  body { max-width: 100vw; overflow-x: hidden; }
  .app-shell { min-width: 0; max-width: 100vw; }
  .chat { min-width: 0; max-width: 100%; overflow: hidden; }
  .messages { min-width: 0; overflow-x: hidden; }
  .bubble img,
  .bubble pre,
  .bubble table {
    max-width: 100% !important;
    box-sizing: border-box;
  }
  .bubble pre {
    overflow-x: auto;
    word-break: normal;
  }

  /* ===== Admin override (opt out of all chat-shaped clamps) =====
   * Wide tables on /admin/users, /admin/tokens, /admin/feedback,
   * /admin/audit, /admin/notifications need natural body scrolling
   * on mobile. The chat lockdown rules above (and the phone @media
   * iOS scroll-lockdown earlier) would otherwise clip everything.
   * These admin-only overrides:
   *   1. Release the phone-level position:fixed pinning of html/body
   *      so the page can scroll vertically as well as horizontally.
   *   2. Release the tablet-level overflow-x: hidden on body so wide
   *      tables can extend past the viewport.
   *   3. Wrap the admin-table itself in a horizontally-scrollable
   *      container so users can drag-scroll the right-hand columns. */
  html:has(body.admin-body) {
    position: static;
    overflow: visible;
    width: auto;
    height: auto;
  }
  body.admin-body {
    position: static;
    width: auto;
    height: auto;
    max-width: none;
    overflow-x: auto;
    overflow-y: auto;
    display: block;
  }
  body.admin-body .admin-main {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
  body.admin-body .admin-table {
    /* Allow the table to be wider than the viewport so its parent
     * (.admin-main) can scroll horizontally. max-content keeps the
     * table sized to its actual content rather than getting squashed
     * into the available width with broken cell layouts. */
    min-width: max-content;
  }

  /* ===== Swipe-to-delete: reveal delete button on touch devices.
   * Desktop never shows this; clicking the li still selects the thread.
   * Notifications also hide the inline ✕ on touch (swipe replaces it). */
  .thread-list .swipe-delete,
  .messages-list-item .swipe-delete,
  .notif-item .swipe-delete {
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .notif-item-delete { display: none; }
}

/* ---------- Ultra-narrow (≤ 320px) — heavy iOS zoom case ---------- */
@media (max-width: 320px) {
  /* When the visual viewport is heavily zoomed in (or device is iPhone
   * SE 1st gen / certain folded foldables), the brand text becomes too
   * cramped to be useful. Drop it; logo alone identifies the app. */
  .brand .brand-text { display: none; }
}

/* ---------- /Responsive scaffolding ---------- */

