:root {
  /* ---------- 纸张 ----------
     底色是纸，不是「深色模式」。原来那套 #0b0e13 + 金色，是 2024~2025 年
     AI 产品的通用审美：深底让发光的东西好看，而发光是「智能」的视觉隐喻。
     这个站不发光，它量东西。所以换成纸与墨，强调色用朱红 ——
     印刷品里朱红是批注与强调的颜色，压在白纸上也够深。

     【2026-10-31 用户第二次说「能不能做成纯白色」——按字面做了】
     10-29 那次我读成「纸性没被看出来」，于是把暖调推得更重（黄比蓝 6→20）。
     用户第二次提，说明他要的就是 #ffffff，不是「更白的纸」。这次不再解释，
     直接把四个底色变量压成纯白。

     代价必须写清：底色之间的明度差归零，卡片/面板不再靠「浅一档」浮起来。
     层次改由两样东西承担 —— ① 1px 的 --line / --line-strong 描边；
     ② 极淡的朱红给需要呼吸的区域上色（panel2 = 4% 朱红，白底上 1.06:1，
     肉眼是「这个块有颜色」，不是「这个块是灰的」）。 */
  --bg: #ffffff;
  --bg2: #ffffff;      /* 语义仍是「下沉一层」，但纯白下只能用描边表达 */
  --panel: #ffffff;    /* 卡片 */
  --panel2: #faf5f4;   /* 控件填充：白 + 4% 朱红，唯一还留着色相的底 */
  /* 装饰分隔线：卡片、表格、目录行。只负责分区，不承担边界职责。
     纯白底下 #ded3bd 只剩 1.48:1，线几乎看不见，所以加深一档到 1.96:1
     （压 --panel2 的 1.80:1）。它仍然远低于 3:1 —— 这是有意的：
     1.4.11 只管**控件边界**，这条线管的是分区，画成 3:1 就成了硬格子。 */
  --line: #c2b69e;
  /* 交互控件的边框。1.4.11 要求 ≥3:1，且要同时压住控件填充与页面底色：
     实测压 --panel2 4.05:1、压 --bg 4.62:1，取更差的那一侧仍过线。
     纯白化后 --panel2 从 #f0e8d8 提到 #faf5f4（更亮），所以这里必须跟着加深，
     不然原来 3.40:1 的那一侧会掉到 3.0 以下。 */
  --line-strong: #7c7466;
  --text: #191713;
  --text2: #46403a;
  /* 小字（11~13px 的标签、图注、表头）。压 --panel2 4.69:1，留 0.19 余量；
     它只用作 color，全文件无 background/border 引用，改它不影响形状。 */
  --dim: #6d665c;
  --accent: #a33d2e;
  --accent2: #8c3226;
  --good: #2c6242;
  --warn: #7d5310;
  --bad: #9c3527;
  /* 圆角归零。10px 圆角是「卡片」的语言；印刷品用直角与细线分区，
     圆角一出现，整页立刻退回成 SaaS 后台。 */
  --radius: 0;
  --max: 760px;
  /* 衬线只给标题。系统字体栈，不引外链 —— 这个站宣称「照片不离开设备」，
     一个会去 Google Fonts 取字的页面，那句话就不成立了。 */
  --serif: "Songti SC", "SimSun", "Noto Serif CJK SC", "Source Han Serif SC",
    "Hiragino Mincho ProN", Georgia, "Times New Roman", serif;
  color-scheme: light;
  scroll-padding-top: calc(var(--topbar-h, 0px) + 12px);
}

* { box-sizing: border-box; }

/* ---------- [hidden] 兜底 ----------
 * 浏览器默认样式里的 `[hidden] { display: none }` 会被**任何**作者样式里的
 * display 盖掉：只要某个类写了 `display: flex/grid/inline-flex`，这个元素
 * 就算带着 hidden 属性也照样显示。
 *
 * 本项目已经踩过三次（`.sk-row`、`#stage-result`、`.studio-preview` 那个
 * 空的黑缩略图框）。每次都是「界面上多出一块空盒子」，而 HTML 里明明写着 hidden。
 * 与其在第四个地方再补一条 `[hidden]` 规则，不如全局兜住 —— 这条 `!important`
 * 是**有意**的：`hidden` 的语义就是「不显示」，没有合法理由让它显示。 */
[hidden] {
  display: none !important;
}

/* 全局焦点环。
   站里原来只给 .bg-chip / .chip-btn / .color-chip / .lang-select / .tool-select /
   .auth-tab / .field input 写了 :focus-visible，**导航链接和普通按钮全靠浏览器默认环** ——
   而默认环是 1px 近黑（rgb(16,16,16)），压在 #0b0e13 上 1.02:1。键盘用户按 Tab
   走一圈，屏幕上什么都不会变。
   特异性 (0,1,0) 低于上面那些 (0,2,0) 的规则，所以已有的定制环照旧生效。 */
:focus-visible {
  outline: 2px solid var(--accent2);
  outline-offset: 2px;
}

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: system-ui, -apple-system, "PingFang SC", "Hiragino Sans GB",
    "Microsoft YaHei", sans-serif;
  font-size: 16px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

/* 页脚贴底（sticky footer）。
 *
 * 原来 body 是普通块流：内容比视口矮时页脚跟着内容往上跑，下面留一大片
 * 背景色 —— 工具页在 1280×900 下就是这个样子，看起来像"页面没加载完"。
 * 改成 flex 列 + main 吃掉剩余空间，页脚就永远在视口底部。
 *
 * 注意 `min-height: 100vh` 而不是 `height`：内容超过一屏时 body 要能长高，
 * 写死 height 会让超出的部分被裁掉（或者溢出到 body 外面）。
 * main 上加 `width: 100%` 是必须的 —— flex item 的 `margin: 0 auto`
 * 只在有确定宽度时才居中，否则它会按内容宽度收缩，左右留白不对称。
 */
body {
  /* 【2026-10-31 修：这层径向渐变从写下来那天起就没画出来过】
     原文是 `radial-gradient(900px 500px at 50% -10%, …)`。
     定位点在**上方 10% 高度（-50px）之外**，尺寸又只有 500px，
     Chrome 在这种「圆心在盒外 + 固定尺寸椭圆」的组合下把整层画成透明：
     computed style 与 CSS.supports 都显示它有效，屏幕上却是 pure white。

     怎么确认的（06/07 号实验，都写进 README 了）：
       同一个会话里 setProperty 换背景 —— 圆心移到 `at 50% 8%` 立刻出现
       rgb(249,242,241)，改回 `-10%` 立刻回到 255,255,255；
       把颜色换成纯红也一样看不见。所以不是「太淡」，是「没画」。

     现在改成 `at 50% 0%`：中心正好落在顶边上，往下完整铺开。
     实测 4 页（index / login / compare / shapes）中轴 y=90 全部 rgb(249,242,241)，
     往下 300px 内衰减到 0 —— 是「页头有一点暖」，不是「页面有颜色」。
     文字对比度按实测峰值背景 #f9f2f1 重算：text 16.19 / text2 9.25 / dim 5.13 /
     accent 5.82 / line-strong 4.18，全部过 AA（余量最小的是 dim，0.63）。 */
  background:
    radial-gradient(900px 500px at 50% 0%, rgba(163, 61, 46, 0.09), transparent 65%),
    var(--bg);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* ---------- 顶栏 ---------- */
.topbar {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 20px;
  background: rgba(11, 14, 19, 0.82);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line);
}

.brand {
  color: inherit;
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 9px;
  font-weight: 600;
  letter-spacing: 0.02em;
}

.logo {
  color: var(--accent);
  font-size: 20px;
}

/* 顶栏的"照片不上传"徽章。
   原来只有 1px `--line` 边框 —— 而 --line 是**装饰分隔线**的色（1.15:1），
   用在控件边框上等于没有边界（1.4.11 要求 3:1）。它虽然不可点，但它是
   一条常驻的信息胶囊，边界消失后读起来像一段游离的文字。
   现在用 --line-strong 画边、内高光给厚度，并把锁图标单独染成金色。 */
.privacy-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--text2);
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  padding: 4px 12px 4px 10px;
  background: var(--panel);
  /* 纯白化后删掉内高光：白底上 rgba(255,255,255,.0x) 等于不存在 */
  white-space: nowrap;
}

.badge-lock {
  width: 13px;
  height: 13px;
  flex: 0 0 auto;
  color: var(--accent);
}

/* ---------- 舞台 ---------- */
main {
  max-width: var(--max);
  /* flex item 的 `margin: 0 auto` 要有确定宽度才居中，否则按内容宽度收缩 */
  width: 100%;
  margin: 0 auto;
  padding: 0 18px 80px;
  /* main 吃掉 topbar 与 footer 之间的剩余空间（sticky footer 的另一半） */
  flex: 1 0 auto;
  /* main 自己也做成 flex 列，好让工具页的上传段用 `flex: 1` 精确撑满。
     这**不会改变任何间距** —— 已实测：main 的直接子元素（三个 stage section）
     margin 全是 0，且同时只有一个可见，flex 不折叠 margin 这件事在这里无影响。 */
  display: flex;
  flex-direction: column;
}

.stage[hidden] { display: none; }

/* ---------- 上传 ---------- */
.hero {
  padding: 46px 0 26px;
  /* **左对齐，不居中。** 居中大标题是营销页的语法（「看我们多厉害」），
     左对齐加一条横线是报告的语法（「这是本次分析的输入」）。
     这个站的内容本来就是一页报告，居中的排法一直在削弱它。
     工具页的 `.hero-tool` 同步左对齐，两处视觉语言才一致。 */
  text-align: left;
}

.hero h1 {
  font-size: clamp(26px, 6vw, 34px);
  line-height: 1.22;
  margin: 0 0 12px;
  /* 负字距是**拉丁文大标题**的做法（大写字形本身带侧空，收紧更好看）。
     中文每个字都是满格方块，负字距会让笔画贴到一起 —— 36px 下"人脸数据"
     四个字的横画几乎连成一条。所以按语言分开：拉丁文收紧，汉字放宽。
     `:lang()` 读的是 <html lang>，i18n 运行时切换语言时会同步改它。 */
  letter-spacing: -0.015em;
}

:lang(zh) .hero h1,
:lang(ja) .hero h1,
:lang(ko) .hero h1 {
  letter-spacing: 0.012em;
}

.hero-sub {
  color: var(--text2);
  font-size: 15px;
  max-width: 560px;
  margin: 0;
}

/* ---------- 「三庭五眼」示意图 ---------- */
/* 默认不显示。只在 ≥1081px 的双栏 hero 里出现 ——
   手机首屏已经被三行顶栏占掉 209px，再塞一张图，上传框会掉到第二屏。 */
.hero-figure {
  margin: 0;
  display: none;
}

.hero-figure svg {
  display: block;
  width: 100%;
  height: auto;
}

/* 脸轮廓：中性灰，它是「底」，不该比测量线更显眼 */
.fg-outline {
  fill: none;
  stroke: var(--line-strong);
  stroke-width: 1.8;
}

/* 测量线用金色，但压到 45% —— 它们是辅助线。
   满不透明度的金色会把视线从标题抢走，而这张图是解释标题的，不是主角。 */
.fg-grid line {
  stroke: var(--accent);
  stroke-width: 1;
  /* 0.45 实测在 #0b0e13 上几乎看不见（截图里线是断的）。
     提到 0.62：能读清，又不会把视线从标题抢走。 */
  opacity: 0.62;
}

/* 五官示意比轮廓还淡：它们只负责让人认出「这是一张脸」。 */
.fg-feat ellipse,
.fg-feat path {
  fill: none;
  stroke: var(--line-strong);
  stroke-width: 1.5;
  opacity: 0.75;
}

/* 瞳孔：唯一有填充的图形。没有它，杏仁形仍可能被读成「两片叶子」。 */
.fg-feat circle {
  fill: var(--line-strong);
  stroke: none;
  opacity: 0.6;
}

/* 比例数字用 tabular-nums：三个 1/3 竖着排要对齐，比例字体下斜杠会错位。 */
.fg-label text {
  fill: var(--dim);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}

.fg-label--top text {
  /* 10px 在 320px 宽的图里只有 6px 实际高度，截图里糊成一团。 */
  font-size: 11px;
}

/* 副标题第二句（金色那句）。**必须独占一行，不能内联。**
   内联时 `<span>` 与 `<strong>` 之间的换行会被渲染成一个空格，
   中文里就成了「…气色。 每一项」—— 句中多出一个空格，而拉丁文那边这个空格是必需的
   （`tone. Every`）。同一份标记要同时服务两种排版，唯一不靠语言判断的解法是
   让它成块：块首的空格落在上一行行尾，行尾空白自然折叠，两种语言都干净。
   顺带把「这意味着什么」从句子中段提到独立一行 —— 它本来就是这句副标题的重心。 */
.hero-sub strong {
  display: block;
  margin-top: 4px;
  color: var(--accent2);
  font-weight: 600;
}

/* 账号页是**单一任务**（登录 / 注册），标题跟着表单一起居中。
   正文页改成左对齐之后，这一页如果也左对齐，标题贴左边、表单在中间，
   两条对齐线互相打架 —— 读起来像"排错了"。任务页居中、报告页左对齐，
   是两种各自成立的语法，不是不一致。 */
.page-account .hero { text-align: center; }
.page-account .hero-sub { margin: 0 auto; }

/* 出处行：只写**真实在用的模型与标准**，不写「AI 驱动」这类空话。
 *
 * 为什么这一行能提升可信度：它给出的是可查证的具体名称 ——
 * MediaPipe FaceLandmarker（478 个关键点）、ITA°（皮肤科衡量肤色的通用指标）、
 * CIELAB、12 季型。读者不需要相信本站，可以去查这些名字。
 *
 * **不需要翻译。** 全是专有名词与标准代号，16 种语言写法完全一样，
 * 所以这行直接写在 HTML 里，不进 i18n 词典 —— 也不会因为漏翻而半中半英。
 *
 * 排版上它是一道横线加一行小字：横线把「标题区」和「操作区」分开，
 * 小字用 --dim（6.31:1）而不是更淡的灰，因为它是给人读的信息，不是装饰。
 */
/* 首屏能力标签。
   改造后只剩两条（原四条：12 项可复核 / 照片不上传 / 16 种语言 / 可导出）。
   后两条是功能不是理由，已从首屏删掉 —— 四条指向四个方向，等于没有方向。
   两条在 390px 下也折成两行（第一条 17 个汉字，单独就占满一行）。
   圆角 6px 而不是胶囊 —— 胶囊在这套视觉语言里等于「可点的按钮」，
   而这两个标签不可点击。
   边框用 --line-strong（3:1），不用 --line（装饰线）：
   它围的是一个「信息块」，和控件一样需要看得清边界。 */
.hero-chips {
  list-style: none;
  margin: 20px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.hero-chips li {
  padding: 5px 11px;
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  font-size: 12px;
  line-height: 1.45;
  letter-spacing: 0.01em;
  color: var(--text2);
  background: transparent;
  font-variant-numeric: tabular-nums;
}

/* 对比页入口。**故意不是按钮**：首屏的主任务只有上传一个，
   这里给的是「另一条路」，用按钮会和上传框抢注意力。
   但它是链接，就得满足 2.5.8 的 24×24 —— 13px 的行内文字只有 ~19px 高，
   所以用 inline-flex + min-height 撑到 24，不靠 line-height 凑（那会把行距带歪）。
   `flex-wrap` 是给德语/土耳其语留的：那两句在 390px 下放不进一行。 */
.hero-alt,
.report-alt {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2px 8px;
  margin: 14px 0 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text2);
}

.hero-alt-go {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 0 2px;
  color: var(--accent2);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

.hero-alt-go:hover {
  text-decoration-thickness: 2px;
}

.hero-alt-go:focus-visible {
  outline: 2px solid var(--accent2);
  outline-offset: 2px;
  border-radius: 3px;
}

/* 报告页里的同一个入口：上面紧挨着导出按钮、下面就是免责声明，
   首屏那 14px 在这里会三块挤在一起，所以单独给 26px。
   其余样式与 .hero-alt 共用 —— 视觉上必须是同一个东西，用户才会认出来。 */
.report-alt {
  margin: 26px 0 0;
}

/* 手机/平板上目标要 ≥44（2.5.5 AAA + 项目既定），桌面 24（2.5.8 AA）。
   **只加高度，不加字号**：44px 是点击面积，不是视觉高度 —— 文字仍是 13px，
   多出来的是透明 padding，看上去什么都没变，手指却点得准。
   第一版忘了这条，布局审计立刻在 index@768/390/320 各报「小目标 1」。 */
@media (max-width: 899px) {
  .hero-alt-go {
    min-height: 44px;
  }
}

.hero-cite {
  margin: 18px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--line);
  font-size: 12px;
  line-height: 1.7;
  letter-spacing: 0.04em;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
  /* 这一行永远是拉丁文专名，不受「balance 会切断中文词」那条限制 ——
     390px 下普通断行会剩「ITA° · 12-season」孤零零一行，按整段平衡后
     两行长度接近。 */
  text-wrap: balance;
}

.hero-cite b {
  /* 模型名给一点点重量，但不抢主标题。用字重而不是颜色 —— 颜色要留给「指标正常」。 */
  font-weight: 600;
  color: var(--text2);
}

/* 上传框。
 *
 * 原来是 `1.5px dashed` —— 虚线在视觉上读作"占位符/待填"，是原型图的语言，
 * 而且 1.5px 的虚线段在深底上会抖（每段两端都抗锯齿）。
 * 现在改成「实线外框 + 内缩的取景角标」：
 *   外框 1px 实线给出明确边界（--line-strong 保证 3:1，1.4.11）；
 *   内缩 10px 的四角用四条短实线勾出来，读作相机的取景框 ——
 *   与「上传一张正面照」这件事同义，不是纯装饰。
 * 角标用 background-image 的四段 linear-gradient 画，不额外加 DOM 节点
 * （加了就要处理它进不进无障碍树、会不会被 Tab 到）。
 */
.dropzone {
  /* position:relative 是给里面那个 1px 的 input 定位用的（见 .sr-file），
     不加的话它会跑到文档左上角，聚焦时页面会往上滚一屏。 */
  position: relative;
  /* **横排，不是居中的大方块。**
     原来是一块 250px 高、内容居中、中间一个金色辉光圆的区域 ——
     那是「上传你的自拍」的语法。改成「图标在左、两行文字在右」之后，
     它读作一个表单控件（输入源），和上面左对齐的标题连成一条视觉动线。
     顺带把 250px 的空高压到 110px 左右，首屏能多露出一部分说明文字。
     **不用改 HTML**：三个子元素用 grid 定位，图标跨两行。 */
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: 18px;
  row-gap: 3px;
  align-content: center;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background-color: var(--panel);
  padding: 30px 26px;
  text-align: left;
  cursor: pointer;
  transition: border-color 0.18s, background-color 0.18s, box-shadow 0.18s;
}

/* 图标跨两行，两行文字自动落到第二列 */
.dropzone > .dz-icon { grid-column: 1; grid-row: 1 / span 2; align-self: center; }
.dropzone > .dz-main { grid-column: 2; grid-row: 1; }
.dropzone > .dz-hint { grid-column: 2; grid-row: 2; }

/* 取景角标：四个角各两条 18px 的短线，内缩 10px。
   用 4 个渐变 + 4 个尺寸做 8 条线，全部塞进一层 background-image。 */
.dropzone::after {
  content: "";
  position: absolute;
  inset: 10px;
  pointer-events: none;   /* 别挡住 label 的点击区 */
  border-radius: 7px;
  --c: rgba(163, 61, 46, 0.30);
  --len: 18px;
  --w: 1px;
  background-image:
    linear-gradient(var(--c), var(--c)), linear-gradient(var(--c), var(--c)),
    linear-gradient(var(--c), var(--c)), linear-gradient(var(--c), var(--c)),
    linear-gradient(var(--c), var(--c)), linear-gradient(var(--c), var(--c)),
    linear-gradient(var(--c), var(--c)), linear-gradient(var(--c), var(--c));
  background-repeat: no-repeat;
  background-size:
    var(--len) var(--w), var(--w) var(--len),   /* 左上 */
    var(--len) var(--w), var(--w) var(--len),   /* 右上 */
    var(--len) var(--w), var(--w) var(--len),   /* 左下 */
    var(--len) var(--w), var(--w) var(--len);   /* 右下 */
  background-position:
    left top, left top,
    right top, right top,
    left bottom, left bottom,
    right bottom, right bottom;
  transition: opacity 0.18s;
  opacity: 0.9;
}

/* 上传框的 input 必须**仍然可聚焦**。
   原来写的是 `hidden`（= display:none）：label 鼠标点得动，但 Tab 到不了 ——
   整站唯一的入口变成鼠标专属，键盘用户在首页就卡死（2.1.1 键盘）。
   `type=file` 是原生控件没错，可被 CSS 藏掉之后它就不再是入口了。

   这里用「视觉隐藏但仍留在 Tab 顺序与无障碍树里」：1px + clip-path。
   不能用 display:none / visibility:hidden / aria-hidden —— 三者都会把它摘出去。
   也不用 tabindex+role=button 自己转发 click：那等于重写一遍原生控件，
   还要自己处理 Enter/Space 和读屏播报，反而更容易漏。 */
.sr-file {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* 焦点落在那个 1px input 上时，把焦点环画在用户真正看得见的框上。
   否则键盘用户只能看到一个 1px 的角在闪（2.4.7）。 */
.dropzone:focus-within {
  border-color: var(--accent2);
  outline: 2px solid var(--accent2);
  outline-offset: 3px;
  background-color: var(--panel2);
  /* 纯白化后删掉内高光：白底上 rgba(255,255,255,.0x) 等于不存在 */
}

.dropzone:hover,
.dropzone.dragover {
  border-color: var(--accent);
  background-color: var(--panel2);
}

/* 拖进来的时候角标整圈亮起来 —— 这是唯一一处"用颜色表达可放置"的地方，
   鼠标悬停只提亮边框（弱一档），拖拽是更强的意图，给更强的反馈。 */
.dropzone.dragover::after {
  --c: rgba(232, 200, 90, 0.85);
  opacity: 1;
}

/* 加号图标做成圆形底座。
   原来是一个 34px 的 `＋` 字符裸放在那里 —— 单薄，而且它是整块区域里
   唯一的视觉焦点，撑不住 400px 的宽度。
   现在 56px 圆 + 金色描边 + 极淡填充，读作一个"按钮"，与"点这里"一致。 */
.dz-icon {
  /* 44px 的**方形**，不是金色实心圆。
     圆形 + 径向渐变 + 内高光是一套「按钮浮起来」的语法，放在这里读作
     「点我」；方形细边只读作「这里是一个输入位」。金色留给真正表示
     状态的地方（当前页、指标正常），不铺在装饰上。 */
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 22px;
  font-weight: 300;
  line-height: 1;
  color: var(--text2);
  border: 1px solid var(--line-strong);
  border-radius: 9px;
  background: none;
  transition: border-color 0.18s, color 0.18s;
}

/* 窄屏：说明文字**独占一整行**。
   横排在 390px 上只剩约 280px 给文字，「JPG / PNG / WEBP · 建议正面、光线均匀、
   无遮挡」被压成两行，而且断在「均匀」中间（中文没有词边界，浏览器按字断）。
   让 dz-hint 跨两列之后它有 338px，一行放得下。 */
@media (max-width: 560px) {
  .dropzone { padding: 22px 20px; column-gap: 14px; row-gap: 10px; }
  .dropzone > .dz-icon { grid-row: 1; }
  .dropzone > .dz-hint { grid-column: 1 / -1; grid-row: 2; }
}

/* 悬停 / 拖入时图标才亮起来 —— 状态变化用颜色表示，静态时不用。 */
.dropzone:hover .dz-icon,
.dropzone.dragover .dz-icon,
.dropzone:focus-within .dz-icon {
  color: var(--accent2);
  border-color: var(--accent);
}

.dropzone:hover .dz-icon,
.dropzone.dragover .dz-icon {
  border-color: rgba(232, 200, 90, 0.7);
  background: radial-gradient(circle at 50% 30%, rgba(163, 61, 46, 0.26), rgba(163, 61, 46, 0.07));
}

.dz-main {
  font-size: 16px;
  font-weight: 500;
  /* 行距由 `.dropzone` 的 row-gap 控制，这里不再写 margin-bottom ——
     两处都写会让图标和两行文字的垂直中心偏 7px。 */
  margin: 0;
  letter-spacing: 0.01em;
}

.dz-hint {
  font-size: 13px;
  color: var(--dim);
  letter-spacing: 0.01em;
}

/* 断行按整段平衡，而不是「填满一行再挤下一行」。
 *
 * **只对非 CJK 生效。** 中文没有空格，`balance` 会在词中间断 —— 实测：
 * 「光线均匀」被断成「光线均 / 匀」、「随时回看」被断成「随时回 / 看」。
 * 这比它要治的孤行更难读。它真正能治的是英文副标题「最后一个词单独一行」
 * 那种情况（词之间必须有空格，平衡只会挪空格）。
 *
 * `:lang()` 读的是 `<html lang>`，i18n-runtime 每次切换都会写它，
 * 所以这条规则跟着界面语言走。不支持 `text-wrap` 的浏览器退回普通断行。 */
html:not(:lang(zh)):not(:lang(ja)):not(:lang(ko)) .hero-sub,
html:not(:lang(zh)):not(:lang(ja)):not(:lang(ko)) .dz-hint {
  text-wrap: balance;
}

/* 隐私承诺条。
 *
 * 原来是绿色（rgba(74,222,128,...) 的底 + 边）。问题不是"绿不好看"，是
 * **它在语义上被占用过**：--good 这个绿在这个站里专门表示"这一项指标正常"。
 * 用它画一条全站常驻的说明条，等于把"通过"的颜色降级成"提示"，
 * 而且一个金色主调的页面里突然出现第三种色相，看起来像从别的产品贴来的组件。
 *
 * 现在改成中性面板 + 一条金色左脊：仍然"被强调"，但不再和指标抢颜色。
 * `--accent2` 压在 `--panel` 上约 10:1，`--text2` 约 7.4:1，都远超 4.5。
 */
.privacy-note {
  margin-top: 18px;
  padding: 14px 17px 14px 18px;
  border-radius: var(--radius);
  background: var(--panel);
  border: 1px solid var(--line);
  /* 左脊用内阴影而不是 border-left：border 会挤动内边距，
     而这行文字的换行位置在窄屏上很敏感（多 1px 就多一行）。 */
  box-shadow: inset 3px 0 0 0 rgba(163, 61, 46, 0.55);
  font-size: 14px;
  color: var(--text2);
}

.privacy-note strong {
  color: var(--accent2);
}

/* ---------- 特性 ---------- */
.features {
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* 横向留 40px 栏距，纵向**不留** —— 行与行之间由 hairline 分隔。
     四张浮起来的卡片读作「功能卖点」，四条横线分隔的条目读作「规格表」。
     内容是同一份，语法不同，可信度差很多。 */
  gap: 0 40px;
  margin-top: 40px;
  /* 收口线。每条 .feature 只有 border-top，最后一行下面没有线 ——
     列表看起来像「没排完」。容器补一条底边，四组条目才成为一个封闭的表。
     用容器而不是给最后两个子项加 border-bottom：1 列布局（手机）下
     最后一项是第 4 个，2 列布局下是第 3、4 个，写死子项索引会漏。 */
  border-bottom: 1px solid var(--line);
}

/* 四张卡是同一层的并列信息，必须**底部齐平**。
   原来靠内容自然撑高，四段文字长度不同 → 下边缘参差 8~20px，
   第二行（03/04）看起来像"没对齐"。现在让每张卡都是 flex 列，
   正文吃掉剩余空间，卡高由同行最高的那张决定。
   `align-items: stretch` 是 grid 的默认值，显式写出来是为了防止
   以后有人在 .features 上加 align-items: start。 */
.features { align-items: stretch; }

.feature {
  padding: 18px 0 20px;
  /* 去掉背景、圆角、内高光、外阴影。原来那三样是为了让卡片「浮起来」，
     而浮起来的东西读作「营销模块」；同一份文档里的条目才用横线分节。 */
  border-top: 1px solid var(--line);
  display: flex;
  flex-direction: column;
}

.f-num {
  font-size: 11.5px;
  font-weight: 600;
  /* 等宽数字 + 放宽字距：01/02/03/04 是四张卡的"序号列"，
     用比例字宽时 1 比 0 窄，四个编号的左边缘看起来会抖。 */
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.14em;
  margin-bottom: 10px;
  /* 序号是辅助信息，不该和标题同权重。
     **不用 opacity 压它** —— 这个项目已经踩过一次（`.s12-hsb{opacity:.85}`
     把 4.5:1 压到 3.80）。要更弱就给一个更暗的金色，颜色值是可算的，
     opacity 是乘在算完之后、把对比度一起乘掉的。 */
  color: #a8873a;
}

/* 用 h2 而不是 h3：这四张卡是 <h1> 下面的一级分区，
   写成 h3 会让读屏的标题大纲从一级直接跳到三级（1.3.1 / 2.4.6），
   用户按标题跳的时候会以为中间漏了一节。
   字号仍然按卡片的视觉层级写死，不受元素默认样式影响。 */
.feature h2 {
  font-size: 15px;
  margin: 0 0 7px;
  font-weight: 600;
}

.feature p {
  font-size: 13.5px;
  color: var(--dim);
  margin: 0;
  line-height: 1.6;
}

/* ---------- 加载 ---------- */
.loading-box {
  padding: 100px 0;
  text-align: center;
}

.spinner {
  width: 40px;
  height: 40px;
  margin: 0 auto 22px;
  border: 2.5px solid var(--line);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.85s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* ---------- 减少动效（WCAG 2.3.3 / 用户偏好，非 SC） ----------
 * 系统里开了「减少动态效果」的用户，这个转圈加载指示器会一直转 ——
 * 而它转的时候正好是页面**唯一**的活动区。前庭功能敏感的用户会不适。
 * 处理方式不是「不转」，是**换成同样能表达"正在处理"的静态形态**：
 * 一个呼吸式的透明度变化太接近闪烁（2.3.1 三次闪烁），所以干脆只留
 * 一段静态的、颜色更实的半环 + 进度条本身继续走（进度条是宽度变化，
 * 不是旋转，且下面几条 transition 也一并被这个块关掉了）。
 * 这里同时把过渡时长压到 0.01ms：2.3.3 管的是交互触发的位移/缩放，
 * 而不是「颜色渐变」，压到几乎瞬时既满足偏好又不影响可读性。
 */
@media (prefers-reduced-motion: reduce) {
  .spinner {
    animation: none;
    border-top-color: var(--line);
    border-right-color: var(--accent);
    border-width: 3px;
  }
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
}

#loading-text {
  color: var(--text2);
  font-size: 14px;
  margin-bottom: 16px;
}

.loading-bar {
  width: 220px;
  height: 3px;
  margin: 0 auto;
  background: var(--line);
  border-radius: 2px;
  overflow: hidden;
}

#loading-fill {
  height: 100%;
  width: 0;
  background: var(--accent);
  transition: width 0.4s ease;
}

/* ---------- 结果头 ---------- */
.result-head {
  display: flex;
  gap: 18px;
  padding: 26px 0 22px;
  align-items: center;
}

.result-photo {
  width: 108px;
  height: 108px;
  flex: 0 0 108px;
  border-radius: 12px;
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--panel);
}

.result-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* 「脸型判定」这类小标签。
   0.1em 的字距是给**全大写英文**准备的（`FACE SHAPE` 这样排才不挤）。
   中文四个字加 0.1em，在 12px 下每个字之间多出 1.2px，
   读起来像"脸 型 判 定"—— 散架。汉字只需要一点点呼吸感。 */
.eyebrow {
  font-size: 12px;
  color: var(--accent);
  letter-spacing: 0.1em;
  font-weight: 600;
  margin-bottom: 4px;
}

:lang(zh) .eyebrow,
:lang(ja) .eyebrow,
:lang(ko) .eyebrow {
  letter-spacing: 0.04em;
}

.result-title h2 {
  font-size: 30px;
  margin: 0 0 12px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* 汉字标题不用负字距（理由同 .hero h1）：30px 下"椭圆脸"三个字的横画
   会被压得几乎相接。这一条放在后面，特异性也够（0,1,1 对 0,1,0），
   直接覆盖上面的负值。 */
:lang(zh) .result-title h2,
:lang(ja) .result-title h2,
:lang(ko) .result-title h2 {
  letter-spacing: 0.01em;
}

.conf-row {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-bottom: 7px;
}

.conf-label {
  font-size: 12.5px;
  color: var(--dim);
}

.conf-bar {
  flex: 1;
  max-width: 150px;
  height: 5px;
  background: var(--line);
  border-radius: 3px;
  overflow: hidden;
}

#conf-fill {
  height: 100%;
  width: 0;
  background: linear-gradient(90deg, var(--warn), var(--accent2));
  transition: width 0.6s ease;
}

.conf-value {
  font-size: 13px;
  font-weight: 600;
  color: var(--accent2);
}

.conf-explain {
  font-size: 12.5px;
  color: var(--dim);
  margin: 0;
  line-height: 1.55;
}

/* ---------- 卡片 ---------- */
.card {
  margin-top: 14px;
  padding: 20px;
  border-radius: var(--radius);
  background: var(--panel);
  border: 1px solid var(--line);
  /* 与 .feature 同一套质感：顶部 1px 内高光给"厚度"，一层极淡外阴影让卡片
     从背景上浮起来。深色界面上没有这两样，卡片就只是一块颜色略浅的矩形，
     两张并排时读不出层次。 */
  box-shadow: 0 1px 2px rgba(25, 23, 19, 0.07);
}

.card h3 {
  font-size: 15px;
  margin: 0 0 14px;
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 8px;
}

.card h3::before {
  content: "";
  width: 3px;
  height: 14px;
  background: var(--accent);
  border-radius: 2px;
}

.card-sub {
  font-size: 13px;
  color: var(--dim);
  margin: -8px 0 14px;
}

.why {
  font-size: 14.5px;
  color: var(--text2);
  margin: 0 0 16px;
}

/* ---------- 比值 ---------- */
.ratio-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}

.ratio-item {
  padding: 12px;
  border-radius: 10px;
  background: var(--panel2);
  text-align: center;
}

.ratio-item .r-label {
  font-size: 11.5px;
  color: var(--dim);
  margin-bottom: 5px;
}

.ratio-item .r-value {
  font-size: 19px;
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.ratio-item .r-hint {
  font-size: 11px;
  color: var(--dim);
  margin-top: 3px;
}

/* ---------- 三庭 ---------- */
.thirds-viz { margin-bottom: 18px; }

.thirds-bars {
  display: flex;
  gap: 4px;
  height: 42px;
  border-radius: 8px;
  overflow: hidden;
  margin-bottom: 10px;
}

.third-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12.5px;
  font-weight: 600;
  color: #0b0e13;
  transition: flex 0.5s ease;
}

.third-bar.b1 { background: var(--accent); }
.third-bar.b2 { background: var(--accent2); }
/* b3 是三条里最深的一条。原来的 #8b7355 上面压近黑字只有 4.31 ——
   中色调最难办：它既不够亮（黑字不够黑），也不够暗（白字同样只有 4.49）。
   提亮到 #96805f 后黑字 5.11，同时保持「金 → 浅金 → 褐」的递减关系。 */
.third-bar.b3 { background: #96805f; }

.thirds-legend {
  display: flex;
  gap: 16px;
  font-size: 12px;
  color: var(--dim);
  flex-wrap: wrap;
}

.thirds-legend span {
  display: flex;
  align-items: center;
  gap: 6px;
}

.dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  display: inline-block;
}

.dot-1 { background: var(--accent); }
.dot-2 { background: var(--accent2); }
.dot-3 { background: #8b7355; }

.thirds-legend .target { margin-left: auto; }

/* ---------- 指标列表 ---------- */
.metric-list {
  display: flex;
  flex-direction: column;
  gap: 1px;
  background: var(--line);
  border-radius: 10px;
  overflow: hidden;
}

.metric-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 14px;
  background: var(--panel2);
  font-size: 14px;
}

.metric-row .m-label { color: var(--text2); }
.metric-row .m-value {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.metric-row .m-value.ok { color: var(--good); }
/* 1.4.1：偏离标准不能只靠颜色。点状下划线是**非颜色**线索，
   不新增文案（否则要补 15 种语言），也不改布局。
   `.ok` 不需要自己的记号：有没有下划线，就是 ok / off 的区别。 */
.metric-row .m-value.off {
  color: var(--warn);
  text-decoration: underline dotted;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

/* ---------- 偏差 ---------- */
.dev-item {
  padding: 13px 15px;
  border-radius: 10px;
  background: var(--panel2);
  border-left: 3px solid var(--warn);
  margin-bottom: 9px;
}

/* 1.4.1：严重程度不能只靠左边条的颜色。加粗到 5px 是**几何**差异，
   色盲用户也能看出这条比普通偏差更重。box-sizing 是 border-box，不会撑宽卡片。 */
.dev-item.high { border-left-color: var(--bad); border-left-width: 5px; }

.dev-head {
  display: flex;
  /* 必须允许换行：右侧 `.dev-val` 是 nowrap（数值不能被拆开），
     左侧区域名在德/俄/土语里也会变长。两侧都不可收缩时，
     这一行会把卡片顶出去 —— 实测德语 390px + 用户字距 0.12em 时整页横向溢出 5.2px。
     换行后 `margin-left:auto` 让数值仍然靠右，视觉不变。 */
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 5px;
  gap: 2px 10px;
}

.dev-area {
  font-size: 14px;
  font-weight: 600;
}

.dev-val {
  font-size: 12.5px;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  /* 换到第二行时仍贴右；单行时与 space-between 的结果一致 */
  margin-left: auto;
}

.dev-advice {
  font-size: 13.5px;
  color: var(--text2);
  margin: 0;
}

.dev-ok {
  padding: 13px 15px;
  border-radius: 10px;
  background: rgba(74, 222, 128, 0.05);
  border-left: 3px solid var(--good);
  font-size: 13.5px;
  color: var(--text2);
}

/* ---------- 年龄 ---------- */
.age-row {
  display: flex;
  align-items: center;
  gap: 22px;
}

.age-num {
  font-size: 46px;
  font-weight: 700;
  color: var(--accent2);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.age-range {
  font-size: 13.5px;
  color: var(--text2);
  margin-bottom: 6px;
}

.age-note {
  font-size: 12.5px;
  color: var(--dim);
  line-height: 1.55;
}

/* 处境句（改造二）。
   一行，左侧一条金线 —— 和对比页的结论卡同一套语言：
   「左边这条线以内是结论，上面那些是测量」。
   **不加背景块**：一有底色它就成了一个「区块」，而这里要的是
   「在数字旁边多说一句话」，不是又多一张卡。 */
/* 报告页顶部的结论：12 项的收口，不是第 13 项指标。
   比指标旁的处境句（.insight）更重一档：15px、左边 3px 金线、上面留 18px。
   仍然**不用底色** —— 一有底色它就变成又一张卡片，和下面那些并列了。 */
.top-insight {
  margin: 18px 0 0;
  padding: 2px 0 2px 14px;
  border-left: 3px solid var(--accent);
  font-size: 15px;
  line-height: 1.65;
  color: var(--text);
}

.insight {
  margin: 14px 0 0;
  padding: 2px 0 2px 12px;
  border-left: 2px solid var(--accent2);
  font-size: 13px;
  line-height: 1.65;
  color: var(--text);
}

/* ---------- 肤色 ---------- */
.skin-head {
  display: flex;
  align-items: center;
  gap: 15px;
  margin-bottom: 20px;
}

.skin-swatch {
  width: 62px;
  height: 62px;
  border-radius: 12px;
  border: 1px solid var(--line);
  flex: 0 0 62px;
}

.skin-season {
  font-size: 17px;
  font-weight: 600;
  margin-bottom: 3px;
}

.skin-desc {
  font-size: 13px;
  color: var(--dim);
}

.palette-group { margin-top: 15px; }

.palette-label {
  font-size: 12px;
  color: var(--dim);
  margin-bottom: 8px;
}

.palette {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  /* 左对齐：色块要跟上面的「中性色 · 3」标签同一根竖线起头。
     试过 justify-content: center 让换行后的孤块居中，但那样标签在左、
     色块在中间，两者脱节，比原来更乱。 */
  justify-content: flex-start;
}

.swatch {
  /* 色卡改成「名字在上、色块在下」，用纵向空间换横向空间 ——
   * 实测：英文色名（"Light neutral Base"）比中文（"浅中性 底色"）宽一倍，
   * 78px 的标签压在 52px 的格子上，相邻两格必然互压（量到 6 对相交、各 18px）。
   * 横排放不下 8 种语言的词长差异，纵向排可以（每格独占一格宽）。
   * 注意：这里**不能再写 width/height 各一次**（上面的 52px 是旧值，
   * 后写的会覆盖，但同时在同一个块里留两条互相打架的声明，下次改的人一定会看错）。 */
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  gap: 4px;
  /* 85px 的来历：色块数量不固定（中性色 3 个、强调色 5 个），
     而卡片内容宽 463px。5 个要挤进一行需要 5w + 4×8 ≤ 463，
     即 w ≤ 86.2 —— 取 85 留 6px 余量，避免卡片 padding 或字体微调后
     又掉回第二行。

     **这里不能用 `flex-shrink` 救。** 试过 `flex: 1 1 88px`，以为超出的
     9px 会被各格分担，实测仍然换行 —— flex-wrap 判断换行用的是
     hypothetical main size（也就是 flex-basis），**收缩发生在换行之后**。
     basis 放不下就直接折行，每一行内部才轮到 shrink。所以必须让 basis 本身够小。

     `max-width: 88px` 是给 3 个色块那一组用的：basis 85 时 flex-grow
     会把它们拉到 149px，色块间距变得稀稀拉拉；挡住 88 就还是原来的密度。
     85 → 88 之间只有 3px，色名（`overflow-wrap: anywhere` + `height: auto`）
     折行也只在自己的格子里变高，不会压到相邻格 —— 这条在第十八轮量过。 */
  flex: 1 1 85px;
  min-width: 0;
  max-width: 88px;
  height: auto;
}

/* 色块本身：单独一个伪元素把颜色画在下面，文字就不会被背景色压住。 */
.swatch::before {
  content: "";
  display: block;
  order: 1;
  width: 52px;
  height: 52px;
  border-radius: 9px;
  border: 1px solid var(--line);
  background: var(--sw);
}

/* 1.4.5：色块承载的信息不能只靠颜色。可见文字放在色块上方。
 * 原来这里是绝对定位悬挂在色块下方，实测英文下相邻两格互压 18px。 */
.swatch-name {
  order: 0;
  width: 100%;
  font-size: 9.5px;
  line-height: 1.3;
  text-align: center;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
  /* 色名可能很长（俄语 "Светлый нейтральный"），允许断行但不撑破格子 */
  overflow-wrap: anywhere;
}

/* 原来 ::after 用 CSS content 画 hex —— 它不进可访问树。
 * 现在 hex 写进了 .swatch-name 的真实文本里，这条不再需要；
 * 留着会变成「色名 + hex」下面再叠一行 hex，重复且压到分隔线。 */
.swatch::after { content: none; }

.palette {
  /* 8 个色块不再固定在 52px 见方：每格 88px 宽（名字要放得下），
   * 靠 wrap 折行。容器留出下方的分隔距离。 */
  padding-bottom: 12px;
}


/* 状态播报节点的视觉隐藏。
   ⛔ 不能用 display:none / visibility:hidden —— 那样读屏也读不到，等于没做。
   必须是「还在渲染树里、但只剩 1px」这一套。 */
.a11y-live {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* 换背景的选项：色块 + 文字两段。
   不把渐变铺满按钮，是因为最深那一档（#2c3550→#4b3f63）上白字黑字都看不清。 */
.bg-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.bg-chip {
  display: flex; align-items: center; gap: 7px;
  padding: 6px 10px 6px 6px;
  border: 1px solid var(--line); border-radius: 8px;
  background: transparent; color: var(--text);
  font: inherit; font-size: 0.82rem; cursor: pointer;
  /* 触控目标 ≥44px：手机上这一排是最常点的控件 */
  min-height: 44px;
}
.bg-chip:hover { border-color: var(--accent); }
.bg-chip:focus-visible { outline: 2px solid var(--accent2); outline-offset: 2px; }
/* 1.4.1：选中态原先只有边框色 + 内描边。加字重（笔画粗细）作为非颜色线索，
   与 .gender-btn.is-on 用的是同一套办法，视觉语言保持一致。 */
.bg-chip.is-on { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); font-weight: 600; }
.bg-chip-dot {
  width: 26px; height: 26px; border-radius: 50%;
  border: 1px solid rgba(0,0,0,0.12);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 13px; line-height: 1; color: #4b5563;
  flex: 0 0 auto;
}
.bg-chip-label { white-space: nowrap; }


/* ---------- 造型 ---------- */
.style-block { margin-bottom: 18px; }
.style-block:last-child { margin-bottom: 0; }

.style-label {
  font-size: 12.5px;
  color: var(--accent);
  font-weight: 600;
  margin-bottom: 8px;
  letter-spacing: 0.04em;
}

.style-block ul {
  margin: 0;
  padding-left: 18px;
  font-size: 14px;
  color: var(--text2);
}

.style-block li { margin-bottom: 6px; }

.style-block .avoid li { color: #c9a9a9; }

.style-block p {
  margin: 0;
  font-size: 14px;
  color: var(--text2);
}

.tips {
  margin: 0;
  padding-left: 18px;
  font-size: 14px;
  color: var(--text2);
}

.tips li { margin-bottom: 7px; }

/* ---------- 按钮 ---------- */
.actions {
  display: flex;
  gap: 10px;
  margin-top: 22px;
  flex-wrap: wrap;
}

.btn {
  /* 必须显式声明 display。用在 <button> 上时 UA 给的是 inline-block，
     看不出问题；用在 <a> 上时是**行内元素** —— 上下 padding 不参与行高计算，
     按钮会向上溢出、压住前一行文字（影棚页「AI 生成需要先登录」实测被压掉半行）。
     作为 flex 子项时 flex: 1 照样生效，inline-flex 不影响。 */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  flex: 1;
  min-width: 130px;
  padding: 13px 18px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: var(--panel2);
  color: var(--text);
  font-size: 14.5px;
  font-weight: 500;
  cursor: pointer;
  font-family: inherit;
  transition: background 0.16s, border-color 0.16s, transform 0.1s;
}

.btn:hover {
  /* 纯白化前这里是 #232a38 —— 深色主题留下的近黑蓝。压在纸色上勉强能读，
     压纯白就成了一块脏灰。改成「白 + 4% 朱红」：悬停是往主色方向偏，
     不是往「更暗」偏，和 .chip-btn / .task-opt 的悬停语义一致。 */
  background: var(--panel2);
  border-color: var(--line-strong);
}

.btn:active { transform: scale(0.985); }

.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #14100a;
  font-weight: 600;
}

.btn-primary:hover {
  background: var(--accent2);
  border-color: var(--accent2);
}

/* ---------- 免责 ---------- */
.disclaimer {
  margin-top: 24px;
  padding: 16px 18px;
  border-radius: var(--radius);
  background: var(--panel2);
  border: 1px solid var(--line);
  font-size: 12.5px;
  color: var(--dim);
  line-height: 1.7;
}

.disclaimer strong { color: var(--text2); }

/* ---------- 报错 ---------- */
/* 2026-10-31：原来这三行是深色主题的值 —— 浅红字 #fca5a5 压在浅粉底上
   只有 1.9:1，用户报错时基本读不出错在哪。10-29 换纸色时漏了这一处。
   改成 --bad 系：浅底 + 深红边 + 深红字，4.6:1。 */
.error-box {
  margin-top: 18px;
  padding: 15px 17px;
  border-radius: var(--radius);
  background: rgba(156, 53, 39, 0.06);
  border: 1px solid rgba(156, 53, 39, 0.45);
  color: var(--bad);
  font-size: 14px;
}

/* 姿态不合格的提示：比普通错误更详细，用暖黄而不是红色 ——
   这不是「出错了」，是「这张照片不适合分析」 */
.pose-box {
  background: rgba(163, 61, 46, 0.08);
  border-color: rgba(163, 61, 46, 0.30);
  color: var(--accent2);
  line-height: 1.7;
}
.pose-box strong {
  display: block;
  margin-bottom: 6px;
  font-size: 15px;
  color: var(--accent);
}
.pose-box p { margin: 6px 0 0; }
.pose-metrics {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  opacity: 0.95;
}
/* 每项指标单独标状态：过线的淡化，超线的标红。
   用户不用在三行数字里自己比对阈值，一眼看到是哪一项卡住了。 */
.pose-metric { white-space: nowrap; }
.pose-metric b { font-weight: 700; }
.pose-metric.is-over { color: #fb923c; }
.pose-metric.is-over b { text-decoration: underline wavy rgba(251, 146, 60, 0.6); text-underline-offset: 3px; }

.pose-threshold {
  font-size: 12px;
  opacity: 0.62;
  font-variant-numeric: tabular-nums;
}
.pose-tip { font-size: 13px; opacity: 0.8; }
.pose-howto { opacity: 0.95; font-weight: 500; }

/* ---------- 轮廓强度 ---------- */
.strength-block {
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
}
.strength-block h4 {
  margin: 0 0 6px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--accent);
}
.strength-intro {
  margin: 0 0 14px;
  font-size: 12.5px;
  line-height: 1.7;
  opacity: 0.62;
}
.strength-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 18px;
}
.strength-item .s-label {
  font-size: 12px;
  letter-spacing: 0.06em;
  opacity: 0.6;
}
.strength-item .s-value {
  margin: 4px 0 8px;
  font-size: 24px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}
.s-bar {
  height: 4px;
  border-radius: 2px;
  background: rgba(25, 23, 19, 0.09);
  overflow: hidden;
}
.s-bar > i {
  display: block;
  height: 100%;
  width: 0;
  border-radius: 2px;
  background: linear-gradient(90deg, rgba(163, 61, 46, 0.5), var(--accent));
  transition: width 0.7s cubic-bezier(0.4, 0, 0.2, 1);
}
.s-hint {
  margin-top: 8px;
  font-size: 12px;
  line-height: 1.6;
  opacity: 0.55;
}
@media (max-width: 560px) {
  .strength-row { grid-template-columns: 1fr; gap: 16px; }
}

/* ---------- 页脚 ----------
 *
 * 原来 `border-top: 1px solid var(--line)` 紧贴内容，页脚和正文之间
 * 没有任何呼吸 —— 在工具页（正文很短）上它看起来像"内容被截断了"。
 * 现在：上间距拉到 56px、分隔线换成一条从中向两侧淡出的渐变，
 * 字距放宽一点。页脚不是内容，它应该退到背景里。 */
/* ---------- 三步 ---------- */
/* 上面 .features 是「横线分节的规格表」语法；这一段刻意换成
   「三栏 + 大编号 + 竖线分隔」，让页面有第二种节奏。
   同一页全是同一种语法，读起来像一份文档，不像一个产品页。 */
.how {
  margin-top: 48px;
  padding-top: 34px;
  border-top: 1px solid var(--line);
  /* 不写 scroll-margin-top：全局 `html { scroll-padding-top }` 已经留出了
     吸顶栏的高度。两个都写会相加（75 + 83 = 158px 的空白），
     跳过去之后标题离顶栏一大截。 */
}

.how-h {
  margin: 0 0 24px;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--text);
}

.how-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0 28px;
  /* 编号由计数器生成，不写进 HTML —— 阿拉伯语下写死的「1」不会跟着方向走。 */
  counter-reset: how;
}

.how-step {
  counter-increment: how;
  /* 【2026-11-01】原来这里是 `border-right: 1px` + 26px 右内边距，做的是
     「三栏用竖线分开」。用户要求套框后，1578 行那条 border-right 会成为
     框内的一根多余竖线，而且它写在 5379 行那条（设了整圈 border）**之前** ——
     同特异度后来者赢，所以后者没事；但 `:last-child { border-right: 0 }`
     的特异度更高（多一个伪类），会在最后一个框上抠掉右边一条。
     实测就是这么坏的：`.how` 3 个里 1 个少一条边。
     padding-right 也一并交给框化的那条规则。 */
}

/* 编号故意和上面 features 那组「01–04」做得不一样：
   那边是 12px 的条目号（`01` 带前导零、金色实心），这边是 38px 的章节号
   （`1` 不带零、金色 0.28 透明度）。
   第一版两边都用了 26px / 前导零 / opacity .5 —— 截图放在一起看，
   读起来像同一组编号被重复了一遍，等于白做了「第二种节奏」。 */
.how-step::before {
  content: counter(how);
  display: block;
  margin-bottom: 14px;
  font-size: 38px;
  line-height: 0.9;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
  opacity: 0.28;
}

.how-step h3 {
  margin: 0 0 7px;
  font-size: 16.5px;
  font-weight: 600;
  color: var(--text);
}

.how-step p {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--text2);
}

/* 【2026-11-01】手机下原本是「一条上边 + 条目间的下划线」，而 `:last-child` 又把
   最后一条的下边抠掉 —— 套框之后这两种做法都会抠掉框的边。整段交给框化那组规则
   （5379 行附近）处理：单列时是三个上下叠的框，四条边都在。 */

/* ---------- 报告能力清单 ---------- */
/* 「网站太简单」的根因不是排版，是**信息量**：上传态只有 4 条 feature，
   而报告实际有 8 张卡。这一块把 8 项摆出来，让页面自己说明产品有多厚。
   两列而不是三列：每项都有一句 20 字以上的说明，三列在 1024px 下每列只剩 300px，
   说明会折成四行，读起来像脚注。 */
.cap {
  margin-top: 48px;
  padding-top: 34px;
  border-top: 1px solid var(--line);
}

.cap-h {
  margin: 0 0 22px;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--text);
}

/* 「示例值」这句必须紧贴标题，否则读起来像第二段标题。
   cap-h 已经有 22px 下边距，这里上移 12px 抵消一部分。 */
.cap-sub {
  margin: -12px 0 22px;
  max-width: 64ch;
  font-size: 12.5px;
  line-height: 1.65;
  color: var(--dim);
}

/* 三列：8 项排成 3 行，密度够，每列还有 ~360px 放一句 20 字说明。
   两列时每项右侧会空出一大片（说明只有一行），整块看起来像规格表。 */
.cap-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 22px 32px;
}

.cap-item {
  position: relative;
  padding-left: 15px;
}

/* 左侧那道细金脊用伪元素画，不用 border-left ——
   border 会跟着元素高度走到底，而这道脊要在上下各留 3px 的呼吸。
   渐隐到透明，是为了它不像一条「表格竖线」。 */
.cap-item::before {
  content: "";
  position: absolute;
  left: 0;
  top: 3px;
  bottom: 3px;
  width: 2px;
  border-radius: 1px;
  background: linear-gradient(180deg, var(--accent), transparent);
  opacity: 0.55;
}

.cap-item h3 {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin: 0 0 5px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}

/* 示例值由 `data-demo` 属性提供，走 CSS content 而不是写进 HTML 文本 ——
   写进文本的话读屏会把「32 · 34 · 34」当成标题的一部分念出来，
   而它只是示意（真正的数字在用户自己的报告里）。
   等宽字体是为了三列对齐：`32 · 34 · 34` 与 `27–31` 位数不同，
   比例字体下右边缘会参差。 */
.cap-item h3::after {
  content: attr(data-demo);
  flex: 0 0 auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.02em;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}

.cap-item p {
  margin: 0;
  font-size: 13px;
  line-height: 1.6;
  color: var(--text2);
}

@media (max-width: 1024px) {
  .cap-list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px 28px; }
}

@media (max-width: 760px) {
  .cap-list { grid-template-columns: minmax(0, 1fr); gap: 18px; }
}

/*
 * 工具页的列数要单独定：首页主栏 1044px，三列各 312px；
 * 工具页主栏只有约 634px，同样的三列会挤成 **190px 一列** ——
 * 一句 50 字的说明被折成 4 行，末行只剩「片。」这种两字尾巴。
 * 所以工具页固定两列（约 300px），760px 以下仍走单列。
 * 用修饰类而不是改 .cap-list：首页那套三列是量过、截图确认过的，不动它。
 */
/* 【2026-11-01】工具页四张卡 1280 下 224/224/206/206 —— 右列比左列矮 18px。
 * 它们没有编号列，行结构就是「标题 / 说明」，所以不需要跨卡对齐轨道，
 * 只要**同排等高**：`align-items: stretch`（grid 的默认）已经把卡片拉到一样高，
 * 但卡片内部 `align-content: start` 又把它压回内容高度（那是首页为了 subgrid 加的）。
 * 这里把块本身的 align-content 放回 stretch —— 等高由容器决定，内容仍从顶排。
 *
 * 【间距】22/28 那组是在「112px 项名 + 1fr 说明」的窄条形状下调的；改成上下两行
 * 之后 28px 的列间空隙在 484px 的栏里显得比框本身还宽，收到 14。 */
.cap-tool .cap-list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
.cap-tool .cap-item { align-content: stretch; }

@media (max-width: 760px) {
  .cap-tool .cap-list { grid-template-columns: minmax(0, 1fr); gap: 18px; }
  /* 单列时不存在「同排」，stretch 只会把卡片拉成 103.5/82.1 两档不一致的高度
     （实测 @760 是 103.5/103.5/82.1/82.1），回到内容高度。 */
  .cap-tool .cap-item { align-content: start; }
}

/* ---------- 常见问题 ---------- */
.faq {
  margin-top: 48px;
  padding-top: 34px;
  border-top: 1px solid var(--line);
  /* FAQ 是一问一答的长句，铺满 1244px 版心一行 90 多个字，眼睛找不到下一行。
     限到 72ch 但它靠左，不居中 —— 它下面是页脚，居中会让页脚看起来也偏了。 */
  max-width: 72ch;
}

.faq-list { margin-top: 10px; }

.faq-item { border-bottom: 1px solid var(--line); }

.faq-item summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  /* 52px 而不是 44：这一行既是触控目标又是列表项，
     两侧还要留出「上一项的下边框」与「本项文字」之间的呼吸。 */
  min-height: 52px;
  padding: 4px 0;
  font-size: 14.5px;
  font-weight: 500;
  line-height: 1.5;
  color: var(--text);
  cursor: pointer;
  list-style: none;
}

/* 默认三角要两条都关：标准是 list-style，Chrome/Safari 还要 marker 伪元素。 */
.faq-item summary::-webkit-details-marker { display: none; }

/* 箭头用两条边框拼一个 V 再旋转 —— 比字符「＋」可靠：
   字符会受字体影响，而且在某些语言下会被当成文字读出来。 */
.faq-item summary::after {
  content: "";
  flex: 0 0 auto;
  width: 9px;
  height: 9px;
  margin-left: auto;
  margin-right: 3px;
  border-right: 1.5px solid var(--dim);
  border-bottom: 1.5px solid var(--dim);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform 0.18s ease;
}

.faq-item[open] summary::after { transform: rotate(-135deg) translate(-2px, -2px); }

/* 焦点必须看得见：summary 是原生可聚焦元素，浏览器默认轮廓在深色底上几乎不可见。 */
.faq-item summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 3px;
}

.faq-item p {
  margin: 0 0 18px;
  font-size: 13.5px;
  line-height: 1.7;
  color: var(--text2);
}

/* ---------- 法务页 ---------- */
/* 正文限制在 68ch 并居中：法务文本是要读的，不是要「铺满版心」的。
   1080px 版心下如果铺满，一行 90 多个字符，眼睛找不到下一行的起点。 */
.legal {
  max-width: 68ch;
  margin-inline: auto;
  padding-top: 52px;
}

.legal h1 {
  font-size: clamp(26px, 4vw, 34px);
  line-height: 1.25;
  margin: 0 0 10px;
}

/* 汉字标题不放负字距（同 .hero h1 的理由） */
:lang(zh) .legal h1,
:lang(ja) .legal h1,
:lang(ko) .legal h1 { letter-spacing: 0.012em; }

.legal-updated {
  margin: 0 0 34px;
  font-size: 13px;
  color: var(--dim);
}

.legal-block + .legal-block { margin-top: 38px; }

/* 这里也不写 scroll-margin-top，理由同 .how：会和全局 scroll-padding-top 相加。 */

.legal-block h2 {
  font-size: 17px;
  margin: 0 0 12px;
  color: var(--text);
}

/* 小节标题（h3）。法务页原来只有两个 h2 + 六句话，截图上看像占位符 ——
   真正的法务页要有小节：照片怎么处理 / 上传的例外 / 账号存什么 / 用户权利。
   字号只比正文大 1px、字重 600：它要能扫读，但不能和 h2 抢。 */
.legal-block h3 {
  margin: 20px 0 7px;
  font-size: 14.5px;
  font-weight: 600;
  color: var(--text);
}

/* h2 正下方的小节标题不要 20px 的上边距，否则「隐私政策」和第一个小节之间
   会出现一块比段间距大一倍的空白。 */
.legal-block h2 + h3 { margin-top: 14px; }

.legal-block p {
  margin: 0 0 12px;
  font-size: 14.5px;
  line-height: 1.75;
  color: var(--text2);
}

.legal-contact {
  margin-top: 40px;
  padding-top: 20px;
  border-top: 1px solid var(--line);
  font-size: 14px;
}

/* 联系方式单独一行，做成一个真正的触控目标（44px 高）。
   原来它只是段落里一个 18px 高的内联链接，audit-layout 一加进法务页就在
   **5 个断点上全部报「小目标 1」**。这不是 WCAG 2.5.8 的 inline exception
   （那句例外指的是「夹在句子中间的链接，尺寸被行高约束」，而这一行只有它自己），
   所以不能靠豁免绕过去，只能把尺寸做对。
   顺手把它做成了次要按钮：法务页读到最后总要有个出口，而纯下划线文字不像出口。 */
.legal-contact a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 18px;
  border: 1px solid var(--line-strong);
  border-radius: 7px;
  color: var(--accent2);
  font-size: 14px;
  font-weight: 500;
  text-decoration: none;
  transition: border-color 0.15s, color 0.15s;
}

.legal-contact a:hover,
.legal-contact a:focus-visible { color: var(--accent); border-color: var(--accent); }

.footer {
  /* 页脚在 main 之外（sticky footer 的另一半），所以它得自己对齐版心。
     原来靠 `text-align: center` 掩盖了「页脚没有版心约束」这件事 ——
     换成四栏之后，居中的单行立刻暴露成「页脚宽度和正文不一致」。 */
  width: 100%;
  max-width: var(--max);
  margin: 0 auto;
  padding: 40px 18px 40px;
  font-size: 13px;
  color: var(--dim);
  letter-spacing: 0.01em;
  text-align: left;
  /* 用背景渐变画线而不是 border：border 是一条等浓度的实线，
     在 1280px 宽上会把视线横着切断；渐变的线只有中间一段可见，
     读作"分隔"而不是"下边框"。 */
  border-top: 0;
  background-image: linear-gradient(
    90deg,
    transparent,
    var(--line) 22%,
    var(--line) 78%,
    transparent
  );
  background-size: 100% 1px;
  background-position: top center;
  background-repeat: no-repeat;
}

.footer-grid {
  display: grid;
  /* 1.7fr 会让品牌栏右侧空出 200px 的荒地；1.35fr 刚好够
     「浏览器内完成的人脸数据分析」+ 一行本地说明不换行太多。 */
  grid-template-columns: minmax(0, 1.35fr) repeat(3, minmax(0, 1fr));
  gap: 36px 28px;
  padding: 34px 0 30px;
}

.footer-tagline {
  margin: 14px 0 0;
  font-size: 14px;
  color: var(--text);
  opacity: 0.9;
  max-width: 30ch;
}

/* 「所有计算在浏览器内完成」只在底栏出现一次。
   第一版品牌栏里也放了一份，同一句话在 400px 内出现两次 ——
   页脚是最不能有重复句子的地方（它本来就是密集区）。 */

/* 栏目标题：小、大写、宽字距 —— 它是「标签」，不是内容标题。
   仍用 h2 是为了读屏能按标题跳转（页脚有 20 多个链接，没有标题就没法导航）。 */
.footer-h {
  margin: 0 0 14px;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--dim);
}

.footer-col ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 9px;
}

.footer-col a {
  color: var(--text);
  text-decoration: none;
  opacity: 0.8;
  /* 触控目标：行高 20px 太矮，手机上点不准。页脚链接用 padding 撑到 28px，
     行间距 9px 让相邻目标之间不粘连（间距 + padding 合计 >= 44px 的视觉间隔）。 */
  display: inline-block;
  padding: 4px 0;
  /* 【法语把「批量」译成 `Lot`，只有 20px 宽】桌面档的最低目标是 24×24
     （WCAG 2.5.8 AA），而宽度由文案长度决定 —— 中文「批量」46px、英文 `Batch`
     也过线，所以只有法语会红。只撑命中区，不动视觉：背景透明、无边框、文字贴左。
     2026-11-04 第二次补：上一轮修过，被 `ctrl.py restore` 用旧备份吞掉了，
     而 restore 打印的是「全部一致」—— 一致的是备份与磁盘，不是备份与真相。 */
  min-width: 24px;
}

.footer-col a:hover,
.footer-col a:focus-visible {
  opacity: 1;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.footer-base {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 2px;
  padding-top: 22px;
  border-top: 1px solid var(--line);
  font-size: 12.5px;
}

.footer-sep { margin: 0 8px; opacity: 0.5; }

/* 断点用 899px 而不是 760px：768px 的平板上，四栏页脚的链接只有 28px，
   audit-layout 在 tablet 档报了 11 个「小目标」。触控设备的断点应该和
   `.topnav` 一致 —— 那个也是 899px。 */
@media (max-width: 899px) {
  /* 手机上不是「四栏堆成四行」——那样页脚有 800px，等于两屏。
     改成：品牌横跨 → 工具两列 → 关于/法律并排。实测从 803px 降到 ~470px。 */
  .footer-grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas:
      "brand brand"
      "tools tools"
      "about legal";
    gap: 26px 20px;
  }
  .footer-brand { grid-area: brand; }
  .footer-grid > .footer-col:nth-of-type(2) { grid-area: tools; }
  .footer-grid > .footer-col:nth-of-type(3) { grid-area: about; }
  .footer-grid > .footer-col:nth-of-type(4) { grid-area: legal; }

  /* 只有「工具」那一栏排两列（7 个入口，排两列省一半高度）。
     关于/法律不能排两列：320px 下每列只剩 ~140px，而俄语
     Конфиденциальность 一个词就 170px。 */
  .footer-grid > .footer-col:nth-of-type(2) ul {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0 16px;
  }
  /* 触控目标：手机上每个页脚链接 44px。
     桌面端那 28px 是给鼠标的 —— 手指点不准，而且相邻目标只隔 9px。 */
  .footer-col a {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: 0;
    /* 德语 Nutzungsbedingungen / 俄语 Конфиденциальность 是**一个词**，
       没有空格可断。390px 两列布局下实测顶出容器 70px，
       整页横向溢出 53px。这里允许必要时断词（正常情况不会触发）。 */
    overflow-wrap: break-word;
    min-width: 0;
  }
  .footer-tagline { max-width: none; }
}

/* 窄屏再降一档：关于/法律各占一整行。
   390px 下两列只有 ~160px，而德语/俄语的法律词条单字就有 130~190px。 */
@media (max-width: 560px) {
  .footer-grid {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "brand"
      "about"
      "legal";
  }

  /* 「工具」那一栏在手机上收掉。
     它和 60px 上方那个汉堡菜单**是同样 7 个链接**，而且触控目标要 44px，
     7 个入口排两列就是 176px —— 实测页脚因此到 844px，等于一整屏。
     链接仍在 DOM 里（爬虫照常抓得到），只是不重复展示。 */
  .footer-grid > .footer-col:nth-of-type(2) { display: none; }
}


/* ---------- 移动端 ---------- */
@media (max-width: 560px) {
  main { padding: 0 14px 60px; }
  .hero { padding: 34px 0 22px; }
  .features { grid-template-columns: 1fr; }
  /* 比值卡在手机上**不降到单列**。
     每张卡里只有「标签 / 数字 / 一句 5 字说明」，单列时一张卡占满 362px
     宽却只放三个短元素，两张卡上下摞起来白白多滚一屏。
     改成按可用宽度自适应：390px 屏上 362px 内容宽放得下 3 列（每格 114），
     320px 极窄屏上降到 2 列（每格 141）—— 英文标签 "Jaw / cheekbone"
     约 96px，两档都放得下。用 auto-fit 而不是写死 2 列，是因为比值项
     有 2 个也有 3 个：写死 2 列时 3 项会变成 2+1，第三个孤零零占左边。 */
  .ratio-grid { grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); }
  .result-head { gap: 14px; }
  .result-photo { width: 84px; height: 84px; flex-basis: 84px; }
  .result-title h2 { font-size: 25px; }
  .age-row { gap: 16px; }
  .age-num { font-size: 38px; }
  .conf-bar { max-width: 90px; }
  .card { padding: 16px; }
  .dropzone { padding: 40px 18px; }
  .actions { flex-direction: column; }
  .btn { width: 100%; }
}

/* =========================================================================
   可读性档位
   -------------------------------------------------------------------------
   原来全站字号按 --max:760px 定的：正文 13.5px、说明文字 11~12px。
   这在一个手机宽度上是勉强能看的，但桌面版心一放宽就明显偏小。
   下面的做法是把字号抬到基准 17px，并且让版心随视口分三档：
     手机 <720px   单列，边距 14px
     平板 720~1080 单列但更宽，卡片留白加大
     桌面 >1080    版心 1080px，功能卡两列、结果区两栏
   所有数值都来自 audit-layout.mjs 的实测，不是拍脑袋。
   ========================================================================= */

:root {
  --max: 1080px;
  --fs-base: 17px;
  --fs-sm: 14.5px;
}

html, body { font-size: var(--fs-base); }
body { line-height: 1.7; }

/* 说明类文字统一抬到 13.5px 以上，长句再给 1.7 行高 */
.hint, .dz-hint, .r-hint, .s-hint, .dev-advice, .pose-tip, .age-note,
.footer, .feature p, .hero-sub, .why, .conf-explain {
  line-height: 1.7;
}

/* ---- 桌面：两栏 ---- */
@media (min-width: 1081px) {
  .hero { padding: 58px 0 32px; }

  /* 首页 hero 变成「左文右图」两栏。
     用 `:has(.hero-figure)` 限定，工具页的 `.hero.hero-tool` 没有图，不受影响 ——
     它们是「一次只做一件事」的表单，不该被塞一张装饰图。

     `minmax(0, 1fr)` 而不是 `1fr`：`1fr` 的下限是 min-content，
     标题一旦不可断行就会把这一列撑破（德语的长复合词会触发）。 */
  .hero:has(.hero-figure) {
    display: grid;
    /* 300px。360 时图比文字块还高（374 vs 164），首屏的视觉重心整个
       偏到右边，标题反而成了配角 —— 这张图是解释标题的，不该比标题更重。
       300 下 1/5 标注仍然读得清（实测 11px 字号渲染后约 10px 高）。 */
    grid-template-columns: minmax(0, 1fr) 300px;
    align-items: center;
    gap: 44px;
  }
  .hero-figure { display: block; }
  /* 44px 是**居中**标题的尺寸：居中时四周都是空白，需要更大的字撑住版面。
     改成左对齐之后，44px 贴在左边会让整块头重脚轻，缩到 38 才和
     下面的 15px 正文、12px 出处行形成正常的阶梯。 */
  .hero h1 { font-size: 38px; }
  .hero-sub { font-size: 16px; max-width: 620px; }

  .features {
    /* **保持两列，不要四列。** 版心 760px，四列每列只有 170px ——
       14.5px 的正文在那里是 6~7 字一行，读起来像电报。
       两列 340px 才是一段能读的宽度，四条目排成 2×2 的规格表。 */
    grid-template-columns: repeat(2, 1fr);
    gap: 0 56px;
  }
  .feature h2 { font-size: 16px; }
  .feature p { font-size: 14px; }

  .dropzone { padding: 32px 30px; column-gap: 22px; }
  .dz-icon { width: 50px; height: 50px; font-size: 24px; }
  .dz-main { font-size: 17px; }
  .dz-hint { font-size: 13.5px; }

  /* 结果区：照片与标题并排后，正文卡片分两栏读起来不至于一路拖到底 */
  #stage-result {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 18px;
    align-items: start;
  }
  #stage-result[hidden] { display: none; }
  #stage-result .result-head { grid-column: 1 / -1; }

  /* 横跨两栏的卡片。动机不是「想突出」，是「填洞」：
     两栏 grid 里每行的高度由较高那张卡决定，「拍照建议」和
     「缺口说明」都比同行的卡高一大截 —— 留在栏里会在旁边
     拉出几百像素的空白。各占一整行，行高就只由自己决定。

     同时条目分两栏：跨栏后行宽翻倍，一行能排到 100 字以上，
     中文读起来会串行。分两栏后单栏回到 50 字上下。 */
  #stage-result .card--wide { grid-column: 1 / -1; }
  .card--wide .gap-list,
  .card--wide .tips { columns: 2; column-gap: 44px; }
  .card--wide .gap-item,
  .card--wide .tips li { break-inside: avoid; }

  /* 操作按钮在**两栏布局下**改成 2×2，不是一行四个。
     原因是量出来的：这一栏只有 513px，而 4 个按钮各 `min-width: 130px`、
     3 个间隙 30px，合计 550 —— 差 37px，于是变成 3+1：
     第二行的「换一张照片」独占整行 513px，看起来像排错了。
     2×2 后每格 251×47，正好和右栏那条导出工具条并排等高。
     手机上仍是单列（`@media (max-width: 720px)` 里的 flex-direction 规则
     在这里不生效，但 `.btn { width: 100% }` 会让每格各占一行）。 */
  #stage-result .actions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
  }

  /* 免责声明横跨两栏。留在单栏里的话，它右边会空出一整格（513×100），
     而两栏都被上面的操作区占满之后，那一格就是页面上最后一块"洞"。 */
  #stage-result .disclaimer { grid-column: 1 / -1; }

  .card { padding: 24px; }
  .card h3 { font-size: 18px; }
}

/* ---- 平板 ---- */
@media (min-width: 721px) and (max-width: 1080px) {
  .hero h1 { font-size: 38px; }
  .features { grid-template-columns: repeat(2, 1fr); gap: 16px; }
  .feature h3 { font-size: 16.5px; }
  .feature p { font-size: 14.5px; }
}

/* ---- 手机 ---- */
@media (max-width: 720px) {
  :root { --fs-base: 16.5px; }
  .hero h1 { font-size: 28px; }
  .hero-sub { font-size: 16px; }
  .feature p { font-size: 15px; }
  .dz-main { font-size: 17px; }
  .dz-hint { font-size: 14px; }
  .card h3 { font-size: 17px; }
}

/* 触控目标：不小于 44×44（WCAG 2.5.5 建议值） */
.btn, .action-btn, button {
  min-height: 46px;
}

/* ---------- 性别切换 ----------
   做成一行小胶囊，不是大开关：这是文案偏好，不是主功能，
   占太多视觉重量会让「脸型判定与性别无关」这句话失去说服力。
   触摸目标仍保证 ≥ 40px 高（WCAG 2.5.5）。 */
.gender-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin: 2px 0 18px;
  padding: 10px 12px;
  background: var(--panel2);
  border: 1px solid var(--line);
  border-radius: 12px;
}

.gender-label,
.gender-hint {
  font-size: 13px;
  color: var(--dim);
}

.gender-hint {
  margin-left: auto;
  font-size: 12.5px;
}

.gender-btn {
  min-height: 40px;
  padding: 0 18px;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--text2);
  font-size: 14px;
  font-family: inherit;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}

.gender-btn:hover { border-color: var(--accent); color: var(--text); }

.gender-btn.is-on {
  background: var(--accent);
  border-color: var(--accent);
  color: #1a1405;
  font-weight: 600;
}

.gender-btn:focus-visible {
  outline: 2px solid var(--accent2);
  outline-offset: 2px;
}

@media (max-width: 560px) {
  .gender-hint { margin-left: 0; flex-basis: 100%; }
}

/* ---------- 数据导出 ---------- */
.export-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 14px;
  padding: 12px 14px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
}

.export-label {
  font-size: 13.5px;
  color: var(--dim);
  margin-right: 4px;
}

.chip-btn {
  min-height: 40px;
  padding: 0 16px;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: var(--panel2);
  color: var(--text2);
  font-size: 14px;
  font-family: inherit;
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s;
}

.chip-btn:hover { border-color: var(--accent); color: var(--text); }
.chip-btn:focus-visible { outline: 2px solid var(--accent2); outline-offset: 2px; }

.api-snippet {
  margin-top: 12px;
  padding: 18px;
  background: #0a0d12;
  border: 1px solid var(--line);
  border-radius: 12px;
}

.api-lead {
  margin: 0 0 12px;
  font-size: 14.5px;
  color: var(--text2);
  line-height: 1.7;
}

.api-snippet pre {
  margin: 0 0 12px;
  padding: 14px;
  overflow-x: auto;
  background: var(--panel2);
  border-radius: 8px;
  font-size: 13px;
  line-height: 1.65;
  font-family: ui-monospace, "SF Mono", Consolas, "Courier New", monospace;
  color: var(--text2);
  white-space: pre;
}

/* ---------- 语言选择 ----------
   用原生 select：移动端会弹系统滚轮，比自己画的弹层更顺手，
   而且键盘可达、屏幕阅读器可读，这两点自己实现很容易漏。 */
.lang-select {
  min-height: 40px;
  margin-left: 10px;
  padding: 0 30px 0 12px;
  border-radius: 8px;
  border: 1px solid var(--line-strong);
  background: var(--panel) url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath fill='%238a94a5' d='M1 1l5 5 5-5'/%3E%3C/svg%3E") no-repeat right 12px center / 10px;
  color: var(--text2);
  font-size: 13.5px;
  font-family: inherit;
  cursor: pointer;
  appearance: none;
}

.lang-select:hover { border-color: var(--accent); color: var(--text); }
.lang-select:focus-visible { outline: 2px solid var(--accent2); outline-offset: 2px; }

@media (max-width: 560px) {
  /* 1.4.4：系统字号放大到 200% 时，「照片不上传」加 16 语言下拉在 320px 里排不进一行，
     整条顶栏把页面顶出去 26.7px。允许换行 + 给下拉限宽；
     不换行时布局与原来一致（flex-wrap 只在放不下时才换行）。
     放在这个断点里而不是基础层：桌面端 .topbar-right 本来不是 flex，不该顺手改掉。 */
  .topbar-right {
    display: flex; align-items: center; flex-wrap: wrap; min-width: 0;
    /* 导航排在它前面之后（见 .topnav 上的注释），这一组落在自己一行。
       靠右放：视觉上仍读作「顶栏右侧的辅助控件」，而不是第三条导航。
       不加这句会被 space-between 丢在行首，看起来像导航多出一行。 */
    margin-left: auto;
  }
  .lang-select { margin-left: 6px; font-size: 13px; padding-left: 10px; max-width: 100%; }
}

/* ==========================================================================
   扩充分支：采样遮罩 / 12 季型 / Fitzpatrick / 皮肤指标 / 眼型 / 缺口说明
   ==========================================================================
   这些卡的共用原则：数值比标签醒目，解释比数值小一号。
   理由是这些卡里有大量「不可比」的指标（纹理能量、均匀度相对分），
   如果数值和标签一样重，用户会把它当成体检报告里的绝对值。
   ========================================================================== */

/* ---------- 采样遮罩 ---------- */
.mask-modes {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 12px;
}

.mask-modes-label {
  font-size: 12px;
  color: var(--dim);
}

.mask-btn {
  min-height: 34px;
  padding: 0 14px;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: transparent;
  color: var(--text2);
  font-size: 12.5px;
  cursor: pointer;
  transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease;
}

.mask-btn:hover { border-color: var(--accent); color: var(--text); }
.mask-btn.is-on {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--text);
  /* 1.4.1：选中态原来只有底色 + 边框色，加字重作为非颜色线索 */
  font-weight: 600;
}

/* 触控目标：手机端 44px 是底线，桌面 34 视觉上够了，但按 480px 以下要长起来 */
@media (max-width: 480px) {
  .mask-btn { min-height: 44px; padding: 0 18px; }
}

.dot-acne { background: #ff6060 !important; }
.dot-spot { background: #ffc454 !important; }
.dot-line { background: #7ed0ff !important; }

/* 测量线图版：三个色点要和画布上的线**同色**，否则图例在解释另一张图。
 * 画布那边（app.js drawGeometryOverlay）用的是 rgba(240,182,168) /
 * rgba(126,200,255) / rgba(232,192,120)，这里给不透明版本，小色点带透明度
 * 会看不清。 */
.dot-geom-third { background: #f0b6a8 !important; }
.dot-geom-fifth { background: #7ec8ff !important; }
.dot-geom-axis { background: #e8c078 !important; }

/* 三条说明 + 一句推定声明共用一个显隐开关。
 * 拆成三个独立 `.mask-item` 就得在 syncMaskMode 里改三个 `hidden`，
 * 加第四条时必然漏一个 —— 而漏掉的那条只是「一直不出现」，不报错。 */
.mask-legend-group {
  flex: 1 0 100%;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px 22px;
}

/* 采样网格这一卡：**改成单列、图居中、图注横排**。
 *
 * 原来是两列（左 1.15fr / 右 1fr），DOM 顺序是「视图切换 → 画布 → 图例」，
 * grid 自动布局把它们放成：左上=切换、右上=画布、左下=图例。
 * 结果第 1 行的高度由画布（390px）决定，而左格只有一行 34px 的按钮组 ——
 * 左上角空出 350px 纯背景，看起来像内容没加载出来。
 *
 * 两列在这里本来就撑不平：一列是 4:3 的图（400px 高），另一列是三行图例
 * （114px），无论怎么排总有一列空 280px 以上。所以不做两列：
 * 切换器横跨顶部（读作工具栏），图居中（读作插图），图例横排在下方（读作图注）。
 * 图的宽度从 640 降到 620，几乎无差别，但整张卡不再有空洞。
 */
.mask-wrap {
  display: block;
  margin-top: 4px;
}

.mask-modes { margin-bottom: 16px; }

.mask-canvas,
.mirror-canvas {
  width: 100%;
  /* 图不铺满卡片：1040px 宽的卡上放一张 1040 宽的 4:3 照片会高到 780px，
     读者要滚两屏才看完一张图。620 是「看得清采样格」与「不撑爆卡片」的折中，
     实测这张 620×465 上 33px 的采样方格边界清晰。 */
  max-width: 620px;
  height: auto;
  display: block;
  margin: 0 auto 18px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: #05070a;
}

/* 镜像图下面紧跟一行数字（图注的位置），所以把图的底边距收一点 */
.mirror-canvas { margin-bottom: 10px; }

/* 镜像图的两个数：是图注，不是正文。和图一起读，所以居中、贴图。 */
.mirror-stats {
  margin: 0 0 12px;
  text-align: center;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--dim);
}

/* 图例横排：图注就该在图的下面，而不是在另一列当侧栏。 */
.mask-legend {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: center;
  gap: 8px 22px;
}

/* `mask-note`（「这张图是在你自己的浏览器里画出来的」）是整段的说明，
   横排时挤在最后一项旁边会被读成那一项的注解 —— 让它独占一行。 */
.mask-note { flex: 1 0 100%; }

.mask-item {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text2);
}

.mask-item .dot { flex: 0 0 auto; }

.mask-note {
  margin: 2px 0 0;
  /* 12 → 12.5：这句是「发际线是推定值」那种说明，
   * 不是装饰小字。全站正文下限是 12.5px，
   * 单独留一个 12px 只会让人以为「可以小到读不清」。 */
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--dim);
  border-left: 2px solid var(--line);
  padding-left: 10px;
  text-align: left;
}

/* ---------- 12 季型 ---------- */
.s12-head {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-bottom: 16px;
  flex-wrap: wrap;
}

.s12-swatch {
  width: 58px;
  height: 58px;
  border-radius: 12px;
  border: 1px solid var(--line);
  flex: 0 0 auto;
}

.s12-id { min-width: 96px; }

.s12-name {
  font-size: 22px;
  font-weight: 650;
  color: var(--text);
  letter-spacing: 0.01em;
}

.s12-en {
  font-size: 12px;
  color: var(--dim);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* 采样色行：色点 + hex + HSB。
   并进这张卡片是因为旧的「个人色彩」卡片会给出与 12 季型矛盾的季型名。
   排版上必须能换行 —— 窄屏（≤420px）时 hex 和 HSB 会挤，允许折到第二行。 */
.s12-sample {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 6px;
  font-size: 12px;
  color: var(--dim);
  letter-spacing: 0.02em;
}

.s12-dot {
  width: 14px;
  height: 14px;
  border-radius: 4px;
  border: 1px solid var(--line);
  flex: 0 0 auto;
}

.s12-hsb {
  font-variant-numeric: tabular-nums;
  /* 原来这里还有 opacity:.85。它把 --dim 压成 rgb(109,119,135)，
     在 --panel 上只有 3.80 —— 而审计对 opacity 是按合成值算的，
     不是「看不见所以跳过」。12px 的字不需要再压暗一次来降级，
     层级由字号和 --dim 本身表达就够了。 */
}

.s12-stats {
  display: flex;
  gap: 22px;
  margin-left: auto;
  flex-wrap: wrap;
}

.s12-stat {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.s12-stat span { font-size: 11.5px; color: var(--dim); }
.s12-stat b { font-size: 15px; color: var(--text); font-weight: 600; }
.s12-stat em {
  font-size: 10.5px;
  color: var(--dim);
  font-style: normal;
  max-width: 130px;
  line-height: 1.4;
}

/* ---------- Fitzpatrick ---------- */
.fit-head { margin-bottom: 14px; }

.fit-type {
  font-size: 22px;
  font-weight: 650;
  color: var(--text);
}

.fit-desc {
  font-size: 13px;
  color: var(--text2);
  margin-top: 4px;
  line-height: 1.6;
}

.fit-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  margin-bottom: 14px;
}

/* ITA° / L*a*b* / 分档 三个小格。
   `--panel2`(#1c222e) 压在卡片底 `--panel`(#161b25) 上只差 6 个色阶，
   1px 的 `--line` 边框又只有 1.15:1 —— 三个格子几乎融进卡片里，
   只剩里面的字是浮着的。
   修法是给它们一个"凸起的小块"的读法：顶部 1px 内高光（与 .card /
   .feature 同一套语言），边框仍是 --line（这里不是交互控件，
   不需要 1.4.11 的 3:1，用 --line-strong 反而会抢过卡片标题）。
   注意不要配内阴影 —— 内阴影读作"凹陷"，与亮边框的"凸起"互相打架。 */
.fit-item {
  background: var(--panel2);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
  /* 纯白化后删掉内高光：白底上 rgba(255,255,255,.0x) 等于不存在 */
}

.fit-item span { font-size: 11.5px; color: var(--dim); }
.fit-item b {
  font-size: 15px;
  color: var(--text);
  font-weight: 600;
  overflow-wrap: anywhere;
}

@media (max-width: 560px) {
  .fit-grid { grid-template-columns: 1fr; }
  .s12-stats { margin-left: 0; gap: 16px; }
}

/* ---------- 皮肤指标 ---------- */
.sk-row {
  display: grid;
  grid-template-columns: 132px minmax(0, 1fr) auto;
  gap: 8px 12px;
  align-items: center;
  padding: 12px 0;
  border-top: 1px solid var(--line);
}

.sk-row:first-child { border-top: 0; }

/* .sk-row 是 display:grid，会盖掉 [hidden] 默认的 display:none ——
   不加这一条，「黑眼圈测不了」时那一行仍然会显示出来，只是值是空的。
   本项目里 .stage / .tool-error 都踩过同一个坑，各自补过一条。 */
.sk-row[hidden],
#sk-detail[hidden] { display: none; }

/* 分组标题：细分五项和上面「整脸色调」不是同一类测量，需要一条分隔。
   用一条上边框 + 小字，不用卡片嵌套 —— 手机上多一层缩进就少 16px 宽度。 */
.sk-group {
  margin: 18px 0 0;
  padding-top: 14px;
  border-top: 1px solid var(--line);
  font-size: 12px;
  font-weight: 650;
  letter-spacing: 0.04em;
  color: var(--dim);
  text-transform: uppercase;
}

#sk-detail .sk-row:first-child { border-top: 0; }

.sk-label {
  font-size: 13px;
  color: var(--text2);
}

.sk-bar {
  height: 6px;
  border-radius: 4px;
  background: var(--line);
  overflow: hidden;
  min-width: 40px;
}

.sk-bar > i {
  display: block;
  height: 100%;
  width: 0;
  border-radius: 4px;
  background: linear-gradient(90deg, var(--accent), var(--accent2));
  transition: width 0.35s ease;
}

.sk-value {
  font-size: 13.5px;
  color: var(--text);
  font-weight: 600;
  white-space: nowrap;
}

.sk-hint {
  grid-column: 1 / -1;
  margin: 2px 0 0;
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--dim);
}

@media (max-width: 560px) {
  .sk-row { grid-template-columns: 1fr auto; }
  .sk-bar { grid-column: 1 / -1; order: 3; }
}

/* ---------- 眼型 ---------- */
.eye-shape-name {
  font-size: 22px;
  font-weight: 650;
  color: var(--text);
  margin-bottom: 14px;
}

.eye-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 10px;
}

.eye-item {
  background: var(--panel2);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.eye-item span { font-size: 11.5px; color: var(--dim); }
.eye-item b { font-size: 16px; color: var(--text); font-weight: 600; }

/* ---------- 瞳距 / 前框总宽 ----------
 * 复用 .eye-grid / .eye-item 的「指标小卡」外观：同一个东西在两张卡里长得一样，
 * 用户才不会以为这是另一类数据。只补两处：
 *   1) 格子下方的小字（区间 / 取哪端）—— <em> 默认是斜体，压平；
 *   2) 与下面那段定性建议之间留一点距离，否则两段文字会粘在一起。
 */
/* minmax 140 而不是沿用 .eye-grid 的 120：320 下两列每列内宽只有 100px，
   「125–150 mm」需要 104px，`mm` 会被挤到下一行。抬到 140 之后，
   auto-fit 在窄屏上自己退成一列，390 上仍是两列 —— 不加断点。 */
.pd-grid {
  margin-bottom: 12px;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.pd-grid em {
  font-style: normal;
  font-size: 11.5px;
  color: var(--dim);
  line-height: 1.5;
}

/* 批注放进 .style-block 之后，`.style-block p`（0,1,1）会盖掉 `.caveat`（0,1,0）
   的 margin / font-size / color —— 竖脊还在，但字号变成 14px、上间距变 0，
   两段批注粘成一段。这条 (0,2,1) 把批注自己的尺寸拿回来。
   不写在 .caveat 里，是因为 .caveat 在别处（卡片根部）本来就是对的。 */
.style-block p.caveat {
  margin: 12px 0 0;
  font-size: 12.5px;
  color: var(--dim);
}

/* ---------- 共用：贴边界提示 / 不可用 ---------- */
.border-warn {
  margin: 10px 0 0;
  padding: 9px 12px;
  font-size: 12.5px;
  line-height: 1.65;
  color: var(--warn);
  background: rgba(251, 191, 36, 0.07);
  border: 1px solid rgba(251, 191, 36, 0.24);
  border-radius: 9px;
}

.unavailable {
  margin: 0;
  padding: 10px 12px;
  font-size: 12.5px;
  line-height: 1.65;
  color: var(--dim);
  background: var(--panel2);
  border-radius: 9px;
}

/* ---------- 缺口说明 ---------- */
.gap-card {
  /* 视觉上要和「算出来的结论」区分开：左侧一道虚线，
     暗示这是「边界」而不是「数据」 */
  border-left: 3px solid rgba(124, 135, 152, 0.4);
}

.gap-item { padding: 12px 0; border-top: 1px solid var(--line); }
.gap-item:first-of-type { border-top: 0; padding-top: 4px; }

.gap-head {
  display: flex;
  align-items: baseline;
  gap: 9px;
  margin-bottom: 5px;
}

.gap-tag {
  font-size: 10.5px;
  color: var(--dim);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 0 5px;
  line-height: 16px;
  flex: 0 0 auto;
}

.gap-head b { font-size: 14px; color: var(--text2); font-weight: 600; }

.gap-item p {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.72;
  color: var(--dim);
}

/* ---------- 每张卡的边界说明（caveat） ---------- */
.caveat {
  margin: 12px 0 0;
  padding: 10px 12px;
  /* 12.5 而不是 11.5：这段是「照片会上传到第三方」的告知，
     全站最该被读清的一句，不该用全站最小字号。 */
  font-size: 12.5px;
  line-height: 1.72;
  color: var(--dim);
  background: var(--panel2);
  border: 1px solid var(--line);
  border-radius: 9px;
}

.caveat:empty { display: none; }

/* ==========================================================================
   工具箱：顶部导航 / 工具页布局
   ========================================================================== */

/* 顶栏要同时装下 品牌 + 导航 + 右侧控件。
   手机上一行放不下，所以导航换成可横滑的第二行 —— 不用汉堡菜单，
   是因为工具就 5 个，横向滑一下比点开再选少一步。 */
.topbar { flex-wrap: wrap; gap: 10px 14px; }

.topnav {
  /* 这里**不能**写 order。
     原来写的是 `order: 3`，把导航排到语言下拉下面，视觉顺序变成
     「品牌 → 语言 → 导航」，而 DOM 是「品牌 → 导航 → 语言」。
     CSS 的 order 改视觉不改 DOM，Tab 走 DOM —— 结果键盘用户在手机上
     按完 7 个导航链接，焦点会**往上跳 102px** 回到语言框（2.4.3）。
     量出来的：390px 下 7 页全部 1 处逆序（a#nav-account → select#lang-select）。
     现在让视觉顺序等于 DOM 顺序，语言下拉排在导航之后（顶栏多一行，
     但焦点方向和眼睛看到的方向一致）。桌面端不受影响：≥900px 的
     order 仍把导航放回中间。 */
  display: flex;
  /* 从「横向滚动」改成「换行」。
     原因很具体：加到第 6 个入口（登录）之后，390px 上一行放不下，
     横滑方案会把「登录」整个推到视口外 —— 而它恰好是新用户唯一的入口，
     看不见等于没有。换行会让顶栏高出一行，但每个入口都真的在屏幕上。 */
  flex-wrap: wrap;
  gap: 4px;
  width: 100%;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.topnav::-webkit-scrollbar { display: none; }

.topnav a {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;          /* 手机上触控目标不小于 44px */
  padding: 0 14px;
  /* 胶囊 → 6px 圆角矩形。胶囊是「按钮」的形状，而这里是导航；
     而且激活态画的是下划线（`inset 0 -2px`），配 999px 圆角会在
     左下 / 右下两个角露出金色的圆弧尾巴。 */
  border-radius: 6px;
  color: var(--text2);
  font-size: 13.5px;
  text-decoration: none;
  white-space: nowrap;
  border: 1px solid transparent;
}
.topnav a:hover { color: var(--text); background: var(--panel2); }
.topnav a[aria-current="page"] {
  color: var(--accent2);
  /* **下划线，不再是金色胶囊。** 胶囊是「按钮」（可点的动作），
     下划线是「目录里的当前位置」—— 顶栏这七个入口是导航，不是动作。
     顺带解决了另一个问题：金色胶囊在深底上是一块高饱和色斑，
     七个入口里有一个特别亮，视觉重心被拉走；下划线只加 2px 高度，
     位置信息还在，重量却回到了文字上。 */
  background: none;
  border-color: transparent;
  box-shadow: inset 0 -2px 0 0 var(--accent);
}

/* ---------- 手机顶栏：三行 209px → 一行 60px + 汉堡菜单 ----------
 *
 * 这一段改过两次。第一次是「挤爆换行 → 等宽两行」，把 7 个入口排成
 * 4 列 × 2 行的整齐栅格 —— 整齐了，但**代价是高度**：
 *
 *     10(pad) + 44(brand) + 6(gap) + 90(nav 两行) + 6(gap) + 44(工具行) + 8(pad) = 209
 *
 * 209px 在 844 高的屏幕上占掉 **24.8%**，首屏只剩 635px。
 * 而这三行里每一行都是 44px 的触控下限，**想降高度只能减少行数** ——
 * 「排得更紧」这条路走不通（`check-target` 会红）。
 *
 * 所以改成：收起时只有一行（品牌 / 菜单 / 语言），8 个入口进汉堡菜单。
 * 实测 60px，省 149px。
 *
 * **展开时浮在内容上，不推内容。** 用 `position: absolute; top: 100%`
 * 挂在 sticky 顶栏下面。走文档流的话，展开一次整页会往下跳 90px，
 * 用户正在看的位置就丢了。
 *
 * **收起用 `display: none`，不是 visibility/opacity**：只有它能同时把菜单
 * 从「Tab 序」和「读屏的可达树」里摘掉 —— 收起的链接不该被 Tab 到。
 */

/* 汉堡按钮本体。基础规则里是 `display: none`（桌面不显示），
   手机断点里改成 inline-flex。**这条必须写在 media query 之前** ——
   同特异性下后写的赢，写在后面会把手机端的 inline-flex 盖回 none。 */
.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  background: var(--panel);
  color: var(--text);
  cursor: pointer;
}
.nav-toggle svg { width: 20px; height: 20px; display: block; }

@media (max-width: 899px) {
  .topbar {
    /* 三格网格：品牌吃剩余宽度，按钮和语言各占自己那份。
       原来是 `display: flex` + `flex-wrap` 让 8 个入口自然换行。 */
    display: grid;
    /* 第一列写 `minmax(0, 1fr)` 是错的 —— 它允许 1fr 被压到**比内容还窄**，
       而 grid 的默认 `min-width: auto` 被这个显式 min 覆盖了，字就画到格子外面
       被右边的汉堡盖住。2026-10-31 实测（probe-brand）：320px 下品牌右沿 111、
       汉堡左沿 89，**重叠 22px**，汉堡那条 1px 竖边正好描在「FaceLab」的 L 上。
       `scrollWidth <= innerWidth` 对这个缺陷全绿 —— 文字溢出不产生滚动。 */
    grid-template-columns: minmax(auto, 1fr) auto auto;
    align-items: center;
    gap: 10px;
    padding: 8px 14px;
  }

  .brand { grid-column: 1; }
  .nav-toggle { grid-column: 2; display: inline-flex; }
  .topbar-right {
    grid-column: 3;
    width: auto;
    margin-left: 0;
    gap: 8px;
  }

  /* 菜单展开时，顶栏那一行（汉堡 / 语言）要**读成面板的标题行**。
     面板改成纸色之后（见 .topnav.is-open 的注释），它和顶栏自己
     （rgba(11,14,19,.82) 压纸底）几乎同色，中间那条 1px 边界看不见 ——
     截图里像「菜单没开始、牌子悬在空中」。
     做法不是把顶栏重新染深（那会把语言下拉的纸底控件压成墨底），
     而是给这一行铺 --panel2（比面板深一档的纸白）：
     三者形成 纸 → 深一档 → 纸 的三段，边界靠**块面**分，不靠线。 */
  .topbar:has(.topnav.is-open) {
    background: var(--panel2);
    backdrop-filter: none;
    border-bottom: 1px solid var(--line-strong);
  }

  /* 手机上不显示「照片不上传」徽章。
     它不是被删了 —— 每个页面正文里都有一条更完整的隐私说明
     （首页与 5 个工具页都有 `.privacy-note`）。手机上那 44px 更值钱。 */
  .privacy-badge { display: none; }

  .topnav { display: none; }

  .topnav.is-open {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 2px;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    padding: 8px 14px 10px;
    /* **必须完全不透明。**
       第一版写的是 rgba(11,14,19,0.98) + backdrop-filter: blur(14px)，
       量出来 `elementFromPoint` 在菜单三个位置都返回 NAV（没有东西盖在菜单上），
       但截图里 h1 清清楚楚地从菜单中间透出来 —— 剩下那 2% 叠上模糊，
       足以让一个大字号标题变成「菜单位于一段可读文字之上」。
       下拉菜单不需要半透明：它盖住的是正文，让正文露出来只会变成两层文字。
       所以 alpha 改成 1，并去掉 backdrop-filter（也省掉一层合成）。

       **第二处（2026-10-31）：底色原来写的是 #0b0e13。**
       那是深色主题时代的墨色，而全站 2026-10-29 已经换成纸 #f6f0e4 / 墨 #191713。
       结果菜单展开时是一块**纯黑面板 + 墨字**：品牌「FaceLab」几乎看不见
       （墨 25,23,19 压在 11,14,19 上，对比度 1.13:1），四个二级入口也只有 2.4:1。
       截图里看着像「品牌被压住了」，其实是「黑底黑字」。
       配色表里没有一条规则说菜单要深色 —— 它跟顶栏、跟页脚一样是纸。 */
    background: var(--panel);
    border-bottom: 1px solid var(--line);
    box-shadow: 0 14px 28px rgba(25, 23, 19, 0.16);
  }

  .topnav a {
    padding: 0 4px;
    font-size: 13px;
    /* 网格项会被拉满一格，文字要自己居中；桌面上链接宽度就是内容宽，
       这条不生效，所以只写在手机断点里。 */
    justify-content: center;
    /* 展开后每个入口仍然是 44px —— 触控目标不因为「收进菜单」而缩小。 */
    min-height: 44px;
  }
}

@media (min-width: 900px) {
  .topnav {
    order: 2;
    width: auto;
    /* 【从 `flex: 1` 改成 `flex: 0 1 auto`】
       写 `flex: 1` 等于 `1 1 0%`：基础宽 0、还会被拉伸，
       于是导航**永远占满品牌与语言之间剩下的全部宽度**，而它里面的 10 个
       链接是 `flex: 0 0 auto` + `nowrap` —— 放不下时既不收缩也不换行，
       直接越过视口右缘。
       2026-11-03 实测（德语，1024px）：nav 宽 1006、从 x=20 一直伸到 1026，
       而视口可用宽只有 1009，横向溢出 17px、`a.is-on` 的右边界在 1026。
       英语、意大利语、波兰语、土耳其语都是 0 —— 所以这个缺陷**只在长文案的
       语言上出现**，而 `audit-layout` 只量英文，一直全绿。

       现在让导航按内容宽排（`auto` 基宽）、放不下时由下面那条规则匀出余量。
       居中还会一并消失（内容宽 + margin 自动 = 贴左），这是可接受的：
       「品牌 → 导航 → 账户」贴左读起来更像目录，右侧控件仍靠右。 */
    flex: 0 1 auto;
    /* 兜底：万一后面又加了口径，宁可链接挤窄也不能溢出视口。 */
    min-width: 0;
    justify-content: center;
    overflow: visible;
    /* 桌面端**必须显式变回 nowrap**。
       基础层为了让手机上每个入口都真在屏内，写的是 `flex-wrap: wrap`
       （见 `.topnav` 那条注释）；这里不覆盖，桌面端就继承 wrap ——
       一旦内容宽一点，导航就悄悄折成两行，而不是暴露「放不下」。
       2026-10-31 实测：加第 10 个入口后 1280px 上 nav 宽 880、内容 ~906，
       四个中文词折成 3+1 的「人脸报 / 告」，而横向溢出是 0。 */
    flex-wrap: nowrap;
  }
  /* 第 10 个入口（「哪张更好」）加进来之后，900~1280 这一段到了临界点：
     按原来的 `padding: 0 14px` 和 13.5px，1280px 上 nav 宽 880 而内容要 ~906，
     flex-wrap 一换行，四个中文词就变成 3+1 的「人脸报 / 告 · 身材 · 哪张更好 · AI 影棚」
     —— 文字被劈成两行，**横向溢出是 0**，旧判据全绿。
     所以把桌面端的左右内边距收到 10px、字号 13px。
     判据是 `probe-nav-fit` 的「入口都在视口内 / 页不横向溢出」两条，不是肉眼。 */
  .topnav a { min-height: 36px; padding: 0 10px; font-size: 13px; }
  /* 【`margin-left: auto` 是补一条一直缺的规则，不是顺手改版】
     桌面端 `.topbar-right`（隐私徽章 + 语言下拉）一直**落在第二行、贴着左边缘**：
     `.topbar` 是 `flex-wrap: wrap` + `space-between`，而导航在 1280 上就吃满了
     第一行，右侧那一组只能换行；换行之后它是这一行唯一的一项，
     `space-between` 把它推到行首。截图里读起来像「语言下拉从导航里掉出来了」。
     手机上那一组是 `grid-column: 3`（贴右），桌面反而贴左 —— 两个断点不一致。
     2026-11-03 量到：1280px 下它 top=61、left=0。 */
  .topbar-right { order: 3; margin-left: auto; }
}

/*
 * 【桌面端一律允许折行：放不下就折，而不是挤出视口】
 *
 * 背景（读数，不是感觉）：10 个入口 + 品牌 + 语言下拉，德语要 992px、
 * 俄语要 1011px、法语要 1060px 才排得下一行，而 900px 的视口只有 885px 可用。
 * 原来靠 `flex: 1` + `nowrap` 硬塞，结果是**链接被推出视口** ——
 * 最右那个（账户）在 1026，视口到 1009 就没了（WCAG 1.4.10 重排）。
 *
 * 为什么不留 `nowrap` 只收内边距：宽度差从 107 缩到 21 之后，
 * 剩下的 21px 只能靠「字贴字」挤出来 —— 相邻入口读成一串，
 * 换行至少是**每一块都完整可读**。
 *
 * 【为什么是「一律」而不是「900~1199」】
 * 起初只给窄档开折行，理由是「≥1200 有富余，折行说明规则写错了」。
 * 那个理由只在**默认字号、默认字距**下成立。WCAG 1.4.12 要求用户把
 * 字间距加到 0.12em、行高 1.5 时内容不丢 —— 那时导航要宽 12%：
 * 法语在 1280 下从 1150 涨到约 1288，而可用只有 1245。nowrap 的结果是
 * 入口被推出视口右缘，而这不是「折行不好看」，是**内容看不见了**。
 *
 * 所以：`flex-wrap: wrap` 常开，靠 `flex: 0 1 auto` + 内容宽来决定折不折。
 * 默认设置下 1280 仍然是一行（实测 fr 余量 108px），
 * 只有用户放大字距 / 缩小窗口时才会折 —— 那正是它该折的时候。
 * 「默认设置下 ≥1200 必须一行」这条由 `probe-nav-fit` 守着
 * （`LANGS=all` 16 语 × 7 宽 = 1200 条），所以「入口悄悄变多、把导航撑开」
 * 照样会被抓到。
 */
@media (min-width: 900px) {
  .topnav { flex-wrap: wrap; row-gap: 2px; overflow: visible; }
}

/* 900~1079：品牌 + 10 个入口 + 语言下拉在 900px 上真的挤。
   再收一档内边距，字号不动（13px 是这一档可读性的下限）。 */
@media (min-width: 900px) and (max-width: 1079px) {
  .topnav a { padding: 0 7px; }
}

/* 【这里原来有一条 `.topnav:has(a:focus-visible) { overflow-x: auto; padding: 0 6px }`】
 *
 * 它是在「nowrap + flex:1 硬塞」那个方案下写的：链接被压到字贴字，
 * 所以给键盘用户一条「聚焦时松开」的出路。
 *
 * 换成折行之后，**这个前提不存在了** —— 每一块都有 7~10px 内边距，
 * 谁也不用挤谁。而它本身有两个代价：聚焦时 `padding` 从 10 变 6，
 * 十个入口会横向抖一下（2.4.11 焦点外观要求稳定）；`overflow-x: auto`
 * 还会在 nav 里凭空长出一条滚动条。方案换了，补丁就该跟着删，
 * 留着它等于给未来的自己埋一个「为什么聚焦会抖」的谜。
 */

/* ---------- 工具页骨架 ---------- */
.hero-tool { padding-bottom: 26px; }
.hero-tool h1, .hero-tool h2 { font-size: 32px; margin: 0 0 10px; }

/* 工具页的上传态内容比视口矮，大屏上会「全堆在上面、下面空一大片」——
   1280×900 下 bg.html 的下半屏是纯背景色，看起来像页面没加载完。
   让上传段撑满 main 的剩余高度并垂直居中，空出来的空间就变成上下对称的留白，
   读作构图而不是缺口。
 *
 * 为什么用 `flex: 1` 而不是 `min-height: calc(100vh - ...)`：
 *   后者要把顶栏、页脚、main 的 padding 全算进去，而页脚高度会随语言变化
 *   （长文案换行）—— 算小了没效果，算大了冒滚动条。`flex: 1` 直接吃
 *   main 的剩余空间，这些数字一个都不用管。
 *
 * **必须带 `:not([hidden])`。** 这条选择器的特异性 (0,2,2) 高于
 * `.stage[hidden] { display: none }` 的 (0,2,0) —— 不带它的话，隐藏中的
 * 上传段会被强制 `display: flex` 显示出来，页面上同时出现"上传"和"结果"。
 *
 * `:has(.hero-tool)` 把范围限在工具页：首页的上传段（`.hero`）有特性卡撑高，
 * 不需要居中；强行居中会把 hero 顶到奇怪的位置。
 *
 * **手机上也居中**（原来是 ≥900px 才开）。理由是量出来的：390×844 下
 * 上传页的正文只有约 580px，加上 main 的 80px 底部内边距与页脚的 56px 上边距
 * 之后文档高 939px —— 为一个 95px 的空白滚了一屏。居中 + 摘掉那两笔死空间后
 * 文档高正好等于视口，且内容仍是垂直居中的。
 * 内容比剩余空间高时（矮屏 / 结果态），`flex-shrink: 0` 保证它不压缩，照常滚动。
 */
main > section[data-stage="upload"]:not([hidden]):has(.hero-tool) {
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* ↑ 整段居中，保留。

     曾经想改成「标题贴顶、卡片居中」，理由是 5 个工具页的 h1Top 分别是
     279 / 292 / 306 —— 标题会跳 27px。改完实测更糟：
     1280 下标题在 127、卡片在 380，**中间空出 200px**，
     底部又空 300px。原来整组居中时标题和卡片是挨着的、读作一块；
     拆开之后那道空隙读作「这里少了一块内容」。

     结论：**27px 的跳动是「副标题行数不同 + 整段居中」的必然结果，
     不是缺陷**；为了消掉它而破坏「一组内容挨在一起」，代价大于收益。
     真正要修的是「一组东西别一半一半」——要么都居中，要么都贴顶。

     整段居中还有一个前提：`.hero-tool` 的上内边距要去掉（见下面那条），
     否则居中的块内偏置会让整块被压低 34px。 */
  flex: 1 0 auto;
}

/* 居中时去掉 `.hero-tool` 的上内边距。
   这 58px 是给「内容贴着顶栏」的情况留的气口；居中之后它变成块内偏置 ——
   实测文字块上留白 153px、下留白 85px，整块被压低 34px。

   **判据必须写成「在上传态里」，不能写成「排除某些页」。**
   第一版是 `body:not(.page-account) .hero-tool` —— 排除名单，
   因为 login.html 的标题也带 `.hero-tool` 却没有居中逻辑。
   那个排除名单漏掉了 studio.html（结构和 login 一模一样），
   于是 **AI 影棚的 h1 被顶到顶栏下沿（h1Top = 69 = headerH）**，
   比正常位置少了 58px 气口，看起来像标题被顶栏压住。

   排除名单是「新增一页就会漏」，白名单是「不满足条件自动不生效」。 */

/* 把「main 的底部内边距」和「页脚上外边距」从居中计算里摘掉。
   这两笔共 136px 的死空间只加在下边，不摘的话整块内容会比视觉中心高 68px
   （1280×900 实测：上留白 80、下留白 138）。
   **只在工具页上传态生效** —— 结果页内容长，那 80px 是它需要的呼吸空间。
 *
 * 选择器**不能嵌套 `:has()`**（`main:has(... :has(.hero-tool))`）。
 * 实测 Chrome 直接判 "is not a valid selector"，整条规则被**静默丢弃**，
 * 计算值仍是 80px / 56px，页面上一点报错都没有。改成单个 `:has()`
 * 里用后代选择器表达「上传段里有 .hero-tool」。 */
main:has(> section[data-stage="upload"]:not([hidden]) .hero-tool) {
  padding-bottom: 0;
}
main:has(> section[data-stage="upload"]:not([hidden]) .hero-tool) ~ .footer {
  margin-top: 0;
}

@media (min-width: 900px) {
  /* 居中时把 `.hero-tool` 的 58px 上内边距去掉（理由见上一条注释：
     居中之后它变成块内偏置，整块被压低 34px）。

     **判据写成「在上传态里」，而不是「排除某些页」。**
     原来的 `body:not(.page-account) .hero-tool` 是排除名单，漏了 studio.html
     —— 它和 login.html 结构一样（`.hero-tool`，但不在 `section[data-stage]` 里），
     于是 AI 影棚的 h1 被顶到顶栏下沿（h1Top = 69 = headerH）。
     白名单写法下，studio 与 login 都不满足「在上传态里」，自动不受影响。

     **只在 ≥900px 生效**：手机上内容正好占满，去了 h1 会贴着顶栏。
     这条 media query 是它的作用域，别挪到外面去。 */
  main > section[data-stage="upload"]:not([hidden]) .hero-tool { padding-top: 0; }

  main > section[data-stage="upload"]:not([hidden]):has(.hero-tool) {
    /* 上传态是「一次只做一件事」的表单，版心比结果页窄。
       1280 下 1044px 宽的上传框里只有一行字和一个加号，读起来是「框没填满」；
       680px 让框的宽高比落在 2.5:1 附近，视觉上是一张卡而不是一条横幅。
       窄下来之后 `.hero-sub` 的 620px 上限也就不再是瓶颈。 */
    max-width: 680px;
    /* **必须同时写 width: 100%。** flex 项的交叉轴上有 auto 外边距时，
       浏览器不再做 stretch，而是按 fit-content 定宽 —— 实测只到 634px
       （正好是内部最长那行提示文字的宽度），max-width 根本没被碰到。
       显式给 100% 让宽度先吃满、再被 max-width 截住，auto 外边距才用来居中。 */
    width: 100%;
    margin-inline: auto;
  }

  /* 副标题在上传态占满版心。`.hero-sub` 的 560px 上限是给**居中**的首页写的
     （那一页两侧还有留白，560 读起来是「一段话」）；上传态已经收窄到 680，
     再压到 560 会让中文副标题折成「…颜色只落在头 / 发上。」——
     既断词又只剩 3 个字。放宽到整列后 44 字一行放得下。
     其它语言更长，仍是两行，但末行有足够长度。 */
  main > section[data-stage="upload"]:not([hidden]) .hero-sub {
    max-width: 100%;
  }
}

.tool-head { margin: 8px 0 16px; }
.tool-head h1, .tool-head h2 { font-size: 26px; margin: 0 0 6px; }
.tool-meta { margin: 0; font-size: 13px; color: var(--dim); }
.tool-meta.is-warn { color: var(--warn); }

.tool-layout { display: grid; gap: 18px; align-items: start; }

@media (min-width: 900px) {
  .tool-layout {
    grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
    /* 两栏高度天然不等：右边是控制面板，条数随工具变化（试妆 4 组色板 ≈ 780，
       换背景 ≈ 660，换发色 ≈ 520），左边永远是一张方图。
       `align-items: start` 会让左边短一截、下面留一块纯背景色的洞
       （换背景 213px、试妆 320px）。撑满之后预览卡读作「画布视口」，
       图片在卡内垂直居中 —— 而不是「一张小卡 + 一块空」。 */
    align-items: stretch;
  }

  /* 只居中，不拉伸画布：`canvas { width: 100%; height: auto }` 保持不变，
     所以图仍是方的，多出来的高度变成卡片内的上下留白。 */
  .tool-canvas-wrap {
    display: flex;
    align-items: center;
    justify-content: center;
  }
}

.tool-canvas-wrap {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 10px;
  min-width: 0;
}
.tool-canvas-wrap canvas {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 10px;
  /* 画布容器的底色：纯白化前是 --bg2。现在它也是 #fff，而画布本身通常
     是白底图 —— 容器与图同色，看不出图铺没铺满。改 --panel2 当「底片」。 */
  background: var(--panel2);
}

.tool-panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 18px;
  min-width: 0;
}

.panel-block { padding: 0 0 16px; margin-bottom: 16px; border-bottom: 1px solid var(--line); }
.panel-block:last-of-type { border-bottom: 0; margin-bottom: 0; padding-bottom: 0; }

.panel-label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 12.5px;
  letter-spacing: 0.04em;
  color: var(--dim);
  text-transform: uppercase;
  margin-bottom: 10px;
}
.panel-label output { color: var(--text2); font-variant-numeric: tabular-nums; }

.panel-hint { margin: 8px 0 0; font-size: 12px; line-height: 1.65; color: var(--dim); }

/* ---------- 色板 ---------- */
.color-chips {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 8px;
}

.color-chip {
  position: relative;
  aspect-ratio: 1;
  min-height: 40px;
  border-radius: 10px;
  border: 2px solid transparent;
  cursor: pointer;
  padding: 0;
  box-shadow: inset 0 0 0 1px rgba(25, 23, 19, 0.14);
  transition: transform 0.12s ease, border-color 0.12s ease;
}
.color-chip:hover { transform: translateY(-2px); }
.color-chip.is-on { border-color: var(--accent2); box-shadow: 0 0 0 2px rgba(232, 200, 90, 0.25); }
/* 1.4.1：选中态原先只有金色描边。色板本身就是一块颜色，靠颜色区分「选中」
   对色盲用户等于没有线索 ⇒ 叠一个勾，形状线索不依赖色相。
   做法是「深色圆底 + 白勾 + 一圈白描边」：浅色块（白底证件照）靠深底看得见，
   深色块（深棕发色）靠白描边看得见，任意色块都不至于糊成一片。 */
.color-chip.is-on::after {
  content: "✓";
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.58);
  box-shadow: 0 0 0 1.5px rgba(255, 255, 255, 0.92);
  color: #fff;
  font-size: 13px;
  line-height: 1;
  pointer-events: none;
}
.color-chip:focus-visible { outline: 2px solid var(--accent2); outline-offset: 2px; }

.custom-color {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 12px;
  font-size: 13px;
  color: var(--text2);
}
.custom-color input[type="color"] {
  width: 44px;
  height: 44px;   /* 36 → 44：它和它的 label 都只有 36 高，结果态触控目标判据红 */
  padding: 2px;
  /* 这是**可点的控件**，不是分隔线：用装饰色只有 1.15:1，深色底上框等于没有。 */
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  background: var(--panel2);
  cursor: pointer;
}

/* ---------- 滑块与勾选 ---------- */
/* 选择器按 type 兜底，不只认 .range ——
   bg.html 的 #bg-feather 就是漏写了 class="range"，结果它是全站唯一的
   原生滑块：轨道由浏览器画，深色模式下只有 1.54:1，而且和其它页面的滑块长得不一样。
   一个控件的外观不该取决于「有没有记得写那个 class」。 */
input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 44px;            /* 触控高度（项目标准 44）—— 原来写 40，注释说「触控高度」却少了 4px */
  background: transparent;
  cursor: pointer;
}
input[type="range"]::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: 999px;
  /* 右端原来是 20% 透明度的金色，压在面板上几乎等于背景色 ——
     轨道有一半是看不见的，用户不知道滑块能拖到哪（1.4.11 要 ≥3:1）。
     0.6 时对面板只有 **2.66:1**（原注释写 3.33:1，那是按旧的浅色底算的）。
     纸张换成 #f6f0e4 之后底色更亮，同一个 0.6 就掉到线下 —— 改成 0.72。
     2026-10-31 纯白化后复核：0.72 复合成 #bd7369，压 #ffffff 3.61:1、
     压 --panel2 #faf5f4 **3.34:1**，最差的一侧仍过 3:1，所以值不用再动。 */
  background: linear-gradient(90deg, var(--accent), rgba(163, 61, 46, 0.72));
}
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 22px;
  height: 22px;
  margin-top: -8px;
  border-radius: 50%;
  background: var(--accent2);
  border: 2px solid var(--bg);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
input[type="range"]::-moz-range-track { height: 6px; border-radius: 999px; background: rgba(163, 61, 46, 0.72); }
input[type="range"]::-moz-range-thumb { width: 22px; height: 22px; border-radius: 50%; background: var(--accent2); border: 2px solid var(--bg); }

.check-row {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  font-size: 13.5px;
  color: var(--text2);
  cursor: pointer;
}
.check-row input { width: 18px; height: 18px; accent-color: var(--accent); }

.panel-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 18px; }

/* ---------- 工具页错误条 ---------- */
.tool-error {
  margin: 0 0 16px;
  padding: 12px 14px;
  border-radius: 10px;
  border: 1px solid rgba(248, 113, 113, 0.4);
  background: rgba(248, 113, 113, 0.1);
  color: #fecaca;
  font-size: 13.5px;
  line-height: 1.6;
}
.tool-error[hidden] { display: none; }

/* 空的时候收成零高度，但**不能** display:none。
   错误框是 role="alert" 的 live region：display:none 会把它整棵子树
   从无障碍树上摘掉，之后写进去的文字读屏收不到（WCAG 4.1.3）。
   「不占位」和「不在渲染树里」是两件事 —— 这里要的只是前者。
   同理，别用 visibility:hidden / aria-hidden，三者都会摘出可访问性树。 */
.tool-error:empty {
  margin: 0;
  padding: 0;
  border: 0;
}

@media (max-width: 420px) {
  .color-chips { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .tool-panel { padding: 14px; }
  .tool-head h1, .tool-head h2 { font-size: 22px; }
}

/* 工具面板里的下拉框。和顶部语言选择器同一套视觉，但占满宽度。 */
.tool-select {
  width: 100%;
  min-height: 44px;
  padding: 0 34px 0 12px;
  border-radius: 10px;
  border: 1px solid var(--line-strong);
  background: var(--panel2) url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath fill='%238a94a5' d='M1 1l5 5 5-5'/%3E%3C/svg%3E") no-repeat right 12px center / 10px;
  color: var(--text);
  font-size: 14px;
  appearance: none;
}
.tool-select:hover { border-color: var(--accent); }
.tool-select:focus-visible { outline: 2px solid var(--accent2); outline-offset: 2px; }

/* ==========================================================================
   账号页 / 批量页 / 试妆页
   ========================================================================== */

/* ---------- 表单 ---------- */
.auth-card {
  max-width: 420px;
  margin: 0 auto;
  padding: 20px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

/* 页签的「凹槽」原来靠 --bg2（比卡片下沉一档）。纯白化后 --bg2 = #fff，
   于是容器和卡片一模一样白，两个页签浮在一块看不见的板上 —— 截图里
   只剩选中的那块有背景。改成 --panel2（白 + 4% 朱红）：
   凹槽回来了，而且和站点「朱红只表示强调」的约定同向。 */
.auth-tabs {
  display: flex;
  gap: 6px;
  padding: 4px;
  margin-bottom: 18px;
  background: var(--panel2);
  border: 1px solid var(--line);
  border-radius: 12px;
}

.auth-tab {
  flex: 1;
  min-height: 44px;          /* 触控目标下限 */
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--text2);
  font-size: 14px;
  font-family: inherit;
  cursor: pointer;
}
/* 1.4.1：选中的页签原先只有底色和字色。加下划线是纯几何线索，
   而且下划线不占布局，切换页签时不会让相邻元素跳动。 */
.auth-tab.is-on {
  background: var(--panel2);
  color: var(--text);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 6px;
}
.auth-tab:focus-visible { outline: 2px solid var(--accent2); outline-offset: 2px; }

.field { display: block; margin-bottom: 14px; }

.field-label {
  display: block;
  margin-bottom: 6px;
  font-size: 12.5px;
  letter-spacing: 0.04em;
  color: var(--dim);
}

.field input {
  width: 100%;
  min-height: 46px;
  padding: 0 12px;
  border-radius: 10px;
  border: 1px solid var(--line-strong);
  /* 纯白化前是 --bg2（比卡片下沉一档）。现在 --bg2 = #fff，输入框和卡片
     整片同色，只剩一根边框 —— 用户分不清「哪里能打字」。改 --panel2。 */
  background: var(--panel2);
  color: var(--text);
  /* 16px：iOS 上小于 16px 的输入框会在聚焦时自动放大页面 */
  font-size: 16px;
  font-family: inherit;
}
.field input:focus { outline: none; border-color: var(--accent); }
.field input:focus-visible { outline: 2px solid var(--accent2); outline-offset: 1px; }

.field-hint { margin: 0 0 14px; font-size: 12px; line-height: 1.6; color: var(--dim); }

.btn-block { display: block; width: 100%; text-align: center; }
.btn[disabled] { opacity: 0.55; cursor: default; }

/* 主按钮的禁用态**去掉填充**，改成描边。
 *
 * 只靠 opacity 压暗不够：强调色即使压到 0.55 透明度，仍然是一块亮色，
 * 读起来还是「可以按，只是有点脏」。影棚页的「生成」在没选图时就是这种状态 ——
 * 截图里和旁边可用的「去登录」几乎分不出来（当时实测两者底色都是
 * rgb(201,162,39)，只差一个 opacity 0.55 / 1；那个金色已在 2026-10-25 换成朱红）。
 *
 * 放在 `.btn-primary:hover` **之后**：两者特异性都是 (0,2,0)，
 * 同分时后者胜，这样禁用时悬停也不会亮起来。 */
.btn-primary[disabled] {
  background: transparent;
  border-color: var(--line-strong);
  color: var(--dim);
  opacity: 1;
}
.btn.is-busy { cursor: progress; }

.auth-switch {
  margin: 14px 0 0;
  font-size: 13px;
  color: var(--dim);
  text-align: center;
}

.link-btn {
  border: 0;
  background: none;
  /* 44px 下限是量出来的：切换模式的按钮只有「注册」两个字，宽 34px。
     它虽然高度够，但手指横向容易点空 —— 而它是唯一的「去注册」入口。 */
  min-width: 44px;
  min-height: 44px;
  padding: 6px 10px;
  color: var(--accent2);
  font-size: 13px;
  font-family: inherit;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ---------- 恢复码 ----------
   两个用途：注册/重设成功后一次性展示，以及恢复表单的标题。
   一次性卡片用金色左脊（内阴影画），与隐私条同一套语法 —— 它俩都是
   「这段话比周围更重要」，但都不该用边框把它围成一个弹窗。 */
.recovery-card {
  max-width: 420px;
  margin: 0 auto 20px;
  padding: 18px 20px 18px 21px;
  border-radius: var(--radius);
  background: var(--panel);
  border: 1px solid var(--line);
  box-shadow: inset 3px 0 0 0 rgba(163, 61, 46, 0.55);
}

.recovery-h {
  margin: 0 0 8px;
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
}

/* 恢复表单那张卡里的标题要留出下边距，和 auth-tabs 的位置对应 */
#recover-card .recovery-h { margin: 2px 0 10px; }

.recovery-code {
  margin: 12px 0 14px;
  padding: 13px 16px;
  border-radius: 8px;
  background: var(--panel2);
  border: 1px solid var(--line-strong);
  /* 等宽字体不是为了好看：这段码要一个字符一个字符地抄，
     比例字体会让 8 和 B、G 和 6 挤在一起看不出边界。 */
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 19px;
  line-height: 1.5;
  letter-spacing: 0.08em;
  color: var(--accent2);
  /* 点一下整段选中 —— 这段字唯一的用途就是被复制走 */
  user-select: all;
  /* 320px 下 16 位 + 3 个连字符仍可能超出，允许在连字符处断行 */
  overflow-wrap: anywhere;
}

.recovery-actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

/* ---------- 我的账号 ---------- */
.me-layout { display: grid; gap: 18px; align-items: start; }

@media (min-width: 900px) {
  .me-layout { grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); }
}

.me-card, .me-side, .me-timeline {
  padding: 18px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.me-side .btn { margin-bottom: 10px; }

.history-list { list-style: none; margin: 14px 0 0; padding: 0; }

.history-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 0;
  border-top: 1px solid var(--line);
}
.history-item:first-child { border-top: 0; }

.history-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.history-title { font-size: 14.5px; color: var(--text); }
.history-meta { font-size: 12px; color: var(--dim); font-variant-numeric: tabular-nums; }

.icon-btn {
  flex: 0 0 auto;
  width: 44px;                /* 44×44 触控目标 */
  height: 44px;
  border-radius: 10px;
  border: 1px solid var(--line-strong);
  background: var(--panel2);
  color: var(--text2);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}
.icon-btn:hover { color: var(--bad); border-color: rgba(248, 113, 113, 0.5); }
.icon-btn:focus-visible { outline: 2px solid var(--accent2); outline-offset: 2px; }

/* ---------- 批量表格 ---------- */
.batch-actions { display: flex; flex-wrap: wrap; gap: 10px; margin: 0 0 16px; }

.table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  /* 横向可滚时左右两侧出现渐隐阴影，滚到端点自动消失。
     动机是量出来的：390px 上 8 列只露出 5 列（文件/脸型/季型/Fitzpatrick/均匀度），
     色差 / 泛红 / 光泽 三列被裁在右边，而表格**没有任何可滚动的迹象** ——
     触摸设备不显示滚动条，用户不会知道右边还有东西。
 *
 * 原理是 `background-attachment: local/scroll` 的经典组合：
 *   - 前两层（local）是"遮盖"，跟着内容一起滚，停在端点时正好压住阴影；
 *   - 后两层（scroll）是阴影，固定在可视区边缘，只有内容还在滚时才露出来。
 * 于是不需要 JS、也不需要知道表格到底有没有溢出。 */
  background:
    linear-gradient(to right, var(--panel) 40%, rgba(22, 27, 37, 0)) left center / 32px 100% no-repeat local,
    linear-gradient(to left, var(--panel) 40%, rgba(22, 27, 37, 0)) right center / 32px 100% no-repeat local,
    linear-gradient(to right, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0)) left center / 22px 100% no-repeat scroll,
    linear-gradient(to left, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0)) right center / 22px 100% no-repeat scroll,
    var(--panel);
  /* 桌面端给一条细滚动条，让"还能滚"这件事在鼠标环境下也看得见 */
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
}

.batch-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  white-space: nowrap;
}

.batch-table th,
.batch-table td {
  padding: 10px 12px;
  text-align: left;
  border-bottom: 1px solid var(--line);
}

.batch-table thead th {
  position: sticky;
  top: 0;
  background: var(--panel2);
  color: var(--dim);
  font-weight: 500;
  font-size: 12px;
  letter-spacing: 0.03em;
}

.batch-table tbody tr:last-child td { border-bottom: 0; }
.batch-table td { color: var(--text2); font-variant-numeric: tabular-nums; }

.cell-file {
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--text);
}

.batch-table tr.is-bad td { color: #fecaca; }

.row-error { display: block; font-size: 11.5px; color: var(--bad); white-space: normal; }

.row-badge {
  display: inline-block;
  margin-left: 8px;
  padding: 1px 6px;
  border-radius: 999px;
  border: 1px solid rgba(251, 191, 36, 0.45);
  background: rgba(251, 191, 36, 0.12);
  color: var(--warn);
  font-size: 11px;
}

/* ---------- 试妆：无色的「不用」色块 ---------- */
.color-chip.is-none {
  /* 纯白化前这圈斜纹是 panel2 / bg2 两档交替。现在两者同色，斜纹整片消失，
     「不用」色块变成一块空白的圆角方块。改成「panel2 底 + line 色斜线」。 */
  background: repeating-linear-gradient(
    45deg,
    var(--panel2),
    var(--panel2) 6px,
    var(--line) 6px,
    var(--line) 12px
  );
  color: var(--dim);
  font-size: 13px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* ---------- 报告页「保存到账号」提示 ---------- */
.save-hint {
  margin: 10px 0 0;
  font-size: 12.5px;
  color: var(--dim);
}
.save-hint a { color: var(--accent2); }
.save-hint.is-ok { color: var(--good); }
.save-hint[hidden] { display: none; }

/* ---------- 手机上把顶栏的两个小目标补到 44px ---------- */
/* 量出来的：.brand 高 34px、.lang-select 高 40px。手指点 34px 的东西
   会经常点到旁边，而这两个恰好是「回首页」和「换语言」——
   点错的代价是跳到别的页面，不是重试一次就好。 */
@media (max-width: 899px) {
  .brand { min-height: 44px; }
  .lang-select { min-height: 44px; }
}

/* 【这里原来有一条 `@media (max-width: 899px) { .topnav a { padding: 0 8px } }`】
 *
 * 它是「6 个入口挤一行」时代的产物：内边距 14px 时一行要 397px、11px 时 381px，
 * 都超过可用的 350px，收到 8px 后总宽 345px 刚好一行放下。
 *
 * 那个目标已经不存在了 —— 现在手机上导航是 4 列网格 + 汉堡菜单
 * （见上方「手机顶栏」一节），入口从 6 个变成 8 个，靠「压内边距挤一行」
 * 根本不可能（8 × 44 = 352 已经超过 350）。留着它只会和网格里的
 * `padding: 0 4px` 打架，而且注释在讲一个不存在的前提。
 *
 * 教训同「为了修 A 而加的特例」：**前提消失后，规则和它的理由要一起删。**
 */

/* ---------- 发量 / 毛躁 ---------- */
.metric-rows { display: flex; flex-direction: column; gap: 10px; }

.metric-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
}

.metric-name { flex: 1; font-size: 13px; color: var(--text2); }
.metric-value { font-size: 15px; color: var(--text); font-variant-numeric: tabular-nums; }

.metric-level {
  padding: 1px 8px;
  border-radius: 999px;
  border: 1px solid var(--line);
  font-size: 11.5px;
  color: var(--dim);
}
.metric-row[data-level="high"] .metric-level { color: var(--warn); border-color: rgba(251, 191, 36, 0.45); }
.metric-row[data-level="mid"] .metric-level { color: var(--text2); }
.metric-row[data-level="low"] .metric-level { color: var(--good); border-color: rgba(74, 222, 128, 0.4); }

.metric-warn { margin: 0; font-size: 12px; line-height: 1.6; color: var(--warn); }

/* 缺口卡的引导句：说明「这一版补掉了哪些」，和下面的缺口列表分开。
 *
 * 原来是绿底绿框。绿色在本站的语义是**指标正常**（`metric-level[data-level=low]`），
 * 拿它画「已完成」的说明块，等于让读者把「通过」和「解释」读成同一类东西；
 * 而且同一页上「金色左脊的说明条」已经是一种固定语言（`.privacy-note`），
 * 出现第二种带色底的块会让人以为它俩在说不同性质的事。
 * 现在统一成同一种：中性底 + 金色左脊，含义由文字承担。 */
.gap-lead {
  margin: 0 0 14px;
  padding: 10px 12px 10px 13px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: var(--panel2);
  box-shadow: inset 3px 0 0 0 rgba(163, 61, 46, 0.55);
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--text2);
}

/* ---------- 肤色配色卡 ---------- */

.sh-head {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 4px;
}

.sh-skin { display: flex; align-items: center; gap: 10px; min-width: 0; }

.sh-skin-chip {
  width: 44px;
  height: 44px;
  border-radius: 12px;
  border: 1px solid var(--line);
  flex: none;
}

.sh-skin-label {
  font-size: 12px;
  color: var(--dim);
  line-height: 1.4;
}

.sh-skin-num {
  font-size: 13.5px;
  color: var(--text2);
  font-variant-numeric: tabular-nums;
  /* 原来这里是 white-space: nowrap，理由是「LAB 三个数在手机上不能换行换得七零八落」。
     但 1.4.4 实测：系统字号放大到 200% 时这一串宽 402px，在 390px 屏上把整页顶出去 97px。
     改成可换行，并把「L* 68.4」这类组内空格换成不换行空格（见 app.js）——
     组与组之间可以断行，组内不断，原意保住了。 */
  white-space: normal;
}

.sh-depth { text-align: right; }

.sh-depth-value { line-height: 1.25; }
.sh-depth-value b { font-size: 24px; font-weight: 700; color: var(--accent2); }
.sh-depth-value span { font-size: 12.5px; color: var(--dim); margin-left: 2px; }

/*
 * 8 档阶梯。每格是纯展示，不是按钮 —— 所以不套 44px 触控下限，
 * 8 格挤在 390px 里每格 40px 宽才是对的。
 */
.sh-ladder { display: flex; gap: 4px; margin: 16px 0 8px; }

.sh-step { flex: 1; min-width: 0; text-align: center; }

.sh-step i {
  display: block;
  height: 54px;
  border-radius: 8px;
  border: 1px solid var(--line);
}

/* 你的档：实线外框 + 提亮；相邻档：虚线，表示「也可以试」 */
.sh-step.is-mine i {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.sh-step.is-neighbor i { border-style: dashed; border-color: var(--dim); }

.sh-step em {
  display: block;
  font-style: normal;
  font-size: 11px;
  color: var(--dim);
  margin-top: 5px;
  font-variant-numeric: tabular-nums;
}

.sh-step.is-mine em { color: var(--text); font-weight: 600; }

.sh-hint {
  margin: 0 0 4px;
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--dim);
}

.sh-group { margin-top: 15px; }

.sh-chips { display: flex; flex-wrap: wrap; gap: 8px; }

.sh-chip {
  width: 44px;
  height: 44px;
  border-radius: 10px;
  border: 1px solid var(--line);
  flex: none;
}

.sh-actions { margin-top: 16px; }

@media (max-width: 420px) {
  /*
   * 手机上 8 格排一行，每格只有 34~40px —— 色块太窄，相邻档的深浅差看不出来，
   * 而「看得出深浅差」正是这张卡唯一的用处。
   * 折成两行 4 格，每格 80px 以上。代价是多占 60px 高度，值得。
   */
  .sh-ladder { flex-wrap: wrap; }
  .sh-step { flex: 1 0 22%; }
  .sh-depth { text-align: left; }
}

/* ---------- 404 ---------- */
/*
 * 404 页是服务端直接吐的一段 HTML（`seo.js` 的 `notFoundHtml()`），
 * 不加载任何 JS —— 死链页再去拉一堆脚本，出错时更容易连样式都拿不到。
 * 所以样式必须写在这里，不能靠 runtime 补。
 *
 * 版心比正文页窄：这里只有两句话和六个入口，铺满 1180px 会显得空。
 * 上边距用 vh 而不是固定值，让卡片落在视口视觉中心偏上（阅读起点位置）。
 */
.nf {
  max-width: 560px;
  margin: 0 auto;
  padding: 15vh 22px 64px;
  text-align: center;
}

.nf-brand {
  margin: 0 0 22px;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
}

.nf-title {
  margin: 0 0 14px;
  font-size: clamp(24px, 5vw, 32px);
  line-height: 1.25;
  letter-spacing: 0;
}

.nf-body {
  max-width: 430px;
  margin: 0 auto 30px;
  color: var(--text2);
  font-size: 14.5px;
  line-height: 1.75;
  text-wrap: pretty;
}

/* 六个入口排两列。手机上也保持两列 —— 单列会让「回到首页」这个动作
   被挤到很下面，而它是这页唯一的目的。 */
.nf-links {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}

.nf-links a {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: 0 12px;
  border-radius: 8px;
  border: 1px solid var(--line-strong);
  background: var(--panel);
  color: var(--text);
  font-size: 14px;
  text-decoration: none;
  transition: border-color 0.16s, background 0.16s, color 0.16s;
}

.nf-links a:hover {
  border-color: var(--accent);
  background: var(--panel2);
  color: var(--accent2);
}

/* 首页那个入口给主色，和顶栏的当前页标记同一套视觉语言 */
/* 入口个数是奇数时（现在是 7 个），最后一个在 2 列里会孤零零占半行 ——
   让它跨满整行，读作「列表收尾」而不是「排到一半断了」。
   偶数个时这条不命中，加减入口都不用回来改。 */
.nf-links a:last-child:nth-child(odd) {
  grid-column: 1 / -1;
}

.nf-links a:first-child {
  border-color: rgba(163, 61, 46, 0.45);
  background: rgba(163, 61, 46, 0.1);
  color: var(--accent2);
}

/* ==========================================================================
   AI 影棚（studio.html）
   ==========================================================================
   这一页与其余六个工具页的骨架不同：左栏是「选项 → 照片 → 参数 → 生成」
   一条向下的操作链，右栏是「结果 → 历史」。所以不用 .tool-layout
   （那是「画布 + 控制面板」），另起一套。

   视觉语言沿用「权威」那一轮定下的三条：
     · 圆角 10px（卡片），控件 6~8px，不用胶囊；
     · 金色只用于主行动与当前选中；
     · 分组之间用 hairline，不靠阴影堆层级。
*/

.studio,
.hs-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 18px;
  align-items: start;
  margin-top: 22px;
}

@media (min-width: 940px) {
  .studio,
  .hs-layout {
    /* 左栏固定 360：选项名最长的德文「Bild restaurieren」约 150px，
       加图标与内边距之后 360 刚好留一行余量。右栏吃掉剩下的。 */
    grid-template-columns: 360px minmax(0, 1fr);
    gap: 22px;
    align-items: stretch;
  }
}

.studio-side,
.studio-main {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
}

.studio-block {
  padding: 16px 18px 18px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--panel);
}

/* 左栏里最后一块（生成按钮）不要卡片底 —— 它本身就是一个按钮，
   再套一层框会读成「另一个选项」。 */
.studio-side > .studio-run {
  border: 0;
  background: none;
  padding: 0;
}

.studio-h {
  margin: 0 0 12px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--dim);
  /* 中文标题不加字距（见「视觉与排版约定」），只在拉丁语言下放宽 */
}

:lang(zh) .studio-h,
:lang(ja) .studio-h,
:lang(ko) .studio-h {
  letter-spacing: 0.02em;
}

/* ---------- 任务选项 ---------- */

.task-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/*
 * 选项用「原生 radio + label」而不是 role="radio" 的按钮组。
 * 理由不是省事：原生 radio 自带方向键切换、roving tabindex、
 * 读屏的「第 2 项，共 5 项」—— 手写这三个全部要自己实现，
 * 而漏掉方向键是最常见的（2.1.1 键盘可操作）。
 */
.task-opt {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 11px;
  padding: 11px 13px;
  border-radius: 8px;
  border: 1px solid var(--line-strong);
  background: var(--panel2);
  cursor: pointer;
  transition: border-color 0.16s, background 0.16s;
}

.task-opt:hover {
  border-color: var(--accent);
}

/* 选中态：金色左边脊 + 淡金底。左侧脊用 inset 阴影画，
   不用 border-left —— 后者会挤动内边距，让文字在选中时横移 2px。 */
.task-opt:has(input:checked) {
  border-color: rgba(163, 61, 46, 0.55);
  background: rgba(163, 61, 46, 0.08);
  box-shadow: inset 3px 0 0 0 var(--accent);
}

/* 焦点画在用户看得见的框上（.sr-radio 只有 1px）。 */
.task-opt:focus-within {
  outline: 2px solid var(--accent2);
  outline-offset: 2px;
}

/* 视觉隐藏但**可聚焦**。不能用 display:none / visibility:hidden ——
   那样键盘与读屏都拿不到这个控件（2.1.1 / 4.1.2）。
 *
 * 写法必须与 `.sr-file` 完全一致（1px + clip-path: inset(50%)），
 * 不能图省事写 `opacity: 0`：
 *   ① 全站已经有了一种「视觉隐藏但仍可聚焦」的约定，出现第二种，
 *      下一个人就不知道该抄哪个；
 *   ② 版面量具（audit-layout）正是按「≤2px 且 clip-path 不是 none」
 *      来把这类控件从「小目标」里排除的。用 opacity 写的版本会被判成
 *      9 个小目标 —— 实测 studio 页 5 个断点全部不合格。
 *      **换一种隐藏写法 = 换掉量具认得的指纹。** */
.sr-radio {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.task-opt-body {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}

.task-opt-name {
  font-size: 14.5px;
  font-weight: 500;
  color: var(--text);
}

.task-opt-desc {
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--dim);
}

/* ---------- 参数 ---------- */

.studio-opts {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.opt-label {
  margin: 0 0 7px;
  font-size: 12.5px;
  color: var(--text2);
}

.opt-row {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
}

.opt-chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 0 13px;
  border-radius: 7px;
  border: 1px solid var(--line-strong);
  background: var(--panel2);
  color: var(--text2);
  font-size: 13px;
  cursor: pointer;
  transition: border-color 0.16s, background 0.16s, color 0.16s;
}

.opt-chip:hover {
  border-color: var(--accent);
  color: var(--text);
}

.opt-chip:has(input:checked) {
  border-color: rgba(163, 61, 46, 0.55);
  background: rgba(163, 61, 46, 0.12);
  color: var(--accent2);
}

.opt-chip:focus-within {
  outline: 2px solid var(--accent2);
  outline-offset: 2px;
}

/* 窄屏上选项 chip 抬到 44px。
   34px 是鼠标时代的尺寸（指针精度高），手指按不准。
   桌面保持紧凑 —— 这一页左栏有 3 组参数共 11 个 chip，
   全部抬到 44 会让左栏凭空长高 110px。
 *
 * **用宽度断点，不用 `pointer: coarse`。** 后者看起来更"正确"，
 * 但它依赖浏览器真的上报触摸能力：无头 Chrome 的 mobile 模拟下
 * 这条媒体查询不命中，量具量到的仍是 34px —— 而真机上它命中。
 * 结果就是「量具全绿、手机偏小」这种最难发现的分歧。
 * 宽度断点是两边都能算的同一个数。 */
@media (max-width: 899px) {
  /* 【宽度也要 44】韩语把「卷 / 盘」译成单音节 `컬` / `번`，一个字 + 左右 13px
     内边距 = 41px，比手机档的 44 差 3px。中文「卷 / 盘」46px、英文 `Curl` /
     `Bun` 也过线 —— 所以一直全绿，只有韩语红。
     2026-11-04 第二次补：上一轮修过，被 `ctrl.py restore` 用旧备份吞掉了。 */
  .opt-chip { min-height: 44px; min-width: 44px; }
}

/* ---------- 照片 ---------- */

/* 影棚与换发型的上传框同样矮：这两页的重点在右栏（结果 / 发型），
   上传框只是「输入源」。96px 够放图标 + 两行字。
   `.dropzone--hs` 挂在这里而不是另写一条 —— 两页的输入区在版面上是同一个角色。 */
.dropzone--studio,
.dropzone--hs {
  padding: 16px 18px;
  column-gap: 14px;
}

/* 第二个上传位（真试衣的「衣服照」）。
   与第一个之间必须留一道间距：两个紧贴的框会被读成「一个分成上下两半的框」，
   用户不知道这里要两张图，会往第一格里塞两张、然后奇怪为什么只收到一张。 */
.dropzone--second {
  margin-top: 12px;
}

.studio-preview {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 12px;
  padding: 10px;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: var(--panel2);
}

.studio-preview img {
  width: 56px;
  height: 56px;
  object-fit: cover;
  border-radius: 6px;
  flex: 0 0 auto;
  background: var(--bg);
}

.studio-preview-meta {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}

.studio-preview-name {
  font-size: 12.5px;
  color: var(--text2);
  /* 文件名可能极长。截断放在 CSS 里做，不截字符串 ——
     截字符串会把扩展名切掉，用户就看不出自己传的是什么格式。 */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------- 生成按钮与配额 ---------- */

.studio-run {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.btn-wide {
  width: 100%;
  flex: 0 0 auto;
}

/* 「次要动作」变体：只收窄横向内边距与字号，**高度必须仍有 44px**。
   下载 / 删除这两个按钮在结果卡的操作行里，触摸设备上照样要按得准。 */
.btn-sm {
  min-width: 0;
  min-height: 44px;
  padding: 0 14px;
  font-size: 13px;
  border-radius: 7px;
}

/* ---------- 状态消息 ---------- */

.studio-msg {
  margin: 10px 0 0;
  font-size: 13px;
  line-height: 1.6;
  color: var(--text2);
  /* 空消息不占高度：它一直存在（aria-live 容器），
     但没内容时不该在版面上留一条 20px 的空白。 */
}

.studio-msg:empty {
  margin: 0;
}

.studio-msg[data-kind="err"] {
  color: var(--bad);
}

/* 提示条里的按钮（「去登录」）自成一行。不写 margin 会紧贴上面那行字 ——
   行高只有 1.6，按钮又是 inline-flex，视觉上像贴在文字底部。 */
.studio-msg .btn {
  margin-top: 10px;
  min-width: 0;
}

.studio-msg[data-kind="wait"] {
  color: var(--text2);
}

/* 「需要登录」这类**说明**，不是错误。
   原来复用 `err` 的红色，截图里那行红字看起来像「刚才失败了」——
   而用户其实什么都没做错，页面本来就要先登录。
   用项目里说明块的统一做法：中性底 + 金色左脊（内阴影画，不用 border-left，
   border 会挤动内边距、影响换行位置）。 */
.studio-msg[data-kind="note"] {
  color: var(--text2);
  margin: 10px 0 0;
  padding: 14px 16px 14px 17px;
  border-radius: var(--radius);
  background: var(--panel);
  box-shadow: inset 3px 0 0 0 rgba(163, 61, 46, 0.55);
}

/* ---------- 结果 ---------- */

/* 结果卡吃掉右栏的剩余高度。
 *
 * 左栏是「5 个任务 + 照片 + 参数 + 生成」，约 780px；右栏原本只有
 * 一个 220px 的结果卡和一个历史卡，加起来 470px —— 右栏下面空着 300px，
 * 读起来像「还没加载完」。这和工具页「上传框 1044 宽却只有一行字」是同一类问题：
 * 容器比内容大得多的时候，空白不是呼吸，是缺口。
 *
 * 让结果卡 flex: 1 之后，它自己变成一块画布区，空态在中间 ——
 * 有结果时图也居中，不再顶着上边缘。 */
.studio-main > .studio-block:first-child {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

.studio-out {
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-height: 220px;
  flex: 1 1 auto;
  /* 空态与「需要登录」都居中：它们在卡片正中间读起来是「等待中」，
     贴在顶部读起来像「内容没加载出来」。有结果时这条不影响
     —— 图片自己占满宽度。 */
  justify-content: center;
}

.studio-empty {
  margin: 0;
  padding: 26px 12px;
  text-align: center;
  font-size: 13px;
  color: var(--dim);
  /* 「生成过的结果会出现在这里」在 190px 宽的网格格里会折成
     「…出现在这」+「里」。`balance` 只治拉丁文，中文这里靠留白
     让它自然断在两段之间 —— 加 max-width 把断点推到「结果」后面。 */
  text-wrap: balance;
}

.studio-fig {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.studio-media {
  width: 100%;
  height: auto;
  max-height: 62vh;
  object-fit: contain;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: var(--bg);
  display: block;
}

.studio-cap {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

/* 演示模式提示：必须在图上，不能只写在页面底部 ——
   用户看着一张「没变化」的图，会以为生成失败或模型很差。 */
.studio-mock {
  margin: 0;
  padding: 9px 11px;
  border-radius: 7px;
  border-left: 3px solid var(--warn);
  background: rgba(251, 191, 36, 0.07);
  font-size: 12px;
  line-height: 1.6;
  color: var(--text2);
  /* 文案里带一个换行，这里保留它 —— 让「这是什么」与「怎么接真模型」各占一行。
     不这么做的话，整句在 624px 下换行，末行只剩「果。」两个字（量具实测占 5% 行宽）。
     中文孤行只能靠改文案或调宽度治：`text-wrap: balance` 会切断中文词
     （「均匀」→「均/匀」），`pretty` 对中文完全无效。 */
  white-space: pre-line;
}

/* ---------- 历史 ---------- */

.studio-history {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 8px;
}

.hist-item {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: var(--panel2);
  color: var(--text);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  min-height: 52px;
  transition: border-color 0.16s, background 0.16s;
}

.hist-item:hover {
  border-color: var(--accent);
  background: var(--panel2);
}

.hist-thumb {
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  border-radius: 5px;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  color: var(--dim);
  overflow: hidden;
}

.hist-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.hist-meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.hist-name {
  font-size: 12.5px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.hist-when {
  font-size: 11px;
  color: var(--dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---------- 页脚说明 ---------- */

.studio-foot {
  margin: 18px 0 0;
  padding-top: 13px;
  border-top: 1px solid var(--line);
  font-size: 12px;
  line-height: 1.7;
  color: var(--dim);
}

/* 上传型徽章：形状与其他页一致（胶囊 = 标签），只换颜色语义。
   金色边框表示「这一页会把照片发出去」，不是绿色（绿色在本站专指指标正常）。 */
.privacy-badge--upload {
  border-color: rgba(163, 61, 46, 0.45);
  background: rgba(163, 61, 46, 0.09);
  color: var(--accent2);
}

/* 手机上右栏在前 —— 用户传完图最想看的是结果，不是再往下滚过一整列参数。
   左栏（选项 / 照片）在后。用 order 而不是改 DOM 顺序：
   DOM 顺序决定读屏与 Tab 的先后，而「先选任务再传图」才是操作的因果顺序，
   视觉顺序可以为了「先看到结果」而不同。 */
@media (max-width: 939px) {
  .studio-main { order: -1; }
}

/* ==========================================================================
   对比页（compare.html）
   ==========================================================================
   这一页的视觉任务只有一条：**让「A | B」这个并置关系在 390px 上也成立**。
   用户是带着「两张里挑一张」的问题来的，如果两个槽位在手机上被堆成上下两行，
   他就得来回滚动才能回忆「上面那张长什么样」—— 那一瞬间判断就退化成猜测。

   所以槽位永远是两列，窄屏只缩尺寸、不换行。代价是每格约 165px，
   提示文字会折成两行；可以接受，因为信息没丢。
   表格是另一回事：四列在 390px 上每列不到 90px，所以那里**必须**改成堆叠，
   见文件末尾的窄屏块。
   ========================================================================== */

.cmp-sub {
  margin: 0;
  color: var(--text2);
  font-size: 15px;
  line-height: 1.6;
  max-width: 52ch;
}

/* ---------- 两个槽位 ---------- */
.cmp-slots {
  display: grid;
  /* 两列等宽，`minmax(0, 1fr)` 而不是 `1fr` —— 后者的最小尺寸是 auto，
     里面那张缩略图一宽，列就会被撑破，整个页面横向溢出。
     本仓库在 `.tool-layout` 上踩过同一个坑。 */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 12px;
  margin-top: 20px;
}

/* 覆盖 `.dropzone` 的横排布局：这里是竖排、居中、上方留一块正方形给缩略图。
   必须把三个子元素的 `grid-column: 2` 一起打回第 1 列 —— 只改容器的话，
   它们会被放进隐式创建的第二列，整个槽位的文字挤到右半边。 */
.cmp-slot {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: 1fr auto auto;
  justify-items: center;
  align-items: center;
  align-content: center;
  row-gap: 7px;
  column-gap: 0;
  padding: 16px 14px;
  min-height: 196px;
  text-align: center;
}
.cmp-slot > .dz-icon,
.cmp-slot > .dz-main,
.cmp-slot > .dz-hint {
  grid-column: 1;
  grid-row: auto;
}

/* 缩略图。上限 200px：再大就变成「一张照片 + 一行字」，
   两个槽位会撑满整个首屏，而这一页的首屏要同时露出标题和按钮。 */
.cmp-thumb {
  grid-column: 1;
  grid-row: 1;
  width: 100%;
  max-width: 200px;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 6px;
  border: 1px solid var(--line);
  background: var(--panel2);
}

/* A / B 角标。用绝对定位是为了不占 grid 的行 —— 占一行的话，
   有缩略图和没缩略图时文字会差一行高度，两次点击之间整块会跳。 */
.cmp-tag {
  position: absolute;
  top: 9px;
  left: 9px;
  padding: 0 6px;
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  background: var(--panel);
  color: var(--text2);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.6;
}

/* 已选：缩略图有了，边框换成金色，和首页上传框「已就绪」的语言一致 */
.cmp-slot.is-filled {
  border-color: var(--accent);
}

/* ---------- 按钮行 ---------- */
.cmp-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 16px;
  margin-top: 18px;
}
/* `.btn` 自带 `flex: 1`，那是给「两三个按钮并排铺满」的场景。
   这里只有一个主按钮 + 一行状态字，铺满 724px 会读作「整页只有一个操作」。 */
.cmp-actions .btn {
  flex: 0 0 auto;
  min-width: 176px;
}

.cmp-status {
  margin: 0;
  font-size: 14px;
  line-height: 1.5;
  color: var(--dim);
}
/* 报错只换颜色、不换角色：它和提示走同一个 `role="status"` 节点。
   改角色（换成 alert）会让同一条消息被念两遍，而切语言时 `apply()`
   会重画整块，每切一次就重念一次。 */
.cmp-status.is-error { color: var(--bad); }

/* ---------- 加载 ---------- */
.cmp-loading {
  padding: 72px 0;
  text-align: center;
}

/* ---------- 结果 ---------- */
.cmp-h2 {
  font-size: 20px;
  margin: 6px 0 14px;
}

/* 结论块。这是整页唯一一个「先读它，不读它也白来」的元素，
   所以给它左侧一道金线 + 卡片底，和报告页的 `.result-head` 同一套语法。
   第一句 600 字重，后面两组推荐降一档 —— 用户扫一眼只看到第一句，
   正好是「用 A」那一句。 */
.cmp-verdict {
  margin: 0 0 22px;
  padding: 15px 18px;
  border-left: 3px solid var(--accent);
  border-radius: 0 var(--radius) var(--radius) 0;
  background: var(--panel);
  color: var(--text);
  font-size: 16.5px;
  font-weight: 600;
  line-height: 1.6;
}
.cmp-verdict-extra {
  display: block;
  margin-top: 9px;
  font-size: 14.5px;
  font-weight: 400;
  color: var(--text2);
}

.cmp-pair {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 12px;
  margin: 0 0 24px;
}
.cmp-face { margin: 0; }
.cmp-face img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--panel2);
}
.cmp-face figcaption {
  padding-top: 7px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--dim);
}
.cmp-face figcaption b {
  color: var(--accent2);
  font-weight: 600;
}

/* ---------- 逐项表 ---------- */
.cmp-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
}
.cmp-table caption {
  padding-bottom: 10px;
  text-align: left;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--dim);
}
.cmp-table thead th {
  padding: 0 8px 8px;
  text-align: left;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--dim);
  border-bottom: 1px solid var(--line-strong);
}
.cmp-table tbody th,
.cmp-table tbody td {
  padding: 11px 8px;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--line);
}
.cmp-table tbody th {
  font-weight: 500;
  color: var(--text2);
  white-space: nowrap;
}
.cmp-num {
  font-variant-numeric: tabular-nums;
  color: var(--text);
  white-space: nowrap;
}
/* 赢的那一格。金色在本站专指「这里值得看」，绿色只留给「指标正常」，
   所以这里不用绿。非颜色的线索由 `.cmp-sr` 和结论句承担（1.4.1）。 */
.cmp-num.is-win {
  color: var(--accent2);
  font-weight: 600;
}
.cmp-means {
  color: var(--dim);
  font-size: 13px;
  line-height: 1.65;
}

/* 只给读屏看的词（「更强」）。写法和 `.a11y-live` / `.sr-file` 完全一致：
   1px + clip-path。不能用 display:none / visibility:hidden —— 那会把它
   一起摘出无障碍树，等于没写。 */
.cmp-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- 窄屏：表格改堆叠 ----------
 * 四列在 390px 上每列不到 90px，「这意味着什么」那一列会变成
 * 一行四个字的竖条。表头隐藏、每行改成一个块，数值前面用 `data-label`
 * 补上 A / B —— 否则两个数字并排，用户分不清哪个是哪个。
 *
 * `data-label` 由 compare.js 的 `cell()` 写入，是字面量 "A" / "B"，
 * 不参与翻译（两个字母在所有语言里都是这两个字母）。
 */
@media (max-width: 620px) {
  .cmp-table thead { display: none; }
  .cmp-table tbody tr {
    display: block;
    padding: 12px 0;
    border-bottom: 1px solid var(--line);
  }
  .cmp-table tbody th,
  .cmp-table tbody td {
    display: inline-block;
    padding: 0;
    border: 0;
  }
  .cmp-table tbody th {
    display: block;
    margin-bottom: 5px;
    font-weight: 600;
    color: var(--text);
    white-space: normal;
  }
  .cmp-num { margin-right: 16px; }
  .cmp-num::before {
    content: attr(data-label) " ";
    color: var(--dim);
    font-weight: 400;
  }
  .cmp-means {
    display: block;
    margin-top: 6px;
    font-size: 12.5px;
  }
}

/* 390px 上两个槽位各约 165px，提示文字在 390 折 2 行、在 320 折 3 行。
   **这里故意不给 `.dz-hint` 降字号。** 曾经写过 `font-size: 11px`，理由
   是「折到 3 行时两个槽位高度会不一致」—— 实测不成立：`.cmp-slots` 是两列
   grid，默认 `align-items: stretch`，同一行两格永远等高，跟内容折几行无关
   （320/390 × 空槽位/已选图 四种组合，两个槽位高度逐个相等）。
   代价只是 320px 选完图后槽位从 213px 变 242px；收益是这一页回到基础 13px。
   别再降回去：这是审计报的唯一一处 <12px **正文提示**（其余 11px 都在
   伪元素编号 / 等宽小字 / 徽章上，不产生文本节点或不在审计状态里），
   而它在入口页。 */
@media (max-width: 460px) {
  .cmp-slot { min-height: 176px; padding: 14px 10px; }
  .cmp-slot > .dz-main { font-size: 13px; }
  .cmp-verdict { font-size: 15.5px; padding: 13px 14px; }
  /* 14.5 → 14：390px 下这 45 个字（去空白）在 331px 里折成 3 行，
     末行只剩「好。」；降半档变 2 行、末行 22 字。实测见
     .tmp-test/sweep-verdict.mjs。 */
  .cmp-verdict-extra { font-size: 14px; }
  .cmp-actions .btn { min-width: 0; flex: 1 1 auto; }
}

/* ============================================================================
   印刷品视觉层（2026-10-25 重新设计）
   ----------------------------------------------------------------------------
   诊断：上一版的首页问题不在配色，在**语法**。
   深底 + 金色 + 圆角卡片 + 01-04 编号方块 + 三步大数字 + 能力清单 + FAQ ——
   这套骨架在 2024~2025 年的 AI 产品站上几乎逐条对应。它的潜台词是
   「我们在展示一个智能产品」，而不是「这是一份关于你的测量结果」。

   这一层按「改完之后读起来像什么」排：
     1. 纸与墨。深底换米白纸，金色换朱红（印刷品里批注的颜色）。
     2. 衬线标题 + 无衬线正文。印刷品的标准分工，只用系统字体。
     3. 直角与细线。区块之间用 1px 横线分隔，像书页上的栏目。
     4. 目录取代四宫格。9 行「名称 · 说明 · 示例值」，假设读者会挑。
     5. 流程压成一行三栏，编号从 38px 降到 11px，它不再是版面主角。
   ============================================================================ */

/* ---------- 1. 字体分工 ---------- */
.hero h1,
/* 工具页的标题走另一套容器：影棚用 .hero-tool，对比页用 .tool-head。
   漏掉后者时，对比页的「哪张更好？」是全站唯一一个黑体大标题 ——
   在同一屏里和页脚的衬线品牌名打架。 */
.hero-tool h1,
.tool-head h1,
.tool-head h2,
.cap-h,
.how-h,
.faq-h,
.footer-h,
.brand {
  font-family: var(--serif);
  font-weight: 600;
}

.hero h1 {
  font-size: clamp(30px, 6.4vw, 42px);
  line-height: 1.2;
  margin: 0 0 16px;
}

.footer-h {
  font-size: 12.5px;
  letter-spacing: 0.08em;
}

/* 字距只给首屏副标题。
   我原本也给正文段落加了 0.01em，理由是「印刷品会给一点侧空」——
   但中文本来就不缺侧空，而每字多 0.13px 会让一行少放一两个字，
   直接制造 2 字孤行（check-orphan 量到 f1.p「案。」/ how.s2b「备。」/ 
   hair.cap.d4「见。」三处）。已撤回。 */
.hero-sub {
  letter-spacing: 0.01em;
}

/* 目录行与流程说明用 pretty 断行。
   这两类段落都是 2~3 行的短块，末行只剩两个字的情况反复出现
   （check-orphan 量到 how.s2b「备。」、cap.d5「号。」）。
   不用 balance：原文件里已有记录，balance 在中文里会在词中断
   （「光线均 / 匀」），比孤行更难读。pretty 只调末行，不动前面。 */
.cap-item p,
.how-step p,
.faq-item p {
  text-wrap: pretty;
}

/* ---------- 2. 顶栏 ---------- */
.topbar {
  background: var(--bg);
  backdrop-filter: none;
  border-bottom: 1px solid var(--line);
  padding: 15px 20px;
}

/* 顶栏那个「照片不上传」原来是带边框的胶囊。
   胶囊是「卖点罗列」的语法：视觉上像按钮，读起来像标签云。
   印刷品把同一句话写成小字，不喊，但它还在。 */
.privacy-badge {
  border: 0;
  border-radius: 0;
  padding: 0;
  background: none;
  box-shadow: none;
  color: var(--dim);
  font-size: 12.5px;
}

.badge-lock { color: var(--accent); }

/* ---------- 顶栏品牌位：从标题位挪到「人脸报告」左边 ----------
 *
 * 原来 ◐FaceLab 是顶栏左侧一个独立链接，和导航之间隔着一段空白 ——
 * 读起来像「网站名 + 工具列表」两段。挪进导航首位之后，
 * 品牌成了目录的第一行，和它右边那 8 个工具是同一种东西：
 * 一行行的入口。这是印刷品里「书名页与目录同栏」的做法。
 *
 * 三件事必须一起做，少一件就会读成「9 个一模一样的按钮」：
 *   1. 字号大一点、用衬线。它是书名，不是条目。
 *   2. 不参与 `[aria-current]` 的下划线：品牌不是「当前在哪一页」，
 *      它是首页入口。首页上它确实带 aria-current，但那时下划线
 *      是一条 2px 朱红脊，和导航项一致 —— 见下面 :not() 那条的说明。
 *   3. 右侧一条竖线，把「书」和「目录」分开。用 border-right 而不是
 *      margin，因为它要跟着文字高度走。
 */
.topnav .brand {
  /* 继承 .brand 的 flex 与 gap，只覆盖需要变的部分 */
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.01em;
  /* 竖线：与相邻链接的 padding 拉开距离，不贴着字 */
  padding: 0 16px 0 14px;
  margin-right: 6px;
  border-right: 1px solid var(--line-strong);
  color: var(--text);
  min-height: 36px;
}

.topnav .brand .logo {
  font-size: 17px;
  /* 朱红只在图形上，字是墨色 —— 和页脚品牌名同一个关系 */
  line-height: 1;
}

/* 首页时 brand 会吃到 `.topnav a[aria-current="page"]` 的下划线吗？
   **不会，而且不该会。** brand 指向 `/`，它只在首页才是「当前页」，
   但那时真正要标出当前位置的是导航里的「人脸报告」。
   所以每页的 `aria-current` 都留在对应导航项上，brand 一律不带 ——
   本来给 brand 写过一条 `[aria-current="page"]` 的下划线规则，
   既撞上「brand 不该有标记」的语义，也把竖线压掉一截，已删除。 */

/* 手机上 brand 也进了汉堡菜单。菜单是 4 列网格，品牌单独占一整行，
   否则它会被挤进一格、竖线卡在中间很难看。 */
@media (max-width: 899px) {
  /* ⚠️ 收起时品牌必须还在顶栏上。
     品牌挪进 `.topnav` 之后，手机上 `.topnav { display: none }` 会**连品牌一起收掉** ——
     实测 390px 收起态截图里左边一片空白，只剩汉堡和语言框，
     连「这是哪个站」都没有了。品牌不是一个菜单项，它是顶栏的一部分。
     做法：收起时把 brand 用 `display: contents` 的容器让它脱离 nav 的 hidden，
     太绕 —— 直接给 brand 单独一条 `position: absolute` 拉回顶栏左端更省事，
     但那样它和 nav 的 Tab 顺序就对不上了。

     更干净的办法：nav 收起时用 `visibility` 不行（还会被 Tab 到），
     所以改成 **nav 收起时仍占位、但只显示 brand 这一项**，
     其余链接用 `display: none`。这样 brand 在文档流里、Tab 顺序不变、
     顶栏左端有品牌，而 8 个入口照旧收进菜单。 */
  .topnav {
    display: flex;
    position: static;
    width: auto;
    flex: 0 0 auto;
    padding: 0;
    background: none;
    gap: 0;
  }
  .topnav:not(.is-open) > a:not(.brand) { display: none; }
  .topnav .brand {
    border-right: 0;
    padding: 0;
    margin: 0;
    min-height: 44px;
    font-size: 15px;
  }

  /* 展开态：整块变成下拉面板，品牌占满第一行。
     **只补品牌那几条，不要重复整段 `.topnav.is-open`。**
     我写过一版把 `display/grid/position/left/right/background` 又抄了一遍，
     结果 `left: 0; right: 0` 相对的是 `.topbar`（它有 14px 内边距），
     面板左边露出 40px 正文 —— 截图里「你的脸在替你说话」从菜单左边透出来。
     原有那段（第 2969 行附近）已经写对了，这里只加差异。 */
  .topnav.is-open > a:not(.brand) { display: inline-flex; }
  .topnav.is-open .brand {
    grid-column: 1 / -1;
    justify-content: flex-start;
    border-bottom: 1px solid var(--line);
    padding: 0 6px 6px;
    margin: 0 0 4px;
    font-size: 16px;
  }

  /* 顶栏网格：品牌 / 汉堡 / 语言。
     `.topnav` 此时扮演的是「品牌容器」，必须显式钉在第 1 行第 1 列 ——
     不钉的话它作为一个 grid item 会被拉成整行，把汉堡挤到下一行
     （实测顶栏从 85px 变成 139px，品牌掉到第二行、汉堡孤立在第一行中间）。

     ⚠️ 这一段按值来看与上面 2952 行那条**完全重复**（同一份三格定义写了两遍）。
     但**改一份没用** —— 2026-10-31 我先只改了上面那份，probe-brand 的
     320px 读数一个像素都没动（品牌右 111 / 汉堡左 89，重叠 22px），
     因为同特异度下**后声明的赢**，生效的是这里。
     现在两份都改成 `minmax(auto, 1fr)`：保留重复（删掉会动到别处的排版注释），
     但至少在值上一致，下一次改不会又踩「改的是一份死代码」。 */
  .topbar {
    grid-template-columns: minmax(auto, 1fr) auto auto;
    grid-template-rows: auto;
  }
  .topnav {
    grid-column: 1;
    grid-row: 1;
    align-self: center;
  }
  /* 展开时不要写 grid-column / grid-row：它是 `position: absolute`，
     已经在网格流之外。写了反而会把它缩回家的首列宽度 ——
     实测面板左边露出 40px 正文（「你的脸在替你说话」透出来）。

     `left: 0` 相对的是包含块的**内边距框**，而包含块是 `.topbar`
     （`position: sticky`，左右各有 20px 内边距）——
     实测面板 `left=20`、`width=370`，右边贴屏、左边空 20px。
     所以只往左拉 20px、宽度算到包含块的 padding box 宽：
     不能同时写 `margin-right: -20px`，那会在 `right: 0` 之上再扣一次，
     实测面板右沿跑到 410，溢出视口 20px。 */
  .topnav.is-open {
    margin-left: -20px;
    margin-right: 0;
    width: calc(100% + 20px);
  }
  .nav-toggle { grid-column: 2; grid-row: 1; display: inline-flex; }
  .topbar-right { grid-column: 3; grid-row: 1; }
}

/* ---------- 3. 首屏 ---------- */
.hero { padding: 52px 0 22px; }

.hero-sub { max-width: 54ch; }

/* 首屏那两条「徽章」降级成一行脚注，用 · 分开 */
.hero-chips {
  margin: 16px 0 0;
  gap: 0;
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--dim);
}

.hero-chips li {
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  color: inherit;
  font-size: inherit;
  line-height: inherit;
}

.hero-chips li + li::before {
  content: "·";
  margin: 0 9px;
  color: var(--line-strong);
}

/* 出处行：等宽小字，像书里的版权页 */
.hero-cite {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11px;
  letter-spacing: 0.02em;
  color: var(--dim);
}

/* ---------- 4. 上传区 ---------- */
.dropzone {
  border-radius: 0;
  border: 1px solid var(--line-strong);
  background: transparent;
  padding: 18px;
  box-shadow: none;
  /* 与左栏文字同宽，不横贯整页。
     全宽时它从标题左边一直伸到示意图底下，读作一条「带子」；
     收窄到 560px 之后它落在左栏里，读作表单的一个输入行。
     拖拽目标仍然够大（560×86），手机上是满宽。 */
  max-width: 560px;
}

/* 取景角标去掉：它是装饰，这一版靠线宽与留白分区，不靠角标 */
.dropzone::after { content: none; display: none; }

/* 加号从「44px 方形按钮」降成字符本身。它是个标记，不是按钮 ——
   整块区域都可点，再画一个按钮形状只会让人以为要点那个小方块。 */
.dz-icon {
  width: 26px;
  height: 26px;
  border: 0;
  border-radius: 0;
  font-family: var(--serif);
  font-size: 22px;
  font-weight: 400;
  color: var(--accent);
}

.dropzone:hover .dz-icon,
.dropzone.dragover .dz-icon,
.dropzone:focus-within .dz-icon {
  background: none;
  border-color: transparent;
  color: var(--accent2);
}

.dz-main { font-size: 15px; font-weight: 600; }

/* 隐私条：从卡片改成带左脊的一段话。左脊用 border-left ——
   内阴影那版是为了在深底上不发灰，纸底不需要。 */
.privacy-note {
  margin-top: 14px;
  padding: 0 0 0 14px;
  border: 0;
  border-left: 2px solid var(--accent);
  border-radius: 0;
  background: none;
  box-shadow: none;
  font-size: 13px;
  line-height: 1.7;
  color: var(--dim);
}

.privacy-note strong {
  display: block;
  color: var(--text);
  font-weight: 600;
}

/* ---------- 5. 报告目录：9 行，横线分隔 ---------- */
/* 间距与分隔线见「5.1 章节眉标」。这里只管目录自己的排版。 */
.cap-sub {
  margin: 0 0 18px;
  max-width: 62ch;
  font-size: 12.5px;
  color: var(--dim);
}

/* 【2026-10-29 版式重构】目录改成两栏网格：左边「编号 + 名称」，右边「示例值」。
 *
 * 原来 9 行每行压一条满宽横线，是「规格表」的语法 —— 而下面「三步」和「FAQ」
 * 也各自铺线，三块叠在一起读，整页就是一条接一条的横线，像 AE 模板套了三遍。
 * 现在目录**一条线都不用**：靠 2 列 × 行的留白分组，编号（01..09）承担定位，
 * 示例值移到右列做成对齐的数字列。它读起来像书的目录页，不像表格。
 */
.cap-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 40px;
  margin: 0;
  padding: 0;
}

.cap-item {
  display: grid;
  grid-template-columns: 30px minmax(0, 1fr);
  column-gap: 0;
  padding: 14px 0;
  border-top: 0;
}

/* 【2026-11-01】这行是「目录用横线分隔」时代的遗物：最后一条压掉下边线，
   免得表格在底部多一根散线。套框之后它等于**把第 9 个卡片的下边框抠掉** ——
   实测 6 组宽度里每组都有「1/9 条没套上框」，红的就是它。
   现在所有条目都有整圈边框，这行必须删。 */

.cap-item::before { content: none; }

/* 编号退成图注：等宽、--dim、不抢标题。它只服务检索，不服务气势。 */
.cap-no {
  grid-column: 1;
  grid-row: 1 / span 2;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11px;
  line-height: 1.9;
  letter-spacing: 0.06em;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}

.cap-item h3 {
  grid-column: 2;
  grid-row: 1;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  font-size: 14.5px;
  font-weight: 600;
  margin: 0 0 4px;
}

/* 说明吃满第二列。它是这张「目录」的一行，行宽由列决定。 */
.cap-item p {
  grid-column: 2;
  grid-row: 2;
  margin: 0;
  font-size: 13px;
  line-height: 1.65;
}

/* 示例值保持等宽右对齐 —— 它是这张「目录」的数值列 */
.cap-item h3::after { color: var(--dim); }

/* 【2026-11-01 用户要求：九项也套框，跟首页四条一样】
 * 用户原话「都弄成这样的带框子的不好？」，并选了「同一个东西套满框」+「全站统一」。
 *
 * 上一版这里刻意做到「零横线、像书的目录」，理由写在 5146 行附近：
 * 每块都铺线会整页像 AE 模板。但用户要的是**可数的块**，不是「像书」。
 * 两者不冲突：书的目录也可以是一页卡片索引。所以改框，不改编号与右列数值 ——
 * 那两样是这一块的信息骨架（01..09 定位、示例值右对齐成数字列）。
 *
 * 2 列而不是 3 列：卡片要放「编号 + 标题 + 示例值 + 两行说明」，
 * 3 列在 760px 主栏下每格只有 210px，说明折成四行（和 1654 行那条教训同源）。 */
.cap-list {
  gap: 12px;
  /* 【同排等高】默认 grid 会把每张卡都拉到本行最高那张的高度 —— 1280 下实测
     确实做到了 108/108，但**只有一列**（760 / 390）时没有「本行」，每张卡按自己的
     内容定高，于是 104/104/… 一致、而 1280 下出现 86 与 108 并存时就不成立。
     真正的问题是：卡片内部是「标题行 + 说明行」两行网格，一张说明两行、
     一张一行，下面就各留各的白，看起来是「框一样高、字没对齐」。
     解法：卡片自己声明 subgrid，让它那两行**直接落到 .cap-list 的行轨道上**，
     标题、说明天然对齐，高度也由内容决定而不是被拉长。 */
  /* 【为什么要写 span 2】只说 `subgrid` 不够：`.cap-item` 内部是
     「编号列 + 标题行 + 说明行」两行，但浏览器不会自己知道它要占两行 ——
     1280 下实测被自动排成 3 行（编号跨行、标题各占一行），同排卡片高度
     于是分成 79 / 65 两档：框还在，只是不一样高。
     `grid-row: span 2` + `rows: subgrid` 才是「跨两行、且那两行与邻居共用轨道」。 */
  grid-auto-rows: auto;
}

.cap-item { grid-template-rows: subgrid; grid-row: span 2; }

.cap-item {
  gap: 0 12px;
  padding: 16px 16px 18px;
  border: 1px solid var(--line);
  background: var(--bg);
  /* 网格里给一条基准：同排三块一律等高，说明多一行也不会把这一块撑高。
     只写 stretch 不够 —— 必须在框上写，`.cap-list` 的 align-items 继承不到卡片内部。 */
  align-content: start;
}

/* 框里不需要顶部实线 —— 那就是框本身的上边。 */
.cap-item:first-child,
.cap-item:nth-child(2) { border-top: 1px solid var(--line); }

@media (max-width: 900px) {
  .cap-list { grid-template-columns: minmax(0, 1fr); gap: 10px; }
  .cap-item { padding: 14px 14px 16px; }
}

/* ---------- 5.1 章节眉标：让三个板块落在同一层级上 ----------
 *
 * 【问题】「报告里有什么 / 三步拿到报告 / 常见问题」这三个板块是**并列**的，
 * 但改之前它们读起来是一条线：
 *   · 板块标题 21px、正文 13px —— 相差 8px，扫描时几乎分不出来；
 *   · 板块之间 40+26 = 66px，条目之间 25px —— 只差 41px，还不够一眼断句；
 *   · 每块各带一条 1px 横线 —— 线越多，越像一张连续的表格。
 * 结果是整页从「报告里有什么」一路平铺到页脚，用户找不到「这是第几块」。
 *
 * 【改法：靠层级，不靠加线】
 *   1. ~~编号眉标~~ —— **已撤掉**。见下方「2026-10-29 二次重构」：三个板块各自有了内部编号，
 *      标题上方再挂一个 01/02/03 只会读成「模板套了三遍」。
 *      颜色最浅（--dim）。三级层次立刻出来：眉标 < 标题 < 正文条目。
 *   2. **标题加粗一档** 21 → 23px，并将字距收紧（衬线大字号需要负字距才不散）。
 *   3. **间距拉成 3 档**：板块间 74px、标题→内容 18px、条目间 25px。
 *      板块间 / 条目间 = 3.0 倍 —— 这才是「分组」能被看见的比例。
 *   4. **分隔线只留一条**，且改成从中向两侧淡出：它与正文的条目横线（实线、
 *      满宽）是两种东西，不会混成同一张表。页脚那条保留——那是页面结束的边界。
 *
 * 编号用 CSS 计数器，不写进 HTML：阿拉伯语下写死的「01」方向不会跟着走。
 * 三个板块的 DOM 顺序就是阅读顺序，计数器因此不用额外排序。
 *
 * 【踩坑一】最初我把计数器挂在 `main#stage-upload` 上、由 `.cap-h::before` 递增。
 * 实测三个标题**全是 01** —— `counter-increment` 写在元素上不会随兄弟元素累加。
 *
 * 【踩坑二】改成 `<ol class="sec-list">` + `li { counter-increment }` 之后，
 * 实测第一个是 **00**、后面全是 02/03。原因是**计数器名必须和标题那侧的
 * 作用域对上**：`.sec-list > .sec` 上的 `counter-increment` 发生在 li 元素上，
 * 而 `::before` 在 li **内部**建立新的作用域，读到的仍是外层还没自增的值。
 * 现在按规范写法：**`counter-increment` 与 `content` 放在同一个 `::before` 里**，
 * `counter-reset` 留在祖先后代都能继承的地方（`.sec-list`）。
 * 三个编号才会依次是 01 / 02 / 03。
 */
.sec-list {
  list-style: none;
  margin: 0;
  padding: 0;
  counter-reset: sec;
}


/* 标题与正文的落差：21 → 23px，衬线负字距。
 *
 * 【下边距 8 → 16px 的原因】390px 截图里，「三步拿到报告」下面紧跟着
 * 「01 上传一张正面照」—— 板块标题 21px、条目标题 15px，两者只差 6px，
 * 再加 8px 间距，读起来像同一组的两个标题。印刷品里章节标题和它下面的
 * 小标题之间至少要有一行的呼吸，所以给到 16px（约一行正文的高度）。
 * 桌面同步生效：这也正好把「标题 → 第一条内容」和「条目 → 条目」拉开。 */
.cap-h,
.how-h,
.faq-h {
  font-size: 23px;
  letter-spacing: -0.01em;
  line-height: 1.3;
  margin: 0 0 16px;
}

/* 板块间距：三块一致 74px。原来 cap 46 / how 40 / faq 40，
   三个数不同却没有理由 —— 不一致本身就在告诉读者「它们不是一类」。 */
.cap, .how, .faq {
  margin-top: 74px;
  padding-top: 30px;
}

/* 【2026-10-29 版式重构】三块不再共用「顶部分隔线 + 眉标 + 衬线标题」这一套外壳。
 *
 * 用户原话：「这个网站就一条线每个板块感觉一看就是ai写的」。
 * 上一版我给它加了 01/02/03 眉标和三块一样的淡出线 —— 结果是**同构更明显了**：
 * 三个板块长得一模一样，像同一个组件被渲染三次。这正是「AI 写的」的典型特征：
 * 每块都正确，但没有一块有自己的理由。
 *
 * 现在三块各有各的语法：
 *   · 目录（cap）—— 两栏网格，零横线，靠留白与编号定位。像书的目录。
 *   · 流程（how）—— 横向三栏，中间竖线，大号序号。像说明书的一页。
 *   · 问答（faq）—— 左标题右问答的两栏，问题用衬线。像访谈。
 * 所以原来三块共用的外壳规则全部拆掉，只在每块自己的段落里定义。
 */
.cap,
.how,
.faq {
  border-top: 0;
  background-image: none;
}

/* 标题下边距已经从 8px 提到 16px。这里不写 margin-top：相邻兄弟的外边距会折叠，
   写 0 与不写等价，但显式写 0 能让「不要再往上加间距」这件事在代码里看得见。 */
.cap-sub { margin: 0 0 18px; }

/* ---------- 6. 流程：横向三步，竖线分隔 ----------
 * 这一块是**说明书**的语法：三件要做的事并排，读者横向扫一眼就知道步骤。
 * 它和上面那份「目录」（两栏网格、无横线）刻意不一样 ——
 * 三块若长得一样，整页就像同一个组件被渲染了三次。 */
.how {
  margin-top: 74px;
  padding-top: 30px;
}

.how-list {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
  border-top: 0;
}

/* 【2026-11-01】原来是「三栏 + 中间竖线 + 顶边粗线」，像说明书的一页。
 * 用户要框，所以改成三个独立的框，间距 12px 与目录卡片一致。
 * 顶边那条 --line-strong 去掉 —— 框自己的上边已经承担了分组，
 * 再加一条更粗的横线就是三块之外的第四种语法。 */
.how-step {
  padding: 18px 18px 20px;
  border: 1px solid var(--line);
  border-right: 1px solid var(--line);
  /* 深墨块里 .how 给了 --step-bg / --step-line（见 6028 行），浅底时回落到白 + 细线。 */
  border-color: var(--step-line, var(--line));
  background: var(--step-bg, var(--bg));
}

/* 序号做成大号的衬线数字，压在竖线右侧 —— 它承载「第几步」，值得占一行。
   与目录里那排 11px 等宽编号刻意不同：这里的序号是「引导」，那里的编号是「索引」。 */
.how-step::before {
  content: counter(how, decimal-leading-zero);
  display: block;
  margin-bottom: 10px;
  font-family: var(--serif);
  font-size: 26px;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--accent);
  opacity: 0.9;
  font-variant-numeric: tabular-nums;
}

.how-step h3 { font-size: 15px; }
.how-step p { font-size: 13px; }

/* 【2026-11-01 断点 720 → 820】760 时三列各只有 177px 内宽（套框后每列还要
   各扣掉 2px 边框），正文 13.5px 一行 13 个字，「JPG、PNG 或 WEBP · 手机拍的
   原图就可以」被折成 3 行。
   三列要读得顺，每列至少 240px：240×3 + 12×2 = 744，加上版心左右内边距，
   视口到 820 以下就不该再三列了。
   依据来自 tools/check-card-text-width.mjs：它直接把「正文栏宽」打出来，
   760 那档报的就是 177。 */
@media (max-width: 820px) {
  .how-list {
    grid-template-columns: minmax(0, 1fr);
    gap: 10px;
    /* 单列时三个框不必等高 —— 它们上下叠着，撑高没有对照物。
       测试脚本里那条「同排高差」在单列下没有意义（每一行只有一个），
       这里显式写出来，免得下次读代码的人以为是漏了。 */
    align-items: start;
  }
  /* 单列后是「三个上下叠的框」，不是「一行用下边线分隔」—— 所以摘的是 padding，不是边框。 */
  .how-step { padding: 16px 16px 18px; }
  .how-step:last-child { padding-bottom: 18px; }
  .how-step::before { font-size: 20px; margin-bottom: 8px; }
}

/* ---------- 7. FAQ：左栏标题，右栏问答 ----------
 * 第三块又换了一种语法：**访谈**。标题钉在左边不动，问题一条条排右边，
 * 每条问题用衬线、前面挂一个等宽序号 —— 和目录的编号、流程的大数字都不同。
 * 这样三块看第一眼就能分辨：网格 / 三栏 / 左题右答。 */

/* FAQ 标题要对齐右栏第一条问题 —— 判据只有一条：**墨点带**，不是盒子顶也不是 Range 文字框顶。
 * 三种量法给出过三个互相矛盾的数（盒子差 -68、Range 文字顶高 27、墨迹低 24），
 * 前两个量的是行盒/网格行，只有裁剪图的像素能裁决。
 * 实测（1440px，裁 FAQ 区域逐行数非底色像素）：
 *   margin 20/24/30px → 标题墨点带与「01 照片会上传…」的墨点带**同一行起**；
 *   39px             → 标题整整低 24px（一个行高），栏目标签掉到了第一题下面。
 * 取 28px：留在这个区间里，两侧留一点余量防止字号/行高微调后越界。
 * 值必须写在 `.cap-h,.how-h,.faq-h` 那条之后 —— 同特异度，后来者赢。
 * 2026-10-29：原值 39px 是按「-68 与 -56 相差 24」推出来的，数字对、方向错。 */
.faq-h { grid-column: 1; grid-row: 1; margin: 28px 0 0; }

.faq {
  display: grid;
  grid-template-columns: 180px minmax(0, 1fr);
  column-gap: 40px;
  align-items: start;
}

.faq-list {
  grid-column: 2;
  grid-row: 1;
  margin: 0;
  border-top: 0;
  /* 【2026-11-01】四条问答从「一列被横线切开的条目」改成「四个独立的框」。
     gap 用 10px：这一组是长句问答，读者要一条条读，不需要像目录那样挨得紧。 */
  display: grid;
  gap: 10px;
}

.faq-item {
  border: 1px solid var(--line);
  background: var(--bg);
}

.faq-item summary {
  display: flex;
  align-items: baseline;
  gap: 14px;
  padding: 14px 16px;
  font-family: var(--serif);
  font-size: 16px;
  font-weight: 600;
  line-height: 1.5;
  cursor: pointer;
  list-style: none;
  /* 序号 + 问题靠左，箭头由 ::after 的 margin-left:auto 推到最右。
     不能沿用旧的 space-between：那会把中间那段文字拉成居中。 */
  justify-content: flex-start;
}

.faq-item summary::-webkit-details-marker { display: none; }

.faq-no {
  flex: 0 0 auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.06em;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}

.faq-item p { margin: 0; padding: 0 16px 16px 51px; font-size: 13.5px; }

@media (max-width: 720px) {
  .faq { grid-template-columns: minmax(0, 1fr); }
  .faq-h { grid-row: auto; margin-top: 0; margin-bottom: 16px; }
  .faq-list { grid-column: 1; grid-row: auto; }
  .faq-item p { margin-left: 0; }
}

/* ---------- 8. 页脚 ---------- */
/* 页脚原来铺一块 --bg2 的色带，是 SaaS 的「footer band」。
   印刷品的页脚还是同一张纸，只用一条横线断开 —— 色带会把页面切成两截。 */
.footer {
  background: transparent;
  border-top: 1px solid var(--line);
}

/* ---------- 9. 手机断点 ---------- */
@media (max-width: 760px) {
  .hero { padding: 34px 0 18px; }
  .hero h1 { font-size: clamp(26px, 7.2vw, 34px); }
  /* 手机上屏幕更窄，绝对间距可以收，但**比例不能变**：
     条目间距仍是 25px，板块间 56px = 2.2 倍（桌面是 3.0 倍）。
     原来 34/30 + 20 = 54/50，对 25px 的条目间距只有 2 倍出头，且三块又不一致。 */
  .cap, .how, .faq { margin-top: 56px; padding-top: 24px; }
  .cap-h, .how-h, .faq-h { font-size: 21px; }
}

/* ---------- 10. 报告页：收口那句横跨两栏 ---------- */
/* 结果区是两栏 grid（见上面 #stage-result）。右栏「为什么是这个判定」很高，
   而收口那句只有一行 —— 留在左栏会在它下面拉出约 490px 的空白，
   截图里左栏空了一大块，看起来像内容没加载完。
   跨栏之后它读作报告的**导语**，右栏卡片下移到它下面：
   结论本来就该压在所有细节之上，而不是和第一张卡片并排。
   （同一个坑原文件里已经有先例：`.card--wide` 就是为了填这种洞而加的。） */
.top-insight { grid-column: 1 / -1; }

/* 报告的主标题（脸型名）用衬线 —— 它是这一页的标题，
   而标题在这个视觉语言里一律是衬线。 */
#shape-name { font-family: var(--serif); letter-spacing: 0; }

/* ---------- 11. 压在强调色上的文字 ---------- */
/* 深色主题下 --accent 是浅金，所以这些地方的字是深墨；换成朱红之后
   深墨只剩 1.4:1（登录页的「登录」两个字几乎看不见）。一律改纸白：
   #fffdf8 压 #a33d2e 是 6.29:1。 */
.btn-primary,
.gender-btn.is-on,
.third-bar {
  color: #fffdf8;
}

/* 第三根柱子跟着压深一档。
   原来的 #96805f 是为了配黑字才提亮的（见上面 .third-bar.b3 的注释），
   改成白字之后只有 3.78，低于 4.5。压到 #7a6849 后白字 5.38，
   三根柱子仍然保持「朱红 → 暗红 → 褐」的递减关系。 */
.third-bar.b3 { background: #7a6849; }

/* ============================================================================
   工具页：工作表版式（2026-10-25）
   ----------------------------------------------------------------------------
   工具页的语法和首页不同。首页是「读」的 —— 一列文字，读者从上往下扫；
   工具页是「用」的 —— 读者要在一屏里同时看到「怎么给输入」和「会得到什么」。
   上一版把两者套进同一个 680px 单栏，于是工具页读起来像一篇博客：
   标题 → 上传框 → 四段说明 → 三条 FAQ，右侧三分之二的屏幕是空的。

   印刷品的工具页是一张**工作表**：左栏是图版区（给输入），
   右栏是参数表（能给出什么），两栏并排，一屏看完。

   `.sheet` 由页面标记显式给出，不靠 `:has(.cap-tool)` 去猜 —— 影棚页没有
   `.cap`，用 `:has()` 会让它也变成两栏、右栏空着。而这个文件里已经有
   两条 `:has()` 规则（上传态居中 / 摘掉底部死空间），再叠一条只会更难读。

   两条已知约束，改写时别踩：
   ① 选择器必须写到 (0,3,2) 才压得过上面那条 `:has(.hero-tool)` 的
      `max-width: 680px`；靠「后写」赢是合法的（同源、同优先级）。
   ② 只在 ≥900px 开两栏。手机上两栏会把「上传」和「你会得到什么」
      拆到上下两屏，反而更差。
   ============================================================================ */

@media (min-width: 900px) {
  main > section[data-stage="upload"].sheet:not([hidden]) {
    display: grid;
    grid-template-columns: minmax(0, 460px) minmax(0, 1fr);
    column-gap: 56px;
    align-content: start;
    max-width: 1000px;
    width: 100%;
    margin-inline: auto;
  }

  /* 标题横跨两栏。它是这一页的名字，不属于任何一栏。 */
  .sheet > .hero-tool { grid-column: 1 / -1; }

  /* 左栏：图版区。上传框吃掉整栏宽度 —— 它是这一页唯一的入口，
     收窄成 560 的方块会让右栏读起来比它重要。 */
  .sheet > #dropzone {
    grid-column: 1;
    grid-row: 2;
    max-width: none;
    min-height: 152px;
    /* 左右内边距与图标间距都比基础值小一档：440px 宽时文字列只有 350px，
       而试妆那句提示要 351px —— 差 1px 就折成两行、末行只剩一个「挡」字。
       量过才敢调：改完文字列 368px。 */
    padding: 22px 16px;
    column-gap: 12px;
  }

  .sheet > .privacy-note {
    grid-column: 1;
    grid-row: 3;
    margin-top: 20px;
  }

  /* 右栏：参数表。跨两行，与左栏的「图版 + 图注」齐高。 */
  .sheet > .cap-tool {
    grid-column: 2;
    grid-row: 2 / span 2;
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
  }

  /* FAQ 横跨两栏。基础样式给它 `max-width: 62ch`（≈718px），跨栏之后
     它仍然停在 718 —— 横线只画到页面中间，读起来像排版断了。
     阅读宽度下放到答案段落（见下面的 `.faq-item p`）。 */
  .sheet > .faq {
    grid-column: 1 / -1;
    grid-row: 4;
    max-width: none;
  }

  /* 【2026-11-01 框化之后重写】这条规则原来是给「三列一行的规格表」写的：
     左 112px 项名 / 右 1fr 说明，padding 左右为 0（列表靠外边线对齐，卡片自己不给内衬）。
     用户要求全站套框、卡片改成两列之后，它变成了**压垮版面的那一行**：
     卡片在 1280 下只有 228px 宽，112px 项名列吃掉一半，说明只剩
     228 − 112 − 22 = 94px（实测 91px），两句话被排成 9–12 行、卡片撑到 224 高；
     同时 padding 左右为 0，文字直接贴在 1px 边框上。
     现在框本身就是「块」，块里就应该是标题在上、说明在下 —— 不出列。
     行高与内衬交给下面那条统一的 `.cap-tool .cap-item` 规则。 */
  /* 【别用视口宽度猜列数】`.sheet` 是「左 460 + 右 1fr」两栏，右栏多宽取决于
     视口里扣掉左栏和 56px 列间之后还剩多少：
       1280 → 右栏 484，两列各 235（舒服）
       1024 → 右栏 400，两列各 187（正文一行 13 个字，id-photo 的说明排到 150 高）
     900~1100 这一段视口还「宽」，右栏已经很窄了 —— 视口断点猜不出来。
     改用 auto-fit + minmax：列数跟着**右栏可用宽度**走。
     200 是下限（正文 13.5px，一行放得下 14 个汉字），再窄就退成一列。 */
  .sheet .cap-tool .cap-list {
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 14px;
  }

  .sheet .cap-tool .cap-item {
    display: block;
    padding: 16px 16px 18px;
  }

  .sheet .cap-tool .cap-item h3 { margin: 0 0 8px; }
}

/* 表头（「你会得到什么」）在右栏顶部，不跟着 .cap 的上内边距走 */
.sheet .cap-h { margin-bottom: 4px; }

/* 副标题收回 62ch。上面那条 `max-width: 100%` 是 680 版心时代写的，
   版心放到 1000 之后一行能排 70 多个字 —— 中文一行超过 35 字就开始丢行。 */
.sheet .hero-sub { max-width: 62ch; }

/* FAQ 答案的阅读宽度。横线可以横贯整页，字不行。 */
.sheet .faq-item p { max-width: 62ch; }

/* ---------- 影棚：同一张工作表，另一种排法 ---------- */
/* 影棚页已经是两栏（`.studio` 360 + 剩余），缺的只是「卡片」这层语法。
   印刷品的分栏靠线，不靠两片几乎同色的底 —— 一列 `.studio-block` 全带
   边框时，页面读起来像一排待填的表单框。 */
.studio-block {
  background: transparent;
  border: 0;
  border-top: 1px solid var(--line);
  padding: 16px 0 20px;
}

@media (min-width: 940px) {
  .studio-main {
    border-left: 1px solid var(--line);
    padding-left: 26px;
  }
}

/* 预览图的圆角是「卡片」的残留：图版在印刷品里是直角裁切 */
.studio-media { border-radius: 0; }

/* ---------- 结果态：图版 + 批注栏 ---------- */
/* 左栏是图版（细框里放图），右栏是批注（控制项），中间一条竖线。
   原来两块都是同色的 `--panel` 卡片，在纸底上几乎分不出边界，
   读起来是「两块内容」而不是「一张表的两栏」。 */
.tool-canvas-wrap {
  background: transparent;
  border-color: var(--line-strong);
  padding: 12px;
}

.tool-canvas-wrap canvas { border-radius: 0; }

.tool-panel {
  background: transparent;
  border: 0;
  border-left: 1px solid var(--line);
  padding: 0 0 0 26px;
}

@media (max-width: 899px) {
  /* 单栏时竖线变成横线：手机上两栏是上下叠的，左边那条线会贴在屏幕边缘 */
  .tool-panel {
    border-left: 0;
    border-top: 1px solid var(--line);
    padding: 20px 0 0;
  }
}

/* ---------- 全站直角：把「卡片」这层语法彻底摘掉 ---------- */
/* 文件里还有 61 处写死的圆角（`.btn` 10、`.color-chip` 10、`.task-opt` 8、
   `.metric-list` 10……）。逐个列出来改是「新增一个组件就漏」——
   而这一版的语法是「直角是默认，圆角要理由」。

   所以用一条 `*` 兜底，再把「本来就是圆的」东西单独放回来：
   加载指示（转圈）、range 的滑块、胶囊徽章。
   这条必须 `!important`：`*` 的优先级是 0，压不过任何一个类。 */
*,
*::before,
*::after {
  border-radius: 0 !important;
}

/* 转圈必须还是圆的：它靠 border-top-color 的缺口表达「在转」 */
.spinner { border-radius: 50% !important; }

/* range 滑块是圆钮。轨道给 0 —— 一根方头的线更像尺子上的刻度槽。 */
input[type="range"]::-webkit-slider-thumb { border-radius: 50% !important; }
input[type="range"]::-moz-range-thumb { border-radius: 50% !important; }

/* 胶囊留给「状态徽章」：`46% 适中` 这种是标记，不是卡片。
   项目约定一直是「控件 6~8px、徽章才胶囊」，换色板时不该把它一起抹平。 */
.row-badge,
.metric-level { border-radius: 999px !important; }

/* ---------- 结果态：把「卡片」换成「表格 + 批注」 ---------- */
/* `.metric-list` 原来是「1px 间隙露出底色」拼出来的分栏 + 每行一块浅底，
   读起来是两张卡片。印刷品的做法是**表格**：行与行之间一条线，没有底色。 */
.metric-list {
  gap: 0;
  background: transparent;
  border-top: 1px solid var(--line);
}

.metric-row {
  background: transparent;
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
}

/* `.caveat` 是「照片会离开设备」这类告知。它原来是一个带边框的小盒子 ——
   盒子的语法是「附注卡片」，而这段文字的性质是**批注**：
   它贴着旁边的内容，用一道朱红竖脊指出来就够了。 */
.caveat {
  padding: 0 0 0 12px;
  border: 0;
  border-left: 2px solid var(--accent);
  background: none;
}

/* 影棚的「这是演示数据」同理：警告靠颜色，不靠盒子 */
.studio-mock {
  padding: 0 0 0 12px;
  border: 0;
  border-left: 2px solid var(--warn);
  background: none;
}

/* ---------- 影棚的选项：从「卡片」改成「清单」 ---------- */
/* 五个效果选项原来是「描边 + 浅底」的卡片，一列排下来像五张待填的表。
   印刷品的做法是清单：行与行之间一条线，选中项用一道朱红竖脊标出来 ——
   竖脊本来就是选中态的画法（原文件用 inset 阴影，就是为了不挤动内边距）。

   左内边距统一给 11px：竖脊画在 0~3px，文字从 11px 起，
   选中前后文字不会横移（这是原注释里点明要避免的事）。 */
.task-list { gap: 0; }

.task-opt {
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--line);
  padding: 11px 0 11px 11px;
}

.task-opt:hover { border-color: transparent; }

.task-opt:has(input:checked) {
  border-color: transparent;
  background: none;
  box-shadow: inset 3px 0 0 0 var(--accent);
}

/* 参数里的小胶囊（直发 / 卷发 / 短发 / 波波头）同理：去掉浅底，
   留一圈细边 —— 它们是一组可勾选的「格子」，不是卡片。 */
.opt-chip { background: transparent; }

/* 隐私条在 400px 左栏里折成两行，末行只剩「里。」两个字。
   它是「照片不会离开这台设备」的说明，全站最该被读清的一句，
   不能以两字孤行收尾。`pretty` 只调末行、不动前面，不会切断中文词。 */
/* 上传框里那句提示（「JPG / PNG / WEBP · 光线均匀、头发不被遮挡」）
   原来跟在标题下面、只占第二列（440 宽的图版里只剩 368px）。
   试妆那句要 351px —— 差一点点就折成两行、末行只剩一个「挡」字。
   靠收窄内边距或缩字号去凑，只是在「让中文这一版刚好放得下」；
   德语那句更长，一样会断。改成**横跨整个图版**：它本来就
   是图版的注记，不是标题的一部分 —— 印刷品的图注也是通栏写在图下。 */
.sheet .dz-hint { grid-column: 1 / -1; margin-top: 4px; }
/* 图标只占第一行。它原来跨两行（基础样式 `1 / span 2`），
   而提示横跨整行之后，跨两行的图标会和提示撞在同一格里 ——
   截图里那个「＋」正好压在「JPG」前面。 */
.sheet .dz-icon { grid-row: 1; }
.privacy-note { text-wrap: pretty; }

/* ---------- 对比页结果区：单列 ----------
 * ≥1081px 那条 `#stage-result { display: grid; 1fr 1fr }` 是给**首页**写的：
 * 首页的结果区是「照片 + 判定 + 几张卡」，两栏正好。
 *
 * 对比页的结果区（`#stage-result` 同名）结构完全不同 —— 结论句 → 两张脸 →
 * 逐项表 → 操作，是一条**单列**流程。它继承到那条 grid 之后，逐项表被当成
 * 一个普通的网格项，只拿到一栏：实测 513px，而「这意味着什么」那一列剩 311px
 * （内宽 295px）。于是 45 字的说明折成 3 行、末行 2 个字（「张。」「靠。」）。
 *
 * 这不是「文案太长」，是**两列 grid 不跨列** —— 本项目已记录的 CSS 静默失败
 * 之一。所以修法不是缩短文案（16 语 × 7 条，改不起，且改完换个语言又会复发），
 * 是让这一页根本不进那条 grid。
 *
 * 用显式类名而不是 `#stage-result:has(.cmp-table)`：文件里已有两条 `:has()`
 * 规则，且 `:has()` 压过 `[hidden]` 的那一类坑踩过。一个类名读起来也更直接。
 *
 * `display: block` 盖不掉隐藏：`[hidden] { display: none !important }` 是全局兜底。
 */
#stage-result.cmp-result { display: block; }


/* ---------- 测量条件：三个描述性数字 ----------
 * 三个数并列、等宽，不判好坏 —— 所以没有颜色、没有图标、没有「好/差」。
 * 不用 .eye-grid 的 auto-fit：那会在窄屏退成两列，第三个数字孤零零掉到第二行。
 * 三列能成立的前提是标签 ≤5 个字（320 下每列内宽 66px，5 个 11.5px 的字 = 57px）。
 */
.cond-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
/* ================================================================
 * §28 视觉升级：让首屏「动」起来，让中段有一块墨
 * ================================================================
 * 用户的诉求是「好看点、炫酷点」。这一版没有换风格 ——
 * 纸墨 + 朱红 + 衬线 + 直角仍然是底，只加三样东西：
 *
 *   1. 首屏那张「三庭五眼」从静态线稿变成**会扫描的仪器**（线条自绘 + 光带）
 *   2. 「三步拿到报告」整块翻成**深墨反白**，成为整页唯一的强对比锚点
 *   3. 微交互：目录行的荧光笔扫过、上传框的光带、顶部的阅读进度线
 *
 * 为什么不是深色霓虹风：那套是「AI 产品」的通用审美，而这站卖的是
 * 「你的脸被量过了」。测量仪器不需要发光，它需要**在动**。
 * ================================================================ */

/* ---------- 1. 首屏示意图：线条自绘 + 扫描光带 ----------
 * 三层节奏，让眼睛先看到「脸」，再看到「测量」，最后看到「数字」：
 *   0.12s  轮廓自绘（1.35s）
 *   0.50s  五眼竖线依次落下（每条 0.62s，间隔 0.05s）
 *   0.85s  三庭横线依次落下（间隔 0.07s）
 *   1.15s  五官与瞳孔淡入
 *   1.55s  1/3、1/5 标注淡入
 *   2.20s  光带开始循环
 *
 * dasharray 统一取 320：它比最长的那条线（三庭横线 252）还长，
 * 所以「offset = dasharray」对每一条都等价于「整条不可见」，
 * 一套数值管 10 条线，不用逐条量长度。
 */
.fg-outline {
  stroke-dasharray: 1100;
  stroke-dashoffset: 1100;
  animation: fgDraw 1.35s cubic-bezier(.5, .05, .25, 1) .12s forwards;
}

.fg-grid line {
  stroke-dasharray: 320;
  stroke-dashoffset: 320;
  animation: fgDraw .62s cubic-bezier(.4, 0, .2, 1) forwards;
}

.fg-eye line:nth-child(1) { animation-delay: .50s; }
.fg-eye line:nth-child(2) { animation-delay: .55s; }
.fg-eye line:nth-child(3) { animation-delay: .60s; }
.fg-eye line:nth-child(4) { animation-delay: .65s; }
.fg-eye line:nth-child(5) { animation-delay: .70s; }
.fg-eye line:nth-child(6) { animation-delay: .75s; }

.fg-third line:nth-child(1) { animation-delay: .85s; }
.fg-third line:nth-child(2) { animation-delay: .92s; }
.fg-third line:nth-child(3) { animation-delay: .99s; }
.fg-third line:nth-child(4) { animation-delay: 1.06s; }

/* 五官与标注：终值各不同（path .75 / circle .6 / text 1），
   所以用自定义属性传终值，一套 keyframes 收三处。
   直接写 `to { opacity: 1 }` 会把瞳孔从 .6 提到 1 —— 那是另一个东西了。 */
.fg-feat path { --fg-end: .75; }
.fg-feat circle { --fg-end: .6; }
.fg-label text { --fg-end: 1; }

.fg-feat path,
.fg-feat circle,
.fg-label text {
  opacity: 0;
  animation: fgIn .55s ease forwards;
}

/* nth-child 数的是「第几个子元素」，不是「第几个 path」。
   这里的孩子顺序是 path(左眼) / path(右眼) / circle / circle / path(鼻) / path(嘴)，
   所以鼻和嘴是第 5、第 6 个 —— 第一版按「第 3、第 4 个 path」写，
   结果这两条规则永远匹配不到，鼻子和嘴用 shorthand 里的 0s 延迟立刻出现：
   实测 200ms 那一帧里，别的线条还没画，只有一张"嘴"浮在空白上。 */
.fg-feat path:nth-child(1) { animation-delay: 1.15s; }
.fg-feat path:nth-child(2) { animation-delay: 1.20s; }
.fg-feat path:nth-child(5) { animation-delay: 1.28s; }
.fg-feat path:nth-child(6) { animation-delay: 1.34s; }
.fg-feat circle:nth-child(3) { animation-delay: 1.45s; }
.fg-feat circle:nth-child(4) { animation-delay: 1.50s; }

.fg-label text:nth-child(1) { animation-delay: 1.55s; }
.fg-label text:nth-child(2) { animation-delay: 1.59s; }
.fg-label text:nth-child(3) { animation-delay: 1.63s; }
.fg-label text:nth-child(4) { animation-delay: 1.67s; }
.fg-label text:nth-child(5) { animation-delay: 1.71s; }
.fg-label text:nth-child(6) { animation-delay: 1.75s; }
.fg-label text:nth-child(7) { animation-delay: 1.79s; }

/* 扫描光带。3.6s 一个周期：慢到不抢注意力，快到能读出「它在工作」。
   位移 290 是「带顶从额顶走到下颌底」（rect y=62 → 352，高 96 的带子
   这时已经整个移出脸的下方），超出的部分被 SVG 的 viewBox 裁掉。 */
/* `both` 是必需的，不是顺手加的：animation-fill-mode 默认 none，
   在 2.2s 的 delay 期间元素用的是**自己的声明值** —— 我没声明 opacity，
   于是它按 1 渲染，整条 0.26 透明度的朱红带子在页面一加载就压在脸上，
   直到动画开始才"消失一下再出现"。实测 700ms 那一帧就是这样。
   `both` 让 delay 期间也应用 0% 帧（opacity: 0）；再显式写一条 opacity: 0
   兜住「动画被浏览器丢弃」的情况。 */
.fg-sweep {
  opacity: 0;
  animation: fgSweep 3.6s cubic-bezier(.4, 0, .5, 1) 2.2s infinite both;
  pointer-events: none;
}

@keyframes fgDraw {
  to { stroke-dashoffset: 0; }
}

@keyframes fgIn {
  to { opacity: var(--fg-end, 1); }
}

@keyframes fgSweep {
  0%   { transform: translateY(0);     opacity: 0; }
  10%  { opacity: 1; }
  85%  { opacity: 1; }
  100% { transform: translateY(290px); opacity: 0; }
}

/* ---------- 2. 标题下的一枚朱红短横 ----------
 * 3px × 44px，从左边长出来。它替代的是原来那条「标题 → 副标题」之间
 * 只有 12px 的空隙 —— 印刷品的章节标题下面本来就会压一道短规线。
 * 只在有示意图的 hero（首页）出现：工具页的 hero 是表单标题，
 * 加一道线会把「标题 + 输入框」的紧凑感拆掉。
 */
.hero:has(.hero-figure) h1::after {
  content: "";
  display: block;
  width: 44px;
  height: 3px;
  margin-top: 15px;
  background: var(--accent);
  transform-origin: left center;
  animation: fgSeal .72s cubic-bezier(.2, .75, .2, 1) .30s both;
}

@keyframes fgSeal {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* ---------- 3. 目录行：鼠标划过像荧光笔扫过 ----------
 * 用背景渐变从左侧淡出，而不是整块底色：整块底色会把这一行变成
 * 「卡片」，而这里是目录 —— 目录的行不该有边界。
 * 不改 padding / 不改布局：hover 只动颜色，所以不会引起重排（CLS = 0）。
 */
.cap-item {
  transition: background-color .18s ease;
}

.cap-item:hover {
  background-image: linear-gradient(90deg, rgba(163, 61, 46, .055), transparent 62%);
}

.cap-item:hover h3 { color: var(--accent2); }
.cap-item:hover .cap-no { color: var(--accent); }

/* ---------- 4. 上传框：光带扫过一次 ----------
 * 只在 hover / 键盘聚焦时触发一次（不是循环）——
 * 循环会让首屏出现第二个持续动的东西，和示意图抢注意力。
 * ::after 已经被「取景角标」占了，所以用 ::before。
 */
.dropzone {
  overflow: hidden;
}

.dropzone::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(90deg, transparent 0%, rgba(163, 61, 46, .07) 50%, transparent 100%);
  transform: translateX(-101%);
  transition: transform .85s cubic-bezier(.3, 0, .2, 1);
}

.dropzone:hover::before,
.dropzone:focus-within::before,
.dropzone.dragover::before {
  transform: translateX(101%);
}

/* ---------- 5. 「三步拿到报告」：整块翻成深墨 ----------
 * 这是整页唯一的强对比锚点。前面两块（目录 / 问答）都是纸，
 * 中间这一块是墨 —— 页面的节奏因此变成「纸 → 墨 → 纸」，
 * 而不是三段一样的纸。印刷品里这叫「黑页 / 反白」。
 *
 * 代价要说清：这块里**每一个子元素都要重新给颜色**，
 * 漏一个就是「深灰压黑」的不可读文本。所以下面把 h2 / h3 / p / 序号 /
 * 分隔线全部显式写了一遍，一个都没留给继承。
 *
 * 对比度（底色 #191713，lum 0.0074）：
 *   #f6f0e4 → 16.3:1   h2 / h3（纯白化后仍保留这个暖白，因为纯白压墨是 17.9:1，
 *                              在 34px 的内边距里会显得「块和页面一样亮」，反而没了块感）
 *   #c3bbac      → 9.59:1   p
 *   朱红 #d4695a → 5.2:1    序号（26px 大字，门槛 3:1）
 *   var(--accent) #a33d2e 压黑只有 2.86:1 —— 不够，所以序号必须换亮一档。
 */
.how {
  margin: 74px 0 0;
  padding: 34px 34px 38px;
  background: var(--text);
  color: #f6f0e4;
  /* 【2026-11-01】原来这里是「左边一条标题 + 右边三栏用竖线分开」。
     三步各自套白框之后，框用的是 `background: var(--bg)`（纯白）——
     白框落在深墨底上，里面的文字还是深墨块那套浅色（h3 #f6f0e4 / p #c3bbac），
     于是「白底 + 白字」。截图第一次就露出来了：01/02/03 是朱红、下面两行看不清。
     解法：深墨块里的框改成**深色填充 + 浅色字**，与整块统一 ——
     深底上要表达「这是一个块」，靠的是比底色亮一档的填充与描边，不是白底。 */
  --step-bg: #241f19;
  --step-line: rgba(246, 240, 228, .22);
}

.how-h {
  color: #f6f0e4;
}

.how-list {
  border-top-color: rgba(246, 240, 228, .28);
}

/* 深墨块里的三个框：深色填充 + 浅描边，不是白框。
   用变量而不是直接写色值，是为了让 5379 行那条「框化」规则保持唯一来源：
   那边写 `var(--step-bg, var(--bg))`，浅底时变量不存在，自动回落成白。 */
.how-step {
  border-color: var(--step-line);
  background: var(--step-bg);
}

.how-step::before {
  color: #d4695a;
  opacity: 1;
}

.how-step h3 {
  color: #f6f0e4;
}

.how-step p {
  color: #c3bbac;
}

/* ---------- 6. 顶部阅读进度线 ----------
 * 纯 CSS scroll-driven（Chrome 115+）。不支持的浏览器里这条线根本不存在 ——
 * 那是**正确的降级**：一条停在 0 宽度的线比没有更糟（用户会以为加载卡住）。
 * 不用 JS：一个 scroll 监听换 2px 装饰不值得，而且 JS 版在
 * prefers-reduced-motion 下还得再关一次。
 */
html::after {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: 0 50%;
  z-index: 999;
  pointer-events: none;
}

@supports (animation-timeline: scroll()) {
  html::after {
    animation-name: fgProgress;
    animation-duration: auto;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: scroll(root block);
  }

  @keyframes fgProgress {
    from { transform: scaleX(0); }
    to   { transform: scaleX(1); }
  }
}

/* ---------- 7. 减少动效：全部关掉，但**不能停在初始态** ----------
 * 上面每一条都用 forwards / both，配合全局那条
 * `animation-duration: .01ms !important`，会在一帧内跳到终态 ——
 * 也就是「线条已画完、文字已显示」。这是对的。
 * 但扫描光带是 infinite + 终值 opacity 0：被压成 1 帧后它会停在
 * 「已经扫完、透明」的状态，等于不存在。所以直接 display: none，
 * 免得留一个 0.26 透明度的方块压在脸上。
 */
@media (prefers-reduced-motion: reduce) {
  .fg-sweep { display: none; }
  .dropzone::before { display: none; }
  html::after { display: none; }
  .hero:has(.hero-figure) h1::after { animation: none; transform: scaleX(1); }
}

/* ---------- 8. 断点 ----------
 * 必须在这里重写：末尾的普通规则会盖掉 @media (max-width: 760px) 里
 * 那条 `.cap, .how, .faq { margin-top: 56px; padding-top: 24px; }`，
 * 因为两者特异度相同、而它声明在更前面。不重写的话手机上
 * 「三步」会顶着 74px 的桌面间距，而且黑块内边距还是 34px。
 */
@media (max-width: 760px) {
  .how {
    margin-top: 56px;
    padding: 26px 20px 28px;
  }
  .hero:has(.hero-figure) h1::after { margin-top: 12px; width: 36px; }
}

/* 示意图只在 ≥1081px 显示，所以它的动效也只在那一档有意义。
   手机端 SVG 是 display: none，动画不跑，无需处理。 */

/* ===========================================================================
   分享横幅（`/?s=oval&…` 进来时显示「他测出来是什么」）
   ---------------------------------------------------------------------------
   版式沿用印刷品那套：直角、1px 细线分区、左边一道朱红竖线当批注。
   **不做卡片**（不填色、不投影）—— 一旦填色，它会和下面的上传框抢首屏，
   而这一屏的主任务仍然只有一个：上传。

   为什么 dl 而不是 div：标签和值是「一对」，读屏念成「三庭五眼：34% · 33% · 33%」。
   用 div + span 长得一样，但读屏会把它们念成两段不相干的文字。
   =========================================================================== */
.shared {
  margin: 30px 0 0;
  padding: 22px 24px 24px 21px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  background: var(--panel);
}

.shared-lead {
  margin: 0;
  font-family: var(--serif);
  font-size: 24px;
  line-height: 1.35;
  color: var(--text);
}

.shared-stats {
  display: grid;
  /* 上限 220px：auto-fit 配 1fr 会把三格摊到整个 1044px 宽上，
     每格之间空出 150px 的鸿沟，读起来像三个互不相关的东西。 */
  grid-template-columns: repeat(auto-fit, minmax(150px, 220px));
  justify-content: start;
  gap: 16px 24px;
  margin: 18px 0 0;
}

/* 一组「标签 + 数值」= 一个格子（见 share-banner.js 里那层 div） */
.shared-stats > div {
  min-width: 0;
}

.shared-stats dt {
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--dim);
}

.shared-stats dd {
  margin: 4px 0 0;
  font-size: 21px;
  font-weight: 600;
  color: var(--text);
  /* 三庭那格是「34% · 33% · 33%」，窄屏要能断行而不是溢出 */
  overflow-wrap: anywhere;
}

.shared-note {
  margin: 16px 0 0;
  font-size: 13px;
  line-height: 1.6;
  color: var(--dim);
}

.shared-cta {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-top: 14px;
  padding: 0 18px;
  border: 1px solid var(--accent);
  color: var(--accent);
  font-size: 15px;
  font-weight: 600;
  text-decoration: none;
}

.shared-cta:hover {
  background: var(--accent);
  color: var(--panel);
}

/* 本机计数：一行小字，跟着结果按钮走 */
.local-count {
  margin: 10px 0 0;
  font-size: 12px;
  color: var(--dim);
}

@media (max-width: 760px) {
  .shared {
    margin-top: 20px;
    padding: 18px 16px 20px 15px;
  }

  .shared-lead { font-size: 20px; }
  /* 两列 + 第一项整行：三庭那格是「34% · 33% · 33%」，
     在 150px 的列宽里一定会断成两行（断在「·」后面最难看）。
     给它一整行，剩下两项并排 —— 三格变两行，总高度几乎不变。 */
  .shared-stats { grid-template-columns: 1fr 1fr; gap: 14px 18px; }
  .shared-stats > div:first-child { grid-column: 1 / -1; }
  .shared-stats dd { font-size: 19px; }
}

/* 试戴镜框：保存按钮单独一行，和画布之间留一点呼吸 */
.gl-actions {
  margin-top: 14px;
}

/* ==========================================================================
   脸型百科（shapes.html）
   --------------------------------------------------------------------------
   这一页是「查」的页面，不是「用」的页面：正文密度比工具页高一档，
   但没有卡片、没有圆角、没有阴影 —— 六个脸型靠 1px 横线分段，
   和法务页是同一套印刷语法。

   两栏（女性 / 男性）**并排**，不做切换开关：
   搜「圆脸 男生 发型」进来的人不该先点一次性别按钮才看到答案；
   而且只渲染当前性别的话，爬虫只看得到一半内容。
   ========================================================================== */

.hero-shapes { padding-bottom: 4px; }

/* ---------- 目录 ---------- */
.shape-toc {
  margin: 20px 0 0;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--line);
}

.shape-toc-h {
  margin: 0 0 10px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
}

.shape-toc ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* 目录项是链接，要满足 2.5.8 的 24×24：13.5px 的行内文字只有 ~20px 高，
   所以用 inline-flex + min-height 撑到 36，手机再撑到 44。
   直角、1px 边框、纸色底 —— 和首页的 `.hero-chips` 是同一族，
   但那是不可点的标签，这里的可点性靠边框颜色更深（--line-strong）来区分。 */
.shape-toc a {
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding: 0 13px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text2);
  font-size: 13.5px;
  text-decoration: none;
}

.shape-toc a:hover,
.shape-toc a:focus-visible {
  border-color: var(--accent);
  color: var(--accent);
}

@media (max-width: 899px) {
  .shape-toc a { min-height: 44px; }
}

/* ---------- 一个脸型 ---------- */
.shape {
  padding: 32px 0 10px;
  border-bottom: 1px solid var(--line);
}

.shape-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 2px 12px;
  margin: 0 0 12px;
}

/* 衬线只给标题，字号比正文大一档半。六个 h2 是这一页真正的骨架，
   扫读的人靠它定位，所以字重要压住下面的 h4。 */
.shape-head h2 {
  margin: 0;
  font-family: var(--serif);
  font-size: 23px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--text);
}

:lang(zh) .shape-head h2,
:lang(ja) .shape-head h2,
:lang(ko) .shape-head h2 { letter-spacing: 0.012em; }

.shape-tag {
  margin: 0;
  font-size: 13px;
  color: var(--dim);
}

/* 「为什么是这个判定」+ 正文。标签用 inline-block + 右间距，
   不另起一行：这一句是解释，不是小标题，断开会让它看起来像第五节。 */
.shape-why {
  margin: 0 0 22px;
  font-size: 14.5px;
  line-height: 1.75;
  color: var(--text2);
  text-wrap: pretty;
}

.shape-why b {
  display: inline-block;
  margin-right: 4px;
  font-weight: 600;
  color: var(--text);
}

/* ---------- 两栏 ---------- */
.shape-cols {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 24px 32px;
}

/* 640 以下并排只剩 ~150px 一栏，中文一句话要折四行 —— 改单列。
   断点取 640 而不是 760：760 是 main 的上限，两栏在 700 左右还有 300px 一栏，
   足够。 */
@media (max-width: 640px) {
  .shape-cols {
    grid-template-columns: 1fr;
    gap: 18px;
  }
}

.shape-col-h {
  margin: 0 0 12px;
  padding-bottom: 7px;
  border-bottom: 1px solid var(--line);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.1em;
  color: var(--accent);
}

.shape-block { margin: 0 0 15px; }
.shape-block:last-child { margin-bottom: 0; }

.shape-block h4 {
  margin: 0 0 5px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
}

.shape-block p,
.shape-list li {
  font-size: 14px;
  line-height: 1.72;
  color: var(--text2);
  /* 中文孤行只能靠断行策略治：`balance` 会切断中文词（见 `.caveat` 那段），
     `pretty` 只调整最后一行，词内不断。 */
  text-wrap: pretty;
}

.shape-block p { margin: 0; }

.shape-list {
  margin: 0;
  padding-left: 15px;
}

.shape-list li { margin: 0 0 5px; }
.shape-list li:last-child { margin-bottom: 0; }
.shape-list li::marker { color: var(--accent); }

/* ---------- 结尾 ---------- */
.shape-cta {
  margin: 30px 0 16px;
  /* 长页面读到底才给出口：按钮本身不用铺满，靠左和正文对齐 */
  display: flex;
}

.shape-cta .btn {
  flex: 0 0 auto;
  min-width: 0;
  padding: 12px 26px;
}

/* 结尾的免责声明。**不复用 `.caveat` 的盒子**：那一套是工具页的提示框
   （圆角 9px + 浅底 + 边框），放在这一页的印刷语言里是一块异物。
   这里只要一行小字，并且要 `balance`：
   390px 下这句话 31 个字、一行放 29 个，不加处理末行只剩「议。」两个字。
   `balance` 把两行摊成 16/15，孤行消失 —— 代价是断点可能落在词中间，
   对一句法务声明来说可以接受（宁可断词，也不要一个字占一行）。 */
.shape-note {
  margin: 14px 0 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--dim);
  text-wrap: balance;
}
/* ---------- 报告时间线 ---------- */
/*
 * 不跨列：跟着历史报告待在左列（1.5fr）。
 *
 * 试过 `grid-column: 1 / -1` 通栏 —— 面板宽 1006px，而内容块只需要 560px，
 * 右边空出 446px，看起来像「内容没排满」而不是「留白」。
 * 收到左列后 590px 的面板几乎被内容填满，两块卡片也左对齐。
 */

.tl-span {
  margin: 8px 0 0;
  font-size: 12px;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}

.tl-rows {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  /*
   * 560px：让「标签…数值」这一行与下面的折线**右端对齐**。
   * 不设这个上限时，1280px 下数值会被推到 1006px 处、折线只有 420px，
   * 中间空出一大片，眼睛要在两处来回跳。
   */
  max-width: 560px;
}

.tl-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  /*
   * 基线对齐，不用 center。
   *
   * 德语 / 荷兰语 / 俄语的三庭标签会折成两行（Gesamtabweichung der
   * Drittel），center 会把数值压在两行中间 —— 看起来像悬在缝里。
   * baseline 让数值与标签**首行**对齐，折行时读起来仍是「标签 + 数值」。
   */
  align-items: baseline;
  gap: 2px 12px;
  padding: 10px 0;
  border-top: 1px solid var(--line);
}
.tl-row:first-child { border-top: 0; }

.tl-label { font-size: 13px; color: var(--dim); }

.tl-vals {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  font-size: 13.5px;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.tl-vals b { font-weight: 600; }

.tl-arrow { font-size: 12px; color: var(--dim); }

/* 差值不上色：变大变小哪个算「好」是价值判断，本站不做。 */
.tl-delta {
  padding-left: 2px;
  font-size: 12px;
  font-style: normal;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}

/*
 * 高度写死 44px + preserveAspectRatio="none"：折线用 non-scaling-stroke
 * 保住 1.5px 线宽，所以横向拉伸不会把线拉粗。改成 height:auto 的话，
 * 宽屏下这张图会涨到 80px 高，把三行折线撑成一大块空白。
 */
.tl-spark {
  grid-column: 1 / -1;
  /* svg 没有文本基线，不覆盖的话会被 baseline 推到奇怪的位置。 */
  align-self: start;
  display: block;
  width: 100%;
  /*
   * 限宽 560px（原来写 420，值改成 560 之后注释没跟着改 —— 注释里的数字
   * 是断言，跟值对不上就是错的）。
   *
   * 踩坑：第一版只写 width:100%，1280px 下这张 300×44 的图被拉到 1006px，
   * 横向拉伸 3.35 倍 —— 3 岁的变化被压成一条几乎水平的线，截图里看不出
   * 任何趋势。图的意义就是「看得出怎么走的」，拉平等于没画。
   *
   * 390px 屏上容器约 320px，max-width 不生效，仍是满宽。
   */
  max-width: 560px;
  height: 44px;
}
.tl-spark-line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.tl-spark-axis {
  fill: none;
  stroke: var(--line);
  stroke-width: 1;
  stroke-dasharray: 2 3;
}

/* 目标线。
   用墨色而不是朱红：朱红在这站里是「数据本身」（折线、主按钮），
   目标是一条**参照**，两样都红就分不清哪条是自己的体重。
   虚线间距比坐标轴那两条（2 3）更疏，因为它是横穿整幅图的，密了会像网格。 */
.tl-spark-ref {
  fill: none;
  stroke: var(--text2);
  stroke-width: 1;
  stroke-dasharray: 4 3;
}

/* 这三段都是整句说明，窄屏上容易掉出一两个字的孤行。 */
.tl-need, .tl-note, .tl-caveat { text-wrap: balance; }

/* ==========================================================================
   换发型（/hairstyle.html）
   ==========================================================================
   这一页和 AI 影棚是同一族：都要把照片发到服务端才能出图。所以外壳直接复用
   `.studio-block` / `.studio-h` / `.studio-msg` / `.studio-mock`，两栏网格也
   复用 `.studio`（上面那条选择器里加了 `.hs-layout` 别名）。
   这里只写真正属于这一页的东西：发型单选网格、脸型建议块、前后对比。

   不新造一套卡片/消息样式，是因为那套已经量过（对比度、44px 目标、中文孤行），
   复制一份等于把那些结论重新赌一遍。
   -------------------------------------------------------------------------- */

.hs-preview {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 12px;
}

.hs-preview img {
  width: 96px;
  height: 96px;
  object-fit: cover;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: var(--panel2);
}

/* `.btn` 在这站里默认是**整行按钮**：`.btn { flex: 1 }`，而后面还有一条
   `.btn { width: 100% }`（同选择器两块，后者按值生效）。两条一起作用，
   放进这一行就会把它撑成 362px —— 整行宽 362，于是 96px 缩略图 + 12px 间隙
   + 362px 按钮 = 470px，390px 的视口被撑出 95px 横向滚动。

   实测（`.tmp-test/probe-hs-preview.mjs`）：只加 `flex: 0 0 auto` 没用，
   按钮仍是 362px；补上 `width: auto` 之后按钮 69px、scrollWidth 回到 390。
   两个都要写，缺一个都不成立。 */
.hs-preview .btn { flex: 0 0 auto; width: auto; }

/* ---------- 脸型建议（全部在浏览器里算） ---------- */

.hs-shape-box {
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
}

.hs-shape {
  margin: 0 0 10px;
  font-size: 13px;
  color: var(--dim);
}

.hs-shape b {
  display: block;
  margin-top: 2px;
  font-family: var(--serif);
  font-size: 21px;
  font-weight: 600;
  color: var(--text);
}

.hs-gender {
  display: inline-flex;
  gap: 8px;
  margin-bottom: 12px;
}

/* 44 而不是沿用 .gender-btn 的 40：这一页在手机上就是主流程，
   40px 在 ≤899 的判据里是小目标（首页那处是次要控件，情况不同）。 */
.hs-gender .gender-btn { min-height: 44px; }

.hs-tips {
  margin: 0;
  padding-left: 18px;
  font-size: 13.5px;
  line-height: 1.75;
  color: var(--text2);
}

.hs-tips li + li { margin-top: 6px; }
.hs-tips li::marker { color: var(--accent); }

/* ---------- 发型单选 ---------- */

.hs-styles {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}

@media (min-width: 560px) {
  .hs-styles { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.hs-style {
  display: flex;
  align-items: center;
  justify-content: center;
  /* 10 种发型里最长的是德语「Pferdeschwanz」，13.5px 下两行放得下；
     固定 min-height 让两行与一行的高度一致，否则网格会出现锯齿。 */
  min-height: 48px;
  padding: 8px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--panel);
  color: var(--text2);
  font-size: 13.5px;
  line-height: 1.3;
  text-align: center;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s, color 0.15s;
}

.hs-style:hover {
  border-color: var(--line-strong);
  color: var(--text);
}

/* 选中态读的是**原生 radio 的状态**，不是 JS 加的类。
   方向键切换时浏览器自己更新 :checked，样式不会落后一帧；
   手写 .is-on 类的话，漏一次同步就是「选中的是马尾、高亮的是丸子头」。 */
.hs-style:has(input:checked) {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
  background: rgba(163, 61, 46, 0.07);
  color: var(--text);
  font-weight: 600;
}

.hs-style:focus-within {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.hs-style-name { pointer-events: none; }

/* ---------- 结果：前后对比 ---------- */

.hs-result { margin-top: 18px; }

.hs-compare {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}

.hs-fig { margin: 0; }

/* 两张图给**同一个盒子**（4:5 + contain），不按原图比例各自撑开。
   原图竖幅、生成结果多是 1:1，各自撑开的话两张图高度差几十像素，
   并排看会读成「右边那张没加载完」。contain 会留边，但留边比错位诚实。 */
.hs-fig img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: contain;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: var(--panel2);
}

.hs-fig figcaption {
  margin-top: 6px;
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--dim);
}

:lang(zh) .hs-fig figcaption,
:lang(ja) .hs-fig figcaption,
:lang(ko) .hs-fig figcaption {
  letter-spacing: 0;
}

.hs-note { margin: 12px 0 0; }

.hs-download { margin-top: 12px; }

/* 「去登录」那个按钮在状态消息里自成一行，与 studio 的处理一致 */
.hs-login { margin-top: 10px; }

/* ==========================================================================
   身材档案（body.html）
   --------------------------------------------------------------------------
   这一页不处理照片，输入是 5 个数字，输出是 4 张结论卡 + 一条趋势。
   所以视觉语言要跟「工具页」一致（纸、墨、1px 细线、直角），但密度要低一档：
   别的页的右栏是图，这里全是字，字挤在一起会读成表格。

   不新造按钮/消息/卡片体系 —— `.btn` / `.studio-block` / `.studio-h` /
   `.studio-msg` / `.panel-hint` 都已经量过（对比度、44px 目标、中文孤行）。
   这里只写属于这一页的东西。
   ========================================================================== */

/* 只给读屏的一句话。
   不能写 `display:none` / `visibility:hidden` —— 那样 live region 根本不播报。
   也不能 `width:0;height:0` —— 某些读屏会把它当成空节点跳过。
   下面这套是「视觉上 1px、语义上完整」的标准写法。 */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
.bd-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 18px;
  margin-top: 22px;
  align-items: start;
}

/* 940 与 `.studio` / `.hs-layout` 同一条断点，理由也一样：
   左栏是「填数字」，固定 340 就够；右栏吃掉剩下的宽度。
   两栏在 760 的版心里是 340 + 22 + 398 —— 右栏四张卡一张一行，
   每张 398 宽，34px 的衬线数字和「53.5 – 72.0 kg」都放得下。
   试过 2×2：每张只有 190，区间那行会折成两行，读起来像两段。 */
@media (min-width: 940px) {
  .bd-layout {
    grid-template-columns: 340px minmax(0, 1fr);
    gap: 22px;
  }
}

/* ---------- 输入 ---------- */

.bd-sex {
  display: flex;
  gap: 8px;
  margin-bottom: 14px;
}

/* 40 是 `.gender-btn` 的默认高度，这一页在手机上就是主流程，抬到 44。 */
.bd-sex .gender-btn { min-height: 44px; }

/*
 * 一行一个字段：标签在左、输入框在右。
 *
 * 为什么不是两列 grid —— 字段数是会变的（这一页从 5 个长到 7 个），
 * 而**奇数个**字段排两列时，最后一个会单独落到下一行左列：Tab 从
 * 「右列」跳到「左列」是一次**向左的回退**，键盘用户看到的是
 * 「焦点往下走了，却又跳回左边」。
 * 这是 check-focus-order 量出来的（5 个字段时逆序 1 对，dx=-601），不是猜的。
 * 改成单列之后，不管将来加到几个字段都不会再出现这条。
 *
 * 代价是更高：单列一行只放一个字段，两列能把两个并进一行。
 * 这个高度是拿「Tab 不往回跳」换的，在手机上值得 —— 页本来就要滚。
 */
.bd-fields {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 2px;
}

.bd-field {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 44px;
}

.bd-label {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 14px;
  line-height: 1.5;
  color: var(--text2);
}

/* 16px 不是随手写的：iOS 在字号 <16px 的输入框获得焦点时会整页放大，
   而这一页的流程是「点输入框 → 打字 → 看下面的结果」，
   放大会让结果跑出屏幕。 */
.bd-input {
  flex: 0 0 auto;
  width: 108px;
  min-height: 44px;
  padding: 8px 10px;
  font-family: inherit;
  font-size: 16px;
  /* 数字右对齐 + 等宽：竖着扫一列时小数点对得上，改一位不用重找位置 */
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  background: var(--panel);
  border: 1px solid var(--line-strong);
  border-radius: 0;
}

.bd-input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* 越界的那个框。
   只改边框颜色是不够的 —— 色觉障碍看不出「这一格跟别的不一样」。
   加粗 + 左侧一道 3px 的朱红，形状上也认得出。
   `aria-invalid="true"` 是同一个状态的**语义**版本（读屏念「无效」），
   两者必须同时存在：只写样式，读屏不知道；只写 aria，眼睛不知道。 */
.bd-input[aria-invalid="true"] {
  border-color: var(--bad);
  box-shadow: inset 3px 0 0 var(--bad);
  background: #fbf3ee;
}

/* ---------- 结果卡 ---------- */

/* 空态给一块虚线框，而不是一行灰字。
   左栏是有边框的卡片，右栏如果只剩一行字，两块面板的重量差得太远，
   读起来像「右边坏了」。虚线是这一站里「这里将来会有东西」的既有语言
   （上传区的 dropzone 用的也是它）。 */
.bd-empty {
  margin: 0;
  padding: 22px 16px;
  font-size: 14px;
  line-height: 1.7;
  color: var(--dim);
  border: 1px dashed var(--line-strong);
}

/*
 * `auto-fit` 而不是写死列数：右栏在 940 以上是 ~400、以下可能到 720，
 * 写死 2 列会在窄的一侧把「53.5 – 72.0」折成两行。
 * 220 是「一行放得下 11 个 34px 衬线数字」的下限。
 */
.bd-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 12px;
}

.bd-card {
  padding: 14px 16px 15px;
  border: 1px solid var(--line);
  background: var(--panel);
}

.bd-card-h {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin: 0;
  font-size: 12.5px;
  letter-spacing: 0.04em;
  color: var(--dim);
}

/* 公式是**这张卡的依据**，不是装饰：字号比标题再小一档，但要能读清 */
.bd-formula {
  font-size: 11.5px;
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
  color: var(--dim);
  white-space: nowrap;
}

/* 中文不加字距（见「视觉与排版约定」），只在拉丁语言下放宽 */
:lang(zh) .bd-card-h,
:lang(ja) .bd-card-h,
:lang(ko) .bd-card-h {
  letter-spacing: 0.01em;
}

.bd-value {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin: 8px 0 0;
  min-width: 0;
}

.bd-value b {
  font-family: var(--serif);
  font-size: 34px;
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.01em;
  color: var(--text);
}

.bd-unit {
  font-size: 13px;
  color: var(--dim);
}

/*
 * 分级标签：文字本身就是信息（「正常」「偏高」），颜色只是第二层。
 * 只靠颜色的话，红绿色觉障碍看到的是一模一样的两个方块。
 * 边框用 currentColor，所以四档的颜色只要改 `color` 一处。
 */
.bd-band {
  padding: 2px 8px;
  border: 1px solid currentColor;
  font-size: 12.5px;
  line-height: 1.6;
  white-space: nowrap;
}

.bd-band[data-band="normal"] { color: var(--good); }
.bd-band[data-band="low"] { color: var(--warn); }
.bd-band[data-band="high"] { color: var(--warn); }
.bd-band[data-band="veryHigh"] { color: var(--bad); }

.bd-hint {
  margin: 10px 0 0;
  font-size: 12.5px;
  line-height: 1.65;
  color: var(--dim);
}

/* ---------- 照片读身材 ---------- */

/* 整幅宽的一块，在两栏之前。照片预览 + 四个比值并排需要 220 + 20 + 剩余，
   塞进 340px 的左栏会把「肩腰比 1.35」折成两行。 */
.bp-block { margin-top: 18px; }

/* 虚线框是这一站里「这里将来会有东西 / 可以往里放」的既有语言
   （`.bd-empty` 和上传区用的是同一个）。拖放时换成朱红边 ——
   只换颜色不够，边框也从虚线变实线，色觉障碍一样看得出状态变了。 */
.bp-drop {
  margin-top: 14px;
  padding: 18px 16px;
  border: 1px dashed var(--line-strong);
  background: var(--panel);
  text-align: center;
}

.bp-drop.is-over {
  border-style: solid;
  border-color: var(--accent);
  background: var(--panel2);
}

.bp-need {
  margin: 0 0 12px;
  font-size: 13.5px;
  line-height: 1.7;
  color: var(--text2);
}

.bp-acts {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
}

.bp-local {
  margin: 12px 0 0;
  font-size: 12px;
  line-height: 1.6;
  color: var(--dim);
}

.bp-result { margin-top: 16px; }

/* 640 以下单列：手机上一次只放得下一块，并排会让 220px 的画布把
   四个比值挤成每行两个字。 */
.bp-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 16px;
  align-items: start;
}

@media (min-width: 640px) {
  .bp-body {
    grid-template-columns: 220px minmax(0, 1fr);
    gap: 20px;
  }
}

/* 画布按 CSS 宽度缩，位图尺寸由 body.js 按原图比例定。
   给 `background` 是为了透明 PNG：不然底下透出来的颜色会让
   画上去的三条线看不见。 */
.bp-canvas {
  display: block;
  width: 100%;
  max-width: 220px;
  height: auto;
  border: 1px solid var(--line);
  background: var(--panel);
}

.bp-side { min-width: 0; }

.bp-figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 10px;
}

.bp-fig {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  background: var(--panel);
}

.bp-fig-l {
  font-size: 12px;
  line-height: 1.5;
  color: var(--dim);
}

/* 比 `.bd-value b`（34px）小一档：这一块是「补充读法」，
   不该在视觉上压过右边那六张卡。 */
.bp-fig b {
  font-family: var(--serif);
  font-size: 22px;
  font-weight: 600;
  line-height: 1.2;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.bp-note {
  margin: 12px 0 0;
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--dim);
}

.bp-scale {
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}

.bp-scale-h {
  margin: 0 0 6px;
  font-size: 13px;
  letter-spacing: 0.04em;
  color: var(--dim);
}

.bp-scale .btn { margin-top: 10px; }

/* ---------- 变化 ---------- */

.bd-trend { margin-top: 18px; }

/* 指标切换。
   用 `.gender-btn` 那套（同样有 aria-pressed、同样的选中态与焦点环），
   只把内边距收到 12px。
   实测（.tmp-test/probe-bd-metrics.mjs，390 宽）：16 种语言里最宽的一组是
   印尼语，四个按钮合计 266px，可用 362px —— 都排得下，12px 是给更长的
   语言留的余量，不是现在就在救火。
   `flex-wrap: wrap` 是保险：真有语言放不下时宁可换行，也不要横向溢出。 */
.bd-metrics {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 12px;
}

.bd-metrics .gender-btn {
  min-height: 44px;
  padding: 0 12px;
}

/* 折线本身由 `.tl-spark` 负责（账号页那套），这里只交代容器宽度。
   `.tl-spark` 是 `preserveAspectRatio="none"` + `width:100%`，
   所以容器有多宽它就铺多宽，线宽靠 `non-scaling-stroke` 保持 1.5px。 */
.bd-spark {
  margin: 2px 0 10px;
  min-height: 44px;
}

/* 这个指标的点不足 2 个时没有折线可画（一个点的趋势是平的，那是谎）。
   那条路径上摘要是另一句话（「腰围只量过 1 次」），不是空白。
   容器空着的话，44px 的 min-height 会留下一块说不清来历的空白 ——
   截图里读起来像图没加载出来。 */
.bd-spark:empty { display: none; }

/* 「虚线是目标 65.0 kg」。
   图是 aria-hidden 的（只有形状），所以这句是目标线唯一的说明 ——
   没有它，一条没有标注的虚线比不画更让人困惑。
   空的时候不占高度：没设目标、或切到腰围/BMI 时它必须是空的。 */
.bd-goal-note {
  margin: -6px 0 10px;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--dim);
  /* 320px 断点上可用 292px：英语那句 46 个字符（16 语里最长）刚好一行 273px，
     德语反而折成两行。balance 让两行长度接近 —— 实测末行 160px
     （「außerhalb des Diagramms.」），不设的话只挂 72px 的「Diagramms.」一个词。 */
  text-wrap: balance;
}
.bd-goal-note:empty { display: none; }
.bd-summary:empty { display: none; }

.bd-summary {
  margin: 0 0 12px;
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--text2);
}

.bd-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--line);
}

/* 每一行是一个按钮（点一下把那条记录载回表单去更正），所以布局写在 `.bd-item` 上，
   `li` 只留分隔线 —— 按钮要显式 `font: inherit` / `text-align: left` /
   `background: none`，否则会带上浏览器默认的按钮字体、居中和灰底。 */
.bd-list li {
  border-bottom: 1px solid var(--line);
}

/* 【折叠】DOM 里只有真的显示出来的那几行 —— 不渲染、不占位、不留空白。
   这里**没有任何**折叠相关的 CSS，这是有意的：只要让被折叠的行还占位，
   屏幕上就一定会多出一块说不出用途的空白。

   走过四条路，前三条都被截图打脸，别再走回去：
   · display:none           —— 文档高度塌，浏览器夹 scrollY 把用户甩下去（2017 → 1870）。
   · visibility:hidden + min-height:44px
                            —— 高度保住了，但 8 行记录下面跟着 4 行空白。
                               判据全绿（它读属性、读行高），截图一眼假
                               —— 这是最坏的一种「通过」。
   · padding-bottom 补高    —— 同一件衣服换个穿法：空白从「4 行」变成 180px，
                               连分隔线都没了，更像坏了。
   · 不渲染（现在）          —— 页面该矮就矮，跳动的补偿交给 body.js 的点击处理
                               （先量锚点、真动了才 scrollBy）。 */




.bd-item {
  display: flex;
  align-items: baseline;
  gap: 12px;
  width: 100%;
  /* 44px 是触控目标的底线；桌面也一并撑开，列表不至于挤成一条条细线。 */
  min-height: 44px;
  padding: 9px 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 13px;
  color: var(--text2);
  text-align: left;
  cursor: pointer;
}
.bd-item:hover { color: var(--text); }
.bd-item:hover .bd-item-p,
.bd-item:hover .bd-item-b { color: var(--text2); }

/* 正在更正的那一行。用下划线而不是换颜色或加粗：
   换颜色就是「只靠颜色」传达状态，加粗会让这一行的宽度变、320px 下可能折行。 */
.bd-item.is-editing .bd-item-d { text-decoration: underline; text-underline-offset: 3px; }

/* 日期不换行：德语 / 俄语的月份名很长，换行会让这一列忽宽忽窄 */
.bd-item-d { flex: 0 0 auto; white-space: nowrap; }
/* 阶段名是这条记录的附属信息，比日期轻一档。不换字号：同一格里混两种字号，
   基线虽然对得齐，但「10月29日」和「减脂」看起来会像两件事。 */
.bd-item-p { color: var(--dim); }
.bd-item-w { flex: 0 0 auto; font-variant-numeric: tabular-nums; color: var(--text); }
.bd-item-b { flex: 1 1 auto; text-align: right; color: var(--dim); font-variant-numeric: tabular-nums; }

/* 更正模式的状态条。进入更正后表单被换成那条旧记录的值、保存按钮也改了文案，
   这一行是唯一说明「为什么表单变了」的地方 —— 没有它，用户会以为输入被吃掉了。
   左边那道竖线是「这是一条状态，不是一段说明」的形状提示。 */
.bd-edit {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  /* 只包住文字。铺满整行时（1280 下约 900px）这一条淡红会读成「表格里的一行」，
     而它其实只是一句状态说明；`max-width: 100%` 保证长语言在窄屏仍然折行。 */
  width: fit-content;
  max-width: 100%;
  margin: 16px 0 0;
  padding: 6px 10px;
  border-left: 3px solid var(--accent);
  background: rgba(163, 61, 46, 0.06);
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--text2);
}

.bd-edit-cancel {
  min-height: 44px;
  padding: 0 4px;
  border: 0;
  background: none;
  font: inherit;
  color: var(--accent2);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

/* 删除比取消更重：不用下划线（那是「可点的说明」），换成错误色 + 两步确认的底色。
   两步确认的**主信号是按钮文案变化**（「删除这条记录」→「再点一次删除」），
   底色只是辅助 —— 只靠颜色传达状态是不合格的。 */
.bd-edit-del {
  color: var(--bad);
  text-decoration: none;
  border: 1px solid rgba(156, 53, 39, 0.35);
  border-radius: 4px;
  padding: 0 10px;
}
.bd-edit-del.is-danger { background: rgba(156, 53, 39, 0.1); }

/* 分阶段汇总。只在真的跨了两个阶段时出现，所以不占高度（`hidden` 时整块不在）。
   一条细线把它和上面的历史列表分开 —— 两块的读法不同：上面是流水账，
   这里是「每段各走了多远」。 */
.bd-phases {
  margin: 14px 0 4px;
  padding-top: 10px;
  border-top: 1px solid var(--line);
}

.bd-phases-h {
  margin: 0 0 4px;
  font-size: 12.5px;
  letter-spacing: 0.04em;
  color: var(--dim);
}

.bd-phase-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.bd-phase-list li {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 4px 0;
  font-size: 13px;
  line-height: 1.6;
  color: var(--text2);
}

.bd-phase-name { flex: 0 0 auto; color: var(--text); }
.bd-phase-meta {
  flex: 1 1 auto;
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--dim);
}

/* 三颗 chip 的说明。没有它，它们看着像「全局模式」开关 ——
   而它们只作用于**这次要存的那条记录**。 */
.bd-phase-hint {
  margin: 16px 0 6px;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--dim);
  text-wrap: balance;
}

/* 阶段标签：和指标切换同一套 chip。放在保存按钮正上方 ——
   它描述的是「这次要存的记录」，离按钮越近越不会被误读成全局开关。 */
.bd-phase-pick {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0;
}

.bd-phase-pick .gender-btn {
  min-height: 44px;
  padding: 0 12px;
}

.bd-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 16px;
}

/* `.btn` 默认 `flex: 1` + `width: 100%`（两块同名规则，后者按值生效）。
   不写这两行，两个按钮会各占一整行、叠成上下两块 —— 这里它们是一对，
   而且「清空」是次要动作，不该跟「保存」一样宽。 */
.bd-actions .btn {
  flex: 0 1 auto;
  width: auto;
  min-width: 132px;
}

/* 页脚：那句「清掉浏览器数据就没了」+ 它的兑现方式（导出）。
   导出**不放进** .bd-actions —— 320px 下三个按钮会各自独占一行
   （德语「Diesen Eintrag speichern」单是文案就 218px），操作区撑成
   3 行 161px，比上面的状态条还高。而「导出」本来就是对这句话的回应，
   贴着它读才成立。按钮用下划线文字样式，和「取消」同一套语义。

   窄屏用 block、导出单独一行：44px 的触控目标挤在同一行里会把提示语
   压成 4 行，而且紧挨着正文容易误触。宽屏才排成一行、按钮靠右。
   `flex-basis: auto` 在这里是错的 —— 提示语内容很长，它会先占满一行、
   把按钮挤到下一行（1280 下也一样，实测过）。宽屏必须用 `flex: 1 1 0`。 */
.bd-foot { margin-top: 8px; }
.bd-foot .panel-hint { margin: 0; }
/* 两个文字按钮成组：窄屏跟在提示语下面单独一行，宽屏靠右。
   `align-items: baseline` 让「导出记录」和「导入备份」的下划线对齐 ——
   两者 min-height 都是 44，但字号行高不同，不指定会差一两个像素。 */
.bd-foot-acts {
  display: inline-flex;
  align-items: baseline;
  gap: 14px;
}
.bd-export {
  display: inline-block;
  margin-top: 2px;
  min-height: 44px;
  padding: 0 2px;
  border: 0;
  background: none;
  font: inherit;
  font-size: 12px;
  color: var(--accent2);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
@media (min-width: 640px) {
  .bd-foot {
    display: flex;
    align-items: baseline;
    gap: 2px 12px;
  }
  .bd-foot .panel-hint { flex: 1 1 0; min-width: 0; }
  .bd-foot-acts { flex: 0 0 auto; }
  .bd-export { margin-top: 0; }
}

/* ≤899：这一页的主流程在手机上，所有可点区域 ≥44 */
@media (max-width: 899px) {
  .bd-actions .btn { min-height: 44px; }
  .bd-item-d, .bd-item-w, .bd-item-b { font-size: 13.5px; }
}

/* ==========================================================================
   穿什么（#outfit-card）
   --------------------------------------------------------------------------
   一块「用数据」的卡片，不是又一项测量：领口读脸型、配色读季型、剪裁读场合。
   所以它的视觉比测量卡轻一档 —— 标签用 --dim、内容用 chip 与色块，
   不画条、不给百分比。任何看起来像「评分」的东西都会让人以为这几条建议
   和上面那 12 项指标是同一类东西（它们不是：那 12 项有测量依据，这里是建议）。

   纯白底上层次只能靠描边：场合按钮的选中态用**反白**（墨底白字），
   不用「浅一档的底」—— 纯白化之后「浅一档」已经不存在了。
   ========================================================================== */

.of-block { margin-top: 18px; }

.of-label {
  margin-bottom: 8px;
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--dim);
}

/* 场合：十二个筛选项（会换两三行，所以这里用 flex-wrap 而不是 grid）。
   44px 起是因为手机上这是这一块的主要操作区。 */
.of-occ {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.of-occ-btn {
  min-height: 44px;
  padding: 0 14px;
  border: 1px solid var(--line-strong);
  border-radius: 0;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 14px;
  line-height: 1.2;
  cursor: pointer;
}

.of-occ-btn:hover { background: var(--panel2); }

/* 选中态：反白。挂在 `[aria-pressed="true"]` 而不是另加一个 class ——
   视觉与无障碍状态必须同一个来源，否则会出现「看起来选中、读屏说没选中」。 */
.of-occ-btn[aria-pressed="true"] {
  background: var(--text);
  border-color: var(--text);
  color: var(--bg);
}

.of-occ-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.of-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 18px 24px;
  margin-top: 20px;
}

.of-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.of-chip {
  display: inline-block;
  padding: 6px 10px;
  border: 1px solid var(--line);
  background: var(--panel2);
  font-size: 13px;
  line-height: 1.35;
  color: var(--text);
}

/* 「建议回避」用虚线 + 灰字：虚线表达「这条不是实打实的推荐」。
   不用删除线 —— 删除线让 13px 的中文糊成一团，而且读起来像「禁止」，
   与文案里的「建议回避」不是一个语气。 */
.of-chip-avoid {
  border-style: dashed;
  background: var(--bg);
  color: var(--dim);
}

.of-swatches {
  display: flex;
  gap: 8px;
}

/* 色块不给文字：色名在 16 语里没有稳定对应（「藕荷色」在德语里没有词）。
   40px 是因为再小就看不出明度差 —— 而这三个色本来就是按明度拉开的。 */
.of-swatch {
  width: 40px;
  height: 40px;
  border: 1px solid var(--line-strong);
}

.of-note {
  margin: 10px 0 0;
  font-size: 13px;
  line-height: 1.65;
  color: var(--dim);
}

/* 手机：单列。两列时每列只剩 ~160px，「一字领」「正式低饱和」会被挤成两行，
   而两行 chip 并排看起来像两件事。 */
@media (max-width: 640px) {
  .of-grid { grid-template-columns: minmax(0, 1fr); gap: 16px; }
}


/* 「去影棚按这个场合出图」。
   刻意做成描边次按钮而不是主按钮：这一步会上传照片，而这一页的承诺是
   「照片不出设备」。做成实心主按钮会让人以为它是本地预览。
   箭头用 ::after 画，不进词典 —— 十六份译文里各带一个箭头，
   迟早有几份把它写成别的字符。 */
.of-gen {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 20px;
  min-height: 44px;
  padding: 0 18px;
  border: 1px solid var(--text);
  border-radius: 0;
  color: var(--text);
  font-size: 14px;
  text-decoration: none;
}

.of-gen::after { content: "→"; }

.of-gen:hover { background: var(--panel2); }

.of-gen:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
