/* ============================================================================
   BitcoinPuzzles — the transcript

   One conversation, wherever it is held: a direct-message thread between two
   people, and an alliance room with a crew in it. The two surfaces frame it
   differently — a thread is a file on a page, a room is the page — but what
   goes inside the frame is the same object, and it was written twice. It is
   written here once.

   The rule for this sheet: it owns every `.chat-*` selector, including the
   states and variants only one of the two surfaces currently renders (a
   picture in a bubble reaches a room and not a thread; being named reaches a
   room and not a thread). A surface sheet extends the transcript through its
   own block names — `.al-attach` — and never redeclares a `chat-` one. So a
   chat styling fix is made in exactly one place, and both surfaces get it.

   The `@name` a room's lines carry is not one of those extensions: it is
   `mentions.css`, shared with a puzzle's comments, which is a surface with no
   transcript at all.

   Attribution is stated three times over — side, face, name — because a
   transcript is read at scrolling speed and still has to answer "who said
   this" on every line without being studied. Never by colour alone, and never
   by an orange fill: the accent belongs to Enviar.

   Alignment follows the writing direction — a flex row and logical properties
   throughout — so an RTL locale mirrors the whole transcript, gutter included,
   without a single physical property.

   Depends on tokens.css and base.css, and consumes their primitives (.avatar,
   .btn, .input, .empty, .label, .mono) rather than restating them.
   ========================================================================= */

/* ---------------------------------------------------------------------------
   The transcript — the one region on either surface that scrolls.
   ------------------------------------------------------------------------ */

/* The size of the face a message hangs off. One number, because the gutter,
   the measure and the indent every line of a run sits on are all built off it
   — and because .avatar declares --avatar-size on itself, which a descendant
   cannot read back out, the face is handed this instead. */
.chat-transcript {
  --chat-face: 28px;

  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* A transcript never scrolls sideways: it is read down, and a horizontal bar
     under it is always something having overflowed. The rows are capped so
     nothing does — see --chat-aside — and this is the backstop for whatever
     still might, a pasted address inside a quote being the likeliest. */
  overflow-x: clip;
  overscroll-behavior: contain;
  padding: var(--sp-4) var(--sp-5);
  display: flex;
  flex-direction: column;
}

/* The floor is the surface's to set, because what stands on it is. A room
   floats a typing line and a way back to the newest message over the foot of
   its transcript, and both would otherwise be drawn on top of the last thing
   said — the scrim washing a bubble's chips out, the pill covering them. The
   surface declares `--chat-floor` deep enough for whatever it floats; a
   transcript with nothing over it keeps the inset it always had. */
.chat-transcript {
  padding-block-end: var(--chat-floor, var(--sp-5));
}

/* A three-message conversation rests on the composer instead of floating at
   the top of an empty well. The margin resolves to zero the moment the
   transcript overflows, so a long one is unaffected. */
.chat-transcript > :first-child {
  margin-block-start: auto;
}

/* The region scrolls, so it has to be reachable and visible from the keyboard.
   Inset, because both frames clip their overflow and a ring drawn outside
   would lose its inline edges and read as two loose bars. */
.chat-transcript:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--ink-950), inset 0 0 0 4px var(--accent);
}

/* The transcript is a scrolling flex column, so its empty state centres itself
   in the well rather than sitting at the top of it. Only the centring is
   stated here — the band is `.empty--compact`, shared with every other empty
   state that reports on a small box. */
.chat-transcript > .empty {
  margin-block: auto;
}

/* --- Reaching back through the room ---------------------------------------
   A room opens on its newest page; the rest arrives above it as the reader
   scrolls up. This is what asks for the next one, pinned above everything
   loaded so that each page lands under it and the next reach goes above that.

   Quiet, and centred rather than aligned to either side: it belongs to the
   transcript as a whole and not to anybody in it, which is the same reason the
   date rule is centred. It is the one control in the column that is neither
   a message nor about one.

   Pressed by scrolling to it far more often than by aiming at it — see the
   thread controller — so it is sized to be read rather than to be hit.
   ------------------------------------------------------------------------ */

.chat-earlier {
  flex: none;
  display: flex;
  justify-content: center;
  padding-block: var(--sp-2) var(--sp-4);
}

.chat-earlier__btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: var(--sp-1) var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  background: var(--ink-850);
  color: var(--ink-300);
  font-size: var(--fs-xs);
  text-decoration: none;
  transition: background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease);
}

.chat-earlier__btn:hover {
  background: var(--ink-800);
  border-color: var(--line-hi);
  color: var(--ink-050);
}

.chat-earlier__btn:focus-visible {
  outline: none;
  box-shadow: var(--ring);
  border-color: var(--accent);
  color: var(--ink-050);
}

/* Asked, and waiting. The control stays exactly where it is — what it is
   fetching lands directly underneath it, so moving it would move the page it
   is about to be pushed down by. It only stops answering, and says so by
   breathing. */
.chat-earlier.is-loading .chat-earlier__btn {
  pointer-events: none;
  color: var(--ink-500);
  animation: chat-earlier-wait 1.4s var(--ease) infinite;
}

@keyframes chat-earlier-wait {
  50% {
    opacity: 0.45;
  }
}

@media (prefers-reduced-motion: reduce) {
  .chat-earlier__btn {
    transition: none;
  }

  /* The state still has to read as a state, so it holds the dimmed end of the
     breath rather than losing the signal along with the motion. */
  .chat-earlier.is-loading .chat-earlier__btn {
    animation: none;
    opacity: 0.55;
  }
}

/* Day rule — the date lives here so each message only carries its clock time,
   the same division the ledger draws between a day heading and a row. */
.chat-day {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-block: var(--sp-6) var(--sp-1);
}

.chat-day::before,
.chat-day::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line);
}

.chat-day__label {
  flex: none;
  color: var(--ink-400);
}

/* ---------------------------------------------------------------------------
   A line.

   The gutter is the face plus its gap, named because two other things are
   measured from it: the row's own width, and the indent the rest of a run
   hangs on. It collapses on the reader's own lines, which carry no face.
   ------------------------------------------------------------------------ */

.chat-msg {
  --chat-gutter: calc(var(--chat-face) + var(--sp-2));
  /* What the row leaves free on its outer side for the cluster a surface may
     hang there — see .chat-msg__aside. The cluster is positioned outside the
     row, so the row has to be capped short of it or the transcript is wider
     than itself and scrolls sideways. Zero on a surface that hangs nothing:
     a direct-message thread draws no cluster and keeps the whole measure. */
  --chat-aside: 0px;

  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  max-width: min(80%, calc(100% - var(--chat-aside)), calc(54ch + var(--chat-gutter)));
  margin-block-start: var(--sp-1);
}

/* The message that opens a run takes the wider gap; the ones following it stay
   tight against it, so a burst reads as one turn in the conversation. */
.chat-msg.is-lead {
  margin-block-start: var(--sp-5);
}

.chat-day + .chat-msg.is-lead {
  margin-block-start: var(--sp-2);
}

.chat-msg--them {
  align-self: flex-start;
}

.chat-msg--self {
  --chat-gutter: 0px;

  align-self: flex-end;
}

/* The column beside the face: name, bubble, clock. It is what carries the
   side, so a bubble narrower than the row still sits on its own edge. */
.chat-msg__body {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.chat-msg--them .chat-msg__body {
  align-items: flex-start;
}

.chat-msg--self .chat-msg__body {
  align-items: flex-end;
}

/* Drawn on the line that opens a run; held open as an empty box on every line
   under it, so the whole run stays hung off the one face. */
.chat-msg__face {
  flex: none;
  width: var(--chat-face);
}

.chat-msg__face .avatar {
  --avatar-size: var(--chat-face);
}

/* Nobody needs their own face to know they said it — the side and the "Você"
   are already that. Which side a line sits on is settled in the browser in a
   room, so this has to be CSS: the markup reaches every reader identical. */
.chat-msg--self .chat-msg__face {
  display: none;
}

/* A room's face is a link to the member's profile and answers a hover; a
   thread's is a plain mark, because there is one other person in it and the
   cover sheet above already carries their face straight to their profile.
   Selected on the element rather than on a second class: the difference is
   literally whether it is a link. */
a.chat-msg__face .avatar {
  transition: border-color var(--dur-1) var(--ease);
}

a.chat-msg__face:hover .avatar {
  border-color: var(--line-hard);
}

/* The name that opens a run, set as a name — not run through the uppercase
   micro-label treatment, which turns "Ana Beatriz Vasconcelos" into a tracked
   banner wider than the message under it, and not left at --ink-400 either:
   at the tertiary weight it read as a second timestamp, and the one thing a
   transcript is scanned for was its quietest mark. */
.chat-msg__who {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  margin: 0 0 var(--sp-1);
  /* The bubble's own inset, border included, so the name and the stamp line up
     with the first character of the message rather than sitting a ragged
     twelve pixels outside it. */
  padding-inline: calc(var(--sp-4) + 1px);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-200);
  max-width: 100%;
}

/* The name truncates; the clock beside it never does. */
.chat-msg__name {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  unicode-bidi: isolate;
}

.chat-msg__bubble {
  padding: var(--sp-3) var(--sp-4);
  background: var(--ink-850);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
}

/* A full step of the ramp between the two sides, not the hairline --ink-800
   the room used to take: side and face are the load-bearing signals, but the
   fill is the one that survives a glance at a thumbnail, and two greys six
   hundredths apart were not a signal at all. Still a neutral — the orange is
   Enviar's. */
.chat-msg--self .chat-msg__bubble {
  background: var(--ink-750);
  border-color: var(--line-hi);
}

.chat-msg__text {
  margin: 0;
  font-size: var(--fs-base);
  line-height: var(--lh-body);
  color: var(--ink-100);
  /* Line breaks the sender typed are theirs to keep, and an address pasted
     with no spaces in it has to break somewhere rather than widen the bubble
     past the column. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* A line that is nothing but emoji is the emoji — see
   AllianceMessage#emoji_only?. Chat has one convention everybody already knows
   for these and it is simply size: a thumbs-up sent on its own, set at the
   size of a word, makes the reader hunt for the one glyph in an otherwise
   empty bubble.

   The line-height comes down with it, because 1.6 around a 34px glyph is a
   bubble twice as tall as the picture in it. */
.chat-msg__text--jumbo {
  font-family: var(--font-emoji);
  font-size: var(--fs-3xl);
  line-height: 1.15;
}

/* --- A picture -----------------------------------------------------------
   Sent on its own it is the bubble: no padding ring around it, because the
   frame would only draw a second border inside the first. With a caption the
   bubble keeps its padding and the picture sits above the text.
   ------------------------------------------------------------------------ */

.chat-msg__bubble--shot {
  padding: 0;
  overflow: hidden;
}

.chat-msg__shot {
  display: block;
  border-radius: var(--r-md);
  overflow: hidden;
}

.chat-msg__bubble--shot .chat-msg__shot {
  border-radius: 0;
}

.chat-msg__shot:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Capped both ways: wide enough to read a screenshot of a terminal, short
   enough that one picture cannot take the whole transcript. Tapping it opens
   the file at full size. */
.chat-msg__img {
  display: block;
  width: auto;
  max-width: 100%;
  max-height: 320px;
  object-fit: contain;
  background: var(--ink-950);
}

.chat-msg__shot + .chat-msg__text {
  margin-block-start: var(--sp-2);
}

.chat-msg__shot:hover .chat-msg__img {
  opacity: 0.92;
}

/* --- The clock -----------------------------------------------------------
   On the line that opens a run it sits beside the name, printed once for the
   whole run — which is at most RUN_GAP long, so it dates every bubble under it
   to within five minutes.

   On those bubbles it hangs in the margin instead, out of the flow entirely so
   it costs the measure nothing, and shown on hover or when anything inside the
   line takes focus. There is always room for it: a line is capped at 80% of
   the transcript and sits on its own side, so the fifth of the column on the
   other side is free by construction. Opacity rather than display, so it is
   still read out and still copied with the transcript.
   ------------------------------------------------------------------------ */

.chat-msg__time {
  flex: none;
  font-size: var(--fs-2xs);
  font-weight: 400;
  color: var(--ink-400);
}

.chat-msg__line {
  position: relative;
  min-width: 0;
  max-width: 100%;
}

.chat-msg__time--run {
  position: absolute;
  inset-block-end: var(--sp-2);
  opacity: 0;
  pointer-events: none;
  white-space: nowrap;
  transition: opacity var(--dur-2) var(--ease);
}

.chat-msg--them .chat-msg__time--run {
  inset-inline-start: calc(100% + var(--sp-2));
}

.chat-msg--self .chat-msg__time--run {
  inset-inline-end: calc(100% + var(--sp-2));
}

.chat-msg:hover .chat-msg__time--run,
.chat-msg:focus-within .chat-msg__time--run {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .chat-msg__time--run {
    transition: none;
  }
}

/* --- The margin cluster --------------------------------------------------
   Where a surface has something to offer about one line rather than to say on
   it — answering it, so far — the margin holds a cluster instead of a bare
   clock, and the clock moves inside it. Same slot, same reveal, one box.

   A surface that offers nothing keeps the clock exactly where it was: a
   direct-message thread draws no cluster at all, which is why the rules above
   still stand on their own.
   ------------------------------------------------------------------------ */

.chat-msg__aside {
  position: absolute;
  inset-block-end: var(--sp-1);
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  opacity: 0;
  transition: opacity var(--dur-2) var(--ease);
}

.chat-msg--them .chat-msg__aside {
  inset-inline-start: calc(100% + var(--sp-2));
}

.chat-msg--self .chat-msg__aside {
  inset-inline-end: calc(100% + var(--sp-2));
}

/* Inside the cluster the clock no longer hangs on its own: the cluster is what
   is positioned and what is revealed, and the clock is one of the two things
   riding in it. */
.chat-msg__aside .chat-msg__time--run {
  position: static;
  opacity: 1;
  transition: none;
}

/* Focus, not only hover: the control in here is a button, and a keyboard is
   how it is reached without a pointer. */
.chat-msg:hover .chat-msg__aside,
.chat-msg:focus-within .chat-msg__aside {
  opacity: 1;
}

/* The two controls the margin offers about one line — answer it with an
   emoji, answer it with a sentence. One box, because they sit side by side and
   a pair of controls at two different weights reads as one control and one
   mistake. */
.chat-msg__reply,
.chat-msg__react {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--ink-400);
  cursor: pointer;
  transition: color var(--dur-1) var(--ease), background-color var(--dur-1) var(--ease),
    border-color var(--dur-1) var(--ease);
}

.chat-msg__reply:hover,
.chat-msg__react:hover {
  background: var(--ink-800);
  border-color: var(--line);
  color: var(--ink-050);
}

.chat-msg__reply:focus-visible,
.chat-msg__react:focus-visible {
  outline: none;
  box-shadow: var(--ring);
  border-color: var(--accent);
  color: var(--ink-050);
}

/* No hover to reveal it with, so the control is drawn all the time — quietly,
   because it repeats down every line — and given a thumb-sized box. The clock
   steps out of the cluster there rather than crowd it: it was hover-only, so
   on a touch screen it was never being read anyway. */
@media (hover: none) {
  .chat-msg__aside {
    opacity: 1;
    transition: none;
  }

  .chat-msg__aside .chat-msg__time--run {
    display: none;
  }

  .chat-msg__reply,
  .chat-msg__react {
    width: 36px;
    height: 36px;
    color: var(--ink-500);
  }
}

@media (prefers-reduced-motion: reduce) {
  .chat-msg__aside,
  .chat-msg__reply,
  .chat-msg__react {
    transition: none;
  }
}

/* --- The line an answer quotes -------------------------------------------
   A reply carries the line it answers at the top of its own bubble: who said
   it, and one line of what they said. It is a link back to the original, but
   drawn as a quoted block and not as prose with an underline — it is being
   shown, not read, and the bubble underneath is the message.

   The bar down its leading edge is the quote mark, and it is drawn in a tone
   that can actually be seen against the fill it sits on — a hairline here is a
   rounded corner, not a mark. Neutral, not the accent: in a column of bubbles
   the orange edge means one line names the reader, and a quotation on every
   answer would spend that signal on the commonest thing in the room.

   It follows the writing direction, so an RTL room quotes down its own side.
   ------------------------------------------------------------------------ */

.chat-quote {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  min-width: 0;
  margin-block-end: var(--sp-2);
  padding-block: var(--sp-2);
  padding-inline: var(--sp-3) var(--sp-2);
  border-inline-start: 2px solid var(--ink-400);
  border-radius: var(--r-sm);
  background: var(--ink-900);
  text-decoration: none;
  overflow: hidden;
}

/* The reader's own bubble is a step lighter, so the quote inside it steps the
   other way to stay a block set into the message rather than a patch of the
   same grey. */
.chat-msg--self .chat-quote {
  background: var(--ink-850);
}

.chat-quote:hover {
  border-inline-start-color: var(--ink-200);
}

.chat-quote:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Name over text, both clipped to one line: a quote that grows to the height
   of the message it quotes stops being a quote. */
.chat-quote__who,
.chat-quote__text {
  display: block;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  unicode-bidi: isolate;
}

.chat-quote__who {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--ink-200);
}

.chat-quote__text {
  font-size: var(--fs-sm);
  color: var(--ink-400);
}

/* --- Lines that are marked out -------------------------------------------
   Being named is what a member scrolls a room looking for, so the line
   carrying it takes the accent on its edge and can be found while scrolling
   past rather than only read once it is reached. The border alone, at one
   weight all the way round: the mention chip inside is already the loud mark,
   and a bar down one side on top of it would be a third accent doing the
   second one's job. In a column of bubbles ruled in --line, one ruled in
   orange is found at a glance.

   Answering the notification lands on one line in the middle of a wall of
   them. The wash says which without moving anything, then gets out of the way
   — it has done its job the moment the eye arrives.
   ------------------------------------------------------------------------ */

.chat-msg.is-named .chat-msg__bubble {
  border-color: var(--accent-dim);
}

.chat-msg.is-flagged .chat-msg__bubble {
  animation: chat-flag 2.4s var(--ease) forwards;
}

@keyframes chat-flag {
  0%,
  55% {
    background: var(--accent-wash-hi);
    border-color: var(--accent);
  }
}

@media (prefers-reduced-motion: reduce) {
  .chat-msg.is-flagged .chat-msg__bubble {
    animation: none;
    border-color: var(--accent);
  }
}

/* --- The chips under a line -----------------------------------------------
   What the room answered without saying anything. A reaction is an
   acknowledgement with no bubble: it hangs off the line it is about, and it
   costs the transcript one row however many people join in — which is the
   whole reason it exists, because a crew of eight otherwise turns every "yes"
   into a message.

   The row sits under the bubble and on the bubble's own side, so it reads as
   part of that turn in the conversation rather than as the next one. It is not
   in the margin: the margin holds what is offered, and this is what was said.

   The chip is a toggle and it is drawn as one. Pressed is the accent, because
   "I am in this count" is the one thing the reader is looking for in a row of
   them; every other chip is a neutral pill. Which is which is settled in the
   browser — see the thread controller — so nothing here may depend on the
   server having known who was reading.
   ------------------------------------------------------------------------ */

.chat-reactions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1);
  margin-block-start: var(--sp-1);
  max-width: 100%;
}

/* The form is still there when the row is: it is what a reaction from the
   panel is posted through, and it has to exist before the first chip does. */
.chat-reactions__form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1);
  min-width: 0;
}

.chat-reactions.is-empty {
  display: none;
}

.chat-reaction {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  padding: 0.1rem 0.45rem;
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  background: var(--ink-850);
  color: var(--ink-300);
  font-size: var(--fs-xs);
  line-height: 1.5;
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease);
}

/* A step lighter, to stay a pill set into the reader's own bubble rather than
   a patch of the same grey — the same step the quote inside one takes. */
.chat-msg--self .chat-reaction {
  background: var(--ink-800);
}

.chat-reaction:hover {
  background: var(--ink-750);
  border-color: var(--line-hi);
  color: var(--ink-050);
}

/* Pressed: this reader is one of the people behind it, and tapping takes it
   back. The wash rather than a fill, because a row of solid orange pills under
   a bubble would outweigh the message. */
.chat-reaction.is-mine {
  background: var(--accent-wash);
  border-color: var(--accent-dim);
  color: var(--accent-hi);
}

.chat-reaction.is-mine:hover {
  background: var(--accent-wash-hi);
  border-color: var(--accent);
}

.chat-reaction:focus-visible {
  outline: none;
  box-shadow: var(--ring);
  border-color: var(--accent);
}

/* The glyph is set in the emoji face and a size up from the count beside it:
   it is the thing being read, and the number is the footnote. */
.chat-reaction__glyph {
  font-family: var(--font-emoji);
  font-size: var(--fs-md);
  line-height: 1;
}

.chat-reaction__count {
  font-size: var(--fs-2xs);
  font-variant-numeric: tabular-nums;
}

/* One face is the chip's own glyph plus a "1" that says nothing. The number
   appears when there is a crowd to count. */
.chat-reaction[data-reactors]:not([data-reactors*=","]) .chat-reaction__count {
  display: none;
}

/* No hover to reveal the control with, so a chip is hit with a thumb. */
@media (hover: none) {
  .chat-reaction {
    padding: 0.25rem 0.55rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .chat-reaction {
    transition: none;
  }
}

/* ---------------------------------------------------------------------------
   What floats over the transcript's floor.

   Two momentary things: a name that is writing, and a count of what arrived
   while the reader was further up. Both belong over the transcript rather than
   in the column's flow, because both come and go — a row that appeared and
   disappeared in the flow would shove the conversation up and down the screen
   every time somebody started a sentence.

   They need a positioned frame around the scroller, which is the surface's job
   to provide: a room draws `.al-well`. A surface that draws neither needs no
   frame, and a thread draws neither.
   ------------------------------------------------------------------------ */

/* Who is writing. Full width and fading into the ground rather than sitting on
   a panel, so the last line of the transcript passes under it legibly instead
   of being cut in half by an edge. */
.chat-typing {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin: 0;
  padding: var(--sp-3) var(--sp-5) var(--sp-2);
  font-size: var(--fs-xs);
  color: var(--ink-300);
  pointer-events: none;
  /* The frame's own ground, so the fade is invisible where it ends. Both
     surfaces sit a transcript on --ink-950 and neither paints the scroller
     itself, which is why this can be stated once rather than handed in. */
  background: linear-gradient(to top, var(--ink-950) 45%, transparent);
}

.chat-typing[hidden] {
  display: none;
}

.chat-typing__text {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  unicode-bidi: isolate;
}

/* Three dots keeping time. The one piece of decoration on this surface that
   earns its place: it is what says the line is live rather than a caption that
   was left behind, and it is the same figure every chat application on the
   reader's phone uses for it. */
.chat-typing__dots {
  flex: none;
  display: inline-flex;
  align-items: flex-end;
  gap: 3px;
  height: 10px;
}

.chat-typing__dots i {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--ink-400);
  animation: chat-typing 1.2s var(--ease) infinite;
}

.chat-typing__dots i:nth-child(2) {
  animation-delay: 0.15s;
}

.chat-typing__dots i:nth-child(3) {
  animation-delay: 0.3s;
}

@keyframes chat-typing {
  0%,
  60%,
  100% {
    transform: translateY(0);
    opacity: 0.45;
  }

  30% {
    transform: translateY(-4px);
    opacity: 1;
  }
}

/* Motion is the whole message here, so with it off the dots stay put and stay
   lit: three steady dots still read as "something is happening", where three
   dots at 45% opacity read as a control that is disabled. */
@media (prefers-reduced-motion: reduce) {
  .chat-typing__dots i {
    animation: none;
    opacity: 1;
  }
}

/* The way back to the newest line, offered only to a reader who has scrolled
   away from it. On the trailing side, clear of the typing line on the other,
   and above it — a room can be doing both at once. */
.chat-jump {
  position: absolute;
  inset-inline-end: var(--sp-5);
  /* Inside the floor the surface reserved, so the pill sits below the last
     line rather than on it. */
  inset-block-end: var(--sp-2);
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  min-height: 32px;
  padding-inline: var(--sp-2);
  border: 1px solid var(--line-hi);
  border-radius: var(--r-full);
  background: var(--ink-800);
  color: var(--ink-200);
  box-shadow: var(--sh-2);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease);
}

.chat-jump[hidden] {
  display: none;
}

.chat-jump:hover {
  background: var(--ink-750);
  border-color: var(--line-hard);
  color: var(--ink-050);
}

.chat-jump:focus-visible {
  outline: none;
  box-shadow: var(--ring);
  border-color: var(--accent);
}

/* Something actually arrived while the reader was away, so the control stops
   being a way back and starts being news. */
.chat-jump.has-news {
  border-color: var(--accent-dim);
  color: var(--accent-hi);
}

.chat-jump__count {
  font-size: var(--fs-2xs);
  font-weight: 700;
}

/* Nothing to count is nothing to print: the arrow on its own is the whole
   control for somebody who has simply scrolled up. */
.chat-jump:not(.has-news) .chat-jump__count {
  display: none;
}

@media (prefers-reduced-motion: reduce) {
  .chat-jump {
    transition: none;
  }
}

/* ---------------------------------------------------------------------------
   The composer — the one primary action on either surface, so the one orange.

   Positioned, because a room hangs its name picker over the transcript from
   here. A thread has nothing to hang, and an empty containing block costs it
   nothing.
   ------------------------------------------------------------------------ */

.chat-composer {
  position: relative;
  flex: none;
  padding: var(--sp-3) var(--sp-4);
  background: var(--ink-900);
  border-block-start: 1px solid var(--line);
}

.chat-composer__row {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-3);
}

/* Fill, border, hover and focus all come from .input; what is bespoke here is
   that the field is one line that grows into the message rather than a fixed
   box with a scrollbar in it. Height is set by the composer controller. */
.chat-composer__input {
  flex: 1;
  min-width: 0;
  min-height: 44px;
  padding-block: var(--sp-2);
  font-family: inherit;
  line-height: var(--lh-body);
  resize: none;
  overflow-y: hidden;
}

/* The field is one line until something is typed into it, and the prompt in it
   has to be one line too: wrapped, it made an untouched composer on a phone
   two lines tall and showed the second of them cut in half by the box. */
.chat-composer__input::placeholder {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chat-composer__send {
  flex: none;
  min-height: 44px;
}

.chat-composer__error {
  margin: var(--sp-2) 0 0;
}

/* ---------------------------------------------------------------------------
   Below 768px — the transcript spends its room on the conversation. Tighter
   insets on all three parts, and a line takes a little more of the column
   because 80% of a 360px screen is not a message, it is a word per line.
   ------------------------------------------------------------------------ */

@media (max-width: 767px) {
  .chat-transcript {
    padding: var(--sp-3) var(--sp-4);
    padding-block-end: var(--chat-floor, var(--sp-4));
  }

  .chat-msg {
    max-width: min(86%, calc(100% - var(--chat-aside)));
  }

  .chat-typing {
    padding-inline: var(--sp-4);
  }

  .chat-jump {
    inset-inline-end: var(--sp-4);
  }

  .chat-composer {
    padding: var(--sp-3);
  }

  /* Every pixel the row spends between its four controls is a pixel off the
     one of them that is being written in. */
  .chat-composer__row {
    gap: var(--sp-2);
  }

  .chat-composer__send {
    padding-inline: var(--sp-3);
  }
}
