/* v2 inbox page group (2026-10-02): the header of every page, and the inbox: /inbox, /inbox/requests and a
   conversation (/inbox/@handle). Loaded on every page after ui.css and style.css, whose tokens it uses and which it
   does not change. Colour comes from the robots only; the one thing that moves is an online robot's blink. A row takes
   one step of ground when pointed at and two when it is the open one.
   The frame (list column 360px, two panes above 925px, the message column at most 696px) is Telegram Web K's
   (github.com/morethanwords/tweb); the list and the messages are Reddit's chat (the rules at the end). */

/* ---- header: Lobby, Inbox with the unread count, your robot's face ---- */
nav a.nav-inbox { display: inline-flex; align-items: center; gap: 6px; }
/* the unread count, on the inbox icon and beside the back arrow: blue, white figures (.ib-num, .count below; devil-blueonly) */
.count { box-sizing: border-box; min-width: 20px; height: 20px; padding: 0 var(--sp-1); border-radius: var(--radius-full); background: var(--c-text); color: var(--c-bg); font-size: var(--text-xs); font-weight: 600; line-height: 20px; text-align: center; font-variant-numeric: tabular-nums; }
/* your robot: its face alone. No ground at rest; pointed at (or on /me), a round one that reaches past the column's
   edge, so at rest the face itself lines up with the edge */
nav a.me { justify-content: center; width: 32px; height: 32px; margin-right: -4px; padding: 0; border-radius: 50%; background: none; }
nav a.me:hover, nav a.me[aria-current] { background: var(--bg2); }
/* phones: the brand leads home, so Lobby goes; Inbox and its count stay */
@media (max-width: 439px) { nav a.nav-inbox { display: inline-flex; } }
@media (max-width: 359px) { nav a.me { padding: 0; } }

/* ---- the app: under the header it fills the window from edge to edge (WhatsApp, Telegram, Messenger web): the list
   on the left, the open conversation in all the rest ---- */
main.ib { --ib-side-w: 384px; display: flex; height: calc(100vh - 52px); height: calc(var(--chat-viewport, 100dvh) - 52px); min-height: 0; }
/* the list pane, at the window's left edge. Its words and the faces of its rows start where the brand above starts:
   24px in (16px on a phone, the header's own gutter). Rows have no ground at rest; the ground of a row pointed at
   reaches 9px further out, so the list itself starts 9px short of that line. 360px of list, as Telegram's. */
.ib-side { flex: none; display: flex; flex-direction: column; width: var(--ib-side-w); min-height: 0; border-right: 1px solid var(--c-border); }
.ib-head { flex: none; display: flex; align-items: center; gap: var(--sp-1); min-height: 64px; padding: 0 var(--sp-4) 0 var(--sp-6); }
.ib-head h1 { margin: 0; font-size: var(--t-l); line-height: 1.19048; }
.ib-back { flex: none; display: grid; place-items: center; width: 36px; height: 36px; margin-left: -8px; border-radius: 50%; color: var(--fg); }
.ib-back:hover { background: var(--bg2); }
.ib-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding-bottom: var(--sp-4); }
.ib-note { margin: 0 16px 12px 24px; color: var(--muted); font-size: var(--t-m); line-height: 1.42858; text-wrap: pretty; }
.ib-list { list-style: none; margin: 0; padding: 0 var(--sp-2) 0 var(--sp-4); }
/* nothing yet: your robot, what arrives here, and the one thing to do first */
.ib-empty { padding: 8px 16px 0 24px; }
.ib-empty > .ava { margin-bottom: 20px; }
.ib-empty h2 { margin: 0; font-size: var(--t-l); line-height: 1.19048; }
.ib-empty p { margin: 8px 0 0; color: var(--muted); font-size: var(--t-m); text-wrap: pretty; }
.ib-empty .cta { margin-top: 24px; }
/* the list with no conversation yet (10-09 night, views/inbox.js noneYet; X's empty inbox; taste: an empty list says so in
   the middle, 48 above, one sentence; under your agent's pinned line when you have one): the sentence at the body's size
   in grey, and 16 under it Copy your link, the site's button */
.ib-none { padding: 48px 16px 0; text-align: center; }
.ib-none p { margin: 0; color: var(--muted); font: var(--level-body); text-wrap: balance; } /* one short sentence: two even lines where it wraps */
.ib-none .btn { margin-top: var(--sp-4); }
/* the right pane before a conversation is open */
.ib-pane { flex: 1 1 auto; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
.ib-blank { align-items: center; justify-content: center; }
.ib-blank p { margin: 0; color: var(--c-text-2); font-size: var(--text-sm); }

/* ---- a conversation: who it is at the top of the messages, which scroll in between and open at the newest (a
   reversed column starts at its end, so no script is needed), the chatbox floating at the foot ---- */
.th-scroll { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column-reverse; overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable both-edges; }
/* (devil-sendscroll, 10-14) the room under the message the owner just sent to their own agent (public/chat-scroll.js, anchor
   mode; ChatGPT's min-height under the last turn): as tall as lets that message stand at the top with the window at its foot;
   the reply grows into it. Only while there is some (data-room): no other window has the extra item. */
.th-scroll[data-room] > .th-body::after { content: ""; flex: none; height: var(--th-room, 0px); }
/* (scrollbar-unify, 10-10) a list or a conversation scrolls up and down only: what hangs past its side (the time and
   actions beside a wide bubble, ::after curls) is cut there, never a sideways scroll with momentum; a code block or a
   table in it keeps its own sideways scroll */
:is(.th-scroll, .ib-scroll, .ib-side, .toc-scroll, .nav-menu, .search-suggest) { overflow-x: hidden; overscroll-behavior-x: none; }
/* the messages: the scroll area spans the pane, the column of messages does not. On a wide window it is centred and
   at most 680px wide, Telegram Web K's (helpers/updateColumnWidths.ts: CHAT_WIDTH_MAX 696px, less .5rem either side),
   with 16px either side. A short conversation sits on the box, as X's, iMessage's and Telegram's do (10-12, Bonan: "下面这么大"):
   the column is reversed, its foot is where the body rests.
   A conversation that opens with the card of who it is reads from the top, as Instagram's messages do (devil-paging,
   10-12; Bonan: "怎么默认是从这么下面，我以为头像距离顶的距离是一定的", then "头像高度对了，但是下面的对话自然跟着头像啊"): the body is
   at least as tall as the window (it grows into the reversed column), the card at its top, the first message right under
   it, the messages down from there, and what room is left at the foot, over the box. Longer than the window there is no
   room: it opens at its newest and the card is where the conversation begins. Without a card (the right column's line)
   everything rests on the foot, on the box. */
.th-body { width: 100%; max-width: 712px; margin: 0 auto; padding: var(--sp-2) var(--sp-4) var(--sp-5); display: flex; flex-direction: column; justify-content: flex-end; }
.th-body:has(> [data-thread-who]) { justify-content: flex-start; }
/* the card is where the conversation begins (devil-paging): while pages before these are still to come ("Earlier
   messages"), it is not over them; the page that reaches the start brings it ([data-older], public/app.js loadOlder), once */
.th-body:has(> .th-more) > [data-thread-who] { display: none; }
/* (scrollbar-unify, 10-10) nothing in a conversation makes it wider than its box: what hangs past the body's sides (the
   time and actions beside a bubble as wide as the right column lets it be) is cut at its edge, not a sideways scroll */
.th-body { overflow-x: clip; }
.th-more { margin: var(--sp-4) 0 0; font-size: var(--text-sm); text-align: center; }
/* the older pages loaded in place (10-09 inbox-jank, public/app.js loadOlder): the link back is theirs, above them, no longer
   the newest page's */
.th-body:has(> [data-older]) > .th-more { display: none; }
.th-list { list-style: none; display: flex; flex-direction: column; margin: 0; padding: 0; }
/* A conversation's loading copy (public/app.js skeleton; 10-09, Bonan: "这个 skeleton 颜色不太对"; taste: the real component,
   re-skinned): real bubbles holding placeholder words of real lengths, their runs and tails real, every bubble the one
   skeleton grey (--bg3) on both sides, never the blue; the words see-through, no bars; the whole list breathing together
   (home.css home-breathe, Tailwind's animate-pulse). */
[data-thread-skeleton] .th-post-text, [data-thread-skeleton] .th-post.mine .th-post-text { --bubble: var(--bg3); color: transparent; user-select: none; }
@media (prefers-reduced-motion: no-preference) { [data-thread-skeleton] .th-list { animation: home-breathe 2s cubic-bezier(.4, 0, .6, 1) infinite; } }
/* the foot (10-02 night, Bonan: "当然人可以直接发消息"): the box to type in and Send, as Reddit's, in the messages'
   column; over it, when there is something to know, one grey line. While you cannot write (your request waits for its
   answer, or you blocked them) the line stands alone: data-can-send="false" (views/inbox.js) hides the box, which is
   never taken out of the page, so a refresh of the line never loses what is being typed. Field and button are ui.css's
   Textarea and Button; only their size in this row is set here. */
.th-dock { flex: none; border-top: 1px solid var(--c-border); padding-bottom: env(safe-area-inset-bottom); }
.th-dock-note { max-width: 712px; margin: 0 auto; padding: var(--sp-3) var(--sp-4) 0; color: var(--c-text-2); font-size: var(--text-sm); line-height: 1.42858; text-align: center; text-wrap: pretty; }
.th-dock-note:empty { display: none; }
.th-dock-note.needs { color: var(--fg); }
.th-dock-note[data-can-send="false"] { padding: 18px 16px; }
.th-dock-note[data-can-send="false"] ~ .th-compose, .th-dock-note[data-can-send="false"] ~ .th-compose-error { display: none; }
/* why the last message did not go (public/app.js, or the page itself without script) */
.th-compose-error { max-width: 712px; margin: 0 auto; padding: var(--sp-3) var(--sp-4) 0; color: var(--c-text); font-size: var(--text-sm); line-height: 1.42858; text-wrap: pretty; }
.th-compose-error[hidden] { display: none; }
.th-compose { display: flex; align-items: flex-end; gap: var(--sp-2); max-width: 712px; margin: 0 auto; padding: var(--sp-3) var(--sp-4); }
/* one line high at first, as tall as the 36px button, growing with what is written (ui.css: field-sizing) up to a third
   of the window, then it scrolls */
.th-compose .textarea { flex: 1 1 auto; min-height: 36px; max-height: 33vh; padding: 7px 12px; resize: none; overflow-y: auto; }
/* a phone's 16px text makes the field 40px high; the button keeps level with it */
@media (max-width: 767px) { .th-compose .btn { height: 40px; } }
.th-jump { position: relative; flex: none; height: 0; }

/* ---- one pane (Telegram docks the two columns only above 925px) ---- */
@media (max-width: 925px) {
  main.ib { display: block; height: auto; }
  .ib-side { width: auto; border-right: 0; }
  .ib-scroll { overflow: visible; }
  .ib[data-view="list"] .ib-pane, .ib[data-view="requests"] .ib-pane, .ib[data-view="thread"] .ib-side { display: none; }
  /* the list is the root: big title, like the messenger apps on a phone */
  /* the phone's title row is every page's (rail.css .panel-band: 56 high, 4 under the header; 44-detail-standards §2) */
  .ib[data-view="list"] .ib-head { min-height: 56px; margin-top: var(--sp-1); padding-top: 0; padding-bottom: 0; }
  .ib[data-view="list"] .ib-head h1 { font-size: var(--t-title); line-height: 1.1; letter-spacing: -.02em; }
  /* requests and a conversation are pushed over it: their own bar with a back button, no site header */
  body:has(> main.ib[data-view="thread"]) > header.top, body:has(> main.ib[data-view="requests"]) > header.top { display: none; }
  .ib[data-view="thread"] { display: flex; height: 100vh; height: var(--chat-viewport, 100dvh); }
}
@media (max-width: 640px) {
  .ib-note { margin-left: 16px; }
  .ib-list { padding-left: var(--sp-2); }
  .ib-empty { padding-left: 16px; }
}

/* ==== 10-02 evening: after Reddit's chat (chat.reddit.com), Bonan: "就是抄袭 reddit" ====
   The list: entry rows with an icon at the top (Requests ›), then one row per conversation: a round face, the name
   with its small tags beside it, the time on the right, one line of the last message under it.
   A conversation: who it is, centred, where it starts; day lines with the date in the middle; every message on the
   left, a face beside a line of name and time, the text under it (no bubbles). Reddit's green MOD tag is grey here:
   colour comes from the robots only. Reddit publishes no source, so sizes are read off its page; type sizes and the
   badge are ui.css's. */

/* ---- 10-02 night (Bonan: "你的设计理念是什么？有些不知所云") ----
   Nothing yet: said once, in the middle of the right pane (Reddit's chat, WhatsApp Web), your robot over one title and
   one sentence; the list beside it stays empty under its title. On a phone, where the list is the screen, it shows
   under the title. */
.ib-pane.ib-empty { padding: 24px; text-align: center; }
.ib-pane.ib-empty > .ava { margin: 0 auto 20px; }
.ib-pane.ib-empty p { max-width: 380px; margin: 8px auto 0; }
/* (on a phone it showed under the list's title until 10-09 night: the list says it there itself now, .ib-none) */
/* the line between the two panes fades in from the top, under the header (same night: "这个分割线上面渐隐") */
@media (min-width: 926px) {
  .ib-side { position: relative; border-right-color: transparent; }
  .ib-side::after { content: ""; position: absolute; top: 0; right: -1px; bottom: 0; width: 1px; pointer-events: none; background: linear-gradient(to bottom, transparent, var(--c-border) 96px); }
}
/* the empty pane's line is two steps under the page's title, as shadcn's Empty (EmptyTitle: text-lg, medium), so it
   does not compete with "Inbox" (10-02 night, Bonan: "inbox和no message一样大没问题对吧？") */
.ib-pane.ib-empty h2 { font-size: var(--text-lg); line-height: var(--lh-lg); font-weight: 500; letter-spacing: var(--tracking-tight); }
.ib-pane.ib-empty > .ava { margin-bottom: 16px; }

/* ==== 10-02 night, second pass. Bonan: "太丑了……左边的list也很丑陋，我们抄袭qq"; "request和其他人的头像对齐就行了";
   "未读用红色"; the conversation: "直接抄袭我cube的chatbox里的conversation怎么渲染不就行了……不要背景，就是她本身";
   "介绍是不是废话有些多". ==== */

/* ---- the list, as QQ's: no lines between rows; a row is 12px in from the list's sides and 12px inside, rounded,
   one step of ground under the pointer and two when it is the open one (taste: an item list). A 48px face (the
   Requests row's round icon in its place, the same size and edge), level with the title and the brand 24px in; the
   name and, at the right end, the time; under them the last message in grey on one line, and at its right end the
   unread count, blue. A conversation that needs you starts that line with a blue dot and [Needs you] (devil-blueonly: red is only for errors and danger). */
.ib-list { list-style: none; display: grid; gap: 0; margin: 0; padding: 0 var(--sp-3); }
.ib-chat > a {
  position: relative; display: grid; grid-template-columns: 48px minmax(0, 1fr); grid-template-rows: auto auto; column-gap: var(--sp-3); row-gap: var(--sp-1);
  align-content: center; min-height: 72px; padding: var(--sp-3); color: var(--c-text); text-decoration: none;
}
.ib-chat .ava, .ib-chat .ib-entry-icon { grid-row: 1 / 3; align-self: center; }
/* the drawing's own left edge on the line, not its box's (views.js faceEdge) */
.ib-chat .ava { margin-left: calc(var(--edge, 0) * calc(-1 * var(--sp-12))); }
.ib-entry-icon { display: grid; place-items: center; width: 48px; height: 48px; border-radius: var(--radius-full); background: var(--bg3); color: var(--fg); }
.ib-entry-icon svg { width: 22px; height: 22px; }
.ib-chat-top, .ib-chat-sub { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
.ib-chat-name { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font: 500 var(--text-base)/var(--lh-base) var(--font); }
.ib-chat-at { flex: none; margin-left: auto; color: var(--c-text-2); font-size: var(--text-sm); line-height: var(--lh-sm); white-space: nowrap; }
.ib-chat-text { flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; color: var(--c-text-2); font-size: var(--text-sm); line-height: var(--lh-sm); }
.ib-flag { color: var(--c-accent); font-weight: 600; }
.ib-flag.is-ended { color: var(--muted); font-weight: 400; } /* (devil-nofinal) Ended: the white flag, grey, plain */
/* (devil-smallfix2 + devil-blueonly) Linear Inbox's way: a dot, the words, the row bold; blue means it needs you, red is only for errors and danger */
.ib-flag-dot { display: inline-block; flex: none; width: 6px; height: 6px; margin-right: .45em; border-radius: var(--radius-full); background: var(--unread); vertical-align: middle; }
.ib-num, .count { background: var(--c-accent); color: var(--c-on-accent); }
.count.count-muted { background: var(--muted); color: var(--bg); }
.ib-num { flex: none; min-width: 20px; height: 20px; padding: 0 var(--sp-1); border-radius: var(--radius-full); font: 600 var(--text-xs)/20px var(--font); text-align: center; font-variant-numeric: tabular-nums; }
@media (max-width: 640px) { .ib-list { padding: 0 var(--sp-1); } }
/* "Select a conversation" the size of "No messages yet" (same night: "Select conversation的字应该和No conversation的字一样大") */
.ib-blank p { color: var(--c-text); font-size: var(--text-lg); line-height: var(--lh-lg); font-weight: 500; letter-spacing: var(--tracking-tight); }

/* ==== the conversation, iMessage's (10-08, Bonan: "这些细节，你真的要参考人家成熟的聊天，比如 iMessage"). Every value and
   where it comes from: eol_research/chat-spacing-2026-10-08/NOTES.md. iOS Messages read in points, which are CSS px on a
   390 phone; Apple publishes none of them. Markup: views/inbox.js messageItems, public/app.js (a message being sent, a
   reply being written).
     a message      li.th-post.mine|theirs [data-author: who writes it, a run's writer] > .th-sep? .th-row
                    (> .th-pics? .th-post-text (the bubble) .th-side) .th-task? .th-post-foot? (who, .th-receipt?)
     bubble         theirs iMessage's grey, yours the site's blue (in a frame the owner's accent); 16/22 (iMessage 17/22)
                    in 7 x 12, so one line is 36 high and a pill (18 round); at most 70% of the column
     run            the same writer again: 2 apart; its name (where runs are named) and the tail on its last bubble only;
                    between runs 8
     time           a centred line over the first message and after a lull (views/inbox.js LULL); a message's own beside it
                    under the pointer
     receipt        on your last message's foot while it is the newest: Delivered, Read 3:41 PM ---- */
:root {
  --bubble-theirs: var(--c-bg-3);  /* iMessage's received grey, light and dark */
  --bubble-mine: var(--c-accent);                          /* the site's one blue (ui.css .btn-blue), where iMessage has its own */
  --on-bubble-mine: var(--c-on-accent);
  --run-gap: var(--sp-run);                                  /* inside a run: iMessage's */
}
.th-post { display: flex; flex-direction: column; margin-top: var(--sp-2); }
/* positioned only where something is placed against it (10-09 inbox-jank: a position is a self-painting layer, and the
   list's every scroll frame walked a drawn message's): the post for its status line, the row for its reactions' badge */
.th-post:has(> .th-post-foot.is-status) { position: relative; }
/* (10-09 inbox-jank) a conversation draws its newest 50 messages, the ones before as they are scrolled up to (pages.js
   showThread, public/app.js loadOlder): every frame the list beside it scrolls, the hover update's hit test, paint and
   layerize walk every message drawn, and a long conversation drawn whole made the list stutter. A message's hover
   actions are no longer position: relative (nothing in them is positioned): four self-painting layers a message less. */
.th-post.mine { align-items: flex-end; }
.th-post.theirs { align-items: flex-start; }
.th-who + .th-list > .th-post:first-child, .th-more + .th-list > .th-post:first-child { margin-top: var(--sp-6); }
/* the run (iMessage's): the same writer as the message before it, with nothing between them: no time line over it
   (data-sep, set where the message draws one) and nothing under the one before (a decision's line, the answers, a post
   card). Enumerated, as CSS cannot compare two attributes; one level of :has, the only kind it allows. A streaming reply
   and a message being sent carry the same data-author as the message they become. */
:is(
  .th-post[data-author="mine-agent"]:not(:has(> :is(.th-event, .th-approval, .th-task))) + .th-post[data-author="mine-agent"]:not([data-sep]):not([data-typing]),
  .th-post[data-author="mine-agent-cc-cc"]:not(:has(> :is(.th-event, .th-approval, .th-task))) + .th-post[data-author="mine-agent-cc-cc"]:not([data-sep]):not([data-typing]),
  .th-post[data-author="mine-human"]:not(:has(> :is(.th-event, .th-approval, .th-task))) + .th-post[data-author="mine-human"]:not([data-sep]):not([data-typing]),
  .th-post[data-author="mine-guest"]:not(:has(> :is(.th-event, .th-approval, .th-task))) + .th-post[data-author="mine-guest"]:not([data-sep]):not([data-typing]),
  .th-post[data-author="theirs-agent"]:not(:has(> :is(.th-event, .th-approval, .th-task))) + .th-post[data-author="theirs-agent"]:not([data-sep]):not([data-typing]),
  .th-post[data-author="theirs-agent-cc"]:not(:has(> :is(.th-event, .th-approval, .th-task))) + .th-post[data-author="theirs-agent-cc"]:not([data-sep]):not([data-typing]),
  .th-post[data-author="theirs-human"]:not(:has(> :is(.th-event, .th-approval, .th-task))) + .th-post[data-author="theirs-human"]:not([data-sep]):not([data-typing]),
  .th-post[data-author="theirs-guest"]:not(:has(> :is(.th-event, .th-approval, .th-task))) + .th-post[data-author="theirs-guest"]:not([data-sep]):not([data-typing]),
  .th-post[data-author="theirs-guest-agent"]:not(:has(> :is(.th-event, .th-approval, .th-task))) + .th-post[data-author="theirs-guest-agent"]:not([data-sep]):not([data-typing])
) { margin-top: var(--sp-run); }

/* the centred time: iMessage's, the day part bold; 16 over it, 8 under */
.th-sep { align-self: stretch; margin: var(--sp-4) 0 var(--sp-2); text-align: center; font: var(--level-meta); color: var(--c-text-3); }
.th-sep b { font-weight: 600; }
/* (devil-needsyou-badge) Slack's "New" line: a hairline across the panel with the word at its right end, in the unread blue */
.th-new { align-self: stretch; display: flex; align-items: center; gap: var(--sp-2); margin: var(--sp-3) 0 var(--sp-2); font: var(--level-meta); font-weight: 600; color: var(--unread); }
.th-new::before { content: ""; flex: 1; height: 1px; background: var(--unread); opacity: .5; order: 0; }
.th-new > span { order: 1; }
.th-list > .th-post:first-child > .th-new { margin-top: 0; }
.th-list > .th-post:first-child > .th-sep { margin-top: 0; }
/* (devil-eventorder) a time line that stands over the grey lines of an event, in a row of its own */
.th-sep-row { display: block; list-style: none; align-self: stretch; }
.th-list > .th-sep-row:first-child > .th-sep { margin-top: 0; }
/* A finished agent process, folded where it was (10-09 night, design/45 §五 2.2, design/46 §3). While two agents talk their
   words are ordinary bubbles; once the request ends and nobody spoke in it, they fold into this one row. (devil-agentfold,
   10-12; Bonan: "这种折叠起来的卡，应该写明是agent之间的交流，否则鬼知道这是什么，最简单的就是agents communicate了几轮。别的都不
   用。") One line, centred as the time is: both sides' faces and "Agents talked · 3 messages", on the plain grey (reminders
   are blue or grey only: the 10-09 glass gradient is gone), no ›. Pointed at, its ground one step darker, at once, as a
   receipt line's (style.css .note-receipt); nothing else moves. Pressed, the bubbles come back under it (public/app.js,
   data-proc-toggle), the row stays on top; pressed again, they fold. Its gap from what is over it is a message's (.th-post:
   --sp-2), devil-receipts' rule for anything in the conversation. */
.th-proc { display: flex; flex-direction: column; align-items: center; align-self: stretch; margin-top: var(--sp-2); }
.th-list > .th-proc:first-child > .th-sep { margin-top: 0; }
.th-proc-card { display: inline-flex; align-items: center; gap: 8px; max-width: 100%; min-height: 32px; box-sizing: border-box; margin: 0; padding: 4px 12px 4px 4px; border: 0; border-radius: 999px; background: var(--bg2); color: var(--muted); font: 400 var(--text-sm)/var(--lh-sm) var(--font); text-align: start; cursor: pointer; transition: none; }
.th-proc-card:hover { background: var(--bg3); }
.th-proc-card:focus-visible { outline: 1px solid var(--line); outline-offset: 2px; }
/* side by side, never stacked: each face's agent mark (its bottom right) stays whole */
.th-proc-faces { display: inline-flex; flex: none; gap: 4px; }
/* one line; on a very narrow column its end fades (taste: no "…") */
.th-proc-words { min-width: 0; overflow: hidden; white-space: nowrap; --fade: 24px; }
.th-proc-list { align-self: stretch; }
.th-proc-list[hidden] { display: none; }
/* the run's foot (10-09, Bonan: "guest，your agent 还是放下面吧"): one line under the run's last bubble, small, grey, in line
   with the bubble's words: who wrote the run, and on your newest its receipt after it ("Your agent · Read 6:50 PM").
   2 under its bubble, the next run 8 under it, so it is its own bubble's. A message the same run goes on after has none
   (the rule after the tails, below). */
.th-post-foot { margin: var(--sp-run) 0 0; padding: 0 var(--sp-3); font: var(--level-meta); color: var(--c-text-3); white-space: nowrap; }
.th-post-clone { color: inherit; text-decoration: none; }
.th-post-clone:hover { color: var(--fg); text-decoration: underline; text-underline-offset: 3px; }

/* the row: what the message shows (pictures, the bubble), and beside it what the pointer brings up */
.th-row { display: flex; flex-direction: column; gap: var(--sp-run); max-width: 70%; min-width: 0; }
.th-post.mine > .th-row { align-items: flex-end; }
.th-post.theirs > .th-row { align-items: flex-start; }
.th-post-text {
  position: relative; isolation: isolate; box-sizing: border-box; max-width: 100%; min-width: 36px; padding: var(--sp-bubble-y) var(--sp-3); border-radius: var(--radius-lg);
  font: 400 var(--text-base)/var(--lh-item) var(--font); overflow-wrap: anywhere;
  --bubble: var(--bubble-theirs); background: var(--bubble); color: var(--c-text);
}
.th-post.mine .th-post-text { --bubble: var(--bubble-mine); color: var(--c-on-accent); }
.th-post-text a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
/* the tail, on the last bubble of a run only (design-critique-1016, round 4; Bonan: "我觉得这个角也不好看……imessage有这个角么"):
   iOS 18's, traced from Apple's and 9to5Mac's screenshots: the bubble's side runs straight down, then flares out in one concave
   sweep to a tip at the bottom outer corner; under it the tail's lower edge rises back into the bubble's rounded corner, which
   shows as a shallow notch in the bottom edge. One piece of the bubble's own colour, cut by a mask (17 x 21, its left 11px over
   the bubble's corner), so it is one body with the bubble on any ground, light or dark; theirs is the same piece mirrored.
   (Until now: a disc of the bubble's colour cut by a disc of the page's, which drew a hook curling outward.) */
.th-post-text::before { content: ""; position: absolute; bottom: 0; z-index: -1; width: 17px; height: 21px; background: var(--bubble);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 17 21'%3E%3Cpath d='M0 0H11V8C11 14.5 12.6 19 17 21C12.2 21.3 8.6 19.6 5.6 15.6Z'/%3E%3C/svg%3E") no-repeat 0 0 / 100% 100%;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 17 21'%3E%3Cpath d='M0 0H11V8C11 14.5 12.6 19 17 21C12.2 21.3 8.6 19.6 5.6 15.6Z'/%3E%3C/svg%3E") no-repeat 0 0 / 100% 100%; }
.th-post.mine .th-post-text::before { right: -6px; }
.th-post.theirs .th-post-text::before { left: -6px; transform: scaleX(-1); }
/* ...on the last bubble of every run, whatever follows it (a time line, a decision, the answers, a receipt, nothing): only a
   bubble the same run goes on after has none (10-08, Bonan: "到底有没有那个发送信息左下角的小尾巴，怎么有时候有有时候没有？"); and a
   message whose ask is a bubble of its own under it keeps its tail for that one */
/* (lists-perf, 10-09 night; PROFILE.md 2) which bubble ends its run is said on it by the server and kept by public/app.js
   (views/inbox.js markRuns: data-run-mid on one the same run goes on after, data-last on the list's last): the :has() looking
   at the next message made every element of a conversation about four times dearer to style. The same bubbles as before. */
.th-post[data-run-mid] .th-post-text::before,
.th-post[data-run-mid] .th-post-text::after,
.th-post:has(> .th-approval > .th-ask) > .th-row .th-post-text::before,
.th-post:has(> .th-approval > .th-ask) > .th-row .th-post-text::after { content: none; }
/* ...and the same for the run's foot: only its last message says who wrote it (and, the newest, its receipt) */
.th-post[data-run-mid] > .th-post-foot { display: none; }

/* beside the bubble, over the column's free side, so nothing moves when it shows: the time and Report or Delete under
   the pointer or a key's focus (taste: the icons, 16 in 28, muted, brighter pointed at, Delete red); level with the
   bubble's first line, Slack's and Discord's hover time (10-09, Bonan: "是不是和上沿对齐啊"): its 28 centred on the line's
   middle (7 + 22 / 2 = 18 from the top), so a tall bubble does not carry it down */
/* the side, in the row's flow rather than positioned (10-09 inbox-jank: no layer a message): first in the row, of no size
   (its gap given back), its time and actions hanging out of it 4 down, 8 beside the row (its width is the percentage) */
.th-side { order: -1; align-self: flex-start; display: flex; align-items: flex-start; gap: var(--sp-2); width: 0; height: 0; min-height: 0; margin-bottom: calc(-1 * var(--sp-run)); white-space: nowrap; }
.th-side > .th-acts { margin-top: var(--sp-1); }
.th-post.theirs .th-side { margin-left: calc(100% + var(--sp-2)); }
.th-post.mine .th-side { margin-left: calc(-1 * var(--sp-2)); flex-direction: row-reverse; }
.th-acts { display: flex; align-items: center; gap: var(--sp-1); }
.th-post.mine .th-acts { flex-direction: row-reverse; }
/* hidden until the pointer is on the message or a key reaches them, by their ink rather than opacity: an opacity under 1
   is a layer a message, and every frame the list beside the conversation scrolled walked them all (10-09 inbox-jank,
   scripts/inbox-jank-chrome.mjs: 6-22 frames a fling over budget with it, 0-3 without). The same pixels at rest and on
   hover (time and icons are their ink: currentColor), still in the tab order, still selectable, the selection unseen. */
.th-post:not(:hover) .th-acts:not(:focus-within) :is(time, .th-post-act) { color: transparent; }
.th-post:not(:hover) .th-acts:not(:focus-within) time::selection { color: transparent; background: transparent; }
.th-acts time { font: var(--level-meta); color: var(--c-text-3); }
/* (devil-cardtime) a card that is a message's own says its time the way a message does: beside the column, under the pointer */
.th-note-post { position: relative; }
.th-note-post > .th-side { position: absolute; right: 20px; margin: var(--sp-4) 0 0; width: auto; height: auto; }
.th-note-post > .th-side > .th-acts { margin-top: 0; }
.th-note-post:not(:hover) .th-acts:not(:focus-within) time { color: transparent; }
.th-note-post:not(:hover) .th-acts:not(:focus-within) time::selection { color: transparent; background: transparent; }
.th-post-act { display: grid; place-items: center; flex: none; width: 28px; height: 28px; padding: 0; border: 0; border-radius: var(--radius); background: none; color: var(--muted); text-decoration: none; cursor: pointer; }
.th-post-act:hover { color: var(--fg); background: var(--bg2); }
/* a phone: no pointer to bring them up; a long press opens them as a menu (public/app.js), the time as its title */
@media (hover: none) { .th-acts { display: none; } }

/* the receipt, on your newest message only (no message after it): iMessage's Delivered / Read 3:41 PM, the status word in
   600, after the run's name on its foot. Your own words have no name: there the receipt is the foot alone, in the room under
   the last message (.th-body's foot), so it moves nothing when it comes or goes. */
.th-receipt b { font-weight: 600; }
/* (ship-1012v) under a run: the receipt after the name is words in the line, not style.css's inline-flex (which dropped the
   space before its " · ": "Your agent· Delivered") */
.th-post-foot > .th-receipt { display: inline; }
.th-post:not([data-last]) > .th-post-foot > .th-receipt, .th-post:not([data-last]) > .th-post-foot.is-status { display: none; }
.th-post-foot.is-status { position: absolute; top: calc(100% + var(--run-gap)); right: 0; margin: 0; }
/* the visitor's email card under that message (inbox.css .th-email-item) starts under the receipt */
.th-post[data-last]:has(> .th-post-foot.is-status) + .th-email-item { margin-top: calc(var(--run-gap) + var(--lh-meta) + var(--sp-2)); }
/* a message being sent (public/app.js drawMessage) has a line of its own under it for what went wrong: nothing while it
   goes; one that did not, red under it, wherever it is (setState) */
.th-post > .th-receipt { display: none; margin: var(--run-gap) 0 0; padding: 0 12px; font: var(--level-meta); color: var(--subtle); white-space: nowrap; }
.th-post.failed > .th-receipt { display: flex; gap: var(--sp-2); color: var(--danger); }
.th-post.pending .th-post-text { opacity: .6; }
/* deleted by its sender: iMessage's unsent line, centred */
.th-post.deleted { align-items: center; }
.th-deleted { margin: 0; font: var(--level-meta); color: var(--subtle); }

/* pictures: iMessage's photos, no bubble, 18 round, square here (the size is not known before it loads: nothing moves) */
/* pictures are their own bubble (10-09, iMessage): rounded 18, no padding around them; several form a small grid; the text, if any,
   is its own bubble under them */
.th-pics { display: grid; gap: 2px; width: 260px; max-width: 100%; overflow: hidden; border-radius: 18px; }
.th-post.mine .th-pics { margin-left: auto; }
.th-pic { display: block; min-width: 0; min-height: 0; overflow: hidden; cursor: zoom-in; background: var(--bubble-theirs); }
.th-pics-1 .th-pic { aspect-ratio: 4 / 3; }
.th-pics-2, .th-pics-3, .th-pics-4 { grid-template-columns: 1fr 1fr; }
.th-pics-2 .th-pic, .th-pics-4 .th-pic { aspect-ratio: 1; }
.th-pics-3 .th-pic:first-child { grid-row: 1 / 3; }
.th-pics-3 .th-pic:not(:first-child) { aspect-ratio: 1; }
.th-pic img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* an agent writing: a rounded pill of their grey with three dots, on its side, with no tail at all (10-09, Bonan: "那个正在思考的
   气泡没必要有尾巴"; the thought circles went, and on your side they had sat at the wrong corner). It is no bubble of a run
   (data-typing, set where it is drawn: views/inbox.js typingBubble, public/app.js drawDrafts while a reply has no words
   yet): the message before it keeps its tail and its foot, and the dots sit a run's gap under them. */
.th-typing-bubble { position: relative; display: inline-flex; gap: 4px; align-items: center; height: 36px; padding: 0 13px; border-radius: 18px; background: var(--bubble-theirs); }
.th-typing-bubble i { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); opacity: .35; animation: th-typing 1.2s ease-in-out infinite; }
.th-post[data-typing] > .th-post-foot { display: none; }

/* the post a message is about: the lobby's card under the text, as a link preview */
.th-task { display: flex; flex-direction: column; box-sizing: border-box; width: min(320px, 70%); margin-top: var(--run-gap); padding: 12px 16px; border-radius: 18px; background: var(--bubble-theirs); color: var(--fg); text-align: left; text-decoration: none; }
.th-task:hover { background: color-mix(in srgb, var(--bubble-theirs) 92%, var(--fg)); }
.th-task-title { font-size: var(--text-base); line-height: var(--lh-base); font-weight: 600; overflow-wrap: anywhere; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; }
/* its text: a post excerpt (ui.css .post-excerpt; 10-09): its own lines, five at most, the last fading out when there is more */
.th-task-text { flex: none; margin-top: 2px; color: var(--muted); font-size: var(--text-sm); line-height: var(--lh-sm); }
.th-task-by { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; min-width: 0; font-size: var(--text-sm); line-height: var(--lh-sm); }
.th-task-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.th-task-handle { color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* the opening's state, the post page's pill (.pill-tag) drawn on the card: a relative tint, so it never matches the card's hover */
.th-task-pill { align-self: flex-start; display: inline-flex; align-items: center; height: 24px; margin-top: 8px; padding: 0 10px; border-radius: var(--radius-full); background: light-dark(rgb(0 0 0 / .055), rgb(255 255 255 / .07)); color: var(--muted); font-size: var(--text-sm); line-height: var(--lh-sm); font-weight: 500; }

/* The dock is the last flex item, below the message scroller. Its own height reserves
   space even while the keyboard or an expanding draft changes the available viewport. */
.ib-pane.th { position: relative; }
.th-dock { position: relative; flex: none; z-index: 2; border-top: 0; background: none; pointer-events: none; }
.th-dock > * { pointer-events: auto; }
.th-compose .chatbox-field { field-sizing: fixed; max-height: 40dvh; }
/* the mic capsule (10-12, Bonan: "直接是个胶囊，点击可以有option，不要这个动画了"): the site's pill made a capsule beside the image
   button, 36px as it is, a still head, who replies, a chevron; its menu opens above it (the box sits at the foot), at once */
.th-compose .chatbox-mic-slot { display: contents; }
.th-compose .chatbox-mic { height: 36px; padding: 0 10px 0 6px; gap: 4px; }
.chatbox-mic-head { display: flex; flex: none; }
.chatbox-mic-head svg { width: 20px; height: 20px; }
.chatbox-mic-menu { width: max-content; min-width: 200px; }
/* (devil-mic2) no agent connected that could answer: Let my agent reply greyed, "Connect an agent first" under it; the row
   is the way to the Agent page (views/mic.js micPill) */
.chatbox-mic-menu .chatbox-mic-none { align-items: flex-start; color: var(--muted); }
.chatbox-mic-none .chatbox-mic-head { opacity: .5; }
.chatbox-mic-none-text { display: grid; min-width: 0; }
.chatbox-mic-none-text small { font: 400 var(--text-sm)/var(--lh-sm) var(--font); color: var(--muted); }
@supports (anchor-name: --a) {
  .chatbox-mic-menu { inset: auto; bottom: calc(anchor(top) + 6px); left: anchor(left); top: auto; }
}
.th-compose.chatbox { display: block; width: calc(100% - 32px); max-width: 680px; margin: 0 auto var(--sp-3); padding: 0; gap: 0; }
.th-dock-note, .th-compose-error { width: fit-content; max-width: calc(100% - 32px); margin: 0 auto var(--sp-2); padding: var(--sp-1) var(--sp-3); border-radius: var(--radius-full); background: var(--c-bg-2); }
.th-dock-note[data-can-send="false"] { padding: 4px 12px; margin-bottom: 16px; }
/* the last message ends 40px above the chatbox (10-02 night, Bonan: "message最下的padding可以大些") */
.th-body { flex: 1 0 auto; padding-bottom: var(--sp-10); }
/* the box floats over the messages (10-11, Bonan: "这个chatbox有bg……左上角本来应该透光"; ChatGPT, Claude): the dock has no ground of its
   own, so the messages run on 16px (the box's corner radius) under its top edge and show through outside its rounded corners; the
   body keeps 16px more at its foot, so the newest message still ends clear above the box */
.th-scroll { margin-bottom: calc(-1 * var(--sp-4)); }
.th-jump { position: relative; z-index: 3; height: 0; }

/* ---- who it is: no bar over the conversation (10-02 night, Bonan: "右侧上面这个header，不需要，搞成那种居中的半透明胶
   囊，等上面的信息被滚上去再出现……看看cube"). At the top of the messages, centred: their face, their name (both lead to
   their page); whether they are online is the dot on their face (views.js faceDot), never a word (10-06). Once it
   has scrolled away, Cube's TitleCapsule (components/cube/Doc/UsageBody.tsx) floats 12px from the
   top, in the middle: their name alone, 14px semibold, in a 36px capsule 16px in, the dark ground at 60% over the
   messages blurred, no edge, no shadow; it fades in and out over 200ms (opacity only) and lets clicks through. */
.th-who { display: flex; flex-direction: column; align-items: center; padding: var(--sp-8) 0 var(--sp-2); text-align: center; }
.th-who > form { margin-top: 8px; }
.th-who-face { display: flex; margin-bottom: var(--sp-3); }
a.th-name { max-width: 100%; color: var(--c-text); font: var(--level-item); text-decoration: none; overflow-wrap: anywhere; }
a.th-name:hover, .th-who-face:hover + .th-name { text-decoration: underline; }
.th-status { color: var(--muted); font-size: var(--text-sm); line-height: var(--lh-sm); }
.th-pill {
  position: absolute; top: 12px; left: 50%; z-index: 3; transform: translateX(-50%);
  display: flex; align-items: center; height: 36px; max-width: 60%; padding: 0 var(--sp-4); border-radius: var(--radius-full);
  background: var(--c-bg-2);
  color: var(--c-text); font: 600 var(--text-sm)/var(--lh-sm) var(--font); text-decoration: none; white-space: nowrap;
  pointer-events: none; opacity: 0; visibility: hidden; transition: opacity .2s, visibility .2s;
}
/* their small face before the name (same night: "胶囊前面加入小logo") */
.th-pill { gap: var(--sp-2); padding-left: var(--sp-2); }
.th-pill > .th-pill-name { overflow: hidden; text-overflow: ellipsis; }
.th-pill.is-shown { opacity: 1; visibility: visible; }
@media (prefers-reduced-motion: reduce) { .th-pill { transition: none; } }
/* back, with one pane (phones): a round button of the same glass at the top left, the unread count beside the arrow */
.th-back { display: none; }
@media (max-width: 925px) {
  .th-back {
    position: absolute; top: 12px; left: 12px; z-index: 3; display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-1);
    height: 36px; min-width: 36px; margin: 0; padding: 0 var(--sp-1); border-radius: var(--radius-full);
    background: var(--c-bg-2); color: var(--c-text);
  }
  /* a circle without a count; with one ("12") a pill of the same height, as iOS Messages' back (10-07, Bonan) */
  .th-back:has(.count) { padding-right: var(--sp-3); }
  .th-back:hover { background: var(--c-bg-3); }
  .th-who { padding-top: 56px; }
}
/* the Requests icon the size a robot is drawn at in its 48px box (the drawings keep about 4px clear of each edge), its
   left edge on the drawings' line (10-02 night, Bonan: "你看request和其他头像就是不对齐") */
.ib-chat .ib-entry-icon { width: 40px; height: 40px; justify-self: start; }
.ib-chat .ib-entry-icon svg { width: 20px; height: 20px; }
.ib-flag { margin-right: .3em; }
/* the list's faces line up on their centres, not their left edges: the drawings differ in width, so only the middles
   can agree (10-02 night, Bonan: "这个request和下面的logo，应该是center对齐，如果左对齐，你得保证他们宽高一样"). Every
   face sits in the middle of its 48px box, the Requests icon too. */
.ib-chat .ava { margin-left: 0; }
.ib-chat :is(.ava, .ib-entry-icon) { justify-self: center; }
/* a person's face, a circle, is drawn at 40px in that box (10-08, views/inbox.js row): the size the robots were drawn at
   and the Requests icon is, so its edge stays on the title's line (--panel-pad, below) */

/* ---- pictures (10-02 night, Bonan: "如果他想要发图片，也要能支持……抄袭cube的处理"): in a message, Cube's ChatPictures,
   96px squares, 12px corners, a 1px zinc-700 edge, 6px apart in a row on the message's side, over its text; pressed,
   one opens large (ui.css .lightbox) */

/* ---- to the latest: Cube's round button in its chat (CubeChatPopover.tsx: h-8 w-8 rounded-full, a 1px zinc-700 edge,
   zinc-900 ground, zinc-300 arrow-down 16px; pointed at, zinc-800 and white), centred 12px over the chatbox, there
   whenever the end of the conversation is more than 70px below (public/app.js). It replaces the "New messages" pill. */
.th-latest {
  position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); display: grid; place-items: center;
  width: 32px; height: 32px; padding: 0; border: 1px solid var(--c-border); border-radius: var(--radius-full);
  background: var(--c-bg-2); color: var(--c-text-hover); cursor: pointer;
}
.th-latest:hover { background: var(--c-bg-3); color: var(--c-text); }
.th-latest[hidden] { display: none; }
/* a visitor's page (the embed, their chat page; 10-10, the embed footage: the ↓ sat on the newest answer's words): the ↓
   has a band of its own between the messages and the box (12px over the box, 32 tall, 4 clear), so it never covers a
   message, wherever the visitor has scrolled; the conversation's foot keeps room for the receipt under the last message */
.guest-chat .th-jump { height: 48px; }
.guest-chat .th-body { padding-bottom: 40px; }
/* ...and every conversation the same (scrollbar-unify, 10-10: in the right column's line the ↓ sat on an answer's words,
   "Thanks for asking ↓ ght now"): the owner's line beside the page, /inbox's conversations, on a desktop and a phone */
:is(main.ib, .rr-panel) .ib-pane.th .th-jump { height: 0; } /* 10-11, Bonan: "怎么都有一行黑色bg": only the pill floats, over the messages (ChatGPT's jump to bottom), no band behind it */
:is(main.ib, .rr-panel) .ib-pane.th .th-body { padding-bottom: var(--sp-16); } /* (devil-ignorecard, 10-13) the pill is there at the foot too while a card waits: 12 under + 32 tall + 16 where the column runs below the pill's band, so it never covers the last message */
/* "Needs you ↓" (10-08, Slack's Jump to unread, iMessage's arrow; public/app.js setJump): the same pill, its words first in the
   words white on the solid blue (devil-blueonly: only blue and grey), the arrow after them */
.th-latest.is-needs { width: auto; gap: var(--sp-1); display: inline-flex; align-items: center; padding: 0 var(--sp-3); border-radius: var(--radius-full); background: var(--c-accent); border-color: var(--c-accent); color: var(--c-on-accent); font: 500 var(--text-sm)/var(--lh-sm) var(--font); white-space: nowrap; }
.th-latest.is-needs:hover { background: var(--c-accent); color: var(--c-on-accent); }
.th-latest.is-calls, .th-latest.is-calls:hover { background: var(--c-accent); border-color: var(--c-accent); color: var(--c-on-accent); } /* the count of questions waiting: blue words and a blue dot (10-12, only blue vs grey) */
/* (devil-blueonly, Bonan: "比较低调的闪烁……直接是蓝色bg，白色字") the pill is the count badge's blue with white words; when it first shows or its number
   grows it brightens once and settles, 500ms (public/app.js setJump adds the class, removes it when played) */
@keyframes th-flash { 40% { filter: brightness(1.15); } }
.th-latest.is-flash { animation: th-flash 500ms ease-out 1; }
@media (prefers-reduced-motion: reduce) { .th-latest.is-flash { animation: none; } }
.th-latest-dot { display: none; } /* (devil-blueonly) no dot: the solid blue already says it, the dot repeated it (tried both, 10-12) */
/* (devil-ignorecard, 10-13) the pill's arrow points at the nearest waiting card: up when it is above the window (public/app.js sets data-dir) */
.th-latest[data-dir="up"] svg { transform: rotate(180deg); }
/* (devil-handbackhint, 10-12) the receipt of Let my agent reply when nothing waits for an answer ("Your agent will reply when
   Ann (visitor) writes next"): in the pill's place, the pill and the ↓ hidden while it shows (public/app.js showReceipt), the
   ↓'s grey (it asks nothing of the owner: only blue does). Its colours, how long it holds and how long it fades are these
   variables and only these (grey and 3s are -3a's reading of Bonan's words, not confirmed yet); only its opacity moves.
   8px from the edges: one line on a 390px phone for a name like "Ann (visitor)" (366px wide); a longer one wraps. */
.th-jump { --receipt-bg: var(--bg2); --receipt-line: var(--line); --receipt-fg: var(--fg-hover); --receipt-hold: 3000ms; --receipt-fade: 200ms; }
.th-micnote {
  position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); width: max-content; max-width: calc(100% - 16px);
  box-sizing: border-box; margin: 0; min-height: 32px; padding: 5px 12px; border: 1px solid var(--receipt-line); border-radius: 16px;
  background: var(--receipt-bg); color: var(--receipt-fg); font: 500 var(--text-sm)/var(--lh-sm) var(--font); text-align: center;
  pointer-events: none; opacity: 1; transition: opacity var(--receipt-fade) ease-out;
}
.th-micnote.is-gone { opacity: 0; }
@media (prefers-reduced-motion: reduce) { .th-micnote { transition: none; } }
/* The calm peeker matches the 404 illustration: 128px with a 24px gap. */
.ib-blank { flex-direction: column; gap: var(--sp-6); }
.ib-blank-art { position: relative; display: block; width: 128px; height: 128px; flex: none; }
/* the site's thought bubble (style.css .think): it pops once as the pane appears, then holds; still for whoever asks for less motion */
.ib-blank-art .think { opacity: 1; transform: scale(1); }
.ib-blank .nf-copy { margin: 0; color: var(--c-text); font: 400 var(--text-xl)/var(--lh-xl) var(--font); letter-spacing: normal; text-align: center; }
/* ---- a line to know over the chatbox, as Cube's minimized strip (CubeChatPopover.tsx; 10-03, Bonan: "上面这个提示，就抄袭
   cube 里这个 minimized 状态的 toolbar 就行了"): a tab tucked behind the box, 16px inside its edges, 28px tall, rounded on
   top with no bottom edge, the box's ground; in it one short notice in grey and an × at the right end (same day: "不用写
   对方agent的信息，直接说那个notification就行……右边是X"). Alone (you cannot write, so the box is hidden) it is a whole
   tab, closed at the foot, and has no ×. */
.th-dock-note {
  display: flex; align-items: center; gap: var(--sp-2); box-sizing: border-box;
  width: calc(min(100% - 32px, 680px) - 32px); max-width: none; height: 28px; margin: 0 auto; padding: 0 var(--sp-3);
  border: 1px solid var(--c-bg-3); border-bottom: 0; border-radius: var(--radius-box) var(--radius-box) 0 0; background: var(--c-bg-2);
  color: var(--c-text-2); font: 400 var(--text-sm)/var(--lh-sm) var(--font); text-align: left; white-space: nowrap;
}
.th-dock-note:empty { display: none; }
.th-dock-note[hidden] { display: none; }
.th-strip-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* the × at the right end, as the strip's chevron sits in Cube: 14px, grey, white when pointed at */
.th-strip-close { display: grid; place-items: center; flex: none; width: 20px; height: 20px; margin-right: calc(-1 * var(--sp-1)); padding: 0; border: 0; border-radius: var(--radius-full); background: none; color: var(--c-text-2); cursor: pointer; position: relative; }
.th-strip-close::after { content: ""; position: absolute; inset: -12px; border-radius: var(--radius-full); } /* 44 to press (44 §8) */
.th-strip-close:hover { color: var(--c-text); }
.th-strip-close svg { width: 14px; height: 14px; }
.th-dock-note[data-can-send="false"] { padding: 0 12px; margin-bottom: 16px; border-bottom: 1px solid var(--bg3); border-radius: 12px; }
/* ---- Conversation starters (10-08, src/views/inbox.js threadPane): a visitor's empty chat, in the middle under who it is
   (ChatGPT's and Claude's empty state; Bonan the same day), the box alone at the foot. The site's pill made a capsule, as
   the box's own Hand to your agent (ui.css .pill.chatbox-tool): 36px, the box's ground and edge, one step lighter pointed
   at, there at once. A centred row that wraps; on a phone and in the embed's panel one under another, centred. An
   extreme one (80 characters) fades at its end and slides when pointed at. ---- */
/* starters: one centred column under the header at every width (10-08, Bonan: desktop stacks too) */
.th-who > .th-suggest { display: flex; flex-direction: column; align-items: center; gap: 8px; align-self: stretch; max-width: 680px; margin: 24px auto 0; }
.th-suggest-chip { max-width: 100%; border-radius: var(--radius-full); }
.otp { display: flex; gap: 8px; }
.otp .otp-box { width: 48px; height: 56px; padding: 0; text-align: center; font: 500 24px/1 var(--font); }
.otp-error { margin: 8px 0 0; color: var(--danger); font: 400 var(--text-sm)/var(--lh-sm) var(--font); }
.otp-error[hidden] { display: none; }
/* Every state line over or in place of the box (blocked, waiting, closed, paused, not taking new messages …) is one compact, centred pill, as wide as its words (10-11, Bonan: "像这种类似banner，其实可以居中，没必要那么长吧？需要多少搞多少"). */
.th-dock-note.th-dock-blocked { width: fit-content; max-width: min(100% - 32px, 680px); height: auto; min-height: 28px; margin-bottom: 8px; padding: 4px 12px; border: 1px solid var(--bg3); border-radius: var(--radius-full); text-align: center; white-space: normal; }
.th-dock-note.th-dock-blocked[data-can-send="false"] { margin-bottom: 16px; }
.th-dock-blocked > svg { flex: none; width: 16px; height: 16px; }
.th-dock-blocked .th-strip-text { overflow: visible; text-overflow: clip; text-wrap: balance; }
/* the list's left margin as wide as its right (10-03, Bonan: "这个确定左边要顶到底？……让他和右padding一样就行"): on the
   right the scrollbar's reserved 12px and the list's 12px make 24; on the left the list keeps 24 too. The title (and the
   Requests back arrow) move with it, so the column's heading and the faces under it still start on one line.
   Desktop only (10-06, Bonan: "你没发现手机整体靠右么？"): a phone's scrollbar floats over the page and reserves
   nothing, so there this 12px-short right side shifted the whole list right; phones keep their own symmetric 16px. */
@media (min-width: 926px) {
  .ib-list { padding: 0 var(--sp-3) 0 var(--sp-6); }
}
/* ---- Report (views/inbox.js, 10-03 night): under each of their messages, Report in the row that shows the time on
   hover, the same grey, shown with it; by their name at the top, a ⋯ that opens the site's menu */
/* Report and Delete under a message are icon buttons (lucide flag, trash-2; ChatGPT and Claude do the same): 16px icon in a 28px
   hit area that does not make the 24px row taller, shown with the time, muted then brighter, Delete red on hover. */
/* their hover hint is the site's one tooltip (data-tip: public/app.js, ui.css .tip), the same as the presence dot's */
/* Local sends keep the bubble's fill; only the text fades while they wait. */
.th-who { position: relative; }
/* the conversation's toolbar (views/inbox.js tools; hotfix-4b, 10-07, Bonan: "不优雅的patch"), as Notion's peek and
   Linear's issue peek: one small group at the pane's top right, Full view then More, the same 36px icon buttons in one
   row 4px apart; no ground at rest, one step under the pointer at once (.icon-btn); their hints open below, inside the
   panel (data-tip-side="below"). 16px in from the pane's edge, clear of the round corner and the scrollbar, on the row of
   the back button and the name pill (top 12px). With one of the two, it alone stands in the same place. */
.th-tools { position: absolute; top: 12px; right: 16px; z-index: 3; display: flex; align-items: center; gap: 4px; }
.th-tools > .icon-btn { color: var(--muted); }
.th-tools > .icon-btn:hover, .th-tools > .icon-btn:has(+ .menu:popover-open) { color: var(--fg); }
/* phones, a conversation pushed over the list: the back button's 12px, mirrored */
@media (max-width: 760px) { .ib:not([data-view="full"]) > .th .th-tools { right: 12px; } }

/* Inbox panes start right below the solid header, so their scrollbar tracks begin at its bottom edge.
   The list heading travels with its rows. Phones keep their existing root list and headerless thread. */
body:has(> main.ib) > .top {
  animation: none;
  border-bottom-color: var(--c-tint-3); background: var(--c-bg);
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
@media (min-width: 926px) {
  .ib-side { display: block; overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable; }
  .ib-side > .ib-scroll { overflow: visible; scrollbar-gutter: auto; }
  /* Keep the divider fixed while the list scrolls, beginning at its original height. */
  .ib-side { position: static; }
  .ib-side::after { position: fixed; top: var(--top-h); right: auto; left: calc(var(--ib-side-w) - 1px); }
}

/* One viewport-sized chat shell; only messages and long drafts scroll. Safari can pan
   its visual viewport independently of layout: anchor the shell at that origin once. */
html:has(body > main.ib:is([data-view="guest"], [data-view="thread"], [data-view="full"])) { scrollbar-gutter: auto; overflow: hidden; }
body:has(> main.ib:is([data-view="guest"], [data-view="thread"], [data-view="full"])) {
  position: fixed; top: var(--chat-viewport-top, 0px); left: 0; right: 0;
  height: var(--chat-viewport, 100dvh); overflow: hidden;
}

/* The guest front desk is a conversation page, with the same scroll and compose dock. */
.ib:is([data-view="guest"], [data-view="full"]) { display: flex; margin-top: 0; height: calc(var(--chat-viewport, 100dvh) - var(--top-h)); }
:is(.ib-pane.th.guest-chat, .ib[data-view="full"] > .ib-pane.th) { display: flex; width: 100%; min-height: 0; overflow: hidden; }
:is(.guest-chat, .ib[data-view="full"] > .th) .th-scroll { padding-top: 0; }
:is(.guest-chat, .ib[data-view="full"] > .th) .th-back { display: inline-flex; align-items: center; justify-content: center; position: absolute; top: 12px; left: 16px; width: 36px; height: 36px; border-radius: var(--radius-full); }
:is(.guest-chat, .ib[data-view="full"] > .th) .th-pill { top: 12px; }

.th-compose .chatbox-send { position: relative; }
.th-compose .chatbox-send::after { content: ""; position: absolute; inset: -4px; border-radius: var(--radius-full); }
@media (max-width: 760px) {
  /* Touch scrolling keeps working without a reserved gutter or a visible scrollbar. */
  .th-scroll, .th-compose .chatbox-field { scrollbar-gutter: auto; scrollbar-width: none; }
  .th-scroll::-webkit-scrollbar, .th-compose .chatbox-field::-webkit-scrollbar { display: none; width: 0; height: 0; }
}

/* A reply in progress lives at the end of the thread, on the replying side. */
.th-typing-bubble i:nth-child(2) { animation-delay: .22s; }
.th-typing-bubble i:nth-child(3) { animation-delay: .44s; }
@keyframes th-typing { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }
/* (devil-listtyping, Bonan: "这个typing会闪烁么？呼吸闪烁") the row's "typing…" breathes as the conversation's dots do: the same keyframes, still for reduced motion */
@media (prefers-reduced-motion: no-preference) { .ib-chat-text[data-typing] { animation: th-typing 1.2s ease-in-out infinite; } }
/* (the loading copies breathe: the list's rows and a conversation's bubbles, 10-09) */
/* (lists-perf, 10-09 night; PROFILE.md 3) on a page with the inbox: body[data-ib], written by the server (views.js page()), no
   longer body:has(main.ib), with which anything put anywhere under body (a row, a message, a menu) restyled every element of
   the page (the rule takes in all of them). The same elements still, the same stillness. */
body[data-ib] *:not(.th-typing-bubble i):not(.ib-chat-text[data-typing]):not(.ib-chat.is-skeleton):not([data-thread-skeleton] .th-list):not(.ovl-thumb):not(:is(.ib-chat-name, .th-pill > .th-pill-name, .th-strip-text, .th-task-name, .th-task-handle, .th-event, .th-context-title, .ctx-menu .menu-label, .menu-head b, .menu-head span span, .hc-name)), body[data-ib] *::before, body[data-ib] *::after { animation: none !important; }
/* Leave your email (10-08, inbox.js threadPane): a small card under the visitor's last message, centred in the column, the
   post card's ground (.th-task); the line, then the site's input and main button in one row filling it (a phone: one
   over the other, full width). Saved: one line in the same card. */
.th-dock-card:empty { display: none; }
/* the sign-in on the line over the box is a text button, as a link in a sentence (devil-waitquote) */
.th-strip-link { all: unset; cursor: pointer; color: var(--fg); text-decoration: underline; text-underline-offset: 2px; }
.th-strip-link:hover { color: var(--accent, var(--fg)); }
.th-strip-link:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; border-radius: 2px; }
.th-email-embed { margin: 0; }
.th-email-embed .th-email-error { margin: 0 0 8px; text-align: center; }
.th-dock-card { margin: 0 0 var(--sp-2); }
.th-dock-card .th-email { width: 100%; }
.th-email-item { display: flex; justify-content: center; margin-top: 16px; }
.th-email { box-sizing: border-box; width: min(480px, 100%); margin: 0; padding: 16px; border-radius: var(--radius-box); background: var(--bg2); color: var(--fg); font: 400 var(--text-sm)/var(--lh-sm) var(--font); text-align: center; }
.th-email-text { margin: 0; text-wrap: pretty; }
.th-email [hidden] { display: none; }
.th-email-error { margin: 8px 0 0; color: var(--danger, #b3261e); }
.th-email-row { display: flex; gap: 8px; margin-top: 12px; }
.th-email-row .input { flex: 1 1 auto; }
.th-email-row .btn { flex: none; }
@media (max-width: 640px) {
  .th-email-row { flex-direction: column; }
  .th-email-row .btn { width: 100%; }
}
.th-email-done { color: var(--muted); overflow-wrap: anywhere; }
.th-email-saved { color: var(--muted); font-size: 12px; }
/* leave a message (10-09): under who it is on a visitor's chat, while the owner has no agent that can answer (the
   embed's own rule, embed.css, was the first) */
.th-away { margin: 4px 0 0; color: var(--muted); font-size: var(--text-sm); line-height: var(--lh-sm); text-wrap: pretty; }
@media (prefers-reduced-motion: reduce) { .th-typing-bubble i { animation: none; opacity: .6; } }

/* ==== the list's right-click / long-press menu (10-06; views/inbox.js ctxMenu, public/app.js ctxMenu) ====
   The site's menu (.menu, the account menu's grammar) placed at the pointer by the script (left/top, kept in the window), with
   the other party's name as its small title; Delete conversation in red like the site's other delete rows. The row it was
   asked on keeps one step of ground while it is open. On rows the phone's own callout and text selection are off, so a long
   press is ours. */
.menu.ctx-menu { inset: auto; width: 220px; }
.ctx-menu [hidden] { display: none !important; }
.ctx-menu .menu-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ctx-menu .ctx-delete { color: var(--c-danger); }
.ctx-menu .menu-row:focus-visible { outline: none; background: var(--c-bg-3); }
.ib-chat[data-ctx-handle] > a { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }

/* A pinned conversation's mark (10-06; views/inbox.js ICON_PIN): Lucide pin, muted, at the right end of the row's second
   line after the unread count, as WhatsApp's. 16px sits inside the count's 20px line, so the row keeps its height. */
.ib-pin { display: inline-flex; flex: none; color: var(--c-text-2); }
.ib-pin svg { display: block; }

/* ---- what a hosted agent is writing (10-06, public/app.js drafts): one item at the end of the list, on its side, as a
   message is drawn, its text plain and growing; until the first words (or while it uses a tool) the typing dots, with
   the tool's one line beside them ("Checking the calendar"). The dots stay the inbox's one animation. ---- */
.th-draft .th-post-text { white-space: pre-wrap; overflow-wrap: anywhere; }
.th-draft-tool { display: inline-flex; align-items: center; gap: 10px; color: var(--muted); font-size: var(--text-sm); line-height: var(--lh-sm); }
.th-draft-tool[hidden] { display: none; }
.th-draft-tool .th-typing-bubble { padding: 0 13px; }
.th-post.mine.th-draft .th-draft-tool { flex-direction: row-reverse; }
/* only while the tool line shows: a hidden one left this 8px on every draft, and the message it became was 8px shorter
   (10-07, "流式结束后会抖动一下") */
.th-draft .th-draft-tool:not([hidden]) + .th-row:has(> .th-post-text:not([hidden])) { margin-top: 8px; }

/* ---- what the owner's hosted agent asks to do (10-06, src/views/inbox.js approvalCard): under its reply, on its side, as
   Claude Code and Codex ask at the end of a turn: what it is, then Deny and Approve. Decided: one quiet line. ---- */
/* decided (10-08, Bonan on the live line: "这什么狗屁对齐策略"): iMessage's system notice, one short centred line in the
   meta level, cut with "…" at the column's width; never the question again (it is the message above) */
.th-event { align-self: stretch; min-width: 0; margin: var(--sp-2) 0 0; overflow: hidden; font: var(--level-meta); color: var(--subtle); text-align: center; white-space: nowrap; text-overflow: ellipsis; }
.th-event.is-trouble { color: var(--danger); }
.th-asks { gap: 0; }
/* a question with options (10-06 night): the options wrap, each a button of its own */

/* a clone's post in the owner's team conversation (10-06 night): the conversation it came from, after "Clone", a link */
.th-post-clone { color: var(--muted); text-decoration: none; }
.th-post-clone:hover { color: var(--fg); text-decoration: underline; text-underline-offset: 3px; }

/* ---- Powered by EarthOnline (10-06, src/views/inbox.js threadPane): one line centred under the box, Intercom's and
   Crisp's place, on every page the frame is on, and on a visitor's own chat page (/@handle/chat, release-3). The site's small size in the muted grey; the mark before the words is
   the single-colour one (currentColor, so the same grey), its drawing as tall as the words' capitals (Inter's cap
   height, .727em = 10.2px: the drawing fills 77.7 of its 96 units, so 12.5px). Under the pointer only the words and
   mark brighten, a label's hover. ---- */
.embed-credit-line { display: flex; justify-content: center; margin: -4px 16px 10px; }
.embed-credit { display: inline-flex; align-items: center; min-width: 0; color: var(--muted); font: 400 var(--text-sm)/var(--lh-sm) var(--font); text-decoration: none; white-space: nowrap; }
.embed-credit:hover { color: var(--fg); }
/* the mark and the name are one lockup (10-09, Stripe's "Powered by [stripe]"): the words around it a word-space away
   (Chinese has no spaces: 由 [mark]地球在线 提供), the mark 4px before the name, both centred on the line */
.embed-credit > span { display: inline-flex; align-items: center; gap: .25em; }
.embed-credit b { display: inline-flex; align-items: center; gap: 4px; font-weight: 500; }
.embed-credit svg.logo { flex: none; width: 12.5px; height: 12.5px; }
/* a visitor's own chat page has no header and no rail (release-3): the conversation fills the window */
body:not([data-rail]) > main.ib:is([data-view="guest"], [data-view="full"]) { height: var(--chat-viewport, 100dvh); }
/* the inbox on one pane (a phone, a narrow window): its list is a column of its own under the title, which scrolls by
   itself; the page never does (release-3, Bonan: whatever the number of conversations) */
@media (max-width: 925px) {
  main.ib:is([data-view="list"], [data-view="requests"]) { display: flex; flex-direction: column; }
  main.ib:is([data-view="list"], [data-view="requests"]) > .ib-side { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
  main.ib:is([data-view="list"], [data-view="requests"]) .ib-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
}
@media (max-width: 760px) {
  main.ib:is([data-view="list"], [data-view="requests"]) { height: calc(100vh - var(--top-h)); height: calc(var(--chat-viewport, 100dvh) - var(--top-h)); }
  html:has(body > main.ib:is([data-view="list"], [data-view="requests"])), body:has(> main.ib:is([data-view="list"], [data-view="requests"])) { overflow: hidden; overscroll-behavior: none; }
}

/* ---- one line for the list's title and its faces (10-07, Bonan: "title的左应该和下面文字的左对齐"; rail.css --panel-pad):
   --panel-pad is where the list's faces are drawn (a row is 12px inside, and a face, centred in its 48px box (10-02),
   keeps about 4px clear in it, as the Requests icon does exactly), and the title (or its back arrow's drawn edge) starts
   on it; the list starts 16px before it, so its rows' ground keeps 12px from a phone's edge, 24px from its column's line
   beside the rail. */
main.ib { --panel-pad: 28px; }
@media (max-width: 640px) { main.ib { --panel-pad: 20px; } }
@media (min-width: 926px) { main.ib { --panel-pad: 40px; } }
.ib-head { padding-left: var(--panel-pad); }
.ib-list { padding-left: calc(var(--panel-pad) - var(--sp-4)); }

/* The owner's line (10-07): a report is the homepage's card (landing.css .landing-card): one paragraph, how many the agent
   interviewed since its last one in grey, Ignore and Chat at the right, the primary last. Decided: one grey line. */
/* a visitor's chat opened from a post's Chat about this (10-07): the post's card under who it is, until they write */
.th-about-post { display: flex; justify-content: center; width: 100%; }
.th-about-post .th-task { margin-top: 16px; }
/* the post a visitor's chat is about (10-08, inbox.js contextBar): one slim row pinned under the header, outside the
   messages (Messenger's Marketplace item bar, Airbnb's listing, LinkedIn's job), the column's width: the card's ground,
   one step lighter pointed at; the author's face, the title on one line, the opening's pill at the right. Sticky at the top of the column
   that scrolls, so a long thread passes under it. */
.th-context {
  position: sticky; top: 0; z-index: 2; display: flex; align-items: center; gap: 8px; box-sizing: border-box;
  width: 100%; height: 44px; margin: 16px 0 0; padding: 0 12px; border-radius: var(--radius-box);
  background: var(--bg2); color: var(--fg); font: 500 var(--text-sm)/var(--lh-sm) var(--font); text-decoration: none;
  box-shadow: 0 0 0 6px var(--bg); /* the page's ground around it, so what scrolls under it never shows at its corners */
}
.th-context:hover { background: var(--bg3); }
.th-context-face { display: flex; flex: none; }
.th-context-face svg { width: 20px; height: 20px; }
.th-context-title { flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.th-context .th-context-state { flex: none; align-self: center; margin-top: 0; }

/* ---- the list's scrollbar, seen (hotfix-ui, 10-07, Bonan: the inbox's list showed no scrollbar on a desktop): its thumb is
   drawn at rest, a lighter grey than while it scrolls (ui.css .is-scrolling), so a long list says it scrolls before anyone
   tries. 10-09 (Bonan: "我们抄袭苹果，苹果可以让滚动条直接压在边界上过去，只要留够足够位置，不要让滚动条压在内容上就行"; then
   "滚动条的样式还是用我们本身的吧，留位置就行。这样就不用那个什么狗屁渐隐了"; "手机端也一样"): the list's own scroller
   (.ib-scroll) keeps the browser's scrolling but draws no bar of its own; the same thumb is drawn over the rows' right edge
   (public/app.js, overlay scrollbars), so the rows' tints and lines run to the column's line under it and nothing keeps a
   lane. The look is ui.css's bar exactly (a 12px bar, the thumb 2px clear of its edges: 8 wide, fully round), at rest and
   scrolling as here. The rows keep 20 clear on the right (rail.css --ib-row-r), so nothing sits under it. A scrolling
   .ib-side (a window wider than 925 without the rail) keeps the site's bar.
   10-09 night (Bonan: "这个scrollbar不是应该滚动才出现，并且hover又效果么？cursorpionter"; taste: "平时不显示、滚动时才出现"): no
   longer drawn at rest. It shows while the list scrolls, with the mouse on its 12px (.on-scrollbar, public/app.js) or
   dragged, 30% of the text colour, and fades .7s a second after (ui.css's bar); on the thumb itself, or dragged, it is
   50% and a step wider (8 to 10), at once, and the pointer a hand. On a phone the same, by the touch's scroll. ---- */
.ib-scroll[data-overlay-scroll] { scrollbar-width: none; scrollbar-gutter: auto; }
.ib-scroll[data-overlay-scroll]::-webkit-scrollbar { display: none; width: 0; height: 0; }
.ovl-bar { position: sticky; top: 0; z-index: 7; display: block; height: 0; pointer-events: none; }
.ovl-bar[hidden] { display: none; }
.ovl-thumb { position: absolute; top: 0; right: 2px; width: 8px; height: var(--ovl-len, 0px); border-radius: var(--radius-full); background-color: var(--c-scroll); opacity: 0; transition: opacity .7s ease; pointer-events: auto; touch-action: none; transform: translateY(2px); cursor: pointer; }
/* its place: an animation on the list's own scroll timeline, run by the compositor, nothing on the main thread while it
   scrolls (public/app.js sets its length and travel when the list changes size); a browser without scroll timelines gets
   the place from app.js */
@supports (animation-timeline: scroll()) {
  .ovl-thumb { animation: ovl-move linear both; animation-timeline: scroll(nearest block); }
}
@keyframes ovl-move { from { transform: translateY(2px); } to { transform: translateY(calc(var(--ovl-travel, 0px) + 2px)); } }
:is(.is-scrolling, .on-scrollbar, .is-dragging) > .ovl-bar > .ovl-thumb, .ovl-thumb:hover { opacity: 1; transition: opacity .12s ease; }
.ovl-thumb:hover, .is-dragging > .ovl-bar > .ovl-thumb { right: 1px; width: 10px; background-color: var(--c-scroll-hover); transition: none; }

/* (variant C's two folding sections, 10-07, went 10-09: one list; the pinned rows are tinted, below) */

/* the list's own title (hotfix-8): the head of the list's column, scrolling with its rows, and the pill it turns into once
   gone (10-08 polish: hotfix-8's, brought back). On every width since 10-09: a phone's list kept .ib-head over the rows,
   which never scrolled, so its search had no row to fold into (the floating row, below); now a phone's "Inbox N" scrolls
   away and floats as every other page's title does, 8 under the header (rail.css .panel-pill-at). Requests keeps its
   .ib-head, back arrow and all. */
main.ib:is([data-view="list"], [data-view="thread"]) .ib-head:not(:has(.ib-back)) { display: none; }
.ib-title { display: flex; align-items: center; gap: var(--sp-2); box-sizing: border-box; height: 56px; padding: 0 var(--panel-pad); }
.ib-title h1 { margin: 0; font: 700 var(--text-xl)/var(--lh-xl) var(--font); letter-spacing: normal; }
.ib-title-n { color: var(--c-text-2); font: 400 var(--text-xl)/var(--lh-xl) var(--font); font-variant-numeric: tabular-nums; }
@media (max-width: 760px) { .ib-title { margin-top: var(--sp-1); } }
.ib-scroll > .panel-pill-at { top: 8px; --pill-row-start: var(--panel-pad); --pill-row-end: var(--ib-row-r, 16px); }

/* ---- the inbox's search, scrolled (10-09, Bonan: "下去以后，这个 search 也缩成小圆胶囊和 inbox 一左一右一行", then "inbox
   胶囊也在中间吧"): once "Inbox" has gone (main[data-collapsed], the pill's own switch: one IntersectionObserver, no scroll
   listener, so the field and the circle swap in the frame the pill comes), the field is hidden where it stands (its room
   kept: nothing moves) and its circle shows at the right of the floating row, its right edge on the field's; the rows
   scroll on under the row alone. While a search is shown (its results in the list's place) the field stays, and the
   circle does not come: what was searched for stays in view. ---- */
main.ib[data-collapsed] .ib-scroll:not(:has(> .ib-search-results:not([hidden]))) > .ib-search { visibility: hidden; }
main.ib[data-collapsed] .ib-scroll:has(> .ib-search-results:not([hidden])) > .panel-pill-at > .ib-search-circle { visibility: hidden; }
/* a search shown: its field under the floating row on a phone too (8 + 36 + 8; beside the rail 56, rail.css) */
@media (max-width: 760px) { .ib-search { top: 52px; } }

/* (hotfix-8's header row over the column that scrolls, 56px of the page's ground, went 10-09: the controls float on glass
   over the messages, "the conversation's top" at the end) */

/* ---- the list, iMessage's conversation list (10-08, the iMessage pass; NOTES.md): the face, the name bold, the time at the
   right; under them the last words on two lines, grey, a size under the name (iMessage's 15 under 17: 14 under 16), the
   second line fading at its end (taste: no "…"); what is new a blue dot (iMessage's unread mark, the site's blue), the
   count said to a screen reader only. 10-09: the dot moved to the row's left gutter and the rows took Cube's lines (below). ---- */
.ib-chat-sub { align-items: flex-start; }
.ib-chat-text {
  white-space: normal; overflow-wrap: anywhere; max-height: calc(2 * var(--lh-meta)); overflow: hidden; color: var(--c-text-2);
  --fade: 40px; --fade-lh: var(--lh-meta);
}

/* a message's menu (10-08, the iMessage pass; public/app.js): the site's menu at the finger, its time as the small title;
   each row its 16px icon before its word, the hover bar's own (10-09, Discord's), Delete red, icon and word (currentColor) */
.menu.th-msg-menu { inset: auto; width: 220px; }
.th-msg-menu .menu-row > svg { flex: none; }
.th-msg-menu .is-danger { color: var(--danger); }
.th-msg-menu .menu-label b { font-weight: 600; }
/* on a phone a long press is the menu's, not the text's (iMessage's) */
@media (hover: none) { .th-post-text { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; } }
.th-receipt-act { color: var(--fg); text-decoration: underline; text-underline-offset: 2px; }
/* [Needs you] on the run's foot: the dot after it is the only space between them */
.th-post-foot .ib-flag { margin-right: 0; }

/* ---- asks in the bubbles' grammar (10-08, Bonan on the report card: "他的宽度为何和气泡不一样？"; iMessage's rich bubbles keep
   the bubble's own shape and width): a report is its agent's bubble, the paragraph its words, how many were interviewed
   a meta line at its foot; an action asked in a conversation is a bubble of its own; the answers a row of the site's small
   buttons right under the bubble, on its side, wrapping (iMessage's reply suggestions, Claude Code's approval actions:
   outside the message). Nothing is wider than the column at any width. ---- */
.th-approval { display: flex; flex-direction: column; gap: var(--run-gap); max-width: 70%; min-width: 0; margin-top: var(--run-gap); }
.th-post.mine > .th-approval { align-items: flex-end; }
.th-post.theirs > .th-approval { align-items: flex-start; }
.th-actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: var(--sp-1) 0 0; }
.th-post.mine .th-actions { justify-content: flex-end; }
.th-actions .btn { max-width: 100%; height: auto; min-height: 36px; white-space: normal; text-align: center; }
.th-ask { white-space: pre-wrap; }
.th-report p { margin: 0; }
/* what an agent changed (owner-rules, 10-10; views/inbox.js changeDiff): GitHub's small diff in the answer's bubble, a
   title in the meta grey, then each changed line on its own tint, − red, + green, the sign in its own column; a block
   between the answer's words, 8px over and under, its rows touching as GitHub's do */
.th-diff { display: flex; flex-direction: column; margin: var(--sp-2) 0; white-space: normal; }
.th-diff:first-child { margin-top: 0; }
.th-diff:last-child { margin-bottom: 0; }
.th-diff-title { margin-bottom: var(--sp-1); font: var(--level-meta); color: var(--c-text-2); }
/* (fix-1014f-memdiff, 10-14; Bonan: "undo其实是这个操作hover上去有的一个小工具") its Undo: a small word at the title's end
   (.th-opt), there under the pointer or a key's focus, hidden by its ink as a message's own tools are (.th-acts): nothing
   moves when it shows, and it stays in the tab order; a pointer reaches it only over the diff, which shows it first. A touch
   screen has no pointer to bring it up, so there it always is. */
.th-diff-head { display: flex; align-items: baseline; gap: var(--sp-2); }
.th-diff-head .th-diff-undo { margin: 0; }
@media (hover: hover) { .th-diff:not(:hover):not(:focus-within) .th-diff-undo .th-opt { color: transparent; } }
/* (fix-1016-pagesize, 10-16) a long change folded after its first lines (views/inbox.js changeDiff): the lines' box is no box of
   its own, and "Show N more lines" a small grey word under them, as a message's own tools; the rest comes when it is pressed */
.th-diff-rows { display: contents; }
.th-diff-more { align-self: flex-start; margin: var(--sp-1) 0 0; padding: var(--sp-1) var(--sp-2); border: 0; background: none; font: var(--level-meta); color: var(--c-text-2); cursor: pointer; }
.th-diff-more:hover { color: var(--c-text); text-decoration: underline; }
.th-diff-more[aria-busy="true"] { opacity: .6; cursor: progress; }
.th-diff-line { display: flex; gap: var(--sp-2); padding: var(--sp-1) var(--sp-2); white-space: pre-wrap; overflow-wrap: anywhere; font: 400 var(--text-sm)/var(--lh-sm) var(--font); }
.th-diff-line:first-of-type { border-radius: var(--radius) var(--radius) 0 0; }
.th-diff-line:last-of-type { border-radius: 0 0 var(--radius) var(--radius); }
.th-diff-line:first-of-type:last-of-type { border-radius: 6px; }
.th-diff-line.is-del { background: var(--c-danger-soft); }
.th-diff-line.is-add { background: var(--c-success-soft); }
.th-diff-sign { flex: none; width: 10px; text-align: center; user-select: none; }
.th-diff-line.is-del .th-diff-sign { color: var(--c-danger); }
.th-diff-line.is-add .th-diff-sign { color: var(--c-success); }
/* your agent's words in your line, formatted (line-markdown, 10-10; views/inbox.js, excerpt.js whole): the excerpt's
   paragraphs, lists and code, at the bubble's own size, 8px between blocks as a reply in ChatGPT and Claude */
.th-md { white-space: normal; }
.th-md > * { margin: 0; }
.th-md > * + * { margin-top: var(--sp-2); }
.th-md :is(ul, ol) { padding-left: 22px; }
.th-md li :is(ul, ol) { margin: 0; }
.th-md ul { list-style: disc; }
.th-md ul ul { list-style: circle; }
.th-md ol { list-style: decimal; }
.th-md li + li { margin-top: 2px; }
.th-md b { font-weight: 600; }
.th-md code { padding: 0 4px; border-radius: 4px; background: var(--chip); font-family: var(--mono); font-size: .875em; }
.th-md a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.th-diff-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.th-post-text.has-diff { min-width: min(100%, 280px); }
.th-report-text { white-space: pre-wrap; overflow-wrap: anywhere; }
/* (10-10) a judge's report at a glance (Gmail's and Linear's summary cards), in the bubble's own words and size: who and what
   they want in the bubble's weight-600, then a check before each thing that matches, then what is not known yet in grey */
.th-report-card .th-report-who { font-weight: 600; }
.th-report-matches { margin: var(--sp-1) 0 0; padding: 0; list-style: none; }
.th-report-matches li { display: flex; align-items: flex-start; gap: 6px; }
.th-report-matches svg { flex: none; margin-top: calc((var(--lh-item) - 16px) / 2); color: var(--muted); }
.th-report-card .th-report-unknown { margin-top: var(--sp-1); color: var(--muted); }
/* (10-10, handled-count) a report's or an escalation's "Meanwhile I handled 7 other visitors." (views/inbox.js handledWords):
   its foot, in the meta size and grey, where the count of people interviewed was */
.th-handled { margin: var(--sp-1) 0 0; white-space: normal; color: var(--muted); font: var(--level-meta); }
.th-handled:lang(zh) { word-break: keep-all; overflow-wrap: anywhere; }

/* the row's ⋯ (10-08, Gmail's hover actions; public/app.js opens the row's menu from it): under the pointer or a key's focus it
   stands where the time is (the time steps aside), the rail's small round icon button; a phone has the long press */
.ib-chat { position: relative; }
/* (lists-perf, 10-09 night; PROFILE.md 4) hidden and shown by its ink, and the time it stands over by its ink too, rather
   than opacity and visibility: an opacity under 1 is a layer a row, and the hover that flips both as rows pass under a
   resting pointer while the list scrolls laid the row out and repainted it every frame. The same pixels at rest and under
   the pointer (the ⋯ and the pin are their ink, currentColor). */
.ib-row-more { position: absolute; top: 10px; right: var(--ib-row-r, 8px); display: grid; place-items: center; width: 28px; height: 28px; padding: 0; border: 0; border-radius: var(--radius-full); background: none; color: transparent; cursor: pointer; }
:where(.ib-chat:hover, .ib-chat.is-ctx) .ib-row-more, .ib-row-more:focus-visible { color: var(--c-text-2); }
.ib-row-more:hover { color: var(--c-text); background: var(--c-bg-3); }
.ib-row-more svg { width: 16px; height: 16px; } /* a hover action's icon, as a message's (.th-post-act): 16 in 28 */
.ib-chat.is-ctx .ib-chat-when { visibility: hidden; }
@media (hover: hover) { .ib-chat:is(:hover, :focus-within) .ib-chat-when > :is(.ib-chat-at, .ib-pin) { color: transparent; } }
@media (hover: none) { .ib-row-more { display: none; } .ib-chat:is(:hover, :focus-within) .ib-chat-when { visibility: visible; } }

/* ---- a conversation not started yet (start-chat, 10-09, Bonan: "参考 linkedin 之类的怎么做输入框的"; views/inbox.js
   threadPane): LinkedIn's New message, X's and Bluesky's DM start. Who it is, centred as ever, with their @handle
   under the name (grey) and the first line of their about under that (the page's text, a size down, one line that fades
   at its end and slides when pointed at: taste, no "…"); then the conversation's one light line, centred, grey; the
   site's composer at the foot, with its Hand to your agent. A person's conversation keeps the handle and the line once
   it has started, so nothing moves when the first message goes. */
.th-handle { margin: 0; color: var(--muted); font: 400 var(--text-sm)/var(--lh-sm) var(--font); overflow-wrap: anywhere; }
/* the about line is the site's long-name box (ui.css .overflow-scroll): past its width it fades, pointed at it slides */
.th-about-line { max-width: min(100%, 480px); margin-top: 8px; color: var(--fg); font: 400 var(--text-sm)/var(--lh-sm) var(--font); }
.th-list > .th-start { margin: var(--sp-6) 0 0; color: var(--muted); font: 400 var(--text-sm)/var(--lh-sm) var(--font); text-align: center; }

/* pinned (10-09, Bonan: "这个 pin 别放在那里，换个好地方"): a 12px pin in the time's grey just before the time, 4 from it, level
   with it; the tint already says pinned, so it stays small and quiet, and the preview keeps its whole width */
.ib-chat-when { flex: none; display: inline-flex; align-items: center; gap: var(--sp-1); margin-left: auto; }
.ib-chat-when > .ib-pin { color: var(--c-text-3); }
.ib-chat-when > .ib-pin svg { width: 14px; height: 14px; }

/* ==== the list's rows (10-09, chat-tweaks) ====
   Lines and tints, Cube's decision queue (Bonan: "她这个 hover 出背景颜色的配置，能不能用分割线，然后参考 cube 里 decision queue
   的设计，接触 scrollbar 那里渐隐过去？"; Cube apps/web/src/app/globals.css .cube-fade-divider, .cube-hover-edge,
   .cube-selected-edge): a row is a straight band the column's width, no corners; between two rows a 1px line that fades out
   over its last 28px toward the scrollbar; under the pointer a straight tint, black 4% (white 4% on dark), the open one 6%
   (white 9%). 10-09 (Bonan: "我们抄袭苹果……滚动条直接压在边界上过去"; "这样就不用那个什么狗屁渐隐了"): the scrollbar is drawn over the
   rows (the list's scrollbar, above), so lines and tints are plain and run to the column's line, no fade into a lane. The
   line is the site's line between the rows of a list (--rail-line), the tints Cube's values. No line over the first row or
   under the last (taste: no line under a title). */
.ib-side {
  --ib-line: var(--c-divider); /* a phone's too: the app's one line (x-layout-2) */
  --row-1: var(--c-tint-1);  /* Cube's hover */
  --row-2: var(--c-tint-3);  /* Cube's selected; light 6% -> 8% (10-09: next to a pinned row's 4% the open one has to read a step deeper) */
  --row-3: var(--c-tint-4);  /* one step past it: a pinned row open */
}
.ib-chat { --row: transparent; }
.ib-chat + .ib-chat { border-top: 1px solid transparent; }
.ib-chat + .ib-chat::before { content: ""; position: absolute; top: -1px; left: 0; right: 0; height: 1px; pointer-events: none; background: var(--c-divider); }
/* (lists-perf, 10-09 night; PROFILE.md 4) the tints written on the row's link itself, not through a custom property on the row
   that every element in it inherits (each hover restyled the whole row). The same steps. */
.ib-chat > a { border-radius: 0; background: var(--row); }
.ib-chat:hover > a, .ib-chat.is-ctx > a { background: var(--c-tint-1); }
.ib-chat > a[aria-current] { background: var(--c-tint-3); }
/* pinned (10-09, Bonan: "my agent 默认一直在上面 pin，pin 的有常驻 bg 颜色不就行了"): your agent's line and the conversations
   you pinned rest on the hover's step, a tinted band between the lines; under the pointer one step deeper, open one more */
.ib-chat.is-pinned > a { background: var(--c-tint-1); }
.ib-chat.is-pinned:is(:hover, .is-ctx) > a { background: var(--row-2); }
.ib-chat.is-pinned > a[aria-current] { background: var(--c-tint-4); }
/* what is new: iMessage's blue dot in the row's left gutter (rail.css: 8 from the column's line, 6 clear of the face), level
   with the face's middle, which is the row's; the count is for a screen reader. Its name bold, the read ones' regular
   (Gmail's and Slack's unread). */
.ib-chat .ib-num { position: absolute; left: 8px; top: 50%; translate: 0 -50%; width: 10px; min-width: 0; height: 10px; margin: 0; padding: 0; border-radius: var(--radius-full); background: var(--c-accent); } /* the counts' one blue (devil-railface) */
.ib-chat:not(.is-unread) .ib-chat-name { font-weight: 400; }
.ib-chat.is-needs :is(.ib-chat-name, .ib-chat-text) { font-weight: 600; }
/* (10-10, Bonan) new, but the agent took care of it: the same dot in grey; in no count, the name regular */
.ib-chat.is-quiet .ib-num { background: var(--c-text-2); }
/* (devil-smallthings, 10-12 night) the dot is only what is unread: blue when it needs the owner, grey otherwise */
.ib-chat:not(.is-needs) .ib-num { background: var(--c-text-2); }

/* ==== the conversation's top (10-09, Bonan: "chat 右边这个上面的 header 不应该有不透明，也是有需要的东西就给他是 sticky 的有半透明
   bg 的就行"): no band over the messages (hotfix-8's 56px row went); they scroll up to the pane's top edge. What is needed
   floats over them on the site's one glass (ui.css --glass-*, no edge, no shadow): back (a phone, full view, a visitor's
   page) and Full view / ⋯ as 36px glass circles 12 from the top; once who it is has gone under them (--th-float: the
   controls' foot, 12 + 36; public/app.js), the pill with their face and name centred at the top, the app's pill
   (rail.css .panel-pill: 36 high, 16 semibold, there at once). The owner's conversation, your agent's line, a visitor's chat
   page, full view; a widget's frame keeps its own bar. ---- */
.ib:not(.embed) > .th .th-scroll { --th-float: 48px; margin-top: 0; }
.ib:not(.embed) > .th .th-who { padding-top: 76px; } /* where it stood under the band: 56 + 20 */
.ib:not(.embed) > .th :is(.th-back, .th-tools > .icon-btn, .th-pill) { background: var(--glass-bg); -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur); }
/* (devil-cardbugs, 10-12 night; Bonan: "这个header咋变成渐隐实心？之前不是镂空么") the top stays see-through, no strip of its own:
   only the pills are glass, and their glass is 94% of its ground (the site's pills are 88%: a card's words scrolling under
   showed through "Your agent" and the back arrow), blurred as the site's glass is; between them the line shows as it is */
.ib:not(.embed) > .th :is(.th-back, .th-tools > .icon-btn, .th-pill) { background: color-mix(in srgb, var(--c-bg-2) 94%, transparent); }
.ib:not(.embed) > .th :is(.th-back, .th-tools > .icon-btn:is(:hover, :has(+ .menu:popover-open))) { color: var(--c-text); }
.ib:not(.embed) > .th :is(.th-back, .th-tools > .icon-btn):is(:hover, :has(+ .menu:popover-open)) { background: color-mix(in srgb, var(--bg2) 92%, transparent); } /* (devil-cardbugs) a little under the pills' 94%, as 86% was under 88% */
.ib:not(.embed) > .th .th-pill { font: 600 var(--fs-body)/var(--lh-item) var(--font); letter-spacing: var(--tracking-tight); transition: none; }
/* a visitor's chat about a post: its bar sticks under the controls, not under the glass */
.ib:not(.embed) > .th .th-context { top: var(--th-float); }

/* ==== the inbox's search (10-09, Bonan: "chat 这里也要来一个 search"; X's Chat; views/inbox.js searchBox) ====
   The Search page's pill (rail.css .search-pill-box: 44 high, a 1px line, fully round, the magnifier) under "Inbox N", from
   the faces' line to the times' end; it stays at the list's top as the list scrolls (beside the rail under the floating
   Inbox pill: 12 + 36 + 8). Only the pill sticks, on the site's glass with its 1px line (10-09, Bonan: "有需要的东西就给他是
   sticky 的有半透明 bg 的就行": no band, opaque or glass); the rows scroll on round it and under it; over the page alone the
   glass is the page itself. Once "Inbox" has gone, its pill and this one float as two pieces of glass. × at its right while
   something is typed. The results take the list's place. */
.ib-search { position: sticky; top: 0; z-index: 5; margin: 0; padding: 0 var(--ib-row-r, var(--sp-4)) var(--sp-2) var(--panel-pad); pointer-events: none; }
@media (min-width: 761px) { body[data-rail] .ib-search { top: 56px; } }
.ib-search .search-pill-box { background: var(--glass-bg); -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur); pointer-events: auto; }
.ib-search-clear { display: none; place-items: center; flex: none; width: 24px; height: 24px; margin-right: calc(-1 * var(--sp-1)); padding: 0; border: 0; border-radius: var(--radius-full); background: none; color: var(--c-text-2); cursor: pointer; }
.ib-search-clear:hover { color: var(--c-text); }
.search-pill-box:has(> input:not(:placeholder-shown)) > .ib-search-clear { display: grid; }
.ib-scroll:has(> .ib-search-results:not([hidden])) > .ib-list { display: none; }
.ib-search-results[hidden] { display: none; }
.ib-search-more { height: 1px; }

/* the message a Delete asks about (views/inbox.js, the delete dialog; 10-09, Bonan: it sat indented 54px, the browser's own
   blockquote margin): on the dialog's text edge, under the title and the line, a muted quote block of the relative tint
   (the dialog's own ground is --bg2, so a --bg2 block would not show: taste, a small thing on a surface takes the tint), 8
   round, 12 inside */
.th-delete-excerpt { margin: 0; padding: var(--sp-3); border-radius: var(--radius); background: var(--c-tint-2); color: var(--c-text-2); font: var(--level-body); overflow-wrap: anywhere; white-space: pre-wrap; }
.th-delete-excerpt:empty { display: none; }

/* ==== what iMessage and Instagram do to a message (10-09, chat-parity; eol_research/chat-parity-2026-10-09/NOTES.md) ==== */
/* the quote over a reply: a small block of the relative tint on the reply's side, who in the text's colour and one line of
   what they said, fading at its end (taste: no "…"); pressed, the conversation goes to it */
.th-quote { display: block; box-sizing: border-box; max-width: 70%; margin: 0 0 var(--run-gap); padding: 6px 12px; border-radius: 14px; background: light-dark(rgb(0 0 0 / .055), rgb(255 255 255 / .07)); color: var(--muted); font: var(--level-meta); text-decoration: none; white-space: nowrap; overflow: hidden; -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent); mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent); }
.th-quote:hover { color: var(--fg); }
.th-quote-who { margin-right: 6px; color: var(--fg); font-weight: 600; }
.th-quote-gone { font-style: italic; }
/* the reactions (design-critique-1016, round 4; Bonan: "那就抄袭imessage吧"): iOS 18's Tapbacks, copied from Apple's screenshots
   (Newsroom WWDC24 "iOS 18 Messages Tapbacks"; the Messages guide for Mac, macOS 15). One circle a reaction, about as tall as a
   one-line bubble (34 on our 36), its emoji ~60% of it; someone else's on the bubble grey, yours on the blue; ringed in the
   page's ground; above the top outer corner of the piece it belongs to (.th-tap), its centre ~2px in from the bubble's side and
   ~12px over its top, only its foot (5px) on the rounded corner, so it covers no word; a two-dot tail going away from the
   bubble, on the front one. Several are a stack (Apple: "they appear as a stack"), the latest in front, the two before it
   behind, each a step outward and up. No number on the message. The run makes room over it (32px). */
.th-post.has-reacts > .th-row { position: relative; margin-top: var(--sp-8); }
.th-reacts { position: absolute; bottom: calc(100% - 5px); z-index: 1; width: 34px; height: 34px; }
.th-post.theirs .th-reacts { left: calc(100% - 19px); }
.th-post.mine .th-reacts { right: calc(100% - 19px); }
.th-react { position: absolute; inset: 0; display: grid; place-items: center; border-radius: var(--radius-full); background: var(--bubble-theirs); box-shadow: 0 0 0 2px var(--c-bg); font-size: 20px; line-height: 1; }
.th-react.is-mine { background: var(--c-accent); cursor: pointer; }
.th-react:nth-child(2) { z-index: -1; }
.th-react:nth-child(3) { z-index: -2; }
.th-react:nth-child(n+4) { display: none; }
.th-post.theirs .th-react:nth-child(2) { translate: 9px -2px; }
.th-post.theirs .th-react:nth-child(3) { translate: 18px -4px; }
.th-post.mine .th-react:nth-child(2) { translate: -9px -2px; }
.th-post.mine .th-react:nth-child(3) { translate: -18px -4px; }
.th-react:first-child::before, .th-react:first-child::after { content: ""; position: absolute; border-radius: var(--radius-full); background: inherit; }
.th-react:first-child::before { width: 10px; height: 10px; bottom: -3px; }
.th-react:first-child::after { width: 5px; height: 5px; bottom: -9px; }
.th-post.theirs .th-react:first-child::before { right: -2px; }
.th-post.theirs .th-react:first-child::after { right: -8px; }
.th-post.mine .th-react:first-child::before { left: -2px; }
.th-post.mine .th-react:first-child::after { left: -8px; }
/* edited: iMessage's word under the bubble, small and grey */
.th-edited { margin: var(--run-gap) 0 0; padding: 0 12px; font: var(--level-meta); color: var(--subtle); }
/* the first link's card: the post card's look, the page's picture over its title and site */
.th-link { display: flex; flex-direction: column; box-sizing: border-box; width: min(320px, 70%); margin-top: var(--run-gap); overflow: hidden; border-radius: 18px; background: var(--bubble-theirs); color: var(--fg); text-decoration: none; }
.th-link:hover { background: color-mix(in srgb, var(--bubble-theirs) 92%, var(--fg)); }
.th-link-img { display: block; width: 100%; aspect-ratio: 1.91 / 1; object-fit: cover; background: var(--bg3); }
.th-link-body { display: flex; flex-direction: column; gap: 2px; padding: 10px 14px 12px; }
.th-link-title { max-height: calc(2 * var(--lh-sm)); overflow: hidden; overflow-wrap: anywhere; font: 600 var(--text-sm)/var(--lh-sm) var(--font);
  /* two lines, the second fading at its end (taste: no "…"), as the inbox's preview (ui.css .fade-clip.fade-lines) */
  --fade: 40px; --fade-lh: var(--lh-sm); }
.th-link-site { color: var(--muted); font: var(--level-meta); }
/* a quote pressed: what it answers lit for a moment (a ring, at once, no fade: taste) */
.th-post-text.is-flash, .th-deleted.is-flash { box-shadow: 0 0 0 3px color-mix(in srgb, var(--bubble-mine) 45%, transparent); border-radius: 18px; }
/* replying or editing: the strip over the box (Cube's minimized toolbar, the dock note's tab), who and one line, × */
.th-reply-strip[hidden] { display: none; }
.th-reply-strip b { font-weight: 600; color: var(--c-text); }
/* the bar of reactions (iMessage's Tapback bar): a pill of the dialog's ground over the bubble, six and +; + opens them all */
.th-react-bar { position: fixed; inset: auto; margin: 0; display: flex; flex-wrap: wrap; gap: 2px; max-width: min(320px, calc(100vw - 16px)); padding: 4px; border: 1px solid var(--bg3); border-radius: 22px; background: var(--bg2); color: var(--fg); }
.th-react-bar:not(:popover-open) { display: none; }
.th-react-pick { display: grid; place-items: center; width: 36px; height: 36px; padding: 0; border: 0; border-radius: var(--radius-full); background: none; font-size: 20px; line-height: 1; cursor: pointer; }
.th-react-pick:hover, .th-react-pick.is-mine { background: var(--bg3); }
.th-react-more { color: var(--muted); font: 400 22px/1 var(--font); }


/* ---- the detail standards (10-09, design/44-detail-standards.md) ---------------------------------------------------- */
/* the composer stands on the messages' lines (44 §9 E8; ChatGPT: one column for both): the dock keeps the same scrollbar
   strips .th-scroll keeps (both edges: 12 each where scrollbars take room, none where they float over the page), so the
   messages' column and the composer are centred in the same width and share both edges */
.th-dock { overflow: hidden; scrollbar-gutter: stable both-edges; }
/* the back (36 round) is 44 to press (44 §8) */
.th-back::after { content: ""; position: absolute; inset: -4px; border-radius: inherit; }

/* ==== a phone's list, iMessage's (10-09, Bonan: "参考 imessage 里，你看头像部分是没有上下 border 的"; "search 在 list 的 header 里跟着滚走") ====
   The line between two rows starts at the name's x (the row's 20 + the 48 face + the 12 gap) and runs to the right edge, so
   nothing crosses the face's column; a pinned row's tint is the row's own and stays. The full search field is part of the
   scrolling content (not sticky): it scrolls away with the list, and the collapsed "Inbox N" pill and search circle take over. */
@media (max-width: 640px) {
  .ib-chat + .ib-chat::before { left: 80px; }
  .ib-search { position: relative; top: auto; }
}

/* Undo and the like (views/inbox.js answers): an option, a small text link under the bubble in the meta line's look, not a button */
.th-opt { padding: 0; border: 0; background: none; color: var(--c-text-2); font: var(--level-meta); cursor: pointer; }
.th-opt:hover { color: var(--c-text); text-decoration: underline; }

/* ---- a turn of the owner's line that did not answer (devil-slow, 10-12; views/inbox.js lineFailed): where its answer would
   be, one quiet line saying why and Try again, as Claude.ai's "Claude's response was interrupted · Retry" ---- */
.th-failed { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) 12px; margin: var(--sp-2) 0 0; }
.th-failed-text { margin: 0; color: var(--muted); font-size: var(--text-sm); line-height: var(--lh-sm); }
.th-failed form { margin: 0; }
/* (devil-oldbuttons, 10-12) a failed line's Try again: a word at its end, as a receipt's Undo (style.css .note-receipt
   .note-also), never a button; the line is the site's grey, its option the text's own colour */
.th-delivery .th-mark { display: inline-block; width: 1.25em; }
/* (devil-agentname, 10-13; Bonan: "Bonan" wrapped to the far left under "Couldn't deliver your answer to") the receipts' one rule
   (style.css .note-receipt .note-resolved): the mark's slot at the start of the first line, the words of every line from the
   same x, the slot's width in; what sits inside starts its own lines at 0 */
.th-delivery .th-failed-text { padding-inline-start: 1.25em; text-indent: -1.25em; }
.th-delivery .th-mark, .th-delivery .th-failed-opt { text-indent: 0; }
.th-failed-opt { display: inline-flex; align-items: baseline; gap: 6px; margin-left: 6px; white-space: nowrap; }
.th-failed-opt form { display: inline; }
.th-failed-opt .th-opt { color: var(--fg); font: inherit; text-decoration: underline; }
.th-failed-opt .th-opt:disabled { color: var(--muted); text-decoration: none; cursor: default; }
/* an answer's Connect card (views/inbox.js connectCard; design/48: the line is the card itself): the app's own logo, its name,
   Connect; done, one quiet line */
.note-connect { margin: var(--sp-2) 0 0; }
.note-connect form { display: block; } /* (devil-permcard2) the permission kind of card: views/perm-shell.js */
/* (devil-smallfix) the app's name is read whole (it wraps, never "G."); the button keeps its size and drops under it when the column is narrow */
.note-connect-name { flex: 1 1 8rem; min-width: 0; font-weight: 600; overflow-wrap: anywhere; }
.note-connect .btn { flex: none; white-space: nowrap; }
/* a message's files (devil-files, views/attachments.js): one card each under its pictures, the link card's ground; a file
   icon, its name on one line, its kind and size; pressed, it downloads. Row hover: one shade darker */
/* (10-12) as wide as its name, up to the bubble's widest (the row's), at least 240px: the name whole, as iMessage and Slack */
.th-files { display: flex; flex-direction: column; align-items: flex-start; gap: var(--run-gap); max-width: 100%; }
.th-post.mine .th-files { align-items: flex-end; }
.th-file { display: flex; align-items: center; gap: 8px; box-sizing: border-box; min-width: min(240px, 100%); max-width: 100%; padding: 10px 14px 10px 10px; border-radius: 18px; background: var(--bubble-theirs); color: var(--fg); text-decoration: none; }
.th-file:hover { background: color-mix(in srgb, var(--bubble-theirs) 92%, var(--fg)); }
.th-file-icon { display: grid; place-items: center; flex: none; width: 32px; height: 32px; border-radius: 9px; background: var(--bg); color: var(--muted); }
.th-file-icon.has-thumb, .th-file-icon.is-page { width: 40px; height: auto; align-self: stretch; min-height: var(--page-h, 40px); background: #fff; overflow: hidden; border-radius: 6px; box-shadow: 0 0 0 1px rgb(0 0 0 / .1); } /* (devil-preview) the PDF's first page; (devil-attachcls) a PDF's box is that size before it comes (views/attachments.js pageBoxHeight), so nothing moves */
.th-file-thumb { display: block; width: 100%; height: auto; max-height: var(--page-h, 56px); object-fit: cover; object-position: top; } /* (devil-attachcls) never taller than its box: a canvas rounded up a pixel does not grow the card */
.th-file-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.th-file-name { display: flex; min-width: 0; white-space: nowrap; font: 600 var(--text-sm)/var(--lh-sm) var(--font); }
/* cut in the middle when the card is too narrow: the start gives way with an ellipsis, the end and .pdf stay */
.th-file-head { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.th-file-tail { flex: none; }
.th-file-meta { color: var(--muted); font: var(--level-meta); }
/* (devil-guestimg) the note under attachments the other side's agent could not read */
.th-unseen { margin: 4px 0 0; color: var(--muted); font: var(--level-meta); }

/* (devil-fourthings) a report's card in the line (views/inbox.js reportCard): the permission shell inside the answers' form */
.th-actions.perm-form { display: block; margin: 0; }
.th-report-box { margin: 0; }
.th-report-box .th-report-who { margin-top: 0; }

/* ---- devil-m2-convui (10-13) ------------------------------------------------------------------------------------------ */
/* The mic's hand-over lines (src/owner-lines.js, views/inbox.js micLine): one grey line, centred, over the first message after
   the hand-over, as a time line is (.th-sep); its Lucide icon first (14px, the words' grey), then the words; the time follows
   the words and shows only under the pointer. */
.th-mic-line { display: block; text-align: center; align-self: stretch; margin: var(--sp-4) 0 var(--sp-2); font: var(--level-meta); color: var(--c-text-3); list-style: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* the icon runs in the words, first in the line (iMessage, Slack): inline, sat on the first line, the words and it centred as one;
   a wrapped line keeps it at the start of its first line */
.th-mic-line .grey-icon { display: inline-block; vertical-align: -2px; margin-right: var(--sp-2); }
.th-mic-line .mic-words { display: inline; }
/* (devil-teachsilent, 10-14) "Your agent couldn't reply — …" (no-reply.js): a longer line whose fixes are words at its end (Top up,
   Replace key, Reconnect, Try again, as a failed line's Try again): it may wrap, centred; its words are never cut */
.th-mic-line.th-no-reply { white-space: normal; overflow: visible; text-wrap: pretty; }
.th-no-reply-fix { white-space: nowrap; }
.th-no-reply .mic-words a { color: var(--fg); text-decoration: underline; }
.th-no-reply-retry { display: inline; margin: 0; }
.th-no-reply-retry .th-opt { color: var(--fg); font: inherit; text-decoration: underline; }
.th-mic-steer { display: block; }
.th-mic-steer summary { display: block; cursor: pointer; list-style: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.th-mic-steer[open] summary { white-space: normal; overflow: visible; text-overflow: clip; }
.th-mic-steer summary::-webkit-details-marker { display: none; }
.th-mic-steer[open] .mic-words, .th-mic-steer details[open] .mic-words { white-space: normal; overflow: visible; text-overflow: clip; text-align: center; }
/* a grey line's time (a receipt's, a hand-over's): none until the pointer is on the line, then right after the words, as a
   message's time shows under the pointer (.th-acts time); never on a touch screen */
.when { display: none; }
@media (hover: hover) {
  .th-mic-line:hover .when, .note-receipt:hover .when { display: inline-flex; align-items: center; gap: var(--sp-2); flex: none; white-space: nowrap; color: var(--c-text-3); font: var(--level-meta); }
}
.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
/* the grey icon of a receipt sits in the mark's slot */
.note-receipt .note-mark .grey-icon { display: block; margin-top: 3px; }
/* the thinking indicator is the dots alone: no words of what the agent is doing beside it */
.th-draft-tool { gap: 0; }
/* a grey line's ground never changes under the pointer (Bonan, 10-13: "不要整个一行都给我有bg"): it only gains its time */
.note-receipt:has(> .note-receipt-open:is(:hover, :focus-visible)) { background: none; }
/* the visitor's "Not now" beside Notify me: a text button, its word grey (devil-m2-convui) */

/* ==== design-critique-1016 (eol_research/design-critique-1016) ===============================================================
   Two kinds of rule. Fixes for everyone: the reaction's badge on its own piece (.th-tap) and a file's kind on its icon.
   Everything else is the new look, behind one switch on the body, new-style (a person's flag; until it is on, the page is as
   it was). The markup this block shows or hides is drawn for everyone and hidden here by default: .th-diff-ico, .th-diff-sum,
   .th-diff-toggle (views/inbox.js changeDiff), .mem-title (views/memory-files.js), .rail-foot-in (views.js rail). ---- */

/* -- for everyone: iMessage's Tapback on the top outer corner of the piece it belongs to (views/inbox.js tapOn): the
   pictures or the files, each in a box of their own width, so the badge never floats beside a narrower picture -- */
.th-tap { position: relative; display: flex; flex-direction: column; max-width: 100%; }
.th-post.mine .th-tap { align-self: flex-end; }
.th-post.theirs .th-tap { align-self: flex-start; }
/* -- for everyone: a file's kind on its icon (views/attachments.js iconBox; iMessage's and Slack's), never a blank page -- */
.th-file-icon.is-kind { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px; width: 36px; height: 40px; border-radius: var(--radius); background: var(--c-bg); color: var(--c-text-2); }
.th-file-icon.is-kind > svg { width: 18px; height: 18px; }
.th-file-kind { font: 700 9px/10px var(--font); letter-spacing: .04em; color: var(--c-text-2); }

/* -- the markup the new look adds, hidden without it -- */
.th-diff-ico, .th-diff-sum, .th-diff-toggle, .mem-title, .rail-foot-in, .rail-sign-more { display: none; }

/* -- 1. a visitor's letter: the unknown face's airy ground and the meta grey, never a robot's colour (person.js is-guest) -- */
body.new-style .pface-mono.is-guest rect { fill: light-dark(rgb(0 0 0 / .055), rgb(255 255 255 / .07)); }
body.new-style .pface-mono.is-guest text { fill: var(--c-text-2); }

/* -- 2. no title row over a head that names the page (My agent, a profile on a phone); a row with a way back keeps the arrow -- */
body.new-style main.wrap:has(> .home-col) > .panel-band:not(:has(> .panel-back)) { display: none; }
body.new-style main.wrap:has(> .home-col) > .panel-band > h1 { visibility: hidden; }
body.new-style[data-rail] main.wrap:has(> .ag-page) > .panel-band:not(:has(> .panel-back)) + .ag-page > .home-row { padding-top: var(--sp-6); }

/* -- 3. your line is you and your own agent (ChatGPT's): no "Your agent" under its runs -- */
body.new-style .th-line .th-post-foot:has(> [data-agent-name]:only-child) { display: none; }

/* -- 5. signed out, the header's Sign in is the quiet button beside the one blue Chat -- */
body.new-style .bar-end > .nav-sign-in { border: 1px solid var(--c-border); background: var(--c-bg-2); color: var(--c-text); }
body.new-style .bar-end > .nav-sign-in:hover { background: var(--c-bg-3); }

/* -- 6. a change in your line is one grey line, ChatGPT's "Memory updated", "+3 −1" and a chevron; pressed, the diff as it was
   (public/app.js data-diff-toggle) -- */
body.new-style .th-post-text.has-diff:has(> .th-diff:not([data-open])) { padding: 0; background: none; color: var(--c-text-2); }
body.new-style .th-post-text.has-diff:has(> .th-diff:not([data-open]))::before,
body.new-style .th-post-text.has-diff:has(> .th-diff:not([data-open]))::after { content: none; }
body.new-style .th-diff:not([data-open]) > :not(.th-diff-head) { display: none; }
/* folded, the line is one line at any width: its row may take the column's width, its words never wrap (a phone's 70% broke it in four) */
body.new-style .th-row:has(> .th-post-text.has-diff > .th-diff:not([data-open])) { max-width: 100%; }
body.new-style .th-diff:not([data-open]) > .th-diff-head { flex-wrap: nowrap; white-space: nowrap; }
body.new-style .th-diff-head { flex-wrap: wrap; align-items: center; gap: var(--sp-1); cursor: pointer; }
body.new-style .th-diff-head > :is(.th-diff-title, .th-diff-sum) { white-space: nowrap; }
body.new-style .th-diff:not([data-open]) .th-diff-title { margin-bottom: 0; }
body.new-style .th-diff-ico { display: inline-flex; flex: none; color: var(--c-text-2); }
body.new-style .th-diff-sum { display: inline; font: var(--level-meta); color: var(--c-text-2); font-variant-numeric: tabular-nums; }
body.new-style .th-diff-toggle { display: inline-grid; place-items: center; width: 20px; height: 20px; margin: 0; padding: 0; border: 0; border-radius: var(--radius-full); background: none; color: var(--c-text-2); cursor: pointer; }
body.new-style .th-diff[data-open] .th-diff-toggle svg { transform: rotate(180deg); }
body.new-style .th-diff-head:hover :is(.th-diff-title, .th-diff-sum, .th-diff-toggle) { color: var(--c-text); }
body.new-style .th-diff[data-open] .th-diff-head { margin-bottom: var(--sp-1); }

/* -- 7. a card waiting in the conversation: its structure as 10-12 set it, a new skin: no frame, as wide as their bubbles at
   most (.th-row's 70%), the raised ground, its options one step up; one blue button, the others outlined -- */
body.new-style .th .note-card:not(.note-connect, .note-report, .note-undo) { max-width: 70%; }
body.new-style .th .note-card:not(.note-connect, .note-report, .note-undo) > form { gap: var(--sp-3); padding: var(--sp-3); border: 0; border-radius: var(--radius-lg); background: var(--c-bg-2); }
body.new-style .th .note-card .ask-opt { background: var(--c-bg-3); }
body.new-style .th .note-card .ask-opt:hover { background: color-mix(in srgb, var(--c-bg-3) 88%, var(--c-text)); }
body.new-style .th .note-card .ask-input { background: var(--c-bg); }
body.new-style .th .note-card .note-ignore { margin-left: calc(-1 * var(--sp-2)); }
body.new-style .th .note-card :is(.note-decisions, .perm-buttons, .grant-buttons) .btn:not(.btn-outline, .btn-danger) { background: var(--c-accent); color: var(--c-on-accent); }
body.new-style .th .note-card :is(.note-decisions, .perm-buttons, .grant-buttons) .btn:not(.btn-outline, .btn-danger) ~ .btn:not(.btn-outline, .btn-danger) { border: 1px solid var(--c-border); background: var(--c-bg); color: var(--c-text); }

/* -- 8. the conversation's column on a wide screen: ChatGPT's (max-w-3xl, 768) in the room right of the list, centred, the
   box under it as wide; the list as it is -- */
body.new-style main.ib { --chat-col: 768px; }
body.new-style main.ib :is(.th-body, .th-compose, .th-dock-note, .th-compose-error) { max-width: calc(var(--chat-col) + 2 * var(--sp-4)); }
body.new-style main.ib .note-composer { max-width: var(--chat-col); }

/* -- 9. signed out, the rail's foot is a quiet Sign in and the settings beside it (ChatGPT's), never a bold "Not signed in" -- */
body.new-style .rail-foot-old { display: none; }
body.new-style .rail-foot-in { display: flex; align-items: center; flex: none; align-self: stretch; margin: auto 0 var(--sp-1); }
body.new-style .rail-sign-in { display: flex; align-items: center; justify-content: center; gap: var(--sp-5); flex: 1 1 auto; min-width: 0; height: 50px; padding: 0 var(--sp-3); border-radius: var(--radius-full); color: var(--c-text-2); text-decoration: none; }
body.new-style .rail-sign-in > svg { flex: none; width: 24px; height: 24px; }
body.new-style .rail-sign-in:hover { background: var(--c-bg-2); color: var(--c-text); }
@media (min-width: 1267px) {
  body.new-style:not([data-rail="closed"]) .rail-sign-in { justify-content: flex-start; }
  body.new-style:not([data-rail="closed"]) .rail-sign-more { display: grid; place-items: center; flex: none; width: 40px; height: 40px; padding: 0; border: 0; border-radius: var(--radius-full); background: none; color: var(--c-text-2); cursor: pointer; }
  body.new-style:not([data-rail="closed"]) .rail-sign-more:hover { background: var(--c-bg-2); color: var(--c-text); }
}

/* -- 10. Memory by what each file is (views/memory-files.js memoryTitle): "Looking for", not looking-for.md -- */
body.new-style .mem-path { display: none; }
body.new-style .mem-title { display: inline; }

/* (new-style names; Bonan: iMessage) your agent's name once, small and grey, over the first bubble of its run; the server draws
   it on every one of your agent's messages (views/inbox.js th-post-by), only a run's first shows it (data-run-first, markRuns). */
.th-post-by { display: none; }
body.new-style .th-post[data-run-first] > .th-post-by { display: block; margin: 0 0 var(--sp-1, 2px); padding: 0 var(--sp-3); font: var(--level-meta); color: var(--c-text-3); white-space: nowrap; }
