/* fluent2-motion.css —— 官方动效的 CSS 落地
   数值出处：learn.microsoft.com/en-us/windows/apps/design/signature-experiences/motion
   （Direct Entrance/Exit 167ms、Point to Point 167/250/333ms、Gentle Exit 167ms、Fade 83ms、
     Strong Entrance 3 关键帧；曲线见 fluent2-tokens.css 的 --fl-ease-*，勿手写时长/缓动）

   页面转场另有实测派生值（283ms / ease-page / 176-72px）：来自 Win11 官方应用 60fps 录屏反解，
   定义在 fluent2-tokens.css 的「实测派生」段，方法与原始数字见 VIDEO-ALIGN.md §9。

   ── 两类动效分开表达（这是本文件的核心约定）──
   1) 一次性动效（入场/出场/页面转场/Strong Entrance）→ keyframe animation
      理由：不占用 transition 属性，控件自身的 hover/active transition 简写不会把它覆盖掉；
            animation-delay 可走 --fl-stagger-delay 自定义属性，不污染元素的其他过渡。
   2) 可逆的状态切换（浮层开合、hover/active、switch 等）→ transition
      理由：中途反转可平滑接续。
   3) 连续型动效（秒针/进度环等永不结束的推进）→ animation + infinite + linear/steps，且不得 both
      理由：没有终态，用 both/forwards 会把角度钉死；红线见校验器 R23。 */

/* ---- Direct Entrance：167ms standard + fade（官方 Fast-In，已有元素入场） ----
   静态隐藏；.fl-in 触发动画；延迟由 --fl-stagger-delay 提供（stagger 不写 transition-delay）
   位移 8px：录屏《时钟》小面板实测 8–12px（+12→+8→+4→0），原先 16px 偏大约一倍 */
.fl-entrance { opacity: 0; }
.fl-entrance.fl-in {
  animation: fl-entrance-kf var(--fl-dur-normal) var(--fl-ease-standard) both;
  animation-delay: var(--fl-stagger-delay, 0ms);
}
@keyframes fl-entrance-kf {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

/* ---- Point to Point：局部内容替换（位移 12px / 250ms point）----
   页面级转场已改用 .fl-page-in/.fl-page-out（见下），本类保留给「块内容就地替换」场景 */
.fl-point-to-point.fl-in {
  animation: fl-point-to-point-kf var(--fl-dur-gentle) var(--fl-ease-point) both;
}
@keyframes fl-point-to-point-kf {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}

/* 位移类元素（拖动/排序/进度等位置变化）直接套用官方 point 曲线（可逆，用 transition） */
.fl-movable { transition: transform var(--fl-dur-normal) var(--fl-ease-point); }

/* ---- Direct Exit：Fast-Out，167ms，官方硬性要求必须与 fade out 组合 ---- */
.fl-exit {
  animation: fl-exit-kf var(--fl-dur-normal) var(--fl-ease-standard) both;
  pointer-events: none; /* 离场元素不再接收交互 */
}
@keyframes fl-exit-kf {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(-8px); }
}

/* ---- Fade：Linear，83ms，仅 Opacity（状态切换，用 transition） ---- */
.fl-fade { transition: opacity var(--fl-dur-fast) var(--fl-ease-linear); }
.fl-fade.fl-hidden { opacity: 0; }

/* ---- Strong Entrance：Elastic In 三关键帧（167+167+333 = 667ms 总时长） ----
   KF1 167ms cubic-bezier(0.85,0,0,1) → KF2 167ms cubic-bezier(0.85,0,0.75,1) → KF3 333ms cubic-bezier(0.85,0,0,1) */
@keyframes fl-strong-entrance-kf {
  0%   { opacity: 0; transform: translateY(32px) scale(0.96); animation-timing-function: var(--fl-ease-strong-1); }
  25%  { opacity: 1; transform: translateY(8px)  scale(1.01); animation-timing-function: var(--fl-ease-strong-2); }
  50%  { transform: translateY(-4px) scale(0.995); animation-timing-function: var(--fl-ease-strong-1); }
  100% { opacity: 1; transform: none; animation-timing-function: var(--fl-ease-strong-1); }
}
.fl-strong-entrance { animation: fl-strong-entrance-kf 667ms var(--fl-ease-strong-1) both; }

/* ---- 页面转场：一套 keyframe 覆盖「两轴 × 四向」（实测派生 283ms / 176-72px）----
   轴与方向由 JS 写入 4 个带符号的自定义属性（单位 px），keyframe 内不写位移字面量（校验器 R20）：
     --fl-page-from-x / -from-y  新页起点偏移（负值 = 从左侧/上方进入）
     --fl-page-to-x   / -to-y    旧页终点偏移
   行程按轴取：x 轴 var(--fl-page-shift-x)=176px、y 轴 var(--fl-page-shift-y)=72px；
   方向取六个值之一（forward / back / up / down / left / right），由 motion.js 的 DIR 表解析成符号，
   缺省按「导航项先后序 → 页面栈」推断 —— 点侧栏更上面的一项即自上方进入（详见 KIT 规则 15）。
   位移与淡入拆成两条 animation。
   节奏按录屏实测（2026-10-21 设置应用逐帧）：**两拍，不是同时** ——
     t=0 … 83ms   旧页滑动 + 淡出（实测旧页在 ≈80-170ms 内就消失干净）
     t=83 … 366ms 新页从行程起点滑入（延迟 = 旧页淡出结束，避免两页文字在中间撞在一起）
   早期版本让两页同时淡入淡出，导致中途「新旧两页文字各半透明叠在一起」，观感是重影。 */
.fl-page-in {
  will-change: transform, opacity;
  animation:
    fl-page-in-slide-kf var(--fl-dur-page) var(--fl-ease-page) both,
    fl-page-in-fade-kf var(--fl-dur-fast) var(--fl-ease-linear) both;
  /* 延迟略短于旧页退场（0.8×167≈133ms）：两拍稍微搭接一点点。
     若延迟正好等于退场时长，旧页淡到 0 的那一帧新页还是全透明 —— 中间会空出 1 帧背景，
     快速转场里这 1 帧就是可见的「闪一下」。 */
  animation-delay: calc(var(--fl-dur-normal) * 0.8);
}
.fl-page-out {
  will-change: transform, opacity;
  pointer-events: none; /* 离场页不再接收交互 */
  animation:
    fl-page-out-slide-kf var(--fl-dur-normal) var(--fl-ease-page-out) both,
    fl-page-out-fade-kf var(--fl-dur-normal) var(--fl-ease-linear) both;
}
@keyframes fl-page-in-slide-kf {
  from { transform: translate3d(var(--fl-page-from-x, 0px), var(--fl-page-from-y, 0px), 0); }
  to   { transform: none; }
}
@keyframes fl-page-out-slide-kf {
  from { transform: none; }
  to   { transform: translate3d(var(--fl-page-to-x, 0px), var(--fl-page-to-y, 0px), 0); }
}
@keyframes fl-page-in-fade-kf  { from { opacity: 0; } to { opacity: 1; } }
@keyframes fl-page-out-fade-kf { from { opacity: 1; } to { opacity: 0; } }

/* ---- 连续型动效：线性推进、无限循环、不保留终态 ----
   与一次性动效的区别：没有「播完」，所以禁用 both/forwards（会把终态钉死，校验器 R23）。
   用法：秒针 .fl-continuous（60s 一圈）；跳秒 .fl-tick（60s 内 60 步，每秒跳 6°）；
        分针/时针用 style="animation-duration: calc(var(--fl-dur-sweep) * 60)" 倍乘。 */
.fl-continuous {
  transform-origin: 50% 100%;
  animation-name: fl-continuous-kf;
  animation-duration: var(--fl-dur-sweep);
  animation-timing-function: var(--fl-ease-linear);
  animation-iteration-count: infinite;
  animation-fill-mode: none;
  will-change: transform;
}
.fl-tick {
  transform-origin: 50% 100%;
  animation-name: fl-continuous-kf;
  animation-duration: var(--fl-dur-sweep);
  animation-timing-function: var(--fl-ease-steps-60);
  animation-iteration-count: infinite;
  animation-fill-mode: none;
  will-change: transform;
}
@keyframes fl-continuous-kf {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

/* ---- 浮层（flyout / dialog / contextmenu）开合 ----
   官方规范（Learn《Animating pop-up UI (XAML)》/《弹出 UI 动画指南》）：
     · 弹出层 = **淡入 + 垂直平移**；**没有缩放**；方向看放置关系：出现在触发元素**下方**就从上方滑入，
       在上方就从下方滑入（"给弹出元素和触发元素之间一个视觉联系"）。
       自定义 Popup 用 `PopupThemeTransition`：平移 50px 即可。
     · ToolTip / 对话框**只用淡入淡出**，不许用弹出动画。
   三类浮层**动画不同**（各有录屏/源码依据，别混用）：
     .fl-combo-popup（下拉列表）= 从顶边向下**揭示**（内容不拉伸、不淡入），关闭瞬变
       —— 依据 `2026-10-21 23-13-35.mp4`；
     .fl-flyout（更多选项 / 上下文菜单）= **整个弹层滑入视野 + 裁剪到自身矩形** + **不淡入**
       —— 依据 `2026-10-22 16-34-16.mp4`（Win11 资源管理器 … 菜单）与 WinUI 源码注释
          "MenuFlyout 使用 windowed popup，带整个 popup 滑入视野的进出场动画，由
          MenuPopupThemeTransition 实现"。
       所以行程不是 50px 而是**弹层自身高度（100%）**：录屏里菜单从工具栏下边缘"长"出来，
       底边一路扫到底（内容整体下移 355 CSS，逐帧 0.46/0.59/0.75/0.85/0.89/0.92/0.96/1.00），
       首帧文字对比度就是满的（没有淡入），≈250ms（ControlNormal 档）减速收尾。
       录屏里那层"裁剪"是资源管理器的工具栏；网页里弹层是 portal、永远在最上层，
       所以用 clip-path 自己裁到**最终矩形**，复现"从触发元素那一侧长出来"的观感。
       向上翻（[data-drop="up"]）时从下方升起、由底边往上长。
   入场用 FastOutSlowIn（0,0,0,1）= --fl-ease-standard，出场用 SlowOutFastIn = --fl-ease-soft。
   关键：visibility 走 0s 过渡 + 延迟，让出场动画播完才真正隐藏（否则关闭瞬间就 hidden，动画看不见）。 */
.fl-overlay {
  transition: opacity var(--fl-dur-fast) var(--fl-ease-standard),
              visibility 0s var(--fl-ease-linear);
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}
.fl-overlay:not(.fl-open) {
  transition: opacity var(--fl-dur-fast) var(--fl-ease-soft),
              visibility 0s var(--fl-ease-linear) var(--fl-dur-fast);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
/* 向上展开（[data-drop="up"]，弹层在触发元素上方）时改为从下方升上来 */
.fl-overlay.fl-flyout[data-drop="up"] { --fl-popup-dir: 1; }
/* 菜单：整个弹层滑入视野（MenuPopupThemeTransition），不淡入；
   同时把弹层**裁剪到自身矩形**（inset 从"零高"长到 -24px），所以看起来是从触发元素那一侧
   "长"出来的，而不是从上面盖过去。向上翻时反向。
   裁剪与位移必须同步：位移 -(1-p)·H、裁掉上方 (1-p)·H —— 两者的合成刚好让可见带的下沿
   贴着内容下沿走（录屏里 b = 769 + 2c 就是这个关系），顶上那条线固定在弹层最终顶边。
   四周留 24px 是给 --fl-shadow-flyout（e32：0 8px 16px）的扩散，和下拉列表同一套做法。 */
.fl-overlay.fl-flyout {
  --fl-popup-shift: 100%;   /* = 弹层自身高度（录屏实测行程 355 CSS = 菜单高），不是 50px 微移 */
  --fl-popup-dir: -1;       /* -1 = 弹层在触发元素下方（从上方滑入）；+1 = 在上方（从下方滑入）。JS 按放置关系写 */
  opacity: 1;               /* 菜单不做淡入：录屏首帧文字对比度已经是满的 */
  transform: translateY(0);
  /* 上沿只留 2px：e32 阴影是 0 8px 16px（往下偏），顶边几乎没有阴影；
     留 24px 会让裁剪线在动画中往上漂 24px、把触发元素压住一截。左/右/下留 24px 给阴影。 */
  clip-path: inset(-2px -24px -24px -24px);
  transition: transform var(--fl-dur-control-normal) var(--fl-ease-standard),
              clip-path var(--fl-dur-control-normal) var(--fl-ease-standard),
              visibility 0s var(--fl-ease-linear);
}
.fl-overlay.fl-flyout:not(.fl-open) {
  opacity: 1;               /* 隐藏靠 visibility，不靠透明度 → 关闭也不会淡出 */
  transform: translateY(calc(var(--fl-popup-shift) * var(--fl-popup-dir)));
  clip-path: inset(100% 0 0 0);   /* 零高，贴在弹层最终顶边 —— 出现时从这里往下长 */
  transition: transform var(--fl-dur-control-fast) var(--fl-ease-soft),
              clip-path var(--fl-dur-control-fast) var(--fl-ease-soft),
              visibility 0s var(--fl-ease-linear) var(--fl-dur-control-fast);
}
/* 向上翻：贴在弹层最终底边，从下往上长（下沿同样只留 2px，其余 24px 给阴影） */
.fl-overlay.fl-flyout[data-drop="up"] { clip-path: inset(-24px -24px -2px -24px); }
.fl-overlay.fl-flyout[data-drop="up"]:not(.fl-open) { clip-path: inset(0 0 100% 0); }

/* ---- 下拉列表（ComboBox 弹层）：从顶边向下"揭示" ----
   录屏实测（2026-10-21 23-13-35.mp4，Win11 设置页 专注周期 下拉，60fps）：
     · 弹层**顶边固定**（y≈527 全程不动），只有**底边**往下扫；
     · 揭示的是**内容**：首帧里"5 分钟/10 分钟"就是正常字号，没有被拉伸 —— 所以不是 scaleY；
     · 首帧文字对比度已达 97%，**没有可见的淡入**；整段几乎没有中间透明度；
     · 关闭是**单帧**（f275 还在 → f276 全没了），没有退场动画。
   **速率**（这一版返修点 2）：上一版把逐帧进度 0.60/0.80/0.87/0.90/0.94/0.97/1.00 的**首帧 60%**
   当成曲线形状，套了 cubic-bezier(0.1,0.9,0.2,1) —— 那条曲线在 16.7ms 就到 58%，真机上用户能看到的
   第一帧就近乎全开，观感等于「没有动画」（用户原话）。
   实际上录屏是 59.73fps，弹层出现的那一帧本来就落在两个采样点的中间：允许自由相位 τ 重新拟合后，
   **(0,0,0,1)@167ms + τ≈25ms 的 RMSE 0.022**，与旧曲线的 0.014 没有统计差异（±1px 的测量噪声就这个量级）。
   所以曲线取**官方 ControlFastOutSlowIn = cubic-bezier(0,0,0,1)**（= --fl-ease-popup），
   起手有可见的加速段、末段收尾，16.7ms 只到 ~27%、83ms 到 ~85%。
   XAML 侧对应 SplitOpenThemeAnimation（ComboBox 打开/关闭的预置分割动画，Duration 预设不可改）。
   实现：`clip-path: inset()` 从"只剩顶边"到"完整"，内容不参与布局（不重排、不拉伸）；
   四周留 -24px 让阴影不被裁掉。向上翻（data-drop=up）时改成从底边往上揭示。 */
.fl-overlay.fl-combo-popup,
.fl-overlay.fl-combo-popup:not(.fl-open) { opacity: 1; }   /* 不淡入，只揭示 */
.fl-overlay.fl-combo-popup {
  clip-path: inset(-24px);
  transition: clip-path var(--fl-dur-popup) var(--fl-ease-popup),
              visibility 0s var(--fl-ease-linear);
}
.fl-overlay.fl-combo-popup:not(.fl-open) {
  clip-path: inset(-24px -24px calc(100% + 24px) -24px);
  /* 关闭瞬变：录屏里没有退场动画，硬切 */
  transition: none;
  visibility: hidden;
  pointer-events: none;
}
/* 向上翻：底边固定，往上揭示 */
.fl-overlay.fl-combo-popup[data-drop="up"]:not(.fl-open) {
  clip-path: inset(calc(100% + 24px) -24px -24px -24px);
}

/* Smoke 模态遮罩：恒半透明黑（官方不分主题），83ms fade，同样延迟隐藏 */
.fl-smoke {
  transition: opacity var(--fl-dur-fast) var(--fl-ease-linear),
              visibility 0s var(--fl-ease-linear);
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}
.fl-smoke:not(.fl-open) {
  transition: opacity var(--fl-dur-fast) var(--fl-ease-linear),
              visibility 0s var(--fl-ease-linear) var(--fl-dur-fast);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

/* ---- stagger 错开：JS 按 83ms 步进（官方 Fade 档）、上限 333ms 写进 --fl-stagger-delay ---- */

/* 无障碍：官方 Resourceful / 可用性原则的网页对应 */
@media (prefers-reduced-motion: reduce) {
  /* 一次性动效：直接呈现终态，不做位移与淡入 */
  .fl-entrance,
  .fl-point-to-point,
  .fl-strong-entrance,
  .fl-page-in {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  .fl-exit,
  .fl-page-out {
    animation: none !important;
    opacity: 0 !important;
    transform: none !important;
  }
  /* 连续型：停住，不退化成「转一圈后停」 */
  .fl-continuous,
  .fl-tick {
    animation: none !important;
  }
  /* 状态切换：瞬时完成，但显隐仍由各自规则（opacity/visibility）决定 */
  .fl-overlay,
  .fl-smoke,
  .fl-movable,
  .fl-fade,
  /* 控件级状态过渡（新增控件必须挂在这里，否则 reduced 下仍有位移/描画） */
  .fl-listitem,
  .fl-listitem::before,
  .fl-combo-item,
  .fl-combo-btn,
  .fl-combo-popup,
  .fl-combo-chevron,
  .fl-check .fl-box,
  .fl-radio .fl-box,
  .fl-check-glyph path,
  .fl-radio .fl-box::after {
    transition: none !important;
  }
}
