/* ============================================================
   GLOBAL STYLES
   ============================================================ */

/* ============================================================
   THEME VARIABLES
   ============================================================ */

:root {
  /* DARK (default) — neutral charcoal theme */
  --bg-main: #131314;
  --bg-panel: #0d0d0d;
  --bg-bubble-bot: #131314; /* #282a2c; */
  --bg-bubble-user: #282a2c;
  --bg-input: rgba(19, 19, 20, 0.85);
  --bg-input-focus: #1e1f20;

  --text-main: #f2f2f2;
  --text-soft: #8f8f95;
  --text-bot: #e8e8e8;

  --border-main: #1e1f20;
  --accent: #bfbfbf; /* neutral accent */
  --accent_send: #282a2c;
}

/* LIGHT MODE */
body.light {
  --bg-main: #f6f7fb;
  --bg-panel: #ffffff;
  --bg-bubble-bot: #f6f7fb; /* #eef2ff; */
  --bg-bubble-user: #6366f1;
  --bg-input: rgba(246, 247, 251, 0.85);
  --bg-input-focus: #d1d5db;

  --text-main: #111827;
  --text-soft: #6b7280;
  --text-bot: #1f2937;

  --border-main: #d1d5db;
  --accent: #6366f1;
  --accent_send: #6366f1;
}

::selection {
  background-color: var(--accent);
  color: var(--bg-main);
}

/* Base layer order */
#titleBar {
  position: absolute;
  z-index: 30;
}

.input-bar {
  position: relative;
  z-index: 10;
  overflow: visible;
}

#chatBox {
  position: relative;
  z-index: 1;
}

/* Title dropdown (header menu) */
#dropdownMenu {
  position: absolute;
  z-index: 31;
}

/* Profile menu */
#profileMenu {
  position: absolute;
  z-index: 100;
}

/* ONLY raise input bar while profile menu is open */
.input-bar:has(#profileMenu.open) {
  z-index: 1000;
}

body.light .header h1 {
  font-weight: 700;
}

@font-face {
  font-family: "Haiku";
  src: url("../fonts/inter.ttf") format("truetype");
  font-weight: 200 900;
}

@font-face {
  font-family: "Noto Sans";
  src: url("../fonts/noto_sans.ttf") format("truetype");
}

@font-face {
  font-family: "Optima";
  src: url("../fonts/optima.woff") format("woff");
  font-weight: 200 900;
}

@font-face {
  font-family: "Gill Sans";
  src: url("../fonts/gill_sans.otf") format("opentype");
  font-weight: 200 900;
}

@font-face {
  font-family: "Primor";
  src: url("../fonts/primor.otf") format("opentype");
  font-weight: 200 900;
}

@font-face {
  font-family: "Cormorant";
  src: url("../fonts/cormorant.otf") format("opentype");
}

@font-face {
  font-family: "mplus";
  src: url("../fonts/mplus.ttf") format("truetype");
}

@font-face {
  font-family: "robotoserif";
  src: url("../fonts/robotoserif.ttf") format("truetype");
}

/* OpenDyslexic (SIL OFL 1.1). Regular and Bold are registered as separate
   faces rather than one file with a synthetic range: the browser would
   otherwise fake bold by smearing the regular outlines, which is
   especially bad here because the whole point of the face is its weighted
   bottoms and distinct letterform shapes.
   font-display:swap keeps replies readable while it loads. */
@font-face {
  font-family: "OpenDyslexic";
  src: url("../fonts/OpenDyslexic.otf") format("opentype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "OpenDyslexic";
  src: url("../fonts/OpenDyslexic-Bold.otf") format("opentype");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

body {
  opacity: 0;
  filter: blur(6px);
  transition: opacity 0.9s ease, filter 1s ease;
  margin: 0;
  padding: 0;
  background: var(--bg-main);
  color: var(--text-main);
  font-family: "Haiku", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
  display: flex;
  flex-direction: column;
  height: 100dvh;
  overflow: hidden;
  user-select: none;
}

body.loaded {
  opacity: 1;
  filter: blur(0px);
}

.app {
  display: flex;
  flex-direction: column;
  height: 100%;
  width: 100%;
  max-width: 860px;
  margin: 0 auto;
}

/* ============================================================
   HEADER + TITLE + DROPDOWN TOGGLE
   ============================================================ */

.header {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 10px;
  padding: 10px 0 18px 0;
  user-select: none;
  z-index: 999;
}

.header h1 {
  margin: 0;
  font-size: 32px;
  font-weight: 800;
  color: var(--text-main);
  letter-spacing: -0.5px;
}

.haiku {
  font-weight: 800;
}
.mini {
  font-weight: 300;
  margin-left: 6px;
  opacity: 0.9;
}

/* Title animation positions */
#titleBar {
  position: absolute;
  top: 38%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 100%;
  text-align: center;
  transition: all 0.6s ease;
}

#titleBar.active {
  position: relative;
  top: 10px;
  left: 0;
  transform: none;
  margin-bottom: 0px;
    /* this is what actually blurs content behind it */
  background: color-mix(in srgb, var(--bg-main) 72%, transparent);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);

  /* soft same-color shadow */
  box-shadow: 0 10px 22px color-mix(in srgb, var(--bg-main) 85%, transparent);
}

/* ============================================================
   CHAT WINDOW
   ============================================================ */

.chat-box {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 10px 4px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s ease, transform 0.5s ease;
  transform: translateY(20px);
}

.chat-box.padded {
  padding-bottom: 120px !important;
  padding-top: 50px;
}

.chat-box::-webkit-scrollbar {
  width: 0px;
  background: transparent;
}

.sidebar-list::-webkit-scrollbar {
  width: 0px;
  background: transparent;
}

#userInput::-webkit-scrollbar {
  width: 0px;
  background: transparent;
}

.chat-box {
  scrollbar-width: none;
  -ms-overflow-style: none;
}

/* Blur everything inside chat when dropdown is open */
.chat-blurred {
  filter: blur(6px);
  opacity: 1;
  pointer-events: none;
  transition: filter 0.35s ease, opacity 0.35s ease;
}

.status-toast {
    display: flex;
    align-items: center;
    position: fixed;
    top: 16px;
    right: 16px;
    background: #ffcccc;
    color: #111827;
    border: 1px solid #ff4d4d55;
    border-radius: 10px;
    padding: 12px 18px;
    font-size: 0.85rem;
    max-width: 300px;
    z-index: 9999;
    opacity: 0;
    transform: translateY(-10px);
    pointer-events: none;
    transition: opacity 0.3s ease, transform 0.3s ease;
}

.status-toast.visible {
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
    top: 32px !important;
    right: 16px !important;
    backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

/* ============================================================
   STATUS TOAST VARIANTS
   ============================================================ */

#statusToast.error {
  color: #ff4d4d;
}

#statusToast.warning {
  color: #f5c84b;
  border-color: color-mix(in srgb, #f5c84b 38%, transparent);
  background:
    linear-gradient(
      135deg,
      color-mix(in srgb, #f5c84b 14%, transparent),
      color-mix(in srgb, var(--bg-panel) 96%, transparent)
    );
  box-shadow:
    0 12px 34px rgba(0, 0, 0, 0.32),
    0 0 0 1px color-mix(in srgb, #f5c84b 18%, transparent);
}

body.light #statusToast.warning {
  color: #9a6a00;
  border-color: rgba(180, 126, 0, 0.32);
  background:
    linear-gradient(
      135deg,
      rgba(245, 200, 75, 0.18),
      rgba(255, 255, 255, 0.96)
    );
  box-shadow:
    0 12px 30px rgba(120, 86, 0, 0.12),
    0 0 0 1px rgba(180, 126, 0, 0.12);
}

/* ============================================================
   BOT AVATAR MORPH
   ============================================================ */

.bot-avatar {
  transition: opacity 0.45s ease, filter 0.45s ease;
}

.bot-avatar.morphing {
  opacity: 0;
  filter: blur(6px);
}

/* ============================================================
   INPUT BAR
   ============================================================ */

.input-bar {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  width: 95%;
  max-width: 800px;
  display: flex;
  align-items: center;
  gap: 6px;
  top: 50%;
  transition: top 0.6s ease, transform 0.6s ease;
}

.input-bar.active {
  top: calc(100% - 90px);
  transform: translateX(-50%);
}

#userInput {
  font-family: Arial !important;
  letter-spacing: 0.03em;
  flex: 1;
  padding: 14px 20px;
  background: var(--bg-input);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  scrollbar-width: none;
  color: white;
  border: 1px solid;
  border-color: var(--border-main);
  border-radius: 30px;
  outline: none;
  font-size: 15px;
  color: var(--text-main);
  transition: background 0.35s ease, border-color 0.35s ease,
    box-shadow 0.35s ease;
}

#userInput::placeholder {
  color: var(--text-main);
  opacity: 0.2;
}
#userInput:focus {
  background: var(--bg-input-focus);
}

.send-btn {
  width: 46px;
  height: 46px;
  border-radius: 50%;
  border: none;
  background: var(--accent_send);
  color: white;
  font-size: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: 0.15s;
}
.send-btn:hover {
  filter: brightness(1.15);
}
.send-btn:active {
  transform: scale(0.95);
}

body.light .send-btn svg path {
  fill: #ffffff !important;
  stroke: #ffffff !important;
}

/* ============================================================
   BUBBLES
   ============================================================ */

.msg-user,
.msg-bot,
.msg-error,
.msg-blocked {
  max-width: calc(100% - 100px);
  word-wrap: break-word;
  overflow-wrap: break-word;
  padding: 12px 16px;
  border-radius: 16px;
  margin: 0 10px;
  letter-spacing: -.025rem;
}

.msg-user {
  align-self: flex-end;
  background: var(--bg-bubble-user);
  color: #ffffff;
  border-bottom-right-radius: 4px;
  transition: background 0.35s ease;
}

.msg-bot {
  align-self: flex-start;
  background: transparent;
  color: var(--text-bot);
  transition: background 0.35s ease;
  /* border-bottom-left-radius:4px; */
}

.msg-error {
  align-self: flex-start;
  background: var(--bg-bubble-bot);
  color: var(--text-soft);
  /* border-bottom-left-radius:4px; */
}

.msg-blocked {
  align-self: flex-end;
  background: rgba(209, 209, 209, 0.35);
  color: white;
  border-bottom-right-radius: 4px;
  opacity: 0.25 !important;
  filter: blur(0.3px);
}

.thinking-row {
  margin-top: 20px;
}

.thinking-msg {
  align-self: flex-start;
  background: #1b2738;
  padding: 5px 15px;
  border-radius: 16px;
  max-width: 80%;
  display: flex;
  align-items: center;
  margin: 0 10px;
}

/* "Thinking…" status label. Materializes from blur, then a pale
   highlight phases across the text.

   Earlier versions ran a cyan-tinted blurred duplicate of the label
   through a moving mask, on top of a three-tone brand halo. Two
   stacked effects on a status line meant the busiest thing on screen
   was the part that says nothing. This is one moving highlight in the
   text's own colour, using the same background-clip technique as the
   generating reply so the two read as the same idea. */
.deep-think-label {
  position: relative;
  font-size: 15px;
  font-weight: 400;
  letter-spacing: 0.02em;
  align-self: center;
  margin-left: 15px;

  background: linear-gradient(
    100deg,
    var(--text-soft) 0%,
    var(--text-soft) 40%,
    var(--text-main) 50%,
    var(--text-soft) 60%,
    var(--text-soft) 100%
  );
  background-size: 260% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;

  opacity: 0;
  filter: blur(1px);
  animation:
    label-materialize 0.42s ease-out forwards,
    text-phase 2.8s linear infinite;
  will-change: opacity, filter, background-position;
}

/* The duplicate-text sweep layer is gone; the gradient above does the
   whole job. Neutralised rather than deleted so any leftover
   data-text attribute cannot render a stray copy of the label. */
.deep-think-label::after {
  content: none;
}

@keyframes text-phase {
  from { background-position: 130% 0; }
  to   { background-position: -130% 0; }
}

@keyframes label-materialize {
  from { opacity: 0; filter: blur(8px); }
  to   { opacity: 1; filter: blur(0);   }
}

.chat-link {
  color: var(--accent);
  text-decoration: underline;
  word-break: break-word;
}

.chat-link:hover {
  opacity: 0.85;
}

/* ============================================================
   BOT MESSAGE ACTIONS (icon row + 3-dot retry menu)
   ============================================================ */

.bot-message-wrap {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  margin-top: 20px;
  margin-bottom: 30px;
}

.bot-actions {
  margin-left: 75px; /* line up under bot bubble after avatar */
  margin-top: 4px;
  margin-bottom: 8px;
  display: flex;
  align-items: center;
  gap: 2px;
  position: relative;
  width: max-content;
  transition: opacity 0.35s ease-out;
}

/* Hidden while the bot is still streaming its reply. JS adds this
   class when the bubble is first appended in generating mode, and
   removes it after the typewriter finishes — at which point the
   transition above fades the buttons in. */
.bot-actions.pending {
  opacity: 0;
  pointer-events: none;
}

/* Minimal icon buttons */
.bot-icon-btn {
  -webkit-appearance: none;
  appearance: none;
  border: none;
  background: transparent;
  color: var(--text-main);
  width: 28px;
  height: 28px;
  border-radius: 8px;
  padding: 0;
  margin: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  line-height: 1;
  box-shadow: none;
  outline: none;
  transition: background 120ms ease, color 120ms ease;
  opacity: 0.55 !important;
}

.bot-icon-btn:hover {
  background: rgba(127, 127, 127, 0.10);
  color: var(--text-main);
}

.bot-icon-btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.bot-icon-btn svg {
  width: 18px;
  height: 18px;
  display: block;
}

.bot-icon-btn.is-active {
  color: var(--accent);
  background: rgba(127, 127, 127, 0.10);
}

/* Horizontal 3-dot icon */
.bot-more-btn {
  gap: 2px;
}

.bot-more-btn .dot {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: currentColor;
  display: block;
}

/* Base hidden state */
.bot-feedback-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 195;
  opacity: 0;
  transform: translateY(-4px) scale(0.98);
  visibility: hidden;
  pointer-events: none;

  transition:
    opacity 160ms ease,
    transform 160ms ease,
    visibility 0s linear 160ms; /* hide after fade-out finishes */

  z-index: 50; /* helps it stay above nearby UI */
  will-change: opacity, transform;
}

/* Visible state */
.bot-feedback-menu.open {
  opacity: 1;
  transform: translateY(0) scale(1);
  visibility: visible;
  pointer-events: auto;

  transition:
    opacity 160ms ease,
    transform 160ms ease,
    visibility 0s linear 0s;
}

.bot-feedback-item {
  display: block;
  padding: 8px 10px;
  border-radius: 8px;
  color: var(--text-main);
  font-size: 13px;
  line-height: 1.2;
  cursor: pointer;
  user-select: none;
  background: transparent;
  white-space: nowrap;
}

.bot-feedback-item:hover {
  background: rgba(127, 127, 127, 0.12);
}

/* Retry is only active on newest bot message */
.bot-actions:not(.show-retry) .bot-retry-menu-item {
  opacity: 0.45;
  pointer-events: none;
}

/* Retry row only (icon + text) */
.bot-retry-menu-item {
  display: flex;          /* overrides .bot-feedback-item { display:block } */
  align-items: center;
  gap: 8px;
}

.bot-retry-menu-item svg {
  width: 14px;
  height: 14px;
  flex: 0 0 auto;
  display: block;
  color: currentColor;    /* inherits from .bot-feedback-item color */
}

.bot-retry-menu-item span {
  line-height: 1;
  pointer-events: none;   /* optional */
}

/* Error rows get visible retry button by default */
.bot-message-wrap[data-msg-role="error"] .bot-actions {
  opacity: 1;
}

.bot-message-wrap[data-msg-role="error"] .bot-retry-btn {
  opacity: 0.75 !important;
}

.bot-feedback-menu-inline {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 8px;
    width: max-content;
    min-width: unset;
    white-space: nowrap;
}

.bot-feedback-menu-inline .bot-feedback-item {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    width: auto;
    white-space: nowrap;
}

.bot-actions:not(.show-retry) .bot-retry-gated {
    opacity: 0.35;
    pointer-events: none;
    cursor: not-allowed;
}

.bot-actions.show-retry .bot-retry-gated {
    opacity: 1;
    pointer-events: auto;
    cursor: pointer;
}

/* Optional: hide non-retry controls on error rows if any slip in */
.bot-message-wrap[data-msg-role="error"] .bot-copy-btn,
.bot-message-wrap[data-msg-role="error"] .bot-up-btn,
.bot-message-wrap[data-msg-role="error"] .bot-down-btn,
.bot-message-wrap[data-msg-role="error"] .bot-more-btn,
.bot-message-wrap[data-msg-role="error"] .bot-feedback-menu {
  display: none !important;
}

/* ============================================================
   BOT GENERATING SHIMMER (during typewriter only)
   ============================================================ */

.msg-bot.generating {
  /* A pale sweep travelling through the text against the normal reply
     colour. The previous version ran a six-stop cyan/violet/pink gradient
     through every generating reply, which put the brightest thing on screen
     on text that is still forming and unreadable. This keeps the reply the
     colour it will settle at and moves a highlight across it instead. */
  background: linear-gradient(
    100deg,
    var(--text-main) 0%,
    var(--text-main) 38%,
    #ffffff 50%,
    var(--text-main) 62%,
    var(--text-main) 100%
  );
  background-size: 260% 100%;
  animation: text-phase 2.4s linear infinite;

  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;

  border-radius: 16px;
}

.msg-bot.generating.deep {
  animation-duration: 4s;
}

/* ============================================================
   NEURAL BACKGROUND
   ============================================================ */

#neuralWrapper {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
}

#neuralBG {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  z-index: -1;
  opacity: 1;
  transition: opacity 1s ease, transform 2.2s ease;
  pointer-events: none;
}

/* ============================================================
   DISCLAIMER
   ============================================================ */

#disclaimer {
  display: flex;
  justify-content: center;
  position: absolute;
  bottom: 30px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 13px;
  color: var(--text-main) !important;
  opacity: 0.6;
  pointer-events: none;
  transition: opacity 0.6s ease;
  min-width: 330px;
  max-width: min(92vw, 640px);
  width: max-content;
}

#disclaimer.hidden {
  opacity: 0;
}

/* ============================================================
   DROPDOWN + SESSIONS PANEL
   ============================================================ */

.dropdown-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  margin-left: 0px;
  user-select: none;
  transition: transform 0.25s ease;
}
.dropdown-toggle.open {
  transform: rotate(180deg);
}

.dropdown-toggle svg path {
  stroke: var(--text-main);
}

.dropdown-menu {
  position: absolute;
  top: 60px;
  background: var(--bg-panel);
  border: 1px solid;
  border-color: var(--border-main);
  border-radius: 10px;
  padding: 6px 0;
  width: 180px;
  opacity: 0;
  transform: translateY(-10px);
  pointer-events: none;
  transition: opacity 0.25s ease, transform 0.25s ease;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
  z-index: 50;
}

.dropdown-menu.open {
  opacity: 1;
  transform: translateY(0px);
  pointer-events: auto;
  z-index: 9999999;
}

.dropdown-item {
  padding: 10px 18px;
  color: var(--text-main);
  cursor: pointer;
  font-size: 14px;
  transition: background 0.15s ease;
}
.dropdown-item:hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

/* Prevent text highlighting */
.dropdown-toggle,
.dropdown-menu,
.dropdown-item {
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}

.theme-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
}

.theme-toggle svg {
  color: var(--text-main);
  opacity: 0.9;
}

.theme-toggle:hover svg {
  opacity: 1;
}

/* Inline icon toggles row inside dropdown */
.dropdown-inline-row {
  display: flex;
  gap: 8px;
  padding: 6px 8px;
  justify-content: center;
}

.dropdown-inline-row .dropdown-item.icon-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 40px;
  height: 36px;
  padding: 0 10px;
  border-radius: 10px;
}

/* Optional subtle state dim when off */
#toggleTTS.is-off {
  opacity: 0.65;
}

/* ============================================================
   PROFILE TOGGLE (⋮)
   ============================================================ */

.profile-toggle {
  width: 18px; /* smaller footprint */
  height: 100%; /* match input height */
  display: flex;
  flex-direction: column;
  justify-content: center; /* vertical centering */
  align-items: center;
  gap: 3px;
  cursor: pointer;
  user-select: none;
  margin-left: 2px; /* subtle separation from edge */
}

.profile-toggle .dot {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--text-main);
  opacity: 0.75;
}

/* ============================================================
   PROFILE MENU (indexed to input bar)
   ============================================================ */

#profileMenu { z-index: 9999; }

.profile-menu {
  position: absolute;
  bottom: 70px;
  left: 0;
  background: var(--bg-panel);
  border: 1px solid var(--border-main);
  border-radius: 10px;
  width: 200px;
  padding: 6px 0;
  opacity: 0;
  transform: translateY(10px);
  pointer-events: none;
  transition: opacity 0.25s ease, transform 0.25s ease;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
  z-index: 9999;
}

.profile-menu.open {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
  z-index: 9999;
}

.profile-item {
  padding: 10px 16px;
  font-size: 14px;
  color: var(--text-main);
  cursor: pointer;
  transition: background 0.15s ease;
}

.profile-item:hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

.profile-item.active {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  font-weight: 600;
}

.profile-item {
  display: flex;
  align-items: center;
  gap: 10px;
}

.profile-item svg {
  flex-shrink: 0;
  color: var(--text-main);
  opacity: 0.9;
}

.profile-item.active svg {
  color: var(--accent);
}

.profile-toggle.active .dot {
  background: var(--accent);
}

/* ============================
   SESSION PANEL
   ============================ */

.session-panel {
  position: absolute;
  top: 60px;
  left: 50%;
  transform: translateX(-50%) translateY(-10px); /* center alignment */
  width: 260px;
  background: var(--bg-panel);
  border: 1px solid;
  border-color: var(--border-main);
  border-radius: 10px;
  padding: 8px 0;
  display: none;
  opacity: 0;
  transition: opacity 0.25s ease, transform 0.25s ease; /* fade + slide */
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  z-index: 99999999999;
}

.session-panel.open {
  opacity: 1;
  transform: translateX(-50%) translateY(0px); /* slide down */
  pointer-events: auto;
}

.session-entry {
  padding: 10px 16px;
  color: var(--text-main);
  cursor: pointer;
  font-size: 14px;
  user-select: none;
}

.session-entry:hover {
  background: #1f2937;
}

.session-entry.active {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  font-weight: 600;
}

.session-preview {
  font-size: 12px;
  color: var(--text-soft);
  margin-top: 3px;
}

/* Two-line session layout */
.session-line {
  display: flex;
  flex-direction: column;
}

/* Long-press invisible overlay */
.session-delete-hint {
  font-size: 11px;
  color: #f87171;
  opacity: 0.8;
  margin-top: 2px;
}

.session-clear-all {
  padding: 12px 16px;
  font-size: 14px;
  color: #dc2626;
  border-top: 1px solid #1f2937;
  cursor: pointer;
  text-align: center;
  transition: background 0.15s ease, color 0.15s ease;
}

.session-clear-all:hover {
  background: rgba(220, 38, 38, 0.15);
  color: #dc2626;
}

.bot-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding-left: 6px;
  transition: opacity 1s ease-in;

  /* The row must have a definite width.

     .bot-message-wrap is a column flex with align-items:flex-start, which
     makes its children shrink-to-fit. Without this, .bot-row sized itself
     to its own content — and .msg-bot's `max-width: calc(100% - 100px)`
     then resolved that percentage against the shrunken row. The result was
     circular: the bubble could never be wider than its own max-content
     minus 100px, so every reply wrapped ~100px early no matter how much
     room the chat column had, and each typewriter character changed the
     row's width and reflowed the text mid-render. */
  width: 100%;
  box-sizing: border-box;
}

.bot-avatar {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  flex-shrink: 0;
  object-fit: cover;
  margin-top: 2px;
  pointer-events: none;
}

.bot-bubble {
    max-width: calc(100% - 100px);
    word-wrap: break-word;
    font-family: "mplus", arial;
    letter-spacing: 0.03em;
    font-weight: 380;
    line-height: 1.6;
    font-size: 15px;
}

/* Animations */
@keyframes slideInLeft {
  from {
    opacity: 0;
    transform: translateX(-8px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}
@keyframes slideInRight {
  from {
    opacity: 0;
    transform: translateX(8px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}
/* ============================================================
   DESKTOP SIDEBAR (chat history)
   Shown on >= 900px; hidden on mobile.
   ============================================================ */

.sidebar {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: 272px;
  display: none; /* enabled at desktop breakpoint below */
  flex-direction: column;
  background: var(--bg-panel);
  border-right: 1px solid var(--border-main);
  z-index: 40;
  user-select: none;
  min-height: 100vh;
}

.sidebar-header {
  padding: 14px 12px;
  border-bottom: 1px solid var(--border-main);
}

.sidebar-new-chat {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 10px 12px;
  border: 1px solid var(--border-main);
  border-radius: 10px;
  background: transparent;
  color: var(--text-main);
  font: inherit;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}

.sidebar-new-chat:hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-color: color-mix(in srgb, var(--accent) 35%, var(--border-main));
}

.sidebar-new-chat svg {
  color: currentColor;
  opacity: 0.85;
}

.sidebar-list {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 8px 8px 12px 8px;
  scrollbar-width: none;
}

.sidebar-list::-webkit-scrollbar { width: 6px; }
.sidebar-list::-webkit-scrollbar-track { background: transparent; }
.sidebar-list::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--text-soft) 30%, transparent);
  border-radius: 3px;
}

.sidebar-empty {
  padding: 24px 14px;
  font-size: 13px;
  color: var(--text-soft);
  text-align: center;
  opacity: 0.7;
}

.sidebar-entry {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 8px 10px;
  border-radius: 8px;
  color: var(--text-main);
  font-size: 14px;
  cursor: pointer;
  transition: background 0.12s ease;
}

.sidebar-entry:hover {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

.sidebar-entry.active {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
}

.sidebar-entry .sidebar-entry-title {
  flex: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-weight: 500;
  line-height: 1.3;
}

.sidebar-entry.active .sidebar-entry-title {
  font-weight: 600;
}

.sidebar-entry-del {
  -webkit-appearance: none;
  appearance: none;
  border: none;
  background: transparent;
  color: var(--text-soft);
  width: 26px;
  height: 26px;
  border-radius: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.15s ease, background 0.12s ease, color 0.12s ease;
}

.sidebar-entry:hover .sidebar-entry-del,
.sidebar-entry.active .sidebar-entry-del {
  opacity: 0.7;
}

.sidebar-entry-del:hover {
  opacity: 1 !important;
  color: #ef4444;
  background: rgba(239, 68, 68, 0.12);
}

.sidebar-entry-del.confirm {
  opacity: 1 !important;
  color: #ef4444;
  background: rgba(239, 68, 68, 0.18);
}

.sidebar-footer {
  border-top: 1px solid var(--border-main);
  padding: 8px;

  /* Row so the support icon can sit beside Settings. Settings keeps its
     width:100% and simply flexes to fill whatever the icon leaves. */
  display: flex;
  align-items: center;
  gap: 6px;
}

/* Compact icon-only sibling to .sidebar-footer-btn. Square rather than
   full-width, and shrink-proof so a long Settings label can never
   squeeze it. Height matches the Settings button's computed box
   (10px padding + 16px icon + 10px) so the two align on one line. */
.sidebar-footer-icon-btn {
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  color: var(--text-main);
  border-radius: 8px;
  cursor: pointer;
  text-decoration: none;
  transition: background 0.12s ease, color 0.12s ease;
}

.sidebar-footer-icon-btn:hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

.sidebar-footer-icon-btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}

.sidebar-footer-btn {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border: none;
  background: transparent;
  color: var(--text-main);
  font: inherit;
  font-size: 14px;
  border-radius: 8px;
  cursor: pointer;
  text-align: left;
  transition: background 0.12s ease;
}

.sidebar-footer-btn:hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

.sidebar-footer-btn svg {
  opacity: 0.85;
  flex-shrink: 0;
}

/* Desktop: enable sidebar, push app right, hide mobile dropdown chevron. */
@media (min-width: 900px) {
  .sidebar { display: flex; }
  body { padding-left: 272px; box-sizing: border-box; }
  /* Make absolute children center within the shifted .app, not the viewport. */
  .app { position: relative; }
  /* Hide the mobile dropdown affordances on desktop. */
  #dropdownToggle { display: none !important; }
  #dropdownMenu  { display: none !important; }
  /* Mobile center-pinned session panel is replaced by the sidebar on desktop. */
  #sessionPanel { display: none !important; }
}

/* ============================================================
   SETTINGS MODAL
   ============================================================ */

.settings-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  z-index: 9998;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
}

.settings-backdrop.open {
  opacity: 1;
  pointer-events: auto;
}

.settings-modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, calc(-50% + 8px));
  width: min(440px, calc(100% - 32px));
  max-height: calc(100% - 64px);
  background: var(--bg-panel);
  border: 1px solid var(--border-main);
  border-radius: 14px;
  box-shadow: 0 14px 40px rgba(0, 0, 0, 0.5);
  z-index: 9999;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.settings-modal.open {
  opacity: 1;
  transform: translate(-50%, -50%);
  pointer-events: auto;
}

.settings-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 18px;
  border-bottom: 1px solid var(--border-main);
}

.settings-head h2 {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  color: var(--text-main);
  letter-spacing: -0.01em;
}

.settings-close {
  -webkit-appearance: none;
  appearance: none;
  border: none;
  background: transparent;
  color: var(--text-soft);
  width: 30px;
  height: 30px;
  border-radius: 8px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background 0.12s ease, color 0.12s ease;
}

.settings-close:hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--text-main);
}

.settings-body {
  padding: 6px 0 8px 0;
  overflow-y: auto;

  /* Scrollbar hidden — it sat against the panel's rounded edge and looked
     bad. scrollbar-width covers Firefox; WebKit needs the pseudo-element,
     otherwise Chrome and Safari still draw it. Scrolling itself is
     unaffected. */
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.settings-body::-webkit-scrollbar {
  width: 0;
  height: 0;
  display: none;
}

.settings-row {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 14px 18px;
}

.settings-row + .settings-row {
  border-top: 1px solid color-mix(in srgb, var(--border-main) 70%, transparent);
}

.settings-row-label {
  flex: 1;
  min-width: 0;
}

.settings-row-title {
  font-size: 14px;
  font-weight: 500;
  color: var(--text-main);
  margin-bottom: 3px;
}

.settings-row-sub {
  font-size: 12px;
  color: var(--text-soft);
  line-height: 1.4;
  opacity: 0.85;
}

.settings-row-sub code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11.5px;
  padding: 1px 5px;
  border-radius: 4px;
  background: color-mix(in srgb, var(--text-main) 8%, transparent);
}

.settings-row-title-inline {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.settings-experimental-label {
  display: inline-flex;
  align-items: center;
  padding: 2px 7px;
  border-radius: 999px;
  font-size: 10.5px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.02em;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent);
}

/* Theme pill: shows "Dark" or "Light", click to toggle */
.settings-pill {
  -webkit-appearance: none;
  appearance: none;
  border: 1px solid var(--border-main);
  background: transparent;
  color: var(--text-main);
  font: inherit;
  font-size: 13px;
  font-weight: 500;
  padding: 7px 14px;
  border-radius: 999px;
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease;
  flex-shrink: 0;
}

.settings-pill:hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-color: color-mix(in srgb, var(--accent) 35%, var(--border-main));
}

/* Canvas theme segmented selector */
.settings-row-canvas-theme {
  align-items: flex-start;
}

.settings-canvas-theme-group {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px;
  border: 1px solid var(--border-main);
  border-radius: 999px;
  background: color-mix(in srgb, var(--text-main) 4%, transparent);
  flex-shrink: 0;
}

.settings-canvas-theme-btn {
  -webkit-appearance: none;
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--text-soft);
  font: inherit;
  font-size: 12.5px;
  font-weight: 500;
  padding: 6px 10px;
  border-radius: 999px;
  cursor: pointer;
  line-height: 1;
  transition:
    background 0.14s ease,
    color 0.14s ease,
    box-shadow 0.14s ease,
    opacity 0.14s ease;
}

.settings-canvas-theme-btn:hover {
  color: var(--text-main);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

.settings-canvas-theme-btn.active {
  color: var(--bg-main);
  background: var(--text-main);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.18);
}

body:not(.light) .settings-canvas-theme-btn.active {
  color: #ffffff;
  background: #6366f1;
}

@media (max-width: 560px) {
  .settings-row-canvas-theme {
    flex-direction: column;
    gap: 10px;
  }

  .settings-canvas-theme-group {
    width: 100%;
    box-sizing: border-box;
  }

  .settings-canvas-theme-btn {
    flex: 1;
    text-align: center;
  }
}

/* Voice selector.
   Deliberately built from the same primitives as .settings-pill so the
   row doesn't read as a foreign control: same border, radius, padding
   rhythm, hover tint and focus ring. The native <select> chrome is
   suppressed and replaced with our own caret. */
.settings-voice-group {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}

.settings-select-wrap {
  position: relative;
  display: inline-flex;
  align-items: center;
}

.settings-select {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  border: 1px solid var(--border-main);
  background: transparent;
  color: var(--text-main);
  font: inherit;
  font-size: 13px;
  font-weight: 500;
  /* Matches .settings-pill's 7px/14px, with right padding opened up
     to clear our caret. */
  padding: 7px 30px 7px 14px;
  border-radius: 999px;
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease;
  min-width: 116px;
  max-width: 200px;
  line-height: 1;
  text-overflow: ellipsis;
}

.settings-select:hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-color: color-mix(in srgb, var(--accent) 35%, var(--border-main));
}

.settings-select:focus-visible {
  outline: none;
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border-main));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}

/* Remove the dotted focus outline Firefox draws inside selects. */
.settings-select:-moz-focusring {
  color: transparent;
  text-shadow: 0 0 0 var(--text-main);
}

.settings-select::-ms-expand {
  display: none;
}

.settings-select-caret {
  position: absolute;
  right: 11px;
  color: var(--text-soft);
  pointer-events: none;
  transition: color 0.12s ease;
}

.settings-select-wrap:hover .settings-select-caret {
  color: var(--text-main);
}

/* Native dropdown popup — browsers ignore most <option> styling, but
   these are widely respected and keep the list readable in dark mode. */
.settings-select option,
.settings-select optgroup {
  background: var(--bg-panel);
  color: var(--text-main);
}

.settings-select optgroup {
  font-size: 11.5px;
  font-weight: 600;
  color: var(--text-soft);
}

/* Preview button. Sized and shaped like .bot-icon-btn (the per-message
   action buttons) rather than inventing a new affordance. The speak
   icon states themselves are styled in the SPEAK-OUT BUTTON section,
   which this button opts into by reusing .speak-icon markup. */
.settings-voice-preview {
  -webkit-appearance: none;
  appearance: none;
  border: none;
  background: transparent;
  color: var(--text-main);
  width: 30px;
  height: 30px;
  border-radius: 8px;
  padding: 0;
  margin: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  line-height: 1;
  flex-shrink: 0;
  opacity: 0.62;
  transition: background 120ms ease, color 120ms ease, opacity 120ms ease;
}

.settings-voice-preview:hover {
  background: rgba(127, 127, 127, 0.10);
  opacity: 1;
}

.settings-voice-preview:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}

.settings-voice-preview:disabled {
  cursor: default;
}

.settings-voice-preview.playing {
  opacity: 1;
}

@media (max-width: 560px) {
  /* Stack label above the control, exactly like .settings-row-canvas-theme.
     Without the column switch the group's width:100% squeezes the label
     to a one-word-per-line column. */
  .settings-row[data-setting="tts-voice"] {
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
  }

  .settings-voice-group {
    width: 100%;
    box-sizing: border-box;
    justify-content: flex-end;
  }

  /* The wrap is the actual flex item of the group — the select's own
     flex:1 only applies inside the wrap, so the wrap has to grow too. */
  .settings-select-wrap {
    flex: 1;
    min-width: 0;
  }

  .settings-select {
    flex: 1;
    max-width: none;
    min-width: 0;
  }
}

/* Switch (used for boolean toggles) */
.settings-switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  flex-shrink: 0;
}

.settings-switch input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.settings-switch-track {
  width: 38px;
  height: 22px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--text-soft) 35%, transparent);
  position: relative;
  transition: background 0.18s ease;
}

.settings-switch-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #ffffff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
  transition: transform 0.18s ease;
}

.settings-switch input:checked + .settings-switch-track {
  background: var(--accent);
}

body:not(.light) .settings-switch input:checked + .settings-switch-track {
  /* In dark mode the neutral accent is light gray; give it a tint so on/off
     is unambiguous without changing brand palette. */
  background: #6366f1;
}

.settings-switch input:checked + .settings-switch-track .settings-switch-thumb {
  transform: translateX(16px);
}

/* ============================================================
   USER MESSAGE ACTIONS (3-dot menu: edit / branch)
   ============================================================ */

.user-message-wrap {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  margin-top: 10px;
}

/* Bubble inherits .msg-user (align-self: flex-end, max-width: calc(100% - 100px),
   margin: 0 10px) so it sits right-aligned and content-sized. We only need
   to keep the action row to the right with matching gutter. */

.user-actions {
  display: flex;
  align-items: center;
  gap: 2px;
  margin-right: 10px;
  margin-top: 2px;
  margin-bottom: 4px;
  position: relative;
  visibility: hidden;
}

.user-actions.show {
  visibility: visible;
}

.user-actions .bot-icon-btn { /* reuse existing icon button skin */
  color: var(--text-main);
}

.user-feedback-menu {
  position: absolute;
  bottom: calc(100% + 6px);
  right: 0;
  background: var(--bg-panel);
  border: 1px solid var(--border-main);
  border-radius: 10px;
  padding: 4px;
  min-width: 180px;
  opacity: 0;
  transform: translateY(4px) scale(0.98);
  visibility: hidden;
  pointer-events: none;
  transition: opacity 160ms ease, transform 160ms ease,
              visibility 0s linear 160ms;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.3);
  z-index: 50;
}

.user-feedback-menu.open {
  opacity: 1;
  transform: translateY(0) scale(1);
  visibility: visible;
  pointer-events: auto;
  transition: opacity 160ms ease, transform 160ms ease,
              visibility 0s linear 0s;
}

.user-feedback-menu .user-feedback-item {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 10px;
  border-radius: 7px;
  font-size: 13px;
  color: var(--text-main);
  cursor: pointer;
  white-space: nowrap;
}

.user-feedback-menu .user-feedback-item:hover {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}

.user-feedback-menu .user-feedback-item svg {
  width: 14px;
  height: 14px;
  opacity: 0.9;
  flex-shrink: 0;
}

/* ============================================================
   USER MESSAGE INLINE EDIT
   ============================================================ */

.user-edit-form {
    align-self: flex-end;
    margin: 0 10px 0 auto;

    display: flex;
    flex-direction: column;
    gap: 8px;

    background: var(--bg-bubble-user);
    border-radius: 16px;
    border-bottom-right-radius: 4px;
    padding: 12px 14px;

    box-sizing: border-box;

    width: var(--original-user-bubble-width, fit-content);
    min-width: 250px;
    max-width: calc(100% - 50px);
}

.user-edit-form textarea {
    width: 100%;
    min-width: 0;
    box-sizing: border-box;
}

.user-edit-form textarea {
  border: none;
  outline: none;
  resize: vertical;
  min-height: 60px;
  max-height: 240px;
  background: transparent;
  color: #ffffff;
  font: inherit;
  font-size: 15px;
  font-family: Arial, system-ui, sans-serif;
  letter-spacing: 0.02em;
  line-height: 1.4;
  scrollbar-width: none;
}

body.light .user-edit-form textarea { color: #ffffff; }

.user-edit-actions {
  display: flex;
  justify-content: flex-end;
  gap: 6px;
}

.user-edit-btn {
  -webkit-appearance: none;
  appearance: none;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: transparent;
  color: #ffffff;
  font: inherit;
  font-size: 13px;
  font-weight: 500;
  padding: 6px 14px;
  border-radius: 999px;
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease;
}

.user-edit-btn:hover {
  background: rgba(255, 255, 255, 0.10);
}

.user-edit-btn.primary {
  background: #ffffff;
  color: #1f2937;
  border-color: #ffffff;
}

.user-edit-btn.primary:hover {
  background: #f3f4f6;
}

.user-edit-btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

/* ============================================================
   SESSION PANEL (mobile) — per-row delete button
   ============================================================ */

.session-entry {
  display: flex;
  align-items: center;
  gap: 6px;
}

.session-entry .session-line {
  flex: 1;
  min-width: 0;
}

.session-entry .session-line > div:first-child {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.session-entry-del {
  -webkit-appearance: none;
  appearance: none;
  border: none;
  background: transparent;
  color: var(--text-soft);
  width: 28px;
  height: 28px;
  border-radius: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  opacity: 0.55;
  flex-shrink: 0;
  transition: background 0.12s ease, color 0.12s ease, opacity 0.12s ease;
}

.session-entry-del:hover,
.session-entry-del.confirm {
  opacity: 1;
  color: #ef4444;
  background: rgba(239, 68, 68, 0.12);
}

.session-entry-del.confirm {
  background: rgba(239, 68, 68, 0.20);
}


/* ============================================================
   SIDEBAR TOGGLE BUTTON  (desktop only)
   Lives outside the sidebar so it stays visible when sidebar
   slides off-screen. Animates its left position when sidebar
   collapses/expands.
   ============================================================ */

.sidebar-toggle {
  position: fixed;
  top: 14px;
  left: 232px;             /* sits at sidebar's right edge when open */
  z-index: 41;             /* above sidebar (which is z:40) */
  width: 32px;
  height: 32px;
  display: none;           /* desktop-only, enabled at >=900px below */
  align-items: center;
  justify-content: center;
  padding: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
  color: var(--text-soft);
  cursor: pointer;
  transition: left 0.28s ease, background 0.15s ease,
              border-color 0.15s ease, color 0.15s ease;
}

.sidebar-toggle:hover {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border-color: color-mix(in srgb, var(--accent) 35%, var(--border-main));
  color: var(--text-main);
}

/* ============================================================
   SIDEBAR BRAND
   ============================================================
   Small wordmark on its own row at the top of the sidebar, sharing
   that line with the floating collapse toggle. On desktop this is the
   persistent brand: the centred hero logo fades out once a chat starts
   (see the title fade-out block further down), so this is what carries
   the identity in the conversation view.

   The sidebar is display:none below 900px, so this is inherently
   desktop-only and needs no media query of its own. */
.sidebar-brand {
  display: flex;
  align-items: center;
  height: 30px;              /* matches the toggle so both sit on one line */
  margin-bottom: 10px;
  padding-right: 44px;       /* keeps the wordmark clear of the toggle */
  font-size: 17px;
  font-weight: 800;
  letter-spacing: -0.3px;
  color: var(--text-main);
  line-height: 1;
  white-space: nowrap;
  overflow: hidden;
}

/* The .mini / .haiku weights are shared with the hero logo. The hero's
   left margin is for centring its pair and would just indent the sidebar,
   so it's cleared. The word gap is set explicitly rather than relying on
   the literal space in the markup: at this size, with the tightened
   letter-spacing, that space collapses to almost nothing and the wordmark
   reads as "ChatHaiku". */
.sidebar-brand .mini {
  margin-left: 0;
}

.sidebar-brand .haiku {
  margin-left: 0.2em;
}

body.sidebar-collapsed .sidebar-toggle {
  left: 14px;              /* slides to viewport edge when sidebar hidden */
}

/* Sidebar slide-out + body padding collapse */
.sidebar {
  transition: transform 0.28s ease;
}

body.sidebar-collapsed .sidebar {
  transform: translateX(-100%);
}

@media (min-width: 900px) {
  .sidebar-toggle { display: inline-flex; }
  body { transition: padding-left 0.28s ease; }
  body.sidebar-collapsed { padding-left: 0; }
}

/* On mobile the sidebar isn't shown at all so the toggle stays hidden too */
@media (max-width: 899px) {
  .sidebar-toggle { display: none !important; }
}

/* ============================================================
   DESKTOP MODEL PICKER
   Sits beside the sidebar toggle on desktop only.
   Mobile keeps model selection inside the 3-dot profile menu.
   ============================================================ */

.desktop-model-picker {
  position: fixed;
  top: 14px;

  /* Sidebar open:
     sidebar width is 272px, so this gives a clean 18px gap
     between the sidebar edge and the model selector. */
  left: 290px;

  z-index: 42;
  display: none;
  user-select: none;
  transition: left 0.28s ease;
}

body.sidebar-collapsed .desktop-model-picker {
  /* Sidebar closed:
     sidebar icon is left: 14px and width: 30px,
     so 52px keeps a tight 8px gap after the icon. */
  left: 52px;
}

body.sidebar-collapsed .desktop-model-picker {
  left: 52px;
}

.desktop-model-toggle {
  -webkit-appearance: none;
  appearance: none;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  padding: 0 9px;
  border-radius: 8px;
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-soft);
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  line-height: 1;
  transition:
    background 0.15s ease,
    border-color 0.15s ease,
    color 0.15s ease;
}

.desktop-model-toggle:hover,
.desktop-model-toggle.open {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border-color: color-mix(in srgb, var(--accent) 35%, var(--border-main));
  color: var(--text-main);
}

.desktop-model-caret {
  flex: 0 0 auto;
  opacity: 0.82;
  transition: transform 0.18s ease;
}

.desktop-model-toggle.open .desktop-model-caret {
  transform: rotate(180deg);
}

.desktop-model-menu {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  width: 236px;
  padding: 5px;
  border-radius: 13px;
  border: 1px solid var(--border-main);
  background: var(--bg-panel);
  box-shadow:
    0 10px 30px rgba(0, 0, 0, 0.32),
    0 1px 0 rgba(255, 255, 255, 0.02) inset;
  opacity: 0;
  transform: translateY(-6px) scale(0.985);
  transform-origin: top left;
  pointer-events: none;
  transition:
    opacity 0.17s ease,
    transform 0.17s cubic-bezier(0.22, 1, 0.36, 1);
}

.desktop-model-menu.open {
  opacity: 1;
  transform: translateY(0) scale(1);
  pointer-events: auto;
}

.desktop-model-item {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  padding: 9px 12px 9px 14px;
  border-radius: 9px;
  color: var(--text-main);
  cursor: pointer;
  transition: background 0.14s ease;
}

/* Hairline separator between items, skipped on the first one and on
   any item that sits directly before the active one (so the active
   pill reads as a clean unbroken block). */
.desktop-model-item + .desktop-model-item::before {
  content: "";
  position: absolute;
  top: 0;
  left: 14px;
  right: 12px;
  height: 1px;
  background: var(--border-main);
  opacity: 0.6;
}

.desktop-model-item:hover {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.desktop-model-item:hover::before,
.desktop-model-item:hover + .desktop-model-item::before {
  opacity: 0;
}

.desktop-model-item.active {
  background: color-mix(in srgb, var(--accent) 15%, transparent);
}
.desktop-model-item.active::before,
.desktop-model-item.active + .desktop-model-item::before {
  opacity: 0;
}

/* Soft accent bar marking the active model. */
.desktop-model-item.active::after {
  content: "";
  position: absolute;
  top: 9px;
  bottom: 9px;
  left: 5px;
  width: 3px;
  border-radius: 3px;
  background: var(--accent);
}

.desktop-model-item-title {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.desktop-model-item-sub {
  font-size: 11.5px;
  line-height: 1.4;
  color: var(--text-soft);
  opacity: 0.82;
}

.desktop-model-item.active .desktop-model-item-sub {
  color: var(--accent);
  opacity: 0.95;
}

@media (min-width: 900px) {
  .desktop-model-picker {
    display: block;
  }
}

@media (max-width: 899px) {
  .desktop-model-picker {
    display: none !important;
  }
}

.desktop-model-name {
  font-weight: 600;
}

.desktop-model-thinking-mode {
  font-weight: 400;
  opacity: 0.62;
  margin-left: 2px;
  white-space: nowrap;
}

.desktop-model-toggle {
  gap: 5px;
}

.desktop-model-toggle {
  max-width: 230px;
}

#desktopModelLabel {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  min-width: 0;
}

/* ============================================================
   WELCOME MESSAGE  (default/empty state under the logo)
   Fades out together with the title when the chat starts.
   ============================================================ */

.welcome-message {
  margin-top: 14px;
  font-family: 'mplus', Inter, arial;
  font-size: 16px;
  font-weight: 400;
  color: var(--text-soft);
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 0.5s ease 0.15s, transform 0.5s ease 0.15s,
              max-height 0.4s ease, margin 0.4s ease;
  letter-spacing: 0.01em;
  user-select: none;
  pointer-events: none;
  max-height: 80px;
  overflow: hidden;
  line-height: 1.4;
}

.welcome-message.ready {
  opacity: 0.78;
  transform: translateY(0);
}

/* Collapse the welcome line into nothing when the chat begins */
#titleBar.active .welcome-message {
  opacity: 0 !important;
  transform: translateY(-4px) !important;
  max-height: 0 !important;
  margin-top: 0 !important;
}

/* ============================================================
   USERINPUT TEXTAREA  (multi-line wrap + auto-grow)
   The element used to be an <input>. We now ship a <textarea>
   that grows vertically with content while preserving the
   single-line look at rest.
   ============================================================ */

textarea#userInput {
  resize: none;            /* user cannot drag-resize */
  min-height: 46px;        /* matches the old <input> height */
  max-height: 200px;       /* before scrolling kicks in */
  line-height: 1.4;
  padding: 12px 20px;
  border-radius: 22px;     /* large radius collapses cleanly when single line */
  overflow-y: auto;
  word-break: break-word;
  white-space: pre-wrap;
  scrollbar-width: thin;
  display: block;
}

textarea#userInput::-webkit-scrollbar { width: 6px; }
textarea#userInput::-webkit-scrollbar-track { background: transparent; }
textarea#userInput::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--text-soft) 35%, transparent);
  border-radius: 3px;
}

/* Anchor the end controls to the bottom edge so they don't drift
   upward as the textarea grows. */
.input-bar {
  align-items: flex-end;
}

/* ============================================================
   EDIT-FORM TEXTAREA  (lock size, no user resize)
   ============================================================ */

.user-edit-form textarea {
  resize: none;            /* was 'vertical' — now locked */
}

/* ============================================================
   AUGMENTED THINKING ROW  (disabled when force-think is off)
   Visually mirrors the disabled state of a child setting.
   ============================================================ */

.settings-row.is-disabled {
  opacity: 0.45;
  pointer-events: none;
}
.settings-row.is-disabled .settings-row-sub {
  opacity: 0.9;
}

/* Subtle inset for the nested sub-toggle row */
.settings-row-sub-toggle {
  padding-left: 28px;
  position: relative;
}
.settings-row-sub-toggle::before {
  content: "";
  position: absolute;
  left: 14px;
  top: 22px;
  bottom: 22px;
  width: 2px;
  background: color-mix(in srgb, var(--accent) 35%, transparent);
  border-radius: 1px;
  opacity: 0.55;
}
.settings-row-sub-toggle.is-disabled::before {
  background: var(--border-main);
  opacity: 0.5;
}

/* ============================================================
   RLHF FEEDBACK MODAL
   ============================================================ */

.rating-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.46);
  backdrop-filter: blur(6px);
  z-index: 10000;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
}

.rating-backdrop.open {
  opacity: 1;
  pointer-events: auto;
}

.rating-modal {
  position: fixed;
  top: 50%;
  left: 50%;
  width: min(520px, calc(100% - 32px));
  max-height: calc(100% - 64px);
  transform: translate(-50%, calc(-50% + 8px));
  background: var(--bg-panel);
  border: 1px solid var(--border-main);
  border-radius: 14px;
  box-shadow: 0 14px 40px rgba(0, 0, 0, 0.5);
  z-index: 10001;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.rating-modal.open {
  opacity: 1;
  transform: translate(-50%, -50%);
  pointer-events: auto;
}

.rating-modal-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
  padding: 16px 18px;
  border-bottom: 1px solid var(--border-main);
}

.rating-modal-head h2 {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  color: var(--text-main);
  letter-spacing: -0.01em;
}

.rating-modal-head p {
  margin: 4px 0 0 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--text-soft);
}

.rating-close {
  -webkit-appearance: none;
  appearance: none;
  border: none;
  background: transparent;
  color: var(--text-soft);
  width: 30px;
  height: 30px;
  border-radius: 8px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background 0.12s ease, color 0.12s ease;
  flex-shrink: 0;
}

.rating-close:hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--text-main);
}

.rating-modal-body {
  padding: 16px 18px 18px;
  overflow-y: auto;
  scrollbar-width: none;
}

.rating-response-preview {
  max-height: 110px;
  overflow: auto;
  padding: 11px 12px;
  margin-bottom: 16px;
  border-radius: 10px;
  border: 1px solid color-mix(in srgb, var(--border-main) 80%, transparent);
  background: color-mix(in srgb, var(--text-main) 5%, transparent);
  color: var(--text-soft);
  font-size: 12px;
  line-height: 1.45;
  white-space: pre-wrap;
}

.rating-field {
  margin-bottom: 15px;
}

.rating-label {
  display: block;
  margin-bottom: 8px;
  color: var(--text-main);
  font-size: 13px;
  font-weight: 500;
}

.rating-check-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.rating-check-grid label {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 34px;
  padding: 8px 10px;
  border: 1px solid color-mix(in srgb, var(--border-main) 80%, transparent);
  border-radius: 10px;
  color: var(--text-soft);
  font-size: 12px;
  line-height: 1.25;
  cursor: pointer;
  background: transparent;
  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}

.rating-check-grid label:hover {
  color: var(--text-main);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  border-color: color-mix(in srgb, var(--accent) 24%, var(--border-main));
}

.rating-check-grid input {
  accent-color: var(--accent);
}

.rating-modal textarea {
  box-sizing: border-box;
  width: 100%;
  resize: vertical;
  min-height: 78px;
  max-height: 180px;
  padding: 10px 11px;
  border-radius: 10px;
  border: 1px solid var(--border-main);
  background: color-mix(in srgb, var(--text-main) 5%, transparent);
  color: var(--text-main);
  font: inherit;
  font-size: 13px;
  line-height: 1.45;
  outline: none;
  scrollbar-width: none;
}

.rating-modal textarea:focus {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border-main));
  background: color-mix(in srgb, var(--text-main) 7%, transparent);
}

.rating-modal textarea::placeholder {
  color: var(--text-soft);
  opacity: 0.75;
}

.rating-modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 18px;
}

.rating-btn {
  -webkit-appearance: none;
  appearance: none;
  border: 1px solid var(--border-main);
  background: transparent;
  color: var(--text-main);
  border-radius: 999px;
  padding: 8px 14px;
  font: inherit;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease, opacity 0.12s ease;
}

.rating-btn:hover {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  border-color: color-mix(in srgb, var(--accent) 30%, var(--border-main));
}

.rating-btn.primary {
  color: var(--bg-main);
  background: var(--text-main);
  border-color: var(--text-main);
}

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

.bot-icon-btn.is-saving {
  opacity: 0.35 !important;
  pointer-events: none;
}

@media (max-width: 560px) {
  .rating-check-grid {
    grid-template-columns: 1fr;
  }

  .rating-modal-actions {
    flex-direction: column-reverse;
  }

  .rating-btn {
    width: 100%;
  }
}

/* ============================================================
   ROUND 2 FIXES — viewport safety, header wrap, input growth,
   sidebar header clearance, branched session badge.
   ============================================================ */

/* Belt-and-suspenders sizing for the root + body. 100dvh alone can
   resolve oddly on some Chrome configurations during transitions;
   the explicit fallbacks keep the layout from ever shrinking below
   the visual viewport. */
html, body {
  height: 100%;
  min-height: 100dvh;
}

/* Header: let the welcome message wrap to its own row beneath the
   logo so the h1 is alone on its row and stays centered no matter
   what's around it. Also a small top inset so the title isn't
   pressed against the viewport edge. */
.header {
  flex-wrap: wrap;
  padding-top: 16px;
}

.welcome-message {
  flex-basis: 100%;     /* force onto its own row in the flex header */
  width: 100%;
  text-align: center;
}

/* Hard collapse the welcome from layout once the chat starts. The
   earlier max-height transition animates the vertical collapse; the
   flex-basis: 100% ensures the welcome sits on its own row so
   collapsing it doesn't reflow the h1 row above. */
#titleBar.active .welcome-message {
  flex-basis: 100%;
  width: 100%;
  height: 0 !important;
  pointer-events: none;
}

/* Sidebar: explicit fallback height + reserve a clear lane on the
   right edge of the header for the floating toggle button. */
.sidebar {
  height: 100vh;        /* fallback */
  height: 100dvh;
}

.sidebar-header {
  /* The floating toggle now shares the brand's row rather than the New
     chat row, so the header no longer reserves a right-hand lane. The
     clearance lives on .sidebar-brand instead. */
  padding-right: 12px;
}

/* Toggle: tighter footprint so it sits cleanly inside the reserved lane. */
.sidebar-toggle {
  width: 30px;
  height: 30px;
  top: 14px;
  left: 234px;          /* sits inside the reserved lane */
}
body.sidebar-collapsed .sidebar-toggle {
  left: 14px;
}

/* Input bar: anchor by BOTTOM when active so the textarea grows
   upward. We use top: calc(100% - --bar-h - bottom-gap) — the JS
   autosize keeps --bar-h in sync with the textarea height, so the
   bar's top edge slides upward as content grows while the bottom
   edge stays a fixed gap above the disclaimer. Doing this via `top`
   (not switching to `bottom: auto`) keeps the original 0.6s glide
   from centered → docked smooth, since `top` interpolates between
   defined values cleanly. */
.input-bar.active {
  --bar-h: 46px;
  top: calc(100% - var(--bar-h) - 30px);
}

/* Profile-toggle (3-dot in input bar): pin to single-line input height
   so it visually aligns with the send button regardless of how tall
   the textarea has grown. */
.profile-toggle {
  height: 46px !important;
}

/* Textarea: tighter resting size, max 5 lines, themed scrollbar that
   matches the sidebar list scrollbar. */
textarea#userInput {
  box-sizing: border-box;
  min-height: 46px;
  height: 46px;         /* explicit resting height; JS autosize overrides */
  max-height: 130px;    /* ~5 lines at line-height 1.4 / fontsize 15 */
  padding: 11px 20px;
  line-height: 1.4;
  scrollbar-width: none;
}
textarea#userInput::-webkit-scrollbar { width: 6px; }
textarea#userInput::-webkit-scrollbar-track { background: transparent; }
textarea#userInput::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--text-soft) 35%, transparent);
  border-radius: 3px;
}
textarea#userInput::-webkit-scrollbar-thumb:hover {
  background: color-mix(in srgb, var(--text-soft) 55%, transparent);
}

/* Branched session badge — sidebar + mobile panel.
   Renders a small fork glyph + the word "Branched" before the title,
   the wider sidebar gets the word, the mobile entry just the glyph
   so the title still fits. */
.sidebar-entry.branched .sidebar-entry-title,
.session-entry.branched .session-line {
  display: flex;
  align-items: baseline;
  gap: 6px;
  min-width: 0;
}

.branched-tag {
  flex-shrink: 0;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  border-radius: 999px;
  padding: 1px 7px;
  line-height: 1.4;
}

.branched-tag::before {
  content: "↳ ";
  letter-spacing: 0;
}

/* The actual title text needs an inline ellipsis under flex */
.sidebar-entry.branched .sidebar-entry-title > .branched-title-text,
.session-entry.branched .session-line > .branched-title-text {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  min-width: 0;
  flex: 1;
}

/* ============================================================
   MOBILE HEADER CARET ALIGNMENT
   Keeps the dropdown caret inline with ChatHaiku in both the
   centered landing state and the active top-header state.
   ============================================================ */

@media (max-width: 899px) {
  #titleBar {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    column-gap: 8px;
    row-gap: 0;
  }

  #titleBar h1 {
    order: 1;
    flex: 0 0 auto;
  }

  #dropdownToggle {
    order: 2;
    flex: 0 0 auto;
    margin-left: 0;
    align-self: center;
  }

  #welcomeMessage {
    order: 3;
    flex-basis: 100%;
    width: 100%;
  }

  #titleBar.active #dropdownToggle {
    align-self: center;
  }
  #dropdownToggle svg {
    display: block;
    transform: translateY(1px);
  }
  #disclaimer {
    font-size: 10px;
  }
}
/* ============================================================
   3-LINE EXPANDING COMPOSER CAP
   Keeps the original expanding feel, but prevents long pasted
   prompts from stretching the page beyond a controlled height.
   ============================================================ */

:root {
  --composer-h: 46px;
}

#inputBar,
.input-bar {
  min-width: 0;
  box-sizing: border-box;
}

#userInput {
  min-height: 46px;
  max-height: 92px;
  resize: none;
  overflow-y: hidden;
  min-width: 0;
  width: 100%;
  box-sizing: border-box;
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* Once the input has more than 3 lines, JS switches it to internal scrolling. */
#userInput:focus {
  outline: none;
}

/* Keep the chat area aware of the composer height while it grows. */
.chat-box.padded {
  padding-bottom: calc(var(--composer-h, 46px) + 84px) !important;
}

/* ============================================================
   FLOATING MENU OUTSIDE-CLICK SCRIM
   Used only for profile preset + desktop model menus.
   It avoids global document click handlers interfering with Settings.
   ============================================================ */

.floating-menu-scrim {
  position: fixed;
  inset: 0;
  display: none;
  background: transparent;
  z-index: 900;
}

.floating-menu-scrim.open {
  display: block;
}

body.floating-selector-open #inputBar,
body.floating-selector-open .input-bar {
  z-index: 1001;
}

body.floating-selector-open #desktopModelPicker,
body.floating-selector-open .desktop-model-picker {
  z-index: 1002;
}

/* Settings must always sit above the floating selector scrim. */
.settings-backdrop {
  z-index: 9998;
}

.settings-modal {
  z-index: 9999;
}

.bot-actions,
.user-actions {
  position: relative;
  z-index: 60;
}

.bot-feedback-menu,
.user-feedback-menu {
  z-index: 70;
}

/* ============================================================
   BOT CODE BLOCKS (rendered from ``` fences in formatBotText)
   ============================================================ */

.bot-code-block {
  background: var(--bg-input-focus);
  color: var(--text-bot);
  border: 1px solid var(--border-main);
  border-radius: 8px;
  padding: 10px 12px;
  margin: 8px 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
    "Liberation Mono", "Courier New", monospace;
  font-size: 0.88em;
  line-height: 1.45;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-width: 100%;
}

.bot-code-block code {
  font-family: inherit;
  background: transparent;
  color: inherit;
  padding: 0;
  display: block;
  user-select: text;
}


/* ============================================================
   BOT AVATAR — GENERATING STATE
   While a reply is being generated (both the pre-typewriter
   placeholder phase and the typewriter phase) the avatar shows
   a soft glowing ring that rotates around its rounded edge and
   the avatar itself pulses gently. The animation stops as soon
   as the typewriter has finished rendering the complete reply
   and the avatar returns to its static state.
   ============================================================ */

.bot-avatar-frame {
  position: relative;
  width: 24px;
  height: 24px;
  flex-shrink: 0;
  margin-top: 12px;        /* preserve the prior .bot-avatar offset */
  margin-left: 12px;
  display: block;
}

/* The avatar img inside the frame fills the frame exactly; the
   frame now carries the layout offset, so the img gets no extra
   margin-top. The pulse animation targets this element when the
   frame has the .generating class. */
.bot-avatar-frame > .bot-avatar {
  margin-top: 0;
  width: 100%;
  height: 100%;
  position: relative;
  z-index: 1;
  border-radius: 50%;
  transform-origin: center;
  will-change: transform, filter;
}

/* Glowing ring overlay. Positioned absolutely so it can extend
   slightly beyond the avatar without disturbing row layout.
   The conic gradient renders a soft cyan -> purple -> pink arc
   that sweeps once per rotation; the avatar img sitting on top
   covers the inner portion, leaving a thin ring along the edge. */
.bot-avatar-ring {
  position: absolute;
  inset: -3px;
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.25s ease;
  background: conic-gradient(
    from 0deg,
    transparent 0deg,
    transparent 240deg,
    rgba(124, 211, 255, 0.45) 285deg,
    rgba(198, 139, 255, 0.90) 325deg,
    rgba(255, 129, 204, 0.65) 355deg,
    transparent 360deg
  );
  will-change: transform;
}

.bot-avatar-frame.generating > .bot-avatar-ring {
  opacity: 1;
  animation: bot-avatar-ring-spin 1.4s linear infinite;
}

.bot-avatar-frame.generating > .bot-avatar {
  animation: bot-avatar-pulse 1.8s ease-in-out infinite;
}

/* Soft outer halo on the avatar frame during generation. Uses
   drop-shadow rather than box-shadow so the glow respects the
   circular alpha of the inner ring + avatar (square box-shadow
   on the 38×38 frame would bleed outside the visible circle).
   Period matches .deep-think-label's breathe so the avatar and
   status text read as one organism thinking. Kept subtle so the
   bloom fades smoothly into the surrounding background instead
   of forming a visible halo perimeter. */
.bot-avatar-frame.generating {
  animation: bot-avatar-halo-breathe 3.5s ease-in-out infinite;
}

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

@keyframes bot-avatar-pulse {
  0%, 100% {
    transform: scale(1);
    filter: brightness(1);
  }
  50% {
    transform: scale(1.05);
    filter: brightness(1.08);
  }
}

@keyframes bot-avatar-halo-breathe {
  0%, 100% {
    filter:
      drop-shadow(0 0 2px rgba(124, 211, 255, 0.10))
      drop-shadow(0 0 5px rgba(198, 139, 255, 0.07));
  }
  50% {
    filter:
      drop-shadow(0 0 4px rgba(124, 211, 255, 0.18))
      drop-shadow(0 0 7px rgba(198, 139, 255, 0.12));
  }
}

/* ============================================================
   TYPEWRITER PER-CHUNK BLUR-IN
   ============================================================
   Each chunk the typewriter streams in is wrapped in a .word-blur
   span. Initial state: blurred + dim. The JS adds .settled on the
   next-next frame (double-rAF pattern), which kicks off a CSS
   transition to clear + opaque over ~320ms.

   Using a transition + class swap rather than a CSS animation
   because animations on many newly-appended elements within the
   same compositor frame can batch/stick at the initial keyframe;
   transitions reliably fire per-element when triggered by a
   class change after the initial paint.

   The 320ms duration with 65–100ms chunk arrivals gives a
   continuous "tail" of 3–5 chunks blurring in behind the
   typewriter cursor, then settling sharp.
*/
.word-blur {
  display: inline;
  filter: blur(1px);
  opacity: 0.5;
  transition: filter 0.32s ease-out, opacity 0.32s ease-out;
  will-change: filter, opacity;
}

.word-blur.settled {
  filter: blur(0);
  opacity: 1;
}

/* Honor system-level reduced-motion preference: rings, pulses,
   halo, status-label breathe, and word-blur all freeze.
   The static look: ring visible as a soft glow, no spin/pulse;
   label fully visible without blur-in; words appear sharp. */
@media (prefers-reduced-motion: reduce) {
  .bot-avatar-frame.generating > .bot-avatar-ring,
  .bot-avatar-frame.generating > .bot-avatar,
  .bot-avatar-frame.generating,
  .deep-think-label {
    animation: none;
  }
  .deep-think-label {
    opacity: 0.85;
    filter: none;
  }
  .bot-avatar-frame.generating > .bot-avatar-ring {
    opacity: 0.85;
  }
  .word-blur,
  .word-blur.settled {
    transition: none;
    filter: none;
    opacity: 1;
  }
}

/* ============================================================
   DPO A/B COMPARISON CARDS
   ============================================================
   Side-by-side comparison layout — two cards in a CSS grid with
   a centered "Which response do you prefer?" header above. Each
   card is a <button> for accessibility (keyboard-focusable, role
   communicated to screen readers, clickable everywhere). Header
   bar inside each card has a mini bot avatar + "Response 1"/
   "Response 2" label; body is the typewriter target.

   On pick, the rejected card fades + collapses, the chosen card
   briefly glows cyan, then the entire comparison wrap tears down
   and a fresh normal .bot-message-wrap takes its place holding
   the chosen reply. The structural diff between "card grid" and
   "normal bot row" is too large to morph gracefully in place;
   the tear-down + fresh render is cleaner.

   Narrow viewports (<=700px): grid collapses to single column.
*/

.dpo-pair {
  /* Outer band — subtle so the cards do the visual work. The
     stronger card borders provide the comparison framing. */
  padding: 14px 14px 4px;
  margin-top: 8px;
}

.dpo-pair-header {
  text-align: center;
  margin: 0 auto 16px;
  max-width: 520px;
  /* Materialize softly when the card appears, matching the
     existing avatar/word-blur language. */
  animation: dpo-fade-in 0.42s ease-out both;
}

.dpo-pair-title {
  font-size: 15px;
  font-weight: 500;
  color: var(--text-main, #f2f2f2);
  letter-spacing: 0.01em;
}

.dpo-pair-hint {
  margin-top: 4px;
  font-size: 12.5px;
  color: var(--text-soft);
  opacity: 0.62;
  font-style: italic;
}

@keyframes dpo-fade-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0);    }
}

/* The grid that holds both cards side by side. Equal-width
   columns so the cards visually weigh the same — neither
   response gets implicit preference by being wider. */
.dpo-card-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
}

/* Each card — a <button> styled as a soft cyan-bordered panel.
   Disabled until the typewriter finishes; .ready signals
   interactive state with hover/focus affordances. */
.dpo-card {
  appearance: none;
  -webkit-appearance: none;
  display: flex;
  flex-direction: column;
  padding: 0;                  /* header + body provide their own padding */
  margin: 0;
  border: 1px solid rgba(124, 211, 255, 0.16);
  background: rgba(124, 211, 255, 0.022);
  border-radius: 14px;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: default;
  opacity: 0.88;
  transition:
    background 0.22s ease,
    border-color 0.22s ease,
    box-shadow 0.32s ease,
    transform 0.42s ease,
    opacity 0.42s ease;
  /* Materialize the card with the header — slightly delayed so
     the header lands first, then the cards arrive. */
  animation: dpo-fade-in 0.42s ease-out 0.1s both;
}

.dpo-card.ready {
  cursor: pointer;
  opacity: 1;
}

.dpo-card.ready:hover {
  background: rgba(124, 211, 255, 0.06);
  border-color: rgba(124, 211, 255, 0.34);
}

.dpo-card.ready:focus-visible {
  outline: none;
  border-color: rgba(124, 211, 255, 0.55);
  box-shadow: 0 0 0 3px rgba(124, 211, 255, 0.18);
}

.dpo-card:active {
  transform: scale(0.995);
}

/* Header bar inside each card — mini avatar + "Response N" label. */
.dpo-card-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 14px;
  border-bottom: 1px solid rgba(124, 211, 255, 0.08);
}

.dpo-card-avatar {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  overflow: hidden;
  flex-shrink: 0;
  display: block;
  background: rgba(255, 255, 255, 0.04);
}

.dpo-card-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.dpo-card-label {
  font-size: 12.5px;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--text-soft);
}

/* Body — the response text. Inherits .msg-bot so typewriter
   chunks (.word-blur), code blocks, links etc. all style the
   same as in a normal bubble. Padding here rather than on
   .msg-bot since we want it scoped to the card. */
.dpo-card-body {
  padding: 12px 14px 14px;
  font-size: 15px;
  line-height: 1.5;
}

/* .msg-bot defaults to align-self: flex-start which would shrink
   the bubble against the card. Stretch it so the body fills the
   card width — same flow the typewriter expects. */
.dpo-card-body.msg-bot {
  background: transparent;
  align-self: stretch;
}

/* Pick reaction states — chosen briefly glows, rejected fades
   and collapses upward. Both transitions complete in ~420ms;
   the JS waits that long before tearing down the wrap. */
.dpo-card.chosen {
  background: rgba(124, 211, 255, 0.12);
  border-color: rgba(124, 211, 255, 0.55);
  box-shadow: 0 0 18px rgba(124, 211, 255, 0.22);
}

.dpo-card.rejected {
  opacity: 0;
  transform: translateY(-6px) scale(0.98);
  pointer-events: none;
}

/* Narrow viewport — stack cards vertically. The grid behavior
   is the same, just one column. */
@media (max-width: 700px) {
  .dpo-card-grid {
    grid-template-columns: 1fr;
  }
}

@media (prefers-reduced-motion: reduce) {
  .dpo-pair-header,
  .dpo-card {
    animation: none;
    transition: opacity 0.2s ease;
    transform: none !important;
  }
}


/* ============================================================
   SPEAK-OUT BUTTON (per-message TTS replay)
   ============================================================
   Sits in the bot action row between thumbs-down and the more-menu.
   Three SVG icons live as siblings inside the button — CSS picks
   which one is visible based on .loading / .playing classes
   toggled by speakMessageOnDemand().

   The settings Voice-preview button (.settings-voice-preview) reuses
   this exact markup and opts into these same rules, so the spinner
   and the playing waves are one implementation rather than two that
   can drift apart.
*/

.bot-speak-btn .speak-icon,
.settings-voice-preview .speak-icon {
  width: 16px;
  height: 16px;
  display: none;
}

/* Idle state — default speaker icon visible. */
.bot-speak-btn .speak-icon-default,
.settings-voice-preview .speak-icon-default {
  display: block;
}

/* Loading state — spinner instead of speaker. Spins until the
   Kokoro fetch completes and audio.onplaying fires. */
.bot-speak-btn.loading .speak-icon-default,
.bot-speak-btn.loading .speak-icon-playing,
.settings-voice-preview.loading .speak-icon-default,
.settings-voice-preview.loading .speak-icon-playing {
  display: none;
}
.bot-speak-btn.loading .speak-icon-loading,
.settings-voice-preview.loading .speak-icon-loading {
  display: block;
  animation: bot-speak-spin 0.8s linear infinite;
}

/* Playing state — speaker icon with pulsing sound waves so the
   user can see at a glance which message is currently speaking. */
.bot-speak-btn.playing .speak-icon-default,
.bot-speak-btn.playing .speak-icon-loading,
.settings-voice-preview.playing .speak-icon-default,
.settings-voice-preview.playing .speak-icon-loading {
  display: none;
}
.bot-speak-btn.playing .speak-icon-playing,
.settings-voice-preview.playing .speak-icon-playing {
  display: block;
  color: var(--accent, #7cd3ff);
}
.bot-speak-btn.playing .speak-wave-1,
.settings-voice-preview.playing .speak-wave-1 {
  animation: bot-speak-wave 1.1s ease-in-out infinite;
}
.bot-speak-btn.playing .speak-wave-2,
.settings-voice-preview.playing .speak-wave-2 {
  animation: bot-speak-wave 1.1s ease-in-out 0.18s infinite;
}

.bot-speak-btn:disabled {
  cursor: default;
}

@keyframes bot-speak-spin {
  to { transform: rotate(360deg); }
}

@keyframes bot-speak-wave {
  0%, 100% { opacity: 0.45; }
  50%      { opacity: 1;    }
}

/* ============================================================
   MIC BUTTON (speech-to-text input)
   ============================================================
   Injected into the input bar just before the send button.
   Idle: neutral; recording: cyan-tinted with a soft pulsing ring
   that signals "this is live."
*/

.mic-btn {
  appearance: none;
  -webkit-appearance: none;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  border: none;
  background: transparent;
  padding: 0;
  margin: 0;
  cursor: pointer;
  color: var(--text-soft);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition:
    background 0.2s ease,
    color 0.2s ease,
    filter 0.2s ease,
    box-shadow 0.25s ease;
}

.mic-btn svg {
  width: 22px;
  height: 22px;
}

.mic-btn:hover {
  /* Match the send button's hover language — same background it has
     at rest, so the two read as a coherent pair on the right side of
     the input bar. */
  background: var(--accent_send);
  color: var(--text-main, #f2f2f2);
  filter: brightness(1.15);
}

.mic-btn:active {
  transform: scale(0.95);
}

.mic-btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(124, 211, 255, 0.22);
}

.mic-btn.recording {
  color: #7cd3ff;
  background: rgba(124, 211, 255, 0.14);
  animation: mic-pulse 1.6s ease-in-out infinite;
}

@keyframes mic-pulse {
  0%, 100% {
    box-shadow: 0 0 0 0 rgba(124, 211, 255, 0.42);
  }
  60%, 100% {
    box-shadow: 0 0 0 10px rgba(124, 211, 255, 0);
  }
}

/* ============================================================
   MIC WAVEFORM CANVAS
   ============================================================
   Sits above the input bar (between chatBox and inputBar).
   Hidden by default — slides down to 56px tall when .active is
   added at recording start. Canvas itself is full-width; the
   three-layer waveform render is driven by the AnalyserNode in
   _startWaveformLoop().
*/

.mic-waveform-wrap {
  /* Floating element anchored directly above the input bar (which is
     this element's positioned ancestor — .input-bar is position:absolute
     so it forms a containing block). Centered horizontally with a max
     width so the wave doesn't span edge-to-edge; that's what gives it
     the "floating shape hovering above the input" feel from the
     reference image vs. an oscilloscope strip. */
  position: absolute;
  bottom: 100%;
  left: 0;
  right: 0;
  margin: 0 auto 10px;
  width: 92%;
  max-width: 480px;
  height: 0;
  opacity: 0;
  overflow: hidden;
  pointer-events: none;
  transition:
    height 0.45s cubic-bezier(0.22, 0.61, 0.36, 1),
    opacity 0.40s ease;
}

.mic-waveform-wrap.active {
  height: 80px;
  opacity: 1;
}

.mic-waveform-canvas {
  width: 100%;
  height: 100%;
  display: block;
}

@media (prefers-reduced-motion: reduce) {
  .mic-btn,
  .mic-btn.recording,
  .bot-speak-btn .speak-icon-loading,
  .bot-speak-btn .speak-wave-1,
  .bot-speak-btn .speak-wave-2,
  .settings-voice-preview .speak-icon-loading,
  .settings-voice-preview .speak-wave-1,
  .settings-voice-preview .speak-wave-2,
  .mic-waveform-wrap {
    animation: none !important;
    transition: opacity 0.2s ease !important;
  }
}

/* ============================================================
   PROFILE MENU — COMPACT LAYOUT
   ============================================================
   The 3-dot menu was previously a flat list of full-width rows:
   three preset rows, a hairline divider, then four two-line model
   rows on mobile. That ran ~344px tall and had no labelling, so
   the grouping was implicit.

   The compact layout uses labelled sections with segmented
   controls for the two settings axes (style, effort) and a
   single-line list for model selection. Roughly 210px on mobile
   for the same functionality.
*/

.profile-menu {
  width: 268px;
  padding: 8px;
}

/* ── Section headers ── */

.menu-section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  padding: 6px 4px 5px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-soft);
  opacity: 0.75;
}

.menu-section-head:not(:first-child) {
  margin-top: 4px;
  border-top: 1px solid var(--border-main);
  padding-top: 10px;
}

.menu-section-note {
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.03em;
  text-transform: none;
  opacity: 0.7;
  padding: 1px 6px;
  border-radius: 8px;
  background: color-mix(in srgb, var(--text-soft) 14%, transparent);
  color: var(--text-soft);
}

/* ── Segmented control (presets + effort) ── */

.menu-segmented {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 4px;
  padding: 2px;
  background: color-mix(in srgb, var(--text-soft) 8%, transparent);
  border-radius: 9px;
}

.menu-seg-btn {
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: none;
  border-radius: 7px;
  padding: 7px 4px 6px;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  color: var(--text-soft);
  font-family: inherit;
  font-size: 11.5px;
  font-weight: 500;
  line-height: 1.1;
  transition: background 0.16s ease, color 0.16s ease;
  min-width: 0;
}

.menu-seg-btn:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--text-main);
}

.menu-seg-btn.active {
  background: color-mix(in srgb, var(--accent) 20%, transparent);
  color: var(--accent);
  font-weight: 600;
}

.menu-seg-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 18px;
}

.menu-seg-icon svg {
  width: 16px;
  height: 16px;
  color: currentColor;
}

.menu-seg-label {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

/* Disabled group — effort on models that don't support it.
   Kept visible rather than hidden so the control is discoverable
   and the "Epos only" note explains the gating. */
.menu-segmented.is-disabled {
  opacity: 0.42;
}
.menu-segmented.is-disabled .menu-seg-btn {
  cursor: default;
}
.menu-segmented.is-disabled .menu-seg-btn.active {
  background: color-mix(in srgb, var(--text-soft) 16%, transparent);
  color: var(--text-soft);
}

/* ── Compact model list (mobile only) ── */

.menu-model-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 2px;
}

.menu-model-item {
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: none;
  border-radius: 7px;
  padding: 8px 10px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  color: var(--text-main);
  font-family: inherit;
  font-size: 13.5px;
  font-weight: 500;
  text-align: left;
  transition: background 0.16s ease, color 0.16s ease;
}

.menu-model-item:hover {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

.menu-model-item.active {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent);
  font-weight: 600;
}

.menu-model-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.menu-model-size {
  flex-shrink: 0;
  font-size: 11px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  opacity: 0.6;
  padding: 1px 7px;
  border-radius: 8px;
  background: color-mix(in srgb, var(--text-soft) 12%, transparent);
  color: var(--text-soft);
}

.menu-model-item.active .menu-model-size {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent);
  opacity: 0.95;
}

/* ── Narrow phones ── */

@media (max-width: 380px) {
  .profile-menu { width: 244px; }
  .menu-seg-btn { font-size: 11px; padding: 6px 3px 5px; }
  .menu-model-item { font-size: 13px; padding: 7px 9px; }
}


/* ============================================================
   INLINE SESSION RENAME
   ============================================================
   Pencil button in each session row (desktop sidebar + mobile
   session panel), sitting immediately before the delete button.
   Clicking swaps the title for a text input and the pencil for
   a checkmark.
*/

/* ── Rename button, desktop sidebar ── */

.sidebar-entry-rename {
  -webkit-appearance: none;
  appearance: none;
  border: none;
  background: transparent;
  color: var(--text-soft);
  width: 26px;
  height: 26px;
  border-radius: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
  opacity: 0;
  transition: opacity 0.15s ease, background 0.12s ease, color 0.12s ease;
}

.sidebar-entry:hover .sidebar-entry-rename,
.sidebar-entry.active .sidebar-entry-rename {
  opacity: 0.7;
}

.sidebar-entry-rename:hover {
  opacity: 1 !important;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}

/* ── Rename button, mobile session panel ── */

.session-entry-rename {
  -webkit-appearance: none;
  appearance: none;
  border: none;
  background: transparent;
  color: var(--text-soft);
  width: 28px;
  height: 28px;
  border-radius: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
  opacity: 0.55;
  transition: background 0.12s ease, color 0.12s ease, opacity 0.12s ease;
}

.session-entry-rename:hover {
  opacity: 1;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}

/* ── Icon swap: pencil ⇄ checkmark ── */

.sidebar-entry-rename .rename-icon-check,
.session-entry-rename .rename-icon-check {
  display: none;
}

.sidebar-entry-rename.editing .rename-icon-pencil,
.session-entry-rename.editing .rename-icon-pencil {
  display: none;
}

.sidebar-entry-rename.editing .rename-icon-check,
.session-entry-rename.editing .rename-icon-check {
  display: block;
}

/* While editing, pin the button visible and tint it as the
   affirmative action — it's now "save", not a passive affordance. */
.sidebar-entry-rename.editing,
.session-entry-rename.editing {
  opacity: 1 !important;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 18%, transparent);
}

/* ── The inline text input ── */

.session-rename-input {
  flex: 1;
  min-width: 0;
  -webkit-appearance: none;
  appearance: none;
  background: var(--bg-input, rgba(255, 255, 255, 0.06));
  border: 1px solid var(--accent);
  border-radius: 6px;
  color: var(--text-main);
  font-family: inherit;
  font-size: 13.5px;
  font-weight: 500;
  line-height: 1.3;
  padding: 4px 8px;
  margin: -2px 0;
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}

.session-rename-input::selection {
  background: color-mix(in srgb, var(--accent) 35%, transparent);
}

/* In the mobile panel the title sits inside .session-line above a
   preview row, so the input needs to fill that line's width rather
   than the whole row. */
.session-line .session-rename-input {
  width: 100%;
  font-size: 14px;
}

@media (max-width: 600px) {
  .session-rename-input {
    /* 16px prevents iOS Safari from zooming the viewport on focus. */
    font-size: 16px;
  }
  .session-line .session-rename-input {
    font-size: 16px;
  }
}
/* ============================================================
   DESKTOP: HERO LOGO FADES INSTEAD OF DOCKING
   ============================================================
   On mobile the centred logo slides up and becomes the top header bar
   (that behaviour is untouched). On desktop the sidebar already carries
   the brand and the model picker sits top-left, so a docked header is
   redundant — the hero logo simply fades out in place when a chat
   starts.

   Keeping position:absolute here is the important part: the idle state
   is already absolutely positioned, so overriding .active back to the
   same geometry means the element fades without any movement, and it
   stays out of document flow so it can't reserve space or push the
   conversation down.

   Placed at the end of the file so it wins over the base #titleBar.active
   rule, which has identical specificity — media queries add none.
   ============================================================ */

@media (min-width: 900px) {
  #titleBar.active {
    position: absolute;
    top: 38%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 100%;
    margin-bottom: 0;

    opacity: 0;
    visibility: hidden;
    pointer-events: none;

    /* Strip the docked-header treatment entirely. */
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    box-shadow: none;

    /* Fade out, then drop from the a11y tree once invisible. */
    transition: opacity 0.45s ease, visibility 0s linear 0.45s;
  }

  /* With no docked header above it, the conversation needs its own
     clearance so the first message doesn't ride up under the fixed
     model picker in the top-left. */
  .chat-box.padded {
    padding-top: 62px;
  }
}

@media (min-width: 900px) and (prefers-reduced-motion: reduce) {
  #titleBar.active {
    transition: none;
  }
}

/* ============================================================
   FONT THEMES
   ============================================================
   Applies to Haiku's replies only — the user's own bubbles, the UI
   chrome and code blocks are untouched, so the setting reads as "how
   Haiku speaks" rather than a global skin.

   Each face needs its own metric correction. Dropping three typefaces
   into one rule at identical size and tracking would leave the serif
   cramped and OpenDyslexic oversized, because they differ sharply in
   x-height and default letter width.
   ============================================================ */

/* Sleek (default) — Inter. Matches the UI; the bubble's own -0.025rem
   tracking already suits it, so this is a no-op made explicit so the
   theme is switchable back without relying on cascade removal. */
body[data-font-theme="sleek"] .msg-bot {
  font-family: "mplus", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
  letter-spacing: 0.01rem;
  line-height: 1.55;
}

/* Stylish — Roboto Serif. Serifs need air: the negative tracking
   inherited from .msg-bot closes up the serif brackets and makes it
   look muddy, so tracking goes to zero and leading opens slightly.
   Roboto Serif also runs a touch small at a given px, hence the bump. */
body[data-font-theme="stylish"] .msg-bot {
  font-family: "robotoserif", Georgia, "Times New Roman", serif;
  font-size: 1.02em;
  letter-spacing: 0;
  line-height: 1.62;
  font-weight: 400;
}

/* Dyslexic — OpenDyslexic. The face already builds in generous letter and
   word spacing (that spacing is the accessibility feature), so the job
   here is to stop the bubble's own tracking from fighting it, not to add
   more: an earlier pass layered extra letter- and word-spacing on top and
   the words floated apart into disconnected clumps. Tracking is therefore
   neutralised rather than opened, and the size pulled back slightly
   because the face carries a large x-height and runs wide. */
body[data-font-theme="dyslexic"] .msg-bot {
  font-family: "OpenDyslexic", "Comic Sans MS", Verdana, sans-serif;
  font-size: 0.95em;
  letter-spacing: 0;
  word-spacing: normal;
  line-height: 1.70;
  font-weight: 400;
}

/* Code keeps its monospace face in every theme — it sets font-family on
   its own element, but size/leading are inherited, so they're reset. */
body[data-font-theme] .msg-bot .bot-code-block,
body[data-font-theme] .msg-bot code {
  font-size: 13px;
  letter-spacing: 0;
  word-spacing: normal;
  line-height: 1.5;
}

/* ---- Font theme selector ---- */

.settings-font-theme-group {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px;
  border: 1px solid var(--border-main);
  border-radius: 999px;
  background: color-mix(in srgb, var(--text-main) 4%, transparent);
  flex-shrink: 0;
}

/* Each button previews its own theme, so the label is set in the face
   it selects. That means the three labels have wildly different optical
   sizes at the same px, so each is corrected individually to keep the
   pill row visually even. */
.settings-font-theme-btn {
  -webkit-appearance: none;
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--text-soft);
  font-size: 12.5px;
  font-weight: 500;
  padding: 6px 10px;
  border-radius: 999px;
  cursor: pointer;
  line-height: 1;
  white-space: nowrap;
  transition:
    background 0.14s ease,
    color 0.14s ease,
    opacity 0.14s ease;
}

.settings-font-theme-btn[data-font-theme="sleek"] {
  font-family: "mplus", system-ui, sans-serif;
  letter-spacing: 0.01em;
}

.settings-font-theme-btn[data-font-theme="stylish"] {
  font-family: "robotoserif", Georgia, serif;
  font-size: 13px;          /* serif reads smaller at the same px */
  letter-spacing: 0.005em;
}

.settings-font-theme-btn[data-font-theme="dyslexic"] {
  font-family: "OpenDyslexic", "Comic Sans MS", Verdana, sans-serif;
  font-size: 11.5px;        /* runs wide and tall, so pulled back */
  letter-spacing: -0.01em;
}

.settings-font-theme-btn:hover {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--text-main);
}

/* Active pill matches the canvas theme group exactly. --accent resolves
   to a neutral grey in dark mode, so the purple is the same hardcoded
   #6366f1 the canvas buttons use, applied via the same light/dark split
   rather than a second, subtly different treatment. */
.settings-font-theme-btn.active {
  color: var(--bg-main);
  background: var(--text-main);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.18);
  font-weight: 600;
}

body:not(.light) .settings-font-theme-btn.active {
  color: #ffffff;
  background: #6366f1;
}

.settings-font-theme-btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}

@media (max-width: 560px) {
  .settings-row-font-theme {
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
  }

  .settings-font-theme-group {
    width: 100%;
    box-sizing: border-box;
  }

  .settings-font-theme-btn {
    flex: 1;
    text-align: center;
  }
}

/* ============================================================
   VOICE PICKER POPUP
   ============================================================
   Replaces the native <select> popup, which can't be styled and was
   the one piece of OS chrome left in the settings panel. Borrows the
   desktop model menu's vocabulary — same radius, border, shadow and
   entry transition — so it reads as part of the same system.

   z-index sits above .settings-modal (9999) because it renders outside
   the modal to escape its overflow:hidden.
   ============================================================ */

.settings-select-native {
  display: none;
}

.settings-select-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.voice-menu {
  position: fixed;
  top: 0;
  left: 0;
  /* Without this, the max-height set by positionVoiceMenu() applies to the
     content box only, so the menu rendered 12px taller than the space it
     was sized for (5px padding + 1px border, doubled) and the viewport
     clamp then dragged it up over its own trigger. */
  box-sizing: border-box;
  z-index: 10001;
  width: 218px;
  max-height: 320px;
  padding: 5px;
  border-radius: 13px;
  border: 1px solid var(--border-main);
  background: var(--bg-panel);
  box-shadow:
    0 10px 30px rgba(0, 0, 0, 0.32),
    0 1px 0 rgba(255, 255, 255, 0.02) inset;
  opacity: 0;
  transform: translateY(-6px) scale(0.985);
  transform-origin: top left;
  pointer-events: none;
  visibility: hidden;
  transition:
    opacity 0.17s ease,
    transform 0.17s cubic-bezier(0.22, 1, 0.36, 1),
    visibility 0s linear 0.17s;

  /* The shell keeps the border, background and radius and never scrolls,
     so the edge fades below can't eat its rounded corners. Scrolling and
     masking both live on the inner .voice-menu-scroll. */
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.voice-menu-scroll {
  overflow-y: auto;
  flex: 1 1 auto;
  min-height: 0;
  overscroll-behavior: contain;

  /* Scrollbar hidden to match the settings panel. The list is still fully
     scrollable by wheel, trackpad, touch and keyboard; the edge fades
     below stand in as the "there's more" cue. overscroll-behavior stops
     a wheel gesture that reaches the end from scrolling the page behind. */
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.voice-menu-scroll::-webkit-scrollbar {
  width: 0;
  height: 0;
  display: none;
}

/* Edge fades. Applied as a mask so they sit over the content without
   needing an overlay element that would scroll along with the list.
   JS toggles each side independently, so a list that fits shows none. */
.voice-menu-scroll.fade-top {
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 18px);
  mask-image: linear-gradient(to bottom, transparent 0, #000 18px);
}

.voice-menu-scroll.fade-bottom {
  -webkit-mask-image: linear-gradient(to top, transparent 0, #000 18px);
  mask-image: linear-gradient(to top, transparent 0, #000 18px);
}

.voice-menu-scroll.fade-top.fade-bottom {
  -webkit-mask-image: linear-gradient(
    to bottom, transparent 0, #000 18px, #000 calc(100% - 18px), transparent 100%);
  mask-image: linear-gradient(
    to bottom, transparent 0, #000 18px, #000 calc(100% - 18px), transparent 100%);
}

.voice-menu.open {
  opacity: 1;
  transform: translateY(0) scale(1);
  pointer-events: auto;
  visibility: visible;
  transition:
    opacity 0.17s ease,
    transform 0.17s cubic-bezier(0.22, 1, 0.36, 1),
    visibility 0s;
}

/* Flips above the trigger when there isn't room below. */
.voice-menu.flip-up {
  transform-origin: bottom left;
  transform: translateY(6px) scale(0.985);
}
.voice-menu.flip-up.open {
  transform: translateY(0) scale(1);
}

.voice-menu-group-label {
  padding: 8px 10px 4px 10px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-soft);
  opacity: 0.75;
  user-select: none;
}

.voice-menu-group + .voice-menu-group .voice-menu-group-label {
  border-top: 1px solid var(--border-main);
  margin-top: 4px;
}

.voice-menu-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px 8px 14px;
  border-radius: 9px;
  font-size: 13.5px;
  font-weight: 500;
  color: var(--text-main);
  cursor: pointer;
  transition: background 0.14s ease;
}

.voice-menu-item:hover,
.voice-menu-item.focused {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

.voice-menu-item.active {
  background: color-mix(in srgb, var(--accent) 15%, transparent);
}

.voice-menu-item.active::after {
  content: "";
  position: absolute;
  top: 8px;
  bottom: 8px;
  left: 5px;
  width: 3px;
  border-radius: 3px;
  background: var(--accent);
}

.voice-menu-item-check {
  margin-left: auto;
  opacity: 0;
  color: var(--accent);
  flex-shrink: 0;
}

.voice-menu-item.active .voice-menu-item-check {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .voice-menu {
    transition: opacity 0.12s ease, visibility 0s linear 0.12s;
    transform: none;
  }
  .voice-menu.open {
    transform: none;
    transition: opacity 0.12s ease, visibility 0s;
  }
}
/* ============================================================
   MESSAGE CONTROLS — REVEAL ON HOVER
   ============================================================
   The per-message controls (copy / rate / speak / more under bot
   replies, and the 3-dot under the last user message) stay hidden
   until their message is hovered, so a finished conversation reads
   as clean text rather than rows of icons.

   Scoped to `@media (hover: hover) and (pointer: fine)`. On touch
   there is no hover state to reveal them with, so phones and tablets
   keep the existing always-visible behaviour — otherwise the controls
   would be unreachable there.
   ============================================================ */

@media (hover: hover) and (pointer: fine) {

  /* ---- Bot replies ---- */

  /* Scoped to data-msg-role="bot" so error rows keep their retry button
     visible at all times — an error is actionable and shouldn't require
     hunting for the control.

     :not(.pending) preserves the existing generating state. .pending
     already hides the row while a reply streams in, and without this
     exclusion the hover rule (higher specificity) would win and pop the
     controls up mid-generation. */
  .bot-message-wrap[data-msg-role="bot"] .bot-actions:not(.pending) {
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s ease;
  }

  .bot-message-wrap[data-msg-role="bot"]:hover .bot-actions:not(.pending),
  /* Keyboard users must still be able to tab to the controls. */
  .bot-message-wrap[data-msg-role="bot"]:focus-within .bot-actions:not(.pending),
  /* An open menu, or audio that is loading or playing, keeps the row
     visible even if the pointer wanders off the message. */
  .bot-message-wrap[data-msg-role="bot"] .bot-actions:not(.pending):has(.bot-feedback-menu.open),
  .bot-message-wrap[data-msg-role="bot"] .bot-actions:not(.pending):has(.bot-speak-btn.playing),
  .bot-message-wrap[data-msg-role="bot"] .bot-actions:not(.pending):has(.bot-speak-btn.loading) {
    opacity: 1;
    pointer-events: auto;
  }

  /* ---- User messages ---- */

  /* .show still gates *eligibility* (only the most recent user message
     can be edited or resent); this adds hover on top of it, so the
     3-dot appears only when that message is hovered. */
  .user-message-wrap .user-actions.show {
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s ease;
  }

  .user-message-wrap:hover .user-actions.show,
  .user-message-wrap:focus-within .user-actions.show,
  .user-message-wrap .user-actions.show:has(.user-feedback-menu.open) {
    opacity: 1;
    pointer-events: auto;
  }
}

@media (prefers-reduced-motion: reduce) {
  .bot-actions,
  .user-actions {
    transition: none !important;
  }
}

/* ============================================================
   CONVERSATION SEARCH + CHAT OUTLINE
   ============================================================
   Both controls are desktop-only, matching .sidebar-toggle, which is
   display:none until >=900px. Enabling them on mobile would collide
   with the dropdown header and the sidebar doesn't exist there anyway.
   ============================================================ */

.sidebar-search-btn {
  position: fixed;
  top: 14px;
  left: 200px;             /* immediately left of the toggle at 234px */
  z-index: 41;
  width: 30px;
  height: 30px;
  display: none;           /* desktop-only, enabled below */
  align-items: center;
  justify-content: center;
  padding: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
  color: var(--text-soft);
  cursor: pointer;
  transition: opacity 0.2s ease, background 0.15s ease,
              color 0.15s ease, visibility 0s;
}

.sidebar-search-btn:hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--text-main);
}

.sidebar-search-btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}

/* Search belongs to the sidebar, so it goes away with it. The toggle
   slides to the viewport edge when collapsed and would otherwise
   overlap this button. */
body.sidebar-collapsed .sidebar-search-btn {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.18s ease, visibility 0s linear 0.18s;
}

/* ---- Outline toggle: mirrors the sidebar toggle on the right ---- */

.thread-toggle {
  position: fixed;
  top: 14px;
  right: 14px;
  z-index: 41;
  width: 30px;
  height: 30px;
  display: none;           /* desktop-only, enabled below */
  align-items: center;
  justify-content: center;
  padding: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
  color: var(--text-soft);
  cursor: pointer;
  transition: right 0.28s ease, background 0.15s ease, color 0.15s ease;
}

.thread-toggle:hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--text-main);
}

.thread-toggle:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}

.thread-toggle.open {
  color: var(--text-main);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}

/* Rides the panel edge when open, the way the sidebar toggle does. */
body.thread-open .thread-toggle {
  right: 286px;
}

/* ---- Outline panel ---- */

.thread-panel {
  position: fixed;
  top: 0;
  right: 0;
  width: 272px;
  height: 100vh;
  height: 100dvh;
  z-index: 40;
  display: none;           /* desktop-only, enabled below */
  flex-direction: column;
  background: var(--bg-panel);
  border-left: 1px solid var(--border-main);
  transform: translateX(100%);
  transition: transform 0.28s ease;
}

/* The closed panel parks itself one full width beyond the right edge,
   and a fixed element sitting outside the viewport still counts toward
   the document's scrollable width — the page became 273px wider than
   the window. One trackpad swipe then scrolled the whole layout left
   and revealed the parked panel, which looked exactly like the outline
   opening by itself with the conversation shoved off screen.

   `clip` rather than `hidden`: clip prevents the scroll without turning
   the element into a scroll container, so it can't interfere with the
   fixed rails or with vertical scrolling. */
html {
  overflow-x: clip;
}

@supports not (overflow-x: clip) {
  html {
    overflow-x: hidden;
  }
}

body.thread-open .thread-panel {
  transform: translateX(0);
}

.thread-panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 58px;
  padding: 0 12px 0 16px;
  border-bottom: 1px solid var(--border-main);
  flex-shrink: 0;
}

.thread-panel-title {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--text-main);
}

.thread-close {
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  background: transparent;
  color: var(--text-soft);
  border-radius: 7px;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}

.thread-close:hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--text-main);
}

.thread-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 8px;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.thread-list::-webkit-scrollbar { width: 0; height: 0; display: none; }

.thread-item {
  display: block;
  width: 100%;
  text-align: left;
  border: 0;
  background: transparent;
  color: var(--text-soft);
  font: inherit;
  font-size: 12.5px;
  line-height: 1.45;
  padding: 8px 10px 8px 20px;
  border-radius: 9px;
  cursor: pointer;
  position: relative;
  transition: background 0.14s ease, color 0.14s ease;
}

/* Two-line clamp keeps long turns from dominating the rail. Applied to
   this span rather than the button — see buildThreadOutline(). */
.thread-item-text {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.thread-item:hover {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--text-main);
}

/* Role marker: a small dot in the left gutter. User turns use the
   accent, bot turns a muted tone, so the conversation's rhythm is
   readable at a glance without labels taking up width. */
.thread-item::before {
  content: "";
  position: absolute;
  left: 8px;
  top: 14px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--text-soft);
  opacity: 0.5;
}

.thread-item.is-user::before {
  background: #6366f1;
  opacity: 0.9;
}

.thread-item.is-user {
  color: var(--text-main);
  font-weight: 500;
}

.thread-empty {
  padding: 14px 12px;
  font-size: 12.5px;
  color: var(--text-soft);
  opacity: 0.8;
}

/* Brief highlight on the message that was jumped to, so the eye can
   find it after the scroll lands. */

/* Bot replies have a transparent background and fill the column, so the
   wrapper's own background can be animated directly. */
.bot-message-wrap.jump-flash {
  animation: jump-flash 1.25s ease-out;
  border-radius: 12px;
}

@keyframes jump-flash {
  0%   { background: color-mix(in srgb, var(--accent) 26%, transparent); }
  100% { background: transparent; }
}

/* The user bubble already paints its own background, so animating
   background-color here would blow it away for the duration of the flash
   and leave the bubble looking transparent until the class is removed.
   A fading overlay tints it instead, leaving the bubble's own fill
   intact, and inherits the radius so it matches the bubble's shape. */
.msg-user.jump-flash {
  position: relative;
}

.msg-user.jump-flash::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: color-mix(in srgb, var(--accent) 30%, transparent);
  animation: jump-flash-fade 1.25s ease-out forwards;
  pointer-events: none;
}

@keyframes jump-flash-fade {
  0%   { opacity: 1; }
  100% { opacity: 0; }
}

/* ---- Search modal ---- */

.search-modal {
  /* Inherits .settings-modal's shell (position, transform, shadow,
     open transition); only the internals differ. */
  max-height: min(560px, calc(100% - 96px));
}

.search-field {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 14px 14px 18px;
  border-bottom: 1px solid var(--border-main);
  flex-shrink: 0;
}

.search-field-icon {
  color: var(--text-soft);
  flex-shrink: 0;
}

.search-input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: transparent;
  color: var(--text-main);
  font: inherit;
  font-size: 15px;
  padding: 2px 0;
  outline: none;
}

.search-input::placeholder {
  color: var(--text-soft);
  opacity: 0.85;
}

.search-results {
  overflow-y: auto;
  padding: 6px;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.search-results::-webkit-scrollbar { width: 0; height: 0; display: none; }

.search-result {
  display: block;
  width: 100%;
  text-align: left;
  border: 0;
  background: transparent;
  color: var(--text-main);
  font: inherit;
  padding: 10px 12px;
  border-radius: 10px;
  cursor: pointer;
  transition: background 0.14s ease;
}

.search-result:hover,
.search-result.focused {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

.search-result.active-session {
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}

.search-result-title {
  font-size: 13.5px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.search-result-snippet {
  margin-top: 3px;
  font-size: 12px;
  color: var(--text-soft);
  line-height: 1.45;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.search-result-meta {
  margin-top: 4px;
  font-size: 11px;
  color: var(--text-soft);
  opacity: 0.75;
}

.search-hit {
  color: var(--text-main);
  background: color-mix(in srgb, #6366f1 30%, transparent);
  border-radius: 3px;
  padding: 0 2px;
}

.search-empty {
  padding: 22px 14px;
  text-align: center;
  font-size: 13px;
  color: var(--text-soft);
}

@media (min-width: 900px) {
  .sidebar-search-btn,
  .thread-toggle {
    display: flex;
  }
  .thread-panel {
    display: flex;
  }

  /* Reserve space rather than overlaying. The sidebar does the same with
     padding-left; without the mirror image here the panel slid over the
     conversation and clipped the right-aligned user bubbles. */
  body {
    transition: padding-left 0.28s ease, padding-right 0.28s ease;
  }

  body.thread-open {
    padding-right: 272px;
    box-sizing: border-box;
  }
}

@media (prefers-reduced-motion: reduce) {
  .thread-panel,
  .thread-toggle,
  .sidebar-search-btn {
    transition: none;
  }
  .bot-message-wrap.jump-flash,
  .msg-user.jump-flash::after {
    animation: none;
  }
}
/* ============================================================
   COLLAPSED-SIDEBAR RAIL
   ============================================================
   A vertical stack descending from the sidebar toggle, visible only
   while the sidebar is hidden. Desktop-only, matching .sidebar-toggle.

   Positioned to line up with the toggle's collapsed coordinates
   (left:14px, top:14px, 30x30) so the column reads as one continuous
   run of controls.
   ============================================================ */

.rail-nav {
  position: fixed;
  left: 14px;
  top: 52px;               /* toggle top (14) + toggle height (30) + 8 gap */
  z-index: 41;
  display: none;           /* enabled on desktop below */
  flex-direction: column;
  gap: 4px;

  /* Hidden by default: the rail belongs to the collapsed state, and the
     sidebar starts open. visibility is delayed until the fade finishes
     so the buttons aren't tabbable while invisible. */
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateX(-6px);
  transition:
    opacity 0.22s ease,
    transform 0.22s ease,
    visibility 0s linear 0.22s;
}

body.sidebar-collapsed .rail-nav {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translateX(0);
  transition:
    opacity 0.22s ease 0.06s,
    transform 0.22s ease 0.06s,
    visibility 0s;
}

.rail-btn {
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 8px;
  background: transparent;
  color: var(--text-soft);
  cursor: pointer;
  text-decoration: none;
  transition: background 0.15s ease, color 0.15s ease;
}

.rail-btn:hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--text-main);
}

.rail-btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}

/* ============================================================
   CHAT TOP SCROLL FADE
   ============================================================
   Messages dissolve into the background as they scroll up past the
   toolbar row, rather than being cut off by a hard edge.

   Implemented as an absolutely positioned sibling of .chat-box inside
   .app: a ::before on .chat-box itself would scroll away with the
   content, and .app is the nearest non-scrolling ancestor.
   ============================================================ */

.app {
  position: relative;
}

.chat-top-fade {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 74px;            /* ends just past the 14px + 30px toolbar row */
  z-index: 5;              /* over messages, under the toggles (z:41) */
  pointer-events: none;    /* must never intercept clicks or scrolling */

  /* Hidden in the idle state. There it would sit over the animated
     canvas rather than over messages, and because the gradient is
     opaque at the top it painted a visible band of flat background
     across the artwork. The fade only has a job once messages exist to
     scroll underneath it. */
  opacity: 0;
  transition: opacity 0.35s ease;

  /* Opaque at the very top so text is fully hidden as it passes under
     the toolbar, easing to transparent. The mid stop keeps the ramp
     weighted toward the top rather than fading linearly across the
     whole band, which reads as a haze over the messages. */
  background: linear-gradient(
    to bottom,
    var(--bg-main) 0%,
    color-mix(in srgb, var(--bg-main) 82%, transparent) 45%,
    transparent 100%
  );
}

/* .padded is added exactly when the UI leaves the centred idle state and
   the canvas is faded out, and it is never removed for the life of the
   page — which makes it the marker that matches the canvas's own
   visibility. Sibling selector because .chat-top-fade follows .chat-box. */
.chat-box.padded ~ .chat-top-fade {
  opacity: 1;
}

@media (min-width: 900px) {
  .rail-nav {
    display: flex;
  }
}

@media (prefers-reduced-motion: reduce) {
  .rail-nav {
    transition: opacity 0.1s ease, visibility 0s;
    transform: none;
  }
  body.sidebar-collapsed .rail-nav {
    transform: none;
  }
}
/* ============================================================
   WEB SEARCH TOGGLE
   ============================================================
   Inherits .mic-btn wholesale so the two sit as a matched pair at
   rest. The only departures are the active state and a slightly
   lighter stroke, since a globe carries more line weight than a
   microphone at the same size.
*/
.web-search-btn svg {
  width: 21px;
  height: 21px;
}

.web-search-btn.active {
  color: #7cd3ff;
  background: rgba(124, 211, 255, 0.14);
}

.web-search-btn.active:hover {
  background: rgba(124, 211, 255, 0.2);
  filter: none;
}

/* ============================================================
   SOURCE DISCLOSURE — STEPS
   ============================================================
   An ordered account of what Epos did: the search, each thing read,
   then any caveat.

   Entirely neutral. An earlier version tinted the web tier cyan to
   separate it from knowledge base results, which put saturated colour
   on every link in the panel and pulled the eye away from the reply
   itself. The two tiers already say which they are in words, so the
   colour was carrying information that was never missing.

   The rail is a border on the container and each step draws a short
   tick out to its text. Dots positioned on top of the border got cut
   in half by it and drifted whenever a title wrapped.
*/
.bot-source-disclosure {
  margin-left: 75px;
  margin-bottom: 10px;
  max-width: calc(100% - 90px);
  font-size: 12px;
  line-height: 1.5;
}

.bot-source-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text-soft);
  font: inherit;
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  transition: color 0.15s ease;
}

.bot-source-toggle:hover { color: var(--text-main); }

.bot-source-caret {
  display: inline-block;
  font-size: 10px;
  opacity: 0.7;
  transition: transform 160ms ease;
}

.bot-source-disclosure.is-open .bot-source-caret {
  transform: rotate(90deg);
}

/* [hidden] is a UA rule of `display: none`, which a later `display: flex`
   in this stylesheet outranks -- so the panel rendered open regardless of
   the property. Restated here at matching specificity. */
.bot-source-steps[hidden] {
  display: none;
}

.bot-source-steps {
  margin: 8px 0 0 4px;
  padding: 2px 0;
  border-left: 1px solid var(--border-soft, rgba(255, 255, 255, 0.12));
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.bot-source-step {
  position: relative;
  padding-left: 18px;
  color: var(--text-soft);
  overflow-wrap: anywhere;
}

/* Connector tick from rail to text, aligned to the first line in em so it
   tracks the font size rather than a fixed pixel offset. */
.bot-source-step::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.72em;
  width: 11px;
  height: 1px;
  background: var(--border-soft, rgba(255, 255, 255, 0.18));
}

/* Process steps are scaffolding around the things actually read, so they
   get a shorter tick and lighter text. */
.bot-source-step.is-muted::before {
  width: 7px;
  opacity: 0.6;
}

.bot-source-step.is-muted {
  opacity: 0.55;
  font-size: 11.5px;
}

.bot-source-step-body { display: block; }

.bot-source-step-title { color: var(--text-soft); }

a.bot-source-step-title {
  color: var(--text-soft);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color 0.15s ease, border-color 0.15s ease;
}

a.bot-source-step-title:hover {
  color: var(--text-main);
  border-bottom-color: var(--border-soft, rgba(255, 255, 255, 0.25));
}

/* Meta sits on its own line. Inline, it wrapped only for long titles, so
   identical rows were laid out differently depending on their length. */
.bot-source-step-meta {
  display: block;
  margin-top: 1px;
  font-size: 10.5px;
  opacity: 0.5;
}

@media (max-width: 600px) {
  .bot-source-disclosure {
    margin-left: 52px;
    max-width: calc(100% - 64px);
  }
}

/* ============================================================
   MODEL PICKER — GROUPED ENTRIES
   ============================================================
   A family shows as one row naming the variant currently selected,
   with a chevron. Clicking that row when its model is already active
   expands the siblings in place; otherwise it selects. Choosing any
   variant collapses the group again, so the picker is back to its
   tidy form next time it opens.
*/
.desktop-model-group { display: block; }

.desktop-model-item.is-group-head {
  position: relative;
  padding-right: 30px;
}

.desktop-model-chevron,
.menu-model-chevron {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 10px;
  opacity: 0.45;
  transition: transform 160ms ease, opacity 0.15s ease;
  pointer-events: none;
}

.desktop-model-group.is-expanded .desktop-model-chevron {
  transform: translateY(-50%) rotate(90deg);
  opacity: 0.8;
}

.desktop-model-item.is-group-head:hover .desktop-model-chevron,
.menu-model-item.is-group-head:hover .menu-model-chevron {
  opacity: 0.8;
}

.desktop-model-variants {
  display: block;
}

.desktop-model-item.is-variant,
.menu-model-item.is-variant {
  padding-left: 26px;
  position: relative;
}

/* Short tick tying a variant back to its family row, matching the
   connector used in the source disclosure. */
.desktop-model-item.is-variant::before,
.menu-model-item.is-variant::before {
  content: "";
  position: absolute;
  left: 14px;
  top: 50%;
  width: 7px;
  height: 1px;
  background: var(--border-soft, rgba(255, 255, 255, 0.18));
}

.desktop-model-item.is-variant .desktop-model-item-title,
.menu-model-item.is-variant .menu-model-name {
  font-size: 0.94em;
  opacity: 0.9;
}

.menu-model-item.is-group-head {
  position: relative;
  padding-right: 30px;
}

.menu-model-item.is-group-head .menu-model-chevron {
  right: 10px;
}

/* Mobile has no wrapper element to hang the expanded state on, so the
   rotation keys off the class the list item itself carries. */
.menu-model-item.is-group-head.is-open .menu-model-chevron {
  transform: translateY(-50%) rotate(90deg);
  opacity: 0.8;
}

/* ============================================================
   SETTINGS — TABBED LAYOUT (desktop)
   ============================================================
   Desktop gets a sidebar of categories beside a scrolling panel.
   Below 900px everything collapses back to the original single
   scrolling list, which is what mobile has always shown: a 6-item
   vertical nav plus a panel does not fit a phone, and the list works.

   The mobile path is the default here and desktop is the override, so
   any browser that fails the media query still gets a usable panel.
   ============================================================ */

.settings-shell {
  display: contents;   /* on mobile the nav is hidden and .settings-body
                          behaves exactly as it did before */
}

.settings-tabs {
  display: none;       /* desktop-only, enabled below */
}

/* Panels are plain containers on mobile: all of them visible, stacked,
   in one scrolling column. The `hidden` attribute in the markup is
   overridden here so the list stays complete. */
.settings-panel[hidden] {
  display: block;
}

.settings-panel-title {
  display: none;       /* the tab label already names the section on
                          desktop; on mobile the rows speak for
                          themselves, as they did before */
}

@media (min-width: 900px) {
  .settings-modal {
    width: min(760px, calc(100% - 48px));
    height: min(520px, calc(100% - 64px));
  }

  .settings-shell {
    display: flex;
    flex: 1 1 auto;
    min-height: 0;     /* lets the panel scroll instead of the shell
                          stretching past the modal */
  }

  .settings-tabs {
    display: flex;
    flex-direction: column;
    gap: 2px;
    width: 188px;
    flex-shrink: 0;
    padding: 10px;
    border-right: 1px solid var(--border-main);
    overflow-y: auto;
    scrollbar-width: none;
  }

  .settings-tabs::-webkit-scrollbar { width: 0; display: none; }

  .settings-tab {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 9px 11px;
    border: 0;
    border-radius: 8px;
    background: transparent;
    color: var(--text-soft);
    font: inherit;
    font-size: 13.5px;
    text-align: left;
    cursor: pointer;
    transition: background 0.14s ease, color 0.14s ease;
  }

  .settings-tab svg { flex-shrink: 0; }

  .settings-tab:hover {
    background: color-mix(in srgb, var(--accent) 10%, transparent);
    color: var(--text-main);
  }

  .settings-tab[aria-selected="true"] {
    background: color-mix(in srgb, var(--text-main) 8%, transparent);
    color: var(--text-main);
    font-weight: 600;
  }

  .settings-tab:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
  }

  .settings-body {
    flex: 1 1 auto;
    min-width: 0;
    min-height: 0;
    padding: 0 0 10px 0;
  }

  /* On desktop the hidden attribute does its normal job again. */
  .settings-panel[hidden] {
    display: none;
  }

  .settings-panel-title {
    display: block;
    margin: 0;
    padding: 18px 20px 6px 20px;
    font-size: 15px;
    font-weight: 700;
    color: var(--text-main);
  }

  .settings-panel .settings-row {
    padding-left: 20px;
    padding-right: 20px;
  }

  /* The first row under a heading shouldn't also carry the divider that
     separates rows from each other. */
  .settings-panel .settings-row:first-of-type {
    border-top: 0;
  }
}

/* ---- Usage ---- */

.settings-usage {
  padding: 14px 18px 4px 18px;
}

.settings-usage-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}

.settings-usage-stat {
  padding: 14px 12px;
  border: 1px solid var(--border-main);
  border-radius: 11px;
  background: color-mix(in srgb, var(--text-main) 3%, transparent);
  text-align: center;
}

.settings-usage-value {
  font-size: 21px;
  font-weight: 700;
  color: var(--text-main);
  line-height: 1.15;
  font-variant-numeric: tabular-nums;
}

.settings-usage-label {
  margin-top: 3px;
  font-size: 11.5px;
  color: var(--text-soft);
}

.settings-usage-split {
  margin-top: 16px;
}

.settings-usage-bar {
  height: 8px;
  border-radius: 999px;
  overflow: hidden;
  background: color-mix(in srgb, var(--text-main) 12%, transparent);
}

.settings-usage-bar-fill {
  display: block;
  height: 100%;
  width: 0%;
  border-radius: 999px;
  background: #6366f1;
  transition: width 0.4s ease;
}

.settings-usage-legend {
  display: flex;
  gap: 18px;
  margin-top: 9px;
  font-size: 12px;
  color: var(--text-soft);
}

.settings-usage-legend b {
  color: var(--text-main);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.settings-usage-legend .dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-right: 6px;
  vertical-align: baseline;
}

.settings-usage-legend .dot.sent { background: #6366f1; }
.settings-usage-legend .dot.recv {
  background: color-mix(in srgb, var(--text-main) 22%, transparent);
}

.settings-usage-note {
  margin: 16px 0 0 0;
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--text-soft);
}

/* ============================================================
   PASTED CONTENT
   ============================================================
   A long paste becomes a chip in the composer rather than several
   hundred lines dumped into a 92px-tall textarea. The full text is
   held in JS and merged back into the message on send, so nothing is
   lost and the model still receives it verbatim.
   ============================================================ */

/* Stacked above the composer rather than inside its flex row.

   As a flex line the tray added its own height to .input-bar, and the bar
   is positioned by `top` — so in the active chat state the row grew
   DOWNWARD and pushed the textarea past the bottom of the viewport
   (measured: bottom at 793px in a 700px window). Positioning the tray
   against the bar's top edge instead leaves the bar exactly 46px tall in
   every state, so the input cannot move. */
.paste-tray {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 0;
  right: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 0 2px;
  box-sizing: border-box;
  z-index: 2;
}

.paste-tray[hidden] {
  display: none;
}

.paste-chip {
  position: relative;
  width: 148px;
  max-height: 118px;
  overflow: hidden;
  padding: 10px 11px 26px 11px;
  border: 1px solid var(--border-main);
  border-radius: 12px;
  background: var(--bg-input);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  cursor: default;
}

.paste-chip-preview {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 9.5px;
  line-height: 1.5;
  color: var(--text-soft);
  word-break: break-word;

  /* Clamped rather than truncated with an ellipsis: the point of the
     preview is to be recognisable at a glance, and more lines of small
     text identify a paste better than one line plus a "…". */
  display: -webkit-box;
  -webkit-line-clamp: 5;
  line-clamp: 5;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.paste-chip-badge {
  position: absolute;
  left: 11px;
  bottom: 9px;
  padding: 2px 6px;
  border-radius: 5px;
  background: color-mix(in srgb, var(--text-main) 14%, transparent);
  color: var(--text-main);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.paste-chip-meta {
  position: absolute;
  right: 11px;
  bottom: 10px;
  font-size: 9px;
  color: var(--text-soft);
  font-variant-numeric: tabular-nums;
}

.paste-chip-remove {
  position: absolute;
  top: 5px;
  right: 5px;
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: color-mix(in srgb, var(--bg-main) 86%, transparent);
  color: var(--text-main);
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.14s ease, background 0.14s ease;
}

.paste-chip:hover .paste-chip-remove,
.paste-chip-remove:focus-visible {
  opacity: 1;
}

.paste-chip-remove:hover {
  background: color-mix(in srgb, var(--text-main) 22%, transparent);
}

/* ============================================================
   USER MESSAGE FORMATTING
   ============================================================
   Without this the bubble inherits white-space:normal, which collapses
   every run of whitespace — so pasted code arrived as one unbroken
   paragraph with its indentation and line breaks gone. pre-wrap keeps
   both while still wrapping long lines to the bubble width.
   ============================================================ */

.msg-user {
  white-space: pre-wrap;
  tab-size: 2;
}

/* Very long messages are capped and expandable. A 300-line paste would
   otherwise produce a bubble taller than the viewport, pushing the reply
   off screen and making the conversation hard to scroll. */
.msg-user.is-clamped .msg-user-body {
  max-height: 300px;
  overflow: hidden;
  /* Fades the final line rather than slicing it, so it reads as
     continuing rather than as a rendering fault. */
  -webkit-mask-image: linear-gradient(to bottom, #000 78%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 78%, transparent 100%);
}

.msg-user.is-expanded .msg-user-body {
  max-height: none;
  -webkit-mask-image: none;
  mask-image: none;
}

.msg-user-toggle {
  display: block;
  margin-top: 8px;
  padding: 0;
  border: 0;
  background: transparent;
  color: #ffffff;
  opacity: 0.75;
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}

.msg-user-toggle:hover { opacity: 1; }

body.light .msg-user-toggle { color: #ffffff; }

/* ---- Outline header actions ---- */

.thread-panel-actions {
  display: flex;
  align-items: center;
  gap: 2px;
}

/* ---- Privacy ---- */

/* The destructive row is tinted rather than merely labelled, so the
   difference between "Download" and "Delete" is visible before reading. */
.settings-row-danger .settings-row-title {
  color: #f87171;
}

body.light .settings-row-danger .settings-row-title {
  color: #dc2626;
}

.settings-pill-danger {
  border-color: color-mix(in srgb, #f87171 45%, var(--border-main));
  color: #f87171;
}

.settings-pill-danger:hover {
  background: color-mix(in srgb, #f87171 14%, transparent);
  border-color: #f87171;
}

body.light .settings-pill-danger {
  border-color: color-mix(in srgb, #dc2626 45%, var(--border-main));
  color: #dc2626;
}

body.light .settings-pill-danger:hover {
  background: color-mix(in srgb, #dc2626 12%, transparent);
  border-color: #dc2626;
}

/* Confirm step. Replaces the single button with an explicit pair rather
   than using window.confirm(), which is unstyled, easy to dismiss by
   reflex, and looks like a browser error next to this UI. */
/* The confirm takes its own line rather than sharing the row. Sat beside
   the description it squeezed that text into a ~90px column, which made
   the warning — the one thing that must stay readable here — the hardest
   part of the row to read. */
.settings-row.is-confirming {
  flex-wrap: wrap;
}

.settings-confirm {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  flex: 0 0 100%;
  margin-top: 12px;
}

.settings-confirm-label {
  font-size: 12px;
  color: var(--text-soft);
  white-space: nowrap;
}

/* ---- Combined thinking + web disclosure ---- */

/* The thinking text is the last step's body rather than a separate panel,
   so it sits on the same rail as the search steps and reads as the final
   thing that happened rather than as a second, unrelated dropdown. */
.bot-source-step-thought {
  margin-top: 6px;
  padding-left: 10px;
  border-left: 1px solid var(--accent);
  color: var(--text-soft);
  opacity: 0.88;
  font-size: 12px;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* The thinking step's title is muted like the other scaffolding steps, but
   its body is the payload — so the step itself must not dim the text. */
.bot-source-step.is-muted .bot-source-step-thought {
  opacity: 0.88;
}

/* Explains a server-side override in the panel it affects, so a user whose
   toggles say "off" but who sees thinking on a web reply has the answer in
   front of them rather than filing it as a bug. */
.settings-panel-note {
  margin: 0;
  padding: 0 20px 4px 20px;
  font-size: 12px;
  line-height: 1.55;
  color: var(--text-soft);
}

@media (max-width: 899px) {
  .settings-panel-note { padding: 0 18px 6px 18px; }
}
