/* ==========================================================================
   FeedScanSystem 全站样式
   --------------------------------------------------------------------------
   依赖 tokens.css（须先于本文件加载）。
   类名与改造前保持完全一致，模板无需结构性改动即可获得统一视觉。

   分区：基础 → 导航 → 面包屑 → 页面骨架 → 按钮 → 表单 → 卡片 → 表格
        → 筛选条 → 徽标 → 消息 → 分页 → 首页 → 权限标签 → 行内操作 → 登录
        → 扫码核验 → 扫码弹窗 → 结果页 → 帮助文档 → 日志审计 → 工具类
        → 系统管理 / 数据维护 → 桶池 / 级联选择器
        → **材质层（毛玻璃 / 网格 / 颗粒 / 柔阴影）**
        → **动效层（入场编排 / 滚动渐入 / 微交互）**
        → 响应式

   材质与动效令牌全部来自 tokens.css，本节只消费。新增浮层容器时请把类名加进
   §26 的选择器组，不要各自写一份玻璃样式。
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. 基础
   -------------------------------------------------------------------------- */
*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100vh;
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  letter-spacing: var(--tracking-body);
  background: var(--bg-page);
  color: var(--gray-900);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

/* 氛围层：渐变网格 + 颗粒纹理。
   用 body 的两个伪元素实现 —— 好处是**不需要改任何模板**，全部页面自动获得；
   若要挂在模板里，每加一个页面都得记得贴一次，那才是真正的维护负担。

   层级安排（这是这套材质能看的前提）：
     body            z 0  —— 页面底色
     body::before    z 0  —— 渐变网格（柔和的冷色光斑）
     body::after     z 1  —— 颗粒纹理（去塑料感）
     .container      z 2  —— 内容
   颗粒压在内容**之下**是刻意的：它只负责让"纸"有质感，不参与文字渲染。
   反过来把颗粒盖在正文上，等于给每个字加了一层噪点，小字号下直接变糊。 */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: var(--z-atmosphere);
  pointer-events: none;
  background: var(--mesh-layer);
}

body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: var(--z-grain);
  pointer-events: none;
  opacity: var(--grain-opacity);
  mix-blend-mode: overlay;
  background-image: var(--grain-url);
}

a {
  color: var(--brand-600);
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

h1,
h2,
h3 {
  color: var(--gray-900);
  font-weight: var(--weight-medium);
  line-height: var(--leading-tight);
  /* 中文标题收紧字距：大字号下默认字距会显得松散、不精密 */
  letter-spacing: var(--tracking-tight);
}

h1 {
  font-size: var(--text-2xl);
  margin: 0 0 var(--space-5);
  letter-spacing: var(--tracking-tighter);
}

h2 {
  font-size: var(--text-lg);
  margin: 0 0 var(--space-4);
}

h3 {
  font-size: var(--text-base);
  margin: 0 0 var(--space-3);
}

/* 统一焦点样式：键盘操作必须可见 */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--brand-600);
  outline-offset: 2px;
}

/* --------------------------------------------------------------------------
   2. 顶部导航
   -------------------------------------------------------------------------- */
.navbar {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  display: flex;
  align-items: center;
  gap: var(--space-6);
  min-height: var(--nav-height);
  padding: 0 var(--space-6);
  /* 深色毛玻璃。白字压在 rgba(13,38,92,.82) 上，与页面浅色网格合成后
     实测对比度 8.8:1（导航项 opacity .88 时 7.4:1），远超 AA 要求的 4.5:1。 */
  background: var(--glass-nav-bg);
  color: #fff;
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  /* 上缘内高光 + 下缘发丝线：片材的"厚度"就靠这两笔，缺一则像贴纸 */
  box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255, 255, 255, 0.12);
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

/* 降级：不支持 backdrop-filter 的浏览器（老 Edge / 老 Firefox）退回不透明底。
   宁可丢掉玻璃感，也不能让白字压在半透明底上读不清 —— 可读性优先于质感。 */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .navbar {
    background: var(--brand-900);
  }
}

.nav-brand {
  flex: 0 0 auto;
  font-size: 17px;
  font-weight: var(--weight-medium);
  letter-spacing: 0.01em;
  white-space: nowrap;
}

.nav-links {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex: 1;
  flex-wrap: wrap;
}

.nav-links a {
  position: relative;
  display: inline-flex;
  align-items: center;
  height: 34px;
  padding: 0 var(--space-3);
  border-radius: var(--radius-md);
  color: #fff;
  font-size: var(--text-sm);
  text-decoration: none;
  opacity: 0.88;
  transition: background-color var(--duration-fast) var(--ease-out),
    opacity var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-spring);
}

/* 按压回弹：触摸屏上手指会盖住元素，背景色的变化基本看不见，
   反而"按下去缩一点"是唯一能穿过手指被感知到的反馈。 */
.nav-links a:active {
  transform: scale(0.96);
}

.nav-links a:hover {
  opacity: 1;
  background: rgba(255, 255, 255, 0.12);
  text-decoration: none;
}

/* 当前所在页面：整改前导航没有当前位置提示，只能靠页面标题辨认。
   除了底色，再加一条居中的短下划线 —— 底色在深色玻璃上不够显眼，
   一条白线才是"你在这一项"最省认知成本的信号。 */
.nav-links a.is-active {
  opacity: 1;
  font-weight: var(--weight-medium);
  background: rgba(255, 255, 255, 0.2);
}

.nav-links a.is-active::after {
  content: "";
  position: absolute;
  left: var(--space-3);
  right: var(--space-3);
  bottom: 3px;
  height: 2px;
  border-radius: var(--radius-full);
  background: rgba(255, 255, 255, 0.92);
}

/* 菜单分组：组内紧凑，组间以细分隔线区隔，替代原先 10 项一长排 */
.nav-group {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

.nav-group:not(:first-child) {
  margin-left: var(--space-2);
  padding-left: var(--space-3);
  border-left: 1px solid rgba(255, 255, 255, 0.24);
}

.nav-user {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 0 0 auto;
  font-size: var(--text-sm);
  white-space: nowrap;
}

.nav-user a,
.nav-link-btn {
  color: #fff;
  opacity: 0.88;
  text-decoration: none;
}

.nav-user a:hover,
.nav-link-btn:hover {
  opacity: 1;
  text-decoration: underline;
}

.nav-logout-form {
  display: inline;
  margin: 0;
  padding: 0;
}

.nav-link-btn {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  cursor: pointer;
}

/* 移动端汉堡按钮：桌面端隐藏，仅作为小屏兜底 */
.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: var(--touch-min);
  height: var(--touch-min);
  margin-left: calc(var(--space-2) * -1);
  background: none;
  border: none;
  color: #fff;
  cursor: pointer;
}

/* --------------------------------------------------------------------------
   3. 面包屑
   -------------------------------------------------------------------------- */
.breadcrumbs {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
  font-size: var(--text-sm);
  color: var(--gray-500);
}

.breadcrumbs a {
  color: var(--gray-500);
}

.breadcrumbs a:hover {
  color: var(--brand-600);
  text-decoration: none;
}

.breadcrumbs .sep {
  color: var(--gray-300);
}

.breadcrumbs .current {
  color: var(--gray-700);
  font-weight: var(--weight-medium);
}

/* --------------------------------------------------------------------------
   4. 页面骨架
   -------------------------------------------------------------------------- */
.container {
  position: relative;
  /* 抬到氛围层（网格 z0 / 颗粒 z1）之上。缺了这行，颗粒会盖在正文上，
     小字号直接变糊 —— 这是"加了纹理反而更难读"的唯一原因。 */
  z-index: var(--z-content);
  max-width: var(--container-max);
  margin: 0 auto;
  padding: var(--space-6);
}

.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-5);
}

.page-header h1 {
  margin: 0;
}

/* --------------------------------------------------------------------------
   5. 按钮
   -------------------------------------------------------------------------- */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 38px;
  padding: var(--space-2) var(--space-4);
  border: 1px solid transparent;
  border-radius: var(--btn-glass-radius);
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: 1;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  /* overflow: hidden 是为了裁住下面那道扫光；它不影响 :focus-visible 的 outline，
     outline 画在边框盒之外，不受 overflow 裁剪。 */
  overflow: hidden;
  transition: background-color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    box-shadow var(--duration-base) var(--ease-out),
    transform var(--duration-fast) var(--ease-spring);
}

.btn:hover {
  text-decoration: none;
}

/* 按压回弹。用弹性曲线而不是线性缩放：手指按下去那一下，"弹回来"的手感
   比"缩下去"的幅度更能传达"这一下被收到了"。 */
.btn:active {
  transform: scale(0.97);
}

/* 悬停扫光：一道 22% 白的高光斜着掠过按钮。
   只给实心底色的按钮（文字型按钮 .btn-link 掠过会很脏）。
   透明度压在 .22 —— 更高会在一瞬间把按钮底色提亮到影响与白字的对比度。
   底层仍是"不依赖 hover 才可读"的设计，扫光只是锦上添花。 */
.btn:not(.btn-link):not(.btn-link-danger)::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    100deg,
    transparent 30%,
    rgba(255, 255, 255, 0.22) 50%,
    transparent 70%
  );
  transform: translateX(-130%);
  transition: transform var(--duration-slow) var(--ease-out);
}

.btn:not(.btn-link):not(.btn-link-danger):hover::after {
  transform: translateX(130%);
}

.btn:disabled,
.btn[disabled] {
  opacity: 0.55;
  cursor: not-allowed;
}

/* 毛玻璃按钮（v1.3.15）：五个实心变体共享同一组玻璃材质 ——
   背景半透明化（color-mix 88%，不支持的旧浏览器落回下行纯色兜底）、
   背后 blur(21px) saturate(170%)、0.5px 半透明白边、统一投影。
   文字型按钮（.btn-link 系列）不参与：没有底色，玻璃无从附着。 */
.btn-primary,
.btn-secondary,
.btn-success,
.btn-danger,
.btn-warning {
  backdrop-filter: blur(var(--btn-glass-blur)) saturate(var(--btn-glass-saturate));
  -webkit-backdrop-filter: blur(var(--btn-glass-blur)) saturate(var(--btn-glass-saturate));
  border: 0.5px solid var(--btn-glass-border);
  box-shadow: var(--btn-glass-shadow);
}

.btn-primary {
  background: var(--brand-600);
  background: color-mix(in srgb, var(--brand-600) 88%, transparent);
  color: #fff;
}

.btn-primary:hover {
  background: var(--brand-700);
  /* 主色按钮是全站唯一的彩色光晕 —— 强调色纪律：关键动作只此一处 */
  box-shadow: var(--shadow-glow);
}

.btn-secondary {
  background: var(--gray-500);
  background: color-mix(in srgb, var(--gray-500) 88%, transparent);
  color: #fff;
}

.btn-secondary:hover {
  background: var(--gray-600);
  box-shadow: var(--shadow-md);
}

/* 原 #059669 白字仅 3.77:1，改用 700 档达 5.48:1 */
.btn-success {
  background: var(--success-700);
  background: color-mix(in srgb, var(--success-700) 88%, transparent);
  color: #fff;
}

.btn-success:hover {
  background: var(--success-900);
  box-shadow: var(--shadow-md);
}

.btn-danger {
  background: var(--danger-600);
  background: color-mix(in srgb, var(--danger-600) 88%, transparent);
  color: #fff;
}

.btn-danger:hover {
  background: var(--danger-700);
  box-shadow: 0 8px 24px -10px rgba(220, 38, 38, 0.45);
}

/* 原 #d97706 白字仅 3.19:1，改用 700 档达 5.02:1 */
.btn-warning {
  background: var(--warning-700);
  background: color-mix(in srgb, var(--warning-700) 88%, transparent);
  color: #fff;
}

.btn-warning:hover {
  background: var(--warning-900);
  box-shadow: 0 8px 24px -10px rgba(180, 83, 9, 0.45);
}

/* 实心按钮悬停时整体微抬 1px。位移极小是刻意的：
   后台按钮常常紧邻排列，抬多了会让工具栏"抖"，反而显得廉价。 */
.btn-primary:hover,
.btn-secondary:hover,
.btn-success:hover,
.btn-danger:hover,
.btn-warning:hover {
  transform: translateY(-1px);
}

/* 抬起来之后按下去要落回原位，否则"按不动"的感觉会抵消掉悬停的轻快 */
.btn-primary:active,
.btn-secondary:active,
.btn-success:active,
.btn-danger:active,
.btn-warning:active {
  transform: translateY(0) scale(0.97);
}

.btn-link {
  background: none;
  color: var(--brand-600);
  padding: var(--space-2);
}

.btn-link:hover {
  background: var(--brand-50);
}

.btn-link-danger {
  background: none;
  border: none;
  padding: 0;
  color: var(--danger-600);
  font: inherit;
  cursor: pointer;
}

.btn-link-danger:hover {
  text-decoration: underline;
}

.btn-sm {
  min-height: 30px;
  padding: var(--space-1) var(--space-3);
  font-size: var(--text-xs);
}

.btn-block {
  width: 100%;
}

.btn-group {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.inline-form {
  display: inline;
}

/* 部门行内的「改名」表单（系统管理 · 部门管理）：输入框 + 按钮排成一行，
   宽度收敛在操作列里 —— 不加这条的话 .form-control 的 width:100% 会把
   操作列撑到整行宽。 */
.dept-rename {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

.dept-rename .form-control {
  width: 140px;
  min-height: 32px;
  padding: var(--space-1) var(--space-2);
}

/* --------------------------------------------------------------------------
   6. 表单
   -------------------------------------------------------------------------- */
.form-control {
  width: 100%;
  min-height: 38px;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  /* 用 88% 白而不是纯白：桌面玻璃透出的一点点底色，让输入框与面板同属一层材质。
     再低就会开始吃掉正文对比度（正文 #111827 需 ≥4.5:1）。 */
  background: var(--glass-bg-strong);
  color: var(--gray-900);
  font-family: inherit;
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  transition: border-color var(--duration-fast) var(--ease-out),
    box-shadow var(--duration-base) var(--ease-out),
    background-color var(--duration-fast) var(--ease-out);
}

/* 未聚焦时的悬停预反馈：鼠标扫过就知道"这块能填"，
   不用等到点下去才确认。浅到几乎看不见，但确实存在。 */
.form-control:hover:not(:disabled):not(:focus) {
  border-color: var(--brand-300);
  background: #fff;
}

/* 聚焦：外圈 4px 软环 + 一圈同色微光。
   只加粗边框是不够的 —— 表单密排时，一条 1px 变 2px 的线在余光里根本认不出来。 */
.form-control:focus {
  outline: none;
  border-color: var(--brand-600);
  background: #fff;
  box-shadow: 0 0 0 4px var(--brand-100),
    0 4px 14px -6px rgba(26, 86, 219, 0.35);
}

.form-control:disabled {
  background: var(--gray-100);
  color: var(--gray-500);
  cursor: not-allowed;
}

.form-group {
  margin-bottom: var(--space-4);
}

.form-group label {
  display: block;
  margin-bottom: var(--space-2);
  font-weight: var(--weight-medium);
  color: var(--gray-700);
}

/* 单个复选框行：需提高特异性以覆盖 .form-group label 的 display:block */
.form-group label.check-line {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: var(--weight-regular);
  cursor: pointer;
}

.form-extra-link {
  margin-top: var(--space-2);
}

.field-error {
  margin-top: var(--space-1);
  color: var(--danger-600);
  font-size: var(--text-xs);
}

.field-help {
  margin-top: var(--space-1);
  color: var(--gray-500);
  font-size: var(--text-xs);
}

/* --------------------------------------------------------------------------
   7. 卡片
   -------------------------------------------------------------------------- */
.form-card,
.detail-card,
.info-box {
  background: var(--bg-surface);
  padding: var(--space-6);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  margin-bottom: var(--space-5);
}

/* 键值对行：标签列固定宽度，保证跨页面纵向对齐 */
.detail-row {
  display: flex;
  gap: var(--space-4);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--gray-100);
}

.detail-row:last-child {
  border-bottom: none;
}

.detail-row .label {
  flex: 0 0 160px;
  color: var(--gray-500);
  font-weight: var(--weight-medium);
}

.detail-row .value {
  flex: 1;
  min-width: 0;
}

/* 编号类字段统一等宽，便于逐字符核对。
   "zero" 打开斜杠零（有该特性的字体才会生效，Consolas / SF Mono 支持）——
   桶编号是在现场拿在手里逐位核对的，把 0 和 O 区分开是功能要求，不是美观偏好。 */
.code,
.cell-code,
.detail-row .code {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "zero" 1;
  letter-spacing: 0.01em;
}

/* --------------------------------------------------------------------------
   8. 表格
   -------------------------------------------------------------------------- */
.table-wrap {
  width: 100%;
  overflow-x: auto;
}

.table {
  width: 100%;
  border-collapse: collapse;
  background: var(--bg-surface);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  /* 全表等宽数字：时间戳、编号、次数纵向扫读时逐位对齐（中文字符不受影响） */
  font-variant-numeric: tabular-nums;
}

.table th,
.table td {
  padding: var(--space-3);
  text-align: left;
  vertical-align: middle;
  border-bottom: 1px solid var(--gray-200);
}

.table th {
  background: var(--gray-50);
  color: var(--gray-700);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  white-space: nowrap;
}

/* 时间戳 / 模式等不可断行列：单行显示。
   断成两行的时间戳（日期一行、时分秒一行）是列表页最伤扫读体验的问题；
   nowrap 后该列只占自身所需宽度，剩余宽度自然让给「提示」这类长文本列。 */
.table td.nowrap {
  white-space: nowrap;
}

/* 行悬停：10 列数据横向扫读时提供定位参照 */
.table tbody tr {
  transition: background-color var(--duration-fast) var(--ease-out);
}

.table tbody tr:hover {
  background: var(--gray-50);
}

.table tbody tr:last-child th,
.table tbody tr:last-child td {
  border-bottom: none;
}

/* 空态：纯文字提示升级为「说明 + 下一步」 */
.empty-state {
  padding: var(--space-10) var(--space-4);
  text-align: center;
  color: var(--gray-500);
}

.empty-state-title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-base);
  color: var(--gray-700);
}

.empty-state-desc {
  margin: 0 0 var(--space-4);
  font-size: var(--text-sm);
}

/* --------------------------------------------------------------------------
   9. 筛选条
   -------------------------------------------------------------------------- */
.filter-bar {
  display: flex;
  align-items: flex-end;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-5);
}

.filter-item label {
  display: block;
  margin-bottom: var(--space-1);
  color: var(--gray-500);
  font-size: var(--text-xs);
}

.filter-item .form-control {
  min-width: 150px;
}

/* --------------------------------------------------------------------------
   10. 徽标
   -------------------------------------------------------------------------- */
.badge {
  display: inline-block;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  line-height: var(--leading-normal);
  white-space: nowrap;
}

/* 审核状态 */
.badge-pending {
  background: var(--warning-100);
  color: var(--warning-900);
}

.badge-approved {
  background: var(--success-100);
  color: var(--success-900);
}

.badge-rejected {
  background: var(--danger-100);
  color: var(--danger-900);
}

/* 任务状态 */
.badge-not_started {
  background: var(--gray-200);
  color: var(--gray-700);
}

.badge-in_progress {
  background: var(--brand-100);
  color: var(--brand-800);
}

/* 任务「已结束」是正常的终态，用中性灰。
   注意：改造前 style.css 中另一条为扫码结果写的规则（.badge-not_found,
   .badge-not_approved,.badge-ended,...）特异性相同且位置靠后，把这里覆盖成了
   红色告警底，导致已结束的任务在列表里显示为异常态。现已按语义拆开。 */
.badge-ended {
  background: var(--gray-100);
  color: var(--gray-500);
}

/* 扫码结果（扫码记录页归并为两级：通过 / 未通过） */
.badge-pass {
  background: var(--success-100);
  color: var(--success-900);
}

/* .badge-fail 为归并后的「未通过」。其余细分 class 保留给操作日志页与
   将来可能恢复的细分展示复用 —— 语义同为告警，故合并到一条规则里。
   注意 .badge-not_approved 另被操作日志页用作「异常」标记，不可删除。 */
.badge-fail,
.badge-not_found,
.badge-not_approved,
.badge-group_mismatch,
.badge-project_mismatch {
  background: var(--danger-100);
  color: var(--danger-900);
}

/* 扫码记录表里的「已结束」表示该桶因任务结束被拦截，属告警语义，
   与任务状态的「已结束」区分开 */
.table-logs .badge-ended {
  background: var(--danger-100);
  color: var(--danger-900);
}

/* 过滤器兜底值，原样式表未定义该状态 */
.badge-unknown {
  background: var(--gray-100);
  color: var(--gray-500);
}

.role-tag {
  display: inline-block;
  margin-right: var(--space-1);
  padding: 2px var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--brand-100);
  color: var(--brand-800);
  font-size: var(--text-xs);
}

/* --------------------------------------------------------------------------
   11. 提示消息
   -------------------------------------------------------------------------- */
.alert {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  /* 左侧 3px 语义条用 currentColor：颜色跟着文字走，
     不必为四种状态各写一条 border-left 色值，也就不会漏掉新增状态。 */
  border-left: 3px solid currentColor;
  border-radius: var(--radius-md);
  margin-bottom: var(--space-4);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  box-shadow: var(--shadow-sm);
  /* 入场用弹性曲线：消息是"刚刚发生了什么"的通知，
     弹一下比平推更能把注意力从当前任务上拽过来。 */
  animation: alert-in var(--duration-base) var(--ease-spring) backwards;
}

/* 关闭（或自动淡出）时的退场。JS 负责加 is-leaving 再移除节点 ——
   直接把节点 remove 掉，用户会看到消息"啪"地消失，像是页面出了错。 */
.alert.is-leaving {
  opacity: 0;
  transform: translateY(-6px) scale(0.98);
  transition: opacity var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-out);
}

@keyframes alert-in {
  from {
    opacity: 0;
    transform: translateY(-8px) scale(0.97);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.alert-success {
  background: var(--success-100);
  color: var(--success-900);
}

.alert-error {
  background: var(--danger-100);
  color: var(--danger-900);
}

.alert-warning {
  background: var(--warning-100);
  color: var(--warning-900);
}

.alert-info {
  background: var(--brand-100);
  color: var(--brand-800);
}

.alert-close {
  flex: 0 0 auto;
  margin-left: auto;
  padding: 0 var(--space-1);
  background: none;
  border: none;
  color: inherit;
  font-size: 18px;
  line-height: 1;
  opacity: 0.5;
  cursor: pointer;
}

.alert-close:hover {
  opacity: 1;
}

.messages {
  margin-bottom: var(--space-2);
}

/* --------------------------------------------------------------------------
   12. 分页
   -------------------------------------------------------------------------- */
.pagination {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-4);
  font-size: var(--text-sm);
  color: var(--gray-500);
}

.pagination a {
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg-surface);
  color: var(--gray-700);
}

.pagination a:hover {
  border-color: var(--brand-600);
  color: var(--brand-600);
  text-decoration: none;
}

/* --------------------------------------------------------------------------
   13. 首页
   -------------------------------------------------------------------------- */
.home-section {
  margin: 0 0 var(--space-8);
}

.home-section-title {
  margin: 0 0 var(--space-4);
  color: var(--gray-600);
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
}

/* 待我处理：审核员登录后最该先看到的信息 */
.todo-banner {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-bottom: var(--space-6);
  padding: var(--space-4) var(--space-5);
  border: 1px solid var(--warning-500);
  border-left-width: 4px;
  border-radius: var(--radius-lg);
  background: var(--warning-50);
}

.todo-banner-count {
  flex: 0 0 auto;
  font-size: var(--text-3xl);
  font-weight: var(--weight-medium);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--warning-900);
}

.todo-banner-text {
  flex: 1;
  min-width: 0;
  color: var(--warning-900);
  font-size: var(--text-sm);
}

.todo-banner-text strong {
  font-weight: var(--weight-medium);
}

.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(172px, 1fr));
  gap: var(--space-4);
}

.stat-card {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: var(--space-2);
  min-height: 96px;
  padding: var(--space-5) var(--space-4);
  border-top: 3px solid var(--brand-600);
  border-radius: var(--radius-lg);
  background: var(--bg-surface);
  box-shadow: var(--shadow-sm);
  text-align: center;
}

.stat-card-pending {
  border-top-color: var(--warning-500);
}

.stat-card-pending .stat-num {
  color: var(--warning-600);
}

.stat-card-rejected {
  border-top-color: var(--danger-600);
}

.stat-card-rejected .stat-num {
  color: var(--danger-600);
}

.stat-card-ready {
  border-top-color: var(--gray-500);
}

.stat-card-ready .stat-num {
  color: var(--gray-600);
}

.stat-card-progress {
  border-top-color: var(--brand-600);
}

.stat-card-progress .stat-num {
  color: var(--brand-600);
}

.stat-card-ended {
  border-top-color: var(--gray-400);
}

.stat-card-ended .stat-num {
  color: var(--gray-500);
}

.stat-num {
  font-size: var(--text-3xl);
  font-weight: var(--weight-medium);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
  color: var(--brand-600);
}

.stat-label {
  color: var(--gray-500);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
}

.stat-link {
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  transition: box-shadow var(--duration-base) var(--ease-out);
}

/* 去掉改造前的 translateY 位移：卡片位移在扫读时反而干扰定位 */
.stat-link:hover {
  text-decoration: none;
  box-shadow: var(--shadow-md);
}

.quick-links {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
}

/* --------------------------------------------------------------------------
   14. 权限标签
   -------------------------------------------------------------------------- */
.perm-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

.perm-tag {
  padding: 2px var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--gray-200);
  color: var(--gray-700);
  font-size: var(--text-xs);
}

/* ⚠ 必须是 div.perm-checkboxes，不能只写 .perm-checkboxes。
   Django 会把 widget 的 class 同时渲染到**容器 div 和每个 <input>** 上，
   而这里是 grid 布局 —— 一旦命中 <input>，复选框自己变成 grid 容器、
   被压成窄柱，标签文字挤成竖排（2026-10-14 用真实页面截图实测到）。
   限定到容器 div 后，input 恢复原生样式。 */
div.perm-checkboxes {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--space-2);
}

.perm-checkboxes label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: var(--weight-regular);
}

/* --------------------------------------------------------------------------
   15. 行内操作
   -------------------------------------------------------------------------- */
.row-actions {
  white-space: nowrap;
}

.row-actions a {
  margin-right: var(--space-3);
}

.row-actions a:last-child {
  margin-right: 0;
}

/* --------------------------------------------------------------------------
   16. 登录
   -------------------------------------------------------------------------- */
.login-box {
  max-width: 400px;
  margin: 80px auto;
  padding: var(--space-8);
  border-radius: var(--radius-lg);
  background: var(--bg-surface);
  box-shadow: var(--shadow-md);
}

.login-box h2 {
  margin: 0 0 var(--space-6);
  text-align: center;
}

/* --------------------------------------------------------------------------
   17. 扫码核验（现场页面，数值保持改造前不变）
   -------------------------------------------------------------------------- */
.scan-panel {
  padding: var(--space-6);
  border-radius: var(--radius-lg);
  background: var(--bg-surface);
  box-shadow: var(--shadow-sm);
  /* 现场页内容窄一点更好读，也避免大屏上表单拉得过长 */
  max-width: 720px;
}

/* 分步区块：把「选项目分组」与「扫码」拆成两个带编号的步骤，
   顺序即操作顺序，降低现场误操作。 */
.scan-step + .scan-step {
  margin-top: var(--space-6);
  padding-top: var(--space-6);
  border-top: 1px dashed var(--border);
}

.scan-step-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.scan-step-no {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-full);
  background: var(--brand-600);
  color: #fff;
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  line-height: 1;
}

.scan-step-title {
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  color: var(--gray-900);
}

.scan-step-body .form-group:last-child {
  margin-bottom: 0;
}

.mode-switch {
  display: flex;
  gap: var(--space-6);
  margin-bottom: var(--space-5);
}

.mode-switch label {
  cursor: pointer;
}

.scan-input-area {
  margin-top: var(--space-5);
}

/* 分步结构里，扫码区顶部间距已由 .scan-step-head 提供，避免叠加 */
.scan-step .scan-input-area {
  margin-top: 0;
}

/* 当前核验对象：连续扫码时的上下文锚点。
   刻意放在扫码区之外 —— 扫码区禁用时会整体降不透明度，
   这条信息留在外面才能始终清晰，避免「忘了现在扫的是哪一组」。 */
.scan-context {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
  padding: 10px var(--space-4);
  border-left: 4px solid var(--brand-600);
  border-radius: var(--radius-md);
  background: var(--brand-50);
}

.scan-context-label {
  flex: 0 0 auto;
  color: var(--gray-600);
  font-size: var(--text-sm);
}

.scan-context-value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-lg);
  font-weight: var(--weight-medium);
  color: var(--brand-700);
  text-align: right;
  overflow-wrap: anywhere;
}

.scan-context-value.is-empty {
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: var(--weight-regular);
  color: var(--gray-500);
}

.scan-waiting-zone {
  position: relative;
  padding: 28px 24px;
  border: 3px solid var(--warning-500);
  border-radius: var(--radius-xl);
  background: linear-gradient(180deg, #fff8e1 0%, #ffcc80 100%);
  box-shadow: 0 4px 20px rgba(245, 158, 11, 0.35);
  text-align: center;
}

.scan-waiting-badge {
  display: inline-block;
  margin-bottom: 14px;
  padding: 8px 28px;
  border: none;
  border-radius: 24px;
  /* 对比度：--warning-500 配白字仅 2.16:1，远低于 WCAG AA 4.5:1；
     --warning-700 为 5.0:1，与 .btn-warning 的既有修法保持同一口径。 */
  background: var(--warning-700);
  color: #fff;
  font-family: inherit;
  font-size: 20px;
  font-weight: var(--weight-medium);
  letter-spacing: 2px;
  cursor: pointer;
  outline: none;
  box-shadow: 0 2px 8px rgba(180, 83, 9, 0.4);
  transition: background-color var(--duration-fast) var(--ease-out),
    box-shadow var(--duration-fast) var(--ease-out);
}

/* 悬停需比基础色更深，否则在黄色底上「变浅」会削弱可读性 */
.scan-waiting-badge:hover {
  background: var(--warning-900);
}

.scan-waiting-badge:active {
  transform: translateY(1px);
}

.scan-waiting-badge:focus-visible {
  outline: 2px solid var(--warning-700);
  outline-offset: 2px;
}

/* 监听中：原 #16a34a 配白字仅 3.3:1，改 success-700 达 5.5:1 */
.scan-waiting-badge.is-active {
  background: var(--success-700);
  box-shadow: 0 2px 8px rgba(4, 120, 87, 0.4);
}

.scan-waiting-badge.is-active:hover {
  background: var(--success-900);
}

.scan-waiting-zone .hint {
  margin-bottom: var(--space-4);
  color: var(--warning-900);
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
}

.scan-waiting-zone .scan-input {
  background: #fff;
  border: 3px solid var(--warning-500);
  color: var(--warning-900);
  font-size: 24px;
  font-weight: var(--weight-medium);
}

.scan-waiting-zone .scan-input::placeholder {
  color: var(--warning-600);
  opacity: 0.8;
}

.scan-waiting-zone .scan-input:focus {
  outline: none;
  border-color: var(--warning-600);
  box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.35);
}

/* 隐藏的捕获输入框：承接扫码枪的键盘流，需覆盖在按钮之上 */
.scan-capture {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 48px;
  margin: 0;
  padding: 0;
  border: 0;
  opacity: 0;
  z-index: 2;
  cursor: text;
}

/* 现场读数区（本页最关键的显示元素）。
   有值时 32px 等宽：动物房的实际观看距离下，原 24px 偏小；
   空态另用小字号（见 .is-empty），否则一行占位提示会撑出一片空荡。
   常态即 flex 居中，使空态/有值态切换时高度不跳动。 */
.scan-display {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 84px;
  padding: var(--space-4);
  border: 3px solid var(--warning-600);
  border-radius: var(--radius-lg);
  background: #fff;
  color: var(--warning-900);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 32px;
  font-weight: var(--weight-medium);
  line-height: 1.3;
  text-align: center;
  word-break: break-all;
}

.scan-display.is-empty {
  color: var(--warning-700);
  font-family: inherit;
  font-size: var(--text-base);
}

.scan-alert {
  margin: var(--space-4) 0;
  padding: 14px 18px;
  border: 2px solid var(--warning-500);
  border-radius: var(--radius-lg);
  background: var(--warning-100);
  color: var(--warning-900);
  font-size: 15px;
  font-weight: var(--weight-medium);
}

.scan-zone-disabled {
  opacity: 0.55;
  filter: grayscale(0.15);
}

.scan-zone-disabled .scan-waiting-badge {
  background: var(--gray-400);
  box-shadow: none;
  cursor: not-allowed;
}

.scan-zone-disabled .scan-waiting-badge:hover {
  background: var(--gray-400);
}

.scan-result {
  margin-top: var(--space-6);
  padding: var(--space-6);
  border-radius: var(--radius-lg);
  font-size: var(--text-lg);
  text-align: center;
}

.scan-result.pass {
  background: var(--success-100);
  color: var(--success-900);
  border: 2px solid var(--success-600);
}

.scan-result.fail {
  background: var(--danger-100);
  color: var(--danger-900);
  border: 2px solid var(--danger-600);
}

.hint {
  color: var(--gray-500);
  text-align: center;
}

/* 表格上方的说明文字：与表格左缘对齐（居中版留给扫码页等待区那类"舞台中央"提示） */
.hint-left {
  text-align: left;
}

/* --------------------------------------------------------------------------
   18. 扫码弹窗
   -------------------------------------------------------------------------- */
.scan-modal {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
}

.scan-modal.hidden {
  display: none !important;
}

.scan-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
}

.scan-modal-box {
  position: relative;
  width: 90%;
  max-width: 480px;
  padding: 32px 28px;
  border-radius: var(--radius-xl);
  background: var(--bg-surface);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
  text-align: center;
}

.scan-modal-box.modal-pass {
  border-top: 6px solid var(--success-600);
}

.scan-modal-box.modal-fail {
  border-top: 6px solid var(--danger-600);
}

.scan-modal-icon {
  margin-bottom: var(--space-3);
  font-size: 56px;
  font-weight: var(--weight-medium);
  line-height: 1;
}

.modal-pass .scan-modal-icon {
  color: var(--success-600);
}

.modal-fail .scan-modal-icon {
  color: var(--danger-600);
}

.scan-modal-title {
  margin-bottom: var(--space-3);
  font-size: var(--text-2xl);
  font-weight: var(--weight-medium);
}

.scan-modal-message {
  margin-bottom: var(--space-4);
  font-size: var(--text-lg);
  line-height: var(--leading-normal);
}

.scan-modal-detail {
  margin-bottom: var(--space-5);
  padding: var(--space-4);
  border-radius: var(--radius-lg);
  background: var(--bg-muted);
  font-size: 15px;
  text-align: left;
}

.scan-modal-detail .detail-line {
  display: flex;
  justify-content: space-between;
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--gray-200);
}

.scan-modal-detail .detail-line:last-child {
  border-bottom: none;
}

.scan-modal-detail .detail-line span {
  color: var(--gray-500);
}

.scan-modal-detail .match-ok,
.scan-modal-detail .match-fail {
  justify-content: center;
  margin-top: var(--space-2);
  font-weight: var(--weight-medium);
}

.scan-modal-detail .match-ok {
  color: var(--success-600);
}

.scan-modal-detail .match-fail {
  color: var(--danger-600);
}

/* --------------------------------------------------------------------------
   19. 手机扫二维码结果页
   -------------------------------------------------------------------------- */
.verify-page {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 80vh;
}

.verify-card {
  width: 100%;
  max-width: 480px;
  padding: 40px 32px;
  border-radius: var(--radius-xl);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
  text-align: center;
}

.verify-pass {
  border-top: 6px solid var(--success-600);
  background: var(--success-50);
}

.verify-fail {
  border-top: 6px solid var(--danger-600);
  background: var(--danger-50);
}

.verify-icon {
  margin-bottom: var(--space-4);
  font-size: 64px;
  font-weight: var(--weight-medium);
}

.verify-pass .verify-icon {
  color: var(--success-600);
}

.verify-fail .verify-icon {
  color: var(--danger-600);
}

.verify-message {
  margin: var(--space-4) 0;
  font-size: var(--text-lg);
}

.verify-detail {
  margin-top: var(--space-4);
  padding: var(--space-4);
  border-radius: var(--radius-lg);
  background: var(--bg-surface);
  text-align: left;
}

.verify-detail .detail-line {
  display: flex;
  justify-content: space-between;
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--gray-200);
  font-size: var(--text-base);
}

.verify-detail .detail-line:last-child {
  border-bottom: none;
}

.verify-detail .detail-line span {
  color: var(--gray-500);
}

/* --------------------------------------------------------------------------
   20. 帮助文档
   -------------------------------------------------------------------------- */
.doc-layout {
  display: flex;
  align-items: flex-start;
  gap: var(--space-6);
  flex-wrap: wrap;
}

.doc-nav {
  flex: 0 0 200px;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  border-radius: var(--radius-lg);
  background: var(--bg-surface);
  box-shadow: var(--shadow-sm);
}

.doc-nav a {
  display: block;
  padding: 10px 14px;
  border-radius: var(--radius-md);
  color: var(--gray-700);
  font-weight: var(--weight-medium);
}

.doc-nav a:hover {
  background: var(--gray-100);
  text-decoration: none;
}

.doc-nav a.active {
  background: var(--brand-100);
  color: var(--brand-800);
  font-weight: var(--weight-medium);
}

.doc-content {
  flex: 1;
  min-width: 280px;
  padding: 28px 32px;
  border-radius: var(--radius-lg);
  background: var(--bg-surface);
  box-shadow: var(--shadow-sm);
  line-height: var(--leading-loose);
}

.doc-content h2 {
  margin: 28px 0 var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--gray-200);
  font-size: var(--text-xl);
  color: var(--gray-800);
}

.doc-content h2:first-child {
  margin-top: 0;
}

.doc-content h3 {
  margin: var(--space-5) 0 var(--space-2);
  font-size: var(--text-base);
  color: var(--gray-700);
}

.doc-content ul,
.doc-content ol {
  margin: var(--space-2) 0 var(--space-4);
  padding-left: var(--space-6);
}

.doc-content li {
  margin: var(--space-1) 0;
}

.doc-content code {
  padding: 2px var(--space-1);
  border-radius: var(--radius-sm);
  background: var(--gray-100);
  font-family: var(--font-mono);
  font-size: 13px;
}

.doc-pre {
  padding: var(--space-4);
  border-radius: var(--radius-lg);
  background: var(--gray-800);
  color: var(--gray-50);
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: var(--leading-relaxed);
  overflow-x: auto;
}

.doc-table {
  margin: var(--space-3) 0 var(--space-5);
  font-size: var(--text-sm);
}

.doc-faq dt {
  margin-top: var(--space-4);
  color: var(--gray-700);
  font-weight: var(--weight-medium);
}

.doc-faq dd {
  margin: var(--space-1) 0 0;
  color: var(--gray-500);
}

.doc-version {
  margin-top: var(--space-8);
  padding-top: var(--space-4);
  border-top: 1px solid var(--gray-200);
  font-size: 13px;
}

/* --------------------------------------------------------------------------
   21. 日志审计
   -------------------------------------------------------------------------- */
.page-subtitle {
  margin: calc(-1 * var(--space-2)) 0 var(--space-6);
  color: var(--gray-500);
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
}

.audit-stats {
  margin-bottom: var(--space-6);
}

/* 统计卡片的辅助说明：比 stat-label 更弱，不抢数字的注意力 */
.stat-hint {
  /* 原为 --gray-400：它压在纯白底上也只有 2.54:1，12px 正文远不达 4.5:1。
     （既有缺陷，不是材质改造引入的 —— 纯白基准同样算不过。）
     退到 --gray-500 后常态 4.59:1，仍比 stat-label 弱，层级没丢。 */
  color: var(--gray-500);
  font-size: var(--text-xs);
  line-height: var(--leading-normal);
}

/* 通用面板：与 .stat-card 同一视觉语言（圆角 + 极浅阴影） */
.panel {
  margin-bottom: var(--space-6);
  padding: var(--space-5);
  border-radius: var(--radius-lg);
  background: var(--bg-surface);
  box-shadow: var(--shadow-sm);
}

.panel > h2 {
  margin: 0 0 var(--space-4);
  font-size: var(--text-lg);
  font-weight: var(--weight-medium);
  color: var(--gray-900);
}

.panel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-bottom: var(--space-4);
}

.panel-head > h2 {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: var(--weight-medium);
  color: var(--gray-900);
}

/* 两栏分组统计：窄屏自动堆叠 */
.panel-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: var(--space-5);
  margin-bottom: var(--space-6);
}

.panel-grid > .panel {
  margin-bottom: 0;
}

/* 图例 */
.legend {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  color: var(--gray-500);
  font-size: var(--text-xs);
}

.legend-item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.legend-dot {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: var(--radius-sm);
}

.legend-op {
  background: var(--brand-600);
}

.legend-scan {
  background: var(--success-600);
}

/* 趋势图：等比高度，柱体宽度由后端算好，这里只负责尺寸约束 */
.trend-chart {
  display: block;
  width: 100%;
  height: 200px;
  margin-bottom: var(--space-3);
  overflow: visible;
}

.trend-axis-label {
  fill: var(--gray-400);
  font-size: 10px;
  font-family: var(--font-mono);
}

.trend-group:hover rect {
  opacity: 0.82;
}

.trend-group rect {
  transition: opacity var(--duration-fast) var(--ease-out);
}

/* 占比条：宽度由模板内联 style 提供，这里只管外观 */
.bar {
  display: block;
  height: 8px;
  min-width: 2px;
  border-radius: var(--radius-full);
  background: var(--brand-600);
}

/* 数字列右对齐，便于纵向比对量级 */
.col-num {
  width: 72px;
  text-align: right !important;
}

/* 紧凑表格：分组统计用，行高压低一档 */
.table-compact th,
.table-compact td {
  padding: var(--space-2) var(--space-3);
}

.cell-detail {
  max-width: 320px;
  color: var(--gray-600);
  overflow-wrap: anywhere;
}

/* 异常行：左侧色条 + 极浅底色，扫读时不刺眼 */
.row-abnormal > td:first-child {
  box-shadow: inset 3px 0 0 var(--danger-600);
}

.row-abnormal {
  background: var(--danger-50);
}

.row-abnormal:hover {
  background: var(--danger-100);
}

/* 日志类型徽标：与任务状态徽标区分开 */
.badge-op {
  background: var(--brand-100);
  color: var(--brand-700);
}

.badge-scan {
  background: var(--success-100);
  color: var(--success-900);
}

/* 审计筛选条：逐项换行，最后一行右侧放按钮 */
.filter-bar-audit .filter-item {
  flex: 0 1 180px;
}

.filter-item-wide {
  flex: 1 1 260px;
}

.filter-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-left: auto;
}

.filter-note {
  margin: 0 0 var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-left: 3px solid var(--warning-500);
  border-radius: var(--radius-sm);
  background: var(--warning-50);
  color: var(--warning-900);
  font-size: var(--text-xs);
}

/* 审计页的提示文字左对齐：居中在长表格上方会显得悬空 */
.panel .hint {
  margin: var(--space-3) 0 0;
  text-align: left;
  font-size: var(--text-xs);
}

/* --------------------------------------------------------------------------
   22. 工具类
   -------------------------------------------------------------------------- */
.text-success {
  color: var(--success-600);
}

.text-danger {
  color: var(--danger-600);
}

.text-muted {
  color: var(--gray-500);
}

.hidden {
  display: none !important;
}

/* --------------------------------------------------------------------------
   23. 系统管理标签页 / 基础数据三栏级联
   仅新增类名，不改动既有规则；色值一律取自 tokens.css。
   -------------------------------------------------------------------------- */

/* 视觉隐藏但对读屏可见。用于给内联编辑的输入框配 label —— 
   编号框没有可见标签（列标题已表达含义），但读屏需要文字标签。 */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---------- 标签页 ---------- */
.tab-bar {
  display: flex;
  gap: var(--space-1);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--space-6);
}

.tab-item {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--touch-comfort);
  padding: 0 var(--space-4);
  color: var(--gray-600);
  font-weight: var(--weight-medium);
  border-radius: var(--radius-md) var(--radius-md) 0 0;
  transition: color var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out);
}

.tab-item:hover {
  color: var(--brand-700);
  background: var(--brand-50);
  text-decoration: none;
}

.tab-item.is-active {
  color: var(--brand-700);
}

/* 选中态是压在分隔线上的一条细线，而不是整块染色：
   标签属于导航，强调色留给关键动作。 */
.tab-item.is-active::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  background: var(--brand-600);
}

/* 计数用等宽数字，切换标签时不会因位数变化而左右抖动 */
.tab-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 20px;
  padding: 0 var(--space-1);
  border-radius: var(--radius-full);
  background: var(--gray-100);
  color: var(--gray-600);
  font-size: var(--text-xs);
  font-weight: var(--weight-regular);
  font-variant-numeric: tabular-nums;
}

.tab-item.is-active .tab-count {
  background: var(--brand-100);
  color: var(--brand-700);
}

/* --------------------------------------------------------------------------
   23b. 角色管理表（固定列宽）
   --------------------------------------------------------------------------
   为什么需要单独定列宽（2026-09-20 实测）：权限列的标签数量差异极大 ——
   内置「管理员」有 15 个标签，自建角色可能 0 个。`table-layout: auto` 按
   各列 max-content 分配宽度，权限列被分到约 570px，结果「说明」被压成
   两行（末字单独悬在第二行）、「操作」只剩约 40px 贴在表格边缘 ——
   整张表看起来列宽失衡。
   改为固定布局 + colgroup 指定宽度：权限列吃剩余宽度，标签在自身列内换行。
   -------------------------------------------------------------------------- */
.table-roles {
  table-layout: fixed;
  /* 窄屏不把列压扁，交给 .table-wrap 的 overflow-x 横向滚动 */
  min-width: 880px;
}

.table-roles .col-role-name { width: 132px; }
.table-roles .col-role-code { width: 92px; }
.table-roles .col-role-desc { width: 200px; }
.table-roles .col-role-perm { width: auto; }
.table-roles .col-role-users { width: 64px; }
.table-roles .col-role-actions { width: 104px; }

/* 列宽已由 colgroup 决定，这里只管对齐（.col-num 会额外写死宽度，故不复用） */
.table-roles .is-num {
  text-align: right;
}

/* ---------- 级联工作台 ---------- */
/* 左：试验项目（根，窄）；右：动物分组 / 制剂 / 桶池 三段纵排。
   右侧刻意纵排而不是三栏并排：分组与制剂都归属选中的项目，并排会读成同级关系；
   桶池虽与项目无关，但放在同一列最下方，正好用"它不跟着变"来传达它的独立性。 */
.cascade {
  display: grid;
  grid-template-columns: minmax(200px, 260px) minmax(0, 1fr);
  gap: var(--space-5);
  align-items: start;
  margin-bottom: var(--space-5);
}

.cascade-stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  min-width: 0;
}

.cascade-path {
  margin-bottom: var(--space-4);
}

.cascade-col {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
  padding: var(--space-5);
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}

.cascade-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.cascade-head h2 {
  margin: 0;
  font-size: var(--text-base);
}

.cascade-scope {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--gray-500);
}

/* ---------- 状态筛选（卡片内三档） ----------
   把做完（已停用）的编号从列表里收起来，列表就只留进行中的。

   **为什么不直接复用 .tab-bar**：那是**页面级**导航标签，.tab-item 左右各留
   16px 内边距，三档连计数徽标加起来约 300px；而项目栏是 grid 的
   minmax(200px, 260px)，最窄只有 200px —— 会当场挤成两行。这里用同一套视觉
   语言、按卡片宽度重新量的紧凑版：灰底槽 + 白色浮起的小块。 */
.cascade-filter {
  display: flex;
  gap: var(--space-1);
  padding: 3px;
  background: var(--gray-100);
  border-radius: var(--radius-md);
}

.cascade-filter-item {
  display: inline-flex;
  flex: 1 1 0;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  min-width: 0;
  /* 桌面密集筛选项，32px 够点；不用 44px 触摸下限 —— 那会让这一条
     在项目栏里比标题还抢眼，而它只是个过滤器。 */
  min-height: 32px;
  padding: 0 var(--space-2);
  border-radius: var(--radius-sm);
  color: var(--gray-600);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  white-space: nowrap;
  transition: color var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out);
}

.cascade-filter-item:hover {
  color: var(--brand-700);
  text-decoration: none;
}

/* 选中档 = 白底浮起 + 品牌色文字。
   用底色而不是 .tab-bar 那条下划线：下划线是"当前页面"的语言，
   这里表达的是"当前过滤条件"，浮起的小块更贴近语义。 */
.cascade-filter-item.is-active {
  background: var(--bg-surface);
  color: var(--brand-700);
  box-shadow: var(--shadow-sm);
}

/* 计数沿用 .tab-count 的等宽数字（切档时位数变化不会左右抖），
   但要把它的灰底抹掉：底槽本身就是 --gray-100，同色等于没画，
   露出来反而像个没对齐的小色块。改成继承字色 + 降透明度。 */
.cascade-filter-item .tab-count {
  min-width: 0;
  height: auto;
  padding: 0;
  background: transparent;
  color: inherit;
  opacity: 0.72;
}

/* ---------- 刚新增的那一条 ----------
   新增成功后会跳到它并选中它，但"跳到某一行"本身没有视觉重量 ——
   一屏十几行编号，眼睛还是得自己找。这里让那一行从品牌浅底淡出，
   给一次 ~2.4s 的提示。类由模板直接渲染，不需要任何 JS。

   **必须是 animation，不能是 transition**：这个类在服务端渲染时就已经挂在
   元素上了，不存在"前一帧"，transition 拿不到起始值会直接停在终态、
   一点都不闪（v1.3.0 的 .reveal 正是栽在这儿，见 UI-DESIGN.md §10）。 */
@keyframes cascade-item-new {
  from { background-color: var(--brand-100); }
  to { background-color: transparent; }
}

.cascade-item.is-new {
  animation: cascade-item-new 2.4s var(--ease-out) 1;
}

/* 减弱动效时不必另写规则：文件末尾的全局兜底会把动画时长压到 0.01ms，
   这一闪自然消失，而 .is-selected 的选中态不受影响 —— "新增了哪一条"
   这个信息不会因为关掉动效就丢掉。 */

.cascade-list {
  margin: 0;
  padding: 0;
  list-style: none;
  max-height: 380px;
  overflow-y: auto;
  border-top: 1px solid var(--border);
}

.cascade-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--touch-min);
  padding-left: var(--space-3);
  /* 左侧预留 3px 作为选中标记的位置，让选中/未选中的内容左边界严格对齐 */
  border-left: 3px solid transparent;
  border-bottom: 1px solid var(--border);
  transition: background-color var(--duration-fast) var(--ease-out),
              border-left-color var(--duration-fast) var(--ease-out);
}

.cascade-item:hover {
  background: var(--gray-50);
}

.cascade-item.is-selected {
  background: var(--brand-50);
  border-left-color: var(--brand-600);
}

.cascade-link {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  padding: var(--space-2) 0;
  color: var(--gray-900);
}

.cascade-link:hover {
  text-decoration: none;
}

.cascade-link:not(.is-static):hover .cascade-code {
  color: var(--brand-700);
}

.cascade-link.is-static {
  cursor: default;
}

.cascade-code {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cascade-meta {
  font-size: var(--text-xs);
  color: var(--gray-500);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cascade-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 0 0 auto;
  font-size: var(--text-xs);
}

/* 行内动作常态即"低对比但不隐藏"：藏到 hover 才出现会难发现，
   触屏上更是基本点不到。悬停时再升到品牌色/危险色。 */
.cascade-actions a,
.cascade-actions .btn-link-danger {
  color: var(--gray-500);
  font-size: var(--text-xs);
}

.cascade-actions a:hover {
  color: var(--brand-700);
}

.cascade-actions .btn-link-danger:hover {
  color: var(--danger-600);
}

.cascade-empty {
  padding: var(--space-6) var(--space-3);
  text-align: center;
  color: var(--gray-500);
  border-bottom: 1px solid var(--border);
}

.cascade-empty-title {
  display: block;
  margin-bottom: var(--space-1);
  color: var(--gray-700);
}

.cascade-empty-desc {
  display: block;
  font-size: var(--text-xs);
}

/* ---------- 批量新增 ---------- */
.batch-add {
  margin-top: auto;
  padding-top: var(--space-3);
  border-top: 1px dashed var(--border-strong);
}

.batch-add > summary {
  display: flex;
  align-items: center;
  min-height: var(--touch-min);
  color: var(--brand-600);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  cursor: pointer;
  list-style: none;
}

.batch-add > summary::-webkit-details-marker {
  display: none;
}

.batch-add > summary:hover {
  color: var(--brand-700);
}

/* 展开指示符用文字而非三角形字符，避免不同系统字形差异 */
.batch-add > summary::before {
  content: "＋";
  margin-right: var(--space-2);
}

.batch-add[open] > summary::before {
  content: "－";
}

.batch-add form {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.batch-add .form-control {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}

/* ---------- 内联编辑行 ---------- */
.preset-edit {
  flex: 1 1 100%;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-2) 0;
}

.preset-edit-fields {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.preset-edit .form-control {
  min-height: 32px;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}

.preset-edit-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

.preset-edit-actions {
  display: flex;
  gap: var(--space-2);
}

/* 批量导入用户：参数摘要与解析提示都只是短列表，不需要表格那么重 */
.import-params {
  margin: 0;
  padding-left: var(--space-5);
  color: var(--gray-700);
}

.import-params li {
  margin-bottom: var(--space-2);
}

.import-params li:last-child {
  margin-bottom: 0;
}

.import-errors {
  margin: 0;
  padding-left: var(--space-5);
  color: var(--danger-700);
}

.import-errors li {
  margin-bottom: var(--space-1);
}

.import-errors li:last-child {
  margin-bottom: 0;
}

/* --------------------------------------------------------------------------
   24. 桶池占用态 / 任务级联选择器
   -------------------------------------------------------------------------- */

/* ---------- 桶池 ---------- */
/* 「独立池」胶囊标记：说明这个编号池不挂在任何试验项目下 */
.cascade-tag {
  margin-left: var(--space-2);
  padding: 1px var(--space-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-full);
  color: var(--gray-500);
  font-size: var(--text-xs);
  font-weight: var(--weight-regular);
  vertical-align: middle;
}

/* 单栏版级联容器：桶池独立成页后只有一列，用不上 .cascade 的两栏网格。

   这里**刻意不限宽**。试过 max-width: 820px，结果在 1440 宽屏上列表卡比它正上方的
   统计卡窄掉近 300px —— 一页之内两种卡片宽度，看着像没对齐的意外。
   满宽之后与统计卡同宽，且「编号在左、操作在右」本来就是分组/制剂卡的既有排布。
   限宽的版本实测过，见 UI-DESIGN.md §7「桶池独立页」那段的记录。 */
.cascade-solo {
  max-width: 100%;
}

.cascade-list-short {
  max-height: 220px;
}

.cascade-list-scroll {
  max-height: 320px;
}

/* 占用中的桶：灰化 + 编号划掉。划掉是必要的第二重信号 ——
   只靠灰度在低对比屏幕或色觉差异下几乎看不出区别。 */
.cascade-item.is-locked .cascade-code {
  color: var(--gray-500);
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}

.cascade-item.is-locked .cascade-meta strong {
  color: var(--gray-700);
}

/* ---------- 任务级联选择器 ---------- */
/* 纵向 flex + 统一 gap：四个步骤、摘要条、备注、按钮的间距由容器统一给，
   各块不必各自写 margin，改一处即可整页对齐。 */
#task-picker {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

#task-picker .form-group {
  margin-bottom: 0;
}

.picker-step {
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-muted);
  /* backwards 让动画在延迟期间保持起点状态，无需 JS 加类 ——
     脚本没跑起来时元素也一定是可见的终态 */
  animation: picker-in var(--duration-base) var(--ease-out) backwards;
}

@keyframes picker-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

/* 一次编排好的入场：四组错峰 70ms，正好跟着"选项目 → 分组/制剂 → 桶"的顺序走 */
.picker-step[data-step="project"] { animation-delay: 0ms; }
.picker-step[data-step="group"]   { animation-delay: 70ms; }
.picker-step[data-step="prep"]    { animation-delay: 140ms; }
.picker-step[data-step="bucket"]  { animation-delay: 210ms; }

.picker-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}

.picker-head label {
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--gray-700);
}

/* 步骤序号小圆牌：选中后点亮，"走到第几步"一眼可见 */
.picker-no {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  border-radius: var(--radius-full);
  background: var(--gray-200);
  color: var(--gray-600);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  transition: background-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.picker-step.is-filled {
  background: var(--bg-surface);
  border-color: var(--brand-200);
}

.picker-step.is-filled .picker-no {
  background: var(--brand-600);
  color: #fff;
}

/* 编号一律等宽：0/O、1/I 认错在拿桶这件事上是灾难性的 */
.picker-step .form-control {
  min-height: var(--touch-comfort);
  font-family: var(--font-mono);
  font-size: var(--text-base);
}

/* 离散选项给 iOS 一个原生滚轮，比让用户在小下拉里翻更容易点准 */
.picker-step select {
  appearance: auto;
}

.picker-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4);
}

.picker-hint {
  margin: var(--space-2) 0 0;
  color: var(--gray-500);
  font-size: var(--text-xs);
  line-height: var(--leading-normal);
}

.picker-hint strong {
  color: var(--gray-700);
}

.picker-summary {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--bg-muted);
  transition: border-color var(--duration-fast) var(--ease-out),
    background-color var(--duration-fast) var(--ease-out);
}

/* 四项齐了才变绿：这是"可以提交了"的唯一视觉信号，不必去数下拉框 */
.picker-summary.is-ready {
  border-style: solid;
  border-color: var(--success-600);
  background: var(--success-50);
}

.picker-summary-label,
.picker-summary-count {
  flex: 0 0 auto;
  color: var(--gray-500);
  font-size: var(--text-xs);
}

.picker-summary-count {
  font-variant-numeric: tabular-nums;
}

.picker-summary-value {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  color: var(--gray-700);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.picker-summary-value .sep {
  padding: 0 var(--space-1);
  color: var(--gray-400);
}

.form-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
}

/* 筛选条里的「显示已结束」：与其它输入框底对齐（.filter-bar 是 align-items: flex-end） */
.filter-toggle .check-line {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 38px;
  color: var(--gray-700);
  font-size: var(--text-sm);
  cursor: pointer;
}

@media (max-width: 768px) {
  /* 分组与制剂在窄屏各占一行 —— 两栏并排时下拉框只剩一半宽度，
     长编号会被截断，而编号恰恰是这里唯一要看清的信息 */
  .picker-row {
    grid-template-columns: 1fr;
  }

  .picker-summary {
    flex-wrap: wrap;
  }

  .picker-summary-value {
    flex-basis: 100%;
  }
}

/* --------------------------------------------------------------------------
   26. 材质层：毛玻璃 / 渐变网格 / 颗粒 / 柔阴影
   --------------------------------------------------------------------------
   这一节是"哪些容器是玻璃、用哪一档透明度"的**唯一来源**：只在这里声明一次。

   为什么不逐个改组件的 background：
     玻璃是一件事（同一套模糊半径、同一套描边、同一套阴影），不是 N 件事。
     分散到十几处去写，第 N+1 个容器加进来时必然漏一个 —— 而那种漏法在页面上
     表现为"这一块看着不对"，肉眼很难归因到是漏了一行。

   ⚠ 有语义边框的容器**不在下面这个组里**，见紧随其后的两条单独规则：
     .stat-card       顶部 3px 状态色条（.stat-card-pending 等只改 border-top-color）
     .scan-modal-box  顶部 6px 通过/未通过条（.modal-pass / .modal-fail）
   一旦对它们写 border 简写，会把 border-top-color 一并重置，状态色**静默消失** ——
   页面照常打开，只是"待审核"和"已通过"长得一模一样。所以那两条只写
   background / backdrop-filter / box-shadow，绝不碰 border。
   -------------------------------------------------------------------------- */
.form-card,
.detail-card,
.info-box,
.panel,
.scan-panel,
.login-box,
.cascade-col {
  background: var(--glass-bg);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  border: 1px solid var(--glass-border);
  /* 三层叠加：外投影（高度）+ 上缘内高光（片材厚度）+ 一圈极淡的外描边
     （在浅色网格上留住轮廓，只靠白边会"化"进背景） */
  box-shadow: var(--shadow-md), var(--glass-sheen), 0 0 0 1px var(--glass-hairline);
  transition: box-shadow var(--duration-base) var(--ease-out),
    transform var(--duration-base) var(--ease-out);
}

/* 无损状态条的玻璃（不写 border） */
.stat-card,
.scan-modal-box {
  background: var(--glass-bg-strong);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  box-shadow: var(--shadow-md), var(--glass-sheen), 0 0 0 1px var(--glass-hairline);
}

/* 表格：用 94% 白而不是 72%。
   表体是逐行读文本的地方（桶编号要逐字符核对），玻璃透出的底色会实打实
   压低小字号对比度 —— 这里保留"同属一层材质"的观感，换取可读性不打折。 */
.table {
  background: rgba(255, 255, 255, 0.94);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  box-shadow: var(--shadow-md), 0 0 0 1px var(--glass-hairline);
}

/* 表头做半透明底 + 发丝下边线，比纯灰底更像一块"压在纸上的薄片" */
.table th {
  background: rgba(249, 250, 251, 0.86);
  border-bottom-color: rgba(16, 44, 107, 0.1);
}

/* 任务级联选择器的四个步骤：软玻璃。
   比外层 .form-card 更透一档，形成"面板裹着面板"的层次，而不是四块一样的白。 */
.picker-step {
  background: var(--glass-bg-soft);
  -webkit-backdrop-filter: blur(12px) saturate(var(--glass-saturate));
  backdrop-filter: blur(12px) saturate(var(--glass-saturate));
  box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.8);
}

/* 不支持的浏览器：退回不透明，宁可丢质感也不丢可读性 */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .form-card,
  .detail-card,
  .info-box,
  .panel,
  .scan-panel,
  .login-box,
  .cascade-col {
    background: var(--bg-surface);
  }

  .stat-card,
  .scan-modal-box,
  .picker-step {
    background: var(--bg-surface);
  }

  .table {
    background: var(--bg-surface);
  }
}

/* 弹窗遮罩：从纯黑半透明改成"磨砂玻璃罩"。
   模糊背景比压黑更能表达"下面那层被暂时挂起"，
   又不会把背景压到看不出自己在哪个页面（误关弹窗后容易迷路）。 */
.scan-modal-backdrop {
  background: rgba(16, 44, 107, 0.28);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

/* 可悬停的浮层：抬起 + 阴影升一档。
   只在真有指针设备时启用 —— 触屏上 hover 会"黏"在最后一次点到的元素上。 */
@media (hover: hover) {
  .form-card:hover,
  .detail-card:hover,
  .info-box:hover,
  .panel:hover,
  .cascade-col:hover {
    box-shadow: var(--shadow-lg), var(--glass-sheen), 0 0 0 1px var(--glass-hairline);
  }
}

/* --------------------------------------------------------------------------
   27. 动效层：入场编排 / 滚动渐入 / 微交互反馈
   --------------------------------------------------------------------------
   三条纪律：

     1. **只动 transform 与 opacity。** 这两个属性由合成器处理，不触发重排重绘；
        动 width/margin/top 会让 10 列大表每帧重算布局，2 核机器上直接卡顿。
     2. **一次编排好的入场，胜过一堆零散微交互。** 页面加载时按阅读顺序
        错峰 60ms 依次浮现（共 6 组），比"所有元素同时淡入"更显精密，
        也比给每个元素单独做动画更容易维护。
     3. **全部动效在 prefers-reduced-motion 下关闭。** 文件末尾已有全局兜底，
        这里再对 .reveal 补一条 —— 全局兜底只压缩时长，压不掉 opacity:0 的初值。
   -------------------------------------------------------------------------- */

/* ---------- 页面入场编排：容器直接子元素按顺序浮现 ---------- */
@keyframes rise-in {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: no-preference) {
  .container > * {
    /* backwards 让延迟期间停在起点（透明），到点才开始动。
       没有它，元素会先以终态闪一下再跳回起点重播。 */
    animation: rise-in var(--duration-base) var(--ease-out) backwards;
  }

  /* 6 组，步长 60ms —— 能分辨出先后顺序的下限，再小就成了"同时出现" */
  .container > *:nth-child(2) { animation-delay: calc(var(--stagger) * 1); }
  .container > *:nth-child(3) { animation-delay: calc(var(--stagger) * 2); }
  .container > *:nth-child(4) { animation-delay: calc(var(--stagger) * 3); }
  .container > *:nth-child(5) { animation-delay: calc(var(--stagger) * 4); }
  .container > *:nth-child(6) { animation-delay: calc(var(--stagger) * 5); }
  /* 第 7 个起不再累加：延迟过了 300ms，用户已经以为"页面加载完了"，
     再等下去就变成"卡"。 */
}

/* ---------- 滚动渐入 ---------- */
/* 初值由 CSS 给、类名由 JS 加。
   这样即使 JS 完全没跑起来（内网老旧浏览器、脚本被拦），元素也只是"没有入场效果"，
   不会停在 opacity:0 变成一片空白 —— 动画只能锦上添花，不能成为可见性的开关。

   `animation: none` 是必须的：这类元素往往同时是 .container 的直接子元素，
   上面那条 .container > * 的入场动画会一直占着 opacity。用 animation:none 明确
   让位，避免"两套动画抢同一个属性"导致的闪烁。 */
.reveal {
  animation: none;
  opacity: 0;
  transform: translateY(12px);
}

/* 渐入用 animation 而不是 transition —— 这是实测改的，不是偏好：
   transition 的前提是"同一个属性存在前后两个可比较的计算值"。
   .reveal 是 JS 现加的类、.is-in 又紧随其后加上，浏览器很容易把两步
   合并进同一轮样式计算，于是只有"终值"、没有"起始值"，过渡压根不启动，
   元素就永久停在 opacity:0（探针实测就是这个结果）。
   animation 不依赖前后值对比，给个 backwards 就有确定的起点。 */
@keyframes reveal-in {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.reveal.is-in {
  /* 这两条是兜底：万一动画被拦（老浏览器 / 系统减弱动效），
     元素也必须落在可见状态，不能靠动画来决定"内容显不显示"。 */
  opacity: 1;
  transform: none;
  /* v1.3.16：duration-slow(280ms) → base(180ms)。滚动渐入发生在用户
     "正在找内容"的过程中，慢于 200ms 就会被读成"卡顿"；位移也收到
     12px（与首屏 rise-in 一致），长页面连续触发时不再觉得在抖。 */
  animation: reveal-in var(--duration-base) var(--ease-out) backwards;
}

/* ---------- 微交互：状态色徽标弹入 ---------- */
@keyframes badge-pop {
  0% {
    opacity: 0;
    transform: scale(0.72);
  }
  60% {
    transform: scale(1.06);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}

@media (prefers-reduced-motion: no-preference) {
  .badge {
    animation: badge-pop var(--duration-base) var(--ease-spring) backwards;
  }
}

/* ---------- 微交互：表格行定位条 ---------- */
/* 悬停时在行左缘长出一条 2px 主色条。
   比整行变色更精准：10 列横向扫读时，眼睛靠的是"行首那一点"来定位，
   底色变化在余光里几乎察觉不到。 */
.table tbody tr td:first-child {
  position: relative;
}

.table tbody tr td:first-child::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--brand-600);
  transform: scaleY(0);
  transition: transform var(--duration-fast) var(--ease-out);
}

.table tbody tr:hover td:first-child::before {
  transform: scaleY(1);
}

/* ---------- 呼吸感：渐变网格缓慢漂移 ---------- */
/* 90 秒一轮，位移不到 2%。刻意做得几乎察觉不到 ——
   目的是让静止的页面"活着"，而不是让人注意到背景在动。
   只动 transform，合成器层面完成，不参与布局。 */
@keyframes mesh-drift {
  from {
    transform: scale(1) translate3d(0, 0, 0);
  }
  to {
    transform: scale(1.08) translate3d(-1.5%, -1%, 0);
  }
}

@media (prefers-reduced-motion: no-preference) {
  body::before {
    animation: mesh-drift 90s var(--ease-in-out) infinite alternate;
  }
}

/* ---------- 降级：减弱动效下彻底关闭 ---------- */
@media (prefers-reduced-motion: reduce) {
  .reveal,
  .reveal.is-in {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

/* --------------------------------------------------------------------------
   28. 响应式
   桌面端为当前主战场，以下断点仅保证小屏不破版。
   -------------------------------------------------------------------------- */
@media (max-width: 992px) {
  .navbar {
    gap: var(--space-4);
    padding: 0 var(--space-4);
  }

  .nav-user .nav-user-name {
    display: none;
  }

  .container {
    padding: var(--space-4);
  }

  /* 级联工作台在窄屏纵向堆叠（左栏落在最上方）。同时取消栏内滚动 —— 
     嵌套滚动区域在触屏上很难准确操作，堆叠后交给页面滚动更自然。 */
  .cascade {
    grid-template-columns: 1fr;
    gap: var(--space-4);
  }

  .cascade-list {
    max-height: none;
    overflow-y: visible;
  }

  .cascade-actions {
    gap: var(--space-4);
  }
}

@media (max-width: 768px) {
  .nav-toggle {
    display: inline-flex;
  }

  /* 菜单折叠为纵向面板，由 nav-toggle 控制展开 */
  .nav-links {
    display: none;
    flex-basis: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-1);
    padding-bottom: var(--space-3);
  }

  .nav-links.is-open {
    display: flex;
  }

  .nav-links a {
    height: var(--touch-comfort);
    padding: 0 var(--space-3);
  }

  .nav-group {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-1);
  }

  /* 纵向排列后，分组改为上方分隔线 */
  .nav-group:not(:first-child) {
    margin-top: var(--space-2);
    margin-left: 0;
    padding-top: var(--space-2);
    padding-left: 0;
    border-top: 1px solid rgba(255, 255, 255, 0.24);
    border-left: none;
  }

  .stats-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .detail-row {
    flex-direction: column;
    gap: var(--space-1);
  }

  .detail-row .label {
    flex-basis: auto;
  }

  .doc-nav {
    flex: 1 1 100%;
    flex-direction: row;
    flex-wrap: wrap;
  }

  .doc-nav a {
    flex: 1;
    min-width: 120px;
    text-align: center;
  }

  .filter-bar {
    align-items: stretch;
  }

  .filter-item,
  .filter-item .form-control {
    width: 100%;
  }

  /* 审计筛选条在窄屏改为纵向堆叠，按钮不再靠右 */
  .filter-bar-audit .filter-item,
  .filter-item-wide {
    flex: 1 1 100%;
  }

  .filter-actions {
    width: 100%;
    margin-left: 0;
    justify-content: flex-start;
  }

  .trend-chart {
    height: 160px;
  }

  /* iOS Safari 在控件字号低于 16px 时，聚焦会强制放大整页 */
  .form-control {
    font-size: var(--text-base);
    min-height: var(--touch-comfort);
  }

  .btn {
    min-height: var(--touch-min);
  }

  /* 小屏降载：模糊半径砍半。
     手机 GPU 上全屏 backdrop-filter 是这套材质里唯一可能拖帧的一环，
     而小屏下玻璃面积本来就小，减到 10px 肉眼几乎无差别。 */
  .navbar,
  .form-card,
  .detail-card,
  .info-box,
  .panel,
  .scan-panel,
  .login-box,
  .cascade-col,
  .stat-card,
  .scan-modal-box,
  .table {
    --glass-blur: 10px;
  }

  /* 颗粒与网格漂移在手机上关掉：省电优先，静态纹理已经足够 */
  body::after {
    opacity: 0.03;
  }

  body::before {
    animation: none;
  }

  /* 入场编排在小屏取消错峰：手机首屏更短，逐条浮现反而像"加载慢" */
  .container > * {
    animation-delay: 0ms;
  }


  .row-actions a {
    display: inline-flex;
    align-items: center;
    min-height: var(--touch-min);
  }
}

@media (max-width: 480px) {
  .stats-grid {
    grid-template-columns: 1fr;
  }

  .panel {
    padding: var(--space-4);
  }

  /* x 轴日期标签在极窄屏会重叠，横轴改为可横向滚动 */
  .trend-chart {
    min-width: 560px;
  }

  .panel-grid {
    grid-template-columns: 1fr;
  }

  .todo-banner {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-3);
  }
}

/* 尊重系统的减弱动效设置 */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
