/* ============================================================
   配色与质感（2026-09-11 第三版：收敛）

   前两版都翻车了：紫色黏土（厚阴影）、粉彩（渐变底 + 网点 + 三栏各自的
   彩色标题条 + 渐变按钮）。共同的毛病是**到处都在上色**。

   这版参照 bilibili 的取值（实测它线上的 CSS 变量）：
     页底 #F1F2F3   卡片 #FFFFFF   描边 #E3E5E7 / #F1F2F3
     文字 #18191C / #61666D / #9499A0   品牌粉 #FF6699
   结构是「灰底 + 白卡 + 1px 细描边 + 近黑文字」，**没有一处渐变、
   卡片上没有阴影**，粉色只出现在主按钮和选中态——它是重音，不是底色。

   使用者给的五个色留着，但降级成**小面积语义填充**：薄荷=直接命中、
   珊瑚=可迁移命中。这两档必须始终是两个色相（语义区分，不是装饰），
   其余地方一律走中性灰。

   品牌粉 #FF6699 白字只有 2.76:1，bilibili 自己也不过线——不照抄。
   强调色压到 #D81E5B（双向 4.9:1），当文字、当按钮底都够。

   字体退回原来的系统栈。上一版换的圆体（Yuanti SC）是"油腻"的一部分。

   `.resume-sheet` 那张纸一个字都不动——它是一张要打印的 A4，尺寸全部从
   --u 派生、密度靠实测高度二分，动它的 padding/圆角会直接改变"一页装得
   下多少"。视觉和版面在这里是两套东西，别混。
   ============================================================ */
:root {
  /* 使用者的色卡原色。只用在两档命中的填充上，不当界面底色。 */
  --c-blossom: #FFD7E1;
  --c-coral:   #FFB1AD;
  --c-mint:    #DCF2EE;
  --c-sage:    #B2CCC4;
  --c-peri:    #CFD5E0;

  --bg: #F1F2F3;
  --bg-tint: #F6F7F8;
  --panel-bg: #FFFFFF;
  --border: #E3E5E7;
  --border-soft: #F1F2F3;
  --text: #18191C;         /* 17.6:1 on #fff */
  --muted: #61666D;        /* 5.8:1  on #fff */
  --muted-soft: #82878E;   /* 4.0:1——只给不承载信息的辅助文字 */
  /* 2026-09-12：把"重音色"拆成两个 token，因为它在干两份要求不同的活。
     --accent 是**白底上的文字/描边**（链接、聚焦框、选中态），文字要 4.5:1，
     所以它不能再浅了；--accent-fill 是**按钮底色**，上面压白字，那是 UI 组件
     而不是正文，3:1 即可。合成一个 token 的时候，"按钮浅一点"和"链接看得清"
     永远只能满足一个。 */
  --accent: #D81E5B;       /* 4.9:1 on #fff——作文字用 */
  --accent-strong: #B8154B;
  --accent-soft: #FFF0F4;
  --accent-fill: #E9527F;        /* 白字 3.5:1；比原来浅、更粉 */
  --accent-fill-hover: #DE4272;  /* 悬停压深一点，手感上要"按下去有反应" */
  --hit: #1F6B5A;          /* 5.4:1 on --hit-soft */
  --hit-soft: #F0FAF8;
  --warn: #9C4A28;         /* 5.3:1 on --warn-soft */
  --warn-soft: #FFF4F0;
  --danger: #C0392B;
  --danger-soft: #FDF0EE;
  --radius: 8px;
  --radius-sm: 6px;

  /* ---------- 字号阶梯（2026-09-13） ----------
     这之前这个文件里散着 75 处 px 字号、12 个不同取值：10 / 10.5 / 11 /
     11.5 / 12 / 12.5 / 13 / 13.5 / 14 / 14.5 / 15 / 18。每一处单看都有道理
     （"这里比旁边小一点"），合起来就是没有层级——差 0.5px 的两级人眼分不出
     来，于是"大小"不再传达任何信息，只剩下随手调过的痕迹。

     收敛成 7 档。**不允许再写字面 px 字号**，要么用这些 token，要么说明
     为什么这一处必须例外。

     --fs-4 是正文，从原来的 13px 抬到 14px：界面是中文的，13px 的中文在
     1.5 倍行距下笔画会糊（"简""繁"这种密的字尤其明显）。14/1.6 是中文
     界面的下限，不是口味问题。

     `.resume-sheet` 及其后代一个都不在这里——纸上的字号一律是 N * --u，
     跟纸宽成正比。那是另一套单位系统，混用会让打印排版跟着屏幕走样。 */
  --fs-1: 28px;    /* 落地页主标题，全站只此一处 */
  --fs-2: 20px;    /* 区块标题 */
  --fs-3: 15px;    /* 面板标题、弹窗标题 */
  --fs-4: 14px;    /* 正文：对话气泡、档案条目、输入框 */
  --fs-5: 12.5px;  /* 按钮、次级信息、日期 */
  --fs-6: 11.5px;  /* 分组标签、脚注、状态行 */
  --fs-7: 10.5px;  /* 浮层小按钮、条目 id——只给不承载阅读的微型控件 */

  /* ---------- 间距阶梯 ----------
     同样的毛病：7 / 9 / 11 / 13 / 18 / 22 都出现过。全部取 4 的倍数。 */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;   /* 面板内边距的标准值 */
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;   /* 只在落地页用 */

  /* 阴影只留给"浮起来的东西"（弹窗、浮层）。卡片和面板一律不给——
     这是上一版最油的地方：满屏的东西都在投影。 */
  --shadow-sm: none;
  --shadow-md: none;
  --shadow-pop: 0 6px 24px rgba(0, 0, 0, 0.12);
  --ring: 0 0 0 2px rgba(216, 30, 91, 0.25);
}
* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
  background: var(--bg);
  color: var(--text);
  height: 100vh;
  display: flex;
  flex-direction: column;
  -webkit-font-smoothing: antialiased;
}

/* ---------- 顶部 ---------- */
header {
  padding: 12px 20px;
  background: var(--panel-bg);
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: 12px;
  flex-shrink: 0;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark {
  width: 30px; height: 30px; border-radius: 9px; flex-shrink: 0;
  display: block;
  object-fit: contain;
}
.brand-text h1 { font-size: var(--fs-3); margin: 0; font-weight: 700; letter-spacing: -0.01em; }
.brand-text .tagline { font-size: var(--fs-6); color: var(--muted-soft); margin-top: 1px; }
header .spacer { flex: 1; }
header button, .toolbar button {
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: var(--radius-sm);
  padding: 7px 14px;
  font-size: var(--fs-5);
  font-weight: 500;
  cursor: pointer;
  transition: background 0.15s ease;
}
header button:hover, .toolbar button:hover { background: var(--accent-fill-hover); }
header button.secondary, .toolbar button.secondary {
  background: #fff;
  color: var(--text);
  border: 1px solid var(--border);
}
header button.secondary:hover, .toolbar button.secondary:hover { background: #fafbfd; }
.status {
  font-size: var(--fs-5); color: var(--muted); display: flex; align-items: center; gap: 6px;
  padding: 4px 10px; border-radius: 999px; background: var(--bg);
}
.status .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--muted-soft); flex-shrink: 0; }
.status.ok .dot { background: var(--hit); }
.status.bad .dot { background: var(--danger); }

/* ---------- 主体三栏 ---------- */
main {
  flex: 1;
  display: grid;
  /* 2026-09-10 三栏定为「我的档案 / 简历 / 对话」。简历在正中间且拿最大的
     一份宽度：它装的是一张 A4 比例的纸，是三栏里最吃宽度的——栏太窄的话
     密度自适应只能一路收紧，字会小得没必要。档案是卡片列表、对话是气泡流，
     窄一点都不影响阅读。 */
  grid-template-columns: minmax(280px, 1fr) minmax(400px, 1.55fr) minmax(300px, 1.05fr);
  gap: 12px;
  padding: 12px;
  min-height: 0;
}
.panel {
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}
.panel > h2 {
  font-size: var(--fs-5);
  margin: 0;
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--border-soft);
  color: var(--text);
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 8px;
  letter-spacing: 0.01em;
}
.panel > h2 .icon { font-size: var(--fs-3); }
.panel .body {
  flex: 1;
  overflow-y: auto;
  padding: var(--sp-4);
}

/* ---------- 聊天面板 ---------- */
#chat-messages { display: flex; flex-direction: column; gap: 12px; }
.msg-row { display: flex; gap: 8px; align-items: flex-end; }
.msg-row.user { flex-direction: row-reverse; }
.avatar {
  width: 24px; height: 24px; border-radius: 50%; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: var(--fs-5); font-weight: 600; color: #fff;
}
.avatar.assistant { background: linear-gradient(135deg, var(--accent), #7c3aed); }
.avatar.user { background: #cbd2e0; color: #fff; }
.msg {
  max-width: 78%; padding: 9px 13px; border-radius: 14px; font-size: var(--fs-4);
  line-height: 1.55; white-space: pre-wrap; word-break: break-word;
  box-shadow: var(--shadow-sm);
}
.msg.user { background: var(--accent-fill); color: #fff; border-bottom-right-radius: 4px; }
.msg.assistant { background: #f3f4f8; border-bottom-left-radius: 4px; }
.msg.system {
  align-self: center; background: var(--warn-soft); color: var(--warn);
  font-size: var(--fs-5); border-radius: 8px; max-width: 90%;
}
.msg-row.system { justify-content: center; }

/* 状态指示（SSE流式进度） */
.status-row { display: flex; gap: 8px; align-items: center; padding: 2px 0 2px 32px; }
.typing-dots { display: inline-flex; gap: 3px; align-items: center; }
.typing-dots span {
  width: 5px; height: 5px; border-radius: 50%; background: var(--accent);
  animation: typing-bounce 1.1s infinite ease-in-out;
}
.typing-dots span:nth-child(2) { animation-delay: 0.15s; }
.typing-dots span:nth-child(3) { animation-delay: 0.3s; }
@keyframes typing-bounce {
  0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }
  30% { transform: translateY(-3px); opacity: 1; }
}
.status-label { font-size: var(--fs-5); color: var(--muted); }

.chat-input-row {
  display: flex; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); border-top: 1px solid var(--border-soft); flex-shrink: 0;
  background: #fafbfd;
}
.chat-input-row textarea {
  flex: 1; resize: none; border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 9px 12px; font-size: var(--fs-4); font-family: inherit; background: #fff;
}
.chat-input-row textarea:focus { outline: none; border-color: var(--accent); }
.chat-input-row button {
  background: var(--accent-fill); color: #fff; border: none; border-radius: var(--radius-sm);
  padding: 0 18px; cursor: pointer; font-weight: 500; font-size: var(--fs-5);
}
.chat-input-row button:disabled { background: var(--muted-soft); cursor: not-allowed; }
.chat-hint { font-size: var(--fs-6); color: var(--muted-soft); padding: 0 var(--sp-4) var(--sp-3); }

/* ---------- 简历/匹配结果面板 ---------- */
.badge { display: inline-block; font-size: var(--fs-6); padding: 3px 10px; border-radius: 999px; font-weight: 500; }
.badge.satisfied { background: var(--hit-soft); color: var(--hit); }
.badge.unsatisfied { background: var(--warn-soft); color: var(--warn); }
.gap-list { margin: 10px 0; padding-left: 18px; font-size: var(--fs-4); color: var(--warn); line-height: 1.6; }
.req-match { border: 1px solid var(--border-soft); border-radius: var(--radius-sm); padding: 8px 10px; margin-bottom: 7px; font-size: var(--fs-5); background: #fbfbfd; }
.req-match .tier { font-weight: 600; }
.req-match .tier.direct { color: var(--hit); }
.req-match .tier.transferable { color: var(--warn); }
.req-match .tier.gap { color: var(--danger); }
.req-match .ids { color: var(--muted-soft); }
#resume-latex {
  width: 100%; min-height: 200px; font-family: "SFMono-Regular", Consolas, monospace; font-size: var(--fs-5);
  border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px; resize: vertical;
  background: #1e1f26; color: #d6d9e3; line-height: 1.6;
}
.placeholder { color: var(--muted-soft); font-size: var(--fs-4); padding: 30px 0; text-align: center; }
#pdf-frame-wrap { margin-top: 10px; }
#pdf-frame { width: 100%; height: 380px; border: 1px solid var(--border); border-radius: var(--radius-sm); }

/* ---------- 用户信息窗口 ---------- */
.kind-group { margin-bottom: 16px; }
.kind-group > .kind-title { font-size: var(--fs-6); font-weight: 600; color: var(--muted-soft); margin-bottom: 7px; text-transform: uppercase; letter-spacing: 0.04em; }
.entry-card { border: 1px solid var(--border-soft); border-radius: var(--radius-sm); padding: 9px 11px; margin-bottom: 7px; font-size: var(--fs-5); position: relative; background: #fdfdfe; }
/* 2026-09-10（profile-hit-details）：命中从二元改成按 match_analysis 的
   三层判断分档。direct 沿用原来的绿、transferable 用琥珀；两档除颜色外
   都带各自的文字标签（见 profile.js 的 TIER_LABELS），不让颜色单独承载
   信息。gap 不会有记录挂上来，所以只有两档。 */
.entry-card.hit-direct { border-color: var(--hit); background: var(--hit-soft); }
.entry-card.hit-transferable { border-color: var(--warn); background: var(--warn-soft); }
.entry-card[tabindex]:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.entry-card .hit-tag { position: absolute; top: 7px; right: 9px; font-size: var(--fs-7); font-weight: 600; }
.entry-card.hit-direct .hit-tag { color: var(--hit); }
.entry-card.hit-transferable .hit-tag { color: var(--warn); }

/* ---------- 命中理由浮层 ---------- */
.hit-popover {
  position: fixed;
  z-index: 200;
  width: 300px;
  max-height: 70vh;
  overflow-y: auto;
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  padding: 10px 12px;
  font-size: var(--fs-5);
  line-height: 1.55;
  pointer-events: none;   /* 浮层本身不该拦鼠标，否则移到它上面卡片就 mouseleave 了 */
}
.hit-popover-title { font-size: var(--fs-6); color: var(--muted); margin-bottom: 7px; }
.hit-req { border-top: 1px solid var(--border-soft); padding-top: 6px; margin-top: 6px; }
.hit-req:first-of-type { border-top: none; padding-top: 0; margin-top: 0; }
.hit-req-head { display: flex; gap: 6px; align-items: baseline; }
.hit-req-text { flex: 1; font-weight: 500; }
.hit-popover .tier {
  flex-shrink: 0; font-size: var(--fs-7); font-weight: 600; padding: 1px 5px; border-radius: 4px;
}
.hit-popover .tier.direct { color: var(--hit); background: var(--hit-soft); }
.hit-popover .tier.transferable { color: var(--warn); background: var(--warn-soft); }
.hit-req-why { color: var(--muted); margin-top: 3px; }
.entry-card .title-line { font-weight: 600; margin-bottom: 2px; }
.entry-card .sub-line { color: var(--muted); font-size: var(--fs-6); margin-bottom: 4px; }
.entry-card ul { margin: 4px 0 0 0; padding-left: 16px; }
.entry-card .entry-id { color: var(--muted-soft); font-size: var(--fs-7); margin-top: 4px; }
.section-toggle { display: flex; align-items: center; gap: 8px; cursor: pointer; }
.empty-hint { color: var(--muted-soft); font-size: var(--fs-5); padding: 6px 0; }
.root-card { border: 1px dashed var(--border); border-radius: var(--radius-sm); padding: 10px 12px; margin-bottom: 16px; font-size: var(--fs-5); background: var(--accent-soft); }

/* 「开发者工具」那一整块（绕开对话框直接调接口的抽取/入库面板）已于
   2026-09-12 删除——它是开发期的脚手架，对用户只是噪音；文档导入早就作为
   正式功能挪到「我的档案」面板顶部了。 */
.error-text { color: var(--danger); }

/* ============================================================
   简历渲染器（2026-09-09"去LaTeX、改前端渲染"新增）

   视觉对齐原来的 professional LaTeX 模板（已删除，可用
   `git show <去LaTeX那个commit>~1:data/templates/professional/resume.tex.jinja`
   翻出来比对）：主色 #0052D9、日期灰 #666、标签底色 #EEF3FF、
   板块标题下方 0.6pt 主色横线、列表 bullet 用主色。

   排版密度不再是 LaTeX 时代的三档离散预设，而是一个连续的缩放变量
   --density：resume.js 渲染完测量实际高度，二分收敛到不超过一页。
   所有跟密度相关的尺寸都从它派生，不要在下面写死 px。
   ============================================================ */
/* 2026-09-11：纸不再跟着栏宽走，而是**永远 210mm**，屏幕上用
   transform: scale() 缩到栏宽里。

   为什么推翻上一版的容器查询方案：那一版让纸的宽度跟着栏宽变、尺寸从
   cqw 派生，指望"等比缩放"让屏幕测出来的密度在打印时依然成立。它不成立。
   实测同一份内容、同一个 --density，栏宽 561px 时纸高 793px，宽 790px 时
   纸高 1336px——等比预测应该是 1117px，**差了 20%**。原因是排版本身不是
   尺度不变的：文字在不同宽度下换行位置不同，行数一变高度就全变了，再加上
   小字号下行高取整的误差被放大。

   于是屏幕和打印是两套排版，测哪一套都救不了另一套——"前端显示一页、
   导出 PDF 两页"就是这么来的。现在纸只有一套布局（210mm 的那套），屏幕
   看到的是它缩小后的样子，所见即所得是**构造上成立**的，不是靠两次测量
   凑出来的。 */
#resume-render { position: relative; }

.resume-sheet {
  --r-accent: #0052D9;
  --r-date: #666666;
  --r-tagbg: #EEF3FF;
  --density: 1;
  /* 纸宽固定 210mm，所以 --u 也不再需要容器查询：210mm / 600 = 0.35mm，
     即"基准宽 600px 时 --u = 1px * density"，跟上一版的量级完全一致，
     下面那些系数一个都不用改。 */
  --u: calc(0.35mm * var(--density));
  width: 210mm;
  /* 屏幕上缩到栏宽：scale 由 resume.js 按栏宽算好写进来，打印时置为 none。
     缩放后纸在文档流里仍然占原始尺寸，所以 resume.js 同时把 #resume-render
     的高度设成缩放后的高度。 */
  transform-origin: top left;
  background: #fff;
  color: #16181d;
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: var(--shadow-sm);
  /* 左右 1.7cm、上 1.1cm、下 1cm（原 professional 模板的 geometry）。
     不乘 --density：密度调的是内容疏密，纸的天头地脚不该跟着缩。 */
  padding: 1.1cm 1.7cm 1cm;
  font-size: calc(10.2 * var(--u));
  line-height: calc(1.42 * var(--density));
  /* 高度按内容自然撑开，不锁死。锁死 + 裁切有个很糟的失败模式：只要测量
     不准，超出的内容会被悄无声息地裁掉。现在装不下就把纸撑高、内容看得见，
     同时画一条虚线标出一页的边界（.overflowing）。 */
  min-height: 297mm;
  overflow: visible;
  position: relative;
  font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
}
/* 连最紧的密度都装不下时：画一条虚线标出一页的边界，让用户一眼看出
   "超出去的是哪些内容"，配合工具栏上的「内容已超出一页」提示。 */
.resume-sheet.overflowing::after {
  content: "一页到此为止";
  position: absolute;
  left: 0;
  right: 0;
  top: var(--page-h);
  border-top: 1px dashed var(--danger);
  color: var(--danger);
  font-size: var(--fs-7);
  padding-top: 2px;
  pointer-events: none;
}

.resume-head { text-align: center; margin-bottom: calc(6 * var(--u)); }
.resume-head.with-photo { display: flex; align-items: center; gap: calc(12 * var(--u)); text-align: left; }
.resume-head.with-photo .resume-head-main { flex: 1; text-align: center; }
/* 证件照。宽度挂在外面那层 slot 上（2026-09-13 套的 wrapper，见
   resume.js::renderResumePhoto）——只读模式下没有 wrapper，img 直接当 flex
   项，所以两条选择器都要给宽度，不然后台里那张照片会撑满整行。 */
.resume-head .resume-photo-slot,
.resume-head > img.resume-photo { width: 13%; flex-shrink: 0; }
/* **两条选择器必须互斥，不能写成 `.resume-head img.resume-photo`。** 那样它
   跟上面那条同权重、又排在后面，于是连只读模式下那张直接挂在 head 上的图
   也被判成 100%——实测表现是后台里证件照撑满整行、把名字挤没。
   `> img` 只命中"没有 wrapper"那一种，`.resume-photo-slot > img` 只命中
   "有 wrapper"那一种，一张图永远只可能落进其中一条。 */
.resume-photo-slot > img.resume-photo { width: 100%; }
.resume-head img.resume-photo {
  display: block;
  border-radius: 2px;
  object-fit: cover;
}

/*
  悬停交互跟「我的档案」里那张对齐（2026-09-13）。

  用户前后说了两次"简历上的头像看起来完全不像可以交互的"。第一次只改了档案
  面板那张（`.avatar-slot`），纸上这张漏了——它一直能点，但除了一个原生 title
  之外没有任何反馈，而原生 title 要停住一秒多才出来，等于没有。

  **字号必须写成 N * --u**，不能用 --fs-7 那种固定 px：纸是先按 210mm 排好、
  再整体 transform: scale() 缩进栏里的，固定 px 会被一起缩——栏窄的时候那行
  字会缩到看不清。--u 跟纸宽成正比，纸上正文大约是 10.2 * --u。
  （同一个坑在 .resume-mode-badge 上踩过一次。）
*/
.resume-photo-slot {
  position: relative;
  overflow: hidden;
  border-radius: 2px;
  cursor: pointer;
  transform-origin: center;
  transition: transform 0.22s ease-out, box-shadow 0.22s ease-out;
}
.resume-photo-slot:hover,
.resume-photo-slot:focus-visible { transform: scale(1.06); }
.resume-photo-slot::after {
  content: "换一张";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: calc(1.5 * var(--u)) 0 calc(2 * var(--u));
  text-align: center;
  font-size: calc(7.6 * var(--u));
  line-height: 1.4;
  color: #fff;
  background: rgba(24, 25, 28, 0.66);
  opacity: 0;
  transition: opacity 0.22s ease-out;
  pointer-events: none;
}
.resume-photo-slot:hover::after,
.resume-photo-slot:focus-visible::after { opacity: 1; }
.resume-photo-slot:focus-visible { outline: none; box-shadow: var(--ring); }
@media (prefers-reduced-motion: reduce) {
  .resume-photo-slot:hover, .resume-photo-slot:focus-visible { transform: none; }
}
.resume-name { font-size: calc(21 * var(--u)); font-weight: 700; letter-spacing: -0.01em; }
.resume-contact { font-size: calc(9.4 * var(--u)); margin-top: calc(4 * var(--u)); }
.resume-contact .sep { color: var(--r-accent); margin: 0 calc(5 * var(--u)); }
.resume-tagline { font-size: calc(9.4 * var(--u)); margin-top: calc(2 * var(--u)); }

.resume-section { margin-top: calc(10 * var(--u)); }
.resume-section > h3 {
  font-size: calc(12.4 * var(--u));
  font-weight: 700;
  color: var(--r-accent);
  margin: 0 0 calc(1 * var(--u));
  padding: 0;
  border: none;
  letter-spacing: 0;
}
.resume-section > h3::after {
  content: "";
  display: block;
  /* 用 border-top 而不是 height + background 画这条线：浏览器默认**不打印
     背景色**（打印对话框里"背景图形"默认关着），用 background 画出来的线
     在屏幕上好好的、一打印就整条消失。边框和文字颜色不受这个开关影响。
     2026-09-11 修，现象是"每个标题下面的蓝线都打印不出来"。 */
  height: 0;
  border-top: 0.6px solid var(--r-accent);
  margin-top: calc(3 * var(--u));
}
.resume-entry { margin-top: calc(6 * var(--u)); }
.resume-entry-head { display: flex; align-items: baseline; gap: 8px; }
.resume-entry-title { font-weight: 700; font-size: calc(11.4 * var(--u)); flex: 1; }
.resume-entry-title .role { font-weight: 700; margin-left: calc(6 * var(--u)); }
/* 公司和职位之间要有一个**看得见的**分隔符，不能只靠间距。
   2026-09-14 真机：「MeituanBackend Development Engineer」——两段同样粗细的
   字挨在一起，英文里尤其糊，因为词本来就是用空格分开的，多一个空格读不出
   任何边界。「我的档案」那边一直用的是 `·`，这里跟它对齐。

   用 ::before 而不是往 DOM 里塞一个文本节点：那两个 span 是可编辑的，
   textContent 必须逐字等于字段值（加粗数字那套就是靠这个校验的），
   混进一个分隔符会把它破坏掉。CSS 生成的内容不进 textContent。 */
.resume-entry-title .role::before {
  content: "·";
  margin-right: calc(6 * var(--u));
  font-weight: 400;
  color: var(--r-muted, #888);
}
.resume-entry-date { font-size: calc(9.2 * var(--u)); color: var(--r-date); white-space: nowrap; }
.resume-tagline-row { margin-top: calc(3 * var(--u)); font-size: calc(9.6 * var(--u)); }
.resume-tag {
  display: inline-block;
  background: var(--r-tagbg);
  color: var(--r-accent);
  font-weight: 700;
  padding: 0 calc(4 * var(--u));
  border-radius: 2px;
  margin-right: calc(5 * var(--u));
}
.resume-stack .resume-tag { font-weight: 400; margin-right: calc(4 * var(--u)); }
.resume-entry ul { margin: calc(3 * var(--u)) 0 0; padding-left: calc(13 * var(--u)); }
.resume-entry li { margin-top: calc(2 * var(--u)); }
.resume-entry li::marker { color: var(--r-accent); }
.resume-entry strong { font-weight: 700; }
.resume-edu-line { display: flex; align-items: baseline; gap: 8px; margin-top: calc(3 * var(--u)); }
.resume-edu-line .school { font-weight: 700; }
.resume-edu-line .detail { font-size: calc(9.4 * var(--u)); flex: 1; }
.resume-flat-list { margin: calc(3 * var(--u)) 0 0; padding-left: calc(13 * var(--u)); }
.resume-flat-list li { margin-top: calc(2 * var(--u)); }
.resume-flat-list li::marker { color: var(--r-accent); }

/* ---------- 编辑态：拖动手柄与删除按钮（打印时全部隐藏） ---------- */
.resume-block { position: relative; }
.resume-block .block-ctrl {
  position: absolute;
  top: 0;
  right: 0;
  display: none;
  gap: 3px;
  z-index: 2;
}
.resume-block:hover > .block-ctrl { display: flex; }
.block-ctrl button {
  border: 1px solid var(--border);
  background: #fff;
  color: var(--muted);
  border-radius: 4px;
  font-size: var(--fs-7);
  line-height: 1;
  padding: 3px 5px;
  cursor: pointer;
}
.block-ctrl button:hover { color: var(--danger); border-color: var(--danger); }
.block-ctrl button.drag-handle { cursor: grab; }
.resume-block.dragging { opacity: 0.4; }
.resume-drop-marker { height: 2px; background: var(--accent); border-radius: 2px; margin: 2px 0; }

.resume-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 10px 0 8px;
  flex-wrap: wrap;
}
.resume-toolbar .overflow-flag {
  font-size: var(--fs-6);
  color: var(--danger);
  background: var(--danger-soft);
  padding: 3px 8px;
  border-radius: 999px;
}
.resume-toolbar .dirty-flag {
  font-size: var(--fs-6);
  color: var(--warn);
  background: var(--warn-soft);
  padding: 3px 8px;
  border-radius: 999px;
}
.print-hint { font-size: var(--fs-6); color: var(--muted-soft); }

/* ---------- 确认弹窗 ---------- */
.modal-backdrop {
  position: fixed; inset: 0; background: rgba(16, 24, 40, 0.32);
  display: flex; align-items: center; justify-content: center; z-index: 100;
}
.modal-card {
  background: #fff; border-radius: var(--radius); padding: 20px;
  max-width: 400px; box-shadow: var(--shadow-md);
}
.modal-card h4 { margin: 0 0 8px; font-size: var(--fs-3); }
.modal-card p { margin: 0 0 16px; font-size: var(--fs-4); color: var(--muted); line-height: 1.6; }
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; }

/* ============================================================
   打印样式：导出 PDF 走浏览器原生打印（见 openspec 变更
   frontend-resume-rendering 的 design.md D6）。整页只留 .resume-sheet，
   界面本身的一切（三栏布局/工具栏/拖动手柄/删除按钮）全部隐藏。
   ============================================================ */
@page { size: A4; margin: 0; }
/* ---------- 生成模式（2026-09-11 resume-generation-modes） ---------- */

/* 模式选择器：贴在对话输入框上面。严格/包装两项，彩蛋由 app.js 在
   ?mode=unhinged 时插进来。 */
.mode-row {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--sp-3); padding: var(--sp-2) var(--sp-4) 0;
  font-size: var(--fs-5); color: var(--muted);
}
.mode-row label { display: flex; align-items: center; gap: 4px; cursor: pointer; }
.mode-row input { margin: 0; }
.mode-hint { flex: 1 1 180px; min-width: 0; font-size: var(--fs-6); }
.mode-hint.danger { color: #C0392B; font-weight: 600; }
.mode-row label.mode-unhinged span { color: #C0392B; }

/* 工具栏上的模式提示：颜色之外再给一句文字，不靠颜色单独传达信息 */
.mode-flag {
  font-size: var(--fs-6); padding: 1px 7px; border-radius: 9px;
  border: 1px solid currentColor;
}
.mode-flag-polished { color: #B26A00; }
.mode-flag-unhinged { color: #C0392B; font-weight: 600; }

/* ---------- 从档案拖到简历上（2026-09-13） ---------- */
.entry-card[draggable="true"] { cursor: grab; }
.entry-card.is-dragging-out { opacity: .45; cursor: grabbing; }
/* 纸上给一圈虚线表示"可以松手了"。用 outline 不用 border：border 会改变
   盒子尺寸，而这张纸的高度正被二分收敛盯着，加 2px 就可能把它推过一页。 */
.resume-sheet.drop-target {
  outline: 2px dashed var(--accent);
  outline-offset: -6px;
}
/*
  拖动时那条黄色提示。

  **停在屏幕上不自己消失**——它讲的不是"成功了"，是一个要在松手之前读到的
  提醒。用几秒就淡掉的那种提示，用户多半还没松手它就没了。

  pointer-events: none 是必需的：它浮在纸的上方，能接事件的话，指针划过它
  就会触发一次 dragleave，落点高亮会一路闪。
*/
.drop-hint {
  position: fixed;
  left: 50%;
  top: 18px;
  transform: translateX(-50%);
  z-index: 200;
  max-width: min(560px, 92vw);
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 16px;
  border-radius: var(--radius);
  border: 1px solid #E8C46A;
  background: #FEF6E0;
  color: #6B4E11;
  box-shadow: var(--shadow-pop);
  font-size: var(--fs-5);
  pointer-events: none;
}
.drop-hint strong { font-weight: 600; }
.drop-hint span { color: #8A6A24; font-size: var(--fs-6); line-height: 1.6; }

/* ---------- 版本与模板那一行（2026-09-13） ---------- */
.resume-versions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-2);
  padding: var(--sp-2) 0 var(--sp-3);
  border-bottom: 1px solid var(--border-soft);
  margin-bottom: var(--sp-3);
  font-size: var(--fs-6);
}
.vbar-label { color: var(--muted-soft); letter-spacing: .04em; }
.vbar-sep {
  width: 1px;
  align-self: stretch;
  background: var(--border);
  margin: 0 var(--sp-2);
}
.slot-chip, .tpl-chip, .slot-add {
  font-size: var(--fs-6);
  padding: 2px 10px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: #fff;
  color: var(--text);
  cursor: pointer;
  transition: border-color .15s ease, background .15s ease, color .15s ease;
}
.slot-chip:hover, .tpl-chip:hover, .slot-add:not(:disabled):hover { border-color: var(--accent); }
.tpl-chip.is-on {
  border-color: var(--accent);
  background: var(--accent-fill);
  color: #fff;
}
.slot-add:disabled { color: var(--muted-soft); cursor: default; }
/* 删除那个 ✕ 藏在 chip 里，悬停才露出来：五个 chip 各挂一个常驻的叉，
   整行看起来像随时要出事，而真正该显眼的是版本名字 */
.slot-x {
  margin-left: 6px;
  color: var(--muted-soft);
  opacity: 0;
  transition: opacity .15s ease, color .15s ease;
}
.slot-chip:hover .slot-x, .slot-chip:focus-within .slot-x { opacity: 1; }
.slot-x:hover { color: var(--danger); }
.quota-flag {
  margin-left: auto;
  color: var(--muted-soft);
  font-size: var(--fs-7);
}
.quota-flag.is-low { color: var(--warn); }
.text-prompt-input {
  width: 100%;
  box-sizing: border-box;
  margin: 0 0 var(--sp-4);
  padding: 8px 10px;
  font-size: var(--fs-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.text-prompt-input:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }

/* ============================================================
   简历模板（2026-09-13 openspec 变更 resume-templates）
   ============================================================

   **模板只改样子，不改内容。** 挂点是 .resume-sheet 上的 data-template，
   下面每套模板允许动的只有这几样：

     --r-accent / --r-date / --r-tagbg 三个颜色变量、字体、板块标题那条线、
     抬头的对齐、标签的画法。

   **不许动的**：纸的宽度、padding（那是纸的天头地脚，不是设计）、--u 的
   算式、min-height。这四样一动，"预览和 PDF 是同一套排版"这个前提就没了
   ——那条前后修过三次才对（见 doc/wiki/pitfalls.md）。

   密度不用为模板重新标定：前端是**实测 DOM 高度二分收敛**的，字号变了它
   自己会收敛到新的密度。`estimate_layout_params` 那两个阈值只是个起点。

   加一套新模板要动三处：这里一段 CSS、resume.js 的 RESUME_TEMPLATES、
   以及（如果它引入了新的结构假设）打印段。三处都在测试的视野里。 */

/* —— 墨白：无彩色，衬线标题。学术、传统行业、或者单纯不想让简历有颜色 —— */
.resume-sheet[data-template="ink"] {
  --r-accent: #1A1A1A;
  --r-date: #5A5A5A;
  --r-tagbg: transparent;
}
.resume-sheet[data-template="ink"] .resume-name,
.resume-sheet[data-template="ink"] .resume-section > h3 {
  font-family: Georgia, "Songti SC", "SimSun", serif;
  letter-spacing: .01em;
}
.resume-sheet[data-template="ink"] .resume-section > h3::after {
  border-top-width: 1.2px;
}
/* 标签改成描边：没有底色的模板里，浅色块会显得脏 */
.resume-sheet[data-template="ink"] .resume-tag {
  border: 0.6px solid #BBBBBB;
  color: #333333;
  font-weight: 400;
}

/* —— 素雅：一条线都没有，靠留白和字重分层。经历多、想省版面的人用 —— */
.resume-sheet[data-template="quiet"] {
  --r-accent: #2E2E2E;
  --r-date: #8A8A8A;
  --r-tagbg: #F2F2F2;
}
.resume-sheet[data-template="quiet"] .resume-head { text-align: left; }
.resume-sheet[data-template="quiet"] .resume-head.with-photo .resume-head-main { text-align: left; }
.resume-sheet[data-template="quiet"] .resume-section > h3 {
  font-size: calc(9.6 * var(--u));
  letter-spacing: .18em;
  color: #8A8A8A;
  font-weight: 500;
}
/* 去掉标题下面那条线。**用 display:none 而不是改颜色**——改成白色在
   打印时会变成一条真的白线压在纸上，某些打印机会把它印成浅灰。 */
.resume-sheet[data-template="quiet"] .resume-section > h3::after { display: none; }
.resume-sheet[data-template="quiet"] .resume-section { margin-top: calc(13 * var(--u)); }
.resume-sheet[data-template="quiet"] .resume-tag { color: #444444; font-weight: 400; }

/* —— 松绿：低饱和的青绿，标题线只留在名字下面。想有颜色但不想太"互联网" —— */
.resume-sheet[data-template="pine"] {
  --r-accent: #0F6E64;
  --r-date: #6B7280;
  --r-tagbg: #E8F2F0;
}
.resume-sheet[data-template="pine"] .resume-head {
  border-bottom: 1.2px solid var(--r-accent);
  padding-bottom: calc(4 * var(--u));
}
.resume-sheet[data-template="pine"] .resume-section > h3::after { display: none; }
.resume-sheet[data-template="pine"] .resume-section > h3 {
  font-size: calc(11.6 * var(--u));
  letter-spacing: .04em;
}

/* 纸面本身的标记。严格模式不带 data-mode，所以下面这些一条都不命中——
   "没标记"就是"可追溯"的意思。 */
.resume-sheet[data-mode] { position: relative; }
.resume-sheet[data-mode="polished"] { border-left: 4px solid #E8A33D; }
.resume-sheet[data-mode="unhinged"] { border: 2px solid #C0392B; }

.resume-mode-badge {
  position: absolute; top: 0; right: 0;
  padding: 2px 9px;
  /* 纸上的字号一律是 N * --u（--u 跟纸宽成正比），body 大约是 10.2 * --u。
     第一版这里写成 1.6 * --u，算下来不到 3px，打印预览里是一行看不清的
     灰点——尺寸单位是 --u 不是 px，写错一个数量级不会报错、只会变得看不见。 */
  font-size: calc(8.2 * var(--u));
  letter-spacing: .04em;
  border-bottom-left-radius: 6px;
  color: #fff;
}
.resume-sheet[data-mode="polished"] .resume-mode-badge { background: #E8A33D; }
.resume-sheet[data-mode="unhinged"] .resume-mode-badge { background: #C0392B; }

/* 2026-09-13：页脚那行字（"本简历经表述优化生成…"）连同它上面那根虚线
   整个删掉了，屏幕和打印件上都不再有。它原来是纸上唯一的模式标记。

   取舍见 openspec 变更 print-clean-mode-marks：那行字会跟着 PDF 一起进到
   HR 手里，而它对读简历的人没有意义、对写简历的人只有损害。模式标记现在
   **只在屏幕上**——左边那道色条、右上角的角标、工具栏那个 flag（鼠标停上去
   能看到完整说明）。 */

@media print {
  body { height: auto; display: block; background: #fff; }
  header, main > .panel:not(#panel-resume),
  #panel-resume > h2, .resume-toolbar,
  /* 这两个是 id 不是 class——之前写成 .resume-meta，选择器压根没命中，
     匹配分析被一起打印出来，正好把简历挤成两页 */
  #resume-meta, #resume-placeholder, .hit-popover, .mode-row, .resume-versions, .drop-hint,
  .block-ctrl, .resume-drop-marker { display: none !important; }

  /* 模式标记**一样都不上纸**（2026-09-13 改，openspec 变更
     print-clean-mode-marks）。在这之前页脚那行字是刻意打在纸上的，理由是
     "导出物也要能区分模式"；现在的判断是那行字只会跟着 PDF 进到 HR 手里，
     对读的人没意义、对写的人只有损害。标记留在屏幕上。

     三条写成 !important 是因为上面那条隐藏规则列得很长，而下面那条
     `.resume-sheet { border: none }` 的选择器比 `[data-mode]` 弱一级——
     不写 !important 的话，色条会照样印出来。 */
  .resume-mode-badge, .resume-mode-note { display: none !important; }
  .resume-sheet[data-mode] { border: none !important; }
  /* 证件照上那层悬停提示是给屏幕看的。纸上不会有 hover，但 ::after 的盒子
     仍然在——写死隐藏，免得哪天 opacity 之外又加了别的样式。 */
  .resume-photo-slot::after { display: none !important; }
  .resume-photo-slot { transform: none !important; box-shadow: none !important; }
  main { display: block; padding: 0; gap: 0; }
  #panel-resume { border: none; box-shadow: none; border-radius: 0; overflow: visible; }
  #panel-resume > .body { overflow: visible; padding: 0; }
  /* 屏幕上的缩放在纸上取消：纸本来就是 210mm，1:1 打印。
     #resume-render 的高度是 resume.js 为缩放后的纸留的位置，打印时不需要。 */
  #resume-render { width: 210mm; height: auto !important; }
  .resume-sheet { transform: none !important; }
  .resume-sheet {
    /* 打印时不再按屏幕容器缩放：直接铺满整张 A4，边距用 LaTeX 版原来的
       左右 margin_cm / 上 1.1cm / 下 1cm 这一组值。
       min-height 是 resume.js 按屏幕宽度内联设的 px 值，打印时必须覆盖掉
       （内联样式只有 !important 压得住）。 */
    /* 尺寸不在这里重写：纸上一切都从 --u 派生、--u 跟纸宽成正比，
       所以把宽度设成 210mm 之后，整套版面自动按 A4 等比放大，屏幕上
       二分出来的那个 --density 在这里依然成立。 */
    width: 210mm;
    /* 高度不写死成 297mm：写死时纸正好等于一个页面盒（1122.52px），
       任何亚像素取整都会让文档总高变成 1123px，浏览器就为这 0.48px 再排
       一整页——导出的 PDF 第二页全白。让它按内容自然撑开，取整永远顶不
       过一页。2026-09-11 修。 */
    height: auto;
    min-height: 0 !important;
    border: none; border-radius: 0; box-shadow: none;
    /* 强制打印背景色：能力标签那种浅底色块靠它，否则打印出来只剩文字。
       这条不影响上面那根线（线已经改成 border，本来就不吃这个开关）。 */
    print-color-adjust: exact;
    -webkit-print-color-adjust: exact;
  }
  /* 超页的虚线标记是给屏幕上看的，打印时不要 */
  .resume-sheet.overflowing::after { display: none !important; }
  .resume-entry, .resume-edu-line, .resume-section { break-inside: avoid; }
  .resume-section > h3 { break-after: avoid; }
}

/* ---------- 档案直接编辑（2026-09-10 profile-inline-editing） ---------- */
.entry-card { padding-right: 11px; }
.entry-actions {
  display: none;
  gap: 8px;
  margin-top: 6px;
  padding-top: 5px;
  border-top: 1px solid var(--border-soft);
}
.entry-card:hover .entry-actions, .entry-card:focus-within .entry-actions { display: flex; }
.link-btn {
  background: none; border: none; padding: 0; cursor: pointer;
  font-size: var(--fs-6); color: var(--accent);
}
.link-btn:hover { text-decoration: underline; }

/* 档案页上那些 ✓ 的抬头：说清楚它们是按哪份 JD 标的，并给一个关掉的出口。
   它不是一条警告，只是一句出处，所以颜色压到 muted、不占视觉重量。 */
.hit-legend {
  display: flex; align-items: center; gap: var(--sp-2);
  margin-bottom: var(--sp-2);
  font-size: var(--fs-7); color: var(--muted);
}
.link-btn.danger { color: var(--danger); }
.root-card { position: relative; }
.root-card .root-edit { position: absolute; top: 9px; right: 11px; }

.edit-modal { width: min(460px, 92vw); max-height: 84vh; display: flex; flex-direction: column; }
.edit-modal .edit-form { overflow-y: auto; margin: 4px 0 14px; padding-right: 4px; }
.form-row { margin-bottom: 10px; }
.form-row label { display: block; font-size: var(--fs-6); color: var(--muted); margin-bottom: 3px; }
.form-row input[type="text"], .form-row textarea, .form-row select {
  width: 100%; font: inherit; font-size: var(--fs-5);
  padding: 6px 8px; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: #fff; color: var(--text); resize: vertical;
}
.form-row input:disabled, .form-row textarea:disabled { background: var(--bg); color: var(--muted-soft); }
.form-row input:focus, .form-row textarea:focus, .form-row select:focus {
  outline: none; border-color: var(--accent);
}
.field-error, .form-error-summary { color: var(--danger); font-size: var(--fs-6); margin-top: 3px; line-height: 1.5; }
.form-section-title { font-size: var(--fs-6); color: var(--muted); font-weight: 600; margin: 14px 0 6px; }
.modal-actions button.danger { background: var(--danger); }

/* ---------- 自动保存状态（2026-09-10 resume-autosave） ---------- */
.resume-toolbar .save-flag { font-size: var(--fs-6); padding: 3px 8px; border-radius: 999px; }
.resume-toolbar .save-flag.pending { color: var(--muted); background: var(--bg); }
.resume-toolbar .save-flag.failed { color: var(--danger); background: var(--danger-soft); }

/* ---------- 文档导入审阅（2026-09-11 document-import-review） ---------- */
/* 2026-09-13 这一行从「我的档案」面板顶部挪到了对话输入框上方（理由见
   index.html 那段注释）。它现在跟模式选择器是同一层东西：输入框上面的两条
   辅助行，所以内边距对齐 .mode-row。 */
.import-row {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--sp-2); padding: var(--sp-2) var(--sp-4) 0; font-size: var(--fs-5); color: var(--muted);
}
.import-file { cursor: pointer; }
/* 回形针 + 文字。图标跟着文字颜色走（currentColor），所以悬停变色时一起变。 */
.import-row .link-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
.import-row .link-btn svg { width: 14px; height: 14px; display: block; }

.import-review {
  position: fixed; inset: 0; z-index: 50;
  background: rgba(0,0,0,.35);
  display: flex; align-items: flex-start; justify-content: center;
  padding: 40px 16px; overflow: auto;
}
.import-review[hidden] { display: none !important; }
.import-card {
  background: #fff; border-radius: var(--radius); padding: 20px;
  width: 100%; max-width: 680px; box-shadow: var(--shadow-md);
}
.import-card h4 { margin: 0 0 4px; font-size: var(--fs-3); }

.import-row-head { display: flex; align-items: center; gap: 8px; }
.import-row-title { font-weight: 600; font-size: var(--fs-4); flex: 1; }
.import-review .import-row,
.import-basic-row {
  display: block; padding: 7px 0; border-bottom: 1px solid var(--border-soft);
}
.import-basic-row {
  display: flex; align-items: center; gap: 8px; font-size: var(--fs-5);
}
.import-basic-row .basic-now { color: var(--muted); }
.import-basic-row .basic-new { color: var(--accent); }
/* 导入过程的状态行（2026-09-13）。
   之前 #import-status 一点样式都没有，紧挨着"支持 .docx / .pdf…"那行灰字接着
   排，看起来像同一句话的后半截——用户反馈"居然没有任何状态显示"，其实是有字
   但看不出来。现在它独占一行，忙的时候左边有个会呼吸的点。 */
.import-status {
  display: flex; align-items: center; gap: 6px;
  flex-basis: 100%; margin-top: 2px;
  font-size: var(--fs-5); color: var(--muted);
}
.import-status::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent); flex: none;
}
.import-status.is-busy::before { animation: import-pulse 1.1s ease-in-out infinite; }
.import-status.is-error { color: var(--danger); }
.import-status.is-error::before { background: var(--danger); }
@keyframes import-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: .35; transform: scale(.7); }
}
@media (prefers-reduced-motion: reduce) {
  .import-status.is-busy::before { animation: none; }
}

/* 「还需要补充」：抽出来了但缺一个必填字段的记录。它是清单里唯一需要用户
   动手的部分，所以给一层底色把它跟"勾一下就行"的那些分开。 */
.import-pending {
  margin: 4px 0 10px; padding: 10px 12px;
  background: var(--accent-soft, rgba(233,82,127,.06));
  border: 1px solid var(--border-soft); border-radius: 8px;
}
.import-pending .sub-line { margin: 2px 0 6px; }
.import-review .import-row.is-pending { border-bottom: none; }
.pending-flag {
  font-size: var(--fs-6); padding: 1px 7px; border-radius: 999px;
  color: var(--danger); background: var(--danger-soft);
}
/* 缺的那个输入框自己指出来，比让用户对着一行提示去数是哪个快 */
.import-pending .is-missing {
  border-color: var(--danger);
  background: var(--danger-soft);
}

/* 疑似重复的候选：默认不勾，视觉上也压低一档，但不隐藏——用户可以坚持导入 */
.import-review .import-row.is-dup { opacity: .72; }
.dup-flag {
  font-size: var(--fs-6); color: #B26A00;
  border: 1px solid currentColor; border-radius: 9px; padding: 0 6px;
}
.import-actions {
  display: flex; align-items: center; gap: 8px;
  margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border);
}
.import-count { flex: 1; font-size: var(--fs-5); color: var(--muted); }
@media print { .import-review { display: none !important; } }

/* ---------- 空态引导与模式说明（2026-09-11 first-run-guidance） ---------- */
.first-run {
  border: 1px dashed var(--accent);
  border-radius: var(--radius);
  padding: 12px 14px;
  margin-bottom: 12px;
  background: #FAFBFF;
}
.first-run-title { font-size: var(--fs-4); font-weight: 600; margin-bottom: 6px; }
.first-run-title + button { margin-bottom: 4px; }
.first-run-note { font-size: var(--fs-6); color: var(--muted); margin-bottom: 10px; }
.first-run-note:last-child { margin-bottom: 0; }

.mode-help {
  width: 18px; height: 18px; padding: 0; line-height: 1;
  border-radius: 50%; font-size: var(--fs-6);
  background: none; color: var(--muted); border: 1px solid var(--border);
  cursor: pointer;
}
.mode-help:hover { color: var(--accent); border-color: var(--accent); }

.mode-explainer {
  flex-basis: 100%;
  margin-top: 8px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: #fff;
  box-shadow: var(--shadow-sm);
  font-size: var(--fs-5);
}
.explainer-row { display: flex; gap: 8px; margin-bottom: 5px; line-height: 1.5; }
.explainer-label {
  flex: 0 0 60px; color: var(--muted); font-size: var(--fs-6);
}
.explainer-label.polished { color: #B26A00; font-weight: 600; }
@media print { .first-run, .mode-explainer { display: none !important; } }

/* ============================================================
   组件层（2026-09-11 第三版）

   放在文件末尾整段覆盖，而不是回去逐条改原有规则——原有规则里混着大量
   跟版面相关的声明（padding/grid/flex），逐条动的回归面比整段覆盖大得多。
   这里**只覆盖观感属性**：背景、圆角、边框、颜色、过渡。

   一条自律：这一层里**不允许出现 linear-gradient**，卡片和面板**不允许
   出现 box-shadow**。前两版就是从"给这块加个渐变"开始一路油下去的。

   `.resume-sheet` 及其后代一律不在这里出现。
   ============================================================ */

body {
  /* 字体退回原来的系统栈——不引 webfont、不换圆体 */
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
               "Microsoft YaHei", sans-serif;
  background: var(--bg);
  color: var(--text);
}

/* ---------- 顶部 ---------- */
header {
  background: var(--panel-bg);
  border-bottom: 1px solid var(--border);
  box-shadow: none;
  backdrop-filter: none;
}
/* logo 现在是一张图，不再需要底色——铺一层粉色底反而会在狐狸周围露出一圈。 */
.brand-mark { border-radius: var(--radius-sm); background: none; }

/* 左上角那只狐狸是可点的（连点 7 下解锁彩蛋模式）。这里只给"能点"的手感，
   **不给任何暗示它有功能的视觉**——彩蛋被明示出来就不是彩蛋了。
   user-select 关掉是因为连点会把 logo 和标题一起选中，很难看。 */
.brand-mark.is-tappable { cursor: pointer; user-select: none; -webkit-user-drag: none; }
.brand-mark.is-bouncing { animation: fox-bounce .32s ease; }
@keyframes fox-bounce {
  0%   { transform: scale(1)    rotate(0deg); }
  35%  { transform: scale(1.14) rotate(-7deg); }
  70%  { transform: scale(.97)  rotate(4deg); }
  100% { transform: scale(1)    rotate(0deg); }
}
@media (prefers-reduced-motion: reduce) {
  .brand-mark.is-bouncing { animation: none; }
}
.brand-text h1 { color: var(--text); }
.brand-text .tagline { color: var(--muted); }

/* ---------- 按钮 ---------- */
header button, .toolbar button, .import-actions button,
.modal-actions button, .first-run button, .chat-input-row button,
#profile-body button:not(.link-btn), .import-row button:not(.link-btn) {
  border: none;
  border-radius: var(--radius-sm);
  font-family: inherit;
  font-weight: 500;
  cursor: pointer;
  color: #fff;
  background: var(--accent-fill);
  box-shadow: none;
  transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
header button:hover, .toolbar button:hover, .import-actions button:hover,
.modal-actions button:hover, .first-run button:hover,
.chat-input-row button:hover,
.resume-toolbar button:not(.link-btn):hover,
#profile-body button:not(.link-btn):hover,
.import-row button:not(.link-btn):hover {
  background: var(--accent-fill-hover);
  transform: none;
}
/* .secondary 这条必须**自带全部**观感属性，不能只改颜色就指望继承上面那条
   规则——「刷新」按钮挂在 .panel > h2 底下，上面那串祖先够不到它，只补颜色
   的话它的 border/圆角还是浏览器默认的 2px outset 黑框。祖先全列一遍是为了
   压过 base 层里 header button.secondary 的权重。 */
header button.secondary, .toolbar button.secondary,
.import-actions button.secondary, .modal-actions button.secondary,
#profile-body button.secondary, .resume-toolbar button.secondary,
.panel > h2 button.secondary, .import-row button.secondary,
button.secondary {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-family: inherit;
  font-weight: 500;
  cursor: pointer;
  background: #fff;
  color: var(--text);
  box-shadow: none;
}
header button.secondary:hover, .toolbar button.secondary:hover,
.import-actions button.secondary:hover, .modal-actions button.secondary:hover,
#profile-body button.secondary:hover, .resume-toolbar button.secondary:hover,
.panel > h2 button.secondary:hover, .import-row button.secondary:hover,
button.secondary:hover {
  background: var(--bg-tint);
  color: var(--accent);
  border-color: var(--border);
}
button:disabled, .chat-input-row button:disabled {
  background: var(--bg-tint) !important;
  color: var(--muted-soft) !important;
  border-color: var(--border) !important;
  cursor: not-allowed;
}
button:focus-visible, .link-btn:focus-visible, summary:focus-visible,
input:focus-visible, textarea:focus-visible, select:focus-visible {
  outline: none;
  box-shadow: var(--ring);
}

.status {
  background: var(--bg-tint);
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 999px;
  box-shadow: none;
  font-weight: 400;
}

/* ---------- 面板：白卡 + 1px 描边，没有阴影也没有彩色标题条 ---------- */
.panel {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel-bg);
  box-shadow: none;
  overflow: hidden;
}
/* 上一版给三栏各配了一条彩色标题条（粉/薄荷/长春花），那是"油"的主要来源。
   现在标题就是一行字加一条细线，三栏靠内容区分，不靠颜色。 */
.panel > h2 {
  background: var(--panel-bg);
  border-bottom: 1px solid var(--border);
  font-size: var(--fs-3);
  font-weight: 600;
  color: var(--text);
}
.panel > h2 .icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px;
  background: none;
  box-shadow: none;
  color: var(--muted);
}
.panel > h2 .icon svg { width: 16px; height: 16px; display: block; }

/* ---------- 聊天气泡 ---------- */
/* 2026-09-12 改回圆形。这一层当初把它拉成 6px 圆角是为了跟卡片的方角统一，
   但聊天头像是**人**的位置，圆形是所有人都认得的约定；接上真实证件照之后
   方角更明显地不对（一张方脸贴在气泡旁边）。 */
.avatar { box-shadow: none; color: #fff; border-radius: 50%; }
/* AI 头像用 logo。背景设成白色而不是透明：这张图是带透明通道的，压在浅灰
   底上边缘会发灰。 */
.avatar.assistant {
  background: #fff;
  overflow: hidden;
  border: 1px solid var(--border-soft);
}
.avatar.assistant img { width: 100%; height: 100%; object-fit: contain; display: block; }
.avatar.user { background: var(--accent-fill); }
/* 传过证件照就用照片当头像（2026-09-12）。底色要去掉——照片是圆角矩形裁出来
   的，留着底色会在边缘露出一圈粉边。 */
.avatar.user.has-photo { background: none; overflow: hidden; }
.avatar.user.has-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.msg { border-radius: var(--radius); box-shadow: none; }
.msg.user { background: var(--accent-fill); color: #fff; }
.msg.assistant { background: var(--bg-tint); color: var(--text); border: none; }
.msg.system {
  background: var(--warn-soft);
  border: 1px solid #F5DFD6;
  color: var(--warn);
}
.typing-dots span { background: var(--muted-soft); }

/* ---------- 输入框：灰底，聚焦才变白 ---------- */
.chat-input-row textarea,
.form-row input, .form-row textarea, .form-row select {
  /* 2026-09-14：原来是 transparent，只靠一层浅底色标出输入区。弹层本身是
     白底，那层底色差得太小——真机报告的原话是"边框极弱、近乎不可见，空字段
     看起来像缺失控件"。一个空输入框必须自己说明"这里能填字"，这件事不能
     指望背景色。 */
  border: 1px solid var(--border) !important;
  border-radius: var(--radius-sm);
  background: var(--bg-tint);
  color: var(--text);
  font-family: inherit;
  box-shadow: none;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}
.chat-input-row textarea:focus, .form-row input:focus,
.form-row textarea:focus, .form-row select:focus {
  outline: none;
  background: #fff;
  border-color: var(--accent) !important;
  box-shadow: none;
}

/* ---------- 档案卡片 ---------- */
.entry-card {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: #fff;
  box-shadow: none;
  margin-bottom: 8px;
  transition: border-color 0.15s ease, background-color 0.15s ease;
}
.entry-card:hover { background: var(--bg-tint); }
/* 命中着色：上一版是整块填满饱和色，太抢。改成**左边一条色条 + 极淡底色**，
   两档仍然是两个色相（薄荷=直接命中、珊瑚=可迁移），语义区分不能并成一个色。 */
.entry-card.hit-direct {
  background: var(--hit-soft);
  border-color: var(--border);
  border-left: 3px solid var(--c-sage);
}
.entry-card.hit-transferable {
  background: var(--warn-soft);
  border-color: var(--border);
  border-left: 3px solid var(--c-coral);
}
.kind-group > .kind-title { color: var(--muted); font-weight: 500; }
.root-card {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: #fff;
  box-shadow: none;
}
.badge { font-weight: 400; box-shadow: none; border-radius: var(--radius-sm); }
.badge.satisfied { background: var(--hit-soft); color: var(--hit); border: 1px solid var(--c-sage); }
.badge.unsatisfied { background: var(--warn-soft); color: var(--warn); border: 1px solid var(--c-coral); }

/* ---------- 弹窗 / 浮层 / 导入清单：只有这些浮起来的东西才给阴影 ---------- */
.modal-card, .import-card, .hit-popover, .mode-explainer {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: #fff;
  box-shadow: var(--shadow-pop);
}
.modal-backdrop, .import-review { background: rgba(0, 0, 0, 0.4); backdrop-filter: none; }
.import-row, .import-basic-row { border-bottom: 1px solid var(--border-soft); }
.dup-flag, .mode-flag { border-radius: var(--radius-sm); box-shadow: none; }
/* .link-btn 是"长得像链接的按钮"（档案卡片上的编辑/删除）。上面那条按容器挂
   的规则会把它们刷成实心按钮，所以那里逐个 :not(.link-btn) 排除，这里再把它
   自己的样子写死一遍，免得以后又被别的规则扫到。 */
.link-btn {
  background: none !important;
  border: none !important;
  box-shadow: none !important;
  padding: 0;
  color: var(--accent);
  font-weight: 400;
  cursor: pointer;
}
.link-btn:hover { color: var(--accent-strong); text-decoration: underline; }
.hit-popover .tier { color: var(--muted); }

.first-run {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-tint);
  box-shadow: none;
}
.first-run-title { color: var(--text); }

input[type="file"] { font-size: var(--fs-5); color: var(--muted); }
input[type="file"]::file-selector-button {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 5px 12px;
  margin-right: 9px;
  font-size: var(--fs-5);
  font-weight: 500;
  font-family: inherit;
  color: var(--text);
  background: #fff;
  box-shadow: none;
  cursor: pointer;
}
input[type="file"]::file-selector-button:hover { background: var(--bg-tint); color: var(--accent); }

.panel .body::-webkit-scrollbar { width: 8px; }
.panel .body::-webkit-scrollbar-thumb {
  background: #C9CCD0;
  border-radius: 999px;
  border: 2px solid transparent;
  background-clip: content-box;
}
.panel .body::-webkit-scrollbar-thumb:hover { background: #9499A0; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

@media print {
  body { background: #fff !important; }
  .panel {
    background: #fff !important;
    box-shadow: none !important;
    border: none !important;
    border-radius: 0 !important;
  }
}

/* ============================================================
   档案面板的三处交互返工（2026-09-11）
   ============================================================ */

/* 0. 先补一条全局的：任何自己声明了 display 的元素，`hidden` 属性都压不住它
   ——UA 的 `[hidden]{display:none}` 权重只有 0,0,1，随便一个类选择器都盖过去。
   这个坑这个项目已经踩过一次（`.import-review[hidden]` 那条 !important 就是
   当时的补丁），这次证件照弹窗的空态又踩了第二次。与其每次单独打补丁，不如
   在这里一次性写死。 */
[hidden] { display: none !important; }

/* 1. 编辑/删除从卡片正文流里挪到右上角。
   原来它插在标题和 highlights **中间**，hover 才 display:flex——于是鼠标
   一进卡片，下面所有内容整体往下跳一截，整列都在动。绝对定位之后它不占
   文档流，显隐不再引起任何重排。

   用 opacity 而不是 display:none，是为了键盘还能 Tab 进去：display:none
   的元素拿不到焦点，:focus-within 那条规则就永远不会触发。 */
.entry-actions {
  position: absolute;
  top: 4px;
  right: 6px;
  display: flex;
  gap: 10px;
  margin: 0;
  padding: 3px 7px;
  border-top: none;
  border-radius: var(--radius-sm);
  /* 悬停时这排按钮会盖在标题上。底色取卡片自己的（命中卡片是淡绿/淡橙），
     不写死白色，否则在命中卡片上会露出一块白斑。 */
  background-color: inherit;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease;
}
.entry-card:hover .entry-actions,
.entry-card:focus-within .entry-actions { opacity: 1; pointer-events: auto; }
/* 命中卡片的右上角已经被 .hit-tag 占着，往下让一行 */
.entry-card.hit-direct .entry-actions,
.entry-card.hit-transferable .entry-actions { top: 22px; }

/* 2. 头像与个人基础信息卡并排 */
.profile-header {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  margin-bottom: 16px;
}
.profile-header > .root-card,
.profile-header > .empty-hint {
  flex: 1;
  margin-bottom: 0;
  min-width: 0;          /* 不加这条，长邮箱会把 flex 项撑破、挤没头像 */
}

.avatar-slot {
  position: relative;
  flex-shrink: 0;
  width: 64px;
  height: 80px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--bg-tint);
  overflow: hidden;
  cursor: pointer;
  transition: border-color 0.15s ease;
}
/*
  悬停时轻轻放大（2026-09-13）。

  用户原话：「鼠标放到头像处没有任何反应，虽然点击可以更改头像，但是没有任何
  提示看起来完全不像可以交互的」。原来只有一条 border-color——1px 的描边换个
  颜色，在一个 64×80 的小块上几乎看不见。

  用 transform 而不是改宽高：transform 不触发重排，旁边那张个人信息卡不会跟着
  动一下。1.06 是"看得出来但不跳"的量；0.22s ease-out 让它是"渐渐"变大而不是
  弹出来。cursor 早就是 pointer 了，但一个不动的东西光有手型指针不够说服人。
*/
.avatar-slot {
  transform-origin: center;
  transition: border-color 0.15s ease, transform 0.22s ease-out, box-shadow 0.22s ease-out;
}
.avatar-slot:hover,
.avatar-slot:focus-visible {
  border-color: var(--accent);
  transform: scale(1.06);
}
/* 悬停时右下角冒一行小字。光放大只说明"它活着"，说不出"点了会发生什么"。 */
.avatar-slot::after {
  content: "换一张";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 2px 0 3px;
  text-align: center;
  font-size: var(--fs-7);
  line-height: 1.4;
  color: #fff;
  background: rgba(24, 25, 28, 0.66);
  opacity: 0;
  transition: opacity 0.22s ease-out;
  pointer-events: none;
}
.avatar-slot:hover::after,
.avatar-slot:focus-visible::after { opacity: 1; }
/* 还没传过照片的那个空框自己就写着提示，不用再叠一层 */
.avatar-slot.no-photo::after { content: none; }
@media (prefers-reduced-motion: reduce) {
  .avatar-slot:hover, .avatar-slot:focus-visible { transform: none; }
}
.avatar-slot:focus-visible { outline: none; box-shadow: var(--ring); }
.avatar-slot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.avatar-placeholder {
  display: none;
  position: absolute;
  inset: 0;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-6);
  color: var(--muted-soft);
}
/* 没有证件照时：图片藏掉、占位框顶上。**不能只藏图片**——那样这块区域
   什么都没有，用户没有入口传第一张。 */
.avatar-slot.no-photo img { display: none; }
.avatar-slot.no-photo { border-style: dashed; }
.avatar-slot.no-photo .avatar-placeholder { display: flex; }

/* 3. 证件照弹窗 */
.photo-modal { width: min(320px, 92vw); text-align: center; }
.photo-modal h4 { margin: 0 0 12px; }
.photo-modal-img {
  width: 150px;
  height: 188px;         /* 1.25 —— 跟缩略图同比例，别让人看到两个形状 */
  object-fit: cover;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--bg-tint);
}
.photo-modal-empty {
  width: 150px;
  height: 188px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px dashed var(--border);
  border-radius: var(--radius-sm);
  color: var(--muted-soft);
  font-size: var(--fs-5);
}
.photo-modal .sub-line { min-height: 16px; margin-top: 8px; }
.photo-modal .modal-actions { justify-content: center; }

/* ============================================================
   简历就地编辑（2026-09-12 openspec 变更 resume-inline-text-editing）

   **只在屏幕上有提示，纸上一点痕迹都不能留。**下面所有规则都在
   @media print 里被关掉——虚线框、背景色、光标样式印出来就是废纸。

   另一条硬约束：编辑态**不许改变行高与字号**。这张纸的排版密度是靠实测
   DOM 高度二分出来的，一进编辑就把行高撑开，整页会当场跳一下，而且用户
   改完退出又跳回去。所以下面只动颜色和轮廓，不动任何影响布局的属性。
   ============================================================ */
.resume-sheet .editable-text {
  cursor: text;
  border-radius: 2px;
  /* 用 box-shadow 画轮廓而不是 border/outline-offset：它不占据布局空间，
     不会把这一行推开。 */
  box-shadow: none;
  transition: background-color 0.12s ease, box-shadow 0.12s ease;
}
.resume-sheet .editable-text:hover {
  background-color: rgba(216, 30, 91, 0.07);
}
.resume-sheet .editable-text:focus,
.resume-sheet .editable-text.editing {
  outline: none;
  background-color: rgba(216, 30, 91, 0.06);
  box-shadow: 0 0 0 1px rgba(216, 30, 91, 0.45);
}
.resume-sheet .editable-text:focus-visible {
  box-shadow: 0 0 0 2px rgba(216, 30, 91, 0.55);
}

@media print {
  .resume-sheet .editable-text,
  .resume-sheet .editable-text:hover,
  .resume-sheet .editable-text:focus,
  .resume-sheet .editable-text.editing {
    background: none !important;
    box-shadow: none !important;
    outline: none !important;
    cursor: auto !important;
  }
}

/* ---------- 同步询问弹窗（2026-09-12 profile-resume-edit-sync） ---------- */
.sync-modal { width: min(460px, 92vw); }
.sync-diff {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  margin: 10px 0;
  font-size: var(--fs-5);
  line-height: 1.6;
}
.sync-diff + .sync-diff { margin-top: 8px; }
.sync-row { display: flex; gap: 10px; padding: 8px 10px; }
.sync-row + .sync-row { border-top: 1px solid var(--border-soft); }
/* 两行**不用红绿**：这不是"删掉/加上"，是"两份都合法、你选一份"。
   用绿色标新值会暗示旧值是错的，而档案里那句往往才是用户的原话。 */
.sync-row-new { background: var(--bg-tint); }
.sync-label { flex: 0 0 74px; color: var(--muted); }
.sync-text { flex: 1; min-width: 0; color: var(--text); word-break: break-word; }
.sync-note {
  margin: 8px 0 0;
  font-size: var(--fs-5);
  line-height: 1.6;
  color: var(--warn);
  background: var(--warn-soft);
  border: 1px solid #F5DFD6;
  border-radius: var(--radius-sm);
  padding: 7px 9px;
}

/* ===== 登录门（2026-09-12 openspec 变更 account-auth） =====
   盖住整个界面的一层，而不是另一个页面（理由见 index.html 那段注释）。
   沿用这一层的自律：不用 linear-gradient、卡片不加 box-shadow，靠 1px 描边
   和底色分层——登录页最容易被做成一张"很有设计感"的卡，那会跟主界面脱节。 */
.auth-gate {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  /* 不透明：未登录时主界面不该透出来，哪怕只是一片灰色的骨架——那等于告诉
     访客"这里面有东西"，也会让人以为是加载卡住了。 */
  background: var(--bg);
}
.auth-card {
  width: 100%;
  max-width: 360px;
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 26px 24px 20px;
}
.auth-card h2 { margin: 0 0 6px; font-size: var(--fs-2); }
.auth-sub {
  margin: 0 0 18px;
  font-size: var(--fs-5);
  line-height: 1.7;
  color: var(--muted);
}
/* 邀请链接填好码之后的一行说明。语气要像"已经替你办好了"，不是"这里有个
   你要填的字段"——收到链接的人不需要知道邀请码是什么。 */
/* 「选填」这种字段标注：比在 label 里写"（可选）"更轻，也更容易一眼扫到。 */
.field-optional {
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: 999px;
  font-size: var(--fs-6);
  font-weight: 400;
  color: var(--muted-soft);
  background: var(--bg);
}
.auth-invite-hint {
  margin: 6px 0 0;
  font-size: var(--fs-5);
  line-height: 1.6;
  color: var(--muted);
}
.auth-card label {
  display: block;
  margin: 0 0 5px;
  font-size: var(--fs-5);
  color: var(--muted);
}
.auth-card input {
  width: 100%;
  box-sizing: border-box;
  padding: 8px 10px;
  margin-bottom: 14px;
  font: inherit;
  font-size: var(--fs-4);
  color: var(--text);
  background: #fff;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.auth-card input:focus {
  outline: none;
  border-color: var(--accent);
}
.auth-card button[type="submit"] {
  width: 100%;
  padding: 9px 12px;
  font: inherit;
  font-size: var(--fs-4);
  color: #fff;
  background: var(--accent-fill);
  border: 1px solid var(--accent-fill);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.auth-card button[type="submit"]:hover {
  background: var(--accent-fill-hover);
  border-color: var(--accent-fill-hover);
}
.auth-card button[type="submit"]:disabled { opacity: .6; cursor: default; }
.auth-card .link-btn { margin-top: 12px; font-size: var(--fs-5); }
.auth-error {
  margin: 0 0 12px;
  padding: 7px 9px;
  font-size: var(--fs-5);
  line-height: 1.6;
  color: var(--danger);
  background: var(--danger-soft);
  border: 1px solid #F3D6D2;
  border-radius: var(--radius-sm);
}
/* 顶栏的"当前是谁"。弱化处理：它是状态不是功能，不该比旁边的按钮更显眼。 */
.whoami {
  font-size: var(--fs-5);
  color: var(--muted);
  padding: 2px 6px;
  /* 它现在是个 <button>，得把浏览器默认的按钮长相全部清掉——顶栏那串
     "header button" 规则够不着它（那条挂在 header button 上，而这个也是
     header button…所以反过来要显式压过去）。 */
  background: none !important;
  border: 1px solid transparent !important;
  border-radius: var(--radius-sm);
  font-family: inherit;
  font-weight: 400;
  cursor: pointer;
  box-shadow: none !important;
}
.whoami:hover { color: var(--text); border-color: var(--border) !important; }
.whoami:focus-visible { outline: none; box-shadow: var(--ring) !important; }

/* 点用户名弹出来的那张卡。跟反馈气泡同一套做法：fixed + 手算坐标，因为顶栏
   不是定位上下文，而且这一层要盖在所有面板之上。 */
.account-card {
  position: fixed;
  z-index: 150;
  min-width: 180px;
  max-width: calc(100vw - 24px);
  padding: var(--sp-3);
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-pop);
}
.account-card-name {
  font-size: var(--fs-6);
  color: var(--muted-soft);
  padding-bottom: var(--sp-2);
  margin-bottom: var(--sp-2);
  border-bottom: 1px solid var(--border-soft);
  word-break: break-all;
}
/* 卡上的动作一律做成整行的文字链，不是实心按钮。

   这张卡是个菜单，不是一个"请选择"的对话框——实心按钮会把其中一项说成
   "主要操作"，而这三项里没有哪一项是我们希望他多点的。

   2026-09-13 起有三项（反馈建议 / 退出登录 / 注销账号），从轻到重排。
   只有最重的那个是红的：三个都红等于没有红。 */
.account-card .card-action,
.account-card .danger-link {
  display: block;
  width: 100%;
  text-align: left;
  background: none !important;
  border: none !important;
  box-shadow: none !important;
  padding: var(--sp-2) 0;
  font: inherit;
  font-size: var(--fs-5);
  cursor: pointer;
  border-radius: 0;
}
.account-card .card-action { color: var(--text); }
.account-card .card-action:hover { color: var(--accent); }
/* 注销跟上面两项之间画一条线：它跟"说句话""这次不用了"不是一类事 */
.account-card .danger-link {
  margin-top: var(--sp-2);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--border-soft) !important;
}

/* 红色那一项自己的颜色。**只写颜色，别把上面那条重复一遍**——两条同权重、
   这条在后面，重复的属性会静默盖掉上面刚设的 padding-top 和分隔线。同权重
   后来居上这个坑今天已经踩过两次了。 */
.account-card .danger-link { color: var(--danger); }
.account-card .danger-link:hover { text-decoration: underline; }

/* ===== 技术栈标签的编辑（2026-09-12） =====
   纸上的样子一个像素都不变：下面这些全是屏幕上的悬停态，`@media print` 里
   统一关掉。这条纪律不是洁癖——排版密度是实测 DOM 高度二分收敛出来的，
   任何在屏幕上占了流内空间的控件都会让打印排版跟着走样。 */
.resume-tag-editable .tag-ctrl {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(100% + 2px);
  display: none;
  gap: 2px;
  white-space: nowrap;
  z-index: 3;
}
.resume-tag-editable:hover .tag-ctrl,
.resume-tag-editable:focus-within .tag-ctrl { display: flex; }
.resume-tag-editable .tag-ctrl button {
  border: 1px solid var(--border);
  background: #fff;
  color: var(--muted);
  border-radius: 4px;
  font-size: var(--fs-7);
  line-height: 1;
  padding: 2px 4px;
  cursor: pointer;
  font-family: inherit;
}
.resume-tag-editable .tag-ctrl button:last-child:hover {
  color: var(--danger);
  border-color: var(--danger);
}
/* 整行编辑（2026-09-13 下午）。**没有悬停控件**——一行就是一个可编辑的
   文本块，点进去直接改，改完顿号分隔的每一段各自认回自己那条记录。
   上午那套逐项的 ‹ › ✕ 已经删掉：两套编辑模式并存只会让人不知道该点哪儿。 */
.resume-cap-items { display: inline; }
/* 行尾的「+」：平时不占位（display:none），悬停这一行才出现。那时行高已经
   由现有标签撑起来了，所以出现的瞬间不会把版面顶一下。 */
.resume-tag-add {
  display: none;
  border: 1px dashed var(--r-accent);
  background: transparent;
  cursor: pointer;
  font: inherit;
  font-weight: 400;
  line-height: inherit;
}
.resume-stack:hover .resume-tag-add,
.resume-stack:focus-within .resume-tag-add { display: inline-block; }
.resume-tag-add:hover { background: var(--r-tagbg); }
/* 标签里的文字块是 inline 的可编辑节点，编辑时不要撑破标签的圆角底色 */
.resume-tag-text { display: inline-block; min-width: 1ch; outline: none; }

@media print {
  .resume-tag-editable .tag-ctrl,
  .resume-tag-add { display: none !important; }
}

/* 一闪而过的提示（日期格式不对之类）。挂在 body 上、fixed 定位——简历的
   排版密度是实测 DOM 高度二分出来的，往简历里塞临时元素会把版面顶一下，
   还会被那次测量算进去。 */
.resume-hint-toast {
  position: fixed;
  left: 50%;
  transform: translateX(-50%) translateY(8px);
  z-index: 200;
  max-width: 420px;
  padding: 9px 14px;
  font-size: var(--fs-5);
  line-height: 1.6;
  color: #fff;
  background: rgba(24, 25, 28, 0.92);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease, transform 0.18s ease;
}
/* 两个位置：就地编辑的反馈走 bottom，整份简历被换掉走 top。
   top 定在 18% 而不是垂直居中——正中会盖住简历标题那一块，而那正是用户
   看到"更新了"之后第一眼要确认的地方。 */
.resume-hint-toast.is-bottom { bottom: 28px; }
/* 2026-09-13 从 18% 提到 7%：18% 在常见窗口高度上正好落在简历标题那一带，
   而那恰恰是用户看到"更新了"之后第一眼要确认的地方——提示自己挡住了它要
   指向的东西。7% 贴在面板顶部下面一点，谁都不挡。 */
.resume-hint-toast.is-top { top: 7%; }

/* 顶部那条是**成功**提示（简历已更新 / 已换回上一版 / 反馈已收到），所以
   用命中那一档的浅绿，跟档案卡片上"直接命中"是同一个视觉语言：绿=这件事成了。
   底部那条留着深色——它报的是问题（日期格式不对之类），两种消息不该长一样。 */
.resume-hint-toast.is-top {
  color: var(--hit);
  background: var(--hit-soft);
  border-color: var(--c-sage);
  font-weight: 500;
}
.resume-hint-toast.is-top.visible { transform: translateX(-50%) translateY(0); }
.resume-hint-toast.is-bottom.visible { transform: translateX(-50%) translateY(0); }
.resume-hint-toast.visible { opacity: 1; }
@media print { .resume-hint-toast { display: none !important; } }
@media (prefers-reduced-motion: reduce) {
  .resume-hint-toast { transition: opacity .12s linear; transform: translateX(-50%); }
  .resume-hint-toast.visible { transform: translateX(-50%); }
}

/* 日期拆成两个可编辑位置之后，空的那一半（"至今"/"—"）要看得出是可点的 */
.resume-date-part.is-empty { color: var(--r-muted, #888); }
.resume-entry-date .date-sep { opacity: .7; }


/* ============================================================
   缺口提示 / 分组计数 / 可点提示（2026-09-13 界面改版）
   ============================================================ */

/* 缺口：JD 要的东西档案里没有。它以前是简历下面一个裸 <ul>，紧跟着匹配
   分析那一长串卡片——用户唯一必须读到的一句话，混在最不容易读的地方。
   现在独占一块，跟"分析"在视觉上分开。 */
.gap-block {
  margin-top: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--warn-soft);
  border: 1px solid #F3DED4;
  border-radius: var(--radius-sm);
}
.gap-block-title {
  font-size: var(--fs-6);
  font-weight: 500;
  letter-spacing: 0.06em;
  color: var(--warn);
  margin-bottom: var(--sp-2);
}
/* 这条覆盖上面 .gap-list 原来的 margin/padding：它现在住在一个有内边距的
   盒子里，再带一份自己的外边距就会两头都空一截。 */
.gap-block .gap-list { margin: 0; padding-left: var(--sp-4); color: var(--warn); }

.match-warning {
  margin-top: var(--sp-3);
  font-size: var(--fs-5);
  line-height: 1.6;
  color: var(--warn);
}

/* 分组标题后面的计数。用一个淡底小胶囊而不是"工作经历（3）"：括号会跟
   标题挤成一句话，计数是给人扫的，不是给人读的。 */
.kind-count {
  margin-left: var(--sp-2);
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--bg);
  color: var(--muted);
  font-size: var(--fs-7);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}

/* 待处理的候选：跟已确认的计数**长得不一样**。它们是两件事——一个是"档案里
   有什么"，一个是"还有什么等你挑"。做成同一种圆点的话，用户只会读成"我有
   6 段工作经历"，而真实岗位只有两三个。 */
.kind-pending {
  margin-left: var(--sp-2);
  font-size: var(--fs-7);
  font-weight: normal;
  color: var(--muted);
}

/* 「收进档案」时补字段那个框顶上那句说明。它不是报错——缺字段在这条路上是
   常态（抽取器猜不出"角色"这种东西），所以用中性的底色，不是红的。 */
.adopt-note {
  margin-bottom: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--radius-sm);
  background: var(--bg-tint);
  color: var(--muted);
  font-size: var(--fs-6);
}
/* 缺的那几个输入框标出来，省得用户在一屏字段里自己找 */
.edit-form .is-missing {
  border-color: var(--accent) !important;
  background: #fff;
}

/* ---------- 版本号与更新公告（2026-09-14） ---------- */

/* 顶栏那个 v0.4.0-alpha。**刻意做得很轻**：它是个可查的事实，不是一条需要
   被注意的信息。做成醒目的徽章会在每一屏上抢走一点注意力，而用户一天里只
   有一次真的想看它。 */
.version-badge {
  border: none;
  background: none;
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  color: var(--muted-soft, var(--muted));
  font-size: var(--fs-7);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.version-badge:hover { color: var(--accent); background: var(--bg-tint); }
.version-badge:empty { display: none; }   /* 还没拿到版本号时不占位 */

.changelog-card { width: min(520px, 92vw); }
.changelog-body { max-height: 52vh; overflow-y: auto; margin: var(--sp-3) 0; }
.changelog-entry + .changelog-entry {
  margin-top: var(--sp-4);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--border-soft);
}
.changelog-head {
  display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap;
  margin-bottom: var(--sp-2);
}
.changelog-ver {
  font-weight: 600; font-variant-numeric: tabular-nums;
}
.changelog-title { color: var(--text); }
.changelog-date { margin-left: auto; color: var(--muted-soft, var(--muted)); font-size: var(--fs-7); }
.changelog-body ul { margin: 0; padding-left: 1.2em; }
.changelog-body li { margin: 4px 0; color: var(--muted); }

@media print { .version-badge { display: none !important; } }

/* 可点的一闪提示（窄屏上"简历已更新 · 点这里查看"）。基类是
   pointer-events:none 的——它悬在页面正上方，平时绝不该挡住底下的东西，
   所以只有带回调的那一次才放开。 */
.resume-hint-toast.is-clickable {
  pointer-events: auto;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* 数字要对齐的地方（日期、计数、匹配分数）统一开表格数字 */
.entry-card .sub-line, .resume-entry-date, .import-count { font-variant-numeric: tabular-nums; }

/* ============================================================
   用户反馈（2026-09-13 openspec 变更 admin-console-and-feedback）
   ============================================================ */

/* 星星。用一个 button 而不是 span：它是可点的，那就该能用键盘 Tab 到、
   能被读屏软件念出来。 */
.fb-star {
  background: none;
  border: none;
  padding: 0 2px;
  font-size: var(--fs-2);
  line-height: 1;
  color: var(--border);
  cursor: pointer;
  transition: color 0.12s ease, transform 0.12s ease;
}
.fb-star:hover { color: var(--accent-fill); transform: scale(1.12); }
.fb-star.is-on { color: var(--accent); }
.fb-star:focus-visible { outline: none; box-shadow: var(--ring); border-radius: 4px; }
@media (prefers-reduced-motion: reduce) { .fb-star:hover { transform: none; } }

/* 弹出来主动问一句的气泡。fixed 定位、坐标由 feedback.js 按顶栏那个按钮算——
   顶栏本身不是定位上下文，而且这一层要盖在所有面板之上。 */
.fb-bubble {
  position: fixed;
  z-index: 150;
  width: 260px;
  max-width: calc(100vw - 24px);
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-pop);
}
/* 指向顶栏那个按钮的小三角。纯装饰，读屏软件不需要知道它存在。 */
.fb-bubble::before {
  content: "";
  position: absolute;
  top: -6px;
  right: 18px;
  width: 10px;
  height: 10px;
  background: var(--panel-bg);
  border-left: 1px solid var(--border);
  border-top: 1px solid var(--border);
  transform: rotate(45deg);
}
.fb-bubble-title { font-size: var(--fs-4); font-weight: 600; padding-right: var(--sp-4); }
.fb-bubble-stars { margin: var(--sp-2) 0 var(--sp-1); margin-left: -2px; }
.fb-bubble-note { font-size: var(--fs-6); color: var(--muted); line-height: 1.6; }
.fb-bubble-close {
  position: absolute;
  top: 6px;
  right: 8px;
  background: none;
  border: none;
  padding: 2px 4px;
  font-size: var(--fs-6);
  line-height: 1;
  color: var(--muted-soft);
  cursor: pointer;
}
.fb-bubble-close:hover { color: var(--text); }

/* 反馈表单 */
.fb-modal { width: min(420px, 92vw); }
.fb-modal h4 { margin: 0 0 var(--sp-1); font-size: var(--fs-3); }
.fb-sub { margin: 0 0 var(--sp-3); font-size: var(--fs-5); color: var(--muted); line-height: 1.7; }
.fb-stars { display: flex; align-items: center; gap: 2px; margin-bottom: var(--sp-3); }
.fb-rating-label { margin-left: var(--sp-2); font-size: var(--fs-5); color: var(--muted); }
.fb-modal textarea {
  width: 100%;
  font: inherit;
  font-size: var(--fs-4);
  line-height: 1.7;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-tint);
  color: var(--text);
  resize: vertical;
}
.fb-modal textarea:focus { outline: none; background: #fff; border-color: var(--accent); }
.fb-error {
  margin-top: var(--sp-2);
  font-size: var(--fs-5);
  color: var(--danger);
}

@media print { .fb-bubble { display: none !important; } }


/* ============================================================
   初步预览的遮罩（2026-09-13 openspec 变更 first-resume-preview）

   用户第一次传完自己的简历之后，中间栏原来是一句"还没有匹配结果——在对话框里
   发一段JD文本试试"：他刚做了一件事，屏幕上什么都没发生。现在那一刻会先按
   档案原样排一版出来，并盖上这层说明。

   **半透明而不是盖死。** 这层东西要说的是"这版不算数"，而用户此刻最需要的
   恰恰是看见自己刚传进去的内容真的变成了一张纸。盖死了就等于没显示。
   ============================================================ */
#resume-body { position: relative; }

.preview-mask {
  position: absolute;
  inset: 0;
  z-index: 20;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: var(--sp-6) var(--sp-4);
  /* 上浓下淡：话说在纸的上半截，下半截留着让人看清内容 */
  background: linear-gradient(
    to bottom,
    rgba(246, 247, 248, 0.94) 0%,
    rgba(246, 247, 248, 0.88) 38%,
    rgba(246, 247, 248, 0.55) 100%
  );
  backdrop-filter: blur(1px);
}
.preview-mask-card {
  max-width: 340px;
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  padding: var(--sp-4);
  box-shadow: var(--shadow-pop);
}
.preview-mask-title { font-size: var(--fs-3); font-weight: 600; margin-bottom: var(--sp-2); }
.preview-mask-body { font-size: var(--fs-5); color: var(--muted); line-height: 1.8; }
.preview-mask-actions { margin-top: var(--sp-3); display: flex; justify-content: flex-end; }

/* 收掉遮罩之后留下的那条细提示。话说过一次就够了，一直挡着会变成骚扰，
   但也不能让人翻两页之后忘了这版是怎么来的。 */
.preview-banner {
  margin: var(--sp-2) 0 var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid #F7C8D8;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  color: var(--accent-strong);
  font-size: var(--fs-6);
}

.mode-flag.preview-flag { color: var(--accent); }

@media print {
  .preview-mask, .preview-banner { display: none !important; }
}


/* ---------- 简历语言切换（2026-09-13 openspec 变更 resume-language-toggle） ---------- */
/* 译文是个派生视图，库里存的始终是原文那一份。这个标记要一直在，否则用户会
   以为英文版已经存下来了。 */
.mode-flag.lang-flag { color: var(--muted); border-color: var(--border); }
.resume-toolbar button.is-busy { opacity: .6; cursor: progress; }

/* ---------- 反馈字数 ---------- */
.fb-count {
  margin-top: 4px;
  text-align: right;
  font-size: var(--fs-7);
  color: var(--muted-soft);
  font-variant-numeric: tabular-nums;
}
.fb-count.is-over { color: var(--danger); }

/* ---------- 后台：点开某个用户的档案 / 简历 ---------- */
.admin-table .drill {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.admin-table .drill:disabled {
  color: var(--muted-soft);
  cursor: default;
  text-decoration: none;
}
.drill-modal {
  width: min(860px, 94vw);
  max-height: 86vh;
  display: flex;
  flex-direction: column;
}
.drill-modal h4 { margin: 0 0 var(--sp-1); font-size: var(--fs-3); }
.drill-sub { margin: 0 0 var(--sp-3); font-size: var(--fs-6); color: var(--muted-soft); }
.drill-body {
  overflow: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-tint);
}
.drill-body table { width: 100%; border-collapse: collapse; font-size: var(--fs-5); }
.drill-body th {
  position: sticky;
  top: 0;
  background: var(--bg-tint);
  text-align: left;
  font-size: var(--fs-6);
  font-weight: 500;
  color: var(--muted-soft);
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.drill-body td {
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--border-soft);
  vertical-align: top;
  line-height: 1.7;
}
.drill-body td.k { white-space: nowrap; color: var(--muted); width: 1%; }
.drill-body ul { margin: 4px 0 0; padding-left: 18px; }
.drill-body li { margin-bottom: 2px; }
.drill-empty { padding: var(--sp-5); text-align: center; color: var(--muted-soft); font-size: var(--fs-5); }
.drill-group-title {
  padding: var(--sp-3) var(--sp-3) var(--sp-2);
  font-size: var(--fs-6);
  letter-spacing: .06em;
  color: var(--muted-soft);
  background: var(--bg-tint);
  border-bottom: 1px solid var(--border-soft);
  position: sticky;
  top: 0;
}


/* ---------- 后台：嵌进来的简历 / 反馈的环境行（2026-09-13） ---------- */
/* 后台里那份简历原来只有 400px 宽——`.modal-card` 的 `max-width: 400px` 一直
   压着它，而这里只写了 `width`，同权重下前面那条并不会被覆盖。A4 纸是按容器
   宽度等比缩放的（resume.js::applyPreviewScale），于是纸上的正文缩到了几个
   像素，等于摆着好看。

   宽度定在"刚好够 A4 原尺寸"：A4 是 210mm ≈ 794px，缩放那一步写的是
   `Math.min(1, avail / pageW)`——**再宽也不会放得更大**，只会在纸两边留白。
   940 − 弹窗左右内边距 40 − iframe 里 .wrap 的 32 ≈ 868，够 794 站直。 */
.resume-modal {
  width: min(940px, 96vw);
  max-width: none;
  max-height: 94vh;
}
.resume-frame {
  width: 100%;
  /* A4 原尺寸大约 1123px 高，屏幕一般装不下整张，所以这里给到视口的八成，
     剩下的在 iframe 内部滚。比原来的 70vh/820px 高一截：820px 连半张纸都
     不到，看的人得一直滚。 */
  height: min(80vh, 1180px);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-tint);
}
.fb-item-env {
  margin-top: var(--sp-2);
  padding-top: var(--sp-2);
  border-top: 1px solid var(--border-soft);
  display: flex;
  gap: var(--sp-3);
  align-items: baseline;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--fs-7);
  color: var(--muted-soft);
}
.fb-env-ip { flex: none; }
/* UA 串很长，一行放不下就截断——它是"要用的时候点开看"的东西，不是要读的 */
.fb-env-ua { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* 一个日期都没有的条目：屏幕上留一个"＋ 日期"的入口，纸上什么都不印。
   2026-09-14 之前这里印出来的是「— － 至今」，在 PDF 上就是一行坏数据。 */
@media print {
  .resume-entry-date.has-no-date { display: none !important; }
  /* 只给屏幕看的日期占位（项目没填结束时间时那个「＋ 结束」和它前面的
     连接号）。纸上只剩开始时间，什么都不断言。 */
  .resume-entry-date .is-screen-only { display: none !important; }
}


/* ============================================================
   ↑↑↑ 新的样式请写在这一行**上面**。↑↑↑

   下面这一节（响应式 + 窄屏）必须留在整个文件的最后。

   2026-09-15 的教训：它原本在文件中段，后面陆续追加了「版本号徽章」
   「反馈入口」等几节。那几节里的 `.version-badge { display: inline-block }`
   跟窄屏块里的 `.version-badge { display: none }` **特指度完全相同**，
   于是按层叠规则「后写的赢」——窄屏那条被静默吃掉，手机上版本号照样显示、
   照样跟账号按钮抢位置。单测只能证明那行字还在，真机上它不生效。

   放在最后就不需要 `!important`，也不需要每加一节都回头检查一遍。
   `tests/test_report_0915.py` 里有一条盯着这个顺序。
   ============================================================ */

/* ============================================================
   窄屏：三栏 → 顶部标签 + 单栏（2026-09-13）

   三栏在 1200px 以下就已经难看了：简历那张纸是 210mm 的固定版面，靠
   transform: scale() 缩进栏里，栏一窄纸就缩成一张看不清字的缩略图，而
   左右两栏又各自窄到只能塞下半个标题。

   只设**一个断点**。中间那档（900–1199，分屏/iPad 横屏）不做两栏方案：
   多一套布局就多一套要维护的状态，而它服务的场景本来就少。

   `screen and` 不能省——打印时页面宽度约 794px，不写的话这整块会在打印时
   命中，把 #panel-resume 也一起藏掉（它那时没有 .is-active），导出的 PDF
   会是一张白纸。
   ============================================================ */
.pane-tabs { display: none; }

@media screen and (max-width: 1199px) {
  .pane-tabs {
    display: flex;
    flex-shrink: 0;
    background: var(--panel-bg);
    border-bottom: 1px solid var(--border);
  }
  /* 这几个按钮不在组件层那串"按容器挂"的选择器里，所以观感属性要自己写全，
     否则会露出浏览器默认的灰底外凸按钮。 */
  .pane-tabs button {
    flex: 1;
    appearance: none;
    -webkit-appearance: none;
    background: none;
    border: none;
    border-bottom: 2px solid transparent;
    border-radius: 0;
    padding: var(--sp-3) var(--sp-1);
    font: inherit;
    font-size: var(--fs-4);
    color: var(--muted);
    cursor: pointer;
    transition: color 0.15s ease, border-color 0.15s ease;
  }
  .pane-tabs button[aria-selected="true"] {
    color: var(--accent);
    font-weight: 600;
    border-bottom-color: var(--accent);
  }
  main {
    grid-template-columns: 1fr;
    padding: var(--sp-3);
  }
  /* 没被选中的那两栏是真的不渲染，不是缩到一边——简历那张纸会按容器宽度
     量高度做密度收敛，留在文档流里（哪怕看不见）会被量成一个错的值。 */
  main > .panel { display: none; }
  main > .panel.is-active { display: flex; }

  header { padding: var(--sp-3) var(--sp-4); }
  /* 长邮箱/长用户名不该把品牌区挤没 */
  .brand-text { min-width: 0; }

  /* 标签栏上已经写着「对话」，面板自己再写一遍就是同一个词占两行——手机上
     一屏总共就那么高。所以窄屏时标题退化成一条只放动作按钮的细条；一个按钮
     都没有的面板（简历）整条收掉。
     `:has()` 不支持的浏览器会退回到"留着一条空标题栏"，难看但不坏事。 */
  main > .panel > h2 .icon,
  main > .panel > h2 .pane-title { display: none; }
  main > .panel > h2 {
    justify-content: flex-end;
    padding: var(--sp-2) var(--sp-3);
    border-bottom: none;
  }
  main > .panel > h2:not(:has(button)) { display: none; }
}

/* 更窄一档：只收紧留白，不改结构 */
@media screen and (max-width: 480px) {
  header { gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); }
  main { padding: var(--sp-2); }
  .panel .body { padding: var(--sp-3); }

  /* **`.whoami` 不能再隐藏了。**（2026-09-15）
     这里原来写的是 `.whoami { display: none }`，注释是"顶栏放不下了；退出
     按钮还在"。那句话在 2026-09-13 就不成立了——那天把反馈建议 / 退出登录 /
     注销账号**全部收进了这个下拉菜单**，顶栏上不再有任何一个账号相关的按钮。
     也就是说从那天起，手机用户既退不了登录、也提不了反馈、更注销不了账号，
     而这条 CSS 一个字没改，谁也没发现。

     教训：**把入口收拢到一处的那次改动，必须回头检查所有"隐藏那一处"的
     规则。** 收拢让每一条隐藏规则的代价都变大了。

     顶栏确实窄，所以让位的是版本号——它在账号菜单里有「更新历史」这个入口，
     而账号菜单本身没有任何替代入口。 */
  .version-badge { display: none; }
  .whoami {
    max-width: 8em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

@media print {
  .pane-tabs { display: none !important; }
  /* 上面那块已经用 `screen and` 隔开了，这条是第二道保险：无论屏幕上当前
     选的是哪一栏，打印出来的永远是简历。 */
  #panel-resume { display: block !important; }
}
