    /* ── 左栏门头: LOGO ──
       字标的尺寸/字号/字重照抄外部那份 index.html(.logo-text),
       但**不带底色** —— 那份的深底是 .sidebar 的 #111827, 这套左栏是白的, 不铺。
       底色一去, 字就不能再是白的了(白字压白底等于没字), 改用 --brand。 */
    .brand {
      flex-shrink: 0;
      display: flex;
      align-items: center;
      gap: 10px;
      height: var(--h-head);
      padding: 0 12px;
      border-bottom: 1px solid var(--line);
      min-width: 0;
    }

    /* 字标: 那份文件里 .logo-text 本身只有 nowrap, 排版是从外层 .logo 继承的
       (14px / 600), 这里照搬到 .brand-link 上, 由字标继承 —— 字重除外:
       字标比外层再重一档(700), 压住旁边浅灰的 "AI Agent" 后缀。 */
    .brand .logo-text {
      white-space: nowrap;
      font-weight: 700;
      transition: color .15s ease;
    }

    /* 字标后缀 "AI Agent": 它是 SPSSAU 品牌**旗下**的平台名, 不是第二个牌子 ——
       所以小一档(13px)、退成次级灰, 主次一眼分得开。
       基线对齐而不是居中: 小字压在大字的基线上, 才像印刷体的"品牌 + 型号";
       居中会让两行字的上下缘都错开, 读着像两个并排的控件。
       gap 比 .brand-link 的 10px 小: 那 10px 是 mark 与字标之间的距离。 */
    .brand .logo-word {
      display: inline-flex;
      align-items: baseline;
      gap: 6px;
      min-width: 0;
    }

    .brand .logo-sub {
      font-size: 13px;
      font-weight: 400;
      letter-spacing: .6px;
      color: var(--muted);
      white-space: nowrap;
      transition: color .15s ease;
    }

    /* mark 或字标 → 官网(新标签)。悬停只变色, 不加下划线 */
    .brand-link {
      display: inline-flex;
      align-items: center;
      gap: 10px;
      min-width: 0;
      text-decoration: none;
      color: var(--brand);
      /* 参照文件是 14px, 这套看着偏小, 抬到 18px, 后来又抬到 20 —— 它是这一行的主角:
         56px 的门头里就它一个要抢眼, 18 那档在 1440 窗口下仍像个副标题 */
      font-size: 18px;
      font-weight: 600;
    }

    .brand-link:hover .logo-text {
      color: var(--brand-deep);
    }

    /* 后缀跟着一起加深一档, 免得悬停时字标动了它没动, 看着像掉队 */
    .brand-link:hover .logo-sub {
      color: var(--ink-2);
    }

    .brand-link:focus-visible {
      outline: 2px solid var(--brand);
      outline-offset: 3px;
      border-radius: 10px;
    }

    /* ── 门头: 语言切换(图标键 + 下拉) ──
       尺寸跟右缘那颗「开合把手」对齐(同为 30×30 / 圆角 8): 同一行里的两颗图标键大小
       不齐会显脏。不并排是刻意的, 见 index.html 里 .lang 的注释。 */
    .lang {
      position: relative;
      display: flex;
      align-items: center;
      flex-shrink: 0;
      /* 在 .brand 的 10px gap 之上再让开 6: 那 10px 是"mark 与字标之间"的口径,
         地球照着这个距离贴上去会被读成字标的一部分, 而不是门头上的一颗控件 */
      margin-left: 6px;
    }

    .lang-btn {
      /* 原来是定宽 30(纯图标)。带上语种名后随内容走, 但下限仍是 30 —— 名称被
         .lang-tight 收掉时(见下), 它跟右缘那颗开合把手还是同一个尺寸 */
      min-width: 30px;
      height: 30px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      padding: 0 8px;
      gap: 6px;
      /* 光学对齐, 不是盒对齐: 字标是文字, 行盒在基线之下还留着一截降部空隙, "SPSSAU"
         全大写用不到那截, 于是大写字母的墨迹重心比盒心低一截 —— 按盒居中, 地球就比
         字标高一线, 下移这一格才齐。
         数字是 3 倍截图数出来的墨迹行(字号 20): 字标 21.7~36.0, 重心 28.8; 地球
         20.7~36.0, 重心 28.3。字标从 18 抬到 20 时它自己下沉 0.3px, 于是这一格补完
         还差 0.5px —— 落到半像素上了: 改成 2px 会往另一边偏半个, 一样, 那就不动。 */
      position: relative;
      top: 1px;
      /* 平时无边框无底色, 悬停才浮出来 —— 与 .nav-toggle 同一套口径, 免得门头上
         常驻两个描边方块, 压过左边那个字标 */
      border: 1px solid transparent;
      border-radius: 8px;
      background: transparent;
      color: var(--muted);
      font-family: var(--font);
      cursor: pointer;
      flex-shrink: 0;
    }

    /* 当前语种名(默认中文, 选过就换)。用自己的写法写(简体中文 / English / Русский),
       与下拉里那列同源 —— 不按界面语言翻译 */
    .lang-cur {
      font-size: 12px;
      line-height: 1;
      white-space: nowrap;
      /* 放不下时先截名字, 不去挤左边那个字标: 字标没有省略号, 被挤就是"SPSSAU AI Agent"
         压在按钮上。真正卡住的是右边那颗把手(见 NAV_W_LANG), 这条只兜字体度量更宽的
         机器 —— 名字截个尾巴, 总好过门头上两样东西叠在一起 */
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
    }

    /* 侧栏窄到放不下语种名 —— 只留地球, 退回 30×30 纯图标键; 名称仍在 data-tip 与
       aria-label 里, 没有丢线索。判据由 JS 给(applyNavW 量的是真宽度, 见 NAV_W_LANG):
       同一个窗口下面板宽度可拖 220~560, 视口查不出"现在多宽"这一条 */
    .lang-tight .lang-cur {
      display: none;
    }

    .lang-tight .lang-btn {
      padding: 0;
      gap: 0;
    }

    .lang-btn svg {
      width: 17px;
      height: 17px;
      flex-shrink: 0;
      /* 名字被截时地球不跟着缩 */
    }

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

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

    /* 下拉: 往下开(挂在门头行, 上面就是窗口边), 从按钮左缘往右铺。
       宽 168 比按钮宽得多, 溢出的那截落在对话区上 —— 浮层的正常读法,
       有白底和阴影兜着(同 .acct-pop 的取法)。 */
    .lang-pop {
      position: absolute;
      top: calc(100% + 6px);
      left: 0;
      z-index: 40;
      width: 168px;
      /* 11 个语种自然高约 336px, 不设死上限(设小了会平白多出一条滚动条);
         这里的上限只防窗口太矮时整块出画 —— 门头在页面顶端, 面板下方
         到窗口底还剩多少就吃多少 */
      max-height: calc(100vh - 72px);
      overflow-y: auto;
      display: flex;
      flex-direction: column;
      padding: 5px;
      background: var(--card);
      border: 1px solid var(--line);
      border-radius: 10px;
      box-shadow: 0 10px 28px rgba(3, 30, 95, .14);
      scrollbar-width: thin;
      scrollbar-color: var(--line-strong) transparent;
    }

    /* 必须显式写: 上面的 display:flex 会盖掉 [hidden] 自带的 display:none */
    .lang-pop[hidden] {
      display: none;
    }

    .lang-item {
      width: 100%;
      display: flex;
      align-items: center;
      gap: 7px;
      padding: 7px 9px;
      border: 0;
      border-radius: 7px;
      background: transparent;
      color: var(--ink-2);
      /* 语种名一律用它自己的写法(日本語 / Русский / Français), 不随界面语言翻译 ——
         用户找的是"我认得的那行字", 翻成当前界面语言反而认不出 */
      font-family: var(--font);
      font-size: 13px;
      text-align: left;
      cursor: pointer;
    }

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

    /* 勾位常驻占宽(没选中的是空串, 不是不渲染): 否则选中那行被勾一挤,
       整列文字会随选择左右跳动 */
    .lang-tick {
      width: 11px;
      flex-shrink: 0;
      color: var(--brand);
      font-size: 12px;
    }

    /* ── 账号: 左栏底部那颗圆形头像 ──
       两层各管各的: 头像这一下只开下面那张小面板(身份 + AI点余额 + 各入口); 面板里的
       条目点下去, 内容才在「账号与设置」弹窗(#acct, 见本文件后面那节)里打开。 */
    .acct {
      position: relative;
      flex-shrink: 0;
    }

    .acct-pop {
      position: absolute;
      /* 头像挂在左栏底下(见 .side-foot), 所以这张面板往上开(下面是窗口边)、左对齐往右铺
         (左边就是窗口边, 右对齐会整块出画)。宽 266 比栏宽(最小 220)大, 溢出的那截落在
         对话区上 —— 浮层的正常读法, 有白底和阴影兜着。 */
      bottom: calc(100% + 8px);
      left: 0;
      z-index: 40;
      width: 266px;
      display: flex;
      flex-direction: column;
      /* 分区之间用 .acct-sep 隔, 不再靠 gap 拉开 —— 每块自己管自己的内边距 */
      padding: 6px;
      background: var(--card);
      border: 1px solid var(--line);
      border-radius: 12px;
      box-shadow: 0 10px 28px rgba(3, 30, 95, .14);
    }

    /* 必须显式写: 上面的 display:flex 会盖掉 [hidden] 自带的 display:none */
    .acct-pop[hidden] {
      display: none;
    }

    /* 身份块: 头像 + 账号名称(点一下复制) + 会员徽章 */
    .acct-id {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 8px 8px 9px;
    }

    .acct-btn {
      width: 34px;
      height: 34px;
      flex-shrink: 0;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      padding: 0;
      border: 1px solid var(--line);
      border-radius: 50%;
      background: var(--card);
      color: var(--navy);
      cursor: pointer;
      box-shadow: var(--shadow-sm);
      transition: border-color .15s ease, background .15s ease, color .15s ease;
    }

    .acct-btn svg {
      width: 17px;
      height: 17px;
    }

    .acct-btn:hover,
    .acct-btn.on,
    .acct-btn:active {
      border-color: var(--brand);
      background: var(--brand-soft);
      color: var(--brand-deep);
    }

    /* 头像那颗圆: 小面板里放图标, 弹窗里放名字首字(见 renderAcct) —— 同一颗, 只换内容 */
    .acct-ava {
      width: 36px;
      height: 36px;
      flex-shrink: 0;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      border-radius: 50%;
      background: var(--brand-soft);
      color: var(--brand-deep);
    }

    .acct-ava svg {
      width: 19px;
      height: 19px;
    }

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

    /* 账号名称: 按钮洗成纯文本, 只在悬停时浮出复制图标 —— 平时别吵, 但得让人
       知道这里能点(见 JS 的 acct 段)。 */
    .acct-name {
      display: inline-flex;
      align-items: center;
      gap: 5px;
      align-self: flex-start;
      max-width: 100%;
      padding: 0;
      border: none;
      background: none;
      font: inherit;
      font-size: 13.5px;
      font-weight: 700;
      color: var(--navy);
      cursor: pointer;
      transition: color .15s ease;
    }

    .acct-name #acctNameText {
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .acct-copy {
      width: 13px;
      height: 13px;
      flex-shrink: 0;
      color: var(--faint);
      opacity: 0;
      transition: opacity .15s ease, color .15s ease;
    }

    .acct-name:hover {
      color: var(--brand-deep);
    }

    .acct-name:hover .acct-copy {
      opacity: 1;
      color: var(--brand);
    }

    /* 复制成功/失败时把图标常亮, 配合"已复制"那几个字 */
    .acct-name.copied .acct-copy {
      opacity: 1;
      color: var(--green-deep);
    }

    /* 身份第二行: 登录方式。字号比名字小, 压灰 —— 它是注解不是身份本身 */
    .acct-login {
      font-size: 11px;
      color: var(--faint);
      letter-spacing: .2px;
    }

    /* 会员徽章: 非会员时删掉这个节点即可, 不占位也就不会留空 */
    .acct-vip {
      flex-shrink: 0;
      padding: 2px 7px;
      border-radius: 999px;
      background: var(--amber-soft);
      border: 1px solid var(--amber-border);
      color: var(--amber-deep);
      font-size: 10.5px;
      font-weight: 700;
      letter-spacing: .3px;
    }

    .acct-sep {
      height: 1px;
      margin: 4px 6px;
      background: var(--line-soft);
    }

    /* AI点余额。整行是个入口(见 index.html 那段): 鼠标搭上来给个落点反馈, 与下面那六条一致。
       刷新那颗自己占着右端、有自己的悬停反馈(见 .acct-refresh:hover), 压在它上面时这一行
       不跟着亮 —— 那一小块点下去并不换页(见 JS 的 acctBal)。 */
    .acct-bal {
      display: flex;
      align-items: baseline;
      justify-content: space-between;
      gap: 8px;
      padding: 7px 8px;
      border-radius: 8px;
      cursor: pointer;
      transition: background .15s ease;
    }

    .acct-bal:hover:not(:has(.acct-refresh:hover)) {
      background: var(--brand-soft);
    }

    .acct-bal-k {
      font-size: 12px;
      color: var(--muted);
    }

    /* 数值 + 刷新同一行: 刷新那颗比数字高(.acct-refresh 是 24px 的方块), 这里改居中对齐,
       否则那颗会把数字顶下去、整行高度跟着跳 */
    .acct-bal-v {
      display: inline-flex;
      align-items: center;
      gap: 4px;
      font-size: 12px;
      color: var(--ink-2);
    }

    .acct-bal-v b {
      font-size: 15px;
      font-weight: 700;
      color: var(--navy);
      letter-spacing: .3px;
    }

    /* 外观(明暗)切换: 一行标签 + 一颗"分段器"(segmented control)—— 浅色/深色并进同一条
       凹陷轨道, 选中的那格是"抬起的小块"(卡底 + 投影), 没选的只留文字。这是 Linear /
       Vercel / iOS 设置里同一种分段控件语言: 一个控件, 而不是两颗各自为政、边框还撞色的按钮。
       轨道取页面底 --bg-tint(凹陷)、小块取 --card(抬起): 浅色 = 白块浮在浅蓝底上, 深色 =
       亮一档的暗块浮在近黑的底上 —— 深浅方向随 token 自动反, 不用另写覆盖。 */
    .acct-theme {
      padding: 6px 8px;
    }

    .acct-theme-k {
      display: block;
      font-size: 11px;
      color: var(--muted);
      margin-bottom: 6px;
    }

    .acct-theme-opts {
      display: flex;
      gap: 2px;
      padding: 2px;
      background: var(--bg-tint);
      border: 1px solid var(--line);
      border-radius: 9px;
    }

    .acct-theme-o {
      flex: 1;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 5px;
      height: 28px;
      padding: 0 6px;
      border: 0;
      border-radius: 7px;
      background: transparent;
      font-family: var(--font);
      font-size: 12px;
      font-weight: 600;
      color: var(--muted);
      cursor: pointer;
      white-space: nowrap;
      transition: background .18s ease, color .18s ease, box-shadow .18s ease;
    }

    .acct-theme-ic {
      width: 13px;
      height: 13px;
      flex: none;
    }

    .acct-theme-o:hover {
      color: var(--ink-2);
    }

    .acct-theme-o.on {
      background: var(--card);
      color: var(--ink);
      box-shadow: var(--shadow-sm);
    }

    /* 六条入口: 上面五条 + 隔一道垫底的退出系统 */
    .acct-menu {
      display: flex;
      flex-direction: column;
    }

    /* 面板里这六条的行样式。其中两条是 <a>(跳主站, 见 index.html), 所以 text-decoration
       得显式关掉 —— 全站没有 a 的全局重置, 底下那条线会跟着进来。 */
    .acct-item {
      display: flex;
      align-items: center;
      gap: 8px;
      width: 100%;
      padding: 8px;
      border: none;
      border-radius: 8px;
      background: none;
      font: inherit;
      font-size: 13px;
      color: var(--ink-2);
      text-align: left;
      text-decoration: none;
      cursor: pointer;
      transition: background .15s ease, color .15s ease;
    }

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

    .acct-chev {
      width: 13px;
      height: 13px;
      margin-left: auto;
      flex-shrink: 0;
      color: var(--faint);
    }

    .acct-item:hover .acct-chev {
      color: var(--brand);
    }

    /* 退出系统: 位置(隔一道 + 垫底)已经够区分了, 颜色平时就跟同伴一样素,
       只在下手那一刻转红 —— 常驻红字会把整张面板的气氛带成警告色, 不值当。
       放在 .acct-item:hover 之后: 两条选择器同权重, 靠顺序压它一头。 */
    .acct-item--out:hover {
      background: var(--red-soft);
      color: var(--red);
    }

    .acct-item--out:hover .acct-chev {
      color: var(--red);
    }

    /* 开发用换身份: 原有功能, 降级收在头像面板(身份块 / 余额 / 六条入口)的最底下 */
    .acct-dev {
      display: flex;
      align-items: center;
      gap: 6px;
      padding: 6px 8px 4px;
    }

    /* spssau 模式整块隐藏: flex 会盖掉原生 [hidden] 的 display:none */
    .acct-dev[hidden],
    #acctDevSep[hidden] {
      display: none !important;
    }

    .acct-dev label {
      flex-shrink: 0;
      font-size: 10.5px;
      color: var(--faint);
      cursor: default;
    }

    .acct-dev input {
      flex: 1;
      min-width: 0;
      padding: 4px 7px;
      border: 1px solid var(--line);
      border-radius: 6px;
      outline: none;
      font: inherit;
      font-size: 11.5px;
      font-weight: 600;
      color: var(--navy);
      letter-spacing: .3px;
      transition: border-color .15s ease, box-shadow .15s ease;
    }

    .acct-dev input:focus {
      border-color: var(--brand);
      box-shadow: var(--ring);
    }

    /* 登录 Cookie: 长串且要逐段核对, 用小号等宽字并单行省略, 不撑破面板 */
    .acct-dev--cookie input {
      font-size: 10.5px;
      font-weight: 500;
      letter-spacing: 0;
      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
      text-overflow: ellipsis;
    }

    /* ── 账号与设置弹窗(#acct): 左分区导航 + 右内容 ──
       外壳用的是通用的 .mdl(见 data-panel.css): ✕ / 点遮罩 / Esc 三条退出路径由它给,
       这里只写两栏布局与各块内容。选择器一律带 .mdl-acct 前缀 —— chat.css 在
       data-panel.css **之前**加载, 光靠 .mdl-panel 这种同权重的写法压不住它的宽度。 */
    .mdl-acct .mdl-panel {
      width: min(680px, 100%);
    }

    /* 正文改两栏: 左导航定宽不滚, 右内容自己滚 —— 弹窗高度不跟着页面走。
       高度是钉死的(568px), 不是"按内容撑、至少 360": 六页内容长短差得远, 不锁死的话换一次
       分区弹窗就跳一次高度, 视线跟着飘。568 是「购买AI点」那一页自然撑出来的高度 —— 最长的一页
       定调, 别的页就都跟它齐平; 装不下的页自己滚(右栏 .set-panes 是 overflow:auto)。
       (这个数是跟着那一页长出来的: 它原先是 525, 加了「仅购买 AI 点」那一行之后自然要 561 ——
       一行按钮 29 + 一次 .set-pane 的间距 14, 留几个像素余量取 568。改那一页的排面就要回来
       核这个数, 别让它变成"某一页自己滚" —— 那正是它要避免的。)
       几个写法都是有讲究的:
         flex-basis 用 auto 而不是 .mdl-body 那个 0% —— basis:0 时高度看的是内容, height 白写;
         min-height 归零 —— 矮屏时让 .mdl-panel 的 max-height(100%) 收得动, 不然会顶出屏幕
         (.mdl-panel 是 overflow:hidden, 顶出去就是切掉); 70vh 是给矮屏留的余量。 */
    .mdl-acct .acct-body {
      flex: 1 1 auto;
      flex-direction: row;
      gap: 0;
      padding: 0;
      overflow: hidden;
      height: min(568px, 70vh);
      min-height: 0;
    }

    .set-nav {
      flex-shrink: 0;
      width: 152px;
      display: flex;
      flex-direction: column;
      gap: 2px;
      padding: 12px 10px;
      border-right: 1px solid var(--line-soft);
      background: var(--bg-tint);
      overflow: auto;
    }

    /* 前三条是换页按钮, 后两条(订单与发票 / 人工反馈)是跳主站的 <a> —— 同一个壳, 所以
       text-decoration 得显式关掉(全站没有 a 的全局重置); 它们不挂 data-nav, 因此永远
       拿不到下面那条 aria-current 的白底。 */
    .set-tab {
      display: flex;
      align-items: center;
      gap: 8px;
      width: 100%;
      padding: 8px 10px;
      border: none;
      border-radius: 8px;
      background: none;
      font: inherit;
      font-size: 13px;
      color: var(--ink-2);
      text-align: left;
      text-decoration: none;
      cursor: pointer;
      transition: background .15s ease, color .15s ease;
    }

    .set-tab svg {
      width: 15px;
      height: 15px;
      flex-shrink: 0;
      color: var(--muted);
      transition: color .15s ease;
    }

    .set-tab:hover {
      background: var(--card);
      color: var(--brand-deep);
    }

    /* 当前分区: 左栏底色是浅蓝, 选中的那块铺白 —— 比描边更像"你就停在这栏" */
    .set-tab[aria-current="true"] {
      background: var(--card);
      color: var(--navy);
      font-weight: 700;
      box-shadow: var(--shadow-sm);
    }

    .set-tab[aria-current="true"] svg {
      color: var(--brand);
    }

    /* 退出系统: 不属上面那五个页签, 没有图标, 单拎出来排 */
    .set-tab--plain {
      padding-left: 10px;
    }

    /* 退出系统: 平时与同伴一样素, 只在下手那一刻转红 —— 常驻红字会把整栏带成警告色 */
    .set-tab--out:hover {
      background: var(--red-soft);
      color: var(--red);
    }

    .set-nav-sep {
      height: 1px;
      margin: 6px 8px;
      background: var(--line);
    }

    .set-panes {
      flex: 1;
      min-width: 0;
      overflow: auto;
      display: flex;
      flex-direction: column;
      gap: 14px;
      padding: 18px 20px;
    }

    /* flex 那三项: 撑满右栏剩下的高度 —— 内容短的页(账号信息/订单)也是这样, 于是页内那些
       该占满的东西(记录表 / 二维码)才有富余可拿。
       收缩那一位不能是 0: 记录表这页的 basis 是"按内容算"的(表多长它就多长), 不许缩就成了
       "越长越把右栏撑破"。缩回右栏高之后, 表那块(flex:1)才在自己的高度里滚起来 */
    .set-pane {
      flex: 1 1 auto;
      min-height: 0;
      /* 不归零的话列向 flex 的 min-height:auto 会把"按内容算"顶回来, 缩不动 */
      display: flex;
      flex-direction: column;
      gap: 14px;
    }

    /* 必须显式写: 上面的 display:flex 会盖掉 [hidden] 自带的 display:none */
    .set-pane[hidden] {
      display: none;
    }

    .set-head {
      display: flex;
      flex-direction: column;
      gap: 3px;
    }

    /* 页名那一行: 页名 / 紧跟着它的附属信息(余额) / 顶到最右的说明链接(计费规则) ——
       三样一行, 按基线对齐。字大小不一, 基线对齐比居中稳当 */
    .set-head-row {
      display: flex;
      align-items: baseline;
      gap: 10px;
    }

    /* 说明链接: 页名那一行右边那颗(「AI点数计费规则」)。小字 + 品牌蓝, 不带下划线 ——
       与正文里那个《会员服务协议》的区别就在这; 新页打开, 别把这一页顶掉。
       不写 align-self —— 跟页名、余额一样吃 .set-head-row 的 baseline: 一行里三样都落在
       同一条基线上, 才叫"排在一行"。原先给它单独拎了 flex-start(想当右上角那颗角标),
       结果是**它自己**飘在那一行的顶边 —— 量出来比页名与余额高 5px(393 下 291.5 vs 296.5),
       右边那颗小字看着跟左边的字不在一条线上。字大小不一不是理由: 同一行的小字(余额)
       本来就是跟着基线走的, 分两套对齐反而露馅 */
    .set-link {
      margin-left: auto;
      flex-shrink: 0;
      font-size: 12.5px;
      color: var(--brand);
      text-decoration: none;
      white-space: nowrap;
    }

    .set-h {
      font-size: 17px;
      font-weight: 700;
      color: var(--navy);
    }

    .set-sub {
      font-size: 12px;
      color: var(--muted);
    }

    /* 信息块: 一行一件事, 行与行之间一道细线 */
    .set-card {
      background: var(--card);
      border: 1px solid var(--line);
      border-radius: 12px;
    }

    .set-row {
      display: flex;
      align-items: center;
      gap: 10px;
      min-height: 46px;
      padding: 8px 14px;
    }

    .set-row+.set-row {
      border-top: 1px solid var(--line-soft);
    }

    .set-k {
      font-size: 12.5px;
      color: var(--muted);
    }

    /* 弹窗里的账号名称: 纯文本(小面板那颗才接复制, 见 #acctPop 的 acctName),
       所以这里就是那行字的样式本身, 不带按钮那身 */
    .set-name {
      font-size: 13.5px;
      font-weight: 700;
      color: var(--navy);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    /* 值一律靠右: 左边是"这是什么", 右边是"它是什么" */
    .set-v {
      margin-left: auto;
      font-size: 13px;
      color: var(--ink-2);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .set-v b {
      font-size: 15px;
      font-weight: 700;
      color: var(--navy);
      letter-spacing: .3px;
    }

    /* 占位说明(接口没接、内容还没得可列): 小字压灰, 别抢标题的视线 */
    .set-note {
      font-size: 12px;
      line-height: 1.75;
      color: var(--faint);
    }

    /* 行内那颗小按钮(「账号信息」里余额旁边的「购买」): 浅色小按钮 —— 它是条近路,
       主操作在左栏那条上, 所以不套 .mdl-btn 那身蓝 */
    .set-btn {
      flex-shrink: 0;
      padding: 5px 14px;
      border: 1px solid var(--line);
      border-radius: 8px;
      background: var(--card);
      font-family: var(--font);
      font-size: 12.5px;
      font-weight: 600;
      color: var(--ink-2);
      cursor: pointer;
      transition: color .15s ease, border-color .15s ease, background .15s ease;
    }

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

    /* AI点余额旁边那颗刷新 */
    .acct-refresh {
      flex-shrink: 0;
      width: 24px;
      height: 24px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      padding: 0;
      border: none;
      border-radius: 7px;
      background: none;
      color: var(--muted);
      cursor: pointer;
      transition: background .15s ease, color .15s ease;
    }

    .acct-refresh:hover {
      background: var(--brand-soft);
      color: var(--brand-deep);
    }

    .acct-refresh svg {
      width: 14px;
      height: 14px;
    }

    /* 拉取期间转起来: 转的是图标本身, 按钮尺寸不变, 行高不跳 */
    .acct-refresh.spin svg {
      animation: acct-spin .7s linear infinite;
    }

    @keyframes acct-spin {
      to {
        transform: rotate(360deg);
      }
    }

    /* ── AI点使用记录(设置弹窗里的一页): 类别 / 使用时间 / AI点数 三列 ──
       表在自己那块里滚, 不是整页跟着滚: 记录只会越攒越长, 表头得一直钉在最上面看着
       (同 .peek-table 那套: sticky 表头 + 数字列右对齐)。这块高度不写死, 见下面的 flex。 */
    .pts-wrap {
      flex: 1;
      /* 右栏剩下多少就给表多少: 记录越攒越长, 这块本来就该是占满的那个 */
      min-height: 0;
      overflow: auto;
      border: 1px solid var(--line);
      border-radius: 12px;
      background: var(--card);
    }

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

    .pts-table th,
    .pts-table td {
      padding: 9px 14px;
      text-align: left;
      white-space: nowrap;
    }

    /* 表头钉在滚动区顶上: 底色得是不透明的, 否则下头的行会从字缝里透出来 */
    .pts-table thead th {
      position: sticky;
      top: 0;
      z-index: 1;
      border-bottom: 1px solid var(--line);
      background: var(--bg-tint);
      color: var(--navy);
      font-weight: 700;
    }

    /* 行与行之间一道细线, 首行不画(上面表头那道已经把它分开了) */
    .pts-table tbody tr+tr td {
      border-top: 1px solid var(--line-soft);
    }

    /* 带 .pts-table 前缀: 光写 .pts-num 压不住上面那条 th/td 的 text-align */
    .pts-table .pts-num {
      text-align: right;
      font-variant-numeric: tabular-nums;
    }

    /* 时间那列是一串数字, 压低一号 —— 让"类别"和点数先被看见 */
    .pts-at {
      color: var(--muted);
    }

    /* 一条记录都没有时占的那一行 */
    .pts-empty {
      padding: 26px 14px;
      text-align: center;
      color: var(--faint);
    }

    /* 翻页条: 贴在记录表下面。表那块是 flex:1 占满, 这条按内容沉在最底 */
    .pts-foot {
      flex: 0 0 auto;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 12px;
    }

    /* 必须显式写: 上面的 display:flex 会盖掉 [hidden] 自带的 display:none */
    .pts-foot[hidden] {
      display: none;
    }

    /* 上一页 / 下一页: 样式对齐预览弹窗的翻页键(见 data-panel.css 的 .peek-page-btn) */
    .pts-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;
    }

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

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

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

    /* 口径说明: 记录表下面那行小字(只回溯最近 1000 条) */
    .pts-note {
      flex: 0 0 auto;
      margin: 0;
      font-size: 12px;
      line-height: 1.5;
      color: var(--faint);
      text-align: center;
    }

    /* ── 订单与发票 / 人工反馈(设置弹窗里新增的两页) ──
       .set-btn 那身是"浅色近路"按钮; 这两页有真主操作(开票/提交), 给它加两个变体:
       蓝主按钮与红危险按钮, 与全站其他 modal 按钮同源。 */
    .set-btn--primary {
      background: var(--brand);
      border-color: var(--brand);
      color: #fff;
    }
    .set-btn--primary:hover {
      background: var(--brand-deep);
      border-color: var(--brand-deep);
      color: #fff;
    }
    .set-btn--primary:disabled {
      background: var(--line);
      border-color: var(--line);
      color: var(--faint);
      cursor: default;
    }
    .set-btn--danger {
      color: var(--red);
    }
    .set-btn--danger:hover {
      background: var(--red-soft);
      border-color: var(--red);
      color: var(--red);
    }

    /* 订单/发票: 用卡片列表, 不列表格 —— 9 列必然横向滚动, 卡片纵向堆叠才不挤。
       每单一行卡: 勾选 | 金额+状态 | 订单号+时间 | 报销/合同操作; 发票记录卡只读。 */
    .ord-sec {
      display: flex;
      flex-direction: column;
      gap: 8px;
    }
    .ord-sec-head {
      display: flex;
      align-items: center;
      gap: 10px;
    }
    .ord-h {
      margin: 0;
      font-size: 13.5px;
      font-weight: 700;
      color: var(--navy);
    }
    /* 开票选择提示: 顶到右边, 未选时是灰字引导, 选好后变成"已选 n 单 · 合计" */
    .ord-sel {
      margin-left: auto;
      flex-shrink: 0;
      font-size: 12px;
      color: var(--muted);
      font-variant-numeric: tabular-nums;
    }
    .ord-sel.has {
      color: var(--brand-deep);
      font-weight: 600;
    }
    /* 表头「全选」: 与单行勾选框同一套观感, 部分选中时走 indeterminate */
    .ord-checkall {
      flex-shrink: 0;
      display: inline-flex;
      align-items: center;
      gap: 5px;
      font-size: 12px;
      color: var(--muted);
      cursor: pointer;
      white-space: nowrap;
    }
    .ord-checkall input {
      width: 15px;
      height: 15px;
      margin: 0;
      accent-color: var(--brand);
      cursor: pointer;
    }

    .ord-list {
      display: flex;
      flex-direction: column;
      gap: 8px;
    }
    .ord-card {
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      gap: 12px;
      padding: 12px 14px;
      border: 1px solid var(--line);
      border-radius: 12px;
      background: var(--card);
    }
    .ord-pick {
      flex-shrink: 0;
      display: inline-flex;
      align-items: center;
    }
    .ord-pick input {
      width: 16px;
      height: 16px;
      margin: 0;
      accent-color: var(--brand);
      cursor: pointer;
    }
    .ord-pick input:disabled { cursor: not-allowed; }

    .ord-main {
      flex: 1;
      min-width: 0;
      display: flex;
      flex-direction: column;
      gap: 4px;
    }
    .ord-line {
      display: flex;
      align-items: center;
      gap: 8px;
    }
    .ord-amount {
      font-size: 14px;
      font-weight: 700;
      color: var(--navy);
      font-variant-numeric: tabular-nums;
    }
    .ord-state {
      flex-shrink: 0;
      padding: 1px 7px;
      border-radius: 999px;
      font-size: 11px;
      font-weight: 600;
      line-height: 1.7;
    }
    .ord-state--warn { background: var(--red-soft); color: var(--red); }
    .ord-state--doing { background: var(--brand-soft); color: var(--brand-deep); }
    .ord-state--done { background: var(--green-soft); color: var(--green-deep); }
    .ord-state--off { background: var(--line); color: var(--muted); }

    .ord-meta {
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      font-size: 12px;
      color: var(--muted);
    }
    .ord-meta > span + span::before {
      content: "·";
      margin-right: 6px;
      color: var(--faint);
    }
    .ord-title {
      min-width: 0;
      font-size: 13px;
      font-weight: 600;
      color: var(--navy);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .ord-mono {
      min-width: 0;
      max-width: 240px;
      font-variant-numeric: tabular-nums;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .ord-acts {
      flex-shrink: 0;
      display: flex;
      gap: 6px;
    }
    .ord-act {
      padding: 4px 10px;
      border: 1px solid var(--line);
      border-radius: 7px;
      background: var(--card);
      font-family: var(--font);
      font-size: 12px;
      font-weight: 600;
      color: var(--ink-2);
      cursor: pointer;
      white-space: nowrap;
      transition: color .15s ease, border-color .15s ease, background .15s ease;
    }
    .ord-act:hover {
      color: var(--brand);
      border-color: var(--brand);
      background: var(--brand-soft);
    }
    .ord-act:disabled {
      color: var(--faint);
      border-color: var(--line);
      background: var(--line);
      cursor: not-allowed;
    }

    .ord-empty {
      padding: 22px 14px;
      text-align: center;
      color: var(--faint);
      font-size: 12.5px;
    }
    .ord-empty[hidden] { display: none; }
    .ord-empty--box {
      border: 1px dashed var(--line);
      border-radius: 12px;
    }

    /* 开票 / 合同表单: 内联在页里的一张浅色卡 */
    .ord-form {
      display: flex;
      flex-direction: column;
      gap: 10px;
      padding: 12px 14px;
      border: 1px solid var(--line);
      border-radius: 12px;
      background: var(--card);
    }
    .ord-form[hidden] { display: none; }
    .ord-form-head { display: flex; align-items: baseline; gap: 10px; }
    .ord-form-total { margin-left: auto; font-size: 12.5px; color: var(--muted); }
    .ord-form-total b { font-size: 14px; color: var(--navy); }
    .ord-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
    .ord-field { display: flex; flex-direction: column; gap: 5px; font-size: 12px; color: var(--muted); }
    .ord-field input,
    .ord-field select,
    .cst-ask textarea,
    .cst-reply textarea {
      padding: 7px 9px;
      border: 1px solid var(--line);
      border-radius: 6px;
      background: transparent;
      font: inherit;
      font-size: 12.5px;
      color: var(--navy);
      outline: none;
      transition: border-color .15s ease, box-shadow .15s ease;
    }
    .ord-field input:focus,
    .ord-field select:focus,
    .cst-ask textarea:focus,
    .cst-reply textarea:focus {
      border-color: var(--brand);
      box-shadow: var(--ring);
    }
    .ord-field textarea { resize: vertical; }
    .ord-form-actions { display: flex; justify-content: flex-end; gap: 8px; }

    /* 头像小面板里的未读红点(与 chevron 并排, 一左一右夹着文字) */
    .acct-dot {
      flex-shrink: 0;
      width: 7px;
      height: 7px;
      border-radius: 50%;
      background: var(--red);
      margin-right: 6px;
    }
    .acct-dot[hidden] { display: none; }
    /* 左栏 tab 里的未读红点: 靠右, 顶到图标/文字那一行的尾 */
    .set-dot {
      flex-shrink: 0;
      width: 7px;
      height: 7px;
      border-radius: 50%;
      background: var(--red);
      margin-left: auto;
    }
    .set-dot[hidden] { display: none; }

    /* 人工反馈: 提问卡 / 问题列表 / 单问题气泡线程(我方蓝气泡靠右, 客服白气泡靠左) */
    .cst-ask {
      display: flex;
      flex-direction: column;
      gap: 10px;
      padding: 12px 14px;
      border: 1px solid var(--line);
      border-radius: 12px;
      background: var(--card);
    }
    .cst-ask[hidden] { display: none; }
    .cst-ask textarea { width: 100%; resize: vertical; }
    .cst-ask-row { display: flex; align-items: center; gap: 8px; }
    .cst-counter { margin-left: auto; flex-shrink: 0; font-size: 12px; color: var(--faint); font-variant-numeric: tabular-nums; }
    .cst-counter b { color: var(--muted); font-weight: 600; }

    .cst-list { display: flex; flex-direction: column; gap: 8px; }
    .cst-list[hidden] { display: none; }
    .cst-item {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 11px 14px;
      border: 1px solid var(--line);
      border-radius: 12px;
      background: var(--card);
      font: inherit;
      text-align: left;
      cursor: pointer;
      transition: border-color .15s ease, background .15s ease;
    }
    .cst-item:hover { border-color: var(--brand); background: var(--brand-soft); }
    .cst-item-main {
      flex: 1;
      min-width: 0;
      display: flex;
      flex-direction: column;
      gap: 5px;
    }
    .cst-item-text {
      font-size: 13px;
      color: var(--navy);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .cst-item--new .cst-item-text { font-weight: 700; }
    .cst-item-meta { display: flex; align-items: center; gap: 8px; }
    .cst-item-time { font-size: 12px; color: var(--muted); }
    .cst-badge {
      flex-shrink: 0;
      padding: 1px 7px;
      border-radius: 999px;
      background: var(--brand-soft);
      color: var(--brand-deep);
      font-size: 11px;
      font-weight: 600;
    }
    .cst-badge--closed { background: var(--line); color: var(--muted); }
    .cst-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: 50%; background: var(--red); }
    .cst-chev { flex-shrink: 0; font-size: 16px; line-height: 1; color: var(--faint); }

    .cst-detail { display: flex; flex-direction: column; gap: 14px; }
    .cst-detail[hidden] { display: none; }
    .cst-detail-head { display: flex; align-items: center; gap: 10px; }
    .cst-head-acts { margin-left: auto; display: flex; align-items: center; gap: 12px; }
    .cst-link {
      padding: 0;
      border: none;
      background: none;
      font: inherit;
      font-size: 12px;
      font-weight: 600;
      color: var(--muted);
      cursor: pointer;
      transition: color .15s ease;
    }
    .cst-link:hover { color: var(--red); }
    .cst-link--danger { color: var(--red); }

    .cst-thread { display: flex; flex-direction: column; gap: 12px; }
    .cst-msg { max-width: 86%; display: flex; flex-direction: column; }
    .cst-msg--you { align-self: flex-end; align-items: flex-end; }
    .cst-msg--cs { align-self: flex-start; align-items: flex-start; }
    .cst-msg-who { margin: 0 0 4px; font-size: 11.5px; color: var(--muted); }
    .cst-msg-body {
      padding: 9px 12px;
      border-radius: 12px;
      font-size: 13px;
      line-height: 1.7;
      overflow-wrap: anywhere;
    }
    .cst-msg--you .cst-msg-body { background: var(--brand); color: #fff; border-bottom-right-radius: 4px; }
    .cst-msg--you .cst-msg-body a { color: #fff; text-decoration: underline; }
    .cst-msg--cs .cst-msg-body { background: var(--card); border: 1px solid var(--line); color: var(--ink-2); border-bottom-left-radius: 4px; }
    .cst-msg-body img { display: block; max-width: 100%; border-radius: 6px; margin: 4px 0; }
    .cst-msg-body a { color: var(--brand); }

    .cst-reply { display: flex; flex-direction: column; gap: 8px; }
    .cst-reply textarea { width: 100%; resize: vertical; }
    .cst-reply-row { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
    #cstReplyImgBtn { margin-right: auto; }

    /* 提问/追问里已选图片的缩略图条 */
    .cst-thumbs { display: flex; flex-wrap: wrap; gap: 8px; }
    .cst-thumbs[hidden] { display: none; }
    .cst-thumb {
      position: relative;
      width: 64px;
      height: 64px;
      border: 1px solid var(--line);
      border-radius: 8px;
      overflow: hidden;
      background: var(--bg-tint);
    }
    .cst-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
    .cst-thumb-x {
      position: absolute;
      top: 2px;
      right: 2px;
      width: 18px;
      height: 18px;
      line-height: 16px;
      padding: 0;
      border: none;
      border-radius: 50%;
      background: rgba(0, 0, 0, .55);
      color: #fff;
      font-size: 13px;
      cursor: pointer;
    }
    .cst-thumb-x:hover { background: rgba(0, 0, 0, .8); }
    .cst-feedback {
      display: flex;
      align-items: center;
      gap: 8px;
      padding-top: 12px;
      border-top: 1px solid var(--line-soft);
    }
    .cst-fb-q { font-size: 12.5px; color: var(--muted); margin-right: 2px; }

    /* ── 购买AI点(设置弹窗里的一页) ──
       从上到下的顺序就是下单的顺序: 套餐 → 数量 → 支付方式 → 二维码 → 协议。
       份数经 count 传给后端(1 份 = 30 点), 码是主站 payUrl 画出来的。 */

    /* 余额: 跟着页名走(那一行的布局见 .set-head-row)—— 它原先是块单独的信息卡, 一行就为了
       说个数, 把"买"这件事整个往下压了一截 */
    .buy-bal {
      font-size: 12.5px;
      color: var(--muted);
    }

    .buy-bal b {
      margin: 0 2px;
      font-size: 14px;
      font-weight: 700;
      color: var(--navy);
    }

    /* 仅购买 AI 点(只在"现在还在会员期内"时才有这一行, 见 index.html 那段): 左边一句说明、
       右边一颗近路。说明那半句不能省 —— 单摆一颗「仅购买 AI 点」夹在四张会员卡上面, 会被
       读成"第五张卡"。
       这一行是**纯新增的高度**, 而这一页的高度是钉死的(那条 525 的账见 .mdl-acct .acct-body):
       它一动, 那笔账就得跟着重算 —— 桌面那条改了上限, 手机那条另收了间距, 两处都写在各自
       的注释里。
       flex-wrap 是给极窄处的兜底: 320 上那两半挤不到一行, 宁可折成两行(那一档本来就滚 ——
       "宁可出滚动条, 不压扁", 见 .buy-qr 那条批注), 也别顶出面板右缘。折了之后这一行是
       54 高而不是 29, 320 的滚动量因此又多 25 —— 那一档的账早就是明着认亏的。 */
    .buy-go-pts {
      display: flex;
      align-items: center;
      justify-content: space-between;
      flex-wrap: wrap;
      gap: 8px;
    }

    /* 必须显式写(同 .set-pane 那条): 上面那句 display:flex 会盖掉 [hidden] 自带的 none ——
       这一行是 JS 按身份开合的(见 renderAcct), 漏了它非会员也会看见 */
    .buy-go-pts[hidden] {
      display: none;
    }

    .buy-go-pts-k {
      font-size: 12.5px;
      color: var(--ink-2);
    }

    /* 会员四档与数量: 上下两行 —— 上面那行是"买哪一档"(四张卡并排, 就是一道选择题),
       下面那行是"买几份"。原先只有一档时卡片与数量并排(一问一答), 四档并排后卡片自己
       已成一行, 数量再挤在旁边会把卡片压到读不清 */
    .buy-line {
      display: flex;
      flex-direction: column;
      gap: 8px;
    }

    /* 四档: 一行四张。桌面与窄屏都是四列 —— 卡片里就三行短字(档位 / 价格 / 到账点数),
       393 的手机上每张分到 74px 也装得下; 改成两行两列反倒让这一页高出一整行,
       协议勾选框要掉到折线以下(那笔老账见 ≤560 那一段) */
    .buy-plans {
      display: grid;
      grid-template-columns: repeat(4, minmax(0, 1fr));
      gap: 8px;
    }

    /* 卡片: 竖排三行。四档之后卡片不再"天然选中", 所以这里给 cursor:pointer ——
       点得动的东西才配得上那只手(点击那套见 JS 的 paintTiers) */
    .buy-plan {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      gap: 1px;
      padding: 8px 10px;
      border: 1px solid var(--line);
      border-radius: 10px;
      background: var(--card);
      font-family: var(--font);
      text-align: left;
      cursor: pointer;
      transition: border-color .15s ease, background .15s ease;
    }

    .buy-plan:hover {
      border-color: var(--brand);
    }

    /* 选中: 浅蓝底 + 蓝边就够了 —— 白底换成浅蓝, 一眼看得出选的是哪张(底色差要够,
       所以卡片本身是白的: 白 → 浅蓝比浅蓝 → 另一个浅蓝清楚得多) */
    .buy-plan.on {
      border-color: var(--brand);
      background: var(--brand-soft);
    }

    /* 档位名: 卡片里第一行, 只作认人用 —— 四张卡里三行字都压到 13px 以下, 是为了
       四列分下来每张还读得清(手机上每张 74px)。
       line-height 逐行钉死: 这一页的高度是按"勾选框必须留在视野里"抠出来的(见 ≤560
       那段的老账), 三行的行距各随各的字体行高, 加起来能差出十来个像素 —— 那十来个
       像素就是协议勾选框在折线上下的区别 */
    .buy-plan-n {
      font-size: 13px;
      line-height: 1.35;
      font-weight: 700;
      color: var(--navy);
    }

    /* 价钱: 第二行, 比档位名大一档 —— 四张卡里真正比的是这个数 */
    .buy-plan-p {
      font-size: 15px;
      line-height: 1.3;
      font-weight: 700;
      color: var(--ink-2);
    }

    .buy-plan-p em {
      margin-left: 1px;
      font-size: 10.5px;
      font-style: normal;
      font-weight: 600;
      color: var(--muted);
    }

    .buy-plan.on .buy-plan-p {
      color: var(--brand);
    }

    /* 到账点数: 第三行小字 —— 它才是这几档之间真正的差别(价钱差得远, 点数也差得远) */
    .buy-plan-g {
      font-size: 11px;
      line-height: 1.35;
      color: var(--muted);
      white-space: nowrap;
    }

    .buy-plan.on .buy-plan-g {
      color: var(--brand-deep);
    }

    /* 数量那组: 标签 + 步进器。上限与"1 份是什么"都挪进了标签的 data-tip —— 一行里塞不下
       三样, 而拨到头按钮自己就按不动了, 那句本来也只是个补充说明。
       铺满整宽后由 justify-content 接管左右: 标签贴左、步进器贴右, 两边正好与上面那行
       卡片的左右边缘是同一条。原先它是并排里的右半截, 靠 margin-left:auto 站住;
       现在自己一行, 那条 auto 只会把它顶成右半截 */
    .buy-qty {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
    }

    .buy-qty-k {
      flex-shrink: 0;
      font-size: 12.5px;
      color: var(--muted);
    }

    .buy-step {
      display: flex;
      align-items: center;
      border: 1px solid var(--line);
      border-radius: 8px;
      background: var(--card);
      overflow: hidden;
    }

    .buy-step-b {
      width: 30px;
      height: 32px;
      border: none;
      background: none;
      font-family: var(--font);
      font-size: 15px;
      line-height: 1;
      color: var(--ink-2);
      cursor: pointer;
      transition: background .15s ease, color .15s ease;
    }

    .buy-step-b:hover:not([disabled]) {
      background: var(--brand-soft);
      color: var(--brand-deep);
    }

    /* 到头了(1 或 100): 按不动就画成按不动的样子, 别让人以为点漏了 */
    .buy-step-b[disabled] {
      color: var(--faint);
      cursor: not-allowed;
    }

    /* 输入框: 中间那格, 宽度按三位数给 —— 拨到 100 时字不会挤着边框(大一号的格也是为这个:
       数字是这行里唯一要读的东西, 给它宽一点比给卡片空着强) */
    .buy-step-i {
      width: 64px;
      height: 32px;
      padding: 0;
      border: none;
      border-left: 1px solid var(--line);
      border-right: 1px solid var(--line);
      background: var(--card);
      font-family: var(--font);
      font-size: 13.5px;
      font-weight: 600;
      color: var(--navy);
      text-align: center;
      outline: none;
    }

    .buy-step-i:focus {
      background: var(--brand-soft);
    }

    /* 支付方式: 两颗单选并排等宽, 选中那颗与套餐卡同一套语言(蓝边 + 浅蓝底) */
    .buy-pays {
      display: flex;
      gap: 10px;
    }

    .buy-pay {
      flex: 1;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 6px;
      height: 36px;
      border: 1px solid var(--line);
      border-radius: 10px;
      background: var(--card);
      font-family: var(--font);
      font-size: 13px;
      font-weight: 600;
      color: var(--ink-2);
      cursor: pointer;
      transition: border-color .15s ease, background .15s ease, color .15s ease;
    }

    .buy-pay:hover {
      border-color: var(--brand);
    }

    .buy-pay.on {
      border-color: var(--brand);
      background: var(--brand-soft);
      color: var(--brand-deep);
    }

    .buy-pay svg {
      flex-shrink: 0;
      width: 17px;
      height: 17px;
    }

    /* 二维码: 码在左、金额与提示在右, 整块自己一块底 —— 上面选什么都落到这张码上。
       flex:1 是这一页的收尾: 上面几行是固定高度的, 剩下多少全给这张码(见 .set-pane 的注释)。
       min-height 是给矮屏留的底 —— 再挤也不许把这块压成一条, 宁可由右栏出滚动条。
       这个底从 --qr-px 推, 不写死: 它本来就是"码 + 上下内边距(14×2)"这一件事。
       原来写死 228, 是照 --qr-px:200 算的; 手机档把码收到 160 之后 228 就比这块真正
       需要的(188)高出 40, 而右栏高度是钉死的(见 ≤560 那块), 这 40 只能从下面的协议行
       身上扣 —— "必须勾了才能扫码"那个勾选框就是这么被挤出折线的。跟着 --qr-px 走,
       以后谁再改码的尺寸都不用回来改这个数。 */
    .buy-qr {
      flex: 1;
      min-height: calc(var(--qr-px) + 24px);
      --qr-px: 200px;
      /* 码的边长, 只有这一处是真源: 下面码盒的宽高都用它。画二维码时按盒子量到的
         宽度写回位图, 所以这里改小, 码跟着变小, 不会出现"200 的位图缩到 160 显示"
         那种发虚。手机档把它收到 160, 见 ≤560 那块。 */
      display: flex;
      align-items: center;
      gap: 16px;
      padding: 12px;
      border: 1px solid var(--line);
      border-radius: 12px;
      background: var(--bg-tint);
    }

    /* 上面那条 display: flex 会盖掉 [hidden] 自带的 display: none(作者样式压过浏览器默认),
       所以藏它得明写一条 —— 成功态与扫码卡是同一位置的两张脸, 不藏就两张一起摆着 */
    .buy-qr[hidden] {
      display: none;
    }

    /* 码的那块底 —— 富余的高度都归了这块, 码再小就白瞎那些地方了 */
    .buy-qr-code {
      position: relative;
      flex-shrink: 0;
      width: var(--qr-px);
      height: var(--qr-px);
      border-radius: 8px;
      background: var(--card);
      overflow: hidden;
    }

    .buy-qr-cv {
      display: block;
      width: var(--qr-px);
      height: var(--qr-px);
    }

    .buy-qr-cv[hidden] {
      display: none;
    }

    .buy-qr-cv canvas,
    .buy-qr-cv img {
      display: block;
      width: 100%;
      height: 100%;
    }

    /* 会员下单通道未开通时的占位(见 JS 的 BUY_ORDER_ON): 与码同一个盒子、同一个尺寸,
       开通后把码画进来即可, 这一页的高度与布局一个字都不用动。
       画一张付不了款的码, 或空着让人对着空白发愣, 都比明说一句差 */
    .buy-qr-soon {
      /* 铺在码那一格上(与 .buy-qr-mask 同一个写法): 空着的画布宿主在流里仍占满一整格,
         用 height:100% 的话它会被顶到格子下面、被 .buy-qr-code 的 overflow 剪掉 ——
         铺成绝对定位就不看流里还有什么了, 谁来谁走都不影响这句话露不露脸 */
      position: absolute;
      inset: 0;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      gap: 5px;
      padding: 12px;
      font-size: 11.5px;
      line-height: 1.6;
      color: var(--faint);
      text-align: center;
    }

    .buy-qr-soon b {
      font-size: 12.5px;
      font-weight: 700;
      color: var(--ink-2);
    }

    /* 必须显式写: 上面那句 display:flex 会盖掉 [hidden] 自带的 display:none */
    .buy-qr-soon[hidden] {
      display: none;
    }

    .buy-qr-status.err {
      color: var(--red-deep);
    }

    .buy-qr-status[hidden] {
      display: none;
    }

    /* 没同意协议: 码上面盖一层 —— 扫不成的码就该看不见, 留着人对着它扫半天更糟。
       这一层自己也是按钮(点它 = 同意并下单, 见 JS 的 wireBuyHost), 所以给手型:
       一句"点击此处同意…"摆在大字格里, 不给的话没人知道它能点 */
    .buy-qr-mask {
      position: absolute;
      inset: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      z-index: 2;
      padding: 16px;
      background: var(--overlay);
      font-size: 12px;
      line-height: 1.7;
      color: var(--muted);
      text-align: center;
      cursor: pointer;
    }

    /* 必须显式写: 上面那句 display:flex 会盖掉 [hidden] 自带的 display:none */
    .buy-qr-mask[hidden] {
      display: none;
    }

    .buy-qr-info {
      display: flex;
      flex-direction: column;
      gap: 5px;
      min-width: 0;
    }

    .buy-qr-amount {
      font-size: 12.5px;
      color: var(--muted);
    }

    /* 应付金额: 品牌蓝就够了, 不加大字号 —— 这一块里就它一个带色的, 已经跳出来了。
       (原来放到 21px, 比周围那行字大出一倍, 反而把"应付金额""元"挤成了脚注) */
    .buy-qr-amount b {
      margin: 0 3px 0 6px;
      font-weight: 700;
      color: var(--brand);
      letter-spacing: .3px;
    }

    .buy-qr-tip {
      font-size: 11.5px;
      color: var(--faint);
    }

    .buy-qr-tip b {
      margin: 0 2px;
      font-weight: 700;
      color: var(--ink-2);
    }

    /* 到期日整串不断行: 浏览器允许在连字符后换行, 393 那一档右栏窄,
       "2026-10-26" 会被断成 "2026-" / "10-26"。放不下时让"当前会员到期"
       自己折在上面一行, 日期保持完整 */
    .buy-qr-date {
      white-space: nowrap;
    }

    /* ── 支付成功态 ──
       与 .buy-qr 共用那个盒子(边框/圆角/最小高度都是它那套), 换上去这一页不跳高度;
       里面改成竖排的"对勾 + 已到账点数 + 余额"。绿是全站"已完成"那一档(同 .q-card.answered),
       正是这一刻唯一要说的事 */
    .buy-ok {
      flex-direction: column;
      justify-content: center;
      gap: 5px;
      text-align: center;
    }

    .buy-ok[hidden] {
      display: none;
    }

    .buy-ok-ic {
      display: flex;
      align-items: center;
      justify-content: center;
      width: 42px;
      height: 42px;
      margin-bottom: 3px;
      border-radius: 50%;
      background: var(--green-soft);
      border: 1px solid var(--green-border);
      color: var(--green-deep);
    }

    .buy-ok-ic svg {
      width: 21px;
      height: 21px;
    }

    .buy-ok-t {
      font-size: 14px;
      font-weight: 600;
      color: var(--navy);
    }

    .buy-ok-pts {
      font-size: 12.5px;
      color: var(--muted);
    }

    .buy-ok-pts b {
      margin: 0 2px;
      font-weight: 700;
      color: var(--green-deep);
    }

    .buy-ok-bal {
      font-size: 11.5px;
      color: var(--faint);
    }

    .buy-ok-bal b {
      margin: 0 2px;
      font-weight: 700;
      color: var(--ink-2);
    }

    .buy-ok-bal[hidden] {
      display: none;
    }

    /* 再买一份: 次要动作(白底描边, 同 .set-btn 那一档)—— 这一页的主操作本来是"扫码",
       成功之后它没了, 剩下这颗不该长得像主键 */
    .buy-ok-again {
      margin-top: 5px;
      padding: 5px 14px;
      border: 1px solid var(--line);
      border-radius: 8px;
      background: var(--card);
      font-family: var(--font);
      font-size: 12.5px;
      font-weight: 600;
      color: var(--ink-2);
      cursor: pointer;
      transition: color .15s ease, border-color .15s ease;
    }

    .buy-ok-again:hover {
      color: var(--brand);
      border-color: var(--brand);
    }

    /* 协议: 一整个 label 可点(点字也算勾), 链接自己吃掉冒泡, 不改勾选状态 */
    .buy-agree {
      display: flex;
      align-items: flex-start;
      gap: 7px;
      font-size: 12.5px;
      line-height: 1.6;
      color: var(--ink-2);
      cursor: pointer;
      user-select: none;
    }

    .buy-agree input {
      flex-shrink: 0;
      width: 14px;
      height: 14px;
      margin: 3px 0 0;
      accent-color: var(--brand);
      cursor: pointer;
    }

    .buy-agree a {
      color: var(--brand);
      text-decoration: none;
    }

    /* 买点弹窗(#pts)那一行商品: 一左一右两样 —— "每份 30 点" 与 "¥9.99 / 份"。
       排布照 .buy-qty(标签左、值右), 但基线对齐: 标签是小字、价钱是大字, 居中会让两截
       错开半行。这一行替代的是会员页那一整行四张档位卡 —— 那一枚弹窗只卖一样东西。
       价钱用品牌蓝, 与 .buy-qr-amount 同一套语言。 */
    .pts-pack {
      display: flex;
      align-items: baseline;
      justify-content: space-between;
      gap: 10px;
      font-size: 12.5px;
      color: var(--muted);
    }

    .pts-pack-n b {
      margin: 0 2px;
      font-weight: 700;
      color: var(--ink-2);
    }

    .pts-pack-p {
      font-size: 15px;
      font-weight: 700;
      letter-spacing: .3px;
      color: var(--brand);
    }

    .pts-pack-p em {
      margin-left: 3px;
      font-size: 10.5px;
      font-style: normal;
      font-weight: 600;
      color: var(--muted);
    }

    /* 窄屏: 两栏并排会把内容挤成一条, 改成上下两块(左栏那一列摊平成一行横向滚) */
    /* ── 手机档(≤560): 设置弹窗 ──
       手机(360~430)本来就落在 720 与 900 里面了, 所以这一轮没有新增断点 —— 要动的
       只有这个弹窗, 就都收在这一块里。 */
    @media (max-width: 560px) {

      /* 高度: 桌面那条 min(568px, 70vh) 是"最长那页自然撑出来的高度", 为的是切分区时
         弹窗不跳。可 70vh 在手机上比内容需要的还少 —— 393×659 下 70vh=461, 而购买页
         要 518, 于是"必须勾了才能扫码"那个协议勾选框掉到折线下 82px。而 iOS 不画滚动条,
         屏幕上没有任何提示下面还有东西。
         换成"视口减掉弹窗外边距与门头", 那才是手机上真正可用的高度:
           100dvh − 24×2(.mdl 的内边距) − 50(.mdl-head) = 100dvh − 98, 取 100 留 2px 余量。
         封顶那个数一个字没动(见上面那条, 它跟着购买页长), 所以"切分区不跳高度"照旧:
         手机上六页也还是同一个数。
         **但那道封顶在矮屏上够不着**: 100dvh−100 在 640 高的屏上只有 540, 而加过
         「仅购买 AI 点」那一行之后这一页要 561 —— 少 21。这一页自己再收一次:
         块间距 14 → 9(见下面 #acctPaneBuy 那条), 五个间隙省下 25。两笔合起来才够。
         两条并排写, 与 base.css 那条同一个写法: 不认 dvh 的丢掉后一条、退回 vh; 两条
         都不认的整块作废、退回上面那条 70vh, 也就是今天的样子, 不会更坏。
         底下两道自动兜底别删: .mdl-panel 的 max-height:100% 先夹住面板, .acct-body 的
         min-height:0(见上面那条)再把身体收回去 —— 两者合力保证面板任何情况下都不会
         比 .mdl 的盒子高, 所以"内容藏在被切掉的那半截里"在数学上不可能发生。 */
      .mdl-acct .acct-body {
        height: min(568px, calc(100vh - 100px));
        height: min(568px, calc(100dvh - 100px));
      }

      /* 「会员与 AI 点」这一页自己收间距: 它是六页里最高的那一页(封顶那个数就是它定调的),
         手机上最紧的也是它 —— 收别人的间距救不了它。14 → 9 一次省 25px(这一页有六个块),
         正好补上「仅购买 AI 点」那一行在矮屏上多出来的那截(见上面那条的账)。
         只认这一页: 别页的内容用不着这么挤, 而"切分区时六页一样高"靠的是那个封顶数,
         与这一页内部的间距无关。 */
      #acctPaneBuy {
        gap: 9px;
      }

      /* 两栏(左导航 + 右内容)改成上下 */
      .mdl-acct .acct-body {
        flex-direction: column;
      }

      .set-nav {
        width: 100%;
        flex-direction: row;
        gap: 4px;
        padding: 8px 10px;
        border-right: none;
        border-bottom: 1px solid var(--line-soft);
        /* 六个 tab 横排在 357px 里放不下, 这一行是要横着滚的(JS 会把当前那条滚进来,
           见 syncTabIntoView)。滚动条在图底下会挤掉一行, 关掉 */
        scrollbar-width: none;
      }

      .set-nav::-webkit-scrollbar {
        display: none;
      }

      .set-tab {
        width: auto;
        white-space: nowrap;
      }

      .set-tab--plain {
        padding-left: 10px;
      }

      .set-nav-sep {
        width: 1px;
        height: auto;
        margin: 4px 2px;
      }

      /* 码与金额: 竖排是原来那条, 但它把码的 200 全摊在高度上 —— 竖排的内容要 286,
         而这块盒子当时被 min-height:228 钉着, 于是信息块溢出卡片下边框 59px(实测),
         正好压在下面那行协议上。这是手机上"购买AI点"看着乱成一团的头一个原因, 桌面上
         看不到是因为那儿富余高度够, 盒子会涨。
         改成并排、码收到 160: 高度由最高的那项定, 信息块自然装在里面, 比竖排的 286
         省下近百像素。160 的码在手机上扫得动(常见下限约 150), 这一档只在 ≤560 生效。
         第二个原因在 .buy-qr 那条 min-height 上(它跟着 --qr-px 走, 在这里自动收到 188) ——
         两处都改完, 360/393/430/560 四个宽度实测「要滚 0, 勾选框可见」。
         320 还差 46px: 那个宽度下"码 160 + 间隙 12"已经吃掉卡片内宽的九成, 信息块只剩
         46px, 并排本身就摆不开。没有为它单开一档 —— 全站只有 900/720/560 三个宽度断点,
         不为已淘汰的机型破这条; 那里按 .buy-qr 注释里写的退让走: 宁可出滚动条, 不压扁。 */
      .buy-qr {
        flex-direction: row;
        gap: 12px;
        padding: 14px 12px;
        --qr-px: 160px;
      }

      /* 成功态在窄屏仍是竖排: 上面那条 row 是给"码 + 信息块"并排用的(见上面那段注释),
         成功态里没有要并排的东西 —— 不写这条, 手机档会把它按横排摆开, 挤成一行 */
      .buy-ok {
        flex-direction: column;
      }

      /* 买点弹窗(#pts): 把 .mdl 壳的留白收一档, 为的是它底下那颗「购买会员，到账 AI 点」。
         那一枚比会员页多一行(见 HTML 里那段注释), 而 320×568 上这一页正好卡在"面板高 =
         可用高"的线上 —— 不收这 20px, 那颗按钮就落在折线下面, 得滚一下才看得见; 而它正是
         这一枚里"想买会员"唯一的路。
         收的三处都不动别的弹窗(带 .mdl-pts 前缀)也不动 360 以上那些宽度的观感: gap 与内边距
         各小 2~4px, 六个空档加起来刚好 20。码的尺寸(--qr-px)与协议行一个字没动 ——
         "必须勾了才能扫码"那个勾选框不受影响。 */
      .mdl-pts .mdl-head {
        padding: 11px 16px;
      }

      .mdl-pts .mdl-body {
        gap: 12px;
        padding: 14px;
      }

      /* 会员卡: 桌面四列分到 393 上是每张 74px —— 内边距与字号不各缩一档,
         「到账 2500 点」那句(四张卡里最长的一行)会顶到卡片边框上。
         卡片本身不改两行两列: 四列在这一档更省高度(见 .buy-plans 的注释),
         而这一页高出来的每一分都得从底下那个协议勾选框身上扣 */
      .buy-plan {
        padding: 7px 8px;
      }

      .buy-plan-n {
        font-size: 12px;
      }

      .buy-plan-p {
        font-size: 13.5px;
      }

      .buy-plan-g {
        font-size: 10px;
      }
    }

    .file-box {
      display: flex;
      align-items: center;
      gap: 8px;
      min-width: 0;
    }

    .upload-btn {
      display: inline-flex;
      align-items: center;
      gap: 5px;
      padding: 7px 16px;
      border: none;
      border-radius: 8px;
      background: var(--brand);
      color: #fff;
      font-size: 12.5px;
      cursor: pointer;
      white-space: nowrap;
      transition: filter .15s ease, transform .1s ease;
    }

    .upload-btn svg {
      width: 13px;
      height: 13px;
    }

    .upload-btn:hover {
      filter: brightness(1.05);
    }

    .upload-btn:active {
      transform: translateY(1px);
    }

    /* ── 对话区 ── */
    .chat {
      flex: 1;
      overflow-y: auto;
      /* ── 横向的兜底: 这一条轴上绝不滚 ──
         .chat 是 overflow-y:auto, 按规范"另一轴是 visible 就也算 auto", 于是整列**天生**
         带着一条横向滚动条 —— 只要有任何一个盒子比列宽, 手机上就表现成"整块对话能左右拖"。
         拖出来的那截通常是空白(溢出的只是某一行文字的墨迹, 卡片底色并没有跟着变宽), 而
         正文反倒被拖出视野: 用户看到的是"右边空一片 + 左边缘的字被切掉"(2026-10-07 用户
         在手机上就是这么报的)。
         断行的账在 .chat-inner 上(见那里, 已把长串一律改成可断行); 这一条是最后一道保险:
         真还有哪块内容比列宽(新加的盒子、图片、算错宽度的公式), 宁可把多出来那截裁掉,
         也不要把整列变成可拖的 —— 一条横向滚动条会把"纵向阅读"整个搅乱。
         写在 .chat 上而不是 .chat-inner 上: 要裁就得裁在滚动的那个盒子上, 写在内层
         等于没裁(内层自己不会滚, 溢出的部分照样顶到外层去)。 */
      overflow-x: hidden;
      /* 右侧只留 --rail-gap 那一道(现在与左侧同宽): 控件与定位条都不占列宽, 这块整片归正文 */
      padding: 16px var(--rail-gap) 16px 6px;
      min-height: 0;
      scrollbar-width: thin;
      scrollbar-color: var(--line-strong) transparent;
    }

    .chat::-webkit-scrollbar {
      width: 8px;
    }

    .chat::-webkit-scrollbar-thumb {
      background: var(--line-strong);
      border-radius: 4px;
    }

    .chat::-webkit-scrollbar-track {
      background: transparent;
    }

    .chat-inner {
      display: flex;
      flex-direction: column;
      gap: 16px;
      /* ── 长串一律允许断行(从这里继承到整列) ──
         这一列里凡是用户/AI/后端带来的文本, 都可能夹着一条**没有断点**的长串: 英文文件名
         (China_Millennial_Luxury_Goods_Survey_v2_cleaned.xlsx)、路径、工具名、报错原文、
         URL、一长串数字。CJK 每个字都是断点, 所以中文再长也会自己折; 这种拉丁/数字串
         断不了, 就把自己的盒子顶宽, 一路把 .chat 顶出横向滚动条(现象见 .chat 那一段)。
         2026-10-07 实测(无头 Chrome, 触摸档, 列宽 336/366/388 三档) —— 下面这几个盒子
         当时都**没有**断行规则, 一条长文件名就够拉出 100~200px 的横向滚动:
           · .tl .usage-line              "数据文件：<文件名> · Agent 开始分析 · 运行号 …"
           · .report-wrap > .usage-line   "本轮用量 · 总 TOKEN …"
           · .error-banner                请求失败/分析中断的原文
           · .report-empty                本轮未生成报告：<原因>
           · .msg.system                  系统提示
         写在这一层而不是逐个补: 它们都是"顺带说一句"的行, 断行规则没有理由各家不同;
         挂在这儿, 以后新加的盒子自动继承(报告正文那一支本来就有自己的 anywhere)。
         用 anywhere 不用 break-word: 前者连 min-content 一起收到一个字, flex 项才真压得
         下去(break-word 只改断行、不改内在宽度, 该顶宽还是顶宽)。
         不影响表格与公式 —— 单元格是 white-space: nowrap(见 .report-body th/td), 公式是
         KaTeX 自己的 nowrap 盒, overflow-wrap 对 nowrap 的内容不起作用, 它们各自的
         .tbl-scroll / .math-block 照旧横滚。 */
      overflow-wrap: anywhere;
    }

    /* ═══════════════════════════════════════════════════════════════
       空态 landing(js 的 addWelcome)
       新进用户、删光全部回答、换数据之后都是这一块。它是**页面**, 不是一条消息:
       原先用 .msg.agent 气泡挂在顶部, 下面空出六七百像素, 读起来像"加载卡住了"。
       现在整块在对话区里垂直居中, 第一轮消息一落就撤掉(见 js 的 setEmpty), 消息流
       照旧从顶部开始 —— ChatGPT / Claude 那一档的空态都是这个结构。
       ═══════════════════════════════════════════════════════════════ */
    .chat-inner.empty {
      /* 撑满 .chat, 让 .welcome 的 auto margin 有富余空间可分 */
      min-height: 100%;
    }

    .welcome {
      width: 100%;
      max-width: 760px;
      /* 居中靠子项的 auto margin, 不用 justify-content: center: 窄屏/矮窗口下内容
         比视口高时 auto margin 归零、退化成正常滚动; justify-content: center 会把
         上边缘裁到视野外, 而且滚不回去。 */
      margin: auto;
      padding: 24px 0;
      display: flex;
      flex-direction: column;
      align-items: center;
    }

    .welcome-hero {
      display: flex;
      flex-direction: column;
      align-items: center;
      text-align: center;
      gap: 10px;
      margin-bottom: 26px;
    }

    .welcome-title {
      font-size: 22px;
      line-height: 1.3;
      font-weight: 700;
      letter-spacing: .2px;
      color: var(--navy);
    }

    .welcome-sub {
      max-width: 520px;
      font-size: 13.5px;
      line-height: 1.75;
      color: var(--muted);
    }

    /* 桌面一句话, 窄屏断成两句各占一行(见下面的 ≤720 块): 让它自己折,
       360px 上会折在"给 / 全。"中间, 末行只剩两个字。 */
    .welcome-sub-2 {
      display: inline;
    }

    /* ── 四张结果卡 ──
       标题是"用户想要的结果", 副标题里才露出 Agent 的差异点(自动选方法/通读数据/
       按规范排版)。卡本身是 <button>: 键盘可达、回车即发。 */
    .welcome-cards {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 12px;
      width: 100%;
    }

    /* 窄栏收成一列: 类由 js 按对话列的实际宽度加(见 fitWelcome) —— 左栏展开、
       绘图栏分栏都会压窄这一列, 只看视口断点判不出来。 */
    .welcome.one-col .welcome-cards {
      grid-template-columns: 1fr;
    }

    .welcome-card {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      gap: 6px;
      padding: 15px 16px 16px;
      font: inherit;
      text-align: left;
      color: inherit;
      background: var(--card);
      border: 1px solid var(--line-soft);
      border-radius: 14px;
      box-shadow: var(--shadow-sm);
      cursor: pointer;
      transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease;
    }

    .welcome-card:hover {
      border-color: var(--line-strong);
      box-shadow: 0 4px 16px rgba(3, 30, 95, .09);
      transform: translateY(-1px);
    }

    .welcome-card:active {
      transform: translateY(0);
    }

    .welcome-card:focus-visible {
      outline: 2px solid var(--brand);
      outline-offset: 2px;
    }

    .welcome-card-t {
      display: flex;
      align-items: center;
      gap: 8px;
      width: 100%;
      font-size: 14px;
      font-weight: 600;
      color: var(--ink);
    }

    /* 箭头只是"能点"的提示, 用伪元素挂 —— 不进读屏、也不会被当成正文复制 */
    .welcome-card-t::after {
      content: '→';
      margin-left: auto;
      font-weight: 500;
      color: var(--brand);
      transition: transform .15s ease;
    }

    .welcome-card:hover .welcome-card-t::after {
      transform: translateX(3px);
    }

    .welcome-card-sub {
      font-size: 12.5px;
      line-height: 1.65;
      color: var(--muted);
    }

    /* ── 没数据时的上传投放区 ──
       它同时接点击(开文件选择器)与拖拽(见 js 的 drop 那一段), 所以长得要像落点。 */
    .welcome-upload {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 7px;
      width: 100%;
      padding: 34px 20px;
      font: inherit;
      color: inherit;
      background: var(--overlay-soft);
      border: 1.5px dashed var(--line-strong);
      border-radius: 16px;
      cursor: pointer;
      transition: border-color .15s ease, background .15s ease, box-shadow .15s ease;
    }

    .welcome-upload:hover,
    .welcome-upload.drag-over {
      border-color: var(--brand);
      background: var(--brand-soft);
      box-shadow: inset 0 0 0 1px rgba(23, 138, 236, .12);
    }

    .welcome-upload:focus-visible {
      outline: 2px solid var(--brand);
      outline-offset: 2px;
    }

    .welcome-upload-ic {
      width: 44px;
      height: 44px;
      margin-bottom: 3px;
      display: grid;
      place-items: center;
      color: var(--brand);
      background: var(--brand-soft);
      border-radius: 13px;
    }

    .welcome-upload-ic svg {
      width: 22px;
      height: 22px;
    }

    .welcome-upload-t {
      font-size: 14px;
      font-weight: 600;
      color: var(--ink);
    }

    .welcome-upload-sub {
      font-size: 12.5px;
      color: var(--faint);
    }

    .welcome-points {
      display: flex;
      flex-wrap: wrap;
      justify-content: center;
      gap: 8px 20px;
      margin-top: 20px;
      list-style: none;
      font-size: 12px;
      color: var(--faint);
    }

    .welcome-points li {
      display: flex;
      align-items: center;
      gap: 6px;
    }

    .welcome-points li::before {
      content: '';
      width: 5px;
      height: 5px;
      border-radius: 50%;
      background: var(--brand);
      opacity: .55;
    }

    /* ── 右侧提问定位条 ──
       一份数据连着问十几个问题是常态, 想回看第 3 问的报告只能一路往上翻 —— 这条就是
       对话流的"目录": 右下角常驻一颗胶囊, 上面写着"问题 3/17", 点开是一张列着全部提问
       的卡片, 点哪一条滚到哪一条。
       入口只有这一颗, 三档设备同一个长相、同一套交互 —— 原先桌面与平板走的是"一排刻度
       线, 悬停展开", 手机走胶囊, 两套行为; 现在并成一套: 悬停那套在触屏上根本没有对应
       动作(触屏的展开靠 .tap-open 撑着), 而触屏要的那套(点开、点条目、点别处收)在鼠标
       上照样成立。并成一套之后, "同一件事有两种说法"这类分支也随之消失了。
       收起时那张列表是**看不见也在原地**的(透明底、字宽为 0), 所以它常态 visibility:
       hidden —— 否则右缘凭白多出一片隐形的点击区, 鼠标从右边经过就被吞掉; 而且里头那十几条
       是按钮, 只靠"透明"的话键盘一 Tab 就落在看不见的条目上。展开时才显形、才接事件。
       竖向与 .chat 同高(由 layoutRail 按它的盒同步), 居中即对话区的正中: 输入框那行
       随示例词/附件时高时低, 用 CSS 的 top/bottom 定死会跟着漂。
       它浮在对话列右缘的正文之上, 不再从列宽里抠出一块空档 —— 代价是正文右侧得让出
       一道 --rail-gap, 好处是省下的列宽归了绘图栏(见 .container 的 max-width 与 JS 的
       VIZ_W_MAX)。展开的那张卡片压住正文是正常的: 它有白底和阴影。 */
    .qa-rail {
      position: absolute;
      /* 横向: 与容器内边距(20)对齐 —— 胶囊贴着对话区右缘, 与输入框那一列对得上;
         卡片右缘也因此盖住对话区自己那道滚动条(#C6D9EF, 画在右缘 8px 内), 不会
         露出半截浅蓝跟卡片并排。窄屏同一条规则见本文件末尾的 ≤720 那块。 */
      right: 20px;
      width: var(--rail-w);
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: flex-end;
      /* 浮在正文上, 但要让真弹层(账号下拉 40 这类)压得住它 */
      z-index: 5;
      /* 空档其余地方不挡鼠标, 只有底下列表那一块(展开时)收事件 */
      pointer-events: none;
    }

    /* .qa-rail 是 display:flex, 会盖掉 [hidden] 自带的 display:none, 得自己补一条 */
    .qa-rail[hidden] {
      display: none;
    }

    .qa-list {
      display: flex;
      flex-direction: column;
      align-items: stretch;
      gap: 7px;
      max-width: 100%;
      max-height: 100%;
      padding: 7px 6px;
      border: 1px solid transparent;
      border-radius: 12px;
      background: transparent;
      /* 收起时只是个透明的空壳(见 .qa-rail 的注释): visibility 一并关掉, 里头那十几条
         就既不收指针、也不进 Tab 序 —— 光靠"透明"的话, 键盘一 Tab 就落在一串看不见的
         条目上, 而它们是按钮, 回车还会跳走。 */
      visibility: hidden;
      transition: background .16s ease, border-color .16s ease, box-shadow .16s ease;
      /* 问题多时列表比对话区还高: 得能滚, 还得让人看得出"下面还有" —— 细滚动条(见下)。
         槽位一直留着(scrollbar-gutter), 否则展开的一瞬间槽位才冒出来, 二十来条会整体
         左移 6px 跳一下。老 Safari 没有 scrollbar-gutter, 会跳这一下。 */
      overflow-y: auto;
      scrollbar-gutter: stable;
    }

    /* 点开才显形: 白卡片浮起来, 提问原文从收着的宽度里展开。
       .tap-open 由 JS 按点击给(见 buildRail / qaPill.onclick), 三种收起的路(再点胶囊、
       点某一条、点别处)对应的是同一个类的增删。 */
    .qa-rail.tap-open .qa-list {
      background: var(--card);
      border-color: var(--line);
      box-shadow: 0 2px 10px rgba(3, 30, 95, .07);
      visibility: visible;
      pointer-events: auto;
    }

    /* WebKit 侧的同一条滚动条。宽度定在这儿, 上面 scrollbar-gutter 占的槽位就是它。
       滑块用 --faint: 它是个要抓的控件(非文字), 得够看得见 —— 段线那个 #AEB8C4
       压白底只有 1.5:1, 当刻度行, 当滚动条就等于没有。 */
    .qa-list::-webkit-scrollbar {
      width: 6px;
    }

    .qa-list::-webkit-scrollbar-track {
      background: transparent;
    }

    .qa-list::-webkit-scrollbar-thumb {
      background: transparent;
      border-radius: 3px;
    }

    .qa-rail.tap-open .qa-list::-webkit-scrollbar-thumb {
      background: var(--faint);
    }

    /* Firefox 不认 ::-webkit-scrollbar, 只能走标准的那两个属性。单独关在 @supports 里
       是因为它们跟上面那套是互斥的: 一旦写了 scrollbar-width/-color(非 auto), Blink
       就改走标准滚动条、把 ::-webkit-scrollbar 那几条全丢掉 —— 一条 6px 的窄条会变成
       系统宽度(Windows 上十来 px), 条目跟着左移一截。所以按"认不认 webkit 伪元素"分流。 */
    @supports not selector(::-webkit-scrollbar) {
      .qa-list {
        scrollbar-width: thin;
        scrollbar-color: transparent transparent;
      }

      .qa-rail.tap-open .qa-list {
        scrollbar-color: var(--faint) transparent;
      }
    }

    .qa-item {
      display: flex;
      align-items: center;
      justify-content: flex-end;
      gap: 8px;
      padding: 2px 3px;
      border: none;
      border-radius: 7px;
      background: none;
      /* button 默认不带 font: 不继承就是浏览器默认的 13.33px 宋体 */
      font: inherit;
      text-align: right;
      cursor: pointer;
    }

    .qa-item:hover {
      background: var(--bg-tint);
    }

    /* 宽度预算: 300(--rail-w) − 2(列表边框) − 12(列表内边距) − 6(条目内边距)
       − 6(滚动条槽位, 见 .qa-list) = 274(刻度线那排删掉之后比原来宽出 20)。
       186 远在预算之内, 卡住它的不是地方, 是这张卡片本身: 它浮在正文右缘上, 展开多宽
       就压掉多宽的正文字。原来放到 246(约 20 个字)时, 长提问几乎把右半屏盖满, 读报告
       得先等它收回去 —— 收在 186, 认得出是哪一问, 也让得开。
       186px 约合 15 个汉字(font-size 12 的汉字正好 1em 宽, 15×12=180, 余 6 当垫底)。
       再短就不行: 早先卡在 106(约 9 个字)时, 长一点的提问展开后仍然只剩个
       "把年龄按 18-30/3…", 认不出是哪一问, 等于白点开。
       多出来的由 ellipsis 收尾, 卡片不会撑破 --rail-w 再往左压正文。 */
    .qa-t {
      flex: 0 1 auto;
      min-width: 0;
      max-width: 0;
      font-size: 12px;
      line-height: 1.45;
      color: var(--muted);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      opacity: 0;
      transition: max-width .18s ease, opacity .16s ease, color .16s ease;
    }

    .qa-rail.tap-open .qa-t {
      max-width: 186px;
      opacity: 1;
    }

    /* 当前落在哪一问: 只上品牌蓝, 与对话里别处"蓝=当前"是同一个语汇 */
    .qa-item.on .qa-t {
      color: var(--brand);
    }

    /* 悬停的那条压过"当前"的蓝(同为 0,3,0, 排在后面即胜出): 指哪条哪条深一档 */
    .qa-item:hover .qa-t {
      color: var(--navy);
    }

    /* ── 那颗胶囊: 定位条唯一的入口 ──
       钉在定位条的右下角: 定位条是绝对定位的、右下角就是对话区右下角(输入条上沿那条线
       上方), 与输入框右缘对齐。三档设备都显形(原先是手机专属, 现在刻度线那排整个删了)。
       文字由 JS 写成"问题 2/17"(见 setRailActive) —— 与展开的那张卡片说的是同一件事
       (当前在哪一问), 所以跟着同一个函数走, 不另算一份。 */
    .qa-pill {
      position: absolute;
      right: 0;
      bottom: 6px;
      display: flex;
      align-items: center;
      padding: 7px 13px;
      border: 1px solid var(--line);
      border-radius: 999px;
      background: var(--card);
      box-shadow: var(--shadow-sm);
      font: inherit;
      font-size: 12.5px;
      line-height: 1.2;
      color: var(--muted);
      white-space: nowrap;
      cursor: pointer;
      pointer-events: auto;
    }

    .qa-pill:active {
      border-color: var(--brand);
      color: var(--brand-deep);
    }

    /* ── 对话搜索(顶部放大镜, 见 HTML 的 #chatSearch / JS 的 chatSearch 段) ──
       浮在对话区右上, 与右下那颗胶囊(.qa-pill)同款白底描边, 只是钉在顶部。
       按钮钉最右, 展开的输入条往左长(row-reverse 处理了这个顺序)。 */
    .chat-search {
      position: absolute;
      top: 10px;
      right: 20px;
      z-index: 6;
      display: flex;
      flex-direction: row-reverse;
      align-items: center;
      gap: 8px;
    }

    .chat-search-btn {
      width: 34px;
      height: 34px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      padding: 0;
      border: 1px solid var(--line);
      border-radius: 50%;
      background: var(--card);
      color: var(--muted);
      box-shadow: var(--shadow-sm);
      cursor: pointer;
    }

    .chat-search-btn:hover,
    .chat-search.open .chat-search-btn {
      color: var(--brand);
      border-color: var(--brand);
    }

    .chat-search-btn svg {
      width: 16px;
      height: 16px;
    }

    .chat-search-bar {
      display: flex;
      align-items: center;
      gap: 6px;
      padding: 4px 6px 4px 12px;
      border: 1px solid var(--line);
      border-radius: 999px;
      background: var(--card);
      box-shadow: var(--shadow-sm);
    }

    .chat-search-bar[hidden] {
      display: none;
    }

    .chat-search-bar input {
      width: 190px;
      border: none;
      outline: none;
      background: none;
      font: inherit;
      font-size: 12.5px;
      line-height: 1.5;
      color: var(--ink);
    }

    .chat-search-bar input::placeholder {
      color: var(--muted);
    }

    .chat-search-meta {
      font-size: 11.5px;
      color: var(--muted);
      white-space: nowrap;
    }

    /* 搜索命中: 提问气泡加一圈品牌蓝描边; 右侧定位条里非命中项收掉 */
    .msg.user.qa-hit {
      box-shadow: 0 0 0 2px var(--brand), 0 1px 2px rgba(3, 30, 95, .04), 0 2px 8px rgba(3, 30, 95, .05);
    }

    .qa-item.hide {
      display: none;
    }

    /* 搜索词命中: 报告/提问正文里命中的词包一层琥珀黄底, 与整条命中的蓝描边(.qa-hit)区分开。
       底用 --hl-bg、字用 --hl-ink(深棕压黄底), 两种皮肤都写死不继承 —— 浅色下 --amber-soft
       太贴近白卡看不出来, 深色下那层棕和页面底分不开、继承来的浅字更是发糊。box-decoration-break
       让跨行的命中段每一行都有圆角, 而不是只在首尾。 */
    mark[data-qa-hl] {
      background: var(--hl-bg);
      color: var(--hl-ink);
      border-radius: 3px;
      padding: 0 2px;
      box-decoration-break: clone;
      -webkit-box-decoration-break: clone;
    }

    .msg {
      max-width: 100%;
      animation: msg-in .28s cubic-bezier(.2, .7, .3, 1);
    }

    @keyframes msg-in {
      from {
        opacity: 0;
        transform: translateY(6px);
      }

      to {
        opacity: 1;
        transform: translateY(0);
      }
    }

    .msg.system {
      align-self: center;
      background: var(--card-3);
      color: var(--faint);
      font-size: 12px;
      padding: 5px 16px;
      border-radius: 999px;
    }

    /* 用户气泡: 淡蓝底 + 深海军蓝字。
       原来是与 --brand 同色的实心蓝 + 白字, 一屏里最抢眼的就是它 —— 但这一屏
       要读的是 AI 的报告, 用户那句问话不该压过正文。改淡之后按 12.5:1 也更清楚
       (实心蓝 + 白字只有 3.57:1, 13.5px 的字本就偏虚)。
       底 #D8E9FC 比 --brand-soft 深一档: 页面底就是 --bg-tint(#F3F9FE),
       再浅就糊进背景里看不出这是块气泡了。 */
    .msg.user {
      position: relative;
      align-self: flex-end;
      background: var(--brand-tint);
      color: var(--navy);
      border: none;
      padding: 10px 15px;
      border-radius: 14px 14px 4px 14px;
      font-size: 13.5px;
      line-height: 1.65;
      white-space: pre-wrap;
      word-break: break-word;
      max-width: 76%;
      box-shadow: 0 1px 2px rgba(3, 30, 95, .04), 0 2px 8px rgba(3, 30, 95, .05);
    }

    /* 长提问折叠: 超过 4 行截断加省略号(行数由 JS 写进 style, 见 clampUserMsg),
       气泡里补一颗「展开全文」。折叠期间正文仍在 DOM 里, 复制/修改拿到的都是全文。 */
    .msg-text.clamp {
      display: -webkit-box;
      -webkit-box-orient: vertical;
      overflow: hidden;
    }

    /* 「展开全文 / 收起」: 贴着正文左下的一行小字, 不加边框 —— 气泡里再放个按钮
       太吵, 这里只需要一个"还能展开"的信号。它是气泡的正常子元素(不是绝对定位),
       高度算进气泡: 折叠省下来的高度正是为了让下面的报告往上挪。 */
    .msg-more {
      display: block;
      margin: 5px 0 0;
      padding: 0;
      border: none;
      background: none;
      color: var(--brand);
      font-size: 12px;
      line-height: 1.6;
      text-align: left;
      cursor: pointer;
    }

    /* 气泡左外侧的三颗操作键(复制/修改/删除): 平时不显形(一屏十几条提问, 每个都挂
       图标太吵), 悬停气泡或键盘聚焦时才出来。绝对定位 + right:100% 挂在气泡
       **外面**, 既不占气泡宽度, 也不会被"选中气泡文字"的操作碰上。
       摆成「复制在上、删除+修改并排在下」: 复制与修改上下对齐(右缘都贴 8px, 两件
       "改字"的事在同一列上), 删除在左(红、不可逆, 单独贴到最外侧 right=100%+40)。
       竖直两行中心差 32(复制 50%-16、并排 50%+16), 即 top = 50%-29 / 50%+3。
       手机/平板与桌面同款(原先触屏改回三颗竖排, 现按需求与桌面统一)。 */
    .msg-copy,
    .msg-edit,
    .msg-del {
      position: absolute;
      right: calc(100% + 8px);
      width: 26px;
      height: 26px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      padding: 0;
      border: 1px solid var(--line);
      border-radius: 50%;
      background: var(--card);
      color: var(--muted);
      cursor: pointer;
      opacity: 0;
      transition: opacity .15s ease, color .15s ease, border-color .15s ease;
    }

    .msg-copy {
      top: calc(50% - 29px);
    }

    .msg-edit {
      top: calc(50% + 3px);
    }

    .msg-del {
      top: calc(50% + 3px);
      right: calc(100% + 40px);
    }

    .msg.user:hover .msg-copy,
    .msg.user:hover .msg-edit,
    .msg.user:hover .msg-del,
    .msg-copy:focus-visible,
    .msg-edit:focus-visible,
    .msg-del:focus-visible,
    .msg-copy:active,
    .msg-edit:active,
    .msg-del:active {
      opacity: 1;
    }

    .msg-copy:hover,
    .msg-edit:hover,
    .msg-copy:active,
    .msg-edit:active {
      color: var(--brand);
      border-color: var(--brand);
    }

    /* 删除是唯一不可逆的一颗: 用红而不是蓝, 与全站"红=危险/不可逆"同一语汇 */
    .msg-del:hover,
    .msg-del:active {
      color: var(--red);
      border-color: var(--red);
    }

    .msg-copy svg,
    .msg-edit svg,
    .msg-del svg {
      width: 13px;
      height: 13px;
    }

    /* ── 修改提问(就地编辑, 见 JS 的 startEditMsg) ──
       编辑态: 原文让位给输入框, 左外侧那两颗圆形操作键一并收起 —— 它们指向的
       正是此刻正在被改写的文字, 留着只会让人点错。
       「展开全文」同理: 它折叠的是此刻已经让位的正文, 留着点它没反应。 */
    .msg.user.editing .msg-text,
    .msg.user.editing .msg-more,
    .msg.user.editing .msg-copy,
    .msg.user.editing .msg-edit,
    .msg.user.editing .msg-del {
      display: none;
    }

    /* 编辑时气泡撑到最宽: 原气泡是按文字长短缩的, 短问句缩成窄窄一条, 在里面
       改字得左右滚。76% 与 .msg.user 的 max-width 同值。 */
    .msg.user.editing {
      width: 76%;
    }

    .msg-edit-box {
      display: block;
      width: 100%;
      resize: none;
      padding: 8px 10px;
      border: 1px solid var(--brand);
      border-radius: 8px;
      background: var(--card);
      color: var(--navy);
      font-family: var(--font);
      font-size: 13.5px;
      line-height: 1.65;
      overflow-y: auto;
    }

    .msg-edit-box:focus-visible {
      box-shadow: var(--ring);
    }

    .msg-edit-acts {
      display: flex;
      justify-content: flex-end;
      gap: 8px;
      margin-top: 8px;
    }

    .msg-edit-acts button {
      flex-shrink: 0;
      padding: 5px 14px;
      border-radius: 8px;
      font-family: var(--font);
      font-size: 12.5px;
      font-weight: 600;
      cursor: pointer;
    }

    /* 主按钮与 .mdl-btn 同一套品牌蓝口径, 只是小一号 */
    .msg-edit-ok {
      border: 1px solid var(--brand);
      background: var(--brand);
      color: #fff;
    }

    .msg-edit-ok:hover {
      background: var(--brand-deep);
      border-color: var(--brand-deep);
    }

    /* 内容空着就点不动: 别让它看起来像点了没反应(同 .mdl-btn[disabled] 那条) */
    .msg-edit-ok[disabled] {
      border-color: var(--ghost);
      background: var(--ghost);
      cursor: not-allowed;
    }

    .msg-edit-cancel {
      border: 1px solid var(--line);
      background: var(--card);
      color: var(--muted);
    }

    .msg-edit-cancel:hover {
      color: var(--brand);
      border-color: var(--brand);
    }

    .msg.agent {
      position: relative;
      align-self: stretch;
      background: var(--card);
      border: 1px solid var(--line-soft);
      border-radius: 12px;
      padding: 14px 16px;
      box-shadow: 0 1px 2px rgba(3, 30, 95, .04), 0 2px 10px rgba(3, 30, 95, .05);
    }

    .agent-head {
      display: flex;
      align-items: center;
      gap: 9px;
      margin-bottom: 10px;
      flex-wrap: nowrap;
      /* 右侧留位给右上角悬浮的"执行过程"按钮 */
      padding-right: 96px;
    }

    .agent-head .name {
      font-weight: 700;
      font-size: 13.5px;
      color: var(--ink);
    }

    .agent-head .status {
      font-size: 12px;
      color: var(--muted);
      font-weight: 500;
      /* 状态文字过长时省略, 保证"执行过程"始终靠右一行排布 */
      min-width: 0;
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
    }

    .agent-head .status.busy {
      color: var(--amber);
      font-weight: 600;
    }

    /* 折叠的旧回答占位卡: 点"展开"才原地渲染完整报告/工具卡(见 addFoldedRound) */
    .fold-expand {
      display: inline-block;
      margin-top: 4px;
      padding: 6px 12px;
      font-size: 12.5px;
      font-weight: 600;
      color: var(--brand);
      background: transparent;
      border: 1px dashed var(--line-soft);
      border-radius: 8px;
      cursor: pointer;
    }

    .fold-expand:hover {
      border-color: var(--brand);
      background: rgba(23, 138, 236, .06);
    }

    /* 运行态文字: 逐字波浪。
       waveChars() 早就把每个字拆成独立的 .ch 并写好了递增的 animation-delay
       (差 0.09s), 只是这条规则一直没写 —— 于是那些 span 全是静止的, 状态文字
       看着就是普通一句话, 只有右边的转环在说明"正在进行"。
       这里补上动画: 每个字在自己的相位上轻轻沉一下(变淡 + 上浮一点点), 相位差
       让这口气沿着"分析中"三个字依次走过去 —— 是"字在动", 不是整块闪。
       周期 1.3s 定得比"一圈"长: 8 个字("Agent 分析中")的相位铺开也只占半个
       周期, 波浪走完一遍还剩一段静止, 读起来才不闹。
       轻重就看下面这两个数, 被连压过两轮: .38 / 1.4px → .58 / 1px → 现在
       .72 / .6px, 一路往"几乎看不出、但余光能感觉到在动"收。要再轻就继续往
       1.0 和 0 靠; 但别把周期跟着拉长 —— 相位差是绝对的 0.09s, 周期一长,
       同一时刻落在坑里的字就变多, 波浪会糊成"整块一起呼吸"。 */
    @keyframes ch-wave {

      0%,
      36%,
      100% {
        opacity: 1;
        transform: translateY(0);
      }

      18% {
        opacity: .72;
        transform: translateY(-0.6px);
      }
    }

    /* inline-block 是 transform 的入场券: 非替换行内元素上的 transform 不生效。
       空格在 waveChars() 里没有被包 span, 所以词间距照旧, 只有字在动。 */
    .ch {
      display: inline-block;
      animation: ch-wave 1.3s ease-in-out infinite;
    }

    /* 省略号: 转环。文字自己已经会动了, 这里只管"…"那个位置。
       转环是空元素 —— 三点跳动的 old 版已删, 连同那三颗 <i> 一起清掉了。 */
    .ldots {
      display: inline-block;
      width: 11px;
      height: 11px;
      margin-left: 5px;
      vertical-align: -1px;
      border-radius: 50%;
      border: 1.5px solid rgba(122, 140, 163, .30);
      border-top-color: var(--brand);
      animation: spin .7s linear infinite;
    }

    /* 忙碌状态(琥珀文字)下转环顶弧同色 */
    .agent-head .status.busy .ldots {
      border-color: rgba(230, 162, 60, .30);
      border-top-color: var(--amber);
    }

    /* 执行过程 折叠按钮 */
    .proc-toggle {
      position: absolute;
      top: 10px;
      right: 10px;
      padding: 4px 12px;
      border-radius: 999px;
      border: 1px solid var(--line);
      background: var(--card);
      font-size: 11.5px;
      color: var(--muted);
      cursor: pointer;
      font-weight: 600;
      transition: color .15s ease, border-color .15s ease, background .15s ease;
    }

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

    .tl {
      display: flex;
      flex-direction: column;
      gap: 8px;
    }

    .tl.closed {
      display: none;
    }

    /* ── 工具调用卡片 ── */
    .tool-card {
      border: 1px solid var(--line);
      border-radius: 10px;
      overflow: hidden;
      background: var(--card-2);
    }

    .tool-head {
      display: flex;
      align-items: center;
      gap: 9px;
      padding: 8px 12px;
      font-size: 13px;
      background: var(--card-2);
      border-bottom: 1px solid var(--line-soft);
      /* 整条卡片头就是展开/收起的点击区(比底下那条 11.5px 的按钮好点得多);
         卡内文字仍可拖选复制 —— 见 JS 里 click 的选区判断 */
      cursor: pointer;
      transition: background .15s ease;
    }

    .tool-head:hover {
      background: var(--brand-soft);
    }

    /* 注: 结果正文(.tool-result)不挂点击 —— 里面是要拖选的文本, 点它不该收面板 */

    .tool-head .status {
      width: 11px;
      height: 11px;
      border-radius: 50%;
      flex-shrink: 0;
    }

    /* 运行中: 琥珀转环; 终态: 实心圆点 */
    .tool-head .status.running {
      border: 1.5px solid rgba(230, 162, 60, .30);
      border-top-color: var(--amber);
      background: transparent;
      border-radius: 50%;
      animation: spin .7s linear infinite;
    }

    .tool-head .status.completed {
      background: var(--green);
    }

    .tool-head .status.failed {
      background: var(--red);
    }

    .tool-head .name {
      font-weight: 700;
      font-family: var(--mono);
      font-size: 12px;
      color: var(--navy);
      /* 卡头是一行(状态点 | 方法名 | 已筛选 | 徽章 | 取图按钮), 方法名是里面唯一可
         伸缩的那一项。不给它 nowrap 的话, 窄栏里它先折成两行 —— 「已筛选」胶囊一上
         卡头就更容易触发(实测无头 Chrome: ≥400px 卡头 39px, 360px 与 340px 折成两行
         变 45px)。折行与省略号二选一, 这里选省略号: 卡头高度恒为一行, 名字读不全时
         右边那截进省略号, 完整名字在展开的结果正文里(方法返回的首行就是它)。
         min-width:0 是必须的 —— flex 项默认 min-width:auto, 不置 0 就压不到内容宽度
         以下, 省略号永远不出现。 */
      min-width: 0;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }

    /* 「已筛选」标记: 这次分析只用了筛出的样本(条件挂在 data-tip 上, 悬停由全站
       那枚 #tip 弹 —— 见 index.html 的 filtChipOf)。克制的中性配色是有意的: 它是
       "本次调用的口径说明", 不是一个动作, 别和旁边那颗可点的 .viz-btn 长成一个样。 */
    .tool-head .filt-chip {
      flex-shrink: 0;
      font-size: 10.5px;
      font-weight: 600;
      padding: 2px 8px;
      border-radius: 999px;
      background: var(--bg-tint);
      border: 1px solid var(--line);
      color: var(--muted);
    }

    .tool-head .badge {
      /* 卡片头不再显示入参(见 index.html addToolCard 的注释), 原先由 .args 的
         flex:1 把徽章(及它右边的取图按钮)推到行尾, 现在由这里接管 */
      margin-left: auto;
      font-size: 10.5px;
      padding: 2px 9px;
      border-radius: 999px;
      font-weight: 600;
      flex-shrink: 0;
    }

    .badge.completed {
      background: var(--green-soft);
      color: var(--green-deep);
    }

    .badge.failed {
      background: var(--red-soft);
      color: var(--red);
    }

    .badge.running {
      background: var(--amber-soft);
      color: var(--amber);
    }

    .tool-result {
      padding: 9px 12px;
      font-size: 12px;
      color: var(--ink-2);
      white-space: pre-wrap;
      word-break: break-word;
      max-height: 200px;
      overflow: auto;
      font-family: var(--mono);
      line-height: 1.6;
    }

    .tool-result.closed {
      display: none;
    }

    /* 工具卡的结果面板有两种口味, 由 JS 按"这条返回是不是 markdown"二选一
       (见 index.html 的 renderToolMd):
         · 默认(上面 .tool-result 本体): 等宽 + pre-wrap 的纯文本, 给变量清单、
           报错这类本就按行铺的输出 —— 它对齐靠空格, 渲染反而会散;
         · 下面 .md-rendered: 分析方法的返回是 markdown(### 标题、**加粗**、
           管道符表格), 原样铺出来满屏是 "|" 和 "####", 读不成句。
       排版规则这一段**一条都不重抄**: 正文挂 .report-body 类, 直接用报告那一套
       (见下面「报告正文」段)。报告里怎么长, 工具卡里就怎么长 —— 同一张表在
       两处不会长成两个样, 也不会因为改了一处而漂移。
       所以这里只管面板自己的框: 换回正文口味(等宽/pre-wrap 都撤掉)、放宽高度
       (200px 装不下带标题的正文)。 */
    .tool-result.md-rendered {
      white-space: normal;
      font-family: inherit;
      padding: 10px 12px 12px;
      max-height: 420px;
    }

    .tool-result-toggle {
      display: block;
      width: 100%;
      text-align: left;
      padding: 5px 12px;
      font-size: 11.5px;
      color: var(--brand);
      cursor: pointer;
      background: var(--card-2);
      border: none;
      border-top: 1px solid var(--line-soft);
      transition: background .15s ease;
    }

    .tool-result-toggle:hover {
      background: var(--brand-soft);
    }

    /* ── 图表旁路(viz): 右侧绘图层(列表/下拉 + 大图 + 工具条) ──
       两种宿主, 类名区分, 见 JS 的 applyVizMode:
         分栏(--col, 默认) —— .shell 的第三栏, 出图时挤压左栏与对话列
         抽屉(--float)    —— 右缘浮层, 谁也不挤压 */
    :root {
      /* 绘图栏宽: 分栏与抽屉共用这一个变量, 生效值由 JS 写进 <html>(两种形态各记各的
         偏好, 再按窗口夹一道, 见 initViz); 这里只是 JS 跑起来之前的缺省, 比例跟 JS 的
         VIZ_W_DEF_RATIO 是同一个, 改一个记得改另一个 */
      --viz-w: 45vw;
    }

    /* 工具卡 .tool-head 里的常驻取图按钮(点开抽屉; 图本体不在卡片内) */
    .tool-head .viz-btn {
      flex-shrink: 0;
      border: 1px solid var(--line);
      background: var(--card);
      color: var(--brand);
      font-size: 11px;
      font-weight: 600;
      padding: 2px 9px;
      border-radius: 999px;
      cursor: pointer;
      transition: color .15s ease, border-color .15s ease, background .15s ease;
    }

    .tool-head .viz-btn:hover,
    .tool-head .viz-btn:active {
      border-color: var(--brand);
      background: var(--brand-soft);
    }

    /* 面板本体: 两个宿主共用的那部分 */
    .viz-panel {
      display: flex;
      flex-direction: column;
      min-height: 0;
      background: var(--card);
    }

    /* 开页瞬间(脚本还没跑)面板尚未挂上 --col/--float 宿主类, 它还留在 body 流里、
       白占下半屏(里面那句「图表区空闲」也会露出来)。没宿主类就整体藏掉 —— applyVizMode
       一进来就给它挂上其中一个类, 那之后照常显示, 不影响任何后续开合。 */
    .viz-panel:not(.viz-panel--col):not(.viz-panel--float) {
      display: none;
    }

    /* 宿主一 · 抽屉: 浮层, 从右缘滑出。正文完全不动, 想看/想滚都照旧,
       关闭走 Esc / ✕ / 点面板外(见 JS —— 这几条只在抽屉形态下挂)。
       宽度同样能拖(缺省 clamp(640px, 68vw, 960px), 由 JS 给): 它不挤别人, 所以能一路
       拖到只给窗口留一条缝。下面这道 max-width 是最后一道保险 —— JS 给的上限也是
       100vw - 40px, 同一个数, 窗口太窄时由它兜底。 */
    .viz-panel--float {
      position: fixed;
      top: 0;
      right: 0;
      bottom: 0;
      z-index: 50;
      width: var(--viz-w);
      max-width: calc(100vw - 40px);
      border-left: 1px solid var(--line);
      box-shadow: -6px 0 24px rgba(26, 54, 93, .10);
      transform: translateX(105%);
      transition: transform .18s ease;
      visibility: hidden;
    }

    .viz-panel--float.open {
      transform: none;
      visibility: visible;
    }

    /* 宿主二 · 分栏(默认): .shell 的第三栏, 出图时挤压左栏与对话列。
       这里原先只有抽屉一种形态, 理由是"自动弹图发生在分析中途, 用户正在读的正文会跟着
       跳一下; 而且抽屉宽 68vw, 让完位对话列也剩不下多少"。分栏改回默认时那条理由并没有
       消失, 是被三件事压住了: 栏宽从抽屉那道 68vw 改成 --viz-w(分栏缺省窗口的 45%, 可拖), 挤压小得多;
       开合走 .24s 过渡而不是瞬间跳; 且只在**本卡第一次出图**时展开(见 maybeViz 的
       _vizReady 去重), 同一张卡后续的图只换图、不动宽度。真嫌挤就点头部那颗
       「切到单图模式」, 选择记在本地。 */
    .viz-panel--col {
      position: relative;
      flex-shrink: 0;
      width: var(--viz-w);
      border-left: 1px solid var(--line);
      transition: margin-right .24s cubic-bezier(.4, 0, .2, 1), visibility .24s;
    }

    /* 收起: 整条滑出右缘(宽度不变 → 面板内部零重排, 出画的部分交给 .shell 的
       overflow-x 裁掉)。visibility 一起动 —— margin 让它离开画面, 这条让它连
       Tab 焦点一起出局; 过渡方向也对: 收起来时它一直可见到滑完, 展开时立刻可见。 */
    .viz-panel--col.closed {
      margin-right: calc(-1 * var(--viz-w));
      visibility: hidden;
    }

    /* 分栏窄, 放不下 215px 的竖列表: 多图切换改走头部那颗下拉(见 syncVizChrome) */
    .viz-panel--col .drawer-list {
      display: none;
    }

    .viz-panel--col .drawer-head {
      padding: 8px 10px;
    }

    /* 面板标题(图型 · 数据集)在分栏里是废话: 紧挨着的工具卡已经写着这次分析是什么、
       跑在哪份数据上。抽屉才需要它 —— 那块浮层盖住了正文, 得自带身份。 */
    .viz-panel--col .drawer-title {
      display: none;
    }

    .viz-panel--col .drawer-stage {
      padding: 8px 10px 10px;
    }

    .viz-panel--col .stage-host {
      margin-top: 6px;
    }

    /* 面板全屏: 应用内覆盖层(不用浏览器 Fullscreen API —— 头部工具栏要留着能按,
       也要和每张图自己的「⛶ 全屏」并存)。.shell 没有 transform/filter/perspective/
       contain, position:fixed 的包含块仍是视口; 以后别给 .shell 加这几样。
       z-index 55 低于预览 60 / 弹窗 62, 模态仍能盖在面板之上。 */
    .viz-panel.viz-panel--full {
      position: fixed;
      inset: 0;
      z-index: 55;
      width: auto;
      max-width: none;
      height: 100vh;
      height: 100dvh;
      margin: 0;
      border-left: none;
      box-shadow: none;
      transform: none;
      visibility: visible;
      transition: none;
    }

    .viz-panel--full .drawer-head {
      padding: 12px 14px;
    }

    /* 分栏里被藏掉的标题, 全屏有地方放 —— 铺满后更需要「这些图跑在哪份数据上」 */
    .viz-panel--full .drawer-title {
      display: block;
    }

    /* 宽度抓取带无处可拖 */
    .shell.viz-fs .viz-resize {
      display: none;
    }

    /* 分栏头部那颗多图下拉: 多图时它就是这一栏的标题, 所以比 .tb-sel 的色阶口径大一号 */
    .stage-sel {
      font-size: 12px;
      font-weight: 600;
      color: var(--navy);
      max-width: 200px;
    }

    /* ── 绘图栏宽度的抓取带(镜像 .nav-resize) ──
       钉在 .shell 上, 位置由 --viz-w 从右缘推出来 —— 分栏时落在第三栏的左缘, 抽屉时
       落在浮层的左缘(浮层也从右缘起算, 同一个式子)。命中区 7px, 视觉只有 3px 的握把;
       握把贴在这条带的**左**缘(左栏那条贴右缘, 方向相反)。 */
    .viz-resize {
      position: absolute;
      top: 0;
      bottom: 0;
      right: var(--viz-w);
      width: 7px;
      z-index: 22;
      cursor: col-resize;
      touch-action: none;
      -webkit-user-select: none;
      user-select: none;
      transition: right .24s cubic-bezier(.4, 0, .2, 1), opacity .18s ease;
    }

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

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

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

    /* 抽屉形态: 浮层 z-index 50 压在抓取带(22)上面 —— 不抬起来的话, 面板会把左缘那
       7px 的指针事件全吃掉, 这条边就成了摆设。51 就够(更上面还有预览 60 / 弹窗 62) */
    .shell:not(.viz-col) .viz-resize {
      z-index: 51;
    }

    /* 收起时面板整条滑走, 抓取带留在原地会指向空气(两种形态都看开合, 不看形态) */
    .shell:not(.viz-open) .viz-resize {
      opacity: 0;
      pointer-events: none;
    }

    /* 拖拽中: 关掉缓动 —— 每帧都过渡一次会让把手追不上指针; 光标全屏保持 col-resize */
    body.viz-resizing {
      cursor: col-resize;
      -webkit-user-select: none;
      user-select: none;
    }

    body.viz-resizing .viz-panel--col,
    body.viz-resizing .viz-resize {
      transition: none;
    }

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

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

    /* 头部三颗小胶囊: 「切到单图模式 / 切到多图模式」、「全屏/收回」与「✕ 关闭」。退到右侧靠
       .tb-spacer 撑开 —— 不用 margin-left:auto 是因为按钮各挂一个 auto 边距时, 剩余空间
       会被对半分, 彼此之间多出一道缝。 */
    .drawer-close,
    .viz-mode,
    .viz-full-btn {
      flex-shrink: 0;
      border: 1px solid var(--line);
      background: var(--card);
      color: var(--muted);
      font-size: 11px;
      font-weight: 600;
      padding: 2px 10px;
      border-radius: 999px;
      cursor: pointer;
      transition: color .15s ease, border-color .15s ease, background .15s ease;
    }

    .drawer-close:hover,
    .viz-mode:hover,
    .viz-full-btn:hover,
    .drawer-close:active,
    .viz-mode:active,
    .viz-full-btn:active {
      color: var(--brand);
      border-color: var(--brand);
      background: var(--brand-soft);
    }

    /* 全屏那颗只有图标: 胶囊口径不变, 内部用 line-height:0 收掉 inline 基线空隙,
       两颗 SVG 按 aria-pressed 二选一显示 */
    .viz-full-btn {
      padding: 3px 8px;
      line-height: 0;
    }

    .viz-full-btn svg {
      width: 14px;
      height: 14px;
      display: block;
    }

    .viz-full-btn[aria-pressed="false"] .viz-fs-exit,
    .viz-full-btn[aria-pressed="true"] .viz-fs-enter {
      display: none;
    }

    /* 窗口太窄、分栏进不来时: 按钮留着(让人知道有这个选项), 但按不动, 文案说原因 */
    .viz-mode[disabled] {
      opacity: .5;
      cursor: not-allowed;
    }

    .viz-mode[disabled]:hover,
    .viz-mode[disabled]:active {
      color: var(--muted);
      border-color: var(--line);
      background: var(--card);
    }

    .drawer-body {
      flex: 1;
      min-height: 0;
      display: flex;
    }

    .drawer-list {
      width: 215px;
      flex-shrink: 0;
      overflow-y: auto;
      border-right: 1px solid var(--line-soft);
      padding: 8px;
      scrollbar-width: thin;
      scrollbar-color: var(--line-strong) transparent;
    }

    .list-item {
      display: block;
      width: 100%;
      text-align: left;
      padding: 6px 10px;
      border-radius: 8px;
      border: none;
      background: none;
      font-size: 12px;
      color: var(--ink-2);
      cursor: pointer;
      line-height: 1.45;
    }

    .list-item:hover {
      background: var(--brand-soft);
    }

    .list-item.active {
      background: var(--brand-tint);
      color: var(--brand);
      font-weight: 700;
    }

    .drawer-stage {
      flex: 1;
      min-width: 0;
      display: flex;
      flex-direction: column;
      padding: 10px 14px 14px;
    }

    .stage-tool {
      flex-shrink: 0;
      display: flex;
      align-items: center;
      gap: 8px;
      flex-wrap: wrap;
    }

    .stage-t {
      font-size: 13px;
      font-weight: 700;
      color: var(--navy);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .tb-spacer {
      flex: 1;
    }

    .tb-chk {
      display: inline-flex;
      align-items: center;
      gap: 4px;
      font-size: 11px;
      color: var(--muted);
      cursor: pointer;
      -webkit-user-select: none;
      user-select: none;
    }

    .tb-sel {
      font-size: 11px;
      border: 1px solid var(--line);
      border-radius: 999px;
      padding: 2px 9px;
      color: var(--muted);
      background: var(--card);
      cursor: pointer;
      outline: none;
    }

    .tb-sel:focus {
      border-color: var(--brand);
    }

    .tb-color {
      width: 26px;
      height: 22px;
      padding: 0;
      border: 1px solid var(--line);
      border-radius: 4px;
      background: var(--card);
      cursor: pointer;
      vertical-align: middle;
    }

    /* 图表样式面板: 点「样式」弹出的分组浮层(色彩/标签设置/坐标轴/字体·刻度) */
    .viz-style-wrap {
      position: relative;
      flex-shrink: 0;
    }

    .viz-style-panel {
      position: fixed;
      width: 340px;
      max-width: calc(100vw - 16px);
      max-height: calc(100vh - 16px);
      overflow: auto;
      background: var(--card);
      border: 1px solid var(--line);
      border-radius: 12px;
      box-shadow: 0 12px 40px rgba(15, 23, 42, .18);
      padding: 14px;
      z-index: 200;
    }

    .vsp-h {
      font-size: 13px;
      font-weight: 600;
      color: var(--navy);
      margin: 0 0 9px;
    }

    .vsp-sec+.vsp-sec {
      margin-top: 13px;
      padding-top: 13px;
      border-top: 1px solid var(--line-soft);
    }

    .vsp-pal {
      display: flex;
      flex-direction: column;
      gap: 5px;
    }

    .vsp-pal .pal {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      padding: 5px 8px;
      border: 1px solid transparent;
      border-radius: 8px;
      cursor: pointer;
    }

    .vsp-pal .pal:hover {
      background: var(--brand-soft);
    }

    .vsp-pal .pal.active {
      border-color: var(--brand);
      background: var(--brand-tint);
    }

    .vsp-pal .pal-name {
      font-size: 12px;
      color: var(--muted);
      white-space: nowrap;
    }

    .vsp-pal .pal-swatch {
      display: inline-flex;
      align-items: center;
      gap: 3px;
    }

    .vsp-pal .pal-swatch i {
      width: 20px;
      height: 20px;
      box-sizing: border-box;
      border: 1px solid rgba(0, 0, 0, .12);
      border-radius: 3px;
      display: inline-block;
    }

    .vsp-pal .pal-swatch i.tex {
      background: repeating-linear-gradient(45deg, var(--ghost) 0 2px, var(--card) 2px 4px);
      border: 1px solid var(--line-soft);
    }

    .vsp-pal .pal-swatch input[type=color] {
      width: 20px;
      height: 20px;
      box-sizing: border-box;
      padding: 0;
      border: 1px solid rgba(0, 0, 0, .12);
      border-radius: 3px;
      cursor: pointer;
      background: var(--card);
    }

    .vsp-row {
      display: flex;
      flex-wrap: wrap;
      gap: 9px 13px;
      align-items: center;
    }

    .vsp-row label {
      display: inline-flex;
      align-items: center;
      gap: 5px;
      font-size: 12px;
      color: var(--muted);
    }

    .vsp-row select,
    .vsp-row input[type=color] {
      font-size: 12px;
      border: 1px solid var(--line);
      border-radius: 6px;
      padding: 2px 7px;
      background: var(--card);
      color: var(--muted);
    }

    .vsp-row input[type=color] {
      width: 30px;
      height: 24px;
      padding: 0;
    }

    .vsp-row input[type=checkbox] {
      cursor: pointer;
    }

    /* ── 样式面板 · 底部抽屉形态(手机/平板竖屏, JS 在 ≤900px 时加 .viz-style-panel--sheet) ──
       桌面仍是上面的 340px 浮层; 窄屏改成从下缘滑出的全宽抽屉, 拇指好点、点遮罩收起。
       这里用类名当开关(不写进 @media): 类由 JS 按窗口宽度决定, 窗口转来转去时抽屉样式
       跟着类走, 不会出现"类还在、媒体查询却关了"的错位。 */
    .viz-style-panel--sheet {
      left: 0;
      right: 0;
      bottom: 0;
      top: auto;
      width: 100%;
      max-width: 100%;
      max-height: 72vh;
      border: none;
      border-radius: 16px 16px 0 0;
      box-shadow: 0 -8px 32px rgba(3, 30, 95, .20);
      padding: 6px 16px calc(16px + env(safe-area-inset-bottom, 0px));
    }

    /* 抽屉顶部的把手(纯装饰: 提示这是可点遮罩收起的抽屉) */
    .viz-style-panel--sheet::before {
      content: "";
      display: block;
      width: 36px;
      height: 4px;
      margin: 2px auto 12px;
      border-radius: 2px;
      background: var(--line);
    }

    /* 抽屉形态的遮罩: 铺满视口、压在面板之下(z 199 < 面板 200), 点它收起 */
    .viz-style-backdrop {
      position: fixed;
      inset: 0;
      z-index: 199;
      background: rgba(3, 30, 95, .30);
    }

    .stage-host {
      flex: 1;
      min-height: 260px;
      margin-top: 8px;
      border: 1px solid var(--line);
      border-radius: 8px;
      background: var(--card);
      position: relative;
      /* 拖宽度/开合过渡的那两百多毫秒里, plotly 的 SVG 还是旧尺寸: 先裁掉溢出的那截,
         等拖完/过渡走完再 resize(见 vizResize) —— 好过让它把面板顶开。 */
      overflow: hidden;
    }

    /* 这一批里没出成图的变量(逐变量出图: 少的那几张为什么少, 见 explore.
       _freq_visualize 的 skipped) —— 不是错误、其余图照出, 所以走琥珀不走红;
       压在图上方一行, 单图/全部两种模式下都看得见 */
    .stage-skip {
      flex-shrink: 0;
      margin-top: 8px;
      padding: 6px 9px;
      border: 1px solid var(--amber-border);
      border-radius: 8px;
      background: var(--amber-soft);
      color: var(--amber-deep);
      font-size: 11px;
      line-height: 1.5;
    }

    .stage-note {
      flex-shrink: 0;
      margin-top: 6px;
      font-size: 11px;
      color: var(--muted);
      line-height: 1.5;
    }

    /* 面板里的胶囊按钮(「本轮全图」「批量下载」, 以及每张图自己那条工具条上的
       复制/下载/全屏等): 与 .tool-head .viz-btn 同款 */
    .viz-btn {
      flex-shrink: 0;
      border: 1px solid var(--line);
      background: var(--card);
      color: var(--brand);
      font-size: 11px;
      font-weight: 600;
      padding: 2px 9px;
      border-radius: 999px;
      cursor: pointer;
      transition: color .15s ease, border-color .15s ease, background .15s ease;
    }

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

    /* 选中态(「本轮全图」切到「仅此方法」, 以及「全屏」按着的那颗): 实底蓝 + 强字,
       与 .send-btn.on 同一个 "on" 语汇。深色下 --brand-soft 和卡片底分不开, 必须用
       --brand-tint 实底 + --ink-strong 亮字才看得出哪颗被按着; 全屏那颗的图标靠
       aria-pressed 换 enter/exit(见下), 这里再补一层底让按着的那颗显形。 */
    .viz-btn[aria-pressed="true"],
    .viz-full-btn[aria-pressed="true"] {
      border-color: var(--brand);
      background: var(--brand-tint);
      color: var(--ink-strong);
    }

    /* 取图按钮前那颗图标: 14px 内联 SVG(常量见 index.html 的 ICON_CHART), 与 11px 文字同行
       时按 -0.21em 下沉(图标比文字盒高出的那 2px 上下各分一半, 视觉居中), 右边 4px 顶替掉
       原来 emoji 后面那个空格。
       14px 而不是跟着字号走的 12px: 12px 下里面那条折线只剩 1px 宽, 抗锯齿一糊就发灰
       (拿无头 Chrome 按 1x 实拍比过)。
       选 svg 而不是 .viz-ic 本身 —— .viz-ic 还挂在「⬇ PNG」那颗按钮的文字 span 上,
       给它定死尺寸会把 ⬇ 压变形。 */
    .viz-btn .viz-ic svg {
      width: 14px;
      height: 14px;
      vertical-align: -0.21em;
      margin-right: 4px;
    }

    /* 手机上把胶囊按钮里的图标(取图那颗 SVG / ⬇)藏起来: 窄屏下图标占宽, 后面的文字(图名/
       "下载 PNG")容易被挤到一行之外或撑不下, 只留文字更利落。桌面照旧保留图标当提示。 */
    @media (max-width: 900px) {
      .viz-btn .viz-ic {
        display: none;
      }
    }

    /* 「全部」子模式: 宿主变成上下滚动的列表, 每张图一个块, 之间虚线分隔 */
    .stage-host.stacking {
      overflow-y: auto;
      padding: 10px 12px;
    }

    .viz-stack {
      display: flex;
      flex-direction: column;
      gap: 12px;
    }

    .stack-item {
      border-bottom: 1px dashed var(--line);
      padding-bottom: 14px;
    }

    .stack-item:last-child {
      border-bottom: none;
      padding-bottom: 0;
    }

    .stack-head {
      display: flex;
      align-items: center;
      gap: 8px;
      flex-wrap: wrap;
      margin-bottom: 6px;
    }

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

    .stack-tools {
      display: flex;
      align-items: center;
      gap: 8px;
      flex-wrap: wrap;
    }

    .stack-plot {
      height: auto;
      min-height: 120px;
      border: 1px solid var(--line-soft);
      border-radius: 8px;
      background: var(--card);
    }

    .stack-note {
      margin-top: 6px;
      font-size: 11px;
      color: var(--muted);
      line-height: 1.5;
    }

    .stage-err {
      position: absolute;
      inset: 8px;
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      justify-content: center;
      gap: 10px;
      color: var(--red);
      background: var(--red-soft);
      border: 1px solid var(--red-border);
      border-radius: 8px;
      padding: 8px 10px;
      font-size: 12px;
      line-height: 1.5;
    }

    .viz-loading {
      color: var(--faint);
      font-size: 12px;
      padding: 10px 2px;
    }

    .plot-err {
      color: var(--red);
      background: var(--red-soft);
      border: 1px solid var(--red-border);
      border-radius: 8px;
      padding: 8px 10px;
      font-size: 12px;
      line-height: 1.5;
    }

    /* Agent 的过程注记(数据文件/开始分析/已请求停止/日志) — 沿用 usage-line 类, 落在过程区。
       字号全站一个口径: 12px —— 与报告区顶上的用量条、报告里的「已停止生成」同号
       (那两处原先各自是 11 / 12.5px, 同一种"顺带说一句"的字没道理三个尺寸)。 */
    .tl .usage-line {
      text-align: left;
      font-size: 12px;
      color: var(--faint);
      padding: 2px 8px;
      line-height: 1.6;
    }

    .error-banner {
      background: var(--red-soft);
      color: var(--red);
      border: 1px solid var(--red-border);
      border-radius: 10px;
      padding: 9px 12px;
      font-size: 12.5px;
      line-height: 1.6;
      font-weight: 500;
    }

    /* ── 分析报告 ── */
    .report-wrap {
      margin-top: 12px;
      border-top: 1px dashed var(--brand-tint);
      padding-top: 12px;
      line-height: 1.75;
    }

    .report-head {
      display: flex;
      align-items: center;
      gap: 8px;
      margin-bottom: 8px;
    }

    .report-head .bar {
      width: 4px;
      height: 16px;
      border-radius: 2px;
      background: var(--brand);
    }

    .report-head .t {
      font-weight: 700;
      font-size: 13.5px;
      color: var(--navy);
      letter-spacing: .3px;
    }

    .report-actions {
      margin-left: auto;
      display: flex;
      gap: 8px;
    }

    .report-actions button {
      padding: 3px 12px;
      border-radius: 7px;
      border: 1px solid var(--line);
      background: var(--card);
      font-size: 11.5px;
      cursor: pointer;
      color: var(--muted);
      font-weight: 600;
      transition: color .15s ease, border-color .15s ease, background .15s ease;
    }

    .report-actions button:hover {
      border-color: var(--brand);
      color: var(--brand);
      background: var(--brand-soft);
    }

    /* "复制"那颗定住宽度: 回执是就地换字("复制"→"已复制", 见 JS 的 copyReport),
       不钉住的话按钮会变宽, 右边"Word报告"跟着跳一下 */
    .report-actions .report-copy {
      min-width: 64px;
    }

    /* "Word报告"同样定宽: 转换期间就地换字("Word报告"→"转换中…", 见
       downloadReportDocx), 不钉住的话旁边按钮会跟着抖。取两者较宽一侧钉住 ——
       无头 Chrome 实测(字号/内边距/字体栈都照抄本文件): "Word报告" 78.17px、
       "转换中…" 72.00px, 所以定 79。
       上界取"较宽的哪个"而不是随手给个整数: 小于 78.17 就是钉不住, 转换一开始按钮先缩
       2px、旁边那颗跟着挪 —— 正是这条要防的事。
       换文案或改字号/内边距时这几个数会变, 得重测。 */
    .report-actions .report-docx {
      position: relative;
      min-width: 79px;
    }

    /* 右上角的「三线表」小标签: 按钮文案仍是用户熟悉的「Word报告」, 三个字挂在
       右上方, 一眼说明导出的是三线表排版。绝对定位, 不占按钮宽度, 所以转换期间
       换字时它原地不动, 也不会把旁边按钮挤开。
       向上探出 7px、向右探出 5px: 像盖在角上的小戳, 一半压着按钮边框才不散;
       外面那圈 2px 卡片底色当描边, 压在按钮的边框线上也看得清。
       pointer-events:none —— 鼠标从标签上划过时事件仍落在按钮上, 悬停态与
       按钮自身的提示气泡都不受影响。 */
    .report-actions .report-docx .docx-badge {
      position: absolute;
      top: -7px;
      right: -5px;
      padding: 1px 5px;
      border-radius: 999px;
      background: var(--brand);
      color: #fff;
      font-size: 9px;
      line-height: 1.45;
      font-weight: 700;
      letter-spacing: .2px;
      white-space: nowrap;
      pointer-events: none;
      box-shadow: 0 0 0 2px var(--card), 0 1px 3px rgba(3, 30, 95, .18);
    }

    /* 复制后的回执: 停 1.2s 再恢复。绿色与表格/账号那两处同一套(见 .tbl-copy.done) */
    .report-actions button.done {
      border-color: var(--green-border);
      color: var(--green-deep);
    }

    /* 报告末尾再挂一条同样的动作条: 报告一长(表格多的时候要滚好几屏), 读到最后手边也得
       有一颗。按钮长相/回执/定宽整条复用 .report-actions, 这里只补两件事 —— 拉开距离,
       再用一条虚线跟正文分家(与 .report-wrap 顶上那条同一画法, 前后呼应)。
       头部那条靠 margin-left:auto 顶到右边(它是 .report-head 这个 flex 容器里的 flex 项),
       这条自己就是容器, margin-left:auto 落成 0, 改由 justify-content 把按钮顶过去。 */
    .report-foot {
      margin-top: 14px;
      padding-top: 12px;
      border-top: 1px dashed var(--brand-tint);
      justify-content: flex-end;
    }

    /* 报告占位(分析中/已停止生成/本轮未生成报告) —— 与过程注记同一口径 12px */
    .report-empty {
      color: var(--muted);
      font-size: 12px;
      padding: 6px 0 10px;
    }

    /* 本轮失败: 报告占位从波浪"分析中"变为静态红字错误 */
    .report-empty.failed {
      color: var(--red);
      font-weight: 600;
    }

    /* 失败占位里的"重新提问": 跟着红字走一行, 尺寸向 .report-actions 的次级按钮
       对齐(11.5px/圆角 7), 但不吃那边的红字字重 —— 按钮自己写 500。 */
    .report-empty .retry-btn {
      margin-left: 8px;
      padding: 2px 11px;
      border-radius: 7px;
      border: 1px solid var(--line);
      background: #fff;
      font-size: 11.5px;
      font-weight: 500;
      font-family: inherit;
      color: var(--muted);
      cursor: pointer;
      transition: color .15s ease, border-color .15s ease, background .15s ease;
    }

    .report-empty .retry-btn:hover {
      border-color: var(--brand);
      color: var(--brand);
      background: var(--brand-soft);
    }

    .report-empty .retry-btn:disabled {
      opacity: .55;
      cursor: default;
    }

    /* 本轮用量汇总(报告区顶部细条) —— 与过程注记同一口径 12px(见 .tl .usage-line) */
    .report-wrap>.usage-line {
      text-align: left;
      font-size: 12px;
      color: var(--faint);
      padding: 6px 4px 10px;
      letter-spacing: .2px;
    }

    .report-body {
      font-size: 13px;
      /* 长串内容(英文/数字/路径)允许任意断点换行, 防止撑破卡片横向溢出 */
      overflow-wrap: anywhere;
    }

    .report-body a {
      color: var(--brand);
      text-decoration: none;
    }

    /* 报告里的图(目前是 data 文件预览/雷达等小图, HTML 里常带死宽度)一律按列宽缩。
       overflow-wrap 管不住图片 —— 那是**没有断点的替换元素**, 800px 的 width 属性在
       360px 的手机上就是实打实多出 400 多像素, 一路把 .chat 顶成可左右拖。
       实测: <img width="800"> 在 360/390/412 三档分别溢出 +487/+457/+435。
       height:auto 是配套的: 只压 width 不改 height 会把图压扁。 */
    .report-body img {
      max-width: 100%;
      height: auto;
    }

    /* ── 公式(KaTeX, 见 JS 的 renderMd) ──
       行内公式跟着正文走就行(KaTeX 自己按 1.21em 随字号缩), 要管的只有两处:
       行内不断行 —— KaTeX 自家的排版是一整个 nowrap 的盒子, 允许它从中间断开的话,
       "x_i + y_i" 会被折成两个式子读;
       独立成行的公式 KaTeX 给的是 white-space:nowrap, 宽一点的(矩阵、多行推导)在手机上
       会顶破卡片, 所以外面这层 .math-block 让它自己横向滚 —— 与表格的 .tbl-scroll 同一套办法。 */
    .report-body .math-inline {
      white-space: nowrap;
    }

    .report-body .math-block {
      overflow-x: auto;
      overflow-y: hidden;
      padding: 2px 0;
    }

    /* 行内公式也有放不下的时候(上面那条 nowrap 只保证它不从中间断开, 不保证它放得下)——
       宽过一整行的公式既不能折行、也没有自己的滚动盒, 就把 .chat 顶出一条横向滚动
       (.chat 是 overflow-y: auto, 按规范另一轴也成 auto), 手机上表现为"整块对话能左右拖"。
       所以给放不下的那几条补上和 .math-block 同一套壳: 独立成行 + 自己横向滚。
       谁算"放不下"由 JS 判(见 renderMd 后的 fitInlineMath), 这里只管长相:
       只动被判定放不下的那几条 —— 其余行内公式仍随之写一行一个字不多。 */
    .report-body .math-inline.math-wide {
      display: block;
      overflow-x: auto;
      overflow-y: hidden;
      padding: 2px 0;
    }

    /* MD 报告渲染全部统一 13px: 标题 h1~h6 也固定 13px(覆盖浏览器默认字号,
       层级只靠字重/分隔线区分, 不再逐级放大) */
    .report-body h1,
    .report-body h2,
    .report-body h3,
    .report-body h4,
    .report-body h5,
    .report-body h6 {
      font-size: 13px;
    }

    .report-body h2 {
      margin: 12px 0 8px;
      color: var(--ink);
    }

    .report-body h3 {
      margin: 16px 0 6px;
      padding-bottom: 5px;
      border-bottom: 1px solid var(--line-soft);
      color: var(--navy);
    }

    .report-body h4 {
      margin: 12px 0 6px;
      color: var(--ink);
    }

    .report-body p {
      margin: 6px 0;
      color: var(--ink-2);
    }

    /* 过宽的表格左右滚: 壳在 showReport 里后加(marked 直接吐 <table>, 只能在渲染后包)。
       为什么不让 table 自己滚: 直接给它 overflow-x 得先 display:block, 那会把画在 table
       盒上的圆角/描边拆掉; 而这两个诉求也本来就不该落在同一个盒子上 —— 表格要 width:100%
       铺满卡片, 壳只管"装不下就滚"。margin 也一并挪到壳上, 免得滚动条挤在两段留白中间。 */
    .tbl-scroll {
      margin: 10px 0;
      overflow-x: auto;
    }

    /* 表格再外面套一层, 只用来钉两颗"复制"按钮(右上/右下各一颗, 见 JS 的表格复制段)。
       按钮不能直接挂进 .tbl-scroll: 那是滚动容器, 绝对定位的子元素会跟着表格
       一起横向滚走, 表格一宽按钮就没了。这层不滚动, 按钮才待得住。 */
    .tbl-wrap {
      position: relative;
    }

    .tbl-copy {
      position: absolute;
      top: 4px;
      right: 4px;
      z-index: 1;
      padding: 2px 8px;
      border: 1px solid var(--line);
      border-radius: 6px;
      background: var(--overlay);
      font: inherit;
      font-size: 11px;
      line-height: 1.6;
      color: var(--muted);
      cursor: pointer;
      /* 平时不显形: 它压在表头右上角, 常驻会挡住一个数据格。悬停表格(或键盘聚焦)才浮出
         —— 与账号名称上那颗复制图标同一套做法(见 .acct-copy)。 */
      opacity: 0;
      transition: opacity .15s ease, color .15s ease, border-color .15s ease;
    }

    /* 右下那颗: 长相与行为都跟右上那颗一模一样, 只把定位翻到底边。表一长, 右上那颗
       就跟着表头滚出视野了, 读到最后一行时手边也得有一颗。 */
    .tbl-copy--bottom {
      top: auto;
      bottom: 4px;
    }

    .tbl-wrap:hover .tbl-copy,
    .tbl-copy:focus-visible {
      opacity: 1;
    }

    .tbl-copy:hover {
      border-color: var(--brand);
      color: var(--brand);
    }

    /* 复制后的回执: 停 1.2s 再恢复(见 JS)。绿色与账号那处的"已复制"同一个色 */
    .tbl-copy.done {
      opacity: 1;
      border-color: var(--green-border);
      color: var(--green-deep);
    }

    .report-body table {
      border-collapse: collapse;
      width: 100%;
      margin: 0;
      box-shadow: 0 0 0 1px var(--line) inset;
      border-radius: 8px;
      overflow: hidden;
    }

    .report-body th,
    .report-body td {
      border: 1px solid var(--line);
      padding: 7px 10px;
      /* 报告表格一律居中(表头同理): 这类表多是"指标名 + 几个数", 左对齐时
         数字位数不齐反而难比大小 —— 与 SPSSAU 三线表的读法一致 */
      text-align: center;
      /* 不折行: 数字被折成两行("0.32" 换个行就是 "3**", 显著性星号还会掉到下一行)
         比出滚动条难读得多。代价是表格可能比卡片宽 —— 由 .tbl-scroll 兜底。 */
      white-space: nowrap;
    }

    .report-body th {
      background: var(--card-2);
      color: var(--navy);
      font-weight: 700;
      white-space: nowrap;
    }

    .report-body tr:nth-child(even) td {
      background: var(--card-2);
    }

    .report-body ul,
    .report-body ol {
      margin: 6px 0 6px 20px;
    }

    .report-body li {
      margin: 3px 0;
    }

    .report-body code {
      background: var(--card-3);
      border-radius: 4px;
      padding: 1px 5px;
      font-family: var(--mono);
      color: var(--navy-ink);
    }

    .report-body pre {
      background: var(--card-2);
      border: 1px solid var(--line-soft);
      border-radius: 8px;
      padding: 10px 12px;
      white-space: pre-wrap;
      /* 代码块长行折行, 避免撑破卡片 */
      overflow-wrap: anywhere;
    }

    .report-body pre code {
      background: none;
      padding: 0;
      color: inherit;
    }

    .report-body blockquote {
      border-left: 3px solid var(--brand);
      padding-left: 12px;
      color: var(--muted);
      margin: 8px 0;
      background: var(--bg-tint);
      border-radius: 0 6px 6px 0;
      padding-top: 2px;
      padding-bottom: 2px;
    }

    .report-body hr {
      border: none;
      border-top: 1px solid var(--line);
      margin: 14px 0;
    }

    .report-body strong {
      color: var(--ink);
    }

    /* ── 底部输入区 ── */
    .input-bar {
      flex-shrink: 0;
      /* 与 .chat 同宽同右缘, 输入框右缘仍然和消息卡片右缘对齐(同一道 --rail-gap) */
      padding: 6px var(--rail-gap) 16px 0;
    }

    .input-box {
      display: flex;
      gap: 10px;
      align-items: flex-end;
      background: var(--card);
      border: 1px solid var(--line-strong);
      border-radius: 12px;
      /* 左内侧改由「+」那颗按钮自己撑(与右侧发送键的 10 对称), 所以左内边距收到 10 */
      padding: 10px;
      box-shadow: 0 1px 2px rgba(3, 30, 95, .04), 0 2px 10px rgba(3, 30, 95, .05);
      transition: border-color .15s ease, box-shadow .15s ease;
    }

    .input-box:focus-within {
      border-color: var(--brand);
      box-shadow: 0 0 0 3px rgba(23, 138, 236, .12);
    }

    /* 拖拽投放态: 文件拖到输入框上方时亮起来(见 JS 的 document dragover) —— 没有反馈
       就不知道能不能往这儿放。光晕与边框口径同上, 换虚线+浅底把"可以松手"和"正在输入"
       区分开(虚线也是投放区的通用记号)。两条同优先级(.input-box.x 与 .input-box:x),
       顺序在 :focus-within 之后 —— 边打字边拖时以拖拽态为准。 */
    .input-box.drag-over {
      border-style: dashed;
      border-color: var(--brand);
      background: var(--brand-soft);
      box-shadow: 0 0 0 3px rgba(23, 138, 236, .12);
    }

    /* ── 输入框里的「+」: 圆形浅底 + 深色加号 ──
       高度与发送键同档, 底边靠 .input-box 的 align-items:flex-end 自动对齐。 */
    .plus-wrap {
      position: relative;
      flex-shrink: 0;
    }

    .plus-btn {
      width: 34px;
      height: 34px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      padding: 0;
      border: none;
      border-radius: 50%;
      background: var(--card-3);
      color: var(--navy);
      cursor: pointer;
      transition: background .15s ease, color .15s ease;
    }

    .plus-btn svg {
      width: 18px;
      height: 18px;
    }

    .plus-btn:hover,
    .plus-btn.on,
    .plus-btn:active {
      background: var(--brand-tint);
      color: var(--brand-deep);
    }

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

    /* 菜单向上弹: 输入框贴着视口底, 往下弹会出屏。
       z-index 40 与账号面板同档 —— 这里**不可能**跟图表抽屉(50)打架: .container 带
       z-index:1, 自成层叠上下文, 菜单关在里面, 子元素 z-index 再高也压不过外面的抽屉。
       所以改成让两者永不同框: 开菜单就收抽屉(plusBtn), 开抽屉就收菜单(showViz)。
       分栏形态下图表面板是 .shell 里另一栏, 不在 .container 的层叠上下文里, 两者
       谁也没挡着谁 —— 那两处互收的逻辑只对抽屉形态生效(见 JS 里的 vizCol 判断)。 */
    .plus-pop {
      position: absolute;
      bottom: calc(100% + 10px);
      left: 0;
      z-index: 40;
      min-width: 214px;
      display: flex;
      flex-direction: column;
      gap: 2px;
      padding: 6px;
      background: var(--card);
      border: 1px solid var(--line);
      border-radius: 10px;
      box-shadow: 0 10px 28px rgba(3, 30, 95, .14);
    }

    /* 必须显式写: 上面的 display:flex 会盖掉 [hidden] 自带的 display:none */
    .plus-pop[hidden] {
      display: none;
    }

    .plus-item {
      display: flex;
      align-items: center;
      gap: 9px;
      width: 100%;
      padding: 8px 10px;
      border: none;
      border-radius: 8px;
      background: transparent;
      color: var(--ink-2);
      font: inherit;
      font-size: 13px;
      text-align: left;
      cursor: pointer;
      /* 不许折行: 右侧那条 plus-sub 一长("Excel / CSV / SPSS / Stata / SAS"), 左边标题就被
         flex 挤成"一个字一行"的竖排。nowrap 后 flex 的 min-content 就是整行宽度,
         挤不动它, 面板宽度则由 min-width 让路自己涨。 */
      white-space: nowrap;
      transition: background .15s ease, color .15s ease;
    }

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

    .plus-item svg {
      width: 16px;
      height: 16px;
      flex-shrink: 0;
      color: var(--brand);
    }

    .plus-t {
      flex: 1;
    }

    .plus-sub {
      font-size: 11px;
      color: var(--faint);
    }

    textarea {
      flex: 1;
      border: none;
      outline: none;
      resize: none;
      font-size: 13.5px;
      line-height: 1.6;
      font-family: inherit;
      background: transparent;
      max-height: 130px;
      min-height: 22px;
      padding: 4px 0;
      color: var(--ink);
      caret-color: var(--brand);
      align-self: center
    }

    textarea::placeholder {
      color: var(--faint);
    }

    /* ── 发送键: 圆形图标键, 三态 ──
       空输入 = 灰底灰箭头(disabled, 不可点) / 有输入 = 浅蓝底黑箭头(可发送) /
       运行中 = 浅蓝底黑方块(点它停止本轮)。尺寸与左侧「+」同档(34),
       底边靠 .input-box 的 align-items:flex-end 自动对齐。 */
    .send-btn {
      width: 34px;
      height: 34px;
      padding: 0;
      border: none;
      border-radius: 50%;
      flex-shrink: 0;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      background: var(--card-3);
      color: var(--ghost);
      cursor: default;
      transition: background .15s ease, color .15s ease, transform .1s ease;
    }

    .send-btn.on,
    .send-btn.stop {
      background: var(--brand-tint);
      color: var(--ink-strong);
    }

    .send-btn.on {
      cursor: pointer;
    }

    .send-btn.on:hover,
    .send-btn.stop:hover:not(:disabled) {
      background: var(--brand-tint);
    }

    .send-btn.on:active {
      transform: translateY(1px);
    }

    .send-btn:disabled {
      cursor: not-allowed;
    }

    .send-btn svg {
      width: 18px;
      height: 18px;
    }

    /* 图标二选一: 待发是箭头, 运行中换方块(停止) */
    .send-btn .ic-stop {
      display: none;
    }

    .send-btn.stop .ic-arrow {
      display: none;
    }

    .send-btn.stop .ic-stop {
      display: block;
    }

    @keyframes spin {
      to {
        transform: rotate(360deg);
      }
    }

    /* ── Agent 提问卡片(question 工具 → 用户交互确认) ──
       排面用中性色: 底取页面那层浅蓝(--bg-tint)、描边 --line, 整张卡自己不占颜色。
       原来是琥珀底 + 琥珀描边, 提交后又整张转绿 —— 同一张卡在"刚渲染 / 选中 / 已回答"
       三种状态下换三种颜色, 而"这里要你确认一下"并不需要靠颜色说。安静下来之后,
       视线落点是全卡唯一的一处强调色: 选中的选项与那颗提交键。
       与对话里别处"蓝=当前/可动"是同一个语汇(见 .qa-item.on / .msg-copy:hover)。 */
    .q-card {
      border: 1px solid var(--line);
      background: var(--bg-tint);
      border-radius: 10px;
      padding: 11px 14px;
      font-size: 13px;
      margin-top: 8px;
    }

    .q-head {
      display: flex;
      align-items: center;
      gap: 7px;
      font-weight: 700;
      color: var(--navy);
      font-size: 12.5px;
      margin-bottom: 3px;
    }

    .q-head::before {
      content: "";
      width: 3px;
      height: 13px;
      border-radius: 2px;
      background: var(--brand);
      flex-shrink: 0;
    }

    .q-hint {
      font-size: 11.5px;
      color: var(--muted);
      margin-bottom: 5px;
      line-height: 1.55;
    }

    .q-title {
      color: var(--ink-2);
      font-weight: 600;
      margin: 8px 0 6px;
      line-height: 1.55;
      word-break: break-word;
    }

    .q-opts {
      display: flex;
      flex-wrap: wrap;
      gap: 7px;
    }

    .q-opt {
      border: 1px solid var(--line);
      background: var(--card);
      border-radius: 8px;
      padding: 5px 13px;
      font-size: 12.5px;
      cursor: pointer;
      color: var(--ink-2);
      max-width: 100%;
      word-break: break-word;
      transition: all .15s ease;
    }

    .q-opt:hover {
      border-color: var(--brand);
      color: var(--brand);
    }

    .q-opt.sel {
      border-color: var(--brand);
      background: var(--brand-soft);
      color: var(--brand);
      font-weight: 600;
    }

    .q-opt:disabled,
    .q-other:disabled {
      opacity: .6;
      cursor: not-allowed;
    }

    .q-other {
      width: 100%;
      box-sizing: border-box;
      margin-top: 7px;
      border: 1px solid var(--line);
      border-radius: 8px;
      padding: 6px 11px;
      font-size: 12.5px;
      background: var(--card);
      color: var(--ink);
      font-family: inherit;
    }

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

    .q-other::placeholder {
      color: var(--faint);
    }

    .q-foot {
      display: flex;
      align-items: center;
      gap: 10px;
      margin-top: 10px;
    }

    .q-submit {
      background: var(--brand);
      color: #fff;
      border: none;
      border-radius: 8px;
      padding: 6px 20px;
      font-size: 12.5px;
      cursor: pointer;
      flex-shrink: 0;
      transition: filter .15s ease;
    }

    .q-submit:hover {
      filter: brightness(1.05);
    }

    .q-submit:disabled {
      background: var(--ghost);
      /* 与 .mdl-btn[disabled] 同一档: 全站"点不动"是这一个灰 */
      cursor: not-allowed;
    }

    .q-state {
      font-size: 12px;
      color: var(--muted);
      line-height: 1.45;
    }

    /* 已回答: 整张卡转绿。这是**状态**不是排面 —— 绿色在全站就是"已完成"
       (权限卡的「总是允许」、报告里的通过/成功同一套), 而这正是这一刻唯一要说的事。
       转绿之后不再有第二个颜色: 选中的那个选项仍是那点品牌蓝, 提交键只是变灰。 */
    .q-card.answered {
      border-color: var(--green-border);
      background: var(--green-soft);
    }

    .q-card.answered .q-head {
      color: var(--green-deep);
    }

    .q-card.answered .q-head::before {
      background: var(--green);
    }

    .q-card.answered .q-hint,
    .q-card.answered .q-title {
      color: var(--green-deep);
    }

    /* 已作废(等回答超时 / 本轮已收尾): 这张卡不再是能作答的东西。
       绿那档留给"已回答", 这档只把它褪成灰 —— 左边那根竖条、标题、提示一起转 --muted,
       与禁用的选项/输入框(透明度 .6)是同一个"点不动"的意思, 不引新色: 卡片失效不是
       报错, 红色留给真出错的那档(见 .report-empty.failed)。 */
    .q-card.expired .q-head {
      color: var(--muted);
    }

    .q-card.expired .q-head::before {
      background: var(--faint);
    }

    .q-card.expired .q-hint,
    .q-card.expired .q-title {
      color: var(--muted);
    }

    /* 过期原因(状态行)比常态那档灰再实一点: 它是这张卡此刻唯一要说的事 */
    .q-card.expired .q-state {
      color: var(--ink-2);
    }

    /* ── 权限确认卡(permission.asked → 用户选 仅本次/总是允许/拒绝) ── */
    .perm-row {
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      gap: 6px 8px;
      margin-top: 9px;
      padding-top: 9px;
      border-top: 1px dashed var(--line);
    }

    .perm-row:first-of-type {
      margin-top: 5px;
      padding-top: 5px;
      border-top: none;
    }

    .p-tool {
      font-family: var(--mono);
      font-size: 11.5px;
      font-weight: 700;
      color: var(--navy);
      background: var(--card);
      border: 1px solid var(--line);
      border-radius: 6px;
      padding: 2px 9px;
      word-break: break-all;
    }

    .p-tag {
      font-size: 11px;
      color: var(--muted);
      font-weight: 600;
    }

    .p-btns {
      display: flex;
      gap: 6px;
      flex-wrap: wrap;
      margin-left: auto;
    }

    .perm-btn {
      border: 1px solid var(--line);
      background: var(--card);
      border-radius: 8px;
      padding: 4px 13px;
      font-size: 12px;
      cursor: pointer;
      color: var(--ink-2);
      font-weight: 600;
      transition: all .15s ease;
    }

    .perm-btn:hover {
      border-color: var(--brand);
      color: var(--brand);
    }

    .perm-btn.once {
      color: var(--brand);
      border-color: var(--line-strong);
      background: var(--card);
    }

    .perm-btn.always {
      color: var(--green-deep);
      border-color: var(--green-border);
      background: var(--card);
    }

    .perm-btn.reject {
      color: var(--red);
      border-color: var(--red-border);
      background: var(--card);
    }

    .perm-btn.once:hover {
      background: var(--brand-soft);
    }

    .perm-btn.always:hover {
      background: var(--green-soft);
    }

    .perm-btn.reject:hover {
      background: var(--red-soft);
    }

    .perm-btn:disabled {
      opacity: .55;
      cursor: not-allowed;
    }

    .perm-row .p-state {
      width: 100%;
      font-size: 11.5px;
      color: var(--muted);
    }

    /* 未批复就作废的那一行(见 question 卡的 .q-card.expired): 工具名从 --navy 褪到
       --muted, 与三个按钮的 :disabled 观感对齐。(那句 p-tag 本来就是 muted, 不动) */
    .perm-row.expired .p-tool {
      color: var(--muted);
    }

    /* 无障碍: 键盘焦点可见 */
    button:focus-visible {
      outline: none;
      box-shadow: var(--ring);
    }

    textarea:focus-visible {
      outline: none;
    }

    /* ── 窄屏适配 ── */
    @media (max-width: 720px) {
      .container {
        padding: 0 12px;
      }

      /* 胶囊贴到与输入框同一个右缘: 容器内边距收到 12, 定位条跟着贴 12 —— 桌面那个
         20 是配着 20 的内边距写的(见 .qa-rail 的注释)。 */
      .qa-rail {
        right: 12px;
      }

      /* 90% 会把气泡的左边挤到只剩 ~30px, 复制键(26 + 8 间隙)就出屏了 ——
         触发 .chat 的横向滚动条。收窄气泡给它让位。 */
      .msg.user {
        max-width: 84%;
        padding: 11px 15px;
      }

      /* 行距比桌面放宽一档(1.65 → 1.78): 长提问在手机上要折成十来行, 行距贴着字面
         时十行看下来糊成一片 —— 折行越多越吃行距, 而这正是窄屏上唯一变多的东西。
         1.78 是照着正文那一档取的(报告正文 1.75, 见 .report-wrap), 一句问话与它下
         面的报告疏密相当, 看着才是一份文档。改字用的输入框是同一份字号, 编辑态跟着
         一起放宽, 免得"改一半"时两处疏密不一样。 */
      .msg.user,
      .msg-edit-box {
        line-height: 1.78;
      }

      /* 空态: 四张卡改单列 —— 手机上两列会把副标题压成三行, 一列反而更好扫读。
         hero 一起收紧一档, 给卡和输入框留出高度。 */
      .welcome {
        padding: 12px 0;
      }

      .welcome-hero {
        gap: 8px;
        margin-bottom: 20px;
      }

      .welcome-title {
        font-size: 20px;
      }

      .welcome-sub {
        font-size: 13px;
      }

      .welcome-sub-2 {
        display: block;
      }

      .welcome-cards {
        grid-template-columns: 1fr;
      }

      .welcome-card {
        padding: 13px 14px 14px;
      }

      .welcome-upload {
        padding: 26px 16px;
      }

      /* 三条卖点窄屏必然各占一行: 换行后按 justify-content:center 居中,
         每行长短不一, 左边缘会参差; 改成竖排左对齐(整块仍由父级居中)。 */
      .welcome-points {
        flex-direction: column;
        align-items: flex-start;
        gap: 6px;
      }
    }

    /* ── 触屏: 常驻那些"悬停才出现"的按钮 ──
       这一节起头是气泡外侧那两颗(复制/修改): 它们 opacity:0 靠 .msg.user:hover 浮出,
       不常驻的话触屏上等于没有。表格角上那两颗(.tbl-copy)同理, 账号名上那颗
       (.acct-copy)也是同一个毛病 —— 全站查过一遍 opacity: 0, 这是最后三处可达性缺口。
       (表格那两颗只是常驻还不够 —— 它们常驻下来会压住表格内容, 见下面单独那条。)
       下面几条(.lang-pop 高度 / 输入框字号 / 抓取带宽度 / 图列表宽度 / 定位条尺寸 /
       touch-action)也一并挂在这个判据下: 它们都是**看得见**的东西, 必须键在 hover 上,
       不能键在 any-pointer 上(见 base.css 开头那张判据表)。 */
    @media (hover: none) {

      .msg-copy,
      .msg-edit,
      .msg-del,
      .tbl-copy,
      .acct-copy {
        opacity: 1;
      }

      /* ── 表格那颗「复制」在触屏上换一种摆法: 从"压在表格角上"改成"表格上方一行" ──
         桌面靠 .tbl-wrap:hover 一闪即收, 浮层不碍事; 触屏没有 hover, 常驻的浮层就一直
         压在表格右缘。而它压的不是"一个角"那么轻 —— 按钮钉在 .tbl-wrap(**不滚**)上,
         表格在底下的 .tbl-scroll 里横滚, **滚到哪个位置, 右缘那一格就被压住**: 实测一张
         8 列宽表, 滚到底时末列短值("N" / "1.02")被压掉 100%, 滚到中间也还有 99%。
         (反过来, 表格放得下时一个字都不压 —— 格子是居中的, 按钮正好落在末格的右内边距
         里。所以这不是"按钮太大、让开几像素"的问题, 是"表格会横滚"才发作; 而手机上表格
         大多要横滚。)
         改法: 两颗并成一颗 —— 底部那颗在这里 display:none, 它原本的理由("表长时滚到底
         手边也得有一颗")在换到表格上方这一行之后就不成立了, 那一行不再随阅读位置漂走。
         剩下的那颗回到文档流、排到 .tbl-scroll 前面、靠右, 右边缘与表格齐平。
         实测三个滚动位 × 三种表型(放得下 / 末列窄 / 8 列宽表), 遮挡一律 0%。
         代价是每张表多约 28px 高, 这是这一档唯一花的钱。
         position 写 relative 而**不是 static**: relative 同样在文档流里, 同时又给上面
         any-pointer 那一批的 .tbl-copy::after(44px 热区)留出定位上下文 —— 写成 static
         的话那块热区会挂到 .tbl-wrap(它就是 relative)上去, 反而盖住整张表。 */
      .tbl-wrap {
        display: flex;
        flex-direction: column;
      }

      .tbl-copy {
        position: relative;
        /* 必须把上面那对 top/right 清掉: relative 之下它们不再是"从角上量", 而是把盒子
           从它在文档流里的位置**整体挪开**(实测右缘缩进 4px、与表格的间距少掉 4px)。 */
        top: auto;
        right: auto;
        order: -1;                 /* 排到 .tbl-scroll 前面 */
        align-self: flex-end;      /* 靠右, 与表格右缘齐平 */
        /* 高度 26 是"每张表多 28px"这笔预算定死的(26 + 下面那 2px margin), 不是随手取的
           —— 它同时顶住上面 any-pointer 那一批给的**看不见的** 44px 热区: 热区以按钮中心
           上下各伸 (44 - 盒高) / 2。盒高 26 时各伸 9, 而按钮到表格有 12px(2px margin +
           表格自己 10px 的上外边距), 余 3px, 热区探不进表头。盒高退回基线那 24 就不够了:
           各伸 10, 只剩 2px, 点表头上沿那一小条会变成复制。 */
        min-height: 26px;
        margin: 0 0 2px;
      }

      /* 底部那颗只在桌面上留(那里它是"滚到表格底部才浮出"的第二落点); 触屏改成上方
         一行之后, 它就成了同一位置的第二颗重复按钮。 */
      .tbl-copy--bottom {
        display: none;
      }

      /* ── 输入框字号: 12px ──
         这里原来写 16, 理由是"小于 16px 的输入框被聚焦时 iOS Safari 会把整页放大",
         16 因此被当成不能商量的线。但那是**聚焦不跳**的代价, 不是排面本身: 手机上按
         16 排, 输入的字比气泡里的提问(13.5)还大一档, 一屏里最抢眼的是正在打的那行字。
         按要求收到 12 —— 与定位条卡片、示例词同一档, 输入区整体安静下来。
         代价照旧存在: iOS 聚焦这个框时仍会缩放整页, 安卓无此问题。
         必须逐个点名, 不能只写 textarea: `.msg-edit-box` 的类选择器(0,1,0)压得住
         元素选择器(0,0,1)。#input 也列上 —— 一个 ID 不花钱, 且将来谁给 #input 补一条
         font-size 也顶不掉它。编辑框(改提问)跟着一路: 它与这里写的是同一句话, 一边
         大一边小,"改一半"时最难看。
         select.tb-sel(11px)先不动: iOS 对 select 是弹滚轮不是缩放, 真机验证后再补。
         桌面那两档(13.5 / 13.5)一个字不动 —— 桌面没有这套自动放大。 */
      #input,
      textarea,
      .msg-edit-box {
        font-size: 12px;
      }

      /* 登录页那三个框不动: 那页整页字号本就大一档(14, 见 login.css), 且它没有
         "输入区该安静"这回事 —— 那上面就三个框, 正在输的那个大一点才对。 */
      .login-field input {
        font-size: 16px;
      }

      /* ── 输入区在手机上的排面 ──
         字收到 12 之后, 原来那套给 16 配的留白(框内 10, 文本域上下各 4)就显松了:
         一行字孤零零吊在 40 高的框里。收紧为框内 8、文本域上下各 3; 多行时行距也从
         1.6 收到 1.5 —— 折到三四行时, 1.6 让每一行都像单独一条, 1.5 才读得出是一段话。
         两颗圆键(+, 发送)尺寸不动: 它们是与输入框对齐的控件, 26 的圆配 12 的字在这个
         高度上刚好。 */
      .input-box {
        padding: 8px;
      }

      #input,
      textarea {
        line-height: 1.5;
        padding: 3px 0;
      }

      /* ── 语言下拉的高度上限 ──
         它原来挂在 100vh 上(见 .lang-pop), 跟着换成 dvh —— 否则地址栏一收起, 下拉比
         可见高度还高, 底下那几个语种永远够不着。 */
      .lang-pop {
        max-height: calc(100dvh - 72px);
      }

      /* ── 抓取带: 7px → 20px ──
         手指按不准 7px, 而这条带子带着 touch-action: none —— 落在上面滚动会被吞掉,
         太窄就成了一条"暗礁": 用户不知道它在那儿, 蹭到却卡一下。20px 宽到能看见、
         能瞄准, 就成了一条明确的边而不是陷阱。握把那截 3×30 的 --faint 是常态可见的
         (见 .viz-resize::after), 所以这个宽度有对应的可见物, 不是凭空多出来一块。
         不给 display: none —— 那会白白丢掉调宽能力; 代价只是触屏上双击复位没了
         (dblclick 见 JS), 真机键盘的方向键仍可用。
         (≤900 那一档是例外: 抽屉被定成 100% 宽、这条边无处可拖, 直接 display: none 了,
         见下面那条 @media。)
         下面这条朝左长(right 定的位), 20 正好走完 .container 右侧那 20px 内边距 —— 分栏时
         第三栏的左缘就是容器的右缘, 这条带子停在 .chat 的右缘上, 一个字都不压。
         (从前进的是 --rail-gap 那 58px, 那是定位条还占列宽时的旧账, 见 base.css。)
         左栏那条(见 data-panel.css)朝右长, 会吃到对话列左边 6px 的留白和约 7px 正文 ——
         那是这条边所在的位置推出来的, 记在这儿不当它不存在。 */
      .viz-resize {
        width: 20px;
      }

      /* ── 图列表给图表让位 ──
         215 是"面板宽得下"时的样子; 面板被窗口挤窄时它不该还占着 215 —— 那一栏是**图**
         的舞台, 列表退一步, 图才画得开。0.34 与上限 215 的交点在 215 / 0.34 = 632:
         632 以上与原来一模一样, 之下才开始让。下限 150 是"序号 + 两三个字"还读得出来
         (.list-item 的字号是 12)。只写在触屏里是为了不动桌面 —— 桌面把抽屉拖到 480 时
         列表也才 170, 那是既有的状态, 不在这件事的范围内。分栏那条 .drawer-list 本来
         就是 display: none(窄栏放不下竖列表, 改走头部下拉)。 */
      .drawer-list {
        width: clamp(150px, calc(var(--viz-w) * 0.34), 215px);
      }

      /* ── 关掉控件上的双击缩放 ──
         连点两下(比如发送键点快了)iOS 会把整页放大。manipulation 关掉这一条, 同时
         不挡滚动(那是 pan-* 的事)。只给控件 —— 正文的捏合缩放留着, 报告里的表和
         公式需要它。两条抓取带是 div 且自带 touch-action: none, 不受这里影响。 */
      button,
      .tool-head,
      .f-item,
      .list-item {
        touch-action: manipulation;
      }

      /* ── 定位条在触屏上的尺寸 ──
         每格至少 32 高、格间缝 7→4。算式: 20 问 × 32 + 19 × 4 = 716px,
         820 高的 iPad 竖屏放得下且不滚; 再多 .qa-list 本来就有 max-height:100% 与
         overflow-y: auto 会自己滚。
         每格**不取 44**: 20 问 × 44 = 880px 必滚, 而这条栏的用途就是"一眼看完整段
         对话", 滚起来就废了。32 是"一条提问还读得出是哪一问"与"手指点得中"之间的
         取舍, 配合下面 .qa-item 那圈 2px 3px 的内边距, 实际可点区比 32 还大一点。 */
      .qa-list {
        gap: 4px;
      }

      .qa-item {
        min-height: 32px;
      }
    }

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

      .acct-refresh::after,
      .lang-btn::after,
      .acct-btn::after,
      .plus-btn::after,
      .send-btn::after,
      .tool-head .viz-btn::after,
      .drawer-close::after,
      .viz-mode::after,
      .viz-full-btn::after,
      .msg-copy::after,
      .msg-edit::after,
      .msg-del::after,
      .tbl-copy::after,
      .proc-toggle::after,
      .buy-step-b::after,
      .buy-pay::after,
      /* AI点余额那一行(见 index.html 与 .acct-bal 那条): 7+24+7 = 38 高, 够不着 44,
         同样只上下撑。上下各伸 3px 落在两侧那道 .acct-sep 的空档(4+1+4)里 —— 那一带
         没有任何可点物, 不会抢邻居的点击。行内那颗 .acct-refresh 的热区比这一行还高、
         且 tree 序在后(盖在上面), 它自己那一格照旧归它。 */
      .acct-bal::after,
      /* 行内那颗小按钮(.set-btn): 5+5 padding + 12.5 字 ≈ 28 高, 够不着 44。
         它在两处:「账号信息」余额旁边那颗「购买」、会员页顶上那颗「仅购买 AI 点」。
         两处上下紧挨的都是文字行(不是可点物), 所以只补热区、不动盒高就够。 */
      .set-btn::after {
        content: "";
        position: absolute;
        left: 0;
        right: 0;
        top: 50%;
        height: var(--tap);
        margin-top: calc(var(--tap) / -2);
      }

      /* 上面这批的唯一例外: 用户气泡左边那三颗。复制在上、删除+修改并排在下(见前面
         .msg-copy/.msg-edit 那条), 竖直两行中心差 32 —— 两个 44 叠上去就是 12px 的
         重叠带, 而重叠带盖住的是**看得见的圆**——那颗圆便有一小条点了不响应。这里把
         命中区收到 30(< 32): 既不重叠, 26 的圆也整个包在里面(上下各余 2px)。
         判据是 hover:none 不是 any-pointer:coarse, 因为这条**只在触屏常显这三颗时
         才成立**——带触摸屏的 Windows 笔记本(hover:hover)三颗仍靠 hover 显形, 44 的
         命中区在那儿不冲突, 不该跟着缩。 */
      @media (hover: none) {

        .msg-copy::after,
        .msg-edit::after,
        .msg-del::after {
          height: 30px;
          margin-top: -15px;
        }
      }

      /* 上面这批是常态定位(static), 得先给出基准。另四颗(.msg-copy / .msg-edit /
         .tbl-copy / .proc-toggle)本来就是 absolute, 给它们写 relative 会把它们从
         原处拽回文档流 —— 所以分成两条写, 别合并。 */
      .acct-refresh,
      .lang-btn,
      .acct-btn,
      .plus-btn,
      .send-btn,
      .tool-head .viz-btn,
      .drawer-close,
      .viz-mode,
      .viz-full-btn,
      .buy-step-b,
      .buy-pay,
      .set-btn,
      .acct-bal {
        position: relative;
      }

      /* AI点那一行的热区是整行铺的(见上面那条), 而**伪元素是原名下的最后一个子节点**:
         .acct-bal::after 的 tree 序在 .acct-refresh::after 之后 —— 两块同是 absolute /
         z-index: auto, 于是画的、命中的都压在刷新上面。手指点刷新那颗的中心, 命中的是
         这一行, 那一格就成了"去开通"(实测 elementFromPoint 回的就是 .acct-bal)。
         这一格还给刷新: 给它一个层级, 它连同自己那块热区整块盖在行热区上面。
         只在触屏档里写 —— 桌面上这两块热区都不存在, 一个像素都不动。 */
      .acct-refresh {
        z-index: 1;
      }

      /* 卡片自己 overflow: hidden(.tool-card), 会把上面那块 44 裁到卡片边缘 —— 约 34。
         补一句最小高度让扩张块露头, 视觉上只是把胶囊撑到与同行其它按钮齐平。 */
      .tool-head .viz-btn {
        min-height: 34px;
      }

      /* 设置弹窗的分区 tab: padding 8 + 13px 字 ≈ 32px, 够不着 44。
         这条**不能**写进上面那个 ≤560 的块里 —— 后者在"窄窗口的桌面"上也会命中,
         那台机器没有手指, 观感一个像素都不该动。高度是看得见的, 所以判据只能是
         有没有触摸屏(any-pointer), 不能是窗口窄不窄。 */
      .set-tab {
        min-height: var(--tap);
      }

      /* 「会员与 AI 点」那一页: 30×32 的加减、152×36 的两颗支付键都够不着 44, 用上面那批
         同一套 ::after(只上下撑)。上下不叠: 量过 390 与 320 两档, 加减那一行到底下支付键
         的间距都是 15px, 两边各伸 6 / 4 还有 5px 富余。
         **协议那一行故意不进这个名单**: .buy-agree 是个 <label>, 里面裹着《会员服务协议》
         的 <a>; 整行铺一块 absolute 的 ::after 会盖在链接上, 把链接的点击整个吃掉 ——
         而它本来就整行可点(314 宽), 只差高度。要收它得单独给那一小格划热区, 不值当。
         份数输入框同理不进来: <input> 挂不了伪元素, 而给它 min-height 长的是看得见的高度,
         这一页"手机上不滚"的账是拿高度换的(见 .acct-body 那段)。 */

      /* 份数输入框在这一档要 16px —— 与命中区无关, 是 iOS Safari 的规矩: 聚焦 font-size
         < 16px 的控件时它会把**整页**放大(跳一下, 而且失焦后不自己缩回来, 得手动捏回去),
         而拨份数正是这条路上绕不开的一格。
         只改字号, 不动盒: 盒高由 .buy-step-i 自己的 height:32px 定死, 所以这一页"手机上
         不滚"的高度账一分不花 —— 量过, 改前改后盒都是 64×32, 面板 447 不变。
         装得下: 内宽 62, "100" 在 13.5px 下量得 24.4, 16px 下约 29。 */
      .buy-step-i {
        font-size: 16px;
      }
    }

    /* ── 窄屏: 图表抽屉改全屏 ──
       900 与侧栏抽屉同一个断点(见 data-panel.css): 竖屏平板和手机走全屏, 横屏平板
       仍与桌面一样保留浮层 —— 而 835~1132 那段是空档, 所以这条线永不切在某台设备当中。
       为什么改满: 抽屉缺省宽是 max(640, innerWidth × 0.68), 那个 640 的下限在 941 以下
       一直生效 —— iPad 竖屏 744 下占 86% 屏宽, 820 下 78%, 抽屉一开正文就没影了, 而
       这两档正是这次要照顾的屏。既如此不如给满: 图能画到最大, 左边也不再留一条只能
       看不能用的缝。
       全屏之后没有"点面板外收起"了, 关闭只剩头部那颗 ✕ —— 它已在命中区那批名单里。
       左缘那道抓取带(触屏档已放宽到 20)在全屏下不但无处可拖, 还看得见 —— 见下面那条。 */
    @media (max-width: 900px) {

      .viz-panel--float {
        width: 100%;
        max-width: 100%;
        border-left: none;
      }

      /* ── 全屏抽屉: 收掉左缘那道抓取带 ──
         它按 right: var(--viz-w) 定位(见 .viz-resize), 而这一档面板的宽度被上面那条
         width: 100% 定死 —— --viz-w 不再影响面板, 抓取带却照旧从右缘推出它的位置:
         390 宽的手机上落在 x=20~40, **钻进了面板里**, 那截 3×30 的把手就浮在图的左边上
         (看着像图上多了条莫名其妙的竖线)。且它带着 touch-action: none 又是 20 宽,
         正好在图上吃掉一条竖着的滑动。
         拖它还是白拖: 面板宽度这一档不由 --viz-w 说了算, 拖出来的只是个改不动的偏好值。
         所以这一档直接收掉 —— 唯一握着"无处可拖"这条毛病的入口, 也就一起没了。 */
      .viz-resize {
        display: none;
      }

      /* 竖列表让位给图 —— 照抄分栏那套(.viz-panel--col .drawer-list): 切换改走头部
         那颗 #stageSel 下拉。两种宿主共用同一个 <select>, 不用新 UI; 但它在抽屉形态下
         本来是隐藏的(syncVizChrome 只在分栏里显示它), 那边要跟着补一句 —— 见 JS。 */
      .viz-panel--float .drawer-list {
        display: none;
      }

      /* 「切到单图模式」在全屏下没有对面可切: 分栏要窗口 ≥1366(见 VIZ_COL_MIN_VW), 窄屏
         永远够不着, 留着就是一颗按不动的死按钮(它那份"按不动"的文案本来就是为窄窗口
         写的, 现在连形态都不成立)。 */
      .viz-mode {
        display: none;
      }

      /* 窄屏抽屉本来就是 100% 宽 + 满高, 「全屏」与现状没有区别, 留着只是颗死按钮 */
      .viz-full-btn {
        display: none;
      }

      /* 头部工具栏: 控件全保留, 放不下的折行 —— 标题独占首行(身份), 动作按钮与关闭走第二行。
         不再横向溢出(窄屏里那颗「批量下载」会把 ✕ 顶出屏幕)。 */
      .drawer-head {
        flex-wrap: wrap;
        row-gap: 6px;
      }

      .drawer-head .drawer-title {
        min-width: 0;
        flex: 1 1 100%;
      }

      .drawer-head .tb-spacer {
        display: none;
      }

      .drawer-head .drawer-close {
        margin-left: auto;
      }
    }