/*
 * dsh-remote-access — phone-first UI.
 *
 * Written to the numbers measured on the real phone, not to desktop habits:
 *
 *  1. Every input is >= 16px. A smaller focused input makes iOS zoom the whole
 *     page (measured `visualViewport.scale = 1.143`), which is what makes a
 *     keyboard "cover" the composer.
 *  2. The composer is fixed at `bottom: 0` with no JS height math. Measured:
 *     a fixed element with `bottom: 0` sits exactly on the visual viewport's
 *     bottom edge, so it stays above the keyboard for free.
 *  3. Nothing but `#thread` scrolls, so the keyboard can never move the
 *     composer — the jitter in DSH's own shell came from a composer inside the
 *     scroller.
 *  4. `env(safe-area-inset-*)` reads real values here because the document
 *     declares `viewport-fit=cover`.
 *  5. Tap targets are >= 44px, and no control is hover-only.
 */

:root {
  --bg: #0d1117;
  --bg-raised: #161b22;
  --bg-input: #1c2230;
  --border: #2a3038;
  --text: #e6edf3;
  --text-dim: #8b949e;
  /*
    The desktop's own blue (its icons stroke #539CFA), not a GitHub lookalike: one product,
    one accent. `--accent-dim` is derived, so the pair can never drift apart.
  */
  --accent: #539cfa;
  --accent-dim: color-mix(in srgb, var(--accent) 30%, transparent);
  --danger: #f85149;
  --ok: #3fb950;
  /* The "waiting on a person" colour — approvals, questions. Neither ok-green nor danger-red. */
  --warn: #d29922;
  --radius: 12px;
  --tap: 44px;
  /*
    System fills, the iOS way.

    A native control separates itself from the background with a *translucent fill* rather than
    a border: `secondarySystemFill` for the message field, `tertiarySystemFill` for the small
    round buttons, `separator` for the hairlines, `placeholderText` for placeholder text.
    Deriving them from the text colour keeps the dark theme one palette instead of two.
  */
  --fill: color-mix(in srgb, var(--text) 11%, transparent);
  --fill-strong: color-mix(in srgb, var(--text) 18%, transparent);
  --placeholder: color-mix(in srgb, var(--text-dim) 70%, transparent);
  --hairline: color-mix(in srgb, var(--border) 70%, transparent);
  /* The composer's measured height, written by app.js on every resize. */
  --composer-h: 64px;
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  /*
    What the composer keeps of that bottom inset: 24px（2026-10-14，operator：先 6 再 12 再 14 再 15 再 18 再 20 再 24，
    「还可以再往上一点」「再向上两个 px」「再上一个 px」「位置再高一点」「收起的输入框 位置再高一点」）。
    只在 fullscreen 下留这点呼吸；浏览器/standalone 留 0（它们各有各的底栏/死区）。收敛不再随 `kb-open` 类瞬间发生——改由视口驱动（app.js
    的 syncClearanceWithKeyboard）：vvresize 意味着键盘真的在动，clearance 随它归零/复原，
    不再先掉 14px 再被顶上去。
  */
  --composer-clearance: 0px;
  /*
    iOS 首次键盘异常时，布局视口不随视觉视口一起收缩，`bottom: 0` 会落在可见区下方。
    实测（dsh-phone-bridge/state/remote-access.log，冷启动后第一次聚焦）：

        innerHeight=741  visualViewport.height=465  offsetTop=152.7
        可见底 = 152.7 + 465 = 617.7
        seat.bottom = 741  →  比可见底低 123px（belowSeat = -123，被键盘挡住）

    第二次聚焦时 iOS 把布局视口收缩成与视觉视口一致（465/465），差值为 0，所以"再开一次就好"。
    `--composer-lift` 由 app.js 按 `innerHeight - offsetTop - height` 写入；正常与二次聚焦都是 0，
    对既有行为零影响。
  */
  --composer-lift: 0px;
}
@media (display-mode: fullscreen) {
  :root { --composer-clearance: 24px; }
}

/*
  Light theme.

  The `:root` palette above is the dark default; this overrides the *same* colour variables, and
  every derived colour (`--fill`, `--fill-strong`, `--placeholder`, `--hairline`) is a `color-mix`
  of those, so there is no second list to keep in sync.

  The choice is not this page's own: it comes from the host's `ui-theme` setting
  (`{preference: 'system'|'light'|'dark'}`, measured), which the desktop shares. `app.js` puts the
  preference on `<html data-theme="…">`; for `system` the attribute is absent, and the media query
  below decides — which is also why the explicit `light` rule repeats the same variables.
*/
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    color-scheme: light;
    --bg: #ffffff;
    --bg-raised: #f1f3f5;
    --bg-input: #eceff3;
    --border: #d7dbe0;
    --text: #14181d;
    --text-dim: #667085;
    /* The same brand blue, deepened enough to keep white text legible on it (4.6:1). */
    --accent: #4561ee;
    --accent-dim: color-mix(in srgb, var(--accent) 14%, transparent);
    --danger: #c0392b;
    --ok: #1a7f37;
    --warn: #9a6700;
  }
}

:root[data-theme="light"] {
  color-scheme: light;
  --bg: #ffffff;
  --bg-raised: #f1f3f5;
  --bg-input: #eceff3;
  --border: #d7dbe0;
  --text: #14181d;
  --text-dim: #667085;
  --accent: #4561ee;
  --accent-dim: color-mix(in srgb, var(--accent) 14%, transparent);
  --danger: #c0392b;
  --ok: #1a7f37;
  --warn: #9a6700;
}

:root[data-theme="dark"] { color-scheme: dark; }

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  /* `100%` on both html and body keeps the fixed layout honest; `dvh` is a
     fallback for browsers that support it. Neither is used for keyboard math. */
  background: var(--bg);
  color: var(--text);
  /*
    Type scale: Apple's own text styles for web content, not sizes we picked.
    `font: -apple-system-body` is the `body` style of the system scale (17pt at the
    default Dynamic Type size, and *it follows the size the reader chose in iOS
    Settings* — HIG "Supporting Dynamic Type"). WebKit documents the whole list in
    "Using the System Font in Web Content"; engines that do not know the value drop the
    declaration, which is why the explicit px fallback comes first.
    The CJK leading below it is deliberate: the system style's own leading is tuned for
    Latin, and Chinese wants more air between lines (clreq §6.4).
  */
  font: 17px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
    "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
  font: -apple-system-body;
  /*
    2026-10-14（operator：「正文的行高再大一丢丢」）：1.45 → 1.55。此前 1.45 是按
    参考截图量的（16px × 1.45 = 23.2px 行距，对参考的 23.9px）；实测上眼偏紧。
    表格（1.5）与输入框（1.45）各有自己的行高，不受这条影响；日志行家族用
    caption1 简写，也不随这条。
  */
  line-height: 1.55;
  -webkit-text-size-adjust: 100%;
  overscroll-behavior-y: none;
}

body { overflow: hidden; }

/* ── top bar ─────────────────────────────────────────────────────────────── */

#bar {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: var(--tap);
  padding: calc(var(--safe-top) + 6px) 10px 6px;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  /*
    A UIKit navigation bar: translucent, saturated, blurred, with a hairline that is thinner
    than a CSS pixel on a retina screen — and no hard 1px rule, which is the web tell.
  */
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border-bottom: .5px solid var(--hairline);
}

#bar-title {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

#bar-main {
  font-size: 16px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

#bar-sub {
  font-size: 12px;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-height: 1em;
}

.icon-btn {
  flex: none;
  width: var(--tap);
  height: var(--tap);
  display: grid;
  place-items: center;
  color: var(--text);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 10px;
  cursor: pointer;
  transition: background .15s ease, transform .12s ease;
  -webkit-tap-highlight-color: transparent;
}

.icon-btn:active { background: var(--bg-raised); transform: scale(.92); }
.icon-btn[hidden] { display: none; }

/*
 * The icon set (app.js `icon()`, inline markup in index.html): one 24px-grid stroke icon
 * per control, drawn in `currentColor`, so an icon always matches its label and its
 * neighbours. The base size is 20px; contexts override below. `flex: none` keeps a flex
 * parent from squeezing an icon into a smear.
 */
svg.i { display: block; width: 20px; height: 20px; flex: none; }

/* Creating a session is a host round trip; while it runs the button says so by fading. */
.icon-btn:disabled { opacity: .35; }

/*
 * A blanket version of the same rule, applied to every focusable field.
 *
 * Focus-zoom on iOS is triggered by the computed size of whatever gets focus,
 * so a 16px rule on one selector is a promise that is easy to break by adding
 * another field later. `touch-action: manipulation` also drops the 300 ms
 * double-tap-zoom delay, which otherwise reads as "the buttons do not respond".
 */
@media (pointer: coarse) {
  input, textarea, select, [contenteditable="true"] {
    font-size: max(16px, 1em);
    touch-action: manipulation;
  }
}

/* ── the one scroller ───────────────────────────────────────────────────── */

#thread {
  position: fixed;
  inset: 0 0 0 0;
  /* 在列表之上（列表 20）：左滑返回手势期间会话视图是一整层盖在列表上右移，露出的是
     列表、边界是顶层视图的左缘——iOS 的样子。放在 10 时列表会先盖住整屏，文字稿只剩
     右边一条（operator 2026-10-16 截图：「还没滚完，下层就出来了」）。正常态下另一个
     视图总是 display:none，这个 z-index 只在手势期间起作用。 */
  z-index: 25;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-y: contain;
  /* 竖向滚动交给浏览器，横向手势（左边缘右滑返回）交给 JS——两者分工，不互相抢。
     表格/代码块自己是横向滚动容器，touch-action 不受这条影响。 */
  touch-action: pan-y;
  /* Room for the bar above and the composer below, both of which are fixed. The
     `- var(--composer-lift)` keeps the last message clear of the lifted composer: when iOS
     leaves the layout viewport taller than the visual one, the seat moves up by that much,
     so the reserved space moves up with it instead of stranding content behind it. */
  padding: calc(var(--safe-top) + 58px) 0 calc(var(--composer-h) + var(--composer-clearance) + 8px - var(--composer-lift));
  /* 不透明底色：手势期间会话视图是一张盖在列表上的卡片——没有这层，列表就从文字稿
     的透明缝隙里透出来，两层内容叠在一起（operator 2026-10-16 截图：「移动的时候
     变透明了？」）。常态下这是 no-op：body 本来就是同一个 var(--bg)，渲染不变。 */
  background: var(--bg);
}

#thread[hidden] { display: none; }

/* Session list reuses the same scroller geometry. */
#list-view {
  position: fixed;
  inset: 0 0 0 0;
  z-index: 20;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-y: contain;
  padding: calc(var(--safe-top) + 58px) 0 calc(var(--safe-bottom) + 24px);
  background: var(--bg);
}

#list-view[hidden] { display: none; }

/* ── message rows ───────────────────────────────────────────────────────── */

/*
  Rows are the conversation, not a table of contents.

  * No `border-top` between rows: a hairline every few lines was most of what made the
    transcript look like a list of records. Rhythm comes from the padding instead, and the
    user's bubble is the separator that matters (`row-user` below).
  * No 「你」/「DSH」 label row either — the alignment and the bubble colour say who spoke,
    and the label cost a line of height on every single message.
  * 18px side padding, not 14: the reference measures 18.7px, and the extra 4px narrows the
    line from ~26 to ~25 Chinese characters, which also knocks the sawtooth off the right edge.
*/
.row { padding: 7px 18px; }

/*
  Rhythm pass (排版反馈, 2026-10-13):
  * Log lines sit tighter than paragraphs — a tool run or a folded process is a one-line
    caption between messages, and giving it prose-level padding made the transcript read
    as evenly spaced noise where nothing stood out.
  * The fold *title* is metadata, not content: dim and normal-medium weight, so the prose
    it interrupts stays the loudest thing on the page.
  * A new exchange breathes: the person's bubble carries a little extra air on top.
  * Prose right after a log line gets a beat of its own, so an answer never reads as the
    tail of the tool line above it.
*/
.row-tool-group,
.row.row-process,
.row-thinking,
.row-injected { padding-top: 5px; padding-bottom: 5px; }

/*
 * 优先级陷阱（2026-10-14，operator 圈图：工具组行的边距与字体和思考行不一致）：
 * 日志行家族的规则必须压过卡片基线 `details.block > summary`（0,1,2）与
 * `details.block > summary .name`（0,2,2）。`.tool-group > summary`（0,1,1）这种写法
 * 永远赢不了——规则明明写在那儿，浏览器里却从未生效（工具组行一直吃着卡片的
 * 15px subheadline、600 字重、8px 12px 内边距）。家族规则统一用
 * `.row-tool-group details.block > summary` / `.row.row-process details.block > summary`
 * （0,3,2）这一档。
 */
.row-tool-group details.block > summary .name,
.row.row-process details.block > summary .name {
  color: var(--text-dim);
  font-weight: 400;
  /* Native DisclosureRow: the title never shrinks (flex:none) — the preview beside it is
     what truncates. Without this, a crowded row wrapped 「思考」/「已调用…」 vertically
     (实测：内容满时"思考"挤成上下两行). */
  flex: none;
}

/* Prose right after a log line gets a beat of its own, so an answer never reads as the
   tail of the tool line above it. */
.row-tool-group + .row-assistant,
.row-process + .row-assistant { padding-top: 12px; }

/*
 * …但思考行紧跟着工具行时是另一回事（operator 2026-10-14：「思考行和工具行之间的
 * 行间距不用留那么大」，看过 2px 版后仍嫌大）：推理后面紧跟的调用就是那段推理
 * 想出来的，两行是一件事的上下半。行盒从 40px 降到 32px、外包装归零——两条文字
 * 之间只剩 4+4px，视觉上仍分得开。
 */
/* ── 工作栈节奏（2026-10-14，operator 圈图：「行距不太对劲」）─────────────────
   桌面节奏（ui-chat README）：日志行互相贴紧，回答与过程行之间 12px。落到本页：
   · 思考块永远住在助手行里，summary 统一 32px（它不是独立行，不吃独立行高）；
   · 日志行（工具/过程/运行状态/思考独占一行的助手行）互为邻居时，相邻两侧 padding 归零；
   · 工具/过程行后面是以**正文**开头的助手行：仍是「散文跟在日志行后面」的 12px 上留白。 */
.row:has(> details.think:last-child) { padding-top: 5px; padding-bottom: 5px; }
:is(.row-tool-group, .row-tool, .row-process, .row-thinking):has(+ :is(.row-tool-group, .row-tool, .row-process, .row-thinking, .row-assistant)) { padding-bottom: 0; }
:is(.row-tool-group, .row-tool, .row-process, .row-thinking) + :is(.row-tool-group, .row-tool, .row-process, .row-thinking) { padding-top: 0; }
.row:has(> details.think:last-child) + :is(.row-tool-group, .row-tool, .row-process, .row-thinking, .row-assistant) { padding-top: 0; }
:is(.row-tool-group, .row-tool, .row-process, .row-thinking) + .row-assistant:has(> details.think:first-child) { padding-top: 0; }

/*
 * A row that *arrives* enters from a touch below. `renderThread` adds the class only to
 * new keys after first paint, so neither the initial 600 rows nor a stream rebuilding
 * the same line replays it. (Reduced motion: the global rule at the bottom kills it.)
 */
.row-enter { animation: row-in .16s ease; }
@keyframes row-in { from { opacity: 0; transform: translateY(6px); } }

/* The person's side of an IM: right-aligned, in a bubble that never spans the screen. */
.row-user {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  /* A new exchange breathes: extra air above the bubble, so one turn never reads as the
     tail of the previous answer. */
  padding-top: 13px;
}
.row-user .bubble {
  max-width: 80%;
  padding: 8px 13px;
  /* iMessage's own geometry: an 18px corner and a solid accent fill with white text. */
  border-radius: 18px;
  background: var(--accent);
  color: #fff;
  white-space: pre-wrap;
  /* Same Chinese rules as `.body`: what a person typed deserves the same line breaking
     as what the model wrote. */
  line-break: strict;
  word-break: normal;
  overflow-wrap: break-word;
  hanging-punctuation: allow-end; /* same reason as `.body` */
  text-autospace: normal;
  text-spacing-trim: trim-both; /* same as `.body`: see the note there */
}

.row-system { color: var(--text-dim); font-size: 13px; }

/*
  A run that failed. It has to read as an event, not as prose: a red left edge, the
  reason in full, and one action. Measured complaint behind it — a transport failure
  left the phone showing nothing at all ("我什么也不知道，就一直在这里等").
*/
.row-failure { background: color-mix(in srgb, var(--danger) 9%, transparent); }

.failure {
  border-left: 3px solid var(--danger);
  padding-left: 10px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-items: flex-start;
}

.failure-title {
  color: var(--danger);
  font: 15px/1.4 -apple-system, "PingFang SC", sans-serif;
  font: -apple-system-subheadline;
  font-weight: 600;
}
.failure-body {
  color: var(--text);
  font-size: 13px;
  line-break: strict;
  word-break: normal;
  overflow-wrap: break-word;
}

.failure-retry {
  font: inherit;
  font-size: 13px;
  color: var(--text);
  background: color-mix(in srgb, var(--danger) 22%, transparent);
  border: 1px solid color-mix(in srgb, var(--danger) 45%, transparent);
  border-radius: 8px;
  padding: 6px 14px;
  min-height: 34px;
}

/*
  (The 「你」/「DSH」 label rows are gone, and with them this block and its `pulse` dot. Two
  rules survived the move: Chinese labels never get `letter-spacing` or `text-transform`
  — Han characters are 1:1 squares set flush against one another (clreq §1.2) — and text
  sizes come from the system scale. `mobile-typography-check.mjs` now asserts the first of
  those across the whole stylesheet instead of on this one selector.)
*/

@keyframes spin { to { transform: rotate(360deg); } }

/*
  Chinese line breaking, from clreq and from what Apple's own site does by hand:

  * `line-break: strict` — 行首行尾禁则 (§6.1.1): a closing punctuation mark may not
    begin a line. Without it the browser happily starts a line with 「，」or 「。」.
  * `word-break: normal` + `overflow-wrap: break-word` — a Latin token stays whole
    unless it genuinely cannot fit. `anywhere` (what this used to say) split
    `mobile-ui-composer-check` and `session-2c465e34` mid-word, which read as corruption;
    Apple protects such tokens with `&nbsp;`. Breaking stays available for the one case
    that needs it — code and URLs (`pre`, `code` below).
  * `text-wrap: pretty` — the automatic form of Apple's manual `nowrap` trick on the last
    word of a paragraph, which exists to avoid a one-character last line (孤行).
  * `text-autospace: normal` — clreq §6.3.3 asks for up to a quarter-em between Han
    characters and Latin/digits; Apple inserts it as literal spaces in its copy, the
    platform does it for apps, and this is the property that finally does it on the web.
  * `text-spacing-trim` — Apple narrows full-width punctuation to 0.5–0.6 em in SF Pro SC
    so a heading does not gape around its 「，」; this trims it at the line edges.
*/
.body {
  line-break: strict;
  word-break: normal;
  overflow-wrap: break-word;
  text-autospace: normal;
  /*
    The fix for "感觉没到头就换行了".

    A closing 「，」/「。」 may not begin a line (clreq §6.1.1 行首禁则), so when one is next the
    browser breaks the line *before* it and leaves a whole character of blank at the right edge
    — on a ~25-character line that is 4% of the measure, and it happens on most lines in Chinese
    prose. `hanging-punctuation: allow-end` lets that mark hang into the 18px margin instead.
    WebKit has had this property for years (it originated there); engines without it simply
    ignore the line and keep the old behaviour.
  */
  hanging-punctuation: allow-end;
  /*
    `trim-both`, not `space-first`.
    Measured on the phone (440 CSS px wide): the left edge of a paragraph is a hard 14.9px on
    every line, while the right edge lands anywhere between 29px and 98px from the screen —
    CJK wraps on whole characters, and a full-width 「，」/「。」 at the line end keeps half a
    character of empty space. `space-first` *preserves* that space (it is the traditional
    setting for line starts); `trim-both` takes it off both ends, which is what pulls the
    right edge in and stops the block from reading as though it leans left.
  */
  text-spacing-trim: trim-both;
}
/*
  Paragraph break ≈ a full blank line (measured on the reference: 47px between the last line
  of one paragraph and the first of the next, against a 23.9px line pitch). That is what makes
  a paragraph read as a paragraph — the previous 0.9em (14px) was barely more than the leading
  it was supposed to stand out from.
*/
.body p { margin: 0 0 1.3em; }
/*
  Left-aligned, ragged right — deliberately **not** 均等排.
  Justification was tried for a day (2026-10-07), because the left edge is a hard 14.9px on
  every line while the right edge wandered 29–98px and the block read as though it leaned
  left. Seen on the phone it was worse: a phone line holds ~24 Chinese characters, so
  stretching a short line pushes the gaps between characters wide open and the text looks
  spaced out ("这个很别扭"). Chinese messaging apps keep the ragged right edge for the same
  reason. What actually pulls the right edge in is `text-spacing-trim: trim-both` above —
  that stays.
*/
.body p:last-child { margin-bottom: 0; }
.body ul, .body ol { margin: .4em 0 .9em; padding-left: 1.3em; }
.body li { margin: .3em 0; }
.body a { color: var(--accent); }
.body code {
  /*
    Inline code, calmed down.

    Measured on the phone: the old chip was 13.1px (0.82em) with a 1px border, sitting in
    16px Chinese — it read as a different typeface pasted into the line, and a bordered box
    on every identifier is most of what made a technical paragraph look noisy. Same family,
    nearly the same size, no border: the tint alone says "this is code".
  */
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .9em;
  /* Inherit the paragraph's leading: a chip must not change the line box it sits in. */
  line-height: inherit;
  background: color-mix(in srgb, var(--text) 9%, transparent);
  border: 0;
  border-radius: 4px;
  padding: .06em .3em;
  /* `text-autospace` only spaces characters *within* a text run, so a chip needs its own
     breathing room against the Chinese on either side. */
  margin: 0 .12em;
  /* The one place a long token may be split: a path or a hash has no break opportunity of
     its own, and an overflowing code span is worse than a broken one. */
  overflow-wrap: anywhere;
}
/*
  Code blocks, without the boxy look.

  The previous shape — a 1px border around a raised fill, 8px radius, 10/12px padding — read as a
  web form element next to the rest of the page, and on a phone it was both cramped and heavy
  ("代码块显示很丑"). A code block is content, not a control: a soft translucent fill, a 12px
  radius, roomier padding, and a slightly smaller monospace size so more of a line fits before it
  has to scroll.
*/
.body pre {
  margin: .6em 0;
  padding: 12px 14px;
  background: color-mix(in srgb, var(--text) 7%, transparent);
  border: 0;
  border-radius: 12px;
  overflow-x: auto;
  tab-size: 4;
  font-size: 13px;
  line-height: 1.55;
  -webkit-overflow-scrolling: touch;
}
.body pre code { background: none; border: 0; padding: 0; margin: 0; font-size: inherit; }

/*
  A fenced block as a card (app.js `codeCard()`): a header strip with the language and
  a copy control, then the code. The header is what a naked `<pre>` could not offer —
  "what language is this" and a copy target that does not scroll away with the code.
  The card owns the radius and the fill; the `<pre>` inside keeps only its scrolling.
*/
.code-card {
  margin: .6em 0;
  overflow: hidden;
  background: color-mix(in srgb, var(--text) 7%, transparent);
  border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
  border-radius: 12px;
}

.code-head {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 30px;
  padding: 1px 4px 1px 12px;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
}

.code-lang {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  color: var(--text-dim);
}

.code-copy {
  flex: none;
  margin-left: auto;
  width: 30px;
  height: 28px;
  display: grid;
  place-items: center;
  background: transparent;
  border: 0;
  border-radius: 7px;
  color: var(--text-dim);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.code-copy svg.i { width: 14px; height: 14px; }
.code-copy:active { background: var(--fill-strong); }

.code-card > pre { margin: 0; background: transparent; border-radius: 0; }
.body h1, .body h2, .body h3 { margin: .8em 0 .35em; line-height: 1.4; }
.body h1 { font-size: 1.35em; font-weight: 700; }
.body h2 { font-size: 1.12em; font-weight: 600; }
/* h3 used to be the same size as body text, so the third level carried no hierarchy at
   all. HIG: express hierarchy with size *and* weight. */
.body h3 { font-size: 1em; font-weight: 600; }
.body blockquote {
  margin: .4em 0;
  padding: .1em .8em;
  border-left: 3px solid var(--border);
  color: var(--text-dim);
}
/*
  A table on a phone: it scrolls sideways, and its **first column stays put**.
  Measured on this device, a three-column Chinese comparison table is about 1.6× the
  viewport width — so this is the normal case, not the edge case. Scrolling the row label
  out of view is what makes such a table unreadable: the numbers stay, the meaning goes.
*/
.table-wrap { position: relative; margin: .5em 0; }

/* Says "there is more to the right" without adding a word of UI text. Toggled by
   `markTables()` in app.js, which measures the table and listens for its scroll. */
.table-wrap::after {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 28px;
  background: linear-gradient(to right, transparent, var(--bg));
  opacity: 0;
  transition: opacity .18s ease;
  pointer-events: none;
}

.table-wrap.scrollable::after { opacity: 1; }
.table-wrap.at-end::after { opacity: 0; }

.body table {
  /* `separate`, not `collapse`: collapsed borders and sticky cells fight each other, and
     the frozen column loses its frame. The frame is drawn per cell below instead. */
  border-collapse: separate;
  border-spacing: 0;
  width: 100%;
  margin: 0;
  /* 14px in a 440pt-wide phone spent the width on padding instead of content; .85em with
     more air reads better, and it follows the reader's text size with the rest. */
  font-size: .85em;
  line-height: 1.5;
  display: block;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  /* Opaque, so the frozen column and the cells sliding under it share a backdrop. */
  background: var(--bg);
}

.body th, .body td {
  padding: 8px 10px;
  text-align: left;
  border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.body tr > :first-child { border-left: 1px solid var(--border); }
.body thead th { border-top: 1px solid var(--border); background: var(--bg-raised); }
/* Short headers such as 「修前」were wrapping to two lines — 修 / 前 — which is worse
   than a wider column. */
.body th { white-space: nowrap; }

/* The frozen row label. Opaque background, or the columns scroll *through* it; the
   hairline travels with it so the freeze reads as deliberate rather than as a glitch. */
.body th:first-child,
.body td:first-child {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--bg);
  box-shadow: 1px 0 0 var(--border);
}
.body thead th:first-child { background: var(--bg-raised); }

/*
  Back to the newest message: a floating pill just above the composer, only while the reader
  is away from the bottom.
*/
#jump {
  position: fixed;
  right: 16px;
  bottom: calc(var(--composer-h) + var(--composer-clearance) + 14px - var(--composer-lift));
  z-index: 45;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--bg-raised);
  color: var(--text);
  box-shadow: 0 2px 10px rgb(0 0 0 / 35%);
}
#jump svg.i { width: 18px; height: 18px; }
#jump[hidden] { display: none; }

/*
  Copy, as an icon at the **bottom left** of the reply and only once the reply is complete.
  The icon is an inline SVG (`icon('copy')` in app.js): `⧉` and friends are missing from
  some system fonts, and a tofu box where a button should be is worse than no button.
  「从此处分叉」（.fork）与复制共用这一套盒子与字号——同一行里的两个小图标。
*/
.copy, .fork {
  display: grid;
  place-items: center;
  margin: 8px auto 0 0;
  width: 34px;
  height: 30px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text-dim);
}

/* 图标与旁边的说明小字同号（13px；operator 2026-10-14：「复制图标有点大」）。点击区不变。 */
.copy svg.i, .fork svg.i { width: 13px; height: 13px; }

/* Copied: a one-second green check where the glyph was (the desktop's answer, no toast). */
.copy.done,
.code-copy.done { color: var(--ok); }

/* The copy control and the fork share one strip under a finished answer. */
.row-actions { display: flex; align-items: center; gap: 4px; margin-top: 8px; }
.row-actions .copy, .row-actions .fork { margin: 0; }

/* 用时与结束时间：操作条最右侧的一行小字（桌面操作行的 token 用量+时间戳位）。 */
.row-times {
  margin-left: auto;
  padding: 0 4px;
  color: var(--text-dim);
  font: 12px/1.3 -apple-system, "PingFang SC", sans-serif;
  font: -apple-system-caption1;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.fb {
  min-height: 30px;
  padding: 0 10px;
  background: transparent;
  border: 0;
  border-radius: 8px;
  color: var(--text-dim);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.fb:active { opacity: .55; }
.fb.on { color: var(--accent); background: var(--bg-raised); }

.copy:active { opacity: .55; }

/* The reader's own bubble is the copy button. */
.row-user .bubble { cursor: pointer; }
.row-user .bubble:active { opacity: .75; }

/*
  The answer to a question card, as one quiet line.

  It used to render as an ordinary message — a bubble full of `{"kind":"answer_to_pending_question",
  …}`. The card above it already carries the question, so all that is worth keeping is what was
  chosen.
*/
.row-answered {
  padding-top: 2px;
  padding-bottom: 2px;
  color: var(--text-dim);
  font-size: 14px;
}
.row-answered .body { font-size: 14px; }

/*
  A finished turn's working-out, folded away.

  It reads like the tool rows it contains (quiet, no card) because it *is* one of them as far as
  the reader is concerned — the difference is that it stands for a whole run instead of one call.
  The children are ordinary rows, so the nesting is flattened visually: no extra padding, no
  second hairline. (The title's colour/weight live with the other fold titles, near `.row`.)
*/
/*
  The fold's body is flat like the family's other bodies: no second hairline, no card
  padding — the inner rows carry their own rhythm.
*/
.row.row-process .block-body.process-body { border-top: 0; padding: 0 0 8px; }

.process-body > .row { padding-left: 0; padding-right: 0; }
.process-body > .row:first-child { padding-top: 2px; }
.process-body > .row:last-child { padding-bottom: 2px; }

/*
  A question the run is blocked on.

  It has to read as something to *do*, not as another message: an accent edge, real buttons,
  and a submit. Measured before this existed: an `ask_user_question` call arrived as an
  ordinary tool row, so the phone showed a turning gear forever while the agent waited for a
  person who could not see the question.
*/
.question {
  /*
   * Amber, not accent: this card means "a person has to act", and the brand blue already
   * means "tap to open". The two meanings stay distinct at a glance.
   */
  border-left: 3px solid var(--warn);
  background: color-mix(in srgb, var(--warn) 12%, transparent);
  border-radius: 10px;
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.question-head {
  font: 12px/1.4 -apple-system, "PingFang SC", sans-serif;
  font: -apple-system-caption1;
  color: var(--warn);
  font-weight: 600;
}

.question-block { display: flex; flex-direction: column; gap: 6px; }
.question-header { color: var(--text-dim); font-size: .85em; }
.question-text { font-weight: 600; }
.question-detail { color: var(--text-dim); font-size: .9em; white-space: pre-wrap; }

.question-options { display: flex; flex-wrap: wrap; gap: 6px; }

.question-option {
  font: inherit;
  font-size: .95em;
  padding: 8px 12px;
  min-height: 38px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg-raised);
  color: var(--text);
  text-align: left;
}
.question-option.chosen {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 28%, transparent);
}
.question-option[disabled] { opacity: .55; }

.question-custom {
  font: inherit;
  font-size: .95em;
  padding: 9px 10px;
  min-height: 40px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--bg-input);
  color: var(--text);
}

.question-submit {
  align-self: flex-start;
  font: inherit;
  font-size: .95em;
  font-weight: 600;
  padding: 9px 18px;
  min-height: 40px;
  border: 0;
  border-radius: 10px;
  background: var(--accent);
  color: #fff;
}
.question-submit[disabled] { opacity: .5; }

/*
  The bottom-of-conversation status while a run is in flight. (It used to be a dim
  「正在思考」 line with a caret; 2026-10-13 it became the desktop's run status — the
  caret survives only *inside* a streaming answer, where it marks the writing point.)
*/
/*
  The bottom running status, copied from the desktop's「深度求索中，用时 N ···」.
  2026-10-14（operator：「思考行、工具调用行、深度求索行这三行样式要统一起来」）：
  几何与字体并入日志行家族——同一个 40px 行高、7px 上下、6px 部件间距、14px 左图标、
  13px caption1 常规字重的 dim 文字。鲸尾图标保留 accent 色与游动（它就是家族里
  「还在跑」的齿轮）；文字的扫光保留，但底色从 accent 蓝改成家族的 dim 灰。
*/
.run-status {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  padding: 4px 0;
}
/*
  The whale tail (native RunningWhaleTail): an APNG sprite applied as a mask, painted in
  the accent colour by `background: currentColor` — the APNG owns its own animation timing.
  The stroked static tail is the fallback for no-mask engines and reduced motion.
*/
.run-status .whale { position: relative; flex: none; width: 14px; height: 14px; color: var(--accent); }
.run-status .whale-anim { display: none; position: absolute; inset: 0; }
.run-status .whale-still { width: 100%; height: 100%; }
@supports (mask-mode: alpha) and (mask-image: url('')) {
  @media (prefers-reduced-motion: no-preference) {
    .run-status .whale-anim {
      display: block;
      background: currentColor;
      will-change: transform;
      -webkit-mask: url('/m/whale-tail.png') center / contain no-repeat;
      mask: url('/m/whale-tail.png') center / contain no-repeat;
    }
    .run-status .whale-still { display: none; }
  }
}
.run-status .run-text {
  /* 与思考行/工具行同一字级字重（caption1，随 Dynamic Type）；扫光图案与流式
     思考预览相同（dim → 正文色 → dim），运动是这一行"还活着"的信号。 */
  font: 13px/1.4 -apple-system, "PingFang SC", sans-serif;
  font: -apple-system-caption1;
  font-weight: 400;
  background-image: linear-gradient(90deg, var(--text-dim) 42%, var(--text) 50%, var(--text-dim) 58%);
  background-size: 200% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: think-sweep 1.6s linear infinite;
}

.cursor {
  display: inline-block;
  width: 8px;
  height: 1.05em;
  margin-left: 2px;
  vertical-align: text-bottom;
  background: var(--accent);
  animation: blink 1s steps(2, start) infinite;
}

@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }

/* ── collapsible blocks (reasoning, tool calls) ─────────────────────────── */

details.block {
  margin: 8px 0 0;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
}

details.block > summary {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: var(--tap);
  padding: 8px 12px;
  /* A tool row's heading is body-sized text; the status pill next to it is a caption.
     Two sizes, both from the system scale — HIG's "minimize the number of typefaces /
     keep the hierarchy" without inventing numbers. */
  font: 15px/1.4 -apple-system, "PingFang SC", sans-serif;
  font: -apple-system-subheadline;
  color: var(--text-dim);
  cursor: pointer;
  list-style: none;
  -webkit-tap-highlight-color: transparent;
}

/*
  Tool rows are lines, not cards.
  The operator's reference (ChatGPT on iOS) draws a tool activity as one plain line —
  `已运行 asc web privacy --help; rg --files api | he…  ›` — with no box around it. Measured
  on this device, that reference fits five activities where our cards fit two. So the card
  chrome (background, border, radius) goes; the row keeps its chevron, its status and its
  expandable body, and nothing else.

  2026-10-13 统一（operator: 工具行和「深度求索中」状态行要对齐）：工具组行、过程折叠行、
  运行状态行是同一个"日志行家族"——同一个左缘（行的 18px 内边距处）、同一个 15px 图标、
  同一个 13px 说明文字级、都没有卡片 chrome。
*/
.row-tool details.block,
.row-tool-group details.block,
.row.row-process details.block,
.tool-group-body details.block {
  margin: 0;
  background: transparent;
  border: 0;
  border-radius: 0;
  overflow: visible;
}

.row-tool details.block > summary,
.row-tool-group details.block > summary,
.row.row-process details.block > summary {
  /* One text line plus a little air. 32px is below the 44pt tap guideline on purpose: these
     are disclosure lines inside a scroller, and a run of them is what has to stay compact —
     the expanded body is where a thumb actually lands. Side padding is zero: the icon sits
     on the row's own 18px edge, exactly like the run-status line below.
     （行高沿革：36 → 40（2026-10-14「行距增大一丢丢」）→ 32（同日「工具行间距还是大，再缩」）。） */
  min-height: 32px;
  padding: 4px 0;
  /* Native DisclosureRow rhythm: 6px between the parts (icon, title, dot, summary). */
  gap: 6px;
}

/* One caption size for the whole log-line family (13px, same as the run status). */
.row-tool-group details.block > summary,
.row.row-process details.block > summary {
  font: 13px/1.4 -apple-system, "PingFang SC", sans-serif;
  font: -apple-system-caption1;
}

/* And one icon size — 14px, the native DisclosureRow's glyph (in its 16px box). */
.row-tool-group details.block > summary .kind svg.i,
.row.row-process details.block > summary .kind svg.i,
details.block.think > summary .kind svg.i { width: 14px; height: 14px; }

/* The browser's own disclosure triangle would double the chevron. */
details.block > summary::-webkit-details-marker { display: none; }

/*
  A fold answers the finger. Every other tappable on the page darkens or scales on :active;
  a summary that did nothing read as dead (排版反馈, 2026-10-13).
*/
details.block > summary:active { opacity: .6; }

/* The kind icon at the left of a tool line (the reference has small icons there). */
details.block > summary .kind {
  flex: none;
  display: inline-flex;
  align-items: center;
  color: var(--text-dim);
}
details.block > summary .kind svg.i { width: 14px; height: 14px; }

/*
  A call that is still out turns its gear. Motion is the one signal a static row cannot
  fake, so the row that moves is the row that is working — no need to read the status word.

  尺寸不再放大（operator 2026-10-14：「工具的图标好像比其他两行的要大」）——三行
  （思考/工具/深度求索）的图标统一 14px；运行中的识别靠 accent 色 + 转动 + 脉动，
  不靠更大。
*/
details.block > summary .kind.running {
  color: var(--accent);
  animation: spin 1.1s linear infinite, glow 1.1s ease-in-out infinite;
}

@keyframes glow { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }

/*
  A tool line's text is *what the call did* — `rg --files api`, `app.js`, `Mobile-UI` — not
  a function name, so it is content rather than a heading; it truncates instead of pushing
  the status and the chevron off the line. Same type as the thinking row (native ToolRow's
  summary is the DisclosureRow's proportional caption, dim — not monospace).
*/
.row-tool details.block > summary .name,
.tool-group-body details.block > summary .name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: inherit;
  font-weight: 400;
  color: inherit;
}

details.block > summary .chev {
  flex: none;
  margin-left: 4px;
  display: inline-flex;
  align-items: center;
  color: var(--text-dim);
  transition: transform .15s ease;
}
details.block > summary .chev svg.i { width: 15px; height: 15px; }
details.block[open] > summary .chev { transform: rotate(90deg); }

details.block > summary .name { color: var(--text); font-weight: 600; }
details.block > summary .tag {
  margin-left: auto;
  /* A word, not a pill: the border was most of the visual weight on a run of tool lines,
     and "成功" is the default state — it only has to be legible, not loud. Failure and
     running are the states that earn colour. */
  border: 0;
  padding: 0 4px;
  font: 12px/1.3 -apple-system, "PingFang SC", sans-serif;
  font: -apple-system-caption1;
  color: var(--text-dim);
}
details.block > summary .tag.err { color: var(--danger); }
details.block > summary .tag.ok { color: var(--text-dim); }

.block-body {
  padding: 0 12px 12px;
  font-size: 14px;
  color: var(--text);
  border-top: 1px solid var(--border);
  padding-top: 10px;
}

.block-body pre { margin: 0; max-height: 46vh; }

/*
 * 注入的上下文（授权决定、后台作业通知等）与日志行家族同款：去掉卡片 chrome，
 * 一行扁平的 caption（operator 2026-10-14：「授权决定卡也可以改成工具行的样子」）。
 * 左缘的小箭头保留——这行没有类别图标，它是唯一的"可展开"提示。
 */
.row-injected details.block {
  margin: 0;
  background: transparent;
  border: 0;
  border-radius: 0;
  overflow: visible;
}
.row-injected details.block > summary {
  min-height: 32px;
  padding: 4px 0;
  gap: 6px;
  color: var(--text-dim);
  font: 13px/1.4 -apple-system, "PingFang SC", sans-serif;
  font: -apple-system-caption1;
}
.row-injected details.block > summary .name { color: inherit; font-weight: 400; }
.row-injected .block-body {
  border-top: 0;
  padding: 4px 0 8px 22px;
  font-size: 13px;
  color: var(--text-dim);
}

/* Between a tool call's arguments and its output inside one block body. */
.blk-sep {
  height: 1px;
  margin: 10px 0;
  background: var(--border);
}

/* The middle of a long output, folded to one quiet line (desktop rule: head+tail kept). */
.fold-more {
  display: block;
  width: 100%;
  margin: 6px 0;
  padding: 7px 10px;
  background: transparent;
  border: 1px dashed var(--border);
  border-radius: 8px;
  color: var(--text-dim);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.fold-more:active { background: var(--fill); }

/*
  The thinking row, native spec (ReasoningRow + DisclosureRow, measured from ui-chat /
  ui-primitives): a quiet log line inside the answer — no card, no border, no background.
  14px think glyph, 「思考」 13px regular in dim, a 2px separator dot, then a one-line
  preview in the same dim caption; the streaming preview fades at the right edge (native
  mask) under the shimmer sweep. The body indents 22px (under the text, not the icon).
*/
details.block.think {
  margin: 0;
  background: transparent;
  border: 0;
  border-radius: 0;
  overflow: visible;
}
details.block.think > summary {
  /* 思考块永远住在助手行里，不是独立行：32px 行盒（2026-10-14 工作栈节奏，见上方
     「工作栈节奏」一节；40px 的独立行高是散文与散文之间的东西）。 */
  min-height: 32px;
  padding: 3px 0;
  gap: 6px;
  color: var(--text-dim);
  font: 13px/1.4 -apple-system, "PingFang SC", sans-serif;
  font: -apple-system-caption1;
}
details.block.think > summary .kind { color: inherit; }
details.block.think > summary .kind svg.i { width: 14px; height: 14px; }
details.block.think > summary .name { font-weight: 400; color: inherit; flex: none; }

.think-sep {
  flex: none;
  width: 2px;
  height: 2px;
  border-radius: 1px;
  margin: 0 2px;
  background: var(--text-dim);
}

.think-preview {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: inherit;
  font-weight: 400;
  color: inherit;
}

@keyframes think-sweep { from { background-position: 200% 0; } to { background-position: -100% 0; } }
.think-preview.streaming {
  background-image: linear-gradient(90deg, var(--text-dim) 42%, var(--text) 50%, var(--text-dim) 58%);
  background-size: 200% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: think-sweep 1.6s linear infinite;
  /* 原生流式预览：右缘 48px 渐隐、不吃省略号（DisclosureRow data-streaming 的 mask）。 */
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 48px), transparent);
  mask-image: linear-gradient(90deg, #000 calc(100% - 48px), transparent);
  text-overflow: clip;
}

.think-body {
  border-top: 0;
  padding: 4px 0 8px 22px;
  font-size: 13px;
  color: var(--text-dim);
}

/*
  A run of tool calls, folded into one row.

  Reported with a screenshot: eight consecutive calls drew eight full-width cards, so a
  whole screen held nothing but shells. The group is one card; inside it each call is a
  plain line on a rail, not another card — nesting cards was what made the run heavy.
*/
/*
  The group line is ONE quiet caption, not three font treatments stitched together
  (排版反馈 2026-10-13: "和文字不太协调"): caption size and the same dim colour for the
  title, the histogram and the status — so it reads as a single log line between two
  paragraphs of prose. The failure count is the only colour on the line, and red is
  the whole signal (×2, no "失败" word). (字号本体在上方"日志行家族"规则里。)
*/
.row-tool-group details.block > summary .name { white-space: nowrap; }
.row-tool-group details.block > summary .names {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* 与标题同字同号（原生 ToolRow 的 summary 就是 DisclosureRow 的比例字 caption），整条一个灰度。 */
  font-size: inherit;
  color: inherit;
}

.tool-group-body {
  /* The rail: the calls belong to the group header above them. */
  padding: 4px 0 6px 10px;
  border-left: 2px solid color-mix(in srgb, var(--border) 80%, transparent);
  margin-left: 12px;
  font-size: inherit;
}

/* Inside the group, a call is a line, not a card. */
.tool-group-body details.block {
  margin: 0;
  background: transparent;
  border: 0;
  border-radius: 0;
}
.tool-group-body details.block + details.block {
  border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
}
.tool-group-body details.block > summary {
  /* 组内成员与外层日志行同高（32px）：展开后是列表里的行，不是卡片。 */
  min-height: 32px;
  padding: 4px 12px 4px 0;
  /* 与外层日志行同一个字级（原生嵌套子调用也不缩字号）。 */
  font-size: inherit;
}
.tool-group-body .block-body { padding: 0 12px 10px 0; border-top: 0; padding-top: 0; }
.tool-group-body .block-body pre { max-height: 34vh; }

.empty {
  padding: 40px 20px;
  text-align: center;
  color: var(--text-dim);
  font-size: 14px;
  /* Some empty states explain themselves in two lines. */
  white-space: pre-line;
}

/* ── skeletons: the shape of the content while it loads ─────────────────── */

/*
 * A soft sweep over placeholder bars, in the palette's own fills. With reduced motion
 * (or anywhere animations are off) the gradient simply sits still — a quiet fill,
 * not a broken shimmer.
 */
.skel { padding: 16px 18px; display: flex; flex-direction: column; gap: 15px; }
.skel-row, .skel-bar {
  background: linear-gradient(90deg, var(--fill) 25%, var(--fill-strong) 50%, var(--fill) 75%);
  background-size: 200% 100%;
  animation: skel-sweep 1.3s ease-in-out infinite;
}
.skel-row { height: 16px; border-radius: 8px; width: 70%; }
.skel-row.skel-user { width: 46%; align-self: flex-end; border-radius: 9px; }
@keyframes skel-sweep { from { background-position: 160% 0; } to { background-position: -60% 0; } }

.skel-list-row {
  display: flex;
  flex-direction: column;
  gap: 9px;
  padding: 15px 14px;
  border-bottom: 1px solid var(--hairline);
}
.skel-bar { height: 13px; border-radius: 6px; }
.skel-bar.skel-thin { height: 10px; }

/* ── session list ───────────────────────────────────────────────────────── */

/*
 * 列表这一整块不长按选字。
 *
 * 行上有长按手势（弹菜单），iOS Safari 会同时起它自己的长按呼出（选字放大镜 + Copy/Lookup）
 * ——两个叠在一起。这一条把列表 chrome（行、说明行、分组标题，以及以后加进来的都算）全兜住；
 * `.list-item` 上那份带着来由注释，两者不重复劳动。转写正文在 #thread 里，选字复制照旧。
 */
#list-view {
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

.list-item {
  display: block;
  width: 100%;
  text-align: left;
  /* Room on the right for the disclosure chevron, and a tall enough row for a thumb
     (HIG's 44pt minimum, with the two text lines inside it). */
  padding: 12px 30px 12px 14px;
  min-height: 58px;
  position: relative;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--border);
  color: var(--text);
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  /*
   * 长按是我们自己的动作（弹菜单），不是浏览器选字。
   *
   * 实测 2026-10-17：iOS Safari 上长按列表行，在弹出我们的菜单的同时也会起它自己的长按
   * 呼出（选字放大镜 + Copy/Lookup 菜单）——两个叠在一起。三条各管一层：
   *   • `user-select: none`（含 -webkit- 前缀）——根本不开始选字；
   *   • `-webkit-touch-callout: none`——iOS 专有的那一项，禁掉长按呼出图片/链接预览那套；
   *   • `contextmenu` 的 preventDefault 在 app.js 里（桌面右键）。
   * 只打在列表行上：转写正文里的选字复制是读者要的功能，不动它。
   */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

/*
 * Apple's own affordance for "this opens something": a chevron with nothing else.
 * Drawn from two borders rather than a '›' character, whose size and weight are the
 * system font's business — the drawn one looks the same on every phone.
 */
.list-item::after {
  content: '';
  position: absolute;
  right: 16px;
  top: 50%;
  width: 8px;
  height: 8px;
  border-top: 1.8px solid var(--text-dim);
  border-right: 1.8px solid var(--text-dim);
  border-top-right-radius: 1.5px;
  transform: translateY(-50%) rotate(45deg);
  pointer-events: none;
}

.list-item:active { background: var(--bg-raised); }

.list-top { display: flex; align-items: center; gap: 8px; margin-bottom: 2px; }
.list-title {
  flex: 1;
  /* System styles again, so the list follows the reader's text size: a session title is
     a `subheadline`, its metadata a `caption1`. */
  font: 15px/1.4 -apple-system, "PingFang SC", sans-serif;
  font: -apple-system-subheadline;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.list-meta {
  display: block;
  /* One line, truncated as a whole — see the note in `renderList`. */
  font: 12px/1.4 -apple-system, "PingFang SC", sans-serif;
  font: -apple-system-caption1;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.badge {
  flex: none;
  font: 12px/1.4 -apple-system, "PingFang SC", sans-serif;
  font: -apple-system-caption1;
  padding: 1px 7px;
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--text-dim);
}

.badge.running {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: var(--ok);
  border-color: color-mix(in srgb, var(--ok) 40%, transparent);
}

/* A live dot next to the word: motion says "in flight" where a static pill only says a state. */
.badge.running::before {
  content: '';
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--ok);
  animation: glow 1.6s ease-in-out infinite;
}

/* ── composer: fixed, bottom: 0, no keyboard math ───────────────────────── */

/*
 * The keyboard covers the home indicator, but iOS keeps reporting the bottom
 * safe-area inset while it is open. Measured in a standalone PWA, that left the
 * composer floating ~34 px — about one line of text — above the keyboard. The
 * class is set by `trackKeyboard()` in app.js from the visual-viewport shrink,
 * so no page code ever computes a keyboard height.
 */
html.kb-open {
  --safe-bottom: 0px;
}

#composer-seat {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  /*
    首次键盘异常时把座位抬回可见区（见 `--composer-lift` 的定义）。二次聚焦与正常状态下
    这个变量是 0，transform 不产生任何位移；`transition` 与 padding-bottom 同速，免得
    座位和内边距一个先动一个后动。
  */
  transform: translateY(calc(-1 * var(--composer-lift, 0px)));
  /* 下内边距 8px → 2px（2026-10-14，operator 看完分析后：「去掉我看看效果」——收键盘后
     控件行下方的留白本来就是这几层 padding 叠的；Safari 底栏与 home indicator 都在
     可视视口之外，贴底不碍事）。
     clearance 的收敛交给视口驱动（app.js）：随视口一起动，不向聚焦瞬间跳水。 */
  padding: 8px 10px calc(var(--composer-clearance) + 2px);
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter: blur(12px);
  /* 顶部横线去掉（operator 2026-10-14：「输入框上面的一条横线能不能去掉」）。 */
  border-top: 0;
  transition: padding-bottom .18s ease, transform .18s ease;
}

#composer-seat[hidden] { display: none; }

/* ── pending image attachments ───────────────────────────────────────────── */

#attachments {
  display: flex;
  gap: 8px;
  padding: 0 2px 8px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

#attachments[hidden] { display: none; }

.attachment {
  position: relative;
  flex: none;
  width: 64px;
  height: 64px;
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  background: var(--bg-input);
}

.attachment img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.attachment-remove {
  position: absolute;
  top: 2px;
  right: 2px;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  color: #fff;
  background: rgba(0, 0, 0, .62);
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.attachment-remove svg.i { width: 11px; height: 11px; stroke-width: 2.4; }

/*
  The composer, the iOS way.

  A system composer has no outlines: the field is a *filled pill*, the small buttons beside it
  are filled circles, and the only state change is colour — the send arrow is grey until there
  is something to send, then accent. Everything here is derived from that: fills instead of
  borders, `caret-color` and `::placeholder` from the same palette, and 0.15 s transitions so
  the controls answer the finger the way UIKit controls do.
*/
#attach {
  flex: none;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  color: var(--text);
  background: var(--fill);
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  touch-action: manipulation;
  transition: background .15s ease, transform .12s ease;
  -webkit-tap-highlight-color: transparent;
}

#attach svg.i { width: 16px; height: 16px; }
#attach:active { background: var(--fill-strong); transform: scale(.94); }

/* An image message renders as its marker text; keep it visually distinct. */
.image-note {
  margin: 6px 0 0;
  padding: 8px 10px;
  font-size: 14px;
  color: var(--text-dim);
  background: var(--bg-raised);
  border: 1px dashed var(--border);
  border-radius: 8px;
}

/*
  One field, the way the harness's own composer is built.

  The textarea and its controls share **one** rounded container: text on top, a row of chips and
  round buttons underneath, send at the end of that row. The container owns the shape and the
  fill, so the textarea itself is transparent and borderless — and focus is shown on the
  container (`:focus-within`), which is what a native control does.
*/
#composer {
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* 下内边距 6px → 4px → 2px（收起时高度再矮一点，2026-10-14）：与座位那 2px 一起，
     收键盘后控件行下方只剩 ~6px（见座位规则）。 */
  padding: 8px 8px 2px;
  background: var(--fill);
  border: 1px solid transparent;
  border-radius: 20px;
  /* iPhone 屏幕那种连续圆角（squircle，不是普通圆弧）；不支持的浏览器静默回退到
     border-radius（MDN: corner-shape, Safari 18.4+）。 */
  corner-shape: squircle;
  /* 收起↔展开的连续形变（2026-10-14，operator：「能否做从收起到放大的 有个连续过程
     的感觉」）：圆角/内边距/宽度/输入高度全部随 kb-open 渐变，而非硬切。 */
  transition: border-color .15s ease, background .15s ease, border-radius .18s ease,
    padding .18s ease, max-width .18s ease;
}

/*
 * 收起时（键盘不在场）：输入区收成单行胶囊（2026-10-14，operator 贴 ChatGPT 截图）。
 * `display: contents` 把按钮行摊平：+ 在左、单行输入居中、发送在右；命令/模型/权限
 * 三个小图标收起来不占地方。聚焦（kb-open）时恢复成原来的两行卡片。
 * 上下 padding 对称（4/4）——align-items:center 是对内容盒居中，不对称 padding 会让
 * 按钮看起来偏下（operator：没有垂直居中）。
 */
#composer {
  flex-direction: row;
  align-items: center;
  padding: 4px 8px 4px;
  border-radius: 999px;
}
#composer #composer-tools { display: contents; }
#composer #attach { order: 0; }
#composer > #input { order: 1; flex: 1 1 auto; min-width: 0; }
#composer-end { order: 2; margin-left: 0; }
/* 三个按钮（+ / 发送 / 停止）统一成一类形状（operator 2026-10-14：「这三个按钮的样式
   不是应该同一类的吗」）：和 #send/#stop 同规格——32px 圆、图标 17px、dim 底。 */
#composer #attach {
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--fill);
  color: var(--text-dim);
  display: grid;
  place-items: center;
}
#composer #attach svg.i { width: 17px; height: 17px; }
#composer-tools > #commands,
#composer-tools > #models,
#composer-tools > #access { display: none; }
/* 收起时再窄一点（operator 2026-10-14）：340px 居中，只在单行胶囊态；展开打字时回满宽。 */
#composer { max-width: 340px; margin-inline: auto; }
#composer > #input {
  height: 24px;
  /* 基础 #input 有 min-height: 46px 且在文件后面（同特异性后写赢）——必须先压掉它，
     否则高度永远是 46，胶囊就矮不下来（operator：「高度并没有减少掉」）。
     选择器特意用 #composer > #input（两个 id，压过基础 #input 与其后的恢复规则）。
     垂直 padding 也必须归零：24 高里再留 6+2，23px 行高就被裁掉/偏下（operator：站位
     文字高度不对、没有居中）。 */
  min-height: 0;
  padding: 0 10px;
  white-space: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  /* 高度也参与形变：收起 24px ↔ 展开 autoGrow 的计算值，连续长开。
     padding 也一起过渡：聚焦时 collapsed→expanded 的 padding 会跳变（0 10px → 6px 10px 2px），
     不过渡的话文字在盒子里先顿一下再长（2026-10-16 operator：首次聚焦时输入框的动画奇怪）。 */
  transition: height .18s ease, padding .18s ease;
}
html.kb-open #composer > #input {
  /* 展开时换行必须回来：collapsed 的 nowrap 只该属于收起态（operator：换行怎么没有了）。
     overflow-y 也一并还给 auto：否则展开的长文本会被裁掉而不是可滚动。
     垂直 padding 还给基础值 6px 10px 2px。 */
  white-space: normal;
  overflow-y: auto;
  padding: 6px 10px 2px;
  /* 两行起步：空框也占两行高（2 × 1.45 行高 + 6px/2px 垂直 padding），第一个字不会再让
     输入框长高（operator 2026-10-16）。**min-height 不参与过渡**——autoGrow 拿它当底线，
     任何时候读都是终值；早期在 JS 里用 2×lineHeight+padding 现算，而 padding 自己在过渡中
     （聚焦瞬间还是 0），底线会从 46.4 漂到 54.4，输入框正好跳 8px。改 padding 时同步这里。 */
  min-height: calc(2 * 1.45em + 8px);
}
html.kb-open #composer {
  flex-direction: column;
  align-items: stretch;
  /* 形变终态的宽度也要给数值（max-width:none 不能插值），才能从 340px 连续放开到满宽。 */
  max-width: 100vw;
  /* 不要在这里写 margin-inline: 0——auto→0 不可插值，t=0 瞬移，而 max-width 的放大要 180ms，
     于是盒子先向左跳到屏幕边再横向长大（operator 2026-10-16：「感觉先靠左，再放大的」）。
     留着基线的 margin-inline: auto：放大过程中盒子始终居中，从中间对称展开；终态两者等价
     （max-width 撑满容器时 auto 边距解析为 0）。 */
  padding: 8px 8px 2px;
  border-radius: 20px;
}
html.kb-open #composer #composer-tools { display: flex; }
html.kb-open #composer #attach,
html.kb-open #composer > #input,
html.kb-open #composer-end { order: initial; }
html.kb-open #composer-tools > #commands,
html.kb-open #composer-tools > #models,
html.kb-open #composer-tools > #access { display: block; }

#composer:focus-within {
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  background: var(--fill-strong);
}

#composer-tools {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 2px 2px;
}

#composer-end { margin-left: auto; display: flex; align-items: center; gap: 6px; }

/* Model and permissions: quiet pills until touched, like the desktop composer's own controls. */
.chip {
  flex: none;
  height: 30px;
  max-width: 42vw;
  padding: 0 11px;
  display: inline-flex;
  align-items: center;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font: inherit;
  font-size: 13px;
  color: var(--text-dim);
  background: transparent;
  border: 0;
  border-radius: 15px;
  cursor: pointer;
  touch-action: manipulation;
  transition: background .15s ease, color .15s ease;
  -webkit-tap-highlight-color: transparent;
}

.chip:active { background: var(--fill-strong); color: var(--text); }

/* A wide-open sandbox is worth a colour: it is the one fact that changes what a run may do. */
.chip.warn { color: var(--danger); }

/*
  Icon chips (2026-10-13, operator): 命令 / 模型 / 权限 carry no words — the spelled-out
  values (`k3 · max`, `工作区可写 · 需审批`) stretched this row past the message field on a
  phone, and the value is one tap away in each chip's sheet. The glyphs are the same 30px
  circles as the attach button, so the whole tools row is one line of icons; the live value
  rides in aria-label, where a screen reader still gets it. The lock keeps `.warn`'s red.
*/
.chip.icon-chip {
  width: 30px;
  max-width: none;
  padding: 0;
  justify-content: center;
  border-radius: 50%;
}
.chip.icon-chip svg.i { width: 16px; height: 16px; }

#access-sheet { display: flex; flex-direction: column; gap: 10px; padding: 4px 2px; }
#access-sheet .access-row { display: flex; justify-content: space-between; gap: 12px; font-size: 15px; }
#access-sheet .access-row span:first-child { color: var(--text-dim); }
#access-sheet .access-note { font-size: 13px; color: var(--text-dim); line-height: 1.5; }

/* The switchable presets: full-width rows, the current one ticked. */
#access-presets { display: flex; flex-direction: column; gap: 6px; }
#access-presets .access-preset {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 44px;
  padding: 0 14px;
  font: inherit;
  font-size: 15px;
  text-align: left;
  color: var(--text);
  background: var(--fill);
  border: 0;
  border-radius: 12px;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
#access-presets .access-preset:active { background: var(--fill-strong); }
#access-presets .access-preset.current { color: var(--accent); }
#access-presets .access-preset.current::after { content: '✓'; }

#input {
  flex: 1;
  /* Two lines by default — the field is a place to write, not a single-line box. */
  min-height: 46px;
  max-height: 40vh;
  padding: 6px 10px 2px;
  /*
   * 16px is the floor that stops iOS from zooming a focused field, and that zoom
   * is what pushes the send button off screen. `font-size` is its own longhand on
   * purpose: inside the `font` shorthand it depends on the shorthand's ordering
   * rules, and a declaration dropped for that reason would silently fall back to
   * a size that zooms.
   */
  font-family: inherit;
  font-size: 16px;
  line-height: 1.45;
  color: var(--text);
  /* The container carries the fill and the shape; the field is a hole in it. */
  background: transparent;
  border: 0;
  border-radius: 0;
  caret-color: var(--accent);
  resize: none;
  overflow-y: auto;
  outline: none;
  touch-action: manipulation;
  /* iOS paints its own inner shadow on a focused textarea. */
  -webkit-appearance: none;
  appearance: none;
}

#input::placeholder { color: var(--placeholder); }

/*
  The send arrow: 34px circle, accent when there is something to send, a plain system fill when
  there is not — the single most recognisable trait of a native composer, and the reason
  `syncSend()` exists in the client. `#stop` keeps the same shape in red.
*/
#send, #stop {
  flex: none;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  color: #fff;
  background: var(--accent);
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  touch-action: manipulation;
  transition: background .15s ease, color .15s ease, transform .12s ease;
  -webkit-tap-highlight-color: transparent;
}

#send svg.i, #stop svg.i { width: 17px; height: 17px; }

#send:active, #stop:active { transform: scale(.94); }

/* Nothing to send: the system's own "inactive control" look, not a dimmed accent. */
#send.idle,
#send:disabled {
  background: var(--fill);
  color: var(--text-dim);
}
#stop { background: var(--danger); }
#send[hidden], #stop[hidden] { display: none; }

#hint {
  padding: 4px 2px 0;
  font-size: 12px;
  color: var(--text-dim);
  min-height: 0;
}

#hint:empty { display: none; }
#hint.err { color: var(--danger); }

/* ── model sheet ────────────────────────────────────────────────────────── */

#sheet {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: flex-end;
  background: rgba(0, 0, 0, .5);
  /* The backdrop fades in; the body slides. Both restart on open because `hidden` drops
     the boxes — the animation below is attached to the children, not played by JS. */
  animation: sheet-dim .2s ease;
}

@keyframes sheet-dim { from { opacity: 0; } }

#sheet[hidden] { display: none; }

#sheet-body {
  width: 100%;
  max-height: 88vh;
  display: flex;
  flex-direction: column;
  background: var(--bg-raised);
  border-top-left-radius: 16px;
  border-top-right-radius: 16px;
  padding-bottom: var(--safe-bottom);
  /* A soft spring: mostly covered distance, then settle. No overshoot — a sheet that
     bounces past its resting place reads as a bug on a phone. */
  animation: sheet-up .26s cubic-bezier(.3, .9, .35, 1);
}

@keyframes sheet-up { from { transform: translateY(28px); opacity: .6; } }

/*
 * The long-press row menu — a right-click popover at the finger, not a sheet.
 *
 * Positioned by JS at the press point (clamped into the viewport), so the card is
 * `position: fixed` with its own coordinates. The pop is the Apple half of the feel:
 * a slight overshoot then settle, which is what a context menu on iOS does. The
 * haptic half is navigator.vibrate (Android) plus a 12 ms tick (iPhone, which gives
 * web pages no vibration permission at all) — see hapticTick in app.js.
 */
#context-backdrop {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: transparent;
}
#context-menu {
  position: fixed;
  z-index: 61;
  min-width: 168px;
  max-width: calc(100vw - 24px);
  padding: 4px;
  border-radius: 14px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  box-shadow: 0 10px 34px rgba(0, 0, 0, .42);
  /* The spring overshoot is the point: a menu that just fades in reads as a tooltip,
     one that settles reads as iOS. */
  animation: context-pop .22s cubic-bezier(.34, 1.5, .64, 1);
}
@keyframes context-pop {
  from { transform: scale(.86); opacity: 0; }
}
#context-menu .context-title {
  display: block;
  padding: 7px 12px 5px;
  font-size: 12px;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#context-menu .context-item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  border: none;
  background: none;
  border-radius: 10px;
  padding: 10px 12px;
  font: inherit;
  font-size: 15px;
  color: var(--text);
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
#context-menu .context-item:active {
  background: color-mix(in srgb, var(--text) 10%, transparent);
}
#context-menu .context-item.context-danger {
  color: var(--danger, #ff6b6b);
}
#context-menu .context-item svg {
  flex: none;
  width: 17px;
  height: 17px;
}

/*
 * The grabber, drawn as the body's first flex item. Every sheet gets the same one without
 * a line of JS, which is the point of drawing it here: ten openers, one chrome.
 */
#sheet-body::before {
  content: '';
  flex: none;
  width: 36px;
  height: 5px;
  margin: 6px auto 0;
  border-radius: 999px;
  background: color-mix(in srgb, var(--text) 24%, transparent);
}

.sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px 12px;
  font-weight: 600;
  border-bottom: 1px solid var(--border);
}

#sheet-content { overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 8px 0 16px; }

.group-title {
  padding: 12px 14px 6px;
  font-size: 12px;
  /*
    The one place tracking and capitals are allowed: these are *provider names* from the
    model catalog (`DeepSeek`, `CommandCode`, `Anthropic`) — Latin brand words in a section
    header, which is the usual iOS list-header treatment. Chinese never gets either (clreq
    §1.2): `mobile-typography-check.mjs` whitelists exactly this selector.
  */
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--text-dim);
}

.opt {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: var(--tap);
  padding: 10px 14px;
  background: transparent;
  border: 0;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.opt:active { background: var(--bg-input); }
.opt .check { margin-left: auto; color: var(--accent); font-size: 16px; }
.opt .sub { font-size: 12px; color: var(--text-dim); }

.efforts { display: flex; flex-wrap: wrap; gap: 8px; padding: 10px 14px 16px; }

/* ── settings sheet ─────────────────────────────────────────────────────── */

.settings-group { display: flex; flex-direction: column; }
.settings-group .opt { justify-content: space-between; }
#session-presets .opt { justify-content: space-between; }

/* ── workspace files ────────────────────────────────────────────────────── */

.file-path { padding: 6px 14px 10px; font-size: 12px; color: var(--text-dim); word-break: break-all; }

.file-row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: var(--tap);
  padding: 10px 14px;
  background: transparent;
  border: 0;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.file-row:active { background: var(--bg-input); }
.file-dir .file-name { font-weight: 600; }
.file-up { color: var(--accent); }
.file-name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.file-size { flex: none; font-size: 12px; color: var(--text-dim); }

/* ── workspace picker rows ───────────────────────────────────────────────── */
/* Two lines inside one .file-row: the workspace title on top, its path under. */

.ws-row { align-items: center; }
.ws-cell { flex: 1; min-width: 0; }
.ws-row .file-name { font-weight: 600; }
.ws-path {
  margin-top: 2px;
  font-size: 12px;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ws-meta { text-align: right; line-height: 1.5; }

/* The folder browser's action strip (上级 / 主目录 / 此电脑) and its footer button. */

.ws-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 4px 14px 10px;
}
.ws-choose {
  display: block;
  margin: 10px 14px 14px;
  padding: 10px 16px;
  border-radius: 10px;
  background: var(--accent);
  color: var(--bg);
  font-weight: 600;
}
.ws-choose:disabled { opacity: 0.6; }

#file-preview .file-text {
  margin: 0;
  padding: 10px 14px calc(var(--safe-bottom) + 16px);
  max-height: 62vh;
  overflow: auto;
  font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
  white-space: pre;
  color: var(--text);
}

/* ── images ─────────────────────────────────────────────────────────────── */

/*
  A chip, not a thumbnail: the bytes are not in the payload, so drawing a placeholder box
  that looks like an image would be a lie. This says what it is and what the tap costs.
*/
.image-block { display: flex; flex-direction: column; gap: 6px; margin: 6px 0 2px; /* 作为用户行里的 flex 项：宽度不得越出所在行（实测 393px 宽下超宽，文字在右缘被啃掉）。 */ max-width: 100%; min-width: 0; }

.image-chip {
  display: block;
  width: 100%;
  min-height: var(--tap);
  padding: 10px 12px;
  background: var(--bg-raised);
  border: 0;
  border-radius: 12px;
  color: var(--text);
  font: inherit;
  font-size: 14px;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  /* 一行元信息（看图片 · 尺寸 · 大小 · 文件名）：长了就省略号，不许被右缘无声裁掉。 */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.image-chip:active { background: var(--fill-strong); }

/*
 * 内联图片预览（桌面同款：读字节 → <img>；operator 2026-10-14：只显示基础信息看不到预览）。
 * 占位框用引用里的宽高比，字节没到版面不跳；太高的图按顶对齐裁到 340px（看全图点一下
 * 进查看器）。用户行里靠右（与气泡同侧）。
 */
.image-thumb {
  display: block;
  width: min(62vw, 240px);
  max-height: 340px;
  padding: 0;
  border: .5px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
  background: var(--bg-raised);
  cursor: zoom-in;
  -webkit-tap-highlight-color: transparent;
}
.image-thumb:active { opacity: .8; }
.image-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: top; }
.row-user .image-thumb { align-self: flex-end; }

#viewer {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: center;
  padding: calc(var(--safe-top) + 48px) 12px calc(var(--safe-bottom) + 12px);
  background: color-mix(in srgb, #000 88%, transparent);
  /* Same rule as the sheet: `hidden` drops the boxes, so the fade replays on every open. */
  animation: sheet-dim .18s ease;
}

#viewer[hidden] { display: none; }
#viewer-close { position: fixed; top: calc(var(--safe-top) + 8px); right: 10px; z-index: 61; color: #fff; }
#viewer-body { display: grid; place-items: center; max-width: 100%; max-height: 100%; }
#viewer-body .empty { color: #fff; }
.viewer-image { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 10px; }

/* ── paging ─────────────────────────────────────────────────────────────── */

/*
  One row at the very top of the transcript. Quiet, full width, and it says how much is left —
  a "load older" that never reports a bound reads like a bug when it keeps coming back.
*/
.older-button {
  display: block;
  width: calc(100% - 28px);
  margin: 10px 14px 6px;
  padding: 10px 12px;
  min-height: var(--tap);
  background: var(--bg-raised);
  border: 0;
  border-radius: 12px;
  color: var(--text-dim);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.older-button:active { background: var(--fill-strong); }

/* A converted document: the frame fills the viewer, and the link out sits under it. */
.viewer-frame {
  width: 100%;
  flex: 1;
  min-height: 0;
  border: 0;
  background: #ffffff;
  border-radius: 8px;
}

.viewer-open {
  flex: none;
  margin-top: 8px;
  padding: 10px 12px;
  min-height: var(--tap);
  background: var(--bg-raised);
  border-radius: 10px;
  color: var(--text);
  font-size: 14px;
  text-align: center;
  text-decoration: none;
}

/* A file chip has no thumbnail: name and size, in the same 44px-high strip. */
.attachment-file {
  display: flex;
  align-items: center;
  max-width: 60vw;
  padding: 0 8px;
  height: 100%;
  overflow: hidden;
  color: var(--text);
  font-size: 12px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The tool arguments on an approval card: a code-ish block, wrapped, never wider than the card. */
.question-args {
  margin: 4px 0 8px;
  padding: 8px 10px;
  max-height: 30vh;
  overflow: auto;
  background: var(--bg-input);
  border-radius: 8px;
  color: var(--text-dim);
  font-size: 12px;
  white-space: pre-wrap;
  word-break: break-word;
}

/* The list's own note line: sessions filed away out of this list. */
.list-note {
  padding: 8px 14px 4px;
  color: var(--text-dim);
  font-size: 12px;
}

/*
 * The archived note is a *door*, not just a line: it opens the archived sheet (where the
 * way back lives). Quiet on purpose — same size and dimness as the plain note, with the
 * accent only on the label, so the list stays scannable.
 */
.list-note-action {
  display: block;
  width: 100%;
  border: none;
  background: none;
  padding: 8px 14px 4px;
  color: var(--text-dim);
  font: inherit;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}
.list-note-action .list-note-label {
  color: var(--accent);
}

/*
 * Section headers (置顶 / 今天 / 昨天 / 本周 / 更早). Small and dim, and never tracked or
 * uppercased — those are Latin list-header treatments, and Han characters are 1:1 squares
 * (clreq §1.2); the typography check enforces exactly that.
 */
.list-group {
  padding: 14px 14px 5px;
  font-size: 12px;
  color: var(--text-dim);
}

/* ── `@` references ─────────────────────────────────────────────────────── */

/*
  Above the composer, capped in height: it appears while you type and must never push the input off
  a phone screen. Rows are 44px targets — this is a list you tap with a thumb, mid-sentence.
*/
#reference-list {
  max-height: 38vh;
  overflow-y: auto;
  margin: 0 10px 6px;
  background: var(--bg-raised);
  border-radius: 10px;
}
#reference-list[hidden] { display: none; }

.reference-head {
  padding: 6px 10px 2px;
  color: var(--text-dim);
  font-size: 12px;
}

.reference-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: var(--tap);
  padding: 8px 10px;
  background: transparent;
  border: 0;
  color: var(--text);
  font: inherit;
  font-size: 14px;
  text-align: left;
  cursor: pointer;
}

.reference-row:active { background: var(--fill-strong); }

.reference-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.reference-detail { flex: none; color: var(--text-dim); font-size: 12px; }

/* ── queue strip ────────────────────────────────────────────────────────── */

/*
  Above the composer, quiet: a queued message is a *waiting* state, not an error. The head is a
  button so "tap to refresh" is available without spending a request per second.
*/
#queue-strip { margin: 0 10px 6px; }
#queue-strip[hidden] { display: none; }
.queue-head {
  display: block;
  width: 100%;
  padding: 6px 2px;
  background: transparent;
  border: 0;
  color: var(--text-dim);
  font: inherit;
  font-size: 12px;
  text-align: left;
}
.queue-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  background: var(--bg-raised);
  border-radius: 10px;
  margin-bottom: 4px;
}
.queue-text {
  flex: 1;
  min-width: 0;
  font-size: 14px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--text);
}
.queue-act {
  flex: none;
  min-height: 32px;
  min-width: 32px;
  padding: 0 10px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-input);
  border: 0;
  border-radius: 8px;
  color: var(--text);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.queue-act svg.i { width: 14px; height: 14px; }
.queue-act:active { background: var(--fill-strong); }

/* ── goal bar ───────────────────────────────────────────────────────────── */

/*
  One line, tappable, above the composer: `⚑ objective` plus the phase and the round count.
  It is a *status* line, so it stays quiet — dim, one line, ellipsized — and only a blocked
  goal raises its voice (that is the state that needs a person).
*/
#goal-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 10px 6px;
  padding: 8px 12px;
  min-height: var(--tap);
  background: var(--bg-raised);
  border-radius: 12px;
  color: var(--text-dim);
  font-size: 13px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

#goal-bar[hidden] { display: none; }
#goal-bar:active { background: var(--fill-strong); }
#goal-bar .goal-flag { flex: none; color: var(--accent); }
#goal-bar .goal-text { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text); }
#goal-bar .goal-phase { flex: none; }
#goal-bar .goal-rounds { flex: none; font-size: 12px; }
#goal-bar.blocked { color: var(--danger); }
#goal-bar.blocked .goal-flag { color: var(--danger); }

#goal-sheet { padding: 4px 0 8px; }
#goal-sheet .goal-sheet-text { padding: 8px 14px; font-size: 15px; color: var(--text); }
#goal-actions { display: flex; flex-wrap: wrap; gap: 8px; padding: 8px 14px 4px; }

/* ── command palette ─────────────────────────────────────────────────────── */

/*
  Rows are a name and one line of explanation, and a command that takes arguments gets its
  field in the same row — so "what can I run" and "what does it want" are one glance, not
  two screens. The field is a real input, so the ≥16px rule applies to it (see the coarse
  pointer block), which is why its font size is not set here.
*/
.cmd-filter {
  display: block;
  width: calc(100% - 28px);
  margin: 4px 14px 8px;
  min-height: 40px;
  padding: 8px 12px;
  background: var(--bg-input);
  border: 0;
  border-radius: 10px;
  color: var(--text);
}

.cmd-row { border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent); }

.cmd-main {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 100%;
  min-height: var(--tap);
  padding: 10px 14px;
  background: transparent;
  border: 0;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.cmd-main:active { background: var(--bg-input); }
.cmd-name { font-size: 16px; font-weight: 600; }
.cmd-desc { font-size: 12px; color: var(--text-dim); }
.cmd-input { display: flex; gap: 8px; padding: 0 14px 12px; }

.cmd-arg {
  flex: 1;
  min-width: 0;
  min-height: 40px;
  padding: 8px 12px;
  background: var(--bg-input);
  border: 0;
  border-radius: 10px;
  color: var(--text);
}

.cmd-run {
  flex: none;
  min-height: 40px;
  padding: 8px 16px;
  background: var(--fill-strong);
  border: 0;
  border-radius: 10px;
  color: var(--text);
  font: inherit;
  cursor: pointer;
}

.cmd-run:active { background: var(--accent); color: var(--bg); }

/* The rename sheet: one field and its save button, in the same rhythm as a command row. */
.rename-box { display: flex; gap: 8px; padding: 4px 14px 16px; }

.rename-input {
  flex: 1;
  min-width: 0;
  min-height: 44px;
  padding: 10px 12px;
  background: var(--bg-input);
  border: 0;
  border-radius: 10px;
  color: var(--text);
}


.effort {
  min-height: 38px;
  padding: 6px 14px;
  font: 14px/1 inherit;
  color: var(--text);
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: 999px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.effort[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: #fff; }

.sheet-note { padding: 8px 14px; font-size: 12px; color: var(--text-dim); }

/* ── toast ──────────────────────────────────────────────────────────────── */

#toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--composer-h) + var(--composer-clearance) + 18px - var(--composer-lift));
  transform: translateX(-50%);
  z-index: 80;
  max-width: 88vw;
  padding: 9px 14px;
  font-size: 14px;
  color: var(--text);
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: 999px;
  box-shadow: 0 6px 24px rgba(0, 0, 0, .45);
  /* `hidden` drops the box, so this replays on every show. */
  animation: toast-in .18s ease;
}

@keyframes toast-in { from { opacity: 0; transform: translateX(-50%) translateY(8px); } }

#toast[hidden] { display: none; }

/* ── refresh affordance ─────────────────────────────────────────────────── */

#puller {
  position: fixed;
  top: calc(var(--safe-top) + 60px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 25;
  padding: 5px 12px;
  font-size: 12px;
  color: var(--text-dim);
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 999px;
  opacity: 0;
  transition: opacity .15s ease;
  pointer-events: none;
}

#puller.on { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
  /* The shimmer's text is transparent by construction; with motion off it reverts to plain dim. */
  .think-preview.streaming { background: none; -webkit-background-clip: initial; background-clip: initial; color: var(--text-dim); }
  .run-status .run-text { background: none; -webkit-background-clip: initial; background-clip: initial; color: var(--text-dim); }
}

/* ── 终端 ────────────────────────────────────────────────────────────────────
   A mirror of a real terminal on the host, so it keeps its own palette instead of
   following the page theme: a terminal that turns white in light mode is not a
   terminal. The grid is drawn as text runs; `\u001b`-derived colours come from the
   SGR subset the renderer understands (16 foreground colours + bold).
   Height is explicit (`78vh`) rather than `100%`: the sheet only caps at 88vh and
   `#sheet-content` scrolls, so a flex child with no definite height would push the
   input row out of the sheet.
   ------------------------------------------------------------------------- */
#terminal-view { display: flex; flex-direction: column; height: 78vh; }

/* 终端清单的一行：可点的整行 + 自己的 ■，两个按钮并排（不是嵌套）。 */
.term-row { display: flex; align-items: center; gap: 6px; padding-right: 12px; }
.term-row .file-row { flex: 1; min-width: 0; width: auto; }

.term-status { padding: 4px 14px 8px; font-size: 12px; color: var(--text-dim); }

.term-screen {
  flex: 1;
  min-height: 0;
  overflow: auto;
  margin: 0 12px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: #0b0f14;
  color: #d6dee8;
  font: 13px/1.3 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  white-space: pre;
  -webkit-overflow-scrolling: touch;
}

.term-line { min-height: 1.3em; }
.term-bold { font-weight: 700; }
/* The cursor cell: the host tells us where it is, and a terminal without one feels dead. */
.term-cursor { background: var(--accent); color: #0b0f14; }

.term-fg-0 { color: #4a5058; }
.term-fg-1 { color: #e05561; }
.term-fg-2 { color: #8cc265; }
.term-fg-3 { color: #d9a94a; }
.term-fg-4 { color: #4aa5f0; }
.term-fg-5 { color: #c162de; }
.term-fg-6 { color: #42b3ba; }
.term-fg-7 { color: #d6dee8; }
.term-fg-8 { color: #7d8590; }
.term-fg-9 { color: #ff616e; }
.term-fg-10 { color: #a5e844; }
.term-fg-11 { color: #ffd479; }
.term-fg-12 { color: #79b8ff; }
.term-fg-13 { color: #e2a8e8; }
.term-fg-14 { color: #66e0e0; }
.term-fg-15 { color: #ffffff; }

.term-keys { display: flex; gap: 8px; padding: 10px 12px 0; }

.term-key {
  min-height: 36px;
  padding: 0 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-raised);
  color: var(--text);
  font: 13px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

.term-key:active { background: var(--fill-strong); }
.term-back { margin-left: auto; font-family: inherit; }

.term-input-row { display: flex; gap: 8px; padding: 10px 12px 12px; }

.term-input {
  flex: 1;
  min-width: 0;
  height: var(--tap);
  padding: 0 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-input);
  color: var(--text);
  /* 16px on purpose: anything smaller makes iOS zoom the whole page on focus. */
  font: 16px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

.term-send {
  min-width: 64px;
  height: var(--tap);
  border: none;
  border-radius: 10px;
  background: var(--accent);
  color: #fff;
  font-size: 15px;
}

.term-input:disabled,
.term-send:disabled,
.term-key:disabled { opacity: .45; }

/* ── 提醒开关（Web Push） ───────────────────────────────────────────────────
   铃铛沿用 .icon-btn 的尺寸与点击反馈，只加三个状态：
   · .on          已经订阅 → 点亮点一个小圆点
   · .is-blocked  iOS 未装到主屏幕 → 变暗（点了会解释原因，不是禁用）
   ─────────────────────────────────────────────────────────────────────── */
.push-bell { position: relative; }

.push-bell.on { color: var(--accent); }

.push-bell.on::after {
  content: '';
  position: absolute;
  top: 6px;
  right: 6px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
}

.push-bell.is-blocked { color: var(--text-dim); }
