/* ==========================================================================
   KilgoreAI — chat app stylesheet
   Design tokens, fonts and the base reset live in tokens.css — load it first.
   ========================================================================== */

* { margin: 0; padding: 0; }
html, body { height: 100%; }
body {
  background: var(--bg);
  color: var(--text);
  font: var(--fs-body) / var(--lh-body) var(--font);
  display: flex;
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  transition: background var(--dur-slow) var(--ease), color var(--dur-slow) var(--ease);
}
h1, h2, h3 { text-wrap: balance; }
p { text-wrap: pretty; }

/* ============ sidebar ============ */
#sidebar {
  width: var(--sidebar-w);
  min-width: var(--sidebar-w);
  height: 100vh;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  /* Gentle top-down gradient + a hairline inner edge so the panel reads as a
     raised surface instead of a flat strip. */
  background: linear-gradient(180deg, var(--bg-elev-2), var(--bg-elev) 132px);
  box-shadow: inset -1px 0 0 rgb(var(--shadow-rgb) / 0.22);
  border-right: 1px solid var(--border);
  transition: width var(--dur-slow) var(--ease), transform var(--dur-slow) var(--ease), left var(--dur-slow) var(--ease), background var(--dur-slow) var(--ease);
  z-index: var(--z-sidebar);
}
.side-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--sp-4) var(--sp-4) var(--sp-2);
}
.logo {
  /* No flex gap: the bare "Kilgore" text node is its own flex item, so a gap
     here splits the wordmark into "Kilgore AI". Space the image instead. */
  display: flex; align-items: center;
  font-size: var(--fs-lg); font-weight: var(--wt-bold); letter-spacing: -0.02em;
}
.logo img { width: 28px; height: 28px; border-radius: var(--r-sm); flex: none; margin-right: var(--sp-2); }
.logo span { color: var(--accent); transition: color var(--dur); }
.logo-link {
  display: flex; align-items: center;
  color: inherit; text-decoration: none;
  border-radius: var(--r-sm); transition: opacity var(--dur-fast);
}
.logo-link:hover { opacity: 0.85; }
.icon-btn {
  background: none; border: none; color: var(--text-faint);
  cursor: pointer; font-size: var(--fs-sm); border-radius: var(--r-xs);
  width: 28px; height: 28px; display: grid; place-items: center;
  transition: background var(--dur-fast), color var(--dur-fast);
}
.icon-btn:hover { background: var(--bg-hover); color: var(--text); }
.icon-btn:active { transform: scale(0.92); }
#side-close { display: none; }

.new-btn {
  display: flex; align-items: center; gap: var(--sp-2);
  margin: var(--sp-1) var(--sp-3) var(--sp-2-5); padding: var(--sp-2) var(--sp-3);
  background: var(--accent-soft); color: var(--accent-strong);
  border: 1px solid var(--accent-border); border-radius: var(--r-sm);
  font: var(--wt-semi) var(--fs-sm) var(--font); cursor: pointer;
  transition: background var(--dur), border-color var(--dur), color var(--dur),
    box-shadow var(--dur) var(--ease);
}
.new-btn:hover {
  background: rgb(var(--accent-rgb) / 0.2);
  border-color: var(--accent-border-strong);
  box-shadow: 0 0 0 2px rgb(var(--accent-rgb) / 0.08);
}
.new-btn:active { transform: translateY(1px); }
.new-btn svg { flex: none; }

.search-box { margin: 0 var(--sp-3) var(--sp-2-5); position: relative; }
.search-box svg {
  position: absolute; left: 9px; top: 50%; transform: translateY(-50%);
  color: var(--text-faint); pointer-events: none;
}
.search-box input {
  width: 100%; background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: var(--sp-2) var(--sp-2-5) var(--sp-2) var(--sp-8); font: var(--fs-xs) var(--font);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.search-box input::placeholder { color: var(--text-faint); }
.search-box input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgb(var(--accent-rgb) / 0.14);
}
.search-box input:focus + svg,
.search-box:has(input:focus) svg { color: var(--accent-strong); }

.side-btn {
  display: flex; align-items: center; gap: var(--sp-2);
  margin: 0 var(--sp-3) var(--sp-2-5); padding: var(--sp-2) var(--sp-3);
  background: none; color: var(--text-dim);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  font: var(--wt-semi) var(--fs-sm) var(--font); cursor: pointer;
  transition: background var(--dur), border-color var(--dur), color var(--dur);
}
.side-btn:hover {
  background: var(--bg-hover); color: var(--text); border-color: var(--border-strong);
}
.side-btn:active { transform: translateY(1px); }
.side-btn svg { flex: none; }

.side-label {
  padding: var(--sp-1) var(--sp-4) var(--sp-1-5); font-size: var(--fs-micro); font-weight: var(--wt-semi);
  letter-spacing: var(--tr-label); text-transform: uppercase; color: var(--text-faint);
}
.side-label.pinned-label {
  display: flex; align-items: center; gap: var(--sp-1-5);
}
.side-label.pinned-label svg { color: var(--warn); }
.conv-list { flex: 1; overflow-y: auto; padding: var(--sp-05) var(--sp-2) var(--sp-2); }
.conv-item {
  position: relative; display: flex; align-items: center; gap: var(--sp-1-5);
  padding: var(--sp-2) var(--sp-2-5); border-radius: var(--r-sm);
  cursor: pointer; border: 1px solid transparent;
  transition: background var(--dur-fast);
}
.conv-item:hover { background: var(--bg-hover); }
.conv-item.active {
  background: linear-gradient(90deg, rgb(var(--accent-rgb) / 0.10), rgb(var(--accent-rgb) / 0.04) 55%, transparent);
  border-color: var(--accent-border);
  box-shadow: inset 0 1px 0 rgb(var(--accent-rgb) / 0.08);
}
.conv-item.active::before {
  content: ""; position: absolute; left: -1px; top: 8px; bottom: 8px;
  width: 3px; border-radius: var(--r-2xs); background: var(--accent);
  box-shadow: 0 0 8px rgb(var(--accent-rgb) / 0.45);
}
.conv-main { flex: 1; min-width: 0; }
.conv-item .conv-title {
  font-size: var(--fs-sm); color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.conv-item.active .conv-title { color: var(--text); }
/* Live dot: this conversation is generating in the background. */
.conv-item.conv-streaming .conv-title { color: var(--text); }
.conv-item .conv-live-dot {
  position: absolute; right: 10px; top: 50%; margin-top: -3px;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent);
  animation: live-pulse 1.2s ease-in-out infinite;
}
@keyframes live-pulse {
  0%, 100% { opacity: 0.35; transform: scale(0.85); }
  50% { opacity: 1; transform: scale(1.1); }
}
.conv-meta { font-size: var(--fs-micro); color: var(--text-faint); margin-top: var(--sp-05); font-variant-numeric: tabular-nums; }
/* Kept in flow and faded instead of display:none, so revealing the actions on
   hover does not re-lay out the title next to them. */
.conv-actions {
  display: flex; align-items: center; gap: var(--sp-1); flex: none;
  opacity: 0; visibility: hidden; transform: translateX(4px);
  /* Same grace delay as message actions: icons revealed on hover must not
     vanish while the cursor is still travelling toward them. */
  transition: opacity var(--dur-fast) linear 200ms,
              transform var(--dur-fast) ease 200ms,
              visibility 0s linear calc(200ms + var(--dur-fast));
}
.conv-item:hover .conv-actions,
.conv-item:focus-within .conv-actions {
  opacity: 1; visibility: visible; transform: none;
}
/* Hover targets stay small (26px) but the clickable area does not: the
   ::after sheet stretches the hit region without changing the visual size
   or the row layout, so neighbouring icons stay far enough apart to
   mis-click-proof the destructive one. */
.conv-actions .icon-btn { width: 26px; height: 26px; position: relative; }
.conv-actions .icon-btn::after {
  content: ""; position: absolute; inset: -5px; border-radius: 50%;
}
.conv-actions .icon-btn:hover { color: var(--accent-strong); }
.conv-actions .icon-btn.danger:hover { color: var(--err); background: var(--err-soft); }
.conv-actions .icon-btn.pinned { color: var(--warn); }
.conv-actions .icon-btn.confirming { color: var(--err); background: var(--err-soft); font-size: var(--fs-label); width: auto; padding: 0 var(--sp-1-5); }
.rename-input {
  width: 100%; background: var(--bg); color: var(--text);
  border: 1px solid var(--accent); border-radius: var(--r-xs);
  padding: var(--sp-1) var(--sp-1-5); font: var(--fs-sm) var(--font);
}
.side-foot {
  padding: var(--sp-2-5) var(--sp-3); border-top: 1px solid var(--border);
  font-size: var(--fs-micro); color: var(--text-faint);
  display: flex; flex-direction: column; gap: var(--sp-2);
}
.side-foot .side-row { display: flex; align-items: center; gap: var(--sp-2); }
.side-foot .dot { width: 7px; height: 7px; }
.side-foot .theme-panel, .side-foot .lang-panel {
  top: auto; bottom: calc(100% + 8px);
  left: 0; right: auto;
  max-width: calc(var(--sidebar-w) - 24px);
}
.side-empty {
  padding: var(--sp-6) var(--sp-4); text-align: center; color: var(--text-faint); font-size: var(--fs-sm);
}

.scrim {
  position: fixed; inset: 0; background: rgb(var(--shadow-rgb) / 0.55);
  opacity: 0; pointer-events: none; transition: opacity var(--dur) var(--ease); z-index: var(--z-scrim);
}

/* ============ main ============ */
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; height: 100vh; height: 100dvh; position: relative; }
/* iOS keyboard: visualViewport JS feeds --app-h because dvh ignores the
   keyboard — without this the composer hides behind it. The var is unset on
   first paint, so the dvh value above stays the fallback. */
.main { height: var(--app-h, 100dvh); }

/* ============ touch & scroll hygiene ============
   No grey flash on every tap (reads as web, not app), no double-tap zoom
   delay on controls, and scroll containment so flicking the transcript never
   chains into the page behind it or triggers pull-to-refresh mid-read. */
html { -webkit-tap-highlight-color: transparent; }
body { overscroll-behavior-y: contain; }
button, a, label, select, summary,
[role="option"], [role="menuitem"], [role="tab"], [role="listbox"] {
  touch-action: manipulation;
}
#msgs, .conv-list, .model-panel, .theme-panel, .lang-panel {
  overscroll-behavior: contain;
}

.topbar {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-5); border-bottom: 1px solid var(--border);
  background: var(--bg);
  /* Lifts the bar off the transcript the moment content starts moving under it. */
  box-shadow: 0 6px 20px -12px rgb(var(--shadow-rgb) / 0.5);
  transition: background var(--dur) ease;
}
.menu-btn { display: none; }
.topbar .conv-title {
  flex: 1; min-width: 0;
  font-size: var(--fs-h3); font-weight: var(--wt-semi); letter-spacing: var(--tr-h3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.topbar-right { display: flex; align-items: center; gap: var(--sp-2); }

/* .dot / @keyframes pulse now live in components.css */

/* ============ topbar dropdowns (model / theme / settings) ============ */
.model-dd, .theme-dd, .sett-dv, .lang-dd { position: relative; }
.model-btn {
  display: flex; align-items: center; gap: var(--sp-2);
  background: var(--bg-elev); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: var(--sp-1) var(--sp-2-5) var(--sp-1) var(--sp-1-5); font: var(--wt-medium) var(--fs-xs) var(--font);
  cursor: pointer; white-space: nowrap;
  transition: border-color var(--dur), background var(--dur);
}
.model-btn:hover { border-color: var(--border-strong); background: var(--bg-hover); }
/* Focus ring comes from the global :focus-visible contract; the accent border
   here only marks the open state. */
.model-btn.open { border-color: var(--accent); }
.model-btn .m-chev { color: var(--text-faint); flex: none; transition: transform var(--dur); }
.model-btn.open .m-chev { transform: rotate(180deg); }
#model-btn-name {
  max-width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.m-logo { border-radius: var(--r-xs); flex: none; object-fit: contain; }
.model-btn .m-logo { width: 16px; height: 16px; }
.m-opt .m-logo, .m-group-head img { filter: drop-shadow(0 0 0 transparent); }
/* Marks that render black need a light plate; marks baked white need a dark
   one. kimi.svg is fill="#fff", so a light plate hid it in every theme. */
.m-logo.onlight, .m-logo.ondark { padding: var(--sp-1); border-radius: var(--r-xs); box-sizing: content-box; }
.m-logo.onlight { background: var(--logo-plate); }
.m-logo.ondark { background: var(--logo-plate-dark); }

.model-panel {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: var(--z-dropdown);
  width: min(300px, calc(100vw - 2 * var(--sp-6))); max-height: 380px; overflow-y: auto;
  background: var(--bg-elev); border: 1px solid var(--border-strong);
  border-radius: var(--r-md); box-shadow: var(--e3);
  padding: var(--sp-1-5); display: none;
  animation: ddIn var(--dur-fast) ease;
}
@keyframes ddIn {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: none; }
}
.model-panel.open { display: block; }
.m-group-head {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%;
  /* sticky inside the scrolling model list — the group name stays visible
     while long catalogs scroll beneath it (desktop dropdown + phone sheet). */
  position: sticky; top: calc(var(--sp-1-5) * -1); z-index: var(--z-inlayer);
  padding: var(--sp-2) var(--sp-2-5) var(--sp-1-5); font-size: var(--fs-label); font-weight: var(--wt-bold);
  letter-spacing: var(--tr-label); text-transform: uppercase; color: var(--text-faint);
  background: var(--bg-elev); border: none; cursor: pointer; border-radius: var(--r-xs);
  text-align: left;
}
.m-group-head:hover { color: var(--text-dim); background: var(--bg-hover); }
.m-group-head img { width: 14px; height: 14px; flex: none; }
.m-group-head .m-chev { flex: none; transition: transform var(--dur); color: var(--text-faint); }
.m-group-head .m-gname { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.m-group-head .m-gcount {
  font: var(--fs-label) var(--mono); color: var(--text-faint); background: var(--bg);
  border: 1px solid var(--border); border-radius: var(--r-pill); padding: 0 var(--sp-2);
  font-variant-numeric: tabular-nums;
}
.m-group-body { display: block; }
.m-group.collapsed .m-group-head .m-chev { transform: rotate(-90deg); }
.m-group.collapsed .m-group-body { display: none; }
.m-opt {
  display: flex; align-items: center; gap: var(--sp-2-5); width: 100%;
  background: none; border: none; border-radius: var(--r-sm); padding: var(--sp-2) var(--sp-2-5);
  cursor: pointer; text-align: left; color: var(--text);
  transition: background var(--dur-fast), box-shadow var(--dur) var(--ease);
}
.m-opt:hover { background: var(--bg-hover); box-shadow: inset 0 0 0 1px var(--border-strong); }
.m-opt.selected { background: var(--accent-soft); }
.m-opt .m-logo { width: 22px; height: 22px; }
.m-opt .m-body { flex: 1; min-width: 0; }
.m-opt .m-name { font-size: var(--fs-sm); font-weight: var(--wt-medium); line-height: 1.3; }
.m-opt .m-id { font: var(--fs-micro) var(--mono); color: var(--text-faint); }
.m-opt .m-check {
  color: var(--accent-strong); font-size: var(--fs-sm); flex: none;
  opacity: 0; transform: scale(0.6); transition: opacity var(--dur-fast), transform var(--dur-fast);
}
.m-opt.selected .m-check { opacity: 1; transform: scale(1); }
.model-panel .m-empty { padding: var(--sp-3-5); text-align: center; color: var(--text-faint); font-size: var(--fs-xs); }

/* theme switcher */
.theme-btn {
  display: grid; place-items: center;
  width: 38px; height: 38px; border-radius: var(--r-sm);
  background: var(--bg-elev); border: 1px solid var(--border-strong);
  color: var(--text-dim); cursor: pointer;
  transition: color var(--dur-fast), border-color var(--dur-fast), background var(--dur-fast),
    box-shadow var(--dur) var(--ease);
}
.theme-btn svg { width: 18px; height: 18px; }
.theme-btn:hover { color: var(--text); border-color: var(--accent-border); background: var(--bg-hover); box-shadow: 0 0 0 2px rgb(var(--accent-rgb) / 0.08); }
.theme-btn:active { transform: scale(0.94); }
.theme-btn.open { border-color: var(--accent); color: var(--accent-strong); }
.theme-panel {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: var(--z-dropdown);
  width: min(230px, calc(100vw - 2 * var(--sp-6))); background: var(--bg-elev); border: 1px solid var(--border-strong);
  border-radius: var(--r-md); box-shadow: var(--e3); padding: var(--sp-1-5); display: none;
  animation: ddIn var(--dur-fast) ease;
}
.theme-panel.open { display: block; }
.theme-opt {
  display: flex; align-items: center; gap: var(--sp-2-5); width: 100%;
  background: none; border: none; border-radius: var(--r-sm); padding: var(--sp-2) var(--sp-2-5);
  cursor: pointer; text-align: left; color: var(--text); font: var(--fs-sm) var(--font);
  transition: background var(--dur-fast);
}
.theme-opt:hover { background: var(--bg-hover); }
.theme-opt .swatch {
  width: 22px; height: 22px; border-radius: var(--r-xs); flex: none;
  border: 1px solid var(--border-strong);
}
.theme-opt .t-name { flex: 1; }
.theme-opt .t-check { color: var(--accent-strong); opacity: 0; font-size: var(--fs-xs); }
.theme-opt.selected .t-check { opacity: 1; }

/* settings entry (sidebar foot) — same shape as the old "more" icon button */
.more-btn {
  display: grid; place-items: center;
  width: 38px; height: 38px; border-radius: var(--r-sm);
  background: var(--bg-elev); border: 1px solid var(--border-strong);
  color: var(--text-dim); cursor: pointer;
  transition: color var(--dur-fast), border-color var(--dur-fast), background var(--dur-fast);
}
.more-btn svg { width: 18px; height: 18px; }
.more-btn:hover { color: var(--text); border-color: var(--accent-border); background: var(--bg-hover); }
.more-btn:active { transform: scale(0.94); }
.more-btn.open { border-color: var(--accent); color: var(--accent-strong); }
.donate-btn {
  display: grid; place-items: center;
  width: 38px; height: 38px; border-radius: var(--r-sm);
  background: var(--bg-elev); border: 1px solid var(--border-strong);
  color: var(--text-dim); cursor: pointer;
  transition: color var(--dur-fast), border-color var(--dur-fast), background var(--dur-fast), transform var(--dur-fast) var(--ease);
}
.donate-btn svg { width: 18px; height: 18px; }
.donate-btn:hover { color: var(--accent-strong); border-color: var(--accent-border); background: var(--bg-hover); }
.donate-btn:active { transform: scale(0.94); }

/* ============ settings modal (tabbed) ============
   Two columns: a vertical tab rail on the left (active tab gets a left
   accent bar + raised background, as in the reference shot) and the panel
   body on the right. Everything reads from the shared tokens, so every
   theme and the `paper` light theme work without extra rules. */
.modal.settings-modal {
  width: min(860px, 100%);
  max-height: min(86dvh, 720px);
  padding: 0;
  display: grid;
  grid-template-columns: 208px 1fr;
  /* The implicit row is auto-sized, i.e. as tall as the CONTENT. That made
     .settings-body a content-height box, so its own overflow-y:auto never
     engaged and this overflow:hidden clipped whatever spilled past the
     modal — providers 7+ and the Add button became unreachable, with no
     scrollbar anywhere to reach them. minmax(0,1fr) pins the row to the
     modal's max-height instead. */
  grid-template-rows: minmax(0, 1fr);
  overflow: hidden;
}
.settings-side {
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--sp-4) var(--sp-3);
  background: var(--bg);
  border-right: 1px solid var(--border);
  overflow-y: auto;
}
.set-tab {
  position: relative;
  display: flex; align-items: center; gap: var(--sp-2-5);
  width: 100%; padding: var(--sp-2-5) var(--sp-3);
  background: none; border: none; border-radius: var(--r-sm);
  color: var(--text-dim); cursor: pointer; text-align: left;
  font: var(--wt-semi) var(--fs-sm) var(--font);
  transition: background var(--dur-fast), color var(--dur-fast);
}
.set-tab svg { width: 16px; height: 16px; flex: none; color: var(--text-faint); }
.set-tab:hover { background: var(--bg-hover); color: var(--text); }
.set-tab.active { background: var(--bg-elev-2); color: var(--text); }
.set-tab.active::before {
  content: ""; position: absolute; left: 0; top: 50%;
  width: 3px; height: 60%; border-radius: var(--r-2xs);
  background: var(--accent); transform: translateY(-50%);
}
.set-tab.active svg { color: var(--accent-strong); }
.settings-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
.settings-head {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5) var(--sp-3);
  border-bottom: 1px solid var(--border);
}
.settings-head h3 { flex: 1; margin: 0; font-size: var(--fs-h3); letter-spacing: var(--tr-h3); }
.settings-head .lb-close {
  flex: none; display: grid; place-items: center;
  width: 30px; height: 30px; border-radius: var(--r-sm);
  background: none; border: none; color: var(--text-faint); cursor: pointer;
  transition: color var(--dur-fast), background var(--dur-fast);
}
.settings-head .lb-close:hover { color: var(--text); background: var(--bg-hover); }
.settings-body { flex: 1; overflow-y: auto; padding: var(--sp-4) var(--sp-5) var(--sp-5); }
.settings-body > h3 { margin: 0 0 var(--sp-1); font-size: var(--fs-h4); letter-spacing: var(--tr-h3); }
.settings-body .set-desc { margin: 0 0 var(--sp-4); font-size: var(--fs-xs); color: var(--text-faint); }

/* section label + row box */
.set-section {
  font-size: var(--fs-label); font-weight: var(--wt-semi); letter-spacing: var(--tr-label);
  text-transform: uppercase; color: var(--text-faint);
  margin: var(--sp-4) 0 var(--sp-2); user-select: none;
}
.set-section:first-of-type { margin-top: 0; }
.set-box {
  border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--bg-elev); overflow: hidden;
}
.set-row {
  display: flex; align-items: center; gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
}
.set-row + .set-row { border-top: 1px solid var(--border); }
.set-row-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.set-row-txt b { font: var(--wt-semi) var(--fs-sm) var(--font); color: var(--text); }
.set-row-txt small { font: var(--fs-xs) var(--font); color: var(--text-faint); }

/* controls */
.set-seg {
  display: inline-flex; flex: none; padding: 3px; gap: 2px;
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--r-sm);
}
.set-seg-btn {
  padding: var(--sp-1-5) var(--sp-3); border: none; border-radius: 6px;
  background: none; color: var(--text-dim); cursor: pointer;
  font: var(--wt-semi) var(--fs-xs) var(--font); white-space: nowrap;
  transition: background var(--dur-fast), color var(--dur-fast);
}
.set-seg-btn:hover { color: var(--text); }
.set-seg-btn.on { background: var(--bg-elev-2); color: var(--text); box-shadow: var(--e1); }
.set-seg-btn.set-keys-btn { background: var(--accent); color: var(--on-accent); }
.set-seg-btn.set-keys-btn:hover { background: var(--accent-strong); color: var(--on-accent); }
.set-seg-btn.set-primary { background: var(--accent); color: var(--on-accent); }
.set-seg-btn.set-primary:hover { background: var(--accent-strong); color: var(--on-accent); }
.set-file {
  font-size: var(--fs-sm); color: var(--text-dim);
  max-width: 100%; overflow: hidden;
}
.set-file::file-selector-button {
  font: inherit; color: var(--text);
  background: var(--bg-elev-2); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--sp-1-5) var(--sp-3);
  margin-right: var(--sp-2); cursor: pointer;
}
.set-seg-btn.set-danger { color: var(--err); }
.set-seg-btn.set-danger:hover { background: var(--err-soft); }
.set-seg-btn:disabled { opacity: 0.55; cursor: default; }

/* Advanced tab — one collapsible card per custom provider.
   The header row is the whole point: name, endpoint, model count and
   on/off fit on one line, so N providers stay comparable without
   scrolling and the common actions (glance, disable, delete) never
   need the form open. */
.set-prov + .set-prov { margin-top: var(--sp-2); }
/* The add button lives outside the card list (paint() clears that one),
   so it carries its own top gap instead of a sibling margin. */
.set-prov-add { margin-top: var(--sp-2); }
.set-prov { padding: 0; overflow: hidden; }
.set-prov-top {
  display: flex; align-items: center; gap: var(--sp-2);
  padding-right: var(--sp-3-5); padding-left: 0;
}
.set-prov-head {
  flex: 1; min-width: 0; display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-1) var(--sp-3) var(--sp-3-5);
  background: none; border: 0; color: inherit; text-align: left; cursor: pointer;
}
.set-prov-head:hover .set-prov-id { color: var(--accent); }
.set-prov-id {
  font: var(--wt-semi) var(--fs-sm) var(--font); color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 40%;
}
.set-prov-sub {
  flex: 1; min-width: 0; font: var(--wt-regular) var(--fs-xs) var(--mono);
  color: var(--text-faint); white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; direction: ltr;
}
.set-prov-count {
  flex: none; min-width: 20px; padding: 0 var(--sp-1-5); border-radius: 999px;
  background: var(--bg-elev-2); color: var(--text-dim);
  font: var(--wt-semi) var(--fs-micro) var(--font); text-align: center;
}
.set-prov-state { flex: none; font: var(--wt-medium) var(--fs-micro) var(--font); }
.set-prov-state[data-on="1"] { color: var(--ok); }
.set-prov-state[data-on="0"] { color: var(--text-faint); }
.set-acc-chev {
  flex: none; color: var(--text-faint); transition: transform var(--dur-fast);
}
.set-prov.open .set-acc-chev { transform: rotate(180deg); }

/* Body is display:none until opened rather than a height transition: the
   content is a form whose height changes as chips are added, and animating
   an unknown height is the usual source of a stuck half-open card. */
.set-prov-body { display: none; flex-direction: column; gap: var(--sp-3); padding: 0 var(--sp-3-5) var(--sp-3-5); }
.set-prov.open .set-prov-body { display: flex; }
.set-prov-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2-5); }
.set-prov-grid > .set-prov-field:first-child { grid-column: 1 / -1; }
.set-prov-field { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.set-prov-field > span, .set-prov-lbl {
  font: var(--wt-medium) var(--fs-micro) var(--font); letter-spacing: var(--tr-micro);
  color: var(--text-faint); text-transform: uppercase;
}
/* .set-input carries flex:none + min-width:180px + max-width:240px for the
   .set-row control column; a grid cell here must fill instead, or every
   field renders at a fixed 180px and spills out of its cell. Scoped under
   .set-prov-body so it outranks the later, single-class .set-input rule —
   at equal specificity the later one wins. */
.set-prov-body .set-prov-in { flex: 1 1 auto; min-width: 0; max-width: none; width: 100%; }

.set-prov-models { display: flex; flex-direction: column; gap: var(--sp-1); }
/* The model list is the one part of the card with no natural ceiling — a
   self-hosted box can advertise hundreds — so it scrolls inside the card
   instead of pushing Save and the Options block off the bottom. */
.set-prov-mlist {
  display: flex; flex-direction: column; gap: 2px;
  max-height: 232px; overflow-y: auto;
}
.set-prov-mrow {
  border: 1px solid var(--border); border-radius: var(--r-xs);
  background: var(--bg-elev-2);
}
.set-prov-mrow.open { border-color: var(--accent); }
.set-prov-mtop { display: flex; align-items: center; }
.set-prov-mhead {
  display: flex; align-items: center; gap: var(--sp-2); flex: 1 1 auto; min-width: 0;
  padding: var(--sp-1) var(--sp-2); background: none; border: 0; cursor: pointer;
  text-align: left; color: var(--text-dim); font: var(--fs-xs) var(--mono);
}
.set-prov-mhead:hover { color: var(--text); }
.set-prov-mlogo {
  flex: none; width: 16px; height: 16px; border-radius: 3px; object-fit: contain;
}
.set-prov-mid { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The display name is the exception to "the id is the thing you read":
   it is what the picker shows, so it earns the second column. */
.set-prov-mname {
  flex: none; max-width: 42%; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; color: var(--text-faint); font-family: var(--font);
}
.set-prov-mbody {
  display: none; flex-direction: column; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-2) var(--sp-2-5); border-top: 1px solid var(--border);
}
.set-prov-mrow.open .set-prov-mbody { display: flex; }
.set-prov-mgrid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2); }
.set-prov-mgrid > .set-prov-field:first-child { grid-column: 1 / -1; }
.set-prov-logo-row { display: flex; align-items: center; gap: var(--sp-2); }
.set-prov-logo-prev {
  flex: none; width: 30px; height: 30px; border-radius: var(--r-xs);
  border: 1px solid var(--border); background: var(--bg); object-fit: contain;
}
.set-prov-x {
  flex: none; width: 20px; height: 20px; margin-right: var(--sp-1);
  line-height: 1; border: 0; border-radius: 50%;
  background: none; color: var(--text-faint); cursor: pointer; font-size: 14px;
}
.set-prov-x:hover { background: var(--err-soft); color: var(--err); }
.set-prov-entry { flex: 1 1 160px; min-width: 120px; max-width: none; }

/* ---- Options (sampling defaults, timeout, headers) ---- */
.set-prov-opt { border: 1px solid var(--border); border-radius: var(--r-sm); }
.set-prov-opt-head {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%;
  padding: var(--sp-1-5) var(--sp-2); background: none; border: 0; cursor: pointer;
  color: var(--text-faint); font: var(--wt-medium) var(--fs-micro) var(--font);
  letter-spacing: var(--tr-micro); text-transform: uppercase;
}
.set-prov-opt-head:hover { color: var(--text-dim); }
.set-prov-opt.open .set-acc-chev { transform: rotate(180deg); }
.set-prov-opt-body {
  display: none; flex-direction: column; gap: var(--sp-2-5);
  padding: 0 var(--sp-2) var(--sp-2-5);
}
.set-prov-opt.open .set-prov-opt-body { display: flex; }
/* auto-fit rather than a fixed count: four numeric fields in a 2x2 grid at
   every width beats a row that reflows into a ragged 3+1. */
.set-prov-num {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
  gap: var(--sp-2);
}
.set-prov-hdr { display: flex; flex-direction: column; gap: var(--sp-1); }
.set-prov-hdr-row { display: flex; align-items: center; gap: var(--sp-1); }
.set-prov-hdr-row .set-input { flex: 1 1 auto; min-width: 0; max-width: none; }
.set-prov-hdr-add { align-self: flex-start; }

.set-prov-foot {
  display: flex; align-items: center; gap: var(--sp-2);
  padding-top: var(--sp-1); border-top: 1px solid var(--border);
}
/* Text label rides the tooltip, not a second line: the header row is the
   one place where vertical space is scarce. */
.set-prov-acts { display: flex; align-items: center; gap: var(--sp-2); flex: none; }
.set-prov-en { display: inline-flex; align-items: center; gap: var(--sp-2); cursor: pointer; flex: none; }
.set-prov-en > span { display: none; }

@media (max-width: 640px) {
  .set-prov-grid { grid-template-columns: 1fr; }
  .set-prov-id { max-width: 60%; }
}
.set-select {
  flex: none; min-width: 180px; max-width: 240px;
  background: var(--bg); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--sp-2) var(--sp-2-5);
  font: var(--fs-xs) var(--font); cursor: pointer;
}
.set-select:focus { border-color: var(--accent); outline: none; }
.set-input {
  flex: none; min-width: 180px; max-width: 240px;
  background: var(--bg); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--sp-2) var(--sp-2-5);
  font: var(--fs-xs) var(--font);
}
.set-input:focus { border-color: var(--accent); outline: none; }
.set-toggle {
  flex: none; position: relative;
  width: 40px; height: 23px; padding: 0;
  border: 1px solid var(--border-strong); border-radius: var(--r-pill);
  background: var(--bg-elev-2); cursor: pointer;
  transition: background var(--dur), border-color var(--dur);
}
.set-toggle::after {
  content: ""; position: absolute; top: 50%; left: 2px;
  width: 17px; height: 17px; border-radius: 50%;
  background: var(--text-faint);
  transform: translateY(-50%);
  transition: transform var(--dur) var(--ease), background var(--dur);
}
.set-toggle.on { background: var(--accent); border-color: transparent; }
.set-toggle.on::after { transform: translate(17px, -50%); background: var(--on-accent); }
.set-toggle:focus-visible { box-shadow: 0 0 0 3px var(--accent-focus-ring); }

/* theme swatches */
.set-themes {
  display: flex; flex-wrap: wrap; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4) var(--sp-3);
}
.set-themes + .set-row { border-top: 1px solid var(--border); }
.set-swatch {
  display: inline-flex; align-items: center; gap: var(--sp-1-5);
  padding: 5px var(--sp-2-5) 5px 5px;
  border: 1px solid var(--border); border-radius: var(--r-pill);
  background: var(--bg); color: var(--text-dim); cursor: pointer;
  font: var(--fs-xs) var(--font); white-space: nowrap;
  transition: border-color var(--dur-fast), color var(--dur-fast);
}
.set-swatch:hover { color: var(--text); border-color: var(--accent-border); }
.set-swatch.on { border-color: var(--accent); color: var(--text); }
.set-swatch-bg {
  width: 16px; height: 16px; border-radius: 50%;
  border: 1px solid rgb(0 0 0 / 0.18);
}

/* instruction persona chips */
.set-chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-bottom: var(--sp-4); }
.set-chip {
  padding: var(--sp-1-5) var(--sp-3);
  border: 1px solid var(--border-strong); border-radius: var(--r-pill);
  background: var(--bg-elev); color: var(--text-dim); cursor: pointer;
  font: var(--wt-semi) var(--fs-xs) var(--font);
  transition: background var(--dur-fast), color var(--dur-fast), border-color var(--dur-fast);
}
.set-chip:hover { color: var(--text); border-color: var(--accent-border); }
.set-chip.on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-strong); }
.set-textarea {
  display: block; width: 100%; min-height: 130px; resize: vertical;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: var(--sp-3); font: var(--fs-sm) var(--font); line-height: 1.55;
}
.set-textarea:focus { border-color: var(--accent); outline: none; }

/* model picker inside the settings box — one collapsible group per model
   kind. The header always shows the picked model, so a closed group still
   says what it holds, and the 30-row chat catalog can't bury the two
   pickers below it. */
.set-acc-head {
  display: flex; align-items: center; gap: var(--sp-2-5); width: 100%;
  padding: var(--sp-2-5) var(--sp-3); cursor: pointer; text-align: left;
  background: none; border: none; color: var(--text);
  font: var(--wt-medium) var(--fs-sm) var(--font);
  transition: background var(--dur-fast);
}
.set-acc-head:hover { background: var(--bg-hover); }
.set-acc-cur { flex: 1; min-width: 0; display: flex; align-items: center; gap: var(--sp-2); }
.set-acc-cur > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.set-acc-cur .m-logo { width: 18px; height: 18px; }
.set-acc-chev { flex: none; color: var(--text-faint); transition: transform var(--dur); }
.set-acc.open .set-acc-chev { transform: rotate(180deg); }
.set-acc-body { display: none; border-top: 1px solid var(--border); max-height: 320px; overflow-y: auto; }
.set-acc.open .set-acc-body { display: block; }
.set-mopt {
  display: flex; align-items: center; gap: var(--sp-2-5); width: 100%;
  background: none; border: none; border-radius: var(--r-sm);
  padding: var(--sp-2) var(--sp-3); cursor: pointer; text-align: left; color: var(--text);
  transition: background var(--dur-fast);
}
.set-mopt:hover { background: var(--bg-hover); }
.set-mopt.on { background: var(--accent-soft); }
.set-mopt .m-logo { width: 20px; height: 20px; }
.set-mopt .m-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.set-mopt .m-name { font-size: var(--fs-sm); font-weight: var(--wt-medium); line-height: 1.3; }
.set-mopt .m-id { font: var(--fs-micro) var(--mono); color: var(--text-faint); }
.set-mopt .m-check {
  color: var(--accent-strong); font-size: var(--fs-sm); flex: none;
  opacity: 0; transform: scale(0.6); transition: opacity var(--dur-fast), transform var(--dur-fast);
}
.set-mopt.on .m-check { opacity: 1; transform: scale(1); }
.set-mopt + .set-mopt { border-top: 1px solid var(--border); }
.set-box > .m-empty { padding: var(--sp-3-5); text-align: center; color: var(--text-faint); font-size: var(--fs-xs); }

/* rows that carry a whole v-row / p-row (range, radio, avatar) */
.set-row .v-row, .set-row .p-row { flex: 1; border: none; padding: 0; background: none; }
.set-row .v-row input[type="range"] { max-width: 220px; }

/* phones: the two-column layout stops fitting well before the sheet kicks in,
   so drop to a single column with a horizontal, scrolling tab strip. */
@media (max-width: 700px) {
  .modal.settings-modal { grid-template-columns: 1fr; max-height: min(90dvh, 760px); }
  .settings-side {
    flex-direction: row; gap: var(--sp-1);
    padding: var(--sp-2) var(--sp-3);
    border-right: none; border-bottom: 1px solid var(--border);
    overflow-x: auto; overflow-y: hidden;
  }
  .set-tab { flex: none; padding: var(--sp-2) var(--sp-2-5); }
  .set-tab.active::before { left: 50%; top: auto; bottom: 0; width: 60%; height: 2px; transform: translateX(-50%); }
  .settings-body { padding: var(--sp-3) var(--sp-4) var(--sp-5); }
  .set-row { flex-wrap: wrap; gap: var(--sp-2); }
  .set-select { min-width: 0; width: 100%; max-width: none; }
}
@media (prefers-reduced-motion: reduce) {
  .set-toggle::after { transition: none; }
}

/* language switcher */
.lang-btn {
  display: grid; place-items: center;
  width: 38px; height: 38px; border-radius: var(--r-sm);
  background: var(--bg-elev); border: 1px solid var(--border-strong);
  color: var(--text-dim); cursor: pointer;
  transition: color var(--dur-fast), border-color var(--dur-fast), background var(--dur-fast);
}
.lang-code {
  font: var(--wt-bold) var(--fs-xs) var(--font); letter-spacing: var(--tr-label);
  padding-top: var(--sp-05); line-height: 1;
}
.lang-btn:hover { color: var(--text); border-color: var(--accent-border); background: var(--bg-hover); }
.lang-btn.open { border-color: var(--accent); color: var(--accent-strong); }
.lang-panel {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: var(--z-dropdown);
  width: 150px; background: var(--bg-elev); border: 1px solid var(--border-strong);
  border-radius: var(--r-md); box-shadow: var(--e3); padding: var(--sp-1-5); display: none;
  animation: ddIn var(--dur-fast) ease;
}
.lang-panel.open { display: block; }
.lang-opt {
  display: flex; align-items: center; width: 100%;
  background: none; border: none; border-radius: var(--r-sm); padding: var(--sp-2) var(--sp-2-5);
  cursor: pointer; text-align: left; color: var(--text); font: var(--fs-sm) var(--font);
  transition: background var(--dur-fast);
}
.lang-opt:hover { background: var(--bg-hover); }
.lang-opt.selected { color: var(--accent-strong); background: var(--accent-soft); }
.lang-opt.selected::after { content: "✓"; margin-left: auto; color: var(--accent-strong); font-size: var(--fs-xs); }

/* ============ messages ============ */
.msgs {
  flex: 1; overflow-y: auto; scroll-behavior: smooth;
  padding: var(--sp-6) 0 var(--sp-2);
}
.msgs-inner { max-width: var(--content-w); margin: 0 auto; padding: 0 var(--sp-6); display: flex; flex-direction: column; gap: var(--sp-6); }

/* welcome-area ad slot (consent-gated; zero-height until adsense.js boots) */
.ad-welcome {
  flex: none;
  max-width: var(--content-w);
  margin: 0 auto;
  padding: 0 var(--sp-6) var(--sp-2);
  width: 100%;
  box-sizing: border-box;
}

/* sidebar ad slot below the conversation list */
.ad-sidebar {
  flex: none;
  display: flex; justify-content: center;
  padding: var(--sp-2) var(--sp-2);
  border-top: 1px solid var(--border);
}
.ads-on .ad-sidebar ins.adsbygoogle { max-width: 300px; }

/* Support-level side rails (index only): 300x600 units hugging the viewport
   edges, vertically centered. Off unless adsense.js booted (html.ads-on) AND
   the visitor picked "more ads" (html.ads-more) AND the viewport is wide
   enough that a 300px rail clears the 800px chat column (media queries
   below; the flow sidebar width decides how wide "wide enough" is). */
.ad-rail {
  display: none;
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  width: 300px;
  flex-direction: column;
  align-items: center;
  padding: 0;
  border: none;
  z-index: var(--z-rail);
}
/* Rails hug the edges of the usable viewport: the left rail starts at the
   sidebar's right edge (264px expanded / 44px collapsed), the right rail
   hugs the viewport edge. Media queries below still gate visibility: the
   rail (300px) must clear the chat column before it is shown. */
.ads-on.ads-more body:not(.sidebar-collapsed) .ad-rail-l { left: var(--sidebar-w); }
.ads-on.ads-more body.sidebar-collapsed .ad-rail-l { left: 44px; }
.ads-on.ads-more .ad-rail-r { right: 0; }
/* Expanded sidebar is 264px: rails clear the chat column from 1664/1696px. */
@media (min-width: 1664px) {
  .ads-on.ads-more body:not(.sidebar-collapsed) .ad-rail-l { display: flex; }
}
@media (min-width: 1696px) {
  .ads-on.ads-more body:not(.sidebar-collapsed) .ad-rail-r { display: flex; }
}
/* Collapsed (44px) — rails fit from 1444/1476px. */
@media (min-width: 1444px) {
  .ads-on.ads-more body.sidebar-collapsed .ad-rail-l { display: flex; }
}
@media (min-width: 1476px) {
  .ads-on.ads-more body.sidebar-collapsed .ad-rail-r { display: flex; }
}
/* 300x600 unit: skip on short viewports instead of clipping. */
@media (max-height: 639px) {
  .ads-on.ads-more .ad-rail { display: none !important; }
}

/* first-visit ads support-level ask */
.ads-ask-actions { justify-content: center; }
.ads-ask-note { margin-top: var(--sp-2); text-align: center; font-size: var(--fs-xs); color: var(--text-faint); }

.msg { position: relative; display: flex; flex-direction: column; gap: var(--sp-1); animation: fadeUp var(--dur-slow) var(--ease) both; }
@keyframes fadeUp {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}
.msg .meta {
  display: flex; align-items: center; gap: var(--sp-2-5);
  font-size: var(--fs-micro); color: var(--text-faint); padding: 0 var(--sp-05);
}
.msg .meta .avatar {
  flex: none; width: 26px; height: 26px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--bg-elev-2); border: 1px solid var(--border-strong);
  overflow: hidden;
}
.msg .meta .avatar.user { color: var(--accent-strong); }
.msg .meta .avatar img { width: 16px; height: 16px; display: block; }
/* Assistant-avatar provider marks need the same plate treatment as the
   model picker's .m-logo — without it the black OpenAI knot renders as a
   smudge directly on the elevated avatar background. */
.msg .meta .avatar img.onlight,
.msg .meta .avatar img.ondark { box-sizing: content-box; padding: 2px; border-radius: var(--r-xs); }
.msg .meta .avatar img.onlight { background: var(--logo-plate); }
.msg .meta .avatar img.ondark { background: var(--logo-plate-dark); }
.msg .meta .names { display: flex; flex-direction: column; line-height: 1.25; }
.msg .meta .names .who { font-weight: var(--wt-semi); letter-spacing: 0.02em; font-size: var(--fs-xs); }
.msg .meta .names .who.user { color: var(--accent-strong); }
/* JS emits `assistant` from the message role but `ai` for synthetic/error
   bubbles, so both spellings must be styled or one of them silently loses
   its rules. */
.msg .meta .names .who.ai,
.msg .meta .names .who.assistant { color: var(--text); }
.msg .meta .names .sub { font-size: var(--fs-label); color: var(--text-faint); }
.msg .meta .time { flex: 1; text-align: right; }
/* Action bar: one flat row of outline icons directly under every message,
   always visible (no hover gating), chrome-free like a bare icon strip. */
.msg .actions {
  position: static; margin-top: var(--sp-05);
  display: flex; align-items: center; gap: var(--sp-1);
  padding: 0;
  background: transparent; border: none; box-shadow: none;
  opacity: 1; visibility: visible; pointer-events: auto; transform: none;
}
.msg .actions .icon-btn {
  width: 31px; height: 31px;
  border-radius: 50%;
  background: transparent; border: none; box-shadow: none;
  color: var(--text);
}
.msg .actions .icon-btn svg { width: 18px; height: 18px; }
.msg .actions .icon-btn:hover {
  color: var(--accent-strong); background: var(--bg-hover);
}
.msg .actions .copy-btn {
  width: 31px; height: 31px; display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: none; box-shadow: none;
  color: var(--text); cursor: pointer;
  padding: 0; border-radius: 50%;
  transition: color var(--dur-fast), background var(--dur-fast);
}
.msg .actions .copy-btn:hover { color: var(--accent-strong); background: var(--bg-hover); }
.msg .actions .copy-btn.done { color: var(--ok); background: transparent; }

/* attachments + images + sources in messages */
.attach-preview { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: 0 0 var(--sp-1-5); }
.attach-preview .ap-item {
  display: flex; align-items: center; gap: var(--sp-1-5);
  background: var(--bg-elev-2); border: 1px solid var(--border-strong);
  border-radius: var(--r-md); padding: var(--sp-1); max-width: 240px;
}
.attach-preview .ap-item img {
  width: 40px; height: 40px; border-radius: var(--r-sm); object-fit: cover;
  border: 1px solid var(--border); cursor: zoom-in;
}
.attach-preview .ap-item span {
  font-size: var(--fs-micro); color: var(--text-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.attach-row { display: flex; flex-wrap: wrap; gap: var(--sp-1-5); margin: 0 0 var(--sp-1-5); }
.attach-chip {
  display: inline-flex; align-items: center; gap: var(--sp-1-5);
  background: var(--bg-elev-2); border: 1px solid var(--border-strong);
  color: var(--text-dim); font-size: var(--fs-micro); padding: var(--sp-1) var(--sp-2-5) var(--sp-1) var(--sp-1);
  border-radius: var(--r-pill); max-width: 220px;
}
.attach-chip img {
  width: 20px; height: 20px; border-radius: 50%; object-fit: cover;
  border: 1px solid var(--border-strong);
}
.attach-chip span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attach-chip svg { color: var(--text-faint); flex: none; }
.img-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--sp-2-5); margin: var(--sp-2-5) 0 var(--sp-05);
}
.img-grid img {
  width: 100%; border-radius: var(--r-sm);
  border: 1px solid var(--border); display: block;
  background: var(--code-bg);
  cursor: zoom-in;
  transition: border-color var(--dur), transform var(--dur-fast);
}
.img-grid img:hover { border-color: var(--border-strong); transform: scale(1.01); }

/* ============ streamed image reveal ============
   Images arriving mid-stream sit in .img-slot wrappers rebuilt from JS state
   each paint pass: .loading shows a frame sized to the requested ratio
   (--ar), then the first finished frame gets one blur-rise reveal (.enter)
   and never replays it on later rebuilds. */
.img-slot { position: relative; display: block; border-radius: var(--r-sm); }
.img-slot.loading { aspect-ratio: var(--ar, 1 / 1); overflow: hidden; }
.img-slot.ready { position: relative; display: block; }
.img-slot.loading img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; opacity: 0; pointer-events: none;
}
.img-slot.loading::after {
  content: ""; position: absolute; inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, var(--bg-hover), transparent);
  animation: skelSweep 1.3s ease-in-out infinite;
}
.img-slot.enter img { animation: imgIn 0.45s var(--ease); }
@keyframes imgIn {
  from { opacity: 0; transform: translateY(8px) scale(0.98); filter: blur(6px); }
}

/* ============ interactive image placeholder (.ph) ============
   Rendered while the model paints: the exact aspect ratio the user picked,
   an aurora that drifts, a spotlight riding the cursor, a slight 3D tilt,
   click ripples and parallax sparks. All driven by --mx/--my/--rx/--ry
   custom props that wireImgPlaceholder() writes on pointer events. */
.img-slot.ph {
  border: 1px solid var(--border);
  background: var(--code-bg);
  cursor: crosshair;
  transform: perspective(900px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transition: transform 0.3s var(--ease), border-color var(--dur);
  will-change: transform;
}
.img-slot.ph:hover { border-color: var(--accent-border); }
.ph-aur, .ph-grid, .ph-label, .ph-spark, .ph-ripple { position: absolute; }
.ph-aur {
  inset: 0;
  background:
    radial-gradient(circle at var(--mx, 50%) var(--my, 50%), var(--accent), transparent 30%),
    radial-gradient(circle at 22% 28%, var(--accent), transparent 36%),
    radial-gradient(circle at 78% 72%, var(--accent), transparent 32%),
    conic-gradient(from 0deg at 55% 45%, var(--bg-elev-2), var(--accent), var(--bg-elev-2), var(--accent), var(--bg-elev-2));
  opacity: 0.16;
  filter: blur(14px);
  animation: phDrift 9s ease-in-out infinite alternate;
}
@keyframes phDrift {
  from { transform: scale(1) translate(0, 0); }
  to { transform: scale(1.18) translate(2%, -3%); }
}
.ph-grid {
  inset: 0;
  background-image: radial-gradient(var(--border-strong) 1px, transparent 1.2px);
  background-size: 16px 16px;
  opacity: 0.45;
  -webkit-mask-image: radial-gradient(circle at 50% 50%, black 25%, transparent 85%);
  mask-image: radial-gradient(circle at 50% 50%, black 25%, transparent 85%);
}
.ph-spark {
  width: 4px; height: 4px; border-radius: 50%;
  background: var(--accent-strong);
  box-shadow: 0 0 6px var(--accent);
  opacity: 0.2;
  animation: phSpark 2.6s ease-in-out infinite;
}
.ph-spark.s1 { left: 18%; top: 30%; }
.ph-spark.s2 { left: 70%; top: 22%; animation-delay: 0.7s; }
.ph-spark.s3 { left: 32%; top: 70%; animation-delay: 1.3s; }
.ph-spark.s4 { left: 82%; top: 60%; width: 3px; height: 3px; animation-delay: 1.9s; }
@keyframes phSpark {
  0%, 100% { transform: translate(0, 0) scale(1); opacity: 0.15; }
  30% { opacity: 0.9; }
  50% {
    transform: translate(calc((var(--mx, 50%) - 50%) * 0.06), calc((var(--my, 50%) - 50%) * 0.06)) scale(1.5);
    opacity: 1;
  }
}
.ph-label {
  inset: 0; z-index: var(--z-inlayer-top);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-1);
  font-size: var(--fs-sm); color: var(--text-dim);
  text-align: center; padding: var(--sp-3);
  pointer-events: none;
}
.ph-label small { font-size: var(--fs-micro); color: var(--text-faint); }
.ph-dots { display: flex; gap: 4px; }
.ph-dots i {
  width: 4px; height: 4px; border-radius: 50%; background: var(--accent);
  animation: hop 1.1s ease-in-out infinite;
}
.ph-dots i:nth-child(2) { animation-delay: var(--dur); }
.ph-dots i:nth-child(3) { animation-delay: var(--dur-slow); }
.ph-ripple {
  width: 24px; height: 24px; border-radius: 50%;
  border: 2px solid var(--accent);
  transform: translate(-50%, -50%) scale(0);
  opacity: 0.9;
  pointer-events: none;
  animation: phRipple 0.55s var(--ease) forwards;
}
@keyframes phRipple {
  to { transform: translate(-50%, -50%) scale(6); opacity: 0; }
}

/* Video generation output. Both of these were inline styles / an unstyled
   class in app.js — the status line had no rule at all, so queue progress
   rendered as unstyled body text. */
.vid-out {
  width: 100%; max-width: 100%; display: block;
  margin: var(--sp-2) 0 var(--sp-1);
  border-radius: var(--r-md); border: 1px solid var(--border);
  background: var(--code-bg);
}
.vid-status {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  margin: var(--sp-1) 0;
  font-size: var(--fs-xs); color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
.vid-status::before {
  content: ""; flex: none; width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent);
  animation: vidPulse 1.4s var(--ease) infinite;
}
@keyframes vidPulse {
  0%, 100% { opacity: 0.35; }
  50% { opacity: 1; }
}
.src-wrap { display: flex; flex-wrap: wrap; gap: var(--sp-1-5); margin: var(--sp-1) 0 0; align-items: center; }
.src-toggle {
  font-size: var(--fs-micro); color: var(--text-faint); background: transparent;
  border: 1px solid var(--border); border-radius: var(--r-pill);
  padding: var(--sp-1) var(--sp-2-5); cursor: pointer; transition: border-color var(--dur), color var(--dur);
}
.src-toggle:hover { color: var(--accent-strong); border-color: var(--accent-border); }
.src-links { display: none; flex: 1 1 100%; flex-wrap: wrap; gap: var(--sp-1-5); }
.src-links.open { display: flex; }
.src-chip {
  font-size: var(--fs-micro); color: var(--accent-strong);
  background: var(--accent-soft); border: 1px solid var(--accent-border);
  padding: var(--sp-1) var(--sp-3); border-radius: var(--r-pill); text-decoration: none;
  max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition: border-color var(--dur), background var(--dur);
}
.src-chip:hover {
  border-color: var(--accent-border-strong);
  background: rgb(var(--accent-rgb) / 0.2);
  text-decoration: none;
}

.msg .body {
  white-space: pre-wrap; word-break: break-word;
  border-radius: var(--r-md); padding: var(--sp-2-5) var(--sp-3-5);
}
.msg.user { align-items: flex-end; }
.msg.user .actions { left: auto; right: 0; }
.msg.user .body {
  /* A whisper of the accent gives the user's own words a surface identity —
     they no longer rely on alignment alone to read as "mine". */
  background: linear-gradient(135deg, var(--user-bg), color-mix(in srgb, var(--user-bg) 88%, var(--accent) 12%));
  border: 1px solid color-mix(in srgb, var(--border) 70%, var(--accent) 30%);
  box-shadow: 0 1px 2px rgb(var(--shadow-rgb) / 0.14);
  max-width: min(80%, 640px); color: var(--text);
  transition: background var(--dur), border-color var(--dur), box-shadow var(--dur);
}
.msg.ai .body,
.msg.assistant .body { width: 100%; padding: var(--sp-05) var(--sp-1-5) var(--sp-05); }
/* Kill the trailing block margin so the last line / Sources box ends near
   the bubble edge and the action pill below reads as attached, not floating. */
.msg.ai .body > :last-child,
.msg.assistant .body > :last-child { margin-bottom: 0; }

.msg .body.err {
  color: var(--err); background: var(--err-bubble);
  border: 1px solid var(--err-bubble-border);
}
.msg .body.err code { color: inherit; background: var(--err-code-bg); }

/* markdown */
.body p { margin: 0 0 var(--sp-2); }
.body p:last-child { margin-bottom: 0; }
.body .katex-display { margin: var(--sp-2) 0; overflow-x: auto; overflow-y: hidden; padding: var(--sp-05) 0; }
.body .katex { font-size: 1.06em; }
/* display-math shell + copy-LaTeX button (button injected post-sanitize) */
.body .math-blk { position: relative; display: block; }
.body .math-copy {
  position: absolute; top: 0; right: 0;
  display: inline-flex; align-items: center;
  padding: 3px var(--sp-1); border: 1px solid var(--border);
  border-radius: var(--r-xs); background: var(--bg-elev); color: var(--text-dim);
  cursor: pointer; opacity: 0; transition: opacity var(--dur), color var(--dur), border-color var(--dur);
}
.body .math-blk:hover .math-copy,
.body .math-blk:focus-within .math-copy { opacity: 1; }
.body .math-copy svg { display: block; }
.body .math-copy .ic-check { display: none; }
.body .math-copy.done { color: var(--accent-strong); border-color: var(--accent-strong); }
.body .math-copy.done .ic-check { display: block; }
.body .math-copy.done .ic-copy { display: none; }
.body h1, .body h2, .body h3, .body h4 {
  margin: var(--sp-3-5) 0 var(--sp-1-5); line-height: 1.3; letter-spacing: -0.01em;
}
.body h1 { font-size: var(--fs-lg); } .body h2 { font-size: var(--fs-h3); } .body h3 { font-size: var(--fs-body); } .body h4 { font-size: var(--fs-sm); }
.body ul, .body ol { margin: 0 0 var(--sp-2); padding-left: var(--sp-6); }
.body li { margin: var(--sp-05) 0; }
.body a { color: var(--accent-strong); text-decoration: none; }
.body a:hover { text-decoration: underline; }
.body blockquote {
  margin: var(--sp-2) 0; padding: var(--sp-1-5) var(--sp-3-5); border-left: 3px solid var(--border-strong);
  color: var(--text-dim); background: var(--bg-elev); border-radius: 0 var(--r-sm) var(--r-sm) 0;
}
.body hr { border: none; border-top: 1px solid var(--border); margin: var(--sp-3) 0; }
.body code:not(pre code) {
  font: 0.88em var(--mono); background: var(--code-inline-bg);
  padding: 1.5px var(--sp-1-5); border-radius: var(--r-xs);
}
.body pre {
  margin: var(--sp-2-5) 0; border-radius: var(--r-sm); overflow: hidden;
  border: 1px solid var(--border); background: var(--code-bg);
}
.body pre .code-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--sp-1-5) var(--sp-3); background: var(--code-head-bg); border-bottom: 1px solid var(--border);
  font: var(--fs-micro) var(--mono); color: var(--text-faint);
}
.body pre .code-head .lang { text-transform: lowercase; letter-spacing: 0.04em; }
.body pre .code-head button {
  background: none; border: 1px solid var(--border-strong); color: var(--text-dim);
  font: var(--fs-label) var(--font); padding: var(--sp-05) var(--sp-2-5); border-radius: var(--r-xs); cursor: pointer;
  transition: color var(--dur-fast), border-color var(--dur-fast);
}
.body pre .code-head button:hover { color: var(--text); border-color: var(--code-btn-hover); }
.body pre .code-head button.done { color: var(--ok); border-color: var(--ok); }
.body pre .code-head .code-actions { display: flex; align-items: center; gap: var(--sp-1-5); }
.body pre code {
  display: block; padding: var(--sp-3) var(--sp-3-5); font: var(--fs-xs)/1.6 var(--mono);
  white-space: pre; overflow-x: auto;
}
.body pre code:not(.hljs) { background: var(--code-bg); }
/* Block-level table: a wide table scrolls inside its own box instead of
   blowing the bubble past the viewport on phones. */
.body table {
  display: block; min-width: 100%; width: 100%;
  overflow-x: auto; scrollbar-width: thin;
  border-collapse: collapse; margin: var(--sp-2-5) 0; font-size: var(--fs-sm);
}
.body th, .body td { border: 1px solid var(--border-strong); padding: var(--sp-1-5) var(--sp-3); text-align: left; }
.body th { background: var(--bg-elev-2); font-weight: var(--wt-semi); }

.cursor {
  display: inline-block; width: 8px; height: 16px; margin-left: var(--sp-05);
  background: var(--accent); border-radius: var(--r-2xs); vertical-align: -2px;
  animation: blink 1s step-end infinite;
}
@keyframes blink { 50% { opacity: 0; } }

.thinking {
  display: flex; align-items: center; gap: var(--sp-2);
  color: var(--text-faint); font-size: var(--fs-sm); padding: var(--sp-1-5);
}
.thinking .bar { display: flex; gap: var(--sp-1); }
.thinking .bar i {
  width: 5px; height: 5px; border-radius: 50%; background: var(--accent);
  animation: hop 1.1s ease-in-out infinite;
}
.thinking .bar i:nth-child(2) { animation-delay: var(--dur); }
.thinking .bar i:nth-child(3) { animation-delay: var(--dur-slow); }
@keyframes hop { 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; } 30% { transform: translateY(-4px); opacity: 1; } }
.thinking .think-label { transition: opacity 0.25s var(--ease); }
.thinking .think-label.swap { opacity: 0; }

/* While an assistant bubble streams, a blinking accent caret rides the last
   rendered block — pure CSS, so markdown/math repaints never drop it. */
.msg.streaming .body > :last-child::after {
  content: "";
  display: inline-block;
  width: 8px; height: 16px; margin-left: var(--sp-05);
  background: var(--accent); border-radius: var(--r-2xs);
  vertical-align: -2px;
  animation: blink 1s step-end infinite;
}

@media (prefers-reduced-motion: reduce) {
  .thinking .think-label { transition: none; }
  .msg.streaming .body > :last-child::after { animation: none; }
  .img-slot.enter img { animation: none; }
  .img-slot.loading::after { animation: none; }
  .img-slot.ph { transform: none; transition: none; }
  .ph-aur { animation: none; }
  .ph-spark { animation: none; opacity: 0.4; }
  .ph-ripple { display: none; }
}

/* empty state — deliberately off the centre axis */
.empty-state {
  padding: clamp(40px, 9vh, 88px) var(--sp-6) var(--sp-8);
  max-width: 620px;
}
.empty-state .glyph {
  width: 76px; height: 76px; margin: 0 0 var(--sp-5); border-radius: var(--r-xl);
  border: 1px solid var(--accent-border);
  overflow: hidden; box-shadow: var(--e3);
  transform: rotate(-3deg);
  animation: glyphFloat 6s ease-in-out infinite;
}
@keyframes glyphFloat {
  0%, 100% { transform: rotate(-3deg) translateY(0); }
  50% { transform: rotate(-1deg) translateY(-4px); }
}
.empty-state .glyph img { width: 100%; height: 100%; object-fit: contain; display: block; background: var(--bg-elev); }
.empty-state h2 {
  font-size: var(--fs-h1); line-height: var(--lh-h1); letter-spacing: var(--tr-h1);
  font-weight: var(--wt-semi); margin-bottom: var(--sp-2);
}
.empty-state p {
  color: var(--text-dim); font-size: var(--fs-body);
  max-width: 46ch; margin: 0 0 var(--sp-6);
}
.empty-state .providers {
  display: flex; gap: var(--sp-2); margin: 0 0 var(--sp-6);
  padding-left: var(--sp-05);
}
.empty-state .providers img {
  width: 28px; height: 28px; border-radius: var(--r-sm); background: var(--bg-elev-2);
  border: 1px solid var(--border); padding: var(--sp-1-5);
  transition: transform var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.empty-state .providers img:hover { transform: translateY(-2px); border-color: var(--border-strong); }
.empty-state .providers img.onlight { background: var(--logo-plate); }
.empty-state .providers img.ondark { background: var(--logo-plate-dark); }
.chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); max-width: 520px; }
.chips .chip {
  background: var(--bg-elev); border: 1px solid var(--border); color: var(--text-dim);
  font-size: var(--fs-xs); padding: var(--sp-2) var(--sp-3-5); height: auto; border-radius: var(--r-pill);
  cursor: pointer; text-align: left;
  transition: border-color var(--dur) var(--ease), color var(--dur) var(--ease),
    background var(--dur) var(--ease), transform var(--dur-fast) var(--ease);
}
.chips .chip:hover { border-color: var(--accent-border-strong); color: var(--accent-strong); background: var(--accent-faint); }
.tip-strip {
  display: flex; align-items: flex-start; gap: var(--sp-2-5);
  margin-top: var(--sp-6); padding: var(--sp-2-5) var(--sp-3);
  background: var(--bg-elev); border: 1px dashed var(--border-strong);
  border-radius: var(--r-sm); color: var(--text-dim); font-size: var(--fs-xs);
  line-height: 1.55; max-width: 520px;
}
.tip-strip .tip-label {
  flex: none; font-weight: var(--wt-semi); color: var(--accent-strong);
  text-transform: uppercase; letter-spacing: var(--tr-label); font-size: var(--fs-label);
  padding-top: var(--sp-05);
}
.tip-strip .tip-text { flex: 1; }
.tip-strip .tip-dismiss {
  flex: none; background: none; border: none; color: var(--text-faint);
  font-size: var(--fs-h3); line-height: 1; cursor: pointer; padding: var(--sp-05) var(--sp-1);
  border-radius: var(--r-xs); transition: color var(--dur-fast), background var(--dur-fast);
}
.tip-strip .tip-dismiss:hover { color: var(--text); background: var(--bg-hover); }

/* ============ composer ============ */
/* Toolbar, attachments and the input share one elevated shell instead of
   floating as three unrelated strips. Bottom padding runs a touch larger
   than the top so the block reads optically centred. */
.composer-zone {
  padding: var(--sp-2) var(--sp-6) var(--sp-4);
  background: var(--bg);
  position: relative;
  transition: background var(--dur-slow) var(--ease);
}
.composer-shell {
  max-width: var(--content-w); margin: 0 auto;
  background: var(--glass-bg);
  backdrop-filter: blur(var(--glass-blur));
  -webkit-backdrop-filter: blur(var(--glass-blur));
  border: 1px solid var(--glass-border);
  border-radius: var(--r-lg);
  box-shadow: var(--e2), inset 0 1px 0 rgb(255 255 255 / 0.03);
  padding: var(--sp-2) var(--sp-3) var(--sp-2-5);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease),
    background var(--dur-slow) var(--ease);
}
.composer-shell:focus-within {
  border-color: var(--accent-border-strong);
  box-shadow: var(--e3), 0 0 0 3px var(--accent-focus-ring), inset 0 1px 0 rgb(255 255 255 / 0.05);
}
@keyframes micPulse {
  0%, 100% { opacity: .5; }
  50% { opacity: 1; }
}
.toolbar {
  margin: 0 0 var(--sp-2);
  display: flex; align-items: center; gap: var(--sp-1);
}
.toolbar-spacer { flex: 1; }
.tool-btn {
  display: grid; place-items: center;
  width: 30px; height: 30px; border-radius: var(--r-sm);
  background: none; border: 1px solid var(--border);
  color: var(--text-faint); cursor: pointer;
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease),
    background var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.tool-btn:hover { color: var(--text); border-color: var(--border-strong); background: var(--bg-hover); }
.tool-btn:active { transform: scale(0.94); }
.tool-btn.active {
  color: var(--accent-strong); border-color: var(--accent-border-strong);
  background: var(--accent-soft);
}
.tool-btn:disabled { opacity: 0.4; cursor: default; }
/* Mic dictation + call-mode chip states */
#mic-btn.recording {
  color: var(--err); border-color: var(--err-strong);
  background: var(--err-soft);
  animation: micPulse 1.4s ease-in-out infinite;
}
#mic-btn.busy { color: var(--warn); border-color: var(--warn); }
/* While recording the mic reads as "stop", so it swaps to a square — the same
   affordance as #stop. Purely icon-driven in CSS; no JS re-render per state. */
#mic-btn .ic-stop { display: none; }
#mic-btn.recording .ic-mic { display: none; }
#mic-btn.recording .ic-stop { display: inline; }

/* ============ speech-to-text pill ============
   The transcription phase has no visible home otherwise: a title attribute is
   invisible while you are staring at the button, and the transcript only lands
   in #input after whisper finishes. This pill replaces the input while voice is
   live, so "it is listening" / "it is transcribing" is written where the user is
   already looking. */
.stt-pill {
  flex: none;
  min-height: 24px;
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--r-sm);
  font: var(--wt-medium) var(--fs-sm) / 1.4 var(--font);
  color: var(--text-dim);
  background: var(--accent-faint);
  border: 1px solid var(--accent-border);
}
.stt-pill[hidden] { display: none; }
.stt-pill::before {
  content: ""; flex: none; width: 7px; height: 7px; border-radius: 50%;
  background: currentColor;
}
.stt-pill.listening { color: var(--ambient-user); background: var(--accent-faint); }
.stt-pill.listening::before { animation: micPulse 1s ease-in-out infinite; }
.stt-pill.thinking { color: var(--warn); background: rgb(var(--warn-rgb) / 0.08); }
.stt-pill.thinking::before { animation: micPulse 0.7s ease-in-out infinite; }
.stt-pill.speaking { color: var(--ambient-ai); }
.stt-pill.waiting { color: var(--text-faint); }
/* Between "transcribed" and "first word out" there are 13-20s of backend think
   time. Same amber as .thinking, because it IS waiting on the same work — the
   two used to share a label, and the pill said the AI was reading aloud
   through a stretch where nothing was being read. */
.stt-pill.waiting-ai { color: var(--warn); background: rgb(var(--warn-rgb) / 0.08); }
.stt-pill.waiting-ai::before { animation: micPulse 0.7s ease-in-out infinite; }

/* Call mode: the button ring tracks live mic level so the user can see the
   VAD is hearing them without watching the transcript. */
#call-btn {
  --call-level: 0;
  box-shadow: 0 0 0 calc(var(--call-level) * 7px) color-mix(in srgb, var(--accent) calc(var(--call-level) * 45%), transparent);
  transition: box-shadow 90ms linear;
}
#call-btn.busy { color: var(--warn); border-color: var(--warn); }
body.call-mode #input { opacity: .5; }
/* The waveform takes the composer's own slot instead of sitting behind the
   transcript: the input is where the user's voice is going, so that is where
   the picture of their voice belongs. Two colours — mic above the centre line,
   the AI reading below — so you can see who holds the floor. */
.stt-stage { display: none; flex: 1; flex-direction: column; gap: var(--sp-1); }
body.mic-live .stt-stage { display: flex; }
/* Call mode needs the stage too, and nothing above turned it on: mic-live is
   deliberately false there (the input stays on screen in a conversation, and
   micSetTitle clears the class whenever callMode is set), so the pill lived
   inside a display:none parent and rendered at 0x0 in every state — the one
   piece of feedback telling the user their words were being picked up. The
   pill shows; the canvas below does not, which is what body.call-mode hides. */
body.call-mode .stt-stage { display: flex; }
/* flex: 1 above is right for dictation, where the input is display:none and the
   pill takes over the whole row. In call mode the input STAYS (a conversation
   still shows what you typed), so the stage would split the row with it as a
   second flex:1 sibling and halve the input — 492px down to 244px. Call mode
   wants the pill on its own line under the input instead, so drop it out of the
   row and let it take the full width. */
body.call-mode .composer { flex-wrap: wrap; }
body.call-mode .stt-stage { flex: 1 0 100%; order: 1; }
body.mic-live #input { display: none; }
.ambient {
  width: 100%; height: 40px; flex: none;
  display: block;
}
/* Call mode is a conversation, not a waveform: the mic stays open across
   turns and there is no single utterance to plot, so the canvas steps aside
   for a soft glow above the composer instead. */
body.call-mode .ambient { display: none; }
body.call-mode .composer-shell { justify-content: center; padding-block: var(--sp-3); }
.v-gender { display: inline-flex; gap: var(--sp-3); }
.v-radio { display: inline-flex; align-items: center; gap: var(--sp-1-5); cursor: pointer; }
.v-radio input { accent-color: var(--accent); }
#voice-gender-row.disabled { opacity: .45; pointer-events: none; }
.img-dd { position: relative; }
.img-dd.open .chip-menu { display: flex; }
.img-dd.off { opacity: 0.45; pointer-events: none; }
/* image/video tool dropdowns run larger than the composer chip menus */
.img-dd .tool-menu {
  min-width: min(290px, calc(100vw - 2 * var(--sp-6))); max-height: min(460px, 70vh);
  padding: var(--sp-2);
  gap: var(--sp-1);
}
.img-dd .tool-menu .chip-opt {
  font-size: var(--fs-sm);
  padding: var(--sp-2-5) var(--sp-3);
}
.img-dd .tool-menu .chip-check { font-size: var(--fs-xs); }
.img-dd .tool-menu .menu-toggle {
  font-size: var(--fs-sm);
  padding: var(--sp-2-5) var(--sp-3);
}
.img-dd .tool-menu .chip-sec {
  font-size: var(--fs-xs);
  padding: var(--sp-2) var(--sp-2-5) 0;
}
.img-dd .tool-menu .chip-sec .sec-caret { font-size: var(--fs-xs); }
.chip-sec {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
  width: 100%; text-align: left;
  background: none; border: none; cursor: pointer;
  padding: var(--sp-2) var(--sp-2-5) 0;
  font-size: var(--fs-xs); color: var(--text-faint);
  text-transform: uppercase; letter-spacing: var(--tr-label);
}
.chip-sec:hover { color: var(--text-dim); }
.chip-sec .sec-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chip-sec .sec-val { font-weight: var(--wt-regular); text-transform: none; letter-spacing: 0; color: var(--text-dim); }
.chip-sec .sec-caret { flex: none; font-size: var(--fs-micro); transition: transform var(--dur-fast); }
.chip-sec.open .sec-caret { transform: rotate(90deg); }
.menu-toggle {
  font-weight: var(--wt-semi);
  color: var(--text);
  border-bottom: 1px solid var(--border);
  border-radius: var(--r-sm) var(--r-sm) 0 0;
  margin-bottom: var(--sp-05);
}
.menu-toggle.selected { color: var(--accent-strong); }
.chip-dd { position: relative; }
.chip-dd .chip {
  display: inline-flex; align-items: center; gap: var(--sp-1-5);
  height: 28px; padding: 0 var(--sp-2-5); border-radius: var(--r-pill);
  background: var(--bg-elev); border: 1px solid var(--border);
  color: var(--text-dim); font-size: var(--fs-xs); cursor: pointer;
  transition: color var(--dur-fast), border-color var(--dur-fast), background var(--dur-fast);
}
.chip-dd .chip:hover { color: var(--text); border-color: var(--border-strong); background: var(--bg-hover); }
.chip:active { transform: translateY(1px); }
.chip b { font-weight: var(--wt-semi); color: var(--text); white-space: nowrap; max-width: 130px; overflow: hidden; text-overflow: ellipsis; }
.chip-caret { font-size: var(--fs-nano); color: var(--text-faint); opacity: 0.8; }
.chip-menu {
  position: absolute; bottom: calc(100% + 4px); left: 0; z-index: var(--z-dropdown);
  min-width: min(160px, calc(100vw - 2 * var(--sp-6))); max-height: 250px; overflow-y: auto;
  background: var(--bg-elev-2); border: 1px solid var(--border-strong);
  border-radius: var(--r-md); box-shadow: var(--e3); padding: var(--sp-1-5);
  display: none; flex-direction: column; gap: var(--sp-05);
}
.chip-dd.open .chip-menu { display: flex; }
.chip-opt {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2-5);
  text-align: left; background: none; border: none;
  color: var(--text-dim); padding: var(--sp-2) var(--sp-2-5); border-radius: var(--r-sm);
  cursor: pointer; font-size: var(--fs-xs); line-height: 1.35; white-space: nowrap;
}
.chip-opt:hover { background: var(--bg-hover); color: var(--text); }
.chip-opt.selected { background: var(--accent-soft); color: var(--accent-strong); }
.chip-check { color: var(--accent); font-size: var(--fs-micro); }
.attachments {
  margin: 0 0 var(--sp-2);
  display: flex; flex-wrap: wrap; gap: var(--sp-2);
}
.att-chip {
  display: flex; align-items: center; gap: var(--sp-2);
  background: var(--bg-elev); border: 1px solid var(--border-strong);
  border-radius: var(--r-md); padding: var(--sp-1) var(--sp-1-5) var(--sp-1) var(--sp-1);
  font-size: var(--fs-xs); color: var(--text-dim); max-width: 260px;
}
.att-chip img {
  width: 30px; height: 30px; border-radius: var(--r-sm); object-fit: cover;
  border: 1px solid var(--border);
}
.att-chip span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.att-rm {
  flex: none; width: 20px; height: 20px; border-radius: var(--r-xs);
  background: none; border: none; color: var(--text-faint); cursor: pointer;
  font-size: var(--fs-micro); display: grid; place-items: center;
}
.att-rm:hover { background: var(--err-soft); color: var(--err); }
.att-edit {
  flex: none; width: 20px; height: 20px; border-radius: var(--r-xs);
  background: none; border: none; color: var(--text-faint); cursor: pointer;
  display: grid; place-items: center;
  transition: background var(--dur-fast), color var(--dur-fast);
}
.att-edit:hover { background: var(--bg-elev-2); color: var(--accent); }
.composer {
  display: flex; align-items: flex-end; gap: var(--sp-2);
  background: none; border: none; padding: 0;
  position: relative;   /* anchors .call-orb */
}
.composer textarea {
  /* The only outline: none left in the file, and it has a real replacement —
     .composer-shell:focus-within draws the ring around the whole shell. */
  flex: 1; resize: none; background: none; border: none; outline: none;
  color: var(--text); font: var(--fs-body) / 1.5 var(--font);
  max-height: 180px; min-height: 24px; padding: var(--sp-1) var(--sp-05);
}
.composer textarea::placeholder { color: var(--text-faint); }
/* Call mode is a conversation, not a form: the placeholder talks about Enter,
   which is irrelevant once the mic owns the turn, and it sits exactly where
   the orbs are. A void composer also stops the orbs reading as a button
   label attached to a text field. */
body.call-mode #input::placeholder { color: transparent; }

/* ============ call-mode orbs ============
   A cluster of soft circles floating in the middle of the composer, one per
   speaker. Circles rather than a gradient because a single wide wash reads as
   a lighting change on the wallpaper; discrete orbs read as objects reacting
   to sound, which is what the mic is actually doing.

   Nodes are built by JS (callOrbBuild) because each needs its own --i for the
   delay and scale; CSS only owns position, colour and the level response. */
/* Position and width are set from JS (callOrbFit) to match the textarea's own
   box, not the composer's: the thinking chip and the send/mic/call buttons sit
   to the right of the textarea, so centring on the composer pushed the orbs
   underneath the chip. "Centred in the text input" is what was asked for.

   top/height do the same job vertically and now have to be JS too: the STT pill
   takes its own line under the input, so the composer is taller than the
   textarea and a hardcoded 50% slid the orbs 18px off the field they belong to. */
.call-orb {
  position: absolute; top: 0;
  left: 0; width: 60%; height: 46px;
  transform: translate(-50%, -50%);   /* left is the centre, set by callOrbFit */
  pointer-events: none;
  display: none;
  /* The orbs are the composer in call mode, so they have to sit over the
     textarea — it is position-static, which means DOM order alone would put
     the placeholder on top. */
  z-index: 1;
}
body.call-mode .call-orb { display: block; }
.call-orb i {
  position: absolute; left: 50%; top: 50%;
  width: 30px; height: 30px; margin: -15px 0 0 -15px;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 42%,
    color-mix(in srgb, var(--orb-color) 78%, white 22%) 0%,
    var(--orb-color) 52%,
    transparent 70%);
  /* Barely any blur: at 6px the five circles per side merged into one smear,
     which is the exact failure the node-per-orb design was meant to avoid. A
     2px feather still softens the edge without fusing neighbours. */
  filter: blur(2px);
  opacity: 0;
  transform: translateX(var(--dx, 0px)) scale(0.35);
  transition: opacity 160ms ease-out, transform 200ms cubic-bezier(0.32, 0.72, 0, 1);
  will-change: opacity, transform;
}
.call-orb i.on { opacity: var(--lvl, 0); transform: translateX(var(--dx, 0px)) scale(var(--scl, 0.5)); }
/* The two speakers are separated by position, not only hue: user orbs left of
   centre, AI right. Colourblind-safe, and readable at a glance. */
.call-orb i.user { --orb-color: var(--ambient-user); }
.call-orb i.ai { --orb-color: var(--ambient-ai); }

.send-btn {
  flex: none; width: 32px; height: 32px; border: none; border-radius: var(--r-sm);
  background: var(--accent); color: var(--on-accent); cursor: pointer;
  display: grid; place-items: center;
  box-shadow: 0 2px 8px rgb(var(--accent-rgb) / 0.25);
  transition: background var(--dur), transform var(--dur) cubic-bezier(0.34, 1.56, 0.64, 1),
    box-shadow var(--dur) var(--ease);
}
.send-btn:hover {
  background: var(--accent-strong);
  box-shadow: 0 4px 14px rgb(var(--accent-rgb) / 0.35);
}
.send-btn:active { transform: scale(0.92); }
.send-btn:disabled { background: var(--btn-disabled-bg); color: var(--text-faint); cursor: default; transform: none; box-shadow: none; }
.stop-btn {
  flex: none; width: 32px; height: 32px; border-radius: var(--r-sm); cursor: pointer;
  border: 1px solid var(--err); background: var(--err-soft); color: var(--err);
  display: none; place-items: center;
  animation: fadeUp var(--dur) ease;
}
.stop-btn:hover { background: rgb(var(--err-rgb) / 0.22); border-color: var(--err-strong); }
.stop-btn:active { transform: scale(0.96); }
.stop-btn.show { display: grid; }
.composer-hint {
  margin: 0; text-align: right;
  font-size: var(--fs-micro); color: var(--text-faint);
}
.toolbar .composer-hint { color: var(--text-faint); }

/* ============ drop overlay ============ */
.drop-overlay {
  position: fixed; inset: 0; z-index: var(--z-drop);
  display: none; place-items: center;
  background: var(--accent-faint);
  pointer-events: none;
}
.drop-overlay.show { display: grid; }
.drop-overlay .drop-inner {
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-2-5);
  padding: var(--sp-10) var(--sp-16); border-radius: var(--r-lg);
  border: 2px dashed var(--accent-border-strong);
  background: var(--bg-elev); color: var(--accent-strong);
  font-size: var(--fs-sm); font-weight: var(--wt-semi);
  box-shadow: var(--e3);
  animation: ddIn var(--dur) ease;
}

/* ============ modal ============ */
/* The dialog primitives (.modal-backdrop, .modal, .m-desc, .m-actions and its
   button variants) live in components.css, shared with the landing page.
   Only the chat-app-specific parts stay here. */
.modal textarea {
  width: 100%; min-height: 160px; resize: vertical;
  background: var(--bg); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--sp-3);
  font: var(--fs-xs) / 1.6 var(--mono);
  transition: border-color var(--dur) var(--ease);
}
.modal textarea:focus { border-color: var(--accent); }

/* ============ donate modal ============
   Single definition. A second, conflicting block used to live near the end of
   this file and won on every overlap — it forced the wallet address onto one
   nowrap line with an ellipsis, so the string could not be read to verify it.
   Addresses wrap on purpose here. */
.donate-modal { max-width: 380px; }
.donate-list { display: flex; flex-direction: column; gap: var(--sp-2); margin: var(--sp-4) 0; }
.donate-row {
  display: flex; flex-direction: column; gap: var(--sp-1);
  padding: var(--sp-2) var(--sp-3); border-radius: var(--r-sm);
  background: var(--bg-elev); border: 1px solid var(--border);
}
.donate-net {
  font: var(--wt-semi) var(--fs-label) / 1.4 var(--font);
  letter-spacing: var(--tr-label); text-transform: uppercase;
  color: var(--text-faint); user-select: none;
}
.donate-tokens { font-size: var(--fs-micro); color: var(--text-faint); }
.donate-line { display: flex; align-items: stretch; gap: var(--sp-2); min-width: 0; }
.donate-addr {
  flex: 1; min-width: 0; align-self: center;
  font: var(--fs-xs) / 1.5 var(--mono); color: var(--text);
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: var(--sp-2) var(--sp-2-5); word-break: break-all; user-select: all;
  white-space: pre-wrap;
}
.donate-copy {
  flex: none; align-self: center;
  padding: var(--sp-2) var(--sp-3); border-radius: var(--r-sm);
  background: var(--accent-faint); border: 1px solid var(--accent-border);
  color: var(--accent); font: var(--wt-semi) var(--fs-xs) / 1 var(--font);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.donate-copy:hover { background: var(--accent-soft); border-color: var(--accent-border-strong); }
.donate-copy:active { transform: translateY(1px); }
.donate-copy.done { color: var(--ok); border-color: var(--ok); background: rgb(var(--ok-rgb) / 0.1); }
.donate-warn {
  margin: var(--sp-3) 0 var(--sp-2-5); font-size: var(--fs-xs); color: var(--err);
  line-height: 1.5; padding-left: var(--sp-5); position: relative;
}
.donate-warn::before {
  content: "⚠"; position: absolute; left: 0; top: 0;
  font-size: var(--fs-micro); line-height: 1.5;
}
.donate-discord { margin: 0 0 var(--sp-1); font-size: var(--fs-xs); line-height: 1.5; color: var(--text-dim); }
.donate-discord a { color: var(--accent-strong); text-decoration: none; }
.donate-discord a:hover { text-decoration: underline; }
.donate-premium {
  margin: var(--sp-2) 0 0; font-size: var(--fs-sm); line-height: 1.5;
  padding: var(--sp-2) var(--sp-3); border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--border));
  border-radius: var(--r-sm); background: color-mix(in srgb, var(--accent) 8%, transparent);
}
.donate-premium a { color: var(--accent-strong); font-weight: var(--wt-semi); text-decoration: none; }
.donate-premium a:hover { text-decoration: underline; }

/* account chip (topbar, top-right) — see components.css */

/* notification bell (topbar, next to account chip) */
.notif-btn {
  position: relative; flex: none; display: grid; place-items: center;
  width: 34px; height: 34px; border-radius: var(--r-md);
  background: transparent; border: 1px solid transparent;
  color: var(--text-dim); cursor: pointer;
  transition: background var(--dur-fast), border-color var(--dur-fast), color var(--dur-fast), transform var(--dur-fast) var(--ease);
}
.notif-btn:hover { background: var(--bg-hover); border-color: var(--border-strong); color: var(--text); }
.notif-btn:active { transform: scale(0.94); }
.notif-badge[hidden] { display: none; }
.notif-badge {
  position: absolute; top: -3px; right: -3px;
  min-width: 16px; height: 16px; padding: 0 var(--sp-1); border-radius: var(--r-pill);
  display: grid; place-items: center;
  background: var(--err); color: var(--on-solid);
  font-size: var(--fs-nano); font-weight: var(--wt-bold); line-height: 1;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 0 0 2px var(--bg);
}

/* account modal — see components.css */
.tips-modal { width: min(380px, 100%); }
.tips-off-modal { width: min(360px, 100%); }
.tips-off-modal .m-actions { margin-top: var(--sp-3-5); }

/* like / dislike — one thumb control with a choice popover, same size as
   the other action icons; tooltip carries "rate this reply". */
.msg .actions .rate-wrap { position: relative; display: inline-flex; }
.msg .actions .rate-btn {
  width: 31px; height: 31px;
  border-radius: 50%;
  color: var(--text);
}
.msg .actions .rate-btn .rate-ic {
  width: 100%; height: 100%;
  display: inline-flex; align-items: center; justify-content: center;
}
.msg .actions .rate-btn .rate-ic svg { width: 18px; height: 18px; }
.msg .actions .rate-btn.on-good { color: var(--ok); }
.msg .actions .rate-btn.on-bad { color: var(--err); }
.rate-pop {
  position: absolute; top: calc(100% + 6px); left: -6px; z-index: var(--z-dropdown);
  display: flex; flex-direction: column; gap: 2px;
  min-width: 180px; padding: var(--sp-1);
  background: var(--bg-elev-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md); box-shadow: var(--e2);
}
.rate-pop .rate-opt {
  display: flex; align-items: center; gap: var(--sp-1-5);
  padding: var(--sp-2) var(--sp-2); border-radius: var(--r-sm);
  background: transparent; border: none; cursor: pointer;
  color: var(--text); font-size: var(--fs-sm); text-align: left;
  transition: background var(--dur-fast);
}
.rate-pop .rate-opt:hover { background: var(--bg-hover); }
.rate-pop .rate-opt svg { width: 18px; height: 18px; flex: none; }
.fb-modal, .compose-modal { width: min(480px, 100%); }
.fb-modal .fb-reasons {
  display: flex; flex-wrap: wrap; gap: var(--sp-1-5); margin-bottom: var(--sp-3);
}
.fb-modal .fb-reason {
  padding: var(--sp-1-5) var(--sp-3); border-radius: var(--r-pill); cursor: pointer;
  background: var(--bg); border: 1px solid var(--border);
  color: var(--text-dim); font: var(--fs-xs) var(--font);
  transition: border-color var(--dur-fast), color var(--dur-fast), background var(--dur-fast);
}
.fb-modal .fb-reason:hover { border-color: var(--accent-border); color: var(--text); }
.fb-modal .fb-reason.sel {
  background: var(--accent-soft); border-color: var(--accent);
  color: var(--accent-strong); font-weight: var(--wt-semi);
}
.fb-modal .fb-text, .compose-modal .compose-text {
  width: 100%; min-height: 80px; resize: vertical;
  background: var(--bg); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--sp-3);
  font: var(--fs-xs) / 1.6 var(--font);
  transition: border-color var(--dur) var(--ease);
}
.fb-modal .fb-text:focus, .compose-modal .compose-text:focus { border-color: var(--accent); }

/* Out of flow on purpose: in-flow it sits between the bubble and the
   action pill's top:100% anchor and shoves the pill ~30px away from the
   reply. Float it below the pill instead (~28px tall). */
.fb-status {
  position: absolute; top: 100%; margin: 34px 0 0 var(--sp-05);
  font-size: var(--fs-micro); color: var(--text-faint);
  padding: var(--sp-1) var(--sp-2-5); border-left: 2px solid var(--accent-border);
  background: var(--accent-faint); border-radius: 0 var(--r-xs) var(--r-xs) 0;
}
.fb-status b { color: var(--accent-strong); font-weight: var(--wt-semi); }
.myfb-modal { width: min(520px, 100%); }
.myfb-modal .myfb-list {
  max-height: 46dvh; overflow-y: auto; display: flex; flex-direction: column;
  gap: var(--sp-2); padding-right: var(--sp-05);
}
.myfb-item {
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--sp-2-5) var(--sp-3);
}
.myfb-top { display: flex; align-items: center; gap: var(--sp-1-5); margin-bottom: var(--sp-1-5); flex-wrap: wrap; }
.myfb-badge {
  font-size: var(--fs-nano); font-weight: var(--wt-bold); letter-spacing: var(--tr-badge);
  padding: var(--sp-05) var(--sp-2); border-radius: var(--r-pill); text-transform: uppercase;
}
.myfb-b-fb { background: var(--accent-soft); color: var(--accent-strong); }
.myfb-b-sug { background: rgb(var(--warn-rgb) / 0.14); color: var(--warn); border: 1px solid rgb(var(--warn-rgb) / 0.35); }
.myfb-s-new { background: var(--bg-hover); color: var(--text-dim); border: 1px solid var(--border); }
.myfb-s-seen { background: var(--accent-faint); color: var(--accent-strong); border: 1px solid var(--accent-border); }
.myfb-s-replied { background: rgb(var(--ok-rgb) / 0.14); color: var(--ok); border: 1px solid rgb(var(--ok-rgb) / 0.35); }
.myfb-time { margin-left: auto; font-size: var(--fs-micro); color: var(--text-faint); font-variant-numeric: tabular-nums; }
.myfb-text { font-size: var(--fs-sm); color: var(--text); white-space: pre-wrap; word-break: break-word; }
.myfb-reply {
  margin-top: var(--sp-2); padding: var(--sp-2) var(--sp-2-5); border-left: 2px solid var(--ok);
  background: rgb(var(--ok-rgb) / 0.08); border-radius: 0 var(--r-xs) var(--r-xs) 0;
  font-size: var(--fs-xs); color: var(--text-dim);
}
.myfb-reply b { color: var(--ok); font-weight: var(--wt-semi); display: block; margin-bottom: var(--sp-05); }
.myfb-note { margin-top: var(--sp-1-5); font-size: var(--fs-xs); color: var(--text-dim); font-style: italic; word-break: break-word; }
.myfb-sec {
  font-size: var(--fs-micro); font-weight: var(--wt-bold); letter-spacing: var(--tr-label);
  text-transform: uppercase; color: var(--text-faint); margin: var(--sp-3-5) 0 var(--sp-1-5);
}
.myfb-empty { color: var(--text-faint); text-align: center; padding: var(--sp-5) 0; font-style: italic; }

/* ============ voice modal ============ */
.voice-modal { width: min(440px, 100%); }
.v-row {
  display: flex; align-items: center; gap: var(--sp-2-5);
  padding: var(--sp-2-5) 0; font-size: var(--fs-sm); color: var(--text-dim);
}
.v-row input[type="range"] { flex: 1; accent-color: var(--accent); }
.v-row b { min-width: 34px; text-align: right; font-variant-numeric: tabular-nums; color: var(--text); }
.v-row select {
  width: 100%; margin-top: var(--sp-1-5);
  background: var(--bg); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--sp-2) var(--sp-2-5); font: var(--fs-xs) var(--font);
}
.v-row select:focus { border-color: var(--accent); }

/* ============ voice reading indicator ============ */
.voice-ind {
  order: 99; margin-top: var(--sp-2); display: flex; align-items: flex-start; gap: var(--sp-2);
  font-size: var(--fs-xs); color: var(--accent-strong);
  background: var(--accent-soft); border: 1px solid var(--accent-border-strong);
  border-radius: var(--r-sm); padding: var(--sp-1-5) var(--sp-2-5);
  max-height: 60px; overflow: hidden;
}
.voice-ind .v-ind-text { flex: 1; min-width: 0; word-break: break-word; max-height: 46px; overflow: hidden; }
.voice-ind::before { content: ""; flex: none; width: 7px; height: 7px; border-radius: 50%; background: currentColor; margin-top: var(--sp-1); animation: micPulse 1s ease-in-out infinite; }
.v-ind-stop { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; padding: 0; border: none; border-radius: var(--r-xs); background: transparent; color: inherit; cursor: pointer; opacity: .8; }
.v-ind-stop:hover { background: var(--accent-strong); color: var(--on-accent); opacity: 1; }

/* ============ my files modal ============ */
.files-modal { width: min(680px, 100%); }
.files-modal h3 { display: flex; align-items: center; gap: var(--sp-2); }
.files-loading { text-align: center; color: var(--text-faint); font-size: var(--fs-xs); padding: var(--sp-6) 0; }
.files-grid { max-height: 62dvh; overflow-y: auto; display: flex; flex-direction: column; gap: var(--sp-4); }
.f-empty { text-align: center; color: var(--text-faint); font-size: var(--fs-sm); padding: var(--sp-8) 0; }
.f-sec-title {
  font-size: var(--fs-micro); font-weight: var(--wt-semi); letter-spacing: var(--tr-label);
  text-transform: uppercase; color: var(--text-faint); margin-bottom: var(--sp-2);
}
.f-none { color: var(--text-faint); font-size: var(--fs-xs); padding: var(--sp-1) 0 var(--sp-3); }
.f-imgs {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--sp-2-5);
}
.f-tile {
  background: var(--bg-elev-2); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--sp-1-5); overflow: hidden;
  transition: border-color var(--dur-fast);
}
.f-tile:hover { border-color: var(--border-strong); }
.f-thumb {
  width: 100%; aspect-ratio: 1; object-fit: cover;
  border-radius: var(--r-sm); cursor: zoom-in; background: var(--code-bg);
  display: block;
}
.f-name {
  font-size: var(--fs-xs); color: var(--text); margin-top: var(--sp-1-5);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.f-meta { font-size: var(--fs-label); color: var(--text-faint); margin-top: var(--sp-05); font-variant-numeric: tabular-nums; }
.f-actions {
  display: flex; gap: var(--sp-05); margin-top: var(--sp-1-5); justify-content: flex-end;
}
.f-actions .icon-btn { width: 26px; height: 26px; }
.f-actions .icon-btn.danger:hover { color: var(--err); }
.f-actions .icon-btn.confirming {
  color: var(--err); background: var(--err-soft); font-size: var(--fs-label); width: auto; padding: 0 var(--sp-1-5);
}
.f-rows { display: flex; flex-direction: column; gap: var(--sp-1-5); }
.f-row {
  display: flex; align-items: center; gap: var(--sp-2-5);
  background: var(--bg-elev-2); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--sp-2) var(--sp-2-5);
  transition: border-color var(--dur-fast);
}
.f-row:hover { border-color: var(--border-strong); }
.f-ic { font-size: var(--fs-h3); flex: none; opacity: 0.85; }
.f-main { flex: 1; min-width: 0; }
.f-row .f-name { margin-top: 0; }

/* ============ incognito button ============ */
.incog-btn {
  position: relative; display: grid; place-items: center;
  width: 32px; height: 32px; border-radius: var(--r-pill);
  background: none; border: 1px solid var(--border);
  color: var(--text-faint); cursor: pointer;
  transition: color var(--dur-fast), border-color var(--dur-fast), background var(--dur-fast);
}
.share-top-btn {
  display: grid; place-items: center;
  width: 32px; height: 32px; border-radius: var(--r-pill);
  background: none; border: 1px solid var(--border);
  color: var(--text-faint); cursor: pointer;
  transition: color var(--dur-fast), border-color var(--dur-fast), background var(--dur-fast);
}
.share-top-btn:hover { color: var(--text); border-color: var(--border-strong); background: var(--bg-hover); }
.share-top-btn:active { transform: scale(0.94); }
.keys-top-btn{
  display:inline-flex;align-items:center;gap:var(--sp-1-5);
  height:32px;padding:0 var(--sp-3);border-radius:var(--r-pill);
  background:none;border:1px solid var(--border);
  color:var(--text-faint);cursor:pointer;text-decoration:none;
  transition:color var(--dur-fast),border-color var(--dur-fast),background var(--dur-fast),transform var(--dur-fast);
}
.keys-top-label{ font-size:var(--fs-sm);font-weight:var(--wt-semi);white-space:nowrap; }
.keys-top-btn:hover{ color:var(--text);border-color:var(--border-strong);background:var(--bg-hover); }
.keys-top-btn:active{ transform:scale(0.94); }
.incog-btn:hover { color: var(--text); border-color: var(--border-strong); background: var(--bg-hover); }
.incog-btn:active { transform: scale(0.94); }
.incog-btn.active {
  color: var(--accent-strong); border-color: var(--accent-border-strong);
  background: var(--accent-soft);
}
.incog-btn .incog-dot {
  position: absolute; top: -1px; right: -1px; width: 8px; height: 8px;
  border-radius: 50%; background: transparent;
  border: 2px solid var(--bg); transition: background var(--dur-fast);
}
.incog-btn.active .incog-dot { background: var(--accent); }
.conv-title.incog { color: var(--accent-strong); }

/* Capacity pill: a verdict, never a count. Not a button — nothing to click,
   the dot alone carries the state and the tooltip carries the words. */
.cap-btn {
  display: grid; place-items: center;
  width: 32px; height: 32px; border-radius: var(--r-pill);
  background: none; border: 1px solid var(--border); flex: none;
}
.cap-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--text-faint);
  transition: background var(--dur-fast);
}
.cap-btn[data-state="ok"] .cap-dot { background: var(--ok); }
.cap-btn[data-state="warn"] .cap-dot { background: var(--warn); }
.cap-btn[data-state="down"] .cap-dot { background: var(--err); }

/* ============ discord button ============ */
.discord-btn {
  display: grid; place-items: center;
  width: 32px; height: 32px; border-radius: var(--r-pill);
  background: var(--discord); border: 1px solid transparent;
  color: var(--on-solid); cursor: pointer; text-decoration: none;
  box-shadow: 0 1px 4px rgb(var(--discord-rgb) / 0.35);
  transition: background var(--dur-fast), transform var(--dur-fast), box-shadow var(--dur-fast);
}
.discord-btn:hover {
  background: var(--discord-strong); color: var(--on-solid);
  box-shadow: 0 0 0 3px rgb(var(--discord-rgb) / 0.25), 0 2px 8px rgb(var(--discord-rgb) / 0.45);
}
.discord-btn:active { transform: scale(0.94); }

/* ============ lightbox ============ */
.lightbox {
  position: fixed; inset: 0; z-index: var(--z-lightbox); background: var(--lb-bg);
  display: none; align-items: center; justify-content: center;
  user-select: none; cursor: zoom-out;
}
.lightbox.open { display: flex; }
.lightbox img {
  max-width: 92vw; max-height: 88dvh; border-radius: var(--r-sm);
  box-shadow: 0 12px 48px rgb(var(--shadow-rgb) / 0.6);
  cursor: grab; object-fit: contain;
  transition: transform var(--dur-fast) ease;
  will-change: transform;
  /* pointer events drive pinch/pan/swipe — the browser must not steal moves */
  touch-action: none;
}
.lightbox img.dragging { cursor: grabbing; transition: none; }
.lightbox .lb-btn {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--lb-chrome-border);
  background: var(--lb-chrome-bg); color: var(--lb-chrome-fg); font-size: var(--fs-lg); cursor: pointer;
  display: grid; place-items: center; transition: background var(--dur-fast);
}
.lightbox .lb-btn:hover { background: var(--lb-chrome-bg-hover); }
.lightbox .lb-btn:active { transform: translateY(-50%) scale(0.92); }
.lightbox .lb-prev { left: 18px; }
.lightbox .lb-next { right: 18px; }
.lightbox .lb-close {
  position: absolute; top: 18px; right: 18px; width: 36px; height: 36px;
  border-radius: 50%; border: 1px solid var(--lb-chrome-border);
  background: var(--lb-chrome-bg); color: var(--lb-chrome-fg); font-size: var(--fs-body); cursor: pointer;
  transition: background var(--dur-fast), transform var(--dur-fast) var(--ease);
}
.lightbox .lb-close:hover { background: var(--lb-chrome-bg-hover); }
.lightbox .lb-close:active { transform: scale(0.92); }
.lightbox .lb-count {
  position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%);
  color: var(--lb-chrome-dim); font: var(--fs-xs) var(--mono);
  font-variant-numeric: tabular-nums;
  background: var(--lb-count-bg); padding: var(--sp-1) var(--sp-3); border-radius: var(--r-pill);
}

/* ---- image annotate (draw / erase / text, then save back as a file) ----
   While editing, the CANVAS becomes the photo: #lb-img is hidden so exactly
   one image is on screen, and the canvas sits on a transparent black
   backdrop so the surrounding chrome reads as dimmed, the way Gemini does it.
   Pan/zoom/swipe are off — a drag is a stroke, not a pan. */
.lb-dim {
  position: absolute; inset: 0; background: #000;
  opacity: 0; pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease);
}
.lb-canvas {
  position: absolute; left: 50%; top: 50%;
  /* -50%,-50% FIRST so the canvas centre lands on the dialog centre; lbSyncCanvas
     then prepends the shared pan offset and scale. Without it the canvas sits
     half a canvas down-right of the photo. */
  transform: translate(-50%, -50%);
  transform-origin: center center;
  pointer-events: none; /* off until the edit mode is armed */
  touch-action: none;
}
.lightbox.annotating { cursor: default; }
.lightbox.annotating #lb-img { visibility: hidden; }
.lightbox.annotating #lb-dim { opacity: 0.55; }
.lightbox.annotating .lb-canvas { pointer-events: auto; cursor: crosshair; }
.lightbox.annotating .lb-btn,
.lightbox.annotating .lb-count { display: none; }

.lb-edit-toggle {
  position: absolute; top: 18px; right: 66px; width: 36px; height: 36px;
  border-radius: 50%; border: 1px solid var(--lb-chrome-border);
  background: var(--lb-chrome-bg); color: var(--lb-chrome-fg); cursor: pointer;
  display: grid; place-items: center;
  transition: background var(--dur-fast), transform var(--dur-fast) var(--ease);
}
.lb-edit-toggle:hover { background: var(--lb-chrome-bg-hover); }
.lb-edit-toggle:active { transform: scale(0.92); }
.lb-edit-toggle[hidden] { display: none; }

.lb-tools {
  position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: var(--sp-1);
  padding: var(--sp-1); border-radius: var(--r-pill);
  background: var(--lb-chrome-bg); border: 1px solid var(--lb-chrome-border);
}
.lb-tools[hidden] { display: none; }
.lb-tool {
  width: 34px; height: 34px; border: 0; border-radius: 50%;
  background: transparent; color: var(--lb-chrome-fg); cursor: pointer;
  display: grid; place-items: center;
  transition: background var(--dur-fast), transform var(--dur-fast) var(--ease);
}
.lb-tool:hover { background: var(--lb-chrome-bg-hover); }
.lb-tool:active { transform: scale(0.92); }
.lb-tool.is-on { background: var(--lb-chrome-bg-hover); }
.lb-tool-go { color: var(--accent); }
.lb-tools-sep { width: 1px; height: 20px; background: var(--lb-chrome-border); }
.lb-size { width: 84px; accent-color: var(--accent); cursor: pointer; }
.lb-colors { display: flex; align-items: center; gap: var(--sp-1); }
.lb-color {
  width: 20px; height: 20px; padding: 0; border-radius: 50%;
  border: 1px solid var(--lb-chrome-border);
  background: var(--sw); cursor: pointer;
  transition: transform var(--dur-fast) var(--ease);
}
.lb-color:hover { transform: scale(1.12); }
.lb-color.is-on { box-shadow: 0 0 0 2px var(--lb-chrome-bg), 0 0 0 3.5px var(--accent); }

.lb-textwrap {
  position: fixed; left: 50%; bottom: 76px; transform: translateX(-50%);
  z-index: var(--z-lightbox);
  display: flex; flex-direction: column; gap: var(--sp-2);
  padding: var(--sp-3); border-radius: var(--r-md);
  background: var(--lb-chrome-bg); border: 1px solid var(--lb-chrome-border);
  width: min(360px, 90vw);
}
.lb-textwrap[hidden] { display: none; }
.lb-text-head {
  font: var(--fs-xs) var(--font);
  color: var(--lb-chrome-dim);
}
.lb-text-input {
  width: 100%; padding: var(--sp-2); border-radius: var(--r-sm);
  border: 1px solid var(--lb-chrome-border);
  background: var(--lb-count-bg); color: var(--lb-chrome-fg);
  font: var(--fs-body) var(--font);
}
.lb-text-actions { display: flex; gap: var(--sp-2); justify-content: flex-end; }
.lb-text-actions button {
  padding: var(--sp-2) var(--sp-3); border-radius: var(--r-pill);
  border: 1px solid var(--lb-chrome-border);
  background: transparent; color: var(--lb-chrome-fg); cursor: pointer;
  font: var(--fs-sm) var(--sans);
}
.lb-text-actions button:hover { background: var(--lb-chrome-bg-hover); }

/* ============ misc ============ */
.jump-btn {
  position: fixed; right: var(--sp-6); bottom: calc(var(--composer-h) + var(--sp-4)); z-index: var(--z-jump);
  width: var(--tap); height: var(--tap); border-radius: 50%;
  background: var(--bg-elev-2); border: 1px solid var(--border-strong); color: var(--text-dim);
  cursor: pointer; display: none; place-items: center; font-size: var(--fs-h3);
  box-shadow: var(--e3);
  transition: color var(--dur), background var(--dur), border-color var(--dur),
    box-shadow var(--dur) var(--ease), transform var(--dur-fast) var(--ease);
}
.jump-btn:hover {
  color: var(--accent-strong); background: var(--bg-elev);
  border-color: var(--accent-border);
  box-shadow: var(--e3), 0 0 0 3px rgb(var(--accent-rgb) / 0.10);
}
.jump-btn:active { transform: scale(0.92); }
.jump-btn.show { display: grid; }

.toast {
  position: fixed; bottom: 26px; left: 50%; transform: translate(-50%, 12px);
  z-index: var(--z-toast); background: var(--bg-elev-2); border: 1px solid var(--border-strong);
  color: var(--text); font-size: var(--fs-sm); padding: var(--sp-2-5) var(--sp-5); border-radius: var(--r-md);
  box-shadow: var(--e3); opacity: 0; pointer-events: none;
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease); max-width: 80vw;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast.err { border-color: var(--err-border); color: var(--err); }
.toast.ok { border-color: rgb(var(--ok-rgb) / 0.5); color: var(--ok); }
.toast.info { border-color: var(--accent-border-strong); color: var(--accent-strong); }

/* ============ responsive ============ */
@media (max-width: 1180px) {
  :root { --sidebar-w: 228px; }
  .msgs-inner { padding: 0 var(--sp-5); }
}

@media (max-width: 880px) {
  #sidebar {
    position: fixed; top: 0; left: -100%;
    width: min(84vw, 300px); min-width: min(84vw, 300px);
    box-shadow: var(--e3);
  }
  #ad-welcome { display: none; }
  /* Drawer hides via `left`, not `transform`: a transformed ancestor would
     become the containing block for `position: fixed`, which the mobile
     bottom-sheet panels (theme/lang live inside #sidebar) depend on. */
  #sidebar.open { left: 0; }
  #side-close { display: grid; }
  .menu-btn { display: grid; }
#side-collapse { display: none; }
  .scrim.show { opacity: 1; pointer-events: auto; }
  .acct-chip .acct-txt { display: none; }
  .acct-chip { padding: 0 var(--sp-1-5); }
  /* Fluid gutter: scales with the viewport between the 480px floor and the
     desktop value instead of stepping at a hard breakpoint. */
  .msgs-inner { padding: 0 clamp(var(--sp-3), 3vw, var(--sp-4)); }
  .composer-zone { padding: var(--sp-2) var(--sp-3) var(--sp-3); }
  .msg.user .body { max-width: 92%; }
  .lightbox .lb-prev { left: 8px; }
  .lightbox .lb-next { right: 8px; }
  .empty-state { padding-top: var(--sp-8); }
  .empty-state h2 { font-size: var(--fs-h2); line-height: var(--lh-h2); letter-spacing: var(--tr-h2); }
}

/* ============ mobile: compact topbar + bottom sheets ============ */
@media (max-width: 640px) {
  .topbar { padding: var(--sp-2) var(--sp-3); gap: var(--sp-2); }
  .topbar .conv-title { font-size: var(--fs-body); }
  .topbar-right { gap: var(--sp-1-5); }
  /* model button collapses to logo + chevron */
  .model-btn { padding: var(--sp-1) var(--sp-1-5); gap: var(--sp-1); }
  #model-btn-name { display: none; }
  /* discord is a link; the landing footer still carries it */
  .discord-btn { display: none; }

  /* dropdowns become bottom sheets: fixed, full-width, slide up */
  .model-panel, .theme-panel, .lang-panel {
    display: block; visibility: hidden; opacity: 0; pointer-events: none;
    position: fixed; left: 0; right: 0; top: auto; bottom: 0; width: 100%;
    max-height: min(70dvh, 520px); overflow-y: auto;
    border-radius: var(--r-lg) var(--r-lg) 0 0; border-bottom: none;
    box-shadow: 0 -10px 40px rgb(var(--shadow-rgb) / 0.45);
    animation: none;
    z-index: var(--z-modal);
    transform: translateY(105%);
    transition: transform var(--dur-slow) var(--ease), opacity var(--dur) var(--ease), visibility var(--dur-slow);
    padding-top: var(--sp-5);
  }
  .model-panel::before, .theme-panel::before, .lang-panel::before {
    content: ""; position: absolute; top: 8px; left: 50%; transform: translateX(-50%);
    width: 40px; height: 4px; border-radius: var(--r-2xs); background: var(--border-strong);
  }
  .model-panel.open, .theme-panel.open, .lang-panel.open {
    visibility: visible; opacity: 1; pointer-events: auto; transform: none;
  }
  .side-foot .theme-panel, .side-foot .lang-panel {
    top: auto; bottom: 0; left: 0; right: 0; max-width: none;
  }
  /* virtual keyboards make Enter/Shift+Enter hints meaningless */
  .composer-hint { display: none; }
  .composer-zone { padding: var(--sp-2) var(--sp-2) var(--sp-3); }

  /* topbar: keep the row to one line on ~360px — the API-keys verb lives in
     the more-menu and /keys, so the button collapses to an icon here. */
  .keys-top-btn { padding: 0 var(--sp-2); }
  .keys-top-label { display: none; }

  /* code blocks: the copy verb needs a real target and long lines read best
     when the block uses the full bubble width instead of inset margins. */
  .msg .body pre { margin-inline: calc(var(--sp-1-5) * -1); border-left-width: 0; border-right-width: 0; }
  .msg .body pre .code-head { padding: var(--sp-1) var(--sp-2-5); }
  .msg .body pre .code-head button { padding: var(--sp-1) var(--sp-3); }
  .msg .body pre code { padding: var(--sp-2-5) var(--sp-3); }
  /* streaming rows of icons under a short bubble wrap into one tight line */
  .msg .actions { gap: var(--sp-05); }

  /* attach previews run one thin scrollable line instead of wrapping the
     composer into a tall stack; the strip scrollbar is decorative noise. */
  .attach-preview {
    flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
  }
  .attach-preview::-webkit-scrollbar { display: none; }
  .attach-preview .ap-item, .attach-preview .attach-chip { flex: none; }

  /* toasts live above the keyboard — give long ones room and time */
  .toast { max-width: 92vw; text-align: center; }

  /* ============ modals become bottom sheets ============
     Same shape language as the model/theme/lang panels: full-width, rounded
     top, slides up, inner scroll, safe-area foot. Actions stick to the sheet
     bottom so "Lưu" is always reachable one-thumbed. */
  .modal-backdrop { place-items: end stretch; padding: 0; }
  .modal {
    position: relative; width: 100%;
    max-width: none;
    max-height: min(86dvh, 640px); overflow-y: auto; overscroll-behavior: contain;
    border-radius: var(--r-lg) var(--r-lg) 0 0; border-bottom: none;
    padding-bottom: 0;
    animation: sheetIn var(--dur-slow) var(--ease);
  }
  /* per-modal width caps (donate 380, settings 860...) would re-shrink the
     sheet to a floating card — re-assert full width, same specificity but later. */
  .modal.donate-modal, .modal.settings-modal { max-width: none; width: 100%; }
  /* the settings panel scrolls its own body, so the sheet must not scroll too */
  .modal.settings-modal { overflow: hidden; padding: 0; }
  .modal::before {
    content: ""; position: sticky; top: var(--sp-1); z-index: var(--z-inlayer);
    display: block; width: 40px; height: 4px; margin: 0 auto var(--sp-1);
    border-radius: var(--r-2xs); background: var(--border-strong);
  }
  .modal .m-actions {
    position: sticky; bottom: 0; z-index: var(--z-inlayer);
    margin: var(--sp-3) calc(var(--sp-5) * -1) 0;
    padding: var(--sp-2) var(--sp-5) max(var(--sp-3), calc(env(safe-area-inset-bottom) + var(--sp-2)));
    background: var(--bg-elev); border-top: 1px solid var(--border);
  }
  /* sheets that render their own foot rows (share/profile) keep the inset too */
  .modal > :last-child:not(.m-actions) {
    margin-bottom: max(var(--sp-3), calc(env(safe-area-inset-bottom)));
  }
  /* settings sheet: the inset belongs on the scrollable body, not the grid */
  .modal.settings-modal > :last-child:not(.m-actions) { margin-bottom: 0; }
  .modal.settings-modal .settings-body {
    padding-bottom: max(var(--sp-5), calc(env(safe-area-inset-bottom) + var(--sp-3)));
  }

  /* generated images: the column width rides the viewport — two-up on
     typical phones, one-up below ~356px, three-up on large ones — instead of
     snapping between fixed counts. */
  .img-grid { grid-template-columns: repeat(auto-fill, minmax(min(160px, 45vw), 1fr)); }
}
@keyframes sheetIn {
  from { transform: translateY(100%); }
  to { transform: none; }
}

@media (max-width: 480px) {
  .notif-btn { display: none; }
  .keys-top-btn { display: none; }
  .acct-chip { padding: 0 var(--sp-1); }
  .acct-chip .acct-av { width: 30px; height: 30px; }
  .toolbar { padding: 0 var(--sp-05); }
  .msg.user .body { max-width: 96%; }
  .toast { bottom: calc(var(--composer-h) + var(--sp-6) + var(--sp-1)); }
}

/* Desktop sidebar collapse: the chevron in the sidebar header folds the sidebar
   into a slim rail so the chat takes nearly the full width. Mobile keeps the
   overlay drawer instead. */
@media (min-width: 881px) {
  #side-collapse { display: grid; }
  #side-collapse svg { transition: transform var(--dur-slow) var(--ease); }
  body.sidebar-collapsed #sidebar { width: 44px; min-width: 44px; }
  body.sidebar-collapsed .logo,
  body.sidebar-collapsed .new-btn,
  body.sidebar-collapsed .search-box,
  body.sidebar-collapsed .side-btn,
  body.sidebar-collapsed .side-label,
  body.sidebar-collapsed .conv-list,
  body.sidebar-collapsed .ad-sidebar,
  body.sidebar-collapsed .side-foot { display: none; }
  body.sidebar-collapsed .side-head { justify-content: center; padding: var(--sp-4) 0; }
  body.sidebar-collapsed #side-collapse svg { transform: rotate(180deg); }
}

/* Touch targets: every control in this file sits between 20 and 34px, which is
   fine for a mouse and unusable with a thumb. Grow them only on coarse pointers
   so the desktop density is untouched. */
@media (any-pointer: coarse) {
  .icon-btn,
  .conv-actions .icon-btn,
  .f-actions .icon-btn,
  .msg .actions .icon-btn { min-width: 44px; min-height: 44px; }
  /* no hover on touch — keep message actions visible in flow, hugging the
     bubble content above just like the desktop hover pill does. */
  .msg .actions {
    position: static; margin-top: var(--sp-05);
    padding: 0; background: transparent; border: none; box-shadow: none;
    opacity: 1; visibility: visible; pointer-events: auto; transform: none;
  }
  .tool-btn, .theme-btn, .more-btn, .lang-btn, .donate-btn,
  .incog-btn, .share-top-btn, .send-btn, .stop-btn, .cap-btn { width: 44px; height: 44px; }
  .discord-btn { width: 44px; height: 44px; }
  .notif-btn { width: 44px; height: 44px; }
  .acct-chip { height: 44px; padding: 0 var(--sp-4) 0 var(--sp-2); }
  .acct-chip .acct-av { width: 28px; height: 28px; }
  .att-rm { width: 32px; height: 32px; }
  .jump-btn { width: 44px; height: 44px; }
  .chip-dd .chip { height: 36px; }
  /* small tap targets that slipped under 44px on touch */
  .ad-anchor-btn { width: 44px; height: 44px; }
  .src-toggle, .src-chip { min-height: 44px; display: inline-flex; align-items: center; }
  .chip-opt { min-height: 44px; }
  .m-opt { min-height: 44px; }
  .p-preset { min-width: 44px; min-height: 44px; }
  .lightbox .lb-btn, .lightbox .lb-close { width: 44px; height: 44px; }
  /* Touch has no hover, so the conversation row actions must stay reachable. */
  .conv-actions { opacity: 1; visibility: visible; transform: none; }
}

/* ============ pressed state ============
   One shared rule instead of eighteen near-identical :active blocks. Every
   hoverable row in the app confirms the press by dipping 1px. */
.conv-item:active, .m-opt:active, .theme-opt:active, .lang-opt:active,
.more-item:active, .chip-opt:active, .f-tile:active, .f-row:active,
.acct-tab:active, .fb-reason:active, .m-group-head:active, .src-toggle:active,
.att-rm:active, .tip-dismiss:active, .profile-chip-btn:active,
.logo-link:active {
  transform: translateY(1px);
}

/* tts accent */
.msg .actions .icon-btn.tts-btn,
.tts-btn.playing { color: var(--accent-strong); }

/* ============ skeletons ============ */
/* Replaces the bare "đang tải…" strings — a skeleton that matches the shape of
   what is coming reads as progress; a line of grey text reads as a stall. */
.skel {
  position: relative; overflow: hidden;
  background: var(--bg-elev-2); border-radius: var(--r-xs);
}
.skel::after {
  content: ""; position: absolute; inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, var(--bg-hover), transparent);
  animation: skelSweep 1.3s ease-in-out infinite;
}
@keyframes skelSweep { to { transform: translateX(100%); } }
.skel-line { height: 10px; margin-bottom: var(--sp-1-5); }
.skel-line.w-70 { width: 70%; }
.skel-line.w-45 { width: 45%; }
.skel-conv {
  display: flex; flex-direction: column; gap: var(--sp-1-5);
  padding: var(--sp-2-5) var(--sp-2-5); border-radius: var(--r-sm);
}
.skel-tile { aspect-ratio: 1; border-radius: var(--r-sm); }
.skel-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--sp-3);
}
@media (prefers-reduced-motion: reduce) {
  .skel::after { animation: none; opacity: 0.4; }
}

/* ============ classes referenced by app.js that had no rules ============ */
/* .stale — messages start dimmed and fade in after the transcript loads */
.msg.stale, .stale { opacity: 0.55; transition: opacity var(--dur) var(--ease); }
/* .f-sec — a section wrapper in the files modal */
.f-sec { display: flex; flex-direction: column; }
.f-sec + .f-sec { margin-top: var(--sp-2); }
/* .conv-empty — "no conversations" / "no results" in the sidebar */
.conv-empty {
  padding: var(--sp-6) var(--sp-4); text-align: center;
  color: var(--text-faint); font-size: var(--fs-sm);
}
/* .renaming — the row swaps its title for an input; the hover actions and the
   timestamp only get in the way while you are typing. */
.conv-item.renaming { background: var(--bg-elev-2); border-color: var(--accent-border); }
.conv-item.renaming .conv-actions { display: none; }
.conv-item.renaming .conv-meta { opacity: 0.45; }
/* .incog-hint — sits above the (empty) history list while incognito is on */
.incog-hint {
  padding: var(--sp-4) var(--sp-3);
  color: var(--accent-strong);
  background: var(--accent-faint);
  border: 1px dashed var(--accent-border);
  border-radius: var(--r-sm);
  margin: 0 var(--sp-1) var(--sp-2);
  font-size: var(--fs-xs);
}

.sr-only {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ============ syntax highlighting on the light theme ============ */
/* vendor/github-dark.min.css is a hardcoded dark palette, so every code block
   stayed near-black in `paper`. Remap the tokens rather than ship a second
   vendored theme. */
body[data-theme="paper"] .hljs { background: var(--code-bg); color: #24292f; }
body[data-theme="paper"] .hljs-doctag,
body[data-theme="paper"] .hljs-keyword,
body[data-theme="paper"] .hljs-meta .hljs-keyword,
body[data-theme="paper"] .hljs-template-tag,
body[data-theme="paper"] .hljs-template-variable,
body[data-theme="paper"] .hljs-type,
body[data-theme="paper"] .hljs-variable.language_ { color: #cf222e; }
body[data-theme="paper"] .hljs-title,
body[data-theme="paper"] .hljs-title.class_,
body[data-theme="paper"] .hljs-title.class_.inherited__,
body[data-theme="paper"] .hljs-title.function_ { color: #8250df; }
body[data-theme="paper"] .hljs-attr,
body[data-theme="paper"] .hljs-attribute,
body[data-theme="paper"] .hljs-literal,
body[data-theme="paper"] .hljs-meta,
body[data-theme="paper"] .hljs-number,
body[data-theme="paper"] .hljs-operator,
body[data-theme="paper"] .hljs-selector-attr,
body[data-theme="paper"] .hljs-selector-class,
body[data-theme="paper"] .hljs-selector-id,
body[data-theme="paper"] .hljs-variable { color: #0550ae; }
body[data-theme="paper"] .hljs-meta .hljs-string,
body[data-theme="paper"] .hljs-regexp,
body[data-theme="paper"] .hljs-string { color: #0a3069; }
body[data-theme="paper"] .hljs-built_in,
body[data-theme="paper"] .hljs-symbol { color: #953800; }
body[data-theme="paper"] .hljs-code,
body[data-theme="paper"] .hljs-comment,
body[data-theme="paper"] .hljs-formula { color: #6e7781; }
body[data-theme="paper"] .hljs-name,
body[data-theme="paper"] .hljs-quote,
body[data-theme="paper"] .hljs-selector-pseudo,
body[data-theme="paper"] .hljs-selector-tag { color: #116329; }
body[data-theme="paper"] .hljs-subst { color: #24292f; }
body[data-theme="paper"] .hljs-section { color: #0550ae; font-weight: var(--wt-bold); }
body[data-theme="paper"] .hljs-bullet { color: #953800; }
body[data-theme="paper"] .hljs-emphasis { color: #24292f; font-style: italic; }
body[data-theme="paper"] .hljs-strong { color: #24292f; font-weight: var(--wt-bold); }
body[data-theme="paper"] .hljs-addition { color: #116329; background-color: #dafbe1; }
body[data-theme="paper"] .hljs-deletion { color: #82071e; background-color: #ffebe9; }
body[data-theme="paper"] .body pre .code-head button { color: var(--text-dim); }

/* ============ profile (name + avatar) ============ */
.profile-chip { padding: var(--sp-05) var(--sp-05) 0; }
.profile-chip-btn {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%;
  padding: var(--sp-1-5) var(--sp-2); border: none; border-radius: var(--r-sm);
  background: none; color: var(--text); cursor: pointer; text-align: left;
  transition: background var(--dur-fast);
}
.profile-chip-btn:hover { background: var(--bg-hover); }
.profile-chip-btn .p-chip-av {
  flex: none; width: 22px; height: 22px; border-radius: 50%;
  display: grid; place-items: center; overflow: hidden;
  background: var(--bg-elev-2); border: 1px solid var(--border-strong);
}
.profile-chip-btn .p-chip-av svg { width: 13px; height: 13px; }
.profile-chip-btn .p-chip-name {
  flex: 1; min-width: 0; font-size: var(--fs-xs); font-weight: var(--wt-semi);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.u-avatar { width: 100%; height: 100%; object-fit: cover; display: block; }
.u-avatar.preset {
  display: flex; align-items: center; justify-content: center;
  color: var(--on-solid); font-size: var(--fs-micro); font-weight: var(--wt-bold); line-height: 1;
}
.msg .meta .avatar.clickable { cursor: pointer; }
.msg .meta .avatar.clickable:hover { border-color: var(--accent); }
.msg .meta .avatar.user .u-avatar { width: 100%; height: 100%; }
.msg .meta .avatar.user .u-avatar.preset { font-size: var(--fs-micro); }
.profile-modal { width: min(460px, 100%); }
.p-row {
  display: flex; align-items: center; gap: var(--sp-2-5);
  padding: var(--sp-2-5) 0; font-size: var(--fs-sm); color: var(--text-dim);
}
.p-row input {
  flex: 1; background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: var(--sp-2) var(--sp-2-5); font: var(--fs-xs) var(--font);
}
.p-row input:focus { border-color: var(--accent); }
.p-avatar-row { display: flex; align-items: center; gap: var(--sp-3-5); padding: var(--sp-2-5) 0; }
.p-avatar-row .p-preview {
  flex: none; width: 56px; height: 56px; border-radius: 50%;
  display: grid; place-items: center; overflow: hidden;
  background: var(--bg-elev-2); border: 1px solid var(--border-strong);
}
.p-avatar-row .p-preview img { width: 100%; height: 100%; object-fit: cover; }
.p-avatar-row .p-preview .p-pv {
  width: 100%; height: 100%; display: flex; align-items: center;
  justify-content: center; color: var(--on-solid); font-size: var(--fs-h2); font-weight: var(--wt-bold);
}
.p-avatar-row .p-preview .p-pv.p-pv-default { background: none; color: var(--accent-strong); }
.p-avatar-row .p-preview .p-pv.p-pv-default svg { width: 26px; height: 26px; }
.p-avatar-actions { display: flex; flex-direction: column; gap: var(--sp-2); align-items: flex-start; }
.p-avatar-actions .ghost {
  padding: var(--sp-1-5) var(--sp-3); border-radius: var(--r-sm); font: var(--wt-semi) var(--fs-xs) var(--font); cursor: pointer;
  border: 1px solid var(--border-strong); background: var(--bg-elev-2); color: var(--text);
  transition: background var(--dur-fast);
}
.p-avatar-actions .ghost:hover { background: var(--bg-hover); border-color: var(--accent-border); }
.p-avatar-actions .ghost-danger {
  padding: var(--sp-1-5) var(--sp-3); border-radius: var(--r-sm); font: var(--wt-semi) var(--fs-xs) var(--font); cursor: pointer;
  border: none; background: none; color: var(--err); transition: background var(--dur-fast);
}
.p-avatar-actions .ghost-danger:hover { background: var(--err-soft); }
.p-presets { display: flex; flex-wrap: wrap; gap: var(--sp-2); padding: var(--sp-1-5) 0 var(--sp-05); }
.p-preset {
  flex: none; width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center; cursor: pointer;
  border: 2px solid transparent; padding: 0; transition: transform var(--dur-fast);
}
.p-preset:hover { transform: scale(1.1); }
.p-preset.selected { border-color: var(--text); box-shadow: 0 0 0 2px var(--bg-elev); }
.p-preset .p-pp {
  width: 22px; height: 22px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: rgb(255 255 255 / 0.22); color: var(--on-solid);
  font-size: var(--fs-micro); font-weight: var(--wt-bold); line-height: 1;
}

/* ============ share modal ============ */
.share-modal { width: min(520px, 100%); }
.s-row { display: flex; flex-direction: column; gap: var(--sp-1-5); padding: var(--sp-2) 0 var(--sp-05); }
.s-radio {
  display: flex; align-items: center; gap: var(--sp-2-5); padding: var(--sp-2) var(--sp-2-5);
  border: 1px solid var(--border); border-radius: var(--r-md); cursor: pointer;
  font-size: var(--fs-sm); color: var(--text); background: var(--bg-elev);
  transition: border-color var(--dur-fast), background var(--dur-fast);
}
.s-radio:hover { border-color: var(--border-strong); background: var(--bg-hover); }
.s-radio:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.s-radio input { accent-color: var(--accent); margin: 0; }
.s-range { display: none; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
.s-range.on { display: grid; }
.s-result { border-top: 1px solid var(--border); margin-top: var(--sp-1-5); }
.share-line { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
.share-line input {
  flex: 1; min-width: 0; font-size: var(--fs-xs); font-family: var(--mono);
  background: var(--bg); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--sp-2) var(--sp-2-5);
  transition: border-color var(--dur) var(--ease);
}
.share-line input:focus { border-color: var(--accent); }
.share-line .primary {
  flex: none; font: var(--wt-semi) var(--fs-xs) var(--font); cursor: pointer;
  padding: var(--sp-2) var(--sp-4); border-radius: var(--r-pill);
  border: 1px solid transparent; background: var(--accent); color: var(--on-accent);
  transition: background var(--dur-fast), border-color var(--dur-fast);
}
.share-line .primary:hover { background: var(--accent-strong); }
.share-line .primary:active { transform: translateY(1px); }
.s-list { display: flex; flex-direction: column; gap: var(--sp-2); margin-top: var(--sp-3); }
.s-empty { font-size: var(--fs-xs); color: var(--text-faint); padding: var(--sp-2-5) 0; }
.s-link-row {
  display: flex; align-items: center; gap: var(--sp-2-5); padding: var(--sp-2-5) var(--sp-2-5);
  border: 1px solid var(--border); border-radius: var(--r-md); background: var(--bg-elev);
}
.s-link-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--sp-05); }
.s-link-url {
  font-size: var(--fs-micro); font-family: var(--mono); color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.s-link-info small { font-size: var(--fs-label); color: var(--text-faint); }
.s-link-row .ghost-danger {
  flex: none; font: var(--wt-semi) var(--fs-xs) var(--font); cursor: pointer;
  padding: var(--sp-1-5) var(--sp-3); border-radius: var(--r-sm);
  border: 1px solid var(--border-strong); background: var(--bg-elev-2); color: var(--err);
  transition: background var(--dur-fast), border-color var(--dur-fast);
}
.s-link-row .ghost-danger:hover { background: var(--err-soft); border-color: var(--err); }
.s-link-row .ghost-danger:active { transform: translateY(1px); }
.share-modal select {
  flex: 1; min-width: 0; cursor: pointer;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: var(--sp-2) var(--sp-2-5) var(--sp-2) var(--sp-3); font: var(--fs-xs) var(--font);
  transition: border-color var(--dur) var(--ease);
}
.share-modal select:focus { border-color: var(--accent); }

/* ============ safe areas (viewport-fit=cover) ============
   The app is an installed PWA with a translucent status bar, so on notch /
   home-indicator devices fixed chrome must never assume the viewport starts
   at 0. env() resolves to 0px everywhere else, so max() keeps the exact
   current spacing on desktop and non-cover contexts. Appended last so these
   win the cascade over the media-block shorthand paddings above. */
.topbar { padding-top: max(var(--sp-3), env(safe-area-inset-top)); }
.side-head { padding-top: max(var(--sp-4), env(safe-area-inset-top)); }
.side-foot { padding-bottom: max(var(--sp-2-5), env(safe-area-inset-bottom)); }
.composer-zone { padding-bottom: max(var(--sp-4), env(safe-area-inset-bottom)); }
/* Keyboard open: the composer owns the screen — the jump pill and toasts
   would just overlap it, so they step aside until --kb collapses again. */
.kb-open .jump-btn { opacity: 0; pointer-events: none; }
.jump-btn {
  right: max(var(--sp-6), env(safe-area-inset-right));
  bottom: calc(var(--composer-h) + max(var(--sp-4), env(safe-area-inset-bottom)));
}
.lb-close { top: max(18px, env(safe-area-inset-top)); right: max(18px, env(safe-area-inset-right)); }
.lb-prev { left: max(18px, env(safe-area-inset-left)); }
.lb-next { right: max(18px, env(safe-area-inset-right)); }
.lb-count { bottom: max(16px, env(safe-area-inset-bottom)); }

/* Bottom sheets need their own foot inset; the handle stays at the visual top. */
@media (max-width: 640px) {
  .model-panel, .theme-panel, .lang-panel {
    padding-bottom: max(var(--sp-5), calc(env(safe-area-inset-bottom) + var(--sp-3)));
  }
}

/* ============ iOS focus zoom guard ============
   Safari force-zooms any focused field below 16px. Every input in the app
   inherits the 15px --fs-body, so each composer focus jumped the viewport.
   16px only where the zoom can happen (coarse pointers / phone widths). */
@media (any-pointer: coarse), (max-width: 640px) {
  :is(#input, .search-box input, .rename-input, .p-row input,
      .modal textarea, .fb-modal .fb-text, .compose-modal .compose-text,
      .share-line input, .donate-addr, .v-row select, .share-modal select) {
    font-size: 16px;
  }
}

/* ============ topbar elevation on scroll ============
   Flat hairline at rest; once the transcript scrolls under the bar it gets a
   tinted shadow so content passing beneath reads as depth, not overlap. */
.topbar { transition: background var(--dur) ease, box-shadow var(--dur-slow) var(--ease); }
.topbar.scrolled { box-shadow: 0 6px 18px rgb(var(--shadow-rgb) / 0.13); }

/* ============ collapsed rail keeps its verbs ============
   The 44px desktop rail used to hide everything except the fold chevron —
   a dead column. New-chat and My-files survive as icon-only buttons. */
@media (min-width: 881px) {
  body.sidebar-collapsed .new-btn,
  body.sidebar-collapsed .side-btn {
    display: flex;
    justify-content: center;
    width: 32px; min-width: 32px;
    margin-left: auto; margin-right: auto;
    padding: var(--sp-2) 0;
    overflow: hidden;
  }
  body.sidebar-collapsed .new-btn span,
  body.sidebar-collapsed .side-btn span { display: none; }
}

/* ============ composer spotlight border (hover-capable devices) ============
   Cursor-tracked glow riding the shell's border ring. JS feeds --mx/--my;
   defaults park the hotspot off-canvas so nothing shows before first move. */
@media (hover: hover) and (pointer: fine) {
  .composer-shell { position: relative; }
  .composer-shell::after {
    content: ""; position: absolute; inset: -1px;
    border-radius: inherit; padding: 1px; pointer-events: none;
    background: radial-gradient(170px circle at var(--mx, -300px) var(--my, -300px),
      rgb(var(--accent-rgb) / 0.4), transparent 72%);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask-composite: exclude;
    opacity: 0;
    transition: opacity var(--dur-slow) var(--ease);
  }
  .composer-shell:hover::after,
  .composer-shell:focus-within::after { opacity: 1; }
}
/* ---- thinking effort selector (composer row) ---- */
.composer .composer-think { flex: none; }
.composer .composer-think .chip { height: 32px; padding: 0 var(--sp-2); font-size: var(--fs-xs); border-radius: var(--r-sm); border-color: var(--border-strong); background: var(--bg-elev-2); }
.composer-think.has-effort .chip { color: var(--accent-strong); border-color: var(--accent-border-strong); background: var(--accent-faint); }
.composer-think.has-effort .chip b { color: var(--accent-strong); }
.think-menu { left: auto; right: -6px; bottom: calc(100% + 8px); min-width: 150px; }

/* ---- mobile composer polish ----
   Small screens: drop decorative weight from the toolbar row and let the
   effort pill read as a compact value chip. Lives at end-of-file so it wins
   the cascade against the base rules above without specificity games. */
@media (max-width: 640px) {
  .toolbar { gap: var(--sp-2); }
  .tool-btn { border-color: var(--border); background: transparent; }
  .composer .composer-think .chip { height: 30px; padding: 0 var(--sp-2); }
  .composer-think.has-effort .chip {
    color: var(--accent-strong);
    border-color: var(--accent-border);
    background: transparent;
  }
  /* effort is on: the accent value alone carries the meaning */
  .composer-think.has-effort .chip-prefix { display: none; }
}

/* ============ touch conversation rows: swipe reveal + long-press sheet ============
   Four 26px icons beside the title are thumb-hostile. On coarse pointers the
   actions become a solid slab revealed by dragging the row left (app.js toggles
   .swiped), and a long-press opens the same verbs as a bottom sheet driving the
   hidden buttons. Desktop keeps the hover pill untouched. Lives at end-of-file
   to win the cascade over the coarse-pointer reveal above. */
@media (any-pointer: coarse) {
  .conv-item { overflow: hidden; }
  .conv-item .conv-main { transition: transform var(--dur) var(--ease); }
  .conv-item.swiped .conv-main { transform: translateX(-200px); }
  .conv-item.swiped { background: var(--bg-elev-2); border-color: var(--border-strong); }
  .conv-item .conv-actions {
    position: absolute; top: 0; bottom: 0; right: 0;
    padding: 0 var(--sp-1-5);
    background: var(--bg-elev-2);
    box-shadow: -14px 0 18px rgb(var(--shadow-rgb) / 0.16);
    transition: none;
  }
  .conv-item:not(.swiped) .conv-actions {
    opacity: 0; visibility: hidden; pointer-events: none;
  }
  /* long-press action sheet */
  .conv-ctx .ctx-title {
    max-width: 100%; margin: 0 0 var(--sp-2);
    font-size: var(--fs-body); font-weight: var(--wt-semi); color: var(--text-faint);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .conv-ctx .more-item { width: 100%; min-height: 48px; }
  .conv-ctx .more-item.danger { color: var(--err); }
  /* delete confirmation dialog raised from the sheet */
  .modal .m-desc {
    max-width: 100%;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
}

/* ============ ultra-narrow phones (~320-400px) ============
   Everything on the topbar row shrinks one notch so the title keeps room
   before the breakpoints hide the remaining verbs at 480. */
@media (max-width: 400px) {
  .topbar { padding: var(--sp-2); gap: var(--sp-1-5); }
  .topbar-right { gap: var(--sp-1); }
  .model-btn { padding: var(--sp-1); }
  .keys-top-btn { padding: 0 var(--sp-1); }
  .acct-chip { padding: 0 var(--sp-05); }
}

/* ============ mobile top anchor ad (ads-mobile.js) ============
   Slides down from the top edge on narrow viewports after its AdSense
   unit reports a fill. JS drives the states: hidden until consent+fill,
   .kilg-min shrinks it to the corner pill, dismiss hides for the session.
   The bar uses transform only (never display:none while pushable) so the
   unit stays fillable. Lives at end-of-file to win the cascade. */
.ad-anchor {
  position: fixed;
  left: 0; right: 0; top: 0;
  z-index: var(--z-anchor);
  pointer-events: none;
}
.ad-anchor-bar {
  pointer-events: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1-5);
  padding: max(var(--sp-1-5), env(safe-area-inset-top)) var(--sp-2) var(--sp-1-5);
  background: var(--bg-elev-2);
  border-bottom: 1px solid var(--border);
  box-shadow: 0 8px 24px rgb(var(--shadow-rgb) / 0.18);
  transform: translateY(-110%);
  transition: transform var(--dur-slow) var(--ease);
}
/* visible = container un-hidden and not minimized (both driven by JS) */
.ad-anchor:not([hidden]):not(.kilg-min) .ad-anchor-bar { transform: translateY(0); }
.ad-anchor ins.adsbygoogle { flex: none; }

.ad-anchor-btn {
  flex: none;
  width: 26px; height: 26px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--border);
  border-radius: var(--r-xs);
  background: var(--bg-elev);
  color: var(--text-faint);
  font: var(--fs-sm) var(--font);
  line-height: 1;
  cursor: pointer;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.ad-anchor-btn:hover { color: var(--text); border-color: var(--border-strong); }

/* minimized pill: tap to slide the banner back down */
.ad-anchor-pill {
  pointer-events: auto;
  position: absolute;
  right: var(--sp-2);
  top: max(var(--sp-1-5), env(safe-area-inset-top));
  display: none;
  align-items: center;
  padding: var(--sp-1) var(--sp-2-5);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
  background: var(--bg-elev-2);
  color: var(--text-faint);
  font: var(--fs-xs) var(--font);
  cursor: pointer;
  box-shadow: var(--e2);
}
.ad-anchor.kilg-min .ad-anchor-pill { display: inline-flex; }
.ad-anchor.kilg-min .ad-anchor-bar { transform: translateY(-110%); pointer-events: none; }

/* keep the topbar clear of the open banner on phones (JS toggles
   html.adm-anchor-open when the banner is expanded) */
@media (max-width: 880px) {
  html.adm-anchor-open .main { padding-top: 66px; }
}
/* The ad unit is a fixed 320px wide, so the bar needs 320 + 2x26px buttons
   + 2x6px gaps + 2x8px padding = 400px before it fits at all. The old 359px
   threshold was below that arithmetic, which is why 360px overflowed by 12px.
   ads-mobile.js shrinks the unit to a corner pill below this breakpoint. */
@media (max-width: 399px) {
  .ad-anchor-bar ins.adsbygoogle { max-width: calc(100vw - 112px); }
}
@media (min-width: 881px) {
  .ad-anchor { display: none !important; }
}
@media (prefers-reduced-motion: reduce) {
  .ad-anchor-bar { transition: none; }
}
