/* ==========================================================================
   历史口袋宝典 · 动效层（V1.08 新增）
   --------------------------------------------------------------------------
   独立成层：本文件只负责「进场与响应」，不参与任何布局与渲染。
   整段删除后，页面回到 V1.07 的行为（styles.css 里的基线与令牌都还在）。

   三条硬规则：
     1. 只动 transform / opacity / box-shadow / outline。
        绝不动 width / height / top / left —— 全站约 2.3 万个元素，一次全页重排
        就是几十毫秒，得不偿失。
     2. 一切「初始隐藏」只挂在 html.anim-ready 之下。这个类由 motion.js 在确认
        环境支持 IntersectionObserver 且用户未要求减少动态效果之后才加，
        并带 1.4s 兜底，杜绝「内容永久不可见」这类事故。
     3. 关闭方式有三种，任一命中都让页面完全回到原样：
        未加载 motion.js / 环境不支持 IntersectionObserver / prefers-reduced-motion。
     4. V1.11 起**动效不再进入表1 的滚动路径**：时间轴的行左侧强调条与生命条
        生长两项已移除。滚动一帧只动首尾几行、且都是「元素尚未入 DOM」时写样式，
        因此纵向滚动过程中不会触发任何过渡或动画。
   ========================================================================== */

/* ---------- 1. 交互控件的悬停与按压 ----------
 * 悬停上浮 1px、按下回缩到 0.97，是「有响应」的最便宜来源。
 * 这些元素都是 flex item 或 button，transform 一定生效（行内元素不行，故未纳入）。 */
.chip, .dyn-tab, .china-btn, .copy-btn, .top-nav .nav-link, .intro-related a.ir-item {
  transition: transform var(--t-fast) var(--ease-out),
              background var(--t-fast) var(--ease-io),
              border-color var(--t-fast) var(--ease-io),
              color var(--t-fast) var(--ease-io);
}
.chip:hover, .dyn-tab:hover, .china-btn:hover, .copy-btn:hover,
.top-nav .nav-link:hover, .intro-related a.ir-item:hover { transform: translateY(-1px); }
.chip:active, .dyn-tab:active, .china-btn:active, .copy-btn:active,
.top-nav .nav-link:active, .intro-related a.ir-item:active { transform: translateY(0) scale(.97); }

/* ---------- 2. 表格行：左侧强调条滑出 ----------
 * <tr> 上不渲染伪元素，所以画在首格的内阴影上。过渡只声明在首格，
 * 约 2000 个单元格有声明但不触发就是零成本。 */
.table-scroll tbody td:first-child,
.events-scroll tbody td:first-child { transition: box-shadow var(--t-fast) var(--ease-out); }
.table-scroll tbody tr:hover td:first-child,
.events-scroll tbody tr:hover td:first-child { box-shadow: inset 3px 0 0 var(--motion-accent); }

/* ---------- 3. 表头：当前排序列的下划线滑出 ----------
 * 钩子是已经存在的 aria-sort（app.js 每次排序都会写），因此不必改一行 JS。
 * thead th 本身就是 position:sticky，是定位元素，::after 可循它定位，无需再加 relative。 */
thead th[data-key]:not(.no-sort)::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
  background: var(--motion-accent); transform: scaleX(0); transform-origin: 50% 100%;
  transition: transform var(--t-base) var(--ease-out);
}
thead th[aria-sort="ascending"]::after,
thead th[aria-sort="descending"]::after { transform: scaleX(1); }
thead th[data-key]:not(.no-sort):active { transform: translateY(1px); }

/* ---------- 4. 开卷序列：首屏四拍落位 ----------
 * 只播一次，且只动首屏的四个元素。延迟由 CSS 写死，比 JS 排队更省事。
 * 用 animation 的 both 填充：延迟期间停在起始态，类一摘就回到常驻态，
 * 因此不存在「摘类后闪一下」。 */
html.anim-ready.intro .site-head .seal { animation: mIntro .5s var(--ease-out) .05s both; }
html.anim-ready.intro .site-head h1   { animation: mIntro .5s var(--ease-out) .13s both; }
html.anim-ready.intro .site-head .rule{ animation: mIntro .5s var(--ease-out) .21s both; }
html.anim-ready.intro .top-nav        { animation: mIntro .5s var(--ease-out) .29s both; }
@keyframes mIntro { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* ---------- 5. 卡片错峰入场 ----------
 * 只对 33 张卡片外壳（时间轴 1 + 表格 1 + 内容表 28 + 信息面板 2 + 地图 1 中的非名句条项），
 * 不对任何表格行 —— 行数以千计，且时间轴的行会随虚拟窗口平移而换入换出，
 * 行级动画必然被反复重放（V1.11 即因此撤掉了时间轴的行强调条与生命条生长）。
 * 错峰延迟由 motion.js 按同屏次序写入 --rd（上限 180ms）。 */
html.anim-ready .reveal { opacity: 0; transform: translateY(var(--reveal-y)); }
html.anim-ready .reveal.is-in {
  opacity: 1; transform: none;
  transition: opacity var(--t-slow) var(--ease-out) var(--rd, 0ms),
              transform var(--t-slow) var(--ease-out) var(--rd, 0ms);
}

/* ---------- 6. 表格滚动容器的柔化下边缘 ----------
 * 29 个滚动容器（.table-scroll 1 + .events-scroll 28）在「下面还有内容」时，
 * 底边 24px 淡出。用 position:sticky 的伪元素实现：它随滚动停在容器底边，
 * 且负 margin 抵消自身高度，不改变滚动高度。
 * 只做下边缘：上边缘已被吸顶表头（含边框与投影）表达，再加一层会与表头打架。 */
.scroll-mask { position: relative; }
.scroll-mask::after {
  content: ""; position: sticky; bottom: 0; left: 0; display: block;
  height: 24px; margin-top: -24px; z-index: 5; pointer-events: none;
  opacity: 0; transition: opacity var(--t-base) var(--ease-out);
  background: linear-gradient(0deg, var(--card) 12%, rgba(255, 255, 255, 0));
}
.scroll-mask.more-bottom::after { opacity: 1; }

/* ---------- 7. 二级窗口的面板入场 ----------
 * 现在弹窗只有整层 0.2s 淡入（relFade），面板本身没有任何动效。
 * 这里给面板补一次「上浮 + 极轻收缩」，位移与缩放都不影响 fit() —— fit() 量的是
 * svg.clientWidth/clientHeight，那是布局尺寸，不受 transform 影响。
 * 动画默认 fill-mode（不回填），播完即回到无 transform 的常驻态。
 * 名片↔关系图是瞬时切换，必须继续走 no-anim 分支，保持「同面板换内容」的观感。 */
.rel-modal.show .rel-panel { animation: mPanelIn var(--t-slow) var(--ease-out); }
.rel-modal.show.no-anim .rel-panel { animation: none; }
@keyframes mPanelIn {
  from { opacity: 0; transform: translateY(10px) scale(.99); }
  to   { opacity: 1; transform: none; }
}
/* 名片窗口内的三块（头 / 身 / 脚）依次淡入，配合面板的上浮形成层次。
 * 只做 opacity，不再叠加位移 —— 与面板的位移叠在一起会显得晃。 */
.rel-modal.show:not(.no-anim) .intro-panel > * { animation: mFade var(--t-base) var(--ease-out) both; }
.rel-modal.show:not(.no-anim) .intro-panel > *:nth-child(2) { animation-delay: .06s; }
.rel-modal.show:not(.no-anim) .intro-panel > *:nth-child(3) { animation-delay: .12s; }
.rel-modal.show:not(.no-anim) .intro-panel > *:nth-child(4) { animation-delay: .18s; }
@keyframes mFade { from { opacity: 0; } to { opacity: 1; } }

/* ---------- 8. 焦点环补强 ----------
 * 滚动容器用 outline 而不是 box-shadow：它们 overflow:auto，内阴影会被内容盖住，
 * 外阴影会被相邻卡片裁掉。改为向内偏移的描边，边缘更清楚。 */
.table-scroll:focus-visible, .events-scroll:focus-visible, .timeline-scroll:focus-visible {
  outline: 2px solid #4a90c9; outline-offset: -3px; box-shadow: none;
}

/* ---------- 9. 关系图：连线绘出 + 节点弹入（V1.09） ----------
 * 线长由 app.js 按节点坐标算好写进 --len —— 不用 pathLength 归一，
 * 免得兼容性差异把实线画成虚线。dashoffset 由 --len 收到 0 就是「画出来」。
 * 节点圆圈单独包了一层 .rel-pop：决定位置的是外层 <g> 的 transform **属性**，
 * 直接动画它会被覆盖掉，所以只在内层做缩放。
 * transform-box: fill-box 让缩放以圆自身（而非整个 SVG 视口）为中心。 */
.rel-edge.draw {
  stroke-dasharray: var(--len, none);
  animation: mDraw .5s var(--ease-out) both;
  animation-delay: calc(var(--i, 0) * 32ms);
}
@keyframes mDraw { from { stroke-dashoffset: var(--len, 0); } to { stroke-dashoffset: 0; } }
.rel-pop.draw {
  transform-box: fill-box; transform-origin: center;
  animation: mPop .42s var(--ease-out) both;
  animation-delay: calc(var(--i, 0) * 26ms);
}
@keyframes mPop { from { opacity: 0; transform: scale(.5); } to { opacity: 1; transform: none; } }
.rel-nlabel.draw {
  animation: mFade .34s var(--ease-out) both;
  animation-delay: calc(var(--i, 0) * 26ms + 130ms);
}

/* ---------- 10. 地图：朝代轮廓水墨渗开（V1.09） ----------
 * pathLength="1" 归一之后，stroke-dasharray: 1 就等于「整条轮廓是一段」，
 * dashoffset 由 1 收到 0 即描边先走一遍；填色到后半程才浮起来，像墨渗进纸里。 */
.china-map-svg .dyn-outline {
  stroke-dasharray: 1;
  animation: mInk .62s var(--ease-out) both;
}
.china-map-svg .dyn-outline.dying { animation: mInkOut .3s var(--ease-io) both; }
@keyframes mInk {
  from { stroke-dashoffset: 1; fill-opacity: 0; }
  55%  { fill-opacity: 0; }
  to   { stroke-dashoffset: 0; fill-opacity: .32; }
}
@keyframes mInkOut { from { opacity: 1; } to { opacity: 0; } }

/* ---------- 11. 吸顶导航：滚过首屏后「落定」（V1.09） ----------
 * 只改配色与投影，**不改任何尺寸**。导航高度被 --nav-h / --bar-h 引用
 * （锚点 scroll-margin 与 31 条名句条的等高都靠它）：一旦让它变矮，
 * 全站锚点定位会整体失准，31 条名句条也会跟着重排。 */
.top-nav {
  transition: background var(--t-base) var(--ease-io),
              box-shadow var(--t-base) var(--ease-out),
              border-color var(--t-base) var(--ease-io);
}
html.nav-settled .top-nav {
  background: rgba(255, 255, 255, .97);
  /* 落定后底色已近乎不透明（.97），模糊在观感上几乎看不出差别，
     但 backdrop-filter 会让浏览器在整页滚动时**每帧重新采样并模糊背后的内容** ——
     那是这条 sticky 导航最贵的一笔开销。滚过首屏就直接关掉。 */
  backdrop-filter: none;
  box-shadow: var(--shadow);
  border-color: var(--border-soft);
}

/* ---------- 12. 打印：一切未播完的入场、柔化边缘与裁切都必须归零 ---------- */
@media print {
  .scroll-mask::after { display: none !important; }
  html.anim-ready .reveal { opacity: 1 !important; transform: none !important; }
  .rel-edge.draw, .rel-pop.draw, .rel-nlabel.draw,
  .dyn-outline, .dyn-outline.dying { animation: none !important; }
  .dyn-outline { stroke-dasharray: none !important; stroke-dashoffset: 0 !important; }
}

/* ---------- 13. 用户要求减少动态效果时：直接解除隐藏与裁切 ----------
 * styles.css 的全局降级块会把时长压到 0.001ms，但「初始隐藏 / 初始裁切」是普通声明，
 * 需要在这里显式解除，避免任何时序意外。 */
@media (prefers-reduced-motion: reduce) {
  html.anim-ready .reveal { opacity: 1 !important; transform: none !important; }
  .scroll-mask::after { opacity: 0 !important; }
  .rel-edge.draw { stroke-dasharray: none !important; stroke-dashoffset: 0 !important; }
  .dyn-outline { stroke-dasharray: none !important; stroke-dashoffset: 0 !important; }
}
