/* ==========================================================================
   一云 · Design System  (design-system.css)
   --------------------------------------------------------------------------
   全站「单一事实来源」：设计令牌 / 基础组件 / 动效关键帧 / Vue 过渡族 / 无障碍降级。

   基线：index.html（所有分歧一律以 index.html 为准）。
   引入顺序：design-system.css → theme.css（旧暗色兼容层）→ 页面内联 <style>。

   用法：所有页面在 <head> 里最早引入本文件，然后删除自己内联里重复的
        令牌块 / 组件块 / 关键帧块，避免两份定义打架。
   ========================================================================== */

/* glassfx（vendored @ /vendor/glassfx）—— 液态玻璃库：.glass 工具类提供
   边缘高光 rim / 光标跟随 bloom / Chromium 真实折射（@supports 兜底到 frost）。
   必须放在所有规则之前；本文件后续对 .card/.modal 等的定义会在源顺序上
   覆盖 .glass 的 background/border/box-shadow/radius，从而保留项目既有设计，
   只取它的 rim + bloom + 折射增强。 */
@import url("/vendor/glassfx/glass.css");

/* ==========================================================================
   1. Design Tokens —— 设计令牌
   ========================================================================== */
:root {
  /* 品牌色 */
  --primary: #0284c7;
  --primary-rgb: 2, 132, 199;
  --primary-light: #38bdf8;
  --primary-dark: #0369a1;
  --primary-surface: #e0f2fe;

  /* 语义色 */
  --c-danger: #e11d48;
  --c-danger-soft: rgba(225, 29, 72, 0.1);
  --c-warn: #f59e0b;
  --c-success: #10b981;
  --c-info: #0284c7;
  --c-danger-strong: #ef4444;
  --c-success-text: #15803d;

  /* 背景与玻璃层 */
  --bg-body: #f0f7ff;
  --bg-gradient: radial-gradient(at 8% 8%, #e0f2fe 0px, transparent 45%),
                 radial-gradient(at 92% 92%, #bae6fd 0px, transparent 48%),
                 #f0f7ff;
  --glass-surface: rgba(255, 255, 255, 0.84);
  --glass-elevated: rgba(255, 255, 255, 0.95);
  --glass-subtle: rgba(255, 255, 255, 0.65);
  --glass-border: rgba(255, 255, 255, 0.95);
  --glass-border-ice: rgba(186, 230, 253, 0.75);
  --glass-specular: rgba(255, 255, 255, 0.95);

  /* 文本 */
  --text-main: #0f172a;
  --text-sub: #334155;
  --text-muted: #64748b;

  /* 阴影 */
  --shadow-soft: 0 8px 24px -4px rgba(2, 132, 199, 0.08), 0 2px 8px -2px rgba(0, 0, 0, 0.03);
  --shadow-elevated: 0 16px 36px -6px rgba(2, 132, 199, 0.16), 0 4px 12px -2px rgba(0, 0, 0, 0.04);
  --glow-primary: 0 3px 10px rgba(var(--primary-rgb), 0.26);

  /* 圆角：只 5 档 */
  --r-xs: 8px;
  --r-sm: 12px;
  --r-md: 16px;
  --r-lg: 22px;
  --r-pill: 9999px;

  /* 间距：只用 4 的倍数 */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  --sp-6: 24px;

  /* 字号：6 档 */
  --fs-xs: 0.68rem;
  --fs-sm: 0.78rem;
  --fs-md: 0.88rem;
  --fs-lg: 1rem;
  --fs-xl: 1.15rem;
  --fs-2xl: 1.35rem;

  /* 控件高度 */
  --h-ctrl-sm: 30px;
  --h-ctrl: 36px;
  --h-ctrl-lg: 44px;

  /* ================================================
     动效 —— Apple 风格缓动体系
     ================================================ */
  --ease-apple: cubic-bezier(0.32, 0.72, 0, 1);     /* 页面级大转场（iOS sheet） */
  --ease-fluid: cubic-bezier(0.16, 1, 0.3, 1);      /* expo-out：元素入场 */
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* 回弹：开关 / 弹窗 */
  --ease-press: cubic-bezier(0.4, 0, 0.2, 1);       /* 按压反馈 */
  --dur-fast: 0.16s;
  --dur-base: 0.24s;
  --dur-slow: 0.34s;
  --dur-enter: 0.5s;
  --transition: all var(--dur-base) var(--ease-fluid);

  /* —— Apple 风统一玻璃 / 缓动令牌（重构基线）——
     saturate(1.8) == 180%；blur 统一 20px；移动端再降级到 14px。 */
  --glass-blur: 20px;
  --glass-saturate: 1.8;          /* = saturate(180%) */
  --glass-brightness: 1.05;       /* 供 glassfx .glass 使用 */
  --glass-rim: 0.5;
  --glass-sheen: 0.14;
  --glass-bloom: 255, 255, 255;
  --glass-bloom-size: 220px;
  --glass-radius: 24px;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);   /* Apple 落地/卡片入场统一缓动 */
  --r-xl: 28px;                                  /* 大面板/模态圆角上限（20–28px） */

  /* 节点状态过渡（识别延迟 → 平滑收敛） */
  --dur-resolve: 0.62s;

  /* 字体 */
  --font-sans: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI',
               Roboto, 'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB',
               'Microsoft YaHei', sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* ------------------------------------------------
     兼容别名：旧页面仍在使用的历史命名，统一指向新令牌。
     新代码请直接用左边的规范名。
     ------------------------------------------------ */
  --radius-xs: var(--r-xs);
  --radius-sm: var(--r-sm);
  --radius-md: var(--r-md);
  --radius-lg: var(--r-lg);
  --radius-pill: var(--r-pill);

  --primary-color: var(--primary);
  --primary-text: var(--primary-dark);
  --accent-color: var(--primary-surface);
  --accent: var(--primary-surface);
  --card-bg: var(--glass-surface);
  --surface-1: var(--glass-surface);
  --surface-2: var(--glass-elevated);
  --shadow: var(--shadow-soft);
  --shadow-hover: var(--shadow-elevated);
  --shadow-lift: var(--shadow-elevated);

  --success: var(--c-success);
  --warn: var(--c-warn);
  --danger: var(--c-danger);
}

html[data-theme="dark"] {
  --primary: #38bdf8;
  --primary-rgb: 56, 189, 248;
  --primary-light: #7dd3fc;
  --primary-dark: #0284c7;
  --primary-surface: rgba(2, 132, 199, 0.18);
  --bg-body: #070d19;
  --bg-gradient: radial-gradient(at 10% 10%, rgba(2, 132, 199, 0.18) 0px, transparent 50%),
                 radial-gradient(at 90% 90%, rgba(14, 165, 233, 0.12) 0px, transparent 50%),
                 #070d19;
  --glass-surface: rgba(15, 27, 49, 0.85);
  --glass-elevated: rgba(20, 36, 66, 0.95);
  --glass-subtle: rgba(15, 27, 49, 0.6);
  --glass-border: rgba(255, 255, 255, 0.14);
  --glass-border-ice: rgba(56, 189, 248, 0.25);
  --glass-specular: rgba(255, 255, 255, 0.2);
  --text-main: #f8fafc;
  --text-sub: #cbd5e1;
  --text-muted: #94a3b8;
  --c-success-text: #4ade80;
  --shadow-soft: 0 10px 30px -6px rgba(0, 0, 0, 0.55);
  --shadow-elevated: 0 18px 45px -6px rgba(0, 0, 0, 0.7);
}

html[data-theme="light"] { color-scheme: light; }
html[data-theme="dark"]  { color-scheme: dark; }

/* ==========================================================================
   2. Reset & Base —— 只放全站一致、不会改变既有排版的部分
      （刻意不做 margin/padding 全局清零：preview 等富文本页依赖浏览器默认间距）
   ========================================================================== */
[v-cloak] { display: none !important; }

*, *::before, *::after { box-sizing: border-box; }

body {
  background: var(--bg-gradient);
  background-attachment: fixed;
  color: var(--text-main);
  font-family: var(--font-sans);
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
  transition: background-color 0.55s var(--ease-apple), color 0.55s var(--ease-apple);
}

/* ==========================================================================
   3. 通用卡片
   ========================================================================== */
/* .liquid-card 是 .card 的历史别名（admin/gallery/paste 曾用），合并为同一实现 */
.card,
.liquid-card {
  background: var(--glass-surface);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  border: 1px solid var(--glass-border);
  box-shadow: var(--shadow-soft);
  border-radius: var(--r-lg);
  transition: background-color 0.45s var(--ease-apple),
              border-color 0.45s var(--ease-apple),
              box-shadow 0.45s var(--ease-apple),
              transform 0.45s var(--ease-out);
  position: relative;
}
.card::before,
.liquid-card::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--glass-specular), transparent);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  pointer-events: none;
}
.card--inset { border-radius: var(--r-md); box-shadow: none; }
.card--inset::before { display: none; }
.card__header,
.liquid-card__header {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-2); padding: var(--sp-4);
}
.card__body,
.liquid-card__body { padding: 0 var(--sp-4) var(--sp-4); }

/* ==========================================================================
   4. 通用按钮：统一 BEM 修饰符（--primary / --danger / --ghost / --icon / --sm / --block）
      形态：实心主按钮 = --primary，危险 = --danger，次要/描边 = 默认，无框 = --ghost
   ========================================================================== */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  height: var(--h-ctrl);
  padding: 0 var(--sp-4);
  border-radius: var(--r-pill);
  border: 1px solid var(--glass-border);
  background: var(--glass-surface);
  color: var(--text-sub);
  font: inherit;
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
  letter-spacing: -0.005em;
  cursor: pointer;
  white-space: nowrap;
  text-decoration: none;
  transition: var(--transition);
  -webkit-tap-highlight-color: transparent;
  will-change: transform;
}
.btn i { font-size: 0.76rem; }
.btn:hover:not(:disabled) {
  color: var(--primary);
  border-color: rgba(var(--primary-rgb), 0.42);
  background: rgba(var(--primary-rgb), 0.06);
  transform: translateY(-1px);
}
.btn:active:not(:disabled) {
  transform: scale(0.955);
  transition-duration: 0.09s;
  transition-timing-function: var(--ease-press);
}
.btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none; }

.btn--primary {
  border-color: transparent;
  background: linear-gradient(135deg, var(--primary), var(--primary-dark));
  color: #fff;
  box-shadow: var(--glow-primary);
}
.btn--primary:hover:not(:disabled) { color: #fff; box-shadow: 0 5px 14px rgba(var(--primary-rgb), 0.36); }
.btn--primary:disabled { background: rgba(100, 116, 139, 0.16); color: var(--text-muted); box-shadow: none; }

.btn--danger { color: var(--c-danger); border-color: rgba(244, 63, 94, 0.3); }
.btn--danger:hover:not(:disabled) {
  color: var(--c-danger);
  background: var(--c-danger-soft);
  border-color: rgba(244, 63, 94, 0.5);
}

.btn--ghost { border-color: transparent; background: transparent; }
.btn--ghost:hover:not(:disabled) { background: rgba(var(--primary-rgb), 0.1); }

.btn--icon { width: var(--h-ctrl); padding: 0; border-radius: 50%; }
.btn--icon.btn--sm { width: var(--h-ctrl-sm); }
.btn--sm { height: var(--h-ctrl-sm); font-size: var(--fs-xs); padding: 0 var(--sp-3); }
.btn--sm i { font-size: var(--fs-xs); }
.btn--lg { height: var(--h-ctrl-lg); font-size: var(--fs-md); padding: 0 var(--sp-5); }
.btn--block { width: 100%; }

/* 文字按钮：无底、无边框，用于导航栏等轻量场景 */
.text-btn {
  border: none; background: none; padding: var(--sp-1) var(--sp-1);
  font: inherit; font-size: var(--fs-md); font-weight: 590;
  letter-spacing: -0.005em; color: var(--primary);
  cursor: pointer; white-space: nowrap;
  transition: opacity var(--dur-fast), transform 0.1s var(--ease-press), color var(--dur-fast);
}
.text-btn:active { opacity: 0.45; transform: scale(0.94); }
.text-btn--strong { font-weight: 700; }
.text-btn--danger { color: var(--c-danger); }
.text-btn--danger.armed {
  color: #fff; background: var(--c-danger);
  border-radius: var(--r-xs); padding: var(--sp-1) var(--sp-3);
  animation: pop 0.28s var(--ease-apple);
}

/* 兼容别名：旧页面用的单连字符写法 / 无修饰符写法，统一指向上面的实现。
   新代码请一律用 .btn--primary 这类双连字符写法。 */
.btn-primary,
.btn.primary {
  border-color: transparent;
  background: linear-gradient(135deg, var(--primary), var(--primary-dark));
  color: #fff;
  box-shadow: var(--glow-primary);
}
.btn-primary:hover:not(:disabled),
.btn.primary:hover:not(:disabled) {
  color: #fff;
  transform: translateY(-1px);
  box-shadow: 0 5px 14px rgba(var(--primary-rgb), 0.36);
}
.btn-primary:disabled,
.btn.primary:disabled { background: rgba(100, 116, 139, 0.16); color: var(--text-muted); box-shadow: none; }

.btn-danger, .btn.danger { color: var(--c-danger); border-color: rgba(244, 63, 94, 0.3); }
.btn-danger:hover:not(:disabled),
.btn.danger:hover:not(:disabled) {
  color: var(--c-danger);
  background: var(--c-danger-soft);
  border-color: rgba(244, 63, 94, 0.5);
}

.btn--ghost, .btn.ghost { border-color: transparent; background: transparent; }
.btn--ghost:hover:not(:disabled),
.btn.ghost:hover:not(:disabled) { background: rgba(var(--primary-rgb), 0.1); }

.btn-block, .btn.block { width: 100%; }

/* 危险操作的确认按钮：实心红，强化阻断感（以 index.html 的实现为基准，
   admin.html 原先用 #ef4444→#b91c1c 的渐变，已统一到语义色 --c-danger） */
.btn--danger-solid {
  background: linear-gradient(135deg, var(--c-danger), #be123c);
  color: #fff;
  border-color: transparent;
  box-shadow: 0 4px 14px rgba(225, 29, 72, 0.32);
}
.btn--danger-solid:hover:not(:disabled) {
  background: linear-gradient(135deg, #f43f5e, #e11d48);
  color: #fff;
  border-color: transparent;
  transform: translateY(-1px);
  box-shadow: 0 6px 18px rgba(225, 29, 72, 0.4);
}
.btn--danger-solid:active:not(:disabled) {
  transform: scale(0.96);
  transition-duration: 0.09s;
  transition-timing-function: var(--ease-press);
}

/* ==========================================================================
   5. 表单控件
   ========================================================================== */
.input, .select, .textarea {
  width: 100%;
  height: var(--h-ctrl);
  padding: 0 var(--sp-3);
  border-radius: var(--r-sm);
  border: 1px solid var(--glass-border-ice);
  background: var(--glass-subtle);
  color: var(--text-main);
  font: inherit;
  font-size: var(--fs-md);
  transition: border-color var(--dur-base) var(--ease-apple),
              box-shadow var(--dur-base) var(--ease-apple),
              background var(--dur-base) var(--ease-apple);
}
.textarea { height: auto; min-height: 88px; padding: var(--sp-2) var(--sp-3); line-height: 1.6; resize: vertical; }
.input:focus, .select:focus, .textarea:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.12);
}
.input::placeholder, .textarea::placeholder { color: var(--text-muted); }
.input:disabled, .select:disabled, .textarea:disabled { opacity: 0.5; cursor: not-allowed; }

.field { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.field > label { font-size: var(--fs-xs); font-weight: 700; color: var(--text-sub); }
.field__hint,
.field-hint,
.field-help { font-size: 0.7rem; color: var(--text-muted); line-height: 1.5; }

/* ==========================================================================
   6. 开关 —— 全站唯一实现（44×26，回弹 + 按压拉伸）
   ========================================================================== */
.switch {
  position: relative;
  width: 44px; height: 26px;
  flex: none;
  cursor: pointer;
  display: inline-block;
  -webkit-tap-highlight-color: transparent;
}
.switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.switch__track {
  position: absolute; inset: 0;
  border-radius: 13px;
  background: rgba(148, 163, 184, 0.38);
  transition: background 0.34s var(--ease-apple);
}
html[data-theme="dark"] .switch__track { background: rgba(148, 163, 184, 0.28); }
.switch__thumb {
  position: absolute; top: 3px; left: 3px;
  width: 20px; height: 20px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.24);
  transition: transform 0.38s var(--ease-spring), width 0.2s var(--ease-press);
}
.switch:active .switch__thumb { width: 24px; }
.switch input:checked + .switch__track { background: var(--primary); }
.switch input:checked + .switch__track + .switch__thumb { transform: translateX(18px); }
.switch input:checked:active + .switch__track + .switch__thumb { transform: translateX(14px); }
.switch input:focus-visible + .switch__track { box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.25); }

/* ==========================================================================
   7. 加载指示器 —— 全站唯一实现（34px，--sm 20px，--lg 44px）
   ========================================================================== */
.spinner {
  width: 34px; height: 34px;
  border-radius: 50%;
  border: 3px solid rgba(var(--primary-rgb), 0.18);
  border-top-color: var(--primary);
  animation: spin 0.8s linear infinite;
}
.spinner--sm { width: 20px; height: 20px; border-width: 2px; }
.spinner--lg { width: 44px; height: 44px; animation-duration: 0.9s; }
.spinner--on-primary {
  border-color: rgba(255, 255, 255, 0.3);
  border-top-color: #fff;
}
/* 兼容别名 */
.spinner-ring { width: 34px; height: 34px; border-radius: 50%; border: 3px solid rgba(var(--primary-rgb), 0.18); border-top-color: var(--primary); animation: spin 0.8s linear infinite; margin: 0 auto; }

/* ==========================================================================
   8. Toast —— 全站唯一实现（顶部居中、胶囊、玻璃拟态）
      .toast-stack 是容器（多条时纵向堆叠），.toast 是单条。
   ========================================================================== */
.toast-stack {
  position: fixed;
  top: calc(20px + env(safe-area-inset-top, 0px));
  left: 50%;
  transform: translateX(-50%);
  z-index: 9999;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  pointer-events: none;
  width: max-content;
  max-width: calc(100vw - 32px);
}
.toast {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 9px var(--sp-5);
  border-radius: var(--r-pill);
  background: var(--glass-elevated);
  border: 1.5px solid var(--glass-border-ice);
  box-shadow: var(--shadow-elevated);
  font-size: var(--fs-md);
  font-weight: 600;
  line-height: 1.4;
  max-width: 100%;
  /* 独立合成层：toast 是 position:fixed 容器里的浮层，且带 box-shadow，
     低端设备上若不提前提层，每帧都要重新 raster，掉帧时肉眼可见抖动。
     这里主动 translateZ(0) + will-change，把绘制成本前移，属于「提质量」而非降质量。
     注意：入场动画只由下面的 Vue 过渡族负责，此处**不再**挂 animation —— 两套
     动画争同一个 transform/opacity 会让元素在掉帧时来回跳（历史 bug，见文件末注释）。 */
  transform: translateZ(0);
  backface-visibility: hidden;
  will-change: transform, opacity;
}
.toast i { font-size: 0.9rem; flex: none; }

/* 语义色（同时支持 .toast--success 与旧写法 .toast.success） */
.toast--success, .toast.success { color: var(--primary-dark); border-color: rgba(var(--primary-rgb), 0.4); }
.toast--success i, .toast.success i { color: var(--primary); }

.toast--error, .toast.error { color: #dc2626; border-color: rgba(239, 68, 68, 0.4); }
.toast--error i, .toast.error i { color: #ef4444; }

.toast--warn, .toast.warn { color: #b45309; border-color: rgba(245, 158, 11, 0.45); }
.toast--warn i, .toast.warn i { color: var(--c-warn); }

.toast--info, .toast.info { color: var(--primary-dark); border-color: rgba(var(--primary-rgb), 0.35); }
.toast--info i, .toast.info i { color: var(--primary); }

html[data-theme="dark"] .toast--error,
html[data-theme="dark"] .toast.error { color: #fca5a5; }
html[data-theme="dark"] .toast--success,
html[data-theme="dark"] .toast.success { color: var(--primary-light); }
html[data-theme="dark"] .toast--warn,
html[data-theme="dark"] .toast.warn { color: #fbbf24; }
html[data-theme="dark"] .toast--info,
html[data-theme="dark"] .toast.info { color: var(--primary-light); }

@media (max-width: 680px) {
  .toast { padding: 9px var(--sp-4); font-size: var(--fs-sm); }
  /* 移动端分级降模糊：20px → 14px、saturate 略降；不关模糊。
     真正低端机仍走 html[data-perf="low"] 的 !important 全关规则。 */
  :root {
    --glass-blur: 14px;
    --glass-saturate: 1.5;
  }
}

/* ==========================================================================
   9. 空状态
   ========================================================================== */
.empty-state {
  padding: 56px var(--sp-5);
  text-align: center;
  color: var(--text-muted);
  font-size: var(--fs-sm);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
}
.empty-state i {
  font-size: 2.4rem;
  opacity: 0.35;
  animation: floatSoft 3.4s var(--ease-apple) infinite alternate;
}
.empty-state .btn { margin-top: 2px; }
.empty-state--sm { padding: var(--sp-6) var(--sp-4); }
.empty-state--sm i { font-size: 1.7rem; }
/* 兼容别名 */
.result-empty {
  padding: var(--sp-6) var(--sp-4);
  text-align: center;
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

/* ==========================================================================
   10. 选择圈 / 进度条
   ========================================================================== */
.check {
  position: relative;
  flex: none;
  width: 22px; height: 22px;
  border-radius: 50%;
  border: 1.5px solid rgba(100, 116, 139, 0.45);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 0.6rem;
  color: transparent;
  transition: background var(--dur-base) var(--ease-spring),
              border-color var(--dur-base) var(--ease-spring),
              color var(--dur-base) var(--ease-spring),
              transform var(--dur-base) var(--ease-spring);
}
/* 44px 隐形热区：移动端更好点 */
.check::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: 44px; height: 44px;
  transform: translate(-50%, -50%);
}
.check i { transform: scale(0.5); transition: transform var(--dur-base) var(--ease-spring); pointer-events: none; }
.check.on { background: var(--primary); border-color: var(--primary); color: #fff; }
.check.on i { transform: scale(1); animation: pop 0.32s var(--ease-spring); }
.check:hover { border-color: var(--primary); }
.check:active { transform: scale(0.9); }
.check input { position: absolute; opacity: 0; width: 0; height: 0; margin: 0; pointer-events: none; }

.progress {
  height: 4px;
  border-radius: var(--r-pill);
  background: rgba(148, 163, 184, 0.2);
  margin-top: 3px;
  overflow: hidden;
}
.progress__val {
  height: 100%; width: 100%;
  transform-origin: left center;
  transform: scaleX(0);
  border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--primary-light), var(--primary));
  transition: transform 0.4s var(--ease-fluid), background 0.3s var(--ease-apple);
  will-change: transform;
}
.progress--lg { height: 8px; margin-top: var(--sp-2); }

/* ==========================================================================
   10.5 同步状态点 —— 全站唯一实现（.sync-dot）
   ---------------------------------------------------------------------------
   语义（四态，与「存储连接状态」的 .status-pulse 严格区分）：
     is-idle     灰   — 尚无同步动作（初始态，不呼吸）
     is-syncing  黄   — 同步中（向外扩散的警示环）
     is-synced   绿   — 已同步（向外扩散的确认环，短暂停留后自然回到 idle）
     is-failed   红   — 同步失败（重试耗尽；只做明暗呼吸，不扩散）

   为什么必须放在共享层、而不是各页各写一份：
     本组件在 admin.html 与 index.html 都要用，且外观必须一致。
     一旦两页各写一份，就会重演历史上「改一页、另一页漂移」的老问题
     （见 check_shared.py 的 [C]/[D] 检查）。

   宿主：任意 inline-flex 容器即可。它是「纯展示 + 可点击」的原子组件，
   状态机与重试逻辑在 app-core.js 的 KVault.createSyncTracker()，
   CSS 只负责把状态映射成颜色与动画。 */
.sync-dot {
  width: 9px; height: 9px;
  border-radius: 50%;
  flex: none;
  display: inline-block;
  background: var(--text-muted);
  box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
  transition: background var(--dur-base) var(--ease-apple),
              box-shadow var(--dur-base) var(--ease-apple);
}
.sync-dot.is-syncing {
  background: var(--c-warn);
  animation: syncPulseWarn 1.5s var(--ease-apple) infinite;
}
.sync-dot.is-synced {
  background: var(--c-success);
  animation: syncPulseOk 2.2s var(--ease-apple) infinite;
}
.sync-dot.is-failed {
  background: var(--c-danger);
  animation: syncAlarmBreath 1.6s var(--ease-apple) infinite;
}

/* 可点击态：把圆点扩成一个有热区的按钮，避免 9px 难以点中。
   注意热区靠 padding 撑开、视觉尺寸不变（内层仍是 .sync-dot）。 */
.sync-dot-btn {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 6px; margin: -6px;      /* 负 margin 抵消，视觉占位不变 */
  border: 0; background: none; cursor: pointer;
  border-radius: var(--r-pill);
  line-height: 0;
  transition: background var(--dur-base) var(--ease-apple),
              opacity 0.25s var(--ease-apple);
}
.sync-dot-btn:hover { background: var(--glass-subtle); }
.sync-dot-btn:active { transform: scale(0.92); transition-duration: 0.09s; }
.sync-dot-btn:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

/* idle（无同步动作）不显示：灰点只会让人疑惑「这是出问题了吗」。
   只有黄/绿/红三种有含义的状态才出现。用 opacity + visibility 隐藏
   （而非 display:none），占位保留，出现/消失时旁边的元素不会跳动。 */
.sync-dot-btn.is-idle {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.25s var(--ease-apple), visibility 0s linear 0.25s;
}

/* ==========================================================================
   11. 关键帧 —— Apple 风格（全站共用，禁止页面再单独定义同名关键帧）
   ========================================================================== */
@keyframes riseIn {
  from { opacity: 0; transform: translate3d(0, 16px, 0) scale(0.985); }
  to   { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
}
@keyframes riseInSm {
  from { opacity: 0; transform: translate3d(0, 10px, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}
@keyframes scaleIn {
  from { opacity: 0; transform: scale(0.92) translate3d(0, 10px, 0); }
  to   { opacity: 1; transform: scale(1) translate3d(0, 0, 0); }
}
/* sheetIn / dialogIn 两个关键帧已删除：它们原先挂在 .modal--sheet /
   .modal--dialog / .modal--preview 上，与 <transition name="sheet|dialog"> 的
   过渡争同一轨道（低端设备闪烁的根因）。其起点与节奏已完整并入下方的
   .sheet-enter-* / .dialog-enter-* 过渡，观感不变。
   maskIn 仍被 gallery.html 使用，保留。 */
@keyframes maskIn { from { opacity: 0; } to { opacity: 1; } }

/* 上下文菜单：用 keyframes 而非 class 互换 —— 菜单是「插入 DOM + 切 class」
   同帧发生，浏览器来不及建立起始状态，过渡会被整帧跳过。 */
@keyframes menuIn {
  from { opacity: 0; transform: scale(0.86) translate3d(0, -8px, 0); }
  to   { opacity: 1; transform: scale(1) translate3d(0, 0, 0); }
}
@keyframes menuOut {
  from { opacity: 1; transform: scale(1) translate3d(0, 0, 0); }
  to   { opacity: 0; transform: scale(0.9) translate3d(0, -6px, 0); }
}
/* 行内操作按钮（复制/更多）：进出都要有动画，避免「直接没掉」 */
@keyframes actionIn {
  from { opacity: 0; transform: translate3d(8px, 0, 0) scale(0.9); }
  to   { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
}
@keyframes actionOut {
  from { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
  to   { opacity: 0; transform: translate3d(8px, 0, 0) scale(0.9); }
}

@keyframes panelIn {
  from { opacity: 0; transform: translate3d(0, 18px, 0) scale(0.94); }
  to   { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
}
@keyframes dockIn {
  from { opacity: 0; transform: translate3d(-50%, 96px, 0) scale(0.88); }
  to   { opacity: 1; transform: translate3d(-50%, 0, 0) scale(1); }
}
/* 底部批量操作栏（<transition name="bar">）的进出场。
   这里刻意用 @keyframes 而不是 transition，理由与上方 menuIn/menuOut 相同：
   底栏是「插入 DOM + 同帧切 class」出现的，浏览器来不及为 transition 建立
   起始值 —— 实测在「选中后立刻取消」时，进场两帧都停在 opacity:0，
   离场于是从 0 起算，表现为底栏瞬间消失（无任何可见过渡）。
   keyframes 把两端写死在动画里，不依赖样式是否被提交过，因此不受该竞态影响。
   translateX(-50%) 是底栏自身的水平居中偏移，两端都必须带上，否则进出场会横向跳位。 */
@keyframes barIn {
  from { opacity: 0; transform: translateX(-50%) translate3d(0, 18px, 0) scale(0.98); }
  to   { opacity: 1; transform: translateX(-50%) translate3d(0, 0, 0) scale(1); }
}
@keyframes barOut {
  from { opacity: 1; transform: translateX(-50%) translate3d(0, 0, 0) scale(1); }
  to   { opacity: 0; transform: translateX(-50%) translate3d(0, 18px, 0) scale(0.98); }
}
@keyframes pop {
  from { transform: scale(0.72); opacity: 0; }
  to   { transform: scale(1);    opacity: 1; }
}
/* 文件名/标题入场：随整行错峰上浮淡入（克制、不抢戏）。
   backwards 填充：动画结束后回落到无 transform 常态，不长期占用合成层。 */
@keyframes titleIn {
  from { opacity: 0; transform: translate3d(0, 6px, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}
@keyframes shakeIn {
  0%   { opacity: 0; transform: translateX(0); }
  20%  { opacity: 1; transform: translateX(-7px); }
  40%  { transform: translateX(6px); }
  60%  { transform: translateX(-4px); }
  80%  { transform: translateX(2px); }
  100% { opacity: 1; transform: translateX(0); }
}
@keyframes pulseRing {
  0%   { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.5); }
  70%  { box-shadow: 0 0 0 7px rgba(16, 185, 129, 0); }
  100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
}
@keyframes orbFloat {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(28px, -34px, 0) scale(1.08); }
}
@keyframes floatSoft {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(0, -8px, 0); }
}
@keyframes bounceDown {
  0%, 100% { transform: translate3d(0, 0, 0); }
  50%      { transform: translate3d(0, 5px, 0); }
}
@keyframes settleBreathe {
  0%   { transform: scale(1);    opacity: 1; }
  45%  { transform: scale(0.94); opacity: 0.72; }
  100% { transform: scale(1);    opacity: 1; }
}
@keyframes spin { to { transform: rotate(360deg); } }

/* 高度类展开/收起（grid-template-rows 真插值，配合 .tree-children / .row-expand） */
@keyframes treeGrowIn  { from { grid-template-rows: 0fr; } to { grid-template-rows: 1fr; } }
@keyframes treeGrowOut { from { grid-template-rows: 1fr; } to { grid-template-rows: 0fr; } }
@keyframes rowExpandIn  { from { grid-template-rows: 0fr; } to { grid-template-rows: 1fr; } }
@keyframes rowExpandOut { from { grid-template-rows: 1fr; } to { grid-template-rows: 0fr; } }

/* 通用容器展开/收起（grid-template-rows 真插值，进出场都补齐，配 <transition name="collapse">）。
   用于「出现/消失会带动容器高度变化」的面板（如 index 的交付结果），让「缩小」也走非线性缓动，
   而非瞬间塌缩。结构约定（务必加一层无样式的 .collapse__inner 包裹，否则带 padding 的卡片
   无法真正收到 0，会卡在内容最小高度）：
     <transition name="collapse">
       <div class="collapse" v-if="...">
         <div class="collapse__inner"><section class="card panel">…</section></div>
       </div>
     </transition>
   .collapse 为单格 grid，唯一子元素（.collapse__inner）在动画期间被 overflow:hidden 裁切，
   grid-template-rows 在 0fr↔1fr 间插值。裁切仅限动画进行中（enter/leave-active 类随动画
   起止由 Vue 自动加减）——静止时必须放开溢出，否则面板内的上拉菜单等溢出弹层会被裁掉。 */
@keyframes collapseIn  { from { grid-template-rows: 0fr; } to { grid-template-rows: 1fr; } }
@keyframes collapseOut { from { grid-template-rows: 1fr; } to { grid-template-rows: 0fr; } }
.collapse { display: grid; grid-template-rows: 1fr; }
.collapse > * { min-height: 0; min-width: 0; }
.collapse-enter-active > *,
.collapse-leave-active > * { overflow: hidden; }
.collapse-enter-active { animation: collapseIn 0.5s var(--ease-apple) both; }
.collapse-leave-active { animation: collapseOut 0.44s var(--ease-apple) both; }

/* —— 面板外阴影在展开/收起期间的「抽搐」 ——
   上面那条 overflow:hidden 是收到 0 高度的必要条件，但它同时会裁掉面板的
   **外**阴影（.card 的 --shadow-soft 扩散约 24px）。实测时序：
       t=7ms   高度 0    overflow=hidden   阴影被裁
       t=523ms 高度 134  overflow=visible  阴影瞬间蹦出
   也就是说阴影被整整藏了 0.5s，然后在最后一帧硬切出来，没有任何过渡 ——
   这就是用户报的那下「抽搐」。收起时反向同理（visible→hidden，阴影突然消失）。
   解法：动画期间干脆不画外阴影，动画结束后由 .card / .liquid-card 自带的
   transition: box-shadow 0.45s 平滑补上。这样「阴影出现」变成一段渐入，
   而不是一次硬跳。inset 阴影不溢出、不受裁切影响，可以保留。
   注：别想着用 overflow:clip + overflow-clip-margin 给阴影留边 ——
   高度收到 0 时那圈 margin 会让内容残留几十像素，collapse 就收不干净了。 */
.collapse-enter-active > * > .card,
.collapse-leave-active > * > .card,
.collapse-enter-active > * > .liquid-card,
.collapse-leave-active > * > .liquid-card { box-shadow: none; }

/* 数字/计数切换 */
@keyframes countIn  { from { opacity: 0; transform: scale(0.6) translate3d(0, -6px, 0); } to { opacity: 1; transform: scale(1) translate3d(0, 0, 0); } }
@keyframes countOut { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.6) translate3d(0, 6px, 0); } }

/* 上传队列整卡进出 */
@keyframes taskIn  { from { opacity: 0; transform: translate3d(0, 12px, 0) scale(0.98); } to { opacity: 1; transform: translate3d(0, 0, 0) scale(1); } }
@keyframes taskOut { from { opacity: 1; transform: translate3d(0, 0, 0) scale(1); } to { opacity: 0; transform: translate3d(0, -10px, 0) scale(0.98); } }

/* 页签内容错峰 */
@keyframes dropIn {
  from { opacity: 0; transform: translate3d(0, -12px, 0) scale(0.97); }
  to   { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
}
@keyframes paneItemIn { from { opacity: 0; transform: translate3d(0, 8px, 0); } to { opacity: 1; transform: translate3d(0, 0, 0); } }

/* 兼容别名：旧页面用过的名字，统一在这里收口（新代码请用上面的规范名）。
   dockRise 保留独立取值：画廊页的 dock 只上浮 26px，
   而全站 dockIn 是 96px 的「从屏幕外升起」，两者语义不同。
   原先还留着 fadeInUp / fadeUp / slideDown 三个别名，随各页面迁移到
   riseIn / riseInSm 后已无任何引用，故删除（check_tokens.py 会拦这类死定义）。 */
@keyframes fadeIn    { from { opacity: 0; } to { opacity: 1; } }

/* 同步状态点（.sync-dot）——三态各自的呼吸/告警节奏
   与 pulseRing 的区别：pulseRing 是「已连接」的固定绿环（存储连接状态），
   这里的三条是「数据同步」的过程状态，颜色随态变化，故不能复用。 */
@keyframes syncPulseWarn {
  0%   { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.55); }
  70%  { box-shadow: 0 0 0 6px rgba(245, 158, 11, 0); }
  100% { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0); }
}
@keyframes syncPulseOk {
  0%   { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.5); }
  70%  { box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); }
  100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
}
/* 失败：不扩散，只做轻微明暗呼吸 —— 与「进行中」的扩散状形成明确的语义区分 */
@keyframes syncAlarmBreath {
  0%, 100% { opacity: 1;    box-shadow: 0 0 0 0 rgba(225, 29, 72, 0.45); }
  50%      { opacity: 0.72; box-shadow: 0 0 0 3px rgba(225, 29, 72, 0); }
}

/* ==========================================================================
   12. 入场编排 —— 首屏元素错峰浮现（替代各页面手写的 boot 规则）
       .boot-rise / .boot-rise-sm / .boot-scale + --boot-delay 自定义延迟
   ========================================================================== */
.boot-rise    { animation: riseIn 0.52s var(--ease-fluid) backwards; }
.boot-rise-sm { animation: riseInSm 0.44s var(--ease-fluid) backwards; }
.boot-scale   { animation: scaleIn 0.46s var(--ease-spring) backwards; }
.boot-dock    { animation: dockIn 0.72s var(--ease-fluid) 0.1s backwards; }
[class*="boot-"] { animation-delay: var(--boot-delay, 0s); }

/* ==========================================================================
   13. Vue 过渡族 —— 进出场都补齐，禁止页面再单独定义同名过渡
   ========================================================================== */
.fade-enter-active { transition: opacity 0.24s var(--ease-apple); }
.fade-leave-active { transition: opacity 0.18s var(--ease-press); }
.fade-enter-from,
.fade-leave-to     { opacity: 0; }

.pop-enter-active { transition: opacity 0.2s var(--ease-apple), transform 0.26s var(--ease-spring); }
.pop-leave-active { transition: opacity 0.14s var(--ease-press), transform 0.18s var(--ease-press); }
.pop-enter-from,
.pop-leave-to     { opacity: 0; transform: scale(0.92) translateY(-6px); }

.rise-enter-active { transition: opacity 0.26s var(--ease-apple), transform 0.34s var(--ease-fluid); }
.rise-leave-active { transition: opacity 0.18s var(--ease-press), transform 0.24s var(--ease-press); }
.rise-enter-from,
.rise-leave-to     { opacity: 0; transform: translate3d(0, 14px, 0); }

.drop-enter-active { transition: opacity 0.26s var(--ease-apple), transform 0.34s var(--ease-fluid); }
.drop-leave-active { transition: opacity 0.16s var(--ease-press), transform 0.2s var(--ease-press); }
.drop-enter-from,
.drop-leave-to     { opacity: 0; transform: translate3d(0, -14px, 0); }

/* 底部批量操作栏：见 barIn / barOut 关键帧处的说明。
   opacity 置 1 是为了让 class 交换不参与插值 —— 真正的起止值由 keyframes 提供，
   与 .menu-enter-from / .menu-leave-to 同一套做法。 */
.bar-enter-active { animation: barIn 0.34s var(--ease-spring) both; }
.bar-leave-active { animation: barOut 0.2s var(--ease-press) both; }
.bar-enter-from,
.bar-leave-to     { opacity: 1; }

/* 控件组互换（选择/清空 ↔ 全选/完成）：错峰淡入淡出，避免「啪」地互替 */
.ctrl-enter-active { transition: opacity 0.2s var(--ease-apple), transform 0.28s var(--ease-spring); }
.ctrl-leave-active { transition: opacity 0.14s var(--ease-press), transform 0.18s var(--ease-press); }
.ctrl-enter-from   { opacity: 0; transform: translate3d(0, 6px, 0) scale(0.96); }
.ctrl-leave-to     { opacity: 0; transform: translate3d(0, -4px, 0) scale(0.98); }

/* Toast：单条（顶部居中，容器自身带 translateX(-50%)）。
   这是 toast 入场动画的**唯一**实现 —— 元素上不再挂 @keyframes，
   两套动画争同一轨道会在掉帧时闪烁。起点/时长/缓动与原先的 toastIn 完全一致，
   观感不变；额外补上 translateZ(0) 保持既有的独立合成层，避免过渡期间被降级。 */
.toast-enter-active { transition: opacity 0.26s var(--ease-apple), transform 0.42s var(--ease-spring); }
.toast-leave-active { transition: opacity 0.16s var(--ease-press), transform 0.2s var(--ease-press); }
.toast-enter-from   { opacity: 0; transform: translate3d(0, -22px, 0) scale(0.9) translateZ(0); }
.toast-leave-to     { opacity: 0; transform: translate3d(0, -14px, 0) scale(0.94) translateZ(0); }

/* 列表项（transition-group）：离场必须脱离文档流，否则剩余项要等它淡完才「跳」上来 */
.list { position: relative; }
.list-enter-active { animation: taskIn 0.28s var(--ease-fluid) both; }
.list-leave-active {
  animation: taskOut 0.18s var(--ease-press) both;
  position: absolute; left: 0; right: 0;
  width: 100%; pointer-events: none;
}
.list-move { transition: transform 0.32s var(--ease-apple); }

/* 上下文菜单 */
.menu-enter-active { animation: menuIn 0.3s var(--ease-spring) both; transform-origin: inherit; }
.menu-leave-active { animation: menuOut 0.18s var(--ease-press) both; transform-origin: inherit; }
.menu-enter-from,
.menu-leave-to { opacity: 1; }

/* 行内操作按钮：离场必须绝对定位，否则离场按钮仍占流宽 → 文件名被挤（移动端「抽搐」） */
.rowAct-enter-active { animation: actionIn 0.32s var(--ease-spring) both; }
.rowAct-leave-active {
  position: absolute; top: 0; bottom: 0; right: var(--sp-4);
  display: flex; align-items: center;
  animation: actionOut 0.18s var(--ease-press) both;
  pointer-events: none;
}
.taskAct { position: relative; display: flex; align-items: center; justify-content: flex-end; gap: var(--sp-1); }
.taskAct-leave-active {
  position: absolute; top: 0; right: 0; margin: 0;
  animation: actionOut 0.16s var(--ease-press) both;
}
.taskAct-enter-active { animation: actionIn 0.26s var(--ease-fluid) both; }

/* 选择圈：宽 + 负 margin + 边框宽 一起补间，避免行内内容被硬挤 */
.check-enter-from {
  width: 0; margin-right: calc(-1 * var(--row-gap, var(--sp-3)));
  border-width: 0; opacity: 0; transform: scale(0.4);
}
.check-enter-active {
  transition: width var(--dur-base) var(--ease-fluid),
              margin-right var(--dur-base) var(--ease-fluid),
              border-width var(--dur-base) var(--ease-fluid),
              opacity 0.2s ease,
              transform 0.3s var(--ease-spring);
}
.check-leave-active {
  transition: width 0.2s var(--ease-press),
              margin-right 0.2s var(--ease-press),
              border-width 0.2s var(--ease-press),
              opacity 0.16s var(--ease-press),
              transform 0.2s var(--ease-press);
}
.check-leave-to {
  width: 0; margin-right: calc(-1 * var(--row-gap, var(--sp-3)));
  border-width: 0; opacity: 0; transform: scale(0.5);
}

/* 高度展开（grid-template-rows 真插值 + transition-duration 作为 Vue 的时长信号） */
.tree-children { display: grid; grid-template-rows: 1fr; min-height: 0; }
.tree-children > * { min-height: 0; overflow: hidden; }
.tree-children-enter-active {
  animation: treeGrowIn 0.42s var(--ease-fluid) both;
  transition: opacity 0.42s var(--ease-fluid), grid-template-rows 0.42s var(--ease-fluid);
}
.tree-children-leave-active {
  animation: treeGrowOut 0.26s var(--ease-press) both;
  transition: opacity 0.26s var(--ease-press), grid-template-rows 0.26s var(--ease-press);
}
.tree-children-enter-from,
.tree-children-leave-to { opacity: 1; }

.row-expand { display: grid; grid-template-rows: 1fr; min-height: 0; }
.row-expand > * { min-height: 0; overflow: hidden; }
.rowExpand-enter-active {
  animation: rowExpandIn 0.42s var(--ease-fluid) both;
  transition: opacity 0.42s var(--ease-fluid), grid-template-rows 0.42s var(--ease-fluid);
}
.rowExpand-leave-active {
  animation: rowExpandOut 0.24s var(--ease-press) both;
  transition: opacity 0.24s var(--ease-press), grid-template-rows 0.24s var(--ease-press);
}
.rowExpand-enter-from,
.rowExpand-leave-to { opacity: 1; }

.count-enter-active { animation: countIn 0.34s var(--ease-spring) both; }
.count-leave-active { animation: countOut 0.18s var(--ease-press) both; }

/* 抽屉页签 · 方向感知转场 */
.pane-forward-enter-active,
.pane-back-enter-active {
  transition: opacity 0.3s var(--ease-apple),
              transform 0.46s var(--ease-fluid),
              filter 0.3s var(--ease-apple);
  will-change: transform, opacity, filter;
}
.pane-forward-leave-active,
.pane-back-leave-active {
  transition: opacity 0.18s var(--ease-press),
              transform 0.24s var(--ease-press);
}
.pane-forward-enter-from { opacity: 0; transform: translate3d(32px, 0, 0) scale(0.982); }
.pane-forward-leave-to   { opacity: 0; transform: translate3d(-24px, 0, 0) scale(0.986); }
.pane-back-enter-from    { opacity: 0; transform: translate3d(-32px, 0, 0) scale(0.982); }
.pane-back-leave-to      { opacity: 0; transform: translate3d(24px, 0, 0) scale(0.986); }
.pane-forward-enter-active > *,
.pane-back-enter-active > * { animation: paneItemIn 0.34s var(--ease-fluid) backwards; }
.pane-forward-enter-active > *:nth-child(1),
.pane-back-enter-active > *:nth-child(1) { animation-delay: 0.01s; }
.pane-forward-enter-active > *:nth-child(2),
.pane-back-enter-active > *:nth-child(2) { animation-delay: 0.03s; }
.pane-forward-enter-active > *:nth-child(3),
.pane-back-enter-active > *:nth-child(3) { animation-delay: 0.05s; }
.pane-forward-enter-active > *:nth-child(4),
.pane-back-enter-active > *:nth-child(4) { animation-delay: 0.07s; }
.pane-forward-enter-active > *:nth-child(n + 5),
.pane-back-enter-active > *:nth-child(n + 5) { animation-delay: 0.09s; }

/* admin 后台的视图切换叫 .view-*，与 .pane-* 是同一套方向感知转场，
   统一在这里定义（离场元素绝对定位，避免两页同时占位把容器撑高）。 */
.view-forward-enter-active,
.view-back-enter-active {
  transition: opacity 0.3s var(--ease-apple),
              transform 0.46s var(--ease-fluid),
              filter 0.3s var(--ease-apple);
  will-change: transform, opacity, filter;
}
.view-forward-leave-active,
.view-back-leave-active {
  transition: opacity 0.18s var(--ease-press),
              transform 0.24s var(--ease-press);
  position: absolute; left: 0; right: 0; top: 0; pointer-events: none;
}
.view-forward-enter-from { opacity: 0; transform: translate3d(32px, 0, 0) scale(0.982); }
.view-forward-leave-to   { opacity: 0; transform: translate3d(-24px, 0, 0) scale(0.986); }
.view-back-enter-from    { opacity: 0; transform: translate3d(-32px, 0, 0) scale(0.982); }
.view-back-leave-to      { opacity: 0; transform: translate3d(24px, 0, 0) scale(0.986); }
.view-forward-enter-active > *,
.view-back-enter-active > * { animation: paneItemIn 0.34s var(--ease-fluid) backwards; }
.view-forward-enter-active > *:nth-child(1),
.view-back-enter-active > *:nth-child(1) { animation-delay: 0.01s; }
.view-forward-enter-active > *:nth-child(2),
.view-back-enter-active > *:nth-child(2) { animation-delay: 0.03s; }
.view-forward-enter-active > *:nth-child(3),
.view-back-enter-active > *:nth-child(3) { animation-delay: 0.05s; }
.view-forward-enter-active > *:nth-child(4),
.view-back-enter-active > *:nth-child(4) { animation-delay: 0.07s; }
.view-forward-enter-active > *:nth-child(n + 5),
.view-back-enter-active > *:nth-child(n + 5) { animation-delay: 0.09s; }

/* 入场已纯 transform + opacity，无 filter：低性能档无需再单独撤 blur */

/* --------------------------------------------------------------------------
   文字级切换（.label / .morph）—— 状态胶囊、当前上限数值等「原地换字」
   --------------------------------------------------------------------------
   重构（Apple 风）：入场/出场**只动 transform + opacity**，彻底移除原先的
   filter: blur() 绘制开销（见铁律：只动 transform/opacity）。
     - 位移 + 淡入的观感与原来一致（起点、spring 缓动都保留）
     - 元素提前提层（will-change / translateZ(0)），走合成器而非每帧重绘
     - 低端设备（data-perf="low"）仍保留一次性 transform+opacity 入场，
       不再需要单独撤 blur（因为本来就没有）
   -------------------------------------------------------------------------- */
.label-enter-active {
  transition: opacity 0.3s var(--ease-apple),
              transform 0.42s var(--ease-spring);
  will-change: transform, opacity;
}
.label-leave-active {
  transition: opacity 0.18s var(--ease-press),
              transform 0.22s var(--ease-press);
  will-change: transform, opacity;
}
.label-enter-from { opacity: 0; transform: translate3d(6px, 0, 0) translateZ(0); }
.label-leave-to   { opacity: 0; transform: translate3d(-6px, 0, 0) translateZ(0); }

/* 更大的文字块切换（标题 / 段落级）：位移与模糊幅度略大，同样提层 */
.morph-enter-active {
  transition: opacity 0.34s var(--ease-apple),
              transform 0.46s var(--ease-spring);
  will-change: transform, opacity;
}
.morph-leave-active {
  transition: opacity 0.2s var(--ease-press),
              transform 0.26s var(--ease-press);
  will-change: transform, opacity;
}
.morph-enter-from { opacity: 0; transform: translate3d(0, 10px, 0) scale(0.975) translateZ(0); }
.morph-leave-to   { opacity: 0; transform: translate3d(0, -8px, 0) scale(0.975) translateZ(0); }

/* 入场已纯 transform + opacity，低性能档无需再撤 blur */

/* 承载这两个过渡的元素（行内文字）本身也要提层，否则 will-change 加在过渡类上
   会随「进出场类名」被反复添加/移除，反而引起层重创建。这里对常见宿主一次性提层。
   display:inline-block 是 transition 对行内元素生效的前提（transform 对纯 inline 无效），
   原先散落在 index.html 里，随本次统一一起收口到这里。 */
.status-pill__text,
.summary__value {
  display: inline-block;
  transform: translateZ(0);
  backface-visibility: hidden;
}

/* Sheet / Dialog
   ---------------------------------------------------------------------------
   注意：这四个浮层的入场动画**只在这里定义**。宿主元素（.modal-mask /
   .modal--sheet / .modal--dialog / .modal--preview）上**不再**挂 @keyframes
   （原先是 maskIn / sheetIn / dialogIn）。原因：那样会在同一个元素上叠两套动画，
   两套曲线争同一个 transform/opacity 轨道 —— GPU 空闲时看不出，低端设备一掉帧
   相位就错开，紧贴面板边沿的内容（如存储节点首行）会来回抖。
   下面把这几个 keyframes 的起点与节奏完整并入过渡，观感保持一致。 */
.sheet-enter-active, .sheet-leave-active,
.dialog-enter-active, .dialog-leave-active {
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}
.sheet-enter-active { transition: opacity 0.3s var(--ease-apple); }
.sheet-leave-active { transition: opacity 0.22s var(--ease-apple); }
/* 面板上浮：0.46s 与原先 sheetIn 的 0.42s 接近，观感一致 */
.sheet-enter-active .modal { transition: transform 0.46s var(--ease-apple); }
.sheet-leave-active .modal { transition: transform 0.26s var(--ease-press), opacity 0.22s var(--ease-press); }
.sheet-enter-from,
.sheet-leave-to { opacity: 0; }
/* 起点与 sheetIn 的 from 完全一致：translate3d(0,100%,0)，面板从屏幕外升起 */
.sheet-enter-from .modal { transform: translate3d(0, 100%, 0); }
.sheet-leave-to   .modal { transform: translate3d(0, 100%, 0); opacity: 0; }

.dialog-enter-active { transition: opacity 0.26s var(--ease-apple); }
.dialog-leave-active { transition: opacity 0.18s var(--ease-press); }
/* 保留 dialogIn 的「透明度先到位、缩放随后跟上」节奏（原 keyframes 在 60% 处
   opacity 已到 1）：透明度 0.26s，transform 0.44s，两者错开即等效。 */
.dialog-enter-active .modal,
.dialog-enter-active .dialog-panel { transition: transform 0.44s var(--ease-spring); }
.dialog-leave-active .modal,
.dialog-leave-active .dialog-panel { transition: transform 0.22s var(--ease-press), opacity 0.18s var(--ease-press); }
.dialog-enter-from,
.dialog-leave-to { opacity: 0; }
/* 起点对齐 dialogIn 的 from：scale(0.86) translate3d(0,16px,0) */
.dialog-enter-from .modal,
.dialog-enter-from .dialog-panel { transform: scale(0.86) translate3d(0, 16px, 0); }
.dialog-leave-to   .modal,
.dialog-leave-to   .dialog-panel { transform: scale(0.92) translate3d(0, 10px, 0); opacity: 0; }

/* ==========================================================================
   13.9 模态面板基类
   ----------------------------------------------------------------------
   原先只有 index.html 在自己的 <style> 里定义 .modal 的玻璃质感，
   其余页面的模态面板各自散落、外观不完全一致。
   以 index 为准上收到这里，页面只需写自己的尺寸/布局增量。
   ========================================================================== */
.modal {
  background: var(--glass-elevated);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  border: 1.5px solid var(--glass-border-ice);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-elevated);
  overflow-y: auto;
  will-change: transform, opacity;
}

/* 主题切换时，玻璃容器平滑换色而不是硬切 */
.card, .liquid-card, .menu, .dock, .selection-bar, .modal, .modal-mask {
  transition: background-color 0.45s var(--ease-apple),
              border-color 0.45s var(--ease-apple),
              box-shadow 0.45s var(--ease-apple),
              color 0.45s var(--ease-apple);
}

/* ==========================================================================
   13.10 环境光背景 & 密码门
   ----------------------------------------------------------------------
   .ambient-glow / .ambient-orb 原先在 index / admin / gallery / paste
   四页各写一份，其中 .ambient-glow 四页**完全相同** —— 典型的手抄漂移。
   这里上收容器与光球基类；.orb-1 / .orb-2 的**位置、尺寸、配色**是各页
   有意的差异化设计（以 index 为准），仍留在各页。

   .password-gate 系列原先在 share / preview 各写一份，preview 那份把
   设计令牌全写成了硬编码回退值（#0ea5e9 / #ef4444 / #64748b），
   导致换主题时 password 弹层不跟随。这里以 share 的令牌版为准上收。
   ========================================================================== */

/* --- 环境光容器：四页完全一致 --- */
.ambient-glow {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  overflow: hidden;
}

/* --- 光球基类：以 index 为准（不加 blur，用径向渐变自身柔化） ---
   gallery 曾额外加 filter: blur(90px)，与径向渐变叠加属冗余，
   且 blur 是绘制属性、在低端设备上掉帧，故统一为不带 blur。 */
.ambient-orb {
  position: absolute;
  border-radius: 50%;
  opacity: 0.48;
}

/* --- 密码门：以 share 的令牌版为准（preview 原为硬编码色值） --- */
.password-gate {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  text-align: center;
  max-width: 380px;
  padding: 28px 24px;
  border-radius: var(--radius-lg);
  background: var(--surface-1);
  border: 1px solid var(--glass-border);
  box-shadow: var(--shadow-soft);
}
.password-gate-icon {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.3rem;
  color: var(--primary);
  background: rgba(var(--primary-rgb), 0.12);
}
.password-gate-title { font-size: 1rem; font-weight: 700; }
.password-gate-desc {
  font-size: 0.8rem;
  color: var(--text-muted);
  line-height: 1.5;
}
.password-gate-form { display: flex; gap: 8px; width: 100%; margin-top: 6px; }
.password-gate-form input {
  flex: 1;
  min-width: 0;
  font-family: inherit;
  font-size: 0.86rem;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid var(--glass-border-ice);
  background: var(--surface-2);
  color: inherit;
  outline: none;
  transition: var(--transition);
}
.password-gate-form input:focus { border-color: var(--primary); }
.password-gate-form button {
  font-family: inherit;
  font-size: 0.86rem;
  font-weight: 600;
  color: #fff;
  background: var(--primary);
  border: none;
  border-radius: 10px;
  padding: 10px 16px;
  cursor: pointer;
  white-space: nowrap;
  transition: var(--transition);
}
.password-gate-form button:hover:not(:disabled) { background: var(--primary-dark); }
.password-gate-form button:disabled { opacity: 0.6; cursor: progress; }
.password-gate-error { font-size: 0.78rem; color: var(--danger); }

/* --- 小工具类：原先 index / admin 各写一份，写法不同但语义一致 --- */
.mono { font-family: 'JetBrains Mono', monospace; }
.grow { flex: 1; }

/* ==========================================================================
   13.12 滚动揭示（reveal on scroll）
   ----------------------------------------------------------------------
   配套 app-core.js 的 KVault.revealOnScroll()：给元素加 [data-reveal]，
   进入视口时由 JS 加 .is-revealed。**只动 transform + opacity**，零依赖、
   零风险；Apple 风统一缓动 --ease-out。respects prefers-reduced-motion：
   命中时 app-core 直接全量揭示，不做位移动画。
   ========================================================================== */
[data-reveal] {
  opacity: 0;
  transform: translate3d(0, 14px, 0);
  transition: opacity 0.6s var(--ease-out), transform 0.6s var(--ease-out);
  will-change: transform, opacity;
}
[data-reveal].is-revealed {
  opacity: 1;
  transform: translate3d(0, 0, 0);
  will-change: auto;
}

/* ==========================================================================
   14. 性能分级 & 无障碍降级
   ========================================================================== */
/* 低端设备：去掉模糊 */
html[data-perf="low"] .card,
html[data-perf="low"] .menu,
html[data-perf="low"] .dock,
html[data-perf="low"] .selection-bar,
html[data-perf="low"] .modal,
html[data-perf="low"] .modal-mask {
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}
html[data-perf="low"] .ambient-orb { animation: none; }
/* 入场已纯 transform + opacity，低性能档无需再撤 blur */

/* 灵动引擎关闭（html[data-perf="low"]）：只撤「贵」的部分，保留入场质感。
   早先这里是「动画全停」（duration 压到 0.01ms），代价是把弹簧手感也一起杀掉了。
   现在改为：
     - 撤掉 backdrop-filter（每帧都要重算模糊，是低端设备最大开销）
     - 撤掉常驻/循环动画（ambient-orb、spin 之类会持续占 GPU）
     - **保留**一次性的 transform + opacity 入场过渡（纯合成，开销极低）
   这样低端设备也能拿到与高端一致的入场观感，而不再是「啪」一下出现。 */
html[data-perf="low"] *,
html[data-perf="low"] *::before,
html[data-perf="low"] *::after {
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}

/* 常驻循环动画在低性能档位直接停掉（它们才是持续吃 GPU 的那部分） */
html[data-perf="low"] .ambient-orb,
html[data-perf="low"] .spinner,
html[data-perf="low"] .spinner-ring,
html[data-perf="low"] .fa-spin,
html[data-perf="low"] [class*="pulse"],
html[data-perf="low"] [class*="float"] {
  animation: none !important;
}

/* 同步状态点：低性能档停掉循环动画，但**颜色必须保留** ——
   颜色是语义本体（黄=同步中/绿=已同步/红=失败），停掉动画只是省掉呼吸，
   绝不能连颜色一起丢，否则用户会彻底看不到同步状态。 */
html[data-perf="low"] .sync-dot {
  animation: none !important;
}

/* 浮层（toast / 菜单 / 弹窗 / 面板）保留入场过渡，只把时长压短一点，
   让低端设备不至于因为长过渡而掉帧，但曲线与终态完全一致。 */
html[data-perf="low"] .toast,
html[data-perf="low"] .toast-stack * {
  will-change: transform, opacity;
}
html[data-perf="low"] .toast-enter-active { transition-duration: 0.2s, 0.32s; }
html[data-perf="low"] .toast-leave-active { transition-duration: 0.14s, 0.16s; }
/* 模糊停用后，玻璃层必须改为不透明，否则背景光晕会透上来压低文字对比度 */
html[data-perf="low"] {
  --glass-surface: #ffffff;
  --glass-elevated: #ffffff;
  --glass-subtle: #f8fafc;
  --glass-border: rgba(15, 23, 42, 0.1);
}
html[data-theme="dark"][data-perf="low"] {
  --glass-surface: #0f1b31;
  --glass-elevated: #142442;
  --glass-subtle: #0d1626;
}

@media (prefers-reduced-transparency: reduce) {
  /* 一并覆盖 glassfx 的 .glass：否则 .glass::after（bloom）与 ::before（rim）
     在关掉模糊后仍常驻，且 .glass 自身的 backdrop-filter 会漏网。 */
  .card, .menu, .dock, .selection-bar, .modal, .modal-mask, .glass {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
  }
  .glass::after { display: none; }
}

/* 尊重系统「减弱动态效果」 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
    will-change: auto !important;
  }
  /* Dock 滑动指示器：直接跳格，不做弹性滑动（页面内联不重复声明该媒体查询）。 */
  .dock__indicator { transition: none !important; }
}
