/* Chat — a centred reading column: the assistant's replies as plain text,
   the user's turns as soft bubbles, and a floating composer card below. */

.view[data-view="chat"] { padding: 0; position: relative }
.view[data-view="chat"].active { display: flex; flex-direction: column }

#log {
  flex: 1;
  overflow-y: auto;
  /* Side padding grows with the view so the conversation stays a readable
     column (--content-w) instead of stretching across a wide screen. */
  padding: 1.5rem max(1rem, calc((100% - var(--content-w)) / 2)) 1rem;
  display: flex;
  flex-direction: column;
  gap: .65rem;
  min-height: 0;
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb) transparent;
}

/* Top fade — soft frosted band that masks the hard edge where messages
   would otherwise be clipped as they scroll up under the main bar.

   Two effects layered behind one mask:
     • backdrop-filter blur — frosts whatever passes beneath
     • bg gradient (var(--bg) → transparent) — adds a faint fog tint that
       lets text fade INTO the page colour rather than just blurring
   The mask `linear-gradient(black 30% → transparent)` keeps the top third
   at full strength and lets both effects taper smoothly to zero near the
   bottom — so the chat is fully crisp by the time #log's top padding ends
   and real messages begin. */
.chat-fade-top {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1.5rem;
  z-index: 3;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    var(--bg) 0%,
    color-mix(in srgb, var(--bg) 55%, transparent) 50%,
    transparent 100%
  );
  backdrop-filter: blur(8px) saturate(1.05);
  -webkit-backdrop-filter: blur(8px) saturate(1.05);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 30%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0%, #000 30%, transparent 100%);
}
#log::-webkit-scrollbar { width: 8px }
#log::-webkit-scrollbar-track { background: transparent }
#log::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 4px; transition: background .15s }
#log::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover) }

/* Row wrapper around each bubble. Owns the side-alignment so the
   timestamp (a sibling of the bubble, not a child) sits flush with the
   bubble's tail edge — right under the user's bubble, left under the
   assistant's. The bubble itself no longer carries `align-self`; the
   row does, and `align-items` mirrors it so the bubble and its
   timestamp share the same edge regardless of either's width. */
.msg-row {
  display: flex;
  flex-direction: column;
  max-width: 85%;
  min-width: 0;
  gap: 3px;
}
.msg-row.user      { align-self: flex-end;   align-items: flex-end; }
.msg-row.assistant,
.msg-row.error     { align-self: flex-start; align-items: flex-start; }
/* A reply is prose, not a bubble — it may use the whole column. */
.msg-row.assistant { max-width: 100% }
/* The notice a chat was opened on (Notifications → "Discuss with agent"):
   the Notifications card itself (base.css .job-card), across the column at
   the head of the conversation. */
.msg-row.notice { align-self: stretch; max-width: 100% }

/* Message system. The user's turn and an error are bubbles (neutral / red,
   with a side-tail corner); the assistant's reply sits directly on the page
   so long answers read like a document. */
.msg {
  padding: .6rem .9rem;
  border-radius: 12px;
  line-height: 1.45;
  font-size: .94rem;
  font-family: 'Inter', system-ui, -apple-system, "Segoe UI", sans-serif;
  font-feature-settings: "kern", "liga";
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-weight: 400;
  letter-spacing: 0;
  word-break: break-word;
  /* Never wider than its row. The row stops at the column, but the bubble
     in it sizes to its content, and a code block's longest line (.md-pre,
     white-space: pre) stretched the bubble — and the whole log sideways —
     past the screen. Held to the row, the code block scrolls in its own box. */
  max-width: 100%;
}
.msg .who { display: none } /* bubble form already indicates sender */
.msg .body { white-space: pre-wrap; word-wrap: break-word }

.msg.user {
  background: var(--bubble-user);
  color: var(--text);
  border: 1px solid transparent;
  border-radius: 16px 16px 4px 16px;
}
.msg.assistant {
  padding: .2rem .15rem;
  background: transparent;
  color: var(--text);
  border: 1px solid transparent;
  line-height: 1.6;
}
.msg.error {
  background: var(--error-bubble-bg);
  color: var(--error-bubble-text);
  border: 1px solid var(--error-bubble-border);
  border-radius: 12px 12px 12px 2px;
}
/* Voice-input badge inside the user bubble — small, faded, floats below
   the transcribed text so the user can quickly see which of their turns
   came in via mic without it competing with the message itself. */
.msg.user .voice-input-badge {
  margin-top: 6px;
  font-size: 11px;
  opacity: 0.65;
  font-style: italic;
  letter-spacing: 0.2px;
  user-select: none;
}
/* Per-message timestamp — sibling of the bubble, not a child, so it
   reads as caption rather than as bubble content. Small, faded, inset
   one bubble-corner radius from the row edge so it visually nests
   under the bubble's tail corner. Wall-clock time surfaces on hover
   (full date/time in the user's locale via title=…). The row's
   align-items keeps it on the bubble side; padding-inline mirrors
   the bubble's tail so it doesn't cling to the row edge. */
.msg-row .timestamp {
  font-size: 10.5px;
  opacity: 0.5;
  letter-spacing: 0.2px;
  user-select: none;
  cursor: default;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  padding-inline: .55rem;
  transition: opacity .15s;
}
.msg-row .timestamp:hover { opacity: 0.85; }
/* Links inside an error bubble — match the bubble's red so they don't
   shout in green/blue accent. Underlined for clear affordance, slightly
   brightened on hover. */
.msg.error .body a {
  color: currentColor;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--danger-action) 55%, transparent);
  text-underline-offset: 2px;
}
.msg.error .body a:hover {
  text-decoration-color: currentColor;
}
/* "Try again" inside an agent-unreachable card — a quiet outlined button
   in the bubble's own red, on its own line under the hint. */
.msg.error .msg-retry {
  display: block;
  margin-top: .55rem;
  padding: .3rem .75rem;
  font: inherit;
  font-size: .88rem;
  font-weight: 600;
  color: currentColor;
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--danger-action) 55%, transparent);
  border-radius: 8px;
  cursor: pointer;
}
.msg.error .msg-retry:hover { border-color: currentColor; }

/* Composer (attachment tray + input row). The textarea sizes itself to
   its content (chat.js auto-grow) up to a third of the viewport, then
   scrolls — there is no manual expand/collapse control. */
#form {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0;
  /* Same column as #log above, so the composer lines up with the messages. */
  padding: .35rem max(1rem, calc((100% - var(--content-w)) / 2)) .55rem;
  flex-shrink: 0;
}
/* The composer card: the textarea and its buttons share one rounded,
   softly lifted surface; the textarea itself carries no chrome. */
.composer-row {
  display: flex;
  /* Bottom-aligned: once the textarea grows past one line on a long draft
     the buttons stay anchored bottom-right instead of floating mid-height. On a single line every control is 40px tall,
     so the alignment is invisible there. */
  align-items: flex-end;
  gap: .25rem;
  padding: .4rem .45rem .4rem .35rem;
  background: var(--composer-bg);
  border: 1px solid var(--border2);
  border-radius: 18px;
  box-shadow: var(--composer-shadow);
  transition: border-color .15s ease;
}
.composer-row:focus-within { border-color: var(--outline-hover) }
#input {
  flex: 1;
  min-height: 40px;
  /* Auto-grow ceiling: a third of the viewport. chat.js sizes the textarea
     to its content on every keystroke and this is the hard cap after which
     the textarea scrolls instead of pushing the chat log further up.
     dvh tracks the mobile browser chrome; vh is the fallback. */
  max-height: 33.333vh;
  max-height: calc(100dvh / 3);
  resize: none;
  border: 0;
  border-radius: 12px;
  background: transparent;
  font-size: .97rem;
  padding: .6rem .75rem;
  line-height: 1.4;
  /* Default to no scrollbar — JS auto-grow keeps the textarea sized to its
     content, so a scrollbar would only appear due to sub-pixel rounding
     between scrollHeight and clientHeight. We re-enable overflow only when
     content actually exceeds the cap (.is-overflowing). */
  overflow-y: hidden;
}
#input:focus { box-shadow: none }
#input.is-overflowing { overflow-y: auto }
/* Attach + mic + Send. Next to a short draft they sit in one row, as they
   always did. Once the textarea is a few lines tall (chat.js toggles
   #form.is-tall) the row would leave three controls strung along the
   bottom of a tall box — so the two icon buttons move up above Send,
   right-aligned, as a compact bottom-right column. The gaps mirror the
   old flat row so the single-line layout is pixel-identical. */
.composer-actions {
  display: flex;
  align-items: flex-end;
  gap: .25rem;
  flex-shrink: 0;
}
.composer-tools {
  display: flex;
  align-items: center;
  gap: .25rem;
}
#form.is-tall .composer-actions {
  flex-direction: column;
  align-items: flex-end;
}
#form.is-tall #send { margin-left: 0 }
#send {
  height: 38px;
  margin-left: .2rem;
  padding: 0 1.05rem;
  border-radius: 12px;
  /* The one solid accent fill on the page: the primary action. */
  background: var(--accent-solid);
  border-color: var(--accent-solid);
  color: var(--on-accent-solid);
  font-weight: 600;
  font-size: .9rem;
  min-width: 76px;
  flex-shrink: 0;
  /* 38px in a 40px row, bottom-aligned: lift by 1px so it reads centred
     next to the 40px icon buttons on a single-line composer. */
  margin-bottom: 1px;
  /* Smooth the colour swap when we toggle .is-stop on/off so the button
     doesn't blink between turns. Animating background + border-color
     keeps the transition cheap (no layout thrash). */
  transition: background .18s ease, border-color .18s ease, color .18s ease;
}
#send:hover {
  background: var(--accent-solid);
  border-color: var(--accent-solid);
  color: var(--on-accent-solid);
  filter: brightness(1.08);
}
#send:disabled { opacity: .4; cursor: not-allowed }

/* Busy state — #send while attachments upload (slow connections). Distinct
   from a plain :disabled so the user reads it as "working", not "dead":
   a wait cursor + a gentle opacity pulse. The `submitting` latch in chat.js
   blocks the duplicate submit underneath; this is purely the visible cue. */
#send.is-busy { cursor: wait; animation: send-busy-pulse 1s ease-in-out infinite; }
@keyframes send-busy-pulse { 0%, 100% { opacity: .4 } 50% { opacity: .7 } }
@media (prefers-reduced-motion: reduce) {
  #send.is-busy { animation: none; }
}

/* Stop state — sendBtn doubles as a "stop generating" button while the
   agent is streaming a reply. Same physical button, different paint job
   so the form layout doesn't shift the moment streaming starts. We keep
   it tagasihoidlik (muted) per the user's brief: a clear cue without
   shouting. The hover deepens slightly to confirm interactivity. */
#send.is-stop {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--muted);
}
#send.is-stop:hover {
  border-color: var(--danger-action);
  color: var(--danger-action);
}
/* Tiny square glyph rendered with a pseudo-element so the button label
   keeps the simple "Stop" text node (matches our rule of no inline SVG
   for trivial shapes). The square is the universally-understood stop
   icon and reads at a glance even at sub-15px sizes. */
#send.is-stop::before {
  content: '';
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-right: .4rem;
  background: currentColor;
  vertical-align: 1px;
  border-radius: 1px;
}

/* Under the composer: what answers the next message — model, then provider
   and credit, on one quiet centred line. They name a state, in one tone; the
   composer is the action. */
.composer-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0 .1rem;
  min-height: 1.7rem;
  margin-top: .4rem;
  padding: 0 .35rem;
}
.composer-meta .pill,
.composer-meta .pill.model {
  min-width: 0;
  max-width: 100%;
  padding: .22rem .55rem;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  background: transparent;
  border-color: transparent;
  border-radius: 999px;
  color: var(--muted);
  font-size: .76rem;
  font-weight: 500;
}
.composer-meta .pill.clickable:hover {
  background: var(--shell-hover);
  border-color: transparent;
  color: var(--text);
}
.composer-meta-sep {
  color: var(--muted2);
  font-size: .76rem;
  user-select: none;
}
.composer-meta .pill[aria-expanded="true"] {
  background: var(--shell-active);
  color: var(--text);
}
/* Before the first refresh lands there is nothing to name: the line keeps
   its height (no jump when the labels arrive) but shows no placeholder. */
.composer-meta.is-empty { visibility: hidden }

/* The picker a pill opens (composer_picker.js): a small list anchored under
   the pill by popover.js, in the menu's own dress — one surface, a quiet
   caption, the current choice ticked, and the way into Settings at the foot.
   Fixed in the viewport so the scrolling chat can't clip it. */
.cp-pop {
  position: fixed;
  z-index: 960;
  min-width: 14rem;
  max-width: min(22rem, calc(100vw - 16px));
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: .3rem;
  overflow-y: auto;
  background: var(--menu-bg);
  border: 1px solid var(--border2);
  border-radius: 12px;
  box-shadow: var(--shadow-pop);
  animation: cp-pop-in .14s ease-out;
}
@keyframes cp-pop-in {
  from { opacity: 0; transform: translateY(4px) }
  to   { opacity: 1; transform: translateY(0) }
}
@media (prefers-reduced-motion: reduce) {
  .cp-pop { animation: none }
}
.cp-caption {
  padding: .3rem .65rem .35rem;
  color: var(--muted2);
  font-size: .66rem;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cp-note {
  padding: .45rem .65rem;
  color: var(--muted);
  font-size: .8rem;
  line-height: 1.4;
}
/* Rows are <button>s; undo the everyday button chrome so they read as list
   entries, like .menu-item. */
.cp-opt, .cp-link {
  display: flex;
  align-items: center;
  gap: .6rem;
  width: 100%;
  padding: .42rem .65rem;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: .82rem;
  font-weight: 400;
  line-height: 1.3;
  letter-spacing: 0;
  text-align: left;
  cursor: pointer;
  box-shadow: none;
}
.cp-opt:hover, .cp-opt:focus-visible,
.cp-link:hover, .cp-link:focus-visible {
  background: var(--shell-hover);
  border: 0;
  color: var(--text);
  outline: none;
  box-shadow: none;
}
.cp-opt:active, .cp-link:active { transform: none }
.cp-opt:disabled {
  opacity: .45;
  cursor: default;
}
.cp-opt:disabled:hover { background: transparent }
.cp-opt[aria-selected="true"] {
  color: var(--accent);
  font-weight: 600;
}
.cp-opt-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cp-opt-meta {
  flex: none;
  /* An absolute cap, not a share of the row: the row is sized by its
     content, so a percentage cut a long note beside a short name ("Codex"
     next to "(your subscription)") to an ellipsis for no reason. The name
     yields first when the popover reaches its own maximum width. */
  max-width: 12rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--muted);
  font-size: .68rem;
  font-weight: 500;
}
.cp-opt[aria-selected="true"] .cp-opt-meta { color: var(--accent); opacity: .8 }
/* The tick marks the current choice; the column is reserved on every row so
   names line up. */
.cp-opt-check {
  display: inline-flex;
  flex: none;
  width: 14px;
  height: 14px;
  visibility: hidden;
}
.cp-opt-check svg { width: 14px; height: 14px }
.cp-opt[aria-selected="true"] .cp-opt-check { visibility: visible }
/* The choice in use sits in its own tinted box (composer_picker.js
   _current): in the model list together with the effort scale, so the scale
   plainly belongs to that model; in the provider list on its own, in the
   same dress. The box spans the row's width, so its text lines up with the
   other rows. */
.cp-current {
  display: flex;
  flex-direction: column;
  padding: .1rem 0 .15rem;
  margin: 1px 0;
  border-radius: 9px;
  background: var(--accent-soft);
}
.cp-current .cp-opt { border-radius: 7px }
.cp-current .cp-effort { margin: 0; padding: 0 .65rem .3rem }
/* The reasoning-effort scale under the model in use (composer_picker.js
   _effortBar): a track with one stop per level, filled up to the level in
   use, the names underneath. The stops share the width equally, so the
   first and last dot centres sit half a stop in from each edge; the track
   line runs between those centres and the fill is the in-use stop's share
   of that run (--cp-effort-n stops, --cp-effort-fill 0..1, set inline). */
.cp-effort {
  margin: 0 0 .35rem;
  padding: .15rem .65rem .25rem;
}
.cp-effort-label {
  display: block;
  margin-bottom: .5rem;
  color: var(--muted);
  font-size: .68rem;
  font-weight: 500;
}
.cp-effort-track {
  position: relative;
  display: flex;
  align-items: flex-start;
  /* Every level name is written out in full: each stop gets room for the
     longest usual one ("medium" at this size), which widens the list past
     its 14rem floor when the scale is in it (five stops ≈ 19rem, inside
     the list's 22rem cap and any phone's width). */
  min-width: calc(var(--cp-effort-n, 1) * 3.4rem);
}
.cp-effort-track::before,
.cp-effort-track::after {
  content: '';
  position: absolute;
  top: 5px;
  height: 2px;
  border-radius: 1px;
  pointer-events: none;
}
.cp-effort-track::before {
  left: calc(50% / var(--cp-effort-n, 1));
  right: calc(50% / var(--cp-effort-n, 1));
  background: var(--border2);
}
.cp-effort-track::after {
  left: calc(50% / var(--cp-effort-n, 1));
  width: calc((100% - 100% / var(--cp-effort-n, 1)) * var(--cp-effort-fill, 0));
  background: var(--accent);
}
.cp-effort-stop {
  position: relative;
  z-index: 1;
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .32rem;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: .62rem;
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 0;
  cursor: pointer;
  box-shadow: none;
}
.cp-effort-stop:hover, .cp-effort-stop:focus-visible {
  background: transparent;
  color: var(--text);
  outline: none;
  box-shadow: none;
}
.cp-effort-stop:active { transform: none }
.cp-effort-knob {
  box-sizing: border-box;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--menu-bg);
  border: 2px solid var(--muted2);
  transition: transform .12s ease, border-color .12s ease, background .12s ease, box-shadow .12s ease;
}
.cp-effort-stop.is-passed .cp-effort-knob { border-color: var(--accent) }
.cp-effort-stop:hover .cp-effort-knob,
.cp-effort-stop:focus-visible .cp-effort-knob {
  border-color: var(--accent);
  transform: scale(1.15);
}
.cp-effort-stop:focus-visible .cp-effort-knob { box-shadow: 0 0 0 3px var(--accent-soft) }
.cp-effort-stop[aria-checked="true"] { color: var(--accent); font-weight: 600 }
.cp-effort-stop[aria-checked="true"] .cp-effort-knob {
  background: var(--accent);
  border-color: var(--accent);
  transform: scale(1.25);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.cp-effort-name { white-space: nowrap }
.cp-foot {
  display: flex;
  flex-direction: column;
  gap: 1px;
  margin-top: .2rem;
  padding-top: .25rem;
  border-top: 1px solid var(--border);
}
.cp-link {
  color: var(--muted);
  font-size: .78rem;
}

/* Start of a conversation (.is-start, kept by chat.js): nothing said yet, so
   the composer sits mid-page under a greeting and moves to the bottom edge
   with the first message. On a first visit the welcome note in the log
   stands in for the greeting. */
.chat-start { display: none }
.view[data-view="chat"].is-start {
  justify-content: center;
  padding-bottom: 12vh;
}
.is-start .chat-start {
  display: block;
  margin: 0 0 1.5rem;
  padding: 0 1.25rem;
  font-size: clamp(1.45rem, 1.1rem + 1.2vw, 1.9rem);
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: -.02em;
  text-align: center;
  color: var(--text);
}
.is-start:has(.chat-welcome) .chat-start { display: none }
.is-start #log {
  flex: 0 0 auto;
  overflow: visible;
  padding-top: 0;
  padding-bottom: .75rem;
}
.is-start #log:empty,
.is-start .chat-fade-top { display: none }
.is-start .composer-row {
  padding: .55rem .6rem .55rem .5rem;
  border-radius: 20px;
}

/* Loading a conversation (chat.js renderLoadingPlaceholder): three ghost
   bubbles where the messages will be, breathing in turn. The whole group
   fades in after a short delay, so a session that arrives quickly never
   flashes it. */
.chat-loading {
  display: flex;
  flex-direction: column;
  gap: .65rem;
  opacity: 0;
  animation: chat-loading-in .25s ease-out .15s forwards;
}
.chat-loading-row { display: flex }
.chat-loading-row.user { justify-content: flex-end }
.chat-loading-bubble {
  display: block;
  height: 2.4rem;
  border-radius: 18px;
  background: var(--shell-hover);
  animation: chat-loading-breathe 1.8s ease-in-out infinite;
}
.chat-loading-row.assistant .chat-loading-bubble { width: 62% }
.chat-loading-row.user .chat-loading-bubble { width: 38%; height: 2.1rem }
.chat-loading-row:nth-child(2) .chat-loading-bubble { animation-delay: .2s }
.chat-loading-row:nth-child(3) .chat-loading-bubble { animation-delay: .4s; width: 48% }
@keyframes chat-loading-in { to { opacity: 1 } }
@keyframes chat-loading-breathe {
  0%, 100% { opacity: .45 }
  50%      { opacity: 1 }
}

/* Markdown rendering: the agent's reply in a chat bubble, a notice's text
   (base.css .job-card-body — in Notifications and on the card a chat opens
   on) and a pin opened in the pin list (.pin-list-full, message_pins.js),
   which are the same renderMarkdown() output and read the same. Block
   elements break out of the surrounding pre-wrap flow on their own; we still
   want tight vertical rhythm so the bubble doesn't bloat. */
.msg.assistant .body .md-h,
.job-card-body .md-h,
.pin-list-full .md-h {
  font-weight: 600;
  margin: .35em 0 .2em;
  line-height: 1.25;
}
.msg.assistant .body h1.md-h, .job-card-body h1.md-h, .pin-list-full h1.md-h { font-size: 1.22rem }
.msg.assistant .body h2.md-h, .job-card-body h2.md-h, .pin-list-full h2.md-h { font-size: 1.12rem }
.msg.assistant .body h3.md-h, .job-card-body h3.md-h, .pin-list-full h3.md-h { font-size: 1.05rem }
.msg.assistant .body .md-code,
.job-card-body .md-code,
.pin-list-full .md-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .92em;
  background: var(--surface-hover-strong);
  padding: .05em .35em;
  border-radius: 4px;
}
.msg.assistant .body .md-pre,
.job-card-body .md-pre,
.pin-list-full .md-pre {
  background: var(--surface-control);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: .55rem .7rem;
  margin: .35em 0;
  overflow-x: auto;
  white-space: pre;
  font-size: .9rem;
}
.msg.assistant .body .md-pre code,
.job-card-body .md-pre code,
.pin-list-full .md-pre code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  background: transparent;
  padding: 0;
}
.msg.assistant .body a,
.job-card-body a,
.pin-list-full a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.msg.assistant .body strong,
.job-card-body strong,
.pin-list-full strong { font-weight: 600 }

/* Tables / blockquotes — block constructs renderMarkdown() emits as real
   HTML. The browser renders the genuine article (Telegram only approximates
   with monospace). A table is as wide as its columns need and no wider than
   the text; its cells wrap between words, never inside one. When the words
   do not fit even so, the table scrolls sideways in its own box, as a code
   block does: display: block makes the table element that box, and its rows
   are laid out in an anonymous table inside it. */
.msg.assistant .body .md-table,
.job-card-body .md-table,
.pin-list-full .md-table {
  display: block;
  max-width: 100%;
  overflow-x: auto;
  border-collapse: collapse;
  margin: .4em 0;
  font-size: .92em;
}
.msg.assistant .body .md-table th,
.msg.assistant .body .md-table td,
.job-card-body .md-table th,
.job-card-body .md-table td,
.pin-list-full .md-table th,
.pin-list-full .md-table td {
  border: 1px solid var(--border);
  padding: .25em .5em;
  text-align: left;
  /* The bubble's and the card's word-break: break-word may break any word,
     and a table squeezes each column to its narrowest such break — on a
     phone "Markdown" came out as "Markdow" / "n" (13.10). */
  word-break: normal;
}
.msg.assistant .body .md-table th,
.job-card-body .md-table th,
.pin-list-full .md-table th {
  font-weight: 600;
  background: var(--surface-hover);
}
.msg.assistant .body .md-quote,
.job-card-body .md-quote,
.pin-list-full .md-quote {
  margin: .35em 0;
  padding: .1em 0 .1em .7em;
  border-left: 3px solid var(--border);
  opacity: .9;
}

@media (max-width: 680px) {
  .msg-row { max-width: 92% }
  #log { padding: 1rem .75rem .75rem }
  #form { padding: .35rem .6rem .45rem }
  /* Slightly tighter fade on phones to match the reduced top padding. */
  .chat-fade-top { height: 1rem }
}

/* Thinking indicator — three dots that pulse in sequence, riding along
   the tail of the assistant's bubble for as long as the stream is alive.
   chat.js attaches the same DOM node before the network call, re-attaches
   it at the bubble root after every renderMarkdown() pass (innerHTML
   reassignment wipes children), and removes it in `finally` once
   streaming ends or errors.

   Visual placement (chat.js placeThinking, for the panel's own turn and
   for one sent from Telegram alike): the dots go last in the bubble being
   written. After words they stay on the last word's line, a clear gap
   away -- an ellipsis glued to the sentence read as part of it. After a
   block (a code block, a table, a heading, a quote, a list, an image)
   they take a line of their own under it, with air above, never pressed
   against its edge. In an empty bubble they start where the text will.

   Sizing: the bubble's vertical padding (.6rem) means a 6px dot row sits
   on the same line height as a single text line — no jumpy resize when
   the first character of the reply lands.

   Accessibility: role=status + aria-label announces "Agent is thinking"
   to screen readers; prefers-reduced-motion swaps the bouncy keyframe
   for a slower fade so users sensitive to motion still see *something*
   moving (silent freeze is worse — they'd think the panel hung). */
.thinking {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  /* The gap after the last word -- on the text's side in a right-to-left
     language too. */
  margin-inline-start: .6em;
  height: 1.2em;
  vertical-align: middle;
}
.thinking.is-alone { margin-inline-start: 0 }
.thinking.is-below {
  display: flex;
  margin-inline-start: 0;
  margin-top: .5em;
}
/* The steps block keeps its own air below it. */
.msg-trace + .thinking.is-below { margin-top: 0 }
/* `display: inline-flex` above outranks the UA's `[hidden] { display: none }`. */
.thinking[hidden] { display: none }
.thinking-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  opacity: .35;
  animation: thinking-pulse 1.2s ease-in-out infinite;
}
.thinking-dot:nth-child(2) { animation-delay: .15s }
.thinking-dot:nth-child(3) { animation-delay: .30s }
@keyframes thinking-pulse {
  0%, 80%, 100% { opacity: .25; transform: translateY(0)    scale(.85) }
  40%           { opacity: 1;   transform: translateY(-2px) scale(1.0) }
}

@media (prefers-reduced-motion: reduce) {
  .chat-loading-bubble { animation: none; opacity: .7 }
  .thinking-dot {
    animation: thinking-fade 1.6s ease-in-out infinite;
  }
  @keyframes thinking-fade {
    0%, 100% { opacity: .25 }
    50%      { opacity: .9  }
  }
}

/* Finalized minimal-mode tool trace above the answer. */
.msg-trace {
  margin: 0 0 .7rem;
  padding: .45rem .65rem;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--bg3);
  color: var(--muted);
  font-size: .82rem;
}
.msg-trace summary { cursor: pointer; color: var(--text); font-weight: 600; }
/* Step count — the shared .tab-count pill (base.css); nudged to sit on the
   label's optical centre inside the summary line. */
.msg-trace summary .tab-count { vertical-align: middle; position: relative; top: -1px; }
.msg-trace ol { margin: .45rem 0 .1rem 1.2rem; padding: 0; }
.msg-trace li { margin: .18rem 0; }

/* Detailed-style tool-step bubble — one small muted bubble per tool call,
   posted live as the agent works (panel twin of Telegram's tool bubbles).
   Meta-info styling on purpose: quieter than the assistant's prose so the
   actual answer bubbles stay visually dominant. */
.msg-row.toolstep { align-self: flex-start; align-items: flex-start; }
.msg.toolstep {
  background: var(--bg3);
  color: var(--muted);
  border: 1px dashed var(--border);
  border-radius: 12px;
  font-size: .82rem;
  padding: .35rem .65rem;
}
/* A tool step's code-like args preview — the shell command, file path or
   search pattern (paintStepLine) — in the code font, in the tool bubble and
   the minimal-style step list alike. */
.step-detail {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .95em;
  overflow-wrap: anywhere;
}

/* Phase 2A activity-line — short header above a streaming assistant
   bubble that tells the user why nothing new arrives (the reconnecting
   note). Reasoning is not shown here: the thinking dots already say
   the reply is still coming. Tool calls are not repeated here either:
   detailed style posts each one as its own bubble.
   Subtle by design: this is meta-info, not part of the reply itself.
   Removed by chat.js when the stream finishes / aborts / errors. */
.activity-line {
  font-size: .82em;
  color: var(--muted);
  margin-bottom: .35em;
  font-style: italic;
  letter-spacing: .01em;
}
.activity-line[hidden] { display: none; }

/* The way back to the newest message (#jumpToEnd in index.html) — a small
   round chevron that fades in once the reader has scrolled up and away
   from the end of the log, in a stream or not, and fades out again at the
   bottom (chat.js toggles .is-visible).

   Anchored to #form (`position: relative`) rather than the scroll
   container, so it stays put while #log scrolls and hangs off the
   composer's real top edge: the textarea auto-grows, so a fixed offset
   from the view's bottom would end up under a tall draft. It keeps to the
   right edge of the conversation column — the same expression #form uses
   for its side padding — and floats just above the composer card.
   Dressed like the composer, muted until hovered: a hint, not a call.
   The short rise on show is covered by reduced-motion. */
.jump-to-end {
  position: absolute;
  /* Centred over the composer: left + right 0 with a fixed width lets the
     auto margins split the slack. Not left: 50% + translateX — transform
     is already the show / hide animation below. */
  left: 0;
  right: 0;
  margin: 0 auto;
  /* #form's top edge is .35rem above the card (its padding-top), so this
     leaves half a rem of air between the chevron and the card. */
  bottom: calc(100% + .15rem);
  z-index: 5;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 1px solid var(--border2);
  border-radius: 50%;
  background: var(--composer-bg);
  color: var(--muted);
  cursor: pointer;
  box-shadow: var(--composer-shadow);
  opacity: 0;
  transform: translateY(6px);
  pointer-events: none;
  transition: opacity .18s ease-out, transform .18s ease-out, color .15s, border-color .15s;
}
.jump-to-end.is-visible {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}
.jump-to-end:hover {
  color: var(--text);
  border-color: var(--outline-hover);
  background: var(--composer-bg);
}
/* The global button:active translateY(1px) would replace the show
   transform; the press nudge is the same distance, so keep it as is. */
.jump-to-end.is-visible:active { transform: translateY(1px) }
.jump-to-end svg { width: 16px; height: 16px; display: block }

@media (prefers-reduced-motion: reduce) {
  .jump-to-end { transition: opacity .18s ease-out, color .15s, border-color .15s; transform: none }
  .jump-to-end.is-visible { transform: none }
}

@media (max-width: 680px) {
  /* Slightly smaller on phones. */
  .jump-to-end { width: 32px; height: 32px }
}

/* ── Attachments ─────────────────────────────────────────────────────
   Composer-side: attach button + pending tray + drop overlay.
   Bubble-side: image previews + file download chips that render the
   ![](url) markdown and [[SEND_FILE:...]] markers chat.js extracts. */

/* Attach button (📎). Same height as the textarea so the row stays
   visually centred; transparent so it doesn't pull attention away from
   the primary Send action. */
.attach-btn {
  flex: 0 0 auto;
  height: 40px;
  width: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border-radius: 9px;
  background: transparent;
  border: 1px solid transparent;
  color: var(--muted2);
  cursor: pointer;
  transition: color .15s, background .15s, border-color .15s;
}
.attach-btn:hover {
  color: var(--accent);
  background: var(--bg3, color-mix(in srgb, var(--text) 4%, transparent));
  border-color: var(--border);
}
.attach-btn svg { width: 18px; height: 18px; display: block }
/* Override the global button:active translateY(1px) — the icon button
   already nudges via background, no need to slide. */
.attach-btn:active { transform: translateY(1px) }

/* Mic button — same shape and behaviour as attach-btn, plus a pulsing
   record state and a subtle busy-spinner ring while transcribing. */
.mic-btn {
  flex: 0 0 auto;
  height: 40px;
  width: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border-radius: 9px;
  background: transparent;
  border: 1px solid transparent;
  color: var(--muted2);
  cursor: pointer;
  transition: color .15s, background .15s, border-color .15s, box-shadow .15s;
}
.mic-btn:hover {
  color: var(--accent);
  background: var(--bg3, color-mix(in srgb, var(--text) 4%, transparent));
  border-color: var(--border);
}
.mic-btn svg { width: 18px; height: 18px; display: block }
.mic-btn:active { transform: translateY(1px) }

/* Recording state: tinted background + pulsing red dot under the icon. */
.mic-btn.is-recording {
  color: var(--err);
  background: color-mix(in srgb, var(--err) 12%, transparent);
  border-color: color-mix(in srgb, var(--err) 35%, transparent);
  animation: mic-pulse 1.2s ease-in-out infinite;
}
@keyframes mic-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--err) 45%, transparent) }
  50%      { box-shadow: 0 0 0 6px color-mix(in srgb, var(--err) 0%, transparent) }
}

/* Busy state — transcribing, waiting on Groq. Spinner ring around the
   icon so the user can tell the click was registered. */
.mic-btn.is-busy {
  color: var(--accent);
  pointer-events: none;
  position: relative;
}
.mic-btn.is-busy::after {
  content: "";
  position: absolute;
  inset: 4px;
  border-radius: 50%;
  border: 2px solid transparent;
  border-top-color: var(--accent);
  animation: mic-spin .8s linear infinite;
}
@keyframes mic-spin { to { transform: rotate(360deg) } }
@keyframes mic-busy-fade { 50% { opacity: .35 } }
@media (prefers-reduced-motion: reduce) {
  /* The red tint alone says the mic is recording; the busy ring fades
     instead of spinning, so the wait still reads as work under way. */
  .mic-btn.is-recording { animation: none }
  .mic-btn.is-busy::after { animation: mic-busy-fade 1.6s ease-in-out infinite }
}
@media (max-width: 540px) {
  /* 36px in a 40px bottom-aligned row: 2px lift keeps it optically centred. */
  .mic-btn { width: 36px; height: 36px; margin-bottom: 2px }
}

/* Pending-attachment tray: chips listing files queued for the next send.
   Sits above the composer-row when populated; hidden
   when empty so the composer keeps its compact layout. */
.attach-tray {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem;
  padding: .25rem 0 .5rem;
  margin: 0;
}
.attach-tray[hidden] { display: none }
.attach-chip {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  max-width: 220px;
  padding: .25rem .35rem .25rem .5rem;
  border-radius: 8px;
  background: var(--bg3, color-mix(in srgb, var(--text) 5%, transparent));
  border: 1px solid var(--border);
  font-size: .8rem;
  color: var(--text);
}
.attach-chip-thumb {
  width: 26px;
  height: 26px;
  border-radius: 5px;
  object-fit: cover;
  flex-shrink: 0;
  background: rgba(0,0,0,.3);
}
.attach-chip-icon {
  width: 26px;
  height: 26px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 5px;
  background: rgba(0,0,0,.3);
  font-size: 1rem;
  flex-shrink: 0;
}
.attach-chip-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1;
  min-width: 0;
}
.attach-chip-remove {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--muted2);
  cursor: pointer;
  font-size: 1.1rem;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.attach-chip-remove:hover { color: var(--danger-action); background: color-mix(in srgb, var(--text) 6%, transparent) }
/* In-flight upload: a fill bar grows behind the chip content (width from
   --upload-pct, set by chat_attach.js on every xhr progress event) and a
   tabular percent sits next to the name. isolation + z-index:-1 keeps the
   fill above the chip background but under the label/thumbnail. */
.attach-chip.is-uploading {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border-color: var(--accent-dim);
}
.attach-chip.is-uploading::before {
  content: '';
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: var(--upload-pct, 0%);
  background: var(--accent-soft, color-mix(in srgb, var(--accent) 12%, transparent));
  transition: width .15s linear;
  z-index: -1;
  pointer-events: none;
}
.attach-chip-pct {
  flex-shrink: 0;
  min-width: 2.6em;
  text-align: right;
  font-size: .72rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}
.attach-chip.is-error { border-color: var(--danger-action); color: var(--danger-action) }

/* Drop overlay — shown while the user is dragging files over the chat
   view. Pointer events off until visible so it doesn't interfere with
   normal clicks; chat.js toggles .is-visible on dragenter / off on
   dragleave + drop. */
.drop-overlay {
  position: absolute;
  inset: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--bg2) 88%, transparent);
  border: 2px dashed var(--accent);
  border-radius: 6px;
  pointer-events: none;
  opacity: 0;
  transition: opacity .12s ease-out;
}
.drop-overlay.is-visible { opacity: 1 }
.drop-overlay-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .5rem;
  color: var(--accent);
  font-weight: 600;
  font-size: 1.05rem;
}
.drop-overlay-icon { font-size: 2.5rem }

/* Bubble-side rendering — both user and assistant attachments share
   the same visual recipe so the chat reads symmetrically. A notice's text
   (.job-card-body) and a pin opened in the pin list (.pin-list-full) are
   the same renderMarkdown() output and take the same recipe: without it a
   notice's screenshot showed at its full size, 2400 px on a 360 px phone,
   and pushed the view sideways past the screen (14.10).
   The `a.att-file` selector deliberately includes the element type so
   its specificity (0,3,1) matches `.msg.assistant .body a` (0,3,1) and
   wins by source order — without `a.` the cascade would re-apply the
   assistant link rule's `text-decoration: underline` to file chips. The
   twins (0,2,1) outrank their own link rules (0,1,1) outright. */
.msg .body .att-image,
.job-card-body .att-image,
.pin-list-full .att-image {
  display: block;
  max-width: 100%;
  max-height: 280px;
  margin: .35em 0;
  border-radius: 8px;
  border: 1px solid var(--border);
  cursor: zoom-in;
  transition: border-color .12s ease, filter .12s ease;
}
/* The thumbnail is a control: it opens the full-screen viewer
   (image_lightbox.js), so it answers hover and keyboard focus like one. */
.msg .body img.att-image:hover,
.job-card-body img.att-image:hover,
.pin-list-full img.att-image:hover {
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
  filter: brightness(1.05);
}
.msg .body img.att-image:focus-visible,
.job-card-body img.att-image:focus-visible,
.pin-list-full img.att-image:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* A voice reply or an audio file: the browser draws its player 300 px wide
   whatever the room, past a notice card on a 320 px phone (14.10). */
.msg .body .att-audio,
.job-card-body .att-audio,
.pin-list-full .att-audio { max-width: 100% }
.msg .body a.att-file,
.job-card-body a.att-file,
.pin-list-full a.att-file {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  margin: .25em .25em .25em 0;
  padding: .4rem .7rem;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg3, color-mix(in srgb, var(--text) 4%, transparent));
  text-decoration: none;
  color: var(--text);
  font-size: .88rem;
  font-weight: 500;
  line-height: 1.2;
  transition: border-color .12s ease, background-color .12s ease, color .12s ease;
}
.msg .body a.att-file:hover,
.msg .body a.att-file:focus-visible,
.job-card-body a.att-file:hover,
.job-card-body a.att-file:focus-visible,
.pin-list-full a.att-file:hover,
.pin-list-full a.att-file:focus-visible {
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
  background: color-mix(in srgb, var(--accent) 10%, var(--bg3, color-mix(in srgb, var(--text) 4%, transparent)));
  color: var(--text);
  text-decoration: none;
  outline: none;
}
.msg .body a.att-file:active,
.job-card-body a.att-file:active,
.pin-list-full a.att-file:active {
  transform: translateY(1px);
}
.msg .body a.att-file::before,
.job-card-body a.att-file::before,
.pin-list-full a.att-file::before { content: "📎"; opacity: .85; font-size: .95em }

/* Inline error chip for MEDIA links whose backing file no longer exists.
   Replaces the broken-image icon / 404 download dialog with a clear
   "agent referenced a missing file" hint. Triggered by chat.js onerror
   handlers (img/video/audio) and the click-time HEAD-check on att-file. */
.msg .body .att-missing,
.job-card-body .att-missing,
.pin-list-full .att-missing {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  margin: .25em .25em .25em 0;
  padding: .35rem .6rem;
  border-radius: 8px;
  border: 1px solid var(--err);
  background: color-mix(in srgb, var(--err) 8%, transparent);
  color: var(--err);
  font-size: .82rem;
  font-weight: 500;
}
.msg .body .att-missing::before,
.job-card-body .att-missing::before,
.pin-list-full .att-missing::before { content: "⚠️"; opacity: .9 }

/* Screenshot chip — .att-missing geometry, neutral tone: the image WAS
   sent and seen by the agent, only the old-session preview is gone, so
   the red error styling (and its ⚠️) would read as a fault. The 📷 lives
   in the chip text itself (chat.js _screenshotChip). */
.msg .body .att-screenshot {
  border-color: var(--outline-row);
  background: var(--surface-hover);
  color: var(--muted);
}
.msg .body .att-screenshot::before { content: none }

@media (max-width: 680px) {
  .attach-btn { width: 36px; height: 36px }
  .attach-chip { max-width: 160px; font-size: .75rem }
  .msg .body .att-image,
  .job-card-body .att-image,
  .pin-list-full .att-image { max-height: 200px }
}

/* First-visit welcome banner — chat.js renders it once, when the log is
   empty AND the user has never sent a message before. Centered in the
   chat viewport, soft styling so it reads as a hint, not a chat bubble. */
.chat-welcome {
  margin: auto;
  padding: 2rem 1.25rem;
  max-width: 26rem;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .55rem;
  color: var(--muted);
}
.chat-welcome-emoji {
  font-size: 2.4rem;
  line-height: 1;
}
.chat-welcome-title {
  font-size: 1.2rem;
  font-weight: 600;
  color: var(--text);
}
.chat-welcome-body {
  font-size: .94rem;
  line-height: 1.5;
}

/* ── Pinned messages (message_pins.js) ──────────────────────────────── */

/* A message's caption line: its time and its pin. Reserved on every user /
   assistant row, so neither the time (it appears a minute in) nor the pin
   (it shows on hover) moves the conversation when it turns up. */
.msg-meta {
  display: flex;
  align-items: center;
  gap: .1rem;
  min-height: 16px;
}
.msg-row.user .msg-meta { flex-direction: row-reverse }

/* Shared conversations (shared_chat.js, sessions.js sessionLogEntry):
   somebody else's message sits on the far side with their name over it, like
   a group chat; while their turn runs, a line over the composer says who is
   being answered. */
.msg-row.user.is-other { align-self: flex-start; align-items: flex-start; }
.msg-row.user.is-other .msg.user {
  border-radius: 16px 16px 16px 4px;
  border-color: var(--border);
}
.msg-row.user.is-other .msg-meta { flex-direction: row }
.msg-author {
  font-size: .72rem;
  font-weight: 600;
  color: var(--muted2);
  margin-bottom: 3px;
}
.shared-turn {
  font-size: .78rem;
  color: var(--muted2);
  padding: 0 .25rem .35rem;
}
.shared-turn::before {
  content: '\25CF';
  color: var(--accent);
  margin-right: .4rem;
  animation: shared-turn-pulse 1.4s ease-in-out infinite;
}
@keyframes shared-turn-pulse { 50% { opacity: .35 } }
@media (prefers-reduced-motion: reduce) {
  .shared-turn::before { animation: none }
}
/* A 24px target in the caption's 16px line: the margins give the height back. */
.msg-pin {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  margin: -4px 0;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--muted2);
  cursor: pointer;
  box-shadow: none;
  opacity: 0;
  pointer-events: none;
  transition: opacity .15s, color .15s;
}
.msg-pin svg { width: 13px; height: 13px; display: block; fill: none }
/* A touch screen keeps :hover on whatever was tapped last: there a tap shows
   the pin (.tools-open, message_pins.js) and the next hides it. */
@media (hover: hover) {
  .msg-row:hover .msg-pin { opacity: .7; pointer-events: auto }
  .msg-pin:hover { opacity: 1; color: var(--text); background: var(--shell-hover) }
}
.msg-row.tools-open .msg-pin,
.msg-pin:focus-visible { opacity: .7; pointer-events: auto }
.msg-pin:active { transform: none }
.msg-row.is-pinned .msg-pin { opacity: 1; pointer-events: auto; color: var(--accent) }
.msg-row.is-pinned .msg-pin svg { fill: currentColor }
/* No pin where there is nothing to pin yet: an error card, a reply still
   being written (it has no time until it is done), a chat whose first turn
   has not named its conversation. */
.msg-row.error .msg-pin,
.msg-meta:not(:has(> .timestamp)) > .msg-pin,
#log.pins-off .msg-pin { display: none }

/* The bar: the pin shown now, hanging from the main bar over the top of the
   log like the pinned bar of a messenger, in the composer's card. Where the main
   bar has room for it beside the session's title (message_pins.js
   measures; a long title narrows it from that side) it rises into the main
   bar itself, a little slimmer, and the log keeps its top. */
.pin-bar {
  position: absolute;
  top: calc(100% + .45rem);
  left: 0;
  right: 0;
  z-index: 4;
  width: min(var(--content-w), calc(100% - 2rem));
  margin: 0 auto;
  display: flex;
  align-items: center;
  gap: .1rem;
  padding: .2rem .25rem .2rem .2rem;
  background: color-mix(in srgb, var(--composer-bg) 90%, transparent);
  -webkit-backdrop-filter: blur(10px) saturate(1.05);
          backdrop-filter: blur(10px) saturate(1.05);
  border: 1px solid var(--border2);
  border-radius: 12px;
  box-shadow: var(--composer-shadow);
}
.pin-bar[hidden] { display: none }
.pin-bar.is-raised {
  top: 50%;
  translate: 0 -50%;
  padding-block: .1rem;
}
.pin-bar.is-raised .pin-bar-main { padding-block: .2rem }
/* It is the chat's: on any other page it steps out of sight with the title. */
.shell:not([data-main="chat"]) .pin-bar { visibility: hidden }
.pin-bar-main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: .55rem;
  padding: .3rem .5rem;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--text);
  font: inherit;
  text-align: start;
  cursor: pointer;
  box-shadow: none;
}
.pin-bar-main:hover { background: var(--shell-hover); color: var(--text); border: 0 }
.pin-bar-main:active { transform: none }
.pin-bar-icon { flex: none; width: 16px; height: 16px; color: var(--accent); fill: currentColor }
.pin-bar-text {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.pin-bar-label {
  color: var(--accent);
  font-size: .7rem;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: .02em;
}
.pin-bar-count {
  color: var(--muted);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}
.pin-bar-snippet {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: .84rem;
  line-height: 1.3;
}
.pin-bar-list,
.pin-bar-unpin {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--muted2);
  cursor: pointer;
  box-shadow: none;
}
.pin-bar-list[hidden] { display: none }
.pin-bar-list:hover,
.pin-bar-unpin:hover { background: var(--shell-hover); color: var(--text); border: 0 }
.pin-bar-list:active,
.pin-bar-unpin:active { transform: none }
.pin-bar-list svg,
.pin-bar-unpin svg { width: 16px; height: 16px; display: block }
/* While the bar shows, the conversation starts below it. */
.view[data-view="chat"].has-pin-bar #log { padding-top: 4.1rem }
/* Compact layout (the sidebar a drawer, shell.css): the room under the title
   is tighter. The bar hangs right at the main bar's edge, and the chat rises
   by as much into the main bar's empty lower edge (it has no background of
   its own), so the conversation shows that much closer to the title. */
@media (max-width: 820px) {
  .pin-bar { top: 100% }
  .view[data-view="chat"] { margin-top: -.45rem }
}

/* The message a pin led to lights up for a moment. */
@keyframes pin-flash {
  0%, 40% {
    background-color: var(--accent-soft);
    box-shadow: 0 0 0 4px var(--accent-soft);
  }
}
.msg-row.pin-flash .msg { animation: pin-flash 1.6s ease-out }
@media (prefers-reduced-motion: reduce) {
  .msg-row.pin-flash .msg { animation: none; box-shadow: 0 0 0 2px var(--accent-soft) }
}

/* Every pin of the conversation, under the bar. A pin whose message is not
   on the page opens its text in place. */
.menu-pop.pin-list {
  max-width: calc(100vw - 16px);
  max-height: 60vh;
  gap: 2px;
}
.pin-list-item {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  border-radius: 8px;
}
.pin-list-item:hover { background: var(--shell-hover) }
.pin-list-open {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: .4rem .55rem;
  border: 0;
  background: transparent;
  color: var(--text);
  font: inherit;
  text-align: start;
  cursor: pointer;
  box-shadow: none;
}
.pin-list-open:hover { background: transparent; color: var(--text); border: 0 }
.pin-list-open:active { transform: none }
.pin-list-who {
  color: var(--muted);
  font-size: .64rem;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.pin-list-text {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: .83rem;
  line-height: 1.35;
}
.pin-list-unpin {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  margin-block-start: .25rem;
  margin-inline-end: .2rem;
  padding: 0;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--muted2);
  cursor: pointer;
  box-shadow: none;
}
.pin-list-unpin svg { width: 14px; height: 14px; display: block }
.pin-list-text[hidden] { display: none }
.pin-list-unpin:hover {
  background: color-mix(in srgb, var(--text) 10%, transparent);
  color: var(--text);
  border: 0;
}
.pin-list-unpin:active { transform: none }
/* The opened pin's whole text, on a line of its own under the pin. It
   shrinks by its side margins: a full-width line that could not shrink
   stuck out of the list by them, and the list scrolled sideways (14.10). */
.pin-list-full {
  flex: 1 1 100%;
  max-height: 40vh;
  overflow-y: auto;
  margin: 0 .55rem .5rem;
  padding: .45rem .65rem;
  border-inline-start: 2px solid var(--accent);
  color: var(--text);
  font-size: .85rem;
  line-height: 1.5;
  white-space: pre-wrap;
  word-wrap: break-word;
}
