/* Talk — violet flame.
 *
 * Benji, 2026-08-16: "Update and modernize the styles. Use violet flame colors."
 *
 * The palette runs cool-to-hot the way a flame does — deep violet at the base,
 * rising through orchid to a pale core — rather than one purple reused six
 * times. The heat is spent only where it means something: the record button,
 * the live waveform, the caret in the box she is typing into.
 *
 * The constraint is contrast, not mood. This is read by someone who may be 85,
 * possibly in a bright room without her glasses. Every text tone below clears
 * WCAG AA against its own background, and a light scheme is provided because
 * a dark UI in daylight is genuinely hard to read — a beautiful screen she
 * cannot read is a failed screen.
 */

:root {
  color-scheme: dark light;

  /* base — deep violet, not grey */
  --bg: #150a24;
  --panel: #1e0f33;
  --raised: #2a1547;
  --line: #3a2059;

  /* flame */
  --flame-deep: #6d28d9;
  --flame: #a855f7;
  --flame-hot: #d946ef;
  --flame-core: #f0abfc;
  --flame-grad: linear-gradient(135deg, var(--flame-deep) 0%, var(--flame) 55%, var(--flame-hot) 100%);

  /* ink */
  --ink: #f4edfd;      /* 15.8:1 on --bg  */
  --dim: #bda9dc;      /*  7.1:1 on --bg  */

  --mine: #4c1d95;
  --theirs: #241338;

  --radius: 20px;
  --radius-sm: 14px;
  --tap: 3.25rem;      /* every control clears the 44px minimum comfortably */
}

/* Daylight. Not an afterthought: this is the condition an older person most
   often reads in, and pure-dark UIs wash out badly in it. */
@media (prefers-color-scheme: light) {
  :root {
    --bg: #f7f4fb;
    --panel: #ffffff;
    --raised: #f2ebfd;
    --line: #e2d6f5;
    --ink: #241a33;    /* 14.6:1 on --bg */
    --dim: #5f5178;    /*  6.2:1 on --bg */
    --mine: #ede4ff;
    --theirs: #ffffff;
    --flame: #7c3aed;  /* darkened so it still passes on a light ground */
  }
  .msg { border: 1px solid var(--line); }
  .msg.mine { border-color: #cdb8f5; }
  .who { color: var(--flame-deep); }
}

* { box-sizing: border-box; }

/* This must outrank every layout rule below it.
 *
 * The browser's own stylesheet hides [hidden] with `display:none`, but that is
 * an attribute selector — any author rule using an ID beats it. `#app { display:
 * grid }` therefore silently un-hid the app, and the signed-out gate was being
 * drawn on top of a fully rendered, fully interactive screen. The recording
 * panel never hid either, and neither did the room menu.
 *
 * Found by loading the page in a real browser: computed display was "grid" on
 * an element carrying the hidden attribute. Nothing in the source looked wrong. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--ink);
  font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
}

/* Fluid type: scales with the viewport instead of snapping at a breakpoint,
   so a 4-inch phone and a 12-inch tablet both land somewhere readable. */
body[data-scale="large"]  { font-size: clamp(20px, 2.6vw + 12px, 26px); line-height: 1.6; }
body[data-scale="normal"] { font-size: clamp(16px, 1.2vw + 12px, 19px); line-height: 1.5; }

/* ---------- gate ---------- */

#gate {
  height: 100%;
  display: grid;
  place-content: center;
  text-align: center;
  padding: 2rem;
  gap: 1.1rem;
}
#gate h1 {
  font-size: 2.2rem;
  margin: 0;
  letter-spacing: 2px;
  background: var(--flame-grad);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
#gate p { color: var(--dim); max-width: 24rem; margin: 0 auto; }
#gate p:empty { display: none; }

#gate form {
  display: grid;
  gap: .8rem;
  width: min(22rem, 100%);
  margin: .4rem auto 0;
  text-align: left;
}
#gate label { font-size: .9em; color: var(--dim); }
#gate input {
  font: inherit;
  color: var(--ink);
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: .85rem 1rem;
  min-height: var(--tap);
  width: 100%;
  transition: border-color .15s, box-shadow .15s;
}
#gate input:focus {
  outline: none;
  border-color: var(--flame);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--flame) 35%, transparent);
}
/* The code is the one thing she reads off a screen and types. Give it room. */
#code { font-size: 1.5em; letter-spacing: .35em; text-align: center; font-family: "Courier New", monospace; }
#gate button {
  font: inherit;
  border: 0;
  border-radius: var(--radius);
  padding: .9rem 1.1rem;
  min-height: var(--tap);
  background: var(--flame-grad);
  color: #fff;
  cursor: pointer;
  box-shadow: 0 4px 16px color-mix(in srgb, var(--flame) 40%, transparent);
}
#gate button:active { transform: scale(.99); }
#gate button.quiet {
  background: none;
  color: var(--dim);
  box-shadow: none;
  text-decoration: underline;
  min-height: 2.5rem;
  padding: .4rem;
}
#gate button[disabled] { opacity: .55; cursor: default; }

/* ---------- shell ---------- */

/* A grid item's default min-width is auto, so a row whose content is wider than
   the shell widens the COLUMN rather than overflowing inside it — and then the
   header and the timeline get dragged out to match, and the whole page scrolls
   sideways. Adding the photo button made the composer's min-content 517px in a
   500px shell and did exactly that. Setting this on every child means the next
   control added to any row cannot repeat it. */
#app > * { min-width: 0; }

#app {
  height: 100%;
  display: grid;
  /* bar, pull indicator, timeline, composer. The pull row is 0 high at rest
     and costs nothing until somebody drags. */
  grid-template-rows: auto auto 1fr auto;
  max-width: 46rem;
  margin: 0 auto;
}

#bar {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: max(.75rem, env(safe-area-inset-top)) 1.1rem .75rem;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  position: relative;
}
/* the flame, as a hairline under the header */
#bar::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 2px;
  background: var(--flame-grad);
  opacity: .9;
}
#room-name { flex: 1; min-width: 0; font-size: .95em; color: var(--dim);
             overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#rooms-btn {
  background: none; border: 0; color: var(--ink);
  font-size: 1.3em; cursor: pointer;
  min-width: 2.5rem; min-height: 2.5rem;
  border-radius: 50%;
}
#rooms-btn:active { background: var(--raised); }

#state { width: .7rem; height: .7rem; border-radius: 50%; background: var(--dim); }
#state.ok  { background: #4ade80; box-shadow: 0 0 8px #4ade8066; }
#state.bad { background: #fb7185; box-shadow: 0 0 8px #fb718566; }
#state.busy{ background: var(--flame-hot); box-shadow: 0 0 8px var(--flame-hot); }

/* CAREFUL: #rooms is the app's legacy room-switcher PANEL, and this rule makes
   any element carrying that id an absolutely-positioned overlay.
   style.css is shared with admin.html, so a <tbody id="rooms"> added there
   silently became a floating panel over the header — the rooms table appeared
   as a band across the top of People, permanently, on top of the h1. An id in a
   shared stylesheet is a global name; admin's table is #rooms-list. */
#rooms {
  position: absolute;
  top: 3.6rem; left: 0; right: 0;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  display: flex; flex-direction: column;
  max-height: 70vh; overflow-y: auto;
  z-index: 5;
  box-shadow: 0 12px 28px #00000055;
}
#rooms .room {
  background: none; border: 0; border-bottom: 1px solid var(--line);
  color: var(--ink); text-align: left; font: inherit;
  padding: 1.05rem 1.2rem; cursor: pointer;
  min-height: var(--tap);
}
#rooms .room:active { background: var(--raised); }

/* ---------- timeline ---------- */

#timeline {
  overflow-y: auto;
  padding: 1.1rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
}

.msg {
  max-width: 85%;
  padding: .8rem 1.05rem;
  border-radius: var(--radius);
  background: var(--theirs);
  align-self: flex-start;
  border-bottom-left-radius: var(--radius-sm);
  animation: rise .22s ease-out;
}
.msg.mine {
  background: var(--mine);
  align-self: flex-end;
  border-bottom-left-radius: var(--radius);
  border-bottom-right-radius: var(--radius-sm);
}
@keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

.who { font-size: .78em; color: var(--flame-core); margin-bottom: .3rem; letter-spacing: .4px; }
.text { white-space: pre-wrap; word-wrap: break-word; }
.caption { font-size: .85em; color: var(--dim); margin-top: .4rem; }
.photo { max-width: 100%; border-radius: var(--radius-sm); display: block; }
.msg audio { width: min(20rem, 60vw); }
.when { display: block; font-size: .7em; color: var(--dim); margin-top: .4rem; text-align: right; }

/* ---------- composer ---------- */

#rec-hint {
  margin: 0;
  padding: .5rem 1.1rem 0;
  color: var(--flame-core);
  font-size: .9em;
  text-align: center;
}
#rec-hint:empty { display: none; }

#composer {
  display: flex;
  gap: .65rem;
  padding: .8rem 1.1rem max(.8rem, env(safe-area-inset-bottom));
  background: var(--panel);
  border-top: 1px solid var(--line);
  align-items: flex-end;
}
#input {
  flex: 1;
  /* A flex item's default min-width is auto, not 0, so the textarea refused to
     shrink below its content width and pushed the send button off the right
     edge once the photo button joined the row. Adding a control to a flex row
     is exactly when this bites. */
  min-width: 0;
  resize: none;
  font: inherit;
  color: var(--ink);
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: .7rem 1rem;
  /* Match the buttons, and give the placeholder a full line to sit on: a
     rows="1" box with a placeholder that wraps clips it mid-sentence, which is
     exactly how "Type here, or tap the microphone" reached a phone as
     "Type here, or tap the". */
  min-height: var(--tap);
  max-height: 160px;
  caret-color: var(--flame-hot);
  transition: border-color .15s, box-shadow .15s;
}
#input:focus {
  outline: none;
  border-color: var(--flame);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--flame) 35%, transparent);
}

#send {
  flex: 0 0 auto;
  width: var(--tap); height: var(--tap);
  border-radius: 50%;
  cursor: pointer;
  font-size: 1.35rem;
  line-height: 1;
  transition: transform .12s;
}
/* Mic and photo are the two ways she puts something INTO the conversation, so
   they look like each other: a labelled pill, not a bare icon. */
#rec, #photo-btn {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: .35rem;
  min-height: var(--tap);
  padding: 0 .85rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--raised);
  color: var(--ink);
  font: inherit; font-size: 1.05rem;
  cursor: pointer;
  transition: transform .12s;
}
#rec span, #photo-btn span { font-size: .82rem; color: var(--dim); }
#rec:active, #photo-btn:active { transform: scale(.94); }
/* Below this width the words cost more than they earn AT NORMAL SCALE: the
   icons plus their aria-labels still say what they are, and the send button
   stays reachable. At large scale the buttons get their own row instead (see
   below), so their labels survive — that is the setting where they matter most. */
@media (max-width: 30rem) {
  body[data-scale="normal"] #rec span,
  body[data-scale="normal"] #photo-btn span { display: none; }
}

/* Large scale — the memoir setting, for someone who may be 85.
 *
 * Four controls in one row squeezes the text box to about 85px on a phone:
 * measured, not guessed. Squeezing is the wrong answer when the whole point of
 * this scale is that things are big and obvious, so the two ways she puts
 * something INTO the conversation get a row of their own, labelled, and the
 * text box gets the full width underneath. */
body[data-scale="large"] #composer {
  display: grid;
  grid-template-columns: auto auto 1fr auto;
  row-gap: .6rem;
  align-items: center;
}
body[data-scale="large"] #rec       { grid-area: 1 / 1; }
body[data-scale="large"] #photo-btn { grid-area: 1 / 2; }
body[data-scale="large"] #input     { grid-area: 2 / 1 / 3 / 4; }
body[data-scale="large"] #send      { grid-area: 2 / 4; }
#send {
  border: 0;
  background: var(--flame-grad);
  color: #fff;
  box-shadow: 0 4px 16px color-mix(in srgb, var(--flame) 45%, transparent);
}
#send:active { transform: scale(.94); }
body[data-scale="large"] #send { width: 3.9rem; height: 3.9rem; font-size: 1.55rem; }
body[data-scale="large"] #rec,
body[data-scale="large"] #photo-btn { min-height: 3.9rem; font-size: 1.3rem; }

/* ---------- photographs ---------- */

/* The photo button carries a word as well as an icon. An icon alone is a guess,
   and this is the single most valuable action in the app: a picture of the 1962
   kitchen gets more out of an 84-year-old than any question can. */
/* The tray of photos waiting to go. */
#tray {
  display: flex; flex-wrap: wrap; gap: .5rem;
  padding: .5rem .9rem 0;
}
.thumb {
  position: relative;
  width: 4.5rem; height: 4.5rem;
  border-radius: var(--radius-sm);
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--raised);
}
.thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Still uploading: dimmed, so "not yet" is visible without a spinner to explain. */
.thumb.sending img { opacity: .45; }
.thumb.failed { border-color: #fb7185; }
.thumb.failed img { opacity: .3; }
.thumb-x {
  position: absolute; top: 2px; right: 2px;
  width: 1.5rem; height: 1.5rem;
  border: 0; border-radius: 50%;
  background: rgba(0,0,0,.65); color: #fff;
  font-size: 1rem; line-height: 1; cursor: pointer;
}
.thumb-retry {
  position: absolute; inset: auto 0 0 0;
  border: 0; background: #fb7185; color: #fff;
  font: inherit; font-size: .7rem; padding: .15rem 0; cursor: pointer;
}

/* Photos inside the transcript. Generous, because they are the point — and
   capped so one portrait shot doesn't push the reply off the screen. */
.photos { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: .45rem; }
.photo {
  max-width: min(100%, 22rem);
  max-height: 20rem;
  border-radius: var(--radius-sm);
  display: block;
  cursor: zoom-in;
  background: var(--raised);
  min-width: 3rem; min-height: 3rem;   /* holds space while the bytes arrive */
}

/* ---------- recording ---------- */

#recbar {
  display: grid;
  gap: .85rem;
  padding: 1.1rem 1.1rem max(1.1rem, env(safe-area-inset-bottom));
  background: var(--panel);
  border-top: 2px solid transparent;
  border-image: var(--flame-grad) 1;
}
#recmeta { display: flex; align-items: center; gap: .65rem; color: var(--dim); font-size: .9em; }
#recmeta .dot {
  width: .85rem; height: .85rem; border-radius: 50%;
  background: var(--flame-hot);
  box-shadow: 0 0 10px var(--flame-hot);
  animation: pulse 1.6s ease-in-out infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: .3; transform: scale(.85); } }
#recmeta .hint { margin-left: auto; }
#wave { width: 100%; height: 56px; display: block; }

/* The largest control in the app. She may need to find it mid-sentence. */
#stop {
  width: 100%;
  padding: 1.15rem;
  font: inherit;
  font-size: 1.15em;
  border: 0;
  border-radius: var(--radius);
  background: var(--flame-grad);
  color: #fff;
  cursor: pointer;
  box-shadow: 0 6px 20px color-mix(in srgb, var(--flame) 40%, transparent);
  min-height: var(--tap);
}
#stop:active { transform: scale(.99); }

/* A pulsing dot beside someone with a vestibular disorder is not a nicety to
   disable — and neither is a screen that animates every message on arrival. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  #recmeta .dot { opacity: 1; }
}

/* ---------- voice, before it becomes words ---------- */

/* Her recording is playable in the thread the moment it is saved, before any
   transcript exists. The audio is the artefact; the words are derived from it,
   and derivation can wait. */
.voice { width: min(100%, 20rem); display: block; }
.pending-note {
  font-size: .82rem;
  color: var(--dim);
  margin-top: .35rem;
  font-style: italic;
}

/* ---------- the way out ---------- */

/* Benji: "I can only see that convo. No menu no sign out no admin." The app was
   a one-way door. */
/* Both panels: the account menu under the hamburger, the room list under the
   room name. Same behaviour, different owners -- and deliberately not the same
   list, because switching rooms should never be a near-miss on Sign out. */
nav.panel, #menu {
  position: absolute;
  /* Opens under the hamburger that opened it. It used to open on the right
     while the button sits on the left, so the thumb that tapped it then had to
     travel the width of the screen to use it. */
  top: 3.4rem; left: .6rem;
  min-width: 14rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  box-shadow: 0 14px 34px #00000066;
  z-index: 6;
  overflow: hidden;
  display: flex; flex-direction: column;
}
nav.panel .item, #menu .item {
  background: none; border: 0;
  border-bottom: 1px solid var(--line);
  color: var(--ink); font: inherit; text-align: left;
  padding: 1rem 1.15rem;
  min-height: var(--tap);
  cursor: pointer;
}
nav.panel .item:last-child, #menu .item:last-child { border-bottom: 0; }
nav.panel .item:active, #menu .item:active { background: var(--raised); }

/* The room list opens under the name, which sits to the right of the burger. */
#room-switcher.panel { left: 3.4rem; right: .6rem; min-width: 0; }
/* Not a button, so it has to LOOK like a thing you can press. */
#room-name { cursor: pointer; }
/* The control that opens the room list. A button, and it says "Rooms" -- the
   chevron marks it as opening something rather than doing something. */
#rooms-trigger {
  background: none; border: 0; color: var(--ink);
  font: inherit; font-size: .95em; cursor: pointer;
  padding: .35rem .5rem; border-radius: var(--radius-sm);
  flex: 0 0 auto; min-height: 2.5rem;
}
#rooms-trigger::after { content: " ▾"; color: var(--dim); font-size: .85em; }
#rooms-trigger:active { background: var(--raised); }
nav.panel .item.empty { color: var(--dim); cursor: default; }

/* ---------- rooms in the menu ---------- */

/* A room line carries two facts: who is in it, and what kind of thing it is.
   The kind matters because a memoir and a room sit in the same list and are
   not remotely the same act. */
nav.panel .item.room, #menu .item.room { display: flex; align-items: baseline; gap: .6rem; }
nav.panel .room-name, #menu .room-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
nav.panel .room-kind, #menu .room-kind { flex: 0 0 auto; font-size: .72em; color: var(--dim); letter-spacing: .4px; }
/* The one you are already in. A tick would need a column; the flame edge says
   "here" without widening anything. */
nav.panel .item.room.current, #menu .item.room.current { border-left: 3px solid var(--flame-core); padding-left: calc(1.15rem - 3px); }
nav.panel .item.room.current .room-name, #menu .item.room.current .room-name { color: var(--flame-core); }
.menu-rule { border: 0; border-top: 1px solid var(--line); margin: 0; }

/* The menu button sits at the left of the header, so it is where a thumb
   already goes and never competes with the connection dot. */
#rooms-btn { order: -1; }

/* A recording that failed to upload. Playable, so she can hear it was caught,
   with its own retry — the microphone must never be held hostage by it. */
.failed-take { border: 1px solid #fb7185; }
.failed-take .retry {
  margin-top: .5rem;
  border: 0; border-radius: 999px;
  background: var(--flame-grad); color: #fff;
  font: inherit; font-size: .85rem;
  padding: .45rem 1rem; min-height: 2.6rem;
  cursor: pointer;
}
.failed-take .retry[disabled] { opacity: .6; }

/* The build line in the menu: present, tappable, and quiet enough not to
   compete with Sign out. */
#menu .version {
  color: var(--dim);
  font-size: .8rem;
  text-decoration: none;
  padding: .7rem 1.15rem;
  min-height: 0;
}

/* ---------- what you can do to one message ---------- */

.msg { cursor: pointer; }
.msg[data-open="1"] { outline: 2px solid var(--flame); outline-offset: 2px; }

/* A machine's reading of a recording, marked as such. Quoted and dimmer than
   the message, because it is an interpretation and not the thing said. */
.transcript {
  margin-top: .5rem; padding-left: .7rem;
  border-left: 2px solid var(--line);
  color: var(--dim); font-style: italic;
  white-space: pre-wrap; word-wrap: break-word;
}

.msg-actions { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .7rem; }
.msg-action {
  font: inherit; font-size: .8em;
  background: var(--raised); color: var(--ink);
  border: 1px solid var(--line); border-radius: 999px;
  padding: .5rem .9rem; min-height: 2.6rem; cursor: pointer;
}
.msg-action:active { background: var(--flame-deep); }
.msg-action[disabled] { opacity: .55; cursor: default; }
/* Spec B8, said where it is true rather than in a settings page nobody opens. */
.msg-note { flex: 1 1 100%; font-size: .72em; color: var(--dim); margin-top: .2rem; }

/* ---------- replying ---------- */
/* Benji, 2026-08-31: "Let's also add a 'swip to reply' feature." */

/* What you are answering, while you type. Above the composer, not inside it, so
   it never competes with the text box for room as the message grows. */
#replying {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .5rem .75rem;
  margin: 0 .5rem;
  background: var(--raised);
  border-left: 3px solid var(--flame);
  border-radius: 10px 10px 0 0;
  font-size: .85em;
}
#replying-body { flex: 1; min-width: 0; }
#replying-who { display: block; color: var(--flame); font-size: .85em; }
/* One line, clipped. A long quote must never push the composer off a phone. */
#replying-text {
  display: block;
  color: var(--dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#replying-cancel {
  flex: none;
  min-width: var(--tap); min-height: var(--tap);
  background: none; border: 0; color: var(--dim);
  font-size: 1.4rem; line-height: 1; cursor: pointer;
}
#replying-cancel:hover { color: var(--ink); }

/* The quote inside a sent message. Subordinate on purpose: it is context for
   the words below it, never a message in its own right. */
.quote {
  border-left: 3px solid var(--flame);
  padding: .25rem .5rem;
  margin-bottom: .4rem;
  background: rgb(0 0 0 / .18);
  border-radius: 4px;
  font-size: .85em;
  cursor: pointer;
}
@media (prefers-color-scheme: light) { .quote { background: rgb(0 0 0 / .05); } }
.quote-who { color: var(--flame); font-size: .9em; }
/* Two lines at most. A quote taller than its own reply inverts the hierarchy. */
.quote-text {
  color: var(--dim);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* The parent is not visible to THIS reader -- expired, dropped or unsent. Not
   an error: their copy genuinely differs from the sender's. */
.quote.gone { border-left-color: var(--line); cursor: default; }
.quote.gone .quote-text { font-style: italic; opacity: .75; }

/* Mid-swipe. No transition here -- the bubble must track the thumb exactly, and
   the spring back is applied in JS when the finger lifts. */
.msg[data-swiping] { user-select: none; }
/* Armed: past the threshold, so lifting now will reply. The only feedback that
   the gesture has committed to anything. */
.msg[data-swiping="armed"] { box-shadow: -3px 0 0 var(--flame); }

/* Where a tapped quote landed you. Without it the jump is indistinguishable
   from the timeline having moved on its own. */
.msg[data-found] { outline: 2px solid var(--flame); outline-offset: 2px; }

/* A gesture is an accelerator, never the only way in (spec D8) -- and for
   anybody who cannot drag, the tap-to-open actions panel carries Reply. */
@media (prefers-reduced-motion: reduce) {
  .msg { transition: none !important; }
}


/* ---------- pull down to refresh ---------- */
/* Benji, 2026-08-31: "I need the ability to pull down and refresh."
   A real grid row rather than an overlay: growing it pushes the timeline down,
   which IS the pull-to-refresh look, and needs no positioning tricks that would
   have to know how tall the bar is. At rest it is 0 high and shifts nothing. */
#pull {
  height: 0;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  color: var(--dim);
  font-size: .85em;
  opacity: 0;
  pointer-events: none;      /* never intercepts the gesture that reveals it */
}
#pull-icon {
  display: inline-block;
  color: var(--flame);
  font-size: 1.1em;
  transition: transform .18s ease-out;
}
@media (prefers-reduced-motion: reduce) { #pull-icon { transition: none; } }

/* The way out of the actions panel. Benji, 2026-08-31: "This really needs an x
   for dismiss." Pushed to the far right of the row so it never sits where a
   thumb is heading for Reply, and given the same tap target as everything else
   here -- a close control you have to aim at is its own small insult. */
.msg-close {
  order: 99;                 /* last visually, first in the DOM for keyboards */
  margin-left: auto;
  font: inherit;
  font-size: 1.15em;
  line-height: 1;
  background: none;
  color: var(--dim);
  border: 1px solid var(--line);
  border-radius: 999px;
  min-width: 2.6rem; min-height: 2.6rem;
  cursor: pointer;
}
.msg-close:hover, .msg-close:focus-visible { color: var(--ink); }

/* ---------- who owns the gesture: the browser, or us ---------- */
/* Benji, 2026-08-31: "Drag right is not working half of the time. When it works
   nothing still happens."
   
   Both symptoms, one omission: there was no touch-action in this stylesheet at
   all. The default is `auto`, which means the BROWSER owns panning -- and the
   instant it decides a drag is a pan it seizes the gesture and fires
   pointercancel at the element. The swipe handler treated that as "abandon", so
   the bubble sprang back and nothing happened. Whether it got seized depended on
   the angle of the thumb, which is exactly "half the time".
   
   pan-y hands vertical scrolling to the browser and keeps horizontal for us, so
   a sideways drag on a message is never stolen and pointercancel stops firing
   for it.
   
   Worth saying plainly: NO SYNTHETIC POINTER EVENT TEST CAN CATCH THIS. Dispatching
   PointerEvents bypasses the compositor entirely, so the gesture always "worked"
   in the walk while failing on a real thumb. It needed a person with a phone. */
.msg { touch-action: pan-y; }

/* Nothing scrolls in the header, so the pull gesture there is ours outright. */
#bar { touch-action: none; }
