    /* ═══════════════════════════════════════════════════════════════
       外壳: 左侧会话栏 + 主列(会话持久化见 JS sessions_* 段)
       ═══════════════════════════════════════════════════════════════ */
    .shell {
      display: flex;
      flex: 1;
      min-height: 0;
      width: 100%;
      /* 绘图栏(第三栏)收起时整条滑出右缘 —— 得把出画的那截裁掉, 否则它会在窗口
         右侧撑出一条横向滚动条。用 clip 而不是 hidden: 不建滚动容器、不影响纵向
         (顶栏那排按钮是故意溢到下方对话流上的, 不能被顺手裁掉)。 */
      overflow-x: clip;
      /* ≤900px 时侧栏是 absolute 浮层, 定位基准要落在 .shell(顶栏之下)而不是视口 ——
         否则浮层和遮罩会连顶栏一起盖住, 连"收起"的按钮都点不着 */
      position: relative;
      /* 数据栏宽度: 唯一的口径。侧栏宽度、收起时滑出的距离、开合把手的位置、
         拖拽把手的位置全部由它推出来 —— 加宽只改这一个值(见 JS 的 applyNavW) */
      --nav-w: 300px;
    }

    /* ── 左侧数据栏: 上=数据文件(点即切换), 下=当前文件的变量列表 ──
       收起是整条滑出 --nav-w 那么宽(不留竖条, 那点宽度还给面板正文), 开合把手另钉在
       .shell 上(见 .nav-toggle) —— 把手若留在面板里, 整条滑走会把唯一的入口一起藏掉。
       宽度可拖(见 .nav-resize): 变量名/文件名很长时由用户自己放宽。
       收放只动 margin-left(宽度不变): 侧栏内部零重排, 收放是整块平移,
       不会逐帧挤压里面的文字换行。 */
    .sessions {
      position: relative;
      width: var(--nav-w);
      flex-shrink: 0;
      display: flex;
      flex-direction: column;
      min-height: 0;
      background: var(--card);
      border-right: 1px solid var(--line);
      transition: margin-left .24s cubic-bezier(.4, 0, .2, 1);
    }

    .shell.nav-hidden .sessions {
      margin-left: calc(-1 * var(--nav-w));
    }

    .d-sec {
      display: flex;
      flex-direction: column;
      min-height: 0;
      border-bottom: 1px solid var(--line);
    }

    .d-vars {
      flex: 1;
      border-bottom: none;
    }

    /* 左栏最底下那条: 账号 + 上传数据(原来漂在对话区右上角那条 158px 空档里)。
       它是"面板的脚"而不是又一段列表, 所以上边框 + 浅蓝底跟变量列表分开; 高度由内容定
       (flex-shrink:0), 剩下的高度仍然归 .d-vars。面板拖到最窄(220px)时也就这两样, 放得下。 */
    .side-foot {
      flex-shrink: 0;
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 8px;
      padding: 9px 12px;
      border-top: 1px solid var(--line);
      background: var(--bg-tint);
    }

    .d-sec-h {
      flex-shrink: 0;
      display: flex;
      align-items: center;
      gap: 6px;
      padding: 11px 12px 8px;
      /* 分区标题要比条目醒目一档: 它是左栏的骨架, 条目只是内容 */
      font-size: 13.5px;
      font-weight: 700;
      letter-spacing: .3px;
      color: var(--navy);
    }

    /* auto 只挂这一行里最靠左的那颗 —— 剩余空白全给它, 后面的「刷新 / 上传」才会
       跟着一起贴到行末。多挂一颗就会把空白对半分, 中间凭空裂开一道。
       变量列表行的「刷新」直接用这条; 数据文件行最靠左的是「授权」(见 #fileAuth)。 */
    .d-sec-h .v-btn,
    .d-sec-h .d-btn {
      margin-left: auto;
    }

    .d-cnt,
    .v-meta {
      font-size: 11px;
      font-weight: 400;
      color: var(--faint);
    }

    /* 数据文件份数到顶(见 config.MAX_DATA_DOCS): "100 / 100 份" 改用警示色并加粗 ——
       这时上传/授权/对话建表都会被拒, 计数是唯一常驻在那儿的提示(弹层只在动作之后才出现),
       所以到顶得自己吭声。
       用 --red 而不是 --amber: 这一行只有 11px, amber(#E6A23C)在白底上约 2:1, 小字读不出来;
       --red 是本项目小字警告的既有用色(见 .error-banner / .report-empty.failed), 约 4:1。
       与 .v-meta 无关: 那条是变量数, 没有上限问题。 */
    .d-cnt.full {
      color: var(--red);
      font-weight: 600;
    }

    /* 数据文件: 点条目即切换(当前的用 .active 高亮 + 蓝点) */
    .d-list {
      /* 行高口径: 条目高度和这个 max-height 都从它推 —— 改行距只改这一个值,
         "最多 5 条" 自己跟着走, 不会剩半条第 6 条露在下面。
         末尾 +8 是下面的 padding-bottom(border-box, max-height 把它算在内)。
         32 → 27: 两个条目之间的上下间隙收 15%(32 × 85% = 27.2, 取整 27), 省出来的
         高度归下面的变量列表。内容里最高的仍是 24px 的 .f-act 按钮, 27 放得下。 */
      --f-row: 27px;
      flex-shrink: 0;
      /* 最多 5 条, 第 6 条起出滚动条; 外面再套一道 32vh —— 矮窗口里不至于
         把下面的变量列表挤没(那时宁可少显示两条)。
         --files-max 由数据区/变量区之间的抓取带改写(见 .files-resize 与 initFilesResize):
         没拖过时走上面的缺省(5 行 / 32vh), 拖过就把上限抬到用户定的像素值, 好一次看全。 */
      max-height: var(--files-max, min(calc(var(--f-row) * 5 + 8px), 32vh));
      overflow-y: auto;
      padding: 0 8px 8px;
      scrollbar-width: thin;
      scrollbar-color: var(--line-strong) transparent;
    }

    /* ── 数据区 / 变量区之间的抓取带(上下拖改数据列表高度) ──
       在 .d-files 与 .d-vars 之间占一条 7px 高的 flex 行(两侧高度照旧), 中间露出一根
       3×30 的短杠作握把 —— 与 .nav-resize 同一套「常态小露、悬停/拖拽加长」的语言。
       高亮只加长不变色; 取 --faint 压在 .sessions 的白底上仍看得清(那条底线 --line 在
       纯白上太浅)。拖拽改的是 .d-list 的 max-height(见 --files-max), 不是本行高。 */
    .files-resize {
      flex-shrink: 0;
      height: 7px;
      position: relative;
      z-index: 21;
      cursor: row-resize;
      touch-action: none;
      -webkit-user-select: none;
      user-select: none;
    }

    .files-resize::after {
      content: "";
      position: absolute;
      left: 50%;
      top: 50%;
      width: 30px;
      height: 3px;
      margin: -1.5px 0 0 -15px;
      border-radius: 3px;
      background: var(--faint);
      transition: width .15s ease, margin-left .15s ease;
    }

    .files-resize:hover::after,
    .files-resize:focus-visible::after,
    body.files-resizing .files-resize::after {
      width: 52px;
      margin-left: -26px;
    }

    .files-resize:focus-visible {
      outline: none;
    }

    /* 拖拽中: 关掉所有缓动 —— 每帧都过渡一次会让把手追不上指针(橡皮筋);
       user-select 挡住拖过文本时的选区, 光标全屏保持 row-resize */
    body.files-resizing {
      cursor: row-resize;
      -webkit-user-select: none;
      user-select: none;
    }

    .f-item {
      display: flex;
      align-items: center;
      gap: 7px;
      /* 行高交给 min-height, 不再用纵向内边距: 条目行高才是那个精确的 --f-row。
         38 → 32 收的就是这里 —— 内容里最高的其实是 24px 的 .f-act 按钮,
         原来的 7px 上下内边距全压在它外面, 看着才松。 */
      min-height: var(--f-row);
      border-radius: 8px;
      padding: 0 9px;
      cursor: pointer;
      transition: background .15s ease;
    }

    .f-item:hover {
      background: var(--card-2);
    }

    .f-item.active {
      background: var(--brand-soft);
    }

    .f-dot {
      width: 6px;
      height: 6px;
      border-radius: 50%;
      background: var(--line-strong);
      flex-shrink: 0;
    }

    .f-item.active .f-dot {
      background: var(--brand);
    }

    .f-name {
      flex: 1;
      min-width: 0;
      font-size: 12.5px;
      color: var(--ink-2);
      line-height: 1.4;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .f-item.active .f-name {
      color: var(--brand-deep);
      font-weight: 600;
    }

    /* 下载/查看/删除: 平时隐形(但仍占位, 免得移上去整行抖一下), 移上去或键盘聚焦才浮出来。
       opacity 而不是 display —— display 切换会让文件名宽度跳变。 */
    .f-acts {
      display: flex;
      gap: 2px;
      flex-shrink: 0;
      opacity: 0;
      pointer-events: none;
      transition: opacity .15s ease;
    }

    .f-item:hover .f-acts,
    .f-item:focus-within .f-acts {
      opacity: 1;
      pointer-events: auto;
    }

    /* 触屏没有 hover: 常驻显示, 否则这两个入口永远点不到 */
    @media (hover: none) {
      .f-acts {
        opacity: 1;
        pointer-events: auto;
      }
    }

    .f-act {
      width: 24px;
      height: 24px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      padding: 0;
      border: none;
      border-radius: 6px;
      background: transparent;
      color: var(--muted);
      cursor: pointer;
      flex-shrink: 0;
      transition: background .15s ease, color .15s ease;
    }

    .f-act:hover {
      background: var(--card);
      color: var(--brand-deep);
    }

    /* 删除是唯一不可撤销的动作: hover 用警示色, 与下载/查看区分开 */
    .f-act.danger:hover {
      color: var(--red);
    }

    .f-act:focus-visible {
      outline: 2px solid var(--brand);
      outline-offset: 1px;
    }

    .f-act svg {
      width: 15px;
      height: 15px;
    }

    .f-empty {
      font-size: 12px;
      color: var(--faint);
      padding: 8px 12px 4px;
      line-height: 1.7;
    }

    /* 空态那颗「试用示例数据」。一份数据都没有时它是左栏唯一的可点项, 而用户此刻的
       处境恰恰是"手头没有文件可传" —— 所以给它一个框, 而不是接着写灰字。做成描边按钮
       而不是实心主色: 主路径仍是上传(文案就在它上面一行), 它是另一条路, 不该更抢眼。
       只在空态渲染(见 renderFileList): 有数据 → 走文件列表那条分支, 这颗按钮根本不在,
       "有数据就不需要示例"不靠额外判断。 */
    .f-demo {
      display: block;
      width: calc(100% - 24px);
      margin: 4px 12px 10px;
      padding: 7px 10px;
      border: 1px solid var(--line);
      border-radius: 8px;
      background: var(--card);
      color: var(--brand);
      font-family: inherit;
      font-size: 12px;
      line-height: 1.5;
      cursor: pointer;
      transition: border-color .15s ease, background .15s ease, color .15s ease;
    }

    .f-demo:hover {
      border-color: var(--brand);
      background: var(--brand-soft);
      color: var(--brand-deep);
    }

    .f-demo:focus-visible {
      outline: 2px solid var(--brand);
      outline-offset: 1px;
    }

    /* ── 开合把手: 就一个图标, 不占单独一列 ──
       钉在 .shell 上(position:absolute), 不在 .sessions 里 —— 侧栏收起时整条滑出去,
       把手跟着走就再没有展开的入口了。展开时它停在「数据文件」那一行的末位,
       收起时随侧栏一起滑到最左边, 始终留在屏幕上; 只动 left, 与侧栏同一时长同一条缓动,
       看起来就是被侧栏"推"过去的。收起后它是浮在内容上的, 所以补个底+描边, 免得压在字上认不出。 */
    .nav-toggle {
      position: absolute;
      /* 与左栏 LOGO 行同心: 行高 56, 按钮 30 → (56-30)/2 */
      top: 13px;
      /* 贴在面板右缘内侧 6px: 300 - 30 - 6 */
      left: calc(var(--nav-w) - 36px);
      z-index: 21;
      width: 30px;
      height: 30px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      padding: 0;
      border: 1px solid transparent;
      border-radius: 8px;
      background: transparent;
      color: var(--muted);
      font-family: var(--font);
      font-size: 15px;
      line-height: 1;
      cursor: pointer;
      flex-shrink: 0;
      transition: left .24s cubic-bezier(.4, 0, .2, 1), background .15s ease,
        color .15s ease, border-color .15s ease, box-shadow .15s ease;
    }

    .nav-toggle:hover {
      background: var(--brand-soft);
      color: var(--brand-deep);
    }

    .nav-toggle:focus-visible {
      outline: 2px solid var(--brand);
      outline-offset: 1px;
    }

    /* 收起后它浮在内容上(窄窗口下正是会话卡片的左上角) —— 贴住左缘、只圆右边两角,
       当成窗口边上的一片拉手, 而不是一块恰好压住卡片的补丁; border-left 留透明 1px
       是为了让盒子尺寸不跳。 */
    .shell.nav-hidden .nav-toggle {
      left: 0;
      border-color: var(--line);
      border-left-color: transparent;
      border-radius: 0 9px 9px 0;
      background: var(--card);
      box-shadow: 2px 1px 6px rgba(3, 30, 95, .13);
    }

    /* ── 拖拽改宽度: 面板右缘那条 7px 的抓取带 ──
       起点取 var(--nav-w) 而不是 --nav-w 减几像素: 落在面板**外面**, 不压住
       .d-list / .vars-body 贴右缘的滚动条(那 8px 是能点的)。视觉只有 3px,
       命中区给到 7px —— 细了不好抓。 */
    .nav-resize {
      position: absolute;
      top: 0;
      bottom: 0;
      left: var(--nav-w);
      width: 7px;
      z-index: 22;
      cursor: col-resize;
      touch-action: none;
      -webkit-user-select: none;
      user-select: none;
      transition: left .24s cubic-bezier(.4, 0, .2, 1), opacity .18s ease;
    }

    /* 常态就露出一小截"握把" —— 只在悬停时才显形的话, 用户不会知道这条边能拖。
       悬停/聚焦/拖拽时只加长, **不变色**: 长度本身就是"抓到了"的反馈, 再叠一层
       品牌蓝反而把这条静默的边做成一个会闪的控件。
       取色用 --faint 而不是 --line: 抓取带落在面板**外面**的浅蓝底(--bg-tint)上,
       --line(#E3EDF7)压在那个底色里几乎看不见。 */
    .nav-resize::after {
      content: "";
      position: absolute;
      top: 50%;
      left: 0;
      width: 3px;
      height: 30px;
      margin-top: -15px;
      border-radius: 0 3px 3px 0;
      background: var(--faint);
      transition: height .15s ease, margin-top .15s ease;
    }

    .nav-resize:hover::after,
    .nav-resize:focus-visible::after,
    body.nav-resizing .nav-resize::after {
      height: 52px;
      margin-top: -26px;
    }

    .nav-resize:focus-visible {
      outline: none;
    }

    /* 收起时面板整条滑走, 抓取带留在原地会指向空气 */
    .shell.nav-hidden .nav-resize {
      opacity: 0;
      pointer-events: none;
    }

    /* 拖拽中: 关掉所有缓动 —— 每帧都过渡一次会让把手追不上指针(橡皮筋);
       user-select 挡住拖过文本时的选区, 光标全屏保持 col-resize */
    body.nav-resizing {
      cursor: col-resize;
      -webkit-user-select: none;
      user-select: none;
    }

    body.nav-resizing .sessions,
    body.nav-resizing .nav-toggle,
    body.nav-resizing .nav-resize {
      transition: none;
    }

    /* ── 分区标题行里的图标按钮(「数据文件」右侧的上传入口) ── */
    .side-btn {
      width: 24px;
      height: 24px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      padding: 0;
      border: 1px solid transparent;
      border-radius: 7px;
      background: transparent;
      color: var(--muted);
      cursor: pointer;
      flex-shrink: 0;
      transition: background .15s ease, color .15s ease;
    }

    .side-btn:hover {
      background: var(--brand-soft);
      color: var(--brand-deep);
    }

    .side-btn:focus-visible {
      outline: 2px solid var(--brand);
      outline-offset: 1px;
    }

    .side-btn svg {
      width: 15px;
      height: 15px;
    }

    .side-btn:disabled {
      opacity: .45;
      cursor: default;
    }

    /* 刷新进行中: 图标转起来(@keyframes spin 与加载圈共用) */
    .side-btn.spin svg {
      animation: spin .7s linear infinite;
    }

    /* 窄屏浮层的遮罩: 只在 ≤900px 生效(见媒体查询) */
    .nav-backdrop {
      display: none;
    }

    /* ── 数据预览弹窗(左栏条目的「查看」) ──
       数据表列多且宽, 300px 的侧栏装不下, 所以用居中弹窗而不是侧栏内展开。
       预览用的数据单独取, 不进对话, 也不占用模型上下文。 */
    .peek {
      position: fixed;
      inset: 0;
      z-index: 60;
      display: none;
      align-items: center;
      justify-content: center;
      padding: 30px 18px;
      background: rgba(3, 30, 95, .38);
    }

    .peek.open {
      display: flex;
    }

    /* 高度钉死, 不按内容撑: 一页 100 行和最后一页剩 3 行, 撑出来的面板会差出四五百像素,
       翻页/筛选一次就跳一次; 翻页条出现或消失(hidden)也再跳一次。760 是上限, 100% 是
       .peek 容器去掉上下各 30px 留白后的可用高度 —— 矮屏自动收到可用高, 不出屏。 */
    .peek-panel {
      display: flex;
      flex-direction: column;
      width: min(1100px, 100%);
      height: min(760px, 100%);
      background: var(--card);
      border-radius: 12px;
      box-shadow: 0 18px 48px rgba(3, 30, 95, .22);
      overflow: hidden;
    }

    .peek-head {
      position: relative;
      /* 高于表头 sticky(z-index 3)，否则「下载数据」下拉会被表头盖住 */
      z-index: 4;
      flex-shrink: 0;
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 13px 16px;
      border-bottom: 1px solid var(--line-soft);
    }

    .peek-title {
      font-size: 14px;
      font-weight: 700;
      color: var(--navy);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .peek-meta {
      flex-shrink: 0;
      font-size: 12px;
      color: var(--faint);
    }

    /* 预览「筛选」: 与下载同排的描边按钮; 有生效条件时加点缀提示 */
    .peek-filt-btn {
      display: inline-flex;
      align-items: center;
      gap: 5px;
      margin-left: auto;
      flex-shrink: 0;
      border: 1px solid var(--line);
      background: var(--card);
      color: var(--ink-2);
      font-size: 12px;
      font-weight: 600;
      font-family: var(--font);
      padding: 4px 12px;
      border-radius: 6px;
      cursor: pointer;
      transition: background .15s ease, color .15s ease, border-color .15s ease;
    }

    .peek-filt-btn svg {
      width: 14px;
      height: 14px;
      flex-shrink: 0;
    }

    .peek-filt-btn:hover {
      border-color: var(--brand);
      color: var(--brand);
      background: var(--brand-soft);
    }

    .peek-filt-btn.on {
      border-color: var(--brand);
      color: var(--brand-deep);
      background: var(--brand-soft);
    }

    /* 预览「下载数据」: 对齐主站蓝描边按钮 + 下拉四格式(均免费) */
    .peek-dl {
      position: relative;
      flex-shrink: 0;
    }

    .peek-dl-btn {
      display: inline-flex;
      align-items: center;
      gap: 5px;
      border: 1px solid var(--brand);
      background: var(--card);
      color: var(--brand);
      font-size: 12px;
      font-weight: 600;
      font-family: var(--font);
      padding: 4px 12px;
      border-radius: 6px;
      cursor: pointer;
      transition: background .15s ease, color .15s ease;
    }

    .peek-dl-btn svg {
      width: 14px;
      height: 14px;
      flex-shrink: 0;
    }

    /* 主按钮右侧那颗开格式菜单的小箭头: 默认不显示, 只在没有悬停的设备上出现(见下面的
       @media (hover: none))。判据照 base.css 开头那张表 —— 它是个**看得见**的东西, 就得
       键在 hover: none 上, 不能键 any-pointer: coarse; 带触摸屏的 Windows 笔记本有悬停,
       悬停就能开菜单, 那颗在那儿只是白占半格。 */
    .peek-dl-caret {
      display: none;
    }

    .peek-dl-btn:hover,
    .peek-dl.open .peek-dl-btn {
      background: var(--brand-soft);
      color: var(--brand-deep);
    }

    /* 这里不必自己抬 z-index: 整个浮层跟着 .peek-head 一起压在表头之上 —— 那一条的
       position/z-index 就是为这个下拉加的(表头 .peek-table thead th 是 sticky, 列一多
       横铺到整条可视宽度, 菜单顶部那一整条正好在它底下), 别当冗余删掉。实测 25 列表格:
       两处都撤掉时, 菜单与表头重叠 168px(整个菜单宽), 在表头带内取点返回 TH 而不是菜单项。 */
    .peek-dl-menu {
      position: absolute;
      top: calc(100% + 6px);
      left: 0;
      min-width: 176px;
      padding: 5px;
      background: var(--card);
      border: 1px solid var(--line-soft);
      border-radius: 8px;
      box-shadow: 0 8px 24px rgba(3, 30, 95, .14);
    }

    .peek-dl-menu[hidden] {
      display: none;
    }

    /* 按钮与菜单之间那 6px 是空的(菜单在 top:100%+6px): 指针从按钮挪到菜单正好穿过它,
       不补一块透明的桥就会被判成"离开"、菜单在手底下收走。铺满这 6px 且与菜单同宽,
       走对角线也盖得住; JS 那边另有一小段缓冲时间兜底。 */
    .peek-dl-menu::before {
      content: "";
      position: absolute;
      left: 0;
      right: 0;
      top: -6px;
      height: 6px;
    }

    /* 项左右各缩 5px(跟菜单内边距同值)并自带圆角: hover 的方块才不会顶破菜单的 8px 圆角 */
    .peek-dl-item {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 8px;
      width: 100%;
      border: 0;
      border-radius: 6px;
      background: transparent;
      color: var(--ink-2);
      font-size: 13px;
      font-family: var(--font);
      text-align: left;
      padding: 9px;
      cursor: pointer;
    }

    .peek-dl-item:hover {
      background: var(--brand-soft);
      color: var(--brand-deep);
    }

    /* 右列格式后缀: 等宽排成一列 —— 一是把删掉 VIP 角标后空出来的右半边填上,
       二是「Excel数字格式」和「Excel文本格式」只差一个字, 后缀是最省字的区分。
       值的口径跟实际下载到的文件名走: csv 那一支出口给的就是 .csv, 不是 .xlsx
       (见 api/dataset.py 的 dataset_download)。 */
    .peek-dl-ext {
      flex-shrink: 0;
      min-width: 40px;
      text-align: right;
      font-family: var(--mono);
      font-size: 11px;
      color: var(--faint);
    }

    .peek-dl-item:hover .peek-dl-ext {
      color: var(--brand-deep);
    }

    /* 菜单底那行说明: 主按钮点一下就直下 Excel, 这层交互不写出来用户会以为点错了
       (鼠标这边菜单是被悬停展开的, 更容易看着菜单去点主按钮)。只给鼠标看 —— 触屏上
       开菜单的那一下点的就是小箭头本身, 拆成两段的意思一目了然, 这句反而是多余的一行。 */
    .peek-dl-note {
      margin-top: 4px;
      padding: 7px 9px 2px;
      border-top: 1px solid var(--line-soft);
      font-size: 11px;
      line-height: 1.5;
      color: var(--faint);
      white-space: nowrap;
    }

    @media (hover: none) {
      .peek-dl-note {
        display: none;
      }

      /* 触屏上「下载数据」拆成两段: 主区点一下直下 Excel 数字格式(与鼠标点它、与左栏那枚
         下载图标同一个默认), 右边那颗小箭头开四种格式的菜单。合成一颗就没有两条路 ——
         要么点一下开菜单、再点一下才选(要两下), 要么一下直下、另外三种格式永远点不到。
         这一版替掉的是原来"触屏点按钮开合菜单": 实测触屏上聚焦本身就是开菜单那一步,
         focusin 先开、click 又切回关, 点一下正好抵消(安卓上还每次重新聚焦都空一下,
         要三下才下到文件); 拆开之后 Excel 一下、其余三种两下。
         border-left: 0 —— 两段中间只留一条竖线(就是主按钮自己的右边框), 否则是双线。 */
      .peek-dl {
        display: flex;
        align-items: stretch;
      }

      .peek-dl-btn {
        border-radius: 6px 0 0 6px;
      }

      .peek-dl-caret {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 26px;
        padding: 0;
        border: 1px solid var(--brand);
        border-left: 0;
        border-radius: 0 6px 6px 0;
        background: var(--card);
        color: var(--brand);
        cursor: pointer;
        transition: background .15s ease, color .15s ease;
      }

      .peek-dl-caret svg {
        width: 13px;
        height: 13px;
      }

      /* 按下去与菜单开着, 都染上主按钮 hover 那套底色: 触屏没有悬停, 这三态就是全部反馈 */
      .peek-dl-caret:active,
      .peek-dl.open .peek-dl-caret {
        background: var(--brand-soft);
        color: var(--brand-deep);
      }
    }

    /* 预览「文字格式」: 勾选后把有值标签的单元格换成标签名(如 1→男) */
    .peek-fmt {
      flex-shrink: 0;
      display: inline-flex;
      align-items: center;
      gap: 5px;
      font-size: 12px;
      color: var(--ink-2);
      cursor: pointer;
      -webkit-user-select: none;
      user-select: none;
    }

    .peek-fmt input {
      margin: 0;
      width: 14px;
      height: 14px;
      accent-color: var(--brand);
      cursor: pointer;
    }

    .peek-close {
      flex-shrink: 0;
      border: 1px solid var(--line);
      background: var(--card);
      color: var(--muted);
      font-size: 11px;
      font-weight: 600;
      font-family: var(--font);
      padding: 3px 11px;
      border-radius: 999px;
      cursor: pointer;
      transition: color .15s ease, border-color .15s ease, background .15s ease;
    }

    .peek-close:hover,
    .peek-close:active {
      color: var(--brand);
      border-color: var(--brand);
      background: var(--brand-soft);
    }

    .peek-body {
      flex: 1;
      min-height: 0;
      overflow: auto;
    }

    .peek-foot {
      flex-shrink: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 12px;
      padding: 10px 16px;
      border-top: 1px solid var(--line-soft);
      background: var(--card);
    }

    .peek-foot[hidden] {
      display: none;
    }

    .peek-page-btn {
      border: 1px solid var(--line);
      background: var(--card);
      color: var(--ink-2);
      font-size: 12px;
      font-weight: 600;
      font-family: var(--font);
      padding: 4px 12px;
      border-radius: 6px;
      cursor: pointer;
      transition: color .15s ease, border-color .15s ease, background .15s ease;
    }

    .peek-page-btn:hover:not(:disabled) {
      color: var(--brand);
      border-color: var(--brand);
      background: var(--brand-soft);
    }

    .peek-page-btn:disabled {
      color: var(--faint);
      cursor: default;
      opacity: .55;
    }

    .peek-page-info {
      font-size: 12px;
      color: var(--muted);
      font-variant-numeric: tabular-nums;
      min-width: 7em;
      text-align: center;
    }

    .peek-msg {
      padding: 24px 18px;
      font-size: 13px;
      color: var(--faint);
      text-align: center;
    }

    .peek-table {
      border-collapse: separate;
      border-spacing: 0;
      font-size: 12.5px;
      color: var(--ink-2);
    }

    .peek-table th,
    .peek-table td {
      padding: 6px 10px;
      border-bottom: 1px solid var(--line-soft);
      border-right: 1px solid var(--line-soft);
      text-align: left;
      white-space: nowrap;
      max-width: 240px;
      overflow: hidden;
      text-overflow: ellipsis;
    }

    .peek-table thead th {
      position: sticky;
      top: 0;
      z-index: 2;
      background: var(--bg-tint);
      color: var(--navy);
      font-weight: 700;
    }

    /* 行号列横向滚动时也钉住, 不然滚到右边就不知道看的是第几行 */
    .peek-table .peek-rn {
      position: sticky;
      left: 0;
      z-index: 1;
      background: var(--card);
      color: var(--faint);
      text-align: right;
      font-variant-numeric: tabular-nums;
    }

    .peek-table thead th.peek-rn {
      z-index: 3;
      text-align: right;
    }

    .peek-table tbody tr:nth-child(even) td {
      background: var(--card-2);
    }

    .peek-table tbody tr:nth-child(even) .peek-rn {
      background: var(--card-2);
    }

    .peek-table td.num {
      text-align: right;
      font-variant-numeric: tabular-nums;
    }

    .peek-table td.nil {
      color: var(--faint);
    }

    /* ── 筛选条件弹窗(叠在预览之上, 用通用 .mdl 壳) ──
       宽度写成 .mdl-panel.filt-panel 而不是光 .filt-panel: 本文件里 .mdl-panel 那条
       (在下面 200 行开外)与它同权重(0,1,0), 靠源码顺序分胜负 —— 排在前面的这条会被
       后面的 470px 顶掉, 面板实测只剩 470, 内宽 438 装不下五个子项(最小合计 446),
       减号就被挤到第二行去。带上 .mdl-panel 前缀是 (0,2,0), 谁前谁后都压得住。
       同样的坑 chat.css 那边的 #acct 踩过, 见 .mdl-acct .mdl-panel 上面的注释。 */
    .mdl-panel.filt-panel {
      width: min(640px, 100%);
    }

    .filt-body {
      gap: 12px;
    }

    .filt-logic select {
      min-width: 88px;
      height: 32px;
      border: 1px solid var(--line);
      border-radius: 6px;
      background: var(--card);
      color: var(--ink-2);
      font-size: 13px;
      font-family: var(--font);
      padding: 0 8px;
      cursor: pointer;
    }

    .filt-rows {
      display: flex;
      flex-direction: column;
      gap: 10px;
      max-height: min(360px, 50vh);
      overflow: auto;
    }

    /* 桌面一行锁死(nowrap): 五个子项最小合计 416px(120+88+120+28+28+四道 8px 缝),
       640px 面板内宽 608 有富余 —— 放得下就不该折, 折了减号会和别的控件错开半行。
       手机上塞不下是另一回事, 那条在 ≤560 的块里改成固定两行。 */
    .filt-row {
      display: flex;
      align-items: center;
      gap: 8px;
      flex-wrap: nowrap;
    }

    .filt-row select,
    .filt-row input[type="text"] {
      height: 34px;
      border: 1px solid var(--line);
      border-radius: 6px;
      background: var(--card);
      color: var(--ink-2);
      font-size: 13px;
      font-family: var(--font);
      padding: 0 10px;
    }

    .filt-row .filt-col {
      flex: 1 1 140px;
      min-width: 120px;
    }

    .filt-row .filt-op {
      flex: 0 1 100px;
      min-width: 88px;
    }

    .filt-row .filt-val {
      flex: 1 1 140px;
      min-width: 120px;
    }

    /* 值这一格的尺寸写在 host 上: 里面装什么(有值标签是下拉、没有是输入框)由 JS
       按变量换, 外壳不跟着换。这三句原先内联在 JS 拼的那段 HTML 里(见 addFiltRow),
       内联优先级压过媒体查询 —— 手机端要让它另起一行就排不动, 所以搬进 CSS。
       不涨(flex-grow 0)、定宽 160: 这一格只装数字或「本科以下」这类短标签, 用不着
       跟变量下拉平分富余 —— 改前两家各 flex-grow 1, 富余对半分, 值格实测 210,
       和变量下拉一样宽(一个「男」字独占 210px)。省下的宽度让给变量下拉: 实测 1280
       下变量 210 → 260、值格 210 → 160; 桌面最窄那档 561 变量 145、值格仍 160,
       再窄就进 ≤560 的手机布局, 那边这一格是整行(见块里的 calc(100% - 104px))。
       160 这个数的代价(也是实测): 可用文字宽约 120(减边框 2、内边距 20、下拉箭头
       18), 本地那份 46 变量的清单里, 有 8 个变量的最长标签超过它, 收起来时会**硬裁**
       (Chrome 不给省略号, 如「中层（如年级书记/主任）」显示成「中层（如年级书记/主」)
       —— 下拉展开时仍是全的。要一个都不裁得写到 180(实测: 180 与 210 一样只裁 2 个,
       但变量名放得下的从 15 个降到 13 个)。 */
    .filt-row .filt-val-host {
      flex: 0 1 160px;
      min-width: 120px;
      display: flex;
    }

    .filt-row select:focus,
    .filt-row input:focus,
    .filt-logic select:focus {
      outline: none;
      border-color: var(--brand);
      box-shadow: 0 0 0 2px var(--brand-soft);
    }

    .filt-add,
    .filt-rm {
      flex-shrink: 0;
      width: 28px;
      height: 28px;
      border: 1px solid var(--line);
      border-radius: 6px;
      background: var(--card);
      color: var(--muted);
      font-size: 16px;
      line-height: 1;
      cursor: pointer;
      display: inline-flex;
      align-items: center;
      justify-content: center;
    }

    .filt-add:hover,
    .filt-rm:hover {
      border-color: var(--brand);
      color: var(--brand);
      background: var(--brand-soft);
    }

    .filt-foot {
      justify-content: flex-end;
      padding: 0 16px 16px;
      border-top: 0;
    }

    .filt-cancel {
      border: 0;
      background: transparent;
      color: var(--muted);
      font-size: 13px;
      font-family: var(--font);
      cursor: pointer;
      padding: 7px 12px;
    }

    .filt-cancel:hover {
      color: var(--brand);
    }

    /* ═══════════════════════════════════════════════════════════════
       预览与筛选的窄屏适配
       断点只有 900 / 720 / 560 三档, 与全站一致。下面全是**看得见**的排面改动,
       所以键在宽度上(与 chat.css 里 #acct 的 ≤560 块同一口径); 隐形的命中区不在这儿,
       在文件末尾的 any-pointer: coarse 块里 —— 两处判据的分工见 base.css 开头。
       ═══════════════════════════════════════════════════════════════ */

    /* ── ≤900: 预览头部允许折行 ──
       这一行里装着「标题 + 元信息 + 筛选查看 + 下载数据 + 文字格式 + 关闭」六样, 自然
       宽度约 554px。768/820 的平板有富余, 这条对它不生效(flex 排得下就不折); 但
       561~638 这段(手机横屏、拖窄的窗口)放不下 —— 不折的话排在前面的标题被压成 0 宽
       (它的 box 被挤没了), 末尾的关闭键直接跑到屏外, 整个弹窗关不掉。
       只写折行、不写 flex-basis: 折在哪由可用宽度自己决定, 不去规定它折成几行。 */
    @media (max-width: 900px) {
      .peek-head {
        flex-wrap: wrap;
        row-gap: 6px;
      }

      .peek-title {
        flex: 1 1 auto;
        min-width: 0;
      }
    }

    /* ── ≤560 手机: 预览铺满整屏 ──
       小屏上居中卡片是最差的选择: 四边让掉 30/18px 留白后, 数据表只剩一条缝, 而这张表
       的列本来就多到要横向滚。铺满之后头部才有地方排成三行, 关闭键也一定在屏内。
       全屏先例是同在窄屏改全屏的图表抽屉(见 chat.css)。
       理由同 #acct: 手机上要的是一屏能用的界面, 不是缩小版的桌面弹窗。 */
    @media (max-width: 560px) {
      .peek {
        padding: 0;
      }

      .peek-panel {
        width: 100%;
        height: 100%;
        border-radius: 0;
      }

      .peek-head {
        padding: 10px 12px;
      }

      /* 三行: ①标题+关闭 ②元信息独占一行 ③三颗控件。
         元信息那句(「12 列 · 第 1–100 行(共 1000 行) · 已筛选」)自然宽 209px, 留在标题
         那一行会把标题挤没, 所以给它 flex-basis: 100% 单独占一行。 */
      .peek-title {
        order: 1;
      }

      .peek-close {
        order: 2;
      }

      .peek-meta {
        order: 3;
        flex-basis: 100%;
      }

      /* 三颗控件给到 40px: 与 --tap(44) 只差 4px, 而 44 会把这一行撑得比数据区还抢眼。
         剩下那几像素由 any-pointer: coarse 的隐形热区补上。 */
      .peek-filt-btn {
        order: 4;
        margin-left: 0;
        height: 40px;
      }

      .peek-dl {
        order: 5;
      }

      .peek-dl-btn {
        height: 40px;
      }

      .peek-fmt {
        order: 6;
      }

      /* 下载浮层改成向左展开: 桌面那套 left: 0 是从按钮左缘往右铺 168px, 手机上右半截
         必然出屏, 再被 .peek-panel 的 overflow: hidden 裁掉。贴住按钮右缘往左铺就全在屏内。 */
      .peek-dl-menu {
        left: auto;
        right: 0;
      }

      /* ── 筛选条件行折成固定两行 ──
         五个控件(变量/运算/值/加/减)手机上塞不进一行, 而"塞不下的结果"是减号被挤到
         第二行、和别的控件错开半行。改成固定两行: 变量+运算 一行, 值+加+减 一行。 */
      /* 带 .mdl 前缀(0,2,0): 光写 .mdl-filt 与下面 .mdl 那条同权重, 而 .mdl 在文件里
         排在更后面, 会被它的 24/18 顶掉 —— 与本文件 .filt-panel 踩的是同一个坑。 */
      .mdl.mdl-filt {
        padding: 12px;
      }

      .filt-row {
        flex-wrap: wrap;
      }

      /* basis 必须写死 0, 不能写 auto: 下拉的 flex-basis: auto 取的是**内容宽**, 也就是
         最长那个变量名 —— 真实清单里能到 21 个字(「在正常的工作日内, 您平均每天的实际
         工作时间」), 折行时拿这个宽度去比, 变量自己就占满一整行, 一个条件行变三行。
         写 0 之后变量按 0 参与折行、再涨满该行剩下的宽度, 才是要的两行。
         实测 430: auto 时 col 374(整行)、值格 278、行高 148(三行); 0 时 col 278、值格 270、
         行高 96(两行: 变量+运算 / 值+加+减)。 */
      .filt-row .filt-col {
        flex: 1 1 0;
        min-width: 0;
      }

      .filt-row .filt-op {
        flex: 0 0 auto;
      }

      /* 值另起一行, 并给右边两颗按钮留出位置: 104 = 8 + 44 + 8 + 44(两道 8px 缝加两颗
         44px 按钮)。这儿**不能写 100%** —— 100% 会把加号减号挤到第三行去, 一个条件行
         变三行。无头 Chrome 上量过 430/375 两档, 两档结果一致: 100% 行高 148(三行,
         加减不与值同行), calc 行高 96(两行, 加减与值同行)。 */
      .filt-row .filt-val-host {
        flex: 1 1 calc(100% - 104px);
        min-width: 0;
        order: 3;
      }

      .filt-row .filt-add,
      .filt-row .filt-rm {
        order: 4;
        width: 44px;
        height: 44px;
      }

      /* 触屏控件一律撑到 44: 手机上 34px 高的下拉/输入框按不准(那正是 --tap 那个数) */
      .filt-row select,
      .filt-row input[type="text"] {
        height: 44px;
      }

      .filt-row .filt-val {
        width: 100%;
        min-width: 0;
      }

      .filt-logic select {
        height: 44px;
      }

      .filt-foot .mdl-btn,
      .filt-cancel {
        min-height: 44px;
      }
    }

    /* ── 通用弹窗壳(mdl) ──
       购买 AI 点、数据授权共用: 固定遮罩 + 居中面板 + 头/体 + 点遮罩/✕/Esc 关。
       与数据预览(.peek)是两套: peek 自己那套宽度到 1100px(要铺数据表), 这里是小面板。
       z-index 62 压过预览(60)和图表抽屉(50) —— 都是从「+」菜单或侧栏点开的,
       底下可能还开着别的层。 */
    .mdl {
      position: fixed;
      inset: 0;
      z-index: 62;
      display: none;
      align-items: center;
      justify-content: center;
      padding: 24px 18px;
      background: rgba(3, 30, 95, .38);
    }

    .mdl.open {
      display: flex;
    }

    .mdl-panel {
      width: min(470px, 100%);
      max-height: 100%;
      display: flex;
      flex-direction: column;
      background: var(--card);
      border-radius: 12px;
      box-shadow: 0 18px 48px rgba(3, 30, 95, .22);
      overflow: hidden;
    }

    .mdl-head {
      flex-shrink: 0;
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 13px 16px;
      border-bottom: 1px solid var(--line-soft);
    }

    .mdl-title {
      font-size: 14px;
      font-weight: 700;
      color: var(--navy);
    }

    .mdl-close {
      margin-left: auto;
      flex-shrink: 0;
      border: 1px solid var(--line);
      background: var(--card);
      color: var(--muted);
      font-size: 11px;
      font-weight: 600;
      font-family: var(--font);
      padding: 3px 11px;
      border-radius: 999px;
      cursor: pointer;
      transition: color .15s ease, border-color .15s ease, background .15s ease;
    }

    .mdl-close:hover,
    .mdl-close:active {
      color: var(--brand);
      border-color: var(--brand);
      background: var(--brand-soft);
    }

    .mdl-body {
      flex: 1;
      min-height: 0;
      overflow: auto;
      display: flex;
      flex-direction: column;
      gap: 14px;
      padding: 16px;
    }

    .mdl-lead {
      font-size: 12.5px;
      line-height: 1.75;
      color: var(--muted);
    }

    /* 授权弹窗的清单口径提示(仅未授权): 一条浅蓝提示条。口径说明是"这列表是什么"的
       陈述, 混在导语里会被当成第二句操作引导读过去; 单独成条(底 --brand-soft + 细蓝边,
       与 .t-num 的提示配色同族)才一眼认得出。负 margin 把它和上方导语收成一组。 */
    .auth-hint {
      display: flex;
      align-items: center;
      gap: 7px;
      margin: -6px 0 0;
      padding: 7px 10px;
      border-radius: 8px;
      border: 1px solid var(--line-strong);
      background: var(--brand-soft);
      font-size: 12px;
      line-height: 1.6;
      color: var(--brand-deep);
    }

    .auth-hint svg {
      flex-shrink: 0;
    }

    .auth-hint b {
      font-weight: 600;
    }

    /* 授权弹窗里的文件名搜索: 与左栏 .v-search 同款皮, 但夹在提示条与清单之间,
       左右按面板自身的内边距对齐, 不吃 .v-search 那对负 margin。 */
    .auth-search {
      flex-shrink: 0;
      margin: 0;
      padding: 6px 10px;
      border: 1px solid var(--line);
      border-radius: 8px;
      font-size: 12.5px;
      font-family: var(--font);
      color: var(--ink-2);
    }

    .auth-search:focus {
      outline: none;
      border-color: var(--brand);
      box-shadow: var(--ring);
    }

    .mdl-foot {
      display: flex;
      align-items: center;
      gap: 10px;
      padding-top: 2px;
    }

    .mdl-note {
      flex: 1;
      font-size: 11.5px;
      line-height: 1.6;
      color: var(--faint);
    }

    .mdl-btn {
      flex-shrink: 0;
      padding: 7px 18px;
      border: none;
      border-radius: 8px;
      background: var(--brand);
      color: #fff;
      font-family: var(--font);
      font-size: 13px;
      font-weight: 600;
      cursor: pointer;
    }

    /* 功能没开: 按钮做成明确的"点不动", 别让它看起来像点了没反应 */
    .mdl-btn[disabled] {
      background: var(--ghost);
      cursor: not-allowed;
    }

    /* ── 授权弹窗里的数据清单(#auth) ──
       一行一份: 左边文件名 + 上传日期(上下两行), 右边一颗「授权」。
       行高按 48 定(padding 9 + 按钮 30): 触屏那块要给按钮撑起 44 的命中区(见下面
       @media (any-pointer: coarse)), 而命中区是**以按钮为中心**往外撑的 —— 行不够高
       就会越到邻行去, 那正是"抢点击"。48 刚好把这个 44 装在自己这一行里。 */
    .auth-docs {
      display: flex;
      flex-direction: column;
      gap: 6px;
      /* 清单可能很长(几十份数据): 面板总高已有上限(max-height: 100%), 这里再给清单
         一个自己的上限, 让页脚那句说明与「刷新列表」始终露在外面 */
      max-height: 46vh;
      overflow-y: auto;
      scrollbar-width: thin;
      scrollbar-color: var(--line-strong) transparent;
    }

    .auth-row {
      flex-shrink: 0;
      display: flex;
      align-items: center;
      gap: 10px;
      min-height: 48px;
      padding: 9px 10px;
      border: 1px solid var(--line-soft);
      border-radius: 9px;
      background: var(--bg-tint);
    }

    .auth-meta {
      flex: 1;
      min-width: 0;
      display: flex;
      flex-direction: column;
      gap: 2px;
    }

    /* 文件名是用户自己传的, 可能很长: 一行截断 —— 不然它会把右边的「授权」挤出面板 */
    .auth-name {
      font-size: 12.5px;
      font-weight: 600;
      color: var(--ink-2);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .auth-date {
      font-size: 11px;
      color: var(--faint);
    }

    .auth-grant {
      flex-shrink: 0;
      position: relative;      /* 触屏那块撑命中区的基准 */
      padding: 5px 14px;
      border: 1px solid var(--brand);
      border-radius: 999px;
      background: var(--card);
      color: var(--brand);
      font-family: var(--font);
      font-size: 12px;
      font-weight: 600;
      cursor: pointer;
      transition: background .15s ease, color .15s ease;
    }

    .auth-grant:hover {
      background: var(--brand);
      color: #fff;
    }

    /* 已授权 / 正在授权中: 收成灰的并封住光标, 别让它看着还能再点一下 */
    .auth-grant[disabled] {
      border-color: var(--line);
      color: var(--faint);
      background: var(--card);
      cursor: default;
    }

    /* 加载中 / 没有数据 / 读取失败: 整块就一句说明 —— 空盒子比一句"还没有数据"更让人
       以为是没加载出来(与 #ptsBody 空态那句同一个道理) */
    .auth-msg {
      margin: 0;
      padding: 14px 2px;
      font-size: 12.5px;
      line-height: 1.7;
      color: var(--faint);
    }

    /* ── 变量列表(左栏下半, 常驻) ──
       左栏窄, 所以每行两行式: 第一行 tid + 变量名 + 类型徽章, 第二行小字概况/值标签 */
    .v-search {
      flex-shrink: 0;
      margin: 0 12px 8px;
      padding: 5px 9px;
      border: 1px solid var(--line);
      border-radius: 7px;
      font-size: 12px;
      font-family: var(--font);
      color: var(--ink-2);
    }

    .v-search:focus {
      outline: none;
      border-color: var(--brand);
      box-shadow: var(--ring);
    }

    /* 分区标题行右侧的小文字按钮, 两处共用一套皮:
       变量列表「刷新」(v-btn, 变量=variables)、数据文件「授权」(d-btn, 数据=data)。
       图标按钮那套是 .side-btn, 用来放"刷新/上传"这种一眼看得懂的图形。 */
    .v-btn,
    .d-btn {
      padding: 3px 8px;
      border: 1px solid var(--line);
      border-radius: 6px;
      background: var(--card);
      color: var(--ink-2);
      font-size: 11.5px;
      font-family: var(--font);
      font-weight: 400;
      cursor: pointer;
      flex-shrink: 0;
    }

    .v-btn:hover,
    .d-btn:hover {
      border-color: var(--brand);
      color: var(--brand-deep);
    }

    /* 「行 × 列」后面那两颗(下载 / 查看): 与「刷新」同一套皮(白底描边), 只是做成方块。
       这一行本来就贴着折行线, 所以只用图标 —— 换成文字按钮(「查看」两个字 ≈ +18px)当场
       就把「刷新」挤下去。加了第二颗之后实测(≤900 那一档, 内容宽 = 面板宽 − 24, 最长那条
       meta 是「12480 行 × 156 列」≈ 98px):
         w=300(手机默认宽) 单行 43px —— 五样都放得下;
         w=260             「300 行 × 12 列」起转两行(未加这颗时还是单行);
         w=220             加不加都已是两行, 与这颗无关。
       折行是这一行**设计好的**兜底(见 ≤900 那段的 flex-wrap + row-gap), 不是坏了 ——
       拖窄了就让「刷新」带着 margin-left:auto 去第二行靠右站住。 */

    .v-peek {
      width: 22px;
      height: 22px;
      padding: 0;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      border: 1px solid var(--line);
      border-radius: 6px;
      background: var(--card);
      /* 图标比同行的 11.5px 文字略亮一档: 1.8 的线宽吃色, 用 --ink-2 会盖过「刷新」 */
      color: var(--muted);
      cursor: pointer;
      flex-shrink: 0;
    }

    .v-peek:hover {
      border-color: var(--brand);
      color: var(--brand-deep);
    }

    .v-peek svg {
      width: 14px;
      height: 14px;
    }

    .v-peek:focus-visible {
      outline: 2px solid var(--brand);
      outline-offset: 1px;
    }

    /* 没数据 / 读取失败时收起(见 setVarsMeta)。必须显式写 —— 上面的 inline-flex 会盖掉
       [hidden] 自带的 display: none。 */
    .v-peek[hidden] {
      display: none;
    }

    .vars-body {
      flex: 1;
      overflow-y: auto;
      min-height: 0;
      scrollbar-width: thin;
      scrollbar-color: var(--line-strong) transparent;
    }

    /* 整行都可点(见 renderVars 里挂在 .v-row 上的 onclick); 悬停只变色, 不加下划线 */
    .v-row {
      padding: 6px 12px 7px;
      border-bottom: 1px solid var(--line-soft);
      cursor: pointer;
    }

    .v-row:last-child {
      border-bottom: none;
    }

    .v-row:hover {
      background: var(--card-2);
    }

    .v-head-row {
      display: flex;
      align-items: center;
      gap: 6px;
    }

    .v-tid {
      font-family: var(--mono);
      font-size: 10.5px;
      color: var(--faint);
      flex-shrink: 0;
      min-width: 15px;
    }

    .v-name {
      font-size: 12.5px;
      color: var(--ink);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      transition: color .12s ease;
    }

    .v-row:hover .v-name {
      color: var(--brand);
    }

    .v-head-row .t-badge {
      margin-left: auto;
      flex-shrink: 0;
      display: none
    }

    .v-labels {
      display: block;
      font-size: 10.5px;
      color: var(--faint);
      margin: 1px 0 0 21px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .v-msg {
      padding: 14px 12px;
      font-size: 12px;
      color: var(--faint);
      text-align: center;
    }

    .t-badge {
      display: inline-block;
      padding: 1px 7px;
      border-radius: 999px;
      font-size: 11px;
      border: 1px solid;
      white-space: nowrap;
    }

    /* 括号里的细分说明: 与主类型同色, 只压一档透明度 */
    .t-badge .t-sub {
      opacity: .7;
    }

    /* 类型徽章只有三套色: 定量(蓝) / 定类(琥珀) / 其它(灰)。
       「01 二分类」「应该量表」这类细分不另配色, 只在同一枚徽章里淡一档(见 .t-sub)。 */
    .t-num {
      color: var(--brand-deep);
      background: var(--brand-soft);
      border-color: var(--line-strong);
    }

    .t-cat {
      color: var(--amber-deep);
      background: var(--amber-soft);
      border-color: var(--amber-border);
    }

    .t-unknown {
      color: var(--muted);
      background: var(--card-2);
      border-color: var(--line);
    }

    /* ── 窄屏: 数据栏改为浮层(默认收起) ──
       浮层会盖住内容, 所以它有两条退路: 遮罩(点一下收起)和 Esc(见 JS 里的 keydown)。
       收起后整条滑走, 入口只剩 .nav-toggle(它钉在 .shell 上, 不在浮层里)。 */
    @media (max-width: 900px) {
      .sessions {
        position: absolute;
        top: 0;
        bottom: 0;
        left: 0;
        z-index: 20;
        box-shadow: 0 0 24px rgba(3, 30, 95, .14);
      }

      /* ── 抓取带降到侧栏浮层**之下** ──
         .nav-resize 的 z-index 是 22、在 .sessions(20) 之上 —— 桌面端没问题, 因为那
         时 .sessions 只是普通 flex 子项、没有 z-index, 不自成层叠上下文, 里头的账号小
         面板(.acct-pop, z-index 40)与抓取带是**同一层**比大小, 40 > 22 压得住。
         可这一档 .sessions 拿到了 z-index 20 → 自成层叠上下文, 面板再怎么设高也逃不出
         这个 20 的壳, 于是 20 < 22, 抓取带横着压在面板上。
         面板宽 266、左缘 12(右缘 278), 而抽屉宽度是 min(max(220, 屏宽-72), 300) ——
         屏宽 ≲350 时抽屉窄于 278, 面板溢出的那截正落在抓取带上, 表现就是"点头像弹出
         的面板上横着一条竖线"。抽屉被拖窄到 220 时任何屏宽都会撞上。
         抓取带本来就落在面板**外面**(left: var(--nav-w), 与侧栏的盒子不重叠), 不需要
         压过侧栏 —— 降到 20 以下, 让侧栏里的一切(含那张面板)都盖在它上面即可; 仍高于
         遮罩(15)与内容(1)。它只改绘制次序, 命中区(触屏 20px)与拖拽行为一个像素不动。 */
      .nav-resize {
        z-index: 19;
      }

      /* ── 分区标题行在窄抽屉里自己折行 ──
         手机(≤540)上 JS 会把抽屉夹到 NAV_W_MIN = 220(navWMax 那条), 一行的内宽只剩
         196px, 而这一行装着「数据文件 + 20份 + 授权 + 刷新 + 上传」五样 —— 塞不下的
         结果是两个 <span> 各自折行, 屏幕上出现"数据文/件"和"20/份"这种半截标题。
         那不是密, 是坏了。这两条一起用才成立:
           nowrap  —— 标题和计数绝不在字中间断开;
           wrap    —— 一行放不下就把整颗按钮挪到第二行(按钮自己不拆)。
         只在 ≤900 这一档写: 抽屉在这档才可能是浮层、才可能被夹到 220。平板 744 拿到
         424 宽, 这两条对它不生效(flex 有富余就不折行); 桌面默认 300 也一样。 */
      .d-sec-h {
        flex-wrap: wrap;
        /* 列间距 6 → 5: 手机抽屉宽 220、内宽 195, 而这一行五个子项合计 171 加四道 6px
           的缝正好 195 —— 卡在等号上, 亚像素一舍入最右边那颗(上传)就被挤到第二行去,
           孤零零挂在那儿像掉了。收 1px 换 4px 余量, 够它站住。
           行间距 2 是备用的: 计数涨到三位数时这一行会重新放不下, 那时走折行而不是断字。 */
        column-gap: 5px;
        row-gap: 2px;
      }

      .d-sec-h>span {
        white-space: nowrap;
      }

      /* z-index 夹在中间: 内容(.container, z-index 1)之上、侧栏(z-index 20)之下 */
      .nav-backdrop {
        display: block;
        position: absolute;
        inset: 0;
        z-index: 15;
        background: rgba(3, 30, 95, .3);
        opacity: 0;
        pointer-events: none;
        transition: opacity .24s ease;
      }

      .shell:not(.nav-hidden) .nav-backdrop {
        opacity: 1;
        pointer-events: auto;
      }
    }

    /* ═══════════════════════════════════════════════════════════════
       触屏 / 平板: data-panel.css 这一侧
       判据分工见 base.css 开头 —— (hover: none) 担看得见的, (any-pointer: coarse)
       只担隐形的热区。
       ═══════════════════════════════════════════════════════════════ */

    @media (hover: none) {

      /* ── 左栏那条抓取带: 7px → 20px ──
         手指按不准 7px, 而它带着 touch-action: none —— 落在上面滚动会被吞掉, 太窄就
         成了一条用户不知道、蹭到却卡一下的"暗礁"。握把那截 3×30 的 --faint 是常态
         可见的(见 .nav-resize::after), 所以这个宽度有对应的可见物。
         不给 display: none: 那会白白丢掉调宽能力; 代价只是触屏上双击复位没了
         (dblclick 见 JS), 真机键盘的方向键仍可用。
         它 left 定在 var(--nav-w), 所以 20 是往**右**长进对话列 —— 吃到左边 6px 的
         留白和约 7px 正文。那是这条边所在的位置推出来的, 记在这儿不当它不存在。
         绘图栏那条同宽(见 chat.css), 那条 right 定的位, 往左长进 .container 的 20px 内边距,
         停在 .chat 的右缘上, 不压字。 */
      .nav-resize {
        width: 20px;
      }

      /* 数据区那条横的抓取带同理: 7px → 20px 高, 手指才瞄得准(它是 flex 行,
         高度长出来会直接挤掉两侧, 这里取 20 已经够贴手; 与 .nav-resize 一样
         触屏上双击复位没了, 键盘上下键仍可用)。 */
      .files-resize {
        height: 20px;
      }
    }

    /* ── 触屏: 手指的命中区 ──
       只改"点得到点不到", 排面一个像素都不动 —— 撑起来的是一个透明的块。
       为什么不把盒子本身放大: 这些按钮都挤在 24px 的行里(.f-act 三颗只隔 2px), 盒子一
       放大那一行就得跟着重排, 顺手把整块面板的密度改掉 —— 平板该看到的是同一份界面,
       只是手指够得着。
       只往**上下**撑, 不往左右: 横向撑会和左右邻居的自身命中区叠上, 叠了就是抢点击。
       判据用 any-pointer: coarse: 接了触控板的 iPad 报 hover: hover, 但手指还在屏幕上,
       这条得照样生效; 而它只有命中区、没有外观, 落在带触摸屏的 Windows 笔记本上也无害。 */
    @media (any-pointer: coarse) {

      /* 预览里的那颗关闭键本来就在; 同排的筛选/下载/文字格式、以及底下的翻页键一并补上
         —— 它们都只有 23~26px 高, 手指按不准。手机上(≤560)这几颗另有一份 40px 的排面,
         这里是给平板(561~900, 排面同桌面)兜底的。下载浮层的菜单项不在此列: 它们上下
         紧挨着, 纵向撑开会盖住邻项、变成抢点击。

         .mdl-foot .mdl-btn 是同一个 .mdl-btn, 但筛选浮层页脚(.filt-foot)在 ≤560 里
         另给了 min-height: 44px, 弹窗页脚这边一直没给 —— #auth 的「刷新列表」、
         #confirm 的「取消 / 开通会员」、#pts 的「购买会员, 到账 AI 点」都停在内边距 7
         + 13px 字 = 34。这里只撑热区、不用真实高度, 两个理由:
           ① 高度是看得见的, 而这个块的门槛是"设备上有没有手指"(带触摸屏的 Windows
              笔记本也命中), 长 10px 就破了那条分工(见 chat.css 的 .set-tab 那条同款);
           ② #pts 页脚底下只剩 14px —— 那 14 正好是 ≤560 里 .mdl-pts 收内边距换来的
              320×568 不滚的账, 真长到 44 就又把滚动条推出来。
         纵向上下各伸 5px, 碰不到邻行(页脚上是 .mdl-body 的 12~14px 缝); 横向不出按钮
         自己的框, 所以 #confirm 里并排的取消/开通会员不会互相盖 —— 那两颗本来就 10px 缝。
         带 .mdl-note 的页脚(说明字在左、按钮在右)同理, 热区只盖按钮那一格。

         **:not(.filt-foot) 是必须的**: 筛选浮层的页脚也是 .mdl-foot(见 HTML 的
         <div class="mdl-foot filt-foot">), 而它上面紧挨着 .filt-rows 那个 overflow: auto
         的滚动容器, 且 .filt-foot 自己没有 padding-top —— 最后一行筛条件可以正好停在
         容器下沿。热区往上的那 5px 就会盖住那一行右端那颗「+」, 用户瞄着「+」的下缘
         点到的却是页脚的「应用」。这正是本文件前面警告过的"抢点击", 所以那一处在
         ≤560 里另用真实高度解决(它是个宽度档, 平板的排面同桌面, 不在那条里)。 */
      .side-btn::after,
      .nav-toggle::after,
      .peek-close::after,
      .peek-filt-btn::after,
      .peek-dl-btn::after,
      .peek-dl-caret::after,
      .peek-fmt::after,
      .peek-page-btn::after,
      .mdl-close::after,
      .v-btn::after,
      .d-btn::after,
      .auth-grant::after,
      .mdl-foot:not(.filt-foot) .mdl-btn::after {
        content: "";
        position: absolute;
        left: 0;
        right: 0;
        top: 50%;
        height: var(--tap);
        margin-top: calc(var(--tap) / -2);
      }

      /* 上面这批是常态定位(static), 得先给出基准。.nav-toggle 本来就是 absolute
         (它钉在 .shell 上), 写 relative 会把它从原处拽回文档流 —— 所以排除在外。 */
      .side-btn,
      .peek-close,
      .peek-filt-btn,
      .peek-dl-btn,
      .peek-dl-caret,
      .peek-fmt,
      .peek-page-btn,
      .mdl-close,
      .v-btn,
      .d-btn,
      .auth-grant,
      .mdl-foot:not(.filt-foot) .mdl-btn {
        position: relative;
      }

      /* ── 文件行那颗(查看/下载/删除): 只能撑到一行高, 不能撑到 44 ──
         这一栏的行距是 --f-row = 27px, 而每个文件行在**同一个位置**都有一组 .f-act。
         44 的命中块会越过行界、盖住上下各一行 —— 那两颗是**别的文件**的按钮, 点歪一下
         就是删错文件。所以这里刻意不取 44, 收到一整行高: 铺满自己这行, 一行不多占。
         27 比 24 只多 3px, 确实也不算好按 —— 要真正解决得把触屏下的 --f-row 抬高
         (行距一改, 列表高度和 max-height 那笔账也要跟着重算), 那是另一件事, 不混在
         这次里做。 */
      .f-act::after {
        content: "";
        position: absolute;
        left: 0;
        right: 0;
        top: 50%;
        height: var(--f-row);
        margin-top: calc(var(--f-row) / -2);
      }

      .f-act {
        position: relative;
      }
    }
