/* Design system ported from ensoul-public — clean skin, colors from
   flutter-beta/lib/core/theme/colors.dart (AppColors, the only place a
   complete light+dark palette exists; ensoul.css itself is dark-only),
   typography/radius/shadow from ensoul.css. No arcane mode (Decision E) —
   nothing to switch between besides light/dark.

   Theme resolution is the standard three-state pattern: :root carries the
   light palette as the bare default; @media (prefers-color-scheme: dark)
   applies dark values unless an explicit light override is set via
   data-theme; :root[data-theme="dark"] applies dark values regardless of
   system preference, so a manual toggle always wins. See app.js's theme
   toggle for how data-theme gets set/persisted. */

@import url('https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&family=Inter:ital,opsz,wght@0,14..32,300..700;1,14..32,300..700&display=swap');

* { margin: 0; padding: 0; box-sizing: border-box; }

input, textarea, button, select {
  font-family: inherit; font-size: inherit; font-weight: inherit; line-height: inherit;
}

:root {
  --bg-primary: #FAFAF9;
  --bg-secondary: #FFFFFF;
  --bg-tertiary: #F2F2F1;
  --bg-elevated: #FFFFFF;

  --surface-dark: #ECECEA;
  --surface-medium: #F2F2F1;
  --surface-light: #FFFFFF;

  --accent: #605C58;
  --accent-subtle: rgba(96, 92, 88, 0.12);
  --accent-glow: rgba(96, 92, 88, 0.35);

  --border-subtle: rgba(0, 0, 0, 0.06);
  --border-light: rgba(0, 0, 0, 0.10);
  --border-medium: rgba(0, 0, 0, 0.18);

  --text-primary: #0D0D0D;
  --text-secondary: #4C4C4C;
  --text-muted: #9A9A9A;

  --user-bubble: #ECECEA;
  --assistant-bubble: #FFFFFF;

  --font-ui: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif;
  --font-body: 'Lora', Georgia, 'Times New Roman', serif;
  --font-scale: 1; /* Appearance -> Font size (settings-plan Phase 8) — everything else already cascades in em/relative units off body's font-size, so scaling that one value scales the whole app */

  --radius: 10px;
  --radius-sm: 8px;
  --radius-sheet: 16px;
  --radius-pill: 999px;

  --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.10);
  --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.14);
  --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.18);

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;

  --icon-btn-size: 34px;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg-primary: #161616;
    --bg-secondary: #1C1C1C;
    --bg-tertiary: #252525;
    --bg-elevated: #1C1C1C;

    --surface-dark: #202020;
    --surface-medium: #252525;
    --surface-light: #2E2E2E;

    --accent: #A09588;
    --accent-subtle: rgba(160, 149, 136, 0.12);
    --accent-glow: rgba(160, 149, 136, 0.35);

    --border-subtle: rgba(255, 255, 255, 0.07);
    --border-light: rgba(255, 255, 255, 0.12);
    --border-medium: rgba(255, 255, 255, 0.20);

    --text-primary: #EBEBEB;
    --text-secondary: #8C8C8C;
    --text-muted: #5A5A5A;

    --user-bubble: #252525;
    --assistant-bubble: #161616;

    --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.4);
    --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.5);
    --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.6);
  }
}

:root[data-theme="dark"] {
  --bg-primary: #161616;
  --bg-secondary: #1C1C1C;
  --bg-tertiary: #252525;
  --bg-elevated: #1C1C1C;

  --surface-dark: #202020;
  --surface-medium: #252525;
  --surface-light: #2E2E2E;

  --accent: #A09588;
  --accent-subtle: rgba(160, 149, 136, 0.12);
  --accent-glow: rgba(160, 149, 136, 0.35);

  --border-subtle: rgba(255, 255, 255, 0.07);
  --border-light: rgba(255, 255, 255, 0.12);
  --border-medium: rgba(255, 255, 255, 0.20);

  --text-primary: #EBEBEB;
  --text-secondary: #8C8C8C;
  --text-muted: #5A5A5A;

  --user-bubble: #252525;
  --assistant-bubble: #161616;

  --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.4);
  --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.5);
  --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.6);
}

body {
  background: var(--bg-primary);
  color: var(--text-primary);
  font-family: var(--font-ui);
  font-size: calc(15px * var(--font-scale));
  line-height: 1.5;
  height: 100vh;
  /* dvh accounts for mobile Safari/Chrome's collapsing address bar —
     100vh there is the LARGEST possible viewport, so fixed-height flex
     children (the whole #app shell, which relies on body never scrolling)
     get clipped under the toolbar until you scroll. Kept as a second
     declaration, not a replacement: browsers without dvh support just
     ignore it and keep the 100vh above. */
  height: 100dvh;
  overflow: hidden;
}

button {
  cursor: pointer;
  background: var(--surface-light);
  color: var(--text-primary);
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  padding: 9px 16px;
  font-size: 14px;
  font-weight: 500;
  transition: background 0.15s, border-color 0.15s, transform 0.1s, box-shadow 0.15s;
}
button:hover { background: var(--bg-tertiary); border-color: var(--border-medium); }
button:active { transform: scale(0.98); }
button.primary { background: var(--accent); color: var(--bg-primary); border-color: var(--accent); font-weight: 600; box-shadow: 0 1px 2px var(--accent-glow); }
button.primary:hover { opacity: 0.92; }
button.primary:active { transform: scale(0.98); }
button:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
button.danger { color: #a83c3c; border-color: rgba(200, 80, 80, 0.4); }
button.danger:hover { background: rgba(200, 80, 80, 0.12); border-color: #a83c3c; }

/* --- Icon system (chat UI beautification pass) --- */

.icon { display: block; flex-shrink: 0; }

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: var(--icon-btn-size);
  height: var(--icon-btn-size);
  padding: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-secondary);
}
.icon-btn:has(span) { width: auto; padding: 0 12px 0 10px; }
.icon-btn:hover { background: var(--bg-tertiary); color: var(--text-primary); border-color: transparent; }
.icon-btn.active { background: var(--accent-subtle); color: var(--accent); border-color: var(--accent); }
.icon-btn.danger:hover { background: rgba(200, 80, 80, 0.12); color: #a83c3c; }
.icon-btn span { font-size: 13px; white-space: nowrap; }

.overflow-menu-wrap { position: relative; }
.overflow-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  min-width: 200px;
  padding: 4px;
  background: var(--bg-elevated);
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
  display: flex;
  flex-direction: column;
  z-index: 50;
}
.overflow-menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  background: none;
  border: none;
  color: var(--text-primary);
  font-size: 13px;
  text-align: left;
}
.overflow-menu-item:hover { background: var(--bg-tertiary); }
.overflow-menu-item.danger { color: #a83c3c; }
.overflow-menu-item.danger:hover { background: rgba(200, 80, 80, 0.12); }

/* Reactions (Phase 1, best-in-class chat features) — the emoji-grid
   popover shares .overflow-menu-wrap's position:relative trigger pattern,
   just a grid instead of a column of labeled rows. */
.reaction-picker {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 2px;
  padding: 6px;
  background: var(--bg-elevated);
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
  z-index: 50;
}
.reaction-picker-emoji {
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 17px;
  line-height: 1;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
}
.reaction-picker-emoji:hover { background: var(--bg-tertiary); }
.reaction-picker-emoji.active { background: var(--accent-subtle); border-color: var(--accent); }

/* The chip row under a message's content, one per active user reaction —
   clicking one removes it. */
.reaction-chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
.reaction-chip {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  font-size: 13px;
  line-height: 1.4;
  background: var(--bg-tertiary);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-pill);
}
.reaction-chip:hover { border-color: var(--accent); }

/* The badge(s) on the corner of a USER's bubble showing a companion's
   reaction to it — .message needs position:relative for this to anchor
   correctly, added on that rule directly rather than here. */
.companion-reaction-badge-row {
  position: absolute;
  bottom: -10px;
  right: 4px;
  display: flex;
  gap: 2px;
}
.companion-reaction-badge {
  font-size: 15px;
  line-height: 1;
  background: var(--bg-elevated);
  border: 1px solid var(--border-light);
  border-radius: 50%;
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-sm);
}

textarea, input[type="text"], input[type="password"] {
  background: var(--bg-tertiary);
  color: var(--text-primary);
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  padding: 10px 12px;
  width: 100%;
}
textarea:focus, input:focus { outline: none; border-color: var(--accent); }

/* --- Native form controls, restyled (UI beautification pass) ---
   input[type=range]/checkbox/select all rendered as full OS-native
   chrome before this pass — default blue accents, mismatched fonts and
   corner radii, the one visible gap in an otherwise fully-designed
   system (buttons and text inputs already had their own rules above). */

/* Range sliders — used by Generation Defaults and every per-companion
   override slider (buildOverrideSlider). --range-fill is a 0-100% custom
   property app.js's updateRangeFill() keeps in sync with the slider's own
   value on init and on every input event, so the filled portion actually
   shows progress in the accent color instead of a uniform gray bar —
   Firefox gets the same effect natively via ::-moz-range-progress, no JS
   needed there. */
input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 20px;
  margin: 0;
  background: transparent;
  cursor: pointer;
}
input[type="range"]::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: var(--radius-pill);
  background: linear-gradient(to right, var(--accent) var(--range-fill, 0%), var(--bg-tertiary) var(--range-fill, 0%));
}
input[type="range"]::-moz-range-track { height: 4px; border-radius: var(--radius-pill); background: var(--bg-tertiary); }
input[type="range"]::-moz-range-progress { height: 4px; border-radius: var(--radius-pill); background: var(--accent); }
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 16px;
  height: 16px;
  margin-top: -6px; /* centers the 16px thumb on the 4px track: (4 - 16) / 2 */
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--bg-secondary);
  box-shadow: var(--shadow-sm);
  transition: transform 0.1s ease, box-shadow 0.1s ease;
}
input[type="range"]::-moz-range-thumb {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--bg-secondary);
  box-shadow: var(--shadow-sm);
}
input[type="range"]:hover::-webkit-slider-thumb { transform: scale(1.12); }
input[type="range"]:hover::-moz-range-thumb { transform: scale(1.12); }
input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.2); box-shadow: 0 0 0 6px var(--accent-subtle); }
input[type="range"]:active::-moz-range-thumb { transform: scale(1.2); box-shadow: 0 0 0 6px var(--accent-subtle); }
input[type="range"]:focus-visible { outline: none; }
input[type="range"]:focus-visible::-webkit-slider-thumb { outline: 2px solid var(--accent); outline-offset: 2px; }
input[type="range"]:focus-visible::-moz-range-thumb { outline: 2px solid var(--accent); outline-offset: 2px; }
input[type="range"]:disabled { opacity: 0.5; cursor: not-allowed; }

/* Checkboxes — plain settings toggles (Voice's auto-play, Image
   generation's auto-detect) that don't fit buildOverrideSelect's
   inherit/on/off shape (see that function's own comment on why a bare
   global default has no such "unset" state to represent). */
input[type="checkbox"] {
  -webkit-appearance: none;
  appearance: none;
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  border-radius: 5px;
  border: 1px solid var(--border-medium);
  background: var(--bg-tertiary);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background 0.12s, border-color 0.12s;
}
input[type="checkbox"]:hover { border-color: var(--accent); }
input[type="checkbox"]:checked {
  background: var(--accent);
  border-color: var(--accent);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23FAFAF9' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3.5 8.5 6.5 11.5 12.5 4.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
}
input[type="checkbox"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
input[type="checkbox"]:disabled { opacity: 0.5; cursor: not-allowed; }
/* The checkmark's stroke is baked into the SVG data URI above as a fixed
   light color (contrasts against light mode's dark --accent fill) — dark
   mode's --accent is a much lighter taupe, so the checkmark needs to flip
   to a dark stroke there instead, same two-selector theme-override shape
   every other theme-dependent rule in this file already uses. */
:root[data-theme="dark"] input[type="checkbox"]:checked {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23161616' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3.5 8.5 6.5 11.5 12.5 4.5'/%3E%3C/svg%3E");
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) input[type="checkbox"]:checked {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23161616' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3.5 8.5 6.5 11.5 12.5 4.5'/%3E%3C/svg%3E");
  }
}

/* Color picker — Appearance's accent-color swatch (found in a follow-up
   sweep after the rest of this pass, same root cause: a native form
   control never pulled into the app's own design language). The swatch
   itself is browser-drawn and can't be replaced, but its frame can —
   this trades the default sharp-cornered, thin-gray-bordered rectangle
   for one that matches every other control's radius/border here. */
input[type="color"] {
  width: 40px;
  height: 32px;
  padding: 2px;
  border: 1px solid var(--border-light);
  border-radius: var(--radius-sm);
  background: var(--bg-tertiary);
  cursor: pointer;
}
input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; border-radius: calc(var(--radius-sm) - 2px); overflow: hidden; }
input[type="color"]::-webkit-color-swatch { border: none; border-radius: calc(var(--radius-sm) - 2px); }
input[type="color"]::-moz-color-swatch { border: none; border-radius: calc(var(--radius-sm) - 2px); }

/* Selects — Voice's provider picker, notification frequency, font size,
   reasoning effort, and every buildOverrideSelect-produced dropdown.
   appearance:none strips each browser's own wildly-inconsistent native
   chrome; the chevron is a plain inline SVG (currentColor won't tint a
   background-image, so it's baked in as --text-secondary directly) so it
   never needs a separate absolutely-positioned icon element. */
select {
  -webkit-appearance: none;
  appearance: none;
  background-color: var(--bg-tertiary);
  color: var(--text-primary);
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  padding: 9px 34px 9px 12px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%234C4C4C' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='4 6 8 10 12 6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  /* Without an explicit size, a viewBox-only SVG (no width/height
     attributes) has no intrinsic size for a background-image to fall back
     on, so the browser blows it up to fill the available box instead of
     rendering it as a small chevron — found live: it rendered huge enough
     to overlap the selected option's text. */
  background-size: 12px 12px;
  cursor: pointer;
}
select:hover { border-color: var(--border-medium); }
select:focus { outline: none; border-color: var(--accent); }
select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
select:disabled { opacity: 0.5; cursor: not-allowed; }
:root[data-theme="dark"] select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%238C8C8C' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='4 6 8 10 12 6'/%3E%3C/svg%3E"); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%238C8C8C' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='4 6 8 10 12 6'/%3E%3C/svg%3E"); }
}

/* Custom tooltip system (UI beautification follow-up) — replaces the
   browser's own instant, unstyled native tooltip with one small
   animated pill, fading and rising in slightly on a short hover delay
   (see app.js's showAppTooltip/positionAppTooltip). One shared element,
   repositioned per target rather than one node per tooltip. */
.app-tooltip {
  position: fixed;
  z-index: 1000;
  pointer-events: none;
  max-width: 260px;
  padding: 5px 9px;
  border-radius: 6px;
  background: var(--text-primary);
  color: var(--bg-primary);
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: 500;
  line-height: 1.4;
  box-shadow: var(--shadow-md);
  opacity: 0;
  transform: translateY(4px) scale(0.96);
  transform-origin: bottom center;
  transition: opacity 0.12s ease, transform 0.12s ease;
}
.app-tooltip.below { transform: translateY(-4px) scale(0.96); transform-origin: top center; }
.app-tooltip.visible { opacity: 1; transform: translateY(0) scale(1); }
@media (prefers-reduced-motion: reduce) {
  .app-tooltip { transition: opacity 0.12s ease; transform: none !important; }
}

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-medium); border-radius: 999px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }

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

#app {
  display: flex;
  height: 100vh;
}

.sidebar {
  width: 260px;
  flex-shrink: 0;
  background: var(--bg-secondary);
  border-right: 1px solid var(--border-subtle);
  display: flex;
  flex-direction: column;
  overflow: hidden; /* the sidebar itself never scrolls — only .sidebar-scroll does */
}

/* Pinned top: brand + the single most important action (new companion).
   Never scrolls away, matching Claude/ChatGPT's top-pinned "new chat". */
.sidebar-header {
  flex-shrink: 0;
  padding: 16px 16px 8px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* The actual fix for "can't reach things at the bottom": companions/groups
   lists get their OWN scroll region instead of sharing one with the
   pinned header/footer buttons. min-height:0 is required — a flex child
   doesn't shrink below its content size (and so never triggers overflow)
   without it. The mask fades both edges as a scroll-affordance so more
   content below is visible without needing to hover for the scrollbar. */
.sidebar-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 4px 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, black 12px, black calc(100% - 12px), transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, black 12px, black calc(100% - 12px), transparent 100%);
}

/* Pinned bottom: Settings + theme, always reachable regardless of list length. */
.sidebar-footer {
  flex-shrink: 0;
  padding: 8px 16px 16px;
  border-top: 1px solid var(--border-subtle);
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.sidebar-brand-row { display: flex; align-items: center; justify-content: space-between; }
.sidebar-brand {
  display: flex;
  align-items: center;
  gap: 8px;
}
.sidebar-brand img { width: 20px; height: 20px; border-radius: 4px; }

/* Drawer close (X) — only meaningful once the mobile breakpoint turns the
   sidebar into an overlay; see that media query below for the flip to
   display:flex. Docked desktop sidebar never shows it. */
.sidebar-close-btn { display: none; }

/* Logo hover liveliness (Daniel: "animate it slightly on hover... only
   subtle") — a small tilt+scale on the existing favicon.svg <img>, no
   markup changes needed since transform/transition work on an <img> the
   same regardless of its source being an SVG. Kept to a single shared
   class so both places the mark appears (sidebar brand, empty-state)
   animate identically. */
.ensoul-mark { transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); }
.sidebar-brand:hover .ensoul-mark, .ensoul-mark:hover { transform: rotate(12deg) scale(1.08); }
@media (prefers-reduced-motion: reduce) {
  .ensoul-mark { transition: none; }
  .sidebar-brand:hover .ensoul-mark, .ensoul-mark:hover { transform: none; }
}
.sidebar-brand span {
  font-size: 14px;
  font-weight: 600;
  color: var(--text-primary);
  letter-spacing: 0.02em;
}

.sidebar h1 {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 4px;
}

.vessel-list { display: flex; flex-direction: column; gap: 4px; }

.vessel-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: var(--radius);
  cursor: pointer;
  color: var(--text-secondary);
  border: 1px solid transparent;
  font-family: var(--font-ui);
}
.vessel-item:hover { background: var(--bg-tertiary); color: var(--text-primary); }
.vessel-item.active { background: var(--accent-subtle); color: var(--text-primary); border-color: var(--accent); }
.vessel-item-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.vessel-avatar {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  font-weight: 600;
  color: #fff;
}

/* Theme toggle (UI polish: "make it an icon too... a bit cleaner than
   text") — an icon-btn showing the *current* theme's own icon (monitor/
   sun/moon) rather than the literal word "system"/"light"/"dark", so
   clicking through the three states reads as switching an icon instead
   of re-reading a label each time. Just adds a resting border on top of
   icon-btn's normal transparent one — everything else (size, hover,
   focus) is icon-btn's shared styling. */
.theme-toggle {
  align-self: flex-start;
  border-color: var(--border-subtle);
}

.main {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* Narrow-viewport nav bar (mobile pass) — the one consistent way back to
   the vessel/group drawer from every screen (chat, settings, gallery,
   memories, group picker…), so a hamburger doesn't need patching into
   each screen's own header separately. display:none here, flipped to
   flex under the mobile breakpoint below — desktop never renders it. */
.mobile-topbar {
  display: none;
  flex-shrink: 0;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border-subtle);
  background: var(--bg-primary);
}
.mobile-topbar-title {
  font-size: 14px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Mobile web → native app nudge (see app.js's renderMobileAppNudge) —
   same display:none-until-the-breakpoint gating as .mobile-topbar. */
.mobile-app-nudge {
  display: none;
  flex-shrink: 0;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 14px;
  background: var(--accent-subtle);
  border-bottom: 1px solid var(--border-subtle);
}
.mobile-app-nudge p { margin: 0; font-size: 12.5px; color: var(--text-secondary); }
.mobile-app-nudge-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
.mobile-app-nudge-link {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
  white-space: nowrap;
}
.mobile-app-nudge-link:hover { text-decoration: underline; }

/* Drawer scrim — dims/blocks the content behind the mobile sidebar while
   it's open, and closes it on tap (see app.js's closeMobileSidebar).
   display:none outside the mobile breakpoint as a defensive backstop:
   mobileSidebarOpen can't meaningfully become true on desktop, but this
   guarantees the scrim can never visually block a desktop layout either. */
.sidebar-backdrop { display: none; }

/* --- Chat --- */

/* The wrapper renderChat()/renderGroupChat() actually return. Must itself
   be a flex column filling .main's height — without this, it's a plain
   block <div> that .chat-messages's flex:1/overflow-y:auto rules can't
   reach (those only work when the direct parent is a flex container), so
   the wrapper just grows to fit all messages and gets clipped by body's
   overflow:hidden with nothing able to scroll it back into view. This is
   the actual root cause of the "long conversation has no scrollbar" bug —
   the earlier min-height:0 fix on .chat-messages was correct but inert
   without this. */
.chat-screen {
  flex: 1;
  min-height: 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.chat-header {
  padding: 16px 24px;
  border-bottom: 1px solid var(--border-subtle);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.chat-header h2 { font-size: 16px; font-weight: 600; }
.chat-header-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }

/* Wraps .chat-messages so the jump-to-bottom button below can be
   positioned over just the scrollable viewport, not the whole .chat-screen
   column (which also contains the composer). Needs its own flex:1/
   min-height:0 — those properties on .chat-messages itself only take
   effect when its DIRECT parent is a flex container (the same lesson
   .chat-screen's own fix already applied one level up). */
.chat-messages-wrap {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.chat-messages {
  flex: 1;
  min-height: 0; /* the sidebar-scroll fix, applied here too — see that rule's comment: a flex child in a column layout won't shrink below its content size (and so never triggers its own overflow) without this. Long conversations were growing .main past the viewport with nothing to scroll it back into view. */
  overflow-y: auto;
  padding: 24px 24px 32px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  font-family: var(--font-body);
}

/* UI beautification pass: a decorative scrim, not a content mask — the
   transcript visually fades into the composer's dock instead of the
   abrupt hard-edge cutoff a message card's own shadow otherwise leaves
   right above the input pill. Fixed in place at the bottom of the
   scrollable viewport (not tied to scroll position, unlike an
   opacity/mask-image approach), sitting above the messages but below the
   composer, pointer-events:none so it never blocks clicks on content
   underneath. */
.chat-messages-wrap::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 28px;
  background: linear-gradient(to bottom, transparent, var(--bg-primary));
  pointer-events: none;
}

/* Best-in-class history pass: "jump to latest" — the same floating button
   Claude/ChatGPT show once you've scrolled away from the bottom of a
   conversation. Hidden by default; .visible is toggled by a scroll
   listener (see wrapScrollableMessages), not this file. */
.jump-to-bottom-btn {
  position: absolute;
  bottom: 16px;
  left: 50%;
  transform: translateX(-50%) translateY(6px);
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--bg-elevated);
  border: 1px solid var(--border-light);
  box-shadow: var(--shadow-md);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s ease, transform 0.15s ease;
  z-index: 5;
}
.jump-to-bottom-btn.visible { opacity: 1; pointer-events: auto; transform: translateX(-50%) translateY(0); }
.jump-to-bottom-btn:hover { background: var(--bg-tertiary); }

.message { position: relative; max-width: 70%; padding: 12px 16px; border-radius: var(--radius); box-shadow: var(--shadow-sm); animation: message-in 0.22s ease-out; }
/* Extra bottom margin so a companion-reaction badge overlapping the
   corner (see .companion-reaction-badge-row) never gets clipped by the
   next message's top edge in a tight .chat-messages gap. */
.message.user:has(.companion-reaction-badge-row) { margin-bottom: 6px; }
.message.user { align-self: flex-end; background: var(--user-bubble); }
.message.assistant { align-self: flex-start; background: var(--assistant-bubble); border: 1px solid var(--border-subtle); }
.message .role { font-family: var(--font-ui); font-size: 11px; color: var(--text-muted); margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.05em; }

/* Best-in-class history pass: WhatsApp/iMessage-style date divider between
   messages — purely cosmetic, computed from already-loaded timestamps
   (see formatDividerLabel), never a storage boundary. */
.date-divider {
  align-self: center;
  font-family: var(--font-ui);
  font-size: 12px;
  color: var(--text-muted);
  background: var(--bg-tertiary);
  padding: 4px 12px;
  border-radius: var(--radius-pill);
}

/* Infinite-scroll-up loading row, shown at the top of .chat-messages while
   an older page is being fetched (see loadOlderHistory). */
.history-loading {
  align-self: center;
  font-size: 12px;
  padding: 4px 0;
}

/* --- Markdown message content --- */

/* --- Message meta row: timestamp + actions --- */

.message-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
  min-height: 18px;
}
.message-time {
  font-family: var(--font-ui);
  font-size: 11px;
  color: var(--text-muted);
}
.message-actions {
  display: flex;
  gap: 2px;
  opacity: 0;
  transition: opacity 0.12s;
}
.message:hover .message-actions,
.message:focus-within .message-actions { opacity: 1; }
.message-actions .icon-btn { width: 26px; height: 26px; }

.message-content { line-height: 1.6; }
.message-content > *:first-child { margin-top: 0; }
.message-content > *:last-child { margin-bottom: 0; }
.message-content p { margin: 0 0 0.6em; }
.message-content ul, .message-content ol { margin: 0 0 0.6em; padding-left: 1.4em; }
.message-content li { margin: 0.15em 0; }
.message-content blockquote { margin: 0 0 0.6em; padding-left: 0.8em; border-left: 2px solid var(--border-medium); color: var(--text-secondary); }
.message-content h1, .message-content h2, .message-content h3 { font-family: var(--font-ui); font-weight: 600; margin: 0.8em 0 0.4em; line-height: 1.3; }
.message-content h1 { font-size: 1.25em; }
.message-content h2 { font-size: 1.15em; }
.message-content h3 { font-size: 1.05em; }
.message-content a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.message-content hr { border: none; border-top: 1px solid var(--border-subtle); margin: 0.8em 0; }
.message-content img { max-width: 100%; border-radius: var(--radius-sm); }
.message-content code {
  font-family: 'SF Mono', ui-monospace, Menlo, Consolas, monospace;
  font-size: 0.88em;
  background: var(--surface-medium);
  padding: 0.15em 0.4em;
  border-radius: 4px;
}
.message-content pre {
  position: relative;
  margin: 0 0 0.6em;
  padding: 12px 14px;
  border-radius: var(--radius-sm);
  background: var(--surface-dark);
  border: 1px solid var(--border-subtle);
  overflow-x: auto;
}
.message-content pre code {
  background: none;
  padding: 0;
  font-size: 0.85em;
  line-height: 1.5;
  white-space: pre;
}
.code-copy-btn {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 26px;
  height: 26px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-ui);
  background: var(--surface-medium);
  border: 1px solid var(--border-light);
  border-radius: 6px;
  color: var(--text-secondary);
  opacity: 0;
  transition: opacity 0.12s;
}
.message-content pre:hover .code-copy-btn { opacity: 1; }
.code-copy-btn:hover { color: var(--text-primary); background: var(--surface-light); }

@keyframes message-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}

/* --- Typing indicator — shown in place of the reply while it's generating --- */

.typing-indicator {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 14px 16px;
}
.typing-indicator span {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--text-muted);
  animation: typing-bounce 1.1s infinite ease-in-out;
}
.typing-indicator span:nth-child(2) { animation-delay: 0.15s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.3s; }

@keyframes typing-bounce {
  0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
  30% { transform: translateY(-4px); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .message { animation: none; }
  .typing-indicator span { animation: none; opacity: 0.7; }
  .modal-box { animation: none; }
}

/* Rounded-pill composer (chat UI beautification pass) — one bordered
   container holding icon buttons, the textarea, and a circular send/stop
   button, matching Claude/ChatGPT's composer instead of a bare row of
   boxy buttons + a plain textarea. */
.chat-input-row {
  position: relative; /* stacks above .chat-messages-wrap::after's fade scrim, which shares this bottom-of-column area */
  z-index: 1;
  display: flex;
  align-items: flex-end;
  gap: 4px;
  margin: 0 24px 16px;
  padding: 6px 6px 6px 10px;
  border: 1px solid var(--border-light);
  border-radius: var(--radius-sheet);
  background: var(--bg-tertiary);
  box-shadow: var(--shadow-sm); /* gives the composer the same "floating" weight a message card's shadow already has, instead of sitting visually flatter than the content above it */
  transition: border-color 0.15s, box-shadow 0.15s;
}
.chat-input-row:focus-within { border-color: var(--accent); box-shadow: var(--shadow-md); }
.chat-input-row textarea {
  resize: none;
  min-height: 40px;
  max-height: 160px;
  font-family: var(--font-body);
  background: none;
  border: none;
  padding: 9px 6px;
}
.chat-input-row textarea:focus { outline: none; border: none; }

.icon-btn.primary { background: var(--accent); color: var(--bg-primary); border-color: var(--accent); }
.icon-btn.primary:hover { opacity: 0.9; background: var(--accent); }
.icon-btn.primary:disabled { opacity: 0.4; }

.composer-send-btn {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  flex-shrink: 0;
}
.composer-send-btn:has(span) { width: auto; border-radius: var(--radius-pill); padding: 0 16px 0 14px; }

/* --- Image attachments (chat UI polish pass, plan §6) --- */

.image-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 0 24px 8px;
}
.message .image-chips { padding: 0 0 8px; }
.image-chip {
  position: relative;
  width: 64px;
  height: 64px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  border: 1px solid var(--border-subtle);
}
.image-chip img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* A sent/generated image displayed in the message flow (chat UI
   beautification pass, follow-up) — a real, ChatGPT/Claude-sized image
   instead of a tiny 64px attachment thumbnail. The small square chip
   above stays for the composer's pending-attachment preview strip. */
.image-chip-large {
  width: auto;
  height: auto;
  cursor: pointer;
  transition: opacity 0.15s;
}
.image-chip-large:hover { opacity: 0.9; }
.image-chip-large img {
  /* All the real sizing lives on the <img> itself — width:100% on a
     width:auto container is circular (nothing to be 100% of), which is
     why this rendered as a tiny broken-image-sized box before. Letting
     the image size itself (capped, aspect-ratio preserved) and having
     the chip div just shrink-wrap around it sidesteps that entirely. */
  display: block;
  width: auto;
  height: auto;
  max-width: 320px;
  max-height: 320px;
  object-fit: contain;
}

.image-chip-expired {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-size: 10px;
  color: var(--text-muted);
  background: var(--surface-medium);
  padding: 4px;
}
.image-chip-remove {
  position: absolute;
  top: 2px;
  right: 2px;
  width: 18px;
  height: 18px;
  line-height: 16px;
  padding: 0;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  border: none;
  font-size: 13px;
  cursor: pointer;
}

/* --- Image gallery (plan §9) --- */

.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 10px;
  margin-top: 16px;
}
.gallery-tile {
  aspect-ratio: 1;
  border-radius: var(--radius-sm);
  overflow: hidden;
  border: 1px solid var(--border-subtle);
  cursor: pointer;
}
.gallery-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }

.gallery-fullscreen-box {
  background: var(--bg-elevated);
  border: 1px solid var(--border-light);
  border-radius: var(--radius-sheet);
  box-shadow: var(--shadow-lg);
  padding: 20px;
  max-width: min(90vw, 800px);
  width: 90%;
  max-height: 90vh;
  overflow-y: auto;
}
.gallery-fullscreen-image-wrap {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 200px;
}
.gallery-fullscreen-image-wrap img { max-width: 100%; max-height: 70vh; object-fit: contain; border-radius: var(--radius-sm); }

/* Prev/next controls for the browsable fullscreen viewer (ChatGPT-style
   in-chat image click), floating over the image rather than in the
   actions row so they read as navigation, not a form action. */
.gallery-nav-btn {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(0, 0, 0, 0.5);
  color: #fff;
  border-radius: 50%;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.gallery-nav-btn:hover { background: rgba(0, 0, 0, 0.7); }
.gallery-nav-prev { left: 8px; }
.gallery-nav-next { right: 8px; }

/* --- In-conversation search (plan §10) --- */

.search-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 24px;
  border-bottom: 1px solid var(--border-subtle);
  background: var(--bg-secondary);
}
.search-bar input {
  flex: 1;
  min-width: 0;
}
.search-count {
  font-size: 12px;
  color: var(--text-muted);
  white-space: nowrap;
  min-width: 40px;
  text-align: center;
}
.search-highlight {
  background: var(--accent-subtle);
  color: inherit;
  border-radius: 2px;
}
.search-highlight-current {
  background: var(--accent);
  color: var(--bg-primary);
}

/* --- Heartbeat banner (Phase 7, §9.4) --- */

.heartbeat-banner {
  border: 1px solid var(--border-subtle);
  background: var(--bg-tertiary);
  border-radius: var(--radius);
  padding: 12px 14px;
  margin: 12px 24px 0;
}
.heartbeat-banner p { margin-bottom: 8px; }
.heartbeat-banner p:last-of-type { margin-bottom: 10px; font-family: var(--font-body); }

/* --- Onboarding hint (empty state / create-vessel, when no API key yet) --- */

.setup-hint {
  max-width: 440px;
  background: var(--accent-subtle);
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  color: var(--text-secondary);
}
.setup-hint p { margin: 0; }
.setup-hint button { flex-shrink: 0; }

/* --- Companion creation loading bar (replaces a plain status line) --- */

.import-progress {
  width: 100%;
  max-width: 480px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}
.import-progress-track {
  width: 100%;
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--bg-tertiary);
  overflow: hidden;
}
.import-progress-fill {
  position: relative;
  height: 100%;
  border-radius: var(--radius-pill);
  background: var(--accent);
  transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
  overflow: hidden;
}
.import-progress-fill::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
  animation: import-shimmer 1.3s infinite;
}
.import-progress-label { font-size: 13px; color: var(--text-secondary); margin: 0; }

@keyframes import-shimmer {
  from { transform: translateX(-100%); }
  to { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) {
  .import-progress-fill::after { animation: none; }
}

/* --- Empty / centered states --- */

.empty-state {
  flex: 1;
  min-height: 0; /* same flex/overflow fix as .chat-messages — the create-vessel screen's content (hint box, textarea, progress bar, buttons) can exceed a short window's height. */
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  color: var(--text-secondary);
  padding: 24px;
  text-align: center;
}
.empty-state textarea { max-width: 480px; min-height: 160px; }
.empty-state img.mark { width: 40px; height: 40px; border-radius: 8px; opacity: 0.9; }

/* Top-anchored, scrollable screens (Settings, Gallery, Moments, Memory
   review, Group picker) — distinct from .empty-state, which is a
   centered "nothing here yet" layout and is semantically wrong for a
   screen that has real, populated content. */
.panel-screen {
  flex: 1;
  min-height: 0; /* same flex/overflow fix as .chat-messages and .sidebar-scroll — required for long content (many keys, many memories, a big gallery) to actually scroll instead of growing past the viewport with nothing to bring it back into view. */
  overflow-y: auto;
  padding: 24px;
}
.panel-screen h2 { font-size: 18px; font-weight: 600; margin-bottom: 20px; }

.settings-screen { max-width: 760px; }

/* Left-nav sectioned settings (Claude.ai/ChatGPT pattern) — replaces the
   old single flat scroll of stacked cards. Each nav item shows one tab's
   content at a time in .settings-content; .settings-nav collapses to a
   horizontal scrollable pill row below ~640px since there's no room for a
   side column next to the sidebar on narrow viewports. */
.settings-layout { display: flex; gap: 24px; align-items: flex-start; }
.settings-nav { flex-shrink: 0; width: 180px; display: flex; flex-direction: column; gap: 2px; }
.settings-nav-item {
  text-align: left;
  background: transparent;
  border: none;
  border-radius: var(--radius);
  padding: 8px 12px;
  font-size: 14px;
  color: var(--text-secondary);
}
.settings-nav-item:hover { background: var(--bg-secondary); color: var(--text-primary); }
.settings-nav-item.active { background: var(--bg-tertiary); color: var(--text-primary); font-weight: 500; }
.settings-content { flex: 1; min-width: 0; }
.settings-save-row { display: flex; align-items: center; gap: 12px; margin-top: 4px; }
.settings-saved-note { font-size: 13px; }
@media (max-width: 640px) {
  .settings-layout { flex-direction: column; }
  .settings-nav { width: 100%; flex-direction: row; overflow-x: auto; gap: 6px; padding-bottom: 4px; }
  .settings-nav-item { flex-shrink: 0; }
}

.settings-section {
  background: var(--bg-secondary);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius);
  padding: 16px;
  margin-bottom: 16px;
}
.settings-section-title {
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-secondary);
  margin-bottom: 12px;
}
.settings-section > * + * { margin-top: 10px; }
.settings-checkbox-row { display: flex; align-items: center; gap: 8px; }
.settings-accent-row { display: flex; align-items: center; gap: 8px; }

/* Per-companion generation settings (best-in-class settings pass) — one
   "checkbox enables an input" row per overridable param (see
   buildOverrideSlider/buildOverrideNumber in app.js). Rows within one
   .settings-section already get the `> * + *` spacing above; this just
   adds a bit more separation between whole override blocks and a subtle
   divider so a multi-row Advanced section doesn't read as one wall. */
.override-row { padding-top: 10px; border-top: 1px solid var(--border-subtle); }
.override-row:first-child { padding-top: 0; border-top: none; }
.settings-slider-row { display: flex; align-items: center; gap: 10px; }
.settings-slider-row input[type="range"] { flex: 1; }
.settings-slider-value { min-width: 34px; text-align: right; font-variant-numeric: tabular-nums; }

/* Consistent back-chevron + title row for drill-down screens (Gallery,
   Moments, Memory review) — replaces each screen's own stacked
   "← Back to chat" text button + heading. */
.screen-header { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
.screen-header h3 { font-size: 18px; font-weight: 600; }

/* Shared card treatment for Memory review's memory/moment/thread items and
   the Saved Moments screen's cards — was three copies of the same inline
   border/radius/padding, now one class. */
.review-item-card {
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius);
  padding: 12px 14px;
  margin: 12px 0;
}
.review-item-card.disabled { opacity: 0.5; }
.moment-note { font-style: italic; color: var(--text-secondary); }

.group-picker-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius);
  cursor: pointer;
}
.group-picker-row:hover { background: var(--bg-tertiary); }

.error-banner {
  background: rgba(200, 80, 80, 0.12);
  border: 1px solid rgba(200, 80, 80, 0.35);
  color: #a83c3c;
  padding: 10px 14px;
  border-radius: var(--radius);
  margin: 12px 24px 0;
  font-size: 13px;
}
:root[data-theme="dark"] .error-banner { color: #ffb3b3; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .error-banner { color: #ffb3b3; }
}

.muted { color: var(--text-muted); font-size: 13px; }

/* Coming-soon provider gating (Aug 2026: "hide all apis bar openrouter
   behind coming soon... unlock them in the ui as we go") — every locked
   provider collapses into one compact wrapped row of name-only chips
   below the active (OpenRouter) row, instead of repeating a full
   label+hint+disabled-input block per provider (Daniel: "make the other
   model section smaller and cleaner... reads pretty duplicative"). */
.provider-label-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.coming-soon-intro { margin-top: 16px; }
.coming-soon-chip-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.coming-soon-badge {
  font-size: 12px;
  font-weight: 500;
  color: var(--text-muted);
  background: var(--bg-tertiary);
  border: 1px solid var(--border-light);
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  white-space: nowrap;
}

/* --- Group conversation (§8.2) --- */

.group-participants {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 12px 24px;
  border-bottom: 1px solid var(--border-subtle);
}

.participant-chip {
  padding: 4px 10px;
  font-size: 12px;
  border-radius: 999px;
  background: var(--surface-medium);
  border: 1px solid var(--border-light);
}
.participant-chip.muted { opacity: 0.5; text-decoration: line-through; }

/* --- Confirm modal (replaces window.confirm() — see app.js's requestConfirm) --- */

.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
}

.modal-box {
  background: var(--bg-elevated);
  border: 1px solid var(--border-light);
  border-radius: var(--radius-sheet);
  box-shadow: var(--shadow-lg);
  padding: 20px;
  max-width: 400px;
  width: 90%;
  max-height: 85vh; /* a long confirm/note message shouldn't be able to grow past the viewport with no way to reach its buttons */
  overflow-y: auto;
  animation: modal-in 0.16s ease-out;
}
.modal-box p { color: var(--text-primary); }

.modal-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}
.modal-header p { margin: 0; flex: 1; }
.modal-header .icon-btn { flex-shrink: 0; margin-top: -4px; margin-right: -6px; }

.modal-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  margin-top: 16px;
}

@keyframes modal-in {
  from { opacity: 0; transform: translateY(8px) scale(0.98); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}

/* ================================================================
   Mobile pass — this app had never been touched for small screens
   before (#app is a bare two-column flex row, sizes and paddings all
   fixed px tuned for a desktop window). Rather than a full reflow,
   this turns the sidebar into an off-canvas drawer (toggled by
   .mobile-topbar's hamburger, see app.js's toggleMobileSidebar/
   renderMobileTopbar/renderSidebar) and trims fixed spacing that was
   eating too much of a ~375-430px viewport. Single breakpoint: below
   this, phones and small tablets in portrait; above it, the existing
   desktop layout is untouched.
   ================================================================ */
@media (max-width: 768px) {
  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    width: 84vw;
    max-width: 300px;
    height: 100vh;
    height: 100dvh;
    z-index: 200;
    transform: translateX(-100%);
    transition: transform 0.22s ease;
  }
  .sidebar.mobile-open { transform: translateX(0); box-shadow: var(--shadow-lg); }
  .sidebar-close-btn { display: inline-flex; }
  .sidebar-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 190;
    background: rgba(0, 0, 0, 0.45);
    animation: mobile-backdrop-in 0.15s ease-out;
  }
  .mobile-topbar { display: flex; }
  .mobile-app-nudge { display: flex; }

  /* Bigger touch targets — 34px icon buttons are below the ~40-44px
     comfortable tap-target size; only the icon-btn's own box grows, the
     glyph inside stays the same drawn size. */
  :root { --icon-btn-size: 40px; }

  /* iOS Safari auto-zooms the whole page on focusing any input under
     16px; the composer/search text inputs inherit body's 15px otherwise. */
  textarea, input[type="text"], input[type="password"] { font-size: 16px; }

  /* Fixed 24px horizontal gutters were tuned for a wide desktop window —
     on a 375-430px phone they eat a fifth of the usable width before any
     content starts. */
  .chat-header { padding: 12px 14px; }
  .chat-messages { padding: 14px 14px 24px; }
  .chat-input-row { margin: 0 10px 10px; }
  .image-chips { padding: 0 14px 8px; }
  .search-bar { padding: 8px 14px; }
  .error-banner { margin: 10px 14px 0; }
  .heartbeat-banner { margin: 10px 14px 0; }
  .group-participants { padding: 10px 14px; }
  .panel-screen { padding: 16px; }

  /* 70% max-width was tuned against a desktop-wide .chat-messages column;
     on a narrow phone it forces short replies to wrap far earlier than
     the available width needs. */
  .message { max-width: 90%; }

  /* image-chip-large's img has a fixed 320px cap (see that rule's own
     comment on why it can't be width:100% of an auto-width container) —
     on a phone that alone can exceed a 90%-wide bubble's actual pixel
     width once its own padding/border are subtracted. */
  .image-chip-large img { max-width: min(320px, calc(100vw - 96px)); max-height: min(320px, calc(100vw - 96px)); }
}

@keyframes mobile-backdrop-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .sidebar { transition: none; }
  .sidebar-backdrop { animation: none; }
}
