/* --- Messages --- */
.msg {
  margin-bottom: 16px;
  max-width: 100%;
}

/* WHERE THE JUMP LANDED — the row "Go to first unread" put at the top of the
   chat, marked for about the second it takes an eye to arrive there
   (goToFirstUnread in public/js/chat.js).
   A wash AND an outline, because a counted row is not one shape: an agency
   notice is a bubble pushed right, the agent's text runs the width, a report is
   a folded chip, an error is a banner. The outline draws whichever it is, and
   outlines take no layout, so nothing on screen moves when it goes on or comes
   off — which matters when the row has just been scrolled to a measured place.
   The theme's accent and not one theme's blue, the same correction the sidebar's
   subagent flash carries (02-sidebar.css). */
#messages > .jumped-to {
  border-radius: 8px;
  outline: 2px solid transparent;
  outline-offset: 3px;
  animation: jump-flash 1s ease-out;
}
@keyframes jump-flash {
  0% {
    background: color-mix(in srgb, var(--accent) 28%, transparent);
    outline-color: var(--accent);
  }
  100% { background: transparent; outline-color: transparent; }
}
/* The mark IS the answer to "which message", so reduced motion keeps it and
   drops only the fade: it stands still for its second instead. */
@media (prefers-reduced-motion: reduce) {
  #messages > .jumped-to {
    animation: none;
    background: color-mix(in srgb, var(--accent) 28%, transparent);
    outline-color: var(--accent);
  }
}

/* Reasoning / extended-thinking bubbles (codex `reasoning` items, claude
   `thinking` blocks) — dimmer + smaller than a real answer, with a rule down the
   side, so scratch thinking reads as distinct from the actual reply. */
.msg-agent.reasoning .bubble {
  opacity: 0.65;
  font-size: 13px;
  border-left: 2px solid var(--border);
  padding-left: 10px;
}
/* A reasoning block renders folded when foldThinking says so (chat.js): every one
   in a DeepSeek session, elsewhere one over 10 lines. Its body holds the
   bubble above, not a tool dump, so it drops the tool body's monospace, pre-wrap,
   break-all and 300px scroll box — opened, it reads like a short block does. */
.msg-agent.reasoning .tool-call-body {
  font-family: inherit;
  white-space: normal;
  word-break: normal;
  max-height: none;
}

/* Collapsed subagent-report chip in the lead's chat (the report bundle arrives as
   a user-role message; we render it click-to-expand instead of a full bubble). */
.msg-report .tool-call { border-left: 2px solid var(--accent); }
/* Body renders Markdown via .bubble (tables/code/lists like a normal message),
   just at a slightly smaller size to read as a nested report. */
.msg-report .report-body {
  word-break: break-word;
  font-size: 13px;
  padding: 4px 2px;
}

/* Two things stacked at the right edge: the bubble, and under it the row of
   chrome that acts on it (.msg-chrome, 14-saved.css). A column rather than the
   `justify-content: flex-end` row this was, because the chrome is a SIBLING of
   the bubble now and not something floating in its corners — it has to take its
   own line, and a line it shares with the bubble is not one.
   `align-items: flex-end` keeps both against the right edge, which is where
   flex-end put the bubble before; the bubble's own `max-width: 80%` still
   resolves against this row and so is unchanged. */
.msg-user {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}
.msg-user .bubble {
  background: color-mix(in srgb, var(--accent) 15%, var(--bg-surface));
  color: var(--text);
  padding: 10px 14px;
  border-radius: 16px 16px 4px 16px;
  max-width: 80%;
  white-space: pre-wrap;
  word-break: break-word;
}
.msg-user .bubble a {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
.msg-user .bubble a:visited { color: var(--accent); }

/* --- Copy button on messages --- */
.msg .bubble {
  position: relative;
}
.copy-btn {
  position: absolute;
  top: 4px;
  right: 4px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 12px;
  padding: 2px 6px;
  opacity: 0;
  transition: opacity 0.15s;
}
.copy-btn.copied { color: var(--green); }
.code-copy-btn {
  top: 4px;
  right: 4px;
  font-size: 11px;
  padding: 1px 5px;
}
@media (hover: hover) {
  pre:hover > .code-copy-btn { opacity: 1; }
}
@media (hover: none) {
  .copy-btn { opacity: 0.5; }
}

.msg-agent .bubble {
  background: var(--bg-surface);
  padding: 12px 16px;
  border-radius: 4px 16px 16px 16px;
  border: 1px solid var(--border);
  max-width: 100%;
  overflow-x: auto;
  /* Font, weight and brightness are user-tunable (Settings → Agent text). Font
     and weight are discrete ladders via data-agent-font / data-agent-weight on
     <html>; brightness is a continuous slider written as an inline --agent-text
     by JS. Defaults below reproduce the original look (system sans, normal
     weight, theme's --text colour). */
  font-family: var(--agent-font, var(--font));
  font-size: var(--agent-size, 14px);
  /* Colour precedence: brightness-mixed --agent-text (set by JS when the slider
     is off-centre) → custom --agent-base hex → theme --text. Brightness mixes
     whatever the base is, so a custom colour and the slider compose. */
  color: var(--agent-text, var(--agent-base, var(--text)));
  font-weight: var(--agent-weight, 400);
  /* The other two message bodies have carried a wrap rule from the start
     (.msg-user .bubble, .msg-report .report-body); this one never did, and a
     path in inline code is one long token with no space in it — the browser
     will not break on a slash or a dot, so it ran past the bubble and the
     bubble's own overflow-x turned a plain answer into something you scroll
     sideways. Measured at 390px with `Lambda.Cad.Core/docs/audit/README.md`:
     291px of token in 299 of line, so it fit by 8 — and overflowed by 23 at the
     L text size, 67 at XL, 9 at 360px and 49 at 320px. That 8px is why it only
     happened sometimes.
     `break-word` rather than `anywhere`, and the difference is not academic
     here: both wrap the prose identically, but `anywhere` also shrinks
     min-content, which relaid the tables in this bubble — the table went 370px
     to 324 and .md-table-scroll stopped scrolling. Tables are a separate
     question; this rule leaves them exactly as they were.
     `pre` is untouched either way: `white-space: pre` forbids the soft wrap
     this property chooses between, so blocks still scroll inside themselves,
     which is what a code block should do. */
  overflow-wrap: break-word;
}
/* Code keeps the mono font and normal weight regardless of the prose settings,
   so picking a different prose font/weight never restyles code blocks. Its size
   is relative (em) so the size control scales code along with the prose. */
.msg-agent .bubble pre,
.msg-agent .bubble pre code,
.msg-agent .bubble code { font-family: var(--mono); }
.msg-agent .bubble pre,
.msg-agent .bubble pre code { font-weight: 400; }

/* Discrete ladders (set on <html>; default = first/normal when absent).
   All font options are sans-serif system stacks. */
html[data-agent-font="system"]    { --agent-font: var(--font); }
html[data-agent-font="helvetica"] { --agent-font: "Helvetica Neue", Helvetica, Arial, sans-serif; }
html[data-agent-font="verdana"]   { --agent-font: Verdana, Geneva, "DejaVu Sans", Tahoma, sans-serif; }
html[data-agent-size="s"]  { --agent-size: 13px; }
html[data-agent-size="m"]  { --agent-size: 14px; }
html[data-agent-size="l"]  { --agent-size: 16px; }
html[data-agent-size="xl"] { --agent-size: 18px; }
html[data-agent-weight="light"]  { --agent-weight: 300; }
html[data-agent-weight="normal"] { --agent-weight: 400; }
html[data-agent-weight="medium"] { --agent-weight: 500; }
html[data-agent-weight="bold"]   { --agent-weight: 600; }

.msg-agent .bubble p { margin-bottom: 8px; }
.msg-agent .bubble p:last-child { margin-bottom: 0; }
/* Restore list padding stripped by the global reset — otherwise ol/ul
   markers (list-style-position: outside by default) sit left of the
   bubble's content box and get clipped by padding + overflow-x:auto.
   Two-digit numbers like "10." are the most visible casualty. */
.msg-agent .bubble ol,
.msg-agent .bubble ul {
  padding-left: 1.75em;
  margin: 8px 0;
}
.msg-agent .bubble ol:last-child,
.msg-agent .bubble ul:last-child { margin-bottom: 0; }
.msg-agent .bubble li { margin: 2px 0; }
.msg-agent .bubble li > p { margin-bottom: 4px; }
/* The code block, wherever it is drawn: the transcript's bubbles and Settings'
   own panels (a login run's output, a host tool's). One rule, because a block
   that only LOOKS like the chat's — no sideways scroll, no gutter for the
   per-line mark — is a block whose copy buttons are decoration (the owner,
   2026-09-21). */
.msg-agent .bubble pre,
.code-block {
  background: var(--bg);
  padding: 12px;
  /* The gutter, and the only thing the per-line copy mark costs a block nobody
     is pointing at: 10px more on the left, which is what holds a 17px mark that
     starts 2px in and ends 3px clear of the block's own box. Measured at 1280:
     the <code> box starts at 22 into the <pre> instead of 12, and nothing else
     about the block moves. The mark is absolute in THIS padding (copy.js), so it
     never pushes the text — and it has to be this padding rather than the 11.8px
     the highlighter's stylesheet gives `.hljs` inside it, because that one is
     within the box that scrolls sideways: a block scrolled right would run its
     code under a mark standing there. */
  padding-left: 22px;
  border-radius: 6px;
  overflow-x: auto;
  margin: 8px 0;
  font-size: 0.92em;
}
/* --- One line of a code block ---
   Every line of a rendered block is an element of its own (wrapCodeLines,
   copy.js), so that a mark can stand beside ONE of them and a flash can land on
   one. `display: block` is the whole of the drawing: the line's newline is still
   inside it, so the text is unchanged and an empty line keeps its height from
   that newline rather than from a min-height. Full width, so the whole row
   answers the pointer and not just the characters on it. */
.code-line { display: block; }
/* THE FLASH — the line says what was taken, for the ~500ms the class is on it
   (copy.js). The transition is on the resting rule and not on the flash, so the
   colour arrives at once and FADES out: an acknowledgement that eased in would
   be half over before it was visible. */
.code-line { transition: background-color 0.35s ease; }
.code-line.code-line-copied {
  background-color: color-mix(in srgb, var(--accent) 22%, transparent);
  transition: none;
}
@media (hover: hover) {
  /* The mark is a `.msg-copy.code-copy` like the block's own two buttons, so the
     box, the chip, the muted colour, the accent under the pointer and the reveal
     (`pre:hover .msg-copy.code-copy`) are all already written — 14-saved.css.
     What is left is where it stands: 2px into the padding this block grew, at
     the top and height of the line under the pointer, both set by copy.js.
     `user-select: none` keeps it out of a selection dragged over the block; it
     carries no text node either, so nothing it draws can reach a clipboard.
     This query is belt and braces: where there is no pointer copy.js makes no
     mark at all, so there is nothing here to hide. */
  .msg-copy.code-line-copy {
    left: 2px;
    padding: 0 2px;
    user-select: none;
  }
}
/* KaTeX display formulas can be wider than the bubble — scroll instead of
   overflowing the layout. (KaTeX glyphs are font-based and inherit color, so
   they follow the theme automatically; no color rule needed.) */
.msg-agent .bubble .katex-display {
  overflow-x: auto;
  overflow-y: hidden;
  padding: 2px 0;
}
/* Codex inline-vis artifacts (::codex-inline-vis{file=…}) rendered as an
   openable link — a small marker sets them apart from plain file links. */
.codex-vis-link::before {
  content: "▶ ";
  opacity: 0.7;
}
.msg-agent .bubble a {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
.msg-agent .bubble a:visited { color: var(--accent); }
/* An inline image that failed to load, shown as its path plus why
   (watchInlineImage, linkify.js). Outside the link, so not underlined. */
.inline-image-hint {
  margin-left: 6px;
  color: var(--text-muted);
  font-size: 0.88em;
}
.inline-image {
  max-width: 100%;
  max-height: 400px;
  border-radius: 6px;
  margin: 6px 0;
  display: block;
  cursor: pointer;
}
.msg-agent .bubble code {
  font-family: var(--mono);
  font-size: 0.92em;
}
.msg-agent .bubble :not(pre) > code {
  background: var(--bg);
  padding: 2px 6px;
  border-radius: 4px;
}

/* --- Working indicator --- */
#working-bar {
  position: relative;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 16px;
  background: var(--bg-surface);
  border-top: 1px solid var(--border);
  overflow: hidden;
}
#working-bar .working-progress {
  position: absolute;
  top: 0;
  left: 0;
  height: 2px;
  width: 30%;
  background: var(--accent);
  opacity: 0.4;
  animation: working-slide 3s ease-in-out infinite;
}
@keyframes working-slide {
  0% { left: -30%; }
  100% { left: 100%; }
}
/* A drawn mark from the sprite (activity.js picks it), 13px like the emoji it
   replaced. Flex, so the svg is centred on the bar's row by its box rather than
   sat on a text baseline 0.125em low. */
#working-bar .working-icon { display: flex; }
#working-bar .working-icon:empty { display: none; }
/* The mark moves only while nothing has come back yet, and each motion says
   which kind of nothing. Booting — the CLI still loading — turns the ring, at
   the pace of a spinner that is not in a hurry. Thinking breathes: opacity and
   not dot-breathe's hue drift, which animates a background and would have to
   borrow the running dot's orange for a mark that is otherwise the bar's text
   colour — and it never drops below half, so the mark is always there to read.
   A tool in hand holds still: the label beside it is changing already. */
#working-bar[data-phase="boot"] .working-icon .icon { animation: spin 1.2s linear infinite; }
#working-bar[data-phase="think"] .working-icon .icon { animation: working-breathe 1.6s ease-in-out infinite; }
@keyframes working-breathe {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.45; }
}
/* The same two selectors, not a shorter one: `#working-bar .working-icon .icon`
   loses to the attribute in each of them, and the marks went on moving. */
@media (prefers-reduced-motion: reduce) {
  #working-bar[data-phase="boot"] .working-icon .icon,
  #working-bar[data-phase="think"] .working-icon .icon { animation: none; }
}
#working-bar .working-text {
  font-size: 11px;
  color: var(--text);
  font-weight: 600;
  letter-spacing: 0.02em;
  flex-shrink: 0;
}
#working-bar .working-detail {
  font-size: 11px;
  color: var(--text);
  font-family: var(--mono);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 1px 6px;
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#working-bar .working-detail:empty { display: none; }
/* Trailing the line, not tucked behind the label. It is the one part of this bar
   that is a MEASUREMENT rather than a description, and it is now the only place
   an open session states its elapsed time — the sidebar rows gave theirs up. So
   it gets a fixed edge to be read at, instead of sliding left and right as the
   label and the detail chip change length under it. */
#working-bar .working-timer {
  margin-left: auto;
  font-size: 10px;
  color: var(--text-muted);
  font-family: var(--mono);
  opacity: 0.6;
  flex-shrink: 0;
}
#working-bar .working-timer:empty { display: none; }

/* --- Queue bar (above input) --- */
#queue-bar,
#inbox-bar,
#pins-bar,
#wakeups-bar,
#owed-bar {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 6px 16px;
  background: var(--bg-surface);
  border-top: 1px solid var(--border);
  max-height: 140px;
  overflow-y: auto;
}
.wake-chip {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  background: var(--bg-surface2);
  border: 1px solid color-mix(in srgb, var(--orange) 35%, var(--border));
  border-radius: 6px;
  font-size: 13px;
  color: var(--text);
}
.wake-chip-icon { flex-shrink: 0; color: var(--orange); }
/* A chip whose wake-up has already fired is not counting down to anything, so
   its own colour stops being about a schedule. Kept quiet: what it is doing is
   said by the word in the slot below, in the shared vocabulary. */
.wake-chip.held, .wake-chip.parked, .wake-chip.fired { border-color: var(--border); }
.wake-chip.parked { border-color: color-mix(in srgb, var(--orange) 45%, var(--border)); }
.wake-chip-eta {
  flex-shrink: 0; color: var(--text-muted); font-family: var(--mono); font-size: 11px;
}
.wake-chip-text {
  flex: 1; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  color: var(--text-muted); font-size: 12px;
}
/* Fire-now and cancel are one pair of controls, drawn alike — the pin chip's
   insert and remove are the precedent, down to the hover colours. */
.wake-chip-fire,
.wake-chip-cancel {
  background: none; border: none; color: var(--text-muted); cursor: pointer;
  font-size: 13px; padding: 2px 6px; flex-shrink: 0; border-radius: 4px;
  transition: color 0.15s, background 0.15s;
}
@media (hover: hover) {
  .wake-chip-fire:hover { color: var(--accent); background: rgba(78,168,222,0.1); }
  .wake-chip-cancel:hover { color: var(--red); background: rgba(239,83,80,0.1); }
}
