@charset "UTF-8";
/* ══ 动效语言（工作台界面优化稿 · 动效规范，Tim 10-11）══════════════════════════════════════════════
   规范正文：docs/design/WORKBENCH-STYLE.md 第 9 节「动效语言」。本文件在 styles.css、mobile.css 之后加载。
   原则：动效只为说明状态变化；一个动作只给一个反馈；不做装饰性循环（循环只用于「加载中」和「等手机扫码」）；
   位移、缩放只用 transform，淡入淡出只用 opacity；颜色提示只用在新增行和完成的步骤上。
   系统开「减少动态效果」时：关掉全部位移、缩放和循环，只留淡入和颜色（本文件最后一段）。
   扫码会话（scan.css、styles.css 扫码段落）用同一组变量；扫码层自己的动效写在那边，这里不重复。 */

/* ── 令牌 ── */
:root{
  --ease-standard:cubic-bezier(.2,.8,.2,1);       /* 标准曲线：进入、离开、推进都用它 */
  --ease-press-release:cubic-bezier(.2,2.6,.4,1);  /* 松开按钮：从 96 % 回到 100 % 时冲过约 2 %，即 1.02 */
  --dur-in-fast:120ms;   /* 按下 */
  --dur-in:200ms;        /* 对话框、通知、新增行、勾画线 */
  --dur-in-slow:300ms;   /* 进度条推进、卡片首次出现 */
  --dur-out:150ms;       /* 离开：对话框关闭、通知滑出、松开回弹 */
  --dur-glow:1.2s;       /* 新增行的钢蓝浅底淡出 */
  --dur-breathe:1.8s;    /* 等手机扫码的呼吸 */
  --stagger-step:60ms;   /* 卡片错开间隔 */
}

/* ── 关键帧 ── */
@keyframes motion-fade-in{from{opacity:0}}
@keyframes motion-fade-out{to{opacity:0}}
@keyframes motion-toast-in{from{opacity:0;transform:translateX(24px)}}
@keyframes motion-toast-out{to{opacity:0;transform:translateX(24px)}}
@keyframes motion-row-in{from{opacity:0;transform:translateY(-10px)}}
@keyframes motion-row-glow{from{background-color:#eff5f9}}
@keyframes motion-tick-draw{from{stroke-dashoffset:var(--tick-len,20)}to{stroke-dashoffset:0}}
@keyframes motion-tick-pop{0%{opacity:0;transform:scale(.6)}60%{opacity:1;transform:scale(1.06)}100%{opacity:1;transform:scale(1)}}
@keyframes motion-breathe{from{opacity:.55;transform:scale(1)}to{opacity:0;transform:scale(var(--breathe-scale,1.5))}}
@keyframes motion-card-in{from{opacity:0;transform:translateY(8px)}}
@keyframes motion-banner-in{from{opacity:0;transform:translateY(-8px)}}
@keyframes motion-shimmer{from{transform:translateX(-100%)}to{transform:translateX(100%)}}

/* ── 按钮按下：缩到 96 %（120 ms），松开冲到 102 % 再回 100 %（150 ms）。扫码键、扫码层里的按钮由扫码会话管 ── */
:where(.primary,.secondary,.rowbtn,.icon-button,.toast-close):not(:is(.scan-key,.scan-layer,.scan-layer *)){transition:transform var(--dur-out) var(--ease-press-release),background-color .16s ease-out,border-color .16s ease-out,color .16s ease-out}
:where(.primary,.secondary,.rowbtn,.icon-button,.toast-close):not(:is(.scan-key,.scan-layer,.scan-layer *)):active:not(:disabled){transform:scale(.96);transition-duration:var(--dur-in-fast);transition-timing-function:ease-out}

/* ── 对话框打开 / 关闭：淡入并从 98 % 放大到 100 %（200 ms）；关闭反向（150 ms）。
   送样 / 线下出库对话框（#editor:has(#offline-ship-unit)）也走这一套。扫码层（dialog.scan-layer）由扫码会话管 ── */
dialog:not(.scan-layer){opacity:0;transform:scale(.98);transition:opacity var(--dur-out) var(--ease-standard),transform var(--dur-out) var(--ease-standard),overlay var(--dur-out) allow-discrete,display var(--dur-out) allow-discrete}
dialog:not(.scan-layer)[open]{opacity:1;transform:none;transition-duration:var(--dur-in)}
@starting-style{dialog:not(.scan-layer)[open]{opacity:0;transform:scale(.98)}}
dialog:not(.scan-layer)::backdrop{opacity:0;transition:opacity var(--dur-out) var(--ease-standard),overlay var(--dur-out) allow-discrete,display var(--dur-out) allow-discrete}
dialog:not(.scan-layer)[open]::backdrop{opacity:1;transition-duration:var(--dur-in)}
@starting-style{dialog:not(.scan-layer)[open]::backdrop{opacity:0}}

/* ── 操作结果通知：从右侧滑入（200 ms），滑出（150 ms）── */
.toast{animation:motion-toast-in var(--dur-in) var(--ease-standard) both}
.toast.is-in{animation:none}   /* 已经滑进来的：通知区挪位置、重新抬到最上层时不重播入场 */
.toast.is-leaving{animation:motion-toast-out var(--dur-out) var(--ease-standard) forwards;pointer-events:none}

/* ── 新增一行（发货、入库、扫码结果等）：从上方滑入（200 ms），底色钢蓝浅底淡出（1.2 s）。
   表格行加在 <tr> 上（底色落在单元格），列表项、卡片加在元素本身。app.js 刷新数据后自动给新出现的表格行加，实机验收左栏新进的记录也用它 ── */
.motion-row-new{animation:motion-row-in var(--dur-in) var(--ease-standard) both}
:not(tr).motion-row-new{animation:motion-row-in var(--dur-in) var(--ease-standard) both,motion-row-glow var(--dur-glow) ease-out}
tr.motion-row-new>td{animation:motion-row-glow var(--dur-glow) ease-out}

/* ── 步骤完成：勾画出（200 ms），进度条推进（300 ms）── */
.motion-tick path{stroke-dasharray:var(--tick-len,20);stroke-dashoffset:0}
.motion-tick.is-new path{animation:motion-tick-draw var(--dur-in) ease-out both}
.motion-tick.is-new{animation:motion-fade-in var(--dur-in) var(--ease-standard)}
.motion-progress{transform-origin:left center;transform:scaleX(var(--progress,0));transition:transform var(--dur-in-slow) var(--ease-standard)}

/* ── 等手机扫码：钢蓝圆点外圈呼吸，1.8 s 一次，只在等待时出现 ── */
.motion-wait{position:relative;display:inline-flex;align-items:center;gap:8px;padding:4px 10px 4px 24px;border-radius:999px;background:#eff5f9;color:#3d6e8c;font-size:13px;font-weight:400;line-height:1.5;white-space:nowrap}
.motion-wait::before,.motion-wait::after{content:"";position:absolute;left:10px;top:50%;width:7px;height:7px;margin-top:-3.5px;border-radius:50%;background:#3d6e8c}
.motion-wait::after{--breathe-scale:2.6;animation:motion-breathe var(--dur-breathe) var(--ease-standard) infinite}

/* ── 页面卡片首次出现：错开上浮（300 ms，间隔 60 ms，最多 4 张）。只在打开工作台后的第一页播放，切页不播 ── */
.motion-card-in{animation:motion-card-in var(--dur-in-slow) var(--ease-standard) both;animation-delay:calc(var(--stagger-index,0) * var(--stagger-step))}

/* ── 加载中：骨架行上一道浅光扫过（1.2 s 循环，只在加载时出现）── */
.skeleton-bar::after{content:"";position:absolute;inset:0;background:linear-gradient(90deg,#fff0,#ffffffb3,#fff0);transform:translateX(-100%);animation:motion-shimmer 1.2s linear infinite}

/* ── 扫码会话交给这里的三处（只加动效，不改结构）──
   1. 送样 / 线下出库对话框：见上面「对话框」一段。
   2. 手机上「电脑正在实机验收」提示条（#scan-wait，scan-key.js）：入场下落淡入。
   3. 发货三步（#scan-steps，scan-key.js）：刚完成的那一步勾弹出、底色淡出。scan-key.js 每次输入都重画整列，
      所以「刚完成」由 ui-polish.js 比对前后两次的完成状态后加 .motion-just-done，只播一次。 */
.scan-wait:not([hidden]){animation:motion-banner-in var(--dur-in) var(--ease-standard) both}
.scan-step.motion-just-done{animation:motion-row-glow var(--dur-glow) ease-out}
.scan-step.motion-just-done .scan-step-mark{animation:motion-tick-pop var(--dur-in) var(--ease-standard) both}
.scan-step.motion-just-done .scan-step-mark path{--tick-len:24;stroke-dasharray:24;animation:motion-tick-draw var(--dur-in) ease-out both}

/* ── 减少动态效果：关掉全部位移、缩放、循环，只留淡入和颜色 ── */
@media (prefers-reduced-motion:reduce){
  :where(.primary,.secondary,.rowbtn,.icon-button,.toast-close):not(:is(.scan-key,.scan-layer,.scan-layer *)){transition:background-color .16s ease-out,border-color .16s ease-out,color .16s ease-out}
  :where(.primary,.secondary,.rowbtn,.icon-button,.toast-close):not(:is(.scan-key,.scan-layer,.scan-layer *)):active:not(:disabled){transform:none}
  dialog:not(.scan-layer),dialog:not(.scan-layer)[open]{transform:none}
  @starting-style{dialog:not(.scan-layer)[open]{transform:none}}
  .toast{animation:motion-fade-in var(--dur-in) linear both}
  .toast.is-in{animation:none}
  .toast.is-leaving{animation:motion-fade-out var(--dur-out) linear forwards}
  .motion-row-new,:not(tr).motion-row-new{animation:motion-fade-in var(--dur-in) linear both}
  :not(tr).motion-row-new{animation:motion-fade-in var(--dur-in) linear both,motion-row-glow var(--dur-glow) ease-out}
  .motion-tick.is-new path{animation:none}
  .motion-progress{transition:none}
  .motion-wait::after{animation:none;display:none}
  .motion-card-in{animation:motion-fade-in var(--dur-in-slow) linear both;animation-delay:0s}
  .skeleton-bar::after{animation:none;display:none}
  .scan-wait:not([hidden]){animation:motion-fade-in var(--dur-in) linear both}
  .scan-step.motion-just-done .scan-step-mark,.scan-step.motion-just-done .scan-step-mark path{animation:motion-fade-in var(--dur-in) linear both}
}
