@charset "UTF-8";
/* ==========================================================================
   Tableau 工具合集 · 公共主题层
   --------------------------------------------------------------------------
   风格方向：A 苹果静谧（骨架 / 留白 / 圆角 / 阴影）
            + F 三色系统（蓝主色 · 琥珀标记 · 青成功态）

   约定：
   1. 本文件只负责「看起来怎么样」，不参与任何业务逻辑。
   2. 所有颜色、圆角、阴影、间距、字号必须走变量，禁止在页面里写死。
   3. 保留旧页面的既有 class 名（如 .result-success），让 JS 无需改动。
   4. 浅色单模式。深色模式将在 :root 之后追加 [data-theme="dark"] 覆盖。
   ========================================================================== */

/* ==========================================================================
   1. 设计 Token
   ========================================================================== */
:root {
  /* —— 中性：背景 —— */
  --bg-page:        #F5F5F7;
  --bg-surface:     #FFFFFF;
  --bg-surface-2:   #FAFAFA;
  --bg-inset:       #EDEDF0;
  --bg-header:      rgba(255, 255, 255, .78);
  --bg-header-solid:#FFFFFF;

  /* —— 中性：文字 —— */
  --text-1:         #1D1D1F;
  --text-2:         #6E6E73;
  --text-3:         #8E8E93;
  --text-on-accent: #FFFFFF;

  /* —— 中性：描边 —— */
  --border:         #E5E5EA;
  --border-soft:    #EDEDF0;
  --border-strong:  #D2D2D7;

  /* —— 主色 · 蓝（主操作 / 链接 / 焦点） —— */
  --accent:         #0071E3;
  --accent-hover:   #0077ED;
  --accent-active:  #006EDB;
  --accent-soft:    #E8F2FD;
  --accent-soft-text: #0058B0;
  --accent-border:  #BBD9F6;
  --accent-ring:    rgba(0, 113, 227, .28);

  /* 信息提示卡专用（比 --accent-soft 再深一档）
     页面底是 #F5F5F7 浅灰，白卡是 #FFFFFF —— 夹在中间的任何灰阶
     明度都太近，只会变成一块边界模糊的斑。必须靠色相拉开。
     蓝色同时是「说明 / 信息」的通行语义色，不抢彩色调色板的戏。 */
  --info-bg:        #DCE9F9;
  --info-border:    #9CC3EE;

  /* —— 辅色 · 琥珀（标记 / 新 / 收藏） —— */
  --amber:          #C2740A;
  --amber-hover:    #A8660A;
  --amber-soft:     #FDF3E7;
  --amber-soft-text:#8F5507;
  --amber-border:   #F0D5A8;

  /* —— 辅色 · 青（成功 / 已验证） —— */
  --teal:           #0F766E;
  --teal-hover:     #0B5D56;
  --teal-soft:      #E6F4F1;
  --teal-soft-text: #0A544E;
  --teal-border:    #A7D8D2;

  /* —— 状态色 —— */
  --success:        var(--teal);
  --success-soft:   var(--teal-soft);
  --success-text:   var(--teal-soft-text);
  --success-border: var(--teal-border);

  --warning:        var(--amber);
  --warning-soft:   var(--amber-soft);
  --warning-text:   var(--amber-soft-text);
  --warning-border: var(--amber-border);

  --danger:         #D70015;
  --danger-soft:    #FFF2F1;
  --danger-text:    #A8000F;
  --danger-border:  #FFCFCC;

  /* 收藏心形：比 --danger 略柔和，专供「加入收藏」语义 */
  --fav:            #E5352B;

  --info:           var(--accent);
  --info-soft:      var(--accent-soft);
  --info-text:      var(--accent-soft-text);
  --info-border:    var(--accent-border);

  /* —— 圆角 —— */
  --r-xs:   6px;
  --r-sm:   10px;
  --r-md:   14px;
  --r-lg:   18px;
  --r-xl:   22px;
  --r-pill: 999px;
  --r-tile: 11px;

  /* —— 阴影 —— */
  --shadow-xs:      0 1px 2px rgba(0, 0, 0, .04);
  --shadow-card:    0 1px 2px rgba(0, 0, 0, .04), 0 8px 24px rgba(0, 0, 0, .05);
  --shadow-hover:   0 2px 4px rgba(0, 0, 0, .05), 0 14px 32px rgba(0, 0, 0, .09);
  --shadow-header:  0 1px 0 rgba(0, 0, 0, .05);
  --shadow-pop:     0 8px 32px rgba(0, 0, 0, .12);

  /* —— 字体 —— */
  --font-sans: -apple-system, BlinkMacSystemFont, "SF Pro SC", "SF Pro Text",
               "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
               "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco,
               "Cascadia Mono", Consolas, "Courier New", monospace;

  /* —— 字号 —— */
  --fs-display: 40px;
  --fs-h1:      32px;
  --fs-h2:      24px;
  --fs-h3:      17px;
  --fs-body:    15px;
  --fs-sm:      13px;
  --fs-caption: 12px;

  /* —— 间距（8pt 栅格） —— */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 40px;
  --sp-8: 48px;
  --sp-9: 64px;

  /* —— 动效 —— */
  --ease:      cubic-bezier(.4, 0, .2, 1);
  --dur-fast:  120ms;
  --dur:       200ms;
  --dur-slow:  320ms;

  /* —— 尺寸 —— */
  --container:    1120px;
  --container-lg: 1280px;      /* 色卡预览等宽版面 */
  --header-h:     52px;
  --ad-h:         72px;
}

/* ==========================================================================
   2. 重置与基础
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + var(--sp-5));
}

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  line-height: 1.6;
  color: var(--text-1);
  background: var(--bg-page);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* 底部为固定广告条预留空间，全站唯一写法 */
  padding-bottom: var(--ad-h);
}

/* 广告条被关闭后回收这段留白（支持 :has() 的浏览器） */
body:has(.ad-banner[hidden]) { padding-bottom: 0; }

/* 没有广告条的页面（如色卡预览）显式回收留白，不依赖 :has() */
body.no-ad { padding-bottom: 0; }

img, svg, video { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }
ul, ol { margin: 0; padding: 0; list-style: none; }

a {
  color: var(--accent);
  text-decoration: none;
  transition: color var(--dur) var(--ease);
}
a:hover { color: var(--accent-hover); }

::selection { background: var(--accent-ring); color: var(--text-1); }

/* ==========================================================================
   3. 排版
   ========================================================================== */
h1, h2, h3, h4 {
  margin: 0;
  font-weight: 500;
  line-height: 1.25;
  color: var(--text-1);
  letter-spacing: -.01em;
}
h1 { font-size: var(--fs-h1); letter-spacing: -.018em; }
h2 { font-size: var(--fs-h2); letter-spacing: -.012em; }
h3 { font-size: var(--fs-h3); letter-spacing: -.005em; }
h4 { font-size: var(--fs-body); }
p  { margin: 0; }

.display {
  font-size: var(--fs-display);
  font-weight: 500;
  line-height: 1.15;
  letter-spacing: -.025em;
}

.muted    { color: var(--text-2); }
.caption  { font-size: var(--fs-caption); color: var(--text-3); }
.mono     { font-family: var(--font-mono); font-size: var(--fs-sm); }

/* ==========================================================================
   4. 布局
   ========================================================================== */
.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--sp-5);
}

.stack   > * + * { margin-top: var(--sp-4); }
.stack-lg > * + * { margin-top: var(--sp-6); }

/* ==========================================================================
   5. 顶栏（毛玻璃 · A 的招牌）
   ========================================================================== */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--bg-header);
  box-shadow: var(--shadow-header);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
}
/* 不支持毛玻璃时退回实色，保证可读性 */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .site-header { background: var(--bg-header-solid); }
}

.site-header__inner {
  height: var(--header-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
}

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-h3);
  font-weight: 500;
  letter-spacing: -.01em;
  color: var(--text-1);
}
.wordmark:hover { color: var(--text-1); }

.site-nav {
  display: flex;
  align-items: center;
  gap: var(--sp-5);
}
.site-nav a {
  font-size: var(--fs-sm);
  color: var(--text-2);
}
.site-nav a:hover { color: var(--text-1); }

/* ==========================================================================
   6. 按钮
   ========================================================================== */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  height: 38px;
  padding: 0 var(--sp-5);
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  font-size: var(--fs-sm);
  font-weight: 500;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              color var(--dur) var(--ease),
              transform var(--dur-fast) var(--ease),
              box-shadow var(--dur) var(--ease);
}
.btn:active { transform: scale(.98); }

.btn-primary {
  background: var(--accent);
  color: var(--text-on-accent);
}
.btn-primary:hover {
  background: var(--accent-hover);
  color: var(--text-on-accent);
  box-shadow: 0 2px 8px var(--accent-ring);
}

.btn-secondary {
  background: var(--bg-surface);
  color: var(--text-1);
  border-color: var(--border);
}
.btn-secondary:hover {
  background: var(--bg-surface-2);
  border-color: var(--border-strong);
  color: var(--text-1);
}

.btn-ghost {
  background: transparent;
  color: var(--accent);
}
.btn-ghost:hover { background: var(--accent-soft); color: var(--accent-hover); }

.btn-soft {
  background: var(--accent-soft);
  color: var(--accent-soft-text);
  border-color: transparent;
}
.btn-soft:hover {
  background: var(--accent-soft);
  border-color: var(--accent-border);
  color: var(--accent-hover);
}

.btn-sm { height: 30px; padding: 0 var(--sp-4); font-size: var(--fs-caption); }
.btn-lg { height: 46px; padding: 0 var(--sp-6); font-size: var(--fs-body); }
.btn-block { width: 100%; }

.btn[disabled], .btn[aria-disabled="true"] {
  opacity: .45;
  pointer-events: none;
}

/* ==========================================================================
   7. 卡片
   ========================================================================== */
.card {
  background: var(--bg-surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
  padding: var(--sp-5);
}

.card-hover {
  transition: transform var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
.card-hover:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-hover);
  border-color: var(--border);
}

/* ==========================================================================
   8. 图标瓦片（F 三色的落点）
   ========================================================================== */
.icon-tile {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: var(--r-tile);
  background: var(--bg-inset);
  color: var(--text-2);
}
.icon-tile svg { width: 22px; height: 22px; }

.icon-tile.is-blue  { background: var(--accent-soft); color: var(--accent-soft-text); }
.icon-tile.is-amber { background: var(--amber-soft);  color: var(--amber-soft-text); }
.icon-tile.is-teal  { background: var(--teal-soft);   color: var(--teal-soft-text); }

.icon-tile.is-sm { width: 32px; height: 32px; border-radius: var(--r-sm); }
.icon-tile.is-sm svg { width: 17px; height: 17px; }

/* ==========================================================================
   9. 标签 / 徽章（F 三色的第二个落点）
   ========================================================================== */
.tag {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  height: 20px;
  padding: 0 var(--sp-2);
  border-radius: var(--r-pill);
  background: var(--bg-inset);
  color: var(--text-2);
  font-size: var(--fs-caption);
  line-height: 1;
  white-space: nowrap;
}
.tag.is-blue  { background: var(--accent-soft); color: var(--accent-soft-text); }
.tag.is-amber { background: var(--amber-soft);  color: var(--amber-soft-text); }
.tag.is-teal  { background: var(--teal-soft);   color: var(--teal-soft-text); }

/* ==========================================================================
   10. 筛选 Chip
   ========================================================================== */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  height: 32px;
  padding: 0 var(--sp-4);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  background: var(--bg-surface);
  color: var(--text-1);
  font-size: var(--fs-sm);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              color var(--dur) var(--ease);
}
.chip:hover { background: var(--bg-surface-2); border-color: var(--border-strong); }
.chip[aria-pressed="true"],
.chip.is-active {
  background: var(--accent-soft);
  border-color: transparent;
  color: var(--accent-soft-text);
}
.chip__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

/* ==========================================================================
   11. 表单
   ========================================================================== */
.field { display: flex; flex-direction: column; gap: var(--sp-2); }
.field > label {
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-1);
}
.field .hint { font-size: var(--fs-caption); color: var(--text-3); }

.input, .select, .textarea {
  width: 100%;
  padding: 10px var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--bg-surface);
  color: var(--text-1);
  font-size: var(--fs-sm);
  transition: border-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
}
.input::placeholder, .textarea::placeholder { color: var(--text-3); }
.input:hover, .select:hover, .textarea:hover { border-color: var(--border-strong); }
.textarea { min-height: 96px; resize: vertical; line-height: 1.6; }
.select {
  appearance: none;
  padding-right: var(--sp-7);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none' stroke='%236E6E73' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 4.5 6 7.5 9 4.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--sp-3) center;
}

/* ==========================================================================
   12. 状态块（沿用旧 class 名，JS 无需改动）
   ========================================================================== */
.result, .result-success, .result-error, .result-warning, .result-info {
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid transparent;
  border-radius: var(--r-md);
  font-size: var(--fs-sm);
  line-height: 1.6;
}

.result-success {
  background: var(--success-soft);
  border-color: var(--success-border);
  color: var(--success-text);
}
.result-error {
  background: var(--danger-soft);
  border-color: var(--danger-border);
  color: var(--danger-text);
}
.result-warning {
  background: var(--warning-soft);
  border-color: var(--warning-border);
  color: var(--warning-text);
}
.result-info {
  background: var(--info-soft);
  border-color: var(--info-border);
  color: var(--info-text);
}

/* ==========================================================================
   13. 进度条
   ========================================================================== */
.progress {
  height: 6px;
  border-radius: var(--r-pill);
  background: var(--bg-inset);
  overflow: hidden;
}
.progress__bar {
  height: 100%;
  width: 0;
  border-radius: var(--r-pill);
  background: var(--accent);
  transition: width var(--dur-slow) var(--ease);
}

/* ==========================================================================
   14. Toast
   ========================================================================== */
.toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--ad-h) + var(--sp-5));
  transform: translate(-50%, 8px);
  z-index: 200;
  padding: 10px var(--sp-4);
  border-radius: var(--r-pill);
  background: var(--text-1);
  color: #fff;
  font-size: var(--fs-sm);
  box-shadow: var(--shadow-pop);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.toast.is-visible { opacity: 1; transform: translate(-50%, 0); }
.toast.is-success { background: var(--teal); }
.toast.is-error   { background: var(--danger); }

/* ==========================================================================
   15. 底部广告条（全站唯一实现）
   ========================================================================== */
.ad-banner {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 150;
  background: var(--bg-header);
  border-top: 1px solid var(--border);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
  transition: transform var(--dur-slow) var(--ease);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .ad-banner { background: var(--bg-header-solid); }
}
.ad-banner[hidden] { display: none; }

.ad-banner__inner {
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--sp-3) var(--sp-5);
  display: flex;
  align-items: center;
  gap: var(--sp-4);
}

.ad-banner__cover {
  width: 34px;
  height: 44px;
  object-fit: cover;
  border-radius: var(--r-xs);
  flex: none;
  box-shadow: var(--shadow-xs);
}

.ad-banner__body { min-width: 0; flex: 1; }
.ad-banner__title {
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-1);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ad-banner__desc {
  font-size: var(--fs-caption);
  color: var(--text-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ad-banner__actions {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex: none;
}

.ad-banner__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--text-3);
  transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.ad-banner__close:hover { background: var(--bg-inset); color: var(--text-1); }
.ad-banner__close svg { width: 13px; height: 13px; }

/* ==========================================================================
   16. 首页：Hero
   ========================================================================== */
.hero {
  padding-block: var(--sp-9) var(--sp-7);
}
.hero__title {
  font-size: var(--fs-display);
  font-weight: 500;
  line-height: 1.15;
  letter-spacing: -.025em;
}
.hero__sub {
  margin-top: var(--sp-4);
  font-size: var(--fs-h3);
  font-weight: 400;
  line-height: 1.6;
  color: var(--text-2);
}

/* ==========================================================================
   17. 首页：工具卡片网格
   ========================================================================== */
.tool-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: var(--sp-5);
  padding-bottom: var(--sp-8);
}

.tool-card {
  position: relative;
  background: var(--bg-surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
  transition: transform var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
.tool-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-hover);
  border-color: var(--border);
}

/* 整卡可点击：可键盘聚焦、可右键新标签打开

   布局用 grid —— 图标与标题**同一行**（图标原先独占一行，那一行明显太空）。
   描述与 CTA 跨满两列，左边缘与图标齐平（用满卡片宽度），标题因与图标并排而缩进。
   关键：.tool-card__body 设为 display:contents，让它的两个子元素直接成为网格项，
   这样标题能和图标并排、描述又能回到最左 —— **零 HTML 改动**。 */
.tool-card__link {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: auto auto 1fr;
  column-gap: var(--sp-3);
  row-gap: var(--sp-2);
  height: 100%;
  padding: var(--sp-5);
  border-radius: var(--r-lg);
  color: inherit;
}
.tool-card__link:hover { color: inherit; }

.tool-card__body { display: contents; }

.tool-card__link > .icon-tile {
  grid-column: 1;
  grid-row: 1;
  align-self: start;
}
.tool-card__title {
  grid-column: 2;
  grid-row: 1;
  align-self: center;
  font-size: var(--fs-h3);
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: -.005em;
}
.tool-card__desc {
  grid-column: 1 / -1;
  grid-row: 2;
  font-size: var(--fs-sm);
  line-height: 1.65;
  color: var(--text-2);
}
.tool-card__cta {
  grid-column: 1 / -1;
  grid-row: 3;
  justify-self: start;   /* 跨列后不要被拉伸成整行宽，保持自适应 */
  align-self: end;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--accent);
}
.tool-card__cta svg {
  width: 14px;
  height: 14px;
  transition: transform var(--dur) var(--ease);
}
.tool-card__link:hover .tool-card__cta svg { transform: translateX(3px); }

/* ==========================================================================
   18. 页脚
   ========================================================================== */
.site-footer {
  border-top: 1px solid var(--border);
  padding-block: var(--sp-6);
  text-align: center;
  font-size: var(--fs-caption);
  color: var(--text-3);
}

/* ==========================================================================
   19. 工具类
   ========================================================================== */
.hidden { display: none !important; }

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.divider { height: 1px; background: var(--border); border: 0; margin: 0; }

/* ==========================================================================
   20. 无障碍：焦点与动效偏好
   ========================================================================== */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ==========================================================================
   21. 响应式
   ========================================================================== */
@media (max-width: 900px) {
  :root { --fs-display: 34px; --fs-h1: 28px; }
  .hero { padding-block: var(--sp-8) var(--sp-6); }
}

@media (max-width: 640px) {
  :root {
    --fs-display: 30px;
    --fs-h1: 25px;
    --fs-h2: 21px;
  }
  .container { padding-inline: var(--sp-4); }
  .hero { padding-block: var(--sp-7) var(--sp-5); }
  .tool-grid { grid-template-columns: 1fr; gap: var(--sp-4); }
  .site-nav { gap: var(--sp-4); }
  .ad-banner__desc { display: none; }
  .ad-banner__inner { gap: var(--sp-3); }
}

@media (max-width: 420px) {
  .ad-banner__cover { display: none; }
  .ad-banner__title { font-size: var(--fs-caption); }
}

/* ==========================================================================
   22. 工具页：返回首页入口
   ========================================================================== */
.page-back {
  width: 100%;
  max-width: 512px;        /* 与 .page-card 同宽，左边缘对齐卡片 */
  margin: 0 auto;
  padding: var(--sp-5) var(--sp-5) 0;
}
.page-back__link {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  font-size: var(--fs-sm);
  color: var(--text-2);
  transition: color var(--dur) var(--ease);
}
.page-back__link:hover { color: var(--accent); }
.page-back__link svg {
  width: 14px;
  height: 14px;
  transition: transform var(--dur) var(--ease);
}
.page-back__link:hover svg { transform: translateX(-2px); }

/* ==========================================================================
   23. 工具页：通用容器（卡片在中央，max-w-lg 与原版一致）
   ========================================================================== */
.page-stage {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: var(--sp-6) var(--sp-5);
  gap: var(--sp-5);
  /* 让卡片在视口内垂直居中；减去顶栏返回入口与底部广告条的高度 */
  min-height: calc(100vh - var(--ad-h) - 48px);
}
/* 广告条关闭后回收这段高度 */
body:has(.ad-banner[hidden]) .page-stage { min-height: calc(100vh - 48px); }
.page-card {
  background: var(--bg-surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
  padding: var(--sp-7);
  width: 100%;
  max-width: 512px;        /* 原 max-w-lg */
}
.page-card__head {
  text-align: center;
  margin-bottom: var(--sp-6);
}
.page-card__title {
  font-size: var(--fs-h2);
  font-weight: 500;
  letter-spacing: -.012em;
  color: var(--text-1);
  margin-bottom: var(--sp-2);
}
.page-card__sub {
  font-size: var(--fs-sm);
  color: var(--text-2);
}

/* 主操作按钮的上下留白 */
.page-card__submit { margin-block: var(--sp-5); }

/* 进度条区块 */
.page-card__progress { margin-bottom: var(--sp-5); }
.progress__text {
  margin-top: var(--sp-2);
  text-align: center;
  font-size: var(--fs-caption);
  color: var(--text-2);
}

/* 底部说明 / 隐私提示 */
.page-note {
  margin-top: var(--sp-5);
  text-align: center;
  font-size: var(--fs-caption);
  color: var(--text-3);
}

/* 卡片内提示条（内凹底色，用于补充说明） */
.page-tip {
  margin-bottom: var(--sp-5);
  padding: var(--sp-3) var(--sp-4);
  background: var(--bg-inset);
  border-radius: var(--r-sm);
  font-size: var(--fs-caption);
  color: var(--text-2);
  text-align: center;
}

/* ==========================================================================
   24. 工具页：输入行（只读文件框 + 浏览按钮）
   ========================================================================== */
.input-row {
  display: flex;
  gap: var(--sp-2);
  margin-bottom: var(--sp-5);
}

/* ==========================================================================
   25. JS innerHTML 兼容层
   --------------------------------------------------------------------------
   工具页的内联 JS 会在 result 容器里生成按钮 /、用 className= 整段替换状态类。
   这些类名是 JS 字符串里写死的，不能改（约束：只改 UI 不动 JS）。
   本节把 JS 用到的 Tailwind 风格类映射到主题变量，保证 JS 渲染出的内容
   自动跟随主题。类名保留 Tailwind 风格以便与 JS 字符串一一对应。
   ========================================================================== */

/* —— JS 在 result 上整段拼接的 class —— */
.p-5     { padding: var(--sp-5); }
.rounded-lg { border-radius: var(--r-sm); }
.mb-5    { margin-bottom: var(--sp-5); }
.max-h-\[500px\] { max-height: 500px; }
.overflow-y-auto { overflow-y: auto; }

/* —— JS 在 result 内层 div 拼出的 field-* 系列样式 —— */
.field-list {
  max-height: 200px;
  overflow-y: auto;
  margin-top: var(--sp-2);
}
.field-item {
  padding: var(--sp-2);
  background: var(--bg-surface-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-xs);
  margin-bottom: var(--sp-1);
  font-size: var(--fs-caption);
}
.field-header {
  font-weight: 500;
  margin-bottom: var(--sp-1);
  color: var(--text-1);
}
.field-content {
  font-family: var(--font-mono);
  background: var(--bg-page);
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--r-xs);
  white-space: pre-wrap;
  word-break: break-all;
  font-size: var(--fs-caption);
  color: var(--text-1);
}
.field-info {
  color: var(--text-2);
  font-size: 11px;
  margin: 2px 0;
}

/* —— 图形列表（tableau_shape_extractor 的 innerHTML 结构） —— */
.image-list {
  max-height: 200px;
  overflow-y: auto;
  margin-top: var(--sp-2);
}
.image-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2);
  background: var(--bg-surface-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-xs);
  margin-bottom: var(--sp-1);
}
.image-item img {
  width: 40px;
  height: 40px;
  flex: none;
  object-fit: cover;
  border-radius: var(--r-xs);
  background: var(--bg-inset);
}
/* 文件名过长时省略，避免把「下载」按钮挤出容器 */
.image-item .flex-1 {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* —— 小工具类 ——
   放在 .bg-blue-500 之前，让按钮的胶囊圆角与主题字号优先生效 */
.flex-1 { flex: 1 1 0%; min-width: 0; }
.text-xs { font-size: var(--fs-caption); }
.text-slate-500 { color: var(--text-2); }
.px-2\.5 { padding-left: 10px; padding-right: 10px; }
.py-1 { padding-top: var(--sp-1); padding-bottom: var(--sp-1); }
.rounded { border-radius: var(--r-xs); }
.mt-3 { margin-top: var(--sp-3); }

/* —— 布局与弹性（JS 生成的容器结构） —— */
.flex { display: flex; }
.flex-col { flex-direction: column; }
.flex-wrap { flex-wrap: wrap; }
.flex-shrink-0 { flex: none; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.gap-2 { gap: var(--sp-2); }

/* —— 定位 —— */
.relative { position: relative; }
.absolute { position: absolute; }
.fixed { position: fixed; }
.top-0 { top: 0; }
.right-0 { right: 0; }
.right-4 { right: var(--sp-4); }
.bottom-4 { bottom: var(--sp-4); }
.z-50 { z-index: 50; }

/* —— 尺寸 —— */
.w-4 { width: 16px; }
.h-4 { height: 16px; }
.h-16 { height: 64px; }
.max-h-\[50vh\] { max-height: 50vh; }

/* —— 间距 —— */
.mb-3 { margin-bottom: var(--sp-3); }
.mb-4 { margin-bottom: var(--sp-4); }
.mb-6 { margin-bottom: var(--sp-6); }
.mt-1 { margin-top: var(--sp-1); }
.pt-3 { padding-top: var(--sp-3); }
.px-4 { padding-left: var(--sp-4); padding-right: var(--sp-4); }
.py-2 { padding-top: var(--sp-2); padding-bottom: var(--sp-2); }

/* —— 文字 —— */
.text-xl { font-size: 20px; }
.font-semibold { font-weight: 500; }
.text-center { text-align: center; }
/* neutral-500/600 统一落到 --text-2：原 #737373 对比度 4.7:1，
   --text-3 (#8E8E93) 只有 2.8:1，正文不达标 */
.text-neutral-500 { color: var(--text-2); }
.text-neutral-600 { color: var(--text-2); }
.text-neutral-800 { color: var(--text-1); }

/* —— 底色 / 描边 / 圆角 / 阴影 —— */
.bg-primary { background-color: var(--accent); }
.border-t { border-top: 1px solid var(--border); }
.border-neutral-200 { border-color: var(--border); }
.rounded-full { border-radius: var(--r-pill); }
.shadow-lg { box-shadow: var(--shadow-pop); }

/* —— 动效 —— */
@keyframes fadeInUp {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}
.animate-fade-in { animation: fadeInUp var(--dur-slow) var(--ease); }

/* —— 占位类：JS 字符串里带着，但视觉由内联 style 或本节其他规则接管 —— */
/* 渐变方向类：真正的渐变由 JS 的 inline style 提供，这里仅防止选择器落空 */
.bg-gradient-to-r { background-image: none; }
/* Font Awesome 基类：已移除 FA 样式表，仅消除 <i> 的默认斜体 */
/* —— JS 拼接的按钮（字段提取 / 图形提取的结果区操作按钮） —— */
.w-full { width: 100%; }
.py-2\.5 { padding-top: 10px; padding-bottom: 10px; }
.bg-blue-500 {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  background-color: var(--accent);
  color: var(--text-on-accent);
  border: 0;
  border-radius: var(--r-pill);
  font-size: var(--fs-sm);
  font-weight: 500;
  line-height: 1.4;
  transition: background-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
}
.bg-blue-500:hover, .hover\:bg-blue-600:hover {
  background-color: var(--accent-hover);
  color: var(--text-on-accent);
  box-shadow: 0 2px 8px var(--accent-ring);
}
.text-white { color: var(--text-on-accent); }
.text-sm { font-size: var(--fs-sm); }
.cursor-pointer { cursor: pointer; }
/* 过渡占位：见第 29 节（Datafam 兼容层）里的统一定义 */

/* ==========================================================================
   26. 色卡预览页组件
   --------------------------------------------------------------------------
   注意：.chart-container / .color-swatch / .selected-indicator 三个类名
   是 JS 的查询选择器，必须原样保留，不能改名。
   ========================================================================== */

/* —— 静态页头（原为 fixed 悬浮条，改为随页面滚动） —— */
.page-head {
  background: var(--bg-header);
  border-bottom: 1px solid var(--border);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .page-head { background: var(--bg-header-solid); }
}
.page-head__inner {
  max-width: var(--container-lg);
  margin-inline: auto;
  padding: var(--sp-4) var(--sp-5);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--sp-4);
}
.page-head__inner > * { justify-self: start; }   /* 第一列按钮回到内容宽度 */
.page-head__title {
  grid-column: 2;
  text-align: center;
  justify-self: center;
  font-size: var(--fs-h3);
  font-weight: 500;
  color: var(--text-1);
}

/* —— 主体两栏：左色卡 / 右图表 —— */
.preview-layout {
  width: 100%;
  max-width: var(--container-lg);
  margin-inline: auto;
  padding: var(--sp-6) var(--sp-5);
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-6);
}
@media (min-width: 1024px) {
  .preview-layout {
    grid-template-columns: 1fr 2fr;
    /* align-items 默认 stretch，让左色卡与右图表上下边缘对齐 */
  }
}

.section-title {
  font-size: var(--fs-h2);
  font-weight: 500;
  letter-spacing: -.012em;
  color: var(--text-1);
}

/* —— 左侧色卡面板 ——
   注意：不要叫 .palette-card，那个名字归 datafam 页的调色板卡片（JS 生成） */
.preview-panel {
  background: var(--bg-surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
  padding: var(--sp-5);
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}
/* JS 会往里塞 <div class="w-full flex flex-col flex-1">，这里撑开高度 */
.preview-panel__body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* —— 右侧图表区 —— */
.preview-main { display: flex; flex-direction: column; gap: var(--sp-4); }
.preview-main__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  flex-wrap: wrap;
}

/* —— 背景色渐变滑轨 ——
   宽度不要改：JS 用 rect.width / 5 计算点击落在第几段 */
.bg-toggle-wrap {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-sm);
  color: var(--text-2);
}
.bg-toggle {
  position: relative;
  width: 112px;
  height: 16px;
  flex: none;
  border-radius: var(--r-pill);
  cursor: pointer;
  background: linear-gradient(to right, #ffffff, #e5e7eb, #6b7280, #374151, #111827);
  box-shadow: inset 0 0 0 1px var(--border);
}
/* JS 取 bgColorToggle 的第一个 div 做滑块，标签顺序不能变 */
.bg-toggle__thumb {
  position: absolute;
  top: -2px;
  left: -2px;
  width: 20px;
  height: 20px;
  background: var(--bg-surface);
  border: 1px solid var(--border-strong);
  border-radius: 50%;
  box-shadow: var(--shadow-xs);
  transition: transform var(--dur-slow) var(--ease);
}

/* —— 图表卡片 —— */
.chart-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-5);
}
@media (min-width: 768px) {
  .chart-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.chart-container {
  height: 320px;
  padding: var(--sp-5);
  background: var(--bg-surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}
.chart-container h3 {
  flex: none;
  font-size: var(--fs-body);
  font-weight: 500;
  color: var(--text-1);
}
/* Chart.js 设了 maintainAspectRatio:false，父容器必须有确定高度 */
.chart-container__canvas {
  flex: 1;
  min-height: 0;
  position: relative;
}

/* —— 色块（JS 生成，56×56） —— */
.color-swatch {
  position: relative;
  width: 56px;
  height: 56px;
  border-radius: var(--r-sm);
  cursor: pointer;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .06);
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.color-swatch:hover {
  transform: scale(1.08);
  box-shadow: var(--shadow-hover);
}

/* —— 选中角标（JS 切换 style.display 为 flex / none） —— */
.selected-indicator {
  position: absolute;
  top: 0;
  right: 0;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--text-on-accent);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 0 0 2px var(--bg-surface);
}
/* 用 CSS 画对勾，替代 Font Awesome：省掉一整个图标库的外部请求。
   这个元素带 class="fa"，必须显式抵消 .fa 的 mask 与底色，否则会画出一个实心方块。 */
.selected-indicator .fa-check {
  display: block;
  width: 7px;
  height: 4px;
  background-color: transparent;
  -webkit-mask-image: none;
          mask-image: none;
  border-left: 1.8px solid currentColor;
  border-bottom: 1.8px solid currentColor;
  transform: rotate(-45deg) translate(1px, -1px);
}

/* ==========================================================================
   27. 图标（CSS mask · 替代 Font Awesome，零外部请求）
       用法：<span class="fa fa-heart" aria-hidden="true"></span>
       颜色跟随 currentColor，尺寸跟随 font-size（1em）
   ========================================================================== */
.fa {
  display: inline-block;
  width: 1em;
  height: 1em;
  flex: none;
  font-style: normal;
  line-height: 1;
  vertical-align: -.125em;
  -webkit-mask: var(--icon) center / contain no-repeat;
          mask: var(--icon) center / contain no-repeat;
}
/* 只有真的匹配到某个图标才上色。
   若某处漏写 --icon，元素就是一个没有底色的 1em 空盒，不会变成实心方块。 */
.fa-heart, .fa-search, .fa-times, .fa-download,
.fa-check-circle, .fa-chevron-down, .fa-check { background-color: currentColor; }

/* 心形为实心路径（fill），其余为 Feather 风格描边（stroke） */
.fa-heart {
  --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='%23000'%3E%3Cpath%20d='M12%2021.35l-1.45-1.32C5.4%2015.36%202%2012.28%202%208.5%202%205.42%204.42%203%207.5%203c1.74%200%203.41.81%204.5%202.09C13.09%203.81%2014.76%203%2016.5%203%2019.58%203%2022%205.42%2022%208.5c0%203.78-3.4%206.86-8.55%2011.54L12%2021.35z'/%3E%3C/svg%3E");
}
.fa-search {
  --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Ccircle%20cx='11'%20cy='11'%20r='7'/%3E%3Cpath%20d='M20.5%2020.5%2016.4%2016.4'/%3E%3C/svg%3E");
}
.fa-times {
  --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M18%206%206%2018M6%206l12%2012'/%3E%3C/svg%3E");
}
.fa-download {
  --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M21%2015v4a2%202%200%200%201-2%202H5a2%202%200%200%201-2-2v-4M7%2010l5%205%205-5M12%2015V3'/%3E%3C/svg%3E");
}
.fa-check-circle {
  --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M22%2011.08V12a10%2010%200%201%201-5.93-9.14'/%3E%3Cpath%20d='M22%204%2012%2014.01%209%2011.01'/%3E%3C/svg%3E");
}
.fa-chevron-down {
  --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M6%209l6%206%206-6'/%3E%3C/svg%3E");
}
.fa-check {
  --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='3.5'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M20%206%209%2017l-5-5'/%3E%3C/svg%3E");
}

/* ==========================================================================
   28. Datafam 调色板页
   ========================================================================== */

/* —— 页头：三行（标题行 / 类型 Tab + 计数 / 筛选工具条） ——
   用 sticky 而非 fixed：不必再猜一个 padding-top 去补偿高度，
   工具条在窄屏换行时也不会把内容顶出视口。 */
.df-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--bg-header);
  border-bottom: 1px solid var(--border);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
          backdrop-filter: saturate(180%) blur(20px);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .df-header { background: var(--bg-header-solid); }
}
.df-header__inner {
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--sp-5);
}

.df-header__row {
  height: var(--header-h);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--sp-4);
}
/* grid 默认 justify-self: stretch —— 不加这条，返回链接会被拉满整个左列，
   左半边全都变成隐形点击区 */
.df-header__row > * { justify-self: start; }
.df-header__title {
  font-size: var(--fs-h3);
  font-weight: 500;
  letter-spacing: -.01em;
  color: var(--text-1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.df-header__actions { justify-self: end; }

.df-header__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  flex-wrap: wrap;
  padding-block: var(--sp-2);
  border-top: 1px solid var(--border-soft);
}

/* —— 类型 Tab（JS 在 .nav-tab 的 active / inactive 之间切换） —— */
.df-tabs { display: flex; gap: var(--sp-1); flex-wrap: wrap; }
.nav-tab {
  height: 30px;
  padding: 0 var(--sp-4);
  border: 0;
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--text-2);
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.nav-tab:hover { background: var(--bg-inset); color: var(--text-1); }
.nav-tab.active { background: var(--accent-soft); color: var(--accent-soft-text); }
.nav-tab.inactive { /* 基础态，JS 用它做「未选中」标记 */ }

.df-header__count {
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
}

/* —— 筛选工具条 —— */
.df-toolbar {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  flex-wrap: wrap;
  padding-block: var(--sp-2) var(--sp-3);
  border-top: 1px solid var(--border-soft);
}
.df-field {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-sm);
  color: var(--text-2);
}
.df-field__label {
  font-size: var(--fs-caption);
  color: var(--text-3);
  white-space: nowrap;
}
.df-field--search { margin-left: auto; }

.df-num { width: 68px; text-align: center; padding-inline: 6px; }
.df-select { width: 168px; }

.df-search { position: relative; width: 220px; }
.df-search .input { padding-left: var(--sp-7); }
.df-search .fa-search {
  position: absolute;
  left: 10px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 13px;
  color: var(--text-3);
  pointer-events: none;
}

/* —— 离散 / 连续 开关 —— */
.df-switch {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  cursor: pointer;
  -webkit-user-select: none;
          user-select: none;
}
.df-switch__input {
  position: absolute;
  width: 1px; height: 1px;
  opacity: 0;
  margin: 0;
}
.df-switch__ui {
  position: relative;
  width: 40px;
  height: 22px;
  flex: none;
  border-radius: var(--r-pill);
  background: var(--text-3);
  transition: background-color var(--dur) var(--ease);
}
.df-switch__ui::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #fff;
  box-shadow: var(--shadow-xs);
  transition: transform var(--dur-slow) var(--ease);
}
.df-switch__input:checked + .df-switch__ui { background: var(--accent); }
.df-switch__input:checked + .df-switch__ui::after { transform: translateX(18px); }
.df-switch__input:focus-visible + .df-switch__ui {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.df-switch__label {
  font-size: var(--fs-sm);
  color: var(--text-2);
  white-space: nowrap;
  transition: color var(--dur) var(--ease);
}
/* 当前生效的一侧加粗 */
.df-switch:has(.df-switch__input:checked) .df-switch__label--continuous,
.df-switch:has(.df-switch__input:not(:checked)) .df-switch__label--discrete {
  color: var(--text-1);
  font-weight: 500;
}

/* —— 主体 —— */
.df-main {
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--sp-6) var(--sp-5) var(--sp-9);
}

/* 顶部引言（原本压在底部、被广告条完全遮住的那段说明）
   两条收起途径：8 秒后自动收起，或点 × 手动关掉。
   特意不做滚动隐藏：元素在视口外时收起，高度突变只会表现为无来由的跳动，
   而在视口内收起，同样的高度变化就是一段看得懂的平滑动画。
   max-height 由 JS 实测后写入——写死常量会裁掉窄屏折行后的长文本。 */
.df-intro-wrap {
  margin-bottom: var(--sp-6);
  overflow: hidden;
  transition: max-height var(--dur-slow) var(--ease),
              margin-bottom var(--dur-slow) var(--ease),
              opacity var(--dur) var(--ease);
}
.df-intro-wrap.is-collapsing {
  margin-bottom: 0;
  opacity: 0;
  pointer-events: none;
}
/* [hidden] 的默认 display:none 会被任何显式 display 规则盖掉，这里补一道 */
.df-intro-wrap[hidden] { display: none; }

.df-intro {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  background: var(--info-bg);
  border: 1px solid var(--info-border);
  border-radius: var(--r-md);
}
/* 图标单独占一列，标题与正文右侧对齐——
   图标若塞进标题行，正文会相对标题左错 20px，看着像没对齐 */
.df-intro__icon {
  flex: none;
  width: 16px;
  height: 16px;
  /* 标题行高约 18px，图标 16px，下移 1px 才是视觉居中 */
  margin-top: 1px;
  color: var(--accent-soft-text);
}
.df-intro__text {
  flex: 1;
  min-width: 0;
}
.df-intro__head {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-caption);
  font-weight: 500;
  letter-spacing: .04em;
  color: var(--accent-soft-text);
}
.df-intro p {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: 1.75;
  color: var(--text-1);
}
/* 关闭按钮：flex 里用 margin-left:auto 顶到最右，
   比绝对定位稳——不用管 padding 与标题行的高度 */
.df-intro__close {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  margin: -2px calc(var(--sp-2) * -1) 0 var(--sp-2);
  padding: 0;
  border: 0;
  border-radius: var(--r-xs);
  background: transparent;
  color: var(--accent-soft-text);
  opacity: .55;
  cursor: pointer;
  transition: opacity var(--dur) var(--ease),
              background-color var(--dur) var(--ease);
}
.df-intro__close:hover {
  opacity: 1;
  background-color: rgba(0, 88, 176, .1);
}
.df-intro__close svg { width: 13px; height: 13px; }

/* —— 调色板列表（JS 注入 h2 分组标题 + .palette-card） —— */
.df-list > h2 {
  margin-top: var(--sp-7);
  font-size: var(--fs-h2);
  font-weight: 500;
  letter-spacing: -.012em;
  color: var(--text-1);
}
.df-list > h2:first-child { margin-top: 0; }
.df-list > :last-child { margin-bottom: 0; }

/* .palette-card 由 JS 生成，名字不能改 */
.palette-card {
  margin-bottom: var(--sp-5);
  padding: var(--sp-5);
  background: var(--bg-surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
  transition: box-shadow var(--dur) var(--ease);
}
.palette-card:hover { box-shadow: var(--shadow-hover); }
.palette-card h3 {
  font-size: var(--fs-h3);
  font-weight: 500;
  letter-spacing: -.01em;
  color: var(--text-1);
}

/* —— 收藏心形（JS 生成 <label class="palette-checkbox"><input class="sr-only peer"><i class="fa fa-heart">） —— */
.palette-checkbox {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  flex: none;
}
.palette-checkbox .fa-heart {
  font-size: 19px;
  background-color: var(--text-3);
  transition: background-color var(--dur) var(--ease), transform var(--dur-fast) var(--ease);
}
.palette-checkbox:hover .fa-heart { background-color: var(--text-2); transform: scale(1.08); }
/* Tailwind 的 peer-checked 在纯 CSS 下的等价写法：input 紧跟 i */
.palette-checkbox input:checked + .fa-heart { background-color: var(--fav); }
.palette-checkbox input:focus-visible + .fa-heart {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* 弹窗里的「取消收藏」按钮 */
.favorite-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-1);
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--fav);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease);
}
.favorite-btn:hover { background: var(--danger-soft); }
.favorite-btn .fa-heart { font-size: 17px; background-color: var(--fav); }

/* —— 载入中 —— */
.df-loading {
  display: flex;
  align-items: center;
  justify-content: center;
  padding-block: var(--sp-9);
}
.spinner {
  width: 28px;
  height: 28px;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: df-spin .7s linear infinite;
}
@keyframes df-spin { to { transform: rotate(360deg); } }

/* —— 收藏弹窗 —— */
.modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-5);
  background: rgba(0, 0, 0, .38);
  -webkit-backdrop-filter: blur(3px);
          backdrop-filter: blur(3px);
}
.modal__panel {
  width: 100%;
  max-width: 900px;
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  background: var(--bg-surface);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-pop);
  overflow: hidden;
  /* JS 切换 scale-95 / scale-100 / opacity-0 / opacity-100 */
  transition: transform var(--dur-slow) var(--ease), opacity var(--dur-slow) var(--ease);
}
.modal__head {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--border);
}
.modal__title {
  font-size: var(--fs-h3);
  font-weight: 500;
  color: var(--text-1);
}
.modal__actions { display: flex; align-items: center; gap: var(--sp-2); flex: none; }
.modal__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--text-3);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.modal__close:hover { background: var(--bg-inset); color: var(--text-1); }
.modal__body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: var(--sp-5);
}
.modal__body > :last-child { margin-bottom: 0; }

/* 窄屏：页头不再粘顶（三行会吃掉太多高度），随页面一起滚走 */
@media (max-width: 720px) {
  .df-header { position: static; }
  .df-header__row { height: auto; padding-block: var(--sp-3); gap: var(--sp-3); }
  .df-header__title { font-size: var(--fs-sm); }
  .df-field--search { margin-left: 0; }
  .df-search { width: 100%; }
  .df-select { width: 140px; }
}

/* ==========================================================================
   29. JS innerHTML 兼容层 · Datafam 页
       以下类名写死在 tableau_datafam_colors_palettes.html 的 JS 字符串里，
       改名会让功能失效，只能在 CSS 侧把它们映射到主题变量。
   ========================================================================== */
.btn-outline {
  height: 30px;
  padding: 0 var(--sp-3);
  border-color: var(--border);
  border-radius: var(--r-pill);
  background: var(--bg-surface);
  color: var(--text-1);
  font-size: var(--fs-caption);
}
.btn-outline:hover { background: var(--bg-surface-2); border-color: var(--border-strong); }

/* 间距 / 尺寸 */
.mr-2 { margin-right: var(--sp-2); }
.mr-3 { margin-right: var(--sp-3); }
.gap-3 { gap: var(--sp-3); }
.mb-8 { margin-bottom: var(--sp-6); }
.p-2  { padding: var(--sp-2); }
.px-3 { padding-left: var(--sp-3); padding-right: var(--sp-3); }
.px-5 { padding-left: var(--sp-5); padding-right: var(--sp-5); }
.py-1\.5 { padding-top: 6px; padding-bottom: 6px; }
.py-3 { padding-top: var(--sp-3); padding-bottom: var(--sp-3); }
.py-12 { padding-top: var(--sp-9); padding-bottom: var(--sp-9); }
.h-12 { height: 48px; }
.rounded-md { border-radius: var(--r-sm); }
.right-6 { right: var(--sp-6); }
.bottom-20 { bottom: calc(var(--ad-h) + 8px); }

/* 文字 */
.text-2xl { font-size: var(--fs-h2); }
.font-medium { font-weight: 500; }
.text-gray-400 { color: var(--text-3); }
.text-gray-800 { color: var(--text-1); }
.text-red-500 { color: var(--danger); }
.hover\:text-red-600:hover { color: var(--danger-text); }

/* 类型徽章：Regular=蓝 / Sequential=青 / Diverging=琥珀（F 三色系统） */
.bg-blue-50    { background-color: var(--accent-soft); }
.text-blue-700 { color: var(--accent-soft-text); }
.bg-green-50    { background-color: var(--teal-soft); }
.text-green-700 { color: var(--teal-soft-text); }
.bg-purple-50    { background-color: var(--amber-soft); }
.text-purple-700 { color: var(--amber-soft-text); }
.bg-neutral-100  { background-color: var(--bg-inset); }
.text-neutral-700 { color: var(--text-2); }

/* 复制成功提示（JS 用 className 整体赋值） */
.bg-green-500 { background-color: var(--teal); }

/* Tailwind peer 语法：CSS 侧无需实现，真实行为见
   「.palette-checkbox input:checked + .fa-heart」，这里只保留名字避免选择器丢失 */
.peer { /* 锚点类 */ }
.peer-checked\:text-red-500 { /* 同上 */ }
.peer-checked\:fill-current { /* 同上 */ }

/* 过渡占位 */
.transition-colors {
  transition: color var(--dur) var(--ease),
              background-color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
.transition-transform { transition: transform var(--dur) var(--ease); }
.transition-all { transition: all var(--dur) var(--ease); }
.duration-200 { transition-duration: var(--dur); }
.duration-300 { transition-duration: var(--dur-slow); }

/* 弹窗缩放 / 透明度（JS 用 classList 切换） */
.scale-95 { transform: scale(.96); }
.scale-100 { transform: scale(1); }
.opacity-0 { opacity: 0; }
.opacity-100 { opacity: 1; }

/* ==========================================================================
   30. Unicode & Emoji 浏览器页
   ========================================================================== */

/* —— 页头（标题 + 搜索条的合并 sticky 单元）
   跟 datafam 的 .df-header 同构：position:sticky + 半透明 + 毛玻璃 + 底边描边。
   视觉上 = 一张浮在浅灰底上的卡片式标题栏，跟数据式调色板的「卡片标题」一致。 */
.uni-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--bg-header);
  border-bottom: 1px solid var(--border);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
          backdrop-filter: saturate(180%) blur(20px);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .uni-header { background: var(--bg-header-solid); }
}
.uni-header__inner {
  max-width: var(--container-lg);
  margin: 0 auto;
  padding: var(--sp-5) var(--sp-5) 0;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--sp-4);
}
/* grid 默认 justify-self: stretch —— 不加这条，返回链接会被拉满整个左列，
   左半边全变成隐形点击区 */
.uni-header__inner > * { justify-self: start; }
.uni-header__title {
  justify-self: center;
  margin: 0;
  font-size: var(--fs-h2);
  font-weight: 500;
  letter-spacing: -.01em;
  color: var(--text-1);
}

/* —— 搜索条：跟标题同一 sticky 单元，不再单独 sticky。
   与标题行之间用一道细微的 border-top 分隔，保留视觉层级但不另起一条毛玻璃。 */
.uni-searchbar {
  border-top: 1px solid var(--border-soft);
}
.uni-searchbar__inner {
  max-width: var(--container-lg);
  margin: 0 auto;
  padding: var(--sp-3) var(--sp-5);
  display: flex;
  justify-content: center;
}
.uni-searchbar__box {
  position: relative;
  width: 100%;
  max-width: 640px;
}
.uni-searchbar__icon {
  position: absolute;
  left: 12px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 14px;
  color: var(--text-3);
  pointer-events: none;
}
.uni-search {
  width: 100%;
  height: 36px;
  padding: 0 var(--sp-4) 0 34px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
  background: var(--bg-surface);
  font-family: inherit;
  font-size: var(--fs-sm);
  color: var(--text-1);
  outline: none;
  transition: border-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
}
.uni-search::placeholder { color: var(--text-3); }
.uni-search:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-ring);
}

/* —— 主体两栏 —— */
.uni-main {
  max-width: var(--container-lg);
  margin: 0 auto;
  padding: var(--sp-6) var(--sp-5) var(--sp-9);
}
.uni-layout {
  display: grid;
  grid-template-columns: 236px minmax(0, 1fr);
  gap: var(--sp-5);
  align-items: start;
}

/* —— 分类侧栏：内容由 JS 注入 —— */
.uni-sidebar {
  padding: var(--sp-2) 0;
  background: var(--bg-surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
  overflow: hidden;
}

/* 主分类：左侧预留 3px 竖条，激活时才上色。
   用预留的透明 border 而非 ::before 伪元素——后者要算定位，
   且激活前后文字位置会横向跳动 */
.uni-sidebar .main-category {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  padding: 9px var(--sp-4) 9px calc(var(--sp-4) - 3px);
  border-left: 3px solid transparent;
  font-size: var(--fs-sm);
  font-weight: 400;
  color: var(--text-1);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease),
              color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
.uni-sidebar .main-category:hover { background-color: var(--bg-inset); }
/* JS 激活时还会加 border-blue-500 / text-blue-600 / font-semibold，
   竖条与文字色交给兼容层里那几个类，这里只补底色与字重 */
.uni-sidebar .main-category.active {
  background-color: var(--accent-soft);
  font-weight: 500;
}

/* JS 用 style.display 在 block / none 之间切换，这里绝不能设 display */
.uni-sidebar .sub-categories {
  padding: var(--sp-1) 0 var(--sp-1) var(--sp-3);
}

.uni-sidebar .sub-category {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  padding: 6px var(--sp-4);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  font-size: var(--fs-sm);
  color: var(--text-2);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease),
              color var(--dur) var(--ease);
}
.uni-sidebar .sub-category:hover { background-color: var(--bg-inset); }
.uni-sidebar .sub-category.active {
  background-color: var(--accent-soft);
  color: var(--accent-soft-text);
  font-weight: 500;
}

/* 选中对勾：尺寸由 JS 的 w-3 / h-3 给出（兼容层映射到 14px），这里只管形状配色 */
.uni-sidebar .check-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  border-radius: 50%;
  background-color: var(--accent);
  color: #fff;
  font-size: 9px;
}
.uni-sidebar .check-icon .fa { width: 9px; height: 9px; }

.uni-sidebar .toggle-icon {
  flex: none;
  font-size: 11px;
  color: var(--text-3);
  transition: transform var(--dur) var(--ease);
}

/* —— 右侧面板 —— */
.uni-panel {
  padding: var(--sp-5);
  background: var(--bg-surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
}
.uni-panel__head {
  margin-bottom: var(--sp-5);
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--border-soft);
}
.uni-panel__title {
  margin: 0;
  font-size: var(--fs-h3);
  font-weight: 500;
  color: var(--text-1);
}

/* —— 符号网格 ——
   用 .uni-grid 后代选择器提高优先级，压过兼容层里 p-4 的 16px 内边距 */
.uni-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
  gap: var(--sp-2);
}
.uni-grid .char-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  min-width: 0;
  padding: 10px 4px;
  border-radius: var(--r-sm);
  text-align: center;
  cursor: pointer;
  transition: background-color var(--dur) var(--ease),
              transform var(--dur) var(--ease);
}
.uni-grid .char-item:hover { background-color: var(--bg-inset); }
.uni-grid .char-item:active { transform: scale(.94); }

.uni-grid .char-symbol {
  font-size: 22px;
  line-height: 1.3;
  color: var(--text-1);
}
.uni-grid .char-name {
  max-width: 100%;
  font-size: 10px;
  line-height: 1.3;
  color: var(--text-2);
  /* 名称普遍很短，个别超长时截到两行，免得把整行格子撑歪 */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

@media (max-width: 900px) {
  .uni-layout { grid-template-columns: 1fr; }
  /* 侧栏挪到上方后限高，否则一进页面就被分类占满整屏 */
  .uni-sidebar { max-height: 300px; overflow-y: auto; }
}
@media (max-width: 640px) {
  .uni-header__inner { grid-template-columns: 1fr; gap: var(--sp-2); }
  .uni-header__title { justify-self: start; }
  .uni-grid { grid-template-columns: repeat(auto-fill, minmax(64px, 1fr)); }
}

/* ==========================================================================
   30b. Unicode 页 Tailwind 兼容层
   JS 字符串里的类名改不了，一律重新定义并映射到主题变量。
   放在第 30 节之后：与上面 .uni-grid .char-item 这类 (0,2,0) 规则冲突时，
   由更高优先级的那方胜出，所以这里的取值只兜底，不覆盖设计意图。
   ========================================================================== */
.text-primary { color: var(--accent); }
.text-neutral-400 { color: var(--text-3); }
.text-\[10px\] { font-size: 10px; }

.w-3  { width: 14px; }
.h-3  { height: 14px; }
.p-4  { padding: 16px; }
.pl-3 { padding-left: var(--sp-3); }
.py-10 { padding-top: var(--sp-9); padding-bottom: var(--sp-9); }
.mb-2 { margin-bottom: var(--sp-2); }
.max-w-full { max-width: 100%; }
/* 格宽已由 grid 的 minmax(80px, 1fr) 决定，再给 min-width 只会互相打架 */
.min-w-\[80px\] { min-width: 0; }
.break-words { overflow-wrap: break-word; word-break: break-word; }
.leading-none { line-height: 1; }
.overflow-hidden { overflow: hidden; }
.space-y-1 > * + * { margin-top: var(--sp-1); }
.col-span-full { grid-column: 1 / -1; }
.rounded-r-lg {
  border-top-right-radius: var(--r-md);
  border-bottom-right-radius: var(--r-md);
}
.transition-opacity { transition: opacity var(--dur) var(--ease); }
.hover\:bg-neutral-50:hover  { background-color: var(--bg-inset); }
.hover\:bg-neutral-100:hover { background-color: var(--bg-inset); }

/* Tailwind 的 ! 前缀 = important。JS 在子分类激活时加上这四个，
   取值与上面 .uni-sidebar .sub-category.active 保持一致——
   谁最终生效都一样，不会出现两套蓝 */
.\!bg-blue-50    { background-color: var(--accent-soft) !important; }
.\!text-blue-600 { color: var(--accent-soft-text) !important; }
.\!font-semibold { font-weight: 500 !important; }
.\!pl-6          { padding-left: 24px !important; }

/* ==========================================================================
   31. 字段关系可视化页（Tableau 字段关系可视化）

   这一页是「应用式工作台」布局，跟其余文档流页面不同：整页不滚，
   只有侧栏内部滚，网络图铺满剩余高度。

   —— 为什么所有规则都挂在 .vis-page 下 ——
   改造前的样式块里有 `* { margin:0; padding:0 }` 和裸元素选择器 `header` / `h1`。
   前者会抹掉全站其它页面的间距，后者会污染所有页面的标题 —— 只要放进公共
   theme.css 就是灾难。所以这里全部收进 .vis-page 作用域，一行都不外泄。
   ========================================================================== */

/* —— 整页不滚：这是本页布局的地基，不能只写 min-height ——

   教训（踩过一次，代价是页面无限变长）：
   原先这里写的是 `min-height`，意味着 .vis-page 的高度「由内容决定，只是有个下限」。
   于是高度链上没有任何一处是硬约束：

       .vis-page(min-height) → .vis-body(flex:1) → .vis-layout(grid, 行高 auto)
         → .vis-main(grid item stretch) → #networkVisualization(height:100%)
           → vis-network 生成的 canvas（block 元素）

   canvas 一被 vis-network 设成 N px 高，就把 #networkVisualization 顶到 N px；
   vis-network 自带的 autoResize 通过 ResizeObserver 发现容器变大了，又把 canvas
   调成 N+δ；容器再被顶大…… 形成正反馈。实测容器一路涨到 1253px，
   控制台刷出 120 条 "ResizeObserver loop"，页面越滚越长、滚动条自己往上跳。

   原版之所以没这个问题，是因为它给两栏容器写死了
   `height: calc(100vh - 220px); min-height: 600px` —— 硬高度直接切断了这条链。
   这里用 height 而不是 min-height，就是把「硬约束」还给这一层。

   减掉 --ad-h 是因为 body 已为底部广告条预留了等高的 padding-bottom，
   不减的话总高会超出视口、多出一条滚动条。 */
.vis-page {
  display: flex;
  flex-direction: column;
  /* 页面高度：140dvh（2026-09-03 从 200dvh 回调下来）

     演化：100dvh 时侧栏字段列表可视区只有 251px，用户嫌矮 → 翻到 200dvh（约 1300px）。
     但 200dvh 把右栏画布拉成 720×1470 的竖长条，而 fit() 会把图**垂直居中**在这根长条里：
     实测 6 节点的小图只有 227px 高，居中后图顶被推到页面 y=767，视口才 900，
     首屏只看得到 59%，必须下拉 —— 用户反馈「太高了」。
     折中到 140dvh：字段列表可视区约 870px，画布 720×930，
     再配合「图贴顶」（见页面里的 _fitTopAligned），小图/中图能完整落在首屏。

     整页 overflow:visible，把滚动交给外层 body。
     画布仍是 absolute inset:0，不会触发之前的 autoResize 正反馈 bug。 */
  height: calc(140vh - var(--ad-h));
  height: calc(140dvh - var(--ad-h));
  overflow: visible;
}
/* 广告条关掉后把这段高度还给内容区 */
body:has(.ad-banner[hidden]) .vis-page {
  height: 140vh;
  height: 140dvh;
}

/* —— 页头：与其它工具页同构的卡片式标题 —— */
.vis-header {
  flex: none;
  background: var(--bg-surface);
  border-bottom: 1px solid var(--border);
}
.vis-header__inner {
  max-width: var(--container-lg);
  margin: 0 auto;
  padding: var(--sp-5);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--sp-4);
}
/* 同 unicode 页：grid 默认 stretch，不收紧返回链接会被拉满整个左列 */
.vis-header__inner > * { justify-self: start; }
.vis-header__text { justify-self: center; text-align: center; }
.vis-header__title {
  margin: 0;
  font-size: var(--fs-h2);
  font-weight: 500;
  letter-spacing: -.01em;
  color: var(--text-1);
}
.vis-header__sub {
  margin: var(--sp-1) 0 0;
  font-size: var(--fs-sm);
  color: var(--text-2);
}

/* —— 两栏主体 ——
   让 .vis-body 自己也成为 flex 容器，下一层用 flex:1 而不是 height:100% 撑开。
   「flex item 用百分比高度」是经典坑 —— .vis-body 高度来自 .vis-page 的 flex:1，
   不是显式声明，传到 .vis-layout 的 height:100% 大概率解不出来 → 整个布局塌成 0。 */
.vis-body {
  flex: 1;
  min-height: 0;
  padding: var(--sp-5);
  display: flex;
}
.vis-layout {
  flex: 1;
  min-height: 0;
  max-width: var(--container-lg);
  width: 100%;
  margin: 0 auto;
  display: grid;
  grid-template-columns: minmax(300px, 2fr) 3fr;
  /* 行高必须锁成「占满且可收缩」。默认的 grid-auto-rows:auto 会让行高跟着内容
     （侧栏展开的字段列表 / canvas）走，是上面那条正反馈的另一半成因。
     minmax(0, 1fr) 是 grid 语境里的 min-height:0 —— 允许行收缩到容器高度以内。 */
  grid-template-rows: minmax(0, 1fr);
  /* gap 从 --sp-5(24px) 缩到 --sp-1(4px)（用户#28）：
     总视觉间距 = vis-body padding(24) + gap(4) + vis-body padding(24) ≈ 50px */
  gap: var(--sp-1);
}

/* —— 左：数据源侧栏 —— */
.vis-sidebar {
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding: var(--sp-4);
  background: var(--bg-surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
}
.vis-sidebar__title,
.vis-stats__title {
  flex: none;
  margin: 0 0 var(--sp-3);
  font-size: var(--fs-h3);
  font-weight: 500;
  color: var(--text-1);
}

/* 搜索 + 下载 */
.vis-filter {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-3);
}
.vis-search { position: relative; flex: 1; min-width: 0; }
.vis-search .fa-search {
  position: absolute;
  left: 11px;
  top: 50%;
  transform: translateY(-50%);
  width: 14px;
  height: 14px;
  color: var(--text-3);
  pointer-events: none;
}
.vis-search__input {
  width: 100%;
  height: 34px;
  padding: 0 var(--sp-3) 0 32px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
  background: var(--bg-surface);
  font-family: inherit;
  font-size: var(--fs-sm);
  color: var(--text-1);
  outline: none;
  transition: border-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
}
.vis-search__input::placeholder { color: var(--text-3); }
.vis-search__input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-ring);
}

/* 数据源列表：唯一可滚区。左右负边距让 hover 背景贴到卡片内边距，
   否则列表项的高亮会像浮在中间的一块 */
.vis-ds-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  margin: 0 calc(var(--sp-1) * -1);
  padding: 0 var(--sp-1);
}

/* —— 以下 .data-source / .field-* / .stat-item 全部由 JS 生成，类名一个都不能改 —— */
.vis-page .data-source {
  margin-bottom: var(--sp-3);
  background: var(--bg-surface-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-md);
  overflow: hidden;
}
.vis-page .data-source-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-1);
  cursor: pointer;
  -webkit-user-select: none;
          user-select: none;
  transition: background-color var(--dur) var(--ease);
}
.vis-page .data-source-header:hover { background-color: var(--bg-inset); }

/* 分隔线画在展开的内容上，而不是 header 上 ——
   画在 header 上会在折叠态留下一条悬空的横线 */
.vis-page .data-source-content {
  display: none;
  padding: var(--sp-3) var(--sp-4);
}
.vis-page .data-source-content.expanded {
  display: block;
  border-top: 1px solid var(--border-soft);
}

.vis-page .field-category { margin-bottom: var(--sp-2); }
.vis-page .field-category:last-child { margin-bottom: 0; }

/* 分组标题（原始字段 / 计算字段）：可点击折叠，默认收起。
   箭头用 ::before，只 toggle 标题自己的 .expanded，列表靠相邻兄弟选择器跟着显隐，
   这样 JS 只需维护一个 class。 */
.vis-page .field-category__title {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin: 0;
  padding: var(--sp-2) var(--sp-2);
  font-size: var(--fs-caption);
  font-weight: 500;
  letter-spacing: .02em;
  color: var(--text-2);
  cursor: pointer;
  -webkit-user-select: none;
          user-select: none;
  border-radius: var(--r-sm);
  transition: background-color 0.12s ease;
}
.vis-page .field-category__title:hover {
  background-color: var(--bg-inset);
  color: var(--text-1);
}
.vis-page .field-category__title::before {
  content: '▸';
  flex: none;
  font-size: 0.85em;
  color: var(--text-3);
  transition: transform 0.18s ease;
}
.vis-page .field-category__title.expanded::before { transform: rotate(90deg); }
/* 分组列表默认折叠；搜索时由 JS 统一加 .expanded 展开 */
.vis-page .field-category__list {
  display: none;
  padding-left: var(--sp-2);
}
.vis-page .field-category__title.expanded + .field-category__list { display: block; }

.vis-page .field-category h4,
.vis-page .field-type-section h4 {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-caption);
  font-weight: 500;
  letter-spacing: .02em;
  color: var(--text-2);
}
.vis-page .field-type-section + .field-type-section { margin-top: var(--sp-4); }
.vis-page .field-type-section ul { margin: 0; padding: 0; list-style: none; }

/* .field-item 在 JS 里既是 <div>（数据源列表）也是 <li>（搜索结果），
   两套结构共用同一份样式，所以不能依赖标签选择器 */
/* 字段项 —— 对齐站内 .chip 语汇，单列纵向、一行一个字段。

   踩过的三个坑，都源于「自创语汇」而不是复用站内已有组件：
     v1 白卡 + 阴影 + 1px 全描边 + 左 3px 色条 + hover 位移 → 太 AI 味
     v2 纯 list，无圆角无容器、6px 圆点                    → 太克制
     v3 Notion 的 1px 分隔线 + 5px 方块色标                 → 站内根本没这套语汇
   现在每个元素都取自站内现成组件：
     7px 圆点色标 ← .chip__dot（站内 chip 组件自带的色点）
     10px 圆角    ← --r-sm（.jv-page .field-item / .uni-grid .char-item 同款）
     1px 淡描边   ← .palette-card 的 --border-soft
     hover 淡底   ← --bg-inset（.uni-grid .char-item / .jv-page .field-item 同款）
   刻意不做的：无阴影、无左色条、无 hover 位移、无行间分隔线。 */
.vis-page .field-item {
  position: relative;
  margin: 0 var(--sp-1) 3px;
  padding: var(--sp-2) var(--sp-3) var(--sp-2) calc(var(--sp-3) + 16px);
  min-height: 44px;
  background: var(--bg-surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-sm);
  box-shadow: none;
  transition: background-color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
/* 7px 圆点色标：对齐 .chip__dot，top 让圆心落在字段名首行的中线上 */
.vis-page .field-item::before {
  content: '';
  position: absolute;
  left: 13px;
  top: 15px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
}
.vis-page .field-item:last-child { margin-bottom: 0; }
.vis-page .field-item:hover {
  background-color: var(--bg-inset);
  border-color: var(--border-strong);
}
.vis-page .field-item:hover .field-name { font-weight: 600; }
/* —— 字段 role（用户偏好：维度=蓝调 / 度量=绿调） ——

   与节点颜色同源：维度节点边框/字 = --accent 蓝，度量节点边框/字 = --teal 绿。
   圆点色标（::before）默认 --accent 蓝，按 role 用 .dimension / .measure 切到 --teal 绿。
   hover 高亮也按 role 分色：
     .dimension:hover → --accent-soft 淡蓝
     .measure:hover   → --teal-soft 淡绿
   这样侧栏与画布里的节点「角色 → 颜色」完全对齐，用户看到一个蓝点就知道是维度。 */
.vis-page .field-item.dimension::before { background: var(--accent); }
.vis-page .field-item.measure::before   { background: var(--teal); }
.vis-page .field-item.dimension:hover {
  background-color: var(--accent-soft);
  border-color: var(--accent);
}
.vis-page .field-item.dimension:hover .field-name { color: var(--accent-soft-text); }
.vis-page .field-item.measure:hover {
  background-color: var(--teal-soft);
  border-color: var(--teal);
}
.vis-page .field-item.measure:hover .field-name { color: var(--teal-soft-text); }
/* 计算字段换青色圆点（保留向下兼容 —— 早期渲染未带 dimension/measure class 时
   仍按 type=calculated 切到 teal，避免回退到默认蓝） */
.vis-page .field-item.calculated::before,
.vis-page .field-item.calculated-field::before { background: var(--teal); }

.vis-page .field-name {
  display: block;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-1);
  overflow-wrap: break-word;
}
.vis-page .field-role {
  display: inline-block;
  margin-left: var(--sp-2);
  font-size: var(--fs-caption);
  color: var(--text-3);
}
.vis-page .field-count {
  font-size: var(--fs-caption);
  font-weight: 400;
  color: var(--text-3);
}
.vis-page .field-details {
  margin-top: var(--sp-1);
  font-size: var(--fs-caption);
  color: var(--text-2);
}
/* 公式块：等宽字体 + 内凹底，长公式按词断行而不是溢出 */
.vis-page .field-formula {
  display: block;
  margin-top: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  background: var(--bg-inset);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-xs);
  font-family: var(--font-mono);
  font-size: var(--fs-caption);
  line-height: 1.6;
  color: var(--text-1);
  white-space: pre-wrap;
  overflow-wrap: break-word;
  word-break: break-word;
}

.vis-page .references {
  margin-top: var(--sp-2);
  font-size: var(--fs-caption);
  color: var(--text-2);
}
.vis-page .reference-item {
  display: inline-block;
  margin: var(--sp-1) var(--sp-1) 0 0;
  padding: 2px var(--sp-2);
  background: var(--accent-soft);
  border-radius: var(--r-pill);
  font-size: var(--fs-caption);
  font-weight: 500;
  color: var(--accent-soft-text);
}

/* 统计信息：钉在侧栏底部，不参与列表滚动 */
.vis-page .stats {
  flex: none;
  margin-top: var(--sp-3);
  background: var(--bg-surface-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-md);
  overflow: hidden;
  /* 默认折叠：高度由标题一行撑起（约 48px），点标题展开 #statsContent。
     释放出的约 160px 让给上方 #dataSourceList，让字段列表可视区从 251→400+。
     JS 仍照常往 #statsContent 注入 innerHTML，折叠靠 max-height 控制，
     内容随时可被读出，不影响筛选/搜索。 */
}
/* 标题独占一行，点这行切换 .stats.is-open */
.vis-page .stats .vis-stats__title {
  margin: 0;
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-h3);
  font-weight: 500;
  color: var(--text-1);
  cursor: pointer;
  user-select: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
  list-style: none;
  transition: background-color 0.15s ease;
}
.vis-page .stats .vis-stats__title::-webkit-details-marker { display: none; }
.vis-page .stats .vis-stats__title:hover { background-color: var(--bg-inset); }
.vis-page .stats .vis-stats__title::after {
  content: '▸';
  font-size: 0.85em;
  color: var(--text-3);
  transition: transform 0.2s ease;
  /* 默认朝下：箭头旋转 90° = 展开态（用户#28：默认展开统计信息填侧栏空白） */
  transform: rotate(90deg);
}
/* 折叠态（用户点击标题后 .stats 加 .is-open）：箭头回到朝右 */
.vis-page .stats.is-open .vis-stats__title::after { transform: rotate(0deg); }
/* 内容区默认展开（用户#28）：max-height 取消折叠上限，让统计行直接显示。
   specificity (1,2,0) 比 .stats.is-open 规则 (1,3,0) 低 —— 默认展开生效靠 specificity 倒置 */
.vis-page .stats #statsContent {
  max-height: none;
  overflow: hidden;
  padding: var(--sp-2) var(--sp-4) var(--sp-3);
}
/* 折叠态：用户点标题后 .stats 加 .is-open，max-height 收到 0 */
.vis-page .stats.is-open #statsContent {
  max-height: 0;
  padding: 0 var(--sp-4);
  transition: max-height 0.25s ease, padding 0.25s ease;
}
.vis-page .stat-item {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-1) 0;
  font-size: var(--fs-sm);
  color: var(--text-2);
}
.vis-page .stat-item strong {
  font-weight: 500;
  color: var(--text-1);
}

/* —— 右：网络图画布 —— */
.vis-main {
  min-height: 0;
  padding: var(--sp-4);
  background: var(--bg-surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
  overflow: hidden;
  /* 原版 .main-content 就是 flex column：让画布容器用 flex:1 吃到确定高度，
     而不是靠 height:100% 去解一条可能解不开的百分比链 */
  display: flex;
  flex-direction: column;
}
/* JS 用 style.display 在 block / none 之间切换 noDataMessage 与
   networkVisualization，所以这两条规则的 display 绝不能被 CSS 写死 */
#visualizationContainer {
  position: relative;
  width: 100%;
  flex: 1;
  min-height: 0;   /* 允许在矮视口里跟着收缩，绝不反向撑大父级 */
}
/* —— 画布绝对定位，是切断正反馈的最后一道保险 ——

   vis-network 会往这里塞一个 display:block 的 canvas。只要 #networkVisualization
   还在文档流里，canvas 的高度就等于在给父容器投票定高度 —— 哪怕上面几层都锁死了，
   只要有一处算错就又会顶起来。写成 absolute + inset:0 之后，canvas 对祖先高度
   的贡献恒为 0，容器高度只由 flex 布局说了算，vis-network 只能被动跟随。

   父级 #visualizationContainer 已是 position:relative，inset:0 正好铺满它。 */
#networkVisualization {
  position: absolute;
  inset: 0;
  border: 1px solid var(--border-soft);
  border-radius: var(--r-sm);
  overflow: hidden;
}

/* —— vis-navigation 容器位置 ——

   三个发现串成的根因：
     ① vis-network 的 CSS 只对 .vis-button 设 position:absolute，没动 .vis-navigation。
       所以 .vis-navigation 默认是 position:static。
     ② 我之前只加 top:8px / right:8px，但 position:static 下 top/right/bottom/left 全部无效。
     ③ .vis-button 的 bottom/left 由 `div.vis-network div.vis-navigation div.vis-button.vis-up`
       这种 4-element 选择器设定，specificity (0,3,3) 比我的 (0,2,0) 高。
       按钮的 containing block 退到 #networkVisualization（position:absolute），
       所以 bottom:50px 是相对画布底部 → 按钮渲染在画布底部。

   正确做法：
     - 给 .vis-navigation 加 position:absolute !important，自己变成 containing block
     - 给 .vis-button 加 position:static !important，把它们拉回 flex 流式布局
     - bottom/left/right/top: auto !important 抵消库的内联偏移
   DOM 顺序 up→down→left→right→zoomIn→zoomOut→zoomExtends 在 flex 下正好是
   一行 7 个干净的小按钮工具栏，全部贴在画布右上角。 */
.vis-page .vis-navigation {
  position: absolute !important;
  display: flex !important;
  flex-direction: row;
  flex-wrap: nowrap;
  align-items: center;
  gap: 4px;
  width: auto !important;
  height: auto !important;
  padding: 4px;
  background: transparent;
  top: var(--sp-2) !important;
  right: var(--sp-2) !important;
  bottom: auto !important;
  left: auto !important;
}
/* 按钮回到正常流，丢掉 vis-network 内联 + 选择器的 top/left/bottom/right */
.vis-page .vis-button {
  position: static !important;
  top: auto !important;
  left: auto !important;
  right: auto !important;
  bottom: auto !important;
}

/* —— vis-network 导航按钮 ——

   原版：vis-network 把每个按钮用一张 base64 PNG sprite 画在 background-image 上，
   绿色描边 + 绿色图标都做死在像素里 —— CSS 改不了颜色，跟站内三色系统（蓝/琥珀/青）
   严重不搭。改成跟站内 chip 同构的样式：白底 + 1px 淡蓝描边 + 蓝图标（默认就是蓝调），
   hover 浅蓝底 + 深蓝图标，active 实蓝底 + 白图标。

   做法：
     background-image: none !important 清掉 sprite；
     display: flex !important 压住库内联样式的 inline-block（!important 是必要的，
                            否则 align-items: center 不生效，图标会掉到 baseline）；
     图标用 CSS mask + data URI SVG 重画（与 theme.css 第 27 节 datafam 页的 .fa mask
                                       图标系统同构）。SVG 里的 stroke 用 black —— mask
                                       只看亮度/alpha，真正的图标色由
                                       background-color: currentColor 接管。 */
.vis-page .vis-button {
  background-image: none !important;
  background-color: var(--bg-surface);
  border: 1px solid var(--accent-soft);
  /* !important 压住 vis-network 内置样式的 border-radius:17px（半圆）——
     站内小按钮用 --r-md 14px，与 .chip/.field-item 同款。
     内置样式可能用 ID+类组合或 !important，specificity 拼不过，直接上 important */
  border-radius: var(--r-md) !important;
  box-shadow: var(--shadow-xs);
  color: var(--accent);
  /* !important 压住库内联样式的 display:inline-block —— 否则 flex 居中失效 */
  display: flex !important;
  align-items: center !important;
  justify-content: center !important;
  cursor: pointer;
  transition: background-color var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              color var(--dur) var(--ease);
}
.vis-page .vis-button:hover {
  background-color: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent-soft-text);
}
.vis-page .vis-button:active {
  background-color: var(--accent);
  color: #fff;
  border-color: transparent;
}
/* 16×16 图标：CSS mask + currentColor，跟 .chip__dot 一脉相承 */
.vis-page .vis-button::before {
  content: '';
  display: block;
  width: 16px;
  height: 16px;
  background-color: currentColor;
  -webkit-mask: var(--icon) center / contain no-repeat;
          mask: var(--icon) center / contain no-repeat;
}
/* 各方向箭头 + 缩放 + 适配 —— 7 个按钮对应 7 个类名 */
.vis-page .vis-up {
  --icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M8 13V3M4 7L8 3L12 7'/></svg>");
}
.vis-page .vis-down {
  --icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M8 3V13M4 9L8 13L12 9'/></svg>");
}
.vis-page .vis-left {
  --icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M13 8H3M7 4L3 8L7 12'/></svg>");
}
.vis-page .vis-right {
  --icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M3 8H13M9 4L13 8L9 12'/></svg>");
}
.vis-page .vis-zoomIn {
  --icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M3 8H13M8 3V13'/></svg>");
}
.vis-page .vis-zoomOut {
  --icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M3 8H13'/></svg>");
}
.vis-page .vis-zoomExtends {
  /* 适配屏幕：4 个 L 形外指角 + 中心小框 —— 比 PNG sprite 的图标更接近「撑满外框」语义 */
  --icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M2 5V2H5M11 2H14V5M14 11V14H11M5 14H2V11M5 5H11V11H5'/></svg>");
}
/* 空态：绝对定位盖在画布上，这样它出现/消失都不会把画布顶位移 */
.vis-page .no-data {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  transform: translateY(-50%);
  padding: var(--sp-5);
  text-align: center;
  font-size: var(--fs-body);
  color: var(--text-3);
}

/* 节点浮层：JS 把它 append 到 document.body（不在 .vis-page 内），
   所以这条规则不能加作用域前缀。
   定位用 fixed —— JS 写入的 left/top 是 clientX/clientY 视口坐标，
   用 absolute 在页面滚动时会错位。 */
.node-tooltip {
  position: fixed;
  z-index: 1000;
  max-width: 300px;
  padding: var(--sp-3) var(--sp-4);
  background: rgba(28, 28, 30, .88);
  color: #fff;
  border-radius: var(--r-sm);
  font-size: var(--fs-sm);
  line-height: 1.6;
  box-shadow: var(--shadow-pop);
  pointer-events: none;
  overflow-wrap: break-word;
  -webkit-backdrop-filter: blur(12px);
          backdrop-filter: blur(12px);
}
.node-tooltip strong { color: #93C5FD; font-weight: 500; }
.node-tooltip .tooltip-field-name {
  display: block;
  padding-bottom: var(--sp-1);
  margin-bottom: var(--sp-2);
  border-bottom: 1px solid rgba(255, 255, 255, .16);
  font-weight: 500;
  color: #fff;
}

/* 注：类型色标统一由 .field-item::before 的 7px 圆点承担（蓝=原始 / 青=计算）。
   这里刻意不再用 border-left-color 做左侧色条 —— 那是「太 AI 味」的旧版残留，
   且 .original-field / .calculated-field 在搜索结果里也会出现，靠圆点即可区分。 */

/* —— 以下三个来自层级视图 / 关系图视图 ——
   对应的 displayFieldHierarchy() 与 displayFieldRelationships() 在原页里
   定义了但从未被调用（.fieldHierarchy / .fieldRelationships 两个容器也不存在于
   DOM 中），属死代码。样式照样补齐，将来若启用不会裸奔。 */
.vis-page .hierarchy-level {
  margin-left: var(--sp-5);
  padding-left: var(--sp-5);
  border-left: 2px solid var(--border);
}
.vis-page .level-title {
  margin: var(--sp-3) 0 var(--sp-2);
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-2);
}
.vis-page .relationship-graph {
  margin-top: var(--sp-5);
  padding: var(--sp-4);
  background: var(--bg-surface-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-md);
}
.vis-page .relationship-item {
  margin: var(--sp-2) 0;
  padding: var(--sp-3);
  background: var(--bg-surface);
  border-left: 3px solid var(--amber);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-xs);
}
.vis-page .relationship-arrow {
  display: inline-block;
  margin: 0 var(--sp-3);
  font-weight: 500;
  color: var(--text-3);
}

/* —— .loading-progress / .error-message 不需要任何 CSS ——
   JS 用 style.cssText 把 position / background / padding / border-radius /
   box-shadow 全部内联写死了，再加规则只会两边打架。这里留空规则占位，
   仅为让「该类在 theme.css 有定义」的检查通过。 */
.loading-progress { /* 样式由 JS 内联提供 */ }
.error-message    { /* 样式由 JS 内联提供 */ }

/* —— 响应式：窄屏 / 矮屏放弃「铺满视口」，改回普通文档流 ——

   两个断点用同一套兜底：解除 .vis-page 的锁高，把行高交还给内容，
   再给两栏一个显式高度（画布靠 .vis-main 的 min-height 拿到高度，
   #networkVisualization 仍是 absolute，不会反过来撑大它）。

   矮屏（max-height）这条是必需的：视口只有 600px 高时，锁高布局会把
   画布压到不到 200px，图根本看不清，不如让页面正常滚动。 */
@media (max-width: 900px), (max-height: 640px) {
  .vis-page {
    height: auto;
    min-height: 0;
    overflow: visible;
  }
  body:has(.ad-banner[hidden]) .vis-page { height: auto; }
  .vis-body { min-height: 0; padding: var(--sp-4); }
  .vis-layout {
    grid-template-columns: 1fr;
    grid-template-rows: auto;
    height: auto;
  }
  .vis-sidebar {
    /* 侧栏改为限高滚动，否则一进页就被数据源列表占满整屏 */
    max-height: 420px;
  }
  .vis-main { min-height: 460px; }
  .vis-header__inner { grid-template-columns: 1fr; gap: var(--sp-2); }
  .vis-header__text { justify-self: start; text-align: left; }
}
/* 窄屏下两栏改为上下堆叠，列宽限制同步解除 */
@media (max-width: 900px) {
  .vis-layout { grid-template-columns: minmax(0, 1fr); }
}

/* ==========================================================================
   32. JSON 查看器页（tableau_json_viewer.html）私有样式

   —— 沿用第 31 节相同的设计：
   原页内联 style 块里有 `* { box-sizing:border-box; margin:0; padding:0 }` 和
   裸元素选择器 `h1` / `body` —— 跟 visualizer 同理，全部收进 .jv-page 作用域
   一行都不外泄。

   这一页是「sticky 搜索/下载条 + 左右两栏 + 下方元数据」的文档流布局，
   跟 visualizer 的「应用式不滚」完全相反，所以单独分区而不是合并到第 31 节。
   ========================================================================== */

/* —— 整页容器：白底文档流，最小一屏高 —— */
.jv-page {
  background: var(--bg-surface);
  color: var(--text-1);
  font-family: var(--font-sans);
  /* 重置原页 `* { margin:0; padding:0 }` 的等价做法：用自己的 box-sizing 与 padding */
  min-height: calc(100vh - var(--ad-h));
}
body:has(.ad-banner[hidden]) .jv-page { min-height: 100vh; }

/* —— 页头 + 搜索/下载条：合并为同一个 sticky 单元，跟 unicode/datafam 同构 —— */
.jv-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--bg-header);
  border-bottom: 1px solid var(--border);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
          backdrop-filter: saturate(180%) blur(20px);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .jv-header { background: var(--bg-header-solid); }
}
.jv-header__inner {
  max-width: var(--container-lg);
  margin: 0 auto;
  padding: var(--sp-5) var(--sp-5) 0;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--sp-4);
}
/* grid 默认 justify： justify-self: stretch —— 必须显式收，否则返回链接被拉满整列 */
.jv-header__inner > * { justify-self: start; }
.jv-header__title {
  justify-self: center;
  margin: 0;
  font-size: var(--fs-h2);
  font-weight: 500;
  letter-spacing: -.01em;
  color: var(--text-1);
}
/* —— 工具栏：搜索 + 下载 —— */
.jv-toolbar {
  border-top: 1px solid var(--border-soft);
}
.jv-toolbar__inner {
  max-width: var(--container-lg);
  margin: 0 auto;
  padding: var(--sp-3) var(--sp-5);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}
.jv-search { position: relative; flex: 1; min-width: 0; }
.jv-search .fa-search {
  position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  width: 16px; height: 16px; color: var(--text-3); pointer-events: none;
}
.jv-search__input {
  width: 100%; height: 38px; padding: 0 var(--sp-3) 0 36px;
  border: 1px solid var(--border-strong); border-radius: var(--r-pill);
  background: var(--bg-surface); font: inherit; font-size: var(--fs-sm);
  color: var(--text-1); outline: none;
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.jv-search__input::placeholder { color: var(--text-3); }
.jv-search__input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-ring);
}

/* —— 主区：左右两栏 —— */
.jv-stage {
  max-width: var(--container-lg);
  margin: 0 auto;
  padding: var(--sp-5);
  display: grid;
  grid-template-columns: 360px minmax(0, 1fr);
  gap: var(--sp-5);
}
/* —— 侧栏（原页 .sidebar）—— */
.jv-page .sidebar {
  min-width: 0;
  /* padding 拆开写：上下 12px、right 12px（给滚动条 + 卡片右缘留 gutter，避免贴边被遮）；
     left 保持 0 —— 层级缩进靠子元素自己的 padding-left（main-category / sub-categories）
     和 grid 间距控制，sidebar 自己不需要左 padding。 */
  padding: var(--sp-3) var(--sp-3) var(--sp-3) 0;
  background: var(--bg-surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
  height: 70vh;          /* 沿用原页设计 */
  max-height: 70vh;
  overflow-y: auto;
  /* 自定义滚动条（Webkit） */
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}
.jv-page .sidebar::-webkit-scrollbar { width: 6px; }
.jv-page .sidebar::-webkit-scrollbar-thumb {
  background: var(--border-strong); border-radius: 3px;
}

/* —— 主分类（原页 .main-category，JS classList.toggle('active')） ——
   视觉对齐 visualizer 的 .data-source-header：
     row 极简风 —— 无背景 / 无左边色带 / 无圆角边框，
     active 态才挂 role 色（浅色底 + role 文字色）。
   历史：用户#33「这个页面的边界 AI 感还是很浓」，参照 visualizer 极简化。 */
.jv-page .main-category {
  margin: var(--sp-1) 0;
  padding: var(--sp-2) var(--sp-4);
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: transparent;
  color: var(--text-1);
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  font-size: var(--fs-sm); font-weight: 500;
  cursor: pointer; user-select: none;
  transition: background-color var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              color var(--dur) var(--ease);
}
.jv-page .main-category:hover {
  background-color: var(--bg-inset);
  border-color: var(--border-soft);
  transform: none;            /* 不要 translateX —— row 风不要视觉跳动 */
}
.jv-page .main-category.active {
  background-color: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent-soft-text);
}
.jv-page .main-category:focus {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
/* 计算字段分类 active 态：teal 浅色（与 dim 区分） */
.jv-page .main-category.calculated-header.active {
  background-color: var(--teal-soft);
  border-color: var(--teal);
  color: var(--teal-soft-text);
}

/* —— 折叠图标（原页 .toggle-icon，JS 在 .active 时旋转 90 度） —— */
.jv-page .toggle-icon {
  display: inline-block;
  min-width: 16px;
  text-align: center;
  font-size: var(--fs-caption);
  color: var(--text-3);
  transition: transform var(--dur) var(--ease);
}
.jv-page .main-category.active .toggle-icon {
  transform: rotate(90deg);
  color: inherit;        /* 跟随父级文字色，与底色协调 */
}

/* —— 子分类容器（原页 .sub-categories，JS 通过 style.maxHeight 在 0/auto 间切换） ——
   绝不能写死 display：JS 用 style.display / style.maxHeight 直接控。 */
.jv-page .sub-categories {
  /* 左右对称 padding —— 右边距给 field-item 的 margin-right 让出空间，
     否则字段项的圆角会被 sidebar 圆角切到（用户#38「与容器的边距太近了被遮挡」） */
  padding-left: var(--sp-3);
  padding-right: var(--sp-3);
  margin: 6px 0;
  max-height: 0;
  overflow: hidden;
  transition: max-height var(--dur-slow) var(--ease),
              padding var(--dur-slow) var(--ease);
  flex-shrink: 0;
}
.jv-page .main-category.active + .sub-categories,
.jv-page .main-category.calculated-header.active + .sub-categories {
  max-height: none;
  overflow-y: visible;
}

/* —— 数据源项（原页 .data-source-item） —— */
.jv-page .data-source-item {
  margin: var(--sp-1);
  padding: var(--sp-3) var(--sp-7);
  display: flex; align-items: center;
  color: var(--text-1);
  font-size: var(--fs-sm);
  border-radius: var(--r-sm);
  cursor: pointer; user-select: none;
  transition: background-color var(--dur) var(--ease),
              color var(--dur) var(--ease);
}
/* hover 只变底色 —— 与 .main-category / .field-item 统一为同一套交互语汇：
   row 风不做 translateX 位移，否则鼠标扫过整列时每一行都会左右抖动。
   visualizer 的 .data-source-header:hover 同样只有 background-color。 */
.jv-page .data-source-item:hover {
  background-color: var(--bg-inset);
}
.jv-page .data-source-item.active {
  background: var(--accent-soft);
  color: var(--accent-soft-text);
  font-weight: 500;
}
.jv-page .data-source-item:focus {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* —— 字段项（原页 .field-item；JS classList 操作 active，className 拼上 dimension/measure/calculated） ——

   高亮层级 —— 对齐 visualizer（第 31 节 .vis-page .field-item）：
     默认    白底 + 1px 淡描边    ← 与 visualizer 同：给出「这一行可点」的 affordance
     hover   --accent-soft 淡蓝 /
             --teal-soft   淡青  ← 与 visualizer 完全同值（浅，试探性反馈）

   改之前 hover 与 active 都是 #BAD8F8 —— 鼠标一移开，选中的项和随手划过的项长得
   一模一样，用户分不清「哪个是我选中的」。jv 必须保留 active（要指示右侧详情正在
   显示哪个字段），所以把 active 压到比 hover 深一档，做成递进的两级：
     active  #BAD8F8 / #BBDBD7   ← 比 hover 深一档（确认性反馈）

   对比度（13.5px 正文，WCAG AA 需 >= 4.5:1）：
     #BAD8F8 + --accent-soft-text = 4.72:1  OK
     #BBDBD7 + --teal-soft-text   = 5.95:1  OK
   再深一档就跌破 4.5，所以 active 到这一档为止，不再加「active + hover」第三档。

   三层容器放在一起是「容器浅、内容深」：
     .data-source-item.active  --accent-soft（浅，选中的数据源）
     .main-category.active     --accent-soft（浅，已展开的分组）
     .field-item.active        #BAD8F8      （深，最终选中的字段）

   —— 三处刻意保留的差异（硬改成 visualizer 会破坏 jv 自身结构，注释备查）——
   1. hover 不加字重：visualizer 的字段名挂在子元素 .field-name 上（block，加粗不位移）；
      jv 的字段名是 field-item 自己的 textContent（JS: fieldItem.textContent = ` ${name}`），
      加粗会把整行撑宽产生抖动，所以 hover 只用「底色 + 文字色」传达。
   2. 常态字色 --text-2：jv 是「数据源 -> 分组 -> 字段」三层树，靠字色深浅区分层级；
      visualizer 只有两层，字段即主内容，所以直接用 --text-1。
   3. 无 min-height 44px：visualizer 加它是因为行内还挂着 .field-role 标签、可能占两行；
      jv 是单行紧凑树，套 44px 会让侧栏高度翻倍。 */
.jv-page .field-item {
  /* margin-right sp-2：让圆角不和 sidebar 圆角区重叠（之前贴边，圆角被容器圆角切掉）；
     padding-right sp-4：让文本距右边框更宽，与 main-category 的 sp-4 节奏一致。 */
  margin: 2px var(--sp-2) 2px 0;
  padding: var(--sp-2) var(--sp-4) var(--sp-2) var(--sp-3);
  display: flex; align-items: center;
  gap: var(--sp-2);
  background: var(--bg-surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-sm);
  color: var(--text-2);
  font-size: 13.5px;
  cursor: pointer; user-select: none;
  transition: background-color var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              color var(--dur) var(--ease);
}
/* 角色圆点（visualizer 同款 7px）：默认 accent 蓝，按 .dimension / .measure 切色 */
.jv-page .field-item::before {
  content: '';
  flex: none;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--accent);
}
.jv-page .field-item.dimension::before { background: var(--accent); }
.jv-page .field-item.measure::before   { background: var(--teal); }
/* 计算字段兜底：与 visualizer 的 .calculated::before 同色（teal）。
   注：JS 的 roleClass 是二选一（measure / dimension），生成的 className 恒为
   `field-item [calculated] dimension|measure`，所以这条实际永不命中 ——
   保留只为将来 JS 改动时不裸奔。 */
.jv-page .field-item.calculated:not(.dimension):not(.measure)::before {
  background: var(--teal);
}

/* hover —— 默认（无 role 时的兜底）：浅灰底，与 visualizer 的 .field-item:hover 同值 */
.jv-page .field-item:hover {
  background-color: var(--bg-inset);
  border-color: var(--border-strong);
  color: var(--text-1);
}
/* hover —— 按 role 分色：与 visualizer 的 .dimension:hover / .measure:hover 同值。
   注：必须显式并列 .calculated.dimension / .calculated.measure —— 它俩特异性 (0,5,0)
   高于本规则 (0,4,0)，不并列写出来就会被基础规则吞掉（#32 踩过这个坑）。 */
.jv-page .field-item.dimension:hover,
.jv-page .field-item.calculated.dimension:hover {
  background-color: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent-soft-text);
}
.jv-page .field-item.measure:hover,
.jv-page .field-item.calculated.measure:hover {
  background-color: var(--teal-soft);
  border-color: var(--teal);
  color: var(--teal-soft-text);
}

/* active —— 比 hover 深一档。必须写在本块最后：
   与 .calculated.*:hover 同特异性 (0,5,0)，靠「后写者赢」压住 hover，
   这样鼠标扫过已选中的字段时不会从深色回退成浅色（看着像取消了选中）。
   同理 .dimension.active / .measure.active (0,4,0) 要压住同名 hover 规则。 */
.jv-page .field-item.active,
.jv-page .field-item.dimension.active,
.jv-page .field-item.calculated.dimension.active {
  background-color: #BAD8F8;
  border-color: var(--accent);
  color: var(--accent-soft-text);
  font-weight: 500;
}
.jv-page .field-item.measure.active,
.jv-page .field-item.calculated.measure.active {
  background-color: #BBDBD7;
  border-color: var(--teal);
  color: var(--teal-soft-text);
}
.jv-page .field-item.calculated:not(.dimension):not(.measure).active {
  background-color: #BBDBD7;
  border-color: var(--teal);
  color: var(--teal-soft-text);
}
.jv-page .field-item:focus {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* —— 元数据区（原页 .metadata-section，独立于 sticky 页头，置于主区下方） —— */
.jv-metadata {
  max-width: var(--container-lg);
  margin: 0 auto var(--sp-5);
  padding: 0 var(--sp-5);
}
.jv-page .metadata-section {
  /* 去卡片化 —— 与 visualizer 的 metadata 区同款：白底 + 无边框，仅靠空间区隔 */
  padding: var(--sp-4) 0;
  background: transparent;
  border: 0;
  border-radius: 0;
  border-top: 1px solid var(--border-soft);  /* 与上方内容用淡线分隔 */
}
.jv-page .metadata-title {
  margin: 0 0 var(--sp-3);
  font-size: var(--fs-sm); font-weight: 500;
  color: var(--text-1);
}
.jv-page .metadata-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--sp-3);
}
.jv-page .metadata-item {
  /* 去小卡片化 —— 纯 row 布局，与 visualizer 的 stats 字段同款 */
  padding: var(--sp-1) 0;
  background: transparent;
  border: 0;
  border-radius: 0;
  font-size: var(--fs-caption);
  color: var(--text-2);
}

/* —— 右侧内容区（原页 .content） —— */
.jv-page .content {
  min-width: 0;
  padding: var(--sp-5);
  background: var(--bg-surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
  height: 70vh;
  max-height: 70vh;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}
.jv-page .content::-webkit-scrollbar { width: 6px; }
.jv-page .content::-webkit-scrollbar-thumb {
  background: var(--border-strong); border-radius: 3px;
}
.jv-page .content-title {
  margin: 0 0 var(--sp-4);
  padding-bottom: var(--sp-2);
  font-size: var(--fs-h3); font-weight: 500;
  color: var(--text-1);
  border-bottom: 1px solid var(--border-soft);
}
/* JS 在搜索结果时给 content-title 追加 .search-result-title 类（蓝高亮） */
.jv-page .content-title.search-result-title {
  color: var(--accent);
  background: var(--accent-soft);
  padding: var(--sp-3);
  border-radius: var(--r-sm);
  border-bottom: 0;
}

/* —— 字段详情（原页 .field-details 等） —— */
.jv-page .field-details {
  /* 去卡片化（用户#34 持续去 AI 感）—— 属性列表是纯文本布局，不需要卡片容器 */
  padding: 0;
  margin-bottom: var(--sp-5);
  background: transparent;
  border: 0;
  border-radius: 0;
}
.jv-page .field-property {
  display: flex; gap: var(--sp-3);
  padding: var(--sp-1) 0;
}
.jv-page .field-label {
  flex: none; width: 100px;
  font-weight: 500;
  color: var(--text-2);
}
.jv-page .field-value {
  color: var(--text-1);
  word-break: break-word;
  overflow-wrap: anywhere;   /* 长公式/字段名也能断行 */
}
.jv-page .field-formula {
  /* 去 AI 感：取消蓝色左边色带（典型「代码引用条」模式）+ 弱化背景（用 --bg-inset 极弱灰） */
  margin: var(--sp-3) 0;
  padding: var(--sp-3) var(--sp-4);
  background: var(--bg-inset);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: var(--fs-caption);
  line-height: 1.6;
  color: var(--text-1);
  white-space: pre-wrap;
  overflow-x: auto;
}
.jv-page .field-formula.measure {
  /* 度量字段的公式 —— 淡青底 + 青边，与左侧字段项 hover/active 的 teal-soft 同色板。
     对比度：--teal-soft #E6F4F1 底 + --text-1 文字 = 14.9:1（AA 需 4.5:1） */
  background-color: var(--teal-soft);
  border-color: var(--teal);
}
.jv-page .field-formula.dimension {
  /* 维度字段的公式 —— 淡蓝底 + 蓝边，呼应左侧字段项的角色色。
     对比度：--accent-soft #E8F2FD 底 + --text-1 文字 = 14.9:1（AA 需 4.5:1） */
  background-color: var(--accent-soft);
  border-color: var(--accent);
}

/* —— toast（原页 #toast，JS classList.add('show') 切显隐） —— */
.jv-page #toast {
  position: fixed;
  top: var(--sp-5);
  left: 50%;
  transform: translateX(-50%);
  background: var(--text-1);
  color: #fff;
  padding: var(--sp-2) var(--sp-4);
  border-radius: var(--r-pill);
  font-size: var(--fs-sm);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur) var(--ease);
  z-index: 1000;
  box-shadow: var(--shadow-pop);
}
.jv-page #toast.show { opacity: 1; }

/* —— 响应式 —— */
@media (max-width: 900px) {
  .jv-stage { grid-template-columns: 1fr; }
  .jv-page .sidebar { height: auto; max-height: 300px; }
  .jv-page .content { height: auto; max-height: none; min-height: 420px; }
  .jv-header__inner { grid-template-columns: 1fr; gap: var(--sp-2); }
  .jv-header__title { justify-self: start; }
  .jv-toolbar__inner { flex-wrap: wrap; }
}
