:root {
  color-scheme: light;
  --ink: #192331;
  --muted: #758091;
  --line: #e5e9ef;
  --panel: #f7f9fc;
  --blue: #2168e8;
  --blue-soft: #eaf1ff;
  --green: #198754;
  --amber: #ae7100;
  --red: #c73a48;
  font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
}

* { box-sizing: border-box; }
/* hidden 属性必须能压过组件自身的 display（.alert-banner 之类的 flex/block）：
   否则 JS 里设了 hidden，元素照样占着版面显示出来。 */
[hidden] { display: none !important; }
body { margin: 0; color: var(--ink); background: #fff; font-size: 13px; }
button, input, textarea, select { font: inherit; }
button { cursor: pointer; }
h1, h2, p { margin: 0; }
.shell { display: grid; grid-template-columns: 180px minmax(0, 1fr); min-height: 100vh; }
/* 滚动容器：`overflow-y: auto` 会让 overflow-x 的 visible 计算成 auto，
   必须显式 hidden，否则表格 min-width 撑出的横向溢出会在主内容区多出一条横向滚动条。
   内边距（尤其顶部）必须为 0：滚动发生在 .main 内侧，任何一个像素的内边距
   都会变成固定区永远吃不掉的空档。首屏留白由 .view-tabs 的 margin-top 表达。 */
.main { min-width: 0; width: 100%; height: 100vh; padding: 0 0 48px; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; }
/* 滚动条常驻占位：名单够长时滚动条出现/消失不再让整表横向跳一下。
   顶部固定条带是 100% 宽，已把这条滚动条算在内。 */
@supports (scrollbar-gutter: stable) { .main { scrollbar-gutter: stable; } }
/* 固定区整体的滚动分隔线：名单越长越需要它提示"固定区下面还有内容"。
   任务记录页仍是分块吸附，阴影照旧画在筛选条底边。 */
.main.is-scrolled .merchant-fixed { box-shadow: 0 7px 6px -6px rgba(25, 35, 49, .16); }


:root {
  /* 主内容区水平内边距：滚动容器自己不留左右内边距（见下），
     改由视图内容各自使用，这样固定区可以横向铺满、又不会把内容顶到滚动条上。 */
  --main-pad-inline: 48px;
  /* 首屏纵向留白：由 .view-tabs 的 margin-top 撑出（它随滚动正常走完，
     不会像容器内边距那样变成吃不掉的空档）。必须 ≥ 页签自身高度，
     否则页签会顶进下面的内容里（页签实测 40px 高：10 + 18 + 11 + 1px 下边框）。
     48 → 32：页名大标题下线后，页签上方不再需要为"标题也要有一点呼吸位"留这么多。 */
  --sticky-first-gap: 32px;
  /* 页签吸顶后占的高度：下方固定区（.merchant-fixed）贴它下沿吸顶。
     默认 40px 与页签实测高度一致，脚本会跟随字体/内边距变化重写这个值。 */
  --sticky-tabs-band: 40px;
  /* 表单控件统一高度：按钮、下拉框、图标按钮、纯文字按钮都按它对齐。
     39 = 9×2 内边距 + 17 行高 + 1×2 边框。浏览器默认给下拉框只有 29px、
     图标按钮 34px、主按钮 37px，同一行并排就会高低不齐（人工复核实测提出过）。 */
  --control-height: 39px;
}

/* ── 固定工作台头部 ───────────────────────────────────────────
   需求：名单往下滚时，「页签 + 标题 + 搜索筛选/选择条」一直停在顶部，
   表格以上区域不再被卷走；只有表格部分滚动。

   组成：全局页签（.view-tabs，跨视图共用）吸顶在窗口最顶边；
        商家名单页的固定区（.merchant-fixed）紧贴页签下沿吸顶。
   两者都是 position: sticky，浏览器自己负责堆叠——没有"偏移量累加"的算术，
   也就不存在改一处字号就要重算一长串 top 的问题。

   前提：纵向滚动发生在 .main 自己身上（height:100vh + overflow-y:auto），
   且 .main 的上下内边距必须是 0——sticky 的 top 只相对滚动容器的 padding box 计算，
   留在容器上的内边距会变成一块吃不掉的顶部空档，名单从它下方被裁切。
   首屏留白则由 .view-tabs 的 margin-top 表达：它是内容的一部分，随滚动正常走完。 */
/* 固定区整体吸顶：页签下方的「筛选条 + 动作行 + 选择条」。
   背景必须白：粘性块下方是滚动的表格，透明会看到表格从文字底下穿过。 */
.merchant-fixed { position: sticky; top: var(--sticky-tabs-band, 40px); z-index: 2; background: #fff; }
/* 页签与下方内容之间的距离 28 → 12：页名大标题下线后，这段间距不再需要
   "把标题和内容分开"，只保留"页签和内容不是同一块"的最小提示。
   六个页签共用这一处，所以改一次六页一起收，不存在某页还空着的可能。
   （之前实测：六个页签下沿到首块内容统一是 28px，视觉上就是一条空带。） */
.view-tabs { display: flex; align-items: center; gap: 4px; border-bottom: 1px solid var(--line);
  margin: var(--sticky-first-gap) var(--main-pad-inline) 12px;
  overflow-x: auto; overflow-y: hidden; scrollbar-width: none; background: #fff;
  /* top 就是吸附位置本身：0 = 贴住窗口最顶边。
     未滚动时它只是普通文档流里的一块（margin-top 撑出首屏留白），
     滚动超过留白后才吸过去，两者在数值上没有补偿关系。 */
  position: sticky; top: 0; z-index: 3; }
.view-tabs::-webkit-scrollbar { display: none; width: 0; height: 0; }
.view-tab { flex: 0 0 auto; border: 0; border-bottom: 2px solid transparent; background: transparent; color: var(--muted); padding: 10px 14px 11px; margin-bottom: -1px; white-space: nowrap; }
.view-tab:hover { color: var(--blue); }
.view-tab.active { border-bottom-color: var(--blue); color: var(--blue); font-weight: 600; }
/* 此处曾有 `.collect-actionbar`（采集需求页的独立动作行）：已随"上半部分一行化"下线——
   三个页面动作并进了派发概览块（.collect-strip / .collect-strip__actions），
   吸顶与背景规则一并搬到那里，见文件后段「采集需求页与执行器页的采集设置」一节。
   样式表里保留这块占位是为了让"动作行去哪了"有处可查，不引入任何新规则。 */
.muted { color: var(--muted); }
.primary-button, .secondary-button, .danger-button { border: 1px solid transparent; border-radius: 5px; padding: 9px 13px; height: var(--control-height); font-weight: 600; white-space: nowrap; }
.primary-button { background: var(--blue); color: #fff; }
.primary-button:hover { background: #1856c5; }
.secondary-button { background: #fff; color: var(--ink); border-color: var(--line); }
.secondary-button:hover { background: var(--panel); }
/* 「规则说明」这类"打开说明"的按钮：形状走 .secondary-button（和同排的「刷新」「新建」
   同为按钮家族），只多一个「?」提示它是说明而不是动作。
   图标用 ::before 而不是写进按钮文本：按钮的 textContent 必须保持「规则说明」——
   UI 用例按 `textContent === '规则说明'` 断言入口文案，图标混进文本会让它假红。 */
.secondary-button--help::before {
  content: '?'; display: inline-grid; place-items: center; width: 14px; height: 14px;
  margin-right: 6px; border: 1px solid #b9c4d4; border-radius: 50%;
  color: var(--blue); font-size: 10px; font-weight: 700; line-height: 1; vertical-align: 1px;
}
.secondary-button--help:hover::before { border-color: var(--blue); }
.danger-button { background: var(--red); color: #fff; }
/* 禁用态必须有可见反馈：否则重跑在途时被锁住的按钮看起来仍然可点。 */
.primary-button:disabled, .secondary-button:disabled, .danger-button:disabled { opacity: .5; cursor: not-allowed; }
.primary-button:disabled:hover { background: var(--blue); }
.secondary-button:disabled:hover { background: #fff; }
.icon-button { width: 34px; height: var(--control-height); border: 1px solid var(--line); border-radius: 5px; background: #fff; color: var(--muted); font-size: 18px; }
/* 统计卡随内容滚动（不常驻顶部，避免挤掉名单行），所以它紧接固定区下方、
   与表格之间只留一点呼吸位。 */
.stats-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin: 8px var(--main-pad-inline) 14px; }
.stat-card { border: 1px solid var(--line); border-radius: 6px; padding: 14px; }
.stat-card strong { display: block; font-size: 22px; }
.stat-card span { display: block; color: var(--muted); font-size: 11px; margin-top: 6px; }
.stat-card.success strong { color: var(--green); }
.stat-card.warning strong { color: var(--amber); }
.stat-card.danger strong { color: var(--red); }
/* 商家名单页头部只剩筛选条 + 选择条（页名与连接状态已下线，见 index.html 注释）。
   页签下沿到筛选条之间留 12px：页签自身 margin-bottom 也是 12px，所以首块内容
   与页签的分隔靠这一处表达；原来由 25px 大标题撑出的 60px 空档随标题一起去掉。 */
.merchant-fixed > .toolbar { margin-top: 12px; }
/* ≤1200px 时 .toolbar 里的搜索框 + 状态筛选 + ↻ + 动作组可能放不下，靠 flex-wrap 换行；
   这里把左右内边距移交给 .merchant-fixed 统一给，腾出那 96px（48×2）给搜索框——
   不腾的话搜索框最少时只剩 264px，动作组会被拆成两行。
   阈值 1200px 是算出来的：视口 − 侧边栏 180 = 可用宽度，再减两侧各 48 的留白，
   减去搜索框最小 220 + 状态筛选约 150 + ↻ 34 + 动作组 256 + 5 个 8px 间距 ≈ 1048px；
   宽于它就该是一行，窄于它就换行。
   上内边距保持 .toolbar 自己的 12px（sticky 只认滚动容器的 padding box，
   横向内边距对吸顶没有影响）。 */
@media (max-width: 1200px) {
  .merchant-fixed { padding: 0 var(--main-pad-inline); }
  .merchant-fixed > .toolbar,
  .merchant-fixed > .selection-toolbar { margin-left: 0; margin-right: 0; }
}
/* 筛选/选择条（固定区里最该常驻的一块——名单几百行时靠它定位商家）。
   底部这 10px 用 padding 而不是 margin 表达：吸附状态下 margin 是透明带，
   滚动内容会从那道缝里透出来。
   这一行里装六件东西，宽屏下**一行放完**：
     左 = 搜索（吃掉剩余宽度）+ 状态筛选 + ↻刷新（都作用在同一份名单数据上）
     右 = .toolbar__action：最近批次 / 批次执行 / 导入名单（页面级动作，靠右成组）
   窄屏为什么必须换行（≤1000px 见下方媒体查询）：一行要装约 610px 的固定内容，
   390px 视口下 flex 默认不换行，实测把 .main 撑到 scrollWidth 689px（横向溢出）。 */
.toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-bottom: 10px; margin: 0 var(--main-pad-inline); }
/* 搜索框吃掉剩余宽度，把它右边的「状态筛选 + ↻刷新」推到中间偏右、动作组顶到最右。
   为什么不用 justify-content: space-between：那会把状态筛选甩到正中间，
   刷新与它之间被撑出一大段空白，看起来像"刷新"和它筛的那份数据没关系。 */
.toolbar > .search-control { flex: 1 1 220px; min-width: 0; }
/* 左边距 auto = 把三个页面级动作推到这一行最右端（它们与筛选控件不是一回事）。
   为什么用显式类名 .toolbar__actions 而不是 `:first-of-type`：
   `:first-of-type` 按**标签**计数，`.toolbar__action:first-of-type` 匹配的是这一行里
   第一个 <button>（也就是 ↻ 刷新），不是「最近批次」——本轮实测踩过这个坑：
   规则"看起来生效了"，实际作用在刷新按钮上，动作组照样被拆行。 */
.toolbar__actions { display: flex; align-items: center; gap: 8px; margin-left: auto; }
/* 同一行里所有控件的**高度必须一致**：下拉框和图标按钮天生比按钮矮
   （下拉 29~35px、图标 34px、按钮 37px，实测同行三档高度）。
   统一口径是全局变量 --control-height（见 :root），这里只补下拉框的：
   按钮与图标按钮已在各自基础规则里用了同一个变量。 */
.toolbar > select { height: var(--control-height); min-width: 132px; }
/* 三颗页面级动作按钮等宽等高：文案虽然都是 4 个字，但"导入名单"的框天生更宽，
   定宽后视觉成组、扫描更快；窄屏也不变形。高度显式给到 39px——
   与同行的下拉框、↻ 完全一致（按钮默认 37px，会比下拉框矮 2px）。 */
.toolbar__actions > .primary-button,
.toolbar__actions > .secondary-button { min-width: 92px; height: var(--control-height); text-align: center; }
.align-right { justify-content: flex-end; margin-top: 24px; }
.selection-toolbar { display: flex; align-items: center; gap: 16px; min-height: 42px; padding-bottom: 10px; margin: 0 var(--main-pad-inline); }
.selection-toolbar > span:first-child { font-weight: 600; }
/* 操作按钮成组靠右，视觉上明确是三个动作，而不是散落的链接文字 */
.selection-actions { display: flex; align-items: center; gap: 8px; margin-left: auto; }
.selection-actions .primary-button, .selection-actions .secondary-button { padding: 7px 12px; }
/* 选择条三件东西尺寸完全一致（宽 91 × 高 39）：
   两个按钮默认 33px、纯文字按钮只有 25px，宽度也各随文案长短（91 / 60 / 60），
   并排就高低不齐、宽窄不一。现在三颗都是按钮形态 + 同一个高度与最小宽度。 */
.selection-actions > .primary-button,
.selection-actions > .secondary-button { height: 39px; min-width: 91px; }
/* 「批量文案」是主按钮，但没选商家时是 disabled：
   全站通用禁用态是 `opacity .5`，蓝底会褪成"灰蓝"，看着就不像能点的主动作。
   这里保留蓝底、只用透明度表达不可用，并在 hover 时维持底色（通用规则会改回 var(--blue)，
   那会让禁用按钮在鼠标悬停时变亮，反而更像可点）。
   「清除选择」走通用 secondary 禁用态即可：白底描边按钮变淡不会引起"能不能点"的误判。 */
.selection-actions .primary-button:disabled { opacity: .55; }
.selection-actions .primary-button:disabled:hover { background: var(--blue); }
.select-cell { width: 42px; text-align: center; }
.select-cell input { width: 16px; height: 16px; accent-color: var(--blue); }
.search-control { flex: 1; display: flex; align-items: center; gap: 7px; border: 1px solid var(--line); border-radius: 5px; padding: 0 10px; color: var(--muted); }
.search-control input { width: 100%; border: 0; outline: 0; padding: 9px 0; }
select { border: 1px solid var(--line); border-radius: 5px; background: #fff; padding: 0 10px; color: var(--ink); }
/* 窄屏（≤1200px）：搜索框占满第一行，状态筛选 + ↻ 落第二行，三个动作整组落第三行靠右。
   阈值与上面 .merchant-fixed 的留白移交一致（同一个"一行放不下"的判断）。
   换行靠 flex-wrap 自己发生；动作组三颗按钮靠组容器的 flex-basis 保证不被拆开。
   位置必须在 .toolbar / .search-control / .toolbar__actions 三条基础规则**之后**：
   媒体查询本身不提升优先级，写在前面会被后面的基础规则按同优先级覆盖。 */
@media (max-width: 1200px) {
  .toolbar > .search-control { flex: 1 1 100%; }
  /* 动作组整组换行：组容器自己占满一行，组内三颗按钮 8px 横排、整组贴右沿。
     散着放是不行的：浏览器会把"最近批次"塞进上一行剩下的 167px，
     再把另外两颗甩到下一行（实测 390px）。 */
  .toolbar > .toolbar__actions { flex: 1 1 100%; }
}
.table-wrap { margin: 0 var(--main-pad-inline); border: 1px solid var(--line); border-radius: 6px; overflow: auto; }
table { width: 100%; min-width: 760px; border-collapse: collapse; }
th, td { padding: 12px 11px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: middle; }
th { background: var(--panel); color: var(--muted); font-size: 11px; white-space: nowrap; }
tbody tr:last-child td { border-bottom: 0; }
.sr-only { position: fixed; top: auto; left: -10000px; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
.detail-control-heading { width: 38px; padding-left: 0; padding-right: 14px; }
.detail-toggle-cell { width: 38px; padding-left: 0; padding-right: 14px; text-align: right; }
.run-row[data-run-toggle] td { cursor: pointer; }
.detail-toggle { display: inline-grid; place-items: center; width: 28px; height: 28px; border: 0; border-radius: 4px; background: transparent; color: var(--muted); padding: 0; font-size: 18px; line-height: 1; }
.detail-toggle:hover, .detail-toggle:focus-visible { color: var(--blue); outline: 0; }
.run-row.is-expanded td { background: #fbfcfe; border-bottom-color: transparent; }
.run-detail-row > td { padding: 0; background: #fbfcfe; border-bottom: 1px solid var(--line); }
.run-detail { padding: 20px 24px 22px 67px; border-top: 1px solid #eef1f5; }
.run-detail__topline { display: grid; grid-template-columns: minmax(300px, 1fr) minmax(300px, 1fr); gap: 28px; padding-bottom: 18px; }
.run-detail__eyebrow { color: var(--muted); font-size: 11px; font-weight: 600; margin-bottom: 6px; }
.run-detail__merchant { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 16px; }
.run-detail__facts { display: flex; flex-wrap: wrap; gap: 8px 18px; margin-top: 9px; color: var(--muted); line-height: 1.5; }
.run-detail__facts b { font-weight: 600; }
.status-text--success { color: var(--green); }.status-text--no_match { color: var(--amber); }.status-text--failed, .status-text--timeout, .status-text--login_expired, .status-text--captcha_required { color: var(--red); }
.run-detail__business { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; align-content: start; }
.run-detail__business div { min-width: 0; }
.run-detail__business span { display: block; color: var(--muted); font-size: 11px; margin-bottom: 5px; }
.run-detail__business strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
.run-detail__section { border-top: 1px solid var(--line); padding-top: 16px; }
.run-detail__section-heading { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; margin-bottom: 12px; }
.run-detail__section-heading strong { font-size: 13px; }
.run-detail__section-heading span { color: var(--muted); font-size: 11px; }
.run-step-grid { display: grid; grid-template-columns: repeat(4, minmax(145px, 1fr)); gap: 8px 12px; margin: 0; padding: 0; list-style: none; }
.run-step { display: flex; align-items: center; gap: 8px; min-width: 0; min-height: 42px; padding: 7px 9px; border: 1px solid var(--line); border-radius: 5px; background: #fff; }
.run-step__marker { display: inline-grid; flex: 0 0 22px; place-items: center; width: 22px; height: 22px; border: 1px solid #d7dde6; border-radius: 50%; color: #a3adba; font-size: 12px; font-weight: 700; }
.run-step__copy { display: grid; min-width: 0; gap: 2px; }
.run-step__copy strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
.run-step__copy small { color: var(--muted); font-size: 10px; }
.run-step[data-step-state="completed"] { border-color: #cfe9da; background: #f5fbf7; }.run-step[data-step-state="completed"] .run-step__marker { border-color: #72c898; color: var(--green); background: #e9f7ef; }
.run-step[data-step-state="failed"] { border-color: #f3c7cc; background: #fff7f7; }.run-step[data-step-state="failed"] .run-step__marker { border-color: #ed9da6; color: var(--red); background: #fff0f1; }
.run-step[data-step-state="current"] { border-color: #f3c7cc; background: #fff7f7; }.run-step[data-step-state="current"] .run-step__marker { border-color: #ed9da6; color: var(--red); background: #fff0f1; }.run-step[data-step-state="current"] .run-step__copy small { color: var(--red); }
.run-step[data-step-state="pending"] .run-step__marker { color: var(--red); border-color: #efb4ba; background: #fff7f7; }
.run-diagnostic { margin-top: 18px; padding: 14px 16px; border: 1px solid; border-left-width: 3px; border-radius: 5px; }
.run-diagnostic--danger { border-color: #f1c3c8; border-left-color: var(--red); background: #fff7f7; }.run-diagnostic--warning { border-color: #f0d79b; border-left-color: var(--amber); background: #fffaf0; }
.run-diagnostic dl { display: grid; grid-template-columns: minmax(90px, 110px) minmax(0, 1fr); gap: 7px 14px; margin: 0; font-size: 12px; line-height: 1.55; }.run-diagnostic dl div { display: contents; }.run-diagnostic dt { color: var(--muted); }.run-diagnostic dd { margin: 0; overflow-wrap: anywhere; }.run-diagnostic code { color: inherit; font: 11px Consolas, monospace; }
.run-detail--loading, .run-detail--error { min-height: 160px; display: flex; align-items: center; gap: 10px; color: var(--muted); }.run-detail--loading { justify-content: center; }.run-detail__loading-mark { color: var(--blue); font-size: 20px; }.run-detail--error { display: block; color: var(--red); }.run-detail--error p { margin: 8px 0 10px; color: var(--muted); overflow-wrap: anywhere; }.run-detail--error .text-button { padding: 0; }
.merchant-name { max-width: 190px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.status-tag { display: inline-flex; border-radius: 99px; padding: 4px 8px; font-size: 11px; white-space: nowrap; }
.status-pending { color: var(--muted); background: #f1f3f6; }
.status-running, .status-queued, .status-searching, .status-matched, .status-product_selected, .status-detail_opened, .status-contact_opened, .status-chat_target, .status-contact_sent, .status-image_sent, .status-message_sent { color: var(--blue); background: var(--blue-soft); }
.status-success { color: var(--green); background: #e9f7ef; }
.status-no_match { color: var(--amber); background: #fff3cf; }
.status-failed, .status-login_expired, .status-captcha_required, .status-risk_verification_required, .status-timeout { color: var(--red); background: #fff0f1; }
.run-error-cell { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 执行环境标识：等宽字体便于逐字符比对（ziyun-01 / ziyun-02 差别只在末位）。
   「default（旧数据未记录环境）」用弱化样式——它既可能是单环境部署，也可能是标识改造前的历史，
   两种情况都无法回溯真实环境，所以不宣称具体是哪一个。 */
.executor-column { white-space: nowrap; }
.executor-tag { display: inline-block; border-radius: 4px; padding: 2px 6px; background: var(--panel); color: var(--ink); font: 11px Consolas, monospace; }
.executor-tag--legacy { background: #f1f3f6; color: var(--muted); font-family: inherit; }
/* 「执行来源」列的两行布局：上行环境、下行触发方式。 */
.run-source-cell { display: grid; gap: 3px; }
.run-source-cell__executor { line-height: 1.4; }
.run-source-cell__trigger { font-size: 12px; line-height: 1.4; }
.batch-link { border: 0; background: transparent; color: var(--blue); padding: 0; text-align: left; font-weight: 600; line-height: 1.45; }
.batch-link:hover, .batch-link:focus-visible { text-decoration: underline; }
/* 操作列的按钮必须保持横排：列被挤窄时 flex 子项默认会逐字换行，
   实拍会变成"详/情"各占一行（加「执行环境」列后触发过）。
   表格本就在 .table-wrap 内横向滚动，宁可整表横向滚动，也不让按钮竖排。 */
.row-actions { display: flex; gap: 8px; align-items: center; white-space: nowrap; }
.row-actions .link-button { flex: none; white-space: nowrap; }
.link-button, .text-button { border: 0; background: transparent; color: var(--blue); font-weight: 600; padding: 4px; }
.link-button.danger { color: var(--red); }
.link-button:disabled, .text-button:disabled { color: #a6afbd; cursor: wait; }
.empty { height: 130px; text-align: center; color: var(--muted); }
.config-panel { width: auto; margin: 24px var(--main-pad-inline) 0; border: 1px solid var(--line); border-radius: 6px; padding: 24px; }
/* 首张底板紧跟吸顶保存条：保存条自己带了 12px 下边距，这里再留 24px 会变成两层空带。 */
.config-head + .config-panel { margin-top: 0; }
/* 建联配置拆成多张底板：底板 A「建联动作」、批次参数、账号级限制、功能开关配置、采集需求参数。
   每块各自有完整边框，分组靠底板间距表达，不再用底板内部的 1px 分隔线——
   原来混在一张底板时，用户会把"账号级闸门"当成批次参数的一部分（它实际按执行器判定，
   而且还管采集链路）。除「建联动作」外只做间距，不引入新的视觉语言。 */
.config-panel--batch, .config-panel--account, .config-panel--switches { margin-top: 20px; }
/* 「文案分段」落在「建联动作」的两栏布局之下：跨满整行 + 一条虚线分隔，
   让它读起来是"话术那一步的补充设置"，而不是第三个栏目。 */
.config-layout-extra { margin-top: 18px; padding-top: 16px; border-top: 1px dashed var(--line); }
/* 「功能开关配置」区里没有网格，只有一个独立开关块：去掉它自带的上虚线（外面已是底板边框）。 */
.config-advanced--standalone { margin-top: 12px; padding-top: 0; border-top: 0; }
.config-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(360px, .9fr); gap: 42px; }
.config-section-title { font-size: 16px; line-height: 1.3; }
/* 分区标题旁的「?」：贴着标题把这一组参数的说明点开。
   用 inline-grid + vertical-align 而不是绝对定位——标题长短变化（以后加词）时它自己跟着走，
   不用重算偏移；也不影响其它共用 .config-section-title 的分区。 */
.help-dot {
  display: inline-grid; place-items: center; width: 18px; height: 18px; margin-left: 8px;
  border: 1px solid #c9d3e0; border-radius: 50%; background: #fff; color: var(--muted);
  font-size: 12px; font-weight: 700; line-height: 1; cursor: pointer; vertical-align: 2px;
}
.help-dot:hover, .help-dot:focus-visible { border-color: var(--blue); background: var(--blue-soft); color: var(--blue); outline: 0; }
/* 底板 A 内的分区标题（执行步骤 / 建联图片库）比底板标题低一级。
   h3 不在全局 margin 重置里，必须显式归零，否则浏览器默认 margin 会把标题顶开。 */
.config-subsection-title { font-size: 14px; line-height: 1.3; margin: 0; }
.config-panel__note { color: var(--muted); line-height: 1.6; margin-top: 6px; }
.config-toggle { display: flex; justify-content: space-between; align-items: center; gap: 20px; padding: 14px 0; border-bottom: 1px solid var(--line); }
.config-toggle:first-of-type { padding-top: 18px; }
.config-toggle span { display: grid; gap: 4px; }
.config-toggle small { color: var(--muted); }
.config-toggle input { width: 38px; height: 20px; accent-color: var(--blue); }

/* 执行步骤：每行 = 说明文案 + 排序按钮 + 开关。
   这里**不能**再像 .config-toggle 那样用 label 包整行：label 内的点击都会切换 checkbox，
   于是用户点一次 ↑ 就会把这一步关掉（"我只想调顺序，结果这一步不发了"）。 */
.step-rows { display: grid; }
.config-step-row { display: flex; align-items: center; gap: 14px; padding: 14px 0; border-bottom: 1px solid var(--line); }
.config-step-row:first-child { padding-top: 18px; }
.config-step-row__text { display: grid; gap: 4px; min-width: 0; flex: 1; }
.config-step-row__text small { color: var(--muted); }
.config-step-row__order { display: inline-flex; gap: 4px; flex: none; }
.step-move { width: 28px; height: 28px; padding: 0; line-height: 1; border: 1px solid #b9c4d4; border-radius: 5px; background: #fff; color: var(--ink); font-size: 13px; }
.step-move:hover:not(:disabled) { border-color: var(--blue); color: var(--blue); background: var(--blue-soft); }
.step-move:disabled { border-color: var(--line); color: #ccd3dd; background: var(--panel); cursor: not-allowed; }
.config-step-row input[type="checkbox"] { width: 38px; height: 20px; flex: none; accent-color: var(--blue); }
/* 顺序改动的**就地**提示：执行步骤在页面中部，统一保存条在页面底部，
   只靠底部提示用户很容易漏看，进而以为"顺序改不了"。展示时机由快照比对决定，
   不是常显文案。 */
.step-order-hint { color: var(--amber); }
.field-label { display: block; font-size: 12px; font-weight: 600; margin: 20px 0 7px; }
textarea { width: 100%; resize: vertical; border: 1px solid var(--line); border-radius: 5px; padding: 10px; outline-color: var(--blue); }
.image-config { border-left: 1px solid var(--line); padding-left: 42px; }
.config-section-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.image-status { color: var(--green); background: #e9f7ef; border-radius: 99px; padding: 4px 8px; font-size: 11px; white-space: nowrap; }

/* 建联图片库（按执行器隔离） */
.executor-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 14px; }
.executor-bar strong { font-family: Consolas, "Courier New", monospace; font-size: 13px; padding: 3px 8px; border-radius: 5px; background: var(--blue-soft); color: var(--blue); }
.executor-input { border: 1px solid var(--line); border-radius: 5px; padding: 6px 9px; outline-color: var(--blue); min-width: 220px; }
.image-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); gap: 12px; margin-top: 16px; }
.image-card { border: 1px solid var(--line); border-radius: 6px; overflow: hidden; background: #fff; display: flex; flex-direction: column; }
.image-card.is-active { border-color: var(--blue); box-shadow: 0 0 0 2px var(--blue-soft); }
.image-card img { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: contain; background: var(--panel); }
.image-card__body { display: grid; gap: 5px; padding: 9px 10px 11px; }
.image-card__body strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
.image-card__body .muted { font-size: 11px; }
.image-card__badge { justify-self: start; border-radius: 99px; padding: 3px 8px; background: var(--blue-soft); color: var(--blue); font-size: 11px; }
.image-card__body .text-button { justify-self: start; padding: 0; font-size: 12px; }
.image-card__body .text-button.danger { color: var(--red); }
.image-card--default { grid-column: 1 / -1; flex-direction: row; align-items: center; gap: 14px; }
.image-card--default img { width: 160px; }
.image-card--default .image-card__body { flex: 1; }
.image-picker { display: inline-flex; }
.image-actions { display: flex; align-items: center; gap: 12px; margin-top: 16px; }
.image-picker.is-disabled { opacity: .6; pointer-events: none; }
.single-image-warning { margin: 12px 0 0; padding: 9px 11px; border: 1px solid #f0d9a8; border-radius: 6px; background: #fffaf0; color: #8a5a00; font-size: 12px; line-height: 1.6; }
.config-actions { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-top: 24px; padding-top: 18px; border-top: 1px solid var(--line); }
/* 统一保存条：五张底板的开关/参数都由它一次提交，
   因此它**不套在任何 .config-panel 内**——留在一块底板内部会表达成"只保存这一块"。
   下面 AI 话术生成底板有自己的保存按钮，两者互不影响。

   2026-09-21：从"页尾一条"改为"吸顶第一行"。
   原因是这一页有五张底板，保存按钮在最底部时，改了「建联动作」要滚过整页才够得着；
   吸顶后滚动途中随时可保存。吸附位置与商家名单页的固定区同一套（贴页签下沿），
   外层 .config-head 负责吸顶（背景必须白：吸顶时下面滚的是底板），
   内层 .config-actionbar 仍是那条卡片，只是内边距交给外层统一给。 */
.config-head { position: sticky; top: var(--sticky-tabs-band, 40px); z-index: 2; background: #fff; padding: 12px var(--main-pad-inline) 12px; margin-bottom: 12px; }
/* 吸顶后才画分隔阴影：静止时它只是首屏第一块，画阴影会像"浮在上面"。 */
.main.is-scrolled .config-head { box-shadow: 0 7px 6px -6px rgba(25, 35, 49, .16); }
.config-actionbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin: 0; padding: 12px 18px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); }
.config-actionbar__status { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; min-width: 0; }
.config-dirty-hint { color: var(--amber); font-weight: 600; white-space: nowrap; }
/* 顺序非默认时的常显风险提示：它是当前配置的状态描述，不是一次性的确认框。
   文案较长，允许换行（保存条左侧是 flex-wrap 容器）。 */
.config-risk-hint { color: var(--amber); line-height: 1.5; }
/* 字段数量从 3 个增加到 8 个后，固定三列会把输入框挤到溢出；改为按最小宽度自适应换行。 */
.batch-config-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 14px; margin-top: 16px; }
.number-field { display: grid; gap: 7px; min-width: 0; }
.number-field > span { font-weight: 600; }
.number-field > div { display: flex; align-items: center; gap: 8px; }
.number-field input { width: 104px; border: 1px solid var(--line); border-radius: 5px; padding: 9px 10px; outline-color: var(--blue); }
.number-field input:invalid { border-color: var(--red); background: #fff7f7; }
.number-field em { color: var(--muted); font-style: normal; }
.number-field small, .config-hint { color: var(--muted); line-height: 1.5; }
/* 「文案分段」是一组参数：开关（是否按空行拆条）+ 上限（最多几条）。
   两者必须同处一个字段卡内，否则网格换行会把它们拆到两行，看不出是一回事。
   跨 2 列是安全的：>1000px 是 auto-fit（底板宽度足够放 3 列以上），
   ≤760px 由下面的媒体查询强制 2 列，都不会出现只有 1 列时 span 2 撑出横向滚动。 */
.segment-field { display: grid; gap: 7px; min-width: 0; grid-column: span 2; }
.segment-field > span { font-weight: 600; }
.segment-field__row { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.segment-field__toggle { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; }
.segment-field__toggle input { width: 38px; height: 20px; margin: 0; accent-color: var(--blue); }
.segment-field__toggle em, .segment-field__limit em { font-style: normal; }
.segment-field__limit { display: inline-flex; align-items: center; gap: 7px; }
.segment-field__limit input { width: 78px; border: 1px solid var(--line); border-radius: 5px; padding: 8px 9px; outline-color: var(--blue); }
.segment-field__limit input:invalid { border-color: var(--red); background: #fff7f7; }
.segment-field__limit em { color: var(--muted); }
.segment-field small { color: var(--muted); line-height: 1.5; }
.config-hint { margin-top: 14px; }
.time-field { display: grid; gap: 7px; min-width: 0; }
.time-field > span { font-weight: 600; }
.time-field > div { display: flex; align-items: center; gap: 8px; }
.time-field input { border: 1px solid var(--line); border-radius: 5px; padding: 8px 10px; outline-color: var(--blue); }
.time-field small { color: var(--muted); line-height: 1.5; }
.config-advanced { margin-top: 18px; padding-top: 14px; border-top: 1px dashed var(--line); }

/* 商家名单的「文案」列：单行截断 + 点击编辑。
   260/240 → 380/360：文案改成对标核心话术后普遍是 3~4 段、90 字上下，
   原来 240px 只能看到第一段的开头几个字，运营无法在名单页判断内容对不对。
   加宽后表格仍由 .table-wrap 内部横向滚动兜底，不会把 body 撑出横向滚动条。 */
.outreach-column { max-width: 380px; }
.outreach-cell { display: block; width: 100%; max-width: 360px; padding: 4px 6px; border: 1px solid transparent; border-radius: 5px; background: transparent; color: var(--ink); font: inherit; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
.outreach-cell:hover { border-color: var(--line); background: var(--panel); }
.outreach-cell:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }
.outreach-cell--empty { color: var(--muted); }
.outreach-missing { display: inline-flex; border-radius: 99px; padding: 3px 8px; background: #eef1f5; color: var(--muted); font-size: 11px; }
/* 未写文案的商家会在建批次/单条建联时被拦截，因此这里用中性灰而不是错误红。 */

/* 「商品」列收窄：它只是辅助线索（可能为空、也可能很长），
   而「文案」列才是这一页要核对的正文。一行省略号 + title 悬停看全，把宽度让给文案列。 */
.product-column { max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* 文案编辑弹窗 */
#outreachDialog { width: min(560px, calc(100vw - 32px)); }
.outreach-dialog-card textarea { width: 100%; box-sizing: border-box; resize: vertical; line-height: 1.6; }
.outreach-meta { display: flex; align-items: baseline; gap: 10px; margin-top: 6px; }
.outreach-hint { color: var(--muted); font-size: 11px; line-height: 1.5; }
#outreachLength.is-invalid { color: var(--red); font-weight: 600; }
.outreach-actions { display: flex; align-items: center; gap: 10px; margin-top: 12px; }

/* 分段预览：让人在保存前就知道"这条会发成几条"。切分口径与扩展侧同源（空行分段）。 */
.outreach-segment-summary { margin-top: 10px; font-size: 12px; font-weight: 600; color: var(--ink); }
.outreach-segment-summary.is-muted { color: var(--muted); font-weight: 400; }
.outreach-segment-summary.is-warning { color: var(--amber); }
.outreach-segment-preview { margin-top: 6px; max-height: 160px; overflow-y: auto; border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; background: var(--panel); }
.outreach-segment-row { display: flex; align-items: baseline; gap: 8px; padding: 4px 0; font-size: 12px; line-height: 1.5; }
.outreach-segment-row + .outreach-segment-row { border-top: 1px dashed var(--line); }
.outreach-segment-index { flex: none; color: var(--blue); }
.outreach-segment-length { flex: none; min-width: 76px; color: var(--muted); font-size: 11px; }
.outreach-segment-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 过短只是提示而非错误：单段太短发出去像半句话，但仍允许保存。 */
.outreach-segment-row.is-short .outreach-segment-length { color: var(--amber); font-weight: 600; }
/* 名单「文案」列的段数标记：提醒运营这条会发多条消息。 */
.outreach-segment-badge { display: inline-flex; margin-right: 6px; border-radius: 99px; padding: 1px 6px; background: var(--blue-soft); color: var(--blue); font-size: 10px; vertical-align: middle; }

/* 红线校验结果（话术池下线后仅服务于商家文案） */
.message-check { margin-top: 12px; border: 1px solid var(--line); border-radius: 6px; padding: 10px 12px; background: var(--panel); }
.message-check__item { margin: 0 0 6px; font-size: 12px; line-height: 1.55; }
.message-check__item:last-child { margin-bottom: 0; }
.message-check__item--error { color: var(--red); }
.message-check__item--warn { color: #a96a00; }
.message-check__item--ok { color: var(--green); }

/* 文案候选弹窗：AI 生成 3 条，用户挑一条再进编辑弹窗确认保存 */
#outreachGenerateDialog { width: min(620px, calc(100vw - 32px)); }
.variant-list { display: grid; gap: 10px; margin-top: 4px; max-height: min(420px, calc(100vh - 320px)); overflow: auto; }
/* 候选是「读 + 挑」而不是「编辑」，因此用卡片而不是 textarea；正文换行要保留（两段式文案靠它分段） */
.variant-card { border: 1px solid var(--line); border-radius: 6px; padding: 10px 12px; background: #fff; }
.variant-card__text { margin: 0; line-height: 1.7; white-space: pre-wrap; overflow-wrap: anywhere; }
.variant-card__meta { display: flex; align-items: center; gap: 10px; margin-top: 9px; color: var(--muted); font-size: 11px; }
/* 角标由「档位」改为「N 段 · N 字」：长度不再由档位决定，而是对标核心话术。 */
.variant-card__band { display: inline-flex; border-radius: 99px; padding: 2px 8px; background: var(--blue-soft); color: var(--blue); font-weight: 600; }
/* 对标偏差：达标时是弱化的对照信息，跑偏（段数不等/字数出容差）才标红。 */
.variant-card__target.is-over { color: var(--red); font-weight: 600; }
.variant-card__use { margin-left: auto; }
/* 候选按钮在卡片里要能一眼看出是主操作，而不是普通链接 */
.variant-card__use.primary-button { padding: 5px 11px; }
.outreach-generate-meta { margin-top: 12px; color: var(--muted); font-size: 11px; line-height: 1.6; }
/* 已写文案的行不需要强调；未写的行让按钮自己冒出来 */
.row-actions .link-button.is-urgent { color: var(--amber); }

/* 批量生成弹窗：事实条 + 对标口径 + 清单。
   宽度必须显式放宽：默认 dialog 只有 430px（内容区 388px），
   装"对标口径 + 清单（含商品线索）"会又挤又折行。 */
#bulkGenerateDialog { width: min(600px, calc(100vw - 32px)); }
.bulk-facts { margin: 0 0 14px; padding: 9px 12px; border-radius: 6px; background: var(--panel); color: var(--ink); font-size: 12px; line-height: 1.6; }
.bulk-facts .fact-sep { margin: 0 7px; color: var(--muted); }
/* 对标口径（只读）：取代了原来的「长度比例输入 + 档位分布条」——
   长度与段数现在由核心话术决定，界面上只该**告知**口径，不该再给可调项。 */
.bulk-reference { margin: 0 0 14px; font-size: 12px; line-height: 1.6; }
.bulk-reference.is-muted { color: var(--muted); }
.bulk-reference .muted { color: var(--muted); font-size: 11px; }
/* 清单里的元信息可能较长（含商品线索），截断而不是撑高行 */
.batch-item__meta { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* 强制包含已联系商家 */
.force-include-list { display: grid; gap: 8px; margin: 12px 0; max-height: 260px; overflow: auto; }
.force-include-row { display: flex; align-items: center; gap: 10px; border: 1px solid var(--line); border-radius: 6px; padding: 9px 11px; }
.force-include-row input { width: 16px; height: 16px; accent-color: var(--blue); }
.force-include-row span { display: grid; gap: 3px; min-width: 0; }
.force-include-row small { color: var(--muted); }

/* 批次确认里的一致性/配额告警 */
.batch-warning { margin: 12px 0 0; padding: 9px 11px; border: 1px solid #f0d9a8; border-radius: 6px; background: #fffaf0; color: #8a5a00; font-size: 12px; line-height: 1.6; }
dialog { border: 0; padding: 0; background: transparent; width: min(430px, calc(100vw - 32px)); }
#batchDialog { width: min(760px, calc(100vw - 32px)); }
dialog::backdrop { background: rgba(25, 35, 49, .35); }
.dialog-card { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 20px; box-shadow: 0 14px 40px rgba(25, 35, 49, .18); }
.batch-dialog-card { max-height: min(760px, calc(100vh - 32px)); overflow: auto; }
.batch-dialog-card .dialog-heading { position: sticky; top: -20px; z-index: 1; padding: 20px 0 12px; margin: -20px 0 0; background: #fff; }
.batch-summary-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
.batch-summary-grid > div { border: 1px solid var(--line); border-radius: 5px; padding: 10px; }
.batch-summary-grid strong, .batch-summary-grid span { display: block; }
.batch-summary-grid strong { font-size: 18px; }
.batch-summary-grid span { margin-top: 4px; color: var(--muted); font-size: 11px; }
.batch-progress { height: 7px; margin: 14px 0 18px; overflow: hidden; border-radius: 99px; background: #edf0f4; }
.batch-progress > div { height: 100%; border-radius: inherit; background: var(--blue); transition: width .2s ease; }
.batch-current-item, .batch-cooldown, .batch-risk-box { margin: 12px 0; padding: 14px 16px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); }
.batch-current-heading, .batch-risk-box strong { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.batch-current-heading span { color: var(--muted); font-size: 11px; }
.batch-current-name { margin-top: 8px; font-size: 16px; font-weight: 600; }
.batch-current-step { margin-top: 6px; color: var(--muted); }
.batch-current-step-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 8px; color: var(--muted); }
.batch-current-step-row strong { flex: 0 0 auto; color: var(--blue); font-size: 11px; }
.batch-task-progress { height: 6px; margin-top: 8px; overflow: hidden; border-radius: 99px; background: #edf0f4; }
.batch-task-progress > div { height: 100%; border-radius: inherit; background: #4b82ed; transition: width .2s ease; }
.batch-current-error { margin-top: 8px; color: var(--red); overflow-wrap: anywhere; }
.batch-cooldown { display: flex; justify-content: space-between; gap: 12px; color: var(--amber); }
.batch-risk-box { border-color: #f1c3c8; border-left: 3px solid var(--red); background: #fff7f7; }
.batch-risk-box p { margin: 8px 0; line-height: 1.55; }
.batch-risk-box span { color: var(--muted); font-size: 11px; }
/* 批次项：两列显式栅格，序号固定在第 1 列第 1 行，其余内容全部落在第 2 列。
   历史缺陷是三列自动排布把「重跑该项」按钮挤进 28px 序号列后逐字换行，这里从结构上消除该路径。 */
.batch-items { margin-top: 4px; }
.batch-items__heading { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 16px 0 10px; }
.batch-items__heading strong { font-size: 13px; }
.batch-items__heading span { color: var(--muted); font-size: 11px; }
.batch-item-list { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
.batch-item-list > li { display: grid; grid-template-columns: 24px minmax(0, 1fr); align-content: start; gap: 5px 10px; padding: 12px 14px; border: 1px solid var(--line); border-radius: 6px; background: #fff; }
.batch-item-list > li.is-confirming { border-color: #bcd0f5; background: #f5f8ff; }
.batch-item-list > li.is-reviving { border-color: #bcd0f5; }
.batch-item__seq { grid-column: 1; grid-row: 1; display: grid; place-items: center; width: 22px; height: 22px; border-radius: 50%; background: var(--panel); color: var(--muted); font-size: 11px; }
.batch-item__head { grid-column: 2; display: flex; align-items: baseline; justify-content: space-between; gap: 10px; min-width: 0; }
.batch-item__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.batch-item__reason { grid-column: 2; margin: 0; color: var(--muted); line-height: 1.55; overflow-wrap: anywhere; }
.batch-item__foot { grid-column: 2; display: flex; align-items: center; justify-content: space-between; gap: 8px 12px; flex-wrap: wrap; }
.batch-item__meta { color: var(--muted); font-size: 11px; }
.batch-item__actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex-wrap: wrap; margin-left: auto; }
.batch-item__confirm-copy, .batch-item__blocked { color: var(--muted); font-size: 11px; }
.batch-item__failure { grid-column: 2; margin: 0; color: var(--red); font-size: 11px; overflow-wrap: anywhere; }
.primary-button.button--sm, .secondary-button.button--sm { padding: 5px 11px; font-size: 12px; border-radius: 4px; }
.batch-confirm-facts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
.batch-confirm-facts > div { display: grid; gap: 4px; padding: 10px; background: var(--panel); border-radius: 5px; }
.batch-confirm-facts span { color: var(--muted); font-size: 11px; }
.merchant-detail-list { display: grid; gap: 10px; margin: 0; }
.merchant-detail-list div { display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 12px; }
.merchant-detail-list dt { color: var(--muted); }
.merchant-detail-list dd { margin: 0; overflow-wrap: anywhere; }
.merchant-detail-section { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--line); }
.merchant-detail-section p { margin: 8px 0 4px; line-height: 1.5; }
.dialog-heading { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 18px; }
.dialog-heading h2 { font-size: 19px; }
.file-row, .dialog-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 12px; }
.dialog-actions { justify-content: flex-end; margin-top: 20px; }
.info-box { margin-top: 12px; padding: 10px; border-radius: 5px; background: var(--panel); color: var(--muted); }
.dialog-copy { line-height: 1.7; }

.toast { position: fixed; left: 50%; bottom: 18px; transform: translate(-50%, 12px); padding: 10px 14px; border-radius: 5px; background: var(--ink); color: #fff; opacity: 0; pointer-events: none; transition: .2s; }
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* AI 配置页：模型与密钥。外框复用 .config-panel，这里只补字段栅格。 */
.ai-config-grid { display: grid; grid-template-columns: minmax(0, 260px) minmax(0, 1fr); gap: 18px; margin-top: 16px; }
.ai-field { display: grid; gap: 7px; min-width: 0; }
.ai-field > span { font-weight: 600; }
/* minmax(0,…) + width:100% + box-sizing：粘一把超长 Key 进来时不能把栅格撑破，
   Key 输入框一旦撑破容器，整页会出现横向滚动条（760px 下的溢出断言会直接报红）。 */
.ai-field select, .ai-field input { width: 100%; box-sizing: border-box; border: 1px solid var(--line); border-radius: 5px; padding: 9px 10px; outline-color: var(--blue); font: inherit; }
.ai-field small { color: var(--muted); line-height: 1.5; }
.ai-config-actions { justify-content: flex-end; gap: 12px; }
/* 「清除已保存的 Key」是破坏性操作，推到最左、与「保存」拉开距离，避免误点。 */
.ai-config-actions .text-button { margin-right: auto; }

/* AI 话术生成配置：参考话术 + 核心话术层 */
.copywriter-panel { margin-top: 22px; }
.copywriter-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin-top: 14px; }
.copywriter-field { display: grid; gap: 6px; min-width: 0; }
.copywriter-field > span, .copywriter-field > label { font-weight: 600; }
.copywriter-field textarea { width: 100%; box-sizing: border-box; resize: vertical; line-height: 1.6; }
.copywriter-field small { line-height: 1.5; }
.copywriter-points { margin-top: 22px; padding-top: 18px; border-top: 1px dashed var(--line); }
.copywriter-points .config-section-heading { align-items: baseline; gap: 10px; }
.point-list { display: grid; gap: 12px; margin-top: 12px; }
.point-card { border: 1px solid var(--line); border-radius: 6px; padding: 11px 13px; background: var(--panel); display: grid; gap: 8px; }
.point-card__head { display: flex; align-items: center; gap: 12px; }
.point-card__head strong { font-size: 12px; }
.point-card__mustsay { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; }
.point-card__mustsay input { width: 15px; height: 15px; accent-color: var(--blue); }
.point-card__head .text-button { margin-left: auto; }
.point-card__head .text-button.danger { color: var(--red); }
.point-card__label, .point-card textarea { width: 100%; box-sizing: border-box; background: #fff; }
.point-card textarea { resize: vertical; line-height: 1.55; }
.point-card__examples { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.point-card__examples label { display: grid; gap: 4px; }
.point-card__examples span { color: var(--muted); font-size: 11px; }

@media (max-width: 1000px) {
  .config-layout { grid-template-columns: 1fr; gap: 28px; }
  .image-config { border-left: 0; border-top: 1px solid var(--line); padding: 28px 0 0; }
  .batch-config-grid { grid-template-columns: 1fr 1fr; }
  .copywriter-grid { grid-template-columns: 1fr; }
  /* 模型下拉与 Key 输入并排在中屏开始变挤：Key 输入框至少要留得下 40 个字符的可见宽度。 */
  .ai-config-grid { grid-template-columns: 1fr; }
}

/* ── 执行器监控页 ───────────────────────────────────────────── */
/* 第一行工具条：自动刷新开关 / 立即刷新 / 注册说明入口。
   ① 左右外边距必须吃 --main-pad-inline，与下方概览卡、表格**同一条左沿**。
      这一行原来是未加任何样式的 <header class="heading">（本文件里根本没有 .heading 规则），
      左右内边距为 0，实测比概览卡和表格左沿各靠左 48px，人工验收原话是"明显太左侧"。
      （旧测试量的是"没贴边、没溢出"，贴边 0 也满足，所以这条错位一直没被量出来。
      现在 scripts/test-layout-regression-ui.mjs 直接断言这一行与概览卡左沿相等。）
   ② 上外边距 0：与页签之间的 12px 由 .view-tabs 自己的 margin-bottom 提供，
      和商家名单页的 .toolbar、采集需求页的 .collect-strip 同一个节奏；
      这里再加一段，执行器页的首块内容就比别的页低一截。 */
.executor-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 var(--main-pad-inline); }
/* 说明入口推到最右：左边两颗是"作用在列表数据上的动作"，右边这颗是"看说明书"，
   两者不该混成一组。margin-left:auto 而不是 justify-content:space-between ——
   窄屏换行后它仍然贴右，不需要额外规则。 */
.executor-toolbar__help { margin-left: auto; height: var(--control-height); padding: 0 4px; }
/* 自动刷新按钮的开关态。为什么不用复选框：勾选态在 12px 方框里读不出来，
   而这一行三件事全是动作。开 = 蓝底蓝字（和主色一致的"启用中"），
   关 = 回到普通 secondary 的白底描边 + 灰字，一眼能看出现在是不是在自动刷。 */
.executor-refresh-toggle[aria-pressed="true"] { background: var(--blue-soft); border-color: #bcdcff; color: var(--blue); }
.executor-refresh-toggle[aria-pressed="true"]:hover { background: #dfeaff; }
.executor-refresh-toggle[aria-pressed="false"] { color: var(--muted); }
.alert-banner { display: flex; gap: 12px; border: 1px solid var(--line); border-radius: 6px; padding: 14px 16px; margin: 20px var(--main-pad-inline) 0; }
.alert-banner--danger { border-color: #f0c8cd; background: #fdf4f5; }
.alert-banner__icon { color: var(--red); font-size: 15px; line-height: 1.5; }
.alert-banner__body { min-width: 0; }
.alert-banner__body strong { color: var(--red); }
.alert-banner__body p { margin-top: 6px; line-height: 1.6; }
.alert-banner__detail { margin-top: 10px; }
.alert-banner__detail summary { color: var(--blue); cursor: pointer; }
.alert-banner__detail ol { margin: 8px 0 0; padding-left: 20px; line-height: 1.8; }
/* 概览卡：跟在第一行工具条下面，20px 的间距表示"换了一块内容"。
   它自己不再承担首屏留白（那部分已由 .executor-toolbar 与页签间距表达）。 */
.executor-overview { display: flex; flex-wrap: wrap; gap: 10px; margin: 20px var(--main-pad-inline) 18px; }
.executor-overview .stat-card { flex: 1 1 150px; min-width: 0; }
.stat-card--wide { flex: 1 1 220px; }
.stat-card__label { display: block; color: var(--muted); font-size: 11px; }
.stat-card__value { display: block; font-size: 22px; margin-top: 4px; }
.stat-card__hint { display: block; color: var(--muted); font-size: 11px; margin-top: 6px; }
.stat-card--ok .stat-card__value { color: var(--green); }
.stat-card--warn .stat-card__value { color: var(--amber); }
.stat-card--danger .stat-card__value { color: var(--red); }
.executor-table-wrap table { min-width: 900px; }
/* 操作列（最后一列）：列宽由「详情」按钮撑开即可，表头沿用默认 11px 缩进，
   与其它表头文字对齐。**不要**套 .detail-control-heading（38px + padding-left:0），
   那是给"整列只有一个箭头图标"写的，套上去表头会比按钮左沿多缩进 11px。 */
.executor-actions-heading { white-space: nowrap; }
.executor-row-actions { white-space: nowrap; }
.executor-key strong, .executor-account strong, .executor-quota strong { display: block; }
/* 「今日配额」列不再有第二行小字（剩余 X 条 · 来源）后，这里只剩环境/账号两列需要它。 */
.executor-key small, .executor-account small { display: block; margin-top: 3px; font-size: 11px; }
.executor-status { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
.executor-status i { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); flex: 0 0 auto; }
.executor-status--ready { color: var(--green); }
.executor-status--ready i { background: var(--green); }
.executor-status--stale { color: var(--amber); }
.executor-status--stale i { background: var(--amber); }
.executor-status--pending { color: var(--muted); }
.executor-status--conflict { color: var(--red); font-weight: 600; }
.executor-status--conflict i { background: var(--red); }
.executor-tag { display: inline-block; border-radius: 3px; padding: 0 5px; font-size: 11px; font-weight: 400; }
.executor-tag--ok { background: #e7f4ec; color: var(--green); }
.executor-tag--warn { background: #fdf3e0; color: var(--amber); }
/* 「怎么把新环境注册进来」弹窗（原先是执行器页末尾的折叠面板，2026-09-21 收进弹窗）：
   步骤列表与两句 config-hint 是原样搬过来的，所以 .executor-help* 那几条规则继续生效，
   这里只补弹窗自身的宽度；宽度必须落在 dialog 上（内层 form 加宽会溢出 dialog）。 */
#executorHelpDialog { width: min(560px, 92vw); }
.executor-help__steps { margin: 12px 0 0; padding-left: 20px; line-height: 1.9; }
.executor-help .config-hint { margin-top: 10px; }
.executor-help code { background: var(--panel); border-radius: 3px; padding: 1px 5px; }
/* 宽度必须落在 dialog 本身：全局 dialog 被限成 430px，若把 560px 加在内层 form 上，
   form 会溢出到 dialog 盒子之外，超出部分被 backdrop 盖住，看起来就是"文字被压成灰条"。 */
#executorDetailDialog { width: min(560px, 92vw); }
.executor-detail-list { margin: 4px 0 0; }
.executor-detail-list > div { display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: 12px; padding: 9px 0; border-bottom: 1px solid var(--line); }
.executor-detail-list > div:last-child { border-bottom: 0; }
.executor-detail-list dt { color: var(--muted); }
.executor-detail-list dd { margin: 0; line-height: 1.6; }
/* 详情弹窗里的「账号级限制」编辑块（第三批）：与配置页同一套 number-field / time-field，
   只加一条上分隔线与操作行，不新造视觉语言。 */
.executor-limits { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--line); }
.executor-limits .batch-config-grid { margin-top: 10px; }
.executor-limits__actions { display: flex; align-items: center; gap: 12px; margin-top: 14px; flex-wrap: wrap; }
.executor-limits__actions .muted { font-size: 11px; }

@media (max-width: 760px) {
  .shell { grid-template-columns: 1fr; }
  .copywriter-grid, .point-card__examples { grid-template-columns: 1fr; }
  /* 窄屏内边距收窄：48px 的左右缩进在 390px 上会把可用宽度压到 264px，
     导致筛选条里"搜索 / 筛选 / 动作"被拆成四五行（实测）。
     改法是把 --main-pad-inline 调小并让 .main 回归 0 内边距——
     所有块的左右留白（页签、筛选条、选择条、动作行、表格、卡片）都挂在这一个变量上，
     改它一处即全部对齐；给 .main 加内边距反而会多出 15px 的双重留白，
     而且固定区吸顶时左侧会突然少一截。
     上下内边距必须保持 0：sticky 只相对滚动容器的 padding box 计算，
     容器上的上下内边距会变成吃不掉的顶部空档。 */
  :root { --main-pad-inline: 15px; --sticky-first-gap: 24px; }
  .main { padding: 0 0 30px; }
  /* 手机上没有 hover：执行器状态说明（.collect-executor-panel）只在宽屏悬停/聚焦时出现，
     这里显式禁用，避免"点一下卡片就把说明框弹出来又点不掉"。窄屏仍可读卡片下方的提示行。 */
  .collect-executor-panel { display: none !important; }
  .stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .run-detail { padding: 16px; }
  .run-detail__topline { grid-template-columns: 1fr; gap: 18px; }
  .run-detail__business { grid-template-columns: 1fr 1fr; gap: 12px; }
  .run-step-grid { grid-template-columns: repeat(2, minmax(125px, 1fr)); }
  .run-diagnostic dl { grid-template-columns: 76px minmax(0, 1fr); }
  .image-grid { grid-template-columns: repeat(auto-fill, minmax(128px, 1fr)); }
  .image-card--default { flex-direction: column; align-items: stretch; }
  .image-card--default img { width: 100%; }
  .config-actions { align-items: flex-start; flex-direction: column; }
  /* 竖排后"清除在左、保存在右"的横向语义消失，auto 外边距会把按钮顶出一条空隙。 */
  .ai-config-actions .text-button { margin-right: 0; }
  .ai-config-actions .primary-button, .ai-config-actions .secondary-button { width: 100%; }
  .config-actionbar { align-items: stretch; flex-direction: column; }
  /* 但**吸顶保存条不能竖排**：页签下面的吸顶块高度是"吃掉"的阅读区，
     竖排后它会在手机上占掉大半个首屏。横向排列下「保存配置」约 110px、
     状态文字可换行，与品牌区并排放得下（同样空间首屏可多显示约 70px）。 */
  .config-head .config-actionbar { align-items: center; flex-direction: row; }
  .config-head .config-actionbar__status { flex: 1; justify-content: flex-start; }
  .config-head .config-actionbar .primary-button { flex: 0 0 auto; }
  .config-actionbar__status { justify-content: flex-start; }
  .selection-toolbar { align-items: flex-start; flex-wrap: wrap; gap: 8px 14px; }
  .selection-toolbar .text-button { margin-left: 0; }
  .selection-actions { margin-left: 0; flex-wrap: wrap; }
  .collect-stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
  .collect-stat-card { min-height: 72px; padding: 12px; }
  .collect-stat-card strong { font-size: 20px; }
  .collect-section-heading { flex-direction: column; gap: 8px; }
  .collect-section-meta { justify-items: start; text-align: left; }
  .batch-config-grid, .batch-confirm-facts, .batch-summary-grid { grid-template-columns: 1fr 1fr; }
  .batch-dialog-card .dialog-heading { position: static; padding: 0; margin: 0 0 18px; }
  .batch-cooldown, .batch-current-heading { align-items: flex-start; flex-direction: column; }
}

/* ── 「采集需求」页与执行器页的采集设置 ───────────────────────────────────── */

/* 派发概览 = 采集需求页的**上半部分**，也是这一页唯一的吸顶块（原先是"动作行 + 概览卡"两层）：
     左：四张数字卡（执行器工作中 / 任务可派发 / 任务进行中 / 任务冷却中）
     右：三个页面动作（规则说明 / ↻ / + 新建采集需求）
   两者合成一行（人工要求 2026-09-21）：它们本来就是一行文字能表达的东西，
   分成上下两行时页签下方是两条独立横带，视觉上碎、也多占一行高度。
   吸顶的意义：不吸顶时页面一滚，「+ 新建采集需求」跟着滚走，运营要跑回页顶才能新建。
   吸附位置 = 页签吸顶后占的高度（--sticky-tabs-band，脚本实测写回），z-index 与
   .merchant-fixed 同级（都在页签 z-index 3 之下），背景必须白：吸顶时下面滚的是表格。
   顶部边距 12：页签下沿已经留了 12px，这块再留 20px 会变成两层空带。 */
.collect-strip {
  position: sticky;
  top: var(--sticky-tabs-band, 40px);
  z-index: 2;
  background: #fff;
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 8px;
  margin: 12px var(--main-pad-inline) 0;
}
/* 卡片区是说明框的定位基准：说明框挂在它下面（作为第一张卡的同级节点），
   这样"贴着第一张卡右沿、右沿对齐卡片区"是纯 CSS 能表达的，
   而塞进卡片里就会往左溢出到侧边栏下面（实测：先被 NAV.sidebar 盖掉半张）。
   min-width 0：没有它，网格里 nowrap 的大数字会把整行顶宽（横向溢出）。
   align-self: center —— 卡片区**不跟着这一行的高度被拉高**：
   `.collect-strip` 是 `align-items: stretch`，块高由里面最高的东西决定（下过雨的提示行一换行
   就能把卡片拉到 100px），于是卡片比动作区高一截、两块看着对不齐。
   改成"按内容高（80px）居中"，卡片与两行按钮（84px）的中心对齐，只差 4px。 */
.collect-strip__cards { position: relative; flex: 1 1 auto; min-width: 0; align-self: center; }
/* 动作区固定**两行**（人工 2026-09-21 第二条口径）：①「规则 + ↻」②「+ 新建采集需求」。
   为什么不是三颗一行：一行铺开要吃掉约 295px，卡片区只能拿剩下的宽度；两行后整体高度
   39×2+6 = 84px，与卡片（80px）基本同高——两块才像"同一块"（实测差 4px，比原来差 20px 好得多）。
   用 **grid 定两列**而不是 flex 比例：两行等宽要求"第一行两颗之和 = 主按钮宽"，
   在 flex 里这等于让两行的参数互相解方程，实测直接翻车——「规则」被 `min-width: 0` 压成
   34px（与 ↻ 同宽）、三颗各占一行。定两列之后第一列 = 动作区宽 − 34 − 6，是确定值。
   动作区宽度由 app.js 实测主按钮写进 `--collect-actions-width`
   （本值是首屏兜底，主按钮还没渲染时也不塌）。
   `align-self: center`：卡片区按内容高居中了，动作区也要居中，两块的中心才对得上。 */
.collect-strip__actions {
  flex: 0 0 auto;
  align-self: center;
  width: var(--collect-actions-width, 147px);
  display: grid;
  grid-template-columns: minmax(0, 1fr) 34px;
  gap: 6px;
}
/* 主按钮跨两列 ⇒ 它的宽度恒等于动作区宽度，"两行等宽"就是这一条的直接结果。 */
.collect-strip__actions > .primary-button { grid-column: 1 / -1; }
/* 注意：不要给这里的 ↻ 加 padding/width 覆盖——它必须与「商家名单」页的 ↻ 逐项同形
   （`test-view-header-and-actions-ui` 的用例4 会把两者的计算样式逐项比对）。
   它落在定宽 34px 列里、自身也是 34px，本来就不需要额外约束。 */
.collect-stats-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
}
/* 四卡在窄屏（含 760px 视口）收成两列：每列过窄时 24px 的数字会被 ellipsis 截成"…"。 */
@media (max-width: 860px) {
  .collect-stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* 窄屏放不下"两列卡 + 动作组"，动作整组换到卡片下方、占满一行并贴右沿；
     列宽改由内容决定（两行不再强求等宽——贴内容区右沿比"两行严格同宽"更整齐）。
     卡片区这里必须显式 `align-self: stretch` 覆盖"按内容高居中"：容器在窄屏是**纵向**换行，
     交叉轴变成水平方向，`align-self: center` 会把卡片区缩成内容宽、不再占满整行。 */
  .collect-strip__cards { flex: 1 1 100%; align-self: stretch; }
  /* 窄屏提示行已经独占自己的行，不再需要 basis 100%（否则会多占一次换行）。 */
  .collect-strip__hint { flex: 1 1 auto; }
  .collect-strip__actions {
    flex: 1 0 100%;
    width: auto;
    grid-template-columns: max-content max-content;
    justify-content: end;
  }
}
/* 手机宽度：主按钮占满整行（手指目标更大），第一行「规则 + ↻」仍贴右沿。 */
@media (max-width: 520px) {
  .collect-strip__actions { grid-template-columns: minmax(0, 1fr) 34px; }
  .collect-strip__actions > .primary-button { width: 100%; }
}
.collect-stat-card {
  position: relative;
  min-width: 0;
  /* 84px = 右侧动作区的高度（两行按钮 39×2 + 间距 6），两块因此**完全等高**（上下沿也齐）。
     人工 2026-09-21（"调整下卡片的高度"）：原来卡片 80px、动作区 84px，虽然中心对齐，
     但上下沿各错 2px，看着仍像两个件。改高度只钉这一处，不动全局 --control-height
     （按钮高度是全站口径，碰它会牵连别的页面）。 */
  min-height: 84px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 6px;
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: #fff;
}
/* 执行器状态卡：它是"需求会不会被领走"的前提，用一个区别于三个需求计数的颜色。
   它还是说明框的**触发点**：悬停或键盘聚焦（tabindex="0"）时展开逐台状态。
   卡片自己不用 position：说明框的定位基准是上面的 .collect-strip__cards。 */
.collect-stat-card--executor { cursor: help; }
.collect-stat-card--executor:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
/* 悬停/键盘聚焦时把整个卡片区抬到最上层：说明框是绝对定位节点，
   否则会被**右侧那几张卡**（同层、DOM 在后）盖掉。
   卡片区自己一个层叠层（z-index 4）、说明框在它内部（5），
   两者都高于 .collect-strip 的 z-index 2，吸顶时也不会被压住。 */
.collect-strip__cards:hover, .collect-strip__cards:focus-within { z-index: 4; }
.collect-stat-card strong {
  overflow: hidden;
  color: var(--ink);
  font-size: 24px;
  line-height: 1;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.collect-stat-card span { color: var(--muted); font-size: 11px; }
.collect-stat-card--warning strong { color: var(--amber); }
.collect-stat-card--success strong { color: var(--green); }
/* 执行器状态卡：它是"需求会不会被领走"的前提，用一个区别于三个需求计数的颜色。 */
.collect-stat-card--executor strong { color: var(--blue); }

/* 执行器状态说明：贴在卡片区下方、左沿与第一张卡对齐、右沿贴卡片区的右沿。
   它替代原先常显在卡片下方的「⚠ 为什么还有执行器没在跑」告示牌——那段话本来就是这张卡的注解，
   常显时每次刷新都在页面上占 3~5 行，而它只在"有人没跑"时才有信息量。
   为什么右沿贴卡片区右沿：说明框 420px 比一张卡（约 210~290px）宽得多，
   若让它从卡片左沿往右铺，在 1280 这类视口会把第三、四张卡整片盖住；
   改成右沿对齐后它压在第三、四张卡的位置上，"谁被压住"是固定的，
   宽度也天然被卡片区夹住（`left:auto` + `right:0` + `max-width:100%` 不会越界）。
   用 CSS :hover 而不是 JS 显隐：说明内容随轮询重渲染，JS 维护显隐状态会让"鼠标停在卡片上时
   内容刚被换掉"这类时序问题跑出来；纯 CSS 的显隐不持有任何状态。
   触发条件是**第一张卡**（`:first-child`）：只有它旁边有说明框。 */
.collect-executor-panel {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 5;
  display: none;
  width: 420px;
  max-width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: #fff;
  box-shadow: 0 8px 24px rgba(15, 23, 42, .12);
  color: var(--ink);
  font-size: 12px;
  font-weight: 400;
  line-height: 1.6;
  text-align: left;
  white-space: normal;
}
/* 触发条件是**第一张卡**的悬停/聚焦。卡片在 `.collect-stats-grid` 里、说明框在卡片区下，
   两者不是兄弟节点，所以用 `:has()` 往上找（Chrome 105+，本仓库现场用 Chrome 153）。
   `:focus-within` 覆盖键盘 Tab 到卡片那一档（卡片带 tabindex="0"）。 */
.collect-strip__cards:has(.collect-stat-card--executor:hover) .collect-executor-panel,
.collect-strip__cards:has(.collect-stat-card--executor:focus-within) .collect-executor-panel { display: block; }
.collect-executor-panel__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 8px;
  padding-bottom: 6px;
  border-bottom: 1px solid #eef1f5;
}
.collect-executor-panel__head strong { color: var(--ink); font-size: 12px; overflow: visible; white-space: normal; }
.collect-executor-panel__head span { color: var(--muted); font-size: 11px; }
.collect-executor-panel__note { margin: 6px 0 0; color: var(--muted); }
.collect-executor-list { margin: 6px 0 0; padding: 0; list-style: none; }
.collect-executor-list__item {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  padding: 2px 0;
}
.collect-executor-list__item code { color: var(--ink); font-size: 11px; }
.collect-executor-list__status { color: var(--muted); }
.collect-executor-list__meta { color: var(--muted); font-size: 11px; }
/* 正常在跑：绿字（与"任务进行中"同色），扫一眼就知道这台不用管。 */
.collect-executor-list__item.is-running .collect-executor-list__status { color: var(--green); }
/* 在线但没在跑：橙字，是列表里唯一需要人动手的一档。 */
.collect-executor-list__item.is-attention .collect-executor-list__status { color: #a86a00; }
/* 离线：整行压暗（与原先告示牌里的 .is-offline 一致，不再另起一套表达）。 */
.collect-executor-list__item:not(.is-running):not(.is-attention) { opacity: .6; }
.collect-executor-list__rest { color: var(--muted); padding: 2px 0; }
.collect-strip__hint {
  /* 概览块的第三个 flex 项：占满一行 ⇒ 自然落到"卡片 + 动作"那一行的下方。
     它**不能**待在卡片区里：那样会把卡片区撑到 100px（四卡 80 + 提示 20），
     比右侧两行动作区高，两块对不齐（人工 2026-09-21）。 */
  flex: 1 1 100%;
  padding: 0 2px;
  font-size: 12px;
  line-height: 1.7;
}
/* 没有提示时的空节点不参与布局（样式表加载慢于 HTML 的那一瞬间也安全）。 */
.collect-strip__hint:empty { display: none; }
/* [hidden] 必须显式压过上面的 `flex: 1 1 100%`（用户代理样式表里是 display:none，
   但任何带 display 的作者样式都会赢过它）。 */
.collect-strip__hint[hidden] { display: none; }

.collect-config__grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(180px, 1fr));
  gap: 14px;
}
.collect-config__grid .field-label { display: flex; flex-direction: column; gap: 4px; }
.collect-config__grid input { width: 100%; }
.collect-config__grid small { font-size: 11px; line-height: 1.5; }
.collect-config__actions {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 16px;
}

.collect-section { margin-top: 22px; }
.collect-content-tabs {
  display: flex;
  align-items: center;
  gap: 4px;
  margin: 20px var(--main-pad-inline) 0;
  border-bottom: 1px solid var(--line);
}
.collect-content-tab {
  border: 0;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  padding: 9px 12px 10px;
  background: transparent;
  color: var(--muted);
  font-weight: 600;
  white-space: nowrap;
}
.collect-content-tab:hover { color: var(--blue); }
.collect-content-tab.is-active { border-bottom-color: var(--blue); color: var(--blue); }
.collect-section--tasks { margin-top: 22px; }
.collect-section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin: 0 var(--main-pad-inline) 10px; }
.collect-section-heading__main { display: flex; align-items: flex-start; gap: 10px; min-width: 0; }
.collect-section-kicker { flex: 0 0 auto; margin-top: 2px; padding: 3px 7px; border-radius: 4px; background: var(--blue-soft); color: var(--blue); font-size: 11px; font-weight: 600; line-height: 1.4; }
.collect-section-kicker--task { background: #f1f3f6; color: #667085; }
.collect-section-description { max-width: 760px; margin-top: 4px; color: var(--muted); font-size: 12px; line-height: 1.55; }
.collect-section-meta { display: grid; flex: 0 0 auto; gap: 3px; justify-items: end; color: var(--muted); font-size: 11px; line-height: 1.4; text-align: right; }
.collect-section-meta strong { color: var(--ink); font-size: 13px; }
/* 采集需求表最后一列（行内动作按钮）的**列头**：原来这一格是 `<span class="sr-only">操作</span>`，
   视觉上就是个空表头 —— 运营扫到最右边那排「手动派发 / 暂停 / 置顶 / 终止 / 删除」，
   读不出这列是什么（人工验收 2026-10-01 提出）。现在直接写出「操作」。
   刻意**不复用** `.detail-control-heading`：那条是给 38px 宽的"详情"箭头列写的
   （width:38px + padding-left:0），套在这一列上会让表头文字与下面的按钮左沿错位；
   这一列的宽度本来就由按钮组撑开（约 145px），表头沿用默认 11px 缩进即可与其它表头对齐。 */
.collect-recipe-actions-heading { white-space: nowrap; }
.collect-row-actions { white-space: nowrap; }
.collect-row-actions .text-button { margin-right: 8px; }
.collect-row-actions .text-button:last-child { margin-right: 0; }
/* 执行记录的"需求来源"标记：解释"为什么有执行记录、任务池却是空的"。 */
.collect-origin {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 6px;
  border: 1px solid #d8dee8;
  border-radius: 3px;
  background: #f4f6f9;
  color: #667085;
  font-size: 10px;
  line-height: 1.6;
  white-space: nowrap;
  vertical-align: middle;
}

@media (max-width: 760px) {
  .collect-stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* 采集页的两个筛选工具条（任务池「状态」/ 执行记录「状态 + 执行器」）**共用一份定义**：
   同一页里两个筛选条只要有一处字号、内边距或左边距不同，看起来就是"没对齐"，所以不给它们各写一份。
   左边距必须是 `--main-pad-inline`，与下方 .table-wrap 完全一致——少这一道的后果是
   筛选条贴到内容区最左边，与表格首列错开一截（原「显示已完成」勾选框就是这么错的）。 */
.collect-task-toolbar,
.collect-recipe-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 18px;
  margin: 0 var(--main-pad-inline) 10px;
}
.collect-task-toolbar__field,
.collect-recipe-toolbar__field {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--muted);
  font-size: 12px;
}
.collect-task-toolbar__field select,
.collect-recipe-toolbar__field select {
  min-width: 150px;
  padding: 5px 8px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: #fff;
  color: var(--ink);
  font-size: 12px;
}
.collect-task-more {
  display: flex;
  justify-content: center;
  margin: 12px var(--main-pad-inline) 0;
}

/* 执行记录详情：dl 两列（标签 / 值），值列允许长 id 换行不撑破弹窗。 */
.collect-task-detail-card { width: min(560px, 92vw); }
.collect-task-detail { display: grid; gap: 0; margin: 4px 0 0; }
.collect-task-detail > div {
  display: grid;
  grid-template-columns: 88px minmax(0, 1fr);
  gap: 10px;
  padding: 7px 0;
  border-bottom: 1px solid #f0f1f3;
}
.collect-task-detail > div:last-child { border-bottom: 0; }
.collect-task-detail dt { color: var(--muted); font-size: 12px; }
.collect-task-detail dd {
  margin: 0;
  overflow-wrap: anywhere;
  color: var(--ink);
  font-size: 12px;
  line-height: 1.6;
}
.text-button--danger { color: var(--danger, #d93025); }

/* 条件/任务状态徽标：四态各自可辨，颜色只做辅助，文字是主要信息。
   `white-space: nowrap`：徽标文字**一律不许折行**。状态列窄（尤其"阶段"列还要在下面
   挂一行小字）时，`inline-block` 会把「准备筛选·推断」断成「准备筛选·推 / 断」——
   那是这个词组里最不该断的地方，读起来像两个不同的阶段。 */
.collect-status {
  display: inline-block;
  padding: 1px 8px;
  border-radius: 10px;
  font-size: 12px;
  line-height: 18px;
  border: 1px solid transparent;
  white-space: nowrap;
}
.collect-status--queued { background: #f2f3f5; color: #4e5969; }
.collect-status--running { background: #e8f3ff; color: #1d6fd0; border-color: #bcdcff; }
/* `paused` 是**两张表共用**的类名（需求级「已暂停」+ 任务级「待人工处理」），语义都是"要人管、可恢复"，
   所以统一给紫色：它必须和"等一会儿就好"的冷却中（琥珀）分开，否则运营看不出哪些要自己动手。 */
.collect-status--paused { background: #f3efff; color: #6b4bd6; border-color: #ddd2ff; }
.collect-status--archived, .collect-status--done { background: #f1f3f6; color: #667085; border-color: #e2e6ec; }
/* 需求派发状态：**七态各自一个色相**，不共用。类名与后端 `dispatchState` 取值逐字一致，不做映射表。
   为什么必须七色分开（人工验收 2026-09-21 指出）：原来「可派发 / 已完成 / 已终止」都是同一个绿，
   运营扫一眼分不出"还能跑的"和"已经结束的"；而「已暂停」与「冷却中」都是琥珀，
   分不出"要人管"和"等一会儿就好"。现在按**四类语义**配色：
     绿 = 就绪可跑（可派发）        蓝 = 正在跑（进行中）
     琥珀 = 等时间，会自动好（冷却中） 紫 = 要人决定，可恢复（已暂停）
     红 = 要人修（占用异常）        青绿 = 成功终态（已完成）   灰 = 人工放弃，历史（已终止） */
.collect-status--dispatchable { background: #e8f8ef; color: #1a7f47; border-color: #b7e4c8; }
.collect-status--in_flight { background: #e8f3ff; color: #1d6fd0; border-color: #bcdcff; }
.collect-status--cooling { background: #fff7e6; color: #a86a00; border-color: #ffe0a3; }
.collect-status--stale_hold { background: #fdecec; color: #c0392b; border-color: #f7c6c6; }
.collect-status--completed { background: #e3f6f0; color: #0f766e; border-color: #b7e6da; }
.collect-status--archived { background: #f1f3f6; color: #667085; border-color: #e2e6ec; }

/* 采集**执行记录**的阶段色与失败分类色（2026-09-21 新增）。
   为什么阶段必须配色而不能只有文字（人工验收原话："不同阶段的状态没有颜色标签"）：
   一张记录表里失败往往占多数，全是同一个灰标签时，"跑到建联才失败"和"连筛选都没进去"
   在扫视时完全没有区别——而这两件事的排查方向恰好相反（前者看商家/风控，后者看页面/扩展）。
   配色按**语义**分，不按漂亮分：
     灰   = 还没进入采集（准备筛选）
     蓝   = 在采集（滚列表、去重）
     琥珀 = 等外部条件（等中心），会自动好
     青   = 在发消息（建联中，风险最高的动作，单独一个色相）
     青绿 = 收尾完成（成功终态）
   分类色（`--kind-*`）给「终态原因」列用，与失败处置表的四类一一对应：
     红 = 要人处理（风控）  琥珀 = 要改需求（配置）  蓝 = 环境瞬时可重试  灰 = 中心回收 */
.collect-status--preparing { background: #f2f3f5; color: #4e5969; border-color: #e2e6ec; }
.collect-status--collecting { background: #e8f3ff; color: #1d6fd0; border-color: #bcdcff; }
.collect-status--waiting_center { background: #fff7e6; color: #a86a00; border-color: #ffe0a3; }
.collect-status--building { background: #e6f6f9; color: #0e7490; border-color: #b6e3ec; }
.collect-status--done { background: #e3f6f0; color: #0f766e; border-color: #b7e6da; }
.collect-status--unknown { background: #f6f7f9; color: #98a2b3; border-color: #e6e8ec; }
.collect-status--kind-environment { background: #e8f3ff; color: #1d6fd0; border-color: #bcdcff; }
.collect-status--kind-center { background: #f1f3f6; color: #667085; border-color: #e2e6ec; }
.collect-status--kind-risk { background: #fdecec; color: #c0392b; border-color: #f7c6c6; }
.collect-status--kind-recipe { background: #fff7e6; color: #a86a00; border-color: #ffe0a3; }
.collect-status--kind-business { background: #e3f6f0; color: #0f766e; border-color: #b7e6da; }

/* **推断值**必须一眼看出与真实记录不同：虚线边框 + 「·推断」后缀。
   现场 13 条失败里 11 条没有阶段上报，页面按失败原因反推——不标出来的话，
   运营会把推断当成事实，一旦发现是猜的，之后连真的也不信了。 */
.collect-status.is-inferred { border-style: dashed; }
.collect-inferred-mark { font-style: normal; font-size: 10px; opacity: 0.75; }

/* 「终态原因」列：分类徽标 + 中文文案，**同一行**（前面标签、后面具体原因——人工验收 2026-09-21 要求）。
   文案允许在标签**右侧**换行（长句子截断会丢关键信息），但标签不得被挤成单独一行：
   所以去掉 `flex-wrap`，改由文案自己收缩换行（`min-width: 0` 是 flex item 能收缩的前提）。 */
.collect-reason-cell { display: flex; align-items: baseline; gap: 6px; min-width: 180px; }
.collect-reason-cell__text { font-size: 12px; line-height: 1.5; min-width: 0; }
.collect-reason-cell .collect-status { flex: none; }

/* 「候选 / 接受 / 已建联」列：小字只在不占位时才出现（本次没收到计数上报时的说明）。 */
.collect-counts-cell small { display: block; font-size: 11px; line-height: 1.4; white-space: nowrap; }

/* 执行记录工具条里的「阶段说明」入口：与筛选下拉同一行、垂直居中，
   刻意**不做成按钮形**（它是一个说明入口，不是动作），只加大一点可点区域。 */
.collect-task-toolbar__help { padding: 2px 6px; font-size: 12px; }

/* 「阶段说明」弹窗：色卡 + 阶段原名 + 含义。 */
.collect-phase-help-card { width: min(520px, 92vw); }
.collect-phase-help { display: grid; gap: 10px; margin: 4px 0 0; padding: 0; list-style: none; }
.collect-phase-help li { display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: 10px; align-items: baseline; }
.collect-phase-help li strong { display: block; color: var(--muted); font-size: 11px; font-weight: 500; }
.collect-phase-help li small { font-size: 12px; line-height: 1.5; }
.collect-phase-help__note { margin-top: 12px; font-size: 12px; line-height: 1.6; }

/* 终态（已完成 / 已终止）整行淡化：它们排在列表末尾，不该和待处理的抢注意力。 */
tr.is-terminal td { opacity: 0.62; }

/* 置顶行：**底色 + 左侧竖条**与其它行区分（人工要求"底框样式不一样"）。
   为什么用底色而不是只加一个「置顶」小标签：长表格里整行底色比单点标记更容易被扫到；
   而且置顶行可能同时是终态（`is-terminal` 的淡化会叠加），底色能保证它仍然看得出来。
   注：表格里**不再显示「优先级」列**——它是"置顶"的内部实现，露出来只会让人以为要手工调数值。 */
tr.is-pinned td { background: #fff9e8; }
tr.is-pinned td:first-child { box-shadow: inset 3px 0 0 #f0b429; }

/* 状态列：标签 + 一行小字（冷却剩余 / 自动暂停）。
   小字存在的意义就是"不用悬停也能读到"——冷却剩余时间以前只在 title 里，运营看得到"在冷却"、
   看不到"还要等多久"，只能一个个去悬停。 */
.collect-status-cell { display: grid; gap: 3px; justify-items: start; }
.collect-status-cell small { font-size: 11px; line-height: 1.3; white-space: nowrap; }

/* 「累计建联」列：上行 `12 / 200`、下行小字"成功 8"。两个数口径不同，必须一起显示。 */
.collect-accepted-cell { white-space: nowrap; }
.collect-accepted-cell small { display: block; font-size: 11px; line-height: 1.4; }

/* 采集任务池工具行（状态筛选）的盒子与字段样式**与执行记录工具条同源**，
   定义在上面 `.collect-task-toolbar` 那一组选择器里，这里不再写第二份。 */

/* 弹窗里的「目标建联人数」输入：沿用「建联配置」页 .number-field 的 `div > input + em` 观感。 */
.collect-target-row { display: inline-flex; align-items: center; gap: 6px; }
.collect-target-row input { width: 120px; }
.collect-target-row em { font-style: normal; color: var(--muted); font-size: 13px; }

.collect-form-error { color: var(--danger, #d93025); font-size: 13px; margin: 10px 0 0; }

#collectCategoryPicker { width: 100%; min-height: 150px; }

/* 执行器页：分组输入 + 自动执行开关 */
.executor-group-input {
  width: 90px;
  padding: 3px 6px;
  border: 1px solid var(--border, #e4e6eb);
  border-radius: 6px;
  font-size: 13px;
}
.executor-switch { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
.executor-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.executor-switch__track {
  position: relative;
  width: 34px;
  height: 18px;
  border-radius: 9px;
  background: #d5d8dd;
  transition: background .15s ease;
  flex: none;
}
.executor-switch__track::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #fff;
  transition: transform .15s ease;
}
.executor-switch input:checked + .executor-switch__track { background: #1d6fd0; }
.executor-switch input:checked + .executor-switch__track::after { transform: translateX(16px); }
.executor-switch input:disabled + .executor-switch__track { opacity: .5; }
.executor-switch__label { font-size: 12px; color: #4e5969; }

@media (max-width: 720px) {
  .collect-config__grid { grid-template-columns: 1fr; }
}

/* ── 新建采集条件的「三级级联」 ─────────────────────────────────────────────── */

#collectRecipeDialog { width: min(640px, calc(100vw - 32px)); }
#collectRecipeDialog .dialog-card { max-height: min(820px, calc(100vh - 32px)); overflow: auto; padding: 24px 26px 20px; }
.collect-recipe-dialog-heading { align-items: flex-start; margin-bottom: 22px; }
.collect-recipe-dialog-title { display: grid; gap: 4px; min-width: 0; }
.collect-recipe-dialog-title h2 { font-size: 22px; letter-spacing: 0; line-height: 1.25; }
.collect-recipe-dialog-title .muted { max-width: 460px; line-height: 1.55; }
.collect-recipe-preview__eyebrow { color: var(--blue); font-size: 11px; font-weight: 700; letter-spacing: .02em; line-height: 1.3; }

.collect-recipe-module { margin-top: 14px; border: 1px solid #e1e7ef; border-radius: 8px; background: #fff; overflow: hidden; }
.collect-recipe-module:first-of-type { margin-top: 0; }
.collect-recipe-module__heading { display: flex; align-items: center; min-height: 44px; padding: 0 16px; border-bottom: 1px solid #edf0f4; background: #fbfcfe; }
.collect-recipe-module__heading h3 { font-size: 14px; line-height: 1.3; }
.collect-recipe-module__body { padding: 0 16px 16px; }

.collect-recipe-step { padding: 18px 0 20px; border-top: 1px solid #edf0f4; }
.collect-recipe-step:first-of-type { padding-top: 12px; border-top: 0; }
.collect-recipe-field-label { margin-top: 0; margin-bottom: 8px; }

.collect-cascade {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  align-items: end;
  margin-top: 0;
}
.collect-cascade__col { display: flex; flex-direction: column; gap: 4px; }
.collect-cascade__col > span { color: #667085; font-size: 11px; font-weight: 600; }
.collect-cascade__col select { width: 100%; min-height: 38px; padding: 0 10px; border-color: #dbe2eb; border-radius: 6px; }
.collect-cascade__col select:focus,
.collect-recipe-field select:focus,
.collect-target-row input:focus { border-color: var(--blue); outline: 3px solid rgba(33, 104, 232, .12); }
.collect-cascade__hint { min-height: 18px; margin: 8px 0 0; font-size: 11px; line-height: 1.45; }

.collect-recipe-target-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(220px, .86fr); gap: 18px; }
.collect-recipe-field { min-width: 0; }
.collect-recipe-field .field-label { margin: 0 0 7px; }
.collect-recipe-field select { width: 100%; min-height: 38px; padding: 0 10px; border-color: #dbe2eb; border-radius: 6px; }
.field-optional { margin-left: 4px; color: var(--muted); font-size: 11px; font-weight: 400; }
.collect-target-row { display: flex; align-items: center; gap: 8px; }
.collect-target-row input { width: 100%; min-height: 38px; padding: 0 10px; border: 1px solid #dbe2eb; border-radius: 6px; color: var(--ink); }
.collect-target-row em { flex: 0 0 auto; color: var(--muted); font-size: 13px; font-style: normal; }

.collect-recipe-preview { margin: 2px 0 14px; padding: 14px 16px; border: 1px solid #cfe0ff; border-radius: 8px; background: linear-gradient(180deg, #f7faff 0%, #f1f6ff 100%); }
.collect-recipe-preview__heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.collect-recipe-preview__heading > div { display: grid; gap: 3px; }
.collect-recipe-preview__heading strong { font-size: 13px; line-height: 1.3; }
.collect-recipe-preview__state { flex: 0 0 auto; padding: 3px 7px; border-radius: 99px; background: #e3edff; color: var(--blue); font-size: 11px; line-height: 1.3; }
.collect-recipe-preview__state.is-ready { background: #e8f8ef; color: var(--green); }
.collect-recipe-preview__grid { display: grid; grid-template-columns: minmax(0, 1.5fr) repeat(2, minmax(100px, .7fr)); gap: 12px; margin-top: 13px; }
.collect-recipe-preview__grid > div { min-width: 0; display: grid; gap: 4px; }
.collect-recipe-preview__grid span { color: #71809a; font-size: 11px; }
.collect-recipe-preview__grid strong { overflow: hidden; color: var(--ink); font-size: 12px; line-height: 1.45; text-overflow: ellipsis; white-space: nowrap; }

.collect-recipe-flow { display: flex; align-items: center; gap: 10px; margin: 0 0 12px; padding: 10px 12px; border: 1px solid #e4ebf7; border-radius: 7px; background: #fbfcff; }
.collect-recipe-flow__item { display: inline-flex; min-width: 0; align-items: center; gap: 7px; color: #4d5c70; font-size: 11px; white-space: nowrap; }
.collect-recipe-flow__item > span { display: inline-grid; flex: 0 0 23px; place-items: center; width: 23px; height: 23px; border-radius: 6px; background: var(--blue-soft); color: var(--blue); font-size: 10px; font-weight: 700; }
.collect-recipe-flow__arrow { flex: 1 1 auto; min-width: 18px; color: #9aa8bc; font-size: 16px; line-height: 1; text-align: center; }
.collect-recipe-note { display: grid; gap: 9px; margin: 0; padding: 11px 12px; border: 1px solid #e6ebf2; border-radius: 7px; background: #f7f9fc; color: #697586; font-size: 11px; line-height: 1.6; }
.collect-recipe-note ul { min-width: 0; margin: 0; padding-left: 16px; }
.collect-recipe-note li + li { margin-top: 3px; }
.collect-recipe-note__secondary { padding-top: 9px !important; border-top: 1px solid #e6ebf2; }
.collect-recipe-note strong { color: #4d5c70; }
.collect-recipe-actions { position: sticky; bottom: -1px; z-index: 2; margin: 18px -26px -20px; padding: 16px 26px 20px; border-top: 1px solid #edf0f4; background: #fff; box-shadow: 0 -8px 14px -14px rgba(25, 35, 49, .45); }
.collect-recipe-actions .primary-button { min-width: 104px; }
.collect-form-error { padding: 9px 11px; border: 1px solid #f4c6cb; border-radius: 6px; background: #fff5f6; line-height: 1.5; scroll-margin-bottom: 76px; }

/* 说明与采集参数一样保留完整模块底板；两组规则列表统一收进同一块提示底板。 */
.collect-recipe-module--help .collect-recipe-module__body { padding-top: 10px; }

/* 条件行里的「派发」是主动作，但比主按钮轻一档：表格里放满蓝色按钮会看不清重点。 */
.secondary-button--compact { padding: 3px 10px; font-size: 12px; }

@media (max-width: 720px) {
  .collect-cascade { grid-template-columns: 1fr; }
  #collectRecipeDialog .dialog-card { padding: 20px 18px 16px; }
  .collect-recipe-dialog-title h2 { font-size: 20px; }
  .collect-recipe-module__heading { min-height: 42px; padding: 0 12px; }
  .collect-recipe-module__body { padding: 0 12px 12px; }
  .collect-recipe-target-grid, .collect-recipe-preview__grid { grid-template-columns: 1fr; gap: 12px; }
  .collect-recipe-preview__grid strong { white-space: normal; }
  .collect-recipe-actions { position: static; flex-wrap: wrap; margin: 18px 0 0; padding: 16px 0 0; background: transparent; box-shadow: none; }
  .collect-recipe-actions button { flex: 1 1 140px; }
}

/* ── 中心服务改造：登录覆盖层 / 待审批 / 待人工确认（docs/中心服务改造 §2.2）───────── */

/* 页签角标：待人工确认的条数。它的价值就在于"被看见"——只在切到任务记录页才拉，
   等于运营不主动去看就永远不知道有东西等着处理。 */
.view-tab__badge {
  display: inline-block;
  margin-left: 6px;
  min-width: 16px;
  padding: 0 4px;
  border-radius: 9px;
  background: var(--red, #c73a48);
  color: #fff;
  font-size: 11px;
  line-height: 16px;
  text-align: center;
}

/* 登录覆盖层：远程访问必须登录（本机免密时服务端直接下发会话，这一层不会出现）。 */
.login-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f2f5f9;
}
.login-card {
  width: min(360px, calc(100vw - 48px));
  padding: 28px 24px 24px;
  border: 1px solid var(--line, #e5e9ef);
  border-radius: 12px;
  background: #fff;
  box-shadow: 0 12px 32px rgba(25, 35, 49, .08);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.login-card__title { font-size: 16px; margin-bottom: 2px; }
.login-card__hint { margin-bottom: 8px; }
.login-card__submit { width: 100%; margin-top: 6px; }
.login-card__error {
  margin: 0;
  color: var(--red, #c73a48);
  font-size: 12px;
}

/* 待人工确认：`manual_review` 是"发送后结果不明确"的终态，这里只做处理入口，不重发。 */
.manual-review {
  margin: 0 var(--main-pad-inline, 48px) 16px;
  padding: 12px 14px 4px;
  border: 1px solid #f0d9a8;
  border-radius: 10px;
  background: #fffaf0;
}
.manual-review__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; margin-bottom: 8px; }
.manual-review__head strong { color: var(--amber, #ae7100); }
.manual-review .table-wrap { margin: 0 -14px; }
/* 「标记已处理」必须单行显示：单元格很窄，换行后四个字会竖着摞起来，看起来像个坏掉的按钮。 */
.manual-review__action { white-space: nowrap; }

/* 待审批：注册后 status=pending 的环境必须由人核对后再点「允许」。 */
.executor-pending {
  margin: 0 var(--main-pad-inline, 48px) 16px;
  padding: 12px 14px 4px;
  border: 1px solid #f0d9a8;
  border-radius: 10px;
  background: #fffaf0;
}
.executor-pending__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; margin-bottom: 8px; }
.executor-pending__head strong { color: var(--amber, #ae7100); }
.executor-pending .table-wrap { margin: 0 -14px; }
/* 两个动作必须单行排开：单元格窄，换行后「拒绝」会竖着摞起来，看起来像个坏掉的按钮。 */
.executor-pending__cell { white-space: nowrap; }
.executor-pending__actions { display: flex; gap: 8px; justify-content: flex-end; }
