/* ==========================================================================
   Hupa 品牌主題層
   --------------------------------------------------------------------------
   掛在 sleek.css 之後, 靠載入順序與選擇器特異性覆蓋, 不動 sleek.css 原檔。
   色值一律沿用 sleek.css 裡既有的值, 不自創新色。
   ========================================================================== */

:root {
  /* 色票 —— 括號裡是這個值在 sleek.css 的出處 */
  --hupa-primary: #4c84ff;          /* sleek.css:15745 .app-brand */
  --hupa-primary-hover: #3a70e8;    /* primary 加深 12%, 給 hover 用 */
  --hupa-sidebar-bg: #212a39;       /* sleek.css:16499 .sidebar-dark .left-sidebar */
  /* 桌機展開態的側欄寬度。sleek 原值 250px (sleek.css:15654), 但選單最長的
     「AI 模型管理」右緣只到 132px, 右邊空 118px。收成 token 是因為這個寬度
     同時出現在三個地方 (側欄自己 / page-wrapper 與 main-header 的 padding-left),
     散開寫遲早會對不起來。手機不吃這個值 —— 那邊側欄是 250px 的抽屜, 而且
     跟 .sidebar-mobile-in/out 的 translateX(-250px) 綁在一起。 */
  --hupa-sidebar-w: 180px;
  --hupa-mobile-nav-height: 64px;
  --hupa-heading: #1b223c;          /* sleek.css:12604 .breadcrumb-wrapper h1 */
  --hupa-text-muted: #8a909d;       /* sleek.css:63 body color */
  --hupa-border: #e5e9f2;           /* sleek.css:16546 .sidebar-light .sidebar */
  --hupa-page-bg: #f5f6fa;          /* sleek.css:65 body background */
  --hupa-surface: #ffffff;
  --hupa-surface-hover: #f0f3f8;    /* page-bg 與 border 之間, 給 hover 底色 */
  --hupa-success: #29cc97;          /* sleek.css:2621 .badge-success */
  --hupa-success-soft: rgba(41, 204, 151, 0.1);
  --hupa-success-border: rgba(41, 204, 151, 0.35);
  --hupa-success-text: #176b50;
  --hupa-danger: #dc3545;           /* sleek.css .btn-danger, 給移除/刪除用 */
  --hupa-danger-soft: rgba(220, 53, 69, 0.1);
  --hupa-danger-text: #a41a26;      /* danger 加深, 淡底上的字要過對比 */
  --hupa-warning: #fec400;          /* sleek.css:25 --warning, 註記用同一個黃 */
  --hupa-orange: #fd7e14;           /* 已刪除的列用的橘, 和註記黃區隔開 */
  /* 橘的淡底與深字: 淡底是 orange 壓 12% alpha, 深字是同色相壓暗到
     能在那塊淡底上過 4.5:1 —— 實心橘配白字在小尺寸標籤上太吵。 */
  --hupa-orange-soft: rgba(253, 126, 20, 0.12);
  --hupa-orange-text: #c2570a;
  /* primary 的淡底, 給「這一列被選起來了」用 (/projectedit 的已加入列)。
     實心 primary 會把整列的文字壓成不可讀, 而整列狀態要的是一眼分群而不是
     搶眼。rgb(76,132,255) 就是 --hupa-primary (#4c84ff) —— CSS 沒辦法直接
     對一個 hex token 加 alpha, 所以跟 --hupa-focus-ring 一樣整條收成 token。
     hover 那支比較重: 沒有它的話滑過去會被全站的整列 hover 底色蓋掉,
     選起來的那幾列就消失了。 */
  --hupa-primary-soft: rgba(76, 132, 255, 0.08);
  --hupa-primary-soft-hover: rgba(76, 132, 255, 0.14);
  /* M-11: 深色底上用的色。上面那組都是給淺色底的, 這裡不能沿用。
     收成 token 才不會散在規則裡各寫各的 alpha。
     縮圖卡片 (.thumbnail 的黑/灰 letterbox) 上的文字: */
  --hupa-on-dark: #ffffff;
  --hupa-on-dark-muted: rgba(255, 255, 255, 0.72);
  /* 深色側欄上的分隔線與捲軸 —— 都是 --hupa-border (#e5e9f2) 加不同 alpha,
     直接用 #e5e9f2 在深色底上太亮, 所以壓透明度而不是換色。 */
  --hupa-on-dark-divider: rgba(229, 233, 242, 0.12);
  --hupa-on-dark-scrollbar: rgba(229, 233, 242, 0.24);
  --hupa-on-dark-scrollbar-hover: rgba(229, 233, 242, 0.4);

  /* 形狀 */
  --hupa-radius: 8px;
  --hupa-radius-sm: 6px;
  --hupa-shadow-sm: 0 1px 2px rgba(33, 42, 57, 0.06);
  --hupa-shadow: 0 2px 8px rgba(33, 42, 57, 0.08);
  /* 手機底部抽屜往上打的陰影, 與浮起的 FAB / 操作選單。兩條都比
     --hupa-shadow 重: 它們是疊在內容之上的層, 不是頁內的卡片。 */
  --hupa-shadow-sheet: 0 -4px 20px rgba(33, 42, 57, 0.18);
  --hupa-shadow-raised: 0 4px 12px rgba(33, 42, 57, 0.24);
  /* 往上打的陰影, 給貼在底部的 sticky 底座用 (--hupa-shadow 是往下的)。 */
  --hupa-shadow-dock: 0 -2px 10px rgba(33, 42, 57, 0.06);
  /* 疊在照片上的圖示 (手機縮圖的精選愛心)。照片亮暗不定, 沒有這層描邊時
     淺色的夜拍會把白圖示整個吃掉。 */
  --hupa-shadow-on-media: 0 1px 2px rgba(0, 0, 0, 0.55);
  /* M-11: 輸入框 focus 的光暈。rgb(76,132,255) 就是 --hupa-primary (#4c84ff),
     只是 CSS 沒辦法直接對一個 hex token 加 alpha, 所以整條收成 token,
     不散在 .form-control:focus 裡。 */
  --hupa-focus-ring: 0 0 0 3px rgba(76, 132, 255, 0.12);
  /* 彈窗遮罩。bootstrap 的 .modal-backdrop 是純黑 0.5 (sleek.css:6124) —— 那是
     「整頁被接管」的重量, 而且純黑會把底下的介面壓成灰階。改用 --hupa-heading
     (#1b223c = rgb(27,34,60)) 壓透明度, 全站彈窗共用。
     --soft 給對話框疊在別的彈窗上時用: 兩層 0.4 疊起來約 0.64, 底下的彈窗會
     整片糊掉, 所以第二層再淡一階。 */
  --hupa-overlay: rgba(27, 34, 60, 0.4);
  --hupa-overlay-soft: rgba(27, 34, 60, 0.16);

  /* 疊在照片上的控件底色 (縮圖卡片左上角那顆「加註記」)。照片底下可能是任何
     亮度, 圖示要靠自己這塊底才看得見。用純黑壓透明度: 帶藍的 --hupa-heading
     疊在夜拍的灰階照片上會偏成藍調。 */
  --hupa-scrim: rgba(0, 0, 0, 0.45);
  --hupa-scrim-strong: rgba(0, 0, 0, 0.65);

  /* 照片等比縮放後補邊的底色 (letterbox)。--alt 是同一支影片的後續幀
     (.thumbnail.slaveframe) 用的, 淺一階當視覺線索。 */
  --hupa-letterbox: #10141c;
  --hupa-letterbox-alt: #555555;
  /* 影像舞台載入中的掃光。底色是 --hupa-letterbox (深色), 所以這條是
     打在深色上的一層白光, 不是任何一個介面色。 */
  --hupa-letterbox-shimmer: rgba(255, 255, 255, 0.09);

  /* 圖示按鈕 (.btn-edit-icon) 的底色。用純黑壓透明度而不是 --hupa-heading
     那種帶藍的深色 —— 這顆鈕會疊在白卡片、.table 的斑馬列與彈窗標頭上, 帶色
     的底在斑馬列上會偏成另一個顏色, 純黑只是把底下的顏色壓一階。
     CSS 沒辦法對 hex token 加 alpha, 所以整條收成 token, 不散在規則裡。 */
  --hupa-icon-btn-bg: rgba(0, 0, 0, 0.05);
  --hupa-icon-btn-bg-hover: rgba(0, 0, 0, 0.1);

  /* DataTables 的整列 hover 底色。#f6f6f6 是 datatables.bootstrap4.min.css
     給 `table.dataTable.hover tbody tr:hover` 的值 —— 凍結欄有自己的底色會
     蓋掉那一列, 要照抄同一個數字才會是同一片底色。抄的是別人的常數, 但值仍
     然只寫在 :root (M-11)。 */
  --hupa-datatable-row-hover: #f6f6f6;

  /* 側欄底部版本號列 (.hupa-sidebar-version)。
     --hupa-on-dark-text 抄 sleek.css:16507 `.sidebar-dark .sidebar .sub-menu
     > li > a` 的顏色 —— 跟它一樣是深色側欄上的次要文字。
     --hupa-notify 是唯一不是抄 sleek 既有值的顏色: 未讀紅點要跟 --hupa-danger
     (#dc3545, 刪除/危險動作用) 區隔開, 不然使用者會把「有新公告」誤讀成
     「有東西壞了」。 */
  --hupa-on-dark-text: #b7c0cd;
  --hupa-notify: #f95372;

  /* 頂端橫幅 (.hupa-banner) 的色。bootstrap 的 .alert-warning / .alert-danger
     是「跳出來一次」的提示色, 而這兩條橫幅常駐在每一頁最上面 —— 飽和底色放
     一整天會被當成背景無視, 所以壓成淡底 + 深字, 對比留在文字上。
     底與框是 --hupa-warning (#fec400) / --hupa-danger (#dc3545) 同色相往白的
     方向調出來的, 字是同色相壓暗到在那塊淡底上過 4.5:1。--detail 給次要那行,
     比 --text 淡一階但仍過 4.5:1 (降的是重量, 不是可讀性)。
     --icon-bg 是原色壓 alpha, 給圖示那塊圓底。 */
  --hupa-banner-warning-bg: #fff8e6;
  --hupa-banner-warning-border: #f2dca6;
  --hupa-banner-warning-text: #6b4d00;
  --hupa-banner-warning-detail: #8a6a17;
  --hupa-banner-warning-icon: #a06f00;
  --hupa-banner-warning-icon-bg: rgba(254, 196, 0, 0.18);
  --hupa-banner-danger-bg: #fdeceb;
  --hupa-banner-danger-border: #f3bfbc;
  --hupa-banner-danger-text: #8a1f1a;
  --hupa-banner-danger-detail: #a3453f;
  --hupa-banner-danger-icon: #c62f27;
  --hupa-banner-danger-icon-bg: rgba(220, 53, 69, 0.12);

  /* 字體 —— sleek 只指定 Roboto, 中文會掉到系統預設而且各機器長得不一樣。
     這裡補上中文字族, 讓中文字重與 Roboto 搭得起來。全部是系統字, 不連外。 */
  --hupa-font-stack: "Roboto", "Noto Sans TC", "PingFang TC",
    "Microsoft JhengHei", "Heiti TC", sans-serif;

  /* 手機字級 —— 一套六階, 全站手機只能用這六個值。

     沒有這組 token 之前每個手機元件各寫各的 px/rem: 篩選抽屜的標題吃
     Bootstrap 的 h2 (2rem), 單列操作 sheet 的標題是 .h6, 兩個同樣是
     「這一層在講什麼」卻差了一倍; 清單標題 15px、篩選欄位標籤 16px,
     掃過去像兩套設計。 */
  --hupa-m-page: 1.375rem;    /* 頁名, 例如「佈設管理」 */
  --hupa-m-heading: 1.0625rem; /* 一層的標題: 抽屜、sheet、詳情主標 */
  --hupa-m-title: 0.9375rem;  /* 清單列的主標 */
  --hupa-m-body: 0.875rem;    /* 內文與表單控件 */
  --hupa-m-label: 0.8125rem;  /* 欄位標籤 */
  --hupa-m-meta: 0.75rem;     /* 摘要、次要說明 */
  --hupa-m-micro: 0.6875rem;  /* eyebrow、指標標籤、狀態標籤 */
  /* 可輸入控件專用。不是排版尺度, 是 iOS 的硬限制: 小於 16px 的欄位
     一聚焦就會把整頁放大, 而且不會縮回去。 */
  --hupa-m-control: 1rem;     /* input / select / textarea */
}

body {
  font-family: var(--hupa-font-stack);
}

/* --------------------------------------------------------------------------
   A1 —— Logo 區
   sleek 給 .app-brand 塗了 #4c84ff, 在深色側欄上切出一塊亮藍方塊。
   改成透明讓 .left-sidebar 的底色透上來, 換 sidebar-light 主題時也會自動跟著對。
   -------------------------------------------------------------------------- */
.app-brand {
  background-color: transparent;
  border-bottom: 1px solid var(--hupa-on-dark-divider);
  /* logo 靠左, 收合鈕靠右。sleek 原本是 display:block (sleek.css:15743),
     只裝得下 logo 一個東西。 */
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* V-1: Task 1 把這裡從 75px (sleek.css:15749-15757 .app-brand a 的原值,
   剛好等於 sleek.css:14876-14883 .navbar 的 min-height:74px + border-bottom:1px)
   改成 64px, 目的是配合上面 .app-brand 新增的 border-bottom, 但這個
   border-bottom 是新增的 1px, 不是拿掉高度換來的 —— 結果 logo 區變成
   64+1=65px, header 仍是 74+1=75px, 兩邊差了 10px, 側欄底色下緣比 header
   下緣高一截。改回 74px, 讓 .app-brand 的 border-bottom (1px) + 這裡的
   74px 等於 header 的 74px (navbar min-height) + 1px (navbar border-bottom)。 */
.app-brand a {
  height: 74px;
  line-height: 74px;
}

.app-brand a img {
  max-width: 116px;
}

/* --------------------------------------------------------------------------
   A1-b —— 側欄收合鈕 (從 header 搬進 logo 這排)
   離開 .navbar 之後 sleek 那組 .navbar .sidebar-toggle (sleek.css:14902-14921)
   全部選不到了, 三件事要自己補:
   1. mdi 的 \f9a7 icon (原本在 .navbar .sidebar-toggle:before)
   2. 顏色 —— 不補的話吃到 sleek 的 #8a909d 灰, 在深色側欄上很鈍
   3. 拿掉 border-right: 1px solid #e5e9f2 —— 那是給白色 header 用的分隔線,
      畫在深色底上會變成一條刺眼白線
   -------------------------------------------------------------------------- */
.app-brand .sidebar-toggle {
  flex: 0 0 auto;
  height: 74px;
  padding: 0 1.25rem;
  background: transparent;
  border: 0;
  color: var(--hupa-on-dark-muted);
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
}

.app-brand .sidebar-toggle::before {
  content: "\f9a7";
  font-family: "Material Design Icons";
}

.app-brand .sidebar-toggle:hover,
.app-brand .sidebar-toggle:focus {
  color: var(--hupa-on-dark);
  outline: none;
}

/* sleek 給 .app-brand a 寫死 width:250px (sleek.css:15749-15757) = 側欄全寬,
   在 flex 容器裡會把收合鈕整個擠出去。改成讓 logo 吃剩下的空間。 */
.app-brand a {
  width: auto;
  flex: 1 1 auto;
  min-width: 0;
}

/* 桌機的收合鈕搬走後 header 左邊空出一塊, top_banner 原本是靠那顆鈕的
   padding 撐開的, 現在自己補左內距。(navbar-right 靠右見下方 A2。) */
@media (min-width: 768px) {
  .navbar {
    padding-left: 1.5rem;
  }
}

/* V-2: 側欄收合 (.sidebar-minified, >=768px 時寬度 75px) 時, 75px 裝不下
   logo + 收合鈕兩個。這裡選「只留收合鈕」:
   收合態下唯一需要的操作就是展回來, 鈕不能藏 —— 藏了就永遠回不來。
   logo 反而是此時可以不露的。

   早期版本是收合時把 logo 換成只有爪印的 hupa_paw.svg (因為 .app-brand a
   寬 250px 且 overflow:hidden, 116px 的完整 logo 會被切成「爪印+Hup」);
   收合鈕進來之後這條路不適用了, 爪印那張圖跟對應規則一起拿掉。 */
@media (min-width: 768px) {
  .sidebar-minified .left-sidebar .app-brand {
    justify-content: center;
  }

  .sidebar-minified .left-sidebar .app-brand a {
    display: none;
  }

  .sidebar-minified .left-sidebar .app-brand .sidebar-toggle {
    width: 75px;
    padding: 0;
    text-align: center;
  }
}

/* --------------------------------------------------------------------------
   V-3 —— 關掉收合態的 hover 偷看

   sleek 的設計是收合成 75px 之後, 滑鼠移到側欄上會暫時展回 250px
   (sleek.css:16041-16047 `.sidebar-minified .left-sidebar:hover`), 滑走再縮。
   收合鈕搬進側欄之後這個行為壞掉了: 按下鈕的那一刻游標必然還停在側欄上,
   於是 .sidebar-minified 一加上, hover 規則同一瞬間又把寬度撐回 250px ——
   使用者看到的是「按了沒反應」, 要抖一下滑鼠才會縮。

   試過用 pointer-events:none 讓側欄不再是命中目標 (:hover 就整組失效),
   但瀏覽器不會因為 pointer-events 變了就立刻重算 :hover, 要等下一次滑鼠
   移動事件 —— 症狀只是變短, 沒有消失。所以改成直接把 hover 偷看關掉:
   收合態就是常駐 75px 的 icon rail, 要看文字就點鈕展開。

   選擇器跟 sleek 同特異性, 靠 hupa-theme.css 載在 sleek.css 之後蓋掉。
   section-title 的 hover 規則在 layout.html 的內嵌 <style> 裡, 那是我們自己
   寫的, 直接刪掉不用在這裡蓋。
   sub-menu / .brand-name 那幾條 hover 規則沒蓋 —— 側欄沒有 .has-sub 也沒有
   .brand-name 元素, 選不到任何東西。
   -------------------------------------------------------------------------- */
@media (min-width: 768px) {
  .sidebar-minified .left-sidebar:hover {
    width: 75px;
    margin-right: 0;
  }

  /* sleek.css:16072-16077 收合態 hover 會把選單文字的 opacity 轉回 1。
     寬度被釘在 75px 之後這些文字會擠在 icon rail 裡, 所以一併關掉。 */
  .sidebar-minified .left-sidebar:hover .nav > li > a > .caret,
  .sidebar-minified .left-sidebar:hover .nav > li > a > span {
    opacity: 0;
  }
}

/* --------------------------------------------------------------------------
   V-4 —— 選單文字的淡入淡出跟著寬度走, 不要再多等

   側欄寬度動畫是 0.3s, 但文字的 opacity 兩個方向都被 sleek 加了 delay,
   看起來就是「側欄縮好了字還在」/「側欄開好了字還沒出來」:

   收合: 按下鈕的那一刻游標必然還在側欄上, 所以套用的是 hover 那條
     (sleek.css:16072-16077) `opacity 0.3s ease-in` + `delay 0.15s`;
     疊上 V-3 強制的 opacity:0, 文字要 0.45s 才消失。
   展開: sleek.css:16234-16244 是 `opacity 0.3s ease-in-out 0.3s` ——
     故意等寬度動畫整個跑完才開始淡入, 合計 0.6s。

   sleek 加 delay 的動機應該是怕文字在動畫中溢出窄側欄, 但 layout.html 的
   #sidebar-body 已經 overflow-x:hidden 會切掉, 不需要等。所以兩邊都把
   delay 歸零並縮短時間: 收合讓文字先走 (0.12s), 展開跟著寬度一起進 (0.18s)。

   prefers-reduced-motion 的 transition:none 在 sleek 裡是寫在這幾條之後的
   獨立 block, 這裡蓋掉之後會把動畫還原給那些使用者, 所以自己再補一次。
   -------------------------------------------------------------------------- */
@media (min-width: 768px) {
  .sidebar-minified .left-sidebar:hover .nav > li > a > .caret,
  .sidebar-minified .left-sidebar:hover .nav > li > a > span {
    transition: opacity 0.12s ease-out;
    transition-delay: 0s;
  }

  .sidebar-minified-out .sidebar .nav > li > a span,
  .sidebar-minified-out .sidebar li > a .caret {
    transition: opacity 0.18s ease-in-out;
  }
}

@media (min-width: 768px) and (prefers-reduced-motion: reduce) {
  .sidebar-minified .left-sidebar:hover .nav > li > a > .caret,
  .sidebar-minified .left-sidebar:hover .nav > li > a > span,
  .sidebar-minified-out .sidebar .nav > li > a span,
  .sidebar-minified-out .sidebar li > a .caret {
    transition: none;
  }
}

/* --------------------------------------------------------------------------
   V-5 —— 展開態側欄收窄

   sleek 的 250px 是給有 .brand-name、有子選單、標籤較長的英文版型用的。
   Hupa 的標籤都是四到六個中文字, 最寬的「AI 模型管理」右緣在 132px, 右側
   有 118px 是純空白。收到 180px: 文字右邊還留 48px, logo 右緣 (101px) 跟
   收合鈕 (64px 寬) 之間也還有 15px。

   三個值必須一起改, 少改一個就會露出縫或壓到內容:
   1. 側欄自己的寬度 (sleek.css:15652-15658)
   2. .page-wrapper 的 padding-left (sleek.css:16317-16322, 原 15.6rem)
   3. .main-header 的 padding-left (sleek.css:16324-16329, 原 15.6rem)
   收合態的 75px 不動 —— .sidebar-minified 那組特異性比這裡高。

   手機 (<768px) 的抽屜寬度也要在這裡明確寫出來。sleek 沒給 .left-sidebar
   基礎寬度, 手機那邊是靠最寬的子元素撐出來的 —— 而那個子元素就是被寫死
   250px 的 .app-brand a (sleek.css:15755)。A1-b 把它改成 width:auto 之後,
   手機抽屜跟著塌成 157px, 跟 .sidebar-mobile-in/out 的 translateX(-250px)
   對不起來。不再靠子元素的副作用決定抽屜寬度。
   -------------------------------------------------------------------------- */
.left-sidebar {
  width: 250px;
}

@media (min-width: 768px) {
  .left-sidebar {
    width: var(--hupa-sidebar-w);
  }

  .sidebar-fixed-offcanvas .page-wrapper,
  .sidebar-fixed .page-wrapper,
  .sidebar-fixed-offcanvas .main-header,
  .sidebar-fixed .main-header,
  .sidebar-static.header-fixed .main-header,
  .sidebar-static-offcanvas.header-fixed .main-header {
    padding-left: var(--hupa-sidebar-w);
  }
}

/* --------------------------------------------------------------------------
   A2 —— navbar 靠右
   原本 navbar-right 是靠 .search-form 的 margin-right:auto 被推到右邊的。
   search-form 整塊拿掉之後要自己撐。
   -------------------------------------------------------------------------- */
.navbar .navbar-right {
  margin-left: auto;
}

/* --------------------------------------------------------------------------
   A6 —— 側欄捲軸
   模板寫了 data-simplebar 但專案沒載 simplebar 套件, 所以那個屬性是死的,
   顯示的是瀏覽器原生捲軸。直接把原生捲軸調細調暗。
   -------------------------------------------------------------------------- */
#sidebar-body {
  scrollbar-width: thin;
  scrollbar-color: var(--hupa-on-dark-scrollbar) transparent;
}

#sidebar-body::-webkit-scrollbar {
  width: 6px;
}

#sidebar-body::-webkit-scrollbar-track {
  background: transparent;
}

#sidebar-body::-webkit-scrollbar-thumb {
  background-color: var(--hupa-on-dark-scrollbar);
  border-radius: 3px;
}

#sidebar-body::-webkit-scrollbar-thumb:hover {
  background-color: var(--hupa-on-dark-scrollbar-hover);
}

/* --------------------------------------------------------------------------
   A7 —— 按鈕收斂
   現況 pill (btn-pill)、方角 (btn)、裸 icon (btn-edit-icon) 三套混用。
   統一成同一個圓角; btn-edit-icon 之前只在 modal-header 底下有定義,
   其他地方是完全沒樣式的原生 <button>, 這裡補上全域定義。

   sleek.css:12732 用 .btn.btn-pill (特異性 0,0,2,0) 定 100px, 單獨寫
   .btn-pill (0,0,1,0) 特異性不夠, 光靠載入順序蓋不掉, 這裡必須用同樣的
   複合選擇器才追得平 —— pill 按鈕最終統一收斂成 8px, 不再是藥丸形狀。
   -------------------------------------------------------------------------- */
.btn,
.btn.btn-pill {
  border-radius: var(--hupa-radius);
}

.btn-primary {
  background-color: var(--hupa-primary);
  border-color: var(--hupa-primary);
}

.btn-primary:hover,
.btn-primary:focus {
  background-color: var(--hupa-primary-hover);
  border-color: var(--hupa-primary-hover);
}

/* sleek.css:14824-14841 在這三個 modal-header 底下另外訂了
   .btn-edit-icon 的 padding: 0 5px (特異性 0,1,2,0), 比這裡的
   0,0,1,0 高, 光靠載入順序蓋不掉, 導致這三個 modal 裡的 padding
   跟其他地方不一致。加上同樣的選擇器把這三處也收進來, 讓 padding
   全站統一。
   M-4: sleek.css 同一組規則也定義了 font-size:1.125rem 和
   color:#8a909d。這裡追平的目的是 padding, color 值本來就跟 sleek 一樣
   (var(--hupa-text-muted) 也是 #8a909d), 在這三個 modal 裡重複宣告純屬
   冗餘, 不是筆誤; font-size 則刻意不追平, 沿用 sleek 的 1.125rem。 */
.btn-edit-icon,
#modal-add-contact .modal-header .btn-edit-icon,
#modal-contact .modal-header .btn-edit-icon,
#modal-add-member .modal-header .btn-edit-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background-color: var(--hupa-icon-btn-bg);
  border: 0;
  padding: 0.35rem;
  margin: 0 0.15rem;
  line-height: 1;
  color: var(--hupa-text-muted) !important;
  border-radius: var(--hupa-radius-sm);
  transition: color 0.15s ease, background-color 0.15s ease;
  cursor: pointer;
  vertical-align: middle;
}

.btn-edit-icon:hover,
.btn-edit-icon:focus {
  color: var(--hupa-primary) !important;
  background-color: var(--hupa-icon-btn-bg-hover);
  outline: none;
}

/* --------------------------------------------------------------------------
   卡片
   -------------------------------------------------------------------------- */
.card {
  border-radius: var(--hupa-radius);
  border-color: var(--hupa-border);
  box-shadow: var(--hupa-shadow-sm);
}

/* --------------------------------------------------------------------------
   表格 —— 全站表格儲存格垂直置中
   -------------------------------------------------------------------------- */
.table td,
.table th {
  vertical-align: middle;
}

/* --------------------------------------------------------------------------
   表單控制項 —— 跟按鈕同一個圓角語彙
   -------------------------------------------------------------------------- */
.form-control {
  border-radius: var(--hupa-radius-sm);
  border-color: var(--hupa-border);
}

.form-control:focus {
  border-color: var(--hupa-primary);
  box-shadow: var(--hupa-focus-ring);
}

/* --------------------------------------------------------------------------
   A4 / A5 —— 頁首
   由 templates/_macros.html 的 page_header macro 產生。
   sleek 的 .breadcrumb-wrapper h1 是 1.63rem/500; 中文字在同字級下比拉丁字母
   視覺重量大得多, 降到 1.5rem 讀起來才跟按鈕、表格平衡。
   -------------------------------------------------------------------------- */
.page-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.5rem;
}

.page-header-main {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  min-width: 0;
}

.page-title {
  color: var(--hupa-heading);
  font-size: 1.5rem;
  font-weight: 500;
  line-height: 1.3;
  margin: 0;
}

.page-header .breadcrumb {
  background: transparent;
  padding: 0;
  margin: 0;
  font-size: 0.8125rem;
}

.page-header .breadcrumb-item a {
  color: var(--hupa-text-muted);
}

.page-header .breadcrumb-item.active {
  color: var(--hupa-heading);
}

.page-header-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-shrink: 0;
}

/* --------------------------------------------------------------------------
   佈設管理 (/deploy)
   原本這一段是 deployment.html 的 inline <style>, 搬過來集中管理。

   I-3: 這三條原本只影響 /deploy 一頁, 搬進這裡之後套用範圍變成全站
   (gallery、explorer、audio、以及所有 modal 裡的 select2)。這是刻意保留的
   擴散, 不是疏忽: select2 的下拉選單 (.select2-dropdown) 是掛在 <body>
   底下的獨立節點, 不是 .select2-container 的子元素, 沒辦法用頁面層級的
   class 把作用範圍收斂回單一頁面, 所以只能全域套用 (已使用者確認)。

   I-3 (續, 真正的迴歸): 下面 max-height:560px 的選擇器
   `.select2-container--default .select2-results > .select2-results__options`
   跟 select2.min.css 裡真正贏的那條
   `.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}`
   逐字比對 (只差可讀性用的空格) 特異性完全相同 (0,3,0)。select2.min.css
   在 layout.html:52 的 {% block head %} 載入, 排在 hupa-theme.css
   (layout.html:30) 之後, 同特異性時後載入的贏 —— 所以搬家前這段
   還沒改動選擇器時, 560px 其實是一條輸掉 cascade 的死規則, /deploy 的
   下拉高度默默變回 200px, 而上一輪的 fix report 誤以為「三條都全站生效」
   就結案了。
   靠順序或靠特異性數字微調都擋不住同分, 這裡在選擇器前面多疊一層 `body`
   (元素選擇器, 特異性 +0,0,0,1), 讓這條變成 (0,3,1), 不管 select2.min.css
   先載或後載都贏, 不需要 !important。z-index 那兩條沒事: 見下方各自的
   特異性比較。 */
body .select2-container--default .select2-results > .select2-results__options {
  max-height: 560px;
  overflow-y: auto;
}

.select2-container .select2-dropdown {
  z-index: 10000;
}

/* --------------------------------------------------------------------------
   共用篩選面板 (.hupa-filters)
   由 templates/_macros.html 的 filter_panel macro 產生, /deploy 與 /explorer
   共用。

   原本這一段叫 .deployment-filters —— 名字綁頁面但功能是通用的。/explorer 要
   套同一套外觀時, 選擇是「複製一份 .explorer-filters」或「改名成共用層」。
   複製會讓兩邊的 padding/gap/字級隨時間漂開, 那正是風格不一致的成因, 所以
   改名。連帶 tests/test_ui_facelift.py 有 4 處寫死舊選擇器的斷言跟著改。

   面板要有底色與 1px 邊: 篩選區跟下方表格同在一張白卡片裡, 沒有分界時 7 個
   欄位看起來像浮在卡片上半部的一片留白。
   -------------------------------------------------------------------------- */
.hupa-filters {
  background-color: var(--hupa-page-bg);
  border: 1px solid var(--hupa-border);
  border-radius: var(--hupa-radius);
  padding: 1.25rem;
  margin-bottom: 1.5rem;
}

/* 欄數固定 (手機 1 欄 / 平板 2 欄 / 桌機 3 欄), 不用 auto-fit。

   第一版是 repeat(auto-fit, minmax(220px, 1fr)), 在 1500px 寬的螢幕上排出 5 欄
   —— 兩頁的可見篩選欄位都剛好是 6 個, 於是變成「第一列 5 個 + 第二列 1 個」,
   第二列右邊空了四格, 比原本 col-md-3 的一個破洞更難看。auto-fit 排得滿的是
   「列」, 不是「格子」。

   6 個欄位在 3 欄下剛好是完整的兩列, 兩頁都一樣。桌機上每格因此有 250px 以上,
   中文標籤與 select2 的箭頭都不擠。欄位數以後變成 7~9 個時, 3 欄仍然只會在
   最後一列留最多兩格, 不會出現一整列的空白。 */
/* 每一格都要用 minmax(0, 1fr) 而不是 1fr。1fr 展開是 minmax(auto, 1fr), 下限是
   內容的 min-content —— /explorer 的 select2 多選會把已選項目的 tag 算進
   min-content, 量出來三欄變成 276/261/251px, 標籤起點對不齊。minmax(0, 1fr)
   把下限壓成 0, 三欄才真的等寬 (溢出的內容由控件自己處理)。 */
.hupa-filters-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1rem 1.25rem;
}

@media (min-width: 576px) {
  .hupa-filters-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 992px) {
  .hupa-filters-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* 列距交給 grid 的 gap, .form-group 自己的 margin 會疊上去變成不等距。 */
.hupa-filters .form-group {
  margin-bottom: 0;
}

/* 舊值是 0.75rem + --hupa-text-muted, 掃視時抓不到欄位名。提到 0.8125rem /
   500 / 標題色 —— 跟 .hupa-field-label (彈窗表單) 是同一組值, 兩處的標籤
   語彙才一致。 */
.hupa-filters .filter-label {
  display: block;
  font-size: 0.8125rem;
  font-weight: 500;
  margin-bottom: 0.375rem;
  color: var(--hupa-heading);
}

.hupa-filters .filter-range {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
}

/* 把面板裡的輸入框拉回 .form-control 的基準字級與 padding。

   這條是 M-8 / Minor-新2 那串推導漏掉的一層, 在瀏覽器上量出來才發現:
   sleek.css:13954 有
     .form-group .form-control, .input-group .form-control {
       font-size: 0.98rem; padding: .5rem 1.06rem;
     }
   而篩選欄位每一個都包在 .form-group 裡, 所以它們的字級不是 .form-control
   基準的 .875rem 而是 .98rem。sleek.css:2025 的高度是
   calc(1.5em + 1.18rem + 2px), 那個 1.5em 相對的是元素自己的 font-size ——
   字級被放大, 高度就跟著變成 1.5×0.98rem + 1.18rem + 2px ≈ 44.4px,
   而 select2 那兩條算的是 1.5×0.875rem + 1.18rem + 2px ≈ 41.9px。
   /deploy 現在的下拉就比旁邊的輸入框矮約 2.5px, 一整排看起來參差。

   兩種修法: 把 select2 改成 44.4px, 或把輸入框拉回基準的 41.9px。選後者ーー
   0.98rem (15.68px) 只有這條 sleek 規則在用, 面板標籤是 0.8125rem、表格內文
   是 0.875rem, 輸入框跟著回到 0.875rem 才是同一套字級, 而且 select2 那兩條
   既有的 calc 不用改。
   特異性: 這裡是 (0,3,0), 贏過 sleek 的 (0,2,0), 不需要 !important。 */
.hupa-filters .form-group .form-control,
.hupa-filters .input-group .form-control {
  font-size: 0.875rem;
  padding: 0.59rem 1rem;
}

/* 動作列 (清除篩選 / 蒐尋) 自成一排靠右。

   B2 的舊做法是讓它佔一個 col-md-3 網格格子, 再用 min-height
   calc(1.125rem + 0.25rem + 1.3125rem + 1.18rem + 2px) 把按鈕壓到跟同列
   輸入框的底線齊高。那個 min-height 的前提是「按鈕跟欄位同列」, 現在動作列
   已經獨立成排, 前提消失, 整條移除 —— 留著會讓動作列莫名高出 42px。 */
.hupa-filters .filter-actions {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 0.5rem;
  margin-top: 1rem;
}

/* select2 產生的 span 高度跟原生 .form-control 對不上, 拉齊才不會參差。

   Minor-新2: 原本這裡抄原廠 Bootstrap 公式 calc(1.5em + 0.75rem + 2px),
   算出來 ≈35px, 跟 sleek.css:2025 覆寫過的 .form-control 實際高度 (≈41.9px)
   差約 7px —— 規則的目的是「對齊避免參差」, 結果反而製造參差。
   sleek.css:2025 整條覆寫了 .form-control:
     height: calc(1.5em + 1.18rem + 2px);
     padding: .59rem 1rem;
     font-size: .875rem;
     line-height: 1.5;
   padding 實際是 1.18rem 不是原廠的 0.75rem; 1.5em 是相對 .form-control 自己
   的 font-size (.875rem) 換算 = 1.3125rem, 不是相對根字級。所以正確的高度是
   calc(1.3125rem + 1.18rem + 2px)。下面兩條共用這個推導, 不要重算。 */
.hupa-filters .select2-container--default .select2-selection--single,
.hupa-modal .select2-container--default .select2-selection--single {
  height: calc(1.3125rem + 1.18rem + 2px);
  border-color: var(--hupa-border);
  border-radius: var(--hupa-radius-sm);
}

/* select2 單選的文字塊 (.select2-selection__rendered) 是 line-height:28px 的獨立
   盒子, 上面那條只把外框拉到 41.88px, 文字仍然貼在框內頂端 (量出來只離上緣
   1px, 下面空 12px), 看起來像沒對齊 —— 這是使用者指出的「下拉選單內容沒對齊
   中間」。line-height 補成「容器高度扣掉上下各 1px 邊框」就會垂直居中。
   左邊 padding 原本是 8px, 旁邊 .form-control 是 16px, 一整排的文字起點對不齊;
   右邊留 2rem 給箭頭, 長字串才不會壓在箭頭底下。
   箭頭本身也是 26px 的獨立盒子 (top:1px), 一樣要拉到扣掉邊框後的全高才會居中。 */
.hupa-filters .select2-container--default .select2-selection--single .select2-selection__rendered,
.hupa-modal .select2-container--default .select2-selection--single .select2-selection__rendered {
  line-height: calc(1.3125rem + 1.18rem);
  padding-left: 1rem;
  padding-right: 2rem;
}

.hupa-filters .select2-container--default .select2-selection--single .select2-selection__arrow,
.hupa-modal .select2-container--default .select2-selection--single .select2-selection__arrow {
  height: calc(1.3125rem + 1.18rem);
}

/* 多選 (/explorer 的團隊/樣站/物種/標籤) 只能用 min-height: 選了幾項就長出
   幾個 tag, 寫死 height 會把第二列 tag 切掉。 */
.hupa-filters .select2-container--default .select2-selection--multiple,
.hupa-modal .select2-container--default .select2-selection--multiple {
  min-height: calc(1.3125rem + 1.18rem + 2px);
  border-color: var(--hupa-border);
  border-radius: var(--hupa-radius-sm);
}

/* 多選的內容 (已選 tag 與輸入游標) 也要垂直居中。select2 的原始做法是給
   .select2-selection__rendered 一點 padding、再給每個 tag 與 inline 搜尋框各自
   margin-top:5px 撐位置 —— 外框被拉高之後那組固定 margin 不會跟著長, 量出來
   placeholder 離上緣 5.7px、離下緣 15.2px, 跟單選一樣是「內容沒對齊中間」。
   改成 flex 置中, tag 之間的距離交給 gap, 原本的 margin 一律歸零。
   左右 padding 用 0.75rem: tag 自己還有 padding, 加起來才跟 .form-control 的
   1rem 文字起點對得上。 */
.hupa-filters .select2-container--default .select2-selection--multiple .select2-selection__rendered,
.hupa-modal .select2-container--default .select2-selection--multiple .select2-selection__rendered {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem;
  padding: 0.25rem 0.75rem;
  margin: 0;
  min-height: calc(1.3125rem + 1.18rem);
}

.hupa-filters .select2-container--default .select2-selection--multiple .select2-selection__choice,
.hupa-modal .select2-container--default .select2-selection--multiple .select2-selection__choice,
.hupa-filters .select2-container--default .select2-selection--multiple .select2-search--inline,
.hupa-modal .select2-container--default .select2-selection--multiple .select2-search--inline,
.hupa-filters .select2-container--default .select2-selection--multiple .select2-search--inline .select2-search__field,
.hupa-modal .select2-container--default .select2-selection--multiple .select2-search--inline .select2-search__field {
  margin: 0;
}

/* 已選項目的 chip。select2 原廠是 #e4e4e4 底 + #aaa 邊, sleek 又把裡面的
   文字染成主色 —— 出來是淺紫底藍字, 一格塞三四個時整片發藍 (使用者實機
   回報「藍色很醜」), 而且藍字看起來像連結, 會讓人以為點了會跳頁。

   改成中性的 chip: 底色與物種 chip (縮圖卡片) 同一階, 文字用標題色, 只有
   移除的 × 在 hover 時轉成 --hupa-danger —— 顏色留給「這一顆要被拿掉」這件
   事, 而不是拿來標示「有東西被選中」(那件事由 chip 本身的存在表達)。

   選擇器多疊一層 body: select2.min.css 在 hupa-theme.css 之後載入。 */
body .hupa-filters .select2-container--default .select2-selection--multiple .select2-selection__choice,
body .hupa-modal .select2-container--default .select2-selection--multiple .select2-selection__choice {
  background-color: var(--hupa-surface-hover);
  border: 1px solid var(--hupa-border);
  border-radius: 999px;
  color: var(--hupa-heading);
  font-size: 0.8125rem;
  line-height: 1.5;
  padding: 0.0625rem 0.5rem;
}

body .hupa-filters .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,
body .hupa-modal .select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
  color: var(--hupa-text-muted);
  margin-right: 0.25rem;
  border: 0;
  background: transparent;
  font-weight: 400;
}

body .hupa-filters .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,
body .hupa-modal .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
  color: var(--hupa-danger);
}

/* allowClear 的「全部清掉」×。原廠是跟 chip 同粗的黑字, 擺在最左邊時看起來
   像第一顆 chip 的一部分; 壓成次要文字色, 只在 hover 時明確起來。 */
body .hupa-filters .select2-container--default .select2-selection--multiple .select2-selection__clear,
body .hupa-modal .select2-container--default .select2-selection--multiple .select2-selection__clear {
  color: var(--hupa-text-muted);
  font-weight: 400;
  margin-right: 0.375rem;
}

body .hupa-filters .select2-container--default .select2-selection--multiple .select2-selection__clear:hover,
body .hupa-modal .select2-container--default .select2-selection--multiple .select2-selection__clear:hover {
  color: var(--hupa-danger);
}

/* select2 的 focus 狀態是 `border: solid black 1px` (select2.min.css 裡的
   `.select2-container--default.select2-container--focus .select2-selection--multiple`,
   特異性 0,3,0)。上面那些規則只有 0,2,0 / 0,3,0, 而 select2.min.css 在
   layout.html 的 {% block head %} 載入, 排在 hupa-theme.css 之後 —— 同分或
   更高就贏, 所以點進欄位的瞬間邊框變成純黑方框, 跟旁邊 .form-control:focus
   的藍框加光暈完全不是同一套。這裡多疊 .hupa-filters (0,4,0) 追過去,
   focus 的樣子跟原生輸入框統一。 */
.hupa-filters .select2-container--default.select2-container--focus .select2-selection--multiple,
.hupa-modal .select2-container--default.select2-container--focus .select2-selection--multiple,
.hupa-filters .select2-container--default.select2-container--focus .select2-selection--single,
.hupa-modal .select2-container--default.select2-container--focus .select2-selection--single,
.hupa-filters .select2-container--default.select2-container--open .select2-selection--multiple,
.hupa-modal .select2-container--default.select2-container--open .select2-selection--multiple,
.hupa-filters .select2-container--default.select2-container--open .select2-selection--single,
.hupa-modal .select2-container--default.select2-container--open .select2-selection--single {
  border-color: var(--hupa-primary);
  box-shadow: var(--hupa-focus-ring);
}

/* 展開時 select2 會把控件朝下拉那一側的圓角壓平
   (`.select2-container--default.select2-container--open.select2-container--below
   .select2-selection--*` 的 border-bottom-*-radius: 0, 特異性 0,4,0), 做出
   「控件跟下拉黏成一塊」的舊式外觀 —— 而下拉自己的圓角是 4px, 控件是 6px,
   接縫兩邊還對不齊。

   這裡改成把下拉當一張浮起來的小卡片 (見下面 .select2-dropdown 那段), 控件
   四角維持 --hupa-radius-sm 不動。多疊一層 body (0,4,1) 才追得過 select2 的
   0,4,0 —— 跟 I-3 那條 max-height 同一個手法, 不需要 !important。 */
body .hupa-filters .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple,
body .hupa-modal .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple,
body .hupa-filters .select2-container--default.select2-container--open.select2-container--below .select2-selection--single,
body .hupa-modal .select2-container--default.select2-container--open.select2-container--below .select2-selection--single,
body .hupa-filters .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple,
body .hupa-modal .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple,
body .hupa-filters .select2-container--default.select2-container--open.select2-container--above .select2-selection--single,
body .hupa-modal .select2-container--default.select2-container--open.select2-container--above .select2-selection--single {
  border-radius: var(--hupa-radius-sm);
}

/* --------------------------------------------------------------------------
   select2 下拉選單 (全站)

   .select2-dropdown 是掛在 <body> 底下的獨立節點, 不是 .select2-container 的
   子元素 —— 沒辦法用頁面或面板層級的 class 把作用範圍收斂回單一頁面, 只能
   全域套用。這跟 I-3 (max-height) 是同一個已知且已確認的擴散: 影響 deploy /
   explorer / gallery / audio 與所有 modal 裡的 select2。往「全站一致」的方向
   走, 所以刻意保留。

   一律多疊一層 body 元素選擇器: select2.min.css 對這些節點的規則特異性跟
   不加 body 時完全相同, 而它排在 hupa-theme.css 之後載入, 同分就輸。
   -------------------------------------------------------------------------- */

/* 原廠是 border: 1px solid #aaa + border-radius: 4px + 沒有陰影, 而且展開時
   只有朝控件那一側是方角。改成一張浮起來的卡片: 四角同 --hupa-radius-sm、
   邊框用主題邊框色、加一層陰影撐出層次, 並跟控件之間留 4px 縫。 */
body .select2-container--default .select2-dropdown {
  border-color: var(--hupa-border);
  border-radius: var(--hupa-radius-sm);
  box-shadow: var(--hupa-shadow);
  overflow: hidden;
  /* 自己留一圈內距 (使用者實機回報選項「貼近方框左邊」): 選項的 hover /
     選中底色是整條寬, 沒有這圈內距時那塊底色會直接壓在外框線上, 看起來像
     選項被框線切掉。有了內距, 底色縮在框裡, 配合下面選項自己的圓角就是
     一顆一顆分開的列。 */
  padding: 0.25rem;
}

body .select2-container--default .select2-dropdown--below {
  margin-top: 4px;
}

body .select2-container--default .select2-dropdown--above {
  margin-bottom: 4px;
}

/* 選項的左內距對齊控件裡的文字起點 (1rem), 原廠是 6px, 一開一關文字會跳。 */
body .select2-container--default .select2-results__option {
  /* 左內距要跟收起來的控件文字起點對齊 (1rem)。上面那圈 0.25rem 已經由
     下拉本體吃掉, 所以這裡是 0.75rem, 兩者相加正好 1rem。 */
  padding: 0.5rem 0.75rem;
  font-size: 0.875rem;
  border-radius: var(--hupa-radius-sm);
}

/* 原廠的 highlighted 是 #5897fb 滿版藍底白字 —— 那是 select2 自己的藍, 不是
   --hupa-primary (#4c84ff), 而且以「鍵盤/滑鼠游標所在」這個弱狀態來說太重。
   改成跟表格列 hover 同一套的淺底深字。真正「已選中」的那一項才用主色。 */
body .select2-container--default .select2-results__option--highlighted[aria-selected],
body .select2-container--default .select2-results__option--highlighted[data-selected] {
  background-color: var(--hupa-surface-hover);
  color: var(--hupa-heading);
}

body .select2-container--default .select2-results__option[aria-selected="true"],
body .select2-container--default .select2-results__option[data-selected="true"] {
  background-color: var(--hupa-page-bg);
  color: var(--hupa-primary);
  font-weight: 500;
}

/* 單選的下拉自帶搜尋框, 原廠是 1px solid #aaa 的方框。 */
body .select2-container--default .select2-search--dropdown {
  padding: 0.5rem;
}

body .select2-container--default .select2-search--dropdown .select2-search__field {
  border: 1px solid var(--hupa-border);
  border-radius: var(--hupa-radius-sm);
  padding: 0.375rem 0.5rem;
  font-size: 0.875rem;
}

body .select2-container--default .select2-search--dropdown .select2-search__field:focus {
  border-color: var(--hupa-primary);
  box-shadow: var(--hupa-focus-ring);
  outline: none;
}

/* B7 —— 表格橫向捲動時把 ID 與管理欄凍在左邊, 不然捲到右邊就不知道在看哪一列。

   Important-新2: 只有 deployment.html 的 #datatable 開了 scrollX: true
   (explorer.html:170、filelist.html:32、site.html:20 的 #datatable 都沒開),
   而 .dataTables_scrollHead / .dataTables_scrollBody 這兩個容器只有開了
   scrollX 才會出現在 DOM 裡。下面的選擇器要求這兩層當祖先, 等於順便把
   sticky 與寬度規則收斂回 /deploy 專屬, 不會外溢到其他三頁的第 1 欄
   (之前外溢的是新加的 width/min-width/max-width 三行; sticky 本身在沒有
   scrollX 的頁面上沒有水平捲動可畫, 但寬度鎖死是實際會看到的破版)。

   C-1: DataTables 的 scrollX 會把可見的 <thead> 整個「搬」進一張
   removeAttr("id") 過的複本 table (放進 div.dataTables_scrollHead), 帶
   id="datatable" 的原表格只剩 <tbody> 留在 div.dataTables_scrollBody 裡
   (見 jquery.datatables.min.js 的 wb() 函式)。_fnScrollDraw 量測欄寬時,
   還會把「已經沒有 id」的那份可見 thead 再 clone 一份、塞回原表格當隱藏的
   零高度量尺 (na() 函式), 讓原表格重新「看起來」有 id="datatable" 的 th ——
   但那份 th 是零高度的量尺, 不是使用者看得到的表頭。
   所以 `#datatable th:nth-child(n)` 命中的其實是這份量尺, 使用者真正看到、
   會捲動的表頭 (在 scrollHead 裡) 完全沒被選到。

   Important-新2 (續): 上一輪改用 `#datatable_wrapper table.dataTable ...`
   (單純 descendant 選擇器) 涵蓋 scrollHead + scrollBody 兩張真表, 但
   jquery.datatables.min.js 的 _fnCalculateColumnWidths 裡還有一段
   `k=f(b).clone().css("visibility","hidden").removeAttr("id")`, 把整張表
   複製一份 (只拿掉 id, class 沒動, 一樣有 dataTable) 拿去量欄寬, 再用
   `f("<div/>").append(k).appendTo(p)` 包一層新的 <div> 塞進
   `p = table.parentNode`, scrollX 下 `p` 正是 div.dataTables_scrollBody ——
   descendant 選擇器連這份量測複本也咬到, 等於用 CSS 干擾 DataTables 自己的
   欄寬演算法。DataTables 產生 scrollHead / scrollFoot 時另外包了一層
   `dataTables_scrollHeadInner` / `dataTables_scrollFootInner` (兩者都有
   "Inner" 版本, 見 sScrollHeadInner/sScrollFootInner), 唯獨 scrollBody 沒有
   對應的 "Inner" class —— 因為 scrollBody 本身就是使用者真正橫向捲動的
   容器, 它的親生 <table> 直接是它的第一層子元素, 不需要再包一層拿去做
   捲動位移的技巧。量測複本額外包的 <div> 因此讓它比親生表格多了一層,
   於是:
     - scrollHead 底下只有一張真表, 沒有量測複本跟它搶, 用 descendant
       選擇器 (`.dataTables_scrollHead table.dataTable`) 安全。
     - scrollBody 底下用「直接子層」選擇器
       (`.dataTables_scrollBody > table.dataTable`), 只咬住親生表格,
       量測複本因為多包一層 <div> 摸不到。
   兩條合起來涵蓋使用者看到的 scrollHead 表頭與 scrollBody 表身, 又排除
   DataTables 自己拿來量欄寬的隱藏複本。

   C-2: `left` 不能假設第 1 欄 (ID) 剛好是 3.5rem 寬。
   datatables.bootstrap4.min.css 的
   `table.dataTable td,table.dataTable th{box-sizing:content-box}`
   (特異性 0,1,1) 贏過 Bootstrap 的 `*{box-sizing:border-box}`, 所以下面的
   width/min-width/max-width 釘的是內容寬; 表頭那格還要另外加
   sleek.css:1654 `.table th{padding:0.75rem}` 的 padding, 以及可排序欄
   (deployment 的 order:[[0,'desc']], 第 1 欄可排序) th.sorting_desc 額外
   留給排序箭頭的 30px —— 兩邊加起來至少 12+56+30=98px, 跟 3.5rem(56px)
   對不上, 純 CSS 沒有辦法讓兩個數字保證閉合。改成在 deployment.html 用 JS
   量出第 1 欄實際渲染寬度, 寫進 `--deploy-col1` 這個 CSS 變數, 第 2 欄的
   `left` 直接讀這個變數 —— 兩邊保證是同一個來源算出來的同一個數字, 不再是
   CSS 這邊寫一次、JS columnDefs 那邊猜一次的兩份各自維護的魔術數字。
   fallback 的 3.5rem 只在 `--deploy-col1` 還沒被 JS 設定時 (例如 JS 被停用)
   頂著用, 不影響「兩邊同源」這個保證。 */
#datatable_wrapper .dataTables_scrollHead table.dataTable th:nth-child(1),
#datatable_wrapper .dataTables_scrollBody > table.dataTable td:nth-child(1) {
  position: sticky;
  left: 0;
  z-index: 2;
  background-color: var(--hupa-surface);
  width: 3.5rem;
  min-width: 3.5rem;
  max-width: 3.5rem;
}

/* 操作欄改放最後一欄, 用 sticky-right 凍在右邊: 拉到最右仍然看得到操作
   按鈕, 跟其他管理頁「操作固定在右側」的排版一致。第 1 欄 (ID) 保持
   sticky-left, 讓橫向捲動時仍然對得回是哪一列。 */
#datatable_wrapper .dataTables_scrollHead table.dataTable th:last-child,
#datatable_wrapper .dataTables_scrollBody > table.dataTable td:last-child {
  position: sticky;
  right: 0;
  z-index: 2;
  background-color: var(--hupa-surface);
}

#datatable_wrapper .dataTables_scrollHead table.dataTable th:last-child::before,
#datatable_wrapper .dataTables_scrollBody > table.dataTable td:last-child::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  width: 1px;
  background-color: var(--hupa-border);
}

/* 沒開 scrollX 的管理頁: 把「操作」欄凍在右邊。

   /deploy 是 scrollX: true, 走上面 B7 那組 (.dataTables_scrollHead /
   .dataTables_scrollBody) 的規則; 其他管理頁的橫向捲動發生在
   `.basic-data-table .dataTables_wrapper > .row > .col-sm-12` (見下面
   「表格自己在欄位裡橫向捲動」那一段), 所以要另外一組選擇器。

   opt-in 而不是直接吃 .basic-data-table: 不是每一頁的最後一欄都是操作 ——
   explorer 是標籤、oi 是合計、taxa 是學名, 凍住那些只會擋住內容。

   `> table` 是必要的, 不能寫成 descendant: DataTables 的
   _fnCalculateColumnWidths 會 clone 整張表 (只 removeAttr("id"), class 照留)
   包一層 <div> 塞回 table.parentNode 拿去量欄寬。真表格是 col-sm-12 的直接
   子層, 複本多包一層 div, 用直接子層選擇器才不會把 CSS 套到那份量尺上去干擾
   DataTables 自己的欄寬計算 (與 B7 段 scrollBody 的理由相同)。 */
.basic-data-table.freeze-actions .dataTables_wrapper > .row > .col-sm-12:not([class*="col-md"]) > table.dataTable th:last-child,
.basic-data-table.freeze-actions .dataTables_wrapper > .row > .col-sm-12:not([class*="col-md"]) > table.dataTable td:last-child {
  position: sticky;
  right: 0;
  z-index: 2;
  background-color: var(--hupa-surface);
}

/* 整列 hover 是全站規則, 不是某幾張表的選配。

   舊狀態: DataTables 的 hover 來自 `table.dataTable.hover tbody tr:hover`
   (datatables.bootstrap4.css), 只有模板記得在 <table> 上寫 hover / table-hover
   的那幾張才有 —— /project、/deploy、/device 有, 而 /customtaxa、/projectedit、
   /oi、/trash、/subscription 的表沒有。同一個站裡有些表滑過去會亮、有些不會
   (使用者實機回報「應該都要套用到各版面」)。

   底色下在 td 而不是 tr: .table-striped 的斑馬列是設在 tr 上, 下在 tr 會被
   斑馬列蓋掉 (同特異性, 而 sleek 的 striped 後載)。

   #oi-heatmap 例外: 那張表的每一格底色就是資料本身 (OI 值的熱度), 蓋掉會
   讓滑過去的那一列讀不出數值。 */
body .table tbody tr:hover > td {
  background-color: var(--hupa-datatable-row-hover);
}

body #oi-heatmap tbody tr:hover > td {
  background-color: inherit;
}

/* 凍結的格子有自己的底色, 會蓋掉 DataTables 的整列 hover。不補這條的話滑過
   去只有操作欄留白, 看起來像跟其他欄位錯開了。值收在 :root 的
   --hupa-datatable-row-hover。 */
.basic-data-table.freeze-actions .dataTables_wrapper > .row > .col-sm-12:not([class*="col-md"]) > table.dataTable tbody tr:hover > td:last-child {
  background-color: var(--hupa-datatable-row-hover);
}

/* 凍結欄與被它蓋過去的內容之間要有一條界線, 否則捲動時會像貼在一起。 */
.basic-data-table.freeze-actions .dataTables_wrapper > .row > .col-sm-12:not([class*="col-md"]) > table.dataTable th:last-child::before,
.basic-data-table.freeze-actions .dataTables_wrapper > .row > .col-sm-12:not([class*="col-md"]) > table.dataTable td:last-child::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  width: 1px;
  background-color: var(--hupa-border);
}

.dataTables_scrollBody::-webkit-scrollbar {
  height: 8px;
}

.dataTables_scrollBody::-webkit-scrollbar-thumb {
  background-color: var(--hupa-border);
  border-radius: 4px;
}

/* --------------------------------------------------------------------------
   影像瀏覽 (/gallery)
   工具列原本被 gallery.py 用 top_banner 塞進 layout 的 <nav> 裡, 把 header
   擠爆。搬到 content 之後改成 sticky, 捲動時一樣跟著在上面。
   top 用 layout.html 那段 JS 量出來的 --header-offset, 不要寫死。
   -------------------------------------------------------------------------- */
.gallery-toolbar {
  position: sticky;
  top: var(--header-offset);
  z-index: 900;
  background-color: var(--hupa-surface);
  border: 1px solid var(--hupa-border);
  border-radius: var(--hupa-radius);
  box-shadow: var(--hupa-shadow-sm);
  padding: 0.75rem 1rem;
  margin-bottom: 1rem;
}

.gallery-toolbar-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.75rem;
}

.gallery-toolbar-row + .gallery-toolbar-row {
  margin-top: 0.75rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--hupa-border);
}

/* 工具列的分組。原本兩列各是一長排平鋪的控件 (分頁那列 8 個、動作那列 9 個),
   每顆的尺寸、顏色、有沒有圖示都不一樣 —— 掃視時分不出哪些是篩選、哪些會改
   資料 (使用者實機回報「想要有一致性、整齊」)。

   分組之後每一段之間畫一條細線: 組內 gap 小 (0.375rem)、組間 padding 大
   (0.75rem), 用間距與線條表達層級, 不靠顏色。 */
.gallery-toolbar-group {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.375rem;
}

.gallery-toolbar-group + .gallery-toolbar-group {
  padding-left: 0.75rem;
  margin-left: 0.375rem;
  border-left: 1px solid var(--hupa-border);
}

/* 工具列裡所有控件拉成同一個高度。.form-control-sm 是 31px, 圖示鈕原本是
   1.8rem (28.8px) 加 24px 的圖示, 兩者並排會高低參差。 */
.gallery-toolbar .btn-edit-icon {
  width: 1.9375rem;
  height: 1.9375rem;
}

.gallery-toolbar .btn-edit-icon .mdi {
  font-size: 1.125rem;
  line-height: 1;
}

/* 字級。原本標籤 0.75rem、按鈕吃 .btn-sm 的預設, 在 1080p 以上的螢幕上偏小
   (使用者實機回報兩次)。整條工具列拉到 0.8125 / 0.875rem —— 0.875rem 就是
   站上表格內文的字級, 工具列跟表格因此同一個尺度。 */
.gallery-toolbar .gallery-filter,
.gallery-toolbar .gallery-pager-range,
.gallery-toolbar .gallery-pager-meta,
.gallery-toolbar .gallery-size-label {
  font-size: 0.8125rem;
}

.gallery-toolbar .btn-sm,
.gallery-toolbar .form-control-sm,
.gallery-toolbar .gallery-filter .form-control {
  font-size: 0.875rem;
}

.gallery-toolbar .btn-sm {
  padding: 0.3125rem 0.625rem;
}

/* 按鈕裡的圖示: 統一大小與跟文字的距離, 不再靠各自的 mr-1 utility。 */
.gallery-toolbar .btn .mdi {
  font-size: 1rem;
  line-height: 1;
  margin-right: 0.25rem;
  vertical-align: -0.125em;
}

/* 分頁的頁碼/筆數輸入框: 三個框寬度一致, 數字居中。 */
.gallery-pager-range .form-control,
.gallery-pager-meta .form-control {
  width: 3.5rem;
  text-align: center;
}

.gallery-size {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0 auto;
  min-width: 12rem;
}

.gallery-size-label {
  font-size: 0.75rem;
  color: var(--hupa-text-muted);
  white-space: nowrap;
}

.gallery-size input[type='range'] {
  flex: 1 1 auto;
}

/* 分頁列 —— 原本靠無效的 grid class 撐版, 改用 flex。 */
/* .hupa-pager 是同一份規則的共用名。gallery.html 的 pager 是 JS 拼字串產生的
   (gallery.html:1734-1742), 改它的 class 名要動 9 行字串; /explorer 需要的是
   一模一樣的外觀。所以這裡用並列選擇器讓兩個名字共用同一份宣告 —— gallery
   一行不用改, explorer 掛 .hupa-pager 就得到相同結果, 而且值只有一處可改。
   新頁面請用 .hupa-pager, .gallery-pager 保留是為了不動 gallery。 */
.hupa-pager,
.gallery-pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem;
}

.hupa-pager-range,
.hupa-pager-meta,
.gallery-pager-range,
.gallery-pager-meta {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.8125rem;
  color: var(--hupa-text-muted);
  white-space: nowrap;
}

.hupa-pager-range .form-control,
.hupa-pager-meta .form-control,
.gallery-pager-range .form-control,
.gallery-pager-meta .form-control {
  width: 4.5rem;
  text-align: center;
}

.hupa-pager-meta,
.gallery-pager-meta {
  margin-left: 0.5rem;
}

/* 每頁筆數的下拉比頁碼輸入框寬一點才裝得下三位數 + 箭頭。 */
.hupa-pager-meta select.form-control {
  width: 5.5rem;
}

/* pager 要能把兩段 (分頁控制 / 筆數摘要) 推到兩端。 */
.hupa-pager {
  justify-content: flex-start;
  width: 100%;
}

.hupa-pager .hupa-pager-meta {
  margin-left: auto;
}

/* --------------------------------------------------------------------------
   C2 / C3 —— gallery 工具列元件 (篩選下拉、清空按鈕)
   -------------------------------------------------------------------------- */
.gallery-filter {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  margin: 0;
  font-size: 0.75rem;
  color: var(--hupa-text-muted);
  white-space: nowrap;
}

.gallery-filter .form-control {
  min-width: 8rem;
  font-size: 0.8125rem;
}

/* 「清空」跟「全選」是一對動作, 被 margin-left:auto 推到工具列最右邊的話
   會看起來像不相干的東西。字級交給 .btn-sm, 這裡只留不換行。 */
.gallery-clear {
  white-space: nowrap;
}

/* 選取數的角標。原本是 .badge-pill 的左右內距 + 999px 半徑, 一位數時左右
   內距比字寬還大, 出來是一顆橢圓 (使用者實機回報「清空 0 的圓變形了」)。
   鎖成正圓, 兩位數以上才靠 min-width 往兩側長成藥丸 —— 跟縮圖上的註記
   角標同一個處理。 */
.gallery-clear #selcounts {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.25rem;
  height: 1.25rem;
  padding: 0 0.3125rem;
  font-size: 0.75rem;
  line-height: 1;
  vertical-align: -0.125em;
}

.gallery-groups {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 0.75rem;
  width: 100%;
}

/* --------------------------------------------------------------------------
   C4 / C5 —— 影像卡片
   .thumbnail-media 是給以後重構卡片標記用的 class, 現在 DOM 裡還不存在;
   .thumbnail > svg 是目前 gallery.html 五處拼字串實際產生的結構, 不用多包
   一層容器就能吃到固定比例。兩個選擇器共用同一組規則, 將來換掉 svg 版標記
   時 CSS 不用跟著改。
   -------------------------------------------------------------------------- */
.thumbnail {
  /* border-radius 已經由通用的 .card 規則 (本檔案上方) 提供, 這裡不重複寫;
     只補 .card 沒有的 overflow: hidden, 讓內部方形的 svg 縮圖被圓角裁切。
     position: relative 是給浮在照片上的註記列 (.mediataglist) 當定位基準。

     底色改白 (使用者實機回報「黑框太醜」): 卡片帶 .p-1 (縮圖之間的間隙靠
     它撐), 底色是黑的時候那 4px 內距就變成照片外面一圈黑框。黑色現在只掛在
     <svg> 自己身上 (見下一條), 補邊該黑的地方還是黑的。 */
  overflow: hidden;
  position: relative;
  background-color: var(--hupa-surface);
}

/* I-1: object-fit 只對 replaced element 生效, inline <svg> 不是 replaced
   element —— gallery.html 拼出來的 <svg> 內容縮放是靠 svg 預設的
   preserveAspectRatio="xMidYMid meet" (等比縮放置中, 四周補底色), object-fit
   在這裡完全不生效, 寫上去只是誤導。aspect-ratio 本身有效, 靠它固定外框
   讓卡片高度對齊; 圖片本身是留黑邊而不是裁切 (使用者已裁決走這個效果)。
   不要加 preserveAspectRatio="slice" —— 那會連 <rect class="label"> 標註框
   一起裁掉。
   I-2: background-color 用 inherit 取代寫死的 #000, 讓
   .thumbnail.slaveframe (gallery.html:25, 標示同一支影片的後續幀, 底色
   #555) 能透過父層分流出不同底色; 寫死 #000 會把 slaveframe 的視覺線索蓋掉。 */
.thumbnail .thumbnail-media,
.thumbnail > svg {
  display: block;
  width: 100%;
  /* 這裡的 16/9 只是備援。真正生效的是每張 svg 自己 inline 的
     `aspect-ratio: <原生寬> / <原生高>` (gallery.html 的三處卡片字串) ——
     外框比例等於照片自己的比例, 就完全不會有補邊。

     一路的演進: 4/3 (上下兩條很寬的黑帶) → 16/9 (16:9 的照片沒事, 但相機
     陷阱實際上混著 4:3、2:1 等比例, 那些照片照樣有黑邊, 使用者回報「黑邊
     還是很粗」) → 每張用自己的比例。

     代價是同一列的卡片高度會隨照片比例不同而不齊。這是刻意的取捨: 一列裡
     比例不同的照片本來就少 (同一台相機的照片比例一致), 而黑邊是每一張都看
     得到。備援值留著給 imgs 裡沒有 width/height 的異常資料 (0 會讓
     aspect-ratio 失效, 那時至少還有一個固定外框, 不會塌成 0 高)。

     仍然是補邊而不是裁切: preserveAspectRatio 的 slice 會把 <rect class="label">
     標註框一起裁掉 (見上方註解)。 */
  aspect-ratio: 16 / 9;
  /* 補邊的底色掛在 svg 自己身上, 不再 inherit 卡片底色 —— 卡片已經是白的。
     slaveframe (同一支影片的後續幀) 的線索改由下面那條分流。 */
  background-color: var(--hupa-letterbox);
}

.thumbnail.slaveframe .thumbnail-media,
.thumbnail.slaveframe > svg {
  background-color: var(--hupa-letterbox-alt);
}

/* 資訊區改回淺底 (使用者實機回報: 整張卡片黑底不好看)。

   上一版是 background-color: inherit —— 資訊區跟著 .thumbnail 的 letterbox
   一起變成 #000 (slaveframe 是 #555), 於是每一張縮圖底下都掛一塊黑條, 一頁
   幾十張排下來整片發黑, 而且卡片邊界跟頁面白底的對比比縮圖本身還強, 視線先
   被黑條抓走而不是照片。

   黑色只留在 letterbox (照片等比縮放後補的邊, 那裡本來就該深色, 才不會跟
   照片本身搶亮度)。資訊區走 --hupa-surface, 跟站上其他卡片同一個底色, 上緣
   用 1px --hupa-border 跟 letterbox 斷開 —— 深色照片直接貼白底時, 沒有這條
   線會看不出照片在哪裡結束。

   slaveframe (同一支影片的後續幀) 的視覺線索因此只剩 letterbox 的 #555,
   那條線索本來也就在 letterbox 上, 不受影響。 */
.thumbnail .card-body {
  background-color: var(--hupa-surface);
  color: var(--hupa-heading);
  border-top: 1px solid var(--hupa-border);
  /* 資訊區 (時間 / 序號 / 下載 / 結果 / 收藏) 貼齊卡片下緣。

     每張縮圖的外框比例是照片自己的原生比例 (見上面那條規則的註解), 所以
     4:3 的照片跟 16:9 的影片放同一列時影像區高度不同; 資訊區跟著影像走的話,
     同一列的時間列與結果列各自在不同高度 (使用者實機回報「大小不一, 不好看」)。
     把影像裁成同一比例會切掉畫面、補邊會把黑邊帶回來, 兩個都被否決 —— 改成
     高度差全部吃在影像下方的留白, 底下的資訊區對齊。

     .thumbnail 在 #gallery.row 裡是 align-items: stretch 的 flex item, 同一列
     本來就等高; 缺的只是把資訊區推到底。flex: 0 0 auto 是必要的 —— bootstrap
     的 .card-body 預設 flex: 1 1 auto, 會把剩餘空間吃光, 只寫 margin-top: auto
     不會有任何效果 (沒有剩餘空間可推)。 */
  flex: 0 0 auto;
  margin-top: auto;
}

.thumbnail .card-text {
  font-size: 0.875rem;
  color: var(--hupa-text-muted);
  margin-bottom: 0.375rem;
  font-variant-numeric: tabular-nums;
}

/* 註記列浮到照片上。原本它是 svg 上方的一個 block, 於是在照片之外多撐出一條
   黑帶專門放那顆「加註記」圖示 —— 卡片因此有三段 (黑帶 / 照片 / 資訊區),
   而黑帶裡九成是空白。改成絕對定位疊在照片左上角, 卡片回到兩段。

   圖示原本靠 .text-light 在黑帶上才看得見, 疊到照片上之後底下可能是任何亮度,
   所以自己帶一塊半透明深色底 —— 這也是它可以點的提示。 */
.thumbnail .mediataglist {
  position: absolute;
  top: 0.375rem;
  left: 0.375rem;
  right: 0.375rem;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem;
}

.thumbnail .mediataglist .mediatag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border: 0;
  /* 角標是 position: absolute, 要以這顆鈕為基準 */
  position: relative;
  border-radius: var(--hupa-radius-sm);
  background-color: var(--hupa-scrim);
  color: var(--hupa-on-dark);
  cursor: pointer;
}

.thumbnail .mediataglist .mediatag:hover {
  background-color: var(--hupa-scrim-strong);
}

/* 已經有註記的縮圖改成實心黃 (使用者裁決): 半透明深底那顆是「可以加註記」的
   入口, 兩種狀態長一樣的話, 掃一頁縮圖看不出哪幾張被註記過 —— 而那正是註記
   的用途。黃色沿用 sleek 的 --warning (註記 chip 也是這個黃), 圖示與角標換成
   深色才讀得出來。 */
.thumbnail .mediataglist .mediatag.has-tags {
  background-color: var(--hupa-warning);
  color: var(--hupa-heading);
  /* 有註記時鈕裡多一段 .mediatag-text (gallery.html renderMediaTagToggle
     直接把第一筆註記顯示在圖上)。基礎 .mediatag 是鎖死的 1.75rem 方框, 那個
     寬度只夠放圖示 —— 文字會溢出黃底右邊, 變成「黃底沒把字蓋滿」(使用者實機
     回報)。有文字的狀態改成隨內容長的膠囊: 寬度放開、補左右內距、圖示與文字
     之間留 gap。min-height 沿用原本的 1.75rem 讓沒註記/有註記兩顆一樣高。 */
  width: auto;
  min-width: 1.75rem;
  height: auto;
  min-height: 1.75rem;
  padding: 0 0.4375rem;
  gap: 0.25rem;
  max-width: 100%;
}

/* 疊在照片上的第一筆註記文字。單行截尾, 上限 9rem —— 註記本文可以很長, 這一列
   又疊在照片上, 讓它自由長就會橫跨整張照片。多筆時右上角標 (.icon-count-badge)
   給數字, 全部內容在點開的浮層裡。 */
.thumbnail .mediataglist .mediatag .mediatag-text {
  max-width: 9rem;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 0.8125rem;
  line-height: 1.2;
}

.thumbnail .mediataglist .mediatag.has-tags:hover {
  background-color: var(--hupa-warning);
  filter: brightness(0.94);
}

.thumbnail .mediataglist .mediatag.has-tags .icon-count-badge {
  background-color: var(--hupa-heading);
  color: var(--hupa-on-dark);
}

/* 愛心 hover 要跟旁邊的下載鈕一樣有底色 (使用者實機回報)。愛心的尺寸與
   顏色宣告在 gallery.html 的 <style> 裡 (只有那一頁用), 但 hover 的底色是
   「這一列的圖示鈕手感要一致」的規則, 跟下載鈕寫在一起才不會又漂開。
   卡片資訊區是白底, 所以用 --hupa-surface-hover 那一階。 */
/* 精選 (愛心) 靠右 —— 使用者裁決放卡片右下角。序號/下載/物種是「這張是什麼」
   的資訊, 愛心是「我對它做了什麼」, 推到另一端才不會混在同一串裡。 */
.media-meta-row .favorite-toggle {
  margin-left: auto;
  border-radius: var(--hupa-radius-sm);
  transition: color 0.15s ease, background-color 0.15s ease;
}

.media-meta-row .favorite-toggle:hover,
.media-meta-row .favorite-toggle:focus {
  background-color: var(--hupa-surface-hover);
}

/* 彈窗控制列是深色底 (影像編輯器), 淺灰底 + 次要文字色在上面讀不出來。 */
.modal-media-actions .media-sn {
  background-color: var(--hupa-on-dark-divider);
  color: var(--hupa-on-dark-muted);
}

/* 下載原始檔。跟愛心 (.favorite-toggle, gallery.html 的 <style>) 同一組尺寸
   與 hover 手感, 兩顆並排時大小要一致; 顏色平時是次要文字色, hover 才上主色
   —— 它不是這張卡片的主要動作。 */
.media-download {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.8rem;
  height: 1.8rem;
  border-radius: var(--hupa-radius-sm);
  color: var(--hupa-text-muted);
  transition: color 0.15s ease, background-color 0.15s ease;
}

.media-download:hover,
.media-download:focus {
  color: var(--hupa-primary);
  background-color: var(--hupa-surface-hover);
  text-decoration: none;
}

/* 彈窗標頭那一排是深色底 (影像編輯器), 次要文字色在上面看不清楚。 */
#modal-control-panel .media-download,
.modal .media-download {
  color: var(--hupa-on-dark-muted);
}

#modal-control-panel .media-download:hover,
.modal .media-download:hover {
  color: var(--hupa-on-dark);
  background-color: var(--hupa-scrim);
}

/* 註記 chip。結構是「文字 + 移除鈕 (×)」—— 舊版整顆 chip 就是一顆
   .btn-warning, 點下去跳確認框才能刪, 而且看不出它可以刪。 */
.media-tag-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.125rem;
  margin: 0;
  padding: 0.1875rem 0.25rem 0.1875rem 0.5rem;
  background-color: var(--hupa-warning);
  color: var(--hupa-heading);
  border-radius: 999px;
  font-size: 0.8125rem;
  line-height: 1.5;
}

.media-tag-chip-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.125rem;
  height: 1.125rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background-color: transparent;
  color: var(--hupa-heading);
  cursor: pointer;
  line-height: 1;
}

.media-tag-chip-remove .mdi {
  font-size: 0.875rem;
  line-height: 1;
}

/* hover 才轉紅: 平常那顆 × 只是個提示, 不需要一直喊「這裡可以刪」。 */
.media-tag-chip-remove:hover,
.media-tag-chip-remove:focus {
  background-color: var(--hupa-danger);
  color: var(--hupa-on-dark);
  outline: none;
}

/* .media-meta-row 原本只在 gallery.html 的 <style> 區塊裡定義, 現在搬進
   theme 檔案統一管理; 順便補上從來沒定義過的 .badge / .speciesbuttons 排版。 */
.media-meta-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.375rem;
}

.media-meta-row .badge {
  font-weight: 500;
}

.media-meta-row .speciesbuttons {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
}

/* 物種是 .btn.btn-secondary (深灰實心鈕) —— 它們不是動作, 點下去沒有任何事
   發生, 只是「這張照片被標到什麼」的標示。實心深灰在一列裡有三四個時比照片
   還搶眼, 所以降成淺底藥丸 chip: 底色跟表格 hover 同一階, 文字用標題色。 */
.media-meta-row .speciesbuttons .btn {
  background-color: var(--hupa-surface-hover);
  border-color: var(--hupa-border);
  color: var(--hupa-heading);
  font-size: 0.875rem;
  line-height: 1.5;
  padding: 0.25rem 0.6875rem;
  border-radius: 999px;
  box-shadow: none;
}

/* 序號 (.sn)。這個數字是「這一頁的第幾筆」(gallery.html 的 initgallery 傳的是
   index + page_first), 不是標記數 —— 舊的 title 寫「標記數」是錯的, 一併改掉。

   外觀從藍色實心藥丸改成跟圖示鈕 (.btn-edit-icon) 同一套語彙: 灰底、
   --hupa-radius-sm 的方框、次要文字色。理由是它跟愛心、下載那兩顆並排,
   而它不是動作也不是狀態, 只是一個編號 —— 用主色實心圓會讓它變成那一列
   最重的東西 (使用者實機回報, 要求「方框 + 數字, 跟 icon 風格一致」)。 */
.media-sn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.8rem;
  height: 1.8rem;
  padding: 0 0.375rem;
  background-color: var(--hupa-icon-btn-bg);
  border-radius: var(--hupa-radius-sm);
  color: var(--hupa-text-muted);
  font-size: 0.875rem;
  font-weight: 500;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   表單彈窗 —— .hupa-modal 統一版型
   舊版每個 modal 都是「左側 col-sm-4 標籤 + 右側 col-sm-8 輸入框」的
   .form-group.row, 外加 .mb-6 (sleek.css:8341, 4.5rem) 當列距。中文標籤只有
   三四個字, 左欄卻固定佔 1/3 寬, 於是標籤與輸入框之間拉出一大段空白, 列與列
   之間又空了 4.5rem —— modal-lg 裡放兩個欄位就變成一片留白。而且沒有
   modal-header, 使用者看不到自己在編輯什麼, 也沒有右上角的關閉鈕。

   新版把標籤改成置於輸入框上方 (跟 .deployment-filters .filter-label 同一套
   語彙), 欄位排在 .hupa-form-grid 的兩欄網格上, 短欄位 (經緯度、廠牌/型號)
   用 .hupa-field--half 併成一列。列距交給 grid 的 gap, 模板裡不再需要
   .mb-6 / .form-group.row / col-* 那一整套。

   選擇器一律用 .hupa-modal 前綴: sleek.css 對 .modal-header / .modal-footer /
   .close 都有自己的宣告, 加一層 class 才有足夠特異性, 同時不影響還沒改版的
   彈窗 (影像檢視、上傳對話框)。
   -------------------------------------------------------------------------- */
.hupa-modal .modal-content {
  border: 0;
  border-radius: var(--hupa-radius);
  box-shadow: var(--hupa-shadow);
}

.hupa-modal .modal-header,
.hupa-modal .modal-footer {
  border-color: var(--hupa-border);
}

.hupa-modal .modal-header {
  align-items: center;
  padding: 1.125rem 1.5rem;
}

.hupa-modal .modal-title {
  margin: 0;
  /* 頁首標題是 1.5rem (見 .page-header .page-title), 彈窗標題比它低一階,
     跟表格標頭同一個重量。 */
  font-size: 1.0625rem;
  font-weight: 500;
  line-height: 1.4;
  color: var(--hupa-heading);
}

/* bootstrap 的 .close 是 float:right + 負 margin 撐開點擊區; 這裡改用
   flex 的 margin-left:auto 靠右, 才不會在 flex 容器裡跟標題疊在一起。
   text-shadow / opacity 是 bootstrap 原廠給白底大 × 用的, 在這個字級下
   看起來髒, 一併關掉。 */
.hupa-modal .modal-header .close {
  float: none;
  padding: 0.25rem 0.5rem;
  margin: -0.25rem -0.5rem -0.25rem auto;
  font-size: 1.5rem;
  font-weight: 400;
  line-height: 1;
  color: var(--hupa-text-muted);
  text-shadow: none;
  opacity: 1;
}

.hupa-modal .modal-header .close:hover,
.hupa-modal .modal-header .close:focus {
  color: var(--hupa-heading);
  opacity: 1;
  outline: none;
}

.hupa-modal .modal-body {
  padding: 1.5rem;
}

/* 欄位多的彈窗 (樣站) 在筆電螢幕上會比視窗還高 —— modal-dialog-centered 會把
   超出的部分推到視窗上緣外, 標題就看不到了。內容區自己捲, 標題列與底部按鈕
   留在畫面上。3.5rem 是 .modal-dialog 的上下 margin (1.75rem × 2)。

   只加在明確標了 .hupa-modal--scroll 的彈窗上, 不要變成 .hupa-modal 的通則:
   overflow 會建立裁切區, 而 bootstrap-datetimepicker 的日期面板是插在
   .input-group.date 裡面的絕對定位節點 (佈設、檔案管理的時間欄位), 一旦父層
   可捲動就會被裁掉。有日期選擇器的彈窗一律不掛這個 class。 */
.hupa-modal--scroll .modal-content {
  max-height: calc(100vh - 3.5rem);
  overflow: hidden;
}

/* 這些彈窗的 header/body/footer 是包在一個 <form> 裡的, 所以 .modal-content
   的唯一 flex 子項是那個 form。bootstrap 只讓 .modal-body 可伸縮, form 自己
   是普通 block —— 高度限制到 .modal-content 就斷了, 內容照樣把 form 撐到
   797px 再被 overflow:hidden 裁掉 (底部按鈕直接看不到)。把 form 也變成
   flex column 並允許縮, 高度限制才傳得下去。 */
.hupa-modal--scroll .modal-content > form {
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}

/* .modal-content 是 flex column, 但 flex item 的 min-height 預設是 auto ——
   內容區不會縮到比自己的內容矮, 於是 max-height 只是把 .modal-content 的框
   畫小, 欄位照樣溢出去、外層 .modal 產生捲軸 (視覺上就是標題被推出畫面上緣)。
   min-height:0 才讓它真的可以縮, overflow-y 也才接得上。 */
.hupa-modal--scroll .modal-body {
  min-height: 0;
  overflow-y: auto;
}

.hupa-modal .modal-footer {
  padding: 1rem 1.5rem;
}

/* 刪除是破壞性動作, 推到最左邊跟「取消/儲存」隔開, 不要並排在同一叢按鈕裡
   讓人誤按。用 margin-right:auto 而不是換容器, 這樣各頁 JS 現有的
   $('#deletebutton').show()/.hide() 不用動。 */
.hupa-modal .modal-footer > .hupa-modal-delete {
  margin-right: auto;
}

.hupa-form-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.125rem 1.25rem;
}

/* 預設整列 —— 併成半欄要自己加 .hupa-field--half, 這樣新增欄位時的預設
   行為是安全的 (寬一點), 不會突然跟隔壁欄位擠在一起。
   min-width:0 是給 grid item 的必備解: select2 產生的 span 會用內容寬度
   撐爆網格欄, 沒有這行就會橫向溢出。 */
.hupa-field {
  grid-column: 1 / -1;
  min-width: 0;
}

.hupa-field--half {
  grid-column: span 1;
}

/* 手機上兩欄擠不下, 半欄一律攤成整列。575.98px 是 bootstrap 的 sm
   斷點上界, 沿用同一個門檻。 */
@media (max-width: 575.98px) {
  .hupa-field--half {
    grid-column: 1 / -1;
  }
}

.hupa-field-label {
  display: block;
  margin-bottom: 0.375rem;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--hupa-text-muted);
}

/* 標籤那一行右邊要塞按鈕時用 (樣站的「匯入行政區及海拔」)。原本那顆鈕是
   包在 <label> 裡面的, 點文字會觸發 label 的 for 目標, 語意也不對。 */
.hupa-field-label-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.375rem;
}

.hupa-field-label-row .hupa-field-label {
  margin-bottom: 0;
}

.hupa-field-label-row .btn {
  padding: 0.25rem 0.625rem;
  font-size: 0.8125rem;
}

/* 同一個欄位裡疊多個控制項 (行政區三層下拉) 的間距。 */
.hupa-field-stack {
  display: grid;
  gap: 0.5rem;
}

/* 唯讀的識別欄 (ID / Team ID)。只吃這個 class, 不吃 [readonly] ——
   日期選擇器的輸入框也是 readonly 的, 那些要看起來可點, 不能一起灰掉。 */
.hupa-modal .hupa-input-readonly {
  background-color: var(--hupa-page-bg);
  color: var(--hupa-text-muted);
}

/* 彈窗裡的輸入框拉回 .form-control 的基準字級與 padding, 理由與特異性推導
   跟上面 .hupa-filters 那條完全一樣 (sleek.css:13954 的
   `.form-group .form-control, .input-group .form-control` 把字級放大成
   0.98rem, 高度就從 41.9px 變成 44.4px)。彈窗裡的時間欄位包在
   .input-group.date 裡, 旁邊的下拉是 select2 的 41.9px —— 不拉齊就會一列
   高一列矮。 */
.hupa-modal .form-group .form-control,
.hupa-modal .input-group .form-control {
  font-size: 0.875rem;
  padding: 0.59rem 1rem;
}

/* sleek.css:13950 給 .form-group / .input-group 一律加了 margin-bottom:1.25rem。
   彈窗的列距是 .hupa-form-grid 的 gap 在管, 這層額外的下緣會讓包在
   .input-group.date 裡的時間欄位比同列的移除鈕多墊一段, 看起來沒對齊。 */
.hupa-modal .hupa-form-grid .form-group,
.hupa-modal .hupa-form-grid .input-group {
  margin-bottom: 0;
}

/* bootstrap 的 .form-control[readonly] 會塗成 #e9ecef 的灰底 (sleek.css:2080)。
   日期選擇器的輸入框是 readonly 的 —— 它只是「不能用鍵盤打字」, 點下去會開
   面板, 灰掉會讓人以為欄位被停用, 跟旁邊白底的下拉也不是同一個外觀。
   .hupa-input-readonly (ID / Team ID 那種真的不可編輯的欄位) 仍然維持灰底,
   所以這裡限定在 .input-group.date 底下, 不是對所有 [readonly] 開洞。 */
.hupa-modal .input-group.date .form-control[readonly] {
  background-color: var(--hupa-surface);
  cursor: pointer;
}

/* 佈設的「開始／結束時間」可以有多組區間, 每組一列。
   .hupa-clip-list 就是 JS 複製列時抓的 .clip 容器。 */
.hupa-clip-list {
  display: grid;
  gap: 0.5rem;
}

.hupa-clip-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
  gap: 0.5rem;
  align-items: start;
}

/* 移除鈕吃 .btn 的預設 padding, 高度是 38px, 旁邊的時間欄位是 41.9px ——
   同一列兩個高度。改成跟輸入框同一條高度公式 (推導見上面 select2 那段),
   寬度也做成正方形, 一整欄的 ✕ 才會對齊。 */
.hupa-clip-row > .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: calc(1.3125rem + 1.18rem + 2px);
  height: calc(1.3125rem + 1.18rem + 2px);
  padding: 0;
}

/* --------------------------------------------------------------------------
   登入 / 註冊 —— .hupa-auth
   這兩頁原本套整個應用外殼: 還沒登入就看到一整排管理選單 (點了都會被權限
   擋掉), 內容區被側欄壓成一條, 表單本身則是四個沒有標籤、只有 placeholder
   的輸入框疊在一張 p-5 的卡片裡。

   改成置中的單卡版面: layout.html 的 sidebar / header / footer 三個 block
   在這兩頁被覆寫成空, body 也換掉 class (sidebar-fixed 會替側欄保留左邊
   的位置), 所以這裡要自己負責背景與置中。
   -------------------------------------------------------------------------- */
body.hupa-auth {
  min-height: 100vh;
  background-color: var(--hupa-page-bg);
}

.hupa-auth .hupa-auth-shell {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  padding: 2rem 1rem;
}

.hupa-auth-card {
  width: 100%;
  max-width: 26rem;
  padding: 2.5rem 2rem;
  background-color: var(--hupa-surface);
  border: 1px solid var(--hupa-border);
  border-radius: var(--hupa-radius);
  box-shadow: var(--hupa-shadow);
}

.hupa-auth-brand {
  margin-bottom: 1.5rem;
  text-align: center;
}

.hupa-auth-title {
  margin-bottom: 0.25rem;
  font-size: 1.5rem;
  font-weight: 500;
  color: var(--hupa-heading);
  text-align: center;
}

.hupa-auth-subtitle {
  margin-bottom: 1.75rem;
  font-size: 0.875rem;
  color: var(--hupa-text-muted);
  text-align: center;
}

/* --------------------------------------------------------------------------
   整頁表單卡 (合約編輯) —— 沿用彈窗那套 .hupa-form-grid / .hupa-field
   卡片跟著頁面滿版的話, 一個日期欄會被拉成一千多 px 寬: 標籤在最左、輸入
   游標在最右, 眼睛要橫掃整個螢幕才讀得完一列, 而欄位再寬也不會更好填。
   釘一個上限讓欄位停在可讀的長度, 卡片本身仍靠左對齊頁面。
   -------------------------------------------------------------------------- */
.hupa-form-card {
  max-width: 52rem;
}

/* .card-body 原廠是 1.25rem, 在整頁表單上等於欄位貼著卡片邊緣。 */
.hupa-form-card .card-body {
  padding: 1.75rem;
}

/* 列距比彈窗鬆一階。彈窗是刻意壓縮的臨時介面, 有高度預算要顧; 這一頁是
   坐下來慢慢填的, 沒有那個限制, 沿用彈窗的 1.125rem 只會顯得擠。 */
.hupa-form-card .hupa-form-grid {
  gap: 1.5rem 1.25rem;
}

/* 前言跟第一個欄位的距離不是純空白能解決的: 兩者都是靠左的小級數灰字
   (11px 說明 vs 13px 標籤), 隔多少空白都還是同一種東西, 讀起來像上一段
   還沒完。拉一條跟 .hupa-form-actions 同一套的分隔線, 表單本體上下各有
   一道細線收邊, 前言就明確退成頁面層級的說明。 */
.hupa-form-card .hupa-form-intro {
  padding-bottom: 1.25rem;
  margin-bottom: 1.75rem;
  line-height: 1.7;
  border-bottom: 1px solid var(--hupa-border);
}

/* 勾選項彼此留一點距離 —— 模組一多就會黏成一塊, 看不出有幾個選項。 */
.hupa-form-card .form-check + .form-check {
  margin-top: 0.5rem;
}

/* --------------------------------------------------------------------------
   訂閱狀態頁的免費試用卡
   跟 .hupa-form-card 同一套 (標籤在輸入框上方), 但這張卡跟頁面上其他卡並排,
   不能套 max-width, 所以另開一個 class 只帶內距與排版。
   -------------------------------------------------------------------------- */
.hupa-trial {
  padding: 1.75rem;
}

.hupa-trial .card-title {
  margin-bottom: 0.5rem;
}

.hupa-trial p {
  margin-bottom: 1.5rem;
}

/* 勾選項彼此留一點距離 —— 模組一多就會黏成一塊。 */
.hupa-trial .form-check + .form-check {
  margin-top: 0.5rem;
}

/* --------------------------------------------------------------------------
   開通完成後的一次性帳密
   這組明文密碼只存在於那一次的 response (見 subscription_trial 為什麼不
   redirect), 所以視覺上要夠顯眼、值要好複製 —— 用等寬字並讓它可以被選取
   成一整段, 不要跟說明文字混在同一個灰階裡。
   -------------------------------------------------------------------------- */
.hupa-credentials {
  border-color: var(--hupa-success);
}

.hupa-credentials-list {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.5rem 1rem;
  align-items: baseline;
}

.hupa-credentials-list dt {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--hupa-text-muted);
}

.hupa-credentials-list dd {
  margin: 0;
}

.hupa-credentials-list code {
  padding: 0.25rem 0.5rem;
  font-size: 0.9375rem;
  background: var(--hupa-page-bg);
  border: 1px solid var(--hupa-border);
  border-radius: 0.2rem;
  /* 雙擊要能選到整串, 不要在連字號處斷開。 */
  user-select: all;
}

/* 送出/取消跟最後一個欄位之間要有一道分隔, 不然按鈕看起來像黏在備註框上,
   而 .hupa-form-grid 的 gap 只管欄位之間, 管不到網格外面。 */
.hupa-form-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  padding-top: 1.25rem;
  margin-top: 1.5rem;
  border-top: 1px solid var(--hupa-border);
}

/* 登入頁的欄位沿用彈窗的 .hupa-field / .hupa-field-label (標籤在輸入框上方),
   兩邊是同一套表單語彙, 不要各寫一份。 */
.hupa-auth-alt {
  margin-top: 1.25rem;
  margin-bottom: 0;
  font-size: 0.875rem;
  color: var(--hupa-text-muted);
  text-align: center;
}

.hupa-auth-footer {
  margin-top: 1.5rem;
  margin-bottom: 0;
  font-size: 0.75rem;
  color: var(--hupa-text-muted);
}

/* 維護單位字樣接在 copyright 下面, 兩行之間不要再撐 1.5rem。 */
.hupa-auth-footer + .hupa-auth-vendor {
  margin-top: 0.25rem;
}

/* --------------------------------------------------------------------------
   DataTables 控制列 —— 搜尋框跟表格右緣對齊
   表格帶 .nowrap, 內容欄位多的時候實際寬度會超過卡片 (樣站、成員、佈設),
   但 DataTables 的「顯示 N 筆 / 搜尋」那一列是普通 block, 寬度只到卡片邊,
   於是搜尋框停在畫面中間, 表頭卻一路延伸到右邊 —— 看起來就是沒對齊。

   讓表格自己在 .basic-data-table 裡橫向捲動, 表格的可視右緣就跟控制列同一條
   線, 搜尋框自然貼齊右邊 (右對齊本身是 datatables.bootstrap4.css 給
   .dataTables_filter 的 text-align:right, 這裡只是把量到的寬度修正)。

   佈設頁是 scrollX: true, DataTables 會自己產生 .dataTables_scroll 容器並
   把凍結欄的寬度量在上面 (見上面 B7 / C-2 的說明), 外層再多一層 overflow
   不影響那組計算。
   -------------------------------------------------------------------------- */
.basic-data-table {
  /* 捲動不能掛在這一層。.basic-data-table 是樣板裡包住 <table> 的 div, 而
     DataTables 是在它「裡面」才生出 .dataTables_wrapper:

         .basic-data-table
           └ .dataTables_wrapper
               ├ .row  顯示 N 筆 / 搜尋
               ├ .row > .col-sm-12 > table
               └ .row  第 N-M 筆 / 上一頁 下一頁

     掛在外層等於把控制列與分頁列一起關進捲動容器 —— 分頁鈕會跟著表格左右
     跑, 捲軸還壓在分頁列底下。範圍縮到表格自己那一欄 (見下一條規則)。 */
  overflow-x: visible;
}

/* datatables.bootstrap4 的版面字串裡, 只有表格那一列是 .col-sm-12 而沒有
   col-md-*; 控制列 (col-md-6) 與分頁列 (col-md-5 / col-md-7) 都帶著。所以
   這個選擇器剛好只命中表格那一欄。 */
.basic-data-table .dataTables_wrapper > .row > .col-sm-12:not([class*="col-md"]) {
  overflow-x: auto;
}

/* 搜尋框本身: 標籤與輸入框同一行, 貼右緣。datatables.bootstrap4.css 把
   .dataTables_filter label 設成 text-align:left 的 block, 中文標籤「搜尋:」
   會跟輸入框拆成兩行左右分離, 這裡收成 inline-flex。 */
/* sleek.css:14370 把 .basic-data-table .dataTables_filter 設成
   `display: inline-block !important`, 於是它只佔內容寬度、貼在欄位左緣 ——
   datatables.bootstrap4.min.css 給的 text-align:right 只排到它自己的內容,
   整塊仍然停在中間, 看起來就是搜尋框沒跟表格右緣對齊。

   對手是 !important, 只有同樣掛 !important 才蓋得掉 (載入順序與特異性都
   贏不過 !important)。這是全檔唯一一處 !important, 就是為了這條。
   撐成 block 之後 text-align:right 才真的把搜尋框推到欄的右緣, 也就是
   .basic-data-table 的右緣 (表格超寬時的可視右緣)。 */
.basic-data-table .dataTables_wrapper .dataTables_filter {
  display: block !important;
  text-align: right;
}

.dataTables_wrapper .dataTables_filter label {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0;
  color: var(--hupa-text-muted);
}

/* 窄螢幕: 上面那組「貼右緣」的規則在手機上會把整頁撐破。

   「搜尋:」標籤加輸入框合計約 228px, 兩者都不縮, 而 .dataTables_filter 是
   float:right —— 375px 的畫面上它從 179px 開始畫, 右緣落在 407px, 文件因此
   橫向溢出 32px。溢出之後 Chrome 的 layout viewport 跟著撐到 407, 而
   .main-header 是 position:fixed; left:0; right:0, 它照撐大後的 viewport 對齊,
   於是靠右的使用者選單 (登出在裡面) 被推到畫面外, 要先橫向捲動才點得到。

   不用 body { overflow-x: hidden } 蓋掉: 那只是把捲軸藏起來, 選單一樣在畫面
   外, 而且之後任何一個新的溢出元素都會就此靜音, 再也看不出來。 */
@media (max-width: 575.98px) {
  /* 佈設管理的控制列是 flex (.top.d-flex.justify-content-between), 搜尋框是
     其中一個 flex item。flex item 的預設 min-width 是 auto —— 也就是「不能縮
     到比 min-content 更窄」, 而這一塊的 min-content 就是「搜尋:」加一個
     width:auto 的輸入框 = 228px。所以每一層都要 min-width:0, 只放 width:100%
     壓不下去。 */
  .basic-data-table .dataTables_wrapper .dataTables_filter,
  .dataTables_wrapper .dataTables_filter {
    float: none;
    text-align: left;
    width: 100%;
    min-width: 0;
  }

  .dataTables_wrapper .dataTables_filter label {
    display: flex;
    width: 100%;
    min-width: 0;
  }

  /* 選擇器要帶上 .form-control 才追得過 datatables.bootstrap4.min.css 的
     `div.dataTables_wrapper div.dataTables_filter input { width: auto }`
     —— 那條是 (0,2,2), 跟不帶 .form-control 的版本同分, 靠載入順序決勝,
     而 datatables 的樣式排在後面。 */
  .dataTables_wrapper .dataTables_filter label input.form-control {
    flex: 1 1 auto;
    width: 100%;
    min-width: 0;
  }
}

/* DataTables 的分頁鈕太大。

   來源是 sleek.css 的兩條:
     .page-link { padding: 1rem 1.25rem; }
     .pagination .page-item:first-child .page-link,
     .pagination .page-item:last-child .page-link { height: 51px; }
   一顆「上一頁」因此是 51px 高、左右各 20px 內距, 比同一頁的按鈕
   (.btn 約 42px)、輸入框 (41.9px) 都重, 一整排浮在表格底下很突兀。

   sleek 自己其實有一組縮小版 (.basic-data-table / .expendable-data-table /
   .hoverable-data-table 底下是 padding:8px、height:35px), 但這幾頁的表格容器
   是 .datatable, 沒掛那些 class, 所以吃到的是 51px 那版。與其去幫每頁的
   容器補 class (會連帶套用 sleek 那整套表格外觀), 不如直接對 DataTables 產生
   的分頁列定尺寸。

   收斂到 .dataTables_wrapper 底下: 這幾頁的 .pagination 全部是 DataTables 產生
   的, 不會誤傷手寫的分頁 (/explorer 的分頁是 .hupa-pager, 不走 .pagination)。
   height 那條的特異性是 (0,4,0) —— .pagination + .page-item + :first-child +
   .page-link, 前面多一層 .dataTables_wrapper 變 (0,5,0) 才追得過。 */
/* /explorer 的「顯示 N 筆」是手寫的 (那頁的分頁在伺服器端做), DataTables
   自己產生的那份會被 datatables.bootstrap4.css 排成一行, 手寫的沒有 ——
   label 是 block、select 又是 .form-control (width:100%), 於是「顯示」、
   下拉、「筆」三段各自佔一行。這裡把它排回一行。 */
.dataTables_length label {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  margin-bottom: 0;
  font-size: 0.8125rem;
  color: var(--hupa-text-muted);
  white-space: nowrap;
}

.dataTables_length label select.form-control {
  width: auto;
  min-width: 4.5rem;
}

/* .hupa-table-pager 是 /explorer 手寫的那一排 (分頁在伺服器端做, DataTables
   的 paging 是關掉的)。它是 .dataTables_wrapper 的兄弟節點而不是子節點 ——
   DataTables 只包住 <table>, 所以上面那層限定 .dataTables_wrapper 的規則對它
   完全不生效, 少了這個並列選擇器它吃到的會是 sleek 的 51px 版分頁鈕。 */
.dataTables_wrapper .pagination .page-link,
.hupa-table-pager .pagination .page-link {
  padding: 0.375rem 0.75rem;
  font-size: 0.875rem;
  line-height: 1.5;
  border-color: var(--hupa-border);
}

.dataTables_wrapper .pagination .page-item:first-child .page-link,
.dataTables_wrapper .pagination .page-item:last-child .page-link,
.hupa-table-pager .pagination .page-item:first-child .page-link,
.hupa-table-pager .pagination .page-item:last-child .page-link {
  height: auto;
}

/* 手寫那一排要靠右。DataTables 自己那份靠的是
   `.dataTables_wrapper .dataTables_paginate ul.pagination{justify-content:flex-end}`
   (datatables.bootstrap4.css), 選擇器限定在 wrapper 裡, 兄弟節點吃不到 ——
   所以 /explorer 的分頁鈕原本是靠左的 (使用者實機回報「應靠右」)。 */
.hupa-table-pager .pagination {
  justify-content: flex-end;
  margin: 0;
  flex-wrap: wrap;
}

/* 分頁列與表格最後一列之間要留白。DataTables 的底列只有 .pagination 自己的
   2px margin (datatables.bootstrap4.css), 所以分頁鈕幾乎貼在最後一列的操作
   圖示下面 (使用者實機回報「貼太近」) —— 兩排都是可以按的東西, 貼在一起會
   讓人不確定按鈕屬於哪一列。 */
/* 分頁列與表格之間的留白。前兩版都沒真的生效, 原因是 margin 加在
   .dataTables_paginate 上, 而 jquery.datatables.css 給它 `float: right` ——
   浮動元素不參與父層高度, 底列容器 (.bottom, 或預設版型的最後一個 .row)
   高度是 0, 於是那段 margin 沒有東西可以推開, 分頁鈕看起來還是貼在表格
   (/deploy 是貼在水平捲軸) 底下。

   改成兩件事一起做: 取消浮動 (換成 flex 靠右, 排版結果一樣), 讓底列容器
   量得到高度; 留白則加在容器的 padding-top 上, 不再靠浮動元素的 margin。
   容器補 display: flow-root, 萬一還有其他浮動的子元素也一併包住。

   多疊一層 body 是因為 datatables.bootstrap4.css 在 hupa-theme.css 之後
   載入, 同特異性時後載的贏。 */
body .dataTables_wrapper .bottom,
body .dataTables_wrapper > .row:last-child {
  display: flow-root;
  padding-top: 1.25rem;
}

body .dataTables_wrapper .dataTables_paginate {
  float: none;
  display: flex;
  justify-content: flex-end;
  margin: 0;
}

body .dataTables_wrapper .dataTables_info {
  float: none;
  margin: 0;
}

.hupa-table-pager {
  margin-top: 1.25rem;
}

/* --------------------------------------------------------------------------
   影像瀏覽 —— 縮圖列表與上方工具列切齊
   #gallery 是 .row row-cols-N (縮圖大小的 range 會重寫這個 class), bootstrap
   的 .row 帶 margin: 0 -15px。上方的 .gallery-toolbar 是普通 block, 沒有這組
   負邊界, 所以縮圖列每邊都比工具列外凸 15px —— 兩排東西的左右緣對不上。

   縮圖直接就是 .row 的子元素 (沒有 .col-* 包一層), 所以那 -15px 也沒有對應的
   欄距在補; 直接歸零, 縮圖之間的間隙由 .thumbnail 自己的 .p-1 負責。
   -------------------------------------------------------------------------- */
#gallery.row {
  margin-right: 0;
  margin-left: 0;
}

/* --------------------------------------------------------------------------
   提示與確認對話框 —— .hupa-dialog (hupa-ui.js 的 hupaAlert / hupaConfirm)
   原生 alert()/confirm() 長瀏覽器的樣子: 標題是「localhost:5000 說」、按鈕是
   作業系統風格、圓角與陰影都跟站上不一樣。這一層讓它跟 .hupa-modal 同一套
   語彙 —— 版型宣告沿用 .hupa-modal (JS 產生的節點同時掛了兩個 class),
   這裡只補對話框特有的部分。

   訊息只有一兩行, modal 預設的 500px 太寬, 收到 420px; 按鈕給 min-width
   讓「確定/取消」等寬, 不會因為字數差一個字就一寬一窄。
   -------------------------------------------------------------------------- */
.hupa-dialog .modal-dialog {
  max-width: 420px;
}

.hupa-dialog .modal-body {
  padding: 1.25rem 1.5rem;
}

.hupa-dialog-message {
  margin: 0;
  font-size: 0.9375rem;
  line-height: 1.7;
  color: var(--hupa-heading);
  /* 訊息裡可能有很長的檔名或不帶空白的字串 (刪除檔案的提示), 讓它斷行,
     不要把對話框撐寬。 */
  overflow-wrap: break-word;
}

.hupa-dialog .modal-footer {
  padding: 0.875rem 1.5rem;
}

.hupa-dialog .modal-footer .btn {
  min-width: 5rem;
}

/* 取消是次要動作, 用白底描邊而不是 bootstrap 的灰底 .btn-light ——
   跟表單彈窗的「取消」同一個外觀。 */
.hupa-dialog .hupa-dialog-cancel {
  color: var(--hupa-heading);
  background-color: var(--hupa-surface);
  border-color: var(--hupa-border);
}

.hupa-dialog .hupa-dialog-cancel:hover,
.hupa-dialog .hupa-dialog-cancel:focus {
  color: var(--hupa-heading);
  background-color: var(--hupa-surface-hover);
  border-color: var(--hupa-border);
}

/* 對話框會疊在已開啟的表單彈窗上 (使用者管理: 編輯彈窗裡按刪除)。bootstrap 4
   對所有 .modal 都給 z-index:1050 / backdrop 1040, 兩層一樣高時後插入的節點
   雖然畫在上面, 但底下那層的 backdrop 也蓋在對話框上, 看起來就是灰掉不能按。
   把這一層整組推高一階; .hupa-dialog-backdrop 是 hupa-ui.js 在 shown 時加到
   最後一個 .modal-backdrop 上的, 只有這個對話框自己的遮罩會拿到。 */
.hupa-dialog {
  z-index: 1080;
}

.hupa-dialog-backdrop {
  z-index: 1075;
}

/* 關掉的彈窗在淡出的那 150ms 還蓋在畫面上, 吃得到點擊。

   實際症狀: 提示彈窗按「確定」之後馬上按「登入」/「儲存」, 那一下完全沒有
   反應, 要再按一次才生效 —— 使用者以為系統當掉。bootstrap 4 是等
   transitionend 才把 .modal 設回 display:none, 在那之前 .modal 是一層
   position:fixed 的全螢幕元素, .modal-backdrop 也還在。

   .show 一被拿掉就代表這一層在退場, 不該再接收任何點擊。 */
.modal.fade:not(.show),
.modal-backdrop.fade:not(.show) {
  pointer-events: none;
}

/* 遮罩顏色 —— 全站彈窗共用一個。bootstrap 的 .modal-backdrop.show 是純黑
   opacity 0.5 (sleek.css:6138), 換成品牌深藍加透明度: 純黑會把底下的介面壓成
   灰階, 深藍留得住頁面本來的色溫。選擇器跟 bootstrap 同級 (兩個 class), 靠
   hupa-theme.css 排在 sleek.css 後面取勝; 濃度改由 background-color 的 alpha
   決定, opacity 交回 1, 淡入淡出還是 .fade 在管。 */
.modal-backdrop.show {
  background-color: var(--hupa-overlay);
  opacity: 1;
}

/* 對話框疊在別的彈窗上時 (使用者管理: 編輯彈窗裡按刪除) 用淡一階的那顆 ——
   底下那層自己的遮罩已經把頁面壓暗了, 這一層只要再退一點。class 是
   hupa-ui.js 判斷底下還有彈窗開著時才加的。 */
.modal-backdrop.hupa-dialog-backdrop--stacked.show {
  background-color: var(--hupa-overlay-soft);
}

/* 進場動畫。bootstrap 的 .modal.fade .modal-dialog 是從上方 -50px 滑進來
   (sleek.css:6044) —— 那是給大表單彈窗的位移感; 一個 420px 寬、垂直置中的
   提示框從畫面上方掉下來, 位移距離跟框的大小不成比例, 看起來就是晃一下。
   改成原地輕微放大 + 淡入, 時間從 0.3s 縮到 0.16s: 提示框是打斷流程用的,
   越快到位越好。 */
.hupa-dialog.fade .modal-dialog {
  transition: transform 0.16s ease-out, opacity 0.16s ease-out;
  transform: scale(0.96);
  opacity: 0;
}

.hupa-dialog.show .modal-dialog {
  transform: none;
  opacity: 1;
}

/* backdrop:'static' 時點遮罩外面, bootstrap 4.6 會加 .modal-static 抖一下當
   回饋 (sleek.css:6059 的 scale(1.02))。上面那條 .hupa-dialog.show 的
   transform:none 特異性跟它同級、又排在後面, 會把回饋吃掉 —— 補回來。 */
.hupa-dialog.modal-static .modal-dialog {
  transform: scale(1.02);
}

@media (prefers-reduced-motion: reduce) {
  .hupa-dialog.fade .modal-dialog {
    transition: none;
    transform: none;
  }
}

/* ── 引導式空狀態 (W10) ─────────────────────────────────────────────
   DataTables 的 emptyTable 只給一行字, 第一次使用的人看不出下一步。有新增
   入口的管理頁改成圖示 + 說明 + 立即新增; 沒有入口的頁面維持預設一行字。 */
.hupa-empty {
  padding: 2.5rem 1rem;
  text-align: center;
  color: var(--hupa-text-muted);
}
.hupa-empty .mdi {
  opacity: .35;
  line-height: 1;
}
.hupa-empty-text {
  margin: .75rem 0 .25rem;
  font-size: 1rem;
  font-weight: 500;
}
.hupa-empty-hint {
  margin: 0 auto 1rem;
  max-width: 32rem;
  font-size: .8125rem;
}

/* ── 上傳鈕的張數角標 (W9) ─────────────────────────────────────────
   角標疊在雲朵圖示右上角。btn-edit-icon 本身沒有 position, 這裡補上, 否則
   絕對定位會跑去對齊整個表格。 */
.deploy-gallery-link .btn-edit-icon {
  position: relative;
}
/* 圖示鈕右上角的數量角標。/deploy 操作欄的影像瀏覽鈕先有這個樣子, 影像瀏覽
   縮圖的註記鈕比照沿用 (使用者指名要一致), 所以兩個名字共用同一份宣告。 */
.deploy-photo-badge,
.icon-count-badge {
  position: absolute;
  top: -.25rem;
  right: -.4rem;
  font-size: .625rem;
  font-weight: 600;
  line-height: 1;
  padding: .2em .4em;
}

/* ── 已刪除的列 ───────────────────────────────────────────────────
   /deploy 打開「顯示已刪除的佈設」之後, 死活兩種列混在同一頁。狀態是整列
   的, 所以標記也給整列: 左緣一條橘線讓人一眼分群, 內容文字降成 muted 表示
   這一列不是可操作的主體。ID 欄那顆小標籤只是替橘線寫上名字, 用淡底深字而
   不是實心橘 —— 實心色塊在 ID 旁邊會比 ID 本身還顯眼。 */
.deploy-row-deleted > td {
  color: var(--hupa-text-muted);
}
.deploy-row-deleted > td:first-child {
  box-shadow: inset 3px 0 0 var(--hupa-orange);
}
.deploy-deleted-badge {
  background-color: var(--hupa-orange-soft);
  color: var(--hupa-orange-text);
  font-size: .6875rem;
  font-weight: 600;
  line-height: 1;
  padding: .25em .45em;
  vertical-align: middle;
}

/* ==========================================================================
   側欄底部的版本號與更新記錄入口
   --------------------------------------------------------------------------
   刻意不用主題內建那組「側欄含 footer」的樣式規則 (要另外在側欄外層多包一個
   class 才會生效): sleek.css:16343-16352 給它留了 padding-bottom: 260px
   (>768px 是 284px), 那是為了塞一整塊促銷區設計的, 一行版本號用它等於側欄
   底部空掉 260px。
   高度釘在 2.25rem, layout.html 的 #sidebar-body calc 要跟著減掉同一個值。
   ========================================================================== */
.hupa-sidebar-version {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 2.25rem;
  display: flex;
  align-items: center;
  padding: 0 1.56rem;
  border-top: 1px solid var(--hupa-on-dark-divider);
}

.hupa-sidebar-version > a {
  display: flex;
  align-items: center;
  color: var(--hupa-on-dark-text);
  font-size: 0.75rem;
  line-height: 1;
  white-space: nowrap;
}

.hupa-sidebar-version > a:hover,
.hupa-sidebar-version > a:focus {
  color: var(--hupa-on-dark);
  text-decoration: none;
}

.hupa-sidebar-version .mdi {
  font-size: 1rem;
  margin-right: 0.5rem;
}

.hupa-version-dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-left: 0.4rem;
  border-radius: 50%;
  background-color: var(--hupa-notify);
}

/* 側欄收合時只留圖示與紅點 —— 紅點不能一起收掉, 不然收合狀態下使用者永遠
   不知道有新版。

   收合態就是常駐 75px 的圖示 rail, V-3 已經把 `.sidebar-minified
   .left-sidebar:hover` 的寬度釘死在 75px (不再展回 250px/180px, 見本檔上方
   V-3 區塊的說明)。側欄本身不再變寬, 所以不存在「hover 展開文字」這件事。
   .section-title 早先就是這樣處理的: 規則刪掉, 測試改成斷言選擇器不存在。
   要看版本文字就用側欄自己的收合鈕展開整條側欄。 */
@media (min-width: 768px) {
  .sidebar-minified .left-sidebar .hupa-sidebar-version {
    padding: 0;
    justify-content: center;
  }
  .sidebar-minified .left-sidebar .hupa-version-text {
    display: none;
  }
  .sidebar-minified .left-sidebar .hupa-sidebar-version .mdi {
    margin-right: 0;
  }
}

/* 版本一多就讓 modal 內部捲動, 不要把整個對話框撐出畫面。 */
.hupa-changelog-body {
  max-height: 60vh;
  overflow-y: auto;
}

.hupa-changelog-version + .hupa-changelog-version {
  margin-top: 1.25rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--hupa-border);
}

.hupa-changelog-version-title {
  margin-bottom: 0.5rem;
  font-weight: 600;
}

.hupa-changelog-version-title small {
  margin-left: 0.5rem;
  font-weight: 400;
}

/* 過去的版本收在 <details> 裡 (見 _changelog_modal.html)。標題用弱化色, 讓
   攤開的最新一版一眼就是主角; 收合時標題下方不留空白, 否則每一列之間會空出
   一截像是漏了內容。 */
.hupa-changelog-past > summary {
  cursor: pointer;
  color: var(--hupa-text-muted);
}

.hupa-changelog-past:not([open]) > summary {
  margin-bottom: 0;
}

.hupa-changelog-section-title {
  display: inline-block;
  margin-bottom: 0.25rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--hupa-text-muted);
}

.hupa-changelog-section ol {
  margin-bottom: 0.75rem;
  padding-left: 1.25rem;
}

.hupa-changelog-section li {
  margin-bottom: 0.25rem;
  line-height: 1.6;
  /* 明寫 decimal: sleek.css:12128-12129 把 `ul li` 的 list-style-type 設成
     none, 第一版沒蓋掉它, 項目在畫面上就擠成一片沒有任何標記。這條選擇器
     (0,1,1) 特異性高於 `ul li` (0,0,2), 而 hupa-theme.css 也載在 sleek.css
     之後。 */
  list-style-type: decimal;
}

/* 頁腳的「不再顯示」勾選框跟關閉鈕不同高, custom-control 預設沒有把 checkbox
   跟文字對齊 modal-footer 裡按鈕的中線, 加個 flex 對齊。 */
.hupa-changelog-dismiss {
  display: flex;
  align-items: center;
}

/* ==========================================================================
   專案佈設管理 (/projectedit) —— 存檔列、狀態 toggle、視圖切換
   --------------------------------------------------------------------------
   這頁原本是左右兩張表 (候選 / 已加入), 使用者實測否決: 加入完要在兩張表之間
   來回對照才知道清單對不對。改成一張表 + 每列一顆狀態 toggle 之後, 有三件事
   在 sleek.css 裡沒有現成的樣式: 一直看得到的存檔列、已加入那一列的視覺標記、
   視圖切換的藥丸按鈕。
   ========================================================================== */

/* 復原提示。/projectedit 的加入/移除是當場寫進資料庫的, 所以這頁沒有存檔鈕
   (那顆鈕的位子搬過四次還是覺得怪 —— 多餘的是那一步本身)。誤按的出口是這條
   提示上的「復原」。

   fixed 貼右下角: 它跟頁面上任何一塊內容都沒有從屬關係, 是一則短暫的回饋,
   不是版面的一部分。z-index 1047 低於 .mobile-bottom-nav (1048) 與
   .mobile-sheet (1050) —— 手機上它停在導覽列上方, 不跟它爭。 */
.hupa-undo-toast {
  position: fixed;
  right: 1.5rem;
  bottom: 1.5rem;
  z-index: 1047;
  display: flex;
  gap: 0.75rem;
  align-items: center;
  max-width: calc(100vw - 3rem);
  padding: 0.625rem 0.75rem 0.625rem 1rem;
  border: 1px solid var(--hupa-border);
  border-radius: 999px;
  background-color: var(--hupa-surface);
  box-shadow: var(--hupa-shadow-raised);
}

.hupa-undo-toast[hidden] {
  display: none;
}

.hupa-undo-toast-text {
  color: var(--hupa-heading);
  white-space: nowrap;
}

/* 復原是這條提示的唯一動作, 做成文字鈕: 提示本身已經是浮起來的一塊,
   裡面再放一顆實心按鈕會比它想搶救的那次點擊還搶眼。 */
.hupa-undo-toast-action {
  padding: 0.25rem 0.75rem;
  border: 1px solid var(--hupa-border);
  border-radius: 999px;
  background-color: transparent;
  color: var(--hupa-primary);
  font: inherit;
  font-weight: 600;
}

.hupa-undo-toast-action:hover,
.hupa-undo-toast-action:focus {
  background-color: var(--hupa-primary-soft);
  color: var(--hupa-primary-hover);
}

.hupa-undo-toast-action[hidden] {
  display: none;
}

/* 已加入的那一列: 左緣一條色條 + 淡底。跟 .deploy-row-deleted (已刪除的列)
   是同一套語彙, 只是換成 primary —— 兩頁的「整列狀態」看起來要像同一件事。

   底色下在 td 而不是 tr: .table-striped 的斑馬列設在 tr 上, 下在 tr 會被
   斑馬列蓋掉。選擇器前面那個 body 是為了追上全站 hover 規則
   (`body .table tbody tr:hover > td`) 的特異性, 否則滑過去整列會變回 hover 底色,
   看不出哪幾列已經加入了。 */
body .table tbody tr.is-selected > td {
  background-color: var(--hupa-primary-soft);
}

body .table tbody tr.is-selected:hover > td {
  background-color: var(--hupa-primary-soft-hover);
}

body .table tbody tr.is-selected > td:first-child {
  box-shadow: inset 3px 0 0 var(--hupa-primary);
}

/* 狀態 toggle。已加入那顆 hover/focus 時換字成「移除」—— 同一顆按鈕既是狀態
   也是動作, 不換字的話使用者不知道按下去會發生什麼。兩段文字都在 DOM 裡
   (不是 CSS content), 所以輔助技術讀得到, 而 aria-pressed 講的是狀態本身。 */
.projectedit-toggle .projectedit-toggle-off {
  display: none;
}

.projectedit-toggle:hover .projectedit-toggle-on,
.projectedit-toggle:focus .projectedit-toggle-on {
  display: none;
}

.projectedit-toggle:hover .projectedit-toggle-off,
.projectedit-toggle:focus .projectedit-toggle-off {
  display: inline;
}

/* 狀態欄擺第一欄, 不橫向捲就按得到 (所以這頁不再需要 freeze-actions)。
   nowrap: 「已加入 ✓」被折成兩行的話, 整列高度會跟其他列不一樣。 */
.projectedit-state-cell {
  white-space: nowrap;
}

/* 表頭不換行。舊版是每欄 min-width:90px 撐開 (表頭那列塞了 8 個搜尋框, 不撐開
   placeholder 會被切成 "S"); 搜尋框拔掉之後那組 min-width 跟着刪了, 但少了它
   之後瀏覽器會把 9 欄硬擠成等寬 —— 中文可以逐字斷行, 所以在 768~900px 這段
   寬度「相機型號」會被壓成直排, 表格反而永遠不會溢出、也就永遠捲不動。
   改成釘住表頭這一列的最小寬度: 版面不夠就整張表變寬, 由 .basic-data-table
   既有的 overflow-x 橫向捲動 (資料格仍然可以換行)。 */
#deploytable thead th {
  white-space: nowrap;
}

/* 視圖切換 (全部 / 已加入 / 本次新增)。是三個互斥的檢視範圍, 不是三個動作,
   所以用藥丸按鈕群而不是 .btn-group —— 選中的那顆用實心底表示「現在看的是
   這個」, aria-pressed 同步同一件事。 */
.hupa-view-switch {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
  margin-bottom: 0.75rem;
}

.hupa-view-chip {
  padding: 0.375rem 0.875rem;
  background-color: var(--hupa-surface);
  border: 1px solid var(--hupa-border);
  border-radius: 1rem;
  color: var(--hupa-text-muted);
  font-size: 0.8125rem;
  font-weight: 600;
}

.hupa-view-chip:hover {
  background-color: var(--hupa-surface-hover);
}

.hupa-view-chip.is-active {
  background-color: var(--hupa-primary);
  border-color: var(--hupa-primary);
  color: var(--hupa-on-dark);
}

/* 這頁的篩選抽屜是手機專屬的第二套 DOM: 桌機有自己那份 .hupa-filters 平鋪在
   表格上方, 兩份同時顯示就會在 1280px 上出現兩排一樣的篩選欄位。

   .mobile-sheet 的預設是 display:block (它在別的頁面「桌機就是那塊面板」),
   所以要像 .mobile-shell-sheet 一樣自己關掉。手機那條開回來的規則帶
   :not([hidden]) —— 媒體查詢不加特異性, 少了它就會贏過媒體查詢裡的
   `.mobile-sheet[hidden] { display: none }`, 抽屜變成永遠開著。 */
.mobile-sheet.projectedit-filter-sheet {
  display: none;
}

/* ==========================================================================
   D1 —— 手機全域尺度 (max-width: 575.98px)

   站上所有尺寸都是桌機值: 卡片內距、頁面留白、navbar 高度都是固定 rem/px,
   螢幕變窄它們不變。登入頁最明顯 —— 375px 螢幕上外殼左右各 1rem, 卡片內距
   左右各 2rem, 光留白就 96px, 欄位可用寬只剩 247px (螢幕的 66%), 整頁看起來
   是「一小塊浮在空白中」。

   收到 0.75rem + 1.25rem = 左右共 64px 之後, 欄位可用寬 311px (83%)。
   tests/test_mobile_rwd.py 有一條把這個百分比當下限的斷言, 調數值要連它一起看。

   整段只活在窄螢幕斷點內, 桌機一個像素都不動 —— 這是 RWD spec 的硬性約束,
   測試會斷言桌機那幾條原值仍在。
   ========================================================================== */
@media (max-width: 575.98px) {
  .hupa-auth .hupa-auth-shell {
    min-height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom));
    /* 保留既有 shorthand 契約，再逐邊加入 16px 與 safe area。 */
    padding: 1.5rem 0.75rem;
    padding-top: calc(1rem + env(safe-area-inset-top));
    padding-right: 1rem;
    padding-bottom: calc(1rem + env(safe-area-inset-bottom));
    padding-left: 1rem;
  }

  .hupa-auth-card {
    padding: 2rem 1.25rem;
    overflow-wrap: anywhere;
  }

  .hupa-auth .form-control,
  .hupa-auth .hupa-pwd-toggle,
  .hupa-auth #login-submit,
  .hupa-password-page .form-control,
  .hupa-password-page .hupa-pwd-toggle,
  .hupa-password-page #password-submit {
    min-height: 48px;
  }

  .hupa-auth .hupa-pwd-toggle,
  .hupa-password-page .hupa-pwd-toggle {
    min-width: 48px;
    padding: 0;
  }

  .hupa-auth #login-submit,
  .hupa-password-page #password-submit {
    width: 100%;
  }

  .hupa-password-page {
    max-width: 100%;
    padding-bottom: calc(16px + 4rem + env(safe-area-inset-bottom));
    overflow-wrap: anywhere;
  }

  .hupa-password-page .hupa-form-actions {
    display: block;
  }

  /* 登入頁的外殼本身就是置中容器, 包在外面的 .content 還帶著 sleek 給的
     左右各 0.94rem —— 在這一頁是第三層重複的溝槽。三層疊起來每邊
     15 + 12 + 20 = 47px, 375px 螢幕上欄位可用寬只有 281px (75%)。

     歸零之後回到 311px (83%)。tests/test_mobile_rwd.py 的留白預算只算外殼
     與卡片兩層, 前提就是這一層沒有溝槽 —— 這條規則就是那個前提的實作,
     所以那邊也有一條斷言盯著它。 */
  body.hupa-auth .content {
    padding-left: 0;
    padding-right: 0;
  }

  /* 整頁表單卡 (改密碼、合約編輯) 與訂閱頁的試用卡是同一組內距, 桌機都是
     1.75rem。手機上那等於左右吃掉 56px, 而這兩種卡片本來就滿版, 沒有登入卡
     那種「卡片要看得出邊界」的需求。兩個選擇器要一起改: 訂閱頁同一個畫面上
     兩種卡片並存, 只收一邊會看出 0.625rem 的落差。 */
  .hupa-form-card .card-body,
  .hupa-trial {
    padding: 1.125rem;
  }

  /* navbar 原廠 min-height 是 74px (sleek.css:14879)。手機直立螢幕上, 一條
     固定在頂端的 74px 白條佔掉 9% 可視高度, 而它上面只有一顆側欄開關鈕跟
     使用者選單。56px 仍在觸控目標下限 (44px) 之上。

     --header-offset 要跟著改, 但不是在這裡 —— 它宣告在 layout.html 的
     inline <style> 裡, 那塊排在 hupa-theme.css 的 <link> 之後, 同為 :root
     特異性相同, 寫在這個檔案裡贏不了。

     特異性: 這條跟 sleek.css 的 .navbar 同分 (0,1,0), 靠 hupa-theme.css 載在
     sleek.css 之後決勝 —— 不要為了保險加 !important。 */
  .navbar {
    min-height: 56px;
  }
}

/* --------------------------------------------------------------------------
   /taxa 的「從 TBN TaxaTree 匯入物種」結果表

   這張表是搜尋完才由 JS 畫出來的, 不走 DataTables, 所以吃不到
   .dataTables_wrapper 那層的橫向捲動。沒有自己的捲動容器時, 表格的內在寬度
   會把它所在的卡片一起撐開 —— 在 375px 上看到的是整塊往右移、左邊被切掉。
   -------------------------------------------------------------------------- */

.taxa-tt-results {
  width: 100%;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.taxa-tt-results:empty {
  display: none;
}

/* 學名是拉丁文長字串, 斷不了詞。這是表格裡唯一會頂寬的欄位。 */
.taxa-tt-sci {
  overflow-wrap: anywhere;
}

.taxa-tt-results .taxa-tt-add {
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   /oi 的群計物種標籤

   原本用 .list-inline-item.badge.badge-secondary: badge 的內距是為了單獨
   一顆角標設計的, 一次列十幾二十種時每顆都又高又寬, 一個名字佔掉半行,
   整塊要捲三四屏。改成自己的密排標籤 —— 一行放得下三四個, 刪除鈕仍然
   維持 24px 的可點範圍 (整顆標籤本身不是按鈕, 只有 × 是)。

   桌機與手機同一份: 使用者要的是「擠一點」, 不是只有手機擠。
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   /oi 計算說明里的三張卡 (整體 OI / 閾值 / 群計物種)。

   刻意不用 Bootstrap 的 .row/.col: 手機斷點那條
   `#oi-page .row { max-width: 100% }` 會把 .row 的 -15px 負邊距夾掉,
   整排卡的右邊會空出一截 (使用者實機回報)。flex + gap 沒有負邊距,
   不會被那條規則拿捕。

   flex-basis 沒有斷點: 主卡最窄 15rem, 兩張參數卡最窄 8rem —— 375px
   放不下三張時主卡自己排一列, 參數卡並排; 桌機一列三張。 */

.oi-formula-cards {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

.oi-formula-card {
  flex: 1 1 8rem;
  /* 不加就會被裡面的公式小字撒寬, 採不到 flex-basis。 */
  min-width: 0;
}

.oi-formula-card--main {
  flex: 2 1 15rem;
}

.oi-group-list {
  display: flex;
  flex-wrap: wrap;
  /* 橫向間距比縱向大: 相鄰兩顆的 × 才不會挨在一起變成同一片可點區。 */
  gap: 0.25rem 0.375rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.oi-group-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.125rem;
  max-width: 100%;
  padding: 0.0625rem 0.125rem 0.0625rem 0.5rem;
  border: 1px solid var(--hupa-border);
  border-radius: 1rem;
  background: var(--hupa-surface-hover);
  color: var(--hupa-heading);
  font-size: 0.8125rem;
  line-height: 1.5;
}

.oi-group-chip-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* × 是這一排唯一可以按的東西。

   刻意小於全站 44px 的觸控下限, 而且是唯一一處: 這一排一次會有十八顆以上
   (預設群計物種就是 18 種), 每顆 44px 高時整塊要捲三四屏, 使用者實機回報
   的就是這件事。28px 是「還按得到」與「列得完」之間的取捨, 橫向再靠 gap
   把相鄰的 × 隔開。

   #oi-page 那條 `button { min-height: 44px }` 是用 id 選到的, 所以這裡也要
   帶 #oi-page 才贏得過它 —— 不是用 !important 硬蓋。 */
#oi-page .oi-group-chip-remove,
.oi-group-chip-remove {
  display: inline-flex;
  width: 28px;
  height: 28px;
  min-height: 28px;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--hupa-text-muted);
  font-size: 1.125rem;
  line-height: 1;
}

.oi-group-chip-remove:hover,
.oi-group-chip-remove:focus {
  background: var(--hupa-surface);
  color: var(--hupa-danger);
}

/* 加入失敗/重複的說明。空的時候不佔位。 */
.oi-group-hint {
  margin-top: 0.375rem;
}

.oi-group-hint[hidden] {
  display: none;
}

/* ===================================================================
   共用手機介面元件 (mobile presentation layer)

   手機呈現層在 900.98px 以下啟用。這讓 768px 平板與桌面瀏覽器的
   narrow device toolbar 都走同一套手機資訊架構；DataTables 自己的
   responsive 欄位斷點仍保留原本規則, 不在這裡混用。

   桌機預設全部 display:none, 所以桌機端完全看不到這些節點, 也不會在
   1280px 上多出重複的操作入口。
   =================================================================== */

.mobile-only {
  display: none;
}

/* 手機殼層與表單是獨立 DOM；桌機預設完全不參與排版。 */
.mobile-shell-root {
  display: none;
}

.mobile-auth-only {
  display: none;
}

/* 回收桶的全選: 桌機的表頭只有 2.5rem 寬, 放不下文字 (名稱走 label 的
   title)。手機的卡片模式才把文字放出來 —— 見手機斷點裡的 .hupa-selectall。 */
.hupa-selectall-label {
  display: none;
}

/* /oi 有自己的一組手機摘要；桌機仍只看既有完整分析工作區。 */
.oi-mobile-only {
  display: none;
}

/* 錄音檔導覽的箭頭只在手機出現 (桌機放得下完整動作名)。不寫這一條的話
   桌機會變成「上一錄音檔‹」。 */
.audio-nav-glyph {
  display: none;
}

/* 「計算說明與資料涵蓋」的收合鈕。整條 header 都是觸發區, 右邊帶狀態字與
   箭頭 —— 只掛一個標題的話, 收起來時那張卡看起來就是「有標題、沒內容」。 */
.oi-method-toggle {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  min-height: 44px;
  color: inherit;
  text-align: left;
}

.oi-method-toggle-state {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  color: var(--hupa-text-muted);
  font-size: 0.8125rem;
  white-space: nowrap;
}

/* 篩選抽屜的外殼在桌機是透明的 —— 它包着 .hupa-filters, 而那層才是
   桌機的篩選面板。三條都要贏過 UA 的 [hidden]{display:none}: 作者樣式
   本來就贏 UA 樣式, 不需要 !important。markup 上的 hidden 是寫給手機的,
   目的是「JS 還沒載入時篩選就已經是收合的」。 */
.mobile-sheet,
.mobile-sheet-body,
.mobile-sheet-footer {
  display: block;
}

/* 影像瀏覽的三個手機面板 (篩選 / 單張操作 / 註記) 在桌機完全不存在 ——
   桌機那邊是工具列上的下拉、彈窗裡的操作列與縮圖上的註記浮層。不寫這一條
   的話, 它們會照 .mobile-sheet 的桌機預設 (display:block) 攤在頁面上。 */
.mobile-sheet.mobile-gallery-sheet {
  display: none;
}

@media (max-width: 900.98px) {

  .desktop-shell-only {
    display: none;
  }

  .desktop-ui-only {
    display: none;
  }

  .mobile-shell-root {
    display: block;
  }

  .mobile-auth-only {
    display: block;
  }

  .mobile-shell-root[hidden],
  .mobile-auth-only[hidden],
  .mobile-shell-sheet[hidden],
  .mobile-shell-backdrop[hidden],
  .mobile-auth-message[hidden] {
    display: none;
  }

  .mobile-only {
    display: block;
  }

  .gallery-toolbar {
    display: none;
  }

  /* --- /oi: 手機只保留摘要、排名、單一物種分布與 CSV ----------- */

  #oi-page,
  #oi-page .row,
  #oi-page [class*="col-"],
  #oi-page .card,
  #oi-page .card-body {
    min-width: 0;
    max-width: 100%;
  }

  #oi-page .oi-mobile-only {
    display: block;
  }

  #oi-page .oi-desktop-only {
    display: none !important;
  }

  #oi-page .oi-mobile-empty-data {
    display: none;
  }

  #oi-page button,
  #oi-page select,
  #oi-page .btn {
    min-height: 44px;
  }

  #oi-page .oi-mobile-name,
  #oi-page .oi-mobile-ranking,
  #oi-page .oi-mobile-site-distribution,
  #oi-page .oi-error-page,
  #oi-page p {
    overflow-wrap: anywhere;
  }

  /* 佈設涵蓋: 一站一段, 欄名變成標籤。桌機那張五欄表在同一個斷點被
     .oi-desktop-only 收起來。 */
  #oi-page .oi-coverage-item {
    padding: 0.75rem 0;
    border-bottom: 1px solid var(--hupa-border);
  }

  #oi-page .oi-coverage-item:last-child {
    border-bottom: 0;
  }

  #oi-page .oi-coverage-title {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    margin: 0 0 0.375rem;
    color: var(--hupa-heading);
    font-size: var(--hupa-m-title);
  }

  #oi-page .oi-coverage-status {
    flex: 0 0 auto;
    padding: 0.125rem 0.5rem;
    border-radius: 999px;
    background: var(--hupa-surface-hover);
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-micro);
  }

  /* 「未納入」是這張表存在的理由 (整批影像被丟掉卻沒人講), 不能跟
     「全數納入」長得一樣, 而且不能只靠顏色 —— 文字本身已經說明狀態。 */
  #oi-page .oi-coverage-status[data-status="none"] {
    background: var(--hupa-orange-soft);
    color: var(--hupa-orange-text);
  }

  #oi-page .oi-coverage-facts {
    margin: 0;
  }

  #oi-page .oi-coverage-facts > div {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.25rem 0;
  }

  #oi-page .oi-coverage-facts dt {
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-label);
    font-weight: 400;
  }

  #oi-page .oi-coverage-facts dd {
    margin: 0;
    color: var(--hupa-heading);
    font-size: var(--hupa-m-body);
    text-align: right;
  }

  #oi-page .oi-coverage-note {
    margin: 0.25rem 0 0;
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-meta);
  }

  #oi-page .oi-mobile-rank-row,
  #oi-page .oi-mobile-site-row {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    min-width: 0;
    padding: 0.75rem 0;
    border-bottom: 1px solid var(--hupa-border);
  }

  #oi-page .oi-mobile-rank-row:last-child,
  #oi-page .oi-mobile-site-row:last-child {
    border-bottom: 0;
  }

  #oi-page .oi-mobile-rank-number {
    flex: 0 0 2rem;
    color: var(--hupa-text-muted);
    font-weight: 600;
    text-align: center;
  }

  #oi-page .oi-mobile-name {
    flex: 1 1 auto;
    min-width: 0;
  }

  #oi-page .oi-mobile-value {
    flex: 0 0 auto;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
  }

  #oi-page .oi-mobile-csv {
    display: flex;
    align-items: center;
    justify-content: center;
  }

  #oi-page .oi-error-actions {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
  }

  /* --- 手機底部導覽 -------------------------------------------- */

  .left-sidebar {
    display: none;
  }

  .main-header .sidebar-toggle {
    display: none;
  }

  /* viewport-fit=cover 讓版面鋪到瀏海兩側, 所以橫放時左右也要讓開凹口。
     直放與沒有安全區的機型 env() 是 0, 等於沒寫這兩條。 */
  .page-wrapper {
    width: 100%;
    margin-left: 0;
    padding-left: env(safe-area-inset-left);
    padding-right: env(safe-area-inset-right);
    padding-bottom: calc(var(--hupa-mobile-nav-height) + env(safe-area-inset-bottom));
  }

  .mobile-shell-header {
    position: fixed;
    top: 0;
    right: 0;
    left: 0;
    z-index: 1038;
    display: flex;
    /* 48px 是列高; 上方安全區由 padding 撐開, 標頭自己蓋住狀態列那一段,
       這樣捲動時內容不會從瀏海底下透出來。 */
    height: calc(48px + env(safe-area-inset-top));
    padding-top: env(safe-area-inset-top);
    align-items: center;
    justify-content: space-between;
    padding-right: calc(0.75rem + env(safe-area-inset-right));
    padding-left: calc(1rem + env(safe-area-inset-left));
    background: var(--hupa-surface);
    border-bottom: 1px solid var(--hupa-border);
  }

  .mobile-shell-brand {
    display: inline-flex;
    align-items: center;
      /* 觸控目標下限 (第六批全站掃描: 原本是 47x30)。 */
    min-height: 44px;
  }

  .mobile-shell-brand img {
    display: block;
    width: auto;
    height: 30px;
  }

  .mobile-shell-icon-button {
    display: inline-flex;
    width: 44px;
    min-height: 44px;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--hupa-heading);
    font-size: 1.5rem;
  }

  .mobile-shell-icon-button:focus,
  .mobile-shell-icon-button:hover,
  .mobile-shell-icon-button[aria-expanded="true"] {
    background: var(--hupa-surface-hover);
    color: var(--hupa-primary);
    outline: none;
  }

  .mobile-bottom-nav {
    display: grid;
    position: fixed;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 1048;
    /* 欄數是殼層算出來的 (沒有 ai_audio 權限就只有三個入口)。寫死 4 欄的話,
       三個入口的帳號會在最右邊空出一格, 「更多」不貼邊。 */
    grid-template-columns: repeat(var(--mobile-primary-count, 4), minmax(0, 1fr));
    height: calc(var(--hupa-mobile-nav-height) + env(safe-area-inset-bottom));
    padding-right: env(safe-area-inset-right);
    padding-bottom: env(safe-area-inset-bottom);
    padding-left: env(safe-area-inset-left);
    background: var(--hupa-surface);
    border-top: 1px solid var(--hupa-border);
    box-shadow: var(--hupa-shadow-raised);
  }

  .mobile-bottom-nav.mobile-only {
    display: grid;
  }

  .mobile-bottom-nav-item {
    display: flex;
    min-width: 0;
    min-height: 44px;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    gap: 0.125rem;
    padding: 0.25rem;
    border: 0;
    background: none;
    color: var(--hupa-text-muted);
    font-size: 0.6875rem;
    line-height: 1.2;
    white-space: nowrap;
  }

  .mobile-bottom-nav-item .mdi {
    font-size: 1.375rem;
    line-height: 1;
  }

  .mobile-bottom-nav-item[aria-current="page"] {
    background: var(--hupa-surface-hover);
    color: var(--hupa-primary);
  }

  .mobile-shell-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1049;
    background: var(--hupa-overlay);
  }

  .mobile-shell-sheet {
    position: fixed;
    right: 0;
    bottom: calc(var(--hupa-mobile-nav-height) + env(safe-area-inset-bottom));
    left: 0;
    z-index: 1050;
    display: flex;
    max-height: min(72vh, calc(100dvh - var(--hupa-mobile-nav-height) - 1rem));
    flex-direction: column;
    overflow: hidden;
    border-radius: 1rem 1rem 0 0;
    background: var(--hupa-surface);
    box-shadow: var(--hupa-shadow-sheet);
  }

  .mobile-shell-account-sheet {
    max-height: min(44vh, 24rem);
  }

  .mobile-shell-sheet-header {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: space-between;
    min-height: 56px;
    padding: 0.375rem 0.75rem 0.375rem 1rem;
    border-bottom: 1px solid var(--hupa-border);
  }

  .mobile-shell-sheet-body {
    min-height: 0;
    padding: 0.5rem 1rem calc(1rem + env(safe-area-inset-bottom));
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .mobile-shell-menu-section {
    padding: 0.375rem 0 0.625rem;
  }

  .mobile-shell-menu-section + .mobile-shell-menu-section {
    border-top: 1px solid var(--hupa-border);
  }

  .mobile-shell-menu-section h3 {
    margin: 0.25rem 0 0.125rem;
    color: var(--hupa-text-muted);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.04em;
  }

  .mobile-shell-menu-section a,
  .mobile-shell-account-action {
    display: flex;
    min-height: 48px;
    align-items: center;
    gap: 0.75rem;
    color: var(--hupa-heading);
    font-weight: 600;
    text-decoration: none;
  }

  .mobile-shell-menu-section a .mdi,
  .mobile-shell-account-action .mdi {
    width: 1.5rem;
    color: var(--hupa-text-muted);
    font-size: 1.2rem;
    text-align: center;
  }

  .mobile-shell-account-identity {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.5rem 0 0.875rem;
    border-bottom: 1px solid var(--hupa-border);
  }

  .mobile-shell-account-identity > span:last-child {
    display: flex;
    min-width: 0;
    flex-direction: column;
  }

  .mobile-shell-account-identity strong,
  .mobile-shell-account-identity small {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .mobile-shell-account-avatar {
    display: inline-flex;
    width: 40px;
    height: 40px;
    flex: 0 0 40px;
    align-items: center;
    justify-content: center;
    border-radius: 999px;
    background: var(--hupa-primary-soft);
    color: var(--hupa-primary);
    font-weight: 700;
  }

  /* 開著覆蓋層時背景不跟著捲。三個 class 是三個控制器各自掛的:
     mobile-shell.js 掛 .mobile-shell-sheet-open, mobile-ui.js 掛
     .mobile-sheet-open (它以前掛了但沒有對應規則, 所以篩選抽屜底下的清單
     一直可以捲), mobile-overlay.js 掛 .hupa-overlay-open (詳情層、全頁表單、
     檢視器、單列操作 sheet 共用, 巢狀時用計數解鎖)。
     html 與 body 兩層都要: iOS 只鎖 body 時捲動會落到 html 上。 */
  html.hupa-overlay-open,
  html.hupa-overlay-open body,
  body.mobile-shell-sheet-open,
  body.mobile-sheet-open {
    overflow: hidden;
    overscroll-behavior: none;
  }

  /* --- 手機專用登入 / 註冊 / 修改密碼 ------------------------- */

  .mobile-auth-only {
    min-height: 100dvh;
    background: var(--hupa-page-bg);
  }

  .mobile-auth-page {
    display: flex;
    width: min(100%, 31rem);
    min-height: 100dvh;
    flex-direction: column;
    margin: 0 auto;
    padding: max(1.5rem, env(safe-area-inset-top)) 1rem max(1rem, env(safe-area-inset-bottom));
  }

  .mobile-auth-brand {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5625rem;
    padding: 4vh 0 2.25rem;
    color: var(--hupa-primary);
    font-size: 1.5625rem;
    font-weight: 800;
    letter-spacing: -0.04em;
  }

  .mobile-auth-brand img {
    width: auto;
    height: 42px;
  }

  .mobile-auth-panel {
    padding: 1.5rem 1.25rem;
    border: 1px solid var(--hupa-border);
    border-radius: 0.875rem;
    background: var(--hupa-surface);
    box-shadow: var(--hupa-shadow-sm);
  }

  .mobile-auth-eyebrow {
    margin: 0 0 0.375rem;
    color: var(--hupa-primary);
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.08em;
  }

  .mobile-auth-panel h1 {
    margin: 0;
    color: var(--hupa-heading);
    font-size: 1.625rem;
    line-height: 1.25;
  }

  .mobile-auth-subtitle {
    margin: 0.5rem 0 1.5rem;
    color: var(--hupa-text-muted);
    font-size: 0.875rem;
    line-height: 1.55;
    overflow-wrap: anywhere;
  }

  .mobile-auth-field {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    margin-top: 1rem;
    color: var(--hupa-heading);
    font-size: 0.8125rem;
    font-weight: 600;
  }

  .mobile-auth-field input {
    width: 100%;
    min-height: 48px;
    padding: 0.75rem 0.875rem;
    border: 1px solid var(--hupa-border);
    border-radius: var(--hupa-radius-sm);
    background: var(--hupa-surface);
    color: var(--hupa-heading);
    font-size: 1rem;
    font-weight: 400;
  }

  .mobile-auth-field input:focus {
    border-color: var(--hupa-primary);
    box-shadow: var(--hupa-focus-ring);
    outline: none;
  }

  .mobile-auth-password {
    position: relative;
    display: block;
  }

  .mobile-auth-password input {
    padding-right: 3.25rem;
  }

  .mobile-auth-password button {
    position: absolute;
    top: 2px;
    right: 2px;
    display: inline-flex;
    width: 44px;
    min-height: 44px;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--hupa-text-muted);
    font-size: 1.25rem;
  }

  .mobile-auth-submit {
    width: 100%;
    min-height: 48px;
    margin-top: 1.5rem;
    border: 0;
    border-radius: var(--hupa-radius-sm);
    background: var(--hupa-primary);
    color: var(--hupa-on-dark);
    font-weight: 700;
  }

  .mobile-auth-submit:disabled {
    opacity: 0.65;
  }

  .mobile-auth-message,
  .mobile-auth-notice {
    margin-top: 1rem;
    padding: 0.75rem;
    border: 1px solid;
    border-radius: var(--hupa-radius-sm);
    font-size: 0.8125rem;
    line-height: 1.5;
    overflow-wrap: anywhere;
  }

  .mobile-auth-message--error {
    border-color: var(--hupa-banner-danger-border);
    background: var(--hupa-banner-danger-bg);
    color: var(--hupa-banner-danger-text);
  }

  .mobile-auth-message--success {
    border-color: var(--hupa-success-border);
    background: var(--hupa-success-soft);
    color: var(--hupa-success-text);
  }

  .mobile-auth-notice {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
    border-color: var(--hupa-banner-warning-border);
    background: var(--hupa-banner-warning-bg);
    color: var(--hupa-banner-warning-text);
  }

  .mobile-auth-field small {
    color: var(--hupa-text-muted);
    font-size: 0.75rem;
    font-weight: 400;
  }

  .mobile-auth-field small.is-valid {
    color: var(--hupa-success);
  }

  .mobile-auth-alt,
  .mobile-auth-vendor {
    margin: 1.25rem 0 0;
    color: var(--hupa-text-muted);
    font-size: 0.8125rem;
    text-align: center;
  }

  .mobile-auth-vendor {
    margin-top: auto;
    padding-top: 1.5rem;
  }

  .mobile-auth-back {
    display: inline-flex;
    min-height: 44px;
    align-self: flex-start;
    align-items: center;
    gap: 0.4rem;
    margin-bottom: 1rem;
    color: var(--hupa-heading);
    font-size: 0.875rem;
    font-weight: 600;
  }

  .mobile-auth-page--signup,
  .mobile-auth-page--password {
    justify-content: center;
  }

  .hupa-auth .page-wrapper {
    padding: 0;
  }

  /* 專用帳號按鈕取代桌機下拉選單；兩者同時存在會多出一個箭頭觸發區。 */
  .desktop-account-menu {
    display: none;
  }

  .mobile-sheet.mobile-gallery-sheet {
    display: flex;
  }

  .mobile-sheet.mobile-gallery-sheet[hidden] {
    display: none;
  }

  /* --- 底部抽屜 -------------------------------------------------- */

  /* 最大高度 85vh 是規格寫死的: 留 15% 讓使用者看得到底下還有頁面,
     知道這是一層覆蓋而不是換頁。body 自己捲動, footer 固定。 */
  .mobile-sheet {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1050;
    display: flex;
    flex-direction: column;
    max-height: 85vh;
    background: var(--hupa-surface);
    border-radius: 0.75rem 0.75rem 0 0;
    box-shadow: var(--hupa-shadow-sheet);
  }

  .mobile-sheet[hidden] {
    display: none;
  }

  /* 遮罩在抽屜之下 (1050)、頁面之上。沒有它的話背後的列表還看得到,
     抽屜看起來像做一半 —— 這是實機回饋的其中一點。 */
  .mobile-sheet-backdrop {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 1049;
    background: var(--hupa-overlay);
  }

  .mobile-sheet-backdrop[hidden] {
    display: none;
  }

  /* 主要按鈕告訴使用者「按下去就回去看結果」。跟次要的「清除條件」
     各佔一半, 兩顆都到得了 44px。 */
  .mobile-filter-apply {
    flex: 1 1 0;
    min-height: 44px;
  }

  .mobile-sheet-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.75rem 1rem;
    border-bottom: 1px solid var(--hupa-border);
  }

  .mobile-sheet-body {
    flex: 1 1 auto;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    padding: 1rem;
  }

  /* footer 是固定的「清除條件 / 套用」, 所以它要吃安全區, body 才不會
     被 iPhone 的 home indicator 蓋住最後一顆按鈕。 */
  /* footer 在 DOM 上是 .hupa-filters 的子元素 (桌機的 .filter-actions 選擇器
     才不會斷掉), 所以它跟欄位在同一個滾動容器裡。用 sticky 而不是
     把它搬到抽屜外層: 效果一樣是固定在底, 但 DOM 沒動過。 */
  .mobile-sheet-footer {
    position: sticky;
    bottom: 0;
    z-index: 1;
    margin: 0 -1rem -1rem;
    padding: 0.75rem 1rem calc(0.75rem + env(safe-area-inset-bottom));
    background: var(--hupa-surface);
    border-top: 1px solid var(--hupa-border);
  }

  /* 底部改成直向堆疊。三個控制項擠在一行時, 「清除篩選」會被壓成
     逐字直排 —— 中文可以逐字斷行, 所以 flex 以為它塞得下。 */
  .mobile-sheet-footer {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    align-items: stretch;
  }

  .mobile-sheet-footer .filter-actions {
    flex-wrap: wrap;
    margin-top: 0;
  }

  /* 檢視開關 (「顯示已刪除的佈設」) 自己一行。它不是按鈕, 不該跟
     按鈕掆寬度。 */
  .mobile-sheet-footer .filter-actions > .custom-control {
    flex: 1 0 100%;
    margin-right: 0;
  }

  .mobile-sheet-footer .filter-actions > .btn {
    flex: 1 1 auto;
    min-height: 44px;
    white-space: nowrap;
  }

  .mobile-sheet-footer .filter-actions > .btn {
    flex: 1 1 0;
    min-height: 44px;
  }

  /* 抽屜已經有自己的邊框與內距, 裡面的面板再畫一層框就是雙框。 */
  .mobile-sheet-body > .hupa-filters {
    margin-bottom: 0;
    padding: 0;
    background: none;
    border: 0;
  }

  /* 起訖 (.filter-range) 在手機疊成一欄。

     桌機那條是 `grid-template-columns: 1fr 1fr`, 而 `1fr` 展開是
     `minmax(auto, 1fr)` —— 下限是內容的 min-content。桌機的起訖是 jQuery
     浮層的文字框 (placeholder 只有「起」「迄」), min-content 很小, 兩欄沒事;
     手機把同一組欄位換成原生選擇器 (mobile-datetime.js 的
     data-mobile-native-datetime), zh-TW 的 datetime-local 量出來 min-content
     是 239px (「yyyy/月/dd -- --:--」加上面板的 .59rem/1rem padding)。
     抽屜在 375px 上可用寬 339px, 兩欄合計 486px —— 第二格 (迄) 整個畫到
     抽屜外, 就是實機回報的「訖日破版」。

     不改成 `minmax(0, 1fr)` 兩欄了事: 那只是把欄寬壓到 165px, 破版換成框內
     的日期字串被裁掉, 一樣讀不完整。疊成一欄每格有 337px, 整串日期時間放得下。
     單欄仍寫 minmax(0, 1fr) 而不是 1fr, 下限才不會又是那個 239px。 */
  .hupa-filters .filter-range {
    grid-template-columns: minmax(0, 1fr);
  }

  /* --- 篩選入口與條件標籤 ---------------------------------- */

  .mobile-filter-bar {
    display: flex;
    margin-bottom: 0.5rem;
  }

  .mobile-filter-trigger {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    min-height: 44px;
    padding: 0.5rem 1rem;
    background: var(--hupa-surface);
    border: 1px solid var(--hupa-border);
    border-radius: 0.25rem;
  }

  /* 條件數量徽章。沒條件時由 JS 上 hidden, 按鈕就只剩「篩選」。 */
  .mobile-filter-count {
    min-width: 1.375rem;
    padding: 0 0.375rem;
    background: var(--hupa-primary);
    border-radius: 0.75rem;
    color: var(--hupa-on-dark);
    font-size: 0.75rem;
    line-height: 1.375rem;
    text-align: center;
  }

  .mobile-filter-count[hidden] {
    display: none;
  }

  .mobile-filter-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem;
    margin-bottom: 0.75rem;
  }

  .mobile-filter-chips:empty {
    display: none;
  }

  .mobile-filter-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    min-height: 32px;
    padding: 0.25rem 0.625rem;
    background: var(--hupa-surface-hover);
    border: 1px solid var(--hupa-border);
    border-radius: 1rem;
    color: var(--hupa-heading);
    font-size: 0.8125rem;
    overflow-wrap: anywhere;
  }

  /* --- 新增 FAB -------------------------------------------------- */

  /* 手機只能有一個新增入口。桌機那顆帶文字的按鈕在這裡收掉 ——
     兩個入口同時存在是規格明文禁止的。只藏按鈕本體: /team 的新增
     需要先輸入團隊名稱, 那個欄位要留在頁首。 */
  .desktop-create-action {
    display: none;
  }

  /* 篩選控制項仍留在 DOM 並維持桌機查詢流程，只在手機隱藏次要欄位。 */
  .desktop-filter-only {
    display: none;
  }

  /* 有手機卡片的頁面, 桌機那張表在這裡收掉。兩份同樣的資料疊著
     出現是規格明文禁止的 (「手機版不呈現被壓縮的桌機表格」)。

     是 opt-in 而不是一律藏 .basic-data-table: 還沒有手機卡片的頁面
     被藏掉的話, 手機上會是一頁空白。 */
  .desktop-table-only {
    display: none;
  }

  /* --- 分頁切換 (兩份並排清單改一次顯示一份) -------------------- */

  .mobile-tablist.mobile-only {
    display: flex;
  }

  .mobile-tab {
    flex: 1 1 0;
    min-height: 44px;
    padding: 0.5rem;
    border: 0;
    background: var(--hupa-surface);
    color: var(--hupa-text-muted);
    font-weight: 600;
  }

  .mobile-tab + .mobile-tab {
    border-left: 1px solid var(--hupa-border);
  }

  .mobile-tab[aria-selected="true"] {
    background: var(--hupa-primary);
    color: var(--hupa-on-dark);
  }

  /* 桌機的雙欄結構整段不動; 只有在這個斷點內, 未啟用的分頁面板才收起來。 */
  .mobile-tabpanel[data-mobile-tab-active="false"] {
    display: none;
  }

  /* --- /trash: 同一張表用 CSS 轉成卡片, 不建第二份 DOM ------------ */

  /* wireCard() (trash.html) 對 .hupa-pick / .hupa-trash-expand 這批節點的
     唯一性假設很重: 全選要數出「總共幾個 checkbox」, 展開要對到剛好一組
     detail row。複製一份 Jinja 迴圈做手機卡片的話, 這些查詢全部會多抓到
     一個節點。所以這裡讓同一張 <table> 在窄螢幕直接重排成卡片外觀,
     JS 完全不用碰。 */
  .hupa-trash-table-mobile thead th:not(:first-child) {
    /* 標題文字在卡片模式不需要 —— data-label 已經把欄位名稱印在每個值前面;
       只留第一欄, 因為全選 checkbox 本體在裡面。 */
    display: none;
  }

  .hupa-trash-table-mobile tbody tr {
    display: block;
    margin-bottom: 0.75rem;
    padding: 0.625rem 0.75rem;
    border: 1px solid var(--hupa-border);
    border-radius: 0.5rem;
  }

  .hupa-trash-table-mobile tbody tr.hupa-trash-detail,
  .hupa-trash-table-mobile tbody tr.hupa-audio-detail {
    margin-top: -0.75rem;
    padding-top: 0;
    border-top: 0;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
  }

  .hupa-trash-table-mobile tbody tr[hidden] {
    display: none;
  }

  .hupa-trash-table-mobile td {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.375rem 0;
    text-align: left;
  }

  .hupa-trash-table-mobile td.text-right {
    text-align: left;
  }

  .hupa-trash-table-mobile td::before {
    content: attr(data-label);
    flex: 0 0 auto;
    color: var(--hupa-text-muted);
    font-size: 0.75rem;
  }

  .hupa-trash-table-mobile td[colspan] {
    display: block;
    padding: 0;
  }

  .hupa-trash-table-mobile td[colspan]::before {
    content: none;
  }

  .hupa-trash-selectall,
  .hupa-pick,
  .hupa-detail-pick {
    width: 1.25rem;
    height: 1.25rem;
  }

  /* 全選在卡片模式是一列, 不是一個浮在標題下面的裸方塊 (使用者實機回報)。
     表頭在這個斷點只剩這一格, 所以讓它自己長成一整列。 */
  .hupa-trash-table-mobile thead th:first-child {
    display: block;
    width: auto !important;
    padding: 0 0 0.5rem;
    border: 0;
    text-align: left !important;
  }

  .hupa-selectall {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    min-height: 44px;
    margin: 0;
    padding: 0 0.75rem;
    border: 1px solid var(--hupa-border);
    border-radius: 0.5rem;
    background: var(--hupa-surface);
    color: var(--hupa-heading);
    font-size: var(--hupa-m-body);
    font-weight: 400;
  }

  .hupa-selectall-label {
    display: inline;
  }

  .hupa-trash-expand {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  /* --- 影像瀏覽的手機呈現層 --------------------------------------
     桌機的縮圖網格與預覽 modal 在這個斷點整組收起來 (.desktop-ui-only),
     手機用自己的方格與檢視器, 兩邊沒有共用節點, 也沒有互相搬 DOM。 */
  .mobile-gallery-root {
    display: block;
  }

  .mobile-gallery-filterbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    margin-bottom: 0.5rem;
  }

  .mobile-gallery-total {
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-meta);
    font-variant-numeric: tabular-nums;
  }

  .mobile-gallery-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem;
    margin-bottom: 0.5rem;
  }

  .mobile-gallery-chips:empty {
    display: none;
  }

  .mobile-gallery-chip {
    display: inline-flex;
    align-items: center;
    max-width: 100%;
    padding: 0.125rem 0.5rem;
    border-radius: 999px;
    background: var(--hupa-surface-hover);
    color: var(--hupa-heading);
    font-size: var(--hupa-m-micro);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .mobile-gallery-chip.is-muted {
    color: var(--hupa-text-muted);
  }

  /* 註記跟物種要分得出來: 註記是人寫上去的字, 物種是辨識結果。 */
  .mobile-gallery-chip.is-tag {
    background: var(--hupa-orange-soft);
    color: var(--hupa-orange-text);
  }

  /* 一列兩張。再窄就看不出拍到什麼, 再寬則一屏放不下幾張。 */
  .mobile-gallery-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.5rem;
  }

  .mobile-gallery-tile {
    position: relative;
    min-width: 0;
    border: 1px solid var(--hupa-border);
    border-radius: var(--hupa-radius);
    background: var(--hupa-surface);
    overflow: hidden;
  }

  .mobile-gallery-tile-open {
    display: block;
    width: 100%;
    padding: 0;
    border: 0;
    background: none;
    text-align: left;
  }

  .mobile-gallery-tile-frame {
    position: relative;
    display: block;
    aspect-ratio: 4 / 3;
    background: var(--hupa-letterbox);
  }

  .mobile-gallery-tile-image {
    display: block;
    width: 100%;
    height: 100%;
  }

  /* AI 框。座標是照片的原生像素 (見 mediaCanvas), 線寬改用畫面單位算,
     否則同一個值在不同解析度的照片上會差十幾倍粗。 */
  .mobile-gallery-box {
    fill: none;
    stroke: var(--hupa-warning);
    stroke-width: 2;
    vector-effect: non-scaling-stroke;
  }

  /* 未核認的框用虛線, 不是只換顏色。 */
  .mobile-gallery-box[data-review="pending"] {
    stroke-dasharray: 6 4;
  }

  .mobile-gallery-tile-frame.is-missing {
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--hupa-surface-hover);
  }

  .mobile-gallery-tile-missing {
    padding: 0 0.5rem;
    color: var(--hupa-danger);
    font-size: var(--hupa-m-micro);
    text-align: center;
  }

  .mobile-gallery-tile-type,
  .mobile-gallery-tile-labels {
    position: absolute;
    bottom: 0.25rem;
    padding: 0.0625rem 0.375rem;
    border-radius: 999px;
    background: var(--hupa-scrim-strong);
    color: var(--hupa-on-dark);
    font-size: var(--hupa-m-micro);
    line-height: 1.4;
  }

  .mobile-gallery-tile-type {
    left: 0.25rem;
  }

  .mobile-gallery-tile-labels {
    right: 0.25rem;
  }

  .mobile-gallery-tile-caption {
    display: block;
    padding: 0.375rem 0.5rem 0.5rem;
  }

  .mobile-gallery-tile-time {
    display: block;
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-meta);
    font-variant-numeric: tabular-nums;
  }

  .mobile-gallery-tile-species {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
    margin-top: 0.25rem;
  }

  /* 精選是方格上唯一的直接操作, 疊在照片右上角。44px 的觸控區靠內距撐出來,
     圖示本身不必那麼大。 */
  .mobile-gallery-tile-favorite {
    position: absolute;
    top: 0;
    right: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    border: 0;
    background: none;
    color: var(--hupa-on-dark);
    font-size: 1.25rem;
    text-shadow: var(--hupa-shadow-on-media);
  }

  .mobile-gallery-tile-favorite.is-selected {
    color: var(--hupa-danger);
  }

  .mobile-gallery-empty {
    margin: 1.5rem 0;
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-body);
    text-align: center;
  }

  /* 桌機限定功能的說明。規格要求手機顯示原因與去處, 而不是一排失效控制項。 */
  .mobile-gallery-desktop-note {
    margin: 1rem 0 0;
    padding: 0.625rem 0.75rem;
    border-radius: var(--hupa-radius);
    background: var(--hupa-surface-hover);
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-meta);
  }

  .mobile-gallery-filter-field {
    margin-bottom: 0.875rem;
  }

  .mobile-gallery-filter-field label {
    display: block;
    margin-bottom: 0.25rem;
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-label);
  }

  .mobile-gallery-filter-field .form-control {
    min-height: 48px;
  }

  /* 已精選的愛心保持紅色 (狀態不只靠文字), 但整列文字維持一般色。 */
  .mobile-sheet-item[data-selected="true"] .mdi {
    color: var(--hupa-danger);
  }

  .mobile-gallery-taglist {
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem;
    margin-bottom: 0.875rem;
  }

  .mobile-gallery-taglist-empty {
    margin: 0;
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-meta);
  }

  /* --- 檢視器 ---------------------------------------------------- */
  .mobile-gallery-viewer {
    position: fixed;
    inset: 0;
    /* 要蓋住頁面但留在 sheet 遮罩 (1049) 與 sheet (1050) 之下, 不然操作
       面板打開時背景不會變暗。底部導覽由 .mobile-gallery-viewer-open 收掉,
       所以不必爬到 1048 之上。 */
    z-index: 1040;
    display: flex;
    flex-direction: column;
    background: var(--hupa-page-bg);
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .mobile-gallery-viewer[hidden] {
    display: none;
  }

  /* 檢視器是一整層, 底部導覽要讓位 —— 不然「返回影像瀏覽」與分頁列會疊在
     同一塊。 */
  .mobile-gallery-viewer-open .mobile-bottom-nav {
    display: none;
  }

  .mobile-gallery-viewer-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding: calc(0.5rem + env(safe-area-inset-top)) 0.5rem 0.5rem;
    border-bottom: 1px solid var(--hupa-border);
    background: var(--hupa-surface);
  }

  .mobile-gallery-viewer-back {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 0.5rem;
    border: 0;
    background: none;
    color: var(--hupa-primary);
    font-size: var(--hupa-m-label);
  }

  .mobile-gallery-viewer-back::before {
    content: '\2039';
    margin-right: 0.375rem;
    font-size: 1.25rem;
    line-height: 1;
  }

  .mobile-gallery-viewer-position {
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-meta);
    font-variant-numeric: tabular-nums;
  }

  .mobile-gallery-viewer-more {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    border: 0;
    background: none;
    color: var(--hupa-heading);
    font-size: 1.25rem;
  }

  /* 影像要在首屏 —— 舞台高度綁視窗高而不是內容高, 捲動之前就看得到整張圖。 */
  /* dvh 而不是 vh: vh 在 iOS 是「網址列收起來時」的高度, 捲動時舞台會
     跟著長高再縮回去, 圖片跟著跳。 */
  .mobile-gallery-stage {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 52dvh;
    background: var(--hupa-letterbox);
  }

  /* 原圖還在下載時給一條動態底紋。純黑的舞台跟「這張檔案不在磁碟上」
     長得一樣, 使用者會以為壞了而一直按下一張。 */
  .mobile-gallery-stage.is-loading {
    background-image: linear-gradient(
      100deg,
      transparent 30%,
      var(--hupa-letterbox-shimmer) 50%,
      transparent 70%);
    background-size: 220% 100%;
    animation: hupa-stage-loading 1.1s linear infinite;
  }

  @keyframes hupa-stage-loading {
    from { background-position: 120% 0; }
    to { background-position: -120% 0; }
  }

  @media (prefers-reduced-motion: reduce) {
    .mobile-gallery-stage.is-loading {
      animation: none;
    }
  }

  /* 橫放時 52dvh 只剩約 200px, 照片小到認不出物種 —— 而橫放正是使用者
     為了把照片放大才做的動作。改成吃掉除了標頭與換張列以外的高度,
     資訊區往下捲得到。 */
  @media (orientation: landscape) {
    .mobile-gallery-stage {
      height: calc(100dvh - 8.5rem);
      min-height: 11rem;
    }
  }

  .mobile-gallery-stage-media {
    display: block;
    max-width: 100%;
    max-height: 100%;
  }

  /* 影片的原生尺寸是 300x150, 不撐開的話整塊舞台幾乎是空的。contain 保住
     直式影片的比例。 */
  video.mobile-gallery-stage-media {
    width: 100%;
    height: 100%;
    object-fit: contain;
  }

  .mobile-gallery-steps {
    display: flex;
    gap: 0.5rem;
    padding: 0.5rem 0.75rem;
  }

  .mobile-gallery-step {
    display: inline-flex;
    flex: 1 1 0;
    align-items: center;
    justify-content: center;
    gap: 0.25rem;
    min-height: 44px;
    border: 1px solid var(--hupa-border);
    border-radius: var(--hupa-radius);
    background: var(--hupa-surface);
    color: var(--hupa-heading);
    font-size: var(--hupa-m-label);
  }

  .mobile-gallery-step[disabled] {
    color: var(--hupa-text-muted);
    opacity: 0.6;
  }

  .mobile-gallery-meta {
    padding: 0 0.75rem;
  }

  .mobile-gallery-meta-title {
    margin: 0;
    color: var(--hupa-heading);
    font-size: var(--hupa-m-title);
    overflow-wrap: anywhere;
  }

  .mobile-gallery-meta-time {
    margin: 0.125rem 0 0.5rem;
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-meta);
  }

  .mobile-gallery-meta-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem;
    margin-bottom: 0.5rem;
  }

  .mobile-gallery-meta-facts {
    margin: 0.75rem 0 0;
  }

  .mobile-gallery-meta-fact {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.5rem 0;
    border-top: 1px solid var(--hupa-border);
  }

  .mobile-gallery-meta-fact dt {
    margin: 0;
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-label);
    font-weight: 400;
  }

  .mobile-gallery-meta-fact dd {
    margin: 0;
    color: var(--hupa-heading);
    font-size: var(--hupa-m-body);
    overflow-wrap: anywhere;
    text-align: right;
  }

  .mobile-gallery-viewer .mobile-gallery-desktop-note {
    margin: 1rem 0.75rem calc(1rem + env(safe-area-inset-bottom));
  }

  /* 手機詳細面板裡的標記清單: 桌機那份深色 .card-body 底色搬過來會整塊
     變黑底, 這裡收成跟其他 mobile-sheet 一致的淺色。只有這個詳細面板的
     slot 底下會出現 .card-body, 範圍不會波及其他 mobile-sheet。 */
  .mobile-sheet-body .card-body {
    background: transparent !important;
    max-height: none !important;
  }

  /* 56px 是規格值。bottom 同樣加安全區, 否則在有 home indicator 的機型
     上圓鈕會壓在系統手勢區上, 點不到。 */
  .mobile-fab {
    position: fixed;
    right: 1rem;
    bottom: calc(var(--hupa-mobile-nav-height) + 1rem + env(safe-area-inset-bottom));
    z-index: 1040;
    width: 56px;
    height: 56px;
    min-width: 44px;
    min-height: 44px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--hupa-primary);
    color: var(--hupa-on-dark);
    font-size: 1.75rem;
    line-height: 1;
    box-shadow: var(--hupa-shadow-raised);
  }

  .mobile-fab-menu {
    position: fixed;
    right: 1rem;
    bottom: calc(var(--hupa-mobile-nav-height) + 4.5rem + env(safe-area-inset-bottom));
    z-index: 1040;
    min-width: 12rem;
    padding: 0.25rem 0;
    background: var(--hupa-surface);
    border-radius: 0.5rem;
    box-shadow: var(--hupa-shadow-raised);
  }

  .mobile-fab-menu[hidden] {
    display: none;
  }

  /* 清單最後一張卡會被 FAB 蓋住, 所以卡片列表自己留出 FAB 的高度。
     不用 body padding, 否則沒有 FAB 的頁面也會多一塊空白。 */
  .mobile-card-list {
    display: block;
    margin: 0;
    padding: 0 0 calc(var(--hupa-mobile-nav-height) + 5rem + env(safe-area-inset-bottom));
    list-style: none;
  }

  /* --- 清單摘要、狀態與分頁 ------------------------------------ */

  .mobile-collection-summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    margin-bottom: 0.5rem;
    color: var(--hupa-text-muted);
    font-size: 0.8125rem;
  }

  .mobile-collection-sort {
    min-height: 44px;
    padding: 0.25rem 0.5rem;
    border: 0;
    background: none;
    color: inherit;
  }

  .mobile-sort-menu {
    position: absolute;
    right: 1rem;
    z-index: 5;
    min-width: 10rem;
    padding: 0.25rem 0;
    background: var(--hupa-surface);
    border: 1px solid var(--hupa-border);
    border-radius: 0.5rem;
    box-shadow: var(--hupa-shadow-raised);
  }

  .mobile-sort-menu[hidden] {
    display: none;
  }

  .mobile-sort-menu button {
    display: block;
    width: 100%;
    min-height: 44px;
    padding: 0.625rem 1rem;
    border: 0;
    background: none;
    text-align: left;
  }

  .mobile-collection-state {
    padding: 1rem 0;
    text-align: center;
  }

  .mobile-collection-state[hidden] {
    display: none;
  }

  /* 骨架要有最小高度, 否則資料載入後版面會從幾乎零高暴漲。 */
  .mobile-card-skeleton {
    min-height: 72px;
    margin-bottom: 0.5rem;
    background: var(--hupa-surface-hover);
    border-radius: 0.5rem;
  }

  /* 停用的按鈕靠 opacity 變掃, 位置保留 —— 版面不跟著頁碼跳動。 */
  .mobile-pager {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding-bottom: calc(var(--hupa-mobile-nav-height) + 5rem + env(safe-area-inset-bottom));
  }

  .mobile-pager-btn {
    min-width: 44px;
    min-height: 44px;
    padding: 0.5rem 1rem;
    background: var(--hupa-surface);
    border: 1px solid var(--hupa-border);
    border-radius: 0.25rem;
  }

  .mobile-pager-btn[disabled] {
    opacity: 0.45;
  }

  /* 卡片清單自己的底部留白改由分頁列提供 (FAB 遮罩區)。 */
  .mobile-collection .mobile-card-list {
    padding-bottom: 0;
  }

  .mobile-collection {
    position: relative;
  }

  /* --- 資料卡片 -------------------------------------------------- */

  .mobile-card {
    position: relative;
    margin-bottom: 0.5rem;
    background: var(--hupa-surface);
    border: 1px solid var(--hupa-border);
    border-radius: 0.5rem;
  }

  .mobile-card-header {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
    padding: 0.75rem;
  }

  /* 展開鈕吃掉 header 剩下的寬度, 「⋮」自己一格。兩者都是獨立的觸控
     目標 —— 規格要求點選單不得同時展開卡片。 */
  .mobile-card-toggle {
    flex: 1 1 auto;
    min-height: 44px;
    padding: 0;
    border: 0;
    background: none;
    text-align: left;
  }

  .mobile-card-title {
    display: block;
    font-weight: 600;
    overflow-wrap: anywhere;
  }

  /* 狀態徽章。顏色不是唯一的訊息源 —— 徽章裡面本來就是狀態文字。 */
  .mobile-card-status {
    display: inline-block;
    margin-top: 0.25rem;
    padding: 0.125rem 0.5rem;
    background: var(--hupa-surface-hover);
    border-radius: 0.75rem;
    font-size: 0.75rem;
  }

  /* 展開箭頭。展開態翻面 —— 與 aria-expanded 同一個事實的視覺形式。 */
  .mobile-card-chevron {
    position: absolute;
    top: 0.875rem;
    right: 3rem;
  }

  .mobile-card[data-expanded="true"] .mobile-card-chevron {
    transform: rotate(180deg);
  }

  /* 分隔點直接寫字元。用 CSS 轉義 '\30FB' 寫過一次, 瀏覽器把它斷成
     \30F 加上字面的 B, 畫面上每兩項摘要中間會冒出一個「FB」。
     轉義序列需要結尾空白才會停, 直接寫字元沒有這個陷阱。 */
  .mobile-card-summary + .mobile-card-summary::before {
    content: '･';
  }

  .mobile-card-summaries {
    display: block;
    margin-top: 0.25rem;
    color: var(--hupa-text-muted);
    font-size: 0.8125rem;
    overflow-wrap: anywhere;
  }

  .mobile-card-details {
    margin: 0;
    padding: 0 0.75rem 0.75rem;
  }

  .mobile-card-details[hidden] {
    display: none;
  }

  .mobile-card-details dt {
    color: var(--hupa-text-muted);
    font-size: 0.75rem;
    font-weight: 500;
  }

  /* 長檔名、長 URL 沒有空白可斷, anywhere 是唯一不會撐破卡片的值。 */
  .mobile-card-details dd {
    margin: 0 0 0.5rem;
    overflow-wrap: anywhere;
  }

  /* /deploy 在手機是獨立資訊架構，不把桌機表格硬塞進窄畫面，也不沿用
     一筆一張浮卡的視覺。外層卡片退場，結果改成一個有分隔線的清單面板。 */
  .deployment-results-card {
    background: transparent;
    border: 0;
    box-shadow: none;
  }

  .deployment-results-card > .card-body {
    padding: 0;
  }

  #deploy-mobile-results .mobile-card-list {
    overflow: hidden;
    background: var(--hupa-surface);
    border: 1px solid var(--hupa-border);
    border-radius: var(--hupa-radius);
    box-shadow: var(--hupa-shadow-sm);
  }

  #deploy-mobile-results .deploy-mobile-row {
    margin: 0;
    border: 0;
    border-radius: 0;
  }

  #deploy-mobile-results .deploy-mobile-row + .deploy-mobile-row {
    border-top: 1px solid var(--hupa-border);
  }

  #deploy-mobile-results .mobile-card-header {
    align-items: flex-start;
    gap: 0.25rem;
    padding: 0;
  }

  #deploy-mobile-results .mobile-card-toggle {
    position: relative;
    min-width: 0;
    padding: 0.875rem 2rem 0.875rem 1rem;
    border-radius: 0;
  }

  #deploy-mobile-results .mobile-card-toggle:hover,
  #deploy-mobile-results .mobile-card-toggle:focus {
    background: var(--hupa-surface-hover);
  }

  #deploy-mobile-results .mobile-card-eyebrow {
    display: block;
    margin-bottom: 0.125rem;
    color: var(--hupa-primary);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.02em;
  }

  #deploy-mobile-results .mobile-card-title {
    padding-right: 0.5rem;
    color: var(--hupa-heading);
    font-size: 1rem;
    line-height: 1.35;
  }

  #deploy-mobile-results .mobile-card-status {
    margin-top: 0.375rem;
    background: var(--hupa-orange-soft);
    color: var(--hupa-orange-text);
    font-weight: 600;
  }

  #deploy-mobile-results .mobile-card-summaries {
    display: grid;
    gap: 0.125rem;
    margin-top: 0.375rem;
    line-height: 1.45;
  }

  #deploy-mobile-results .mobile-card-summary {
    display: block;
  }

  #deploy-mobile-results .mobile-card-summary + .mobile-card-summary::before {
    content: none;
  }

  #deploy-mobile-results .mobile-card-metrics {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    margin-top: 0.75rem;
    padding-top: 0.625rem;
    border-top: 1px solid var(--hupa-border);
  }

  #deploy-mobile-results .mobile-card-metric {
    display: grid;
    gap: 0.125rem;
    min-width: 0;
    padding: 0 0.375rem;
    text-align: center;
  }

  #deploy-mobile-results .mobile-card-metric:first-child {
    padding-left: 0;
  }

  #deploy-mobile-results .mobile-card-metric + .mobile-card-metric {
    border-left: 1px solid var(--hupa-border);
  }

  #deploy-mobile-results .mobile-card-metric-value {
    color: var(--hupa-heading);
    font-size: 0.9375rem;
    font-weight: 600;
    line-height: 1.2;
    overflow-wrap: anywhere;
  }

  #deploy-mobile-results .mobile-card-metric-label {
    color: var(--hupa-text-muted);
    font-size: 0.6875rem;
    line-height: 1.2;
  }

  /* 原本的實心 ▼ 太重，改成細線 chevron；它只提示可展開，操作仍由整個
     左側按鈕承擔。右側 ⋮ 是唯一的操作入口。 */
  #deploy-mobile-results .mobile-card-chevron {
    top: 1.25rem;
    right: 3.75rem;
    width: 0.5rem;
    height: 0.5rem;
    overflow: hidden;
    border-right: 2px solid var(--hupa-text-muted);
    border-bottom: 2px solid var(--hupa-text-muted);
    color: transparent;
    font-size: 0;
    transform: rotate(45deg);
  }

  #deploy-mobile-results .mobile-card[data-expanded="true"] .mobile-card-chevron {
    transform: rotate(225deg);
  }

  #deploy-mobile-results .mobile-action-menu-trigger {
    margin: 0.75rem 0.375rem 0 0;
    border-radius: 50%;
    color: var(--hupa-text-muted);
    font-size: 1.375rem;
  }

  #deploy-mobile-results .mobile-action-menu-trigger:hover,
  #deploy-mobile-results .mobile-action-menu-trigger:focus,
  #deploy-mobile-results .mobile-action-menu-trigger[aria-expanded="true"] {
    background: var(--hupa-surface-hover);
    color: var(--hupa-heading);
  }

  #deploy-mobile-results .mobile-card-details {
    grid-template-columns: minmax(5rem, 0.45fr) minmax(0, 1fr);
    gap: 0.5rem 0.75rem;
    padding: 0.875rem 1rem 1rem;
    border-top: 1px solid var(--hupa-border);
    background: var(--hupa-page-bg);
  }

  #deploy-mobile-results .mobile-card-details:not([hidden]) {
    display: grid;
  }

  #deploy-mobile-results .mobile-card-details dt,
  #deploy-mobile-results .mobile-card-details dd {
    margin: 0;
  }

  /* 操作選單改成列內的文字操作區，不再浮在卡片上遮住內容。 */
  #deploy-mobile-results .mobile-action-menu {
    position: static;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.375rem;
    min-width: 0;
    padding: 0.5rem;
    background: var(--hupa-page-bg);
    border: 0;
    border-top: 1px solid var(--hupa-border);
    border-radius: 0;
    box-shadow: none;
  }

  #deploy-mobile-results .mobile-action-menu[hidden] {
    display: none;
  }

  #deploy-mobile-results .mobile-action-menu a,
  #deploy-mobile-results .mobile-action-menu button {
    display: flex;
    align-items: center;
    min-width: 0;
    padding: 0.625rem 0.75rem;
    background: var(--hupa-surface);
    border: 1px solid var(--hupa-border);
    border-radius: var(--hupa-radius-sm);
    color: var(--hupa-heading);
    line-height: 1.35;
    white-space: normal;
  }

  /* --- 卡片操作選單 ---------------------------------------------- */

  .mobile-action-menu-trigger {
    flex: 0 0 auto;
    min-width: 44px;
    min-height: 44px;
    padding: 0;
    border: 0;
    background: none;
    line-height: 1;
  }

  .mobile-action-menu {
    position: absolute;
    top: 3rem;
    right: 0.75rem;
    z-index: 5;
    min-width: 10rem;
    padding: 0.25rem 0;
    background: var(--hupa-surface);
    border: 1px solid var(--hupa-border);
    border-radius: 0.5rem;
    box-shadow: var(--hupa-shadow-raised);
  }

  .mobile-action-menu[hidden] {
    display: none;
  }

  .mobile-action-menu a,
  .mobile-action-menu button {
    display: block;
    width: 100%;
    min-height: 44px;
    padding: 0.625rem 1rem;
    border: 0;
    background: none;
    text-align: left;
    white-space: nowrap;
  }

  /* 危險操作不能只靠顏色。呼叫端另外給文字, 這裡只是加上顏色語意。 */
  .mobile-action-menu .mobile-action-danger {
    color: var(--hupa-danger);
  }

  /* --- 聲音分析結果 -------------------------------------------- */

  #job-overview,
  #file-viewer,
  .audio-file,
  .audio-file .card-body {
    min-width: 0;
    max-width: 100%;
  }

  .audio-file .card-body {
    overflow: hidden;
  }

  .audio-desktop-table,
  .audio-desktop-editing,
  .audio-desktop-meta,
  .audio-desktop-downloads {
    display: none;
  }

  /* bootstrap 的 .align-items-center 帶 !important, 所以這裡也要 ——
     不然返回鈕與批次切換只有內容寬度, 擠在卡片正中間。 */
  .audio-result-overview .card-body,
  .audio-result-primary-actions {
    align-items: stretch !important;
    flex-direction: column;
    width: 100%;
  }

  .audio-result-primary-actions {
    gap: 0.5rem;
    margin-right: 0 !important;
  }

  /* 返回一整列, 置中。桌機是靠左 (它在那邊只是方位指示), 但手機上整列
     就這一顆鈕, 靠左會讓 44px 高的觸控區看起來像沒對齊的文字連結。
     .btn 預設 inline-block, 不改成 flex 的話 min-height 撐出來的空間
     不會把文字垂直置中。 */
  .audio-result-primary-actions > .btn {
    display: flex;
    min-height: 44px;
    margin: 0 !important;
    align-items: center;
    justify-content: center;
    font-size: 1rem;
    text-align: center;
  }

  /* 批次切換一整列, 左右各半。 */
  .audio-result-primary-actions .btn-group {
    display: flex;
    width: 100%;
    margin: 0 !important;
  }

  /* ‹ › 在 btn-sm 的 0.875rem 下小到看不出是箭頭 (使用者實機回報),
     放大到 1.5rem 並置中; line-height 收成 1 才不會把鈕撐高。 */
  .audio-result-primary-actions .btn-group .btn {
    display: flex;
    min-height: 44px;
    flex: 1 1 0;
    align-items: center;
    justify-content: center;
    font-size: 1.5rem;
    line-height: 1;
  }

  .audio-mobile-metadata {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    width: 100%;
    margin: 0;
    gap: 0.75rem;
  }

  .audio-mobile-metadata dt,
  .audio-mobile-card-meta {
    color: var(--hupa-text-muted);
    font-size: 0.75rem;
  }

  .audio-mobile-metadata dd {
    margin: 0.125rem 0 0;
    overflow-wrap: anywhere;
  }

  /* 下載直接列出來, 左右各一。原本收在 <details> 的「更多下載」後面 ——
     分析做完最常做的事就是把結果帶走, 藏一層等於多一次點擊。 */
  .audio-mobile-downloads {
    display: flex;
    gap: 0.5rem;
    width: 100%;
    margin-top: 0.75rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--hupa-border);
  }

  .audio-mobile-downloads > .btn {
    display: flex;
    flex: 1 1 0;
    min-height: 44px;
    align-items: center;
    justify-content: center;
  }

  .audio-mobile-species-list,
  .audio-mobile-file-list,
  .audio-mobile-label-list {
    padding: 0.75rem;
  }

  .audio-mobile-species-card,
  .audio-mobile-file-card,
  .audio-mobile-label-card {
    display: flex;
    width: 100%;
    min-height: 44px;
    align-items: stretch;
    flex-direction: column;
    padding: 0.75rem;
    border: 1px solid var(--hupa-border);
    border-radius: 0.5rem;
    background: var(--hupa-surface);
    color: var(--hupa-heading);
    text-align: left;
    overflow-wrap: anywhere;
  }

  .audio-mobile-species-card {
    min-height: 44px;
  }

  .audio-mobile-file-card {
    min-height: 44px;
  }

  .audio-mobile-label-card {
    min-height: 44px;
  }

  .audio-mobile-species-card + .audio-mobile-species-card,
  .audio-mobile-file-card + .audio-mobile-file-card,
  .audio-mobile-label-card + .audio-mobile-label-card {
    margin-top: 0.625rem;
  }

  a.audio-mobile-file-card:hover,
  a.audio-mobile-file-card:focus {
    border-color: var(--hupa-primary);
    color: var(--hupa-primary);
    text-decoration: none;
  }

  .audio-mobile-species-card.is-disabled,
  .audio-mobile-file-card.is-failed {
    background: var(--hupa-surface-hover);
  }

  .audio-mobile-card-heading {
    display: flex;
    min-width: 0;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.5rem;
  }

  .audio-mobile-card-summary,
  .audio-mobile-card-meta {
    display: block;
    margin-top: 0.25rem;
  }

  .audio-mobile-card-scientific {
    font-style: italic;
  }

  /* 短頂列: 返回目的地佔滿剩餘寬度, 上一／下一收成兩顆箭頭。
     原本三顆按鈕各自換行, 加上檔案下拉與勾選共四排 —— 頻譜被推到摺線下面,
     而這一頁存在的理由就是看頻譜。 */
  .audio-mobile-nav {
    gap: 0.5rem;
  }

  .audio-mobile-nav .btn {
    min-height: 44px;
  }

  .audio-mobile-nav .audio-nav-back {
    flex: 1 1 auto;
    min-width: 0;
    margin-right: 0 !important;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* 兩顆箭頭各佔剩下寬度的一半, 圖示在正中央。原本是 inline-flex + 自然寬,
     箭頭字元 (‹ ›) 的字面又小又偏上, 在一顆被撐開的按鈕裡看起來像貼在左上角。
     改用 mdi 的 chevron: 字面本身就置中, 放大也不會糊。 */
  .audio-mobile-nav .audio-nav-step {
    flex: 1 1 0;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    padding: 0;
    line-height: 1;
  }

  /* 箭頭是給眼睛看的, 完整動作名留在 aria-label 上 (icon-only 控制的規格)。 */
  .audio-mobile-nav .audio-nav-label {
    display: none;
  }

  .audio-mobile-nav .audio-nav-glyph {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.75rem;
    line-height: 1;
  }

  .audio-mobile-nav .audio-nav-glyph .mdi {
    line-height: 1;
  }

  /* 檔案下拉與「只看某物種」不是頂列的事: 它們移到頻譜下面。 */
  .audio-file-picker {
    order: 5;
    display: flex;
    flex: 1 0 100%;
    flex-wrap: wrap;
    gap: 0.5rem;
  }

  .audio-file-picker #file-select {
    width: 100% !important;
    min-height: 44px;
    margin: 0 !important;
  }

  .audio-file-picker .custom-control {
    width: 100%;
    min-height: 44px;
    margin-left: 0 !important;
    padding-top: 0.625rem;
  }

  /* 卡片內容重新排序: 頻譜與播放器第一, 控制項在後。DOM 順序不動 ——
     桌機那邊維持「先控制項再頻譜」, 而且 audioviewer.js 抓的是 class,
     不是位置。 */
  .audio-file-body {
    display: flex;
    flex-direction: column;
  }

  .audio-file-stage {
    order: 1;
    min-width: 0;
    max-width: 100%;
  }

  .audio-file-tools {
    order: 2;
    margin-top: 0.5rem;
  }

  .audio-file-filters {
    order: 3;
  }

  .audio-file-body > .legend {
    order: 4;
  }

  .audio-mobile-readonly-notice {
    order: 5;
  }

  .audio-mobile-label-list {
    order: 6;
  }

  .audio-file .zoom-out,
  .audio-file .zoom-in {
    min-width: 44px;
    min-height: 44px;
  }

  /* 手機的工具列只剩縮放 (人工標註是桌機限定), 靠右像是頻譜的附屬控制。 */
  .audio-file-tools {
    justify-content: flex-end !important;
  }

  .audio-file-filters {
    align-items: stretch !important;
    flex-wrap: wrap;
    gap: 0.5rem;
  }

  .audio-file-filters .score-filter {
    flex: 1 1 calc(100% - 7rem);
  }

  .audio-file-filters .custom-control {
    flex: 1 0 100%;
    min-height: 44px;
    margin-left: 0 !important;
    padding-top: 0.625rem;
  }

  .spectrogram-scroll {
    width: 100%;
    max-width: 100%;
    min-width: 0;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
  }

  .audio-mobile-readonly-notice {
    margin: 1rem 0 0;
    padding: 0.75rem;
  }

  #progress-log-toggle {
    min-width: 44px;
    min-height: 44px;
  }

  /* --- /projectedit: 存檔列、視圖切換、卡片上的加入/移除 ---------- */

  /* 手機的復原提示停在 .mobile-bottom-nav 正上方 (那裡是導覽列), 通欄一條:
     375px 上靠右的膠囊會把「已加入 N 筆佈設」擠成兩行。 */
  .hupa-undo-toast {
    right: 0.75rem;
    bottom: calc(var(--hupa-mobile-nav-height) + env(safe-area-inset-bottom) + 0.75rem);
    left: 0.75rem;
    max-width: none;
    justify-content: space-between;
    border-radius: var(--hupa-radius);
  }

  .hupa-undo-toast-action {
    min-height: 44px;
  }

  /* 三顆視圖切換橫向可捲: 375px 上「本次新增 4」放不進剩下的寬度, 換行會
     把篩選列往下推。每顆到得了 44px 觸控下限。 */
  .hupa-view-switch {
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }

  .hupa-view-chip {
    flex: 0 0 auto;
    min-height: 44px;
    white-space: nowrap;
  }

  /* 卡片標題列裡的主要操作 (加入 / 移除)。44×44 是觸控下限; flex:0 0 auto
     才不會被標題那顆展開鈕壓掉寬度。已刪除或無權限的列放的是靜態 badge,
     不是按鈕 —— 那顆不需要 44px 的觸控高度, 只要跟按鈕對齊中線。 */
  .mobile-card-primary-action {
    flex: 0 0 auto;
    min-width: 44px;
    min-height: 44px;
    white-space: nowrap;
  }

  /* 展開箭頭原本是對整張卡片定位的 (right: 3rem —— 那個值是為「header 右邊
     只有一顆 ⋮」算出來的)。這頁的 header 右邊是加入/移除那顆按鈕, 照舊值
     會壓在按鈕上。改成對展開鈕自己定位, 箭頭就落在按鈕左邊, 不用去猜按鈕
     實際多寬; padding-right 是留給箭頭的位置, 免得長樣站名穿過去。 */
  #projectedit-deploys-mobile .mobile-card-toggle {
    position: relative;
    padding-right: 1.5rem;
  }

  #projectedit-deploys-mobile .mobile-card-chevron {
    top: 0.125rem;
    right: 0;
  }

  .mobile-card-primary-action.is-static {
    display: inline-flex;
    align-items: center;
    min-height: 0;
    white-space: normal;
  }

  /* 這頁的篩選抽屜在手機才打開。:not([hidden]) 見上面那條非手機規則的註解:
     媒體查詢不加特異性, 不帶它就會贏過 .mobile-sheet[hidden]。 */
  .mobile-sheet.projectedit-filter-sheet:not([hidden]) {
    display: flex;
  }

  /* --- /subscription: 唯讀狀態、模組與合約歷程 ------------------ */

  #subscription-page,
  #subscription-page .card,
  #subscription-page .card-body,
  #subscription-page .row,
  #subscription-page [class*="col-"],
  #subscription-edit-page,
  #subscription-edit-page .card,
  #subscription-edit-page .card-body {
    min-width: 0;
    max-width: 100%;
  }

  #subscription-page .desktop-only {
    display: none !important;
  }

  #subscription-edit-page .desktop-only {
    display: none !important;
  }

  #subscription-page .subscription-expiry-warning,
  #subscription-page .subscription-management-notice,
  #subscription-page .subscription-module-name,
  #subscription-page .subscription-history-card dd {
    overflow-wrap: anywhere;
  }

  #subscription-page .subscription-expiry-warning {
    display: flex;
    align-items: flex-start;
    gap: 0.25rem;
  }

  #subscription-page .subscription-management-notice {
    margin: 0.75rem 0 1rem;
  }

  #subscription-page .subscription-module-list {
    padding: 0;
    margin: 0;
    list-style: none;
  }

  #subscription-page .subscription-module-list li {
    display: flex;
    align-items: center;
    justify-content: space-between;
    min-height: 44px;
    gap: 0.75rem;
    padding: 0.625rem 0;
    border-top: 1px solid var(--hupa-border);
  }

  #subscription-page .subscription-module-list li:last-child {
    border-bottom: 1px solid var(--hupa-border);
  }

  #subscription-page .subscription-module-name {
    min-width: 0;
  }

  #subscription-page .subscription-module-list .badge {
    flex: 0 0 auto;
  }

  #subscription-page .subscription-management-links {
    margin-top: 1rem;
  }

  #subscription-page .subscription-member-link {
    display: flex;
    align-items: center;
    justify-content: space-between;
    min-height: 44px;
    padding: 0.625rem 0;
    border-bottom: 1px solid var(--hupa-border);
  }

  #subscription-page .subscription-history {
    margin-top: 1.5rem;
  }

  #subscription-page .subscription-history-title {
    margin-bottom: 0.75rem;
  }

  #subscription-page .subscription-history-card {
    padding: 1rem;
    margin-bottom: 0.75rem;
    border: 1px solid var(--hupa-border);
    border-radius: 0.375rem;
    background: var(--hupa-surface);
  }

  #subscription-page .subscription-history-card-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.75rem;
    margin-bottom: 0.75rem;
  }

  #subscription-page .subscription-history-card dl {
    display: grid;
    grid-template-columns: minmax(3rem, auto) minmax(0, 1fr);
    gap: 0.375rem 0.75rem;
    margin: 0;
  }

  #subscription-page .subscription-history-card dt,
  #subscription-page .subscription-history-card dd {
    margin: 0;
  }

  #subscription-page .subscription-history-card dt {
    color: var(--hupa-text-muted);
  }

  #subscription-page .hupa-trial .form-control,
  #subscription-page .hupa-trial .btn {
    min-height: 48px;
  }

  #subscription-page .hupa-trial .form-check {
    display: flex;
    align-items: center;
    min-height: 44px;
  }

  #subscription-page .hupa-trial .form-check-input {
    width: 1.25rem;
    height: 1.25rem;
  }

  #subscription-page .hupa-trial .form-check-label {
    padding: 0.625rem 0;
  }

  #subscription-edit-page .subscription-edit-mobile-notice {
    margin-top: 1rem;
  }

  #subscription-edit-page .subscription-edit-back {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 48px;
  }

  #subscription-page,
  #subscription-edit-page {
    padding-right: calc(env(safe-area-inset-right) + 0px);
    padding-left: calc(env(safe-area-inset-left) + 0px);
  }
}

/* 減少動態時直接切換狀態: 抽屜與選單不滑入, 立刻就位。 */
@media (prefers-reduced-motion: reduce) {

  .mobile-sheet,
  .mobile-fab,
  .mobile-fab-menu,
  .mobile-card,
  .mobile-action-menu {
    transition: none;
    animation: none;
  }
}

/* --------------------------------------------------------------------------
   全站頂端橫幅 (合約到期 / 儲存空間)
   --------------------------------------------------------------------------
   markup 在 templates/layout.html 的 page-wrapper 最上面, 兩條共用同一套。

   一條橫幅要在一眼之內回答三件事: 是什麼事 (圖示)、現況如何 (標題)、
   我現在該做什麼 (按鈕)。原本整條是一段置中的長句, 三件事全糊在一起 ——
   換行點由視窗寬度決定, 讀的人得整段掃完才知道跟自己有沒有關係。

   排版: 圖示徽章 / 文字兩行 / 動作按鈕, 靠左, 整組置中收在內容寬度裡。
   模板上的 .text-center 是 bootstrap 的 !important 工具類, 沒有樣式表時
   它讓橫幅仍是置中的一段字; 這裡不跟它比 !important, 而是由 __inner 這個
   子元素自己宣告 text-align —— 子元素上的規則本來就蓋得過繼承來的值。
   -------------------------------------------------------------------------- */

.hupa-banner {
  padding: 0.875rem 1.5rem;
  border-width: 0 0 1px;
  /* 頂端橫幅疊在內容之上, 但不該像卡片一樣浮起來 —— 只留下緣一條線。 */
  box-shadow: none;
}

.hupa-banner__inner {
  display: flex;
  align-items: center;
  gap: 0.875rem;
  /* 滿版的橫幅在寬螢幕上一行會拉到整個視窗寬, 眼睛回行時容易跳行。
     收在跟頁面內容差不多的寬度裡, 左緣才跟底下的卡片對得起來。 */
  max-width: 1180px;
  margin: 0 auto;
  text-align: left;
}

/* 圖示不是裝飾, 是這條橫幅在講哪一件事的入口 (磁碟 / 合約)。
   給它一塊圓底是為了跟旁邊的文字分層 —— 裸圖示放在同色底上會被當成標點。 */
.hupa-banner__icon {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 50%;
  font-size: 1.25rem;
}

/* flex-basis 給 0 而不是 auto: auto 會拿文字的長度當基準, 手機上換行時
   整塊文字就被擠到圖示的下一行, 圖示自己孤零零佔一行。 */
.hupa-banner__body {
  display: flex;
  flex: 1 1 0;
  flex-direction: column;
  gap: 0.125rem;
  min-width: 0;
}

.hupa-banner__headline {
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.45;
}

/* 細節那行降一階: 同色系但淡一點、小一號。降的是重量不是可讀性,
   所以用同色相的淺色而不是壓透明度 (透明度會讓它在黃底上糊掉)。 */
.hupa-banner__detail {
  font-size: 0.8125rem;
  line-height: 1.5;
}

/* 「去哪裡處理」要是一顆可以按的東西。寫在句子裡的「請到檔案回收桶」
   看的人還得自己去側欄找。

   min-width 是為了兩條橫幅同時出現時 (合約 + 磁碟) 上下兩顆鈕一樣寬 ——
   文案字數不同, 不給寬度的話兩顆的左緣會差一截, 疊起來看就是歪的。
   值取最長的那句「前往檔案回收桶」加左右內距, 之後文案再長就由它自己撐。 */
.hupa-banner__action {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  gap: 0.375rem;
  min-width: 11.5rem;
  padding: 0.4375rem 0.875rem;
  border: 1px solid transparent;
  border-radius: var(--hupa-radius-sm);
  background: var(--hupa-surface);
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.3;
  white-space: nowrap;
  text-decoration: none;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}

.hupa-banner__action:hover,
.hupa-banner__action:focus {
  text-decoration: none;
}

.hupa-banner__action .mdi {
  font-size: 1rem;
}

.hupa-banner__toggle {
  display: none;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  min-height: 2.75rem;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--hupa-radius-sm);
  background: transparent;
  color: inherit;
}

.hupa-banner__toggle .mdi {
  font-size: 1.25rem;
  line-height: 1;
}

/* 色 —— bootstrap 的 .alert-warning / .alert-danger 給的底色偏飽和, 而橫幅是
   常駐在每一頁最上面的東西, 太吵會被當成背景無視。這裡壓成淡底 + 深字,
   對比留在文字上。選擇器帶 .hupa-banner 才蓋得過 .alert-* 的原值。 */
.hupa-banner.alert-warning {
  color: var(--hupa-banner-warning-text);
  background-color: var(--hupa-banner-warning-bg);
  border-color: var(--hupa-banner-warning-border);
}

.hupa-banner.alert-warning .hupa-banner__icon {
  color: var(--hupa-banner-warning-icon);
  background: var(--hupa-banner-warning-icon-bg);
}

.hupa-banner.alert-warning .hupa-banner__detail {
  color: var(--hupa-banner-warning-detail);
}

.hupa-banner.alert-warning .hupa-banner__action {
  color: var(--hupa-banner-warning-text);
  border-color: var(--hupa-banner-warning-border);
}

.hupa-banner.alert-warning .hupa-banner__action:hover,
.hupa-banner.alert-warning .hupa-banner__action:focus {
  color: var(--hupa-banner-warning-text);
  background: var(--hupa-banner-warning-icon-bg);
  border-color: var(--hupa-banner-warning-icon);
}

.hupa-banner.alert-danger {
  color: var(--hupa-banner-danger-text);
  background-color: var(--hupa-banner-danger-bg);
  border-color: var(--hupa-banner-danger-border);
}

.hupa-banner.alert-danger .hupa-banner__icon {
  color: var(--hupa-banner-danger-icon);
  background: var(--hupa-banner-danger-icon-bg);
}

.hupa-banner.alert-danger .hupa-banner__detail {
  color: var(--hupa-banner-danger-detail);
}

.hupa-banner.alert-danger .hupa-banner__action {
  color: var(--hupa-banner-danger-text);
  border-color: var(--hupa-banner-danger-border);
}

.hupa-banner.alert-danger .hupa-banner__action:hover,
.hupa-banner.alert-danger .hupa-banner__action:focus {
  color: var(--hupa-banner-danger-text);
  background: var(--hupa-banner-danger-icon-bg);
  border-color: var(--hupa-banner-danger-icon);
}

/* 兩條同時成立時 (合約快到期 + 磁碟快滿) 上下疊著, 中間那條線由上面那條的
   border-bottom 畫, 不要再各自加 margin 把它們推開 —— 它們是同一疊通知。 */
.hupa-banner + .hupa-banner {
  border-top: 0;
}

@media (max-width: 900.98px) {

  .hupa-banner {
    padding: 0.75rem 1rem;
  }

  /* 手機上三欄並排會把中間的文字欄擠到剩兩三個字寬。改成圖示與標題一行,
     細節與按鈕各自換行 —— 按鈕撐滿一行才按得到 (48px 觸控高度)。 */
  .hupa-banner__inner {
    flex-wrap: wrap;
    gap: 0.625rem;
  }

  .hupa-banner__icon {
    width: 1.875rem;
    height: 1.875rem;
    font-size: 1.125rem;
  }

  .hupa-banner__action {
    flex: 1 1 100%;
    justify-content: center;
    min-height: 44px;
  }

  .hupa-banner__toggle {
    display: inline-flex;
    margin-left: auto;
  }

  .hupa-banner--collapsed {
    padding-top: 0.5rem;
    padding-bottom: 0.5rem;
  }

  .hupa-banner--collapsed .hupa-banner__inner {
    flex-wrap: nowrap;
  }

  .hupa-banner--collapsed .hupa-banner__body {
    gap: 0;
  }

  .hupa-banner--collapsed .hupa-banner__detail,
  .hupa-banner--collapsed .hupa-banner__action {
    display: none;
  }
}

/* ============================================================
   手機清單 / 詳情 / 單列操作 sheet (獨立手機呈現層, 第二批)

   桌機完全不吃這一段: 選擇器都是 .mobile-list-* / .mobile-detail-* /
   .mobile-row-sheet-*, 而且整段包在 900.98px 的斷點裡。
   ============================================================ */
@media (max-width: 900.98px) {

  /* --- 清單面板: 一列一筆, 用分隔線而不是一張張厚卡片 ---------- */

  .mobile-card-list:has(.mobile-list-row) {
    overflow: hidden;
    border: 1px solid var(--hupa-border);
    border-radius: 0.5rem;
    background: var(--hupa-surface);
  }

  .mobile-list-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0 0.25rem 0 0;
  }

  .mobile-list-row + .mobile-list-row {
    border-top: 1px solid var(--hupa-border);
  }

  .mobile-list-row-main {
    display: flex;
    flex: 1 1 auto;
    min-width: 0;
    flex-direction: column;
    gap: 0.125rem;
    min-height: 44px;
    padding: 0.75rem;
    border: 0;
    background: transparent;
    color: var(--hupa-heading);
    text-align: left;
  }

  .mobile-list-row-main:active {
    background: var(--hupa-surface-hover);
  }

  .mobile-list-eyebrow {
    color: var(--hupa-primary);
    font-size: var(--hupa-m-micro);
    font-weight: 700;
  }

  .mobile-list-headline {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    min-width: 0;
  }

  .mobile-list-title {
    overflow-wrap: anywhere;
    font-size: var(--hupa-m-title);
    font-weight: 700;
  }

  .mobile-list-status,
  .mobile-detail-status {
    display: inline-flex;
    flex: 0 0 auto;
    padding: 0.125rem 0.5rem;
    border-radius: 999px;
    background: var(--hupa-surface-hover);
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-micro);
    font-weight: 700;
  }

  .mobile-list-status[data-tone="active"],
  .mobile-detail-status[data-tone="active"] {
    background: var(--hupa-success-soft);
    color: var(--hupa-success-text);
  }

  .mobile-list-status[data-tone="removed"],
  .mobile-detail-status[data-tone="removed"] {
    background: var(--hupa-orange-soft);
    color: var(--hupa-orange-text);
  }

  .mobile-list-summary {
    overflow-wrap: anywhere;
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-meta);
    line-height: 1.45;
  }

  .mobile-list-metrics {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin-top: 0.25rem;
  }

  .mobile-list-metric {
    display: inline-flex;
    align-items: baseline;
    gap: 0.25rem;
  }

  .mobile-list-metric-value {
    color: var(--hupa-heading);
    font-size: var(--hupa-m-label);
    font-weight: 700;
  }

  .mobile-list-metric-label {
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-micro);
  }

  /* 每一列都要按的動作 (加入/移除)。排在 ⋮ 左邊, 高度與觸控目標對齊。 */
  .mobile-list-row-primary {
    flex: 0 0 auto;
    min-height: 36px;
    align-self: center;
    white-space: nowrap;
    font-size: var(--hupa-m-meta);
  }

  .mobile-list-row-primary.is-static {
    display: inline-flex;
    align-items: center;
  }

  /* POST 型的操作在 sheet 裡包了一層 form, 那一層不能把按鈕壓成 inline。 */
  .mobile-sheet-form {
    display: contents;
  }

  .mobile-list-row-more {
    display: grid;
    flex: 0 0 auto;
    width: 44px;
    height: 44px;
    place-items: center;
    border: 0;
    border-radius: 0.375rem;
    background: transparent;
    color: var(--hupa-text-muted);
    font-size: 1.25rem;
    line-height: 1;
  }

  /* --- 詳情層 -------------------------------------------------- */

  .mobile-detail-layer {
    position: fixed;
    inset: 0;
    z-index: 1045;
    display: flex;
    flex-direction: column;
    background: var(--hupa-page-bg);
  }

  .mobile-detail-layer[hidden],
  .mobile-row-sheet[hidden],
  .mobile-row-sheet-backdrop[hidden] {
    display: none;
  }

  .mobile-detail-head {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    gap: 0.25rem;
    padding: 0 0.5rem;
    padding-top: env(safe-area-inset-top);
    height: calc(48px + env(safe-area-inset-top));
    border-bottom: 1px solid var(--hupa-border);
    background: var(--hupa-surface);
  }

  /* 標頭上的筆名。單行截尾 —— 樣站/佈設的名稱可以很長, 換行會把 48px 的
     標頭撐成兩層。 */
  .mobile-detail-head-title {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-meta);
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .mobile-detail-head-title:empty {
    display: none;
  }

  .mobile-detail-back {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 0.75rem;
    border: 0;
    background: transparent;
    color: var(--hupa-heading);
    font-size: var(--hupa-m-body);
    font-weight: 700;
  }

  .mobile-detail-back::before {
    margin-right: 0.375rem;
    content: "‹";
    font-size: 1.25rem;
    line-height: 1;
  }

  /* overscroll-behavior: 捲到底之後再滑不要把底下那一頁一起帶著捲
     (覆蓋層是全螢幕的, 背景動起來看不出所以然, 只覺得畫面在漂)。 */
  .mobile-detail-body {
    flex: 1 1 auto;
    padding: 1rem 0.875rem calc(1.5rem + env(safe-area-inset-bottom));
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .mobile-detail-hero {
    padding: 0.9375rem;
    border: 1px solid var(--hupa-border);
    border-radius: 0.625rem;
    background: var(--hupa-surface);
  }

  .mobile-detail-title {
    margin: 0.4375rem 0 0.1875rem;
    overflow-wrap: anywhere;
    font-size: var(--hupa-m-heading);
    font-weight: 700;
  }

  .mobile-detail-subtitle {
    margin: 0;
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-meta);
  }

  .mobile-detail-stats {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.5rem;
    margin-top: 0.625rem;
  }

  .mobile-detail-stat {
    padding: 0.6875rem;
    border: 1px solid var(--hupa-border);
    border-radius: 0.5rem;
    background: var(--hupa-surface);
  }

  .mobile-detail-stat strong {
    display: block;
    color: var(--hupa-primary);
    font-size: var(--hupa-m-heading);
  }

  .mobile-detail-stat span {
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-micro);
  }

  .mobile-detail-section {
    margin-top: 0.6875rem;
    overflow: hidden;
    border: 1px solid var(--hupa-border);
    border-radius: 0.5rem;
    background: var(--hupa-surface);
  }

  .mobile-detail-section-title {
    margin: 0;
    padding: 0.5625rem 0.75rem;
    border-bottom: 1px solid var(--hupa-border);
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-micro);
    font-weight: 700;
  }

  .mobile-detail-item {
    display: flex;
    min-height: 46px;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.5rem 0.75rem;
    color: var(--hupa-heading);
    font-size: var(--hupa-m-body);
  }

  /* 詳情裡可以點的項目有兩種: 連結與按鈕。按鈕不會自己繼承寬度與對齊,
     不補這一條的話成員那一段會縮成一顆置中的小按鈕。 */
  button.mobile-detail-item {
    width: 100%;
    border: 0;
    background: transparent;
    font: inherit;
  }

  a.mobile-detail-item:hover,
  a.mobile-detail-item:focus,
  button.mobile-detail-item:hover,
  button.mobile-detail-item:focus {
    background: var(--hupa-surface-hover);
    color: var(--hupa-heading);
    text-decoration: none;
  }

  .mobile-detail-item + .mobile-detail-item {
    border-top: 1px solid var(--hupa-border);
  }

  .mobile-detail-item-value {
    overflow-wrap: anywhere;
    color: var(--hupa-text-muted);
    text-align: right;
  }

  .mobile-detail-item-chevron {
    color: var(--hupa-text-muted);
  }

  /* 詳情層是獨立畫面, 不是清單上的一層浮層 —— 底部導覽與 FAB 留著會讓
     「返回」跟導覽同時存在, 使用者會分不出哪一個才是回清單。 */
  .mobile-detail-open .mobile-bottom-nav,
  .mobile-detail-open .mobile-fab {
    display: none;
  }

  /* --- 單列操作 sheet ------------------------------------------ */

  .mobile-row-sheet-backdrop {
    position: fixed;
    inset: 0;
    /* 底部導覽是 1048, FAB 是 1040 —— sheet 要蓋過它們, 不然最後一個
       選項會被導覽列壓住 (跟 .mobile-shell-sheet 同層)。 */
    z-index: 1049;
    background: var(--hupa-overlay);
  }

  .mobile-row-sheet {
    position: fixed;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 1050;
    max-height: 75vh;
    overflow-y: auto;
    overscroll-behavior: contain;
    border-radius: 0.875rem 0.875rem 0 0;
    background: var(--hupa-surface);
    padding-bottom: env(safe-area-inset-bottom);
  }

  .mobile-row-sheet-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.875rem 0.75rem 0.875rem 1rem;
    border-bottom: 1px solid var(--hupa-border);
  }

  .mobile-row-sheet-body {
    display: flex;
    flex-direction: column;
    padding: 0.25rem 0 0.5rem;
  }

  .mobile-sheet-item {
    display: flex;
    min-height: 48px;
    align-items: center;
    gap: 0.5rem;
    padding: 0 1rem;
    border: 0;
    background: transparent;
    color: var(--hupa-heading);
    font-size: var(--hupa-m-body);
    text-align: left;
  }

  /* 一般 .mobile-sheet-body 是 block, 而按鈕預設是行內盒 —— 不補滿寬的話
     項目只有文字那麼寬, 點擊區也跟著縮水。 */
  .mobile-sheet-body > .mobile-sheet-item {
    width: 100%;
  }

  .mobile-sheet-item:hover,
  .mobile-sheet-item:focus {
    background: var(--hupa-surface-hover);
    color: var(--hupa-heading);
    text-decoration: none;
  }

  .mobile-sheet-item.is-danger {
    color: var(--hupa-danger);
  }
}

/* ============================================================
   手機字體與字級的單一來源

   桌機完全不吃這一段 (整段在 900.98px 斷點裡)。這裡做兩件事:
   1. 把字族補到表單控件上 —— button/input/select 不會自動繼承 body 的
      font-family, 所以原本手機上的下拉與按鈕是系統 UI 字, 跟旁邊的
      內文不同一套字。
   2. 讓每一個手機層的標題吃同一組 token: 篩選抽屜的標題原本是 Bootstrap
      的 h2 (2rem), 單列操作 sheet 是 .h6 —— 兩個都是「這一層在講什麼」,
      卻差了一倍。
   ============================================================ */
@media (max-width: 900.98px) {

  body,
  button,
  input,
  select,
  textarea {
    font-family: var(--hupa-font-stack);
  }

  /* 可輸入的控件一律 16px。iOS Safari 對字級小於 16px 的欄位會在聚焦時
     自動放大整頁, 而且不會縮回去 —— 使用者每點一個欄位版面就跳一次。
     --hupa-m-body (14px) 是內文用的, 不能拿來當控件字級; 登入頁早就寫死
     1rem 避開這件事, 其餘表單漏了。

     這是唯一一組不吃 --hupa-m-* 的字級, 因為它不是排版決定, 是平台限制。 */
  input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="button"]):not([type="submit"]):not([type="reset"]),
  select,
  textarea,
  .form-control,
  .mobile-form-control,
  .select2-container .select2-search__field {
    font-size: var(--hupa-m-control);
  }

  .page-header .page-title {
    font-size: var(--hupa-m-page);
  }

  .mobile-sheet-title,
  .mobile-shell-sheet-header h2,
  .mobile-row-sheet-head h2,
  .mobile-form-title {
    margin: 0;
    font-size: var(--hupa-m-heading);
    font-weight: 700;
    line-height: 1.35;
  }

  .mobile-filter-trigger,
  .mobile-collection-count,
  .mobile-pager-btn,
  .mobile-pager-page {
    font-size: var(--hupa-m-body);
  }

  .hupa-filters .filter-label,
  .mobile-sheet-body label {
    font-size: var(--hupa-m-label);
  }

  .mobile-sheet-body .custom-control-label {
    font-size: var(--hupa-m-body);
  }

  /* 控件走 --hupa-m-control (見該 token 的說明)。原本這兩個選擇器把篩選抽屜
     與 sheet 裡的欄位壓回 14px, 特異性又高過通用那條, 所以 iOS 只有這兩處
     還是會放大。 */
  .hupa-filters .form-control,
  .mobile-sheet-body .form-control {
    font-size: var(--hupa-m-control);
  }

  /* 抽屜裡的控件不吃 .form-control-sm 的高度 —— 那是為桌機一排 inline 欄位
     設計的 (約 31px), 在手機上不但點不準, 選單的文字還會被切掉半行。 */
  .mobile-sheet-body .form-control,
  .mobile-sheet-body .form-control-sm {
    height: auto;
    min-height: 48px;
  }

  /* --- 手機全頁表單 ------------------------------------------- */

  .mobile-form-layer {
    position: fixed;
    inset: 0;
    z-index: 1051;
    display: flex;
    flex-direction: column;
    background: var(--hupa-page-bg);
  }

  .mobile-form-layer[hidden] {
    display: none;
  }

  /* 全頁層鋪到瀏海底下 (viewport-fit=cover), 標頭自己吃掉上方安全區。 */
  .mobile-form-head {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    padding: 0 0.5rem;
    padding-top: env(safe-area-inset-top);
    height: calc(48px + env(safe-area-inset-top));
    border-bottom: 1px solid var(--hupa-border);
    background: var(--hupa-surface);
  }

  .mobile-form-back {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 0.75rem;
    border: 0;
    background: transparent;
    color: var(--hupa-heading);
    font-size: var(--hupa-m-body);
    font-weight: 700;
  }

  .mobile-form-back::before {
    margin-right: 0.375rem;
    content: "‹";
    font-size: 1.25rem;
    line-height: 1;
  }

  .mobile-form-title {
    padding: 0.875rem 0.875rem 0;
    color: var(--hupa-heading);
  }

  .mobile-form {
    display: flex;
    flex: 1 1 auto;
    min-height: 0;
    flex-direction: column;
  }

  .mobile-form-body {
    flex: 1 1 auto;
    padding: 0.875rem;
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .mobile-form-field,
  .mobile-form-group {
    margin-bottom: 0.875rem;
  }

  .mobile-form-label {
    display: block;
    margin-bottom: 0.3125rem;
    color: var(--hupa-heading);
    font-size: var(--hupa-m-label);
    font-weight: 700;
  }

  .mobile-form-label-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    margin-bottom: 0.3125rem;
  }

  .mobile-form-control {
    display: block;
    width: 100%;
    min-height: 48px;
    padding: 0 0.6875rem;
    border: 1px solid var(--hupa-border);
    border-radius: 0.4375rem;
    background: var(--hupa-surface);
    color: var(--hupa-heading);
    font-size: var(--hupa-m-control);
  }

  select.mobile-form-control[multiple] {
    height: auto;
    min-height: 108px;
    padding: 0.375rem 0.6875rem;
  }

  .mobile-form-control[aria-invalid="true"] {
    border-color: var(--hupa-danger);
  }

  .mobile-form-hint {
    margin: 0.3125rem 0 0;
    color: var(--hupa-text-muted);
    font-size: var(--hupa-m-meta);
  }

  .mobile-form-error {
    margin: 0.3125rem 0 0;
    color: var(--hupa-danger);
    font-size: var(--hupa-m-meta);
  }

  .mobile-form-error[hidden] {
    display: none;
  }

  /* 開始與結束各佔一整行。兩格並排時每格只剩約 140px, 原生的
     datetime-local 在那個寬度會把「年/月/日 --:--」擠成一條, 手指點不準
     哪一段; 撐滿一行之後每一段都在 48px 的可點高度裡, 點下去開的是手機
     自己的日期時間選擇器。 */
  .mobile-form-repeat-item {
    position: relative;
    display: grid;
    grid-template-columns: 1fr;
    gap: 0.5rem;
    padding: 0.625rem 3.25rem 0.625rem 0.625rem;
    border: 1px solid var(--hupa-border);
    border-radius: 0.5rem;
    background: var(--hupa-surface);
  }

  .mobile-form-repeat-item + .mobile-form-repeat-item {
    margin-top: 0.5rem;
  }

  .mobile-form-repeat-item .mobile-form-field {
    margin-bottom: 0;
    min-width: 0;
  }

  .mobile-form-repeat-add {
    min-height: 36px;
    padding: 0 0.6875rem;
    border: 1px solid var(--hupa-border);
    border-radius: 0.4375rem;
    background: var(--hupa-surface);
    color: var(--hupa-heading);
    font-size: var(--hupa-m-meta);
    font-weight: 700;
  }

  .mobile-form-repeat-remove {
    position: absolute;
    top: 0.625rem;
    right: 0.625rem;
    display: grid;
    width: 44px;
    height: 44px;
    place-items: center;
    border: 1px solid var(--hupa-border);
    border-radius: 0.4375rem;
    background: var(--hupa-surface);
    color: var(--hupa-danger);
    font-size: 1.125rem;
    line-height: 1;
  }

  /* 危險操作排在內容最後、送出列之上 —— 不跟「儲存」並排, 免得誤觸。 */
  .mobile-form-danger {
    padding: 0 0.875rem;
  }

  .mobile-form-danger-action {
    display: block;
    width: 100%;
    min-height: 48px;
    margin-bottom: 0.875rem;
    border: 1px solid var(--hupa-danger);
    border-radius: 0.4375rem;
    background: var(--hupa-surface);
    color: var(--hupa-danger);
    font-size: var(--hupa-m-body);
    font-weight: 700;
  }

  .mobile-form-actions {
    display: grid;
    flex: 0 0 auto;
    grid-template-columns: 1fr 1.6fr;
    gap: 0.5625rem;
    padding: 0.625rem 0.875rem calc(0.625rem + env(safe-area-inset-bottom));
    border-top: 1px solid var(--hupa-border);
    background: var(--hupa-surface);
  }

  .mobile-form-cancel,
  .mobile-form-submit {
    min-height: 48px;
    border-radius: 0.4375rem;
    font-size: var(--hupa-m-body);
    font-weight: 700;
  }

  .mobile-form-cancel {
    border: 1px solid var(--hupa-border);
    background: var(--hupa-surface);
    color: var(--hupa-heading);
  }

  .mobile-form-submit {
    border: 0;
    background: var(--hupa-primary);
    color: var(--hupa-on-dark);
  }

  /* --- 卡片內距: 標頭、內容與手機清單對齊同一條線 ---------------
     原本 .card-header 是 20px、.card-body 依頁面有 0 / 20 / 30px, 手機清單
     自己 0 —— 同一張卡上「錄音批次」在 36px、「3 筆」在 16px, 看起來像
     沒對齊 (使用者實機回報「3 筆太靠右邊」)。375px 上 30px 的內距也吃掉
     太多可用寬度。 */
  .card > .card-header,
  .card > .card-body {
    padding: 0.75rem;
  }

  .card > .mobile-collection {
    padding: 0 0.75rem 0.75rem;
  }

  /* card-body 已經給了留白, 裡面的清單不再疊一層。 */
  .card-body > .mobile-collection {
    padding: 0;
  }

  /* --- 時間選擇: 手機一律原生, 外觀跟其他輸入框一致 --------------
     值的格式由 mobile-datetime.js 負責 (見該檔的說明)。 */
  input[type="date"],
  input[type="datetime-local"],
  input[type="time"] {
    min-height: 44px;
    width: 100%;
    font-size: var(--hupa-m-control);
  }

  /* --- 觸控目標下限 (第六批全站掃描) ---------------------------
     44x44 是規格值。這一段收的是「整類控制項」而不是某一頁的某一顆 ——
     逐頁補的話, 下一頁加一顆 .btn-sm 又會漏掉。

     .custom-control-input 不在這裡: 它是 opacity:0 疊在 .custom-control-label
     上的代理節點, 真正被按到的是 label, 對它加尺寸只會把版面推歪。 */
  .btn,
  .btn-sm,
  .btn-group-sm > .btn,
  .form-control {
    min-height: 44px;
  }

  /* 圖示型的小按鈕 (批次前後切換的 ‹ ›、chip 上的 ×) 只有高度到位, 寬度
     還是一個字元。 */
  /* 三顆以上的動作在 375px 上排不下 (影像瀏覽的單張模式就是三顆),
     而 .page-header-actions 是 nowrap + flex-shrink:0 —— 整頁會被撐出
     水平捲軸。手機讓它換行。 */
  .page-header-actions {
    flex-wrap: wrap;
    /* 桌機那條規則是 flex-shrink: 0 —— 不放開的話容器停在 max-content 寬度,
       子元素永遠不必換行, wrap 等於沒寫。 */
    flex-shrink: 1;
    min-width: 0;
  }

  .page-header-actions .btn,
  .btn-group-sm > .btn,
  .mobile-list-row-primary,
  .badge .close,
  .list-inline-item .close {
    min-width: 44px;
  }

  /* --- /taxa 的 TBN 匯入結果表 ---------------------------------- */

  /* 不給 min-width: 在 375px 上只要多出十幾 px, 被切掉的就是最右邊那一欄,
     而那一欄正是「加入」—— 唯一要按的東西。讓學名換行就都放得下了;
     真的更窄的機型還有外層的 overflow-x 接著。

     這一段要留在這個大手機區塊裡, 不能自己另開一個 @media: 有兩支測試是用
     「第一個 max-width: 900.98px 區塊」當桌機/手機的分界線, 在它前面多開一個
     會把分界往前推, 後面那些桌機規則就被當成手機規則了。 */
  .taxa-tt-results th,
  .taxa-tt-results td {
    padding: 0.5rem 0.375rem;
    font-size: var(--hupa-m-meta);
    vertical-align: middle;
  }

  /* 中文名與學名讓出空間給階層與動作欄。 */
  .taxa-tt-results td:first-child,
  .taxa-tt-results .taxa-tt-sci {
    overflow-wrap: anywhere;
  }

  .taxa-tt-results .taxa-tt-add {
    min-height: 36px;
    padding: 0 0.5rem;
  }

  /* 分頁鈕。/explorer 與 /filelist 的分頁是伺服器端做的, 這一排在手機上是
     唯一的換頁入口 (DataTables 自己那排在 .desktop-table-only 裡, 手機看不到),
     但它原本是 0.375rem 內距 + 14px = 約 33px 高, 按不準。

     頁碼窗口最多七顆, 375px 上排不下要能換行, 否則整頁被撐出水平捲軸。 */
  .dataTables_wrapper .pagination,
  .hupa-table-pager .pagination {
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 0.25rem;
  }

  .dataTables_wrapper .pagination .page-link,
  .hupa-table-pager .pagination .page-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
  }

  /* 地圖的縮放鈕是 leaflet 自己畫的 (30px)。這是唯一一組第三方控制項,
     其餘都是站內樣式。 */
  .leaflet-control-zoom-in,
  .leaflet-control-zoom-out {
    width: 44px !important;
    height: 44px !important;
    line-height: 44px !important;
  }

  .form-control-sm,
  select.form-control-sm,
  .custom-select-sm {
    min-height: 44px;
  }

  /* 勾選框本身是原生控制項 (padding 在 Chrome 會被忽略), 所以放大到 24px,
     再讓它所在的那一列撐到 44px —— 手指按的是整列, 不是那 24px。 */
  input[type="checkbox"]:not(.custom-control-input),
  input[type="radio"]:not(.custom-control-input) {
    width: 24px;
    height: 24px;
  }

  /* 回收桶在窄螢幕是同一張表用 CSS 轉成卡片 (見 .hupa-trash-table-mobile),
     所以撐高度的是那些 <td>, 不是另一套卡片節點。 */
  .hupa-trash-table-mobile td {
    min-height: 44px;
  }

  .hupa-trash-expand {
    min-width: 44px;
  }

  /* 表單是一整頁, 底部導覽與 FAB 要讓位 (跟詳情層同一個理由)。 */
  .mobile-form-open .mobile-bottom-nav,
  .mobile-form-open .mobile-fab,
  .mobile-audio-upload-open .mobile-bottom-nav,
  .mobile-audio-upload-open .mobile-fab {
    display: none;
  }

  /* 新增錄音批次的手機層。欄位跟桌機是同一份 macro, 但外殼是全頁層而不是
     modal —— 檔案選擇 + 進度 + 部分失敗處理在 375px 的對話框裡放不下。
     欄位本身沿用站內的 .hupa-form-grid / .form-control, 不另立一套。 */
  .mobile-audio-upload {
    position: fixed;
    inset: 0;
    z-index: 1051;
    display: flex;
    flex-direction: column;
    background: var(--hupa-page-bg);
  }

  .mobile-audio-upload[hidden] {
    display: none;
  }

  .mobile-audio-upload-head {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    padding: 0 0.5rem;
    padding-top: env(safe-area-inset-top);
    height: calc(48px + env(safe-area-inset-top));
    border-bottom: 1px solid var(--hupa-border);
    background: var(--hupa-surface);
  }

  .mobile-audio-upload-form {
    display: flex;
    flex: 1 1 auto;
    min-height: 0;
    flex-direction: column;
  }

  .mobile-audio-upload-body {
    flex: 1 1 auto;
    padding: 0.875rem;
    overflow-y: auto;
  }

  .mobile-audio-upload-body .form-control,
  .mobile-audio-upload-body .form-control-file {
    min-height: 48px;
  }

  .mobile-audio-upload-body .hupa-field-label {
    font-size: var(--hupa-m-label);
  }

  /* 上傳中「取消上傳」會取代「取消」, 兩顆不會同時出現, 所以這一列用
     auto-flow 而不是固定欄數 —— 固定兩欄的話換成三顆時會擠成一團。 */
  .mobile-audio-upload-actions {
    display: flex;
    flex: 0 0 auto;
    gap: 0.5625rem;
    padding: 0.625rem 0.875rem calc(0.625rem + env(safe-area-inset-bottom));
    border-top: 1px solid var(--hupa-border);
    background: var(--hupa-surface);
  }

  .mobile-audio-upload-actions > * {
    flex: 1 1 0;
    min-height: 48px;
  }

  .mobile-audio-upload-actions [hidden] {
    display: none;
  }
}
