.message-area {
  display: contents;

  &:has(.message),
  &:has(.dm-conversation),
  &:has(.thread-card),
  &:has(.bookmark-card) {
    .message-area--empty {
      display: none;
    }
  }

  /*
   * Empty state layout: switches from display:contents to a flex column so the
   * empty state message centers in the remaining space.
   */
  &:not(:has(.message)):not(:has(.dm-conversation)):not(:has(.thread-card)):not(
      :has(.bookmark-card)
    ) {
    --opacity: 0.2;

    block-size: 100vh;
    display: flex;
    flex-direction: column;

    .message-area--empty {
      flex: 1;
      display: grid;
      /* Top-aligned like every other empty state (the 70px block padding comes
         from .empty-state itself); only the horizontal axis centers. */
      place-content: start center;
      animation: empty-state-in 350ms cubic-bezier(0.25, 1, 0.5, 1) both;
      order: 1;

      @media (prefers-reduced-motion: reduce) {
        animation-name: fade-in;
      }
    }

    /* The list container rides flex:1 even when the inbox is empty, which
       shoves the empty state halfway down the page. Collapse it when the query
       rendered nothing. */
    .messages:not(:has(*)) {
      display: none;
    }
  }
}

/* Labeled accent pill floating over the composer */
.message-area__return-to-latest {
  --btn-background: var(--color-accent-brand);
  --btn-border-color: transparent;
  --btn-border-radius: 999px;
  --btn-padding: 7px 15px;

  box-shadow: 0 4px 14px rgb(15 17 26 / 0.18);
  color: var(--color-text-reversed);
  font-size: 0.78125rem;
  font-weight: 600;
  gap: 6px;
  /* --footer-height understates the composer card, and the typing indicator is
     now an out-of-flow overlay that no longer pads the composer — clear the card
     with a larger margin so the pill floats above it */
  inset-block-end: calc(var(--footer-height) + 6.25em);
  inset-inline-start: 50%;
  outline: 0 solid var(--color-selected-dark);
  position: absolute;
  translate: -50% 0;
  white-space: nowrap;

  &.pulse {
    animation: pulsing-outline 2s infinite both;

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

  /* Keep the pill anchored while the spinner shows — the shared .busy rule
     flips position to relative, which shifts a centered absolute pill sideways. */
  &.busy {
    position: absolute;
  }
}

/* Let the phone transcript shrink with the composer, and anchor the floating
   control to its bottom edge instead of estimating the composer's height. */
@media (max-width: 833.9px) {
  #message-area:has(> .message-area__return-to-latest) {
    display: flex;
    flex: 1;
    flex-direction: column;
    block-size: auto;
    min-block-size: 0;
    position: relative;

    .messages {
      min-block-size: 0;
    }

    .message-area__return-to-latest {
      inset-block-end: var(--block-space-half);
      transform: none;
      translate: -50% 0;
    }
  }
}

#system_welcome {
  --avatar-size: 5ch;

  content-visibility: visible;
  contain: none;
  display: block;
  margin-block-start: calc(var(--avatar-size) * 2.5);
  position: relative;
  z-index: 1;

  a:has(.account-logo) {
    display: inline-block;
  }

  .avatar {
    margin-block-start: calc(var(--avatar-size) * -0.5);
  }

  /* The display logo is the hero here — let the initial fill the tile rather
     than the half-height sizing the small sidebar/nav logos share. */
  .account-logo__initial {
    font-size: calc(var(--avatar-size) * 0.6);
  }

  /* The block notice stacks its copy and button tightly */
  .block-notice {
    gap: 4px;
  }

  .message__body {
    display: block;
    max-inline-size: unset;
  }

  .message__body-content {
    max-inline-size: unset;
    overflow: unset;
    padding-block: var(--block-space-double);
  }

  .invite-label {
    display: none;
  }
}

.messages {
  --message-column-gap: 0.66ch;
  /* Stream rhythm from the redesign canvas: rows sit 14px apart, a row's hover
     band insets its content by 4px, and same-author follow-on rows touch. In
     rem so the separators' smaller type doesn't shrink their share of it. */
  --message-space: 0.875rem;
  --message-row-pad: 4px;

  contain: layout style;
  display: grid;
  flex: 1;
  grid-auto-rows: min-content;
  /* Pin the single column to the container: without this the implicit track
     is content-sized, and content-visibility's remembered widths keep rows at
     the pre-panel width when the thread/roster column narrows the area. */
  grid-template-columns: minmax(0, 1fr);
  /* A vertical stream never scrolls sideways: clip the cross axis so nothing
     (a wide embed, a stray absolutely-positioned child) can raise a horizontal
     scrollbar. overflow-y:auto alone computes overflow-x to auto, which is what
     let the bar appear. Code blocks keep their own inner overflow-x:auto. */
  overflow-x: clip;
  overflow-y: auto;
  overscroll-behavior: contain;
  position: relative;
  row-gap: var(--message-space);
}

/* Compact density tightens the message stream too, not just the sidebar (owner
   reversed the sidebar-only scope — see docs/redesign/handoff-divergences.md).
   Doc §09: spacing only — body type never drops below 14px, avatars never below
   28px, so only the spacing tokens move here. */
:root[data-density="compact"] .messages {
  --message-space: 0.5rem;
  --message-row-pad: 2px;
}

:root[data-density="compact"] .message {
  --content-padding-block: 0.4rem;
}

.message {
  --content-padding-block: 0.5rem;
  --content-padding-inline: calc(var(--inline-space) * 1.5);

  column-gap: 12px;
  content-visibility: auto;
  contain-intrinsic-size: auto 6em;
  display: grid;
  grid-auto-columns: 36px 1fr min-content;
  grid-auto-rows: min-content;
  grid-template-areas:
    "loading_up loading_up loading_up"
    "new new new"
    "sep sep sep"
    "avatar body body"
    "replies_sep replies_sep replies_sep"
    "loading_down loading_down loading_down";
  /* A fixed 20px inline gutter; the avatar keeps its 36px track and 12px gap.
     Row rhythm comes from --message-space on the stream. The grid's own tracks
     carry no gap, so the empty separator rows add no height of their own. */
  padding-inline: 1.25rem;
  position: relative;
  visibility: hidden;
  z-index: 0;

  /* Fallback: show messages after 2s if JS hasn't formatted them */
  animation: message-fallback-show 0s 2s forwards;

  @keyframes message-fallback-show {
    to {
      visibility: visible;
    }
  }

  &.message--formatted {
    visibility: visible;
    animation: none;
  }

  /* Subtle entrance for newly arriving messages (not initial page load) */
  &[data-new].message--formatted {
    animation: message-arrive 250ms cubic-bezier(0.25, 1, 0.5, 1) both;

    @media (prefers-reduced-motion: reduce) {
      animation-name: fade-in;
    }
  }

  /* content-visibility:auto implies paint containment, which would clip the
     floating action bar and the popup menus — lift it whenever any shows.
     [open] is the <details> popup, [data-open] is the inline @mention popup;
     :popover-open keeps the bar up while a top-layer anchored menu is open. */
  &:hover,
  &:focus-within,
  &:has([open], [data-open], :popover-open) {
    content-visibility: visible;
    contain: none;
    z-index: 1;
  }

  + .message--threaded {
    /* Grouped same-author rows touch: cancel the stream's row-gap so only the
       two rows' --message-row-pad insets separate their lines of text. */
    margin-block-start: calc(var(--message-space) * -1);
  }

  &.message--failed .message__body-content {
    animation: wiggle 0.4s both;
    border-radius: var(--border-radius-md);
    outline: 1px dashed color-mix(in srgb, var(--color-negative) 45%, transparent);
    outline-offset: 2px;

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

  /* The undelivered draft dims; the chip stays at full strength */
  &.message--failed .message__body-content > :not(.message__failed-notice),
  &.message--retrying .message__body-content > :not(.message__failed-notice) {
    opacity: 0.5;
  }
}

/* The phone transcript follows the Responsive handoff exactly: 14px row
   inset, a 32px avatar, then a 10px gutter. Defining the tracks here matters
   because the desktop grid's 36px implicit avatar column otherwise remains
   after the avatar itself has shrunk to 32px. */
@media (max-width: 499.9px) {
  .message {
    column-gap: 10px;
    grid-template-columns: 32px minmax(0, 1fr) min-content;
    padding-inline: 14px;
  }
}

/* Failed-send chip: the draft stays in place, the chip says so and offers the
   one action that helps */
.message__failed-notice {
  align-items: center;
  background-color: color-mix(in srgb, var(--color-negative) 7%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-negative) 32%, transparent);
  border-radius: 8px;
  color: var(--color-negative);
  display: flex;
  font-size: 0.78125rem;
  gap: 10px;
  inline-size: fit-content;
  margin-block-start: 6px;
  padding: 5px 10px;

  &::before {
    background-color: var(--color-negative);
    border-radius: 50%;
    block-size: 6px;
    content: "";
    flex-shrink: 0;
    inline-size: 6px;
  }
}

.message__failed-retry {
  --btn-background: transparent;
  --btn-border-color: color-mix(
    in srgb,
    var(--color-negative) 40%,
    transparent
  );
  --btn-border-radius: 6px;
  --btn-color: var(--color-negative);
  --btn-padding: 2px 9px;
  --hover-color: transparent;
  --hover-filter: none;

  font-size: 0.71875rem;
  font-weight: 600;

  @media (hover: hover) and (pointer: fine) {
    &:hover {
      background-color: color-mix(
        in srgb,
        var(--color-negative) 12%,
        transparent
      );
    }
  }
}

/* A provisional thread panel (Rooms::ThreadsController#new) renders its parent
   message statically, with no messages controller to add .message--formatted, so
   reveal it directly — the parent is server-rendered and needs no JS formatting. */
.message-area--provisional .message {
  visibility: visible;
}

.message-separator {
  align-items: center;
  font-size: 0.8rem;
  font-weight: 600;
  grid-template-columns: 1fr auto 1fr;
  inline-size: 100%;
  text-align: center;
  text-transform: uppercase;

  time,
  span {
    padding: 0.66em 2.33ch;
    background-color: var(--color-bg-sunk);
    border-radius: var(--border-radius-pill);
  }

  &::after,
  &::before {
    content: "";
  }
}

.message__loading-indicator {
  display: none;
  visibility: hidden;
  margin-block: var(--message-space);

  span {
    background-color: var(--color-bg-sunk);
  }

  &:first-of-type {
    grid-area: loading_up;
  }

  &:last-of-type {
    grid-area: loading_down;
  }

  .message--loading-up &:first-of-type,
  .message--loading-down &:last-of-type {
    display: block;
    visibility: visible;
  }
}

.message__day-separator,
.message__replies-separator {
  display: none;
  grid-area: sep;
  /* The stream's row-gap already holds the pill off the row above */
  margin-block: 0 var(--message-space);
  visibility: hidden;

  /* Comp treatment: a deliberate bordered pill between darker hairlines —
     the date grid reads as structure, not noise */
  time,
  span {
    background-color: var(--color-bg);
    border: 1px solid var(--color-border-dark);
    color: var(--color-text-muted);
    font-size: var(--font-size-x-small);
    font-weight: 700;
    padding: 2px 12px;
    text-transform: none;
  }

  &::after,
  &::before {
    border-top: 1px solid var(--color-border-dark);
  }

  .message--first-of-day & {
    display: grid;
    visibility: visible;
  }
}

/* The "N replies" divider under a post's opening message reads as a quiet
   left-aligned small-caps label, not a pill */
.message__replies-separator {
  grid-area: replies_sep;
  margin-block: var(--message-space) 0;
  display: flex;
  align-items: center;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  color: var(--color-text-muted);
  text-align: start;
  text-transform: uppercase;
  visibility: visible;

  span {
    background-color: transparent;
    /* Reset the pill chrome that leaks in from .message-separator (border-radius)
       and .message__day-separator (border) — this divider is a flat label. */
    border: 0;
    border-radius: 0;
    padding: 0;
  }

  &::after,
  &::before {
    display: none;
  }
}

/* Hidden until the first reply exists: the count span renders empty at zero, and
   the reply-count broadcast fills it (Message::Threadable#update_thread_reply_count),
   which flips :has() and reveals the divider without re-rendering it. */
.message__replies-separator:not(:has(.message__replies-count:not(:empty))) {
  display: none;
}

.message__day-separator--parent {
  display: grid !important;
  visibility: visible !important;
}

.message__event-separator {
  column-gap: 12px;
  display: grid;
  grid-area: event;
  text-align: center;
  font-size: 0.75rem;
  font-weight: 400;
  text-transform: none;
  color: var(--color-text-subtle);
  padding-block: 0.25rem;

  span {
    background-color: transparent;
  }

  /* Comp draws hairlines running to both edges of the event line */
  &::after,
  &::before {
    border-top: 1px solid var(--color-border);
  }
}

/* The comp's deliberate red exception to the accent system: a hairline
   running left into a right-anchored label. */
.message__new-separator {
  grid-area: new;
  margin-block: 0 var(--message-space);
  display: flex;
  align-items: center;
  gap: 12px;
  animation: fade-in 250ms cubic-bezier(0.25, 1, 0.5, 1) both;

  span {
    background-color: transparent;
    color: var(--color-negative);
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    padding: 0;
  }

  &::before {
    border-top: 1px solid
      color-mix(in oklch, var(--color-negative) 50%, transparent);
    content: "";
    flex: 1;
  }

  &::after {
    display: none;
  }

  .message:first-child & {
    margin-block-start: var(--message-space);
  }
}

.message__avatar {
  --avatar-border-size: 1px;
  --avatar-size: 36px;

  /* Avatar shrinks with the row as the shell narrows: 34px at
     tablet-portrait, 32px on phones. */
  @media (max-width: 833.9px) {
    --avatar-size: 34px;
  }

  @media (max-width: 499.9px) {
    --avatar-size: 32px;
  }

  grid-area: avatar;
  margin-block-start: var(--message-row-pad);
  z-index: 2;
  display: flex;
  box-shadow: 0 0 0 var(--hover-size) var(--color-bg);

  /* Avatar wrappers inherit overflow:hidden from .avatar; keep the wrapper
     open so the hover ring isn't clipped. The <button class="btn avatar">
     around the image still clips it to the rounded shape. */
  overflow: visible;
}

.thread__avatar {
  overflow: visible;
}

.message__avatar.avatar--stacked {
  flex-direction: row;

  .avatar + .avatar,
  .btn.avatar + .btn.avatar {
    margin-inline-start: -1ch;
  }
}

.message__meta {
  align-items: center;
  column-gap: calc(var(--message-column-gap) / 2);
  display: flex;
  justify-content: space-between;
  line-height: 1.5;
  inline-size: 100%;
  margin: 0 0 2px;

  .message__meta-block {
    font-size: 0.8rem;
    font-weight: 400;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-inline-size: 0;
    flex-shrink: 10;
  }

  .message__original-room,
  .message__room {
    a {
      color: inherit;
      text-decoration: none;
    }
  }

  .message__room {
    display: none;
    visibility: hidden;
  }

  .message__original-room {
    display: inline-block;
    visibility: visible;
  }

  .message__original-room--with-in {
    &::before {
      content: "";
    }
  }

  .message__room-in {
    margin-inline-start: 0.25em;
  }

  .message__room-link {
    color: inherit;
    text-decoration: none;
    margin-inline-start: 0.25em;

    @media (hover: hover) and (pointer: fine) {
      &:where(:not(:active):hover) {
        text-decoration: underline;
      }
    }
  }

  .message__heading {
    align-items: baseline;
    column-gap: 7px;
    display: inline-flex;
    margin: 0;
    min-inline-size: 0;
    flex: 1;

    > :is(.message__badge, .message__op-badge, .message__bookmark-indicator, .txt-x-small) {
      align-self: center;
    }
  }
}

/* Meta line: author 14.5/700, time and "(edited)" 11.5 subtle */
.message__author {
  color: var(--color-text);
  font-size: 0.90625rem;
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex-shrink: 1;
  min-inline-size: 0;
}

.message__badge,
.message__bookmark-indicator,
.message__heading > .txt-x-small {
  align-items: center;
  display: inline-flex;
  flex-shrink: 0;
  gap: calc(var(--message-column-gap) / 2);
  white-space: nowrap;

  /* Rendered partials can leave whitespace inside an otherwise empty wrapper. */
  &:not(:has(*)) {
    display: none;
  }
}

.message__badge > .txt-x-small {
  display: inline-flex;
}

.message__permalink {
  color: var(--color-text-subtle);
  font-size: 0.71875rem;
  font-variant-numeric: tabular-nums;
  font-weight: 400;
  text-decoration: none;
  white-space: nowrap;
  flex-shrink: 0;
}

.message__edited {
  color: var(--color-text-subtle);
  font-size: 0.71875rem;
  margin-inline-start: 4px;
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex-shrink: 2;
}

/* Spans the whole body column: it is the containing block for the floating
   action bar, which sits at the row's right edge. The reading measure is capped
   on .message__body-content instead, so the text stays at 80ch while the bar
   still reaches the edge. */
.message__body {
  column-gap: var(--inline-space-half);
  display: flex;
  grid-area: body;
  max-inline-size: 100%;
  padding-block: var(--message-row-pad);
  position: relative;
  inline-size: 100%;

  .threads {
    margin-block-start: calc(var(--block-space-half) / 2);

    /* The partial leaves whitespace inside, so :empty never matches */
    &:not(:has(*)) {
      display: none;
    }
  }

  .thread__link {
    font-family: var(--font-family);
    font-size: 0.85em;
    text-decoration: none;
    color: inherit;
    gap: 0.5em;
    align-items: center;
    padding: 0.4em 0.75em;
    border: 1px solid var(--color-border);
    border-radius: 999px;
    background-color: var(--color-bg);
    flex-wrap: wrap;
    transition:
      background-color 150ms ease-out,
      border-color 150ms ease-out;

    @media (hover: hover) and (pointer: fine) {
      &:where(:not(:active):hover) {
        background-color: var(--color-bg-hover);
        border-color: var(--color-border-darker);
      }
    }

    &:active {
      animation: thread-link-highlight 400ms cubic-bezier(0.25, 1, 0.5, 1) both;
    }

    .thread__count {
      color: var(--color-link);
      font-weight: 600;
    }

    .thread__separator {
      color: var(--color-text-subtle);
    }

    /* Below the wide breakpoint the card keeps the reply count and drops the
       last-reply stamp (and its separator), per the responsive doc. */
    @media (max-width: 1023.9px) {
      .thread__separator,
      .thread__last-reply {
        display: none;
      }
    }

    .txt-muted {
      color: var(--color-text-muted);
      font-size: 0.95em;
    }
  }

  .thread__avatar {
    --avatar-size: 20px;

    inline-size: auto;
  }

}

/* The "replying…" pill reuses the thread badge's pill shape and the typing dots.
   The dots pulse on their own here — there is no typing-indicator--active ancestor
   to drive them the way the composer's do. */
.agent-replying__dots span {
  animation: typing-dot 1.2s infinite both;

  &:nth-child(2) { animation-delay: 0.15s; }
  &:nth-child(3) { animation-delay: 0.3s; }

  @media (prefers-reduced-motion: reduce) {
    animation-name: pulse-reduced;
  }
}

/* Flat rows per the redlines: text sits on the page, no bubble. The tinted
   variants (mention, failed) paint the row, not a chip around the text. */
.message__body-content {
  background-color: var(--message-background, transparent);
  border-radius: var(--border-radius-md);
  color: var(--message-color, var(--color-text-lighter));
  font-size: 0.9375rem;

  /* Body trims to 14.5px at tablet-portrait, then returns to 15px on
     phones where the column is full-width and reads best larger. */
  @media (max-width: 833.9px) {
    font-size: 0.90625rem;
  }

  @media (max-width: 499.9px) {
    font-size: 0.9375rem;
  }

  line-height: 1.55;
  font-weight: 400;
  inline-size: 100%;
  max-inline-size: min(80ch, 100%);
  padding: 0;
  word-break: break-word;

  &:has(.message__attachment) {
    margin-block-end: 0.33em;
  }

  &.message__body-content--editing:has(.message__attachment) {
    max-inline-size: calc(min(80ch, 100%) - var(--btn-size));
  }
}

.message__attachment {
  block-size: auto;
  border: 1px solid var(--color-border);
  border-radius: 0.66em;
}

img.message__attachment {
  cursor: zoom-in;
}

.message__actions {
  align-content: center;
  display: flex;
  flex-wrap: wrap;
  justify-content: start;
}

/* The action bar: quick reactions, boost, Reply and the overflow menu. On
   pointer devices it floats above the row, its bottom edge on the row's top
   edge at the right (revealed on hover/focus-within — see overrides.css); on
   touch it stays in the meta row and keeps the picker, Reply and overflow. */
.message__meta .message__actions {
  align-items: center;
  column-gap: 2px;
  flex-wrap: nowrap;
}

@media (hover: hover) and (pointer: fine) {
  .message__meta .message__actions {
    background-color: var(--color-bg-raised);
    border: 1px solid var(--color-border-darker);
    border-radius: var(--border-radius-md);
    box-shadow: var(--shadow-pop);
    inline-size: max-content;
    /* shrink-to-fit under-measures the flex row */
    inset-block-end: 100%;
    inset-inline-end: 0;
    padding: 3px;
    position: absolute;
    z-index: 3;
  }
}

.message__quick-reactions {
  display: inline-flex;
  column-gap: 2px;
}

.message-reaction-menu__touch-choice {
  display: none;

  @media (hover: none) and (pointer: coarse) {
    display: inline-flex;
  }
}

/* The quick reaction carries the accent tint while the current user is already
   in that reaction (reaction_bar_controller); clicking it then removes theirs. */
.message__quick-reaction.message__quick-reaction--active {
  --btn-background: var(--color-selected);
  --btn-border-color: var(--color-selected-dark);
}

.message__reply-label {
  font-size: 0.8rem;
  font-weight: 600;
}

.message__action-btn {
  --btn-background: transparent;
  --btn-border-color: transparent;
  --outline-offset: 0;
  --width: 25%;

  flex-basis: var(--width);
  font-size: 0.8rem;

  @media (hover: hover) and (pointer: fine) {
    &:where(:not(:active):hover) {
      --btn-background: var(--color-bg-hover);
      --hover-size: 0;

      opacity: 1;
    }
  }

  &:where(:not(:active)):focus-visible {
    opacity: 1;
  }
}

/* Bar-level buttons: compact, content-sized (the 25% flex-basis above is for
   the overflow menu context — a %-basis child also breaks the floating bar's
   max-content measurement, so it must be neutralised here). */
.message__bar-btn {
  --btn-padding: 0.25em 0.5em;

  aspect-ratio: unset;
  block-size: auto;
  border-radius: calc(var(--border-radius-md) - 3px);
  flex-basis: auto;
  flex-shrink: 0;
  font-size: 0.9rem;
  white-space: nowrap;

  .boost-character {
    font-size: 1.05rem;
    line-height: 1.3;
  }
}

.bookmark-indicator--active {
  animation: bookmark-pop 250ms cubic-bezier(0.25, 1, 0.5, 1) both;

  @media (prefers-reduced-motion: reduce) {
    animation-name: fade-in;
  }
}

.message__edit-btn {
  display: none;

  .admin &,
  .message--me & {
    display: inline-flex;
  }
}

.popup-menu {
  --arrow-size: 1.5em;
  --boost-border-color: var(--color-selected-dark);
  --border-color: var(--color-border-darker);
  --border-size: 1px;
  --popup-close-dur: 150ms;

  background-color: var(--color-bg);
  border-radius: calc(var(--arrow-size) / 2);
  display: none;
  padding: var(--inline-space-half);
  position: absolute;

  /* [data-open] is the inline @mention popup (mention-popup controller); [open]
     is every <details>-based popup */
  [open] > &,
  [data-open] > & {
    display: block;
    opacity: 1;
    scale: 1;
    transition: opacity 180ms cubic-bezier(0.23, 1, 0.32, 1),
                scale 180ms cubic-bezier(0.23, 1, 0.32, 1);

    @starting-style {
      opacity: 0;
      scale: 0.97;
    }
  }

  [open] > &.is-closing,
  [data-open] > &.is-closing {
    opacity: 0;
    scale: 0.99;
    transition-duration: var(--popup-close-dur);
  }

  @media (prefers-reduced-motion: reduce) {
    [open] > &,
    [data-open] > & {
      scale: 1;
      transition: opacity 100ms ease;

      @starting-style {
        opacity: 0;
        scale: 1;
      }
    }

    [open] > &.is-closing,
    [data-open] > &.is-closing {
      scale: 1;
    }
  }

  &::before {
    aspect-ratio: 2;
    background: var(--border-color);
    clip-path: polygon(50% 0, 100% 100%, 0 100%);
    inline-size: var(--arrow-size);
    content: "";
    display: block;
    inset-block-start: calc(var(--arrow-size) / -2);
    inset-inline-start: 50%;
    position: absolute;
    transform: translateX(-50%);
  }

  &::after {
    aspect-ratio: 2;
    background: var(--color-bg);
    clip-path: polygon(50% 0, 100% 100%, 0 100%);
    inline-size: var(--arrow-size);
    content: "";
    display: block;
    inset-block-start: calc(
      (var(--arrow-size) / -2) + var(--border-size) * 1.5
    );
    inset-inline-start: 50%;
    position: absolute;
    transform: translateX(-50%);
  }

  /*@media (max-width: 100ch) {*/
  /*  inset-block-start: calc(var(--btn-size) / 1.5);*/
  /*}*/

  .popup-orientation-top & {
    inset-block-end: var(--btn-size);
    inset-block-start: auto;
    transform-origin: bottom left;

    &::before {
      clip-path: polygon(50% 100%, 100% 0, 0 0);
      inset-block-end: auto;
      inset-block-start: 100%;
    }

    &::after {
      clip-path: polygon(50% 100%, 100% 0, 0 0);
      inset-block-end: auto;
      inset-block-start: calc(100% - var(--border-size) * 1.5);
    }

    /*@media (max-width: 100ch) {*/
    /*  inset-block-end: calc(var(--btn-size) / 1.5);*/
    /*}*/
  }
}

/* Scoped to .popup-menu: this absolute positioning belongs to the details-based
   mention popup only — the quick profile is an .anchored-popover now and places
   itself from the trigger rect (popover_controller). */
.popup-menu.message__avatar-menu {
  --block-space: 1rem;
  --arrow-size: 1.1em;
  --avatar-border-size: 1px;

  inline-size: fit-content;
  width: 20ch;
  padding: calc(var(--block-space) * 2 / 3);

  inset: calc(
      var(--avatar-size) + var(--avatar-border-size) * 2 + var(--arrow-size) / 2
    )
    auto auto auto;
  transform-origin: top left;
  z-index: 4;

  &::before {
    inset-block-start: calc(var(--arrow-size) / -2);
    inset-inline-start: 0;
    transform: translateX(
      calc(var(--avatar-size) / 2 + var(--arrow-size) / -2)
    );
  }

  &::after {
    inset-block-start: calc(
      (var(--arrow-size) / -2) + var(--border-size) * 1.5
    );
    inset-inline-start: 0;
    transform: translateX(
      calc(var(--avatar-size) / 2 + var(--arrow-size) / -2)
    );
  }

  .popup-orientation-top & {
    inset-block-end: calc(
      var(--avatar-size) + var(--avatar-border-size) * 2 + var(--arrow-size) / 2
    );
    inset-block-start: auto;
    transform-origin: bottom left;
  }
}

.mention .message__avatar-menu {
  --avatar-size: 1em;
  --avatar-border-size: 0px;
  --arrow-size: 0.7em;

  padding: var(--block-space);
}

.lexxy-content .message__avatar-menu {
  padding: var(--block-space);
}

/* Me — own messages render as ordinary left-aligned rows, one alignment for
   everyone. The .message--me class is kept (the formatter still applies it) only
   for the edit affordance; see .message__edit-btn. */

/* Event (system messages) */
.message--event {
  grid-template-columns: 1fr;
  grid-template-areas:
    "loading_up"
    "new"
    "sep"
    "event"
    "loading_down";
}

/* Welcome message (new member joined) */
.message--welcome {
  .message__welcome {
    background-color: color-mix(in srgb, var(--color-positive) 8%, transparent);
    border-radius: var(--border-radius-md);
    padding: var(--content-padding-block) var(--content-padding-inline);

    @media (prefers-color-scheme: dark) {
      background-color: color-mix(
        in srgb,
        var(--color-positive) 15%,
        transparent
      );
    }
  }

  .message__welcome-text {
    font-size: 0.95rem;
    line-height: 1.4;
    flex: 1;
  }
}

.message--mentioned.message--mentioned-unread [data-messages-target="body"] {
  border-radius: var(--border-radius-md);
  animation: mention-fade-out 2s ease-out;

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

/* Pinned thread-parent message reads as context, not the first reply: a sunk,
   bordered block. Keyed off the parent-only day separator, so no extra markup. */
.message:has(.message__day-separator--parent) .message__body-content {
  background-color: var(--color-bg-sunk);
  border: 1px solid var(--color-border);
  border-radius: var(--border-radius-md);
  padding: var(--content-padding-block) var(--content-padding-inline);
}

/* Deactivated author: the name is already struck through — desaturate their
   avatar to match. The message text stays intact. Keyed off that same hook. */
.message:has(.message__author .txt-strikethrough) .message__avatar {
  filter: grayscale(100%);
  opacity: 0.6;
}

.message__grouped-time {
  display: none;
  grid-area: avatar;
  align-self: start;
  /* Centred in the same line box as the row's first line of text */
  padding-block-start: var(--message-row-pad);
  line-height: calc(0.9375rem * 1.55);
  font-size: 0.625rem;
  color: var(--color-text-subtle);
  text-decoration: none;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Follow-on rows keep the author grouping; each time remains keyboard-reachable. */
.message--threaded {
  .message__meta {
    justify-content: end;
    /* The heading is hidden; its trailing margin must not push the text down */
    margin-block-end: 0;
  }

  /* On touch the actions stay in the meta row. With no heading beside them
     they would take a line of their own above every grouped message, so float
     them to the end of the first line of text instead. flow-root keeps the
     taller tap targets inside the row. */
  @media (hover: none) and (pointer: coarse) {
    .message__body-content {
      display: flow-root;
    }

    .message__meta {
      float: inline-end;
      inline-size: auto;
      margin-inline-start: 0.5rem;
    }
  }

  .message__heading,
  .message__avatar {
    display: none;
  }

  .message__grouped-time {
    display: block;
    opacity: 0;
    pointer-events: none;
  }

  &:is(:hover, :focus-within) .message__grouped-time {
    opacity: 1;
    pointer-events: auto;
  }

  /* Touch has no hover to reveal it, and an always-on stamp beside every
     grouped line reads as noise on a phone, so the gutter stays empty there. */
  @media (hover: none) and (pointer: coarse) {
    .message__grouped-time {
      display: none;
    }
  }
}

.message__edit-btns div:only-of-type {
  margin-inline: auto;
}

.message--emoji:not(:has(.composer--edit)) {
  .message__body-content {
    margin-block-end: 0;
    text-shadow: 0 0 3px var(--color-text-reversed);

    .lexxy-content {
      font-size: 2.4em;
      line-height: 1.4;
    }
  }
}

.message.search-highlight {
  .message__body-content {
    border: 2px solid var(--color-text);
    animation: border-fade-out 0.5s 1s forwards;
  }
}

/* Pending uploads */
.message__pending-upload {
  border-radius: 0.66em;
  overflow: hidden;
  padding: var(--content-padding-block) var(--content-padding-inline);
  position: relative;

  &::before {
    background-color: var(--color-border-dark);
    block-size: 100%;
    content: "";
    inline-size: 100%;
    inset-inline-start: var(--percentage);
    position: absolute;
    transform: translateX(-100%);
    z-index: 1;
  }

  > div {
    z-index: 2;
  }

  span {
    inline-size: 3rem;
    display: inline-block;
  }

  .composer__file-thumbnail {
    aspect-ratio: 1;
    background-size: contain;
    inline-size: 1.4em;
  }
}

/* The agent's live draft — a message bubble in a drafting state, replaced in
   place by the real message once the turn ends. It reuses the message layout;
   only the streaming affordances below are its own. */
.message__draft-text {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* A blinking caret while text is still arriving; gone implicitly when the real
   message replaces the whole bubble. */
.message--streaming .message__draft-text::after {
  content: "";
  display: inline-block;
  inline-size: 0.5ch;
  block-size: 1.1em;
  margin-inline-start: 1px;
  vertical-align: text-bottom;
  background-color: var(--color-text-muted);
  animation: draft-caret 1s steps(1) infinite;
}

@keyframes draft-caret {
  50% { opacity: 0; }
}

.message__draft-activity {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-block-start: 0.35rem;
}

/* Dots and the current-activity text ride together; the cancel control trails. */
.message__draft-activity-status {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}

/* The cancel control sits at the end of the activity row, quiet in the muted line. */
.message__draft-cancel-form {
  margin-inline-start: auto;
}

.message__draft-cancel {
  color: inherit;
  text-decoration: underline;
}

/* Just the pulse until a real tool title arrives. */
.message__draft-activity-text:empty {
  display: none;
}

/* The pulse is the typing indicator's dots, animated here without the
   active-state gate its own parent applies. */
.message__draft-activity .typing-dots span {
  animation: typing-dot 1.2s infinite both;

  @media (prefers-reduced-motion: reduce) {
    animation-name: pulse-reduced;
  }
}
.message__draft-activity .typing-dots span:nth-child(2) { animation-delay: 0.15s; }
.message__draft-activity .typing-dots span:nth-child(3) { animation-delay: 0.3s; }
