  /* ================= 配色令牌 =================
     2026-10-26 亮哥定案：品牌双色 —— 丁香紫 #D8DCFC 做底、嫣红 #E95581 做主色。
     （这一版是当天第三稿：先洋红/极地蓝、再奶油白/蒂芙尼蓝，最终落到这套丁香紫/嫣红。
       结构始终没变 —— 浅底 + 白卡 + 深色字，每轮只换色相，所以令牌一直是同色相替换。）
     ⚠️ 一个必须做的技术处理：纯 #E95581 压在 #D8DCFC 上只有 2.56:1（WCAG 正文要 4.5），
        正文直接用它必然发虚。所以：
          --brand  = #E95581  ← 亮哥指定的那个色：按钮底、链接、图标、边框、标题强调都用它
          --text   = #B02A52  ← 同色相加深，正文专用（丁香紫底 4.72:1、白卡 6.37:1，过 AA）
        想强行全区都用 #E95581，把 --text 改成 #e95581 即可，代价是正文看不清。
     除本段外，样式表里不出现颜色字面量。 */
  :root {
    color-scheme: light;
    /* —— 背景渐变（沿用 10-13 定过的四层叠加结构）：丁香紫底 + 三团极淡嫣红光晕。
       原来那是玫红光晕；现在红光晕呼应主色，主渐变仍是丁香紫本色。 */
    --bg-image:
      radial-gradient(1300px 760px at 8% -12%, rgba(233, 85, 129, 0.13), transparent 60%),
      radial-gradient(1050px 680px at 98% 2%, rgba(233, 85, 129, 0.09), transparent 58%),
      radial-gradient(950px 760px at 50% 112%, rgba(233, 85, 129, 0.08), transparent 62%),
      linear-gradient(168deg, #d8dcfc 0%, #e3e6fd 48%, #d8dcfc 100%);
    --panel: #ffffff;
    --panel-solid: #ffffff;
    --line: rgba(233, 85, 129, 0.22);
    --line-strong: rgba(233, 85, 129, 0.45);
    --line-mid: rgba(233, 85, 129, 0.32);
    --text: #b02a52;
    /* 次要文字：原来是暖灰，现在是同色相的紫灰，白卡 4.85:1 */
    --muted: #5c5f85;
    --muted-2: rgba(92, 95, 133, 0.88);
    --label: #a8264f;
    --brand: #e95581;
    --brand-2: #f07a9d;
    --ok: #128a5f;
    --warn: #a06806;
    --danger: #c33b3b;
    --title-1: #b02a52;
    --title-2: #e95581;
    --title-3: #f07a9d;
    --accent-text: #e95581;
    --link: #c93f6b;
    --link-hover: #a8264f;
    --btn-fg: #ffffff;
    --hover-fg: #ffffff;
    --topline-a: rgba(240, 122, 157, 0.90);
    --topline-b: rgba(233, 85, 129, 0.90);
    --brand-tint: rgba(233, 85, 129, 0.10);
    --upload-bg: rgba(233, 85, 129, 0.05);
    --focus-bg: rgba(233, 85, 129, 0.07);
    --focus-ring: rgba(233, 85, 129, 0.26);
    --input-bg: #ffffff;
    --btn-ghost-bg: rgba(233, 85, 129, 0.08);
    --row-bg: rgba(233, 85, 129, 0.045);
    --disabled-bg: rgba(168, 38, 79, 0.08);
    --placeholder: rgba(92, 95, 133, 0.62);
    --scroll-thumb: rgba(233, 85, 129, 0.26);
    --scroll-thumb-hover: rgba(233, 85, 129, 0.48);
    --inset-top: rgba(255, 255, 255, 0.90);
    --shadow-card: 0 20px 60px rgba(58, 47, 110, 0.10);
    --shadow-brand: 0 0 60px rgba(233, 85, 129, 0.14);
    --shadow-brand-strong: 0 10px 24px rgba(233, 85, 129, 0.28);
    --shadow-brand-hover: 0 12px 30px rgba(233, 85, 129, 0.36);
    --ok-tint: rgba(18, 138, 95, 0.09);
    --ok-border: rgba(18, 138, 95, 0.28);
    --ok-tint-soft: rgba(18, 138, 95, 0.05);
    --warn-tint-soft: rgba(160, 104, 6, 0.06);
    --warn-border: rgba(160, 104, 6, 0.28);
    /* 副标题徽章：跟随主色（丁香紫底上用嫣红最清楚）。
       变量名为了不动下面那三条引用（976-978 行）一直没改，值已经是嫣红。 */
    --accent-purple: #e95581;
    --accent-purple-tint: rgba(233, 85, 129, 0.10);
    --accent-purple-border: rgba(233, 85, 129, 0.28);
    --danger-tint: rgba(195, 59, 59, 0.08);
    --danger-border: rgba(195, 59, 59, 0.26);
    --head-h: 42px;

    /* —— 布局可调参数：亮哥改这几个数字就能调界面，不用懂 CSS —— */
    --shell-radius: 18px;        /* 输入区外壳圆角 */
    --shell-max: 820px;          /* 输入区最大宽（当前未启用，外壳撑满卡片；想封顶就给 .form-shell 加 max-width: var(--shell-max)） */
    --shell-pad-x: 16px;         /* 输入区左右内边距 */
    --title-size: 55px;          /* 大标题字号 */
    --title-gap: 14px;           /* 标题 → 标签 间距 */
    --subtitle-size: 15px;       /* 副标题字号 */
    --tag-gap: 10px;             /* 标签之间间距 */
    --tag-gap-block: 26px;       /* 标签 → 输入区 间距 */
    --radius-field: 8px;         /* 输入框圆角 */
  }



  /* 界面最下方的版权行：与 .card 同宽同列，只占一行高度。
     亮哥 2026-10-15：整页改成可滚动文档后，原来那套 position:absolute;bottom:10px
     会把版权行钉在**第一屏**底部、并压住下方的提示词库 —— 所以回到正常流，排在最后。 */
  .foot {
    position: static;
    width: 100%;
    max-width: 1280px;
    margin: 22px auto 6px;
    text-align: center;
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--muted);
  }
  /* 页脚链接：与版权文字**同基线**、间距全交给「 · 」分隔符统一控制。
     原来的 display:inline-block + margin-left:10px + vertical-align:middle 会让
     分隔点偏向左侧（第一个点前留 10px、第二个点紧贴链接）、链接还比文字下沉 3px
     —— 亮哥 2026-10-16 指出「没对齐」，故移除这三项改回纯 inline。 */
  .foot-kf {
    color: var(--accent-text);
    font-size: 12.5px;
    line-height: 1.5;
    white-space: nowrap;
    text-decoration: underline;
    text-underline-offset: 2px;
    text-decoration-thickness: 1px;
    transition: color 0.16s;
  }
  .foot-kf:hover {
    color: var(--brand);
  }

  * { box-sizing: border-box; }
  html, body { margin: 0; padding: 0; }
  /* 亮哥 2026-10-15「滚轮失效」定案：
     滚动必须由 **根元素(html)** 承担，不能让 body 自己当滚动容器 ——
     ① body 一旦 `overflow-y:auto` 且高度=全部内容，滚动就发生在 body 上，
        鼠标停在 iframe 里滚轮时，滚动链要「iframe → body」多跨一层，很多情况下断掉；
     ② iframe 内部同理**不能**用 `overflow:hidden`（会建滚动容器、切断滚动链），
        必须用 `overflow:clip`（纯裁切、不进滚动链）—— 见 prompts.html 的 html.embed。
     html 这里显式声明可滚，body 只当内容容器。 */
  html { overflow-y: auto; overflow-x: hidden; }
  body {
    color: var(--text);
    font: 15px/1.6 -apple-system, "Segoe UI", "Microsoft YaHei", Roboto, sans-serif;
    display: flex;
    flex-direction: column;
    align-items: center;
    min-height: 100vh;
    height: auto;            /* 不再锁死成单屏：内容高过时整页可滚动 */
    padding: 76px 16px 18px; /* 顶部 76 = 固定导航栏（52）+ 余量（亮哥 10-15：主体下移给导航栏让位） */
    background: var(--bg-image);
    background-attachment: fixed;
  }

  /* 改版：不再是一张白色卡片 —— 所有元素直接铺在页面背景上。
     这里只留「宽度上限 + 纵向排布」，背景 / 边框 / 阴影 / 毛玻璃 / 顶线全部去掉。 */
  .card {
    position: relative;
    width: 100%;
    max-width: 1280px;
    padding: 0 28px;
    align-self: center;
    flex: 0 1 auto;            /* 改 1 1 auto→0 1 auto：不再强制撑满视口，内容多高就多高 */
    min-height: 0;
    margin: auto 0;            /* 内容短时整体垂直居中；内容高过时回到顶部、整页可滚 */
    display: flex;
    flex-direction: column;
    /* 去掉 max-height:100% 与 overflow:hidden：提示词库在下方、整页一起滚动 */
  }
  /* 「下载客户端」是次要操作（装一次就再也不管），做得比「全部下载」轻一点 ——
     两个都叫"下载"、长得又一样，并排着容易点错。 */
  .save-btn.minor { font-size: 12.5px; padding: 4px 11px; color: var(--muted); border-style: dashed; }
  .save-btn.minor:hover:not(:disabled) { border-style: solid; }
  /* 手机上这个按钮换成一句说明（自定义协议只在电脑上注册，装了也用不了） */
  .client-mobile-hint { font-size: 12.5px; color: var(--muted); }
  /* 「下载客户端」（2026-10-15 亮哥：文字胶囊换成下载图标，收进右上 .entry-icons 组）。
     样式全走 .entry-ico 图标钮；这里只补两件套：
     ① .entry-ico 自带 display:inline-flex，会盖掉 [hidden] 的 UA 样式 ⇒ 必须显式压一道
        （本文件的老坑：.notice / .entry-badge / .toast 全栽过）；
     ② flex:none 防止它在导航栏里被拉伸。 */
  .entry-ico[hidden] { display: none; }
  #btnClient { flex: none; }

  /* 下载说明后面的问号：平时只是个圈，鼠标放上去（或键盘 Tab 到）才弹汽泡 */
  .help {
    flex: none;
    width: 17px;
    height: 17px;
    border: 1px solid var(--line-strong);
    border-radius: 50%;
    color: var(--muted);
    font-size: 11.5px;
    font-weight: 700;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: help;
    user-select: none;
  }
  .help:hover, .help:focus-visible { color: var(--accent-text); border-color: var(--brand-2); }
  .help-bub {
    position: absolute;
    left: 0;
    right: 0;
    top: calc(100% + 9px);
    padding: 10px 12px;
    border: 1px solid var(--line-strong);
    border-radius: 9px;
    background: var(--panel-solid);
    color: var(--text);
    font-size: 12.5px;
    font-weight: 400;
    line-height: 1.68;
    text-align: left;
    box-shadow: 0 12px 28px rgba(0, 0, 0, .3);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transform: translateY(-4px);
    transition: opacity .14s ease, transform .14s ease;
    z-index: 30;
  }
  .help:hover .help-bub,
  .help:focus-visible .help-bub {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
  }
  .help-bub b { color: var(--text); font-weight: 700; }
  /* 小按钮：任务列表标题右边的「全部下载」，以及每个任务卡片右上角的「下载到本地」。
     里面可以放一个圆环（.dl-ring），所以按 inline-flex 排。 */
  .save-btn {
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 6px 14px;
    border: 1px solid var(--line-strong);
    border-radius: 8px;
    background: var(--btn-ghost-bg);
    color: var(--accent-text);
    font: inherit;
    font-size: 13px;
    line-height: 1.5;
    white-space: nowrap;
    cursor: pointer;
    transition: color .16s, background .16s, border-color .16s, opacity .16s;
  }
  .save-btn:hover:not(:disabled) {
    color: var(--btn-fg);
    background: var(--brand);
    border-color: var(--brand);
  }
  .save-btn:disabled { opacity: .45; cursor: not-allowed; }

  /* 下载状态圆圈：空环=还没下 / 转圈=正在下 / 满环+勾=已下载 / 红环+叹号=查不到。
     现在它长在「下载到本地」按钮里，含义由按钮上的文字负责，圆环只做视觉。 */
  .dl-ring {
    position: relative;
    flex: none;
    width: 22px;
    height: 22px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .dl-ring svg { display: block; width: 100%; height: 100%; transform: rotate(-90deg); }
  .dl-ring circle { fill: none; stroke-width: 4.5; }
  .dl-ring .track { stroke: var(--line-strong); }
  .dl-ring .bar { stroke: var(--brand-2); stroke-linecap: round; transition: stroke-dashoffset .35s ease; }
  .dl-ring.done .bar { stroke: var(--brand-2); }
  .dl-ring.err .track { stroke: var(--danger); opacity: .35; }
  .dl-ring.err .bar { stroke: var(--danger); }
  /* 转动的是 SVG 本身，里面的勾/叹号是另一个元素，所以文字不会跟着转 */
  .dl-ring.spin svg { animation: dlSpin 1s linear infinite; }
  .dl-ico {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-style: normal;
    font-size: 12px;
    font-weight: 700;
    line-height: 1;
    pointer-events: none;
  }
  .dl-ring.done .dl-ico { color: var(--brand-2); }
  .dl-ring.err .dl-ico { color: var(--danger); }
  /* 圆环塞进按钮里要缩一圈，不然比旁边 13px 的文字高一大截 */
  .save-btn .dl-ring { width: 15px; height: 15px; }
  .save-btn .dl-ico { font-size: 9px; }
  @keyframes dlSpin { from { transform: rotate(-90deg); } to { transform: rotate(270deg); } }

  /* 任务卡右上角的下载按钮：就长在 .task-head 这一行里，
     靠 .task-msg 的 flex:1 顶到右边；比列表页那颗小一号，免得把标题行撑高 */
  .task-dl-btn { padding: 4px 11px; font-size: 12.5px; }
  /* 未下载（可点击下载 / 重试 / 查询中）：实心品牌红，作为明确的行动态，
     和"已下载"的浅红描边明显区分开 */
  .task-dl-btn:not(.is-done) {
    background: var(--brand);
    color: var(--btn-fg);
    border-color: var(--brand);
  }
  .task-dl-btn:not(.is-done):hover:not(:disabled) {
    /* 用亮度微调做悬停反馈，避免引入令牌段之外的颜色字面量（#e61e37 与 --brand 不一致） */
    filter: brightness(0.92);
  }
  /* 实心红底上的圆环：track/bar/图标原本是红调（--line-strong 半透明红、--brand-2 浅红），
     红对红看不清（亮哥 2026-10-16），换白色系。specificity (0,4,0) 压过 .dl-ring.err 的 (0,2,1)。 */
  .task-dl-btn:not(.is-done) .dl-ring .track { stroke: rgba(255,255,255,.45); }
  .task-dl-btn:not(.is-done) .dl-ring .bar { stroke: #ffffff; }
  .task-dl-btn:not(.is-done) .dl-ring .dl-ico { color: #ffffff; }
  /* 已下载完的按钮保持可见但不发灰（它不是"不可用"，是"没事可做"），鼠标也换成默认箭头 */
  .task-dl-btn.is-done:disabled { opacity: 1; color: var(--brand-2); border-color: var(--brand-2); cursor: default; }
  h1, .col-title {
    font-size: 22px;
    line-height: 1.6;
    margin: 0 0 6px;
    font-weight: 600;
    background: linear-gradient(96deg, var(--title-1) 0%, var(--title-2) 46%, var(--title-3) 100%);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
  }
  .res-empty { color: var(--muted); font-size: 14px; padding: 52px 12px; text-align: center; }

  .field { margin-bottom: 13px; }
  /* 相邻的短字段并排一行：在 config.json 的 fields 里给字段标 "inline": true 即可（如「图片尺寸」+「生成数量」）。
     省下一行高度，让整张表单在常见视口高度下不用向下滚动。列宽自适应，窄了会自己退回单列。 */
  .field-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(156px, 1fr));
    gap: 0 14px;
    align-items: start;
  }
  .field > label { display: block; font-size: 14px; margin-bottom: 6px; font-weight: 500; color: var(--label); }
  .req { color: var(--brand-2); margin-left: 2px; }
  input[type=text], input[type=number], input[type=tel], input[type=date], input[type=url], textarea, select {
    width: 100%;
    padding: 9px 12px;
    border: 1px solid var(--line);
    border-radius: 8px;
    font: inherit;
    color: var(--text);
    background: var(--input-bg);
    outline: none;
    transition: border-color .15s, box-shadow .15s, background .15s;
  }
  input:focus, textarea:focus, select:focus {
    border-color: var(--brand);
    background: var(--focus-bg);
    box-shadow: 0 0 0 4px var(--focus-ring);
  }
  /* 四个并排控件统一钉在 44 高（亮哥 2026-10-15 截图：同一行里尺寸 select 42、
     数量/目录 input 44、参考图虚线框 44，高高低低不在一条线上）。
     44 本来就是多数派的值（提交按钮、参考图框也都是 44），所以统一到 44 而不是 42。
     ⚠️ 用 #form 前缀钉死，别靠「padding + 字号」把高度撑出来 —— 那样换字体/字号就又不一样了。
     ⚠️ 控件行高度参与 .form-shell 里 textarea 的 calc(30vh - 86px) 那个 86；
        行高没变（本来就被 44 的 input 撑着）⇒ 86 不用动（探针实测 shell 溢出仍为 0）。 */
  #form input[type=text],
  #form input[type=number],
  #form select { height: 44px; }
  /* 去掉 select 的原生小箭头（亮哥 2026-10-15 截图：那个 ▼ 难看）。
     ⚠️ 这里的下拉点开走的是 app.js 自绘菜单（原生弹层早被拦了），箭头没有功能作用；
        appearance:none 只去箭头，点击行为不变。 */
  #form select {
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
  }
  /* 按钮类的键盘焦点环（2026-10-14 补）：表单控件早就有 focus 光环，但按钮几乎全漏了 ——
     键盘用户 Tab 到「全部下载」「删除任务」「关闭提示」时**完全看不出焦点在哪**。
     这里统一补一道 focus-visible（:focus-visible 只在键盘聚焦时生效，
     鼠标点击不会平白多出一圈，正是我们要的语义）。 */
  .save-btn:focus-visible,
  .upload-btn:focus-visible,
  .dir-btn:focus-visible,
  .task-x:focus-visible,
  .toast-x:focus-visible,
  .drawer-x:focus-visible,
  .entry-ico:focus-visible,
  .ratio-item:focus-visible,
  button[type=submit]:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
  }
  ::placeholder { color: var(--placeholder); }
  select option { background: var(--panel-solid); color: var(--text); }
  textarea { resize: vertical; min-height: 72px; }
  .choices { display: flex; flex-wrap: wrap; gap: 8px 16px; padding-top: 2px; }
  .choices label { display: inline-flex; align-items: center; gap: 6px; font-weight: 400; font-size: 14px; }
  .checkline { display: inline-flex; align-items: center; gap: 8px; font-size: 14px; }

  /* 上传框：贴着内容收（width:fit-content + padding 0），虚线框完全包住按钮/缩略图、四周不留空隙。
     不用 inline-flex —— 行内盒按文字基线对齐，框会随内容上下漂（实测 y 漂 5~7px，就是「对不齐」的元凶）；
     改成块级 flex + fit-content，宽照样贴内容，垂直位置跟普通控件一样顶格。
     不再写 min-height —— 以前那 44px 的最小高就是按钮四周空隙的来源。 */
  .upload {
    display: flex;
    width: fit-content;
    /* 外层 .opt 是 flex 列、默认 stretch：手机端参考图占整行时会把框拉满宽，这里掐住 */
    align-self: flex-start;
    align-items: center;
    gap: 10px;
    border: 1px dashed var(--line-strong);
    border-radius: 8px;
    padding: 0;
    background: var(--upload-bg);
  }
  /* 提示文字（上传中 / 报错）出现时给点左右内边距，别让字贴着虚线；
     上下不给 —— 框的高度是锁死的（见 #form .upload），文字靠 align-items 居中。
     :has 不支持的旧浏览器只是字贴边，不影响功能。 */
  .upload:has(.upload-hint:not(:empty)) { padding: 0 12px; }
  .upload-body { flex: 1; min-width: 0; }
  .upload input[type=file] { display: none; }
  .upload-btn {
    padding: 7px 16px;
    border: 1px solid var(--line-strong);
    border-radius: 8px;
    background: var(--btn-ghost-bg);
    color: var(--text);
    font: inherit;
    font-size: 14px;
    cursor: pointer;
    transition: border-color .15s, color .15s, background .15s;
  }
  .upload-btn:hover { border-color: var(--brand); color: var(--hover-fg); background: var(--brand-tint); }
  /* 上传成功后按钮会被 JS 藏掉（亮哥 2026-10-13：框里只留缩略图）；
     作者样式没设 display，[hidden] 本来就生效，这条只是防止以后有人给它加 display */
  .upload-btn[hidden] { display: none; }
  .upload-hint { color: var(--muted); font-size: 13px; margin-top: 8px; }
  .upload-hint.ok { color: var(--ok); }
  .upload-hint.err { color: var(--danger); }
  /* 参考图缩略图：直接用 FileReader 读出来的 dataURL，不发额外请求。
     按钮在上传成功后会被藏掉，缩略图就是唯一的点击入口（点了重新选图），要给手型 */
  .upload-thumb {
    display: block;
    flex: none;
    width: 72px;
    height: 72px;
    margin: 0;
    border-radius: 8px;
    border: 1px solid var(--line-strong);
    background: var(--row-bg);
    object-fit: cover;
    cursor: pointer;
  }
  .upload-thumb[hidden] { display: none; }
  /* 缩略图行（亮哥 2026-10-14 改成最多 3 张）：横排。gap 要小 ——
     3 张 42px + 2 个 ×（各压出 7px）+ 按钮 42px + 提示，很容易把这一格撑爆；
     参考图那格是 flex:0 0 auto（贴内容收），宽了会把整行挤到换行。 */
  .upload-thumbs { display: flex; align-items: center; gap: 10px; flex: none; }
  .upload-thumbs[hidden] { display: none; }
  /* 缩略图 + 删除小钮的定位壳：× 钉在图右上角（压出去半个身位，徽章式）。
     ⚠️ display:inline-block 是保险：行容器（.upload-thumbs）万一因缓存没拿到 flex 规则，
     块级壳会把几张图竖着堆成一列（2026-10-14 实测翻过车：css 忘升 ?v=，js 新 css 旧）。
     inline-block 至少保证横排，flex 只是再加间距和对齐。 */
  .upload-thumb-wrap { position: relative; display: inline-block; flex: none; line-height: 0; vertical-align: top; }
  .upload-thumb-wrap[hidden] { display: none; }
  /* 参考图删除小钮（亮哥 2026-10-14）：小圆 ×，平时弱色、悬停变红；
     徽章式压在角上，虚线框和缩略图都不用为它留位置 */
  .upload-del {
    position: absolute;
    top: -7px;
    right: -7px;
    width: 18px;
    height: 18px;
    padding: 0;
    border: 1px solid var(--line-strong);
    border-radius: 50%;
    background: var(--panel-solid, #ffffff);
    color: var(--muted);
    font: inherit;
    font-size: 12px;
    line-height: 1;
    cursor: pointer;
    transition: color .15s, border-color .15s;
  }
  .upload-del:hover { color: var(--danger); border-color: var(--danger); }
  .upload-del[hidden] { display: none; }

  /* 「选择文件夹」这一行：输入框占满剩余宽度，按钮贴右边、高度跟着输入框走 */
  .dir-row {
    display: flex;
    align-items: stretch;
    gap: 10px;
  }
  .dir-row > input {
    flex: 1 1 auto;
    width: auto;
    min-width: 0;
  }
  .dir-btn {
    flex: none;
    padding: 0 14px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--btn-ghost-bg);
    color: var(--label);
    font: inherit;
    font-size: 13px;
    white-space: nowrap;
    cursor: pointer;
    transition: color .15s, background .15s, border-color .15s;
  }
  .dir-btn:hover {
    color: var(--hover-fg);
    background: var(--brand);
    border-color: var(--brand);
  }
  /* 「优化提示词」图标按钮（亮哥 2026-10-18）：跟「选择文件夹」同款布局，但用品牌色区分 */
  .opt-prompt-btn {
    flex: none;
    padding: 0 14px;
    border: 1px solid var(--brand);
    border-radius: 8px;
    background: var(--brand-tint);
    color: var(--brand-2);
    font: inherit;
    font-size: 13px;
    font-weight: 500;
    white-space: nowrap;
    cursor: pointer;
    transition: color .15s, background .15s, border-color .15s, opacity .15s;
  }
  .opt-prompt-btn:hover:not(:disabled) {
    color: var(--hover-fg);
    background: var(--brand);
    border-color: var(--brand);
  }
  .opt-prompt-btn:disabled {
    opacity: .6;
    cursor: default;
  }
  .opt-prompt-btn[hidden] { display: none; }
  /* 优化中：按钮内转圈 + 文案（亮哥 2026-10-17），复用 dlSpin 动画 */
  .opt-spin {
    width: 13px; height: 13px;
    flex: none;
    margin-right: 5px;
    vertical-align: -1px;
    animation: dlSpin 0.9s linear infinite;
  }
  .opt-spin circle { fill: none; stroke-width: 3; }
  .opt-spin .opt-track { stroke: currentColor; opacity: .25; }
  .opt-spin .opt-bar { stroke: currentColor; stroke-linecap: round; stroke-dasharray: 16 44; }
  .dir-hint {
    margin-top: 6px;
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--muted);
  }
  .dir-hint.warn { color: var(--warn); }

  button[type=submit] {
    width: 100%;
    margin-top: 8px;
    padding: 12px 16px;
    border: 0;
    border-radius: 8px;
    background: linear-gradient(135deg, var(--brand) 0%, var(--brand-2) 100%);
    color: var(--btn-fg);
    font: inherit;
    font-weight: 600;
    letter-spacing: .02em;
    cursor: pointer;
    box-shadow: 0 10px 26px var(--shadow-brand-strong);
    transition: filter .15s, box-shadow .15s, transform .1s;
  }
  button[type=submit]:hover:not(:disabled) {
    filter: brightness(1.12);
    box-shadow: 0 12px 32px var(--shadow-brand-hover);
  }
  button[type=submit]:active:not(:disabled) { transform: translateY(1px); }
  button[type=submit]:disabled {
    background: var(--disabled-bg);
    color: var(--muted);
    box-shadow: none;
    cursor: default;
  }

  .status {
    margin-top: 16px;
    padding: 10px 14px;
    border-radius: 8px;
    font-size: 14px;
    /* 与输入区同宽同列（亮哥 2026-10-13）：不做的话它是块级元素、撑满整张卡片，
       比下面 66.6667vw 居中的输入壳宽出一截。口径跟 .quota-hint 完全一致。 */
    width: 66.6667vw;
    max-width: 100%;
    margin-left: auto;
    margin-right: auto;
    /* 文案 + 右侧「×」关闭钮（亮哥 2026-10-17） */
    display: flex;
    align-items: flex-start;
    gap: 10px;
  }
  /* ⚠️ 上面设了 display:flex 会盖掉 [hidden] 的默认 display:none（幽灵显示坑），
     必须补这条，否则收起指令失效。 */
  .status[hidden] { display: none; }
  .status-text { flex: 1; min-width: 0; }
  .status-x {
    flex: none;
    width: 22px;
    height: 22px;
    margin: -1px -6px 0 0;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: inherit;
    font-size: 16px;
    line-height: 22px;
    text-align: center;
    cursor: pointer;
    opacity: .5;
  }
  .status-x:hover { opacity: 1; background: rgba(0,0,0,.07); }
  .status-x:focus-visible { outline: 2px solid currentColor; outline-offset: 1px; }
  .status.ok {
    background: var(--ok-tint);
    color: var(--ok);
    border: 1px solid var(--ok-border);
  }
  .status.err {
    background: var(--danger-tint);
    color: var(--danger);
    border: 1px solid var(--danger-border);
  }
  .hint { color: var(--muted); font-size: 13px; margin-top: 4px; }
  /* 每日额度提示：数字是服务端算的，这里只负责显示。
     宽度/居中方式跟上面的 .form-shell 一模一样（4/6 屏宽 + margin auto），
     这样这行小字的左缘正好压着提示词框的左缘（亮哥 2026-10-13）。 */
  .quota-hint { width: 66.6667vw; max-width: 100%; margin: 12px auto 0; font-size: 13px; color: var(--muted); }
  .quota-hint.out { color: var(--warn); }
  .loading { color: var(--muted); font-size: 14px; padding: 12px 0; text-align: center; }

  .result { min-height: 240px; }
  .res-sub { color: var(--muted); font-size: 14px; margin-bottom: 16px; line-height: 1.7; }
  .res-sub b { color: var(--accent-text); }
  /* 任务卡里的缩略图墙（亮哥 2026-10-14 选方案 A）：
     原来每个 li 里是个下划线的「图片 1」文字链接，现在直接铺图 —— 链接文字全消失，
     一眼看到出的是什么。链接本身还在（点图开原图），只是里面装的是 <img>。 */
  .res-list { list-style: none; padding: 0; margin: 0 0 18px; }
  .res-list li { margin-bottom: 8px; }
  .res-list li:last-child { margin-bottom: 0; }
  /* 格子是个相对定位的壳：占位层 / 失败提示层 / img 三层叠在一起。
     ⚠️ 给 .multi 单列一个 min-height，是因为占位层要有地方撑开 —— 图没来时 img 高 0，
     整面墙会塌成一条线，然后图到了再"蹦"开（那是比慢更难受的跳版）。 */
  .res-list a {
    position: relative;
    display: block;
    border-radius: 8px;
    overflow: hidden;
    background: var(--row-bg);
    line-height: 0;          /* 消掉 img 基线下的那几 px 缝 */
  }
  /* 单图不给固定高：让它按原始宽高比铺满整行（多图才要统一裁成正方形，否则墙会参差） */
  .res-list a > img { display: block; width: 100%; height: auto; }

  /* 占位层：图没到时铺一层微光在扫（.is-loading 就是"还没结算"的意思，JS 里结算时摘掉）。
     用 ::after 而不是额外标签，是省一个 DOM 节点。 */
  .res-list a.is-loading::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(100deg,
      transparent 20%, rgba(255, 255, 255, 0.55) 50%, transparent 80%);
    background-size: 220% 100%;
    animation: resShimmer 1.15s linear infinite;
  }
  @keyframes resShimmer {
    from { background-position: 140% 0; }
    to   { background-position: -40% 0; }
  }
  /* 加载完淡入：直接"啪"地出现比它慢一点出现更显突兀 */
  .res-list a.is-loaded > img { animation: resFade .28s ease-out both; }
  @keyframes resFade { from { opacity: 0; } to { opacity: 1; } }

  /* 加载失败：图撤掉、占位停住，中间留一句小字 —— 比一个空洞或者浏览器那个破图标好看 */
  .res-list .res-ph, .res-list .res-fail { display: none; }
  .res-list a.is-loading .res-ph { display: block; position: absolute; inset: 0; }
  .res-list a.is-failed > img { visibility: hidden; }
  .res-list a.is-failed .res-fail {
    display: flex;
    position: absolute;
    inset: 0;
    align-items: center;
    justify-content: center;
    color: var(--muted);
    font-size: 12px;
    line-height: 1;
    background: var(--row-bg);
  }

  /* 多张图分列（列数走配置 form.imageLinksPerRow，默认 4，经 --img-cols 传入）。
     ⚠️ 用 CSS 变量而不是内联 grid-template-columns：内联样式会连 ≤720px 的单列媒体查询一起压掉。 */
  .res-list.multi {
    display: grid;
    grid-template-columns: repeat(var(--img-cols, 4), 1fr);
    gap: 8px;
  }
  /* 统一裁成正方形（object-fit 默认就是 cover，这里显式写出来是给后来人看的）：
     出图常见 1:1 / 16:9 / 9:16 混在同一批里，不裁的话每格高度都不一样、整面墙是斜的。
     ⚠️ min-height 不是装饰：它是**占位层的高度来源** —— 没有它，图没来时每格高 0。 */
  .res-list.multi a > img { height: auto; aspect-ratio: 1 / 1; }
  .res-list.multi a { aspect-ratio: 1 / 1; }
  @media (max-width: 720px) {
    .res-list.multi { grid-template-columns: repeat(3, 1fr); }
  }

  /* 只生成一张图的任务（亮哥 2026-10-14）：也收成**缩略图**，不铺满卡片宽度。
     原来是单图独占一行、按原图比例铺满（实测能撑到 500px 宽、三四百像素高），
     任务列表一多就是把整个抽屉喂给这一张图；而他点图就能看原图，这里只需要"看一眼出的什么"。
     ⚠️ 尺寸跟多图墙那一格**必须一模一样**（亮哥 2026-10-14 第二版：「一张也 89×89 吧」）。
     所以这里不用任何写死的数：`--thumb` 由 JS 按配置（列数 + 卡片可用宽）算出来写在这一格上，
     单图直接吃同一个值 —— 改 form.imageLinksPerRow 两边一起变，不会一个 89 一个 168。
     兜底 89px 只是 JS 算不出来时（比如卡片还没进 DOM）的保底。 */
  .res-list.solo li { display: inline-block; width: var(--thumb, 89px); }
  .res-list.solo a { aspect-ratio: 1 / 1; }
  .res-list.solo a > img { height: auto; aspect-ratio: 1 / 1; }

  .task {
    border: 1px solid var(--line);
    border-radius: 10px;
    padding: 9px 12px;
    margin-bottom: 8px;
    background: var(--row-bg);
    transition: border-color .2s, background .2s, box-shadow .2s;
  }
  .task:hover { border-color: var(--line-strong); box-shadow: 0 3px 12px var(--focus-ring); }
  .task-done { border-color: var(--ok-border); background: var(--ok-tint-soft); }
  .task-timeout { border-color: var(--warn-border); background: var(--warn-tint-soft); }
  /* 生成失败（亮哥 2026-10-14）：用现成的 danger 色，跟「还在生成中」的 warn 黄分开 */
  .task-failed { border-color: var(--danger-border); background: var(--danger-tint); }
  .task-head { display: flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; }
  /* 折叠箭头：默认朝下（展开态）；卡片收起时朝右 */
  .task-caret {
    flex: none;
    width: 0;
    height: 0;
    border-left: 5px solid var(--muted);
    border-top: 4px solid transparent;
    border-bottom: 4px solid transparent;
    transform: rotate(90deg);
    transition: transform .18s;
  }
  .task.collapsed .task-caret { transform: rotate(0deg); }
  .task.collapsed .task-body { display: none; }
  /* 进行中任务：「生成中…」文字前面的呼吸灯 */
  .task-pulse {
    flex: none;
    position: relative;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--brand-2);
    animation: breathe 1.6s ease-in-out infinite;
  }
  .task-pulse::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background: var(--brand-2);
    animation: breathe-halo 1.6s ease-out infinite;
  }
  @keyframes breathe {
    0%, 100% { opacity: 0.45; }
    50%      { opacity: 1; }
  }
  @keyframes breathe-halo {
    0%   { transform: scale(1);   opacity: 0.55; }
    100% { transform: scale(3.4); opacity: 0; }
  }
  .task-no {
    flex: none;
    font-size: 12px;
    font-weight: 600;
    color: var(--accent-text);
    background: var(--brand-tint);
    border: 1px solid var(--line-mid);
    border-radius: 4px;
    padding: 2px 7px;
  }
  /* 用 flex 是为了让里面的呼吸灯（.task-pulse）能当 flex 项撑出宽高，
     并和文字自动保持 8px 间距 —— 它原本是行内元素，光靠 width/height 撑不起来。 */
  .task-msg { flex: 1; display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 500; }
  .task-done .task-msg { color: var(--ok); }
  .task-timeout .task-msg { color: var(--warn); }
  .task-failed .task-msg { color: var(--danger); }
  /* 失败原因（表里原文）单独一行，按正文色显示、可换行 */
  .task .res-err { color: var(--text); white-space: pre-wrap; overflow-wrap: anywhere; }
  .task-x {
    flex: none;
    border: 0;
    background: none;
    color: var(--muted);
    font-size: 17px;
    line-height: 1;
    cursor: pointer;
    /* 命中区扩张（2026-10-14）：原来是 `padding: 0 4px`，实际可点宽度只有 ~22px。
       用负 margin 抵消掉多出来的 padding，视觉位置和尺寸不变、可点区域变大。 */
    padding: 8px 10px;
    margin: -8px -6px;
    transition: color .15s;
  }
  .task-x:hover { color: var(--danger); }
  .task .res-list { margin: 8px 0 0; }
  .task .res-sub { margin: 8px 0 0; }
  /* 卡片内紧凑：行距收一档；最后一行不再垫底边距（那是卡片底部空一截的元凶） */
  .task .res-list li { margin-bottom: 6px; }
  .task .res-list li:last-child { margin-bottom: 0; }
  /* 网格里行距交给 gap，li 自身不再垫 margin（否则列高不齐、底部空一截） */
  .task .res-list.multi li { margin-bottom: 0; }

  /* 「已保存至：<目录>」那一行（亮哥 2026-10-13：客户端不再弹结果页，改在卡片上告诉他存哪了）。
     贴在图片链接行下面、不抢眼：小一号 + 弱色；长路径必须能折行（用户会填很长的目录名）。 */
  .task .task-dir {
    margin-top: 8px;
    color: var(--muted);
    font-size: 12.5px;
    line-height: 1.6;
    word-break: break-all;
  }

  /* 「已保存至：」后面那一段目录 —— 可点，点了调起本机 feishudl://open 用资源管理器打开。
     刻意做得像普通链接（下划线 + 主题色），否则用户不知道能点（2026-10-13 亮哥选 C 方案）。 */
  .task .task-dir .dir-open {
    color: var(--link);
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
  }
  .task .task-dir .dir-open:hover {
    filter: brightness(1.15);
  }

  /* 窗口不够宽时先收窄列间距，别让两列挤在一起 —— 已随两栏改单栏一起删（2026-10-13 死代码清理） */

  @media (max-width: 880px) {
    /* 滚动交给根元素（见文件顶部 html 段），body 不再自己当滚动容器 */
    body {
      height: auto;
      min-height: 100vh;
      padding: 76px 14px 32px;
      background-attachment: scroll;   /* iOS Safari 对 fixed 背景处理很差：滚动时背景被截断、还掉帧 */
    }
    .card { max-height: none; overflow: visible; }
    /* 页脚必须回到正常流。桌面那套是 absolute 钉在视口底部，因为桌面内容不滚动；
       这一档页面已经能滚了，absolute 会让它停在**第一屏**底部、并压住后面的内容。 */
    .foot {
      position: static;
      left: auto;
      bottom: auto;
      transform: none;       /* ⚠️ 必须显式清掉：transform 对 static 元素依然生效，会把页脚左移半个宽度 */
      margin-top: 20px;
    }
    /* 两栏布局残留（.cols / .col-head / .col-left / .col-right）已随单栏改版一起删
       —— 2026-10-13 死代码清理 */
  }

  /* ================================================================
     单栏改版（2026-10-11）：顶栏居中大标题 + 右上角「任务列表」入口，
     中间一大块提示词输入区，选项收成一排芯片，任务列表改成右侧抽屉。
     这一段放在样式表末尾，用来覆盖前面按旧两栏布局写的 .field 等规则。
     ================================================================ */

  /* ---------- 顶栏 ---------- */
  .topbar {
    position: relative;
    flex: none;
    display: flex;
    flex-direction: column;    /* 标签行要排在标题**下面**，topbar 从横向改纵向 */
    align-items: center;
    padding: 0 0 4px;
  }
  /* ---------- 顶部固定导航栏（亮哥 2026-10-15）----------
     把「下载客户端 / 公告 / 三入口」收进同一条 fixed 横条；主体靠 body 的
     padding-top:76px 下移让位。毛玻璃白底 + 细分隔线，滚到哪都看得见。 */
  .topnav {
    position: fixed;
    top: 0; left: 0; right: 0;
    z-index: 30;             /* 抽屉 41、toast 50，导航栏垫在其下 */
    display: flex;
    align-items: center;
    gap: 12px;
    height: 52px;
    padding: 0 28px;
    background: rgba(255, 255, 255, 0.55);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    border-bottom: 1px solid var(--line);
  }

  /* ---------- 品牌横版 logo（亮哥 2026-11-02）----------
     导航栏左上角那颗「mark + YUANSOL」，替换掉原来的顶部公告。
     ① 高度定死 26px、宽度 auto —— 原图 viewBox 是 314.73×64（约 4.92:1），
        跟着 height 走能保住比例，别只写 width 否则会拉变形。
     ② 图标组靠 .entry-icons 的 margin-left:auto 顶到最右，这中间的空档由它吃掉。
     ③ 用 <img> 引外部 svg（不是内联、也不是 background）：这份 logo 自带 4 个
        defs id，内联进页面有撞名风险；background 又没法给读屏软件 alt。 */
  .topnav-brand {
    flex: none;
    display: block;
    height: 30px;
    width: auto;
  }

  /* ---------- 顶部公告（亮哥 2026-10-13 定文案基调；10-15 收进 .topnav；10-16 移到最左）----------
     ① 位置：普通流里的首个子项（不再是绝对居中）——公告在左、图标组在右，
        .entry-icons 的 margin-left:auto 吃掉落差把它顶到最右。
     ② 不要背景：白胶囊底/边框/阴影全去掉，只留玫红字。
     ③ 小喇叭图标在 app.js 填文案时一起塞进去（SVG，stroke=currentColor 跟文字同色）。
     ④ 太长裁断：一行高，长句超出就出省略号（手机档回两行，见媒体查询）。 */
  .notice {
    /* 2026-10-16 亮哥：公告挪到最左。min-width:0 让它能被压缩，内层 span 出省略号，不挤图标组。 */
    /* 2026-10-17 亮哥「弄得显眼一些」：字号 13.5→15.5、字重 700→800。仍然**不要背景**
       （10-16 他明确去掉白胶囊底/边框/阴影，只留玫红字）——显眼靠字号字重，不加底色。 */
    flex: 0 1 auto;
    min-width: 0;
    display: inline-flex;
    align-items: center;
    gap: 7px;
    color: var(--brand);
    font-size: 15.5px;
    font-weight: 800;
    letter-spacing: .01em;
    text-align: left;
    white-space: nowrap;
    /* 用 clip 不用 hidden：hidden 会让公告成为**滚动容器**（虽是内联小元素，
       但"可滚祖先"越多越容易在链上出意外）；clip 只裁切、不进滚动链。 */
    overflow: clip;
  }
  .notice span {
    overflow: clip;
    text-overflow: ellipsis;
  }
  .notice svg { flex: none; }
  /* ⚠️ 必须补这条：「作者样式里的 display 会盖掉 [hidden]」——这个文件已经为此栽过
     （.toast / .upload-body 都是先例），display:inline-flex 一写，hidden 就要显式压一道。 */
  .notice[hidden] { display: none; }
  .topbar h1 {
    margin: 0;
    font-size: var(--title-size);
    font-weight: 800;
    letter-spacing: -0.02em;
    color: var(--title-1);          /* 兜底色：不支持文字裁剪的浏览器仍看得见 */
    text-align: center;
    /* 2026-11-02 亮哥换品牌 logo：标题改成「图形 mark + 站名」横排。
       用 flex 是为了让左边那颗 mark 和文字垂直居中、间距跟着字号走。 */
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .34em;
  }
  /* 品牌 mark（2026-11-02）。
     ⚠️ 用 ::before 挂背景图，**不要**在 HTML 里塞 <img>：app.js 里有
     `elTitle.textContent = cfgStr(cfg.title, '生图任务')`，会把 #title 的子节点整体替换掉，
     真图片节点会被抹没；伪元素不吃 textContent，安全。
     尺寸用 em 跟着 --title-size 走（桌面 55px 字号 → 约 47px），窄屏媒体查询里自动缩。 */
  .topbar h1::before {
    content: '';
    flex: none;
    width: .92em;
    height: .92em;
    background: url("logo.svg") no-repeat center / contain;
  }
  /* 标题特效：玫红渐变字 + 柔光投影 + 缓慢流光。
     只在支持 background-clip:text 时才裁剪，否则文字会变透明看不见 —— 所以用 @supports 兜住。
     想关掉全部特效，删掉这个 @supports 块即可。 */
  @supports ((-webkit-background-clip: text) or (background-clip: text)) {
    .topbar h1 {
      background-image: linear-gradient(96deg, var(--title-1) 0%, var(--title-2) 40%, var(--title-3) 62%, var(--title-2) 100%);
      background-size: 220% 100%;
      -webkit-background-clip: text;
      background-clip: text;
      color: transparent;
      -webkit-text-fill-color: transparent;
      filter: drop-shadow(0 6px 16px rgba(233, 85, 129, 0.22));
      animation: titleSheen 4s ease-in-out infinite;
    }
  }
  @keyframes titleSheen {
    0%, 100% { background-position: 0% 50%; }
    50%      { background-position: 100% 50%; }
  }
  /* 系统设了「减少动态效果」就停掉流光，只留静态渐变 */
  @media (prefers-reduced-motion: reduce) {
    /* ⚠️ 连 filter 一起停（2026-10-14 补）：原来只关了 animation，但 h1 上常驻着
       `filter: drop-shadow(...)`，而 filter 会建合成层、让这个 55px 大标题每一帧都重算。
       前庭功能障碍的用户要的是"完全静止"，只停动画留投影是半吊子。 */
    .topbar h1 { animation: none; filter: none; }
    /* 任务卡的呼吸灯 / 下载转圈也一起停（这两处原来漏了） */
    .task-pulse, .task-pulse::after, .dl-ring.spin, .opt-spin { animation: none; }
    /* 缩略图的微光扫动也别扫了：静止的浅灰占位就够表达"在图了" */
    .res-list a.is-loading::after { animation: none; }
    .res-list a.is-loaded > img { animation: none; }
    /* 提交按钮那个转圈箭头也停掉 —— 转动的图形是最容易诱发不适的一类动效 */
    .form-shell > button[type=submit].is-busy .ico-spin { animation: none; }
    /* 飞向任务列表的箭头同理：整段动画都不要，直接走"数字 +1"的结果态 */
    .fly-ico { display: none !important; }
    /* 副标题徽章的呼吸光晕也停（2026-10-21 加） */
    .subtitle { animation: none; }
    * { scroll-behavior: auto !important; }
  }
  /* 标题下那句引导语（可选），文案走 config.json 的 form.subtitle，没配就隐藏 */
  .subtitle {
    /* 亮哥 2026-10-21：副标题徽章，与下方玫红标签那排在色相上区分开；加一道柔和呼吸光晕（reduce 下关闭） */
    width: fit-content;
    margin: 12px auto 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 7px 16px;
    font-size: var(--subtitle-size);
    font-weight: 600;
    color: var(--accent-purple);
    background: var(--accent-purple-tint);
    border: 1px solid var(--accent-purple-border);
    border-radius: 999px;
    letter-spacing: .01em;
    white-space: nowrap;
    animation: subGlow 2.6s ease-in-out infinite;
  }
  @keyframes subGlow {
    0%, 100% { box-shadow: 0 0 0 0 transparent; }
    50%      { box-shadow: 0 0 11px 1px currentColor; }
  }
  .subtitle::before {
    content: "";
    width: 14px;
    height: 14px;
    flex: none;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%237c5cff' stroke-width='2'%3E%3Cpath d='M5 3h11l3 3v15H5z'/%3E%3Cpath d='M8 3v5h7'/%3E%3Crect x='9' y='12' width='6' height='6'/%3E%3C/svg%3E") no-repeat center / contain;
  }
  .subtitle[hidden] { display: none; }
  /* 标题下的卖点标签（无需注册 / 免费…），文案走 config.json 的 form.taglines */
  .taglines {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--tag-gap);
    margin-top: var(--title-gap);
    margin-bottom: var(--tag-gap-block);   /* 标签和输入区之间再空出一行，别贴太死 */
  }
  .taglines[hidden] { display: none; }
  .tagline {
    padding: 6px 17px;
    font-size: 13.5px;
    font-weight: 700;
    letter-spacing: .01em;
    color: var(--brand);
    background: linear-gradient(180deg, rgba(233, 85, 129, 0.11), rgba(233, 85, 129, 0.05));
    border: 1px solid var(--line-strong);
    border-radius: 999px;
    white-space: nowrap;
    box-shadow: 0 2px 10px var(--focus-ring);
  }

  /* ---------- 右上角图标组（亮哥 2026-10-14，方案五）----------
     文档中心 / 提示词库 / 任务列表 三个入口。原来是三颗白胶囊（.tl-btn / .doc-link），
     跟正文里的「下载到本地」「全部下载」「下载客户端」长得一模一样 ⇒ 用户分不清
     哪些是跳转入口、哪些是操作按钮。现在：
       ① 三颗收成一组，统一放视口右上角（原来两个在左上、一个在右上，视线要跑两个角）；
       ② 做成 34×34 的**方形图标钮**（圆角 10px，不是 999px 胶囊），跟正文的圆角矩形按钮
          在形状上就分开了；图标是内联 SVG，不占文字宽度；
       ③ 上色只用浅色描边 + 半透明白底，落在渐变标题区也不抢眼；hover 才染品牌色。
     名字保留 title / aria-label ⇒ 悬停有 tooltip，读屏也念得出来（去掉了可见文字，
     无障碍信息不能跟着丢）。 */
  .entry-icons {
    /* 亮哥 2026-10-15 收进 .topnav：不再是 fixed，就是导航栏最右的 flex 子项 */
    flex: none;              /* 别被左侧公告挤瘦 */
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 20px;               /* 亮哥 2026-10-17：去掉「|」分隔符后，纯文字与图标的项间距拉宽一点 */
    /* 2026-11-02 亮哥「菜单名之前间距稍调大一点儿」：14 → 20。
       这些项是「文字 + ▾」的纯文字导航，14px 时上一项的箭头几乎贴到下一项的字，
       两组菜单的分界看不出来。窄屏那两档（≤720 的 5px / ≤480 的 4px）不动 —— 那里是纯图标。 */
  }
  .entry-ico {
    position: relative;      /* 角标的定位父级 */
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 34px;            /* 维持导航栏行高对齐（去框后只作占位/点击区） */
    gap: 7px;
    font: inherit;           /* 让 <button> 也用页面字体（否则走 UA 默认） */
    color: var(--muted);     /* 默认低调：入口是辅助导航，不该跟正文按钮抢视觉 */
    background: none;        /* 亮哥 2026-10-17：去掉方框壳；显式清掉 <button> 的 UA 灰底 */
    border: 0;
    border-radius: 0;
    padding: 0;              /* 清掉 <button> 的 UA 默认内边距 */
    text-decoration: none;   /* 不要下划线（仅留图标+文字） */
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: color .16s, opacity .16s;
  }
  .entry-ico:hover { color: var(--brand); }   /* 去框后 hover 仅变色，不再有背景/抬升等"按钮感" */
  .entry-ico:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
  /* 图标旁的文字标签（亮哥 2026-10-16「下载 / 文档 / 联系客服」这种）。
     窄屏（≤720）会隐藏它、只留图标，见下方媒体查询。 */
  .entry-ico__text {
    font-size: 15px;
    font-weight: 600;
    white-space: nowrap;
    line-height: 1;
  }
  /* 顶栏「账号 · 30 积分」里那个数字（亮哥 2026-10-25）：与菜单顶部那行同一色号。
     自己带 color ⇒ 不随 .entry-ico:hover 变品牌色（余额一直保持红的，别看着像链接）。
     等宽数字：余额从 999 跳到 1000 时不会左右抖。 */
  .entry-ico .credits-red {
    color: var(--danger);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
  }
  /* 顶栏「昵称 · N 积分」里的昵称（亮哥 2026-10-25：把「账号」换成昵称）。
     昵称是用户自己填的，长度不可控 ⇒ 截断加省略号，别让它把导航撑爆、
     把「合作 / 定价」挤成两行。全名在按钮的 title 里（悬停能看到）。 */
  .entry-ico .ico-uname {
    display: inline-block;
    max-width: 7em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: bottom;
  }
  /* 导航图标（亮哥 2026-10-25：整体调大一点）。只作用于项内的直接子图标，
     不影响下拉菜单里的 svg；窄屏媒体查询里的 .entry-ico svg（16/15px）会覆盖本规则。 */
  .entry-ico > svg {
    width: 20px;
    height: 20px;
  }

  /* ---------- 顶栏下拉菜单（亮哥 2026-10-16 起：图标正下方弹出子菜单）----------
     ① .entry-ico-wrap 只是图标+菜单的定位父级，不改变图标组布局（仍是 34×34 一格）。
     ② 下拉用 opacity+visibility 过渡，**开合只认 .is-open**（见下面那条，2026-11-02 改）；
        ::before 透明桥原本是给 hover 填补图标与卡片之间 8px 间隙用的，改成点击后
        留着无害（鼠标从菜单里划过时不至于掉出下边界），先不动。
     ③ topnav 无 overflow 裁切、z-index 30，下拉绝对定位在其下方、z-index 40 压住正文卡片。 */
  .entry-ico-wrap {
    position: relative;
    display: inline-flex;
  }
  .entry-menu {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    min-width: 144px;
    padding: 6px;
    background: var(--panel);
    border: 1px solid var(--line-strong);
    border-radius: 12px;
    box-shadow: var(--shadow-card);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-4px);
    transition: opacity .15s ease, transform .15s ease, visibility .15s ease;
    z-index: 40;
  }
  .entry-menu::before {
    content: "";
    position: absolute;
    left: 0; right: 0; bottom: 100%;
    height: 10px;
  }
  /* 展开态：**只认 .is-open**（JS 切这个 class），没有 hover / focus-within 展开。
     亮哥 2026-11-02：「把有子菜单的菜单全部修改为点击展开，与账户那一个菜单效果一致」。
     原来「实用工具 / 下载 / 文档」三颗是 hover 就弹 —— 鼠标横穿导航栏去点右边那个，
     沿途会连着闪三次，误触比方便更多。统一成：点按钮开、点菜单项 / 点外面 / Esc 收。
     （选 .entry-menu 而不是 .entry-menu--click：--click 现在只剩「更宽」这一个含义，
       账号菜单 min-width 200px、客服菜单在下面自己覆盖定位，都还带这个 class。） */
  .entry-menu.is-open {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
  }
  .entry-menu__item {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    border-radius: 8px;
    color: var(--text);
    background: transparent;
    font-size: 13.5px;
    font-weight: 600;
    white-space: nowrap;
    text-decoration: none;
    transition: color .14s ease, background .14s ease;
  }
  .entry-menu__item:hover {
    color: var(--brand);
    background: var(--brand-tint);
  }
  /* 菜单项也用作 <button>（账号/登录）：抹掉浏览器默认按钮样式，外观与 <a> 项一致 */
  button.entry-menu__item {
    width: 100%;
    border: 0;
    cursor: pointer;
    font-family: inherit;
    text-align: left;
  }
  /* ⛔ 上面 .entry-menu__item 的 display:flex 会盖掉 [hidden] 自带的 display:none
     （作者样式优先于浏览器默认）⇒ 显式补一条，否则 hidden 的菜单项照样显示。 */
  .entry-menu__item[hidden] { display: none; }
  /* 用户菜单（点击式）：更宽、底部「退出登录」用分隔线隔开。
     （原来顶部那块「用户名 · N 积分」的表头 2026-10-25 删了：昵称和积分都挪到顶栏入口上，
       菜单里再挂一行纯属重复，对应的 .entry-menu__head 样式也一起删掉。） */
  .entry-menu--click { min-width: 200px; }
  .entry-menu__item--last {
    margin-top: 6px;
    border-top: 1px solid var(--line);
    border-radius: 0 0 8px 8px;
    padding-top: 12px;
  }
  /* 任务列表这颗：唯一「有状态」的入口（其余两颗是跳转），所以给它品牌色打底 */
  .entry-ico--task {
    color: var(--accent-text);
  }

  /* ============ 右侧客服悬浮（亮哥 2026-10-20）============ */
  /* 整页滚动时图标固定在右侧居中：position:fixed 的包裹层 + translateY(-50%) 垂直居中，
     "随页面滚动"即不随内容流走。
     ⚠️ 必须挂 body 直接子级（.card 有 overflow 会裁掉、.drawer 有 transform 会成为内部
        fixed 的包含块），理由同 #btnTop；z-index 20 垫低，抽屉(41)/toast(50)/弹窗(60)
        照旧能压住它。
     ⚠️ wrap 带 transform ⇒ 它是内部绝对定位元素的包含块（菜单正靠这个相对它定位），
        但 #kefuQrModal 不能放进来（弹窗的 fixed;inset:0 会变成相对 wrap）——
        弹窗在 index.html 里挂在 wrap 外面，同 .drawer 铁律。 */
  .kefu-fab-wrap {
    position: fixed;
    right: 24px;
    top: 50%;
    transform: translateY(-50%);
    z-index: 20;
    display: inline-flex;
  }
  /* 图标本身：跟 .to-top 同一张脸（44 圆钮 + 品牌色 + 同款投影），保持右下角语言一致 */
  .kefu-fab {
    width: 44px; height: 44px;
    display: flex; align-items: center; justify-content: center;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--brand);
    color: var(--btn-fg);
    box-shadow: 0 6px 18px rgba(233, 85, 129, .32);
    cursor: pointer;
    transition: transform .18s ease;
    /* 间隔跳动动效（亮哥 2026-10-22）：每 2.4s 跳一下，吸引注意但不打扰；
       animation-composition:add 让「跳动」与 hover/active 的位移叠加而非互相覆盖 */
    animation: kefu-bounce 2.4s ease-in-out infinite;
    animation-composition: add;
  }
  .kefu-fab:hover { transform: translateY(-2px); }
  .kefu-fab:active { transform: translateY(0); }
  .kefu-fab:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; }
  /* 客服按钮间隔跳动：静止约 1.7s，再快速上跳一次回弹，无限循环。
     translateY 用 add 合成，所以悬停上浮（-2px）仍然叠加可见。 */
  @keyframes kefu-bounce {
    0%, 72%, 100% { transform: translateY(0); }
    80% { transform: translateY(-12px); }
    88% { transform: translateY(0); }
  }
  /* 用户系统开启了「减少动态效果」时，关掉跳动，避免引起不适 */
  @media (prefers-reduced-motion: reduce) {
    .kefu-fab { animation: none; }
  }

  /* ===== gpt-image-2 模式切换 + 面板 ===== */
  .mode-switch {
    display: inline-flex;
    gap: 6px;
    margin: 14px 0 4px;
    padding: 3px;
    background: var(--bg-soft, #e3e6fd);
    border-radius: 999px;
  }
  .mode-btn {
    border: 0;
    background: transparent;
    color: var(--text-soft, #5c5f85);
    padding: 6px 16px;
    border-radius: 999px;
    font-size: 14px;
    cursor: pointer;
    transition: background .15s, color .15s;
  }
  .mode-btn.active {
    background: var(--brand, #e95581);
    color: #ffffff;
    box-shadow: 0 2px 6px rgba(79,124,255,.3);
  }
  .gpt-panel { margin-top: 10px; }
  .gpt-row {
    display: flex;
    align-items: center;
    gap: 14px;
    flex-wrap: wrap;
  }
  .gpt-n-label {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-size: 14px;
    color: var(--text-soft, #5c5f85);
  }
  .gpt-n-input {
    width: 64px;
    padding: 6px 8px;
    border: 1px solid var(--border, rgba(233, 85, 129, 0.22));
    border-radius: 8px;
    font-size: 14px;
  }
  .gpt-gen-btn { min-width: 140px; }
  .gpt-hint {
    margin-top: 8px;
    font-size: 12px;
    color: var(--text-soft, #8a8db0);
  }
  .gpt-result {
    margin-top: 14px;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
    gap: 12px;
  }
  .gpt-card {
    border: 1px solid var(--border, rgba(233, 85, 129, 0.22));
    border-radius: 12px;
    overflow: hidden;
    background: #ffffff;
    display: flex;
    flex-direction: column;
  }
  .gpt-img {
    width: 100%;
    aspect-ratio: 1 / 1;
    object-fit: cover;
    background: var(--bg-soft, #e3e6fd);
  }
  .gpt-card .gpt-dl {
    margin: 8px;
    text-align: center;
    text-decoration: none;
  }
  .gpt-meta {
    padding: 0 8px 8px;
    font-size: 11px;
    color: var(--text-soft, #8a8db0);
    text-align: center;
  }
  /* 菜单复用 .entry-menu--click 的开合（JS 切 .is-open），只改定位：右侧边缘没空间，
     改成朝左弹出 —— right:calc(100% + 12px)（贴wrap左侧外移12px）+ top:50% +
     translateY(-50%) 跟图标垂直居中。
     特异性说明：本条是 (.kefu-fab-wrap .entry-menu)=两个类，压过 .entry-menu 的定位；
     与 .entry-menu--click.is-open 同级但写在更后面 ⇒ transform 由这里定，
     is-open 只负责翻 opacity/visibility，居中态不受影响。 */
  .kefu-fab-wrap .entry-menu {
    top: 50%;
    right: calc(100% + 12px);
    transform: translateY(-50%);
    min-width: 132px;
  }
  /* ::before 透明桥是给「正下方下拉」补鼠标间隙的：朝左弹用不上，还会变成悬在菜单
     上方的一条透明点击区（挡住底下的页面）—— 直接关掉。 */
  .kefu-fab-wrap .entry-menu::before { display: none; }
  /* 菜单项图标用品牌色，跟顶栏下拉的 currentColor 区分开，也跟圆钮呼应 */
  .kefu-fab-wrap .entry-menu__item svg { color: var(--brand); flex: none; }
  /* 小屏：边距收窄、尺寸缩一档（44 → 40），跟 .to-top 的媒体查询对齐 */
  @media (max-width: 720px) {
    .kefu-fab-wrap { right: 14px; }
    .kefu-fab { width: 40px; height: 40px; }
    .kefu-fab svg { width: 18px; height: 18px; }
  }

  /* 客服二维码弹窗：在登录弹窗（360px）基础上加宽一档，图居中、白底圆角 */
  .kefu-qr { width: 320px; text-align: center; }
  .kefu-qr .modal-title { margin-bottom: 12px; }
  .kefu-qr__imgwrap {
    background: #ffffff;
    border: 1px solid var(--line);
    border-radius: 12px;
    padding: 10px;
  }
  .kefu-qr__imgwrap img {
    display: block;
    width: 100%; height: auto;
    border-radius: 6px;
  }
  .kefu-qr__note {
    margin: 12px 0 0;
    font-size: 13px;
    line-height: 1.7;
    color: var(--muted);
  }
  .entry-ico--task:hover { color: var(--brand); }   /* 去框后仅变色，计数角标仍表状态 */
  /* 任务数角标：钉在图标右上角，做成「小红点 + 数字」而不是原来的内联小胶囊 ——
     胶囊会把按钮撑宽、三个图标就参差不齐了；绝对定位则一颗不多占宽度。
     数字从 1 位涨到 2、3 位时角标会向左上撑开，故用 translate 把它锚在右上角。 */
  .entry-badge {
    position: absolute;
    top: -4px;
    right: -4px;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 10.5px;
    font-weight: 700;
    line-height: 1;
    color: #ffffff;
    background: var(--brand);
    border: 2px solid #ffffff;   /* 描一圈白边，压在渐变背景上也分得清 */
    border-radius: 999px;
    box-shadow: 0 2px 6px rgba(233, 85, 129, .35);
  }
  .entry-ico--task:hover .entry-badge { background: #ffffff; color: var(--brand); border-color: var(--brand); }

  /* 提交成功后那枚「飞过去的箭头」—— 由 app.js 的 flyToTaskList() 动态插入 body 后自删。
     起点/终点差值走 --fly-x / --fly-y 两个自定义属性，JS 只量一次矩形、不碰动画细节。
     ⚠️ z-index 必须高于抽屉(41)与遮罩(40)：抽屉开着时提交也要能看见它飞过；
        低于 toast(z-index 见上)，别把成功提示压住。 */
  .fly-ico {
    position: fixed;
    left: 0;
    top: 0;
    z-index: 45;
    color: var(--brand);
    pointer-events: none;
    will-change: transform, opacity;
    /* ⚠️ 别用带负值的贝塞尔（如 cubic-bezier(.5,-0.2,.3,1)）：它会先把箭头往反方向
       「蓄力」一下（实测 t=99~220ms 期间 x 1123→1117→1123、y 624→637→625 原地抖），
       看着像卡了半拍。这里改成全程单调加速、末段缓出的曲线。 */
    animation: flyToTask .55s cubic-bezier(.32, 0, .35, 1) forwards;
  }
  @keyframes flyToTask {
    0%   { transform: translate(0, 0) scale(1); opacity: 1; }
    70%  { opacity: 1; }
    100% { transform: translate(var(--fly-x), var(--fly-y)) scale(.5); opacity: 0; }
  }
  /* 图标被"砸中"的那一下：先弹大再回位，配一点点亮，让「+1」有落点感。 */
  .entry-ico.is-hit { animation: icoHit .42s ease-out; }
  @keyframes icoHit {
    0%   { transform: scale(1); }
    35%  { transform: scale(1.22); }
    70%  { transform: scale(.94); }
    100% { transform: scale(1); }
  }
  /* 角标单独再弹一次 —— 它和图标是两个元素，只弹图标的话数字变化容易被忽略。 */
  .entry-ico.is-hit .entry-badge { animation: badgeHit .42s ease-out; }
  @keyframes badgeHit {
    0%   { transform: scale(1); }
    35%  { transform: scale(1.35); }
    100% { transform: scale(1); }
  }

  /* ---------- 主输入区 ---------- */
  .compose {
    flex: 0 0 auto;          /* 不再占满剩余高度 —— 让标题+输入区作为整体跟着 .card 居中 */
    min-height: 0;
    display: flex;
    flex-direction: column;
    padding: 0 0 6px;
    /* ⚠️ 亮哥 2026-10-15「滚轮失效」的第二个真凶：这里原先同时有
       `overflow-y:auto` + `overscroll-behavior:contain` ⇒
       ① overflow-y:auto 让它成了**滚动容器**（"可滚祖先"）；
       ② overscroll-behavior:contain 又**禁止滚动链传给父级**。
       结果：鼠标停在输入区上滚轮时，它自己滚不动、又不许页面接手 ⇒ 整个区域滚轮死掉。
       现在页面已是整页滚动（html 承担），输入区不需要自己滚 ⇒ 两条一起删。 */
  }

  /* 模型选择框（亮哥 2026-10-15 二次改）：挤进表单选项行、放在「图片尺寸」前面，
     跟其他选项一个样 —— 没标签、44 高、无原生箭头。
     ⚠️ 高度 44 和 appearance:none 不用在这里写 —— 它在 #form 里，
     上面那两条 `#form select` 规则已经罩住它了；这里只管宽度。
     ⚠️ 自绘的 ▼ 也不要（亮哥 2026-10-15 三次改）：「图片尺寸」那个框本来就没箭头，
        再给模型框画一个就又不一致了 —— 排在一起时以**没有箭头**为准。 */
  #form .opt.model-opt { flex: 0 0 auto; position: relative; }
  /* 模型选择框：选项行里 C 位，给它一点品牌辨识度 —— 极淡粉底 + 玫红左条 + 略大圆角，
     比旁边白底的其他 select 精致、又能一眼看出「这是选模型的地方」。
     ⚠️ 仍然无箭头（和「图片尺寸」一致，亮哥 2026-10-15 定的）；
     ⚠️ 高度 44 与 appearance:none 由上方 #form select 罩着，这里不重写得，免得和那两条打架；
         要盖 background/border 必须用「#form .model-sel」这个特异性 ≥ #form select 的选择器。 */
  #form .model-sel {
    width: auto;
    min-width: 124px;
    max-width: 220px;
    border: 1px solid var(--line-strong);
    border-left: 3px solid var(--brand);
    background: var(--brand-tint);
    color: var(--text);
    font-weight: 500;
    border-radius: 10px;
    padding: 0 14px;
    cursor: pointer;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55);
    transition: border-color .15s ease, background .15s ease, box-shadow .15s ease;
  }
  #form .model-sel:hover {
    border-color: var(--brand);
    background: rgba(233, 85, 129, 0.14);
  }
  #form .model-sel:focus {
    border-color: var(--brand);
    background: var(--focus-bg);
    box-shadow: 0 0 0 4px var(--focus-ring);
    outline: none;
  }

  /* 表单：主字段占满一行，其余字段（.opt）像芯片一样横向排开 */
  #form { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-start; }
  /* 垂直居中改由 .card 的 justify-content 管（标题+输入区整体居中），这里不再用 margin:auto */
  /* ⚠️ 这条不能省：作者样式里的 display 会盖掉 [hidden] 的 UA 样式（display:none）。
     #form 初始带 hidden，少了这条，页面刚打开、字段还没渲染时就会露出一个空白框
     —— 看起来就像"表单加载不出来"。 */
  #form[hidden] { display: none; }
  /* 「切模型中」：表单**不藏**，只是轻微变淡 + 挡住点击（亮哥 2026-10-15
     「重选模型会触发表单加载，如何让用户无感」）。
     老做法是 elForm.hidden = true 露出「正在加载表单…」，整块消失再出现，视觉上狠狠闪一下；
     现在就地一个柔和的透明度变化，等字段到位后原地换掉 —— 布局不动、不跳、不白屏。
     ⚠️ 别用 visibility/display（那就是"藏起来"了，白搭）；pointer-events 只在切换的这一小会儿挡住。 */
  #form.is-swapping { opacity: .55; pointer-events: none; transition: opacity .12s ease; }
  #form > * { margin-bottom: 0; }
  /* 「尺寸 / 数量」这类 inline 字段原本被包进 .field-row 想并排 —— 用 contents 让它透明，
     子元素直接参与上面那个 flex 排列 */
  #form .field-row { display: contents; }

  /* 提示词：就一个干净的大输入框，不要标签页 */
  .field-primary {
    position: relative;
    width: 100%;
  }
  .field-primary > label { display: none; }
  .field-primary textarea {
    min-height: 198px;
    font-size: 17px;
    line-height: 1.7;
    border-radius: 14px;
  }

  /* 四个选项：直接平铺在输入框下面，不再折叠成芯片。
     flex-basis 用 calc(25% - 9px)：4 个加起来 + 3 个 gap 正好 = 整行 100%，
     于是提交按钮必然被挤到下一行（width:100% 那招没用 —— max-width 也参与换行计算，
     会在算尺寸时就把按钮收窄，结果又跟选项挤回同一行）。
     ⚠️ 字段数量如果变了，这里的 25% 和 9px 要跟着调。
     align-items: stretch 让四格等高，看起来才是整齐的一行。 */
  /* 选项行：尺寸和数量窄、参考图和目录宽、提交按钮跟在最右边 —— 全排成一条线。
     align-items: flex-start 让四格的「字段名」顶边对齐；
     按钮没有字段名，所以单独给它 margin-top 让到和控件同一水平线（≈ 13px 字高 + 6px 间距）。 */
  #form { align-items: flex-start; }
  .opt { min-width: 0; display: flex; flex-direction: column; }

  /* 按字段名分配宽度（data-field 由 addField 写在**外层 wrap** 上）：
     「图片尺寸 / 生成数量」的内容本来就只有几个字符，给窄一点；
     参考图要放下上传区、目录要显示完整 placeholder，给宽一点。
     窄屏排不下时 flex-wrap 会自己折行，按钮上有 margin-left:auto 收在行尾。 */
  #form .opt[data-field="图片尺寸"]       { flex: 0 0 92px; }
  /* 生成数量：只有一个 1~3 位数，缩到 64（亮哥 2026-10-13）——也是为了让提交按钮能挤进同一行 */
  #form .opt[data-field="生成数量"]       { flex: 0 0 64px; }
  /* 参考图这格：上传框改成了贴着内容收（2026-10-13 二次改），格宽跟着内容走，不再写死 160。
     报错文字很长时靠 .upload-hint 的 max-width 兜底，别让这格把整行撑爆。 */
  #form .opt[data-field="参考图"]         { flex: 0 0 auto; }
  /* 目录这格（亮哥 2026-10-13）：输入框宽度跟 placeholder 提示文案一样长 ——
     格子跟着贴内容收；真正的宽度数值是 JS 量的（CSS 量不了 placeholder，见 fitPlaceholderWidth） */
  #form .opt[data-field="下载到本地目录"] { flex: 0 0 auto; }

  /* 每个选项上面一行小字当标识（纯文字，不带圆点）
     ⚠️ 这行标签**已经不要了**（亮哥 2026-10-14）：控件靠自己的形态（带比例小框的下拉、
     数字框、上传图标、带路径 placeholder 的输入框）自解释，再顶一行「图片尺寸/生成数量」
     属于重复噪音。刻意用 display:none 而不是删节点 —— ① 它是控件的可访问名（点标签能聚焦、
     读屏靠它念出字段名）；② 「参考图」那格是按 label 做折叠开关的（见 app.js 的 toggleOpt），
     删了折叠就没了。整体想恢复把这条注释掉即可。 */
  #form .opt > label {
    display: none;
  }
  /* 必填星号：提示词那格已经换成大输入框了，这里只有选填字段，别把 * 画出来 */
  #form .opt > label .req { display: none; }

  .opt-body { display: block; }
  .opt-body > input,
  .opt-body > select { width: 100%; }
  .opt-body textarea { min-height: 88px; }

  /* 参考图那格把上传区压紧一点，别比其他几格高出一大截 */
  /* 参考图上传区（2026-10-13 三次改）：虚线框要完全包住按钮/缩略图、不留空隙，
     padding 归零；高度锁死 44px 跟同一行的输入框对齐（实测 数量/目录 input = 44px）。
     框 44 = 内容 42 + 上下边框各 1，按钮/缩略图占满内容区 ⇒ 对齐了、也仍然零空隙。
     （尺寸 select 实测 42，本来就比 input 矮 2px，跟着 input 走。） */
  #form .upload { height: 44px; }
  /* 缩略图不能比虚线框还高 —— 原来写死 72×72，把框撑破了。
     42 正好是框的内容高（44 − 上下边框各 1px）；非方图靠 object-fit:cover 裁中间。 */
  #form .upload-thumb { width: 42px; height: 42px; border-radius: 7px; }
  /* 按钮填满虚线框（亮哥 2026-10-13）：body 先拉伸到整高，按钮再撑满 body。
     ⚠️ 只给按钮写 height:100% 没用 —— 它的父级 .upload-body 是 align-items:center 的行，
     自身高度只有按钮内容高（实测 23px），撑不出 42。
     按钮自己的边框/底色同时去掉：虚线框就是它的唯一边界，不再有「大框里套个小按钮」的双线。
     四次改（同日）：按钮文字换成云上传图标 —— 定宽 42 拉成正方形，图标用 currentColor 描边，
     悬停变红自动生效。 */
  #form .upload-btn {
    height: 100%;
    width: 42px;
    padding: 0;
    border: 0;
    border-radius: 7px; /* 框内沿圆角 = 框的 8 − 边框 1 */
    background: transparent;
    font-size: 13px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  /* 悬停时字体变红（亮哥 2026-10-13）：原来的 --hover-fg 在粉色底上几乎看不见 */
  #form .upload-btn:hover { color: var(--brand); }
  /* nowrap：这行提示要一行放下 —— 上面把参考图那格加宽就是为了它 */
  /* 按钮和提示排成一行：提示跟到「选择图片」后面，不再单独占一行 */
  #form .upload-body { display: flex; align-items: center; gap: 10px; align-self: stretch; }
  /* 上传成功后按钮藏了、提示也空了，JS 会把 body 整个 hidden 掉 ——
     不补这条的话，上面的 display:flex 会盖掉 [hidden]，body 还占着一个 gap 的空位 */
  #form .upload-body[hidden] { display: none; }
  /* 按钮同款坑（2026-10-14 二踩）：上面 #form .upload-btn 的 display:inline-flex 会盖掉
     [hidden] 的 UA 规则 —— 上传中/传完后 JS 明明把 btn.hidden 置了 true，云图标还杵在那。
     探针截图抓到的：属性是 hidden、画面上却看得见。必须用同权重的带 #form 规则压回去。 */
  #form .upload-btn[hidden] { display: none; }
  /* 不再 nowrap：默认说明文字已去掉（2026-10-13），这行现在只在报错时出现，
     而错误信息（"上传失败：…"）可能很长，必须允许折行，否则会撑破这格。 */
  #form .upload-hint { font-size: 12px; margin-top: 0; line-height: 1.4; max-width: 240px; }
  /* 上传成功后提示行被清空（不再报「已上传：xxx」），整块收掉，别留一截空白 */
  #form .upload-hint:empty { display: none; }

  /* 比例标识（「图片尺寸」那格）：小矩形画当前画面比例，叠在选中值前面。
     亮哥 2026-10-15：「自动匹配」这类**没有数字比例**的选项前不画框，其他比例值保留。
     box 上加 .no-ico 时不预留左内边距（否则「自动匹配」左边空一块、看着像缺了个图标）。 */
  .ratio-box { position: relative; }
  .ratio-box > select { padding-left: 40px; }
  .ratio-box.no-ico > select { padding-left: 12px; }
  /* GPT-Image-2 尺寸锁死「自动匹配」（2026-10-26 亮哥定）：整格变淡且点不动。
     必须打在 .ratio-box 上 —— 自绘下拉 .ratio-menu 是它的子节点、独立 DOM，
     只给 <select> 加 disabled 挡不住那套菜单。 */
  .ratio-box.is-locked { opacity: .62; pointer-events: none; }
  .ratio-box.is-locked > select { cursor: not-allowed; }
  .ratio-ico {
    position: absolute;
    left: 13px;
    top: 50%;
    transform: translateY(-50%);
    border: 1.5px solid var(--brand);
    border-radius: 2px;
    box-sizing: border-box;
    pointer-events: none;    /* 别挡住点击和下拉 */
  }
  .ratio-ico[hidden] { display: none; }

  /* 自己画的下拉菜单：fixed 定位（JS 按 select 的视口坐标摆位）。
     不能用 absolute 挂在 select 正下方 —— 会被 .card 的 overflow:hidden 裁掉
     （实测菜单只露出 select 底边一条，下面的全没了）。 */
  .ratio-menu {
    position: fixed;
    z-index: 40;
    padding: 5px;
    background: var(--panel-solid);
    border: 1px solid var(--line-strong);
    border-radius: 10px;
    box-shadow: 0 12px 30px rgba(58, 47, 110, 0.18);
    overflow-y: auto;
  }
  .ratio-menu[hidden] { display: none; }
  .ratio-item {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 6px 9px;
    font-size: 13px;
    line-height: 1.4;
    color: var(--text);
    border-radius: 6px;
    cursor: pointer;
    white-space: nowrap;
  }
  .ratio-item:hover { background: var(--brand-tint); }
  .ratio-item.is-on { color: var(--accent-text); font-weight: 600; background: var(--brand-tint); }
  /* 菜单里的图标跟着排，不要绝对定位 */
  .ratio-item > .ratio-ico { position: static; transform: none; flex: none; }

  /* ---- 「一整块」输入区 ----
     提示词框和下面那行控件（尺寸/数量/参考图/目录/提交）共用同一个边框，
     看起来是一整个输入区。外壳是 JS 在字段生成完之后搬 DOM 建的（见 buildForm）。 */
  .form-shell {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;     /* 控件行底边对齐（各格带不带 label 都齐） */
    column-gap: 10px;          /* 12 → 10：为了让「控件 + 提交按钮」挤进同一行（亮哥 2026-10-13） */
    row-gap: 4px;              /* 14 → 4：分隔线到控件行标签之间那一截空白去掉（亮哥 2026-10-13） */
    /* 亮哥 2026-10-13：这格占屏幕宽的 4/6（≈66.67vw）、高的 30vh（原为 1/3 屏，11-02 调低）。
       水平居中靠 margin:auto（#form 是 flex 行容器，auto margin 吃掉两侧剩余空间）；
       垂直方向跟标题作为整体由 .card 的 justify-content:center 居中 ——
       标题还压在框上方，框的正中会比屏幕正中低约半个标题高，要绝对居中得先把标题挪开。 */
    width: 66.6667vw;
    /* 高度锁死 30vh（不是 min-height —— min 会被内容撑高，实测撑到 368）。
       控件行在 60vw 下常常要折成两行，多出来的高度由 align-content:stretch 分给各行，
       提示词区再自己撑满第一行（见 .field-primary），所以框高恒定、不会溢出。
       min-height 兜极端矮屏：宁可略高于 30vh，也不要让内容顶出边框。
       ⚠️ 2026-11-02 亮哥「把这个框高度调低一点儿」：原来是 1/3 屏（33.333vh），
          收到 30vh（900 高的窗口 ≈ 300 → 270，矮一档 30px）；min-height 同步
          280 → 250，否则视口高 ≤933 时会被 min 卡住、调了等于没调。
          textarea 的 calc 里那个 33.333vh 必须一起改（见 .field-primary textarea）。 */
    height: 30vh;
    min-height: 250px;
    max-width: 100%;
    margin: 0 auto;
    /* 上下内边距：顶部 14/16 → 8 → 4 → **0**（亮哥 2026-10-14：去掉标签与虚线后顶部留白
       失去参照物，一路收到 0；此时文字距壳顶只剩 textarea 自身的 2px，已是极限，
       再往下就得动字号/行高了）。底部 = 16 = 与左右 --shell-pad-x 相同。 */
    padding: 0 var(--shell-pad-x) 16px;
    border: 1px solid var(--line-strong);
    border-radius: var(--shell-radius);
    background: var(--panel-solid);
    box-shadow: 0 4px 18px rgba(58, 47, 110, 0.07);
    /* 2026-11-02：聚焦光边 / 扫光两个 ::before/::after 装饰层要相对外壳定位。
       ⚠️ 别再给它加 overflow:hidden —— 壳里的模型下拉是绝对定位、会溢出边界，一裁就没了；
       扫光因此改成 background-position 动画（见下面那段），不靠裁切。 */
    position: relative;
  }
  /* ---------- 输入区动效（亮哥 2026-11-02）----------
     三件在 CSS：聚焦光边 / 提交按钮波纹 / 提交成功扫光；第四件「占位符轮播打字机」在 app.js。
     两条铁律：
       ① 装饰层全部 pointer-events:none —— 再花哨也不能挡点击；
       ② 段末有一条 prefers-reduced-motion 把它们全关掉，跟标题流光、转圈那套保持一致。 */

  /* ① 聚焦光边：光标进到输入区，一圈渐变光沿边框绕行（3 秒一圈）。
     conic-gradient 转角度靠 @property 注册的自定义属性，纯 CSS、不需要 JS。
     不支持 @property 的浏览器（老 Safari）会退化成**静态**渐变边 —— 仍然好看，不影响使用。 */
  @property --ys-glow-a { syntax: '<angle>'; initial-value: 0deg; inherits: false; }
  .form-shell::before {
    content: '';
    position: absolute;
    inset: -1px;                       /* 压住外层那条 1px 的 --line-strong，别露双线 */
    border-radius: calc(var(--shell-radius) + 1px);
    padding: 1.5px;                    /* 光的粗细 */
    background: conic-gradient(from var(--ys-glow-a),
      transparent 0deg, transparent 200deg,
      var(--brand) 250deg, #7f77dd 300deg, transparent 350deg);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask-composite: exclude;           /* 两条 mask 异或 ⇒ 只留 padding 那一圈，中间透空 */
    opacity: 0;
    transition: opacity .35s ease;
    pointer-events: none;
  }
  /* 用 :focus-within 而不是 :has(textarea:focus)：前者老浏览器也认，代价是点开壳里任何一个
     控件（模型下拉、数量框）也会亮 —— 但那恰恰说明"人在这块区域里"，不算错。 */
  .form-shell:focus-within::before {
    opacity: 1;
    animation: shellGlow 3s linear infinite;
  }
  @keyframes shellGlow {
    from { --ys-glow-a: 0deg; }
    to   { --ys-glow-a: 360deg; }
  }

  /* ② 提交成功扫光：一道斜向高光自左向右刷过整块壳（0.62 秒）。
     ⚠️ 关键做法：**不移动元素**，而是给 inset:0 的整面做 background-position 动画。
     移动元素就必须给 .form-shell 加 overflow:hidden，可壳里的模型下拉是绝对定位、溢出边界的，
     一裁就没了。用 background-position 天然不出界，也不需要裁。 */
  .form-shell::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    /* ⚠️ 别只画白色高光：外壳底色本身就是近白的 --panel-solid，纯白刷过去看不出来（实测第一版就是这样，
       截图上什么都没有）。改成"粉 → 白 → 紫"的窄带，靠两侧的色差把这条光显出来，
       中间那道白只是让带子看起来有亮度层次。 */
    background: linear-gradient(105deg,
      transparent 40%,
      rgba(233, 85, 129, .16) 46.5%,
      rgba(255, 255, 255, .92) 50%,
      rgba(127, 119, 221, .14) 53.5%,
      transparent 60%);
    background-size: 280% 100%;
    background-position: 135% 0;
    opacity: 0;
    pointer-events: none;
    z-index: 2;                        /* 压在控件之上扫过去；不吃事件，所以不影响操作 */
  }
  .form-shell.is-swept::after { animation: shellSweep .62s ease-out; }
  @keyframes shellSweep {
    0%   { background-position: 135% 0; opacity: 0; }
    18%  { opacity: 1; }
    100% { background-position: -35% 0; opacity: 0; }
  }

  /* ③ 提交按钮的波纹（按下回弹写在下一条规则里，跟 hover 的 scale 同级）。
     波纹画在按钮自己的圆里，scale 到 2.35 倍散出去；.is-firing 由 app.js 点击时加、动画结束摘掉。 */
  .form-shell > button[type=submit]::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 50%;
    border: 2px solid var(--brand);
    opacity: 0;
    pointer-events: none;
  }
  .form-shell > button[type=submit].is-firing::after { animation: btnPulse .72s ease-out; }
  @keyframes btnPulse {
    0%   { opacity: .85; transform: scale(1); }
    100% { opacity: 0;   transform: scale(2.35); }
  }

  /* ④ 关掉动效：系统设了「减少动态效果」就一律不演 —— 沿边框绕圈和整块扫光对前庭敏感的人
     是最难受的两类。轮播打字机在 app.js 里也单独挡了一道。 */
  @media (prefers-reduced-motion: reduce) {
    .form-shell::before,
    .form-shell::after { animation: none !important; opacity: 0 !important; }
    .form-shell > button[type=submit]::after { display: none; }
    .form-shell > button[type=submit]:active:not(:disabled) { transform: none !important; }
  }

  /* 外壳内部的行距由外壳自己的 row-gap 管，别再叠 `.field` 自带的 margin-bottom
     —— 不然后面那半截的底边会差一个 margin 的高度（实测差 13px，怎么都对不齐）。
     ⚠️ `.opt` 是 .field-row 的子元素（那层是 display:contents），所以不能只写 `>` 选择器。 */
  .form-shell > .field,
  .form-shell .opt { margin-bottom: 0; }

  /* 提示词占满第一行；它自己不再画边框 —— 边框归外层，这样才是一整块 */
  .form-shell > .field-primary {
    flex: 1 1 100%;
    /* 亮哥 2026-10-14：原来这里有一条粉色虚线，把「写提示词」和「下面那行控件」劈成两半；
       整块壳子本来就是一体的，多这条线反而显得是两个东西。去掉后底部不留 padding，
       省下的高度全部让给上面的输入框（见下面 textarea 的 -86px）。 */
    padding-bottom: 0;
    /* 亮哥 2026-11-02：首行文字离上边框比离左右边框宽，要往回收。
       根因在 .form-shell 的 align-items:flex-end —— 壳高固定 30vh、内容比它矮，
       富余被 align-content:stretch 均分给各行，第一行行盒比 textarea 高约 8px，
       flex-end 又把 textarea 压到行盒底部 ⇒ 顶部凭空多出 8px。
       不给整块改 align-content（改了富余会全堆到底部、控件行下面空一大块），
       只让这一格单独贴行盒顶：首行位置此后完全由 textarea 自己的 padding-top 决定。 */
    align-self: flex-start;
  }
  .form-shell .field-primary textarea {
    /* 高度 = 框的 30vh 减去框里其它部分。
       118 = 上下 padding 30 + 分隔线区 13 + 行距 4 + 控件行 ≈71（有虚线那版的口径）；
       2026-10-14 去掉选项行标签(≈25)和那条虚线(≈14)后改成 92 —— 差额补给输入框；
       同日顶部 padding 一路收到 0（累计再省 30）⇒ 74；底部 padding 由 4 调回 16 与左右一致（多占 12）⇒ 定稿 86。
       始终保证壳高恒定（2026-11-02 由 33.333vh 收到 30vh）、里面不留空档。
       短屏时由 min-height 兜底，框宁可略高于 30vh 也不挤烂控件行。 */
    height: calc(30vh - 86px);
    min-height: 88px;
    padding: 12px 2px 0;
    border: 0;
    background: transparent;
    box-shadow: none;
    /* 关掉右下角那个拖拽缩放角标（亮哥 2026-10-14）：高度是定值算出来的，
       手动拖了也会被下一段渲染顶回去，留着只会让人以为是能拖的、拖完又弹回。 */
    resize: none;
  }
  .form-shell .field-primary textarea:focus { box-shadow: none; }

  /* 提交按钮：改成了红色向右箭头图标（亮哥 2026-10-13）——
     不再要红底白字的大按钮：currentColor 红箭头，仍跟控件底边对齐、贴着外壳内右缘；
     文案（提交任务 / 提交中… / 满载原因）全在 title / aria-label 里，由 updateSubmitState 维护。
     ⚠️ 高度 44（亮哥 2026-10-14「跟前面的输入框一样高」）：数量/目录 input 实测 44、
     尺寸 select 42、参考图虚线框 44，按钮跟它们齐平，整行视觉终于一条线。
     2026-10-16 亮哥：「做成圆形，直径 44」⇒ 宽 88→44、圆角 10→50%。
     2026-10-21 亮哥：「默认状态加上背景色，鼠标放上去时变为红色，像优化提示词按钮一样处理」
     ⇒ 默认淡粉圆底(--brand-tint) + 红箭头；hover 实心红底(--brand) + 白箭头(--hover-fg)，
        跟 .opt-prompt-btn（优化提示词）同款。disabled 仍保留淡粉圆底、只把箭头灰掉，
        免得按钮在「未填提示词/提交中」时圆底突然消失、只剩一个飘着的箭头。 */
  .form-shell > button[type=submit] {
    flex: 0 0 auto;
    align-self: flex-end;      /* 外壳是 flex-end 对齐，按钮跟着控件底边齐 */
    width: 44px;
    height: 44px;
    margin-left: auto;
    margin-top: 0;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--brand-tint);
    color: var(--brand);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: relative;        /* 波纹 ::after 的定位基准 */
    /* transform 换成带回弹的曲线（亮哥 2026-11-02）：hover 放大、按住缩小，松手要"弹"一下 */
    transition: background .15s, transform .18s cubic-bezier(.34, 1.56, .64, 1), color .15s;
  }
  .form-shell > button[type=submit]:hover:not(:disabled) {
    background: var(--brand);
    color: var(--hover-fg);
    filter: none;   /* 通用规则那条 brightness(1.12) 会把红提亮一档；这里跟 .opt-prompt-btn 一样要纯 --brand */
    transform: scale(1.06);   /* 圆形按钮用缩放反馈；原来的 translateX(2px) 会像没对齐 */
  }
  /* 按住缩到 .92，松手弹回 1.06（亮哥 2026-11-02）——
     ⚠️ 必须写在上面 hover 之后：两者权重相同（都是 (0,4,1)），同权重后到者胜。
     「减少动态效果」那条在下面的媒体查询里带了 !important，压得住这里。 */
  .form-shell > button[type=submit]:active:not(:disabled) { transform: scale(.92); }
  /* 提交中 / 满载 / 额度用完 → 箭头变灰（亮哥 2026-10-13「点击提交后箭头变灰直至提示提交成功」）。
     提交中灰着，成功后 updateSubmitState() 把它解禁、颜色自己淡回红色（上面 transition 里的 color）。
     ⚠️ 这里**必须**带 `.form-shell >` 前缀。文件前面那条通用 `button[type=submit]:disabled`
     （--disabled-bg 灰底 + --muted 灰字）跟这条 base 规则权重**完全相同**都是 (0,2,1)，
     同权重后到者胜 —— base 在 935 行、通用规则在 428 行，所以不加前缀的话通用灰字会被
     base 的 `color: var(--brand)` 整个盖掉，箭头在提交中**一直是红的**（2026-10-13 实测复现：
     disabled=true 但 color 仍为 rgb(255,39,66)）。前缀加到 (0,3,1) 才压得住。 */
  .form-shell > button[type=submit]:disabled {
    background: var(--brand-tint);   /* 图标版不要通用规则那块灰底，淡粉圆底留着，箭头灰掉 */
    color: var(--muted);
    box-shadow: none;
    cursor: default;
  }
  /* 提交中：转圈的箭头（亮哥 2026-10-14，替换掉上一版「提交中变灰」——他说灰着看不出在干活）。
     两个 SVG 都常驻按钮里：默认显箭头藏转圈；.is-busy 反过来。
     ⚠️ 权重必须压过上面那条 :disabled 的灰字规则（(0,3,1)），这里带 .is-busy 到 (0,4,1)。 */
  .form-shell > button[type=submit] .ico-spin { display: none; }
  .form-shell > button[type=submit].is-busy { color: var(--brand); cursor: wait; }
  .form-shell > button[type=submit].is-busy:disabled { color: var(--brand); background: var(--brand-tint); }
  .form-shell > button[type=submit].is-busy .ico-arrow { display: none; }
  .form-shell > button[type=submit].is-busy .ico-spin {
    display: block;
    animation: icoSpin 0.9s linear infinite;
  }
  @keyframes icoSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }

  /* ---------- 提交成功的 toast ----------
     之前成功后页面上几乎什么都没有：任务卡进了右侧抽屉、抽屉默认还关着，
     桌面上唯一的变化是右上角多了个小数字。这里补一个会自己退场的提示。
     分工：错误依旧走表单上方的 .status（要停留、要读），成功用 toast（要显眼、会自己走）。 */
  .toast {
    position: fixed;
    top: 20px;
    left: 50%;
    z-index: 50;                 /* 压过抽屉（41），抽屉开着也能看到 */
    display: flex;
    align-items: flex-start;
    gap: 11px;
    max-width: min(430px, calc(100vw - 32px));
    padding: 13px 20px 13px 15px;
    background: var(--panel-solid);
    border: 1px solid var(--ok-border);
    border-radius: 14px;
    box-shadow: var(--shadow-card);
    opacity: 0;
    transform: translate(-50%, -16px) scale(0.96);
    transition: opacity .26s ease, transform .3s cubic-bezier(.2, .9, .3, 1.18);
    pointer-events: none;
    cursor: default;
  }
  .toast.show {
    opacity: 1;
    transform: translate(-50%, 0) scale(1);
    pointer-events: auto;
  }
  /* ⚠️ 这条不能省：上面作者样式里的 display:flex 会盖掉 [hidden] 自带的 UA 样式（display:none）。
     少了它，toast 在没弹出的状态下仍是 display:flex —— 视觉上靠 opacity:0 看不见，
     但读屏软件会把 opacity:0 的元素当可见内容读出来。（同一个坑 #form[hidden] 也遇到过） */
  .toast[hidden] { display: none; }
  /* ✓ 图标单独再弹一下，比整块滑入更有"成了"的感觉 */
  .toast-icon {
    flex: none;
    width: 26px;
    height: 26px;
    display: grid;
    place-items: center;
    border-radius: 50%;
    background: var(--ok-tint);
    border: 1px solid var(--ok-border);
    color: var(--ok);
    font-size: 14px;
    font-weight: 800;
    line-height: 1;
  }
  .toast.show .toast-icon { animation: toastPop .42s cubic-bezier(.2, .9, .3, 1.5) .06s both; }
  /* 提醒语气（亮哥 2026-10-13「需先安装客户端」）：只换颜色，形状/动效一律不动。
     绿 ✓ 是「成了」，拿它去说「你还缺个客户端」会让人误以为成功。 */
  .toast.warn { border-color: var(--warn-border); }
  .toast.warn .toast-icon {
    background: var(--warn-tint-soft);
    border-color: var(--warn-border);
    color: var(--warn);
  }
  /* 常驻提示（warn）：**不自动退场**，必须用户亲手关（亮哥 2026-10-13「需要手动关闭」）。
     气泡本身也能点着关（app.js 里一直绑着 click → hideToast），所以给个手型提示。
     ⚠️ 这条必须写在 .toast.show 之后：两者同为 (0,2,0)，同权重后到者胜。 */
  .toast.warn { cursor: pointer; }
  /* 手动关闭按钮：只在常驻提示上出现 */
  .toast-x {
    position: relative;         /* ::after 扩张命中区的定位父级 */
    flex: none;
    width: 22px;
    height: 22px;
    margin: -2px -7px 0 0;      /* 往右上角贴一点，视觉上更像"关闭" */
    display: grid;
    place-items: center;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--muted);
    font: inherit;
    font-size: 17px;
    line-height: 1;
    cursor: pointer;
  }
  .toast-x:hover { color: var(--brand); background: var(--brand-tint); }
  /* 命中区扩张（2026-10-14）：视觉上还是 22px 的小圆，但用伪元素往外撑到 ~44px，
     手指点得中。移动端可点区域的标准就是 44×44，而"关闭"恰恰是最需要点准的操作之一。
     伪元素是透明的、只吃指针事件，不影响布局。 */
  .toast-x::after {
    content: "";
    position: absolute;
    inset: -11px;
  }
  /* ⚠️ 老坑：作者样式里的 display 会盖掉 [hidden]，必须显式压一道 */
  .toast-x[hidden] { display: none; }
  @keyframes toastPop {
    from { transform: scale(0.3); }
    to   { transform: scale(1); }
  }
  .toast-title { display: block; font-size: 14.5px; font-weight: 800; color: var(--text); line-height: 1.35; }
  .toast-note  { display: block; margin-top: 2px; font-size: 12.5px; color: var(--muted); line-height: 1.45; }
  @media (prefers-reduced-motion: reduce) {
    .toast { transition: none; }
    .toast.show .toast-icon { animation: none; }
  }

  /* ---------- 任务列表抽屉 ---------- */
  .drawer-mask {
    position: fixed;
    inset: 0;
    z-index: 40;
    background: rgba(40, 30, 70, 0.42);
  }
  .drawer {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    z-index: 41;
    width: min(560px, 92vw);
    display: flex;
    flex-direction: column;
    padding: 26px 28px 22px;
    background: var(--panel-solid);
    border-left: 1px solid var(--line-strong);
    box-shadow: -24px 0 70px rgba(58, 47, 110, 0.16);
    transform: translateX(102%);
    transition: transform .28s cubic-bezier(.22, .61, .36, 1);
  }
  body.drawer-open .drawer { transform: translateX(0); }
  .drawer-head {
    flex: none;
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: var(--head-h);
    margin-bottom: 10px;
  }
  .drawer-head > .col-title { margin: 0; font-weight: 800; color: var(--title-1); }
  /* 关闭按钮跟右上角「任务列表」同一套 ghost pill 风格，hover 填品牌色，两个按钮观感统一 */
  .drawer-x {
    flex: none;
    margin-left: auto;
    min-width: 34px;
    min-height: 34px;
    padding: 0 14px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font: inherit;
    font-size: 18px;
    line-height: 1;
    color: var(--accent-text);
    background: var(--btn-ghost-bg);
    border: 1px solid var(--line-strong);
    border-radius: 999px;
    cursor: pointer;
    transition: color .16s, background .16s, border-color .16s;
  }
  .drawer-x:hover { color: #ffffff; background: var(--brand); border-color: var(--brand); }
  .drawer-actions {
    flex: none;
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 14px;
    /* 窄屏（手机 92vw 的抽屉）四个按钮一行放不下 —— 允许换行，别让它们挤成竖条或溢出。
       桌面宽度下够放，视觉上不会变。2026-10-25 加「生成历史」时一并加上的。 */
    flex-wrap: wrap;
  }
  .drawer-actions .save-btn.minor { margin-left: 0; }
  .drawer-actions .save-btn:not(.minor) { margin-left: auto; }

  /* 这几个元素的样式里自己写了 display（flex / inline-flex），会盖掉 [hidden] 的 UA 规则
     （display:none）—— 特别是「下载客户端」和任务数角标，它们一开始就是 hidden，
     要等接口回来才决定显不显示。统一补回来，免得加载那一下露出空壳。 */
  .save-btn[hidden],
  .entry-badge[hidden],
  .opt-body[hidden] { display: none; }
  .drawer .result {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--scroll-thumb) transparent;
  }
  .drawer .result::-webkit-scrollbar { width: 6px; }
  .drawer .result::-webkit-scrollbar-track { background: transparent; }
  .drawer .result::-webkit-scrollbar-thumb { background: var(--scroll-thumb); border-radius: 3px; }
  .drawer .result::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hover); }
  /* 抽屉底部的常驻提示（亮哥 2026-10-13）：flex 列里不加 flex，天然钉在 .result 正下方 ——
     列表怎么滚它都看得见，「及时保存」这种话就得常驻才有用。 */
  .drawer-note {
    flex: none;
    margin: 10px 0 0;
    padding-top: 10px;
    border-top: 1px dashed var(--line);
    color: var(--muted);
    font-size: 13px;
    line-height: 1.6;
    text-align: center;
  }

  /* 窄屏：输入区外壳收窄、内边距和间距减小，标题和导航栏里的图标组收一档，别挤 */
  @media (max-width: 720px) {
    /* 主体顶部让位改由 body 的 padding-top 负责（导航栏是 fixed 的），卡片自己不用再留 */
    .card { padding: 12px 18px 0; }
    body { padding-top: 76px; }   /* 公告去掉后导航栏不再折行，回到一行高度（52）+ 余量 */
    .topnav {
      flex-wrap: wrap;             /* 留着兜底：万一窄到 logo + 图标组一行放不下，图标组自己折下去 */
      height: auto;
      min-height: 48px;
      padding: 8px 16px;
      row-gap: 2px;
    }
    .topnav-brand { height: 26px; }
    .topbar h1 { font-size: 36px; }
    .entry-icons { order: 2; gap: 5px; }
    .entry-ico { width: 30px; height: 30px; padding: 0; border-radius: 9px; }
    .entry-ico__text { display: none; }   /* 窄屏只留图标（文案仍在 title/aria-label） */
    .entry-ico svg { width: 16px; height: 16px; }
    .entry-badge { top: -3px; right: -3px; min-width: 15px; height: 15px; font-size: 10px; }
    /* 手机上公告占导航栏第二行整行：文案长短不受控，own row 才不会跟按钮/标题打架。
       ⚠️ transform 对 static 元素依然生效（.foot 踩过的坑），必须显式清掉 translate(-50%,-50%)。 */
    .notice {
      position: static;
      transform: none;
      order: 3;
      flex: 1 1 100%;
      max-width: 100%;
      white-space: normal;
      overflow: visible;
      padding: 0;
      margin: 0;
    }
    .form-shell { padding: 4px var(--shell-pad-x) 4px; column-gap: 10px; row-gap: 12px; }
    .field-primary textarea { min-height: 170px; }

    /* ⚠️ 关键：桌面那排选项里，尺寸/数量用的是**写死的固定宽**（112 / 88；参考图已改成贴内容收）。
       手机上再加 gap 就挤不下，会横向溢出。这里全部改成自适应：
       尺寸和数量并排各占半行，参考图和目录各占整行。 */
    #form .opt[data-field="图片尺寸"],
    #form .opt[data-field="生成数量"]       { flex: 1 1 calc(50% - 5px); }
    #form .opt[data-field="参考图"],
    #form .opt[data-field="下载到本地目录"] { flex: 1 1 100%; }
    /* 目录输入框桌面端是按 placeholder 量出来的定宽（JS 写的行内样式），
       手机上这格占整行，框也该占满 —— !important 压掉行内宽度 */
    #form .opt[data-field="下载到本地目录"] input[type=text] { width: 100% !important; }
    /* 提交按钮（箭头图标版）：手机上同样 44 高，跟控件行齐平（桌面 2026-10-14 改的口径同步过来） */
    .form-shell > button[type=submit] { flex: 0 0 auto; width: 44px; height: 44px; margin-left: auto; }

    /* iOS Safari 的经典坑：表单控件字号 < 16px 时，聚焦会**自动放大整个页面**。
       只把控件提到 16px，不动正文，免得整体排版走样。 */
    input[type=text], input[type=number], input[type=tel], input[type=date], input[type=url], textarea, select {
      font-size: 16px;
    }
  }
  /* 手机：图标组再缩一档、标签收紧，标题别被导航栏压住 */
  @media (max-width: 480px) {
    .card { padding: 8px 14px 0; }
    body { padding-top: 72px; }   /* 同 720 档：公告没了，导航栏一行就够 */
    .topnav { padding: 6px 12px; }
    .topnav-brand { height: 24px; }
    .topbar h1 { font-size: 28px; }
    .entry-icons { gap: 4px; }
    .entry-ico { width: 28px; height: 28px; padding: 0; border-radius: 8px; }
    .entry-ico svg { width: 15px; height: 15px; }
    .entry-badge { min-width: 14px; height: 14px; font-size: 9.5px; padding: 0 3px; }
    .notice { margin: 0; }
    .subtitle { font-size: 13.5px; }
    .tagline { padding: 5px 12px; font-size: 12.5px; }
    .form-shell { padding: 10px 12px 12px; }
    .taglines { margin-bottom: 18px; gap: 8px; }
    /* 手机纵向空间最紧张：提示词框收一档，少挤一点 */
    .field-primary textarea { min-height: 132px; }
  }

  /* 矮视口：横屏手机 / 横屏平板 / 被压矮的桌面窗口。
     ⚠️ 上面所有断点都是按**宽度**判断的，所以 1024×600 这类会走"桌面"分支；
     而桌面分支里 .card 是 justify-content:center + overflow:hidden ——
     内容比视口高时，居中会把顶部挤出容器，而且滚不到（flex 居中溢出的经典毛病）。
     这里按**高度**再兜一道，把页面切成可滚动。 */
  @media (max-height: 700px) and (min-width: 881px) {
    body {
      height: auto;
      min-height: 100vh;
      padding: 64px 16px 28px;   /* 顶部给固定导航栏让位（矮视口收一档：52 + 余量） */
    }
    .card { max-height: none; overflow: visible; justify-content: flex-start; }
    .foot { position: static; left: auto; bottom: auto; transform: none; margin-top: 18px; }
    .topbar h1 { font-size: 40px; }
    .form-shell .field-primary textarea { min-height: 96px; }
  }

  /* ================================================================
     ≤880 回收桌面版式（亮哥 2026-10-13）：4/6 屏宽 + 锁高 1/3 + 定宽目录框
     在窄屏上必然挤爆，全部恢复流式 —— 框占满行宽、高度由内容决定。
     ⚠️ 这块**必须放在文件末尾**：.form-shell / .quota-hint 的基础规则在单栏改版段
     （文件后段），媒体查询写在前面会被同权重的后到规则盖掉 ——
     实测手机端壳变成 66.67vw 定宽定高、字段溢出到框外压住额度文字，就是这么来的。
     ================================================================ */
  @media (max-width: 880px) {
    .form-shell { width: 100%; height: auto; min-height: 0; margin: 0; }
    .form-shell .field-primary textarea { height: auto; }
    .quota-hint { width: 100%; margin-left: 0; margin-right: 0; }
    .status { width: 100%; margin-left: 0; margin-right: 0; }
  }

  /* 提示词库嵌入区：整页 prompts.html 以 iframe 放在绘图提示词输入框下方。
     亮哥 2026-10-15：**不要外面的框**，要它看起来就是主页面的一部分 ——
     直接平铺在输入框下面、跟着整页一起往下滚（不是一块独立滚动的小窗口）。
     做法：① iframe 归零边框/圆角/背景/阴影，跟主页面无缝；
          ② iframe 高度 = 里面内容高度（app.js 的 syncEmbedHeight 量完写内联 px），
             所以**不出现** iframe 自己的滚动条，滚动只有主页面那一条。
     ⚠️ 为什么还用 iframe、不内联：prompts.html 与 app.css 有 6 个同名选择器
        （.card / .foot / body / h1 / :root / *），内联必串样式。iframe 是样式隔离，
        只是把它的"边框"藏掉，视觉上等同平铺。
     ⚠️ iframe **无法**用 CSS 自适应到子文档内容高度，高度只能 JS 量。
        下面的 min-height 只是 JS 没跑/失败时的兜底，别删。 */
  .prompts-embed { position: relative; flex: 0 0 auto; margin-top: 22px; min-height: 480px; }
  .prompts-embed iframe {
    position: absolute; inset: 0;
    /* ⚠️ 必须显式写 height:100%：<iframe> 是**替换元素**，只给 inset:0 而 height:auto
       时，它按内在尺寸（150px）走，**不会**被 top/bottom 撑开 —— 实测就是这里
       卡在 150px、iframe 自己冒出滚动条。width:100% 同理要写全。 */
    width: 100%; height: 100%;
    border: 0; border-radius: 0;
    display: block; background: transparent;
    color-scheme: light;
  }

  /* ============ 回到顶部（亮哥 2026-10-16）============ */
  /* 右下角悬浮圆钮。
     ⚠️ z-index 20 是故意垫低的：抽屉 41 / toast 50 / 弹窗 60 都在它上面。
        抽屉是从右侧滑出来的，抽屉一开按钮就被它自然盖住，不会跟抽屉抢右侧同一块位置；
        toast 与弹窗更该压住它（那时候用户的注意力不在滚动上）。
     ⚠️ 元素本身必须挂在 body 直接子级（在 .card/.drawer 外面）—— 见 index.html 里的注释。 */
  .to-top {
    position: fixed;
    right: 24px; bottom: 24px;
    z-index: 20;
    width: 44px; height: 44px;      /* 跟并排控件统一 44 高（10-15 定的版式约定） */
    display: flex; align-items: center; justify-content: center;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--brand);
    color: var(--btn-fg);
    box-shadow: 0 6px 18px rgba(233, 85, 129, .32);
    cursor: pointer;
    /* 默认藏着：没滚过阈值时不该占位，更不该吃掉右下角的点击。
       用 opacity/visibility 而非 display:none，是为了能淡入 —— 但 visibility:hidden
       顺带保证了它在藏起来时也点不到、也不进 tab 焦点顺序。 */
    opacity: 0;
    visibility: hidden;
    transform: translateY(10px);
    transition: opacity .18s ease, transform .18s ease, visibility .18s;
  }
  .to-top.is-show {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
  }
  /* hover/active 的 transform 写在 .is-show 之后：两者特异性相同（都是 2 个选择器），
     靠后来者居上覆盖 —— 藏起来时 hover 到也看不出问题（本来就是 visibility:hidden）。 */
  .to-top:hover { transform: translateY(-2px); }
  .to-top:active { transform: translateY(0); }
  .to-top:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; }
  /* 小屏：边距收窄、尺寸缩一档（44 → 40），别在窄屏上占掉太多右下角 */
  @media (max-width: 720px) {
    .to-top { right: 14px; bottom: 14px; width: 40px; height: 40px; }
    .to-top svg { width: 18px; height: 18px; }
  }

  /* ============ 账号体系：登录入口 / 弹窗 / 云端记录 ============ */
  /* 登录入口：未登录是普通图标；登录后转品牌色，提示已登录 */
  #btnLogin.is-user { color: var(--brand); }
  #btnLogin.is-user svg { fill: var(--brand-tint); }

  /* 弹窗遮罩 + 卡片 */
  .modal-mask {
    position: fixed; inset: 0; z-index: 60;
    background: rgba(40, 30, 70, 0.52);
    display: flex; align-items: center; justify-content: center;
    padding: 20px;
  }
  .modal-mask[hidden] { display: none; }
  .modal {
    width: 360px; max-width: 100%;
    background: var(--panel-solid); color: var(--text);
    border: 1px solid var(--line); border-radius: 16px;
    box-shadow: 0 18px 50px rgba(58, 47, 110, .22);
    padding: 22px 22px 24px; position: relative;
  }
  .modal-title { font-size: 17px; font-weight: 700; margin: 0 0 14px; }
  .modal-x {
    position: absolute; top: 10px; right: 12px;
    width: 30px; height: 30px; border: 0; background: transparent;
    color: var(--muted); font-size: 22px; line-height: 1; cursor: pointer; border-radius: 8px;
  }
  .modal-x:hover { background: var(--brand-tint); color: var(--brand); }

  /* ---------- 生成历史（读服务端 tasks 表，2026-10-25）----------
     跟抽屉里的任务列表分工不同：抽屉是「本次会话在跑/刚跑完」，
     这里是我这个账号生成过的全部记录 ⇒ 需要更宽，且列表要能滚。
     宽度取 780px：一张 84px 缩略图 + 两行提示词 + 一行元信息刚好不挤；
     max-height 卡 84vh，超出就只在 .hist-body 里滚，标题和工具条始终钉住。 */
  .modal--wide {
    width: 780px; max-width: 100%; max-height: 84vh;
    display: flex; flex-direction: column;
  }
  .hist-bar {
    flex: 0 0 auto; display: flex; align-items: center; gap: 10px;
    font-size: 12.5px; color: var(--muted); margin: -4px 0 10px;
  }
  .hist-spacer { flex: 1; }
  .hist-body {
    flex: 1 1 auto; min-height: 140px; overflow-y: auto;
    display: flex; flex-direction: column; gap: 10px; padding-right: 4px;
  }
  .hist-item {
    display: flex; gap: 12px; padding: 10px 12px;
    border: 1px solid var(--line); border-radius: 12px; background: var(--panel-solid);
  }
  .hist-thumb {
    flex: 0 0 84px; width: 84px; height: 84px; border-radius: 10px;
    object-fit: cover; border: 1px solid var(--line); background: var(--row-bg);
    cursor: zoom-in; display: block;
  }
  .hist-thumb--none {
    display: flex; align-items: center; justify-content: center;
    font-size: 12px; color: var(--muted); cursor: default; text-align: center; line-height: 1.4;
  }
  .hist-main { flex: 1 1 auto; min-width: 0; }
  .hist-meta {
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    font-size: 12px; color: var(--muted);
  }
  .hist-dot { width: 3px; height: 3px; border-radius: 50%; background: var(--line-strong); }
  .hist-prompt {
    margin: 5px 0 6px; font-size: 13.5px; color: var(--text);
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  }
  /* 状态徽章：跟抽屉卡片上的语义对齐（done 绿 / 失败红 / 等待中橙） */
  .hist-status {
    padding: 1px 8px; border-radius: 999px; font-size: 11.5px; font-weight: 600;
    border: 1px solid var(--line); color: var(--muted); white-space: nowrap;
  }
  .hist-status.is-done { color: var(--ok); border-color: var(--ok-border); background: var(--ok-tint); }
  .hist-status.is-fail { color: var(--danger); border-color: var(--danger-border); background: var(--danger-tint); }
  .hist-status.is-wait { color: var(--warn); border-color: var(--warn-border); background: var(--warn-tint-soft); }
  .hist-err {
    margin: 2px 0 0; font-size: 12.5px; color: var(--danger); word-break: break-all;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  }
  .hist-empty {
    flex: 1 1 auto; display: flex; align-items: center; justify-content: center;
    color: var(--muted); font-size: 13.5px; padding: 40px 0; text-align: center;
  }
  @media (max-width: 640px) {
    .hist-thumb { flex-basis: 64px; width: 64px; height: 64px; }
    .modal--wide { max-height: 88vh; }
  }

  /* 登录：标签切换 */
  .login-tabs { display: flex; gap: 8px; margin-bottom: 16px; }
  .login-tab {
    flex: 1; padding: 8px 0; border: 1px solid var(--line); background: var(--panel);
    border-radius: 10px; font-size: 14px; color: var(--muted); cursor: pointer;
  }
  .login-tab.active { color: var(--btn-fg); background: var(--brand); border-color: var(--brand); }

  .login-pane { display: flex; flex-direction: column; gap: 10px; }
  /* 注册专属的那几格（再输入一次密码 / 昵称 / 邀请码）被包在 #paneRegisterExtra 里，
     .login-pane 的 gap 只管直接子元素、管不到它 ⇒ 那几格会紧贴在一起
     （亮哥 2026-10-26：「这 3 个框挨在一起了」）。这里给容器自己一份同样的间距。
     ⚠️ 必须写成 :not([hidden])：直接给 div 设 display:flex 会盖掉 [hidden] 的 display:none，
        切回登录模式时这个块就藏不住了。 */
  .login-pane-extra:not([hidden]) { display: flex; flex-direction: column; gap: 10px; }
  .login-input {
    width: 100%; box-sizing: border-box; padding: 10px 12px; font-size: 14px;
    color: var(--text); background: var(--input-bg);
    border: 1px solid var(--line); border-radius: 10px; outline: none;
  }
  .login-input:focus { border-color: var(--brand); background: var(--focus-bg); }
  /* 2026-10-16 亮哥：「获取验证码」要和左边输入框等高 —— 用 stretch 让按钮撑到行高
     （行高由较高的 .login-input 决定），按钮内部本来就 align-items:center，文字仍居中。 */
  .login-row { display: flex; gap: 8px; align-items: stretch; }
  .login-row .flex1 { flex: 1; }
  .login-err { color: var(--danger); font-size: 12.5px; min-height: 16px; margin: -4px 0 0; }
  /* 没错误时不占位（空元素直接 display:none）——否则它那 16px 预留高会让手机号框和验证码行之间
     白多出一段（亮哥 2026-10-16「怎么离得那么宽」）。用 :empty，setPhoneErr('') 清空后即命中。 */
  .login-err:empty { display: none; }
  .login-tip { font-size: 13px; color: var(--muted); line-height: 1.6; margin: 0 0 4px; }
  .login-note { font-size: 12px; color: var(--muted); margin: 8px 0 0; }
  /* 「或」分隔：手机号 / 微信 两种登录方式同屏（亮哥 2026-10-16 要保留微信登录按钮） */
  .login-or { position: relative; text-align: center; color: var(--muted); font-size: 12.5px; margin: 14px 0 10px; }
  .login-or::before, .login-or::after { content: ''; position: absolute; top: 50%; width: 44%; height: 1px; background: var(--line); }
  .login-or::before { left: 0; }
  .login-or::after { right: 0; }
  .login-agree { display: flex; align-items: flex-start; gap: 7px; font-size: 12px; color: var(--muted); line-height: 1.5; margin: 12px 0 2px; cursor: pointer; user-select: none; }
  .login-agree input[type="checkbox"] { margin: 2px 0 0; width: 14px; height: 14px; flex: none; accent-color: var(--brand); cursor: pointer; }
  .login-agree a { color: var(--brand); text-decoration: none; }
  .login-agree a:hover { text-decoration: underline; }
  .login-agree-err { font-size: 12px; color: var(--danger, #c33b3b); margin: 4px 0 0; line-height: 1.5; }
  .save-btn.block { width: 100%; justify-content: center; margin-top: 4px; }
  /* 登录弹窗里的整排按钮都与输入框同高 44px（亮哥 2026-10-16）：
     「登录 / 注册」在 .login-pane 内，「微信登录」在 pane 外，两者都要覆盖。 */
  .login-pane .save-btn.block,
  .save-btn.block.wechat { height: 44px; }
  .save-btn.wechat { background: #07c160; border-color: #07c160; color: #ffffff; }
  .save-btn.wechat:hover:not(:disabled) { background: #06ad56; border-color: #06ad56; }

  /* 已登录账户视图 */
  .acct { display: flex; gap: 12px; align-items: center; margin-bottom: 14px; }
  .acct-avatar {
    width: 44px; height: 44px; border-radius: 50%; flex: 0 0 auto;
    background: var(--brand); color: #ffffff; font-size: 20px; font-weight: 700;
    display: flex; align-items: center; justify-content: center;
  }
  .acct-name { font-size: 15px; font-weight: 700; }
  .acct-sub { font-size: 12.5px; color: var(--muted); margin-top: 2px; }
  .acct-stat { font-size: 13px; color: var(--muted); margin-bottom: 8px; }
  /* 积分体系 UI（2026-10-16 加）：余额、卡密兑换、顶栏积分 chip、模型成本、提交前提示 */
  .acct-credits { font-size: 14px; font-weight: 700; color: var(--brand); margin-bottom: 12px; }
  .acct-redeem { display: flex; gap: 8px; margin-bottom: 4px; }
  .acct-redeem .login-input { flex: 1; }
  .acct-redeem-msg { font-size: 12.5px; color: var(--muted); min-height: 16px; margin: 0 0 12px; }
  .acct-redeem-msg:empty { margin: 0; }
  /* 账户弹窗：卡密获取说明 + 积分明细列表（2026-10-16 加） */
  .acct-redeem-note { font-size: 12px; color: var(--muted); line-height: 1.6; margin: 0 0 12px; }
  .acct-logs { margin-bottom: 12px; }
  .acct-logs-h { font-size: 12.5px; font-weight: 700; color: var(--label); margin-bottom: 6px; }
  .acct-logs-list {
    max-height: 156px; overflow-y: auto; -webkit-overflow-scrolling: touch;
    border: 1px solid var(--line); border-radius: 10px; padding: 4px 8px; background: var(--row-bg);
  }
  .acct-log-row { display: flex; align-items: center; gap: 8px; padding: 5px 4px; font-size: 12.5px; }
  .acct-log-row + .acct-log-row { border-top: 1px solid var(--line); }
  .acct-log-reason { flex: 1 1 auto; color: var(--text); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .acct-log-time { flex: 0 0 auto; color: var(--muted); font-size: 11.5px; font-variant-numeric: tabular-nums; }
  .acct-log-delta { flex: 0 0 auto; font-weight: 700; font-variant-numeric: tabular-nums; }
  .acct-log-delta.plus { color: var(--ok); }
  .acct-log-delta.minus { color: var(--brand); }
  .acct-logs-empty, .acct-logs-loading { display: block; padding: 8px 4px; color: var(--muted); font-size: 12px; }

  /* 积分提示：贴在提交按钮**左边**、同一行（亮哥 2026-10-16）。
     #form/.form-shell 是 flex 行，margin-left:auto 把它和提交按钮一起推到行尾；
     按钮原本那条 margin-left:auto 用更高权重的相邻选择器压成 6px 间距，两者才紧挨着。
     ⚠️ :not([hidden]) 必须带 —— 提示隐藏（定价没拉到）时按钮得自己吃回
        margin-left:auto 继续贴右缘，否则箭头会缩到芯片后面不贴边。
     对齐（亮哥 10-16「与提交按钮水平对齐」）：镜像按钮的盒子 —— 同为 44 高、
     align-self:flex-end 底对齐、line-height 44 让文字在盒内垂直居中 ⇒ 与箭头同一水平线。 */
  #form .credit-line {
    flex: 0 0 auto;
    align-self: flex-end;
    height: 44px;
    display: inline-flex;
    align-items: center;
    gap: 5px;
    margin: 0 0 0 auto;
    font-size: 13px;
    color: var(--text);
    white-space: nowrap;
  }
  /* 「✨ 2/张 共 4」样式（亮哥 2026-10-17 截图）：火花图标 + 深色单价 + 蓝色合计 */
  #form .credit-line .credit-spark { width: 15px; height: 15px; flex: none; }
  #form .credit-line .credit-unit { color: var(--text); font-weight: 600; font-size: 14px; }
  #form .credit-line .credit-unit i {
    font-style: normal;
    font-weight: 400;
    font-size: 12.5px;
    color: var(--muted);
    margin-left: 1px;
  }
  #form .credit-line .credit-total { color: #7c5cff; font-size: 13px; margin-left: 2px; }
  #form .credit-line .credit-total b { font-weight: 600; font-size: 14px; }
  #form .credit-line[hidden] { display: none; }
  #form .credit-line:not([hidden]) + button[type=submit] { margin-left: 6px; }
