/* ---- themes ----------------------------------------------------------------
   Every colour is light-dark(LIGHT, DARK); the chosen appearance sets
   `color-scheme` on <html> (light / dark / "light dark" = follow the OS), so a
   single set of tokens drives both modes. Themes are selected with a
   data-theme attribute on <html>. The app sets it to "classic" by default (see
   DEFAULT_THEME in ui.js); the bare :root values below double as the Lit Room
   theme and the no-JS fallback. Structural tokens (radius, fonts, shadow, pills,
   status colours) live here once; theme blocks only override what differs. Order
   matters: themes come after this block so they win on equal specificity. */
:root,
[data-theme="litroom"] {
  --radius: 18px;
  /* Message + composer text size. The desktop default is a touch over Mac
     iMessage's (we used to inherit the browser's 16px); the coarse-pointer
     block further down bumps it for touch and layers Dynamic Type on top. A
     member's picked size (theme sheet slider, lchat_text_size) is applied by
     shell.js as an inline override on <html> — desktop only; touch follows
     the system. */
  --msg-font-size: 15px;
  --font-display: ui-rounded, "SF Pro Rounded", "Avenir Next", -apple-system, system-ui, sans-serif;
  /* depth + status colours are theme-agnostic */
  --shadow: light-dark(rgba(20, 30, 80, 0.10), rgba(0, 0, 0, 0.55));
  --danger: light-dark(#c2410c, #f0936b);
  --warn: light-dark(#b4530a, #e29457);
  --pill-bg: light-dark(#23252e, #eef0f6);
  --pill-ink: light-dark(#ffffff, #1c2333);
  /* selected-chat row background; defaults to the hover grey, themes may override */
  --row-active: var(--bubble-them);
  /* Lifted-panel fill shared by text inputs (chat composer, App Studio, …) — see
     the .eleven-input class. Derived from --panel via color-mix, so it adapts to
     every theme; light lifts a touch above the panel, dark blends into the band. */
  --input-bg: light-dark(color-mix(in srgb, var(--panel) 50%, white 50%), transparent);

  /* Lit Room — warm dusk, honey glow */
  --bg: light-dark(#efe3d6, #1b1418);
  --panel: light-dark(#fff8f0, #241a1e);
  --sidebar: light-dark(#fbf1e6, #1f161a);
  --ink: light-dark(#2a211b, #f4e9dc);
  --muted: light-dark(#736657, #b09a89);
  --border: light-dark(#ece2d4, rgba(255, 255, 255, 0.10));
  --accent: light-dark(#f0a93c, #ffb23e);
  --accent-ink: light-dark(#3a2412, #2a1808);
  --link: light-dark(#956925, #ffb23e);
  --bubble-me: light-dark(#ffb23e, #ffb23e);
  --bubble-me-ink: light-dark(#3a2412, #2a1808);
  --bubble-them: light-dark(#f1ece4, #2f2329);
}

/* Classic — the original lchat blue (now with a dark mode) */
[data-theme="classic"] {
  --font-display: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --bg: light-dark(#f6f7fb, #15171c);
  --panel: light-dark(#ffffff, #1d2026);
  --sidebar: light-dark(#f8f9fc, #1a1d22);
  --ink: light-dark(#1c2333, #e7e9f0);
  --muted: light-dark(#5a6170, #9aa1b3);
  --border: light-dark(#e7e9f0, rgba(255, 255, 255, 0.10));
  --accent: light-dark(#5666f0, #7d8bff);
  --accent-ink: light-dark(#ffffff, #0a0e2a);
  --link: light-dark(#5666f0, #7d8bff);
  --bubble-me: light-dark(#5666f0, #5666f0);
  --bubble-me-ink: light-dark(#ffffff, #ffffff);
  --bubble-them: light-dark(#eef0f6, #2a2e38);
  --row-active: light-dark(#d9dfec, #2a2e38);
}

/* Lagoon — cool, fresh; a coral accent that pops */
[data-theme="lagoon"] {
  --bg: light-dark(#d7ecea, #06201f);
  --panel: light-dark(#f3fbfa, #0c2a29);
  --sidebar: light-dark(#e7f6f4, #082523);
  --ink: light-dark(#123a3a, #d8efec);
  --muted: light-dark(#4c7271, #6fa6a3);
  --border: light-dark(#d2e8e6, rgba(255, 255, 255, 0.10));
  --accent: light-dark(#b85840, #ff8a6a);
  --accent-ink: light-dark(#ffffff, #2a0f08);
  --link: light-dark(#b2553e, #ff8a6a);
  --bubble-me: light-dark(#0e827e, #16b3ad);
  --bubble-me-ink: light-dark(#ffffff, #042220);
  --bubble-them: light-dark(#ffffff, #123634);
}

/* Sunny — bright-side daytime */
[data-theme="sunny"] {
  --bg: light-dark(#ffeecb, #1c1710);
  --panel: light-dark(#fffaf0, #241d12);
  --sidebar: light-dark(#fff4df, #1f190f);
  --ink: light-dark(#3a2f1c, #f6ecd6);
  --muted: light-dark(#796d57, #b6a079);
  --border: light-dark(#f0e6d0, rgba(255, 255, 255, 0.10));
  --accent: light-dark(#ffc62e, #ffc62e);
  --accent-ink: light-dark(#3a2f1c, #2a2106);
  --link: light-dark(#8a6b19, #ffc62e);
  --bubble-me: light-dark(#2c7ab0, #3aa0e8);
  --bubble-me-ink: light-dark(#ffffff, #04243a);
  --bubble-them: light-dark(#ffffff, #2e2616);
}

/* Linen — quiet, grown-up; warmth through restraint */
[data-theme="linen"] {
  --font-display: "Avenir Next", "Helvetica Neue", -apple-system, system-ui, sans-serif;
  --bg: light-dark(#e7e1d6, #181714);
  --panel: light-dark(#f5f1ea, #211f1b);
  --sidebar: light-dark(#ece6dc, #1c1b17);
  --ink: light-dark(#33302a, #ece7dd);
  --muted: light-dark(#69655c, #9c958a);
  --border: light-dark(#e2dccf, rgba(255, 255, 255, 0.10));
  --accent: light-dark(#647686, #93a8ba);
  --accent-ink: light-dark(#ffffff, #0c1217);
  --link: light-dark(#5c6e7c, #93a8ba);
  --bubble-me: light-dark(#647686, #7d94a8);
  --bubble-me-ink: light-dark(#ffffff, #0c1217);
  --bubble-them: light-dark(#fbf9f4, #2a2823);
}

/* Plum — soft grape; playful but not childish, light or deep-aubergine dark */
[data-theme="plum"] {
  --bg: light-dark(#ece2f0, #1a1320);
  --panel: light-dark(#fdf8fd, #221829);
  --sidebar: light-dark(#f4ecf6, #1e1525);
  --ink: light-dark(#2f2436, #f0e6f4);
  --muted: light-dark(#6f6578, #ab9bb5);
  --border: light-dark(#e7dced, rgba(255, 255, 255, 0.10));
  --accent: light-dark(#9456c9, #b57ae8);
  --accent-ink: light-dark(#ffffff, #20102e);
  --link: light-dark(#9456c9, #b57ae8);
  --bubble-me: light-dark(#9456c9, #b57ae8);
  --bubble-me-ink: light-dark(#ffffff, #20102e);
  --bubble-them: light-dark(#f1e8f5, #2e2238);
}

/* Default appearance: follow the OS. The app overrides this on <html> per the
   saved preference (light / dark / system). */
html { color-scheme: light dark; }

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--ink);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
}
/* The app shell must fill the viewport so the flex height chain (.tt → .thread-view
   → .feed flex:1) resolves and the composer pins to the bottom. Without an explicit
   height here, .tt's height:100% resolves against an auto-height parent, the layout
   collapses to content height, and the composer floats with blank space below it. */
#app { height: 100%; }

/* Full-screen loading cover (see index.html) — matches the pre-painted theme bg,
   fades out once the app's first real paint is ready. */
.app-loading {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 20px;
  background: var(--bg);
  transition: opacity 0.25s ease;
}
.app-loading.hidden { opacity: 0; pointer-events: none; }
/* The cover's EXIT class (hideLoading): opacity-only, unlike the global
   `.hidden` utility whose display:none !important would snap the cover off
   with no transition. The boot scene's exit is two quick fades — the scene
   bows to the bare cover (bootscene.js), then this fades the cover to the
   chat (.scene-out tunes the second fade's length). */
.app-loading.cover-out { opacity: 0; pointer-events: none; }
.app-loading.scene-out { transition-duration: 0.35s; }
/* The join page's "Plugging in…" flash between claiming and the app. */
.plugging-in {
  min-height: 22px;
  color: var(--muted);
  font-size: 15px;
  text-align: center;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 0.3s ease, transform 0.3s ease;
}
.plugging-in.show { opacity: 1; transform: none; }
/* The boot scene (bootscene.js): its elements start invisible and are posed
   per-frame from JS (ease-in at start, the bow at the end) — so the embed and
   signed-out paths, which never start the scene, only ever show the plain
   theme bg. (Scoped to the scene's own classes: join.html shares .app-loading
   for its plain spinner.) The rules here are layout + type only. */
.boot-canvas, .boot-brand, .boot-spin, .boot-quote, .boot-goesto { opacity: 0; }
.boot-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
/* Whose space this is — the same photo + name lockup as the join card. */
.boot-brand {
  position: absolute;
  left: 0;
  right: 0;
  top: 18%;
  text-align: center;
  pointer-events: none;
}
.boot-avatar {
  position: relative;
  width: 88px;
  height: 88px;
  margin: 0 auto 16px;
  border-radius: 22px;
  overflow: hidden;
  background: linear-gradient(160deg, #6b6cf0, #4a48c8);
  box-shadow: 0 10px 32px rgba(0, 0, 0, 0.3);
}
.boot-avatar-letter {
  display: block;
  color: #fff;
  font-size: 40px;
  font-weight: 600;
  line-height: 88px;
}
.boot-avatar img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.boot-name {
  font-size: 17px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink);
}
/* A quiet spinner dead centre — the app's own, at a zen 2.4s/rev. */
.boot-spin {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 26px;
  height: 26px;
  margin: -13px 0 0 -13px;
  border-radius: 50%;
  border: 3px solid color-mix(in srgb, var(--ink) 14%, transparent);
  border-top-color: var(--accent);
  animation: spin 2.4s linear infinite;
  transition: opacity 0.3s ease;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .boot-spin { animation: none; }
}
/* The quotes: what a member actually reads while the green drifts. A quiet
   serif, so they read as words from somewhere else, not UI. */
.boot-quote {
  position: absolute;
  left: 0;
  right: 0;
  top: 66%;
  padding: 0 28px;
  text-align: center;
  pointer-events: none;
}
.boot-quote-text {
  font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
  font-style: italic;
  font-size: 21px;
  line-height: 1.55;
  color: var(--ink);
  max-width: 38ch;
  margin: 0 auto 12px;
  text-wrap: balance;
}
.boot-quote-attr {
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
}
/* The one clickable thing on the cover — where all this came from. */
.boot-goesto {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 16px;
  text-align: center;
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--muted);
  text-decoration: none;
}
.boot-goesto b { font-weight: 600; color: var(--accent); }
.boot-goesto:hover { color: var(--ink); }

/* The join page's card yields entirely to the full-screen loader while a claim
   is in flight (see showLoader/showCard in join.js). */
.card.hidden { display: none; }
.spinner {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 3px solid color-mix(in srgb, var(--ink) 15%, transparent);
  border-top-color: var(--accent);
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; opacity: 0.5; }
}

button {
  font: inherit;
  cursor: var(--ui-cursor, pointer);
  border: none;
  border-radius: 999px;
  padding: 10px 18px;
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
  white-space: nowrap;
  line-height: 1.2;
}
button.secondary {
  background: var(--bubble-them);
  color: var(--ink);
}
button:disabled { opacity: 0.5; cursor: default; }

input, textarea {
  font: inherit;
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px 14px;
  background: var(--panel);
  color: var(--ink);
  width: 100%;
  caret-color: var(--accent); /* the typing cursor picks up the accent (à la iMessage) */
}
input:focus, textarea:focus { outline: 2px solid var(--accent); border-color: transparent; }
/* the message box doesn't get the bright focus ring (chat-app convention) — the
   coloured caret is the cue. */
.composer .composer-input:focus { outline: none; border-color: var(--border); }

/* ---- centered card layout (join / fallback) ---- */
.center {
  min-height: 100%;
  display: grid;
  place-items: center;
  padding: 24px;
}
.card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 24px;
  padding: 28px;
  width: 100%;
  max-width: 420px;
  box-shadow: 0 12px 40px var(--shadow);
}
.card h1 { margin: 0 0 6px; font-size: 24px; font-family: var(--font-display); }
.card p { margin: 0 0 18px; color: var(--muted); }

/* Transient join states ("Setting up…", "Signing you in…") — a spinner + message
   centered in the card, instead of a small line stuck top-left. */
.card.status {
  min-height: 180px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}
.card.status .status-msg { margin: 0; font-size: 16px; color: var(--ink); }
.status-spinner {
  width: 30px; height: 30px; border-radius: 50%;
  border: 3px solid var(--border); border-top-color: var(--accent);
  animation: spin 0.8s linear infinite; margin-bottom: 14px; /* @keyframes spin defined above */
}
.card .row { display: flex; gap: 10px; margin-top: 14px; }

/* Landing branding: the server photo + name — plaintext by design (CLAUDE.md
   branding exception), so a signed-out visitor sees whose door they're at.
   The photo stays hidden until /api/group confirms one. */
.join-brand { display: flex; flex-direction: column; align-items: center; gap: 10px; margin: 6px 0 22px; }
.join-brand-photo { width: 72px; height: 72px; border-radius: 18px; object-fit: cover; }
.join-brand-name { font-weight: 650; font-size: 18px; font-family: var(--font-display); }
/* The single-CTA landing: branding, ONE Sign in button, and the invite-paste
   form folded behind a quiet text link ("Have an invite link?"). */
.card.landing { text-align: center; }
.card.landing .row { justify-content: center; margin-top: 16px; }
.card.landing #paste-join .row { margin-top: 14px; }
/* The folded-form reveal — quiet, link-like. */
.card .signin-link {
  background: none; border: none; padding: 0; font: inherit; font-size: 14px;
  color: var(--muted); text-decoration: underline; text-underline-offset: 2px;
  cursor: pointer;
}
.card .signin-link:hover { color: var(--link); }



/* kebab settings menu */
.icon-btn {
  background: transparent;
  color: var(--ink);
  font-size: 22px;
  line-height: 1;
  padding: 4px 10px;
  border-radius: 10px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.icon-btn svg { display: block; }
#thread-menu-btn svg { width: 19px; height: 19px; }
#back-btn svg { width: 22px; height: 22px; }
/* The Views dropdown (thread header): a labelled button, and menu rows with a
   fixed glyph column so long view titles wrap under their own text, never
   under the emoji. */
.views-btn { padding: 6px 10px; font-size: 13px; font-weight: 600; gap: 4px; }
#views-menu-label { max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.views-btn svg { width: 14px; height: 14px; }
.views-item-glyph svg { width: 15px; height: 15px; display: inline-block; vertical-align: -2px; }
.menu-item.views-item { display: flex; align-items: center; gap: 0; padding: 0; }
.menu-item.views-item:hover { background: none; }
.views-item-main {
  flex: 1 1 auto; min-width: 0; display: flex; align-items: flex-start; gap: 8px;
  background: none; border: none; color: inherit; font: inherit; text-align: left;
  padding: 10px 12px; border-radius: 10px; cursor: pointer;
}
.views-item-main:hover, .views-item-main:focus-visible { background: var(--bubble-them); }
.views-item-glyph { flex: 0 0 20px; text-align: center; }
.views-item-label { flex: 1 1 auto; min-width: 0; }
.views-item-x {
  flex: 0 0 auto; border: none; background: none; color: var(--muted); cursor: pointer;
  padding: 2px; border-radius: 6px; min-width: 24px; min-height: 24px;
}
.views-item-x svg { width: 13px; height: 13px; display: block; margin: auto; }
.views-item-x:hover, .views-item-x:focus-visible { color: var(--ink); background: var(--bubble-them); }
.icon-btn:hover { background: var(--bubble-them); }
.menu-wrap { position: relative; flex: none; }
.menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: 0 12px 40px var(--shadow);
  min-width: 230px;
  padding: 6px;
  z-index: 20;
}
.menu-item {
  display: block;
  width: 100%;
  text-align: left;
  background: transparent;
  color: var(--ink);
  border-radius: 10px;
  padding: 10px 12px;
  font-weight: 500;
  white-space: normal;
}
.menu-item:hover { background: var(--bubble-them); }
.menu-item.danger { color: var(--danger); }
.menu-item.disabled,
.menu-item.disabled:hover { color: var(--muted); cursor: default; background: transparent; }
.msg {
  display: flex;
  flex-direction: column;
  align-self: flex-start; /* shrink to the bubble width (me overrides to flex-end) */
  max-width: 78%;
  margin-top: 10px;
  position: relative; /* anchors hover affordances + the swipe-to-reply icon */
  touch-action: pan-y; /* vertical scroll stays native; we handle horizontal swipes */
}
/* continuation of a same-sender, same-minute run — tighter gap (see refreshGrouping) */
.msg.cont { margin-top: 2px; }
/* swipe-to-reply (touch): reply arrow that fades in just left of the bubble */
.swipe-reply-icon {
  position: absolute;
  top: 50%;
  left: 0;
  transform: translate(-130%, -50%);
  color: var(--link);
  opacity: 0;
  pointer-events: none;
  display: inline-flex;
}
.swipe-reply-icon svg { width: 20px; height: 20px; }
.msg .name { font-size: 12px; color: var(--muted); margin: 0 6px 3px; }
.msg .bubble {
  padding: 9px 13px;
  border-radius: var(--radius);
  background: var(--bubble-them);
  font-size: var(--msg-font-size);
  line-height: 1.35;
  white-space: pre-wrap;
  /* break long unbroken strings (e.g. a giant URL) instead of overflowing */
  overflow-wrap: anywhere;
  min-width: 0;
}
.msg .bubble code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.92em;
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  padding: 1px 5px;
  border-radius: 5px;
}
.msg.me .bubble code { background: color-mix(in srgb, var(--bubble-me-ink) 22%, transparent); }
/* ``` fenced code block — the <code> inside inherits the font rules above but
   drops the inline chip background; long lines scroll rather than wrap */
.msg .bubble pre {
  margin: 6px 0;
  padding: 8px 10px;
  border-radius: 8px;
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  overflow-x: auto;
  white-space: pre;
  line-height: 1.4;
}
.msg .bubble pre code { background: none; padding: 0; }
.msg.me .bubble pre code { background: none; padding: 0; }
.msg.me .bubble pre { background: color-mix(in srgb, var(--bubble-me-ink) 22%, transparent); }
/* "> " blockquote — quiet left rule, slightly dimmed text */
.msg .bubble blockquote {
  margin: 6px 0;
  padding: 1px 0 1px 10px;
  border-left: 3px solid color-mix(in srgb, var(--ink) 30%, transparent);
  color: color-mix(in srgb, var(--ink) 72%, transparent);
}
.msg.me .bubble blockquote {
  border-left-color: color-mix(in srgb, var(--bubble-me-ink) 55%, transparent);
  color: color-mix(in srgb, var(--bubble-me-ink) 85%, transparent);
}
/* Markdown blocks (#531): paragraphs and lists inside bubbles. Lists render
   tight always (the parse tree erases loose/tight), so item paragraphs are
   flush; nested lists indent from their <li>. */
.msg .bubble p { margin: 6px 0; }
.msg .bubble ul, .msg .bubble ol { margin: 6px 0; padding-left: 22px; }
.msg .bubble li { margin: 2px 0; }
.msg .bubble li p, .msg .bubble li ul, .msg .bubble li ol { margin: 0; }
/* blocks flush with the bubble's own padding at the edges */
.msg .bubble p:first-child, .msg .bubble ul:first-child, .msg .bubble ol:first-child,
.msg .bubble pre:first-child, .msg .bubble blockquote:first-child { margin-top: 0; }
.msg .bubble p:last-child, .msg .bubble ul:last-child, .msg .bubble ol:last-child,
.msg .bubble pre:last-child, .msg .bubble blockquote:last-child { margin-bottom: 0; }
.msg.me { align-self: flex-end; align-items: flex-end; }
.msg.me .bubble { background: var(--bubble-me); color: var(--bubble-me-ink); border-bottom-right-radius: 6px; }
.msg.them .bubble { border-bottom-left-radius: 6px; }
.msg .bubble a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
/* a message of just a few emoji: large and bubble-less, no background or padding (see fillContent) */
.msg .bubble.jumbo-emoji,
.msg.me .bubble.jumbo-emoji { background: none; color: inherit; padding: 2px 0; font-size: 44px; line-height: 1.15; }

/* centered time separator that opens a burst (replaces per-message timestamps) */
.time-sep {
  align-self: center;
  margin: 16px 0 6px;
  font-size: 11px;
  color: var(--muted);
  text-align: center;
}
.time-sep strong { color: var(--ink); font-weight: 600; }

/* persistent "Edited" label under an edited bubble */
.edited-tag { font-size: 11px; color: var(--muted); margin: 1px 6px 0; }

/* Hover affordances live in a full-height strip beside the bubble: the hover zone
   spans the whole message height, so you can move straight from anywhere in the
   bubble to an icon (no need to find the vertical centre), and the action icons +
   the exact time sit in a row so they never overlap. Revealed on hover (desktop). */
.msg-actions {
  position: absolute;
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  gap: 2px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s;
}
.msg.them .msg-actions { left: 100%; padding-left: 4px; } /* just right of the bubble */
.msg.me .msg-actions { right: 100%; padding-right: 4px; flex-direction: row-reverse; } /* just left */
.msg-actions button {
  background: transparent;
  color: var(--muted);
  padding: 4px 6px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  line-height: 1;
}
.msg-actions button:hover { background: var(--bubble-them); color: var(--accent); }
.msg-actions button svg { width: 18px; height: 18px; }
.msg-actions .msg-time {
  font-size: 11px;
  color: var(--muted);
  white-space: nowrap;
  margin: 0 4px;
}
@media (hover: hover) {
  .msg:hover .msg-actions { opacity: 1; pointer-events: auto; }
}

/* message content (text bubble + any attachments) */
.content {
  position: relative; /* anchors the absolute hover reply icon */
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: flex-start;
  min-width: 0;
}
.msg.me .content { align-items: flex-end; }

.msg-image {
  max-width: min(260px, 100%);
  max-height: 320px;
  border-radius: var(--radius);
  display: block;
  background: var(--bubble-them);
  cursor: zoom-in;
}
/* a GIF's click toggles play/pause, so the zoom-in cursor would lie */
.msg-image.msg-gif { cursor: pointer; }

/* full-screen image viewer */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 80;
  background: transparent; /* the dark backing is its own layer (.lightbox-backdrop) */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
/* separate layer so dimming animates via opacity (GPU-composited) — no repaint */
.lightbox-backdrop {
  position: absolute;
  inset: 0;
  z-index: 0;
  background: rgba(0, 0, 0, 0.9);
  will-change: opacity;
}
.lightbox img {
  position: relative;
  z-index: 1; /* sit above the backdrop */
  max-width: 100%;
  max-height: 100%;
  border-radius: 8px;
  touch-action: none; /* we own the gesture: pinch-zoom, pan, flick-to-dismiss */
  user-select: none;
  -webkit-user-drag: none;
  will-change: transform;
}

/* the lightbox open/close is driven in JS (a FLIP fly-in/out from the thumbnail) */
@keyframes fade-in { from { opacity: 0; } }

/* a newly-sent/arrived message eases up into the feed (initial history doesn't) */
.msg.appear { animation: msg-appear 0.22s cubic-bezier(0.2, 0.8, 0.2, 1); }
@keyframes msg-appear {
  from { opacity: 0; transform: translateY(10px); }
}

@media (prefers-reduced-motion: reduce) {
  .msg.appear { animation: none; }
}

/* drag-and-drop overlay */
.drop-overlay {
  position: absolute;
  inset: 0;
  z-index: 40;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border: 3px dashed var(--accent);
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 18px;
  color: var(--link);
  pointer-events: none;
}
.file-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--bubble-them);
  color: var(--ink);
  padding: 10px 14px;
  border-radius: var(--radius);
  text-decoration: none;
  font-size: 14px;
  max-width: 260px;
  word-break: break-word;
}
.msg.me .file-chip { background: var(--bubble-me); color: var(--bubble-me-ink); }
.file-chip .file-icon { font-size: 22px; line-height: 1; flex: none; }
.file-chip .file-meta { display: flex; flex-direction: column; min-width: 0; }
.file-chip .file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-chip .file-sub { font-size: 12px; opacity: 0.7; }

/* click-to-play YouTube card (see buildYouTubeCard) — 16:9, bubble-styled */
.yt-card {
  width: min(420px, 100%);
  aspect-ratio: 16 / 9;
  border-radius: var(--radius);
  overflow: hidden;
  background: #000;
  border: 1px solid var(--border);
}
.yt-card.yt-bare { display: none; } /* dead/private video → no card, just the link */
.yt-poster {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  padding: 0;
  border-radius: 0;
  background: #000;
}
.yt-thumb { width: 100%; height: 100%; object-fit: cover; display: block; }
.yt-play {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: #fff;
}
.yt-play svg {
  width: 56px;
  height: 56px;
  padding: 10px 8px 10px 12px;
  box-sizing: border-box;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  transition: transform 0.15s ease, background 0.15s ease;
}
.yt-poster:hover .yt-play svg { background: rgba(0, 0, 0, 0.75); transform: scale(1.06); }
.yt-frame { width: 100%; height: 100%; border: 0; display: block; }

/* send-and-play video card (see buildVideoCard) — poster + play/download badge;
   the clip is fetched + decrypted only when tapped (never auto-downloaded) */
.msg-video {
  position: relative;
  display: block;
  max-width: min(260px, 100%);
  max-height: 320px;
  border-radius: var(--radius);
  overflow: hidden;
  background: #000;
  cursor: pointer;
  line-height: 0;
}
.msg-video-poster {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: var(--bubble-them);
}
.msg-video-play {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: #fff;
  pointer-events: none;
}
.msg-video-play svg {
  width: 52px;
  height: 52px;
  padding: 12px;
  box-sizing: border-box;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  transition: transform 0.15s ease, background 0.15s ease;
}
.msg-video:hover .msg-video-play svg { background: rgba(0, 0, 0, 0.75); transform: scale(1.06); }
.msg-video.downloading .msg-video-play svg { opacity: 0.5; }
.msg-video-dur {
  position: absolute;
  right: 6px;
  bottom: 6px;
  padding: 1px 6px;
  border-radius: 10px;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
/* radial download-progress ring around the play button (see showVideoRing) */
.msg-video-ring {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
}
.msg-video-ring svg { width: 60px; height: 60px; transform: rotate(-90deg); }
.msg-video-ring .ring-track { fill: none; stroke: rgba(255, 255, 255, 0.3); stroke-width: 3; }
.msg-video-ring .ring-fill {
  fill: none;
  stroke: #fff;
  stroke-width: 3;
  stroke-linecap: round;
  transition: stroke-dashoffset 0.12s linear;
}

/* full-screen video player in the lightbox */
.video-lightbox { animation: fade-in 0.2s ease; }
.lightbox-video {
  position: relative;
  z-index: 1; /* above the backdrop */
  max-width: 100%;
  max-height: 100%;
  border-radius: 8px;
  background: #000;
}
/* staged video preview: poster tile with a small play glyph over it */
.preview-chip.preview-video { position: relative; }
.preview-chip.preview-video .preview-play {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: #fff;
  pointer-events: none;
}
.preview-chip.preview-video .preview-play svg {
  width: 24px;
  height: 24px;
  padding: 5px;
  box-sizing: border-box;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.5);
}

/* link (OpenGraph) preview card */
.link-preview {
  display: flex;
  flex-direction: column;
  max-width: 280px;
  background: var(--bubble-them);
  color: var(--ink);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  text-decoration: none;
}
.lp-image { width: 100%; max-height: 150px; object-fit: cover; display: block; }
.lp-body { padding: 8px 11px; }
.lp-title { font-weight: 600; font-size: 14px; line-height: 1.3; }
.lp-desc {
  font-size: 12px;
  color: var(--muted);
  margin-top: 2px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.lp-host { font-size: 11px; color: var(--muted); margin-top: 5px; }

/* composer attachment preview. .composer-float is the in-flow anchor at the top
   of the composer stack; the typeahead drop-up absolutely anchors to it. */
.composer-float { position: relative; z-index: 5; }
/* The staging strip occupies real layout space (the feed shrinks above it) —
   floating it over the feed covered whatever sat at the bottom, including
   tappable action chips ("Skip for now" became dead text under a thumbnail). */
.composer-preview {
  padding: 8px 16px 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.composer-preview.hidden { display: none; }
.preview-spinner {
  width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid var(--border); border-top-color: var(--accent);
  animation: spin 0.8s linear infinite; flex: none;
}
.preview-chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  /* Panel bg + border + shadow so the chip reads as a floating card over
     whatever messages happen to be underneath it. */
  background: var(--panel);
  border: 1px solid var(--border);
  box-shadow: 0 6px 20px var(--shadow);
  border-radius: 12px;
  padding: 6px 34px 6px 8px;
  max-width: 100%;
}
.preview-chip img { width: 40px; height: 40px; object-fit: cover; border-radius: 8px; }
.preview-file { font-size: 24px; }
/* video upload progress chip: percentage label above a determinate bar */
.preview-chip.preview-uploading {
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  width: 160px;
}
.preview-progress {
  height: 6px;
  border-radius: 3px;
  background: var(--border);
  overflow: hidden;
}
.preview-progress-fill {
  height: 100%;
  width: 0;
  background: var(--accent);
  border-radius: 3px;
  transition: width 0.15s ease;
}
.preview-name {
  font-size: 13px;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 220px;
}
.preview-remove {
  position: absolute;
  right: 6px;
  top: 50%;
  transform: translateY(-50%);
  background: var(--ink);
  color: var(--panel);
  width: 22px;
  height: 22px;
  padding: 0;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.preview-remove svg { width: 13px; height: 13px; }

/* Action chips: one-tap buttons a helper attaches under its bubble */
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.action-chip {
  background: var(--panel); color: var(--link); border: 1px solid var(--border);
  border-radius: 14px; padding: 7px 14px; font-size: 14px; font-weight: 600; cursor: pointer;
}
.action-chip:hover:not(:disabled) { background: var(--bubble-them); }
.action-chip:disabled { color: var(--muted); cursor: default; }
/* A spent chip (its step was answered/skipped): a quiet part of the record,
   not something that still looks tappable. */
.action-chip--spent { background: transparent; border-color: transparent; padding-left: 6px; padding-right: 6px; }
/* A trailing chip run (the quiet alternative path, e.g. the quick form) sits at
   the row's far end: auto inline-start margin on its first chip pushes it over
   (RTL-safe), or onto its own right-aligned line when the row wraps. */
.action-chip--trailing { margin-inline-start: auto; }

/* Local notice: a command's private "only visible to you" reply — a client-only
   feed row (never sent, gone on reload). It lives IN the feed and PERSISTS (no
   auto-dismiss). It reads like a chat reply: the sender (+ AI/BOT badge) and the
   "Only visible to you" tag sit ABOVE, outside the bubble (like a message's name
   row); the reply text is a chat-style bubble. */
.local-notice {
  /* No side inset: notice bubbles share the message grid, so they line up with
     .msg bubbles and the typing indicator (which sit flush with the feed). */
  align-self: stretch; margin: 6px 0 8px;
  display: flex; flex-direction: column; align-items: flex-start;
}
/* A continuation in a same-speaker run: no header, tucked up under the previous
   bubble — the same rhythm as .msg.cont. */
.local-notice--cont { margin-top: -4px; }
.local-notice-head { display: flex; align-items: center; gap: 6px; margin-bottom: 3px; align-self: stretch; }
.local-notice-glyph { font-size: 15px; line-height: 1; }
.local-notice-from { font-weight: 600; font-size: 14px; color: var(--ink); }
.local-notice-badge {
  font-size: 10px; font-weight: 600; letter-spacing: 0.03em; line-height: 1;
  padding: 2px 5px; border-radius: 6px; background: var(--accent); color: #fff;
}
.local-notice-tag { margin-left: auto; font-size: 11px; color: var(--muted); }
.local-notice-bubble {
  max-width: 90%; padding: 10px 12px; border-radius: 14px; background: var(--bubble-them);
}
.local-notice-body { font-size: 15px; color: var(--ink); white-space: pre-wrap; }
.local-notice-body code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.92em;
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  padding: 1px 5px;
  border-radius: 5px;
}
.local-notice-body a { color: var(--accent); }
/* Markdown blocks in notice bodies (#531) — same tight-list treatment as bubbles */
.local-notice-body p, .local-notice-body ul, .local-notice-body ol { margin: 6px 0; }
.local-notice-body ul, .local-notice-body ol { padding-left: 22px; }
.local-notice-body li p, .local-notice-body li ul, .local-notice-body li ol { margin: 0; }
.local-notice-body p:first-child, .local-notice-body ul:first-child, .local-notice-body ol:first-child { margin-top: 0; }
.local-notice-body p:last-child, .local-notice-body ul:last-child, .local-notice-body ol:last-child { margin-bottom: 0; }
/* Quoted machine output on a notice (a provider's error): a quotation, set off
   with a rule and muted — readable prose framing above, evidence below. Not
   monospaced: it's a quote, not code. */
.local-notice-quote {
  margin-top: 8px; padding: 6px 10px;
  border-left: 3px solid color-mix(in srgb, var(--muted) 45%, transparent);
  font-size: 13px; color: var(--muted);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
/* Optional preview image on a notice (in-code same-origin URLs only — e.g. the
   setup conversation previewing the just-uploaded server photo). Fixed box so
   the feed's scroll position is right BEFORE the image loads. */
.local-notice-img {
  display: block; margin-top: 8px; width: 180px; height: 180px; object-fit: cover;
  border-radius: 12px; border: 1px solid var(--border);
  background: color-mix(in srgb, var(--ink) 6%, transparent);
}
/* An avatar preview renders as the circle it will actually be. */
.local-notice-img--round { border-radius: 50%; object-fit: cover; }
/* Notices ease in like live messages (see .msg.appear). */
.local-notice.appear { animation: msg-appear 0.22s cubic-bezier(0.2, 0.8, 0.2, 1); }
@media (prefers-reduced-motion: reduce) {
  .local-notice.appear { animation: none; }
}
.local-notice .chip-row { margin-top: 8px; }
/* A chips-only notice row (the live actions under a persisted bubble): tuck it
   right up under the bubble it belongs to instead of floating a full row lower.
   (Doubled class so it outranks the base rules above regardless of order.) */
.local-notice.local-notice--chips { margin-top: 2px; } /* + the feed's 4px gap = a 6px breath */
.local-notice.local-notice--chips .chip-row { margin-top: 0; }
/* A confirmation is loud only for a while: after ~30s it "blinds up" (its height
   animates down — see collapseNotice) to this compact one-line record
   (glyph · who · what happened · action). */
.local-notice--compact {
  display: flex; flex-direction: row; align-items: baseline; flex-wrap: wrap; gap: 5px;
  margin: 2px 10px; padding: 3px 12px;
  background: transparent; border: none; /* shed the bubble */
  font-size: 13px; color: var(--muted);
}
.local-notice--compact .local-notice-glyph { font-size: 14px; align-self: center; }
.local-notice--compact .lc-from { font-weight: 600; color: var(--ink); }
.local-notice--compact .lc-summary { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.local-notice--compact .lc-action {
  flex: 0 0 auto; background: none; border: none; padding: 0; font: inherit; font-weight: 600;
  color: var(--link); cursor: var(--ui-cursor, pointer);
}
.local-notice--compact .lc-action::before { content: "·"; margin-right: 5px; color: var(--muted); font-weight: 400; }
/* App-thread action line: a centered, muted /me-style record of an interaction
   ("added 'milk'") — deliberately unlike a chat bubble. */
.chatapp-action { align-self: center; text-align: center; color: var(--muted); font-size: 13px; padding: 3px 14px; margin: 10px 0; max-width: 90%; }
/* The "start of your chat history" intro: a calm, centered explainer pinned at
   the top of a bot's chat (a persistent first row that scrolls up with history). */
.chatapp-intro {
  align-self: center; max-width: 40ch; text-align: center;
  color: var(--muted); font-size: 14px; line-height: 1.5; padding: 18px 16px 22px;
}

/* Text selection works everywhere now: desktop reacts with a stationary hold
   (drag-select cancels it) and touch reacts with a double-tap, so long-press is
   free for native selection/copy on mobile. */
.reactions { display: flex; flex-wrap: wrap; gap: 4px; margin: 3px 2px 0; }
.msg.me .reactions { justify-content: flex-end; }
.reactions:empty { display: none; }
.reaction-chip {
  background: var(--bubble-them);
  color: var(--ink);
  border: 1px solid transparent;
  border-radius: 999px;
  padding: 2px 9px;
  font-size: 13px;
  font-weight: 500;
  line-height: 1.35;
}
.reaction-chip.mine { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 16%, var(--panel)); }
.emoji-picker {
  position: fixed;
  display: flex;
  gap: 2px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 4px 6px;
  box-shadow: 0 12px 40px var(--shadow);
  z-index: 60;
}
.emoji-opt {
  background: transparent;
  font-size: 22px;
  padding: 4px 6px;
  border-radius: 50%;
  line-height: 1;
}
.emoji-opt:hover { background: var(--bubble-them); }
/* the "Reply" action in the message menu, set off from the emoji row */
.picker-action {
  background: transparent;
  color: var(--link);
  padding: 4px 8px;
  margin-left: 2px;
  border-radius: 50%;
  border-left: 1px solid var(--border);
  display: inline-flex;
  align-items: center;
  line-height: 1;
}
.picker-action svg { width: 22px; height: 22px; }
.picker-action:hover { background: var(--bubble-them); }

/* mobile long-press message menu (WhatsApp-style): a dim backdrop, the emoji
   reactions floating above the message, and a labelled action menu below it
   (see openMessageSheet in shell.js). */
.msg-sheet {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(0, 0, 0, 0.38);
  animation: fade-in 0.15s ease;
}
/* the lifted copy of the pressed message, above the dim (see openMessageSheet) */
.msg-sheet-clone {
  position: fixed;
  margin: 0 !important;
  pointer-events: none;
}
.msg-sheet-reactions,
.msg-sheet-menu {
  position: absolute;
  background: var(--panel);
  border: 1px solid var(--border);
  box-shadow: 0 12px 40px var(--shadow);
}
.msg-sheet-reactions {
  display: flex;
  gap: 2px;
  padding: 5px 7px;
  border-radius: 999px;
}
.sheet-emoji {
  background: transparent;
  font-size: 26px;
  line-height: 1;
  padding: 6px 8px;
  border-radius: 50%;
}
.sheet-emoji:active { background: var(--bubble-them); }
.msg-sheet-menu {
  min-width: 200px;
  border-radius: 14px;
  overflow: hidden;
}
.msg-sheet-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  width: 100%;
  padding: 13px 16px;
  background: transparent;
  font-size: 16px;
  text-align: left;
  color: var(--ink);
}
.msg-sheet-item:active { background: var(--bubble-them); }
.msg-sheet-item.danger { color: var(--danger); }
.msg-sheet-icon { display: inline-flex; color: var(--muted); flex: none; }
.msg-sheet-item.danger .msg-sheet-icon { color: var(--danger); }
.msg-sheet-icon svg { width: 20px; height: 20px; }

/* a reply: a tappable quote of the parent sits above the bubble (à la Messages),
   and the parent gets a "N Replies" tag below it. */
.reply-quote {
  align-self: stretch;
  text-align: left;
  max-width: 100%;
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 6px 11px;
  margin-bottom: 1px;
  font-weight: 400;
  white-space: normal;
}
.msg.me .reply-quote { text-align: right; }
.reply-quote-name { font-size: 11px; color: var(--muted); margin-bottom: 1px; }
.reply-quote-text {
  /* two notches under the bubble text at every size (15→13, touch 17→15) */
  font-size: calc(var(--msg-font-size) - 2px);
  color: var(--muted);
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.msg.me .reply-quote-text { margin-left: auto; }
/* "N Replies" tag under a message that's been replied to (opens the thread view) */
.reply-count {
  font-size: 11px;
  font-weight: 600;
  color: var(--link);
  margin: 3px 6px 0;
  cursor: pointer;
}
.reply-count[hidden] { display: none; }
/* flash a message when you jump to it from a quote */
.msg.flash { animation: flash-msg 1.2s ease; border-radius: var(--radius); }
@keyframes flash-msg {
  0%, 100% { background: transparent; }
  25% { background: color-mix(in srgb, var(--accent) 16%, transparent); }
}
/* sticky highlight for the current search result, until another is picked or
   search ends (unlike .flash, this doesn't fade on its own). A dashed ring
   OUTSIDE the bubble, on the page background — not a fill on the bubble
   itself — so it stays visible on "me" bubbles, whose background is often
   the same hue as --accent (a fill there nearly disappears). */
.msg.search-active .bubble { outline: 2px dashed var(--accent); outline-offset: 3px; }

/* ---- reply-thread view (apps/chat.js): tap a quote or a "N Replies" tally and
   the thread lifts over the transcript, iMessage-style — the root pinned on
   top, its replies below, the real composer beneath staged to the root. The
   overlay lives inside .chatapp-feedwrap, so it covers exactly the feed's box
   and never the composer. All colors are theme vars → light/dark for free. */
.chatapp-thread { position: absolute; inset: 0; z-index: 30; display: flex; }
.chatapp-thread-dim {
  position: absolute;
  inset: 0;
  /* lighter than the msg-sheet dim: the frosted panel reads the transcript
     through itself, so the backdrop only needs to recede, not vanish */
  background: rgba(0, 0, 0, 0.25);
  animation: fade-in 0.15s ease;
}
.chatapp-thread-panel {
  position: relative; /* above the dim */
  margin: 10px;
  flex: 1;
  min-height: 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 16px;
  box-shadow: 0 12px 40px var(--shadow);
  overflow: hidden;
  animation: thread-rise 0.18s cubic-bezier(0.2, 0.8, 0.2, 1);
}
@keyframes thread-rise { from { opacity: 0; transform: translateY(12px); } }
/* Frosted glass: the conversation stays visible under the lifted thread — a
   translucent panel over a backdrop blur, both themes via --panel. The solid
   var(--panel) above is the fallback where backdrop-filter/color-mix aren't
   supported (a see-through panel with NO blur would be unreadable). */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) and (background: color-mix(in srgb, red 50%, transparent)) {
  .chatapp-thread-panel {
    background: color-mix(in srgb, var(--panel) 72%, transparent);
    -webkit-backdrop-filter: blur(18px) saturate(1.15);
    backdrop-filter: blur(18px) saturate(1.15);
  }
}
@media (prefers-reduced-motion: reduce) {
  .chatapp-thread-dim,
  .chatapp-thread-panel { animation: none; }
}
/* embedded in the native shells: keep the panel clear of the notch/home bar */
.native-embed .chatapp-thread-panel {
  margin-top: calc(10px + var(--native-top-inset, 0px));
  margin-bottom: calc(10px + var(--native-bottom-inset, 0px));
}
.chatapp-thread-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 12px 14px 10px;
  border-bottom: 1px solid var(--border);
}
.chatapp-thread-title { font-weight: 700; font-size: 15px; color: var(--ink); }
.chatapp-thread-count { flex: 1; min-width: 0; font-size: 12px; color: var(--muted); }
.chatapp-thread-x {
  flex: none;
  align-self: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.chatapp-thread-x svg { width: 16px; height: 16px; }
.chatapp-thread-x:hover { background: var(--bubble-them); }
/* the pinned root message — capped so a long one can't swallow the replies */
.chatapp-thread-rootmsg {
  flex: none;
  max-height: 40%;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 12px 16px 10px;
  border-bottom: 1px solid var(--border);
  display: flex;
  flex-direction: column;
}
/* the quoted snapshot standing in for a root that isn't loaded */
.chatapp-thread-rootfall { border-left: 2px solid var(--accent); padding-left: 10px; }
.chatapp-thread-fallsnippet { font-size: var(--msg-font-size); color: var(--ink); overflow-wrap: anywhere; }
.chatapp-thread-note {
  padding: 8px 16px;
  font-size: 12px;
  color: var(--muted);
  border-bottom: 1px solid var(--border);
}
.chatapp-thread-note.hidden { display: none; }
.chatapp-thread-msgs {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden; /* clip mid swipe-to-reply, like the feed */
  padding: 12px 16px 16px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
/* inside the view, every row is already the thread — a per-row quote card or
   tally would only point back at where you're standing */
.chatapp-thread .reply-quote,
.chatapp-thread .reply-count { display: none; }

/* composer "Replying to …" / "Editing message" banners, above the composer
   (and any attachment preview). They share the .reply-bar inner markup. */
.composer-reply,
.composer-edit {
  padding: 8px 14px;
  border-top: 1px solid var(--border);
  background: var(--panel);
}
.composer-reply.hidden,
.composer-edit.hidden { display: none; }
.composer-edit { display: flex; align-items: center; justify-content: space-between; color: var(--muted); font-size: 14px; }
.banner-x { background: transparent; color: var(--muted); font-size: 20px; line-height: 1; padding: 0 6px; }
.reply-bar { display: flex; align-items: center; gap: 10px; }
.reply-bar-icon { flex: none; color: var(--link); display: inline-flex; }
.reply-bar-icon svg { width: 18px; height: 18px; }
.reply-bar-body {
  flex: 1;
  min-width: 0;
  border-left: 2px solid var(--accent);
  padding-left: 10px;
}
.reply-bar-name { font-size: 12px; font-weight: 600; color: var(--ink); }
.reply-bar-text {
  font-size: 13px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.reply-bar-cancel {
  flex: none;
  width: 26px;
  height: 26px;
  padding: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.reply-bar-cancel svg { width: 15px; height: 15px; }
.reply-bar-cancel:hover { background: var(--bubble-them); }

/* Subtle "… is typing" line. It renders INSIDE the feed, below the newest
   message (shell.js appends/removes it), not in a band between feed and
   composer: a band resized the feed as it toggled — jolting the stream — and
   the native shells float their composer over the page bottom, which covered
   it entirely. In-stream it scrolls with the messages, like an incoming one. */
.typing {
  align-self: flex-start;
  max-width: 78%;
  margin-top: 10px;
  padding: 0 6px;
  font-size: 12px;
  font-style: italic;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* Make the row behave like an incoming .msg — the feed is a flex column, so a
     bare row misbehaves two ways. flex-shrink:0: with overflow:hidden a flex
     item's auto min-height is 0, so the feed (once it overflows) shrinks this
     row to nothing — the group "X is typing…" line was present but 0px tall.
     position:relative: like .msg, it makes the row its own containing block so
     the bubble's overflow:visible (the dots bobbing out of the box) can't
     propagate up and give the whole app a second scrollbar. */
  flex-shrink: 0;
  position: relative;
}
/* DM variant: an iMessage-style incoming bubble with pulsing dots — the typer
   can only be the one other person, so no name (groups keep the line above). */
.typing.typing-bubble {
  display: flex;
  align-items: center;
  gap: 3px;
  padding: 13px 14px;
  border-radius: var(--radius);
  border-bottom-left-radius: 6px;
  background: var(--bubble-them);
  overflow: visible; /* the dots bob upward, out of the content box */
}
.typing-bubble .dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--ink) 45%, transparent);
  animation: typing-dot 1.3s ease-in-out infinite;
}
.typing-bubble .dot:nth-child(2) { animation-delay: 0.18s; }
.typing-bubble .dot:nth-child(3) { animation-delay: 0.36s; }
@keyframes typing-dot {
  0%, 60%, 100% { opacity: 0.35; transform: translateY(0); }
  30% { opacity: 1; transform: translateY(-3px); }
}
@media (prefers-reduced-motion: reduce) {
  .typing-bubble .dot { animation: none; opacity: 0.5; }
}
/* screen-reader-only text (the bubble's dots carry no words) */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* composer autocomplete (:emoji / @mention / slash) — a drop-UP that OVERLAYS the
   feed rather than a flex band that shoves the messages up as you type. It's
   anchored to .composer-float (the zero-height anchor at the top of the composer
   stack), so it floats over the messages and rises with any reply/edit banner. */
.typeahead {
  position: absolute;
  bottom: 6px;
  left: 8px;
  right: 8px;
  max-height: 216px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--panel);
  box-shadow: var(--shadow);
  /* Clip the full-bleed active row to the rounded corners (overflow-y already
     makes this a clipping container). */
  overflow-x: hidden;
}
.typeahead.hidden { display: none; }
.ta-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  text-align: left;
  background: transparent;
  color: var(--ink);
  border: none;
  border-radius: 0; /* override the global pill button radius — the active row fills edge to edge */
  padding: 8px 12px;
  font-size: 15px;
}
.ta-item.active { background: var(--bubble-them); }
.ta-item .avatar { width: 28px; height: 28px; font-size: 13px; flex: none; }
.ta-emoji { font-size: 20px; width: 24px; text-align: center; flex: none; }
.ta-code { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ta-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Command typeahead: the "/todo" name, then a muted one-line description after it. */
.ta-desc { color: var(--muted); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* an @mention inside a message — just bold; it inherits the bubble ink, so it
   stays legible in both the grey "them" and accent "me" bubbles. */
.mention { font-weight: 600; }
.mention-link { cursor: pointer; }
.mention-link:hover { text-decoration: underline; text-underline-offset: 2px; }
/* the little menu a tapped mention opens (anchored + fixed, like .emoji-picker) */
.mention-menu {
  position: fixed;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 12px 40px var(--shadow);
  padding: 6px;
  z-index: 60;
}
.mention-menu-item {
  display: block;
  width: 100%;
  text-align: left;
  background: transparent;
  color: var(--ink);
  border-radius: 8px;
  padding: 9px 14px;
  font-weight: 500;
  white-space: nowrap;
}
.mention-menu-item:hover { background: var(--bubble-them); }

.composer {
  display: flex;
  gap: 8px;
  align-items: flex-end;
  /* Desktop (fine pointer): the input hugs the bottom — env() is 0, so no fixed
     bottom padding. Touch devices get breathing room + the safe-area inset below
     (see the pointer:coarse rule), which desktop never picks up. */
  padding: 12px 16px;
  padding-bottom: env(safe-area-inset-bottom, 0px);
  background: var(--panel);
}
/* Touch: 12px of breathing room ABOVE the Home indicator / swipe bar (the inset),
   so the input never sits flush against the bottom edge or a browser toolbar.
   Scoped to coarse pointers so desktop stays at the bare inset (≈0). */
@media (pointer: coarse) {
  .composer { padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)); }
  /* While typing, the keyboard covers the home-indicator inset, so drop it entirely
     (the composer sits directly on the keyboard) — .editing is toggled on focus. */
  .composer.editing { padding-bottom: 0; }
  /* Long-press opens the message menu, so suppress the native selection/callout that
     would otherwise fight it (Copy lives in the menu instead). */
  .feed .bubble, .chatapp-feed .bubble, .chatapp-thread .bubble { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
  /* A phone is held further from the eye than a laptop screen sits — bump the
     core reading + typing text up a notch so messages are comfortable. Desktop
     (fine pointer) keeps its sizes. Composer stays ≥16px so iOS doesn't
     auto-zoom the page when the field is focused. */
  :root { --msg-font-size: 17px; }
  /* iOS/iPadOS honour the system Text Size (Dynamic Type): -apple-system-body
     tracks it, so a member who's set large type gets large messages. Browsers
     without the keyword (Android) drop the whole shorthand and keep the 17px
     variable from the base rule; the two declarations after it restore what
     the shorthand resets. Higher-specificity sizes (.jumbo-emoji) still win. */
  .msg .bubble { font: -apple-system-body; font-family: inherit; line-height: 1.35; }
  .composer .composer-input { font-size: 17px; }
  .thread-row-name { font-size: 17px; }
  .thread-row-preview { font-size: 14px; }
}
.composer-field {
  position: relative;
  flex: 1;
  display: flex;
}
/* The inset field label ("Your name:") — the Mail/Messages "To:" pattern: a
   quiet accent pill pinned INSIDE the input on its first line; chat.js indents
   the textarea's text past it. Absolute (over the textarea, which owns the box
   visuals) and inert to the pointer; top 9px centres it in the 40px rest
   height and keeps it on line one as the box grows. */
.composer-label {
  position: absolute;
  /* Above the input: the rich composer's contenteditable is position:relative
     (unlike the classic textarea), so without a z-index its opaque pill paints
     over this label in DOM order. */
  z-index: 1;
  left: 12px;
  top: 9px;
  padding: 1px 9px;
  border-radius: 999px;
  /* The theme's designed on-accent pairing — the one combination every theme
     guarantees AA contrast for (a tinted-text pill fails on amber). */
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 12.5px;
  font-weight: 600;
  line-height: 20px;
  white-space: nowrap;
  pointer-events: none;
  opacity: 1;
  transition: opacity 120ms ease;
}
.composer-label[hidden] {
  display: block; /* keep it measurable/animatable; hidden = faded out */
  opacity: 0;
}
@media (prefers-reduced-motion: reduce) {
  .composer-label { transition: none; }
}
.composer .composer-input {
  flex: 1;
  resize: none;
  max-height: 140px;
  /* 8+8 padding + 22 line + 2 border = 40px at rest. Symmetric so the text is
     vertically centred in the box; right padding leaves room for the send button.
     Type at the reading size; past 16px the 22px line would get cramped, so the
     line grows with the font from there (and the box grows with it). */
  padding: 8px 46px 8px 14px;
  font-size: var(--msg-font-size);
  line-height: max(22px, 1.35em);
  min-height: 40px;
  border-radius: 20px;
  font-family: inherit;
  /* Autogrow caps at max-height (see the JS); past that the field must scroll its
     own overflow rather than swallow the earlier lines — so overflow-y is auto,
     not hidden. overflow-x stays hidden since the text wraps. */
  overflow-x: hidden;
  overflow-y: auto;
  /* The shared lifted-panel input fill (see --input-bg / .eleven-input). */
  background: var(--input-bg);
}
/* The rich composer (composer-pm.js): a ProseMirror contenteditable wearing
   the same skin. Auto-grow is pure CSS here — min/max-height + overflow — so
   there is no JS measuring to go stale (the lesson from the reverted native
   attempt). The textarea's implicit border comes back explicitly. */
.composer .composer-input[contenteditable] {
  position: relative;
  border: 1px solid var(--border);
  white-space: pre-wrap;
  overflow-wrap: break-word;
  color: var(--ink);
  caret-color: var(--accent);
}
.composer .composer-input[contenteditable] p { margin: 0; }
/* In-composer blocks: the bubble treatments at composer scale. */
.composer .composer-input[contenteditable] ul,
.composer .composer-input[contenteditable] ol { margin: 2px 0; padding-left: 22px; }
.composer .composer-input[contenteditable] blockquote {
  margin: 2px 0;
  padding: 1px 0 1px 10px;
  border-left: 3px solid color-mix(in srgb, var(--ink) 30%, transparent);
  color: color-mix(in srgb, var(--ink) 72%, transparent);
}
.composer .composer-input[contenteditable] pre {
  margin: 2px 0;
  padding: 6px 8px;
  border-radius: 8px;
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.92em;
  overflow-x: auto;
  white-space: pre;
}
.composer .composer-input[contenteditable] code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.92em;
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  padding: 1px 5px;
  border-radius: 5px;
}
.composer .composer-input[contenteditable] pre code { background: none; padding: 0; font-size: 1em; }
/* Placeholder: contenteditable has no native one — the pm-empty class is
   maintained by the backend; float keeps the caret on the first line. */
/* Theme-sheet composer preference rows (#531) */
.composer-pref-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 8px 0;
  font-size: 14px;
  color: var(--ink);
  cursor: pointer;
}
.composer-pref-row input[type="checkbox"] { accent-color: var(--accent); width: 18px; height: 18px; margin: 0; }
.composer-pref-hint { margin: -4px 0 8px 28px; font-size: 12px; color: var(--muted); }
.composer .composer-input.pm-empty::before {
  content: attr(data-placeholder);
  color: var(--muted);
  float: left;
  height: 0;
  pointer-events: none;
}

/* eleven-input — the reusable text-input surface: the same lifted-panel fill the
   chat composer uses, so inputs across the app and its mini-apps read as one.
   Layer your own shape (radius override, min-height, monospace, …) on top. */
.eleven-input {
  background: var(--input-bg);
  color: var(--ink);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: inherit;
}
.eleven-input:focus { outline: none; border-color: var(--accent); }
/* circular up-arrow send button, tucked into the bottom-right of the field */
.send-btn {
  position: absolute;
  right: 4px;
  bottom: 3px;
  width: 34px;
  height: 34px;
  padding: 0;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--accent);
  color: var(--accent-ink);
}
.send-btn[hidden] { display: none; }
.send-btn:disabled { opacity: 0.45; cursor: default; }
/* A send queued behind an in-flight upload (Enter mid-upload — see chat.js
   sendQueued): swap the arrow for a spinner, at full strength so it reads as
   "working", not the greyed can't-send state. */
.send-btn.send-waiting svg { display: none; }
.send-btn.send-waiting:disabled { opacity: 1; }
.send-btn.send-waiting::before {
  content: "";
  width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--accent-ink) 35%, transparent);
  border-top-color: var(--accent-ink);
  animation: spin 0.8s linear infinite; /* @keyframes spin, top of file */
}
/* Enlarge the send button's tap target — mostly to the right, into the composer's
   padding — so a near-miss on touch triggers Send instead of falling through to the
   field/feed and dismissing the keyboard. */
.send-btn::after {
  content: "";
  position: absolute;
  top: -10px;
  right: -20px;
  bottom: -10px;
  left: -6px;
}
/* left-side attach (＋) button */
.attach-btn {
  flex: none;
  width: 40px;
  height: 40px;
  padding: 0;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--bubble-them);
  color: var(--ink);
  font-size: 24px;
  line-height: 1;
}


/* invite QR (admin) — full-screen opaque takeover so nothing else is on screen */
.qr-screen {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: var(--bg);
  overflow: auto;
}
.qr-modal {
  margin: 8vh auto;
  max-width: 360px;
  text-align: center;
  border: none;
  box-shadow: none;
}
.qr-modal h2 { margin: 0 0 6px; font-size: 20px; }
.qr-wrap {
  display: flex;
  justify-content: center;
  margin: 18px 0;
}
.qr-wrap canvas {
  max-width: 100%;
  height: auto;
  image-rendering: pixelated; /* keep modules crisp when CSS scales the canvas down */
  border-radius: 8px;
}
.qr-modal .row { justify-content: center; }

/* input + button on one row (e.g. Create invite) */
.inline-form {
  display: flex;
  gap: 10px;
  align-items: stretch;
}
.inline-form input {
  flex: 1;
  min-width: 0;
}

.banner {
  text-align: center;
  font-size: 13px;
  color: var(--muted);
  padding: 6px;
}
.banner.warn { color: var(--warn); }


/* ---- admin ---- */
.admin { max-width: 760px; margin: 0 auto; padding: 24px; }
.admin h1 { margin: 0 0 4px; }
.admin .sub { color: var(--muted); margin: 0 0 24px; }
.admin section {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 18px;
  padding: 18px;
  margin-bottom: 18px;
}
.admin section h2 { margin: 0 0 12px; font-size: 16px; }
.invite {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 0;
  border-top: 1px solid var(--border);
}
.invite:first-of-type { border-top: none; }
.invite .meta { flex: 1; min-width: 0; }
.invite .meta .label { font-weight: 600; }
.invite .meta .status { font-size: 12px; color: var(--muted); }
.invite .device-note { flex: none; max-width: 220px; font-size: 12px; color: var(--muted); text-align: right; }
/* custom-app rows: the name is an identifier, so it's mono — but row-title sized,
   not .mono's 12px */
.invite .label.mono { font-size: 14px; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
.key-box {
  background: var(--bubble-them);
  border-radius: 12px;
  padding: 12px;
  word-break: break-all;
  margin: 10px 0;
}
.hidden { display: none !important; }
.toast {
  position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%);
  background: var(--pill-bg); color: var(--pill-ink); padding: 10px 16px; border-radius: 999px;
  font-size: 14px; opacity: 0; transition: opacity .2s; pointer-events: none;
  z-index: 100; /* above full-screen overlays (qr-screen) so it's never hidden */
}
.toast.show { opacity: 1; }


/* full-screen onboarding cards */
.card.onboard { text-align: center; }
.card.onboard .row { justify-content: center; }
.card.onboard p { margin-bottom: 14px; }
.onboard-icon { font-size: 46px; line-height: 1; margin-bottom: 10px; }
/* the instance branding photo, when set — replaces the emoji on onboarding cards */
.onboard-photo {
  width: 72px; height: 72px; border-radius: 18px; object-fit: cover;
  margin: 0 auto 12px; display: block;
}
/* the browser-only caveat on the welcome card: present but quiet, with clear
   air between it and the button it annotates. (.card selectors so this outranks
   the generic .card p margin rule.) */
.card .welcome-caveat { font-size: 13px; color: var(--muted); margin: 20px auto 4px; max-width: 42ch; }
/* the space's own description on the welcome card: the admin's note to the
   person joining — content to read, not chrome. A tinted panel with
   full-strength text sets it apart from the muted boilerplate around it. */
.card .welcome-desc {
  font-size: 15px; color: var(--ink); text-align: left; line-height: 1.5;
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  border-radius: 12px; padding: 12px 16px; margin: 2px auto 18px; max-width: 46ch;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.steps {
  text-align: left;
  color: var(--muted);
  line-height: 1.6;
  padding-left: 20px;
  margin: 4px 0 18px;
}
.steps li { margin-bottom: 6px; }
.steps strong { color: var(--ink); }

/* admin: a labelled checkbox row (e.g. "let members invite people") */
.toggle-row { display: flex; align-items: center; gap: 10px; cursor: pointer; font-weight: 600; }
/* profile sheet: the Away toggle + its inline error line (never a toast) */
.away-row { margin-top: 14px; font-size: 14px; justify-content: space-between; }
.away-note { color: var(--muted); font-size: 13px; min-height: 1em; margin: 4px 0 0; }
/* A slider switch (settings idiom) — a styled checkbox, so behaviour/forms
   stay stock. input.switch outranks the generic .toggle-row input sizing. */
input.switch {
  -webkit-appearance: none; appearance: none; margin: 0; flex: none;
  width: 42px; height: 26px; border-radius: 13px; position: relative;
  background: var(--bubble-them); cursor: pointer; transition: background 0.15s;
}
input.switch::after {
  content: ""; position: absolute; top: 2px; left: 2px; width: 22px; height: 22px;
  border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
  transition: left 0.15s;
}
input.switch:checked { background: var(--accent); }
input.switch:checked::after { left: 18px; }
input.switch:disabled { opacity: 0.5; cursor: default; }
/* BOT badge on admin People rows (shell.js has its own inline-styled agentTag) */
.bot-tag {
  margin-left: 6px; font-size: 0.7em; font-weight: 600; letter-spacing: 0.03em;
  padding: 1px 5px; border-radius: 6px; background: var(--accent, #888); color: #fff; vertical-align: middle;
}
.toggle-row input { width: 18px; height: 18px; }
.toggle-row input.switch { width: 42px; height: 26px; } /* the switch keeps its own size */

/* pending DM: waiting for an invited person to join (web/static/shell.js). While
   waiting there's no one to talk to, so the feed + composer are hidden and a single
   centered card fills the pane. It clears once the invitee joins and chats.
   .app-pane is included so the chat mini-app's full-pane feed/composer (which
   mount there instead of .feed/.composer) get the same treatment. */
.thread-view.waiting .feed,
.thread-view.waiting .app-pane,
.thread-view.waiting .composer,
.thread-view.waiting .composer-reply,
.thread-view.waiting .composer-edit,
.thread-view.waiting .composer-preview,
.thread-view.waiting .typing { display: none !important; }
.pending-banner {
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  overflow-y: auto;
}
.pending-card {
  width: 100%;
  max-width: 580px; /* wide enough that a typical invite link fits on one line */
  text-align: center;
  padding: 28px 24px;
  border: 1px solid var(--border);
  border-radius: 24px; /* the onboarding-card radius — same family as join */
  background: var(--bubble-them);
}
.pending-card .pending-avatar { margin: 0 auto 4px; }
.pending-card .pending-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 20px;
  margin: 8px 0 6px;
}
.pending-card p { margin: 0 0 16px; font-size: 15px; color: var(--muted); }
/* The link well hugs the link (centered), so it wraps only when the pane is
   genuinely too narrow for one line. */
.pending-card .key-box {
  width: fit-content; max-width: 100%;
  margin: 0 auto 16px; text-align: left; background: var(--panel);
}
/* display: flex is load-bearing: this card is .pending-card, not .card, so it
   never inherits .card .row's flex — without it the gap and centering are
   no-ops and the buttons sit flush. */
.pending-card .row { display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; }
.linkish.danger { color: var(--danger); }

/* Per-viewer names (#577): a DM partner renamed themself while you hold your
   own name for them. A slim inline row between header and feed — it occupies
   layout space (never floats over content) and clears once you choose. The
   choices wear the action-chip idiom (the app's one-tap-decision shape, same
   as a helper's chips), so "the app is asking you something" reads instantly;
   the two NAMES carry the emphasis, the frame stays quiet. */
.rename-banner {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  flex-wrap: wrap;
  padding: 8px 12px;
  font-size: 14px;
  color: var(--ink);
  background: var(--panel);
  border-bottom: 1px solid var(--border);
}
.rename-banner .rename-msg { margin-right: 2px; }
.rename-name { font-weight: 600; }
.rename-banner .action-chip:focus-visible,
.rename-note .action-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The same choice on a profile card (the group-chat path to it) — identical
   chips, so both surfaces of the one decision look like the one decision. */
.rename-note {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  flex-wrap: wrap;
  margin: 2px 0 8px;
  font-size: 14px;
  color: var(--ink);
}
/* The profile card's "what I call them" block: the Goes-by fact, the rename
   entry point, and the inline editor. */
.nick-area { text-align: center; margin: 2px 0 4px; }
.nick-area .nick-input {
  padding: 8px 12px;
  width: 240px;
  max-width: 100%;
  text-align: center;
  font-size: 15px;
}
.nick-area .row { display: flex; justify-content: center; gap: 10px; margin-top: 8px; }

/* ---- multi-thread layout (docs/threads-and-dms.md) ---- */
.tt { display: flex; flex-direction: column; height: 100%; width: 100%; background: var(--panel); }
/* The sidebar + thread row lives below the (usually absent) integrity banner. */
.tt-body { display: flex; flex: 1; min-height: 0; width: 100%; }

/* Tamper-evidence failure banner: a full-width band across the top of the whole
   shell, shown ONLY on an unresolved integrity alert (fork/rollback/withheld/
   bad-signature). Amber, with a ⚠️ glyph so it's never colour-alone; occupies
   layout (pushes the shell down), never a floating overlay. */
.integrity-banner {
  flex: none;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px 16px calc(12px + env(safe-area-inset-top, 0px));
  background: color-mix(in srgb, #d97706 16%, var(--panel));
  border-bottom: 1px solid color-mix(in srgb, #d97706 45%, transparent);
  color: var(--ink);
  font-size: 13.5px;
  line-height: 1.45;
}
.integrity-banner.hidden { display: none; }
.integrity-banner .ib-icon { font-size: 18px; line-height: 1.2; flex: none; }
.integrity-banner .ib-text { flex: 1; min-width: 0; }
.integrity-banner .ib-title { font-weight: 700; }
.integrity-banner .ib-actions { display: flex; gap: 8px; flex: none; }
.integrity-banner button {
  font: inherit;
  font-weight: 600;
  border-radius: 8px;
  padding: 6px 12px;
  min-height: 32px;
  cursor: pointer;
  border: 1px solid color-mix(in srgb, #d97706 55%, transparent);
  background: transparent;
  color: var(--ink);
}
.integrity-banner button.ib-primary {
  background: color-mix(in srgb, #d97706 85%, black 5%);
  color: #fff;
  border-color: transparent;
}
.integrity-banner button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-color-scheme: dark) {
  .integrity-banner { background: color-mix(in srgb, #f59e0b 22%, var(--panel)); }
}
@media (max-width: 640px) {
  .integrity-banner { flex-wrap: wrap; }
  .integrity-banner .ib-actions { width: 100%; }
}
/* floating connection-status pill (only shown during a sustained outage) */
.conn-status {
  position: fixed;
  top: calc(10px + env(safe-area-inset-top, 0px));
  left: 50%;
  transform: translateX(-50%);
  z-index: 90;
  background: var(--pill-bg);
  color: var(--pill-ink);
  font-size: 13px;
  font-weight: 600;
  padding: 6px 14px;
  border-radius: 999px;
  box-shadow: 0 6px 20px var(--shadow);
}
.conn-status.hidden { display: none; }
.sidebar { width: 320px; flex: none; display: flex; flex-direction: column; background: var(--sidebar); border-right: 1px solid var(--border); min-height: 0; }
/* Inline passkey-backup nudge: sits in the sidebar between the search box and the
   chat list, occupying its own layout band (never a floating toast). A subtle
   accent tint sets it apart without shouting. Contents are built in backup.js. */
.backup-banner {
  flex: none;
  margin: 8px 12px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.backup-banner.hidden { display: none; }
.backup-banner-title { font-weight: 700; font-size: 14px; color: var(--ink); }
.backup-banner-body { font-size: 13px; line-height: 1.4; color: var(--muted); }

/* The "a new app is available" offer — a slim inline sidebar nudge, not a
   forced reload. Deliberately quieter than the backup banner. */
.update-banner {
  flex: none;
  margin: 4px 12px 8px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--bubble-them);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.update-banner.hidden { display: none; }
.update-banner-msg { font-size: 13px; color: var(--ink); }
.update-banner-row { display: flex; gap: 8px; }
.update-banner-btn {
  cursor: var(--ui-cursor, pointer);
  font-size: 13px;
  padding: 5px 12px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--ink);
}
.update-banner-btn.primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.backup-banner-cta { align-self: flex-start; padding: 8px 14px; font-size: 13px; }
/* Full-width + left-aligned so the escalated "I can lose access…" label wraps
   inside the banner instead of overflowing its right edge. */
.backup-banner-dismiss {
  align-self: stretch;
  text-align: left;
  font-size: 13px;
  white-space: normal;
  overflow-wrap: anywhere;
}
/* Both headers share a height so the thread pane's header lines up with the
   sidebar's. Only the thread header carries a divider — the sidebar's "Chats"
   header flows straight into the chat list (with the search field below it). */
.side-head, .thread-head { box-sizing: border-box; min-height: 62px; padding: 8px 16px; }
.thread-head { border-bottom: 1px solid var(--border); }
.side-head { display: flex; align-items: center; gap: 8px; }
.side-head .title { flex: 1; min-width: 0; font-family: var(--font-display); font-weight: 700; font-size: 17px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.head-actions { display: flex; align-items: center; gap: 2px; flex: none; }
.avatar {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 34px; height: 34px; border-radius: 50%; color: #fff; font-weight: 700; font-size: 15px; line-height: 1;
}
.avatar-btn { background: transparent; padding: 0; border-radius: 50%; flex: none; }
.avatar-lg { width: 64px; height: 64px; font-size: 28px; }
.avatar.zoomable { cursor: pointer; } /* header avatar with a real photo: tap to zoom */
.field-label { display: block; font-size: 13px; color: var(--muted); margin: 2px 0 4px; }
/* Filter the chat list. Sits between the header and the list; hidden when there
   are no chats (renderThreadList toggles .hidden). */
.chat-search { flex: none; padding: 4px 12px 6px; display: flex; gap: 8px; }
.chat-search input {
  flex: 1; min-width: 0; box-sizing: border-box; font: inherit; font-size: 14px;
  padding: 8px 12px; border: 1px solid transparent; border-radius: 12px;
  background: var(--bubble-them); color: var(--ink); -webkit-appearance: none; appearance: none;
}
/* The funnel beside search: opens the list-filter menu (online-only +
   People/Bots/Apps). Highlights while any filter is narrowing the list.
   Stretched to the search field's height so the pair reads as one control. */
.chat-search .menu-wrap { display: flex; align-items: stretch; } /* so the funnel can match the field's height */
.filter-btn { color: var(--muted); padding: 0; width: 38px; align-self: stretch; justify-content: center; }
.filter-btn.active { color: var(--accent); }
.filter-menu { display: flex; flex-direction: column; gap: 2px; min-width: 180px; padding: 6px; }
/* Rows read like menu items: box, label, hover wash — mirroring .menu-item. */
.filter-row {
  display: flex; align-items: center; gap: 10px; cursor: pointer;
  font-size: 14px; font-weight: 500; padding: 8px 10px; border-radius: 8px;
}
.filter-row:hover { background: var(--bubble-them); }
.filter-sep { height: 1px; background: var(--border); margin: 4px 8px; }
/* The checkbox is drawn entirely here: the app-wide `input` rule (width:100%,
   panel fill, field padding) mangles a native checkbox into a blank pill, so
   appearance:none and take over — accent fill + a white tick when checked. */
input.filter-check {
  -webkit-appearance: none; appearance: none; margin: 0; padding: 0; flex: none;
  width: 18px; height: 18px; border-radius: 6px; cursor: pointer;
  border: 1.5px solid var(--border); background: var(--panel);
  display: inline-grid; place-content: center;
}
input.filter-check::before {
  content: ""; width: 10px; height: 10px; background: #fff;
  clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
  transform: scale(0); transition: transform 0.1s;
}
input.filter-check:checked { background: var(--accent); border-color: var(--accent); }
input.filter-check:checked::before { transform: scale(1); }
/* Keyboard focus keeps the app's accent ring; pointer clicks stay quiet. */
input.filter-check:focus { outline: none; }
input.filter-check:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.chat-search input::placeholder { color: var(--muted); }
/* Message search results (#586): an inline sidebar section under the search
   box — rows read like slim thread rows; everything keyboard-reachable. */
.msg-results { flex: none; overflow-y: auto; max-height: 45%; padding: 0 8px 6px; border-bottom: 1px solid var(--border); }
.msg-results-count { font-size: 12px; font-weight: 600; color: var(--muted); padding: 6px 8px 4px; }
.msg-result {
  display: block; width: 100%; text-align: left; box-sizing: border-box;
  font: inherit; color: var(--ink); background: none; border: none;
  padding: 8px 10px; border-radius: 10px; cursor: pointer;
}
.msg-result:hover { background: var(--bubble-them); }
.msg-result:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* the result currently jumped to (its message carries .msg.search-active) */
.msg-result.active { background: color-mix(in srgb, var(--accent) 16%, transparent); }
.msg-result-top { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; }
.msg-result-who { font-weight: 600; }
.msg-result-where { color: var(--muted); flex: none; max-width: 55%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-result-snippet { font-size: 13px; color: var(--muted); margin-top: 2px; overflow-wrap: anywhere; }
/* The deeper-search affordance + its progress line ("Searching back to…"). */
.msg-result-deeper, .msg-result-cancel {
  font: inherit; font-size: 13px; font-weight: 600; color: var(--accent);
  background: none; border: none; cursor: pointer; padding: 8px 10px; border-radius: 10px;
}
.msg-result-deeper:hover, .msg-result-cancel:hover { background: var(--bubble-them); }
.msg-result-deeper:focus-visible, .msg-result-cancel:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.msg-result-status { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 13px; color: var(--muted); padding: 4px 0 4px 10px; }
/* Keep the app's standard focus ring (global input:focus); just lift the fill. */
.chat-search input:focus { background: var(--panel); }
.thread-list { flex: 1; overflow-y: auto; min-height: 0; padding: 6px; display: flex; flex-direction: column; gap: 2px; }
.thread-empty-note { color: var(--muted); padding: 16px; font-size: 14px; }
.thread-row {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  background: transparent; color: var(--ink); border-radius: 14px; padding: 9px 14px;
}
.thread-row:hover { background: var(--bubble-them); }
.thread-row.active { background: var(--row-active); }
/* Bigger avatar in the chat list (à la WhatsApp/iMessage); scoped to the list so
   header/profile/reaction avatars keep their sizes. */
.thread-row .avatar { width: 52px; height: 52px; font-size: 21px; }
/* Presence: a coloured dot + status text (the DM header line, the member
   sheet's note) — never a pip on an avatar. Green/yellow/red per Paul's call. */
:root { --online: #34c759; --away: #f5b400; --offline: #ff3b30; }
.presence-line { display: inline-flex; align-items: center; gap: 6px; } /* dot truly centred on the text */
.p-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.p-dot.online { background: var(--online); }
.p-dot.away { background: var(--away); }
.p-dot.offline { background: var(--offline); }
/* Member sheet: the muted status note beside a member's name. */
.member-presence { margin-left: 8px; font-size: 12px; color: var(--muted); flex: none; }
/* Access-level chips on the profile card — the person's levels ("Coach",
   "VIP"), a quiet accent-tinted badge (names are plaintext space policy). */
.level-chips { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; margin: 8px 0 4px; }
.level-chip {
  font-size: 12px; font-weight: 600; line-height: 1;
  color: var(--accent-ink); background: var(--accent);
  border-radius: 999px; padding: 5px 10px;
}

/* Membership sheet (paid access levels). Quiet cards in the app's own
   vocabulary; the price is the one emphasized number, one accent, and the
   state line ("Renews …" / "Access ends …") is the reassuring signature —
   always dated, never color alone. */
.plan-card {
  border: 1px solid var(--border); border-radius: 14px;
  padding: 14px 16px; margin: 10px 0; display: flex; flex-direction: column; gap: 8px;
}
.plan-card.current { border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); }
.plan-head { display: flex; align-items: baseline; gap: 10px; }
.plan-name { font-size: 16px; font-weight: 650; color: var(--ink); flex: 1; min-width: 0; }
.plan-price { font-size: 15px; font-weight: 700; color: var(--ink); white-space: nowrap; }
.plan-price .per { font-size: 12px; font-weight: 500; color: var(--muted); }
/* The dated status line — the one thing a member always reads. Icon + words,
   so a state is never carried by colour alone (WCAG 2.2 AA). */
.plan-state { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--muted); }
.plan-state.warn { color: light-dark(#9a6a00, #f0b84a); } /* payment-failed, still icon+words */
.plan-state .dot { width: 7px; height: 7px; border-radius: 999px; background: currentColor; flex: none; }
.plan-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 2px; }
.plan-actions .pill-btn { font-size: 13px; }
.membership-note { font-size: 13px; color: var(--muted); margin: 4px 0 10px; }
.membership-sec { font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); margin: 16px 0 2px; }
.thread-row-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.thread-row-name { font-size: 16px; font-weight: 600; display: flex; align-items: center; min-width: 0; }
.thread-row-name-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Up to two lines of the latest message, then ellipsize (the spacious look).
   white-space:normal undoes the nowrap inherited from the global `button` rule
   (.thread-row is a <button>) — without it the line-clamp can't wrap and the
   preview gets hard-clipped on a single line. overflow-wrap lets long unbroken
   tokens (e.g. a pasted URL) wrap too. */
.thread-row-preview {
  font-size: 13px; font-weight: 400; color: var(--muted); line-height: 1.3;
  white-space: normal; overflow-wrap: anywhere;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* Time sits top-right, aligned with the name, even when the preview wraps to two lines. */
.thread-row-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 5px; flex: none; align-self: flex-start; padding-top: 3px; }
.thread-row-time { font-size: 11px; font-weight: 400; color: var(--muted); white-space: nowrap; }
.thread-row-pin { color: var(--muted); flex: none; }
.thread-row-pin svg { width: 15px; height: 15px; display: block; transform: rotate(45deg); }
.thread-row-mute { color: var(--muted); flex: none; }
.thread-row-mute svg { width: 14px; height: 14px; display: block; }
/* A muted member's folded message (issue #578): a quiet, obviously-interactive
   stub occupying the bubble's place — never a silent gap in the conversation. */
.muted-stub {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 8px 14px;
  border-radius: 16px;
  border: 1px dashed var(--border);
  color: var(--muted);
  font-size: 13px;
  font-style: italic;
}
.muted-stub-show {
  all: unset;
  cursor: pointer;
  font-style: normal;
  font-weight: 600;
  font-size: 13px;
  color: var(--accent);
  padding: 4px 8px; /* padding keeps the tap target comfortably ≥24px tall */
  border-radius: 6px;
}
.muted-stub-show:hover { text-decoration: underline; }
.muted-stub-show:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* A closed DM's banner (issue #578): sits where the composer was — inline,
   occupying layout space, never floating over the feed. */
.dm-closed-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 14px 16px;
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-size: 14px;
}
/* Small helper line under a settings field (e.g. the DM-privacy select). */
.field-note { margin-top: 6px; font-size: 12px; color: var(--muted); }
/* Destructive button — the moderation ladder's remove/ban rungs. */
button.danger { background: transparent; border: 1px solid var(--danger); color: var(--danger); }
/* Report sheet (issue #578): category chips + quoted evidence. */
/* The send button's in-flight spinner: the join loader's ring at button
   scale, inheriting the button's text color so it reads on the filled bg. */
.btn-spinner {
  display: inline-block;
  width: 14px;
  height: 14px;
  vertical-align: -2px;
  border-radius: 50%;
  border: 2px solid color-mix(in srgb, currentColor 30%, transparent);
  border-top-color: currentColor;
  animation: spin 0.7s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .btn-spinner { animation: none; opacity: 0.5; }
}
.report-cats { display: flex; gap: 8px; margin: 10px 0; flex-wrap: wrap; }
.report-cat {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--ink);
  border-radius: 999px;
  padding: 6px 14px;
  font-size: 13px;
}
.report-cat.on { border-color: var(--accent); color: var(--accent); font-weight: 600; }
.report-quote {
  margin: 10px 0;
  padding: 8px 12px;
  border-left: 3px solid var(--border);
  font-size: 13px;
  color: var(--muted);
  overflow-wrap: anywhere;
}
.report-mute-row { display: flex; align-items: center; gap: 6px; font-size: 13px; margin-top: 10px; cursor: pointer; }
/* The sent confirmation that replaces the report form. */
.report-done { text-align: center; padding: 30px 10px; font-size: 15px; }
/* Escape the app-wide `input { width:100% }` rule (the #761 fix's pattern) —
   a full-width checkbox becomes the whole flex row and shoves its label text
   clean out of the sheet. */
.report-mute-row input { width: 18px; height: 18px; flex: none; margin: 0; }
/* An admin warning's framing on the bubble (issue #578). */
.warn-tag { font-size: 12px; font-weight: 700; color: var(--danger); margin-bottom: 4px; }
/* Admin app: report queue + audit log (issue #578). */
.admin-report { border: 1px solid var(--border); border-radius: 12px; padding: 12px 14px; margin: 10px 0; }
.admin-report.resolved { opacity: 0.65; }
.admin-report-head { display: flex; justify-content: space-between; gap: 10px; align-items: baseline; }
.admin-report-when { font-size: 11px; color: var(--muted); white-space: nowrap; }
.admin-report-byline { font-size: 12px; color: var(--muted); margin-top: 2px; }
.admin-report-words { font-size: 13px; margin: 8px 0 0; overflow-wrap: anywhere; }
.admin-report-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.admin-report-detail { margin-top: 8px; display: flex; flex-direction: column; gap: 8px; }
.admin-audit-row {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  font-size: 13px;
  padding: 6px 0;
  border-bottom: 1px solid var(--border);
}
.admin-audit-row:last-child { border-bottom: none; }
.unread-dot { width: 8px; height: 8px; border-radius: 50%; background: transparent; flex: none; }
.unread-dot.on { background: var(--accent); }
/* Mobile list treatment (à la Messages/WhatsApp): rows run edge-to-edge, the active
   state is a full-width fill (no rounded bubble), and a hairline divider separates
   rows starting just after the avatar (not under it). */
@media (pointer: coarse) {
  .thread-list { padding: 0; gap: 0; }
  .thread-row { border-radius: 0; padding: 11px 16px; position: relative; }
  .thread-row::after {
    content: "";
    position: absolute;
    left: calc(16px + 52px + 12px); /* row padding-left + avatar width + gap */
    right: 0;
    bottom: 0;
    height: 1px;
    background: var(--border);
  }
  /* No divider under the last row, nor under the active row (its fill owns the edge). */
  .thread-row:last-child::after,
  .thread-row.active::after { display: none; }
}
.thread-pane { flex: 1; display: flex; min-height: 0; min-width: 0; position: relative; }
.empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--muted); gap: 6px; text-align: center; padding: 24px; }
.empty-art { font-size: 42px; }
/* min-width:0 lets these panes shrink below their content so a long unbroken
   URL wraps inside a bubble instead of widening the whole column. */
.thread-view { flex: 1; display: flex; flex-direction: column; min-height: 0; min-width: 0; width: 100%; position: relative; }
.thread-head { display: flex; align-items: center; gap: 6px; }
.thread-head-title { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
/* App-thread view toggle: a small segmented [💬 | ⏰] control beside the kebab —
   Chat (the DM with the app's bot) vs App (the interactive UI). */
.view-toggle { display: flex; gap: 2px; flex: none; background: var(--bubble-them); border-radius: 10px; padding: 2px; }
.view-toggle-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 30px; border: none; border-radius: 8px; background: transparent;
  font-size: 15px; line-height: 1; color: var(--ink); cursor: var(--ui-cursor, pointer);
}
.view-toggle-btn.active { background: var(--panel); box-shadow: 0 1px 4px var(--shadow); }

/* App-thread Views: the Chat pane (#app-pane) + the Interface pane
   (#interface-pane). A normal chat thread has only the chat pane — the interface
   pane stays empty and hidden, so it fills the width unchanged. Shown only in
   .app-mode; the framework CSS already gives each .app-pane its display/flex. */
.app-split { display: none; flex: 1 1 auto; min-height: 0; }
.thread-view.app-mode #app-split { display: flex; }
#interface-pane:empty { display: none; } /* a normal chat has no interface pane */
.view-toggle-btn.split-only { display: none; } /* Split is desktop-only (revealed below) */
.app-split-divider { display: none; } /* only ever shown in split view on desktop, below */
@media (min-width: 721px) {
  /* Desktop: three views — chat | split (both, the default) | interface. */
  #app-split { flex-direction: row; }
  #interface-pane { border-left: 1px solid var(--border); }
  .view-toggle-btn.split-only { display: inline-flex; }
  .thread-view[data-app-view="chat"] #interface-pane { display: none; }
  .thread-view[data-app-view="interface"] #app-pane { display: none; }
  /* Split: the divider between the panes is draggable (mouse/touch/pen) and
     keyboard-operable (arrow keys), and owns the separating line — so the
     plain border above is switched off here in favour of the divider's own. */
  .thread-view[data-app-view="split"] #app-pane {
    flex: 0 0 auto;
    width: calc(var(--split-ratio, 0.5) * 100%);
  }
  .thread-view[data-app-view="split"] #interface-pane {
    flex: 1 1 auto;
    border-left: none;
  }
  .thread-view[data-app-view="split"] #app-split-divider {
    display: flex; align-items: center; justify-content: center;
    flex: 0 0 11px; width: 11px; min-height: 0;
    cursor: col-resize; touch-action: none; position: relative;
  }
  .app-split-divider::before {
    content: ""; position: absolute; top: 0; bottom: 0; left: 50%;
    width: 1px; background: var(--border);
  }
  .app-split-divider::after {
    content: ""; width: 3px; height: 32px; border-radius: 2px;
    background: var(--border);
  }
  .app-split-divider:hover::after,
  .app-split-divider:focus-visible::after,
  .app-split-divider.dragging::after { background: var(--muted); }
  .app-split-divider:focus-visible { outline: none; } /* the grip itself shows focus */
  .app-split-divider:focus-visible::after { box-shadow: 0 0 0 2px var(--accent); }
}
@media (max-width: 720px) {
  /* Mobile: one pane, no split — chat/split → Chat, interface → Interface. */
  .thread-view.app-mode #interface-pane { display: none; }
  .thread-view[data-app-view="interface"] #app-pane { display: none; }
  .thread-view[data-app-view="interface"] #interface-pane { display: flex; }
}
.thread-title { font-family: var(--font-display); font-weight: 700; font-size: 16px; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.thread-sub { font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.back-btn { display: none; font-size: 24px; padding: 0 8px; }
.feed { flex: 1; overflow-y: auto; overflow-x: hidden; min-height: 0; min-width: 0; padding: 16px; display: flex; flex-direction: column; gap: 4px; }
.feed > .msg { max-width: 78%; }
.feed::before { content: ""; margin-top: auto; }

/* modal sheet (new chat / add device) — centered overlay */
.overlay {
  position: fixed; inset: 0; z-index: 60; background: rgba(20, 30, 80, 0.45);
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
.sheet { max-width: 460px; width: 100%; max-height: 82vh; overflow-y: auto; }
.sheet-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
/* A sheet's title spaces the same whether or not it's wrapped in a .sheet-head:
   most sheets write a bare <h2>, and the browser's default heading margin then
   pushed it far off the card's top padding. Both shapes now leave 10px below. */
.sheet-head h2 { margin: 0; }
.sheet > h2:first-child { margin: 0 0 10px; }
.linkish { background: transparent; color: var(--link); padding: 6px 8px; font-weight: 600; }
.sheet-input { margin-bottom: 10px; }
/* The quick forms ("Fill in a form instead"): labelled fields over the shared
   sheet inputs, an Extras group heading, and inline (never floating) errors. */
.qf-label { display: block; font-size: 13px; font-weight: 600; color: var(--ink); margin: 6px 2px 4px; }
.qf-note { margin: 2px 2px 0; }
.qf-error { margin: 8px 2px 0; font-size: 14px; color: var(--danger); }
.qf-error[hidden] { display: none; }
/* full-bleed list inside the sheet: rows span the card edges (which pads 28px) */
.pick-list { margin: 4px -28px 0; max-height: 56vh; overflow-y: auto; }
/* Picker sheets (new chat / add people) had nested scrolling — the sheet AND the
   .pick-list both scrolled — plus a stray horizontal scrollbar (overflow-y:auto
   promotes overflow-x to auto, so a sub-pixel overflow showed a bar). Make the
   sheet a clipped flex column so its head, search, and Apps footer stay put and
   ONLY the list scrolls: one clean scroll region. :has() scopes this to sheets
   that actually hold a pick-list, leaving every other modal untouched. */
.sheet:has(.pick-list) { display: flex; flex-direction: column; overflow: hidden; }
.sheet:has(.pick-list) > * { flex: none; }
.sheet:has(.pick-list) .pick-list {
  flex: 0 1 auto; min-height: 0; max-height: none;
  overflow-y: auto; overflow-x: hidden;
}
.list-row {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  background: transparent; color: var(--ink); border: none; border-bottom: 1px solid var(--border);
  padding: 14px 28px; border-radius: 0; font-weight: 500; font-size: 16px; cursor: pointer;
}
.list-row:hover:not(.static) { background: var(--bubble-them); }
.list-row.static { cursor: default; }
.list-row input { width: auto; }
/* The border is a BETWEEN-rows divider — the last row's would just float under
   the list (same rule the thread list applies to its rows). */
.pick-list .list-row:last-child { border-bottom: none; }
/* Helpers catalog: bots sit under their own picker heading, with a one-line blurb */
.pick-section {
  padding: 16px 28px 6px; font-size: 12px; font-weight: 700; color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.helper-desc { display: block; font-size: 13px; font-weight: 400; color: var(--muted); margin-top: 2px; }
/* member row: tappable identity on the left, a Remove action on the right */
.member-main {
  flex: 1; min-width: 0; display: flex; align-items: center; gap: 12px;
  background: transparent; color: inherit; border: none; text-align: left; font: inherit; padding: 0; cursor: pointer;
}
.member-main span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.member-remove { flex: none; background: transparent; color: var(--danger); font-weight: 600; font-size: 14px; padding: 4px 8px; }
.sheet-foot { display: flex; justify-content: flex-end; gap: 10px; margin-top: 16px; }
/* An inline failure reason in a sheet (#786) — occupies layout space above
   the buttons, never a floating toast. Colour is not the only signal: the
   role=alert announcement + the wording carry it. */
.sheet-error { color: var(--danger, #c0392b); font-size: 14px; margin: 10px 0 0; }

/* Open groups. Two pieces of vocabulary, both built from the sheet's existing
   language so community UI reads as native, not bolted on:
   - .opt-row: a choice with its consequence spelled out underneath (label sets
     the what, .opt-desc the so-what) — checkboxes stop needing run-on labels.
   - Discover rows: roster-style avatar + name + quiet facts, action on the
     right. Join is the sheet's ONE loud control (accent pill); everything a
     member already has (Open) stays quiet. */
.opt-row {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 10px 2px; cursor: pointer; font-weight: 550;
}
.opt-row input { width: auto; flex: none; margin-top: 4px; accent-color: var(--accent); }
.opt-desc { display: block; font-size: 13px; font-weight: 400; color: var(--muted); margin-top: 1px; }
/* A dependent option that can't apply right now (default needs open) dims as a
   unit — state is the row's opacity AND the disabled input, never color alone. */
.opt-row:has(input:disabled) { opacity: 0.45; cursor: default; }
.pill-btn {
  flex: none; background: var(--accent); color: var(--accent-ink);
  border-radius: 999px; padding: 7px 16px; font-weight: 650; font-size: 14px;
}
.pill-btn.quiet { background: var(--bubble-them); color: var(--ink); }
.discover-main { flex: 1; min-width: 0; }
.discover-main > div:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* theme picker (kebab → Theme) */
.appearance-seg { display: flex; gap: 6px; background: var(--bubble-them); border-radius: 12px; padding: 4px; }
.appearance-seg button { flex: 1; background: transparent; color: var(--muted); border-radius: 9px; padding: 9px; font-weight: 600; }
.appearance-seg button[aria-pressed="true"] { background: var(--panel); color: var(--ink); box-shadow: 0 1px 4px var(--shadow); }
.theme-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
.theme-swatch {
  display: flex; align-items: center; gap: 11px; text-align: left;
  background: var(--panel); color: var(--ink);
  border: 1px solid var(--border); border-radius: 14px; padding: 12px;
  font-weight: 600; font-family: var(--font-display);
}
.theme-swatch:hover { background: var(--bubble-them); }
.theme-swatch.active { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.theme-swatch .disc { width: 26px; height: 26px; border-radius: 50%; flex: none; box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12); }
/* "Text size" slider in the theme sheet (desktop only): A ——◦—— A */
.text-size-row { display: flex; align-items: center; gap: 12px; }
.text-size-row input[type="range"] { flex: 1; margin: 0; accent-color: var(--accent); }
.text-size-a { color: var(--muted); font-weight: 600; line-height: 1; }
.text-size-a.small { font-size: 12px; }
.text-size-a.big { font-size: 20px; }

/* mobile: show one pane at a time, toggled by .show-thread */
@media (max-width: 720px) {
  .sidebar { width: 100%; border-right: none; }
  .thread-pane { display: none; }
  .tt.show-thread .sidebar { display: none; }
  .tt.show-thread .thread-pane { display: flex; }
  .back-btn { display: inline-flex; }
}

/* Native wrapper (html.native, set from the "lchatMac" user agent): app chrome uses
   the normal arrow, not the web "hand" cursor. Content is untouched — message URLs and
   link-preview cards keep the pointer, images keep zoom-in.
   Buttons switch through the --ui-cursor variable so each button has just ONE cursor
   declaration (resolving to default) rather than a base `pointer` + an override — a
   WKWebView flickers when it has two competing values to choose between. The remaining
   non-button pointer bits still override directly (!important because #thread-sub sets
   its cursor via an inline JS style). Mirror new chrome cursors here. */
html.native { --ui-cursor: default; }
html.native #thread-sub,
html.native .toggle-row,
html.native .reply-count,
html.native .mention-link,
html.native .avatar.zoomable { cursor: default !important; }

/* The header is a window-drag region in the native wrapper, so its title text isn't
   selectable — don't show the text I-beam or a selection affordance on it. */
html.native .side-head,
html.native .thread-head {
  cursor: default;
  -webkit-user-select: none;
  user-select: none;
}

/* Native shell (iOS/macOS) embeds ONE thread's feed — the app draws its own
   sidebar, title bar, and composer. Hide the web sidebar / thread header / composer
   and let the feed fill the view (overriding the mobile one-pane-at-a-time rules).
   Gated on body.native-embed, which only shell.js sets when window.__lchatEmbed is
   injected; browsers are unaffected. */
.native-embed .sidebar { display: none !important; }
.native-embed .thread-pane { display: flex !important; }
.native-embed .thread-head,
.native-embed .composer,
.native-embed .composer-reply,
.native-embed .composer-edit {
  display: none;
}
/* Keep the attachment preview visible: it's how a staged photo shows up just above
   the native composer (drag-drop or the native + button stage into it). The composer
   below is hidden, so clear the native bar's height (plus the old 10px breathing room). */
.native-embed .composer-preview { padding-bottom: calc(10px + var(--native-bottom-inset, 0px)); }
/* The Interface view AND the Chat/takeover pane (both .app-pane scrollers —
   #interface-pane and #app-pane) run under the same floating native bars as
   the feed: pad their top with the injected inset so the app's own UI (To-do's
   add row, Reminders' form, a takeover app like Vault) starts clear of the
   header, and end with a real in-flow spacer — not padding-bottom, which
   WebKit drops at a scroller's end — so the last row clears the home
   indicator/keyboard. #app-pane hosting a nested chat mini-app instead
   (:has(> .chatapp-root) — a companion app's Chat view) is excluded: that
   nested .chatapp-feed already pads itself with the same vars, so double-
   padding the outer pane too would push it down/shrink it twice over. */
.native-embed #interface-pane,
.native-embed #app-pane:not(:has(> .chatapp-root)) {
  padding-top: calc(16px + var(--native-top-inset, 0px));
}
.native-embed #interface-pane::after,
.native-embed #app-pane:not(:has(> .chatapp-root))::after {
  content: "";
  flex: none;
  height: calc(16px + var(--native-bottom-inset, 0px));
}

/* Command-host embed (embed.mode === "command"): the page the native Mac shell
   keeps beside each thread purely to run slash commands — everything except a
   command's panel collapses, so the webview the shell reveals IS the panel.
   The shell owns the frame (and caps the height via the posted measure). */
body.command-host .chatapp-root > :not(.cmd-panel) { display: none !important; }
body.command-host .cmd-panel { border: none; box-shadow: none; } /* the native frame draws the border + shadow */
/* The shell grants the frame; the body scrolls within it (100vh = the frame). */
body.command-host .cmd-panel-body { max-height: calc(100vh - 40px); }
