/* rivendell — a restrained dark theme. System font stack on purpose: no external
   font fetches (one less dependency, and nothing leaks to a CDN). */

:root {
  --bg: #16181d;
  --bg-2: #1d2026;
  --bg-3: #23272f;
  --bg-hover: #2a2f38;
  --border: #2e333d;
  --text: #d8dce3;
  --text-dim: #8b929e;
  --text-faint: #626975;
  --accent: #6ea8fe;
  --accent-dim: #3a5a8c;
  --online: #43b581;
  --away: #e0a458;
  --dnd: #e06c75;
  --danger: #e06c75;
  --radius: 8px;
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* Themes. The default (above) is the built-in dark look; a theme is selected by
   data-theme on <html> and only re-points the color variables — layout/sizing
   live in the rules below and are theme-agnostic. The set here must mirror
   validThemes in internal/httpapi/handlers.go and the <select> in index.html.
   button.primary / .unread-badge paint dark text (#0b1220) on --accent, so every
   --accent stays light/bright enough to read against. */

html[data-theme="light"] {
  --bg: #ffffff;
  --bg-2: #f4f5f7;
  --bg-3: #e9ebf0;
  --bg-hover: #e2e5ea;
  --border: #d4d8e0;
  --text: #1f2329;
  --text-dim: #5c6370;
  --text-faint: #8b929e;
  --accent: #4d8bff;
  --accent-dim: #aac4f5;
  --online: #2faa6b;
  --away: #c98a2e;
  --dnd: #d64550;
  --danger: #d64550;
}

html[data-theme="forest"] {
  --bg: #121a14;
  --bg-2: #18221a;
  --bg-3: #1f2c22;
  --bg-hover: #26352a;
  --border: #2c3d30;
  --text: #d6e4d8;
  --text-dim: #8aa18f;
  --text-faint: #5f7565;
  --accent: #6fcf8f;
  --accent-dim: #3a6b4a;
  --online: #6fcf8f;
  --away: #d8b25a;
  --dnd: #e0726c;
  --danger: #e0726c;
}

html[data-theme="hotpink"] {
  --bg: #1a1016;
  --bg-2: #23141d;
  --bg-3: #2c1924;
  --bg-hover: #381f2d;
  --border: #432536;
  --text: #f3dce9;
  --text-dim: #c08aa6;
  --text-faint: #8a6178;
  --accent: #ff66b2;
  --accent-dim: #8a2d5d;
  --online: #4fd1a0;
  --away: #e0a458;
  --dnd: #ff5d7a;
  --danger: #ff5d7a;
}

html[data-theme="contrast"] {
  --bg: #000000;
  --bg-2: #000000;
  --bg-3: #0a0a0a;
  --bg-hover: #1f1f1f;
  --border: #ffffff;
  --text: #ffffff;
  --text-dim: #e6e6e6;
  --text-faint: #c2c2c2;
  --accent: #ffd400;
  --accent-dim: #b39600;
  --online: #00ff66;
  --away: #ffaa00;
  --dnd: #ff4040;
  --danger: #ff4040;
}

html[data-theme="vermillion"] {
  --bg: #1a1310;
  --bg-2: #221813;
  --bg-3: #2c1f18;
  --bg-hover: #382820;
  --border: #45342a;
  --text: #f0e3da;
  --text-dim: #c0a08f;
  --text-faint: #8a7263;
  --accent: #ff5a3c;
  --accent-dim: #8a3525;
  --online: #5fcf8f;
  --away: #e0a458;
  --dnd: #ff5d5d;
  --danger: #ff5d5d;
}

html[data-theme="cool-blue"] {
  --bg: #0d1117;
  --bg-2: #131c2b;
  --bg-3: #192336;
  --bg-hover: #1f2d45;
  --border: #253550;
  --text: #cce0ff;
  --text-dim: #7ea8d8;
  --text-faint: #4a6a9a;
  --accent: #38bdf8;
  --accent-dim: #0c4a6e;
  --online: #34d399;
  --away: #fbbf24;
  --dnd: #f87171;
  --danger: #f87171;
}

* { box-sizing: border-box; }

/* The `hidden` attribute must win over class-level `display` rules (.modal,
   .centered, .app all set display, which would otherwise defeat [hidden]). */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  height: 100%;
  /* The page itself never scrolls — the message list / drawers scroll inside a
     viewport-sized app. This stops mobile keyboards from scrolling the header
     off the top when the composer is focused. */
  overflow: hidden;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
}

button { font-family: inherit; cursor: pointer; }
input, textarea, select { font-family: inherit; font-size: 14px; }

/* --- centered auth views --- */
.centered {
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--app-height, 100%);
  padding: 1rem;
}
.card {
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 2.2rem;
  width: 100%;
  max-width: 360px;
}
.brand {
  font-weight: 700;
  letter-spacing: -0.03em;
  margin: 0;
  font-size: 2rem;
}
.brand.small { font-size: 1.1rem; }
.tagline { color: var(--text-dim); margin: 0.2rem 0 1.6rem; font-size: 0.9rem; }
form label { display: block; margin-bottom: 0.9rem; color: var(--text-dim); font-size: 0.8rem; }
form input, form textarea, form select, #composer-input, .admin-create input, .admin-create select, .status-select {
  width: 100%;
  margin-top: 0.3rem;
  padding: 0.55rem 0.65rem;
  background: var(--bg-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
}
form textarea { resize: vertical; line-height: 1.4; }
form input:focus, form textarea:focus, form select:focus, #composer-input:focus { outline: none; border-color: var(--accent-dim); }
/* Native file inputs ship an unstyled "Browse..." button — theme it to match
   the rest of the form so it doesn't render as a bare system button. */
input[type="file"] { padding: 0.35rem 0.5rem; }
input[type="file"]::file-selector-button {
  margin-right: 0.6rem;
  padding: 0.35rem 0.7rem;
  background: var(--bg-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font: inherit;
  cursor: pointer;
}
input[type="file"]::file-selector-button:hover { background: var(--border); }
button.primary {
  width: 100%;
  padding: 0.6rem;
  background: var(--accent);
  color: #0b1220;
  border: none;
  border-radius: var(--radius);
  font-weight: 600;
}
button.primary:hover { filter: brightness(1.08); }
.error { color: var(--danger); font-size: 0.85rem; min-height: 1.1em; margin: 0.6rem 0 0; }
/* The error <p> is always a form's trailing element (below the submit button), so
   its reserved min-height/margin only ever shows as dead space — and in a scrollable
   modal-card that becomes pointless scroll. Collapse it until it actually holds text. */
.error:empty { min-height: 0; margin-top: 0; }

/* --- app layout --- */
.app {
  display: grid;
  grid-template-columns: 240px 1fr 200px;
  grid-template-rows: 100%;
  height: var(--app-height, 100%);
  overflow: hidden; /* the page itself never scrolls; columns scroll internally */
}
/* Grid/flex children default to min-height:auto, which lets tall content push the
   track past the viewport (whole-page scroll). Pinning min-height:0 lets the
   inner overflow-y:auto regions (message list, channel list, members) scroll. */
.sidebar, .main, .members { min-height: 0; }
.sidebar { background: var(--bg-2); border-right: 1px solid var(--border); display: flex; flex-direction: column; }
.sidebar-head { display: flex; align-items: center; justify-content: space-between; padding: 0.9rem 1rem; border-bottom: 1px solid var(--border); }
.conn { width: 9px; height: 9px; border-radius: 50%; background: var(--text-faint); }
.conn.online { background: var(--online); }
.conn.offline { background: var(--danger); }
/* Global "missed notifications" total: sits at the right with the connection
   dot. Relies on [hidden]{display:none!important}. The signout button's
   margin-right:auto eats the free space, so this group stays right-aligned. */
.notif-total { background: var(--danger); color: #fff; font-size: 0.66rem; font-weight: 700; min-width: 16px; height: 16px; padding: 0 5px; border-radius: 8px; display: flex; align-items: center; justify-content: center; }
.sidebar-head .conn { margin-left: 0.5rem; }
.signout-btn { background: none; border: none; font-size: 1.1rem; cursor: pointer; padding: 0; margin-left: 0.5rem; margin-right: auto; line-height: 1; opacity: 0.55; }
.signout-btn:hover { opacity: 1; }
.hint { color: var(--text-faint); font-size: 0.8rem; margin: 0.1rem 0 0.6rem; min-height: 1.1em; }

/* "A new version is available" — a small, non-blocking bar pinned bottom-centre,
   above the composer. Relies on [hidden]{display:none!important}. */
.update-banner {
  position: fixed; left: 50%; bottom: 1rem; transform: translateX(-50%);
  z-index: 50; display: flex; align-items: center; gap: 0.6rem;
  background: var(--bg-3); border: 1px solid var(--accent-dim); border-radius: var(--radius);
  padding: 0.5rem 0.6rem 0.5rem 0.9rem; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
  font-size: 0.85rem; max-width: calc(100vw - 2rem);
}
.update-banner .link { color: var(--accent); font-weight: 600; }

.section-label { text-transform: uppercase; font-size: 0.68rem; letter-spacing: 0.08em; color: var(--text-faint); font-weight: 600; }
/* The scroll region holds both the Channels and Direct messages lists so they
   share one scrollbar and the sidebar foot stays pinned. */
.sidebar-scroll { flex: 1; min-height: 0; overflow-y: auto; }
.channels-head { display: flex; align-items: center; justify-content: space-between; padding: 1rem 1rem 0.4rem; }
.channel-list { list-style: none; margin: 0; padding: 0 0.5rem; }
.channel { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; padding: 0.32rem 0.5rem; border-radius: var(--radius); color: var(--text-dim); cursor: pointer; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
/* Mods reorder by dragging a row (the affordance that replaced the ↑/↓ glyphs):
   a grab cursor hints at it, the dragged row lifts, and grabbing locks while held.
   Once picked up (mouse hold-to-lift or touch long-press → .dragging) the row lifts
   clear of the list — a pronounced drop-shadow + slight scale so it reads as "in
   hand". z-index floats the shadow over its neighbors; the transition animates the
   pickup moment (during the drag the row follows the pointer via DOM reorder, not
   transform, so the transition only plays on lift). */
.channel-list.reorderable .channel { cursor: grab; }
.channel.dragging { background: var(--bg-3); color: var(--text); opacity: 1; box-shadow: 0 10px 26px rgba(0, 0, 0, 0.55); transform: scale(1.02); position: relative; z-index: 5; cursor: grabbing; transition: box-shadow 0.12s ease, transform 0.12s ease; }
body.ch-dragging, body.ch-dragging .channel { cursor: grabbing; }
.ch-voice { width: 100%; padding-left: 1.2rem; margin-top: -0.15rem; font-size: 0.75rem; color: var(--text-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.channel:hover { background: var(--bg-hover); }
.channel.active { background: var(--bg-3); color: var(--text); }
.ch-hash { color: var(--text-faint); flex-shrink: 0; }
.ch-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ch-controls { display: none; gap: 0.05rem; margin-left: auto; flex-shrink: 0; }
.channel:hover .ch-controls { display: flex; }
.ch-ctl { background: none; border: none; color: var(--text-faint); font-size: 0.82rem; padding: 0 0.15rem; line-height: 1; }
.ch-ctl:hover { color: var(--text); }
.ch-ctl.danger:hover { color: var(--danger); }
/* Unread: bold the name and show a count pill; the pill hides on hover so the
   mod controls (also right-aligned) have room. */
.channel.unread .ch-name { color: var(--text); font-weight: 600; }
/* Muted channels read as quieted: dimmed name and hash/dot. */
.channel.muted .ch-name, .channel.muted .ch-hash { color: var(--text-faint); }
.channel.muted .ch-name { font-weight: 400; }
.unread-badge { margin-left: auto; flex-shrink: 0; background: var(--accent); color: #0b1220; font-size: 0.66rem; font-weight: 700; min-width: 16px; height: 16px; padding: 0 5px; border-radius: 8px; display: flex; align-items: center; justify-content: center; }
.unread-badge.mention { background: var(--danger); color: #fff; }
#channel-list .channel:hover .unread-badge { display: none; }

.sidebar-foot { display: flex; align-items: center; gap: 0.55rem; padding: 0.9rem 0.8rem; border-top: 1px solid var(--border); }
.me-meta { flex: 1; min-width: 0; }
.me-name { font-weight: 600; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; }
.me-name:hover { color: var(--accent); }
.me-status-text { color: var(--text-dim); font-size: 0.78rem; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.me-status-text:hover { color: var(--text); }
.status-select { width: auto; margin: 0; padding: 0.2rem; font-size: 0.72rem; }

.avatar {
  width: 34px; height: 34px; border-radius: 50%;
  background: var(--accent-dim); background-size: cover; background-position: center;
  display: flex; align-items: center; justify-content: center;
  font-size: 0.72rem; font-weight: 700; color: #dce6ff; flex-shrink: 0;
}
/* Your own avatar (lower-left) is the avatar uploader — make it feel clickable. */
.me-avatar { cursor: pointer; }
.me-avatar:hover { box-shadow: 0 0 0 2px var(--accent); }

.link { background: none; border: none; color: var(--text-dim); font-size: 0.8rem; padding: 0; cursor: pointer; }
.link:hover { color: var(--accent); }
.link.danger:hover { color: var(--danger); }
.icon-btn { background: var(--bg-3); border: 1px solid var(--border); color: var(--text); border-radius: 6px; width: 24px; height: 24px; line-height: 1; }
.icon-btn:hover { background: var(--bg-hover); }

/* --- main column --- */
.main { display: flex; flex-direction: column; min-width: 0; }
.channel-header { padding: 1rem 1.2rem; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 0.6rem; }
.channel-header-main { flex: 1; min-width: 0; }
/* The drawer toggles are mobile-only; hidden on desktop where the sidebar and
   members panel are permanent grid columns. */
#sidebar-toggle, #members-toggle { display: none; align-self: center; flex-shrink: 0; }
#members-toggle { margin-left: auto; }
#pins-btn { align-self: center; flex-shrink: 0; }
.drawer-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 45; }
.channel-title { font-weight: 700; }
#channel-dm-dot { display: inline-block; vertical-align: middle; margin: 0 0.7rem 0 0.4rem; }
.channel-topic { color: var(--text-dim); font-size: 0.85rem; }
/* Moderator+ can click the topic to edit it inline. */
.channel-topic.editable { cursor: pointer; }
.channel-topic.editable:hover { color: var(--text); text-decoration: underline dotted; text-underline-offset: 2px; }
.channel-topic.placeholder { font-style: italic; opacity: 0.7; }
.topic-edit { font-size: 0.85rem; padding: 0.1rem 0.4rem; border: 1px solid var(--border); border-radius: 6px; background: var(--bg-3); color: var(--text); width: min(420px, 60vw); }

.message-list { flex: 1; min-height: 0; overflow-y: auto; padding: 1rem 1.2rem; }
/* Infinite-scroll trip wires; height 1px so they're real IntersectionObserver targets. */
.scroll-sentinel { height: 1px; margin: 0; padding: 0; pointer-events: none; }
.msg { display: flex; gap: 0.7rem; padding: 0.18rem 0; position: relative; }
.msg.grouped { padding-top: 0; }
/* Optimistic (not-yet-acked) send: dim the row and italicize its "sending…" time
   until the server's message.new echo reconciles it into a normal row. */
.msg.pending { opacity: 0.55; }
.msg-time-pending { font-style: italic; }
.msg-gutter { width: 38px; flex-shrink: 0; }
.msg-avatar { width: 38px; height: 38px; border-radius: 50%; background: var(--accent-dim); background-size: cover; background-position: center; display: flex; align-items: center; justify-content: center; font-size: 0.78rem; font-weight: 700; color: #dce6ff; flex-shrink: 0; margin-top: 0.15rem; }
.msg-main { min-width: 0; flex: 1; }
.msg-head { display: flex; align-items: baseline; gap: 0.5rem; }
.msg-author { font-weight: 600; }
.msg-avatar.clickable, .msg-author.clickable { cursor: pointer; }
.msg-author.clickable:hover { color: var(--accent); }
.msg-avatar.clickable:hover { box-shadow: 0 0 0 2px var(--accent); }

/* Read-only profile card (opened by clicking an avatar/name). */
.user-card { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; text-align: center; padding: 0.5rem 0.25rem 0.25rem; }
.user-card-avatar { width: 88px; height: 88px; border-radius: 50%; background: var(--accent-dim); background-size: cover; background-position: center; display: flex; align-items: center; justify-content: center; font-size: 1.6rem; font-weight: 700; color: #dce6ff; }
.user-card-name { font-size: 1.2rem; font-weight: 600; display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; justify-content: center; }
.user-card-pronouns { font-size: 0.8rem; font-weight: 400; color: var(--text-faint); }
.user-card-handle { color: var(--text-faint); font-size: 0.85rem; }
.user-card-badges { display: flex; gap: 0.35rem; }
.user-card-badges:empty { display: none; }
.user-card-status { font-style: italic; color: var(--text-dim); }
.user-card-bio { align-self: stretch; text-align: left; white-space: pre-wrap; word-break: break-word; background: var(--bg-3); border-radius: var(--radius); padding: 0.6rem 0.7rem; margin-top: 0.25rem; line-height: 1.45; }
.user-card-since { min-height: 0; margin: 0.25rem 0 0; }
.user-card-note-label { align-self: stretch; text-align: left; font-size: 0.78rem; color: var(--text-faint); margin-top: 0.5rem; display: flex; flex-direction: column; gap: 0.3rem; }
.user-card-note { width: 100%; resize: vertical; font-size: 0.85rem; background: var(--bg-3); border: 1px solid var(--border); border-radius: var(--radius); padding: 0.5rem 0.6rem; color: var(--text); font-family: inherit; line-height: 1.4; }
.msg-time { color: var(--text-faint); font-size: 0.72rem; text-decoration: none; }
.msg-time:hover { text-decoration: underline; }
@keyframes anchor-flash { 0% { background: rgba(224, 164, 88, 0.28); } 100% { background: transparent; } }
.msg-anchor { animation: anchor-flash 2.5s ease-out forwards; border-radius: 4px; }
.history-banner { padding: 0.3rem 1.2rem; background: var(--bg-3); border-bottom: 1px solid var(--border); font-size: 0.82rem; color: var(--text-dim); }
.reply-quote { display: flex; align-items: baseline; gap: 0.3rem; font-size: 0.78rem; color: var(--text-dim); cursor: pointer; margin-bottom: 0.12rem; max-width: 100%; }
.reply-quote:hover .reply-quote-text { text-decoration: underline; }
.reply-quote-arrow { color: var(--text-faint); flex-shrink: 0; }
.reply-quote-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.reply-quote-text.deleted { font-style: italic; color: var(--text-faint); }
.reply-quote-author { font-weight: 600; }
.msg-body { word-wrap: break-word; overflow-wrap: anywhere; }
.msg-body.deleted { color: var(--text-faint); font-style: italic; }
.msg.deleted-run { padding: 0.05rem 0; }
.msg.deleted-run .msg-body.deleted { font-size: 0.8rem; }
.msg-body .edited { color: var(--text-faint); font-size: 0.72rem; }
.msg.msg-system { display: flex; align-items: center; justify-content: center; gap: 0.5rem; padding: 0.2rem 0; }
.msg-system-text { color: var(--text-faint); font-size: 0.8rem; font-style: italic; }
.msg-system-time { color: var(--text-faint); font-size: 0.72rem; opacity: 0.7; }
.msg-body a { color: var(--accent); }
/* The 360px cap lives on the shrink-to-fit wrappers, not the <img>: Firefox
   ignores a descendant's min(<length>, <percentage>) max-width while sizing a
   shrink-to-fit ancestor, so a cap on the image left the link/spoiler box wider
   than the image. A bare percentage on the image is sized correctly. */
.msg-body a.msg-image-link { display: inline-block; max-width: min(360px, 100%); }
.msg-body .spoiler:has(a.msg-image-link) { max-width: min(360px, 100%); }
.msg-body img.msg-image { display: inline-block; vertical-align: top; max-width: 100%; max-height: 280px; width: auto; height: auto; margin: 0.3rem 0; border-radius: var(--radius); background: var(--bg-3); object-fit: contain; }
.msg-body code { background: var(--bg-3); padding: 0.05rem 0.3rem; border-radius: 4px; font-family: var(--mono); font-size: 0.85em; }
.msg-body .spoiler { background: var(--text-faint); color: transparent; border-radius: 3px; padding: 0 3px; cursor: pointer; user-select: none; display: inline-block; transition: background 0.15s, color 0.15s; }
.msg-body .spoiler.revealed { background: var(--bg-3); color: var(--text); user-select: auto; }
.msg-body .spoiler:not(.revealed) img.msg-image { filter: blur(16px); }
.msg-body .spoiler:not(.revealed) a { color: transparent; }
/* color:transparent hides plain text but not emoji — custom emoji are <img> and
   Unicode emoji are color-font glyphs that ignore `color`. Hide both behind the
   bar while preserving layout so the spoiler keeps its width. */
.msg-body .spoiler:not(.revealed) img.emoji,
.msg-body .spoiler:not(.revealed) .emoji-uni { opacity: 0; }
/* The wrapper is the non-scrolling positioning context for the language label;
   the inner <pre> owns horizontal scroll, so the label stays pinned to the frame. */
.msg-body .code-block-wrap { position: relative; }
.msg-body pre.code-block { background: var(--bg-3); padding: 0.7rem 0.9rem; border-radius: var(--radius); overflow-x: auto; line-height: 1; }
.msg-body pre.code-block code { background: none; padding: 0; }
/* Language label shown top-right when a language hint is present. */
.msg-body .code-block-wrap[data-lang]::after { content: attr(data-lang); position: absolute; top: 0.3rem; right: 0.6rem; font-size: 0.68rem; color: var(--text-faint); font-family: var(--mono); pointer-events: none; }
/* Syntax highlight token colors — dark themes share One Dark–inspired palette. */
.msg-body pre.code-block .hl-kw { color: #c678dd; }
.msg-body pre.code-block .hl-str { color: #98c379; }
.msg-body pre.code-block .hl-cm { color: #6b7280; font-style: italic; }
.msg-body pre.code-block .hl-num { color: #d19a66; }
.msg-body pre.code-block .hl-fn  { color: #61afef; }
.msg-body pre.code-block .hl-bi  { color: #e5c07b; }
/* diff/patch line tokens: added (green), removed (red), hunk header (cyan). */
.msg-body pre.code-block .hl-ins  { color: #98c379; }
.msg-body pre.code-block .hl-del  { color: #e06c75; }
.msg-body pre.code-block .hl-hunk { color: #56b6c2; }
/* Light theme overrides. */
html[data-theme="light"] .msg-body pre.code-block .hl-kw { color: #7c3aed; }
html[data-theme="light"] .msg-body pre.code-block .hl-str { color: #16a34a; }
html[data-theme="light"] .msg-body pre.code-block .hl-cm { color: #6b7280; }
html[data-theme="light"] .msg-body pre.code-block .hl-num { color: #c2410c; }
html[data-theme="light"] .msg-body pre.code-block .hl-fn  { color: #1d4ed8; }
html[data-theme="light"] .msg-body pre.code-block .hl-bi  { color: #92400e; }
html[data-theme="light"] .msg-body pre.code-block .hl-ins  { color: #16a34a; }
html[data-theme="light"] .msg-body pre.code-block .hl-del  { color: #dc2626; }
html[data-theme="light"] .msg-body pre.code-block .hl-hunk { color: #0891b2; }
.msg-body blockquote { border-left: 3px solid var(--border); margin: 0.2rem 0; padding-left: 0.7rem; color: var(--text-dim); }
.msg-body table.md-table { border-collapse: collapse; margin: 0.3rem 0; max-width: 100%; font-size: 0.95em; }
.msg-body table.md-table th, .msg-body table.md-table td { border: 1px solid var(--border); padding: 0.3rem 0.55rem; text-align: left; vertical-align: top; }
.msg-body table.md-table th { background: var(--bg-3); font-weight: 700; }
.msg-body table.md-table tbody tr:nth-child(even) td { background: var(--bg-2); }
.msg-body h3, .msg-body h4, .msg-body h5 { margin: 0.15rem 0; font-weight: 700; line-height: 1.4; }
.msg-body h3 { font-size: 1.15em; }
.msg-body h4 { font-size: 1.05em; }
.msg-body h5 { font-size: 0.95em; }
.msg-body ul { margin: 0.2rem 0; padding-left: 1.4rem; list-style: disc; }
.msg-body ul li { margin: 0.1rem 0; }
/* Inline message embed card (same-origin permalink dropped as a bare URL). */
.msg-embed { margin: 0.35rem 0 0.1rem; padding: 0.45rem 0.65rem; border: 1px solid var(--border); border-left: 3px solid var(--accent-dim); border-radius: var(--radius); background: var(--bg-2); cursor: pointer; max-width: 460px; }
.msg-embed:hover { background: var(--bg-3); }
.msg-embed-head { display: flex; gap: 0.45rem; align-items: baseline; margin-bottom: 0.18rem; }
.msg-embed-author { font-weight: 600; font-size: 0.875rem; color: var(--text); }
.msg-embed-time { font-size: 0.75rem; color: var(--text-dim); }
/* Normal block flow (no line-clamp) so a forwarded/uploaded blob image renders
   inline; messages are short enough that the full quote is fine. overflow-wrap
   keeps a long unbroken token from spilling past the card edge. */
.msg-embed-body { font-size: 0.875rem; color: var(--text); overflow-wrap: anywhere; }
/* The .msg-body image caps don't reach inside an embed card, so size them here.
   A card image is a preview — cap it SMALLER than an inline message image (180 vs
   280px); clicking the card jumps to the original, where it renders full inline. */
.msg-embed-body a.msg-image-link { display: inline-block; max-width: 100%; }
.msg-embed-body img.msg-image { display: inline-block; vertical-align: top; max-width: 100%; max-height: 180px; width: auto; height: auto; margin: 0.3rem 0 0; border-radius: var(--radius); object-fit: contain; }

/* External link preview card (og: meta-tags from allowlisted domains). */
.link-preview { display: flex; gap: 10px; align-items: flex-start; border: 1px solid var(--border); border-left: 3px solid var(--accent-dim); border-radius: var(--radius); padding: 8px 10px; margin: 0.35rem 0 0.1rem; max-width: min(460px, 100%); overflow: hidden; text-decoration: none; color: inherit; background: var(--bg-2); }
.link-preview:hover { background: var(--bg-3); }
.link-preview-text { flex: 1; min-width: 0; }
.link-preview-site { font-size: 0.72em; color: var(--text-dim); margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.link-preview-title { font-weight: 600; font-size: 0.875rem; color: var(--text); word-break: break-word; }
.link-preview-desc { font-size: 0.8em; color: var(--text-dim); margin-top: 2px; display: -webkit-box; -webkit-line-clamp: 6; -webkit-box-orient: vertical; overflow: hidden; }
.link-preview-image { width: 72px; height: 72px; object-fit: cover; border-radius: calc(var(--radius) - 1px); flex-shrink: 0; }

.yt-thumb { display: block; position: relative; max-width: 480px; margin: 0.4rem 0 0.1rem; border-radius: var(--radius); overflow: hidden; text-decoration: none; }
.yt-thumb img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; background: var(--bg-3); }
.yt-play { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 2.5rem; color: #fff; text-shadow: 0 0 12px rgba(0,0,0,.8); background: rgba(0,0,0,.15); transition: background .15s; }
.yt-thumb:hover .yt-play { background: rgba(0,0,0,.32); }

/* Author-only "remove embed" affordance: a light × that wraps the URL in <> so the
   card/image collapses to a plain link. The button is appended INTO the embed
   element itself (the preview card, or a bare-URL image's anchor), which becomes its
   own .embed-host positioning box — so the embed's outer margin sits outside the
   border box and can't push the × up into the gap above it. On an image anchor the
   img's own margin is moved out to the anchor for the same reason. Revealed on ROW
   hover (like .msg-actions) — keying the reveal on the button's own host would
   deadlock (a pointer-events:none control can't receive the hover that enables it).
   Hidden on touch (the long-press sheet carries the action instead). */
.embed-host { position: relative; }
.msg-body a.msg-image-link.embed-host { margin: 0.3rem 0; }
.msg-body a.msg-image-link.embed-host > img.msg-image { margin: 0; }
.embed-remove {
  position: absolute; top: 6px; right: 6px; z-index: 2;
  width: 20px; height: 20px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: none; border-radius: 50%;
  background: rgba(0, 0, 0, 0.55); color: #fff;
  font-size: 0.95rem; line-height: 1; cursor: pointer;
  visibility: hidden; opacity: 0; pointer-events: none; transition: opacity 0.12s;
}
.msg:hover .embed-remove { visibility: visible; opacity: 1; pointer-events: auto; }
.embed-remove:hover { background: rgba(0, 0, 0, 0.82); }
.msg-actions { position: absolute; top: 0.15rem; right: 0; display: flex; gap: 0.3rem; padding: 0 0.2rem 0 0.8rem; background: linear-gradient(to right, transparent, var(--bg-2) 30%); visibility: hidden; pointer-events: none; }
.msg-act { background: none; border: none; padding: 0 0.05rem; cursor: pointer; font-size: 1rem; line-height: 1; color: var(--text-dim); opacity: 0.75; transition: opacity .1s; }
.msg-act:hover { opacity: 1; }
.msg-act.danger:hover { opacity: 1; }
.msg:hover { background: var(--bg-hover); border-radius: 6px; }
.msg:hover .msg-actions { visibility: visible; animation: msg-actions-activate 0s 300ms forwards; background: linear-gradient(to right, transparent, var(--bg-hover) 30%); }
@keyframes msg-actions-activate { to { pointer-events: auto; } }
/* Inline message editor (replaces the body while editing your own message). */
.msg-edit { margin-top: 0.15rem; position: relative; }
.msg-edit-input { width: 100%; resize: none; max-height: 200px; line-height: 1.4; padding: 0.5rem 0.6rem; background: var(--bg-3); border: 1px solid var(--accent-dim); border-radius: var(--radius); color: var(--text); font: inherit; }
.msg-edit-input:focus { outline: none; border-color: var(--accent); }
/* The autocomplete popup anchors to the edit box itself, not the composer. */
.msg-edit .mention-popup { left: 0; right: 0; }
.msg-edit-controls { display: flex; align-items: center; gap: 0.6rem; margin-top: 0.3rem; }
.msg-edit-emoji { background: none; border: none; padding: 0; cursor: pointer; font-size: 1rem; line-height: 1; opacity: 0.7; }
.msg-edit-emoji:hover { opacity: 1; }
.msg-edit-hint { color: var(--text-faint); font-size: 0.72rem; }
/* "New messages" divider — rendered before the first unread message */
.unread-marker { display: flex; align-items: center; gap: 0.6rem; margin: 0.35rem 0.5rem; pointer-events: none; }
.unread-marker::before, .unread-marker::after { content: ""; flex: 1; height: 1px; background: var(--accent); opacity: 0.35; }
.unread-marker-label { color: var(--accent); font-size: 0.68rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; white-space: nowrap; opacity: 0.85; }
.msg.pinned { background: rgba(110, 168, 254, 0.06); border-radius: 6px; }
.msg.pinned .msg-actions { background: linear-gradient(to right, transparent, var(--bg-2) 30%); }
.msg.mentioned { background: rgba(224, 164, 88, 0.09); border-radius: 6px; box-shadow: inset 2px 0 0 var(--away); }
.msg.mentioned .msg-actions { background: linear-gradient(to right, transparent, var(--bg-2) 30%); }
.mention { color: var(--accent); background: rgba(110, 168, 254, 0.14); border-radius: 4px; padding: 0 0.15em; }
.mention.mention-me { color: var(--text); background: rgba(224, 164, 88, 0.28); }
a.channel-link { color: var(--accent); background: rgba(110, 168, 254, 0.14); border-radius: 4px; padding: 0 0.15em; text-decoration: none; cursor: pointer; }
a.channel-link:hover { background: rgba(110, 168, 254, 0.25); text-decoration: underline; }
.pin-mark { font-size: 0.7rem; opacity: 0.85; }
.msg.grouped .msg-gutter .pin-mark { display: block; text-align: center; }

/* Reaction pills under a message. Persistent (unlike the hover-only actions). */
.reactions { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.3rem; }
.reaction { display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.05rem 0.45rem; min-height: 1.5rem; border: 1px solid var(--border); border-radius: 999px; background: var(--bg-3); color: var(--text-dim); cursor: pointer; font-size: 0.85rem; line-height: 1.3; }
.reaction:hover { background: var(--bg-hover); }
.reaction.mine { border-color: var(--accent-dim); background: rgba(110, 168, 254, 0.16); color: var(--text); }
.reaction .r-emoji { font-size: 1rem; line-height: 1; }
.reaction img.emoji { height: 1.1rem; max-width: 1.6rem; vertical-align: middle; }
.reaction .r-count { font-variant-numeric: tabular-nums; }
.reaction.orphan { opacity: 0.55; border-style: dashed; }
.reaction.orphan[disabled] { cursor: default; }
.reaction.orphan:not([disabled]):hover { background: var(--bg-hover); }

.typing-indicator { padding: 0.2rem 1.2rem 0.1rem; font-size: 0.8rem; color: var(--text-dim); font-style: italic; min-height: 1.4em; }
.composer { position: relative; padding: 0.7rem 1.2rem 1rem; border-top: 1px solid var(--border); }
.composer-reply { display: flex; align-items: center; gap: 0.5rem; padding: 0.3rem 0.6rem; margin-bottom: 0.5rem; background: var(--bg-3); border-left: 3px solid var(--accent); border-radius: 4px; font-size: 0.8rem; color: var(--text-dim); }
.composer-reply-who { font-weight: 600; color: var(--text); }
.composer-reply-snippet { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text-faint); }
.composer-reply-cancel { margin-left: auto; background: none; border: none; color: var(--text-dim); cursor: pointer; font-size: 0.9rem; padding: 0 0.2rem; line-height: 1; }
.composer-reply-cancel:hover { color: var(--text); }
/* The composer is a contenteditable div (image paste — see index.html). It
   auto-grows as a normal block between min/max-height; no JS sizing. Integer
   line-height + integer vertical padding keep heights whole-pixel so the caret
   never scrolls into sub-pixel slack (the old textarea "jiggle"). */
#composer-input {
  position: relative;            /* anchors the :empty placeholder below */
  min-height: 38px;              /* single line: 20px text + 16px padding + 2px border */
  max-height: 180px;
  overflow-y: auto;
  white-space: pre-wrap;         /* don't collapse spaces/newlines */
  overflow-wrap: anywhere;       /* long unbroken tokens wrap, not overflow */
  cursor: text;
  line-height: 20px; padding-top: 8px; padding-bottom: 8px; padding-right: 4.5rem;
}
/* contenteditable has no placeholder attribute; paint data-ph when empty.
   position:absolute keeps it out of flow so the caret sits at the true start
   (in flow it would push the caret/typed text rightward). */
#composer-input:empty::before {
  content: attr(data-ph);
  position: absolute;
  pointer-events: none;
  color: var(--text-faint);
}
/* Locked state (ended secret session) — the facade's `disabled` setter
   toggles this class; :disabled can't match a contenteditable div. */
#composer-input.disabled {
  opacity: 0.6;
  cursor: default;
}
/* Live markdown decoration (composer-richtext.js). The markers stay in the text
   for predictable caret behavior — .md-mk just dims them — while the delimited
   run gets its effect. Unlike the rendered message, the composer's spoiler is
   NOT blacked out (you're editing it), only faintly tinted. */
#composer-input .md-mk { opacity: 0.4; }
#composer-input .md-strong { font-weight: 700; }
#composer-input .md-em { font-style: italic; }
#composer-input .md-del { text-decoration: line-through; }
#composer-input .md-code { font-family: var(--mono); font-size: 0.92em; }
#composer-input .md-spoiler { background: var(--bg-3); border-radius: 3px; }
/* Fenced code blocks: each line is a monospace strip on the code background
   (markers/newlines kept, so the value stays byte-identical). The ``` fences
   ride the same dimmed .md-mk as every other marker; the language hint is faint. */
#composer-input .md-codeblock { font-family: var(--mono); font-size: 0.92em; background: var(--bg-3); border-radius: 3px; }
#composer-input .md-cb-lang { color: var(--text-faint); }
/* @mention / #channel / :emoji: tinting — echoes the rendered message's accent
   without the heavier pill backgrounds (the composer stays calm). Only REAL
   tokens are tinted (validated against live state in richContext). */
#composer-input .md-mention,
#composer-input .md-channel,
#composer-input .md-emoji { color: var(--accent); }
#composer-input .md-mention-me { color: var(--accent); font-weight: 600; }
/* The emoji + attach buttons sit on the composer's bottom-right (Enter sends, so
   the right edge is otherwise unused); the picker pops above like the mentions.
   Two buttons, so they're laid out side by side — emoji rightmost, attach to its
   left — and the composer's right padding clears both. */
.emoji-btn { position: absolute; right: 1.5rem; bottom: 1.15rem; width: 30px; height: 30px; padding: 0; border: none; background: transparent; cursor: pointer; font-size: 1.1rem; line-height: 1; opacity: 0.65; }
#attach-btn { right: 3.4rem; }
.emoji-btn:hover { opacity: 1; }
/* Pending-upload tray: image previews sit above the composer. Each tile shows the
   image with an X to remove; a spinner overlays it until the upload resolves. */
.composer-attachments { display: flex; flex-wrap: wrap; gap: 0.5rem; padding: 0 0 0.55rem; }
.attachment { position: relative; width: 72px; height: 72px; border-radius: var(--radius); overflow: hidden; border: 1px solid var(--border); background: var(--bg-3); cursor: pointer; }
.attachment img { width: 100%; height: 100%; object-fit: cover; display: block; }
.attachment.uploading { cursor: default; }
.attachment.uploading img { opacity: 0.45; }
.attachment.copied::after { content: "Copied"; position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 600; color: #fff; background: rgba(0,0,0,.55); }
.attachment-remove { position: absolute; top: 2px; right: 2px; width: 20px; height: 20px; padding: 0; display: flex; align-items: center; justify-content: center; border: none; border-radius: 50%; background: rgba(0,0,0,.6); color: #fff; font-size: 1rem; line-height: 1; cursor: pointer; }
.attachment-remove:hover { background: rgba(0,0,0,.85); }
.attachment-spoiler-btn { position: absolute; bottom: 2px; left: 2px; height: 18px; padding: 0 5px; display: flex; align-items: center; justify-content: center; border: none; border-radius: 3px; background: rgba(0,0,0,.6); color: rgba(255,255,255,.7); font-size: 0.6rem; font-weight: 700; letter-spacing: 0.04em; line-height: 1; cursor: pointer; }
.attachment-spoiler-btn:hover { background: rgba(0,0,0,.85); color: #fff; }
.attachment-spoiler-btn.active { background: rgba(255,255,255,.25); color: #fff; }
.attachment.spoiler-marked img { filter: blur(6px); }
.attachment-spinner { position: absolute; top: 50%; left: 50%; width: 22px; height: 22px; margin: -11px 0 0 -11px; border: 2px solid rgba(255,255,255,.35); border-top-color: #fff; border-radius: 50%; animation: attachment-spin 0.7s linear infinite; }
@keyframes attachment-spin { to { transform: rotate(360deg); } }
.emoji-wrap { position: absolute; bottom: 100%; right: 1.2rem; margin: 0 0 3px; width: 239px; background: var(--bg-3); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: 0 -4px 16px rgba(0,0,0,.45); z-index: 10; display: flex; flex-direction: column; }
.emoji-picker { padding: 0.4rem; max-height: 240px; overflow-y: auto; display: flex; flex-wrap: wrap; gap: 0.25rem; }
.emoji-choice { width: 34px; height: 34px; display: flex; align-items: center; justify-content: center; padding: 3px; border: none; border-radius: 6px; background: transparent; cursor: pointer; touch-action: manipulation; }
.emoji-choice:hover { background: var(--bg-hover); }
/* Keyboard highlight (aria-activedescendant): ringed so it reads distinctly from hover. */
.emoji-choice[aria-selected="true"] { background: var(--bg-hover); outline: 2px solid var(--accent); outline-offset: -2px; }
.emoji-choice img { height: 24px; width: 24px; object-fit: contain; }
.emoji-choice .emoji-uni { font-size: 1.3rem; line-height: 1; }
.emoji-empty { flex-basis: 100%; color: var(--text-dim); font-size: 0.85rem; padding: 0.4rem; }
/* Search field pinned above the scrolling grid; filters by shortcode across sections. */
.emoji-search { margin: 0; padding: 0.45rem 0.55rem; border: none; border-bottom: 1px solid var(--border); border-radius: var(--radius) var(--radius) 0 0; background: var(--bg-2); color: var(--text); font-size: 0.85rem; outline: none; }
.emoji-search::placeholder { color: var(--text-dim); }
.emoji-search:focus { background: var(--bg-3); }
/* Full-width dim header labelling each grid section (Recent / Emoji / Results / Custom). */
.emoji-section { flex-basis: 100%; margin: 0.15rem 0.1rem 0; color: var(--text-dim); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em; }
.emoji-section:first-child { margin-top: 0; }
/* Moderator+ "manage custom emojis" affordance — a footer strip below the emoji grid. */
.emoji-manage-btn { width: 100%; display: flex; align-items: center; justify-content: flex-end; gap: 0.3rem; padding: 0.3rem 0.5rem; border: none; border-top: 1px solid var(--border); border-radius: 0 0 var(--radius) var(--radius); background: var(--bg-2); color: var(--text-dim); cursor: pointer; font-size: 0.8rem; touch-action: manipulation; }
.emoji-manage-btn:hover { background: var(--bg-hover); color: var(--text); }
/* Inline custom emoji inside a message: scale to the line, nudged to sit on the baseline. */
img.emoji { height: 1.4em; width: auto; max-width: 2.4em; object-fit: contain; vertical-align: -0.3em; }
.mention-popup { position: absolute; bottom: 100%; left: 1.2rem; right: 1.2rem; margin: 0 0 3px; padding: 0.25rem 0; list-style: none; background: var(--bg-3); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: 0 -4px 16px rgba(0,0,0,.45); max-height: 240px; overflow-y: auto; z-index: 10; }
.mention-item { padding: 0.35rem 0.8rem; cursor: pointer; display: flex; align-items: center; gap: 0.5em; white-space: nowrap; overflow: hidden; touch-action: manipulation; }
.mention-item img.emoji { height: 20px; width: 20px; max-width: 20px; object-fit: contain; vertical-align: middle; flex-shrink: 0; }
.mention-item:hover, .mention-item.active { background: var(--bg-hover); }
.mention-item-name { flex-shrink: 0; }
.mention-item-display { color: var(--text-dim); font-size: 0.85em; overflow: hidden; text-overflow: ellipsis; }

/* --- members --- */
.members { background: var(--bg-2); border-left: 1px solid var(--border); padding: 1rem; overflow-y: auto; }
/* DMs are 1:1 — no roster to show. Hide the members toggle at every width; on
   desktop also collapse the members column so it leaves no empty gap. */
body.dm-active #members-toggle { display: none; }
@media (min-width: 721px) {
  body.dm-active .app { grid-template-columns: 240px 1fr 0; }
  body.dm-active .members { display: none; }
}
.members-head { display: flex; align-items: center; justify-content: space-between; }
.member-list { list-style: none; margin: 0.6rem 0 0; padding: 0; }
/* All rows share the same negative margin + padding so the clickable hover
   background can bleed to the panel edge without shifting content — the
   self row (non-clickable) stays aligned with the others. */
.member { display: flex; align-items: center; gap: 0.45rem; padding: 0.25rem 0.4rem; margin: 0 -0.4rem; border-radius: var(--radius); }
.member.clickable { cursor: pointer; }
.member.clickable:hover { background: var(--bg-hover); }
.dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.dot.online { background: var(--online); }
.dot.away { background: var(--away); }
.dot.dnd { background: var(--dnd); }
.dot.offline { background: var(--text-faint); }
.member-text { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.2; }
.member-name { font-size: 0.86rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.member-status { color: var(--text-faint); font-size: 0.68rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The mod remove (✕) control stays out of the way until you hover the row. */
.member .ch-ctl { display: none; flex-shrink: 0; }
.member:hover .ch-ctl { display: inline; }

/* About dialog (opened from the sidebar brand). */
#about-btn { cursor: pointer; }
#about-btn:hover { color: var(--accent); }
.about-body { text-align: center; padding: 0.6rem 0 0.2rem; }
.about-body .brand { font-size: 1.8rem; }
.about-body .hint { text-align: center; }
.about-blurb { color: var(--text-dim); font-size: 0.85rem; margin: 0.4rem 0 0; }

.notice { color: var(--text-dim); font-size: 0.85rem; padding: 0.4rem 0; }

/* --- modal --- */
.modal { position: fixed; inset: 0; z-index: 60; background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; padding: 1rem; }
.modal-card { background: var(--bg-2); border: 1px solid var(--border); border-radius: 12px; width: 100%; max-width: 640px; max-height: 85vh; overflow-y: auto; padding: 1.4rem 1.6rem; }
.modal-head { display: flex; align-items: center; justify-content: space-between; }
.modal-card h2 { margin: 0; }
.modal-card h3 { margin: 1.4rem 0 0.6rem; font-size: 0.95rem; color: var(--text-dim); }
.admin-stats { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1rem; }
.admin-stat { display: flex; flex-direction: column; align-items: center; min-width: 4.5rem; padding: 0.5rem 0.75rem; background: var(--bg-3); border-radius: var(--radius); }
.admin-stat-value { font-size: 1.25rem; font-weight: 600; line-height: 1.2; }
.admin-stat-label { font-size: 0.72rem; color: var(--text-faint); margin-top: 0.15rem; }

.admin-create { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.admin-create input, .admin-create select { width: auto; flex: 1; min-width: 120px; margin: 0; }
.admin-create button.primary { width: auto; }
.admin-out { margin-top: 0.6rem; }
.admin-hint { font-size: 0.8rem; color: var(--text-faint); margin: 0 0 0.6rem; }
.linkbox { width: 100%; margin-top: 0.4rem; padding: 0.5rem; background: var(--bg-3); border: 1px solid var(--accent-dim); border-radius: var(--radius); color: var(--text); font-family: var(--mono); font-size: 0.8rem; }
.admin-table { width: 100%; border-collapse: collapse; font-size: 0.82rem; margin-top: 0.4rem; }
.admin-table th { text-align: left; color: var(--text-faint); font-weight: 600; padding: 0.3rem 0.5rem; border-bottom: 1px solid var(--border); }
.admin-table td { padding: 0.35rem 0.5rem; border-bottom: 1px solid var(--border); }
.admin-table select { width: auto; margin: 0; padding: 0.2rem; }
.bot-badge { display: inline-block; font-size: 0.68rem; font-weight: 600; padding: 0.1em 0.4em; border-radius: 3px; background: var(--bg-3); border: 1px solid var(--border); color: var(--text-faint); vertical-align: middle; }
.admin-emoji-list { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; }
.admin-emoji { display: flex; align-items: center; gap: 0.4rem; padding: 0.3rem 0.5rem; background: var(--bg-3); border: 1px solid var(--border); border-radius: var(--radius); font-size: 0.82rem; }
.admin-emoji img { height: 22px; width: 22px; object-fit: contain; }
.admin-emoji code { font-family: var(--mono); color: var(--text-dim); }

/* --- admin panel ---
   Admin outgrew its modal: it's now a full-screen surface that takes over the
   conversation area (sidebar stays visible on desktop so you can click back to a
   channel, which dismisses it). Covers the whole viewport on mobile. */
.admin-panel { position: fixed; inset: 0; z-index: 40; background: var(--bg); display: flex; flex-direction: column; }
.admin-panel-head { display: flex; align-items: center; justify-content: space-between; padding: 0.9rem 1.4rem; border-bottom: 1px solid var(--border); flex-shrink: 0; }
.admin-panel-head h2 { margin: 0; font-size: 1.1rem; }
.admin-panel-body { flex: 1; min-height: 0; overflow-y: auto; padding: 1.4rem; }
.admin-panel-body .admin-stats { max-width: 1100px; margin-left: auto; margin-right: auto; }
/* Cards adapt to width: forms sit side-by-side on a wide screen, stack on narrow;
   the big tables (.admin-section-wide) always span the full row. */
.admin-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1rem; max-width: 1100px; margin: 0 auto; align-items: start; }
.admin-section { background: var(--bg-2); border: 1px solid var(--border); border-radius: 12px; padding: 1.1rem 1.3rem; min-width: 0; }
.admin-section-wide { grid-column: 1 / -1; overflow-x: auto; }
.admin-section h3 { margin: 0 0 0.7rem; font-size: 0.95rem; color: var(--text-dim); }
/* On desktop the sidebar grid column stays put; the panel starts at its edge so
   admin reads like a conversation devoted to settings rather than a hard takeover. */
@media (min-width: 721px) { .admin-panel { left: 240px; } }

/* Stacked modal forms (new channel, edit profile) reuse the shared
   `form label`/`form input` rules; only the private-toggle row needs its own
   (inline, non-stretched) layout. */
.modal-card.narrow { max-width: 380px; }
/* Image lightbox: the picked image fills most of the viewport, centred on the
   dark .modal backdrop. The image itself doesn't dismiss (only backdrop/Esc/×),
   so the cursor is a zoom-out hint over the backdrop only. */
.lightbox { padding: 2.5rem; cursor: zoom-out; }
.lightbox-img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; border-radius: var(--radius); cursor: default; box-shadow: 0 8px 40px rgba(0,0,0,0.6); }
.lightbox-close { position: fixed; top: 1rem; right: 1rem; width: 36px; height: 36px; font-size: 1.4rem; cursor: pointer; }
/* Gallery prev/next: vertically-centred arrows pinned to the viewport edges,
   over the backdrop. Hidden for a lone image (toggled in showLightboxAt). */
.lightbox-nav { position: fixed; top: 50%; transform: translateY(-50%); width: 44px; height: 44px; font-size: 1.8rem; line-height: 1; cursor: pointer; }
.lightbox-prev { left: 1rem; }
.lightbox-next { right: 1rem; }
/* The pinned-messages list is cramped at the narrow width; give it more room on
   wider screens (.modal-card is width:100%, so phones are unaffected). */
#pins-modal .modal-card { max-width: 560px; }
/* Search results (message snippets) need room to read on desktop; the narrow
   width truncates every row. .modal-card is width:100%, so phones are unaffected. */
#search-modal .modal-card { max-width: 640px; }
/* The custom-emoji manager's shortcode/file/add row plus the emoji grid are
   cramped at the narrow width; widen on desktop. .modal-card is width:100%, so
   phones are unaffected. */
#emoji-manager-modal .modal-card { max-width: 560px; }
/* The profile editor is the tallest modal; its full form sits a few px over the
   shared 85vh ceiling on mid-height phones, producing a sliver of dead scroll past
   Save. Give just this card a touch more headroom so it fits without scrolling. */
#profile-modal .modal-card { max-height: 90vh; }
.modal-form .optional { color: var(--text-faint); }
.modal-form .checkbox-row { display: flex; align-items: center; gap: 0.5rem; margin: 0.2rem 0 0.9rem; cursor: pointer; color: var(--text); }
.modal-form .checkbox-row input { width: auto; margin: 0; }

.invite-list { list-style: none; margin: 0.6rem 0 0; padding: 0; max-height: 50vh; overflow-y: auto; }
/* Forward picker rows are clickable; give them a hover cue like search results. */
#forward-filter { width: 100%; box-sizing: border-box; margin: 0.6rem 0 0; }
.invite-item { cursor: pointer; padding: 0.4rem 0.5rem; border-radius: var(--radius); }
.invite-item:hover { background: var(--surface-2, rgba(127,127,127,0.08)); }
/* Keyboard-highlighted forward target (arrow keys); mirrors the emoji picker cue. */
#forward-list .invite-item[aria-selected="true"] { background: var(--bg-hover); outline: 2px solid var(--accent); outline-offset: -2px; }
.invite-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.35rem 0; border-bottom: 1px solid var(--border); }
.invite-row .member-name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.invite-in { color: var(--text-faint); font-size: 0.78rem; }

.pins-list { list-style: none; margin: 0.6rem 0 0; padding: 0; max-height: 60vh; overflow-y: auto; }
.pin-row { padding: 0.5rem 0; border-bottom: 1px solid var(--border); }
.pin-row:last-child { border-bottom: none; }
.pin-head { display: flex; align-items: baseline; gap: 0.5rem; margin-bottom: 0.2rem; }
.pin-head .msg-time { margin-right: auto; }

/* Search results reuse the pin-row layout but are clickable and put the channel
   name first with the timestamp pushed to the right. */
#search-input { width: 100%; box-sizing: border-box; }
.search-row { cursor: pointer; }
.search-row:hover { background: var(--surface-2, rgba(127,127,127,0.08)); }
.search-row .search-channel { font-weight: 600; }
.search-row .msg-time { margin-right: 0; margin-left: auto; }
#search-more { display: block; margin: 0.5rem auto 0; }

/* --- voice calling ----------------------------------------------------- */

.ring-banner {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.5rem 1rem;
  background: var(--surface-2, rgba(127,127,127,0.12));
  border-bottom: 1px solid var(--border);
  font-size: 0.9rem;
  color: var(--text);
  flex-shrink: 0;
  flex-wrap: wrap;
}
.ring-banner #ring-banner-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ring-banner #secret-banner-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.secret-banner { background: color-mix(in srgb, var(--accent) 8%, var(--bg-2)); border-color: color-mix(in srgb, var(--accent) 30%, var(--border)); }
button.primary.small { padding: 0.2rem 0.7rem; font-size: 0.82rem; width: auto; flex-shrink: 0; }

/* Secret session styles */
.secret-header { display: flex; align-items: center; justify-content: center; gap: 0.5rem; padding: 0.6rem 1rem; font-size: 0.82rem; color: var(--text-dim); border-bottom: 1px solid var(--border); flex-shrink: 0; cursor: pointer; }
.secret-header:hover { color: var(--text); }
.secret-header.verified { color: #4caf50; }
.secret-header.verified:hover { color: #81c784; }
.msg.secret { opacity: 0.96; }
.secret-btn-active { opacity: 1; }
.secret-btn-unverified { color: var(--text-dim); }
.secret-btn-verified { color: #4caf50; }
.secret-req-badge { display: inline-block; margin-left: 4px; line-height: 1; animation: lock-jiggle 0.45s ease-in-out infinite alternate; transform-origin: center bottom; }
.secret-ended-notice { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; padding: 1rem; margin-top: 0.5rem; font-size: 0.85rem; color: var(--text-dim); border-top: 1px solid var(--border); text-align: center; }
@keyframes lock-jiggle { 0% { transform: rotate(-18deg); } 100% { transform: rotate(18deg); } }

/* Safety number modal */
.safety-body { padding: 0.6rem 0; }
.safety-number { font-family: monospace; font-size: 1.05rem; letter-spacing: 0.08em; text-align: center; padding: 0.9rem 0; color: var(--text); line-height: 1.9; word-break: break-all; }
.safety-status { text-align: center; font-size: 0.9rem; margin: 0.3rem 0 0.6rem; }
.safety-status.verified { color: #4caf50; }
.safety-meta { margin-top: 1rem; font-size: 0.8rem; }

/* ---- video grid (DM video calls, phase 1) -------------------------------- */
.video-grid {
  position: relative;
  background: #000;
  display: flex;
  align-items: stretch;
  flex-shrink: 0;
  height: 240px;
  overflow: hidden;
}
.video-tile {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #111;
  overflow: hidden;
  position: relative;
}
.video-tile video { width: 100%; height: 100%; object-fit: contain; display: block; }
.video-tile-local {
  position: absolute;
  bottom: 0.6rem;
  right: 0.6rem;
  width: 120px;
  height: 90px;
  object-fit: cover;
  border-radius: 4px;
  border: 2px solid rgba(255,255,255,0.25);
  background: #000;
  z-index: 2;
}
/* While sharing the screen, the local PiP shows the whole shared surface rather
   than cropping it to fill (a camera self-view fills; a screen must not lose its
   edges). Remote/group tiles are already object-fit:contain, so only the PiP needs it. */
.video-tile-local.sharing { object-fit: contain; }
.video-avatar {
  display: flex; flex-direction: column; align-items: center; gap: 0.5rem;
}
.video-avatar-img {
  width: 64px; height: 64px; border-radius: 50%; object-fit: cover; background: var(--bg-3);
}
.video-avatar-initials {
  width: 64px; height: 64px; border-radius: 50%;
  background: var(--surface-3, rgba(127,127,127,0.25));
  display: flex; align-items: center; justify-content: center;
  font-size: 1.4rem; font-weight: 700; color: var(--text-dim);
}
.video-avatar-name { font-size: 0.875rem; color: rgba(255,255,255,0.75); }
/* ---- group video gallery (1.4.0) ---------------------------------------- */
/* N-tile responsive grid: auto-fit columns size themselves to the container so
   the tile count drives the column count (2 people → 2 wide, a crowd → wraps).
   A taller frame than the DM strip since several tiles share it. */
.video-grid.group-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  grid-auto-rows: 1fr;
  gap: 2px;
  height: 360px;
}
.video-grid.group-grid .video-tile { border-radius: 4px; }
/* ---- spotlight view (2.0.1) -------------------------------------------------
   Opt-in single-stream focus over the gallery: one big tile (auto-follows the
   active speaker / a screen-share, or a clicked pin) above a thumbnail filmstrip
   of everyone else. Overrides group-grid's CSS grid back to a flex column. */
.video-grid.group-grid.spotlight {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.spotlight-stage { flex: 1; min-height: 0; display: flex; }
.spotlight-stage .video-tile { flex: 1; border-radius: 4px; cursor: pointer; }
.spotlight-strip {
  display: flex; gap: 2px; height: 84px; flex-shrink: 0; overflow-x: auto;
}
.spotlight-strip .video-tile {
  flex: 0 0 auto; width: 120px; border-radius: 4px; cursor: pointer;
}
/* Active-speaker promotion: a glowing ring on the speaker's tile (toggled live
   by onSpeaking, mirroring the member-roster speaking ring). */
.video-tile.speaking { outline: 3px solid var(--accent, #5b8cff); outline-offset: -3px; }
.video-spotlight-btn {
  position: absolute; top: 0.4rem; right: 2.6rem; z-index: 3;
  background: rgba(0,0,0,0.45); border: none; border-radius: var(--radius);
  color: rgba(255,255,255,0.8); font-size: 1rem; padding: 0.15rem 0.4rem;
  cursor: pointer; line-height: 1; opacity: 0;
  transition: opacity 0.15s;
}
.video-grid:hover .video-spotlight-btn { opacity: 1; }
.video-fullscreen-btn {
  position: absolute; top: 0.4rem; right: 0.4rem; z-index: 3;
  background: rgba(0,0,0,0.45); border: none; border-radius: var(--radius);
  color: rgba(255,255,255,0.8); font-size: 1rem; padding: 0.15rem 0.4rem;
  cursor: pointer; line-height: 1; opacity: 0;
  transition: opacity 0.15s;
}
.video-grid:hover .video-fullscreen-btn { opacity: 1; }
@media (max-width: 720px) {
  body.video-active .video-grid { height: auto; flex: 1; min-height: 0; }
  body.video-active .message-list,
  body.video-active #typing-indicator,
  body.video-active .composer { display: none; }
  .video-fullscreen-btn, .video-spotlight-btn { opacity: 0.6; }
}
@media (min-width: 721px) {
  #header-camera-btn { display: none !important; }
}
/* The screen-share button is the desktop counterpart: getDisplayMedia is a
   desktop affordance, so hide it on narrow/touch layouts (mirrors the mobile-only
   #header-camera-btn rule above, inverted). */
@media (max-width: 720px) {
  #header-share-btn { display: none !important; }
}

.call-strip {
  display: flex; align-items: center; gap: 0.3rem;
  padding: 0.35rem 0.6rem;
  background: var(--surface-2, rgba(80,180,80,0.12));
  border-top: 1px solid var(--border);
}
.call-strip-label { flex: 1; min-width: 0; font-size: 0.82rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
.call-ctl {
  display: inline-flex; align-items: center; justify-content: center;
  height: 1.7rem; min-width: 1.7rem; line-height: 1;
  background: none; border: none; border-radius: var(--radius);
  padding: 0 0.35rem; cursor: pointer; font-size: 1rem; color: var(--text-dim);
  transition: background 0.1s;
}
.call-ctl:hover { background: var(--surface-3, rgba(127,127,127,0.15)); color: var(--text); }
.call-ctl.danger { color: var(--red, #e74c3c); }
.call-ctl.danger:hover { background: rgba(231,76,60,0.12); }
.call-ctl.active { color: var(--text); opacity: 0.55; }
/* Push-to-talk pill in the call strip (shown in place of the mute button when
   PTT is on); lights green while the key is held and the mic is transmitting. */
.call-ptt {
  font-size: 0.7rem; font-weight: 600; letter-spacing: 0.02em;
  padding: 0.18rem 0.45rem; border-radius: var(--radius);
  background: var(--surface-3, rgba(127,127,127,0.15)); color: var(--text-dim);
  white-space: nowrap; user-select: none;
}
.call-ptt.active { background: rgba(80,180,80,0.30); color: var(--text); }
/* Push-to-talk key-rebind row in the profile modal. */
.ptt-keyrow { display: flex; align-items: center; gap: 0.6rem; }
.ptt-key {
  min-width: 4rem; padding: 0.25rem 0.6rem;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--bg-1, var(--bg-2)); color: var(--text);
  cursor: pointer; font: inherit;
}
.ptt-key:hover { border-color: var(--accent-dim); }

/* On-call cue in the member list: a 🔊 badge plus a faint green wash on the row
   so it reads as "in the call" at a glance. */
.member.on-call { background: rgba(80,180,80,0.10); }
.member-call { margin-left: auto; font-size: 0.9rem; line-height: 1; flex: none; }
/* Speaking indicator: a green ring pulsing around the presence dot while voice
   metering (voice.js AnalyserNode) reports this participant above the threshold. */
.member.speaking .dot { box-shadow: 0 0 0 3px rgba(80,180,80,0.45); animation: speaking-pulse 1.1s ease-in-out infinite; }
@keyframes speaking-pulse {
  0%, 100% { box-shadow: 0 0 0 2px rgba(80,180,80,0.35); }
  50%      { box-shadow: 0 0 0 4px rgba(80,180,80,0.75); }
}
@media (prefers-reduced-motion: reduce) {
  .member.speaking .dot { animation: none; box-shadow: 0 0 0 3px rgba(80,180,80,0.6); }
}
/* Per-user volume slider: a slim full-width range under an on-call member's
   name. Sits in the .member-text column so it spans the row's text width. */
.member-volume {
  width: 100%;
  margin-top: 0.2rem;
  height: 4px;
  cursor: pointer;
  accent-color: var(--accent);
}

/* Header on-call cue for DMs (no roster there): the other party is connected.
   Doubles as a click target that toggles the partner's volume slider, so it
   gets a pointer and a hover hint. */
.header-call { margin-left: 0.35rem; font-size: 0.9rem; line-height: 1; cursor: pointer; }
.header-call:hover { filter: brightness(1.3); }

/* The DM partner's volume slider, revealed inline beside the header 🔊. Slim,
   fixed width (no roster column to span); mirrors .member-volume's look. */
.header-volume { width: 90px; margin-left: 0.4rem; height: 4px; vertical-align: middle; cursor: pointer; accent-color: var(--accent); }

/* Desktop: lock the four "corner" bars to matched heights so the sidebar/main
   column dividers meet flush. The top pair (sidebar header + channel header) share
   one height; the bottom pair (sidebar footer + composer) share a taller one (the
   composer holds a textarea). The composer keeps min-height + flex-end so it still
   grows upward as you type while its bottom-anchored buttons stay put — the slack
   sits above the textarea. Mobile uses slide-in drawers and is exempt (the corners
   don't meet there), so these live behind a desktop-only query. */
@media (min-width: 721px) {
  .sidebar-head, .channel-header { min-height: 57px; }
  .sidebar-foot { min-height: 72px; }
  .composer { min-height: 72px; display: flex; flex-direction: column; justify-content: flex-end; }
}

/* In-app ping toasts: slide in from the top when a ping arrives while the tab is
   focused (OS notifications are suppressed then). z-150 sits above admin panel
   (z-40), drawers (z-50), and modals (z-60) but below the context menu (z-200). */
.ping-toasts { position: fixed; top: env(safe-area-inset-top, 0px); left: 0; right: 0; z-index: 150; pointer-events: none; }
.ping-toast {
  display: flex; align-items: baseline; gap: 0.6rem;
  padding: 0.85rem 1.2rem; background: #f59e0b; border-bottom: 2px solid #d97706;
  box-shadow: 0 4px 18px rgba(0,0,0,0.5);
  pointer-events: all; cursor: pointer; user-select: none;
  animation: ping-toast-in 0.2s ease-out;
}
.ping-toast:active { background: #d97706; }
.ping-toast-who { font-weight: 700; font-size: 1rem; flex-shrink: 0; color: #1c1008; }
.ping-toast-body { min-width: 0; overflow: hidden; color: #3b1f05; font-size: 1rem; overflow-wrap: anywhere; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 4; line-clamp: 4; }
@keyframes ping-toast-in {
  from { opacity: 0; transform: translateY(-100%); }
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) { .ping-toast { animation: none; } }

/* Mobile message context menu (bottom sheet, shown on long-press). */
.mobile-ctx { position: fixed; inset: 0; z-index: 200; }
.mobile-ctx::before { content: ""; position: absolute; inset: 0; background: rgba(0,0,0,0.45); }
.mobile-ctx-sheet { position: absolute; bottom: 0; left: 0; right: 0; background: var(--bg-2); border-radius: 12px 12px 0 0; padding: 0.4rem 0 calc(env(safe-area-inset-bottom, 0px) + 0.5rem); max-height: 70vh; overflow-y: auto; }
.mobile-ctx-btn { display: flex; align-items: center; gap: 0.6rem; width: 100%; padding: 0.85rem 1.2rem; border: none; background: transparent; color: var(--text); font: inherit; font-size: 1rem; text-align: left; cursor: pointer; }
.mobile-ctx-btn:active { background: var(--bg-hover); }
.mobile-ctx-btn.danger { color: var(--red, #e74c3c); }
.mobile-ctx-sep { height: 1px; background: var(--border); margin: 0.25rem 0; }
.mobile-ctx-reactions { padding: 0.1rem 1.2rem 0.4rem; }
.mobile-ctx-reaction-row { display: flex; align-items: center; gap: 0.6rem; padding: 0.5rem 0; border-bottom: 1px solid var(--border); }
.mobile-ctx-reaction-row:last-child { border-bottom: none; }
.mobile-ctx-reaction-emoji { font-size: 1.3rem; min-width: 2.2rem; display: flex; align-items: center; }
.mobile-ctx-reaction-names { color: var(--text-dim); font-size: 0.9rem; }
.mobile-ctx-no-reactions { color: var(--text-faint); font-size: 0.9rem; padding: 0.8rem 0 0; margin: 0; }

/* --- initial loading screen --- */
#loading-screen {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  transition: opacity 0.3s ease;
}
#loading-screen.done { opacity: 0; pointer-events: none; }
.loading-dots { display: flex; gap: 10px; align-items: center; }
.loading-dots span {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--text-dim);
  animation: loading-dot 1.4s ease-in-out infinite;
}
.loading-dots span:nth-child(2) { animation-delay: 0.2s; }
.loading-dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes loading-dot {
  0%, 60%, 100% { opacity: 0.2; transform: translateY(0); }
  30% { opacity: 0.85; transform: translateY(-5px); }
}

@media (max-width: 720px) {
  /* The conversation takes the whole screen; the sidebar (channels/DMs/profile)
     and members panel become slide-in drawers from the left and right. */
  .app { grid-template-columns: 1fr; grid-template-rows: 100%; }
  /* Hide hover-only message controls; long-press shows the context sheet instead. */
  .msg-actions { display: none !important; }
  .embed-remove { display: none !important; }
  /* Suppress text selection and native callouts on messages so long-press is clean. */
  .msg { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
  #sidebar-toggle, #members-toggle { display: inline-flex; align-items: center; justify-content: center; }
  .sidebar, .members {
    position: fixed; top: 0; bottom: 0;
    width: 80%; max-width: 300px;
    transition: transform 0.2s ease;
    z-index: 50;
  }
  .sidebar { left: 0; transform: translateX(-100%); box-shadow: 8px 0 24px rgba(0, 0, 0, 0.45); }
  .members { right: 0; transform: translateX(100%); box-shadow: -8px 0 24px rgba(0, 0, 0, 0.45); }
  body.sidebar-open .sidebar { transform: translateX(0); }
  body.members-open .members { transform: translateX(0); }
  .link-preview-desc { -webkit-line-clamp: 10; }
}
