/**
 * The chat surface: controls, the capabilities panel, chips, log, bubbles, meta, progress,
 * composer.
 *
 * Notable fixes carried in this file, each a verified defect in the previous stylesheet:
 *   - The three language pills ate a full header row for a control most guests touch once. They
 *     became one native `<select>`, and that select is now a hidden conduit behind the utility
 *     drawer's language rows (see the Language picker note below, and `.lang-sheet` in
 *     css/glass.css).
 *   - The composer's mic and send buttons sat visibly lower than the text field, because the
 *     character counter's `visibility: hidden` box was still inside the flex row doing the
 *     aligning. The row now contains only the three controls, and all three derive their height
 *     from one token, `--control-h`.
 *   - The conversation had no visible scrollbar (overlay scrollbars). See layout.css.
 *   - Header chrome assumed a dark brand-gradient header and hardcoded white translucency; it
 *     now reads `--ghost-*` / `--header-ink`, so one rule works in both themes.
 *   - `.meta` was fully styled and **never created** by any code path — dead CSS. It is now
 *     real (timestamp + attribution + agent badge).
 *   - Three visual languages in one control strip (`➤` glyph, `🔊`/`🎤` colour emoji). Now one
 *     inline-SVG icon set (js/icons.js) with `currentColor`, so icons inherit the theme.
 */

/* -------------------------------------------------------------------------------------------
 * Buttons — one base, three variants. Every icon button is a real 44px touch target.
 * ----------------------------------------------------------------------------------------- */

.btn {
  font: inherit;
  font-size: var(--fs-small);
  border-radius: var(--radius-pill);
  border: 1px solid transparent;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease);
}

.btn:disabled {
  opacity: 0.45;
  cursor: default;
}

/* Header-chrome buttons. Tokenised rather than hardcoded white translucency, so they work on the
   light header, the dark header, and any tenant brand. */
.btn-ghost {
  background: var(--ghost-bg);
  border-color: var(--ghost-line);
  color: var(--header-ink);
  padding: calc(var(--sp-1) + 2px) var(--sp-3);
  min-height: calc(36px * var(--type-scale));
}

.btn-ghost:not(:disabled):hover {
  background: var(--ghost-bg-hover);
}

.btn-ghost[aria-pressed="true"],
.btn-ghost.active {
  background: var(--accent);
  border-color: color-mix(in srgb, var(--accent-ink) 30%, var(--accent));
  color: var(--accent-ink);
  font-weight: 700;
}

/* The one header control we actively want the room to press. */
.btn-accent {
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
  font-weight: 600;
  padding-inline: var(--sp-4);
}

.btn-accent:not(:disabled):hover {
  background: var(--brand-light);
  border-color: var(--brand-light);
}

.btn-icon {
  width: calc(36px * var(--type-scale));
  min-height: calc(36px * var(--type-scale));
  padding: 0;
}

/* An icon is a <span class="icon"> wrapping an inline <svg> (js/icons.js) — the span carries the
   size so one class controls every icon, and the SVG inherits colour via `currentColor`. */
.icon {
  width: calc(19px * var(--type-scale));
  height: calc(19px * var(--type-scale));
  flex-shrink: 0;
  display: block;
}

.icon > svg {
  width: 100%;
  height: 100%;
  display: block;
}

/* -------------------------------------------------------------------------------------------
 * Language picker — the <select> is a HIDDEN CONDUIT now, not a control.
 *
 * It still holds the value, still fires the `change` event js/app.js listens to, and is still what
 * syncLangSelect() writes; nothing about the language system changed. What changed is that it is no
 * longer drawn. A native select can only show one of its own options when closed and the OS's own
 * list when open — a full-width grey sheet sliding up from the foot of an Android screen — and that
 * was the one object in the header that could not be made to belong to the stationery around it.
 *
 * The visible control is the utility drawer's `.lang-sheet` (markup in index.html, dressed in
 * css/glass.css): one row per endonym, in the drawer's first group. Its rows set this element's
 * value and dispatch `change`, so every downstream path is reached through the code it always was.
 * They were a "◎ EN" popover in the header band until the drawer gave the once-a-stay controls one
 * room to share; the rows themselves are the same rows.
 *
 * ~200 lines went with the old control: the `.lang-field` pill, the abspos globe and caret, and the
 * whole `@supports (appearance: base-select)` block that themed the popup on Chromium 135+. They
 * described an element that no longer exists in the markup, and a stylesheet that dresses a control
 * nobody can see is worse than one that is missing a rule. The popover they were trying to become
 * is now drawn directly, in every engine, at no cost in capability: the same three endonyms, the
 * same checkmark on the current one, the same keyboard model (real buttons).
 *
 * The clip below, not `display: none`: a select removed from layout can behave inconsistently as a
 * programmatic value holder across engines, and this costs nothing. `pointer-events: none` because
 * nothing should ever hit it — the drawer's rows are the target, and this element is `aria-hidden`
 * and `tabindex="-1"` in the markup so a screen reader and the keyboard meet those rows exactly once.
 * ----------------------------------------------------------------------------------------- */

#lang-select {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  pointer-events: none;
  appearance: none;
  -webkit-appearance: none;
}

/* -------------------------------------------------------------------------------------------
 * Capabilities panel.
 *
 * The single most-asked question in a room of officials is "what else does it do?", and the old
 * answer was four bullet points of 12px grey text behind a link. This is now a real capability
 * map: four journey phases, three named capabilities each, all twelve visible at once on a
 * projector.
 *
 * It is a *sheet over* the chat pane, not a band above it. Inline, twelve items need ~450px,
 * which on a 720px-tall laptop left the conversation with nothing — the panel squeezed the demo
 * off its own screen and the cards were sliced mid-sentence by the cap that stopped it. As an
 * overlay it gets the whole pane to breathe in, the conversation behind it is untouched when it
 * closes, and (above 1200px) the receipts rail stays visible alongside, so the routing story and
 * the capability story can be told in the same breath.
 *
 * The capability map itself (#about-panel) has since moved off this shared rule: it expands in
 * place inside the utility drawer now (see .drawer-about, css/glass.css) rather than opening as a
 * fourth full-pane overlay, so the id no longer exists in this file's markup. The other three —
 * the marketplace, sign-in, and the demo guest picker — are still real overlays and still share it.
 * ----------------------------------------------------------------------------------------- */

#market-panel,
#login-panel,
#demo-guest-panel {
  position: absolute;
  inset: 0;
  z-index: 5;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--surface-2);
  border-bottom: 1px solid var(--hairline);
  padding: var(--sp-5) var(--sp-4) var(--sp-5);
  font-size: var(--fs-small);
  color: var(--ink-soft);
  animation: sheet-in var(--dur) var(--ease) both;
}

@keyframes sheet-in {
  from {
    opacity: 0;
    transform: translateY(-8px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/*
 * THE SIGN-IN HAND-OFF's blank gap (see the inline script in index.html's <head>). While
 * `data-entry-signin` is set, `#hero` and `#canvas` — the concierge shell #login-panel is about to
 * cover — are not painted, so there is nothing behind the sheet to flash before js/app.js mounts
 * it. `visibility: hidden`, not `display: none`: layout (and anything measuring it during init,
 * e.g. the orb/composer sizing in js/app.js) stays exactly as it would be once revealed, only the
 * pixels don't paint. What shows instead is `body`'s own canvas colour (css/glass.css), not a blank
 * white flash — the same ground the sheet itself sits on.
 *
 * `#login-panel` is a SIBLING of `#hero`/`#canvas` inside `#chat-pane`, not a descendant, so hiding
 * these two never hides the sheet that is about to cover them.
 */
html[data-entry-signin="1"] #hero,
html[data-entry-signin="1"] #canvas {
  visibility: hidden;
}

.cap-head {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-4);
  margin-bottom: var(--sp-4);
}

.cap-title {
  margin: 0;
  font-size: var(--fs-h2);
  font-weight: 700;
  color: var(--ink);
  line-height: var(--lh-tight);
}

.cap-sub {
  margin: var(--sp-1) 0 0;
  font-size: var(--fs-small);
  color: var(--ink-soft);
  max-width: 72ch;
}

.cap-close {
  margin-left: auto;
  flex-shrink: 0;
  width: calc(36px * var(--type-scale));
  height: calc(36px * var(--type-scale));
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-pill);
  border: 1px solid var(--hairline-strong);
  background: var(--surface);
  color: var(--ink-soft);
  cursor: pointer;
}

.cap-close:hover {
  background: var(--bubble-out);
  color: var(--ink);
}

.cap-groups {
  display: grid;
  /* 200px is chosen so all four phases sit on one row from ~840px of content width upward —
     i.e. on any laptop or projector the demo will actually run on. Below that they wrap and the
     sheet scrolls, which is the right behaviour on a phone. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
  gap: var(--sp-3);
}

.cap-group {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
  box-shadow: var(--shadow-1);
}

.cap-group-label {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin: 0 0 var(--sp-3);
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--brand-ink);
}

.cap-group-label::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--hairline);
}

.cap-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.cap-item {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
}

.cap-item .icon {
  color: var(--brand-ink);
  background: var(--brand-wash-strong);
  border-radius: var(--radius-sm);
  width: calc(28px * var(--type-scale));
  height: calc(28px * var(--type-scale));
  padding: calc(5px * var(--type-scale));
  margin-top: 1px;
}

.cap-text {
  min-width: 0;
}

.cap-name {
  display: block;
  font-weight: 700;
  color: var(--ink);
  font-size: var(--fs-small);
}

.cap-body {
  display: block;
  color: var(--ink-soft);
  font-size: var(--fs-small);
  line-height: var(--lh-body);
}

/* -------------------------------------------------------------------------------------------
 * Suggestion chips
 * ----------------------------------------------------------------------------------------- */

/* Sits between the log and the composer (see index.html), so its rule is a top hairline and the
   composer's own top border is removed to avoid a double line. */
#chips {
  flex-shrink: 0;
  display: flex;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4) 0;
  overflow-x: auto;
  background: var(--paper);
  border-top: 1px solid var(--hairline);
}

#chips:empty {
  display: none;
}

/* `--brand-ink`, not `--brand`: the base brand green is a mid-dark teal that clears only ~3.4:1
   on white and all but disappears on the dark surface. --brand-ink is the per-theme readable
   variant (see tokens.css). */
.chip {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  min-height: var(--tap, 44px); /* was 38px */
  background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--brand-ink) 40%, transparent);
  color: var(--brand-ink);
  border-radius: var(--radius-pill);
  padding: var(--sp-2) var(--sp-4);
  font: inherit;
  font-size: var(--fs-small);
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: var(--shadow-1);
  transition: background var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}

.chip:not(:disabled):hover {
  background: var(--bubble-out);
}

.chip:not(:disabled):active {
  transform: scale(0.97);
}

.chip:disabled {
  opacity: 0.4;
  cursor: default;
  box-shadow: none;
}

/* -------------------------------------------------------------------------------------------
 * Log
 * ----------------------------------------------------------------------------------------- */

#log-wrap {
  flex: 1;
  min-height: 0;
  position: relative;
  display: flex;
}

#log {
  flex: 1;
  /* `scroll`, not `auto`: the track is then part of the layout from first paint, so the room can
     see the conversation is a scrollable record and the column never jumps sideways when the
     first reply pushes it past one screen. Styling lives in layout.css. */
  overflow-y: scroll;
  overflow-anchor: none; /* we control scroll anchoring ourselves in js/render.js */
  padding: var(--sp-4) var(--sp-4) var(--sp-5);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  background-image: radial-gradient(var(--brand-wash) 1px, transparent 1px);
  background-size: 16px 16px;
  scroll-behavior: smooth;
}

/* Anchor the conversation to the bottom, as every chat product does: without it a one-message
   conversation sat at the top of a 900px void on a projector, which reads as a half-loaded page.
 *
 * An auto margin on the first row, *not* `justify-content: flex-end` on #log. End-alignment does
 * not grow the scrollable overflow region: once the transcript outgrew the pane, Chromium reported
 * `scrollHeight === clientHeight` and a max scrollTop of 0 while the older turns sat hundreds of
 * pixels above the top content edge — painted, clipped, and unreachable by wheel, keyboard or JS.
 * An auto margin resolves to 0 as soon as there is no free space, so the overflow scrolls normally
 * and short conversations still rest on the composer. */
#log > *:first-child {
  margin-top: auto;
}

.row {
  display: flex;
  flex-direction: column;
  max-width: min(72ch, 88%);
  animation: bubble-in var(--dur) var(--ease) both;
}

.row.user {
  align-self: flex-end;
  align-items: flex-end;
}

.row.assistant,
.row.error,
.row.system {
  align-self: flex-start;
  align-items: flex-start;
}

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

.bubble {
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--radius);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  word-wrap: break-word;
  overflow-wrap: anywhere;
  box-shadow: var(--shadow-1);
  background: var(--bubble-in);
  border: 1px solid var(--hairline);
  color: var(--ink);
}

.row.user .bubble {
  background: var(--bubble-out);
  border-color: color-mix(in srgb, var(--brand) 22%, transparent);
  border-bottom-right-radius: var(--sp-1);
}

.row.assistant .bubble {
  border-bottom-left-radius: var(--sp-1);
}

.row.error .bubble {
  background: var(--danger-bg);
  color: var(--danger-ink);
  border: 1px solid var(--danger-line);
}

/* -------------------------------------------------------------------------------------------
 * Rendered markdown inside a bubble. The renderer emits real DOM nodes — never innerHTML of
 * model text — so these selectors style genuine <strong>/<ul>/<code> elements.
 * ----------------------------------------------------------------------------------------- */

.bubble > :first-child {
  margin-top: 0;
}

.bubble > :last-child {
  margin-bottom: 0;
}

/*
 * The still-arriving tail of a streaming reply (js/markdown.js createStreamRenderer). A bare
 * wrapper, so the blocks inside it are styled by the same .bubble rules as the closed blocks above
 * it and there is no visible seam between "written" and "being written" — the only thing marking
 * the boundary is the caret below. It exists as one element purely so the tail can be replaced
 * wholesale on each delta without touching the blocks that are already final.
 */
.bubble-pending > :last-child {
  margin-bottom: 0;
}

.bubble p {
  margin: 0 0 0.65em;
}

.bubble strong {
  font-weight: 700;
  color: var(--ink);
}

.bubble em {
  font-style: italic;
}

.bubble ul,
.bubble ol {
  margin: 0.5em 0 0.7em;
  padding-left: 1.35em;
}

.bubble li {
  margin: 0.3em 0;
}

.bubble li::marker {
  color: var(--brand-ink);
}

.bubble code {
  font-family: var(--font-mono);
  font-size: 0.92em;
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  padding: 0.1em 0.35em;
  border-radius: var(--radius-sm);
}

.bubble a {
  color: var(--brand-ink);
  text-underline-offset: 2px;
}

.bubble h3 {
  margin: 0.4em 0 0.3em;
  font-size: var(--fs-h3);
}

/* -------------------------------------------------------------------------------------------
 * .meta — timestamp + attribution + agent badge. Previously styled and never created.
 * ----------------------------------------------------------------------------------------- */

.meta {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-top: var(--sp-1);
  padding-inline: var(--sp-1);
  font-size: var(--fs-micro);
  color: var(--ink-faint);
  flex-wrap: wrap;
}

/* Tighter than `.meta`'s own gap (--sp-2, 8px): a 14px badge wants to sit close to the word it
   marks, not spaced like a second piece of metadata. Negative margin on the following element
   rather than a wrapper, so `.meta`'s flex-wrap keeps treating every child as its own item. */
.meta-mark {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  border-radius: 4px;
  margin-right: calc(var(--sp-2) * -1 + 4px);
}

.meta-author {
  font-weight: 600;
  color: var(--ink-soft);
}

.agent-badge {
  font-size: var(--fs-micro);
  font-weight: 600;
  padding: 1px var(--sp-2);
  border-radius: var(--radius-pill);
  background: var(--brand-wash-strong);
  color: var(--brand-ink);
  border: 1px solid color-mix(in srgb, var(--brand) 30%, transparent);
  white-space: nowrap;
}

.retry-btn {
  font: inherit;
  font-size: var(--fs-micro);
  background: none;
  border: 1px solid var(--danger-line);
  color: var(--danger-ink);
  border-radius: var(--radius-pill);
  padding: 1px var(--sp-2);
  cursor: pointer;
}

/* -------------------------------------------------------------------------------------------
 * Progress line — driven by the real /run_sse event stream, not a timer. Replaces three
 * bouncing dots holding the screen for the whole latency of a multi-agent turn.
 * ----------------------------------------------------------------------------------------- */

.progress {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  font-size: var(--fs-small);
  color: var(--ink-soft);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: var(--sp-2) var(--sp-4);
  box-shadow: var(--shadow-1);
}

.progress-label {
  min-width: 0;
}

.typing {
  display: inline-flex;
  gap: 4px;
  flex-shrink: 0;
}

.typing span {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--brand);
  opacity: 0.5;
  animation: bounce 1.1s infinite ease-in-out;
}

.typing span:nth-child(2) {
  animation-delay: 0.15s;
}

.typing span:nth-child(3) {
  animation-delay: 0.3s;
}

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

/*
 * A caret on the bubble that is currently streaming tokens.
 *
 * It hangs off the last text-bearing element of the pending tail rather than off .bubble itself.
 * As a .bubble::after it was an inline-block appended after the bubble's last child, which was
 * correct while the bubble held a single text node — but the streaming bubble now holds real block
 * elements, and an inline-block after a <p> or a <ul> lands on a line of its own below the text
 * instead of trailing the word being typed.
 */
.bubble.streaming .bubble-pending > p:last-child::after,
.bubble.streaming .bubble-pending > h3:last-child::after,
.bubble.streaming .bubble-pending > ul:last-child > li:last-child::after,
.bubble.streaming .bubble-pending > ol:last-child > li:last-child::after {
  content: "";
  display: inline-block;
  width: 2px;
  height: 1em;
  margin-left: 2px;
  vertical-align: -0.15em;
  background: var(--brand);
  animation: caret 1s step-end infinite;
}

@keyframes caret {
  50% {
    opacity: 0;
  }
}

/* -------------------------------------------------------------------------------------------
 * Jump to latest
 * ----------------------------------------------------------------------------------------- */

/* Off to the right and hard against the foot of the transcript, not centred over it: centred, this
   pill sat squarely on the newest reply — the one thing the guest is reading — and it is a transient
   control, so it should hug the edge the way every scroll-to-latest affordance does. */
#jump-latest {
  position: absolute;
  right: var(--sp-4);
  bottom: var(--sp-3);
  transform: translateY(6px);
  min-height: var(--tap, 44px);
  background: var(--brand);
  color: #fff;
  border: none;
  border-radius: var(--radius-pill);
  padding: var(--sp-2) var(--sp-4);
  font: inherit;
  font-size: var(--fs-small);
  cursor: pointer;
  box-shadow: var(--shadow-2);
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  opacity: 0;
  pointer-events: none;
  /* --ease-out-quart is orb.css's own calm-settle curve (orb-breath/orb-invite) — borrowed here so
     this entrance reads as the same restrained motion language, not a second, snappier one that
     happens to live outside the 72px orb. 280ms rather than --dur's 200ms: a hair slower suits a
     settle, where --dur's snappier feel suits the hover/focus micro-transitions it was tuned for. */
  transition: opacity 280ms var(--ease-out-quart), transform 280ms var(--ease-out-quart);
  z-index: 2;
}

#jump-latest[data-visible="1"] {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

/* -------------------------------------------------------------------------------------------
 * Composer.
 *
 * The alignment rule: mic, field and send are the *only* children of `#composer-row`, and all
 * three are exactly `--control-h` tall in the one-line state — the buttons because that is their
 * width and height, the textarea because its `min-height` is the same token and its vertical
 * padding (`--control-pad`) is derived from it. Bottom-aligned, so the buttons stay level with
 * the last line as the textarea grows. Nothing here is a nudge; the boxes are equal by
 * construction.
 * ----------------------------------------------------------------------------------------- */

#composer {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  padding: var(--sp-3) var(--sp-4);
  padding-bottom: calc(var(--sp-2) + env(safe-area-inset-bottom));
  background: var(--paper);
  /* No top border: #chips directly above already draws the separator from the log. */
}

#composer-row {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-2);
}

#composer-field {
  flex: 1;
  min-width: 0;
  display: flex;
}

/* An auto-growing textarea, not a single-line input: a long question used to scroll out of
   view in a one-line <input> with no way to re-read it before sending. */
#text-input {
  width: 100%;
  resize: none;
  font: inherit;
  font-family: var(--font-indic);
  /* NEVER below 16px. --fs-body resolves to 15px on a phone (its clamp floor), and iOS Safari
     force-zooms the whole page when a focused field is under 16px — the guest then types into a
     zoomed layout with the send button off-screen and has to pinch back out. The max() keeps every
     larger scale (--type-scale, stage mode) working. */
  font-size: max(16px, var(--fs-body));
  line-height: var(--lh-body);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-lg);
  min-height: var(--control-h);
  padding: var(--control-pad) var(--sp-4);
  background: var(--surface);
  color: var(--ink);
  max-height: 8.5em;
  overflow-y: auto;
  box-shadow: var(--shadow-1);
  transition: border-color var(--dur-fast) var(--ease);
}

#text-input::placeholder {
  color: var(--ink-faint);
}

#text-input:focus {
  border-color: var(--brand);
}

#text-input:disabled {
  opacity: 0.6;
}

/* Character counter — appears only when the guest is close to maxlength, so it isn't noise.
   It lives *below* #composer-row, never inside it: as a hidden-but-laid-out sibling of the
   textarea it used to push the mic and send buttons ~18px below the field they belong to.
   `visibility` rather than `display` so its box is always reserved and revealing it never
   shifts the composer mid-sentence. */
#char-count {
  align-self: flex-end;
  font-size: var(--fs-micro);
  line-height: 1.35;
  color: var(--ink-faint);
  padding: 2px var(--sp-3) 0;
  visibility: hidden;
}

/* A genuine warning tier, not just "the count has started being shown". It used to turn visible in
   the same neutral --ink-faint the counter is hidden in, so there was really only one visible
   state (plain) before the hard red stop at 0 — no graduated step in between. */
#char-count[data-near="1"] {
  visibility: visible;
  color: var(--warn-ink);
  font-weight: 600;
}

#char-count[data-at-limit="1"] {
  color: var(--danger-ink);
  font-weight: 600;
}

#send-btn {
  width: var(--control-h);
  height: var(--control-h);
  border-radius: 50%;
  border: none;
  background: var(--brand);
  color: #fff;
  cursor: pointer;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-1);
  transition: background var(--dur-fast) var(--ease);
}

#send-btn:not(:disabled):hover {
  background: var(--brand-light);
}

#send-btn:not(:disabled):active {
  background: var(--brand-dark);
}

/* The composer mic is gone: the hero orb (css/orb.css) is the mic now, so "tap to speak" is the
   whole centre of the screen rather than one round button in the corner. The composer is the
   quiet text fallback. */

/* One class for every "there is nothing for this control to do" case (no worklet, no resolved
   guest, voice killed by env flag). Still used by the tts/theme/rail chrome. */
.hidden {
  display: none !important;
}
