/* Chat surface. Owned by the chat builder.
   Two-pane on desktop, single pane on phone. The chrome stays quiet: people
   get the body face, the record-keeping (times, dividers, the TTL sentence,
   context labels) gets the mono face. Own bubbles are magenta with NEAR-BLACK
   text — never white — per docs/brand.md measured contrast. */

.chat {
  /* The measure comes from :root in app.css — one declaration for the whole
     app, consumed here by the scroller, the composer and the dock. It was
     declared on this class first, which left the interview surface outside
     its scope and produced two chat widths (punchlist 2026-08-08). Do not
     re-declare it here; test/measure.test.js fails on a second declaration. */
  display: grid;
  grid-template-columns: 320px minmax(0, 1fr);
  /* One explicit row track: an implicit (auto) row grows past the viewport
     when a conversation is long, and the composer falls off the screen. */
  grid-template-rows: 100%;
  height: 100%;
}

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

.chat-list {
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* Above the river's content floor, so its rows stay clickable with immersive
   * on even if the scaled space overreaches (belt to the pane's overflow clip). */
  position: relative;
  z-index: 10;
}
.chat-list__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 22px 18px 14px;
}
.chat-list__head h1 { font-size: 22px; }
.chat-list__rows {
  overflow-y: auto;
  min-height: 0;
  padding: 0 10px 24px;
  display: flex;
  flex-direction: column;
  gap: 6px;   /* was 2px: the rows now carry their own stroke and need air */
}

.chat-new {
  width: 34px; height: 34px;
  border: 0; border-radius: var(--r-pill);
  background: var(--raised);
  color: var(--text);
  font-size: 19px; line-height: 1;
  cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  transition: background var(--t), transform var(--t);
}
.chat-new:hover { background: var(--accent-tint); color: var(--accent-text); }
.chat-new:active { transform: scale(.94); }

/* Same stroke construction as .listrow in app.css. The thread list and the
   quest list are one visual family and must not diverge. */
.chat-row {
  display: block;
  width: 100%;
  text-align: left;
  border: 0;
  color: var(--text);
  padding: 12px;
  border-radius: var(--r-m);
  cursor: pointer;
  transition: box-shadow var(--t), background var(--t);
  font: inherit;
  background: var(--canvas);
  box-shadow: inset 0 0 0 1px var(--raised);
}
.chat-row:hover { background: var(--canvas); box-shadow: inset 0 0 0 1px var(--line); text-decoration: none; }
.chat-row:active { background: var(--surface); box-shadow: inset 0 0 0 1px var(--line); }
.chat-row.selected { background: var(--accent-tint); box-shadow: inset 0 0 0 1px var(--accent-line-soft); }
.chat-row__top {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}
.chat-row__name {
  font-weight: 600; font-size: 14.5px;
  color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chat-row__time {
  font-family: var(--mono); font-size: 11px;
  color: var(--muted);
  flex: none;
}
.chat-row__preview {
  margin-top: 2px;
  font-size: 13px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chat-row__ctx {
  margin-top: 3px;
  font-family: var(--mono); font-size: 11px;
  color: var(--muted);
}
.chat-row--fac .chat-row__name { display: inline-flex; align-items: center; gap: 8px; }
.chat-row--fac .rung { color: var(--accent-text); }
.chat-row--fac .lotus-edge { margin: 10px 2px 0; }
.chat-list__hint {
  padding: 18px 14px;
  font-size: 13px;
  color: var(--muted);
}

/* row pending state (starting a thread) */
.chat-row[aria-busy="true"] { opacity: .55; pointer-events: none; }

/* Picker pending state, shaped like the person rows it becomes:
   avatar circle + two text bars, under the section header. */
.chat-rowskel {
  display: flex; align-items: center; gap: 12px;
  padding: 12px;
}
.chat-rowskel__ava { width: 34px; height: 34px; border-radius: 50%; flex: none; }
.chat-rowskel__body {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column; gap: 7px;
}
.chat-rowskel__bar { height: 11px; border-radius: 6px; }

/* ------------------------------------------------------------ the divider */

.chat-list__sep {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 16px 14px 6px;
}

/* ------------------------------------------------------------ right pane */

.chat-pane {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto auto; /* head, scroll, offer, composer */
  min-width: 0;
  height: 100%;
  /* CLIP THE SCALED RIVER. .imm-space carries transform: scale(1.8), which
   * scales its visual AND hit box (not its layout box) about origin 50% 44% —
   * ~600px to the left and ~320px above its grid cell, over the whole
   * conversation list and the header. transform makes it a stacking context that
   * paints and hit-tests above those static siblings, and it deliberately takes
   * pointer events for the manual hit-test, so it ate every click in the header
   * and the list. The lab prototype's .pane had overflow: hidden; this did not.
   * A porting error, not a design one. */
  overflow: hidden;
}

.chat-none {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 14px;
  color: var(--muted);
  text-align: center;
  padding: 24px;
}
.chat-none .rung { width: 22px; height: 22px; color: var(--line); }

.chat-head {
  display: flex; align-items: center; gap: 12px;
  padding: 16px 20px 14px;
  border-bottom: 1px solid var(--line);
  min-width: 0;
  /* Above the river content floor — the toggle and the menu must stay clickable. */
  position: relative;
  z-index: 10;
}
.chat-head__names { min-width: 0; flex: 1; }
.chat-head__names h2 {
  font-size: 17px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  display: flex; align-items: center; gap: 8px;
}
.chat-head__names h2 .rung { color: var(--accent-text); width: 12px; height: 12px; }
/* The honesty line: what the system does with this room, in the record voice. */
.chat-head__note {
  margin-top: 3px;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted);
}
.chat-back {
  display: none;
  border: 0; background: transparent; color: var(--accent-text);
  font-size: 20px; cursor: pointer; padding: 4px 8px; border-radius: var(--r-s);
}
.chat-back:active { background: var(--accent-tint); }

/* menu */
.chat-menu { position: relative; flex: none; }
.chat-menu > button {
  border: 0; background: transparent; color: var(--muted);
  font-size: 18px; letter-spacing: .1em;
  padding: 6px 10px; border-radius: var(--r-s); cursor: pointer;
  transition: background var(--t), color var(--t), transform var(--t);
}
.chat-menu > button:hover { background: var(--surface); color: var(--text); }
.chat-menu > button:active { background: var(--raised); transform: scale(.94); }
.chat-menu__pop {
  position: absolute; right: 0; top: calc(100% + 6px);
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--r-m);
  box-shadow: var(--shadow-2);
  padding: 6px;
  min-width: 220px;
  z-index: 20;
}
.chat-menu__pop button {
  display: block; width: 100%; text-align: left;
  border: 0; background: transparent; color: var(--text);
  font: inherit; font-size: 14px;
  padding: 10px 12px; border-radius: var(--r-s); cursor: pointer;
}
.chat-menu__pop button:hover { background: var(--surface); }
.chat-menu__pop button:active { background: var(--accent-tint); }
/* In flight (busy() sets aria-busy + disabled on the click tick): the item
   must read pending, not dead — dim it and stop further clicks. */
.chat-menu__pop button[aria-busy="true"],
.chat-menu__pop button:disabled { opacity: .55; pointer-events: none; }
.chat-menu__pop .whisper {
  padding: 4px 12px 8px;
  font-size: 12px; color: var(--muted);
}

/* ------------------------------------------------------------- messages */

/* The scroller, the bubbles, the dots, the skeletons and the composer all
 * live in thread.css now (docs/build-one-chat.md — one component, one
 * stylesheet). This file keeps only what is chat-shell-specific. The
 * header skeleton bars: */
.skel--name { display: inline-block; width: 130px; height: 16px; border-radius: 8px; }
.skel--note { display: inline-block; width: 210px; max-width: 55vw; height: 11px; border-radius: 6px; }

.chat-left-note {
  text-align: center;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted);
  padding: 16px 20px calc(20px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--line);
}
@media (max-width: 760px) {
  .chat-left-note { padding-bottom: calc(var(--nav-h) + 16px); }
}

/* The interview dock. It holds whatever the current turn calls for — the
   composer at a typed turn, a row of equally weighted buttons at a choice — so
   it carries the composer's border and padding only when it is NOT holding the
   composer, which brings its own. */
.chat-dock { min-width: 0; }
.chat-dock--pad {
  padding: 12px max(20px, (100% - var(--measure)) / 2) calc(16px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--line);
}

/* Review offer: one quiet affordance above the composer, facilitator only. */
.chat-offer {
  display: flex;
  justify-content: center;
  padding: 10px max(20px, (100% - var(--measure)) / 2) 0;
}
.chat-offer .btn--ghost { font-size: 13px; }

/* --------------------------------------------------------------- phone */

@media (max-width: 760px) {
  /* minmax(0,1fr), not 1fr: nowrap previews otherwise inflate the track's
     min-content width past the viewport. */
  .chat { grid-template-columns: minmax(0, 1fr); }
  .chat-list { border-right: 0; min-width: 0; }
  .chat.open .chat-list { display: none; }
  .chat:not(.open) .chat-pane { display: none; }
  .chat-list__rows { padding-bottom: calc(var(--nav-h) + 20px); }
  .chat-compose { padding-bottom: calc(var(--nav-h) + 12px + env(safe-area-inset-bottom, 0px)); }
  .chat-dock--pad { padding-bottom: calc(var(--nav-h) + 12px + env(safe-area-inset-bottom, 0px)); }
  .chat-back { display: inline-flex; }
  .chat-head { padding: 14px 14px 12px; }
  /* The back arrow belongs to the TITLE line. Centering it against the whole
     title+note block floats it between the note's lines whenever the mono
     honesty sentence wraps (critic 2, finding 4). Top-align the header row
     and give the arrow the same line box as the h2 so their centers match. */
  .chat-head { align-items: flex-start; }
  .chat-back { padding-top: 0; padding-bottom: 0; line-height: 26px; }
}

/* A thread's own unread. Cleared by opening THAT thread, which is a different
   act from entering Chat — see the comment in chat.js. */
.chat-row__unread {
  float: right;
  margin-left: 8px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: var(--r-pill);
  background: var(--accent);
  color: var(--accent-on);
  font: 500 11px/18px var(--mono);
  text-align: center;
}

/* The feedback interview, hosted in a chat pane.
   interview.css sizes the standalone column to the VIEWPORT — .ob is
   min-height:100dvh and .ob-col is height:100dvh — which is right when it owns
   the screen and overflows a pane by exactly the height of the chat header.
   These three lines re-anchor it to the pane instead. It spans the pane's
   remaining rows because the column brings its own scroller and dock. */
.chat-fb {
  grid-row: 2 / -1;
  min-height: 0;
  display: flex;
}
.chat-fb .ob--inchat {
  flex: 1;
  min-width: 0;
  min-height: 0;
  display: grid;
  justify-items: center;   /* the reading column stays a column */
}
/* Two of the three rows the standalone column declared: the brand lockup is
   gone (the thread header says Level Up), so the grid is conversation + dock,
   and the dock pins to the bottom of the PANE rather than floating under the
   last bubble. Without this the composer sat mid-pane and read as unfinished. */
.chat-fb .ob--inchat .ob-col {
  width: 100%;
  min-width: 0;
  height: 100%;
  min-height: 0;
  grid-template-rows: minmax(0, 1fr) auto;
}

/* A pending peer thread. A div, not a button: the row itself is not clickable,
   because the only two things you can do with it are the two named actions. */
.chat-row--pending { cursor: default; }
.chat-row--pending:hover { background: transparent; }
.chat-pending__acts { margin-top: 10px; }

/* ------------------------------- leave, revealed on the thread row itself

   A peer thread row is a div wrapping the swipe mechanism (swiperow.css):
   the main button opens the thread, the visible chevron is the non-gesture
   reveal (SC 2.5.7), and the well holds Leave — which arms a second press
   before anything is written. Facilitator, feedback and pending rows are
   not swipe rows: there is nothing to leave in them.

   The row's inset stroke moves to an ::after overlay because the sliding
   body needs an opaque background (it is what covers the well when shut),
   and an opaque child would paint over the container's own inset shadow. */

.chat-row--thread { padding: 0; box-shadow: none; }
.chat-row--thread::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1px var(--raised);
  pointer-events: none;
  z-index: 2;
  transition: box-shadow var(--t);
}
.chat-row--thread:hover { background: var(--canvas); }
.chat-row--thread:hover::after { box-shadow: inset 0 0 0 1px var(--line); }
.chat-row--thread.selected { background: var(--canvas); }
.chat-row--thread.selected::after { box-shadow: inset 0 0 0 1px var(--accent-line-soft); }

.chat-row--thread .swrow__body {
  display: flex;
  align-items: stretch;
  background: var(--canvas);
  border-radius: inherit;
}
/* selected: the tint rides the sliding body — it is the opaque layer. */
.chat-row--thread.selected .swrow__body {
  background-color: var(--canvas);
  background-image: linear-gradient(var(--accent-tint), var(--accent-tint));
}
.chat-row--thread .chat-row__main {
  flex: 1;
  min-width: 0;
  display: block;
  text-align: left;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  padding: 12px 0 12px 12px;
  cursor: pointer;
}
.chat-row--thread .swrow-open { align-self: center; margin: 0 8px; }

/* The armed well sits in a 320px column: tighter padding, smaller line. */
.chat-row--thread .swrow__acts { padding: 0 12px; gap: 10px; }
.chat-row--thread .swrow-why { font-size: 10.5px; }

/* Armed: the row IS the confirm surface. The well leaves its absolute perch
   and sizes the row to the stated consequence plus the deciding press —
   a fixed row height clipped the sentence, and a consequence you cannot
   read is not a confirmation. The slid-away body is display:none here; the
   disarm path (module close -> onClose) rebuilds the resting well anyway. */
.chat-row--thread.swrow--armed .swrow__body { display: none; }
.chat-row--thread.swrow--armed .swrow__acts {
  position: static;
  visibility: visible;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
  padding: 12px;
}
.chat-row--thread.swrow--armed .swrow-why { flex: none; }
.chat-row--thread.swrow--armed .swrow-act { align-self: flex-end; }


/* ------------------------------------------------------- immersive mode: the river
 *
 * docs/build-immersive-river.md, 2026-08-10. A conversation is a river running
 * away from you; depth reads as SIZE because the far end is pitched back to
 * centre. immersive.js owns the behaviour and the settled constants; what
 * matters here:
 *
 *  - .imm-space is a perspective volume scaled by the zoom; .imm-world carries
 *    the inverse camera pose so pitch swings the head, not the river.
 *  - a message is TYPE on the stream — no background, no border-radius, no
 *    bubble. Both voices are white on the dark ground (theme-aware tokens, so a
 *    light page gets dark type, not an invisible halo). Face, weight, side,
 *    justification and glow carry the who — not colour.
 *  - clicks land on .imm-space and immersive.js hit-tests by hand; the message
 *    boxes are pointer-events: none so the near one cannot swallow a click aimed
 *    at a far one.
 *  - reduced motion does not build the river at all (immersive.js returns
 *    early): the plain, scrollable transcript stays, a static readable stack.
 *    The block at the foot kills anything that slips through.
 *  - phones never see any of this: at or below the app's one breakpoint (760px,
 *    BREAKPOINT in immersive-core.js) the mode does not mount — tablet and
 *    desktop only (owner, 2026-08-11).
 */

.chat-imm-toggle {
  flex: none;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--muted);
  border-radius: var(--r-pill);
  padding: 6px 12px;
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
  transition: color var(--t), border-color var(--t);
}
.chat-imm-toggle:hover { color: var(--text); }
.chat-imm-toggle[aria-pressed="true"] { color: var(--accent-text); border-color: var(--accent-line-soft); }

.chat-pane--imm { position: relative; }

/* THE SPACE — a real perspective volume in the messages row. The zoom scales
 * the finished projection (not a camera move): the geometry is untouched and
 * the type size is the type size. */
.imm-space {
  grid-row: 2 / 3;
  position: relative;
  z-index: 0;                          /* the content floor: chrome sits above it */
  overflow: hidden;
  /* The stage is a drag-scrub surface for ANY pointer (immersive.js armDrag):
   * without this the browser claims a touch as a scroll gesture and cancels
   * the pointer stream mid-drag. The stage is not a native scroller, so
   * nothing is lost. Not device detection — it binds to the surface. */
  touch-action: none;
  perspective: 2500px;                 /* R.persp — a long, filmic lens */
  perspective-origin: 50% 44%;
  transform: scale(var(--imm-zoom, 1.8));  /* R.zoom */
  transform-origin: 50% 44%;
}
/* The world holds the inverse of the camera pose; its origin IS the camera. */
.imm-world {
  position: absolute; inset: 0;
  transform-style: preserve-3d;
  transform-origin: 50% 44% 0;
  will-change: transform;
}
/* The message box is a transparent rectangle; only manual hit-testing picks
 * between overlapping ones, so the box itself never takes the pointer. */
.imm-msg {
  position: absolute; left: 50%; top: 44%;
  width: min(56vw, 620px); margin-left: min(-28vw, -310px);
  transform-style: preserve-3d;
  text-align: center;
  pointer-events: none;
}

.imm-say { font-size: 44px; line-height: 1.24; letter-spacing: -.015em; }  /* R.size */
/* Coach — the display face, flat, no glow, aligned left. */
.imm-coach .imm-say {
  font-family: var(--display); font-weight: 600;
  color: var(--imm-type); text-shadow: none; text-align: left;
}
/* You — a bold sans, lit, aligned right. */
.imm-you .imm-say {
  font-family: var(--body); font-weight: 600;
  font-size: 39px; letter-spacing: -.02em;
  color: var(--imm-type); text-shadow: var(--imm-glow-soft); text-align: right;
}

/* The credit line is on EVERY message: a three-sentence reply is three objects
 * in the river, and by the time the third is in front of you the first is far
 * downstream. The coach's credit is the one place a hue costs nothing. */
.imm-credit {
  font-family: var(--mono); font-size: 11px; letter-spacing: .16em;
  text-transform: uppercase; opacity: .6; margin-bottom: 8px;  /* R.aop */
  color: var(--text-mid);
}
.imm-coach .imm-credit { text-align: left; color: var(--accent-text); }
.imm-you .imm-credit { text-align: right; }

/* A word in flight is dim and strongly lit — arriving out of the dark and
 * settling. It lands at opacity 1, transform none, fill-mode both, so the
 * spans are already final and are NEVER swapped back to plain text (that swap
 * relaid the line and nudged every word sideways on the right-justified voice). */
.imm-w {
  display: inline-block; white-space: pre;
  animation: imm-word-in var(--wdur, 160ms) cubic-bezier(.2, .7, .3, 1) both;
}
.imm-coach .imm-w { --fz: -900px; --fy: 0px; }
.imm-you .imm-w { --fz: -120px; --fy: 140px; }
.imm-w.imm-flying { color: var(--imm-type-dim); text-shadow: var(--imm-glow-strong); }
@keyframes imm-word-in {
  from { opacity: 0; transform: translate3d(var(--wx, 0px), calc(var(--fy) + var(--wy, 0px)), var(--fz)); }
  to { opacity: 1; transform: none; }
}

/* THE TRANSPORT, bottom-right: the two one-message steps (the single-pointer
 * alternative SC 2.5.7 requires for the stage drag) above Return to now.
 * A right-aligned COLUMN, because above the breakpoint the 320px list column
 * is always beside the pane: at a 761px window the pane is ~440px wide, and
 * three pills in a row would collide with the corner card. The column fits
 * every width the river exists at, without a second breakpoint. Return to now
 * dims rather than hides when the camera is already home, so it does not
 * appear and disappear under the cursor. */
.imm-transport {
  position: absolute; right: max(24px, (100% - var(--measure)) / 2);
  bottom: 92px; z-index: 20; display: flex;             /* above the content */
  flex-direction: column; align-items: flex-end; gap: 8px;
}
.imm-now, .imm-step {
  all: unset; cursor: pointer; font-family: var(--mono); font-size: 9.5px;
  letter-spacing: .14em; text-transform: uppercase; color: var(--accent-text);
  border: 1px solid var(--accent-line-soft); border-radius: var(--r-pill);
  padding: 5px 12px; transition: opacity var(--t);
}
.imm-now[data-home="1"] { opacity: .28; cursor: default; }
.imm-now:focus-visible, .imm-step:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 2px; }

/* THE HISTORY CARD — the real scroller, complete and scrolled to the end,
 * bottom-left. It keeps its ordinary semantics (a labelled region, never
 * role="button", which would make every bubble presentational to a screen
 * reader). Near-transparent at rest; fully legible the moment it has attention.
 * Focusable, so the arrow keys can scrub the river from the transcript — the
 * keyboard path. The way back is the header toggle, Escape, and the transparent
 * button overlaid here. A fixed height, so that overlay matches it exactly. */
.chat-pane--imm .chat-scroll {
  position: absolute;
  left: 16px;
  bottom: 92px;
  width: 264px;
  height: 168px;
  opacity: .5;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-m);
  box-shadow: var(--shadow-1);
  padding: 10px 12px;
  overflow-y: auto;
  cursor: default;
  transition: opacity var(--t);
  z-index: 20;                         /* the mini transcript, above the content */
}
.chat-pane--imm .chat-scroll:hover,
.chat-pane--imm .chat-scroll:focus-visible { opacity: 1; outline: 2px solid var(--accent-line-soft); outline-offset: 2px; }
.chat-pane--imm .chat-scroll .msg { max-width: 100%; font-size: 11.5px; padding: 4px 8px; cursor: pointer; }
.chat-pane--imm .chat-scroll .chat-day { font-size: 8.5px; padding: 8px 0 4px; }
.chat-pane--imm .chat-scroll .grp__stamp { display: none; }

/* The transparent THIRD exit, exactly over the corner card. Its accessible name
 * (chat.immersive.back) is the control; the card under it stays a readable
 * region. z-index just above the card so the click and Enter land on it. */
.imm-card-btn {
  position: absolute;
  left: 16px;
  bottom: 92px;
  width: 264px;
  height: 168px;
  border: 0;
  background: transparent;
  border-radius: var(--r-m);
  cursor: pointer;
  z-index: 21;
  /* A drag here scrolls the CARD (immersive.js restates it by hand — the
   * button is the scroller's sibling, outside its native scroll chain), so
   * the browser must keep delivering pointermove instead of claiming the
   * touch as a gesture. A tap still exits. */
  touch-action: none;
}
.chat-pane--imm:has(.imm-card-btn:hover) .chat-scroll,
.chat-pane--imm:has(.imm-card-btn:focus-visible) .chat-scroll { opacity: 1; }
.imm-card-btn:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 2px; }

.chat-pane--imm .chat-offer { position: relative; z-index: 15; }
/* The composer stays above the river content floor — it must never be eaten. */
.chat-compose { position: relative; z-index: 20; }

/* A DECISION CARD, pinned. While unresolved it renders centre-stage at full
 * size, interactive, above the river — never in the corner at reduced opacity.
 * Time slows around it and it holds here until an option is selected
 * (immersive.js, build-immersive-river.md §6). The corner card is lifted to full
 * opacity meanwhile, because CSS opacity on the scroller would otherwise cap
 * this pinned child. The legibility fallback (a small viewport, a card taller
 * than the stage) yields to full instead — handled in JS. */
.chat-pane--imm-cardheld .chat-scroll { opacity: 1; }
.imm-pinned {
  position: fixed;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  z-index: 20;
  width: min(92vw, var(--measure));
  max-height: 82vh;
  overflow-y: auto;
  opacity: 1;
  pointer-events: auto;
}

/* NO phone rules for the river (owner, 2026-08-11). At or below the app's one
 * breakpoint — 760px, the same threshold as BREAKPOINT in immersive-core.js —
 * the mode never MOUNTS: immersive.js returns early, so the toggle is absent
 * (not disabled) and none of these selectors can exist there. The old 760px
 * block — the scale(1.05) ease-back, the reduced river type, the card and
 * transport repositioned above the nav — was dead code and is deliberately
 * gone, not an oversight. */

/* Reduced motion: the river is never built (immersive.js returns early), so the
 * plain transcript stays — a static, readable stack, with scrolling for the
 * scrub. These rules kill anything that could still move if a node slipped in. */
@media (prefers-reduced-motion: reduce) {
  .imm-world, .imm-msg, .imm-w, .chat-imm-toggle {
    animation: none;
    transition: none;
  }
  .imm-w { color: var(--imm-type); text-shadow: var(--imm-glow-soft); }
}
