/*
  視覺骨架：編輯式極簡（editorial minimalism / type-driven）。
  規則只有幾條，改任何東西之前先看一眼：

  1. 近無彩色。暖白底、近黑字，整頁幾乎沒有顏色。顏色只在「有功能」時出現
     ——成功／失敗／版號標籤——其餘一律用灰階。
  2. 層次靠字型，不靠色塊。大小、粗細、字距三件事就要撐起全部階層；
     想強調某個區塊時，第一個選項是改字，不是加底色或加彩色邊條。
  3. 線是頭髮絲。區塊「內部」的分隔一律 1px 細線，不用色底。
  4. 區塊「邊界」用實體化。卡片有邊框 + 一組很淡的陰影（見 --shadow），
     讓人一眼看出這一塊到哪裡為止。這不是立體感，是分界。
  5. metadata 走等寬字。票號、路徑、時間、版號這些「資料」用 mono，
     跟「文案」在視覺上分開，不必額外加框。
  6. 留白給足。寧可高一點，不要把東西擠在一起。

  刻意不做的事：彩色左邊條、彩色整列底、發光外框、大圓角、彩色漸層、重複紋理。
  （登入頁背景的那道斜切是唯一的例外：它不是加圖案上去，是把平面折一下。）
*/

:root {
  /* 暖白而不是純白：純白 + 純黑是螢幕感，暖白 + 近黑才是紙感。 */
  --bg: #f4f2ee;
  --card: #fbfaf8;
  --sidebar: #f4f2ee;
  --text: #1a1815;
  --muted: #6e6a62;
  --faint: #a5a099;
  --line: #e3dfd6;
  --hover: #edeae3;

  /* accent 是墨色，不是藍色。整套介面只有「主要按鈕」跟「目前位置」用它。 */
  --accent: #22201c;
  --accent-soft: #e9e5dc;
  --accent-ink: #fbfaf8;

  /* 功能色：土系、低飽和，放在灰階裡不會跳出來搶焦點。 */
  --ok-bg: #e9efe8;   --ok-ink: #33654a;
  --warn-bg: #f2ecdf; --warn-ink: #7d6224;
  --err-bg: #f4e7e4;  --err-ink: #8f3b34;

  /*
    區塊「實體化」用的陰影：一道貼邊的硬陰影界定邊緣，一道大範圍的柔陰影
    把區塊從背景抬起來。跟登入頁那張產品卡是同一組語彙，只是更收斂。
    這不是立體感，是讓人一眼看出「這一塊到哪裡為止」。
  */
  --shadow: 0 1px 1px rgba(26, 24, 21, .03), 0 10px 26px rgba(26, 24, 21, .05);
  /* 「這一區現在動不了」的遮罩。刻意極淡——蓋一層看得出來，但後面的字還讀得到。 */
  --veil: rgba(251, 250, 248, .34);
  --radius: 7px;
  --side-w: 232px;
  --side-w-min: 60px;

  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

:root[data-theme="dark"] {
  --bg: #16150f;
  --card: #1c1b15;
  --sidebar: #16150f;
  --text: #e9e5db;
  --muted: #9b958a;
  --faint: #6c675e;
  --line: #2e2b23;
  --hover: #26241d;

  --accent: #e9e5db;
  --accent-soft: #2b2820;
  --accent-ink: #16150f;

  --ok-bg: #1c2c22;   --ok-ink: #8cc0a1;
  --warn-bg: #2c2617; --warn-ink: #cfae6b;
  --err-bg: #2e1d1a;  --err-ink: #d69289;

  --shadow: none;
  --veil: rgba(22, 21, 15, .38);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 14px/1.65 -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans TC",
        "PingFang TC", "Microsoft JhengHei", sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* 線條再細一點，整體才有 hairline 的質感。 */
svg { width: 18px; height: 18px; flex: 0 0 18px; fill: none; stroke: currentColor;
      stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }

/*
  eyebrow：小型大寫 + 加寬字距。這是這套設計裡的「標籤」手法，
  但要省著用——它是重音，到處都是重音就等於沒有重音。
  只用在真正的區塊小標，表頭那種每列都出現的東西不要用。
*/
.eyebrow, .hero-eyebrow, .hero-card-label {
  text-transform: uppercase;
  letter-spacing: .14em;
  font-size: 10.5px;
  font-weight: 600;
}

/* ============ 側邊選單 ============ */

.shell { display: flex; align-items: flex-start; min-height: 100vh; }

.sidebar {
  position: sticky; top: 0;
  width: var(--side-w); flex: 0 0 var(--side-w);
  height: 100vh;
  background: var(--sidebar);
  border-right: 1px solid var(--line);
  display: flex; flex-direction: column;
  padding: 20px 14px;
  transition: width .16s ease, flex-basis .16s ease;
  overflow: hidden;
}
/*
  收合狀態只存在於桌機。手機上那條選單在畫面底部，沒有「收合」這回事——
  但這些選擇器的優先權比底下的手機規則高，不關掉的話會把整條列壓回 60px 寬，
  變成左下角一個大部分項目都被裁掉、按也按不到的小方塊。
*/
@media (min-width: 641px) {
  html.sidebar-collapsed .sidebar { width: var(--side-w-min); flex-basis: var(--side-w-min); }
  html.sidebar-collapsed .lbl { display: none; }
  html.sidebar-collapsed .side-user { justify-content: center; }
  html.sidebar-collapsed .side-head { justify-content: center; }
  html.sidebar-collapsed .side-head .brand { display: none; }
  html.sidebar-collapsed #side-toggle svg { transform: rotate(180deg); }
}

.side-head { display: flex; align-items: center; gap: 8px; padding: 0 2px 24px; }
.icon-btn {
  flex: 0 0 auto; padding: 6px; border: none; background: none;
  color: var(--faint); border-radius: var(--radius); cursor: pointer;
}
.icon-btn:hover { background: var(--hover); color: var(--text); }
.brand { display: flex; align-items: center; gap: 10px; text-decoration: none; color: var(--text); min-width: 0; }
.brand-mark {
  flex: 0 0 26px; width: 26px; height: 26px; border-radius: 3px;
  background: var(--accent); color: var(--accent-ink);
  display: grid; place-items: center; font-size: 11px; font-weight: 700; letter-spacing: .06em;
}
.brand .lbl { font-weight: 600; white-space: nowrap; letter-spacing: -.01em; }

/*
  工作區（事業部）切換。
  它不是導覽項目，是「你現在站在哪裡」——所以位置在選單最上面、視覺上比
  導覽項目重，但仍然只用墨色與細線，不加彩色底。
*/
.ws-pick { position: relative; margin-bottom: 18px; }
.ws-pick > summary {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 9px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card); cursor: pointer; list-style: none;
  white-space: nowrap; overflow: hidden;
}
.ws-pick > summary::-webkit-details-marker { display: none; }
.ws-pick > summary:hover { border-color: var(--faint); }
.ws-pick[open] > summary { border-color: var(--accent); }
.ws-mark {
  flex: 0 0 24px; width: 24px; height: 24px; border-radius: 3px;
  border: 1px solid var(--line); background: var(--bg);
  display: grid; place-items: center; font-size: 12px; font-weight: 600;
}
.ws-pick .ws-name { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
.ws-pick .ws-name em {
  font-style: normal; font-size: 9.5px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--faint); font-weight: 600;
}
.ws-pick .ws-name strong {
  font-size: 13px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ws-caret { flex: 0 0 16px; width: 16px; height: 16px; margin-left: auto; color: var(--faint); }
.ws-pick[open] .ws-caret { transform: rotate(180deg); }

/*
  選單浮在導覽之上，不把下面的東西推開。寬度收在側邊欄之內——
  側邊欄是 overflow:hidden，超出去的部分會被裁掉。
*/
.ws-menu {
  position: absolute; z-index: 30; left: 0; right: 0; top: calc(100% + 5px);
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 4px; max-height: 55vh; overflow: auto;
}
.ws-menu button {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 7px 9px; border: none; border-radius: 4px; background: none;
  color: var(--muted); font-size: 13px; text-align: left; text-decoration: none;
  cursor: pointer; white-space: nowrap;
}
.ws-menu button > span { overflow: hidden; text-overflow: ellipsis; }
.ws-menu button:hover { background: var(--hover); color: var(--text); }
.ws-menu button.on { color: var(--text); font-weight: 600; }
.ws-menu button em {
  margin-left: auto; font-style: normal; font-size: 10px; color: var(--faint);
  letter-spacing: .06em;
}

/*
  側邊欄收合時只留下代號那個方塊。選單直接關掉——側邊欄是 overflow:hidden，
  浮出來的部分會被裁成一條，展不開的選單比沒有選單更糟。
  要切換就先把側邊欄展開。
*/
@media (min-width: 641px) {
  html.sidebar-collapsed .ws-pick > summary { justify-content: center; padding: 7px 2px; }
  html.sidebar-collapsed .ws-pick .ws-name,
  html.sidebar-collapsed .ws-caret,
  html.sidebar-collapsed .ws-menu { display: none; }
  html.sidebar-collapsed .ws-pick > summary { pointer-events: none; }
}

/* 頁面標題旁邊標出「這是哪個工作區的資料」。 */
.ws-tag {
  margin-left: 10px; padding: 2px 7px; border: 1px solid var(--line); border-radius: 3px;
  font-size: 11px; font-weight: 500; color: var(--muted); vertical-align: middle;
}

.side-nav { display: flex; flex-direction: column; gap: 1px; }
.side-nav:first-of-type { flex: 1; }
.side-nav.secondary { padding-top: 16px; margin-top: 8px; border-top: 1px solid var(--line); }
.side-nav a {
  display: flex; align-items: center; gap: 11px;
  padding: 7px 10px; border-radius: var(--radius);
  color: var(--muted); text-decoration: none; font-weight: 450; font-size: 13.5px;
  white-space: nowrap;
}
.side-nav a:hover { background: var(--hover); color: var(--text); }
/*
  目前所在的頁：用字重與墨色表示，不用彩色底。
  左邊那條細線是位置指示，不是裝飾——只有 2px，而且是墨色不是彩色。
*/
.side-nav a.on {
  background: none; color: var(--text); font-weight: 600;
  box-shadow: inset 2px 0 0 var(--accent);
  border-radius: 0 var(--radius) var(--radius) 0;
}

.side-foot { display: flex; flex-direction: column; gap: 1px; border-top: 1px solid var(--line); padding-top: 14px; }
.side-btn {
  display: flex; align-items: center; gap: 11px; width: 100%;
  padding: 7px 10px; border: none; border-radius: var(--radius);
  background: none; color: var(--muted); font: inherit; font-size: 13px; font-weight: 450;
  cursor: pointer; text-align: left; white-space: nowrap;
}
.side-btn:hover { background: var(--hover); color: var(--text); }
.side-user {
  display: flex; align-items: center; gap: 11px;
  padding: 7px 10px; color: var(--muted); font-size: 13px;
  overflow: hidden;
}
.side-user .lbl { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.avatar {
  flex: 0 0 24px; width: 24px; height: 24px; border-radius: 50%;
  background: none; color: var(--muted); border: 1px solid var(--line);
  display: grid; place-items: center; font-size: 10.5px; font-weight: 600;
}

.side-ver {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 6px 10px; margin-top: 4px; border: none; border-radius: var(--radius);
  background: none; color: var(--faint); font-size: 11px; white-space: nowrap;
  cursor: pointer;
}
.side-ver:hover { background: var(--hover); color: var(--text); }
.side-ver .lbl { text-transform: uppercase; letter-spacing: .13em; font-weight: 600; }
.ver-tag { font: 11px/1.5 var(--mono); margin-left: auto; }
@media (min-width: 641px) {
  html.sidebar-collapsed .side-ver { justify-content: center; padding: 6px 2px; }
  html.sidebar-collapsed .ver-tag { margin-left: 0; }
}

/* ============ 版本沿革 ============ */

.timeline { list-style: none; margin: 22px 0 0; padding: 0 0 0 24px; position: relative; }
.timeline::before {
  content: ""; position: absolute; left: 3px; top: 8px; bottom: 8px;
  width: 1px; background: var(--line);
}
.tl-item { position: relative; padding-bottom: 30px; }
.tl-item:last-child { padding-bottom: 0; }
.tl-item::before {
  content: ""; position: absolute; left: -25px; top: 8px;
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--bg); border: 1px solid var(--faint);
}
.tl-item.now::before { border-color: var(--accent); background: var(--accent); }

.tl-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.tl-ver { font: 600 14px/1.4 var(--mono); letter-spacing: -.02em; }
.tl-title { margin: 6px 0 12px; font-size: 15px; font-weight: 500; letter-spacing: -.01em; }

.chg-group { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 10px; }
.chg-group > .kind { margin: 2px 0 0; flex: 0 0 auto; width: 46px; text-align: left; }
.chg-items {
  margin: 0; padding-left: 18px; flex: 1; min-width: 0;
  font-size: 13px; color: var(--muted);
}
.chg-items li { margin-bottom: 4px; }
.chg-items li::marker { font-variant-numeric: tabular-nums; }

/*
  異動類型：小型大寫的純文字，不做彩色小方塊。
  四種類型靠文字本身分辨，不需要四種顏色。
*/
.kind {
  display: inline-block; margin-right: 8px;
  text-transform: uppercase; letter-spacing: .1em;
  font-size: 10px; font-weight: 700; color: var(--faint);
  white-space: nowrap;
}
.kind-add { color: var(--ok-ink); }
.kind-fix, .kind-tweak, .kind-remove { color: var(--faint); }

/* ============ 頁內對話框 ============ */

.modal-backdrop {
  position: fixed; inset: 0; z-index: 50;
  background: rgba(26, 24, 21, .38);
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
.modal {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: 0 20px 50px rgba(26, 24, 21, .14);
  width: 100%; max-width: 480px;
}
.modal-head {
  display: flex; align-items: center; gap: 12px;
  padding: 20px 24px 16px; border-bottom: 1px solid var(--line);
}
.modal-lg { max-width: 720px; }
.modal-head h3 { margin: 0; font-size: 17px; font-weight: 550; letter-spacing: -.015em; }
.modal-head .spacer { flex: 1; }
.modal-body { padding: 20px 24px; font-size: 13.5px; line-height: 1.8; color: var(--muted); }
.modal-body strong { color: var(--text); font-weight: 600; }
.modal-body.scroll { max-height: min(70vh, 620px); overflow-y: auto; }
.modal-foot {
  display: flex; justify-content: flex-end; gap: 8px; flex-wrap: wrap;
  padding: 16px 24px; border-top: 1px solid var(--line);
}
.modal-foot:empty { display: none; }
.modal-line { margin: 0 0 10px; }
.modal-line:last-child { margin-bottom: 0; }

/*
  逐項確認清單（歸檔前那幾項沒做的）。

  一項一列、各自一個勾，而不是一句「是否繼續」——那一句按下去等於把幾件事
  一起蓋章。整列就是 <label>，點名字、點「確認」兩個字都算數，
  不用瞄準那個小方框。
*/
.ack-list { display: flex; flex-direction: column; gap: 8px; margin: 14px 0 4px; }
.ack-row {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg);
}
.ack-row:hover { background: var(--hover); }
/* 確認過的那一列退成背景色，剩下沒勾的自然浮出來。 */
.ack-row:has(input:checked) { background: var(--card); border-color: var(--accent); }
.ack-name { color: var(--text); font-size: 13.5px; font-weight: 550; }
.ack-state { font-size: 12px; color: var(--warn-ink); }
.ack-row:has(input:checked) .ack-state { color: var(--faint); }
.ack-box { margin-left: auto; width: auto; }
.ack-mark { font-size: 12.5px; color: var(--muted); min-width: 3.2em; }
.ack-row:has(input:checked) .ack-mark { color: var(--text); }

/* ============ 意見回饋 ============ */

/*
  右下角的浮動按鈕。

  z-index 排序是有意義的：對話框（50）在它上面，浮動提示（60）又在對話框上面。
  少了這個順序，按開對話框之後這顆按鈕會浮在遮罩上，看起來像還能按。
*/
.fab {
  position: fixed; z-index: 45;
  right: 24px; bottom: 24px;
  width: 46px; height: 46px; padding: 0;
  border-radius: 50%; border: 1px solid var(--accent);
  background: var(--accent); color: var(--accent-ink);
  display: grid; place-items: center;
  box-shadow: 0 6px 20px rgba(26, 24, 21, .18);
}
/* 全域的 svg 是 flex: 0 0 18px，在 grid 置中的圓形按鈕裡會被壓歪，這裡指定大小。 */
.fab svg { width: 19px; height: 19px; flex: none; stroke-width: 1.7; }
.fab:hover:not(:disabled) { background: var(--accent); opacity: .88; }
/*
  浮動提示要讓開這顆按鈕。它是暫時的、按鈕是常駐的，
  蓋在上面會讓人以為按鈕不見了。
*/
body.has-fab .toast-wrap { bottom: 84px; }

/*
  類型二選一。用一條分成兩格的框，不用兩顆散在那裡的圓鈕——
  只有兩個選項而且互斥，做成一整塊比較看得出「這是同一個問題的兩個答案」。
  原生的 radio 藏起來但仍然在，鍵盤與螢幕閱讀器照常運作。
*/
.fb-kind { display: flex; margin-bottom: 14px; }
.fb-kind label { flex: 1; margin: 0; }
/*
  上色的是 span 而不是 label：選中狀態要靠 input:checked + span 這種
  相鄰選擇器表達，那是每個瀏覽器都認得的寫法。改用 label:has(input:checked)
  的話，在不認得 :has 的瀏覽器上會變成「兩格都沒有選中的樣子」——
  而那是這個元件唯一要傳達的資訊。
*/
.fb-kind span {
  display: block; padding: 8px 10px; text-align: center;
  border: 1px solid var(--line); cursor: pointer;
  font-size: 13px; color: var(--muted); user-select: none;
}
.fb-kind label:first-child span { border-radius: var(--radius) 0 0 var(--radius); }
.fb-kind label:last-child span { border-radius: 0 var(--radius) var(--radius) 0; border-left: none; }
.fb-kind span:hover { background: var(--hover); color: var(--text); }
/* 原生的圓鈕藏起來但仍然在，鍵盤與螢幕閱讀器照常運作。 */
.fb-kind input { position: absolute; opacity: 0; width: 0; height: 0; }
.fb-kind input:checked + span {
  background: var(--accent); border-color: var(--accent); color: var(--accent-ink);
}
/* 鍵盤走到時要看得出焦點在哪，滑鼠點的時候不要冒出一圈框。 */
.fb-kind input:focus-visible + span { outline: 1px solid var(--accent); outline-offset: 2px; }

/* 回饋是給人讀的句子，不是設定值，所以不用等寬字。 */
#fb-text { margin-top: 0; font-family: inherit; font-size: 13.5px; line-height: 1.7; }

.fb-note { margin: 10px 0 0; font-size: 12px; color: var(--faint); line-height: 1.7; }
/* code 預設會 break-all，版本號那麼短沒必要斷，斷了會變成「v0.2」跟「5.0」兩行。 */
.fb-note code { font-size: 11.5px; word-break: keep-all; white-space: nowrap; }
.fb-err {
  margin: 12px 0 0; padding: 9px 0 9px 12px;
  border-left: 2px solid var(--err-ink);
  color: var(--err-ink); font-size: 12.5px; line-height: 1.7;
}

/* ============ 公告 ============ */

/*
  公告的內文。內容裡的一小撮 Markdown 記號已在伺服器端轉成 <p>／<ul>／<strong>／<a>
  （見 richtext.go），所以這裡不再靠 white-space 保留換行，改用區塊標籤自己的間距。
  顏色用 --text 而不是彈出框內文預設的灰：這一段是主角，不是補充說明。
*/
/* overflow-wrap：貼進來的長網址不斷行的話會把整個框撐寬。 */
.ann-text {
  margin: 0; overflow-wrap: anywhere;
  color: var(--text); line-height: 1.85;
}
.ann-text p, .ann-text ul, .ann-text ol { margin: 0.7em 0; }
/* 縮排的段落：層級由 --ind 帶進來（見 richtext.go），一層約一個 tab 的寬。 */
.ann-text p { padding-left: calc(var(--ind, 0) * 1.6em); }
/* 頭尾不留外距，內文才貼齊框的上下緣。 */
.ann-text > :first-child { margin-top: 0; }
.ann-text > :last-child { margin-bottom: 0; }
.ann-text ul, .ann-text ol { padding-left: 1.6em; }
.ann-text li { margin: 0.2em 0; }
/* 巢狀清單貼著上一項就好，不要再撐開一個段距。 */
.ann-text li > ul, .ann-text li > ol { margin: 0.2em 0; }
.ann-text strong { font-weight: 600; }

/*
  發布表單。標題一列、內容一列，不跟其他設定一樣擠成一行——
  內容框跟旁邊的欄位分寬度的話，會窄到看不出自己正在寫第幾行。

  「發布」與「收起公告」都在表單外面的那一列（靠 form= 指回這裡），
  兩顆按鈕才排得在一起。
*/
.ann-form { margin-top: 14px; display: flex; flex-direction: column; gap: 12px; align-items: flex-start; }
.ann-form label { width: 100%; }
/* 發布用的內容框跟公告本身一樣是給人讀的句子，不用等寬字。 */
#ann-body { font-family: inherit; font-size: 13.5px; }

/*
  支援哪些格式的提示。極短一行，每個記號用 code 標成 chip；細節在 README。
  緊貼在內容框底下（負的上邊距把 gap 收掉一半），讀起來像那個框的註腳。
*/
.ann-syntax { margin: -4px 0 0; display: flex; flex-wrap: wrap; gap: 4px 12px; align-items: baseline; }
.ann-syntax code { word-break: keep-all; white-space: nowrap; }

/*
  即時預覽。跟公告彈出框同一套 .ann-text 樣式，外面加一個框跟標籤，
  一眼看出這塊是「發出去會長這樣」，不是還要填的欄位。
*/
.ann-preview-wrap { margin-top: 4px; width: 100%; }
.ann-preview-tag { display: block; margin-bottom: 6px; }
.ann-preview { border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; background: var(--card); }

/*
  紀錄表格裡的內容欄：內文已是 <p>／<ul>／<ol> 區塊，收緊各自的外距，別把列撐高。
  寬度上限沿用 td.detail。選擇器帶上 .grid 壓過 td.detail 的預設。
*/
.grid td.ann-cell p, .grid td.ann-cell ul, .grid td.ann-cell ol { margin: 0; }
.grid td.ann-cell p { padding-left: calc(var(--ind, 0) * 1.4em); }
.grid td.ann-cell p + p, .grid td.ann-cell p + ul, .grid td.ann-cell p + ol,
.grid td.ann-cell ul + p, .grid td.ann-cell ol + p { margin-top: 4px; }
.grid td.ann-cell ul, .grid td.ann-cell ol { padding-left: 1.4em; }
.grid td.ann-cell li { margin: 0; }

/* ============ 浮動提示 ============ */

.toast-wrap {
  position: fixed; right: 24px; bottom: 24px; z-index: 60;
  display: flex; flex-direction: column; gap: 8px; align-items: flex-end;
  pointer-events: none;
}
.toast {
  background: var(--text); color: var(--bg);
  padding: 10px 18px; border-radius: var(--radius); font-size: 13px; font-weight: 450;
  box-shadow: 0 10px 30px rgba(26, 24, 21, .18);
  animation: toast-in .18s ease-out;
}
.toast.out { opacity: 0; transition: opacity .4s ease; }
@keyframes toast-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

.ico-moon { display: none; }
:root[data-theme="dark"] .ico-sun { display: none; }
:root[data-theme="dark"] .ico-moon { display: block; }

.content { flex: 1; min-width: 0; }
/*
  下面的留白跟上面一樣，不另外留一段給那顆浮動按鈕。

  底下多留的每一 px 都算進文件高度。內容差一點就填滿一個畫面時（說明那一頁就是），
  那一段自己把文件推過 100vh，於是長出一條捲軸——捲下去只有空白。
  為了閃一顆本來就浮在內容上的按鈕，換來每一頁都可能多一條捲不到東西的捲軸，不划算。
  代價是捲到底時它會蓋住右下角一小塊；那一塊只要往上捲一點就看得到，
  一條假的捲軸卻是每次都在。
*/
.wrap { max-width: 1180px; margin: 0 auto; padding: 30px 34px; }

/* ============ 示範模式的橫幅 ============ */

/*
  讀教學期間整站畫的是假資料（見 demo.go），而那幾頁跟真的一模一樣——同樣的按鈕、
  同樣的版號、按下去同樣回「已推送」。分不出來的時候人不敢按，而不敢按就學不起來。

  所以這一條固定在畫面最上緣，跟著捲：捲兩下就看不見的保證等於沒有保證。
  高度只有 30px，字也小一號——它是背景音，不是這一頁的內容。

  顏色是整頁唯一一塊。這不違反「顏色只在有功能時出現」：它講的正是
  「你現在不在真的資料上」，跟成功／失敗訊息一樣是狀態，不是裝飾。

  z-index 55 壓在對話框（50）與教學那幾層（46-48）上面。示範模式沒有
  「這一下不算」的時候，被任何一層蓋掉都會讓人以為自己回到真的資料上了。
*/

:root { --demo-h: 30px; }

.demo-bar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 55;
  height: calc(var(--demo-h) + env(safe-area-inset-top));
  padding: env(safe-area-inset-top) 14px 0;
  display: flex; align-items: center; justify-content: center; gap: 7px;
  /*
    整條上色，不是淡淡一層底。淡的那一版在暖白底上幾乎看不出來，
    而這條的工作就是被看見——第一眼沒看到，它等於不存在。
    前景背景都是同一組 warn，深淺色主題各自對調，兩邊都讀得清楚。
  */
  background: var(--warn-ink); color: var(--warn-bg);
  font-size: 12.5px; font-weight: 500; letter-spacing: .01em;
  /* 窄畫面上寧可切掉尾巴，也不要讓它折成兩行把版面往下推一整段。 */
  white-space: nowrap; overflow: hidden;
}
.demo-bar svg { width: 14px; height: 14px; flex: none; stroke-width: 1.7; }
.demo-bar b { font-weight: 600; }

/*
  整份內容往下讓開它的高度。讓在 body 上而不是各頁自己讓——
  漏掉一頁的代價是那一頁的第一行被蓋住，而那通常正是標題。
*/
body[data-demo="1"] { padding-top: calc(var(--demo-h) + env(safe-area-inset-top)); }
/* 讓開的那一段算進文件高度，min-height 不扣掉的話每一頁都會多一條捲不到東西的捲軸。 */
body[data-demo="1"] .shell { min-height: calc(100vh - var(--demo-h) - env(safe-area-inset-top)); }

/* 側邊欄是黏在頂端的，黏的位置也要往下，不然它的上緣會捲進橫幅底下。 */
@media (min-width: 641px) {
  body[data-demo="1"] .sidebar {
    top: calc(var(--demo-h) + env(safe-area-inset-top));
    height: calc(100vh - var(--demo-h) - env(safe-area-inset-top));
  }
}

@media (max-width: 860px) {
  .sidebar { width: var(--side-w-min); flex-basis: var(--side-w-min); }
  .sidebar .lbl { display: none; }
  .sidebar .ws-pick .ws-name,
  .sidebar .ws-caret,
  .sidebar .ws-menu { display: none; }
  .sidebar .ws-pick > summary { justify-content: center; padding: 7px 2px; pointer-events: none; }
  .wrap { padding: 24px 18px; }
}

/* ============ 手機 ============ */

/*
  手機上把側邊選單放到底部，變成一條橫的圖示列。

  留在左邊的話，一支 390px 寬的手機要拿 54px 去放一排圖示——
  剩下給內容的不到 260px，表格與表單全部被擠扁。改到底部之後
  內容拿到整個寬度，而且圖示落在拇指構得到的地方。
  項目與圖示都跟桌機一模一樣，只是換了方向。
*/
@media (max-width: 640px) {
  .shell { display: block; }

  .sidebar {
    position: fixed; inset: auto 0 0 0; z-index: 40;
    width: auto; flex-basis: auto; height: auto;
    flex-direction: row; align-items: center; gap: 1px;
    border-right: none; border-top: 1px solid var(--line);
    padding: 5px 8px calc(5px + env(safe-area-inset-bottom));
    padding-left: max(8px, env(safe-area-inset-left));
    padding-right: max(8px, env(safe-area-inset-right));
    overflow-x: auto; overflow-y: visible;
    /*
      項目多的時候（跨多個工作區、又是管理員）這條列會超過畫面寬度，
      那時它就是可以橫向捲的。捲軸本身藏起來——一條 3px 的灰線橫在
      螢幕最下緣只會被當成畫面壞掉。
    */
    scrollbar-width: none;
  }
  .sidebar::-webkit-scrollbar { display: none; }
  /* 收合鈕與品牌字樣在這裡沒有意義：沒有東西可以收合，標題列也已經在頁面上。 */
  .side-head { display: none; }

  .side-nav { flex-direction: row; gap: 1px; }
  .side-nav:first-of-type { flex: 0 0 auto; }
  .side-nav.secondary { border-top: none; padding-top: 0; margin-top: 0; }
  .side-foot { flex-direction: row; border-top: none; padding-top: 0; margin-left: auto; }
  /* 版本號與使用者名稱在這條列上只是佔位置，前者在版本沿革看得到，後者在設定頁。 */
  .side-ver, .side-user { display: none; }

  .side-nav a, .side-btn {
    justify-content: center;
    width: 40px; min-width: 40px; height: 40px; padding: 0;
  }
  /* 目前所在的頁：指示線從左邊移到上邊，方向跟著這條列轉。 */
  .side-nav a.on {
    box-shadow: inset 0 2px 0 var(--accent);
    border-radius: var(--radius);
  }

  /* 內容讓開底部那條列的高度，最後一張卡片才不會被蓋住。 */
  .content { padding-bottom: calc(58px + env(safe-area-inset-bottom)); }
  .wrap {
    padding: calc(16px + env(safe-area-inset-top)) 14px 24px;
    padding-left: max(14px, env(safe-area-inset-left));
    padding-right: max(14px, env(safe-area-inset-right));
  }
  /* 瀏海那一段已經由橫幅讓過了，這裡不能再讓一次。 */
  body[data-demo="1"] .wrap { padding-top: 16px; }

  /*
    工作區切換器也在這條列上（只有跨多個工作區的人看得到）。
    選單改成往上開、且用 fixed 定位——這條列是可以橫向捲的，
    一般的絕對定位會被裁掉。
  */
  .ws-pick { margin-bottom: 0; flex: 0 0 auto; }
  .sidebar .ws-pick > summary {
    justify-content: center; padding: 0; pointer-events: auto;
    width: 40px; min-width: 40px; height: 40px; border: none; background: none;
  }
  .sidebar .ws-pick .ws-name, .sidebar .ws-caret { display: none; }
  .sidebar .ws-menu {
    display: block; position: fixed; top: auto;
    left: 10px; right: 10px; bottom: calc(56px + env(safe-area-inset-bottom));
  }

  .card { padding: 16px 14px; }
  .card h2 { font-size: 15.5px; }

  /* 標題列與工具列一律可以換行，否則右邊的按鈕會被推出畫面。 */
  .card-head, .sys-head, .row, .card-actions { flex-wrap: wrap; }
  .sys-head { align-items: center; row-gap: 8px; padding: 12px 14px; }
  .sys-head .spacer { flex-basis: 100%; height: 0; }
  .sys-count { white-space: nowrap; }
  .sys-body { padding: 2px 14px 12px; }
  .edit-panel { padding: 12px 14px; }

  /* 手指的目標至少 34px 高，密度讓一點沒關係，按不到才是問題。 */
  button, .btn, select, input[type="text"], input[type="email"], input[type="url"],
  input[type="password"], input[type="number"] { min-height: 38px; }
  .grid button.tiny { min-height: 32px; padding: 6px 10px; }
  .w-act button { margin: 2px 0 2px 4px; }
  /* checkbox 放大，順便把它跟文字的距離拉開。 */
  input[type="checkbox"] { width: 17px; height: 17px; }

  /*
    iOS 對字級小於 16px 的輸入框會自動放大整個畫面，放大之後再也回不去。
    這裡只提高手機上的輸入字級，不動桌機。
  */
  input, select, textarea { font-size: 16px; }
  /* 意見回饋的框有自己的字級（id 選擇器蓋得過上面那條），要在這裡補一次，
     否則 iOS 在它身上照樣會放大整個畫面。 */
  #fb-text { font-size: 16px; }

  /*
    表格的欄位不要被擠成一行一個字，寧可讓表格橫向捲。

    .w-act 平常用 width:1% 來把「操作」欄縮到剛好，那是 width:100% 的表格才成立的
    寫法；表格一旦改成 max-content，1% 會反過來把表格撐成那一欄的一百倍寬。
    這裡把它換回 auto。
  */
  .table-scroll .grid { width: max-content; min-width: 100%; }
  .table-scroll .grid .w-act { width: auto; }
  .table-scroll .grid td, .table-scroll .grid th { white-space: nowrap; }
  /* 這兩欄是長度不定的文字，跟著上面那條變成不折行的話，表格會寬到
     橫向捲不完。它們維持折行，其餘欄位照樣一列一行。 */
  .table-scroll .grid td.title { white-space: normal; min-width: 220px; }
  .table-scroll .grid td.detail { white-space: normal; min-width: 200px; max-width: 260px; }
  /* 專案的名稱與路徑同理，而且更長：路徑動輒四層，名稱還可能被貼成一整條網址。
     一定要給 max-width——表格是 max-content，只讓它折行的話那一格還是會量成
     整條字串的寬度，結果是橫向捲一大段而不是折成兩行。 */
  .table-scroll .grid td.proj-cell { white-space: normal; min-width: 160px; max-width: 240px; }

  .modal { width: calc(100vw - 24px); max-width: none; }
  .flash-float { max-width: calc(100vw - 32px); }

  /* 選單在這個寬度是橫在畫面最底下的一條，浮動按鈕要坐在它上面，不能疊上去。 */
  .fab {
    right: max(14px, env(safe-area-inset-right));
    bottom: calc(68px + env(safe-area-inset-bottom));
  }
  body.has-fab .toast-wrap { bottom: calc(126px + env(safe-area-inset-bottom)); }
}

/* ============ 卡片 ============ */

.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 20px 24px;
  margin-bottom: 16px;
  /* 進度條是 sticky 的，捲過來時卡片頂端要留出被它蓋住的高度。 */
  scroll-margin-top: 88px;
}
.card h2 { margin: 0; font-size: 16.5px; font-weight: 600; letter-spacing: -.02em; }
.card h2 + .hint { margin-top: 8px; }
.card-head + .hint { margin-top: 8px; }
/*
  標題區與內容之間的分界線，讓「這一塊的頭在哪」看得出來。

  線是卡片的事，行長是文字的事，這兩件事剛好長在同一個框上。用 max-width 收行長
  會把線一起收掉——線停在半路，看起來像畫到一半斷掉。所以框留整張卡的寬度，改用
  padding 把文字推回 --hint-measure：文字斷在哪裡跟以前一模一樣，只有線變成橫過
  整張卡。卡片比 --hint-measure 還窄時 calc 算出負值會被夾回 0，手機上不必另外處理。
*/
.card > .hint:first-of-type {
  max-width: none;
  padding-right: calc(100% - var(--hint-measure));
  padding-bottom: 14px;
  border-bottom: 1px solid var(--line);
}
/*
  後面沒有東西可以分隔的時候就不要畫。

  說明底下的表單預設是收起來的（成員管理、工作區的「新增」），收起來時說明就是
  卡片裡最後一個看得到的東西，線會貼在卡片自己的邊框上面——兩條平行線離 16px，
  看起來像多畫了一條。整張卡只有標題加一句說明時（設定頁的「安裝到主畫面」）也一樣。
*/
.card > .hint:first-of-type:not(:has(~ *:not(.hidden))) {
  padding-bottom: 0;
  border-bottom: none;
}
form.inline { display: inline-block; }
.guide-steps { margin: 14px 0 18px; padding-left: 20px; color: var(--muted); font-size: 13px; }
.guide-steps li { margin-bottom: 8px; }
.guide-steps strong { color: var(--text); }
.card h3 { margin: 20px 0 6px; font-size: 13.5px; font-weight: 600; letter-spacing: -.01em; }
.card-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 16px; flex-wrap: wrap;
}
.card-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

/* 步驟編號：等寬字 + 細線圓圈，不填色塊。 */
.step { display: flex; align-items: baseline; gap: 12px; }
.step-no {
  flex: 0 0 auto;
  font: 600 11px/1 var(--mono); color: var(--faint);
  letter-spacing: .1em;
}
.step-no::after { content: " —"; }

/* ============ 文字 ============ */

.muted { color: var(--muted); }
/* 一句話的錯誤說明。排在 .muted 後面才蓋得掉它——兩者同分，靠順序決定
   （見 .sys-head .spacer 那個踩過的坑）。 */
.err-text { color: var(--err-ink); }
.small { font-size: 12.5px; }
/* 行長上限寫成變數，是因為卡片頂端那條分界線要拿它去算 padding，見 .card > .hint。 */
.hint { color: var(--muted); font-size: 13px; margin: 0; --hint-measure: 68ch; max-width: var(--hint-measure); }
.nowrap { white-space: nowrap; }

/*
  一句話包成一個 .sentence。

  中文沒有詞距，瀏覽器可以在任何兩個字之間斷行，兩句話的說明常常斷成
  「暫時不發版的專案請／用「停用」」這種樣子——斷在那裡對讀的人沒有任何意義。
  包成 inline-block 之後，能斷的位置只剩句與句之間；單句真的長過一行時它仍然
  會自己折，不會溢出去。

  樣板裡每一句要寫在同一行、句與句之間不留空白字元：換行會被 HTML 併成一個
  半形空格，中文句號後面多一個空格看起來像排版壞掉。
*/
.sentence { display: inline-block; }

/*
  由 .sentence 組成的說明不吃 68ch 的行長上限。

  行長上限是為了不讓一整段文字拉得太長；但這種說明本來就只有兩句，能斷的位置
  也只剩句與句之間，上限唯一的效果就是卡片明明還有一半是空的，兩句話卻被拆成
  兩行。句子夠短就讓它們待在同一行，卡片寬度自己就是上限。
*/
.hint:has(> .sentence) { --hint-measure: 100%; }

/*
  這台主機連不到 Jira 時的說明。
  一般的 .hint 是「順帶一提」，這句不是——它要解釋眼前為什麼有一整塊是灰的，
  所以給它一條左邊的線把它從內文裡拉出來，但仍然不填色塊、不搶版面。
*/
.offline-note {
  margin: 12px 0 0; padding: 10px 0 10px 14px;
  border-left: 2px solid var(--err-ink);
  color: var(--muted); font-size: 13px; line-height: 1.7; max-width: 68ch;
}
.offline-note strong { color: var(--text); }
/* 「還沒設定」不是壞掉，是還沒做，用警示色而不是錯誤色。 */
.offline-note.warn { border-left-color: var(--warn-ink); }

/*
  .is-off ＝「這台主機上沒有這個功能」，跟一般 disabled 的「還沒填所以先不能按」
  是兩件事，所以再壓一層灰階：整塊退成無彩色，一眼看得出是關掉的，不是漏做的。
  fieldset 的預設外框與 min-width 會撐壞裡面的彈性版面，一併歸零。
*/
.is-off { filter: grayscale(1); opacity: .62; cursor: not-allowed; }
/*
  整塊已經淡掉了，裡面的按鈕不要再各自淡一次——
  .62 疊上 button:disabled 的 .4 只剩 .25，字會淡到讀不出來，
  變成「不知道那裡有什麼」而不是「那裡有東西但現在不能用」。
*/
.is-off button:disabled { opacity: 1; }
.is-off input:disabled { background: var(--hover); color: var(--muted); }
fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }

/*
  .is-off 的另一種說法，給「一整區都動不了」用：不把整塊灰掉，只蓋一層
  極淡的遮罩，中間放一句「怎樣才用得到」。

  為什麼不沿用 .is-off：那一層是 grayscale + .62，欄位裡他之前填的帳號會淡到
  讀不出來，變成「不知道那裡有什麼」。這裡要的是「那裡有東西，只是現在動不了」，
  所以遮罩淡到不影響讀後面的字。

  遮罩不吃點擊（pointer-events: none）：真正的停用是 fieldset 的 disabled，
  遮罩只是視覺上的暗示。混在一起的話，那些本來就該一直能按的動作
  （例如把自己存進去的密碼清掉）會被一層看不見的東西擋住。
*/
/* 探測失敗的原文。是給人照著去找網管的線索，不是要嚇人，所以只用等寬字，
   不套錯誤色——真正的訊號是上面那個徽章。 */
.probe-why { margin: -4px 0 14px; }
.probe-why code { font-size: 11.5px; color: var(--muted); word-break: break-all; }

.veil-wrap { position: relative; }
.veil-note {
  position: absolute; inset: -6px -10px; margin: 0;
  /* 貼著底部而不是正中間：欄位的標題在上面那一行，正中間會剛好壓在
     「Jira 密碼」上，把一個本來讀得到的字蓋掉。 */
  display: flex; align-items: flex-end; justify-content: center;
  padding-bottom: 7px;
  background: var(--veil); border-radius: var(--radius);
  pointer-events: none;
}
/*
  那句話做成一條斜貼上去的封條。遮罩本身淡到幾乎看不見，這一條才是「這裡封起來了」
  的訊號，所以它要夠重——但重量給在字上，不給在顏色上。

  用墨色實心條而不是粗體紅字：紅色在這套介面裡是「壞掉了」（見 --err-ink），
  而連不到 Jira 不是壞掉，是這台主機本來就沒有這條路，報成錯誤只會讓人去找哪裡出問題。
  對比、字重、字距這三件事已經夠搶眼，那也是這一頁其他地方建立階層的方式
  （見檔頭第 2 條），加一個新顏色反而會把它從這套語彙裡切出去。

  圓角收到 4px：封條是一條貼上去的紙，不是藥丸；大圓角在這套介面裡也是明列不做的事。
*/
/* 只吃直接子層：裡面那兩個 .sentence 是用來控制折行位置的，
   寫成 .veil-note span 的話它們會各自再長出一條封條。 */
.veil-note > span {
  background: var(--accent); color: var(--accent-ink);
  border-radius: 4px;
  padding: 10px 24px;
  font-size: 14px; font-weight: 600; letter-spacing: .04em; line-height: 1.5;
  /* 斜一點點就好。角度再大會變成貼歪的貼紙，不是封條。 */
  transform: rotate(-1.8deg);
  box-shadow: 0 6px 20px rgba(26, 24, 21, .18);
  /* 窄畫面上這句話比欄位還寬，不收就會頂出卡片外面。 */
  max-width: 100%; text-align: center;
}
/* 手機上欄位是一欄一欄疊下來的，貼著底部會蓋掉最下面那顆「儲存」。
   封條本來就是橫過去封住整塊的東西，擺中間讀起來最像那麼一回事。 */
@media (max-width: 640px) {
  .veil-note { align-items: center; padding: 0; }
}

/*
  設定頁的說明文字不另外設行長，跟著卡片走。

  --hint-measure 預設 68ch（量出來約 557px），那是給表格頁用的——那些頁卡片很寬，
  說明只是表格上面的一句話，收窄反而好讀。設定頁不一樣：說明的正下方就是同一件事
  的欄位，欄位寬到 770px、說明只到 557px，就會看到「存進去之後不會再顯／示。」
  這種一個字掉到下一行的斷法。

  兩者對齊到同一條右邊界就好了。這裡的說明本來每句都只有一行，不設上限之後
  它們根本不會斷行，也就沒有行長太長的問題。
*/
.page-settings .hint { --hint-measure: 100%; }
.page-settings .offline-note { max-width: none; }

/*
  metadata 走等寬字。極淡的底色不是裝飾，是讓票號在整段中文裡有個落點；
  完全不給底的話，一堆 ELD-329 混在句子裡會分不出哪裡開始哪裡結束。
*/
code {
  font: 12.5px/1.5 var(--mono);
  background: var(--hover); border: none;
  padding: 1px 5px; border-radius: 3px;
  color: inherit; word-break: break-all;
}
/* 表格與清單裡整欄都是票號，落點很清楚，就不用再加底。 */
td code, .rel-ver { background: none; padding: 0; }
/*
  表格裡的 Email。它比票號長得多，用同一級字會把「操作」那一欄擠出畫面，
  所以壓小一級並降成 muted——它是身分的註記，不是那一列的重點。
*/
.mail { font-size: 11.5px; color: var(--muted); }

/* 連結用底線，不用彩色。 */
a {
  color: var(--text);
  text-decoration: underline;
  text-decoration-color: var(--faint);
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}
a:hover { text-decoration-color: var(--text); }

kbd {
  font: 11px var(--mono); background: var(--hover);
  border: 1px solid var(--line);
  border-radius: 3px; padding: 1px 5px;
}

/* ============ 表單 ============ */

label { display: block; font-size: 12px; color: var(--muted); }
/*
  這串是逐個型別列的，不是 input 通吃——checkbox 與 radio 不能套這一組
  （會變成一個佔滿整行的方塊）。代價是新增一種型別就要記得補進來：
  Google Email 那格用的是 type=email，漏了它就整個掉回瀏覽器的預設外觀，
  在一排自訂樣式的欄位中間顯得特別突兀。
*/
input[type=text], input[type=email], input[type=url],
input[type=password], input[type=number], input[type=search], textarea, select {
  width: 100%; padding: 8px 11px; margin-top: 6px;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--bg); color: var(--text);
  font: inherit; font-size: 13.5px;
}
textarea { font-family: var(--mono); font-size: 12.5px; resize: vertical; line-height: 1.8; }
input:focus, textarea:focus { outline: 1px solid var(--accent); outline-offset: -1px; border-color: var(--accent); }
input[type=checkbox] { accent-color: var(--accent); cursor: pointer; }

/*
  自己畫的挑選清單，取代 <datalist>（樣板見 layout.html 的 opt-pick）。

  summary 的高度、邊框與圓角刻意跟上面那組輸入框對齊：同一排欄位裡混著
  一個長得不一樣的東西，比整排都醜更顯眼。

  展開的清單浮在內容之上，不把下面的表格推開；上限一個高度之後自己捲，
  操作者可以有上百個。
*/
.opt-pick { position: relative; margin-top: 6px; min-width: 170px; }
.opt-pick > summary {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 11px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--bg); color: var(--text); font-size: 13.5px;
  cursor: pointer; list-style: none; white-space: nowrap;
}
.opt-pick > summary::-webkit-details-marker { display: none; }
.opt-pick > summary:hover { border-color: var(--faint); }
.opt-pick[open] > summary { border-color: var(--accent); }
.opt-pick > summary > span { overflow: hidden; text-overflow: ellipsis; }
.opt-caret { flex: 0 0 16px; width: 16px; height: 16px; margin-left: auto; color: var(--faint); }
.opt-pick[open] .opt-caret { transform: rotate(180deg); }
.opt-menu {
  position: absolute; z-index: 30; left: 0; top: calc(100% + 5px);
  min-width: 100%; max-width: 76vw;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 4px; max-height: 320px; overflow: auto;
}
/*
  清單長的時候由 JS 補上的搜尋框（見 app.js 的 pickSearchBox）。

  黏在清單頂端：往下捲了三十列還想改關鍵字時，搜尋框不能跟著捲出去。
  負的外距把它撐到清單的內距之外，捲動時底下的列才不會從邊上露出來。
*/
.opt-search {
  position: sticky; top: -4px; z-index: 1;
  margin: -4px -4px 4px; padding: 4px; background: var(--card);
}
.opt-search svg { left: 14px; }
.opt-search input { padding: 6px 10px 6px 32px; font-size: 13px; }
.opt-menu p.empty { margin: 0; padding: 8px 9px; font-size: 12.5px; }

/* 兩種列：篩選用的是連結（挑了直接套用），表單裡的是按鈕（挑了寫進 hidden 欄位）。 */
.opt-menu a, .opt-menu button {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 7px 9px; border: 0; border-radius: 4px; background: none;
  color: var(--muted); font-size: 13px; text-align: left; text-decoration: none;
  white-space: nowrap; cursor: pointer;
}
.opt-menu a:hover, .opt-menu button:hover { background: var(--hover); color: var(--text); text-decoration: none; }
.opt-menu a.on, .opt-menu button.on { color: var(--text); font-weight: 600; }
/* 靠右的淡字：兩個系統底下都有 backend 的時候，分辨的依據就是這個。 */
.opt-menu a em, .opt-menu button em {
  margin-left: auto; font-style: normal; font-size: 10px; color: var(--faint);
  letter-spacing: .06em;
}

.inline-form { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; margin-top: 14px; }
.inline-form .grow { flex: 1; min-width: 230px; }
.inline-form .narrow { width: 82px; }
.inline-form label.check { display: flex; align-items: center; gap: 6px; padding-bottom: 9px; }
.inline-form label.check input { width: auto; margin: 0; }
/*
  一組按鈕綁在一起。整排欄位塞不進一行時，「建立」跟「取消」要一起換到下一行——
  沒有這一層的話換行只落在它們中間，變成「取消」一個人待在第二行。
*/
.inline-form .btns { display: flex; gap: 12px; align-items: flex-end; }

/*
  欄位一列、勾選框與按鈕另一列（.pick-foot）的表單。兩塊各自帶上緣間距，
  這裡只負責把它們上下疊好——勾選框留在欄位那一列會被推到最右邊，
  看起來像最後一個欄位，但它是「按下送出時要不要順便給管理員」。
*/
.stack-form { display: block; }

.row { display: flex; gap: 10px; align-items: center; margin-top: 14px; flex-wrap: wrap; }
.hidden { display: none !important; }

button, .btn {
  display: inline-flex; align-items: center; gap: 7px;
  font: inherit; font-size: 13px; font-weight: 500;
  padding: 7px 15px; border-radius: var(--radius); cursor: pointer; text-decoration: none;
  border: 1px solid var(--line); background: none; color: var(--text);
  white-space: nowrap;
}
button:hover:not(:disabled), .btn:hover { background: var(--hover); text-decoration: none; }
button:disabled { opacity: .4; cursor: not-allowed; }
button.primary, .btn.primary {
  background: var(--accent); border-color: var(--accent); color: var(--accent-ink);
}
button.primary:hover:not(:disabled), .btn.primary:hover { background: var(--accent); opacity: .88; }
button.danger { color: var(--err-ink); }
button.danger:hover:not(:disabled) { background: var(--err-bg); }
button.tiny { padding: 4px 10px; font-size: 12px; }

/* ============ 提示 / 徽章 ============ */

.flash {
  padding: 12px 16px; border-radius: var(--radius); margin-bottom: 22px; font-size: 13px;
  border: 1px solid var(--line);
}
.flash-ok  { color: var(--ok-ink);  border-color: var(--ok-ink); }
.flash-err { color: var(--err-ink); border-color: var(--err-ink); }
.flash a { color: inherit; font-weight: 600; }
.flash-float { max-width: 360px; margin: 24px auto -8px; }

/*
  徽章：小型大寫的文字 + 一條細框，不填色塊。
  只有文字本身帶顏色，所以在灰階的頁面裡讀得到但不刺眼。
*/
.badge {
  display: inline-block; padding: 1px 7px;
  border: 1px solid currentColor; border-radius: 2px;
  text-transform: uppercase; letter-spacing: .09em;
  font-size: 10px; font-weight: 600; white-space: nowrap;
  color: var(--faint);
}
.badge.ok   { color: var(--ok-ink); }
.badge.warn { color: var(--warn-ink); }
.badge.err  { color: var(--err-ink); }
/*
  「進行中」是這一頁上唯一還會變的東西，所以填色——其餘徽章都是細框，
  一顆實心的在整片灰階裡一眼就找得到。
*/
.badge.live {
  background: var(--accent); border-color: var(--accent); color: var(--accent-ink);
}
/* 「未歸檔」講的是「這裡沒有它的帳」，做得比成功／失敗更輕。 */
.badge.unfiled { color: var(--muted); }
/*
  按得動的徽章：狀態本身就是入口（例如點人數看名單）。
  用 <button class="badge badge-btn">——按鈕的預設外觀在這一段之前宣告，
  所以徽章的字級、邊框與顏色蓋得過去，這裡只要補上背景與游標。
*/
.badge-btn { background: none; cursor: pointer; line-height: 1.7; }
.badge-btn:hover { background: var(--hover); }

/*
  Label 標籤：色相（--h）由 app.js 的 chipHue 逐一算好、用 inline style 帶進來，
  灑滿整個色環，不相干的 label 才不容易撞同一個顏色。
  飽和度壓得很低——在近無彩色的頁面裡，只要「色相不同」就分得出來，
  不需要飽和到跳出來跟標題搶注意力。
*/
.chip {
  --h: 210;
  display: inline-block; margin: 1px 5px 1px 0; padding: 1px 7px;
  border-radius: 3px; font: 600 11px/1.6 var(--mono); white-space: nowrap;
  background: hsl(var(--h), 52%, 92%);
  color: hsl(var(--h), 62%, 26%);
  border: 1px solid hsl(var(--h), 40%, 78%);
}
:root[data-theme="dark"] .chip {
  background: hsl(var(--h), 34%, 20%);
  color: hsl(var(--h), 62%, 76%);
  border-color: hsl(var(--h), 28%, 36%);
}
.chips-cell { max-width: 200px; }

/* ============ 進度條（step by step） ============ */

/*
  規則只有一條：一次只展開「目前這一步」，其餘全部收成圓圈。
  四顆主步驟從頭到尾都在，位置固定，所以看得到自己在整條流程的哪裡；
  收合用 max-width 從有到 0 做動畫，才有往左收進去的感覺。

  1-2-3-4 那條線不換行是刻意的：流程斷成兩排就不像流程，像兩排按鈕。
  會換行的只有右邊那群確認項目（見 .stepper-group），它本來就不在那條線上。
*/
.stepper {
  position: relative; z-index: 20;
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px 2px;
  list-style: none; margin: 0 0 16px; padding: 12px 16px;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  overflow-x: auto;
}
.stepper-item { display: flex; align-items: center; flex: 0 0 auto; min-width: 0; }

/* 圓圈之間的連接線。第一顆前面不畫。 */
.stepper-item + .stepper-item::before {
  content: ''; flex: 1 1 auto; min-width: 12px; max-width: 46px;
  height: 1px; margin: 0 10px; background: var(--line);
}
.stepper-item.is-done::before { background: var(--faint); }

/*
  確認項目屬於第三步：比對出結果之前不存在，彼此之間也不連線。

  間隔壓到 6px＋3px 邊距，不跟主流程一樣寬：這一群有五項，每一項多 14px
  就要從左邊那條主流程借走 70px，而被借走的正好是「目前這一步」。
*/
.stepper-item.is-action + .stepper-item.is-action::before {
  flex: 0 0 6px; max-width: 6px; margin: 0 3px; background: none;
}

/*
  確認項目整組推到最右邊，跟 1-2-3-4 那條主流程分開。

  它們不是流程上的第五、六、七步，是第三步底下的檢查清單——排在 4 後面
  會讓人以為「還有好幾步要走」，而真正沒做完的是第四步。分成兩群之後，
  左邊讀「走到哪了」，右邊讀「該做的做了沒」。

  包成一顆 .stepper-group（而不是五顆各自排在 .stepper 底下）是為了換行：
  攤開來整列要 1100px，扣掉側欄與留白，視窗要接近 1400px 才放得下，一般筆電
  就是不夠——差的那段會被切在字的中間。包成一群之後，放不下時是整群掉到
  第二行，不會第一行三顆第二行兩顆；而且門檻由 flex 自己算，不必挑斷點，
  Jira 連不到只剩三項、側欄收合都自動對得上。
*/
.stepper-group { display: none; flex: 1 0 100%; margin-left: 0; }
/*
  確認項目自成一條，不跟 1-2-3-4 併排。

  併排時它從畫面中間開始、靠右結束，左邊留一塊空白——看起來像「第三步後面
  還接著五步」，而它們其實是第三步底下的清單。拉成一整條、跟主流程隔一條線，
  上下兩層各自讀：上面是「走到哪了」，下面是「該做的做了沒」。
*/
.stepper.at-result .stepper-group,
.stepper.at-done .stepper-group {
  display: block; margin-top: 9px; padding-top: 9px; border-top: 1px solid var(--line);
}
.stepper-actions {
  display: flex; align-items: center;
  list-style: none; margin: 0; padding: 0;
}
/* 五項均分整條，每一項拿到一樣寬的位子——寬度一致，字才不會這裡擠那裡空。
   第四步時它們收成圓圈（見底下 .at-done 那條），那時就不必再撐開。 */
.stepper.at-result .stepper-item.is-action { flex: 1 1 132px; }

.stepper-btn {
  display: flex; align-items: center; gap: 10px; min-width: 0; width: 100%;
  padding: 5px 9px; border: 0; border-radius: var(--radius);
  background: none; color: inherit; cursor: pointer; text-align: left;
  font: inherit;
}
.stepper-btn:hover { background: var(--hover); }

/* 空心細圈 →（做完）實心墨點。沒有發光、沒有彩色。 */
.stepper-dot {
  flex: 0 0 22px; width: 22px; height: 22px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font: 600 10.5px/1 var(--mono);
  background: none; color: var(--faint);
  border: 1px solid var(--line);
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}
.stepper-item.is-done .stepper-dot {
  background: var(--accent); border-color: var(--accent); color: var(--accent-ink);
}
/* 目前這一步：邊框轉成墨色、字也是墨色，用對比而不是光暈。 */
.stepper-item.is-current .stepper-dot {
  border-color: var(--accent); color: var(--text);
}
.stepper-item.is-current.is-done .stepper-dot { color: var(--accent-ink); }

/*
  還沒歸檔的第四步。

  壓完 label、複製完清單之後，畫面上的東西全都做完了，唯一沒完成的
  「完成 Release」卻長得跟前面幾步一樣安靜——很多批次就是這樣被留在
  未歸檔狀態。這裡用虛線圈把它標成「還欠這一步」：虛線＝未完成的形狀，
  不是新的顏色，跟站上其餘部分同一套語言。

  只在有比對結果之後才掛（見 app.js 的 updateStepper），
  一進站就閃著一個做不到的步驟只會讓人困惑。
*/
.stepper-item.is-todo .stepper-dot {
  border-style: dashed; border-color: var(--accent); color: var(--accent);
}
.stepper-item.is-todo .stepper-label em { color: var(--accent); }

/*
  剛打勾的那一顆確認項目，在那裡漾一圈。

  只有右邊那五顆會漾：它們是自己打勾的（按下去、請求回來、圈就填成墨色），
  而它們在畫面最上面——手還在下面那張表上的人不會剛好抬頭。一個小圈換個顏色
  說不動這件事，一圈往外漾開的水波可以。主流程那四顆不漾，那是你自己走過去的。

  三圈錯開出發，才是一圈一圈漾開；同時出發只是一個圈在放大。
  圈從那顆圓點自己的大小起跳（--dot 由 app.js 量了給），看起來是它在漾。

  排在進度條（20）之上、浮動按鈕（45）之下：它要蓋過卡片，但教學的遮罩
  壓下來時該讓位——那時候該亮的是被圈起來的地方，不是這裡。
*/
.dot-wave { position: fixed; z-index: 30; width: 0; height: 0; pointer-events: none; }
.dot-wave i {
  position: absolute; left: 0; top: 0;
  width: var(--dot, 22px); height: var(--dot, 22px);
  margin: calc(var(--dot, 22px) / -2) 0 0 calc(var(--dot, 22px) / -2);
  border: 2px solid var(--accent); border-radius: 50%;
  opacity: 0; animation: dot-wave .9s cubic-bezier(.15, .7, .3, 1) both;
}
.dot-wave i:nth-child(2) { animation-delay: .12s; }
.dot-wave i:nth-child(3) { animation-delay: .24s; }
@keyframes dot-wave {
  from { transform: scale(1); opacity: .85; }
  to { transform: scale(4.2); opacity: 0; }
}
/* 不會動的版本：圈直接就在那裡淡掉，一樣看得出剛剛打勾的是哪一顆。 */
@keyframes dot-wave-still {
  from { transform: scale(2.6); opacity: .8; }
  to { transform: scale(2.6); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .dot-wave i { animation-name: dot-wave-still; }
}

/* ── 展開／收合 ── */

/*
  預設是展開的：第一、二步時畫面很空，四個步驟攤開來看得懂在做什麼。
  走到第三步之後才收合成「只展開目前這一步」——那時下面的結果表很長，
  進度條該讓位。收合用 max-width 從有到 0，才有往左收進去的感覺。
*/
.stepper-label {
  display: flex; flex-direction: column; min-width: 0;
  font-size: 12.5px; font-weight: 550; line-height: 1.4;
  overflow: hidden; white-space: nowrap;
  max-width: 220px; opacity: 1; margin-left: 0;
  transition: max-width .3s ease, opacity .18s ease, margin-left .3s ease;
}
.stepper-label em {
  font-style: normal; color: var(--faint);
  overflow: hidden; text-overflow: ellipsis;
}
.stepper-item.is-current .stepper-label { font-weight: 650; }
.stepper-item:not(.is-done):not(.is-current) .stepper-label { color: var(--muted); }

/* 走到第三步（或第四步）之後：除了目前這一步，其餘主流程一律收成圓圈。 */
.stepper.at-result .stepper-item:not(.is-current):not(.is-action) .stepper-label,
.stepper.at-done   .stepper-item:not(.is-current):not(.is-action) .stepper-label {
  max-width: 0; opacity: 0; margin-left: -10px;
}

/* 第三步底下的確認項目：跟著第三步展開，到第四步一起收合。 */
.stepper.at-done .stepper-item.is-action .stepper-label {
  max-width: 0; opacity: 0; margin-left: -10px;
}

/*
  例外：還沒歸檔的第四步不跟著收起來。

  在比對結果那一頁，「完成 Release」是唯一還欠的一步，而上面那條規則
  會把它縮成一個圓圈——那正是最需要被看到的時候。留著字，
  它才講得出「還沒歸檔」。

  選擇器要照抄上面那條再加 .is-todo，一個 :not() 都不能省：:not() 的參數也算
  權重，少寫一個就變成 5 比 6，寫在後面也蓋不過去，這條規則會整條失效。
*/
.stepper.at-result .stepper-item.is-todo:not(.is-current):not(.is-action) .stepper-label {
  max-width: 220px; opacity: 1; margin-left: 0;
}
/*
  目前這一步不縮。

  它原本可以縮（flex: 0 1 auto），而擠不下時 flex 只會挑它下手——別的項目都是
  0 0 auto。結果是最該讀得到的那幾個字被切掉，圓圈旁邊空著一片，
  反而看不出自己在哪。擠不下就讓右邊那群確認項目換行（見 .stepper-group），
  真的還是不夠寬才橫向捲（見最上面的 overflow-x，切換步驟時 app.js 會把
  目前這一步捲進畫面）。
*/
.stepper-item.is-current { flex: 0 0 auto; }

@media (prefers-reduced-motion: reduce) {
  .stepper-label { transition: none; }
}
@media (max-width: 720px) {
  .stepper-label em { display: none; }
}

/*
  手機上步驟列一定放不下四步的文字：只留目前這一步的字，其餘剩圓圈，
  確認項目那一群再掉到第二行——這樣四步加五個動作也排得下。
  真的還是不夠寬時它仍然可以橫向捲，捲軸藏起來，改用「目前這一步捲到看得見」
  來帶位置（app.js 切換時會捲）。
*/
@media (max-width: 640px) {
  .stepper { padding: 10px 12px; gap: 8px 0; scrollbar-width: none; }
  .stepper::-webkit-scrollbar { display: none; }
  .stepper-item + .stepper-item::before { min-width: 8px; max-width: 14px; margin: 0 5px; }
  .stepper-btn { padding: 5px 6px; gap: 7px; }
  .stepper-item:not(.is-current) .stepper-label { display: none; }
  .stepper-label { white-space: nowrap; }
}


/*
  分頁式步驟：一次只顯示一個 .step-panel。
  每一步都獨佔畫面，所以卡片可以放心給足高度，不必擔心把後面的東西推下去。
*/
.step-panel { min-height: 340px; display: flex; flex-direction: column; }
/* 底部的上一步／下一步固定貼在分頁最下緣，位置每一步都一樣才不用找。 */
.step-nav {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: auto; padding-top: 18px;
  border-top: 1px solid var(--line);
}
/* 往回的動作靠左、往前的靠右，中間撐開。少了這條就會全部擠在左邊。 */
.step-nav .spacer { flex: 1; min-width: 20px; }
.scope, #result-groups, #paste, #iss-list { margin-bottom: 24px; }

/*
  兩種模式的切換器。擺在進度條上面：它決定的是「這條路有幾步」，
  不是流程裡的一步，混進進度條裡會變成一個永遠停不下來的第零步。
*/
.batch-modes { margin: 0 0 14px; }

/* 不屬於目前模式的步驟整顆收掉，連它前面那條連接線一起。 */
.stepper[data-mode="pick"] [data-mode-only="paste"],
.stepper[data-mode="paste"] [data-mode-only="pick"] { display: none; }
/*
  收掉前兩顆之後，「貼上內容」變成第一顆，它前面不該再有一截線
  ——那條線本來是用「第一個元素沒有 ::before」畫出來的，而它現在
  已經不是第一個元素了。
*/
.stepper[data-mode="paste"] [data-step="paste"]::before { content: none; }

/* 分頁切換時淡入，讓「換了一頁」這件事看得出來，但不要拖時間。 */
.step-panel:not(.hidden) { animation: panel-in .16s ease-out }
@keyframes panel-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .step-panel:not(.hidden) { animation: none }
}

/* 完成 Release 那一步：先讓人確認這次歸檔的內容，再填追蹤資訊。 */
.done-summary {
  margin: 16px 0 0; padding: 14px 16px;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);
  font-size: 13px;
}
.done-summary .k { color: var(--faint); margin-right: 8px; }
.done-summary .row-line { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
.done-summary .row-line:last-child { margin-bottom: 0; }
.done-form { display: flex; flex-direction: column; gap: 14px; margin: 20px 0 24px; max-width: 640px; }
.done-form textarea { font-family: inherit; font-size: 13.5px; }

/*
  引導區塊（未設定 PAT 時）：用一條上緣細線 + 小型大寫標籤帶出來，
  不用彩色左邊條。
*/
.guide { border-top: 2px solid var(--accent); }
.guide ol { margin: 14px 0 18px; padding-left: 20px; color: var(--muted); font-size: 13px; }
.guide ol li { margin-bottom: 8px; }
.guide ol li strong { color: var(--text); }

/* ============ 表格 ============ */

/*
  表格自己負責橫向捲動。
  少了這一層，窄畫面上多欄表格會被擠到後面幾欄整個推出卡片外——
  「編輯／刪除」那一欄拿不到，等於功能在手機上消失。
*/
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.table-scroll .grid { margin-top: 14px; }

.grid { width: 100%; border-collapse: collapse; margin-top: 14px; font-size: 12.5px; }
.grid th, .grid td {
  padding: 7px 10px; border-bottom: 1px solid var(--line);
  text-align: left; vertical-align: middle;
}
.grid th:first-child, .grid td:first-child { padding-left: 0; }
.grid th:last-child, .grid td:last-child { padding-right: 0; }
/* 表頭：安靜的小字就夠了。這裡每一欄都是標籤，再套小型大寫會整片都在喊。 */
.grid thead th {
  font-size: 11.5px; font-weight: 500; color: var(--muted);
  white-space: nowrap; letter-spacing: .01em;
  border-bottom-color: var(--faint);
  padding-bottom: 8px;
}
.grid tbody tr:hover { background: var(--hover); }
.grid tbody tr:last-child td { border-bottom: none; }
.grid td input { margin-top: 0; }
/* 欄名在上、全選框在下。整塊是 <label>，點欄名也等於點那顆勾選框。 */
.th-cb {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  color: inherit; font-size: 11px; cursor: pointer; user-select: none;
}
.w-cb { width: 52px; text-align: center !important; }
.w-cb input { display: block; margin: 0 auto; }
.w-act { width: 1%; white-space: nowrap; text-align: right !important; }
.w-act button { margin-left: 4px; }
.w-narrow { width: 80px; }
.w-pw { width: 118px !important; display: inline-block !important; }
td.title { max-width: 400px; }

/*
  重複與範圍外：用文字明度表示，不塗整列底色、也不加彩色邊條。
  真正的說明在「結果」欄的文字裡，這裡只負責讓它在視覺上退一階。
*/
tr.dup td, tr.out-of-scope td { color: var(--muted); }
tr.out-of-scope td.title, tr.dup td.title { color: var(--muted); }
.state-closed { color: var(--faint); }

/*
  「重抓」補進來的那幾列：左邊一條細邊，其餘照舊。

  跟上面那兩種一樣不塗底色——但它不是「退一階」的東西，而是「這一列是後來
  補進來的」，所以用一條邊而不是把字變淡。字變淡的話，它看起來像可以忽略的，
  而它正好是這次最該看一眼的幾列。
*/
tr.from-gitlab td:first-child { box-shadow: inset 2px 0 0 var(--accent); }

/* Jira key 欄不斷字：ELD-329 被折成兩行會很難讀。 */
td.key { white-space: nowrap; }

/*
  時間戳記：日期一行、時刻一行。

  時間本來就是最容易被折斷的一欄（它最長、又全是可以斷的字元），而
  「08-12 19:39:」加上另一行的「11」比兩行完整的值難讀得多。與其讓瀏覽器
  挑一個難看的斷點，不如自己決定要斷在哪裡。
*/
.stamp { white-space: nowrap; line-height: 1.5; }
.stamp > * { display: block; }
/* 時刻退一階：同一天的紀錄看的是時刻，跨天才需要日期，兩者不必等重。 */
.stamp .t { color: var(--faint); }

/*
  一格裡疊兩樣東西（Email + 狀態徽章）。

  用 flex 而不是把子元素設成 block：徽章要維持自己的寬度，
  變成 block 會讓那個框拉滿整欄，看起來像一條色帶。

  這個 class 一定要下在 td 裡面的 div 上，不能直接下在 td 上——
  display:flex 會讓那一格不再是表格儲存格，整列的邊框與垂直對齊都會跟著跑掉。
*/
.stack { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }
/* code 預設 break-all，Email 被折在中間比讓表格橫向捲更難讀。 */
.stack .mail { white-space: nowrap; word-break: normal; }

/*
  「細節」是這幾張表裡唯一長度不定的欄位，上下限都要給。

  上限：沒有的話它會把整列的剩餘寬度吃光——其他欄位被擠到折行，而它自己
  變成一條橫跨半個螢幕、眼睛得來回掃的長句。多出來的寬度會分給其他欄位，
  那些反正都不折行，寬一點也不會怎樣。

  下限：其他欄位全都不折行，所以畫面一窄，瀏覽器唯一能壓的就是這一欄——
  它會被壓成一條、每個字元自成一行。有了下限，表格會改成整個超出寬度，
  由 .table-scroll 橫向捲，那才是預期的行為。

  overflow-wrap 用 break-word 而不是 anywhere：後者會讓這一欄的最小寬度
  變成「一個字元」，於是上面那個下限就失效了，照樣被壓扁。
*/
td.detail {
  min-width: 220px; max-width: 340px;
  white-space: normal; overflow-wrap: break-word;
}
/* 失敗那幾筆的細節寫的是失敗原因，是整列最該讀的東西，不要跟著淡下去。 */
td.detail-err { color: var(--err-ink); }
/* 「無工作區」跟在名字後面，間距比標題那顆窄——那顆離的是一整行標題，
   這顆離的是一個名字，照 10px 會看起來像跟隔壁欄同一組。 */
.grid .ws-tag { margin-left: 6px; }
/* 歸檔時還沒做的確認項目。不是錯誤，是「這批發版少做了一件事」，用警示色。 */
.skip-list { color: var(--warn-ink); }

/* Release 紀錄裡的 issue 編號：看起來像純文字，滑過去才顯出是連結。
   整欄都是連結時，一片藍色會比表格內容本身還搶眼。 */
.iid-link { color: inherit; text-decoration: none; }
.iid-link:hover { color: var(--text); text-decoration: underline; }

/* issue 編號直接接在標題後面，用等寬字跟標題分開，不用顏色。 */
.iid { margin-left: 8px; white-space: nowrap; font: 11.5px var(--mono); color: var(--faint); text-decoration: none; }
.iid:hover { color: var(--text); text-decoration: underline; }
.empty { padding: 26px 10px; text-align: center; color: var(--faint); font-size: 12.5px; }

/* 比對結果：一個專案一張卡，各自帶版號欄。 */
#result-groups { display: flex; flex-direction: column; gap: 12px; margin-top: 14px; }
.res-proj {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card); overflow: hidden;
}
.res-proj.out-of-scope { border-style: dashed; }
.res-proj-head {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 11px 14px; background: var(--bg);
  cursor: pointer; user-select: none; list-style: none;
}
.res-proj-head::-webkit-details-marker { display: none; }
.res-proj-head:hover { background: var(--hover); }
.res-proj[open] .res-proj-head { border-bottom: 1px solid var(--line); }
.res-caret { flex: 0 0 12px; width: 12px; height: 12px; color: var(--faint); transition: transform .15s ease; }
.res-proj[open] .res-caret { transform: rotate(90deg); }
/* 版號欄與按鈕在 summary 裡，點它們不該連帶收合，游標也要回到正常樣子。 */
.res-proj-head .res-label, .res-proj-head .res-apply { cursor: auto; }
.res-proj-head .res-apply { cursor: pointer; }
.res-proj-head strong { font-size: 13.5px; font-weight: 600; }
.res-proj-head .spacer { flex: 1; }
.res-proj-head .res-label {
  width: 190px; margin-top: 0; font-family: var(--mono); font-size: 12px;
}
.res-proj-head .res-apply { padding: 6px 14px; font-size: 12.5px; }
.res-proj .grid { margin-top: 0; }
.res-proj .grid th:first-child, .res-proj .grid td:first-child { padding-left: 14px; }
.res-proj .grid th:last-child, .res-proj .grid td:last-child { padding-right: 14px; }
.res-proj .grid tbody tr:last-child td { border-bottom: none; }
/* 結果欄：GitLab 與 Jira 各佔一列，兩件事的結果並存不互相覆蓋。 */
.res-cell { min-width: 150px; }
.res-slot:empty { display: none; }

/*
  狀態那一格底下的 Jira 狀態與兩種警示（見 app.js 的 paintSync）。

  警示的列整列鋪一層底色、左邊一條線：清單一長，眼睛是往下掃的，
  只靠格子裡一顆小徽章會被掃過去。
*/
.state-cell .jira-sync { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 6px; margin-top: 3px; }
.state-cell .jira-sync:empty { display: none; }
/* 這兩顆警示裡有站名（Resolved）與系統名（GitLab、Jira），照原樣寫才認得出
   是哪一個東西——徽章預設的全大寫會把 Resolved 變成另一種寫法。 */
.state-cell .jira-sync .badge { text-transform: none; letter-spacing: .02em; font-size: 10.5px; }
.jira-state { font-size: 11.5px; color: var(--muted); white-space: nowrap; }
#result-groups tr.sync-off td, #result-groups tr.needs-rd td { background: var(--warn-bg); }
#result-groups tr.sync-off td:first-child, #result-groups tr.needs-rd td:first-child { box-shadow: inset 3px 0 0 var(--warn-ink); }
#result-groups tr.sync-off .jira-state, #result-groups tr.needs-rd .jira-state { color: var(--warn-ink); font-weight: 600; }

/* 「送測試申請單」剛複製完的那兩秒（見 app.js 的 flashCopied）。 */
#btn-copy.just-copied { color: var(--ok-ink); border-color: var(--ok-ink); background: var(--ok-bg); }
#btn-copy.just-copied .count { display: none; }

/* 剪貼簿裡的那份清單，貼之前先看一眼。長了就自己捲，不把對話框撐出畫面。 */
.copy-preview {
  margin: 0 0 10px; padding: 10px 12px; max-height: 240px; overflow: auto;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);
  font: 12px/1.6 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere;
}

/* 轉狀態失敗視窗裡「請 RD 完成到 Resolved」那一塊。 */
.rd-note {
  margin: 4px 0; padding: 9px 12px; border-radius: var(--radius);
  background: var(--warn-bg); color: var(--warn-ink); border-left: 3px solid var(--warn-ink);
  line-height: 1.7;
}
.rd-note code { color: var(--text); }
.res-slot { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; margin-bottom: 3px; }
.res-msg { overflow-wrap: anywhere; }

/* ============ Release 紀錄（收合的發版批次） ============ */

.rel-group {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  margin-bottom: 12px;
}
/*
  收合列排成固定的幾欄：箭頭｜系統與專案｜日期與版號｜右邊那塊。

  以前是一排 flex：版號緊跟在系統名後面，系統名一長一短，版號的起點就跟著
  左右跑；右邊那塊塞不下就整塊掉到下一行。每一列都長得不一樣，一長串看下來
  像沒排過。

  每一列是各自的一個 grid，只有寫死寬度的東西才會跨列對齊——所以系統那一欄
  和日期都給固定寬度，版號接在日期後面，起點就每列一樣。
*/
.rel-head {
  display: grid; align-items: center; column-gap: 18px; row-gap: 6px;
  grid-template-columns: 12px minmax(0, 15rem) minmax(0, 1fr) auto;
  padding: 13px 18px; cursor: pointer; user-select: none;
  list-style: none;   /* 拿掉預設三角形，改用自己的箭頭圖示 */
}
.rel-mid { display: flex; align-items: center; gap: 18px; min-width: 0; }
.rel-mid > .rel-date { flex: 0 0 8.5rem; }
.rel-mid > .rel-vers { flex: 1 1 auto; min-width: 0; }
.rel-head::-webkit-details-marker { display: none; }
.rel-head:hover { background: var(--hover); }
.rel-group[open] .rel-head { border-bottom: 1px solid var(--line); }

/* 這一列的右半邊：整塊靠右，塞不下時整塊換行，仍然靠右。 */
.rel-tail {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  justify-content: flex-end;
}
.rel-caret {
  flex: 0 0 12px; width: 12px; height: 12px; align-self: center;
  color: var(--faint); transition: transform .15s ease;
}
.rel-group[open] .rel-caret { transform: rotate(90deg); }

/* 系統名是這一批的標題，字級拉大；版號是 metadata，走等寬字。 */
.rel-sys { font-size: 15px; font-weight: 600; letter-spacing: -.015em; }
/* 借來的系統名旁邊那個小標記。做得比名字輕，它是註解不是名字的一部分。 */
.rel-src {
  margin-left: 6px; padding: 1px 5px; border-radius: 4px;
  border: 1px solid var(--line); background: var(--bg);
  font: 500 10.5px/1.5 var(--mono); color: var(--muted);
  letter-spacing: 0; vertical-align: 1px;
}
/* 系統一行、專案一行。專案是「這個系統裡的哪幾個」，所以縮在標題底下。 */
.rel-what { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.rel-projs { font-size: 12px; color: var(--muted); }
.rel-ver { font: 500 12.5px var(--mono); color: var(--muted); background: none; padding: 0; }
.rel-date { white-space: nowrap; font-family: var(--mono); font-size: 11.5px; }

.rel-body { padding: 4px 18px 14px; }
.rel-users { margin: 14px 0 0; }

.rel-proj { margin-top: 16px; }

/* Release 卡片的追蹤資訊：兩欄的定義清單，標籤靠左對齊。 */
.rel-meta { display: grid; grid-template-columns: 84px 1fr; gap: 6px 14px; margin: 12px 0 0; }
.rel-meta dt { color: var(--faint); font-size: 12px; }
.rel-meta dd { margin: 0; font-size: 12.5px; min-width: 0; overflow-wrap: anywhere; }
.rel-note { margin-top: 16px; display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap; }
.rel-note label { flex: 1; min-width: 240px; }
.rel-note textarea { font-family: inherit; font-size: 13px; }
/* 歸檔後還能補的那張表單：兩串網址加備註，一樣一列，不再排成一橫排。 */
.rel-edit { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; max-width: 640px; }
.rel-edit > button[type="submit"] { justify-self: start; }

/*
  可以好幾筆的網址（測試預約、討論串）。一格一個，「＋ 再加一筆」多開一格
  （見 app.js 的 .url-add）。標題字跟一般 label 長得一樣：它就是那一欄的 label，
  只是底下不只一個輸入框，包不進單一個 <label> 裡。
*/
.url-list { display: flex; flex-direction: column; gap: 6px; }
.url-cap { font-size: 12px; color: var(--muted); }
.url-rows { display: flex; flex-direction: column; gap: 6px; }
.url-add { align-self: flex-start; }
.url-line { display: block; overflow-wrap: anywhere; }
.rel-vers { display: flex; gap: 6px; flex-wrap: wrap; }
/* 刪除放在收合列上，不必展開才找得到。 */
.rel-del { display: inline-flex; }
.rel-danger { margin-top: 14px; }
/*
  三種狀態同一份清單（見 releases.html），所以列本身也要分得出來：
  進行中的左邊一條實線，未歸檔的整列退一階。光靠右邊那顆徽章不夠——
  清單一長，眼睛是先掃左邊再讀右邊的。
*/
.rel-group.live { border-left: 2px solid var(--accent); }
.rel-group.stray .rel-sys { font-weight: 500; color: var(--muted); }
.rel-proj-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.rel-proj-head strong { font-size: 13.5px; font-weight: 600; }
.rel-proj-head .small { font-family: var(--mono); font-size: 11.5px; }
.rel-proj .grid { margin-top: 8px; }
/* 展開後第二張表的小標。跟上一張表隔開一段，看得出是另一件事。 */
.rel-sub { margin: 18px 0 6px; font-size: 12.5px; font-weight: 600; color: var(--muted); }

/*
  手機上擠不下五欄：箭頭與標題一行，版號、日期、右邊那塊依序往下排，
  全部對齊標題的左緣。

  排在桌機的 .rel-head 後面才蓋得掉——同分的規則誰在後面誰算。
*/
/*
  側欄加上四欄放不下的寬度：標題和右邊那塊留在第一行，日期與版號整塊
  移到標題底下。不這樣做的話，被擠的是中間那一欄，版號會窄到一個字一行。
*/
@media (max-width: 1100px) {
  .rel-head { grid-template-columns: 12px minmax(0, 1fr) auto; }
  .rel-head > .rel-mid { grid-column: 2 / -1; grid-row: 2; }
}
@media (max-width: 640px) {
  .rel-head { grid-template-columns: 12px minmax(0, 1fr); }
  .rel-head > .rel-mid { grid-column: 2; grid-row: 2; flex-wrap: wrap; row-gap: 4px; }
  .rel-head > .rel-tail { grid-column: 2; grid-row: 3; justify-content: flex-start; }
}

/* Release 作業最上面那一條：做到一半的東西在這一頁不留痕跡，靠它講出來。 */
.open-note {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 9px 14px; margin-bottom: 14px;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--accent-soft); font-size: 12.5px; color: var(--text);
}
.open-note .spacer { flex: 1; }
.open-note a.tiny { text-decoration: none; }

/* ============ 接續作業（進行中的批次） ============ */

/*
  一批一張卡，不收合。進行中的批次永遠是少數，而看的人要的是「走到哪了」
  ——那句話要一眼讀完，展開才看得到就失去意義了。
*/
.rb-card {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 14px 18px 12px; margin-bottom: 12px;
}
.rb-top { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; }
.rb-who {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  justify-content: flex-end; margin-left: auto;
}
/* 自己的那幾批標一個實心的記號，別人的只寫名字：這一頁上大部分是別人的，
   被標出來的該是「你可以直接接下去的那幾張」。 */
.rb-mine {
  padding: 1px 7px; border-radius: 999px;
  background: var(--accent-soft); color: var(--text);
  font-size: 11.5px; font-weight: 600;
}
.rb-owner { font-size: 12.5px; color: var(--text); }
.rb-mid { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-top: 8px; }

/* 五顆燈橫著排，順序就是作業順序（見 store.ReleaseChecks）。 */
.rb-steps {
  display: flex; flex-wrap: wrap; gap: 6px 16px;
  margin: 12px 0 0; padding: 0; list-style: none;
}
.rb-step { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--faint); }
.rb-dot {
  width: 9px; height: 9px; border-radius: 50%; flex: 0 0 9px;
  border: 1.5px solid var(--line); background: none;
}
.rb-step.on { color: var(--text); }
.rb-step.on .rb-dot { border-color: var(--ok-ink); background: var(--ok-ink); }

.rb-act {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  justify-content: flex-end; margin-top: 12px;
}

@media (max-width: 640px) {
  /*
    窄螢幕上五顆燈改成直的一排。橫著擺會在「送測試申請單」那幾個字上斷行，
    斷行之後點與字對不起來，看起來像多了一顆沒有名字的燈。

    這一段排在桌機規則後面才蓋得掉——app.css 前段那幾個 @media 同分時
    會輸給後面出現的規則（見檔案開頭那段說明）。
  */
  .rb-steps { flex-direction: column; gap: 5px; }
  .rb-act { justify-content: space-between; }
}

/* ============ 成員管理 ============ */

/*
  「加入現有帳號」的挑人清單。

  這裡刻意不用瀏覽器的下拉（datalist）：那東西每家瀏覽器長得都不一樣，
  跟這套介面其他地方也不像，而且 Email 與「現在在哪些工作區」只塞得進
  它的灰字欄位——那兩件事恰恰是決定「該不該加這個人」的依據。
  整塊自己畫，樣式才跟站上其他清單一致。
*/
.pick-form { margin-top: 14px; }
.pick-form .scope-search { margin-top: 0; }

/* 清單本身有高度上限：帳號一多就在框內捲，不要把底下的按鈕推到畫面外。 */
.pick-list {
  margin-top: 10px; max-height: 320px; overflow-y: auto;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg);
}

/*
  會捲動的清單要看得出來它會捲。

  macOS 的捲軸平常是隱形的，而清單剛好被切在容器邊緣時，最後一列看起來就像
  「就這些」——底下還有五十列也一樣。所以捲軸讓它常駐：它同時說得出「還有多少」。

*/
.pick-list, .imp-tree { scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
.pick-list::-webkit-scrollbar, .imp-tree::-webkit-scrollbar { width: 11px; }
.pick-list::-webkit-scrollbar-track, .imp-tree::-webkit-scrollbar-track { background: transparent; }
.pick-list::-webkit-scrollbar-thumb, .imp-tree::-webkit-scrollbar-thumb {
  background: var(--line); border-radius: 6px; border: 3px solid transparent; background-clip: content-box;
}
.pick-list:hover::-webkit-scrollbar-thumb, .imp-tree:hover::-webkit-scrollbar-thumb { background: var(--faint); background-clip: content-box; }

.pick-row {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 10px 14px; cursor: pointer; border-bottom: 1px solid var(--line);
}
.pick-row:last-of-type { border-bottom: 0; }
.pick-row:hover { background: var(--hover); }
/* 選中的那一列要一眼看得出來——按下「加入」之前的最後一次確認就是它。 */
.pick-row:has(input:checked) { background: var(--card); box-shadow: inset 2px 0 0 var(--accent); }
.pick-row input[type=radio] { width: auto; margin: 0; accent-color: var(--accent); }

.pick-who { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.pick-name { font-weight: 500; font-size: 13.5px; }
.pick-who code { font-size: 12px; color: var(--faint); }

/* 右半邊是判斷依據：信箱與「現在在哪」。窄畫面時整組掉到下一行，不擠成一團。 */
.pick-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-left: auto; }

.pick-foot { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 12px; }
.pick-foot label.check { display: flex; align-items: center; gap: 6px; }
.pick-foot label.check input { width: auto; margin: 0; }
/* 勾選框與按鈕整組推到右邊，跟卡片右上角那兩顆按鈕對齊同一條邊界。 */
.pick-foot .spacer { flex: 1; min-width: 12px; }
.pick-foot .btns { display: flex; gap: 12px; }

/* ============ 系統與專案設置 ============ */

.sys-card { padding: 0; overflow: hidden; }
.sys-head {
  display: flex; align-items: baseline; gap: 12px;
  padding: 13px 20px;
  cursor: pointer; user-select: none;
  list-style: none;   /* 拿掉預設三角形，改用自己的箭頭圖示 */
}
.sys-head::-webkit-details-marker { display: none; }
.sys-head:hover { background: var(--hover); }
/* 那條線只在展開時畫：收起來的時候它底下什麼都沒有，畫了就變成一條斷在半空的線。 */
.sys-card[open] > .sys-head { border-bottom: 1px solid var(--line); }
/* 這個箭頭兩頁共用：這裡一張系統卡片，Release 作業第一步的挑選清單一個系統一列。
   基本樣式寫在這裡，轉向各自寫在自己的 [open] 規則上。 */
.sys-caret { flex: 0 0 12px; width: 12px; height: 12px; color: var(--faint); transition: transform .15s ease; }
/* 標題列是 baseline 對齊的，箭頭得自己置中，不然它會掛在文字的底線上。 */
.sys-head .sys-caret { align-self: center; }
.sys-card[open] .sys-caret { transform: rotate(90deg); }
.sys-title { font-size: 15px; font-weight: 600; margin: 0; letter-spacing: -.015em; }
.sys-count { color: var(--faint); font-size: 12px; font-family: var(--mono); }
.sys-head .spacer { flex: 1; }
/* 手機上 spacer 自己佔滿一整列，把整組按鈕推到標題底下。
   上面的 @media 區塊裡本來就有同一條，但它排在這一行之前——同分的時候
   後面的贏，所以那一份從來沒生效過，按鈕會斷在標題那一列的中間。 */
@media (max-width: 640px) {
  .sys-head .spacer { flex: 0 0 100%; height: 0; }
}
.sys-body { padding: 2px 20px 14px; }
.edit-panel { padding: 14px 20px; background: var(--bg); border-bottom: 1px solid var(--line); }
.edit-panel .inline-form { margin-top: 0; }
/*
  一列一個專案，名稱在上、路徑在下。

  整條 https://gitlab1.program.com.tw/… 曾經自成一欄，但每一列只有結尾那幾個字
  不一樣——十個專案就是十行讀不完的重複前綴，而那正是這張表看起來很亂的原因。
  名稱本身當連結（要開 GitLab 點名字就好），底下留路徑：路徑才是這個專案的身分，
  名稱是自己取的，可以叫任何東西。
*/
.proj-cell { min-width: 0; }
/* 每個專案後面都跟著一列藏起來的編輯表單，所以真正的最後一列是倒數第二個
   ——`tr:last-child` 對不到它，卡片底部就會多一條沒有接到任何東西的線。 */
.sys-body .grid tbody tr:nth-last-child(2) td { border-bottom: none; }
.proj-name { font-weight: 500; color: inherit; text-decoration: none; }
.proj-name:hover { text-decoration: underline; }
.proj-cell .badge { margin-left: 8px; }
/* 名稱可能被貼成一整條網址（從 GitLab 匯入時取的名字就是專案名，但手動新增的
   可以打任何東西）。讓它在欄內斷行，不要把「操作」那幾顆推出畫面。 */
.proj-name, .proj-path { overflow-wrap: anywhere; }
.proj-path { display: block; margin-top: 2px; color: var(--faint); font-size: 11.5px; }
/* 停用的專案整列降一階明度，但仍然讀得到。 */
tr.is-off td { color: var(--faint); }
tr.is-off .proj-name { font-weight: 400; }

/*
  從 GitLab 匯入。三步收在同一個面板裡，用編號與細線分段，不用彈窗
  ——清單加預覽在彈窗裡捲不動，手機上尤其。

  刻意跟站上其他挑選介面共用 .scope-search 與 .pick-list：
  這裡多畫一套只會多一種「看起來很像但不太一樣」的清單。
*/
#gl-import { margin-top: 16px; border-top: 1px solid var(--line); }

/* 資料時間與重新抓取。放在面板最上面而不是某一步裡面：
   撈回來的東西整個面板共用一份，那顆按鈕也是。 */
.imp-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding-top: 12px; }
.imp-bar .spacer { flex: 1; }
/* 「找不到專案？」跟按鈕貼在一起，讀起來要是一句話而不是兩個零件。
   清單留得很久，這句話就是清單該不該更新的真正入口。 */
.imp-miss { white-space: nowrap; }

/* 分隔線畫在「上面那一步的下緣」而不是自己的底：後面兩步在選好系統之前
   整段不在，畫在底部會留下一條沒有下文的線。 */
.imp-step { padding: 14px 0; }
.imp-step + .imp-step { border-top: 1px solid var(--line); }
.imp-step:last-of-type { padding-bottom: 4px; }
.imp-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.imp-head .spacer { flex: 1; }
/* 步驟編號跟批次作業的進度圓點是同一個語彙，只是小一號。 */
.imp-no {
  flex: 0 0 auto; display: grid; place-items: center;
  width: 19px; height: 19px; border-radius: 50%;
  background: var(--accent-soft); color: var(--muted);
  font: 600 11px var(--mono);
}
.imp-title { font-size: 13.5px; font-weight: 600; }
.imp-sum { color: var(--muted); font-size: 12.5px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.imp-body { margin-top: 4px; }
.imp-body .scope-search { margin-top: 10px; }
/* 收起來的那一步只留標題與摘要。
   選過群組之後標題列是雙向的開關：攤開去換群組，收起來看這一層的專案。 */
.imp-step.is-folded .imp-body { display: none; }
.imp-step.is-pickable .imp-head { cursor: pointer; }
.imp-step.is-pickable .imp-head:hover .imp-title { color: var(--text); }
.imp-step.is-folded .imp-head:hover .imp-sum { color: var(--text); }

/*
  一棵就地展開的樹。一列一個群組或一個專案，縮排靠一個 padding 數字，
  不用巢狀的 div——深一點就會疊出一堆只為了縮排而存在的框。
*/
.imp-tree {
  margin-top: 10px; max-height: 420px; overflow-y: auto;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg);
}
.imp-tree > .empty { margin: 0; padding: 18px 14px; }

.imp-node {
  display: flex; align-items: center; gap: 8px; min-width: 0;
  padding-top: 6px; padding-bottom: 6px; padding-right: 12px;
  border-bottom: 1px solid var(--line); cursor: pointer;
}
.imp-node:last-child { border-bottom: 0; }
.imp-node:hover { background: var(--hover); }
.imp-node input[type=checkbox] { width: auto; margin: 0; flex: 0 0 auto; accent-color: var(--accent); }
/* 群組那一列站在自己底下那幾列前面，字重稍微重一點，掃過去分得出層次。 */
.imp-node.is-group { background: var(--card); }
.imp-node.is-group .imp-node-main > span:first-child { font-weight: 600; }
.imp-node-main { display: flex; align-items: baseline; gap: 8px; min-width: 0; cursor: pointer; }
.imp-node-main > span:first-child { font-size: 13px; overflow-wrap: anywhere; }
.imp-node-main code { color: var(--faint); font-size: 11.5px; overflow-wrap: anywhere; }
.imp-node-n { margin-left: auto; font-family: var(--mono); white-space: nowrap; padding-left: 8px; }
.imp-node-note { margin: 0; padding-top: 8px; padding-bottom: 8px; text-align: left; }

/* 展開鈕跟勾選框分開：一個是「給我看看裡面」，一個是「我要這一整包」。
   合成一個的話，想看一眼就會勾走五十個專案。 */
.imp-tog {
  flex: 0 0 auto; width: 22px; height: 22px; padding: 0;
  color: var(--faint); background: none; border: 0; border-radius: 4px;
  display: inline-flex; align-items: center; justify-content: center;
  transition: transform .15s;
}
.imp-tog svg { width: 7px; height: 10px; fill: currentColor; stroke: none; }
.imp-tog:hover:not(:disabled) { background: var(--line); color: var(--text); }
.imp-tog.is-open { transform: rotate(90deg); }

/* 已經加入過的那幾列鎖住，整列降一階明度但仍然讀得到。 */
.imp-node.is-taken { cursor: default; color: var(--faint); }
.imp-node.is-taken:hover { background: none; }

/*
  第一步：這一批要進哪個系統。

  一個輸入框加一份既有系統清單，不是兩種模式——匯入本來就是照名稱找系統，
  找不到才新建，所以「建一個新的」與「挑既有的」在資料上是同一件事。
  旁邊那個徽章負責把差別講出來：新系統／併入既有。
*/
.imp-sys-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 12px; }
input#imp-sys-name { width: 260px; margin-top: 0; font-size: 13.5px; }
/* 通知網址那一格撐滿剩下的寬度：它是一整串帶 key 與 token 的網址，
   給固定寬度只看得到開頭那一小段，貼錯了也看不出來。 */
input#imp-sys-hook { flex: 1; min-width: 260px; margin-top: 0; font-size: 13.5px; }
#imp-sys-badge:empty { display: none; }
.imp-step[data-step="sys"] .hint { margin: 12px 0 0; }

/* 既有系統的清單，點一列把名稱填進去。跟群組那幾列同一種東西（一列一個按鈕），
   所以同樣要先把 button 的預設外觀清掉。 */
#imp-existing { margin-top: 8px; max-height: 200px; }
.imp-sys {
  width: 100%; text-align: left; font: inherit; color: inherit;
  background: none; border: 0; border-bottom: 1px solid var(--line);
  border-radius: 0; padding: 10px 14px;
}
.imp-sys:hover:not(:disabled) { background: var(--hover); }
/* 目前正在填的那一個。不標的話點下去畫面上沒有任何回應。 */
.imp-sys.is-on { background: var(--card); box-shadow: inset 2px 0 0 var(--accent); }
.imp-sys.is-on .pick-name { font-weight: 600; }

#imp-cart:not(:empty) { margin-top: 4px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg); }
#imp-cart > .empty { margin: 4px 0 0; }
.imp-plan-item { border-bottom: 1px solid var(--line); }
.imp-plan-item:last-child { border-bottom: 0; }
/* 正在填的那一組。跟第一步既有系統清單上那個標記同一種畫法。 */
.imp-plan-item.is-on { background: var(--card); box-shadow: inset 2px 0 0 var(--accent); }
.imp-plan-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 8px 14px;
}
/* 系統名稱是個輸入框：改名改在這裡，游標點進去就等於「我現在填這一組」。
   帶上元素名是為了壓過 input[type=text] 那一組的寬度（那組是 100%，
   不壓的話這個框會撐滿整列，把徽章與「N 個專案」擠到下一行）。 */
input.imp-plan-name { width: 190px; margin-top: 0; font-size: 13px; padding: 6px 10px; }
/* 「N 個專案」是這一列唯一可以按的東西，所以要畫成按得下去的樣子：
   框線、箭頭、展開時反白。畫成一段灰字的話，沒有人會去點它。 */
.imp-plan-n {
  margin-left: auto; display: inline-flex; align-items: center; gap: 6px;
  font: inherit; font-size: 12.5px; color: var(--muted);
  background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 4px 10px;
}
.imp-plan-n:hover:not(:disabled) { border-color: var(--faint); color: var(--text); }
.imp-plan-n[aria-expanded="true"] {
  background: var(--accent-soft); border-color: var(--accent-soft); color: var(--text);
}
.imp-plan-chev { display: inline-block; font-size: 10px; line-height: 1; transition: transform .15s; }
.imp-plan-n[aria-expanded="true"] .imp-plan-chev { transform: rotate(180deg); }
.imp-plan-list { margin: 0; padding: 2px 14px 10px 26px; list-style: none; }
.imp-plan-list li { display: flex; align-items: baseline; gap: 8px; padding: 2px 0; font-size: 12.5px; }
/* 路徑橫跨好幾個群組，沒有共同前綴可以省，所以會很長。讓它在框內斷行，
   不要把「N 個專案」跟「拿掉」擠到下一行去——那正是這份清單看起來很亂的原因。 */
.imp-plan-list code { color: var(--faint); font-size: 11.5px; min-width: 0; word-break: break-all; }

.imp-foot { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 14px; }
.imp-foot .spacer { flex: 1; }

/* ============ 批次作業 ============ */

/* 搜尋列：跨系統與專案一起找，打字就即時篩。
   成員管理的「加入現有帳號」也用同一組（見 .pick-form），兩邊長得一樣。 */
.scope-search { display: flex; align-items: center; gap: 12px; margin: 22px 0 0; }
/* 圖示疊在輸入框裡，靠左邊的內縮讓出位置。 */
.scope-search-box { position: relative; flex: 1; min-width: 0; display: block; }
.scope-search-box svg {
  position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  width: 14px; height: 14px; color: var(--faint); pointer-events: none;
}
/*
  appearance: none 是關鍵：input[type=search] 在 Safari／Chrome 有自己的預設外觀
  （內凹的圓角框線），會蓋掉我們設的邊框，長出一圈跟整站格格不入的線。
*/
.scope-search-box input {
  -webkit-appearance: none; appearance: none;
  margin-top: 0; padding: 9px 12px 9px 34px;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);
}
.scope-search-box input:focus { background: var(--card); }
.scope-search-box input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }

/*
  每個系統一張等高的卡片。用 grid 而不是 columns，是因為要「所有卡片一樣高」
  ——高度一致才不會像之前那樣高高低低。專案多的就在卡片內部捲，
  卡片本身的高度永遠不變。
*/
.scope {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 12px; margin-top: 16px;
}
.system {
  margin: 0; padding: 0; min-width: 0;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); overflow: hidden;
  align-self: start;
}
/* 收合時只有標題列；展開後固定高度，專案再多也只在裡面捲。 */
.system > .proj-list { max-height: 168px; overflow-y: auto; }
.system[open] { align-self: stretch; }
/* 箭頭的點擊範圍要夠大才按得到，所以外面包一層有內距的區塊。 */
.sys-caret-btn {
  flex: 0 0 auto; display: grid; place-items: center;
  width: 20px; height: 20px; margin-left: -4px; border-radius: 4px;
}
.sys-caret-btn:hover { background: var(--line) }
.system[open] .sys-caret { transform: rotate(90deg) }
/*
  勾選框就是勾選框，點得到。

  原本把它設成 pointer-events: none，想讓整條標題列都能勾選；但實際上
  只有名稱那一段真的會勾（見 app.js），點在方框上什麼都不會發生——
  而那正是每個人第一下會點的地方。一個看起來能點、點了沒反應的勾選框，
  比沒有勾選框還糟。
*/
.sys-pick .sys-toggle { cursor: pointer }
/* 名稱是「勾選」的按鈕，其餘區域是「展開收合」，游標各自反映。 */
.sys-pick .sys-name { cursor: pointer; padding: 2px 4px; margin: -2px -4px; border-radius: 3px }
.sys-pick .sys-name:hover { background: var(--line) }
.sys-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
/* 搜尋沒中的整塊藏起來。 */
.system.filtered-out, .proj.filtered-out { display: none }
.system > legend { display: none; }
.sys-pick {
  display: flex; align-items: center; gap: 9px;
  padding: 9px 12px;
  background: var(--bg);
  font-weight: 600; color: var(--text); font-size: 13px;
  cursor: pointer; list-style: none;
}
.sys-pick::-webkit-details-marker { display: none }
.sys-pick:hover { background: var(--hover) }
.system[open] .sys-pick { border-bottom: 1px solid var(--line) }
.sys-pick .sys-n { margin-left: auto; font: 11px var(--mono); color: var(--faint); font-weight: 400 }
/* 這個系統有勾東西時，計數變成墨色，收合狀態下也看得出來選了幾個。 */
.sys-pick .sys-on { color: var(--faint); font-weight: 600 }
.system.has-picks .sys-pick .sys-on { color: var(--text) }
.sys-pick input { margin: 0; }
.proj-list { padding: 7px 12px 9px; }
.proj-list .empty { padding: 8px 0; text-align: left; font-size: 12px; }
.proj { display: flex; align-items: center; gap: 4px; padding: 2px 0; min-width: 0; }
.proj label {
  display: flex; align-items: center; gap: 9px; flex: 1; min-width: 0;
  color: var(--text); font-size: 13px; cursor: pointer;
}
.proj input { margin: 0; flex: 0 0 auto; }
.proj label span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 快速另開 GitLab。放在 label 外面，點它不會連帶切換勾選。 */
.proj-open {
  flex: 0 0 auto; display: grid; place-items: center;
  width: 24px; height: 24px; border-radius: var(--radius);
  color: var(--faint); opacity: .6; text-decoration: none;
}
.proj-open svg { width: 13px; height: 13px; flex: none; }
.proj-open:hover { opacity: 1; background: var(--hover); color: var(--text); }

/*
  結果表的工具列。

  分兩區：「GitLab 欄」與「Jira 欄」，各自標出這一區的按鈕讀哪一欄——八顆按鈕
  排成一片時，最常犯的錯是勾錯欄再按，而畫面上原本沒有東西把按鈕跟欄位連起來。

  區名用一行小字，不用有框的標籤。三列各自以一個方框開頭時，那三個框會比
  它們要說明的內容還搶眼；而外框本身在一個已經有底色的面板裡也是多的。
*/
/* 上下不對稱：頂端只有一行小字（「GitLab 欄」），留 16px 會空出一條看起來像忘了填東西
   的白帶；底部收著一整列按鈕，那裡要留得住。 */
.toolbar {
  padding: 9px 18px 14px; margin-top: 16px;
  background: var(--bg); border-radius: var(--radius);
}
/* 版號是一小串字，輸入框沒有理由吃掉整條列。壓上限之後右邊空出來的地方
   給主要動作，那顆才是這一區真正要按的東西。 */
.toolbar .grow { flex: 1; min-width: 200px; max-width: 320px; }
.tb-row > button.primary { margin-left: auto; }

/* 兩組之間隔一條線就夠了。線的上下各留 14px 的話，中間空出一條白帶，
   看起來像兩張分開的卡片——它們是同一個工具列上的兩組。 */
.tb-group + .tb-group { margin-top: 12px; padding-top: 9px; border-top: 1px solid var(--line); }

/*
  挑選模式的篩選那一排（長在「包版與送測」那張表上面）：一顆狀態下拉、一個搜尋框。

  下拉是篩選器，不是選取器：勾起來只是決定清單列出什麼，這批是哪幾張仍然只認
  每一列自己的勾選框。兩件事分開之後，「這批幾張」這個數字才不會因為換個篩選
  就跳來跳去。

  色點用的色相跟結果表的 .chip 是同一套（見 app.js 的 chipHue），
  同一個 label 在兩個地方是同一個顏色。
*/
.iss-bar {
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-top: 22px;
}
.iss-filter { position: relative; flex: 0 0 auto; }
/* 搜尋框吃掉中間剩下的寬度：不撐開的話，輸入框只有預設的那幾十像素，
   連 placeholder 都寫不完。高度對齊左邊那顆下拉——並排站著，差幾像素
   看起來就是兩種東西。 */
.iss-bar .scope-search-box { flex: 1 1 180px; min-width: 120px; }
.iss-bar .scope-search-box input { width: 100%; padding-block: 5px; }

/* 那顆按鈕：左邊「狀態」兩個字是它問的問題，右邊是現在的答案。
   高度對齊旁邊的搜尋框與那兩顆按鈕——並排站著，差幾像素看起來就是兩種東西。 */
.iss-pick {
  display: inline-flex; align-items: center; gap: 8px; max-width: 320px;
  padding: 6px 10px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card); color: var(--text); font: inherit; font-size: 12px;
  cursor: pointer;
}
.iss-pick:hover { background: var(--hover); }
.iss-pick[aria-expanded="true"] { border-color: var(--accent); }
.iss-pick-cap { font-weight: 600; flex: 0 0 auto; }
/* 答案長了切尾巴，不要把箭頭擠出去——箭頭是「這裡打得開」唯一的線索。 */
.iss-pick-sum {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font: 400 11.5px var(--mono); color: var(--muted);
}
/* 箭頭是「這裡打得開」唯一的線索，所以不要畫得比旁邊的字還淡：
   跟著按鈕的文字同一個顏色，打開時轉過來。 */
.iss-pick-chev {
  flex: 0 0 14px; width: 14px; height: 14px; stroke-width: 2;
  color: var(--muted); transition: transform .15s ease;
}
.iss-pick:hover .iss-pick-chev { color: var(--text); }
.iss-pick[aria-expanded="true"] .iss-pick-chev { transform: rotate(180deg); color: var(--text); }

/* 下拉自己刻（select／datalist 在各家瀏覽器上長得都不一樣，也塞不進核取框與張數）。
   浮在表格上面，不佔位置：佔位置的話，每開一次關一次，底下整張表就跳一次。 */
.iss-menu {
  position: absolute; z-index: 30; top: calc(100% + 4px); left: 0;
  width: 300px; max-width: calc(100vw - 32px);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card); box-shadow: 0 8px 24px rgba(0, 0, 0, .12);
  padding: 8px;
}
:root[data-theme="dark"] .iss-menu { box-shadow: 0 8px 24px rgba(0, 0, 0, .5); }
.iss-menu.hidden { display: none; }
.iss-menu-q { width: 100%; margin-bottom: 6px; }
/* 一個專案十幾種 label 是常態，超過就在框裡捲，不要把整頁撐長。 */
.iss-menu-list { max-height: 264px; overflow-y: auto; }
.iss-menu-sec {
  padding: 8px 6px 4px; font: 600 10.5px var(--mono); color: var(--faint);
  letter-spacing: .04em;
}
.iss-menu-sec:first-child { padding-top: 2px; }
.iss-menu-item {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 6px; border-radius: 6px; cursor: pointer;
  font-size: 12.5px; color: var(--muted);
}
.iss-menu-item:hover { background: var(--hover); }
.iss-menu-item.on { color: var(--text); }
.iss-menu-dot {
  flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%;
  background: hsl(var(--h), 52%, 78%); border: 1px solid hsl(var(--h), 40%, 66%);
}
:root[data-theme="dark"] .iss-menu-dot {
  background: hsl(var(--h), 34%, 34%); border-color: hsl(var(--h), 28%, 46%);
}
.iss-menu-name {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--mono); font-size: 11.5px;
}
.iss-menu-n { flex: 0 0 auto; font: 500 11px var(--mono); color: var(--faint); }
.iss-menu-none { padding: 10px 6px; font-size: 12px; color: var(--faint); }
.iss-menu-foot {
  display: flex; align-items: center; gap: 8px;
  margin-top: 6px; padding-top: 8px; border-top: 1px solid var(--line);
}
.iss-menu-foot .small { flex: 1; }
.iss-menu-foot button[disabled] { opacity: .45; cursor: default; }

/* 「還有 N 張沒列」。不縮也不截斷：左邊那些長了會切尾巴，而這一句是這一排上
   唯一說得出「清單還有東西沒列」的地方，切掉就沒有人講了。 */
.isf-more { flex: 0 0 auto; white-space: nowrap; font: 400 11.5px var(--mono); color: var(--muted); }
.iss-bar .small { white-space: nowrap; }

/*
  篩掉的列與整組被篩光的專案都不畫。

  篩選只決定看得到什麼，勾選一概不動——被篩掉卻仍然勾著的那幾張還是這批的一員，
  搜尋列右邊會寫出來。少了那一句，按下去的動作會處理到畫面上根本看不到的票。
*/
#result-groups tbody tr.filtered-out { display: none; }
.res-proj.filtered-out { display: none; }

@media (max-width: 720px) {
  /* 窄畫面上先讓標題活下來：label 與 GitLab 連結是輔助資訊。 */
  .iss-chips { display: none; }
}

/*
  重排表格用的那兩顆：不送出任何東西，所以不放在工具列裡，也不放進篩選的框裡
  ——它們不篩東西。

  跟篩選那一條並排、靠右站著：貼著它們真正作用的那張表，右邊界也只剩一條。
  混在工具列裡的話，它會變成「Jira 欄」那一組的第二排，而那一組每一顆按下去
  都會寫進 GitLab 或 Jira。

  線跟按鈕包在一起，理由同 .tb-tail：分開排隊的話，窄畫面換行時線會留在上一行的尾巴。
*/
.ss-tail { display: inline-flex; align-items: center; gap: 8px; margin-left: auto; }
/* 框是有邊框的，按鈕沒有——差一條線的高度，對齊要補回來。 */
.iss-bar .ss-tail { margin-top: 1px; }

/* 被篩掉卻仍然勾著的那幾張自己一行，貼著上面那一條。 */
.iss-hidden { margin: 8px 0 0; }

/*
  選擇器要寫成 h3.tb-tag，不能只寫 .tb-tag：卡片裡的 h3 有一條 `.card h3`
  （見上面），它的權重比單一個 class 高，會把這裡的 margin 蓋掉——標題上方就多出
  一條 20px 的白帶，兩組各一條，看起來像兩個空白行。
*/
h3.tb-tag {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 0 0 9px; font-size: 13.5px; font-weight: 700; color: var(--text);
}
.tb-tag span { font-weight: 400; font-size: 11.5px; color: var(--faint); }

/*
  標題前面那個勾選框：這兩組講的就是底下那張表最左邊兩欄的勾選框
  （見 app.js 的 headCheck），畫一個一模一樣的記號比多寫一句話快。

  純裝飾，點不動——真的要整欄勾選的那顆在表頭上。
*/
.tb-cb {
  flex: 0 0 auto; width: 13px; height: 13px; border-radius: 3px;
  border: 1px solid var(--faint); background: var(--card);
  position: relative; top: 1px;
}
.tb-cb::after {
  content: ''; position: absolute; left: 3px; top: 1px;
  width: 4px; height: 7px; border: solid var(--faint);
  border-width: 0 1.5px 1.5px 0; transform: rotate(45deg);
}

.tb-row { display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; }

/*
  「這顆為什麼按不動」那一句，貼在它說明的那顆按鈕底下。

  不用 .offline-note 那條左邊線：那一句講的是整台主機少了什麼（連不到 Jira），
  份量本來就比較重；這裡少的只是一項還沒填的設定，還沒填不是壞掉。
  也不放在同一列——那一列已經站著一顆按鈕，一句話擠進去會把它推到換行。
*/
.tb-off { margin: 7px 0 0; font-size: 12px; color: var(--muted); }

/* 「前往 Jira」跟前面那幾顆讀同一欄，但它不推動任何東西，只是開一個分頁去看
   ——隔一條線，並靠到這一列的右邊。

   線跟按鈕包在一起（.tb-tail），不是兩個各自排隊的元素：分開的話窄畫面換行時，
   線會留在上一行的尾巴，變成一條沒有作用的懸空線。 */
.tb-tail { display: inline-flex; align-items: center; gap: 10px; margin-left: auto; }
.tb-div { flex: 0 0 auto; width: 1px; height: 22px; background: var(--line); }

/*
  各專案版號。

  這一區是一張小表格，不是四個各自漂浮的輸入框：一列一個專案，四欄固定
  ——系統、專案、會壓到幾筆、版號。欄位對齊之後，「哪一格是哪個專案的」
  用看的就知道，不必把眼睛從最左邊拉到最右邊。

  外框跟底色是為了把它跟工具列上其他按鈕分開：那些是「按下去會發生事情」，
  這裡是「先把要壓的東西填好」。
*/
.lv-list {
  margin-top: 10px;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card); overflow: hidden;
}
.lv-bar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 6px 12px; border-bottom: 1px solid var(--line);
}
.lv-list.is-folded .lv-bar { border-bottom: 0; }
/* 「全部壓上」靠到標題列的右端：這一列與底下每一列的右內緣都是 12px，
   所以它的右邊界自己就對到那一欄版號上。

   縮到跟版號輸入框同高（連著 .lv-bar 收掉的那 2px 內距一起看）——整顆按鈕的預設尺寸
   會讓標題列比底下每一列高出一截，一張小表格的頭比身體重。 */
/* 「重抓」與「全部壓上」是一組，一起靠到標題列的右端。

   「重抓」問的是壓之前該先知道的事（GitLab 上已經有哪幾張掛著這些版號了），
   所以要跟那顆在同一處。靠右由這個外框做，不是各自 margin-left:auto——
   那樣窄畫面折行時兩顆會分家，一顆留在上一行右端、一顆掉到下一行左端。

   兩顆都縮到跟版號輸入框同高（連著 .lv-bar 收掉的那 2px 內距一起看）——整顆按鈕的
   預設尺寸會讓標題列比底下每一列高出一截，一張小表格的頭比身體重。 */
.lv-acts { display: flex; align-items: center; gap: 10px; margin-left: auto; }
.lv-bar .primary { padding: 4px 13px; font-size: 12.5px; }
.lv-bar #btn-rescan { padding: 4px 11px; font-size: 12.5px; }
.lv-head {
  display: inline-flex; align-items: center; gap: 8px;
  font: inherit; font-size: 12px; font-weight: 600; color: var(--text);
  background: none; border: 0; border-radius: 6px; padding: 2px 6px; margin-left: -6px;
}
.lv-head:hover:not(:disabled) { background: var(--hover); }
.lv-progress { font: 400 11.5px var(--mono); color: var(--faint); }
/* 全部填完了就把數字點亮。這是按「全部壓上」之前唯一要確認的事。 */
.lv-progress.is-done { color: var(--text); }
/* inline 元素吃不到 transform，箭頭轉不動。 */
.lv-chev { display: inline-block; font-size: 10px; color: var(--faint); transition: transform .15s; }
.lv-list.is-folded .lv-chev { transform: rotate(-90deg); }
.lv-list.is-folded .lv-rows { display: none; }

/* 一列一個專案。名稱那一欄自由伸縮，其餘三欄固定寬度——
   專案名稱長短差很多（backend 到 dms-backend-service），塞進固定寬度就得切字，
   而切掉的正好是分辨兩個專案的那幾個字。 */
.lv-rows { padding: 2px 0; }
.lv-row {
  display: flex; align-items: center; gap: 10px;
  margin: 0; padding: 7px 12px; cursor: pointer;
}
.lv-row + .lv-row { border-top: 1px solid var(--line); }
.lv-row:hover { background: var(--hover); }
.lv-name {
  flex: 1; min-width: 0; font-size: 13px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* 系統標籤給一個最小寬度，短名字（DMS、IoT、PLS）才會把專案名稱對到同一條線上。 */
.lv-row .res-sys { min-width: 42px; text-align: center; }
/* 沒登錄過的專案沒有系統，位子留著但不畫東西——不然那一列的名稱會自己跑到最左邊。 */
.lv-row .res-sys.is-blank { background: none; border-color: transparent; }
/* 沒登錄過的專案：名字是路徑最後一段，前面那幾層縮成後面的小字。 */
.lv-path { margin-left: 6px; font: 400 11px var(--mono); font-style: normal; color: var(--faint); }
/* 這個版號會壓到幾筆。一筆都沒勾的那一列連版號都不必填，整列淡掉。 */
.lv-n { flex: 0 0 auto; font: 400 11.5px var(--mono); color: var(--faint); }
.lv-row.is-empty { color: var(--faint); }
.lv-row.is-empty .lv-name { color: var(--faint); }
input.lv-input {
  flex: 0 0 auto; width: 132px; margin-top: 0; padding: 5px 10px;
  font: 600 12.5px var(--mono); text-align: center;
  background: var(--bg);
}
input.lv-input:focus { background: var(--card); }
/* 從「全部壓上」跳過來時閃一下還沒填的那一列：每一格長得一樣，
   游標停在哪一格用看的要看兩眼。 */
@keyframes lv-hint {
  from { background: var(--hover); }
  to { background: transparent; }
}
.lv-row.is-hinted { animation: lv-hint 1.4s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .lv-row.is-hinted { animation: none; }
}
/* 提示字要一眼看得出是提示：跟填進去的版號同一個字體、同一個位置，
   只差在深淺與粗細——不然四格空的看起來像四格都填好了。 */
input.lv-input::placeholder { font-weight: 400; color: var(--faint); opacity: .75; }
/* 填好的那格站出來一點：掃過去就知道還有哪幾格是空的。 */
.lv-row.is-filled input.lv-input { background: var(--card); border-color: var(--faint); color: var(--text); }

@media (max-width: 560px) {
  /* 窄畫面上「幾筆」讓位，名稱與輸入框才有地方。 */
  .lv-n { display: none; }
  input.lv-input { width: 108px; }
}

/*
  系統標籤。放在專案名稱前面，取代原本的「系統 / 專案」。

  斜線在這個站台上到處都是 GitLab 的路徑分隔（BackendGroup/test/gitlab-ai-test），
  拿同一個符號去接系統與專案，讀起來就像同一條路徑上的兩段——而它們是兩種
  完全不同的東西。兩邊名字剛好相近時更看不出哪一半是系統。
*/
.res-sys {
  display: inline-block; flex: 0 0 auto;
  font-size: 11.5px; font-weight: 600; line-height: 1.5; color: var(--muted);
  background: var(--bg); border: 1px solid var(--line); border-radius: 5px;
  padding: 1px 7px; white-space: nowrap;
}

/* 結果欄裡那一格「Release Version 填了沒」（見 app.js 的 resultCell）。 */
.res-ver { font-size: 12.5px; color: var(--muted); white-space: nowrap; }

/* 專案卡標題列上的版號。填得了，跟上面那份清單是同一個值。
   「壓上」就在旁邊，只想壓這一個專案的人不必捲回去找那一列。

   沒填的時候要看得出來——留一片空白的話，「刻意跳過」與「忘了填」長得一樣，
   而按下「全部壓上」時那個專案會被安靜略過。 */
.res-verbox { display: inline-flex; align-items: center; gap: 7px; }
.res-ver-lab { font-size: 12.5px; color: var(--muted); white-space: nowrap; }
.res-verbox.is-none .res-ver-lab { color: var(--warn-ink); }
input.res-ver-input {
  flex: 0 0 auto; width: 118px; margin-top: 0; padding: 4px 10px;
  font: 600 12.5px var(--mono); text-align: center;
  background: var(--card); border-color: var(--faint); color: var(--text);
}
/* 空的那一格退到背景色：跟填好的那幾張卡並排時，一眼看得出少了誰。 */
.res-verbox.is-none input.res-ver-input { background: var(--bg); border-color: var(--line); }
input.res-ver-input::placeholder { font-weight: 400; color: var(--faint); opacity: .75; }

@media (max-width: 560px) {
  /* 窄畫面上「本次版號」四個字讓位給輸入框與「壓上」。 */
  .res-ver-lab { display: none; }
  input.res-ver-input { width: 96px; }
}
.count { font-variant-numeric: tabular-nums; font-family: var(--mono); font-size: 11.5px; opacity: .7; }
.stats { font: 11.5px var(--mono); color: var(--faint); }

/* 找不到的 key：等寬字排成一排，不做彩色藥丸。 */
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.chips span {
  background: none; color: var(--muted);
  border: 1px solid var(--line);
  padding: 2px 8px; border-radius: 2px; font: 11.5px var(--mono);
}

/*
  第三步的說明句兩個模式各一句，一次只出現一個。

  挑選模式那一句先講「都幫你勾好了」：上一步才剛挑完，這一步又是一份長得像的
  清單，不講的話走過來會以為要再挑一次。貼上模式沒有上一步，講的就只是哪一欄
  餵哪一邊。
*/
#result-card[data-mode="pick"] .only-paste,
#result-card[data-mode="paste"] .only-pick { display: none; }

/* ============ 批次結案 ============ */

/* 三個入口的切換。做成一整條分段控制，不是三顆按鈕：
   它要看得出「一次只會有一個」，而不是「這裡有三件事可以做」。

   不設 max-width：三格的字長短差很多（「這裡發過的版本」對上「直接挑 issue」），
   壓在同一個寬度裡會把長的那格擠到換行。改成各自照內容寬，整條靠左收邊。 */
.seg { display: flex; margin: 18px 0 4px; width: fit-content; max-width: 100%; }
.seg label { margin: 0; }
.seg input { position: absolute; opacity: 0; width: 0; height: 0; }
.seg span {
  display: block; text-align: center; cursor: pointer;
  padding: 7px 10px; font-size: 13px; color: var(--muted);
  border: 1px solid var(--line); border-left: none; background: var(--bg);
}
.seg label:first-child span { border-left: 1px solid var(--line); border-radius: var(--radius) 0 0 var(--radius); }
.seg label:last-child span { border-radius: 0 var(--radius) var(--radius) 0; }
.seg span:hover { background: var(--hover); color: var(--text); }
.seg input:checked + span { background: var(--card); color: var(--text); font-weight: 600; box-shadow: inset 0 -2px 0 var(--accent); }
.seg input:disabled + span { cursor: not-allowed; opacity: .55; }
.seg input:focus-visible + span { outline: 1px solid var(--accent); outline-offset: 2px; }
/* 同一條分段控制，換成連結的版本：切的是「看哪一份」，每一份都有自己的網址。 */
.seg-links a {
  display: block; text-align: center; padding: 7px 10px; font-size: 13px;
  color: var(--muted); text-decoration: none;
  border: 1px solid var(--line); border-left: none; background: var(--bg);
}
.seg-links a:first-child { border-left: 1px solid var(--line); border-radius: var(--radius) 0 0 var(--radius); }
.seg-links a:last-child { border-radius: 0 var(--radius) var(--radius) 0; }
.seg-links a:hover { background: var(--hover); color: var(--text); }
.seg-links a.on { background: var(--card); color: var(--text); font-weight: 600; box-shadow: inset 0 -2px 0 var(--accent); }

/* 窄畫面上三格只差十幾 px 就擺不下，而一擺不下是三格一起折成兩行。
   縮一號字與左右內距換得回那一行：這條列是切換用的，比正文小一號不影響讀。 */
@media (max-width: 640px) {
  .seg span { font-size: 12px; padding: 7px 7px; }
}

.cl-mode { margin-top: 14px; }
/* 一張票都沒有時只留那句話：空表頭配一顆「結案 0」看起來像按了沒反應。 */
#cl-result.is-empty .toolbar, #cl-result.is-empty .table-scroll { display: none; }

/* 版號欄要自己撐開。.grow 的寬度規則掛在 .toolbar 底下，而這一列不在裡面，
   不補的話整欄會縮成內容寬度，placeholder 直接被切一半。 */
.cl-mode .grow { flex: 1; min-width: 220px; max-width: 420px; }
#cl-label { margin-top: 0; }

/* 用版號挑 issue 是兩件事：先勾範圍，再填版號。中間要留得出一條界線，
   不然「全選」那一列會貼在「版號 label」的頭上，看起來像同一組欄位。 */
.cl-mode[data-mode="label"] .tb-row { margin-top: 16px; }

/* 直接挑 issue。一列一個：代號在前（那是人平常在講的東西），名字跟在後面。 */
.cl-proj code { font-size: 12px; color: var(--faint); flex: 0 0 auto; }
.cl-proj:has(input:checked) code { color: var(--text); font-weight: 600; }
.cl-proj input { flex: 0 0 auto; margin: 0; }
.cl-proj .pick-name { min-width: 0; overflow-wrap: anywhere; }
#cl-projs { margin-top: 10px; }
/* 讀不到清單那一格：一句原因加一顆「重試」，兩個擺成一行。 */
#cl-projs .empty { display: flex; align-items: center; justify-content: center; gap: 10px; }

/*
  verified 以外的那幾站。

  收起來是刻意的：大部分時候要收的就是 verified，而這裡多勾一個，
  按下結案時就會有票被一路推過驗收。它要留著可用，但不該跟專案清單一樣顯眼。
*/
.cl-extra { margin-top: 14px; }
.cl-extra > summary {
  cursor: pointer; font-size: 12.5px; color: var(--muted);
  /* 換成自己的三角形，各家瀏覽器預設的那顆大小與間距都不一樣。 */
  list-style: none; display: flex; align-items: center; gap: 7px;
}
.cl-extra > summary::-webkit-details-marker { display: none; }
.cl-extra > summary::before {
  content: ''; width: 0; height: 0;
  border: 4px solid transparent; border-left-color: var(--faint);
  transition: transform .12s;
}
.cl-extra[open] > summary::before { transform: rotate(90deg) translateX(1px); }
.cl-extra > summary:hover { color: var(--text); }
.cl-extra .hint { margin: 10px 0 12px; }
.cl-extra-list { display: flex; gap: 20px; flex-wrap: wrap; }
.cl-extra-list label.check { display: flex; align-items: center; gap: 7px; margin: 0; }
.cl-extra-list label.check input { width: auto; margin: 0; }
/* 站名在前、要走幾步跟在後面：那是決定要不要勾它的唯一根據。 */
.cl-extra-list .muted { margin-left: 6px; }

/*
  還沒做完的那幾站（open、TO DO、Doing）。

  跟上面那幾站之間畫一條線：那幾站是「做完了，只差推到終點」，這幾站是
  「這件事不做了」。並排成一整排勾選框的話，兩件事看起來會像同一件。
*/
.cl-unfin { margin-top: 14px; padding-top: 12px; border-top: 1px dashed var(--line); }
.cl-unfin .hint { margin: 0 0 12px; }

/*
  一次指定理由。放在工具列上、跟「全選」那幾顆同一排，所以不吃 .opt-pick
  預設的那 6px 上距——那是給表單欄位用的。
*/
.cl-bulk { margin-top: 0; min-width: 0; }
.cl-bulk > summary { padding: 7px 11px; font-size: 13px; }

/*
  表格裡那一列自己的理由清單。

  比表單上的窄一點、字小一點：它擠在「結案」那一欄裡，跟旁邊的徽章、
  說明文字排在一起，照原尺寸會把整列撐高一倍。
*/
.cl-reason { margin-top: 5px; min-width: 132px; max-width: 200px; }
.cl-reason > summary { padding: 4px 8px; font-size: 12.5px; }
.cl-reason .opt-menu { font-size: 12.5px; }
/* 打開時貼著視窗定位（見 app.js 的 reasonPick）：表格是捲動容器，
   留在裡面的話往上、往下展都會被切掉。位置由 JS 照按鈕算。
   z-index 壓在對話框（50）與示範列（55）底下：它是表格裡的東西，不該蓋過那兩個。 */
.cl-reason .opt-menu.float { position: fixed; z-index: 40; }

/*
  路徑上「推出來的」那幾步（見 app.js 的 planCell）。

  比第一步淡一階：第一步是問 Jira 問來的，後面幾步是照正規流程推的，
  而那幾步真的會不成立（Bug 的 release 沒有 bypass）。印成同一種字，
  看的人會以為整條路都確認過了。
*/
.cl-guessed { color: var(--faint); }

/* Release 紀錄裡這裡發過的版本。一列一次，版號在左、範圍在中、筆數與時間靠右。 */
.rel-pick {
  width: 100%; text-align: left; background: none; border-radius: 0;
  border: 0; border-bottom: 1px solid var(--line); color: inherit;
}
.rel-pick:last-of-type { border-bottom: 0; }
.rel-pick:hover { background: var(--hover); }
/* 勾起來的那幾列要一眼看得出來——按下結案之前的最後一次確認就是它。 */
.rel-pick:has(input:checked) { background: var(--card); box-shadow: inset 2px 0 0 var(--accent); }
.rel-pick:has(input:checked) .pick-name code { color: var(--text); font-weight: 600; }
.rel-pick input { flex: 0 0 auto; margin: 0; }
.rel-pick .rel-when { font: 12px var(--mono); color: var(--faint); flex: 0 0 auto; }
/* 一個專案配一個版號。專案名在上、版號在下，兩個黏在一起才看得出是一組。 */
.rel-part {
  display: inline-flex; flex-direction: column; gap: 1px;
  padding: 2px 8px; border: 1px solid var(--line); border-radius: 4px; background: var(--bg);
}
.rel-part em { font-style: normal; font-size: 10.5px; color: var(--faint); line-height: 1.3; white-space: nowrap; }
.rel-part code { font-size: 12px; line-height: 1.4; white-space: nowrap; }
.rel-pick .pick-name { display: flex; gap: 6px; flex-wrap: wrap; }
.rel-pick.is-busy { opacity: .55; }

/* 已選幾次 + 清空，貼在清單底下。 */
.pick-foot { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 10px; }

/* 專案卡標題列上的「整個專案」勾選。 */
/* 專案卡上的正式版本。它是這一批的身分，比第四碼的流水號重要。
   做成有框的標籤：跟它左右兩邊的專案名與張數都是文字，不框起來三段會黏成一坨。 */
.grp-family {
  font: 12px var(--mono); color: var(--text); white-space: nowrap;
  background: var(--card); border: 1px solid var(--line); border-radius: 4px;
  padding: 1px 7px;
}
.grp-family:empty { display: none; }

/* 張數前面補一個分隔點，跟版號分得開。 */
.grp-count::before { content: "·"; margin-right: 7px; color: var(--faint); }

/* 工具列跟第一張專案卡之間要留白：那是兩件事（挑哪幾張 / 這一批是誰的），
   貼在一起會讀成同一塊。 */
#cl-rows { margin-top: 20px; }
#cl-swept, #cl-errs { margin-top: 16px; }

/* 整組勾選就在勾選欄的正上方，不必再寫「整個專案」。 */
.grp-all { display: block; width: auto; margin: 0 auto; }
/* 整列可點，游標要說得出這件事。 */
#cl-rows tbody tr { cursor: pointer; }
#cl-rows .res-proj { margin-bottom: 12px; }
#cl-rows .res-proj:last-child { margin-bottom: 0; }
/* 表格裡的版號，一列可能不只一個。 */
.cl-ver { display: inline-block; margin-right: 5px; white-space: nowrap; }
.rel-pick .pick-name { display: flex; gap: 6px; flex-wrap: wrap; }
.rel-pick code { font-size: 12px; }
.rel-pick .rel-scope { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#cl-picks { max-height: 260px; }

/* 「還要走哪幾站」那行小字，接在標記底下自己一行。 */
.cl-note { display: block; margin-top: 3px; }

/* 確認框裡逐張列出還沒到 verified 的票。 */
.cl-warn-list { list-style: none; margin: 10px 0 0; padding: 0; }
.cl-warn-list li {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  padding: 6px 0; border-top: 1px solid var(--line); font-size: 12.5px;
}
.cl-warn-list li span { color: var(--muted); }

#cl-result .grid td { vertical-align: top; }
#cl-rows .title { min-width: 220px; }
/* 狀態名不要被拆成兩行（In Progress 這種帶空白的最容易中）。 */
#cl-rows code { white-space: nowrap; }

/* ============ 狀態流程 ============ */

/*
  那張圖是一整個 SVG，不是排出來的版面。

  三層（誰的事 / 走到哪 / 工具管哪一段）共用同一組座標，座標算在 Go 那邊
  （見 handlers_flow.go）——先前三層各自是一列 flex，而內距是實打實的寬度、
  flex-basis 卻是 0，只涵蓋一站的那一段就比別人寬，整排看起來是歪的。
  同一組座標之後，三層是真的對齊，不是「看起來差不多」。

  風格是素描：每一筆都微微彎，圓描兩次，圓裡用斜線影代替填滿。線條的座標算在
  Go 那邊（見 sketch.go）——先前是拿 SVG 濾鏡把整張圖推歪，畫出來是「印壞了」
  不是「手畫的」：長線被推成波浪、直角被推圓、邊緣還是糊的。

  影線是整套介面唯一的紋理（開頭把重複紋理列為刻意不做的事），例外的理由是
  這一頁不操作任何東西，它就是一張說明用的圖；而且仍然沒有顏色，只有墨色深淺
  ——工具碰不到的前四站淡一階、圓點留白。
*/
.flow-fig { margin-top: 20px; overflow-x: auto; }
.flow-svg {
  /* 全域的 svg 規則把每個 svg 釘成 18px，這一張要自己撐開。 */
  display: block; flex: none; width: 100%; height: auto;
  /* 九站排開至少要這麼寬才不會擠成一團；窄畫面靠外層捲動。
     上限就是 viewBox 的原寬，再放大只會讓字比設計的級數大。 */
  min-width: 700px; max-width: 900px; margin: 0 auto;
  color: var(--text);
  /* 手畫的筆觸收筆是圓的，接角也是圓的。方頭會讓那些微彎的線看起來是折的。 */
  stroke-linecap: round; stroke-linejoin: round;
}

/* 筆觸。fill 由全域規則關掉了，這裡只調粗細與深淺。 */
.fl-rail { stroke-width: 1.4; }
/* 描過去的第二筆：淡到只剩「這一筆畫了兩次」的痕跡，看得出來就太多了。 */
.fl-ghost { stroke-width: 1.1; opacity: .3; }
.fl-bkt { stroke-width: 1.15; color: var(--muted); }
.fl-cut { stroke-width: 1; stroke-dasharray: 2.5 6; color: var(--faint); }
.fl-i { stroke-width: 1.6; color: var(--muted); }

/* 站。底層是紙色，把那條線在圓裡切斷；圈起來的那一筆描兩次才像手畫的。 */
.fl-node-bg { fill: var(--card); stroke: none; }
/* 工具管得到的那五站打斜線影，其餘留白。 */
.fl-hatch { stroke-width: .9; opacity: .8; }
.fl-node-ring { stroke-width: 1.5; }
.fl-node-ring2 { stroke-width: 1.1; opacity: .3; }

.fl-ink .out, .fl-icons .out, .fl-node.out { color: var(--faint); }

/* 字不進濾鏡：字歪掉就不是手繪感，是印壞了。 */
.flow-svg text { text-anchor: middle; stroke: none; }
.fl-stage { font-size: 13.5px; font-weight: 600; fill: var(--text); }
.fl-stage.out { fill: var(--muted); }
.fl-who { font: 600 10.5px var(--mono); letter-spacing: .06em; fill: var(--faint); }
.fl-stop { font: 11.5px var(--mono); fill: var(--text); }
.fl-stop.out { fill: var(--faint); }
.fl-scope { font-size: 12px; fill: var(--muted); }
.fl-scope.out { fill: var(--faint); }

/* 表格那一欄裡的圖示。 */
.flow-ico { width: 15px; height: 15px; flex: none; color: var(--muted); }

/* 表格：工具碰不到的那兩段整列淡一階，跟圖上的空心圓點是同一件事。 */
.flow-table td { vertical-align: top; }
.flow-table tr.out td { color: var(--muted); }
.flow-cell { display: inline-flex; align-items: center; gap: 7px; }
/* 狀態那一欄是標籤（.chip）。整列淡下去時標籤也要跟著退，
   不然工具碰不到的那兩段反而是整張表上最跳的東西。 */
.flow-table tr.out .chip { opacity: .55; }
.flow-who { font-style: normal; font: 600 11.5px var(--mono); color: var(--muted); }

.flow-notes { margin: 16px 0 0; padding: 14px 0 0; border-top: 1px solid var(--line); list-style: none; }
.flow-notes li { font-size: 13px; color: var(--muted); line-height: 1.65; }
.flow-notes li + li { margin-top: 8px; }
.flow-notes strong { color: var(--text); font-weight: 600; }

/* ============ 登入 ============ */

/*
  左右分割：左邊講「這是什麼」，右邊才是表單。
  這是整個站唯一放大字級、唯一出現襯線斜體的地方——登入頁是門面，
  進到 App 之後就要安靜下來，兩邊的語氣不必一致。
*/
/*
  100svh 而不是 100vh：手機瀏覽器的 100vh 是「網址列收起來之後」的高度，
  網址列還在的時候畫面就比 100vh 矮，於是明明沒東西可看也能上下捲一段。
  svh 取的是最小可視高度，內容塞得下就一定捲不動；塞不下（小螢幕、鍵盤跳出來）
  才會捲，那是應該的。
*/
.auth { display: grid; grid-template-columns: 1fr 1fr; min-height: 100vh; min-height: 100svh; }

.auth-hero {
  position: relative; overflow: hidden;
  background: var(--bg); border-right: 1px solid var(--line);
  display: flex; align-items: center; padding: 60px;
}
/*
  背景：一道斜線把畫面切成兩個明度，像一張對折過的紙。

  刻意不用任何重複紋理（格線、點陣、斜線）——那些都是「加東西上去」，
  這個是「把既有的平面折一下」。零圖案，只有一條邊界和兩個明度差，
  所以永遠不會跟前景的字打架，但畫面不再是死平的一塊。

  107deg 是刻意讓它跟標題的視覺重心錯開，不要正好切在文字上。
*/
.auth-hero::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(107deg, transparent 0 46%, rgba(26, 24, 21, .035) 46% 100%);
}
/* 折線本身：只有 1px，是這片背景唯一的「線」。 */
.auth-hero::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(107deg,
    transparent 0 46%, var(--line) 46% calc(46% + 1px), transparent calc(46% + 1px));
}
:root[data-theme="dark"] .auth-hero::before {
  background: linear-gradient(107deg, transparent 0 46%, rgba(255, 253, 245, .028) 46% 100%);
}
/*
  左右兩欄的內容都置中。
  左欄原本是 margin-left:auto 貼著中線，右欄卻是置中——兩邊規則不一樣，
  結果左邊空一大片、右邊只剩一指寬（349px 對 61px），整張畫面看起來是斜的。
*/
.auth-hero-inner { position: relative; z-index: 1; width: 100%; max-width: 430px; margin: 0 auto; }

.hero-eyebrow { margin: 0 0 24px; color: var(--faint); }

.hero-title {
  margin: 0;
  font-size: clamp(38px, 4vw, 52px); line-height: 1.08;
  font-weight: 700; letter-spacing: -.035em;
}
/* 襯線斜體只給這一個字：出版感的來源，多用就變成花俏。 */
.hero-title em {
  font-family: Georgia, "Times New Roman", "Songti TC", serif;
  font-style: italic; font-weight: 400; letter-spacing: -.02em;
}

.hero-lede { margin: 26px 0 0; color: var(--muted); font-size: 13.5px; line-height: 2; }

/*
  產品縮影卡片。整頁的視覺主角，靠的是這一組大範圍的柔陰影把它從背景抬起來。
  右邊的 Google 按鈕也有陰影，但那是貼邊的 1px，只用來讓按鈕看起來按得下去，
  不會抬起任何東西——兩者不搶。
*/
.hero-card {
  margin-top: 40px; max-width: 400px;
  background: var(--card); border: 1px solid var(--line);
  border-radius: 7px; box-shadow: 0 14px 34px rgba(26, 24, 21, .07);
  overflow: hidden;
}
.hero-card-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
  padding: 14px 16px; border-bottom: 1px solid var(--line);
}
.hero-card-head strong { display: block; font: 600 13px var(--mono); }
.hero-card-meta { display: block; margin-top: 4px; font-size: 11.5px; color: var(--faint); }
.hero-card-done { font-size: 11.5px; font-weight: 600; color: var(--ok-ink); white-space: nowrap; }
/* 還沒發生的事用等待色，不要用綠色講「完成」。 */
.hero-card-done.is-wait { color: var(--warn-ink); }

.hero-rows { list-style: none; margin: 0; padding: 5px 0; }
.hero-rows li { display: flex; align-items: baseline; gap: 10px; padding: 7px 16px; font-size: 12.5px; }
.hero-rows code { flex: 0 0 auto; font-size: 11.5px; color: var(--muted); background: none; padding: 0; }
.hero-rows span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hero-rows em { flex: 0 0 auto; font: 11px var(--mono); font-style: normal; color: var(--faint); }

.hero-card-foot {
  display: flex; align-items: center; gap: 5px;
  padding: 11px 16px; border-top: 1px solid var(--line);
}
.hero-card-label { color: var(--faint); margin-right: 5px; font-size: 9.5px; }
.hero-bar { height: 3px; width: 32px; border-radius: 2px; background: var(--line); }
.hero-bar.on { background: var(--accent); }
.hero-count { margin-left: auto; font: 11px var(--mono); color: var(--faint); }

.hero-foot { margin: 36px 0 0; font-size: 12px; color: var(--faint); }
.hero-foot em { font-family: Georgia, "Times New Roman", serif; }

.auth-form { display: flex; align-items: center; padding: 60px; background: var(--card); }
.auth-form-inner { width: 100%; max-width: 320px; margin: 0 auto; }
.auth-brand { margin-bottom: 48px; }
.auth-brand .brand-mark { width: 24px; height: 24px; font-size: 10px; }
.auth-brand .lbl { font-size: 14px; }
.auth-title { margin: 0; font-size: 27px; font-weight: 650; letter-spacing: -.03em; }
.auth-sub { margin: 7px 0 32px; font-size: 13px; color: var(--muted); }
.auth-form label { margin-bottom: 16px; }
.auth-form button.primary { width: 100%; justify-content: center; padding: 10px; margin-top: 8px; }
.auth-note { margin: 24px 0 0; font-size: 12px; line-height: 1.9; color: var(--faint); }

/*
  「你的帳號是這一個」。這串字要貼給管理員，所以給它一個獨立的框，
  一眼就知道哪一段是要複製的東西。
*/
.auth-ident {
  display: flex; align-items: baseline; gap: 10px;
  margin-top: 26px; padding: 11px 13px;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg);
}
.auth-ident-label {
  font-size: 10px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--faint); font-weight: 600; white-space: nowrap;
}
.auth-ident code {
  font: 13px var(--mono); color: var(--text); background: none; padding: 0;
  overflow: hidden; text-overflow: ellipsis;
}

/* 沒有表單的那幾頁，動作按鈕自己排一列，主要動作佔滿寬度。 */
.auth-acts { display: flex; flex-direction: column; gap: 8px; margin-top: 22px; }
.auth-acts .btn, .auth-acts button { width: 100%; justify-content: center; padding: 10px; }

/*
  Google 登入按鈕。

  Google 不允許把那個 G 重新上色，所以四個顏色是固定的，能調的只有底。
  兩個主題各挑一種，理由不一樣：

  淺色用純白。這一區的底本來就是 --card（暖白），按鈕再用 --card 就只剩一條邊
  在撐，看起來不像一顆按鈕；純白會浮起來，而且正好是 Google 的標準淺色樣板。

  深色用 --card。純白按鈕在深色頁面上會是唯一一塊高亮，刺眼，而且會把視線
  從左邊那張產品卡整個搶走；暖深底配彩色 G 讀得清楚，也留在這套配色裡面。

  兩邊都不套 .primary 的實心墨底：那個底在深色模式會翻成亮色，於是 G 沒有
  任何一種上色方式在兩個主題都成立（單色 G 是規範明文禁止的）。

  顏色寫成這一層的區域變數，深色模式只要把四個值重新指過去就好。
*/
.g-btn {
  --g-bg: #fff;
  --g-bg-hover: #f7f6f4;
  --g-line: rgba(26, 24, 21, .22);
  /* 貼邊的 1px，不是卡片那組會把東西整塊抬起來的 --shadow。 */
  --g-shadow: 0 1px 2px rgba(26, 24, 21, .1);

  font-size: 14px; font-weight: 600;
  background: var(--g-bg); border-color: var(--g-line);
  color: var(--text); box-shadow: var(--g-shadow);
}
.g-btn:hover { background: var(--g-bg-hover); }

:root[data-theme="dark"] .g-btn {
  --g-bg: var(--card);
  --g-bg-hover: var(--hover);
  --g-line: var(--muted);
  --g-shadow: none;
}

/*
  padding 帶上父層一起寫，是因為上面那條 .auth-acts .btn 已經定了 10px，
  而它有兩個 class——只寫 .g-btn 的話比它少一個，12px 會被吃掉，
  而且這種對消不會有任何徵兆，畫面只是「看起來有點擠」。
*/
.auth-acts .g-btn { padding: 12px; }
/*
  全域的 svg 規則把 fill 關掉、改用 currentColor 描邊，那是給線性圖示的。
  G 是實心填色的圖形，所以這裡整組推翻回來。
*/
.g-mark { flex: 0 0 18px; width: 18px; height: 18px; stroke: none; }
.g-mark .g-b { fill: #4285f4; }
.g-mark .g-g { fill: #34a853; }
.g-mark .g-y { fill: #fbbc05; }
.g-mark .g-r { fill: #ea4335; }

/*
  密碼登入的折疊區。只有超級管理者用得到，所以收起來，
  但要看得出它是一個可以點開的東西，不是一行說明文字。
*/
.auth-alt { margin-top: 26px; padding-top: 18px; border-top: 1px solid var(--line); }
.auth-alt > summary {
  cursor: pointer; font-size: 12.5px; color: var(--muted);
  /* 換成自己的三角形，各家瀏覽器預設的那顆大小與間距都不一樣。 */
  list-style: none; display: flex; align-items: center; gap: 7px;
}
.auth-alt > summary::-webkit-details-marker { display: none; }
.auth-alt > summary::before {
  content: ''; width: 0; height: 0;
  border: 4px solid transparent; border-left-color: var(--faint);
  transition: transform .12s;
}
.auth-alt[open] > summary::before { transform: rotate(90deg) translateX(1px); }
.auth-alt > summary:hover { color: var(--text); }
.auth-alt .hint { margin: 12px 0 16px; }
.auth-alt form { margin-top: 4px; }
.auth-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin-top: 44px; padding-top: 18px; border-top: 1px solid var(--line);
}
.auth-foot button { border: none; background: none; padding: 0; color: var(--faint); font-size: 12px; }
.auth-foot button:hover { background: none; color: var(--text); }
.auth-ver { font: 11px var(--mono); color: var(--faint); }

/* 登入頁沒有側邊欄，flash 改成固定在頂端，不去推擠版面。 */
.flash-float {
  position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
  z-index: 70; max-width: 360px; margin: 0; background: var(--card);
}

@media (max-width: 900px) {
  .auth { grid-template-columns: 1fr; }
  .auth-hero { display: none; }
  .auth-form {
    min-height: 100vh;
    min-height: 100svh;
    padding: calc(32px + env(safe-area-inset-top)) calc(24px + env(safe-area-inset-right))
             calc(32px + env(safe-area-inset-bottom)) calc(24px + env(safe-area-inset-left));
  }
}

/* 更窄或更矮的機型（iPhone SE 這種）把留白再收一點，讓表單整個進得去。 */
@media (max-width: 480px) {
  .auth-brand { margin-bottom: 28px; }
  .auth-sub { margin-bottom: 24px; }
  .auth-foot { margin-top: 28px; }
}

/* ============ 教學 ============ */

/*
  一個洞加一張卡。

  圈起來的那一塊沒有另外畫底色，是靠 .guide-mask 那道往外撐開 9999px 的陰影
  ——它把畫面上其他地方壓暗，而它自己是透明的，於是底下的東西維持原樣透出來。

  z-index 排在浮動按鈕（45）之上、對話框（50）之下：教學要蓋過畫面上的東西，
  但一篇讀完之後那個「下一篇」對話框要在最上面。
*/

.guide-catch { position: fixed; inset: 0; z-index: 46; }
/* 不指任何東西的那幾步沒有洞可以挖，壓暗的事就由這一層自己做。 */
.guide-wrap.is-center .guide-catch { background: rgba(26, 24, 21, .52); }
:root[data-theme="dark"] .guide-wrap.is-center .guide-catch { background: rgba(6, 6, 4, .66); }
/*
  「換你按」的那幾步要讓開，不然他按不到自己該按的東西。
  壓暗的效果留著——那是在指路，不是在擋路。
*/
.guide-wrap.is-live .guide-catch { pointer-events: none; }

.guide-mask {
  position: fixed; z-index: 47; pointer-events: none;
  /* 位置由 transform 給（見 app.js 的 place）：捲動時只有它在變，交給合成器就好。 */
  top: 0; left: 0;
  border-radius: var(--radius);
  box-shadow: 0 0 0 9999px rgba(26, 24, 21, .52), 0 0 0 1.5px var(--accent) inset;
  transition: transform .18s ease, width .18s ease, height .18s ease;
  will-change: transform;
}
:root[data-theme="dark"] .guide-mask {
  box-shadow: 0 0 0 9999px rgba(6, 6, 4, .66), 0 0 0 1.5px var(--accent) inset;
}

/*
  要操作的那一步：圈起來的框自己呼吸。

  這是整套介面唯一一個會動的東西，因為它要說的話沒有別的講法——
  「下一步」那顆按鈕已經拿掉了，往前走的路只剩「真的去按這裡」。
  一個靜止的框說不出「換你了」，而寫一行字又會被當成前面那兩句說明的一部分。

  光暈自己一層（洞裡那個 <i>），不跟壓暗那道陰影擠在同一個 box-shadow 裡。

  以前是同一個：一支 keyframes 同時管「外擴 7px 的光暈」與「往外 9999px 的
  壓暗」，於是每一格都得把那塊 9999px 重畫一次——整個畫面，一秒六十次，
  而且是在他正要動手的那幾步（會呼吸的正是那幾步）。整站跟著鈍掉的就是這個。

  拆開之後壓暗那一道是靜止的，畫一次就放著；會動的只剩這一圈，而它動的是
  opacity，瀏覽器交給合成器處理，不必重畫任何東西。看起來一樣。

  光暈在洞的上面，才會蓋在壓暗的那一層上；擴散只到 7px，
  再大就從「指著這裡」變成「這裡在閃」。
*/
.guide-glow {
  position: absolute; inset: 0; border-radius: inherit;
  box-shadow: 0 0 0 7px rgba(26, 24, 21, .22);
  opacity: 0;
}
:root[data-theme="dark"] .guide-glow { box-shadow: 0 0 0 7px rgba(233, 229, 219, .18); }
.guide-wrap.is-live .guide-glow {
  animation: guide-pulse 1.9s ease-in-out infinite;
  will-change: opacity;
}
@keyframes guide-pulse { 0%, 100% { opacity: 0 } 50% { opacity: 1 } }

@media (prefers-reduced-motion: reduce) {
  /* 不會動的版本：光暈固定亮著，訊息一樣傳得到。 */
  .guide-wrap.is-live .guide-glow { animation: none; opacity: 1; }
}

/*
  連線。畫在遮罩同一層，指的是「上面這個」跟「下面那個」是同一件事。

  每一條都畫兩次：粗的淺色打底、細的墨色壓上去。線會同時經過被打亮的那一塊
  （紙色）與被壓暗的地方，只用一個顏色一定有一段看不見。
*/
.guide-links { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 47; pointer-events: none; }
.guide-link, .guide-link-halo { fill: none; stroke-linecap: round; stroke-linejoin: round; }
/* 打底那一筆只要比墨線寬一點點就夠讀了。太寬會變成一條白帶子壓在畫面上。 */
.guide-link-halo { stroke: var(--card); stroke-width: 3.6; opacity: .9; }
.guide-link { stroke: var(--accent); stroke-width: 1.6; }


/*
  線是「畫出來」的：從無到有，一路拉到目的地。箭頭接在同一條路徑後面，
  所以它是同一筆的收尾，不必另外給它動畫。

  一次畫一條、後面那條晚一點開始——兩條同時竄出來看不出誰對誰，
  而這一步要講的正是誰對誰。
*/
@keyframes guide-draw {
  from { stroke-dashoffset: var(--len); }
  to { stroke-dashoffset: 0; }
}
.guide-link-line.is-drawing {
  stroke-dasharray: var(--len);
  animation: guide-draw .6s ease-out both;
}

@media (prefers-reduced-motion: reduce) {
  /* 不會動的版本：線直接就在那裡，訊息一樣傳得到。 */
  .guide-link-line.is-drawing { animation: none; stroke-dasharray: none; }
}

.guide-tip {
  position: fixed; z-index: 48; width: 350px; max-width: calc(100vw - 24px);
  /* 內容再長也不能長到畫面外面去：矮的筆電加上「換你按」那一列就會頂到底。 */
  max-height: calc(100vh - 24px); overflow: hidden;
  display: flex; flex-direction: column;
  background: var(--card); color: var(--text);
  border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: 0 1px 1px rgba(26, 24, 21, .06), 0 18px 44px rgba(26, 24, 21, .22);
  padding: 14px 16px 12px;
}
/*
  說明會捲，動作與按鈕不會。

  min-height: 0 是關鍵：flex 項目預設不會縮到比內容小，少了它，說明那塊
  會把整張框撐破。換句話說，空間不夠時被收起來的一定是「讀完就算的句子」。

  這裡不用 position: sticky——內容比框高的時候，黏在底部的那塊會被推到
  說明上面去，兩邊疊在一起。
*/
.guide-read { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
/*
  釘住那一塊上緣畫一條線。

  說明捲到一半被切在這裡是常態（矮的畫面放不下），少了這條線，那個切口
  看起來就是「畫面壞了」；有了它，切口落在一條邊界上，讀起來是「上面還有」。
*/
.guide-pin { flex: 0 0 auto; border-top: 1px solid var(--line); padding-top: 10px; }
.guide-wrap.is-center .guide-tip { left: 50%; top: 50%; transform: translate(-50%, -50%); }

.guide-tip-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.guide-tip-head .icon-btn { margin-right: -6px; }
/* 「哪一篇、第幾步」是資料，走等寬字。 */
.guide-no { font: 11px var(--mono); color: var(--faint); letter-spacing: .04em; }
/* 這一步在哪一頁。教學會自己換頁，看的人得隨時知道自己被帶到哪裡了。 */
.guide-where {
  font-size: 10.5px; letter-spacing: .1em; color: var(--muted);
  padding: 1px 7px; border: 1px solid var(--line); border-radius: 999px;
  white-space: nowrap;
}
.guide-tip h3 { margin: 0 0 6px; font-size: 15.5px; letter-spacing: -.01em; }
.guide-what { margin: 0; font-size: 13.5px; line-height: 1.7; }
/* 說明底下那幾條。編號自己看得到有幾件，不用讀完一長串頓號再回頭數。 */
.guide-list { margin: 4px 0 0; padding-left: 20px; font-size: 13.5px; line-height: 1.7; }
.guide-list li::marker { color: var(--muted); font-variant-numeric: tabular-nums; }
/* 「為什麼」淡一階、縮進來一點：照著做的人看上面那句就夠了。 */
.guide-why {
  margin: 8px 0 0; padding-left: 10px; border-left: 1px solid var(--line);
  color: var(--muted); font-size: 12.5px; line-height: 1.7;
}

/*
  分岔的選項。做成兩張可以點的卡片而不是兩顆按鈕：選項底下那句「什麼情況下
  會選它」才是決定的依據，而按鈕塞不下那句話。
*/
.guide-ask { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
.guide-choice {
  display: flex; flex-direction: column; gap: 2px; align-items: flex-start;
  text-align: left; width: 100%; padding: 10px 12px;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
}
.guide-choice:hover { background: var(--hover); border-color: var(--accent); }
.guide-choice strong { font-size: 13.5px; font-weight: 600; }
.guide-choice span { font-size: 12px; color: var(--muted); line-height: 1.6; }

/*
  「換你按」。用底色帶出來，是這張卡片上唯一一塊有底的地方——
  它跟其他句子不一樣：那幾句是讀的，這一句是要動手的。
*/
.guide-do {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 0; padding: 7px 9px;
  background: var(--accent-soft); border-radius: var(--radius);
  font-size: 12.5px; font-weight: 500; line-height: 1.55;
}
/* 說明與按鈕同一行。各佔一行的話這一塊就吃掉兩倍高度，
   而它是釘住的——高度全從說明那邊扣。 */
.guide-do span { flex: 1 1 120px; min-width: 0; }

/*
  要貼的那段示範內容，整段印出來。

  不印的話，想自己動手貼的人根本不知道要貼什麼——而這一步練的正是
  「把手上那段亂七八糟的東西貼進來」。等寬字：它是資料，不是說明。
*/
.guide-sample {
  margin: 10px 0 0; padding: 8px 10px; background: var(--bg);
  border: 1px solid var(--line); border-radius: var(--radius);
  font: 11.5px/1.7 var(--mono); color: var(--muted);
  white-space: pre-wrap; word-break: break-all;
  /* 兩行高、自己捲。它是「長什麼樣子」的樣本，不是要在這裡讀完的東西。 */
  max-height: 42px; overflow-y: auto;
}
.guide-do button { flex: 0 0 auto; }

/*
  示範畫面：等一下那一頁長什麼樣、要按的是哪一顆（內容見 app.js 的 shots）。

  整套教學只有 Token 那一步會把人送出站外，而 GitLab 那一頁蓋不到光圈、也嵌不進來，
  所以在這裡先畫一張它的樣子。

  畫的是「一張圖」，不是「一塊介面」：退到紙色底上、字級再小一階、欄位不給任何
  按得下去的樣子——不然看的人會伸手去點圖裡的東西。唯一強調的是要按的那一顆，
  用的是光圈同一種強調色：整套教學裡「該動的東西」都長這樣，這裡不另外發明一套。
*/
.guide-shot { margin: 8px 0 2px; }
.gs-bar {
  display: flex; align-items: center; gap: 4px; padding: 5px 8px;
  border: 1px solid var(--line); border-bottom: 0;
  border-radius: var(--radius) var(--radius) 0 0;
  background: var(--hover); font: 10px var(--mono); color: var(--faint);
}
/* 那三顆點只是「這是另一個視窗」的記號，不必畫得像某一個瀏覽器。 */
.gs-bar i { flex: none; width: 4px; height: 4px; border-radius: 50%; background: var(--line); }
.gs-bar span { margin-left: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.gs-body {
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
  padding: 9px 10px 10px; background: var(--bg);
  border: 1px solid var(--line); border-radius: 0 0 var(--radius) var(--radius);
}
/* 那一頁最上面那一列。畫它是為了右上角那顆：表單沒展開的人要按的就是它。 */
.gs-head { display: flex; align-items: center; gap: 8px; align-self: stretch; margin-bottom: 1px; }
.gs-head strong { font-size: 12px; }
.gs-head .gs-ghost { margin-left: auto; }
/* 那一頁上「不是這一步要按」的按鈕。整張圖只有一塊實心的，其他一律畫成空框。 */
.gs-ghost {
  flex: none; padding: 3px 7px; background: var(--card);
  border: 1px solid var(--line); border-radius: var(--radius);
  font-size: 10px; color: var(--muted);
}
/* 中間省掉的那幾個權限。api 排在清單中段，接著 read_api 畫的話會以為它就在第二個。 */
.gs-more { color: var(--faint); font-size: 11px; letter-spacing: .2em; line-height: 1; padding-left: 1px; }

/* 欄位名照抄那一頁的英文：過去之後要認的就是同一個字。 */
.gs-cap { font-size: 10px; letter-spacing: .04em; color: var(--faint); }
.gs-field {
  display: flex; align-items: center; gap: 6px; align-self: stretch;
  padding: 4px 7px; background: var(--card);
  border: 1px solid var(--line); border-radius: var(--radius);
  font-size: 11px; color: var(--muted);
}
.gs-field.mono { font-family: var(--mono); font-size: 10px; }
.gs-check { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--faint); }
.gs-check::before {
  content: ''; flex: none; display: grid; place-items: center;
  width: 11px; height: 11px; border: 1px solid var(--line); border-radius: 2px;
  background: var(--card); font-size: 8px; line-height: 1;
}
/* 勾起來的那一項。權限是網址帶好的，這裡只要讓人認得出「本來就該是這樣」。 */
.gs-check.on { color: var(--text); }
.gs-check.on::before { content: '✓'; background: var(--text); border-color: var(--text); color: var(--card); }

/*
  要按的那一顆：整張圖唯一的實心塊。

  欄位撐滿、按鈕只有自己那麼寬——那一頁本來就是這樣，而且拉成整條的按鈕
  看起來像一塊底色，不像一顆按得下去的東西。
*/
.gs-cta {
  display: flex; align-items: center; gap: 6px; align-self: flex-start; margin-top: 3px;
  padding: 5px 9px; border-radius: var(--radius);
  background: var(--accent); color: var(--accent-ink);
  font-size: 11px; font-weight: 600;
}
.gs-cta svg { flex: 0 0 12px; width: 12px; height: 12px; stroke-width: 1.7; }
/* 底下那一排。旁邊那顆 Cancel 照畫，那一頁本來就有兩顆挨著——
   只畫一顆的話，過去之後多出來的那顆會讓人懷疑是不是走錯地方。 */
.gs-actions { display: flex; align-items: center; gap: 8px; margin-top: 3px; }
.gs-actions .gs-cta { margin-top: 0; }

/* 建好之後那一塊綠色的成功框。用站上既有的功能色，不另外調一個綠。 */
.gs-alert {
  display: flex; flex-direction: column; gap: 5px; align-self: stretch;
  padding: 8px 9px; border-radius: var(--radius); background: var(--ok-bg);
}
.gs-alert-head {
  display: flex; align-items: center; gap: 5px;
  font-size: 11px; font-weight: 600; color: var(--ok-ink);
}
.gs-alert-head svg { flex: 0 0 12px; width: 12px; height: 12px; }
/* 欄位與右邊那兩顆在同一行，跟那一頁一樣。 */
.gs-row { display: flex; align-items: center; gap: 5px; align-self: stretch; }
.gs-row .gs-field { flex: 1; min-width: 0; letter-spacing: .06em; }
.gs-row .gs-cta { margin-top: 0; }
/* 只有圖示的那一顆（看內容）。它不是這一步要按的，所以畫成空框。 */
.gs-icon {
  flex: none; display: grid; place-items: center; width: 24px; height: 24px;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--muted);
}
.gs-icon svg { flex: none; width: 12px; height: 12px; }
/* 那一頁自己的提醒：這一串只出現這一次。照抄，不要翻成中文——
   等一下要在畫面上認的就是這一句。 */
.gs-note { font-size: 9.5px; line-height: 1.5; color: var(--ok-ink); }

/* 我們自己貼上去的標記（已帶好、按這顆）要跟那一頁上的字分得開。 */
.guide-shot em {
  margin-left: auto; flex: none; font-style: normal; white-space: nowrap;
  padding: 1px 6px; border-radius: 999px;
  font-size: 9.5px; letter-spacing: .04em;
  background: var(--accent-soft); color: var(--text);
}
.gs-cta em { background: var(--accent-ink); color: var(--accent); }
/* 註腳是我們的話，不是那一頁上的東西：擺在框外面。 */
.guide-shot figcaption { margin: 6px 0 0; font-size: 11px; color: var(--faint); line-height: 1.6; }

.guide-track { height: 2px; background: var(--line); margin: 10px 0 8px; border-radius: 2px; flex: 0 0 auto; }
.guide-track i { display: block; height: 100%; background: var(--accent); border-radius: 2px;
                 transition: width .2s ease; }

/*
  動作、進度、按鈕黏成一塊釘在框底。

  說明可以捲，但「要按什麼」跟「怎麼離開」永遠得看得到——那兩件事被自己的
  捲軸吃掉就等於卡死。捲掉的只會是讀完就算的句子。
*/
.guide-foot { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.guide-foot button { padding: 6px 12px; font-size: 12.5px; white-space: nowrap; }
/* 「略過這一步」是退路，不是選項：淡一階，不跟旁邊那幾顆搶。 */
.guide-foot [data-guide="skip"] { border-color: transparent; color: var(--muted); }
.guide-foot [data-guide="skip"]:hover { color: var(--text); }

/* ---- 挑一篇 ---- */

.guide-menu { display: flex; flex-direction: column; gap: 8px; }
.guide-menu-row {
  display: flex; flex-direction: column; gap: 3px; align-items: flex-start;
  text-align: left; width: 100%; padding: 12px 14px;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
}
.guide-menu-row:hover { background: var(--hover); border-color: var(--accent); }
.guide-menu-head { display: flex; align-items: baseline; gap: 8px; width: 100%; }
.guide-menu-head strong { font-size: 14px; font-weight: 600; }
.guide-menu-head em { font: 11px var(--mono); font-style: normal; color: var(--faint); }
/* 那一句話要會換行。兩件事都得寫：這一顆是按鈕，白空白從 button 那邊繼承成
   nowrap；而 align-items: flex-start 讓它只長到內容那麼寬，不吃整排的寬度。
   少了任一句，句子一長就直接衝出卡片右邊，而不是折成第二行。 */
.guide-menu-row > span {
  font-size: 12.5px; color: var(--muted); line-height: 1.7;
  align-self: stretch; white-space: normal;
}

/* ---- 側邊選單底下那顆小的 ---- */

.side-meta { display: flex; align-items: center; gap: 4px; }
.side-meta .side-ver { flex: 1; min-width: 0; }
.side-learn {
  flex: 0 0 auto; display: grid; place-items: center;
  width: 28px; height: 28px; padding: 0;
  background: none; border: none; color: var(--faint); border-radius: var(--radius);
}
.side-learn:hover { background: var(--hover); color: var(--text); }
.side-learn svg { width: 15px; height: 15px; flex-basis: 15px; }

@media (max-width: 640px) {
  /*
    手機上提示框改成底部的一張卡：畫面窄，跟著圈起來的位置浮動的話，
    它常常整張蓋住自己正在講的那個東西。
  */
  .guide-tip {
    left: 8px !important; right: 8px; top: auto !important; width: auto; max-width: none;
    bottom: calc(58px + env(safe-area-inset-bottom));
  }
  .guide-wrap.is-center .guide-tip { transform: none; }
}

/* ===================== 轉單 ===================== */

/*
  這一頁有兩種列：查回來的那兩張卡（票、issue），以及兩份逐項清單（檢查、動作）。
  兩種都是「左邊一個記號，右邊一句話」，所以版面只有一套：記號欄寬度固定，
  右邊那一欄自己流動——標題與說明一長，換行的是它，前面那一欄仍然對齊成一直線。
*/

.tf-cards { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }

/* 一列一個東西：左邊標 Jira／GitLab，右邊是編號、狀態與標題。
   標題一定自己一行——它常常是三十個字，跟編號擠在同一行兩邊都讀不下去。 */
.tf-card {
  display: grid; grid-template-columns: 52px minmax(0, 1fr); gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--bg);
}
.tf-card.is-warn { background: var(--warn-bg); border-color: transparent; }
.tf-tag {
  font-size: 11px; letter-spacing: .04em; text-transform: uppercase;
  color: var(--faint); padding-top: 2px;
}
/* 卡片裡是 div、留言那一列是 span，兩邊都要當成一塊：
   維持 inline 的話，裡面那個兩行的預覽會自己跳出去從最左邊開始排。 */
/* 卡片裡是 div、留言那一列是 span，兩邊都要當成一塊：
   維持 inline 的話，裡面那個兩行的預覽會自己跳出去從最左邊開始排。

   basis 給 0 而不是 auto：.pick-row 是會換行的 flex，而 auto 是拿「整段文字攤平」
   當寬度去排的——留言長一點，那一塊就整個被擠到勾選框底下自成一行，
   於是同一份清單裡，長的那幾列比短的那幾列往左凸出 25px。 */
.tf-main { display: block; flex: 1 1 0; min-width: 0; }
.tf-line { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; min-width: 0; }
.tf-id { font-family: var(--mono); font-size: 13px; }
.tf-title { margin-top: 2px; font-size: 13px; color: var(--muted); }
.tf-chip {
  flex: 0 0 auto; font-size: 11px; padding: 1px 7px; border-radius: 999px;
  background: var(--accent-soft); color: var(--muted);
}

/* 小標。它是一句指示，不是一段說明，所以比 .hint 更收斂。 */
.tf-sub { margin: 18px 0 6px; font-size: 12px; color: var(--muted); }

.tf-links.is-empty .tf-sub { display: none; }
.tf-link { align-items: start; }
.tf-link .tf-line code { font-size: 12px; }
/* 留言原文只是拿來認「是不是這一則」，兩行就夠；整則攤開會把清單撐到看不完。 */
.tf-body {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; margin-top: 3px;
  font-size: 12px; color: var(--muted); line-height: 1.5;
}

/* 收起來的那幾則留言。它們不是選項，是「萬一猜錯了」的出路，
   所以連標題都做得小：預設那一則才是要看的。 */
.tf-more { margin-top: 2px; }
.tf-more > summary {
  list-style: none; cursor: pointer;
  padding: 8px 4px 8px 34px;
  font-size: 12px; color: var(--muted);
}
.tf-more > summary::-webkit-details-marker { display: none; }
.tf-more > summary:hover { color: var(--text); }

.tf-list { list-style: none; margin: 12px 0 0; padding: 0; }
.tf-item {
  display: flex; align-items: baseline; gap: 10px;
  padding: 8px 4px; border-top: 1px solid var(--line);
  color: var(--faint);
}
.tf-item:first-child { border-top: 0; }
/* 記號那一格寬度固定，名字才會對齊成一直線。 */
.tf-mark { flex: 0 0 20px; text-align: center; font-weight: 700; color: var(--faint); }
.tf-name { flex: 0 0 auto; min-width: 128px; }
.tf-msg { flex: 1 1 200px; min-width: 0; color: var(--muted); font-size: 13px; }

/* 還沒問到的那幾列維持灰的：亮起來的就是問過的。 */
.tf-item.is-run { color: var(--text); }
.tf-item.is-run .tf-mark { color: var(--muted); }
.tf-item.is-ok { color: var(--text); }
.tf-item.is-ok .tf-mark { color: var(--ok-ink); }
.tf-item.is-warn { color: var(--text); }
.tf-item.is-warn .tf-mark { color: var(--warn-ink); }
.tf-item.is-warn .tf-msg { color: var(--warn-ink); }
.tf-item.is-bad { color: var(--text); }
.tf-item.is-bad .tf-mark { color: var(--err-ink); }
.tf-item.is-bad .tf-msg { color: var(--err-ink); }
.tf-item.is-skip .tf-mark { color: var(--faint); }
.tf-item.is-todo .tf-msg { color: var(--faint); }

.tf-steps { margin-top: 20px; padding-top: 4px; border-top: 1px solid var(--line); }
.tf-steps h3 { margin: 12px 0 0; }

.tf-foot { display: flex; align-items: center; gap: 10px; margin-top: 14px; flex-wrap: wrap; }
.tf-foot .spacer { flex: 1; min-width: 12px; }

@media (max-width: 640px) {
  /* 窄螢幕上名字與說明各站一行：擠成一列的話兩邊都只剩兩三個字。 */
  .tf-item { flex-wrap: wrap; }
  .tf-name { min-width: 0; }
  .tf-msg { flex: 1 1 100%; margin-left: 30px; }
  .tf-card { grid-template-columns: minmax(0, 1fr); gap: 2px; }
}

@media (max-width: 640px) {
  /* 手機上那一排東西擠不下：狀態下拉與搜尋框各自佔滿一行，那兩顆按鈕掉到下一列。
     擠同一列的話，搜尋框會被壓到只剩幾十像素，裡面的字整排看不到。

     寫在檔案最後面：這一份的 @media 多半排在前段，同分的桌機規則排在它後面，
     覆寫會靜靜地失效（.iss-bar 與 .iss-pick 的本體就在上面）。 */
  .iss-filter, .iss-bar .scope-search-box { flex: 1 1 100%; }
  .iss-pick { width: 100%; max-width: none; }
  .iss-bar .ss-tail { margin-left: 0; }
}
