/* =============================================================================
   DESIGN TOKENS — "Refined Quiet Space"
   Ported from Superdesign draft 5d9fdcfe (project c7b00faa).
   See specs/2026-08-13-refined-quiet-space-design.md.

   Two groups, and the split matters:
     - Skinnable: surfaces, text, accent. Overridden per [data-skin].
     - Structural: radii, shadows, easing. IDENTICAL in every skin, so all
       skins share one shape language and only the color changes.
   Semantic colors (halls green/red, CW amber, pulse pink, mood auras, ember
   glow) are deliberately NOT tokens — they mean something.
   ========================================================================== */
:root {
  /* ---- Skinnable: cool default. ---- */
  --bg-deep:     #0a0c12;
  --bg-surface:  #121620;
  --bg-sunken:   #0d111c;
  --bg-raised:   #1a1f2e;

  --border:      rgba(255, 255, 255, 0.08);
  --border-soft: rgba(255, 255, 255, 0.05);

  --text:        #e8eaf0;
  --text-muted:  #94a3b8;
  --text-faint:  #7c8ba0;

  --accent:       #4fc3f7;
  --accent-hover: #81d4fa;
  --accent-wash:  rgba(79, 195, 247, 0.10);
  --accent-ring:  rgba(79, 195, 247, 0.30);

  /* --glass/--glass-chrome: this skin's --bg-surface with an alpha channel —
     the opaque surface tokens have none, and glass (blur + translucency)
     needs one to blur what's behind it. --glass (0.72) backs popovers,
     dialogs and modals; --glass-chrome (0.6) backs header/tab-strip/composer
     chrome. Every skin block below must set both, or its glass renders in
     THIS skin's tint instead of its own. */
  --glass:        rgba(18, 22, 32, 0.72);
  --glass-chrome: rgba(18, 22, 32, 0.6);

  /* ---- Structural: identical in every skin ---- */
  --radius:      16px;
  --radius-sm:   12px;
  --radius-lg:   24px;
  --radius-pill: 999px;
  --shadow-soft: 0 4px 16px -4px rgba(0, 0, 0, 0.40);
  --shadow-lift: 0 8px 24px -8px rgba(0, 0, 0, 0.50);
  --ease:        300ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* Skins — user-selectable palettes, picked from the ••• menu and applied
   via html[data-skin]. Each overrides ONLY the skinnable token group above;
   structural tokens and semantic colors (halls green/red, crisis amber,
   pulse pink, keepsake gold, moment purple, ember glow, mood auras,
   private-message/campfire orange) are untouched by design — a skin
   repaints the app's neutral surfaces, not what colors mean.
   See public/js/skin.js and specs/2026-08-13-refined-quiet-space-design.md.

   The five dark palettes that used to live here (candlelight/teal/dusk/
   sage/lavender) only ever differed by --accent* — their --bg/--text/
   --border tokens were near-identical near-black variants of each other,
   so picking one of six dark skins meant picking an accent color while
   dragging an invisible background choice along with it. Collapsed into
   ONE dark base (this :root block) + the accent overrides below, which
   apply the same five accent hues via [data-accent] instead of [data-skin].
   Contrast re-verified against the surviving base tokens (not each accent's
   old, now-deleted background): worst case lavender/--bg-raised at 6.85:1,
   still clear of the 4.5:1 gate. */

/* Accent — recolors the dark base's --accent/--accent-hover/--accent-wash/
   --accent-ring only. minimal is a MODE, not a palette (see below), and
   keeps its own contrast-tuned accent — :not([data-skin="minimal"]) makes
   these inert there instead of fighting that tuning. Default blue needs no
   block; it's already :root's --accent above. */
html[data-accent="coral"]:not([data-skin="minimal"]) {
  --accent:       #ffcf9c;
  --accent-hover: #ffe2c4;
  --accent-wash:  rgba(255, 207, 156, 0.14);
  --accent-ring:  rgba(255, 207, 156, 0.34);
}
/* Coral is the one accent with its own bespoke treatment (from the ember
   redesign exploration) rather than a flat var(--accent) fill — a warm
   two-tone gradient on the two places a sent message actually shows: your
   own bubble and the send button. Dark brown text (not var(--bg-deep))
   because the gradient is light, unlike every other accent's dark chip. */
html[data-accent="coral"]:not([data-skin="minimal"]) .msg.mine .msg-inner {
  background: linear-gradient(160deg, #ffcf9c, #ff8a3d);
  color: #2a1204;
  border-color: transparent;
}
html[data-accent="coral"]:not([data-skin="minimal"]) #send-button {
  background: linear-gradient(160deg, #ffcf9c, #ff8a3d);
  color: #2a1204;
}
html[data-accent="coral"]:not([data-skin="minimal"]) #send-button:hover {
  background: linear-gradient(160deg, #ffe2c4, #ffa35c);
}
html[data-accent="teal"]:not([data-skin="minimal"]) {
  --accent:       #6fd6c8;
  --accent-hover: #86e2d5;
  --accent-wash:  rgba(111, 214, 200, 0.12);
  --accent-ring:  rgba(111, 214, 200, 0.32);
}
html[data-accent="sage"]:not([data-skin="minimal"]) {
  --accent:       #7fc8a0;
  --accent-hover: #a3dcbc;
  --accent-wash:  rgba(127, 200, 160, 0.10);
  --accent-ring:  rgba(127, 200, 160, 0.30);
}
html[data-accent="lavender"]:not([data-skin="minimal"]) {
  --accent:       #b39ddb;
  --accent-hover: #cdbcea;
  --accent-wash:  rgba(179, 157, 219, 0.12);
  --accent-ring:  rgba(179, 157, 219, 0.32);
}

/* minimal — light mode, not a palette. The only skin that is a MODE rather
   than a color swap: "light theme", "no animations", "no glassmorphism" and
   "stark mood grid" can't be expressed as color tokens alone. Its token
   block lives here like every other skin above; its COMPONENT-LEVEL rules
   (semantic-color darkening, animation/gradient suppression, mood-grid and
   input/nav treatment) are sanctioned for THIS skin only and live in their
   own clearly-marked section near the end of this file, still scoped under
   html[data-skin="minimal"] — every other skin stays token-only. */
html[data-skin="minimal"] {
  /* Surface ladder is INVERTED here relative to every dark skin above.
     There, --bg-surface is darker than --bg-deep (cards sit in a dim room,
     lit slightly brighter than the walls). On a light canvas that logic
     flips: a pure-white --bg-deep card on a pure-white page is invisible
     (tinting --bg-deep alone while leaving cards at the old #f7f7f8 grey
     separates at only 1.02:1). So here --bg-surface is the LIGHTEST value
     (raised cards read as whiter than the tinted page behind them) and
     --bg-deep is a barely-tinted cool white (page/body/chat-box), calmer
     than clinical pure white. Do NOT "fix" this back to deep>surface — that
     flattens card-vs-page separation to near zero. --bg-sunken/--bg-raised
     keep their usual meaning (recessed input / chip) and stay darker than
     both, in that order. Verified: cards-vs-page 1.106:1 (was 1.071:1),
     worst text pair --text-faint on --bg-raised 4.75:1. */
  --bg-deep:     #eff4fa;   /* tinted canvas — page/body/chat-box */
  --bg-surface:  #ffffff;   /* cards — lightest surface, reads as raised */
  --bg-sunken:   #e7edf4;   /* inputs, recessed */
  --bg-raised:   #e1e8f1;   /* chips, badges */

  /* dark-alpha: the other skins' white-alpha hairlines are invisible on white */
  --border:      rgba(0, 0, 0, 0.14);
  --border-soft: rgba(0, 0, 0, 0.08);

  --text:        #16181d;
  --text-muted:  #4b5160;
  --text-faint:  #5f6570;

  --accent:       #0b5f91;
  --accent-hover: #094d76;
  --accent-wash:  rgba(11, 95, 145, 0.08);
  --accent-ring:  rgba(11, 95, 145, 0.28);

  /* opaque: kills glassmorphism without touching any backdrop-filter rule */
  --glass:        #f7f7f8;
  --glass-chrome: #ffffff;

  /* structural override — sanctioned for THIS skin only. "Zero visual noise"
     explicitly asks for it, and heavy dark shadows are the main source of
     noise on white. Every other skin keeps the shared values. */
  --shadow-soft: 0 1px 2px rgba(0, 0, 0, 0.06);
  --shadow-lift: 0 2px 8px rgba(0, 0, 0, 0.10);
}

/* Cross-fade for skin switching (Task 8): when [data-skin] changes on <html>,
   these surfaces fade colors instead of snapping. Was previously scoped to
   the now-removed Auto-Night Theme's class selector; kept unscoped and
   always-on since any future skin change needs the same fade. */
body,
header,
#tab-strip,
#chat-input,
#pulse-strip,
#empathy-strip,
.tab-private-subheader,
.tab-panel[data-tab-id="world"] #online-list,
.chat-box,
.stream-invite-banner,
.stream-live,
.pair-banner {
  transition: background 600ms ease, color 600ms ease, border-color 600ms ease;
}

* { box-sizing: border-box; margin: 0; padding: 0; }
/* The reset above strips the UA stylesheet's `dialog { margin: auto }`,
   which is what centers a modal <dialog> in its viewport-filling containing
   block — so every <dialog> without its own explicit centering renders
   pinned to the top-left instead. Restore it here, next to the cause. */
dialog { margin: auto; }

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: var(--bg-deep);
  color: var(--text);
  height: 100dvh;   /* dynamic viewport height — accounts for iOS Safari address bar */
  display: flex;
  flex-direction: column;
}

header {
  position: relative;   /* containing block for the ::before glass layer below */
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 20px;
  border-bottom: 1px solid var(--border-soft);
  gap: 12px;
  flex-wrap: wrap;
}

/* Glass background lives on a pseudo-element rather than on `header`
   itself. backdrop-filter on `header` would make it the containing block
   for the position:fixed popovers nested inside it (mood/sit-mode/my-link/
   open-window/more-menu popovers) — trapping them in header's paint layer
   and hiding them behind #container, which paints later in DOM order.
   header itself deliberately keeps z-index:auto (no explicit value) so it
   never becomes a stacking context — that would trap those same fixed
   popovers again, just via a different property. position:relative alone
   is enough to give this pseudo a containing block without that side
   effect. Its own z-index: -1 (position:absolute + a non-auto z-index
   always creates a stacking context for the element itself, regardless of
   the ancestor) is what sorts it behind header's real content — a
   positioned box otherwise paints above non-positioned flex children
   regardless of tree order, which would hide the wordmark/controls under
   the glass. pointer-events: none so it can't swallow clicks either.
   Caveat for later: because header stays z-index:auto, this -1 resolves
   against the root stacking context, not against header — harmless today
   since header is static and never overlaps #container. If header ever
   becomes sticky/fixed, the glass will paint behind whatever it's meant to
   blur. Don't "fix" that by giving header a z-index — that re-traps the
   fixed popovers this comment opens with. */
header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  /* This skin's surface color at 60% opacity, via --glass-chrome — see the
     token definitions in :root for why glass needs its own alpha-bearing
     tokens instead of the opaque --bg-* ones. */
  background: var(--glass-chrome);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);   /* Safari needs the prefix */
}

/* Groups the wordmark and the anon-badge so the badge sits right beside
   "ember" instead of drifting to header's own justify-content: space-between. */
.brand { display: flex; align-items: center; }

/* Cross-fade on skin change (Task 8 fix round 1) — the wordmark is the
   other most-visible bare var(--accent) consumer outside the hoisted
   transition list; fixed deliberately, not exhaustively (see
   .tab.active::after above — same reasoning, not repeated here). */
header h1 { font-size: 1.2rem; color: var(--accent); white-space: nowrap; transition: color 600ms ease; }

/* Shared sizing for inline lucide SVGs used in place of header emoji —
   fixed box so icons line up regardless of surrounding font-size. */
.icon { width: 18px; height: 18px; display: block; flex: 0 0 auto; }

/* #header-more-btn's only visible content used to be the "•••" text node;
   now it's a block-level .icon plus the unread dot span, so the button
   needs flex to keep them on one line instead of stacking. */
#header-more-btn { display: inline-flex; align-items: center; }

/* pill beside the wordmark — reminds people the platform has no accounts,
   no KYC; this is not a new anonymity feature, just naming what's already true. */
.anon-badge {
  display: inline-block;
  margin-left: 8px;
  padding: 2px 9px;
  border-radius: var(--radius-pill);
  background: transparent;
  border: 1px solid var(--accent);
  color: var(--accent);
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.06em;
}

.header-controls {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

#status-input {
  flex: 1;
  min-width: 0;   /* text inputs impose a large intrinsic min-width by default — without this, flex:1 can't actually shrink it on narrow screens */
  padding: 4px 8px;
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text);
  font-size: 0.8rem;
}
#status-input:focus { outline: none; border-color: var(--accent); }


.header-info { font-size: 0.85rem; color: var(--text-muted); white-space: nowrap; }
.header-online-info { margin-left: 6px; }   /* a little extra separation — this one's about the room, not about you */

/* Ambient life-sign — a live waveform riding in the header at all times,
   not tied to any one feature. currentColor + color:var(--accent) means it
   automatically matches whichever accent the user has picked. */
/* Life-sign for the synchronized pulse — hidden until you've actually
   joined the heartbeat (#pulse-strip.joined, mirrored via app.js), not an
   always-on ambient decoration. Animation lives on the wrapping track div,
   not the <svg> itself — Safari doesn't reliably loop a transform
   animation applied directly to an inline SVG root. */
.header-ecg { display: none; width: 44px; height: 14px; overflow: hidden; color: var(--accent); margin-left: 8px; vertical-align: middle; }
.header-ecg.beating { display: inline-flex; }
.header-ecg-track { display: inline-flex; width: 88px; height: 14px; animation: header-ecg-scroll 2.2s linear infinite; }
.header-ecg svg { width: 88px; height: 14px; display: block; }
@keyframes header-ecg-scroll { from { transform: translateX(0); } to { transform: translateX(-44px); } }
#my-name { color: var(--accent); cursor: pointer; font-weight: bold; }
#my-name:hover { text-decoration: underline; }

#container { display: flex; flex: 1; overflow: hidden; }
#main { flex: 1; display: flex; flex-direction: column; min-width: 0; position: relative; }

/* --- Tab strip --- */
#tab-strip {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 6px 8px;
  /* Glass: no fixed descendants live inside #tab-strip (just static tab
     buttons), so backdrop-filter is safe directly here — unlike header,
     which needed the ::before pseudo-element workaround. */
  background: var(--glass-chrome);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);   /* Safari needs the prefix */
  border-bottom: 1px solid var(--border-soft);
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  flex: 0 0 auto;
}
#tab-strip::-webkit-scrollbar { display: none; }

/* Underline indicator, not a folder tab — the active tab is marked by a
 * ::after bar flush with the strip's bottom edge, not a bordered box. */
.tab {
  background: transparent;
  color: var(--text-muted);
  border: none;
  border-radius: 0;
  padding: 8px 14px;
  font-size: 0.82rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  flex: 0 0 auto;
  position: relative;   /* containing block for .tab.active::after */
  transition: color var(--ease), background var(--ease);
}
.tab:hover { color: var(--text); background: var(--accent-wash); }
.tab.active {
  color: var(--accent);
  background: transparent;
}
.tab.active::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 2px;
  background: var(--accent);
  /* Cross-fade on skin change (Task 8 fix round 1) — this is one of the two
     most visible bare var(--accent) consumers outside the hoisted transition
     list; fixed deliberately, not exhaustively (see header h1 below). Other
     accent consumers (.msg.mine, icon strokes, small accent text) still
     snap — accepted, a barely-perceptible hue jump on small elements. */
  transition: background 600ms ease;
}
.tab.unread::before {
  content: '';
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  margin-right: 2px;
}
.tab.dimmed { opacity: 0.55; }
.tab-close {
  margin-left: 4px;
  padding: 0 4px;
  border-radius: 3px;
  cursor: pointer;
  color: var(--text-faint);
  font-weight: bold;
  line-height: 1;
}
.tab-close:hover { background: rgba(255,255,255,0.1); color: var(--text); }

/* --- Tab panels --- */
#tab-panels {
  flex: 1;
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}
.tab-panel {
  display: none;
  flex: 1;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  position: relative;   /* anchor for pair gate overlay when active */
}
.tab-panel.active { display: flex; }

/* =============================================================================
   PRIMARY NAV — the 4-destination floating dock (Talk/Confessions/Nearby/
   Explore) that replaces #tab-strip visually. #tab-strip itself stays in
   the DOM (hidden below) so registerStaticTab's lookups and every existing
   badge element keep working; app.js's new dock click handlers just call
   the same switchTab(tabId) the old buttons did.
   ========================================================================= */
#tab-strip { display: none; }

/* z-index tier: this app's toast layer sits at 1000 (moment-nudge/mention/
   plane), popovers/modals at 1100-2100. The dock is persistent chrome —
   it must outrank transient toasts (so a once-a-day nudge can't
   permanently block navigation at that screen position) but stay below
   real popovers/modals, which should still win when open over the dock. */
#explore-backdrop {
  position: absolute; inset: 0; z-index: 1040; pointer-events: none;
  background: rgba(0, 0, 0, 0); transition: background 0.25s ease;
}
#explore-backdrop.on { background: rgba(0, 0, 0, 0.55); pointer-events: auto; }

.primary-nav {
  flex: none; position: relative; z-index: 1050; align-self: center; margin: 0 auto 10px;
  display: flex; gap: 4px; background: var(--glass-chrome); backdrop-filter: blur(10px);
  border-radius: var(--radius-pill); padding: 6px; box-shadow: var(--shadow-lift);
}
.primary-nav .nav-item {
  position: relative; display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 8px 14px; border-radius: 20px; border: none; background: transparent;
  color: var(--text-muted); font: inherit; font-size: 0.64rem; cursor: pointer;
  transition: transform 0.18s ease, background 0.18s ease, color 0.18s ease;
}
.primary-nav .nav-icon { font-size: 1.2rem; display: inline-block; }
.primary-nav .nav-item.active {
  color: var(--accent); background: var(--accent-wash);
  transform: translateY(-4px); box-shadow: 0 6px 16px -4px var(--accent-ring);
}
/* While a flyout is open, attention has moved to that button — whichever
   button was .active underneath (e.g. Nearby, mid-flyout on Explore, or
   vice versa) shouldn't still look "current" at the same time. The button
   that opened the flyout borrows the exact same active look; the real tab
   underneath reverts the instant the flyout closes. */
.primary-nav[data-open-flyout="explore"] .nav-item.active:not([data-dock="explore"]),
.primary-nav[data-open-flyout="nearby"] .nav-item.active:not([data-dock="nearby"]) {
  color: var(--text-muted); background: transparent; transform: none; box-shadow: none;
}
.primary-nav[data-open-flyout="explore"] .nav-item[data-dock="explore"],
.primary-nav[data-open-flyout="nearby"] .nav-item[data-dock="nearby"] {
  color: var(--accent); background: var(--accent-wash);
  transform: translateY(-4px); box-shadow: 0 6px 16px -4px var(--accent-ring);
}
/* One tap gesture per button, not an identical highlight for all four —
   restarts every time .active newly matches (adding a class that carries
   an animation restarts it, same as the CSS :checked trick, just JS-driven
   here since the dock toggles .active directly instead of a radio hack). */
.primary-nav .nav-item[data-dock="talk"].active .nav-icon { animation: dock-talk-pop 0.45s ease; }
@keyframes dock-talk-pop { 0% { transform: scale(1); } 35% { transform: scale(1.35) rotate(-6deg); } 65% { transform: scale(0.92); } 100% { transform: scale(1); } }
.primary-nav .nav-item[data-dock="confessions"].active .nav-icon { animation: dock-confess-glow 0.6s ease; }
@keyframes dock-confess-glow { 0% { filter: brightness(1); } 50% { filter: brightness(1.7) drop-shadow(0 0 6px #cfe0ff); } 100% { filter: brightness(1); } }
.primary-nav .nav-item[data-dock="nearby"].active::after { content: ""; position: absolute; inset: 6px; border: 2px solid var(--accent); border-radius: 50%; opacity: 0; pointer-events: none; animation: dock-ping 0.6s ease-out; }
@keyframes dock-ping { 0% { transform: scale(0.4); opacity: 0.9; } 100% { transform: scale(2.1); opacity: 0; } }
.primary-nav .nav-dot {
  position: absolute; top: 4px; right: 6px; width: 7px; height: 7px; border-radius: 50%;
  background: #ff6b81; box-shadow: 0 0 6px rgba(255, 107, 129, 0.8);
}

.nav-item-wrap { position: relative; }
/* The bubble column is centered exactly on the dock button's own icon
   (left:50%/translateX(-50%), sized to the bubble's own 52px, same as the
   button's icon centering) — this is what actually keeps Explore's icon
   vertically in line with Halls/Campfires/Stage's bubbles below it, and
   Nearby's icon in line with Map/Feed's. A flyout that's merely centered
   as a whole box does NOT achieve this: each row is [label][bubble], an
   asymmetric shape, so centering the row still leaves the bubble — the
   part that actually needs to align — offset from the box's own center by
   half the label's width. Anchoring the fixed-width bubble column instead
   makes the label's width irrelevant to alignment; it can be any length
   and float freely to the left without ever moving the bubble. */
.explore-flyout {
  position: absolute; bottom: calc(100% + 10px); left: 50%; transform: translateX(-50%);
  display: none; flex-direction: column; gap: 10px; align-items: center; width: 52px;
}
#primary-nav[data-open-flyout="explore"] #explore-flyout,
#primary-nav[data-open-flyout="nearby"] #nearby-flyout { display: flex; }
#primary-nav[data-open-flyout="explore"] .nav-item[data-dock="explore"] .nav-icon { animation: dock-explore-wiggle 0.5s ease; }
#primary-nav[data-open-flyout="nearby"] .nav-item[data-dock="nearby"] .nav-icon { animation: dock-explore-wiggle 0.5s ease; }
@keyframes dock-explore-wiggle { 0% { transform: rotate(0); } 30% { transform: rotate(-14deg) scale(1.12); } 60% { transform: rotate(10deg); } 100% { transform: rotate(0) scale(1); } }
.flyout-item { position: relative; width: 52px; height: 52px; opacity: 0; transform: scale(0.3) translateY(8px); }
#primary-nav[data-open-flyout] .flyout-item {
  animation: dock-bubble-pop 0.36s cubic-bezier(0.34, 1.56, 0.64, 1) both, dock-float-bob 4.4s ease-in-out 0.5s infinite;
}
#primary-nav[data-open-flyout] .flyout-item:nth-child(1) { animation-delay: 0.18s, 0.68s; }
#primary-nav[data-open-flyout] .flyout-item:nth-child(2) { animation-delay: 0.09s, 0.59s; }
#primary-nav[data-open-flyout] .flyout-item:nth-child(3) { animation-delay: 0s, 0.5s; }
@keyframes dock-bubble-pop { from { opacity: 0; transform: scale(0.3) translateY(8px); } to { opacity: 1; transform: scale(1) translateY(0); } }
@keyframes dock-float-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
/* Floats to the left of its bubble via absolute positioning — its width
   never participates in the column's own layout/centering math above. */
.flyout-label {
  position: absolute; right: calc(100% + 10px); top: 50%; transform: translateY(-50%);
  width: 220px; background: var(--glass-chrome); backdrop-filter: blur(10px);
  padding: 7px 13px; border-radius: 16px; text-align: left; line-height: 1.3;
  box-shadow: var(--shadow-soft);
}
/* Nearby sits further left in the dock than Explore, so there's less room
   between its bubble column and the viewport's left edge — a narrower
   label here, not a wider viewport, is what keeps it from running off
   screen while the bubble stays exactly under the Nearby icon. */
#nearby-flyout .flyout-label { width: 190px; }
.flyout-label strong { display: block; font-weight: 600; font-size: 0.8rem; color: var(--accent); }
.flyout-label span { display: block; font-size: 0.72rem; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.flyout-bubble {
  width: 52px; height: 52px; border-radius: 50%; background: var(--bg-raised);
  border: 1px solid var(--border); display: flex; align-items: center; justify-content: center;
  font-size: 1.3rem; cursor: pointer; box-shadow: var(--shadow-soft); color: var(--text);
}

/* Talk's room-chip switcher — Public, Vibe Rooms, and any live 1:1/pair/
   gathering thread, shown above the feed only while a Talk-family tab is
   active (see #talk-chips[hidden] guard below). */
#talk-chips { display: none; }
#talk-chips:not([hidden]) { display: flex; }
.room-chips { gap: 6px; padding: 8px 12px 0; overflow-x: auto; flex: none; }
.room-chips .chip {
  flex: none; padding: 6px 12px; border-radius: var(--radius-pill); font-size: 0.78rem;
  background: var(--bg-raised); color: var(--text-muted); white-space: nowrap;
  border: none; cursor: pointer; font: inherit;
}
.room-chips .chip.on { background: var(--accent-wash); color: var(--accent); }
.room-chips .chip.pinned { background: rgba(255, 183, 120, 0.15); color: #ffb778; }

/* World tab content */
.tab-panel[data-tab-id="world"] #map {
  flex: 1;
  min-height: 200px;
  position: relative;   /* anchor for #map-center-me */
}
#map-center-me {
  position: absolute; z-index: 1000; right: 12px; bottom: 12px;
  width: 34px; height: 34px; border-radius: 50%;
  background: #1d1726; color: #cdbfe0; border: 1px solid #3a3050;
  font-size: 1.1rem; line-height: 1; cursor: pointer; opacity: 0.85;
}
#map-center-me:hover { opacity: 1; }
.tab-panel[data-tab-id="world"] #online-list {
  background: var(--bg-surface);
  border-top: 1px solid var(--border-soft);
  padding: 12px;
  overflow-y: auto;
}
.tab-panel[data-tab-id="world"] #online-list h3 {
  font-size: 0.85rem;
  color: var(--text-muted);
  margin-bottom: 10px;
  text-transform: uppercase;
  letter-spacing: 1px;
}

/* Chat-box (one per chat-bearing tab) */
.chat-box {
  flex: 1;
  overflow-y: auto;
  padding: 10px 12px;
  background: var(--bg-deep);
}

/* Private tab sub-header (partner info) */
.tab-private-subheader {
  padding: 6px 12px;
  font-size: 0.78rem;
  color: var(--text-muted);
  background: var(--bg-surface);
  border-bottom: 1px solid var(--border-soft);
}
.tab-private-offline {
  padding: 6px 12px;
  font-size: 0.78rem;
  color: #ff9800;
  background: #2e2316;
  border-top: 1px solid #4a3a2e;
}

/* Chat-input visibility — hidden when World is active */
#main.world-active #chat-input { display: none; }
#main.public-active #chat-input { display: none; }   /* public tab has its own RoomComposer */
#main.halls-active #chat-input { display: none; }
#main.stage-active #chat-input { display: none; }
#main.campfires-active #chat-input { display: none; }
#main.confessions-active #chat-input { display: none; }
#main.breathe-active #chat-input { display: none; }   /* wordless — nothing to type */

/* Threaded Icebreakers: the question line + thread button. The hall list
 * and the live replies live INSIDE the thread side-panel (Slack-style),
 * not inline — keeps the main Public chat-box uncluttered. */
.msg.icebreaker .ib-question { display: inline; }

/* Halls tab content. */
#halls-content {
  flex: 1;
  overflow-y: auto;
  padding: 16px 20px 24px;
}

/* Confessions tab — composer pinned to top, feed scrolls below it.
 * Anonymous, no replies, only 🫂 hug / 🤍 nod reactions. */
.tab-panel[data-tab-id="confessions"] {
  flex-direction: column;
}
/* Starry-night atmosphere — a whisper of Night Sky behind the one screen
   that's actually about saying something real at 2am. Three independent
   twinkle rhythms (so it never reads as one blinking sprite), a slow
   drifting glow, and rising motes like carried whispers. z-index: -1 (not
   just DOM order) is required: an absolutely-positioned box paints above
   normal in-flow siblings regardless of source order unless it's given a
   negative z-index. Suppressed on minimal — that skin already strips every
   other atmospheric flourish for the same "zero visual noise" reason. */
.atmosphere { position: absolute; inset: 0; z-index: -1; pointer-events: none; overflow: hidden; }
html[data-skin="minimal"] .atmosphere { display: none; }
.stars-a, .stars-b, .stars-c {
  position: absolute; inset: 0;
  background-image:
    radial-gradient(1px 1px at 12% 18%, #fff 70%, transparent),
    radial-gradient(1px 1px at 32% 62%, #fff 70%, transparent),
    radial-gradient(1.4px 1.4px at 58% 24%, #fff 70%, transparent),
    radial-gradient(1px 1px at 74% 70%, #fff 70%, transparent),
    radial-gradient(1px 1px at 88% 38%, #fff 70%, transparent),
    radial-gradient(1.4px 1.4px at 44% 84%, #fff 70%, transparent);
}
.stars-a { animation: confess-twinkle 4.2s ease-in-out infinite; }
.stars-b { background-position: 30px 40px; opacity: 0.7; animation: confess-twinkle 6.1s ease-in-out infinite 1.2s; }
.stars-c { background-position: -20px 70px; opacity: 0.5; animation: confess-twinkle 5.3s ease-in-out infinite 2.4s; }
@keyframes confess-twinkle { 0%, 100% { opacity: 0.15; } 50% { opacity: 0.65; } }
/* Waxing/waning moon. Moved here from the Sit With Me page, where it competed
   with that page's warp starfield — one slow-moving thing per screen. It suits
   Confessions better anyway: this is the 2am screen, and a moon that is never
   quite the same shape twice belongs on it.
   The phase is a second disc in the page's own background colour sliding across
   the lit one, clipped by overflow:hidden, so the terminator is a real curve
   rather than the straight edge a rectangular inset shadow would sweep.
   It animates `transform`, NOT `box-shadow`: Safari will not interpolate a
   box-shadow carrying a custom property, so a box-shadow version paints a
   correct still frame and then never moves — it worked in Chromium, which is
   exactly how it passed review once already. Do not reintroduce one. */
.atmosphere .moon {
  position: absolute;
  /* Anchored to the BOTTOM on purpose. .atmosphere is z-index:-1, so it paints
     beneath the background of every in-flow sibling — and #void-card, which is
     top-anchored, has an opaque background-IMAGE (a gradient; its
     background-color is transparent, so it does not look opaque in devtools).
     A top-anchored moon lands inside that card and vanishes completely. Bottom
     anchoring cannot collide with it at any viewport size or card height. The
     feed below is transparent, so the moon shows through it the way the stars
     already do. */
  bottom: 24%;
  right: 8%;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  overflow: hidden;
  background: #f0ece1;
  box-shadow: 0 0 22px 4px rgba(240, 236, 225, 0.16);
  opacity: 0.5;
}
.atmosphere .moon::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  /* Must match whatever actually paints behind .atmosphere — it has z-index:-1
     and no background of its own, so that is body's --bg-deep. Hardcoding a
     colour here would show up as a visible disc as soon as a skin changed it. */
  background: var(--bg-deep);
  /* Negative delay starts the cycle already part-lit. Without it the animation
     begins at 0% — fully covered, i.e. a new moon — so for the first ~20s of a
     120s cycle there is visibly nothing there, and most visits are shorter than
     that. -40s lands about a third in, on a fat crescent. */
  animation: confess-moon-phase 120s linear -40s infinite;
}
/* 0% fully covering = new moon; 50% clear of the disc = full moon. */
@keyframes confess-moon-phase {
  0%, 100% { transform: translateX(0); }
  50%      { transform: translateX(100%); }
}
.breeze-glow {
  position: absolute; width: 380px; height: 380px; border-radius: 50%;
  background: radial-gradient(circle, rgba(207, 224, 255, 0.09), transparent 70%);
  filter: blur(10px); animation: confess-breeze 46s ease-in-out infinite alternate;
}
@keyframes confess-breeze { 0% { transform: translate(-15%, 60%); } 100% { transform: translate(65%, -15%); } }
.mote { position: absolute; bottom: 0; width: 3px; height: 3px; border-radius: 50%; background: #cfe0ff; opacity: 0; animation: confess-mote-rise 9s linear infinite; }
@keyframes confess-mote-rise { 0% { opacity: 0; transform: translateY(0); } 12% { opacity: 0.6; } 88% { opacity: 0; } 100% { transform: translateY(-170px); } }
#confession-post {
  margin-left: auto;
  padding: 5px 14px;
  background: var(--bg-raised); color: #f0e6d2;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  cursor: pointer; font-size: 0.85rem;
}
#confession-post:hover:not(:disabled) { background: var(--accent-wash); }
#confession-post:disabled { opacity: 0.4; cursor: not-allowed; }

#confessions-feed {
  flex: 1; overflow-y: auto;
  padding: 12px 20px 24px;
  display: flex; flex-direction: column; gap: 10px;
}
#confessions-feed[hidden] { display: none; }   /* [hidden] beats the flex rule */
.confession {
  background: var(--bg-surface);
  border-left: 2px solid #ffd54f44;
  border-radius: var(--radius-sm);
  padding: 10px 14px;
}
.confession-text {
  color: var(--text); font-size: 0.95rem; line-height: 1.5;
  white-space: pre-wrap; word-wrap: break-word;
}
.confession-reactions {
  display: flex; gap: 8px;
  margin-top: 8px;
}
/* Content Warnings (CW) — author-marked, blurred for non-author readers,
 * tap card to reveal. Pill is amber-warm, matches the existing confession
 * border-left accent. See xscan/chat/specs/2026-06-18-content-blurs-design.md. */
.cw-pill {
  display: inline-block;
  background: rgba(255, 152, 0, 0.18);
  border: 1px solid rgba(255, 152, 0, 0.55);
  color: #ffc278;
  border-radius: var(--radius);
  padding: 1px 10px;
  font-size: 0.74rem;
  letter-spacing: 0.02em;
  margin-bottom: 6px;
  font-style: italic;
}
.cw-pill::before { content: 'CW · '; opacity: 0.7; font-style: normal; }
.confession.cw-blurred { cursor: pointer; }
.confession.cw-blurred .confession-text {
  filter: blur(8px);
  user-select: none;
  transition: filter var(--ease);
}
.confession.cw-blurred:hover .confession-text {
  filter: blur(6px);
}

.cw-toggle {
  display: inline-flex;
  gap: 6px;
  align-items: center;
  cursor: pointer;
  color: var(--text-muted);
}
.cw-toggle input { margin: 0; }
.cw-toggle:hover { color: var(--text); }

/* Risk-aware composer — crisis hotline resource panel.
 * Fired client-side when confession draft matches crisis trigger patterns.
 * Same amber palette as CW so the visual family signals "this is an
 * author-side this-needs-care affordance."
 * See xscan/chat/specs/2026-06-18-risk-aware-composer-design.md. */
.crisis-resources-panel {
  background: rgba(255, 152, 0, 0.10);
  border: 1px solid rgba(255, 152, 0, 0.42);
  border-radius: var(--radius-sm);
  padding: 12px 36px 12px 14px;
  margin-bottom: 8px;
  position: relative;
  color: #f0e6d2;
  font-size: 0.85rem;
  line-height: 1.5;
}
.crisis-resources-panel[hidden] { display: none; }
.crisis-headline { font-style: italic; opacity: 0.95; margin-bottom: 6px; }
.crisis-links { margin-bottom: 6px; }
.crisis-links a {
  color: #ffd54f;
  text-decoration: none;
  border-bottom: 1px dotted rgba(255, 213, 79, 0.5);
}
.crisis-links a:hover {
  color: #fff;
  border-bottom-color: #fff;
}
.crisis-links .crisis-sep { color: var(--text-faint); padding: 0 2px; }
.crisis-prompt { opacity: 0.85; margin-bottom: 8px; }
/* Privacy line — quiet but explicit. Most users won't notice it; the
 * skeptical ones will read it and know the matching is local. */
.crisis-privacy {
  opacity: 0.55;
  font-size: 0.72rem;
  font-style: italic;
}
.crisis-dismiss {
  position: absolute;
  top: 4px;
  right: 6px;
  background: transparent;
  border: none;
  color: var(--text-muted);
  font-size: 1.2rem;
  cursor: pointer;
  line-height: 1;
  padding: 2px 6px;
}
.crisis-dismiss:hover { color: #fff; }

.confessions-empty {
  margin-top: 40px;
  text-align: center;
  color: var(--text-faint);
  font-size: 0.9rem;
}

/* Daily Void Question — pinned card above the composer on Confessions tab.
 * One universal prompt per UTC day. Quiet, reverent, no badges. */
#void-card {
  padding: 14px 20px 12px;
  background: linear-gradient(180deg, #0c1226 0%, #0f1730 100%);
  border-bottom: 1px solid #1a2a4a;
  border-left: 2px solid #6a5acd55;
}
#void-card-eyebrow {
  font-size: 0.72rem;
  color: #8b87b0;
  letter-spacing: 0.08em;
  text-transform: lowercase;
  margin-bottom: 6px;
}
#void-card-prompt {
  font-size: 1rem;
  color: #e8e6f0;
  line-height: 1.45;
  font-style: italic;
  margin-bottom: 10px;
}
#void-card-answer {
  background: transparent;
  color: #b9b2dd;
  border: 1px solid #4a3f7a;
  border-radius: var(--radius);
  padding: 4px 14px;
  font-size: 0.82rem;
  cursor: pointer;
  letter-spacing: 0.02em;
}
#void-card-answer:hover { background: #1a1530; color: #d4cdef; border-color: #6a5acd; }

/* The Moment — a distinct night-sky "place" entered from the Void card */
#void-card-actions { display: flex; gap: 10px; align-items: center; }
#moment-enter {
  background: linear-gradient(135deg, #2a2350 0%, #1a2a4a 100%);
  color: #f3e1b8; border: 1px solid #6a5acd;
  border-radius: var(--radius); padding: 6px 16px;
  font-size: 0.86rem; cursor: pointer; letter-spacing: 0.02em;
}
#moment-enter:hover { background: linear-gradient(135deg, #352b6a 0%, #24365e 100%); }

#moment-view { position: relative; }
#moment-back {
  margin: 10px 0 0 14px;
}
#moment-sky {
  margin: 8px 14px 16px;
  padding: 28px 22px 22px;
  border-radius: var(--radius);
  border: 1px solid #2a2350;
  color: #e8e6f0;
  background:
    radial-gradient(1px 1px at 20% 30%, #ffffffcc 0, transparent 100%),
    radial-gradient(1px 1px at 70% 20%, #ffffffaa 0, transparent 100%),
    radial-gradient(1.5px 1.5px at 45% 60%, #ffffff99 0, transparent 100%),
    radial-gradient(1px 1px at 85% 70%, #ffffff88 0, transparent 100%),
    radial-gradient(1px 1px at 30% 80%, #ffffff77 0, transparent 100%),
    linear-gradient(180deg, #0a0a20 0%, #12123a 55%, #0d1a33 100%);
}
#moment-title {
  text-align: center; font-size: 1.35rem; color: #f3e1b8;
  letter-spacing: 0.04em; margin-bottom: 4px;
}
#moment-subtitle {
  text-align: center; font-size: 0.78rem; color: #9a94c4;
  letter-spacing: 0.03em; margin-bottom: 18px;
}
#moment-view-prompt {
  text-align: center; font-size: 1.05rem; font-style: italic;
  color: #eae7f5; line-height: 1.5; margin-bottom: 16px;
}
#moment-box { margin: 0 auto; max-width: 520px; }
#moment-input {
  width: 100%; box-sizing: border-box;
  background: #0a0f24cc; color: #e6e6e6;
  border: 1px solid #3a3466; border-radius: var(--radius-sm);
  padding: 10px 12px; font-size: 0.92rem; line-height: 1.45;
  resize: vertical; min-height: 64px; font-family: inherit;
}
#moment-input:focus { outline: none; border-color: #f3e1b8; }
#moment-answer-row {
  display: flex; gap: 12px; align-items: center; margin-top: 8px;
}
#moment-error { color: #ff9c9c; flex: 1; font-size: 0.78rem; }
#moment-share {
  margin-left: auto; padding: 6px 16px;
  background: #2a2340; color: #f3e1b8;
  border: 1px solid #6a5acd; border-radius: var(--radius);
  cursor: pointer; font-size: 0.84rem; letter-spacing: 0.02em;
}
#moment-share:hover { background: #352b52; }
#moment-locked-hint {
  margin: 10px auto 0; max-width: 520px;
  text-align: center; font-size: 0.82rem; font-style: italic; color: #b8a76f;
}
#moment-wall-header {
  margin: 22px auto 10px; max-width: 520px;
  font-size: 0.74rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: #9a94c4; border-bottom: 1px solid #2a2350; padding-bottom: 6px;
}
#moment-wall {
  margin: 0 auto; max-width: 520px;
  display: flex; flex-direction: column; gap: 8px;
}
.moment-answer {
  background: #ffffff0d; border: 1px solid #ffffff1a;
  border-left: 2px solid #f3e1b8; border-radius: var(--radius-sm);
  padding: 9px 13px; color: #eae7f5; font-size: 0.92rem;
  line-height: 1.5;
  display: flex; align-items: flex-start; gap: 8px;
}
.moment-answer-text { flex: 1; white-space: pre-wrap; word-wrap: break-word; }
/* Keepsake "share as card" icon button — matches other row-action buttons. */
.keepsake-btn {
  background: none; border: none; cursor: pointer;
  color: var(--text-faint); font-size: 0.9rem; padding: 2px 4px; line-height: 1;
  flex: 0 0 auto;
}
.keepsake-btn:hover { color: var(--accent-hover); }

/* Keepsake preview modal — see the card before Share/Save. */
#keepsake-modal {
  position: fixed; top: 50%; left: 50%;
  transform: translate(-50%, -50%); margin: 0;
  border: 1px solid var(--border); border-radius: var(--radius);
  /* Glass: native <dialog> in the top layer, no fixed descendants inside
     it, so backdrop-filter goes directly on the element. */
  background: var(--glass);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);   /* Safari needs the prefix */
  color: var(--text);
  padding: 16px; max-width: 92vw;
}
#keepsake-modal::backdrop { background: rgba(6, 10, 20, 0.72); }
#keepsake-preview-wrap { display: flex; justify-content: center; }
#keepsake-preview {
  display: block; max-width: min(360px, 78vw); max-height: 64vh;
  width: auto; height: auto; border-radius: var(--radius-sm);
  box-shadow: 0 6px 24px rgba(0,0,0,0.5);
}
#keepsake-actions {
  display: flex; gap: 10px; justify-content: center;
  flex-wrap: wrap; margin-top: 16px;
}
#keepsake-actions button {
  padding: 8px 16px; border-radius: var(--radius); cursor: pointer;
  font-size: 0.9rem; letter-spacing: 0.02em;
  background: var(--bg-raised); color: #f0e6d2; border: 1px solid var(--border);
}
#keepsake-actions button:hover { background: var(--accent-wash); }
#keepsake-share { background: #24365e; border-color: var(--accent); color: #cfe9fb; }
#keepsake-share:hover { background: #2c4370; }
#keepsake-share[hidden] { display: none; }
#keepsake-copy-img[hidden] { display: none; }
#keepsake-close { background: none; border-color: var(--border); color: var(--text-muted); }
#keepsake-close:hover { background: var(--bg-raised); }
#keepsake-hint {
  margin: 12px auto 0; max-width: 360px;
  font-size: 0.74rem; line-height: 1.4; color: var(--text-faint); text-align: center;
}
.confessions-moment-dot {
  display: inline-block; width: 7px; height: 7px;
  margin-left: 5px; border-radius: 50%;
  background: #f3e1b8; vertical-align: middle;
}
.confessions-moment-dot[hidden] { display: none; }

/* Once-per-day nudge toast (clickable) */
#moment-nudge-toast {
  position: fixed; bottom: 195px; left: 50%;
  transform: translate(-50%, 60px);
  background: linear-gradient(135deg, #1a1540 0%, #16213e 100%);
  color: #f3e1b8; border: 1px solid #6a5acd; border-radius: var(--radius-sm);
  padding: 10px 18px; font-size: 0.85rem; letter-spacing: 0.02em;
  opacity: 0; pointer-events: none; transition: transform 220ms ease, opacity 220ms ease;
  z-index: 1000; cursor: pointer; max-width: 90vw; text-align: center;
}
/* pointer-events: none above means the always-in-the-DOM, invisible-once-
   dismissed toast can never again block clicks at this screen position
   (this is exactly where the bottom dock now lives) — only .visible should
   ever intercept a tap. */
#moment-nudge-toast.visible { transform: translate(-50%, 0); opacity: 1; pointer-events: auto; }

#confession-void-header {
  font-size: 0.82rem;
  color: #b9b2dd;
  background: #1a1530;
  border: 1px solid #4a3f7a;
  border-radius: var(--radius-sm);
  padding: 6px 10px;
  margin-bottom: 6px;
  display: flex; align-items: center; gap: 8px;
}
#confession-void-header[hidden] { display: none; }
#confession-void-prompt { flex: 1; font-style: italic; color: #d4cdef; }
#confession-void-cancel {
  background: transparent;
  color: #8b87b0;
  border: none;
  cursor: pointer;
  font-size: 1.1rem;
  line-height: 1;
  padding: 0 4px;
}
#confession-void-cancel:hover { color: #d4cdef; }

.confession.void-tagged {
  border-left-color: #6a5acd99;
}
.confession-void-marker {
  display: inline-block;
  margin-right: 6px;
  opacity: 0.7;
  font-size: 0.85rem;
}

.halls-empty {
  margin-top: 40px;
  text-align: center;
  color: var(--text-faint);
  font-size: 0.95rem;
  line-height: 1.5;
}
.halls-empty-sub {
  display: inline-block;
  margin-top: 8px;
  font-size: 0.82rem;
  color: var(--text-faint);
  font-style: italic;
}
.halls-question {
  margin-bottom: 22px;
  padding: 12px 14px;
  background: rgba(255, 213, 79, 0.04);
  border-left: 3px solid rgba(255, 213, 79, 0.5);
  border-radius: 0 6px 6px 0;
}
.halls-question h3 {
  font-size: 0.95rem;
  font-weight: normal;
  color: #ffd54f;
  margin-bottom: 8px;
  letter-spacing: 0.02em;
}
.halls-question ul {
  margin: 0;
  padding-left: 14px;
  list-style: none;
}
.halls-question li {
  padding: 4px 0 4px 8px;
  font-style: italic;
  color: #d8c8a8;
  font-size: 0.92rem;
  line-height: 1.45;
  border-left: 2px solid rgba(255, 213, 79, 0.18);
  margin-bottom: 2px;
  display: flex;
  align-items: flex-start;
  gap: 10px;
}
.halls-question li .hall-text { flex: 1; }

/* Hall up/down vote buttons. Used in BOTH the thread panel hall and the
 * Halls tab. Compact pill pair; the user's own pick gets a bright
 * highlight. */
.hall-votes {
  flex: 0 0 auto;
  display: inline-flex;
  gap: 4px;
  align-self: center;
}
.hall-vote-up,
.hall-vote-down {
  background: transparent;
  border: 1px solid rgba(255, 213, 79, 0.3);
  border-radius: var(--radius);
  color: var(--text-muted);
  padding: 1px 7px;
  font-size: 0.72rem;
  font-style: normal;
  cursor: pointer;
  transition: background var(--ease), border-color var(--ease), color var(--ease);
  line-height: 1.2;
}
.hall-vote-up:hover   { background: rgba(102, 187, 106, 0.18); color: #66bb6a; border-color: #66bb6a; }
.hall-vote-down:hover { background: rgba(239, 83, 80, 0.18);  color: #ef5350; border-color: #ef5350; }
.hall-vote-up[data-mine="true"] {
  background: #66bb6a;
  color: #0d1730;
  border-color: #66bb6a;
}
.hall-vote-down[data-mine="true"] {
  background: #ef5350;
  color: #0d1730;
  border-color: #ef5350;
}
.hall-vote-up .count,
.hall-vote-down .count { font-variant-numeric: tabular-nums; }

/* Thread-panel hall li also flexes for the same vote-button layout. */
.thread-hall li {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  list-style: none;
}
.thread-hall li .hall-text { flex: 1; }
.thread-hall li::before { content: ""; }   /* override the · prefix when votes are present */
/* Online list entries (moved from #sidebar) */
.user-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 6px 8px;
  margin-bottom: 4px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 0.85rem;
  color: var(--text);
}
.user-item:hover { background: var(--bg-raised); color: var(--accent); }
.user-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user-time { font-size: 0.7rem; color: var(--text-faint); margin-left: 4px; }
.user-orbit-btn {
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 0.95rem;
  padding: 1px 5px;
  margin-left: 6px;
  line-height: 1;
  opacity: 0.55;
  transition: opacity var(--ease), background var(--ease), border-color var(--ease);
}
.user-orbit-btn:hover {
  opacity: 1;
  background: rgba(243, 225, 184, 0.12);
  border-color: rgba(243, 225, 184, 0.4);
}

/* Chat bubbles: .msg is a flex column; .msg-inner is the bubble.
 * Sender on the right, receiver on the left. system/typing/icebreaker keep
 * full-width with no bubble. */
.msg {
  margin-bottom: 6px;
  font-size: 0.9rem;
  line-height: 1.4;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  position: relative;
}
.msg-inner {
  background: var(--bg-raised);
  padding: 8px 14px;
  border-radius: var(--radius);
  /* The row (.msg) stays full-bleed so alignment (flex-start/flex-end)
     pins each bubble to the TRUE left/right screen edge — a centered,
     capped row was tried and made bubbles float in the middle of a wide
     screen, touching neither edge, which read as broken rather than
     deliberate. 75% alone isn't enough on its own on a wide screen (75%
     of 1800px is a 1350px bubble), so it's now a ceiling under an
     absolute one: whichever is smaller governs, so a phone still gets
     75% and nothing on a wide desktop grows past 600px. */
  max-width: min(75%, 600px);
  word-wrap: break-word;
  position: relative;
  line-height: 1.6;
  box-shadow: var(--shadow-soft);
}
.msg.mine { align-items: flex-end; }
.msg.mine .msg-inner {
  background: #17324d;
  color: #e6f4ff;
  border: 1px solid var(--accent-ring);   /* faint accent hairline on own bubbles */
}
.msg.mine { color: var(--accent); }
.msg.public { color: var(--text); }
.msg.private { color: #ff9800; }
.msg.system {
  color: var(--text-faint); font-style: italic; font-size: 0.8rem;
  text-align: center; align-items: center;
}
.msg.typing { color: var(--text-faint); font-style: italic; font-size: 0.8rem; }
.msg.icebreaker {
  color: #ffd54f; background: #ffd54f11; border-left: 2px solid #ffd54f;
  padding: 6px 10px; margin: 8px 0; border-radius: 3px;
}

/* Sender name + timestamp, outside the bubble as a sibling of .msg-inner —
 * .msg's own flex column (see above) aligns this line left/right the same
 * way it aligns the bubble, for free. Given equal specificity to
 * .msg.public/.msg.private/.msg.mine above (two classes) and placed after
 * them in source, so this always wins their color for the meta line — it
 * stays neutral even on a private (orange) or own (accent) message; only
 * the bubble body keeps those semantic colors. Own messages hide the name
 * (redundant — it's you) and, since the separator lives on .msg-time's
 * ::before rather than .msg-name's ::after, hiding the name alone also
 * removes the dangling "name · " with nothing after it. */
.msg .msg-meta { font-size: 0.72rem; color: var(--text-faint); margin-top: 3px; }
.msg.mine .msg-name { display: none; }
.msg:not(.mine) .msg-time::before { content: " · "; }

/* Drop a Song — inline media embeds for YouTube + Spotify. Sized to fit
 * inside the chat bubble. The iframe relies on cross-origin isolation
 * for security; the embed src is built from a regex-validated ID, never
 * raw user input. */
.msg-embed { margin-top: 8px; max-width: 320px; }
.msg-embed iframe {
  display: block; width: 100%; border: 0;
  border-radius: 8px; background: rgba(0, 0, 0, 0.2);
}
.msg-embed-youtube iframe { aspect-ratio: 16 / 9; height: auto; }
.msg-embed-spotify iframe { height: 80px; }
.msg.mine .msg-embed { margin-left: auto; }

/* Message Glow — three tiers based on cumulative reaction count.
 * The glow is on the bubble (.msg-inner), so left/right alignment and
 * mine/private/system color rules are unaffected. Top tier breathes
 * gently so highly-resonant messages feel alive. */
.msg-inner.glow-1 {
  box-shadow: 0 0 12px rgba(255, 213, 79, 0.22);
  transition: box-shadow 600ms ease;
}
.msg-inner.glow-2 {
  box-shadow: 0 0 18px rgba(255, 183, 77, 0.35);
  transition: box-shadow 600ms ease;
}
.msg-inner.glow-3 {
  box-shadow: 0 0 22px rgba(255, 152, 0, 0.5);
  animation: msg-glow-pulse 4s ease-in-out infinite;
}
@keyframes msg-glow-pulse {
  0%, 100% { box-shadow: 0 0 22px rgba(255, 152, 0, 0.45); }
  50%      { box-shadow: 0 0 32px rgba(255, 183, 77, 0.7);  }
}
/* .msg-pin is position:absolute against its nearest positioned ancestor
 * (.msg-inner/.vibe-msg-inner/.gathering-msg-inner all have
 * position:relative for exactly this) and holds the add-reaction trigger
 * (every surface) plus the campfire trigger (chat only — appended by
 * appendMessageToTab, absent on Vibe/Gathering/thread replies). Visible on
 * real mouse hover for free via :hover; on touch, a message tap toggles
 * .msg-pin-open (see the delegated click handler below) since there's no
 * real hover state to rely on there.
 *
 * Default position (used as-is by Vibe/Gathering, whose .vibe-msg-inner/
 * .gathering-msg-inner are max-width:100% content-sized spans with no
 * .mine side, so there's no guaranteed gutter to park in): over the
 * top-right corner. Public/Private/Pair chat bubbles + thread replies
 * override this below — their .msg-inner is capped at max-width:75%, so
 * the cluster is parked in the gutter beside the bubble instead, to avoid
 * covering the message text. */
.msg-pin {
  display: none;
  position: absolute;
  right: 4px;
  top: 2px;
  align-items: center;
  gap: 2px;
  background: var(--bg-raised);
  border-radius: 10px;
  padding: 1px 2px;
}
/* Chat bubbles (.msg-inner, max-width:75%): beside the bubble, vertically
 * centered on it, not over the text. Others' messages (left-aligned) get
 * the cluster on their right, in the gutter; own messages (.msg.mine,
 * right-aligned) get it on their left. */
.msg-inner > .msg-pin {
  right: auto;
  left: calc(100% + 6px);
  top: 50%;
  transform: translateY(-50%);
}
.msg.mine .msg-inner > .msg-pin {
  left: auto;
  right: calc(100% + 6px);
}
@media (hover: hover) and (pointer: fine) {
  .msg:hover .msg-pin { display: flex; }
}
.msg-pin.msg-pin-open {
  display: flex;
}
.msg-pin-emoji, .msg-pin-reply {
  cursor: pointer;
  background: transparent;
  border: 0;
  border-radius: 50%;
  width: 18px;
  height: 18px;
  text-align: center;
  line-height: 18px;
  padding: 0;
  color: var(--text-muted);
}
/* The two glyphs need DIFFERENT font-sizes to look the same size, which is why
   this isn't in the shared rule above. ☺ (U+263A) draws its face at roughly
   two-thirds of its em box, while ↩ (U+21A9) fills most of its own — so at an
   equal font-size the react trigger reads noticeably smaller than the reply
   arrow beside it. Same reason .reaction-add below runs larger than the
   surrounding pill text. `color` and the hover inversion only do anything at
   all because these are line-art glyphs; a colour emoji renders in its own
   palette and ignores both. */
.msg-pin-emoji { font-size: 1.05rem; }
.msg-pin-reply { font-size: 0.85rem; }
.msg-pin-emoji:hover, .msg-pin-reply:hover { background: var(--accent); color: var(--bg-deep); }
.msg-pin .campfire-btn { margin-left: 0; }   /* sits beside the emoji trigger inside the pin's own flex row + gap, not pushed right */

.msg-footer { display: flex; align-items: center; margin-top: 4px; }
/* Quote excerpt on a reply, on the live-stream surfaces (Public, Private,
 * Pair, Vibe, Gathering). Static text, not a link — the quoted message may be
 * far above or already gone from the DOM, so there's nothing to jump to.
 * Nearby and Campfire use tree position instead (see reply-tree.js). */
.msg-reply-quote {
  display: block;
  font-size: 0.76rem;
  color: var(--text-muted);
  border-left: 2px solid var(--accent);
  padding: 2px 0 2px 8px;
  margin: 3px 0;
  line-height: 1.35;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Own bubbles (.msg.mine .msg-inner) are a fixed dark/blue fill (#17324d,
 * or var(--accent) on minimal), not the token pair var(--text-muted) /
 * var(--accent) above were tuned against — on minimal that border is
 * literally invisible (same color as the bubble) and the quote text fails
 * contrast on both minimal and dusk. White-based overrides measured against
 * both: quote text 5.44:1 on minimal's #0b5f91, 9.89:1 on dark skins'
 * #17324d; border 3.23:1 on minimal (clears the 3:1 non-text bar; 0.45 alpha
 * measured 2.65, too faint). */
.msg.mine .msg-reply-quote {
  color: rgba(255, 255, 255, 0.85);
  border-left-color: rgba(255, 255, 255, 0.55);
}
/* Plain-text links (renderMessageBody's linkifyEscape) are unstyled, so they
 * render in the browser default link blue — near-invisible on own bubbles'
 * dark/blue fill for the same reason as the quote above. Match the
 * surrounding bubble text color instead of hardcoding a new one. */
.msg.mine a {
  color: inherit;
}
.reactions { display: block; margin-top: 4px; }
.reactions:empty { display: none; }   /* no gap under messages with zero reactions */
.msg-footer .reactions { margin-top: 0; }   /* .msg-footer already carries the top gap */
.reaction-badge {
  display: inline-block;
  background: var(--bg-raised);
  border: 1px solid transparent;
  border-radius: var(--radius);
  padding: 1px 6px;
  font-size: 0.75rem;
  margin-left: 3px;
  cursor: pointer;
  transition: background var(--ease), border-color var(--ease);
}
.reaction-badge:hover { background: var(--accent-wash); }
/* Highlight the badge for the emoji THIS user picked. Click to remove. */
.reaction-badge[data-mine="true"] {
  background: var(--accent);
  color: var(--bg-sunken);
  border-color: var(--accent);
}
.reaction-badge[data-mine="true"]:hover { background: var(--accent-hover); }

#chat-input {
  padding: 10px 14px;
  border-top: 1px solid var(--border-soft);
  /* Glass: the emoji picker and plus menu anchored to this bar are always
     appended to document.body (see popover.js), never nested inside
     #chat-input, so they can't be trapped by backdrop-filter here — safe
     directly, unlike header. */
  background: var(--glass-chrome);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);   /* Safari needs the prefix */
  flex: 0 0 auto;
}
/* The actual input row: capped and centered so it doesn't stretch edge-to-
   edge on a wide screen (#chat-input above stays full width so the glass
   bar and its border-top still span the whole app — only the content
   inside is constrained). width:100% + max-width means the ratio of row-
   width to screen-width is 1 on any screen narrower than 880px, then falls
   as the screen grows past that — proportionally narrower on a big display,
   full-width on a small one, with no fixed breakpoint. */
.chat-input-row {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  max-width: 880px;
  width: 100%;
  margin: 0 auto;
}

#message-input {
  flex: 1;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-sunken);
  color: var(--text);
  font-size: 0.9rem;
  /* textarea-specific: inherit chat font, no default font-family quirks. */
  font-family: inherit;
  /* Auto-grow up to ~5 lines. JS resets height on input; max-height clamps. */
  resize: none;
  line-height: 1.4;
  min-height: 36px;
  max-height: 7em;
  overflow-y: auto;
  /* Dynamic Typography: --type-speed (0 calm .. 1 breathless) is set live by
   * public/js/dynamic-typography.js from keystroke cadence. */
  font-weight: calc(400 + var(--type-speed, 0) * 300);
  letter-spacing: calc(0.03em - var(--type-speed, 0) * 0.05em);
  transition: font-weight 0.2s ease, letter-spacing 0.2s ease,
    border-color var(--ease), box-shadow var(--ease);
}
#message-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-ring);
}

#send-button {
  height: 40px;
  box-sizing: border-box;
  padding: 0 14px;
  background: var(--accent);
  color: var(--bg-deep);
  border: none;
  border-radius: var(--radius);
  cursor: pointer;
  font-weight: bold;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background var(--ease);
}
#send-button:hover { background: var(--accent-hover); }
/* Deep Breath Pacing — slowmode countdown state. min-width prevents the
 * button from visibly shrinking as digits change (15s → 9s → 1s). */
#send-button.throttled {
  background: var(--bg-raised);
  color: var(--text-muted);
  cursor: not-allowed;
  font-variant-numeric: tabular-nums;
  min-width: 3em;
}
#send-button.throttled:hover { background: var(--bg-raised); }
.tab .slowmode-badge {
  font-size: 0.72rem;
  opacity: 0.6;
  margin-left: 6px;
  font-weight: normal;
}
.tab .slowmode-badge[hidden] { display: none; }

/* Marker pulse animation */
@keyframes marker-pulse {
  0% { transform: scale(1); opacity: 1; }
  50% { transform: scale(2); opacity: 0.5; }
  100% { transform: scale(1); opacity: 1; }
}
.pulse {
  animation: marker-pulse 0.8s ease-out;
}

/* Mobile */
@media (max-width: 768px) {
  /* iOS Safari auto-zooms the page when a focused text input/textarea has
   * font-size under 16px. This is the fix for the whole class of bug (zoom
   * on focus, and the zoom staying stuck through send since nothing zooms
   * if it never started) — one rule, can't miss a composer, no per-input
   * enumeration to maintain as new ones are added. !important is required:
   * several composer inputs (.room-composer-input, .post-composer-input,
   * .thread-reply-input) declare their own font-size later in this file at
   * equal specificity, which would otherwise win the cascade tie and
   * silently defeat this rule on exactly the inputs it exists to fix; ID
   * selectors like #status-input/#message-input also outrank a bare
   * attribute/element selector regardless of source order. */
  input[type="text"], textarea { font-size: 16px !important; }
  header { padding: 8px 12px; gap: 8px; }
  header h1 { font-size: 0.95rem; }
  .header-controls { gap: 6px; }
  .mood-header-btn { padding: 4px 6px; }
  .header-info-online-word { display: none; }   /* "· 1 online" → "· 1" — the word costs real width the count doesn't need */
  .tab { max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
  .tab-panel[data-tab-id="world"] {
    flex-direction: column;
  }
  .tab-panel[data-tab-id="world"] #map { flex: 0 0 45vh; }
  .tab-panel[data-tab-id="world"] #online-list { flex: 1; }
  .user-time { display: none; }
}

/* Desktop */
@media (min-width: 769px) {
  .tab-panel[data-tab-id="world"] {
    flex-direction: row;
  }
  .tab-panel[data-tab-id="world"] #map { flex: 1; }
  .tab-panel[data-tab-id="world"] #online-list {
    flex: 0 0 280px;
    border-left: 1px solid var(--border);
    border-top: none;
  }
}

.sit-btn {
  background: var(--bg-raised);
  color: #f0e6d2;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 4px 10px;
  font-size: 0.78rem;
  cursor: pointer;
  white-space: nowrap;
}
.sit-btn:hover { background: var(--accent-wash); }
#plane-modal {
  /* Glass: native <dialog>, no fixed descendants inside it, so
     backdrop-filter goes directly on the element. */
  background: var(--glass);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);   /* Safari needs the prefix */
  color: #f0e6d2;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 18px 20px;
  width: min(420px, 90vw);
}
#plane-modal::backdrop { background: rgba(5, 8, 16, 0.7); }
#plane-modal h3 {
  font-size: 1rem;
  margin-bottom: 12px;
  color: #f3e1b8;
  font-weight: normal;
  letter-spacing: 0.03em;
}
#plane-text {
  width: 100%;
  background: var(--bg-sunken);
  color: #f0e6d2;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  font-size: 0.9rem;
  font-family: inherit;
  resize: vertical;
}
#plane-text:focus { outline: none; border-color: #f3e1b8; }
.plane-modal-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 12px;
}
#plane-counter {
  flex: 1;
  font-size: 0.78rem;
  opacity: 0.6;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
}
#plane-counter.warn { color: #ff9800; opacity: 1; }
#plane-cancel, #plane-throw {
  background: var(--bg-raised);
  color: #f0e6d2;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 4px 14px;
  font-size: 0.82rem;
  cursor: pointer;
}
#plane-throw[disabled] { opacity: 0.4; cursor: not-allowed; }
#plane-throw:not([disabled]):hover { background: var(--accent-wash); }
#plane-toast {
  position: fixed;
  bottom: 195px;
  left: 50%;
  transform: translate(-50%, 60px);
  background: var(--bg-surface);
  color: #f3e1b8;
  border: 1px solid #4a5a7c;
  border-radius: 6px;
  padding: 8px 16px;
  font-size: 0.85rem;
  letter-spacing: 0.03em;
  opacity: 0;
  transition: transform 200ms ease, opacity 200ms ease;
  z-index: 1000;
  pointer-events: none;
}
#plane-toast.visible {
  transform: translate(-50%, 0);
  opacity: 1;
}

.plane-arriving {
  position: fixed;
  bottom: 24px;
  right: 24px;
  width: 80px;
  height: 80px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  cursor: pointer;
  transition: transform 1.2s ease-out, opacity 800ms ease;
  z-index: 900;
  filter: drop-shadow(0 0 12px rgba(243, 225, 184, 0.6));
}
.plane-arriving.from-left  { transform: translate(-120vw, -10vh) rotate(-15deg); }
.plane-arriving.from-right { transform: translate(120vw, -10vh) rotate(20deg); }
.plane-arriving.from-top   { transform: translate(0, -100vh) rotate(160deg); }
.plane-arriving.plane-landed { transform: translate(0, 0) rotate(-8deg); }
.plane-arriving.plane-departing { transform: translate(0, -80vh) rotate(45deg); opacity: 0; transition: transform 800ms ease-in, opacity 800ms ease; }

.plane-svg { width: 48px; height: 48px; }

.plane-hint {
  font-size: 0.7rem;
  color: #f3e1b8;
  opacity: 0.8;
  animation: plane-hint-pulse 2s ease-in-out infinite;
  white-space: nowrap;
}
@keyframes plane-hint-pulse {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 0.95; }
}
.plane-card {
  position: fixed;
  bottom: 24px;
  right: 24px;
  width: min(360px, 90vw);
  background: var(--bg-surface);
  color: #f0e6d2;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 14px 16px;
  z-index: 900;
  box-shadow: 0 0 24px rgba(243, 225, 184, 0.25);
}
.plane-card-text {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 12px;
}
.plane-layer {
  font-size: 0.92rem;
  line-height: 1.4;
  padding-left: 10px;
  border-left: 2px solid var(--border);
  color: #f3e1b8;
}
.plane-layer + .plane-layer { border-left-color: #6b8cb3; }
.plane-reply {
  width: 100%;
  background: var(--bg-sunken);
  color: #f0e6d2;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 6px 8px;
  font-size: 0.85rem;
  font-family: inherit;
  resize: none;
  height: 50px;
}
.plane-reply:focus { outline: none; border-color: #f3e1b8; }
.plane-reply[disabled] { opacity: 0.4; cursor: not-allowed; }
.plane-card-actions {
  display: flex;
  gap: 10px;
  margin-top: 10px;
}
.plane-keep, .plane-rethrow {
  flex: 1;
  background: var(--bg-raised);
  color: #f0e6d2;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 6px 10px;
  font-size: 0.82rem;
  cursor: pointer;
}
.plane-keep:hover, .plane-rethrow:hover { background: var(--accent-wash); }

#kept-corner {
  position: fixed;
  bottom: 24px;
  left: 24px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 800;
}
.kept-icon {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: rgba(243, 225, 184, 0.1);
  border: 1px solid rgba(243, 225, 184, 0.3);
  color: #f3e1b8;
  font-size: 1rem;
  cursor: pointer;
  filter: drop-shadow(0 0 6px rgba(243, 225, 184, 0.3));
  transition: transform var(--ease), background var(--ease);
}
.kept-icon:hover { transform: scale(1.1); background: rgba(243, 225, 184, 0.2); }

.kept-overlay {
  position: fixed;
  inset: 0;
  background: rgba(5, 8, 16, 0.7);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 950;
}
.kept-overlay .plane-card {
  position: static;
  width: min(420px, 90vw);
}

@media (max-width: 640px) {
  #kept-corner { gap: 6px; }
  .kept-icon { width: 26px; height: 26px; font-size: 0.8rem; }
}

.plane-card.plane-departing {
  transform: translate(0, -80vh) rotate(45deg);
  opacity: 0;
  transition: transform 800ms ease-in, opacity 800ms ease;
}

.plane-mute-btn {
  font-size: 0.95rem;
  padding: 4px 10px;
  min-width: 36px;
  text-align: center;
}

/* --- Mood Check-in --- */

.mood-header-btn {
  background: var(--mood-aura, var(--bg-raised));
  color: #f0e6d2;
  border: 1px solid var(--mood-aura, var(--border));
  border-radius: 4px;
  padding: 4px 10px;
  font-size: 0.78rem;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.2s, border-color 0.2s;
}
.mood-header-btn:hover { filter: brightness(1.15); }

.mood-status-label {
  background: transparent;
  border: 0;
  color: #f0e6d2;
  font-size: 0.85rem;
  text-align: left;
  cursor: pointer;
  padding: 2px 0;
}
.mood-status-label:hover { color: var(--accent); }

#mood-modal {
  /* Glass: native <dialog>, no fixed descendants inside it, so
     backdrop-filter goes directly on the element. */
  background: var(--glass);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);   /* Safari needs the prefix */
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lift);
  padding: 32px 28px;
  width: min(560px, 94vw); /* wider than other dialogs — 5 mood columns need it */
}
/* Single neutral dark scrim, not tokenized: it sits behind a blurred modal
   where warm-vs-cool makes no visible difference, so a third glass token
   would add nothing. */
#mood-modal::backdrop { background: rgba(5, 8, 16, 0.7); backdrop-filter: blur(4px); }
#mood-modal h3 {
  font-size: 1.6rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text);
  text-align: left;
  margin-bottom: 4px;
}
.mood-modal-sub { color: var(--text-muted); font-size: 0.85rem; margin-bottom: 22px; }

.mood-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 10px;
  margin-bottom: 14px;
}
.mood-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  aspect-ratio: 1;
  min-width: 0; /* grid items default to min-width:auto, which lets long uppercase
                   labels (e.g. "overwhelmed") force the 1fr track wider than its
                   share and overflow the grid — allow it to shrink instead */
  background: var(--bg-surface);
  color: var(--text);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  padding: 10px 6px;
  font-size: 0.78rem;
  cursor: pointer;
  transition: transform var(--ease), background var(--ease), border-color var(--ease), box-shadow var(--ease);
}
.mood-btn:hover {
  transform: translateY(-4px);
  background: var(--accent-wash);
  /* each mood glows its OWN aura color on hover (set inline per-button by openMoodPicker()
     from the MOODS table) — this is the semantic mood-aura color, never re-hue it */
  border-color: var(--mood-aura, var(--border));
  box-shadow: var(--shadow-lift);
}
.mood-btn .mood-emoji { font-size: 1.6rem; line-height: 1; transition: transform var(--ease); }
.mood-btn:hover .mood-emoji { transform: scale(1.1); }
.mood-btn .mood-label {
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--text-muted);
}
.mood-btn:hover .mood-label { color: var(--accent); }

/* MOTION POLICY (whole app, product decision 2026-08-22): every visual effect
   plays regardless of the OS "reduce motion" setting. There is deliberately no
   @media (prefers-reduced-motion: reduce) block anywhere in this file, and the
   five that used to live here (header ECG, dock nav icons, nav flyout entrance,
   the Confessions star/breeze/mote ambience, and the breathe ring/circle) were
   removed on purpose — the ambient motion IS the product's character, and it
   was chosen over honouring that setting. Do not re-add suppression without
   asking; the same applies to sit/sit.css and sit/sit.js.
   Cost worth knowing: the setting exists for vestibular disorders and motion
   sickness, so this trades that accommodation away. The gentlest lever if it
   ever needs walking back is slowing effects rather than disabling them — how
   #pulse-glow was already handled (server/pulse.js: PULSE_TICK_INTERVAL_MS is
   2s, not 1s) so it reads as settling rather than urgent.
   If a block is ever added back: put it in ONE block at the very END of this
   file. @media adds no specificity, so a block placed here silently loses on
   source order to the base rules defined further down — that exact bug shipped
   once and looked like the feature working. */

.mood-skip-link {
  display: block;
  margin: 0 auto;
  background: none;
  border: none;
  color: var(--text-faint);
  font-size: 0.78rem;
  cursor: pointer;
  text-decoration: underline;
}
.mood-skip-link:hover { color: var(--text-muted); }

/* 640px override lives here (after the base rule above) rather than in the
   earlier #kept-corner 640px block, so it isn't lost to source-order cascade. */
@media (max-width: 640px) {
  .mood-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 480px) {
  .mood-grid { grid-template-columns: repeat(2, 1fr); }
}

/* Map marker — mood emoji with aura halo */
.mood-marker {
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: radial-gradient(circle, var(--aura, rgba(120, 144, 156, 0.35)) 0%, transparent 70%);
  position: relative;
}
.mood-marker-emoji {
  font-size: 18px;
  line-height: 1;
  filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.6));
}
.mood-marker-null {
  background: radial-gradient(circle, rgba(120, 120, 120, 0.35) 0%, transparent 70%);
}
.mood-marker-null::after {
  content: '';
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #888;
  position: absolute;
}
.mood-marker-self {
  outline: 2px solid #4fc3f7;
  outline-offset: -1px;
}
.mood-marker.pulse {
  animation: marker-pulse 0.8s ease-out;
}

/* --- Orbiting / Silent Sitting — anonymous mood-emoji dots in a slow ring
 *     around the orbited user's marker. Visible to everyone. */
.orbit-ring {
  position: absolute;
  /* Extend the ring beyond the 32px marker so dots sit OUTSIDE the marker
   * proper, around its perimeter. */
  inset: -16px;
  pointer-events: none;
  animation: orbit-spin 60s linear infinite;
  /* The Leaflet divIcon parent has overflow:visible already; the dots
   * positioned outside the marker bounds appear correctly. */
}
.orbit-dot {
  position: absolute;
  top: 50%;
  left: 50%;
  font-size: 13px;
  line-height: 1;
  filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.7));
  /* Place each dot at a fixed angle around the ring: idx * (360 / total).
   * Outer translate(0, -28px) offsets along the ring's radius. */
  --angle: calc(var(--idx) * (360deg / var(--total)));
  transform: translate(-50%, -50%) rotate(var(--angle)) translate(0, -28px);
}
.orbit-overflow {
  position: absolute;
  bottom: -6px;
  right: -6px;
  font-size: 0.65rem;
  font-weight: bold;
  color: #f3e1b8;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
  background: rgba(0, 0, 0, 0.4);
  border-radius: 6px;
  padding: 0 3px;
  line-height: 1.2;
}
@keyframes orbit-spin {
  to { transform: rotate(360deg); }
}

.emotive-toggle {
  cursor: pointer;
  opacity: 0.4;
  transition: opacity 0.2s;
}
.emotive-toggle:has(input:checked) { opacity: 1; }
.emotive-toggle input { display: none; }

/* Location-hidden toggle. Inverted semantics vs other toggles:
 * default (unchecked) = location SHOWN (normal map-pin icon),
 * checked = location HIDDEN (same icon, red, with its slash line shown).
 * The icon stays full opacity in both states so it remains discoverable.
 * The slash lives inside the SVG as a <line class="icon-slash"> (last
 * child, hidden by default) rather than a text-decoration — CSS
 * text-decoration has no effect on inline SVG content. */
.location-hidden-toggle {
  cursor: pointer;
  opacity: 1;
  transition: color 0.2s;
}
.location-hidden-toggle input { display: none; }
.icon-slash { opacity: 0; }
.location-hidden-toggle:has(input:checked) { color: #ef5350; }
.location-hidden-toggle:has(input:checked) .icon-slash { opacity: 1; }

/* --- Random Pair --- */

#pair-banner {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  background: var(--bg-raised);
  color: #f3e1b8;
  border-bottom: 1px solid var(--border);
  font-size: 0.85rem;
}
#pair-banner.warning {
  background: #5c4a2e;
  color: #ffb74d;
}
#pair-banner-text { flex: 1; }
#pair-banner-action {
  background: var(--bg-raised);
  color: #f3e1b8;
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 3px 10px;
  font-size: 0.78rem;
  cursor: pointer;
}
#pair-banner-action:hover { background: var(--accent-wash); }

#main { position: relative; }
#pair-gate-overlay {
  position: absolute;
  inset: 0;
  background: rgba(13, 23, 48, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;   /* above Leaflet's pane z-indexes (max 700) */
  cursor: pointer;
}
.pair-gate-card {
  background: var(--bg-surface);
  color: #f0e6d2;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 24px 32px;
  text-align: center;
  box-shadow: 0 0 32px rgba(243, 225, 184, 0.25);
}
.pair-gate-card h3 {
  font-size: 1.1rem;
  font-weight: normal;
  color: #f3e1b8;
  margin-bottom: 8px;
}
.pair-gate-card p {
  font-size: 0.9rem;
  opacity: 0.85;
  margin-bottom: 12px;
}
.pair-gate-countdown {
  font-size: 0.78rem;
  color: var(--text-muted);
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
}

/* --- Breathe Together ---
 * Same warm/cool orb + ripple rings as the standalone /sit/ breathe page
 * (sit/sit.css) — same semantic ember colors (Global Constraint 5), ported
 * here so the random-matched version reads as the same experience. Ticks
 * arrive over the main /ws at 10Hz (server/breathe-match.js); the CSS
 * transition below eases between them, so no rAF loop is needed here. */
.breathe-stage {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 32px;
  background: radial-gradient(circle at 50% 45%, rgba(20, 32, 61, 0.4) 0%, rgba(20, 32, 61, 0) 60%), var(--bg-deep);
}

.breathe-orb {
  position: relative;
  width: 220px;
  height: 220px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.breathe-circle {
  position: relative;
  z-index: 2;
  width: 130px;
  height: 130px;
  border-radius: 50%;
  background: radial-gradient(circle at 38% 32%, #fbefd6 0%, #f3e1b8 30%, #cba876 62%, #8a6f4a 88%, rgba(138, 111, 74, 0) 100%);
  box-shadow: 0 0 32px 5px rgba(243, 225, 184, 0.26), 0 0 70px 16px rgba(243, 225, 184, 0.09), inset 0 0 24px rgba(255, 255, 255, 0.08);
  transition: transform 100ms linear, background 900ms cubic-bezier(0.4, 0, 0.2, 1), box-shadow 900ms cubic-bezier(0.4, 0, 0.2, 1);
}
.breathe-circle.cool {
  background: radial-gradient(circle at 38% 32%, #eef6ff 0%, #d6e6f5 30%, #93b3d4 62%, #4c6685 88%, rgba(76, 102, 133, 0) 100%);
  box-shadow: 0 0 32px 5px rgba(107, 140, 179, 0.28), 0 0 70px 16px rgba(107, 140, 179, 0.11), inset 0 0 24px rgba(255, 255, 255, 0.10);
}

.breathe-ring {
  position: absolute;
  z-index: 1;
  width: 140px;
  height: 140px;
  border-radius: 50%;
  border: 1.5px solid rgba(243, 225, 184, 0.28);
  opacity: 0;
  animation: breathe-ring-ripple 8s cubic-bezier(0.2, 0.6, 0.4, 1) infinite;
  transition: border-color 900ms ease;
}
.breathe-ring-1 { animation-delay: 0s; }
.breathe-ring-2 { animation-delay: 2.6s; }
.breathe-ring-3 { animation-delay: 5.2s; }
@keyframes breathe-ring-ripple {
  0%   { transform: scale(1);    opacity: 0; }
  14%  { opacity: 0.8; }
  100% { transform: scale(1.75); opacity: 0; }
}
.breathe-circle.cool ~ .breathe-ring { border-color: rgba(107, 140, 179, 0.32); }

/* Names the stranger on the other end, so the orb doesn't read as breathing
 * with no one. Sits above the orb — identity first, then the shared act. */
.breathe-partner {
  font-size: 0.85rem;
  letter-spacing: 0.02em;
  color: var(--text-muted);
  text-align: center;
  transition: opacity 300ms ease;
}

.breathe-caption {
  font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
  font-size: 1.1rem;
  font-style: italic;
  letter-spacing: 0.02em;
  color: #f0e6d2;
  opacity: 0.75;
  transition: opacity 700ms ease;
  text-align: center;
}
.breathe-caption.silent { opacity: 0.12; }


.msg.pair .msg-inner {
  border-left: 2px solid var(--border);
}
.msg.mine.pair { color: var(--accent); }

/* --- Synchronized Pulse --- */

#pulse-strip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 0.78rem;
  color: var(--text-faint);
  flex: 0 0 auto;
}

.pulse-btn {
  background: transparent;
  border: 1px solid transparent;
  border-radius: 50%;
  width: 22px;
  height: 22px;
  font-size: 0.85rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  line-height: 1;
  transition: background var(--ease), border-color var(--ease), transform 0.1s;
}
.pulse-btn:hover { background: rgba(236, 64, 122, 0.15); }
.pulse-btn:active { transform: scale(0.95); }
#pulse-strip.joined .pulse-btn {
  background: rgba(236, 64, 122, 0.25);
  border-color: rgba(236, 64, 122, 0.6);
}
#pulse-strip.joined { color: #ec407a; }
#pulse-countdown {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.68rem;
  opacity: 0.7;
}

/* Each tick: heart icon does a quick scale; if .joined, also runs viewport glow */
.pulse-btn.beating { animation: pulse-beat-btn 600ms ease-out; }
@keyframes pulse-beat-btn {
  0%   { transform: scale(1); }
  20%  { transform: scale(1.35); }
  100% { transform: scale(1); }
}

#pulse-glow {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 950;
  opacity: 0;
  background: radial-gradient(ellipse at center,
              transparent 50%,
              rgba(236, 64, 122, 0.35) 100%);
}
#pulse-glow.pulsing { animation: pulse-glow-edge 600ms ease-out; }
@keyframes pulse-glow-edge {
  0%   { opacity: 0; }
  30%  { opacity: 0.6; }
  100% { opacity: 0; }
}


/* --- Empathy Filter --- */

#empathy-strip {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  background: #5c4a2e;
  color: #ffd180;
  border-top: 1px solid #7a6240;
  font-size: 0.82rem;
  flex: 0 0 auto;
}
#empathy-strip[hidden] { display: none; }
#empathy-strip .empathy-text { flex: 1; }
#empathy-strip button {
  background: #7a6240;
  color: #ffd180;
  border: 1px solid #a08055;
  border-radius: var(--radius-sm);
  padding: 3px 10px;
  font-size: 0.78rem;
  cursor: pointer;
}
#empathy-strip button:hover { background: #8a7250; }
#empathy-strip #empathy-send-anyway {
  background: #5c2e2e;
  border-color: #7c4a4a;
  color: #ffb4a8;
}
#empathy-strip #empathy-send-anyway:hover { background: #6c3a3a; }

#reply-strip {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  background: var(--bg-raised);
  color: #f3e1b8;
  border-top: 1px solid var(--border);
  font-size: 0.82rem;
  flex: 0 0 auto;
}
#reply-strip[hidden] { display: none; }
#reply-strip .rt-label { flex: 1; }
#reply-strip button {
  background: var(--bg-raised);
  color: #f3e1b8;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 2px 8px;
  cursor: pointer;
}

/* --- Stream Mode (live keystrokes) --- */

.stream-toggle {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text-muted);
  padding: 1px 6px;
  font-size: 0.78rem;
  cursor: pointer;
  margin-left: 8px;
  vertical-align: middle;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.stream-toggle:hover { background: rgba(243, 225, 184, 0.1); color: #f3e1b8; }
.stream-toggle.waiting {
  border-color: #ffb74d;
  color: #ffb74d;
  animation: stream-toggle-pulse 1.5s ease-in-out infinite;
}
.stream-toggle.active {
  background: rgba(243, 225, 184, 0.2);
  border-color: #f3e1b8;
  color: #f3e1b8;
}
@keyframes stream-toggle-pulse {
  0%, 100% { opacity: 0.6; }
  50%      { opacity: 1; }
}

.stream-invite-banner {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  background: var(--bg-raised);
  color: #f3e1b8;
  border-bottom: 1px solid var(--border);
  font-size: 0.82rem;
  flex: 0 0 auto;
}
.stream-invite-banner .stream-invite-text { flex: 1; }
.stream-invite-banner button {
  background: var(--bg-raised);
  color: #f3e1b8;
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 3px 10px;
  font-size: 0.78rem;
  cursor: pointer;
}
.stream-invite-banner button:hover { background: var(--accent-wash); }
.stream-invite-banner .stream-decline { background: #5c2e2e; border-color: #7c4a4a; }
.stream-invite-banner .stream-decline:hover { background: #6c3a3a; }
.stream-invite-banner .stream-invite-countdown {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.72rem;
  opacity: 0.7;
}

/* The Venting Stage — one global stage tab, asymmetric room.
 * Empty state offers "Take the Stage"; active state shows speaker stream
 * + listener reaction palette OR speaker textarea.
 * See xscan/chat/specs/2026-06-17-venting-stage-design.md. */
.tab-panel[data-tab-id="stage"] {
  flex-direction: column;
  padding: 0;
}

#stage-empty {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 32px 24px;
  text-align: center;
}
#stage-empty[hidden] { display: none; }
.stage-headline {
  font-size: 1.1rem;
  color: #f3e1b8;
  letter-spacing: 0.02em;
}
.stage-sub {
  font-size: 0.85rem;
  color: var(--text-muted);
  max-width: 360px;
  line-height: 1.5;
}
#stage-take {
  background: rgba(243, 213, 79, 0.16);
  color: #f3e1b8;
  border: 1px solid rgba(243, 213, 79, 0.5);
  border-radius: 22px;
  padding: 8px 22px;
  font-size: 0.95rem;
  letter-spacing: 0.04em;
  cursor: pointer;
  transition: background var(--ease), border-color var(--ease);
  margin-top: 8px;
}
#stage-take:hover:not(:disabled) {
  background: rgba(243, 213, 79, 0.28);
  border-color: rgba(243, 213, 79, 0.8);
}
#stage-take:disabled { opacity: 0.4; cursor: not-allowed; }
.stage-cooldown-hint {
  font-size: 0.78rem;
  color: #c9b97a;
  font-style: italic;
  margin-top: 4px;
}
.stage-cooldown-hint[hidden] { display: none; }
.stage-listener-count {
  font-size: 0.82rem;
  color: var(--text-faint);
  margin-top: 12px;
}

#stage-active {
  flex: 1;
  display: flex;
  flex-direction: column;
  position: relative;
  overflow: hidden;
}
#stage-active[hidden] { display: none; }

.stage-tally {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 10px 16px 8px;
  border-bottom: 1px solid var(--border);
  background: rgba(64, 50, 100, 0.18);
  flex: 0 0 auto;
}
.stage-tally[hidden] { display: none; }
.stage-tally-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 0.85rem;
  background: rgba(243, 225, 184, 0.08);
  border: 1px solid rgba(243, 225, 184, 0.18);
  border-radius: var(--radius);
  padding: 2px 10px;
  color: #f3e1b8;
}
.stage-tally-pill.zero {
  opacity: 0.35;
  background: transparent;
}
.stage-tally-count {
  font-variant-numeric: tabular-nums;
  font-size: 0.78rem;
  font-weight: 600;
}

.stage-stream {
  flex: 1;
  overflow-y: auto;
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  background: linear-gradient(180deg, rgba(20, 20, 50, 0.4) 0%, rgba(13, 23, 48, 0.85) 100%);
}
.stage-msg {
  background: rgba(243, 225, 184, 0.10);
  border-left: 2px solid rgba(243, 213, 79, 0.45);
  border-radius: var(--radius-sm);
  padding: 8px 12px;
  color: #f0e6d2;
  font-size: 0.95rem;
  line-height: 1.5;
  white-space: pre-wrap;
  word-wrap: break-word;
}
.stage-system {
  text-align: center;
  font-size: 0.82rem;
  color: var(--text-faint);
  font-style: italic;
  padding: 12px 0;
}

/* Reaction floats — emojis rise from the bottom of the stage panel,
 * fade, and disappear. Pure-CSS keyframe; one DOM node per fire. */
.stage-floats {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 200px;
  pointer-events: none;
  overflow: hidden;
}
.stage-float {
  position: absolute;
  bottom: 0;
  font-size: 1.6rem;
  opacity: 0;
  animation: stage-float-rise 2.2s ease-out forwards;
}
@keyframes stage-float-rise {
  0%   { bottom: 0;     opacity: 0;   transform: scale(0.7); }
  20%  { opacity: 0.85; transform: scale(1);   }
  100% { bottom: 180px; opacity: 0;   transform: scale(1.05); }
}

#stage-listener-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 4px 16px;
  font-size: 0.78rem;
  color: var(--text-faint);
  background: rgba(20, 30, 60, 0.5);
  border-top: 1px solid var(--border);
  flex: 0 0 auto;
}
.stage-timer {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.85rem;
  color: #f3e1b8;
  letter-spacing: 0.05em;
  font-variant-numeric: tabular-nums;
}
.stage-timer[hidden] { display: none; }

.stage-reactions {
  display: flex;
  gap: 6px;
  justify-content: center;
  padding: 12px 16px;
  flex-wrap: wrap;
  flex: 0 0 auto;
  border-top: 1px solid var(--border);
  background: rgba(13, 23, 48, 0.6);
}
.stage-reactions[hidden] { display: none; }
.stage-react-btn {
  background: rgba(243, 225, 184, 0.06);
  border: 1px solid rgba(243, 225, 184, 0.22);
  border-radius: 50%;
  width: 44px;
  height: 44px;
  font-size: 1.4rem;
  cursor: pointer;
  transition: background var(--ease), border-color var(--ease), transform 0.1s;
}
.stage-react-btn:hover:not(:disabled) {
  background: rgba(243, 225, 184, 0.18);
  border-color: rgba(243, 225, 184, 0.45);
  transform: scale(1.06);
}
.stage-react-btn:disabled {
  opacity: 0.35;
  cursor: not-allowed;
  transform: none;
}

#stage-speaker-bar {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px 16px 14px;
  border-top: 1px solid var(--border);
  background: rgba(13, 23, 48, 0.7);
  flex: 0 0 auto;
}
#stage-speaker-bar[hidden] { display: none; }
/* RoomComposer mounted here inherits stage's dark/gold palette instead of the
   default blue room theme (see public/js/room-composer.js). */
#stage-speaker-bar .room-composer { border-top: none; background: transparent; }
#stage-speaker-bar .room-composer-input {
  background: var(--bg-sunken);
  color: #f0e6d2;
  border: 1px solid rgba(243, 225, 184, 0.3);
}
#stage-speaker-bar .room-composer-input:focus {
  outline: none;
  border-color: rgba(243, 213, 79, 0.6);
}
#stage-speaker-bar .room-composer-input::placeholder { color: rgba(240, 230, 210, 0.35); }
#stage-speaker-actions {
  display: flex;
  justify-content: flex-end;
}
#stage-yield {
  background: transparent;
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 4px 14px;
  font-size: 0.82rem;
  cursor: pointer;
}
#stage-yield:hover {
  color: #f3e1b8;
  border-color: var(--accent);
  background: rgba(243, 225, 184, 0.08);
}

/* Whisper Mode — ephemeral 1-on-1 messaging.
 * Toggle in private subheader, dual-purpose banner (invite UI + active
 * indicator), faded-out vanishing messages.
 * See xscan/chat/specs/2026-06-16-whisper-mode-design.md. */
.whisper-toggle {
  background: transparent;
  border: 1px solid #4a3f7a;
  border-radius: 4px;
  color: var(--text-muted);
  padding: 1px 6px;
  font-size: 0.78rem;
  cursor: pointer;
  margin-left: 8px;
  vertical-align: middle;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.whisper-toggle:hover { background: rgba(185, 178, 221, 0.1); color: #d4cdef; }
.whisper-toggle.waiting {
  border-color: #ffb74d;
  color: #ffb74d;
  animation: stream-toggle-pulse 1.5s ease-in-out infinite;
}
.whisper-toggle.active {
  background: rgba(185, 178, 221, 0.18);
  border-color: #b9b2dd;
  color: #d4cdef;
}

.whisper-banner {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  background: #1e1838;
  color: #d4cdef;
  border-bottom: 1px solid #4a3f7a;
  font-size: 0.82rem;
  flex: 0 0 auto;
}
.whisper-banner.active .whisper-active-text {
  flex: 1;
  font-style: italic;
  letter-spacing: 0.02em;
  opacity: 0.85;
}
.whisper-banner .whisper-invite-text { flex: 1; }
.whisper-banner button {
  background: #4a3f7a;
  color: #e8e6f0;
  border: 1px solid #6a5acd;
  border-radius: 4px;
  padding: 3px 10px;
  font-size: 0.78rem;
  cursor: pointer;
}
.whisper-banner button:hover { background: #5a4f8a; }
.whisper-banner .whisper-decline { background: #5c2e2e; border-color: #7c4a4a; }
.whisper-banner .whisper-decline:hover { background: #6c3a3a; }
.whisper-banner .whisper-invite-countdown {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.72rem;
  opacity: 0.7;
}

/* Whisper messages — slightly muted; fade to zero opacity in the last 5s. */
.msg.whisper { opacity: 0.85; }
.msg.whisper.fading {
  opacity: 0;
  transition: opacity 5s linear;
}

.stream-live {
  display: none;
  align-items: flex-start;
  gap: 8px;
  padding: 8px 12px;
  background: var(--bg-surface);
  border-top: 1px dashed var(--border);
  color: #f3e1b8;
  font-size: 0.88rem;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  white-space: pre-wrap;
  word-wrap: break-word;
  max-height: 4em;
  overflow-y: auto;
  flex: 0 0 auto;
}
.stream-live.visible { display: flex; }
.stream-live-name {
  flex: 0 0 auto;
  font-weight: bold;
  color: var(--text-muted);
  font-family: inherit;
}
.stream-live-text { flex: 1; }
.stream-live-cursor {
  display: inline-block;
  margin-left: 1px;
  color: #f3e1b8;
  animation: stream-cursor-blink 1s step-end infinite;
}
@keyframes stream-cursor-blink {
  50% { opacity: 0; }
}

/* --- Drift Away (graceful exit) --- */

.drift-away-btn {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text-muted);
  padding: 1px 6px;
  font-size: 0.78rem;
  cursor: pointer;
  margin-left: 4px;
  vertical-align: middle;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.drift-away-btn:hover {
  background: rgba(243, 225, 184, 0.15);
  color: #f3e1b8;
  border-color: #f3e1b8;
}

/* Fade animation applied to the panel + tab button on drift. The 600ms here
 * matches the closeTab timeout in onDriftAway() so the tab closes right when
 * the animation finishes. */
.tab-panel.drifting,
.tab.drifting {
  animation: drift-fade 600ms ease forwards;
  pointer-events: none;
}
@keyframes drift-fade {
  0%   { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(-8px); }
}

/* --- Worry Stones (shared fidget object in private tabs) --- */

.stone-toggle {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text-muted);
  padding: 1px 6px;
  font-size: 0.78rem;
  cursor: pointer;
  margin-left: 4px;
  vertical-align: middle;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.stone-toggle:hover {
  background: rgba(243, 225, 184, 0.15);
  color: #f3e1b8;
  border-color: #f3e1b8;
}
.stone-toggle.active {
  background: rgba(180, 130, 90, 0.3);
  color: #ffd180;
  border-color: #d4a060;
}

.worry-stone {
  position: absolute;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%,
              rgba(255, 220, 180, 0.95) 0%,
              rgba(180, 130, 90, 0.9) 60%,
              rgba(80, 50, 30, 0.95) 100%);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.45),
              inset -4px -6px 8px rgba(0, 0, 0, 0.25),
              inset 4px 4px 6px rgba(255, 255, 255, 0.18);
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
  z-index: 50;
  transform: translate(-50%, -50%);
  transition: left 80ms linear, top 80ms linear;
}
.worry-stone:active { cursor: grabbing; }
.worry-stone.held {
  /* Subtle scale on the stone itself; the big visual is the halo + text below. */
  animation: stone-breathe 10s ease-in-out infinite;
}
@keyframes stone-breathe {
  0% {
    transform: translate(-50%, -50%) scale(1);
  }
  40% {                               /* end of inhale */
    transform: translate(-50%, -50%) scale(1.08);
  }
  100% {                              /* end of exhale, back to rest */
    transform: translate(-50%, -50%) scale(1);
  }
}

/* Breathe halo — big radial glow around the stone. Sized larger than a
 * thumb so it's visible even when the user's finger covers the stone. */
.stone-breathe-halo {
  position: absolute;
  width: 280px;
  height: 280px;
  border-radius: 50%;
  pointer-events: none;
  z-index: 49;       /* under the stone (z 50) so the stone stays clickable */
  transform: translate(-50%, -50%) scale(0.35);
  background: radial-gradient(circle at center,
              rgba(255, 200, 140, 0.55) 0%,
              rgba(255, 200, 140, 0.30) 35%,
              rgba(255, 200, 140, 0.08) 60%,
              transparent 80%);
  opacity: 0;
  animation: stone-breathe-halo 10s ease-in-out infinite;
}
@keyframes stone-breathe-halo {
  0% {                                /* start of inhale */
    transform: translate(-50%, -50%) scale(0.35);
    opacity: 0.25;
  }
  40% {                               /* peak of inhale */
    transform: translate(-50%, -50%) scale(1);
    opacity: 0.9;
  }
  50% {                               /* brief hold at top */
    transform: translate(-50%, -50%) scale(1);
    opacity: 0.9;
  }
  100% {                              /* end of exhale */
    transform: translate(-50%, -50%) scale(0.35);
    opacity: 0.25;
  }
}

/* Floating "breathe with me" text. Lives in the panel, above the stone,
 * always visible regardless of where the stone has been dragged.
 * z-index above halo + stone so finger can't hide the cue. */
/* Floating "breathe in… / breathe out…" cue. Centered in the panel so it
 * sits in the middle of the chat area — never overlaps the sub-header
 * controls (Stream toggle, Drift Away, Worry Stones button) or the chat
 * input. Sized large for presence: this IS the meditation focus during a
 * held state, not a side label. Strong text-shadow for legibility on top
 * of whatever messages are scrolled behind it. */
.stone-breathe-text {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none;
  font-size: 1.6rem;
  font-style: italic;
  font-weight: 300;
  letter-spacing: 0.08em;
  color: #ffd180;
  text-shadow:
    0 2px 12px rgba(0, 0, 0, 0.75),
    0 0 24px rgba(0, 0, 0, 0.55);
  opacity: 0;
  animation: stone-breathe-text-fade 10s ease-in-out infinite;
  z-index: 60;
  white-space: nowrap;
  text-align: center;
}
@keyframes stone-breathe-text-fade {
  0%, 100% { opacity: 0.55; }
  40%, 50% { opacity: 1; }
}
@media (max-width: 480px) {
  .stone-breathe-text { font-size: 1.3rem; }
}

.stone-ripple {
  position: absolute;
  width: 64px;
  height: 64px;
  border: 1.5px solid rgba(255, 220, 180, 0.5);
  border-radius: 50%;
  pointer-events: none;
  transform: translate(-50%, -50%) scale(0.3);
  opacity: 0;
  /* transform: scale() is GPU-composited — handles many simultaneous
   * ripples without thrashing the render thread (mobile Safari was
   * crashing on rapid-tap with width/height animation). The border
   * stroke does scale up with the element; at scale 20 the 1.5px border
   * becomes ~30px wide, which actually feels MORE like a real water
   * wave crest (a soft band) than a hairline ring. */
  animation: stone-ripple 4500ms cubic-bezier(0.1, 0.5, 0.3, 1) forwards;
  z-index: 51;
  will-change: transform, opacity;
}
@keyframes stone-ripple {
  0%   { transform: translate(-50%, -50%) scale(0.3); opacity: 0;    }
  10%  {                                              opacity: 0.5;  }
  60%  {                                              opacity: 0.30; }
  100% { transform: translate(-50%, -50%) scale(20);  opacity: 0;    }
}

/* --- Thread panel (Slack-style) for icebreaker replies ---
 * Desktop: 360px-wide sidebar from the right.
 * Mobile: full-screen overlay with z-index above the tab strip. */
.thread-panel {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: 380px;
  max-width: 100vw;
  /* Glass: itself a leaf position:fixed panel (not an ancestor of any other
     fixed element — its reply composer's emoji/plus popovers always append
     to document.body, never to this panel), so backdrop-filter goes
     directly on it. Reply content inside stays opaque per its own rules. */
  background: var(--glass);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);   /* Safari needs the prefix */
  border-left: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  z-index: 1100;
  box-shadow: -8px 0 24px rgba(0, 0, 0, 0.45);
  animation: thread-panel-slide 240ms ease-out;
}
.thread-panel[hidden] { display: none; }
@keyframes thread-panel-slide {
  from { transform: translateX(100%); }
  to   { transform: translateX(0); }
}
@media (max-width: 640px) {
  .thread-panel { width: 100vw; border-left: none; }
}

.thread-header {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px 14px;
  background: var(--bg-sunken);
  border-bottom: 1px solid var(--border);
  flex: 0 0 auto;
}
.thread-title {
  flex: 1;
  font-size: 0.92rem;
  color: #ffd54f;
  font-style: italic;
  line-height: 1.4;
}
.thread-close {
  background: transparent;
  border: none;
  color: var(--text-muted);
  font-size: 1.4rem;
  cursor: pointer;
  padding: 0 6px;
  line-height: 1;
}
.thread-close:hover { color: var(--text); }

.thread-body {
  flex: 1;
  overflow-y: auto;
  padding: 12px 14px;
  background: var(--bg-deep);
}
.thread-hall {
  margin-bottom: 12px;
  padding: 8px 10px 8px 12px;
  border-left: 2px solid rgba(255, 213, 79, 0.5);
  background: rgba(255, 213, 79, 0.04);
  border-radius: 0 4px 4px 0;
}
.thread-hall:empty { display: none; }
.thread-hall .label {
  font-size: 0.72rem;
  color: var(--text-muted);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  margin-bottom: 4px;
  font-style: normal;
}
.thread-hall ul { list-style: none; padding: 0; margin: 0; }
.thread-hall li {
  padding: 3px 0 3px 8px;
  font-style: italic;
  color: #c9b97a;
  font-size: 0.86rem;
  line-height: 1.45;
}
.thread-hall li::before { content: "· "; opacity: 0.6; }

.thread-messages .msg { margin-bottom: 6px; }

.thread-compose {
  display: flex;
  gap: 8px;
  padding: 10px 12px;
  background: var(--bg-surface);
  border-top: 1px solid var(--border);
  flex: 0 0 auto;
}
.thread-reply-input {
  flex: 1;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-deep);
  color: var(--text);
  font-size: 0.9rem;
}
.thread-reply-input:focus { outline: none; border-color: var(--accent); }
.thread-send {
  padding: 8px 14px;
  background: var(--accent);
  color: var(--bg-deep);
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-weight: bold;
}
.thread-send:hover { background: var(--accent-hover); }

/* The icebreaker line in Public now shows a "💬 N replies" button instead
 * of the inline ↩ Reply. Replies themselves don't render in the main
 * chat-box at all — they live in the thread panel. */
.msg.icebreaker .ib-thread-btn {
  background: transparent;
  color: var(--accent);
  border: 1px solid var(--accent-ring);
  border-radius: 4px;
  padding: 1px 8px;
  margin-left: 8px;
  font-size: 0.78rem;
  cursor: pointer;
  vertical-align: middle;
}
.msg.icebreaker .ib-thread-btn:hover {
  background: var(--accent-wash);
  border-color: var(--accent);
}
.msg.icebreaker .ib-thread-btn.has-replies {
  color: #ffd54f;
  border-color: rgba(255, 213, 79, 0.5);
}
.msg.icebreaker .ib-thread-btn.has-replies:hover {
  background: rgba(255, 213, 79, 0.12);
  border-color: #ffd54f;
}

/* Quiet Appreciation — a light arrives: brief warm glow + the phrase. */
#light-glow {
  position: fixed; inset: 0; z-index: 9000; pointer-events: none;
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(circle at 50% 45%, rgba(224,115,58,0.34) 0%, rgba(224,115,58,0.12) 38%, rgba(224,115,58,0) 70%);
  opacity: 0; animation: light-glow-fade 2.6s ease-in-out forwards;
}
#light-glow[hidden] { display: none; }
#light-glow-phrase {
  font-family: Georgia, serif; font-size: 1.6rem; color: #f4efe6;
  text-align: center; max-width: 80%; text-shadow: 0 0 18px rgba(224,115,58,0.6);
}
#light-glow-phrase::after { content: ' 💛'; }
@keyframes light-glow-fade {
  0% { opacity: 0; } 18% { opacity: 1; } 72% { opacity: 1; } 100% { opacity: 0; }
}
/* #light-glow deliberately has no reduced-motion suppression: it's a 2.6s
   opacity fade with no movement (no scale/translate/spin), which isn't the
   class of effect prefers-reduced-motion guards against. showLightGlow() in
   app.js used to branch on the media query and show a static toast instead;
   that check was removed so the moment plays for everyone. The toast path
   remains only as a fallback for a missing element. */

/* --- Empathy Tokens --- */

.gift-btn {
  background: transparent;
  border: 1px solid transparent;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.95rem;
  padding: 1px 6px;
  margin-left: 4px;
  line-height: 1;
  opacity: 0.7;
  transition: opacity 0.15s, background 0.15s, border-color 0.15s;
}
.gift-btn:hover {
  opacity: 1;
  background: rgba(255, 213, 79, 0.18);
  border-color: rgba(255, 213, 79, 0.5);
}

.empathy-picker {
  position: fixed;
  z-index: 1200;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 6px;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
  min-width: 180px;
  max-width: calc(100vw - 16px);
  /* Cap right at the viewport edge so we don't overflow on small screens. */
  max-height: 70vh;
  overflow-y: auto;
}

/* Sit With Me — three-mode picker popover. Same shape as .empathy-picker
 * but with a per-row layout that includes a label + small description. */
.sit-mode-popover {
  position: fixed;
  z-index: 1200;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 6px;
  /* Glass: itself a leaf position:fixed popover (not an ancestor of any
     other fixed element), so backdrop-filter goes directly on it rather
     than a ::before pseudo — see the header::before comment above for why
     that distinction matters. */
  background: var(--glass);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);   /* Safari needs the prefix */
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lift);
  min-width: 240px;
  max-width: calc(100vw - 16px);
}
.sit-mode-popover[hidden] { display: none; }
/* A submenu of the "•••" menu. Default placement is a LEFT flyout aligned to
   the row that opened it (positionFlyout) — the only arrangement that avoids
   covering the menu while staying tied to its row. On viewports too narrow for
   a panel beside the menu it falls back to overlaying the menu, centered; the
   .is-flyout class records which of the two happened.
   overflow-y pairs with the max-height positionFlyout sets for panels taller
   than the viewport, so they scroll instead of being clipped.
   .sit-mode-popover is z-index 1200; the panels that aren't .sit-mode-popover
   (.ambient-picker) get the same treatment via this class, so it sets
   position/z-index rather than assuming the base class did.
   The compound selectors are not redundant: .ambient-picker declares its own
   z-index:1200 FURTHER DOWN this file, and a single-class rule here would tie
   on specificity and lose on source order — which is exactly what happened,
   leaving the ambient picker level with the menu instead of above it. */
.is-submenu,
.sit-mode-popover.is-submenu,
.ambient-picker.is-submenu {
  position: fixed;
  z-index: 1210;
  overflow-y: auto;
  box-shadow: 0 18px 48px -12px rgba(0, 0, 0, 0.66);
}
#partner-info-popover { font-size: 0.8rem; line-height: 1.6; }
#partner-info-popover a { color: var(--accent); text-decoration: none; }
#partner-info-popover a:hover { text-decoration: underline; }
.sit-mode-option {
  display: flex;
  align-items: center;
  gap: 12px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  color: #f3e1b8;
  cursor: pointer;
  text-align: left;
  transition: background 0.12s;
}
.sit-mode-option:hover { background: var(--bg-raised); border-color: var(--border); }
.sit-mode-emoji { font-size: 1.4rem; line-height: 1; flex: 0 0 auto; }
.sit-mode-text { display: flex; flex-direction: column; line-height: 1.25; }
.sit-mode-text strong { font-size: 0.92rem; font-weight: 600; }
.sit-mode-text small { font-size: 0.78rem; opacity: 0.7; }

/* ---- "•••" menu grouping ----
   The menu is grouped by what an action DOES, not by how it's built:
   "Together now" = a live two-way session; "Send & receive" = asynchronous,
   no session (which is why Throw sits with the inboxes and not with the
   pairing rows); "Your identity" = who you are on this device. The three
   look/sound controls are toggles, so they get a strip rather than rows. */
.more-section {
  margin: 0;
  padding: 7px 10px 2px;
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.more-divider { border: 0; border-top: 1px solid var(--border-soft); margin: 5px 6px 1px; }
/* Pushed to the far edge, so it lines up regardless of label length. */
.more-chevron { margin-left: auto; padding-left: 8px; opacity: 0.45; font-size: 1.05rem; flex: 0 0 auto; }
/* .openwindow-dot ships with margin-left:5px for an inline context; in this
   flex row it needs to claim the leftover space instead. */
#header-more-menu .openwindow-dot { margin-left: auto; flex: 0 0 auto; }

.more-toggles { display: flex; gap: 4px; padding: 2px 4px 3px; }
.more-toggle {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 7px 4px;
  color: #f3e1b8;
  cursor: pointer;
  transition: background 0.12s;
}
.more-toggle:hover { background: var(--bg-raised); border-color: var(--border); }
/* refreshAmbientButton() toggles .active when a soundscape is playing. */
.more-toggle.active { background: var(--accent-wash); border-color: var(--accent-ring); }
.more-toggle-icon { font-size: 1.15rem; line-height: 1; }
.more-toggle-label { font-size: 0.68rem; opacity: 0.7; }

/* Emoji Picker — searchable, category-tabbed emoji grid popover. */
.emoji-picker {
  z-index: 200;
  width: 280px;
  max-height: 320px;
  display: flex;
  flex-direction: column;
  background: var(--bg-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px;
  box-shadow: var(--shadow-lift);
}
.emoji-picker-search {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  padding: 6px 8px;
  margin-bottom: 6px;
}
.emoji-picker-tabs {
  display: flex;
  gap: 2px;
  margin-bottom: 6px;
}
.emoji-picker-tab {
  flex: 1;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  padding: 4px;
  font-size: 1rem;
  cursor: pointer;
  opacity: 0.55;
}
.emoji-picker-tab.active { opacity: 1; background: var(--bg-raised); }
.emoji-picker-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 2px;
  overflow-y: auto;
}
/* Pinned quick-reaction row above the search box (EmojiPicker's `quick`
   option). Same item buttons as the grid — this only lays them out in one
   row and separates them from the search field below. */
.emoji-picker-quick {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  padding-bottom: 6px;
  margin-bottom: 6px;
  border-bottom: 1px solid var(--border);
}
.emoji-picker-item {
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  padding: 4px;
  font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
  font-size: 1.3rem;
}
.emoji-picker-item:hover { background: var(--bg-raised); }

.plus-menu {
  z-index: 200;
  width: 200px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: var(--bg-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 6px;
  box-shadow: var(--shadow-lift);
  overflow-y: auto;
}
.plus-menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  height: 40px;
  box-sizing: border-box;
  cursor: pointer;
  color: var(--text);
  font-size: 0.9rem;
  text-align: left;
}
.plus-menu-item:hover { background: var(--bg-raised); }
/* The icons are inline Lucide SVGs now, not emoji glyphs. An inline <svg> sits
   on the text baseline and reserves descender space below it, which pushed the
   icon a couple of px above the label's centre and made the fixed-height row
   look mis-set; the flex box also collapses to the svg's own size instead of a
   line box. `display: flex` + `svg { display: block }` is the same fix the
   .media-capture-* rules use, for the same reason. */
.plus-menu-icon { display: flex; }
.plus-menu-icon svg { display: block; }

/* Talk Link — personal shareable URL. Reuses the sit-mode-popover shell
 * for visual consistency. See xscan/chat/specs/2026-06-18-talk-link-design.md. */
/* Fixed width, not min-width: the URL text arrives over the socket after the
   panel is already positioned, and a content-sized box kept growing past the
   centring/clamp that had just been computed — leaving it flush to the viewport
   edge. Its sibling panels (#openwindow-panel 340, #link-device-panel 320) are
   fixed for the same reason. #my-link-url already has word-break: break-all,
   so a long origin wraps instead of widening the box. */
#my-link-popover { width: 320px; }
#my-link-row {
  display: flex;
  gap: 6px;
  align-items: stretch;
  padding: 4px;
}
#my-link-url {
  flex: 1;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.78rem;
  background: rgba(243, 225, 184, 0.06);
  border: 1px solid rgba(243, 225, 184, 0.18);
  border-radius: var(--radius-sm);
  padding: 6px 8px;
  color: #f0e6d2;
  word-break: break-all;
  display: flex;
  align-items: center;
  min-height: 28px;
}
#my-link-copy {
  flex: 0 0 auto;
  background: rgba(243, 225, 184, 0.1);
  color: #f3e1b8;
  border: 1px solid rgba(243, 225, 184, 0.28);
  border-radius: var(--radius-sm);
  padding: 4px 12px;
  font-size: 0.8rem;
  cursor: pointer;
  transition: background 0.12s;
}
#my-link-copy:hover { background: rgba(243, 225, 184, 0.22); }
#my-link-rotate {
  background: transparent;
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 4px 10px;
  font-size: 0.78rem;
  cursor: pointer;
  margin: 2px 4px;
}
#my-link-rotate:hover { background: rgba(243, 225, 184, 0.08); color: #f0e6d2; }
.my-link-hint {
  font-size: 0.74rem;
  color: var(--text-faint);
  padding: 4px 6px 2px;
  font-style: italic;
  line-height: 1.4;
}

/* Open Window — anonymous personal question inbox owner panel. */
#openwindow-panel { width: 340px; padding: 14px; }
.ow-panel-section { margin-bottom: 14px; }
.ow-panel-label { display: block; font-size: 12px; color: #8b87b0; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
#ow-question-input { width: 100%; background: #120e18; color: #efeaf6; border: 1px solid #33283f; border-radius: var(--radius-sm); padding: 8px; font: inherit; resize: vertical; }
#ow-question-save, #ow-link-copy, #ow-link-share, #ow-link-rotate { background: none; border: 1px solid #3a3050; color: #e0b3a0; border-radius: var(--radius-pill); padding: 5px 14px; cursor: pointer; font: inherit; font-size: 13px; margin-top: 6px; }
#ow-link-row { display: flex; align-items: center; gap: 8px; }
#ow-link-url { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: #cdbfe0; background: #120e18; padding: 6px 8px; border-radius: var(--radius-sm); }
#ow-link-actions { display: flex; gap: 8px; }
.ow-panel-hint { font-size: 11px; color: #7a7089; margin: 6px 0 0; line-height: 1.4; }
#ow-answers { max-height: 240px; overflow-y: auto; }
.ow-question-group-header { font-size: 13px; color: #e0b3a0; line-height: 1.35; margin: 12px 0 4px; padding-bottom: 4px; border-bottom: 1px solid #2c2338; }
.ow-question-group-header:first-child { margin-top: 0; }
.ow-question-group-header.ow-question-legacy { color: #6f6685; font-style: italic; }
.ow-answer { display: flex; align-items: flex-start; gap: 8px; padding: 8px 0; border-bottom: 1px solid #241d30; }
.ow-answer-text { flex: 1; font-size: 14px; color: #efeaf6; line-height: 1.4; }
.ow-answer-del { background: none; border: none; cursor: pointer; opacity: 0.5; }
.ow-answer-del:hover { opacity: 1; }
.openwindow-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #e0b3a0; margin-left: 5px; vertical-align: middle; }
.openwindow-dot[hidden] { display: none; }

/* Link Device — claim this identity's clientId from a second browser via a
 * short-lived pairing code. Shell/section classes reused from Open Window. */
#link-device-panel { width: 320px; padding: 14px; }
#link-device-copy, #link-device-redeem { background: none; border: 1px solid #3a3050; color: #e0b3a0; border-radius: var(--radius-pill); padding: 5px 14px; cursor: pointer; font: inherit; font-size: 13px; }
#link-device-code-row, #link-device-redeem-row { display: flex; align-items: center; gap: 8px; }
#link-device-code { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; letter-spacing: 1px; color: #cdbfe0; background: #120e18; padding: 6px 8px; border-radius: var(--radius-sm); }
#link-device-input { flex: 1; background: #120e18; color: #efeaf6; border: 1px solid #33283f; border-radius: var(--radius-sm); padding: 6px 8px; font: inherit; text-transform: uppercase; }
#link-device-error { color: #e0b3a0; }

/* Waiting overlay — recipient opened a Talk Link while the holder is
 * offline. Floats over everything; close button cancels the wait. */
#talk-waiting-overlay {
  position: fixed;
  inset: 0;
  z-index: 2000;
  background: rgba(5, 8, 16, 0.86);
  display: flex;
  align-items: center;
  justify-content: center;
}
#talk-waiting-overlay[hidden] { display: none; }
.talk-waiting-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding: 28px 36px;
  max-width: 380px;
  text-align: center;
  background: radial-gradient(circle at center, var(--bg-sunken) 0%, var(--bg-deep) 100%);
  border: 1px solid rgba(243, 225, 184, 0.18);
  border-radius: var(--radius);
  color: #f0e6d2;
}
.talk-waiting-text {
  font-size: 1rem;
  letter-spacing: 0.02em;
}
.talk-waiting-sub {
  font-size: 0.85rem;
  color: var(--text-muted);
  font-style: italic;
}
#talk-waiting-close {
  background: transparent;
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 4px 18px;
  font-size: 0.82rem;
  cursor: pointer;
  margin-top: 4px;
}
#talk-waiting-close:hover {
  color: #f0e6d2;
  border-color: var(--accent);
  background: rgba(243, 225, 184, 0.08);
}

/* First-run overlay — offers linking an existing identity before minting
 * one on this browser. Shell/card reuse #talk-waiting-overlay/.talk-waiting-card. */
#first-run-overlay {
  position: fixed;
  inset: 0;
  z-index: 2100;
  background: rgba(5, 8, 16, 0.86);
  display: flex;
  align-items: center;
  justify-content: center;
}
#first-run-overlay[hidden] { display: none; }
/* .first-run-card reuses .talk-waiting-card's gradient/border shell (don't
   touch that shared class — the Talk Link waiting screen uses it too) and
   adds its own elevation + a restrained glow off the app's own ember-mark
   gradient (see .ember-mark above), the one signature touch for a first
   impression. position:relative/visible overflow lets the glow bleed past
   the rounded corners into the dark scrim behind, like candlelight. */
.first-run-card {
  position: relative;
  width: 320px;
  box-shadow: var(--shadow-lift);
}
.first-run-card::before {
  content: "";
  position: absolute;
  top: -34px;
  left: 50%;
  width: 160px;
  height: 120px;
  transform: translateX(-50%);
  background: radial-gradient(circle, rgba(243, 160, 106, 0.28) 0%, rgba(224, 115, 58, 0.10) 45%, transparent 75%);
  filter: blur(4px);
  pointer-events: none;
}
#first-run-title { position: relative; font-weight: 600; letter-spacing: 0.03em; }
/* Tabs — one sunken track instead of two separate outlined plates, so the
   active side reads as "selected" rather than "another button". */
.first-run-tabs {
  display: flex;
  gap: 2px;
  width: 100%;
  padding: 3px;
  background: var(--bg-sunken);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-sm);
}
.first-run-tab {
  flex: 1;
  background: transparent;
  color: var(--text-muted);
  border: none;
  border-radius: calc(var(--radius-sm) - 3px);
  padding: 7px 10px;
  font: inherit;
  font-size: 0.82rem;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.first-run-tab:hover { color: var(--text); }
.first-run-tab.active {
  background: rgba(243, 225, 184, 0.14);
  color: #f3e1b8;
  font-weight: 600;
}
.first-run-section {
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 100%;
}
.first-run-section[hidden] { display: none; }
/* Primary action — subtle bevel (inset top highlight) + a soft ember-toned
   lift on hover, rather than a flatter fill/border darkening. Kept as a
   wash, not a solid gradient fill, so the existing gold text keeps its
   already-audited contrast unchanged. */
.first-run-btn {
  background: linear-gradient(180deg, rgba(243, 225, 184, 0.16), rgba(243, 225, 184, 0.07));
  color: #f3e1b8;
  border: 1px solid rgba(243, 225, 184, 0.3);
  border-radius: var(--radius-sm);
  padding: 9px 14px;
  font: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.05) inset;
  transition: background 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}
.first-run-btn:hover {
  background: linear-gradient(180deg, rgba(243, 225, 184, 0.22), rgba(243, 225, 184, 0.1));
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.05) inset, 0 6px 16px -6px rgba(224, 115, 58, 0.45);
  transform: translateY(-1px);
}
.first-run-btn:active { transform: translateY(0); }
.first-run-input {
  background: #120e18;
  color: #efeaf6;
  border: 1px solid #33283f;
  border-radius: var(--radius-sm);
  padding: 9px 10px;
  font: inherit;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
/* Was falling through to the browser's default focus outline (a hard,
   saturated blue square ring) — no :focus rule existed at all. Matches the
   soft accent-ring glow every other composer input in the app already uses
   (#message-input, .room-composer-input, ...). */
.first-run-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-ring);
}

.empathy-pick {
  display: flex;
  align-items: center;
  gap: 10px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  padding: 6px 10px;
  font-size: 0.9rem;
  color: #f3e1b8;
  cursor: pointer;
  text-align: left;
  transition: background 0.12s;
}
.empathy-pick:hover { background: rgba(255, 213, 79, 0.12); border-color: rgba(255, 213, 79, 0.4); }
.empathy-pick-emoji { font-size: 1.4rem; line-height: 1; }
.empathy-pick-label { flex: 1; }

/* Token bubble in chat. Centered, soft warm background, subtle shimmer
 * on arrival. Distinct from regular .msg.mine / .msg.public alignment. */
.msg.empathy-token {
  display: flex;
  justify-content: flex-start;       /* theirs (received) — left side */
  margin: 12px 0;
  animation: empathy-arrive 700ms ease-out;
}
.msg.empathy-token.mine {
  justify-content: flex-end;          /* mine (sent) — right side */
}
.empathy-token-bubble {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 14px 22px 12px;
  background: linear-gradient(135deg, rgba(255, 213, 79, 0.18), rgba(255, 152, 0, 0.10));
  border: 1px solid rgba(255, 213, 79, 0.35);
  border-radius: 12px;
  box-shadow: 0 0 18px rgba(255, 213, 79, 0.18);
  max-width: 80%;
  text-align: center;
}
.empathy-token-emoji {
  font-size: 2.4rem;
  line-height: 1;
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4));
}
.empathy-token-line {
  font-size: 0.86rem;
  color: #f3e1b8;
  line-height: 1.4;
}
.empathy-token-line b { color: #fff; font-weight: 600; }
.empathy-token-caption {
  font-size: 0.78rem;
  color: #c9b97a;
  font-style: italic;
}
@keyframes empathy-arrive {
  0%   { opacity: 0; transform: scale(0.6); }
  60%  { opacity: 1; transform: scale(1.04); }
  100% { opacity: 1; transform: scale(1); }
}

/* Per-token emoji animations (loop subtly while bubble is on screen). */
.empathy-token-tea    .empathy-token-emoji { animation: tea-steam 3s ease-in-out infinite; }
.empathy-token-breath .empathy-token-emoji { animation: breath-pulse 4s ease-in-out infinite; }
.empathy-token-candle .empathy-token-emoji { animation: candle-flicker 1.6s ease-in-out infinite; }
.empathy-token-hug    .empathy-token-emoji { animation: hug-warmth 3.5s ease-in-out infinite; }
.empathy-token-sprig  .empathy-token-emoji { animation: sprig-sway 4s ease-in-out infinite; }
@keyframes tea-steam     { 0%, 100% { transform: translateY(0); }
                            50%      { transform: translateY(-3px); } }
@keyframes breath-pulse  { 0%, 100% { transform: scale(1);   filter: drop-shadow(0 2px 4px rgba(0,0,0,0.4)); }
                            50%      { transform: scale(1.08); filter: drop-shadow(0 4px 8px rgba(255, 213, 79, 0.6)); } }
@keyframes candle-flicker { 0%, 100% { transform: scale(1) rotate(0deg); }
                            33%      { transform: scale(1.04) rotate(-1deg); }
                            66%      { transform: scale(0.98) rotate(1deg); } }
@keyframes hug-warmth     { 0%, 100% { filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4)); }
                            50%      { filter: drop-shadow(0 0 12px rgba(255, 152, 0, 0.6)); } }
@keyframes sprig-sway     { 0%, 100% { transform: rotate(-4deg); }
                            50%      { transform: rotate(4deg); } }

/* --- Ambient Soundscapes --- */

#ambient-btn.active {
  background: var(--accent-wash);
  border-color: rgba(79, 195, 247, 0.6);
  color: var(--accent);
}

.ambient-picker {
  position: fixed;
  z-index: 1200;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 8px;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lift);
  min-width: 200px;
  max-width: calc(100vw - 16px);
}
.ambient-pick {
  display: flex;
  align-items: center;
  gap: 10px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 6px 10px;
  font-size: 0.9rem;
  color: var(--text);
  cursor: pointer;
  text-align: left;
  transition: background 0.12s;
}
.ambient-pick:hover { background: rgba(79, 195, 247, 0.12); border-color: rgba(79, 195, 247, 0.4); }
.ambient-pick.active {
  background: rgba(79, 195, 247, 0.25);
  border-color: var(--accent);
  color: var(--accent);
}
.ambient-emoji { font-size: 1.4rem; line-height: 1; }
.ambient-label { flex: 1; }

/* --- Skin picker --- shape borrows from .ambient-pick (row-per-option,
   built dynamically, .active marks the current choice), swapping the
   emoji for a color swatch. */
.skin-pick {
  display: flex;
  align-items: center;
  gap: 10px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 6px 10px;
  font-size: 0.9rem;
  color: var(--text);
  cursor: pointer;
  text-align: left;
  transition: background 0.12s;
}
.skin-pick:hover { background: var(--bg-raised); border-color: var(--border); }
.skin-pick.active {
  background: var(--accent-wash);
  border-color: var(--accent);
  color: var(--accent);
}
.skin-swatch {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  flex: 0 0 auto;
  border: 1px solid var(--border);
}
.skin-label { flex: 1; }

/* Accent row — five round swatches under the dark/light rows above, only
   rendered while the dark base is active (accent recolors the dark base
   only; minimal keeps its own tuned accent, see styles.css token section).
   Plain flex row, not a picker-with-active-state list, since there's no
   label to read — the color ring around the current pick is the label. */
.accent-row { display: flex; gap: 8px; padding: 4px 10px 2px; }
.accent-swatch {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 1px solid var(--border);
  cursor: pointer;
  padding: 0;
}
.accent-swatch.active {
  box-shadow: 0 0 0 2px var(--bg-surface), 0 0 0 4px var(--accent);
}
.ambient-volume {
  margin-top: 4px;
  padding: 6px 10px;
  border-top: 1px solid var(--border);
  font-size: 0.78rem;
  color: var(--text-muted);
}
.ambient-volume label { display: flex; align-items: center; gap: 8px; }
.ambient-volume input[type="range"] {
  flex: 1;
  accent-color: var(--accent);
}

/* =============================================================================
   CAMPFIRE THREADS — 🔥 button
   ============================================================================= */
.campfire-btn {
  background: none;
  border: 0;
  padding: 2px 6px;
  cursor: pointer;
  font-size: 0.95em;
  transition: background 120ms ease;
  border-radius: var(--radius-sm);
  display: inline-flex;
  align-items: center;
  gap: 4px;
  vertical-align: middle;
  line-height: 1;
  margin-left: 8px;   /* a little breathing room from the "+" react button nearby */
}
.campfire-btn .campfire-icon {
  width: 22px;
  height: 22px;
  display: block;
  filter: saturate(0.35) brightness(0.95) opacity(0.65);
  position: relative;
  z-index: 1;
  /* pointer-events:none so the visually-enlarged icon doesn't steal hover
     from the button itself (which is the click target and stays at 22px). */
  pointer-events: none;
  transform: scale(1);
  transform-origin: center;
  transition: transform 240ms cubic-bezier(0.2, 0.7, 0.3, 1),
              filter 180ms ease;
}
.campfire-btn .campfire-count {
  font-size: 0.85em;
  color: #d97a3c;
}
.campfire-btn:hover .campfire-icon {
  transform: scale(3);
  filter: saturate(0.8) opacity(0.95);
  z-index: 10;
  /* Only zoom on a deliberate hover — a quick mouse pass-through (e.g. aiming
     for the nearby "+" react button) ends before this delay elapses, so the
     transition never starts. Hovering back out (base rule, no delay) is
     immediate. */
  transition-delay: 180ms;
}
.campfire-btn.campfire-lit {
  background: rgba(255, 140, 50, 0.12);
}
.campfire-btn.campfire-lit .campfire-icon { filter: none; }
.campfire-btn.campfire-lit:hover .campfire-icon { filter: none; }
.campfire-btn.campfire-lit:hover {
  background: rgba(255, 140, 50, 0.22);
}

/* =============================================================================
   CAMPFIRE THREADS — tab + list + per-fire view
   ============================================================================= */

/* Tab count badge */
.campfires-tab-count {
  display: inline-block;
  font-size: 0.75em;
  margin-left: 3px;
  background: rgba(255, 140, 50, 0.18);
  color: #c66;
  padding: 0 5px;
  border-radius: var(--radius-sm);
  vertical-align: middle;
}
.campfires-tab-count[hidden] { display: none; }

/* Tab panel layout — campfires panel fills the panel area */
.tab-panel[data-tab-id="campfires"] {
  display: none;
  flex-direction: column;
  overflow: hidden;
  height: 100%;
}
.tab-panel[data-tab-id="campfires"].active {
  display: flex;
}

/* List view */
#campfires-list-view {
  flex: 1;
  overflow-y: auto;
  padding: 8px;
}
.campfires-empty {
  color: var(--text-faint);
  padding: 16px;
  text-align: center;
  font-style: italic;
}
/* The go-out rule, stated on the list and inside a fire. Deliberately quiet —
   it's reassurance for when a thread disappears, not a warning. */
.campfires-ttl-note, .campfire-ttl-note {
  color: var(--text-faint);
  font-size: 0.74rem;
  font-style: italic;
}
.campfires-ttl-note { padding: 10px 16px 0; text-align: center; }
.campfire-ttl-note { margin-top: 6px; }
.campfires-ttl-note[hidden] { display: none; }
.campfire-card {
  display: block;
  width: 100%;
  text-align: left;
  background: rgba(255, 140, 50, 0.06);
  border: 1px solid rgba(255, 140, 50, 0.18);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  margin: 0 0 8px 0;
  cursor: pointer;
  color: inherit;
  transition: background 120ms ease;
}
.campfire-card:hover { background: rgba(255, 140, 50, 0.14); }
.campfire-card-quote { font-size: 0.95em; margin-bottom: 4px; line-height: 1.35; }
.campfire-card-meta  { font-size: 0.75em; color: var(--text-faint); }
/* Compact card preview of the source message's media —
   MediaAttachment.renderPreview, used by the campfires landing list. Capped
   short so a card stays a card: a 20 MB video's frame at full width would push
   the meta line off the first screen. Non-interactive by construction (no
   lightbox button, no controls), so it needs no cursor/hover treatment of its
   own — the .campfire-card button around it owns the click. */
.msg-attachment-preview { margin: 0 0 6px; }
.msg-attachment-preview img,
.msg-attachment-preview video {
  display: block;
  max-width: 100%;
  max-height: 120px;
  width: auto;
  border-radius: var(--radius-sm);
}

/* Per-fire view */
#campfire-view {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: 8px;
  box-sizing: border-box;
}
#campfire-view[hidden] { display: none; }
.link-btn {
  background: none;
  border: 0;
  color: var(--text-faint);
  cursor: pointer;
  padding: 4px 0;
  align-self: flex-start;
  font-size: 0.9em;
}
.link-btn:hover { color: var(--text-muted); }
.campfire-source {
  background: rgba(255, 140, 50, 0.08);
  border-left: 3px solid rgba(255, 140, 50, 0.4);
  padding: 8px 12px;
  margin: 4px 0 8px 0;
  border-radius: 0 6px 6px 0;
  font-size: 0.92em;
}
.campfire-source-quote { font-style: italic; color: var(--text-muted); }
.campfire-source-from  { font-size: 0.78em; color: var(--text-faint); margin-top: 4px; }
#campfire-messages {
  flex: 1;
  overflow-y: auto;
  padding: 4px 2px;
}
.campfire-msg { margin-bottom: 8px; }
/* Same treatment as .nearby-reply-alias, so the author reads as a label rather
   than as the first words of the message. */
.campfire-msg-from  { font-size: 0.78rem; color: var(--accent); font-weight: 600; margin-bottom: 1px; }
.campfire-msg-body  { font-size: 0.95em; line-height: 1.4; }
/* The reactions row and the ↩ trigger share one footer. The button must NOT
 * live inside .campfire-msg-reactions — ReactionBar.render() clears and
 * rebuilds that container. */
.campfire-msg-footer { display: flex; align-items: center; gap: 8px; margin-top: 3px; }
.campfire-reply-btn {
  background: none; border: none; color: var(--text-faint);
  font-size: 0.8rem; cursor: pointer; padding: 2px 4px;
}
.campfire-reply-btn:hover { color: var(--accent); }
/* Resting state stays quiet: the ↩ trigger and the ☺️ add-reaction button
 * hide until hover, same split .msg-pin uses elsewhere — a reaction someone
 * actually left is content and stays visible, the add trigger is just
 * chrome. No real hover on touch, so a tap toggles .actions-open on the row
 * as the fallback (delegated handler in app.js). */
.campfire-reply-btn,
.campfire-msg-reactions .reaction-add {
  display: none;
}
@media (hover: hover) and (pointer: fine) {
  .campfire-msg:hover .campfire-reply-btn,
  .campfire-msg:hover .campfire-msg-reactions .reaction-add { display: inline-block; }
}
.campfire-msg.actions-open .campfire-reply-btn,
.campfire-msg.actions-open .campfire-msg-reactions .reaction-add { display: inline-block; }

/* Reply-list toolbar (sort + infinite/paginated toggle) and numbered-page
 * controls — shared by Campfire threads and Nearby post threads. See the
 * REPLY LIST DISPLAY block in app.js. */
.reply-list-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 4px 0; font-size: 0.8rem; }
.reply-list-sort { background: var(--bg-sunken); color: var(--text-muted); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 2px 6px; font: inherit; font-size: 0.78rem; }
.reply-list-mode { display: flex; gap: 4px; }
.reply-list-mode-btn { background: none; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 2px 8px; font-size: 0.78rem; cursor: pointer; color: var(--text-muted); }
.reply-list-mode-btn.active { background: rgba(255, 140, 50, 0.18); color: var(--text); border-color: rgba(255, 140, 50, 0.4); }
.reply-list-mode-btn:hover { color: var(--text); }
.campfire-page-controls { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; margin-top: 6px; }
.campfire-page-btn {
  background: none; border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 2px 7px; font-size: 0.78rem; cursor: pointer; color: var(--text-muted);
}
.campfire-page-btn.active { background: rgba(255, 140, 50, 0.18); color: var(--text); border-color: rgba(255, 140, 50, 0.4); }
.campfire-page-btn:hover { color: var(--text); }
.campfire-page-jump { display: flex; align-items: center; gap: 4px; margin-left: 6px; }
.campfire-page-jump input { width: 44px; padding: 2px 4px; font-size: 0.78rem; }
.campfire-page-jump button { font-size: 0.78rem; padding: 2px 8px; cursor: pointer; }
.reaction-pill {
  background: var(--bg-sunken);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 2px 10px;
  font-size: 0.82rem;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-right: 4px;
}
.reaction-pill:hover:not(:disabled) { background: var(--bg-raised); }
.reaction-pill[data-mine="true"] { background: var(--bg-raised); border-color: var(--accent); color: #fff; }
.reaction-pill:disabled { opacity: 0.35; cursor: not-allowed; }
/* Every ReactionBar mount, tagged by render() itself. The three mount points
   (.nb-reactions, .campfire-msg-reactions, .confession-reaction-pills) had no
   rule between them and .reaction-pill carries no margin, so adjacent pills
   used to render flush against each other. wrap matters on Nearby cards, where
   .nearby-actions is a single non-wrapping flex row — enough distinct emoji
   would otherwise squeeze the 💬 ✉️ ⚑ 📷 controls beside it. */
.reaction-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }

/* ☺ trigger rendered by ReactionBar's free mode. Quieter than a pill — it's
   an affordance, not a datum. */
.reaction-add {
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius);
  /* 4px, not the 8px a pill gets, because ☺ carries roughly 3-4px of its own
     side bearing at this size — its ink sits well inside its advance width.
     Padding it like a pill made the ☺→💬 gap on a Nearby card visibly wider
     than the 💬→✉️ gap next to it. Optical spacing, not nominal. */
  padding: 2px 4px;
  /* Well above the 0.82rem pill text, and above the 💬 reply counter on a
     Nearby card, on purpose: ☺ (U+263A) draws its face at roughly two-thirds
     of its em box, whereas 💬 is a colour emoji filling nearly all of its own.
     Matching font-size across those two would leave this control looking
     visibly smaller than everything around it. Optical size, not nominal. */
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
  /* An explicit colour, not an opacity dial: the glyph is line-art
     (U+263A + VS15), and unlike a colour emoji it actually honours `color`.
     Matched to the app's other quiet icon buttons (.nb-reply-quote,
     .keepsake-btn). */
  color: var(--text-faint);
}
.reaction-add:hover { color: var(--accent); border-color: var(--border); }
/* Quiet Wall Caption — italic, dim, sits below the confession text and
   above the reaction row. Renders only on lonely posts with zero
   reactions; auto-removed on first reaction (see applyReactionCounts). */
.confession-wall-voice {
  margin-top: 6px;
  font-style: italic;
  font-size: 0.85em;
  color: var(--text-faint);
  line-height: 1.4;
}

/* =============================================================================
   PULSE POLLS — see specs/2026-06-23-pulse-polls-design.md.
   Palette/scale follow the dark chat theme: #16213e panels, #0f3460 borders,
   #4fc3f7 cyan accent for the voter's choice. Poll cards live in the chat
   feed as a distinct bordered card so they don't read as a chat bubble.
   ============================================================================= */

#emoji-btn {
  height: 36px;
  box-sizing: border-box;
  background: none;
  color: var(--text-muted);
  border: none;
  border-radius: var(--radius-sm);
  padding: 0 10px;
  cursor: pointer;
  font-size: 1.05rem;
  line-height: 1;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color var(--ease), background var(--ease);
}
#emoji-btn:hover { background: var(--accent-wash); color: var(--text); }

#poll-btn {
  height: 36px;
  box-sizing: border-box;
  background: none;
  color: var(--text-muted);
  border: none;
  border-radius: var(--radius-sm);
  padding: 0 10px;
  cursor: pointer;
  font-size: 1.05rem;
  line-height: 1;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color var(--ease), background var(--ease);
}
#poll-btn:hover { background: var(--accent-wash); color: var(--text); }

#poll-composer {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 10px 12px;
  background: var(--bg-surface);
  border-top: 1px solid var(--border);
  flex: 0 0 auto;
}
#poll-composer[hidden] { display: none; }
#poll-composer input[type="text"] {
  padding: 7px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-deep);
  color: var(--text);
  font-size: 0.9rem;
  font-family: inherit;
}
#poll-composer input[type="text"]::placeholder { color: var(--text-faint); }
#poll-composer input[type="text"]:focus { outline: none; border-color: var(--accent); }
#poll-question { font-weight: 600; }
#poll-options { display: flex; flex-direction: column; gap: 6px; }

.poll-composer-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}
.poll-composer-actions button {
  background: var(--bg-deep);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 5px 12px;
  font-size: 0.82rem;
  cursor: pointer;
  font-family: inherit;
}
.poll-composer-actions button:hover:not(:disabled) {
  background: var(--bg-raised);
  color: var(--accent);
}
.poll-composer-actions button:disabled { opacity: 0.4; cursor: not-allowed; }
#poll-post {
  margin-left: auto;
  background: var(--bg-raised);
  color: var(--accent);
  border-color: var(--accent);
  font-weight: 600;
}
#poll-post:hover:not(:disabled) { background: var(--accent); color: var(--bg-sunken); }
.poll-error {
  color: #ff8a80;
  font-size: 0.78rem;
  flex: 1;
}
.poll-error[hidden] { display: none; }

.poll-card {
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  margin: 6px 12px 6px 0;
  max-width: 480px;
  color: var(--text);
}
/* Mirror chat bubbles: own polls slide to the right, with the cyan accent
   flipping to the right edge so it reads as "this side is mine." */
.poll-card.mine {
  margin-left: auto;
  border-left: 1px solid var(--border);
  border-right: 3px solid var(--accent);
  background: #17324d;
}
.poll-header {
  font-size: 0.78rem;
  color: var(--text-faint);
  margin-bottom: 6px;
}
.poll-author { color: var(--accent); font-weight: 600; }
.poll-question {
  font-size: 0.95rem;
  line-height: 1.4;
  margin-bottom: 10px;
  color: var(--text);
}
.poll-options { display: flex; flex-direction: column; gap: 5px; }

.poll-option-btn {
  text-align: left;
  padding: 8px 12px;
  background: var(--bg-deep);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 0.88rem;
  font-family: inherit;
  transition: background 0.12s, border-color 0.12s;
}
.poll-option-btn:hover { background: var(--bg-raised); border-color: var(--accent); }

.poll-bar {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 12px;
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  overflow: hidden;
  font-size: 0.88rem;
  transition: border-color 0.12s;
}
.poll-bar:hover { border-color: var(--accent); }
.poll-bar-mine { border-color: var(--accent); }
.poll-bar-fill {
  position: absolute;
  top: 0; left: 0; bottom: 0;
  background: var(--accent-wash);
  z-index: 0;
  transition: width 0.3s ease;
}
.poll-bar-mine .poll-bar-fill { background: var(--accent-ring); }
.poll-bar-label,
.poll-bar-count {
  position: relative;
  z-index: 1;
}
.poll-bar-label { color: var(--text); }
.poll-bar-count {
  margin-left: auto;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
  font-size: 0.82rem;
}
.poll-bar-mine .poll-bar-count { color: var(--accent); }

/* =============================================================================
   MINI-GAMES — see specs/2026-06-24-minigames-design.md.
   Round cards live inline in the public chat feed. Warm amber accent
   (#f3e1b8) differentiates them from polls' cyan — same hue the Stage
   already uses for "ritual moments."
   ============================================================================= */

.minigame-card {
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-left: 3px solid #f3e1b8;
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  margin: 6px 12px 6px 0;
  max-width: 480px;
  color: var(--text);
}
.minigame-card--revealed { border-left-color: #d4b85e; }

.minigame-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 0.78rem;
  color: #f3e1b8;
  margin-bottom: 6px;
  letter-spacing: 0.02em;
}
.minigame-keeper { font-weight: 600; }
.minigame-countdown {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.78rem;
  color: #c9b97a;
  font-variant-numeric: tabular-nums;
}
.minigame-countdown.minigame-boast {
  font-family: inherit;
  font-weight: 600;
  color: #f3e1b8;
}

.minigame-prompt {
  font-size: 0.95rem;
  line-height: 1.4;
  margin-bottom: 10px;
  color: var(--text);
}

.minigame-options { display: flex; flex-direction: column; gap: 5px; }
.minigame-option-btn {
  text-align: left;
  padding: 8px 12px;
  background: var(--bg-deep);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 0.88rem;
  font-family: inherit;
  transition: background 0.12s, border-color 0.12s;
}
.minigame-option-btn:hover {
  background: var(--bg-raised);
  border-color: #f3e1b8;
}

.minigame-told { display: flex; flex-direction: column; gap: 6px; }
.minigame-told-input {
  width: 100%;
  padding: 7px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-deep);
  color: var(--text);
  font-size: 0.9rem;
  font-family: inherit;
  resize: none;
}
.minigame-told-input::placeholder { color: var(--text-faint); }
.minigame-told-input:focus { outline: none; border-color: #f3e1b8; }
.minigame-told-submit {
  align-self: flex-end;
  background: rgba(243, 225, 184, 0.16);
  color: #f3e1b8;
  border: 1px solid rgba(243, 225, 184, 0.5);
  border-radius: var(--radius-sm);
  padding: 5px 14px;
  font-size: 0.85rem;
  cursor: pointer;
  font-family: inherit;
}
.minigame-told-submit:hover { background: rgba(243, 225, 184, 0.28); }

.minigame-state-submitted {
  font-size: 0.85rem;
  color: #c9b97a;
  font-style: italic;
  padding: 6px 0;
}

.minigame-reveal-bars { display: flex; flex-direction: column; gap: 4px; }
.minigame-bar {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 12px;
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  font-size: 0.88rem;
}
.minigame-bar-mine { border-color: #f3e1b8; }
.minigame-bar-fill {
  position: absolute;
  top: 0; left: 0; bottom: 0;
  background: rgba(243, 225, 184, 0.14);
  z-index: 0;
  transition: width 0.3s ease;
}
.minigame-bar-mine .minigame-bar-fill { background: rgba(243, 225, 184, 0.28); }
.minigame-bar-label,
.minigame-bar-count {
  position: relative;
  z-index: 1;
}
.minigame-bar-count {
  margin-left: auto;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
  font-size: 0.82rem;
}
.minigame-bar-mine .minigame-bar-count { color: #f3e1b8; }

.minigame-reveal-answers { display: flex; flex-direction: column; gap: 6px; }
.minigame-answers { display: flex; flex-direction: column; gap: 4px; }
.minigame-answer {
  padding: 6px 10px;
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: 0.88rem;
  color: var(--text);
  font-style: italic;
}
.minigame-answer-mine { border-color: #f3e1b8; color: #f3e1b8; }
.minigame-answers-empty {
  font-size: 0.85rem;
  color: var(--text-faint);
  font-style: italic;
}

/* =============================================================================
   HYPERLOCAL FEED ("Nearby") — see specs/2026-06-28-hyperlocal-feed-design.md.
   Feed tab: composer pinned atop, cards below. Bucket chips carry the only
   new visual — a cyan→dim gradient by proximity. Reuses the dark theme.
   ============================================================================= */

#main.nearby-active #chat-input { display: none; }

.tab-panel[data-tab-id="nearby"] { flex-direction: column; }

#nearby-located { display: flex; flex-direction: column; min-height: 0; flex: 1; }
#nearby-located[hidden] { display: none; }

#nearby-compose-row {
  display: flex; gap: 12px; align-items: center;
  margin-top: 6px; font-size: 0.78rem;
}
#nearby-post {
  margin-left: auto;
  padding: 5px 14px;
  background: var(--bg-raised); color: #f0e6d2;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  cursor: pointer; font-size: 0.85rem;
}
#nearby-post:hover:not(:disabled) { background: var(--accent-wash); }
#nearby-post:disabled { opacity: 0.4; cursor: not-allowed; }

#nearby-feed {
  flex: 1; overflow-y: auto;
  padding: 12px 20px 24px;
  display: flex; flex-direction: column; gap: 10px;
}
.nearby-empty, .nearby-quiet-header {
  color: var(--text-muted); font-size: 0.85rem; font-style: italic;
  padding: 4px 2px;
}
.nearby-quiet-header { color: #c9b97a; }

.nearby-card {
  background: var(--bg-surface);
  border-left: 2px solid var(--accent-ring);
  border-radius: var(--radius-sm);
  padding: 10px 14px;
}
/* A row, so the ⋯ can sit at the far right of it. Absolute positioning would
   also work but risks overlapping the mention flag on a narrow card. */
.nearby-meta { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
/* Groups the revealed strip with ⋯ so they move as one right-aligned unit,
 * leaving the bucket chip (and mention flag) pinned left. */
.nearby-meta-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.nearby-meta-right .nb-more { margin-left: 0; }   /* already inside the right-aligned group */
.nb-more {
  margin-left: auto;
  background: none; border: 1px solid transparent; border-radius: var(--radius-sm);
  color: var(--text-faint); cursor: pointer;
  /* ⋯ (U+22EF) is a small glyph in its em box, like ☺ — hence the size bump
     and the tight vertical padding rather than a matching 0.85rem. */
  font-size: 1.1rem; line-height: 1; padding: 0 6px;
}
.nb-more:hover { color: #cfe3ff; background: var(--accent-wash); }
.nearby-card.actions-open .nb-more { color: var(--accent); background: var(--accent-wash); }
.nearby-bucket-chip {
  display: inline-block;
  font-size: 0.72rem; letter-spacing: 0.02em;
  padding: 1px 9px; border-radius: var(--radius);
  border: 1px solid transparent;
}
.nearby-bucket-chip.b-close  { background: color-mix(in srgb, var(--accent) 22%, transparent); color: color-mix(in srgb, var(--accent) 55%, white); border-color: color-mix(in srgb, var(--accent) 55%, transparent); }
.nearby-bucket-chip.b-near   { background: color-mix(in srgb, var(--accent) 14%, transparent); color: color-mix(in srgb, var(--accent) 35%, white); border-color: color-mix(in srgb, var(--accent) 40%, transparent); }
.nearby-bucket-chip.b-region { background: rgba(120,140,170,0.14); color: #9fb0c8; border-color: rgba(120,140,170,0.4); }
.nearby-bucket-chip.b-far    { background: rgba(120,140,170,0.08); color: #7a8aab; border-color: rgba(120,140,170,0.25); }
.nearby-bucket-chip.has-pin  { cursor: pointer; text-decoration: underline dotted; text-decoration-color: currentColor; }
.nearby-bucket-chip.has-pin:hover { filter: brightness(1.25); }

.nearby-category-chip { font-size: 0.85rem; }

.nearby-text {
  color: var(--text); font-size: 0.95rem; line-height: 1.5;
  white-space: pre-wrap; word-wrap: break-word;
}
.nearby-actions { display: flex; gap: 8px; margin-top: 8px; align-items: center; }

/* Status vs. actions: things that carry data (reaction pills, the 💬 reply
 * count) stay on their own line under the text; things that DO something
 * (☺ react, ↩ reply, ✉️ reach out, ⚑ flag, 📷 keepsake) hide behind the ⋯ up
 * in .nearby-meta, revealed together in .nearby-card-actions — same icon set
 * and order as .nearby-thread-post-actions/.nearby-reply-actions, just a
 * different trigger (see below).
 *
 * Revealed by ⋯ ONLY — deliberately no :hover rule. A card's tap belongs to
 * its thread, so it can't toggle .actions-open on tap the way .campfire-msg
 * does; a dedicated button sidesteps that instead of fighting it. Keeping
 * mouse and touch on one path means the two can't drift apart. */
.nearby-card .nb-replies { order: -1; }
.nearby-card-actions { display: none; align-items: center; gap: 8px; }
.nearby-card.actions-open .nearby-card-actions { display: flex; }
.nb-quote,
.nb-edit,
.nb-post-edit,
.nb-reply-edit {
  background: none; border: none; color: var(--text-faint);
  font-size: 0.8rem; cursor: pointer; padding: 2px 4px;
}
.nb-quote:hover,
.nb-edit:hover:not(:disabled),
.nb-post-edit:hover:not(:disabled),
.nb-reply-edit:hover:not(:disabled) { color: var(--accent); }
.nb-edit:disabled,
.nb-post-edit:disabled,
.nb-reply-edit:disabled { opacity: 0.3; cursor: not-allowed; }
.nb-edited-marker { color: var(--text-faint); font-size: 0.78rem; font-style: italic; }
.nb-react {
  background: none; border: 0; color: var(--text-faint);
  font-size: 1.05rem; line-height: 1; cursor: pointer; padding: 0 2px;
}
.nb-react:hover { color: var(--accent); }
.nb-flag,
.nb-post-flag {
  /* No margin-left:auto — ⚑ 📷 used to be right-aligned, which left a gap in
     the middle of the revealed group (💬 [pills] ☺ ✉️ ——gap—— ⚑ 📷). The four
     actions now read as one cluster. */
  background: none; color: var(--text-faint);
  border: 1px solid transparent; border-radius: var(--radius-sm);
  padding: 2px 8px; font-size: 0.85rem; line-height: 1; cursor: pointer;
}
.nb-flag:hover:not(:disabled),
.nb-post-flag:hover:not(:disabled) { color: #ff9c73; background: rgba(255,120,60,0.08); }
.nb-flag:disabled,
.nb-post-flag:disabled { opacity: 0.3; cursor: not-allowed; }

#nearby-located-off {
  flex: 1; display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 10px; padding: 32px 24px; text-align: center;
}
#nearby-located-off[hidden] { display: none; }
.nearby-off-headline { font-size: 1.05rem; color: var(--accent); letter-spacing: 0.02em; }
.nearby-off-sub { font-size: 0.85rem; color: var(--text-muted); max-width: 360px; line-height: 1.5; }
.popup-hint { font-size: 0.8rem; color: var(--text-muted); }

/* Nearby — BBS thread view (see 2026-06-28 spec, 2026-07-07 amendment). */
.nb-replies {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 0.82rem; color: #7a8aab; cursor: pointer;
  padding: 2px 6px;
}
.nb-replies .count { font-variant-numeric: tabular-nums; }
.nearby-card { cursor: pointer; }   /* tap anywhere (but actions) to open thread */
.nearby-card .nearby-actions { cursor: default; }

#nearby-thread-view {
  display: flex; flex-direction: column; min-height: 0; flex: 1;
}
#nearby-thread-view[hidden] { display: none; }
#nearby-thread-back {
  flex: 0 0 auto; align-self: flex-start;
  margin: 10px 16px 4px;
}
.nearby-thread-post {
  position: relative;
  flex: 0 0 auto;
  margin: 4px 16px 8px;
  padding: 12px 14px;
  background: var(--bg-surface);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
}
/* ☺︎ ↩ ✉️ ⚑ 📷 — same absolutely-positioned reveal-on-hover strip
 * .nearby-reply-actions uses; the root is just another post in the thread,
 * so it gets the same trigger a reply does rather than the feed card's ⋯
 * (there's no tap-opens-the-thread conflict here to sidestep). */
.nearby-thread-post-actions {
  position: absolute; top: 4px; right: 6px;
  display: none;
  align-items: center;
  gap: 2px;
}
@media (hover: hover) and (pointer: fine) {
  .nearby-thread-post:hover .nearby-thread-post-actions { display: flex; }
}
.nearby-thread-post.actions-open .nearby-thread-post-actions { display: flex; }
.nb-post-quote {
  background: none; border: none; color: var(--text-faint);
  font-size: 0.8rem; cursor: pointer; padding: 2px 4px;
}
.nb-post-quote:hover { color: var(--accent); }
.nb-post-react {
  background: none; border: 0; color: var(--text-faint);
  font-size: 1.05rem; line-height: 1; cursor: pointer; padding: 0 2px;
}
.nb-post-react:hover { color: var(--accent); }
.nb-post-reactions:empty { display: none; }
.nb-post-reactions { margin-top: 8px; }
#nearby-thread-replies {
  flex: 1; overflow-y: auto;
  padding: 4px 16px 12px;
  display: flex; flex-direction: column; gap: 8px;
}
.nearby-reply {
  position: relative;
  background: var(--bg-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 8px 12px;
}
.nearby-reply-head { margin-bottom: 3px; }
.nearby-reply-alias { font-size: 0.78rem; color: var(--accent); font-weight: 600; }
.nearby-reply-text {
  color: var(--text); font-size: 0.9rem; line-height: 1.45;
  white-space: pre-wrap; word-wrap: break-word;
}
.nb-reply-flag {
  background: none; border: none; color: var(--text-faint);
  font-size: 0.8rem; cursor: pointer; padding: 2px 4px;
}
.nb-reply-flag:hover:not(:disabled) { color: #ff9c73; }
.nb-reply-flag:disabled { opacity: 0.3; cursor: not-allowed; }

/* Quote-reply + @mention */
.nb-reply-quote {
  background: none; border: none; color: var(--text-faint);
  font-size: 0.8rem; cursor: pointer; padding: 2px 4px;
}
.nb-reply-quote:hover { color: var(--accent); }
/* One absolutely-positioned flex strip holds all four actions (↩ ✉️ ⚑ 📷),
 * exactly like .msg-pin on room messages. Previously each button carried its
 * own `right:` offset, which meant the spacing had to be hand-guessed against
 * variable-width emoji glyphs — ✉️ and 📷 overlapped, and any font change
 * would break it again. A flex row + gap lays them out from their real widths,
 * so it can't collide.
 *
 * The strip hides at rest and reveals on hover (these rows carry no reaction
 * pills, so all four move together — same rationale as the campfire block
 * above). No real hover on touch, so a tap toggles .actions-open. */
/* A reply's reaction pills: status, so always visible, on their own line under
   the text. The ☺ that adds one is an ACTION and lives in
   .nearby-reply-actions with ↩ ✉️ ⚑ 📷, revealed together — which is why
   ReactionBar is called with showAdd:false here. Empty until someone reacts,
   and .reaction-row's own rule handles the pill spacing. */
.nb-reply-reactions:empty { display: none; }
.nb-reply-reactions { margin-top: 5px; }
.nb-reply-react {
  background: none; border: 0; color: var(--text-faint);
  /* ☺ is small in its em box — same optical bump the other triggers get. */
  font-size: 1.05rem; line-height: 1; cursor: pointer; padding: 0 2px;
}
.nb-reply-react:hover { color: var(--accent); }
.nearby-reply-actions {
  position: absolute; top: 4px; right: 6px;
  display: none;
  align-items: center;
  gap: 2px;
}
@media (hover: hover) and (pointer: fine) {
  .nearby-reply:hover .nearby-reply-actions { display: flex; }
}
.nearby-reply.actions-open .nearby-reply-actions { display: flex; }
.nb-mention {
  color: var(--accent); background: var(--accent-wash);
  border-radius: var(--radius-sm); padding: 0 3px; font-weight: 600;
}
.nearby-tab-dot {
  display: inline-block; width: 7px; height: 7px;
  margin-left: 5px; border-radius: 50%;
  background: var(--accent); vertical-align: middle;
}
.nearby-tab-dot[hidden] { display: none; }

/* Feed card marker for a thread you were mentioned in (findable after the
   transient toast fades). */
.nearby-card.has-mention { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent-ring); }
.nb-mention-flag {
  margin-left: 8px; font-size: 0.72rem; font-weight: 600;
  color: var(--accent); background: var(--accent-wash);
  border-radius: var(--radius-sm); padding: 1px 6px;
}

/* Unified mention toast — same shape/position as the old #nearby-toast (the
   one it replaces alongside the old reply-ping toast), just its own id since
   it's a different surface-agnostic feature. */
#mention-toast {
  position: fixed; bottom: 195px; left: 50%;
  transform: translate(-50%, 60px);
  background: var(--bg-surface); color: #f3e1b8;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 8px 16px; font-size: 0.85rem; letter-spacing: 0.03em;
  opacity: 0; pointer-events: none; transition: transform 200ms ease, opacity 200ms ease;
  z-index: 1000; cursor: pointer;
}
#mention-toast.visible { transform: translate(-50%, 0); opacity: 1; pointer-events: auto; }

/* Notification Center — one row per mention, event-delegated click in app.js. */
.notif-center-empty {
  color: var(--text-muted); font-size: 0.85rem; font-style: italic;
  padding: 4px 2px;
}
.notif-item {
  display: flex;
  align-items: baseline;
  gap: 8px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  color: #f3e1b8;
  cursor: pointer;
  text-align: left;
  width: 100%;
  transition: background 0.12s;
}
.notif-item:hover { background: var(--bg-raised); border-color: var(--border); }
.notif-item-surface { flex: 0 0 auto; }
.notif-item-body { flex: 1 1 auto; min-width: 0; }
.notif-item-from { font-weight: 600; }
.notif-item-excerpt { color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.notif-item-ts { flex: 0 0 auto; font-size: 0.72rem; color: var(--text-muted); }

/* #notif-center-btn carries .sit-btn but sits among transparent icon toggles
   (emotive-typing, location-hidden) — flatten it to match. border-color, not
   `border: none`, so it doesn't shrink 2px and nudge #header-more-btn. */
#notif-center-btn { background: transparent; border-color: transparent; position: relative; }
#notif-center-btn:hover { background: var(--accent-wash); }
/* .icon is display:block so it wraps onto its own line above a trailing
   inline .openwindow-dot, growing the button (and header) taller once a
   notification arrives. inline-flex keeps icon + dot on one row. */
#notif-center-btn, #header-more-btn { display: inline-flex; align-items: center; }
/* Red counter badge pinned to the bell's top-right corner — absolutely
   positioned so it overlaps the icon instead of adding to the button's
   flow height (unlike the plain .openwindow-dot it replaces here). */
.notif-badge {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 15px;
  height: 15px;
  padding: 0 3px;
  border-radius: 999px;
  background: #d32f2f;
  color: #fff;
  font-size: 0.62rem;
  font-weight: 700;
  line-height: 15px;
  text-align: center;
}
.notif-badge[hidden] { display: none; }

/* @mention autocomplete dropdown — shell reuses .sit-mode-popover's box/
   shadow; only the item rows are new. */
.mention-dropdown { padding: 4px; }
.mention-dropdown-item {
  display: flex;
  align-items: baseline;
  gap: 6px;
  width: 100%;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 6px 8px;
  color: #f3e1b8;
  cursor: pointer;
  text-align: left;
}
.mention-dropdown-item:hover, .mention-dropdown-item.highlighted { background: var(--bg-raised); border-color: var(--border); }
.mention-dropdown-id { color: var(--text-muted); font-size: 0.75rem; }

/* Click-to-jump landing highlight — a brief background flash, no movement.
   No prefers-reduced-motion suppression: matches this file's existing
   deliberate stance (see the comment near .mood-skip-link) that small,
   localised effects like this are mild enough to keep for everyone. */
.msg-jump-highlight { animation: msg-jump-flash 2s ease-out; }
@keyframes msg-jump-flash {
  0% { background: var(--accent-wash); }
  100% { background: transparent; }
}

#nearby-reply-row {
  display: flex; gap: 12px; align-items: center;
  margin-top: 6px; font-size: 0.78rem;
}

/* Constellation — soft ambient glow of shared calm on the world map. */
.constellation-glow {
  border-radius: 50%;
  background: radial-gradient(circle,
    rgba(243, 225, 184, 0.9) 0%,
    rgba(243, 225, 184, 0.5) 35%,
    rgba(243, 225, 184, 0) 70%);
  filter: blur(6px);
  pointer-events: none;
}

/* ember wordmark glyph — a small glowing amber ember before the "ember" header. */
.ember-mark {
  display: inline-block;
  width: 0.62em;
  height: 0.62em;
  margin-right: 0.28em;
  border-radius: 50%;
  vertical-align: baseline;
  background: radial-gradient(circle at 50% 45%, #ffd9a0 0%, #f3a06a 38%, #e0733a 68%, rgba(224,115,58,0) 100%);
  box-shadow: 0 0 10px 2px rgba(224, 115, 58, 0.55);
}

/* =============================================================================
   ROOM COMPOSER (public/js/room-composer.js) — one reusable composer for the
   live group rooms (Vibe, Gathering; Public still on its own #chat-input).
   Mirrors #chat-input / #poll-btn / #empathy-strip / .crisis-resources-panel
   so all composers share one visual language.
   ============================================================================= */
.room-composer { display: flex; flex-direction: column; flex: 0 0 auto; border-top: 1px solid var(--border); background: var(--bg-surface); }
.room-composer-crisis { margin: 8px 12px 0; }
.room-composer-crisis[hidden] { display: none; }
.room-composer-empathy {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  background: #5c4a2e;
  color: #ffd180;
  font-size: 0.82rem;
}
.room-composer-empathy[hidden] { display: none; }
.room-composer-empathy button {
  background: #7a6240;
  color: #ffd180;
  border: 1px solid #a08055;
  border-radius: var(--radius-sm);
  padding: 3px 10px;
  font-size: 0.78rem;
  cursor: pointer;
}
.room-composer-empathy button:hover { background: #8a7250; }
.room-composer-empathy .rc-empathy-send { background: #5c2e2e; border-color: #7c4a4a; color: #ffb4a8; }
.room-composer-empathy .rc-empathy-send:hover { background: #6c3a3a; }
.room-composer-reply-target {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  background: var(--bg-raised);
  color: #f3e1b8;
  font-size: 0.82rem;
}
.room-composer-reply-target[hidden] { display: none; }
.room-composer-reply-target .rt-label { flex: 1; }
.room-composer-reply-target button {
  background: var(--bg-raised);
  color: #f3e1b8;
  border: 1px solid #6b8cb3;
  border-radius: var(--radius-sm);
  padding: 2px 8px;
  cursor: pointer;
}
.room-composer-input-wrap { position: relative; flex: 1; display: flex; }
.room-composer-input {
  flex: 1;
  width: 100%;
  box-sizing: border-box;
  padding: 8px 30px 8px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-deep);
  color: var(--text);
  font-size: 0.9rem;
  font-family: inherit;
  /* Auto-grow up to ~5 lines. JS resets height on input; max-height clamps. */
  resize: none;
  line-height: 1.4;
  min-height: 36px;
  max-height: 7em;
  overflow-y: auto;
}
.room-composer-input:focus { outline: none; border-color: var(--accent); }
.room-composer-bar { display: flex; align-items: flex-end; padding: 8px 12px; gap: 8px; }
/* Cap + center every row inside the composer (crisis panel, empathy strip,
   reply chip, the input bar itself) so none of them stretch edge-to-edge on
   a wide screen — .room-composer above stays full width so its background/
   border-top still span the whole app. margin-left/right only (not the
   `margin` shorthand): several children already set their own margin-top,
   which this must not clobber. Comes after every child's own rule in source
   order so it wins on the horizontal margins specifically, at equal
   selector specificity. */
.room-composer > * { max-width: 880px; width: 100%; margin-left: auto; margin-right: auto; }
.room-composer-poll {
  height: 36px;
  box-sizing: border-box;
  background: none;
  color: var(--text-muted);
  border: none;
  border-radius: var(--radius-sm);
  padding: 0 10px;
  cursor: pointer;
  font-size: 1.05rem;
  line-height: 1;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color var(--ease), background var(--ease);
}
.room-composer-poll:hover { background: var(--accent-wash); color: var(--text); }
.room-composer-poll[hidden] { display: none; }
.room-composer-send {
  height: 40px;
  box-sizing: border-box;
  padding: 0 14px;
  border: none;
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--bg-deep);
  cursor: pointer;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background var(--ease);
}
.room-composer-send:hover { background: var(--accent-hover); }

.room-composer-emoji, .post-composer-emoji {
  height: 36px;
  box-sizing: border-box;
  background: none;
  color: var(--text-muted);
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 1rem;
  padding: 0 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color var(--ease), background var(--ease);
}
.room-composer-emoji:hover, .post-composer-emoji:hover { color: var(--text); background: var(--accent-wash); }

/* =============================================================================
   POST COMPOSER (public/js/post-composer.js) — one reusable composer for the
   Post family (Confession Wall, Nearby posts, Nearby replies). Mirrors
   .room-composer* above; CrisisWatch and the empathy gate are always on here,
   plus a char counter, an error line, an optional CW row (Confession only)
   and an optional reply-target chip (Nearby replies only).
   ============================================================================= */
.post-composer { display: flex; flex-direction: column; flex: 0 0 auto; border-top: 1px solid var(--border); background: var(--bg-surface); }
.post-composer-crisis { margin: 8px 12px 0; }
.post-composer-crisis[hidden] { display: none; }
.post-composer-empathy {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  background: #5c4a2e;
  color: #ffd180;
  font-size: 0.82rem;
}
.post-composer-empathy[hidden] { display: none; }
.post-composer-empathy button {
  background: #7a6240;
  color: #ffd180;
  border: 1px solid #a08055;
  border-radius: var(--radius-sm);
  padding: 3px 10px;
  font-size: 0.78rem;
  cursor: pointer;
}
.post-composer-empathy button:hover { background: #8a7250; }
.post-composer-empathy .pc-empathy-send { background: #5c2e2e; border-color: #7c4a4a; color: #ffb4a8; }
.post-composer-empathy .pc-empathy-send:hover { background: #6c3a3a; }
.post-composer-reply-target {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  background: var(--bg-raised);
  color: #f3e1b8;
  font-size: 0.82rem;
}
.post-composer-reply-target[hidden] { display: none; }
.post-composer-reply-target .rt-label { flex: 1; }
.post-composer-reply-target button {
  background: var(--bg-raised);
  color: #f3e1b8;
  border: 1px solid #6b8cb3;
  border-radius: var(--radius-sm);
  padding: 2px 8px;
  cursor: pointer;
}
.post-composer-cw-row { display: flex; align-items: center; gap: 8px; padding: 6px 12px 0; font-size: 0.82rem; color: var(--text-muted); }
.post-composer-cw-row[hidden] { display: none; }
.post-composer-cw-row .cw-toggle { display: flex; align-items: center; gap: 4px; cursor: pointer; }
.post-composer-cw-row input[type="text"] {
  flex: 1;
  padding: 4px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-deep);
  color: var(--text);
  font-size: 0.82rem;
}
.post-composer-category-row { display: flex; gap: 2px; flex-wrap: wrap; padding: 6px 12px 0; }
.post-composer-category-row[hidden] { display: none; }
.post-composer-category-opt { background: none; border: 1px solid transparent; border-radius: var(--radius-sm); padding: 1px 4px; font-size: 1rem; cursor: pointer; line-height: 1.2; }
.post-composer-category-opt.chosen { border-color: var(--accent); background: var(--bg-raised); }

.post-composer-location-row { display: flex; align-items: center; gap: 8px; padding: 6px 12px 0; font-size: 0.82rem; color: var(--text-muted); }
.post-composer-location-row[hidden] { display: none; }
/* Icon button (was a checkbox+label) — same shape as .post-composer-emoji/
   .post-composer-poll, so it reads as one of the row's action icons rather
   than a labeled field of its own. .active marks a pin armed ("here" or a
   picked spot), same treatment .post-composer-category-opt.chosen uses. */
.post-composer-location-toggle {
  height: 36px;
  box-sizing: border-box;
  background: none;
  color: var(--text-muted);
  border: none;
  border-radius: var(--radius-sm);
  padding: 0 8px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  transition: color var(--ease), background var(--ease);
}
.post-composer-location-toggle:hover { color: var(--text); background: var(--accent-wash); }
.post-composer-location-toggle.active { color: var(--accent); }
.post-composer-location-source { display: flex; gap: 4px; }
.post-composer-location-source[hidden] { display: none; }
.post-composer-location-opt { background: none; border: 1px solid #3a3050; color: #cdbfe0; border-radius: var(--radius-pill); padding: 2px 10px; font: inherit; font-size: 0.78rem; cursor: pointer; }
.post-composer-location-opt.chosen { border-color: var(--accent); background: var(--bg-raised); color: #efeaf6; }
.post-composer-input-wrap { position: relative; flex: 1; display: flex; }
.post-composer-input {
  flex: 1;
  width: 100%;
  box-sizing: border-box;
  padding: 8px 30px 8px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-deep);
  color: var(--text);
  font-size: 0.9rem;
  font-family: inherit;
  /* Auto-grow up to ~5 lines. JS resets height on input; max-height clamps. */
  resize: none;
  line-height: 1.4;
  min-height: 36px;
  max-height: 7em;
  overflow-y: auto;
}
.post-composer-input:focus { outline: none; border-color: var(--accent); }
.post-composer-row { display: flex; align-items: flex-end; padding: 8px 12px; gap: 8px; }
/* config.flatMedia layout (Nearby posts, Confessions): input on its own full-
   width row, every action icon plus send on a second row below it — see
   post-composer.js. Replaces .post-composer-row's single side-by-side line
   for just those two surfaces; wraps rather than overflows on a narrow phone,
   since flatMedia surfaces carry more icons than the default bar does. */
.post-composer-input-row { padding: 8px 12px 4px; }
.post-composer-actions-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px;
  padding: 0 12px 8px;
}
.post-composer-actions-row .post-composer-send { margin-left: auto; }
.post-composer-status-row { display: flex; align-items: center; padding: 0 12px; gap: 8px; }
/* Same cap-and-center treatment as .room-composer's, and for the same
   reason — see the comment there. */
.post-composer > * { max-width: 880px; width: 100%; margin-left: auto; margin-right: auto; }
/* Remaining-character ring, pinned inside the input's right edge, vertically
   centered — fills clockwise as you type (mirrors X/Twitter's composer)
   instead of a separate "N characters left" label taking its own row.
   Faint/neutral until near the limit, when it also gains a small centered
   countdown. Shared by post-composer.js and room-composer.js. */
.post-composer-ring,
.room-composer-ring {
  position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
  width: 20px; height: 20px;
  pointer-events: none;
  color: var(--text-faint);   /* stroke/text both read currentColor, so a skin override needs only this */
}
.post-composer-ring svg,
.room-composer-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.post-composer-ring .ring-track,
.room-composer-ring .ring-track { stroke: var(--border); }
.post-composer-ring .ring-fill,
.room-composer-ring .ring-fill { stroke: currentColor; transition: stroke-dashoffset 0.1s linear; }
.post-composer-ring.warn,
.room-composer-ring.warn { color: #ff8a65; }
.post-composer-ring.over,
.room-composer-ring.over { color: #ff5252; }
.post-composer-ring-text,
.room-composer-ring-text {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 8px; color: inherit;
}
.post-composer-error { color: #ff8a65; font-size: 0.8rem; flex: 1; }
.post-composer-poll {
  height: 36px;
  box-sizing: border-box;
  background: none;
  color: var(--text-muted);
  border: none;
  border-radius: var(--radius-sm);
  padding: 0 10px;
  cursor: pointer;
  font-size: 1.05rem;
  line-height: 1;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color var(--ease), background var(--ease);
}
.post-composer-poll:hover { background: var(--accent-wash); color: var(--text); }
.post-composer-poll[hidden] { display: none; }
.post-composer-send {
  height: 40px;
  box-sizing: border-box;
  padding: 0 14px;
  border: none;
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--bg-deep);
  cursor: pointer;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background var(--ease);
}
.post-composer-send:hover { background: var(--accent-hover); }
.post-composer-send:disabled { opacity: 0.45; cursor: not-allowed; }
.post-composer-send:disabled:hover { background: var(--accent); }

/* Collapsed-by-default trigger + its <dialog> — see post-composer.js. Same
   glass/backdrop treatment as #mood-modal/#keepsake-modal. */
.post-composer-trigger {
  display: block; width: 100%; box-sizing: border-box; text-align: left;
  max-width: 880px; margin: 8px auto; padding: 8px 12px;
  background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
  color: var(--text-faint); font: inherit; font-size: 0.9rem; cursor: pointer;
}
.post-composer-trigger:hover { border-color: var(--accent); color: var(--text-muted); }
.post-composer-dialog {
  position: relative;
  background: var(--glass);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lift);
  padding: 28px 0 0;
  width: min(560px, 94vw);
}
.post-composer-dialog::backdrop { background: rgba(5, 8, 16, 0.7); backdrop-filter: blur(4px); }
.post-composer-dialog .post-composer { border-top: none; background: none; }
.post-composer-dialog-close {
  position: absolute; top: 4px; right: 6px; z-index: 1;
  background: none; border: none; color: var(--text-faint);
  font-size: 1.3rem; line-height: 1; cursor: pointer; padding: 4px 8px;
}
.post-composer-dialog-close:hover { color: var(--text); }

/* =============================================================================
   VIBE ROOMS ("🫧") — ambient rooms segmented by inner state, mirrors the
   Campfires two-view shape (list view / room view).
   ============================================================================= */

#main.viberooms-active #chat-input { display: none; }

#viberooms-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; padding: 12px; }
.viberooms-intro { padding: 12px 12px 0; color: #8b87b0; font-size: 0.9rem; }
.vibe-card { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; background: #1d1726; border: 1px solid #2c2338; border-radius: var(--radius); padding: 14px; cursor: pointer; color: #efeaf6; text-align: left; }
.vibe-card:hover { border-color: var(--accent); }
.vibe-emoji { font-size: 1.6rem; }
.vibe-label { font-size: 1rem; }
.vibe-presence { font-size: 0.72rem; color: #7a7089; }
.vibe-presence-a-few { color: #f3e1b8; }
.vibe-presence-lively { color: var(--accent); }
#viberoom-view { display: flex; flex-direction: column; height: 100%; }
#viberoom-view[hidden] { display: none; }   /* flex would otherwise beat the UA [hidden] rule (recurring trap) */
.viberoom-title { font-size: 1.1rem; padding: 8px 12px; color: #f3e1b8; }
#viberoom-messages { flex: 1; overflow-y: auto; padding: 8px 12px; }
.vibe-msg { display: block; padding: 4px 0; font-size: 0.9rem; }   /* cancel .msg's flex-column: .vibe-msg-inner is the sole child here, no .msg-inner */
.vibe-msg-inner { position: relative; display: inline-block; max-width: 100%; padding-right: 10px; }   /* sized-to-content anchor for .msg-pin (position:absolute) — without this it pins to the row's full width instead of next to the text */
.vibe-msg-alias { color: #8b87b0; margin-right: 8px; }
.vibe-msg-text { color: #efeaf6; }

/* =============================================================================
   GATHERINGS ("✨") — user-created ephemeral map gatherings; glowing pins +
   tap-to-join group chat. Mirrors the Vibe-Room room-view shape.
   ============================================================================= */
.gathering-pin { display: flex; align-items: center; justify-content: center; border-radius: 50%;
  background: radial-gradient(circle, rgba(255,214,120,0.55), rgba(255,214,120,0) 70%); font-size: 1.1rem; }
.gathering-pin-preview { display: flex; align-items: center; justify-content: center; border-radius: 50%;
  background: radial-gradient(circle, rgba(255,64,64,0.7), rgba(255,64,64,0) 70%); font-size: 1.7rem;
  filter: drop-shadow(0 0 2px rgba(0,0,0,0.5)); }
.nearby-pin {
  font-size: 1rem; line-height: 26px; text-align: center;
  width: 26px; height: 26px;
  background: #1d1726; border: 1px solid #3a3050; border-radius: 50%;
  box-shadow: 0 1px 4px rgba(0,0,0,0.4);
  transition: transform 120ms ease-out;
}
.cluster-badge {
  position: absolute;
  bottom: -2px;
  right: -2px;
  font-size: 0.65rem;
  background: rgba(0, 0, 0, 0.75);
  color: #f3e1b8;
  border-radius: var(--radius-pill);
  padding: 0 4px;
  line-height: 1.3;
  pointer-events: none;
}
#gathering-start { position: absolute; z-index: 1000; left: 12px; bottom: 12px;
  background: #1d1726; color: #ffd678; border: 1px solid #3a3050; border-radius: var(--radius-pill); padding: 6px 12px; cursor: pointer; }
.gathering-composer { position: absolute; z-index: 1000; left: 12px; bottom: 52px; display: flex; flex-direction: column; gap: 4px; background: #1d1726; border: 1px solid #2c2338; border-radius: var(--radius); padding: 8px; }
.gathering-composer input { flex: 1; background: var(--bg-sunken); color: #efeaf6; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 4px 8px; font: inherit; font-size: 0.85rem; }
.gathering-composer-row button { background: none; border: 1px solid #3a3050; color: #cdbfe0; border-radius: var(--radius-pill); padding: 3px 12px; font: inherit; font-size: 0.8rem; cursor: pointer; }
/* Only flex-direction/height here — DISPLAY is owned by .tab-panel/.tab-panel.active,
   so the panel stays hidden when inactive (don't set display:flex unconditionally). */
.gathering-panel { flex-direction: column; height: 100%; }
#main.gathering-active #chat-input { display: none; }   /* gathering tab has its own composer */
.gathering-emoji-row { display: flex; gap: 2px; flex-wrap: wrap; margin-bottom: 4px; }
.gathering-emoji-opt { background: none; border: 1px solid transparent; border-radius: var(--radius-sm); padding: 1px 4px; font-size: 1rem; cursor: pointer; line-height: 1.2; }
.gathering-emoji-opt.chosen { border-color: var(--accent); background: var(--bg-raised); }
.gathering-loc-toggle { display: flex; gap: 4px; margin-bottom: 4px; }
.gathering-loc-opt { background: none; border: 1px solid #3a3050; color: #cdbfe0; border-radius: var(--radius-pill); padding: 2px 10px; font: inherit; font-size: 0.78rem; cursor: pointer; }
.gathering-loc-opt.chosen { border-color: var(--accent); background: var(--bg-raised); color: #efeaf6; }
.gathering-start-input { background: var(--bg-sunken); color: #efeaf6; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 4px 8px; font: inherit; font-size: 0.85rem; }
.gathering-composer-row { display: flex; gap: 6px; }
.leaflet-tooltip.gathering-tip { background: #1d1726; color: #ffd678; border: 1px solid #3a3050; border-radius: var(--radius-sm); font-size: 0.75rem; padding: 2px 6px; box-shadow: none; }
.leaflet-tooltip.gathering-tip::before { display: none; }   /* no callout arrow */
.leaflet-tooltip.user-hover-tip { background: var(--bg-surface); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: 0.75rem; line-height: 1.5; padding: 4px 8px; box-shadow: var(--shadow-soft); }
.leaflet-tooltip.user-hover-tip::before { display: none; }   /* no callout arrow */
.gathering-room-header { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: #1d1726; border-bottom: 1px solid #2c2338; flex: 0 0 auto; }
.gathering-room-title { flex: 1; color: #efeaf6; font-size: 0.95rem; }
.gathering-loc-btn, .gathering-leave-btn, .gathering-end-btn, .gathering-update-btn { background: none; border: 1px solid #3a3050; color: #cdbfe0; border-radius: var(--radius-pill); padding: 3px 10px; font: inherit; font-size: 0.78rem; cursor: pointer; }
.gathering-room-status { flex: 1; color: #8b87b0; font-size: 0.78rem; }
.gathering-tab-status { color: #8b87b0; font-size: 0.78em; margin-left: 3px; }
.gathering-update-pop { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; padding: 8px 12px; background: #1d1726; border-bottom: 1px solid #2c2338; flex: 0 0 auto; font-size: 0.82rem; color: #cdbfe0; }
.gathering-update-pop[hidden] { display: none; }
.gathering-update-save { background: none; border: 1px solid #3a3050; color: #cdbfe0; border-radius: var(--radius-pill); padding: 3px 10px; font: inherit; font-size: 0.78rem; cursor: pointer; }
.gathering-loc-map { height: 340px; flex: 0 0 auto; }
.gathering-loc-map[hidden] { display: none; }
.gathering-room-messages { flex: 1; overflow-y: auto; padding: 10px 12px; }
.gathering-msg { display: block; padding: 3px 0; font-size: 0.92rem; }   /* same reason as .vibe-msg above */
.gathering-msg-inner { position: relative; display: inline-block; max-width: 100%; padding-right: 10px; }   /* see .vibe-msg-inner */
.gathering-msg-who { color: #8b87b0; margin-right: 6px; }
.gathering-msg-text { color: #efeaf6; }

/* =============================================================================
   REACH OUT ("✉️") — private first-message request from a Nearby post/reply.
   ============================================================================= */
.nearby-reachout-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); margin-left: 4px; vertical-align: middle; }
.nearby-reachout-dot[hidden] { display: none; }
#reachout-requests { border: 1px solid #2c2338; border-radius: var(--radius); margin: 10px 12px; padding: 10px; background: #1d1726; }
.reachout-requests-title { font-size: 0.8rem; color: #8b87b0; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
.reachout-req { padding: 8px 0; border-bottom: 1px solid #241d30; }
.reachout-req-who { font-size: 0.8rem; color: #8b87b0; }
.reachout-req-msg { font-size: 0.95rem; color: #efeaf6; margin: 2px 0 6px; }
.reachout-req-actions { display: flex; gap: 8px; }
.reachout-req-actions button { background: none; border: 1px solid #3a3050; color: #cdbfe0; border-radius: var(--radius-pill); padding: 3px 12px; font: inherit; font-size: 0.8rem; cursor: pointer; }
/* font-size + line-height explicit, like every other icon-button here — a
 * bare button otherwise falls back to the platform's control font, whose
 * "normal" line-height runs taller than the app's own text and, in an
 * in-flow row (Nearby's revealed .nearby-card-actions), stretches the row
 * (and the whole card) a couple px taller than its siblings. */
.reachout-btn { background: none; border: none; cursor: pointer; opacity: 0.6; font-size: 1.05rem; line-height: 1; padding: 0 2px; }
.reachout-btn:hover { opacity: 1; }
.reachout-composer { display: flex; gap: 6px; margin-top: 6px; }
.reachout-composer input { flex: 1; background: var(--bg-sunken); color: #efeaf6; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 4px 8px; font: inherit; font-size: 0.85rem; }
.reachout-composer button { background: none; border: 1px solid #3a3050; color: #cdbfe0; border-radius: var(--radius-pill); padding: 3px 12px; font: inherit; font-size: 0.8rem; cursor: pointer; }
.nb-edit-composer { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
.nb-edit-composer textarea { background: var(--bg-sunken); color: #efeaf6; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 6px 8px; font: inherit; font-size: 0.85rem; resize: vertical; min-height: 44px; }
.nb-edit-composer-row { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
.nb-edit-save { background: none; border: 1px solid #3a3050; color: #cdbfe0; border-radius: var(--radius-pill); padding: 3px 12px; font: inherit; font-size: 0.8rem; cursor: pointer; }

/* =============================================================================
   BLOCK — general cross-app user block (private tab + pair view controls,
   blocked-people management panel near the location-hidden toggle).
   ============================================================================= */
#blocked-people { border: 1px solid #2c2338; border-radius: var(--radius); margin: 10px 12px; padding: 10px; background: #1d1726; }
.blocked-people-title { font-size: 0.8rem; color: #8b87b0; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
.blocked-row { display: flex; align-items: center; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #241d30; }
.blocked-who { font-size: 0.9rem; color: #efeaf6; }
.unblock-btn { background: none; border: 1px solid #3a3050; color: #cdbfe0; border-radius: var(--radius-pill); padding: 3px 12px; font: inherit; font-size: 0.8rem; cursor: pointer; }
.private-block-btn, .pair-block-btn { background: none; border: none; color: #8b87b0; cursor: pointer; font: inherit; font-size: 0.85rem; opacity: 0.75; }
.private-block-btn:hover, .pair-block-btn:hover { opacity: 1; color: #cdbfe0; }
#blocked-people[hidden] { display: none; }

/* =============================================================================
   MINIMAL SKIN — component-level rules, scoped under html[data-skin="minimal"]
   =============================================================================
   Every other skin above is token-only: it repaints surfaces/text/accent and
   touches nothing else. minimal can't be — "light theme", "no animations",
   "no glassmorphism" and "stark mood grid" aren't colors, they're behavior —
   so this is the one skin allowed component-level rules (see the minimal
   skin plan / CLAUDE.md for the sign-off). Everything below is scoped under
   html[data-skin="minimal"] and changes nothing for the other six skins.

   The app's semantic colors (halls vote green/red, CW/crisis amber, pulse
   pink, keepsake gold, private-message/campfire orange, moment purple, mood
   auras) are normally never re-hued — a skin repaints neutral surfaces, not
   what colors MEAN (see the big comment at the top of this file). That rule
   is relaxed here too, narrowly: those hexes were measured against white and
   most fail 4.5:1 (crisis amber ~1.4:1, keepsake gold ~1.3:1) — on white,
   obeying "never re-hue" literally would destroy the color's meaning far
   more than darkening it does. Section 1 below darkens each hue's VALUE
   while keeping its IDENTITY; sections 2-5 suppress the decoration (motion,
   gradients, glass, shadow) the user explicitly asked to remove.

   This section grew well past the semantic table's own "~9 rules" estimate:
   several of these hexes (#f3e1b8 above all) turned out to be reused as a
   general "warm highlight" text color across a dozen unrelated features,
   not just the one feature each was named for. Every rule below is either
   (a) one of the 8 named semantics used as real text/state a user must
   read, or (b) a plain non-semantic light color (e.g. #f0e6d2, #efeaf6)
   left over from the dark palette, sitting on a background that only
   becomes light IN THIS SKIN (a var(--bg-*) token, or a container flattened
   in section 3 below) — fixed to the nearest text token since it never
   carried any protected meaning to begin with. Low-alpha washes/fills are
   deliberately left alone throughout, per the plan's own instruction — a
   pale wash under darkened text still reads fine on white. Dark,
   self-contained islands (Vibe Rooms list, Gathering room chrome, Reach Out
   inbox, Block panel, Open Window's own URL/textarea fields, own-message
   bubble/poll, several warning banners) were reported-not-fixed in an
   earlier pass — that carve-out was narrowed to true Leaflet-rendered map
   tiles/pins/tooltips only; the rest are fixed in section 6 below. */

/* --- 1. Darkened semantic colors ---------------------------------------- */

/* halls vote up: #66bb6a -> #1e7d32 (5.20:1) */
html[data-skin="minimal"] .hall-vote-up:hover { color: #1e7d32; border-color: #1e7d32; }
html[data-skin="minimal"] .hall-vote-up[data-mine="true"] { background: #1e7d32; color: #fff; border-color: #1e7d32; }

/* halls vote down: #ef5350 -> #b3261e (6.54:1). Also covers the
   location-hidden toggle's checked state and every "error" red left over
   from the dark palette (#ff9c9c/#ff8a80/#ff8a65) — same family, one value. */
html[data-skin="minimal"] .hall-vote-down:hover { color: #b3261e; border-color: #b3261e; }
html[data-skin="minimal"] .hall-vote-down[data-mine="true"] { background: #b3261e; color: #fff; border-color: #b3261e; }
html[data-skin="minimal"] .location-hidden-toggle:has(input:checked) { color: #b3261e; }
html[data-skin="minimal"] #moment-error,
html[data-skin="minimal"] .poll-error,
html[data-skin="minimal"] .post-composer-ring.warn,
html[data-skin="minimal"] .room-composer-ring.warn,
html[data-skin="minimal"] .post-composer-error { color: #b3261e; }

/* CW + crisis amber (text): #ffd54f / #ffc278 -> #8a5a00 (5.93:1) */
html[data-skin="minimal"] .cw-pill,
html[data-skin="minimal"] .crisis-links a,
html[data-skin="minimal"] .halls-question h3,
html[data-skin="minimal"] .msg.icebreaker,
html[data-skin="minimal"] .thread-title,
html[data-skin="minimal"] .msg.icebreaker .ib-thread-btn.has-replies { color: #8a5a00; }
html[data-skin="minimal"] .msg.icebreaker .ib-thread-btn.has-replies:hover { border-color: #8a5a00; }
/* hover state used literal white, also invisible on a light wash */
html[data-skin="minimal"] .crisis-links a:hover { color: var(--text); border-bottom-color: var(--text); }

/* pulse pink: #ec407a -> #a3124b (7.65:1) */
html[data-skin="minimal"] #pulse-strip.joined { color: #a3124b; }

/* private-message orange: #ff9800 -> #8a4b00 (6.80:1). #ffb74d is the same
   family's lighter "waiting" tint, darkened the same way. */
html[data-skin="minimal"] .msg.private,
html[data-skin="minimal"] #plane-counter.warn { color: #8a4b00; }
html[data-skin="minimal"] .stream-toggle.waiting,
html[data-skin="minimal"] .whisper-toggle.waiting { color: #8a4b00; border-color: #8a4b00; }

/* campfire count: #c66 -> #a1352f (6.85:1). #d97a3c is the inline btn's
   lighter tint of the same count — same darkened value for consistency. */
html[data-skin="minimal"] .campfires-tab-count,
html[data-skin="minimal"] .campfire-btn .campfire-count { color: #a1352f; }

/* moment/void purple: #6a5acd -> #4a3aa8 (8.53:1) — applied to the meaningful
   borders (the ones a user relies on to tell a card apart), not every
   decorative tint. Focus rings that used to be #f3e1b8 move to var(--accent)
   instead, matching every other input in the app — a focus ring must stay
   visible (non-negotiable) and this keeps ONE convention for it. */
html[data-skin="minimal"] #void-card { border-left-color: #4a3aa8; }
html[data-skin="minimal"] #void-card-answer:hover { border-color: #4a3aa8; }
html[data-skin="minimal"] #moment-enter { border-color: #4a3aa8; }
html[data-skin="minimal"] .confession.void-tagged { border-left-color: #4a3aa8; }
html[data-skin="minimal"] #moment-input:focus,
html[data-skin="minimal"] #plane-text:focus,
html[data-skin="minimal"] .plane-reply:focus,
html[data-skin="minimal"] .minigame-told-input:focus { border-color: var(--accent); }

/* keepsake gold (as text): #f3e1b8 -> #7a5c1a (6.23:1). By far the most
   reused of the eight — a general "warm highlight" text color the dark
   palette leaned on across Moment, Plane, Pair, Stream, Stage, Nearby,
   Room/Post composers, Vibe Rooms and mini-games, not just Keepsake. Every
   rule below is real text a user must read; the tiny handful of purely
   decorative border-left accent stripes get the same value for a visible
   line rather than an anemic pale one on white, at negligible extra cost. */
html[data-skin="minimal"] #moment-enter,
html[data-skin="minimal"] #moment-title,
html[data-skin="minimal"] .sit-mode-option,
html[data-skin="minimal"] .more-toggle,
html[data-skin="minimal"] .empathy-pick,
html[data-skin="minimal"] #my-link-copy,
html[data-skin="minimal"] #plane-modal h3,
html[data-skin="minimal"] #plane-toast,
html[data-skin="minimal"] .plane-hint,
html[data-skin="minimal"] .plane-layer,
html[data-skin="minimal"] #pair-banner,
html[data-skin="minimal"] #pair-banner-action,
html[data-skin="minimal"] #reply-strip,
html[data-skin="minimal"] #reply-strip button,
html[data-skin="minimal"] .stream-invite-banner,
html[data-skin="minimal"] .stream-invite-banner button,
html[data-skin="minimal"] .stream-toggle:hover,
html[data-skin="minimal"] .stream-toggle.active,
html[data-skin="minimal"] .stream-live,
html[data-skin="minimal"] .stream-live-cursor,
html[data-skin="minimal"] #mention-toast,
html[data-skin="minimal"] .room-composer-reply-target,
html[data-skin="minimal"] .room-composer-reply-target button,
html[data-skin="minimal"] .post-composer-reply-target,
html[data-skin="minimal"] .post-composer-reply-target button,
html[data-skin="minimal"] .minigame-header,
html[data-skin="minimal"] .minigame-bar-mine .minigame-bar-count,
html[data-skin="minimal"] .minigame-answer-mine,
html[data-skin="minimal"] .minigame-boast,
html[data-skin="minimal"] .minigame-told-submit,
html[data-skin="minimal"] .stage-headline,
html[data-skin="minimal"] #stage-take,
html[data-skin="minimal"] .stage-tally-pill,
html[data-skin="minimal"] #stage-yield:hover,
html[data-skin="minimal"] .stage-timer,
html[data-skin="minimal"] .viberoom-title,
html[data-skin="minimal"] .drift-away-btn:hover,
html[data-skin="minimal"] .stone-toggle:hover,
html[data-skin="minimal"] .stone-toggle.active,
html[data-skin="minimal"] .notif-item,
html[data-skin="minimal"] .mention-dropdown-item,
html[data-skin="minimal"] .first-run-tab.active,
html[data-skin="minimal"] .first-run-btn,
html[data-skin="minimal"] .empathy-token-line { color: #7a5c1a; }
html[data-skin="minimal"] .stream-toggle.active,
html[data-skin="minimal"] .drift-away-btn:hover,
html[data-skin="minimal"] .stone-toggle:hover,
html[data-skin="minimal"] .stone-toggle.active { border-color: #7a5c1a; }
html[data-skin="minimal"] .moment-answer,
html[data-skin="minimal"] .minigame-bar-mine,
html[data-skin="minimal"] .minigame-answer-mine,
html[data-skin="minimal"] .minigame-card,
html[data-skin="minimal"] .minigame-card--revealed { border-left-color: #7a5c1a; }
html[data-skin="minimal"] .confessions-moment-dot { background: #7a5c1a; }

/* --- 2. Generic light-on-dark leftovers ---------------------------------
   Not one of the eight protected semantics — just plain body/label text
   from the dark palette (#f0e6d2, #efeaf6, #9a94c4, #8b87b0, #c9b97a, ...)
   that only breaks because its container is a var(--bg-*) token (or one of
   the containers flattened in section 3) and only IN THIS SKIN turns light.
   Mapped to the nearest existing text token — introduces no new colors. */
html[data-skin="minimal"] #confession-post,
html[data-skin="minimal"] .crisis-resources-panel,
html[data-skin="minimal"] .crisis-dismiss:hover,
html[data-skin="minimal"] #keepsake-actions button,
html[data-skin="minimal"] .sit-btn,
html[data-skin="minimal"] #plane-modal,
html[data-skin="minimal"] #plane-text,
html[data-skin="minimal"] #plane-cancel, html[data-skin="minimal"] #plane-throw,
html[data-skin="minimal"] .plane-card,
html[data-skin="minimal"] .plane-reply,
html[data-skin="minimal"] .plane-keep, html[data-skin="minimal"] .plane-rethrow,
html[data-skin="minimal"] .pair-gate-card,
html[data-skin="minimal"] #stage-speaker-bar .room-composer-input,
html[data-skin="minimal"] .stage-msg,
html[data-skin="minimal"] #nearby-post,
html[data-skin="minimal"] .talk-waiting-card,
html[data-skin="minimal"] .first-run-tab:hover,
html[data-skin="minimal"] #my-link-url,
html[data-skin="minimal"] #my-link-rotate:hover,
html[data-skin="minimal"] #talk-waiting-close:hover,
html[data-skin="minimal"] .ow-answer-text,
html[data-skin="minimal"] .post-composer-location-opt.chosen,
html[data-skin="minimal"] .gathering-composer input,
html[data-skin="minimal"] .gathering-loc-opt.chosen,
html[data-skin="minimal"] .gathering-start-input,
html[data-skin="minimal"] .reachout-composer input,
html[data-skin="minimal"] .nb-edit-composer textarea,
html[data-skin="minimal"] #void-card-prompt,
html[data-skin="minimal"] #moment-view-prompt,
html[data-skin="minimal"] .moment-answer,
html[data-skin="minimal"] .halls-question li,
html[data-skin="minimal"] .thread-hall li,
html[data-skin="minimal"] .vibe-msg-text,
html[data-skin="minimal"] .gathering-msg-text,
html[data-skin="minimal"] .mood-status-label,
html[data-skin="minimal"] .empathy-token-line b { color: var(--text); }

html[data-skin="minimal"] #void-card-eyebrow,
html[data-skin="minimal"] #moment-subtitle,
html[data-skin="minimal"] #moment-wall-header,
html[data-skin="minimal"] #void-card-answer,
html[data-skin="minimal"] .post-composer-location-opt,
html[data-skin="minimal"] .ow-panel-label,
html[data-skin="minimal"] .ow-question-group-header,
html[data-skin="minimal"] .vibe-msg-alias,
html[data-skin="minimal"] .gathering-msg-who,
html[data-skin="minimal"] .reachout-composer button,
html[data-skin="minimal"] .nb-edit-save,
html[data-skin="minimal"] .private-block-btn,
html[data-skin="minimal"] .pair-block-btn { color: var(--text-muted); }
html[data-skin="minimal"] #void-card-answer:hover { color: var(--text); background: var(--bg-raised); }
html[data-skin="minimal"] .private-block-btn:hover,
html[data-skin="minimal"] .pair-block-btn:hover { color: var(--text); }

html[data-skin="minimal"] #moment-locked-hint,
html[data-skin="minimal"] .ow-panel-hint,
html[data-skin="minimal"] .more-section,
html[data-skin="minimal"] .ow-question-group-header.ow-question-legacy,
html[data-skin="minimal"] .minigame-countdown,
html[data-skin="minimal"] .minigame-state-submitted,
html[data-skin="minimal"] .stage-cooldown-hint,
html[data-skin="minimal"] .nearby-quiet-header,
html[data-skin="minimal"] .empathy-token-caption { color: var(--text-faint); }

/* small status/notification dots — a light dot on white is a dot nobody
   sees. Reused var(--accent), matching every other "new activity" dot
   (.nearby-tab-dot, .nearby-reachout-dot) already in the app. */
html[data-skin="minimal"] .openwindow-dot { background: var(--accent); }

/* whisper toggle's hover/active states used the same "warm highlight"
   language as the rest of the app; var(--accent) is the token equivalent. */
html[data-skin="minimal"] .whisper-toggle:hover,
html[data-skin="minimal"] .whisper-toggle.active { color: var(--accent); }
html[data-skin="minimal"] .whisper-toggle.active { border-color: var(--accent); }

/* --- 3. No gradients -----------------------------------------------------
   Flattened to a token per the plan: #void-card, #moment-sky, #moment-enter
   (+ its :hover) and #moment-nudge-toast. Stage's four bottom bars
   (#stage-listener-bar/.stage-reactions/#stage-speaker-bar/.stage-stream)
   are the same "atmospheric flourish" pattern under a different name —
   translucent dark-navy washes tuned for a night-sky backdrop that either
   read as visible dark islands or, worse, leave #stage-yield's var(--text-
   muted) label unreadable (dark token text over a dark wash) once the page
   behind them turns white. Flattened for the same reason and verified for
   contrast below, same as the four named ones. */
html[data-skin="minimal"] #void-card { background: var(--bg-sunken); }
/* .first-run-card's ember glow is the same "atmospheric flourish" this
   section already strips elsewhere — an orange bloom over a white card
   reads as noise, not warmth. */
html[data-skin="minimal"] .first-run-card::before { display: none; }
html[data-skin="minimal"] #moment-sky {
  background: var(--bg-surface);
  color: var(--text);
  border-color: var(--border);
}
html[data-skin="minimal"] #moment-enter { background: var(--bg-raised); }
html[data-skin="minimal"] #moment-enter:hover { background: var(--accent-wash); }
html[data-skin="minimal"] #moment-input {
  /* Recessed textarea inside #moment-sky's now-white card — bg-sunken
     reads as "inset" the way every other composer input in this skin does
     (fallout from the surface-ladder inversion above: bg-deep is no longer
     the lightest value, so it can't stand in for "white input" anymore). */
  background: var(--bg-sunken);
  color: var(--text);
  border-color: var(--border);
}
html[data-skin="minimal"] #moment-nudge-toast {
  background: var(--bg-raised);
  color: var(--text);
  border-color: var(--border);
  box-shadow: var(--shadow-lift);
}
html[data-skin="minimal"] .stage-tally { background: var(--bg-raised); }
html[data-skin="minimal"] .stage-stream { background: var(--bg-deep); }
html[data-skin="minimal"] #stage-listener-bar { background: var(--bg-sunken); }
html[data-skin="minimal"] .stage-reactions { background: var(--bg-surface); }
html[data-skin="minimal"] #stage-speaker-bar { background: var(--bg-surface); }

/* --- 4. (removed) -----------------------------------------------------
   minimal is a colour/theme skin only — it deliberately does NOT alter
   motion. The pulse-beat/hover-zoom suppression that used to live here was
   removed outright: the animations are small and localised enough to keep
   for everyone, and the one full-viewport effect (#pulse-glow) was addressed
   by slowing the heartbeat in server/pulse.js instead. See the note above
   .mood-skip-link (~1702) for the full reasoning.
   Do not re-add animation:none/transform:none rules here. */

/* --- 5. Stark mood grid + clean input/nav --------------------------------
   .mood-btn: plain border, bg-surface fill; hover darkens the border to
   var(--accent). Hover lift/shadow and the emoji scale are NOT overridden
   here — they fall through to the base rule's var(--shadow-lift), which
   already resolves to this skin's own light-appropriate shadow value; a
   colour-only skin still moves and lifts like every other skin. bg-surface is
   the fallout fix from the surface-ladder inversion above: this rule wants
   a literal flat white against the modal's glass backdrop, and bg-surface
   (not bg-deep) is now the token that means "white." Each mood's
   own --mood-aura (set inline by openMoodPicker(), never re-hued elsewhere)
   is a translucent color tuned for a dark backdrop; checked live against
   white and it washes out to near-invisible pastel there, so this skin
   ignores it and always uses var(--accent) instead, as the plan allows.
   #message-input/the composers needed no shadow removed — they never had
   one; the focus ring (box-shadow via var(--accent-ring)) is untouched and
   stays visible, as required. Tab strip: the active-tab underline
   (.tab.active::after) is untouched — it's the only thing marking the
   active tab and stays; only the hover wash is dropped for a plain color
   change. */
html[data-skin="minimal"] .mood-btn {
  background: var(--bg-surface);
  border-color: var(--border);
}
html[data-skin="minimal"] .mood-btn:hover {
  background: var(--bg-surface);
  border-color: var(--accent);
}
html[data-skin="minimal"] .tab:hover { background: transparent; }

/* --- 6. Dark islands -------------------------------------------------------
   Section 0's own comment called these seven "self-contained islands" and
   left them alone: own dark bg + own light text, internally readable, just
   visually wrong on a light skin. Folded in here per the follow-up ask.
   Same sweep also turned up several more of the identical defect that
   weren't on that original list (msg.mine bubble, poll-card.mine, keepsake
   share button, moment-share button, whisper/empathy/stream banners+buttons,
   Open Window's save/copy/share/rotate buttons, gathering room chrome, map's
   own center-me button) — reported per the brief, fixed the same way. Left
   alone: actual map tiles/pins/tooltips (Leaflet-rendered, outside this
   stylesheet's reach, same as the plan's original map-tile carve-out) and
   any rule already covered by sections 1-5 above. */

/* Vibe Rooms list card + presence line: #1d1726/#efeaf6/#2c2338/#7a7089 ->
   surface/text/border/muted tokens (5.20-16.59:1, all >=4.5:1). */
html[data-skin="minimal"] .vibe-card {
  background: var(--bg-surface);
  border-color: var(--border);
  color: var(--text);
}
html[data-skin="minimal"] .vibe-presence { color: var(--text-muted); }
html[data-skin="minimal"] .vibe-presence-a-few { color: var(--text-muted); }
html[data-skin="minimal"] .viberooms-intro { color: var(--text-muted); }

/* Gathering composer/start button + map pin chrome: #1d1726/#ffd678,#efeaf6/
   #3a3050,#2c2338/#cdbfe0 -> surface/deep + text/muted + border tokens. */
html[data-skin="minimal"] #gathering-start,
html[data-skin="minimal"] .gathering-composer,
html[data-skin="minimal"] .leaflet-tooltip.gathering-tip,
html[data-skin="minimal"] .gathering-room-header,
html[data-skin="minimal"] .gathering-update-pop {
  background: var(--bg-surface);
  border-color: var(--border);
  color: var(--text);
}
html[data-skin="minimal"] .gathering-composer input,
html[data-skin="minimal"] .gathering-start-input {
  background: var(--bg-sunken);
  color: var(--text);
}
html[data-skin="minimal"] .gathering-emoji-opt.chosen { background: var(--bg-raised); }
html[data-skin="minimal"] .gathering-loc-opt,
html[data-skin="minimal"] .gathering-composer-row button,
html[data-skin="minimal"] .gathering-loc-btn,
html[data-skin="minimal"] .gathering-leave-btn,
html[data-skin="minimal"] .gathering-end-btn,
html[data-skin="minimal"] .gathering-update-btn,
html[data-skin="minimal"] .gathering-update-save {
  border-color: var(--border);
  color: var(--text-muted);
}
html[data-skin="minimal"] .gathering-loc-opt.chosen { background: var(--bg-raised); color: var(--text); }
html[data-skin="minimal"] .gathering-room-title { color: var(--text); }
html[data-skin="minimal"] .gathering-room-status,
html[data-skin="minimal"] .gathering-tab-status { color: var(--text-muted); }
/* map's own "center me" button — same dark-fill defect, same fix */
html[data-skin="minimal"] #map-center-me {
  background: var(--bg-surface);
  color: var(--text-muted);
  border-color: var(--border);
}

/* Reach Out inbox: text-only defect (panel bg is already var(--bg-surface)
   via #reachout-requests below) — #8b87b0/#efeaf6/#cdbfe0/#3a3050. */
html[data-skin="minimal"] #reachout-requests {
  background: var(--bg-surface);
  border-color: var(--border);
}
html[data-skin="minimal"] .reachout-requests-title,
html[data-skin="minimal"] .reachout-req-who { color: var(--text-muted); }
html[data-skin="minimal"] .reachout-req { border-bottom-color: var(--border); }
html[data-skin="minimal"] .reachout-req-msg { color: var(--text); }
html[data-skin="minimal"] .reachout-req-actions button,
html[data-skin="minimal"] .reachout-composer button,
html[data-skin="minimal"] .nb-edit-save { border-color: var(--border); color: var(--text-muted); }
html[data-skin="minimal"] .reachout-composer input,
html[data-skin="minimal"] .nb-edit-composer textarea { background: var(--bg-sunken); color: var(--text); }

/* Block panel: same shape as Reach Out — #efeaf6/#cdbfe0/#8b87b0/#3a3050. */
html[data-skin="minimal"] #blocked-people {
  background: var(--bg-surface);
  border-color: var(--border);
}
html[data-skin="minimal"] .blocked-people-title { color: var(--text-muted); }
html[data-skin="minimal"] .blocked-row { border-bottom-color: var(--border); }
html[data-skin="minimal"] .blocked-who { color: var(--text); }
html[data-skin="minimal"] .unblock-btn { border-color: var(--border); color: var(--text-muted); }

/* Open Window's own textarea/link field: #120e18/#efeaf6,#cdbfe0/#33283f ->
   sunken/text-and-muted/border. Its save/copy/share/rotate buttons and
   group-header both used the same #e0b3a0 "warm highlight" that measures
   1.89:1 on white — mapped to var(--accent), matching every other action
   button/link in this skin (6.86:1 on white, 5.72:1 on --bg-raised). */
html[data-skin="minimal"] #ow-question-input,
html[data-skin="minimal"] #ow-link-url,
html[data-skin="minimal"] #link-device-code,
html[data-skin="minimal"] #link-device-input {
  background: var(--bg-sunken);
  color: var(--text);
  border-color: var(--border);
}
html[data-skin="minimal"] #ow-question-save,
html[data-skin="minimal"] #ow-link-copy,
html[data-skin="minimal"] #ow-link-share,
html[data-skin="minimal"] #ow-link-rotate,
html[data-skin="minimal"] #link-device-copy,
html[data-skin="minimal"] #link-device-redeem {
  /* border-color alone (not var(--border), see the legacy-chrome-buttons
     block below): these have `background: none`, so the border is their
     ONLY affordance — var(--border)'s 1.38:1 isn't enough for that. */
  border-color: rgba(0, 0, 0, 0.44);
  color: var(--accent);
}
html[data-skin="minimal"] .ow-question-group-header {
  border-color: var(--border);
  color: var(--accent);
}

/* Amber "warning/offline" pair — kept as amber, not flattened to neutral:
   #ff9800/#2e2316/#4a3a2e and #ffb74d/#5c4a2e -> darkened amber #8a5a00
   text on a pale amber wash (5.11-5.40:1), same value section 1 already
   uses for the CW/crisis semantic so there's one amber across the skin. */
html[data-skin="minimal"] .tab-private-offline {
  color: #8a5a00;
  background: #fdecd2;
  border-top-color: #f0d3a0;
}
html[data-skin="minimal"] #pair-banner.warning {
  background: #fdecd2;
  color: #8a5a00;
}

/* --- Sweep: same defect, not on the named list ------------------------- */

/* Own poll card: unaffected by the chat-bubble restyle below (out of scope
   here) — kept on the accent-wash token pattern used for "mine" affordances
   elsewhere in this skin (var(--text) on accent-wash: 16.59:1). */
html[data-skin="minimal"] .poll-card.mine {
  background: var(--accent-wash);
  color: var(--text);
  border-color: var(--accent-ring);
}

/* Own chat bubble — reference-aesthetic restyle (not a dark-island fix
   anymore): solid brand blue fill + white text, differentiating your own
   messages from others' the way most messenger apps do. Deliberately
   var(--accent) / #0b5f91, NOT #008fe9 (this skin's header blue; the send
   button uses a third, in-between #0072bd for the same reason this bubble
   avoids #008fe9) or the reference screenshot's own blue (~#0d8bf2) —
   white text on
   either of those measures ~3.44-3.50:1, below the 4.5:1 this normal-weight
   ~14px body text needs (no large-text exemption applies). #0b5f91 is the
   same blue family, already this skin's accent token, and clears white at
   6.86:1. Do not "brighten" this back to #008fe9/#0d8bf2 — that reintroduces
   the contrast failure the reference site itself has. */
html[data-skin="minimal"] .msg.mine .msg-inner {
  background: var(--accent);
  color: #ffffff;
}

/* Others' chat bubble — reference aesthetic: white card fill (vs. this
   skin's default --bg-raised chip color), a thin hairline instead of
   relying on --shadow-soft alone, and the reference's generous pill
   rounding. --shadow-soft is kept, not dropped — already tuned very
   subtle for this skin (1px/2px @ 6% black, see its :root override above)
   so it reads as a gentle lift, not visual noise, alongside the new border. */
html[data-skin="minimal"] .msg-inner {
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}

/* Keepsake modal's "Share" button: dark navy fill + pale-blue text. */
html[data-skin="minimal"] #keepsake-share {
  background: var(--accent);
  color: var(--bg-deep);
}
html[data-skin="minimal"] #keepsake-share:hover { background: var(--accent-hover); }

/* The Moment's "share" action button: dark purple fill + gold text in every
   other skin (#2a2340 / #f3e1b8) — the background override below already
   swaps the fill for this skin's light --bg-raised, but never re-paired the
   text color, leaving the dark skins' gold (#f3e1b8) on a near-white chip:
   1.05:1 measured. var(--text) clears 4.5:1 against --bg-raised here. */
html[data-skin="minimal"] #moment-share { background: var(--bg-raised); color: var(--text); }
html[data-skin="minimal"] #moment-share:hover { background: var(--accent-wash); }

/* Empathy Filter strip + its buttons (chat, room-composer and post-composer
   variants) and the Stream/Whisper invite banners' decline buttons: all the
   same dark-amber/dark-red fill + light text pattern. */
html[data-skin="minimal"] #empathy-strip,
html[data-skin="minimal"] .room-composer-empathy,
html[data-skin="minimal"] .post-composer-empathy {
  background: #fdecd2;
  color: #8a5a00;
}
html[data-skin="minimal"] #empathy-strip button {
  background: var(--bg-raised);
  color: #8a5a00;
  border-color: var(--border);
}
html[data-skin="minimal"] #empathy-strip button:hover { background: var(--accent-wash); }
html[data-skin="minimal"] .room-composer-empathy button,
html[data-skin="minimal"] .post-composer-empathy button {
  background: var(--bg-raised);
  color: #8a5a00;
  border-color: var(--border);
}
html[data-skin="minimal"] .room-composer-empathy button:hover,
html[data-skin="minimal"] .post-composer-empathy button:hover { background: var(--accent-wash); }
html[data-skin="minimal"] #empathy-strip #empathy-send-anyway,
html[data-skin="minimal"] .room-composer-empathy .rc-empathy-send,
html[data-skin="minimal"] .post-composer-empathy .pc-empathy-send {
  background: #fbdcd8;
  border-color: #e8b7b0;
  color: #b3261e;
}
html[data-skin="minimal"] #empathy-strip #empathy-send-anyway:hover,
html[data-skin="minimal"] .room-composer-empathy .rc-empathy-send:hover,
html[data-skin="minimal"] .post-composer-empathy .pc-empathy-send:hover { background: #f6c9c2; }
html[data-skin="minimal"] .stream-invite-banner .stream-decline,
html[data-skin="minimal"] .whisper-banner .whisper-decline {
  background: #fbdcd8;
  border-color: #e8b7b0;
  color: #b3261e;
}
html[data-skin="minimal"] .stream-invite-banner .stream-decline:hover,
html[data-skin="minimal"] .whisper-banner .whisper-decline:hover { background: #f6c9c2; }

/* Whisper banner + its own buttons: dark purple fill + light purple text. */
html[data-skin="minimal"] .whisper-banner {
  background: var(--bg-raised);
  color: var(--text);
  border-bottom-color: var(--border);
}
html[data-skin="minimal"] .whisper-banner button {
  background: var(--bg-raised);
  color: var(--text);
  border-color: var(--border);
}
html[data-skin="minimal"] .whisper-banner button:hover { background: var(--accent-wash); }

/* --- Sweep round 2: found via an automated selector-coverage pass -------
   (every rule above section 6 with a light literal text color, cross-
   checked against what section 6 already covers) rather than by eye. */

/* Confession composer's Void-tagging header: own dark island, same shape
   as Reach Out/Block above — #1a1530/#b9b2dd/#4a3f7a/#d4cdef/#8b87b0. */
html[data-skin="minimal"] #confession-void-header {
  background: var(--bg-surface);
  color: var(--text-muted);
  border-color: var(--border);
}
html[data-skin="minimal"] #confession-void-prompt,
html[data-skin="minimal"] #confession-void-cancel:hover { color: var(--accent); }
html[data-skin="minimal"] #confession-void-cancel { color: var(--text-muted); }

/* Random Pair gate overlay card: dark-surface-tuned gold text on what's now
   a light card — #f0e6d2/#f3e1b8 -> text/accent tokens. */
html[data-skin="minimal"] .pair-gate-card { color: var(--text); }
html[data-skin="minimal"] .pair-gate-card h3 { color: var(--accent); }

/* Own reaction pill: white text on var(--bg-raised) grey nearly disappears
   (1.20:1) — swapped for var(--accent), already used for its border. */
html[data-skin="minimal"] .reaction-pill[data-mine="true"] { color: var(--accent); }

/* Nearby card's "⋯" more-button hover and its region/far distance chips:
   pale-blue/slate-blue literals tuned for a dark map backdrop, unreadable
   on white (1.15-2.21:1). */
html[data-skin="minimal"] .nb-more:hover { color: var(--accent); }
html[data-skin="minimal"] .nearby-bucket-chip.b-region,
html[data-skin="minimal"] .nearby-bucket-chip.b-far { color: var(--text-muted); }

/* Nearby's flag-for-removal icon hover state: pale orange, 2.05:1 on white. */
html[data-skin="minimal"] .nb-flag:hover:not(:disabled),
html[data-skin="minimal"] .nb-post-flag:hover:not(:disabled),
html[data-skin="minimal"] .nb-reply-flag:hover:not(:disabled) { color: #8a4b00; }

/* Kept-plane corner icon + The Moment's appreciation-glow phrase: pale gold
   text, tuned for a dark corner/backdrop — darkened to the same value
   section 1 uses for the "keepsake gold" family (6.23:1 on white). */
html[data-skin="minimal"] .kept-icon,
html[data-skin="minimal"] #light-glow-phrase { color: #7a5c1a; }

/* Worry Stones' floating "breathe in/out" cue: pale amber text with a
   heavy dark text-shadow tuned to sit over a dark chat backdrop — the
   shadow itself would smear on white, so it's dropped along with the
   color darkening. */
html[data-skin="minimal"] .stone-breathe-text {
  color: #8a5a00;
  text-shadow: none;
}

/* --- Legacy chrome buttons: fills/borders, not just text -----------------
   A contrast audit found ~31 low-alpha warm-cream rgba(243,225,184,0.0x-0.3x)
   values used as fills/borders on pre-skin-system chrome. An earlier pass
   (sections 1-6 above) fixed their TEXT for this skin but left the fill/
   border alone, so on white the whole control vanishes and reads as bare
   text — for a button whose only affordance is its outline, that outline
   is required to identify the component (WCAG 1.4.11), unlike the app's
   decorative hairlines (var(--border) elsewhere), which stay untouched.
   rgba(0,0,0,0.44) is the shared boundary value below: measured 3.13-3.23:1
   against every background these buttons actually sit on (popover glass,
   bg-raised, bg-surface/white, bg-deep) — clears the 3:1 non-text/UI bar,
   with --bg-raised the tightest margin at 3.13:1. Dark skins' cream values
   are untouched. */

/* .sit-btn: shared by every "•••" menu row (Sit With Me, Match Me, Throw,
   plane-mute, Ambient, My Link, Skin, Open Window) — one shared class, one
   fix. #header-more-btn also carries this class but is flattened to a
   transparent/borderless pill on the blue header by its own ID-scoped rule
   below (see "MINIMAL SKIN — blue header"), which wins on specificity, so
   it's unaffected here. Before: border-color var(--border) = rgba(0,0,0,
   0.14), 1.38:1 on the header-more-menu popover's --glass. After: 3.22:1. */
html[data-skin="minimal"] .sit-btn { border-color: rgba(0, 0, 0, 0.44); }

/* My Link's copy button: the reported case. Before: fill rgba(243,225,184,
   0.1) + border rgba(243,225,184,0.28), border measured 1.05:1 on --glass.
   After: dark-alpha fill/border pair, border 3.22:1. */
html[data-skin="minimal"] #my-link-copy {
  background: rgba(0, 0, 0, 0.04);
  border-color: rgba(0, 0, 0, 0.44);
}
/* My Link's rotate button: border-only (background stays transparent, its
   text is already fixed above). Before: 1.38:1 on --glass. After: 3.22:1. */
html[data-skin="minimal"] #my-link-rotate { border-color: rgba(0, 0, 0, 0.44); }

/* Random Pair "keep"/"throw again" and Keepsake modal's action buttons:
   background is already a per-skin token (var(--bg-raised)), just the
   border was still var(--border). Before: 1.37:1 on --bg-surface/--glass.
   After: 3.13:1 (.plane-keep/.plane-rethrow, on --bg-surface's bg-raised
   fill) / 3.22:1 (#keepsake-actions button, on --glass). */
html[data-skin="minimal"] .plane-keep,
html[data-skin="minimal"] .plane-rethrow,
html[data-skin="minimal"] #keepsake-actions button {
  border-color: rgba(0, 0, 0, 0.44);
}

/* Skin picker's swatch dot: 16px circle previewing each skin's accent.
   Before: border var(--border) = 1.38:1 on --glass — minimal's own swatch
   is solid white, so the dot had no visible edge at all. After: 3.22:1;
   also crisps the lighter swatches (teal #6fd6c8, sage #7fc8a0). */
html[data-skin="minimal"] .skin-swatch { border-color: rgba(0, 0, 0, 0.44); }

/* Kept-plane corner icon: floats directly on the page (--bg-deep), no
   container to lean on. Before: fill rgba(243,225,184,0.1) + border
   rgba(243,225,184,0.3), border ~1.4:1. After: 3.18:1. */
html[data-skin="minimal"] .kept-icon {
  background: rgba(0, 0, 0, 0.04);
  border-color: rgba(0, 0, 0, 0.44);
}

/* The Stage's emoji-reaction buttons: sit on .stage-reactions, flattened to
   var(--bg-surface)/white above. Before: fill rgba(243,225,184,0.06) +
   border rgba(243,225,184,0.22), border ~1.7:1. After: 3.23:1. */
html[data-skin="minimal"] .stage-react-btn {
  background: rgba(0, 0, 0, 0.04);
  border-color: rgba(0, 0, 0, 0.44);
}

/* Mini-game "tell" submit button. Before: fill rgba(243,225,184,0.16) +
   border rgba(243,225,184,0.5), border ~2.4:1 (the highest starting alpha
   of the family, still short of 3:1). After: 3.23:1. */
html[data-skin="minimal"] .minigame-told-submit {
  background: rgba(0, 0, 0, 0.05);
  border-color: rgba(0, 0, 0, 0.44);
}

/* =============================================================================
   MINIMAL SKIN — blue header (Task 2)
   =============================================================================
   header's glass fill lives on header::before (see the big comment above
   that rule for why: backdrop-filter on header itself would trap this
   skin's position:fixed popovers in header's paint layer). That rule reads
   var(--glass-chrome), which also backs #tab-strip and #chat-input — so it
   is NOT touched here, or all three would turn blue. header::before's own
   background is overridden directly instead, scoped to this skin, leaving
   backdrop-filter itself untouched. */
html[data-skin="minimal"] header::before {
  background: #008fe9;
}

/* Foregrounds are near-black, not merely "darkened" — do NOT simplify these
   back to bare var(--accent)/var(--text)/white. #008fe9 sits in an awkward
   mid-luminance band (relative luminance 0.2553): every plausible dark AND
   light foreground was measured against it and both directions come up
   short except one. var(--accent) #08496f is 2.78:1; var(--text-muted)-ish
   dark greys land around 3.3:1; white #ffffff is 3.44:1 — all fail 4.5:1.
   Only near-black #16181d clears it, at 5.16:1. The global tokens themselves
   stay correct for the rest of the app, which still sits on white — only
   these header-scoped overrides change. */
html[data-skin="minimal"] header h1,
html[data-skin="minimal"] #my-name {
  color: #16181d;   /* 5.16:1 on #008fe9 */
}
html[data-skin="minimal"] .header-info,
html[data-skin="minimal"] .header-controls .icon {
  color: #16181d;   /* 5.16:1 on #008fe9 */
}

/* anon-badge: light-outline pitch pill, same shape as the dark skins, but
   var(--accent) here (#0b5f91) sits too close in hue/luminance to this
   skin's own #008fe9 header to read as an outline. Reuses #16181d, the same
   near-black already audited for h1/.header-info/.icon directly on this
   header (5.16:1) just above, instead of introducing a new pairing. */
html[data-skin="minimal"] .anon-badge {
  border-color: #16181d;
  color: #16181d;
}

/* #header-more-btn / .mood-header-btn: flat at rest, not a white pill.
   Ink directly on #008fe9 is 5.16:1 (clears both the 4.5:1 text bar and
   the 3:1 non-text bar), so no fill is needed to stay legible — a white
   pill just reads as an odd floating chip on the blue. Hover adds a
   translucent-white wash instead of darkening: it composites to
   rgb(64, 171, 238), keeps the ink at 7.00:1, and is a visible 1.356:1 step
   up from the resting blue. */
html[data-skin="minimal"] #header-more-btn {
  background: transparent;
  /* border-color, not `border: none` — .sit-btn's base border is 1px solid
     and content-box; dropping the whole property would shrink the button
     by 2px and nudge every control to its right. */
  border-color: transparent;
}
html[data-skin="minimal"] #header-more-btn:hover {
  background: rgba(255, 255, 255, 0.25);
}

html[data-skin="minimal"] .mood-header-btn {
  background: transparent;
  border-color: transparent;
}
html[data-skin="minimal"] .mood-header-btn:hover {
  background: rgba(255, 255, 255, 0.25);
}

/* header-more-btn's own unread dot (id, not the shared .openwindow-dot
   class — #openwindow-btn's copy elsewhere in the app sits on an ordinary
   light surface and stays var(--accent)). It used to sit on
   #header-more-btn's opaque white fill, so var(--accent) at 6.86:1 was
   fine; now that fill is transparent at rest, the dot sits directly on
   #008fe9 and var(--accent) drops to 1.995:1 — same failure class as the
   header text above, same fix. */
html[data-skin="minimal"] #header-more-dot {
  background: #16181d;
}

/* Toggle labels (emotive-typing, location-hidden): icon-only controls whose
   default color is inherited var(--text) — already 5.16:1, fine — but the
   emotive toggle's unchecked state drops to 0.4 opacity, which thins
   #16181d down to ~1.96:1 against the blue. Raised to 0.7 (3.37:1, clears
   the 3:1 non-text/UI-component bar for this icon) so the dimmed "off"
   state stays legible; checked state (opacity 1) is untouched. */
html[data-skin="minimal"] .emotive-toggle:not(:has(input:checked)) { opacity: 0.7; }

/* online-count text is part of .header-info, already covered above. */

/* Send buttons (#send-button, .room-composer-send, .post-composer-send):
   a slightly deeper blue than the header's #008fe9, specifically so the
   label can be WHITE like every other messenger's send button. #008fe9
   only reaches 3.44:1 against white (small bold text, no large-text
   exemption), which forced a near-black label here originally — legible at
   5.16:1, but it read like a disabled or unstyled button. #0072bd carries
   white at 5.07:1 and, as a bonus, sits in the same blue family as the own-
   message bubble (var(--accent) #0b5f91) rather than looking conspicuously
   brighter than it. Hover therefore goes DARKER now (the opposite of the
   old dark-label version): #005f9e keeps white at 6.71:1 and is a visible
   ~1.32 luminance-ratio step. The header deliberately keeps #008fe9 — a
   near-black wordmark reads fine on a colour band, unlike on a button.
   #send-button.throttled (slowmode countdown) and
   .post-composer-send:disabled are untouched — separate states, not the
   resting colour. */
html[data-skin="minimal"] #send-button,
html[data-skin="minimal"] .room-composer-send,
html[data-skin="minimal"] .post-composer-send {
  background: #0072bd;
  color: #ffffff;
}
html[data-skin="minimal"] #send-button:hover,
html[data-skin="minimal"] .room-composer-send:hover,
html[data-skin="minimal"] .post-composer-send:hover {
  background: #005f9e;
}

/* Reference aesthetic's pill-shaped composer. Focus rings (border-color +
   box-shadow, set elsewhere) are untouched, so they still show on the
   now-rounder shape. */
html[data-skin="minimal"] #message-input,
html[data-skin="minimal"] #send-button,
html[data-skin="minimal"] .room-composer-input,
html[data-skin="minimal"] .room-composer-send,
html[data-skin="minimal"] .post-composer-input,
html[data-skin="minimal"] .post-composer-send {
  border-radius: var(--radius-pill);
}

/* =============================================================================
   RICH MEDIA ATTACHMENTS (public/js/media-capture.js, media-attachment.js).
   See specs/2026-08-23-rich-media-attachments-design.md §4.
   ============================================================================= */
.media-capture { display: inline-flex; align-items: center; gap: 2px; flex: 0 0 auto; }
/* Same resting/hover treatment as .room-composer-emoji rather than a new one:
   these buttons sit shoulder to shoulder with it in the same bar. */
.media-capture-pick, .media-capture-rec {
  height: 36px;
  box-sizing: border-box;
  background: none;
  color: var(--text-muted);
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 1rem;
  padding: 0 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color var(--ease), background var(--ease);
}
.media-capture-pick:hover, .media-capture-rec:hover { color: var(--text); background: var(--accent-wash); }
.media-capture-rec.recording { color: #ff5252; }
.media-capture-rec:disabled { opacity: 0.35; cursor: default; }
/* Lucide icons render at 20px via their own width/height attrs; this just stops
   the inline SVG from sitting on the text baseline and adding descender space. */
.media-capture-pick svg, .media-capture-rec svg { display: block; }

/* ---- viewfinder ----
   The camera opens centred rather than as a thumbnail in the composer, so you
   can aim before committing. A native <dialog> shown via showModal() (see
   media-capture.js's buildViewfinder), which is what actually keeps it above
   a composer that's ALSO an open modal <dialog> — z-index can't win that
   fight since both are promoted to the browser's top layer, which stacks by
   show-order, not z-index. border/padding zeroed because dialog's UA
   stylesheet otherwise adds both (a plain div never had them). */
/* [open] is load-bearing: an unscoped `.media-viewfinder { display: flex }`
   is author-origin CSS, which beats the UA stylesheet's own
   `dialog:not([open]) { display: none }` regardless of specificity (origin
   outranks specificity in the cascade) — so closeViewfinder()'s
   root.close() correctly flipped `.open` false but the box stayed painted
   full-viewport forever, unclosable, because nothing was left to hide it.
   Scoping this rule to [open] leaves the UA's own closed-state rule as the
   only one that applies once `.close()` runs. */
.media-viewfinder[open] {
  position: fixed;
  inset: 0;
  border: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  background: rgba(0, 0, 0, 0.82);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
.media-viewfinder-stage { position: relative; line-height: 0; }
.media-viewfinder-video {
  display: block;
  width: min(88vw, 560px);
  max-height: 62vh;
  object-fit: cover;
  border-radius: var(--radius-md, 12px);
  background: #000;
  /* Mirrored only for a front-facing camera (openViewfinder decides, from the
     track's facingMode) — a self-view that isn't mirrored feels like a stranger,
     while a rear camera mirrored would be plainly wrong. takePhoto flips the
     canvas to match, so the photo is what was aimed at. */
}
.media-viewfinder-video.mirrored { transform: scaleX(-1); }
/* A live portrait camera keeps its intrinsic aspect ratio when max-height wins,
   which can shrink it narrower than the stage on a phone and leave black bars
   around the preview. Give the phone viewfinder an explicit frame and crop the
   feed to cover it. Desktop keeps the natural-ratio preview above. */
@media (max-width: 600px) {
  .media-viewfinder-stage {
    width: 88vw;
    height: 62vh;
  }
  .media-viewfinder-video {
    width: 100%;
    height: 100%;
    max-height: none;
  }
}
.media-viewfinder-close {
  position: absolute;
  top: 8px; right: 8px;
  height: 36px; width: 36px;
  display: inline-flex; align-items: center; justify-content: center;
  border: none; border-radius: 50%;
  background: rgba(0, 0, 0, 0.5);
  color: #fff;
  cursor: pointer;
}
.media-viewfinder-close:hover { background: rgba(0, 0, 0, 0.75); }
.media-viewfinder-switch {
  position: absolute;
  right: 8px; bottom: 8px;
  height: 40px; width: 40px;
  display: inline-flex; align-items: center; justify-content: center;
  border: none; border-radius: 50%;
  background: rgba(0, 0, 0, 0.5);
  color: #fff;
  cursor: pointer;
}
.media-viewfinder-switch:hover { background: rgba(0, 0, 0, 0.75); }
.media-viewfinder-switch:disabled { opacity: 0.45; cursor: default; }
/* The shutter: a ring with a filled core, the one control shape a camera UI is
   always expected to have. touch-action/user-select are load-bearing, not
   polish — it is press-and-hold, so without them holding it on a phone starts a
   scroll or pops the selection callout instead of recording. */
.media-viewfinder-shutter {
  height: 68px; width: 68px;
  border-radius: 50%;
  border: 4px solid rgba(255, 255, 255, 0.9);
  background: #fff;
  box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.35);
  cursor: pointer;
  touch-action: none;
  -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
  transition: transform var(--ease), background var(--ease);
}
.media-viewfinder-shutter:active { transform: scale(0.94); }
/* Recording: the shutter itself turns red, so the thing you are holding is the
   thing telling you it is live. */
.media-viewfinder.recording .media-viewfinder-shutter {
  background: #ff5252;
  border-color: #ff8a80;
}
.media-viewfinder-hint {
  color: rgba(255, 255, 255, 0.75);
  font-size: 0.85rem;
  text-align: center;
}
@media (prefers-reduced-motion: reduce) {
  .media-viewfinder-shutter { transition: none; }
  .media-viewfinder-shutter:active { transform: none; }
}

/* The staged-attachment preview, built by MediaCapture.stagedPreview and dropped
   into each composer's chip label. inline-flex, not flex: all five chips put the
   label in a <span>, so a block-level child would mean either block-in-inline
   splitting or editing five separate label rules to make them flex containers.
   inline-flex sits in an inline context natively and needed nothing changed.

   The chips themselves already set align-items:center, which is what keeps the
   × button centred against a row that is suddenly ~104px tall instead of one
   line of text. */
.media-staged {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  vertical-align: middle;
  margin: 2px 0;   /* the chips' own padding bottoms out at 0, so the thumb would otherwise touch the input */
}
/* A fixed height with a free width: object-fit:cover crops rather than letterboxes,
   so a portrait phone photo and a landscape screenshot make the same-height row
   and the chip doesn't resize as you swap attachments. Rejected max-width with
   height:auto (what .msg-attachment uses) — that is right for a bubble, where the
   image IS the content, but here it made a tall photo shove the whole composer
   down the screen. */
.media-staged-thumb {
  height: 104px;
  max-width: 160px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  background: #000;
  display: block;
}
/* Nothing to look at for a voice note, so the player is the preview. Capped
   because the native audio control stretches to fill whatever it is given. */
.media-staged-audio { max-width: 220px; height: 32px; }
.media-staged-size { white-space: nowrap; }

.room-composer-media-chip {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px 0;
  font-size: 0.82rem;
  color: var(--text-muted);
}
.room-composer-media-chip[hidden] { display: none; }
.room-composer-media-chip button { background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 1rem; line-height: 1; }

/* PostComposer's own chip — same shape, its own class because the two composers
   deliberately share no CSS beyond the .media-capture buttons themselves. */
.post-composer-media-chip {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 2px 6px;
  font-size: 0.82rem;
  color: var(--text-muted);
}
.post-composer-media-chip[hidden] { display: none; }
.post-composer-media-chip button { background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 1rem; line-height: 1; }

/* Inline playback inside a bubble. max-width keeps a 4000px photo from
   stretching the bubble past the message column; height:auto keeps its ratio. */
.msg-attachment { margin: 4px 0; }
.msg-attachment img, .msg-attachment video { max-width: 100%; height: auto; border-radius: var(--radius-sm); display: block; }
.msg-attachment audio { max-width: 100%; }
.msg-attachment-thumb { padding: 0; border: none; background: none; cursor: zoom-in; display: block; max-width: 260px; }

/* Singleton lightbox, appended to <body> once on first open. position:fixed +
   its own stacking context so it paints over the map and the dock. */
.media-lightbox {
  position: fixed;
  inset: 0;
  z-index: 4000;
  background: rgba(0, 0, 0, 0.86);
  display: flex;
  align-items: center;
  justify-content: center;
}
.media-lightbox[hidden] { display: none; }
.media-lightbox-img { max-width: 92vw; max-height: 92vh; object-fit: contain; }
.media-lightbox-close {
  position: absolute;
  top: 12px;
  right: 16px;
  background: none;
  border: none;
  color: #fff;
  font-size: 2rem;
  line-height: 1;
  cursor: pointer;
}
