* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; height: 100%; font-family: -apple-system, "Segoe UI", "Noto Sans CJK TC", sans-serif; background: #1e1e26; color: #e4e4e7; }
#app { height: 100%; display: flex; flex-direction: column; }
header { display: flex; align-items: center; gap: 12px; padding: 8px 16px; background: #111118; border-bottom: 1px solid #2a2a35; }
header .brand { font-weight: 600; font-size: 19px; }
header #user-info { margin-left: auto; font-size: 16px; color: #a1a1aa; }
header button { background: #2a2a35; color: #e4e4e7; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; }
header button:hover { background: #3a3a45; }
.conn { font-size: 14px; padding: 2px 8px; border-radius: 10px; }
.conn.online { background: #064e3b; color: #6ee7b7; }
.conn.offline { background: #4b1d1d; color: #fca5a5; }

/* 2026-08-19 P151（Workboard #1248）·【死規則，已知且刻意保留】
   下面到 `#login-mode-switch a` 為止這 9 條的容器（`#login-panel` / `#login-mode-switch`）
   **在任何載入本檔的頁面都不存在** —— chatroom.html / lobby.html / tests/fixtures/*.html
   全數 0 個（`web/index.html` 的登入面板用的是 `class="login-panel" id="loginPanel"` 且配
   `css/style.css`，不是這裡）。⇒ 這 9 條永遠選不到元素。
   出處 `6e36850`（2026-04-17 從 ccms.git 拆分），拆分當下 chatroom.html 就已經沒有
   `#login-panel` 了 —— 拆分前的第一手歷史**沒找到**（該 commit 是 root、機器上無
   pre-split repo；掃描範圍見 docs/evidence/2026-08-19-p151-1248-step2-blame.md §6）。

   **為什麼留著不刪**：#1248 的範圍是「只動顏色 scope、不動版面／間距」，這 9 條是無人
   消費的死版面規則，刪它屬 dead-code 清理，需要自己的負面 control 與自己的單。
   **留著不等於沒人看著**：tests/e2e/p151-1248-err-color-scope.spec.js §D 把這 2 個容器
   明列在 `KNOWN_DEAD_CONTAINERS`（附理由），任何**新增**的死容器會讓該格轉紅。
   真正有活消費者受害的只有原 `.err` 那一條，已搬到本區塊下方（見該處註解）。 */
#login-panel { max-width: 320px; margin: 80px auto; padding: 24px; background: #111118; border: 1px solid #2a2a35; border-radius: 8px; display: flex; flex-direction: column; gap: 10px; }
#login-panel input { padding: 8px; background: #1e1e26; color: #e4e4e7; border: 1px solid #2a2a35; border-radius: 4px; }
#login-panel button { padding: 8px; background: #4f46e5; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
#login-panel .otp-row { display: flex; gap: 8px; }
#login-panel .otp-row input { flex: 1; }
#login-panel .otp-row button { flex-shrink: 0; font-size: 14px; padding: 8px 12px; white-space: nowrap; }
#login-panel .otp-row button:disabled { background: #3a3a45; cursor: not-allowed; }
#login-mode-switch { text-align: center; font-size: 14px; color: #a1a1aa; }
#login-mode-switch a { color: #818cf8; cursor: pointer; text-decoration: underline; }
/* 2026-08-19 P151（Workboard #1248）：錯誤訊息紅色 —— **不綁容器**。
   原本寫成 `#login-panel .err { color: #fca5a5; font-size: 14px; }`（`6e36850` 拆 repo 時
   從 ccms.git 帶進來，四個月零改動）。但 `#login-panel` 在**任何載入本 CSS 的頁面都不存在**
   —— chatroom.html / lobby.html / tests/fixtures/*.html 全數 0 個（上方 L13-21 那批同樣是
   孤兒，保留理由見該區塊上方註解）。⇒ 這條規則從來沒選到任何元素，`#cw-err` / `#ew-err`
   /「js/chatroom.js 注入到 #wm-rubric 的 <span class="err">」一直吃繼承來的灰。
   實測 computed color：#cw-err / #ew-err = rgb(228, 228, 231)（body #e4e4e7）、
   #wm-rubric .err = rgb(161, 161, 170)（#wm-rubric 自己的 #a1a1aa）。
   證據：docs/evidence/2026-08-19-p151-1248-{step2-blame,negative-control}.md。

   ⚠️ **只搬 color，故意不搬 `font-size: 14px`** —— 別「順手補回來」：
   lobby.html 也載本檔，且自帶 inline `.err { color:#fca5a5; margin-top:8px; min-height:1em }`
   （**沒有** font-size），實測字級 16px。把 font-size 一起放寬會讓 lobby 的錯誤字
   16px → 14px；#cw-err / #ew-err 同樣會 16px → 14px。那是版面變動，#1248 明確不做。
   守衛：tests/e2e/p151-1248-err-color-scope.spec.js 的 CTRL-B*-size（常駐，會轉紅）。

   ⚠️ 特異度刻意維持 (0,1,0)：lobby.html 的 inline `<style>` 同為 (0,1,0) 且在後，
   顏色由 lobby 那條勝出（同色，無視覺差）。拉高成 id/`!important` 會反過來奪走 lobby
   自己的宣告，屬於本單不該做的加法。 */
.err { color: #fca5a5; }
.hidden { display: none !important; }

#main { flex: 1; display: flex; min-height: 0; }
#sidebar { width: 280px; background: #111118; border-right: 1px solid #2a2a35; display: flex; flex-direction: column; }
.sidebar-head { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; border-bottom: 1px solid #2a2a35; font-weight: 600; }
.sidebar-head button { background: transparent; color: #a1a1aa; border: none; font-size: 19px; cursor: pointer; }
#worker-list { list-style: none; margin: 0; padding: 0; overflow-y: auto; flex: 1; }
/* 2026-07-31 P104 批1（Workboard #687）：position: relative 是 .row-actions 改用
   position: absolute 後的**定位包含塊**，不能拿掉（拿掉 ⋯ 會落到 initial containing
   block）。它同時會套到 .worker-section-header / .section-content 兩個 wrapper li，
   但無偏移量的 relative 不產生位移、也不建立 stacking context（未設 z-index），
   故不影響 2026-05-08 那組 wrapper 行為（見下方 .worker-section-header 與
   .section-content 兩處的 2026-05-08 警告註解）。 */
#worker-list li { padding: 10px 14px; border-bottom: 1px solid #1f1f29; cursor: pointer; display: flex; gap: 10px; align-items: flex-start; position: relative; }
#worker-list li:hover { background: #1a1a23; }
#worker-list li.active { background: #232336; }
/* 2026-08-11 P114 項5（Workboard #1009）：worker 列自 js/chatroom.js 的 buildWorkerLi()
   起帶 tabindex="0"（見該處註解），這條是它的焦點指示器。
   沿用同檔同元件 .section-toggle:focus-visible（L126 / L211 / L482）既有慣例，不另創寫法。
   用 :focus-visible 不用 :focus —— 只有鍵盤操作才畫框，滑鼠/觸控點列不畫（與 .section-toggle 一致）。
   ⚠️ 這條**只**負責畫框，不負責叫出 ⋯：⋯ 的顯示條件在 L350-352 那組，且**必須是 :focus-within
   不能是 :focus-visible**（實測理由見該處）。兩件事故意分開寫。
   outline 不進版面流，不影響 AC-1 / AC-3 的列高與列間距。 */
#worker-list li:focus-visible {
  outline: 2px solid #6366f1;
  outline-offset: -2px;
}
#worker-list .avatar { width: 36px; height: 36px; border-radius: 50%; background: #2a2a35; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 17px; color: #a1a1aa; overflow: hidden; }
#worker-list .avatar img { width: 100%; height: 100%; object-fit: cover; }
#worker-list li .meta { flex: 1; min-width: 0; }
/* 2026-07-31 P104 批1：flex-wrap: nowrap + min-width: 0 是「列高不因名字長度跳動」的
   前提 —— nowrap 擋住 .name 這一行 flex item 被換行推成兩行，min-width: 0 讓 .name
   自身可被壓縮（否則 flex item 的自動最小尺寸 = min-content，長名會把容器撐爆）。 */
#worker-list .name { font-size: 16px; font-weight: 600; display: flex; gap: 6px; align-items: center; flex-wrap: nowrap; min-width: 0; }
/* 2026-07-31 P104 批1：.name-text 是**契約名不是樣式名** —— 它同時是
   tests/e2e/long-name-layout.spec.js（AC-8）用來證明「css/chatroom.css 與
   js/chatroom.js 同批部署」的唯一機械證據。改名要同步改 js/chatroom.js:987
   的 buildWorkerLi() template、三個 fixture 與該 spec。
   截斷四件組（nowrap + hidden + ellipsis + min-width:0）缺一，長名就會改以
   「換行」而非「省略號」呈現 → 列高跳動 bug 復發。 */
#worker-list .name-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
#worker-list .preview { font-size: 13px; color: #a1a1aa; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* 2026-07-31 P104 批1：flex-shrink: 0 —— badge / state-dot / viewers-mini 三者是
   .name 這行的固定寬元素，不加就會被長名壓扁（改前實測 state-dot 被壓到 1.0～2.1px、
   badge 被裁）。三者同批加，缺一則該元素成為新的失效出口。 */
#worker-list .badge { background: #dc2626; color: #fff; border-radius: 10px; padding: 1px 7px; font-size: 12px; font-weight: 600; margin-left: auto; flex-shrink: 0; }
#worker-list .warlord { font-size: 12px; color: #fbbf24; }
#worker-list .state-dot { width: 6px; height: 6px; border-radius: 50%; background: #6ee7b7; display: inline-block; flex-shrink: 0; }
#worker-list .state-dot.locked { background: #fbbf24; }
/* 2026-07-31 P104 批1：桌機才替 position: absolute 的 .row-actions 預留 40px。
   判準是**裝置能力**不是 viewport 寬度 —— 打開 ⋯ 的條件是 `#worker-list li:hover`
   / `li.menu-open`（見 `#worker-list li:hover .row-actions` 那條規則），與視窗寬度無關；
   用 max-width 斷點會讓「桌機把視窗縮到 ≤768px」時預留消失而 hover 仍叫得出 ⋯
   → 疊回 badge（診斷書 L162 實測遮蔽 21px ＝ 100%）。
   ❌ 2026-07-31 #729 更正：此處原寫「無 hover 的觸控裝置上 ⋯ 本來就不顯示，預留是純
   損失」——**這句是錯的，已被實測證偽**。觸控瀏覽器 tap 會合成 sticky `:hover`，
   `#worker-list li:hover .row-actions` 照樣把 ⋯ 叫出來（375px/hasTouch/isMobile 實測
   tap 後 `display:block`）。真正的理由是：觸控分支下 ⋯ 是 **in-flow flex item、自己
   佔位**（見下方 #729 註記），不需要預留區，所以這 40px 在觸控上仍然只是純損失 ——
   結論不變，但依據換了。
   ⚠️ 這 40px 與 `#worker-list li .row-actions` 的 `right: 6px` 是硬相依：
   `right + 按鈕實測寬 < 40` 必須成立（本機 6 + 23 = 29 < 40）。改一邊必須同步檢查另一邊。
   ⚠️ 且與下方那組絕對定位宣告**必須共用同一個能力查詢條件**（#729 的根因就是兩者
   條件不對稱）。 */
@media (hover: hover) and (pointer: fine) {
  #worker-list li .meta { padding-right: 40px; }
}
/* 2026-05-08: villager 三段可摺疊 — sidebar section header.
   套到 <li class="worker-section-header"> 上；裡面是 <button class="section-toggle">。
   為什麼用 <button>：a11y（鍵盤可達 + screen reader 自動讀 expanded 狀態）。
   舊樣式（純文字 header）保留，但 cursor: default → pointer 並加 hover。
   2026-05-08 後續修：明確 display: block 覆寫上面 #worker-list li 的 display: flex /
   gap / border-bottom — 否則 wrapper li 變 flex container，inner ul 變 flex item
   寬度被收成 max-content，worker 行渲染窄，且 #worker-list li:hover 在 wrapper
   也會匹配，間接讓 .row-actions hover 規則失靈（desktop 看不到 ⋯ 按鈕）。 */
#worker-list .worker-section-header {
  list-style: none;
  padding: 0;
  display: block;
  border-bottom: none;
  cursor: default;
  gap: 0;
}
#worker-list .worker-section-header .section-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  background: transparent;
  border: none;
  color: #52525b;
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  padding: 10px 14px 4px;
  cursor: pointer;
  text-align: left;
}
#worker-list .worker-section-header .section-toggle:hover {
  color: #a1a1aa;
}
#worker-list .worker-section-header .section-toggle:focus-visible {
  outline: 2px solid #6366f1;
  outline-offset: -2px;
  border-radius: 4px;
}
#worker-list .worker-section-header .section-caret {
  display: inline-block;
  font-size: 10px;
  color: #71717a;
  transition: transform 0.15s ease;
}
#worker-list .worker-section-header .section-toggle[aria-expanded="true"] .section-caret {
  transform: rotate(90deg);
}
#worker-list .worker-section-header .section-emoji {
  font-size: 14px;
  font-style: normal;
  text-transform: none; /* 避免 OS fallback 把 emoji 當文字大寫處理 */
}
#worker-list .worker-section-header .section-label {
  /* 文字 — 跟 emoji 拆開避免 fallback 顯示成方框 */
}
#worker-list .worker-section-header .section-count {
  margin-left: auto;
  font-size: 11px;
  color: #71717a;
  font-weight: 500;
}

/* Section content — collapsed 用 max-height + overflow:hidden，避免 height auto 跳動。
   2026-05-08 後續修：強制 display: block + border-bottom: none + cursor: default
   覆寫繼承自 #worker-list li 的 flex / border / cursor，否則：
   * wrapper li 變 flex 容器 → inner ul 寬度收成 max-content
   * #worker-list li:hover 多匹配到 wrapper li，使 row-actions 的 hover 顯示
     規則語意被稀釋（desktop ⋯ 按鈕看不到）
   * inherit 來的 border-bottom 變成多餘的分隔線 */
#worker-list .section-content {
  list-style: none;
  padding: 0;
  margin: 0;
  overflow: hidden;
  transition: max-height 0.18s ease;
  max-height: 4000px; /* expanded 時夠大；用大數字而非 none，才能 transition */
  display: block;
  border-bottom: none;
  cursor: default;
  gap: 0;
}
#worker-list .section-content.collapsed {
  max-height: 0;
}
#worker-list .section-content-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: block;
  width: 100%;
}

/* 2026-06-16: 武將段第二層 cluster 子分組 header（[igaming-cocos] / [其他]）。
   復用 .section-toggle 內部結構，但縮排 + 字級略小，視覺上是「子標題」。
   collapse 機制復用 .section-content（sub-content 也帶該 class）。
   奇人/村民段不產生此 class — 仍是扁平。 */
#worker-list .worker-subsection-header {
  list-style: none;
  display: block;
  border-bottom: none;
  cursor: default;
}
#worker-list .worker-subsection-header .section-toggle {
  display: flex;
  align-items: center;
  width: 100%;
  gap: 6px;
  padding: 4px 10px 4px 24px; /* 左縮排 24px → 子標題層級感 */
  background: transparent;
  border: none;
  cursor: pointer;
  color: #a1a1aa;
  font: inherit;
  text-align: left;
}
#worker-list .worker-subsection-header .section-toggle:hover {
  background: #1c1c28;
}
#worker-list .worker-subsection-header .section-toggle:focus-visible {
  outline: 2px solid #6366f1;
  outline-offset: -2px;
}
#worker-list .worker-subsection-header .section-caret {
  font-size: 9px;
  transition: transform 0.15s ease;
  color: #71717a;
}
#worker-list .worker-subsection-header .section-toggle[aria-expanded="true"] .section-caret {
  transform: rotate(90deg);
}
#worker-list .worker-subsection-header .section-label {
  font-size: 12px;
  color: #c4b5fd;
  font-weight: 500;
}
#worker-list .worker-subsection-header .section-count {
  margin-left: auto;
  font-size: 10px;
  color: #71717a;
}
/* 子段內 worker row 再縮排一級，與母段 worker 區隔 */
#worker-list .worker-subsection-content .section-content-list > li {
  padding-left: 12px;
}

/* Per-row action menu (⋯ button → dropdown) */
.sidebar-head-btns { display: flex; gap: 4px; }
.sidebar-head-btns button { background: transparent; color: #a1a1aa; border: none; font-size: 19px; cursor: pointer; padding: 2px 6px; border-radius: 4px; }
.sidebar-head-btns button:hover { background: #232336; color: #e4e4e7; }

/* 2026-07-31 P104 批1（Workboard #687 item 3）：⋯ 鈕從「flex item + margin-left:auto」
   改成「絕對定位疊在 .meta 的 40px 預留區上」—— 它一旦離開 flex 流，hover 顯示／隱藏
   就不再改變 .name 可用寬，列高與名字寬度才不會跳動（AC-1 / AC-10 的嚴格 0）。
   ⚠️ 這句自 #729 起**只對桌機成立**，限縮條件見下方 2026-08-01 #738 註記。

   🔧 2026-07-31 #729 修正：**絕對定位那一組已搬進下方的 capability media query，
   本規則維持批 1 落地前的 in-flow 形態（margin-left:auto + align-self:center）。**
   原因：批 1 只把 `.meta{padding-right:40px}` 的預留收進能力查詢，卻把絕對定位留在
   無條件生效的基準規則裡，兩者不對稱。而「叫出 ⋯」的 `#worker-list li:hover` 在觸控
   瀏覽器上**照樣會觸發**（tap 合成 sticky hover，不是「無 hover 能力就永遠不觸發」）
   → 觸控裝置得到「絕對定位的 ⋯」但沒有那 40px 預留，直接疊在未讀 badge 上。
   實測（375px / hasTouch / isMobile，列 v-badge-viewers，tap 後）：
       改前 048fe19^ : overlap {w:0,  h:14, area:0}   badge 取樣 9 hits / 0 misses
       批1  542fe73  : overlap {w:15, h:14, area:210} badge 取樣 5 hits / 4 misses
   守衛：`long-name-layout.spec.js` 的 **AC-11b**（在觸控 context tap 後量交集面積）。
   ⚠️ 兩者是一組：**「絕對定位」與「40px 預留」必須在同一個 media query 條件下**。
   把任一邊搬出去（或改條件）就會重演本 bug —— 改一邊必須同步改另一邊。

   🔧 2026-08-01 #738 限縮（閘 C-2 #701 的 ⭐N-1）：上面第一段講的
   **「AC-1 / AC-10 的嚴格 0」自 #729 起只適用於具 hover 能力的裝置（＝桌機分支）**。
   觸控裝置走的是本規則的 in-flow 形態：tap 合成 sticky hover 把 ⋯ 叫出來時，它是
   **自己佔位的 flex item**，吃掉「按鈕 23px + `#worker-list li` 的 gap 10px」＝ 33px，
   `.meta` 可用寬同額變窄 → 名字寬會縮短。**這是刻意的，不是待修的 bug**：它換來的是
   #729 那個更嚴重的「⋯ 疊住未讀 badge」，而且與本批改動前的行為逐字相同。
   實測（375×800 / hasTouch / isMobile，列 v-cjk40，tap 前後 `.name` 可視寬）：
       改前 68b9648（批 1 落地前）: 301 → 268   Δ = -33   ← 基線
       現況 #729 之後             : 301 → 268   Δ = -33   ← 與基線逐值相同
       批1  542fe73（絕對定位無條件生效）: 301 → 301  Δ = 0（＝ #729 要修掉的形態）
   ⚠️ 不要「順手」讓觸控側也變成 Δ 0 —— 那等於把絕對定位放回無條件生效，直接復刻
   #729 的 badge 遮蔽（實測 overlap 210px²、badge 9 點取樣 4 misses）。
   列高不受影響：兩側 Δ liH 都是 0，#687 原始的「列高跳動」bug 沒有在觸控上復發。
   守衛：`long-name-layout.spec.js` 的 **AC-10t**（觸控 context tap 前後 `.name` 可視寬的
   變化量必須**等於改前基線**，基線值存在 `long-name-layout.baseline.json` 的
   `item15.nameBoxVisWAfterTap`，不寫死在 spec 裡）。 */
#worker-list li .row-actions {
  margin-left: auto;   /* in-flow 時把 ⋯ 推到列右端（＝批 1 前的行為，觸控走這條） */
  display: none;
  background: transparent;
  color: #a1a1aa;
  border: none;
  cursor: pointer;
  font-size: 22px;
  padding: 2px 6px;
  border-radius: 4px;
  align-self: center;  /* in-flow 時垂直置中；abs 分支下改由 top+transform 接手 */
}
/* 桌機（hover 可用）才切換成絕對定位 —— 與上方 `.meta{padding-right:40px}` **同一個
   能力查詢條件**，兩者必須同進同出（見上方 #729 註記）。
   ⚠️ 位置不能往上搬到 L69 那個 media query 區塊：media query 不加特異性，本組宣告
   必須排在 `#worker-list li .row-actions` 基準規則**之後**才覆寫得掉。
   五個宣告是一組，缺一即壞（都不是可有可無的裝飾）：
     * position: absolute           脫離 flex 流（本改動的本體）
     * right: 6px                   缺它 → 落到 flex static position。#worker-list li
                                    未設 justify-content（預設 flex-start）→ ⋯ 會跑到
                                    列**最左側疊在 .avatar 上**，而右側預留區整片空白；
                                    這個左右顛倒能讓 AC-2 / AC-4 / AC-10 / item 5(b) /
                                    item 16(a) 與視覺 QA 六道守衛全綠 —— 唯一抓得到的
                                    是 long-name-layout.spec.js 的 item 18（右緣距 li
                                    右緣 6±1px）。
     * top: 50% + translateY(-50%)  取代已失效的 align-self: center（#worker-list li 是
                                    align-items: flex-start；abs 元素不是 flex item，
                                    align-self 只能經 static position 間接作用，且指定
                                    top 後完全失效 → 明寫，不依賴瀏覽器實作細節）。
     * margin-left: 0               清掉舊的 auto。abs 元素的 auto margin 語意是「吃掉
                                    剩餘空間」，與原本「推到右端」完全不同，留著會讓
                                    right: 6px 的落點不可預期。
   ⚠️ right: 6px 與 `#worker-list li .meta` 的 padding-right: 40px（上方 capability
   media query 內）硬相依：
   `right + 按鈕實測寬 < 40` 必須成立（本機實測按鈕可見寬 23px → 6 + 23 = 29 < 40）。
   按鈕沒有 `font: inherit`，⋯ 的字寬吃 UA 預設 button 字型，跨環境約 23–34px，
   所以判準是這條不等式、不是寫死的算式。改任一邊必須同步檢查另一邊。 */
@media (hover: hover) and (pointer: fine) {
  #worker-list li .row-actions {
    margin-left: 0;
    position: absolute;
    right: 6px;
    top: 50%;
    transform: translateY(-50%);
  }
}
/* ⋯ 的顯示觸發 —— 這一組是**一個宣告、多種觸發**，不是三條各自獨立的規則。
   前兩種自 49402252（2026-04-15）起就成組：指標停在列上（:hover）／選單已開（.menu-open）。
   2026-08-11 P114 項5（Workboard #1009）補第三種：**鍵盤焦點在列內**（:focus-within）。
   ⚠️ 上方 L279 的 `display: none` 預設值**一個字都沒動**，本組只是新增覆寫條件。
      那個 `display:none` 在 2026-07-31 的診斷書被逐字裁決為刻意設計（`visibility:hidden`
      案被否決，理由是「元素仍在版面流中且觸控裝置上可見可點 → 那是新功能不是視覺修」），
      現由 AC-11 / AC-10t / AC-11b 三格硬鎖，不得改隱藏技術。

   ⚠️ 第三條**必須是 `:focus-within`，不能是 `:focus` 或 `:focus-visible`** —— 這不是風格
      偏好，是四選一的實測結果（fixture、Chromium 141、Tab 逐步記錄 activeElement）：
        `li:focus-within`  Tab→LI(block) → Tab→BUTTON.row-actions(block)  ✅ 唯一走得通
        `li:focus`         Tab→LI(block) → Tab→**BODY**(⋯ 消失)           ❌ 焦點陷阱
        `li:focus-visible` Tab→LI(block) → Tab→**BODY**(⋯ 消失)           ❌ 焦點陷阱
        `li:focus-visible` + `.row-actions:focus` 併寫                     ❌ 同上，救不回來
      成因：焦點一離開 li 落到按鈕上，`:focus`／`:focus-visible` 立刻為假 → 按鈕在同一次
      focus 移轉中變回 `display:none` → 瀏覽器把焦點退回 body。只有 `:focus-within`
      在「焦點在按鈕本身」時仍為真，按鈕才留得住。
      → 代價（已知並接受）：`:focus-within` 在滑鼠點列後也成立，故桌機點一列再把滑鼠移開，
        該列的 ⋯ 會留著（實測 none → block）。這是「按鈕留得住」的同一個機制的另一面，
        擋不掉；且 `.menu-open` 本來就已經是「沒有 hover 也顯示」的既有狀態。
        觸控側則**逐值不變**（實測 tap 後 block、tap 到列表外 none，與改前相同）。

   ⚠️ 第三條用 `>`（子選擇器）而不是後代選擇器，**這個字元是載重的**：`#worker-list li`
      同時匹配 worker 列與分組 wrapper li（.section-content / .worker-section-header），
      而 wrapper li 的後代**包含整段的 worker 列**。用後代選擇器時焦點落在任一列會讓整段
      所有列的 ⋯ 一起現身 —— with-sections fixture 實測：focus 一列，後代寫法 2 列的 ⋯
      現身、`>` 寫法只有 1 列。前兩條沿用歷史寫法（後代）不動，避免改到 :hover 既有行為。 */
#worker-list li:hover .row-actions,
#worker-list li.menu-open .row-actions,
#worker-list li:focus-within > .row-actions { display: inline-block; }
#worker-list li .row-actions:hover { background: #2a2a35; color: #e4e4e7; }

.row-menu {
  position: absolute;
  background: #1a1a23;
  border: 1px solid #353545;
  border-radius: 6px;
  padding: 4px 0;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5);
  z-index: 1500;
  min-width: 140px;
  font-size: 16px;
}
.row-menu button {
  display: block;
  width: 100%;
  text-align: left;
  background: transparent;
  color: #e4e4e7;
  border: none;
  padding: 8px 14px;
  cursor: pointer;
}
.row-menu button:hover { background: #232336; }
.row-menu button.danger { color: #fca5a5; }
.row-menu button.danger:hover { background: #3a1f1f; }

/* Create-worker modal */
.cw-reload-btn {
  background: transparent; border: none; color: #52525b; font-size: 18px;
  cursor: pointer; padding: 0; vertical-align: middle;
  margin-left: 6px; transition: color .15s;
}
.cw-reload-btn:hover { color: #a1a1aa; }
.cw-reload-btn:disabled { opacity: .35; cursor: default; }
.cw-filter {
  width: 100%;
  padding: 8px 10px;
  background: #1e1e26;
  color: #e4e4e7;
  border: 1px solid #2a2a35;
  border-radius: 4px;
  font-size: 16px;
  margin-bottom: 10px;
}
.cw-profile-list { list-style: none; margin: 0; padding: 0; max-height: 360px; overflow-y: auto; display: flex; flex-direction: column; gap: 4px; }
.cw-profile-list li {
  padding: 10px 12px;
  border-radius: 6px;
  background: #1a1a23;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.cw-profile-list li:hover { background: #232336; }
/* 2026-08-11 P114 #1008：單行截斷不可移除。
   長 display_name 原本會在 .cw-info（column flex）內換行，使武將列高隨字數浮動
   （實測 4 字 58px vs 40 字 79px，Δ21），違反 audit-08 §5 不變量 #15
   「列高嚴格相等、無容差」。三條宣告要一起在才有效：nowrap 擋換行、
   overflow:hidden 讓 ellipsis 生效、text-overflow 給省略號。
   依賴父層 .cw-info 的 min-width:0（見下方 .cw-info 那條）—— 少了它 flex 子項
   收縮不下來，截斷不會發生。 */
.cw-profile-list .cw-name {
  font-size: 16px; font-weight: 600; color: #e4e4e7;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cw-profile-list .cw-meta { font-size: 13px; color: #71717a; }
.cw-profile-list .cw-scope-shared { color: #6ee7b7; }
.cw-profile-list li.cw-disabled { opacity: 0.35; pointer-events: none; }
.cw-profile-list li.cw-loading { background: #2a2a3e; outline: 1px solid #4f46e5; }
.cw-spinner {
  margin-left: 8px;
  font-weight: 400;
  font-size: 14px;
  color: #a5b4fc;
  animation: cw-pulse 1.2s ease-in-out infinite;
}
@keyframes cw-pulse {
  0%, 100% { opacity: 0.6; }
  50% { opacity: 1; }
}
#create-worker-modal.busy { cursor: progress; }

/* --- Profile list: section headers (2026-05-08 villager + collapse) --- */
/* 高 specificity（用 .cw-profile-list li.cw-section-header）覆寫上面
   .cw-profile-list li 的 padding/background/cursor — 我們的 wrapper li
   不應該長得像可點擊的 profile row。 */
.cw-profile-list li.cw-section-header,
.cw-profile-list li.cw-section-content {
  background: transparent;
  cursor: default;
  display: block;
  padding: 0;
  border-radius: 0;
  flex-direction: initial;
  gap: 0;
}
.cw-profile-list li.cw-section-header:hover,
.cw-profile-list li.cw-section-content:hover {
  background: transparent;
}
.cw-section-header {
  list-style: none;
  padding: 0;
  border-bottom: 1px solid #27272a;
}
.cw-section-header .section-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  background: transparent;
  border: none;
  color: #71717a;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  padding: 8px 4px 4px;
  cursor: pointer;
  text-align: left;
}
.cw-section-header .section-toggle:hover {
  color: #a1a1aa;
}
.cw-section-header .section-toggle:focus-visible {
  outline: 2px solid #6366f1;
  outline-offset: -2px;
  border-radius: 4px;
}
.cw-section-header .section-caret {
  display: inline-block;
  font-size: 10px;
  transition: transform 0.15s ease;
}
.cw-section-header .section-toggle[aria-expanded="true"] .section-caret {
  transform: rotate(90deg);
}
.cw-section-header .section-emoji {
  font-size: 14px;
  font-style: normal;
  text-transform: none;
}
.cw-section-header .section-count {
  margin-left: auto;
  font-size: 11px;
  color: #71717a;
  font-weight: 500;
}

/* WARN: 外層 .cw-profile-list 是 flex column container（為了 gap）+ max-height: 360px。
   本元素含 overflow:hidden（collapse 動畫需要），會觸發 W3C Flexbox L1 §4.5 的
   min-height:auto=0 連鎖，讓 wrapper 被 squeeze 成 0 → 列表內容裁切、scrollbar
   不出現（使用者無法滾到 villager 段最後幾個 profile）。必須 flex-shrink: 0 鎖死。
   移除前先確認 .cw-profile-list 是否也改回 block。
   見 diagnoses/2026-05-08-cw-modal-no-scroll.md */
.cw-section-content {
  list-style: none;
  padding: 0;
  margin: 0;
  overflow: hidden;
  transition: max-height 0.18s ease;
  max-height: 4000px;
  flex-shrink: 0;
}
.cw-section-content.collapsed {
  max-height: 0;
}
.cw-section-content-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

/* 2026-06-16: 登錄 modal 武將段內 cluster||folder 子分組（縮排子標題）。
   摺疊機制復用 .cw-section-content（cw-subsection-content 同時帶這個 class）。
   子段 className 走 cw- 命名空間，與 sidebar 的 worker-subsection-* 不混。 */
.cw-profile-list li.cw-subsection-header,
.cw-profile-list li.cw-subsection-content {
  background: none;
  padding: 0;
}
.cw-subsection-header .section-toggle {
  padding-left: 22px; /* 子段縮排，視覺隸屬武將段 */
  opacity: 0.92;
}
.cw-subsection-header .section-label {
  font-size: 0.86em;
  color: #9aa0c0;
}
.cw-subsection-content {
  padding-left: 10px;
}

/* --- Profile list: two-button layout --- */
.cw-profile-list li {
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
}
.cw-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.cw-actions { display: flex; gap: 6px; flex-shrink: 0; }
.cw-btn {
  padding: 4px 10px;
  border: 1px solid #3f3f5a;
  border-radius: 4px;
  font-size: 14px;
  cursor: pointer;
  background: transparent;
  color: #a1a1aa;
  white-space: nowrap;
}
.cw-btn:hover { background: #2a2a3e; }
.cw-btn-enroll { border-color: #4f46e5; color: #a5b4fc; }
.cw-btn-enroll:hover { background: #4f46e5; color: #fff; }
.cw-btn-dismiss { border-color: #dc2626; color: #fca5a5; }
.cw-btn-dismiss:hover { background: #dc2626; color: #fff; }
.cw-btn-loadout { border-color: #0d9488; color: #5eead4; }
.cw-btn-loadout:hover:not(:disabled) { background: #0d9488; color: #fff; }
.cw-btn-loadout:disabled { border-color: #374151; color: #6b7280; cursor: not-allowed; opacity: .5; }

/* --- Loadout overview (inside profile list) --- */
.lo-overview { cursor: default !important; flex-direction: column !important; }
.lo-overview:hover { background: #1a1a23 !important; }
.loadout-overview-title { font-size: 17px; font-weight: 700; color: #e4e4e7; margin-bottom: 10px; }
.lo-section { margin-bottom: 10px; }
.lo-section-header { display: flex; justify-content: space-between; align-items: center; font-size: 16px; color: #a1a1aa; margin-bottom: 4px; }
.lo-items { display: flex; flex-direction: column; gap: 2px; padding-left: 4px; }
.lo-item { font-size: 14px; color: #d4d4d8; padding: 2px 0; }
.lo-item.global { color: #71717a; }
.lo-lock { font-size: 13px; margin-right: 2px; }
.lo-empty { font-size: 14px; color: #52525b; font-style: italic; }
.lo-footer { margin-top: 8px; text-align: right; }

/* --- Loadout edit modal --- */
.loadout-modal-box { max-width: 580px; width: 92vw; max-height: 88vh; display: flex; flex-direction: column; }
.loadout-modal-body { flex: 1; overflow-y: auto; margin: 10px 0; min-height: 0; }
.loadout-modal-footer { text-align: right; padding-top: 8px; border-top: 1px solid #27272a; }
.loadout-modal-footer .btn-primary {
  padding: 6px 20px;
  border: none;
  border-radius: 4px;
  background: #4f46e5;
  color: #fff;
  font-size: 16px;
  cursor: pointer;
}
.loadout-modal-footer .btn-primary:hover { background: #4338ca; }

.lo-edit-section { margin-bottom: 12px; }
.lo-edit-title { font-size: 14px; font-weight: 700; color: #71717a; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; border-bottom: 1px solid #27272a; padding-bottom: 4px; }
.lo-edit-item { display: flex; justify-content: space-between; align-items: center; padding: 5px 8px; border-radius: 4px; }
.lo-edit-item:hover { background: #232336; }
.lo-edit-item.global { opacity: 0.6; }
.lo-edit-info { display: flex; align-items: center; gap: 6px; min-width: 0; flex: 1; }
/* 2026-07-31 P104 批1（Workboard #687 item 6）：與 #worker-list .name-text 同一類問題的
   第二個出口（flex row 內的文字 + 未 flex-shrink:0 的 sibling）。
   .lo-edit-name 缺截斷四件組 → 長名換行 → .lo-edit-item 列高被內容長度決定（INV-2 紅）；
   .lo-edit-cat / .lo-edit-tag 缺 flex-shrink:0 → 被長名壓扁，「全域」tag 折成 27×38
   而非 34×21（INV-3 紅）。兩者必須同批加，只加 name 的 ellipsis 不夠。
   .lo-edit-name 的 title 屬性（js/chatroom.js 兩處 template）是截斷後看得到全名的
   唯一補救，與這裡的 ellipsis 成對，改一邊要想到另一邊。 */
.lo-edit-name { font-size: 16px; color: #e4e4e7; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.lo-edit-cat { font-size: 13px; color: #52525b; }
.lo-edit-tag { font-size: 12px; color: #6ee7b7; border: 1px solid #065f46; border-radius: 3px; padding: 1px 4px; }
.lo-edit-cat, .lo-edit-tag { flex-shrink: 0; }

.lo-cat-group { margin-bottom: 4px; }
.lo-cat-header { font-size: 14px; color: #a1a1aa; cursor: pointer; padding: 4px 6px; border-radius: 4px; user-select: none; }
.lo-cat-header:hover { background: #232336; }
.lo-cat-chevron { display: inline-block; transition: transform 0.15s; font-size: 13px; }
.lo-cat-group.collapsed .lo-cat-chevron { transform: rotate(-90deg); }
.lo-cat-group.collapsed .lo-cat-items { display: none; }
.lo-cat-count { color: #52525b; font-size: 13px; }
.lo-cat-items { padding-left: 8px; }

/* --- Loadout AI 設定 --- */
.lo-ai-config { background: #1a1a23; border: 1px solid #2a2a35; border-radius: 6px; padding: 10px 12px; margin-bottom: 12px; }
.lo-ai-title { font-size: 14px; font-weight: 700; color: #a5b4fc; margin-bottom: 8px; }
.lo-ai-row { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
.lo-ai-row label { width: 80px; font-size: 13px; color: #a1a1aa; }
.lo-ai-row select { flex: 1; padding: 6px 8px; background: #1e1e26; color: #e4e4e7; border: 1px solid #2a2a35; border-radius: 4px; font-size: 14px; }
.lo-ai-hint { font-size: 12px; color: #71717a; margin-top: 6px; font-style: italic; }

/* --- Loadout tab bar --- */
.lo-tabs {
  display: flex;
  gap: 0;
  border-bottom: 2px solid #27272a;
  margin-bottom: 10px;
}
.lo-tab {
  flex: 1;
  background: transparent;
  color: #71717a;
  border: none;
  padding: 8px 4px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
  transition: color 0.15s, border-color 0.15s;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
}
.lo-tab:hover { color: #a1a1aa; background: #1a1a23; }
.lo-tab.active {
  color: #e4e4e7;
  border-bottom-color: #4f46e5;
}
.lo-tab-count {
  font-size: 13px;
  color: #52525b;
  background: #1e1e26;
  border-radius: 8px;
  padding: 1px 6px;
  font-weight: 500;
}
.lo-tab.active .lo-tab-count {
  color: #a5b4fc;
  background: #2a2a4e;
}

/* 2026-09-08 (#1722, P169): loadout modal 的「顯示其他 cluster」開關。
   由 js/chatroom.js 的 _loadout.renderModal() 產出，位置在 tabs bar 與「已裝備」之間。
   input 明寫 width/height 且 flex 不伸縮：D2v (#1667 F2) 在 legacy modal 踩到
   「.loadout-search input 的 width:100% 把 checkbox 撐成一兩百 px、標籤折兩行」。
   chatroom.css 目前沒有那條規則（機械掃過本檔與 chatroom-light.css，命中 input 且設
   width 的只有 .cw-filter 與 #edit-worker-modal input，兩者都不會套到這個 checkbox），
   但幾何寫死是為了讓「以後有人加一條泛用 input 規則」時不會再破一次版。 */
.lo-cluster-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 10px;
  font-size: 14px;
  color: #71717a;
  cursor: pointer;
  user-select: none;
}
.lo-cluster-toggle input[type="checkbox"] {
  width: 16px;
  height: 16px;
  min-width: 16px;
  flex: 0 0 auto;
  margin: 0;
  cursor: pointer;
}
.lo-cluster-toggle span { white-space: nowrap; }

/* Edit-worker modal */
#edit-worker-modal .modal-box { min-width: 360px; }
.ew-label { display: block; font-size: 14px; color: #a1a1aa; margin: 10px 0 4px; }
#edit-worker-modal input {
  width: 100%;
  padding: 8px 10px;
  background: #1e1e26;
  color: #e4e4e7;
  border: 1px solid #2a2a35;
  border-radius: 4px;
  font-size: 16px;
}
.ew-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
.ew-actions button {
  padding: 8px 16px;
  background: #2a2a35;
  color: #e4e4e7;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
}
.ew-actions button.primary { background: #4f46e5; color: #fff; }
.ew-actions button:hover { filter: brightness(1.15); }

/* M8: viewers stack */
/* 2026-07-31 P104 批1：flex-shrink: 0 —— viewers-mini 是 .name 行的第三個固定寬元素
   （另兩個是 `#worker-list .badge` / `#worker-list .state-dot`）。
   註：同規則的 `gap: -2px` 是無效值（gap 不吃負數），實際疊圖靠下一行 .v-dot 的
   margin-left: -4px；本批**刻意不動它**（#687 明列禁止順手修，避免混入非本批的行為改動）。 */
#worker-list .viewers-mini { display: flex; align-items: center; gap: -2px; margin-left: 4px; flex-shrink: 0; }
#worker-list .viewers-mini .v-dot { width: 14px; height: 14px; border-radius: 50%; background: #4f46e5; color: #fff; font-size: 11px; font-weight: 700; display: flex; align-items: center; justify-content: center; border: 1.5px solid #111118; margin-left: -4px; }
#worker-list .viewers-mini .v-dot.overflow { background: #6b7280; }

.viewers { display: flex; align-items: center; gap: 6px; font-size: 14px; color: #a1a1aa; }
.viewers.hidden { display: none; }
.viewers .v-dot { width: 20px; height: 20px; border-radius: 50%; background: #4f46e5; color: #fff; font-size: 13px; font-weight: 700; display: flex; align-items: center; justify-content: center; border: 2px solid #111118; margin-left: -6px; }
.viewers .v-dot:first-child { margin-left: 0; }
.viewers .v-dot.overflow { background: #6b7280; }
.viewers .v-label { margin-left: 4px; }

#thread { flex: 1; display: flex; flex-direction: column; min-width: 0; position: relative; }
#thread-head {
  padding: 12px 16px;
  border-bottom: 1px solid #353545;
  font-weight: 600;
  display: flex;
  gap: 16px;
  align-items: center;
  background: #161620;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
  position: relative;
  z-index: 2;
}
/* P114 項1（2026-08-11，使用者裁定甲案「卡片優先」）：長標題可收縮並以省略號截斷。
   原本這裡是 `flex-shrink: 0`，出自 071f63b（2026-04-15 M6），當時的優先順序是
   「標題完整 > 武將卡」；實測代價是 800×600／31 字標題時 #warlord-card 可見寬度 -8px
   （整張被擠出可視區、100% 被遮）、#thread-head 溢出 220px。使用者裁定反轉為
   「武將卡可見 > 標題完整」，故改為可收縮 + 截斷。
   ⚠️ 只解除收縮限制，**不要**加 flex-grow / flex: 1 —— #warlord-card 靠 `margin-left: auto`
   靠右對齊，那需要 flex 容器有剩餘空間才成立；標題一旦會伸長吃掉剩餘空間，卡片就會
   左移黏在標題旁邊（手機版的 `flex: 1` 寫法不能照抄，它成立的前提是同一個 media query
   內 `#warlord-card { display: none !important }`）。
   短標題時內容寬 < 可用寬，收縮不生效，版面與改前逐像素一致。 */
#thread-title {
  min-width: 0;          /* 覆蓋 flex item 預設的 min-width: auto，否則縮不到內容寬以下 */
  white-space: nowrap;   /* 不換行，否則標題改可收縮後會折行把 header 撐高 */
  overflow: hidden;
  text-overflow: ellipsis;
}
/* #71: header 武將名超連結 → board 專案頁。繼承標題色（不吃瀏覽器預設藍），
   hover 才顯底線提示可點；不撐開 flex 版面。 */
#thread-title .thread-title-link { color: inherit; text-decoration: none; cursor: pointer; }
#thread-title .thread-title-link:hover { text-decoration: underline; }

/* P114 #1023（2026-08-11）：窄桌機視窗（769–1023px）head 改為可換行。
   ── 為什麼需要 ──
   上面那條（d582787）讓標題可收縮後，桌機 head 五個子元素裡**只剩標題會縮**：
   #folder-picker-btn 是 flex-shrink:0（.fp-btn），#warlord-card 的 min-content
   就是它的 212px（縮不動），#thread-ctx／#thread-viewers 在 800px 早已壓到 min。
   於是整份赤字由標題獨吞 —— 真登入頁 800×600 實測連 5 字標題都只剩 22px（內容需 60px）。
   ⚠️ 真登入頁才量得到：合成頁（route.fulfill 剝掉 <script>）head 子元素較少
   （headH 95），這條結構性測不出來，#1006 因此漏掉。
   ── 為什麼是換行，不是別的 ──
   本檔早有先例且意圖記載明確：9633808（2026-04-15）為手機版 #thread-head 加
   flex-wrap:wrap，body 逐字寫「避免內部元素撐開」。同一個問題類別沿用既有機制。
   實測（真登入頁 800×600，5 字標題／31 字標題）淘汰掉另外兩個方向：
     · #thread-title{min-width:6em}：標題救回 96px，但卡片被遮 25%、head 溢出 54px
       —— 直接推翻使用者已裁定的「卡片優先」；且 1280/1680 短標題 60→96px，
       把 #1006 驗過的寬版面幾何一起弄壞。
     · 窄寬度隱藏 #folder-picker-btn：釋出的 97px 被 #thread-viewers 收回去
       （它不再折行、從 46px 漲回 133px），標題只到 49px，**仍然被截**；
       而且少一個功能入口。
   ── 上下界怎麼定的（都不是拍腦袋）──
   下界 769px：接在 @media(max-width:768px) 手機版之上，不與它重疊。
   上界 1023px：5 字標題所需總寬 632px（60+81+72+133+212 + gap 64 + ctx margin 10），
   桌機可用寬 = 視窗寬 − 280(sidebar) − 1(border) − 32(padding)，
   兩者相等於 945px → ≥1024px 時短標題本來就不會被截，換行是多餘的。
   🚨 **不要把上界拿掉**：1280px／31 字標題現況是「標題截到 427px、卡片完整」，
   那是使用者裁定的卡片優先行為且 #1006 已驗；若讓 1280 也 wrap，標題會拿滿 512px
   並把卡片推到第二行 —— 那是把已驗過的寬版面改掉。
   ── 為什麼不需要再加別的宣告 ──
   不動 flex-shrink／不加 flex-grow／不加 flex:1（d582787 紅字警告照守），也不必抄
   手機版的 #thread-head>*{min-width:0}：實測換行後 head 溢出已是 0px。
   #warlord-card 的 margin-left:auto（071f63b M6 配套）在換行後仍讓卡片貼齊 head
   內容右緣，實測 cardRight 與 head 內容右緣距離 0px。 */
@media (min-width: 769px) and (max-width: 1023px) {
  #thread-head { flex-wrap: wrap; }
}

/* M6: Warlord card in thread header */
#warlord-card { display: flex; gap: 10px; align-items: center; margin-left: auto; padding: 6px 12px; background: #1a1a23; border: 1px solid #2a2a35; border-radius: 8px; cursor: pointer; transition: background 0.15s; }
#warlord-card:hover { background: #232336; }
#warlord-card .wl-meta { display: flex; flex-direction: column; gap: 2px; min-width: 120px; }
#warlord-card .wl-name { font-size: 14px; font-weight: 600; color: #e4e4e7; }
#warlord-card .wl-level { font-size: 12px; color: #fbbf24; font-weight: 500; }
#warlord-card .wl-might { font-size: 13px; color: #fca5a5; }
#warlord-card .wl-bar { width: 120px; height: 4px; background: #0b0b10; border-radius: 2px; overflow: hidden; }
#warlord-card .wl-bar > div { height: 100%; background: linear-gradient(90deg, #4f46e5, #f472b6); transition: width 0.4s ease; width: 0; }

/* Rarity border */
#warlord-card.rarity-C { border-color: #71717a; }
#warlord-card.rarity-B { border-color: #60a5fa; }
#warlord-card.rarity-A { border-color: #a78bfa; }
#warlord-card.rarity-S { border-color: #fbbf24; box-shadow: 0 0 8px rgba(251,191,36,0.3); }
#warlord-card.rarity-epic { border-color: #f472b6; box-shadow: 0 0 10px rgba(244,114,182,0.4); }
#warlord-card.rarity-legendary { border-color: #f59e0b; box-shadow: 0 0 14px rgba(245,158,11,0.5); }

/* Sidebar warlord mini avatar */
#worker-list .avatar.rarity-S, #worker-list .avatar.rarity-epic { box-shadow: 0 0 6px rgba(251,191,36,0.5); }
#worker-list .avatar.rarity-legendary { box-shadow: 0 0 8px rgba(245,158,11,0.6); }
#worker-list .wl-line { font-size: 12px; color: #fbbf24; }
#worker-list .wl-might { color: #fca5a5; }

/* Modal */
.modal { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 1000; }
.modal.hidden { display: none; }
.modal-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.6); }
.modal-box { position: relative; background: #18181f; border: 1px solid #2a2a35; border-radius: 10px; padding: 24px 28px; min-width: 320px; max-width: 520px; max-height: 90vh; overflow-y: auto; }
.modal-close { position: absolute; top: 8px; right: 12px; background: transparent; border: none; color: #a1a1aa; font-size: 29px; cursor: pointer; }
.modal-box h2 { margin: 0 0 12px; font-size: 22px; }

/* 2026-07-07 個人 folder 瀏覽器 modal（folder-picker.js 建 DOM，沿用 .modal/.modal-box）*/
/* 入口鈕（#thread-head 內，非輸入框那排）*/
.fp-btn {
  flex-shrink: 0;
  background: #1f2937; color: #cbd5e1; border: 1px solid #334155;
  border-radius: 6px; padding: 4px 10px; font-size: 13px; cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.fp-btn:hover:not(:disabled) { background: #273549; border-color: #475569; }
.fp-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.folder-picker-box { width: 460px; max-width: 92vw; display: flex; flex-direction: column; }
.fp-breadcrumb {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px; color: #93c5fd; word-break: break-all;
  background: #11131a; border: 1px solid #23252f; border-radius: 6px;
  padding: 6px 8px; margin-bottom: 8px;
}
.fp-toolbar { margin-bottom: 8px; }
.fp-up-btn {
  background: #1f2937; color: #cbd5e1; border: 1px solid #334155;
  border-radius: 6px; padding: 4px 12px; font-size: 13px; cursor: pointer;
}
.fp-up-btn:hover:not(:disabled) { background: #273549; }
.fp-up-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.fp-error {
  color: #fca5a5; background: #2a1416; border: 1px solid #5b2327;
  border-radius: 6px; padding: 6px 8px; margin-bottom: 8px; font-size: 13px;
}
.fp-list {
  list-style: none; margin: 0; padding: 0;
  max-height: 46vh; overflow-y: auto;
  border: 1px solid #23252f; border-radius: 6px; background: #14151b;
}
.fp-entry {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 10px; border-bottom: 1px solid #1e2029;
}
.fp-entry:last-child { border-bottom: none; }
.fp-entry .fp-check { flex-shrink: 0; width: 16px; height: 16px; cursor: pointer; }
.fp-name { font-size: 14px; color: #d4d4d8; word-break: break-all; }
.fp-name.fp-nav { color: #93c5fd; cursor: pointer; }
.fp-name.fp-nav:hover { text-decoration: underline; }
.fp-truncated { margin-top: 6px; font-size: 12px; color: #fbbf24; text-align: center; }
.fp-footer { margin-top: 12px; display: flex; justify-content: flex-end; }
.fp-paste-btn {
  background: linear-gradient(90deg, #4f46e5, #6366f1); color: #fff;
  border: none; border-radius: 6px; padding: 8px 18px; font-size: 14px;
  font-weight: 600; cursor: pointer;
}
.fp-paste-btn:disabled { opacity: 0.4; cursor: not-allowed; background: #3f3f52; }
.wm-row { display: flex; justify-content: space-between; font-size: 16px; padding: 4px 0; border-bottom: 1px solid #222; }
.wm-label { color: #a1a1aa; }
#wm-radar { display: block; margin: 16px auto 8px; }
#wm-rubric { font-size: 14px; color: #a1a1aa; margin-top: 8px; }
#wm-rubric .rubric-row { display: flex; justify-content: space-between; padding: 2px 0; }
/* Toast */
#toast-stack { position: fixed; top: 60px; right: 20px; display: flex; flex-direction: column; gap: 8px; z-index: 2000; pointer-events: none; }
.toast { background: #1a1a23; color: #fbbf24; border: 1px solid #fbbf24; border-radius: 6px; padding: 10px 16px; font-size: 16px; font-weight: 600; box-shadow: 0 4px 12px rgba(0,0,0,0.4); animation: toast-in 1.5s ease forwards; }
.toast.might-up { color: #fca5a5; border-color: #fca5a5; }
.toast.title-unlock { color: #a78bfa; border-color: #a78bfa; }
@keyframes toast-in {
  0%   { opacity: 0; transform: translateX(30px) scale(0.9); }
  15%  { opacity: 1; transform: translateX(0) scale(1.05); }
  25%  { transform: translateX(0) scale(1); }
  85%  { opacity: 1; }
  100% { opacity: 0; transform: translateX(30px); }
}
/* --- 2026-05-22 ⛔ AI 運行中 inline indicator + 中斷 button --- */
/* Singleton system bubble inserted into thread-body when ui_state ∈
   {thinking, tool_running}. Builds on .bubble.system base; adds counter
   tick + interrupt button. WAITING_INPUT is handled by .cr-permission-prompt
   below — these two never co-exist (Plan A §state gate). */
.cr-running-indicator {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  font-style: normal;
  font-size: 14px;
  background: #1f2937;
  border: 1px solid #374151;
  border-radius: 10px;
  padding: 8px 14px;
  margin: 8px auto;
  max-width: 80%;
}
.cr-running-indicator .cr-running-label { color: #93c5fd; font-weight: 600; }
.cr-running-indicator .cr-running-counter { color: #fbbf24; font-variant-numeric: tabular-nums; min-width: 4em; }
.cr-running-indicator .cr-interrupt-btn {
  background: #7f1d1d;
  color: #fff;
  border: 1px solid #b91c1c;
  border-radius: 6px;
  padding: 4px 12px;
  font-size: 13px;
  cursor: pointer;
  transition: background 0.15s;
}
.cr-running-indicator .cr-interrupt-btn:hover:not(:disabled) {
  background: #dc2626;
  border-color: #ef4444;
}
.cr-running-indicator .cr-interrupt-btn:disabled {
  background: #44403c;
  border-color: #57534e;
  color: #a8a29e;
  cursor: wait;
}
.cr-running-indicator .cr-running-note {
  flex-basis: 100%;
  color: #fca5a5;
  font-size: 12px;
  font-style: italic;
}
.cr-running-indicator .cr-running-note:empty { display: none; }
@media (max-width: 600px) {
  .cr-running-indicator { max-width: 95%; padding: 6px 10px; gap: 8px; }
  .cr-running-indicator .cr-running-label { font-size: 13px; }
}

/* --- Permission prompt quick-action buttons --- */
.cr-permission-prompt { margin: 8px 0; padding: 12px 14px; background: #2a2520; border: 1px solid #78580a; border-radius: 10px; }
.cr-prompt-question { font-weight: 600; color: #fbbf24; margin-bottom: 10px; font-size: 17px; line-height: 1.4; }
.cr-prompt-options { display: flex; flex-wrap: wrap; gap: 8px; }
.cr-prompt-btn { background: #1a2a3a; color: #93c5fd; border: 1px solid #334155; border-radius: 6px; padding: 8px 16px; font-size: 16px; cursor: pointer; transition: all .15s; }
.cr-prompt-btn:hover { background: #2563eb; color: #fff; border-color: #2563eb; }
.cr-prompt-btn.cr-prompt-selected { background: #1e3a5f; border-color: #3b82f6; color: #fff; }
.cr-prompt-btn.cr-prompt-cancel { background: #2a1a1a; color: #fca5a5; border-color: #7f1d1d; }
.cr-prompt-btn.cr-prompt-cancel:hover { background: #dc2626; color: #fff; border-color: #dc2626; }
.cr-prompt-answered { color: #4ade80; font-weight: 600; font-size: 16px; padding: 4px 0; }
/* 2026-06-01 AskUserQuestion FULL — multi-question (G2) + free input (G3).
   MODEL A: only the focused question is interactive; the rest render as a
   read-only top tab strip (mirrors the real TUI ☐/✔ strip). Reuses the
   .cr-permission-prompt theme. */
.cr-prompt-question-block { padding: 4px 0; }
.cr-prompt-multi .cr-prompt-question-block { padding: 10px 0; }
.cr-prompt-multi .cr-prompt-question-block.cr-prompt-q-active { border-left: 3px solid #fbbf24; padding-left: 10px; margin-left: -10px; }
/* Read-only progress strip */
.cr-prompt-tabs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid #4a3f1a; }
.cr-prompt-tab { font-size: 13px; padding: 3px 10px; border-radius: 12px; border: 1px solid #4a3f1a; color: #a8a29e; background: #221d18; cursor: default; }
.cr-prompt-tab.cr-prompt-tab-done { color: #4ade80; border-color: #166534; }
.cr-prompt-tab.cr-prompt-tab-active { color: #fbbf24; border-color: #fbbf24; font-weight: 600; }
.cr-prompt-tab.cr-prompt-tab-pending { opacity: 0.65; }
.cr-prompt-footer-actions { margin-top: 10px; }
.cr-prompt-btn.cr-prompt-notes-btn { background: #2a2520; color: #fcd34d; border-color: #78580a; }
.cr-prompt-btn.cr-prompt-notes-btn:hover { background: #78580a; color: #fff; border-color: #fbbf24; }
.cr-prompt-notes-wrap { display: flex; gap: 8px; margin-top: 8px; width: 100%; }
.cr-prompt-notes-input { flex: 1; background: #1a1a1a; color: #e5e7eb; border: 1px solid #334155; border-radius: 6px; padding: 8px 12px; font-size: 16px; }
.cr-prompt-notes-input:focus { outline: none; border-color: #3b82f6; }
.cr-prompt-btn.cr-prompt-notes-send { background: #166534; color: #d1fae5; border-color: #15803d; }
.cr-prompt-btn.cr-prompt-notes-send:hover { background: #15803d; color: #fff; }
.cr-prompt-degraded { color: #fca5a5; font-size: 15px; padding: 4px 0; font-style: italic; }
@media (max-width: 600px) {
  .cr-prompt-options { flex-direction: column; }
  .cr-prompt-btn { width: 100%; text-align: left; }
  .cr-prompt-notes-wrap { flex-direction: column; }
}

/* Compact group members strip: first name + "+N" overflow badge. Clicking
   the strip opens the #members-modal with the full list. */
#thread-members {
  padding: 6px 16px;
  border-bottom: 1px solid #2a2a35;
  background: #15151c;
  font-size: 14px;
  color: #a1a1aa;
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  user-select: none;
  transition: background 0.15s;
}
#thread-members:hover { background: #1a1a23; }
#thread-members.hidden { display: none; }
#thread-members .mm-icon { color: #71717a; font-size: 16px; }
#thread-members .mm-first { color: #e4e4e7; font-weight: 500; }
#thread-members .mm-rest {
  color: #a1a1aa;
  background: #232336;
  padding: 1px 8px;
  border-radius: 10px;
  font-size: 13px;
  font-weight: 600;
}
#thread-members .mm-chev { color: #52525b; margin-left: auto; font-size: 17px; }

/* Members modal list */
#mm-list { list-style: none; margin: 12px 0 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
#mm-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: 6px;
  background: #1a1a23;
}
#mm-list li.me { background: #2a2a3f; }
#mm-list .mm-avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: #4f46e5;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 17px;
  font-weight: 600;
  flex-shrink: 0;
}
#mm-list .mm-name { font-size: 16px; color: #e4e4e7; font-weight: 500; }
#mm-list .mm-me-tag { margin-left: auto; color: #a78bfa; font-size: 13px; }
#mm-count { font-size: 16px; color: #a1a1aa; font-weight: 400; margin-left: 6px; }

#thread-body { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 8px; }
.bubble { max-width: 72%; padding: 8px 12px; border-radius: 10px; font-size: 16px; line-height: 1.5; word-wrap: break-word; }
.bubble.user { align-self: flex-end; background: #4f46e5; color: #fff; }
.bubble.assistant { align-self: flex-start; background: #353545; color: #e4e4e7; }
.bubble p { margin: 0; }
.bubble p + p { margin-top: 8px; }
.bubble.system { align-self: center; background: #1f2937; color: #9ca3af; font-size: 13px; font-style: italic; }
.bubble.tool_use { align-self: flex-start; background: #1f2937; color: #60a5fa; border-left: 3px solid #60a5fa; font-family: monospace; font-size: 14px; }
.bubble.tool_result { align-self: flex-start; background: #1f2937; color: #86efac; border-left: 3px solid #86efac; font-family: monospace; font-size: 14px; white-space: pre-wrap; }
.bubble pre { background: #0b0b10; padding: 8px; border-radius: 4px; overflow-x: auto; }
.bubble code { background: #0b0b10; padding: 1px 4px; border-radius: 3px; font-size: 14px; }
.bubble.user pre, .bubble.user code { background: rgba(0,0,0,0.25); }
.bubble .ts { display: block; font-size: 12px; opacity: 0.6; margin-top: 4px; }
/* 2026-07-13: 泡泡內連結配色 — UA 預設藍在深色泡泡（#353545/#4f46e5）上不好讀，
   改用 amber（與 .warlord accent #fbbf24 同色系）。anchor 不繼承泡泡本體 color，須顯式設。 */
.bubble a { color: #fbbf24; text-decoration: underline; }
.bubble a:visited { color: #f59e0b; }
.bubble a:hover { color: #fde68a; }

/* MQ queue status bar */
#queue-status { padding: 6px 16px; background: #1a1a23; border-top: 1px solid #2a2a35; font-size: 14px; color: #a1a1aa; }
#queue-status.hidden { display: none; }

/* ---------------------------------------------------------------- 安靜版面
   2026-08-12 建航：「AI 運行中」不要把訊息推來推去。
   每一條都以 `body.quiet-chrome` 開頭 → 沒套這個 class 的人一條都不生效，
   其他使用者的畫面完全沒變（做法比照 chatroom-light.css 的 body.theme-light）。

   原本 #queue-status 是輸入框上方的一般區塊，一出現就把上面的訊息往上頂。
   改成浮在輸入框上方的膠囊（脫離文件流）→ 顯示／消失都不推擠版面。
   定位參考同容器內既有的 #scroll-to-bottom-btn（也是 absolute + bottom）。 */
/* 2026-08-13 建航：戰將卡（「伍長 LV.1」＋雷達圖＋戰力條）用不到，隱藏。
   只是不顯示，功能與資料都沒動 —— #warlord-card 是純展示元素（點擊展開資訊），
   隱藏後不影響任何操作。同樣掛 body.quiet-chrome，只對建航生效。
   註：第 1277 行另有一條手機版通用的隱藏，那是所有人共用的版面規則，兩者不衝突。 */
body.quiet-chrome #warlord-card { display: none !important; }

body.quiet-chrome #queue-status {
  position: absolute;
  left: 12px;
  bottom: 74px;
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 10px;
  background: rgba(26, 26, 35, 0.94);
  border: 1px solid #2a2a35;
  border-top: 1px solid #2a2a35;
  border-radius: 999px;
  padding: 5px 14px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
  color: #a1a1aa;
  font-size: 13px;
}
/* 上面那條把 display 蓋成 flex，特異度也比 #queue-status.hidden 高，
   必須自己補一條 hidden，否則永遠藏不起來（同 chatroom-light.css 的坑）。 */
body.quiet-chrome #queue-status.hidden { display: none; }
body.quiet-chrome #queue-status .cr-running-fixed {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  max-width: none;
  font-size: 13px;
  padding: 0;
  border: none;
  background: transparent;
}
/* ccms_quiet_chrome === 'hide' 才會加上這個 class：連運行中集群一起藏。
   ⚠️ ⛔ 中斷鈕也在這個集群裡，藏了就按不到。 */
body.quiet-chrome-hide #queue-status .cr-running-fixed { display: none; }

/* -------------------------------------------------- 2026-08-13 建航：極簡運行列
   回報：那條「🤖 AI 運行中…」膠囊是 absolute 浮在輸入框上方，會蓋住訊息流
   最底下那幾行回覆。參考截圖（IMG_5591）要的是一行輕量的「✨ 思考中…」。

   兩件事一起修，每一條都掛 body.quiet-chrome → 沒套這個 class 的人一條都不生效：
   (1) 這條列改回文件流（position: static），夾在訊息流與輸入框之間
       → **結構上就不可能蓋住訊息**，不必去猜輸入框多高、留幾 px 才夠。
       曾試過「維持浮動 ＋ 訊息流底部留白 46px」，但輸入框會隨打字長高、
       浮動列跟著上移，留白抓多少都會在某些情況又蓋回去，故放棄。
   (2) 外觀降到最低調：去邊框、去陰影、標籤字換成「✨ 思考中…」、中斷鈕縮小

   代價（已知並接受）：它出現／消失時訊息流高度會變一行（約 26px），
   捲到底時看得到內容上下移一點點。cyn 2026-08-12 原本改成浮動就是要消掉這個，
   但她要消的是**訊息流裡面**那顆氣泡造成的位移；這條列在訊息流外面，
   出現時只讓可視區短一行，讀到一半的位置（scrollTop）不動。

   ⚠️ 標籤文字用 CSS 偽元素換掉，**不動 chatroom.js 的字串**。
   理由：syncQueueStrip() 用 /AI 運行中/ 這個比對決定要不要把重複的標籤藏起來
   （「正在回答你的問題…」在場時），直接改 JS 字串會讓那條去重失效，
   變成同一列講兩次同一件事；js/__tests__/cyn-ui-quad-20260812.test.js 也鎖著它。 */
body.quiet-chrome #queue-status {
  position: static;
  left: auto;
  bottom: auto;
  border: none;
  border-top: 1px solid #22222c;
  border-radius: 0;
  box-shadow: none;
  background: transparent;
  padding: 4px 16px;
  color: #9ca3af;
  min-height: 2.4em;   /* 見下方「不准抖」那段 —— 空著時也要撐住這個高度 */
}
/* 🔴 2026-08-13 建航：「思考中就好好思考中」，整個聊天室不准跟著抖。
   病因：後端的狀態判讀在兩段思考之間會短暫看到「閒置」，這一行就整條消失、
   隨即又出現。它排在版面裡（上一批的改法），一消失一出現就是一次高度變化 →
   整片訊息上下彈。
   兩層一起修，這層是結構保證：
     這一行**永遠佔著同一個高度**，藏起來時只是看不見（visibility），
     不是從版面上拿掉（display:none）。高度恆定 → 不管它閃幾次都不可能抖。
   min-height 用 em 不用 px：跟著字級縮放，換裝置／改字級都不會失準。
   visibility: hidden 會連同 border-top 一起隱藏，所以閒置時底部不會留下一條線。

   ⚠️ 一定要 !important：本檔第 23 行有一條全域的 `.hidden { display: none !important }`，
   不加就會被它壓過去，這一行還是會整條從版面上消失（實測過，高度歸零）。
   也因此上方第 995 行那條「補 hidden」在這裡已無作用，以本條為準。 */
body.quiet-chrome #queue-status.hidden {
  display: flex !important;
  visibility: hidden;
}
/* .cr-running-indicator 的 `margin: 8px auto` 會把整串推到正中央（左右 auto）。
   參考截圖是靠左貼齊訊息，故歸零。 */
body.quiet-chrome #queue-status .cr-running-fixed { margin: 0; }
body.quiet-chrome #queue-status .cr-running-label {
  font-size: 0;          /* 蓋掉 JS 寫死的「🤖 AI 運行中…」，字串本身不動 */
  font-weight: 400;
}
body.quiet-chrome #queue-status .cr-running-label::before {
  content: '✨ 思考中';
  font-size: 13px;
  font-weight: 400;
  color: #9ca3af;
  animation: cr-thinking-breathe 2.4s ease-in-out infinite;
}
/* 2026-08-13 建航：點點點要會動，做出「正在想」的感覺。兩層動畫疊在一起：
   (1) ::after 三顆點做「0→1→2→3 顆」的循環（把三個點的寬度裁掉再放出來）
   (2) ::before 的「✨ 思考中」四個字做慢速明暗呼吸

   ⚠️ 用 clip-path 遮住點，**不是**在 @keyframes 裡換 content，也不是改 width。
   - 換 content：非標準寫法，Firefox 不吃，會整個不動。
   - 改 width：實測會把後面的「⏱ 9s」和 ⛔ 中斷鈕整組往左右推 21px，一直抖。
     clip-path 只遮住畫面、不改元素佔的位置，所以三顆點的空間永遠留著，
     後面的東西一動也不動。
   ⚠️ steps(4, jump-none)：讓遮罩只停在 0 / 1 / 2 / 3 顆點這四個位置，
   不寫 jump-none 的話會停在半顆點的位置，點會被切一半。
   ⚠️ inline-block + vertical-align: bottom：偽元素要能吃 width 就得是 block 系，
   而 inline-block 預設用基線對齊，會讓那三顆點浮高，故改對齊底線。 */
body.quiet-chrome #queue-status .cr-running-label::after {
  content: '...';
  display: inline-block;
  vertical-align: bottom;
  width: 3ch;
  font-size: 13px;
  font-weight: 400;
  color: #9ca3af;
  animation: cr-thinking-dots 1.2s steps(4, jump-none) infinite;
}
@keyframes cr-thinking-dots {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}
@keyframes cr-thinking-breathe {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.55; }
}
/* 系統設定為「減少動態效果」時整組停掉（暈眩／前庭敏感的無障礙慣例）。
   停掉後三顆點固定顯示，不會變成看不出在跑。 */
@media (prefers-reduced-motion: reduce) {
  body.quiet-chrome #queue-status .cr-running-label::before,
  body.quiet-chrome #queue-status .cr-running-label::after {
    animation: none;
  }
  body.quiet-chrome #queue-status .cr-running-label::after { clip-path: none; }
}
/* 秒數從 9s 跳到 10s、100s 時字會變寬，把 ⛔ 中斷鈕往右推 → 又是抖動。
   給它一個固定寬度並靠左排，數字再長也不推擠（超過 999s 才會撐開，
   那種長度的等待本來就少見）。原本 chatroom.css 的 4em 是給 14px 字用的，
   這裡字級縮到 12px，改用 3.4em 對齊。 */
body.quiet-chrome #queue-status .cr-running-counter {
  color: #71717a;
  font-size: 12px;
  min-width: 3.4em;
  text-align: left;
}
body.quiet-chrome #queue-status .cr-interrupt-btn {
  background: transparent;
  border: 1px solid #4b5563;
  color: #9ca3af;
  padding: 1px 8px;
  font-size: 12px;
}
body.quiet-chrome #queue-status .cr-interrupt-btn:hover:not(:disabled) {
  background: #7f1d1d;
  border-color: #b91c1c;
  color: #fff;
}

/* 置底按鈕 */
#scroll-to-bottom-btn {
  position: absolute;
  bottom: 80px;
  right: 16px;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: #4f46e5;
  color: #fff;
  font-size: 20px;
  border: none;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0,0,0,0.4);
  transition: opacity 0.2s;
  z-index: 10;
}
#scroll-to-bottom-btn:hover { background: #6366f1; }
#scroll-to-bottom-btn.hidden { display: none; }

/* Sender name in user bubbles */
.bubble .sender-name { font-size: 13px; font-weight: 600; color: #818cf8; margin-bottom: 2px; }

/* Reply tag in AI bubbles */
.bubble .reply-tag { font-size: 12px; color: #6ee7b7; margin-bottom: 4px; }

/* 2026-05-13 chatroom 三狀態 bubble 生命週期（diagnosis 2026-05-13-chatroom-debounce-autosend.md）
   State 1 .pending : 灰色虛線 + 倒計時 + 取消按鈕
   State 2 .queued  : user bubble 樣式 + 排隊位置 + 取消按鈕
   State 3 .delivered : 正常 user bubble（無 footer） */
.bubble.user.pending {
  background: #2a2a35;           /* 與 user 紫不同，灰色 */
  color: #c4c4cf;
  border: 1px dashed #818cf8;    /* 虛線輪廓 */
  opacity: 0.85;
  /* 2026-05-14 v1.2: PENDING bubble 移除時 fade-out（diagnosis P2 風險表）
     —— 還原路徑同步移除 DOM，這個 transition 在 bubble 還未實際 remove 之前的
     瞬間提供視覺平滑；不影響 JS 行為（DOM 仍 synchronous remove）。 */
  transition: opacity 150ms ease-out;
}
.bubble.user.queued {
  background: #4f46e5;            /* 同 .bubble.user 紫 */
  color: #fff;
  border: 1px solid #6366f1;
}
.bubble.user.delivered {
  background: #4f46e5;
  color: #fff;
}
.bubble .bubble-text { word-wrap: break-word; }
.bubble .bubble-footer {
  margin-top: 6px;
  font-size: 12px;
  display: flex;
  align-items: center;
  gap: 8px;
  opacity: 0.85;
}
.bubble .pending-countdown {
  font-weight: 600;
  color: #fbbf24;                 /* amber/yellow — 醒目 */
  min-width: 1.5em;
  text-align: right;
}
.bubble .queue-pos {
  color: #6ee7b7;                 /* green-200 */
}
.bubble .bubble-cancel-btn {
  margin-left: auto;
  background: transparent;
  border: 1px solid currentColor;
  color: inherit;
  border-radius: 3px;
  padding: 1px 8px;
  font-size: 12px;
  cursor: pointer;
  line-height: 1.4;
}
.bubble .bubble-cancel-btn:hover {
  background: rgba(255, 255, 255, 0.08);
}

/* 2026-07-07 auto-grow: flex-end keeps 📎🎤send buttons hugging the textarea's
   bottom edge when it grows to multiple lines (was `center`). */
/* 2026-07-08: flex-wrap: wrap 必要 — #cs-attach-bar（貼圖 chip 列，flex-basis:100%;
   order:-1）需換到自己一行（最上），否則同 flex 行會把 textarea（flex:1 1 0; width:0）
   擠成 0 寬 → 貼圖後打不了字（使用者實測 bug）。mobile #input-form(:1130) 不覆寫
   display，故此 wrap 一併生效，不重複宣告。 */
#input-form { display: flex; flex-wrap: wrap; gap: 0; padding: 10px 16px; border-top: 1px solid #2a2a35; background: #111118; align-items: flex-end; }
#input-form > *:not(:first-child) { margin-left: 8px; }
/* 2026-07-08: #cs-attach-bar 是 form 第一個 DOM child（貼圖 chip 列，full-width、
   order:-1），故緊接的 #attach-btn 會被上面 :not(:first-child) 誤加 8px 左margin
   （即使 bar hidden 也算 first-child）→ 📎 鈕左偏。歸零讓它照原設計貼齊左緣（bar
   本身 full-width 不需水平 margin）。 */
#input-form > #cs-attach-bar + #attach-btn { margin-left: 0; }
#input-form.input-focused > .icon-btn { margin-left: 0; }
#input-form .icon-btn {
  background: #2a2a35;
  color: #e4e4e7;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 22px;
  padding: 6px 10px;
  height: 38px;
  flex-shrink: 0;
  overflow: hidden;
  transition: max-width .25s ease, padding .25s ease, opacity .2s ease, margin .25s ease;
  max-width: 50px;
}
#input-form.input-focused .icon-btn {
  max-width: 0 !important;
  padding: 0 !important;
  margin: 0 !important;
  opacity: 0;
  pointer-events: none;
  border: none;
}
#input-form .icon-btn:hover:not(:disabled) { background: #3a3a45; }
#input-form .icon-btn:disabled { opacity: 0.4; cursor: not-allowed; }
#input-form #mic-btn.recording { background: #dc2626; animation: mic-pulse 1.2s infinite; }
@keyframes mic-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.6); }
  50% { box-shadow: 0 0 0 6px rgba(220, 38, 38, 0); }
}
/* 2026-07-07 auto-grow: fixed height replaced by min/max + JS scrollHeight sizing.
   max-height:40vh caps growth, overflow-y:auto scrolls beyond it. resize:none keeps
   it purely auto (no manual drag handle). See js/chatroom.js autoGrow(). */
#input-form textarea { flex: 1 1 0; min-width: 0; width: 0; resize: none; padding: 8px; background: #1e1e26; color: #e4e4e7; border: 1px solid #2a2a35; border-radius: 4px; font-family: inherit; font-size: 16px; min-height: 38px; max-height: 40vh; overflow-y: auto; line-height: 1.4; }
#input-form button { padding: 0 20px; background: #4f46e5; color: #fff; border: none; border-radius: 4px; cursor: pointer; flex-shrink: 0; height: 38px; }
#input-form #send-btn { min-width: 72px; }
#input-form button:disabled { background: #3a3a45; cursor: not-allowed; }

/* ---------- Mobile (single-pane) layout ---------- */
.mobile-only { display: none; }

#thread-back-btn {
  background: transparent;
  color: #e4e4e7;
  border: none;
  cursor: pointer;
  font-size: 17px;
  padding: 4px 10px 4px 4px;
  display: none;
  align-items: center;
  gap: 2px;
  border-radius: 6px;
}
#thread-back-btn:hover { background: #232336; }
#thread-back-btn .back-arrow {
  font-size: 31px;
  line-height: 1;
  color: #a1a1aa;
  font-weight: 300;
}
#thread-back-btn #thread-back-label { font-size: 16px; }
#thread-back-btn .back-unread {
  background: #dc2626;
  color: #fff;
  border-radius: 10px;
  padding: 1px 7px;
  font-size: 12px;
  font-weight: 600;
  margin-left: 6px;
}

@media (max-width: 768px) {
  /* Hard constrain the whole app to the viewport — no horizontal scroll,
     and pin to dynamic viewport height so mobile URL bar doesn't cut the
     input box off the bottom of the screen. */
  html, body {
    overflow: hidden;
    width: 100%;
    max-width: 100vw;
    height: var(--vvh, 100dvh);
    max-height: var(--vvh, 100dvh);
    position: fixed;
    inset: 0;
  }
  #app {
    width: 100vw;
    max-width: 100vw;
    height: var(--vvh, 100dvh);
    max-height: var(--vvh, 100dvh);
    overflow: hidden;
  }
  #main {
    height: auto;
    min-height: 0;
    flex: 1 1 auto;
    width: 100vw;
    max-width: 100vw;
    min-width: 0;
  }
  #sidebar {
    width: 100vw;
    max-width: 100vw;
    min-width: 0;
    box-sizing: border-box;
  }
  #thread {
    width: 100vw;
    max-width: 100vw;
    min-width: 0;
    box-sizing: border-box;
    overflow: hidden;
  }
  #thread-head {
    flex-shrink: 0;
    min-width: 0;
    width: 100%;
    max-width: 100%;
    box-sizing: border-box;
    flex-wrap: wrap;
    position: sticky;
    top: 0;
    z-index: 5;
    background: #161620;
  }
  #thread-head > * { min-width: 0; }
  #thread-members { flex-shrink: 0; width: 100%; max-width: 100%; box-sizing: border-box; }
  #thread-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    width: 100%;
    max-width: 100%;
    box-sizing: border-box;
  }
  #thread-body .bubble { max-width: 88%; box-sizing: border-box; }
  #thread-body .bubble img { max-width: 100%; }
  #thread-body .bubble table { display: block; overflow-x: auto; max-width: 100%; }
  #input-form { flex-shrink: 0; }
  #main.show-thread #thread {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
    flex: 1 1 auto;
  }

  header { padding: 6px 10px; gap: 8px; }
  header .brand { font-size: 17px; }
  header #user-info { font-size: 13px; }
  header button { padding: 4px 8px; font-size: 14px; }

  #main {
    position: relative;
    width: 100%;
    min-width: 0;
    overflow-x: hidden;
  }
  #sidebar { width: 100%; min-width: 0; border-right: none; }
  #thread {
    display: none;
    width: 100%;
    min-width: 0;
    max-width: 100vw;
    overflow-x: hidden;
  }

  /* When a thread is selected, sidebar slides out and thread takes the
     whole screen. Back button returns to the list view. */
  #main.show-thread #sidebar { display: none; }
  #main.show-thread #thread { display: flex; }

  #thread-back-btn { display: inline-flex; }

  /* Warlord card + viewers take too much room on phones — compact. */
  #warlord-card { display: none !important; }
  #thread-head { padding: 10px 12px; gap: 8px; }
  #thread-title {
    flex: 1;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: 17px;
  }

  #thread-members { padding: 6px 12px; }
  #thread-body {
    padding: 10px;
    min-width: 0;
    width: 100%;
    max-width: 100%;
  }
  .bubble {
    max-width: 88%;
    font-size: 17px;
  }
  /* Long code blocks inside a bubble scroll inside themselves. */
  .bubble pre {
    max-width: 100%;
    overflow-x: auto;
    white-space: pre;
    word-break: normal;
  }
  .bubble pre code { white-space: pre; }

  #input-form {
    padding: 8px 10px;
    width: 100%;
    max-width: 100%;
    min-width: 0;
  }
  #input-form textarea {
    /* iOS Safari auto-zooms when focusing inputs with font-size < 16px,
       which permanently shifts the viewport and breaks our layout widths.
       Keep this at 16px on mobile to suppress the zoom. */
    font-size: 19px;
    min-width: 0;
    max-width: 100%;
    /* 2026-07-07 auto-grow: fixed height → min/max + JS scrollHeight sizing. */
    min-height: 44px;
    max-height: 40vh;
    overflow-y: auto;
  }
  #input-form button { padding: 0 16px; flex-shrink: 0; font-size: 18px; height: 44px; }
  #input-form #send-btn { min-width: 64px; }

  /* Worker list rows a touch taller for finger taps. */
  #worker-list li { padding: 14px 14px; }
  #worker-list .name { font-size: 17px; }
  #worker-list .preview { font-size: 14px; }

  /* Loadout modal: near-fullscreen on mobile */
  .loadout-modal-box {
    width: 100vw;
    max-width: 100vw;
    max-height: var(--vvh, 100dvh);
    height: var(--vvh, 100dvh);
    border-radius: 0;
    padding: 16px;
    margin: 0;
  }
  .loadout-modal-body { max-height: none; }
  .lo-tab { font-size: 17px; padding: 10px 4px; }
  .lo-edit-item { padding: 8px 10px; }
  .lo-edit-name { font-size: 17px; }

  /* 2026-05-08: 一般 modal-box（含「登錄武將」、「編輯武將」、「群組成員」）
     在 mobile 的尺寸對齊 — 解兩個 mobile bug：
     (1) 既有 .modal-box { max-height: 90vh } 在 mobile Safari URL bar
         收合時會跟著跳動，其他元素已切 var(--vvh, 100dvh)，唯獨這個沒跟上。
     (2) iPhone SE 寬度 320px 時 min-width: 320px + padding 24+28=372px 會
         溢出畫面（因 modal-box 預設 min-width: 320px）。
     登錄武將 modal 的 .cw-profile-list 同步把 max-height 從 360px 提到
     60vh，讓 30+ profile 環境在小螢幕看得多一些。
     見 diagnoses/2026-05-08-cw-modal-no-scroll.md follow-up #3 */
  .modal-box {
    max-width: calc(100vw - 16px);
    max-height: var(--vvh, 100dvh);
    min-width: 0;
    width: 100%;
    box-sizing: border-box;
  }
  .cw-profile-list { max-height: 60vh; }
}

/* ---- Edge rate-limit badges (single footer below warrior list) ---- */
/* 5h/7d is per-edge (shared by all warriors), so it's shown once here. */
.server-metrics {
  display: flex;
  align-items: center;
  /* 2026-08-25 P161 #1348：footer 從 2 顆 chip 變 4 顆（+ CPU / MEM）。
     桌機 #sidebar 固定 280px、扣掉 padding 只剩 256px，4 顆 chip 加 gap 就會溢出，
     所以 wrap 是桌機也需要的，不是只給手機的配套。gap 在 flex 同時是 row/column gap，
     換行後行距沿用同一個 8px。 */
  flex-wrap: wrap;
  gap: 8px;
  padding: 10px 12px;
  border-top: 1px solid #2a2a35;
  background: #111118;
  font-size: 14px;
  color: #cbd5e1;
}
.server-metrics .sm-label {
  color: #a1a1aa;
  font-size: 12px;
  margin-right: auto;
}
.server-metrics .sm-metric {
  background: #2a2a35;
  padding: 4px 10px;
  border-radius: 4px;
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  border: 1px solid #3a3a45;
}
/* 🔴 2026-08-25 P161 #1351 —— 這裡原本有一組 `[data-pct^=...]` **字串前綴**門檻規則
   （`d173b52`，2026-04-19）：
       .server-metrics .sm-metric[data-pct^="5"],[data-pct^="6"],[data-pct^="7"] { 黃 }
       .server-metrics .sm-metric[data-pct^="8"],[data-pct^="9"],[data-pct="100"] { 紅 }
   `^=` 是前綴比對、不是數值比較：`"9"` 本身就命中 `^="9"`，所以 0..100 這 101 個整數
   **判錯 5 個** —— 5/6/7 誤上黃、8/9 誤上紅。畫面實測（#1356，claude2 用 PIL 取像素）：
       sm-7d  text='7d 9%'  data-pct='9'  邊框 rgb(220,38,38) 文字 rgb(248,113,113) weight=600
   ＝「7d 用量 9%」（很低）被畫成紅色告警。
   已刪除，改吃 chatroom.js 的 resourceLevel() 算好的 data-level（下方 :sm-metric[data-level]
   那兩條，#1348 導入時就已存在，四顆 chip 現在共用同一組）。門檻 50/80 與原前綴規則語意等價。
   ⚠️ `data-pct` 屬性本身**仍然照掛**（別處可能讀它），但 CSS 端**不得**再有任何規則
      依賴它做門檻判定 —— 加回去就會復刻本缺陷。 */
/* ---- footer 級灰化（statusline `ts` 那條 feed 過期）------------------------
   2026-08-25 P161 #1367：作用面由「整個 .server-metrics」收斂成「**除 cpu/mem 以外的
   直接子元素**」。原規則（d173b52, 2026-04-19）逐字是
       .server-metrics[data-stale="true"] { opacity: 0.4; }
   那時 footer 裡只有 5h/7d 兩顆 chip，「整片灰」與「這條 feed 灰」是同一件事；
   #1348 把第二條 feed 的 CPU / MEM chip 放進同一個 footer 之後才分岔。

   ── 為什麼要改 ──
   footer 級這條由 **statusline 的 `ts`** 判定（120 秒門檻，chatroom.js
   updateAllWorkerMetrics()）；cpu/mem 走的是 **SystemSampler 的 `resource_stale_ms`**
   （60 秒採樣、180 秒門檻）。兩條 feed 在後端就刻意各帶自己的 ts
   （backend/local/core/metrics_cache.py:226-229，a3f809e：「Separate ts (resource_ts)
   from the statusline ts above so the two feeds … don't clobber each other's
   monotonic guards」），顯示層卻讓 statusline 那條的過期蓋掉資源那條 ——
   statusline 沒回報時，60 秒內剛量到的 CPU / MEM 也一起灰掉，等於告訴使用者那兩個
   值不新鮮。

   ── 為什麼不是在 chip 上寫覆蓋 ──
   `opacity` 不是繼承屬性，是**群組合成**：父層一旦 < 1，整個子樹被當成一張圖去做透明
   合成，子元素**無法**用任何宣告把自己加回不透明（子層的 `opacity: 1` 只是「在那張
   已經 0.4 的圖裡不透明」）。唯一改得動的地方是**別讓父層拿到 opacity**。

   ── 行為界線 ──
   * .sm-5h / .sm-7d / .sm-label 照舊灰，且 chip 的算繪結果**逐格相同**：
     原本是父 0.4 × 子 1.0，現在是父 1.0 × 子 0.4。
   * 唯一差別是 .server-metrics **自身**的 background / border-top 不再跟著淡 ——
     容器裡還有新鮮的 cpu/mem，本來就不該整片淡掉。
   * 用子組合子 `>`，前提是 chatroom.html 的 footer markup 是平的（label + 4 顆 chip
     都是直接子元素）。若哪天把 chip 包進 wrapper，wrapper 會接手這條 opacity、
     bug 會原樣長回來 —— 所以測試量的不是「chip 自己有沒有被規則命中」，而是
     「**從 chip 往上到 footer 這條鏈上，有沒有任何一層被灰化規則命中**」
     （js/__tests__/chatroom-footer-cpu-mem-chips.test.js 的 B8），包 wrapper 會被抓到。 */
.server-metrics[data-stale="true"] > :not(.sm-cpu):not(.sm-mem) { opacity: 0.4; }

/* ---- 2026-08-25 P161 #1348 — 警示分級（原為 CPU / MEM chip 專用）----------
   🔴 這兩條規則**吃 chatroom.js 的 resourceLevel() 算好的 data-level，不碰 data-pct**。
      理由：CSS 屬性選擇器的 `^=` 是**字串前綴**比對、不是數值比較，連個位數本身都命中，
      0..100 的整數會判錯 5 個（5/6/7 誤上黃、8/9 誤上紅）。閒置機器 CPU 常態就在 2–9%，
      用前綴比對等於上線第一天就把「CPU 8%」畫成紅色。
   🔴 **2026-08-25 #1351 起，作用面從 cpu/mem 兩顆擴到 footer 全部四顆。**
      #1348 導入本段時，上面 5h/7d 的 `[data-pct^=...]` 前綴規則刻意一字不動、缺陷
      另開延伸單 #1351；#1351 已落地 —— 那組前綴規則**已刪除**（見上方 docblock），
      .sm-5h / .sm-7d 現在也由 JS 掛 data-level，直接命中本段這兩條。
      選擇器維持 `.sm-metric[data-level=...]`（不特指 .sm-cpu/.sm-mem）本來就涵蓋四顆，
      故本段規則**一字未改**，只是服務對象變多。
      同族另外兩處（.ctx-pct、css/lobby-list.css 的 .lr-metric）同批改，見各自註解。
   分級門檻：>= 80 crit（紅）／>= 50 warn（黃）／其餘預設灰（判定在 JS 端做數值比較）。
   🔴 這組 50 / 80 **與被取代的前綴規則語意等價** —— `[data-pct^="5"|"6"|"7"]` 換算成
      數值就是 50 起黃、`^="8"|"9"|"100"` 就是 80 起紅（差別只在前綴比對會多命中個位數）。
      同一個 footer 裡四顆 chip 因此共用同一套配色語意。
   🔴 **與 web/admin/admin-logic.js 的 usagePctClass() 門檻不同**（那邊 < 70 綠 /
      70–89 黃 / >= 90 紅）。那是 admin 頁刻意的另一組門檻，**不是**這裡待對齊的目標；
      從它借來的只有「JS 數值比較」這個做法，不含數字。
   ⚠️ commit `ef72692` 的 message 寫「樣板對齊 usagePctClass()」，**那句就數字而言是錯的**
      （50/80 vs 70/90）；commit message 已入庫改不了，以本註解與 chatroom.js
      resourceLevel() 上方的註解為準。 */
.server-metrics .sm-metric[data-level="warn"] { color: #fbbf24; border-color: #d97706; }
.server-metrics .sm-metric[data-level="crit"] { color: #f87171; border-color: #dc2626; font-weight: 600; }

/* per-chip 過期灰化。資源有自己的時鐘（edge_resource_stale_ms，來自 60s 的 sampler），
   與 footer 級 .server-metrics[data-stale="true"] 綁的 statusline `ts` 是兩條不同的
   feed。門檻 180000ms ＝ 採樣週期的 3 倍，由 chatroom.js 的 resourceIsStale() 判定後
   掛在 chip 上 —— 這是 cpu/mem 兩顆**唯一**的灰化來源（#1367 起 footer 級那條的
   選擇器已把這兩顆排除在外，見上方 docblock；5h/7d 的灰化行為不變）。 */
.server-metrics .sm-metric[data-stale="true"] { opacity: 0.4; }

/* 手機（≤600px，chatroom 既有斷點慣例）：#sidebar 在 ≤768px 撐成 100vw，
   375px 下 4 顆 chip 加 label 仍會超過一行 —— 讓 label 自己佔一整行、chip 收窄，
   4 顆才能完整顯示不被截斷。375px 是驗收量測寬度，不是斷點值。 */
@media (max-width: 600px) {
  .server-metrics { gap: 6px; padding: 8px 10px; }
  .server-metrics .sm-label { flex-basis: 100%; margin-right: 0; }
  .server-metrics .sm-metric { padding: 3px 8px; font-size: 13px; }
}

/* ---- Per-session context badge (thread head) ---- */
button.ctx-pct,
.ctx-pct {
  font-size: 14px;
  margin-left: 10px;
  padding: 3px 10px;
  background: #eef2ff;
  color: #4f46e5;
  border-radius: 4px;
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  vertical-align: middle;
  border: 1px solid transparent;
  cursor: pointer;
  font-family: inherit;
  line-height: 1.4;
  transition: filter 0.15s, border-color 0.15s;
}
button.ctx-pct:hover,
.ctx-pct:hover {
  filter: brightness(0.95);
  border-color: currentColor;
}
button.ctx-pct:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}
/* 🔴 2026-08-25 P161 #1351 —— 這裡原本是 `[data-pct^=...]` **字串前綴**門檻規則
   （`a24ec71`，2026-04-18）：
       .ctx-pct[data-pct^="5"],[data-pct^="6"],[data-pct^="7"] { 黃 }
       .ctx-pct[data-pct^="8"],[data-pct^="9"],[data-pct="100"] { 紅 }
   `^=` 連個位數本身都命中 ⇒ 0..100 判錯 5 個（5/6/7 誤黃、8/9 誤紅）。對 context 這顆
   badge 尤其反直覺：`ctx 9%`（幾乎空的）會被畫成 `#fee2e2`/`#b91c1c` 的「快滿了」紅，
   與真值相反。改吃 chatroom.js updateThreadContextBadge() 掛的 data-level（門檻 50/80，
   共用 resourceLevel()），配色與原規則逐字相同。
   ⚠️ `data-pct` 屬性仍照掛，但 CSS 端不得再有規則依賴它做門檻。 */
.ctx-pct[data-level="warn"] { background: #fef3c7; color: #b45309; }
.ctx-pct[data-level="crit"] { background: #fee2e2; color: #b91c1c; font-weight: 600; }
.ctx-pct[data-stale="true"] { opacity: 0.4; }

/* ---------------------------------------------------------------------------
   2026-08-18 P151 #1203 — header 模型 badge（#thread-model）＋ 連體 pill（#ctx-group）
   spec: central/docs/superpowers/specs/2026-08-18-p151-model-badge-ui-contract.md §4

   ── 為什麼是「連體 pill」而不是再放一顆獨立元素 ──────────────────────────────
   #thread-head 是 flex + gap:16px（桌機）/ 8px（≤768px）。多放一顆直接子元素就多
   一個 gap。把 model 與 ctx 包進 #ctx-group 後，head 的直接子元素數維持 6 個，
   **一個 gap 都沒多**。390px 的 AC-M 驗收餘裕只有 2.64px，這 8px 給不起。
   🔴 **2026-08-21 #1333 補述（原文一字未刪）**：「餘裕只有 2.64px」的前提是
      **未讀總數＝0**；AC-M 門檻本身已由使用者裁示豁免（2026-08-19「甲」，見 #1279）
      —— **知情接受，不是已達成**（未讀 1 時 titleW ＝ 71.5、未讀 99+ 時 57.83，皆低於 100）。
      ⚠️ 豁免的是「要求它達成」，不是「這 8px 可以給」—— 本段「不新增 flex gap」的
      結論**不變**，而且更強：餘裕比 2.64px 還小，更給不起。
      完整說明見本檔下方「≤600px 手機配套 4 項」那段註解。

   ── min-width: 60px 是有上下界的，不是拍腦袋 ─────────────────────────────────
   下界(floor) = 5 個 badge 值（— / Opus / Sonnet / Haiku / Fable）裡最寬的那個。
     實測（Chromium、本檔真實 CSS、padding:3px 8px / font-size:13px）：
       —29.00  Opus 47.08  **Sonnet 57.20 ← floor**  Haiku 49.25  Fable 48.53
     掃描 56/58/60/62/64，第一個讓 shift 歸零的是 58（56 仍有 1.2px 位移）。
     取 60 是為了留 2.8px 餘裕吸收跨平台字型度量差；58 只有 0.8px，太薄。
   上界(ceiling) = 78px。來自本檔 :789-791 那段媒體查詢的算式：5 元素所需總寬 632px、
     桌機可用寬 = W − 313 ⇒ 等式點 W = 945px；連體加 M 後等式點變 945 + M，
     要讓既有 wrap 窗上界 max-width:1023px 繼續成立需 945 + M ≤ 1023 ⇒ M ≤ 78。
   🔎 **已重算 wrap 窗上界：945 + 60 = 1005 < 1023，故 :789-791 無需調整**
      —— 寫在這裡避免後人以為漏做。
   🔴 值域封閉（後端正規化只吐 Opus/Sonnet/Haiku/Fable/null）是本鎖寬能成立的**前提**。
      若哪天後端把原始值透傳過來（`claude-sonnet-4-5-20250929` 自然寬 182.97px），
      鎖寬會失效、390px header 會折成兩排。守那個前提的是後端的值域斷言，不是這裡。

   ── 顏色紅線 ──────────────────────────────────────────────────────────────
   .ctx-pct[data-level] 的綠/黃/紅是**嚴重度語意**（context 快滿了）。
   （2026-08-25 #1351：判準由 `[data-pct^=...]` 前綴比對改為 JS 算好的 data-level；
     行號原寫 :1486-1491，會漂，已移除 —— 用字串 `.ctx-pct[data-level=` 定位。）
   **不要**依模型家族上色，尤其不要用紅/黃 —— 同一列同一套色票不能同時表達
   「危險」與「身分」。#thread-model 固定用單一中性靛色。
   --------------------------------------------------------------------------- */

/* 🔴 2026-08-19 P151 #1252 —— 下面的 `flex-shrink: 0` **今天是幾何 no-op，刻意保留**。
   別把它讀成「這裡已經有一道生效中的防線」，也別因為「它不做事」就順手刪掉。
   （這段是「保留」與「忘了刪」的差別 —— 沒有它，兩者在程式碼裡長得一模一樣。）

   ── 它是什麼：政策宣告，不是防線 ──────────────────────────────────────────
   #thread-head 自 d582787（2026-08-11，使用者裁定「卡片可見 > 標題完整」）起的規則是
   「**只有 #thread-title 收縮**」。7bb716f（#1023）的 commit body 逐字記載該政策的
   兩種實作：`.fp-btn` 用**顯式** `flex-shrink:0`；`#thread-ctx`／`#thread-viewers`
   靠**隱式** min-content（原文「早已壓到 min」）。`#ctx-group` 是 #1203 把 #thread-ctx
   包起來的新容器 —— 這一行把那個隱式不變量寫成顯式宣告，並對齊旁邊 `.fp-btn` 的慣用法。

   ── 為什麼它今天不做事 ────────────────────────────────────────────────────
   `#ctx-group` 是 flex item、`min-width` 是預設 `auto` ⇒ 自動最小尺寸把它擋在
   min-content；組內兩顆都 `white-space: nowrap` 且無 `overflow: hidden`
   ⇒ min-content ＝ max-content ⇒ **本來就縮不動**，`flex-shrink` 給 0 或 1 都一樣。

   ── 🔴 什麼情況下它會從 no-op 變成載重（＝保留它的理由）────────────────────
   下面兩者**各自單獨就足夠**（2026-08-19 真登入頁實跑，claude2；1024/long 那格）：
     (a) 給 `#ctx-group` 加 `min-width: 0`（或 `overflow: hidden`）——
         典型動機：想讓這顆連體 pill 在窄桌機截斷。
         🟢 實測：flex-shrink 0→1 後 groupW **131.8125 → 66.859375**（Δ-64.95）。
     (b) 拿掉 `.ctx-pct { white-space: nowrap }`（就在下面第 3 行）——
         典型動機：想讓 ctx 百分比可換行。
         🟢 實測：flex-shrink 0→1 後 groupW **131.8125 → 110.03125**（Δ-21.78），
            此時 `#ctx-group` 的 min-width 仍是 `auto`、`#thread-model` 仍鎖 60px。
   反例（**不會**讓它變成載重，別把它當防線的支點）：
     (c) 拿掉 `#thread-model { min-width: 60px }` —— 因為該元素自己也 nowrap，
         min-content 只是退到文字自然寬，組仍縮不動。實測 base 與 NC-L3 幾何逐欄相同。

   ── 誰在看守這件事 ────────────────────────────────────────────────────────
   `web/tests/qa/p151-ncl3-noop-invariant.py`（INV-1）。它斷言的是**因果結構**
   （「拿掉這一行 ⇒ 幾何必須逐欄不變」），不是某一條 CSS，所以 (a)/(b) 哪一個先發生
   都會轉紅；FAIL 訊息直接指回 Workboard #1252。上面 (a)(b)(c) 三組數字就是拿它跑出來的。
   ⚠️ 它只在**該格有收縮赤字**（#thread-title 被裁切）時判得動；沒赤字的格子回
      BLOCKED（不是 PASS）。2026-08-19 全 8 格只有 1024/long 與 1280/long 量得到。

   🔴 ── 下面那一行的**字面**是測試錨點，不要重排 ─────────────────────────────
   `web/tests/qa/p151-model-badge-real-page-probe.py`（`NC_NO_CTX_SHRINK`）拿**下面那一行
   從 `flex-shrink` 起到註解裡 `.fp-btn` 行號為止的整段逐字 bytes**（含宣告與註解之間
   那 8 個空白）當 find 字串做替換。動該行任何 byte 會讓 `no-ctx-shrink_hits=0`，
   整條常駐斷言轉 BLOCKED。
   ⚠️ 本段刻意**不**把那串 bytes 原樣抄在這裡，理由有二，兩個都踩過：
      ① 抄了會被同一次替換命中 ⇒ `no-ctx-shrink_hits` 變 2，替換會打到兩個地方；
      ② 替入字串本身含有註解結束符 ⇒ 會把這個註解區塊從中間截斷，
         後面的 `#ctx-group` 規則整條被 CSS parser 吞掉。
      🟢 2026-08-19 實測踩到 ②：常駐斷言當場回報 `base flex-shrink='1'`（規則沒生效）
         並判 BLOCKED —— 那正是它該有的行為。要看 find 字串原文請開探針那兩行。
   ⚠️ 註解裡的 `.fp-btn:828` 在著作當下（680ad701）精確指到 `.fp-btn` 自己的
      `flex-shrink: 0`，今日該宣告已漂到本檔 :861 —— **行號過期但字串不可改**；
      真要改請連上面那支探針一起改。
   --------------------------------------------------------------------------- */
#ctx-group {
  display: inline-flex;
  /* 非 center：兩半 font-size 不同（13 vs 14），center 會有 1.4px 高低差。 */
  align-items: stretch;
  flex-shrink: 0;        /* 對照 .fp-btn:828；本組不參與收縮 */
  margin-left: 10px;     /* 承接原本 .ctx-pct 的 margin-left（:1462） */
}
#ctx-group > .ctx-pct { margin-left: 0; }   /* 覆蓋 :1462 的 margin-left:10px */
.ctx-pct { white-space: nowrap; }           /* 補 :1460-1475 缺的宣告，防折行 */

#thread-model {
  min-width: 60px;       /* ← 鎖寬。取值依據見上方區塊註解 */
  box-sizing: border-box;
  display: inline-flex;  /* 配合 #ctx-group 的 align-items:stretch 做垂直置中 */
  align-items: center;
  justify-content: center;
  padding: 3px 8px;
  font-size: 13px;
  line-height: 1.4;
  white-space: nowrap;
  border-radius: 4px 0 0 4px;
  background: #e0e7ff;
  color: #4338ca;
  font-weight: 500;
  cursor: default;
  user-select: none;
}
#thread-model + .ctx-pct { border-radius: 0 4px 4px 0; }

/* 空態與「僅登錄值」的**可見**弱化 —— 不能只靠 tooltip，手機沒有 hover。
   2026-08-18 P151 #1225：這 4 個值就是 js/chatroom.js `modelSrcView()` 的封閉
   顯示語義值域（後端來源值域是開放字串，在那支函式收斂）。**四條規則不可少一條**：
   少的那個值會落回瀏覽器預設 opacity:1 ＝ 看起來與「實測到的」同級，而它不是
   （`data-src="statusline"` 在 #1225 修好之前正是這樣，實測 computed opacity=1）。
   ⚠️ 只准用**非版面屬性**：min-width 鎖寬撐著 layout shift = 0px 的驗收
   （#1203），border/padding/字級一動就會位移。unknown 因此只給 opacity。 */
#thread-model[data-src="none"]     { opacity: .45; }
#thread-model[data-src="resolved"] { opacity: .72; border-bottom: 1px dashed currentColor; }
#thread-model[data-src="unknown"]  { opacity: .86; }
#thread-model[data-src="live"]     { opacity: 1; }

/* 2026-08-18 P151 #1241 B-6 —— 「只升不降」命中：畫面顯示的是**最後一次實測值**，
   而實測來源已經沉默（典型觸發就是部署本身：ccms-local restart ⇒ 純記憶體的
   MetricsCache 歸零 ⇒ 後端退回 P 層）。模型名照舊凍住（#1225 缺口 B 的原始目的），
   但**可信度的視覺宣告必須降級** —— 不降的話畫面會用 opacity:1 的 live 樣式繼續
   宣稱「這是實測到的」，而那從守衛命中的那一刻起就不是真的。
   🔴 只用 opacity（**非版面屬性**）—— 與上面四條同一條紅線：min-width 鎖寬撐著
      layout shift = 0px 的驗收（#1203），border/padding/字級一動就會位移。
      刻意**不**沿用 resolved 那條的 dashed border：那會讓 badge 高 1px。
   🔴 必須放在四條 data-src 規則**之後**：兩者特異性相同（1 id ＋ 1 屬性選擇器），
      同分時後宣告者勝。往上搬會讓這條規則對 live 失效（而 live 正是唯一會命中它的值）。
   對應 JS：`js/chatroom.js` 的 `el.dataset.held`（held 時 '1'、否則 delete）。 */
#thread-model[data-held="1"]       { opacity: .72; }

/* 手機點擊展開版號時解除鎖寬 —— 這是**唯一**允許位移的時刻（使用者主動觸發，
   不計入 layout shift = 0px 的驗收，該驗收只涵蓋未套 .expanded 的 5 個值）。 */
#thread-model.expanded { min-width: 0; }

/* ctx 的 "ctx " 前綴改由 CSS 加，且只在 ≥601px 加（spec §5.5）。
   ── 為什麼不用 JS 的 matchMedia 分支 ──
   updateThreadContextBadge() 有 4 個呼叫點，沒有一個掛在 resize 上；JS 版在轉螢幕
   方向後前綴狀態會停在舊值。CSS 版零 JS、零 resize 監聽、零狀態。
   ⚠️ 對應 js/chatroom.js 的 textContent 已改成不帶前綴（`42%` / `—`）。
      兩邊必須同批改：只改一邊會變成「ctx ctx 42%」或手機殘留前綴。 */
@media (min-width: 601px) {
  #thread-ctx::before { content: "ctx "; }
}

/* ≤600px 手機配套 4 項（spec §4.3）。
   🔴 這四項不是打磨，是 AC-M（390px + viewers 顯示時 title ≥ 100px）的**必要條件**：
      逐項實測單獨拿掉後的 titleW —— 📁 59.02／viewers 38.95／ctx 去前綴 80.86／
      第 4 項 98.64，全部 FAIL 掉 100px 門檻（全開時 102.64）。餘裕只有 2.64px。
   🔴 **使用者裁示豁免（2026-08-19，「甲」，見 #1279）—— 知情接受，不是已達成。**
      **上面那組數字的前提是「未讀總數＝0」**，原文一字未刪，此處只補前提與裁示指標
      （2026-08-21 #1333；同一句話在 spec §4.3 已由 #1278／#1332 補過，這裡是最後一個載體）。
      · **豁免的是「要求它達成」，不是宣告它做到了。** 技術上未達成：返回鍵的未讀徽章
        （`.back-unread`）吃掉 31.14px（未讀 1）／44.81px（未讀 99+）；390px ＋ n=1 的
        **真登入頁**實測 titleW ＝ **71.5**（未讀 1）／**57.83**（未讀 99+），兩者皆低於 100
        （#1271 r2 換裝實測）。「全開時 102.64、餘裕只有 2.64px」**只在未讀＝0 時成立**。
      · **門檻已分層（2026-08-18 PO 裁定「丙+乙」，見 #1237）**：n=1 為硬性保證、
        n≥2 只保證 `headH === 60`。該分層同樣落在 #1279 的豁免涵蓋內，不因分層而變成「已達成」。
      · 🔴 **這是產品決策，不是技術辯護**（#1279 明令不得寫成「影響不大」「可接受範圍內」等措辭）。
      · ⚠️ **豁免不等於下面這四項可以拿掉** —— 它們讓出的空間仍是今天 titleW 的全部來源，
        拿掉只會讓一個已經未達標的數字更低。上一條「不是打磨，是必要條件」的警告**完全有效**。
   🔴 不得用「≤600px 隱藏 badge」當省空間的槓桿：使用者是在手機 chat 視窗提這個
      需求的，手機看不到＝需求沒被滿足。 */
@media (max-width: 600px) {
  /* 1) 📁 收成 icon-only（文字節點以 font-size:0 收掉，圖示用 ::before 放回來） */
  #folder-picker-btn { font-size: 0; padding: 4px 8px; }
  #folder-picker-btn::before { content: "📁"; font-size: 15px; }

  /* 2) ctx 去掉 "ctx " 前綴 —— 由上方 @media (min-width:601px) 的 ::before 只在桌機
        生效達成，本區塊不需要任何宣告。留這行註解是為了讓 4 項配套在同一處讀得完。 */

  /* 3) viewers 只留圓點 */
  #thread-viewers .v-label { display: none; }

  /* 4) ctx 的左右 padding 對齊 model 的 3px 8px（正式 .ctx-pct 是 3px 10px）。
        單這一項值 4.00px，而門檻只差 1.36px ⇒ 它是必要項不是保險。 */
  #ctx-group > .ctx-pct { padding: 3px 8px; }
}

/* 2026-05-06 W2-step5: chatroom Web Push permission banner */
.chatroom-push-banner {
  position: fixed;
  bottom: 16px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--bg-elev, #ffffff);
  color: var(--text, #111);
  border: 1px solid var(--border, #ddd);
  border-radius: 12px;
  padding: 8px 12px;
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  box-shadow: 0 4px 14px rgba(0,0,0,0.15);
  z-index: 9999;
  max-width: calc(100vw - 32px);
}
.chatroom-push-banner .cta {
  background: var(--accent-primary, #4f8cff);
  color: white;
  border: none;
  padding: 4px 12px;
  border-radius: 8px;
  cursor: pointer;
  font-size: 13px;
}
.chatroom-push-banner .cta:hover { opacity: 0.85; }
.chatroom-push-banner .dismiss {
  background: transparent;
  color: var(--text-secondary, #666);
  border: none;
  cursor: pointer;
  font-size: 16px;
  padding: 0 4px;
}
.chatroom-push-banner .dismiss:hover { color: var(--text, #111); }

/* ============================================================
   2026-06-10 Operator inline browser CARD (ChatGPT-Operator style).
   The card lives IN the conversation flow (#thread-body), shows a STATIC
   thumbnail (N3) + title; clicking it expands the modal (continuous stream).
   Ephemeral (N4) + creator-only (N2): created/painted only by browser-card.js
   when chatroom.js authorizes the surface for the current (session, creator).
   ============================================================ */
.browser-card {
  margin: 8px 10px;
  border: 1px solid var(--border, #2a2f3a);
  border-radius: 12px;
  overflow: hidden;
  background: #0b0d12;
  box-shadow: 0 2px 10px rgba(0,0,0,0.18);
  cursor: pointer;
  /* 2026-06-10 Ace feedback "卡片的框太小了": enlarge the preview to a comfortable
     size. min(420px, 100%) so it never overflows the message stream on narrow /
     mobile viewports (max-width:100% safety). */
  width: 420px;
  max-width: 100%;
  /* 2026-06-10 visual-qa primary-source DOM fix: #thread-body is a flex column
     (display:flex; flex-direction:column @ :694). This card has overflow:hidden
     (rounded-corner clip), which makes flexbox auto-min-size (min-height:auto)
     collapse to 0; combined with the default flex-shrink:1 the card gets crushed
     to ~2px (just the border) even though .bc-head + .bc-thumb-wrap render full
     height. flex:0 0 auto opts out of column shrink/grow so the card keeps its
     intrinsic content height. (A plain-block fixture can't reproduce this — only
     the real flex-column thread-body does.) Do NOT drop this. */
  flex: 0 0 auto;
  transition: border-color 0.15s, box-shadow 0.15s, transform 0.05s;
}
.browser-card:hover {
  border-color: #2563eb;
  box-shadow: 0 4px 16px rgba(37,99,235,0.25);
}
.browser-card:active { transform: scale(0.995); }
.browser-card:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.browser-card .bc-head {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 9px 12px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-secondary, #9aa4b2);
  background: rgba(255,255,255,0.03);
  border-bottom: 1px solid var(--border, #2a2f3a);
  user-select: none;
}
.browser-card .bc-icon { flex: 0 0 auto; font-size: 15px; }
.browser-card .bc-title {
  flex: 1 1 auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--text, #e6e9ef);
}
.browser-card .bc-expand-hint { flex: 0 0 auto; font-weight: 500; font-size: 12px; opacity: 0.7; }
.browser-card .bc-thumb-wrap {
  position: relative;
  display: block;
  width: 100%;
  /* 2026-06-10 Ace "框太小了": give the thumbnail a fixed 16:10 preview box so it
     reads clearly as a clickable browser preview (was min-height:120px). The img
     object-fit:contain keeps the jpeg undistorted inside this box. */
  aspect-ratio: 16 / 10;
  background: #000;
}
.browser-card .bc-thumb {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: #000;
  /* No src until an authorized thumbnail paints (fail-secure: never flash). */
}
.browser-card .bc-thumb-placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  color: var(--text-secondary, #9aa4b2);
}
/* Once a static thumbnail is painted, hide the "載入畫面中…" placeholder. */
.browser-card.has-thumb .bc-thumb-placeholder { display: none; }

/* ============================================================
   2026-06-10 Operator expand MODAL — hosts the RELOCATED browser viewer panel
   (viewer stream + 操作模式 + ⏹停止) + self-service trigger row. Only ONE
   visible at a time (FW08-MODAL-SINGLE-VISIBLE). Ships display:none (inline) +
   .hidden; chatroom.js openBrowserModal() reveals it (starts the stream).
   ============================================================ */
.browser-modal {
  position: fixed;
  inset: 0;
  z-index: 1200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
}
.browser-modal.hidden { display: none !important; }
.browser-modal .modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0,0,0,0.6);
}
.browser-modal .browser-modal-box {
  position: relative;
  z-index: 1;
  width: min(960px, 96vw);
  max-height: 92vh;
  overflow: auto;
  background: #11141a;
  border: 1px solid var(--border, #2a2f3a);
  border-radius: 14px;
  box-shadow: 0 12px 48px rgba(0,0,0,0.5);
  padding: 36px 12px 12px;
}
.browser-modal .browser-modal-close {
  position: absolute;
  top: 8px;
  right: 10px;
  z-index: 2;
  width: 28px;
  height: 28px;
  font-size: 18px;
  line-height: 1;
  color: var(--text-secondary, #9aa4b2);
  background: rgba(255,255,255,0.05);
  border: 1px solid var(--border, #2a2f3a);
  border-radius: 8px;
  cursor: pointer;
}
.browser-modal .browser-modal-close:hover { color: #fff; background: rgba(255,255,255,0.1); }
/* Inside the modal the viewer img may use more vertical space than the old
   fixed top panel (which capped at 42vh). */
.browser-modal .browser-viewer .bv-img { max-height: 72vh; }

/* ============================================================
   2026-06-09 Operator-controlled browser Phase 1 — read-only viewer panel.
   Fail-secure: .browser-viewer ships display:none (inline) + .hidden; only
   chatroom.js authorize() reveals it. NO interaction styling (read-only).
   ============================================================ */
.browser-viewer {
  margin: 8px 10px 0;
  border: 1px solid var(--border, #2a2f3a);
  border-radius: 10px;
  overflow: hidden;
  background: #0b0d12;
  box-shadow: 0 2px 10px rgba(0,0,0,0.18);
}
.browser-viewer.hidden { display: none !important; }
.browser-viewer .bv-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-secondary, #9aa4b2);
  background: rgba(255,255,255,0.03);
  border-bottom: 1px solid var(--border, #2a2f3a);
  user-select: none;
}
.browser-viewer .bv-live-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #e5484d;
  box-shadow: 0 0 0 0 rgba(229,72,77,0.6);
  animation: bv-pulse 1.6s infinite;
}
@keyframes bv-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(229,72,77,0.55); }
  70%  { box-shadow: 0 0 0 6px rgba(229,72,77,0); }
  100% { box-shadow: 0 0 0 0 rgba(229,72,77,0); }
}
.browser-viewer .bv-title { flex: 1 1 auto; }
.browser-viewer .bv-img {
  display: block;
  width: 100%;
  height: auto;
  max-height: 42vh;
  object-fit: contain;
  background: #000;
  /* READ-ONLY INVARIANT (14e11a9): the stream never captures pointer events by
     default. browser-input.js lifts this to pointer-events:auto ONLY while
     operate-mode is ON (inline style), and restores '' (=> this rule) on OFF.
     Do NOT remove this — OFF must always be read-only. */
  pointer-events: none;
  cursor: default;
}
/* 2026-06-10 Phase 2: while operate-mode is ON, signal interactivity. The
   pointer-events lift itself is applied as an inline style by browser-input.js
   (so the canonical read-only default stays here in CSS); this only restyles
   the cursor + panel chrome. */
.browser-viewer.operating { outline: 2px solid #2563eb; outline-offset: -2px; }
.browser-viewer.operating .bv-img { cursor: crosshair; }
.browser-viewer.operating .bv-head { color: #60a5fa; }

/* 操作模式 toggle — hidden by default (inline + .hidden); chatroom.js reveals it
   only when the feature is permitted (sync()). .on = operate-mode active. */
.browser-viewer .bv-operate {
  flex: 0 0 auto;
  padding: 2px 8px;
  font-size: 11px;
  font-weight: 600;
  line-height: 1.6;
  color: var(--text-secondary, #9aa4b2);
  background: rgba(255,255,255,0.04);
  border: 1px solid var(--border, #2a2f3a);
  border-radius: 6px;
  cursor: pointer;
}
.browser-viewer .bv-operate.hidden { display: none !important; }
.browser-viewer .bv-operate:hover { color: var(--text, #e6e9ef); }
.browser-viewer .bv-operate.on {
  color: #fff;
  background: #2563eb;
  border-color: #2563eb;
}

/* 2026-06-10 ⏹ 停止串流 — explicit teardown button. Hidden by default (inline +
   .hidden); chatroom.js syncBrowserStop() reveals it only when the panel is
   visible AND the feature is permitted. Red accent = destructive teardown. */
.browser-viewer .bv-stop {
  flex: 0 0 auto;
  padding: 2px 8px;
  font-size: 11px;
  font-weight: 600;
  line-height: 1.6;
  color: #f3b0b2;
  background: rgba(229,72,77,0.12);
  border: 1px solid rgba(229,72,77,0.4);
  border-radius: 6px;
  cursor: pointer;
}
.browser-viewer .bv-stop.hidden { display: none !important; }
.browser-viewer .bv-stop:hover { color: #fff; background: #e5484d; border-color: #e5484d; }
.browser-viewer .bv-stop:disabled { opacity: 0.5; cursor: not-allowed; }
.browser-viewer .bv-stop-status {
  display: block;
  padding: 0 10px 5px;
  font-size: 11px;
  color: #f3b0b2;
}
.browser-viewer .bv-stop-status:empty { display: none; }
@media (max-width: 600px) {
  .browser-viewer { margin: 6px 8px 0; }
  .browser-viewer .bv-img { max-height: 32vh; }
}

/* ------------------------------------------------------------------
   2026-06-10 Self-service trigger row (URL input + 啟動串流 button).
   Sits above .browser-viewer; default hidden (inline + .hidden) and
   only revealed by chatroom.js syncBrowserTrigger() when the tri-state
   kill-switch permits. Read-only feature: this row only starts a stream.
   ------------------------------------------------------------------ */
.browser-trigger {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin: 8px 10px 0;
  padding: 6px 8px;
  border: 1px solid var(--border, #2a2f3a);
  border-radius: 10px;
  background: rgba(255,255,255,0.02);
}
.browser-trigger.hidden { display: none !important; }
.browser-trigger .bt-url {
  flex: 1 1 180px;
  min-width: 120px;
  padding: 6px 9px;
  font-size: 13px;
  color: var(--text, #e6e9ef);
  background: #0b0d12;
  border: 1px solid var(--border, #2a2f3a);
  border-radius: 7px;
}
.browser-trigger .bt-url:focus {
  outline: none;
  border-color: #3b82f6;
}
.browser-trigger .bt-go {
  flex: 0 0 auto;
  padding: 6px 12px;
  font-size: 13px;
  font-weight: 600;
  color: #fff;
  background: #2563eb;
  border: none;
  border-radius: 7px;
  cursor: pointer;
}
.browser-trigger .bt-go:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.browser-trigger .bt-status {
  flex: 1 0 100%;
  font-size: 12px;
  color: var(--text-secondary, #9aa4b2);
  min-height: 0;
}
.browser-trigger .bt-status:empty { display: none; }
@media (max-width: 600px) {
  .browser-trigger { margin: 6px 8px 0; }
}

/* ============================================================
   2026-07-08 對話截圖（LINE 式選取 + canvas 重畫縮圖 + composer 附件 + lightbox）
   spec: central/docs/superpowers/specs/2026-07-08-chatroom-conversation-screenshot-design.md
   ============================================================ */

/* --- 縮圖 bubble（kind=image；bubble.chat-image 保留 user/assistant 對齊）--- */
.bubble.chat-image { padding: 6px; }
.bubble.chat-image .chat-screenshot-thumb {
  display: block;
  max-width: 240px;
  max-height: 320px;
  width: auto;
  height: auto;
  border-radius: 8px;
  cursor: zoom-in;
  background: #0b0b10;
}
.bubble.chat-image .cs-caption { margin-top: 6px; padding: 0 6px; }

/* --- 檔案卡 bubble（kind=file；bubble.chat-file 保留 user/assistant 對齊）2026-07-10 --- */
.bubble.chat-file { padding: 6px; }
.bubble.chat-file .chat-file-card {
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: 260px;
  padding: 10px 12px;
  border-radius: 8px;
  background: rgba(0, 0, 0, 0.18);
  border: 1px solid rgba(255, 255, 255, 0.14);
  color: inherit;
  text-decoration: none;
}
.bubble.chat-file .chat-file-card:hover { background: rgba(0, 0, 0, 0.28); }
.bubble.chat-file .chat-file-icon { flex: 0 0 auto; font-size: 26px; line-height: 1; }
.bubble.chat-file .chat-file-name {
  font-size: 15px;
  overflow-wrap: anywhere;
  word-break: break-all;
}
.bubble.chat-file .cs-caption { margin-top: 6px; padding: 0 6px; }

/* --- 選取模式：高亮 + cursor --- */
#thread-body.cs-selecting { cursor: pointer; }
#thread-body.cs-selecting .bubble { transition: box-shadow .12s, outline .12s; }
.bubble.cs-selected {
  outline: 2px solid #60a5fa;
  outline-offset: 1px;
  box-shadow: 0 0 0 4px rgba(96,165,250,0.18);
}
.bubble.cs-head { outline-color: #a78bfa; }

/* --- 浮動操作列（產生截圖 / 取消）--- */
.cs-action-bar {
  position: fixed;
  left: 50%;
  bottom: 84px;
  transform: translateX(-50%);
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 14px;
  background: #1f2937;
  border: 1px solid #374151;
  border-radius: 999px;
  box-shadow: 0 6px 24px rgba(0,0,0,0.4);
}
.cs-action-bar.hidden { display: none; }
.cs-action-bar .cs-hint { font-size: 13px; color: #9ca3af; }
.cs-action-bar button {
  border: none;
  border-radius: 999px;
  padding: 6px 14px;
  font-size: 14px;
  cursor: pointer;
}
.cs-action-bar .cs-gen { background: #4f46e5; color: #fff; }
.cs-action-bar .cs-gen:disabled { background: #3730a3; opacity: 0.5; cursor: not-allowed; }
.cs-action-bar .cs-cancel { background: #374151; color: #e5e7eb; }

/* --- composer 附件 chip（截圖縮圖 + × 移除）--- */
.cs-attach-bar {
  flex-basis: 100%;
  width: 100%;
  order: -1;
  display: flex;
  flex-wrap: wrap;              /* 2026-07-10 B：多附件 chip 一行放不下自動換行 */
  gap: 8px;
  padding: 6px 2px 2px;
}
.cs-attach-bar[hidden] { display: none; }
.cs-attach-chip {
  position: relative;
  display: inline-block;
  border-radius: 8px;
  overflow: hidden;
  border: 1px solid #374151;
  background: #0b0b10;
}
.cs-attach-chip .cs-attach-thumb {
  display: block;
  max-width: 96px;
  max-height: 96px;
  width: auto;
  height: auto;
}
/* 2026-07-10 B：非圖片附件 chip = 迷你檔案卡（圖示 + 檔名，卡在 96px 見方縮圖旁協調） */
.cs-attach-chip.cs-attach-file { background: #1f2937; }
.cs-attach-chip .cs-attach-card {
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: 200px;
  min-height: 40px;
  padding: 8px 28px 8px 10px;   /* 右側留白給 × 鈕 */
}
.cs-attach-chip .cs-attach-icon { flex: 0 0 auto; font-size: 22px; line-height: 1; }
.cs-attach-chip .cs-attach-fname {
  font-size: 13px;
  color: #e5e7eb;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cs-attach-chip .cs-attach-remove {
  position: absolute;
  top: 2px;
  right: 2px;
  width: 20px;
  height: 20px;
  line-height: 18px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: rgba(0,0,0,0.65);
  color: #fff;
  font-size: 14px;
  cursor: pointer;
}

/* --- lightbox（大圖 + 下載）--- */
.cs-lightbox {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
}
.cs-lightbox.hidden { display: none; }
.cs-lightbox-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.82); }
.cs-lightbox-body {
  position: relative;
  z-index: 1;
  max-width: 94vw;
  max-height: 94vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.cs-lightbox-img {
  max-width: 94vw;
  max-height: 82vh;
  border-radius: 8px;
  box-shadow: 0 8px 40px rgba(0,0,0,0.6);
}
.cs-lightbox-actions { display: flex; gap: 12px; }
.cs-lightbox-actions a,
.cs-lightbox-actions button {
  border: none;
  border-radius: 8px;
  padding: 8px 18px;
  font-size: 15px;
  cursor: pointer;
  text-decoration: none;
}
.cs-lightbox-download { background: #4f46e5; color: #fff; }
.cs-lightbox-close { background: #374151; color: #e5e7eb; }

@media (max-width: 600px) {
  .bubble.chat-image .chat-screenshot-thumb { max-width: 180px; max-height: 240px; }
  .cs-action-bar { bottom: 72px; }
}

/* ===================================================================
   2026-08-19 建航：側欄 hover 只亮游標所在那一列，不要整個 cluster 區塊一起亮
   ===================================================================

   症狀：滑鼠停在某個 worker 上，整個 [audio-tool-cluster] 區塊背景一起變亮，
   看起來像「這一整群被選到」，會誤導。

   根因不是顏色，是選擇器命中的層級不對。#worker-list 是巢狀清單：
     <li class="worker-section-header">        ← 分組標題（[audio-tool-cluster]）
     <li class="section-content">              ← 外層 wrapper，包住整個區塊
       <ul class="section-content-list">
         <li data-session-id="...">            ← 內層，才是一列一列的 worker
   CSS 的 :hover 會沿著祖先鏈一路成立，所以第 37 行那條無條件的
   `#worker-list li:hover { background: #1a1a23 }` 在滑到 worker 時**同時命中外層
   wrapper**，整區就一起上色了。

   修法比照 chatroom-light.css 2026-08-11 那組（cyntia 的白底版本）：用
   [data-session-id] 限定。只有 buildWorkerLi() 產生的真正 worker 列有這個屬性
   （js/chatroom.js:1203 `li.dataset.sessionId = w.session_id`），wrapper 與兩層
   section header 都沒有。不需要動任何 JavaScript。

   第二條是補回被第一條連帶關掉的回饋：分組標題原本沒有自己的 hover 底色，
   全靠 li:hover 那條給。改成掛在 .section-toggle（按鈕本身，寬度 100%）上，
   視覺跟以前一樣，但範圍只剩標題那一列，不會延伸到底下的 worker。
   第二層子分組標題（.worker-subsection-header .section-toggle:hover）本來就有
   自己的 #1c1c28，不受影響。

   只有建航會變：兩條都掛在 body.quiet-chrome 底下，而 quiet-chrome 只有
   QUIET_CHROME_USERS（peterparker／建航，見 js/chatroom.js:124）會套上，
   其他使用者的 body 沒有這個 class，兩條一條都不會匹配 —— 他們的行為一個字都沒變。

   同根因的第二處（⋯ 按鈕）在下一組一起修，見本檔末尾。 */
body.quiet-chrome #worker-list li:not([data-session-id]):hover { background: transparent; }
body.quiet-chrome #worker-list .worker-section-header .section-toggle:hover { background: #1a1a23; }

/* 2026-08-19 建航（同日第二批）：⋯ 按鈕也只出現在游標所在那一列
   ===================================================================

   同一個根因的第二個出口。第 312 行
     #worker-list li:hover .row-actions { display: inline-block }
   一樣被外層 wrapper 的 hover 命中，所以滑到任一列時，**整個武將段每一列的 ⋯
   都會一起冒出來**（建航 2026-08-19 的截圖就是現場證據：四列的 ⋯ 同時亮著，
   而他當時只有滑鼠停在其中一列）。實測 computed display 四列全為 block。

   為什麼不能沿用上一組那招（把 wrapper 的 hover 效果設成 transparent）：
   display 沒有「透明」這種中性值，而 .row-actions 掛在 li 的**子層**，
   `li:not([data-session-id]):hover .row-actions` 會把游標所在那一列也一起關掉
   —— 因為那一列同時也是 wrapper 的子孫。所以改成「先全關、再單獨開回來」：

   1. 只要有 li 被 hover，該 li 底下所有 ⋯ 一律關閉（特異性 0,1,3,2）
   2. 用**直接子代 `>`** 把游標真正停的那一列開回來（特異性 0,1,4,2，勝過第 1 條）
      `>` 是關鍵：它讓這條只認「這顆 ⋯ 的爸爸就是被 hover 的那一列」，
      wrapper 的孫子輩吃不到。
   3. 選單已展開的那一列（.menu-open）也要開回來，否則選單開著、
      滑鼠移到隔壁列時，錨定用的 ⋯ 會被第 1 條關掉 → 選單失去定位參考。
      與第 1 條特異性同分，靠**排在後面**取勝，順序不可對調。

   不影響既有 e2e：tests/e2e/long-name-layout.spec.js 的 AC-1～AC-13 走的兩份
   fixture（worker-list-long-name-fixture.html、worker-list-with-sections-fixture.html）
   body 上都沒有 quiet-chrome class，本組三條一條都不匹配，那些測試量到的
   仍是原規則的行為。同理，其他使用者的 ⋯ 行為一個字都沒變。 */
body.quiet-chrome #worker-list li:hover .row-actions { display: none; }
body.quiet-chrome #worker-list li[data-session-id]:hover > .row-actions { display: inline-block; }
body.quiet-chrome #worker-list li.menu-open > .row-actions { display: inline-block; }
