/* ============================================================
   HHG Design System — components.css
   v1.18.0 (2026-08-29)
   ⚠️ 這一行是**庫的版本號**，跟著 GUIDELINE.md 的 Changelog 走，改本檔就要一起改。
      （v1.4.0–v1.14.0 期間它漏更新、停在 v1.3.0 停了十二版，2026-08-24 補正。
        這是 tokens.css 同一種漂移的再犯——**改 css 就改檔頭版本**。）

   依賴 tokens.css。全部 class 以 ds- 前綴。
   交互模型參考 OMS /mapping 盤點（04_設計/OMS元件盤點.md @ Permission Request 專案）。

   🔴 ============ 本檔已於 2026-08-24 進入凍結狀態 ============ 🔴

   元件層導入拍板走「形態乙」：元件改寫成 React 元件、樣式用 Tailwind v4
   utility class 寫在元件裡，透過 shadcn registry 分發。**本檔逐族退役。**

   ⇒ 本檔自此的角色是：
      1. **服務 vanilla 試版本／風格探索那一層**（開檔即看，只判斷 token）
      2. 尚未遷移的元件族的現行實作

   ⇒ 本檔**不再是元件層的事實來源**。已遷移的族，事實來源是 registry 裡的
      React 元件；本檔對應那幾族的規則會被刪除。

   ⚠️ **不要在本檔新增元件族。** 新元件一律直接寫成 registry 的 React 元件。
      要改既有未遷移族的樣式仍改這裡（並補 Changelog）。

   🔴 **凍結不等於不修 bug（v1.18.0 立此原則）。** 本檔在 v1.15.0–v1.17.0 期間是零改動，
      v1.18.0 動了兩條——都在 `≤820` 抽屜段、都是修 `data-shell="plain"` 的 specificity bug
      （合併選擇器讓 plain 分支拿到 (0,3,0)，勝過 `.ds-side.is-open` 的 (0,2,0)
       ⇒ plain 的抽屜打不開；同一原因讓 `prefers-reduced-motion` 對 plain 失效）。
      判準：**凍結擋的是「新增元件族」與「順手改外觀」，不擋「兩層一起修一個真 bug」。**
      ⚠️ 而且這一類一定要**兩層一起改**：只改 registry 元件層＝兩層行為分岔，
        而分岔不會被任何測試抓到（兩邊各自都「看起來對」）。見 GUIDELINE 2-3 護欄 9。

   ⚠️ **不要把本檔當「第二份真相」去跟 registry 元件對齊。** 過渡期兩者會不一致，
      那是刻意的：vanilla 那層只判斷 token，不是元件層的驗收基準。

   🔴 **本檔行號不穩定，不要當引用錨點。** 檔頭註解一動就整檔位移——實例：v1.15.0 加上本段
      凍結宣告，全檔 +24 行，所有既有行號引用一次全失效，而且**看起來仍然有效**
      （原本指按鈕 `transition` 的那一行，升版後指到一句完整可讀但意思無關的話）。
      ⇒ **引用規格請用「selector 名稱＋版本號」**（例：`.ds-badge @ DS v1.17.0`），
        也不要引用註解散文裡的數值——本檔的散文數值自己就漂過一整階（v1.14.0 全階升級時
        只改了 tokens.css，註解沒跟上，v1.17.0 才把數字全部刪掉只留 token 名）。
        selector 也不是永久的：已遷移到 registry 的元件族其規則會被刪除，所以一定要帶版本號。

   遷移狀態、成本量測與逐族順序見 GUIDELINE.md 的「元件層導入」節。
   ============================================================ */

/* ---------- 通用 focus（焦點環＝ink＋白色間隙，深淺底都看得見） ---------- */
.ds-btn:focus-visible,.ds-input:focus-visible,.ds-select-trigger:focus-visible,
.ds-seg:focus-visible,.ds-pill:focus-visible,.ds-switch input:focus-visible+.ds-switch-track,
.ds-checkbox input:focus-visible+.ds-checkbox-box,.ds-tab:focus-visible,
.ds-option:focus-visible,.ds-page-btn:focus-visible,.ds-dashed-btn:focus-visible{
  outline:2px solid var(--ink);outline-offset:2px;box-shadow:0 0 0 2px var(--focus-halo);
}

/* ================= 按鈕 ================= */
.ds-btn{
  display:inline-flex;align-items:center;justify-content:center;gap:6px;
  height:var(--h-btn);padding:0 16px;border-radius:var(--r-btn);
  font:500 var(--fs-label)/1 var(--sans);cursor:pointer;border:1px solid transparent;
  transition:background var(--t-fast),border-color var(--t-fast);white-space:nowrap;
}
.ds-btn-primary{background:var(--ink);color:var(--on-ink)}
.ds-btn-primary:hover,.ds-btn-primary:active{background:var(--ink-hi)}
.ds-btn-secondary{background:var(--surface);color:var(--text);border-color:var(--line-strong)}
.ds-btn-secondary:hover,.ds-btn-secondary:active{background:var(--ground)}
.ds-btn-danger{background:var(--red);color:var(--on-danger)}
.ds-btn-danger:hover,.ds-btn-danger:active{filter:brightness(1.08)}
.ds-btn-ghost{background:transparent;color:var(--muted)}
.ds-btn-ghost:hover,.ds-btn-ghost:active{background:var(--ink-tint-05)}
.ds-btn-cta{background:var(--cta);color:var(--on-cta)}       /* 稀有：頁面唯一主行動才用 */
.ds-btn-cta:hover,.ds-btn-cta:active{background:var(--cta-weak);color:var(--ink)}
.ds-btn[disabled]{opacity:.45;cursor:not-allowed;pointer-events:none}
/* `<a class="ds-btn ds-btn-secondary">`＝**看起來像按鈕的連結**（開外部系統、下載、開新分頁）。
   只歸零底線——顏色交給變體 class，所以**用 <a> 時一定要帶一個變體**，否則會拿到瀏覽器的連結色。
   ⚠️ 導向別的網址請用 `<a>`、不要 `<button onclick=window.open>`：中鍵開新分頁、右鍵複製連結、
      螢幕閱讀器唸得出「連結」全部免費，而且不會被瀏覽器的彈出視窗阻擋擋掉。
   ⚠️ `target="_blank"` 一定要配 `rel="noopener noreferrer"`（反向分頁挾持），
      並在文字裡給一個「在新分頁開啟」的 sronly 提示——只有一個 ↗ 圖示，螢幕閱讀器讀不到。 */
a.ds-btn{text-decoration:none}
/* 外部連結的方向箭頭。`aria-hidden` 由 markup 給，這裡只管視覺分量：比文字弱一階，不搶主體 */
.ds-btn .ds-ext-ico{font-size:11px;opacity:.7;flex:none}
/* 2026-08-11：改吃 --h-icon（原本借 --h-tab）。兩者在滑鼠下同值、外觀不變，
   差別只在觸控（pointer:coarse）：icon 鈕要 --hit-min，tab 只能到軌道恆等式允許的高度（見 tokens.css）。
   借同一個 token 的話兩者只能一起動，必有一邊違規。 */
.ds-btn-icon{width:var(--h-icon);height:var(--h-icon);padding:0;border-radius:var(--r-btn);background:transparent;color:var(--muted)}
.ds-btn-icon:hover,.ds-btn-icon:active{background:var(--ink-tint-05)}

/* 密集區按鈕（2026-08-28 v1.17.0 新增）。結清 GUIDELINE 採用登記掛了六版的欠條「small button 尺寸」。
   ⚠️ 挑它的理由與 .ds-input-sm 同一條：**看它跟誰同列，不是看它自己重不重要**。
      卡內密集區／可編輯表格那一列（.ds-input-sm／.ds-select-trigger／.ds-dashed-btn／.ds-ecell）
      原本沒有對應的按鈕，於是只能放 base 的 .ds-btn ⇒ 同列兩種高度，違反原則 4。
   ⚠️ 高度吃 `--h-select` 而**不寫字面值**：那顆 token 在 pointer:coarse 下自動放大到命中下限，
      所以本規則不必另寫 coarse 段。副作用（已在 tokens.css 登記、可接受）：
      **coarse 下 toolbar 階與 dense 階塌成同高**，.ds-btn 與 .ds-btn-sm 在觸控上一樣高——
      那是原則 10 刻意的設計，第一次在平板上驗收時不要去「修」它。
   padding 取 --sp-12：.ds-btn 的 16 在 dense 列裡太寬鬆，而 12 落在間距階梯上、
   又與同高的 .ds-select-trigger（0 10px 0 12px）一致。 */
.ds-btn-sm{height:var(--h-select);padding:0 var(--sp-12);border-radius:var(--r-btn);font-size:var(--fs-label)}

/* ================= 欄位容器＋文字輸入 ================= */
.ds-field{display:flex;flex-direction:column;gap:6px}
.ds-field-label{font:600 var(--fs-sub)/1.2 var(--sans);color:var(--muted)}
.ds-field-label .ds-req{color:var(--req-star);margin-left:2px}
.ds-input{
  height:var(--h-input);padding:0 12px;border-radius:var(--r-input);
  background:var(--field-bg);border:1px solid var(--line);
  font:400 var(--fs-body)/1.4 var(--sans);color:var(--text);width:100%;
  transition:border-color var(--t-fast),background var(--t-fast);
}
.ds-input::placeholder{color:var(--faint)}
.ds-input:hover,.ds-input:active{border-color:var(--line-strong)}
.ds-input:focus{background:var(--surface);border-color:var(--ink);outline:none}
.ds-input.ds-invalid{border-color:var(--red)}
/* 尺寸變體。挑哪一個看**它跟誰同一列**，不是看它自己重不重要——同一列只能有一種高度。
   ⚠️ 本段刻意**不寫數字、只寫 token 名**（v1.17.0）：v1.14.0 全階升級時只改了 tokens.css，
      這段註解的括號數字整組漂了一階，下游照它對齊會對到已經不存在的高度。值一律回 tokens.css 查。
   ds-input（--h-input，form 階）＝表單欄位（一欄一列，不與按鈕並排）
   ds-input-md（--h-btn，toolbar 階）＝工具列／篩選列裡的輸入，與 ds-btn／ds-search／ds-tabs 並排
   ds-input-sm（--h-select，dense 階）＝卡內密集區、與 ds-select-trigger／ds-btn-sm 並排
   〔2026-08-11 新增 md：原本 --h-btn 這一階只存在於 .ds-search 內部，篩選列的輸入只能借 -sm，
     於是稽核紀錄出現「搜尋／篩選／按鈕」同列三種高度。〕
   機器可讀的「列情境 → 高度 token」對映見 tokens.json 的 controlScale 區塊（來源 component-sizes.json）。 */
.ds-input-md{height:var(--h-btn);border-radius:var(--r-btn);font-size:var(--fs-label)}
.ds-input-sm{height:var(--h-select);border-radius:var(--r-btn);font-size:var(--fs-label)}
textarea.ds-input{height:auto;min-height:calc(var(--h-input)*1.6);padding:10px 12px;resize:vertical}
.ds-field-hint{font:400 var(--fs-hint)/1.5 var(--sans);color:var(--faint)}
.ds-field-error{font:400 var(--fs-hint)/1.5 var(--sans);color:var(--red)}

/* ================= 搜尋框 ================= */
.ds-search{
  position:relative;display:inline-flex;align-items:center;
}
.ds-search .ds-input{height:var(--h-btn);border-radius:var(--r-btn);background:var(--ink-tint-05);padding-left:34px;font-size:var(--fs-label)}
/* 搜尋 icon 16（2026-08-12 v1.5.3 使用者裁決，原 14）。
   ⚠️ 這是**圖示字級**不是文字階梯，所以刻意不吃 --fs-app（那顆的語意是「app shell 的品牌名／導覽圖示」，
   借給輸入框內的圖示會讓它同時代表兩種角色）。命中檢查：icon 在 left:12，16px 寬 → 12+16=28 < .ds-input 的 padding-left:34，不重疊。 */
.ds-search .ds-search-ico{position:absolute;left:12px;color:var(--faint);pointer-events:none;font-size:16px;line-height:1}

/* ================= Select（trigger＋白底 menu） ================= */
.ds-select{position:relative;display:inline-block;min-width:200px}
.ds-select-trigger{
  display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;
  height:var(--h-select);padding:0 10px 0 12px;border-radius:var(--r-btn);
  background:var(--surface);border:1px solid var(--line-strong);
  font:400 var(--fs-label)/1 var(--sans);color:var(--text);cursor:pointer;text-align:left;
  transition:border-color var(--t-fast);
}
.ds-select-trigger:hover,.ds-select-trigger:active{border-color:var(--ink)}
/* 表單情境（與文字輸入並排於同一列）：select 對齊輸入框幾何，避免同列 --h-select／--h-input 參差 */
.ds-field>.ds-select .ds-select-trigger,.ds-field.ds-select .ds-select-trigger{height:var(--h-input);border-radius:var(--r-input);padding:0 12px}
/* 工具列／篩選列情境（2026-08-28 v1.17.0 新增）。結清 GUIDELINE「已知缺 token」表登記六版的 .ds-select-md：
   trigger 高度原本恆為 --h-select，工具列放篩選下拉**必然**違反原則 4「同列同高」。
   ⚠️ 選擇器用 `>` **直接子代**，不用後代——combobox／multicombo 的巢內 trigger 不該被連帶命中。
   ⚠️ class 掛**根節點**，同 .ds-select.ds-invalid 的規矩（不必去找 trigger）。
   圓角與字級本來就是 --r-btn／--fs-label，不必再宣告。 */
.ds-select-md>.ds-select-trigger{height:var(--h-btn)}
.ds-field>.ds-select,.ds-field .ds-select{width:100%}
.ds-select.open .ds-select-trigger{border-color:var(--ink)}
/* invalid 視覺態（2026-08-11 補；原本只有 .ds-input 有，select 系列缺）。
   與 .ds-input.ds-invalid 同一條規矩：class 掛在**根節點**上，才不用去找 trigger。
   ⚠️ 顏色不是唯一線索——錯誤訊息一律另外用 .ds-field-error 寫出來（原則：狀態不只靠顏色）。 */
.ds-select.ds-invalid>.ds-select-trigger{border-color:var(--red)}
.ds-select-trigger .ds-caret{color:var(--faint);font-size:11px;transition:transform var(--t-fast);flex:none}
.ds-select.open .ds-caret{transform:rotate(180deg)}
.ds-select-trigger .ds-placeholder{color:var(--faint)}
/* ⚠️ 捲動上限（2026-08-28 v1.17.0 改寫）：**政策是「可見 7 列再捲」，不是 308px。**
   原本寫字面 308，而 308 就是 7 × .ds-option 的列高——收成一顆數值 token 會把這個推導關係
   固化掉：列高一改，選單底部就露出半列，而且**看起來還是有 token**，比手抄更難發現。
   ⇒ 捲動容器上限一律用「可見列數 × 列高」表達（本庫政策，見 CONVERSION.md）。 */
.ds-select-menu{
  position:absolute;z-index:var(--z-select);top:calc(100% + 6px);left:0;min-width:100%;
  max-height:calc(7 * var(--hit-min));overflow-y:auto;
  background:var(--surface);border:1px solid var(--line);border-radius:var(--r-menu);
  padding:5px;box-shadow:var(--sh-menu);display:none;
}
.ds-select.open .ds-select-menu{display:block}
/* min-height 吃 --hit-min（v1.17.0 起）：選單選項一律是點擊目標，base 就要到觸控命中下限——
   這不是「視覺上想要這麼高」，是可及性下限，所以吃的是 --hit-min 不是任何 --h-* 階。 */
.ds-option{
  display:flex;align-items:center;gap:8px;min-height:var(--hit-min);padding:0 12px;border-radius:var(--r-btn);
  font:400 var(--fs-body)/1.35 var(--sans);color:var(--text);cursor:pointer;
}
.ds-option:hover,.ds-option:active{background:var(--ink-tint-05)}
.ds-option[aria-selected="true"]{background:var(--ink);color:var(--on-ink)}
.ds-option .ds-option-check{width:14px;flex:none;font-size:12px;opacity:0}
.ds-option[aria-selected="true"] .ds-option-check{opacity:1}
.ds-option.ds-option-disabled{color:var(--disabled-fg);cursor:not-allowed}
.ds-option-group{padding:10px 12px 4px;font:600 var(--fs-hint)/1 var(--sans);color:var(--faint)}
/* 選項右側的中性小標記（「已持有」「申請中」「未開放」）。不是狀態 badge——
   它標的是「這個選項為什麼不能選」，用中性色，不吃 green/amber/red。 */
.ds-option-badge{
  margin-left:auto;flex:none;padding:2px var(--sp-6);border-radius:var(--r-full);
  background:var(--chip-bg);color:var(--muted);font:600 var(--fs-micro)/1.5 var(--sans);
}
.ds-option[aria-selected="true"] .ds-option-badge{background:color-mix(in srgb,var(--on-ink) 22%,transparent);color:var(--on-ink)}

/* ================= 可搜尋下拉：Combobox（單選）／Multicombo（多選）=================
   2026-08-11 v1.3.0 新增。起因是一個真實事故：專案依「prototype 的示意選項數」選元件，
   供應商代號放了 4 個示意值 → 選了 ds-pills；真實資料是 **1,233 筆**，等於這個欄位不可用。
   （決策表因此改成依「真實選項數」三分，見 GUIDELINE。）

   ⚠️ 它**不是**新的 select，是「select ＋選單裡多一個搜尋框」：
   根節點同時掛 .ds-select，所以 trigger 幾何、選單容器、開合狀態、外點關閉
   **全部沿用既有規則，一個幾何值都沒有新增**。這一區只寫「多出來的那些東西」。
     <div class="ds-select ds-combobox"  data-ds-combobox>   … 單選
     <div class="ds-select ds-multicombo" data-ds-multicombo> … 多選
   行為層見 components.js（DS.initCombobox／DS.initMulticombo／DS.setValue）。

   ── 定死的三條規則（不照做就會各專案各自發明）──
   ① 搜尋框在**選單內最上方**、sticky，不在 trigger 裡。放 trigger 裡會與「顯示已選值」
      互搶同一塊空間，多選時尤其擠。
   ② 大量選項**不做虛擬捲動**：未輸入關鍵字時只渲染前 50 筆＋標明「共 N 筆」。
      虛擬捲動的複雜度與可及性成本（螢幕閱讀器數不到總數、鍵盤跳位）遠高於一句提示。
   ③ multicombo 的 trigger **最多 3 個 chip ＋「＋N」、高度固定不長高**。
      會長高的多選框是表單版面跳動的頭號來源。 */
.ds-combobox .ds-select-value{
  flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left;
}
/* ---- 選單內的 sticky 搜尋區（搜尋框＋計數／已選摘要）---- */
.ds-combobox-search{
  position:sticky;top:0;z-index:1;background:var(--surface);
  /* -5px＝抵銷 .ds-select-menu 的 5px 內距：sticky 貼的是 padding box 上緣，
     不撐滿的話捲動中的選項會從搜尋框左右與上方的縫隙裡透出來。 */
  margin:-5px -5px var(--sp-4);padding:5px 5px var(--sp-6);
  border-bottom:1px solid var(--line-soft);
  display:flex;flex-direction:column;gap:var(--sp-6);
}
.ds-combobox-search .ds-input{width:100%}
/* 「共 1,233 筆，顯示前 50 筆…」／「已選 3 項　清除全部」 */
.ds-combobox-meta{
  display:flex;align-items:center;gap:var(--sp-8);flex-wrap:wrap;
  font:400 var(--fs-hint)/1.5 var(--sans);color:var(--faint);
}
.ds-combobox-meta .ds-link{font-size:var(--fs-hint);padding:0}
.ds-combobox-meta-strong{color:var(--text);font-weight:600}
.ds-combobox-list{display:block}
/* 選項可能是長 email／長廠名 → 允許換行（min-height 44 撐得住兩行），故補上下內距 */
.ds-combobox .ds-option,.ds-multicombo .ds-option{padding-top:var(--sp-6);padding-bottom:var(--sp-6)}
.ds-combobox .ds-option-label,.ds-multicombo .ds-option-label{min-width:0;overflow-wrap:anywhere}
/* 鍵盤游標所在的選項。焦點**留在搜尋框**（aria-activedescendant 模式），
   所以「游標在哪」只能靠這個 class，不能靠 :focus。 */
.ds-option.is-active{background:var(--ink-tint-05)}
.ds-option[aria-selected="true"].is-active{background:var(--ink-hi)}
.ds-combobox-empty{
  padding:var(--sp-16) var(--sp-12);text-align:center;
  font:400 var(--fs-label)/1.6 var(--sans);color:var(--muted);
}

/* ---- 多選：選單內是 checkbox 形態，不是 ink 實心整列 ----
   理由：多選要能連續勾選，整列反白會讓「已選」與「游標所在」分不清。
   勾選方塊直接借 .ds-checkbox-box（20px、勾＝ink），不新增第二種勾選外觀；
   這裡沒有真的 <input>，所以由 aria-selected 驅動它的已勾樣式。 */
.ds-multicombo .ds-option[aria-selected="true"]{background:transparent;color:var(--text)}
.ds-multicombo .ds-option[aria-selected="true"]:hover,.ds-multicombo .ds-option[aria-selected="true"]:active{background:var(--ink-tint-05)}
.ds-multicombo .ds-option[aria-selected="true"].is-active{background:var(--ink-tint-05)}
.ds-multicombo .ds-option[aria-selected="true"] .ds-checkbox-box{background:var(--ink);border-color:var(--ink);color:var(--on-ink)}
.ds-multicombo .ds-option .ds-option-check{display:none}
.ds-multicombo .ds-option[aria-selected="true"] .ds-option-badge{background:var(--chip-bg);color:var(--muted)}
/* ---- 多選 trigger：chips ＋「＋N」，高度固定 ----
   ⚠️ chip 在 trigger 裡是**唯讀摘要、沒有 ✕**。原因有二：
      ① trigger 是 <button>，裡面不能再放 <button>（HTML 不合法＋鍵盤焦點會被關進按鈕裡）
      ② 移除的正解在選單內：未輸入關鍵字時已選項自動置頂，取消勾選一眼可及，
         另有「清除全部」。用 Backspace 也可以（搜尋框空字串時移除最後一個）。
      這一條是對稽核報告「沿用 .ds-pill-x 移除鈕」的**刻意偏離**，理由如上。 */
.ds-multicombo .ds-select-value{
  flex:1;min-width:0;display:flex;align-items:center;gap:var(--sp-4);overflow:hidden;
}
.ds-multicombo .ds-select-value .ds-pill{
  pointer-events:none;      /* 不吃 hover、點擊直接落在 trigger 上 */
  color:var(--text);min-width:0;flex:0 1 auto;
}
.ds-multicombo .ds-select-value .ds-pill>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ds-multicombo-more{flex:none;font:600 var(--fs-sub)/1 var(--sans);color:var(--muted)}

/* ================= Segmented（單選離散 chips） ================= */
.ds-segmented{display:inline-flex;gap:8px;flex-wrap:wrap}
.ds-seg{
  display:inline-flex;align-items:center;height:var(--h-seg);padding:0 14px;border-radius:var(--r-btn);
  background:var(--chip-bg);color:var(--faint);border:1px solid transparent;
  font:500 var(--fs-label)/1 var(--sans);cursor:pointer;transition:background var(--t-fast),color var(--t-fast);
}
.ds-seg:hover,.ds-seg:active{background:var(--line)}
.ds-seg[aria-pressed="true"],.ds-seg.sel{background:var(--ink);color:var(--on-ink)}
.ds-seg[disabled]{opacity:.45;cursor:not-allowed}

/* ================= Pill 多選 ================= */
.ds-pills{display:inline-flex;gap:8px;flex-wrap:wrap}
.ds-pill{
  display:inline-flex;align-items:center;gap:5px;height:var(--h-chip);padding:0 10px;border-radius:var(--r-chip);
  background:var(--chip-bg);color:var(--faint);border:1px solid transparent;
  font:500 var(--fs-sub)/1 var(--sans);cursor:pointer;transition:background var(--t-fast),color var(--t-fast);
}
.ds-pill:hover,.ds-pill:active{background:var(--line)}
.ds-pill[aria-pressed="true"],.ds-pill.sel{background:var(--ink);color:var(--on-ink)}
.ds-pill[disabled]{opacity:.45;cursor:not-allowed}
.ds-pill .ds-pill-x{font-size:10px;opacity:.7}

/* ================= Switch ================= */
.ds-switch{display:inline-flex;align-items:center;gap:8px;cursor:pointer;user-select:none}
.ds-switch input{position:absolute;opacity:0;width:0;height:0}
.ds-switch-track{
  width:34px;height:20px;border-radius:var(--r-full);background:var(--switch-off);position:relative;flex:none;
  transition:background var(--t-med);
}
.ds-switch-track::after{
  content:"";position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;
  background:var(--switch-thumb);box-shadow:0 1px 2px rgba(27,29,34,.25);transition:transform var(--t-med);
}
.ds-switch input:checked+.ds-switch-track{background:var(--ink)}
.ds-switch input:checked+.ds-switch-track::after{transform:translateX(14px)}
.ds-switch input:disabled+.ds-switch-track{opacity:.45;cursor:not-allowed}
.ds-switch-label{font:400 var(--fs-label)/1.2 var(--sans);color:var(--text)}

/* ================= Checkbox ================= */
.ds-checkbox{display:inline-flex;align-items:center;gap:8px;cursor:pointer;user-select:none}
.ds-checkbox input{position:absolute;opacity:0;width:0;height:0}
.ds-checkbox-box{
  width:20px;height:20px;border-radius:var(--r-check);border:1.5px solid var(--line-strong);
  background:var(--surface);display:inline-flex;align-items:center;justify-content:center;flex:none;
  transition:background var(--t-fast),border-color var(--t-fast);
  font-size:12px;color:transparent;
}
.ds-checkbox input:checked+.ds-checkbox-box{background:var(--ink);border-color:var(--ink);color:var(--on-ink)}
.ds-checkbox input:indeterminate+.ds-checkbox-box{background:var(--ink);border-color:var(--ink);color:var(--on-ink)}
.ds-checkbox input:disabled+.ds-checkbox-box{opacity:.45}
.ds-checkbox-label{font:400 var(--fs-label)/1.2 var(--sans);color:var(--text)}

/* ================= 欄位卡片（三態：停用/啟用/必填未填） ================= */
.ds-fieldcard{
  border-radius:var(--r-fieldcard);padding:10px 12px;background:var(--surface);
  border:1px solid var(--line-strong);display:flex;flex-direction:column;gap:8px;
  transition:border-color var(--t-fast),background var(--t-fast);
}
.ds-fieldcard-head{display:flex;align-items:center;justify-content:space-between;gap:8px}
.ds-fieldcard-title{font:600 var(--fs-label)/1.3 var(--sans);color:var(--text);display:inline-flex;align-items:center;gap:6px}
.ds-fieldcard-title .ds-req{color:var(--req-star)}
.ds-fieldcard.off{background:var(--disabled-bg);border-color:var(--line)}
.ds-fieldcard.off .ds-fieldcard-title{color:var(--disabled-fg)}
.ds-fieldcard.off .ds-fieldcard-note{color:var(--disabled-fg-2)}
.ds-fieldcard.req-empty{border-color:var(--req-border)}
.ds-fieldcard-note{font:400 var(--fs-hint)/1.4 var(--sans);color:var(--faint)}
.ds-fieldcard-body{display:flex;flex-direction:column;gap:8px}

/* ================= 虛線加號鈕（＋新增一列 等） ================= */
/* 2026-08-11：補上 height。原本沒有高度規格、由 padding 撐出來（比 dense 階矮一截），
   而它兩個現有用例（系統設定的「＋新增館別」、showcase 的「＋加轉換」）都是與 .ds-input-sm 並排 →
   同一列三顆有一顆矮。定 --h-select 對齊它實際的使用情境（dense 階）。 */
.ds-dashed-btn{
  display:inline-flex;align-items:center;gap:4px;height:var(--h-select);padding:0 10px;border-radius:var(--r-chip);
  border:1px dashed var(--line-strong);background:transparent;color:var(--ink);
  font:500 var(--fs-sub)/1 var(--sans);cursor:pointer;transition:border-color var(--t-fast),background var(--t-fast);
}
.ds-dashed-btn:hover,.ds-dashed-btn:active{border-color:var(--ink);background:var(--ink-tint-05)}

/* ================= Tabs ================= */
/* 2026-08-10 拍板：tablist 選中態改「白 thumb 浮在深軌道上」，不再是 ink 實心塊。
   原因＝ink 實心 tab 與主按鈕同重量，狀態與動作在同一畫面上分不出主次。
   軌道必須是 8%（原 5% 太淺，白 thumb 與軌道差不到一階＝選中態幾乎消失，已實測比較）。
   ⚠️ 這條只適用「有軌道的容器內單選」；ds-seg／ds-pill／分頁 current 沒有軌道、直接坐在白卡上，
   維持 ink 實心（改白＝沒有選中態）。規則全文見 GUIDELINE.md「選中態」節。 */
/* 2026-08-11：軌道內距 3→4，讓容器外高＝32(--h-tab)+4×2＝**40**＝--h-btn。
   原本 38 是全庫唯一不在高度階梯上的值，而 ds-tabs 幾乎永遠與搜尋框（40）或按鈕（40）並排，
   38 vs 40 就是同一列兩種高度。tab 本身維持 32，只有軌道變厚。 */
/* 2026-08-11 v1.3.0 兩件事：
   ① 軌道內距改吃 --ds-tabs-pad（滑鼠 4／觸控 2），維持恆等式
      --h-tab + 2×--ds-tabs-pad ＝ --h-btn（滑鼠 32+8＝40、觸控 40+4＝44），
      tabs 才永遠與同列的搜尋框／按鈕等高。
   ② 補 flex-wrap:wrap（backlog 45⑤）。原本不換行，窄畫面會把 tabs 撐出容器；
      觸控放大後更容易發生。換行後軌道長成兩排、選中的白 thumb 照樣正確。
      ⚠️ 這是唯一會改到既有採用者「溢位當下」表現的變動——不溢位時完全不變。 */
.ds-tabs{
  display:inline-flex;flex-wrap:wrap;gap:2px;padding:var(--ds-tabs-pad);
  border-radius:var(--r-btn);background:var(--ink-tint-08);
}
.ds-tab{
  display:inline-flex;align-items:center;gap:6px;height:var(--h-tab);padding:0 14px;border-radius:var(--r-tag);
  background:transparent;color:var(--muted);border:none;
  font:500 var(--fs-label)/1 var(--sans);cursor:pointer;transition:background var(--t-fast),color var(--t-fast);
}
.ds-tab:hover,.ds-tab:active{color:var(--text)}
.ds-tab[aria-selected="true"],.ds-tab.sel{
  background:var(--surface);color:var(--text);
  box-shadow:0 1px 2px rgba(27,29,34,.10),0 1px 6px rgba(27,29,34,.06);
}
.ds-tab .ds-tab-count{font-size:var(--fs-hint);opacity:.75}

/* ================= Badge / Tag / 統計 chip ================= */
/* ⚠️ `white-space:nowrap` 是**必要的、不是保險**（2026-08-12 補，實測踩到）：
   badge 的高度是**固定的**（`--ds-badge-h`，v1.17.0 起有名字），文字一旦換行就直接溢出那個膠囊——
   「待審核」被拆成兩行、字跑到圓角外面。狀態標籤本來就是 2–3 個字的結構化短字串，
   在任何寬度下都不該換行；真的塞不下要縮的是**欄寬**，不是把標籤折斷。
   （發現情境：窄畫面把表格從 nowrap 改成可換行時，badge 跟著被折斷。） */
.ds-badge{
  display:inline-flex;align-items:center;height:var(--ds-badge-h);padding:0 10px;border-radius:var(--r-full);
  background:var(--ink-tint-08);color:var(--ink);font:500 var(--fs-hint)/1 var(--sans);
  white-space:nowrap;
}
.ds-badge-amber{background:var(--amber-bg);color:var(--amber)}
.ds-badge-green{background:var(--green-bg);color:var(--green)}
.ds-badge-red{background:var(--red-bg);color:var(--red)}
.ds-badge-slate{background:var(--slate-bg);color:var(--slate)}
.ds-tag-mono{
  display:inline-flex;align-items:center;padding:2px 8px;border-radius:var(--r-tag);
  background:var(--slate-bg);color:var(--ink);font:600 12px/1.4 var(--mono);
}
/* 🔴 2026-08-28 v1.17.0 修復漂移（外觀 +2px）：原本寫死 31px，而 **31 就是 v1.13.1 以前的
   `--h-seg` 值**——v1.14.0 全階升級時 --h-seg 31→33，統計 chip 因為寫死所以沒跟上，
   從此與 segmented chip 差 2px。v1.14.0 的 Changelog 把它判為「巧合的 literal、非漏轉 var」，
   那個判斷對 .ds-modal-ico(40) 與 .ds-side-mark(32) 成立，**對 31 不成立**：它在階梯上待過。
   ⇒ 改吃 --h-seg 讓兩者重新綁在一起（視覺分量本來就同級）。這是「字面值不 token 化」
     在庫內部造成漂移的第一個實例，故列為修復而非改動。 */
.ds-stat-chip{
  display:inline-flex;align-items:center;gap:6px;height:var(--h-seg);padding:0 12px;border-radius:var(--r-btn);
  background:var(--ink-tint-05);color:var(--muted);font:400 var(--fs-label)/1 var(--sans);
}

/* ================= 版面容器（2026-08-11 新增） =================
   這一區補的是元件庫最大的結構性缺口：原本只有「控件」，沒有「裝控件的東西」，
   於是每個專案都自己發明一次卡片與工具列——連 bug 都要各自再踩一次。 */

/* ---- 內容卡 ---- */
.ds-card{background:var(--surface);border-radius:var(--r-card);box-shadow:var(--sh-card);margin-bottom:var(--sp-20)}
.ds-card-head{
  display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;
  padding:var(--sp-16) var(--sp-20);border-bottom:1px solid var(--line);
}
.ds-card-head h3,.ds-card-title{font:var(--fw-section) var(--fs-section)/1.3 var(--sans);color:var(--text);margin:0}
.ds-card-body{padding:var(--sp-20)}
/* 卡片底部動作列（與 card-head 同內距，形成上下對稱的框） */
.ds-card-actions{display:flex;gap:10px;flex-wrap:wrap;padding:var(--sp-16) var(--sp-20);border-top:1px solid var(--line)}
/* 表格橫捲容器。⚠️ 這條規則是本庫踩過的真 bug：
   卡片有 14px 圓角但 overflow:visible，而表格列的 hover 底色是直角矩形 →
   滑到最後一列時底色直接蓋掉卡片下緣兩個圓角（看起來像「hover 超出表格範圍」）。
   修法＝把圓角給捲動容器（它本來就因 overflow-x:auto 而是裁切容器），
   **不是**給 .ds-card 加 overflow:hidden——那會連帶裁掉卡內 ds-select 的展開選單。
   限定 :last-child 是因為表格後面還可能接 .ds-card-actions，那時下緣圓角不屬於表格。 */
/* 2026-08-12 v1.6.0：**捲動陰影**——可以往右捲的時候要看得出來。
   起因是真實回饋：使用者看到表格被切斷，回報「RWD 壞掉」。技術上它「可以捲」，
   但 **macOS 的覆蓋式捲軸平常不顯示**，所以畫面上的事實就是「資料被切掉了」。
   ⚠️ **這只是補救，不是解法**：真正放不下的清單應該換資訊結構（見 `.ds-listcards`），
      陰影是給「偶爾差一點點」的表格用的。
   手法＝純 CSS，零 JS：兩層 gradient 疊在同一個 background 上——
     ・遮罩層 `background-attachment:local`＝跟著內容捲，捲到底就把陰影蓋住
     ・陰影層 `background-attachment:scroll`＝釘在容器邊緣不動
   兩者相減的結果就是「只有還有東西沒看到的那一側才有陰影」。 */
.ds-scrollx{
  overflow-x:auto;
  background:
    linear-gradient(to right, var(--surface) 30%, transparent) left center,
    linear-gradient(to left,  var(--surface) 30%, transparent) right center,
    radial-gradient(farthest-side at 0 50%,   rgba(27,29,34,.14), rgba(27,29,34,0)) left center,
    radial-gradient(farthest-side at 100% 50%,rgba(27,29,34,.14), rgba(27,29,34,0)) right center;
  background-repeat:no-repeat;
  background-size:40px 100%, 40px 100%, 14px 100%, 14px 100%;
  background-attachment:local, local, scroll, scroll;
}
/* 捲動容器貼齊卡片邊緣時要繼承卡片圓角。
   ⚠️ 兩條都需要，理由不對稱地重要：`.ds-scrollx` 的捲動陰影是**不透明的 background**
   （左右各 40px 的 `--surface` 遮罩層），而 `.ds-card` 沒有 `overflow:hidden`
   （不能加——會裁掉卡內 `.ds-select` 的展開選單）。所以捲動容器只要貼到卡片的某個角，
   那個角的圓弧就會被它的白底方角蓋掉、看起來像圓角消失。
   表格當卡片首子（無 `.ds-card-h` 標題列）時蓋的是上兩角，當末子時蓋的是下兩角。 */
.ds-card>.ds-scrollx:first-child{border-top-left-radius:var(--r-card);border-top-right-radius:var(--r-card)}
.ds-card>.ds-scrollx:last-child{border-bottom-left-radius:var(--r-card);border-bottom-right-radius:var(--r-card)}

/* ---- 工具列（tabs／搜尋／篩選／動作並排的那一列）----
   ⚠️ 同列同高：放進來的控件一律取 `--h-btn`（toolbar 階：ds-tabs／ds-search／ds-input-md／ds-select-md／ds-btn）。
   要靠右的動作用 margin-left:auto。窄畫面自動換行，不另外寫 media query。 */
.ds-toolbar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:18px}

/* ---- 工具列裡的「標籤＋控件」水平組（2026-08-12 v1.4.0 新增）----
   來源＝權限申請的 `.ffield`（稽核紀錄的時間區間／申請人篩選）。它在專案裡活了快一個月，
   使用者一眼就看出「這一頁的元件跟別頁長得不一樣」——不是做錯，是**遇到庫沒有的需求只好自己長一個**。
   凡是清單頁有進階篩選（日期區間、人名、金額區間）就會再要一次，所以收上來。

   ⚠️ 這是**工具列內**的水平標籤，與表單裡的 `.ds-field`（標籤在上、控件在下）是兩件事：
      工具列一列要塞多組篩選，標籤只能在左邊；表單一欄一列，標籤在上才讀得順。
   ⚠️ 同列同高：裡面的控件一律取 `--h-btn`（toolbar 階：`.ds-input-md`／`.ds-select-md`），與 ds-btn／ds-search／ds-tabs 對齊。 */
.ds-field-inline{display:flex;align-items:center;gap:var(--sp-6);flex:none;
  font:400 var(--fs-label)/1.4 var(--sans);color:var(--muted)}
.ds-field-inline>label{white-space:nowrap}
.ds-field-inline .ds-input{width:auto;min-width:130px;max-width:180px}
/* 區間欄位中間那個連接號（「起日 – 迄日」） */
.ds-field-inline .ds-range-sep{color:var(--faint);flex:none}

/* ---- 日期輸入：**刻意保留原生**（2026-08-12 v1.4.0 拍板）----
   全站其他選擇類控件都是自繪（ds-select／ds-checkbox／ds-tabs／ds-combobox），
   `<input type="date">` 是唯一的原生控件——瀏覽器會畫自己的「年/月/日」與日曆圖示。
   ⚠️ **這是刻意的例外，不是待修的 bug**（同狀態色對比那條）。理由：
     ①自製日曆選擇器是最難寫對的元件之一（鍵盤、區間、時區、i18n、螢幕閱讀器）
     ②原生在手機上直接叫出系統日期滾輪，比任何自製版本都好用
     ③它的**外框**（高度／圓角／邊框／focus）本來就吃 .ds-input，跟其他控件是一致的，
       不一致的只有瀏覽器畫的那塊內部——成本效益不成立。
   本庫能做的是把它的內部渲染盡量收斂，讓它在同一列裡不突兀： */
.ds-input[type="date"]{font-variant-numeric:tabular-nums;color:var(--text)}
/* 未選日期時，瀏覽器把「年/月/日」畫成純黑，比 placeholder 還搶眼 → 拉到 --faint 那一階 */
.ds-input[type="date"]:not(:focus):invalid::-webkit-datetime-edit,
.ds-input[type="date"][value=""]::-webkit-datetime-edit{color:var(--faint)}
/* 日曆圖示：原生是飽和藍灰，改吃文字色階並在 hover 才變深，與 .ds-caret 的分量一致 */
.ds-input[type="date"]::-webkit-calendar-picker-indicator{
  opacity:.45;cursor:pointer;transition:opacity var(--t-fast)}
.ds-input[type="date"]:hover::-webkit-calendar-picker-indicator{opacity:.8}
.ds-input[type="date"]::-webkit-datetime-edit-fields-wrapper{padding:0}

/* ---- 空狀態 ----
   清單「查無資料」與「本來就沒有資料」是兩件事，文案要分開寫（前者給清除篩選的出路）。
   放在 <td colspan> 裡即可，不需要另外的容器。 */
/* 🔴 2026-08-28 v1.17.0 收斂（外觀 +2px）：原本 padding:46px，是間距階梯外的髒值
   （tokens.css 那段登記的「三個階梯外髒值」漏數的第四個）。收斂到新增的 --sp-48。
   依 v1.13.0 已拍板的通則：原實作的值與階梯對不上時一律收斂到階梯，1–2px 位移直接吸收。
   收斂後本規則（--sp-48）與 .ds-empty-compact（--sp-32）都落在階梯上，讀起來才是一組。 */
.ds-empty{padding:var(--sp-48);text-align:center;color:var(--muted)}
.ds-empty-ico{font-size:24px;line-height:1}
.ds-empty b{display:block;color:var(--text);margin-top:8px}
.ds-empty p{font-size:var(--fs-label);margin:4px 0 0}

/* ---- 撐開空間的隔板 ----
   「標題靠左、狀態靠右」這種一列兩端對齊，在既有程式碼裡到處是 <span style="flex:1">。
   給它一個名字，inline style 就能收乾淨。（能用 justify-content:space-between 時優先用那個，
   這個是給「三個以上元素、只想在其中一處斷開」的情況。） */
.ds-spacer{flex:1 1 auto;min-width:0}

/* ================= 提示條 / Inline alert（2026-08-11 v1.3.0 新增）=================
   庫裡原本**完全沒有**「一整條有底色的訊息」這個東西，而每個審批系統都要顯示
   「不通過原因／拒絕開通原因／管理員備註／同步佇列說明／頁面說明」——
   結果同一支 app 就已經長出兩套（.banner 與 .hint2，差別只有 padding 與圓角）。

   ── 顏色規則（與 badge 不同，這裡是刻意分開的）──
   **底色表狀態，文字一律 --text，狀態色只上在圖示。**
   badge 那邊已拍板接受「狀態色文字對淺底未達 AA」的例外（2–3 個字、另有形狀與位置輔助），
   但提示條裝的是**整句、甚至整段要讀的內容**，同一個例外套過來成本完全不同：
   red 文字對 red 底只有 3.77:1，一段兩行的說明就真的讀不下去。
   圖示用狀態色（非文字元素，門檻 3:1，通過），狀態因此仍然看得出來、又不犧牲可讀性。
   ⚠️ 這條**不推翻** badge 的拍板，兩者管的是不同東西。

   ── ARIA ──
   頁面一載入就存在的說明條：**不要**加 role="alert"（assertive 會打斷螢幕閱讀器正在唸的東西）。
   只有「使用者動作之後才冒出來」的錯誤／成功條才加 role="alert"（紅）或 role="status"（綠）。 */
.ds-alert{
  display:flex;align-items:flex-start;gap:var(--sp-10);
  padding:var(--sp-12) var(--sp-16);border-radius:var(--r-btn);
  background:var(--surface-2);color:var(--text);
  font:400 var(--fs-body)/1.6 var(--sans);
  margin-bottom:var(--sp-16);
}
.ds-alert-ico{flex:none;font-size:14px;line-height:1.6;color:var(--muted)}
.ds-alert-body{min-width:0;flex:1 1 auto;overflow-wrap:anywhere}
/* 開頭的那個標籤（「不通過原因：」「已加入同步佇列」）。用它，不要把整段變粗體——
   整段 600 的長句是既有 .banner 的問題，兩行以上就變成一片黑塊。 */
.ds-alert-title{font-weight:600}
.ds-alert-body p{margin:var(--sp-4) 0 0}
.ds-alert-body p:first-child{margin-top:0}
/* info＝中性說明（橙 tint，不是狀態）：頁面說明、帶入提示、規則解釋 */
.ds-alert-info{background:var(--accent2-weak)}
.ds-alert-info .ds-alert-ico{color:var(--cta-ink)}
/* 狀態三色，語意同 badge：green＝通過/開通、amber＝進行中/警示、red＝失敗/破壞性 */
.ds-alert-green{background:var(--green-bg)}
.ds-alert-green .ds-alert-ico{color:var(--green)}
.ds-alert-amber{background:var(--amber-bg)}
.ds-alert-amber .ds-alert-ico{color:var(--amber)}
.ds-alert-red{background:var(--red-bg)}
.ds-alert-red .ds-alert-ico{color:var(--red)}

/* ================= 單據詳情頭區＋鍵值版面（2026-08-11 v1.3.0 新增）=================
   所有「單據詳情頁」的骨架：一張卡，上面是〔標題＋狀態 badge〕與〔單號＋次要資訊〕，
   下面是一排欄位名／值。原本這整組只存在專案層，而且**同一支 app 裡就有兩套鍵值版面**
   （auto-fit 網格用了十幾處、兩欄定義列表另外 3 處），沒有人說得出何時該用哪一套。
   本版收斂成一組 class（.ds-kv／-item／-label／-value），版面差異用一個修飾子表達。 */
.ds-detail-head{padding:var(--sp-20)}
.ds-detail-head-top{display:flex;align-items:center;gap:var(--sp-10);flex-wrap:wrap}
.ds-detail-head-top h2,.ds-detail-head-title{
  margin:0;font:var(--fw-title) var(--fs-title)/1.2 var(--sans);
  color:var(--text);letter-spacing:var(--ls-page);
}
.ds-detail-head-sub{
  display:flex;align-items:center;gap:var(--sp-8);flex-wrap:wrap;margin-top:var(--sp-8);
  font:400 var(--fs-label)/1.5 var(--sans);color:var(--muted);
}

/* ---- 鍵值：一組 class、兩種版面 ----
   何時用哪一種（判準是「怎麼被讀」，不是「有幾個欄位」）：
     預設 .ds-kv          ＝**掃描用**。欄位多、值短（人名、日期、單號、數量），
                            使用者是在「找某一個值」→ auto-fit 自動並排，一眼掃完，
                            寬畫面不會變成一條孤獨的長列。詳情頁摘要區的正解。
     .ds-kv.ds-kv-list    ＝**閱讀用**。欄位少（≤6）、值長或需要逐條確認
                            （送出前的確認摘要、稽核明細），閱讀**順序**有意義 →
                            固定兩欄、由上而下一條一條讀，不會因為視窗寬度改變順序感。
   markup 用 <dl>／<dt>／<dd>（這就是定義列表的語意），中間那層 <div> 在 dl 裡是合法的。 */
.ds-kv{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));
  gap:var(--sp-12) var(--sp-20);margin:0;
}
.ds-kv-item{display:flex;flex-direction:column;gap:var(--sp-2);min-width:0}
.ds-kv-label{font:400 var(--fs-micro)/1.4 var(--sans);color:var(--faint)}
.ds-kv-value{
  margin:0;font:400 var(--fs-body)/1.45 var(--sans);color:var(--text);
  display:flex;align-items:center;gap:var(--sp-6);flex-wrap:wrap;min-width:0;overflow-wrap:anywhere;
}
.ds-kv-list{display:flex;flex-direction:column;gap:var(--sp-8)}
.ds-kv-list .ds-kv-item{flex-direction:row;align-items:baseline;gap:var(--sp-12)}
.ds-kv-list .ds-kv-label{flex:none;width:120px;font-size:var(--fs-body);color:var(--muted)}
/* 窄畫面收窄標籤欄。這是**版面**RWD（由可用寬度決定），與觸控目標放大
   （由輸入裝置決定、走 pointer:coarse）是兩件事，不要混在一起判斷。 */
@media (max-width:640px){
  .ds-kv-list .ds-kv-label{width:96px}
}

/* ================= 狀態時間軸（2026-08-11 v1.3.0 新增）=================
   「這張單走到哪一關、誰在什麼時候做了什麼」——所有審批系統的核心視覺，
   採購審核、權限申請、庫存調整都各有一份。

   ── 顏色規則（寫進庫，不要再各自決定）──
   已完成 .is-done      → --ink 實心。ink 在這裡表示**既成事實**，中性、不搶戲。
   目前這一關 .is-current→ --amber 實心＋amber 光暈。它回答「現在卡在誰身上」＝
                          **狀態**「進行中」，走原則 3 的狀態色。
   不通過 .is-rejected  → --red 實心。
   還沒到（預設）        → 空心＋--line-strong 框。
   ⚠️ **進度指示一律不用 ink 表示「現在」**——ink 是控件選中與主按鈕的顏色，
      拿來表示進度會讓「使用者選的」與「系統走到的」在同一畫面上分不出來。
      同一條原則在多步表單的精靈步驟條上是用 --cta-ink（2026-08-10 拍板）。
      兩者目前不同色（時間軸 amber／精靈 cta-ink），因為它們回答的問題不同：
      前者是「這關還沒結束」（狀態），後者是「我走到第幾步」（位置）。
      要不要統一＝**待拍板**，本版照兩邊的現行實作收錄，不擅自合併。

   ── 可及性 ──
   圓點是純裝飾（aria-hidden）。狀態**只靠顏色**傳達的部分要用 .ds-sronly 補字，
   例如 <span class="ds-sronly">（不通過）</span>；目前這一關另加 aria-current="step"。
   容器用 <ol>，每一關 <li> ＝天然的「第幾關／共幾關」語意。 */
.ds-timeline{
  /* 元件內部變數（不是 design token，不進 tokens.css）：圓點直徑與描邊寬。
     宣告在容器上，讓 .ds-timeline-dot 與 .ds-timeline-step::before 讀到同一份值。 */
  --dot:16px; --dot-bw:2px;
  position:relative;margin:0;padding:0 0 0 var(--sp-6);list-style:none;
}
.ds-timeline-step{position:relative;padding:0 0 var(--sp-20) var(--sp-24)}
.ds-timeline-step:last-child{padding-bottom:0}
/* 連接線：從圓點下緣接到下一個圓點。left ＝(圓點直徑 − 線寬)/2 的置中值，不是間距。
   ⚠️ 2026-08-28 v1.17.0：原本兩邊都是手算字面值（left:7px 與圓點 16px／描邊 2px），
      **改圓點大小要同時改兩處，漏一處連接線就偏心，而且不會報錯**。改成讓關係自己成立：
      圓點尺寸與線寬收成元件內部變數（--dot／--dot-bw，掛在 .ds-timeline 上讓 ::before 也讀得到），
      left 改 calc。⚠️ 這兩顆刻意是**元件內部變數、不進 tokens.css**：它們只服務這一個元件、
      不參與任何「同列同高／同類同級」紀律（判準見 GUIDELINE「什麼樣的幾何值會變成 token」）。
      computed 值零變動：(16 − 2)/2 ＝ 7。 */
.ds-timeline-step::before{
  content:"";position:absolute;left:calc((var(--dot) - var(--dot-bw)) / 2);
  top:18px;bottom:-2px;width:var(--dot-bw);background:var(--line);
}
.ds-timeline-step:last-child::before{display:none}
.ds-timeline-dot{
  position:absolute;left:0;top:1px;width:var(--dot);height:var(--dot);border-radius:50%;
  border:var(--dot-bw) solid var(--line-strong);background:var(--surface);
}
.ds-timeline-step.is-done .ds-timeline-dot{background:var(--ink);border-color:var(--ink)}
.ds-timeline-step.is-current .ds-timeline-dot{
  background:var(--amber);border-color:var(--amber);box-shadow:0 0 0 4px var(--amber-bg);
}
.ds-timeline-step.is-rejected .ds-timeline-dot{background:var(--red);border-color:var(--red)}
.ds-timeline-name{font:var(--fw-section) var(--fs-section)/1.4 var(--sans);color:var(--text)}
.ds-timeline-meta{font:400 var(--fs-sub)/1.5 var(--sans);color:var(--faint);margin-top:var(--sp-2)}

/* ================= 文字工具類（2026-08-11 新增） ================= */
/* 文字按鈕／連結。全庫原本沒有任何連結樣式，而它常常是表格裡唯一的鍵盤入口，
   所以底線要一直在（不能只有 hover 才出現），上下 padding 撐出可點高度。 */
.ds-link{
  display:inline;background:none;border:0;border-bottom:1px solid var(--line-strong);
  padding:6px 0;color:var(--ink);font:600 inherit/inherit var(--sans);font-size:inherit;
  cursor:pointer;text-decoration:none;transition:border-color var(--t-fast);
}
.ds-link:hover,.ds-link:active{border-bottom-color:var(--ink)}
/* 通用小字說明（散落各處的補充文字：卡片標題列右側、表格下方、動作說明）。
   ⚠️ 命名陷阱：`.ds-hint` 是 **13px（--fs-label）**，token `--fs-hint` 是 11.5px，兩者**不是同一個東西**。
      11.5 那一階給的是 badge／表頭／欄位下方的 .ds-field-hint；13 這一階才是「讀得下去的說明句」。
      名字撞在一起是歷史包袱，改名的破壞性大於價值，所以留註解在這裡擋。 */
.ds-hint{font:400 var(--fs-label)/1.5 var(--sans);color:var(--faint)}
/* 等寬＋等寬數字：單號、代號、日期、金額。tabular-nums 讓數字欄位上下對齊。 */
.ds-mono{font-family:var(--mono);font-variant-numeric:tabular-nums}
/* 只給螢幕閱讀器（必填星號改 aria-hidden 後，語意要另外補回來） */
.ds-sronly{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* ================= 忙碌／停用態（2026-08-11 新增） =================
   prototype 全是同步假資料所以踩不到，但真系統一定要——沒有規範，開發會各自發明。
   規則三條：
   ① 送出中的按鈕**不換文案、不換寬度**（避免版面跳動），只換成轉圈＋鎖住互動
   ② 送出中要 aria-busy="true"，讓螢幕閱讀器知道正在處理
   ③ 首次載入用骨架屏（.ds-skeleton），**不要**用整頁 spinner——骨架保留版面、不會閃 */
.ds-btn.is-loading{position:relative;color:transparent!important;pointer-events:none}
.ds-btn.is-loading::after{
  content:"";position:absolute;width:16px;height:16px;border-radius:50%;
  border:2px solid currentColor;border-top-color:transparent;
  color:var(--on-ink);animation:ds-spin .6s linear infinite;
}
.ds-btn-secondary.is-loading::after,.ds-btn-ghost.is-loading::after{color:var(--ink)}
/* spinner 是 currentColor 畫的，所以底色不同的填色鈕要各自指定前景，否則 --on-cta/--on-danger 一旦與 --on-ink 分家，轉圈圈就會消失在自己的底色裡 */
.ds-btn-cta.is-loading::after{color:var(--on-cta)}
.ds-btn-danger.is-loading::after{color:var(--on-danger)}
@keyframes ds-spin{to{transform:rotate(360deg)}}
.ds-skeleton{
  background:linear-gradient(90deg,var(--ink-tint-05) 25%,var(--ink-tint-08) 37%,var(--ink-tint-05) 63%);
  background-size:400% 100%;animation:ds-shimmer 1.4s ease infinite;border-radius:var(--r-btn);
}
.ds-skeleton-text{height:14px;margin:6px 0}
.ds-skeleton-row{height:var(--h-row)}
@keyframes ds-shimmer{0%{background-position:100% 50%}100%{background-position:0 50%}}
/* 尊重系統的「減少動態效果」設定——本庫所有動畫一律受此約束。
   2026-08-11 v1.3.0 補齊：原本只擋了兩個 keyframes 動畫，位移類的 transition 沒擋。
   規則＝**位移與旋轉全部拿掉，淡入淡出保留**（透明度變化不會誘發前庭不適，
   而且完全拿掉會讓 toast 變成「憑空閃現」，反而更難察覺它出現過）。 */
@media (prefers-reduced-motion:reduce){
  .ds-btn.is-loading::after,.ds-skeleton{animation:none}
  .ds-skeleton{background:var(--ink-tint-05)}
  /* toast：不再由下滑入，只留淡入；位移一次到位 */
  .ds-toast{transform:translateX(-50%) translateY(0);transition:opacity var(--t-fast)}
  /* select／combobox 的 caret 不旋轉（開合狀態仍由選單本身與 aria-expanded 表達） */
  .ds-select-trigger .ds-caret{transition:none}
}

/* ================= 表格 ================= */
.ds-table-card{background:var(--surface);border-radius:var(--r-card);box-shadow:var(--sh-card);overflow:hidden}
.ds-table{width:100%;border-collapse:collapse}
/* 2026-08-10：表頭降階。原本 13px/500/muted 與內文 14px/400/text 幾乎同重量，
   表頭與資料在視覺上打平＝清單看起來平。改成小字＋加追蹤＋更弱的顏色，
   表頭退成「結構線」，資料才浮得起來。中文不吃 text-transform，靠字級與追蹤做出差異。 */
.ds-table th{
  height:var(--h-row);padding:0 12px;text-align:left;
  font:600 var(--fs-hint)/1.3 var(--sans);letter-spacing:.06em;color:var(--faint);
  border-bottom:1px solid var(--line);
}
/* 2026-08-11：表頭不換行。短標籤折行只會讓列高忽高忽低，而表頭一旦換行、
   下面整欄資料的視覺起點就跟著跑。要讓某張表整體不換行用 .ds-table-nowrap。 */
.ds-table th{white-space:nowrap}
.ds-table td{
  height:var(--h-row);padding:0 12px;vertical-align:middle;
  font:400 var(--fs-body)/1.4 var(--sans);color:var(--text);border-bottom:1px solid var(--line);
}
/* 2026-08-13 v1.7.0：中文儲存格不准斷在詞中間。
   ⚠️ 這條是在權限申請的 RWD 上被逼出來的，而且**量測完全看不到它**——
   窄畫面把表格從 nowrap 改成可換行後，「六欄全見／填滿 100%／零橫捲」全部成立，
   但畫面是爛的：中文沒有詞界，CSS 預設會在任意兩字之間斷行 →
     「陳大文」→ 陳大/文　「金石堂」→ 金石/堂　「共用雲端硬碟權限」→ …硬碟權/限
   人名與系統名被腰斬，列高還從一致的 54 變成 1~3 行參差。
   `word-break:keep-all` 只抑制「字元之間」的斷點，`・`／`、`／空白 這些標點與
   語意邊界的斷點仍然保留 → 長摘要照樣換行，短標籤不再被切開。
   ⚠️ **這條治不了拉丁／數字識別字串**（`REQ-0006` 仍會斷在連字號、`2026-07-24` 斷在 `-`）——
      那是 Latin 斷點，keep-all 管不到，要靠採用端對該欄下 `.ds-table-nowrap` 或自己的 nowrap。
      實測：只加 keep-all 不加 nowrap，時間戳記會裂成三行且把表格推爆（權限申請 900px）。 */
.ds-table td,.ds-table th{word-break:keep-all}
/* 2026-08-10：表格首末欄的水平內距＝區塊內距（--sp-20），不是儲存格內距（12）。
   表格在卡片裡是滿版的，若首欄只縮 12，卡片標題列文字（內距 20）與表格第一欄文字
   會落在兩條不同的左緣，同一張卡看起來就是「沒對齊」。首末欄拉齊後整張卡只有一條左緣。 */
.ds-table th:first-child,.ds-table td:first-child{padding-left:var(--sp-20)}
.ds-table th:last-child,.ds-table td:last-child{padding-right:var(--sp-20)}
/* ---- 互動列 vs 非互動列（2026-08-11 v1.3.0：規格漏洞修正）----
   ⚠️ 這是一條**破除鏡射副本**的修正。原本 .ds-table 對所有 tbody 列一律 cursor:pointer＋hover，
   但真實頁面到處都是非互動表格（表單摘要、異動紀錄、子表）——於是唯一的採用者不敢掛 ds-table、
   自己手抄了一份 table.tbl，同一份規格從此維護兩次（11 張表全走副本、ds-table 使用 0 次）。
   分野改由 markup 表達，兩種寫法都支援：
     整張表可點 → <table class="ds-table ds-table-clickable">
     只有某些列可點 → <tr class="ds-row-clickable">
   沒掛的列＝沒有手指游標、沒有 hover 底＝老實地告訴使用者「這裡點了沒事」。 */
.ds-table tbody tr{position:relative;transition:background var(--t-fast)}
.ds-table-clickable tbody tr,.ds-table tbody tr.ds-row-clickable{cursor:pointer}
/* 2026-08-11（v1.1.0）：表格列 hover 由 --ink-tint-06（純黑 6% 疊層）改吃 --hover-bg。
   原本全站只有這一個互動態不沾主色，滑過去是死灰、與側欄導覽 hover 講兩種語言。
   統一後「滑鼠所在處」全站同一個底色，深淺階梯＝hover(--hover-bg) < 選中(--nav-sel-bg)。 */
.ds-table-clickable tbody tr:hover,.ds-table-clickable tbody tr:active,.ds-table tbody tr.ds-row-clickable:hover,.ds-table tbody tr.ds-row-clickable:active{background:var(--hover-bg)}
.ds-table tbody tr:last-child td{border-bottom:none}
.ds-table .ds-row-chevron{color:var(--ink);font-size:15px}
/* 勾選中的列要有**持續**的狀態，不能只靠勾選框那一點顏色——批次操作時使用者要能一眼看出
   「我剛剛勾了哪幾列」。沿用導覽選中底，階梯＝hover(--hover-bg) < 選中(--nav-sel-bg)。 */
.ds-table tbody tr:has(input[type=checkbox]:checked){background:var(--nav-sel-bg)}
/* 數字欄靠右＋等寬數字。金額/天數/筆數不靠右，位數就對不齊，掃一眼比大小的能力直接消失。
   （這條在唯一採用者身上存在快一個月、記了一輪 backlog 才升進庫。） */
.ds-table td.ds-num,.ds-table th.ds-num{text-align:right;font-variant-numeric:tabular-nums}
/* 寬表（欄多、內容短）：儲存格一律不換行，靠外層 .ds-scrollx 橫捲。
   ⚠️ 這是**選擇**不是預設——不換行等於放棄窄畫面的可讀性，換來的是欄位對齊。 */
.ds-table-nowrap th,.ds-table-nowrap td{white-space:nowrap}
/* ---- 表格列裡的勾選命中盒 ----
   .ds-checkbox 只管那顆 20px 方塊；它坐在 --h-row 高的列裡、而整列通常掛著「點列進詳情」，
   結果是「想勾選卻點進詳情」的機率遠大於點中（真實回饋，2026-08-11 J-7）。
   手法：**命中盒與視覺尺寸分離**——用 min-width/height 撐出 --h-btn（觸控下＝--hit-min）的
   可點區，再用負 margin 把它在版面上還原成 20px，所以**撐可點區但不撐列高**。
   負 margin 由公式算、跟著 --h-btn 自動變：(20 − var(--h-btn))/2。
   用法：<label class="ds-checkbox ds-check-cell">…（本身就是 label，
   整列導航的守衛 closest('button,a,input,select,label') 會自動連同放大的範圍一起讓路）。 */
.ds-check-cell{
  display:inline-flex;align-items:center;justify-content:center;cursor:pointer;
  min-width:var(--h-btn);min-height:var(--h-btn);margin:calc((20px - var(--h-btn)) / 2) 0;
}

/* ================= 手機列表卡（2026-08-12 v1.4.0 新增）=================
   **為什麼要有這個元件**：資料表在手機上只剩「能橫捲」，那不是 RWD、那是把問題丟給使用者。
   實測權限申請的「我的申請」（390 寬）：時間戳記 152px 就吃掉可見區的 42%，
   而真正要看的「審核狀態」在第 462px——**要橫捲一次才看得到自己的申請過了沒**。
   「主管用手機審核」是這批系統的既定主要情境，所以這不是邊緣案例。

   **判準：窄畫面不是把表格縮小，是換一種資訊結構。**
   表格的前提是「欄位對齊、跨列比較」；手機一次只看得到一筆，比較的前提本來就不成立，
   於是欄位對齊帶來的好處歸零、成本（橫捲）卻是全額。改成一筆一張卡，
   把六個欄位重新分成四個角色：**標題／狀態／摘要／次要資訊**。

   ⚠️ 這組 class 不含任何 media query——**要不要切換由採用者決定**（有些列表在手機上就是要看全欄）。
      建議切換點 ≤640，與本庫其他版面型 RWD 一致。
   ⚠️ 卡片本身可點時掛 .ds-listcard-clickable（同 .ds-row-clickable 的分野：不可點就不要給手指游標）。 */
.ds-listcards{display:flex;flex-direction:column;gap:var(--sp-8);padding:var(--sp-12)}
/* text-decoration/color 歸零＝支援用 <a href> 當卡片。**可導航的卡片建議就用 <a>**：
   鍵盤操作、中鍵開新分頁、螢幕閱讀器的「連結」語意全部免費拿到，
   比 <div role="link" tabindex="0"> ＋ 自己補 Enter/Space 少寫一堆、也少錯一堆。 */
.ds-listcard{
  display:flex;flex-direction:column;gap:var(--sp-6);
  padding:var(--sp-12) var(--sp-14);border:1px solid var(--line);border-radius:var(--r-btn);
  background:var(--surface);text-decoration:none;color:inherit;
}
.ds-listcard-clickable{cursor:pointer;transition:background var(--t-fast),border-color var(--t-fast)}
.ds-listcard-clickable:hover,.ds-listcard-clickable:active{background:var(--hover-bg);border-color:var(--line-strong)}
/* 第一列＝標題＋狀態。標題吃滿剩餘寬度並可截斷，badge 永遠佔得到位置（flex:none）——
   反過來寫的話，長標題會把狀態擠出卡片，而狀態正是使用者打開這一頁要看的東西。 */
.ds-listcard-head{display:flex;align-items:flex-start;gap:var(--sp-8)}
.ds-listcard-title{
  flex:1 1 auto;min-width:0;
  font:600 var(--fs-body)/1.35 var(--sans);color:var(--text);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.ds-listcard-head .ds-badge{flex:none}
/* 摘要：最多兩行，超過收 …。用行數而非字數，才不會因為中英文混排而長短不一。 */
.ds-listcard-sub{
  font:400 var(--fs-label)/1.5 var(--sans);color:var(--muted);
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
}
/* 次要資訊列（時間、單號、目前關卡）：最小字級、可換行；用「・」分隔由採用者自己給 */
.ds-listcard-meta{
  display:flex;align-items:center;gap:var(--sp-6);flex-wrap:wrap;
  font:400 var(--fs-hint)/1.4 var(--sans);color:var(--faint);
}
.ds-listcard-meta .ds-mono{font-size:inherit}

/* ================= 分頁器 ================= */
.ds-pagination{display:inline-flex;align-items:center;gap:6px}
.ds-page-btn{
  width:var(--h-btn);height:var(--h-btn);border-radius:var(--r-btn);border:1px solid transparent;
  display:inline-flex;align-items:center;justify-content:center;
  background:transparent;color:var(--text);font:500 var(--fs-body)/1 var(--sans);cursor:pointer;
  transition:background var(--t-fast);
}
.ds-page-btn:hover,.ds-page-btn:active{background:var(--ink-tint-05)}
.ds-page-btn.cur{background:var(--ink);color:var(--on-ink);border-color:var(--line)}
.ds-page-btn[disabled]{opacity:.35;cursor:not-allowed;pointer-events:none}

/* ================= 說明鈕 Infotip（2026-08-12 v1.5.0 新增）=================
   **這個元件的存在理由是「把兩種提示分開」**（使用者 2026-08-12 提出的分型）：

     ①說明型（功能介紹／補充）  → 看過一次就不用再看 → **本元件**
     ②狀態型（這一筆特有）      → 必須被讀到、每筆內容都不同 → `.ds-alert`
     ③區塊規則                  → 只在那個區塊內有效 → `.ds-alert` 放在那張卡內

   混用的代價是真的：權限申請的稽核紀錄與申請系統管理，把①塞進 `.ds-alert`，
   結果是整頁最重的色塊每一次都在搶視線，講的卻是看過一次就不用再看的事；
   而且它跟頁面副標講同一件事——其中一頁還互相矛盾（副標沒跟著拍板改）。

   ── 互動契約（2026-08-12 拍板：**可 hover 可點擊**）──
   ・**點擊**：所有裝置都可用（觸控裝置只有這條路——「主管用手機審核」是既定主情境）
   ・**hover／focus**：只在 `pointer:fine` 生效，是桌機的加速路徑，不是唯一路徑
   ・WCAG 1.4.13 三條全要滿足：**可關**（Esc／點外面）／**可 hover**（游標移進氣泡不會關掉，
     所以離開時留一段延遲、進入氣泡時取消關閉）／**持續**（不會自己消失）
   ⚠️ **不要退回純 hover tooltip**——那等於手機使用者永遠看不到這段說明。
      本專案 2026-08-10 曾把頁面說明從 tooltip 改成可見副標，正是同一個病的另一面：
      副標**留著**（縮短成一句定位），收進本元件的是**額外補充**，兩者不衝突。 */
.ds-infotip{
  --ring:22px;   /* 元件內部變數（非 design token）：看得見的圓圈直徑。掛在容器上，
                    因為 coarse 段的 .ds-infotip-pop 位移算式也要讀同一份值。 */
  position:relative;display:inline-flex;vertical-align:middle;
}
/* ⚠️ **刻意字面值**（2026-08-28 v1.17.0 補齊理由）：22 是「圓形說明鈕的視覺直徑」，
   與 .ds-badge 的膠囊高度**同值但不同語意**，所以不共用 --ds-badge-h——同值不同語意收成一顆，
   只是把兩個東西錯綁在一起，下次改一邊就兩邊一起錯（同 --surface-2 與 --slate-bg 的分家理由）。
   也不進 --h-* 家族：那個家族帶 coarse 放大契約，而這顆圈**刻意在觸控下維持 22**（見下方 coarse 段）。
   判準三條（跨族共用／情境改寫／≥3 落點且語意相同）都不滿足 ⇒ 元件內部常數，維持字面值。
   ⚠️ 2026-08-28：coarse 段原本有三處手算的 22（負 margin、::before 直徑、氣泡位移），
      改圈大小要同時改三處、漏一處泡泡就偏位 ⇒ 收成元件內部變數 --ring（computed 值零變動）。 */
.ds-infotip-btn{
  position:relative;                 /* coarse 段的 ::before 圈要靠它定位 */
  width:var(--ring);height:var(--ring);flex:none;padding:0;
  border:1px solid var(--line-strong);border-radius:50%;background:transparent;
  color:var(--faint);font:italic 700 12px/1 var(--sans);cursor:pointer;
  display:grid;place-items:center;transition:background var(--t-fast),border-color var(--t-fast),color var(--t-fast);
}
.ds-infotip-btn:hover,.ds-infotip-btn:active{border-color:var(--ink);color:var(--ink);background:var(--ink-tint-05)}
.ds-infotip-btn[aria-expanded="true"]{background:var(--ink);border-color:var(--ink);color:var(--on-ink)}
/* 觸控裝置：命中盒放大到 --h-btn（coarse 下＝--hit-min），視覺圓圈維持 --ring（同 .ds-check-cell 的「命中盒與視覺尺寸分離」手法） */
.ds-infotip-pop{
  position:absolute;top:calc(100% + 8px);left:-6px;z-index:var(--z-popover);
  width:max-content;max-width:min(380px,calc(100vw - 40px));
  padding:var(--sp-12) var(--sp-14);
  background:var(--surface);border-radius:var(--r-input);box-shadow:var(--sh-pop);
  font:400 var(--fs-label)/1.6 var(--sans);color:var(--text);
  text-align:left;white-space:normal;letter-spacing:normal;
}
.ds-infotip-pop[hidden]{display:none}
/* 氣泡裡只允許最低限度的行內語意——它是補充說明，不是版面 */
.ds-infotip-pop b,.ds-infotip-pop strong{font-weight:600}
.ds-infotip-pop p{margin:var(--sp-6) 0 0}
.ds-infotip-pop p:first-child{margin-top:0}

/* ================= Popover（表頭篩選型） ================= */
.ds-popover{
  position:absolute;z-index:var(--z-popover);width:220px;background:var(--surface);border-radius:var(--r-input);
  box-shadow:var(--sh-pop);overflow:hidden;
}
.ds-popover-head{
  display:flex;align-items:center;gap:8px;padding:10px 12px;background:var(--ink);color:var(--on-ink);
  font:600 var(--fs-label)/1 var(--sans);
}
.ds-popover-head .ds-popover-count{
  margin-left:auto;padding:2px 8px;border-radius:var(--r-full);background:color-mix(in srgb,var(--on-ink) 20%,transparent);font-size:var(--fs-sub);
}
.ds-popover-body{padding:8px;display:flex;flex-direction:column;gap:2px;max-height:260px;overflow-y:auto}
.ds-popover-foot{padding:8px 12px;border-top:1px solid var(--line-soft)}

/* ================= 對話框：兩種型態（2026-08-11 拍板） =================
   分界一句話：**「要不要」用 ds-modal，「填什麼」用 ds-dialog**。

   ┌ .ds-modal（確認型）──────────────────────────────────────────────
   │ 使用者只做「要或不要」的判斷，**不含任何輸入欄位**。
   │ 形態：圓形圖示＋標題＋一段說明＋（可選）受影響對象清單 .ds-reflist＋兩顆鈕。
   │ 用於：破壞性／不可逆動作的攔截（確認移除、發起移除、離開會清空已填內容）。
   │ 特徵是**沒有捲軸**——內容一眼看完才叫確認；要捲的內容代表它其實是作業。
   │ JS：DS.confirm()（DS.guard 為其別名，維持既有呼叫）
   └────────────────────────────────────────────────────────────────
   ┌ .ds-dialog（作業型）─────────────────────────────────────────────
   │ **要填東西才能完成**的動作。形態：標題列＋可捲動 body（含表單欄位）＋footer 動作列。
   │ 用於：駁回（原因必填）、任何帶欄位驗證的動作。
   │ JS：DS.dialog()
   │
   │ ▸ dismiss-only 模式（2026-08-11 v1.3.0）：DS.dialog({dismissOnly:true}) ／別名 DS.view()
   │   給「唯讀檢視」用——稽核明細、單據異動紀錄這種「一張唯讀表格＋一顆關閉鈕」。
   │   footer 只出一顆「關閉」，**沒有確定／取消**（那組按鈕會暗示有東西要提交），
   │   焦點落在關閉鈕（沒有可填欄位可落）。
   │   ⚠️ 這是作業型的一個**模式**，不是第三種對話框型態。判斷過：唯讀檢視與作業型的
   │      形態完全相同（可捲 body、head/foot 固定），差別只在「動作集合是空的」；
   │      為此多開一個型態要付出三套 CSS、三個 API、更複雜的決策表，換到的只是換掉一顆鈕的文字。
   │      分型態的判準是**失敗模式不同**（見下），唯讀檢視沒有自己的失敗模式。
   └────────────────────────────────────────────────────────────────
   為什麼不收斂成一套：兩者的**失敗模式不同**。確認型要的是「看清楚再按」，
   多一個輸入框就會讓人先去填欄位、跳過那段說明；作業型要的是「填得完、驗得到」，
   套進確認型的置中小卡就會沒地方放錯誤訊息與捲動。硬合成一套的結果是兩邊都不好用。
   共用：.ds-modal-backdrop（遮罩與置中）、按鈕、焦點環、z-index 階層。 */
.ds-modal-backdrop{
  position:fixed;inset:0;z-index:var(--z-modal);background:var(--backdrop);
  display:flex;align-items:center;justify-content:center;padding:20px;
}
.ds-modal{
  width:min(440px,100%);background:var(--surface);border-radius:var(--r-menu);
  padding:24px;display:flex;flex-direction:column;gap:14px;box-shadow:var(--sh-menu);
}
.ds-modal-head{display:flex;align-items:flex-start;gap:12px}
/* ⚠️ **40 刻意字面值**（2026-08-28 v1.17.0 補理由，沿用 Changelog v1.14.0 的既有立場）：
   這是警示圓圖示的視覺直徑，單一落點、只服務確認型對話框的標題列。
   不吃 --h-icon：那個家族帶 coarse 放大契約，而它不是點擊目標。
   判準三條（跨族共用／情境改寫／≥3 落點且語意相同）皆不滿足 ⇒ 元件內部常數，維持字面值。 */
.ds-modal-ico{
  width:40px;height:40px;border-radius:50%;flex:none;display:flex;align-items:center;justify-content:center;font-size:18px;
}
.ds-modal-ico.warn{background:var(--amber-bg);color:var(--amber)}
.ds-modal-ico.danger{background:var(--red-bg);color:var(--red)}
.ds-modal-title{font:700 var(--fs-title)/1.3 var(--sans);color:var(--text)}
.ds-modal-body{font:400 var(--fs-body)/1.6 var(--sans);color:var(--muted)}
.ds-modal-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:4px}
/* 受影響對象清單：確認型專用。破壞性動作要讓人看見「會動到誰」，不是只給一個數字。
   超過 6 筆才捲（再多就該改成作業型或先做篩選）。 */
.ds-reflist{
  margin:0;padding:10px 12px;list-style:none;max-height:168px;overflow-y:auto;
  background:var(--ground);border-radius:var(--r-input);
  font:400 var(--fs-label)/1.7 var(--sans);color:var(--text);
}
.ds-reflist li+li{border-top:1px solid var(--line)}

/* ---- .ds-dialog（作業型）---- */
.ds-dialog{
  width:min(460px,100%);max-height:88vh;background:var(--surface);
  border-radius:var(--r-menu);box-shadow:var(--sh-menu);
  display:flex;flex-direction:column;overflow:hidden;
}
.ds-dialog-head{
  flex:none;display:flex;align-items:center;gap:10px;
  padding:var(--sp-16) var(--sp-20);border-bottom:1px solid var(--line);
}
.ds-dialog-title{font:var(--fw-title) var(--fs-title)/1.3 var(--sans);color:var(--text);margin:0}
.ds-dialog-body{flex:1;overflow-y:auto;padding:var(--sp-14) var(--sp-20) var(--sp-16)}
.ds-dialog-foot{
  flex:none;display:flex;align-items:center;justify-content:flex-end;gap:10px;
  padding:var(--sp-14) var(--sp-20);border-top:1px solid var(--line);background:var(--ground);
}

/* ================= 抽屜（浮動卡片式） ================= */
.ds-drawer-backdrop{position:fixed;inset:0;z-index:var(--z-drawer);background:var(--backdrop)}
.ds-drawer{
  position:fixed;z-index:calc(var(--z-drawer) + 100);top:10px;right:10px;bottom:10px;width:min(900px,calc(100vw - 20px));
  background:var(--surface);border-radius:var(--r-drawer);display:flex;flex-direction:column;
  box-shadow:var(--sh-menu);
}
.ds-drawer-head{
  height:var(--h-bar);flex:none;display:flex;align-items:center;gap:10px;
  padding:0 16px 0 24px;border-bottom:1px solid var(--line-soft);
  font:700 var(--fs-panel)/1.2 var(--sans);color:var(--text);
}
.ds-drawer-head .ds-drawer-x{margin-left:auto}
.ds-drawer-body{flex:1;overflow-y:auto;padding:20px 24px}
.ds-drawer-foot{
  height:var(--h-bar);flex:none;display:flex;align-items:center;justify-content:flex-end;gap:10px;
  padding:0 16px;border-top:1px solid var(--line-soft);
}

/* ================= Section 標題 ================= */
.ds-section{display:flex;align-items:baseline;gap:8px;margin:18px 0 10px}
.ds-section-title{font:600 var(--fs-section)/1.3 var(--sans);color:var(--ink)}
.ds-section-sub{font:400 var(--fs-hint)/1.4 var(--sans);color:var(--faint)}

/* ================= Toast =================
   2026-08-11 v1.3.0：補**定位與進出場**。原本這裡只定義了「它長什麼樣」，沒有定義
   「它出現在哪、怎麼進來、怎麼走」——於是唯一的採用者自己補了一份（固定定位＋滑入＋圓形圖示），
   庫的版本從此沒人用。缺的那三件事才是每個專案都會重做一次的部分。
   規格：置中貼底、由下滑入、2.4 秒自動消失、不吃滑鼠事件（不擋住底下的按鈕）。
   疊層走 --z-toast（第八階，必須蓋過 modal——對話框裡的動作失敗就是靠它報錯）。
   行為層＝DS.toast(msg, {type,duration})，見 components.js。 */
.ds-toast{
  position:fixed;z-index:var(--z-toast);left:50%;bottom:var(--sp-24);
  transform:translateX(-50%) translateY(var(--sp-20));opacity:0;pointer-events:none;
  display:flex;align-items:center;gap:10px;padding:12px 16px;border-radius:var(--r-input);
  background:var(--ink);color:var(--on-ink);font:400 var(--fs-label)/1.4 var(--sans);box-shadow:var(--sh-menu);
  max-width:min(420px,calc(100vw - 32px));
  transition:opacity var(--t-med),transform var(--t-med);
}
.ds-toast.is-open{opacity:1;transform:translateX(-50%) translateY(0)}
/* 圖示是實心圓底＋白色符號（不是彩色字）：toast 底是深墨，狀態色文字壓在上面讀不出來。
   ok＝green 底打勾／error＝red 底驚嘆號。18px 是裝飾性圓點尺寸，同時間軸的 16px 圓點。 */
.ds-toast-ico{
  width:18px;height:18px;border-radius:50%;flex:none;
  display:grid;place-items:center;font-size:11px;background:var(--green);color:var(--on-green);
}
.ds-toast.is-error .ds-toast-ico{background:var(--red);color:var(--on-danger)}
/* 型錄／文件裡要靜態展示一顆 toast 時用它：解除 fixed 與隱藏，只留外觀 */
.ds-toast-static{position:static;transform:none;opacity:1;pointer-events:auto}

/* ================= 觸控裝置：元件層補充（2026-08-11 v1.3.0）=================
   絕大多數的觸控放大在 tokens.css 就做完了（改高度 token ＝全庫生效）。
   這裡只放**高度不是由 token 決定**的那幾個——它們的可點區是由內容或 padding 撐出來的，
   token 管不到，只能在元件層補 min-height。
   判準與 tokens.css 同一條：看**輸入裝置**（pointer:coarse），不看螢幕寬度。 */
@media (pointer: coarse){
  /* 文字連結常常是表格列裡唯一的鍵盤／觸控入口，卻只有一行字的高度 */
  .ds-link{min-height:var(--hit-min);display:inline-flex;align-items:center}
  /* 勾選框與 switch：方塊本身維持 20px（放大方塊會讓表單看起來像兒童玩具），
     改用 label 的 min-height 撐命中區——命中盒與視覺尺寸分離，同 .ds-check-cell 的手法。
     ⚠️ .ds-check-cell 已自己處理過（負 margin 不撐列高），這裡不會疊加衝突：
        它的 min-height 是 var(--h-btn)，在 coarse 下與本條同值（都是 --hit-min）。 */
  .ds-checkbox,.ds-switch{min-height:var(--hit-min)}
  /* 選單／帳戶選單型的選項列 base 就已經是 --hit-min（.ds-option），不需要再放大 */
  /* 說明鈕：命中盒放大到命中下限，**視覺圓圈維持 --ring**——放大圓圈會讓它變成畫面上的一個主角，
     但它只是補充說明的入口。手法同 .ds-check-cell：**命中盒與視覺尺寸分離**，
     差別是這裡用負 margin 讓它在版面上仍只佔 --ring（不然標題列會被撐高）。 */
  .ds-infotip-btn{
    width:var(--h-btn);height:var(--h-btn);
    margin:calc((var(--ring) - var(--h-btn)) / 2);
    border-color:transparent;background:transparent;
  }
  /* 那顆看得見的圈（--ring）改由 ::before 畫（絕對定位置中，不參與 grid 流） */
  .ds-infotip-btn::before{
    content:"";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
    width:var(--ring);height:var(--ring);border-radius:50%;
    border:1px solid var(--line-strong);transition:background var(--t-fast),border-color var(--t-fast);
  }
  .ds-infotip-btn[aria-expanded="true"]::before{background:var(--ink);border-color:var(--ink)}
  /* 氣泡改吃命中盒的左緣，位置才不會因為負 margin 而偏 */
  .ds-infotip-pop{top:calc(100% + 8px - (var(--ring) - var(--h-btn)) / 2)}
}

/* ================= 應用外殼 App Shell（2026-08-13 v1.9.0 新增） =================
   來源＝三份實作對照，不是憑一份抽的：
     ①權限申請 `#sidebar` 家族（真實專案，有登入態／帳號選單／手機抽屜）
     ②採購審核 `.sidebar` 家族（第二支獨立 app，**對本庫採用率 0**）
     ③`theme-lab.html` 的 `.lab-side`／`[data-shell="framed"]`（純 token 驅動、深色外框）
   ①與②幾乎逐值相同（216 寬／brand 內距 18·gap 11／mark 32／名稱 15／副標 10.5 uppercase .1em／
   nav 內距 12·gap 3／navitem 內距 10·12＋圖示欄 20／計數 mono 11 膠囊／頭像 30＋上分隔線），
   兩支不同業務的 app 獨立長出同一份規格＝通用性沒有疑問。三處不一致的取捨：
     ・navitem 圓角 採購 11px（階梯外髒值）→ 取權限的 --r-btn
     ・角色切換 採購用原生 <select> → 取權限進化後的 .ds-acct ＋ 錨定選單
     ・手機抽屜 採購沒有 → 取權限的（「主管用手機審核」是既定主情境）

   ⚠️ **深色外框是預設**（2026-08-13 使用者拍板）。要白側欄請用 data-shell="plain"。
      `plain` 不是降級版，它就是①②兩份產品實作的現況——採用者可以先用 plain 平移過來、
      之後再拿掉屬性切到深色，遷移不必一次到位。

   ⚠️ **stacking context（原則 7）**：側欄是 position:sticky，而 sticky 會建立 stacking context。
      2026-08-11 踩過的真實 bug 就是側欄的通知面板被主內容蓋住。所以側欄自己必須有明確的
      --z-nav，浮層（帳號選單／通知面板）掛在側欄**內部**才蓋得住主內容。
      不要把浮層掛到 .ds-main 裡再想辦法拉高 z-index，那是反過來打。 */

.ds-appshell{
  display:grid;grid-template-columns:var(--side-w-framed) minmax(0,1fr);
  align-items:start;min-height:100vh;min-height:100dvh;
  /* 2026-08-14 v1.13.1：上內距由 0 補成 12，**四邊一致**（使用者拍板）。
     原本是 `0 12px 12px`＝側欄與主面板直接貼著視窗頂緣，深色外框只有三邊、
     上面沒有框——「外框」這個說法在頂邊是不成立的。
     ⚠️ 改這個值要連動下面三處（它們都是從框內距算出來的）：
       ・.ds-side 的 sticky top（要等於上內距，否則捲動時側欄內容會滑進框的頂邊）
       ・.ds-side 的高度（要扣掉上＋下＝24）
       ・.ds-main 的最小高度（同上）
     ⚠️ 白側欄（data-shell="plain"）與 ≤820 抽屜都是 padding:0，**不受此值影響**，
        但 plain 的側欄必須把 top 還原成 0，見下方。 */
  background:var(--ink);padding:12px;
}
.ds-side{
  position:sticky;top:12px;z-index:var(--z-nav);   /* 12 ＝外殼上內距，捲動時不滑進框的頂邊 */
  height:calc(100vh - 24px);height:calc(100dvh - 24px);   /* 24 ＝外殼上＋下內距，側欄不壓到框邊 */
  display:flex;flex-direction:column;min-width:0;
  background:transparent;color:var(--on-dark-2);
}
.ds-main{
  min-width:0;background:var(--ground);border-radius:var(--r-drawer);
  min-height:calc(100vh - 24px);min-height:calc(100dvh - 24px);
}

/* ---- 白側欄（＝兩份產品實作的現況；遷移用的平移目標） ---- */
.ds-appshell[data-shell="plain"]{
  grid-template-columns:var(--side-w) minmax(0,1fr);background:var(--ground);padding:0;
}
.ds-appshell[data-shell="plain"] .ds-side{
  top:0;                      /* ⚠️ 必須還原：plain 沒有外框內距（padding:0），
                                 沿用 framed 的 top:12px 會在側欄上方留一條沒來由的縫 */
  height:100vh;height:100dvh;
  background:var(--surface);border-right:1px solid var(--line);color:var(--muted);
}
.ds-appshell[data-shell="plain"] .ds-main{background:transparent;border-radius:0;min-height:0}

/* ---- 品牌區 ---- */
.ds-side-brand{display:flex;align-items:center;gap:11px;padding:18px;min-width:0}
/* ⚠️ **32 刻意字面值**（2026-08-28 v1.17.0 補理由，沿用 Changelog v1.14.0 的既有立場）：
   品牌標記的視覺直徑，兩份獨立實作逐值相同（見上方三份實作對照）但只有這一個落點。
   不吃 --h-icon（coarse 放大契約，它不是點擊目標）⇒ 元件內部常數，維持字面值。 */
.ds-side-mark{
  width:32px;height:32px;border-radius:var(--r-btn);flex:none;
  background:var(--cta);color:var(--on-cta);display:grid;place-items:center;
}
.ds-side-mark svg{width:18px;height:18px}
.ds-side-name{font:var(--fw-app) var(--fs-app)/1.25 var(--sans);color:var(--on-dark);min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* 副標刻意 10.5px＋大字距全大寫：它是「這是哪個系統」的識別，不是要被讀的句子。
   10.5 在字級階梯外，是階梯建立前就存在的值——兩份產品實作都是 10.5，故照收不動。 */
.ds-side-sub{font-size:10.5px;color:var(--on-dark-2);letter-spacing:.1em;text-transform:uppercase}
.ds-appshell[data-shell="plain"] .ds-side-name{color:var(--text)}
.ds-appshell[data-shell="plain"] .ds-side-sub{color:var(--faint)}
.ds-appshell[data-shell="plain"] .ds-side-brand{border-bottom:1px solid var(--line)}

/* ---- 主行動鈕區 ---- */
.ds-side-cta{padding:14px 12px 4px}
.ds-side-cta>.ds-btn{width:100%;justify-content:center}

/* ---- 導覽 ---- */
.ds-side-nav{padding:12px;display:flex;flex-direction:column;gap:3px;flex:1;overflow-y:auto;min-height:0}
.ds-side-navlabel{
  font:600 var(--fs-sub)/1 var(--sans);color:var(--on-dark-2);
  letter-spacing:.06em;padding:12px 10px 6px;
}
.ds-appshell[data-shell="plain"] .ds-side-navlabel{color:var(--faint)}
.ds-navitem{
  display:flex;align-items:center;gap:11px;width:100%;min-height:var(--h-btn);
  padding:10px 12px;border:0;border-radius:var(--r-btn);background:transparent;
  color:inherit;font:500 var(--fs-body)/1.3 var(--sans);text-align:left;cursor:pointer;
}
.ds-navitem:hover,.ds-navitem:active{background:var(--on-dark-tint-06);color:var(--on-dark)}
.ds-navitem[aria-current="page"]{background:var(--ink-hi);color:var(--on-dark);font-weight:700}
.ds-appshell[data-shell="plain"] .ds-navitem:hover,.ds-appshell[data-shell="plain"] .ds-navitem:active{background:var(--hover-bg);color:var(--muted)}
.ds-appshell[data-shell="plain"] .ds-navitem[aria-current="page"]{
  background:var(--nav-sel-bg);color:var(--ink);font-weight:700;
}
.ds-navitem-ico{width:20px;flex:none;display:inline-flex;align-items:center;justify-content:center}
.ds-navitem-ico svg{width:18px;height:18px}
/* 選中項的圖示在白側欄轉品牌橘＝橘在畫面上的稀有落點之一（原則 2）。
   深色側欄不轉：深底上橘的可讀性不足，且選中態已經由 --ink-hi 底色講清楚了。 */
.ds-appshell[data-shell="plain"] .ds-navitem-ico{color:var(--faint)}
.ds-appshell[data-shell="plain"] .ds-navitem[aria-current="page"] .ds-navitem-ico{color:var(--cta)}
.ds-navitem-count{
  margin-left:auto;flex:none;font:600 11px/1 var(--mono);
  padding:3px 7px;border-radius:var(--r-full);
  background:var(--on-dark-tint-10);color:var(--on-dark-2);
}
.ds-navitem[aria-current="page"] .ds-navitem-count{color:var(--on-dark)}
.ds-appshell[data-shell="plain"] .ds-navitem-count{background:var(--surface-2);color:var(--muted)}
.ds-appshell[data-shell="plain"] .ds-navitem[aria-current="page"] .ds-navitem-count{
  background:var(--surface);color:var(--ink);
}

/* ---- 底部帳號區 ---- */
.ds-side-foot{
  margin-top:auto;border-top:1px solid var(--on-dark-line);
  padding:14px 16px;display:flex;align-items:center;gap:10px;min-width:0;
  position:relative;   /* ＝帳號選單的定位基準 */
}
.ds-appshell[data-shell="plain"] .ds-side-foot{border-top-color:var(--line)}
/* ⚠️ **30 刻意字面值**（2026-08-28 v1.17.0 補理由）：頭像直徑，兩份獨立實作逐值相同。
   它與 .ds-notif-ico 的 30 是**同值不同語意**（頭像 vs 通知型別圖示），刻意不收成一顆——
   同值不同語意收成一顆只是把兩個東西錯綁在一起。不吃 --h-icon（coarse 放大契約）。
   ⇒ 元件內部常數，維持字面值。展示見 showcase-appshell.html（型錄第 22 節的 iframe）。 */
.ds-avatar{
  width:30px;height:30px;border-radius:50%;flex:none;display:grid;place-items:center;
  font:700 13px/1 var(--sans);background:var(--on-dark-tint-14);color:var(--on-dark);
}
.ds-appshell[data-shell="plain"] .ds-avatar{background:var(--ink);color:var(--on-ink)}
.ds-acct{
  flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:10px;
  border:0;background:transparent;padding:6px;margin:-6px;border-radius:var(--r-btn);
  cursor:pointer;text-align:left;font-family:inherit;color:inherit;
}
.ds-acct:hover,.ds-acct:active{background:var(--on-dark-tint-06)}
.ds-appshell[data-shell="plain"] .ds-acct:hover,.ds-appshell[data-shell="plain"] .ds-acct:active{background:var(--ink-tint-05)}
.ds-acct-meta{min-width:0;flex:1 1 auto}
.ds-acct-name{
  display:block;font:600 13px/1.3 var(--sans);color:var(--on-dark);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.ds-acct-role{
  display:block;font-size:11.5px;color:var(--on-dark-2);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.ds-appshell[data-shell="plain"] .ds-acct-name{color:var(--text)}
.ds-appshell[data-shell="plain"] .ds-acct-role{color:var(--muted)}
.ds-acct-chev{flex:none;font-size:13px;transition:transform var(--t-fast)}
.ds-acct[aria-expanded="true"] .ds-acct-chev{transform:rotate(180deg)}

/* ---- 深色外框下的 icon 鈕（2026-08-13 拍板：只在此覆寫，不動淺色下的現況）----
   庫的 .ds-btn-icon 吃 --muted，在深底上是深壓深＝不可用（theme-lab 曾被迫自畫 .lab-icon-btn 繞過）。
   ⚠️ 這是治標：任何「深底上放 icon 鈕」的新情境（深色 KPI 卡、深色 toast）都要再寫一次。
      根治是讓 .ds-btn-icon 吃 currentColor，但那會改變所有既有採用者的淺色外觀，已登記待拍板。 */
.ds-appshell:not([data-shell="plain"]) .ds-btn-icon{color:var(--on-dark-2)}
.ds-appshell:not([data-shell="plain"]) .ds-btn-icon:hover,.ds-appshell:not([data-shell="plain"]) .ds-btn-icon:active{
  background:var(--on-dark-tint-10);color:var(--on-dark);
}

/* ---- 錨定式通用選單（backlog P1；.ds-select-menu 是 select 專用，不能當通用 menu）----
   預設往上開（bottom:100%）：它最常見的位置是側欄底部的帳號區，往下開會開到視窗外。 */
.ds-menu{
  position:absolute;left:12px;right:12px;bottom:calc(100% + 6px);z-index:var(--z-popover);
  background:var(--surface);border-radius:var(--r-menu);box-shadow:var(--sh-menu);
  padding:6px;display:none;
}
.ds-menu.is-open{display:block}
.ds-menu-scroll{max-height:min(46vh,260px);overflow-y:auto}
.ds-menu-group{font-size:var(--fs-hint);color:var(--faint);padding:6px 10px 4px}
.ds-menu-item{
  display:flex;align-items:center;gap:8px;width:100%;min-height:var(--h-btn);
  padding:8px 10px;border:0;border-radius:var(--r-btn);background:transparent;
  color:var(--text);font:400 var(--fs-body)/1.3 var(--sans);text-align:left;cursor:pointer;
}
.ds-menu-item:hover,.ds-menu-item:active{background:var(--ink-tint-05)}
.ds-menu-item[aria-current="true"]{font-weight:600}
.ds-menu-tick{width:14px;flex:none;font-size:12px;color:var(--ink);opacity:0}
.ds-menu-item[aria-current="true"] .ds-menu-tick{opacity:1}
.ds-menu-cap{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  color:var(--faint);font-size:11.5px}
.ds-menu-sep{height:1px;background:var(--line);margin:6px 4px}
.ds-menu-item.ds-menu-danger{color:var(--red)}
.ds-menu-item.ds-menu-danger:hover,.ds-menu-item.ds-menu-danger:active{background:var(--red-bg)}

/* ---- ≤820：側欄改抽屜 ----
   斷點取 --bp-md(820)：820 以下走抽屜是 2026-07-31 既有拍板（641–820 亦然）。 */
.ds-side-overlay{display:none;position:fixed;inset:0;background:var(--backdrop);z-index:var(--z-nav)}
.ds-side-overlay.is-open{display:block}
@media (max-width:820px){
  .ds-appshell,.ds-appshell[data-shell="plain"]{grid-template-columns:minmax(0,1fr);padding:0}
  .ds-appshell{background:var(--ground)}
  .ds-main{border-radius:0;min-height:0}
  .ds-side,.ds-appshell[data-shell="plain"] .ds-side{
    position:fixed;left:0;top:0;height:100vh;height:100dvh;
    width:var(--side-w-drawer);z-index:calc(var(--z-nav) + 1);
    transform:translateX(-100%);transition:transform .22s ease;box-shadow:var(--sh-pop);
  }
  /* 抽屜一律用白底：深色外框的前提是「框住主內容」，而抽屜蓋在內容上、沒有框可言 */
  .ds-side{background:var(--surface);border-right:1px solid var(--line);color:var(--muted)}
  .ds-side-name{color:var(--text)} .ds-side-sub,.ds-side-navlabel{color:var(--faint)}
  .ds-acct-name{color:var(--text)} .ds-acct-role{color:var(--muted)}
  .ds-side-foot{border-top-color:var(--line)}
  .ds-navitem:hover,.ds-navitem:active{background:var(--hover-bg)}
  .ds-navitem[aria-current="page"]{background:var(--nav-sel-bg);color:var(--ink)}
  .ds-navitem-count{background:var(--surface-2);color:var(--muted)}
  .ds-avatar{background:var(--ink);color:var(--on-ink)}
  /* ⚠️ 抽屜是白底，所以深色外框那條 icon 鈕覆寫必須在這裡還原——
     否則 --on-dark-2（52% 白）壓在白抽屜上＝整顆圖示看不見（實測踩到，見 v1.9.0 changelog）。
     選擇器多帶一層 .ds-side 把 specificity 拉到 (0,4,0)，勝過上面那條 (0,3,0)，
     不靠「後面的規則贏」這種順序巧合。 */
  .ds-appshell:not([data-shell="plain"]) .ds-side .ds-btn-icon{color:var(--muted)}
  .ds-appshell:not([data-shell="plain"]) .ds-side .ds-btn-icon:hover,.ds-appshell:not([data-shell="plain"]) .ds-side .ds-btn-icon:active{
    background:var(--ink-tint-05);color:var(--text);
  }
  .ds-side.is-open{transform:translateX(0)}
  /* 🔴 v1.18.0 bug 修復：上面那條抽屜規則是**合併選擇器**，對 plain 命中的是
     `.ds-appshell[data-shell="plain"] .ds-side` ＝ (0,3,0)，勝過 `.ds-side.is-open` 的 (0,2,0)
     ⇒ data-shell="plain" 的抽屜**永遠打不開**（body 鎖捲動、遮罩蓋上來，但抽屜留在畫面外）。
     補一條 (0,4,0) 的 plain 分支把開啟態拉回來。不靠「後面的規則贏」，與上面 icon 鈕覆寫同一個手法。 */
  .ds-appshell[data-shell="plain"] .ds-side.is-open{transform:translateX(0)}
}
@media (prefers-reduced-motion:reduce){
  /* 🔴 v1.18.0 bug 修復（同一個合併選擇器的第二個後果）：≤820 那條對 plain 是 (0,3,0)，
     原本的還原規則只有 (0,1,0) ⇒ plain 的抽屜在 reduced-motion 下動畫照跑。
     補一條同為 (0,3,0)、行號較大的 plain 分支。 */
  .ds-side,.ds-appshell[data-shell="plain"] .ds-side{transition:none}
}

/* ================= 表格欄寬政策（2026-08-13 v1.10.0，使用者拍板） =================
   **每一欄宣告最小寬度；加總放不下時，在列表區塊內橫捲——不擠壓、不腰斬。**

   它取代的是「調斷點／砍欄位」那一類做法。理由是那些都在遷就欄數，而欄數會變：
   權限申請的待開通表即將從 7 欄變 8 欄（PRD v0.5 加「權限」「開通範圍」兩欄）。
   照當下欄數反推出來的斷點，下次加欄就失效。**這條規則把 RWD 從欄數問題變成算術問題。**

   R1 每欄宣告 min-width（用下方級距），表格 min-width ＝各欄加總
   R2 放不下就在列表區塊內橫捲（沿用 .ds-scrollx；body 永不橫捲）
   R3 操作欄 position:sticky right:0，橫捲時主要動作永遠可按

   ⚠️ **級距是建議值不是量測值**，採用前請對自己的真實資料量一次 min-content。
      特別是含 email／館別字串的欄位——依 v1.7.0 的語意換行政策它們不可斷詞，
      單一欄就可能撐破級距。 */
.ds-col-xs{min-width:64px}    /* 數字、天數 */
.ds-col-sm{min-width:88px}    /* 短標籤、狀態 badge */
.ds-col-id{min-width:96px}    /* 單號等 mono 識別字串 */
.ds-col-md{min-width:120px}   /* 一般文字：人名、系統名 */
.ds-col-lg{min-width:180px}   /* 長文字、多值摘要、雙按鈕操作欄 */

/* ---- R3 黏右操作欄 ----
   ⚠️ **sticky 必須有不透明底，而且要覆蓋三種列狀態**：一般／hover／勾選。
      少一種，滑過去就會在黏住的那一欄看到色差斷層（底下的內容透出來）。 */
.ds-table .ds-col-sticky{position:sticky;right:0;background:var(--surface);z-index:1}
.ds-table-clickable tbody tr:hover .ds-col-sticky,.ds-table-clickable tbody tr:active .ds-col-sticky,
.ds-table tbody tr.ds-row-clickable:hover .ds-col-sticky,.ds-table tbody tr.ds-row-clickable:active .ds-col-sticky{background:var(--hover-bg)}
.ds-table tbody tr:has(input[type=checkbox]:checked) .ds-col-sticky{background:var(--nav-sel-bg)}
/* ⚠️ 表頭**不要**另給灰底。實測 .ds-table 的 thead／tr／th 背景全是 transparent——
   表頭的視覺分界靠的是底線（1px --line）不是色塊，它直接坐在白卡上。
   初版誤給 --surface-2，結果黏住的那一格在表頭多出一條灰帶（已修）。
   所以表頭沿用上面那條 --surface 即可，不需要任何 thead 專屬規則。 */
/* 左緣分隔陰影：**只在真的有東西被蓋住時出現**（無溢出時不該憑空多一條線）。
   `.has-xshadow` 由 DS.initStickyCol 依 scrollLeft 實測後掛上，純 CSS 偵測不到捲動位置。 */
.ds-scrollx.has-xshadow .ds-col-sticky::before{
  content:"";position:absolute;left:0;top:0;bottom:0;width:14px;transform:translateX(-100%);
  background:linear-gradient(to left,rgba(27,29,34,.10),rgba(27,29,34,0));pointer-events:none;
}
/* ⚠️ sticky 會建立 stacking context（原則 7）。這裡刻意用 z-index:1 而不是 --z-* 階梯的值：
   它只需要蓋過同一個表格內的儲存格，不參與全站疊層。
   表格內的浮層（.ds-select 展開、篩選 popover）走既有 --z-select／--z-popover，
   本規則不影響它們——**但採用後請複驗一次**，2026-08-11 就踩過 sticky 把浮層關住的 bug。 */


/* ============================================================================
   v1.13.0（2026-08-14）backlog P1 八項一次結清
   ----------------------------------------------------------------------------
   來源＝兩份實作對照（權限申請 prototype＝A，唯一採用者／採購審核 prototype＝B，
   對本庫採用率 0）。⚠️ 抽取時查證出 backlog 有三處前提是錯的，已在 GUIDELINE 修正：
     ・B 的 .dbwrap/.dbaux 是**零 markup 使用的死樣式**，且血緣是從 B 複製到 A，
       不是「兩份獨立長出來」→ 兩欄版面沒有雙實作背書
     ・B 的 .pagehead 是**返回鈕列**，與 A 的 .page-head（頁標題）是假同源
     ・B 的 .etable（無框儲存格）同樣是死樣式，**核心賣點從未上線**
   真正有雙實作逐值背書的只有：.ftag 六個值、通知面板的面板寬/標題列/未讀徽章整組。

   本輪通則（使用者 2026-08-14 拍板）：**原實作的值與階梯對不上時一律收斂到階梯**，
   唯一採用者的 1–2px 位移直接吸收，逐項列在 GUIDELINE changelog。
   ============================================================================ */


/* ================= 頁標題區 .ds-page-head（P1）=================
   ⚠️ **單一來源元件**：只有 A 一份實作。B 的做法（19px 塞在 sticky 頂欄、無副標）
      正是 A 於 2026-08-10 明文廢除的舊做法，不採計。
   ⚠️ **上內距不歸元件、歸宿主**：兩份實作都把 24/28 寫在內容區容器上。庫的 .ds-main
      目前無 padding，所以用 .ds-appshell 的新 app 若不自己給上內距，26px 標題會直接
      貼在圓角外殼上緣。「內容區內距」已登記為 backlog。 */
.ds-page-head{margin:0 0 var(--sp-20)}   /* 原 18px：階梯外，收斂到 20（A +2px） */
/* ⚠️ h1 用 inline-flex 不是 flex——用 flex 的話 h1 會佔滿整行，
   掛在標題旁的 .ds-infotip（i 鈕）就會被推到頁面最右邊去。 */
.ds-page-head h1,.ds-page-head-title{
  display:inline-flex;align-items:center;gap:var(--sp-8);
  margin:0 0 var(--sp-6);
  font:var(--fw-page) var(--fs-page)/1.25 var(--sans);
  letter-spacing:var(--ls-page);color:var(--text);text-wrap:balance;
}
/* 副標**刻意以 400 重量借用 --fs-section 這一階（13.5）**，不新開 --fs-pagesub：
   兩顆同值 token 的代價是「改一顆忘了另一顆」。借用時重量不同（400 vs --fw-section 600），
   語意不會混淆。 */
.ds-page-head p,.ds-page-head-sub{
  margin:0;font:400 var(--fs-section)/1.6 var(--sans);color:var(--muted);
  max-width:68ch;      /* 行長度（measure），不是間距——庫內無此類 token，也不該為它開一顆 */
  text-wrap:pretty;    /* 避免尾行孤字 */
}
@media (max-width:640px){
  .ds-page-head{margin-bottom:var(--sp-14)}
  /* 20px 不在字級階梯，收斂到 --fs-title(19)：A −1px */
  .ds-page-head h1,.ds-page-head-title{font-size:var(--fs-title)}
  /* ⚠️ 手機第一屏是稀缺資源（A 的 P0 舊案：主要按鈕曾被副標推出第一屏）。 */
  .ds-page-head p,.ds-page-head-sub{display:none}
}


/* ================= 中性分類 tag .ds-tag（P1）=================
   ✅ **本輪證據最強的一項**：A 與 B 解析後六個外觀值 100% 相同（底/字/內距/圓角/字級/字重），
      第三份佐證是 B 自己的 design-system.html。零缺 token，直接進庫。
   ⚠️ **它不是狀態，是分類**。B 拿 .ftag 顯示「已入庫」「已作廢」＝終態狀態，
      那應該用 .ds-badge-slate；B 看起來對只是因為 --slate-bg/--slate 恰好與
      --surface-2/--muted 同 hex。B 日後遷移要逐處判別，不能無腦換名。
   ⚠️ 與既有 .ds-tag-mono 的關係：後者目前是**獨立 class 不是修飾子**（型錄是單獨使用），
      刻意不在本輪改成必須併用——改了會靜靜壞掉。已登記為後續重構項。 */
.ds-tags{display:inline-flex;flex-wrap:wrap;gap:var(--sp-6);align-items:center}
.ds-tag{
  display:inline-flex;align-items:center;   /* 提案值：inline 的垂直內距不進 line box，
                                               塞進 flex 容器時對齊不可控（同 .ds-tag-mono 的做法） */
  padding:var(--sp-2) var(--sp-8);border-radius:var(--r-tag);
  background:var(--surface-2);color:var(--muted);
  font:600 var(--fs-micro)/1.4 var(--sans);white-space:nowrap;
}
/* ⚠️ 為什麼一定要有 .ds-tags 這層容器：A 的樣板把兩顆 ftag 直接相鄰輸出、中間沒有空白字元，
   而 A 的 .ftag 沒有 margin → **相鄰兩顆會黏在一起**。B 是靠 margin-left:6px 避開的，
   但 B 自己在真正的使用情境裡又把 margin 歸零改用容器 gap:6px——B 等於自己否定了 margin 寫法。
   所以庫取「容器 gap」，順手修掉 A 的這個 bug。 */


/* ================= 兩欄詳情版面 .ds-detail-body（P1）=================
   主欄＋右側輔助欄（時間軸／備註通常住在輔助欄）。接在既有 .ds-detail-head 家族後面
   ——那是同一頁的上半身。

   🔴 **用 container query 而不是 media query**（使用者 2026-08-14 拍板）。
      理由是**成因就是容器寬、不是視窗寬**：同樣 900px 的視窗，側欄 216 或 248、
      內容區內距 28 或 14，主欄實際可用寬差很多。A 原本用 @media(max-width:900px)，
      而 900 是庫的原則 8 明令禁止的中間值；若照規矩改成 820，821–900px 這一段會從
      「單欄 764px」變成「兩欄但主欄只剩 235px」——守了規則、畫面反而更難用。
      改吃容器寬之後這題自然消失：**不必發明斷點值，也不會因為側欄改寬而失效。**

   ⚠️ **門檻 732 是算出來的不是挑的**：輔助欄 292 ＋ 溝寬 20 ＋ 主欄最小可用寬 420 ＝ 732。
      （@container 與 @media 一樣不吃 var()，所以只能寫字面值；改 --aux-w 時要回頭重算這個數。）
      這條沿用 v1.10.0 的精神：把 RWD 從「遷就當下的欄數／版面」變成算術。

   🔴 **為什麼多一層 .ds-detail-cols**：container-type 只讓**後代**查詢得到自己的寬度，
      元素無法查詢自己 → 設 container 的那層不能同時是那個 grid。這一層是必要的，不是贅餘。
   ⚠️ **container-type:inline-size 會建立 containing block**（它蘊含 contain:layout）：
      放在 .ds-detail-body 裡面的 position:fixed 後代會改以它為基準定位。
      **所以 .ds-stickybar-pinned（fixed 釘視窗底）不可以放在 .ds-detail-body 內。** */
.ds-detail-body{container-type:inline-size;container-name:ds-detail}
.ds-detail-cols{
  display:grid;grid-template-columns:minmax(0,1fr) var(--aux-w);
  gap:var(--sp-20);          /* 原 22px：階梯外，收斂到 20 */
  align-items:start;         /* 三處實作同值——輔助欄不被主欄拉高 */
}
/* minmax(0,1fr) 不是 1fr：1fr 的 min 是 auto，主欄裝寬表格時會把整格撐爆。
   單欄時同樣要保留 minmax(0,1fr)，理由相同。 */
.ds-detail-main{display:flex;flex-direction:column;gap:0;min-width:0}
/* gap:0 是對的——卡片間距由 .ds-card 自己的 margin-bottom 給。
   ⚠️ A 另有一條 `.stack .ds-card+.ds-card{margin-top:0}` 是 no-op（.ds-card 本來就沒有
      margin-top 可清），**刻意不跟著抄進來**。 */
.ds-detail-aux{
  display:flex;flex-direction:column;gap:0;min-width:0;
  border-left:1px solid var(--line);padding-left:var(--sp-20);
}
/* 有分隔線才說得出主從。B 現役版沒有線＝那是「兩疊卡片」，不是「主＋附屬」。 */
@container ds-detail (max-width:731px){
  .ds-detail-cols{grid-template-columns:minmax(0,1fr)}
  .ds-detail-aux{
    border-left:0;padding-left:0;
    border-top:1px solid var(--line);padding-top:var(--sp-16);margin-top:var(--sp-4);
  }
}


/* ================= 精靈步驟條 .ds-steps（P1）=================
   ⚠️ **顏色一律照 GUIDELINE「進度指示的顏色」那張表，本元件不另創一套**：
      已完成＝ink 實心／進行中＝--cta-ink 空心圈／未達＝--line-strong 空心。
   ⚠️ A 的實作是**扁平 div 序列**（dot、label、line 三種節點並列，無 per-step 包裹），
      庫版改成「每一步一個包裹元素」——否則窄畫面換行會把 dot 和它的 label 拆到兩行。
      A 的 renderStepRow() 因此要改寫。
   ⚠️ 28px 的 dot 尺寸**刻意寫字面值不開 token**：它是元件內部尺寸，不是設計階梯上的角色
      （同庫內既有的 2px 描邊）。不吃 --h-icon 的理由是後者在 pointer:coarse 下會放大到
      命中下限，而步驟條不是點擊目標，跟著放大只會把整條撐爆。 */
.ds-steps{
  display:flex;align-items:center;gap:var(--sp-8);
  margin:0 0 var(--sp-20);          /* 原 22px 收斂到 20 */
  padding:0;list-style:none;
}
.ds-step{display:flex;align-items:center;gap:var(--sp-8);min-width:0}
.ds-step-dot{
  width:28px;height:28px;flex:none;border-radius:50%;
  display:grid;place-items:center;
  border:2px solid var(--line-strong);background:var(--surface);
  font:700 var(--fs-label)/1 var(--sans);color:var(--faint);
}
.ds-step-label{font:var(--fw-label) var(--fs-label)/1.3 var(--sans);color:var(--muted);white-space:nowrap}
.ds-step-line{flex:1 1 auto;height:2px;min-width:var(--sp-20);background:var(--line)}
/* 已完成：ink 實心。⚠️ 文字色吃 --on-ink 不是寫死 #fff——A 那邊還留著一處寫死的
   `.stepdot.done{color:#fff}`，是 v1.8.0 清庫內 18 處時沒掃到的專案端殘留。 */
.ds-step.is-done .ds-step-dot{background:var(--ink);border-color:var(--ink);color:var(--on-ink)}
/* 進行中：空心圈＋橘墨。**不用 ink** —— ink 是「已完成」的顏色，兩者撞色就分不出進度到哪。 */
.ds-step.is-current .ds-step-dot{border-color:var(--cta-ink);color:var(--cta-ink);background:var(--surface)}
.ds-step.is-current .ds-step-label{color:var(--text)}
/* 錯誤態：⚠️ **兩份實作都沒有**，本規則照 .ds-timeline 的 .is-rejected 補齊，屬提議值。 */
.ds-step.is-rejected .ds-step-dot{background:var(--red);border-color:var(--red);color:var(--on-danger)}
.ds-step-cap{font:600 var(--fs-label)/1.3 var(--sans);color:var(--cta-ink)}  /* 「第 N／M 個系統」 */
/* ⚠️ **A 完全沒有步驟條的斷點規則**，以下是庫新增的。
   概算：三步 ≈ 28×3 ＋ 標籤 3×52 ＋ gap 7×8 ＋ 線 2×20 ≈ 336px，而 375 手機扣掉內容區
   左右內距各 14 只剩 347px ＝ 只有 11px 餘裕，**四步必爆**。所以窄畫面只留目前那一步的字。 */
@media (max-width:640px){
  .ds-steps .ds-step:not(.is-current) .ds-step-label{display:none}
  .ds-step-line{min-width:12px}
}


/* ================= 黏頂／黏底操作列 .ds-stickybar（P1）=================
   A 同一支 app 內就手刻了兩次（批次操作列 renderBatchBar／移除列 renderRemoveBar），
   升庫最直接的收益就是收掉這個重複。

   🔴 **疊層**：A/B 兩份實作都寫裸數字（15／50），違反原則 7。庫一律吃 --z-sticky。
      ⚠️ **這條在 A 尚未遷到 .ds-appshell 之前會出事**：A 的側欄疊層是自己一套
      （抽屜 30／遮罩 29／桌機側欄 40），批次列升到 100 之後，**手機打開漢堡抽屜時
      黏頂批次列會浮在抽屜與遮罩之上**。→ 採用本元件必須與 app shell 遷移綁在一起，
      或先把該專案的側欄拉到 --z-nav。已登記在 GUIDELINE 採用登記表。 */
.ds-stickybar{
  display:flex;align-items:center;gap:var(--sp-12);flex-wrap:wrap;
  padding:var(--sp-12) var(--sp-16);
  background:var(--surface);border:1px solid var(--line);
  border-radius:var(--r-card);box-shadow:var(--sh-pop);
  z-index:var(--z-sticky);
}
.ds-stickybar-top{position:sticky;top:var(--ds-sticky-top);margin-bottom:var(--sp-16)}
.ds-stickybar-bottom{position:sticky;bottom:0;margin-top:var(--sp-14);justify-content:space-between}
/* ≤820 釘視窗底。🔴 **必須是 fixed 不是 sticky**：sticky 的包含塊是外層那張卡，
   捲過那張卡之後按鈕就停在畫面中間（A 實測 390 寬捲到頁尾時按鈕落在 y=262／視窗高 844）。
   ⚠️ 因此它也**不可以放在 .ds-detail-body 裡面**（container-type 會變成它的定位基準）。 */
@media (max-width:820px){
  .ds-stickybar-pinned{
    position:fixed;left:0;right:0;bottom:0;margin:0;
    border:0;border-top:1px solid var(--line);border-radius:0;
    box-shadow:0 -8px 24px rgba(27,29,34,.14);   /* 向上的陰影，庫內無對應 token */
    padding:var(--sp-12) var(--sp-16);
    padding-bottom:max(var(--sp-12), env(safe-area-inset-bottom));
  }
  /* 讓位：釘住的那條會蓋住頁尾內容，宿主要自己補下內距（A 用 112px）。
     庫提供一個 spacer 讓採用者不必自己算。 */
  .ds-stickybar-spacer{height:112px}
}
/* ⚠️ 空的批次列**要連 class 一起拿掉**，不要用 display:none——
   留著 class 會留下邊框與陰影的空殼。A 的契約就是 size===0 時 innerHTML='' 且 className=''。 */


/* ================= 通知面板 .ds-notif（P1）=================
   ＝A 的面板結構（鈴鐺＋未讀點＋逐則已讀）＋ B 的類型圖示（info／ok／reject）。
   **兩邊合起來才是完整規格**：A 有面板沒分型圖示，B 有分型圖示沒未讀狀態。

   ✅ 定位與開關**全部複用 .ds-menu ＋ DS.initMenu**，本元件不自己做——
      往上開、Esc、點外面關、同時只開一個，那邊都有了。這裡只補「面板寬度變體＋內容結構」。
   ⚠️ 但 .ds-menu 的 left/right:12px 是「撐滿錨點容器寬」，側欄可用寬只有 216−24＝192，
      撐不出面板要的 344 → 需要下面這個變體。 */
.ds-menu.ds-menu-panel{left:0;right:auto;width:344px;max-width:calc(100vw - 32px);padding:0}
/* 344 與 calc(100vw−32px) 是 A、B **兩份獨立實作逐值相同**的數，通用性沒有疑問。 */
.ds-notif-head{
  display:flex;align-items:center;gap:var(--sp-8);
  padding:var(--sp-12) var(--sp-16);border-bottom:1px solid var(--line);
  font:600 var(--fs-body)/1.3 var(--sans);color:var(--text);
}
/* 捲動掛在內層、標題固定（同 .ds-menu-scroll 的做法）。
   ⚠️ 高度算式取 A 的：它是修過真 bug 的結果——固定 420＋top 64 在橫向手機（高 390）會溢出畫面。 */
.ds-notif-scroll{max-height:min(420px, calc(100dvh - var(--ds-sticky-top) - 28px));overflow-y:auto}
.ds-notif-item{
  display:flex;align-items:flex-start;gap:var(--sp-10);width:100%;
  padding:var(--sp-12) var(--sp-16);   /* 原 13px：階梯外，收斂到 12（−1px） */
  border:0;border-bottom:1px solid var(--line);background:transparent;
  text-align:left;cursor:pointer;transition:background var(--t-fast);
}
.ds-notif-item:last-child{border-bottom:0}
.ds-notif-item:hover,.ds-notif-item:active{background:var(--hover-bg)}
/* ⚠️ 一定要用真的 <button>：B 用 div+role=button+tabindex 還得自己綁 Enter/Space，
   A 早期用純 div ＝鍵盤完全不可達（已在該專案的 a11y 稽核修掉）。 */
.ds-notif-body{min-width:0;flex:1}
/* ⚠️ 這三個一定要 display:block。它們在 markup 裡是 <span>＝inline，只給字級顏色的話
   標題／描述／時間會全部流在同一行，時間戳還會被折斷（2026-08-14 實測截圖抓到）。
   用 <span> 而不是 <div> 的理由是外層 .ds-notif-item 是 <button>——button 內不得放
   區塊級的互動內容，放 <div> 雖然瀏覽器能渲染但 HTML 是不合法的。 */
.ds-notif-title{display:block;font:600 var(--fs-label)/1.45 var(--sans);color:var(--text)}
.ds-notif-desc{display:block;font:400 var(--fs-sub)/1.5 var(--sans);color:var(--faint);margin-top:var(--sp-2)}
.ds-notif-time{display:block;font:400 var(--fs-micro)/1.4 var(--mono);color:var(--faint);margin-top:var(--sp-2)}
/* 未讀點。⚠️ **這是橘色在庫內的第三個落點**（原則 2 原本限「整頁唯一關鍵行動」與導覽選中系），
   已在 GUIDELINE 登記為具名例外——理由是「有事情等你看」正是這顆橘的語意，
   且它是 7px 的點、不與任何行動按鈕競爭注意力。
   ⚠️ **7 刻意字面值**（2026-08-28 v1.17.0 補理由）：它不是可以自由調整的裝飾尺寸——
      **「只有 7px」正是原則 2 那條具名橘色例外的成立條件之一**（放大它就會開始與 CTA 競爭
      注意力，例外的前提就不成立了）。所以它綁在這條例外的論證上、不進 tokens.css，
      改它必須先回 GUIDELINE 原則 2 重新論證那條例外。判準三條亦皆不滿足。
   ⚠️ 已讀時**轉 transparent 而不是移除**：位置保留，整排文字才不會左右跳動。
   ⚠️ 顏色不可是唯一管道 → 未讀必須另掛 .ds-sronly「（未讀）」。 */
.ds-notif-dot{width:7px;height:7px;flex:none;border-radius:50%;background:var(--cta);margin-top:var(--sp-6)}
.ds-notif-item.is-read .ds-notif-dot{background:transparent}
/* 類型圖示（來自 B）。30px 與 9px 圓角都是階梯外髒值 → 圓角收斂到 --r-btn；
   ⚠️ **30 刻意保留字面值**（2026-08-28 v1.17.0 把理由補完整）：
      ①**不吃 --h-icon**——那個家族帶「pointer:coarse 下放大到命中下限」的契約，
        而通知型別圖示是裝飾性的、不是點擊目標，跟著放大只會把整列撐爆；
      ②庫內沒有第二個家族可以放裝飾尺寸，而判準三條（跨族共用／情境改寫／≥3 落點且語意相同）
        它一條都不滿足——它與 .ds-avatar 的 30 是同值不同語意（型別圖示 vs 頭像）；
      ⇒ 元件內部常數，維持字面值。判準全文見 GUIDELINE「什麼樣的幾何值會變成 token」。 */
.ds-notif-ico{
  width:30px;height:30px;flex:none;border-radius:var(--r-btn);
  display:grid;place-items:center;font:700 var(--fs-label)/1 var(--sans);
}
.ds-notif-ico-ok{background:var(--green-bg);color:var(--green)}
.ds-notif-ico-reject{background:var(--red-bg);color:var(--red)}
/* ⚠️ B 的 info 底是 #DEE1E8（它自有的冷灰，不在庫色票）→ 改吃 --ink-tint-08＋--ink，
   與 .ds-badge 預設同一組。外觀會比 B 淺一階。 */
.ds-notif-ico-info{background:var(--ink-tint-08);color:var(--ink)}
/* 未讀徽章：A/B **逐值完全相同的九個值**，照收；只把寫死的 #fff 改成 --on-danger、
   圓角 9px 改 --r-full、10px 字級收斂到 --fs-micro（17px 膠囊放得下）。
   ⚠️ **17 刻意保留字面值**：單一落點、只服務這一個元件，且它與 min-width 是同一個數
      （膠囊在只有一位數時要是正圓）——判準三條皆不滿足 ⇒ 元件內部常數。 */
.ds-notif-badge{
  position:absolute;top:-5px;right:-5px;
  min-width:17px;height:17px;padding:0 var(--sp-4);border-radius:var(--r-full);
  display:flex;align-items:center;justify-content:center;
  background:var(--red);color:var(--on-danger);
  font:700 var(--fs-micro)/1 var(--mono);
}
.ds-notif-bell{position:relative}
/* 空狀態：庫的 .ds-empty 是 padding 46，塞進 344 面板太高 → compact 變體。
   （沒有這個變體，採用端就只能覆寫 ds- class ＝破規矩。） */
.ds-empty-compact{padding:var(--sp-32)}


/* ================= 可編輯明細表格 .ds-etable（建議 P1）=================
   「單頭＋多明細」是這批系統的共同骨架（採購單、庫存調整、客供建檔）。

   🔴 **前提要誠實**：B 的 .etable 家族是**零 markup 使用的死樣式**，這個「無框儲存格」
      從未上線、沒有任何使用者驗證過；B 線上唯一活著的儲存格內編輯（.oin 原貨號欄）
      反而是**有框的**。使用者 2026-08-14 拍板仍照設計稿做無框版，理由是它要解的問題
      （整張表變成格子監獄）是真的。**採用後請優先收集實際填表回饋。**

   🔴 **疊在 .ds-table 上，不自立表格家族**：v1.10.0 的欄寬政策、黏右操作欄、.ds-num、
      word-break:keep-all 全都是 .ds-table 的後代選擇器。自立根節點會一條都拿不到，
      只能整份複製——那正是「鏡射副本」的失敗模式（.ds-table 曾因此使用次數 0）。 */
.ds-etable td{padding:var(--sp-6) var(--sp-8)}   /* 有輸入框時儲存格內距要收，否則列高爆掉 */
.ds-ecell{
  width:100%;height:var(--h-select);
  border:1px solid transparent;   /* ⚠️ 保留 1px 佔位：focus 顯框時版面才不會跳 */
  background:transparent;          /* 跟著列底色走——hover／勾選時才不會出現色塊斷層 */
  border-radius:var(--r-btn);      /* 原 7px 收斂到 --r-btn：它是輸入框，與 .ds-input-sm 同級 */
  padding:0 var(--sp-8);           /* 原 7px 收斂到 --sp-8 */
  font:400 var(--fs-label)/1 var(--sans);color:var(--text);
  transition:border-color var(--t-fast),background var(--t-fast);
  /* 🔴 **`text-align:inherit` 不能省，也不能只靠儲存格宣告**（2026-08-14 實測）。
     對齊在語意上是「欄」的屬性（`.ds-num` 管 th 與 td），所以第一版只在儲存格宣告
     `text-align:right`、賭輸入框會繼承——**沒有繼承**：瀏覽器 UA 樣式把 input 設成
     `text-align:start`，那是作者樣式沒指定時的預設值，會直接擋掉繼承。
     結果是數字欄的表頭與衍生格靠右、可編輯格靠左，**同一欄兩種對齊**，
     而「逐列對齊比較」正是這個元件存在的理由。
     寫 `inherit` 而不是寫死 `right`：這樣對齊仍由欄（`.ds-num` 或預設）決定，
     文字欄不會被迫變成右對齊。 */
  text-align:inherit;
}
/* hover 給最輕的提示「這裡可以打字」。⚠️ B 未定義，此為提議值。 */
.ds-ecell:hover,.ds-ecell:active{border-color:var(--line)}
/* 焦點一律走庫的焦點環規格（原則 6），不抄 B 的方言（它用自有的 #DEE1E8 外環）。 */
.ds-ecell:focus{
  outline:2px solid var(--ink);outline-offset:2px;
  border-color:var(--ink);background:var(--surface);
}
/* 🔴 錯誤態**連 placeholder 一起轉紅**。理由不是好看：無框格子裡「必填未填」的畫面上
   **只剩 placeholder**，placeholder 維持灰色等於錯誤在最需要被看見的那一種情況下不可見。
   （B 在兩個不同地方各寫了一次這條，可信。） */
.ds-ecell.ds-invalid{border-color:var(--red);background:var(--red-bg)}
.ds-ecell.ds-invalid::placeholder{color:var(--red)}
.ds-ecell:disabled{background:var(--surface-2);color:var(--muted);cursor:not-allowed}
/* 衍生欄（箱數、小計）是唯讀的純文字 td，**不是 disabled input**——
   disabled input 看起來仍像個壞掉的控件，純文字才說得出「這格是算出來的」。 */
.ds-etable-derived{color:var(--muted)}
/* 新增一列：用庫既有的 .ds-dashed-btn（它的註解本來就寫著用途「＋新增一列 等」）。
   刻意不做 B 的整寬 ghost 列——庫裡已經有為這件事準備的元件，兩種寫法並存只會漂移。 */
.ds-etable-bar{padding:var(--sp-12) var(--sp-20);border-top:1px solid var(--line)}
/* 合計列：吃 --surface-2 並接卡片下緣圓角。
   ⚠️ 圓角**必須吃 --r-card**：B 寫死 16 等於把卡片圓角複製了一份，主題一改圓角就脫節
      （與 v1.12.0 那個 .ds-scrollx 蓋掉卡片圓角是同一類問題：不透明底 × 圓角）。 */
.ds-etable-total{
  display:flex;justify-content:flex-end;align-items:baseline;gap:var(--sp-14);
  padding:var(--sp-16) var(--sp-20);      /* 原 14/22 收斂，並與 .ds-card-actions 對齊 */
  border-top:1px solid var(--line);background:var(--surface-2);
  border-radius:0 0 var(--r-card) var(--r-card);
}
/* align-items:baseline 不是 center——13px 的 label 與 18px 的數字要對**文字底線**，
   對盒子的話小字會浮在中間。 */
.ds-etable-total-label{font:400 var(--fs-label)/1.4 var(--sans);color:var(--muted)}
.ds-etable-total-value{
  font:700 var(--fs-num-md)/1.2 var(--mono);color:var(--text);
  font-variant-numeric:tabular-nums;letter-spacing:var(--ls-num);
}
/* ⚠️ 可編輯表**比唯讀表更需要逐欄宣告 .ds-col-***：.ds-ecell{width:100%} 讓儲存格的
   min-content 塌到近乎 0，不宣告就會整排被壓扁、橫捲永遠不觸發。
   B 之所以能活，是因為它用表格級 min-width:920px 硬撐——那等於用一個數字掩蓋了
   「缺少逐欄宣告」這件事，不要跟著抄。
   ⚠️ **不要在窄畫面改用 .ds-listcards**：那個元件的前提是「一次看一筆、不做跨列比較」，
      而明細表的存在意義就是逐列填數字＋跨列對齊比較。窄畫面一律橫捲。 */


/* ================= 統計／KPI 卡 .ds-kpi（建議 P1）=================
   ⚠️ 命名刻意用 .ds-kpi* 而不是 .ds-stat*：庫內已有 .ds-stat-chip，
      再開一個 stat 就會有第三種「stat」語意。
   ⚠️ 兩個尺寸是**同一個元件的兩種嵌入層級**，不是兩個元件：
      白卡版坐在頁面上（--surface＋--sh-card），inset 版坐在卡片裡面（--surface-2）
      ——「白卡裡再分一層」正是 --surface-2 被定義出來的用途。 */
.ds-kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:var(--sp-16)}
/* auto-fit ＝寬度不夠自己換行，零 media query。 */
.ds-kpi{
  display:flex;flex-direction:column;gap:var(--sp-10);
  padding:var(--sp-20);background:var(--surface);
  border-radius:var(--r-card);box-shadow:var(--sh-card);
}
.ds-kpi-label{font:400 var(--fs-sub)/1.4 var(--sans);color:var(--faint)}
.ds-kpi-value{
  font:700 var(--fs-num-lg)/1 var(--sans);color:var(--text);
  letter-spacing:var(--ls-num);font-variant-numeric:tabular-nums;
}
.ds-kpi-value small{
  font:var(--fw-label) var(--fs-label)/1 var(--sans);color:var(--faint);
  margin-left:var(--sp-4);letter-spacing:0;
}
/* margin-top:auto 讓多張卡的 delta 列**底部對齊**（各卡高度不一時尤其明顯）。 */
.ds-kpi-foot{display:flex;align-items:flex-end;gap:var(--sp-10);margin-top:auto}
.ds-kpi-delta{font:400 var(--fs-hint)/1.4 var(--sans);color:var(--muted)}
/* inset 版（卡內小格）。原值 11/13 內距與 11px label 都收斂到階梯；
   label 統一取 --fs-sub(12)，不跟白卡版分兩種級距。 */
.ds-kpis-inset{grid-template-columns:1fr 1fr;gap:var(--sp-12)}
@media (max-width:640px){.ds-kpis-inset{grid-template-columns:1fr}}
.ds-kpi-inset{
  padding:var(--sp-10) var(--sp-12);background:var(--surface-2);
  border-radius:var(--r-fieldcard);box-shadow:none;gap:var(--sp-2);
}
.ds-kpi-inset .ds-kpi-value{
  font:700 var(--fs-num-sm)/1.3 var(--mono);overflow-wrap:anywhere;
}
/* overflow-wrap:anywhere ＝長金額硬斷不撐破格子。
   ⚠️ B 的三個用例都用 inline style 把字級壓成 13.5／12.5 來塞內容，那是
      「元件沒給尺寸變體、採用端只好自己壓字級」的典型症狀——庫給了 --fs-num-sm 就不該再發生。 */
/* 深色特色版。🔴 **四個深底硬值全部收進 v1.9.0 既有的六顆 --on-dark* 階梯，零新增 token**：
     #fff       → --on-dark(.93)        v1.9.0 明文：大面積深底用純白會過亮起光暈
     .62（label）→ --on-dark-2(.52)      實算對比 7.18 → 5.45，仍遠過 AA 4.5
     .55（小字）→ --on-dark-2(.52)      差 .03，ΔL* ≈ 0.3 肉眼不可辨
   ⚠️ 副作用要登記：白卡版 label 走 --faint、delta 走 --muted ＝兩階，深色版併成一階。
      這是深色端本來就有的資訊損失，不是收斂造成的。
   ⚠️ 深色 KPI 卡是 v1.9.0 明文預告的「.ds-btn-icon 深底不可用」的**第二個情境**：
      若要在這張卡上放 icon 鈕，它吃 --muted ＝深壓深、看不見。根治（改吃 currentColor）
      仍待拍板，在那之前這張卡上不要放 icon 鈕。 */
.ds-kpi-dark{background:var(--ink);color:var(--on-dark);box-shadow:none}
.ds-kpi-dark .ds-kpi-value{color:var(--on-dark)}
.ds-kpi-dark .ds-kpi-label,
.ds-kpi-dark .ds-kpi-delta,
.ds-kpi-dark .ds-kpi-value small{color:var(--on-dark-2)}
/* ⚠️ sparkline **刻意不升庫**：它的 on bar 吃 --cta ＝把橘用在純裝飾上，直接牴觸原則 2，
   而它只有 theme-lab 一個來源、B 沒有，通用性未被證明。要用就先拍板開例外。 */
