/* Apps::Chat — view-specific styles. */

/*
 * Online presence dot
 *
 * Avoid Bootstrap's `.rounded-circle` here — custom.css overrides that
 * utility with `min-width: 40px; min-height: 40px;`, which inflates the
 * dot to avatar size. This dedicated class keeps the dot tiny and
 * positioned at the avatar's bottom-right corner.
 *
 * Visibility is JS-driven: server renders with `.d-none`; the
 * `apps--chat--online-status` Stimulus controller toggles `.is-online`
 * (and `.d-none`) per user based on /apps/chat/presence/online.
 */
.chat-online-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background-color: var(--bs-success, #198754); /* bg-success */
  border: 1px solid #fff;
  pointer-events: none;
}

.chat-online-dot--lg {
  width: 10px;
  height: 10px;
  border-width: 2px;
}

/*
 * Reactions row
 *
 * Server-rendered initial state lives in
 * `app/views/apps/chat/messages/_reaction_pills.html.erb`. Live updates land
 * via Pusher `message:reacted` and replace the row's innerHTML wholesale.
 * Mirrors the visual language of `_platform_reaction_bar` (small pill with
 * emoji + count) but without the platform partial's tooltip / picker
 * scaffolding — the chat picker has its own floating pill UI.
 */
.chat-message__reactions {
  min-height: 1.75rem;
}

.chat-message__reactions form.d-inline {
  margin: 0;
  padding: 0;
  display: inline-flex;
}

.chat-reaction-pill {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--bs-border-color, #dee2e6);
  background: #fff;
  line-height: 1;
  font-size: 0.875rem;
}

.chat-reaction-pill--mine {
  background: rgba(var(--bs-success-rgb, 25, 135, 84), 0.12); /* bg-success-subtle */
  border-color: rgba(25, 135, 84, 0.5);
  color: #157347;
}

/* Reactions obey the same im_permission gate as sending (P5). The class
 * lands on the room container (rooms/show.html.erb) rather than inside
 * _reaction_pills because the Pusher message:reacted broadcast swaps that
 * partial's HTML viewer-agnostically — an ancestor class is the only gate
 * that survives the swap. Own pills stay clickable: removing your own
 * reaction is allowed (mirrors message deletion), so only the add-picker
 * and other people's pills go inert. The server 403s either way — this
 * just stops presenting controls that can't work. */
.chat-room--no-react .chat-reaction-add {
  display: none;
}

.chat-room--no-react .chat-reaction-pill:not(.chat-reaction-pill--mine) {
  pointer-events: none;
}

.chat-reaction-pill__emoji {
  font-size: 1rem;
}

.chat-reaction-pill__count {
  font-size: 0.8125rem;
}

/*
 * Add-reaction trigger + floating picker pill
 *
 * The trigger is a small icon button kept visible inside `.chat-message`
 * but de-emphasized; on hover (or when the message itself is hovered) it
 * lights up. The floating pill is positioned above the trigger by inline
 * styles in the partial; this rule only covers visual chrome.
 */
.chat-reaction-add-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border-radius: 999px;
  background: transparent;
  border: 1px solid transparent;
  color: var(--bs-secondary-color, #6c757d);
  opacity: 0.55;
  transition: opacity 120ms ease, background-color 120ms ease;
}

.chat-reaction-add-btn:hover,
.chat-reaction-add-btn:focus,
.chat-message:hover .chat-reaction-add-btn {
  opacity: 1;
  background: var(--bs-light-bg-subtle, #f8f9fa);
  border-color: var(--bs-border-color, #dee2e6);
}

.chat-reaction-add-btn__plus {
  position: absolute;
  bottom: -2px;
  right: -2px;
  font-size: 0.55rem;
  background: #fff;
  border-radius: 999px;
  padding: 1px;
  line-height: 1;
}

/*
 * #404526 "UI: Like bar" — the Add-reaction popover was cut off at the right
 * edge of the conversation.
 *
 * It was anchored `left: 0` by an inline style, i.e. it always opened rightward
 * from the ☺+ trigger. On a SENT message the trigger sits near the right edge of
 * the column, so the bar ran past it and the last emoji was unreachable. Sent
 * messages therefore anchor from the trigger's RIGHT edge and open leftward;
 * received messages keep the existing left anchor.
 *
 * The side comes from `chat-message--mine`, already on the row's <li>, rather
 * than from a partial-local: 'message:reacted' replaces only this partial's HTML
 * via innerHTML, so a local would have to be recomputed by the live renderer too
 * — the <li> is untouched by that swap.
 */
.chat-reaction-picker {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  left: 0;
}

/* Sent (right-aligned) messages: open leftward from the trigger's right edge. */
.chat-message--mine .chat-reaction-picker {
  left: auto;
  right: 0;
}

.chat-reaction-picker .reaction-pill-btn {
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border-radius: 999px;
  transition: background-color 120ms ease, transform 120ms ease;
}

.chat-reaction-picker .reaction-pill-btn:hover {
  background: var(--bs-light-bg-subtle, #f8f9fa);
  transform: scale(1.15);
}

.chat-reaction-picker .reaction-pill-btn--mine {
  background: rgba(var(--bs-success-rgb, 25, 135, 84), 0.18);
}

.chat-reaction-picker .reaction-emoji {
  font-size: 1.1rem;
  line-height: 1;
}

/*
 * Attachments row + cards
 *
 * Server-rendered initial state lives in
 * `app/views/apps/chat/messages/_attachments.html.erb`. Live-broadcast
 * state is rendered by `room_controller.js#_renderAttachments`. The two
 * MUST emit byte-aligned HTML for these styles to apply identically.
 *
 * Image attachments render the :preview variant (800px max dimension via
 * libvips) inline, capped to a max-width that fits the message column.
 * Non-image attachments render as a small card with FA icon + filename
 * link + size.
 */
.chat-message__attachments {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.chat-attachment {
  max-width: 100%;
}

.chat-attachment--image {
  max-width: 360px;
}

.chat-attachment__img {
  display: block;
  max-width: 100%;
  max-height: 320px;
  height: auto;
  border-radius: 8px;
  object-fit: cover;
}

.chat-attachment__caption {
  margin-top: 4px;
  font-size: 0.75rem;
  /* Long, unbroken filenames (e.g. underscore_heavy_names) must wrap inside the
     image/video attachment column instead of overflowing the message bubble. */
  overflow-wrap: anywhere;
  word-break: break-word;
}

.chat-attachment--file {
  max-width: 320px;
  background: #fff;
}

/* The file card's filename column uses `min-w-0` in markup. Bootstrap 5 ships
   no such utility, and for a long time this project didn't define one either on
   desktop, so the inner flex child kept the default `min-width: auto`, refused
   to shrink, and the `.text-truncate` filename overflowed the 320px card and
   broke out of the message column. `.min-w-0` is now a real global utility
   (custom.css), so this rule is a belt-and-braces duplicate — it is kept
   because it selects `.flex-grow-1` rather than the class, and so still covers
   the card if the markup ever drops `min-w-0`.
   Applies to server-, optimistic-, and live-rendered cards (same markup). */
.chat-attachment--file .flex-grow-1 {
  min-width: 0;
}

.chat-attachment__name {
  text-decoration: none;
  font-size: 0.9rem;
  color: var(--bs-body-color, #212529);
}

.chat-attachment__name:hover {
  text-decoration: underline;
}

/*
 * Optimistic-bubble variants — the local createObjectURL preview lives
 * here while the upload is in flight. The faded look hints at "not yet
 * sent" without flashing a hard placeholder. Once the live message
 * arrives, the entire optimistic <li> is replaced (see
 * room_controller.js#_onMessageCreated) and `URL.revokeObjectURL` is
 * fired on each `data-blob-object-url="true"` <img> so the browser
 * frees the file from memory.
 */
.chat-attachment--optimistic {
  opacity: 0.85;
}

/*
 * Whole-room drag-drop overlay (Slack/Discord pattern)
 *
 * Driven by `apps--chat--drop-zone` Stimulus controller. Hidden by
 * default; the controller toggles `.is-active` (and Bootstrap's
 * `.d-none`) on dragenter / dragleave / drop. Sits absolutely over the
 * room container so the user can drop anywhere inside the room — not
 * just on the small dropzone in the attachments panel.
 */
.chat-drop-overlay {
  position: absolute;
  inset: 0;
  z-index: 1050;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(var(--bs-primary-rgb, 13, 110, 253), 0.10);
  border: 3px dashed rgba(var(--bs-primary-rgb, 13, 110, 253), 0.55);
  border-radius: 0.5rem;
  pointer-events: none; /* allow the underlying drop event to fire on the room */
  transition: opacity 120ms ease;
}

.chat-drop-overlay__inner {
  text-align: center;
  color: #0a58ca;
  background: rgba(255, 255, 255, 0.92);
  padding: 1.5rem 2rem;
  border-radius: 0.5rem;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}

.chat-drop-overlay__title {
  font-size: 1.25rem;
  font-weight: 600;
}

.chat-drop-overlay__hint {
  margin-top: 0.25rem;
  color: #495057;
}

/*
 * Inline replies (desktop-messenger parity — QA task 404525 items 7/9)
 *
 * Replies are ordinary timeline messages carrying a quoted-parent chip
 * (.chat-reply-snippet) inside their bubble — a port of the desktop
 * client's ReplySnippet (mango-messenger ReplySnippet.scss): 4px theme
 * accent bar on the left, 8px radius, 4px/8px padding, name on its own
 * line ABOVE the single-line ellipsized excerpt (QA follow-up on item 7:
 * name and message must not share a line). The --composer variant is the
 * SAME chip shown above the input while a reply is armed — it keeps a flex
 * row so the ✕ dismiss sits to the right of the stacked name/preview.
 */

.chat-reply-snippet {
  display: block;
  padding: 4px 8px;
  margin-bottom: 4px;
  background: var(--bs-secondary-bg, #e6e9ed);
  border-left: 4px solid var(--bs-primary, #0d6efd);
  border-radius: 8px;
  font-size: 0.85rem;
  max-width: 100%;
}

.chat-reply-snippet[role="button"] {
  cursor: pointer;
}

.chat-reply-snippet--composer {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.chat-reply-snippet__name,
.chat-reply-snippet__text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.chat-reply-snippet__text {
  color: var(--bs-secondary-color, #6c757d);
}

/*
 * Important / Read-Receipt ack bar (desktop MessageAckBar parity — QA task
 * 404525 item 19). Lives inside the bubble above the reply chip/body. The
 * action button (.chat-ack-action-btn) is the desktop's .btn-badge-size —
 * a pill shrunk to badge scale so it optically matches the adjacent
 * "Important Message" badge.
 */
.chat-ack-action-btn {
  --bs-btn-padding-y: 0.1rem;
  --bs-btn-padding-x: 0.6rem;
  --bs-btn-font-size: 0.75rem;
  line-height: 1.4;
}

/*
 * Composer send-option toggles (⭐ Set as Important / ✓ Set Read Receipt).
 *
 * ISS-20260811-355-C358C7: the row was `gap-3` + `p-0` icon-only buttons, so
 * each button was exactly as wide as its own glyph — 17px for the star, 15px
 * for the receipt, i.e. two DIFFERENT widths — with 20px of dead space
 * between them. More gap than icon, and no button chrome to own that space,
 * so the pair read as a control with something missing between them.
 *
 * They now use this app's own icon-button footprint (.chat-reaction-add-btn
 * above): a uniform 28x28 centred target with hover/focus chrome, packed
 * tight so the two read as one group, and a glyph at the same 0.875rem the
 * adjacent paperclip uses so the composer's action icons match in size.
 * 28x28 also lifts the click target over the 24px minimum (it was 17x22).
 *
 * No border-radius here on purpose: the theme's `body.mgx-restyle .btn`
 * (custom.css, specificity 0-2-1) already sets --mgx-ctl-radius and outranks
 * a `.chat-composer-toolbar .btn` rule, so the hover chip picks up the same
 * corner radius as every other control in the app.
 *
 * Colour is deliberately NOT set here — room_controller#_paintToggle flips
 * the .text-secondary/.text-primary utilities to paint the armed state, and
 * those are !important; adding a colour here would fight the armed flip.
 */
.chat-composer-toolbar {
  gap: 2px;
}

.chat-composer-toolbar .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  flex-shrink: 0;
  border: 1px solid transparent;
  text-decoration: none;
  transition: background-color 120ms ease, border-color 120ms ease;
}

.chat-composer-toolbar .btn i {
  font-size: 0.875rem;
  line-height: 1;
}

.chat-composer-toolbar .btn:hover,
.chat-composer-toolbar .btn:focus-visible {
  background: var(--bs-light-bg-subtle, #f8f9fa);
  border-color: var(--bs-border-color, #dee2e6);
}

/*
 * "Enter to send, Shift+Enter for newline" — the guidance that used to ride
 * in the composer placeholder. It moved here because the placeholder wrapped
 * inside a rows=1, overflow-y:auto box below ~1300px, and an unscrolled
 * second line paints as a half-height grey smudge against the inside of the
 * bottom border (ISS-20260812-092-89E8C9).
 *
 * `min-width: 0` is the declaration that makes the `.text-truncate` utility
 * on this span do anything: a flex item defaults to `min-width: auto`, so
 * without it the span refuses to shrink below its own text and widens the
 * row instead of ellipsising. flex-shrink is left at its default so this
 * line yields first — the 28×28 toggles above already set flex-shrink: 0
 * and keep their size.
 */
.chat-composer-hint {
  min-width: 0;
}

/*
 * Backstop for the same defect: a rows=1 textarea is exactly one line tall,
 * but a placeholder that wraps still LAYS OUT its second line, and since the
 * box is never scrolled while empty that line paints half-height against the
 * inside of the bottom border. Pinning the placeholder to one line makes that
 * shape impossible no matter what copy lands here later.
 *
 * The shipped placeholder ("Message…") fits at 768px, the narrowest width
 * this view reaches — tablets are not redirected to /m/ — so this rule is
 * inert today and only engages if the string grows again. That is the whole
 * point: ISS-20260812-092-89E8C9 was introduced by lengthening the string.
 * A horizontal clip at the right edge reads as text continuing past the box;
 * the vertical half-cut read as a broken border, which is why it was filed.
 *
 * Scoped to the composer, and to ::placeholder only — the typed value must
 * still wrap normally, which is what the textarea is for. Both the WebKit
 * and standard pseudo-elements are listed as separate rules on purpose: an
 * unrecognised selector invalidates the whole selector list.
 */
.chat-composer .form-control::placeholder {
  white-space: nowrap;
  overflow: hidden;
}

.chat-composer .form-control::-webkit-input-placeholder {
  white-space: nowrap;
  overflow: hidden;
}

/*
 * Composer textarea while the @mention highlight overlay is mounted
 * (mention_autocomplete_controller#_setupHighlight adds this class). The
 * mirror <div> that paints the mention chips sits at z-index 1 BEHIND a
 * transparent-background textarea, so the textarea has to stay above it.
 *
 * These three declarations used to be written INLINE by that controller,
 * which made them unbeatable — an inline `z-index: 2` outranks the theme's
 * `.input-group > .form-control:focus { z-index: 5 }`, so a focused composer
 * tied with `.input-group .btn` (also z-index 2) and painting fell back to
 * DOM order: the later Send button covered the right edge of the focus
 * border + 0.25rem focus ring while the left edge showed. The composer's
 * outline therefore changed shape the moment you clicked in and typed
 * (ISS-20260811-186-9BB2CB). As a class the focus rule wins again and the
 * ring closes evenly on both sides, exactly like every other input-group
 * in the app.
 *
 * The transparent background is re-asserted at :focus on purpose:
 * `.form-control:focus` repaints the control white, which would hide the
 * mention chips at precisely the moment the user is typing them.
 */
.chat-composer-input {
  position: relative;
  z-index: 2;
}

.chat-composer-input.form-control,
.chat-composer-input.form-control:focus {
  background-color: transparent;
}

/*
 * The composer is ONE control: [attach] [textarea] [Send]. Bootstrap's
 * input-group pulls each segment 1px left onto its neighbour so the three
 * share a single perimeter — which means their border colours have to agree
 * or the outline of that one control changes colour partway along itself.
 * `.btn-outline-secondary` paints #616971 and `.form-control` paints
 * #dee2e6, so what shipped was a dark cap welded to the left of an
 * otherwise light-bordered field, in EVERY state — empty, focused, and with
 * text (ISS-20260811-186-9BB2CB, reopened because the earlier focus-ring
 * fix left this untouched).
 *
 * Deliberately scoped to the composer: `btn-outline-secondary` inside an
 * input-group is used ~190 times across the app, and repainting all of them
 * is a design-system decision, not a bug fix.
 *
 * Overriding the --bs-btn-* variables rather than `border-color` keeps every
 * other btn-outline-secondary behaviour (sizing, focus shadow, disabled) and
 * lets dark mode follow --bs-border-color instead of a hardcoded hex. The
 * selector carries .input-group as well so it outranks the theme's
 * `[data-theme="dark"] .btn-outline-secondary`, which is same-specificity
 * and injected after this file.
 *
 * Hover/active keep the border colour and tint the background instead —
 * matching `.chat-composer-toolbar .btn:hover` above, and keeping the
 * perimeter constant in every state the ticket asked about. The filled Send
 * segment is left alone: a filled primary button closing an input group is
 * the app-wide idiom and that accent is deliberate.
 */
.input-group.chat-composer > .btn-outline-secondary {
  --bs-btn-border-color: var(--bs-border-color, #dee2e6);
  --bs-btn-hover-border-color: var(--bs-border-color, #dee2e6);
  --bs-btn-active-border-color: var(--bs-border-color, #dee2e6);
  --bs-btn-hover-bg: var(--bs-tertiary-bg, #f8f9fa);
  --bs-btn-hover-color: var(--bs-secondary-color, #616971);
  --bs-btn-active-bg: var(--bs-tertiary-bg, #f8f9fa);
  --bs-btn-active-color: var(--bs-secondary-color, #616971);
}

/*
 * The theme paints `color: #fff` on every `.btn-outline-*:hover` / `:active`
 * with a literal declaration (one shared 80-selector rule), so the
 * --bs-btn-hover-color variable above never reaches it and the light hover
 * tint would carry a WHITE, invisible paperclip. Restated here with `.btn` in
 * the selector so it outranks that rule (0,5,0 vs 0,4,0) — no !important.
 */
.input-group.chat-composer > .btn.btn-outline-secondary:hover,
.input-group.chat-composer > .btn.btn-outline-secondary:active {
  color: var(--bs-secondary-color, #616971);
}

/*
 * Attachment file-card icon chip (desktop MediaMetaData parity — QA task
 * 404525 item 15): 32×32 rounded chip, 20px glyph, per-type tint (the
 * desktop's custom.scss values: Word #1766cf, PDF #f25123, Excel #0c8345,
 * PowerPoint #bd3718; everything else theme primary).
 */
.chat-attachment__icon-chip {
  width: 32px;
  height: 32px;
  border-radius: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--bs-secondary-bg, #e6e9ed);
}

.chat-attachment__icon-chip i {
  font-size: 20px;
  color: var(--bs-primary, #0d6efd);
}

.chat-attachment__icon-chip .fa-file-word       { color: #1766cf; }
.chat-attachment__icon-chip .fa-file-pdf        { color: #f25123; }
.chat-attachment__icon-chip .fa-file-excel      { color: #0c8345; }
.chat-attachment__icon-chip .fa-file-powerpoint { color: #bd3718; }

/* Inline audio player (desktop CustomAudioPlayer analogue) — pill-sized. */
.chat-attachment__audio {
  max-width: min(360px, 100%);
  height: 44px;
  display: block;
}

/*
 * Image lightbox (#chatImageViewerModal — desktop ImageViewerModal parity,
 * QA task 404525 item 15). transform-driven zoom/pan; the stage crops
 * overflow and shows grab cursors while panning.
 */
.chat-image-viewer__stage {
  cursor: grab;
}

.chat-image-viewer__stage--grabbing {
  cursor: grabbing;
}

.chat-image-viewer__img {
  max-width: 100%;
  max-height: 100%;
  user-select: none;
  transition: transform 0.05s linear;
}

.chat-image-viewer__zoom {
  width: 180px;
}

/*
 * Jump-to-parent highlight — the 5s ring the desktop client paints on the
 * target bubble (.message-bubble-highlight, box-shadow 0 0 0 2px links
 * color). Applied to the whole row so attachment-only parents ring too.
 */
.chat-message--jump-highlight .chat-message__body,
.chat-message--jump-highlight .chat-message__attachments {
  box-shadow: 0 0 0 2px var(--bs-primary, #0d6efd) !important;
  border-radius: 8px;
}

/*
 * Message rows — match the MangoApps desktop messenger client.
 *
 * The server partial (app/views/apps/chat/messages/_message.html.erb) and the
 * live JS renderer (room_controller.js #_renderMessage / #_renderOptimistic)
 * emit the SAME shape, so these rules style both render paths identically
 * (KEEP IN LOCKSTEP):
 *
 *     <li class="chat-message chat-message--mine|--theirs">
 *       <div class="chat-message__meta"> sender name </div>      ← above bubble
 *       <div class="chat-message__row">                          ← horizontal
 *         <div class="chat-message__body"> … </div>              ← the bubble
 *         <div class="chat-message__hover">                      ← hover cluster
 *           <div class="chat-message__reactions">…</div>            pills + ☺+
 *           <div class="chat-message__menu dropdown">…</div>        ⋮ menu
 *           <div class="chat-message__time"><time>…</time> ✓</div>  time + tick
 *         </div>
 *       </div>
 *       [attachments] [replies-chip] [seen-by]
 *     </li>
 *
 * Layout: the <li> is a flex column (meta, row, attachments, chip, seen-by).
 * `.chat-message__row` is a full-width horizontal flex — bubble on the outer
 * edge, hover cluster on the inner side. Mine mirrors theirs via
 * `flex-direction: row-reverse`, so the cluster order (reactions → menu → time)
 * reads as time → menu → reactions on the sent side, like the reference client.
 * Reaction pills in the cluster show at rest; the cluster's on-demand chrome
 * (⋮ menu, time + tick, ☺+ trigger) is hidden until hover/focus and reserves
 * its width so revealing it never reflows the timeline.
 */
.chat-message {
  display: flex;
  flex-direction: column;
  align-items: flex-start; /* theirs: hug the left edge */
  max-width: 100%;
  margin-bottom: 0.4rem;
}

.chat-message:last-child {
  margin-bottom: 0;
}

.chat-message--mine {
  align-items: flex-end; /* mine: hug the right edge */
}

/* Sender name above the bubble — group/channel received messages only. */
.chat-message__meta {
  margin-bottom: 0.15rem;
  padding: 0 0.25rem;
  font-size: 0.75rem;
  line-height: 1.2;
}

.chat-message__author {
  font-weight: 600;
  color: var(--bs-body-color, #212529);
}

/* Own messages + 1:1 DMs don't repeat a name on every bubble (alignment / tint
   signal "mine"; the DM header already names the other person). The room
   container carries the type attribute, so the DM rule needs no DOM change. */
.chat-message--mine .chat-message__meta,
[data-apps--chat--room-room-type-value="direct"] .chat-message__meta {
  display: none;
}

/* The horizontal row: bubble + hover cluster. */
.chat-message__row {
  display: flex;
  width: 100%;
  align-items: center;
  gap: 0.5rem;
}

.chat-message--mine .chat-message__row {
  flex-direction: row-reverse;
}

/* The bubble. max-width leaves ~170px for the hover cluster so the row never
   overflows the (narrower) group/channel column. */
.chat-message__body {
  max-width: min(560px, calc(100% - 170px));
  padding: 0.45rem 0.7rem;
  border-radius: 12px 12px 12px 4px; /* theirs: squared tail bottom-left */
  background: var(--bs-secondary-bg, #f1f3f5);
  color: var(--bs-body-color, #212529);
  text-align: left;
  line-height: 1.4;
  word-break: break-word;
  overflow-wrap: anywhere;
}

.chat-message--mine .chat-message__body {
  border-radius: 12px 12px 4px 12px; /* mine: squared tail bottom-right */
  background: var(--bs-primary-bg-subtle, #cfe2ff);
}

/* render_message_body runs simple_format → <p>. Strip the default trailing
   margin; keep a small gap between paragraphs in multi-line messages. */
.chat-message__body p {
  margin-bottom: 0;
}

.chat-message__body p + p {
  margin-top: 0.4rem;
}

/*
 * Hover cluster — reactions + ⋮ menu + time/tick.
 *
 * The CLUSTER itself is always visible so that reactions somebody already left
 * are visible at rest: the pills live inside it, and hiding the whole cluster
 * meant a reaction (and every live `message:reacted` update) produced no
 * visible change at all unless you happened to hover that exact bubble.
 *
 * What stays hover-only is the on-demand CHROME — the ⋮ menu, the timestamp +
 * read tick, and the ☺+ add-reaction trigger. Each is hidden with `opacity: 0`
 * (not display:none) so it still reserves its width and the reveal remains
 * reflow-free. Mirrored for mine via row-reverse.
 *
 * ACCESSIBILITY — this used to be `visibility: hidden` alongside the opacity,
 * and that made Reply / Edit / Delete / React UNREACHABLE BY KEYBOARD, full
 * stop: `visibility: hidden` takes an element out of the tab order, and it
 * refuses even a programmatic .focus() (verified in the browser — activeElement
 * did not move). The `:focus-within` reveal below was therefore unreachable
 * from the very controls it exists to reveal: nothing inside the cluster could
 * hold focus, so the selector could only ever fire from some other focusable
 * element in the bubble. A WCAG 2.1.1 (Keyboard, Level A) failure that looked
 * fixed because the selector was present.
 *
 * `opacity: 0` keeps them focusable, so Tab reaches them and `:focus-within`
 * then reveals the cluster around the focused control.
 *
 * Known tradeoff: this adds ~2 tab stops per message (the ⋮ toggle and the ☺+
 * trigger), so tabbing from the top of a 50-message timeline to the composer is
 * now a long trip. That is the correct trade against controls that cannot be
 * reached at all, and the proper fix for the length is a roving tabindex over
 * the list (one stop per message, arrow keys within) — a build, not a CSS
 * change, and worth doing if the focus order draws complaints.
 */
.chat-message__hover {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
}

.chat-message--mine .chat-message__hover {
  flex-direction: row-reverse;
}

.chat-message__hover .chat-message__menu,
.chat-message__hover .chat-message__time,
.chat-message__hover .chat-reaction-add {
  opacity: 0;
  transition: opacity 0.12s ease;
}

.chat-message:hover .chat-message__hover .chat-message__menu,
.chat-message:hover .chat-message__hover .chat-message__time,
.chat-message:hover .chat-message__hover .chat-reaction-add,
.chat-message:focus-within .chat-message__hover .chat-message__menu,
.chat-message:focus-within .chat-message__hover .chat-message__time,
.chat-message:focus-within .chat-message__hover .chat-reaction-add,
.chat-message:has(.dropdown-menu.show) .chat-message__hover .chat-message__menu,
.chat-message:has(.dropdown-menu.show) .chat-message__hover .chat-message__time,
.chat-message:has(.dropdown-menu.show) .chat-message__hover .chat-reaction-add,
.chat-message:has(.chat-reaction-picker:not([hidden])) .chat-message__hover .chat-message__menu,
.chat-message:has(.chat-reaction-picker:not([hidden])) .chat-message__hover .chat-message__time,
.chat-message:has(.chat-reaction-picker:not([hidden])) .chat-message__hover .chat-reaction-add {
  opacity: 1;
}

/* Inline timestamp + sent tick. */
.chat-message__time {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.75rem;
  color: var(--bs-secondary-color, #6c757d);
  white-space: nowrap;
}

/*
 * Message status ladder (desktop parity — QA task 404525 items 13/18):
 *   clock (--pending, #8696a0)  → in-flight / failed (failed adds opacity)
 *   single ✓ (base, #717171)    → stored on the server ("Sent")
 *   grey ✓✓ (--delivered)       → reached a recipient's client (client-
 *                                 delivered Pusher client event, live-only)
 *   teal ✓✓ (--seen, #008b8b)   → read (message:read). Declared LAST so it
 *                                 wins when both classes are present — read
 *                                 outranks delivered.
 * Colors are the desktop client's exact values (ChatItem.scss: #8696a0
 * clock, $secondary #717171 sent, #008b8b msg-seen-check).
 */
.chat-message__tick {
  font-size: 0.7rem;
  color: #717171;
}

.chat-message__tick--pending {
  color: #8696a0;
}

.chat-message__tick--delivered {
  color: #717171;
}

.chat-message__tick--seen {
  color: #008b8b;
}

/*
 * [seen-by] — the persistent read-receipt attribution under the sender's own
 * 1:1 messages ("Seen by Alice").
 *
 * The ✓✓ tick above carries the same state, but it lives inside
 * `.chat-message__time`, which this file holds at opacity:0 until the row is
 * hovered (see the hover-cluster rules above). With the tick as the ONLY
 * carrier the sender saw nothing at all after their message was read, and the
 * reader's name was reachable only by hovering and waiting for a native title
 * tooltip — ISS-20260817-778-F4E9F9. This line is always visible; the tick
 * stays for desktop parity.
 *
 * Rendered on the NEWEST seen own message only, and only in 1:1 rooms — a
 * group/channel gets no slot (Seen is a 1:1-only status here). `align-self`
 * works because `.chat-message` is a flex column.
 */
.chat-message__seen-by {
  align-self: flex-end;
  margin-top: 0.125rem;
  padding: 0 0.125rem;
  font-size: 0.6875rem;
  line-height: 1.25;
  color: var(--bs-secondary-color, #6c757d);
}

/* The slot ships hidden and is unhidden by the server render or by
   room_controller.js#_paintSeenByLine; nothing above sets `display`, but be
   explicit so a future layout rule can't strand a visible empty slot. */
.chat-message__seen-by[hidden] {
  display: none;
}

/* Control buttons (⋮ menu + ☺+ add-reaction) — small boxed icon buttons. The
   add-reaction button also has base rules higher up (round pill); these later
   rules win and square it off to match the menu button. */
.chat-message__menu-btn,
.chat-reaction-add-btn {
  width: 28px;
  height: 28px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 6px;
  border: 1px solid var(--bs-border-color, #dee2e6);
  background: var(--bs-body-bg, #fff);
  color: var(--bs-secondary-color, #6c757d);
  line-height: 1;
  opacity: 1;
}

.chat-message__menu-btn:hover,
.chat-message__menu-btn:focus,
.chat-reaction-add-btn:hover,
.chat-reaction-add-btn:focus,
.chat-message:hover .chat-reaction-add-btn {
  background: var(--bs-tertiary-bg, #f8f9fa);
  border-color: var(--bs-border-color, #dee2e6);
  color: var(--bs-body-color, #212529);
}

.chat-message__menu .dropdown-menu {
  font-size: 0.875rem;
  min-width: 11rem;
}

/* The reactions row lives inside the inline cluster — drop its reserved
   min-height and the _reaction_pills wrapper's top margin so the controls sit
   centred on the bubble baseline. */
.chat-message__hover .chat-message__reactions {
  min-height: 0;
}

.chat-message__hover .chat-message__reactions > div {
  margin-top: 0 !important;
}

/* Optimistic (in-flight) bubble dims until the echo replaces it; a send failure
   tints it red. `--optimistic` / `--failed` set by room_controller.js. */
.chat-message--optimistic .chat-message__body {
  opacity: 0.7;
}

.chat-message--failed .chat-message__body {
  background: var(--bs-danger-bg-subtle, #f8d7da);
  border: 1px solid var(--bs-danger-border-subtle, #f1aeb5);
}

/*
 * Day-group date divider — a centred date with a hairline rule on each side.
 * Emitted by rooms/show.html.erb (server) and room_controller.js
 * (#_maybeInsertDateDivider) between messages whose calendar day differs.
 */
.chat-date-divider {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 0.85rem 0;
  color: var(--bs-secondary-color, #6c757d);
  font-size: 0.78rem;
}

.chat-date-divider::before,
.chat-date-divider::after {
  content: "";
  flex: 1 1 auto;
  border-top: 1px solid var(--bs-border-color, #dee2e6);
}

.chat-date-divider span {
  flex: 0 0 auto;
}

/*
 * Search page — highlighted snippet text from Postgres ts_headline.
 * Subtle yellow background so the matched terms read but the row layout
 * stays calm. Inherits font weight from the surrounding <small>. The
 * <mark> is emitted by ts_headline inside sanitized HTML, so it can't
 * carry a class — this element selector is the only way to style it.
 */
.chat-search-snippet mark {
  background: var(--bs-warning-bg-subtle, #fff3cd);
  color: var(--bs-warning-text-emphasis, #664d03);
  padding: 0 2px;
  border-radius: 2px;
}

/*
 * Mentions inbox — two-line clamp on the message excerpt so the row height
 * stays predictable regardless of message body length. Bootstrap ships no
 * multi-line clamp utility, so this is the one rule the mentions list needs
 * beyond utility classes. Named `.chat-*` (not a generic
 * `.text-truncate-2-lines`) because chat.css is linked on every page — a
 * generic name here would read as a global utility it isn't.
 */
.chat-mention-excerpt {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ============================================================================
 * Two-pane messenger layout (desktop-app parity): persistent conversation list
 * on the left, selected conversation in a Turbo Frame on the right. The list
 * lives OUTSIDE the frame so it survives frame navigation; only the right pane
 * swaps. Rendered by apps/chat/rooms/_two_pane.html.erb.
 * ==========================================================================*/

/* Outer page-scroll lock for the messenger. Toggled by
 * apps--chat--two-pane (connect adds the class, disconnect removes it) — see
 * that controller for why the class lives on <html> while the clip lands on
 * <body>. Load-bearing: <html> must stay `overflow: visible` so body's value
 * propagates to the VIEWPORT instead of making body its own scrollport, which
 * would re-break `.sidebar-rail`'s `position: sticky; top: 56px` and put a
 * blank dark strip above the rail (ISS-20260813-022-54B119). */
html.chat-scroll-locked body {
  overflow: hidden;
}

.chat-two-pane {
  display: flex;
  /* The apps--chat--two-pane controller locks the outer page scroll and sets the
   * exact height so this pane fills the viewport and only the inner
   * message/conversation lists scroll (desktop-app feel). This calc is just the
   * pre-JS fallback — kept a little short so nothing clips before JS runs. */
  height: calc(100vh - 130px);
  min-height: 360px;
  /* The OUTER frame keeps its border + radius — it delineates the whole
   * messenger (both panes) from the page. Only the inner conversation card is
   * flattened (below) so there's no box-within-a-box. overflow:hidden clips
   * the flush panes to the rounded corners. */
  border: 1px solid var(--bs-border-color, #dee2e6);
  border-radius: 0.5rem;
  overflow: hidden;
  background: var(--bs-body-bg, #fff);
}

.chat-two-pane__list {
  width: 340px;
  flex: 0 0 340px;
  display: flex;
  flex-direction: column;
  min-height: 0;
  border-right: 1px solid var(--bs-border-color, #dee2e6);
  background: var(--bs-body-bg, #fff);
}

/* Sticky header (title + compose) above the scrolling list. */
.chat-list-header {
  flex: 0 0 auto;
  border-bottom: 1px solid var(--bs-border-color, #dee2e6);
}

.chat-list-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

.chat-two-pane__conversation {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  overflow: hidden;        /* the FRAME never scrolls — only the message list does */
  display: flex;
  flex-direction: column;
}

/* Desktop-app scrolling: the conversation fills the frame with a fixed header +
 * composer and a single scrolling message list. Each level of the
 * container → row → col → card chain must be a min-height:0 flex column so the
 * height propagates down to .chat-scroll, which is the ONLY overflow. */
.chat-two-pane__conversation > .container-fluid {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  /* Flush to the pane edges — the conversation fills the panel with no gutter
   * (the card chrome is stripped below, so there's no inset box to breathe). */
  padding: 0;
}
.chat-two-pane__conversation .row.g-3 {
  flex: 1 1 auto;
  min-height: 0;
  flex-wrap: nowrap;   /* keep the single column on one line so align-items:stretch sizes it to the row height */
  margin: 0;           /* drop the g-3 negative gutter that made the row taller than its parent */
  --bs-gutter-y: 0;    /* and the matching col padding-top */
  --bs-gutter-x: 0;    /* single col-12 — zero the horizontal gutter so content reaches the pane edges */
}
.chat-two-pane__conversation .row.g-3 > [class*="col-"] {
  flex: 1 1 auto;      /* stretch to fill the row height (Bootstrap's default 0 0 auto sizes to content) */
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.chat-two-pane__conversation .card {
  flex: 1 1 auto;
  min-height: 0;
  margin-bottom: 0;
  display: flex;
  flex-direction: column;
  /* Strip the card chrome: the conversation is the pane, not a box inside it.
   * The card-header/card-footer keep their own dividers (functional separators
   * for the title bar and composer, messenger-style). */
  border: 0;
  border-radius: 0;
  --bs-card-inner-border-radius: 0; /* header/footer first/last-child corners */
  box-shadow: none;
}
/* The MGX global adapter (custom.css `body.mgx-restyle .card`) re-adds the
 * card border/radius/shadow at higher specificity than the rule above —
 * strip it again at matching specificity. */
body.mgx-restyle .chat-two-pane__conversation .card {
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
.chat-two-pane__conversation .card > .card-header,
.chat-two-pane__conversation .card > .card-footer {
  flex: 0 0 auto;
  /* custom.css rounds these corners with `!important` longhands, so the reset
   * needs !important too — with the card chrome gone there's nothing to round. */
  border-radius: 0 !important;
}
.chat-two-pane__conversation .card > .chat-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

/* Selected conversation row. */
.chat-conversation-list .list-group-item.active {
  background: var(--bs-primary-bg-subtle, #cfe2ff);
  color: inherit;
  border-color: transparent;
}

/* Empty right pane (nothing selected yet). */
.chat-empty-pane {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  color: var(--bs-secondary-color, #6c757d);
  padding: 2rem;
}

/* Mobile: stack to a single column. `has-active` (set when a conversation is
 * selected) hides the list and shows the conversation; otherwise the list
 * fills the screen. The `apps--chat--two-pane` controller toggles has-active
 * on frame navigation. */
@media (max-width: 767.98px) {
  .chat-two-pane {
    height: calc(100vh - 130px);
    border: 0;
    border-radius: 0;
  }
  .chat-two-pane__list {
    width: 100%;
    flex-basis: 100%;
    border-right: 0;
  }
  .chat-two-pane.has-active .chat-two-pane__list {
    display: none;
  }
  .chat-two-pane:not(.has-active) .chat-two-pane__conversation {
    display: none;
  }
}

/* Denser conversation rows so more active chats fit in the list panel:
 * tighter vertical padding + a smaller avatar→text gap to match the 32px
 * avatars, and a slightly smaller subtitle. Scoped to the list so no other
 * list-group is affected. */
.chat-conversation-list .list-group-item {
  padding-top: 0.4rem;
  padding-bottom: 0.4rem;
}
.chat-conversation-list .list-group-item .me-3 {
  margin-right: 0.6rem !important;
}
.chat-conversation-list .list-group-item small {
  font-size: 0.75rem;
  line-height: 1.15;
}
