/* ============================================================
   app.css — shared page chrome for the standalone pages
   (account / login / admin / genles / investments).

   These are the rules that used to be copy-pasted into every page's inline
   <style>. They are lifted verbatim from the account page (the reference
   look) so every page starts from the same body, container, header, card,
   form, button and table styling. Colour tokens come from /base.css, theme
   overrides from /theme.css.

   The music shell (index.html) does NOT link this file — it keeps its own
   player layout in /styles.css. Each page loads this BEFORE its inline
   <style>, so page-specific rules (a narrower .container, roomier tables,
   a different h1) still win by source order.
   ============================================================ */

body {
  background: var(--bg);
  color: var(--text);
  /* No web fonts: a system monospace stack keeps every page reachable in
     mainland China. */
  font-family: var(--font-mono);
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  padding: 32px 16px 64px;
}

.container {
  max-width: 920px;
  margin: 0 auto;
}

/* ---------- Top bar: navigation left, wordmark right ---------- */
header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  /* Keep the wordmark pinned top-right; let the nav wrap inside its own flex
     item instead of pushing the wordmark onto a second row. */
  flex-wrap: nowrap;
  max-width: 920px;
  margin: 0 auto 32px;
}

nav {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  flex: 1 1 auto;
  min-width: 0;
}
/* nav a / nav a.active-nav / #language-selector are shared chrome: defined once
   in /base.css and reused here. */

/* ---------- Cards & headings ---------- */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 34px 32px;
}

h1 {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 1.5px;
  color: var(--dim);
  font-weight: 600;
  margin: 0 0 20px;
}

.section-title {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 1.5px;
  color: var(--dim);
  margin: 0 0 10px;
  font-weight: 600;
}

a.link {
  color: var(--accent);
  text-decoration: none;
  font-size: 12px;
}
a.link:hover {
  text-decoration: underline;
}

/* ---------- Forms ---------- */
label {
  display: block;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--dim);
  margin-bottom: 6px;
}
input,
textarea,
select {
  width: 100%;
  background: var(--bg);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 11px 12px;
  color: var(--text);
  font-family: inherit;
  font-size: 14px;
}
input:focus,
textarea:focus,
select:focus {
  outline: none;
  border-color: var(--accent);
}
textarea {
  resize: vertical;
  min-height: 120px;
  line-height: 1.5;
}

button {
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  padding: 11px 14px;
  color: #fff;
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.5px;
  cursor: pointer;
  transition: opacity 0.15s;
}
button:hover {
  opacity: 0.9;
}
button:disabled {
  opacity: 0.55;
  cursor: default;
}

.status {
  font-size: 12px;
  min-height: 17px;
  word-break: break-word;
}
.status.ok {
  color: var(--green);
}
.status.err {
  color: var(--red);
}

/* ---------- Loading / empty / error ---------- */
.loading,
.empty,
.error {
  padding: 40px 10px;
  text-align: center;
  color: var(--dim);
  font-size: 13px;
}
.error {
  color: var(--red);
}

/* ---------- Toolbar ---------- */
.toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  margin-bottom: 14px;
  flex-wrap: wrap;
}
.updated {
  font-size: 11px;
  color: var(--dimmer);
  letter-spacing: 0.5px;
}

/* ---------- Tables ---------- */
.table-wrap {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: 26px;
}
table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  table-layout: fixed;
}
thead th {
  text-align: left;
  padding: 8px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--dim);
  font-weight: 600;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
tbody td {
  padding: 7px 8px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
tbody tr:last-child td {
  border-bottom: none;
}
tbody tr:hover {
  background: var(--surface-2);
}
tfoot td {
  padding: 9px 8px;
  font-weight: 800;
  border-top: 2px solid var(--border-strong);
  font-size: 13px;
}
.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.sym {
  font-weight: 700;
}
.pos {
  color: var(--green);
  font-weight: 600;
}
.neg {
  color: var(--red);
  font-weight: 600;
}

/* ---------- Scrollbar ---------- */
::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}
::-webkit-scrollbar-track {
  background: transparent;
}
::-webkit-scrollbar-thumb {
  background: var(--border-strong);
  border-radius: var(--radius-sm);
}
::-webkit-scrollbar-thumb:hover {
  background: var(--dimmer);
}

@media (max-width: 768px) {
  body {
    padding: 18px 12px 40px;
    font-size: 13px;
  }
}

/* The /admin "chat" tab (public/js/admin/chat.js) — the private chat inbox, laid
   out as the owner's two-pane master/detail: the thread list on the left, the
   open conversation on the right. The message rows + the composer reuse the
   worlds chat chrome loaded from /worlds.css (.worlds-chatbar / .worlds-chat-log
   / .worlds-chat-row / .worlds-chat-form) plus the shared .comment-avatar /
   .comment-main / .chat-time / .chat-presence (/base.css), so a /chat message
   and a console message read identically. */
.chat-preview { color: var(--dim); font-size: 12px; }
/* The list row's live bits: the fan's "typing…" hint (the open conversation shows
   the worlds .worlds-typing inside the chat bar — the same look) and the unread
   count, which borrows the bell's .notif-count colour (the site's unread signal).
   The count's selector carries .chat-inbox so it beats the inline `.tag` rule. */
.chat-typing { color: var(--dim); font-size: 11px; font-style: italic; }
.chat-inbox .chat-unread { color: var(--red); font-variant-numeric: tabular-nums; }
/* Every block of the tab sits on the panel's own 14px gutter (.panel-head /
   .row in admin.html), so the ask bar, the AI panel and the inbox below share
   ONE left edge. */
.chat-ask,
.chat-ask-out,
.ai-presets,
.ai-summary,
.ai-review,
.ai-style,
.chat-inbox { margin: 0 14px 14px; }
.chat-ask { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.chat-ask input {
  flex: 1 1 240px;
  min-width: 0;
  background: var(--bg);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-family: inherit;
  font-size: 12px;
  padding: 8px 10px;
}
.chat-ask input:focus { outline: none; border-color: var(--accent); }
/* The bar's two switches (🌐 all comments / ↩︎ include already replied) are CHECKBOXES,
   not text fields — the `.chat-ask input` rule above would stretch them into 240px
   boxes beside their labels. Put them back to their natural size. */
.chat-ask .ai-auto input {
  flex: 0 0 auto;
  width: auto;
  padding: 0;
  border: 0;
  background: none;
}
.chat-ask-out {
  white-space: pre-wrap;
  word-break: break-word;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  font-size: 12px;
  color: var(--text);
}
.chat-composer textarea {
  width: 100%;
  box-sizing: border-box;
  background: var(--bg);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-family: inherit;
  font-size: 12px;
  padding: 8px;
  /* ONE row, like the worlds game's chat input: the app-wide textarea min-height
     (120px, further up this file) is what would otherwise make this a paragraph
     box, and `flex: 0 0 auto` pins the height against the worlds rule's
     `flex: 1 1 auto` (the composer is a column, so growth would be vertical). The
     inherited `resize: vertical` still lets the owner drag it taller. */
  min-height: 0;
  flex: 0 0 auto;
}
/* The two-pane inbox itself: the list (left) + the open conversation (right) on
   ONE screen. `.chat-pane-conv.worlds-chat-embedded` makes #chatOpen a worlds
   chat bar in the page flow instead of the game's floating drawer (see the
   modifier in /worlds.css). */
.chat-inbox {
  display: grid;
  grid-template-columns: minmax(256px, 1fr) 2fr;
  align-items: start;
  gap: 16px;
}
/* Each pane brings its own inset (.worlds-chatbar's padding / the rows'), so the
   chat list's rows drop theirs and the master column lines up with the panel
   head above it. */
.chat-inbox .row { padding-left: 0; padding-right: 0; }
.chat-pane-list,
.chat-pane-conv { min-width: 0; }
/* The list bar carries the two list-level chips: "show archive" on the left, "next
   unread" on the right (the inbox is inbox-zero — reading a thread files it, so the
   archive is where the read threads live). */
.chat-list-bar { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 0 0 8px; }
/* …and the line under it is the ONE place the console admits the list was CAPPED
   (public/js/admin/chat.js `paintListInfo`): the real size, when the server had to stop
   at its limit. Small print on the list's own surface, so it reads as a note rather than
   a row — the same treatment as the resting pane's `.chat-no-thread`, and `--dim` (never
   `--dimmer`, the deliberately non-text token: see "A contrast floor"). */
.chat-list-info { color: var(--dim); font-size: 12px; padding: 0 0 8px; }
.chat-list-info[hidden] { display: none; }
/* The right half's resting state — the console's default tab, so this is the
   first thing the owner reads. */
.chat-no-thread { color: var(--dim); font-size: 12px; padding: 12px 0; }
.chat-no-thread[hidden] { display: none; }
/* The conversation's head (fan + presence): #chatOpenHead is the only flexible
   item, and the back chip is the phone's return to the list — never desktop's. */
.chat-open-head { min-width: 0; font-size: 12px; color: var(--text-strong); }
.chat-back { display: none; }
/* The composer keeps the console's column shape (the worlds form is a row); the
   textarea's box is the console's own 12px one above — the worlds bar / border /
   radius are the same — and it is ONE row tall (the `min-height: 0` above), like the
   worlds game's chat input. */
.chat-composer { flex-direction: column; }
.chat-open-actions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 8px; }
/* A fan's attached screenshot inside the open conversation (public/js/admin/chat.js):
   the message row points its <img> at GET /api/chat/image/:id, clickable for the
   full-size one. */
.comment-attach { display: inline-block; margin-top: 6px; }
.comment-attach-img {
  display: block;
  max-width: min(360px, 100%);
  max-height: 360px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background-color: var(--surface-2);
}
/* The 💾 chip beside the picture: download-then-clear from the cloud
   (public/js/admin/chat.js `clearChatImage`, the feedback tab's rule). */
.chat-attach-wrap { display: inline-flex; align-items: center; gap: 6px; margin-top: 6px; flex-wrap: wrap; }
.chat-attach-wrap .comment-attach { margin-top: 0; }
.chat-image-clear { font-size: 11px; }
/* A CLEARED screenshot's text id (public/js/admin/chat.js `chatImageIdHtml`): the name
   of the file saved on the owner's device, shown where the picture was — its bytes are
   gone from D1, so this is all that is left of it (docs/features/chat.md -> "Clearing
   images in bulk"). Muted on purpose: it is a record, not a control. */
.chat-image-id {
  display: inline-block;
  margin-top: 6px;
  padding: 1px 6px;
  font-size: 11px;
  color: var(--dim);
  background-color: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

/* THE CONTROL PANEL (Ctrl+K — public/js/admin/chat.js -> "the control panel"; the markup is
   in public/admin.html): the chat tab's shortcuts on ONE card over a dimmed console. A card,
   not a menu: a key opens it, the tab's whole vocabulary is visible at once, and ONE letter
   picks a row (a click does the same). A row that cannot run right now is `.is-off` rather
   than hidden, so the card never changes shape under a habit. */
.chat-cmd-scrim {
  position: fixed;
  inset: 0;
  /* Well above the console's chrome and the worlds rules this page also loads (the game's
     own pinned fallback sits at 200) — the card is a modal, nothing may sit over it. */
  z-index: 500;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 14vh 16px 16px;
  background: rgba(0, 0, 0, 0.6);
}
.chat-cmd-scrim[hidden] { display: none; }
.chat-cmd-card {
  width: min(520px, 100%);
  max-height: 70vh;
  overflow: auto;
  padding: 12px 14px 10px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-2);
  box-shadow: 0 18px 60px rgba(0, 0, 0, 0.65);
}
.chat-cmd-card:focus { outline: none; }
.chat-cmd-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
.chat-cmd-title { color: var(--text-strong); font-size: 12px; letter-spacing: 1px; text-transform: uppercase; }
.chat-cmd-close { border: 0; background: none; color: var(--dim); cursor: pointer; font: inherit; padding: 0 2px; }
.chat-cmd-close:hover { color: var(--text-strong); }
.chat-cmd {
  display: flex;
  align-items: baseline;
  gap: 10px;
  width: 100%;
  padding: 7px 8px;
  border: 0;
  border-top: 1px solid var(--border);
  background: none;
  color: var(--text);
  cursor: pointer;
  font: inherit;
  font-size: 13px;
  text-align: left;
}
.chat-cmd:hover,
.chat-cmd:focus-visible { background: var(--surface-3); }
.chat-cmd kbd {
  flex: 0 0 auto;
  min-width: 20px;
  padding: 0 5px;
  border: 1px solid var(--border-strong);
  border-radius: 4px;
  background: var(--surface);
  color: var(--text-strong);
  font: inherit;
  font-size: 12px;
  text-align: center;
}
/* …and a row that cannot run right now (no draft under the caret, nothing unread) is still
   THERE, just greyed: the key handler skips it and the note under the list says why. */
.chat-cmd.is-off,
.chat-cmd.is-off:hover { background: none; color: var(--dimmer); cursor: default; }
.chat-cmd.is-off kbd { border-color: var(--border); color: var(--dimmer); }
.chat-cmd-state { margin-top: 8px; min-height: 14px; color: var(--dim); font-size: 11px; }

/* The phone line is worlds.css's own (it re-orders + fixes the game's drawer at
   ≤720px), so the console switches its panes there too. */
@media (max-width: 720px) {
  /* A phone shows ONE pane at a time: the conversation replaces the list while a
     thread is open (#chatOpen unhidden), and its #chatBack chip returns. */
  .chat-inbox { grid-template-columns: 1fr; margin: 0 12px 12px; }
  .chat-inbox:has(#chatOpen:not([hidden])) .chat-pane-list { display: none; }
  .chat-back { display: inline-block; }
  .chat-no-thread { display: none; }
  /* worlds.css's phone rules put the game drawer's composer FIRST inside the GAME
     window (its own `.worlds-phone` class — the host card here is a
     `.worlds-chat-embedded` bar, which those rules never match; the game drawer is
     fixed, so the on-screen keyboard would cover it). The console's card sits in the page
     flow, so it keeps the reading order — head, log, typing, composer. */
  .chat-inbox .worlds-chatbar .worlds-chat-log { order: 0; }
  .chat-inbox .worlds-chatbar .worlds-typing { order: 0; }
  .chat-inbox .worlds-chatbar .worlds-chat-form { order: 0; }
}

/* The AI panel inside the chat tab (public/js/admin/ai.js). */
.ai-presets { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.ai-auto { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--dim); }
.ai-summary, .ai-review {
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 10px;
  font-size: 12px;
}
.ai-summary-text { color: var(--text); margin-bottom: 8px; white-space: pre-wrap; }
.ai-cat { margin: 6px 0; }
.ai-cat-title { color: var(--text-strong); margin-bottom: 4px; }
.ai-cat ul { margin: 0; padding-left: 18px; }
.ai-cat li { margin: 3px 0; color: var(--text); }
.ai-kind { color: var(--dim); font-size: 11px; text-transform: uppercase; }
.ai-review-head { color: var(--text-strong); margin-bottom: 8px; }
.ai-draft { border-top: 1px solid var(--border); padding: 8px 0; }
.ai-draft-head { color: var(--text-strong); margin-bottom: 4px; }
/* The row's 💫 problem-queue chip (public/js/admin/ai.js draftHeadHtml -> queueDraftContext):
   a chat draft only, and the same write Ctrl+Shift+U makes — so the head keeps its one-line
   shape and the chip reads as a quiet affordance beside the name, not a second action bar. */
.ai-draft-queue { margin-left: 6px; vertical-align: middle; }
/* The chip's own write state (public/js/admin/ai.js paintQueueChip): ⏳ while the POST flies, then
   ✅ n once the context is on the queue — and the chip INERT in both, which is what stands between
   the owner and a double filing (a second press is answered, never re-sent). Styled here because
   this chip is the one console control that turns itself off as feedback. */
.ai-draft-queue:disabled { cursor: default; opacity: 0.7; }
.ai-draft-queue:disabled.is-done { border-color: var(--green); color: var(--green); opacity: 1; }
.ai-draft-src { color: var(--dim); border-left: 2px solid var(--border-strong); padding-left: 6px; margin-bottom: 4px; white-space: pre-wrap; word-break: break-word; }
/* A CHAT draft's fan message is a TAP HANDLE: the review list's own read-translate — one tap
   shows the English, another flips back (public/js/admin/ai.js draftSrcHtml / paintDraftSource).
   The whole wrap is the handle in BOTH sides; the words themselves keep the chat row's own look
   (`--text`, and `.chat-translated`'s `--accent-fg` for the translation side) from
   public/worlds.css, which the console loads. */
/* The fan-message block above a reply: a chat is answered with ONE message, so every [unread]
   line that reply must cover is stacked here — one bubble per line, oldest first, each its own
   read-translate tap handle (public/js/admin/ai.js draftSrcHtml). A line the fan attached a
   screenshot to shows the picture under its words, OUTSIDE that handle — the wrap's own gap
   spaces it, so the shared .comment-attach top margin must not double up (draftSrcShotHtml). */
.ai-draft-srcs { display: flex; flex-direction: column; gap: 6px; }
.ai-draft-srcs .ai-draft-src { margin-bottom: 0; }
.ai-draft-src.chat-draft-src,
/* …and the same handle inside the Ctrl+Shift+L strip, where the line is a chat row. */
.ai-draft-tail-text.chat-draft-src { cursor: pointer; }
.ai-draft-srcs .comment-attach { margin-top: 0; }
/* The Ctrl+Shift+L strip: the CONVERSATION a focused draft answers, read INSIDE its own row — the
   newest lines with versutus's own replies included, 8 more above them per press
   (public/js/admin/ai.js toggleFocusedDraftThread). It sits between the [unread] burst block and the
   reply box, and it is NOT part of the draft: every way the draft can leave takes it along
   (clearDraftTail), so it is reborn empty and hidden on each render.
   It is drawn as the chat PANE draws a thread — `.worlds-chat-row` / `.comment-avatar` / `.who` /
   `.chat-time` / `.worlds-chat-text` and the two-minute grouping are all worlds.css / base.css
   chrome, the same the right-hand pane uses — because it IS the same conversation one pane down:
   every line carries its author's profile picture and name (the faces arrive WITH the read, see
   public/js/admin/chat.js loadThreadTail, never from whatever thread the pane has open), and a line
   that came with a screenshot shows the picture under its words. TWO deliberate differences from
   the pane's rows: a FAN line's WORDS are the strip's own read-translate tap handle
   (`.ai-draft-tail-text.chat-draft-src`, `data-act="translateContext"` — a tap reads THAT one
   message, while ctrl+shift+k's sweep keeps meaning "the lines this reply answers" because it
   collects `translateSource` handles only), and there are NO chips — nothing inside a draft row may
   quote, translate away, queue or delete a message of a thread the owner has not opened.
   The strip is deliberately UNCAPPED and has no scroll of its own: it grows upward and the PAGE
   moves instead, which is why every press ends by pinning the reply box to the bottom of the screen
   (public/js/admin/ai.js pinDraftBox) — the room above the box is then the conversation, with the
   newest line nearest it. `[hidden]` is re-stated because the flex display would otherwise beat the
   UA's own. */
.ai-draft-context {
  display: flex;
  flex-direction: column;
  gap: 6px;
  border-left: 2px solid var(--accent-fg);
  padding-left: 6px;
  margin: 6px 0 8px;
}
.ai-draft-context[hidden] { display: none; }
.ai-draft-context .ai-draft-n { margin-top: 0; }
/* versutus's own lines: the module stamps `owner-draft-src` on the ROW, and his words keep the
   text-strong the pre-chat strip gave them while a fan's stay the chat's own --text. The avatar and
   the name line already say who wrote either one. */
.ai-draft-context .worlds-chat-row.owner-draft-src .worlds-chat-text { color: var(--text-strong); }
/* The WAITING lines — the ids the ONE reply must answer — are marked on their OWN WORDS: a bar on
   the row would fight the `.worlds-chat-grouped` indent a grouped row gets from worlds.css. */
.ai-draft-context .worlds-chat-row[data-waiting="1"] .worlds-chat-text {
  border-left: 2px solid var(--accent-fg);
  padding-left: 6px;
}
.ai-draft textarea {
  width: 100%;
  box-sizing: border-box;
  background: var(--bg);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-family: inherit;
  font-size: 12px;
  padding: 8px;
  /* The box is SIZED TO ITS TEXT (public/js/admin/ai.js autoGrowDraft): one row for a
     one-liner, N for a longer draft. `overflow-y: auto` stops a scrollbar flickering once
     the box fits its text; `resize: none` stops the drag handle fighting the auto-grow; the
     `40vh` cap keeps a pathological 2000-char draft scrolling in place instead of pushing
     the apply button off-screen. */
  overflow-y: auto;
  resize: none;
  max-height: 40vh;
}
/* A draft the model answered with SEVERAL messages (public/js/admin/ai.js renderReview): one
   box per message, each labelled, so the owner edits them in the order apply posts them. No row
   the server produces has one now (one reply per item — src/chat-ai.js draftReplies); it stays
   for a draft a stale server hands over with several. */
.ai-draft-n { color: var(--dim); font-size: 11px; margin: 4px 0 2px; }
.ai-draft-body + .ai-draft-body { margin-top: 4px; }
/* The ADVANCED LEARNING reason box (public/js/admin/ai.js -> "advanced learning"): the "why did I
   change it" the panel asks the moment a draft is edited, while that tick AND semi-auto are both
   on. It is rendered HIDDEN by every row (draftWhyHtml) and revealed by the first edit, so a list
   of untouched drafts looks exactly as it always did. The dashed left edge is what separates it
   from the reply box above — a note ABOUT the change, never words that go to a fan — and its own
   textarea rides .ai-draft's box styling (autoGrowDraft sizes it) with only the tint changed. Its
   note is read off the DOM the moment the correction leaves (sendOneDraft / collectDrafts). */
.ai-draft-why-wrap { display: flex; flex-direction: column; gap: 2px; margin-top: 6px; padding-left: 6px; border-left: 2px dashed var(--border-strong); }
.ai-draft-why-wrap[hidden] { display: none; }
.ai-draft-why-label { color: var(--dim); font-size: 11px; }
.ai-draft-why { color: var(--dim); }
.ai-like, .ai-supporter { margin-left: 6px; color: var(--dim); font-size: 11px; }
.ai-review-actions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 10px; }

/* The learn run's progress — a CHUNKED run (one request per batch, public/js/admin/ai.js)
   paints this once a batch: a slim bar beside the 📚 chip, with the counts in the tag
   next to it. Hidden outside a run. */
.ai-progress {
  display: inline-block;
  width: 120px;
  height: 6px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  overflow: hidden;
  vertical-align: middle;
}
.ai-progress[hidden] { display: none; }
.ai-progress-fill { display: block; height: 100%; width: 0; background: var(--accent); }

/* The learned voice guide (versutus.md) panel inside the chat tab — the "learn"
   feature (public/js/admin/ai.js + src/chat-ai.js). */
.ai-style {
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 10px;
  font-size: 12px;
}
.ai-style > summary { color: var(--text-strong); cursor: pointer; }
/* LOCKED while a learn run is in flight (public/js/admin/ai.js): the panel is closed and
   refuses to open — the guide it holds is about to be REPLACED, and the refreshed one is
   what unlocks it. */
.ai-style.ai-style-locked > summary { cursor: default; opacity: 0.6; pointer-events: none; }
.ai-style textarea {
  width: 100%;
  box-sizing: border-box;
  margin-top: 8px;
  background: var(--bg);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-family: inherit;
  font-size: 12px;
  padding: 8px;
}
.ai-style-actions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 10px; }
/* The whole md cluster sits in ONE collapsed "learning data" umbrella
   (public/admin.html #aiLearning, public/js/admin/ai.js). The gutter rule further up
   gives EVERY `.ai-style` the panel's own 14px side margin — so a nested md panel (and
   every JS-rendered doc row) would be indented a second time inside the padded
   umbrella. The umbrella owns the gutter; the panels inside only carry the gap between
   them. */
.ai-learning .ai-style { margin: 0 0 8px; }
.ai-learning .ai-style:last-child { margin-bottom: 0; }
