/* 配色与站点头尾沿用 imchong.github.io / Robot_Joint_Order_Check_Tool 的
   Notion 风暖中性色板：中性色偏暖（#37352f / #191919 一系），强调色是同一支蓝
   （亮 #2d74da、暗 #5b9cf6）。节点与连线的分类色由 data/taxonomy.json 提供，
   不在这里改——那是数据语义，不是站点外观。 */

:root {
  --node-w: 208px;
  --col-gap: 92px;
  --row-gap: 14px;

  /* 全站左右内边距。窄屏改小，并且不小于刘海屏横屏时的安全区。 */
  --pad-base: 24px;
  --pad-x: max(var(--pad-base), env(safe-area-inset-left), env(safe-area-inset-right));
  /* 顶栏与工具栏的实测高度，由 main.js 写入；粘性定位的偏移都读这两个值，
     否则顶栏一换行，下面的东西就会压在它底下。这里的初值是量到之前的兜底，
     取参考站的 56px 顶栏高度。 */
  --header-h: 56px;
  --toolbar-h: 45px;

  --radius: 8px;
  --transition: 0.2s ease;

  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", "Noto Sans SC",
    "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Roboto, Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
}

/* color-scheme 管的是浏览器自己画的那部分：滚动条、下拉列表、输入框内的按钮。
   不声明的话它们一律按浅色画，深色主题下滚动条会是一条白槽。 */
html[data-theme="dark"] {
  color-scheme: dark;
  --bg: #191919;
  --bg-soft: #222222;
  --bg-raised: #222222;
  --bg-hover: #2f2f2f;
  --border: #3d3d3d;
  --border-strong: #4e4e4c;
  --text: #e8e8e4;
  --text-soft: #aaa9a5;
  --text-faint: #9b9a97;
  --accent: #5b9cf6;
  --accent-hover: #7ab3ff;
  --accent-soft: rgba(91, 156, 246, 0.16);
  --accent-weak: #22344d;
  --ok: #6cc48f;
  --bad: #e8918a;
  --warn: #d6a55c;
  --warn-border: #56462a;
  --shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
  --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.45);
  --shadow-up: 0 -10px 30px rgba(0, 0, 0, 0.35);
  --grid: rgba(255, 255, 255, 0.035);
}

html[data-theme="light"] {
  color-scheme: light;
  --bg: #f7f6f3;
  --bg-soft: #ffffff;
  --bg-raised: #ffffff;
  --bg-hover: #f1f0ec;
  --border: #e8e8e4;
  --border-strong: #d6d5d0;
  --text: #37352f;
  --text-soft: #6b6b6b;
  --text-faint: #787774;
  --accent: #2d74da;
  --accent-hover: #1a5bb8;
  --accent-soft: rgba(45, 116, 218, 0.1);
  --accent-weak: #eaf1fc;
  --ok: #2c7a52;
  --bad: #c4554d;
  --warn: #9a6b12;
  --warn-border: #e8d3a3;
  --shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
  --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.14);
  --shadow-up: 0 -10px 30px rgba(0, 0, 0, 0.12);
  --grid: rgba(55, 53, 47, 0.05);
}

:root {
  /* 分类色（节点、通道、奖励分组）来自 data/taxonomy.json，是数据语义不是站点外观，
     所以两个主题共用一份。这个变量是压在那些亮色上的文字色：它必须固定成深色，
     跟着主题翻转反而会在浅色主题下变成亮字压亮底。取值是暖中性色板的最深一档。 */
  --on-data-color: #1f1e1b;

  /* 滚动条：轨道一律透明，深色面板上就不会透出一条浅色槽。
     scrollbar-* 会继承，从根上写一次即可覆盖 Firefox。 */
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}
::-webkit-scrollbar {
  width: 11px;
  height: 11px;
  background: transparent;
}
::-webkit-scrollbar-track {
  background: transparent;
}
::-webkit-scrollbar-thumb {
  background-color: var(--border-strong);
  border: 3px solid transparent;
  background-clip: padding-box;
  border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover {
  background-color: var(--text-faint);
}
::-webkit-scrollbar-corner {
  background: transparent;
}

* {
  box-sizing: border-box;
}

/* 顶栏与工具栏都是粘性的，锚点跳转（顶上的「跳到节点图」）要把这两条的高度让出来，
   否则落点会被压在它们底下。和参考站同一个做法，只是这里多算一层工具栏。
   不开 scroll-behavior: smooth：详情浮层升起时那次 scrollTo 是要立刻到位的。 */
html {
  scroll-padding-top: calc(var(--header-h) + var(--toolbar-h) + 8px);
}

/* 本文件里多个可折叠区块都设了 display: grid/flex，会盖掉 hidden 属性自带的
   display: none（UA 样式优先级更低），所以这里显式兜住。 */
[hidden] {
  display: none !important;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

a {
  color: var(--accent);
}

kbd {
  font-family: var(--font-mono);
  font-size: 11px;
  padding: 1px 5px;
  border: 1px solid var(--border-strong);
  border-bottom-width: 2px;
  border-radius: 4px;
  background: var(--bg-raised);
}

.skip-link {
  position: absolute;
  left: -9999px;
}
.skip-link:focus {
  left: 12px;
  top: 12px;
  z-index: 100;
  padding: 8px 12px;
  background: var(--bg-raised);
  border: 1px solid var(--accent);
  border-radius: 6px;
}

/* ---------- 站点顶栏 ---------- */

/* 结构与 imchong.github.io / Robot_Joint_Order_Check_Tool 一致：
   .site-header 粘性 + 毛玻璃，里面一条 .header-inner 把「站名 — 内容 — 右侧动作」
   排成一行。区别只有一处：那两个站的 header-inner 有 max-width 居中，这里的画布
   是通栏的，所以顶栏也跟着通栏，只留 --pad-x 的左右内边距。 */
.site-header {
  position: sticky;
  top: 0;
  z-index: 30;
  background: var(--bg-soft);
  border-bottom: 1px solid var(--border);
  backdrop-filter: blur(8px);
  padding-top: env(safe-area-inset-top, 0px);
  transition: background var(--transition), border-color var(--transition);
}

.header-inner {
  min-height: 56px;
  padding: 8px var(--pad-x);
  display: flex;
  align-items: center;
  /* 顶栏永远是一行。它是粘性的，多出来的每一行都会在每一屏上常驻。
     flex 默认就是 nowrap，写出来是因为这条是约束而不是顺手的默认值：
     谁要往顶栏里再加东西，得先想清楚让谁先走（见下面两段窄屏规则）。 */
  flex-wrap: nowrap;
  gap: 20px;
}

.site-title {
  display: flex;
  align-items: center;
  gap: 11px;
  min-width: 0;
  color: var(--text);
  text-decoration: none;
}
.site-mark {
  width: 28px;
  height: 28px;
  flex: none;
  color: var(--accent);
  transition: color var(--transition);
}
.site-title-text {
  display: flex;
  flex-direction: column;
  line-height: 1.25;
  min-width: 0;
}
/* 站名本身是中英并置的，两半读的是同一件事；窄屏先丢中文那半，再丢副标题。 */
.site-title-text strong {
  font-size: 1rem;
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.site-title-text small {
  font-size: 12px;
  font-weight: 400;
  color: var(--text-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.site-title:hover {
  color: var(--accent);
  text-decoration: none;
}
.site-title:hover .site-mark {
  color: var(--accent-hover);
}

/* ---------- 项目选择器 ---------- */

.project-picker {
  position: relative;
  margin-left: auto;
}

.picker-trigger {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 190px;
  border: 1px solid var(--border-strong);
  background: var(--bg-raised);
  color: var(--text);
  font: inherit;
  padding: 5px 10px 5px 13px;
  border-radius: var(--radius);
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
}
.picker-trigger:hover {
  border-color: var(--accent);
}
.picker-trigger[aria-expanded="true"] {
  border-color: var(--accent);
  background: var(--bg-hover);
}
.picker-current {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
  flex: 1;
}
.picker-trigger-name {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.picker-trigger-count {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-faint);
  margin-left: auto;
}
.picker-caret {
  color: var(--text-faint);
  font-size: 11px;
}

.picker-panel {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  width: min(380px, 88vw);
  padding: 8px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--bg-raised);
  box-shadow: var(--shadow-lg);
  z-index: 40;
}

.picker-search {
  width: 100%;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  padding: 6px 10px;
  margin-bottom: 6px;
}
.picker-search:focus {
  outline: none;
  border-color: var(--accent);
}
.picker-search::placeholder {
  color: var(--text-faint);
}

.picker-list {
  max-height: min(52vh, 380px);
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* 分组标题浮在列表顶上，浮动范围由 .picker-section 界定——粘性元素只能在父块
   的盒子里浮，所以每组必须自成一层容器，否则几个标题会一起钉在列表顶端叠成
   一团（见 project-picker.js 的 renderList）。 */
.picker-group {
  display: flex;
  flex-direction: column;
  padding: 8px 8px 3px;
  font-size: 11px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-faint);
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--bg-raised);
}
.picker-group small {
  text-transform: none;
  letter-spacing: 0;
  font-size: 11px;
  color: var(--text-faint);
  opacity: 0.8;
}

.picker-option {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 6px 8px;
  border-radius: 7px;
  cursor: pointer;
}
.picker-option.active {
  background: var(--bg-hover);
}
.picker-option.current .picker-name {
  color: var(--accent);
}
.picker-check {
  width: 12px;
  flex: 0 0 12px;
  color: var(--accent);
  font-size: 11px;
  line-height: 1.6;
}
.picker-option-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1;
}
/* 列表按发布时间排序，日期就得看得见，否则顺序像是随手定的。 */
.picker-date {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-faint);
  line-height: 1.6;
  font-variant-numeric: tabular-nums;
}
.picker-name {
  font-size: 13px;
  font-weight: 600;
  line-height: 1.4;
}
.picker-sub {
  font-size: 11.5px;
  color: var(--text-faint);
  line-height: 1.4;
}
.picker-empty {
  margin: 6px 8px;
  font-size: 12.5px;
  color: var(--text-faint);
}

.header-right {
  display: flex;
  align-items: center;
  flex: none;
  gap: 0.75rem;
}

/* 仓库入口。宽屏是「图标 + GitHub」的文字链接，窄屏只留图标（见响应式那一节）：
   图标那份认得出来的是同一个标记，不依赖读得懂那五个字母。 */
.github-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 30px;
  padding: 0 2px;
  font-size: 0.85rem;
  color: var(--text-faint);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--transition), border-color var(--transition);
}
.github-mark {
  width: 17px;
  height: 17px;
  flex: none;
}
.github-link:hover {
  color: var(--accent);
  text-decoration: none;
}

.theme-toggle {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 36px;
  height: 30px;
  padding: 0 0.6rem;
  font: inherit;
  font-size: 1rem;
  line-height: 1;
  color: var(--text-faint);
  background: none;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  cursor: pointer;
  user-select: none;
  transition: border-color var(--transition), color var(--transition);
}
.theme-toggle:hover {
  border-color: var(--accent);
  color: var(--accent);
}

/* 工具栏与讲解面板里的按钮。和参考站的 .btn 同一套：面板底色、深一档的描边、
   8px 圆角，hover 只把描边换成强调色。 */
.icon-btn {
  border: 1px solid var(--border-strong);
  background: var(--bg-soft);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  line-height: 1;
  padding: 7px 12px;
  border-radius: var(--radius);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  transition: background 0.13s, color 0.13s, border-color 0.13s;
}
.icon-btn:hover {
  background: var(--bg-hover);
  border-color: var(--accent);
  color: var(--text);
}
.icon-btn[aria-pressed="true"] {
  background: var(--accent-weak);
  border-color: var(--accent);
  color: var(--text);
}

/* ---------- 加载指示 ---------- */

/* 项目文件是按需加载的，切换到没缓存过的项目会有真实延迟，用一条细进度线兜住。 */
.load-bar {
  position: sticky;
  top: var(--header-h);
  z-index: 25;
  height: 2px;
  overflow: hidden;
  background: var(--border);
}
.load-bar span {
  display: block;
  height: 100%;
  width: 32%;
  background: var(--accent);
  animation: load-slide 0.9s ease-in-out infinite;
}
@keyframes load-slide {
  0% {
    transform: translateX(-100%);
  }
  100% {
    transform: translateX(320%);
  }
}

/* ---------- 项目头 ---------- */

.project-head {
  padding: 12px var(--pad-x) 8px;
  display: flex;
  flex-wrap: wrap;
  gap: 8px 24px;
  align-items: baseline;
}
.project-head h1 {
  margin: 0;
  font-size: 21px;
  letter-spacing: -0.01em;
}
.project-head .ph-sub {
  color: var(--text-soft);
  font-size: 13px;
}
.project-head .ph-tagline {
  flex-basis: 100%;
  color: var(--text-soft);
  margin: 0;
  max-width: 72ch;
}
.project-head .ph-diff {
  flex-basis: 100%;
  margin: 0;
  padding: 6px 10px;
  border-left: 3px solid var(--accent);
  border-radius: 0 5px 5px 0;
  background: var(--accent-soft);
  color: var(--text-soft);
  font-size: 12.5px;
  max-width: 92ch;
}
.project-head .ph-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 8px;
  flex-basis: 100%;
  margin-top: 2px;
}
.fact {
  font-size: 11.5px;
  color: var(--text-soft);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 2px 8px;
  background: var(--bg-soft);
}
.fact b {
  color: var(--text);
  font-family: var(--font-mono);
  font-weight: 600;
}
.project-head .ph-links {
  flex-basis: 100%;
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  font-size: 12px;
}

/* ---------- 工具栏 ---------- */

.toolbar {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  padding: 8px var(--pad-x);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  background: var(--bg-soft);
  position: sticky;
  top: var(--header-h);
  z-index: 20;
}

.mode-switch {
  display: flex;
  gap: 2px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 2px;
}
.mode-btn {
  border: 0;
  background: transparent;
  color: var(--text-soft);
  font: inherit;
  padding: 5px 16px;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.mode-btn:hover {
  color: var(--text);
}
.mode-btn[aria-selected="true"] {
  background: var(--bg-raised);
  color: var(--text);
  font-weight: 600;
  box-shadow: var(--shadow);
}

.toolbar-right {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.filters {
  display: flex;
  align-items: center;
  gap: 5px;
  flex-wrap: wrap;
}
.filter-label {
  font-size: 11.5px;
  color: var(--text-faint);
}
.chip {
  border: 1px solid var(--border-strong);
  background: transparent;
  color: var(--text-soft);
  font: inherit;
  font-size: 11.5px;
  padding: 2px 9px;
  border-radius: 999px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
.chip:hover {
  background: var(--bg-hover);
  border-color: var(--accent);
  color: var(--text);
}
.chip[aria-pressed="true"] {
  color: var(--text);
  border-color: var(--chip-color, var(--border-strong));
  background: color-mix(in srgb, var(--chip-color, var(--accent)) 16%, transparent);
}
.chip .dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--chip-color, var(--text-faint));
}

.mode-summary {
  margin: 10px var(--pad-x) 0;
  max-width: 96ch;
  color: var(--text-soft);
  font-size: 13px;
}

/* ---------- 图例 ---------- */

.legend {
  margin: 12px var(--pad-x) 0;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-soft);
  box-shadow: var(--shadow-sm);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 16px 28px;
}
.legend h3 {
  margin: 0 0 6px;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
}
.legend ul {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 12.5px;
}
.legend li {
  display: flex;
  align-items: baseline;
  gap: 8px;
}
.legend .swatch {
  flex: 0 0 auto;
  width: 12px;
  height: 12px;
  border-radius: 3px;
  margin-top: 3px;
}
.legend .line-sample {
  flex: 0 0 26px;
  height: 0;
  border-top: 2px solid currentColor;
  margin-top: 8px;
}
.legend .line-sample.dashed {
  border-top-style: dashed;
}
.legend .lg-name {
  color: var(--text);
}
.legend .lg-desc {
  color: var(--text-faint);
}
.legend .icon-sample {
  font-family: var(--font-mono);
  color: var(--accent);
  width: 14px;
  text-align: center;
}

/* ---------- 画布 ---------- */

.stage {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  gap: 16px;
  padding: 14px var(--pad-x) 0;
  align-items: start;
}

.canvas {
  position: relative;
  height: min(70vh, 720px);
  min-height: 420px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--bg-soft);
  background-image: radial-gradient(circle at 1px 1px, var(--grid) 1px, transparent 0);
  background-size: 22px 22px;
  overflow: hidden;
  cursor: grab;
  /* 画布自己接管全部触摸手势：单指平移、双指捏合。交给浏览器的话，手指一动
     就变成翻页，图根本拖不动。画布高度刻意留有余地，页面照样能从上下滚走。 */
  touch-action: none;
  /* 从卡片上起手拖画布时不要顺手把卡片上的字选中。要复制文字有详情与表格两处。 */
  user-select: none;
}
.canvas:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.canvas.dragging {
  cursor: grabbing;
}

.viewport {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
  will-change: transform;
}

.edges {
  position: absolute;
  top: 0;
  left: 0;
  overflow: visible;
  pointer-events: none;
}

/* 这一层是张铺满整张图的透明纸，卡片之间大片是空的。不让它吃掉指针事件，
   底下的连线才点得到——只有卡片自己把事件收回来，泳道标题不必（它不响应任何
   操作，让它透光，从它底下穿过的连线就也点得到）。 */
.nodes {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
}
.nodes .node {
  pointer-events: auto;
}

.canvas-hint {
  position: absolute;
  left: 10px;
  bottom: 8px;
  font-size: 11px;
  color: var(--text-faint);
  background: color-mix(in srgb, var(--bg-soft) 88%, transparent);
  padding: 2px 8px;
  border-radius: 5px;
  pointer-events: none;
  transition: opacity 0.4s;
}
/* 读者一上手就说明他已经知道怎么用了，提示语让位给图。 */
.canvas-hint.faded {
  opacity: 0;
}

.canvas-zoom {
  position: absolute;
  right: 8px;
  bottom: 8px;
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.cz-btn {
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: color-mix(in srgb, var(--bg-raised) 90%, transparent);
  color: var(--text-soft);
  font: inherit;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  backdrop-filter: blur(3px);
}
.cz-btn:hover {
  color: var(--text);
  border-color: var(--border-strong);
}

.lane-head {
  position: absolute;
  font-size: 11px;
  letter-spacing: 0.05em;
  color: var(--text-faint);
  text-transform: uppercase;
  white-space: nowrap;
}

/* ---------- 节点卡片 ---------- */

.node {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--node-w);
  padding: 8px 10px 9px;
  border: 1px solid var(--border-strong);
  border-left: 3px solid var(--node-color, var(--border-strong));
  border-radius: var(--radius);
  background: var(--bg-raised);
  box-shadow: var(--shadow);
  text-align: left;
  font: inherit;
  color: inherit;
  cursor: pointer;
  transition: opacity 0.25s, filter 0.25s, box-shadow 0.15s, border-color 0.15s;
}
/* 触屏上 :hover 会在点完之后粘住，读起来像是「这个框被选中了」，所以只给鼠标。 */
@media (hover: hover) {
  .node:hover {
    border-color: var(--node-color, var(--accent));
  }
}
.node:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.node.selected {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-soft), var(--shadow);
}
.node.train-only {
  border-style: dashed;
  background: repeating-linear-gradient(
    -45deg,
    var(--bg-raised),
    var(--bg-raised) 7px,
    color-mix(in srgb, var(--bg-hover) 70%, transparent) 7px,
    color-mix(in srgb, var(--bg-hover) 70%, transparent) 14px
  );
}
.node.deploy-hard {
  border-top-style: dotted;
  border-right-style: dotted;
  border-bottom-style: dotted;
}
/* 仅仿真存在：真机没有这个通道，但推理时照样在用，所以不像 train-only 那样打斜纹
   （斜纹在图例里读作「会消失」），只用一条灰色的外框提示「这一格真机上是空的」。 */
.node.sim-only {
  border-top-style: dashed;
  border-right-style: dashed;
  border-bottom-style: dashed;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text-faint) 30%, transparent), var(--shadow);
}
/* 消融式项目里被补丁改过的模块。整张图与父项目几乎一样，这圈高亮就是「差别在这」。 */
.node.touched {
  border-color: var(--accent);
}
.node.touched::after {
  content: "改动";
  position: absolute;
  top: -8px;
  right: 8px;
  padding: 0 5px;
  border-radius: 7px;
  background: var(--accent);
  color: var(--bg);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.02em;
}
.node.dimmed {
  opacity: 0.22;
  filter: grayscale(0.6);
}
.node.filtered-out {
  opacity: 0.12;
  filter: grayscale(0.75);
  pointer-events: none;
}

/* ---------- 讲解态 ---------- */

/* 正在讲的这一格。压暗留给「还没讲到」，所以当前这格要靠加法（描边 + 光晕）
   跳出来，而不是靠别人变暗——否则往回翻步时整张图会忽明忽暗。 */
.node.tour-current {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft), var(--shadow-lg);
  z-index: 2;
}
.node.tour-current::before {
  content: "";
  position: absolute;
  inset: -6px;
  border-radius: 12px;
  border: 1px solid var(--accent);
  opacity: 0.55;
  pointer-events: none;
  animation: tour-pulse 1.8s ease-out infinite;
}
@keyframes tour-pulse {
  0% {
    opacity: 0.55;
    transform: scale(1);
  }
  70%,
  100% {
    opacity: 0;
    transform: scale(1.045);
  }
}
/* 讲过的保持正常亮度：整条链路随着讲解一格一格亮起来，读者能回看走过的路。 */
.node.tour-done {
  opacity: 1;
}
/* 讲解态下鼠标悬停不再改边框——那一套是给「看链路」用的，会和当前步打架。 */
.canvas.touring .node:hover {
  border-color: var(--border-strong);
}
.canvas.touring .node.tour-current:hover {
  border-color: var(--accent);
}

.node-top {
  display: flex;
  align-items: center;
  gap: 5px;
  margin-bottom: 2px;
  min-height: 15px;
}
.node-class {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 700;
  line-height: 1.4;
  padding: 0 4px;
  border-radius: 3px;
  color: var(--on-data-color);
  background: var(--node-color, var(--text-faint));
}
.node-acq {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-faint);
}
.node-conf {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-faint);
  margin-left: auto;
}
.node-label {
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.35;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.node-sub {
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--text-faint);
  margin-top: 2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.node-dim {
  margin-top: 4px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-soft);
}
.node-dim b {
  color: var(--text);
  font-size: 13px;
}
/* 分量顺序。卡片只有 208px 宽，所以夹到三行、余下的挂在 title 上——
   三行是量出来的：「关节 1…30（…）× (t_x, …, n_z)」这类两段式写法正好放得下，
   夹到两行会把分量元组切一半，而元组才是这一行存在的理由。 */
.node-layout {
  margin-top: 2px;
  font-family: var(--font-mono);
  font-size: 10px;
  line-height: 1.35;
  color: var(--text-faint);
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ---------- 连线 ---------- */

.edge {
  fill: none;
  stroke-width: 1.4;
  transition: opacity 0.25s, stroke-width 0.15s;
}
.edge.dashed {
  stroke-dasharray: 5 4;
}
.edge.dimmed,
.edge.filtered-out {
  opacity: 0.1;
}
.edge.active {
  stroke-width: 2.4;
}
/* 选中的那一条：比 hover 更实，配上同色描边的光晕，在一堆同色的线里也认得出。 */
.edge.selected {
  stroke-width: 3;
  filter: drop-shadow(0 0 4px currentColor);
}

/* 1.4px 的线鼠标都难点，手指更不可能。命中区照着同一条 d 加粗到能点但不画出来；
   pointer-events: stroke 让它只在这条带子上响应，不会把整块包围盒都吃掉。 */
.edge-hit {
  fill: none;
  stroke: transparent;
  stroke-width: 16;
  pointer-events: stroke;
  cursor: pointer;
}
.edge-hit.filtered-out {
  /* 筛掉的线在图上已经近乎看不见，再点得中就成了「点到了看不见的东西」。 */
  pointer-events: none;
}
/* 一条边的线、光点、命中区在同一个 <g> 里，所以 hover 一条就够，不必用 JS 记账。 */
.edge-g:hover .edge {
  stroke-width: 2.6;
}
.edge-g:hover .edge-flow {
  opacity: 1;
}

/* 连线上的数据流动：光点是虚线里的一段短划，动画只推 stroke-dashoffset，一个动画
   周期正好推进一个虚线周期（60 用户单位），于是边多长都无缝循环，也不必按长度分别
   算时长——所有边共用这一条 keyframes，看到的速度天然一致。相位（--flow-delay）
   由 render-graph.js 按起点所在的列给，越靠下游越滞后。 */
.edge-flow {
  --flow-period: 1.5s;
  fill: none;
  stroke-width: 3;
  stroke-linecap: round;
  /* 大点在前、小点在后：不光看得出这条线在动，还看得出往哪个方向动。 */
  stroke-dasharray: 2 5 9 44;
  opacity: 0.8;
  animation: edge-flow var(--flow-period) linear var(--flow-delay, 0s) infinite;
}
@keyframes edge-flow {
  to {
    stroke-dashoffset: -60;
  }
}
/* 当前链路上的流动加亮加快，跟 .edge.active 的加粗是同一件事的两种说法。 */
.edge-flow.active {
  --flow-period: 0.95s;
  stroke-width: 3.8;
  opacity: 1;
}
/* 关掉、压暗、被筛掉时直接不画：省下的不只是那几个像素，还有这几十条路径每帧
   的重绘——压暗的边本来也不该有东西在上面爬。 */
.canvas:not(.flowing) .edge-flow,
.edge-flow.dimmed,
.edge-flow.filtered-out {
  display: none;
}

.edge-label {
  font-family: var(--font-mono);
  font-size: 9.5px;
  fill: var(--text-faint);
  transition: opacity 0.25s;
}
/* 边上的标注也是这条线的一部分，点它跟点线是同一件事。 */
.edge-label {
  pointer-events: auto;
  cursor: pointer;
}
.edge-label.dimmed,
.edge-label.filtered-out {
  opacity: 0.08;
}
.edge-label.filtered-out {
  pointer-events: none;
}
.edge-label:hover {
  fill: var(--text);
}
.edge-label.selected {
  fill: var(--text);
  font-weight: 600;
}
.edge-flow.selected {
  opacity: 1;
}
.channel-label {
  font-size: 10px;
  fill: var(--text-faint);
  letter-spacing: 0.05em;
}

/* ---------- 讲解面板 ---------- */

/* 与详情抽屉占同一列、同一套尺寸：两者互斥出现，换成讲解时这一列的宽度、
   高度、粘性偏移都不该跳一下。 */
.tour {
  border: 1px solid var(--accent);
  border-radius: 12px;
  background: var(--bg-soft);
  padding: 14px 16px 16px;
  max-height: min(70vh, 720px);
  overflow-y: auto;
  overscroll-behavior: contain;
  position: sticky;
  top: calc(var(--header-h) + var(--toolbar-h) + 14px);
}

.tour-head {
  display: flex;
  align-items: center;
  gap: 8px;
}
.tour-kicker {
  display: flex;
  align-items: center;
  gap: 7px;
  flex: 1;
  min-width: 0;
  font-size: 11.5px;
  letter-spacing: 0.04em;
  color: var(--accent);
}
.tour-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  flex: 0 0 auto;
}
.tour-close {
  border: 0;
  background: transparent;
  color: var(--text-faint);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  padding: 2px 4px;
}
.tour-close:hover {
  color: var(--text);
}

.tour-bars {
  margin: 9px 0 8px;
  display: grid;
  gap: 3px;
}
/* 上面一条是「讲到哪儿了」。 */
.tour-progress {
  height: 3px;
  border-radius: 2px;
  background: var(--border);
  overflow: hidden;
}
.tour-progress span {
  display: block;
  height: 100%;
  background: var(--accent);
  transition: width 0.25s ease-out;
}
/* 下面一条是自动播放时这一步还剩多久，读者据此判断要不要按暂停。 */
.tour-dwell {
  height: 2px;
  border-radius: 2px;
  background: transparent;
  overflow: hidden;
}
.tour-dwell span {
  display: block;
  height: 100%;
  width: 0;
  background: color-mix(in srgb, var(--accent) 45%, transparent);
  animation-name: tour-dwell;
  animation-timing-function: linear;
  animation-fill-mode: forwards;
}
@keyframes tour-dwell {
  from {
    width: 0;
  }
  to {
    width: 100%;
  }
}

.tour-meta {
  margin: 0 0 8px;
  font-size: 12px;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
.tour-meta b {
  color: var(--text-soft);
}

/* 进到新一列时的开场白。它解释的是「这一列在整轮里干什么」，不是某个模块。 */
.tour-stage {
  margin-bottom: 10px;
  padding: 8px 10px;
  border-left: 3px solid var(--accent);
  border-radius: 0 7px 7px 0;
  background: var(--accent-soft);
  font-size: 12.5px;
  color: var(--text-soft);
  display: grid;
  gap: 2px;
}
.tour-stage b {
  color: var(--text);
}

.tour-title {
  margin: 0;
  padding-left: 9px;
  border-left: 3px solid var(--border-strong);
  font-size: 16px;
  line-height: 1.35;
}
.tour-sub {
  margin: 4px 0 0 12px;
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-faint);
}

.tour-badges {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin: 9px 0 0;
}

.tour-desc {
  margin: 10px 0 0;
  font-size: 13.5px;
  line-height: 1.7;
}
.tour-note {
  margin: 8px 0 0;
  padding-left: 9px;
  border-left: 2px solid var(--border-strong);
  font-size: 12.5px;
  color: var(--text-soft);
}

/* 「上一步给它 / 算完交给 / 绕回去」——图上看得见但字段里没有的那部分。 */
.tour-flow {
  margin-top: 11px;
  padding-top: 9px;
  border-top: 1px solid var(--border);
  display: grid;
  gap: 6px;
}
.tf-row {
  display: grid;
  grid-template-columns: 6.5em minmax(0, 1fr);
  gap: 8px;
  align-items: baseline;
  font-size: 12.5px;
}
.tf-key {
  color: var(--text-faint);
  font-size: 11.5px;
}
.tf-items {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  min-width: 0;
}
.tf-item {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  min-width: 0;
}
.tf-arrow {
  color: var(--accent);
  font-family: var(--font-mono);
}
.tf-name {
  color: var(--text);
}
.tf-edge {
  color: var(--text-faint);
  font-size: 11px;
}

.tour-code {
  margin-top: 9px;
  display: grid;
  grid-template-columns: 6.5em minmax(0, 1fr);
  gap: 8px;
  align-items: baseline;
  font-size: 12px;
  overflow-wrap: anywhere;
}
.tour-code code {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-soft);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0 5px;
  margin-left: 6px;
}

.tour-controls {
  display: flex;
  gap: 6px;
  margin-top: 13px;
}
.tour-controls .icon-btn {
  flex: 1;
  text-align: center;
}
.tour-controls .icon-btn:disabled {
  opacity: 0.4;
  cursor: default;
}
.tour-controls .icon-btn.play {
  border-color: var(--accent);
  color: var(--text);
}
.tour .hint {
  margin: 9px 0 0;
  font-size: 11.5px;
  color: var(--text-faint);
}

/* ---------- 详情抽屉 ---------- */

.drawer {
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--bg-soft);
  padding: 16px;
  max-height: min(70vh, 720px);
  overflow-y: auto;
  position: sticky;
  top: calc(var(--header-h) + var(--toolbar-h) + 14px);
}
.drawer-empty h2 {
  margin: 0 0 8px;
  font-size: 15px;
}
.drawer-empty p {
  margin: 0 0 10px;
  color: var(--text-soft);
  font-size: 13px;
}
.drawer-empty .hint {
  color: var(--text-faint);
  font-size: 12px;
}

.dd-head {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin-bottom: 4px;
}
.dd-head h2 {
  margin: 0;
  font-size: 16px;
  line-height: 1.35;
  flex: 1;
}
.dd-close {
  border: 0;
  background: transparent;
  color: var(--text-faint);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  padding: 2px 4px;
}
.dd-close:hover {
  color: var(--text);
}
.dd-sub {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-faint);
  margin: 0 0 12px;
  word-break: break-all;
}
.dd-badges {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-bottom: 12px;
}
.badge {
  font-size: 11px;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  color: var(--text-soft);
}
.badge.strong {
  color: var(--on-data-color);
  border-color: transparent;
}
.badge.warn {
  border-color: var(--warn-border);
  color: var(--warn);
}

.dd-dim {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 10px 12px;
  margin-bottom: 12px;
  background: var(--bg);
}
.dd-dim .dd-dim-val {
  font-family: var(--font-mono);
  font-size: 22px;
  font-weight: 600;
  line-height: 1.1;
}
.dd-dim .dd-dim-expr {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-faint);
  margin-top: 2px;
}
/* 抽屉里不夹行：分量顺序常常是这一格里最长也最有用的一行。 */
.dd-dim .dd-dim-layout {
  font-family: var(--font-mono);
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--text-soft);
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px dashed var(--border);
}

.dd-section {
  margin-bottom: 12px;
}
.dd-section h3 {
  margin: 0 0 3px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
}
.dd-section p {
  margin: 0;
  font-size: 13px;
  color: var(--text-soft);
}
.dd-section.note p {
  border-left: 2px solid var(--accent);
  padding-left: 10px;
  color: var(--text);
}

.dd-kv {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 3px 12px;
  font-size: 12.5px;
}
.dd-kv dt {
  color: var(--text-faint);
}
.dd-kv dd {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 11.5px;
  word-break: break-all;
}

/* 连线详情里的几行是白话而不是路径，别套 dd-kv 那套等宽 + 任意断行。 */
.dd-kv.prose dd {
  font-family: inherit;
  font-size: 12.5px;
  color: var(--text-soft);
  word-break: normal;
}

.dd-links {
  display: flex;
  flex-direction: column;
  gap: 3px;
  font-size: 12px;
}

/* ---------- 抽屉里的连线 ---------- */

.dd-flow-row {
  display: grid;
  grid-template-columns: 5.2em minmax(0, 1fr);
  gap: 4px 8px;
  align-items: baseline;
  margin-bottom: 4px;
}
.dd-flow-key {
  font-size: 11.5px;
  color: var(--text-faint);
}
.dd-flow-items {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  min-width: 0;
}
/* 连线做成按钮而不是一行文字：它是图上那条线的键盘入口，得能 Tab 到、能回车。 */
.dd-edge {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  max-width: 100%;
  border: 1px solid var(--border);
  border-left: 3px solid var(--edge-color, var(--border-strong));
  border-radius: 6px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 12px;
  line-height: 1.3;
  padding: 4px 8px;
  cursor: pointer;
  text-align: left;
  transition: background 0.13s, border-color 0.13s;
}
.dd-edge:hover {
  background: var(--bg-hover);
  border-color: var(--border-strong);
  border-left-color: var(--edge-color, var(--border-strong));
}
.dd-edge-arrow {
  color: var(--edge-color, var(--text-faint));
  font-family: var(--font-mono);
}
.dd-edge-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dd-edge-tag {
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--text-faint);
  flex: 0 0 auto;
}

/* 标题是「A → B」两段，箭头不跟着字号一起挤在中间。 */
.dd-edge-title {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px;
}
.dd-edge-to {
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-weight: 400;
}

/* ---------- 批注 ---------- */

.callouts {
  padding: 18px var(--pad-x) 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 12px;
}
.callout {
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  padding: 11px 14px;
  background: var(--bg-soft);
  box-shadow: var(--shadow-sm);
}
.callout h3 {
  margin: 0 0 4px;
  font-size: 13.5px;
}
.callout p {
  margin: 0;
  font-size: 13px;
  color: var(--text-soft);
}

/* ---------- 通用小节 ---------- */

.section-head {
  padding: 26px var(--pad-x) 0;
}
.section-head h2 {
  margin: 0;
  font-size: 17px;
}
.section-head p {
  margin: 4px 0 0;
  color: var(--text-soft);
  max-width: 92ch;
}

/* ---------- 奖励面板 ---------- */

.reward-groups {
  padding: 12px var(--pad-x) 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.reward-group {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-soft);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}
.reward-group > summary {
  padding: 9px 14px;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 10px;
  list-style: none;
  font-weight: 600;
  border-left: 3px solid var(--group-color, var(--accent));
}
.reward-group > summary::-webkit-details-marker {
  display: none;
}
.reward-group > summary::after {
  content: "▾";
  margin-left: auto;
  color: var(--text-faint);
  font-size: 11px;
}
.reward-group[open] > summary::after {
  content: "▴";
}
.rg-code {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 700;
  color: var(--on-data-color);
  background: var(--group-color, var(--accent));
  padding: 0 5px;
  border-radius: 3px;
}
.rg-count {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-faint);
  font-weight: 400;
}
.rg-desc {
  font-weight: 400;
  font-size: 12px;
  color: var(--text-faint);
}
.reward-list {
  border-top: 1px solid var(--border);
}
.reward-item {
  display: grid;
  grid-template-columns: 232px 150px minmax(0, 1fr);
  gap: 8px 18px;
  padding: 11px 14px;
  border-bottom: 1px solid var(--border);
  align-items: start;
}
.reward-item:last-child {
  border-bottom: 0;
}
.ri-name {
  font-weight: 600;
  font-size: 13px;
}
.ri-id {
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--text-faint);
  word-break: break-all;
}
.ri-shared {
  display: inline-block;
  font-size: 10px;
  padding: 0 5px;
  border-radius: 3px;
  border: 1px solid var(--border-strong);
  color: var(--text-faint);
  margin-top: 3px;
}
/* 学习式奖励：双线左框 + 来源标签，与手写项一眼分开。
   双线需要 ≥4px 才看得出是两条，3px 会退化成一条细线。
   这里的黄同样是 taxonomy 的分类色，不随主题变。 */
.reward-item.learned {
  border-left: 4px double #ffd166;
  background: color-mix(in srgb, #ffd166 6%, transparent);
}
.ri-kind {
  display: inline-block;
  font-size: 10px;
  padding: 0 5px;
  border-radius: 3px;
  border: 1px double #ffd166;
  color: var(--text-soft);
  margin-top: 3px;
}
.ri-weight-hint {
  font-size: 10px;
  color: var(--text-faint);
}
.ri-model {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 2px 10px;
  margin: 0 0 6px;
  padding: 7px 9px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg-hover);
  font-size: 12px;
}
.ri-model dt {
  color: var(--text-soft);
  white-space: nowrap;
  font-weight: 600;
}
.ri-model dd {
  margin: 0;
  color: var(--text-soft);
}
.ri-weight {
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.ri-weight-val {
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 600;
}
.ri-weight-val.pos {
  color: var(--ok);
}
.ri-weight-val.neg {
  color: var(--bad);
}
.ri-bar {
  height: 5px;
  border-radius: 3px;
  background: var(--bg-hover);
  position: relative;
  overflow: hidden;
}
.ri-bar span {
  position: absolute;
  top: 0;
  bottom: 0;
  border-radius: 3px;
}
.ri-body p {
  margin: 0 0 5px;
  font-size: 12.5px;
  color: var(--text-soft);
}
.ri-formula {
  margin-bottom: 5px;
  font-size: 13px;
  overflow-x: auto;
}
.ri-formula .katex {
  color: var(--text);
}
.ri-meta {
  font-size: 11.5px;
  color: var(--text-faint);
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
}
.ri-meta code {
  font-family: var(--font-mono);
  color: var(--text-soft);
}
.ri-note {
  margin: 4px 0 0;
  font-size: 12px;
  color: var(--text);
  border-left: 2px solid var(--border-strong);
  padding-left: 9px;
}
.reward-scale-note {
  padding: 8px var(--pad-x) 0;
  font-size: 12px;
  color: var(--text-faint);
}

/* ---------- 对比视图 ---------- */

.cmp-controls {
  padding: 8px var(--pad-x) 0;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 13px;
  color: var(--text-soft);
}
.cmp-select {
  border: 1px solid var(--border-strong);
  border-radius: 7px;
  background: var(--bg-raised);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  padding: 4px 8px;
  cursor: pointer;
}
.cmp-select:focus {
  outline: none;
  border-color: var(--accent);
}

.cmp-table-wrap {
  padding: 12px var(--pad-x) 0;
  overflow-x: auto;
}
table.cmp {
  border-collapse: collapse;
  width: 100%;
  min-width: 620px;
  font-size: 13px;
}
table.cmp th,
table.cmp td {
  border: 1px solid var(--border);
  padding: 7px 11px;
  text-align: left;
  vertical-align: top;
}
table.cmp thead th {
  background: var(--bg-soft);
  font-size: 12px;
  color: var(--text-soft);
}
table.cmp tbody th {
  background: var(--bg-soft);
  font-weight: 500;
  color: var(--text-soft);
  white-space: nowrap;
}
table.cmp td {
  font-family: var(--font-mono);
  font-size: 12px;
}
table.cmp td.prose {
  font-family: var(--font-sans);
  font-size: 12.5px;
  color: var(--text-soft);
}
.cmp-mark {
  font-family: var(--font-mono);
  font-weight: 700;
}
.cmp-mark.same {
  color: var(--text-faint);
}
.cmp-mark.only {
  color: var(--warn);
}
.cmp-diff {
  color: var(--warn);
}

/* ---------- 表格视图 ---------- */

.tv-wrap {
  padding: 12px var(--pad-x) 0;
  /* 宽屏故意不写 overflow-x。这一层一旦变成滚动容器，sticky 表头的包含块
     就不再是视口：原先 top: var(--header-h) 会把整行表头错位到第一行数据上。 */
  /* KaTeX 每条公式都带一份 position: absolute 的 MathML（读屏与复制用，视觉上
     裁成 1px）。没有这条 relative，它的包含块是初始包含块，窄屏下就绕过本层的
     overflow-x 跑到表外，把整页撑出一条横向滚动。relative 不会让本层变成滚动
     容器，上面那条约束仍然成立。 */
  position: relative;
}

/* 表格比屏幕宽是刻意的（列不能合并，合并了就没法逐项核对），但横滑这件事
   在手机上没有滚动条提示，得用一句话讲出来。宽屏放得下，不显示。 */
.table-scroll-hint {
  display: none;
  margin: 6px 0 0;
  padding: 0 var(--pad-x);
  font-size: 11.5px;
  color: var(--text-faint);
}
table.tv {
  /* collapse + sticky th 在 Chromium 里会让表头列宽跟表体脱节。
     separate + 0 间距画出同一套 1px 格线，粘性定位才跟列对齐。 */
  border-collapse: separate;
  border-spacing: 0;
  width: 100%;
  min-width: 900px;
  font-size: 12.5px;
}
table.tv th,
table.tv td {
  border: 1px solid var(--border);
  border-width: 0 1px 1px 0;
  padding: 5px 9px;
  text-align: left;
  vertical-align: top;
  /* 中文默认可以在任意字之间断行，列一窄就断成一列一个字、行高涨到十几行。
     keep-all 让列宽跟着词走，宽度不够时由外层横向滚动兜住。 */
  word-break: keep-all;
}
table.tv tr > *:first-child {
  border-left-width: 1px;
}
table.tv thead th {
  border-top-width: 1px;
  background: var(--bg-soft);
  position: sticky;
  /* 视口是包含块时，贴在顶栏 + 工具栏下面。窄屏 wrap 自己横滑时，
     下面的媒体查询会把 top 改成 0，避免再按顶栏高度把表头推下去。 */
  top: calc(var(--header-h) + var(--toolbar-h));
  z-index: 2;
  font-size: 11.5px;
  color: var(--text-soft);
}
table.tv td.num {
  font-family: var(--font-mono);
  text-align: right;
  /* KaTeX 会在二元运算符处断行，−2.5×10⁻⁶ 断成两行就不像一个数了。 */
  white-space: nowrap;
}
table.tv td.mono {
  font-family: var(--font-mono);
  font-size: 11px;
}
/* 「形式」「维度构成」「训练噪声」整格一条公式，交给 KaTeX 排，不走等宽字体
   （等宽是源码的样子）。公式可能比列宽长：让这一格自己横滑，而不是把整张表撑宽。 */
.tv-formula {
  max-width: 320px;
  overflow-x: auto;
  font-size: 12px;
}
/* 「参数」列一格可能并排好几条，行内排；一条参数内部不断行，要断在两条之间。 */
.tv-inline {
  display: inline-block;
  white-space: nowrap;
}
.tv-params {
  font-size: 11.5px;
}
/* 「分量顺序」列。整格是一串坐标名，不该被 keep-all 撑成一条长行。 */
table.tv td.tv-layout {
  font-family: var(--font-mono);
  font-size: 11px;
  min-width: 200px;
  max-width: 340px;
  word-break: break-word;
}
.tv-formula .katex,
.tv-inline .katex,
table.tv td.num .katex {
  color: var(--text);
}
/* KaTeX 没加载出来时的兜底文本，仍按等宽排。 */
.tv-formula code,
.tv-inline code,
.tv-params code {
  font-family: var(--font-mono);
  font-size: 11px;
}
table.tv caption {
  text-align: left;
  padding: 10px 0 6px;
  color: var(--text-soft);
  font-size: 13px;
}

/* ---------- 页脚 ---------- */

/* ---------- 站点页脚 ---------- */

/* 同样对齐参考站：一条上边框把页脚和正文分开，字号压到 12.5px、用最淡的一档
   字色，最后一行是署名。 */
.site-footer {
  margin-top: 34px;
  padding: 22px var(--pad-x) calc(40px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--border);
  color: var(--text-faint);
  font-size: 12.5px;
}
.site-footer p {
  margin: 0 0 6px;
}
.site-footer a {
  color: var(--accent);
}
.footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 18px;
}
/* 署名行和上面的正文信息拉开一档，和参考站的 .footer-copyright 一致：
   链接不抢强调色，只留一条下划线。 */
.footer-copyright {
  margin-top: 14px;
}
.footer-copyright a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* ---------- 响应式 ---------- */

@media (max-width: 1080px) {
  .stage {
    grid-template-columns: minmax(0, 1fr);
  }
  .drawer,
  .tour {
    position: static;
    max-height: none;
  }
  .reward-item {
    grid-template-columns: 1fr;
    gap: 6px;
  }
  /* 中文站名「机器人学习输入输出分析板」比原先短名长一截，
     这一档先收起中文半，把横向空间留给项目选择器。 */
  .site-title-zh {
    display: none;
  }
}

/* ---------- 窄屏（手机竖屏为主） ---------- */

@media (max-width: 720px) {
  :root {
    --pad-base: 14px;
  }

  /* 顶栏必须保持一行：它是粘性的，换成两行就等于常驻吃掉一屏的 1/6。
     让位顺序是「副标题 → 站名的中文半 → 站名英文 → GitHub 那五个字母」，项目选择器
     一直留着，它是这个页面唯一的导航；GitHub 只让出文字，图标按钮留在顶栏里。 */
  .header-inner {
    gap: 10px;
  }
  .site-title {
    flex: 0 1 auto;
    min-width: 0;
    gap: 8px;
  }
  .site-mark {
    width: 24px;
    height: 24px;
  }
  .site-title-text small,
  .site-title-zh {
    display: none;
  }
  .site-title-text strong {
    font-size: 13px;
  }
  .project-picker {
    flex: 1 1 auto;
    min-width: 0;
  }
  .picker-trigger {
    width: 100%;
    min-width: 0;
    padding: 7px 10px 7px 12px;
  }
  /* 顶栏的 GitHub 入口收成图标：文字那半约 48px，收掉之后剩一个和主题按钮
     同尺寸的方钮，省下的宽度全归项目选择器——它是这个页面唯一的导航。
     标记本身留着，源码入口在手机上和在桌面上一样是一眼能找到的东西。 */
  .header-right {
    gap: 8px;
  }
  .github-link {
    justify-content: center;
    min-width: 36px;
    padding: 0;
    border: 1px solid var(--border-strong);
    border-radius: 6px;
  }
  .github-link:hover {
    border-color: var(--accent);
  }
  .github-label {
    display: none;
  }
  /* 下拉在窄屏上贴着两边铺开，并且用 fixed 脱离顶栏，免得列表被顶栏裁掉。 */
  .picker-panel {
    position: fixed;
    left: var(--pad-x);
    right: var(--pad-x);
    top: calc(var(--header-h) + 6px);
    width: auto;
    display: flex;
    flex-direction: column;
    max-height: calc(100vh - var(--header-h) - 24px);
    max-height: calc(100dvh - var(--header-h) - 24px);
  }
  .picker-list {
    flex: 1;
    max-height: none;
  }
  /* 16px 是 iOS Safari 不自动放大页面的下限，输入框比这小一律触发缩放。 */
  .picker-search {
    font-size: 16px;
    padding: 9px 12px;
  }
  .picker-option {
    padding: 9px 8px;
  }
  .picker-name {
    font-size: 14px;
  }

  .icon-btn {
    padding: 8px 12px;
  }

  /* 工具栏不再粘性：模式、视图、筛选三组加起来至少两行，粘着就把画布挤没了。 */
  .toolbar {
    position: static;
    gap: 8px;
    padding: 8px var(--pad-x);
  }
  .mode-switch {
    flex: 1 1 calc(50% - 4px);
  }
  .mode-btn {
    flex: 1;
    padding: 7px 6px;
    text-align: center;
    font-size: 12.5px;
  }
  .toolbar-right {
    margin-left: 0;
    width: 100%;
    gap: 8px;
  }
  /* 筛选按钮横向滑动，而不是折成三四行——它们是次要控件，不该占掉半屏。
     留在「图例」同一行里，工具栏总高度因此是两行而不是三行。 */
  .filters {
    flex: 1 1 0;
    min-width: 0;
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    padding-bottom: 2px;
  }
  .filters::-webkit-scrollbar {
    display: none;
  }
  .filter-label,
  .chip {
    flex: 0 0 auto;
  }
  .chip {
    padding: 5px 11px;
    font-size: 12px;
  }
  /* 画布右下角已经有缩放与适应窗口，工具栏里那个按钮就是重复的。 */
  #fit-btn {
    display: none;
  }

  /* 项目头到画布之间的每一行都在把节点图往折叠线以下推，所以这里逐项收紧：
     标题小一号、行距收拢、关键指标改成横滑一行（它本来要占三行）。 */
  .project-head {
    padding: 10px var(--pad-x) 6px;
    gap: 6px 16px;
  }
  .project-head h1 {
    font-size: 19px;
  }
  .project-head .ph-tagline,
  .project-head .ph-diff {
    font-size: 12.5px;
  }
  .project-head .ph-facts {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    padding-bottom: 2px;
  }
  .project-head .ph-facts::-webkit-scrollbar {
    display: none;
  }
  .fact {
    flex: 0 0 auto;
  }
  .mode-summary {
    font-size: 12.5px;
  }

  /* 画布不铺满一屏：它吃掉了全部触摸手势，上下总得留出能滚页面的地方。 */
  .canvas {
    height: min(52vh, 460px);
    height: min(52dvh, 460px);
    min-height: 300px;
    border-radius: var(--radius);
  }
  .cz-btn {
    width: 38px;
    height: 38px;
    font-size: 15px;
  }

  /* 详情在窄屏上改成底部浮层。排在画布下面的话，点一个模块要再滚一屏才看得到，
     而那一滚正好让人看不见自己点的是哪个框。 */
  .drawer {
    position: fixed;
    left: 0;
    right: 0;
    /* top 得显式清掉：宽屏那条规则给了它粘性偏移，而 fixed 盒子同时有 top 和
       height 时 bottom 会被忽略，浮层就停在半空中。 */
    top: auto;
    bottom: 0;
    z-index: 45;
    max-height: min(50vh, 400px);
    max-height: min(50dvh, 400px);
    overflow-y: auto;
    overscroll-behavior: contain;
    border: 1px solid var(--border-strong);
    border-radius: 14px 14px 0 0;
    padding: 10px var(--pad-x) calc(16px + env(safe-area-inset-bottom));
    box-shadow: var(--shadow-up);
    /* 收起时一并 visibility: hidden——详情为空时浮层本身只有把手那么高，
       只靠 translateY(101%) 挪不出屏幕，会在底边露出一条白。 */
    visibility: hidden;
    transform: translateY(101%);
    transition: transform 0.22s ease-out, visibility 0.22s;
  }
  body.detail-open .drawer {
    visibility: visible;
    transform: translateY(0);
  }
  .drawer::before {
    content: "";
    display: block;
    width: 36px;
    height: 4px;
    margin: 0 auto 10px;
    border-radius: 2px;
    background: var(--border-strong);
  }
  /* 空状态那段是给鼠标和键盘读者的引导，浮层收起时本来就看不见。 */
  .drawer-empty {
    display: none;
  }

  .dd-close {
    font-size: 22px;
    padding: 0 6px;
  }

  .callouts {
    grid-template-columns: minmax(0, 1fr);
  }

  .reward-group > summary {
    flex-wrap: wrap;
    padding: 10px var(--pad-x);
  }
  .rg-desc {
    flex-basis: 100%;
  }
  .reward-item {
    padding: 11px var(--pad-x);
  }
  /* 「吃什么 / 正样本 / 网络」这些值本身就长，再挤成两列就每行两三个字。 */
  .ri-model {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }
  .ri-model dt {
    margin-top: 5px;
  }
  .ri-model dt:first-child {
    margin-top: 0;
  }

  .table-scroll-hint {
    display: block;
  }
  /* 表格右侧不留内边距：宽度全给列，横滑到底就是表格的真实边界。
     overflow-x 只在这里打开：宽屏若也写成 auto，sticky 表头会以 wrap
     为包含块，再叠顶栏高度就会整行错位。 */
  .tv-wrap,
  .cmp-table-wrap {
    padding-right: 0;
    overflow-x: auto;
  }
  table.tv thead th {
    top: 0;
  }
  table.tv {
    font-size: 12px;
  }
  table.cmp {
    min-width: 520px;
  }
  .cmp-select {
    font-size: 16px;
    padding: 7px 10px;
  }
}

/* ---------- 小屏手机（560px 及以下，含 430/440 的「Pro Max」一档） ---------- */

/* 560 而不是 480：站名改成「Robot Learning IO Board Online」后一露出来约 250px，
   520px 的机器上项目选择器会被压到只剩「Beyo… 1/9」——那时它已经不是导航了。 */
@media (max-width: 560px) {
  .site-title-text {
    display: none;
  }
}

/* ---------- 矮视口（横屏手机、分屏窗口） ---------- */

/* 宽度够而高度不够时，粘性工具栏会常驻吃掉小半个屏幕，让它跟着页面滚走，
   省下的高度全给画布——横屏本来就是看这张宽图最合适的姿势。 */
@media (max-height: 620px) {
  /* 横屏手机宽度往往超过 720px，窄屏那段规则不生效，于是站名副标题还在，
     顶栏是两行文字、57px 高——在 390px 高的横屏上就是一屏的 1/7 常驻。
     副标题在页脚与文档里都有，顶栏这一行只留站名。 */
  .site-title-text small {
    display: none;
  }
  .header-inner {
    min-height: 0;
    padding-top: 6px;
    padding-bottom: 6px;
  }
  .toolbar {
    position: static;
  }
  table.tv thead th {
    top: var(--header-h);
  }
  .canvas {
    height: min(78vh, 720px);
    height: min(78dvh, 720px);
    min-height: 260px;
  }
}

/* 表 min-width 是 900px，720px 以上、约 980px 以下也会横溢。overflow-x 开在
   wrap 上好让人横滑；一旦开了，sticky 包含块就是这一层，top 只能是 0。
   放在矮视口规则后面，避免横屏手机被改回顶栏高度后又把表头推到第一行上。 */
@media (max-width: 980px) {
  .tv-wrap,
  .cmp-table-wrap {
    overflow-x: auto;
  }
  table.tv thead th {
    top: 0;
  }
}

/* ---------- 讲解面板的贴边形态 ---------- */

/* 讲解面板必须和图同屏：解说词在说「这一格」，读者却得滚一屏才看得到那一格，
   这功能就没了。所以小屏上它脱离文档流贴到边上——贴哪条边取决于缺什么。
   它和详情抽屉互斥出现（见 main.js 的 syncTourPanels），不会两块浮层叠在一起。 */

/* 窄屏（手机竖屏）：缺宽度，贴底。不做滑入动画——它是逐步换内容的，
   每一步滑一次只会让人晕。 */
@media (max-width: 720px) {
  .tour {
    position: fixed;
    left: 0;
    right: 0;
    top: auto;
    bottom: 0;
    z-index: 45;
    max-height: min(52vh, 420px);
    max-height: min(52dvh, 420px);
    overflow-y: auto;
    overscroll-behavior: contain;
    border: 1px solid var(--accent);
    border-radius: 14px 14px 0 0;
    padding: 10px var(--pad-x) calc(14px + env(safe-area-inset-bottom));
    box-shadow: var(--shadow-up);
  }
  /* 手指够得着才算能用：这三个按钮是这块面板的主要交互。 */
  .tour-controls .icon-btn {
    padding: 9px 8px;
  }
}

/* 矮而宽（横屏手机、分屏窗口）：缺的是高度，贴底会把画布压到只剩一百多像素，
   横向反而有的是地方，所以改贴右侧。画布知道自己被挡掉了右边一条，
   会把当前讲到的模块推到左边露出来（见 render-graph.js 的 overlayInsets）。 */
@media (max-height: 620px) and (min-width: 721px) {
  .tour {
    position: fixed;
    left: auto;
    top: calc(var(--header-h) + 8px);
    right: max(8px, env(safe-area-inset-right));
    bottom: max(8px, env(safe-area-inset-bottom));
    width: min(340px, 40vw);
    max-height: none;
    z-index: 45;
    overflow-y: auto;
    overscroll-behavior: contain;
    border-radius: 12px;
    box-shadow: var(--shadow-lg);
  }
}

/* ---------- 触屏 ---------- */

/* 认 pointer 而不是 hover：无鼠标的桌面浏览器也会报 hover: none，但它不是手指。 */
@media (pointer: coarse) {
  /* 快捷键在没有键盘的设备上不存在，写出来只会占地方。 */
  .drawer-empty .hint,
  .tour .hint {
    display: none;
  }
  /* 手指的落点比光标粗，点得中才谈得上好用。 */
  .picker-option,
  .mode-btn,
  .icon-btn,
  .cz-btn {
    min-height: 36px;
  }
  /* 顶栏右侧这两个在窄屏上都是方钮，一起长高，免得并排的两个按钮不一样高。 */
  .github-link,
  .theme-toggle {
    height: 36px;
  }
}

@media print {
  .site-header,
  .toolbar,
  .tour,
  .stage,
  .legend,
  .canvas-hint {
    display: none !important;
  }
  .table-view {
    display: block !important;
  }
  .tv-wrap,
  .cmp-table-wrap {
    overflow: visible;
  }
  /* 纸上没法横滑，公式该多宽就多宽。 */
  .tv-formula {
    max-width: none;
    overflow: visible;
  }
  table.tv thead th {
    position: static;
  }
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
  /* 上面那条会把脉冲环定格成一圈静止的描边，不如不画——当前格靠边框与光晕
     已经足够显眼。停留进度条同理，它一瞬间就满，留着只是条噪声。 */
  .node.tour-current::before,
  .tour-dwell {
    display: none;
  }
  /* 流动光点的全部内容就是「在动」，被上面那条全局提速改成 0.01ms 只会变成一片
     乱闪。它默认跟随系统偏好关着（见 main.js 的 initialFlow），读者若明确开了它，
     就该按原速播而不是抽搐。 */
  .canvas.flowing .edge-flow {
    animation-duration: var(--flow-period) !important;
  }
}
