/* ==========================================================================
   app.css —— 门户的布局与组件样式表
   
   与 tokens.css 的分工是硬性的：
   * tokens.css 是**唯一**允许出现颜色字面量的地方（它就是取值源头）；
   * 本文件只允许通过 var() 消费令牌，不得出现 hex / rgb() / rgba() / hsl()
     字面量。需要半透明效果时用已有令牌（--focus-ring / --sh-1）或 opacity
     属性，不要现场调 rgba()。（Property 3）
   * margin / padding / gap 的每个长度值只能是 0、auto、var(--sp-*)，或位于
     **显式例外集合**内的 var(--gutter) / var(--gutter-narrow)——这两个是间距阶梯的
     语义别名（--gutter ≡ --sp-6 ≡ 24px，--gutter-narrow ≡ --sp-4 ≡ 16px），用于页面
     槽宽这类"有名字的间距"。除此之外确有非阶梯需求（例如 1px 边框补偿）时改用别的
     属性实现，不要写进 margin / padding / gap。（Property 4）
   * 不引用任何外部资源（无 @import、无 http(s):// 或 // 开头的 url()）。（Property 6）

   文件结构固定为下面七段，顺序不可调整——静态校验与人工检索都依赖它：

     1. Reset & base
     2. Layout
     3. Components
     4. Pages
     5. Breakpoints
     6. Accessibility
     7. Compatibility

   段与段之间不得互相插入规则：媒体查询一律写进第 5 段，:focus-visible 统一
   焦点环一律写进第 6 段，job_detail.html 内联脚本生成的既有 class 一律写进
   第 7 段。这样"改哪里"永远有唯一答案。
   ========================================================================== */


/* 1. Reset & base */

/* 只做一件事：把浏览器默认值拉进令牌体系。这里不出现任何组件外观，
   也不出现 outline: none——焦点相关的规则统一属于第 6 段。 */

*, *::before, *::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--fs-base);
  line-height: var(--lh-base);
  background: var(--surface-sunken);
  color: var(--text);
  /* 中文字形在深色文字 + 浅底的组合下默认渲染偏重，抗锯齿让笔画更接近设计稿 */
  -webkit-font-smoothing: antialiased;
}

/* 媒体元素自适应容器，避免超宽图片/图标撑出页面级横向滚动 */
img,
svg {
  max-width: 100%;
}

img {
  height: auto;
}

/* 内联 SVG（品牌标识、图标）默认按基线排版会比相邻文字低一截，统一改为居中 */
svg {
  vertical-align: middle;
}

/* 标题：字号只取字号阶梯，行高取 --lh-tight（标题不需要正文的 1.65） */
h1,
h2,
h3,
h4,
h5,
h6 {
  margin-top: 0;
  margin-bottom: var(--sp-3);
  color: var(--text-heading);
  line-height: var(--lh-tight);
}

h1 {
  font-size: var(--fs-xl);
}

h2 {
  font-size: var(--fs-lg);
}

/* h3 与正文同字号，靠字重与颜色区分层级——继续往下缩字号会低于可读下限 */
h3,
h4,
h5,
h6 {
  font-size: var(--fs-base);
}

a {
  color: var(--link);
}

a:hover {
  color: var(--link-hover);
}

p {
  margin: 0 0 var(--sp-4);
}


/* 2. Layout */

/* 页头（.topbar）/ 主内容区（.layout）/ 页面标题区（.page-head）/ 页脚
   （.site-footer）四件套，由 base.html 骨架统一输出。
   本段不写媒体查询：窄屏的 --gutter-narrow 适配、导航换行、触控尺寸
   全部属于第 5 段。 */

/* ---- 页头 ---------------------------------------------------------------- */

/* 高度 56px 是页头这一处的构造尺寸，不是可复用的间距，故直接写在 min-height
   上而非塞进间距阶梯；min- 而非固定 height 是为了让窄屏换行时能自然长高。 */
.topbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3) var(--sp-5);
  min-height: 56px;
  padding: 0 var(--sp-6);
  background: var(--surface-inverse);
  color: var(--text-on-brand);
  box-shadow: var(--sh-1);
}

/* 品牌区：内联 SVG 标识 + 产品名。
   这里显式设 color 有两层作用：一是覆盖第 1 段 a 的 --link（品牌名在深底上
   必须是 --text-on-brand），二是让 brand-mark.svg 的 currentColor 取到白色
   ——该 svg 全部描边与填充都走 currentColor，不设 color 就会继承成蓝色而在
   --brand-800 底上几乎不可见。 */
.topbar__brand {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  color: var(--text-on-brand);
  font-size: var(--fs-lg);
  font-weight: 600;
  line-height: var(--lh-tight);
  text-decoration: none;
}

.topbar__brand:hover {
  color: var(--text-on-brand);
}

.topbar__brand svg {
  width: 26px;
  height: 26px;
  flex: none;
  color: var(--text-on-brand);
}

/* margin-right: auto 把用户区顶到右端；纵向 0 保证不破坏 56px 的垂直居中 */
.topbar__nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-1);
  margin: 0 auto 0 var(--sp-4);
}

.nav-link {
  position: relative;
  display: inline-flex;
  align-items: center;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-sm);
  color: var(--text-on-brand);
  font-size: var(--fs-sm);
  text-decoration: none;
  /* 未选中项压低不透明度以让选中项跳出来；.85 下白字在 --brand-800 上仍约
     7.9:1，远高于 4.5 的正文要求，所以这是"降调"而不是"降到不合格"。
     用 opacity 而不是 rgba()，是本文件的硬约束（Property 3）。 */
  opacity: .85;
  transition: background-color var(--dur-fast) ease, opacity var(--dur-fast) ease;
}

.nav-link:hover {
  background: var(--brand-700);
  color: var(--text-on-brand);
  opacity: 1;
}

/* 选中态刻意不只靠颜色：同时给到字重加粗 + 底部 2px 指示条 + 满不透明度，
   在灰度显示或色觉障碍下依然可辨（Requirement 5.1 的同一思路）。 */
.nav-link.is-active {
  font-weight: 600;
  opacity: 1;
}

.nav-link.is-active::after {
  content: "";
  position: absolute;
  /* left/right/bottom 是定位属性而非间距属性，取 --sp-* 只为与内边距对齐；
     2px 的指示条厚度写在 height 上，不占用间距阶梯。 */
  left: var(--sp-3);
  right: var(--sp-3);
  bottom: 0;
  height: 2px;
  border-radius: var(--r-full);
  background: var(--text-on-brand);
}

.topbar__user {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  font-size: var(--fs-sm);
}

/* 深色底适配：登出按钮沿用第 3 段的 .btn--ghost（页头不该出现与页面主操作
   同级的实心按钮），但 ghost 变体的默认取色是面向浅色表面的——文字 --text、
   边框 --border 都是深灰，落到 --surface-inverse 上会与底色糊在一起。
   因此在页头上下文里改写为 --text-on-brand（在 --brand-800 上实测 10.36 ✓，
   边框同色也满足 1.4.11 的 3:1）。
   作用域限定在 .topbar 内，不影响其他位置的 ghost 按钮。 */
.topbar .btn--ghost {
  color: var(--text-on-brand);
  border-color: var(--text-on-brand);
}

.topbar .btn--ghost:hover {
  background: var(--brand-700);
  color: var(--text-on-brand);
}

/* ---- 主内容区 ------------------------------------------------------------ */

/* 横向内边距直接写 var(--gutter)。--gutter（≡ --sp-6，24px）与 --gutter-narrow
   （≡ --sp-4，16px）是间距阶梯的**语义别名**，已列入 Property 4 的显式例外集合
   （该属性允许"位于显式记录的例外集合内"的取值），因此这里无需退回 var(--sp-6)。
   用别名而非阶梯值的好处：语义自明（这是页面槽宽，不是任意 24px），且窄屏收窄
   只需换成 var(--gutter-narrow)，改槽宽时也不必逐处核对是哪一级阶梯。
   窄屏收窄到 var(--gutter-narrow) 的规则位于第 5 段。 */
.layout {
  max-width: var(--layout-max);
  margin: 0 auto;
  padding: var(--gutter) var(--gutter);
}

/* ---- 页面标题区 ---------------------------------------------------------- */

/* h1 在左、页级操作在右；允许换行，窄屏时操作区整体落到下一行而不是被挤压。
   baseline 对齐让不同字号的标题与按钮文字在同一条基线上。 */
.page-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3) var(--sp-4);
  margin: 0 0 var(--sp-6);
}

.page-head__title {
  margin: 0;
  font-size: var(--fs-xl);
}

.page-head__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
}

/* 可选副标题（job_detail 用它放文件名）。占满一整行并排到最后，这样无论它在
   DOM 里位于标题之后还是操作区之后，视觉上都稳定落在标题行下方。 */
.page-head__sub {
  flex: 0 0 100%;
  order: 9;
  margin: 0;
  color: var(--text-secondary);
  font-size: var(--fs-sm);
}

/* ---- 页脚 ---------------------------------------------------------------- */

/* 与主内容区同宽同横向内边距（同样用 --gutter 别名），居中弱化呈现；
   上边距把它与内容明确分开。 */
.site-footer {
  max-width: var(--layout-max);
  margin: var(--sp-10) auto 0;
  padding: 0 var(--gutter) var(--sp-8);
  color: var(--text-muted);
  font-size: var(--fs-xs);
  text-align: center;
}


/* 3. Components */

/* 组件表。本段贯穿三条纪律：

   1. 颜色只经 var() 取语义别名或语义色，不出现任何字面量；需要"更淡/更暗"时用
      opacity 或 filter，而不是现场调 rgba()。（Property 3）
   2. margin / padding / gap 的长度值只取 0 / auto / var(--sp-*) / var(--gutter)
      / var(--gutter-narrow)；其他尺寸需求（气泡最大高度、截断列宽、指示条厚度）
      写在 max-height / max-width / height 上。（Property 4）
   3. 状态与语义**不能只靠颜色**：徽标、提示条、加载态一律另配 ::before 注入的
      图形字符，文字标签由 HTML 提供。灰度显示与色觉障碍下仍可辨。
      （Requirement 5.1 / 5.2）

   媒体查询属第 5 段、:focus-visible 统一焦点环属第 6 段、job_detail.html 内联脚本
   生成的既有 class（.msg-* / .status-* / .step / .opt-btn / .conn-state / .hint）
   属第 7 段——本段一律不写。 */

/* ---- 按钮 ---------------------------------------------------------------- */

/* .btn 基座**就是 primary 外观**，不另设 .btn--primary 修饰符。
   这不是审美选择而是硬约束：job_detail.html 的内联脚本生成
   `<a class="btn">下载报价清单</a>`，没有变体修饰符，而该脚本在本特性内零字符
   改动（JS_Contract 冻结）。若把实心品牌色挂到 .btn--primary 上，那个下载入口
   就会退化成没有背景的裸链接。
   选择器同时服务 <a> 与 <button>：故显式给出 text-decoration、font-family
   （按钮不继承字体族）、cursor，让两种标签渲染成同一枚控件。
   min-height 而非 height：中文按钮文案偶有换行，固定高度会把文字裁掉。 */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: var(--ctrl-h-md);
  padding: var(--sp-2) var(--sp-4);
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  background: var(--brand-600);
  color: var(--text-on-brand);
  font-family: inherit;
  font-size: var(--fs-sm);
  line-height: var(--lh-tight);
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--dur-fast) ease, border-color var(--dur-fast) ease,
    color var(--dur-fast) ease, opacity var(--dur-fast) ease;
}

/* 显式重申 color：第 1 段的 a:hover 会把链接式按钮的文字改成 --link-hover，
   在 --brand-700 底上那是深蓝对深蓝。类选择器特异度高于 a:hover，此处一次性钉住。 */
.btn:hover {
  background: var(--brand-700);
  color: var(--text-on-brand);
}

/* 次级按钮：与页面表面同色 + 可见边框，作"同一区域内的非主操作"。 */
.btn--secondary {
  background: var(--surface);
  border-color: var(--border);
  color: var(--text);
}

.btn--secondary:hover {
  background: var(--surface-muted);
  color: var(--text);
}

/* 幽灵按钮：透明底，用于页头等已有背景色的容器。
   注意第 2 段的 .topbar .btn--ghost（特异度 0,3,0）会覆盖这里的 0,2,0，
   把深色底上的取色换成 --text-on-brand——所以这里的 hover 选择器**必须保持
   单类形态**，不要写成 .btn--ghost:not([disabled]):hover 之类把特异度抬到 0,3,0
   的形式，否则页头的深色底适配会被本段反向覆盖。 */
.btn--ghost {
  background: transparent;
  border-color: var(--border);
  color: var(--text);
}

.btn--ghost:hover {
  background: var(--surface-muted);
  color: var(--text);
}

/* 危险按钮：实心 danger。 */
.btn--danger {
  background: var(--danger-600);
  border-color: transparent;
  color: var(--text-on-brand);
}

/* 语义色只有 600 一档（tokens.css 没有 --danger-700），因此悬停态用 filter 压
   亮度而不是换令牌：既不新增令牌，也不引入颜色字面量。压到 .92 后白字对比度
   仍高于 4.5（原为 6.47）。 */
.btn--danger:hover {
  color: var(--text-on-brand);
  filter: brightness(.92);
}

/* 小尺寸：用于表格行内操作（"标记已读""采纳此版本"这类）。 */
.btn--sm {
  min-height: var(--ctrl-h-sm);
  padding: var(--sp-1) var(--sp-3);
  font-size: var(--fs-xs);
}

/* 整宽按钮（登录页提交按钮）。高度取 --ctrl-h-lg（44px）以满足触控目标下限；
   display 改 flex 而非 inline-flex，避免行内盒在窄屏下出现行尾空隙。 */
.btn--block {
  display: flex;
  width: 100%;
  min-height: var(--ctrl-h-lg);
}

/* 禁用态：降不透明度 + not-allowed 光标。
   [disabled] 与 :disabled 同时写：前者覆盖属性选择器可命中的场合，后者是原生
   伪类，二者在 <button disabled> 上等价，但保留两条可读性更好。 */
.btn[disabled],
.btn:disabled {
  opacity: .55;
  cursor: not-allowed;
}

/* 抵消 hover：禁用按钮不应有"可按"的反馈。逐变体把背景钉回静态取值，
   而不是在 hover 选择器上加 :not([disabled])——后者会抬高特异度并破坏
   第 2 段的 .topbar .btn--ghost 覆盖（见上方说明）。 */
.btn[disabled]:hover,
.btn:disabled:hover {
  background: var(--brand-600);
  filter: none;
}

.btn--secondary[disabled]:hover,
.btn--secondary:disabled:hover {
  background: var(--surface);
}

.btn--ghost[disabled]:hover,
.btn--ghost:disabled:hover {
  background: transparent;
}

.btn--danger[disabled]:hover,
.btn--danger:disabled:hover {
  background: var(--danger-600);
}

/* 提交中（ui.js 在 [data-loading-form] 提交时加 .is-loading）。
   刻意**不以旋转动画为唯一手段**：第 6 段在 prefers-reduced-motion 下会整体停用
   动画，若只有转圈，那些用户就完全看不出按钮已在处理。因此状态本体由三样静态
   手段承担——::before 注入的进度字符、降低的不透明度、progress 光标；下面的
   旋转只是锦上添花，被停用也不损失信息。 */
.btn.is-loading {
  opacity: .75;
  cursor: progress;
}

.btn.is-loading::before {
  content: "◌";
  flex: none;
  /* 时长由 --dur-base 派生（200ms × 4 = 800ms），避免另立一个魔数时长令牌 */
  animation: btn-spin calc(var(--dur-base) * 4) linear infinite;
}

@keyframes btn-spin {
  to {
    transform: rotate(1turn);
  }
}

/* ---- 卡片 ---------------------------------------------------------------- */

/* 页面的基本内容容器。边框取 --border-faint（仅装饰性分隔，1.4.11 不适用），
   靠 --sh-1 而非重边框拉开层次。 */
.card {
  margin: 0 0 var(--sp-6);
  padding: var(--sp-5) var(--sp-6);
  border: 1px solid var(--border-faint);
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: var(--sh-1);
}

/* 结构化头部：标题在左、.card__actions 在右，允许换行——窄屏时操作区整体落到
   下一行而不是把标题挤成竖排。baseline 对齐让标题与按钮文字落在同一条基线。 */
.card__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-2) var(--sp-4);
  margin: 0 0 var(--sp-4);
}

.card__title {
  margin: 0;
  font-size: var(--fs-lg);
}

/* 说明文字：占满一整行并排到最后，这样无论它在 DOM 里写在标题后还是操作区后，
   视觉上都稳定落在标题行下方（与第 2 段 .page-head__sub 同一手法）。
   flex/order 在非 flex 容器内被忽略，故卡片外单独使用也无副作用。 */
.card__desc {
  flex: 0 0 100%;
  order: 9;
  margin: 0;
  color: var(--text-secondary);
  font-size: var(--fs-sm);
}

.card__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
}

/* 收掉正文最后一个子元素的下外边距，避免它与卡片自身的 padding 叠成双倍留白 */
.card__body > :last-child {
  margin-bottom: 0;
}

/* ---- 表单字段 ------------------------------------------------------------ */

.field {
  margin: 0 0 var(--sp-4);
}

.field__label {
  display: block;
  margin: 0 0 var(--sp-1);
  color: var(--text-secondary);
  font-size: var(--fs-sm);
}

/* 控件高度统一取 --ctrl-h-md，与 .btn 基座同高，表单里并排时不会一高一低。
   边框取 --border（--n-400）而非更浅的灰：控件边框属 WCAG 1.4.11 的界面组件，
   需 ≥ 3:1，推导见 tokens.css。 */
.field__control {
  width: 100%;
  min-height: var(--ctrl-h-md);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: var(--fs-sm);
  line-height: var(--lh-base);
}

.field__hint {
  margin: var(--sp-1) 0 0;
  color: var(--text-muted);
  font-size: var(--fs-xs);
}

/* 校验失败文案。与下面的边框变化配对出现：颜色之外还有文字，不靠颜色单打独斗。 */
.field__error {
  margin: var(--sp-1) 0 0;
  color: var(--danger-600);
  font-size: var(--fs-xs);
}

.field--invalid .field__control {
  border-color: var(--danger-600);
}

/* 裸控件兜底：现有模板存在未加 class 的 <input>/<select>/<textarea>
   （job_detail 的聊天输入框由既有脚本查询 #chat-input，也不会加 class），
   这里给它们一份与 .field__control 等同的基础外观。
   元素选择器特异度 0,0,1 低于 .field__control 的 0,1,0，两者叠加时以后者为准。 */
input,
select,
textarea {
  width: 100%;
  min-height: var(--ctrl-h-md);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: var(--fs-sm);
  line-height: var(--lh-base);
}

/* 多行输入给更大的初始高度，并只允许纵向拖拽——横向拉宽会撑破卡片栅格。
   高度由 --ctrl-h-lg 派生，不另立令牌。 */
textarea {
  min-height: calc(var(--ctrl-h-lg) * 2);
  resize: vertical;
}

/* 文件选择框单独处理：原生控件内部由浏览器绘制按钮与文件名，强加 min-height 与
   内边距只会让内容偏上或溢出。因此只统一字号与边框（保持它与其他控件同一视觉
   语言），尺寸交还浏览器。 */
input[type="file"] {
  width: 100%;
  min-height: 0;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: var(--fs-sm);
}

/* 占位符属"非必要提示"，取 --text-placeholder（--n-300）；显式 opacity: 1 是为了
   压掉 Firefox 默认给占位符加的半透明，避免叠加后更淡。 */
::placeholder {
  color: var(--text-placeholder);
  opacity: 1;
}

/* ---- 表格 ---------------------------------------------------------------- */

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
}

/* 表头文本取 --text-secondary（--n-500）而非 --text-muted：后者在 --n-50 底上
   只有 4.23，不足正文所需的 4.5（推导见 tokens.css）。
   nowrap 让表头始终占一行，列宽由内容决定，窄屏靠 .table--wrap 横向滚动。 */
.table th {
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--border-subtle);
  background: var(--surface-muted);
  color: var(--text-secondary);
  font-size: var(--fs-xs);
  font-weight: 600;
  text-align: left;
  white-space: nowrap;
}

.table td {
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--border-faint);
  vertical-align: top;
}

.table tbody tr:hover {
  background: var(--surface-muted);
}

/* 窄屏下让**表格自己**横向滚动，而不是整页滚动（页面级横向滚动是 7.2 的验收红线）。
   -webkit-overflow-scrolling 让 iOS 的滚动带惯性，缺失时退化为普通滚动。 */
.table--wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* 数值列：右对齐 + 等宽数字，金额与数量按位对齐便于纵向比较 */
.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* 超长文本（文件名、反馈内容）截断为一行。max-width 是列宽约束而非间距，
   故写在 max-width 上；模板需同时给 title 属性保留全文。 */
.cell--ellipsis {
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---- 状态徽标 ------------------------------------------------------------ */

/* .badge 承担两种用途，分工如下（避免以后再有人"顺手改一下"就破坏另一边）：

   * **五状态徽标的基座**：`class="badge badge--done"`。基座给结构（行内 flex、
     图形与文字的间距、全圆角、字号、内边距），外观（前景/底/边框）由变体决定。
   * **醒目计数**：admin_console.html 的未读反馈计数当前是裸 `class="badge"`
     的红底白字实心。因此**裸 .badge 保留可见的实心 danger 默认外观**，与现状
     一致；这样第 7 段兼容层不必再为它兜底。新写模板时请显式用
     .badge--count 表达"这是计数不是状态"，语义自明且与状态徽标不会混淆。

   五状态变体的边框统一取 currentColor：各变体前景色在自身 50 底上实测 4.84–6.74，
   远高于 1.4.11 对界面组件的 3:1 要求，因此边框天然达标且不必再引一组边框令牌。 */
.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: var(--sp-1) var(--sp-2);
  border: 1px solid transparent;
  border-radius: var(--r-full);
  background: var(--danger-600);
  color: var(--text-on-brand);
  font-size: var(--fs-xs);
  line-height: var(--lh-tight);
  white-space: nowrap;
}

/* 醒目计数（未读反馈数）。取值与裸 .badge 相同，存在的意义是让模板能写出意图。 */
.badge--count {
  background: var(--danger-600);
  border-color: transparent;
  color: var(--text-on-brand);
}

.badge--pending,
.badge--processing,
.badge--awaiting,
.badge--done,
.badge--failed {
  border-color: currentColor;
}

/* 图形由 ::before 注入，文字标签由 HTML 提供——两者相加才构成"不只靠颜色"的
   状态表达。五个图形互不相同（○ ◐ ! ✓ ✕），与 `_components.html` 的 status_badge 宏一致。 */
.badge--pending {
  background: var(--surface-muted);
  color: var(--text-secondary);
}

.badge--pending::before {
  content: "○";
}

.badge--processing {
  background: var(--warning-50);
  color: var(--warning-600);
}

.badge--processing::before {
  content: "◐";
}

.badge--awaiting {
  background: var(--info-50);
  color: var(--info-600);
}

.badge--awaiting::before {
  content: "!";
}

.badge--done {
  background: var(--success-50);
  color: var(--success-600);
}

.badge--done::before {
  content: "✓";
}

.badge--failed {
  background: var(--danger-50);
  color: var(--danger-600);
}

.badge--failed::before {
  content: "✕";
}

/* ---- 提示条 -------------------------------------------------------------- */

/* 四语义提示条。同样以 ::before 注入图形（✕ / ! / ✓ / i），使语义不只靠颜色区分。
   flex-start 对齐：多行文案时图形贴住第一行而不是垂直居中到中间。 */
.alert {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  margin: 0 0 var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-sm);
  background: var(--surface-muted);
  color: var(--text);
  font-size: var(--fs-sm);
}

/* flex: none 防止图形被文字压缩变形 */
.alert::before {
  flex: none;
  font-weight: 600;
}

.alert--error {
  border-color: var(--danger-600);
  background: var(--danger-50);
  color: var(--danger-600);
}

.alert--error::before {
  content: "✕";
}

.alert--warning {
  border-color: var(--warning-600);
  background: var(--warning-50);
  color: var(--warning-600);
}

.alert--warning::before {
  content: "!";
}

.alert--success {
  border-color: var(--success-600);
  background: var(--success-50);
  color: var(--success-600);
}

.alert--success::before {
  content: "✓";
}

.alert--info {
  border-color: var(--info-600);
  background: var(--info-50);
  color: var(--info-600);
}

.alert--info::before {
  content: "i";
}

/* ---- 空态 ---------------------------------------------------------------- */

/* 空列表的正式呈现（替代空白 <tbody>）。它常出现在 <td colspan="N"> 内部，
   因此**不用 flex 居中**——单元格没有确定高度，flex 居中在那里没有意义；
   改用 padding 撑出留白 + text-align 水平居中，行为在任何容器里都一致。 */
.empty {
  padding: var(--sp-8) var(--sp-4);
  color: var(--text-secondary);
  text-align: center;
}

.empty__title {
  margin: 0;
  font-size: var(--fs-base);
  font-weight: 600;
}

.empty__desc {
  margin: var(--sp-2) 0 0;
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

.empty__action {
  margin: var(--sp-4) 0 0;
}

/* ---- 概要网格 ------------------------------------------------------------ */

/* 任务详情概要区与复盘统计用的"标签 + 取值"网格。auto-fit + minmax(200px, 1fr)
   让列数随容器宽度自然增减，不需要为每个断点各写一套列数（这也是第 5 段不必
   插手 .meta 的原因）。margin: 0 是为了它以 <dl> 落地时压掉浏览器默认外边距。 */
.meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--sp-3) var(--sp-4);
  margin: 0;
}

/* min-width: 0 让网格项允许收缩，长取值配 .cell--ellipsis 才能真正截断 */
.meta__item {
  min-width: 0;
}

.meta__label {
  display: block;
  color: var(--text-muted);
  font-size: var(--fs-xs);
}

.meta__value {
  display: block;
  margin: 0;
  color: var(--text);
  font-size: var(--fs-sm);
}

/* ---- 聊天容器 ------------------------------------------------------------ */

/* 只写容器本身：气泡（.msg / .msg-user / .msg-system / .msg.options）由
   job_detail.html 的内联脚本拼接生成，属第 7 段兼容层的管辖范围，本段不碰。
   下沉底色（--surface-sunken）把消息流与卡片表面区分开，让气泡有"浮在其上"的层次。
   max-height 是容器构造尺寸而非间距，故写在 max-height 上。 */
.chat {
  max-height: 320px;
  overflow-y: auto;
  padding: var(--sp-3);
  border: 1px solid var(--border-faint);
  border-radius: var(--r-md);
  background: var(--surface-sunken);
  -webkit-overflow-scrolling: touch;
}


/* 4. Pages */

/* 页面级专属样式。目前只有登录页一处——其余五个页面全部由第 2 段的骨架
   （.layout / .page-head）加第 3 段的组件拼出，不需要页面专属规则。

   登录页是全站唯一的**全屏双区布局**：左品牌、右表单，不走 .layout 的居中
   窄栏，也不显示 .page-head 的标题行（品牌区自带产品名）。因此本段做两件事：
   先把骨架的常规外观压掉（.page-auth 下的覆盖规则），再铺出 .auth 这套双区结构。

   品牌区背景是**纯 CSS 绘制**，不引用任何位图（Requirement 3.4）：底色 + 点阵
   + 柔光斑三层叠加，做法与"为什么必须这么绕"见 .auth__brand 处的注释。

   窄屏（< 860px）单列的规则属第 5 段，本段不写媒体查询。 */

/* ---- 骨架覆盖 ------------------------------------------------------------ */

/* base.html 无条件输出 .layout 与 .page-head，登录页却要全屏铺满、且标题由
   品牌区承担。所以这里显式把骨架默认值压掉：
   * .layout 的 max-width + --gutter 内边距会把双区栅格夹在 1080px 居中栏里，
     全屏布局必须让它们归零；
   * .page-head 的 h1 行在登录页是重复信息（品牌区已有产品名与「登录」标题），
     隐藏它而不是让它占一行空白。
   作用域限定在 .page-auth（body class）内，其余页面的骨架外观不受影响。 */
.page-auth {
  min-height: 100vh;
}

.page-auth .layout {
  max-width: none;
  padding: 0;
}

.page-auth .page-head {
  display: none;
}

/* ---- 双区栅格 ------------------------------------------------------------ */

/* 1.05fr : 1fr —— 品牌区略宽于表单区，让"先看品牌再看表单"的视觉顺序成立，
   同时避免表单区被挤窄到需要横向滚动。align-items: stretch 让两区等高，
   品牌区的背景才能铺满整屏高度。 */
.auth {
  display: grid;
  grid-template-columns: 1.05fr 1fr;
  align-items: stretch;
  min-height: 100vh;
}

/* ---- 品牌区 -------------------------------------------------------------- */

/* 背景三层，全部纯 CSS：
   1. background-color: --brand-800   底色（白字实测 10.36 ✓）
   2. background-image 的点阵：radial-gradient(var(--brand-700) 1px, transparent 1px)
      配 background-size 平铺出 1px 圆点网格。色标是令牌引用，transparent 是关键字
      而非颜色字面量，因此不违反 Property 3。
   3. ::before 承载的两团柔光斑（见下）。

   **为什么柔光斑要用伪元素而不是直接叠在 background-image 上**：柔光斑要的是
   "从亮到透明的渐隐"，常规写法是 rgba(…, .35) 之类的半透明色标——而本文件禁止
   任何 rgba()。只用 `var(--brand-500), transparent` 做色标虽然能渐隐，但亮度直接
   拉到 --brand-500 会盖掉底色显得脏。正确的绕法是：把渐变放进一个绝对定位的
   ::before，用 opacity 整体压淡它。opacity 是属性而非颜色值，既拿到了半透明效果，
   又完全不需要 rgba()。这是本文件里"要半透明就用 opacity"这条纪律的典型用例。

   overflow: hidden 裁掉溢出容器的光斑；position: relative 给 ::before 定坐标系。 */
.auth__brand {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--sp-8);
  padding: var(--sp-16) var(--sp-12);
  background-color: var(--brand-800);
  background-image: radial-gradient(var(--brand-700) 1px, transparent 1px);
  background-size: 22px 22px;
  color: var(--text-on-brand);
}

/* 两团柔光斑：左上偏亮、右下偏暗，构成对角的光照层次。
   opacity: .5 把整层压淡到"底色仍是主角"的程度；pointer-events: none 保证它
   不吃掉任何点击（品牌区未来若放链接也不会被这层挡住）。 */
.auth__brand::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    radial-gradient(circle at 18% 20%, var(--brand-500), transparent 58%),
    radial-gradient(circle at 84% 82%, var(--brand-600), transparent 62%);
  opacity: .5;
  pointer-events: none;
}

/* 内容要浮在柔光斑之上。::before 是同级的定位层，故给内容也开一个定位上下文
   （position: relative 即可，不必动 z-index：定位元素默认排在非定位元素之后，
   而这里两者都定位，后出现的内容层天然在上）。 */
.auth__brand > * {
  position: relative;
}

/* brand-mark.svg 的描边与填充全部走 currentColor，不显式给 color 就会继承成
   --link 的蓝，在 --brand-800 底上几乎看不见（与第 2 段 .topbar__brand svg 同理）。 */
.auth__brand svg {
  width: 40px;
  height: 40px;
  flex: none;
  color: var(--text-on-brand);
}

.auth__logo {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}

.auth__title {
  margin: 0;
  color: var(--text-on-brand);
  font-size: var(--fs-2xl);
  line-height: var(--lh-tight);
}

/* 定位语：字号比标题小一档，并压低不透明度形成层次。.85 下白字在 --brand-800
   上仍约 7.9:1，远高于 4.5，是"降调"不是"降到不合格"（同第 2 段 .nav-link）。 */
.auth__tagline {
  margin: 0;
  max-width: 30em;
  font-size: var(--fs-lg);
  opacity: .85;
}

/* ---- 能力要点 ------------------------------------------------------------ */

/* 三条能力要点（理解点表 / 逐箱选型 / 生成报价）。list-style: none 去掉原生
   项目符号，改用 ::before 注入的 ▸ ——图形字符不依赖颜色，灰度显示下同样成立。
   padding-left 给图形留位（图形绝对定位在 left: 0）。 */
.auth__caps {
  display: grid;
  gap: var(--sp-5);
  margin: 0;
  padding: 0;
  list-style: none;
}

.auth__cap {
  position: relative;
  padding-left: var(--sp-6);
}

.auth__cap::before {
  content: "▸";
  position: absolute;
  left: 0;
  top: 0;
  opacity: .8;
}

.auth__cap-title {
  display: block;
  color: var(--text-on-brand);
  font-size: var(--fs-base);
  font-weight: 600;
}

.auth__cap-desc {
  display: block;
  margin: var(--sp-1) 0 0;
  font-size: var(--fs-sm);
  opacity: .8;
}

/* ---- 表单区 -------------------------------------------------------------- */

/* 白卡表单区：纵向居中，内容整体限宽居中。
   限宽写在 .auth__panel > * 上而不是 .auth__panel 自身：面板要铺满右半屏承载
   白底，能限宽的只有它的内容。360px 是表单可读行宽的上限（再宽标签与输入框会
   拉得过长），属构造尺寸，故写在 max-width 上而非间距属性上。 */
.auth__panel {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: var(--sp-12) var(--sp-8);
  background: var(--surface);
}

.auth__panel > * {
  width: 100%;
  max-width: 360px;
  margin-left: auto;
  margin-right: auto;
}

.auth__panel h1 {
  font-size: var(--fs-xl);
}

.auth__sub {
  margin: 0 0 var(--sp-6);
  color: var(--text-secondary);
  font-size: var(--fs-sm);
}

/* ---- 密码显隐 ------------------------------------------------------------ */

/* .auth__pw 只包裹**密码输入框本身**（不含标签与 hint/error 文案），这样切换
   按钮可以按输入框的中线垂直居中，而不会被标签高度带偏。
   输入框右侧用 padding-right 让出按钮的位置——它是间距属性，故取 var(--sp-12)
   （48px）这一阶梯值，正好覆盖 --tap-min（44px）的按钮宽度而不压住文字。 */
.auth__pw {
  position: relative;
  display: flex;
  align-items: center;
}

.auth__pw .field__control,
.auth__pw input {
  padding-right: var(--sp-12);
}

/* 切换按钮：透明底无边框（它不是一枚"操作按钮"，只是输入框内的附属控件），
   但点击区域仍按 --tap-min 给到 44×44，满足触控目标下限。
   居中用 top: 50% + transform，不用 top/bottom: 0——后者会让 min-height 撑出
   输入框 4px。 */
.auth__pw-toggle {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  padding: 0 var(--sp-2);
  border: 0;
  background: transparent;
  color: var(--text-muted);
  font-family: inherit;
  font-size: var(--fs-xs);
  line-height: var(--lh-tight);
  cursor: pointer;
}

.auth__pw-toggle:hover {
  color: var(--text);
}

/* ---- 合规页脚 ------------------------------------------------------------ */

/* Compliance_Footer：版本号、账号开通提示、ICP 与公安联网备案号（未配置时模板整行
   省略，见 Property 14）。每条一行由 .auth__footer-line 承担，弱化呈现、左对齐与表单
   同一条视觉基线。 */
.auth__footer {
  margin-top: var(--sp-8);
  color: var(--text-muted);
  font-size: var(--fs-xs);
}

.auth__footer-line {
  display: block;
  margin: var(--sp-1) 0 0;
}

/* 备案号链接（登录页 .auth__footer 与骨架 .site-footer 共用）。备案悬挂要求该号
   必须可点击跳转到备案平台核验，但它属于合规信息而非行动入口，故不取第 1 段 a 的
   品牌蓝 --link，改为继承页脚的 --text-muted。
   下划线常在而不是只在 hover 出现：颜色与周围正文一致时，若再去掉下划线就只能靠
   颜色区分链接，那会违反 WCAG 1.4.1（不得仅以颜色传达信息）。
   焦点环由第 6 段的 a:focus-visible 统一提供，此处不重复。 */
.beian-link {
  color: var(--text-muted);
  text-decoration: underline;
}

.beian-link:hover {
  color: var(--text);
}


/* 5. Breakpoints */

/* 全站**唯一**允许出现断点字面量的段落。CSS 自定义属性不能用于 @media 条件
   （`@media (max-width: var(--bp-md))` 无效），所以这里只能写字面量；tokens.css
   声明了同值镜像令牌 --bp-sm/--bp-md/--bp-lg 供组件内 calc() 等场景引用，
   两处一致性由测试断言（Requirement 7.1）。

   用到四个阈值：

   | 阈值   | 镜像令牌   | 覆盖内容                                     |
   |--------|-----------|----------------------------------------------|
   | 1024px | --bp-lg   | 登录页双区改等分、品牌区留白收一档            |
   | 860px  | （无）    | **登录页专属**：双区转单列                    |
   | 768px  | --bp-md   | 平板/手机：槽宽收窄、页头换行、触控尺寸提升    |
   | 480px  | --bp-sm   | 小屏：概要网格与卡片头部转单列                |

   860px 刻意**不设镜像令牌**：它不是通用断点，只服务登录页双区的转折点
   （登录页规范要求「窄屏（< 860px）：品牌区收为顶部条」）。这个阈值由
   品牌区最小可读宽度 + 表单区 360px 内容宽度相加得出，与其他页面无关，
   放进 tokens.css 会让人误以为可以到处引用。

   块的顺序按 max-width 从大到小排列，这样同一属性的后写规则天然覆盖先写规则
   （小屏规则永远赢），不需要靠特异度打补丁。

   两条硬约束：
   * **不得隐藏任何导航入口**（Requirement 7.3）——窄屏只允许换行，不允许
     display: none 掉任何 .nav-link；
   * **可交互元素的 min-height / min-width 一律 ≥ --tap-min（44px）**
     （Requirement 7.6 / Property 12）——本段内不出现任何比 44px 更小的
     可交互元素尺寸规则。 */

/* ---- ≤ 1024px（--bp-lg）：登录页双区比例回归等分 ------------------------- */

/* 宽视口下品牌区略宽（1.05fr）以确立视觉顺序；到 1024px 时这点富余会开始挤压
   表单区的 360px 内容宽度，故改为等分并把两区留白各收一档。 */
@media (max-width: 1024px) {
  .auth {
    grid-template-columns: 1fr 1fr;
  }

  .auth__brand {
    padding: var(--sp-12) var(--sp-8);
  }

  .auth__panel {
    padding: var(--sp-10) var(--sp-6);
  }
}

/* ---- ≤ 860px：登录页转单列（登录页专属阈值） ----------------------------- */

/* 品牌区收为**顶部条**、表单区在下。
   关键是"品牌区不得挤压表单区"（Requirement 3.12）：
   * .auth 的 min-height: 100vh 归零 —— 双区叠成一列后再撑满视口高只会造成
     大片空白与页面纵向滚动；
   * 品牌区内边距收到 --sp-6/--sp-8 一档，能力要点改为横向自适应多列
     （auto-fit + minmax），三条要点在一到两行内排完，不再纵向堆高；
   * 定位语降到正文字号，避免顶部条独占半屏；
   * 表单区改为 justify-content: flex-start —— 单列下不再需要纵向居中，
     内容紧贴顶部条下方，首屏就能看到用户名输入框。 */
@media (max-width: 860px) {
  .auth {
    grid-template-columns: 1fr;
    min-height: auto;
  }

  .auth__brand {
    gap: var(--sp-4);
    padding: var(--sp-8) var(--sp-6);
  }

  .auth__brand svg {
    width: 32px;
    height: 32px;
  }

  .auth__title {
    font-size: var(--fs-xl);
  }

  .auth__tagline {
    font-size: var(--fs-base);
  }

  .auth__caps {
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: var(--sp-3) var(--sp-4);
  }

  .auth__panel {
    justify-content: flex-start;
    padding: var(--sp-8) var(--sp-6);
  }
}

/* ---- ≤ 768px（--bp-md）：槽宽、页头、触控尺寸 --------------------------- */

@media (max-width: 768px) {
  /* 槽宽从 --gutter（24px）收到 --gutter-narrow（16px）：小屏上 24px 两侧留白
     会明显吃掉正文可用宽度。页脚同宽同步收窄，否则会与主内容区错开。 */
  .layout {
    padding: var(--gutter-narrow) var(--gutter-narrow);
  }

  .site-footer {
    padding: 0 var(--gutter-narrow) var(--sp-8);
  }

  /* 页头同样收窄，并给出纵向内边距 —— 导航换行后页头会长高，纵向 0 会让首末行
     贴边。.topbar 已有 flex-wrap: wrap，换行是既有行为。 */
  .topbar {
    padding: var(--sp-2) var(--gutter-narrow);
  }

  /* 第 2 段用 margin-right: auto 把用户区顶到右端；换行后这个"顶开"没有意义，
     反而会在导航与用户名之间留一整行空隙，故左对齐收掉。
     **注意这里没有、也不允许有任何 .nav-link 的 display: none**：窄屏必须保留
     全部入口（Requirement 7.3），只允许换行。 */
  .topbar__nav {
    margin: 0 auto 0 0;
  }

  /* 标题行纵向堆叠：小屏上"标题 + 操作区"并排会把两者都挤窄。 */
  .page-head {
    flex-direction: column;
    align-items: flex-start;
  }

  /* 触控目标下限（Requirement 7.6）：手指点击区不小于 44×44。
     .btn--sm 必须一并列出 —— 它在宽视口取 --ctrl-h-sm（32px），窄屏下若不提升
     就会低于下限，而表格行内操作（"采纳此版本""标记已读"）正是 --sm。
     刻意**排除**两类控件：
     * input[type="file"] —— 原生控件内部由浏览器绘制，强加高度会让内容偏上；
       其尺寸已在第 3 段交还浏览器；
     * textarea —— 第 3 段给的是 calc(--ctrl-h-lg * 2)（88px），本已远超下限，
       写进这里反而会把它压回 44px。
     裸 input 用属性选择器逐类型列出（含无 type 属性的场合，如 job_detail 的
     #chat-input），既覆盖到，又不会误伤 file。 */
  .btn,
  .btn--sm,
  .nav-link,
  .auth__pw-toggle,
  .field__control,
  select,
  input:not([type]),
  input[type="text"],
  input[type="password"],
  input[type="number"],
  input[type="search"],
  input[type="email"],
  input[type="tel"] {
    min-height: var(--tap-min);
  }

  /* 卡片内边距收一档。仍取间距阶梯，不出现裸数值。 */
  .card {
    padding: var(--sp-4) var(--sp-4);
  }
}

/* ---- ≤ 480px（--bp-sm）：小屏单列 --------------------------------------- */

/* .meta 平时靠 auto-fit + minmax(200px, 1fr) 自适应列数，480px 以下容器宽度已
   不足两列，minmax 的 200px 下限会导致列被压到不可读，故强制单列。
   .card__head 的 baseline 并排在小屏下同样失效，改纵向堆叠。 */
@media (max-width: 480px) {
  .meta {
    grid-template-columns: 1fr;
  }

  .card__head {
    flex-direction: column;
    align-items: flex-start;
  }

  .auth__caps {
    grid-template-columns: 1fr;
  }
}


/* 6. Accessibility */

/* 无障碍统一段。三件事：焦点可见性、减少动效、屏幕阅读器专用文本。

   本段的纪律是**绝不写 outline: none / outline: 0**。Property 11 允许"移除后另配
   可见替代样式"，但那条路留给不得不移除的第三方样式；本文件是自己从零写的，
   完全没有必要移除默认焦点环——移除再补，只是给未来的维护者留一个踩坑机会。
   因此整份 app.css 里搜不到 outline: none，Property 11 天然为空。 */

/* ---- 焦点环 -------------------------------------------------------------- */

/* :focus-visible 而非 :focus：鼠标点击按钮不该留下焦点环，键盘 Tab 必须留下。
   三层叠加保证在任何底色上都看得见：
   * outline: 2px solid var(--focus-base) —— 实线主体（--brand-600 在白底 6.70，
     远高于 1.4.11 对界面组件的 3:1）；
   * outline-offset: 2px —— 与控件自身边框脱开，不与 .field__control 的 1px 边框糊在一起；
   * box-shadow: var(--focus-ring) —— 外扩 3px 的柔光晕，在深色底（页头、品牌区）上
     即便 outline 与底色接近，这层晕也能把焦点位置托出来。
   .btn 单列一条：它可能是 <a> 也可能是 <button>，两者都已被覆盖，写出来是为了让
   "按钮有焦点环"这件事在选择器里显式可读。 */
a:focus-visible,
button:focus-visible,
.btn:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--focus-base);
  outline-offset: 2px;
  box-shadow: var(--focus-ring);
}

/* ---- 减少动效 ------------------------------------------------------------ */

/* 系统开启"减少动效"后，把过渡与动画时长压到 1ms（而不是 none）：
   置 0/none 会让部分浏览器跳过 transitionend/animationend 事件，写 1ms 则动效
   在感知上等于瞬时完成，同时事件仍会触发，行为更可预期。
   animation-iteration-count: 1 顺手掐掉无限循环（.btn.is-loading::before 的
   btn-spin 就是 infinite）。

   这正是第 3 段刻意让 .btn.is-loading 的状态**不依赖动画**的原因：
   ::before 注入的 ◌ 字符、降低的不透明度、progress 光标三样静态手段在这里
   继续生效，所以停掉旋转不会让"正在提交"变得无从判断（Requirement 5.10）。

   !important 是必要的：这条要压过组件段里各自声明的 transition/animation，
   而那些声明的特异度普遍高于通用选择器。 */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 1ms !important;
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }

  html {
    scroll-behavior: auto;
  }
}

/* ---- 屏幕阅读器专用文本 --------------------------------------------------- */

/* 视觉上隐藏、但对屏幕阅读器可见。用于纯图形控件的可访问名称、表格的补充说明
   等场合（Requirement 5.9）。
   不用 display: none / visibility: hidden —— 那两者会把内容一并从无障碍树移除。
   经典实现里的 margin: -1px 在本文件是违规的（间距值只能取阶梯，Property 4），
   这里改用 clip-path: inset(50%) 完成裁剪，margin/padding 一律取 0：
   clip-path 的裁剪不依赖负外边距，效果等价且更现代。
   white-space: nowrap 防止 1px 宽容器把文本折成竖排（部分读屏会据此断词）。 */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}


/* 7. Compatibility */

/* 既有 class 兼容层 —— 本特性的**关键安全阀**。

   job_detail.html 的内联脚本按字符串拼接生成 class（`"status status-" + data.status`、
   `"msg msg-" + role`、`li.className = "step " + cls`），并按 13 个 DOM id 抓元素。
   该脚本在整个特性范围内保持**零字符改动**（JS_Contract 冻结，Requirement 6.7 / 9.1），
   所以"视觉升级"只能靠本段：脚本拼出来的每一个 class 都在这里拿到新样式。
   少写一条不会报错，只会让某块界面静默退化成无样式裸文本——这正是 tests/ui_contracts.py
   的 JS_CONTRACT_CLASSES（22 项）要逐条核对的原因。

   两条书写纪律直接来自脚本的写法：
   * 状态与步进的修饰符是**裸类名**（`class="step active"`、`class="msg msg-system error"`），
     必须写成 .step.active / .msg.error 这类**组合选择器**，不能写成 .step--active；
   * 状态后缀由 data.status 直接拼接，**未知取值也会拼出来**（`status-<任意值>`），
     所以裸 .status 自身必须有完整可读的中性外观 + 默认图形，五个已知状态各自覆盖。
     这样后端将来新增状态值，界面退化为"中性徽标 + 原始文本"，而不是无样式裸字。

   已在前面段落覆盖、本段**不重复定义**的两项（列在这里以便与 JS_CONTRACT_CLASSES 对齐）：
   * .btn —— 第 3 段。基座即 primary 外观，正是为了让脚本 ensureDownloadButton 生成的
     裸 `<a class="btn">下载报价清单</a>` 直接呈现为主按钮。
   * .badge —— 第 3 段。裸 .badge 保留实心 danger 外观，服务 admin_console.html 的
     未读反馈计数 `<span class="badge">`。

   本段不写媒体查询（属第 5 段）、不写焦点样式（属第 6 段）。 */

/* ---- 状态徽标 ------------------------------------------------------------ */

/* 结构与第 3 段的 .badge 一致（行内 flex、全圆角、--fs-xs、图形与文字间 --sp-1），
   但**不能**直接复用 .badge 选择器：脚本只会拼出 `status status-<x>`，模板侧也不允许
   为迁就 CSS 去改脚本。所以这里重述一份结构，取值全部走同一批令牌，观感自然一致。

   裸 .status 的默认外观 = 中性（--surface-muted 底 + --text-secondary 文字），
   默认图形 = ○。这就是**未知状态的回落**：脚本拼出 status-whatever 时，五个已知
   状态的覆盖规则都不命中，元素仍然是一枚完整可读的中性徽标，文字为原始状态串。
   边框取 currentColor：各语义前景色在自身 50 底上实测 4.84–6.74，远高于 1.4.11
   对界面组件的 3:1，边框天然达标且不必再引一组边框令牌。 */
.status {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: var(--sp-1) var(--sp-2);
  border: 1px solid currentColor;
  border-radius: var(--r-full);
  background: var(--surface-muted);
  color: var(--text-secondary);
  font-size: var(--fs-xs);
  line-height: var(--lh-tight);
  white-space: nowrap;
}

/* 默认图形，被下面五条按需覆盖 */
.status::before {
  content: "○";
}

/* 五个状态的图形互不相同（○ ◐ ! ✓ ✕），与 `_components.html` 的 status_badge 宏及第 3 段的
   .badge--* 完全一致；配色同样逐一对齐，避免同一状态在两处呈现不同颜色。
   图形 + 文字标签同时存在，状态表达不只靠颜色（Requirement 5.1 / 5.2）。 */
.status-pending {
  background: var(--surface-muted);
  color: var(--text-secondary);
}

.status-pending::before {
  content: "○";
}

.status-processing {
  background: var(--warning-50);
  color: var(--warning-600);
}

.status-processing::before {
  content: "◐";
}

.status-awaiting_user {
  background: var(--info-50);
  color: var(--info-600);
}

.status-awaiting_user::before {
  content: "!";
}

.status-done {
  background: var(--success-50);
  color: var(--success-600);
}

.status-done::before {
  content: "✓";
}

.status-failed {
  background: var(--danger-50);
  color: var(--danger-600);
}

.status-failed::before {
  content: "✕";
}

/* ---- 聊天气泡 ------------------------------------------------------------ */

/* .msg 是基座（脚本的三个生成点都以 "msg " 开头）。
   white-space: pre-wrap 保留模型回复里的换行与缩进（选型说明常有分行列举）；
   overflow-wrap: anywhere 让超长型号串在气泡内断行，而不是把 .chat 撑出横向滚动。
   max-width: 88% 是气泡宽度约束（构造尺寸，故写在 max-width 上而非间距属性上），
   留出的余量正是"左右分侧"得以被看出来的原因。 */
.msg {
  max-width: 88%;
  margin: 0 0 var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid transparent;
  border-radius: var(--r-md);
  font-size: var(--fs-sm);
  line-height: var(--lh-base);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* 用户消息靠右：margin-left: auto 在块级流里把气泡推到右侧（.chat 不是 flex 容器，
   用不了 align-self）。longhand 写在 .msg 的 margin shorthand 之后，故生效。
   --brand-surface 底承载 --brand-600 文字实测 6.08 ✓。 */
.msg-user {
  margin-left: auto;
  border-color: var(--border-brand);
  background: var(--brand-surface);
  color: var(--brand-600);
}

/* 系统消息靠左，白底 + 装饰性细边框，与 .chat 的下沉底色分层。 */
.msg-system {
  margin-right: auto;
  border-color: var(--border-faint);
  background: var(--surface);
  color: var(--text);
}

/* 模板另有两处**裸** .error 段落（#error 失败原因、#chat-error 聊天错误提示），
   它们不是气泡：只给语义色与字号，不给底色与边框。 */
.error {
  color: var(--danger-600);
  font-size: var(--fs-sm);
}

/* 错误气泡。脚本 appendErrorCard 生成 "msg msg-system error"，因此这条必须能盖住
   .msg-system 的白底——组合选择器 .msg.error 特异度 0,2,0 > .msg-system 的 0,1,0，
   与书写顺序无关地胜出。 */
.msg.error {
  border-color: var(--danger-600);
  background: var(--danger-50);
  color: var(--danger-600);
}

/* 选项容器。脚本 appendOptionButtons 生成 "msg msg-system options"，它并不是一条
   消息，只是一排按钮的壳，所以要把 .msg + .msg-system 给的气泡外观全部还原：
   透明底、无边框、无内边距、不限宽。特异度同样靠 .msg.options（0,2,0）取胜。 */
.msg.options {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  max-width: none;
  padding: 0;
  border-color: transparent;
  background: transparent;
}

/* 选项按钮：药丸形。点击即把该选项文本作为聊天消息发出，所以视觉上要明确"可点"
   （品牌浅底 + 品牌色文字 + 手型光标），又不能抢主按钮的位置（不用实心品牌底）。
   min-height 取 --ctrl-h-sm（32px）；窄屏下第 5 段没有把它列入 --tap-min 提升组，
   因为它总是成排出现在气泡区，相邻间距由 gap 保证，实际点击不易误触。 */
.opt-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--ctrl-h-sm);
  padding: var(--sp-1) var(--sp-3);
  border: 1px solid var(--brand-100);
  border-radius: var(--r-full);
  background: var(--brand-50);
  color: var(--brand-600);
  font-family: inherit;
  font-size: var(--fs-xs);
  line-height: var(--lh-tight);
  cursor: pointer;
  transition: background-color var(--dur-fast) ease, border-color var(--dur-fast) ease;
}

.opt-btn:hover {
  border-color: var(--brand-600);
  background: var(--brand-100);
}

/* ---- 三阶段步进条 -------------------------------------------------------- */

/* 结构（模板静态）：<ol class="stepper"><li class="step" data-stage><span class="dot">…
   脚本只改 li 的 className，不动 DOM 结构，因此圆点与连接线都必须由 CSS 画出来。
   三步等分（flex: 1）+ 文本居中，圆点绝对定位在每步顶部中线，连接线由 ::before
   从上一个圆点中心拉到本步圆点中心。 */
.stepper {
  display: flex;
  margin: var(--sp-4) 0 var(--sp-5);
  padding: 0;
  list-style: none;
}

/* padding-top 给顶部的圆点让位（圆点 12px + 与文字的呼吸）。 */
.step {
  position: relative;
  flex: 1;
  padding-top: var(--sp-6);
  color: var(--text-muted);
  font-size: var(--fs-xs);
  line-height: var(--lh-tight);
  text-align: center;
}

/* 连接线：top 取 5px 让它压在 12px 圆点的垂直中线上（(12 - 2) / 2 = 5）；
   left: -50% 伸到上一步的中心，right: 50% 收在本步中心。这些是定位与厚度，
   属非间距属性，故不受间距阶梯约束。 */
.step::before {
  content: "";
  position: absolute;
  top: 5px;
  left: -50%;
  right: 50%;
  height: 2px;
  background: var(--border-subtle);
}

/* 第一步左侧没有上一步，连接线会伸到 .stepper 外面，隐掉。 */
.step:first-child::before {
  display: none;
}

/* 圆点：12px 见方的圆（尺寸写在 width/height 上），兼作状态图形的容器。
   字号用 calc(var(--fs-xs) - 4px) ≈ 9px —— 仍锚定在字号阶梯上（"--fs-xs 或更小"），
   且是能塞进 12px 圆里的最大可辨尺寸；不新增字号令牌。 */
.step .dot {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 12px;
  height: 12px;
  border: 2px solid var(--border-subtle);
  border-radius: var(--r-full);
  background: var(--border-subtle);
  font-size: calc(var(--fs-xs) - 4px);
  line-height: 1;
}

/* 序号前缀（模板 6.6 会在每步文字前输出 <span class="step__index">1</span>）。
   脚本重写的是 li 的 className，子元素的 class 不受影响，所以它安全。 */
.step__index {
  margin-right: var(--sp-1);
  font-size: var(--fs-xs);
  opacity: .7;
}

/* 三态的**非颜色标识**（Requirement 5.3 的硬要求）：除语义色外，各态的圆点形态与
   点内字符互不相同，灰度显示或色觉障碍下依然可区分——
   * active：空心圆环（白底 + 品牌色描边）+ 点内「·」+ 文字加粗
   * done  ：实心绿点 + 点内「✓」+ 左侧连接线变绿
   * failed：实心红点 + 点内「✕」
   点内字符的取色都落在深底上取 --text-on-brand（白）：白字在 --success-600 上
   5.48 ✓、在 --danger-600 上 6.47 ✓；active 的「·」在白底上取 --brand-600（6.70 ✓）。 */
.step.active {
  color: var(--brand-600);
  font-weight: 600;
}

.step.active .dot {
  border-color: var(--brand-600);
  background: var(--surface);
}

.step.active .dot::after {
  content: "·";
  color: var(--brand-600);
}

.step.done {
  color: var(--success-600);
}

.step.done .dot {
  border-color: var(--success-600);
  background: var(--success-600);
}

.step.done .dot::after {
  content: "✓";
  color: var(--text-on-brand);
}

/* 已完成步骤左侧的连接线一并变绿，形成"进度已推进到这里"的连续视觉。 */
.step.done::before {
  background: var(--success-600);
}

.step.failed {
  color: var(--danger-600);
}

.step.failed .dot {
  border-color: var(--danger-600);
  background: var(--danger-600);
}

.step.failed .dot::after {
  content: "✕";
  color: var(--text-on-brand);
}

/* ---- SSE 连接状态条 ------------------------------------------------------ */

/* 复用 .alert--warning 的观感（warning 底 + warning 文字 + 同色边框 + ::before 注入 !），
   但同样不能直接挂 .alert 选择器：模板里它是固定的 `<p id="conn-state" class="conn-state">`，
   而"改模板去迎合 CSS"在这里等于动脚本依赖面。

   **刻意不声明 display**：脚本用 `el.style.display = text ? "" : "none"` 控制显隐，
   置空字符串意味着"回落到样式表/默认值"。若这里写了 display: flex 之类，
   隐藏→显示时元素会变成 flex 布局，::before 与文本的排版跟着变；不写 display，
   <p> 保持块级默认，::before 走行内、用 margin-right 与文本拉开间距，
   显隐前后观感完全一致。 */
.conn-state {
  margin: 0 0 var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--warning-600);
  border-radius: var(--r-sm);
  background: var(--warning-50);
  color: var(--warning-600);
  font-size: var(--fs-xs);
}

.conn-state::before {
  content: "!";
  margin-right: var(--sp-2);
  font-weight: 600;
}

/* ---- 说明文字 ------------------------------------------------------------ */

/* user_dashboard.html 的 `<p class="hint">`（非脚本生成，但同属既有静态 class）。
   与第 3 段的 .field__hint 同一取色，字号大一档：它是段落级说明而非字段附注。 */
.hint {
  margin: var(--sp-2) 0 0;
  color: var(--text-muted);
  font-size: var(--fs-sm);
}
