/* settings.css — bits specific to the unified Settings view (search +
   sidebar + category divider) plus shared visual flourishes for sections
   that live inside it (Telegram, LLM rows, voice modes, feedback stars).
   The common section/card vocabulary lives in sections.css. */

/* ── Settings view: outer two-column layout ────────────────────────
   Layout map (active state):
     <section data-view="settings" class="view settings-view active">
       <div class="settings-layout">
         <aside class="settings-sidebar">      ← sticky on desktop
           [search][nav]
         <div class="settings-content">         ← scrollable content
           <div class="settings-category">...</div>
           <details class="panel-section">...</details>
           …

   Base `.view.active` sets `display:flex; flex-direction:column`. We let
   that stand and put `.settings-layout` inside as the single direct child
   so the row layout lives at one level down — keeps `.view`'s overflow
   handling intact while the row breathes underneath. */
.settings-view.active { padding: 0 }
.settings-layout {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  gap: 2rem;
  width: 100%;
  max-width: 1180px;
  margin: 0 auto;
  /* No top padding here — visual top breathing room lives inside the
     sidebar's own padding-top instead. Keeping the layout flush to the
     scroll container means the sticky sidebar's natural position is at
     y=0, which matches its `top: 0` threshold exactly. With any non-zero
     padding-top on the layout, the sidebar's natural y starts BELOW the
     threshold and slides up by that amount before pinning — the visible
     "drift" the user used to see. */
  padding: 0 1.25rem 2rem;
}

/* Sidebar — sticky to the view's scroll container. Nav links scroll into
   the matching category section; the active link gets a glowing accent
   rail that mirrors the main tab underline recipe.

   Sticky `top: 0`: the sidebar's border-top sits flush against the scroll
   container's top edge (no layout padding above it), so the threshold and
   natural position coincide. As soon as the user scrolls 1px, it pins —
   no drift window. Visual top breathing room is supplied by this element's
   own padding-top, which is inside the sticky region and therefore moves
   with the pin. The opaque backdrop keeps content underneath from showing
   through while the sidebar is pinned. */
.settings-sidebar {
  flex: 0 0 220px;
  position: sticky;
  top: 0;
  align-self: flex-start;
  display: flex;
  flex-direction: column;
  gap: .9rem;
  padding: 1.25rem 0 .65rem;
  z-index: 5;
  background: linear-gradient(180deg,
    var(--bg) 0%,
    var(--bg) 88%,
    rgba(0, 0, 0, 0) 100%);
}

.settings-search {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  background: var(--bg);
  padding: .25rem 0;
}
.settings-search-icon {
  position: absolute;
  left: .65rem;
  top: 50%;
  transform: translateY(-50%);
  color: var(--muted2);
  width: 16px;
  height: 16px;
  pointer-events: none;
  display: inline-flex;
}
.settings-search-icon svg {
  width: 100%;
  height: 100%;
}
.settings-search input[type="search"] {
  width: 100%;
  padding: .55rem .85rem .55rem 2.1rem;
  font-size: .88rem;
  color: var(--text);
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: 10px;
  outline: none;
  transition: border-color .15s ease, background .15s ease, box-shadow .15s ease;
}
.settings-search input[type="search"]::placeholder { color: var(--muted2) }
.settings-search input[type="search"]::-webkit-search-cancel-button { display: none }
.settings-search input[type="search"]:focus {
  border-color: var(--accent-dim);
  background: var(--card);
  box-shadow: 0 0 0 3px var(--accent-softer);
}
.settings-search-clear {
  position: absolute;
  right: .35rem;
  top: 50%;
  transform: translateY(-50%);
  width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  color: var(--muted);
  font-size: 1.05rem;
  line-height: 1;
  cursor: pointer;
  border-radius: 6px;
  padding: 0;
}
.settings-search-clear:hover { color: var(--text); background: color-mix(in srgb, var(--text) 5%, transparent) }
.settings-search-clear[hidden] { display: none }

.settings-nav {
  display: flex;
  flex-direction: column;
  gap: .15rem;
}
/* Nav link recipe — mirrors the main page tab style:
     ::before = soft radial halo behind the label
     ::after  = feathered, glowing accent rail (vertical on desktop,
                horizontal underline on mobile)
   Active gets full halo + locked-in rail; hover/focus do partial states. */
.settings-nav-link {
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: center;
  gap: .55rem;
  padding: .5rem .9rem;
  border-radius: 8px;
  color: var(--muted);
  font-size: .85rem;
  font-weight: 500;
  text-decoration: none;
  transition: color .22s cubic-bezier(.4, 0, .2, 1);
}
.settings-nav-link::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(ellipse 110% 90% at 0% 50%,
    color-mix(in srgb, var(--accent) 14%, transparent) 0%,
    color-mix(in srgb, var(--accent) 6%, transparent) 45%,
    transparent 80%);
  opacity: 0;
  transition: opacity .25s cubic-bezier(.4, 0, .2, 1);
  pointer-events: none;
  z-index: -1;
}
.settings-nav-link::after {
  content: '';
  position: absolute;
  left: 0;
  top: 18%;
  bottom: 18%;
  width: 2px;
  background: linear-gradient(180deg,
    transparent 0%,
    color-mix(in srgb, var(--accent) 80%, transparent) 18%,
    var(--accent) 50%,
    color-mix(in srgb, var(--accent) 80%, transparent) 82%,
    transparent 100%);
  border-radius: 999px;
  box-shadow: 0 0 12px color-mix(in srgb, var(--accent) 55%, transparent),
              0 1px 4px color-mix(in srgb, var(--accent) 35%, transparent);
  transform: scaleY(0);
  transform-origin: 50% 50%;
  opacity: 0;
  transition: transform .32s cubic-bezier(.34, 1.25, .5, 1),
              opacity .22s cubic-bezier(.4, 0, .2, 1);
  pointer-events: none;
}
.settings-nav-link:hover {
  color: var(--text);
  text-decoration: none;
}
.settings-nav-link:hover::before { opacity: .9 }
.settings-nav-link:hover::after  { opacity: .55; transform: scaleY(.5) }
.settings-nav-link:focus-visible { outline: none; box-shadow: none }
.settings-nav-link:focus-visible::before { opacity: 1 }
.settings-nav-link:focus-visible::after  { opacity: .75; transform: scaleY(.7) }
.settings-nav-link.active {
  color: var(--accent);
}
.settings-nav-link.active::before { opacity: .7 }
.settings-nav-link.active::after  { opacity: 1; transform: scaleY(1) }
.settings-nav-link.active:hover::before { opacity: .95 }
.settings-nav-link.active:hover::after  { opacity: 1; transform: scaleY(1) }
.settings-nav-link .snl-emoji {
  width: 1.1em;
  text-align: center;
  flex-shrink: 0;
}
.settings-nav-link[hidden] { display: none }

/* ── Shared agent: what is everyone's, what is the reader's own ────────
   On an agent with sharing on (users.js), and on a co-user's panel by
   definition (member_view.js), <body> carries data-shared-agent. Settings
   then say once, at the top, that the cards are the agent's and apply to
   every user; the cards that are the reader's own (data-personal, badged
   by settings.js renderPersonalBadges) explain themselves on hover; and
   "My settings" in the list shows just those. An agent of one user sees
   none of this. */
.settings-nav-link--mine,
.settings-scope-note,
.panel-section-title .personal-badge { display: none }
body[data-shared-agent] .settings-nav-link--mine { display: flex }
body[data-shared-agent] .settings-scope-note { display: flex }
body[data-shared-agent] .panel-section-title .personal-badge { display: inline-flex }
/* The legend belongs to "All options" alone (settings.js mirrors the active
   category on the view as data-category). Over one category, over a card
   on its own, or over "My settings" — which the legend itself points to —
   it is noise. */
body[data-shared-agent] .settings-view:not([data-category="all"]) .settings-scope-note { display: none }

.settings-scope-note {
  align-items: flex-start;
  gap: .6rem;
  margin: 0 0 1.1rem;
  padding: .7rem .9rem;
  font-size: .84rem;
  line-height: 1.45;
  color: var(--muted);
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
  background: var(--accent-softer);
  border-radius: 10px;
}
.settings-scope-note .personal-badge { margin-top: .1rem }

.personal-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.15rem;
  height: 1.15rem;
  flex-shrink: 0;
  border-radius: 999px;
  color: var(--accent);
  background: var(--accent-soft);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  cursor: help;
}
.personal-badge svg { width: .72rem; height: .72rem }
.personal-badge:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
  outline-offset: 1px;
}

/* Content column — mirrors the sidebar's padding-top so the first
   category title visually aligns with the search box. */
.settings-content {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1.75rem;
  padding-top: 1.25rem;
  /* Trailing scroll room. The scroll container (`.view`) stops as soon as
     the content bottom meets the viewport bottom, which would otherwise
     pin the last category (Feedback — the form to the mintbot team) to the
     bottom edge where it's awkward to read and reach. This bottom padding
     lets that final group be scrolled up toward the middle of the screen.
     vh-based so it tracks the window height; capped so very tall monitors
     don't get an oversized empty void. */
  padding-bottom: clamp(2rem, 45vh, 540px);
}

/* Category — a soft section divider above each group of cards. The
   emoji + title sit on a translucent rule that mirrors the Telegram
   header lines for visual continuity. */
.settings-category {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 1rem;
  scroll-margin-top: .5rem;
}
.settings-category > .panel-section {
  flex: 1 1 100%;
  min-width: 0;
}
.settings-category > .settings-category-title {
  flex: 1 1 100%;
}
@media (min-width: 760px) {
  .settings-category > .panel-section[data-half] {
    flex: 1 1 calc(50% - 0.5rem);
    max-width: calc(50% - 0.5rem);
  }
}
.settings-category[hidden] { display: none }
.settings-category-title {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .6rem;
  margin: 0;
  font-size: 1.35rem;
  letter-spacing: -0.005em;
  color: var(--text);
  font-weight: 500;
  line-height: 1.25;
}
.settings-category-title .sct-emoji {
  font-size: 1.4rem;
  line-height: 1;
}

/* Restore panel defaults asks once more, in place: the armed button says so
   in the danger tone (colour on the text alone). */
.panel-reset-btn.is-armed { color: var(--danger-action) }

/* Empty state when search returns no matches. */
.settings-empty {
  text-align: center;
  color: var(--muted);
  font-size: .88rem;
  padding: 2rem 1rem;
  margin: 0;
}
.settings-empty[hidden] { display: none }

/* Search-hidden cards collapse out of the flow so the grid doesn't carry
   phantom gaps. The `.panel-section[data-search-hidden]` attribute is
   toggled by settings.js. */
.panel-section[data-search-hidden] { display: none }
/* "My settings" (settings.js showMine): every card but the reader's own. */
.panel-section[data-scope-hidden] { display: none }

/* Highlighted match — subtle accent halo so the user can spot why a card
   survived the filter. Cleared as soon as the search box empties. */
.panel-section[data-search-match] {
  border-color: color-mix(in srgb, var(--accent) 35%, var(--border));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 18%, transparent);
}

/* Picking a category in the sidebar HIDES the others (settings.js toggles
   `hidden` on each .settings-category), so there is no dim/blur state to
   style here — what's on screen is exactly what the user selected. */

/* ── Responsive: on narrow viewports the chip-rail navigation is hidden
      entirely — the sidebar collapses to just the search box stuck to the
      top, and every settings section is rendered inline below. Avoids the
      cramped horizontal chip scroll that didn't fit a phone screen. */
@media (max-width: 900px) {
  .settings-layout {
    flex-direction: column;
    gap: 1rem;
    padding: 0 1rem 1rem;
  }
  /* The category list is gone at this width and shell.js has moved the
     search into the dialog's head, so the sidebar has nothing left to show. */
  .settings-sidebar { display: none }
  /* Breathing room under the dialog head's rule, so the first category
     heading doesn't sit against it. Stretched, because the column layout
     keeps `align-items: flex-start`: a shrink-to-fit column never gets
     narrower than its widest unbreakable content, and one long word (a
     commit SHA, a toggle row's label) pushed the dialog off a phone screen. */
  .settings-content { padding-top: 1.1rem; gap: 1.4rem; align-self: stretch }
  .settings-category-title { font-size: 1.15rem; gap: .5rem }
  .settings-category-title .sct-emoji { font-size: 1.2rem }
}

@media (max-width: 520px) {
  .settings-layout { padding: 0 .85rem .85rem }
}


/* Generic <details> usage outside of .panel-section gets a quiet inline
   triangle. Used by the model picker / BYOK modal collapsibles. */
details:not(.panel-section) {
  margin-top: .75rem;
  border-top: 1px solid var(--border);
  padding-top: .75rem;
}
details:not(.panel-section) > summary {
  cursor: pointer;
  color: var(--muted);
  font-size: .82rem;
  padding: .2rem 0;
  list-style: none;
  display: flex;
  align-items: center;
  gap: .4rem;
}
details:not(.panel-section) > summary::-webkit-details-marker { display: none }
details:not(.panel-section) > summary::before {
  content: '▸';
  font-size: .7rem;
  transition: transform .15s;
}
details:not(.panel-section)[open] > summary::before { transform: rotate(90deg) }
details:not(.panel-section) > summary:hover { color: var(--text) }
details:not(.panel-section)[open] > summary { margin-bottom: .6rem }

/* ── Star rating widget ─────────────────────────────────────────── */
.star-rating {
  display: inline-flex;
  align-items: center;
  gap: .05rem;
  user-select: none;
  flex-wrap: wrap;
}
.star-rating .star {
  background: transparent;
  border: none;
  padding: .1rem .15rem;
  font-size: 1.15rem;
  line-height: 1;
  color: var(--control-track-off);
  cursor: pointer;
  transition: color .12s ease, transform .12s ease, filter .12s ease;
}
.star-rating .star:hover { transform: scale(1.12) }
.star-rating .star.on {
  color: var(--tier-mid);
  filter: drop-shadow(0 0 4px color-mix(in srgb, var(--tier-mid) 40%, transparent));
}
.star-rating .star:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}
.star-clear {
  margin-left: .5rem;
  font-size: .7rem;
  color: var(--muted);
  cursor: pointer;
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 3px;
}
.star-clear:hover { color: var(--text) }

/* Feedback section: the message textarea + select sit at full width. */
#feedbackCategory {
  width: auto;
  min-width: 180px;
}
#feedbackMsg {
  resize: vertical;
  min-height: 90px;
}

/* Screenshots for the report: the thumbnails, each with its remove, then the
   button that adds more (gone once three are on). The row lights up while
   files are dragged over the card. */
.feedback-image-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  border-radius: 12px;
  outline: 2px dashed transparent;
  outline-offset: 4px;
  transition: outline-color .15s ease;
}
.feedback-images.is-dropping .feedback-image-row { outline-color: var(--accent) }
.feedback-image-list { display: contents }
.feedback-image {
  position: relative;
  flex: none;
  width: 64px;
  height: 64px;
  overflow: hidden;
  border: 1px solid var(--border2);
  border-radius: 10px;
  background: var(--surface-row);
}
.feedback-image img,
.feedback-image video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* A video shows its first frame under a play mark; where no frame can be
   drawn, the mark alone on a dark tile still says "video". */
.feedback-image.is-video { background: #111827 }
.feedback-image.is-video::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  margin: -8px 0 0 -5px;
  border-style: solid;
  border-width: 8px 0 8px 13px;
  border-color: transparent transparent transparent #fff;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .5));
  pointer-events: none;
}
button.feedback-image-remove {
  position: absolute;
  top: 3px;
  right: 3px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: color-mix(in srgb, var(--card) 88%, transparent);
  color: var(--text);
  font-size: .95rem;
  font-weight: 500;
  line-height: 1;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .18);
}
button.feedback-image-remove:hover { background: var(--card); color: var(--err) }
button.feedback-attach {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding-left: .7rem;
}
/* `display: inline-flex` above outranks the UA's `[hidden] { display: none }`. */
button.feedback-attach[hidden] { display: none }
.feedback-attach svg { width: 15px; height: 15px; flex: none }
.feedback-images-hint.is-error { color: var(--err) }

/* ── "What else can I integrate?" info card under Messaging apps ─ */
.msg-supported-list {
  list-style: none;
  margin: .15rem 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: .4rem .8rem;
}
.msg-supported-list li {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  font-size: .9rem;
  color: var(--text);
}
.msg-pill {
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .02em;
  text-transform: uppercase;
  padding: .1rem .4rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--muted);
  background: color-mix(in srgb, var(--text) 3%, transparent);
}
.msg-pill.ok {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}

/* ── Other apps + What else can I integrate? — soft tints to match
      the Telegram section's distinct card feel. Warm amber for the
      request/wishlist block, cool indigo for the informational block. */
.panel-section[data-section="messaging"] .subpanel.msg-other {
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--byok) 7%, transparent),
    color-mix(in srgb, var(--byok) 3%, transparent));
  border-color: color-mix(in srgb, var(--byok) 18%, transparent);
  box-shadow: inset 0 1px 0 color-mix(in srgb, var(--text) 3%, transparent);
}
.panel-section[data-section="messaging"] .subpanel.msg-supported {
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--coding) 7%, transparent),
    color-mix(in srgb, var(--coding) 3%, transparent));
  border-color: color-mix(in srgb, var(--coding) 18%, transparent);
  box-shadow: inset 0 1px 0 color-mix(in srgb, var(--text) 3%, transparent);
}

/* Telegram brand glyph used as the section-head emoji in Settings →
   Messaging apps. Sized to match the surrounding emoji rhythm
   (.panel-section-emoji is width: 1.1em) and tinted the official
   Telegram cyan so it reads as a real logo, not a generic plane. */
.panel-section-emoji--tg {
  color: #229ED9;
}
.panel-section-emoji--tg svg {
  width: 1.15em;
  height: 1.15em;
  vertical-align: -0.22em;
  fill: currentColor;
}

/* ── Telegram card (Settings → Messaging) ──────────────────────────
   Two ways in, side by side: a tile each for the shared bot (recommended,
   nothing to set up) and the owner's own BotFather bot. A tile is an icon,
   a title with its pill, the state on the head's right, one paragraph and a
   foot that carries the roster or the actions. Under them, when the client
   runs several agents, the forum-group call-out. No tinted box around it
   all: the section is the card, the tiles sit on it. */
.tg-card { display: flex; flex-direction: column; gap: .9rem }
.tg-options {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: .9rem;
}
@media (max-width: 720px) {
  .tg-options { grid-template-columns: minmax(0, 1fr) }
}
.tg-option {
  display: flex;
  flex-direction: column;
  gap: .75rem;
  min-width: 0;
  padding: 1rem 1.1rem 1.05rem;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface-subtle);
}
/* The recommended way carries the accent on its edge and a breath of it
   behind the head. */
.tg-option-shared {
  border-color: color-mix(in srgb, var(--accent) 32%, var(--border));
  background:
    linear-gradient(180deg, var(--accent-softer), transparent 4.5rem),
    var(--surface-subtle);
}
.tg-option-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: .5rem .75rem;
}
.tg-option-icon {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.35rem;
  height: 2.35rem;
  border-radius: 11px;
}
.tg-option-icon svg { width: 1.3rem; height: 1.3rem }
.tg-option-icon-shared {
  background: color-mix(in srgb, var(--telegram-text) 14%, transparent);
  color: var(--telegram-text);
}
.tg-option-icon-custom { background: var(--byok-soft); color: var(--byok) }
.tg-option-icon-group  { background: var(--accent-soft); color: var(--accent) }
.tg-option-heading {
  flex: 1 1 10rem;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .3rem .5rem;
  padding-top: .2rem;
}
.tg-option-title {
  margin: 0;
  font-size: .95rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--text);
  overflow-wrap: anywhere;
}
.tg-pill {
  font-size: .62rem;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  padding: .14rem .45rem;
  border-radius: 999px;
  white-space: nowrap;
  line-height: 1.25;
}
.tg-pill-recommended {
  background: var(--accent-soft);
  color: var(--accent);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
}
.tg-pill-beta {
  background: color-mix(in srgb, var(--warn) 15%, transparent);
  color: var(--byok);
  border: 1px solid color-mix(in srgb, var(--warn) 30%, transparent);
}
/* The state sits at the head's right: a flat dot and a short label, green
   only while the thing is actually connected. */
.tg-state {
  flex: 0 0 auto;
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding-top: .45rem;
  font-size: .78rem;
  font-weight: 600;
  color: var(--muted);
}
.tg-state-ok { color: var(--success-status) }
.tg-state strong { font-weight: 600; color: var(--text) }
.tg-option > .tg-info { margin: 0 }
.tg-actions {
  margin-top: auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  padding-top: .15rem;
}
.tg-actions a { text-decoration: none }
.tg-actions button {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .55rem 1.05rem;
}
.tg-info {
  margin: 0;
  font-size: .85rem;
  color: var(--muted);
  line-height: 1.5;
}
.tg-info code {
  background: color-mix(in srgb, var(--text) 6%, transparent);
  padding: .05rem .3rem;
  border-radius: 4px;
  font-size: .8rem;
}
.tg-status {
  display: flex;
  align-items: flex-start;
  gap: .45rem;
  font-weight: 600;
  font-size: .9rem;
  line-height: 1.5;
  color: var(--text);
}
.tg-dot {
  flex: none;
  width: 7px;
  height: 7px;
  margin-top: .48rem;
  border-radius: 50%;
  background: var(--muted2);
}
.tg-dot-ok { background: var(--led-on) }
.tg-status-ok { color: var(--success-status) }
/* Inside a head's state chip the dot centres on its single line. */
.tg-state .tg-dot { margin-top: 0 }

/* Who reaches this agent through the shared bot. The Telegram card and the
   Trusted-users card list the same people, so their rows are built to the same
   measurements — a reader moving between the two recognises the shape. */
.tg-roster { display: flex; flex-direction: column; gap: .45rem; margin-top: .1rem }
.tg-roster-title {
  margin: 0; font-size: .74rem; font-weight: 600; letter-spacing: .05em;
  text-transform: uppercase; color: var(--muted2);
}
.tg-roster-list { display: flex; flex-direction: column; gap: .4rem; list-style: none; margin: 0; padding: 0 }
.tg-roster-row {
  display: flex; align-items: center; gap: .7rem; flex-wrap: wrap;
  padding: .55rem .7rem; background: var(--surface-subtle);
  border: 1px solid var(--border); border-radius: 10px;
}
/* The reader's own row is the only one carrying an action, so it is the only
   one tinted — the rest of the list stays a list. */
.tg-roster-you-row { border-color: var(--accent-soft); background: var(--accent-softer) }
.tg-roster-avatar {
  flex: 0 0 auto; display: flex; align-items: center; justify-content: center;
  width: 1.85rem; height: 1.85rem; border-radius: 50%;
  font-size: .8rem; font-weight: 700; text-transform: uppercase;
  background: var(--accent-soft); color: var(--accent);
}
.tg-roster-id { flex: 1 1 10rem; min-width: 0; display: flex; flex-direction: column; gap: .12rem }
.tg-roster-name { font-size: .88rem; color: var(--text); overflow-wrap: anywhere }
.tg-roster-meta { display: flex; align-items: center; font-size: .74rem; color: var(--muted) }
.tg-roster-handle { overflow-wrap: anywhere }
/* Separator in CSS, not in the markup, so a row with only one of the two
   (an account with no @name, the owner row a co-user sees) still reads right. */
.tg-roster-role + .tg-roster-handle::before { content: "·"; margin: 0 .35rem }
.tg-roster-you {
  padding: .1rem .45rem; border: 1px solid var(--border2); border-radius: 999px;
  font-size: .66rem; font-weight: 600; text-transform: uppercase; letter-spacing: .05em;
  color: var(--muted2); white-space: nowrap;
}
/* Same restraint as the users card: quiet until aimed at. */
.tg-roster-disconnect {
  padding: .3rem .7rem; font-size: .76rem;
  color: var(--muted); background: transparent;
  border: 1px solid var(--border2); border-radius: 8px;
}
.tg-roster-disconnect:hover:not(:disabled) { color: var(--danger-action); border-color: var(--danger-action) }
.tg-roster-note { margin: 0; font-size: .74rem; line-height: 1.45; color: var(--muted2) }
.tg-roster-link { color: var(--accent); font-weight: 600; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px }
.tg-roster-link:hover { text-decoration-thickness: 2px }

.beta-pill {
  font-size: .65rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  padding: .12rem .42rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--warn) 15%, transparent);
  color: var(--byok);
  border: 1px solid color-mix(in srgb, var(--warn) 30%, transparent);
  /* Inline in flowing text (the AXP link in the install card). */
  display: inline-block;
  margin-left: .3rem;
  white-space: nowrap;
}
/* "Connect your Telegram account first" hint shown in place of the
   Start-integration button while no account is bound. */
.tg-custom-bind-first { color: var(--muted2); font-size: .85rem }

/* Multi-agent forum-group call-out (Faas 3) — one band under the tiles:
   icon, the pitch, the actions at the end. Quieter once the group exists. */
.tg-multitopic {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .75rem .9rem;
  padding: .95rem 1.1rem;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface-subtle);
}
.tg-multitopic-main {
  flex: 1 1 16rem;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: .3rem;
}
.tg-multitopic .tg-actions { margin-top: 0; padding-top: 0; flex: 0 0 auto }
.tg-multitopic .tg-state { margin-left: 0; padding-top: 0 }
.tg-multitopic-active { background: transparent }
/* "Read the guide" — quiet text link beside the primary button. */
.tg-multitopic-guide {
  font-size: .85rem;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
}
.tg-multitopic-guide:hover { text-decoration: underline }

/* ── Migration: agent picker + modal directory picker ───── */
/* Inline card is just a picker of the owner's other agents; choosing one
   opens the migration modal (migration_panel.js openMigrationModal). */
.mig-agent-list { display: flex; flex-direction: column; gap: .5rem; margin-top: .4rem }
.mig-agent-row {
  display: flex; align-items: center; gap: .65rem; width: 100%;
  padding: .7rem .85rem; text-align: left; cursor: pointer; font: inherit;
  color: var(--text);
  background: var(--surface-2, color-mix(in srgb, var(--text) 4%, transparent));
  border: 1px solid var(--border, color-mix(in srgb, var(--text) 8%, transparent));
  border-radius: 12px;
  transition: border-color .15s, background .15s, transform .05s;
}
.mig-agent-row:hover {
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}
.mig-agent-row:active { transform: translateY(1px) }
.mig-agent-ico { font-size: 1.1rem; line-height: 1 }
.mig-agent-name { flex: 1 1 auto; min-width: 0; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.mig-agent-go { flex: 0 0 auto; color: var(--muted); font-size: 1.05rem }

.mig-modal { display: flex; flex-direction: column; gap: .9rem }
.mig-modal-source { margin: 0; font-weight: 600; color: var(--text) }
.mig-modal-intro { margin: 0; color: var(--muted); font-size: .88rem; line-height: 1.45 }
.mig-block { display: flex; flex-direction: column; gap: .5rem }
/* `display: flex` outranks the UA `[hidden] { display: none }`, so enterRunningMode()
   flipping el.hidden can't hide these sections without an explicit companion rule. */
.mig-block[hidden] { display: none }
.mig-block-title { margin: 0; font-weight: 600; font-size: .92rem; color: var(--text) }
.mig-block .hint { margin: 0 }
.mig-modal .mig-toggle { display: flex; align-items: flex-start; gap: .6rem; cursor: pointer; font-size: .9rem; color: var(--text); text-transform: none; letter-spacing: normal; font-weight: 400; margin: 0 }
.mig-modal input[type="checkbox"] { width: auto; min-width: 0; height: auto; padding: 0; border: 0; border-radius: 0; background: none; flex: 0 0 auto; accent-color: var(--accent); cursor: pointer }
.mig-modal input[type="checkbox"]:focus { box-shadow: none }
.mig-toggle input { margin-top: .15rem }

.mig-dirs { display: flex; flex-direction: column; gap: .45rem; max-height: 320px; overflow-y: auto }
.mig-group {
  flex: 0 0 auto;           /* don't let an expanded sibling compress collapsed groups -- the 320px .mig-dirs scrolls instead */
  border: 1px solid var(--outline-row);
  border-radius: 12px; overflow: hidden; background: var(--surface-row);
}
.mig-group.open { border-color: color-mix(in srgb, var(--accent) 35%, transparent) }
.mig-group-head { display: flex; align-items: center; gap: .6rem; padding: .6rem .7rem }
.mig-modal .mig-group-check { display: flex; align-items: center; gap: .55rem; flex: 1 1 auto; min-width: 0; cursor: pointer; text-transform: none; letter-spacing: normal; font-weight: 400; margin: 0; font-size: inherit }
.mig-group-check input { accent-color: var(--accent) }
.mig-group-title { font-weight: 600; font-size: .9rem; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.mig-group-meta { flex: 0 0 auto; color: var(--muted); font-size: .8rem; white-space: nowrap }
.mig-group-toggle { flex: 0 0 auto; background: none; border: none; color: var(--muted); cursor: pointer; font-size: .9rem; padding: .15rem .3rem; line-height: 1 }
.mig-group-toggle:hover { color: var(--text) }
.mig-group-list { display: flex; flex-direction: column; border-top: 1px solid var(--border, color-mix(in srgb, var(--text) 6%, transparent)) }
/* `display: flex` above outranks the UA's `[hidden] { display: none }`, so the
   collapse toggle (JS flips list.hidden) needs an explicit rule to actually hide. */
.mig-group-list[hidden] { display: none }
.mig-modal .mig-dir-row { display: flex; align-items: center; gap: .55rem; padding: .45rem .7rem .45rem 1.6rem; cursor: pointer; font-size: .86rem; color: var(--text); text-transform: none; letter-spacing: normal; font-weight: 400; margin: 0 }
.mig-dir-row:hover { background: var(--surface-hover) }
.mig-dir-row input { accent-color: var(--accent) }
.mig-dir-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.mig-dir-meta { flex: 0 0 auto; color: var(--muted); font-size: .78rem; white-space: nowrap }

.mig-actions { flex-wrap: wrap }
.mig-confirm-warn {
  flex: 1 1 100%;
  background: color-mix(in srgb, var(--accent) 7%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
  border-radius: 10px; padding: .6rem .75rem; margin-bottom: .2rem;
}
.mig-confirm-warn p { margin: 0 0 .35rem; font-size: .85rem; color: var(--text); line-height: 1.4 }
.mig-confirm-warn p:last-child { margin-bottom: 0 }
.mig-status { margin: 0; font-size: .87rem; color: var(--muted) }
.mig-status.mig-error { color: var(--danger-action) }
.mig-status.mig-ok { color: var(--accent) }
.mig-error { color: var(--danger-action) }
.mig-warn { color: var(--warn) }
.mig-result-list { margin: .2rem 0 0; padding-left: 1.1rem; font-size: .87rem; color: var(--text); line-height: 1.5 }

/* In-flight move: spinner + staged caption, shown once the options collapse. */
.mig-progress { display: flex; flex-direction: column; align-items: center; gap: .7rem; padding: 1.4rem .5rem .6rem; text-align: center }
.mig-spinner { width: 30px; height: 30px; border-radius: 50%; border: 3px solid var(--border2); border-top-color: var(--accent); animation: mig-spin .8s linear infinite }
.mig-progress-stage { margin: 0; font-weight: 600; font-size: .95rem; color: var(--text); animation: mig-pulse 1.8s ease-in-out infinite }
.mig-progress-route { margin: 0; font-size: .86rem; color: var(--muted); font-variant-numeric: tabular-nums }
.mig-progress-note { margin: 0; font-size: .82rem; color: var(--muted) }
@keyframes mig-spin { to { transform: rotate(360deg) } }
@keyframes mig-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .55 } }
@media (prefers-reduced-motion: reduce) {
  .mig-spinner { animation-duration: 1.6s }
  .mig-progress-stage { animation: none }
}
/* Custom-bot modal — BotFather instructions list + token paste field. */
.tg-custom-bot-modal .tg-bot-steps {
  margin: 0 0 .9rem 1.1rem;
  padding: 0;
  font-size: .87rem;
  color: var(--text);
  line-height: 1.5;
}
.tg-custom-bot-modal .tg-bot-steps li { margin-bottom: .35rem }
.tg-custom-bot-modal .tg-bot-steps code {
  background: color-mix(in srgb, var(--text) 6%, transparent);
  padding: .05rem .3rem;
  border-radius: 4px;
  font-size: .82rem;
}
.tg-custom-bot-modal .tg-bot-label {
  display: block;
  font-size: .8rem;
  color: var(--muted);
  margin-bottom: .25rem;
}
.tg-custom-bot-modal .tg-bot-input {
  width: 100%;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .85rem;
}
.tg-custom-bot-modal .tg-bot-note {
  margin-top: .8rem;
  font-size: .78rem;
  color: var(--muted);
  line-height: 1.45;
}

/* ── LLM providers: the connected providers, managed where they stand ──
 * One bordered column, a hairline between rows. Each row: radio, name over
 * account line, balance, then the actions as quiet links — no buttons, the
 * one solid button on the card is "Add LLM provider" under the list. */
.llm-provider-list {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 640px;
  margin-inline: auto;
  border: 1px solid var(--outline-subtle);
  border-radius: 12px;
  overflow: hidden;
}
.llm-provider-list > .hint { margin: 0; padding: .8rem 1rem }
.llm-provider-row {
  display: flex;
  align-items: center;
  gap: .85rem;
  padding: .72rem .95rem;
  border-top: 1px solid var(--outline-subtle);
  background: var(--surface-subtle);
  color: var(--text);
  cursor: pointer;
  transition: background .15s;
}
.llm-provider-row:first-child { border-top: 0 }
.llm-provider-row:hover { background: var(--surface-hover) }
.llm-provider-row:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 1px var(--focus-outline);
}
.llm-provider-row.active {
  background: color-mix(in srgb, var(--accent) 6%, var(--surface-subtle));
}
.llm-provider-row.busy { opacity: .6; cursor: progress }
.llm-provider-radio {
  position: relative;
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  border: 1.5px solid var(--muted2);
  border-radius: 50%;
}
.llm-provider-row.active .llm-provider-radio { border-color: var(--accent) }
.llm-provider-row.active .llm-provider-radio::after {
  content: '';
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background: var(--accent);
}
.llm-provider-text {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: .1rem;
  min-width: 0;
}
.llm-provider-text strong {
  font-size: .92rem;
  font-weight: 600;
  color: var(--text);
}
.llm-provider-meta {
  display: flex;
  align-items: center;
  gap: .5rem;
  min-width: 0;
  font-size: .78rem;
  color: var(--muted);
  line-height: 1.35;
}
/* Only the account line truncates; the plan chip after it stays whole. */
.llm-provider-meta-id {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.llm-provider-plan {
  flex: 0 0 auto;
  padding: .05rem .4rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--text) 7%, transparent);
  color: var(--muted);
  font-size: .68rem;
  font-weight: 600;
  white-space: nowrap;
}
/* The ChatGPT accounts behind Codex, one line each under the name (only
 * when there are several): the owner's label · the email, the plan,
 * "resting" while Hermes has moved on from it after a usage limit, and its
 * own Remove. The row's Log out drops them all. */
.llm-provider-accounts {
  display: flex;
  flex-direction: column;
  gap: .15rem;
  margin-top: .1rem;
  min-width: 0;
}
.llm-provider-account {
  display: flex;
  align-items: center;
  gap: .5rem;
  min-width: 0;
  font-size: .78rem;
  color: var(--muted);
  line-height: 1.35;
}
.llm-provider-account .llm-provider-act { font-size: .72rem }
.llm-provider-account-note {
  flex: 0 0 auto;
  font-style: italic;
  white-space: nowrap;
}
.llm-provider-account.resting .llm-provider-meta-id { opacity: .7 }
.llm-provider-balance {
  flex: 0 0 auto;
  font-size: .82rem;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.llm-provider-balance:empty { display: none }
.llm-provider-balance-text.warn { color: var(--warn) }
.llm-provider-balance-text.err { color: var(--danger-action) }
.llm-provider-actions {
  display: inline-flex;
  align-items: center;
  gap: .9rem;
  flex: 0 0 auto;
}
.llm-provider-actions:empty { display: none }
/* The actions read as quiet links, whether anchor or button: underline on
 * hover only, the remove turning red. Resets the panel's button chrome. */
.llm-provider-act {
  display: inline-flex;
  align-items: center;
  gap: .28rem;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  font: inherit;
  font-size: .76rem;
  font-weight: 500;
  letter-spacing: 0;
  line-height: 1.2;
  color: var(--muted);
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 3px;
  white-space: nowrap;
  cursor: pointer;
  transition: color .15s, text-decoration-color .15s;
}
.llm-provider-act:hover {
  background: transparent;
  border-color: transparent;
  color: var(--text);
  text-decoration-color: currentColor;
}
.llm-provider-act:active { transform: none }
.llm-provider-act.remove:hover,
.llm-provider-act.remove.confirming {
  color: var(--danger-action);
  text-decoration-color: currentColor;
}
.llm-provider-act.remove.confirming { font-weight: 600 }
.llm-provider-act:disabled { opacity: .5; cursor: default }
/* An install that never finished: not selectable, its status in italics —
 * only its Remove works. */
.llm-provider-row.unfinished { cursor: default }
.llm-provider-row.unfinished:hover { background: var(--surface-subtle) }
.llm-provider-row.unfinished .llm-provider-meta { font-style: italic }
.llm-provider-row.unfinished .llm-provider-radio { border-style: dashed }
/* Ghost row while a publisher's script runs: a spinning ring for the radio,
 * no click — the provider isn't activatable until the script finishes. */
.llm-provider-row.pending {
  cursor: default;
  pointer-events: none;
  opacity: .82;
}
.llm-provider-row.pending .llm-provider-meta { font-style: italic }
.llm-provider-spinner {
  border-color: color-mix(in srgb, var(--text) 14%, transparent);
}
.llm-provider-spinner::after {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: 50%;
  border: 1.5px solid transparent;
  border-top-color: var(--accent);
  animation: llm-provider-spin .85s linear infinite;
}
@keyframes llm-provider-spin {
  to { transform: rotate(360deg) }
}
/* On a phone the actions drop under the text, aligned to it. */
@media (max-width: 560px) {
  .llm-provider-row { flex-wrap: wrap; row-gap: .4rem }
  .llm-provider-actions {
    flex-basis: 100%;
    justify-content: flex-start;
    padding-left: calc(16px + .85rem);
  }
}

/* ── The server's tools, linked from Server access ──────────────── */
.settings-tool-links {
  display: flex;
  flex-wrap: wrap;
  gap: .6rem;
}
.settings-tool-link {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .55rem .9rem;
  border: 1px solid var(--outline-subtle);
  border-radius: 10px;
  background: var(--surface-subtle);
  color: var(--text);
  font-size: .88rem;
  font-weight: 500;
  text-decoration: none;
  transition: background .15s, border-color .15s;
}
.settings-tool-link:hover {
  background: var(--surface-hover);
  border-color: var(--border2);
  color: var(--text);
  text-decoration: none;
}
.settings-tool-link .sb-ico { color: var(--muted) }

/* ── Voice replies (off / auto / on) ───────────────────────────── */
.voice-mode-list {
  display: flex;
  flex-direction: column;
  gap: .35rem;
}
.voice-mode-btn {
  display: flex;
  align-items: center;
  gap: .9rem;
  padding: .65rem .8rem;
  border-radius: 8px;
  border: 1px solid var(--border2);
  background: var(--surface-flat);
  color: var(--text);
  cursor: pointer;
  text-align: left;
  font: inherit;
  transition: border-color .12s, background .12s;
}
.voice-mode-btn:hover:not(:disabled) {
  border-color: var(--accent);
}
.voice-mode-btn:disabled {
  opacity: .55;
  cursor: not-allowed;
}
.voice-mode-btn.active {
  border-color: var(--accent-dim);
  background: var(--selected-neutral-surface);
}
.voice-mode-btn .vm-emoji {
  flex: 0 0 auto;
  font-size: 1.15rem;
  line-height: 1;
}
.voice-mode-btn .vm-text {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: .15rem;
  min-width: 0;
}
.voice-mode-btn .vm-text strong {
  color: var(--text);
  font-weight: 600;
  font-size: .9rem;
}
.voice-mode-btn .vm-sub {
  color: var(--muted);
  font-size: .78rem;
  line-height: 1.35;
}
.voice-mode-btn.active .vm-text strong { color: var(--accent) }

.vm-status { margin: .25rem 0 0; font-size: .75rem; color: var(--err) }
.rs-status { margin: .25rem 0 0; font-size: .75rem }
.rs-status.rs-error { color: var(--err) }

/* Inline voice reply chip — appears under the assistant bubble after a
   TTS playback fires. Compact; uses the browser's native audio controls
   so the user gets familiar play/seek/volume affordances. */
.voice-reply-chip {
  margin-top: .4rem;
  padding: .25rem .35rem;
  border-radius: 8px;
  background: color-mix(in srgb, var(--text) 3%, transparent);
  border: 1px solid var(--border2);
  display: inline-block;
  max-width: 100%;
}
.voice-reply-chip audio {
  display: block;
  width: 280px;
  max-width: 100%;
  height: 32px;
}
/* Visible note that takes the player's place when the audio is missing or
   unplayable. Without this the user just sees an empty 0:00 control. */
.voice-reply-chip.voice-reply-chip-failed {
  font-size: .75rem;
  color: var(--err);
  background: color-mix(in srgb, var(--err) 4%, transparent);
  border-color: var(--err);
}
/* The listen button that stands in for the player while voice auto-play is
   off (Settings → Voice replies): the clip is made when it is pressed. */
.voice-reply-chip.voice-reply-chip-listen {
  padding: 0;
  border: 0;
  background: none;
}
.voice-listen-btn {
  font: inherit;
  font-size: .8rem;
  padding: .3rem .65rem;
  border-radius: 8px;
  border: 1px solid var(--border2);
  background: color-mix(in srgb, var(--text) 3%, transparent);
  color: var(--text);
  cursor: pointer;
}
.voice-listen-btn:hover:not(:disabled) {
  background: color-mix(in srgb, var(--text) 7%, transparent);
}
.voice-listen-btn:disabled {
  opacity: .6;
  cursor: default;
}

/* ── Background-jobs admin (Settings → Taustatööd) ─────────────────────
   Each job renders as a tidy card: name + status pill on the head row,
   muted schedule/next-run meta, a dimmed clamped prompt block, and a
   right-aligned controls row. Palette is borrowed from the panel custom
   props + the green accent used across modal.css — no new brand colours. */
.jobs-admin-list {
  display: flex;
  flex-direction: column;
  gap: .6rem;
  margin-top: .25rem;
}

.jobs-admin-row {
  border: 1px solid var(--outline-subtle);
  border-radius: 12px;
  background: var(--surface-row);
  padding: .8rem .9rem;
  transition: border-color .15s, background .15s, opacity .15s;
}
.jobs-admin-row:hover { border-color: var(--border2) }

/* Paused rows recede so the eye lands on the live ones first, but controls
   stay at full contrast: opacity on the whole row would also grey out its
   Resume / Edit / Delete buttons. */
.jobs-admin-row-paused { background: var(--surface-subtle) }
.jobs-admin-row-paused > .jobs-admin-row-head,
.jobs-admin-row-paused > .jobs-admin-meta,
.jobs-admin-row-paused > .jobs-admin-prompt {
  opacity: .6;
  transition: opacity .15s;
}
.jobs-admin-row-paused:hover > .jobs-admin-row-head,
.jobs-admin-row-paused:hover > .jobs-admin-meta,
.jobs-admin-row-paused:hover > .jobs-admin-prompt { opacity: .8 }

.jobs-admin-row-head {
  display: flex;
  align-items: center;
  gap: .6rem;
  margin-bottom: .35rem;
}
.jobs-admin-name {
  font-size: .92rem;
  font-weight: 600;
  color: var(--text);
  word-break: break-word;
  min-width: 0;
}

/* Status pill — small rounded chip, colour keyed to the job state. */
.jobs-admin-status {
  margin-left: auto;
  flex-shrink: 0;
  padding: .12rem .55rem;
  border-radius: 999px;
  font-size: .66rem;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  border: 1px solid var(--border2);
  background: var(--surface-control);
  color: var(--muted);
}
.jobs-admin-status-active {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 38%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.jobs-admin-status-paused {
  color: var(--muted);
  border-color: var(--border2);
  background: color-mix(in srgb, var(--text) 3%, transparent);
}
.jobs-admin-status-once {
  color: var(--text);
  border-color: var(--border2);
  background: color-mix(in srgb, var(--text) 4%, transparent);
}

.jobs-admin-meta {
  font-size: .74rem;
  color: var(--muted);
  line-height: 1.5;
}

/* Prompt preview — dimmed, subtly bordered, clamped to ~2 lines so a long
   prompt can't bloat the row. */
.jobs-admin-prompt {
  margin-top: .5rem;
  padding: .5rem .6rem;
  border-radius: 8px;
  border: 1px solid var(--outline-row);
  background: var(--surface-subtle);
  font-size: .78rem;
  line-height: 1.5;
  color: var(--muted);
  white-space: pre-wrap;
  word-break: break-word;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.jobs-admin-controls {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: .4rem;
  margin-top: .7rem;
}
.jobs-admin-ctl {
  padding: .35rem .8rem;
  font-size: .78rem;
  border-radius: 8px;
}
/* Danger control (Kustuta) reads distinctly red, matching the two-tap
   confirm behaviour driven from JS. */
.jobs-admin-ctl-danger {
  color: var(--danger-action);
  border-color: color-mix(in srgb, var(--err) 42%, transparent);
  background: color-mix(in srgb, var(--err) 6%, transparent);
}
.jobs-admin-ctl-danger:hover {
  color: var(--danger-action-hover);
  border-color: color-mix(in srgb, var(--err) 65%, transparent);
  background: color-mix(in srgb, var(--err) 14%, transparent);
}

/* Inline edit form nested under a row — clearly framed as a sub-panel. */
.jobs-admin-edit {
  margin-top: .7rem;
  padding: .75rem .8rem;
  border-radius: 10px;
  border: 1px solid var(--outline-row);
  background: var(--surface-row);
}

/* Add-job button row sits below the list, right-aligned. */
.jobs-admin-add-foot { margin-top: .8rem }

/* Invalid field highlight in the add-job modal (empty schedule). */
.field-invalid,
.field-invalid:focus {
  border-color: var(--err) !important;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--err) 35%, transparent);
}
.jobs-admin-modal-status.err { color: var(--err) }

/* Schedule preset picker (add/edit job modal): a full-width <select> with the
   daily-time input revealed just beneath it when "every day at …" is chosen. */
.jobs-admin-sched-select { width: 100% }
.jobs-admin-model-select { width: 100% }
.jobs-admin-sched-time { margin-top: .5rem }
.jobs-admin-sched-time-input { width: 100% }

/* Narrow panel widths — let controls stack full-width so nothing crams. */
@media (max-width: 520px) {
  .jobs-admin-controls { justify-content: stretch }
  .jobs-admin-controls .jobs-admin-ctl { flex: 1 1 auto }
}

/* Settings → Payments → Subscription card (subscription.js). One status
   line (icon + sentence, tinted by state), an optional dt/dd detail grid
   (plan / price / next charge / provider) and an action row whose buttons
   reuse the Agent server category's chip geometry: accent for the primary move
   (subscribe / finish checkout), neutral for the billing-portal link. */
.sub-card { display: flex; flex-direction: column; gap: .75rem }
.sub-status {
  display: flex; align-items: center; flex-wrap: wrap; gap: .4rem;
  font-size: .9rem;
}
.sub-status .sub-icon { font-size: 1rem; line-height: 1 }
.sub-status .sub-text { color: var(--muted); font-weight: 600 }
.sub-status.sub-active    .sub-text { color: var(--accent) }
.sub-status.sub-warn      .sub-text { color: var(--warn) }
.sub-status.sub-canceling .sub-text { color: var(--err) }
.sub-status.sub-none      .sub-text { color: var(--muted) }
.sub-card > .hint { margin: 0 }
.sub-grid {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: .3rem 1.1rem;
  margin: 0;
  font-size: .85rem;
  font-variant-numeric: tabular-nums;
}
.sub-grid dt { color: var(--muted); margin: 0 }
.sub-grid dd { margin: 0; color: var(--text); font-weight: 600 }
.sub-actions { display: flex; flex-wrap: wrap; gap: .55rem; align-items: center }
.sub-btn {
  display: inline-flex;
  align-items: center;
  padding: .55rem 1.2rem;
  border-radius: 8px;
  background: var(--bg-soft, color-mix(in srgb, var(--text) 4%, transparent));
  color: var(--text);
  border: 1px solid var(--border, color-mix(in srgb, var(--text) 18%, transparent));
  font-size: .82rem;
  font-weight: 600;
  text-decoration: none;
  transition: background .16s ease, border-color .16s ease, color .16s ease, transform .08s;
}
.sub-btn:hover {
  background: var(--bg, color-mix(in srgb, var(--text) 8%, transparent));
  border-color: var(--accent);
  color: var(--accent);
  text-decoration: none;
}
.sub-btn:active { transform: translateY(1px) }
.sub-btn-accent {
  background: var(--accent-softer);
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 38%, transparent);
}
.sub-btn-accent:hover {
  background: var(--accent-soft);
  border-color: color-mix(in srgb, var(--accent) 62%, transparent);
}

/* Settings → Payments → e-mail delivery opt-in. The switch reuses the
   auto-updates toggle skin (.au-row/.au-toggle, server.css) and sits under
   the Billing e-mail field it governs, so its top margin has to clear that
   field's hint line. */
.inv-email-toggle { margin: .15rem 0 .3rem }

/* `.panel-section-body .field { display: block }` outranks the UA's
   `[hidden] { display: none }` — the same trap .au-row[hidden] hit — so a
   field JS hides (voicePickField on a pre-rollout central) needs this
   companion rule or it paints anyway. */
.panel-section-body .field[hidden] { display: none }

/* Settings → Payments → Invoice history. Anchor rows download the PDF from
   the agent VPS; a row still ⏳ (undelivered / regeneration in flight) is a
   plain div — same layout, no link. The payment-method chip sits between
   the number and the amount. */
.inv-list { display: flex; flex-direction: column; gap: .3rem }
.inv-row {
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .45rem .65rem;
  border: 1px solid var(--border);
  border-radius: 7px;
  color: inherit;
  text-decoration: none;
  font-variant-numeric: tabular-nums;
}
a.inv-row { transition: border-color .15s ease, background-color .15s ease, color .15s ease }
/* base.css's `a:hover { text-decoration: underline }` outranks the row's own
   `text-decoration: none` (0,1,1 vs 0,1,0), so the whole row underlined on
   hover. Kill it here and let the border + background carry the hover state. */
a.inv-row:hover,
a.inv-row:focus-visible {
  text-decoration: none;
  border-color: var(--accent);
  background: var(--bg2);
}
/* An unlinked ⏳ row carries its "when is this due" hover as a title — the
   help cursor is the only hint that hovering is worth anything, since the
   row has none of a link's affordances. */
div.inv-row[title] { cursor: help }
.inv-row .inv-date { color: var(--muted); flex: 0 0 auto }
a.inv-row:hover .inv-date { color: var(--text) }
.inv-row .inv-num { flex: 1 1 auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis }
.inv-row .inv-method {
  flex: 0 0 auto;
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: .02em;
  color: var(--muted);
  padding: .1rem .45rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  white-space: nowrap;
}
a.inv-row:hover .inv-method { color: var(--text); border-color: var(--border2, var(--border)) }
.inv-row .inv-amt { flex: 0 0 auto; font-weight: 600 }
.inv-row .inv-dl { flex: 0 0 auto; color: var(--muted) }
a.inv-row .inv-dl { color: var(--accent) }

/* ── Settings → Users (trusted co-users) ───────────────────────── */
/* `display: flex` outranks the UA's `[hidden] { display: none }` — the same trap
   .au-row[hidden] hit. Without these companions the share-link row painted an
   empty field + Copy button even with no link, and the whole block stayed
   visible while sharing was off (reported 2026-09). Every class below that sets
   `display` needs its name in this list. */
.users-manage[hidden], .users-ways[hidden], .users-pane[hidden],
.users-pane-row[hidden], .users-list[hidden], .users-list-head[hidden] { display: none }

/* The two ways in, offered as equals. Each card unfolds its own pane; nothing
   below the switch is a form until one of them is picked. */
.users-ways {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: .6rem; margin-top: .9rem;
}
.users-way {
  display: flex; align-items: flex-start; gap: .7rem; width: 100%;
  padding: .8rem .9rem; text-align: left; font: inherit; cursor: pointer;
  color: var(--text); background: var(--surface-subtle);
  border: 1px solid var(--border2); border-radius: 12px;
  transition: border-color .15s ease, background .15s ease;
}
.users-way:hover:not(:disabled) { border-color: var(--accent); background: var(--accent-softer) }
.users-way[aria-expanded="true"] { border-color: var(--accent); background: var(--accent-soft) }
.users-way:disabled { opacity: .5; cursor: not-allowed }
.users-way-icon { font-size: 1.15rem; line-height: 1.35 }
.users-way-text { display: flex; flex-direction: column; gap: .15rem; min-width: 0 }
.users-way-title { font-size: .92rem; font-weight: 600 }
.users-way-sub { font-size: .78rem; line-height: 1.35; color: var(--muted) }

/* The unfolded pane — one field set, tied to the choice above by its edge. */
.users-pane {
  display: flex; flex-direction: column; gap: .5rem;
  margin-top: .6rem; padding: .8rem .9rem;
  background: var(--surface-row); border: 1px solid var(--border2);
  border-left: 2px solid var(--accent); border-radius: 10px;
}
.users-pane-label { margin: 0; font-size: .8rem; font-weight: 600; color: var(--muted) }
.users-pane-row { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem }
.users-pane-row > input { flex: 1 1 15rem; min-width: 0 }
.users-pane-hint { margin: 0 }
.users-share > input { flex-basis: 18rem; font-family: var(--mono, monospace); font-size: .8rem }
.users-status:empty { display: none }

/* The list — one row per user of this agent, the owner included. */
.users-list-head {
  display: flex; align-items: center; gap: .5rem; margin: 1.4rem 0 .6rem;
}
.users-list-title {
  font-size: .78rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: .06em; color: var(--muted2);
}
.users-count {
  padding: .05rem .4rem; font-size: .72rem; font-weight: 600; color: var(--muted);
  border: 1px solid var(--border2); border-radius: 999px;
}
.users-count:empty { display: none }
.users-list { display: flex; flex-direction: column; gap: .4rem; list-style: none; margin: 0; padding: 0 }
.users-row {
  display: flex; align-items: center; gap: .75rem; flex-wrap: wrap;
  padding: .6rem .75rem; background: var(--surface-subtle);
  border: 1px solid var(--border); border-radius: 10px;
  transition: border-color .15s ease;
}
.users-row:hover { border-color: var(--border2) }
.users-avatar {
  flex: 0 0 auto; display: flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem; border-radius: 50%;
  font-size: .85rem; font-weight: 700; text-transform: uppercase;
  background: var(--surface-hover); color: var(--muted);
}
.users-row[data-state="owner"] .users-avatar,
.users-row[data-state="active"] .users-avatar { background: var(--accent-soft); color: var(--accent) }
.users-id { flex: 1 1 12rem; min-width: 0; display: flex; flex-direction: column; gap: .15rem }
.users-name { font-size: .9rem; overflow-wrap: anywhere }
.users-row[data-state="expired"] .users-name,
.users-row[data-state="revoked"] .users-name { color: var(--muted) }
/* The line takes its state's colour; the dot lights in the matching LED
   colour (base.css, "Status dots"). */
.users-meta { display: flex; align-items: center; gap: .35rem; font-size: .76rem; color: var(--muted) }
.users-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
}
.users-row[data-state="active"] .users-meta { color: var(--success-status) }
.users-row[data-state="pending"] .users-meta { color: var(--warn) }
.users-row[data-state="revoked"] .users-meta { color: var(--danger-action) }
.users-row[data-state="active"] .users-dot { background: var(--led-on) }
.users-row[data-state="pending"] .users-dot { background: var(--led-warn) }
.users-row[data-state="revoked"] .users-dot { background: var(--led-err) }
/* The Telegram name rides in the status line but is not a status, so it opts
   out of the tint the row's state sets on .users-meta. The separator is drawn
   here rather than in the markup so a row with no status still reads right. */
.users-handle { color: var(--muted); overflow-wrap: anywhere }
.users-state + .users-handle::before { content: "·"; margin: 0 .35rem }
/* How the membership came about — dates, and the browser behind a share link
   that has no name yet. A step quieter than the status line above it. */
.users-detail { font-size: .72rem; color: var(--muted2); overflow-wrap: anywhere }
.users-badges { display: flex; flex-wrap: wrap; gap: .35rem }
.users-badge {
  padding: .1rem .45rem; border: 1px solid var(--border2); border-radius: 999px;
  font-size: .68rem; font-weight: 600; text-transform: uppercase; letter-spacing: .05em;
  color: var(--muted2); white-space: nowrap;
}
/* Quiet until aimed at: a danger-red button on every row shouted louder than
   the people it was listing. */
.users-revoke {
  padding: .3rem .7rem; font-size: .76rem;
  color: var(--muted); background: transparent;
  border: 1px solid var(--border2); border-radius: 8px;
}
.users-revoke:hover:not(:disabled) { color: var(--danger-action); border-color: var(--danger-action) }
.users-empty {
  margin: .4rem 0 0; padding: .85rem; text-align: center; color: var(--muted);
  border: 1px dashed var(--border2); border-radius: 10px;
}
