/* ============================================================================
   MESSAGES - the conversation surface.

   Rows, day dividers, rich text, quotes, reactions, thread affordances,
   attachments, the lightbox and the composer. This is where 95% of the time is
   spent, so density is Slack's: tight vertical rhythm, one strong name/body
   hierarchy, colour reserved for the two things that matter (a mention of you,
   and a thread).

   Ownership notes (four sibling stylesheets are live at the same time):
     - .avatar, .pill*, .badge, .chip, button.icon/.ghost/.sm and the base
       input/textarea look belong to components.css. Only composer- and
       message-specific parts of those are touched here, always scoped.
     - section.msgs and #messages (the scroll container) belong to layout.css.
     - aside#panel chrome belongs to panels.css; the thread panel's *contents*
       are here.
   ========================================================================= */

:root {
  /* The accent used as TEXT needs more contrast than the accent used as a fill.
     Raw --c-accent is 4.3:1 on the light themes' code surface and only 4.6:1 on
     white, so it fails the moment it sits on a hovered row or a tinted pill.
     --c-accent-hover is already the "one step stronger" step of the same hue -
     darker on the light themes, lighter on dark - which is exactly what text
     needs, and it clears AA in all three (6.0 light / 7.4 dark / 8.6 colorful).
     Named for the role rather than aliased at each call site; the --msg- prefix
     keeps it from colliding with a sibling stylesheet. */
  --msg-accent-text: var(--c-accent-hover);
}

/* ============================================================================
   1. THE ROW
   ========================================================================= */

.msg {
  position: relative;
  display: flex;
  gap: var(--s-5);
  /* Full bleed, no radius: the hover fill should read as a band across the
     column the way Slack's does, not as a floating card. */
  padding: var(--s-2) var(--s-6);
  background: transparent;
  transition: background-color var(--m-fast) var(--m-ease);
}

/* A new author (or a new time block) gets air above it; consecutive messages
   from the same person close right up. That contrast is what makes a long
   channel scan as conversation rather than as a list. */
.msg:not(.grouped) { padding-top: var(--s-4); }
.msg.grouped { padding-block: var(--s-1); }

.msg:hover { background: var(--c-hover); }

/* Own messages are deliberately not tinted - the token is transparent in every
   theme. It stays wired up so a future theme can express it without a code change. */
.msg.me { background: var(--c-own); }

/* A mention of me: warm wash plus an edge marker, so it is findable when
   scrubbing a channel at speed. */
.msg.mention {
  background: var(--c-mention);
  box-shadow: inset 3px 0 0 var(--c-mention-edge);
}
/* Hover on a tinted row lays a second translucent wash over the same fill
   rather than mixing a new colour. It deepens the cream on the light themes and
   lifts the olive on dark - correct in all three - and it leaves
   background-color as the row's real, opaque mention colour. */
.msg.mention:hover {
  background-image: linear-gradient(var(--c-warn-quiet), var(--c-warn-quiet));
}

.msg.urgent {
  background: var(--c-danger-quiet);
  box-shadow: inset 3px 0 0 var(--c-danger);
}
.msg.urgent:hover {
  background-image: linear-gradient(var(--c-danger-quiet), var(--c-danger-quiet));
}

/* In flight: dimmed but still legible, and non-interactive so a stray tap
   cannot act on a message that has no id yet. */
.msg.pending { opacity: 0.6; }
.msg.pending .actions,
.msg.pending .rxns { pointer-events: none; }

.msg.failed {
  background: var(--c-danger-quiet);
  box-shadow: inset 3px 0 0 var(--c-danger);
}

.msg.deleted .body {
  color: var(--c-text-2);
  font-style: italic;
}

/* Jump-to highlight. Colour only - never animate layout. */
@keyframes msg-flash {
  0%, 35% { background-color: var(--c-selected); }
  100% { background-color: transparent; }
}
.msg.flash { animation: msg-flash 1.6s var(--m-ease); }

/* ---------------------------------------------------------------- gutter */
.gutter {
  width: var(--gutter);
  flex: none;
  display: flex;
  justify-content: center;
  /* 2px optical nudge so the avatar's mass lines up with the name's cap height. */
  padding-top: 2px;
}

/* The grouped-row timestamp: present in the layout at all times so nothing
   shifts, revealed on hover. */
.ghost-time {
  font-size: var(--t-2xs);
  font-variant-numeric: tabular-nums;
  line-height: var(--t-body);
  color: var(--c-text-2);
  user-select: none;
  /* Faded rather than colour:transparent - transparent text still claims a
     foreground colour, which reads as an unreadable element to contrast tools
     and to anything else inspecting the page. */
  opacity: 0;
  transition: opacity var(--m-fast) var(--m-ease);
}
.msg:hover .ghost-time { opacity: 1; }

/* ---------------------------------------------------------------- body column */
.mbody {
  flex: 1 1 auto;
  /* Without this a long word or a wide <pre> pushes the whole app sideways. */
  min-width: 0;
}

.mhead {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  flex-wrap: wrap;
  margin-bottom: 1px;
}

.who {
  font-size: var(--t-base);
  font-weight: var(--t-bold);
  line-height: var(--t-snug);
  color: var(--c-text);
  cursor: pointer;
  border-radius: var(--r-xs);
}
.who:hover { text-decoration: underline; }
.who:active { opacity: 0.75; }

.msg .t {
  font-size: var(--t-xs);
  font-weight: var(--t-normal);
  font-variant-numeric: tabular-nums;
  color: var(--c-text-2);
  white-space: nowrap;
}

/* ============================================================================
   2. DAY DIVIDER - a centred label sitting on a hairline rule.
   ========================================================================= */

.daydiv {
  /* Slack's signature: the date rides along the top of the viewport while you
     scroll through that day, so you never lose track of when you are. It is a
     direct child of the scroll container (#messages, .thread-msgs), which is
     what sticky needs. Below the action bar's z-index so a hovered row's
     controls still win where the two meet. */
  position: sticky;
  top: 0;
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  gap: var(--s-5);
  margin: var(--s-7) var(--s-6) var(--s-5);
}
.daydiv::before,
.daydiv::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background: var(--c-border);
}
.daydiv span {
  flex: none;
  padding: var(--s-2) var(--s-5);
  border: 1px solid var(--c-border);
  border-radius: var(--r-full);
  background: var(--c-bg);
  color: var(--c-text-2);
  font-size: var(--t-xs);
  font-weight: var(--t-semibold);
  line-height: var(--t-tight);
  white-space: nowrap;
}

/* ============================================================================
   3. RICH TEXT
   ========================================================================= */

.body {
  min-width: 0;
  font-size: var(--t-md);
  line-height: var(--t-body);
  color: var(--c-text);
  /* Pasted URLs and hashes must break rather than widen the column. */
  word-break: break-word;
  overflow-wrap: anywhere;
}

.body p { margin: 0 0 var(--s-2); }
.body p:last-child { margin-bottom: 0; }

.body strong, .body b { font-weight: var(--t-semibold); }

.body h1, .body h2, .body h3 {
  margin: var(--s-4) 0 var(--s-2);
  line-height: var(--t-snug);
}
.body h1 { font-size: var(--t-lg); }
.body h2 { font-size: var(--t-md); }
.body h3 { font-size: var(--t-base); }
.body > :first-child { margin-top: 0; }

.body a {
  color: var(--msg-accent-text);
  text-decoration: underline;
  text-underline-offset: 2px;
  /* A quiet underline at rest, solid on hover - links stay identifiable
     without striping a whole paragraph. */
  text-decoration-color: color-mix(in srgb, var(--c-accent) 40%, transparent);
  transition: color var(--m-fast) var(--m-ease);
}
/* Slack's link hover: the underline firms up rather than the colour shifting.
   Any colour shift here would have to move the opposite way per theme, and a
   solid underline reads the same in all three. */
.body a:hover { text-decoration-color: currentColor; }

.body ul, .body ol {
  margin: var(--s-2) 0;
  padding-left: var(--s-7);
}
.body li { margin: var(--s-1) 0; }
.body li::marker { color: var(--c-text-2); }

.body blockquote {
  margin: var(--s-3) 0;
  padding: var(--s-1) 0 var(--s-1) var(--s-5);
  border-left: 3px solid var(--c-border-strong);
  color: var(--c-text-2);
}

.body code {
  padding: 1px var(--s-2);
  border: 1px solid var(--c-border-subtle);
  border-radius: var(--r-xs);
  background: var(--c-code-bg);
  color: var(--c-text);
  font-size: 0.88em;
}

.body pre {
  margin: var(--s-3) 0;
  padding: var(--s-5) var(--s-6);
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  background: var(--c-code-bg);
  /* The block scrolls inside itself. Nothing here may widen the page. */
  max-width: 100%;
  overflow-x: auto;
  overscroll-behavior-x: contain;
}
.body pre code {
  display: block;
  padding: 0;
  border: none;
  background: none;
  white-space: pre;
  line-height: var(--t-snug);
}

.body table {
  /* display:block is what makes a wide table scroll in its own box; markdown
     hands us no wrapper element to do it properly. */
  display: block;
  max-width: 100%;
  overflow-x: auto;
  border-collapse: collapse;
  margin: var(--s-3) 0;
  font-size: var(--t-sm);
}
.body th, .body td {
  border: 1px solid var(--c-border);
  padding: var(--s-2) var(--s-5);
  text-align: left;
}
.body th {
  background: var(--c-surface-2);
  font-weight: var(--t-semibold);
  white-space: nowrap;
}

.body hr {
  margin: var(--s-5) 0;
  border: none;
  border-top: 1px solid var(--c-border);
}

.body img {
  border-radius: var(--r-sm);
  margin: var(--s-2) 0;
}

/* ---------------------------------------------------------------- inline tags */
.tag {
  padding: 0 var(--s-1);
  border-radius: var(--r-xs);
  background: var(--c-accent-quiet);
  color: var(--msg-accent-text);
  font-weight: var(--t-medium);
}

/* A mention OF ME. Colour alone will not do here: it has to survive sitting on
   an already-tinted .msg.mention row, so it gets a fill AND a ring, and keeps
   full body-text contrast rather than an amber foreground that fails AA on the
   light themes. */
.tag-me {
  /* Two layers rather than one mixed colour: the solid mention fill stays the
     element's real background-color (so anything reading the page still sees a
     legitimate opaque colour behind the text) and the amber wash rides on top
     as an image. The ring is what separates it from a .msg.mention row, which
     is already this exact fill. */
  background-color: var(--c-mention);
  background-image: linear-gradient(var(--c-warn-quiet), var(--c-warn-quiet));
  box-shadow: inset 0 0 0 1px var(--c-mention-edge);
  color: var(--c-text);
  font-weight: var(--t-semibold);
}

.chanlink {
  padding: 0 2px;
  border-radius: var(--r-xs);
  color: var(--msg-accent-text);
  font-weight: var(--t-medium);
  cursor: pointer;
}
.chanlink:hover {
  background: var(--c-accent-quiet);
  text-decoration: underline;
}
.chanlink:active { opacity: 0.75; }

.edited {
  color: var(--c-text-2);
  font-size: var(--t-xs);
}

.send-error {
  color: var(--c-danger);
  font-size: var(--t-sm);
  font-weight: var(--t-medium);
}

/* A send that did not land. Deliberately NOT danger-coloured: the message is
   still on screen, still readable, and one button fixes it. Red here reads as
   "you have lost something", which is not what happened. */
.send-state {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-1);
  color: var(--c-text-2);
  font-size: var(--t-xs);
}
.send-state .ico { width: 13px; height: 13px; flex: none; }
.send-state button {
  padding: var(--s-1) var(--s-3);
  font-size: var(--t-xs);
  border-radius: var(--r-sm);
}

/* ---------------------------------------------------------------- syntax
   The old palette was hard-coded for a dark background and went unreadable in
   light mode. These derive from the theme's own semantic colours and are then
   mixed toward --c-text, which darkens them on the light themes and lightens
   them on dark - one declaration, AA in all three. */
.body pre {
  --msg-syn-key: color-mix(in srgb, color-mix(in srgb, var(--c-accent) 72%, var(--c-danger)) 68%, var(--c-text));
  --msg-syn-str: color-mix(in srgb, var(--c-success) 65%, var(--c-text));
  --msg-syn-num: color-mix(in srgb, var(--c-warn) 68%, var(--c-text));
  --msg-syn-fn: color-mix(in srgb, var(--c-accent) 68%, var(--c-text));
  --msg-syn-tag: color-mix(in srgb, var(--c-danger) 68%, var(--c-text));
  --msg-syn-type: color-mix(in srgb, color-mix(in srgb, var(--c-warn) 70%, var(--c-danger)) 68%, var(--c-text));
}
.hljs-keyword, .hljs-selector-tag, .hljs-built_in, .hljs-meta { color: var(--msg-syn-key); }
.hljs-string, .hljs-attr, .hljs-addition, .hljs-regexp { color: var(--msg-syn-str); }
.hljs-number, .hljs-literal, .hljs-symbol, .hljs-bullet { color: var(--msg-syn-num); }
.hljs-title, .hljs-function .hljs-title, .hljs-section { color: var(--msg-syn-fn); }
.hljs-attribute, .hljs-name, .hljs-tag, .hljs-deletion { color: var(--msg-syn-tag); }
.hljs-type, .hljs-class .hljs-title, .hljs-params { color: var(--msg-syn-type); }
.hljs-comment, .hljs-quote { color: var(--c-text-2); font-style: italic; }
.hljs-emphasis { font-style: italic; }
.hljs-strong { font-weight: var(--t-semibold); }

/* ============================================================================
   4. QUOTE / REPLY CONTEXT
   ========================================================================= */

.quote {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  min-width: 0;
  margin-bottom: var(--s-2);
  padding: 1px var(--s-4);
  border-left: 2px solid var(--c-border-strong);
  color: var(--c-text-2);
  font-size: var(--t-sm);
  line-height: var(--t-snug);
  cursor: pointer;
  transition: color var(--m-fast) var(--m-ease), border-color var(--m-fast) var(--m-ease);
}
.quote:hover {
  border-left-color: var(--c-accent);
  color: var(--c-text);
}
.quote:active { opacity: 0.75; }
.quote b {
  flex: none;
  color: var(--c-text);
  font-weight: var(--t-semibold);
}
/* The snippet is the only part allowed to shrink. */
.quote > span:last-child {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.thread-origin {
  margin-bottom: var(--s-1);
  color: var(--c-text-2);
  font-size: var(--t-xs);
}

/* ============================================================================
   5. HOVER ACTION BAR
   ========================================================================= */

.actions {
  position: absolute;
  top: calc(-1 * var(--s-4));
  right: var(--s-6);
  z-index: var(--z-sticky);
  display: flex;
  align-items: center;
  gap: var(--s-1);
  padding: var(--s-1);
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  background: var(--c-overlay);
  box-shadow: var(--e-2);
  /* Hidden rather than display:none so it can fade and lift into place. */
  opacity: 0;
  visibility: hidden;
  transform: translateY(2px);
  transition: opacity var(--m-fast) var(--m-ease),
              transform var(--m-fast) var(--m-out),
              visibility var(--m-fast) var(--m-ease);
}
.msg:hover > .actions,
.msg:focus-within > .actions {
  opacity: 1;
  visibility: visible;
  transform: none;
}

.actions button {
  min-width: 32px;
  height: 32px;
  padding: 0 var(--s-2);
  border-radius: var(--r-sm);
  font-size: var(--t-base);
  line-height: 1;
}
.actions .quick { font-size: var(--t-md); }

/* The topmost row has nothing above it to overlap and the list clips its own
   overflow, so tuck the bar inside instead of hanging it over the edge. */
#messages > .msg:first-child > .actions,
.thread-msgs > .msg:first-child > .actions { top: var(--s-2); }

/* ============================================================================
   6. REACTIONS
   ========================================================================= */

.rxns {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-top: var(--s-3);
}
.rxns:empty { display: none; }

.rxn {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-height: 26px;
  padding: 0 var(--s-4);
  border: 1px solid var(--c-border);
  border-radius: var(--r-full);
  background: var(--c-surface-2);
  color: var(--c-text);
  font-size: var(--t-sm);
  font-weight: var(--t-normal);
  font-family: var(--t-emoji), var(--t-font);
  line-height: 1;
  cursor: pointer;
  transition: background-color var(--m-fast) var(--m-ease),
              border-color var(--m-fast) var(--m-ease),
              transform var(--m-fast) var(--m-spring);
}
.rxn:hover {
  background: var(--c-surface-3);
  border-color: var(--c-border-strong);
}
.rxn:active { transform: scale(0.94); }

.rxn b {
  font-family: var(--t-font);
  font-size: var(--t-xs);
  font-weight: var(--t-bold);
  font-variant-numeric: tabular-nums;
  color: var(--c-text-2);
}

/* "I reacted" has to be unmistakable at a glance: accent fill, accent edge,
   accent count. */
.rxn.mine {
  background: var(--c-accent-quiet);
  border-color: var(--c-accent);
}
.rxn.mine b { color: var(--msg-accent-text); }
/* A second coat of the same quiet accent - deeper, still the same hue. */
.rxn.mine:hover {
  background-image: linear-gradient(var(--c-accent-quiet), var(--c-accent-quiet));
}

/* The + affordance stays quiet until the row is engaged. */
.rxn-add {
  color: var(--c-text-2);
  font-weight: var(--t-semibold);
  opacity: 0;
  transition: opacity var(--m-fast) var(--m-ease),
              background-color var(--m-fast) var(--m-ease);
}
.msg:hover .rxn-add,
.msg:focus-within .rxn-add,
.rxn-add:focus-visible { opacity: 1; }

/* ============================================================================
   7. THREAD INDICATOR - the product's marquee affordance. It has to look like a
   door, not like a caption.
   ========================================================================= */

.threadind {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  min-height: 30px;
  margin-top: var(--s-3);
  padding: 0 var(--s-4) 0 var(--s-3);
  border: 1px solid transparent;
  border-radius: var(--r-full);
  color: var(--msg-accent-text);
  font-size: var(--t-sm);
  font-weight: var(--t-semibold);
  cursor: pointer;
  transition: background-color var(--m-fast) var(--m-ease),
              border-color var(--m-fast) var(--m-ease);
}
.threadind:hover {
  background: var(--c-accent-quiet);
  border-color: color-mix(in srgb, var(--c-accent) 35%, transparent);
}
.threadind:active {
  background-image: linear-gradient(var(--c-accent-quiet), var(--c-accent-quiet));
}

.th-dot {
  width: 7px;
  height: 7px;
  flex: none;
  border-radius: var(--r-full);
  background: var(--c-accent);
  /* A soft halo so the dot reads as live conversation rather than a bullet. */
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-accent) 22%, transparent);
}

.th-open {
  color: var(--c-text-2);
  font-weight: var(--t-medium);
  transition: color var(--m-fast) var(--m-ease);
}
.threadind:hover .th-open { color: var(--msg-accent-text); }

/* ============================================================================
   8. THREAD PANEL INTERNALS
   ========================================================================= */

.thread-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding-bottom: var(--s-4);
  margin-bottom: var(--s-4);
  border-bottom: 1px solid var(--c-border);
}
.thread-chan {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--t-sm);
  font-weight: var(--t-semibold);
}
.thread-tools {
  display: flex;
  gap: var(--s-3);
  flex: none;
}

.thread-msgs {
  display: flex;
  flex-direction: column;
}
/* The panel is narrow; claw back the channel's generous side padding. */
.thread-msgs .msg { padding-inline: var(--s-4); }

/* The message the thread hangs off. --c-surface-2 alone is only a few percent
   away from the panel behind it, so the card also takes a hairline: this row is
   context, not another reply, and that has to be unmistakable. */
.thread-root {
  padding: var(--s-4);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface-2);
}
.thread-root:hover { background: var(--c-surface-3); }

.thread-sep {
  display: flex;
  align-items: center;
  margin: var(--s-5) var(--s-4);
}
.thread-sep span {
  flex: 1 1 auto;
  height: 1px;
  background: var(--c-border);
}

/* ---------------------------------------------------------------- thread composer */
.thread-composer {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: var(--s-2) var(--s-2) var(--s-3);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--r-lg);
  background: var(--c-bg);
  transition: border-color var(--m-fast) var(--m-ease), box-shadow var(--m-fast) var(--m-ease);
}
.thread-composer:focus-within {
  border-color: var(--c-accent);
  box-shadow: 0 0 0 1px var(--c-accent);
}
.thread-composer textarea {
  width: 100%;
  max-height: 160px;
  padding: var(--s-3) var(--s-4);
  border: none;
  border-radius: 0;
  background: transparent;
  color: var(--c-text);
  font: inherit;
  line-height: var(--t-snug);
  resize: none;
  outline: none;
}
/* Same as the main composer: the surface carries the focus ring, not the field. */
.thread-composer textarea:focus { box-shadow: none; }

.thread-composer-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding-inline: var(--s-2);
}

/* "Also send to #channel" decides whether a thread feels safe to use. It gets a
   real hit area, a real checked state, and the channel name at full text
   contrast - never a hairline afterthought. */
.alsosend {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  min-height: 36px;
  min-width: 0;
  padding: var(--s-2) var(--s-4);
  border: 1px solid transparent;
  border-radius: var(--r-md);
  color: var(--c-text-2);
  font-size: var(--t-sm);
  line-height: var(--t-snug);
  cursor: pointer;
  user-select: none;
  transition: background-color var(--m-fast) var(--m-ease),
              border-color var(--m-fast) var(--m-ease),
              color var(--m-fast) var(--m-ease);
}
.alsosend:hover {
  background: var(--c-surface-2);
  color: var(--c-text);
}
.alsosend input {
  width: 18px;
  height: 18px;
  flex: none;
  margin: 0;
  padding: 0;
  accent-color: var(--c-accent);
  cursor: pointer;
}
.alsosend b {
  color: var(--c-text);
  font-weight: var(--t-semibold);
}
.alsosend span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Checked is a state worth announcing: this reply is about to become public. */
.alsosend:has(input:checked) {
  background: var(--c-accent-quiet);
  border-color: color-mix(in srgb, var(--c-accent) 45%, transparent);
  color: var(--c-text);
}

/* ---------------------------------------------------------------- thread cards */
.thread-card {
  padding: var(--s-5);
  margin-bottom: var(--s-4);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-bg);
  cursor: pointer;
  transition: background-color var(--m-fast) var(--m-ease),
              border-color var(--m-fast) var(--m-ease),
              box-shadow var(--m-fast) var(--m-ease);
}
.thread-card:hover {
  background: var(--c-surface);
  border-color: var(--c-border-strong);
  box-shadow: var(--e-1);
}
.thread-card:active { background: var(--c-surface-2); }
/* Unread reads as a spine on the left; the padding compensates so the text does
   not jump when the card is read. */
.thread-card.unread {
  border-left: 3px solid var(--c-accent);
  padding-left: calc(var(--s-5) - 2px);
}

.tc-head {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  margin-bottom: var(--s-3);
  font-size: var(--t-sm);
}
.tc-time {
  margin-left: auto;
  flex: none;
  white-space: nowrap;
}
.tc-root {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  min-width: 0;
  font-size: var(--t-base);
  color: var(--c-text);
}
.tc-root b { flex: none; font-weight: var(--t-semibold); }
.tc-root span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--c-text-2);
}
.tc-foot {
  margin-top: var(--s-3);
  font-size: var(--t-sm);
  font-weight: var(--t-medium);
}

/* ============================================================================
   9. ATTACHMENTS
   ========================================================================= */

.atts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-4);
  margin: var(--s-4) 0 var(--s-1);
  min-width: 0;
}

/* The inline aspect-ratio from media.js reserves the box, so nothing shifts
   when the signed URL resolves and the bitmap finally arrives. */
.att-img,
.att-vid {
  position: relative;
  max-width: 100%;
  overflow: hidden;
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface-2);
  cursor: pointer;
  transition: border-color var(--m-fast) var(--m-ease), transform var(--m-fast) var(--m-ease);
}
.att-img:hover { border-color: var(--c-border-strong); }
.att-img:active { transform: scale(0.99); }
.att-img img,
.att-vid video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.att-vid { cursor: default; }

.att-broken {
  cursor: default;
  border-style: dashed;
}
.att-broken::after {
  content: "Image unavailable";
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--c-text-2);
  font-size: var(--t-sm);
}

.att-aud {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  max-width: 100%;
  min-width: 0;
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface);
}
.att-aud audio {
  width: 260px;
  max-width: 100%;
  height: 34px;
}
.att-aud span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--t-sm);
}

.att-file {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  min-height: 44px;
  max-width: 100%;
  min-width: 0;
  padding: var(--s-3) var(--s-5);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface);
  color: var(--c-text);
  font-size: var(--t-sm);
  cursor: pointer;
  transition: background-color var(--m-fast) var(--m-ease), border-color var(--m-fast) var(--m-ease);
}
.att-file:hover {
  background: var(--c-surface-2);
  border-color: var(--c-accent);
}
.att-file:active { background: var(--c-surface-3); }
.att-file > span:nth-child(2) {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: var(--t-medium);
}
.att-ico {
  flex: none;
  font-size: var(--t-lg);
  line-height: 1;
}

/* ============================================================================
   10. LIGHTBOX
   ========================================================================= */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: var(--z-lightbox);
  display: flex;
  align-items: center;
  justify-content: center;
  touch-action: none;
  /* Two coats of the scrim token: one coat is tuned for dialogs and is far too
     sheer to sit a photograph on. Stacking composites it to a proper blackout
     without inventing a colour. */
  background-color: var(--c-scrim);
  background-image: linear-gradient(var(--c-scrim), var(--c-scrim));
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.lb-stage {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.lightbox img {
  max-width: 94vw;
  max-height: 84dvh;
  object-fit: contain;
  border-radius: var(--r-md);
  box-shadow: var(--e-4);
  transform-origin: center;
  cursor: grab;
}
.lightbox img:active { cursor: grabbing; }

.lb-bar {
  position: fixed;
  top: max(var(--s-5), env(safe-area-inset-top));
  right: max(var(--s-6), env(safe-area-inset-right));
  display: flex;
  gap: var(--s-3);
}
.lb-bar button {
  min-width: 44px;
  min-height: 44px;
}

.lb-nav {
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  padding: 0;
  border: 1px solid var(--c-border);
  border-radius: var(--r-full);
  background: var(--c-overlay);
  color: var(--c-text);
  font-size: var(--t-2xl);
  line-height: 1;
  box-shadow: var(--e-2);
  transition: background-color var(--m-fast) var(--m-ease), transform var(--m-fast) var(--m-ease);
}
.lb-nav:hover { background: var(--c-surface-2); }
.lb-nav:active { transform: translateY(-50%) scale(0.94); }
.lb-prev { left: max(var(--s-5), env(safe-area-inset-left)); }
.lb-next { right: max(var(--s-5), env(safe-area-inset-right)); }

.lb-count {
  position: fixed;
  bottom: max(var(--s-7), env(safe-area-inset-bottom));
  left: 50%;
  transform: translateX(-50%);
  padding: var(--s-2) var(--s-5);
  border-radius: var(--r-full);
  /* A pill rather than bare text: --c-text-inverse would vanish against the
     scrim in dark mode, so the count carries its own surface. */
  background: var(--c-overlay);
  color: var(--c-text);
  font-size: var(--t-sm);
  font-weight: var(--t-medium);
  font-variant-numeric: tabular-nums;
  box-shadow: var(--e-2);
}
.lb-count:empty { display: none; }

/* ============================================================================
   11. COMPOSER
   ========================================================================= */

#typing {
  /* Reserved height: the composer must not hop every time somebody types. */
  min-height: 18px;
  padding: 0 var(--s-6);
  color: var(--c-text-2);
  font-size: var(--t-sm);
  line-height: 18px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.receipt {
  padding: 0 var(--s-6) var(--s-1);
  font-size: var(--t-xs);
  text-align: right;
}
.receipt:empty { display: none; }

#composerBar {
  position: relative;
  flex: none;
  padding: var(--s-2) var(--s-6) max(var(--s-5), env(safe-area-inset-bottom));
  background: var(--c-bg);
}

/* One rounded surface that contains its own tools - the input is the box and
   the buttons live inside it. */
.crow {
  display: flex;
  align-items: flex-end;
  gap: var(--s-2);
  padding: var(--s-2);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--r-lg);
  background: var(--c-bg);
  transition: border-color var(--m-fast) var(--m-ease), box-shadow var(--m-fast) var(--m-ease);
}
.crow:focus-within {
  border-color: var(--c-accent);
  box-shadow: 0 0 0 1px var(--c-accent);
}

.crow textarea {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 28px;
  max-height: 220px;
  padding: var(--s-3) var(--s-2);
  border: none;
  border-radius: 0;
  background: transparent;
  color: var(--c-text);
  font: inherit;
  line-height: var(--t-snug);
  resize: none;
  outline: none;
}
/* components.css gives every field its own accent border and 3px halo on focus.
   Correct for a standalone input, wrong here: the .crow surface already IS the
   field, so that halo draws a second box inside the first. The surface keeps the
   focus indication; only the inner ring goes. */
.crow textarea:focus { box-shadow: none; }
.crow textarea:disabled {
  color: var(--c-text-2);
  cursor: not-allowed;
}
/* Nowhere to send to yet: the whole surface recedes instead of looking broken. */
.crow:has(textarea:disabled) {
  background: var(--c-surface);
  border-color: var(--c-border);
}

.crow button {
  flex: none;
  min-width: 36px;
  height: 36px;
  padding: 0 var(--s-3);
  border-radius: var(--r-md);
}

.ctools {
  display: flex;
  align-items: center;
  gap: var(--s-1);
  /* Absorb shortfall by scrolling inside itself instead of squeezing the
     textarea to nothing. The phone block below already did this; a narrow
     desktop window with a mouse hit the same wall and got a one-letter-wide
     composer, so the rule is unconditional now. */
  flex: 1 1 0;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}

/* The send arrow's resting look is components.css's (button.icon.send). The one
   thing added here is the commitment: once there is something to send, the
   button stops being a quiet glyph and becomes a filled button.
   :placeholder-shown is the only signal for "has text" available without
   touching the JS. */
.crow:has(#composer:not(:disabled):not(:placeholder-shown)) .send {
  background: var(--c-accent);
  color: var(--c-accent-text);
}
.crow:has(#composer:not(:disabled):not(:placeholder-shown)) .send:hover {
  background: var(--c-accent-hover);
}

/* ---------------------------------------------------------------- reply bar */
#replyBar {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  min-width: 0;
  margin-bottom: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border-left: 3px solid var(--c-accent);
  border-radius: var(--r-sm);
  background: var(--c-surface-2);
  color: var(--c-text-2);
  font-size: var(--t-sm);
}
#replyBar b { color: var(--c-text); font-weight: var(--t-semibold); }
.reply-ico {
  flex: none;
  color: var(--msg-accent-text);
  font-weight: var(--t-bold);
}
.reply-snip {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------------------------------------------------------------- attach chips
   .chip itself is components.css - only the composer-specific parts live here. */
#attachChips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-bottom: var(--s-3);
}
#attachChips .chip {
  position: relative;
  overflow: hidden;
  max-width: 100%;
}
#attachChips .chip-name {
  max-width: 160px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#attachChips .chip-prog {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  background: var(--c-accent);
  transition: width var(--m-base) var(--m-ease);
}
#attachChips .chip .x {
  flex: none;
  padding: 0 var(--s-1);
  color: var(--c-text-2);
  cursor: pointer;
  transition: color var(--m-fast) var(--m-ease);
}
#attachChips .chip .x:hover { color: var(--c-danger); }
#attachChips .chip.failed { border-color: var(--c-danger); }

/* ---------------------------------------------------------------- drop hint */
#dropHint {
  position: fixed;
  inset: var(--s-5);
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px dashed var(--c-accent);
  border-radius: var(--r-xl);
  /* Near-opaque via opacity rather than a translucent fill, so the element keeps
     a real background-color - the label has to stay measurable against it. */
  background: var(--c-bg);
  opacity: 0.97;
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  color: var(--msg-accent-text);
  font-size: var(--t-xl);
  font-weight: var(--t-semibold);
  text-align: center;
  pointer-events: none;
}

/* ---------------------------------------------------------------- new below
   Placed against section.msgs. The offset has to clear the composer plus the
   typing and receipt strips that sit above it. */
.new-below {
  position: absolute;
  left: 50%;
  bottom: calc(var(--s-12) + var(--s-9) + env(safe-area-inset-bottom));
  transform: translateX(-50%);
  z-index: var(--z-sticky);
  padding: var(--s-3) var(--s-6);
  border-radius: var(--r-full);
  font-size: var(--t-sm);
  box-shadow: var(--e-3);
}
.new-below:active { transform: translateX(-50%) scale(0.96); }

/* ---------------------------------------------------------------- autocomplete */
.acpop {
  position: absolute;
  bottom: 100%;
  left: var(--s-6);
  right: var(--s-6);
  z-index: var(--z-dropdown);
  max-height: 300px;
  margin-bottom: var(--s-3);
  padding: var(--s-2);
  overflow-y: auto;
  overscroll-behavior: contain;
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-overlay);
  box-shadow: var(--e-3);
}

.ac-row {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  min-height: 36px;
  padding: var(--s-2) var(--s-4);
  border-radius: var(--r-md);
  color: var(--c-text);
  font-size: var(--t-base);
  cursor: pointer;
  transition: background-color var(--m-fast) var(--m-ease);
}
.ac-row:hover { background: var(--c-surface-2); }
/* Keyboard selection is a different state from mouse hover and has to stay
   readable when the two land on different rows. */
.ac-row.sel {
  background: var(--c-selected);
  box-shadow: inset 2px 0 0 var(--c-accent);
}
.ac-ico {
  flex: none;
  width: 20px;
  text-align: center;
  color: var(--c-text-2);
}
.ac-label {
  flex: none;
  font-weight: var(--t-medium);
}
.ac-hint {
  min-width: 0;
  margin-left: auto;
  padding-left: var(--s-4);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--c-text-2);
  font-size: var(--t-sm);
}

/* ============================================================================
   12. PHONE - the primary target.
   ========================================================================= */

@media (max-width: 860px) {
  .msg { padding-inline: var(--s-5); }
  .daydiv { margin-inline: var(--s-5); }

  .body { font-size: var(--t-base); }

  /* Media is capped by the column, never by a desktop pixel width. */
  .att-img, .att-vid { max-width: 100%; }
  .att-aud audio { width: 200px; }

  #typing, .receipt { padding-inline: var(--s-5); }
  #composerBar { padding: var(--s-2) var(--s-5) max(var(--s-5), env(safe-area-inset-bottom)); }
  /* The phone composer stacks the field above the tools, so it is ~30px taller
     than the desktop one. Measured at 390px the pill was landing on top of the
     typing indicator; this clears the whole strip. */
  .new-below { bottom: calc(var(--s-12) + var(--s-11) + env(safe-area-inset-bottom)); }
  .acpop {
    left: var(--s-5);
    right: var(--s-5);
    max-height: 42dvh;
  }

  .lb-nav { width: 44px; height: 44px; font-size: var(--t-xl); }
  .lightbox img { max-width: 100vw; max-height: 78dvh; }

  .thread-msgs .msg { padding-inline: var(--s-3); }
  .thread-composer-row { flex-wrap: wrap; }
  .alsosend { flex: 1 1 auto; }

  /* A text field plus six 44px targets does not fit across a phone: the field
     collapses to about 12px and the placeholder renders one letter per line.
     So the composer stacks the way Slack's phone composer does - the field
     takes a full-width line of its own, the tools sit underneath it. */
  .crow {
    flex-wrap: wrap;
    align-items: center;
    row-gap: var(--s-1);
  }
  .crow textarea {
    order: -1;
    flex: 1 0 100%;
    padding-inline: var(--s-3);
    /* iOS Safari zooms the whole page whenever a focused field's font is under
       16px. components.css already raises generic inputs to 17px under this
       breakpoint, but `font: inherit` up in section 10 resets THIS field back to
       body size - the one box everyone types into, zooming on every focus. */
    font-size: 17px;
  }
  .crow .send { margin-left: auto; }
  /* Feature modules can register any number of composer tools, and the fixed
     controls alone (attach, emoji, send at 44px each plus gaps) already need
     284px of a 360px screen's 266px row. With an auto basis the row wrapped and
     the send button dropped onto a third line, making the composer 180px tall.
     A zero basis lets the optional tools absorb every shortfall by scrolling
     inside themselves, so the essential three never move and the row never
     wraps at any width. */
  .ctools {
    flex: 1 1 0;
    min-width: 0;
    overflow-x: auto;
    scrollbar-width: none;
  }
}

/* ============================================================================
   13. TOUCH - there is no hover, so every hover-revealed affordance needs a tap
   path or it does not exist on a phone.
   ========================================================================= */

@media (hover: none) {
  /* There is no hover on a phone, so a hover-revealed bar simply does not exist
     and something has to be permanently present.

     What does NOT work - both measured at 390px, where the body column is 258px:
       - the whole bar (six quick emoji plus every registered action) is a ~340px
         strip hanging over the message above it;
       - three or four actions plus a fixed-width notch is ~150px of opaque panel
         whose width depends on what the action registry happens to hold. It
         drifts wider than the notch and then covers names, timestamps and words.
         Worse, .body pre / .body table / .atts each establish their own
         formatting context in order to scroll their own overflow, and a
         formatting-context box SHRINKS to dodge a float instead of flowing
         around it - so every code block and table rendered at a third width.

     So the bar keeps exactly two controls, and it is sized to fit in the empty
     tail of the name/time line rather than over the message:

       1. react - the emoji picker. .rxns is empty until a message already HAS a
                  reaction, so without this there is no way to add the first one.
       2. more  - the ⋯ overflow menu.

     Reply-in-thread, quote reply and every feature action stay on the long-press
     menu messages.js wires to every row through oncontextmenu, which is the
     native idiom on this form factor. Opening an existing thread keeps a
     full-size tap target of its own further down (.threadind).

     Both selectors are anchored rather than chained, so neither walks off the
     end when a feature module registers another action: buildMessage always
     emits the QUICK_EMOJI run first with the picker immediately after it, and
     `more` is registered at order 900 and is therefore always last.

     Everything in this block names .msg rather than styling .actions bare. That
     is not specificity for its own sake: later stylesheets re-state
     `.actions { padding; box-shadow }` and `button:has(.ico){display:inline-flex}`
     unconditionally, which tie with the bare selectors and win on order, putting
     the card and all six buttons back. The bar only ever exists inside a row, so
     naming the row is accurate and settles it without !important. */
  .msg > .actions {
    opacity: 1;
    visibility: visible;
    transform: none;
    top: 0;
    right: var(--s-3);
    gap: 0;
    padding: 0;
    /* No panel. Two quiet glyphs sitting in the row's own whitespace read as
       part of the message; a bordered, shadowed card repeated down every single
       row reads as clutter and was the loudest thing on the screen. */
    border-color: transparent;
    background: transparent;
    box-shadow: none;
  }
  .msg .actions > button { display: none; }
  .msg .actions .quick + button:not(.quick),
  .msg .actions > button:last-child {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 40px;
    height: var(--s-9);
    padding: 0;
    font-size: var(--t-md);
  }
  /* 40x32 is what fits beside a name and a timestamp. The tap target reaches a
     full 44x44 through a transparent extension instead, so ergonomics cost
     nothing in layout. Each button grows AWAY from its neighbour - the react
     button leftwards, the overflow button rightwards into the row's own padding
     - so the two hit areas tile instead of overlapping and stealing each
     other's taps. */
  .msg .actions .quick + button:not(.quick)::after,
  .msg .actions > button:last-child::after {
    content: "";
    position: absolute;
    inset: -6px 0;
  }
  .msg .actions .quick + button:not(.quick)::after { left: -4px; }
  .msg .actions > button:last-child::after { right: -4px; }

  /* Reserve the corner the bar occupies. The bar's own box is 80x32, but the
     reserve is deliberately larger than that on both axes: a later stylesheet
     re-adds padding to .actions, and a notch sized exactly to today's bar would
     start overlapping text the moment it grows by 2px. On a row with a header
     the name and time are far shorter than the column, so nothing moves at all;
     on a grouped row the first line or two shorten and the rest of the message
     runs the full width. */
  .msg .mbody::before {
    content: "";
    float: right;
    width: 88px;
    height: var(--s-10);
  }
  /* The formatting-context boxes described above. Clearing puts them below the
     notch at full width instead of letting them shrink beside it. */
  .body pre,
  .body table,
  .atts,
  .rxns { clear: right; }

  /* Reactions keep their compact look but gain a 44px tall hit area from a
     transparent extension, so density does not pay for ergonomics. */
  .rxns { gap: var(--s-3); margin-top: var(--s-4); }
  .rxn { min-height: 34px; padding-inline: var(--s-5); }
  .rxn::after {
    content: "";
    position: absolute;
    inset: -5px -2px;
  }
  .rxn-add { opacity: 1; }

  /* The thread door is a primary action, and on touch it is now the ONLY way to
     open a thread without going through the long-press menu - so it gets the
     full 44px tap minimum and a visible edge, not the 40px it had. */
  .threadind {
    min-height: 44px;
    margin-top: var(--s-4);
    padding-right: var(--s-5);
    padding-left: var(--s-4);
    border-color: var(--c-border);
    background: var(--c-surface);
  }

  .quote { padding-block: var(--s-2); }

  .crow button { min-width: 44px; height: 44px; }
  /* box-sizing is border-box, so min-height IS the tap target - 48px clears the
     44px floor with the padding inside it. The previous 36px min-height plus
     8px of padding either side measured 52px and gave the composer a 108px
     footprint, an eighth of an iPhone screen, for one empty line. */
  .crow textarea { min-height: var(--s-11); padding-block: var(--s-3); }
  .alsosend { min-height: 44px; }
  .alsosend input { width: 20px; height: 20px; }
  .ac-row { min-height: 44px; }
  .att-file { min-height: 48px; }

  /* The grouped-row time stays hidden on touch. It cannot be revealed by hover
     and a full "05:57 PM" wraps to two lines inside a 40px gutter, which breaks
     the tight grouped rhythm for a value the group's first row already shows. */
}
