/* The chat thread — ticket 07.
 *
 * Every number here traces to reference/visual-spec.md, measured off
 * brief/IMG_2631.PNG at DPR 2. Colours, type and the vertical rhythm all live in
 * tokens.css — never hard-code any of them here.
 *
 * The rhythm was fitted by rendering dev/calibration.txt, scanning both it and
 * the still with reference/scan.py, and closing the gaps one at a time. Redo
 * that loop rather than nudging a number by eye:
 *
 *   python3 app/serve.py
 *   chrome --headless --force-device-scale-factor=2 --window-size=750,1334 \
 *     --screenshot=r.png \
 *     'http://localhost:8850/?config=dev/calibration.txt&scene=cal&stage=667&top'
 *   # the stage is at x 187.5, so crop source columns 375..1125 -> the still's 0..750
 *   python3 .scratch/messenger-prop/reference/scan.py <img> --rect X0 X1 Y0 Y1 --rows
 *
 * NOT ?x2 — decisions 104 and 109 struck it: its CSS scale(2) snaps box origins
 * to a whole CSS px and skips painting a <textarea>'s text, and this spec is
 * full of half-pixel edges. Ticket 10 re-ran the call card on the true-DPR-2
 * harness and found a 2px error 07's ?x2 pass had signed off as exact.
 */

/* ------------------------------------------------------------------- header */

.header {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--stage-w);
  height: var(--header-h);
  background: var(--g-header);
  /* The gradient runs to y=0, behind the iOS status bar (visual-spec §4). */
}

.header-row {
  position: absolute;
  /* -0.25 so the row's centre lands on 37.75, which is where four independent
   * elements in the still agree (visual-spec §4) — not on the 38 that a plain
   * 20 + 36/2 would give. Measured back in the A/B. */
  top: calc(var(--status-bar-h) - 0.25px);
  left: 0;
  width: var(--stage-w);
  height: var(--header-row-h);
}

.header-row > * {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
}

.hdr-back {
  left: 9.5px;
  width: 10.5px; /* measured ink 9.5 -> 20.0 */
  height: 19px;
  /* The still's chevron sits 0.5 above the row's centre — 07's fit, unchanged.
   * 08 re-fitted it after correcting the glyph's stroke weight and -0.5, -0.375
   * and -0.25 all render pixel-identically here (Chrome snaps the sub-pixel
   * offset), so there was nothing to gain by moving it. It may still matter on
   * the device; re-check at 13 with the rest of the type A/B. */
  margin-top: -0.5px;
  color: var(--c-header-accent);
}

.hdr-unread {
  left: 20px;
  width: 22px; /* measured ink 20 -> 42; visual-spec said 21.5 */
  height: 19px;
  border-radius: 9.5px;
  background: var(--c-header-accent);
  color: var(--c-text-on-accent);
  font: var(--t-badge);
  display: flex;
  align-items: center;
  justify-content: center;
}

.hdr-avatars {
  left: 50px;
  width: var(--avatar-header-cluster);
  height: var(--avatar-header-cluster);
}

/* Back avatar up and to the right, front down and to the left, drawn on top,
 * with a 2px ring of background where they overlap (visual-spec §4). */
.hdr-avatars .avatar {
  position: absolute;
  width: var(--avatar-header);
  height: var(--avatar-header);
}
.hdr-avatars .avatar--back {
  right: 0;
  top: 0;
}
.hdr-avatars .avatar--front {
  left: 0;
  bottom: 0;
  box-shadow: 0 0 0 var(--avatar-ring) var(--c-thread-bg);
}

.hdr-title {
  left: 95.5px;
  font: var(--t-header-title);
  letter-spacing: var(--ls-header-title);
  color: var(--c-text);
  white-space: nowrap;
}

.hdr-call {
  /* 291, not 07's 291.5: the Bootstrap stand-in's ink sat half a pixel inside
   * its own box and the position absorbed it. With Facebook's own handset
   * (fb-icons/call.svg) the ink lands on the still's 291.5 from here, and the
   * glyph's mismatch drops from 10.8% to 6.9%. */
  left: 291px;
  width: 21px;
  height: 21px;
  color: var(--c-header-accent);
}

/* Scene 2's only actor-driven control. 34 x 44 centred on the 21 glyph — big
 * enough that a thumb aimed at it on camera cannot visibly miss, and it stops at
 * 325, short of the video camera's 333. Transparent and above the glyph.
 * Ticket 08's .cbtn--send-hit is the same idea in the other corner. */
.hdr-call-hit {
  left: 284.5px;
  width: 34px;
  height: 44px;
  z-index: 2;
}

.hdr-video {
  left: 333px;
  width: 26px;
  height: 17px;
  color: var(--c-header-accent);
}

/* ------------------------------------------------------------------- thread */

.thread {
  position: absolute;
  top: var(--header-h);
  left: 0;
  width: var(--stage-w);
  /* The thread fills whatever the header and the composer leave. --composer-grow
   * and --composer-anchor are both 0 with the keyboard down, so this is 07's
   * number; with the keyboard up the anchor lifts the composer to --keyboard-top
   * and the thread shrinks to meet it (composer.css). Both are constants — no
   * viewport unit reaches this file (decision 66). */
  height: calc(
    var(--stage-h) - var(--header-h) - var(--composer-band)
    - var(--composer-grow, 0px) - var(--composer-anchor, 0px)
  );
  overflow-x: hidden;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch; /* momentum, the way Messenger scrolls */
  overscroll-behavior: contain; /* nothing behind the thread may ever show */
  scrollbar-width: none;
}
.thread::-webkit-scrollbar {
  display: none;
}

.thread-inner {
  padding: var(--sp-thread-top) 0 var(--sp-thread-bottom);
}

.thread-inner > :first-child {
  margin-top: 0;
}

/* ---------------------------------------------------------------- dividers */

.divider {
  font: var(--t-divider);
  letter-spacing: var(--ls-divider);
  color: var(--c-text-secondary);
  text-align: center;
  margin-top: var(--sp-item-to-divider);
  /* Authored verbatim — no text-transform, the config already reads as it prints. */
}
.receipts + .divider {
  margin-top: var(--sp-receipt-to-divider);
}

/* -------------------------------------------------------------------- runs */

.run {
  display: flex;
  align-items: flex-end; /* the sender avatar bottom-aligns to the last bubble */
  gap: var(--avatar-sender-gap, 12px);
  margin-top: var(--bubble-gap-between-runs);
}
.run--in {
  padding-left: var(--avatar-sender-left);
}
.run--out {
  padding-right: var(--bubble-out-right);
  justify-content: flex-end;
}
.divider + .run {
  margin-top: var(--sp-divider-to-item);
}
.receipts + .run {
  margin-top: var(--sp-receipt-to-run);
}
.run--named {
  margin-top: 0; /* the .sender block above it carries the spacing */
}

.sender {
  font: var(--t-sender-name);
  letter-spacing: var(--ls-sender-name);
  color: var(--c-text-secondary);
  margin-left: var(--sender-name-left);
  margin-top: var(--bubble-gap-between-runs);
  padding-bottom: var(--sp-name-to-bubble);
}
.divider + .sender {
  margin-top: var(--sp-divider-to-name);
}
.receipts + .sender {
  margin-top: var(--sp-receipt-to-name);
}

.bubbles {
  display: flex;
  flex-direction: column;
  gap: var(--bubble-gap-in-run);
  min-width: 0;
}

.avatar--sender {
  width: var(--avatar-sender);
  height: var(--avatar-sender);
}

/* ----------------------------------------------------------------- bubbles */

.bubble {
  position: relative;
  max-width: var(--bubble-max-w);
  padding: var(--bubble-pad-y) var(--bubble-pad-x);
  border-radius: var(--bubble-radius);
  font: var(--t-message);
  white-space: pre-wrap;
  overflow-wrap: break-word;
}

.bubble--in {
  background: var(--c-surface);
  color: var(--c-text);
  align-self: flex-start;
}

/* One screen-anchored gradient with the bubbles as masks (visual-spec §2,
 * decision 32's mechanism, clamped by 71). background-position-y is written by
 * paintOutgoing() from the bubble's y on the stage, so a bubble's colour depends
 * on where it sits and changes as the thread scrolls. Flat is the same machinery
 * with two identical stops in --g-outgoing (decision 72). */
.bubble--out {
  background-image: var(--g-outgoing);
  background-size: var(--stage-w) 667px;
  background-repeat: no-repeat;
  background-position: 0 0;
  color: var(--c-text-on-accent);
  align-self: flex-end;
}

/* The two corners at a within-run join tighten to 4, on the sender's side. */
.run--in .bubble:not(:last-child) {
  border-bottom-left-radius: var(--bubble-radius-run);
}
.run--in .bubble:not(:first-child) {
  border-top-left-radius: var(--bubble-radius-run);
}
.run--out .bubble:not(:last-child) {
  border-bottom-right-radius: var(--bubble-radius-run);
}
.run--out .bubble:not(:first-child) {
  border-top-right-radius: var(--bubble-radius-run);
}

/* --------------------------------------------------------- reaction badge */

/* The pill overlapping the bubble's lower edge — same corner for incoming and
 * outgoing (visual-spec §8, corrected by ticket 09).
 *
 * IT IS A PILL, 28 x 22, NOT A 28 CIRCLE. Both badges in IMG_2632 measure 22
 * tall against 28 wide, and the row widths fit a radius-11 pill (3 + a circular
 * end) rather than an ellipse. §8 read "circle, 28 diameter" off the width. */
.reaction {
  position: absolute;
  /* Centre sits --react-badge-inset-x in from the bubble's right edge and
   * --react-badge-offset-y BELOW its bottom edge, so it hangs off the corner
   * rather than covering the last word. Measured on BOTH badges in IMG_2632 —
   * incoming centre 140.5 under a bubble whose right edge is 156.5, outgoing
   * centre 351 under 367 — so 16 in and 6 below, twice. (07's comment here said
   * 15 in, from a right edge read as ~155.5; the token was always right.) */
  right: calc(var(--react-badge-inset-x) - var(--react-badge) / 2);
  bottom: calc(-1 * (var(--react-badge-h) / 2 + var(--react-badge-offset-y)));
  width: var(--react-badge);
  height: var(--react-badge-h);
  border-radius: var(--react-badge-radius);
  background: var(--c-surface);
  box-shadow: 0 0 0 var(--avatar-ring) var(--c-thread-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--react-badge-emoji);
  line-height: 1;
}
/* The actor is tapping a 20pt target on camera and cannot be seen to miss:
 * 44 x 38 of hit area around a 28 x 22 badge. */
.reaction::after {
  content: '';
  position: absolute;
  inset: -8px;
}

/* The badge arriving. INVENTED — no source has any motion in it (visual-spec
 * §9.6) — and it is the film's PING, the thing the actor plays off, so it has
 * to read as landing rather than appearing. Scale from the badge's own centre so
 * it grows out of the bubble corner it is anchored to; the ring and the fill
 * come with it. One overshoot, no wobble: a second bounce reads as a toy.
 * Parked on ticket 20 with 08's send spring for the director to watch. */
@keyframes reaction-pop {
  0% { transform: scale(0); opacity: 0; }
  55% { transform: scale(1.18); opacity: 1; }
  78% { transform: scale(0.95); }
  100% { transform: scale(1); opacity: 1; }
}
.reaction--pop {
  animation: reaction-pop var(--react-pop-ms) var(--sheet-ease) both;
}

/* The badge's ring bottom hangs 19 below the bubble (11 + 6 + 2), so a reacted
 * run has to make room or the read receipt lands on top of it. MEASURED, not
 * inferred — see --sp-reaction-extra. Set from JS, not :has(), so nothing here
 * depends on the hero phone's iOS version (ticket 14) supporting a 2022
 * selector. */
.run--reacted {
  /* PADDING, not margin. As a margin it collapses against the receipt row's own
   * --sp-bubble-to-receipt 4 and you get max(19, 4) = 19 instead of 23, which is
   * the badge's ring sitting exactly on the avatar. Padding does not collapse,
   * so the two clearances add and the receipt lands on the measured 23. */
  padding-bottom: var(--sp-reaction-extra);
}

/* --------------------------------------------------------- read receipts */

.receipts {
  display: flex;
  justify-content: flex-end;
  gap: var(--avatar-receipt-gap);
  padding-right: var(--avatar-receipt-right); /* rightmost edge lands on x=355 */
  margin-top: var(--sp-bubble-to-receipt);
}
.avatar--receipt {
  width: var(--avatar-receipt);
  height: var(--avatar-receipt);
}

/* ------------------------------------------------------------ system lines */

.system {
  font: var(--t-system);
  color: var(--c-text-secondary);
  text-align: center;
  margin-top: var(--sp-system);
}
.callcard + .system {
  margin-top: var(--sp-card-to-system);
}

/* --------------------------------------------------------------- call card */

/* Right-aligned like an outgoing message but built as an incoming-style card.
 * Ticket 10 owns the beat wiring, its inertness and the overlay verdict. */
.callcard {
  margin-left: auto;
  margin-right: var(--bubble-out-right);
  margin-top: var(--bubble-gap-between-runs);
  width: var(--callcard-w);
  height: var(--callcard-h);
  border-radius: var(--bubble-radius);
  background: var(--c-surface);
  display: flex;
  align-items: center;
  padding-left: var(--callcard-pad-x);
  gap: var(--callcard-pad-x);
}
.divider + .callcard {
  margin-top: var(--sp-divider-to-item);
}
.callcard-icon {
  width: var(--callcard-icon);
  height: var(--callcard-icon);
  border-radius: 50%;
  background: var(--c-call-card-icon);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
}
.callcard-title {
  font: var(--t-callcard-title);
  letter-spacing: var(--ls-callcard-title);
  color: var(--c-text);
}
.callcard-sub {
  font: var(--t-callcard-sub);
  color: var(--c-text-secondary);
}

/* ---------------------------------------------------------------- avatars */

.avatar {
  border-radius: 50%;
  object-fit: cover;
  flex: 0 0 auto;
  background: #d9dbde; /* only ever seen if a photo is missing — ticket 20 */
  display: block;
}
