/* ============================================================================
 * theme.css —— 设计令牌层与语义组件
 *
 * 为什么要有这个文件
 * --------------------------------------------------------------------------
 * 改版前，同一个「卡片」在 index.html 里写作
 *     glass rounded-3xl p-8 card-hover
 * 在 app.js 的 renderSubjects() 里写作
 *     bg-white rounded-2xl p-4 border-gray-100 shadow-[0_2px_8px_rgba(0,0,0,0.04)]
 * 颜色则是 text-slate-800 / text-gray-800 / #4f46e5 / rgba(31,38,135,.05) 混用。
 *
 * 后果是 2026-08-28 实测到的这个 bug：夜间模式下科目录入区是 7 张纯白卡片，
 * 「添加科目」的文字对比度只有 1.35:1（WCAG AA 要求 4.5:1），基本看不见。
 * 根因不是「谁忘了写 dark:」—— 是**根本没有「卡片」这个概念**，
 * 只有每处各抄一遍的类名串，于是每新增一处 innerHTML 就会再漏一次。
 *
 * 所以这里定义两样东西：
 *   1. 颜色令牌：亮/暗各一套同名变量。换主题 = 换变量值。
 *   2. 语义组件类：.card / .field / .btn / .chip ...，内部只引用变量。
 *
 * 由此得到一条硬性约束（test/uiInvariants.test.js 会守）：
 *   **组件类的规则体里不允许出现任何裸色值**，一律走 var(--…)。
 *   同时亮暗两块必须定义完全相同的变量名集合 —— 少定义一个就是一处暗色 bug，
 *   这条能被自动抓出来，不用靠肉眼看夜间截图。
 *
 * 与 Tailwind 的关系
 * --------------------------------------------------------------------------
 * 项目没有构建步骤（devDependencies 为空、部署就是拷文件 + pm2 restart），
 * Tailwind 走 Play CDN 在运行时生成。本文件是手写的本地 CSS，不依赖 CDN ——
 * CDN 不可达时，页面至少还剩下这套骨架，而不是完全裸奔。
 * 布局仍然用 Tailwind 的 grid/flex/间距工具类，本文件只管**表面与配色**。
 * ========================================================================= */


/* ============================================================================
 * 1. 结构令牌（不随主题变化，故不放进下面的亮/暗两块）
 *
 * 放在 html 而不是 :root，是为了让「主题变量」与「结构变量」在选择器层面
 * 就分得开 —— 测试据此比对亮暗两块的变量名集合，不会被间距、圆角这些
 * 本来就不该有暗色版本的令牌干扰。html 与 :root 是同一个元素，运行时等价。
 * ========================================================================= */
html {
  /* 字体：中文优先。
     改版前用 @import 拉 Google Fonts 的 Inter —— 它既是渲染阻塞（写在 <style> 里），
     又不含任何中文字形，中文一律回落系统字体。对国内用户是纯风险、零收益，已删。
     顺序按「每种文字各自的首选」排：macOS 靠 -apple-system 同时覆盖拉丁与中文，
     Windows 拉丁走 Segoe UI、中文回落到微软雅黑。字体回退是逐字符的，所以
     把 Segoe UI 排在中文字体前面不会影响中文。 */
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
               "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
               "Liberation Mono", "Courier New", monospace;

  /* 圆角：三档就够。改版前 rounded-xl / 2xl / 3xl / full 混用了 4 档以上，
     卡片、面板、输入框各自为政，看起来像是不同时期拼起来的。 */
  --radius-sm: 8px;
  --radius: 12px;
  --radius-lg: 16px;
  --radius-full: 999px;

  /* 间距：4px 基准 */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;

  /* 头部高度。scroll-margin-top、sticky 导航条的 top 都按它让位 ——
     改版前 header 是 sticky 75px 而全站 0 处 scroll-margin-top，
     4 处 scrollIntoView({block:'start'}) 每次都把标题压在 header 底下。

     ⚠️ 2026-08-29：这个值由 theme-boot.js 的 syncHeaderHeight() 按**实测**改写。
     写死 64px 在窄屏是错的 —— header 会换行，390px 下实测 106px，
     于是 sticky 的分段导航有 34px 藏在 header 后面，手机上等于不可用。
     下面这个 64px 只是首帧的兜底值。 */
  --header-h: 64px;

  /* header 自己的最小高度，**刻意与 --header-h 分开**。
     两者共用一个变量会成环：JS 把实测高度写进 --header-h → min-height 跟着变大
     → 窄屏撑到 106px 之后，即使拉回宽屏也被 min-height 钉在 106px 下不来。
     实测就踩到了这一条（宽屏量出来还是 106）。 */
  --header-h-base: 64px;

  /* 动效。统一时长与缓动，避免 duration-200/300/400 随手乱选。 */
  --ease: cubic-bezier(.4, 0, .2, 1);
  --dur-fast: 120ms;
  --dur: 200ms;

  --ring-width: 3px;
}


/* ============================================================================
 * 2. 颜色令牌 · 亮色
 *
 * 用 HSL 分量存（不带 hsl() 包装），这样可以在组件里派生透明度：
 *     background: hsl(var(--brand) / .12)
 * 这是把「同一个品牌色的浅底版本」从硬编码里解放出来的关键 ——
 * 改版前每处 tint 都是手写的一串 rgba，改主色要全局搜替换。
 * ========================================================================= */
:root,
:root[data-theme="light"] {
  /* —— 表面：页面底 → 卡片 → 卡片内嵌 → 更深内嵌 —— */
  --bg:            220 24% 97%;
  --surface:         0  0% 100%;
  --surface-2:     220 24% 97%;
  --surface-3:     220 18% 93%;
  --surface-inset: 220 20% 98%;

  /* —— 边框 —— */
  --border:        220 14% 90%;
  --border-strong: 220 13% 80%;

  /* —— 文字。muted / subtle 的明度是按对比度反推的，不是随手挑的。
        判据要用**最深的卡片底** surface-3 而不是纯白，否则内嵌面板里就不达标：
          muted  hsl(215 18% 40%) 在 surface-3 上约 6.4:1
          subtle hsl(215 14% 42%) 在 surface-3 上约 4.7:1，过 AA 正文线
        2026-08-28 首版把 subtle 定在 50%，实测只有 4.2:1 —— 差一点也是不合格，
        这就是为什么这些值必须算而不是估。 —— */
  --text:        222 47% 11%;
  --text-muted:  215 18% 40%;
  --text-subtle: 215 14% 42%;
  --text-on-accent: 0 0% 100%;

  /* —— 品牌色 —— */
  --brand:        243 72% 52%;
  --brand-hover:  243 72% 45%;
  --brand-soft:   243 75% 96%;
  --brand-border: 243 60% 86%;

  /* —— 语义色。每种三件套：前景 / 浅底 / 边框。
        前景色的明度是算出来的，判据取**两个最严场景**都过 4.5:1：
          ① 压在自己的浅底上（chip--ok 那种）
          ② 压在最深的卡片底 surface-3 上
        绿色和黄色最容易翻车 —— Tailwind 的 emerald-500 / amber-500
        在白底上分别只有 2.5:1 和 1.9:1，emerald-600 压在浅绿底上也只有 3.3:1。
        2026-08-28 首版 ok 定在 28% 实测 4.43:1，就差那么一点也是不合格。 —— */
  --ok:        160 84% 25%;
  --ok-soft:   152 76% 95%;
  --ok-border: 156 60% 82%;

  --warn:        28 88% 33%;
  --warn-soft:   45 95% 94%;
  --warn-border: 40 85% 78%;

  --danger:        350 74% 44%;
  --danger-soft:   352 90% 96%;
  --danger-border: 350 80% 87%;

  --info:        215 88% 44%;
  --info-soft:   214 95% 95%;
  --info-border: 214 85% 85%;

  /* —— 阴影。亮色下靠阴影建立层次；克制，最多两档。 —— */
  --shadow-1: 0 1px 2px hsl(222 47% 11% / .06), 0 1px 1px hsl(222 47% 11% / .04);
  --shadow-2: 0 4px 12px hsl(222 47% 11% / .07), 0 1px 3px hsl(222 47% 11% / .05);
  --shadow-3: 0 12px 32px hsl(222 47% 11% / .12), 0 2px 8px hsl(222 47% 11% / .06);

  /* —— 遮罩与焦点环 —— */
  --overlay:  222 47% 20%;
  --overlay-alpha: .45;
  --ring:     243 72% 52%;
  --ring-alpha: .28;

  /* —— 骨架屏 —— */
  --skeleton:      220 16% 92%;
  --skeleton-shine: 220 20% 96%;

  /* —— 深底面板上的前景 ——
     管理端的 header 和程序日志区在亮暗两个主题下**都是深色**（自身自洽：深底 + 浅字），
     所以这两个值在两个主题块里刻意相同。定成令牌而不是就地写 hsl()，
     是为了让「组件区不许出现裸色值」那条守卫也能盖到管理端。 */
  --tint-on-dark:  0 0% 100%;
  --text-on-dark:  215 20% 78%;
}


/* ============================================================================
 * 3. 颜色令牌 · 暗色
 *
 * 变量名必须与上面的亮色块**完全一致**（测试会比对两个集合）。
 * 少一个就意味着某处在暗色下会拿到亮色的值 —— 那正是改版前 7 张白卡片的成因。
 * ========================================================================= */
:root[data-theme="dark"] {
  --bg:            222 44%  8%;
  --surface:       222 38% 12%;
  --surface-2:     222 34% 15%;
  --surface-3:     222 30% 19%;
  --surface-inset: 222 40% 10%;

  --border:        220 20% 22%;
  --border-strong: 218 18% 32%;

  /* 暗色下正文不用纯白：纯白在深底上会产生光晕，长时间看很累。 */
  --text:        213 32% 93%;
  --text-muted:  215 20% 72%;
  --text-subtle: 215 16% 64%;
  /* 暗色下主按钮的文字必须是**深色**。
     下面的 --brand 在暗色里被提亮成浅薰衣草（238 90% 76%），
     那是为了当文字/边框用；但 .btn--primary 拿它当**背景**。
     首版这里沿用了白字，实测「智能生成规划」「AI 查询本地中考数据」
     对比度只有 2.86:1 —— 页面上最重要的两个按钮反而最看不清。
     换成深字后是 6.4:1。 */
  --text-on-accent: 240 60% 14%;

  /* 暗底上品牌色要提亮，否则饱和的靛蓝几乎看不清。 */
  --brand:        238 90% 76%;
  --brand-hover:  238 95% 82%;
  --brand-soft:   240 40% 20%;
  --brand-border: 240 40% 32%;

  /* 语义色的浅底（*-soft）在暗色下要压得比想象中更暗、更不饱和。
     2026-08-28 线上实测：warn-soft 原定 36 45% 16%，「激活 AI 查询额度」
     那张卡合成出来是 rgb(59,44,22)，而周围普通卡是 rgb(19,26,42) ——
     它成了全页唯一跳出来的色块，而它承载的只是个次要操作。
     压低之后仍然认得出是「琥珀色的提示卡」，但不再抢主操作的注意力。
     方向上是安全的：底色变暗只会让浅色前景的对比度更高。 */
  --ok:        158 62% 62%;
  --ok-soft:   162 30% 13%;
  --ok-border: 160 28% 26%;

  --warn:        38 92% 64%;
  --warn-soft:   36 32% 13%;
  --warn-border: 36 34% 28%;

  --danger:        350 90% 74%;
  --danger-soft:   350 30% 14%;
  --danger-border: 350 32% 30%;

  --info:        212 92% 72%;
  --info-soft:   214 34% 15%;
  --info-border: 214 36% 30%;

  /* 暗色下阴影几乎不可见，层次改由 border + surface 明度差承担；
     这里保留很淡的阴影只为托一下浮层。 */
  --shadow-1: 0 1px 2px hsl(0 0% 0% / .3);
  --shadow-2: 0 4px 14px hsl(0 0% 0% / .38);
  --shadow-3: 0 16px 40px hsl(0 0% 0% / .5);

  --overlay:  222 47%  4%;
  --overlay-alpha: .66;
  --ring:     238 90% 76%;
  --ring-alpha: .38;

  --skeleton:       222 30% 18%;
  --skeleton-shine: 222 26% 24%;

  /* 深底面板的前景。与亮色块同值 —— 那两个面板两个主题下都是深色 */
  --tint-on-dark:  0 0% 100%;
  --text-on-dark:  215 20% 78%;
}


/* ============================================================================
 * 4. 基础层
 * ========================================================================= */

html {
  /* 改版前 overflow-x:hidden 挂在 body 上，而 html 是 visible ——
     拦不住装饰光晕的溢出。实测手机 390px 视口下 scrollWidth 到了 584px，
     溢出 200px（超过半个屏幕宽），页面可以横向甩出一大片空背景。 */
  overflow-x: hidden;
  scroll-behavior: smooth;
}

/* 默认边框色跟着主题走。
   Tailwind 的 preflight 给 *、::before、::after 都写了 border-color: #e5e7eb，
   于是任何只写了 `border`（有宽度、没指定颜色）的元素在暗色下都会亮出一道浅灰线 ——
   实测管理端的批量查询、数据复核、公告三个面板都中招。
   用 `:root *` 提一级特异性压过 preflight（它是 0,0,0）；
   显式写了 border-emerald-100 之类的地方特异性相同但顺序在后，仍然生效。 */
:root *,
:root ::before,
:root ::after {
  border-color: hsl(var(--border));
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}

body {
  font-family: var(--font-sans);
  background-color: hsl(var(--bg));
  color: hsl(var(--text));
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* 主题切换时整页做一次短过渡，避免闪。
   但排除 transform —— 卡片 hover 的位移不该被拖慢。 */
body,
body * {
  transition-property: background-color, border-color, color, fill, stroke;
  transition-duration: var(--dur);
  transition-timing-function: var(--ease);
}
/* 主题切换的瞬间反而要关掉过渡，否则几百个元素同时补间会卡一下。
   JS 切主题时给 <html> 加上这个类，下一帧移除。 */
html.theme-switching,
html.theme-switching * {
  transition: none !important;
}

/* 数字统一等宽，让上下两行的分数能对齐。
   报告里全是「91/120」「75.8%」这类数字，比例字宽下会参差不齐。 */
.num,
.stat__value,
input[type="number"],
table td,
table th {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* 所有 scrollIntoView 的落点都挂这个类，避开 sticky header。 */
.scroll-anchor {
  scroll-margin-top: calc(var(--header-h) + var(--space-4));
}

/* 报告内部的板块还要多让出导航条的高度 —— 窄屏下导航是横在顶上的 sticky 条，
   只扣 header 的话跳过去仍然会被它盖住标题（实测「学科诊断 7 科」那一行
   被压掉一半）。--nav-h 不写死：宽屏是左侧竖栏（不遮挡，值为 0），
   窄屏是横条且会换行变高，见「报告分段导航」一节。 */
#analysisResults .scroll-anchor,
#analysisResults [data-report-section] {
  scroll-margin-top: calc(var(--header-h) + var(--nav-h, 0px) + var(--space-5));
}

::selection {
  background: hsl(var(--brand) / .22);
  color: hsl(var(--text));
}

/* 滚动条 */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: hsl(var(--border-strong) / .8);
  border-radius: var(--radius-full);
  border: 2px solid transparent;
  background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background-clip: content-box; background-color: hsl(var(--text-subtle) / .6); }

/* 统一焦点样式。键盘可达性在改版前完全没管过。 */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid hsl(var(--ring));
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}


/* ============================================================================
 * 5. 卡片 —— 三档层级
 *
 * 改版前五张卡同尺寸、同圆角、同间距，靠左侧一条彩色竖条（紫/粉/绿）区分，
 * 但那三个颜色没有任何语义，用户第一眼看不出该从哪开始。
 * 现在改成明确的三档：主 / 次 / 内嵌。
 * ========================================================================= */

.card {
  background: hsl(var(--surface));
  border: 1px solid hsl(var(--border));
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
  padding: var(--space-6);
  position: relative;
}

/* 主卡：用户真正要在这里输入东西（学生档案、学情录入）。
   靠更大的内距和更实的边框拉开层级，而不是靠颜色。 */
.card--primary {
  border-color: hsl(var(--border-strong));
  box-shadow: var(--shadow-2);
  padding: var(--space-8);
}

/* 内嵌卡：卡片内部的分组（分数线面板、政策元信息、科目录入格）。
   不加阴影 —— 阴影套阴影会糊成一团。 */
.card--inset {
  background: hsl(var(--surface-inset));
  border: 1px solid hsl(var(--border));
  border-radius: var(--radius);
  box-shadow: none;
  padding: var(--space-4);
}

/* 提示类卡片：带语义色的浅底 */
.card--accent { border-left: 3px solid hsl(var(--brand)); }
.card--warn   { background: hsl(var(--warn-soft)); border-color: hsl(var(--warn-border)); }
.card--danger { background: hsl(var(--danger-soft)); border-color: hsl(var(--danger-border)); }
.card--ok     { background: hsl(var(--ok-soft)); border-color: hsl(var(--ok-border)); }
.card--info   { background: hsl(var(--info-soft)); border-color: hsl(var(--info-border)); }

.card--interactive {
  transition: border-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease),
              transform var(--dur) var(--ease);
}
.card--interactive:hover {
  border-color: hsl(var(--border-strong));
  box-shadow: var(--shadow-2);
}
@media (hover: hover) {
  .card--interactive:hover { transform: translateY(-2px); }
}

.card--compact { padding: var(--space-4); }


/* ============================================================================
 * 6. 区块标题
 * ========================================================================= */

.section-title {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: 1.0625rem;
  font-weight: 700;
  color: hsl(var(--text));
  letter-spacing: -.01em;
}
.section-title--lg { font-size: 1.25rem; }

.section-title__icon {
  flex: none;
  width: 34px;
  height: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius);
  background: hsl(var(--brand-soft));
  color: hsl(var(--brand));
  font-size: 1rem;
}
.section-title__icon svg { width: 18px; height: 18px; }

.section-title__icon--ok     { background: hsl(var(--ok-soft));     color: hsl(var(--ok)); }
.section-title__icon--warn   { background: hsl(var(--warn-soft));   color: hsl(var(--warn)); }
.section-title__icon--danger { background: hsl(var(--danger-soft)); color: hsl(var(--danger)); }
.section-title__icon--info   { background: hsl(var(--info-soft));   color: hsl(var(--info)); }

/* 标题右侧的补充说明（如「7 科」） */
.section-title__meta {
  font-size: .8125rem;
  font-weight: 500;
  color: hsl(var(--text-subtle));
}

.section-sub {
  font-size: .8125rem;
  color: hsl(var(--text-muted));
  line-height: 1.6;
}

/* 只管颜色的次级文字工具类。
   和 .section-sub 分开是有原因的：.section-sub 同时定死了字号和行高，
   套在本来就有 text-base / text-[10px] 的元素上会和 Tailwind 的字号类打架
   （两者特异性都是 0,1,0，谁赢取决于样式表注入顺序 —— 那是不该依赖的）。
   凡是只想换个颜色的地方用这个。 */
.text-muted  { color: hsl(var(--text-muted)); }
.text-subtle { color: hsl(var(--text-subtle)); }

/* 语义前景色工具类。
   存在的理由是替掉散落各处的 Tailwind 语义色：实测 text-emerald-600
   压在浅绿/浅玫瑰 tint 底上只有 3.3:1，text-amber-600 只有 3.13:1 ——
   Tailwind 的 500/600 档是按白底调的，放到 tint 底上就不够了。
   这几个令牌的明度是按「自身浅底 + 最深卡片底」两个最严场景反推的。 */
.text-ok     { color: hsl(var(--ok)); }
.text-warn   { color: hsl(var(--warn)); }
.text-danger { color: hsl(var(--danger)); }
.text-info   { color: hsl(var(--info)); }
.text-brand  { color: hsl(var(--brand)); }

/* ----------------------------------------------------------------------------
 * 存量替换用的「只管颜色」工具类
 *
 * 2026-08-29 管理端接令牌层时加的。admin.html 里有 47 处 bg-white、
 * 53 处 border-slate-200、52 处 text-slate-400 —— 这些值在暗色下**不会变**，
 * 正是用户端「夜间 7 张白卡片」那个 bug 的同一个成因。
 *
 * 为什么不直接换成 .card / .card--inset：那两个类还带内距、圆角和阴影，
 * 换上去会**改布局**。而这次要做的只是把颜色换成跟着主题走的令牌，
 * 布局一个像素都不该动 —— 所以专门给一组只设一个属性的类。
 * ------------------------------------------------------------------------- */
.bg-surface    { background-color: hsl(var(--surface)); }
.bg-surface-2  { background-color: hsl(var(--surface-2)); }
.bg-surface-3  { background-color: hsl(var(--surface-3)); }
.bg-inset      { background-color: hsl(var(--surface-inset)); }
.border-token  { border-color: hsl(var(--border)); }
.border-token-strong { border-color: hsl(var(--border-strong)); }
.divide-token > * + * { border-color: hsl(var(--border)); }
.text-strong   { color: hsl(var(--text)); }

.hover-surface:hover      { background-color: hsl(var(--surface-3)); }
.hover-text-strong:hover  { color: hsl(var(--text)); }


/* 竖分隔线 */
.divider-v { background: hsl(var(--border)); }


/* ============================================================================
 * 7. 表单
 * ========================================================================= */

.field { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }

.field__label {
  font-size: .75rem;
  font-weight: 600;
  color: hsl(var(--text-muted));
  letter-spacing: .02em;
  display: flex;
  align-items: center;
  gap: var(--space-1);
}
.field__label .req { color: hsl(var(--danger)); font-weight: 700; }
.field:focus-within .field__label { color: hsl(var(--brand)); }

.field__control {
  width: 100%;
  padding: 10px var(--space-4);
  font-size: .9375rem;
  font-weight: 500;
  font-family: inherit;
  line-height: 1.5;
  color: hsl(var(--text));
  background: hsl(var(--surface));
  border: 1px solid hsl(var(--border-strong));
  border-radius: var(--radius);
  outline: none;
  transition: border-color var(--dur-fast) var(--ease),
              box-shadow var(--dur-fast) var(--ease),
              background-color var(--dur-fast) var(--ease);
}
.field__control::placeholder { color: hsl(var(--text-subtle)); font-weight: 400; }

.field__control:hover:not(:disabled) { border-color: hsl(var(--text-subtle)); }

.field__control:focus {
  border-color: hsl(var(--brand));
  box-shadow: 0 0 0 var(--ring-width) hsl(var(--ring) / var(--ring-alpha));
}

/* 禁用态。改版前只有 opacity-.5，不说明为什么不能点 ——
   用户面对灰掉的「所在城市」不知道是坏了还是有前置条件。
   现在配合 .field__hint 一起用，把原因写出来。 */
.field__control:disabled {
  background: hsl(var(--surface-3));
  border-color: hsl(var(--border));
  color: hsl(var(--text-subtle));
  cursor: not-allowed;
}

.field__control--invalid {
  border-color: hsl(var(--danger));
}
.field__control--invalid:focus {
  box-shadow: 0 0 0 var(--ring-width) hsl(var(--danger) / var(--ring-alpha));
}

/* select 的箭头容器 */
.select-wrap { position: relative; display: block; }
.select-wrap > select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: var(--space-10);
  cursor: pointer;
}
.select-wrap > select:disabled { cursor: not-allowed; }
.select-wrap::after {
  content: "";
  position: absolute;
  right: var(--space-4);
  top: 50%;
  width: 8px;
  height: 8px;
  margin-top: -6px;
  border-right: 1.5px solid hsl(var(--text-subtle));
  border-bottom: 1.5px solid hsl(var(--text-subtle));
  transform: rotate(45deg);
  pointer-events: none;
}
/* 原生下拉在暗色下会用系统亮色底，必须显式指定 */
.select-wrap > select option,
.select-wrap > select optgroup {
  background: hsl(var(--surface));
  color: hsl(var(--text));
}
.select-wrap > select optgroup { font-weight: 700; color: hsl(var(--text-muted)); }

/* 输入框右侧的单位后缀（「分」「天」） */
.field__suffix {
  position: absolute;
  right: var(--space-4);
  top: 50%;
  transform: translateY(-50%);
  font-size: .8125rem;
  font-weight: 500;
  color: hsl(var(--text-subtle));
  pointer-events: none;
}
.field__wrap { position: relative; }
.field__wrap > .field__control { padding-right: var(--space-10); }

.field__hint {
  font-size: .75rem;
  color: hsl(var(--text-subtle));
  line-height: 1.5;
}
.field__hint--warn   { color: hsl(var(--warn)); }
.field__hint--danger { color: hsl(var(--danger)); }

/* 去掉 number 输入的原生上下箭头：分数输入用键盘打字更快，
   箭头只会误触，还占掉右侧「分」字的位置。 */
.field__control[type="number"] { -moz-appearance: textfield; }
.field__control[type="number"]::-webkit-outer-spin-button,
.field__control[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}


/* ============================================================================
 * 8. 按钮
 * ========================================================================= */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: 10px var(--space-5);
  font-family: inherit;
  font-size: .875rem;
  font-weight: 600;
  line-height: 1.25;
  white-space: nowrap;
  border: 1px solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
  user-select: none;
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease),
              box-shadow var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
  /* 默认＝次要样式 */
  background: hsl(var(--surface));
  border-color: hsl(var(--border-strong));
  color: hsl(var(--text));
}
.btn:hover:not(:disabled) { background: hsl(var(--surface-2)); border-color: hsl(var(--text-subtle)); }
.btn:active:not(:disabled) { transform: scale(.98); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn svg { width: 16px; height: 16px; flex: none; }

.btn--primary {
  background: hsl(var(--brand));
  border-color: hsl(var(--brand));
  color: hsl(var(--text-on-accent));
  box-shadow: var(--shadow-1);
}
.btn--primary:hover:not(:disabled) {
  background: hsl(var(--brand-hover));
  border-color: hsl(var(--brand-hover));
  box-shadow: var(--shadow-2);
}

.btn--ghost { background: transparent; border-color: transparent; color: hsl(var(--text-muted)); }
.btn--ghost:hover:not(:disabled) { background: hsl(var(--surface-3)); border-color: transparent; color: hsl(var(--text)); }

.btn--soft { background: hsl(var(--brand-soft)); border-color: hsl(var(--brand-border)); color: hsl(var(--brand)); }
.btn--soft:hover:not(:disabled) { background: hsl(var(--brand-soft)); border-color: hsl(var(--brand)); }

.btn--danger { background: hsl(var(--danger)); border-color: hsl(var(--danger)); color: hsl(var(--text-on-accent)); }
.btn--danger:hover:not(:disabled) { background: hsl(var(--danger)); box-shadow: var(--shadow-2); }

.btn--warn { background: hsl(var(--warn)); border-color: hsl(var(--warn)); color: hsl(var(--text-on-accent)); }
.btn--warn:hover:not(:disabled) { box-shadow: var(--shadow-2); }

.btn--sm { padding: 6px var(--space-3); font-size: .8125rem; border-radius: var(--radius-sm); }
.btn--sm svg { width: 14px; height: 14px; }
.btn--lg { padding: 13px var(--space-8); font-size: .9375rem; }
.btn--block { display: flex; width: 100%; }

/* 图标按钮 */
.btn--icon { padding: var(--space-2); border-radius: var(--radius-sm); }
.btn--icon svg { width: 16px; height: 16px; }


/* ============================================================================
 * 9. 标签 / 徽章
 * ========================================================================= */

.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px var(--space-2);
  font-size: .75rem;
  font-weight: 600;
  line-height: 1.4;
  border-radius: var(--radius-sm);
  background: hsl(var(--surface-3));
  color: hsl(var(--text-muted));
  border: 1px solid transparent;
  white-space: nowrap;
}
.chip--ok     { background: hsl(var(--ok-soft));     color: hsl(var(--ok));     border-color: hsl(var(--ok-border)); }
.chip--warn   { background: hsl(var(--warn-soft));   color: hsl(var(--warn));   border-color: hsl(var(--warn-border)); }
.chip--danger { background: hsl(var(--danger-soft)); color: hsl(var(--danger)); border-color: hsl(var(--danger-border)); }
.chip--info   { background: hsl(var(--info-soft));   color: hsl(var(--info));   border-color: hsl(var(--info-border)); }
.chip--brand  { background: hsl(var(--brand-soft));  color: hsl(var(--brand));  border-color: hsl(var(--brand-border)); }
.chip--lg { padding: 5px var(--space-3); font-size: .8125rem; }

/* 状态点 */
.dot { width: 6px; height: 6px; border-radius: var(--radius-full); background: currentColor; flex: none; }
.dot--pulse { animation: dot-pulse 1.6s var(--ease) infinite; }
@keyframes dot-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: .3; }
}


/* ============================================================================
 * 10. 指标块
 * ========================================================================= */

.stat { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.stat__label {
  font-size: .75rem;
  font-weight: 600;
  color: hsl(var(--text-muted));
}
.stat__value {
  font-size: 1.625rem;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -.02em;
  color: hsl(var(--text));
}
.stat__value--sm { font-size: 1.25rem; }
.stat__unit { font-size: .8125rem; font-weight: 600; color: hsl(var(--text-subtle)); margin-left: 2px; }
.stat__sub  { font-size: .75rem; color: hsl(var(--text-subtle)); line-height: 1.5; }

.stat__value--brand  { color: hsl(var(--brand)); }
.stat__value--ok     { color: hsl(var(--ok)); }
.stat__value--warn   { color: hsl(var(--warn)); }
.stat__value--danger { color: hsl(var(--danger)); }


/* ============================================================================
 * 11. 空态
 *
 * 改版前「学情数据录入」的空态是个 250px 高的空框，中间一个 emoji 和一行字；
 * 「距离中考」的空态是一整块高饱和实心紫写着「仅剩时间 — 天」，
 * 是全页唯一的实心色块，非常突兀。
 * 空态的作用是**指路**，不是占地方。
 * ========================================================================= */

.empty {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border: 1px dashed hsl(var(--border-strong));
  border-radius: var(--radius);
  background: hsl(var(--surface-2));
  color: hsl(var(--text-muted));
  font-size: .875rem;
}
.empty__icon { flex: none; font-size: 1.125rem; line-height: 1; opacity: .8; }
.empty__text { flex: 1; min-width: 0; line-height: 1.5; }
.empty__text strong { color: hsl(var(--text)); font-weight: 600; }
.empty--center { flex-direction: column; text-align: center; padding: var(--space-8) var(--space-5); }


/* ============================================================================
 * 12. 骨架屏
 * ========================================================================= */

.skeleton {
  background: linear-gradient(
    90deg,
    hsl(var(--skeleton)) 0%,
    hsl(var(--skeleton-shine)) 50%,
    hsl(var(--skeleton)) 100%
  );
  background-size: 200% 100%;
  animation: skeleton-slide 1.4s ease-in-out infinite;
  border-radius: var(--radius-sm);
  color: transparent;
  user-select: none;
}
@keyframes skeleton-slide {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}
.skeleton--line { height: 12px; }
.skeleton--text { height: 1em; }


/* ============================================================================
 * 13. 报告分段导航
 *
 * 报告页实测总高 21,969px，其中 AI 面板 14,459px、学科诊断一块就 8,551px。
 * 想从「可用学习时间」看到「提升策略」要滚过 8 千像素。
 *
 * 一份 DOM，两种形态，全靠这一节的媒体查询切换：
 *   · ≥1280px：报告区变两栏，导航是左侧 216px 的粘性竖栏，**不遮挡**内容，
 *              所以 --nav-h 归零（板块的 scroll-margin-top 不必为它让位）
 *   · <1280px：竖栏放不下，退化成横在报告顶上的粘性条，此时它会遮挡内容，
 *              --nav-h 取 JS 实测的高度（横条在窄屏会换行变高，不能写死）
 *
 * 之所以不做成两个容器各渲染一份：那就又多一处「改一边漏一边」的地方。
 * ========================================================================= */

.report-shell {
  max-width: 1360px;
  margin-left: auto;
  margin-right: auto;
  /* JS 只负责量高度并写 --nav-measured-h（内联样式），
     真正决定用不用这个值的是下面的媒体查询 —— 内联样式会赢过任何选择器，
     所以绝不能让 JS 直接写 --nav-h，否则宽屏下会拿竖栏的几百像素去当偏移量。 */
  --nav-h: var(--nav-measured-h, 44px);
}

.report-nav {
  position: sticky;
  top: calc(var(--header-h) + var(--space-2));
  z-index: 20;
  display: flex;
  gap: var(--space-1);
  padding: var(--space-2);
  margin-bottom: var(--space-5);
  background: hsl(var(--surface) / .92);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 1px solid hsl(var(--border));
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
  overflow-x: auto;
  scrollbar-width: none;
}
.report-nav::-webkit-scrollbar { display: none; }

/* 分组标题（基础报告 / AI 深度分析）。横条形态下纯属噪音，只在竖栏里显示。 */
.report-nav__group { display: none; }

.report-nav__row {
  flex: none;
  display: flex;
  align-items: center;
  gap: 2px;
  border-radius: var(--radius-sm);
  /* 横条形态下给每项一个浅底：相机按钮夹在两个标签之间，
     没有这层底色看不出它属于左边还是右边那一项（实测 390px 下就是这样）。
     竖栏形态在下面的媒体查询里去掉，那里上下排列，归属本来就清楚。 */
  background: hsl(var(--surface-2));
  padding-right: 2px;
}
.report-nav__row:hover { background: hsl(var(--surface-3)); }

.report-nav__item {
  flex: 1;
  min-width: 0;
  padding: 6px var(--space-3);
  font-size: .8125rem;
  font-weight: 600;
  text-align: left;
  color: hsl(var(--text-muted));
  background: transparent;
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: color var(--dur-fast) var(--ease);
}
.report-nav__item:hover { color: hsl(var(--text)); }

.report-nav__row[aria-current="true"] {
  background: hsl(var(--brand-soft));
}
.report-nav__row[aria-current="true"] .report-nav__item { color: hsl(var(--brand)); }

/* 单段截图按钮。整份报告导出常常有大半是用不上的，
   而顾问要发给家长的往往就是其中一两段。 */
/* 分段导航里那个截图按钮的样式在下面的 .shot-btn ——
   .report-nav__shot 只作为 JS 定位用的名字，不再单独写一份观感。 */


/* 截图按钮。分段导航里每一项、学科诊断里每一科各一个，
   两处观感必须一致 —— 所以样式只有这一份，两边都挂 .shot-btn。
   .no-export：截图时会被 captureElementToCanvas 从克隆 DOM 里摘掉，
   免得发给家长的报告图里带着一排 UI 按钮。 */
.shot-btn {
  flex: none;
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: hsl(var(--text-subtle));
  background: transparent;
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  opacity: .6;
  transition: opacity var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
.shot-btn svg { width: 15px; height: 15px; }
.shot-btn:hover {
  opacity: 1;
  color: hsl(var(--brand));
  background: hsl(var(--surface));
}
.shot-btn:disabled { cursor: progress; opacity: .3; }

@media (min-width: 1280px) {
  .report-shell--with-nav {
    display: grid;
    grid-template-columns: 216px minmax(0, 1fr);
    gap: var(--space-6);
    align-items: start;
    /* 竖栏在内容左边，不压住任何东西 */
    --nav-h: 0px;
  }

  .report-shell--with-nav .report-nav {
    flex-direction: column;
    align-items: stretch;
    gap: 1px;
    top: calc(var(--header-h) + var(--space-4));
    margin-bottom: 0;
    max-height: calc(100vh - var(--header-h) - var(--space-8));
    overflow-x: hidden;
    overflow-y: auto;
  }

  .report-shell--with-nav .report-nav__group {
    display: block;
    padding: var(--space-2) var(--space-3) 4px;
    font-size: .6875rem;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: hsl(var(--text-subtle));
  }
  .report-shell--with-nav .report-nav__group:not(:first-child) {
    margin-top: var(--space-2);
    border-top: 1px solid hsl(var(--border));
  }

  /* 竖栏里上下排列，归属本来就清楚，不需要那层浅底。
     :not([aria-current]) 是必需的：不加的话这条与上面的高亮规则特异度相同、
     位置又更靠后，会把「当前段」的高亮底色一起冲掉。 */
  .report-shell--with-nav .report-nav__row:not([aria-current="true"]) {
    background: transparent;
  }
  .report-shell--with-nav .report-nav__row:not([aria-current="true"]):hover {
    background: hsl(var(--surface-3));
  }
}


/* ============================================================================
 * 14. 折叠面板（学科诊断手风琴）
 * ========================================================================= */

.acc { border: 1px solid hsl(var(--border)); border-radius: var(--radius); overflow: hidden; background: hsl(var(--surface)); }
.acc + .acc { margin-top: var(--space-3); }

/* 头部一行 = 可点的标题按钮 + 单科截图按钮。
   截图按钮不能放进 .acc__head 里面：那是个 <button>，按钮不能嵌套。
   悬停底色因此从 .acc__head 上移到这一层，否则鼠标移到相机上时整行会掉色。 */
.acc__bar {
  display: flex;
  align-items: center;
  padding-right: var(--space-2);
  transition: background-color var(--dur-fast) var(--ease);
}
.acc__bar:hover { background: hsl(var(--surface-2)); }

.acc__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 1;
  min-width: 0;
  padding: var(--space-3) var(--space-4);
  background: transparent;
  border: none;
  cursor: pointer;
  text-align: left;
  font-family: inherit;
  color: hsl(var(--text));
}

.acc__title { font-size: .9375rem; font-weight: 700; flex: none; }
.acc__summary {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  font-size: .8125rem;
  color: hsl(var(--text-muted));
}
.acc__caret {
  flex: none;
  width: 8px; height: 8px;
  margin-right: 4px;
  border-right: 1.5px solid hsl(var(--text-subtle));
  border-bottom: 1.5px solid hsl(var(--text-subtle));
  transform: rotate(-45deg);
  transition: transform var(--dur) var(--ease);
}
.acc[data-open="true"] .acc__caret { transform: rotate(45deg); }

.acc__body { padding: 0 var(--space-4) var(--space-4); border-top: 1px solid hsl(var(--border)); padding-top: var(--space-4); }
.acc[data-open="false"] .acc__body { display: none; }


/* ----------------------------------------------------------------------------
 * 整块折叠的面板（<details class="fold">）
 *
 * 与上面的手风琴分开是因为两者的**导出语义相反**：
 *   · .acc（学科诊断）折叠只是为了少滚屏，内容是 AI 针对这个学生生成的，
 *     导出时必须全展开，否则家长收到的报告里 6 科只剩标题行；
 *   · .fold[data-fold-keep]（综合提升指导）是 getAnalysisKnowledge() 的
 *     字典查表结果 —— 按「科目 + 得分率区间」返回固定话术，全国学生看同一段。
 *     用户把它收起来就是不想要它，导出时应当照办。
 * 判据写在 planExportFolds()（analysis-excel-style.js）里。
 * ------------------------------------------------------------------------- */

details.fold > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  cursor: pointer;
  list-style: none;
  user-select: none;
}
details.fold > summary::-webkit-details-marker { display: none; }
details.fold > summary::marker { content: ''; }

.fold__caret {
  flex: none;
  width: 16px; height: 16px;
  transition: transform var(--dur) var(--ease);
  transform: rotate(-90deg);
}
details.fold[open] > summary .fold__caret { transform: none; }

.fold__body { margin-top: var(--space-5); padding-top: var(--space-5); border-top: 1px solid hsl(var(--border)); }

/* 导出期间临时摘掉的板块。用 class 而不是直接改 style.display，
   是为了让还原逻辑不必记住元素原来的 display 值。 */
.export-omit { display: none !important; }


/* ============================================================================
 * 15. 表格
 * ========================================================================= */

.tbl { width: 100%; border-collapse: separate; border-spacing: 0; font-size: .875rem; }
.tbl th {
  text-align: left;
  font-size: .75rem;
  font-weight: 700;
  color: hsl(var(--text-muted));
  padding: var(--space-2) var(--space-3);
  background: hsl(var(--surface-2));
  border-bottom: 1px solid hsl(var(--border));
  white-space: nowrap;
}
.tbl td {
  padding: var(--space-3);
  border-bottom: 1px solid hsl(var(--border));
  color: hsl(var(--text));
}
.tbl tbody tr:last-child td { border-bottom: none; }
.tbl tbody tr:hover td { background: hsl(var(--surface-2)); }
.tbl__num { text-align: right; }


/* ============================================================================
 * 16. 头部 / 页脚 / 遮罩
 * ========================================================================= */

.app-header {
  position: sticky;
  top: 0;
  z-index: 40;
  min-height: var(--header-h-base);
  background: hsl(var(--surface) / .88);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid hsl(var(--border));
}

.app-footer {
  border-top: 1px solid hsl(var(--border));
  background: hsl(var(--surface) / .6);
  color: hsl(var(--text-subtle));
  font-size: .8125rem;
}

.overlay {
  background: hsl(var(--overlay) / var(--overlay-alpha));
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

.panel {
  background: hsl(var(--surface));
  border: 1px solid hsl(var(--border));
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-3);
}

/* 进度条 */
.progress {
  height: 8px;
  border-radius: var(--radius-full);
  background: hsl(var(--surface-3));
  overflow: hidden;
}
.progress__fill {
  height: 100%;
  border-radius: var(--radius-full);
  background: hsl(var(--brand));
  transition: width 400ms var(--ease);
}


/* ============================================================================
 * 17. 主题切换器
 * ========================================================================= */

.theme-switch {
  display: inline-flex;
  padding: 2px;
  gap: 2px;
  background: hsl(var(--surface-3));
  border: 1px solid hsl(var(--border));
  border-radius: var(--radius-full);
}
.theme-switch__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: none;
  border-radius: var(--radius-full);
  background: transparent;
  color: hsl(var(--text-subtle));
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.theme-switch__btn:hover { color: hsl(var(--text)); }
/* 高亮由 <html data-theme-pref> 直接驱动，而不是让 JS 去逐个同步 aria-pressed。
   理由：主题可以从多条路径被改（按钮点击、系统偏好变化、其他脚本调
   __theme.set），任何一条忘了调同步函数，高亮就会和实际主题对不上。
   交给 CSS 属性选择器就不存在"忘了同步"这回事。
   aria-pressed 仍由 JS 维护，那是给读屏软件的，不参与视觉。 */
:root[data-theme-pref="light"]  .theme-switch__btn[data-theme-set="light"],
:root[data-theme-pref="system"] .theme-switch__btn[data-theme-set="system"],
:root[data-theme-pref="dark"]   .theme-switch__btn[data-theme-set="dark"] {
  background: hsl(var(--surface));
  color: hsl(var(--brand));
  box-shadow: var(--shadow-1);
}
.theme-switch__btn svg { width: 15px; height: 15px; }


/* ============================================================================
 * 18. 装饰背景
 *
 * 改版前是三个 500~600px、blur(90px) 的动画光球，z-index:-1，
 * 其中一个用 right:-200px 撑出了 200px 的横向滚动条（见 §4 html 的注释）。
 * 现在收进一个 fixed + overflow:hidden 的裁切容器，不参与文档流、
 * 不可能再撑出滚动条；同时把动画去掉 —— 持续的大面积 blur 合成在
 * 移动端是白付的功耗。
 * ========================================================================= */

.bg-decor {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}
.bg-decor__orb {
  position: absolute;
  border-radius: 50%;
  filter: blur(100px);
  opacity: .35;
}
:root[data-theme="dark"] .bg-decor__orb { opacity: .22; }


/* ============================================================================
 * 19. 打印 / 导出
 *
 * 报告要导出 PDF、截图发家长。打印时强制亮色、去掉阴影和装饰。
 * ========================================================================= */

/* ============================================================================
 * 18. 管理端专有
 *
 * 放在这里而不是 admin.html 的内联 <style> 里，是为了让
 * test/uiInvariants.test.js「语义组件类里不许写裸色值」那条守卫也能盖到它 ——
 * 内联样式表在守卫的视野之外，写死一个 #cbd5e1 没人会发现。
 * ========================================================================= */

/* Tailwind 自带的 .hidden 特异性不足以压过管理端那些带 flex/grid 的类，
   加 !important 是刻意的：这个类在管理端被用来控制十几个面板的显隐。 */
.hidden { display: none !important; }

/* 登录遮罩。整页盖住，所以不能只靠 .overlay 的背景色 */
.auth-overlay {
  position: fixed;
  inset: 0;
  background: hsl(var(--overlay) / .82);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9999;
}

/* 新增日志行的入场动画。管理端的日志面板是「一行行冒出来」的，
   没有动画会让人分不清哪一条是新的 */
.log-entry { animation: log-slide-in .3s ease-out forwards; }
@keyframes log-slide-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* 进度条宽度变化的过渡。管理端有自己的进度条 markup（不是 .progress） */
.progress-bar { transition: width .5s cubic-bezier(.4, 0, .2, 1); }

/* 卡片悬浮微抬。等价于 .card--interactive 的 hover，
   但管理端的卡片没有用 .card，所以单独留一个 */
.card-hover { transition: all .3s cubic-bezier(.4, 0, .2, 1); }
.card-hover:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-3);
}

/* ----------------------------------------------------------------------------
 * 暗色下的彩色 tint 映射
 *
 * 管理端每个面板用一个不同色系做视觉区分（emerald=批量查询、violet=数据复核、
 * amber=卡密、red=错误、indigo=品牌…），一共 15 个色系、385 处彩色类。
 * 把它们全压成 5 个语义令牌能让主题完全自适应，但会**抹掉面板的颜色区分** ——
 * 那是这个界面现有的信息结构，不该在一次「接令牌层」的改动里顺手拆掉。
 *
 * 所以只解决真正坏掉的那一层：`bg-*-50` / `bg-*-100` 是**浅色 tint**，
 * 暗色下它们不变，而文字跟着令牌变浅 → 浅字压浅底，实测低到 1.86:1。
 * 这里按色系把它们映射到对应的深色 tint，色相保留、明度跟着主题走。
 *
 * 只覆盖 -50 / -100 两档（实测只有这两档被用作面板底色），
 * 以及压在上面会变成深字的 text-*-700/800。
 * ------------------------------------------------------------------------- */
/* ⚠️ 下面这个 `:root[data-theme="dark"]` 是**第二个**暗色块，里面的令牌是
   **刻意只在暗色下存在**的：它们的用途是「把不随主题变的 Tailwind 彩色类掰过来」，
   亮色下那些类本来就是对的，不需要对应令牌。
   所以它不参与 uiInvariants 里「亮暗令牌名集合必须一致」那条断言
   （那条比对的是最上面那两个主色块）。 */
:root[data-theme="dark"] {
  /* 深色 tint：色相取自各自色系，饱和度压到 30% 左右、明度 14% 左右。
     和 --ok-soft / --warn-soft 那几个令牌同一套口径。 */
  --tint-emerald: 162 30% 14%;
  --tint-green:   142 28% 14%;
  --tint-teal:    174 30% 13%;
  --tint-amber:    36 32% 14%;
  --tint-yellow:   48 34% 13%;
  --tint-orange:   28 34% 14%;
  --tint-red:     352 30% 15%;
  --tint-rose:    346 30% 15%;
  --tint-pink:    330 28% 15%;
  --tint-indigo:  243 34% 17%;
  --tint-violet:  262 32% 17%;
  --tint-purple:  272 30% 17%;
  --tint-blue:    214 34% 16%;
  --tint-sky:     200 34% 15%;
  --tint-cyan:    188 32% 14%;

  /* 深 tint 上的前景色。明度按「压在自己那档深 tint 上要有 4.5:1」反推 */
  --fg-emerald: 162 60% 66%;
  --fg-green:   142 55% 66%;
  --fg-teal:    174 55% 64%;
  --fg-amber:    38 80% 66%;
  --fg-orange:   28 85% 66%;
  --fg-red:     352 80% 72%;
  --fg-rose:    346 80% 72%;
  --fg-pink:    330 72% 72%;
  --fg-indigo:  238 88% 76%;
  --fg-violet:  258 85% 76%;
  --fg-purple:  270 80% 76%;
  --fg-blue:    214 88% 72%;
  --fg-sky:     200 88% 70%;
}

/* 覆盖 `bg-emerald-50` 也覆盖 `bg-emerald-50/50` 这类带透明度修饰的变体。
   实测踩到：只写 .bg-violet-50 的话，`bg-violet-50/50` 完全不受影响 ——
   那处的标签在暗色下只有 1.86:1。用 [class*=] 一次性把两种写法都收进来。 */
:root[data-theme="dark"] [class*="bg-emerald-50"], :root[data-theme="dark"] [class*="bg-emerald-100"] { background-color: hsl(var(--tint-emerald)); }
:root[data-theme="dark"] [class*="bg-green-50"],   :root[data-theme="dark"] [class*="bg-green-100"]   { background-color: hsl(var(--tint-green)); }
:root[data-theme="dark"] [class*="bg-teal-50"],    :root[data-theme="dark"] [class*="bg-teal-100"]    { background-color: hsl(var(--tint-teal)); }
:root[data-theme="dark"] [class*="bg-amber-50"],   :root[data-theme="dark"] [class*="bg-amber-100"]   { background-color: hsl(var(--tint-amber)); }
:root[data-theme="dark"] [class*="bg-yellow-50"],  :root[data-theme="dark"] [class*="bg-yellow-100"]  { background-color: hsl(var(--tint-yellow)); }
:root[data-theme="dark"] [class*="bg-orange-50"],  :root[data-theme="dark"] [class*="bg-orange-100"]  { background-color: hsl(var(--tint-orange)); }
:root[data-theme="dark"] [class*="bg-red-50"],     :root[data-theme="dark"] [class*="bg-red-100"]     { background-color: hsl(var(--tint-red)); }
:root[data-theme="dark"] [class*="bg-rose-50"],    :root[data-theme="dark"] [class*="bg-rose-100"]    { background-color: hsl(var(--tint-rose)); }
:root[data-theme="dark"] [class*="bg-pink-50"],    :root[data-theme="dark"] [class*="bg-pink-100"]    { background-color: hsl(var(--tint-pink)); }
:root[data-theme="dark"] [class*="bg-indigo-50"],  :root[data-theme="dark"] [class*="bg-indigo-100"]  { background-color: hsl(var(--tint-indigo)); }
:root[data-theme="dark"] [class*="bg-violet-50"],  :root[data-theme="dark"] [class*="bg-violet-100"]  { background-color: hsl(var(--tint-violet)); }
:root[data-theme="dark"] [class*="bg-purple-50"],  :root[data-theme="dark"] [class*="bg-purple-100"]  { background-color: hsl(var(--tint-purple)); }
:root[data-theme="dark"] [class*="bg-blue-50"],    :root[data-theme="dark"] [class*="bg-blue-100"]    { background-color: hsl(var(--tint-blue)); }
:root[data-theme="dark"] [class*="bg-sky-50"],     :root[data-theme="dark"] [class*="bg-sky-100"]     { background-color: hsl(var(--tint-sky)); }
:root[data-theme="dark"] [class*="bg-cyan-50"],    :root[data-theme="dark"] [class*="bg-cyan-100"]    { background-color: hsl(var(--tint-cyan)); }

/* tint 底上的边框：浅色边框在深 tint 上是一道亮边，压下去 */
:root[data-theme="dark"] [class*="border-emerald-1"], :root[data-theme="dark"] [class*="border-emerald-2"],
:root[data-theme="dark"] [class*="border-green-1"],   :root[data-theme="dark"] [class*="border-green-2"],
:root[data-theme="dark"] [class*="border-amber-1"],   :root[data-theme="dark"] [class*="border-amber-2"],
:root[data-theme="dark"] [class*="border-orange-1"],  :root[data-theme="dark"] [class*="border-orange-2"],
:root[data-theme="dark"] [class*="border-yellow-1"],  :root[data-theme="dark"] [class*="border-yellow-2"],
:root[data-theme="dark"] [class*="border-teal-1"],    :root[data-theme="dark"] [class*="border-teal-2"],
:root[data-theme="dark"] [class*="border-cyan-1"],    :root[data-theme="dark"] [class*="border-cyan-2"],
:root[data-theme="dark"] [class*="border-red-1"],     :root[data-theme="dark"] [class*="border-red-2"],
:root[data-theme="dark"] [class*="border-rose-1"],    :root[data-theme="dark"] [class*="border-rose-2"],
:root[data-theme="dark"] [class*="border-pink-1"],    :root[data-theme="dark"] [class*="border-pink-2"],
:root[data-theme="dark"] [class*="border-indigo-1"],  :root[data-theme="dark"] [class*="border-indigo-2"],
:root[data-theme="dark"] [class*="border-violet-1"],  :root[data-theme="dark"] [class*="border-violet-2"],
:root[data-theme="dark"] [class*="border-purple-1"],  :root[data-theme="dark"] [class*="border-purple-2"],
:root[data-theme="dark"] [class*="border-blue-1"],    :root[data-theme="dark"] [class*="border-blue-2"],
:root[data-theme="dark"] [class*="border-sky-1"],     :root[data-theme="dark"] [class*="border-sky-2"] {
  border-color: hsl(var(--border));
}

/* tint 底上的深色文字。600 档在浅 tint 上刚好，在深 tint 上就成了深压深 —— 提亮 */
:root[data-theme="dark"] .text-emerald-600, :root[data-theme="dark"] .text-emerald-700, :root[data-theme="dark"] .text-emerald-800 { color: hsl(var(--fg-emerald)); }
:root[data-theme="dark"] .text-green-600,   :root[data-theme="dark"] .text-green-700,   :root[data-theme="dark"] .text-green-800   { color: hsl(var(--fg-green)); }
:root[data-theme="dark"] .text-teal-600,    :root[data-theme="dark"] .text-teal-700     { color: hsl(var(--fg-teal)); }
:root[data-theme="dark"] .text-amber-600,   :root[data-theme="dark"] .text-amber-700,   :root[data-theme="dark"] .text-amber-800   { color: hsl(var(--fg-amber)); }
:root[data-theme="dark"] .text-orange-500,  :root[data-theme="dark"] .text-orange-600,  :root[data-theme="dark"] .text-orange-700,  :root[data-theme="dark"] .text-orange-800 { color: hsl(var(--fg-orange)); }
:root[data-theme="dark"] .text-red-500,     :root[data-theme="dark"] .text-red-600,     :root[data-theme="dark"] .text-red-700,    :root[data-theme="dark"] .text-red-800 { color: hsl(var(--fg-red)); }
:root[data-theme="dark"] .text-rose-600,    :root[data-theme="dark"] .text-rose-700     { color: hsl(var(--fg-rose)); }
:root[data-theme="dark"] .text-pink-600,    :root[data-theme="dark"] .text-pink-700     { color: hsl(var(--fg-pink)); }
:root[data-theme="dark"] .text-indigo-600,  :root[data-theme="dark"] .text-indigo-700,  :root[data-theme="dark"] .text-indigo-800 { color: hsl(var(--fg-indigo)); }
:root[data-theme="dark"] .text-violet-600,  :root[data-theme="dark"] .text-violet-700   { color: hsl(var(--fg-violet)); }
:root[data-theme="dark"] .text-purple-600,  :root[data-theme="dark"] .text-purple-700   { color: hsl(var(--fg-purple)); }
:root[data-theme="dark"] .text-blue-600,    :root[data-theme="dark"] .text-blue-700,    :root[data-theme="dark"] .text-blue-800   { color: hsl(var(--fg-blue)); }
:root[data-theme="dark"] .text-sky-600,     :root[data-theme="dark"] .text-sky-700      { color: hsl(var(--fg-sky)); }


/* ----------------------------------------------------------------------------
 * 自定义对话框（替掉原生 confirm / alert / prompt）
 *
 * 换掉原生弹窗的理由见 public/admin/dialog.js 的头注释。这里只解决一件事：
 * 让那些**本来就写得很认真**的高危操作文案有地方好好排版 ——
 * 原生弹窗里 `\n\n- 所有高中数据\n- 所有科目配置` 会被压成一坨。
 * ------------------------------------------------------------------------- */
.dialog-overlay {
  position: fixed;
  inset: 0;
  z-index: 10000;
  display: flex;
  align-items: flex-start;      /* 内容比屏幕高时从顶部开始，而不是上下都被切掉 */
  justify-content: center;
  padding: var(--space-4);
  overflow-y: auto;
}

.dialog {
  margin: auto;                 /* 配合 align-items:flex-start，内容不高时仍然居中 */
  width: 100%;
  max-width: 30rem;
  padding: var(--space-6);
}

.dialog__title {
  font-size: 1.0625rem;
  font-weight: 700;
  line-height: 1.4;
  margin-bottom: var(--space-3);
}
.dialog__title--danger { color: hsl(var(--danger)); }
.dialog__title--warn   { color: hsl(var(--warn)); }
.dialog__title--ok     { color: hsl(var(--ok)); }

.dialog__body { display: flex; flex-direction: column; gap: var(--space-3); }

.dialog__message {
  font-size: .875rem;
  line-height: 1.7;
  color: hsl(var(--text-muted));
  white-space: pre-line;        /* 存量文案里还有 \n，让它按原意换行 */
}

.dialog__list {
  margin: 0;
  padding-left: 1.1em;
  list-style: disc;
  font-size: .8125rem;
  line-height: 1.8;
  color: hsl(var(--text-muted));
}

.dialog__note {
  font-size: .75rem;
  line-height: 1.6;
  color: hsl(var(--text-subtle));
}

.dialog__hint:empty { display: none; }

.dialog__actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  margin-top: var(--space-5);
}
@media (max-width: 420px) {
  /* 窄屏上两个按钮并排会各自只剩一半宽，点起来容易误触 */
  .dialog__actions { flex-direction: column-reverse; }
  .dialog__actions .btn { width: 100%; }
}

/* 管理端 header 上的主题切换器。
   那个 header 在亮暗两个主题下都是深色（自身自洽：深底 + 浅字），
   所以切换器要跟旁边那个「用户端」按钮同一套观感，
   而不是套一个浅色药丸贴在深底上。
   颜色走 --tint-on-dark / --text-on-dark 两个令牌而不是就地写 hsl()，
   是为了让「组件区不许出现裸色值」那条守卫也能盖到管理端。 */
.theme-switch--on-dark {
  background: hsl(var(--tint-on-dark) / .06);
  border-color: hsl(var(--tint-on-dark) / .08);
}
.theme-switch--on-dark .theme-switch__btn { color: hsl(var(--text-on-dark)); }
.theme-switch--on-dark .theme-switch__btn:hover { color: hsl(var(--tint-on-dark)); }

/* 深底面板内部的次级文字。
   2026-08-29 实测踩到：把管理端的 text-slate-500 一刀切换成 .text-muted 之后，
   落在深色面板（header、程序日志、服务器信息、统一身份额度）里的那 13 处
   在**亮色**主题下变成了深灰压深底 —— 2.39:1，比改造前更糟。
   根因是 .text-muted 的值跟着主题走，而这些面板不跟 —— 它们两个主题下都是深色。
   所以深底面板里的次级文字要用这一组，值在两个主题块里相同。 */
.text-muted-on-dark  { color: hsl(var(--text-on-dark)); }
.text-strong-on-dark { color: hsl(var(--tint-on-dark)); }

/* 深底面板内部的行底色。程序日志那块是 from-slate-950 到 slate-900 的深色渐变，
   里面每条日志需要一层极淡的叠加才能看出行的边界 ——
   原来写的是 Tailwind 的 `bg-white bg-opacity-10`。
   换令牌是因为 bg-opacity-* 只对 Tailwind 自己的 bg-* 生效，
   一旦底色换成 bg-surface 那类直接写 background-color 的类，透明度就静默失效，
   深色面板里会突然出现一排纯白的行。 */
.row-on-dark { background-color: hsl(var(--tint-on-dark) / .08); }

/* 管理端用户管理（2026-09-16 分组视图）成员行的列布局。
   写成类而不是行内 style：行内的 minmax() 会被 test/adminModuleContract 的「函数调用来源」守卫误认成函数调用。 */
.user-grid {
  display: grid;
  grid-template-columns: minmax(140px, 1.4fr) minmax(120px, 1fr) repeat(4, minmax(56px, .6fr)) minmax(72px, .8fr) minmax(70px, .7fr) auto;
  min-width: 880px;
}

/* 细滚动条。颜色跟着主题走 —— 原来写死的 #cbd5e1 在暗色下是一道亮条 */
.custom-scrollbar::-webkit-scrollbar,
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: hsl(var(--border-strong));
  border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover { background: hsl(var(--text-subtle)); }


@media print {
  .bg-decor, .app-header, .report-nav, .shot-btn, .no-print { display: none !important; }
  .card, .card--primary, .card--inset, .panel {
    box-shadow: none !important;
    border-color: hsl(var(--border)) !important;
    break-inside: avoid;
  }
  /* 手风琴折叠着打印会只剩标题行 —— 那是 AI 针对这个学生写的内容，必须展开 */
  .acc[data-open="false"] .acc__body { display: block !important; }
  /* 反过来，用户主动收起的 data-fold-keep 板块（固定话术）就别印了：
     只留一行标题在纸上既没信息也难看，与截图导出的口径保持一致。 */
  details.fold[data-fold-keep]:not([open]) { display: none !important; }
}
