/* ===== 图谱视觉参数（GraphView DOM/SVG 渲染）
 * 颜色令牌统一来自 config/graph_style.THEME → model.style.theme → 容器 --gv-* CSS 变量。
 * 本文件只引用变量（var(--gv-*, 默认值)），不再硬编码业务色。
 */
.graph-view { position: relative; overflow: hidden;
  background: var(--graph-bg, transparent); }
#graph-backdrop { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; }
#graph-nodes { position: absolute; inset: 0; z-index: 2; }
#graph-edges { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; }

/* ========== 氛围呼吸（静止态）：既有节点以“自身颜色”发光呼吸（径向明暗，不位移防晕） ========== */
.g-node.dot.existing {
  animation: nodeGlow var(--hd, 6s) ease-in-out infinite;
  animation-delay: var(--hdel, 0s);
}
@keyframes nodeGlow {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--ndc, #8E99A3) 0%, transparent); }
  50%      { box-shadow: 0 0 var(--fx-glow-blur, 12px) var(--fx-glow-spread, 5px)
             color-mix(in srgb, var(--ndc, #8E99A3) var(--fx-glow-alpha, 32%), transparent); }
}
/* 环带底色缓慢明暗：底色已加深，明暗起伏可感知（时长/明暗范围由 --fx-bg-* 控制） */
#graph-backdrop { animation: bgBreath var(--fx-bg-dur, 9s) ease-in-out infinite; }
@keyframes bgBreath {
  0%, 100% { opacity: var(--fx-bg-lo, .62); }
  50%      { opacity: var(--fx-bg-hi, 1); }
}
/* 动效关闭档：节点呼吸与背景明暗全部停用 */
.graph-view.fx-off .g-node.dot.existing,
.graph-view.fx-off #graph-backdrop,
.graph-view.fx-off .g-node.dot.g-twinkle { animation: none; }

/* 展示用“灯亮灯灭”：慢速平滑淡入淡出（多点关键帧 + 双层光晕统一结构逐帧插值，无跃迁）：
   放大 scale · 整体亮度 filter/brightness · 双层外辉（内紧外柔），均由 --fx-tw-* 驱动 */
.g-node.dot.g-twinkle {
  animation: gTwinkle var(--fx-tw-dur, 3200ms) linear forwards;
  z-index: 3;
}
@keyframes gTwinkle {
  0%, 100% {
    transform: scale(1);
    filter: brightness(1);
    box-shadow:
      0 0 1px 0 color-mix(in srgb, var(--ndc, #8E99A3) 4%, transparent),
      0 0 0 0 transparent;
  }
  20% {
    transform: scale(calc(1 + (var(--fx-tw-scale, 1.45) - 1) * 0.35));
    filter: brightness(calc(1 + (var(--fx-tw-bright, 1.5) - 1) * 0.35));
    box-shadow:
      0 0 calc(var(--fx-tw-blur, 34px) * 0.45) 3px
        color-mix(in srgb, var(--ndc, #8E99A3) calc(var(--fx-tw-alpha, 90%) * 0.4), transparent),
      0 0 calc(var(--fx-tw-blur, 34px) * 1.1) 5px
        color-mix(in srgb, var(--ndc, #8E99A3) calc(var(--fx-tw-alpha, 90%) * 0.22), transparent);
  }
  45% {
    transform: scale(var(--fx-tw-scale, 1.45));
    filter: brightness(var(--fx-tw-bright, 1.5));
    box-shadow:
      0 0 var(--fx-tw-blur, 34px) 8px
        color-mix(in srgb, var(--ndc, #8E99A3) var(--fx-tw-alpha, 90%), transparent),
      0 0 calc(var(--fx-tw-blur, 34px) * 2.6) 14px
        color-mix(in srgb, var(--ndc, #8E99A3) calc(var(--fx-tw-alpha, 90%) * 0.5), transparent);
  }
  70% {
    transform: scale(calc(1 + (var(--fx-tw-scale, 1.45) - 1) * 0.6));
    filter: brightness(calc(1 + (var(--fx-tw-bright, 1.5) - 1) * 0.6));
    box-shadow:
      0 0 calc(var(--fx-tw-blur, 34px) * 0.7) 5px
        color-mix(in srgb, var(--ndc, #8E99A3) calc(var(--fx-tw-alpha, 90%) * 0.6), transparent),
      0 0 calc(var(--fx-tw-blur, 34px) * 1.6) 9px
        color-mix(in srgb, var(--ndc, #8E99A3) calc(var(--fx-tw-alpha, 90%) * 0.3), transparent);
  }
}

/* ---------- 节点（legacy 胶囊兜底） ---------- */
.g-node {
  position: absolute;
  transform: translate(-50%, -50%);
  display: flex; align-items: center; justify-content: center;
  min-width: 56px; min-height: 30px;
  padding: 5px 9px; border-radius: 16px;
  background: var(--gv-capsule-bg, #ffffff);
  border: 2px solid var(--gv-capsule-border, #a09787);   /* 默认，内联类型色覆盖 */
  color: var(--gv-capsule-text, #4a463f); font-size: 12px; font-weight: 600;
  white-space: nowrap;
  opacity: 0;
  transition: border-color .3s, box-shadow .3s;
}
.g-node.visible { opacity: 1; }
/* 新增：轻弹入（不叠加持续光晕，结果主要由边高亮表达） */
.g-node.new { animation: g-popIn .5s cubic-bezier(.2,.9,.3,1.3) forwards; }
/* 焦点：蓝色光晕（推演中，前端 setFocus 使用） */
.g-node.focus {
  border-color: var(--gv-status-focus, #5c8a7c);
  animation: g-pulse 1.4s ease-out infinite;
}

/* ---------- 边 ---------- */
/* 普通（既有）边：浅灰实线 */
.g-edge {
  stroke: var(--gv-edge-default, #e2ded4);
  stroke-width: 1.4;
}
/* 推理完成：新增/更新边不再用语义色区分——统一灰底，由「场景 B·推理完成」的
   一次「虚线方向流动 → 变粗辉光 → 回弹灰边」动画强调（规则见文件末尾场景区） */

/* ---------- 动画（光晕颜色来自主题令牌） ---------- */
@keyframes g-pulse {
  0%, 100% { box-shadow: 0 0 0 0 var(--gv-glow-focus, rgba(92,138,124,.42)); }
  50%      { box-shadow: 0 0 0 14px transparent; }
}
@keyframes g-popIn {
  0%   { transform: translate(-50%,-50%) scale(0); opacity: 0; }
  70%  { transform: translate(-50%,-50%) scale(1.25); opacity: 1; }
  100% { transform: translate(-50%,-50%) scale(1); opacity: 1; }
}

/* info（症状输入 / 起点）节点：空心圆环外观，与实心推导节点区分 */
.g-node.dot.ringish {
  background: transparent;
  border: 2px solid var(--ndc, #8E99A3);
}
/* ========== 圆环簇 · 圆点编码节点（圆环布局模式） ========== */
.g-node.dot {
  min-width: 0; min-height: 0; padding: 0;
  transform: none;               /* 圆心定位用 margin 负值，取消基础 translate 双重位移 */
  border-radius: 50%;
  border: none;                  /* 去白描边：节点以“自身色发光”呼吸，色块更纯粹 */
  box-shadow: none;
  text-indent: -9999px;          /* 无节点内文字；名称以 .g-node-label 外挂 */
  overflow: visible;
}
/* dot 专用弹入（不叠加 translate(-50%,-50%)，避免与 margin 负定位冲突） */
@keyframes g-dotPopIn {
  0%   { transform: scale(0); opacity: 0; }
  70%  { transform: scale(1.25); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
.g-node.dot.new { animation: g-dotPopIn .5s cubic-bezier(.2,.9,.3,1.3) forwards; }
/* 节点名称标签（外挂，不随节点动画缩放）
   轻盈样式：无白底胶囊 → 细字浅色 + 柔和白色光晕护读，节点圆点作为视觉主体 */
.g-node-label {
  z-index: 3;
  line-height: 1.3;
  color: var(--gv-label-text, #6e6559);
  text-shadow:
    0 0 3px var(--app-white, #fff),
    0 1px 2px rgba(255, 255, 255, 0.92);
}

/* ========== 推理时间线（图谱下方文字进展；颜色走 --gv-step-* 主题令牌） ========== */
.g-step {
  display: flex; align-items: flex-start; gap: 8px;
  padding: 4px 2px;
  color: var(--gv-step-text, #4a463f);
  font-size: var(--fs-sm); line-height: 1.55;
}
.g-step-dot {
  flex: none; width: 7px; height: 7px; border-radius: 50%;
  margin-top: 6px;
  background: var(--gv-step-time, #a09787);
}
.g-step.latest {
  background: var(--gv-step-highlight, rgba(92,138,124,.08));
  border-radius: 8px;
  padding: 5px 8px 5px 6px;
  margin: 2px -6px;
}
.g-step.latest .g-step-dot {
  background: var(--gv-status-focus, #5c8a7c);
  box-shadow: 0 0 0 0 var(--gv-status-focus, #5c8a7c);
}
.g-step.latest.in { animation: g-stepIn .5s ease-out; }
.g-step-text { flex: 1; min-width: 0; }
/* 时间前置：一眼判断新旧；最新一步的时间强调 */
.g-step-time {
  flex: none;
  color: var(--gv-step-time, #a09787);
  font-size: 11px; padding-top: 3px; margin: 0 8px 0 0;
}
.g-step.latest .g-step-time {
  color: var(--gv-status-focus, #5c8a7c);
  font-weight: 600;
}
@keyframes g-stepIn {
  0%   { opacity: 0; transform: translateX(-6px); }
  100% { opacity: 1; transform: none; }
}

/* ========== 高密度档（detail，density≥0.75）信息层：环名标注 + 环带图例 ========== */
.gv-ring-name {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .2px;
  fill: var(--gv-label-text, #6e6559);
  opacity: .92;
  pointer-events: none;
}
.gv-legend {
  position: absolute;
  left: 10px; bottom: 8px;
  z-index: 4;
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  max-width: calc(100% - 20px);
  padding: 5px 9px;
  border-radius: 9px;
  background: var(--gv-label-bg, rgba(255,255,255,.84));
  box-shadow: 0 0 0 1px rgba(0,0,0,.03), 0 4px 12px -6px rgba(15,23,42,.18);
  pointer-events: none;
}
.gv-legend-item {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 10.5px;
  line-height: 1;
  color: var(--gv-label-text, #6e6559);
  white-space: nowrap;
}
.gv-legend-dot {
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
}

.g-node.dot.core::after {
  inset: -9px;
  border: 2px solid color-mix(in srgb, var(--ndc, #9E3350) 60%, transparent);
  box-shadow: 0 0 16px 5px color-mix(in srgb, var(--ndc, #9E3350) 44%, transparent);
  animation: coreBreath 6s ease-in-out infinite;
}
@keyframes coreBreath {
  0%, 100% { transform: scale(1.04); opacity: .7; }
  50%      { transform: scale(1.24); opacity: 1; }
}

/* =====================================================================
 * 动效场景区（按场景模块化管理；新增/替换效果时改对应场景块即可）
 *   场景 A · 推理中（gv-inferring）  —— 起点节点“外圈光环扫描”
 *   场景 B · 推理完成（gv-done）     —— 变化边“流动 → 变粗辉光 → 回弹”
 *   静态氛围（idle）：见文件上方 existing 呼吸 / 环带明暗 / twinkle
 * ===================================================================== */

/* ===== 场景 A · 推理中：起点节点外圈光环（替代原先的自身脉冲发光） ===== */
.g-node.dot.focus {
  animation: none;   /* 关闭自身 box-shadow 脉冲，改由 ::after 外圈光环表达 */
}
.g-node.dot.focus::after {
  content: "";
  position: absolute;
  inset: -9px;
  border-radius: 50%;
  border: 2px solid var(--gv-status-focus, #5c8a7c);
  box-shadow: 0 0 10px 2px color-mix(in srgb, var(--gv-status-focus, #5c8a7c) 55%, transparent);
  opacity: 0;
  pointer-events: none;
  animation: gFocusHalo 1.15s ease-out infinite;   /* 扩圈淡出 → 扫描感 */
}
@keyframes gFocusHalo {
  0%   { transform: scale(.9);  opacity: .95; }
  100% { transform: scale(1.55); opacity: 0; }
}

/* ===== 场景 B · 推理完成：变化边「虚线方向流动 → 变粗辉光 → 回弹灰边」 =====
   视觉基础仍为浅灰边（与 existing 一致）；.finish 由前端对当帧 new/updated 边短暂添加 */
.g-edge.finish {
  animation: gEdgeFinish 1.5s ease-in-out forwards;
}
@keyframes gEdgeFinish {
  0%   { stroke-width: 1.4; opacity: .9; }
  20%  { stroke-width: 1.7; opacity: 1;
         stroke-dasharray: 26 12; stroke-dashoffset: 0; }
  46%  { stroke-width: 1.9; opacity: 1;
         stroke-dasharray: 26 12; stroke-dashoffset: -38;   /* 段沿 source→target 扫过 */
         filter: drop-shadow(0 0 4px color-mix(in srgb, var(--gv-edge-default, #dcd6ca) 55%, #fff)); }
  64%  { stroke-width: 4.2; stroke-dasharray: none; stroke-dashoffset: 0;
         filter: drop-shadow(0 0 7px color-mix(in srgb, var(--gv-edge-default, #dcd6ca) 70%, #fff)); }
  82%  { stroke-width: 2.2;
         filter: drop-shadow(0 0 3px color-mix(in srgb, var(--gv-edge-default, #dcd6ca) 45%, #fff)); }
  100% { stroke-width: 1.6; opacity: 1; stroke-dasharray: none; stroke-dashoffset: 0;
         filter: none; }   /* 回弹为灰边静止 */
}

/* fx 关闭档扩展：光环扫描与完成边动画一并停用 */
.graph-view.fx-off .g-node.dot.focus::after,
.graph-view.fx-off .g-edge.finish { animation: none; }

