:root {
  --bg: #12141a;
  --surface: #1b1e27;
  --border: #2b2f3b;
  --text: #e7e8ec;
  --text-muted: #9297a6;
  --primary: #4c9a6a;
  --primary-hover: #3c8256;
  --error: #f27272;
  --success: #5fc998;
  --radius: 10px;
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
}

/* Light theme -- toggled via Settings (see nav-menu.js's setTheme()),
   which sets data-theme="light" on <html> and persists it to
   localStorage. Every other rule in this file just reads the variables
   above, so this is the only place the two themes need to differ. */
:root[data-theme="light"] {
  --bg: #f4f5f7;
  --surface: #ffffff;
  --border: #dde0e6;
  --text: #1c1e24;
  --text-muted: #666b78;
  --primary: #3c8256;
  --primary-hover: #316b47;
  --error: #c8392f;
  --success: #2f9968;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: var(--font);
  background: var(--bg);
  color: var(--text);
  /* Belt-and-suspenders on top of the min-width:0 fixes below -- the page
     itself must never grow a horizontal scrollbar, even if some
     unforeseen piece of content resists shrinking. */
  overflow-x: hidden;
}

.nav {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  padding: 1rem 2rem;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.nav .brand {
  color: var(--text);
  font-weight: 800;
  font-size: 1.05rem;
}

.nav a {
  color: var(--text-muted);
  text-decoration: none;
  font-weight: 500;
  font-size: 0.95rem;
}

.nav a:hover,
.nav a.active {
  color: var(--primary);
}

/* Plain right-aligned logout link -- still used as-is by triggers.html,
   which doesn't have the hamburger menu (see .nav-menu below) that
   queue.html/chat.html replaced this with. */
.nav a.nav-logout {
  margin-left: auto;
}

/* Debug aid -- see chat.js: shows the logged-in agent's own id at all
   times while in the console, so two agents comparing screens can
   confirm they're actually looking at different accounts. */
.agent-id-badge {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.25rem 0.75rem;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--text-muted);
}

/* Hamburger menu -- replaces the old standalone "Log out" nav link.
   .nav-menu carries the margin-left:auto that link used to have, so the
   button still sits flush right regardless of what else is in the nav. */
.nav-menu {
  position: relative;
  margin-left: auto;
}

.hamburger-btn {
  width: auto;
  background: transparent;
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.35rem 0.6rem;
  font-size: 1.1rem;
  line-height: 1;
}

.hamburger-btn:hover {
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
}

.nav-menu-dropdown {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  min-width: 160px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
  padding: 0.4rem;
  display: flex;
  flex-direction: column;
  z-index: 20;
}

/* Same cascade fix as .modal-overlay[hidden] above -- the unconditional
   display:flex here otherwise beats [hidden]{display:none} regardless of
   the hidden attribute, leaving the dropdown permanently open. */
.nav-menu-dropdown[hidden] {
  display: none;
}

.nav-menu-dropdown a {
  padding: 0.55rem 0.7rem;
  border-radius: 6px;
  font-size: 0.9rem;
}

.nav-menu-dropdown a:hover {
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
}

/* Settings / My Statistics panels */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 30;
}

/* An author rule that sets `display` (like .modal-overlay above) always
   beats the browser's own [hidden]{display:none} rule for that property,
   no matter the specificity -- author-origin styles outrank user-agent
   ones in the cascade outright. Without this, both #settings-modal and
   #my-stats-modal render as permanent full-screen overlays regardless of
   the hidden attribute, with whichever is later in the DOM covering the
   entire page including the nav. .modal-overlay[hidden] has higher
   specificity than the bare .modal-overlay rule above, so this actually
   wins and the hidden attribute works as intended. */
.modal-overlay[hidden] {
  display: none;
}

.modal-panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.75rem;
  width: 320px;
}

/* Wider than every other .modal-panel -- the From/To date inputs plus
   an Apply button next to them (see .date-filter-apply-wrap) don't fit
   three across at the default 320px without one of them overflowing;
   scoped to this modal only, every other one keeps the default width. */
#my-stats-modal .modal-panel {
  width: 420px;
}

/* Full-size photo preview -- triggered by clicking either panel's small
   circular photo (see chat.js's openPhotoLightbox()). Sized to the image
   rather than the default modal width. */
.photo-lightbox-panel {
  width: auto;
  max-width: min(90vw, 640px);
  text-align: center;
}

.photo-lightbox-panel img {
  max-width: 100%;
  max-height: 70vh;
  border-radius: var(--radius);
  display: block;
  margin: 0 auto 1rem;
}

.modal-panel h2 {
  font-size: 1.15rem;
  margin: 0 0 1.25rem;
}

.modal-panel .btn-secondary {
  margin-top: 1.5rem;
  background: transparent;
  color: var(--text-muted);
  border: 1px solid var(--border);
}

.setting-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.6rem 0;
  font-size: 0.92rem;
}

.setting-row + .setting-row {
  border-top: 1px solid var(--border);
}

/* Toggle switch */
.switch {
  position: relative;
  display: inline-block;
  width: 40px;
  height: 22px;
  flex-shrink: 0;
}

.switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

.switch-slider {
  position: absolute;
  inset: 0;
  cursor: pointer;
  background: var(--border);
  border-radius: 999px;
  transition: background 0.15s;
}

.switch-slider::before {
  content: "";
  position: absolute;
  width: 16px;
  height: 16px;
  left: 3px;
  top: 3px;
  background: white;
  border-radius: 50%;
  transition: transform 0.15s;
}

.switch input:checked + .switch-slider {
  background: var(--primary);
}

.switch input:checked + .switch-slider::before {
  transform: translateX(18px);
}

/* New-message notification banner -- see nav-menu.js's notifyNewMessage() */
.notification-banner {
  position: fixed;
  top: 1.25rem;
  right: 1.25rem;
  background: var(--success);
  color: #0d2418;
  font-weight: 700;
  font-size: 0.88rem;
  padding: 0.7rem 1.1rem;
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
  opacity: 0;
  transform: translateY(-8px);
  transition: opacity 0.2s, transform 0.2s;
  z-index: 40;
}

.notification-banner.visible {
  opacity: 1;
  transform: translateY(0);
}

.container {
  max-width: 420px;
  margin: 3.5rem auto;
  padding: 0 1.5rem;
}

.container.wide {
  max-width: 900px;
  margin: 2rem auto;
  padding: 0 1.5rem;
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 2rem;
}

h1 {
  font-size: 1.4rem;
  margin: 0 0 1.25rem;
}

p.subtitle {
  color: var(--text-muted);
  margin: 0 0 1.5rem;
}

label {
  display: block;
  font-size: 0.85rem;
  font-weight: 600;
  margin-bottom: 0.35rem;
  margin-top: 1rem;
}

input[type="email"],
input[type="password"],
input[type="text"],
input[type="number"] {
  width: 100%;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 0.95rem;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
}

input:focus {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
}

button {
  cursor: pointer;
  border: none;
  border-radius: 8px;
  font-size: 0.95rem;
  font-weight: 600;
  padding: 0.65rem 1.2rem;
  background: var(--primary);
  color: white;
  width: 100%;
}

button:hover {
  background: var(--primary-hover);
}

.btn-row {
  margin-top: 1.4rem;
}

/* Generic danger variant -- was only .queue-table-scoped before (see
   below); this one isn't, since the chat view's Block button (chat.html)
   sits outside any table. */
.btn-danger {
  background: var(--error);
}

.btn-danger:hover {
  filter: brightness(90%);
}

/* An inline button dropped into a flex row of text (.chat-meta,
   .report-review-banner) instead of a form's own full-width one -- same
   override .report-review-banner .btn-secondary already applies below. */
.btn-inline {
  width: auto;
  flex-shrink: 0;
}

/* Same compact scale as .queue-table button's own override -- used
   outside a table for chat.html's Block chat button (.chat-meta) and the
   block-reason modal's own Block chat/Cancel buttons. */
.btn-sm {
  padding: 0.4rem 0.8rem;
  font-size: 0.82rem;
}

/* Two fixes for this one button, both needed for .btn-sm above to
   actually show: (1) .btn-row-split's flex-shrink otherwise lets it
   squeeze below its two-word label's natural width and wrap; (2) its
   sibling Cancel carries its own margin-top (.modal-panel .btn-secondary)
   which, combined with .btn-row-split's default align-items: stretch,
   inflates the shared flex row's cross-size and stretches this
   (unmargined) button to fill it. Cancel's own height is unaffected by
   either -- one word never wraps, and its own margin doesn't stretch
   itself -- so it never needed either override. */
#block-chat-confirm-btn {
  white-space: nowrap;
  align-self: flex-start;
  /* Matches Cancel's own 1px border (.modal-panel .btn-secondary) so
     both buttons' boxes come out the same height -- transparent since
     .btn-danger's background should show through unbroken. */
  border: 1px solid transparent;
}

/* .modal-panel .btn-secondary's own margin-top: 1.5rem (meant to
   separate a Cancel button from unrelated content above it in other
   modals) pushes Cancel down relative to the unmargined Confirm button
   right next to it here, breaking the side-by-side alignment .btn-
   row-split otherwise gives them. Scoped to this one button so every
   other modal's Cancel spacing is untouched. */
#block-chat-cancel-btn {
  margin-top: 0;
}

.message {
  margin-top: 1rem;
  padding: 0.6rem 0.8rem;
  border-radius: 8px;
  font-size: 0.88rem;
}

.message.error {
  background: rgba(242, 114, 114, 0.12);
  color: var(--error);
}

.message.success {
  background: rgba(95, 201, 152, 0.12);
  color: var(--success);
}

/* Chat view's restart alert (public/chat.html) -- console-only cue that
   this conversation was pulled into the queue by the restart system
   (see services/restartQueue.js in chatosphere), not a genuine new user
   message. Sits above .chat-meta, same width as the chat window. */
.restart-alert-banner {
  margin: 0 0 1rem;
  padding: 0.7rem 1rem;
  border-radius: var(--radius);
  background: rgba(242, 114, 114, 0.16);
  border: 1px solid var(--error);
  color: var(--error);
  font-weight: 700;
  font-size: 0.9rem;
}

/* Chat view's favourited alert (public/chat.html) -- console-only cue that
   the user just favourited this fictional profile (see
   routes/favorites.js on chatosphere/site2, and this app's own
   POST /console-api/conversations/:id/favourited). Solid bright pink,
   unlike the outlined/tinted restart banner above, so it reads as its own
   distinct kind of alert rather than a variant of it. */
.favourited-alert-banner {
  margin: 0 0 1rem;
  padding: 0.7rem 1rem;
  border-radius: var(--radius);
  background: #ff1493;
  color: white;
  font-weight: 700;
  font-size: 0.9rem;
}

/* Shown on chat.html when arrived at via a report row (public/reports.js)
   -- deliberately a neutral/info tone, not var(--error) like the restart
   banner above, so it doesn't read as "something is wrong with this
   chat" the way the highlighted reported message itself (see
   .message-reported below) already does. */
.report-review-banner {
  margin: 0 0 1rem;
  padding: 0.7rem 1rem;
  border-radius: var(--radius);
  background: rgba(76, 124, 240, 0.12);
  border: 1px solid var(--primary);
  color: var(--primary);
  font-weight: 600;
  font-size: 0.9rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.report-review-banner .btn-secondary {
  width: auto;
  flex-shrink: 0;
}

.report-review-banner[hidden] {
  display: none;
}

.no-results {
  color: var(--text-muted);
  text-align: center;
  padding: 2rem 0;
}

/* Queue */
.queue-table {
  width: 100%;
  border-collapse: collapse;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.queue-table th,
.queue-table td {
  text-align: left;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--border);
  font-size: 0.9rem;
}

.queue-table th {
  color: var(--text-muted);
  font-weight: 600;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.queue-table tr:last-child td {
  border-bottom: none;
}

.queue-table tr.clickable {
  cursor: pointer;
}

.queue-table tr.clickable:hover {
  background: rgba(255, 255, 255, 0.03);
}

.last-message-preview {
  color: var(--text-muted);
  max-width: 320px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Chat view -- three columns: user info | conversation | profile info + memo */
.chat-layout {
  display: grid;
  grid-template-columns: 300px 1fr 320px;
  /* Without this, the implicit single row defaults to grid-auto-rows:
     auto, which sizes to its content's max-content height rather than
     to this container's own explicit height below -- .chat-main's
     min-height: 0 (see its own comment) only lets it shrink that far, it
     doesn't make the row actually BE that size. minmax(0, 1fr) forces
     the row to exactly fill the container (like the columns already do),
     with no content-based minimum, so a tall message (a photo) can't
     grow the row -- and #reply-section along with it -- past the
     viewport; .chat-window's own overflow-y: auto scrolls instead. */
  grid-template-rows: minmax(0, 1fr);
  gap: 1.25rem;
  width: 100%;
  margin: 1.5rem 0;
  padding: 0 1.5rem;
  box-sizing: border-box;
  height: calc(100vh - 100px);
}

.side-panel {
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.25rem;
  overflow-y: auto;
  /* A grid item's automatic minimum width defaults to its content's
     min-content size (only overridden when overflow is non-visible on
     that axis) -- since only overflow-y is set above, this column could
     otherwise be forced wider than its 300px/320px track by anything
     inside that resists shrinking (a memo textarea, a long select
     option), blowing out the whole .chat-layout grid and the page along
     with it. min-width: 0 makes it respect the track width and let its
     own content wrap/scroll instead. */
  min-width: 0;
}

/* Compact header -- small circular photo beside name/age/city/gender,
   deliberately small so the memo section below (the actual workspace)
   dominates the panel instead of a large photo. flex-shrink: 0 since
   .side-panel is now a flex column (see its own comment) -- this and
   panel-details below keep their own natural/capped size, leaving
   memo-section (flex: 1) whatever's left. */
.panel-header {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  margin-bottom: 1rem;
  flex-shrink: 0;
}

.panel-photo-btn {
  all: unset;
  cursor: pointer;
  flex-shrink: 0;
  line-height: 0;
  border-radius: 50%;
}

.panel-photo-btn:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

.panel-photo {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: 800;
  font-size: 1.05rem;
  overflow: hidden;
}

.panel-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.user-panel .panel-photo {
  background: var(--border);
  color: var(--text-muted);
}

.panel-header-info {
  min-width: 0;
  padding-top: 0.15rem;
}

.vip-star {
  color: #d4af37;
  margin-left: 0.3rem;
}

.panel-header-info h2 {
  font-size: 1rem;
  margin: 0 0 0.3rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.panel-header-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  font-size: 0.78rem;
  color: var(--text-muted);
}

.panel-header-meta span:empty {
  display: none;
}

.panel-header-meta span:not(:last-child)::after {
  content: "\2022";
  margin-left: 0.35rem;
  color: var(--border);
}

.credits-display {
  display: flex;
  flex-shrink: 0;
  justify-content: space-between;
  align-items: center;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.5rem 0.75rem;
  margin-bottom: 1.25rem;
  font-size: 0.85rem;
}

.credits-display .credits-label {
  color: var(--text-muted);
}

#user-credits {
  font-weight: 700;
}

/* User/profile full detail fields (see chat.js's renderUserDetails/
   renderProfileDetails) -- its own max-height + scrollbar rather than
   just letting the side panel's own overflow-y: auto handle it, so a
   profile/user with a lot of filled-in fields can't push the memo
   section (right below this) an unpredictable distance down the panel.
   Capped short and flex-shrink: 0 (a fixed share of the panel) so
   memo-section (flex: 1) gets the rest -- see .side-panel's own comment
   on why it's a flex column now, specifically so memo can dominate. */
.panel-details {
  flex-shrink: 0;
  max-height: 160px;
  overflow-y: auto;
  margin-top: 1rem;
  padding-right: 0.4rem;
}

.panel-detail-fields {
  margin: 0;
}

/* Label and value side by side in one row -- a fixed label column
   width works out to aligned columns down the whole list even though
   each field is its own independent grid (a fixed px value, unlike an
   auto/fraction track, doesn't depend on that field's own content, so
   every row's label column ends up identical regardless). Widened
   panels (see .chat-layout's grid-template-columns) are what make this
   worth doing -- stacked label-then-value (the previous layout) no
   longer needed the room this reclaims. */
.panel-detail-field {
  display: grid;
  grid-template-columns: 92px 1fr;
  column-gap: 0.5rem;
  align-items: baseline;
  padding: 0.2rem 0;
  border-bottom: 1px solid var(--border);
}

.panel-detail-field:first-child {
  padding-top: 0;
}

.panel-detail-field dt {
  font-size: 0.62rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--text-muted);
}

.panel-detail-field dd {
  margin: 0;
  font-size: 0.78rem;
  line-height: 1.25;
  word-wrap: break-word;
}

/* Profile's own bio -- directly under the photo/name header (see
   #profile-about-me in chat.html), reading as a caption rather than one
   more label-value row in .panel-details below it. */
.panel-about-me {
  flex-shrink: 0;
  margin: 0.5rem 0 0;
  font-size: 0.78rem;
  font-style: italic;
  color: var(--text-muted);
  line-height: 1.3;
}

/* The dominant feature of the panel -- takes up whatever vertical space
   .panel-header/.panel-about-me/.panel-details (all flex-shrink: 0
   above) don't use, via flex: 1 on this flex column (see .side-panel's
   own comment). min-height: 0 is what actually lets it shrink to fit
   that space instead of growing to its own content's height regardless
   -- same reasoning as every other "flex: 1 + min-height: 0" pairing in
   this app (e.g. .chat-window). Scrolls internally (its own memo list
   can get long) rather than the whole side panel scrolling as one unit. */
.memo-section {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--border);
}

.memo-section h3 {
  font-size: 0.95rem;
  margin: 0 0 0.75rem;
}

.memo-section button {
  margin-top: 0.75rem;
}

#memo-list {
  margin-top: 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.memo-list-item {
  cursor: pointer;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.6rem 0.75rem;
}

.memo-list-item:hover {
  border-color: var(--primary);
}

.memo-list-item.editing {
  border-color: var(--primary);
  background: rgba(76, 154, 106, 0.08);
}

.memo-list-item .memo-item-top {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
}

/* Colored category label -- Name blue, City yellow, Age purple, Job
   orange, Sexual interests pink, Family green; custom/date memos fall
   back to grey (see chat.js's memoCategoryBadgeClass()). Solid flat
   color + white/dark text (picked per swatch for contrast) reads
   consistently in both the dark and light themes, unlike a
   theme-relative tint would. */
.memo-badge {
  display: inline-block;
  padding: 0.15rem 0.55rem;
  border-radius: 999px;
  font-size: 0.7rem;
  font-weight: 700;
  color: white;
  white-space: nowrap;
}

.memo-badge-name {
  background: #3b82f6;
}

.memo-badge-city {
  background: #eab308;
  color: #1c1e24;
}

.memo-badge-age {
  background: #8b5cf6;
}

.memo-badge-job {
  background: #f97316;
}

.memo-badge-sexual-interests {
  background: #ec4899;
}

.memo-badge-family {
  background: #22c55e;
}

.memo-badge-custom {
  background: #6b7280;
}

.memo-list-item .memo-note {
  font-size: 0.85rem;
  color: var(--text-muted);
  margin-top: 0.4rem;
  white-space: pre-wrap;
  word-wrap: break-word;
}

.memo-list-item .memo-when {
  font-size: 0.72rem;
  color: var(--text-muted);
  opacity: 0.7;
  flex-shrink: 0;
}

textarea {
  width: 100%;
  padding: 0.6rem 0.7rem;
  margin-top: 0.5rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 0.9rem;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  resize: vertical;
}

.chat-main {
  display: flex;
  flex-direction: column;
  min-width: 0;
  /* Same "auto minimum size" issue as .side-panel's min-width comment,
     on the vertical axis this time -- without it, this grid item could
     resist shrinking to the .chat-layout row's fixed height, pushing
     the reply section (the last thing in this column) below the
     viewport instead of staying pinned under a scrollable
     .chat-window. */
  min-height: 0;
}

.chat-meta {
  display: flex;
  justify-content: space-between;
  color: var(--text-muted);
  font-size: 0.85rem;
  margin-bottom: 0.75rem;
  /* A flex item's default flex-shrink is 1 -- without this, a tall
     .chat-window (see its own min-height: 0 comment below) can still
     leave .chat-main's children fighting over space, shrinking this bar
     instead of leaving all of that to .chat-window's own scrollbar. */
  flex-shrink: 0;
}

/* Debug aid -- see chat.js: shows exactly which conversation this page
   loaded, so two agents comparing screens can confirm the ids differ. */
.chat-id-badge {
  color: var(--text);
  font-weight: 700;
  font-size: 0.95rem;
}

/* Live reply countdown -- see chat.js's updateCountdownDisplay(). */
.response-countdown {
  font-weight: 700;
  font-size: 0.95rem;
  color: var(--success);
}

.response-countdown.response-countdown-urgent {
  color: var(--error);
}

.date-divider {
  align-self: center;
  color: var(--text-muted);
  font-size: 0.78rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.2rem 0.75rem;
  margin: 0.4rem 0;
}

.chat-window {
  flex: 1;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  /* Same "flex item won't shrink below its content's size" issue as
     .chat-main's own min-height: 0 comment above, one level deeper --
     a flex item's default min-height is auto, not 0, so without this a
     tall message (a 220px-max photo easily does it, plain text rarely
     does) grows .chat-window past its flex:1 share instead of scrolling
     internally, pushing #reply-section below .chat-layout's fixed
     height/the viewport instead of staying pinned under it. */
  min-height: 0;
}

.bubble {
  max-width: 70%;
  padding: 0.6rem 0.85rem;
  border-radius: 14px;
  font-size: 0.92rem;
  line-height: 1.35;
  word-wrap: break-word;
}

.bubble .meta {
  display: block;
  font-size: 0.72rem;
  opacity: 0.7;
  margin-bottom: 0.15rem;
}

/* User messages on the left (incoming); the agent's replies as the
   profile on the right (outgoing) -- mirrors the user's own view at
   localhost:3000, just flipped, since here the profile is "you". */
.bubble.from-user {
  align-self: flex-start;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-bottom-left-radius: 4px;
}

.bubble.from-profile {
  align-self: flex-end;
  background: var(--primary);
  color: white;
  border-bottom-right-radius: 4px;
}

/* The reported message itself, scrolled to and flagged on load when
   chat.html is opened from a report row (see reports.js/chat.js's
   loadConversation isInitial branch). Declared after .bubble.from-profile
   above so its background wins at equal specificity -- a reported
   message is always agent-sent (see the report menu's own gating). */
.bubble.message-reported {
  border: 2px solid var(--error);
  background: rgba(242, 114, 114, 0.35);
}

/* Per-message agent attribution + report menu (see renderMessage in
   public/js/chat.js) -- only ever on an agent-sent (.from-profile)
   bubble. The label sits in normal flow, right-aligned, below the
   message text -- reads as "bottom right of the bubble" without needing
   absolute positioning that could overlap the text above it. */
.message-agent-label {
  display: block;
  width: auto;
  margin: 0.3rem 0 0 auto;
  padding: 0;
  background: none;
  border: none;
  color: inherit;
  opacity: 0.75;
  font-size: 0.72rem;
  text-decoration: underline;
  text-align: right;
}

.message-agent-label:hover {
  opacity: 1;
  background: none;
}

.report-menu {
  min-width: 210px;
  margin-top: 0.4rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
  padding: 0.4rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  text-align: left;
  color: var(--text);
  font-weight: 400;
}

/* Same cascade fix as .nav-menu-dropdown[hidden] above -- the
   unconditional display:flex here otherwise beats [hidden]{display:none}
   regardless of the hidden attribute. */
.report-menu[hidden] {
  display: none;
}

.report-menu-item {
  width: 100%;
  background: transparent;
  color: var(--text);
  border: none;
  border-radius: 6px;
  text-align: left;
  padding: 0.45rem 0.6rem;
  font-size: 0.85rem;
  font-weight: 500;
}

.report-menu-item:hover {
  background: rgba(255, 255, 255, 0.08);
}

.report-menu-other {
  display: flex;
  gap: 0.4rem;
  margin-top: 0.2rem;
  padding: 0 0.2rem;
}

.report-menu-other input[type="text"] {
  flex: 1;
}

.report-menu-other button {
  width: auto;
  flex-shrink: 0;
}

.report-menu .message {
  margin-top: 0.3rem;
}

/* #reply-section (the input row, char counter, and photo-attach strip)
   is a flex child of .chat-main alongside .chat-window -- a flex item's
   default flex-shrink is 1, so without this it competes with
   .chat-window for space instead of staying pinned at its natural size,
   which is what let a tall message (e.g. a photo) squeeze the input and
   Send button out of view. .chat-window's own flex:1 + min-height:0
   (see its comment) is what makes it -- and only it -- the one that
   shrinks/scrolls to absorb that instead. */
#reply-section {
  flex-shrink: 0;
}

.chat-input-row {
  display: flex;
  gap: 0.6rem;
  margin-top: 1rem;
}

.chat-input-row input[type="text"] {
  flex: 1;
}

.chat-input-row button {
  width: auto;
  flex-shrink: 0;
}

.emoji-picker-wrap {
  position: relative;
  flex-shrink: 0;
  /* A flex container so #emoji-picker-btn (its flex item) stretches to
     this wrap's own height -- which itself stretches to .chat-input-row's
     height via that row's default align-items: stretch, same as
     #chat-input right next to it. Without this, the button's height was
     only ever its own padding + the emoji glyph's line-height, which
     renders shorter than the input's text line-height at the same
     font-size, leaving the two visibly mismatched. */
  display: flex;
}

#emoji-picker-btn {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 1.1rem;
  line-height: 1;
  padding: 0.5rem 0.6rem;
  display: flex;
  align-items: center;
  justify-content: center;
}

#emoji-picker-btn:hover {
  background: var(--bg);
}

.emoji-picker-popup {
  position: absolute;
  bottom: 100%;
  left: 0;
  margin-bottom: 0.4rem;
  display: grid;
  /* 6 columns (was 8) at this width gives each item roughly a 44px-wide
     cell -- room for the larger emoji/tap target below -- rather than the
     ~30px cells 8 columns produced at the same popup width. */
  grid-template-columns: repeat(6, 1fr);
  gap: 0.15rem;
  width: 280px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.5rem;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
  z-index: 20;
}

/* .emoji-picker-popup's own `display: grid` above is a class selector,
   so it beats the browser's default `[hidden] { display: none }` rule
   on specificity -- without this override, toggling the element's
   `hidden` property in JS (chat.js's setUpEmojiPicker) has no visual
   effect and the popup stays visible even when closed. Same fix as
   .chat-photo-preview[hidden] elsewhere in this codebase. */
.emoji-picker-popup[hidden] {
  display: none;
}

.emoji-picker-item {
  width: auto;
  background: transparent;
  border: none;
  border-radius: 6px;
  font-size: 1.6rem;
  line-height: 1;
  padding: 0.5rem;
  cursor: pointer;
}

.emoji-picker-item:hover {
  background: var(--bg);
}

/* Minimum-length counter + short-message-allowance checkbox, below the
   reply input -- see chat.js's updateCharCounter(). */
.chat-meta-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.5rem;
  font-size: 0.82rem;
  color: var(--text-muted);
}

#char-counter.char-counter-invalid {
  color: var(--error);
  font-weight: 600;
}

.short-allowance-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-weight: 400;
  cursor: pointer;
}

.short-allowance-row input {
  width: auto;
}

/* Admin dashboard */
select {
  width: 100%;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 0.95rem;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
}

.checkout-row {
  display: flex;
  gap: 1rem;
}

/* Signup/Visit/Purchase trigger forms -- amount + minutes/seconds unit */
.delay-input-row {
  display: flex;
  gap: 0.6rem;
}

.delay-input-row input[type="number"] {
  flex: 1;
}

.delay-input-row select {
  width: auto;
  flex-shrink: 0;
}

.checkout-row > div {
  flex: 1;
}

/* Sits the Apply button's own baseline level with the date inputs next
   to it (a bare label-less div would otherwise start at the row's top,
   same as the labeled From/To ones -- see the Agent Stats page and My
   Statistics panel's date-range filters). */
.date-filter-apply-wrap {
  display: flex;
  align-items: flex-end;
}

/* Recipient filters (Has avatar / Has credits) -- small selects side by
   side, right below the Profile field in each trigger-type form. */
.recipient-filter-row {
  display: flex;
  gap: 0.6rem;
  margin-bottom: 1rem;
}

.recipient-filter-row > div {
  flex: 1;
}

/* $username/$city variable-tag buttons below a message/follow-up
   textarea (Create Triggers page) -- same small-button sizing as
   .queue-table button, just scoped to this class instead of a table. */
.variable-tag-btn {
  padding: 0.4rem 0.8rem;
  font-size: 0.82rem;
}

/* Create Triggers page (public/triggers.html) -- form on the left,
   saved-triggers lists on the right. Collapses to one column on a narrow
   viewport since the form's own fields (message textarea, photo strip)
   need real width to stay usable. */
.trigger-layout {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.5rem;
  align-items: start;
}

@media (max-width: 900px) {
  .trigger-layout {
    grid-template-columns: 1fr;
  }
}

/* Profile picker (Create Triggers' Profile field) -- a native <select>
   with avatar+age options, which no browser can render (a plain <option>
   is text-only), replaced here with a custom button+popup that shows
   them. The real <select> stays in the DOM, visually hidden but still
   rendered (so its own `required` constraint still participates in form
   validation) -- it's the actual source of truth every existing read
   (`profileSelect.value`) and `change` listener in triggers.js keeps
   using unmodified; this only changes how choosing a profile looks. */
.profile-picker {
  position: relative;
}

.profile-picker-native {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.profile-picker-btn {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  text-align: left;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  font-weight: 500;
  font-size: 0.95rem;
  padding: 0.5rem 0.7rem;
}

.profile-picker-btn:hover {
  background: rgba(255, 255, 255, 0.05);
}

.profile-picker-popup {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: 0.3rem;
  max-height: 280px;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
  z-index: 20;
}

.profile-picker-item {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  padding: 0.5rem 0.7rem;
  background: transparent;
  border: none;
  color: var(--text);
  font-weight: 500;
  font-size: 0.95rem;
  text-align: left;
  cursor: pointer;
}

.profile-picker-item:hover,
.profile-picker-item.active {
  background: rgba(255, 255, 255, 0.06);
}

.profile-picker-btn .profile-thumb,
.profile-picker-item .profile-thumb {
  width: 32px;
  height: 32px;
  font-size: 0.7rem;
  flex-shrink: 0;
}

/* Profiles admin's create/edit form -- see public/profiles.html and
   public/js/profiles.js's renderExtraFieldsHtml. Two fields per row;
   .profile-form-field-full (a wide field like About me, or a checkbox
   group like Hobbies/Here for) spans both columns instead. Own class
   rather than reusing .checkout-row (a single-row flex layout elsewhere
   on this and other pages) since this needs to wrap onto multiple rows. */
.profile-form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: 1.5rem;
}

.profile-form-field-full {
  grid-column: 1 / -1;
}

.queue-table .row-actions {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}

.queue-table button {
  width: auto;
  padding: 0.4rem 0.8rem;
  font-size: 0.82rem;
}

.queue-table button.btn-secondary {
  background: transparent;
  color: var(--text-muted);
  border: 1px solid var(--border);
}

.queue-table button.btn-danger {
  background: transparent;
  color: var(--error);
  border: 1px solid var(--error);
}

/* Smaller still than the .queue-table button baseline above -- two
   actions (Release, Ban) sit side by side in one row here, unlike every
   other .queue-table's single-action cell. Scoped to blocked.html's own
   table body so no other page's buttons shrink. */
#blocked-tbody button {
  padding: 0.25rem 0.55rem;
  font-size: 0.75rem;
}

.queue-table input[type="password"].inline-password {
  width: 140px;
  padding: 0.4rem 0.6rem;
  font-size: 0.82rem;
  margin-top: 0;
  display: inline-block;
}

/* Profiles admin's per-row site/language multi-select (see
   public/js/profiles.js's siteMultiSelectHtml) -- same compact treatment
   as .inline-password above, just wide enough for a site's full
   "Name (slug -- LANG)" label instead of a bare language code. */
.queue-table select.site-multiselect {
  width: 260px;
  padding: 0.4rem 0.6rem;
  font-size: 0.82rem;
  margin-top: 0;
}

.languages-cell span {
  margin-right: 0.5rem;
}

.status-active {
  color: var(--success);
}

.status-suspended {
  color: var(--text-muted);
}

/* Left sidebar for the admin area (admin.html) -- same style as the main
   site's admin sidebar. queue.html/chat.html (the chat agent area) keep
   the top .nav above; this is deliberately separate, not a replacement. */
.sidebar {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: 220px;
  background: #0d0f14;
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  padding: 1.5rem 0;
  overflow-y: auto;
}

.sidebar-brand {
  color: var(--text);
  font-weight: 800;
  font-size: 1.05rem;
  padding: 0 1.5rem 1.5rem;
}

.sidebar-nav {
  display: flex;
  flex-direction: column;
  flex: 1;
}

.sidebar-nav a {
  color: var(--text-muted);
  text-decoration: none;
  font-weight: 500;
  font-size: 0.95rem;
  padding: 0.7rem 1.5rem;
  border-left: 3px solid transparent;
}

.sidebar-nav a:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.05);
}

.sidebar-nav a.active {
  color: var(--text);
  background: rgba(255, 255, 255, 0.06);
  border-left-color: var(--primary);
}

/* Sites & Languages sub-menu, appended below the main flat nav list --
   a non-clickable label followed by two indented links, same
   hover/active treatment as every other .sidebar-nav a. */
.sidebar-nav-section {
  color: var(--text-muted);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 1rem 1.5rem 0.3rem;
}

.sidebar-nav a.sidebar-nav-sub {
  padding-left: 2.4rem;
  font-size: 0.9rem;
}

/* Settings collapsible menu item (see public/js/api.js's own generic
   toggle wiring, run on every page since the sidebar is duplicated on
   each one) -- a <button>, not an <a>, since it toggles the submenu
   below it rather than navigating. Overrides the base button rule's own
   background/border-radius/width/padding so it reads as a sidebar row,
   not a filled button. */
.sidebar-nav-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  background: transparent;
  border: none;
  border-radius: 0;
  border-left: 3px solid transparent;
  color: var(--text-muted);
  font-weight: 500;
  font-size: 0.95rem;
  font-family: inherit;
  padding: 0.7rem 1.5rem;
  cursor: pointer;
  text-align: left;
}

.sidebar-nav-toggle:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.05);
}

.sidebar-nav-toggle-arrow {
  display: inline-block;
  font-size: 0.75rem;
  transition: transform 0.15s ease;
}

.sidebar-nav-toggle.expanded .sidebar-nav-toggle-arrow {
  transform: rotate(90deg);
}

.sidebar-submenu {
  display: flex;
  flex-direction: column;
}

/* .sidebar-submenu's own `display: flex` above is a class selector, so it
   beats the browser's default `[hidden] { display: none }` rule on
   specificity -- without this override, setting the element's `hidden`
   property in JS had no visual effect and the submenu always stayed
   expanded, even on first load. */
.sidebar-submenu[hidden] {
  display: none;
}

.sidebar-submenu a {
  padding-left: 2.5rem;
  font-size: 0.9rem;
}

/* Sites page: the .env block shown after creating a site, and the manual
   steps checklist next to it. */
.code-block {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85rem;
  color: var(--text);
  overflow-x: auto;
  white-space: pre;
}

.manual-steps-list {
  margin: 0.5rem 0 0;
  padding-left: 1.3rem;
  color: var(--text);
  font-size: 0.9rem;
}

.manual-steps-list li {
  margin-bottom: 0.4rem;
}

/* Sites page's grouped table -- a bold group-header row (base site name +
   slug) followed by its indented per-language sub-rows. */
.site-group-header td {
  font-weight: 700;
  background: rgba(255, 255, 255, 0.03);
}

.site-group-header .site-group-slug {
  color: var(--text-muted);
  font-weight: 400;
  font-size: 0.85rem;
  margin-left: 0.5rem;
}

.site-sub-row td:first-child {
  padding-left: 2rem;
}

.site-group-header,
.site-sub-row {
  cursor: pointer;
}

.site-group-header:hover td,
.site-sub-row:hover td {
  background: rgba(255, 255, 255, 0.05);
}

/* Wider than the default .modal-panel (320px) -- the DATABASE_URL and
   INTERNAL_API_KEY lines in .code-block don't fit legibly at that width. */
#site-detail-modal .modal-panel {
  width: 480px;
}

.sidebar-logout {
  color: var(--text-muted);
  text-decoration: none;
  font-size: 0.9rem;
  padding: 1rem 1.5rem;
  margin-top: 1rem;
  border-top: 1px solid var(--border);
}

.sidebar-logout:hover {
  color: var(--text);
}

.main-content {
  margin-left: 220px;
  min-height: 100vh;
}

/* Profiles admin */
.site-checklist {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.25rem;
  margin-top: 0.5rem;
}

.site-checklist.inline {
  flex-direction: column;
  gap: 0.3rem;
  font-size: 0.82rem;
}

.site-checklist-item {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.88rem;
  font-weight: 400;
  margin: 0;
}

.site-checklist-item input {
  width: auto;
}

input[type="file"] {
  width: 100%;
  margin-top: 0.5rem;
  font-size: 0.88rem;
  color: var(--text-muted);
}

.profile-thumb {
  width: 44px;
  height: 44px;
  border-radius: 8px;
  object-fit: cover;
  display: block;
}

.profile-thumb-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: 800;
  font-size: 0.85rem;
}

/* Additional-photos section (profile edit page) */
.photo-thumb-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin: 0.75rem 0;
}

.photo-thumb-item {
  position: relative;
  width: 84px;
  height: 84px;
}

.photo-thumb-item img {
  width: 100%;
  height: 100%;
  border-radius: 8px;
  object-fit: cover;
  display: block;
}

.field-hint {
  color: var(--text-muted);
  font-size: 0.82rem;
  margin: 0.4rem 0;
}

/* Chat view -- attach-a-photo strip below the message input */
.photo-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: 0.75rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--border);
}

.photo-strip-item {
  position: relative;
  width: 64px;
  height: 64px;
  cursor: pointer;
}

.photo-strip-item img {
  width: 100%;
  height: 100%;
  border-radius: 8px;
  object-fit: cover;
  display: block;
  border: 2px solid transparent;
}

.photo-strip-item.selected img {
  border-color: var(--primary);
}

.photo-strip-item.already-sent::after {
  content: "\2713";
  position: absolute;
  top: -6px;
  right: -6px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--success);
  color: white;
  font-size: 0.7rem;
  font-weight: 800;
  line-height: 18px;
  text-align: center;
}

#photo-preview-area {
  margin-top: 0.75rem;
  text-align: center;
}

#photo-preview-area img {
  max-width: 260px;
  max-height: 260px;
  border-radius: var(--radius);
  object-fit: contain;
}

#photo-preview-area button {
  display: block;
  width: auto;
  margin: 0.5rem auto 0;
}

.bubble img {
  max-width: 220px;
  max-height: 220px;
  border-radius: 8px;
  display: block;
  object-fit: cover;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

.bubble .meta + img,
.bubble img + .bubble-body {
  margin-top: 0.4rem;
}

/* A gift attached to a message (chatosphere's routes/gifts.js) -- a fixed
   catalog SVG, same inline-image treatment as .bubble img above, just a
   fixed smaller size since it's an icon rather than a photo. */
.gift-message-img {
  max-width: 72px;
  max-height: 72px;
  display: block;
  border-radius: 8px;
}

/* A <span> is inline by default. chat.js already puts the image before
   this in the markup (image, then text) -- explicit display: block here
   removes any ambiguity from that being an inline element and guarantees
   the text always renders as its own full-width line under the image,
   never beside or over it. */
.bubble-body {
  display: block;
}

.photo-thumb-item .btn-delete-photo {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 20px;
  height: 20px;
  padding: 0;
  border-radius: 50%;
  background: var(--error);
  color: white;
  font-size: 0.75rem;
  font-weight: 800;
  line-height: 1;
  border: 2px solid var(--surface);
}

.sites-cell span {
  display: inline-block;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.1rem 0.6rem;
  font-size: 0.78rem;
  margin: 0 0.25rem 0.25rem 0;
  color: var(--text-muted);
}

.checkbox-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 1.1rem;
}

.checkbox-row input {
  width: auto;
}

.checkbox-row label {
  margin: 0;
  font-weight: 400;
  font-size: 0.88rem;
}

.btn-row-split {
  display: flex;
  gap: 0.75rem;
}

.btn-row-split button {
  width: auto;
}

/* Online Schedule page (see js/online-schedule.js) -- one row per fixed
   time slot, a percentage input plus a plain-text suffix. */
.online-schedule-row {
  margin-bottom: 1.4rem;
}

.online-schedule-row:last-child {
  margin-bottom: 0;
}

.online-schedule-input-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.online-schedule-input-row input[type="number"] {
  width: 90px;
  flex: none;
}

.online-schedule-input-row span {
  color: var(--text-muted);
  font-size: 0.88rem;
}

#current-photo-preview {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-top: 0.75rem;
  font-size: 0.82rem;
  color: var(--text-muted);
}

#current-photo-preview img {
  width: 44px;
  height: 44px;
  border-radius: 8px;
  object-fit: cover;
}

.profile-cell {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.queue-table tbody tr.profile-row {
  cursor: pointer;
}

.queue-table tbody tr.profile-row:hover {
  background: rgba(255, 255, 255, 0.03);
}

/* Reports admin page (public/reports.html) -- dismissed status shown in
   place of the Dismiss button once a report's been resolved. */
.report-dismissed-label {
  color: var(--text-muted);
  font-size: 0.85rem;
}

/* Dashboard graphs: quick filter buttons + a responsive grid of chart
   panels, above the existing Live queue table. */
.dashboard-filter-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-bottom: 1.5rem;
}

.dashboard-filter-btn {
  width: auto;
  background: transparent;
  color: var(--text-muted);
  border: 1px solid var(--border);
  padding: 0.5rem 1rem;
  font-size: 0.85rem;
}

.dashboard-filter-btn:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.05);
}

.dashboard-filter-btn.active {
  background: var(--primary);
  color: white;
  border-color: var(--primary);
}

.dashboard-graphs-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.5rem;
}

.dashboard-graph-panel {
  min-width: 0;
}

.dashboard-graph-panel h3 {
  margin: 0 0 1rem;
  font-size: 1rem;
}

.dashboard-graph-panel canvas {
  max-height: 260px;
}

/* Profiles page: name search + site/language filter, side by side above
   the table -- same layout as .recipient-filter-row's label-over-input
   pairs. */
.profile-filters-row {
  display: flex;
  gap: 0.6rem;
  margin-bottom: 1rem;
}

.profile-filters-row > div {
  flex: 1;
}

/* Prev/Next + "Page X of Y" below the Profiles table. */
.pagination-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  margin-top: 1rem;
}

.pagination-row button {
  width: auto;
}

.pagination-row span {
  color: var(--text-muted);
  font-size: 0.9rem;
}
