/* CSS Variables for Premium Design System
   =========================================================================
   THEME ARCHITECTURE (dark default here in :root; light overrides in the
   [data-theme="light"] block right below) - see client.js's "THEME
   SWITCHING" section for how the data-theme attribute itself gets set
   (persisted in localStorage, applied before first paint to avoid a
   flash-of-wrong-theme, and kept in sync with the Preferences modal).

   Two families of tokens:
   - The original small set (--bg-dark/--bg-card/--primary/--text-* etc.) -
     unchanged in spirit, still what most of this file uses directly.
   - A newer --surface / --overlay / --text-table set added alongside it,
     covering colors that used to be hardcoded hex/rgba literals scattered
     through specific components (the results table, the SQL/NL input
     boxes, dropdown menus, the scrollbar, etc.) - each was a dark-only
     "white wash over a dark surface" trick (e.g. rgba(255,255,255,0.05) as
     a subtle hover/border), which does NOT work unchanged on a light
     background, so those all needed their own explicit light-theme value
     rather than just being left as literals.

   --primary/--secondary/--danger keep the SAME hue in both themes (still
   unmistakably "this app's green", "error red", etc.) but the light theme
   shifts each to a slightly deeper shade - the original values were tuned
   for light text on a near-black page and don't have enough contrast for
   dark text on a light one wherever they're used as TEXT color (not just
   as a badge/button background), e.g. .status-success, .stat-card-title.

   PALETTE "SIGNAL" PASS (color-scheme review, see the header/input mockups
   this was proposed against): --accent-cyan was quietly the app's REAL
   on-screen accent - the wordmark, footer link, mic icon, header icon
   buttons, progress banners, and #stopBtn.btn-stop all painted with it -
   while --primary's green only ever showed up as the status dot, and
   --secondary's indigo didn't appear in the header/input chrome at all.
   Fixed (at the time) by making --accent-cyan/--accent-cyan-rgb/
   --on-accent-cyan alias directly to --primary/--primary-rgb/--on-primary
   in both themes, rather than rewriting the 30+ call sites that reference
   the cyan tokens by name - every one of them rendered in the app's actual
   green without any other change. The brand mark itself (the inline header
   SVG, logo.svg, favicon.svg and its derived rasters) was recolored the
   same way, from its old sky-blue family to the matching emerald family,
   so the logo matched the accent it sat next to instead of predating it.
   An earlier version of this pass also gave the model badge its own indigo
   tint from --secondary to give that token a visible header job - reverted
   after review clashed visually with the rest of the then all-green
   header, so the model badge stays plain/neutral like the DB badge beside
   it (both just .status-badge's default styling) and --secondary keeps
   only its pre-existing jobs (the inline "add a chart" link, a couple of
   secondary badges - see its call sites below). --bg-dark/--surface-1 also
   moved apart slightly (see their own comments) since they used to sit
   close enough in value that the results panel read as flush with the
   page background.

   DARK-MODE "WHITE ACCENT" PASS (per explicit request - the green accent
   above read as too dominant once it was everywhere): --accent-cyan/
   --accent-cyan-rgb/--on-accent-cyan/--accent-cyan-glow now carry their
   OWN white-based values in the dark theme below, decoupled from --primary
   - every call site named in the paragraph above (wordmark, footer link,
   mic icon, header icon buttons, progress banners, tabs, focus rings,
   borders/glows, the auth avatar/menu, the guided tour, the Schema
   Viewer's own accents, and so on) renders in white now, again with no
   per-call-site change needed. --primary itself is UNCHANGED and keeps its
   real, narrower job: the Execute-SQL button, every .btn-primary (Save/OK/
   Send Report/Next-style "primary action" buttons), the thumbs-up icons
   (.report-sql-btn--positive/.summary-feedback-btn--up), and the DB
   "connected" status dot (.status-dot.connected) - success/affirmative
   actions and states, not the general UI accent any more. --danger keeps
   its own pre-existing job (cancel/delete buttons, thumbs-down icons, the
   disconnected status dot) and ALSO gained one more: #stopBtn.btn-stop
   (the in-flight "Cancel" button) moved from --accent-cyan to --danger, so
   cancelling a running query now reads as the same red as every other
   cancel/delete action instead of the old "mild, in-progress" blue-green
   tone (see that rule's own comment for why it was accent-cyan before).
   The light theme's --accent-cyan/--accent-cyan-rgb/--on-accent-cyan keep
   aliasing --primary exactly as before - this pass was scoped to dark mode
   only, per the request. The illustrated header/favicon mascot artwork
   itself (hardcoded hex fills, not driven by these tokens at all) was
   deliberately left untouched - it's a multi-color character illustration
   (already using white for its own eyes/highlights), not a single-hue
   accent, and repainting it solid white would erase its shape rather than
   just "de-green" it.
   ========================================================================= */
:root {
  --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-mono: 'Fira Code', 'Courier New', Courier, monospace;

  /* Color Palette */
  /* NEUTRAL GRAPHITE PASS (per explicit request - the previous --bg-dark/
     --bg-card/--bg-input all carried a cool navy cast, e.g. --bg-dark's old
     #0b0f14 has R11/G15/B20 - blue consistently the highest channel by a
     wide margin - which read as "bluish" rather than a true dark gray).
     Each value below keeps its original lightness (so nothing that was
     tuned against it for contrast needs retuning) but flattens the hue to
     R=G=B - true graphite instead of navy. Scoped to dark mode only, same
     as the light theme's own near-white surfaces below, which were never
     part of this complaint. */
  --bg-dark: #0f0f0f;
  --bg-card: rgba(27, 27, 27, 0.65);
  --bg-card-border: rgba(255, 255, 255, 0.08);
  --bg-input: rgba(15, 15, 15, 0.85);

  /* Toned down from the original #10b981 ("Glowing CockroachDB green") in
     two passes per explicit request, each keeping the same hue and
     lightness (so every spot already tuned for contrast, e.g. text/badge
     uses, keeps working) while cutting saturation further - first roughly
     in half, then by another ~40% on top of that - for a calmer, muted
     sage-green feel instead of the original neon emerald. */
  --primary: #487f6d;
  --primary-hover: #6b9f8c;
  --primary-glow: rgba(72, 127, 109, 0.15);
  /* RGB triplets (no alpha) for the rgba(var(--x-rgb), N) glow/shadow
     accents scattered through the file - lets those keep their own
     per-instance alpha while still tracking the theme's actual hue. */
  --primary-rgb: 72, 127, 109;
  --primary-hover-rgb: 107, 159, 140;

  --secondary: #6366f1; /* Modern Indigo - the inline "add a chart" link,
     a couple of secondary status badges (see its call sites below). Not
     used in the header - a model-badge tint was tried and reverted (see
     the PALETTE "SIGNAL" PASS note above) since it clashed against the
     header's now-consistent green. */
  --secondary-hover: #818cf8;
  --secondary-rgb: 99, 102, 241;

  /* Plain <a> tags (help.html's GitHub/Replay-guided-tour links, the
     custom-connection dialog's "see Documentation" note) never had their
     own color rule anywhere in this file - they just rendered in the
     browser's own default link blue (~#0000EE unvisited, ~#551A8B
     visited), which is legible against the light theme's near-white
     background but reads as barely-there against this theme's near-black
     one (per explicit report - "very little contrast... looks fine in the
     light theme"). Scoped to dark mode only for that reason: the light
     theme's own block below just re-declares the classic browser-default
     values verbatim, so nothing changes there. Distinct from --secondary
     above (the "add a chart" link/sort-arrow color) on purpose - changing
     --secondary's own value would have rippled into those unrelated
     call sites; this is its own token specifically for "this text IS a
     hyperlink" - also reused by .summary-chart-inline-link's own color
     (the Summary tab's inline "view chart" trigger, styled as a plain
     underlined link even though it's a <button> - see that rule's own
     comment) so every hyperlink-styled element in the app, real <a> or
     not, shares one legible dark-mode blue rather than two different
     ones. */
  --link-color: #8ab4f8;
  --link-color-hover: #aecbfa;
  --link-color-visited: #c58af9;

  --danger: #f87171;
  --danger-hover: #ef4444;
  --danger-glow: rgba(248, 113, 113, 0.15);
  --danger-rgb: 248, 113, 113;
  --danger-hover-rgb: 239, 68, 68;
  /* Text color that sits ON TOP of a solid --danger-colored badge/button
     (see .report-issue-inline-btn--error) - same reasoning as
     --on-primary/--on-warning/--on-accent-cyan above, computed per-theme
     since this dark theme's --danger is a bright, light-ish red (needs a
     dark label) while the light theme's is a deep one (needs a light
     label). */
  --on-danger: #080b11;

  --warning: #f59e0b;
  --warning-rgb: 245, 158, 11;
  /* Text color that would sit ON TOP of a solid --warning-colored badge -
     same reasoning as --on-primary/--on-accent-cyan above, computed
     per-theme since --warning (unlike --primary) isn't reliably
     bright/dark enough in both themes to share one constant value.
     Currently unused (#stopBtn.btn-stop went straight from --accent-cyan/
     --on-accent-cyan to --danger/--on-danger, never through --warning -
     see the DARK-MODE "WHITE ACCENT" PASS note up top) but kept defined
     alongside --warning itself for whenever a real warning-colored
     badge is needed. */
  --on-warning: #080b11;

  --text-primary: #f8fafc;
  --text-secondary: #94a3b8;
  --text-muted: #64748b;
  /* Deliberately much lower contrast against --bg-dark than even
     --text-muted above (~2:1 vs ~4:1) - the app-wide "de-emphasized but
     still readable" tone isn't quiet enough for .app-footer's copyright
     line, which should recede into the page rather than just read as
     secondary text. */
  --text-footer: #3a4756;

  /* Modal/dropdown/table "surface" scale - this is the scale
     .result-tab-btn/table header/table row backgrounds actually ride on
     (see NEUTRAL GRAPHITE PASS above - same flatten-the-hue-keep-the-
     lightness treatment, since these three are exactly "the results
     headers and table rows" the bluish-tint complaint called out). Each
     one keeps its original spacing/lightness relative to --bg-dark above,
     just with the navy cast removed. */
  --surface-1: #1b1b1b;
  --surface-2: #262626;
  --surface-3: #373737;
  --surface-stripe: #1f1f1f;

  /* Generic translucent-white "wash" tiers - borders/hovers/chip
     backgrounds that used to be one-off rgba(255,255,255,0.0N) literals.
     Named by their dark-theme alpha for readability; the light theme
     below redefines each as a dark-based wash instead (white-on-white
     would be invisible). */
  --overlay-1: rgba(255, 255, 255, 0.03);
  --overlay-2: rgba(255, 255, 255, 0.05);
  --overlay-3: rgba(255, 255, 255, 0.08);
  --overlay-4: rgba(255, 255, 255, 0.12);
  --overlay-6: rgba(255, 255, 255, 0.06);
  --overlay-18: rgba(255, 255, 255, 0.18);

  /* Results-table-specific text tones (distinct from --text-primary/
     --text-secondary, which are tuned for the app chrome, not a dense
     data grid). */
  --text-table: #e2e8f0;
  --text-table-strong: #f1f5f9;
  --chip-text: #cbd5e1;

  /* This app's general UI accent (the active tab, the progress-status
     banner, the header icon buttons, the mic icon, the wordmark/footer
     link, focus rings, borders/glows, and so on - see the DARK-MODE "WHITE
     ACCENT" PASS note up top) - carries its OWN white-based value here,
     independent of --primary, per an explicit request to move the general
     accent off green everywhere except the specific success/save/execute
     buttons and status dot --primary is still reserved for (see that same
     note). #stopBtn.btn-stop moved OFF this token onto --danger as part of
     the same request - see its own comment. Kept as its own token name
     (rather than renamed) purely so this file doesn't need a mass
     find-and-replace across every var(--accent-cyan) use; the light theme
     below still aliases this straight to --primary/--primary-rgb/
     --on-primary, unchanged - this pass was scoped to dark mode only. */
  --accent-cyan: #ffffff;
  --accent-cyan-rgb: 255, 255, 255;
  --on-accent-cyan: #080b11;
  /* White-accent counterpart to --primary-glow, for the handful of pulse/
     focus glows that used to ride on --primary-glow purely because it was
     "the app's accent color's glow" rather than anything semantically
     green (the help-button attention ring, the quick-prompts restore
     button's hover, the input focus glow) - see their own call sites. */
  --accent-cyan-glow: rgba(255, 255, 255, 0.15);

  /* Text color that sits ON TOP of a solid --primary-colored button/badge -
     constant across themes: --primary is always bright enough that a dark
     label reads well on it, on either background. */
  --on-primary: #080b11;

  --scrollbar-thumb: rgba(255, 255, 255, 0.1);
  --scrollbar-thumb-hover: rgba(255, 255, 255, 0.2);

  /* The NL prompt textarea's own text color (not the same as
     --text-primary since it's set with !important against a hardcoded
     .speech-bubble-wrapper background - see that rule). */
  --prompt-text: #ffffff;

  /* Shared by --modal-scrim and --tour-scrim below - both are a full-page
     dimming scrim, just applied via two different CSS mechanisms
     (background-color behind a blurred modal card vs. a spotlight cutout's
     box-shadow). Kept well under full-black alpha so whatever is still
     visible behind the dimming (modal: the page edges; tour: the rest of
     the app around the spotlighted element) stays legible rather than
     crushing to near-black - see the tour-spotlight rule below for why this
     matters there specifically. */
  --scrim-rgb: 3, 6, 12;
  --modal-scrim: rgba(var(--scrim-rgb), 0.25);
  --tour-scrim: rgba(var(--scrim-rgb), 0.25);

  /* .table-responsive/.stats-card-vertical's own "recessed input-like
     panel" background - same rgb as --bg-input's dark-theme value (see
     NEUTRAL GRAPHITE PASS above), but needs its own alpha so it stays
     valid once --bg-input goes solid (opaque, no alpha channel at all) in
     the light theme below. */
  --recessed-panel-bg: rgba(15, 15, 15, 0.45);

  --transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Light theme - see the block comment above :root. Applied via
   <html data-theme="light">, set by client.js before first paint. */
:root[data-theme="light"] {
  --bg-dark: #f5f7f9;
  --bg-card: rgba(255, 255, 255, 0.75);
  --bg-card-border: rgba(15, 23, 42, 0.10);
  --bg-input: #eef2f7;

  /* Same two-pass toned-down-saturation treatment as the dark theme's
     --primary above, same lightness preserved for the same reason
     (contrast on TEXT uses like .status-success/.stat-card-title - see
     the top-of-file THEME ARCHITECTURE note on why light needed a deeper
     shade to begin with). */
  --primary: #356455;
  --primary-hover: #2b5046;
  --primary-glow: rgba(53, 100, 85, 0.15);
  --primary-rgb: 53, 100, 85;
  --primary-hover-rgb: 43, 80, 70;

  --secondary: #4f46e5; /* see the dark theme's --secondary comment above */
  --secondary-hover: #4338ca;
  --secondary-rgb: 79, 70, 229;

  /* Re-declares the plain browser-default link colors verbatim (not a
     redesign) - see the dark theme's --link-color comment above for why
     this token exists at all; the light theme was never part of the
     complaint it was added for, so this side just keeps things looking
     exactly like the un-styled default anchors did before. */
  --link-color: #0000ee;
  --link-color-hover: #0000ee;
  --link-color-visited: #551a8b;

  --danger: #dc2626;
  --danger-hover: #b91c1c;
  --danger-glow: rgba(220, 38, 38, 0.12);
  --danger-rgb: 220, 38, 38;
  --danger-hover-rgb: 185, 28, 28;
  --on-danger: #ffffff;

  --warning: #b45309;
  --warning-rgb: 180, 83, 9;
  --on-warning: #ffffff;

  --text-primary: #0f172a;
  --text-secondary: #475569;
  --text-muted: #64748b;
  /* Same reasoning as the dark theme's --text-footer above - roughly the
     same ~2:1 contrast against --bg-dark, just the light-background
     mirror of it. */
  --text-footer: #aab2b9;

  --surface-1: #ffffff;
  --surface-2: #f1f5f9;
  --surface-3: #e2e8f0;
  --surface-stripe: #f8fafc;

  --overlay-1: rgba(15, 23, 42, 0.03);
  --overlay-2: rgba(15, 23, 42, 0.05);
  --overlay-3: rgba(15, 23, 42, 0.08);
  --overlay-4: rgba(15, 23, 42, 0.12);
  --overlay-6: rgba(15, 23, 42, 0.05);
  --overlay-18: rgba(15, 23, 42, 0.16);

  --text-table: #1e293b;
  --text-table-strong: #0f172a;
  --chip-text: #334155;

  /* Unchanged by the dark-mode "white accent" pass (see the dark theme's
     --accent-cyan comment above) - the light theme keeps aliasing straight
     to --primary/--primary-rgb/--on-primary, exactly as before. */
  --accent-cyan: var(--primary);
  --accent-cyan-rgb: var(--primary-rgb);
  --on-accent-cyan: var(--on-primary);
  --accent-cyan-glow: var(--primary-glow);

  --on-primary: #ffffff;

  --scrollbar-thumb: rgba(15, 23, 42, 0.15);
  --scrollbar-thumb-hover: rgba(15, 23, 42, 0.28);

  --prompt-text: #0f172a;

  --scrim-rgb: 15, 23, 42;
  --modal-scrim: rgba(var(--scrim-rgb), 0.16);
  --tour-scrim: rgba(var(--scrim-rgb), 0.16);

  --recessed-panel-bg: rgba(15, 23, 42, 0.05);
}

/* Reset & Global Styles */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html, body {
  height: 100%;
  height: 100dvh; /* Dynamic viewport height lock */
  overflow: hidden; /* Lock viewport on all screen sizes */
}

body {
  background-color: var(--bg-dark);
  color: var(--text-primary);
  font-family: var(--font-sans);
  display: flex;
  flex-direction: column;
  position: relative;
}

/* Background Gradients/Decorations */
.background-decor {
  position: absolute;
  top: -10%;
  right: -5%;
  width: 500px;
  height: 500px;
  background: radial-gradient(circle, rgba(var(--accent-cyan-rgb), 0.06) 0%, rgba(0,0,0,0) 70%);
  z-index: -1;
  pointer-events: none;
}

.background-decor-2 {
  position: absolute;
  bottom: -10%;
  left: -5%;
  width: 600px;
  height: 600px;
  background: radial-gradient(circle, rgba(var(--secondary-rgb), 0.04) 0%, rgba(0,0,0,0) 70%);
  z-index: -1;
  pointer-events: none;
}

/* App Container */
.app-container {
  max-width: 1400px;
  width: 100%;
  /* flex: 1 (not a fixed height: 100dvh) so .app-container shrinks to fill
     whatever room is left under #liveDemoStrip when that's visible, rather
     than overflowing the locked html/body 100dvh and getting clipped by
     body's overflow: hidden - see .live-demo-strip's own comment below for
     the other half of this. */
  flex: 1;
  min-height: 0;
  margin: 0 auto;
  padding: 0.5rem 1.5rem 0.25rem 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  overflow: hidden;
}

/* DB Connection & Status Badge */
.status-badge,
.db-connection-info {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  border-radius: 9999px;
  background: var(--overlay-1);
  border: 1px solid var(--bg-card-border);
  padding: 0.2rem 0.75rem;
  font-size: 0.8125rem;
  color: var(--text-secondary);
  line-height: 1.2;
  min-width: 0;
  max-width: min(600px, 60vw);
  cursor: pointer; /* Ensures a hand pointer appears on hover */
}

.status-badge .conn-badge-text {
  font-size: 0.8125rem;
  color: var(--text-secondary);
  font-weight: 500;
  padding: 0 0.05rem;
  white-space: nowrap;
  flex-shrink: 0;
}

.status-badge.connected {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Chevron-down affordance on #modelTriggerBadge (see index.html) - marks
   the badge as a dropdown selector rather than plain status text, matching
   the visual language of #resultsDownloadCaretBtn's own chevron elsewhere.
   Muted like the badge text itself rather than a full-strength icon color,
   since it's a secondary affordance cue, not the badge's primary content. */
.model-badge-caret {
  color: var(--text-muted);
  flex-shrink: 0;
  margin-left: -0.05rem;
  transition: transform 0.15s ease;
}

/* Points up instead of down while the list is open - tracks the same
   aria-expanded attribute client.js's modelTriggerBadge click handler
   already sets/clears (see MODEL PICK LIST section), so this needs no
   JS of its own beyond that existing toggle. */
#modelTriggerBadge[aria-expanded="true"] .model-badge-caret {
  transform: rotate(180deg);
}

/* Layout Vertical */
.vertical-layout {
  display: flex;
  flex-direction: column;
  gap: 0.175rem;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}

select {
  background-color: var(--bg-input);
  color: var(--text-primary);
  border: 1px solid var(--bg-card-border);
  border-radius: 6px;
  padding: 0.4rem 2rem 0.4rem 0.8rem;
  font-family: var(--font-sans);
  font-size: 0.85rem;
  cursor: pointer;
  outline: none;
  transition: var(--transition);
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.6rem center;
  background-size: 0.9rem;
}

/* The dropdown chevron above is a data-URI SVG with its stroke color baked
   in at build time (--text-secondary can't be referenced inside a
   data-URI) - this re-bakes it with the light theme's --text-secondary so
   the arrow stays visible against a light dropdown instead of rendering
   as a near-invisible dark-on-dark-ish sliver. */
:root[data-theme="light"] select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
}

select:focus {
  border-color: var(--accent-cyan);
  box-shadow: 0 0 0 3px var(--accent-cyan-glow);
}

/* Base <a> color - see --link-color's own comment (near --secondary
   above) for why this exists as its own token instead of just leaving
   plain anchors at the browser default. A more specific rule elsewhere
   (e.g. .custom-db-field-label a's `color: inherit`) still wins on
   specificity, so this only actually reaches the handful of plain,
   unstyled <a> tags that had no color rule at all before. */
a {
  color: var(--link-color);
}

a:hover {
  color: var(--link-color-hover);
}

a:visited {
  color: var(--link-color-visited);
}

/* .card - USED to be a "glassmorphism" panel (translucent blurred
   background, 1px border, drop shadow, its own inset padding) wrapping
   the NL/SQL editor row (.editor-card) and the whole results area
   (.table-card) each in their own visibly framed box. Removed entirely
   per explicit request - the visible border/background/shadow read as an
   unwanted extra "box" nested inside .app-container's own page margin,
   and .editor-card/.table-card's own horizontal padding was eating into
   space their actual contents (the NL/SQL text boxes, the results table)
   could otherwise use. Now just a layout helper (still a flex column,
   still zero own margin) with no visual chrome of its own and no
   horizontal inset, so those contents grow to the same width
   .app-container itself allows rather than a narrower one this padding
   used to carve out. */
.card {
  display: flex;
  flex-direction: column;
  margin-bottom: 0;
}

.editor-card {
  margin-top: 0;
  flex-shrink: 0;
}

/* Dynamic Height Results Section */
/* margin-top used to add 0.575rem here to compensate for .vertical-layout's
   own tight 0.175rem gap (see that rule's own comment), back when
   .editor-card sat directly next to .table-card with nothing in between.
   That's stale now that #resultsPanesResizer (a real, visible divider) sits
   between them - the resizer's own presence is the deliberate visual
   separation, and this margin was left in by mistake on top of it, making
   the gap BELOW the resizer (~12px) much wider than the gap ABOVE it
   (~2.8px, just .vertical-layout's shared gap). Removed so both sides of
   the resizer sit at that same tight gap, matching how snugly the NL/SQL
   panes sit against #editorPanesResizer above. */
.table-card {
  flex: 1;
  min-height: 0;
  max-height: none;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* Textarea / Editor Base */
textarea {
  width: 100%;
  background-color: var(--bg-input);
  border: 1px solid var(--bg-card-border);
  border-radius: 8px;
  padding: 0.75rem 1rem;
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-size: 0.9rem;
  line-height: 1.4;
  resize: vertical;
  transition: var(--transition);
}

textarea:focus {
  outline: none;
  border-color: var(--accent-cyan);
  box-shadow: 0 0 0 3px var(--accent-cyan-glow);
}

/* Buttons */
.btn {
  padding: 0.4rem 1.25rem;
  border-radius: 8px;
  font-family: var(--font-sans);
  font-size: 0.70rem;
  font-weight: 600;
  cursor: pointer;
  transition: var(--transition);
  border: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
}

.btn-primary {
  background-color: var(--primary);
  color: var(--on-primary);
}

.btn-primary:hover:not(:disabled) {
  background-color: var(--primary-hover);
  box-shadow: 0 0 15px rgba(var(--primary-rgb), 0.35);
}

.btn-primary:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.btn-secondary {
  background-color: var(--overlay-2);
  color: var(--text-primary);
  border: 1px solid var(--bg-card-border);
}

.btn-secondary:hover {
  background-color: var(--overlay-3);
}

/* Microphone Button Styles */
.mic-btn {
  position: absolute;
  right: 2px;
  /* Anchored to the top-right corner (matching #reportSqlGoodBtn/
     #runBtn's own .sql-overlay-actions in the SQL box) rather than
     vertically centered - vertical centering only looked right back when
     this box's height was fixed at a couple of lines; now that its height
     can be dragged much taller (see .editor-panes-row's own comment), a
     centered icon would float in the middle of a mostly-empty box instead
     of sitting with the controls a user expects to find up top. */
  top: 6px;
  background: transparent;
  border: none;
  color: var(--accent-cyan);
  cursor: pointer;
  padding: 6px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: var(--transition);
  z-index: 5;
}

.mic-btn:hover {
  color: var(--accent-cyan);
  background: var(--overlay-2);
}

.mic-btn.listening {
  color: var(--danger);
  animation: pulse 1.5s infinite;
  background: rgba(var(--danger-rgb), 0.1);
}

/* client.js's setButtonsDisabled() already sets the `disabled` attribute
   here while a translate/execute call is in flight, same as #runBtn/
   #reportSqlBtn - but same gap #reportSqlBtn had (see .report-sql-btn:
   disabled's own comment): no visual change went with it, so a disabled-
   but-still-full-color icon read as clickable when it wasn't. Same dimmed/
   not-allowed/hover-suppressed treatment applied there, applied here too. */
.mic-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.mic-btn:disabled:hover {
  color: var(--accent-cyan);
  background: transparent;
}

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

.stat-val {
  font-size: 0.65rem;
  font-weight: 700;
}

.status-unknown { color: var(--text-secondary); }
.status-success { color: var(--primary); text-shadow: 0 0 10px rgba(var(--primary-rgb), 0.2); }
.status-error { color: var(--danger); text-shadow: 0 0 10px rgba(var(--danger-rgb), 0.2); }

/* Tables & Results Container */
.table-responsive {
  max-height: 100%;
  overflow-y: auto;
  overflow-x: auto;
  width: 100%;
  border-radius: 8px;
  border: 1px solid var(--bg-card-border);
  background: var(--recessed-panel-bg);
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  text-align: left;
  font-size: 0.9rem;
}

.data-table th, 
.data-table td {
  padding: 0.85rem 1rem;
  border-bottom: 1px solid var(--bg-card-border);
}

.data-table thead th {
  position: sticky;
  top: 0;
  z-index: 10;
  background-color: var(--surface-1);
  font-weight: 600;
  font-size: 0.75rem;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  box-shadow: 0 1px 0 var(--scrollbar-thumb);
}

.data-table tbody tr {
  transition: var(--transition);
}

.data-table tbody tr:hover {
  background-color: var(--overlay-1);
}

.cell-numeric {
  font-family: var(--font-mono);
  text-align: right;
}

.cell-null {
  font-style: italic;
}

.text-null {
  color: var(--text-muted);
  font-size: 0.8rem;
  letter-spacing: 0.05em;
}

.success-message {
  padding: 1.5rem;
  border-radius: 8px;
  background: rgba(var(--primary-rgb), 0.06);
  border: 1px solid rgba(var(--primary-rgb), 0.2);
  color: var(--text-primary);
}

.success-message strong {
  display: block;
  font-size: 1rem;
  color: var(--primary-hover);
  margin-bottom: 0.5rem;
}

.error-cell {
  padding: 0 !important;
}

.error-container {
  display: flex;
  gap: 1rem;
  padding: 1.5rem;
  background: rgba(var(--danger-hover-rgb), 0.06);
  border-top: 4px solid var(--danger);
  border-radius: 0 0 8px 8px;
}

.error-icon {
  font-size: 1.5rem;
}

.error-details strong {
  display: block;
  color: var(--danger);
  margin-bottom: 0.25rem;
}

.error-details p {
  color: var(--text-secondary);
  font-family: var(--font-mono);
  font-size: 0.85rem;
  white-space: pre-wrap;
}

@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.animate-spin {
  animation: spin 1s linear infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .5; }
}

.animate-pulse {
  animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

/* Header Section
   ------------------------------------------------------------------------
   Plain flex row, left to right: logo/label (.crbot-title-group), the
   active dataset badge (.crbot-dataset-center, sitting right beside the
   logo with its own left margin for breathing room - per explicit request
   to move it next to the icon/label instead of centered in the header),
   then everything else (.header-actions, pushed all the way to the right
   via its own margin-left: auto below, since it's no longer a dedicated
   grid column of its own). Used to be a three-column grid (logo | 1fr
   centered dataset badge | actions) - see git history if that layout is
   ever needed again. */
.crbot-header {
  margin-bottom: 0;
  padding-bottom: 0.075rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
}

.crbot-title-group {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 0.15rem;
  min-width: max-content;
  flex-shrink: 0;
}

/* Sits immediately after the logo/label (see .crbot-header's own comment
   above) rather than centered in the header - margin-left is the actual
   "next to the icon/label, leaving some space" gap; min-width: 0 lets it
   still shrink instead of overflowing on a narrow header, since a flex
   item's default min-width is auto (sized to its content, ignoring the
   parent's available space). */
.crbot-dataset-center {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 0.5rem;
  min-width: 0;
  /* 1.5rem bumped up 50% per explicit follow-up request. */
  margin-left: 2.25rem;
}

.dataset-center-label {
  font-size: 0.9375rem; /* .header-badge-prominent's own 0.8125rem * ~1.15, same math */
  font-weight: 600;
  color: var(--text-secondary);
  white-space: nowrap;
  flex-shrink: 0;
}

/* Originally just the active dataset badge's own size bump, made
   deliberately more prominent than every other header control (per
   explicit request) instead of blending in as just another same-sized
   pill among many - since extended to #modelTriggerBadge too (see
   .model-picker-label's own comment below), per a later explicit request
   that ALL header text/buttons/icons match this size, which is why the
   class name itself dropped "dataset-" - it's the shared, header-wide
   prominent size now, not something dataset-specific reused twice. Larger
   text than the shared .status-badge default (0.8125rem), plus a matching
   caret (below) and, for the dataset badge specifically, its own
   separated info button (.dataset-info-btn above) so that pair together
   still reads as the header's centerpiece. First tried at a full 30% bump
   (1.05625rem); dialed back to a more modest ~15% (0.9375rem) per
   follow-up feedback that the larger size read as too big. Everything
   else about the pill (background, border, padding shape, connected/
   badge-disabled behavior) still comes from the shared .status-badge/
   .connected/.badge-disabled rules elsewhere in this file - this only
   overrides size. */
.header-badge-prominent,
.header-badge-prominent .conn-badge-text {
  font-size: 0.9375rem;
}

.header-badge-prominent {
  gap: 0.5rem;
  padding: 0.3rem 1rem;
}

/* Slightly larger than the shared .status-badge default (8px) so the
   connected/disconnected dot stays visually proportionate to the
   larger-than-usual name next to it instead of looking undersized - not
   scaled by the exact same factor as the text, just nudged up. Only
   #configTriggerBadge actually has a .status-dot child (#modelTriggerBadge
   has no connected/disconnected concept of its own) so in practice this
   only ever applies to the dataset badge - left on the shared class anyway
   so any future badge with its own dot picks this up for free. */
.header-badge-prominent .status-dot {
  width: 10px;
  height: 10px;
}

/* Chevron affordance - same glyph/rotate-on-open language as
   #modelTriggerBadge's own .model-badge-caret (see that rule's own
   comment for the full reasoning): marks the badge as a dropdown-style
   selector rather than plain status text. Flips to point up while
   #datasetPickList is open, tracking the same aria-expanded attribute
   client.js's configTriggerBadge click handler/closeDatasetPickList() now
   toggle, the same pattern already used for the model pick list. */
.dataset-badge-caret {
  color: var(--text-muted);
  flex-shrink: 0;
  margin-left: -0.05rem;
  transition: transform 0.15s ease;
}

#configTriggerBadge[aria-expanded="true"] .dataset-badge-caret {
  transform: rotate(180deg);
}

.crbot-title,
.header-title {
  font-size: clamp(0.9rem, 2.5vw, 1.4rem);
  font-weight: 700;
  color: #f8fafc;
  letter-spacing: -0.02em;
  margin: 0;
  margin-top: -0.15rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: linear-gradient(135deg, #ffffff 0%, #a5f3fc 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Sits as .header-actions' own direct child, right before
   #modelPickerWrapper, so it picks up that row's existing 0.5rem gap for
   free instead of needing its own spacing rule. Font-size now matches
   .dataset-center-label's own 0.9375rem, per a later explicit request that
   ALL header text/buttons/icons match the dataset label/badge's more
   prominent scale rather than the plain .status-badge/.conn-badge-text
   default (0.8125rem) - #modelTriggerBadge itself picked up the same bump
   via .header-badge-prominent (see that class's own comment), so this
   label and the badge text it's labeling grow together and stay in the
   same proportion to each other as before. */
.model-picker-label {
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--text-secondary);
  white-space: nowrap;
  flex-shrink: 0;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-shrink: 1;
  min-width: 0;
  /* Used to be its own dedicated grid column (auto-width, on the right) in
     .crbot-header's old three-column grid - now that the header is a plain
     flex row (see that rule's own comment), this is what still pushes it
     all the way to the right edge, past the dataset badge and whatever
     space is left after it. */
  margin-left: auto;
}

.gear-btn, .help-btn, .history-btn, .feedback-btn, .share-btn, .more-menu-btn {
  background: var(--overlay-2);
  border: 1px solid var(--bg-card-border);
  /* Was halved from 0.35rem alongside the circle's own width/height below
     and the icon size further down (all three shrunk together, per an
     earlier explicit request, so the circle still just barely wrapped the
     smaller icon). Left at 0.175rem even now that the circle has grown
     back up to 29px (matching .dataset-info-btn, per a later explicit
     request that these buttons match that button's more prominent scale):
     the math still centers the larger 18px icon inside a 29px circle with
     room to spare, so there was no need to grow the padding along with it. */
  padding: 0.175rem;
  border-radius: 50%;
  cursor: pointer;
  transition: var(--transition);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 29px;
  height: 29px;
  flex-shrink: 0;
}

/* Doc/History/Preferences/Feedback icons pick up the app's brand blue
   (matches the logo and progress-reporting UI - see --accent-cyan) instead
   of the plain text color, so they read as intentional brand accents rather
   than generic UI chrome. The triple-dot "more" menu button (folds these
   into one menu on narrow screens) intentionally keeps the neutral
   --text-primary color - it wasn't part of this ask and isn't a
   brand-level action the way the others are. */
.more-menu-btn {
  color: var(--text-primary);
}

.gear-btn, .help-btn, .history-btn, .feedback-btn, .share-btn {
  color: var(--accent-cyan);
}

/* Was halved from clamp(20px, 2.2vw, 29px) per an earlier explicit
   request, alongside the surrounding circular button (.gear-btn etc.
   above), which was shrunk to match at the same time. Grown back up to a
   fixed 18px now, matching .dataset-info-btn's own icon size, per a later
   explicit request that these buttons match that button's more prominent
   scale - the surrounding circle (above) grew back to 29px the same way,
   so the icon still sits centered with the same proportion of breathing
   room around it as before. */
.gear-btn svg, .help-btn svg, .history-btn svg, .feedback-btn svg, .share-btn svg, .more-menu-btn svg {
  width: 18px;
  height: 18px;
}

.gear-btn:hover {
  background: var(--overlay-4);
  color: var(--accent-cyan);
  transform: rotate(30deg);
}

.help-btn:hover,
.history-btn:hover,
.feedback-btn:hover,
.share-btn:hover,
.more-menu-btn:hover {
  background: var(--overlay-4);
  color: var(--accent-cyan);
}

/* Wraps #shareBtn + #sharePopover together so the popover can be anchored
   (position: absolute) directly under the button regardless of where it
   sits in the header row - same "wrapper is the positioning context"
   pattern .model-picker-wrapper already uses for #modelPickList. */
.share-btn-wrapper {
  position: relative;
  display: inline-flex;
  flex-shrink: 0;
}

.share-popover {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  background-color: var(--surface-1);
  border: 1px solid var(--bg-card-border);
  border-radius: 8px;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
  width: max(280px, 24vw);
  max-width: min(360px, 90vw);
  z-index: 100;
  padding: 0.6rem;
}

.share-popover.hidden {
  display: none !important;
}

.share-popover-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.share-popover-input {
  flex: 1 1 auto;
  min-width: 0;
  background: var(--overlay-2);
  border: 1px solid var(--bg-card-border);
  border-radius: 6px;
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-size: 0.8rem;
  padding: 0.35rem 0.5rem;
}

.share-popover-copy-btn {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  padding: 0.3rem 0.65rem;
  font-size: 0.8rem;
  font-weight: 500;
  line-height: 1.4;
  border-radius: 6px;
  border: 1px solid rgba(var(--accent-cyan-rgb), 0.4);
  background-color: var(--accent-cyan);
  color: var(--on-accent-cyan);
  cursor: pointer;
}

.share-popover-copy-btn:hover {
  filter: brightness(1.1);
}

.share-popover-status {
  margin-top: 0.35rem;
  font-size: 0.75rem;
  color: var(--text-secondary);
  min-height: 1em;
}

/* "Nothing to share yet" / "failed to create a link" state (see client.js's
   showShareError()) - the Share button is always visible now, so clicking
   it with nothing shareable on screen needs somewhere to say so instead of
   the button just refusing to do anything. Hides the (otherwise empty,
   non-functional) link input + Copy button entirely rather than leaving
   them sitting there uselessly next to the message. */
.share-popover.share-popover-error .share-popover-row {
  display: none;
}

.share-popover-status.share-popover-status-error {
  color: var(--danger);
}

/* Triple-dot "more" menu - collapses Model/Help/History/Preferences/
   Feedback/Sign-in into one menu once the header gets narrow (see the
   @media (max-width: 900px) block further down, right before the
   "Natural Language & SQL Row Layouts" section - same breakpoint as the
   NL/SQL panes switching to stacked, per explicit request) so the
   connection status badge stops getting squashed. Hidden by default;
   only shown under that breakpoint. Reuses the .auth-dropdown-* panel
   styling (see the Google Auth section further down) for visual
   consistency with the existing avatar dropdown - except the Model item,
   which gets its own current-selection display and expandable second-
   level list (.more-menu-model-* below) rather than just forwarding a
   click to a hidden real button, since there's no header badge left to
   open once this fires. */
.more-menu-wrapper {
  position: relative;
  display: none;
  align-items: center;
  flex-shrink: 0;
}

.more-menu-dropdown {
  min-width: 160px;
}

/* .auth-dropdown-menu (this dropdown's own base class - see the Google Auth
   section further down) carries a stray `overflow: hidden` left over from
   an older, otherwise-superseded duplicate of that ruleset - harmless for
   the standalone avatar dropdown, but fatal here: renderAuthUI()'s signed-in
   markup (.auth-menu-wrapper > #authDropdown, also a .auth-dropdown-menu)
   gets reparented straight into #moreMenuAuthSlot below, so its own popup
   dropdown is nested INSIDE this one. With this box clipping its own
   overflow, that nested dropdown - including the "Log out" button - was
   being clipped down to nothing, i.e. present in the DOM and technically
   clickable in theory but invisible and unreachable in practice. An ID
   selector beats a plain class regardless of source order, so this reliably
   wins over that stray declaration without touching the standalone avatar
   dropdown's own (purely cosmetic) corner-clipping elsewhere. */
#moreMenuDropdown {
  overflow: visible;
}

.more-menu-auth-slot:not(:empty) {
  padding: 0.4rem 1rem 0.5rem;
  display: flex;
  align-items: center;
}

.more-menu-auth-slot .auth-container {
  width: 100%;
}

/* The reparented #g_id_signin div is only ever truly empty when Google
   auth isn't configured for this deployment (local dev) or hasn't
   finished loading yet - hide the slot and its divider in that case so the
   menu doesn't show a blank padded row. Reactive to renderAuthUI() filling
   or clearing the container's innerHTML, no JS wiring needed. */
.more-menu-auth-slot:has(> .auth-container:empty) {
  display: none;
}

.auth-dropdown-divider:has(+ .more-menu-auth-slot > .auth-container:empty) {
  display: none;
}

/* First-run onboarding cue: subtle glowing ring on the Help button so a
   brand-new user notices there's something to click, without being as
   disruptive as a tooltip or overlay. Removed permanently (see
   ONBOARDING_SEEN_KEY in client.js) the first time Help is opened. */
.help-btn-attention {
  color: var(--accent-cyan);
  border-color: var(--accent-cyan);
  animation: help-btn-pulse 2s ease-in-out infinite;
}

@keyframes help-btn-pulse {
  0%, 100% { box-shadow: 0 0 0 0 var(--accent-cyan-glow); }
  50% { box-shadow: 0 0 0 6px var(--accent-cyan-glow); }
}

/* "The active dataset just changed" cue on #configTriggerBadge
   (client.js's flashDatasetBadge()) - fired when clicking a MEMBER
   dataset's suggested question inside a dataset group's Schema Viewer
   switches the active connection over to that member (see
   renderGroupMemberOverviewDetail()/switchActiveConnectionToPreset() in
   client.js). Deliberately a FINITE pulse (3 cycles, then stops) rather
   than .help-btn-attention's infinite one above - that one marks a
   standing "there's something to discover here" state that only goes away
   once the user acts; this one is a one-off "this just happened" cue, so
   it has to actually end. Reuses the same --accent-cyan-glow ring look
   (and the same 2s-per-cycle timing) for a consistent "something here just
   got your attention" visual language rather than inventing a second one.
   flashDatasetBadge() removes the class via this animation's own
   'animationend' event, not a hand-tuned setTimeout, so the JS and the
   3-cycle duration here can never drift out of sync with each other. */
.dataset-badge-flash {
  animation: dataset-badge-flash-pulse 2s ease-in-out 3;
}

@keyframes dataset-badge-flash-pulse {
  0%, 100% { box-shadow: 0 0 0 0 var(--accent-cyan-glow); }
  50% { box-shadow: 0 0 0 6px var(--accent-cyan-glow); }
}

/* Generic "grayed out, not clickable right now" look, shared by two
   unrelated situations client.js triggers independently:
   - .icon-disabled on the history button: an anonymous-user restricted
     state (no verified login on this Cloud Run request) - clicking is
     meant to show an explanatory modal instead of the normal one, though
     that particular behavior isn't wired up yet.
   - .badge-disabled on the DB connection / model badges: toggled by
     setButtonsDisabled() while a query is in flight, so the active
     connection/model can't be swapped out from under it - clicking is a
     plain no-op here (see modelTriggerBadge's/configTriggerBadge's own
     'click' listeners). */
.history-btn.icon-disabled {
  opacity: 0.45;
  filter: grayscale(1);
  cursor: not-allowed;
}

.status-badge.badge-disabled {
  cursor: not-allowed;
}

/* opacity/grayscale applied to each child individually here, NOT to
   .status-badge.badge-disabled itself - #connDbDot (configTriggerBadge's
   real connected/disconnected color indicator) is deliberately excluded
   via :not() so it keeps showing its actual status untouched while the
   rest of the badge (name text, edit icon) visibly dims to show it's not
   clickable right now. This can't be done by putting opacity/filter on
   the badge itself and trying to "undo" it on .status-dot: both
   properties composite the ENTIRE rendered subtree as one group once set
   on an ancestor, with no way for a descendant to opt back out - dimming
   each child separately, skipping the dot, is the only way to leave it
   at full color/opacity. modelTriggerBadge has no .status-dot child at
   all, so this dims both of its children exactly as before. */
.status-badge.badge-disabled > *:not(.status-dot) {
  opacity: 0.45;
  filter: grayscale(1);
}

.history-btn.icon-disabled:hover {
  background: var(--overlay-2);
  color: var(--text-primary);
}

.status-badge.badge-disabled:hover {
  background: var(--overlay-1);
}

/* ===========================================================================
   MODEL PICK LIST - an in-place dropdown anchored under #modelTriggerBadge
   (see client.js's MODEL PICK LIST section), replacing the old #modelModal
   popup dialog per explicit request: picking a model should feel like
   choosing from a list right there, not opening a separate dialog.
   Visually modeled on .auth-dropdown-menu below (same position/surface/
   border/shadow recipe, so it reads as the same family of header dropdown)
   but with its own row styling, since each row here is a directly-clickable
   option (click = select AND save immediately) rather than a menu action.
   =========================================================================== */
.model-picker-wrapper {
  position: relative;
  display: inline-flex;
  flex-shrink: 0;
}

.model-pick-list {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  background-color: var(--surface-1);
  border: 1px solid var(--bg-card-border);
  border-radius: 8px;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
  min-width: 220px;
  max-width: min(320px, 90vw);
  max-height: min(360px, 70vh);
  overflow-y: auto;
  z-index: 100;
  padding: 0.5rem 0;
}

.model-pick-list.hidden {
  display: none !important;
}

.model-pick-group-heading {
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  padding: 0.5rem 1rem 0.25rem;
}

.model-pick-group-heading:first-child {
  padding-top: 0.25rem;
}

.model-pick-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  width: 100%;
  background: transparent;
  border: none;
  color: var(--text-primary);
  padding: 0.5rem 1rem;
  font-size: 0.85rem;
  font-family: var(--font-sans);
  text-align: left;
  cursor: pointer;
  transition: var(--transition);
}

.model-pick-option:hover {
  background-color: var(--overlay-2);
}

.model-pick-option.selected {
  color: var(--accent-cyan);
  font-weight: 600;
}

/* Reserves the checkmark's width on every row (not just the selected one)
   so labels all stay left-aligned at the same column instead of the
   selected row's label shifting left to make room for a mark that only it
   has. */
.model-pick-option .model-pick-check {
  visibility: hidden;
  flex-shrink: 0;
}

.model-pick-option.selected .model-pick-check {
  visibility: visible;
}

.model-pick-error {
  display: none;
  color: var(--danger);
  font-size: 0.8rem;
  line-height: 1.4;
  padding: 0.4rem 1rem 0.25rem;
}

/* Model item inside the triple-dot menu (see index.html's own comment on
   #moreMenuModelBtn/#moreMenuModelSubmenu, and this file's own comment
   above .more-menu-wrapper) - the one row here that isn't a plain forward-
   to-the-real-button action. .auth-dropdown-item already gives it the
   same row padding/font/hover as every other item; this just adds the
   space-between layout (label left, caret right) that plain item doesn't
   need. */
.more-menu-model-btn {
  justify-content: space-between;
}

/* Real <button disabled> here (unlike .status-badge.badge-disabled's own
   plain <div> + class-based fake-disabled a few rules up - this element
   has a native disabled state to reach for instead) - toggled by the same
   setButtonsDisabled() while a query is in flight, so a narrow-header user
   can't swap the active model out from under it via this second surface
   any more than a wide-header user could via modelTriggerBadge itself.
   Same grayed-out visual language as .status-badge.badge-disabled for
   consistency, even though the underlying mechanism differs. */
.more-menu-model-btn:disabled {
  opacity: 0.45;
  filter: grayscale(1);
  cursor: not-allowed;
}

.more-menu-model-btn:disabled:hover {
  background: transparent;
}

/* Persistent (not transient) lock for a non-superuser session - see
   IS_SUPERUSER/updateModelBadge() in client.js. Kept as its own class
   rather than reusing .badge-disabled/:disabled above because those are
   toggled on and off by setButtonsDisabled() for the transient in-flight-
   query case; this one must stay applied the whole session for anyone who
   isn't a superuser, regardless of query state, so the two reasons for
   "looking disabled" can't clobber each other. Same grayed-out/not-allowed
   visual language as the transient case for consistency. */
.status-badge.model-restricted > *:not(.status-dot) {
  opacity: 0.45;
  filter: grayscale(1);
}

.status-badge.model-restricted {
  cursor: not-allowed;
}

.status-badge.model-restricted:hover {
  background: var(--overlay-1);
}

.more-menu-model-btn.model-restricted {
  opacity: 0.45;
  filter: grayscale(1);
  cursor: not-allowed;
}

.more-menu-model-btn.model-restricted:hover {
  background: transparent;
}

/* Long model names (rare, but not impossible depending on what a
   deployment's admin configures) get truncated with an ellipsis rather
   than wrapping the row onto a second line or pushing the caret out past
   the menu's own edge - same spirit as .status-badge's own max-width
   ellipsis truncation elsewhere in this file. */
.more-menu-model-current {
  display: inline-block;
  max-width: 160px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
  color: var(--accent-cyan);
  font-weight: 600;
}

/* Same rotate-on-open language as .model-badge-caret/.dataset-badge-caret
   elsewhere in this file - marks this row as an expandable list rather
   than a plain menu action, tracking the same aria-expanded attribute
   client.js's moreMenuModelBtn click handler toggles. */
.more-menu-model-caret {
  color: var(--text-muted);
  flex-shrink: 0;
  transition: transform 0.15s ease;
}

#moreMenuModelBtn[aria-expanded="true"] .more-menu-model-caret {
  transform: rotate(180deg);
}

/* The second-level list itself - expands IN PLACE below #moreMenuModelBtn
   (an accordion, not a flyout to the side) rather than reusing
   .model-pick-list's own position:absolute presentation, since a flyout
   anchored to a row this close to the dropdown's own right edge would run
   off-screen on the narrow viewports this menu exists for in the first
   place. #moreMenuDropdown already has overflow:visible (see that rule's
   own comment, added for the same reason - nested dropdown content that
   needs to actually be seen), so this is free to grow the whole menu
   taller with no clipping. Shares .model-pick-group-heading/
   .model-pick-option/.model-pick-check/.model-pick-error with the header's
   own #modelPickList (see renderModelPickList() in client.js, which fills
   both) for one visual language across the two presentations of the same
   list - none of those carry positioning of their own, only the
   standalone .model-pick-list container does, so they drop in here
   unchanged. */
.more-menu-model-submenu {
  display: flex;
  flex-direction: column;
  background: var(--overlay-1);
}

.more-menu-model-submenu.hidden {
  display: none !important;
}

/* Slightly indented from the toggle button above, so the list reads as
   nested under it rather than as more items at the same level. */
.more-menu-model-submenu .model-pick-option {
  padding-left: 1.75rem;
}

.crbot-icon-wrapper {
  display: flex;
  align-items: center;
  /* Pulled in from 0.5rem to as tight as the icon's own drop-shadow halo
     allows (see the filter below) - any closer and the label started
     visually overlapping the glow instead of just sitting near it, per
     explicit "as close as you can" follow-up request. */
  gap: 0.15rem;
  justify-content: center;
  /* MONOCHROME PASS (see index.html's own comment on the mascot svg) -
     `color` resolves every currentColor shape in that svg. First tried
     --text-primary (full white/near-black) per that request, then backed
     off to --text-secondary (the same muted slate --dataset-center-label
     already uses right next to this icon) per an explicit follow-up - the
     full-contrast pair read as too stark/"sharp" next to that label's own
     softer tone. The glow was a sky-blue rgba(56, 189, 248, ...) literal,
     independent of any token - now var(--overlay-18) (white wash in dark
     mode, dark wash in light mode) so it stays a neutral halo around
     whichever color the icon itself resolves to, instead of a fixed blue
     tint that clashed once the icon stopped being green. */
  color: var(--text-secondary);
  filter: drop-shadow(0px 0px 10px var(--overlay-18));
  flex-shrink: 0;
}

.header-icon-label {
  display: flex;
  flex-direction: column;
  justify-content: center;
  line-height: 1.15;
  white-space: nowrap;
}

/* Two stacked lines - the "Datalect" wordmark over its "Meaning in Data"
   tagline (see index.html's full-text-row--brand/--tagline spans),
   replacing the old two-line "Meaning" / "in Data" split (itself a
   replacement for an even older single "Talk to your Data" line) - font
   sizing (see .full-text-row--brand/--tagline below) keeps the pair
   together still comfortably under the vertical space the 60x60 header
   icon (.crbot-icon-wrapper's svg) takes up beside them, same as both
   those previous versions. Color was #10b981 (the literal emerald-500 hex
   then hardcoded on the header svg's own robot head/arms - see
   index.html) so this label read as the icon's own color specifically.
   MONOCHROME PASS (per explicit request, same one covering the svg
   itself): tried var(--text-primary) first - white in dark mode, near-
   black in light mode - rather than var(--accent-cyan), which still
   aliases to the brand green in light mode and would have brought the
   green back the moment someone switched themes; then backed off to
   var(--text-secondary) per an explicit follow-up (that full-contrast
   pair read as too stark/"sharp"), matching .dataset-center-label's own
   muted tone right next to this icon. Still reads as "the icon's own
   color", since the svg resolves its own currentColor shapes from this
   exact same token via .crbot-icon-wrapper's `color` in this file. */
.header-icon-label .full-text {
  display: flex;
  flex-direction: column;
  /* Centers the two stacked rows on each other rather than left-aligning
     them - per explicit follow-up request. A flex column's default
     align-items (stretch) would otherwise stretch both rows to the width
     of the longer one and leave the shorter row's own text sitting flush
     left within that stretched box; `center` instead sizes each row to
     its own text and centers the narrower one under/over the wider one. */
  align-items: center;
  text-align: center;
  /* BRAND-BLUE PASS (per explicit follow-up request, superseding the
     MONOCHROME PASS above): the exact funnel blue sampled from the concept
     artwork itself (transparent_full_logo_master.png's dominant funnel-arm
     color, rgb(0, 135, 243)) rather than a theme token, since this is
     meant to read as "the icon's own color" specifically, not a neutral
     UI-text tone - and unlike --text-secondary, this stays the same fixed
     blue in both light and dark mode. Kept on the shared .full-text
     wrapper rather than repeated on each row below - still the wordmark
     row's own color (it has no color rule of its own, so it inherits
     this), though the tagline row now overrides it with its own mustard
     color instead (see .full-text-row--tagline below). */
  color: #0087f3;
  line-height: 1.15;
}

/* "Datalect" - the bigger, bolder wordmark row (see index.html). Sized
   relative to .full-text-row--tagline below so the two rows read as
   roughly the same on-screen width despite "Datalect" (8 characters)
   being much shorter than "Meaning in Data" (15) - a short word at the
   same weight/size as a long tagline would look visually lost above it,
   per explicit request that the two lines "take the same space". */
.full-text-row--brand {
  font-size: 1.2rem;
  font-weight: 700;
}

/* "Meaning in Data" - the smaller, lighter tagline row (see index.html) -
   one line now (used to be split "Meaning" / "in Data" across two rows),
   sized down from the wordmark above by roughly the same ratio as the
   two strings' own character-count difference, so the shorter/bigger
   word on top and the longer/smaller phrase below end up about the same
   width rather than one dwarfing the other.
   Color overrides the brand-blue it would otherwise inherit from
   .full-text - #f79806, sampled directly from logo-concept.png's own
   four-pointed star beneath the funnel (per explicit request to match
   it), rather than a guessed/generic "mustard" value. The wordmark row
   above is unaffected and stays brand-blue. */
.full-text-row--tagline {
  font-size: 0.65rem;
  font-weight: 500;
  color: #f79806;
}

.header-icon-label .short-text {
  display: none;
  /* Same BRAND-BLUE PASS as .full-text above - kept in sync with it so the
     mobile/narrow-viewport label doesn't go back to a different color on
     its own. */
  color: #0087f3;
  font-size: 0.85rem;
  font-weight: 700;
}

@media (max-width: 768px) {
  .header-icon-label .full-text {
    display: none;
  }
  .header-icon-label .short-text {
    display: inline;
  }
  /* Same "secondary text goes first" reclaiming of space as the logo's
     own full-text/short-text swap right above - the "Dataset:" label is
     the badge's own explanatory prefix, not the badge itself, so it's the
     first thing dropped once the header gets tight. The badge (name, dot,
     caret) and the separate info button both stay, still at their
     enlarged size, all the way down to the next breakpoint. */
  .dataset-center-label {
    display: none;
  }
}

@media (max-width: 480px) {
  .crbot-header {
    gap: 0.35rem;
  }
  /* Tightened from the wider-screen 1.5rem (.crbot-dataset-center's own
     default) - there's a lot less spare width to spend on breathing room
     once the header's this narrow. */
  .crbot-dataset-center {
    margin-left: 0.5rem;
  }
  .status-badge {
    max-width: 168px;
  }
  /* At this width .header-actions has already collapsed most of its own
     controls into the triple-dot menu (see below), but the dataset center
     column still has to share the row with the model badge and that menu
     button - reverting the dataset badge/info-button's enlargement here
     (rather than at 768px, where there's still enough room to keep it
     prominent) is what keeps that three-way split from overflowing on a
     phone-width screen. Only the dataset badge needs this rule: the model
     badge is already gone from this row at this width (folded into the
     more-menu by the 900px breakpoint above), so there's nothing left of
     .header-badge-prominent here to revert on its behalf. .status-badge's
     own 168px max-width above still applies on top of this, truncating a
     long dataset name with an ellipsis rather than ever actually
     overflowing. */
  .header-badge-prominent,
  .header-badge-prominent .conn-badge-text {
    font-size: 0.8125rem;
  }
  .header-badge-prominent {
    padding: 0.2rem 0.75rem;
    gap: 0.35rem;
  }
  .header-badge-prominent .status-dot {
    width: 8px;
    height: 8px;
  }
  .dataset-info-btn {
    width: 22px;
    height: 22px;
  }
  .dataset-info-btn svg {
    width: 14px;
    height: 14px;
  }
}

/* Collapse Model/Help/History/Preferences/Feedback/Sign-in into the
   triple-dot menu. Deliberately the SAME breakpoint as the "Natural
   Language & SQL Row Layouts" section below switches the NL/SQL panes
   from side-by-side to stacked (max-width: 900px / min-width: 901px) -
   per explicit request, so the header only gives up these six controls
   at the exact width where the app is already reflowing into its narrow,
   single-column layout, rather than at some unrelated breakpoint of its
   own. This used to fire at max-width: 480px (see git history if that's
   ever needed) - moved out to its own query here, at 900px, when that
   changed; the model picker was folded in at the same time, per a
   follow-up request to keep every header re-arrangement on one shared
   breakpoint rather than giving it its own.
   The ">" combinator matters here: it only hides #g_id_signin while it's
   still a direct child of .header-actions (its original header position).
   client.js's relocateAuthContainer() moves that same live node into
   #moreMenuAuthSlot (inside the dropdown below) whenever this breakpoint
   is active, at which point it's no longer a direct child here and stays
   visible in its new spot - so sign-in/sign-out keeps working at this
   width instead of being hidden along with the rest. client.js's own
   NARROW_HEADER_MEDIA_QUERY constant must stay in sync with this number.
   The model picker doesn't need an equivalent reparenting trick - unlike
   the cross-origin sign-in iframe, #modelPickerWrapper has no real
   interactive twin to move around: the more-menu's own #moreMenuModelBtn/
   #moreMenuModelSubmenu (see index.html and the MODEL PICK LIST section
   in client.js) are a second, independent presentation of the exact same
   underlying selection, not a reparented copy of this one. */
@media (max-width: 900px) {
  .header-actions > .model-picker-label,
  .header-actions > .model-picker-wrapper,
  .header-actions > .help-btn,
  .header-actions > .history-btn,
  .header-actions > .gear-btn,
  .header-actions > .feedback-btn,
  .header-actions > .share-btn-wrapper,
  .header-actions > .auth-container {
    display: none !important;
  }
  .more-menu-wrapper {
    display: inline-flex;
  }
}

/* Natural Language & SQL Row Layouts
   ------------------------------------------------------------------------
   .editor-panes-row wraps the NL prompt pane (#editorPaneNl) and the SQL
   pane (#editorPaneSql), with #editorPanesResizer (a draggable divider,
   see initEditorPanesResizer() in client.js) between them. This divider's
   ORIENTATION follows the row's own layout direction, matching whichever
   arrangement is active - it isn't just hidden below the breakpoint:

   > 900px (see the media query below, matching this app's existing
   narrow-layout breakpoint elsewhere): the two panes sit SIDE BY SIDE,
   equal width by default (50/50, both flex: 1 1 0). The divider is
   VERTICAL (col-resize, dragged left/right) and resizes the WIDTH split
   between them. Both panes' actual boxes (.speech-bubble-wrapper /
   .speech-bubble-wrapper.sql-bubble) are also kept the SAME height as each
   other by JS (setEditorAreaHeightPx() in client.js) per explicit request -
   dragging #resultsPanesResizer below changes that shared height.

   <= 900px: the two panes STACK (flex-direction: column), full width each.
   The SAME divider becomes HORIZONTAL (row-resize, dragged up/down) and
   resizes the HEIGHT split between the stacked NL and SQL boxes instead -
   see setNlPaneHeightPx()/applyNlPaneHeightPx() in client.js. Neither box
   has its own native resize handle any more (see
   .speech-bubble-wrapper.sql-bubble below) - #editorPanesResizer here and
   #resultsPanesResizer further down (which still separately controls the
   SQL box's own height, growing/shrinking the whole stacked block) are the
   only two ways to resize anything in the editor area, in both layouts. */
.editor-panes-row {
  display: flex;
  align-items: stretch;
  width: 100%;
  /* The resizer's own thickness plus each pane's padding supplies the
     visual gap in both layouts (see below) - same pattern as
     .schema-viewer-panes/.schema-viewer-panes-resizer. */
  gap: 0;
}

/* Deliberately NOT display:flex - .speech-bubble-wrapper (the pane's one
   child) already carries its own `flex: 1` from back when it sat directly
   inside the old .input-row's flex ROW (see that rule's own comment) - if
   this pane were ALSO a flex container (column direction, so the main axis
   is now height), that same `flex: 1` would hijack the wrapper's HEIGHT
   (flex-basis: 0% grows to fill the pane) and silently override whatever
   explicit height client.js sets on it, which is exactly the bug this
   comment is here to head off. Plain block layout sidesteps that entirely:
   the wrapper just fills the pane's width (normal block sizing) and its
   height comes purely from its own height/min-height/max-height (CSS
   default, or client.js's inline override) with no flex interference. */
.editor-pane {
  min-width: 0;
  min-height: 0;
}

/* The draggable divider itself, between .editor-pane-nl and
   .editor-pane-sql (see initEditorPanesResizer() in client.js) - same
   9px/10px hit-area, 1px-hairline, thicken-on-hover treatment as
   .schema-viewer-panes-resizer/.results-panes-resizer, for a consistent
   drag-handle language across the app. Base rule below is the <= 900px
   (stacked/horizontal) presentation; the media query further down
   overrides it for the > 900px (side-by-side/vertical) presentation. */
.editor-panes-resizer {
  display: block;
  flex: 0 0 auto;
  width: 100%;
  height: 10px;
  min-width: 0;
  position: relative;
  cursor: row-resize;
  /* Without this, a touchstart on the resizer is free to be interpreted as
     a page scroll/pan gesture before the pointerdown handler in client.js
     ever gets a pointermove to work with - the drag never starts on a
     touch device (verified live: this resizer was completely unusable on
     a phone until this was added). Mouse/pen drags don't need it, but it's
     harmless for them. */
  touch-action: none;
}

.editor-panes-resizer::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 1px;
  background: var(--bg-card-border);
  transform: translateY(-50%);
  transition: background-color 0.15s ease, height 0.15s ease;
}

.editor-panes-resizer:hover::after,
.editor-panes-resizer:focus-visible::after,
.editor-panes-resizer.editor-panes-resizer--dragging::after {
  height: 3px;
  background: var(--accent-cyan);
}

.editor-panes-resizer:focus-visible {
  outline: none;
}

@media (max-width: 900px) {
  .editor-panes-row {
    flex-direction: column;
  }

  .editor-pane-nl {
    padding-bottom: 0.5rem;
  }

  .editor-pane-sql {
    padding-top: 0.5rem;
  }
}

@media (min-width: 901px) {
  .editor-pane-nl,
  .editor-pane-sql {
    flex: 1 1 0;
    min-width: 0;
  }

  .editor-pane-nl {
    padding-right: 0.5rem;
  }

  .editor-pane-sql {
    padding-left: 0.5rem;
  }

  /* Rotated to a vertical divider for the side-by-side layout - same
     hairline/hover treatment, just on the perpendicular axis (width
     instead of height, col-resize instead of row-resize). */
  .editor-panes-resizer {
    width: 9px;
    height: auto;
    min-height: 0;
    cursor: col-resize;
  }

  .editor-panes-resizer::after {
    left: 50%;
    right: auto;
    top: 0;
    bottom: 0;
    width: 1px;
    height: auto;
    transform: translateX(-50%);
  }

  .editor-panes-resizer:hover::after,
  .editor-panes-resizer:focus-visible::after,
  .editor-panes-resizer.editor-panes-resizer--dragging::after {
    width: 3px;
    height: auto;
    background: var(--accent-cyan);
  }

  /* "Show SQL" unchecked (Preferences > SQL Execution - see
     applySqlPaneVisibility() in client.js): #editorPaneSql/
     #editorPanesResizer are hidden outright (.hidden, display:none) and
     this grows the NL pane to fill the freed width. !important
     deliberately outweighs any inline flex-basis initEditorPanesResizer()'s
     drag handlers may have left on .editor-pane-nl (e.g. a dragged-to-40%
     split) - "SQL is gone" should always read as full width regardless of
     whatever split was last dragged, and removing this class (Show SQL
     re-checked) simply lets that inline style resume untouched, no JS
     bookkeeping needed. Scoped to this >900px block on purpose - in the
     stacked (<=900px) layout .editor-panes-row's main axis is height, not
     width, so forcing flex-basis: 100% there would fight the NL box's own
     independently-managed height instead of doing anything useful; hiding
     the SQL pane in that layout already leaves the NL pane as the sole
     block filling its column, with no extra CSS needed. */
  .editor-panes-row.sql-hidden .editor-pane-nl {
    flex: 1 1 100% !important;
  }
}

/* The draggable divider between the editor area (both NL/SQL panes above)
   and the results area below (see initResultsPanesResizer() in client.js).
   Present at every screen width - unlike #editorPanesResizer, this one
   isn't about the split BETWEEN the two boxes, it's what REPLACED the SQL
   box's own native resize:vertical handle (see .speech-bubble-wrapper.
   sql-bubble below): dragging it grows/shrinks the SQL box (and, in the
   side-by-side layout, the NL box right along with it, to keep them equal)
   at the expense of the results area, or vice versa. Horizontal
   counterpart of .schema-viewer-panes-resizer - same visual language,
   rotated 90deg (row-resize cursor, a horizontal hairline that thickens on
   hover/focus/drag). */
.results-panes-resizer {
  flex: 0 0 auto;
  width: 100%;
  height: 10px;
  min-width: 0;
  position: relative;
  cursor: row-resize;
  /* See .editor-panes-resizer's own comment above - same touch-drag fix,
     same reason. */
  touch-action: none;
}

.results-panes-resizer::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 1px;
  background: var(--bg-card-border);
  transform: translateY(-50%);
  transition: background-color 0.15s ease, height 0.15s ease;
}

.results-panes-resizer:hover::after,
.results-panes-resizer:focus-visible::after,
.results-panes-resizer.results-panes-resizer--dragging::after {
  height: 3px;
  background: var(--accent-cyan);
}

.results-panes-resizer:focus-visible {
  outline: none;
}

/* Speech/Prompt Containers */
.speech-bubble-wrapper {
  position: relative;
  flex: 1;
  background: var(--surface-1);
  /* Border weight reduced from 3px per explicit request - was reading as a
     heavy outline around the NL/SQL boxes; 1px keeps the accent frame
     visible without dominating the input. */
  border: 1px solid rgba(var(--accent-cyan-rgb), 0.35);
  border-radius: 0;
  transform: none;
  /* FLAT PASS (per explicit request - the old fixed rgba(0, 0, 0, 0.4) drop
     shadow plus inset glow read as "very pronounced" in light mode, where a
     black shadow shows up clearly against a light page instead of blending
     into a dark one) - removed entirely, matching .results-container's own
     "no visible box" flatness (see its comment) rather than just re-tuning
     the shadow's opacity per theme. The 1px border above still frames the
     box; there's just no shadow/glow riding along with it any more. */
  padding: 4px;
  box-sizing: border-box;
}

/* height/min-height/max-height here are just the CSS-level DEFAULT/
   fallback (100px, matching this box's long-standing default size) -
   client.js's setEditorAreaHeightPx() sets an inline `height` directly on
   this element (and on .editor-pane-nl's wrapper too, when side by side -
   see .editor-panes-row's own comment above) whenever the user drags
   #resultsPanesResizer, and an inline style always wins over these class
   rules regardless of source order/specificity.
   resize: none (was resize: vertical) - this box is no longer
   individually resizable by dragging its own corner; that native browser
   resize handle is replaced app-wide by the explicit #resultsPanesResizer
   divider below the editor area, so the SAME drag affects the NL pane too
   in the side-by-side layout instead of only ever affecting this one box.
   min-height dropped from the old 100px to 56px (MIN_EDITOR_AREA_PX in
   client.js - kept in sync with that constant) since the resizer now
   allows shrinking below what the native handle used to permit; max-height
   loosened from a flat 600px to 80vh as a generous CSS-only safety net -
   client.js's own drag clamp (based on how much room .table-card needs to
   keep, not a fixed number) is what actually bounds it day to day. */
.speech-bubble-wrapper.sql-bubble {
  background: var(--surface-1);
  border: 1px solid rgba(var(--accent-cyan-rgb), 0.35);
  border-radius: 0;
  /* FLAT PASS - see .speech-bubble-wrapper's own comment above; same
     removal, kept in sync since this is that same box's SQL variant. */
  height: 100px;
  min-height: 56px;
  max-height: 80vh;
  resize: none;
  overflow: hidden;
  position: relative;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  padding: 6px;
}

/* Textarea Box Styles */
.speech-bubble-wrapper textarea#aiPrompt {
  width: 100%;
  /* Fills the wrapper's full height (was a fixed 34px, ~1 line) rather
     than capping the usable typing area partway down an otherwise-empty
     box - this wrapper's own height is set directly by client.js (see
     .editor-panes-row's own comment in this file), so a definite height
     is always present for this percentage to resolve against. */
  height: 100%;
  background: transparent !important;
  border: none !important;
  box-shadow: none !important;
  color: var(--prompt-text) !important;
  font-family: var(--font-mono) !important;
  font-size: 0.85rem;
  line-height: 1.25;
  /* Top padding bumped from 0.25rem to 0.5rem to match #sqlQuery's own
     top padding just below - was tuned for a single-line box, and read as
     text jammed against the top edge once this box could actually be
     taller than one line. */
  padding: 0.5rem 0.6rem;
  padding-right: 2.2rem !important;
  resize: none;
  outline: none;
  transform: none;
  border-radius: 0;
  box-sizing: border-box;
}

.speech-bubble-wrapper textarea#aiPrompt::placeholder {
  color: var(--text-muted);
  font-weight: 400;
  font-style: italic;
  opacity: 0.8;
}

.speech-bubble-wrapper textarea#aiPrompt:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.speech-bubble-wrapper.sql-bubble textarea#sqlQuery {
  width: 100%;
  height: 100px;
  background: transparent !important;
  border: none !important;
  box-shadow: none !important;
  color: var(--accent-cyan) !important;
  font-family: var(--font-mono) !important;
  font-size: 0.9rem;
  line-height: 1.45;
  padding: 0.5rem 0.8rem;
  resize: none;
  outline: none;
  transform: none;
  border-radius: 0;
  box-sizing: border-box;
}

/* Wraps the Execute button together with the "report accurate SQL"
   thumbs-up (#reportSqlGoodBtn) and "report wrong SQL" thumbs-down
   (#reportSqlBtn) buttons so all three lay out as one unit in the SQL
   box's top-right corner - #runBtn.sql-overlay-btn used to carry its own
   `position: absolute; top/right` for this (see below) back when it was
   the only overlay control; now this wrapper owns the positioning.
   Stacked in a COLUMN (Execute on top, both report buttons in their own
   row underneath - see .sql-overlay-report-btns below) per explicit
   request, rather than all three sitting in a single row beside Execute;
   `align-items: flex-end` keeps both rows pinned to the same right edge
   regardless of which row is wider. */
.sql-overlay-actions {
  position: absolute;
  top: 6px;
  right: 6px;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.3rem;
}

/* The thumbs-up/thumbs-down pair's own row, directly under Execute (see
   .sql-overlay-actions above) - a nested flex row so the two report
   buttons sit side by side within that second row. */
.sql-overlay-report-btns {
  display: flex;
  align-items: center;
  gap: 0.15rem;
}

#runBtn.sql-overlay-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.2rem 0.45rem;
  font-size: 0.65rem;
  font-weight: 500;
  line-height: 1;
  gap: 0.25rem;          /* reduced spacing between icon and text */
  border-radius: 4px;    /* scaled down radius */
  /* z-index: 10; Ensures it renders on top of CodeMirror scroll/gutters */
  /* Deliberately --primary (this app's real green), not --accent-cyan -
     the Execute button stays green even after the dark-mode "white
     accent" pass moved --accent-cyan off green everywhere else (see that
     note up top), per explicit request that Execute keep reading as the
     same "go/success" color as Save and the thumbs-up icons. */
  background: rgba(var(--primary-rgb), 0.85);
  color: #ffffff;
  border: 1px solid rgba(var(--primary-rgb), 0.4);
  border-radius: 6px;
  backdrop-filter: blur(4px);
  cursor: pointer;
  transition: all 0.15s ease-in-out;
}

#runBtn.sql-overlay-btn:hover:not(:disabled) {
  background: rgba(var(--primary-rgb), 1);
  box-shadow: 0 0 10px rgba(var(--primary-rgb), 0.4);
}

#runBtn.sql-overlay-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* "Report wrong SQL" thumbs-down icon - sits beside Execute inside
   .sql-overlay-actions above, opening the same #reportIssueModal used
   everywhere else in the app (in 'wrong_sql' mode - see
   REPORT_CATEGORY_CONFIG.wrong_sql in client.js). Sized to match #micBtn
   (same 16x16 icon, same transparent-background treatment), but colored
   --danger red per explicit request - a thumbs-down icon reading as red
   rather than the app's brand blue - matching the same red now used for
   the error-tab "Report Error" button (see .report-issue-inline-btn--error
   below). Hidden by default, same as #sendFeedbackBtn - shown once
   fetchBackendConfig() confirms ISSUE_REPORTING_ENABLED. */
.report-sql-btn {
  background: transparent;
  border: none;
  color: var(--danger);
  cursor: pointer;
  padding: 6px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: var(--transition);
  flex-shrink: 0;
}

.report-sql-btn:hover {
  color: var(--danger);
  background: rgba(var(--danger-rgb), 0.12);
}

/* client.js's setButtonsDisabled() already sets the `disabled` attribute
   on this button while a translate/execute call is in flight, same as
   #runBtn/#micBtn - but unlike #runBtn.sql-overlay-btn:disabled above,
   there was no visual change to go with it, so a disabled-but-still-full-
   color icon read as clickable when it wasn't. Dimmed + not-allowed cursor
   here, and the hover rule suppressed while disabled so it doesn't still
   light up on mouseover. */
.report-sql-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.report-sql-btn:disabled:hover {
  color: var(--danger);
  background: transparent;
}

/* "Report accurate SQL" thumbs-up icon - the positive counterpart to
   .report-sql-btn above, same sizing/placement/hidden-until-enabled
   treatment, but tinted --primary (this app's own green, matching
   .summary-feedback-btn--up's own up/down green-vs-red convention) instead
   of --danger red. Overrides only color, so every other .report-sql-btn
   rule (padding, disabled state, etc.) still applies unmodified. */
.report-sql-btn--positive {
  color: var(--primary);
}

.report-sql-btn--positive:hover {
  color: var(--primary);
  background: rgba(var(--primary-rgb), 0.12);
}

.report-sql-btn--positive:disabled:hover {
  color: var(--primary);
  background: transparent;
}

.speech-bubble-wrapper.sql-bubble .CodeMirror {
  height: 100% !important; 
  width: 100% !important;
  background: transparent !important;
  border-radius: 0;
}

/* #reportSqlGoodBtn/#reportSqlBtn now stack in their own row UNDER
   Execute (see .sql-overlay-actions/.sql-overlay-report-btns above)
   rather than sitting beside it, so the widest row up here is Execute's
   own - back down near the original single-button 120px, with a small
   buffer, rather than the 160px this needed back when a report button
   sat to Execute's right. */
.sql-bubble .CodeMirror-lines {
  padding-right: 130px;
}

.speech-bubble-wrapper.sql-bubble .CodeMirror.cm-readonly {
  opacity: 0.55;
  cursor: not-allowed;
}

/* Stats Cards */
.stats-card-vertical {
  background: var(--recessed-panel-bg);
  border: 1px solid var(--bg-card-border);
  border-radius: 8px;
  padding: 0.15rem 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  font-size: 0.6rem;
  flex: 1;
  justify-content: center;
}

.stats-card-vertical .stat-item-inline {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
}

.status-working {
  color: var(--warning);
  font-weight: 600;
}

/* Modal Overlay & Card */
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background-color: var(--modal-scrim);
  /* No backdrop-filter blur any more - it was compounding with the scrim
     itself to make the page behind the modal read as "foggy and
     unreadable" rather than just "dimmed." The lighter --modal-scrim alone
     is enough to signal "inactive, a modal has focus" without blurring
     text that's still meant to be legible back there. */
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  transition: opacity 0.2s ease;
  padding: 1rem;
}

.modal-overlay.hidden {
  display: none;
}

.modal-card {
  background-color: var(--surface-1);
  border: 1px solid var(--bg-card-border);
  border-radius: 12px;
  width: 100%;
  max-width: 680px;
  max-height: 85vh;
  display: flex;
  flex-direction: column;
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
  overflow: hidden;
}

.modal-card-large {
  max-width: 1000px;
}

.modal-header {
  padding: 1.25rem;
  border-bottom: 1px solid var(--bg-card-border);
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.modal-header h2 {
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--text-primary);
}

.modal-close-btn {
  background: none;
  border: none;
  color: var(--text-primary);
  opacity: 0.75;
  font-size: 1.5rem;
  cursor: pointer;
}

.modal-close-btn:hover {
  color: var(--text-primary);
  opacity: 1;
}

/* Covers modals whose own JS disables the close button while a request is
   in flight (e.g. schemaViewerModalCloseBtn while /api/schema is loading) -
   modalCloseBtn lives in .modal-header, so no other rule reaches it. */
.modal-close-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* ===========================================================================
   GUIDED TOUR (first-run onboarding)
   =========================================================================== */
.tour-overlay {
  position: fixed;
  inset: 0;
  z-index: 2000;
}

.tour-overlay.hidden {
  display: none;
}

.tour-spotlight {
  position: fixed;
  border-radius: 10px;
  /* Was a hardcoded 0.78 alpha regardless of theme, which crushed the rest
     of the page to near-illegibility in both dark and light themes (0.78
     of an already-dark/near-black color leaves almost nothing behind it
     readable). --tour-scrim gives this its own, lighter, theme-aware
     value instead - see the --tour-scrim definitions near the top of this
     file. The non-zero blur radius (3rd value, before the 9999px spread)
     feathers the edge of the cutout instead of a hard line - a spotlight
     with a razor-sharp boundary between "full brightness" and "dimmed" is
     what actually read as harsh/high-contrast, independent of how light the
     dimmed alpha itself is. */
  box-shadow: 0 0 60px 9999px var(--tour-scrim);
  border: 2px solid var(--accent-cyan);
  pointer-events: none;
  transition: top 0.3s ease, left 0.3s ease, width 0.3s ease, height 0.3s ease;
}

.tour-tooltip {
  position: fixed;
  width: 300px;
  max-width: calc(100vw - 2rem);
  background: var(--surface-1);
  border: 1px solid var(--bg-card-border);
  border-radius: 12px;
  padding: 1.1rem 1.15rem 1rem;
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(var(--accent-cyan-rgb), 0.15);
  transition: top 0.3s ease, left 0.3s ease;
}

.tour-tooltip-step {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent-cyan);
  margin-bottom: 0.4rem;
}

.tour-tooltip h3 {
  font-size: 1rem;
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: 0.4rem;
}

.tour-tooltip p {
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--text-secondary);
}

.tour-tooltip-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 1rem;
}

.tour-tooltip-actions-right {
  display: flex;
  gap: 0.5rem;
}

.tour-nav-btn {
  padding: 0.4rem 0.85rem;
  font-size: 0.82rem;
}

.tour-skip-btn {
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: 0.8rem;
  cursor: pointer;
  padding: 0.4rem 0.2rem;
  transition: var(--transition);
}

.tour-skip-btn:hover {
  color: var(--text-secondary);
}

/* Live Demo Mode's own status banner (#liveDemoStrip) - a separate,
   real-data onboarding flow, never nested inside/sharing styles with
   .tour-overlay above. Deliberately NOT a blocking spotlight overlay like
   the tour: Live Demo Mode drives the real app end to end, including a
   real live translate+execute+summarize call with real, variable network
   latency, so a strip the user can see past (and dismiss with Cancel)
   reads better than a modal sitting on "waiting". See client.js's "LIVE
   DEMO MODE" section.

   A full-width banner, by explicit request for maximum visibility - now
   laid out in normal flow (not position: fixed) so it PUSHES .app-container
   down rather than covering it/.crbot-header. This works because <body> is
   itself a column flexbox (see "html, body" / "body" above) and, once the
   modals and #tourOverlay are excluded (every one of them is its own
   position: fixed overlay, so none of them participate in body's normal
   flow), #liveDemoStrip and .app-container are body's only two flex
   children. `order: -1` below renders the strip first regardless of its
   DOM position (it sits near the end of <body>, after those fixed
   overlays, purely so it stays grouped with the rest of this feature's
   markup) and .app-container's own `flex: 1; min-height: 0;` (see its
   rule) lets it shrink to fill whatever space remains underneath, instead
   of overflowing the locked 100dvh viewport and getting clipped.

   z-index: 2100 is there so the banner keeps showing - sharp, un-blurred,
   above the dimmed/blurred scrim - while a modal (#preferencesModal,
   #schemaViewerModal, etc.) is open over it, rather than being greyed out
   underneath. Flex items respect z-index even at their default
   `position: static` (per the flexbox spec), so this works without also
   giving the strip `position: relative`. .modal-overlay's own z-index is
   1000 and .tour-overlay's is 2000 (both above) - 2100 clears both, since
   a modal or the schema viewer can still be open while the live demo is
   mid-script driving them. */
.live-demo-strip {
  order: -1;
  z-index: 2100;
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  width: 100%;
  background: var(--primary);
  padding: 1rem 1.5rem;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
}

.live-demo-strip.hidden {
  display: none;
}

.live-demo-strip-caption {
  font-size: 1.1rem;
  font-weight: 600;
  line-height: 1.4;
  color: var(--on-primary);
  text-align: center;
  max-width: 70ch;
}

.live-demo-strip-cancel {
  flex: 0 0 auto;
  background: rgba(0, 0, 0, 0.15);
  /* A fixed translucent white, not a var(--on-primary)-derived tint: in
     light mode --on-primary flips to white and --primary stays a
     medium-dark green either way (see :root vs :root[data-theme="light"]
     above), so a white-based border reads consistently against this
     button's --primary background in both themes - no --on-primary-rgb
     token exists in this file to key off instead. */
  border: 1px solid rgba(255, 255, 255, 0.4);
  border-radius: 999px;
  color: var(--on-primary);
  font-size: 0.85rem;
  font-weight: 700;
  cursor: pointer;
  padding: 0.45rem 1.1rem;
  transition: var(--transition);
}

.live-demo-strip-cancel:hover {
  background: rgba(0, 0, 0, 0.3);
}

/* .help-modal-actions (the action row that used to sit above
   #helpModalBody, holding just a "Replay guided tour" button) and
   .restore-quick-prompts-btn (that button's own pill styling, reused from
   an even older, already-removed sample-prompts-restore button) are both
   gone now that #replayTourBtn is a plain inline link inside help.html's
   own text instead - see openHelpModal()/wireHelpModalLinks() in
   client.js and #helpModalBody's own comment in index.html. Neither class
   has any other element using it, so both rules were removed rather than
   left as dead CSS. */

.modal-body {
  padding: 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  overflow-y: auto;
}

/* Preferences dialog only - its 3 sections (Theme/Automatic SQL Execution/
   Bring Your Own Key) read as more distinct, separate settings groups with
   extra breathing room between them than the default .modal-body gap
   gives every other modal that reuses that same class. */
.modal-body-spacious {
  gap: 2rem;
}

.help-dialog-body {
  line-height: 1.6;
  color: var(--text-secondary);
  max-height: 500px;
  flex: 1;
}

.help-dialog-body h3 {
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--accent-cyan);
  margin-top: 0.5rem;
}

.help-dialog-body p {
  font-size: 0.85rem;
}

.help-dialog-body ol,
.help-dialog-body ul {
  font-size: 0.85rem;
  padding-left: 1.25rem;
  margin: 0.4rem 0;
}

.help-dialog-body li {
  margin-bottom: 0.4rem;
}

.help-dialog-body .help-quickstart {
  background: var(--overlay-2);
  border: 1px solid var(--bg-card-border);
  border-radius: 8px;
  padding: 0.9rem 1rem 0.9rem 0.4rem;
  margin-bottom: 1rem;
}

.help-dialog-body .help-quickstart h3 {
  margin-top: 0;
  padding-left: 0.6rem;
}

.config-section {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.config-label {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text-secondary);
}

/* Bring Your Own Key (Preferences dialog's third section) - one row per
   provider, label and its text-input + small "remove" button pair laid
   out side by side rather than stacked, so the three rows read as a
   compact little form/table rather than six separate stacked blocks.
   The label gets a fixed width (byok-row-label below) so all three
   inputs line up in a column regardless of each provider name's length.
   The input itself reuses .config-input's own look untouched;
   .byok-clear-btn reuses .custom-db-remove-btn's "x" icon-button look
   (see that class' comment) even though the two remove very different
   things, purely for visual consistency of "small square x button next
   to a field". */
.byok-row {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.75rem;
}

.byok-row + .byok-row {
  margin-top: 0.5rem;
}

.byok-row-label {
  flex: 0 0 130px;
  font-size: 0.8rem;
  color: var(--text-secondary);
}

.byok-row-fields {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex: 1 1 auto;
  min-width: 0;
}

.byok-row-fields .byok-key-input {
  flex: 1 1 auto;
  min-width: 0;
}

/* The read-only "here's exactly what will be emailed" preview inside
   #reportIssueModal (see client.js's openReportIssueModal()) - a <pre>
   rather than a plain <p> since the content is a plain-text email body
   (SQL/error text included) that needs to keep its own line breaks and
   spacing, same reasoning execute_routes.py's raw error passthrough itself
   relies on being shown verbatim rather than reflowed. Reuses
   .config-input's border/background look so it reads as part of the same
   form, just non-editable and monospace. */
.report-issue-preview {
  width: 100%;
  max-height: 260px;
  overflow: auto;
  margin: 0;
  padding: 0.6rem 0.8rem;
  border-radius: 6px;
  border: 1px solid var(--bg-card-border);
  background-color: var(--bg-input);
  color: var(--text-primary);
  font-family: var(--font-mono, monospace);
  font-size: 0.78rem;
  line-height: 1.4;
  white-space: pre-wrap;
  word-break: break-word;
}

/* The editable counterpart to the <pre> above (#reportIssuePreviewEditable,
   'wrong_sql' category only - see client.js's openReportIssueModal()) -
   shares .report-issue-preview's look but needs its own explicit height/
   resize since a <textarea> doesn't grow to fit its content the way a
   <pre> does. A visible focus ring (unlike the read-only <pre>, which
   never receives one) is the one visual cue that this box - unlike every
   other report preview in the app - is actually meant to be typed into. */
textarea.report-issue-preview {
  height: 160px;
  resize: vertical;
}

textarea.report-issue-preview:focus {
  outline: none;
  border-color: var(--accent-cyan);
}

.config-input, .config-select {
  width: 100%;
  padding: 0.6rem 0.8rem;
  border-radius: 6px;
  border: 1px solid var(--bg-card-border);
  background-color: var(--bg-input);
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: 0.85rem;
  outline: none;
}

.config-input:focus, .config-select:focus {
  border-color: var(--accent-cyan);
}

.modal-actions {
  display: flex;
  justify-content: flex-end;
  margin-top: 0.5rem;
}

.hidden {
  display: none !important;
}

.text-center { text-align: center; }
.text-muted { color: var(--text-muted); }
.py-8 { padding-top: 3rem; padding-bottom: 3rem; }

::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb);
  border-radius: 3px;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--scrollbar-thumb-hover);
}

.app-footer {
  flex-shrink: 0;
  text-align: center;
  padding: 0.35rem 0;
  border-top: 1px solid var(--bg-card-border);
  /* Was var(--accent-cyan) (the app's own bright green) - a copyright line
     shouldn't compete for attention with anything else on the page, so
     this now uses a dedicated, much lower-contrast token instead (see
     --text-footer's own comment) rather than reusing --text-muted, which
     is still meant to read as normal secondary text elsewhere. */
  color: var(--text-footer);
  font-size: 0.825rem;
  margin-top: 0.25rem;
}

.radio-group {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  background-color: var(--bg-input);
  border: 1px solid var(--bg-card-border);
  border-radius: 6px;
  padding: 0.6rem 0.8rem;
}

/* Modifier for a short, mutually-exclusive option list (Preferences'
   Theme radios - Dark/Light - are the only current use) that reads better
   side by side than stacked - unlike .radio-group's other uses (model
   selection, the DB connection list), which can hold many long options
   and need the column flow. Kept as an additive modifier class rather
   than changed on .radio-group itself so those other radio groups are
   untouched. */
.radio-group-row {
  flex-direction: row;
  align-items: center;
  gap: 1.5rem;
}

/* Datasource picker's own empty state for "Private" (see
   renderDatasetPickList() in client.js) - shown instead of the plain
   option list when the user has no private datasources saved yet,
   pointing at where to actually add one (the Preferences dialog's "Bring
   Your Own Data" section, since this dropdown is select-only, same as
   every other option here - creating/editing/deleting a custom connection
   lives there, not here). */
.custom-dbs-empty-hint {
  font-size: 0.82rem;
  margin: 0;
}

/* A button meant to read as inline text, not as a bordered control - used
   where a click needs to run script (so a real <a href> would be
   misleading) but the control itself should look like a plain link inside
   a sentence, e.g. the "add one in Settings" prompt above. */
.link-button {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: var(--link-color);
  text-decoration: underline;
  cursor: pointer;
}

.link-button:hover {
  color: var(--link-color-hover);
}

/* Now used only by Preferences' Theme radios (Dark/Light) - the datasource
   picker (presets/groups/private connections) moved off plain radio inputs
   entirely into its own dropdown (see the DATASET PICK LIST section in
   client.js and .dataset-pick-option below), so this no longer needs to
   handle a long preset/connection name inside a flex column; kept simple
   for its one remaining short-label use. */
.radio-option {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 0.85rem;
  color: var(--text-primary);
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
}

.radio-option input[type="radio"] {
  accent-color: var(--accent-cyan);
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
  cursor: pointer;
}

.radio-option span {
  word-break: normal;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Schema Viewer "i" info button. Used to exist once per preset and once
   per saved custom connection inside the DB connection config dialog
   (letting the Schema Viewer be opened for a connection other than
   whichever one was actually active, and always stacked on top of that
   dialog besides) - both of those are gone now. The one surviving copy
   (#datasetSchemaViewerBtn, see index.html) now sits BESIDE the dataset
   badge, inside .crbot-dataset-center, as its own separately-sized
   control (per explicit request to pull it out of the pill and enlarge it
   to match the badge's own bigger font) rather than nested inside the
   badge at a small, badge-matched size. Being a sibling of the badge now
   (rather than nested inside it) means its clicks no longer bubble
   through the badge's own click-to-configure handler at all - its click
   handler in client.js still calls stopPropagation() as a harmless
   belt-and-suspenders leftover from the nested days, but it isn't
   load-bearing any more. It only ever opens the Schema Viewer for the
   currently ACTIVE connection, so there's no other connection it could
   mean and no config dialog underneath it to get stuck behind. Sized up
   from the old nested version's 18px (badge-
   matched) - see .header-badge-prominent's own comment for how its
   enlargement (and this icon's matching one) were sized. */
.dataset-info-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 29px;
  height: 29px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--surface-3);
  border-radius: 50%;
  color: var(--text-secondary);
  cursor: pointer;
  transition: var(--transition);
}

.dataset-info-btn:hover {
  background: var(--overlay-2);
  color: var(--accent-cyan);
  border-color: var(--accent-cyan);
}

/* Same "grayed out, not clickable right now" treatment as
   .status-badge.badge-disabled (see that rule's own comment) - reused
   here via the same class (toggled by the same setButtonsDisabled() call)
   since #datasetSchemaViewerBtn isn't a .status-badge itself and so isn't
   covered by that selector. */
.dataset-info-btn.badge-disabled {
  cursor: not-allowed;
  opacity: 0.45;
  filter: grayscale(1);
}

.dataset-info-btn.badge-disabled:hover {
  background: transparent;
  color: var(--text-secondary);
  border-color: var(--surface-3);
}

/* ===========================================================================
   DATASET PICK LIST - an in-place dropdown anchored under
   #configTriggerBadge (see client.js's DATASET PICK LIST section),
   replacing the old #configModal popup dialog per explicit request, same
   pattern as - and visually modeled on - the MODEL PICK LIST above (see
   that section's own comment): picking a datasource should feel like
   choosing from a list right there, not opening a separate dialog. Options
   are grouped under two headings, "Public" (every CONFIGURED_DBS preset
   and CONFIGURED_DB_GROUPS dataset group, merged into one flat list, groups
   trailing the presets they're built from) and "Private" (this user's own
   saved custom connections, or an empty-state hint pointing at Settings if
   they have none yet) - reusing .model-pick-group-heading/.model-pick-
   option/.model-pick-check/.model-pick-error wholesale rather than
   inventing parallel classes, since the row shape (a directly-clickable
   option, click = select AND save immediately) is identical either way.
   =========================================================================== */
.dataset-picker-wrapper {
  position: relative;
  display: inline-flex;
  flex-shrink: 0;
  min-width: 0;
}

/* Anchored to the LEFT edge of the wrapper (unlike .model-pick-list's own
   right: 0) - #modelPickerWrapper sits inside the right-aligned
   .header-actions, where right: 0 keeps the flyout from running off the
   header's own right edge, but #datasetPickerWrapper sits inside the
   centered .crbot-dataset-center instead, immediately followed by its own
   sibling #datasetSchemaViewerBtn (not part of this wrapper) - left: 0
   keeps the flyout aligned under the badge itself regardless of where
   that sibling button ends up. */
.dataset-pick-list {
  position: absolute;
  left: 0;
  top: calc(100% + 8px);
  background-color: var(--surface-1);
  border: 1px solid var(--bg-card-border);
  border-radius: 8px;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
  min-width: 240px;
  max-width: min(360px, 90vw);
  max-height: min(400px, 70vh);
  overflow-y: auto;
  z-index: 100;
  padding: 0.5rem 0;
}

.dataset-pick-list.hidden {
  display: none !important;
}

.dataset-pick-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  width: 100%;
  background: transparent;
  border: none;
  color: var(--text-primary);
  padding: 0.5rem 1rem;
  font-size: 0.85rem;
  font-family: var(--font-sans);
  text-align: left;
  cursor: pointer;
  transition: var(--transition);
}

.dataset-pick-option:hover {
  background-color: var(--overlay-2);
}

.dataset-pick-option.selected {
  color: var(--accent-cyan);
  font-weight: 600;
}

/* Reserves the checkmark's width on every row (not just the selected one),
   same reasoning as .model-pick-option's own rule right above. */
.dataset-pick-option .model-pick-check {
  visibility: hidden;
  flex-shrink: 0;
}

.dataset-pick-option.selected .model-pick-check {
  visibility: visible;
}

/* A dataset GROUP's own option, rendered inside the "Composites" flyout
   (#datasetPickCompositeSubmenu, see client.js's renderGroupOption()) rather
   than inline in the Public section any more - no longer bolded (per
   earlier explicit request - it read as visually heavier than every
   ordinary preset beside it, not just "distinct"); no on-hover tooltip of
   its own either (dropped per a later explicit request - the flyout is
   already what marks these apart from an ordinary preset, so the extra
   explanation was redundant), and no "(Composite)" suffix on the name
   needing extra wrap room the way it used to - this still allows wrapping
   since a group's plain name can still run long on its own in a flyout
   this narrow. */
.dataset-pick-option.all-databases-option {
  white-space: normal;
}

/* The "Composites" row itself, inline among the Public section's regular
   .dataset-pick-option buttons in #datasetPickOptions - shares their exact
   row padding/font/hover language via this combined selector (rather than
   literally being a .dataset-pick-option, which would make it match the
   delegated click listener's own select-and-save handling - see client.js,
   which checks for this class FIRST specifically to avoid that). Never
   gets a .selected/.model-pick-check of its own since it doesn't represent
   a choice by itself - see .dataset-pick-submenu-caret below for how its
   open/closed state actually shows. Opens its flyout on hover alone (per
   explicit request - see client.js's openDatasetPickCompositeSubmenu()),
   not just on click, so :hover below doubles as this row's real affordance,
   not just a cosmetic nicety the way it is on an ordinary
   .dataset-pick-option (clicking one of those is still required there). */
.dataset-pick-submenu-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  width: 100%;
  background: transparent;
  border: none;
  color: var(--text-primary);
  padding: 0.5rem 1rem;
  font-size: 0.85rem;
  font-family: var(--font-sans);
  text-align: left;
  cursor: pointer;
  transition: var(--transition);
}

.dataset-pick-submenu-toggle:hover {
  background-color: var(--overlay-2);
}

/* Static right-pointing arrow (the universal "opens a submenu" glyph,
   unlike #datasetPickList's own down-pointing .dataset-badge-caret which
   flips with aria-expanded) - the SAME chevron glyph as every other
   open/close caret in this file, just rotated -90deg rather than drawn as
   a separate icon, so it stays visually consistent with
   .dataset-badge-caret/.model-badge-caret/.more-menu-model-caret. Doesn't
   itself rotate further on open/close the way those do - a nested menu's
   own "has a submenu" arrow conventionally stays put; the flyout's own
   visibility is indication enough that it's open. */
.dataset-pick-submenu-caret {
  color: var(--text-muted);
  flex-shrink: 0;
  transform: rotate(-90deg);
}

/* The "Composites" flyout itself (#datasetPickCompositeSubmenu in
   index.html) - a sibling of .dataset-pick-list, not nested inside it (see
   that element's own comment in index.html for why), positioned with
   inline top/left client.js's openDatasetPickCompositeSubmenu() computes
   fresh on every open rather than a fixed offset here, since the
   "Composites" toggle row's own vertical position moves depending on how
   many presets render above it. Same visual language as .dataset-pick-list
   itself (same surface/border/shadow/radius/scroll treatment) so the two
   panels read as one system despite being separate elements. */
.dataset-pick-submenu {
  position: absolute;
  background-color: var(--surface-1);
  border: 1px solid var(--bg-card-border);
  border-radius: 8px;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
  min-width: 200px;
  max-width: min(320px, 90vw);
  max-height: min(360px, 70vh);
  overflow-y: auto;
  z-index: 100;
  padding: 0.5rem 0;
}

.dataset-pick-submenu.hidden {
  display: none !important;
}

/* Disabled/busy while a selection is being saved (see client.js's
   selectDataset()) - a fresh custom connection's first activation can take
   several seconds (real schema introspection server-side, not just a
   preference flip the way picking a model is), so every other option is
   blocked from being clicked out from under that in-flight request rather
   than just visually implying it. The badge's own connDbDot already shows
   a "checking" pulse for the same duration (see checkDbStatus()'s own
   convention) - this is the dropdown-side half of that same feedback.
   Covers both .dataset-pick-list and its own .dataset-pick-submenu flyout
   - client.js's selectDataset() applies this class to whichever of the two
   a selection was actually made from, but either can be the one visibly
   open and interactive while the other request is in flight. */
.dataset-pick-list.dataset-pick-list-busy,
.dataset-pick-submenu.dataset-pick-list-busy {
  pointer-events: none;
  opacity: 0.6;
}

.checkbox-option {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 0.85rem;
  color: var(--text-primary);
  cursor: pointer;
  user-select: none;
}

.checkbox-option input[type="checkbox"] {
  accent-color: var(--accent-cyan);
  width: 1rem;
  height: 1rem;
  cursor: pointer;
}

/* Locked state - Preferences' "Show SQL" checkbox while automatic
   execution is off (see enforceShowSqlLinkage() in client.js): forced
   checked and disabled, so it needs to visibly read as "on, but not
   yours to change right now" rather than just an ordinary checked box. */
.checkbox-option:has(input:disabled) {
  cursor: not-allowed;
  opacity: 0.6;
}

.checkbox-option input[type="checkbox"]:disabled {
  cursor: not-allowed;
}

/* ===========================================================================
   CUSTOM DB CONNECTION CARDS (Preferences dialog's "Bring Your Own Data"
   section - relocated here from the datasource selector's config modal;
   see renderCustomDbRows() in client.js)
   Each saved custom connection is enclosed between two horizontal lines
   (the list's own top border plus each card's bottom border, so adjacent
   cards share one line rather than doubling up), with its fields laid out
   one logical row per line instead of everything wrapping together.
   =========================================================================== */
.custom-dbs-list {
  display: flex;
  flex-direction: column;
}

.custom-dbs-list:not(:empty) {
  border-top: 1px solid var(--bg-card-border);
}

.custom-db-card {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  /* Tightened from 0.75rem 0 per explicit request: that padding plus the
     border-bottom below put ~1.5rem of empty space between two collapsed
     cards (each card is just its own .custom-db-header-row when
     collapsed, so this padding IS the visible gap) - 3x as loose as the
     dataset picker dropdown above it, whose .dataset-pick-option rows
     sit only 1rem apart (each option's own 0.5rem top/bottom padding,
     back to back). 0.25rem top and bottom mirrors that same 0.5rem-per-
     side rhythm here too, so collapsed Private Datasources rows read
     exactly as tight as the dropdown's own option rows - an expanded
     card (Name/type-specific/
     timeout field rows showing) still gets its own internal 0.55rem
     row-to-row spacing from this rule's `gap`, untouched. */
  padding: 0.25rem 0;
  border-bottom: 1px solid var(--bg-card-border);
}

.custom-db-header-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.custom-db-header-row .custom-db-type-select {
  flex: 0 0 auto;
  width: 8.5rem;
}

.custom-db-header-row .custom-db-name-input {
  flex: 1 1 auto;
  min-width: 0;
}

.custom-db-toggle-btn,
.custom-db-remove-btn {
  /* Doubled from the original 0.15rem 0.6rem padding / 0.7rem font-size -
     these are the only way to expand/collapse or delete a saved custom
     connection, so they need to read as clearly clickable rather than
     blend into the row alongside the type/name fields. */
  flex: 0 0 auto;
  padding: 0.3rem 1.2rem;
  line-height: 1;
  font-size: 1.4rem;
}

.custom-db-field-row {
  display: flex;
  align-items: center;
  gap: 0.5rem 1rem;
  flex-wrap: wrap;
  /* No left indent any more - this used to align each field row under
     the header row's Name input, past the selection radio that used to
     sit before it (radio width + gap). The relocation to the Preferences
     dialog's "Bring Your Own Data" section removed that radio entirely
     (selection now lives only in the read-only list in the datasource
     selector - see renderDbRadioButtons() in client.js), so Name is now
     the header row's own first element and needs no offset to match. */
}

.custom-db-field-row.align-start {
  align-items: flex-start;
}

/* A field-row.align-start row (Service Account Key/Private Key - a
   multi-line textarea) needs its own field(s) top-aligned too, or the
   label-beside-input change above would vertically center the label next
   to a 3-row-tall textarea instead of sitting level with its first line. */
.custom-db-field-row.align-start .custom-db-field {
  align-items: flex-start;
}

.custom-db-field-row.align-start .custom-db-field-label {
  padding-top: 0.3rem;
}

.custom-db-field {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  min-width: 0;
}

/* Every field (inside a field-row or the header row alike) is
   label-beside-input, matching the header row's own Name field - not
   label-above-input the way grouped fields (e.g. Warehouse / Database /
   Schema sharing one line) used to render. Deliberately NOT given a
   min-width (or any other rule that would pad a short label out to match
   a longer one elsewhere) - fields are not meant to visually align into
   columns, each label just sits directly next to its own input. */
.custom-db-field-row .custom-db-field {
  /* Basis/min-width sized so a 3-field row (e.g. Oracle's Service Name /
     SID / Schema, Snowflake's Warehouse / Database / Schema) fits on one
     line at the modal's usual width instead of wrapping its 3rd field
     down early - flex-grow still expands each field to fill the row's
     actual leftover space once wrapping is decided, so 2-field rows (e.g.
     Host / Port) aren't left narrower than before. */
  flex: 1 1 170px;
  min-width: 150px;
  /* Very little space between a label and its input - tighter than the
     0.4rem every other field/input pairing (header row's Name, etc.)
     uses. */
  gap: 0.2rem;
}

.custom-db-header-row .custom-db-field {
  flex: 1 1 auto;
}

.custom-db-field-row .custom-db-field.wide {
  flex: 1 1 100%;
}

.custom-db-field-label {
  font-size: 0.78rem;
  color: var(--text-secondary);
  white-space: nowrap;
  flex: 0 0 auto;
}

.custom-db-field-row .custom-db-field-label {
  white-space: normal;
  /* Caps how much of the field a label can claim on its own line -
     without this, an unwrapped long label could eat the field's entire
     flex-computed width and leave its input squeezed to a sliver. Not an
     alignment device (no min-width here - see the comment above) - just
     an overflow guard. */
  max-width: 8rem;
}

.custom-db-field-label a {
  color: inherit;
  opacity: 0.85;
  text-decoration: underline dotted;
}

.custom-db-field-label .optional-hint {
  display: block;
  font-size: 0.66rem;
  opacity: 0.6;
  font-weight: 400;
}

/* Standalone hint line under the MongoDB Atlas SQL ODBC field (not nested
   inside a .custom-db-field-label, so the rule above doesn't reach it) -
   deliberately smaller/lower-contrast than even that hint style. */
.custom-db-mongo-hint {
  display: block;
  font-size: 0.6rem;
  opacity: 0.45;
  font-weight: 400;
}

.custom-db-field input,
.custom-db-field select,
.custom-db-field textarea {
  flex: 1 1 auto;
  min-width: 0;
  width: 100%;
}

.custom-db-field textarea {
  resize: vertical;
}

.custom-db-add-btn {
  align-self: flex-start;
  margin-top: 0.5rem;
}

/* USED to paint its own background-color: var(--surface-1); border-radius:
   8px - a distinctly-tinted, rounded "lifted panel" look (see --bg-dark's
   own comment on why --surface-1 was kept visually separate from it) that
   read as a second, nested "box" around the whole results area, right
   inside .table-card's own now-removed border/background (see .card's
   comment above) - visible even before any query has run, since it never
   depended on there being a table to show. Removed per the same explicit
   "no box" request: the results area now sits flush against the page,
   with only .table-responsive's own recessed-panel look (once a real
   result renders) providing any framing at all, the same way each NL/SQL
   text box still keeps its own individual border. */
.results-container {
  display: flex;
  flex-direction: column;
  height: 100%;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}

/* Wraps the retry-status banner and the Cancel button as siblings -
   needed because showRetryStatus()/showPhaseStatus()/etc. fully
   overwrite #resultsRetryStatus's own innerHTML on every call, so the
   Cancel button can't just live nested inside that div. The tinted
   "progress banner" background/border is painted on THIS row rather than
   on #resultsRetryStatus alone (see the :has() rule below), so the Cancel
   button sitting beside the status text within this same row visually
   reads as INSIDE the progress-reporting area, not as a separate control
   floating next to it. */
.results-status-row {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* Only paint the banner look while there's actually something to show in
   it - #resultsRetryStatus and #stopBtn each toggle their own .hidden
   class independently (see showRetryStatus()/hideRetryStatus() and
   setButtonsDisabled() in client.js: the Cancel button stays visible for
   an in-flight request's entire duration, including brief moments before
   any status text has arrived yet, which is exactly why it can't just be
   nested inside #resultsRetryStatus's own hidden-toggling). Painting the
   row unconditionally instead would leave a bare colored strip across the
   results area even while nothing is happening. :has() is already used
   elsewhere in this file (see .more-menu-auth-slot above) for the same
   "style a container based on a child's state" need. */
.results-status-row:has(#resultsRetryStatus:not(.hidden)),
.results-status-row:has(#stopBtn:not(.hidden)) {
  padding: 0.4rem 0.75rem;
  background: rgba(var(--accent-cyan-rgb), 0.08);
  border-bottom: 1px solid rgba(var(--accent-cyan-rgb), 0.3);
}

.results-retry-status {
  /* The header/logo's own blue (--accent-cyan) - was --warning's amber
     until the light theme's specific shade (#b45309, a dark rust/brown)
     read as aggressively red rather than as a mild in-progress notice. */
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--accent-cyan);
  font-size: 0.85rem;
}

.results-retry-status .retry-status-icon {
  display: inline-flex;
  font-size: 0.95rem;
  line-height: 1;
}

/* Sized to match #runBtn.sql-overlay-btn (the Execute button overlaying
   the SQL box) exactly - same padding/font-size/line-height/icon-gap/
   border-radius - per design request, even though the two buttons live in
   different parts of the layout and keep their own distinct colors (see
   the color comment below). */
#stopBtn.btn-stop {
  flex-shrink: 0;
  /* Pins this to the row's right edge regardless of whether
     #resultsRetryStatus is present/visible next to it - relying on the
     row's own justify-content to distribute space between the two only
     works while BOTH are visible flex items; with the status text hidden
     (display: none, so it's removed from flex layout entirely) this
     button would otherwise be the row's only item and collapse to the
     left edge instead of staying put on the right. */
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.2rem 0.45rem;
  font-size: 0.65rem;
  font-weight: 500;
  line-height: 1;
  border-radius: 6px;
  /* Matches #runBtn.sql-overlay-btn's own 1px border WIDTH exactly, so the
     two buttons come out the same overall height - without a border here,
     this button rendered 2px shorter than Execute despite every other
     sizing property already matching. Not the same HUE any more, though
     (see the color comment below) - only the width matters here. */
  border: 1px solid rgba(var(--danger-rgb), 0.4);
  /* --danger's red, per explicit request - this used to be --accent-cyan
     (matching .results-retry-status's own progress-banner color above),
     deliberately not --danger, on the reasoning that a bright red felt too
     alarming for "cancel an in-progress query". Overridden by a later,
     more specific request: cancelling a query should read as the same red
     as every other cancel/delete action in the app, not as a mild,
     in-progress notice. */
  background-color: var(--danger);
  color: var(--on-danger);
}

#stopBtn.btn-stop:hover {
  filter: brightness(1.1);
}

/* "A new version of Datalect is available" reload nudge (see client.js's
   fetchClientBuildId()/checkForNewClientVersion()) - a persistent banner
   between the header and the editor card, shown only when the server's
   CLIENT_BUILD_ID (server/app_config.py) has actually changed since page
   load, i.e. a real frontend change landed, not just any server restart.
   --accent-cyan again, matching .results-retry-status above (#stopBtn.
   btn-stop USED to match this same "mild notice, not alarming" tone too,
   before it moved to --danger red - see its own comment) - this banner is
   still only ever a suggestion, never a forced reload, so it keeps the
   milder accent color rather than following that same move to red. */

/* Server-down banner (see client.js's markServerUnreachable()/
   markServerReachable()) - unlike .new-version-banner above, this reports
   an actual ongoing problem rather than a friendly nudge, so it deliberately
   uses --danger (matching .status-error/execution-error styling elsewhere
   in this file) instead of the cyan "mild notice" tone, and has no dismiss
   button: an outage shouldn't be hideable while it's still happening. It
   clears itself automatically once the same check that raised it succeeds. */
.server-down-banner {
  display: flex;
  align-items: center;
  padding: 0.5rem 0.9rem;
  margin: 0 0 0.75rem;
  border-radius: 8px;
  background: rgba(var(--danger-rgb), 0.1);
  border: 1px solid rgba(var(--danger-rgb), 0.35);
  color: var(--danger);
  font-size: 0.85rem;
  font-weight: 500;
}

.server-down-banner-text {
  flex: 1 1 auto;
  min-width: 0;
}

.new-version-banner {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.9rem;
  margin: 0 0 0.75rem;
  border-radius: 8px;
  background: rgba(var(--accent-cyan-rgb), 0.1);
  border: 1px solid rgba(var(--accent-cyan-rgb), 0.35);
  color: var(--accent-cyan);
  font-size: 0.85rem;
}

.new-version-banner-text {
  flex: 1 1 auto;
  min-width: 0;
}

.new-version-banner-btn {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  padding: 0.2rem 0.6rem;
  font-size: 0.8rem;
  font-weight: 500;
  line-height: 1.4;
  border-radius: 6px;
  border: 1px solid rgba(var(--accent-cyan-rgb), 0.4);
  background-color: var(--accent-cyan);
  color: var(--on-accent-cyan);
  cursor: pointer;
}

.new-version-banner-btn:hover {
  filter: brightness(1.1);
}

.new-version-banner-dismiss {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.4rem;
  height: 1.4rem;
  padding: 0;
  border: none;
  background: transparent;
  color: inherit;
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  opacity: 0.75;
}

.new-version-banner-dismiss:hover {
  opacity: 1;
}

/* Shared-link viewing banner (see index.html's #sharedViewBanner and
   client.js's maybeRenderSharedView()) - same mild-notice visual language
   as .new-version-banner above (cyan, not --danger red: opening a shared
   link isn't a problem). Unlike that banner's own hardcoded text, this
   one's text is set dynamically by showSharedViewBanner() (client.js),
   since the right wording depends on whether the share's own dataset could
   actually be picked up for this viewer too. Dismissible, same as
   .new-version-banner - opening a shared link no longer puts the page into
   a special read-only mode this banner would need to keep describing for
   as long as it's up; it's purely informational from the moment the turn
   itself renders, so there's nothing misleading about letting the visitor
   dismiss it early. */
.shared-view-banner {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.9rem;
  margin: 0 0 0.75rem;
  border-radius: 8px;
  background: rgba(var(--accent-cyan-rgb), 0.1);
  border: 1px solid rgba(var(--accent-cyan-rgb), 0.35);
  color: var(--accent-cyan);
  font-size: 0.85rem;
}

.shared-view-banner-text {
  flex: 1 1 auto;
  min-width: 0;
}

/* Same circular "x" treatment as .new-version-banner-dismiss. */
.shared-view-banner-dismiss {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.4rem;
  height: 1.4rem;
  border-radius: 50%;
  border: none;
  background: transparent;
  color: var(--accent-cyan);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  opacity: 0.75;
}

.shared-view-banner-dismiss:hover {
  opacity: 1;
}

/* "Report Error" (see client.js's setReportContext()/reportButtonHtml()) -
   rendered INLINE, directly beside the "Execution Error" title of
   whichever tab is actually showing an error (see .error-title-row below),
   never as a persistent control living outside the tab, so it's never in
   the same row as #stopBtn above (that button is gone by the time a
   result - and therefore this one - ever appears). By explicit request,
   a "Report Wrong Result" button (for a successful/NO-SQL response, as
   opposed to an error) is no longer ever rendered at all - only this one
   category remains, so there's no other variant to distinguish it from.
   --danger/--on-danger (matching the red "Execution Error" title text it
   sits beside, and the same red now used for the SQL box's own "report
   wrong SQL" thumbs-down icon - see .report-sql-btn above) per explicit
   request - this had briefly been --accent-cyan/--on-accent-cyan (the same
   brand blue used elsewhere for reporting/feedback actions), reverted back
   to red here. */
.report-issue-inline-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  flex-shrink: 0;
  padding: 0.2rem 0.5rem;
  font-size: 0.65rem;
  font-weight: 500;
  line-height: 1;
  border: none;
  border-radius: 6px;
  cursor: pointer;
}

.report-issue-inline-btn:hover {
  filter: brightness(1.1);
}

.report-issue-inline-btn--error {
  background-color: var(--danger);
  color: var(--on-danger);
}

/* Thumbs up/down feedback row on the Summary tab (see client.js's
   summaryFeedbackButtonsHtml() - the only place this markup is rendered,
   for both "all databases" and single-connection mode's own Summary tab).
   Sits directly UNDER the summary text (client.js appends it right after
   the .response-text <p>, not before it), and the icons sit snug beside
   the "Was this summary helpful?" label rather than pinned to the row's
   far edge - `flex-start` plus a small `gap` (not `space-between`, which
   would stretch across the full-width cell) is what keeps them right next
   to the text regardless of how wide the tab's own cell is. Unlike
   .report-issue-inline-btn (a colored pill with emoji), these are plain
   icon buttons whose SVGs use stroke="currentColor" specifically so
   --primary (green)/--danger (red) - the two colors explicitly requested
   for this feature - actually tint them; an emoji glyph can't be
   recolored this way. */
.summary-feedback-row {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 0.5rem;
  margin-top: 0.75rem;
}

/* client.js's suggestedSearchesHtml() - "suggested_searches" (see
   server/prompts/summary_single_connection.txt's and summary_all_
   databases.txt's own paragraph on it) rendered as a small row of plain
   text links directly under the Summary tab's own text, ABOVE the "Was
   this summary helpful?" feedback row (see .summary-feedback-row just
   above - same flex/gap approach, its own row rather than merged into
   that one, since this can wrap onto more than one line when there's
   more than one suggested search, unlike the feedback icons). Each link
   opens a plain Google search in a new tab - never a live search run by
   this app itself (see suggestedSearchesHtml()'s own docstring) - styled
   as a small pill (bordered, rounded) rather than plain inline underlined
   text like .summary-chart-inline-link, so it doesn't read as though it
   were part of the model's own prose the way a chart link deliberately
   does - this is an added, clearly-separate suggestion, not a claim the
   summary itself is making. */
.summary-suggested-searches {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.75rem;
}

.summary-suggested-searches-label {
  font-size: 0.75rem;
}

.summary-suggested-search-link {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--bg-card-border);
  border-radius: 999px;
  font-size: 0.75rem;
  color: var(--link-color);
  text-decoration: none;
  white-space: nowrap;
}

.summary-suggested-search-link:hover {
  color: var(--link-color-hover);
  border-color: var(--link-color-hover);
}

/* client.js's renderNoSqlResponse()'s own inviteActionHtml() - the
   clickable "Open the Help guide"/"Open the Schema Viewer" invitation
   appended under a triage "help"/"schema" outcome's short text answer
   (see that function's own docstring: this replaces the old behavior of
   popping the Help modal/Schema Viewer open immediately and unannounced).
   A <button>, not an <a> (there's no URL - clicking it runs
   openInviteAction() in place), so font/background/border are reset here
   before being styled identically to .summary-suggested-search-link just
   above, for the same "small bordered pill, not plain prose-like inline
   text" visual language every other in-page invitation link in this file
   already uses. */
.nosql-invite-row {
  margin-top: 0.75rem;
}

.nosql-invite-link {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--bg-card-border);
  border-radius: 999px;
  font: inherit;
  font-size: 0.75rem;
  background: none;
  color: var(--link-color);
  cursor: pointer;
  white-space: nowrap;
}

.nosql-invite-link:hover {
  color: var(--link-color-hover);
  border-color: var(--link-color-hover);
}

.summary-feedback-label {
  font-size: 0.75rem;
}

.summary-feedback-btns {
  display: inline-flex;
  gap: 0.25rem;
  flex-shrink: 0;
}

.summary-feedback-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.3rem;
  border: none;
  background: transparent;
  border-radius: 6px;
  cursor: pointer;
  line-height: 1;
}

.summary-feedback-btn:hover {
  filter: brightness(1.15);
}

.summary-feedback-btn--up {
  color: var(--primary);
}

.summary-feedback-btn--up:hover {
  background-color: rgba(var(--primary-rgb), 0.12);
}

.summary-feedback-btn--down {
  color: var(--danger);
}

.summary-feedback-btn--down:hover {
  background-color: rgba(var(--danger-rgb), 0.12);
}

/* client.js's applyInlineChartLinks() renders the summarization prompts'
   own model-planted "[<phrase>](chart:<index>)" markup, found IN THE
   MIDDLE of "summary"/"per_database"/"cross_database" wherever the model
   made the point that chart illustrates, as one of these - a click-to-
   jump-to-a-chart trigger styled as plain inline text rather than a
   standalone UI control (deliberately unstyled as a "button" - no border/
   background/pill - just clickable text matching how a real link would
   sit inline). This is the actual text-to-chart integration the
   "visualizations" paragraph's whole design is built around (see
   chart_helpers.py's _clean_visualization docstring). Handled by the
   delegated data-view-chart-trigger click listener
   (jumpToChartableResultTab()) in client.js. This same class is also
   reused, unchanged, by client.js's applyInlineResultLinks() for the
   more general "[<phrase>](result:<index>)" reference link (handled by
   the sibling data-view-result-trigger listener/jumpToResultTab()) - the
   two read as the exact same kind of thing to the user by design, so
   they share one class rather than a near-duplicate second one.
   The Response tab used to ALSO show a row of small preview-chart cards
   underneath the text (one per chartable entry) - removed per explicit
   request (they rendered poorly at that size, and were redundant with
   this same inline link: a result only ever gets a `visualization` at all
   because the model already linked it inline here - see
   _clean_visualization's own docstring). Their CSS (.summary-chart-
   inline-preview*, .summary-chart-preview-grid, .summary-text-chart-row)
   was removed along with them. */
.summary-chart-inline-link {
  display: inline;
  padding: 0;
  border: none;
  background: none;
  /* --link-color, not --secondary - this reads as a hyperlink (underlined,
     clickable inline text) even though the element itself is a <button>,
     so it shares the same dedicated "this IS a hyperlink" token as every
     real <a> in the app instead of the unrelated --secondary/--secondary-
     hover pair (still used elsewhere in this file for the sort-arrow icon
     and this same feature's OWN preview-card hover border - see
     --link-color's own comment near --secondary in the :root palette for
     the full reasoning). */
  color: var(--link-color);
  font-size: inherit;
  font-family: inherit;
  text-decoration: underline;
  text-underline-offset: 0.15em;
  cursor: pointer;
}

.summary-chart-inline-link:hover {
  color: var(--link-color-hover);
}

/* Wraps .report-issue-inline-btn when it has to be inserted as its own
   full-width table row (see client.js's reportButtonRowHtml()) - was for
   the tabular-result "Report Wrong Result" branches, where the button
   would have come after every data row rather than inside an existing
   cell. Unused now that "Report Wrong Result" is never rendered (see the
   block comment above .report-issue-inline-btn) - kept alongside
   reportButtonRowHtml() itself in case that's ever reinstated. */
tr.report-issue-row td {
  padding-top: 0.5rem;
  border-top: none;
  text-align: left;
}

/* Wraps "Execution Error"/its title alongside the inline Report Error
   button - see the block comment above .report-issue-inline-btn. Overrides
   .error-details strong's own margin-bottom (below) since the row's own
   `gap` provides the spacing instead once the button sits beside it.
   Deliberately NOT `justify-content: space-between` - this row's width
   isn't fixed, it grows with the error message's own natural (unwrapped)
   width (see .error-details p below, a sibling under the same flex parent),
   so space-between would push the button that same variable distance away
   from the title instead of keeping it right beside it. `flex-start` (the
   default) plus the row's own `gap` is what keeps the button pinned
   immediately next to the title regardless of how long the error text is. */
.error-title-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.25rem;
}

.error-title-row strong {
  margin-bottom: 0;
}

.results-tabs-nav {
  flex-shrink: 0;
  display: flex;
  align-items: flex-end;
  background-color: var(--surface-2);
  padding: 6px 8px 0 8px;
  gap: 0px;
  overflow-x: auto;
  user-select: none;
  border-bottom: 1px solid var(--surface-3);
}

.result-tab-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  min-height: 34px;
  padding: 6px 16px;
  font-size: 0.8125rem;
  font-family: inherit;
  color: var(--text-secondary);
  background: transparent;
  border: none;
  outline: none;
  cursor: pointer;
  /* Two lines - the database name (multi-database mode only - see
     buildResultsTabsNav()'s dbLabel) above the "Query N (rows)"/"Note"/
     etc. line - separated by a literal "\n" client.js puts in
     textContent, honored here instead of collapsed to a space. No
     max-width and no flex-shrink: a tab is exactly as wide as its longer
     line needs, however many tabs that adds up to - .results-tabs-nav's
     own overflow-x: auto scrolls the row instead of anything here
     truncating or wrapping either line further. */
  white-space: pre-line;
  line-height: 1.3;
  min-width: 120px;
  flex-shrink: 0;
  transition: color 0.15s ease, background-color 0.15s ease;
  z-index: 1;
}

.result-tab-title {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-weight: 400;
}

.result-tab-badge {
  font-size: 0.7rem;
  background: var(--surface-3);
  color: var(--chip-text);
  padding: 1px 6px;
  border-radius: 10px;
  margin-left: 6px;
}

.result-tab-btn::after {
  content: "";
  position: absolute;
  right: 0;
  top: 8px;
  bottom: 8px;
  width: 1px;
  background-color: var(--surface-3);
  transition: opacity 0.15s;
}

.result-tab-btn:hover::after,
.result-tab-btn.active::after,
.result-tab-btn.active + .result-tab-btn::after {
  opacity: 0;
}

.result-tab-btn:hover:not(.active) {
  background-color: var(--surface-3);
  border-radius: 8px 8px 0 0;
  color: var(--text-table-strong);
}

.result-tab-btn.active {
  background-color: var(--surface-1);
  color: var(--accent-cyan);
  font-weight: 500;
  border-radius: 8px 8px 0 0;
  z-index: 3;
}

/* A tab for a statement that failed (see client.js's
   renderResultsWithFailedStatement()) - colored distinctly from every
   other tab state above so it draws the eye immediately, whether or not
   it's the currently-active tab. */
.result-tab-btn--error {
  color: var(--danger);
}

.result-tab-btn--error:hover:not(.active) {
  background-color: var(--danger-glow);
  color: var(--danger-hover);
}

.result-tab-btn--error.active {
  background-color: var(--surface-1);
  color: var(--danger);
  box-shadow: inset 0 -2px 0 var(--danger);
}

/* "All databases" mode's live-streaming placeholder tab (see client.js's
   startAllModeStreaming()/handlePhaseBConnectionDone()) - a connection
   still being fetched, muted and pulsing so it reads as "in progress"
   rather than as just another result. Reuses .animate-pulse (defined
   above) rather than a new keyframe. */
.result-tab-btn--pending {
  color: var(--text-muted);
  font-style: italic;
}

.result-tab-btn--pending:not(.active) {
  animation: pulse 2s ease-in-out infinite;
}

.results-table-wrapper {
  flex: 1;
  min-height: 0;
  overflow: auto;
  position: relative;
}

.results-table-wrapper table {
  width: 100%;
  border-collapse: collapse;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  font-size: 0.8125rem;
  color: var(--text-table);
  text-align: left;
}

.results-table-wrapper th {
  position: sticky;
  top: 0;
  background-color: var(--surface-2);
  color: var(--text-secondary);
  font-weight: 600;
  text-transform: uppercase;
  font-size: 0.75rem;
  letter-spacing: 0.05em;
  padding: 10px 16px;
  border-bottom: 2px solid var(--surface-3);
  white-space: nowrap;
  z-index: 2;
}

.results-table-wrapper td {
  padding: 10px 16px;
  border-bottom: 1px solid var(--surface-2);
  white-space: pre-wrap;
  word-break: break-word;
}

/* Sortable column headers (see client.js's classifySortableColumnType()/
   handleSortableColumnClick()) - purely client-side, no server round-trip.
   Only headers with 2+ rows to reorder get this treatment at all (see
   isSortable in renderTableResult()); a 0/1-row result's plain <th> is
   untouched by these rules. */
.results-table-wrapper th.sortable-col {
  cursor: pointer;
  user-select: none;
}

.results-table-wrapper th.sortable-col:hover {
  background-color: var(--surface-3);
  color: var(--text-primary);
}

.results-table-wrapper th.sortable-col--active {
  color: var(--text-primary);
}

.sortable-col-arrow {
  display: inline-block;
  min-width: 0.9em;
  margin-left: 0.35em;
  font-size: 0.7rem;
  color: var(--secondary);
}

.results-table-wrapper tbody tr:nth-child(even) {
  background-color: var(--surface-stripe);
}

.results-table-wrapper tbody tr:hover {
  background-color: var(--surface-2);
}

.results-table-wrapper th.cell-numeric,
.results-table-wrapper td.cell-numeric {
  text-align: right;
}

.results-table-wrapper td.cell-multiline {
  white-space: pre-wrap !important;
  word-break: break-word;
  line-height: 1.5;
  font-family: var(--font-mono, monospace);
}

/* Table/Chart toggle (see client.js's renderTableResult()/
   renderResultChart()) - only ever shown for the one single-connection
   result tab per turn whose "visualization" decision survived server-side
   validation (translate_routes.py's _clean_visualization); every other
   tab never un-hides this row at all. Small pill-pair, modeled on
   .result-tab-btn's own active/inactive treatment rather than inventing a
   new control style, sitting in its own row between the tabs strip and
   the table/chart area so it reads as "a property of the currently active
   tab", not as another tab itself. */
.results-view-toggle {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 6px 8px;
  background-color: var(--surface-2);
  border-bottom: 1px solid var(--surface-3);
}

/* Wraps just the Table/Chart pair (see renderTableResult()'s own comment
   on why this is now a separate sub-group from .results-view-toggle
   itself): that row is shown for EVERY non-empty tabular result now (the
   download controls below need to be reachable even when there's nothing
   to chart), but the Table/Chart pills specifically still only apply to
   the one result per turn the LLM/server actually validated as
   chartable. */
.results-view-toggle-buttons {
  display: flex;
  gap: 4px;
}

.results-view-toggle-btn {
  padding: 0.3rem 0.75rem;
  font-size: 0.75rem;
  font-weight: 500;
  font-family: inherit;
  color: var(--text-secondary);
  background: transparent;
  border: 1px solid var(--surface-3);
  border-radius: 999px;
  cursor: pointer;
  transition: var(--transition);
}

.results-view-toggle-btn:hover:not(.active) {
  background-color: var(--surface-3);
  color: var(--text-table-strong);
}

.results-view-toggle-btn.active {
  background-color: var(--accent-cyan);
  border-color: var(--accent-cyan);
  color: var(--on-accent-cyan);
}

/* Download (CSV primary + JSON via the caret menu) - see this feature's
   own section comment in client.js above handleResultDownload().
   margin-left: auto pins it to the right end of .results-view-toggle
   regardless of whether .results-view-toggle-buttons is shown next to it
   or hidden (a non-chartable result, the common case). */
.results-download-group {
  position: relative;
  display: flex;
  margin-left: auto;
}

.results-download-btn,
.results-download-caret-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.3rem 0.6rem;
  font-size: 0.75rem;
  font-weight: 500;
  font-family: inherit;
  color: var(--text-secondary);
  background: transparent;
  border: 1px solid var(--surface-3);
  cursor: pointer;
  transition: var(--transition);
}

.results-download-btn {
  /* Sits flush against the caret button, sharing one pill-shaped outline
     rather than looking like two unrelated buttons - the caret is a
     secondary affordance on the same control, not its own action. */
  border-radius: 999px 0 0 999px;
  border-right: none;
}

.results-download-caret-btn {
  border-radius: 0 999px 999px 0;
  padding: 0.3rem 0.4rem;
}

.results-download-btn:hover,
.results-download-caret-btn:hover {
  background-color: var(--surface-3);
  color: var(--text-table-strong);
}

.results-download-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  background-color: var(--surface-1);
  border: 1px solid var(--bg-card-border);
  border-radius: 8px;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
  min-width: 170px;
  z-index: 100;
  padding: 0.35rem 0;
}

.results-download-menu-item {
  display: block;
  width: 100%;
  padding: 0.5rem 1rem;
  background: transparent;
  border: none;
  color: var(--text-primary);
  font-size: 0.8rem;
  font-family: var(--font-sans);
  text-align: left;
  cursor: pointer;
  transition: var(--transition);
}

.results-download-menu-item:hover {
  background-color: var(--overlay-2);
  color: var(--accent-cyan);
}

/* Shown whenever the currently-displayed tab's result was cut off by
   EXECUTE_RESULTS_MAX_ROWS (see backends/base.py's own docstring on why
   that cap exists, and renderTableResult()'s own comment on where this
   text is set) - a genuinely incomplete result set, so this uses --warning
   (a caution) rather than --accent-cyan's "mild, in-progress" tone that
   .results-retry-status/the new-version banner use elsewhere in this
   file. Sits directly above the table it describes, always visible
   without needing to scroll the table itself, and reappears/updates on
   every tab switch via renderTableResult()'s own top-of-function reset -
   same "reset first, only the one branch that needs it turns it back on"
   posture as the toggle/chart-wrapper reset right above it. */
.results-truncated-notice {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  background-color: rgba(var(--warning-rgb), 0.12);
  border-bottom: 1px solid rgba(var(--warning-rgb), 0.3);
  color: var(--warning);
  font-size: 0.8rem;
}

/* Chart.js's own canvas needs an explicit-height positioned ancestor
   (Chart.js reads the parent's box to size itself, then keeps that size
   fixed on window resize via its own ResizeObserver) - a bare `flex: 1`
   like .results-table-wrapper uses would leave it 0px tall. Swaps places
   with #resultsTableWrapper (see renderTableResult()'s own toggle logic) -
   never both visible at once. */
.results-chart-wrapper {
  flex: 1;
  min-height: 0;
  height: 360px;
  position: relative;
  padding: 12px;
  box-sizing: border-box;
}

/* .inputs-column/.input-row used to wrap the NL/SQL rows inside
   .controls-layout-grid - both replaced by .editor-panes-row/.editor-pane
   (see that rule's own comment further up) when the side-by-side editor
   layout was added, and removed here since grep confirms index.html no
   longer references either class anywhere. */

/* Action Buttons Column */
.action-buttons-column {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 0.5rem;
  height: 100%;
  }

.action-buttons-column .btn {
  flex: none;
  height: 30px;
  padding: 0 0.5rem;
  font-size: 0.72rem;
  line-height: 1.1;
}

/* Stats Column */
.stats-column {
  width: 160px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  justify-content: space-between;
}

.stats-column #translationStats {
  margin-bottom: 0.6rem;
}

.stats-column #executionStats {
  margin-top: 0.6rem;
}

.stat-card-title {
  font-size: 0.65rem;
  font-weight: 700;
  color: var(--accent-cyan);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 0.15rem;
  border-bottom: 1px solid var(--overlay-2);
  padding-bottom: 0.15rem;
}

/* Button height consistency in modals */
.modal-actions .btn-primary,
.btn-modal-secondary {
  height: 34px;
  padding: 0.4rem 1.25rem;
  font-size: 0.70rem;
  font-weight: 600;
  border-radius: 8px;
  border: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.btn-modal-secondary {
  background-color: var(--overlay-3);
  color: var(--text-primary);
}

.btn-modal-secondary:hover {
  background-color: var(--overlay-4);
}

/* --- Google Authentication & Avatar Header Styles --- */
.auth-container {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

/* Toggled by setButtonsDisabled() while a query is in flight - same
   "don't let the ground shift under an in-flight turn" reasoning as
   .badge-disabled above, for a control that can ALSO reset in-flight
   state: the signed-out button starts a Google sign-in, and its
   callback (renderAuthUI()) unconditionally calls
   clearActiveQueryState() + fetchBackendConfig() once it completes; the
   signed-in avatar's "Log out" does the exact same thing via
   handleLogout(). Either firing mid-turn was the "unpredictable result"
   this exists to prevent. pointer-events: none is what actually blocks
   it - the signed-out button is Google's own cross-origin iframe
   (google.accounts.id.renderButton()), which client.js has no script
   access to and so cannot otherwise intercept clicks on; pointer-events
   set on this ancestor stops the browser from ever delivering the click
   to it (or to the signed-in state's real DOM avatar button) in the
   first place, entirely at the hit-testing/compositing level, no
   iframe cooperation required. */
.auth-container.auth-disabled {
  pointer-events: none;
  opacity: 0.45;
  filter: grayscale(1);
}

.auth-menu-wrapper {
  position: relative;
  display: inline-flex;
  align-items: center;
}

.auth-avatar-circle {
  background: var(--overlay-2);
  border: 1px solid var(--bg-card-border);
  color: var(--text-primary);
  padding: 0.35rem; /* Match header buttons padding */
  border-radius: 50%;
  cursor: pointer;
  transition: var(--transition);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 18px; 
  height: 18px; 
  /*width: clamp(32px, 3.2vw, 42px);*/ /* Reduced to match standard header buttons */
  /*height: clamp(32px, 3.2vw, 42px);*/
  flex-shrink: 0;
  overflow: hidden;
}

.auth-avatar-circle:hover {
  background: var(--overlay-4);
  border-color: var(--accent-cyan);
  color: var(--accent-cyan);
}

.auth-avatar-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
}

.auth-avatar-initial {
  font-size: 0.55rem;
  /*font-size: clamp(0.9rem, 1.5vw, 1.1rem);*/
  font-weight: 600;
  color: var(--accent-cyan);
}

.auth-dropdown-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  background-color: var(--surface-1);
  border: 1px solid var(--bg-card-border);
  border-radius: 8px;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
  min-width: 200px;
  z-index: 100;
  overflow: hidden;
}

.auth-dropdown-header {
  padding: 0.75rem 1rem;
}

.auth-dropdown-email {
  font-size: 0.8125rem;
  color: var(--text-secondary);
  word-break: break-all;
}

.auth-dropdown-divider {
  height: 1px;
  background-color: var(--bg-card-border);
}

.auth-dropdown-item {
  width: 100%;
  background: transparent;
  border: none;
  color: var(--text-primary);
  padding: 0.65rem 1rem;
  font-size: 0.85rem;
  font-family: var(--font-sans);
  display: flex;
  align-items: center;
  gap: 0.5rem;
  cursor: pointer;
  transition: var(--transition);
  text-align: left;
}

.auth-dropdown-item:hover {
  background-color: var(--overlay-2);
  color: var(--danger);
}

/* Auth Avatar and Menu Styles matching header buttons */
.auth-menu-wrapper {
  position: relative;
  display: inline-block;
  flex-shrink: 0;
}

.auth-avatar-circle {
  background: var(--overlay-2);
  border: 1px solid var(--bg-card-border);
  color: var(--text-primary);
  padding: 0;
  border-radius: 50%;
  cursor: pointer;
  transition: var(--transition);
  display: flex;
  align-items: center;
  justify-content: center;
  width: clamp(36px, 4vw, 50px);
  height: clamp(36px, 4vw, 50px);
  flex-shrink: 0;
  overflow: hidden;
}

.auth-avatar-circle:hover {
  background: var(--overlay-4);
  color: var(--accent-cyan);
  border-color: var(--accent-cyan);
}

.auth-avatar-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
}

.auth-avatar-initial {
  font-size: clamp(0.9rem, 1.5vw, 1.1rem);
  font-weight: 600;
  color: var(--text-primary);
}

/* Auth Dropdown Menu */
.auth-dropdown-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  background-color: var(--surface-1);
  border: 1px solid var(--bg-card-border);
  border-radius: 8px;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
  min-width: 180px;
  z-index: 100;
  padding: 0.5rem 0;
}

.auth-dropdown-menu.hidden {
  display: none !important;
}

.auth-dropdown-header {
  padding: 0.5rem 1rem;
}

.auth-dropdown-email {
  font-size: 0.8125rem;
  color: var(--text-secondary);
  word-break: break-all;
}

.auth-dropdown-divider {
  height: 1px;
  background-color: var(--bg-card-border);
  margin: 0.25rem 0;
}

.auth-dropdown-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  padding: 0.5rem 1rem;
  background: transparent;
  border: none;
  color: var(--text-primary);
  font-size: 0.85rem;
  cursor: pointer;
  text-align: left;
  transition: var(--transition);
}

.auth-dropdown-item:hover {
  background-color: var(--overlay-2);
  color: var(--accent-cyan);
}

.history-modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
}

.history-header-left {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  flex-wrap: nowrap; /* Forces items to stay on the same line */
  min-width: 0;
  flex: 1;
}

.history-header-left .modal-title {
  white-space: nowrap;
  flex-shrink: 0;
}

.history-clear-wrapper {
  display: flex;
  align-items: center;
  min-width: 0;
}

@media (max-width: 600px) {
  .history-header-left {
    gap: 0.5rem;
  }
  
  .history-clear-wrapper .btn {
    padding: 0.3rem 0.6rem;
    font-size: 0.65rem;
  }
}

.history-turns-subtitle:empty {
  display: none;
}

/* History Action Buttons Sizing & Layout */

.modal-header h2,
.modal-header h5.modal-title {
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--text-primary);
  margin: 0;
}

.history-modal-header {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 1rem;
}

.history-clear-wrapper {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.history-action-btn {
  flex: 0 0 auto;
  min-width: 120px;
  height: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 0.4rem 0.9rem;
  line-height: 1.2;
}

.modal-title {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

.history-title-icon {
  width: 1.15em;
  height: 1.15em;
  stroke: currentColor;
  flex-shrink: 0;
}

/* History modal's per-database turn list (see loadChatHistorySummary()/
   renderChatHistoryBucketList() in client.js) - a plain vertical list of
   rows, each one database's name/type/turn-count plus its own Delete
   button, rather than the old data table + charts this modal used to show. */
.chat-history-list-subtitle {
  margin: 0;
  font-size: 0.82rem;
}

.chat-history-bucket-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.chat-history-bucket-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.65rem 0.9rem;
  border: 1px solid var(--bg-card-border);
  border-radius: 8px;
  background: var(--overlay-2);
}

.chat-history-bucket-row--empty {
  justify-content: center;
  border-style: dashed;
  background: transparent;
}

/* An orphaned bucket (its preset/custom connection no longer exists, or
   it's an unsaved ad hoc connection - see _resolve_bucket_display()'s own
   docstring in chat_history_routes.py) reads as clearly de-emphasized -
   still fully listed and deletable, just visually secondary to a real,
   currently-usable database. */
.chat-history-bucket-row--unavailable {
  opacity: 0.65;
}

.chat-history-bucket-info {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.6rem;
  min-width: 0;
}

.chat-history-bucket-name {
  font-weight: 600;
  color: var(--text-primary);
  overflow-wrap: anywhere;
}

.chat-history-bucket-type {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--text-secondary);
  background: var(--overlay-6);
  border-radius: 4px;
  padding: 0.1rem 0.4rem;
}

.chat-history-bucket-count {
  font-size: 0.8rem;
  color: var(--text-secondary);
  white-space: nowrap;
}

/* #deleteAllChatHistoryBtn (index.html's modal-header "Delete All" button)
   is styled identically here, by id - both it and each per-row button are
   destructive/irreversible delete actions in the same modal, so they're
   deliberately given the same look (danger-red outline, fills solid on
   hover) rather than the header button using the ordinary .btn-primary
   look. Deliberately NOT done by sharing this class name on that button
   directly: .chat-history-bucket-delete-btn is also used as a functional
   selector elsewhere (client.js's delegated per-row click handler, and
   several e2e specs' `.chat-history-bucket-delete-btn` locators) to mean
   specifically "one bucket row's own delete button" - putting the same
   class on the unrelated header button would make every one of those
   ambiguously match two elements instead of one. .history-action-btn
   (also on that button) only contributes layout (min-width/alignment
   inside the modal header) on top of this. */
.chat-history-bucket-delete-btn,
#deleteAllChatHistoryBtn {
  flex: 0 0 auto;
  padding: 0.3rem 0.75rem;
  font-size: 0.78rem;
  color: var(--danger, #f87171);
  background: transparent;
  border: 1px solid var(--danger, #f87171);
  border-radius: 6px;
  cursor: pointer;
  transition: var(--transition);
}

.chat-history-bucket-delete-btn:hover:not(:disabled),
#deleteAllChatHistoryBtn:hover:not(:disabled) {
  background: var(--danger, #f87171);
  color: var(--on-danger, #080b11);
}

.chat-history-bucket-delete-btn:disabled,
#deleteAllChatHistoryBtn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* .controls-layout-grid (the old wrapper around .inputs-column) was
   removed along with .inputs-column/.input-row above - see
   .editor-panes-row's own comment for its replacement. */

/* Wide Screen Layout (> 900px) */
@media (min-width: 901px) {
  .action-buttons-column {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 0.7rem;
    padding: 0.25rem 0;
    height: 100%;
  }

  .action-buttons-column .btn {
    flex: 0 0 auto;
    padding: 0.35rem 0.75rem;
    font-size: 0.78rem;
    line-height: 1.2;
    white-space: nowrap;
    width: 100%;
  }

  .stats-column {
    width: 160px;
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    justify-content: space-between;
  }
}

/* Narrow Screens Layout (<= 900px) */
@media (max-width: 900px) {
  /* 4 Action Buttons placed side-by-side under SQL box */
  .action-buttons-column {
    width: 100%;
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    gap: 0.5rem;
    padding: 0.25rem 0;
  }

  .action-buttons-column .btn,
  .btn-control-block {
    flex: 1; /* Distributes all 4 buttons equally across row width */
    height: 36px;
    width: auto;
    font-size: 0.75rem;
    padding: 0 0.35rem;
  }

  /* 2 Stats areas placed side-by-side UNDER the 4 action buttons */
  .stats-column {
    width: 100%;
    display: flex;
    flex-direction: row;
    gap: 0.75rem;
    justify-content: space-between;
    align-items: stretch;
  }

  .stats-column #translationStats,
  .stats-column #executionStats,
  .stats-column .stats-card-vertical {
    flex: 1;
    margin: 0 !important;
    width: 50%;
  }
}
/* Empty state placeholder in results table */
#resultsBody .text-muted {
  font-family: var(--font-mono);
  font-size: 0.85rem;
  font-style: italic;
  color: var(--text-muted);
  opacity: 0.8;
}

/* CodeMirror Placeholder Styling */
.CodeMirror-placeholder {
  color: var(--text-muted) !important;
  font-family: var(--font-mono) !important;
  font-size: 0.9rem !important;
  font-style: italic !important;
  opacity: 0.8 !important;
}

/* DB Connection Dot Indicator */
.status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  display: inline-block;
  flex-shrink: 0;
  margin-right: 2px;
  background-color: var(--text-muted);
  transition: var(--transition);
}

.status-dot.connected,
.status-dot.success {
  background-color: var(--primary);
  box-shadow: 0 0 6px var(--primary);
}

.status-dot.disconnected,
.status-dot.failed,
.status-dot.error {
  background-color: var(--danger);
  box-shadow: 0 0 6px var(--danger);
}

/* Shown the instant a liveness check (checkDbStatus()/api/ping) starts,
   for however long it takes to actually resolve - the check itself now
   runs in the background rather than blocking the datasource picker's
   open/select flow (see client.js), so this is the only visual cue that a
   check is in flight rather than the dot just sitting on a stale prior
   state. Also set explicitly by selectDataset() the moment a selection is
   clicked, for the same reason - a fresh custom connection's first
   activation can take several real seconds (schema introspection), not
   just the near-instant preference flip a preset switch normally is. */
.status-dot.checking {
  background-color: var(--text-muted);
  animation: status-dot-checking-pulse 1.2s ease-in-out infinite;
}

@keyframes status-dot-checking-pulse {
  0%, 100% { opacity: 0.35; }
  50% { opacity: 1; }
}

.response-cell {
  padding: 1rem 1.25rem;
}

/* Tighter top padding specifically when the "Thoughts & Notes" accordion
   is the first thing in this cell (see thoughtsNotesAccordionHtml()) -
   the accordion's own compact header reads as "floating" with the full
   1rem gap a plain response paragraph wants above it; :has() scopes this
   down to just that one case instead of shrinking top padding for every
   other response-cell (plain text, errors, etc.). */
.response-cell:has(> .thoughts-notes-accordion:first-child) {
  padding-top: 0.5rem;
}

/* "Thoughts & Notes" accordion - see client.js's thoughtsNotesAccordionHtml()
   for the full story (the all-mode triage/routing message, kept
   re-expandable instead of vanishing once Phase C's real answer lands).
   Sits first inside .response-cell, so it's the top-most thing in the
   Response tab regardless of what renders under it. Kept deliberately
   tight (small margin, small padding, line-height: 1 on the header) so it
   reads as a compact disclosure strip, not another full paragraph's worth
   of space.
   white-space: normal is load-bearing, not cosmetic: .response-cell sets
   white-space: pre-wrap (so the real response text's own line breaks
   render), which INHERITS onto this div. thoughtsNotesAccordionHtml()'s
   own template literal is indented for readability, so this div's actual
   DOM children include whitespace-only text nodes (the newlines/indent
   between its tags) - under an inherited pre-wrap, a plain block element
   renders those literally as blank lines instead of collapsing them,
   which is what was actually producing the large empty gaps above/below
   this accordion (confirmed directly in the live page's computed
   layout - this div's own content box was ~75-95px taller than its real
   children, matching exactly the number of stray newlines in the
   template). .thoughts-notes-header below doesn't need the same override:
   it's `display: flex`, and flex containers already suppress
   whitespace-only text-node children regardless of `white-space`. */
.thoughts-notes-accordion {
  /* One blank line's worth of space below the drawer, matching how a
     blank line between paragraphs actually renders in .response-text
     just below it (font-size: 0.9rem * line-height: 1.5 = 1.35rem) -
     requested explicitly, rather than the smaller purely-cosmetic gap
     this used to have. */
  margin-bottom: 1.35rem;
  white-space: normal;
}

/* Same caret-rotates-on-.expanded convention as
   .schema-viewer-group-header/.schema-viewer-group-caret (see that pair's
   own comments) - reused here rather than invented fresh, so every
   collapsible header in the app behaves identically. line-height: 1
   (rather than inheriting body's default) keeps this button's own box no
   taller than its padding + the caret/text actually need. white-space:
   normal here too, defensively - see .thoughts-notes-accordion's own
   comment: this is currently a no-op (flex already suppresses whitespace-
   only text nodes) but costs nothing and guards against this ever being
   true only by accident of `display: flex`. */
.thoughts-notes-header {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  width: 100%;
  padding: 0.25rem 0.5rem;
  background: var(--overlay-2);
  border: 1px solid var(--overlay-18);
  border-radius: 6px;
  color: var(--text-secondary);
  font-weight: 600;
  font-size: 0.75rem;
  font-family: inherit;
  line-height: 1;
  white-space: normal;
  text-align: left;
  cursor: pointer;
  transition: var(--transition);
}

.thoughts-notes-header:hover {
  background-color: var(--overlay-3);
  color: var(--text-primary);
}

.thoughts-notes-caret {
  flex-shrink: 0;
  transition: transform 0.15s ease;
}

/* Points right when collapsed, down when expanded - same convention as a
   native <details> disclosure triangle. */
.thoughts-notes-header.expanded .thoughts-notes-caret {
  transform: rotate(90deg);
}

/* No wrapping <p> inside this (see thoughtsNotesAccordionHtml() -
   `bodyHtml` is injected directly, same inline-HTML shape
   renderMarkdownLiteSummaryTab() always returns) - white-space: pre-wrap
   goes on the div itself so the triage message's own line breaks still
   render, same convention .response-text uses for the main text. Margin/
   padding kept small to match the header's own compact footprint above
   it, rather than opening back up into a second full-size paragraph
   block.*/
.thoughts-notes-body {
  margin-top: 0.3rem;
  padding: 0.45rem 0.6rem;
  background: var(--overlay-1);
  border-left: 2px solid var(--overlay-18);
  color: var(--text-secondary);
  font-family: var(--font-sans);
  font-size: 0.82rem;
  line-height: 1.4;
  white-space: pre-wrap;
}

/* The combine step's own note (see thoughtsNotesAccordionHtml()'s
   `combineHtml`) - sits below the triage text inside the same drawer, so
   margin-top (not padding-top) separates it from whatever triage text
   came before it, same as a second paragraph would. The constructed
   `<p>`/`<pre>` markup here is a single-line template literal (no
   embedded newlines), unlike thoughtsNotesAccordionHtml()'s own top-level
   return value - see that function's comment on why a leading newline in
   HTML inserted under a pre-wrap ancestor renders as a literal blank
   line - so this div needs no white-space override of its own; it simply
   inherits .thoughts-notes-body's pre-wrap, which is exactly what the SQL
   block below needs anyway. */
.thoughts-notes-combine {
  margin-top: 0.5rem;
}

.thoughts-notes-combine p {
  margin: 0 0 0.35rem;
}

.thoughts-notes-combine-sql {
  margin: 0;
  padding: 0.4rem 0.55rem;
  background: var(--overlay-2);
  border-radius: 4px;
  font-family: var(--font-mono, monospace);
  font-size: 0.78rem;
  line-height: 1.4;
  white-space: pre-wrap;
  word-break: break-word;
  overflow-x: auto;
}

.response-text {
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-size: 0.9rem;
  line-height: 1.5;
  white-space: pre-wrap;
  margin: 0;
}

/* Markdown-lite emphasis inside a NO-SQL reply (see client.js's
   renderMarkdownLite()) - strong/em already inherit weight/style from the
   browser default, this just gives an inline `code` span a subtle pill so
   it reads as code rather than blending into the surrounding mono text. */
.response-text code {
  background: var(--overlay-3);
  border-radius: 4px;
  padding: 0.1em 0.35em;
  font-size: 0.85em;
}

/* Inline History Navigation Control inside NL Prompt */
.speech-bubble-wrapper .inline-history-nav {
  position: absolute;
  right: 28px;
  /* Top-anchored, same reasoning as .mic-btn just above - sits right next
     to it in a row along the top edge instead of centered down the box. */
  top: 6px;
  display: inline-flex;
  align-items: center;
  background: var(--overlay-6);
  border: 1px solid var(--overlay-18);
  border-radius: 4px;
  overflow: hidden;
  height: 24px;
  z-index: 5;
}

.speech-bubble-wrapper .inline-history-nav button,
.speech-bubble-wrapper .inline-history-nav span {
  padding: 0 0.35rem;
  font-size: 0.7rem;
  font-weight: 700;
  height: 100%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  user-select: none;
}

.speech-bubble-wrapper .inline-history-nav span {
  color: var(--text-primary);
}

/* The </> history-navigation arrows pick up the app's brand blue (see
   --accent-cyan), matching the mic icon in the same input box; the
   turn-position label (e.g. "3/7") next to them stays the neutral
   --text-primary color above since it isn't a clickable control. */
.speech-bubble-wrapper .inline-history-nav button {
  color: var(--accent-cyan);
  cursor: pointer;
  transition: var(--transition);
}

.speech-bubble-wrapper .inline-history-nav button:hover {
  background: var(--overlay-18);
  color: var(--accent-cyan);
}

.speech-bubble-wrapper .inline-history-nav button:first-child {
  border-right: 1px solid var(--overlay-18);
}

.speech-bubble-wrapper .inline-history-nav button:last-child {
  border-left: 1px solid var(--overlay-18);
}

/* #newTurnBtn ("+") joined this pill as a 4th child after #goForwardBtn,
   which cost #goForwardBtn its own :last-child divider above (newTurnBtn
   is :last-child now, not it) - restored explicitly here rather than
   reshuffling the generic first/last-child rules, which every OTHER
   button in this pill still relies on unchanged. */
.speech-bubble-wrapper .inline-history-nav button#goForwardBtn {
  border-left: 1px solid var(--overlay-18);
}

.speech-bubble-wrapper .inline-history-nav button:disabled,
.speech-bubble-wrapper .inline-history-nav button.is-boundary {
  color: var(--text-secondary);
  opacity: 0.5;
  cursor: not-allowed;
  background: transparent;
}

.speech-bubble-wrapper .inline-history-nav button:disabled:hover,
.speech-bubble-wrapper .inline-history-nav button.is-boundary:hover {
  background: transparent;
  color: var(--text-secondary);
}

/* Adjust textarea padding to prevent text overlap with the inline controls & mic */
.speech-bubble-wrapper textarea#aiPrompt {
  padding-right: 125px !important;
}

/* ===========================================================================
   SCHEMA VIEWER (#schemaViewerModal - see client.js's own section comment
   above openSchemaViewer() for the full feature design: a connection
   picker, a searchable table/tab list on the left, and the selected
   entry's full schema-text block on the right, fed by GET /api/schema -
   the exact same schema text and Table:/Table family:/Tab: entry
   convention every real /api/translate prompt already uses (see
   backends/base.py's split_schema_text_into_entries), never a separately
   maintained "for display only" introspection pass.
   =========================================================================== */

/* Overrides the generic .modal-body padding/flex/scroll behavior, which
   would otherwise scroll the WHOLE body (toolbar included) as one unit
   once the entry list/detail text grows tall - this pins the toolbar and
   notice in place and hands scrolling to the two panes below instead,
   each independently. */
#schemaViewerModal .modal-body.schema-viewer-body {
  padding: 1.25rem;
  gap: 0.75rem;
  overflow: hidden;
  min-height: 0;
}

.schema-viewer-card {
  /* Tall enough that a real multi-table schema doesn't need to scroll the
     list AND the detail pane down to a sliver each - .modal-card's own
     max-height: 85vh (style.css) still caps this on a short viewport. */
  height: 80vh;
  /* 20% wider than the shared .modal-card-large's own max-width: 1000px
     (1000px * 1.2 = 1200px) - this dialog's own 20%/80% LHS/RHS split and
     its 6 equal-width, non-wrapping-header schema columns (see the rules
     below) benefit from the extra room more than an ordinary modal would.
     Two classes on the selector (.modal-card-large.schema-viewer-card)
     rather than just .schema-viewer-card, so this reliably wins over
     .modal-card-large's own max-width by specificity (0,2,0 vs. 0,1,0),
     not by relying on source order - #historyModal also uses
     .modal-card-large and must stay at 1000px, unaffected by this. */
}

.modal-card-large.schema-viewer-card {
  max-width: 1200px;
}

/* Wraps the modal title AND the facts line under it (#schemaViewerFactsLine
   in index.html/.schema-viewer-facts-line below) so .modal-header's own
   space-between/align-items: center layout still treats both as one flex
   child together, with the close button staying put on the right.
   flex-direction: column stacks the title above the facts line. (History:
   the original "Session: ..." one-liner used to live right here on its
   own; it was later moved into the Overview tab's own stats block
   alongside the dataset-size/schema-size facts it was shown with; all
   three facts are now shown together back under the title again, in
   #schemaViewerFactsLine - see that element's own comment in index.html.) */
.schema-viewer-title-block {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  min-width: 0;
}

/* The short "Size: ~...; Schema: ... chars" facts line under the modal
   title (see index.html's own comment on #schemaViewerFactsLine and
   client.js's renderSchemaViewerFactsLine()/buildSchemaViewerFactParts())
   - same small, muted treatment (font-size/color) the old
   .schema-viewer-overview-stats box used for this same text when it lived
   inside the Overview tab, but with no background/border/padding box of
   its own here, since it's now part of the header rather than a
   standalone panel. A single concatenated string (client.js joins the two
   facts with "; "), not a multi-item flex row - there are only ever two
   short facts here now, so plain text wrapping (overflow-wrap) is all
   this needs. .hidden (index.html) hides this entirely whenever there's
   nothing to show yet - see buildSchemaViewerFactParts(). */
.schema-viewer-facts-line {
  font-size: 0.8rem;
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}

/* Groups Refresh Schema (plus its status line underneath) with the close
   button, as .modal-header's own space-between/align-items: center's
   second flex child (schema-viewer-title-block above is the first) - see
   index.html's own comment on why Refresh Schema lives in the header now
   rather than the body toolbar an earlier version put it in.
   align-items: flex-start (not center, .modal-header's own default)
   because the refresh block can grow to two lines (button + status) while
   the close button stays one - center would drift the close button
   downward as that second line appears/disappears. */
.schema-viewer-header-actions {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  flex-shrink: 0;
}

.schema-viewer-refresh-block {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.3rem;
}

/* This button only ever had .btn (layout only, no color - see that
   rule's own comment) plus .history-action-btn (more layout) applied to
   it, with no -primary/-secondary color variant ever added - it was
   rendering with the browser's unstyled default button chrome (a flat
   light grey, near-black text) entirely by omission. Given its own
   explicit look here, matching .btn-secondary's existing app-wide
   convention, both so it reads as an intentional part of the UI and so
   its disabled/"Refreshing..." state below has real, known colors to
   apply enough contrast against - dimming an already-unstyled default via
   opacity alone (the bug this whole rule exists to fix) has no reliable
   floor to stop at. */
.schema-viewer-refresh-btn {
  flex-shrink: 0;
  background-color: var(--overlay-2);
  color: var(--text-primary);
  border: 1px solid var(--bg-card-border);
}

.schema-viewer-refresh-btn:hover:not(:disabled) {
  background-color: var(--overlay-3);
}

/* Disabled for the whole duration of the blocking refresh call (see the
   click handler in client.js), while showing "Refreshing..." - a status
   the user is meant to actually read, not just a dimmed-into-illegibility
   inert control. A flat opacity: 0.5 here (the previous treatment, shared
   with the close button below) reads fine on the close button's single
   glyph, but nearly erases this button's own multi-word label, especially
   given the near-white default background it was accidentally rendering
   with beforehand. Kept at full opacity with its own muted-but-legible
   color pairing instead. */
.schema-viewer-refresh-btn:disabled {
  opacity: 1;
  cursor: default;
  background-color: var(--overlay-3);
  color: var(--text-secondary);
  border-color: var(--bg-card-border);
}

/* The close button's own disabled treatment (a single "x" glyph, not a
   status message) - a plain opacity dim remains the right, simpler
   treatment here, unlike the refresh button's own rule above. */
#schemaViewerModalCloseBtn:disabled {
  opacity: 0.5;
  cursor: default;
}

/* The "Last refreshed: ..." time (or, briefly, a refresh failure's own
   error - see setSchemaViewerRefreshStatus()'s own comment in client.js)
   under the Refresh Schema button. Right-aligned under it (via the parent
   .schema-viewer-refresh-block's align-items: flex-end) since it's a
   small aside about that one button's own action, not body copy. */
.schema-viewer-refresh-status {
  max-width: 260px;
  font-size: 0.72rem;
  color: var(--text-secondary);
  text-align: right;
  overflow-wrap: anywhere;
}

.schema-viewer-refresh-status--error {
  color: var(--danger);
}

/* Notice banner - same visual language as .results-truncated-notice (see
   that rule's own comment) for the same reason: a warning about
   incomplete/stale information sitting directly above the content it
   describes. Doubles as the error state (a failed fetch - connection
   unreachable, credentials invalid, nothing to describe) by adding
   .schema-viewer-notice--error, which swaps the warning-amber treatment
   for --danger so a hard failure doesn't read as a mere caution. */
.schema-viewer-notice {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  background-color: rgba(var(--warning-rgb), 0.12);
  border: 1px solid rgba(var(--warning-rgb), 0.3);
  border-radius: 8px;
  color: var(--warning);
  font-size: 0.8rem;
}

.schema-viewer-notice--error {
  background-color: rgba(var(--danger-rgb), 0.12);
  border-color: rgba(var(--danger-rgb), 0.3);
  color: var(--danger);
}

/* Dataset-group Schema Viewer variant (see client.js's
   openGroupSchemaViewer()/loadGroupSchemaViewer()) - shown instead of
   .schema-viewer-panes below, so this needs the same flex/scroll behavior
   that rule documents (fills the remaining body height, scrolls its own
   content rather than the whole modal body). */
.schema-viewer-group-table-wrap {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
}

/* Same font/sizing conventions as .schema-viewer-columns-table (the
   single-connection detail pane's own columns table) below, for visual
   consistency between the two - just 4 equal-width columns here instead
   of 6, and no min-width forcing a horizontal scrollbar, since "Name",
   "Type", "Data Size", "Schema Size (tokens)" comfortably fit an ordinary
   modal width unwrapped even in four equal shares. */
.schema-viewer-group-table {
  width: 100%;
  table-layout: fixed;
  border-collapse: collapse;
  font-family: var(--font-mono);
  font-size: 0.8rem;
}

.schema-viewer-group-table th,
.schema-viewer-group-table td {
  width: 25%;
  padding: 0.4rem 0.65rem;
  text-align: left;
  border-bottom: 1px solid var(--bg-card-border);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.schema-viewer-group-table thead th {
  position: sticky;
  top: 0;
  background: var(--overlay-2);
  color: var(--text-secondary);
  font-weight: 600;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.schema-viewer-group-table tbody tr:last-child td {
  border-bottom: none;
}

.schema-viewer-group-table tbody tr:hover {
  background: var(--overlay-1);
}

/* A member whose schema fetch failed outright (db.py's
   build_group_schema_summaries() "available": false) - dimmed, with a
   title tooltip explaining why, same "something's wrong with just this one
   row" posture the rest of this feature takes rather than failing the
   whole dialog over one bad member. Originally a <tr> class (the group
   dialog's old flat-table layout, client.js's now-retired
   renderGroupSchemaViewerRow()); reused as-is on the "Datasets" tree's own
   per-member <button> row now that the group dialog shares the
   Overview/Datasets tree layout instead (see
   renderGroupSchemaViewerEntryList() in client.js) - opacity applies the
   same way to either element. */
.schema-viewer-group-row--unavailable {
  opacity: 0.55;
}

/* gap: 0 (not the 1rem this used to be) - the draggable resizer between
   the two panes (.schema-viewer-panes-resizer below) now supplies both
   the visual divider and the breathing room on either side of it, via its
   own width plus each pane's own padding-right/padding-left. */
.schema-viewer-panes {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  gap: 0;
}

/* Defaults to a fixed 20%/80% split with the detail pane, exactly like
   before this pane became resizable - NOT content- or modal-width-
   dependent like the old flex: 0 0 260px used to be (260px was a bigger
   fraction of a narrow modal than a wide one, so the split visibly
   shifted as the modal resized). Percentage flex-basis resolves against
   .schema-viewer-panes' own content-box width, and flex-grow/shrink are
   both 0, so this pane is always exactly one fifth of that width no
   matter how wide the modal is - UNTIL the user drags
   #schemaViewerPanesResizer (see initSchemaViewerPanesResizer() in
   client.js), which sets an inline `flex-basis` in px directly on this
   element's `style` - an inline style always wins over this class rule
   regardless of source order/specificity, so no `!important` or extra
   selector weight is needed here for that override to take effect.
   min-width: 0 is load-bearing independent of any of that: a flex item's
   default automatic min-width is its content's min-content size, and a
   long, unbroken table/view name several levels down in the tree (an
   .schema-viewer-entry-item button, white-space: nowrap so it CAN
   truncate with an ellipsis - see that rule below) still counts toward
   that min-content size even though the button's own overflow: hidden
   clips it visually, because its width: 100% is ignored for intrinsic
   sizing. Without min-width: 0 here, a single long enough name would
   silently grow this pane (and shrink the detail pane) well past its
   intended/dragged width, instead of the name itself truncating as
   designed. */
.schema-viewer-list-pane {
  flex: 0 0 20%;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding-right: 0.5rem;
}

/* The draggable divider itself, between .schema-viewer-list-pane and
   .schema-viewer-detail-pane (see initSchemaViewerPanesResizer() in
   client.js for the actual drag mechanics). 9px wide so there's a
   comfortable hit/grab area for a mouse, even though the visible line
   (the ::after pseudo-element below) is a hairline 1px centered inside
   it - a bare 1px element would be nearly impossible to grab reliably.
   flex: 0 0 auto (never grows/shrinks) so its own width is never affected
   by either pane's resizing. */
.schema-viewer-panes-resizer {
  flex: 0 0 auto;
  width: 9px;
  min-height: 0;
  position: relative;
  cursor: col-resize;
  flex-shrink: 0;
  /* See .editor-panes-resizer's own comment in the NL/SQL row section
     above - same touch-drag fix, same reason. */
  touch-action: none;
}

.schema-viewer-panes-resizer::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 1px;
  background: var(--bg-card-border);
  transform: translateX(-50%);
  transition: background-color 0.15s ease, width 0.15s ease;
}

/* Hover/keyboard-focus/actively-dragging all get the same "this is
   grabbable/being grabbed" treatment - a thicker, brand-colored line -
   .schema-viewer-panes-resizer--dragging is toggled by JS for the
   duration of a drag (hover/focus alone don't cover the case where the
   pointer moves faster than the resizer's own width during a fast drag). */
.schema-viewer-panes-resizer:hover::after,
.schema-viewer-panes-resizer:focus-visible::after,
.schema-viewer-panes-resizer.schema-viewer-panes-resizer--dragging::after {
  width: 3px;
  background: var(--accent-cyan);
}

.schema-viewer-panes-resizer:focus-visible {
  outline: none;
}

.schema-viewer-entry-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.schema-viewer-entry-item {
  display: block;
  width: 100%;
  padding: 0.45rem 0.6rem;
  background: transparent;
  border: none;
  border-radius: 6px;
  color: var(--text-primary);
  font-size: 0.82rem;
  font-family: var(--font-mono);
  text-align: left;
  cursor: pointer;
  transition: var(--transition);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.schema-viewer-entry-item:hover {
  background-color: var(--overlay-2);
}

.schema-viewer-entry-item.active {
  background-color: var(--accent-cyan);
  color: var(--on-accent-cyan);
}

/* The left-hand list's 2-level tree (see SCHEMA_VIEWER_GROUPS/
   renderSchemaViewerEntryList() in client.js): one collapsible
   .schema-viewer-tree-group per top-level group (Tables/Views/Indexes/
   Routines), each a .schema-viewer-group-header button (name + live item
   count) followed by a .schema-viewer-group-children list of ordinary
   .schema-viewer-entry-item leaves, indented under it. */
.schema-viewer-tree-group {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.schema-viewer-group-header {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  width: 100%;
  padding: 0.35rem 0.6rem;
  background: transparent;
  border: none;
  border-radius: 6px;
  color: var(--text-secondary);
  font-weight: 600;
  font-size: 0.78rem;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: var(--transition);
}

.schema-viewer-group-header:hover {
  background-color: var(--overlay-2);
  color: var(--text-primary);
}

.schema-viewer-group-caret {
  flex-shrink: 0;
  transition: transform 0.15s ease;
}

/* Points right when collapsed, down when expanded - the same convention
   as a native <details> disclosure triangle. */
.schema-viewer-group-header.expanded .schema-viewer-group-caret {
  transform: rotate(90deg);
}

.schema-viewer-group-children {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.schema-viewer-group-children .schema-viewer-entry-item {
  padding-left: 1.6rem;
}

/* Takes whatever's left after the list pane's width (default 20%, or
   whatever the user last dragged it to - see .schema-viewer-list-pane's
   own comment) and the resizer's own 9px. flex-basis: 0% (rather than
   auto) means this pane's own content never influences its share,
   matching the list pane's own fixed-percentage comment above. min-width:
   0 is load-bearing: a flex item's default min-width is its content's
   min-content size, which would otherwise stop the columns table below
   from ever triggering its own horizontal scrollbar and instead push this
   pane (and the whole modal) wider than intended. */
.schema-viewer-detail-pane {
  flex: 1 1 0%;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding-left: 0.5rem;
}

.schema-viewer-detail-heading {
  flex-shrink: 0;
  font-weight: 600;
  color: var(--text-primary);
  overflow-wrap: anywhere;
}

/* This table's own "[table] NAME: description" catalog comment (see
   parseSchemaComments()/schemaViewerTableCommentText in client.js) -
   deliberately styled as a quiet aside (muted color, italic, smaller than
   the heading it sits under) rather than another bold heading-like line,
   since it's descriptive metadata about the table, not a fact on the same
   level as its name/row count. */
.schema-viewer-table-comment {
  flex-shrink: 0;
  font-size: 0.82rem;
  font-style: italic;
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}

.schema-viewer-detail-text {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  margin: 0;
  padding: 0.85rem 1rem;
  background: var(--overlay-2);
  border: 1px solid var(--bg-card-border);
  border-radius: 8px;
  font-family: var(--font-mono);
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--text-primary);
  white-space: pre-wrap;
  word-break: break-word;
}

/* The structured columns table at the top of the detail pane (see
   parseSchemaEntryColumns() in client.js) - a quick-scan summary of every
   column's name/type/nullability/attributes, parsed best-effort from the
   same schema text .schema-viewer-detail-text shows whatever's left of
   below it. flex: 1 1 auto (same as .schema-viewer-detail-text) lets it
   grow to fill the whole pane by itself in the common case where there's
   no leftover raw text to show (that sibling is then .hidden, so this is
   the only flex child and claims 100% of the space) - only a column list
   long enough to still overflow that full height scrolls internally
   (overflow: auto below). When a last entry's leftover text IS showing
   alongside it, the two flex children split the pane's height evenly.
   .hidden (shared app-wide utility class) is toggled on this wrap
   directly when a given entry's text doesn't fit the parser's convention
   at all (e.g. a Google Sheets dataset's own column format, or the "No
   schema description available." fallback), so there's nothing to show
   but the raw text. */
.schema-viewer-columns-wrap {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  border: 1px solid var(--bg-card-border);
  border-radius: 8px;
  background: var(--overlay-2);
}

/* table-layout: fixed + an explicit equal width per column (below) is
   what makes all 7 columns genuinely equal-width instead of the browser's
   default content-based auto-sizing, which is what used to let a long
   Sample values/Constraints/Comments value visually "steal" width from
   its neighbors and squeeze Column/Type/Nullable's headers into wrapping.
   min-width keeps that equal split from ever being squeezed so tight
   that a header would have to wrap: it's sized so "Sample values" - the
   longest of the 7 headers - comfortably fits, unwrapped, in a single
   14.2857% share (~130px) including this table's own cell padding. Below
   that width, .schema-viewer-columns-wrap's own overflow: auto is what
   gives the table a horizontal scrollbar instead of shrinking further. */
.schema-viewer-columns-table {
  width: 100%;
  min-width: 910px;
  table-layout: fixed;
  border-collapse: collapse;
  font-family: var(--font-mono);
  font-size: 0.8rem;
}

.schema-viewer-columns-table th,
.schema-viewer-columns-table td {
  width: 14.2857%;
  padding: 0.4rem 0.65rem;
  text-align: left;
  border-bottom: 1px solid var(--bg-card-border);
}

/* Headers never wrap - table-layout: fixed plus the equal 16.6667% width
   and this table's own min-width (both above) already guarantee every
   one of the 6 headers has comfortable, identical room. */
.schema-viewer-columns-table th {
  white-space: nowrap;
}

/* Cell CONTENT is a different story: Type ("STRUCT<...>" on BigQuery),
   Attributes (DEFAULT/IDENTITY/AUTO_INCREMENT/...), Constraints (can
   include a full "FOREIGN KEY -> other_table(other_column)" reference)
   and Sample values (a frequent-values list) can all get arbitrarily
   long, so body cells wrap instead of forcing their column wider than
   its equal 16.6667% share (which, with table-layout: fixed, would
   distort every other column's width too, not just this one's). */
.schema-viewer-columns-table td {
  white-space: normal;
  word-break: break-word;
}

.schema-viewer-columns-table thead th {
  position: sticky;
  top: 0;
  background: var(--overlay-2);
  color: var(--text-secondary);
  font-weight: 600;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.schema-viewer-columns-table tbody tr:last-child td {
  border-bottom: none;
}

/* Pinned "Overview"/"ER Diagram" tree rows (see
   renderSchemaViewerEntryList() in client.js) - a slightly bolder weight
   than an ordinary .schema-viewer-entry-item leaf, closer to
   .schema-viewer-group-header's own weight, so the two of them visually
   read as the tree's own top/default entries rather than just another
   table name. Sit above every collapsible group, outside
   .schema-viewer-group-children, so neither is ever affected by any
   group's own expand/collapse state. Shared by both rows rather than a
   dedicated "diagram" variant since they're the same visual tier. */
.schema-viewer-overview-item {
  font-weight: 600;
  font-family: inherit;
}

/* Shared mount point for the pinned "Overview" entry's content (see
   renderSchemaViewerOverviewDetail() in client.js: prose, then suggested
   questions) and the pinned "ER Diagram" entry's content right under it
   (see renderSchemaViewerDiagramDetail(): just the diagram - it used to
   render at the bottom of this same wrap when Overview was selected,
   now it's its own tree row). Occupies the same flex slot
   .schema-viewer-columns-wrap/.schema-viewer-detail-text occupy for every
   other entry (only one of the three is ever visible at once - see that
   function's own hidden/shown toggling), scrollable as one unit since the
   diagram can get tall for a schema with many relationships. */
.schema-viewer-overview-wrap {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* Warnings block pinned to the top of the Overview tab (see
   renderSchemaViewerStatsBlockHtml() in client.js) - a warning row
   whenever this connection is actually hitting the SCHEMA_MAX_TABLES or
   SCHEMA_MAX_SCHEMA_CHARS limit (formerly surfaced only in the
   dismissable notice bar - see loadSchemaViewerConnection()'s own comment
   on data.truncated/data.has_omitted_tables). Rendered above the
   AI-written prose (or the "nothing generated yet" message) since none of
   this depends on that overview ever having been generated. (The
   dataset-size/schema-size/session-info facts this box used to also show
   now live in the persistent facts line under the modal title instead -
   see .schema-viewer-facts-line above - so this box is warnings-only and
   simply isn't rendered at all when there are none.) */
.schema-viewer-overview-stats {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: 0.7rem 1rem;
  background: var(--overlay-2);
  border: 1px solid var(--bg-card-border);
  border-radius: 8px;
  font-size: 0.8rem;
  color: var(--text-secondary);
}

.schema-viewer-overview-warning {
  color: var(--warning);
  font-weight: 500;
  overflow-wrap: anywhere;
}

.schema-viewer-overview-prose {
  margin: 0;
  padding: 0.85rem 1rem;
  background: var(--overlay-2);
  border: 1px solid var(--bg-card-border);
  border-radius: 8px;
  font-size: 0.88rem;
  line-height: 1.55;
  color: var(--text-primary);
}

.schema-viewer-overview-empty {
  margin: 0;
  padding: 0.85rem 1rem;
}

.schema-viewer-overview-diagram-wrap {
  flex-shrink: 0;
  border: 1px solid var(--bg-card-border);
  border-radius: 8px;
  background: var(--overlay-2);
  padding: 0.75rem;
  overflow: auto;
}

.schema-viewer-overview-diagram {
  display: flex;
  justify-content: center;
}

.schema-viewer-overview-diagram svg {
  max-width: 100%;
  height: auto;
}

.schema-viewer-overview-questions-block {
  flex-shrink: 0;
}

.schema-viewer-overview-questions-title {
  font-weight: 600;
  font-size: 0.78rem;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  margin-bottom: 0.5rem;
}

.schema-viewer-overview-questions {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

/* "Fill the prompt box and go" behavior (see
   renderSchemaViewerOverviewDetail() in client.js) - styled as a chip/pill
   since these are one-click actions, not plain body text. */
.schema-viewer-suggested-question {
  display: block;
  width: 100%;
  padding: 0.55rem 0.8rem;
  background: var(--overlay-2);
  border: 1px solid var(--bg-card-border);
  border-radius: 8px;
  color: var(--text-primary);
  font-size: 0.85rem;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: var(--transition);
}

.schema-viewer-suggested-question:hover {
  background-color: var(--overlay-3);
  border-color: var(--accent-cyan);
  color: var(--accent-cyan);
}

.schema-viewer-columns-table tbody tr:hover {
  background-color: var(--overlay-3, rgba(255, 255, 255, 0.04));
}

.schema-viewer-col-name {
  color: var(--text-primary);
  font-weight: 600;
}

.schema-viewer-not-null {
  color: var(--warning);
  font-weight: 600;
}

/* Supplementary, not structural (unlike Attributes/Constraints) - a
   sampled peek at real data, so it's toned down relative to the rest of
   the row rather than competing with it visually. */
.schema-viewer-col-samples {
  color: var(--text-secondary);
  font-style: italic;
}

/* Same treatment as .schema-viewer-col-samples above, for the same reason
   - a column's own catalog comment (see parseSchemaComments() in
   client.js) is descriptive metadata, not a structural fact like
   Attributes/Constraints, so it's toned down the same way. */
.schema-viewer-col-comments {
  color: var(--text-secondary);
  font-style: italic;
}