/* Global Styles - default theme is a dark, terminal-inspired palette matching mixers.media */
:root {
  --bg: #121110;
  --surface: #1a1815;
  --surface-alt: #201d19;
  --border: #39332a;
  --text: #eee7d8;
  --text-muted: #9a9184;
  --accent: #e8963c;
  --accent-hover: #f2a94f;
  --accent-soft: #3a2c1a;
  --danger: #e0605a;
  --danger-soft: #402320;
  --radius: 12px;
}

/* Opt-in light theme, toggled via the topbar button (persisted in localStorage). */
html.theme-light {
  --bg: #f7f5f2;
  --surface: #ffffff;
  --surface-alt: #f1ede6;
  --border: #e5e1da;
  --text: #2b2b2b;
  --text-muted: #6b6b6b;
  --accent: #b5651d;
  --accent-hover: #9c541a;
  --accent-soft: #fbf1e7;
  --danger: #c0392b;
  --danger-soft: #f5d7d0;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, Menlo, Consolas, 'Courier New', monospace;
  background-color: var(--bg);
  color: var(--text);
  line-height: 1.5;
}

.hidden { display: none !important; }

button {
  font-family: inherit;
  cursor: pointer;
}

input, textarea, select {
  font-family: inherit;
  font-size: 15px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  width: 100%;
  background: var(--surface);
  color: var(--text);
}

label { display: block; font-size: 13px; color: var(--text-muted); margin: 10px 0 4px; }

.btn-primary, .btn-secondary, .btn-ghost, .btn-danger {
  border: none;
  border-radius: 8px;
  padding: 10px 16px;
  font-size: 15px;
}
.btn-primary { background: var(--accent); color: white; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-secondary { background: var(--border); color: var(--text); }
.btn-ghost { background: transparent; color: var(--text); }
.btn-ghost:hover { background: var(--border); }
.btn-danger { background: var(--danger); color: white; }
.full-width { width: 100%; }

/* Anything that navigates is a real <a href> so it can be middle-clicked, ctrl-clicked or
   copied like any other link - these rules are what make one wearing a .btn-* class still
   look and size like the button it replaced. */
a.btn-primary, a.btn-secondary, a.btn-ghost, a.btn-danger {
  display: inline-block;
  text-decoration: none;
  text-align: center;
  line-height: normal;
}
a.btn-ghost.full-width, a.btn-secondary.full-width { display: block; }
/* A character's name used as a link inside a row of other text - it has to read as part of that
   row until it's pointed at, rather than as a blue link dropped into it. */
.character-name-link { color: inherit; text-decoration: none; }
.character-name-link:hover, .character-name-link:focus-visible { color: var(--accent); text-decoration: underline; }

.form-error { color: var(--danger); font-size: 13px; margin-top: 8px; min-height: 16px; }
.field-hint { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
.field-ok { color: #2e7d32; }
.field-error { color: var(--danger); }

/* ---------------- Auth pages ---------------- */
.auth-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.auth-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 32px;
  width: 100%;
  max-width: 420px;
}
.auth-card h1 { margin-bottom: 8px; }
.auth-card-header { display: flex; align-items: center; justify-content: space-between; }
.auth-card-header h1 { margin-bottom: 0; }
.auth-subtitle { color: var(--text-muted); font-size: 14px; margin-bottom: 20px; }
.auth-form { display: flex; flex-direction: column; }
.auth-form h2 { margin: 4px 0 12px; font-size: 18px; }
.auth-form button { margin-top: 16px; }
hr { border: none; border-top: 1px solid var(--border); margin: 24px 0; }

/* ---------------- App shell ---------------- */
.app-shell {
  height: 100vh;
  display: flex;
  flex-direction: column;
}

.topbar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}
.topbar .brand { font-size: 18px; flex: 1; }
.topbar-actions { display: flex; gap: 8px; align-items: center; margin-left: auto; }
/* Only shown on mobile, where the site name moves out of the topbar and into this menu. */
.sidebar-brand {
  display: none;
  font-size: 16px;
  font-weight: 600;
  padding: 2px 4px 10px;
  border-bottom: 1px solid var(--border);
}
.badge {
  display: inline-block;
  margin-left: 6px;
  background: var(--accent);
  color: white;
  font-size: 11px;
  line-height: 1;
  padding: 3px 6px;
  border-radius: 999px;
}
.icon-btn {
  background: none;
  border: none;
  font-size: 20px;
  display: none;
}

.layout { flex: 1; display: flex; overflow: hidden; }

.sidebar {
  width: 280px;
  border-right: 1px solid var(--border);
  background: var(--surface);
  padding: 12px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.nav-group { display: flex; flex-direction: column; gap: 2px; }
.nav-btn { text-align: left; }
/* Marks whichever nav button owns the current route (set by the router). */
[data-route].active { background: var(--accent-soft); color: var(--accent); }
.chats-list { display: flex; flex-direction: column; gap: 6px; }
.chat-list-item {
  display: flex;
  align-items: center;
  gap: 4px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
  padding: 6px 6px 6px 10px;
}
.chat-list-item span { font-size: 12px; color: var(--text-muted); }
.chat-list-item:hover { background: var(--surface-alt); }
.chat-list-item.active { border-color: var(--accent); background: var(--accent-soft); }
.chat-list-item-row { flex: 1; min-width: 0; cursor: pointer; }
.chat-delete-btn {
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: 18px;
  line-height: 1;
  padding: 4px 8px;
  border-radius: 6px;
  opacity: 0;
}
.chat-list-item:hover .chat-delete-btn, .chat-list-item:focus-within .chat-delete-btn { opacity: 1; }
.chat-delete-btn:hover { background: var(--border); color: var(--danger); }

.chats-section-heading {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  padding: 8px 4px 2px;
}

.char-group { display: flex; flex-direction: column; }
.char-group-header {
  display: flex;
  align-items: center;
  gap: 8px;
  background: transparent;
  border: none;
  border-radius: 8px;
  padding: 6px 10px;
  text-align: left;
  font-size: 14px;
  color: var(--text);
}
.char-group-header:hover { background: var(--surface-alt); }
.char-group-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.char-group-count { font-size: 11px; color: var(--text-muted); background: var(--surface-alt); border-radius: 999px; padding: 1px 7px; }
.char-group-caret { color: var(--text-muted); font-size: 12px; }
.char-group-chats { display: flex; flex-direction: column; gap: 4px; padding-left: 16px; margin: 2px 0 4px; }
.char-group-add { text-align: left; font-size: 13px; padding: 6px 10px; }

.chat-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }

/* ---------------- Start page ---------------- */
/* Shown whenever no chat/gallery/admin view is open: newest characters, recent chats, updates. */
.chat-empty { flex: 1; overflow-y: auto; }
.home-inner {
  max-width: 1000px;
  margin: 0 auto;
  padding: 28px 20px 56px;
  display: flex;
  flex-direction: column;
  gap: 32px;
}
.home-header h2 { font-size: 22px; }
.home-section { display: flex; flex-direction: column; gap: 12px; }
.home-section-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.home-section-head h3 {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}
.home-section-head .btn-ghost { font-size: 13px; padding: 4px 10px; flex: none; }

/* One horizontally-scrolling row of the poster tiles used in the characters gallery. */
.home-characters { display: flex; gap: 14px; overflow-x: auto; padding-bottom: 6px; }
.home-characters .field-hint { padding: 4px 0; }
.home-character { flex: 0 0 150px; cursor: pointer; }
.home-character:hover, .home-character:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
/* "3d ago" badge in the corner of the portrait - this row is specifically about what's new. */
.home-character-added {
  position: absolute;
  top: 6px;
  right: 6px;
  font-size: 10px;
  padding: 2px 6px;
  border-radius: 999px;
  background: rgba(12, 10, 8, 0.78);
  color: #f2ead9;
}

.home-chats { display: flex; flex-direction: column; gap: 8px; }
.home-chat-row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  text-align: left;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font-size: 14px;
}
.home-chat-row { text-decoration: none; cursor: pointer; }
.home-chat-row:hover { background: var(--surface-alt); border-color: var(--accent); }
.home-chat-row .avatar { flex: none; }
.home-chat-text { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.home-chat-title { font-weight: 600; }
.home-chat-sub, .home-chat-preview { font-size: 12px; color: var(--text-muted); }
/* Names and previews stay on one line each - a long opening message can't stretch the row. */
.home-chat-sub, .home-chat-preview, .home-chat-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.home-chat-time { flex: none; font-size: 11px; color: var(--text-muted); align-self: flex-start; }

/* ---------------- Chats page (/chats) ---------------- */
.chats-page-list {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.chats-page-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
}
.chats-page-row:hover { background: var(--surface-alt); border-color: var(--accent); }
.chats-page-row.active { border-color: var(--accent); background: var(--accent-soft); }
/* The picture, with a camera button tucked into its bottom-right corner. */
.chats-page-avatar { position: relative; flex: none; }
.chat-photo-btn {
  position: absolute;
  right: -4px;
  bottom: -4px;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--surface);
  color: var(--text);
  font-size: 11px;
  line-height: 1;
  padding: 4px 5px;
}
.chat-photo-btn:hover { border-color: var(--accent); color: var(--accent); }
.chats-page-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; color: inherit; text-decoration: none; }
.chats-page-title { display: flex; align-items: center; gap: 8px; min-width: 0; }
.chats-page-title strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chats-page-title .tag { margin-left: 0; flex: none; }
.chats-page-time { margin-left: auto; font-size: 11px; color: var(--text-muted); flex: none; }
.chats-page-meta, .chats-page-preview {
  font-size: 12px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.chats-page-actions { display: flex; gap: 8px; flex: none; }

@media (max-width: 768px) {
  /* Stack the row on narrow screens so the name and preview keep their width. */
  .chats-page-row { flex-wrap: wrap; }
  .chats-page-actions { width: 100%; }
  .chats-page-actions button, .chats-page-actions a { flex: 1; }
}

.home-updates { display: flex; flex-direction: column; gap: 10px; }
.home-update {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 12px 14px;
}
.home-update-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.home-update-title { font-weight: 600; font-size: 14px; }
.home-update-tag { margin-left: 0; text-transform: uppercase; letter-spacing: 0.04em; }
.home-update-tag-new { background: var(--accent-soft); color: var(--accent); }
.home-update-date { margin-left: auto; font-size: 11px; color: var(--text-muted); }
.home-update-items { margin: 8px 0 0; padding-left: 18px; }
.home-update-items li { font-size: 13px; color: var(--text-muted); margin-bottom: 2px; }

.chat-view, .characters-view, .admin-view { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.characters-view .gallery { flex: 1; max-height: none; padding: 16px; margin-top: 0; }

.characters-toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 16px 0;
  flex-wrap: wrap; /* only kicks in on very narrow viewports; one row otherwise */
}
/* The global `input, textarea, select { width: 100% }` rule would put each control on
   its own row, so widths are reset here and the search box takes the slack. */
.characters-toolbar select,
.characters-toolbar .characters-search {
  width: auto;
  font-size: 13px;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 10px;
}
/* A select sized by its content is as wide as its longest option, and the series and tag
   lists are filled from the cards - so one long series name used to blow the control up to
   several hundred pixels and shove the rest of the toolbar onto its own rows. The flex basis
   is what each control asks for and max-width is the ceiling it can grow to, so the row's
   width is now a property of the toolbar rather than of whatever happens to be in the data. */
.characters-toolbar select {
  flex: 1 1 130px;
  min-width: 0;
  max-width: 190px;
  text-overflow: ellipsis;
}
.characters-toolbar .characters-search { flex: 3 1 200px; min-width: 140px; max-width: 460px; }
/* A button sitting among the filters - sized like them rather than like a form button, and never
   stretched or shrunk by the flex row the way the selects are. */
.characters-toolbar .toolbar-btn {
  flex: none;
  font-size: 13px;
  padding: 6px 10px;
  white-space: nowrap;
}
.checkbox-inline {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: none;
  white-space: nowrap;
  font-size: 13px;
  color: var(--text-muted);
}
.checkbox-inline input { width: auto; }

.chat-view { flex-direction: row; }
.chat-conversation { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }

/* ---------------- Chat background ---------------- */
/* The chat's own picture, laid behind the whole conversation column.
 *
 * It goes on the column rather than on .messages because .messages is the thing that scrolls: a
 * background-image there either scrolls away with the transcript or needs background-attachment
 * games that behave differently in every browser. Two absolute layers on a non-scrolling parent
 * are simpler and hold still.
 *
 * ::before is the picture, ::after is the veil that keeps the words on top of it readable. Two
 * elements rather than one stacked background because the veil's strength is a per-chat number
 * (Chat.backgroundDim), and opacity is the only part of a background a CSS variable can drive
 * without needing a colour built at runtime.
 *
 * Both are inert and behind everything: the header, the transcript and the composer are all
 * lifted to z-index 1 below, and the first two keep their own opaque surfaces, so the picture
 * only ever shows through the gap the messages float in. */
.chat-conversation { position: relative; }
.chat-conversation.has-chat-background::before,
.chat-conversation.has-chat-background::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}
.chat-conversation.has-chat-background::before {
  background-image: var(--chat-bg-image);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
.chat-conversation.has-chat-background::after {
  background: var(--bg);
  opacity: var(--chat-bg-dim, 0.45);
}
/* Everything real sits above the two layers. The header and composer are opaque already, so the
   picture is visible exactly where the transcript is. */
.chat-conversation > * { position: relative; z-index: 1; }
/* Nothing else changes when a background is on. The bubbles are already their own opaque surface
   and stay that way - a translucent bubble over an arbitrary photo is a gamble on that photo,
   whereas .messages between them is transparent by design and is the window the picture is seen
   through. Keeping the text on solid ground is what makes the dim slider a decoration control
   rather than the only thing standing between a reader and an unreadable chat. */

/* The manage-chat controls for it. The preview is a strip rather than a thumbnail because that is
   the shape the real thing is seen in, and it carries the same two layers so the slider can be
   judged here instead of by closing the dialog. */
.chat-bg-row { display: flex; gap: 12px; align-items: center; margin-top: 8px; }
.chat-bg-preview {
  position: relative;
  flex: 1;
  min-width: 0;
  height: 72px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface-alt);
  background-image: var(--chat-bg-image);
  background-size: cover;
  background-position: center;
  overflow: hidden;
}
.chat-bg-preview::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--bg);
  opacity: var(--chat-bg-dim, 0.45);
}
/* Nothing chosen yet - says so instead of showing an empty box that looks like a failed image. */
.chat-bg-preview.is-empty { display: flex; align-items: center; justify-content: center; }
.chat-bg-preview.is-empty::after { content: 'No background'; position: static; background: none; opacity: 1; font-size: 12px; color: var(--text-muted); }
.chat-bg-controls { display: flex; flex-direction: column; gap: 6px; flex-shrink: 0; }
#manage-chat-bg-dim { padding: 0; }

.chat-roster {
  width: 260px;
  flex-shrink: 0;
  border-left: 1px solid var(--border);
  background: var(--surface);
  padding: 12px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.chat-roster-top { display: flex; gap: 8px; align-items: center; }
.chat-roster-top select { flex: 1; }
.chat-roster-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); padding-bottom: 8px; }
.roster-tab {
  flex: 1;
  background: transparent;
  border: none;
  border-radius: 6px;
  padding: 6px 4px;
  font-size: 12px;
  color: var(--text-muted);
}
.roster-tab:hover { background: var(--surface-alt); }
.roster-tab.active { background: var(--accent-soft); color: var(--accent); }
.chat-roster-panel { display: flex; flex-direction: column; gap: 8px; }
.chat-roster-panel h4 { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); }
.chat-roster .list { max-height: none; }
.chat-roster .inline-form { flex-wrap: nowrap; }

/* Even split: the form and the list each get half the panel, so neither column is squeezed.
   `min-width: 0` is what actually holds the 50/50 - without it a flex item refuses to shrink
   below its content's min-width, and a long card description would push the form column in. */
.admin-content { flex: 1; display: flex; gap: 20px; padding: 16px; overflow: hidden; }
.admin-form-col, .admin-list-col { flex: 1 1 50%; min-width: 0; overflow-y: auto; }
.admin-list-col .list { max-height: none; margin-top: 0; }
.admin-list-col .list-row > div p { max-width: 100%; overflow-wrap: anywhere; }

/* Heading row at the top of each admin column: title on the left, tools/count on the right. */
.admin-col-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 12px;
  min-height: 38px;
}
.admin-col-head h3 { font-size: 15px; }
.admin-col-head .field-hint { margin-top: 0; }

/* Import / bulk / AI, as one compact button group - the long explanations they used to
   carry live in each control's tooltip instead. */
.card-tools { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.card-tools > .btn-secondary { padding: 8px 14px; font-size: 14px; }
/* Each tool keeps its own error line, but an empty one must not reserve vertical space. */
.card-tool-errors .form-error { min-height: 0; margin-top: 0; }
.card-tool-errors .form-error:empty { display: none; }
.card-tool-errors:not(:empty) { margin-bottom: 8px; }

/* Two fields side by side in the wider form column. Collapses to one column when narrow. */
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 16px; }
.form-grid > div { min-width: 0; }
/* Lines a checkbox up with the input next to it instead of with that input's label. */
.form-grid-inline { display: flex; align-items: flex-end; padding-bottom: 10px; }
.form-grid-inline .checkbox-label { margin: 0; }
@media (max-width: 900px) {
  .form-grid { grid-template-columns: 1fr; }
  .form-grid-inline { padding-bottom: 0; margin-top: 10px; }
}
.chat-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}
.chat-subtitle { font-size: 12px; color: var(--text-muted); }
.chat-header-title { display: flex; align-items: center; gap: 10px; min-width: 0; }
.chat-header-actions { display: flex; gap: 8px; align-items: center; }
.chat-header-actions select { width: auto; }
.admin-tab.active { background: var(--accent-soft); color: var(--accent); }

.messages {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.bubble {
  max-width: 70%;
  padding: 10px 14px;
  border-radius: var(--radius);
  background: var(--surface);
  border: 1px solid var(--border);
}
.bubble-head { display: flex; align-items: baseline; gap: 6px; }
.bubble-author { display: block; font-size: 11px; color: var(--text-muted); margin-bottom: 2px; }
.bubble-edited { font-size: 10px; color: var(--text-muted); font-style: italic; }
/* The buttons sit in the bubble's own header rather than floating over the text, so they can
   never cover a word. Dimmed until the bubble is hovered or one of them is tabbed to - present
   enough to be found, quiet enough not to litter a long transcript. */
.bubble-actions { margin-left: auto; display: flex; align-items: center; gap: 2px; }
.bubble-action-btn {
  background: none;
  border: none;
  padding: 0 3px;
  font-size: 13px;
  line-height: 1;
  color: var(--text-muted);
  opacity: 0.35;
  transition: opacity 0.15s ease;
}
.bubble:hover .bubble-action-btn,
.bubble-action-btn:focus-visible { opacity: 1; }
.bubble-action-btn:hover { color: var(--accent); }
.bubble-delete-btn:hover { color: var(--danger); }
.bubble-action-btn:disabled { opacity: 0.2; cursor: default; }
/* Only the newest message can be asked for again, and not while a reply is already being
   written - markLastMessageRow in app.js is what decides which row that is. */
.bubble-regen-btn { display: none; }
.is-last-message .bubble-regen-btn { display: inline-block; }
.bubble-editing .bubble-actions { visibility: hidden; }

/* On a touch screen there is no hover to reveal the buttons with, so they stay out of the way
   until the message is held down (see enableLongPressActions in app.js), which is also what
   keeps a phone-width bubble from being half buttons. */
@media (hover: none) {
  .bubble-actions { display: none; }
  .bubble-actions-open .bubble-actions { display: flex; }
  .bubble-actions-open .bubble-action-btn { opacity: 1; font-size: 16px; padding: 2px 6px; }
  /* Says which message is being held, since the buttons alone are easy to miss under a thumb. */
  .bubble-actions-open .bubble { box-shadow: 0 0 0 2px var(--accent); }
  /* Suppresses the iOS "Copy / Look Up" callout on the hold. Selecting text still works: a hold
     that drags is treated as a selection and never opens the buttons. */
  .bubble { -webkit-touch-callout: none; }
}

.bubble-editor { margin-top: 4px; }
.bubble-edit-input { resize: none; overflow-y: auto; }
.bubble-edit-error:empty { display: none; }
.bubble-edit-actions { display: flex; justify-content: flex-end; gap: 6px; margin-top: 6px; }
.bubble-edit-actions button { padding: 4px 10px; font-size: 13px; }
/* An open editor needs the room a chat bubble normally doesn't - a line long enough to be worth
   rewriting is hard to work with in a 70%-wide column. */
.bubble-row:has(.bubble-editor) .bubble { max-width: none; flex: 1; }

.bubble p {
  white-space: pre-wrap;
  overflow-wrap: break-word;
}
.bubble p em { color: var(--text-muted); font-style: italic; }
/* Colour only. Which side a bubble sits on is decided by .bubble-row-user on the row, so a
   human keeps this colour even when left-aligned - that's what tells them apart from a
   character in a group chat, where everyone but you is on the left. */
.bubble-user { background: var(--accent-soft); }
.bubble-character { align-self: flex-start; }

.bubble-typing { display: block; }
.typing-dots { display: flex; align-items: center; gap: 4px; margin-top: 2px; height: 14px; }
.typing-dots span {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--text-muted);
  animation: typing-bounce 1.2s infinite ease-in-out;
}
.typing-dots span:nth-child(2) { animation-delay: 0.2s; }
.typing-dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing-bounce {
  0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
  30% { transform: translateY(-4px); opacity: 1; }
}

.message-form {
  display: flex;
  gap: 8px;
  padding: 12px 16px;
  border-top: 1px solid var(--border);
  background: var(--surface);
}
.message-form textarea { resize: none; }

/* ---------------- Modals ---------------- */
.modal {
  /* The `* { margin: 0 }` reset above kills the UA's auto-centering margin, so it's reasserted here. */
  position: fixed;
  inset: 0;
  margin: auto;
  border: none;
  border-radius: var(--radius);
  padding: 24px;
  width: 90%;
  max-width: 480px;
  max-height: 85vh;
  overflow-y: auto;
  background: var(--surface);
  color: var(--text);
}
.modal h2, .modal h3 { color: var(--text); }
.modal-wide { max-width: 640px; }
.modal::backdrop { background: rgba(0,0,0,0.6); }
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
.inline-form { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

/* Progress bar (AI card generation) */
.progress-track {
  height: 10px;
  margin: 16px 0 8px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-alt);
  overflow: hidden;
}
.progress-fill {
  width: 0%;
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--accent), var(--accent-hover));
  transition: width 0.4s ease;
}
.progress-meta {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  font-size: 13px;
  color: var(--text-muted);
}

.checkbox-label { display: flex; align-items: center; gap: 8px; font-size: 14px; color: var(--text); }
.checkbox-label input { width: auto; }

/* Preferences block in the Account modal. Ruled off from the picture and the account line above
   it, since everything below the rule saves itself as it's changed. */
.account-settings {
  margin: 16px 0;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}
.account-settings h3 { font-size: 14px; margin-bottom: 10px; }
.account-settings .checkbox-label { margin-top: 10px; }
/* Each control carries its own explanation, so the gap belongs above the next label rather
   than under the hint that just ended. */
.account-settings label:not(.checkbox-label) { display: block; margin-top: 14px; }
#account-settings-status:empty { display: none; }

.character-picker { position: relative; margin-bottom: 8px; }
.char-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 6px; }
.char-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 4px 10px 4px 4px;
  font-size: 13px;
}
.char-chip-remove {
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: 15px;
  line-height: 1;
  padding: 0 0 0 4px;
}
.char-suggestions {
  position: absolute;
  z-index: 5;
  left: 0;
  right: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  max-height: 220px;
  overflow-y: auto;
  margin-top: 4px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.char-suggestion { display: flex; align-items: center; gap: 8px; padding: 8px 10px; cursor: pointer; }
.char-suggestion:hover { background: var(--bg); }
.char-suggestion-empty { padding: 8px 10px; font-size: 13px; color: var(--text-muted); }

.char-add-list { display: flex; flex-direction: column; gap: 4px; max-height: 240px; overflow-y: auto; margin-top: 6px; }
.char-add-row { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border: 1px solid var(--border); border-radius: 8px; }
.char-add-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
.char-add-empty { padding: 8px 10px; font-size: 13px; color: var(--text-muted); }

.list { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; max-height: 300px; overflow-y: auto; }
.list-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
}
.list-row > div:first-child { flex: 1; min-width: 0; }
.list-row p { font-size: 12px; color: var(--text-muted); }
.list-row button { flex-shrink: 0; }
/* Rows that lead with an avatar can't rely on the `div:first-child` rule above (the <img>
   is the first child), so the text block claims the slack explicitly and the actions get
   pushed to the right edge - lining every row's buttons up in one column. */
.list-row-text { flex: 1; min-width: 0; }
.list-row-actions { display: flex; align-items: center; gap: 6px; flex: none; margin-left: auto; }
.list-row-actions button, .list-row-actions a { padding: 6px 12px; font-size: 13px; }

.tag, .tag-nsfw {
  font-size: 10px;
  padding: 2px 6px;
  border-radius: 4px;
  margin-left: 6px;
}
.tag { background: var(--border); }
.tag-nsfw { background: var(--danger-soft); color: var(--danger); }

.card-form { display: flex; flex-direction: column; }

/* Bulk import queue - one row per picked file, status updated in place as the run proceeds. */
.bulk-list { max-height: 320px; }
.bulk-list .list-row { align-items: flex-start; }
.bulk-row-status { font-size: 12px; margin-top: 2px; }
.bulk-row-status.is-pending { color: var(--text-muted); }
.bulk-row-status.is-active { color: var(--accent); }
/* Parked mid-card waiting for a restarting server - not an error, so it stays out of red, and
   it pulses so a queue that has quietly stopped moving doesn't read as one that has hung. */
.bulk-row-status.is-waiting { color: var(--accent); font-style: italic; animation: bulk-waiting-pulse 1.6s ease-in-out infinite; }
@keyframes bulk-waiting-pulse { 50% { opacity: 0.55; } }
@media (prefers-reduced-motion: reduce) { .bulk-row-status.is-waiting { animation: none; } }
.bulk-row-status.is-done { color: #2e7d32; }
.bulk-row-status.is-error { color: var(--danger); }
.bulk-row-file { font-size: 11px; color: var(--text-muted); overflow-wrap: anywhere; }

.alt-greetings-list { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; margin-bottom: 8px; }
.alt-greetings-list p { font-size: 13px; color: var(--text-muted); margin: 4px 0; }

/* ---------------- Avatars ---------------- */
.avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--border);
  flex-shrink: 0;
}
.avatar-lg { width: 72px; height: 72px; }
.avatar-sm { width: 24px; height: 24px; }
.avatar-row { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
.avatar-upload-btn { display: inline-block; }
.bubble-row { display: flex; gap: 8px; align-items: flex-end; }
.bubble-row.bubble-row-user { flex-direction: row-reverse; }
.chat-list-item-row { display: flex; align-items: center; gap: 10px; color: inherit; text-decoration: none; }
.chat-list-item-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.chat-list-item-text span { font-size: 12px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---------------- Characters gallery ---------------- */
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
  gap: 14px;
  margin-top: 12px;
  max-height: 60vh;
  overflow-y: auto;
  /* Rows must be sized by their cards, never by the gallery's own height. With the default
     `auto` rows, the grid sizes each row somewhere between its min-content and max-content
     height and shares the container's height between them - so a card whose min-content
     height is ~0 (the poster frame is a ratio box holding only an absolutely-positioned
     image) gets whatever's left over: 26 cards in a 611px gallery collapsed to 71px rows,
     clipping every portrait and leaving nothing to scroll. max-content pins each row to the
     card's real height, which lets the grid overflow and `overflow-y: auto` do its job. */
  grid-auto-rows: max-content;
}
.gallery-card {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px;
  text-align: center;
}
.gallery-card .avatar {
  width: 72px;
  height: 72px;
  margin: 0 auto 8px;
}
.gallery-card h3 { font-size: 14px; margin-bottom: 4px; }
.gallery-card p { font-size: 12px; color: var(--text-muted); }

/* Character cards: always dark, poster-style, regardless of the active theme. */
.character-card {
  background: #1a1714;
  border: none;
  border-radius: var(--radius);
  padding: 0;
  text-align: left;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
/* Fixed 3:4 poster frame, so every card is the same size no matter what the source image's
   dimensions are. The padding-top ratio box (rather than `aspect-ratio` on the <img>) keeps
   the height from collapsing on wide source images, and `flex: none` stops the frame from
   being squeezed when a card ends up in a shorter grid row than its content. */
.gallery-card-media {
  position: relative;
  width: 100%;
  padding-top: 133.333%;
  flex: none;
  background: var(--border);
}
.gallery-card-image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  object-position: top center; /* character art is framed head-first - don't crop the face off */
}
/* Name plate laid over the bottom of the portrait - the gradient keeps the text readable
   whatever the art behind it looks like. */
.gallery-card-plate {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 20px 10px 8px;
  background: linear-gradient(to top, rgba(12, 10, 8, 0.92) 45%, rgba(12, 10, 8, 0));
  color: #f2ead9;
  font-size: 13px;
  font-weight: 600;
}
/* Long names ellipsize on one line instead of pushing the NSFW badge out of the card. */
.gallery-card-plate-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.gallery-card-plate .gallery-card-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gallery-card-series {
  font-size: 11px;
  font-weight: 400;
  color: #cfc4ad;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gallery-card-plate .tag-nsfw { flex: none; margin-left: 0; }

/* Explicit-art blur, driven entirely from the class the account setting puts on <body> - so
   switching it repaints every browse grid at once without any of them being rebuilt. Only browse
   tiles are ever blurred: a chat, or a character's own page, is already somewhere you chose to be.
   Cards tagged Explicit are blurred, not every card marked NSFW - see the note above
   applyExplicitBlurSetting in app.js for why the narrower tag is the one worth hiding on.

   The blur is on the image alone, not the tile, so the name plate and the NSFW badge stay
   legible - a wall of anonymous smudges would be worse to browse than the art it hides. The
   image is scaled up because blurring pulls the transparent edge of the box inwards and would
   otherwise leave a pale border around it.

   That scale-up is the `scale` property rather than a transform on purpose: `transform` is
   already spoken for by the hover zoom further down, and keeping the two on separate properties
   lets them compose instead of one having to re-state the other. */
body.blur-explicit .character-card.is-explicit:not(.explicit-revealed) .gallery-card-image {
  filter: blur(16px);
  scale: 1.1;
}
.explicit-veil {
  position: absolute;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  /* On a touch screen the reveal is a tap on the tile itself (see buildCharacterCardTile), and
     the whole tile is one link - so the veil must not eat the tap that has to reach it. */
  pointer-events: none;
  padding: 0 10px;
  text-align: center;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: #f2ead9;
  text-shadow: 0 1px 3px rgba(12, 10, 8, 0.9);
  background: rgba(12, 10, 8, 0.35);
}
body.blur-explicit .character-card.is-explicit:not(.explicit-revealed) .explicit-veil { display: flex; }
/* The label is set here rather than in the markup because which reveal is on offer is a property
   of the device, not of the card - and a tile built before a mouse was plugged in would otherwise
   keep telling you to tap it. */
.explicit-veil::after { content: 'Tap to reveal'; }

/* With a real pointer, hovering the tile is the reveal: no click is spent on it, and moving away
   puts the blur back, so the setting keeps holding for everything you aren't actually looking at.
   Touch screens have no hover to do this with, so there the tap-to-reveal above stands - and
   `pointer: fine` keeps a phone that reports a hover it can't really do out of this.

   Equal specificity to the two rules above, so these win on source order alone; they must stay
   below them. */
@media (hover: hover) and (pointer: fine) {
  .explicit-veil::after { content: 'Hover to reveal'; }
  body.blur-explicit .character-card.is-explicit:hover .gallery-card-image {
    filter: none;
    scale: 1;
  }
  body.blur-explicit .character-card.is-explicit:hover .explicit-veil { display: none; }
}

/* Character tiles cap out at ~188.75 x 215.167. The grid lays down as many fixed-width
   tracks as fit, and the media ratio box (215.167 / 188.75) keeps the height proportional
   if a track has to shrink on a narrow screen. */
.gallery-characters {
  grid-template-columns: repeat(auto-fill, minmax(150px, 188.75px));
  justify-content: start;
  align-content: start;
}
.character-card .gallery-card-media { padding-top: 114%; }

/* Invisible row-wide marker at the end of the gallery; crossing it loads the next batch. */
.gallery-sentinel { grid-column: 1 / -1; height: 1px; }

/* A gallery tile opens that character's page, so it should look pressable. The tile itself is
   the positioning context for the overlay controls below; the <a> inside it is what actually
   carries the link. */
.character-card { position: relative; }
.character-card-link { display: block; color: inherit; text-decoration: none; }
.gallery-characters .character-card { cursor: pointer; }
.gallery-characters .character-card:hover .gallery-card-image,
.gallery-characters .character-card:focus-within .gallery-card-image { transform: scale(1.03); }
.character-card-link:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* filter and scale ride along with the zoom so the hover reveal fades the blur off rather than
   snapping it, which at 16px of blur reads as the picture being swapped out. */
.gallery-card-image { transition: transform 0.15s ease, filter 0.15s ease, scale 0.15s ease; }

/* Per-tile controls (copy link, start a chat, edit the shared card), laid over the top corner
   of the portrait. They are siblings of the tile's link rather than children of it - a button
   nested inside an anchor is invalid, and more to the point it makes "open in a new tab" on
   the tile ambiguous. Hidden until the tile is hovered or something inside it is focused, so
   a wall of portraits stays a wall of portraits. */
.tile-actions {
  position: absolute;
  top: 6px;
  right: 6px;
  display: flex;
  gap: 4px;
  opacity: 0;
  transition: opacity 0.12s ease;
}
.character-card:hover .tile-actions,
.character-card:focus-within .tile-actions { opacity: 1; }
/* Touch screens have no hover to reveal them with, so there they are simply always on. */
@media (hover: none) {
  .tile-actions { opacity: 1; }
}
.tile-action {
  border: none;
  border-radius: 6px;
  background: rgba(12, 10, 8, 0.78);
  color: #f2ead9;
  font-size: 12px;
  line-height: 1;
  padding: 6px 7px;
}
.tile-action:hover { background: var(--accent); color: #fff; }
.tile-action:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; opacity: 1; }

/* Copy confirmations for actions with no status line of their own (a gallery tile's link
   button, mostly). Fixed to the viewport so it reads the same from any view. */
.toast {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  z-index: 100;
  padding: 10px 16px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--surface-alt);
  color: var(--text);
  font-size: 13px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
}

/* ---------------- One character's page ---------------- */
/* Reading view, so the body is capped at a comfortable measure rather than filling the panel. */
.character-detail { flex: 1; overflow-y: auto; padding: 16px 16px 48px; }
.character-detail > * { max-width: 760px; margin-inline: auto; }

.character-hero { display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap; }
.character-portrait {
  width: 180px;
  aspect-ratio: 188.75 / 215.167; /* same proportions as the gallery tile it was opened from */
  object-fit: cover;
  border-radius: var(--radius);
  background: var(--border);
  flex: none;
}
.character-hero-text { flex: 1 1 260px; min-width: 0; }
.character-hero-text h2 { font-size: 24px; overflow-wrap: anywhere; }
.character-series { color: var(--accent); font-size: 14px; margin-top: 2px; }
.character-summary { margin-top: 10px; font-size: 15px; }
.character-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.character-tags .tag { margin-left: 0; }
.character-facts { margin-top: 10px; font-size: 12px; color: var(--text-muted); }
/* Marks the parts of this page that are the reader's own rewrite rather than the shared card. */
.tag-edited { background: var(--accent-soft); color: var(--accent); }
/* The character's own gender, shown ahead of the tags it sits with so it reads as the first
   thing about them rather than as one more label somebody happened to add. */
.tag-gender { background: var(--accent-soft); color: var(--accent); text-transform: capitalize; }
/* A card handed to one person - shown in the admin list and on the character's own page. */
.tag-locked { background: var(--accent-soft); color: var(--accent); }
.character-edited-note {
  margin-top: 10px;
  padding: 8px 10px;
  border-left: 2px solid var(--accent);
  background: var(--accent-soft);
  border-radius: 0 8px 8px 0;
  font-size: 12px;
  color: var(--text-muted);
}

.character-sections { display: flex; flex-direction: column; gap: 18px; margin-top: 28px; }
.character-section {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 14px 16px;
}
.character-section h3 {
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  margin-bottom: 8px;
}
.character-section h4 { font-size: 13px; margin: 14px 0 6px; }
.character-section h4:first-child { margin-top: 0; }
/* Card text is prose, so it gets a longer line height than the app's UI default. */
.character-prose { font-size: 15px; line-height: 1.7; overflow-wrap: anywhere; }
.character-prose p + p { margin-top: 10px; }
.character-prose em { color: var(--text-muted); }

.character-details > summary {
  cursor: pointer;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}
.character-details[open] > summary { margin-bottom: 12px; }
.character-greeting + .character-greeting { margin-top: 14px; }
.character-greeting-label { font-size: 11px; color: var(--text-muted); }

/* Example dialogue: a transcript, with the character's turns tinted to tell them from yours. */
.example-block {
  border-left: 2px solid var(--border);
  padding-left: 12px;
  margin-top: 12px;
}
.example-block + .example-block { margin-top: 16px; }
.example-line + .example-line { margin-top: 8px; }
.example-speaker { display: block; font-size: 12px; color: var(--text-muted); }
.example-line.is-char .example-speaker { color: var(--accent); }

.character-raw {
  font-size: 12px;
  line-height: 1.5;
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px;
  max-height: 320px;
  overflow: auto;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.character-missing { padding: 40px 0; text-align: center; }
.character-missing h2 { font-size: 20px; margin-bottom: 8px; }
.character-missing p { color: var(--text-muted); font-size: 14px; }

@media (max-width: 700px) {
  .character-portrait { width: 128px; }
}

/* ---------------- Personas page ---------------- */
/* Wider tracks than the character gallery: these tiles are text (name + description),
   not portraits, so they read better as cards than as narrow posters. */
.gallery-personas {
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  align-content: start;
}
.persona-card {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 14px;
  text-align: left;
  background: var(--surface);
}
.persona-card-head { display: flex; align-items: center; gap: 10px; min-width: 0; }
.persona-card .avatar { width: 44px; height: 44px; margin: 0; }
.persona-card-name {
  flex: 1;
  min-width: 0;
  font-size: 14px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The badge sits beside the name rather than inside it, so a long name ellipsizes
   against the badge instead of clipping it. */
.persona-card-head .tag { flex: none; margin-left: 0; }
/* Descriptions vary wildly in length - some are pages long - so a tile shows only a control
   until it's opened, which keeps every tile the same height until someone wants to read one. */
.persona-card-desc-wrap > summary {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  cursor: pointer;
  padding: 4px 0;
  list-style: none;
  user-select: none;
}
.persona-card-desc-wrap > summary::-webkit-details-marker { display: none; }
/* The caret is drawn here rather than left to the browser so it can flip when open. */
.persona-card-desc-wrap > summary::before { content: '▸ '; display: inline-block; }
.persona-card-desc-wrap[open] > summary::before { content: '▾ '; }
.persona-card-desc-wrap > summary:hover { color: var(--text); }
.persona-card-desc {
  font-size: 12px;
  color: var(--text-muted);
  overflow-wrap: anywhere;
  white-space: pre-wrap;
  /* Even opened, a runaway description scrolls inside the tile instead of stretching the grid. */
  max-height: 220px;
  overflow-y: auto;
  margin-top: 2px;
}
/* Actions sit on their own line at the bottom of the tile, right-aligned and pushed down
   so tiles with a short description still line their buttons up with the tall ones. */
.persona-card-actions { display: flex; justify-content: flex-end; gap: 6px; margin-top: auto; flex-wrap: wrap; }
.persona-card-actions button, .persona-card-actions a, .persona-card-actions .avatar-upload-btn {
  padding: 6px 12px;
  font-size: 13px;
}

/* ---------------- Universes ---------------- */
/* The page reuses the persona tiles wholesale (.gallery-personas / .persona-card) - a universe
   reads the same way: picture, name, blurb, actions. Only the lore editor inside the modal is
   new, and it is a list of text entries rather than anything picture-led. */
.lore-list { max-height: 260px; }
/* An entry is a block of prose next to its keywords, so the row stacks instead of centering
   one line of text against the buttons. */
.lore-list .list-row { align-items: flex-start; }
.lore-row-text { flex: 1; min-width: 0; }
.lore-row-title { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.lore-row-title .tag { margin-left: 0; }
/* The keywords are what makes an entry fire, so they stay legible rather than muted. */
.lore-row-keys { font-size: 12px; color: var(--accent); overflow-wrap: anywhere; margin-top: 2px; }
.lore-row-content { font-size: 12px; color: var(--text-muted); overflow-wrap: anywhere; margin-top: 2px; }
/* The read-only lines in Manage chat - the chat's universe and its adult content setting. Both
   were fixed when the chat was created, so each is named without being offered as a change, and
   reads as a value rather than a heading: plain text at body weight. */
.manage-chat-fact { font-size: 14px; }

/* ---------------- Read-only share link ---------------- */
/* The link is a read-only input rather than plain text so it can be selected and copied by hand
   on the platforms where the clipboard API isn't available (a plain-http LAN address, mostly) -
   the same fallback copyLink() falls back to. */
.share-link-row { display: flex; gap: 8px; align-items: center; margin: 8px 0; }
.share-link-row input { font-size: 12px; }
.share-link-row button { flex-shrink: 0; }
.share-link-actions { display: flex; gap: 8px; flex-wrap: wrap; }
/* Separates the lore half of the dialog from the universe's own fields above it. */
#universe-lore-section { border-top: 1px solid var(--border); margin-top: 20px; padding-top: 16px; }
#universe-lore-section h4 { margin-top: 16px; font-size: 13px; }

/* A person's row grows when they're speaking as a persona (name + username + description),
   so pin the avatar and button to the top instead of centering them against tall text. */
.roster-person { align-items: flex-start; }
.roster-person .avatar, .roster-person button { margin-top: 2px; }
.roster-person-sub { margin-top: 2px; }
.roster-person-desc { margin-top: 4px; overflow-wrap: anywhere; white-space: pre-wrap; }

/* ---------------- Shared chat (read-only) ---------------- */
/* /shared/<token>, served by frontend/shared.html. Deliberately built out of the same pieces the
   real chat is - .chat-conversation for the background layers, .bubble-row for the transcript -
   so a shared chat looks like the chat it came from rather than like an export of it. What it
   does not have is anything to press: no composer, no roster, no per-message controls. */
.shared-page { height: 100vh; display: flex; flex-direction: column; }
.shared-topbar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.shared-topbar-text { min-width: 0; }
.shared-topbar h1 { font-size: 16px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.shared-topbar-actions { margin-left: auto; display: flex; gap: 8px; align-items: center; flex-shrink: 0; }
/* Says what this page is, once, above the transcript - a reader who was sent a link has no other
   way to know they are looking at a copy they cannot write to. */
.shared-notice {
  flex-shrink: 0;
  padding: 8px 16px;
  font-size: 12px;
  color: var(--text-muted);
  background: var(--surface-alt);
  border-bottom: 1px solid var(--border);
}
.shared-body { flex: 1; display: flex; min-height: 0; }
/* The "sign in to read this" / "link revoked" / "18+ only" state. Centred on the page on its own,
   since there is no transcript behind it to sit over. */
.shared-gate { margin: auto; padding: 32px 20px; max-width: 460px; text-align: center; }
.shared-gate h2 { margin-bottom: 8px; font-size: 18px; }
.shared-gate p { color: var(--text-muted); font-size: 14px; margin-bottom: 16px; }
/* The cast, listed under the header: a reader needs to know who the names in the bubbles are. */
.shared-cast { display: flex; flex-wrap: wrap; gap: 10px; padding: 10px 16px; border-bottom: 1px solid var(--border); }
.shared-cast-member { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted); }

/* ---------------- Mobile ---------------- */
@media (max-width: 768px) {
  /* The cast keeps its place on a phone but stops eating the screen: one scrolling row rather
     than a block that wraps to five lines above a chat nobody can see yet. */
  .shared-cast { flex-wrap: nowrap; overflow-x: auto; }
  .shared-cast-member { flex-shrink: 0; }
  .icon-btn { display: block; }
  /* The title is the first thing to go: it pushed Account and Invites off the right edge. */
  .topbar .brand { display: none; }
  .sidebar-brand { display: block; }
  .sidebar {
    position: fixed;
    top: 56px;
    bottom: 0;
    left: 0;
    z-index: 20;
    transform: translateX(-100%);
    transition: transform 0.2s ease;
  }
  .sidebar.open { transform: translateX(0); }
  .chat-roster {
    position: fixed;
    top: 56px;
    bottom: 0;
    right: 0;
    z-index: 20;
    width: 260px;
    max-width: 80vw;
    transform: translateX(100%);
    transition: transform 0.2s ease;
  }
  .chat-roster.open { transform: translateX(0); }
  .bubble { max-width: 88%; }
  /* The 50/50 admin split has no room on a phone - stack the form above the list. */
  .admin-content { flex-direction: column; overflow-y: auto; }
  .admin-form-col, .admin-list-col { flex: none; overflow: visible; }
}
