/* ============================================================
   Echo — Main Stylesheet v2.1
   ============================================================ */

/* === CSS Variables (Light Theme) === */
:root {
  --accent:       #6c5ce7;
  --accent-light: #a29bfe;
  --accent-dim:   rgba(108,92,231,0.12);
  --bg:           #f0f2f5;
  --bg2:          #ffffff;
  --bg3:          #f7f7fb;
  --text:         #1a1a2e;
  --text2:        #4a4a6a;
  --muted:        #9090b0;
  --border:       #e4e4f0;
  --danger:       #e84393;
  --warning:      #f9a825;
  --success:      #00b894;
  --nav-h:        68px;
  --top-h:        auto;
  --radius:       20px;
  --radius-sm:    12px;
  --shadow:       0 8px 24px rgba(0,0,0,0.08);
  --shadow-lg:    0 16px 48px rgba(0,0,0,0.14);
}

[data-theme="dark"] {
  --bg:    #111120;
  --bg2:   #1a1a2e;
  --bg3:   #16213e;
  --text:  #f0f0f8;
  --text2: #c4c4e0;
  --muted: #9090b8;
  --border:#2e2e50;
  --shadow: 0 8px 24px rgba(0,0,0,0.3);
  --shadow-lg: 0 16px 48px rgba(0,0,0,0.5);
}

/* === Reset === */
*, *::before, *::after {
  box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
}

html {
  overflow-x: hidden;
  max-width: 100vw;
  touch-action: pan-y;
  overscroll-behavior-x: none;
}

body {
  margin: 0;
  font-family: 'Poppins', -apple-system, sans-serif;
  background: var(--bg);
  color: var(--text);
  height: 100dvh;
  height: var(--vvh, 100dvh);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  overscroll-behavior: none;
  overscroll-behavior-x: none;
  touch-action: pan-y;
}

/* === Desktop Phone Frame ===
   min-height matters as much as min-width here. A phone in landscape is roughly 800x360 — wide
   enough to match a bare `min-width: 600px`, but nowhere near tall enough for a layout built
   around a tall window. It was getting the desktop frame (html{overflow:hidden;height:100%},
   3-column grid, fixed chrome) inside ~360px of height, which is what produced the overlapping
   landscape elements in QA. Requiring real height keeps genuine desktop and tablet-landscape
   windows (800px+ tall) on the framed layout and drops short landscape phones back to the plain
   mobile layout, which already scrolls correctly. */
@media (min-width: 600px) and (min-height: 700px) {
  html {
    background: #0d0d1a;
    height: 100%;
    overflow: hidden;
  }
  body {
    max-width: 860px;
    margin: 0 auto;
    transform: translateZ(0);
    will-change: transform;
    box-shadow: -1px 0 0 rgba(255,255,255,0.05), 1px 0 0 rgba(255,255,255,0.05),
                0 0 80px rgba(108,92,231,0.2);
  }
  /* 3-column portrait grid on desktop */
  .posts-grid:not(.layout-full) {
    grid-template-columns: repeat(3, 1fr);
  }
  /* Full-width layout gets 2 columns on desktop */
  .posts-grid.layout-full {
    grid-template-columns: repeat(2, 1fr);
  }
  /* Taller preview in post modal on desktop */
  .post-preview-box {
    height: 50vh;
  }
}

/* === Landscape / short viewports ===
   The height ceiling here is deliberately the SAME 700px the desktop frame above uses as its
   floor, so the two rules meet with no gap. They previously didn't (frame needed >=700, this
   needed <=500), which left anything 500-700px tall — a tablet on its side, most obviously —
   with neither the framed layout nor any landscape handling: content stretched edge to edge
   across the full width, and auto-fitted post text grew to its 60px ceiling to fill it.

   --nav-h is the single source of truth for the bottom nav's footprint (app.js publishes
   --nav-reserve as `var(--nav-h)`), so overriding it here shrinks the nav AND every modal/ad
   offset that reserves space for it, in one place. */
@media (orientation: landscape) and (max-height: 700px) {
  :root { --nav-h: 52px; }
  #top-nav { padding-top: 6px; padding-bottom: 6px; }
  #bottom-tabs .tab i { font-size: 0.95rem; }
  #bottom-tabs .tab span { font-size: 0.62rem; }

  /* Stop content stretching the full width of a landscape screen. Reading a post 2000px wide is
     miserable, and it's what was inflating the auto-fit text to its maximum size. The nav bars
     stay full-bleed; only the scrolling content is capped. */
  #app-content { max-width: 900px; margin-inline: auto; width: 100%; }
  #top-nav > .segments,
  #top-nav > .radius-row,
  #top-nav > #search-bar-row { max-width: 900px; margin-inline: auto; }

  .posts-grid:not(.layout-full) { grid-template-columns: repeat(3, 1fr); }
  .posts-grid.layout-full       { grid-template-columns: repeat(2, 1fr); }

  /* Sheets are capped at a share of the visual viewport; on a short screen that leaves a
     scrap of usable space, so let them take most of it. */
  .sheet-modal { max-height: min(90vh, var(--vvh)); }
  .post-preview-box { height: 45vh; }
  #fab { bottom: calc(var(--nav-h) + 12px); width: 48px; height: 48px; }

  /* --- Thread view ---------------------------------------------------------
     The card's 3/4 aspect ratio is meaningless when the viewport is wider than it is tall: the
     ratio resolves to a height far past the screen, the max-height cap takes over, and the card
     becomes a wide letterbox with its text clipped top and bottom. Give it a height budget that
     leaves room for the topbar, action bar and meta rows instead. */
  .thread-post-card,
  .thread-post-card.post-img { max-height: 52dvh; }
  .thread-post-card.is-story { max-height: 52dvh; }
  .thread-post-text { padding: 12px 16px; }

  .thread-user-bar { padding: 6px 16px; }
  .thread-meta { flex-direction: row; gap: 16px; padding: 6px 16px; }
  .thread-action-bar { padding: 0 6px; }
}

/* === Terms of Use Overlay === */
.terms-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.7);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  z-index: 3000;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: 0;
}
.terms-box {
  background: var(--bg2);
  width: 100%;
  max-width: 560px;
  /* vh first as a fallback: older WebViews (e.g. BlueStacks) don't support dvh, and without a
     bounded height .terms-scroll never becomes the scroll container — its onscroll never fires and
     the "scroll to accept" checkbox can never unlock, locking the user out of the app entirely. */
  height: 92vh;
  height: 92dvh;
  border-radius: 20px 20px 0 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.terms-header {
  padding: 24px 20px 16px;
  border-bottom: 1px solid var(--border);
  text-align: center;
  flex-shrink: 0;
}
.terms-logo {
  font-size: 2rem;
  font-weight: 800;
  color: var(--accent);
  letter-spacing: -1px;
  margin-bottom: 6px;
}
.terms-header p {
  margin: 0;
  font-size: 0.88rem;
  color: var(--text2);
}
.terms-scroll {
  flex: 1;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 20px;
  font-size: 0.85rem;
  line-height: 1.65;
  color: var(--text);
}
.terms-scroll h3 { font-size: 1.1rem; margin-top: 0; }
.terms-scroll h4 { font-size: 0.92rem; margin-top: 20px; margin-bottom: 6px; color: var(--accent); }
.terms-scroll h5 { font-size: 0.85rem; margin-top: 14px; margin-bottom: 4px; }
.terms-scroll ul  { padding-left: 20px; margin: 8px 0; }
.terms-scroll li  { margin-bottom: 4px; }
.terms-scroll a   { color: var(--accent); }
.terms-footer {
  padding: 16px 20px;
  padding-bottom: calc(16px + env(safe-area-inset-bottom,0px));
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex-shrink: 0;
  background: var(--bg2);
}
.terms-checkbox-row {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 0.85rem;
  cursor: pointer;
  color: var(--text2);
}
.terms-checkbox-row input[type=checkbox] {
  width: 18px;
  height: 18px;
  accent-color: var(--accent);
  flex-shrink: 0;
  cursor: pointer;
}
.terms-footer .btn-primary,
.terms-footer .btn-secondary {
  width: 100%;
  text-align: center;
  padding: 13px;
}

/* === Install Banners === */
.install-banner {
  position: fixed;
  top: env(safe-area-inset-top, 0);
  left: 0; right: 0;
  background: var(--accent);
  color: #fff;
  padding: 10px 15px;
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 0.82rem;
  z-index: 2000;
  flex-wrap: wrap;
}
.install-banner span { flex: 1; line-height: 1.4; }
.install-btn {
  background: #fff;
  color: var(--accent);
  border: none;
  padding: 6px 14px;
  border-radius: 20px;
  font-weight: 700;
  font-size: 0.8rem;
  cursor: pointer;
  white-space: nowrap;
}

/* === Top Navigation === */
#top-nav {
  background: var(--bg2);
  padding: calc(env(safe-area-inset-top,0px) + 10px) 15px 10px;
  box-shadow: 0 2px 12px rgba(0,0,0,0.06);
  position: sticky;
  top: 0;
  z-index: 100;
  border-bottom: 1px solid var(--border);
}
/* Native iOS (Capacitor) sets contentInset:"always", so the WebView already insets in-flow
   content below the status bar / Dynamic Island. The top nav then ALSO added
   env(safe-area-inset-top) padding on top of that, double-insetting it into a big empty gap
   under the status bar. So on iOS ONLY we drop back to a flat 10px.
   Android (edge-to-edge under targetSdk 35+) has NO contentInset — the WebView draws behind the
   status bar/camera notch, so it must KEEP the env(safe-area-inset-top) padding from #top-nav
   above, or the nav slides up under the notch. Hence this override is scoped to .is-ios, not all
   native. Web/PWA (no class) also keeps the env() padding.
   The SAME double-inset applies to top-anchored position:fixed layers (full-screen modals, the
   chat header, the install landing, the update banner) — WKWebView's contentInset shifts the
   layout viewport down, so `top:0` is already below the status bar and their own
   env(safe-area-inset-top) padding stacks a second inset on top. Those are corrected alongside
   their base rules further down, each scoped to .is-ios the same way. */
.is-ios #top-nav { padding-top: 10px; }

.segments {
  display: flex;
  background: var(--bg3);
  border-radius: var(--radius-sm);
  padding: 3px;
  gap: 2px;
  /* Falls back to scrolling when the tabs genuinely can't fit (see the button rule below), rather
     than truncating them into unreadable stubs. Scrollbar hidden — the partly-visible tab at the
     edge is the affordance. */
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;              /* Firefox */
}
.segments::-webkit-scrollbar { display: none; }   /* Chrome/Safari */
.segments button {
  /* flex-basis 0 + grow keeps tabs evenly sized whenever they fit, which is the look everywhere
     except the Admin Panel. min-width:max-content is what stops a long label ("Following",
     "Favorites", "User Reports") being squeezed until ellipsis eats it — instead the row overflows
     and scrolls. Previously this was min-width:0 + text-overflow:ellipsis, which produced "Mo…",
     "Use…", "App…" once a row had more tabs than it had room for. */
  flex: 1 1 0%;
  min-width: max-content;
  border: none;
  background: transparent;
  padding: 8px 10px;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--muted);
  border-radius: 10px;
  cursor: pointer;
  transition: all .2s;
  font-family: inherit;
  white-space: nowrap;
  position: relative;   /* anchor for .tab-badge — previously each badged tab needed an inline style */
}
.segments button.active {
  background: var(--bg2);
  color: var(--accent);
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.radius-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  font-size: 0.8rem;
  color: var(--muted);
}
.radius-row input[type=range] {
  flex: 1;
  accent-color: var(--accent);
  height: 4px;
}
#radius-val {
  font-weight: 700;
  color: var(--accent);
  min-width: 32px;
}

/* === Main Content === */
#app-content {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-y: contain;
  touch-action: pan-y;
  /* #bottom-tabs is bottom:0 with a flat height:var(--nav-h) — it doesn't reserve extra space
     for the safe area itself, so nothing else here should either (see .ad-banner for the full
     explanation; this same +safe-area mistake was causing the same kind of gap in several
     places). */
  padding: 15px 15px calc(var(--nav-h) + 60px);
}
/* Extra bottom room for the native AdMob banner (native apps only) so it never covers posts */
body.has-native-ad #app-content {
  padding-bottom: calc(var(--nav-h) + 120px);
}

/* === Ad Banner ===
   The nav bar is ALWAYS docked flush to the true bottom of the screen, on every page, full
   stop — it never moves for anything. The ad instead floats as its own layer directly above
   the nav, overlaying whatever feed content happens to be scrolled underneath it, rather than
   pushing the nav (and every modal's reserved space) around. This is deliberately simpler than
   the previous "nav shifts to make room" design, which was the root cause of a long string of
   overlap bugs across chat, settings, and the composer. */
.ad-banner {
  position: fixed;
  /* #bottom-tabs is bottom:0 with a flat height:var(--nav-h) — it does NOT reserve extra
     space for the safe area itself (that's a purely decorative fill strip below it, see
     #bottom-tabs::after). Adding safe-area-inset-bottom here too floated the ad above the
     nav's actual top edge, leaving a gap on any device with a non-zero safe area. */
  bottom: var(--nav-h);
  left: 0; right: 0;
  z-index: 90;
  min-height: 0;
  max-height: 260px;   /* generous safety cap only */
  overflow: hidden;
  line-height: 0;      /* the iframe/img child is the only content — kill any inline gap under it */
  text-align: center;
  background: var(--bg2);
  border-top: 1px solid var(--border);
  box-shadow: 0 -2px 12px rgba(0,0,0,0.08);
}
.ad-banner:empty { display: none; }

/* Collapse the ad strip until Monetag actually fills its container (empty container = no ad yet) */
#ad-banner:has(> div:empty) { display: none; }

/* === Bottom Navigation === */
#bottom-tabs {
  position: fixed;
  bottom: 0;
  left: 0; right: 0;
  height: var(--nav-h);
  background: var(--bg2);
  display: flex;
  border-top: 1px solid var(--border);
  z-index: 600;
  box-shadow: 0 -4px 20px rgba(0,0,0,0.06);
}
/* Fills the home-indicator safe area with the nav background — no height change to the tab row itself */
#bottom-tabs::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 0; right: 0;
  height: env(safe-area-inset-bottom, 0px);
  background: var(--bg2);
}
.tab {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: 0.7rem;
  font-weight: 500;
  color: var(--muted);
  gap: 3px;
  cursor: pointer;
  transition: color .2s;
  position: relative;
  user-select: none;
}
.tab.active { color: var(--accent); }
.tab i { font-size: 1.35rem; }
.badge {
  position: absolute;
  top: 6px; right: calc(50% - 18px);
  background: var(--danger);
  color: #fff;
  font-size: 0.65rem;
  font-weight: 700;
  min-width: 16px;
  height: 16px;
  border-radius: 8px;
  padding: 0 4px;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Moderation queue badge on the bottom-nav Account tab — offset to the opposite side
   from the personal notification badge so both can show at once without overlapping. */
.mod-nav-badge {
  right: auto;
  left: calc(50% - 18px);
  background: var(--accent);
}

/* === FAB === */
#fab {
  position: fixed;
  bottom: calc(var(--nav-h) + 70px);
  right: 18px;
  width: 56px; height: 56px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--accent), var(--accent-light));
  color: #fff;
  border: none;
  font-size: 1.5rem;
  box-shadow: 0 6px 20px rgba(108,92,231,0.45);
  z-index: 150;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: transform .2s cubic-bezier(.175,.885,.32,1.275), box-shadow .2s;
}
#fab:active { transform: scale(0.88) rotate(45deg); box-shadow: 0 2px 8px rgba(108,92,231,0.3); }

/* === Thread View === */
.thread-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 12px 8px;
  background: var(--bg2);
  border-bottom: 1px solid var(--border);
  /* Deliberately NOT sticky — it's the echo's header and scrolls with it, the same way the
     replies bar is its footer. Pinning it meant a permanently moving element sitting over the
     post; every variation (fixed, or auto-hiding on scroll direction) read as jumpy. Scroll back
     to the top to reach the back arrow. Nothing is stranded by that: Android's hardware back and
     the bottom nav both work from anywhere in the thread. */
  flex-shrink: 0;
}
.thread-title { font-weight: 700; font-size: 1rem; }
.thread-nav-arrows { display: flex; gap: 2px; }

/* User info bar */
.thread-user-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  background: var(--bg2);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.thread-user-info { flex: 1; min-width: 0; }
.thread-username {
  display: block;
  font-weight: 700;
  font-size: 0.95rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.thread-user-stars { font-size: 0.78rem; color: var(--accent); font-weight: 600; }
.thread-user-stars i { font-size: 0.7rem; }
.thread-more-btn { color: var(--muted); flex-shrink: 0; }

/* The echo card */
/* Admin Panel (super admin) */
.admin-stats-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
  padding: 4px 0 14px;
}
.stat-tile {
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 14px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: flex-start;
}
.stat-tile i { color: var(--accent); font-size: 1.05rem; }
.stat-tile strong { font-size: 1.5rem; line-height: 1.1; }
.stat-tile span { font-size: 0.78rem; color: var(--muted); }
.admin-quick { margin-top: 6px; }
.admin-quick .btn-secondary { width: 100%; }
.admin-section-note { font-size: 0.85rem; color: var(--muted); margin: 4px 0 14px; }
/* Oversight callout: a moderator whose decisions keep getting overturned on appeal. Deliberately
   louder than the surrounding notes — it's the one thing in this tab that asks for action. */
.admin-section-note.warn {
  color: #e17055;
  background: rgba(225, 112, 85, 0.10);
  border: 1px solid rgba(225, 112, 85, 0.35);
  border-radius: 10px;
  padding: 10px 12px;
  line-height: 1.45;
}
.admin-section-note.warn i { margin-right: 6px; }
.admin-user-row.mod-flagged { border-left: 3px solid #e17055; padding-left: 9px; }

/* "Don't lose this account" prompt. Two separate blocks so the email route and the recovery-key
   route read as alternatives rather than a form with a stray button attached. */
.secure-block {
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px;
  margin-top: 12px;
  text-align: left;
}
.secure-label {
  display: block;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--muted);
  margin-bottom: 8px;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.secure-block input {
  width: 100%;
  box-sizing: border-box;
  margin-bottom: 8px;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text);
  font-family: inherit;
  font-size: 0.9rem;
}
.secure-error {
  display: none;
  color: #e17055;
  font-size: 0.78rem;
  margin-bottom: 8px;
  line-height: 1.4;
}
/* The key is long and unbreakable — let it wrap rather than blow out the card width. */
.secure-key {
  font-family: 'Roboto Mono', monospace;
  font-size: 0.7rem;
  word-break: break-all;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px;
  margin-bottom: 8px;
  color: var(--text);
}

/* Author name in the report / review queues — tappable through to their profile. Underlined
   rather than just coloured so it reads as a link on a card that already uses accent colour for
   badges and pills. */
.mod-author-link {
  cursor: pointer;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.mod-author-link:active { opacity: 0.6; }

/* Content preview in the Removed tab. Deliberately NOT .report-post-preview — that one is a
   fixed-height purple tile built to show a reported echo's background image, and its line clamp
   targets a child span. Here the text IS the content, so it wants a plain readable block that
   grows to three lines and then crops. */
.removed-post-preview {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 10px;
  font-size: 0.82rem;
  line-height: 1.4;
  color: var(--text);
  word-break: break-word;
}
.admin-user-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
}
/* Pre-chat identity card (shown before a new conversation opens) + incoming request cards */
.prechat-head { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.prechat-avatar {
  width: 46px; height: 46px; border-radius: 50%;
  background: var(--accent); color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 1.1rem; flex-shrink: 0;
  overflow: hidden;
}

/* Round avatar slots that hold a real picture instead of an initial. The wrapper keeps owning the
   size (and, in the chat list, the unread dot), so the image just fills it — that way one class
   covers every slot without each caller re-stating dimensions. */
.chat-avatar.has-img,
.prechat-avatar.has-img { background: none; }
.chat-avatar > img,
.prechat-avatar > img {
  width: 100%; height: 100%;
  border-radius: 50%;
  object-fit: cover;
  display: block;
}
.prechat-facts { list-style: none; padding: 0; margin: 0 0 12px; }
.prechat-facts li {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 0; border-bottom: 1px solid var(--border);
  font-size: 0.85rem;
}
.prechat-facts li:last-child { border-bottom: none; }
.prechat-facts i { width: 18px; text-align: center; color: var(--muted); flex-shrink: 0; }
.prechat-echo {
  background: var(--bg3); border: 1px solid var(--border); border-radius: 10px;
  padding: 10px; font-size: 0.82rem; word-break: break-word;
}
.prechat-actions { display: flex; gap: 10px; }
.prechat-actions button { flex: 1; }

/* Banner on the main chat lists pointing at waiting requests (they're hidden from those lists) */
.chat-requests-banner {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  margin-bottom: 10px;
  border: 1px solid var(--accent);
  border-radius: 12px;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  cursor: pointer;
  font-size: 0.85rem;
}
/* color-mix is unsupported on older WebViews — they simply get the plain bg3 fallback above it */
@supports not (background: color-mix(in srgb, red 10%, transparent)) {
  .chat-requests-banner { background: var(--bg3); }
}
.chat-requests-banner span { flex: 1; }
.chat-requests-banner i:first-child { color: var(--accent); }
.chat-requests-banner i:last-child { color: var(--muted); font-size: 0.75rem; }

/* Incoming chat-request cards (Chat → Requests) */
.chat-request-card {
  border: 1px solid var(--border); border-radius: 12px;
  padding: 12px; margin-bottom: 10px; background: var(--bg2);
}
.chat-request-msg {
  background: var(--bg3); border-radius: 10px; padding: 10px;
  font-size: 0.85rem; margin: 8px 0; word-break: break-word;
}
.chat-request-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.chat-request-actions button { flex: 1; min-width: 88px; }

/* Sort control on the Admin Panel → Users list */
.admin-sort-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
  font-size: 0.82rem;
  color: var(--muted);
}
.admin-sort-row select {
  flex: 1;
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 7px 10px;
  font-family: inherit;
  font-size: 0.82rem;
  color: var(--text);
}

/* ── Moderation: enforcement history, appeals, tags ─────────────────────────────────────── */
.mod-tag {
  display: inline-block;
  font-size: 0.66rem;
  font-weight: 600;
  padding: 2px 7px;
  border-radius: 10px;
  background: var(--bg3);
  border: 1px solid var(--border);
  color: var(--text2);
  vertical-align: middle;
  margin-left: 4px;
  white-space: nowrap;
}
.mod-tag.warn { border-color: #e17055; color: #e17055; }
.mod-tag.good { border-color: #00b894; color: #00b894; }

/* Moderator tools panel on someone's profile. Sits INSIDE .profile-hero, which paints a purple
   gradient and sets color:#fff — so this has to re-establish a normal surface and normal text
   colour, or every child that sets its own colour (.section-title, .muted, .mod-history-row)
   renders dark-on-purple and becomes unreadable. */
.profile-mod-panel {
  width: 100%;
  box-sizing: border-box;
  margin-top: 14px;
  padding: 14px;
  background: var(--bg2);
  color: var(--text);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  text-align: left;
}
.profile-mod-title {
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
  text-align: center;
  margin-bottom: 10px;
}
.profile-mod-title i { margin-right: 5px; }
/* The hero forces white text on its descendants; the panel's own label must not inherit it. */
.profile-mod-panel .nsfw-label { color: var(--text2); }

.mod-history-wrap { text-align: left; margin-top: 6px; }
.mod-history-row {
  display: flex;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
  font-size: 0.85rem;
}
.mod-history-row:last-of-type { border-bottom: none; }
/* Reversed or overturned penalties stay visible for the record but read as no longer counting */
.mod-history-row.is-void { opacity: 0.5; text-decoration: line-through; }
.mod-history-row.is-void .muted { text-decoration: none; }
/* Same treatment in the User History Penalties tab — a reversed penalty stays visible (that it was
   issued and undone is part of the record) but must not read as a live mark against the account. */
.history-item.is-void { opacity: 0.5; }
.history-item.is-void p { text-decoration: line-through; }
.history-item.is-void .muted { text-decoration: none; }

/* Adjustable inactivity filter on the Admin → Users tab. Wraps on narrow screens rather than
   forcing the number input to shrink to nothing. */
.admin-inactive-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 6px;
}
.admin-inactive-row label { font-size: 0.82rem; color: var(--muted); }
.admin-inactive-row input[type="number"] {
  width: 90px;
  padding: 7px 9px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg3);
  color: var(--text);
  font-family: inherit;
  font-size: 0.88rem;
}
.admin-inactive-presets { display: flex; gap: 6px; flex-wrap: wrap; }
.admin-inactive-presets button { font-size: 0.74rem; }

/* Header inside an open conversation in User History → Chats. Sticky so the way back to the
   conversation list is always reachable, however long the thread is. */
.chat-thread-bar {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  margin-bottom: 8px;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}

/* The echo a reply was answering, quoted above it in User History → Replies. Indented and muted so
   it reads as context rather than as something this account said — mixing the two up is how a
   moderator ends up acting on the wrong person's words. Clamped: the parent can be a 2000-char
   Story and it must not bury the reply underneath it. */
.history-parent {
  border-left: 3px solid var(--border);
  padding: 4px 0 4px 8px;
  margin: 4px 0 6px;
  font-size: 0.78rem;
  color: var(--muted);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
  word-break: break-word;
}
.history-parent .muted { display: block; font-size: 0.72rem; margin-bottom: 2px; }

/* Count bubble inside a tab label — User History tabs carry their own totals so an admin can see
   where the substance is before clicking through six tabs. */
.tab-count {
  display: inline-block;
  min-width: 16px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--bg3);
  color: var(--muted);
  font-size: 0.68rem;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
}
.segments button.active .tab-count { background: var(--accent); color: #fff; }

.mod-note {
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 8px 10px;
  margin-bottom: 6px;
  font-size: 0.82rem;
  word-break: break-word;
}

.appeal-card {
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px;
  margin-bottom: 10px;
  background: var(--bg2);
}
.appeal-quote {
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 8px 10px;
  margin-top: 6px;
  font-size: 0.83rem;
  word-break: break-word;
}
/* The user's own words get the accent edge so they're distinguishable from the quoted content */
.appeal-quote.appeal-user { border-left: 3px solid var(--accent); }

/* Pending moderator-application cards (Admin Panel → Moderators) */
.mod-request-card {
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px;
  margin-bottom: 10px;
  background: var(--bg3);
}
.mod-request-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.mod-request-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
.mod-request-actions button { flex: 1; min-width: 90px; }

/* Install landing overlay */
.install-landing-overlay {
  position: fixed;
  inset: 0;
  z-index: 4000;
  background: var(--bg);
  overflow-y: auto;
  display: flex;
  justify-content: center;
  padding: max(24px, env(safe-area-inset-top)) 16px calc(24px + env(safe-area-inset-bottom));
}
.is-ios .install-landing-overlay { padding-top: 24px; }

/* One-time "you're a moderator now" popup */
.mod-welcome-overlay {
  position: fixed;
  inset: 0;
  z-index: 5000;
  background: rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(3px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.mod-welcome-card {
  width: 100%;
  max-width: 400px;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 24px 22px;
  text-align: center;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
}
.mod-welcome-icon {
  width: 64px;
  height: 64px;
  margin: 0 auto 14px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg, var(--accent), #8b7bf0);
  color: #fff;
  font-size: 1.6rem;
}
.mod-welcome-card h2 { margin: 0 0 10px; font-size: 1.25rem; }
.mod-welcome-card p { margin: 0 0 10px; font-size: 0.9rem; line-height: 1.5; }
.mod-welcome-card p.muted { color: var(--muted); font-size: 0.85rem; }
.mod-welcome-card .mod-welcome-icon + h2 { margin-top: 0; }
.mod-welcome-actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 18px;
}
.mod-welcome-actions button { width: 100%; }

/* One-time "how to create an Echo" tips popup — reuses the mod-welcome card look */
.post-help-tips {
  list-style: none;
  margin: 4px 0 0;
  padding: 0;
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.post-help-tips li {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}
.post-help-tips i {
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--accent-dim);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.9rem;
}
.post-help-tips strong { display: block; font-size: 0.9rem; }
.post-help-tips span { display: block; font-size: 0.82rem; color: var(--muted); line-height: 1.4; }

/* Persistent full-screen banned notice */
.banned-overlay {
  position: fixed;
  inset: 0;
  z-index: 6000;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.banned-card {
  width: 100%;
  max-width: 420px;
  text-align: center;
}
.banned-icon {
  width: 72px;
  height: 72px;
  margin: 0 auto 16px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(225, 112, 85, 0.15);
  color: #e17055;
  font-size: 2rem;
}
.banned-card h2 { margin: 0 0 10px; font-size: 1.35rem; }
.banned-card p { margin: 0 0 12px; font-size: 0.92rem; line-height: 1.55; }
.banned-card p.muted { color: var(--muted); font-size: 0.85rem; }
.banned-card a { color: var(--accent); }
.banned-reason {
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  margin: 14px 0;
  text-align: left;
}
.banned-reason span {
  display: block;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
  margin-bottom: 4px;
}
.banned-reason p { margin: 0; font-size: 0.92rem; color: var(--text); }
.install-landing-overlay.hidden { display: none; }
.landing-card {
  width: 100%;
  max-width: 460px;
  margin: auto;
}
.landing-hero { text-align: center; margin-bottom: 18px; }
.landing-hero .recovery-logo { margin: 0; }
.landing-perks {
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 18px;
  margin-bottom: 18px;
}
.landing-perks p { margin: 0 0 10px; }
.landing-perks ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.landing-perks li { display: flex; align-items: center; gap: 10px; font-size: 0.9rem; color: var(--text2); }
.landing-perks li i { color: var(--accent); width: 18px; text-align: center; }
.landing-tabs { margin-bottom: 14px; }
.landing-steps-wrap { margin-bottom: 18px; }
.landing-steps { margin: 0; padding-left: 22px; display: flex; flex-direction: column; gap: 12px; }
.landing-steps li { font-size: 0.95rem; line-height: 1.45; color: var(--text); }
.landing-steps i { color: var(--accent); }
.landing-hint {
  margin: 14px 0 0;
  font-size: 0.82rem;
  color: var(--text2);
  background: var(--bg3);
  border-radius: 10px;
  padding: 10px 12px;
}
.landing-hint i { color: var(--accent); margin-right: 4px; }
.landing-btn { width: 100%; margin-top: 6px; }

/* Recovery-key handoff shown when moving from the website to the native app */
.landing-key-box {
  margin-top: 14px;
  padding: 12px;
  border: 1px solid var(--accent);
  border-radius: 12px;
  background: var(--bg3);
}
.landing-key-box p { margin: 0 0 8px; font-size: 0.85rem; line-height: 1.45; }
.landing-key-value {
  font-family: 'Roboto Mono', monospace;
  font-size: 0.68rem;
  line-height: 1.5;
  word-break: break-all;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px;
  user-select: all; /* one tap selects the whole key if the copy button can't be used */
}
/* Secondary "add to home screen instead" disclosure on the iOS landing */
.landing-alt { margin-top: 16px; }
.landing-alt summary {
  cursor: pointer;
  font-size: 0.85rem;
  color: var(--accent);
  padding: 6px 0;
}
.landing-alt > *:not(summary) { margin-top: 10px; }
.landing-foot { text-align: center; font-size: 0.78rem; margin: 12px 0 0; }

/* Preset avatar picker */
.avatar-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
}
.avatar-pick {
  background: none;
  border: 2px solid transparent;
  border-radius: 50%;
  padding: 0;
  cursor: pointer;
  aspect-ratio: 1/1;
  overflow: hidden;
  transition: transform .12s, border-color .12s;
}
.avatar-pick img {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  display: block;
  object-fit: cover;
}
.avatar-pick:active { transform: scale(0.94); }
.avatar-pick.selected { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }

/* Chat → Echo context banner */
.chat-echo-context {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  background: var(--bg2);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.chat-echo-thumb {
  width: 34px;
  height: 34px;
  border-radius: 8px;
  background-size: cover;
  background-position: center;
  flex-shrink: 0;
}
.chat-echo-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.chat-echo-text small { color: var(--muted); font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.04em; }
.chat-echo-text span {
  font-size: 0.82rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.chat-echo-line {
  display: block;
  font-size: 0.72rem;
  color: var(--accent);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin: 1px 0;
}

.thread-post-card {
  width: 100%;
  aspect-ratio: 3/4;
  /* Caps the card on short/narrow phones so the topbar + card + action bar + meta all fit in
     one screen without scrolling — aspect-ratio alone made it taller than the viewport. */
  max-height: 42dvh;
  position: relative;
  overflow: hidden;
  background-color: var(--accent);
  background-size: cover;
  background-position: center;
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
}
/* Opened echoes show the WHOLE photo (background-size:contain, set inline in renderThreadParent)
   instead of a crop. The card is taller than the feed version so a contained image still reads at
   a decent size, and the leftover letterbox space sits on a neutral dark backdrop rather than the
   accent colour, which looked like a rendering fault around the image. */
.thread-post-card.post-img {
  background-color: #14141c;
  max-height: 56dvh;
}
.thread-post-card.post-img::before {
  content: '';
  position: absolute;
  inset: 0;
  /* Lighter than it was: with the full photo visible the scrim only needs to keep the overlaid
     text legible, not darken a crop that filled the whole card. */
  background: linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.35) 100%);
  z-index: 1;
  pointer-events: none;
}
.thread-post-text {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px 20px;
  text-align: center;
  font-weight: 700;
  line-height: 1.45;
  text-shadow: 0 2px 10px rgba(0,0,0,0.6);
  word-break: break-word;
  overflow: hidden;
  position: relative;
  z-index: 2;
}
.thread-post-text .fit { display: block; width: 100%; }

/* Story thread view — this is the "scrollable" behavior: drop the fixed aspect-ratio/height
   cap and auto-fit-shrink that every other post type uses, let the card grow with its content
   and scroll internally instead. Additive override — nothing here touches .thread-post-card's
   base rules, so Echo/Poll (which never get the .is-story class) are completely unaffected. */
.thread-post-card.is-story {
  aspect-ratio: unset;
  max-height: 55dvh;
  overflow-y: auto;
}
/* A Story sizes to its text (aspect-ratio is unset above), so a short one would shrink the card
   and, with background-size:contain, shrink the photo with it. Give an image-backed Story a floor
   so the picture still gets real space. */
.thread-post-card.is-story.post-img { min-height: 40dvh; }
.thread-post-story-text {
  flex: 1;
  padding: 20px;
  text-align: left;
  position: relative;
  z-index: 2;
}
.thread-post-story-text span {
  display: block;
  font-size: 1rem;
  line-height: 1.6;
  color: #fff;
  text-shadow: 0 1px 4px rgba(0,0,0,0.5);
  word-break: break-word;
}

/* Action bar */
.thread-action-bar {
  display: flex;
  align-items: center;
  padding: 2px 6px;
  border-bottom: 1px solid var(--border);
  background: var(--bg2);
  flex-shrink: 0;
}
.thread-action-btn {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 10px 12px;
  border: none;
  background: none;
  color: var(--text2);
  font-family: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  border-radius: 8px;
  transition: background 0.15s;
}
.thread-action-btn:active { background: var(--bg3); }
.thread-action-btn.starred, .thread-action-btn.starred i { color: var(--accent); }
.thread-action-btn i { font-size: 1rem; }
.thread-chat-btn {
  margin-left: auto;
  background: var(--accent);
  color: #fff;
  border-radius: 20px;
  padding: 8px 16px;
}
.thread-chat-btn:active { background: var(--accent); opacity: 0.85; }

/* Location / time meta */
.thread-meta {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 10px 16px;
  font-size: 0.8rem;
  color: var(--muted);
  border-bottom: 1px solid var(--border);
  background: var(--bg2);
  flex-shrink: 0;
}
.thread-meta span { display: flex; align-items: center; gap: 6px; }

/* Poll voting UI — sits below the thread-post-card, filled in by loadPollDetail()/castPollVote() */
.poll-detail { padding: 10px 16px 4px; }
.poll-option-row {
  position: relative;
  width: 100%;
  text-align: left;
  padding: 12px 14px;
  margin-bottom: 8px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg3);
  cursor: pointer;
  overflow: hidden;
  font-family: inherit;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.poll-option-row:disabled { cursor: default; opacity: 0.85; }
.poll-option-row.mine { border-color: var(--accent); }
.poll-option-fill {
  position: absolute;
  top: 0; left: 0; bottom: 0;
  background: var(--accent-dim);
  transition: width .3s ease;
  z-index: 0;
}
.poll-option-label {
  position: relative;
  z-index: 1;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--text);
}
.poll-option-pct {
  position: relative;
  z-index: 1;
  font-size: 0.82rem;
  color: var(--muted);
  flex-shrink: 0;
}
.poll-meta {
  display: flex;
  justify-content: space-between;
  font-size: 0.78rem;
  color: var(--muted);
  padding: 4px 2px 10px;
}

.thread-replies-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 14px;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--text2);
  border-bottom: 1px solid var(--border);
  background: var(--bg2);
  /* Deliberately NOT sticky. It reads as the footer of the echo above it — the reply count
     belongs to that post — so it scrolls away with the post rather than detaching and riding
     down the screen over the replies it's counting. It was previously pinned under the topbar,
     which made it jump as the topbar auto-hid and left it stranded mid-screen. */
  flex-shrink: 0;
}

/* === Post Cards === */
.posts-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 6px;
  padding: 6px;
}
.posts-grid.layout-full {
  grid-template-columns: 1fr;
  gap: 12px;
  padding: 8px;
}

.post-card {
  border-radius: 10px;
  /* Fixed window — text is auto-sized to fit (see autoFitText) so it never scrolls. */
  aspect-ratio: 2/3;
  display: flex;
  flex-direction: column;
  position: relative;
  overflow: hidden;
  box-shadow: var(--shadow);
  color: #fff;
  background-color: var(--accent);
  background-size: cover;
  background-position: center;
  transition: transform .15s;
  cursor: default;
  /* Skip layout/paint work for off-screen cards in long feeds */
  content-visibility: auto;
  contain-intrinsic-size: 0 300px;
}
/* Readability scrim between an image background and the text */
.post-card.post-img::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0,0,0,0.30) 0%, rgba(0,0,0,0.45) 100%);
  z-index: 1;
  pointer-events: none;
}
.posts-grid.layout-full .post-card { aspect-ratio: 4/3; border-radius: var(--radius); }
.post-card:active { transform: scale(0.98); }

.nsfw-badge {
  position: absolute;
  top: 7px;
  left: 7px;
  background: #e17055;
  color: #fff;
  font-size: 9px;
  font-weight: 700;
  padding: 2px 6px;
  border-radius: 4px;
  letter-spacing: 0.06em;
  z-index: 3;
  text-transform: uppercase;
}

.pinned-badge {
  position: absolute;
  top: 7px;
  left: 7px;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  font-size: 9px;
  font-weight: 700;
  padding: 2px 7px;
  border-radius: 4px;
  letter-spacing: 0.06em;
  z-index: 3;
  text-transform: uppercase;
  backdrop-filter: blur(2px);
}
/* If a post is both NSFW and pinned, nudge the pinned badge below the NSFW one */
.nsfw-badge + .pinned-badge { top: 28px; }

/* Poll/Story badge — top-right, opposite corner from NSFW/pinned so they never collide */
.post-type-badge {
  position: absolute;
  top: 7px;
  left: 7px;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  font-size: 9px;
  font-weight: 700;
  padding: 2px 7px;
  border-radius: 4px;
  letter-spacing: 0.06em;
  z-index: 3;
  text-transform: uppercase;
  backdrop-filter: blur(2px);
  display: flex;
  align-items: center;
  gap: 4px;
}
/* Stack below NSFW/pinned if either is present (same left-side column) */
.nsfw-badge ~ .post-type-badge, .pinned-badge ~ .post-type-badge { top: 28px; }
.nsfw-badge ~ .pinned-badge ~ .post-type-badge { top: 49px; }
.post-badge-poll  { background: rgba(108, 92, 231, 0.85); }
.post-badge-story { background: rgba(0, 0, 0, 0.55); }
.post-badge-local { background: rgba(0, 184, 148, 0.9); }
/* A Local echo can also be a Poll or Story, so the two badges stack rather than overlap. */
.post-type-badge + .post-badge-local { top: 28px; }
.nsfw-badge ~ .post-type-badge + .post-badge-local,
.pinned-badge ~ .post-type-badge + .post-badge-local { top: 49px; }

/* Story feed-card preview — deliberately NOT auto-fit (see renderPosts()'s contentHtml
   comment): truncated with a fade instead of shrinking to cram the full text in. */
.post-content-story {
  flex: 1;
  /* Top padding clears the "STORY" badge + ⋮ menu button, which both float over this card at
     top:7px — without it the first line of text renders right underneath/behind them. */
  padding: 34px 16px 16px;
  overflow: hidden;
  position: relative;
  z-index: 2;   /* sit above the readability scrim (.post-card.post-img::before) so the text
                   isn't dimmed by it — same as .post-content does for Echoes. */
}
.post-content-story span {
  display: block;
  /* Text simply fills the available card space and is hard-clipped at the bottom by the
     parent's overflow:hidden — no fade. A short story shows in full; a long one fills the card
     and cuts off cleanly (the full text is always available in the thread view). */
  font-size: 0.9rem;
  line-height: 1.55;
  color: #fff;
  font-weight: 500;
  text-shadow: 0 1px 6px rgba(0,0,0,0.55);  /* crisp white against the busy background */
  word-break: break-word;
}

/* Posts held for moderation review */
.post-pending {
  position: relative;
  background: repeating-linear-gradient(45deg, #2a2a3e, #2a2a3e 10px, #30304a 10px, #30304a 20px);
  cursor: default;
}
.post-review-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  text-align: center;
  padding: 16px;
  color: #cfcfe6;
  background: rgba(17, 17, 32, 0.78);
  font-size: 0.85rem;
  font-weight: 600;
}
.post-review-overlay i { font-size: 1.5rem; opacity: 0.8; }

/* Moderator Panel → Review tab: pending posts rendered as the real echo card */
.pending-review-item {
  background: var(--bg2);
  border-radius: var(--radius-sm);
  padding: 14px;
  margin-bottom: 12px;
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
}
.pending-review-card-wrap {
  max-width: 240px;
  margin: 4px auto 6px;
}
.pending-review-card-wrap .post-card.post-img { cursor: zoom-in; }

.post-menu-btn {
  position: absolute;
  top: 6px;
  right: 6px;
  z-index: 3;
  width: 26px;
  height: 26px;
  background: rgba(0,0,0,0.5);
  border: none;
  border-radius: 50%;
  color: #fff;
  font-size: 12px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  backdrop-filter: blur(4px);
}

.post-nsfw-overlay {
  position: absolute;
  inset: 0;
  background: rgba(0,0,0,0.72);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: 1px;
  /* Was z-index:2 — tied with .post-content/.thread-post-text (also z-index:2), and with equal
     z-index the LATER element in the DOM wins the stack regardless of intent. The overlay markup
     comes before the content in renderPosts()/renderThreadParent(), so the actual text was
     winning and rendering on top of the blur instead of being hidden behind it. Bumped above
     content but still below the menu/expand buttons (z-index:3), which should stay tappable
     (report/delete) even on a blurred NSFW card. */
  z-index: 3;
  cursor: pointer;
  border-radius: 10px;
}

.post-content {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  /* extra top padding clears the Pinned/NSFW badge + menu button so text isn't covered */
  padding: 34px 10px 12px;
  text-align: center;
  font-size: 0.82rem;
  font-weight: 700;
  line-height: 1.4;
  text-shadow: 0 1px 6px rgba(0,0,0,0.55);
  word-break: break-word;
  overflow: hidden;
  position: relative;
  z-index: 2;              /* sit above the readability scrim */
}
.post-content .fit { display: block; width: 100%; }
.posts-grid.layout-full .post-content { padding: 38px 16px 16px; }

.post-group-banner {
  background: rgba(0,0,0,0.45);
  color: #fff;
  font-size: 0.62rem;
  font-weight: 600;
  padding: 3px 8px;
  display: flex;
  align-items: center;
  gap: 4px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex-shrink: 0;
}
.post-group-banner i { font-size: 0.58rem; opacity: 0.85; }

.post-footer {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  min-height: 70px;
  background: linear-gradient(transparent, rgba(0,0,0,0.65));
  flex-shrink: 0;
  position: relative;
  z-index: 2;            /* above the readability scrim */
}
.post-footer-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 3px 8px 7px;
  gap: 4px;
}
.posts-grid.layout-full .post-footer { min-height: 80px; }
.posts-grid.layout-full .post-footer-bar { padding: 4px 14px 10px; }

.post-meta {
  opacity: .85;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1;
  font-size: 0.62rem;
}
.posts-grid.layout-full .post-meta { font-size: 0.75rem; }

.post-actions { display: flex; gap: 8px; align-items: center; flex-shrink: 0; }
.post-actions button {
  background: none;
  border: none;
  color: #fff;
  font-size: 0.72rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 3px;
  padding: 2px;
  opacity: .9;
  font-family: inherit;
}
.posts-grid.layout-full .post-actions button { font-size: 0.85rem; gap: 4px; }
.post-actions button:active { opacity: 1; transform: scale(1.15); }
.post-actions .starred { color: #ffd700; }
.post-action-count {
  display: flex; align-items: center; gap: 3px;
  font-size: 0.72rem; color: rgba(255,255,255,0.75);
  pointer-events: none;
}

/* === Splash / Loading === */
.splash {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 60vh;
}
.spinner {
  width: 40px; height: 40px;
  border: 3px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.empty-state {
  text-align: center;
  padding: 60px 20px;
  color: var(--muted);
}
.empty-state i { font-size: 3rem; margin-bottom: 16px; display: block; }
.empty-state p { font-size: 0.95rem; margin: 0; }
.blocked-notice p + p { margin-top: 12px; }
.blocked-notice p.muted { font-size: 0.82rem; }
.blocked-notice a { color: var(--accent); }

/* === Modals === */
.full-modal, .sheet-modal {
  position: fixed;
  left: 0; right: 0;
  background: var(--bg2);
  z-index: 500;
  display: flex;
  flex-direction: column;
  transform: translateY(100%);
  /* translateY(100%) is relative to the element's OWN height (var(--vvh)), which changes the
     instant the on-screen keyboard resizes the screen. That recalculation can animate or land
     mid-transition, leaving a "closed" modal poking visibly into view even without the .open
     class. visibility is a hard, non-animated guarantee underneath the transform — delayed on
     close so the slide-down animation still plays, instant on open. */
  visibility: hidden;
  transition: transform .35s cubic-bezier(.4,0,.2,1), visibility 0s linear .35s;
  /* dvh/svh as fallbacks for the instant before app.js runs (or if JS fails) — var(--vvh) is the
     REAL measured visible height via visualViewport, which is what actually wins once set,
     since iOS Safari's dvh/svh units don't reliably track the true visible area for
     position:fixed elements as the address bar shows/hides. */
  max-height: 100dvh;
  max-height: 100svh;
  max-height: var(--vvh, 100svh);
  overflow-x: hidden;
}
.full-modal {
  top: 0;
  height: 100dvh;
  height: 100svh;
  height: var(--vvh, 100svh);
  border-radius: 0;
  /* --nav-reserve (set by app.js) is the nav's footprint — just its own height, since
     #bottom-tabs is bottom:0 with a flat height:var(--nav-h) and does NOT reserve extra space
     for the safe area itself (that's a purely decorative fill strip below it) — but ONLY while
     the nav is actually visible. When the on-screen keyboard is up, the nav is hidden
     underneath it, so app.js sets this to 0 instead of wasting that space above the keyboard.
     The ad banner floats independently above the nav now (see .ad-banner) and no longer
     factors into this at all. */
  padding-bottom: var(--nav-reserve, var(--nav-h));
}
.sheet-modal {
  /* --kb-inset (set by app.js) is the on-screen keyboard's height. position:fixed anchors to the
     LAYOUT viewport, which the keyboard does not shrink, so bottom:0 put the lower part of the
     sheet behind the keyboard — you could drag it into view but it snapped back on release, since
     there was no further scroll extent. Sitting the sheet on top of the keyboard fixes it. */
  bottom: var(--kb-inset, 0px);
  border-radius: 20px 20px 0 0;
  /* Cap to the actually-visible height too, or a 70%-of-full-screen sheet still overflows the
     shrunken area once the keyboard is up. Plain 70dvh first as the pre-JS/no-min() fallback. */
  max-height: 70dvh;
  max-height: min(70vh, var(--vvh, 70vh));
  /* #bottom-tabs sits at z-index:600, above this sheet's z-index:500, so its last row(s) were
     rendering underneath the nav bar — padding needs to clear the nav's own height, not just
     the safe-area strip below it (see the #bottom-tabs notes above for why those are separate). */
  padding-bottom: calc(var(--nav-reserve, var(--nav-h)) + env(safe-area-inset-bottom,0));
}
.full-modal.open, .sheet-modal.open {
  transform: translateY(0);
  visibility: visible;
  transition: transform .35s cubic-bezier(.4,0,.2,1), visibility 0s linear;
}

.modal-header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: calc(env(safe-area-inset-top,0px) + 14px) 16px 14px;
  border-bottom: 1px solid var(--border);
  background: var(--bg2);
  flex-shrink: 0;
}
/* iOS native already insets fixed layers via contentInset — see the #top-nav note above. */
.is-ios .modal-header { padding-top: 14px; }
.sheet-modal .modal-header { padding: 16px; border-radius: 20px 20px 0 0; }
.modal-header h2 { flex: 1; margin: 0; font-size: 1.05rem; font-weight: 700; }

.modal-scroll {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  padding: 10px 14px;
}

.full-modal, .sheet-modal {
  overflow-x: hidden;
}

/* === Buttons === */
.btn-primary {
  background: var(--accent);
  color: #fff;
  border: none;
  padding: 9px 20px;
  border-radius: 20px;
  font-weight: 700;
  font-size: 0.88rem;
  cursor: pointer;
  font-family: inherit;
  white-space: nowrap;
}
.btn-primary:disabled { opacity: .5; cursor: not-allowed; }
.btn-secondary {
  background: var(--bg3);
  color: var(--text);
  border: 1px solid var(--border);
  padding: 9px 18px;
  border-radius: 20px;
  font-weight: 600;
  font-size: 0.88rem;
  cursor: pointer;
  font-family: inherit;
}
.btn-secondary.sm { padding: 6px 14px; font-size: 0.8rem; }

/* === Color swatch (text/background pickers) ===
   Web/PWA: the swatch button sits invisibly on top of the real <input type=color> and never
   intercepts taps — the browser's own color dialog opens as normal.
   Native apps (.is-native-app on <html>): the OS color dialog looks jarring next to EchoX's UI,
   so the input becomes inert and the swatch button takes over, opening our own picker sheet. */
.color-swatch-wrap { position: relative; width: 40px; height: 32px; }
.color-swatch-wrap input[type="color"] {
  width: 40px; height: 32px; padding: 0; border: 1px solid var(--border); border-radius: 8px;
  background: none; cursor: pointer;
}
.color-swatch-btn {
  position: absolute; inset: 0;
  border: 1px solid var(--border); border-radius: 8px;
  background: var(--bg3);
  cursor: pointer;
  pointer-events: none; /* web/PWA: let taps fall through to the real input */
}
.is-native-app .color-swatch-wrap input[type="color"] { pointer-events: none; }
.is-native-app .color-swatch-btn { pointer-events: auto; }

/* Saturation/value square — hue's rainbow backdrop is set inline (depends on the picked hue);
   the white-to-transparent and black-to-transparent layers here are the constant part. */
.color-picker-sv {
  position: relative;
  width: 100%;
  height: 180px;
  border-radius: 14px;
  margin-bottom: 16px;
  touch-action: none;
  border: 1px solid var(--border);
}
.color-picker-sv-cursor {
  position: absolute;
  width: 22px; height: 22px;
  border-radius: 50%;
  border: 3px solid #fff;
  box-shadow: 0 0 0 1px rgba(0,0,0,.35), 0 1px 4px rgba(0,0,0,.4);
  transform: translate(-50%, -50%);
  pointer-events: none;
}
.color-picker-hue-row {
  display: flex; align-items: center; gap: 14px;
  margin-bottom: 20px;
}
.color-picker-swatch-circle {
  width: 34px; height: 34px; border-radius: 50%;
  border: 1px solid var(--border);
  flex-shrink: 0;
}
.color-picker-hue {
  position: relative;
  flex: 1;
  height: 14px;
  border-radius: 7px;
  touch-action: none;
  background: linear-gradient(to right,
    #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
}
.color-picker-hue-cursor {
  position: absolute;
  top: 50%;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: #fff;
  border: 3px solid #fff;
  box-shadow: 0 0 0 1px rgba(0,0,0,.35), 0 1px 4px rgba(0,0,0,.4);
  transform: translate(-50%, -50%);
  pointer-events: none;
}
.color-picker-rgb-row {
  display: flex; gap: 12px;
  margin-bottom: 20px;
}
.color-picker-rgb-field {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}
.color-picker-rgb-field input {
  width: 100%;
  text-align: center;
  padding: 9px 6px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg3);
  color: var(--text);
  font-family: 'Roboto Mono', monospace;
}
.color-picker-rgb-field label { font-weight: 600; color: var(--text2); font-size: 0.8rem; }
.color-picker-actions {
  display: flex; justify-content: flex-end; gap: 10px;
}
.btn-icon {
  background: none;
  border: none;
  width: 38px; height: 38px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.1rem;
  color: var(--text2);
  cursor: pointer;
  flex-shrink: 0;
  transition: background .15s;
}
.btn-icon:hover { background: var(--bg3); }
.btn-danger {
  background: var(--danger);
  color: #fff;
  border: none;
  padding: 9px 18px;
  border-radius: 20px;
  font-weight: 700;
  font-size: 0.85rem;
  cursor: pointer;
  font-family: inherit;
}

/* === Forms === */
.form-group { margin-bottom: 20px; }
.form-group label {
  display: block;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text2);
  margin-bottom: 7px;
}
.form-group input[type=text],
.form-group select,
.form-group textarea {
  width: 100%;
  padding: 12px 14px;
  background: var(--bg3);
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-family: inherit;
  font-size: 0.95rem;
  transition: border-color .2s;
  outline: none;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus { border-color: var(--accent); }
.form-group textarea { height: 90px; resize: none; }
.field-note { font-size: 0.78rem; color: var(--muted); margin: 6px 0 0; }
.field-note.warning { color: var(--warning); }
/* The 7-day age/gender lock notice sits ABOVE its select (you need it before you choose, not
   after), so it needs bottom margin rather than the default top. */
.form-group > .field-note.warning:first-of-type { margin: 0 0 8px; line-height: 1.45; }
.field-note.warning i { margin-right: 4px; }

/* === Post Modal === */
/* === Post composer: 3-step flow === */
.composer-step-3 {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}
.composer-dest-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.composer-dest-btn {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 14px 16px;
  border-radius: var(--radius-sm);
  border: 1.5px solid var(--border);
  background: var(--bg2);
  color: var(--text);
  font-family: inherit;
  font-size: 0.95rem;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.composer-dest-btn i { color: var(--muted); width: 18px; text-align: center; }
.composer-dest-btn.active { border-color: var(--accent); background: var(--accent-dim); }
.composer-dest-btn.active i { color: var(--accent); }
/* "Local only" carries a second line explaining its reach — the row has to wrap for it, and the
   note needs the icon's column width so it lines up under the label rather than the icon. */
.composer-dest-btn { flex-wrap: wrap; }
.composer-dest-note {
  flex-basis: 100%;
  margin-left: 28px;
  font-size: 0.74rem;
  font-weight: 400;
  color: var(--muted);
  line-height: 1.35;
}
.composer-dest-btn.is-disabled { opacity: 0.55; cursor: not-allowed; }

/* Step 1's Echo/Poll/Story picker */
.composer-type-row {
  display: flex;
  gap: 8px;
  margin-top: 8px;
}
.composer-type-btn {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 14px 8px;
  border-radius: var(--radius-sm);
  border: 1.5px solid var(--border);
  background: var(--bg2);
  color: var(--text2);
  font-family: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  cursor: pointer;
}
.composer-type-btn i { font-size: 1.1rem; color: var(--muted); }
.composer-type-btn.active { border-color: var(--accent); background: var(--accent-dim); color: var(--text); }
.composer-type-btn.active i { color: var(--accent); }

.post-preview-box {
  margin: 0 auto;
  align-self: center;
  flex-shrink: 0;
  height: 40vh;
  aspect-ratio: 2/3;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  background-color: var(--accent);
  background-size: cover;
  background-position: center;
  overflow: hidden;
}
/* Match the feed card's readability scrim while composing over an image */
.post-preview-box.has-img::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0,0,0,0.30) 0%, rgba(0,0,0,0.45) 100%);
  z-index: 1;
  pointer-events: none;
}
/* Read-only step-3 preview text — autoFitText() (app.js) sizes the font, this just centers
   and styles it to match how it'll actually look once posted. */
.post-preview-box .fit {
  z-index: 2;
  color: #fff;
  font-weight: 700;
  font-family: inherit;
  text-align: center;
  width: 100%;
  padding: 20px 16px;
  text-shadow: 0 2px 8px rgba(0,0,0,0.5);
  word-break: break-word;
  display: block;
}

/* Step 2 — plain text entry box, no preview styling since that happens in step 3 */
.composer-text-box {
  position: relative;
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  min-height: 160px;
  display: flex;
}
.composer-text-box textarea {
  background: transparent;
  border: none;
  color: var(--text);
  font-size: 1.1rem;
  font-family: inherit;
  width: 100%;
  padding: 16px 16px 28px;
  resize: none;
  outline: none;
}
#char-counter, #poll-char-counter {
  position: absolute;
  bottom: 10px;
  right: 14px;
  font-size: 0.72rem;
  color: var(--muted);
  font-weight: 600;
  pointer-events: none;
}

/* Step 2's poll builder — question textarea reuses .composer-text-box, this is just the
   dynamic option-input list + duration picker below it. */
.composer-poll-options {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 14px;
}
.composer-poll-option-row {
  display: flex;
  align-items: center;
  gap: 8px;
}
.composer-poll-option-row .poll-option-input {
  flex: 1;
  padding: 12px 14px;
  border-radius: var(--radius-sm);
  border: 1.5px solid var(--border);
  background: var(--bg3);
  color: var(--text);
  font-family: inherit;
  font-size: 0.92rem;
}
#poll-add-option-btn { margin-top: 10px; width: 100%; }
.composer-poll-duration {
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 12px;
  font-family: inherit;
  color: var(--text);
}

/* Font picker row */
.font-option {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  padding: 4px 0;
}
.font-btn {
  padding: 5px 10px;
  border-radius: 16px;
  border: 1.5px solid var(--border);
  background: var(--card-bg);
  color: var(--text-primary);
  font-size: 12px;
  cursor: pointer;
  transition: border-color .15s, background .15s;
}
.font-btn.active {
  border-color: var(--accent);
  background: var(--accent);
  color: #fff;
}
.option-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
  font-size: 0.82rem;
  font-weight: 500;
}
.option-row:last-child { border-bottom: none; }
.option-row > label:first-child { color: var(--text2); flex-shrink: 0; min-width: 72px; }
.option-row select, .option-row input[type=color] {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 4px 8px;
  background: var(--bg3);
  color: var(--text);
  font-family: inherit;
  font-size: 0.82rem;
}
.option-row input[type=color] { width: 42px; height: 30px; padding: 2px 4px; cursor: pointer; }
.nsfw-label { display: flex; align-items: center; gap: 8px; cursor: pointer; color: var(--danger); font-weight: 600; }

/* === Discover === */
.search-bar {
  display: flex;
  align-items: center;
  background: var(--bg2);
  border-radius: var(--radius-sm);
  border: 1.5px solid var(--border);
  padding: 0 14px;
  gap: 8px;
  flex: 1;
}
.search-bar input {
  border: none;
  background: none;
  padding: 11px 0;
  flex: 1;
  font-family: inherit;
  font-size: 0.9rem;
  color: var(--text);
  outline: none;
}
.search-bar i { color: var(--muted); }

.discover-header {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 16px;
}

.discover-section-title {
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  margin: 0 0 10px;
}

/* Horizontal scroll row of group pills */
.groups-row {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  padding-bottom: 10px;
  margin-bottom: 20px;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  overscroll-behavior-x: contain;
}
.groups-row::-webkit-scrollbar { display: none; }

.group-pill {
  flex-shrink: 0;
  width: 90px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  cursor: pointer;
  padding: 10px 6px;
  border-radius: var(--radius-sm);
  background: var(--bg2);
  border: 1px solid var(--border);
  transition: transform .15s;
}
.group-pill:active { transform: scale(0.95); }
.group-pill-avatar {
  width: 44px; height: 44px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--accent), var(--accent-light));
  color: #fff;
  font-size: 1.2rem;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}
.group-pill-name {
  font-size: 0.72rem;
  font-weight: 700;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  width: 100%;
  color: var(--text);
}
.group-pill-meta {
  font-size: 0.62rem;
  color: var(--muted);
  text-align: center;
}

.community-card {
  background: var(--bg2);
  border-radius: var(--radius-sm);
  padding: 16px;
  margin-bottom: 12px;
  display: flex;
  align-items: center;
  gap: 14px;
  box-shadow: var(--shadow);
  cursor: pointer;
  transition: transform .15s;
  border: 1px solid var(--border);
}
.community-card:active { transform: scale(0.98); }
.community-card .comm-avatar {
  width: 48px; height: 48px;
  border-radius: 14px;
  background: linear-gradient(135deg, var(--accent), var(--accent-light));
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-size: 1.3rem;
  font-weight: 700;
  flex-shrink: 0;
}
.comm-info { flex: 1; min-width: 0; }
.comm-info strong { display: block; font-size: 0.95rem; }
.comm-info .comm-meta { font-size: 0.75rem; color: var(--accent); margin: 2px 0; }
.comm-info p { font-size: 0.8rem; color: var(--muted); margin: 4px 0 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }


/* === Account === */
.account-hero {
  background: linear-gradient(135deg, var(--accent) 0%, var(--accent-light) 100%);
  border-radius: var(--radius);
  padding: 20px 24px 24px;
  color: #fff;
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-bottom: 20px;
}
.account-username {
  margin: 0;
  font-size: 1.25rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.account-hero-row {
  display: flex;
  align-items: center;
  gap: 14px;
}
.account-avatar-wrap {
  position: relative;
  flex-shrink: 0;
  cursor: pointer;
}
.account-avatar-wrap .avatar-gen,
.account-avatar-wrap .avatar-img {
  border: 3px solid rgba(255,255,255,0.4);
}
.avatar-edit-badge {
  position: absolute;
  bottom: 0; right: 0;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: rgba(0,0,0,0.55);
  color: #fff;
  font-size: 0.65rem;
  display: flex; align-items: center; justify-content: center;
  border: 2px solid rgba(255,255,255,0.5);
}
.account-info { flex: 1; min-width: 0; }
.account-info p  { margin: 0; font-size: 0.8rem; opacity: .85; }
/* Followers, under the age/gender/location line. Full opacity so the count reads as a real stat
   rather than more grey metadata — it's the only number left in the hero. */
.account-followers {
  margin-top: 6px !important;
  opacity: 1 !important;
  display: flex;
  align-items: center;
  gap: 6px;
}
.account-followers strong { font-weight: 700; }
.account-followers i { opacity: 0.7; font-size: 0.75rem; }
.account-actions { flex-shrink: 0; margin-left: auto; display: flex; gap: 6px; align-self: flex-start; }
.account-actions button {
  background: rgba(255,255,255,0.2);
  border: none;
  color: #fff;
  width: 34px; height: 34px;
  border-radius: 50%;
  font-size: 1rem;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}

/* Moderation queue badge on the Moderator Panel shield icon */
.mod-badge {
  position: absolute;
  top: -3px;
  right: -3px;
  background: var(--danger);
  color: #fff;
  font-size: 0.62rem;
  font-weight: 700;
  min-width: 15px;
  height: 15px;
  border-radius: 8px;
  padding: 0 3px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--accent);
}

/* .stats-row / .stat-card removed with the Account screen's four stat cards — Echoes, Stars and
   Following are tabs directly above that row, so the cards restated three of the four. The one
   number without a tab, Followers, moved into the hero as .account-followers. */

.section-title {
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .8px;
  margin: 20px 0 10px;
}

.app-version {
  text-align: center;
  font-size: 0.72rem;
  color: var(--muted);
  padding: 24px 0 16px;
  opacity: 0.5;
}

/* === Notifications === */
.notif-card {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 14px;
  background: var(--bg2);
  border-radius: var(--radius-sm);
  margin-bottom: 10px;
  border: 1px solid var(--border);
  cursor: pointer;
  transition: background .15s;
}
.notif-card.unread { border-left: 3px solid var(--accent); background: var(--accent-dim); }
.notif-card.tappable { cursor: pointer; }
.notif-card.tappable:active { background: var(--bg3); }
.notif-icon {
  width: 38px; height: 38px;
  border-radius: 50%;
  background: var(--accent-dim);
  color: var(--accent);
  display: flex; align-items: center; justify-content: center;
  font-size: 1rem;
  flex-shrink: 0;
}
.notif-content p   { margin: 0; font-size: 0.88rem; font-weight: 500; }
.notif-content span { font-size: 0.75rem; color: var(--muted); }

/* === Settings === */
.settings-section { margin-bottom: 24px; }
.settings-section-title {
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .8px;
  color: var(--muted);
  margin: 0 0 8px;
}
.settings-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 14px 16px;
  background: var(--bg2);
  border-bottom: 1px solid var(--border);
  font-size: 0.9rem;
  cursor: pointer;
}
.settings-row:first-of-type { border-radius: var(--radius-sm) var(--radius-sm) 0 0; }
.settings-row:last-of-type  { border-radius: 0 0 var(--radius-sm) var(--radius-sm); border-bottom: none; }
.settings-row:only-child    { border-radius: var(--radius-sm); }
.settings-label { display: flex; flex-direction: column; }
.settings-label small { font-size: 0.75rem; color: var(--muted); margin-top: 2px; }

/* Toggle switch */
.toggle {
  position: relative;
  width: 46px; height: 26px;
  flex-shrink: 0;
}
.toggle input { opacity: 0; width: 0; height: 0; }
.toggle-slider {
  position: absolute;
  inset: 0;
  background: var(--border);
  border-radius: 13px;
  transition: background .2s;
  cursor: pointer;
}
.toggle-slider::before {
  content: '';
  position: absolute;
  top: 3px; left: 3px;
  width: 20px; height: 20px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 4px rgba(0,0,0,0.2);
  transition: transform .2s;
}
.toggle input:checked + .toggle-slider { background: var(--accent); }
.toggle input:checked + .toggle-slider::before { transform: translateX(20px); }

/* Recovery key box */
.recovery-key-box {
  background: var(--bg3);
  border: 1px dashed var(--border);
  border-radius: var(--radius-sm);
  padding: 14px;
  font-family: monospace;
  font-size: 0.8rem;
  word-break: break-all;
  color: var(--text2);
  margin: 10px 0;
}

/* === Chat === */
.chat-list-item {
  display: flex;
  align-items: center;
  gap: 13px;
  padding: 14px 0;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  transition: background .1s;
  position: relative;
}
/* === Chat list multi-select === */
.chat-list-tools { display: flex; justify-content: flex-end; padding: 6px 0 2px; }

.chat-list-item.is-selectable { padding-left: 4px; }
.chat-list-item.is-selected   { background: var(--accent-dim); }
.chat-select-box {
  flex-shrink: 0;
  width: 22px; height: 22px;
  border-radius: 50%;
  border: 2px solid var(--border);
  display: flex; align-items: center; justify-content: center;
  font-size: 11px;
  color: transparent;   /* the tick only appears once selected */
  transition: background .12s, border-color .12s;
}
.chat-list-item.is-selected .chat-select-box {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

/* Floats above the bottom nav (and above the ad strip, which sits at z-index 90). */
.chat-select-bar {
  position: fixed;
  bottom: var(--nav-h);
  left: 0; right: 0;
  z-index: 130;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  background: var(--bg2);
  border-top: 1px solid var(--border);
  box-shadow: 0 -4px 18px rgba(0,0,0,0.18);
}
.chat-select-count { flex: 1; font-size: 0.82rem; font-weight: 600; color: var(--text2); }
.chat-select-bar button:disabled { opacity: 0.45; }

.chat-avatar {
  width: 48px; height: 48px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--accent), var(--accent-light));
  color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-size: 1.2rem;
  font-weight: 700;
  flex-shrink: 0;
  position: relative;
}
.chat-unread-dot {
  position: absolute;
  top: 0; right: 0;
  width: 12px; height: 12px;
  background: var(--danger);
  border-radius: 50%;
  border: 2px solid var(--bg2);
}
.chat-item-info { flex: 1; min-width: 0; }
.chat-item-info strong { display: block; font-size: 0.92rem; }
.chat-item-info span   { font-size: 0.78rem; color: var(--muted); display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chat-item-meta { text-align: right; flex-shrink: 0; }
.chat-item-meta .time { font-size: 0.72rem; color: var(--muted); }
.chat-unread-count {
  background: var(--accent);
  color: #fff;
  font-size: 0.7rem;
  font-weight: 700;
  min-width: 18px; height: 18px;
  border-radius: 9px;
  padding: 0 5px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-top: 4px;
}
.chat-fav-star { color: var(--warning); font-size: 0.8rem; }

/* Swipe-to-reveal actions on each chat list row — the row itself slides left via transform
   (handled in JS) to expose these two buttons, which sit underneath/behind it at all times. */
.chat-swipe-wrap { position: relative; overflow: hidden; }
.chat-swipe-wrap .chat-list-item {
  background: var(--bg2);
  touch-action: pan-y;
}
.chat-swipe-actions {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: 140px;
  display: flex;
}
.chat-swipe-btn {
  flex: 1;
  border: none;
  color: #fff;
  font-size: 1.05rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.chat-swipe-btn.fav    { background: var(--warning); }
.chat-swipe-btn.delete { background: var(--danger); }

.chat-modal-header { padding-top: calc(env(safe-area-inset-top,0px) + 10px) !important; }
.is-ios .chat-modal-header { padding-top: 10px !important; }

.chat-modal-title-area {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  overflow: hidden;
}
.chat-title-info { flex: 1; min-width: 0; overflow: hidden; }
.chat-title-info strong { display: block; font-size: 0.95rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chat-title-info span   { display: block; font-size: 0.72rem; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.chat-media-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  padding: 4px 6px;
  min-width: 42px;
}
.chat-media-label {
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  line-height: 1;
  color: inherit;
}

.chat-messages {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  -webkit-overflow-scrolling: touch;
}
.msg-bubble {
  max-width: 78%;
  padding: 10px 14px;
  border-radius: 18px;
  font-size: 0.9rem;
  line-height: 1.45;
  word-break: break-word;
  position: relative;
  /* Press-and-hold opens the tapback picker (see _msgPressStart in app.js) — without this,
     the same gesture also triggers the browser's native text-selection/magnifier on mobile,
     fighting with the picker for the same long-press. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
.msg-bubble.me {
  background: var(--accent);
  color: #fff;
  align-self: flex-end;
  border-bottom-right-radius: 4px;
}
.msg-bubble.them {
  background: var(--bg3);
  color: var(--text);
  align-self: flex-start;
  border-bottom-left-radius: 4px;
  border: 1px solid var(--border);
}
.msg-system-notice {
  align-self: center;
  color: var(--text2);
  font-size: 0.8rem;
  font-style: italic;
  text-align: center;
  padding: 6px 12px;
}
.msg-system-notice i { margin-right: 4px; }
.msg-time { font-size: 0.65rem; opacity: .7; margin-top: 4px; display: block; }
/* "+" tapback hint on received bubbles — sits just past the right edge, vertically centered.
   Faint by default so it's discoverable on touch without shouting; brightens on hover/press. */
.msg-tapback-add {
  position: absolute;
  right: -28px;
  top: 50%;
  transform: translateY(-50%);
  width: 22px;
  height: 22px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--bg2);
  color: var(--text2);
  font-size: 0.62rem;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  opacity: 0.5;
  transition: opacity .15s, background .15s, color .15s, border-color .15s;
  -webkit-user-select: none;
  user-select: none;
}
.msg-tapback-add:hover,
.msg-tapback-add:active {
  opacity: 1;
  background: var(--accent);
  color: #fff;
  border-color: var(--accent);
}
.msg-media img, .msg-media video {
  max-width: 220px;
  border-radius: 12px;
  display: block;
  margin-bottom: 4px;
}
.msg-gif img { max-width: 200px; border-radius: 10px; display: block; margin-bottom: 4px; }

/* Tapbacks — double-click or press-and-hold a bubble to react (see chatMsgHtml in app.js).
   The pill sits above the bubble, so a bubble that actually has reactions needs real reserved
   space above it (margin-top) — an offset alone just overlaps the bubble's own text. */
.msg-bubble.has-reactions { margin-top: 20px; }
.msg-reactions {
  position: absolute;
  top: -20px;
  display: flex;
  gap: 3px;
}
.msg-bubble.me .msg-reactions   { left: 8px; }
.msg-bubble.them .msg-reactions { right: 8px; }
.msg-reaction-pill {
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: 10px;
  font-size: 0.72rem;
  padding: 1px 5px;
  box-shadow: var(--shadow);
  color: var(--text);
}
@media (min-width: 600px) {
  .msg-reaction-pill { font-size: 0.9rem; padding: 3px 8px; border-radius: 12px; }
}
.msg-reaction-pill.mine { border-color: var(--accent); }
.msg-reaction-picker {
  position: absolute;
  top: -44px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 2px;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: 4px 6px;
  box-shadow: var(--shadow);
  z-index: 50;
  white-space: nowrap;
}
.msg-reaction-picker button {
  background: none;
  border: none;
  font-size: 1.15rem;
  padding: 2px 4px;
  cursor: pointer;
  line-height: 1;
}
.msg-reaction-picker button:active { transform: scale(1.2); }

.chat-input-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 10px 12px;
  padding-bottom: calc(10px + env(safe-area-inset-bottom,0px));
  background: var(--bg2);
  border-top: 1px solid var(--border);
  flex-shrink: 0;
}
#chat-input {
  flex: 1;
  min-width: 0;
  background: var(--bg3);
  border: 1.5px solid var(--border);
  border-radius: 20px;
  padding: 10px 14px;
  font-family: inherit;
  font-size: 16px; /* must be ≥16px to prevent iOS auto-zoom on focus */
  color: var(--text);
  outline: none;
}
#chat-input:focus { border-color: var(--accent); }
.send-btn { color: var(--accent) !important; }
.gif-btn { font-size: 0; }
.gif-label { font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; color: var(--accent); border: 1.5px solid var(--accent); border-radius: 4px; padding: 1px 4px; }

.following-row { display:flex; align-items:center; gap:10px; padding:10px 0; border-bottom:1px solid var(--border); }
.following-name { flex:1; font-size:0.9rem; font-weight:500; cursor:pointer; color:var(--accent); }

.gif-picker {
  background: var(--bg2);
  border-top: 1px solid var(--border);
  padding: 10px;
  flex-shrink: 0;
}
.gif-picker input {
  width: 100%;
  background: var(--bg3);
  border: 1.5px solid var(--border);
  border-radius: 20px;
  padding: 8px 14px;
  font-family: inherit;
  font-size: 0.85rem;
  color: var(--text);
  outline: none;
  margin-bottom: 8px;
}
.gif-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
  max-height: 200px;
  overflow-y: auto;
}
.gif-grid img {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 8px;
  cursor: pointer;
  transition: transform .15s;
}
.gif-grid img:active { transform: scale(0.95); }

/* Required GIPHY attribution wherever GIF results are browsed — official mark, self-contained
   black badge so it stays legible on both the light and dark themes without swapping assets. */
.giphy-attribution {
  text-align: center;
  padding: 6px 0 2px;
}
.giphy-attribution img { height: 20px; width: auto; border-radius: 4px; }

/* Post background search — a full-screen layer over the whole composer step-3 view (not
   squeezed into it), so the preview never has to shrink/rescale while searching. `inset:0`
   would ignore the modal's own reserved space for the nav bar (absolute positioning resolves
   against the padding box, which includes that reserved padding) and let this panel's content
   run behind the nav — bottom must mirror the same --nav-reserve the modal itself uses. */
.post-search-panel {
  position: absolute;
  top: 0; left: 0; right: 0;
  bottom: var(--nav-reserve, var(--nav-h));
  z-index: 50;
  background: var(--bg2);
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.post-search-panel.hidden { display: none; }
.post-search-tabs {
  display: flex;
  gap: 6px;
  padding: 10px 12px 0;
  flex-shrink: 0;
}
.post-search-tab {
  flex: 1;
  padding: 8px 0;
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text2);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: background .15s, color .15s;
}
.post-search-tab.active {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.post-search-input {
  width: 100%;
  padding: 10px 14px;
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: 20px;
  color: var(--text);
  font-size: 14px;
  outline: none;
  margin: 10px 12px;
  box-sizing: border-box;
  width: calc(100% - 24px);
}
.post-search-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 4px;
  padding: 0 12px;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}
@media (min-width: 600px) {
  .post-search-grid {
    grid-template-columns: repeat(8, 1fr);
    max-height: 300px;
  }
}
.post-search-grid img {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 4px;
  cursor: pointer;
  transition: transform .15s, outline .15s;
  outline: 3px solid transparent;
}
.post-search-grid img:active { transform: scale(0.95); }
.post-search-grid img.selected { outline-color: var(--accent); }
.post-search-more {
  display: block;
  width: 100%;
  margin-top: 8px;
  padding: 8px;
  text-align: center;
  font-size: 13px;
}

/* Chat options sheet */
.action-list { display: flex; flex-direction: column; gap: 4px; }
.action-item {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px 0;
  font-size: 0.92rem;
  cursor: pointer;
  border-bottom: 1px solid var(--border);
  color: var(--text);
}
.action-item:last-child { border-bottom: none; }
.action-item i { width: 22px; text-align: center; color: var(--accent); }
.action-item.danger { color: var(--danger); }
.action-item.danger i { color: var(--danger); }

/* === Report options === */
.report-options { display: flex; flex-direction: column; gap: 6px; margin-top: 12px; }
.report-opt {
  padding: 13px 16px;
  background: var(--bg3);
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  text-align: left;
  font-family: inherit;
  font-size: 0.9rem;
  color: var(--text);
  cursor: pointer;
  transition: background .15s, border-color .15s;
}
.report-opt:hover { background: var(--accent-dim); border-color: var(--accent); }

/* Step 2 of a report: optional free text. */
.field-label {
  display: block;
  font-size: 0.82rem;
  font-weight: 600;
  margin: 14px 0 6px;
  color: var(--text2);
}
.report-details-input {
  width: 100%;
  box-sizing: border-box;
  padding: 10px 12px;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg3);
  color: var(--text);
  font-family: inherit;
  font-size: 0.9rem;
  resize: vertical;
  outline: none;
}
.report-details-input:focus { border-color: var(--accent); }

/* Onboarding age/gender selects. The .mod-welcome-card is centre-aligned, so these need to be
   explicitly full-width and left-aligned or they collapse to the width of their longest option. */
.onb-select {
  width: 100%;
  box-sizing: border-box;
  padding: 11px 12px;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg3);
  color: var(--text);
  font-family: inherit;
  font-size: 0.95rem;
  outline: none;
}
.onb-select:focus { border-color: var(--accent); }
#onboarding-profile .field-label { text-align: left; }
#onboarding-profile .field-note { text-align: left; }
.report-details-actions { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }
.report-details-actions button { width: 100%; }

/* What the reporter wrote, shown on the moderation card. Visually quoted so it reads as
   somebody's words rather than as app copy. */
.report-details-block {
  margin: 8px 0 0;
  padding: 8px 10px;
  background: var(--bg3);
  border-left: 3px solid var(--accent);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.report-details-label {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  margin-bottom: 4px;
}
.report-details-note {
  font-size: 0.84rem;
  color: var(--text);
  word-break: break-word;
  margin-bottom: 4px;
}
.report-details-note:last-child { margin-bottom: 0; }

/* === Admin Panel === */
.admin-tabs { max-width: 320px; }
/* Panel tabs on their own full-width row so labels never truncate */
/* The Admin Panel has 8 tabs and counting — they will never all fit, so skip the even-stretch
   entirely and let every tab sit at its natural width in a scrolling row. Overflow/scrollbar
   handling is inherited from .segments above. */
.admin-tabs-row {
  margin: 10px 12px 4px;
  flex-wrap: nowrap;
  scroll-behavior: smooth;
}
.admin-tabs-row button {
  flex: 0 0 auto;                 /* natural width — never squeezed, never truncated */
  white-space: nowrap;
  font-size: 0.82rem;
  padding: 8px 14px;
  position: relative;
}
/* Per-tab "needs attention" badges — Reports / Review */
.tab-badge {
  position: absolute;
  top: 2px;
  right: 4px;
  background: var(--danger);
  color: #fff;
  font-size: 0.6rem;
  font-weight: 700;
  min-width: 15px;
  height: 15px;
  border-radius: 8px;
  padding: 0 3px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.report-card {
  background: var(--bg2);
  border-radius: var(--radius-sm);
  padding: 14px;
  margin-bottom: 12px;
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
}
.report-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }
.report-card-priority { border: 2px solid var(--danger); box-shadow: 0 0 0 1px var(--danger); }
.report-card-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
.report-post-preview {
  border-radius: 10px;
  height: 80px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background-color: var(--accent);
  background-size: cover;
  background-position: center;
  padding: 10px;
  margin-bottom: 10px;
  position: relative;
}
/* A reported Story can be up to 2000 chars — without this it just spilled text past the
   fixed-height box instead of being cropped to it (same bug the feed-card story preview had). */
.report-post-preview span {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
  font-size: 0.85rem;
  font-weight: 600;
  color: #fff;
  text-shadow: 0 1px 4px rgba(0,0,0,0.5);
  text-align: center;
  word-break: break-word;
}
.report-post-type-badge {
  position: absolute;
  top: 6px;
  left: 6px;
  background: rgba(0,0,0,0.55);
  color: #fff;
  font-size: 8px;
  font-weight: 700;
  padding: 2px 6px;
  border-radius: 4px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.vote-bar { display: flex; gap: 10px; font-size: 0.8rem; color: var(--muted); margin-top: 6px; }
.vote-bar span strong { color: var(--text); }

/* Super-admin-only voter breakdown — who voted which way, to spot abuse/collusion */
.voter-list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 8px;
}
.voter-pill {
  font-size: 0.72rem;
  font-weight: 600;
  padding: 3px 8px;
  border-radius: 10px;
  background: var(--bg3);
  color: var(--text2);
  border: 1px solid var(--border);
}
.voter-pill.good { color: #00b894; border-color: #00b89455; }
.voter-pill.bad  { color: #e17055; border-color: #e1705555; }

/* Two rows: avatar + details on top, actions on their own full-width row underneath.
   This was a single flex row (avatar | info | actions). The actions block has no fixed width, so
   its intrinsic width grew with each button added — and because .user-admin-info carries
   min-width: 0, it was the thing that gave way, collapsing to one word per line while the buttons
   spilled over it. A grid pins the details to a real column and lets the buttons wrap in their own
   row, so the layout holds no matter how many actions a given role sees. */
.user-admin-card {
  background: var(--bg2);
  border-radius: var(--radius-sm);
  padding: 14px;
  margin-bottom: 10px;
  border: 1px solid var(--border);
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas:
    "avatar info"
    "actions actions";
  align-items: center;
  column-gap: 12px;
  row-gap: 10px;
}
.user-admin-card > .chat-avatar { grid-area: avatar; flex-shrink: 0; }
.user-admin-info { grid-area: info; min-width: 0; }
.user-admin-info strong { font-size: 0.92rem; }
.user-admin-info span   { font-size: 0.75rem; color: var(--muted); display: block; }
.user-admin-actions { grid-area: actions; display: flex; gap: 6px; flex-wrap: wrap; }
/* One size for every action in this card. Ban and Delete were full-size while History and Make Mod
   were .sm, which made a wrapped row look ragged and broken rather than merely full. */
.user-admin-actions .btn-primary,
.user-admin-actions .btn-secondary,
.user-admin-actions .btn-danger { padding: 6px 14px; font-size: 0.8rem; }

/* Admin-only user history view (Echoes + full DM content, both sides) */
.history-item {
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  margin-bottom: 8px;
  cursor: pointer;
}
.history-item p { margin: 4px 0 0; font-size: 0.88rem; }
.history-convo {
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  margin-bottom: 10px;
}
.history-convo-header { font-weight: 700; font-size: 0.88rem; margin-bottom: 8px; }
.history-msg { font-size: 0.82rem; padding: 4px 0; border-bottom: 1px solid var(--border); }
.history-msg:last-child { border-bottom: none; }
.history-msg.from-target { color: #e17055; }
.history-msg .muted { display: inline-block; margin-left: 6px; font-size: 0.72rem; }

/* === Pill / Tag === */
.pill {
  display: inline-block;
  padding: 3px 10px;
  border-radius: 20px;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: .3px;
}
.pill-mod    { background: var(--accent-dim); color: var(--accent); }
.pill-banned { background: rgba(232,67,147,.12); color: var(--danger); }
.pill-priority { background: var(--danger); color: #fff; margin-bottom: 6px; display: inline-flex; align-items: center; gap: 4px; }

/* === Utility === */
.hidden  { display: none !important; }
.muted   { color: var(--muted); font-size: 0.85rem; }
.mt-10   { margin-top: 10px; }
.text-center { text-align: center; }

/* === Recovery / New-User Screen === */
.recovery-screen {
  min-height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 32px 20px;
  gap: 28px;
}
.recovery-hero {
  text-align: center;
}
.recovery-logo {
  font-size: 2.8rem;
  font-weight: 900;
  letter-spacing: -1px;
  margin: 0 0 6px;
  color: var(--accent);
}
.recovery-tagline {
  color: var(--text-secondary);
  font-size: 0.9rem;
  margin: 0;
}
.recovery-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 18px;
  padding: 28px 24px;
  width: 100%;
  max-width: 420px;
}
.recovery-card h2 {
  margin: 0 0 8px;
  font-size: 1.25rem;
}
.recovery-key-input {
  display: block;
  width: 100%;
  margin-top: 14px;
  padding: 12px 14px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  color: var(--text);
  font-size: 0.85rem;
  font-family: 'Courier New', monospace;
  box-sizing: border-box;
  resize: none;
}
.recovery-key-input:focus {
  outline: none;
  border-color: var(--accent);
}
.recovery-error {
  margin-top: 8px;
  color: #e17055;
  font-size: 0.82rem;
}
.recovery-divider {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 16px 0;
  color: var(--text-secondary);
  font-size: 0.82rem;
}
.recovery-divider::before,
.recovery-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border);
}
.recovery-key-display {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 14px;
  font-family: 'Courier New', monospace;
  font-size: 0.78rem;
  word-break: break-all;
  color: var(--accent);
  user-select: all;
  -webkit-user-select: all;
  margin: 10px 0;
  line-height: 1.6;
}

/* Recovery method tabs */
.recovery-method-tabs {
  display: flex;
  gap: 8px;
  margin: 14px 0 16px;
}
.recovery-tab {
  flex: 1;
  padding: 9px 12px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text-secondary);
  font-family: inherit;
  font-size: 0.83rem;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.recovery-tab.active {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent);
}
/* Input inside recovery panels should use body font, not monospace */
#recovery-panel-email .recovery-key-input {
  font-family: inherit;
  font-size: 0.9rem;
  margin-top: 0;
  margin-bottom: 8px;
}

/* === Thread back button === */
.back-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 16px;
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
}
.back-row h3 { margin: 0; font-size: 1rem; }

/* === User Profile Modal === */
.profile-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 32px 24px 24px;
  background: linear-gradient(135deg, var(--accent) 0%, var(--accent-light) 100%);
  color: #fff;
  gap: 8px;
}
.profile-hero .avatar-gen,
.profile-hero .avatar-img {
  border: 4px solid rgba(255,255,255,0.4);
  margin-bottom: 4px;
}
.profile-username {
  font-size: 1.15rem;
  font-weight: 700;
  margin-top: 4px;
}
.profile-meta {
  font-size: 0.78rem;
  opacity: 0.8;
}
.profile-stats-row {
  display: flex;
  gap: 28px;
  margin: 12px 0;
}
.profile-stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}
.profile-stat strong {
  font-size: 1.2rem;
  font-weight: 700;
}
.profile-stat span {
  font-size: 0.7rem;
  opacity: 0.8;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

/* === Search tab === */
#search-bar-row input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-dim);
}

/* === Chat header rating === */
.chat-header-rating {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
}
.chat-vote-btn {
  background: none;
  border: 1.5px solid var(--border);
  border-radius: 20px;
  padding: 3px 8px;
  font-size: 0.75rem;
  cursor: pointer;
  color: var(--text2);
  display: flex;
  align-items: center;
  gap: 4px;
  font-family: inherit;
  transition: background 0.15s, color 0.15s;
}
.chat-vote-btn.active-up   { background: var(--success); color: #fff; border-color: var(--success); }
.chat-vote-btn.active-down { background: var(--danger);  color: #fff; border-color: var(--danger); }

/* === btn-danger (v0.3.19) === */
.btn-danger {
  background: var(--danger);
  color: #fff;
  border: none;
  border-radius: 10px;
  padding: 10px 20px;
  font-size: 0.9rem;
  font-family: inherit;
  cursor: pointer;
}
.btn-danger.sm { padding: 6px 14px; font-size: 0.82rem; }

/* === Settings sub-page back header (v0.3.19) === */
.settings-subpage-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 16px 10px;
  font-size: 1rem;
  font-weight: 600;
  color: var(--accent);
  cursor: pointer;
  border-bottom: 1px solid var(--border);
  margin-bottom: 4px;
}
.settings-subpage-header i {
  font-size: 0.9rem;
}

/* ======================== LIGHTBOX ======================== */
#lightbox {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 9000;
  background: rgba(0,0,0,0.92);
  align-items: center;
  justify-content: center;
  backdrop-filter: blur(6px);
}
#lightbox.open {
  display: flex;
}
#lightbox img {
  max-width: 95vw;
  max-height: 90vh;
  object-fit: contain;
  border-radius: 8px;
  user-select: none;
}
.lightbox-close {
  position: absolute;
  top: 16px;
  right: 16px;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: none;
  background: rgba(255,255,255,0.15);
  color: #fff;
  font-size: 1.2rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  backdrop-filter: blur(4px);
  z-index: 9001;
}

/* ======================== POST EXPAND BUTTON ======================== */
.post-expand-btn {
  position: absolute;
  /* Anchored from the bottom used to land on top of the footer's star/reply/chat/flag icon row —
     that row's height isn't fixed (group banner, pinned badge, etc. all change it), so no fixed
     bottom offset stays correct everywhere. Sitting level with .post-menu-btn (top:6px), one
     26px-button-width-plus-gap to its left, keeps both icons on one row instead of stacking. */
  top: 6px;
  right: 38px;
  z-index: 3;
  width: 26px;
  height: 26px;
  background: rgba(0,0,0,0.5);
  border: none;
  border-radius: 50%;
  color: #fff;
  font-size: 11px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  backdrop-filter: blur(4px);
}

/* ======================== PULL-TO-REFRESH ======================== */
#pull-indicator {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 12px 0;
  color: var(--accent);
  font-size: 1.2rem;
  opacity: 0;
  transition: opacity 0.1s;
}
