/* 已決事項6（2026-09-11 UI 批）：自架、精簡子集的襯線字型——原本 --serif 只列字型「名字」，
   沒有 @font-face 就是賭裝置剛好內建同名字型，而 Shippori Mincho／Noto Serif TC 都不是
   iOS/Android/桌面系統預裝字型，實務上幾乎必定 fallback 到 -apple-system/system-ui，
   跟設計圖對不起來——這正是這批要修的落差。子集產出檔＝scripts/subset-fonts.mjs（吃劇本
   試算表快照路徑，可重跑），檔案在 public/fonts/，離線可用靠 sw.js 的 SHELL_FILES 快取
   （見該檔，加字型檔進去時要一起調高 SHELL_CACHE 版本號，不然已安裝的 PWA 拿不到新檔）。
   font-display: swap——字型載入完成前先用系統字顯示（不擋渲染），避免白畫面等字型。
   三個字型各自只需要 400（Regular）：全站 var(--serif) 用法沒有任何 font-weight 覆寫。 */
@font-face {
  font-family: "TMWY Serif Spectral";
  src: url("fonts/spectral-subset.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "TMWY Serif Shippori Mincho";
  src: url("fonts/shippori-mincho-subset.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "TMWY Serif Noto TC";
  src: url("fonts/noto-serif-tc-subset.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* 孵化期 UI 進 App：文字規格為正本，動畫/像素級美術非本 sprint 範圍，但視覺外殼
   （天空層＋光叢＋demo v0.4 的字體語言）已在合格線①②範圍內，不再是佔位樣式。
   --bg0/--bg1/--ink/--dim/--hair/--sheet/--glass 由 sky-clock.js 依 JST 時刻覆寫（見該檔
   開頭註解）；這裡的值只是 JS 尚未跑到、或 JS 失敗時的保底（深夜色，同 demo 起始值）。 */
:root {
  color-scheme: dark;
  --bg0: #04060e;
  --bg1: #0a1120;
  --ink: #e7ecf4;
  --dim: #9fb4d8;
  --hair: rgba(159, 180, 216, 0.25);
  --sheet: rgba(6, 10, 20, 0.92);
  --glass: rgba(6, 10, 20, 0.5); /* 已決事項8：光叢毛玻璃層天光色底，見 sky-clock.js 的 glass token */
  /* 順序＝Spectral 先（讓拉丁字元使用 Yves 為 en 指定的字型，而不是被 CJK 字型內建的拉丁
     字符集搶先命中——CJK 襯線字型幾乎都內附一套拉丁字，順序放後面才會生效），再來是既有
     的 Shippori Mincho／Noto Serif TC 順序（沿用原本已經寫好的字型偏好，這批只是把「字型
     名字」換成「真的有檔案支撐的自架字型名字」，不重新決定 CJK 字型的先後）。目前
     app.js 只渲染 line_zh（ja/en 是資料層先備好、前端三語切換還沒做，見 today.ts 開頭
     註解），所以今天實際生效的幾乎全是 Noto Serif TC；Shippori Mincho 先於它列出，跟原本
     系統字版本的既有順序一致，等三語切換做了也不用回頭改這裡。 */
  --serif:
    "TMWY Serif Spectral", "TMWY Serif Shippori Mincho", "TMWY Serif Noto TC", -apple-system,
    system-ui, serif;
  /* 已決3（2026-09-15 交辦書）：跟 body.app-narrative 的 --narrative-top 同一個
     env(safe-area-inset-top) 讀法，但這裡刻意定義在 :root（不綁 body.app-narrative）——
     光叢在敘事畫面掛上那個 class 之前（出生/回歸播放的當下）就要掛載，見 app.js 的
     mountLightCore 呼叫點。JS 側（light-core.js 的 cy()）沒有辦法直接讀 CSS env()，
     用這個變數的 computed style（app.js 的 safeAreaInsetTopPx()）在 px 之間搭橋。 */
  --safe-area-top: env(safe-area-inset-top, 0px);
}
/* 團內頁面共通原則（ttf2026-rules.md §4）：禁長按選字與雙擊縮放，同 lp-lab 的
   tmwy-shared/touch-lockdown.css（2026-09-04-lplab-ui-v1-variants 已決事項6，字面就是
   「全頁禁長按選字」）。已知取捨：這連帶讓台詞文案本身也不能選取複製——lp-lab 那份 report
   已經標記給 Yves 決定要不要對特定文字類元素開放例外（見該檔開頭註解），這裡照抄同一份
   取捨，不在這批自行決定要不要開例外。
   2026-09-11 20:50 追加（Yves 裁定「點小怪獸會出現灰黑色/藍色的被選取圓圈，能不能拿掉」）：
   -webkit-tap-highlight-color:transparent。根因：#light-canvas 的 pointer-events:none
   讓觸碰穿透到下面的 body（見 light-core.css 開頭註解），而 document 層級掛了好幾個 click
   委派（wireInteractionGestures／liveCameraTapHandler 等）——WebKit／Chromium 只要祖先鏈
   上有任何 click 監聽器，就會在實際被點到的那個元素上畫預設的觸碰反白，跟這裡有沒有真的
   處理這次點擊無關。之前只在個別按鈕類別（.option-btn 等）上單點加這個屬性，光叢／背景這種
   沒有專屬 class 的區域沒有涵蓋到——搬到 html,body,* 這條全站規則上，一次補齊所有情況，
   不用逐一堵每個新出現的可點擊祖先。 */
html, body, * {
  touch-action: manipulation;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}
/* 2026-09-11 20:50 追加：橫向捲動的最後一道防線。/code-review 指出只擋 body 不夠——
   iOS Safari 的水平彈性/回彈捲動是由 documentElement（html）本身驅動的，#light-canvas／
   .sky-glass／#interaction-bar／#sky-a／#sky-b 這些用 position:fixed 貼齊視窗的元素也是
   對 html 的可視區域定位，不是 body 的排版盒——只鎖 body 攔不住那個彈性回彈。html／body
   兩層都鎖，不影響任何內部元素自己刻意的橫向捲動容器（例如未來若加時間軸／橫向列表，
   那種容器有自己的 overflow-x 宣告，不受這裡影響）。#app 的 overflow-y:auto 是完全獨立的
   垂直捲動軸，同樣不受影響。 */
html {
  overflow-x: hidden;
}
body {
  margin: 0;
  font-family: var(--serif);
  background: var(--bg0);
  color: var(--ink);
  min-height: 100vh;
  overflow-x: hidden;
}
/* 天空層：demo v0.4 的 .sky，固定滿版鋪在最底——JST 時刻插值見 public/sky-clock.js，
   app.js 的 applySky() 負責把算出的 token 寫到 :root 上。
   已決事項8（2026-09-11 UI 批，天光進場 x-fade）：原本單一 .sky 元素改成 #sky-a／#sky-b
   兩層疊在同一個 z-index——`transition` 只對「同一個屬性的兩個值之間」做插值，
   background-image 的漸層換掉整串字串（bg0/bg1 換了新的 rgb()）不是那種可插值的變化，
   直接對漸層宣告 transition 只會瞬間跳過去，不會淡。改成兩層用 opacity 交叉淡出/淡入
   （app.js 的 applySkyTokens：每次算出新 token 就把新漸層寫到目前隱藏的那層、把它淡入，
   同時把原本顯示的那層淡出）——同一個 z-index、固定 DOM 順序，兩層都在淡入淡出的中途時
   視覺上會自然疊出漸層混色，這就是「天光進場」不再是一次瞬間跳色的機制本身，不是另外疊
   一層特效。初始態（#sky-a opacity:1 帶 :root 保底漸層、#sky-b opacity:0）讓「JS 還沒跑到
   /冷開機重新整頁」那一刻也有個已提交的起始值可以淡——沒有這個起始值，第一次 applySky()
   寫值會被瀏覽器當成「本來就是這樣」直接跳過去，不會有淡入動畫。 */
#sky-a, #sky-b {
  position: fixed;
  inset: 0;
  z-index: -2;
  transition: opacity 1.5s ease-in-out;
}
#sky-a { opacity: 1; background: linear-gradient(to bottom, var(--bg0) 0%, var(--bg1) 100%); }
#sky-b { opacity: 0; }
/* #app 疊在天空層＋光叢層（z-index -2/-1，見 light-core.css）之上，背景保持透明
   讓兩層透出來——既有訊息卡片本來就沒設不透明背景，這裡只需要顯式給層級與定位。
   已決事項3（2026-09-07 UI 節奏批）：光叢固定在畫面上 1/3（light-core.js 的 cy()，
   LIGHT_ANCHOR_Y_RATIO=1/3），台詞與選項留在下 2/3。
   2026-09-07 現場走查發現（Yves 真機測試）：原本用「一次性 padding-top 36vh ＋整頁
   一般文件流」只在第一屏有效——內容累積超過一屏後往下滑，捲動的舊台詞會經過光叢固定
   的螢幕位置，重現「光叢與文字疊在一起」（光叢本身沒有動，是文字捲過去疊上它）。改成
   #app 自己是一個固定在下 2/3、內部自帶捲動的區塊（position:fixed，不是整頁捲動）——
   不管累積多少歷史，捲動永遠被限制在這個區塊內，光叢的上 1/3 保留區不會再被捲入。
   語言選擇／A2HS 引導這些前置畫面也共用同一個 #app，內容通常一屏內，不需要捲動也不受影響。
   2026-09-11（已決事項7）：Yves 與團員看過 8%／15% 視高兩版 `/preview?day=3` 截圖後選 15%
   ——40vh 減 15vh 成 25vh，跟 light-core.css 的 LIGHT_ANCHOR_Y_RATIO 用同一個 15vh 幅度一起
   動（「光叢＋台詞區重心整體上移」是一件事，不是兩個各自獨立的數字）。 */
/* 已決2 B1-14（2026-09-08）：即時取景預覽全螢幕背景——鋪在光叢層（light-core.css 的
   #light-canvas，z-index -1）與 #app 訊息層（z-index 1）之間，z-index 0 剛好卡在中間，
   訊息層本身背景透明（上面 #app 註解），文字照常疊在預覽畫面之上可讀。開關由 app.js／
   test.js 的 openLiveCameraPreview／closeLiveCameraPreview 控制，預設 hidden。 */
#live-camera-bg {
  position: fixed;
  inset: 0;
  z-index: 0;
  background: #000;
}
#live-camera-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
#app {
  position: fixed;
  z-index: 1;
  top: max(1.5em, 25vh);
  left: 0;
  right: 0;
  /* 已決事項3（2026-09-12 bugbatch）：底界曾經改成讀 --bar-h（ResizeObserver 量測
     #interaction-bar 實高寫入），讓兩區永遠相切、不交疊。2026-09-15 交辦書已決4（Yves 裁，
     翻案這條）：相切代表互動區一出現／內容換行變高，#app 的可視框就跟著縮小，敘事文字被
     往上推、讀者要重新找最新一句。改回固定值——#app 的高度不再受互動區影響，
     #interaction-bar 本身的 position:fixed＋z-index:3（見下方規則）疊在 #app（z-index:1）
     之上，允許 overlap，被蓋住的舊句可捲動看。 */
  bottom: 1.5em;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  max-width: 480px;
  margin: 0 auto;
  padding: 0.5em 1em 1em;
}
/* 已決事項2（2026-09-12 bugbatch，spec §1/§2）：玻璃區重做——「整區同一個毛玻璃感，無第四層
   顏色」，取代夜跑批毛玻璃三次修正的裁定（見 .sky-glass 開頭大註解的完整修正史）。
   ⚠️ 只在**真正的敘事畫面**套用這組新幾何（body.app-narrative，由 app.js 的 paintMessages
   在真的要畫台詞/選項時打上，見該函式）——語言選擇（.lang-choose）與 A2HS 閘門
   （.a2hs-hero 等）沿用 #app 上面那條基礎規則（top:max(1.5em,25vh)，跟原本一樣），
   不受這輪玻璃區擴大影響：那些畫面的版面（.a2hs-hero 的 padding-top:170px 等）是照著
   舊的 top/.sky-glass 邊界量出來的，如果新的 top:1.5em＋透明度遮罩套用到它們身上，
   標題會直接落在新的模糊帶＋半透明遮罩範圍內（同一批算過：390×844 下 .a2hs-hero 的內容
   起點落在新玻璃帶 24–361px 區間，遮罩透明度約五成，會讓「讓我出生」標題若隱若現）。 */
/* 已決事項5（2026-09-14 UI 小修批）：敘事頂界要清掉 iOS 狀態列／瀏海——交辦書原文說
   「#app 的 safe-area-inset-top 內距現在就有，不要重做」，但實查（grep env(safe-area-inset-top)
   全檔只有 .a2hs-layer 一處，body.app-narrative #app 這裡原本只有 top:1.5em≈24px）
   發現這個前提對「真正的敘事畫面」不成立：24px 小於 12 mini 這類瀏海機的 safe-area-inset-top
   （約47–50px）。過去這裡沒事是因為 iOS 預設狀態列是不透明、獨立佔版位的（保留區自動讓開，
   不需要 CSS 自己處理）——這批把狀態列改成 black-translucent（見 index.html）之後，網頁
   內容區會展開吃掉原本讓開的那塊，敘事文字沒有這行 safe-area 補償就會鑽到瀏海/時鐘底下。
   這是本批唯一一處與交辦書字面前提不同的修正，report 未解事項會列出。
   定義成 body.app-narrative 自己的自訂屬性，#app／下面的 .sky-glass 都讀同一個值——不在
   兩處各自重複 calc() 公式而有漂移風險（同一批 .sky-glass 上延的修正也需要知道這個頂界
   在哪裡，見該規則）。 */
body.app-narrative {
  --narrative-top: calc(1.5em + env(safe-area-inset-top, 0px));
}
body.app-narrative #app {
  top: var(--narrative-top);
  /* (c)：純模糊只會讓文字輪廓變軟，亮度／存在感不會真的降，這裡再疊一層透明度遮罩——
     跟 .sky-glass 的 backdrop-blur 用同一個高度算式（25vh+150px-narrative-top）對齊，
     兩層才會同步淡完，不會一層淡完另一層還在糊（見 spec §2(c)）。全程只有 backdrop-filter／
     mask-image 兩種不帶顏色的效果，沒有加任何 background／色染（無第四層顏色）。算式用
     --narrative-top 而不是重複寫 1.5em＋safe-area，理由同上面宣告處註解——下緣位置（絕對值
     25vh+150px）不變，只是頂界往下讓出 safe-area 的量，height 跟著等量收縮，兩者相減後
     的下緣算出來還是原本那個位置。 */
  mask-image: linear-gradient(to bottom, transparent 0, black calc(25vh + 150px - var(--narrative-top)));
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, black calc(25vh + 150px - var(--narrative-top)));
}
/* 已決事項8（2026-09-11 UI 批）：光叢毛玻璃層——「台詞被推到光叢後方時＝毛玻璃感，毛玻璃
   的色調要對應當下天光的顏色」（F15＋F11 併成同一件事：backdrop 模糊＋天光色半透明底，
   不是硬切圖層、不是固定灰）。
   為什麼是「蓋一層在 #app 邊界上」而不是「把 #app 整個往上展開到 top:0」：#app 目前
   top:max(1.5em,25vh) 這個邊界本身就是 2026-09-07 現場走查修過的 bug 的解法（見上面 #app
   規則之前那段大註解——文字捲過光叢固定位置的問題）,把它整個展開回 top:0 等於重新打開
   那個已經修掉的洞。改成疊一層固定在邊界上、只往下蓋一小段的毛玻璃面板：
   舊台詞捲到接近 #app 頂緣時會先「經過」這層玻璃再被裁掉，視覺上是淡出／模糊，不是
   硬生生消失在一條線上，同時 #app 本身的裁切行為完全不變，2026-09-07 那個 bug 不會重現。
   ⚠️ 2026-09-11 17:xx 真機修正：第一版 `top:0` 往下蓋整個 25vh 保留區，把光叢本身（錨點
   LIGHT_ANCHOR_Y_RATIO≈8.33%，就活在這個保留區裡）跟著整片模糊掉——A 機真機測試時
   Yves 回報「整個小怪獸沒有顯示出來」，/browse 對照關掉 .sky-glass 前後截圖確認光叢本來
   就在那裡、是被這層毛玻璃自己糊掉的。保留區上半是光叢的地盤，不該被這層動到；毛玻璃只該
   出現在**緊貼 #app 邊界**、文字真的會捲過去的那一小段，改成 `top` 直接等於 #app 的
   `top`（不再往上多蓋）。
   ⚠️ 2026-09-11 18:xx 第二次真機修正：上一版 `height:48px` 是硬邊矩形，靜止時（沒有文字
   剛好捲過）底下只有單純的天空漸層，模糊＋染色疊上去看起來就是一條灰色橫帶——A 機截圖
   Yves 回報「中間有一道...灰灰的」。改成 `height:80px`＋`mask-image`，但 mask 方向是
   「上緣透明→下緣不透明」，於是換來一個新的硬邊（見下一輪修正）。
   ⚠️ 2026-09-12 夜跑批第三次修正（Yves 9/11 晚 A 機驗收＋9/11 20:xx 那句「還是有分界」）：
   上一版的 mask 方向本身就是問題根源——「上緣（貼著#app邊界、內容下一刻就要被 #app 自己
   的 overflow 裁掉）transparent＝完全不模糊」，「下緣（80px 深，其實還在正常閱讀區裡）
   black＝全模糊」，剛好整個反了：快要消失的文字反而清晰、還在正常閱讀位置的文字反而被
   模糊到底，而且這層只蓋到 80px，超過 80px 完全沒有 backdrop-filter，模糊在 80px 那一點
   從「滿」直接跳到「無」，就是 Yves 看到的那條分界。
   修法：(1) mask 方向反過來——上緣（貼著 #app 邊界，內容即將被裁掉）black＝模糊拉滿，
   下緣（貼著閱讀區）transparent＝完全不模糊，跟「band 之外＝零 backdrop-filter」的狀態
   平滑銜接，不再有任何一點是「滿→無」的瞬間跳變。(2) height 80px→150px，讓這段漸層拉
   長，模糊變化更漸進。(3) 拿掉 `background: var(--glass)` 這個固定色染——backdrop-filter
   模糊一段平滑的垂直天空漸層，效果本身就近似「什麼都沒疊」（沒有文字經過時幾乎看不出這層
   存在，天生滿足「沒文字時看不出帶子」的要求），不需要額外的顏色來源，也少一個會跟
   sky-clock.js 的天光色漂移的變數。--glass token（sky-clock.js／app.js／test.js 仍在算）
   目前沒有任何 CSS 規則消費它——不在本批動 sky-clock.js（本次是視覺修正，不是重構），
   保留給 Yves 真機驗收後若這個方向定案，下一批再一併清掉。
   pointer-events:none 同 #light-canvas／#face-emoticon 的既有理由（不擋下面訊息卡片的
   點擊）。z-index 2＝夾在 #app（1）與 #face-emoticon（3，見 light-core.css）之間。 */
.sky-glass {
  position: fixed;
  top: max(1.5em, 25vh);
  left: 0;
  right: 0;
  height: 150px;
  z-index: 2;
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px); /* iOS Safari 是這批鎖定的真機驗證平台，仍需要前綴 */
  pointer-events: none;
  /* 2026-09-12 第三次修正：模糊拉滿的一端貼著 #app 上緣（內容即將被裁掉之處），淡到
     完全透明的一端貼著閱讀區（跟 band 之外「零 backdrop-filter」的狀態平滑銜接）——
     跟上一版方向相反，見上方大註解。 */
  mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
}
/* 已決事項2（2026-09-12 bugbatch，spec §2(a)(b)）：玻璃區擴大——下緣位置（25vh + 150px）
   跟現況完全相同，讀起來不變；blur 12px→28px（Yves 覆核「blur 再強一點，台詞再模糊沒
   關係」）。跟 #app 的新幾何一樣，只在 body.app-narrative（真正的敘事畫面）套用——語言
   選擇／A2HS 閘門維持原本 150px／12px 的玻璃帶，不受影響（那些畫面沒有 #live-camera-bg
   全螢幕預覽的情境，不需要玻璃延到頂，見下面已決6 的理由）。
   已決事項6（2026-09-14 UI 小修批）：上緣延到螢幕最上緣（top:0，含 safe-area／瀏海底下），
   不再從 #app 的敘事頂界（--narrative-top，見 body.app-narrative 宣告處）才開始——
   #live-camera-bg 全螢幕即時取景預覽時，原本 top 以上那段完全沒有這層玻璃，取景畫面清晰
   穿幫（交辦書已決6：「相機全螢幕時最上層是清晰的取景畫面」）。下緣沿用同一個絕對位置
   （25vh+150px），height 因為 top 改成 0 不再需要扣掉 --narrative-top。
   mask 不是把舊的「黑到透明」直接拉滿整段拉長的高度——那樣會讓模糊在到達
   --narrative-top（敘事文字真正開始、也是這條規則的舊頂界）之前就已經開始衰減，相機
   全螢幕時最上層反而比舊版更淡。改成兩段：0 到 --narrative-top 維持滿黑／模糊拉滿
   （新增的「螢幕頂到敘事頂界」純背景區，沒有文字會被過度模糊，相機取景全螢幕整層蓋住）；
   --narrative-top 到 100%（=25vh+150px）沿用 9/12 玻璃捲動規格既有那段線性淡出（貼
   #app 上緣全糊、往閱讀區淡到透明）——形狀（線性、兩端同樣的黑到透明）沒變，但
   --narrative-top 非 0（真機瀏海機的 safe-area-inset-top）時這段淡出的實際跨距會比
   本批之前短（跨距＝100%的高度減掉 --narrative-top，safe-area 越大跨距越窄），不是
   逐位元組不受影響——兩端錨點（貼#app上緣全糊／閱讀區邊界透明）維持對齊才是不變的
   那一半，淡出的漸進感在瀏海機上會略微收緊，這是安全的（更快到全透明，不會變回
   9/12 修過的「一點瞬間跳變」），headless 測不出真實 safe-area 非零值，真機驗收留意即可。
   光叢 canvas（light-core.css 的 #light-canvas，z-index 4）已經高於這層（z-index 2）——
   已決事項1（2026-09-12 bugbatch）把光叢從 -1 拉到 4 就是為了不讓 .sky-glass 蓋過它，
   早於本批，不會重演 2026-09-11 那次「玻璃蓋住光叢讓小怪獸消失」的回歸（見下面舊版基礎
   規則的大註解記載的修正史，那次光叢還是 -1）。 */
body.app-narrative .sky-glass {
  top: 0;
  height: calc(25vh + 150px);
  backdrop-filter: blur(28px);
  -webkit-backdrop-filter: blur(28px);
  mask-image: linear-gradient(to bottom, black 0, black var(--narrative-top), transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, black 0, black var(--narrative-top), transparent 100%);
}
/* 已決10（2026-09-14 追加二，交辦書「牠出生前，畫面上不該有牠」）：出生前那幾頁
   （Landing／A2HS 說明／語言選擇／設定本手機——這幾頁不掛光叢，也還沒進敘事畫面，見
   app.js main() 的 body.app-prebirth 掛載點）沿用同一個 .sky-glass 元素、不是兩套玻璃，
   只是切一個變體 class：上緣延到螢幕頂（含 safe-area），但縮小成只佔畫面上方約 20%、
   模糊退回 12px（比已決事項6剛升級的敘事版 28px 輕一級，跟這批之前的既有透明度一致——
   這幾頁沒有敘事文字會捲過玻璃，不需要那麼重的模糊，25vh+150px 那條舊帶子也確實壓到
   A2HS 說明與「設定本手機」表單的字，交辦書已指出）。
   test.html／preview.html 兩支工具頁沒有這個 body class，維持沿用上面的基礎 .sky-glass
   規則，不受影響。
   2026-09-14 UI bug 批修正（Yves 12 mini 真機截圖，非 standalone Safari 分頁，往下捲到
   Landing 首屏底部）：上一版的高度（20vh + safe-area）與單段 mask（black 0%→transparent
   100%，跟這幾頁沒有 --narrative-top 可對齊的舊理由見上）合起來會漏出一個洞。#app 自己
   捲動裁切的邊界是它的基礎規則 top:max(1.5em,25vh)（這裡沒有 body.app-prebirth #app 覆寫，
   維持這條，恆大於 20vh），而上一版 mask 在自己的下緣（20vh+safe-area）本來就已經淡到
   transparent＝零 backdrop-filter——不管 safe-area 實際多大，mask 在 #app 裁切邊界那個點的
   效果值都不可能是滿的，玻璃下緣跟 #app 裁切邊界之間留了一段模糊效果嚴重不足的空白帶。往下
   捲時 .a2hs-install-prompt 這類元素捲到 #app 邊界會直接被硬裁掉一半、上緣是一條硬邊，不是
   「先經過玻璃淡出再消失」（這條規則存在的目的，同 narrative 版本），這就是那張截圖裡「圓角
   方塊被水平裁掉、只露出下半截」的成因。改法比照 narrative 版本（25vh+150px 對
   --narrative-top）的兩段式 mask 寫法，換算成這裡的等價值：0 到 max(1.5em,25vh)（#app 邊界
   之前，反正還沒進可視區）維持全黑／模糊拉滿，邊界往下再留一小段線性淡到透明——模糊拉滿的
   一端正好貼著 #app 真正裁切內容的邊界，元素捲到那裡之前會先被模糊軟化，不再是硬邊。
   這段重疊量刻意抓小（32px，遠小於已確認會壓到內容的舊值 150px）：這批也查過 body.app-
   prebirth 底下所有沒有 .a2hs-hero／.a2hs-hero-compact 那種大 padding-top 保護的畫面
   （renderMissingTokenRescue／renderNonSafariHint／claim-render.js 那一串 rescue／
   claim-confirm 畫面——這些都直接掛在 #app 或緊跟在它後面，沒有專屬 CSS class、沒有格外
   的頂部留白，第一行字實測落在可視區 y≈211–227，就在 #app 邊界正下方），重疊量抓大會讓這些
   畫面的文字也進到模糊拉滿的區段——這是這批新增的重疊、以前完全不存在，不是「延續舊行為」。
   32px 讓這些畫面在裁切邊界正下方的效果值收斂到很低（同一份 y≈211–227 量測下 alpha 約
   0.1–0.3），同時 .a2hs-hero 類頁面（第一行字 y≈397+）完全不受影響——兩邊都用同一批真機
   對應 viewport（375×812）的 /browse 截圖核對過，見該批 report。高度不再需要
   env(safe-area-inset-top)：#app 的裁切邊界本來就不含 safe-area，兩者對齊不需要它。
   2026-09-15 交辦書已決5（Yves 裁，翻案上面整段調位置的修正史）：v50 這版仍然沒解決
   ——Safari 非 standalone 分頁上「這裡，還是外面」標題仍被一塊模糊帶蓋住。這次不是再調
   高度/mask，是把這層玻璃在 Landing／A2HS／語言選擇／設定本手機這幾頁整個拿掉
   （display:none）：小螢幕上它只會遮視線，這幾頁沒有 #live-camera-bg 全螢幕取景的情境，
   不需要玻璃來銜接捲動裁切邊界（那是敘事畫面 body.app-narrative 才有的需求，不受影響）。 */
body.app-prebirth .sky-glass {
  display: none;
}
/* 已決4（2026-09-16 交辦書）：Landing／A2HS 內容不從頂端開始——根因是 #app 的基礎規則
   top:max(1.5em,25vh) 本來是敘事畫面（body.app-narrative）留給光叢的可讀區起點，出生前
   畫面（body.app-prebirth，見上面已決10/5 兩段大註解）共用同一個 #app、從來沒有專屬的
   top 覆寫，玻璃拿掉之後（上面已決5）這段 25vh 空白更明顯，畫面看起來像是從螢幕四分之一
   處才開始。只補這一頁：敘事畫面的 top:max(1.5em,25vh) 是特意留給光叢的邊界，不受影響。 */
body.app-prebirth #app {
  top: env(safe-area-inset-top, 0px);
}
button {
  font-size: 1em;
  padding: 0.6em 1.2em;
  border-radius: 8px;
  border: 1px solid #444;
  background: #1a1a1a;
  color: #eee;
}
/* ── A2HS 閘門頁（Landing／A2HS 視覺合成批，2026-09-12）───────────────────────
   demo 正本：lp-lab tmwy-landing-demo 的 landing-a.html＋a2hs-2.html（結構／字級語彙），
   只抄 shared.css 的色票數值，不抄它的機制（語言切換／圖待換佔位框——見交辦書已決7/14）。
   全部沿用既有 --ink/--dim/--hair/--serif 等 CSS 變數（已決10：不新增字型檔／不新增色票，
   E10 階段二的三語襯線與天光色系直接沿用）。 */
.a2hs-layer {
  padding: 1em 0 2em;
  /* 完成定義#3：demo 6648b1a 那條 env(safe-area-inset-top) 補強——#app 自己的
     top:max(1.5em,25vh) 在參考機型（390×844）已遠大於瀏海高度，但保留這個防線讓矮螢幕
     機型也不會被狀態列蓋到這一頁最上緣的內容。 */
  padding-top: calc(1em + env(safe-area-inset-top, 0px));
}
/* 已決事項7：「讓我出生」是這一頁最大的元素——加到主畫面的指示是它底下的次要說明，
   讀起來像故事的下一步，不是系統提示。 */
.a2hs-heading { font-size: 1.8em; font-weight: 500; text-align: center; margin: 0 0 0.6em; }
.a2hs-escape, .a2hs-escape2 { font-size: 0.85em; opacity: 0.75; text-align: center; }

/* 首屏（已決3）：標題＋兩則提示＋往下滑，佔滿第一屏。padding-top:150px 讓標題清出
   .sky-glass 的毛玻璃帶（見該規則：top 與 #app 相同、height:150px）——不然頁面一載入、
   還沒捲動的當下，標題就正好落在那條霧化帶的螢幕位置裡，see 交辦書技術線索段。
   Layer 0（in-app browser）與 Layer 2（Android 一鍵安裝）用 .a2hs-hero-compact：
   上面已經有別的內容（提示列＋長按複製說明＋按鈕，或就只是標題本身），不需要再佔滿
   一屏置中。 */
.a2hs-hero {
  /* 2026-09-12 /design-review 修正：原本 min-height:54vh + padding-top:150px 疊加，
     justify-content:center 在「已經被 padding 撐大的盒子」裡置中，會把標題往下推到
     螢幕下半段、跟光叢之間留一大塊死白（390×844 實測：標題掉到 y≈514，捲動提示掉到
     視窗外）。改成不用 min-height 撐版，padding-top 只負責清出 .sky-glass 的模糊帶，
     其餘用 gap 排間距，讓標題／提示列／往下滑三者自然貼著彼此，落在光叢正下方。 */
  padding: 170px 0.4em 1.5em;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 1.1em;
  position: relative;
}
.a2hs-hero-compact {
  min-height: 0;
  padding: 150px 0.2em 0;
  display: block;
  text-align: center;
}
/* 已決3(b)：標題正下方的顯眼提示列——淺色＝比底色淺一階的半透明底，沿天光色系
   （--dim 的 rgb，159,180,216，同 .option-btn:active 既有用法），不用橘（牠在場的固定
   語意保留色）、不用系統警示色（不能讀成錯誤訊息）。字級比標題小一階但仍一眼可見。 */
.a2hs-install-prompt {
  display: inline-block;
  padding: 0.6em 1.1em;
  background: rgba(159, 180, 216, 0.16);
  border: 1px solid var(--hair);
  border-radius: 12px;
  font-size: 1.05em;
  line-height: 1.5;
  color: var(--ink);
}
/* 已決3(c)：往下滑看說明——demo 的 .n-scrolldown 太小太淡，這裡放大到次標層級，
   帶向下箭頭（見 a2hs.js 的 "↓ " 字面前綴），仍在首屏底部。 */
.a2hs-scrolldown {
  font-size: 1em;
  letter-spacing: 0.06em;
  color: var(--dim);
  animation: a2hs-bob 2.4s ease-in-out infinite;
}
@keyframes a2hs-bob {
  0%, 100% { transform: translateY(0); opacity: 0.65; }
  50% { transform: translateY(6px); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .a2hs-scrolldown { animation: none; }
}

/* 已決事項2：Landing A 三段敘事＋A2HS 2 次要說明／預覽／三步驟／收尾，同一個往下捲區塊。 */
.a2hs-narrative { padding-top: 0.5em; }
.a2hs-scene-heading { font-size: 1.35em; font-weight: 500; text-align: center; margin: 1.8em 0 0.6em; }
.a2hs-scene-body { text-align: center; color: var(--dim); font-size: 1em; margin: 0 0 1.2em; padding: 0 0.2em; }
.a2hs-icon-block { display: flex; flex-direction: column; align-items: center; gap: 0.6em; margin: 1.6em 0; }
.a2hs-icon-frame {
  width: 84px;
  height: 84px;
  border-radius: 22px;
  overflow: hidden;
  box-shadow: 0 0 0 1px var(--hair);
}
.a2hs-icon-frame img { width: 100%; height: 100%; display: block; object-fit: cover; }
.a2hs-icon-caption { font-style: italic; color: var(--dim); font-size: 0.98em; }
.a2hs-secondary { text-align: center; color: var(--dim); font-size: 0.95em; margin: 1.6em 0; padding: 0 0.2em; }

/* 已決6：「加好之後長這樣」icon 示意——hs-demo.png（真機主畫面截圖，Vera 的花 icon）。 */
.a2hs-preview { display: flex; flex-direction: column; align-items: center; gap: 0.7em; margin: 2em 0; }
.a2hs-preview-label { font-size: 0.78em; letter-spacing: 0.1em; color: var(--dim); text-transform: uppercase; }
.a2hs-preview-img { width: min(320px, 82%); max-width: 100%; height: auto; border-radius: 12px; border: 1px solid var(--hair); }

/* 已決6/11：三步驟——真步驟圖依 isIOS() 分流（a2hs.js narrativeAndStepsHtml），既有的
   一行手動教學文字（.a2hs-steps）沿用不變，只是搬進這個區塊當圖說。 */
.a2hs-steps-block { margin: 2em 0; padding-top: 1.6em; border-top: 1px solid var(--hair); text-align: center; }
.a2hs-steps-heading {
  font-size: 0.85em;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--dim);
  font-weight: 500;
  margin: 0 0 1em;
}
.a2hs-steps { font-size: 0.92em; opacity: 0.85; margin: 0.3em 0 1em; }
.a2hs-steps-img {
  display: block;
  width: 100%;
  max-width: 100%;
  height: auto;
  border-radius: 10px;
  border: 1px solid var(--hair);
  margin: 0 auto;
}

/* 已決2：三步驟做完的收尾指示——獨立區塊，視覺上跟旁支訊息分開（同 demo .n-nextstep）。 */
.a2hs-nextstep {
  text-align: center;
  font-size: 0.95em;
  margin: 1.8em 0;
  padding: 1em 0.9em;
  border: 1px solid var(--hair);
  border-radius: 12px;
  background: rgba(159, 180, 216, 0.06);
}

/* 已決12：「已經加好了？」＋「我已經裝好了」鈕＋iOS26 接住文案——既有防呆，字面不動，
   只讓樣式跟上面的敘事同一套（同字型、同底色，同 hairline 分隔）。 */
.a2hs-already { text-align: center; margin-top: 1.8em; padding-top: 1.6em; border-top: 1px solid var(--hair); }
.a2hs-already-heading { font-size: 0.92em; color: var(--dim); margin: 0 0 1em; }

/* 按鈕語彙沿用既有 .option-btn 家族的數值（styles.css 上方「按鈕語彙」段，移植自
   lp-lab/tmwy-ui-v1）——不重新發明一套，這裡的按鈕（複製網址／我已經裝好了／安裝）跟
   互動選項按鈕視覺上是同一個世界，只是語意不同（UI chrome vs. 敘事互動），所以獨立一個
   class 而不是直接掛進 .option-btn 家族的選擇器清單。 */
.a2hs-btn,
#a2hs-copy-url,
#a2hs-already-installed,
#a2hs-install-now,
#non-safari-copy-url {
  display: inline-block;
  margin: 0.3em;
  min-height: 44px;
  padding: 10px 20px;
  border: 1.5px solid var(--dim);
  border-radius: 20px;
  background: none;
  color: var(--ink);
  font-family: var(--serif);
  font-size: 13px;
  letter-spacing: 0.1em;
  cursor: pointer;
}
.a2hs-btn:active,
#a2hs-copy-url:active,
#a2hs-already-installed:active,
#a2hs-install-now:active,
#non-safari-copy-url:active { background: rgba(159, 180, 216, 0.18); }
.offline-banner { background: #332; padding: 0.5em; border-radius: 6px; font-size: 0.85em; }
.message { border-bottom: 1px solid #222; padding: 0.75em 0; cursor: pointer; }
.message:focus-visible { outline: 2px solid #c97c5c; outline-offset: 2px; }

/* 已決事項8（2026-09-11 UI 批）：Day N banner＋日期分隔線——兩者都是 app.js paintMessages
   組 innerHTML 字串時插進訊息流裡的**普通元素**（不是固定頁首/CSS ::before），跟著訊息
   一起被推上去、一起被 #app 的既有捲動吃掉，見該函式開頭「banner 是台詞流的一員」註解。
   分隔線只在兩天之間出現（Day 1 之前沒有上一天可分隔），顏色沿用既有的 --hair 髮絲線
   token（#interaction-bar 的 border-top 已經在用），banner 文字用 --dim（次文字色）
   跟一般台詞的 --ink（主文字）區分開——這是敘事節奏的分隔標記，不是台詞本身。 */
.day-banner { text-align: center; color: var(--dim); font-size: 0.9em; letter-spacing: 0.15em; padding: 1.2em 0 0.6em; }
.day-separator { border: none; border-top: 1px solid var(--hair); margin: 1.5em 2em 0; }

/* 已決事項9（2026-09-12 bugbatch，spec §5）：對稱墊片——最新一句台詞預設置中而非貼底，
   需要目標行前後都有足夠的可捲動高度才捲得到（見 centering-scroll.js 開頭的踩坑記錄）。
   前墊片：單元剛開始、內容還很少時，第一句話上面沒有這段高度就捲不到置中位置。
   後墊片：最後一句要捲到置中，下面沒有這段高度會被瀏覽器原生把 scrollTop 夾在
   scrollHeight-clientHeight，捲不下去。50vh 取自 Yves 在 Artifact 雛型上實測的數字，
   跟前墊片同高——兩者本來就是同一個道理的兩端，不是各自獨立的參數。 */
.reveal-spacer { height: 50vh; }

/* B1「牠不在」狀態機。橘色語義鎖定（tmwy-ui視覺決策_v0 §1：橘色一出現＝牠在場），
   所以 present 用橘、away 刻意退成灰——顏色本身就在講牠在不在，不是裝飾。 */
.presence { display: flex; align-items: center; gap: 0.5em; padding: 0.6em 0; font-size: 0.95em; }
.presence-present { color: #c97c5c; }
.presence-away { color: #777; cursor: pointer; }
.presence .critter { width: 10px; height: 10px; border-radius: 50%; background: currentColor; display: inline-block; }
.presence-present .critter { box-shadow: 0 0 8px rgba(201, 124, 92, 0.7); }
/* 打招呼的佔位動畫。正式光叢動畫接 critter.setMood/play 介面（另軌 9/9 前），
   屆時這段 CSS 會被資產替換掉，不是最終視覺。 */
.critter-play { animation: critter-wave 0.6s ease-out; }
@keyframes critter-wave {
  0% { transform: translateY(0) scale(1); }
  40% { transform: translateY(-4px) scale(1.35); }
  100% { transform: translateY(0) scale(1); }
}
/* 已回應的訊息淡一階——「已經玩過」的視覺回饋，不隱藏內容（略過與回應都看得到全文）。 */
.message-seen { opacity: 0.72; }
.manifest-progress, .manifest-error, .manifest-ok { font-size: 0.85em; opacity: 0.8; }

/* ── 孵化期 UI 進 App：內容行渲染（demo v0.4 的 .speak/.opt 字體語言）───────────
   話在中上、選項在下（合格線①）：這裡不強行把訊息清單改成 demo 那種單畫面定格版面
   （見 report「不重寫資料流」的範圍決定），只讓每一列的文字風格照抄 demo——
   台詞置中、serif、寬行距；選項是無底色的文字按鈕，不是實心方塊。 */
/* 已決事項7：letter-spacing 對齊 lp-lab/tmwy-ui-v1 的 .shell-main .speak（demo 的台詞
   字級語彙，.1em）；font-size／line-height 維持原樣不跟 demo 的 15px/2 完全一致——demo
   那份數字是縮在 62vh 高的模擬器畫面裡量出來的，這支 app 是真機全螢幕，17px/2.1 在真實
   裝置上的可讀性已經驗證過（見報告），不屬於「重設計」，是同一套字體語彙在不同容器下的
   合理對應（rules §4 ⑤ 的「移植」不等於「像素完全相同」）。 */
.line {
  margin: 0.6em 0;
  text-align: center;
  font-size: 17px;
  line-height: 2.1;
  letter-spacing: 0.1em;
  color: var(--ink);
}
/* 已決事項1（2026-09-07 UI 節奏批）：逐句釋出——每個敘事列包一層 .line-reveal，state
   由 app.js 的 renderContentLines 依 reveal-pacing.js 算出的時刻決定。current＝正在讀的
   這句，淡入後維持全亮；history＝已經讀過、淡到暗一階，留著可往上滑回看（不是隱藏，
   同一時間只有一句是「現在」，其餘都還在畫面上）。fade-in 用 @keyframes 而非 opacity
   transition：這個 wrapper 是每次 paintMessages 整段 innerHTML 重寫時新建的 DOM 節點，
   transition 需要「先有舊值才能過渡」，新節點沒有這回事，keyframes 在節點插入當下就會播放。
   2026-09-07 加碼（A1 重跑走查發現的閃爍，見 t081/t087/t120/t141 前後截圖）：current 拆成
   current-first／current-played 兩態——同一句在還沒到 historyAt 前被重畫第二次以後
   （輪詢、或選項解決後的重畫），不該再重放淡入動畫，只是靜態維持全亮；「已播過」旗標由
   app.js 的 revealCssState／LINE_PLAYED_KEY 決定，這裡兩個 class 的差異只在有沒有
   animation。 */
/* 已決事項8（2026-09-12 bugbatch，積累#15）：fade in 慢一倍——0.7s→1.4s。只改這個動畫
   時長，跟 reveal-pacing.js／ui-tunables.js 的 PACING_DEFAULTS（逐句釋出間隔）是完全
   獨立的兩個數字，不影響「下一句何時出現」的時間軸模型（見 report #15 根因說明）。 */
.line-reveal-current-first { animation: line-fade-in 1.4s ease-out; }
.line-reveal-current-played { opacity: 1; }
.line-reveal-history { opacity: 0.45; }
@keyframes line-fade-in {
  0% { opacity: 0; transform: translateY(4px); }
  100% { opacity: 1; transform: translateY(0); }
}
.line-option { padding-left: 0; border-left: none; color: var(--dim); text-align: center; }
/* 已鎖定的選項（已決2：分類成 text 後回到會捲動的敘事區，「以我說的樣式進歷史」）——
   純文字 <p>，不是按鈕，跟 .line-reveal-history 的暗一階疊加。 */
.line-option-locked { opacity: 0.5; text-align: center; }
/* F16（2026-09-10 交辦書 F 段第 16 條，E10 交辦書漏抄，2026-09-12 夜跑批補做）：
   「觀眾的每一句回應要有獨立形象（底色或框），一眼分得出哪句是觀眾、哪句是牠」。橘色語義
   鎖定（tmwy-ui視覺決策_v0 §1：橘色一出現＝牠在場，.presence-present 用的 #c97c5c）——
   觀眾這句不能用橘，改用跟 --hair／--dim 同一個冷色系的淡藍卡片，跟牠的暖色語彙明確分家。
   只套在「真的被選中」的那句（.line-option-chosen，唯一算「觀眾說了這句話」的樣式）——
   同組沒被選中的手足（.line-option-locked 但沒有 -chosen）刻意維持原本的純文字暗一階，
   不給底色／邊框：`_screenshots/2026-09-10-verify/README.md` 記的既有觀察「A1-17.a／.b
   兩句都以觀眾說過的話的樣式留著，讀起來像同時說了兩句互斥的話」，根因正是兩者外觀相同；
   只讓真正被選的那句擁有「這是我說的」視覺身分，未選的手足退回純文字，兩者一眼可分。
   width:fit-content＋margin:auto 讓卡片跟著文字長度縮放、置中——不是新的左右對齊語言
   （app 目前全部內容都置中排版，這裡不引入聊天氣泡式的靠左/靠右分欄，維持既有版面語彙）。 */
/* 已決事項7（2026-09-12 bugbatch，積累#13）：report #13 根因——.line 的 line-height:2.1
   是為多行敘事段落設計，套在只有一行短字（如「唉？」）的 chosen 泡泡裡會撐出遠大於文字
   實際需要的垂直空間，跟 .line-option-chosen 自己的 padding 疊加，讀起來像「行距突然
   變大」。只覆寫這個 class 自己的 line-height，不動 .line 基礎行高（不影響一般敘事段落
   讀性）——真機驗證單字（「唉？」）與長句（「我喜歡海喔！」）兩種長度都要看過（見 report
   給 Yves 的真機驗清單）。 */
.line-option-chosen {
  display: block;
  width: fit-content;
  max-width: 84%;
  margin: 0.6em auto;
  padding: 0.5em 1.1em;
  border-radius: 14px;
  line-height: 1.45;
  background: rgba(159, 180, 216, 0.14);
  border: 1px solid var(--hair);
  color: var(--ink);
  opacity: 1;
}

/* 已決事項2（2026-09-07 UI 節奏批）：選項／婉拒／稱呼輸入「送出／跳過」共用同一套按鈕
   語彙——看得見的邊框，跟裸文字的 .line 明確區分（原本 .option-btn 只有一條細底線，
   跟台詞同字體同位置，錄影證據 sheet_02 62 秒抓到「選項像台詞」正是這個問題）。
   已決事項7：數值移植自 lp-lab/tmwy-ui-v1/styles.css 的 .btn-primary/.btn-secondary/
   .btn-choice（演出日 v0 demo 唯一一套「按鈕」語彙）——border-radius 20px（不是滿圓
   pill）、background:none（無底色，靠邊框與間距區分，不是實心填色）、font-size
   13px／letter-spacing .1em，逐字對齊；min-height 44px 是新增（demo 本身多處按鈕也是
   44px，觸控目標尺寸，不算重設計，是補齊既有語彙一直就有的規格）。 */
.option-btn,
.decline-btn,
.camera-btn,
.touch-hold-btn,
.motion-hold-btn,
.fav-unknown-btn,
.skip-wait-btn,
.text-input-form button,
.name-prompt-form button,
.song-link-btn {
  display: inline-block;
  margin: 0.3em;
  min-height: 44px;
  padding: 10px 20px;
  /* 2026-09-07 現場走查發現（Yves 真機測試）：--hair 是 [ink,ink,ink,0.26]（見
     sky-clock.js），白天亮色調時 ink 轉暗、疊在同樣被天空系統推亮的背景上，0.26 的透明度
     幾乎看不出邊框，選項讀起來又變回「看不出是按鈕」。--dim 同樣是 sky-clock 依時刻算出來
     的、但走的是「向背景收 40%」的對比保底公式（tokensForHour 註解：次文字對比保底），
     不是固定低透明度，日夜都留得住可辨識的邊框——不是新發明一個顏色，是換一個本來就在
     系統裡、專門為「該亮/暗但要維持對比」設計的既有 token。 */
  border: 1.5px solid var(--dim);
  border-radius: 20px;
  background: none;
  color: var(--ink);
  font-family: var(--serif);
  font-size: 13px;
  letter-spacing: 0.1em;
  cursor: pointer;
}
/* 按下的回饋刻意不用 demo 的 orange-deep 色調——那個顏色在這支 app 已經是「牠在場」的
   固定語意保留色（B1「牠不在」狀態機，presence-present 用同一色系），借去當按鈕按壓回饋
   會稀釋這個語意，改用中性色（--hair 的加深版）。 */
.option-btn:active,
.decline-btn:active,
.camera-btn:active,
.touch-hold-btn:active,
.motion-hold-btn:active,
.fav-unknown-btn:active,
.skip-wait-btn:active,
.text-input-form button:active,
.name-prompt-form button:active,
.song-link-btn:active { background: rgba(159, 180, 216, 0.18); }
/* v55 任務6：這顆是 <a>，不是 <button>——borrow 同一套按鈕語彙但要補文字裝飾／基準線兩個
   <button> 天生沒有的重設，不然會讀起來像一般連結（底線＋藍色）而不是按鈕。 */
.song-link-btn { text-decoration: none; vertical-align: baseline; }
.option-btn:disabled,
.decline-btn:disabled,
.camera-btn:disabled,
.touch-hold-btn:disabled,
.motion-hold-btn:disabled,
.fav-unknown-btn:disabled,
.skip-wait-btn:disabled,
.text-input-form button:disabled,
.name-prompt-form button:disabled { opacity: 0.5; cursor: default; }
/* 已決2 C1（2026-09-09 完成條件調整，2026-09-16 交辦書已決6 翻案視覺）：按住摸摸看——
   TOUCH_HOLD_MAX_MS（ui-tunables.js，5000ms）跟下面 .touch-hold-ring 的 transition 時長
   一致（5s，見 test/ui-tunables.test.ts 同步鎖）。放開本身就算完成（見 ui-tunables.js
   TOUCH_HOLD_MAX_MS 宣告處大註解），倒數圈不是「要撐到滿才算數」的進度條，而是「按著會
   看到反應」的回饋；放開時 .touch-holding 被移除，圈立刻歸零。
   已決6 翻案：原本用 background-size 從 0%→100% 橫向填色（rgba(159,180,216,0.35)，低
   透明度矩形），Yves 12 mini 真機驗收回報「好像沒成功」——零回饋，改成看得見的倒數圈
   （conic-gradient＋@property 讓百分比可以被 transition 動畫，同 --hold-pct 宣告）。 */
@property --hold-pct {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}
.touch-hold-ring {
  --hold-pct: 0%;
  display: inline-block;
  /* v54（交辦書已決3 第三項）：閒置態 --hold-pct=0% 時 conic-gradient 整圈都是 transparent
     （見上面已決6 註解，0% 停駐點讓 transparent 立刻接手整圈），節點視覺上完全不可見，但
     原本 width:18px + margin-right:0.5em 仍佔走行內排版空間——`.touch-hold-btn` 的
     text-align:center（繼承自 .touch-hold-prompt）把「隱形圈＋文字」當成一個整體置中，
     隱形圈的寬度全部落在文字左側，讓「按住摸摸看」視覺上偏右、左邊多一截解釋不了的空白
     （Yves 12 mini 真機驗收回報的「行內小空格」）。閒置態把寬度／margin 收成 0，讓文字真正
     置中；只有 .touch-holding 期間（圈開始有東西可看）才展開。
     /code-review 抓到（本批修正）：`scheduleRevealTick`（app.js）會在跟按住手勢完全無關的
     時機點（時間軸揭露邊界）呼叫 `paintMessages()` 整段重畫 innerHTML——這支按鈕的模板
     （`renderTouchLine`）永遠印出閒置態節點，`.touch-holding` 是 pointerdown 委派事後才
     imperatively 加上去的 class，重畫換掉節點後這個 class 會消失。修法前（v53）寬度全程
     固定，這種重畫只會讓圈的填色瞬間歸零（既有小瑕疵，本批未新增）；但本批把寬度改成
     隨狀態變化後，若重畫剛好發生在使用者正按著的那 5 秒內，按鈕會瞬間收窄、文字跟著位移
     ——這是本批新增的風險，補一個短暫的 width／margin-right 過渡（跟 --hold-pct 的 5 秒
     無關，只是防止任何一次寬度變化——不論是正常按住開始，還是重畫意外重置——用瞬間跳動
     呈現）。
     v55 任務5（2026-09-17 交辦書已決5）：18px→40px——中央要塞一個「還剩幾秒」的數字，
     18px 圈實測（/preview 截圖）數字完全擠不進去，肉眼看不到；40px 是 C1／B3 共用的尺寸
     （B3 覆蓋層是全螢幕置中的大版面，40px 在那邊也讀得清楚，見下面 .video-countdown 覆寫），
     不是各自挑一個好看的數字。
     /code-review 抓到的真實回歸：height 原本（18px 時代）不參與零寬收合是因為 18px 本身
     沒有超過 `.touch-hold-btn` 其餘內容撐出來的高度，不會被察覺；改成 40px 後，閒置態
     width:0 的隱形圈仍會用 40px 的高度撐開所在那一行的行高，讓整顆按鈕比同一排其他按鈕
     （min-height 44px）明顯更高（headless 量測：閒置態按鈕從 44px 變成約 62px）。height
     現在也要跟著零寬收合一起收（0）、只有 `.touch-holding` 才展開，不能再假設它不參與。 */
  width: 0;
  height: 0;
  margin-right: 0;
  vertical-align: middle;
  border-radius: 50%;
  background: conic-gradient(var(--ink) var(--hold-pct), transparent var(--hold-pct));
  /* 圈本身只留一圈細環（挖掉中央），不是實心圓餅——radial-gradient 當 mask 把中央挖空。 */
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 3px));
  mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 3px));
  transition:
    --hold-pct 5s linear,
    height 0.2s ease,
    width 0.2s ease,
    margin-right 0.2s ease;
}
.touch-hold-btn.touch-holding .touch-hold-ring { --hold-pct: 100%; width: 40px; height: 40px; margin-right: 0.5em; }
/* v55 任務5（2026-09-17 交辦書已決5）：C1／B3 共用「環形進度＋中央剩餘秒數」——中央數字
   是跟 .touch-hold-ring 平行的手足節點，不是它的子節點：上面的 mask 只挖空 .touch-hold-ring
   自己的中央（讓環變成細圈），不會連帶影響手足節點，所以數字疊在上面完全不受影響，不需要
   另外處理層疊。.ring-countdown 只負責讓兩個手足節點疊在同一個位置（position:relative
   當定位錨點），本身不畫任何東西。 */
.ring-countdown { position: relative; display: inline-flex; align-items: center; justify-content: center; vertical-align: middle; }
.ring-countdown-number { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-variant-numeric: tabular-nums; pointer-events: none; font-size: 0.85em; color: var(--ink); }
.touch-hold-prompt,
.motion-hold-prompt { text-align: center; }
.name-prompt { text-align: center; }

/* 已決 E10（2026-09-11，Yves 裁定「12 顆色點排成圓環」）：色環。
   ——12 顆色點沿圓周等距排列（跟 F13 開場動畫「12 顆繞圓入座」同一套視覺語言），中央留白
   放這一列自己的提示文字（A1-25「喜歡的顏色是？」／A1X-19「借給牠一個你喜歡的顏色」——
   那是給觀眾看的真句子，不是舞台指示，不能不印）。色值來自 ui-tunables.js 的
   COLOR_RING_CHOICES（起點＝light-core 的暖色極＝牠自己的橘），不在這裡寫死。
   排法＝色環在上、「還不知道」在下各自一行（Yves 2026-09-11 選的那個排版）：色環與等待列
   雖然是同一組（isSameSkipGroup，兩顆都留在互動條上），但 groupInteractiveEntries 只把
   「同一個 base id 的 .a/.b」並排成 .option-row，A1-25／A1-26 沒有那種字尾，所以自然各自
   一行。flex-basis 留著只是保險：萬一之後真的被包進某個 flex 容器，色環要獨佔整列，不能
   跟按鈕並排被擠掉。
   尺寸：圓半徑 98px、色點 40px ⇒ 相鄰色點間距 2π×98÷12 ≈ 51px，比色點本身大，手指不會誤觸
   旁邊那顆（觸控目標沿用既有按鈕的 44px 精神：40px 圓＋間距，不是 44px 方形）。 */
.color-ring-prompt { flex-basis: 100%; text-align: center; }
.color-ring {
  position: relative;
  width: 240px;
  height: 240px;
  margin: 0.2em auto;
}
/* 中央的提示文字——絕對定位在圓心，不佔 .color-ring 的排版流（色點也都是絕對定位）。 */
.color-ring-center {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 9em;
  margin: 0;
  font-size: 13px;
  letter-spacing: 0.1em;
}
.color-ring-dot {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 40px;
  height: 40px;
  margin: -20px;
  padding: 0;
  border: 1.5px solid var(--dim);
  border-radius: 50%;
  background: var(--c);
  cursor: pointer;
  /* 第一個 rotate 把色點送到圓周上的角度位置、translateY 推出半徑、第二個 rotate 把色點自己
     轉回來（圓形其實看不出旋轉，留著是為了之後若改成非圓形色片不用重推公式）。--i＝第幾顆。 */
  transform: rotate(calc(var(--i) * 30deg)) translateY(-98px) rotate(calc(var(--i) * -30deg));
}
.color-ring-dot:active { box-shadow: 0 0 0 3px rgba(159, 180, 216, 0.5); }
.color-ring-dot:disabled { opacity: 0.5; cursor: default; }

/* 已決 E10：文字輸入（C2-31「選擇拍下了什麼呢？」＋C2-32 等待列）——版面規格逐條沿用稱呼
   表單（已決14「縮成一列」量過的那一版），只有 maxlength 不同（50 vs 12），所以輸入框寬度
   給多一點 flex-basis。 */
.text-input-prompt { flex-basis: 100%; text-align: center; }
.text-input-form { display: flex; flex-direction: row; flex-wrap: wrap; align-items: center; justify-content: center; gap: 0.5em; }
.text-input-field {
  background: none;
  border: none;
  border-bottom: 1px solid var(--hair);
  color: var(--ink);
  font-family: var(--serif);
  font-size: 1em;
  text-align: center;
  padding: 0.4em;
  flex: 1 1 10em;
  min-width: 8em;
}
/* 已決事項14（2026-09-08 測試頁批，9/7 新增四小項）：原本 column 排列＝輸入框／送出／
   跳過各佔一列，量出來吃掉視窗高度 24%——改 row（allow wrap 給窄螢幕退路），縮成一列。 */
.name-prompt-form { display: flex; flex-direction: row; flex-wrap: wrap; align-items: center; justify-content: center; gap: 0.5em; }
.name-prompt-input {
  background: none;
  border: none;
  border-bottom: 1px solid var(--hair);
  color: var(--ink);
  font-family: var(--serif);
  font-size: 1em;
  text-align: center;
  padding: 0.4em;
  /* /code-review 發現：原本固定 width:8em 是量出來、配合今天的按鈕文案長度／字級調的魔術數字
     ——按鈕文案改了語言、或按鈕字級/padding 之後再調，這個假設會無聲失效。改用 flex 讓輸入框
     在兩顆固定尺寸的按鈕之間彈性吃掉/讓出剩餘寬度，min-width 保留最低可用的輸入區（maxlength
     12，中文全形字通常不需要 12em 那麼寬，6em 在窄螢幕上仍可讀）。 */
  flex: 1 1 6em;
  min-width: 6em;
}

/* 已決事項2：固定底部區塊——選項／婉拒／稱呼輸入的形狀、位置固定在畫面下方，跟上面
   會捲動的台詞明確分開（不是同一段 innerHTML 裡穿插一列，是完全獨立、掛在 body 的節點，
   見 app.js 的 interactionBarEl）。沒有內容時完全不佔位（display:none，不留一條空白列）；
   有內容時才顯示，蓋在 #app 的捲動內容之上，safe-area 比照既有 .diag-card 慣例。
   2026-09-11 20:50 追加（Yves 裁定）：整個區塊再往畫面上方收一點，離開螢幕最底緣——原本
   底緣 padding 只有 0.7em，在沒有 home indicator（沒有 safe-area-inset-bottom）的裝置上
   按鈕幾乎貼著螢幕邊緣，落在「要伸長拇指」而非「自然落點」的範圍。把底緣 padding 加大到
   1.6em，safe-area 仍疊加在後面（notch 裝置維持既有行為，只是額外多出這段固定間距）；
   `bottom` 本身維持 0，改的是內距不是定位模型，跟 z-index／捲動邊界都不受影響。 */
/* 已決事項3（2026-09-12 bugbatch，Yves 9/12 晚裁）：「維持下方選項區域足夠高、不讓觀眾難
   操作」——先強制拉高，看了才知道對不對。這組數字（min-height/padding）沿用 Yves 在
   Artifact 雛型上實測的起點（spec §3），**不是定案**，正式數字待 /design-review 真機
   拇指觸及範圍核對後另外覆寫（見該 skill 的最後一次 commit 與 report 的理由說明）。 */
#interaction-bar {
  display: none;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 3;
  min-height: 140px;
  background: var(--sheet);
  border-top: 1px solid var(--hair);
  padding: 2em 1em calc(5em + env(safe-area-inset-bottom, 0px));
  text-align: center;
}
/* /design-review 發現：min-height:140px 是 content-box（不含上下 padding），單一選項這種
   矮內容（~49px）會被 block 版式硬撐到 140px 高、由上往下排——多出來的 ~91px 空隙整段落在
   內容「下面」、擠在已經有的 5em 底 padding「上面」，於是按鈕反而貼近整條窄帶的頂緣，離螢幕
   底緣（拇指真正容易觸及的地方）比看起來的還遠（實測 390×844：按鈕距離螢幕底緣 167px）。
   改成 flex 直欄置中：內容量少時置中在這段高度中央，比起貼頂更接近可及範圍；內容量多
   （色環、文字輸入＋按鈕）時 flex 置中一樣成立，不影響既有 .option-row／.name-prompt-form
   內部的橫向排版。min-height／padding 兩個數字本身維持 Yves 的雛型起點不動——那是需要真機
   量測拇指觸及範圍才能定案的部分，這裡只修版式安排本身的臭蟲，不是在猜那兩個數字對不對。 */
#interaction-bar.interaction-bar-visible {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.6em;
}
/* 「.a/.b 互斥選項並排」——同一組相鄰選項包成一行，flex-wrap 讓長文案在窄螢幕上自動換行
   而不是被硬擠在一行。 */
.option-row { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.2em; }

.error-banner { color: #c0392b; }
.banner { font-size: 0.9em; }
.banner button { margin-left: 0.5em; }

/* 已決2 B1（2026-09-08）：拍照覆蓋層——刻意獨立掛在 document.body（見 app.js 的
   openCameraCapture 開頭註解：不能是 interactionBarEl/messagesContainer 那種每次重畫就整段
   覆寫 innerHTML 的節點，否則使用者拍照拍到一半、10 秒前景 poll 一到 live 的相機畫面就會被
   砍掉重來）。z-index 蓋過 #interaction-bar（z-index:3）與 A2HS gate。 */
.camera-overlay {
  position: fixed;
  inset: 0;
  z-index: 20;
  background: rgba(0, 0, 0, 0.82);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1em;
}
.camera-panel {
  width: 100%;
  max-width: 480px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.8em;
}
.camera-preview,
.camera-captured {
  width: 100%;
  max-height: 60vh;
  border-radius: 8px;
  background: #000;
  object-fit: cover;
}
.camera-status { color: #fff; font-size: 0.85em; text-align: center; min-height: 1.2em; }
.camera-actions { display: flex; gap: 0.5em; }
.camera-shutter,
.camera-cancel {
  min-height: 44px;
  padding: 10px 20px;
  border: 1.5px solid #fff;
  border-radius: 20px;
  background: none;
  color: #fff;
  font-family: var(--serif);
  font-size: 13px;
  letter-spacing: 0.1em;
  cursor: pointer;
}
.camera-shutter:disabled { opacity: 0.5; cursor: default; }

/* 「照片留畫面」（已決2 B1 完成定義）：B1-32 這一列的顯示元件，跟裸文字的 .line 明確區分。 */
.photo-display img { max-width: 100%; border-radius: 8px; display: block; margin: 0 auto; }

/* A2 四樣已決事項1：medium='image'（A2-20／A2-40）——同 .photo-display img 既有語彙。
   .line-image-fallback 預設 hidden，img 的 onerror（見 app.js 的 renderImageLine）失敗時
   才現身；素材未登記那條退路（imageLineAsset 回 null）直接印成 <p class="line
   line-image-fallback">，不套這裡的 img 相關規則，本身已經是 .line 的裸文字樣式。 */
.line-image img { max-width: 100%; border-radius: 12px; display: block; margin: 0 auto; }
.line-image .line-image-fallback { margin-top: 0.4em; }

/* 已決2 B2（2026-09-09，取代已決2原本「錄音＋播回」規格）：錄音覆蓋層——沿用 .camera-overlay／
   .camera-panel／.camera-status／.camera-shutter／.camera-cancel 既有樣式（見 app.js 的
   openAudioRecorder，按鈕直接掛雙 class），這裡只補錄音特有的兩塊：計時顯示、試聽播放器，
   跟按鈕列在窄螢幕上最多同時 4 顆（試聽／重錄／確認送出／取消）需要能換行。 */
.audio-timer { color: #fff; font-size: 1.4em; font-variant-numeric: tabular-nums; text-align: center; }
.audio-playback { width: 100%; max-width: 320px; }
.audio-actions { flex-wrap: wrap; justify-content: center; }
.audio-record-prompt { text-align: center; }

/* 已決2 B3（2026-09-14，交辦書 2026-09-14-tmwy-b3-video-element）：錄影覆蓋層——沿用
   .camera-overlay／.camera-panel／.camera-status／.camera-shutter／.camera-cancel 既有樣式
   （見 app.js 的 openVideoRecorder，按鈕直接掛雙 class，同 B2 錄音既有手法），這裡只補
   錄影特有的三塊：取景/播放舞台（含無聲小標）、倒數數字、按鈕列換行。 */
.video-stage { position: relative; width: 100%; }
/* 已決13：預覽左右鏡像，存檔不鏡像——CSS transform 只影響顯示，MediaRecorder 錄的是相機
   原始畫面位元組，不受這裡影響（見 app.js 開頭大註解）。 */
.video-live-preview,
.video-playback {
  transform: scaleX(-1);
}
.video-playback { width: 100%; max-height: 60vh; border-radius: 8px; background: #000; object-fit: cover; }
.video-silent-badge {
  position: absolute;
  top: 0.5em;
  right: 0.5em;
  margin: 0;
  padding: 0.2em 0.6em;
  border-radius: 10px;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  font-size: 0.75em;
  letter-spacing: 0.05em;
}
/* 已決59（2026-09-14）翻案（v55 任務5，2026-09-17 交辦書已決5）：Yves 明確要求 B3 倒數
   跟 C1 共用「環＋中央秒數」，不再是「不用動畫」——沿用 .touch-hold-ring 已經在 C1 上線
   過的同一條 --hold-pct 5s linear transition（見上方大註解），是平滑的線性填色，不是已決59
   原本想避免的那種抖動/彈跳動畫，也沒有新增 keyframes，prefers-reduced-motion 維持不特別
   處理（跟 C1 既有的同一個判斷一致）。.counting class 觸發填滿（同 C1 的 .touch-holding），
   由 startCountdown／test.js 的鏡像函式切換。 */
/* /code-review 抓到的真實回歸：這裡原本跟著 v53 就有 `color: #fff`，本批誤刪，改成讓環
   （.touch-hold-ring 的 conic-gradient 背景）與數字（.ring-countdown-number）都繼承
   `var(--ink)`——但 --ink 是 applySkyTokens／sky-clock.js 依 JST 時刻算出來的天光色，白天
   會轉成接近黑色（INK_D）。.camera-overlay（styles.css:876）背景是固定的
   `rgba(0,0,0,0.82)`，不看時刻——所以白天錄影時圈跟數字都會是黑色疊在黑色背景上，肉眼看
   不到（headless 截圖核對過）。同層 .camera-status／.camera-shutter／.video-silent-badge
   早就為了同一個理由各自硬寫 `color:#fff`，不吃 --ink。這裡改成局部覆寫 --ink 本身（CSS
   自訂屬性可以被子樹重新定義）：.touch-hold-ring／.ring-countdown-number 兩個手足節點都讀
   同一個 var(--ink)，只要在這個覆蓋層的祖先重新定義它，兩處會自動一起變白，不用各自加
   color 覆寫，也不會因為換了個寫法又漏掉其中一個。 */
.video-countdown { --ink: #fff; text-align: center; margin: 0; }
/* v55 任務5：數字大小（0.85em）直接吃 .ring-countdown-number 的預設值，跟 C1 刻意長得
   一樣。寬高不能同樣不覆寫——.touch-hold-ring 預設 width:0（C1 專用的零寬收合，只有
   `.touch-hold-btn.touch-holding` 這個祖先組合才會展開回 40px），B3 沒有 .touch-hold-btn
   這層祖先，套不進那條規則，若不覆寫寬度會整顆圈維持 0 寬、完全看不到（不是百分比填色
   的問題，是圈本身的尺寸沒展開，/preview 靜態頁核對過這個回歸）。B3 不需要零寬收合——
   `hidden` 屬性已經處理了「還沒開始倒數時完全不顯示」，圈本身的尺寸不必再看 .counting，
   只有 --hold-pct（填色進度）才看。 */
.video-countdown .touch-hold-ring { width: 40px; height: 40px; margin-right: 0; }
.video-countdown.counting .touch-hold-ring { --hold-pct: 100%; }
.video-actions { flex-wrap: wrap; justify-content: center; }
.video-record-prompt { text-align: center; }

/* ── H.22 左下信號燈＋長按診斷 card ──────────────────────────────────────
   設計意圖（交辦書 H.22）：全綠時近隱形，異常才醒目。所以 ok 態刻意做到很低調
   （小、半透明、無光暈），warn/error 才給尺寸、飽和度與光暈。若哪天覺得「綠燈看不太到」
   ——那是對的，那正是它該有的樣子。 */
/* 「？」＝唯一的系統入口（demo v0.4 #qmark，交辦書已決6：不得再多一個入口）——沿用既有
   H.22 signal-light 的短按/長按邏輯與 DOM（diagnostics.js 完全不動），只把外觀從
   「小圓點」換成 demo 的明朝字符圓框，全綠時近隱形、異常才醒目的既有設計意圖不變，
   只是「醒目」現在是換色（color），不是變大／發光。
   已決事項4（2026-09-12 bugbatch，積累#18）：內容從固定的單字「？」換成
   presenceBannerHtml 同一套三語文案（「牠在。」／「牠先走了。」等，見 diagnostics.js 的
   paintPresenceLabel／presence-copy.js）——40×40 的正圓裝不下一整句話，改成
   min-width:40px 的膠囊形，寬度隨文字內容撐開，高度維持既有的 40px 觸控目標不變。
   這個形狀改動只是本批的第一版起點，非定案，字級/寬度真機讀起來太擠的話留給
   /design-review 再調。 */
.signal-light {
  position: fixed; left: 22px; bottom: calc(22px + env(safe-area-inset-bottom, 0px)); z-index: 40;
  min-width: 40px; height: 40px; padding: 0 12px;
  border: 1px solid var(--hair); border-radius: 20px; background: none;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.signal-light::after {
  /* 已決事項4：content 讀 data-presence-label（diagnostics.js 每次狀態更新就寫入這個
     attribute），不再是寫死的「？」——CSS content:attr() 對 content 屬性本身是廣泛支援的
     既有語法，不是新的 attr()-for-any-property 提案，跨瀏覽器沒有相容性疑慮。 */
  content: attr(data-presence-label); display: block;
  font-family: var(--serif); font-size: 13px; line-height: 1; white-space: nowrap;
  color: var(--dim); opacity: 0.55;
  transition: color 0.2s, opacity 0.2s, transform 0.2s;
}
.signal-light[data-level="warn"]::after { color: #c9a227; opacity: 1; }
.signal-light[data-level="error"]::after { color: #c0392b; opacity: 1; }
/* 已決事項3（2026-09-14 UI 小修批）：短按放大回饋的 class 與規則整條拿掉——狀態字不是按鈕，
   短按不該有任何視覺回饋（見 diagnostics.js 的 pointerdown/click 監聽器同批修改的註解）。 */

.diag-card {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 41;
  background: #10161d; border-top: 1px solid #223;
  padding: 0.9em 1em calc(0.9em + env(safe-area-inset-bottom, 0px));
  transform: translateY(100%); transition: transform 0.22s ease-out;
  font-size: 0.9em;
}
.diag-card-open { transform: translateY(0); }
.diag-perms { list-style: none; margin: 0 0 0.7em; padding: 0; }
.diag-perms li { display: flex; justify-content: space-between; padding: 0.25em 0; border-bottom: 1px solid #1b2530; }
.diag-perms .diag-ok { color: #9ab; }
.diag-perms .diag-bad { color: #c0392b; }
.diag-perms .diag-idle { color: #667; }
/* 預載細條：不寫字，判讀規則是「沒跑滿就有事」（8/24 拍板） */
.diag-bar { height: 3px; background: #1b2530; border-radius: 2px; overflow: hidden; }
.diag-bar i { display: block; height: 100%; background: #3f7a4f; transition: width 0.3s; }
.diag-bar-bad i { background: #c0392b; }
/* 強制 load：收在工程深層＋兩段式，觀眾可見區防誤觸（8/24 拍板） */
.diag-force { margin-top: 0.8em; display: flex; align-items: center; gap: 0.6em; }
.diag-force-reveal { background: none; border: none; color: #445; font-size: 1.1em; cursor: pointer; padding: 0 0.3em; }
.diag-force-go { background: #2a1a18; color: #c0392b; border: 1px solid #4a2420; border-radius: 4px; padding: 0.35em 0.7em; font-size: 0.85em; cursor: pointer; }

/* ── H.21 語言選擇（孵化第一動）────────────────────────────────────────── */
.lang-choose { display: flex; flex-direction: column; gap: 0.75em; padding: 2.5em 0; }
.lang-hint { color: #889; font-size: 0.9em; margin: 0 0 0.5em; }
/* 每個選項用該語言自己的字寫，使用者不需要先懂介面語言就選得下去 */
.lang-opt {
  background: #16202a; color: #e8e8e8; border: 1px solid #26323e;
  border-radius: 6px; padding: 0.85em 1em; font-size: 1.05em; cursor: pointer;
}
.lang-opt:active { background: #1d2a36; }
.lang-note { color: #667; font-size: 0.8em; margin: 0.4em 0 0; }
.lang-loading { padding: 2.5em 0; }
.lang-progress-host { margin: 0.8em 0; min-height: 1.2em; }

/* 換語言：收在診斷卡內（長按之後）＋兩段式展開 */
.diag-lang { display: flex; align-items: center; gap: 0.6em; padding: 0.3em 0 0.6em; border-bottom: 1px solid #1b2530; }
.diag-lang-current { color: #9ab; margin-left: auto; }
.diag-lang-reveal { background: none; border: 1px solid #26323e; color: #778; border-radius: 4px; padding: 0.2em 0.6em; font-size: 0.8em; cursor: pointer; }
.diag-lang-opts { display: flex; flex-wrap: wrap; gap: 0.5em; padding: 0.6em 0; }
.diag-lang-warn { color: #c9a227; font-size: 0.8em; width: 100%; margin: 0; }
.diag-lang-pick { background: #16202a; color: #e8e8e8; border: 1px solid #26323e; border-radius: 4px; padding: 0.35em 0.8em; cursor: pointer; }
.diag-lang-pick:disabled { opacity: 0.4; cursor: default; }

/* C2 預載分層明細 */
.diag-layers { list-style: none; margin: 0.5em 0 0; padding: 0; font-size: 0.85em; }
.diag-layers li { display: flex; justify-content: space-between; padding: 0.15em 0; }
.diag-blocking { color: #c9a227; font-size: 0.8em; margin: 0.4em 0 0; }
/* 已決7（2026-09-16 交辦書）：最近一次上傳耗時——同 .diag-layers 的工程資訊字級，不搶
   permissions／preload bar 這兩個真正影響燈號的區塊視覺重量。 */
.diag-last-upload { font-size: 0.85em; margin: 0.4em 0 0; opacity: 0.8; }
.diag-light { font-size: 0.85em; margin: 0.4em 0 0; opacity: 0.8; word-break: break-all; }

/* 已決 C8（2026-09-10）：診斷卡裡的「允許」按鈕——通知許可的常駐按鈕收掉之後唯一的補救
   入口（劇本 A1-44／A1-45 指的權限管理區）。跟 .diag-lang-reveal 同一個視覺層級：這是
   卡片內的次要動作，不該搶走權限狀態本身的注意力。 */
.diag-perms .diag-perm-ask {
  margin-left: 0.6em; padding: 1px 8px;
  font: inherit; font-size: 11px; line-height: 1.6;
  color: #cfe3ff; background: none;
  border: 1px solid #3a4a5e; border-radius: 3px;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.diag-perms .diag-perm-ask[disabled] { opacity: 0.45; cursor: default; }
