/* chat/04_thread_and_bubble.css
 * ------------------------------------------------------------
 * Split 4 of 10 from web/account/css/chat.css (CLAUDE.md §8.16 — see
 * 01_tokens_reset.css's header for the split's background; loaded fourth
 * in chat.html, right after the list/empty-states file).
 *
 * Covers: the thread screen's own scroll container and sentinel
 * (thread_view.js, message_list.js), the day divider (day_divider.js),
 * and the message bubble itself (message_bubble.js,
 * message_bubble_states.js, message_text.js) — own/other alignment, the
 * system-line variant, emoji-only and media-only bubbles, bubble text and
 * link colouring, the read receipt, and the hover-revealed reply arrow
 * including its two anchor contexts (over a picture vs. inside a bubble).
 * This is the largest single split (440 lines) because the bubble's many
 * interlocking states — own/other, emoji-only, media-only, hover,
 * pending, failed, deleted — read as one continuous argument in the
 * original file and splitting mid-argument would have been worse than a
 * larger file.
 * ------------------------------------------------------------ */

/* ===== Thread screen (thread_view.js, message_list.js, day_divider.js) ===== */

.chat-thread { height: 100%; }

.chat-thread__scroll {
  /* THE scroll region for this screen (thread_scroll.js owns the
     anchoring logic against this exact element) — see the file header. */
  height: 100%;
  overflow-y: auto;
  overscroll-behavior-y: contain; /* stops an overscroll here from also rubber-banding the (non-scrolling) shell body behind it */
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
}

.chat-thread__content {
  min-height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: flex-end; /* a short thread sits at the bottom, not the top */
  padding: 10px 10px 6px;
}

.chat-thread__sentinel {
  /* 1px marker thread_view.js's IntersectionObserver watches to trigger
     loading older messages — invisible, not zero-height (a truly 0px
     element can fail to intersect in some browsers). */
  height: 1px;
}

.chat-message-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.chat-day-divider {
  display: flex;
  justify-content: center;
  margin: 14px 0 10px;
}
.chat-day-divider__label {
  background: var(--chat-wash-medium);
  color: var(--chat-bubble-muted);
  font-size: 12px;
  font-weight: 600;
  padding: 5px 14px;
  border-radius: 12px;
}

/* ===== Message bubble (message_bubble.js, message_bubble_states.js, message_text.js) ===== */

.chat-bubble {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  max-width: 84%;
  margin: 3px 0;
}
.chat-bubble.is-own { align-self: flex-end; }
.chat-bubble.is-other { align-self: flex-start; }

/* The system line (message_bubble.js's early return) has no column wrapper
   at all — it IS `.chat-bubble__system-text` directly inside
   `.chat-bubble.is-system` — so it is styled as a small centred pill in
   its own right rather than through `__column`. */
.chat-bubble.is-system {
  align-self: center;
  max-width: 90%;
  margin: 8px 0;
}
.chat-bubble__system-text {
  display: block;
  background: var(--chat-wash-medium);
  color: var(--chat-bubble-muted);
  font-size: 12.5px;
  text-align: center;
  padding: 6px 14px;
  border-radius: 12px;
}

.chat-bubble__column {
  /* The vertical STACK for one message: the bubble itself, and anything that
     belongs OUTSIDE it. It carries no background and no padding — those moved
     to .chat-bubble__body below, so that the reader avatars can sit under the
     bubble on the wallpaper rather than inside the coloured slab. That is
     where the app puts them, and inside the bubble they read as part of the
     message instead of as a receipt about it. */
  display: flex;
  flex-direction: column;
  /* No gap. The column holds exactly two things — the bubble and the read
     receipt under it — so this single value WAS the distance between them, and
     it stacked on top of the receipt's own margin and padding: 4 + 3 + 2 = 9px
     where the app uses 3. The receipt now owns that spacing alone
     (EdgeInsets.only(top: 3, right: 4) in chat_group_receipt.dart), which is
     also the only way the two clients can stay in step. */
  gap: 0;
}
/* The bubble: the coloured slab the message content sits on. Sized by its
   content, which is why the column above uses align-items rather than letting
   it stretch to the column's full width. */
.chat-bubble__body {
  /* A flex column, like the stack that used to hold these children directly.
     Splitting __column out took the padding with it but left `display: flex`
     behind, and that quietly broke the meta row: `align-self: flex-end` does
     nothing inside a BLOCK container, so the timestamp and read tick stopped
     hugging the right edge and lined up left instead. */
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 7px 10px 6px;
  border-radius: 14px;
  /* Wide enough for the two hover discs to sit inside the bubble.
     Arithmetic, not taste: the ⋯ disc's outer edge is 36 + 26 = 62px in from
     the right, and the bubble's own left padding is 10px, so anything under
     72px has a control hanging over its left edge — which is what a two-letter
     answer looked like. 76px leaves a few pixels of daylight.
     It costs a "Ja" a little more width than it needs, always. That is the
     smaller price: the alternative is a control that only fits on some
     messages, and the ones it fails on are the short ones people send most. */
  min-width: 76px;
  /* The anchor for the reply disc on a TEXT message — the picture's own block
     plays that role when there is one (see .chat-media). Same control, same
     corner; only what the corner belongs to differs. */
  position: relative;
}
/* Each side's stack hugs its own edge, so a receipt row under the bubble lines
   up with the bubble rather than with the column's full width. */
.chat-bubble.is-own .chat-bubble__column { align-items: flex-end; }
.chat-bubble.is-other .chat-bubble__column { align-items: flex-start; }
/* The "tail" corner — flattened on the side that points at the sender,
   the same convention WhatsApp uses to make alignment readable even with
   the colour difference alone removed (kravspec's colour-is-not-the-only-
   signal point, same reasoning as the unread-row weight change above). */
.chat-bubble.is-own .chat-bubble__body {
  background: var(--chat-outgoing-bubble);
  border-bottom-right-radius: 3px;
}
.chat-bubble.is-other .chat-bubble__body {
  background: var(--chat-incoming-bubble);
  border-bottom-left-radius: 3px;
}
.chat-bubble.is-pending .chat-bubble__column { opacity: .7; }

/* A message that is nothing but emoji: drawn LARGE and with no bubble behind
   it, the way the app does it (chat_bubble_appearance.dart, task 85) and the
   way Messenger, WhatsApp and iMessage all do.

   `background: none` has to beat the two `.is-own`/`.is-other` rules above,
   which are more specific than a single class — hence naming the same two
   selectors rather than reaching for !important. The tail corner goes with the
   bubble: there is no shape left for it to point from.

   The size comes from the element's own --emoji-size custom property, set per
   message by ui/message_bubble.js, because it depends on how MANY emoji there
   are (40 / 30 / 24 px). The fallback keeps the rule sane if the property is
   ever missing. line-height 1.15 stops a big glyph from pushing its
   neighbours apart the way the body text's 1.35 would. */
.chat-bubble.is-emoji-only.is-own .chat-bubble__body,
.chat-bubble.is-emoji-only.is-other .chat-bubble__body {
  background: none;
  border-radius: 14px;
  /* Only enough room to breathe between neighbours — the app's
     EdgeInsets.symmetric(vertical: 2) for the same case. */
  padding: 2px 0;
  min-width: 0;
}
/* A picture or video with no caption: no bubble, and no padding either. With
   the slab gone there is nothing left to inset FROM — the padding that keeps a
   photo clear of the bubble's rounded corner would read as a thin, slightly
   off-centre margin instead. The app zeroes it for the same reason
   (chat_bubble_appearance.dart's _paddingFor). Same two-selector specificity
   trick as the emoji rule above, for the same reason. */
.chat-bubble.is-media-only.is-own .chat-bubble__body,
.chat-bubble.is-media-only.is-other .chat-bubble__body {
  background: none;
  padding: 0;
  min-width: 0;
}
/* Flush with the picture's right edge — no inset. The 2px it used to carry was
   an attempt to give it breathing room, but under a bare picture there is no
   bubble edge to stand clear of, and the offset just made the timestamp look
   slightly misaligned with the thing it belongs to. */
.chat-bubble.is-media-only .chat-bubble__meta { padding: 0; }
/* With no bubble behind it, the frame's reserved-space colour stops being an
   invisible placeholder and becomes a grey slab framing the picture — exactly
   the frame the naked treatment exists to remove. `contain` letterboxes onto
   whatever is behind, so behind should be the wallpaper. The colour still does
   its real job everywhere else: holding the space before a signature
   arrives. */
.chat-bubble.is-media-only .chat-media__frame { background: none; }

/* The reply arrow ON the picture, bottom-right, and only while the message is
   hovered — or while the arrow itself has keyboard focus. That second half is
   not optional: revealing on hover ALONE leaves a control a keyboard user can
   tab to and still not see, which is worse than one that is merely quiet.
   A dark disc behind it so the glyph survives a pale photo. */
/* One control, two anchors — see ui/message_bubble.js. The offsets differ
   because the corners do:

   ON A PICTURE it tucks into the picture's own bottom-right corner, directly
   above the timestamp that sits under the picture.

   IN A TEXT BUBBLE the timestamp is INSIDE the bubble, in that same corner, so
   the disc lies ON TOP of it — layered, not moved aside. z-index puts it above,
   and it only exists while the message is hovered, so the time is hidden for
   exactly as long as the pointer is there and no longer. Placing it beside the
   time instead would push the time off the right edge, which is where it was
   deliberately put. */
.chat-bubble__reply.is-overlay {
  position: absolute;
  z-index: 1;
  right: 4px;
  bottom: 4px;
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: color-mix(in srgb, var(--chat-bg) 70%, transparent);
  color: var(--chat-bubble-text);
  /* Visibility is decided by the shared opacity rules further down, not here —
     this block only says WHERE the arrow sits. Two places deciding whether it
     shows is how the two halves would drift apart. */
  transition: opacity .12s ease;
}

.chat-bubble.is-emoji-only .chat-bubble__text {
  font-size: var(--emoji-size, 40px);
  line-height: 1.15;
}
.chat-bubble.is-failed .chat-bubble__body {
  box-shadow: inset 0 0 0 1px var(--error);
}
/* A soft-deleted message's tombstone text (.chat-bubble__text--deleted,
   below) already carries most of the visual difference; the bubble itself
   only needs to read as slightly less prominent than a live message, not
   to look broken. */
.chat-bubble.is-deleted .chat-bubble__column { opacity: .75; }

.chat-bubble__sender {
  font-size: 12.5px;
  font-weight: 700;
  color: var(--chat-sender-name);
}

.chat-bubble__text {
  font-size: 14.5px;
  line-height: 1.4;
  /* pre-wrap, not nowrap: a message's own line breaks are part of what the
     sender wrote and must survive; word-break stops one long unbroken
     token (a URL with no spaces) from forcing the bubble to the max-width
     ceiling instead of wrapping inside it. */
  white-space: pre-wrap;
  overflow-wrap: break-word;
}

/* A LINK INSIDE A MESSAGE.

   Without these two declarations a link is invisible AS a link. The
   surface-wide `a { color: inherit; text-decoration: none; }` reset near the
   top of this file is right for the shell's own navigational anchors — a list
   row is a whole row and already looks like something you press — but a link
   inside a sentence has nothing else to announce it. Stripped of both its
   colour and its underline, an address renders in exactly the same white as
   the prose around it.

   That is what "URLs are not clickable in the web chat" turned out to be: the
   anchor was there and it worked. util/linkify.js had parsed it, validated
   the scheme and built a real <a> — but a reader has no reason to try
   pressing a word that looks like every other word, so nobody ever found out.
   An affordance nobody can see is the same as a missing feature.

   The colour is --chat-sender-name, the muted light sage — NOT
   --chat-read-tick. This used to be the read tick's #53BDEB on the reasoning
   that it was already this surface's one interactive accent, so borrowing it
   beat inventing a new hex. That reasoning held for the token count and
   failed for the eye: a saturated WhatsApp blue is the single coldest thing
   on a warm sage-and-charcoal surface, so every link shouted, and it read as
   borrowed from another product rather than belonging to this one.

   The app had already solved it. chat_message_bubble.dart colours a link in
   someone else's bubble with AppColors.chatSenderName (this same light sage)
   and a link in your own deep-sage bubble white. Web was the odd surface out
   — the same message, opened on the two clients, showed two different link
   colours. Matching the app is therefore not a new design decision; it is
   removing an inconsistency, and it is why no new token was needed.

   The underline still carries the meaning on its own for anyone who cannot
   separate the two colours — that was always the load-bearing half. */
.chat-bubble__text a {
  color: var(--chat-sender-name);
  text-decoration: underline;
  /* Clear of the descenders in a wrapped URL, which is where an underline
     tight against the text becomes hard to read. */
  text-underline-offset: 2px;
}
/* Your own bubble is the deep sage --chat-bubble-own; the light sage above
   sits too close to it to separate cleanly, so a link there goes white —
   again exactly what the app does (linkColor: isMine ? Colors.white : ...).
   Underline unchanged, so the affordance survives the colour swap. */
.chat-bubble.is-own .chat-bubble__text a { color: #FFFFFF; }
.chat-bubble__text a:hover,
.chat-bubble__text a:focus-visible { text-decoration-thickness: 2px; }

.chat-bubble__text--deleted {
  font-style: italic;
  color: var(--chat-bubble-muted);
}
.chat-bubble__expand {
  display: block;
  margin-top: 2px;
  color: var(--chat-read-tick);
  font-size: 13px;
  font-weight: 600;
}
/* Wraps the (possibly truncated) text plus the expand control above —
   message_text.js's own div; a plain block is all it needs, this rule
   exists so the two children stack with the same rhythm as an
   untruncated .chat-bubble__text would have on its own. */
.chat-bubble__text-wrap { display: flex; flex-direction: column; }

.chat-bubble__meta {
  align-self: flex-end;
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 1px;
}
.chat-bubble__time { font-size: 11px; color: var(--chat-bubble-muted); }
.chat-bubble__status { font-size: 11px; color: var(--chat-bubble-muted); font-style: italic; }
/* The read receipt beside the timestamp on an own message: one tick for sent,
   two for seen (ui/message_receipt.js builds the glyph). The colour below is
   the SECOND signal, never the only one — the tick COUNT carries the meaning,
   because a state told apart by hue alone at 11px on a coloured bubble is a
   state a great many readers cannot read at all. --chat-read-tick is the
   palette's own "this has been read" colour, generated from app_colors.dart
   like everything else here; the muted tone is the same one the timestamp
   already uses, so an unread message's tick reads as part of the meta row
   rather than as a warning. */
.chat-bubble__receipt {
  /* 13px is the app's icon size for the same mark (chat_group_receipt.dart /
     the delivery tick), and it needs to be a touch larger than the 11px
     timestamp beside it or the double tick reads as a smudge. */
  font-size: 13px;
  /* One tick, so there is no pair to pull together any more — the
     letter-spacing that used to close the gap between two ✓ characters would
     now just clip the single one. It sits tight against the time it belongs to,
     with nothing on the right: the tick is the LAST thing on the line, and
     padding there would only hold it off the edge it is supposed to sit on. */
  margin-left: -2px;
  line-height: 1;
  /* Slightly tighter than the meta row's own 6px gap: the tick belongs TO the
     timestamp it follows, not beside it as a separate item. */
  margin-left: -2px;
  color: var(--chat-bubble-muted);
}
.chat-bubble__receipt.is-seen { color: var(--chat-read-tick); }
/* Kept at partial opacity rather than 0/hidden at rest: a fully invisible-
   until-hover control is undiscoverable for anyone not currently hovering
   with a mouse, including a keyboard user who has not yet tabbed to it
   (the accessibility rule this stylesheet must not undo). */
.chat-bubble__reply {
  font-size: 13px;
  line-height: 1;
  padding: 2px;
}

/* The arrow appears when the message is hovered, not before. Standing there
   permanently it was one more mark to read past on every single line, which is
   what makes a thread feel busy — and it is an action you go looking for, not
   one you need announced.

   Only where a pointer actually EXISTS. On a touch screen there is no hover, so
   a hover-gated control would simply be unreachable; `@media (hover: hover)` is
   what keeps the rule from applying there, and the arrow stays visible on a
   phone. This is not a nicety: without the query the feature would be a
   regression on exactly the devices this surface is a PWA for.

   :focus-visible reveals it too, so tabbing to it still shows it. A control a
   keyboard user can focus but not see is worse than one that is merely quiet. */
@media (hover: hover) {
  .chat-bubble__reply { opacity: 0; }
  .chat-bubble.is-hovered .chat-bubble__reply,
  .chat-bubble__reply:focus-visible { opacity: 1; }
  /* Same rule, stated again at the overlay's own specificity. Belt and braces:
     the disc is the one control whose absence is silent — nothing errors, the
     corner simply stays empty — so it does not rely on being the only rule in
     the cascade that touches its opacity. */
  .chat-bubble.is-hovered .chat-bubble__reply.is-overlay,
  .chat-bubble__reply.is-overlay:focus-visible { opacity: 1; }

  /* In a TEXT bubble the disc and the meta line occupy the same corner, and a
     26px disc can only ever cover 26px of it — the time and the ticks kept
     showing to its left, which is what "beside" looked like. So the meta steps
     aside while the pointer is on the message: the disc is then the only thing
     in that corner, which is what it is on a picture.
     Not applied to a bare picture: there the timestamp sits UNDER the image and
     the two never overlap, so hiding it would remove information for no reason. */
  /* The two marks the disc lands on top of, hidden while it is there.

     Deliberately stripped back to ONE guard. Earlier versions tried to be
     precise about which bubbles should be affected — first `:has()`, then a
     chain of `:not(.is-pending):not(.is-deleted)` — and both produced a rule
     that worked on some messages and not others. Every added condition is
     another way for the selector to miss, and the cost of missing is silent:
     the time simply stays put under the disc.

     So: the time and the ticks, on any hovered bubble that is not a bare
     picture (there the meta sits UNDER the image and nothing overlaps it). A
     message with no reply control loses its timestamp for as long as the
     pointer rests on it, which is a smaller price than a rule that only
     sometimes fires. */
  .chat-bubble.is-hovered:not(.is-media-only) .chat-bubble__time,
  .chat-bubble.is-hovered:not(.is-media-only) .chat-bubble__receipt {
    opacity: 0;
  }
  .chat-bubble__time,
  .chat-bubble__receipt { transition: opacity .12s ease; }
}
/* Directly over the timestamp in a text bubble. Not inside a hover query:
   WHERE the disc sits is the same question on every device — only WHETHER it
   shows depends on there being a pointer. */
/* DESCENDANT, not child: the reply arrow is a direct child of the bubble, but
   the ⋯ control lives inside its own wrapper (it has a menu to anchor). A `>`
   here reached only the first of the two, so the second fell back to the base
   offset and sat a few pixels higher — two controls in one corner that did not
   line up. */
.chat-bubble__body .chat-bubble__reply.is-overlay {
  right: 6px;
  /* Just clear of the bubble's bottom edge. This has been nudged by eye a few
     times — flush read as too high against the text above it, a 1px overhang
     as a shade too low. Zero is where it settled. */
  bottom: 0;
}

/* The arrow itself, nudged down inside its disc. `↩` is a glyph whose ink sits
   high in its em box — flex centring puts the BOX in the middle, which leaves
   the visible arrow looking like it floats above centre. One pixel of optical
   correction; the disc itself does not move. */
