/* ============================================================
   虹小轩阅卷平台 · 宿主样式（应用层唯一入口）

   本文件解决一个问题：库（vendor/hxx/hxx-ui.css）是「交互组件库」，它给了
   按钮/输入/弹窗/标签页，但**没有后台壳**（侧边栏、顶栏、表格、分页器、分页页脚）。
   36 个页面由不同的人写，这些壳类必须只有一套实现，否则风格立刻散掉。

   五条纪律（违反即缺陷，见 AGENTS.md §5.4 与 UI 规范 §0）：
   1. 色值只能写 var(--hxx-*)；本文件里出现的每一个 `#` 都只允许在注释里。
   2. 不给裸元素写尺寸规则（`input{height:40px}` 会压过库的 38px 输入框）；
      尺寸只写给 `.rv-*` / `.hxx-*` 类。
   3. 不写强制优先级标记；不写滚动条伪元素样式（超细滚动条由库接管，契约 §8）。
   4. 不写第二套深色规则：深色靠契约 §3.7 的令牌覆盖自动成立。
   5. 每个自定义类先登记到 `docs/UI 规范（应用层补充）.md` §8，再写在这里；
      注释里写「为什么需要它」，不写「它做了什么」。

   命名分工：
   · `.rv-*`      本项目后台壳类（页面作者直接引用，是对外契约）
   · `.hxx-*`     库已实现，直接复用，不重写
   · `.data-table` / `.hxx-table-wrap` / `.hxx-toolbar` / `.hxx-stat-grid` /
     `.hxx-detail-grid` / `.hxx-icon-btn`
                 契约 §10.7~§10.10 / §10.16 / §10.31 有规格、库未实现的类。
                 它们与对应的 `.rv-*` 写在**同一条规则**里，只维护一套数值，
                 免得页面作者在「契约名」和「骨架约定名」之间二选一。
   ============================================================ */

/* ========== 契约 §3 令牌：宿主扩展令牌 ==========
   契约的令牌表里没有布局尺寸（侧栏宽、顶栏高、登录卡宽），这些必须由宿主定义。
   用 `--app-` 前缀把它们和品牌令牌分开，一眼能看出「这是应用层补的，不是契约里的」。
   AGENTS.md §5.4 明确允许 var(--app-*)，前提是登记过（见 UI 规范 §8 登记块）。 */
:root {
  --app-sidebar-w: 240px;   /* 后台侧栏宽度：装得下「答题卡模板」这种 6 字菜单项 */
  --app-topbar-h: 56px;     /* 顶栏高度：与契约 §9 导航栏 56px 对齐，视觉同族 */
  --app-login-card-w: 420px;/* 登录表单最大宽度：再宽输入框会显得空，再窄 375px 屏会挤 */
}

/* ========== 契约 §8 全局基础 ==========
   库刻意不做全局重置（它声明「局部盒模型重置，不污染宿主」），所以页面底子归本文件。
   这里只放「整个文档级」的东西：盒模型、字体栈、背景、文字色、禁选。
   字体栈逐字来自契约 §7（系统栈，离线可用，不引 CDN 字体）。 */
*,
*::before,
*::after {
  box-sizing: border-box;
}

* {
  /* 契约 §8：全站默认禁止选中（品牌细节）。表格数据与弹窗正文的例外见文件末尾。 */
  user-select: none;
  -webkit-user-select: none;
}

body {
  margin: 0;
  background: var(--hxx-bg);
  color: var(--hxx-text);
  font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei",
    "Helvetica Neue", Arial, sans-serif;
  font-size: 14px;
  line-height: 1.6;
}

a {
  color: var(--hxx-primary);
  text-decoration: none;
}

a:hover {
  color: var(--hxx-primary-deep);
}

/* ============================================================
   一、后台壳（侧边栏 + 顶栏 + 内容区）
   ------------------------------------------------------------
   为什么自己画壳：契约 §9 的 `.hxx-nav` 是「客户端网站的顶部导航」，
   后台需要的是「左侧分组菜单 + 顶部面包屑」，库没有这套，契约也没有。
   ============================================================ */

/* 壳容器：只作为「侧栏 + 顶栏 + 遮罩」的挂载点存在，本身不参与布局
   （两个子元素都是 fixed）。保留一条规则是为了让人知道它被刻意留空，
   而不是漏写样式。 */
.rv-shell {
  display: contents;
}

/* 汉堡按钮：只在窄屏出现。1023px 与侧栏抽屉的断点必须一致，
   否则会出现「侧栏已收成抽屉、却没有按钮能打开」的死胡同。 */
.rv-nav-toggle {
  display: none;
}

@media (max-width: 1023px) {
  .rv-nav-toggle {
    display: inline-flex;
  }
}

/* 侧边栏：固定左侧、独立滚动。
   用 fixed 而不是 flex 布局，是为了让页面主体（.rv-main）可以毫无约束地
   横向铺开（宽表格、阅卷三栏都要完整宽度），不必再套一层 flex 容器。 */
.rv-sidebar {
  position: fixed;
  inset: 0 auto 0 0;
  z-index: 10001; /* 与契约 §9 导航同层：高于内容、低于弹窗 10002 */
  display: flex;
  flex-direction: column;
  gap: var(--hxx-gap-md);
  width: var(--app-sidebar-w);
  padding: var(--hxx-gap-md) 12px;
  overflow-y: auto;
  background: var(--hxx-surface);
  border-right: 1px solid var(--hxx-border);
}

/* 品牌区：登录页与侧边栏共用同一套标记，避免两处各写一遍 logo 样式。 */
.rv-brand {
  display: flex;
  align-items: center;
  gap: var(--hxx-gap-sm);
  padding: 4px 6px;
  color: var(--hxx-text);
}

.rv-brand:hover {
  color: var(--hxx-text);
}

/* 品牌标记：主色圆角方块 + 居中的「虹」。
   字色为什么不写 #fff：契约没有 "on-primary" 令牌，而 `--hxx-tip-fg` 是契约里
   语义唯一的「深色底上的前景色」（浅色 #ffffff / 深色 #f2f4f8），
   在两种主题的主色底上都可读，正是这里要的东西。 */
.rv-brand-logo {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 30px;
  height: 30px;
  border-radius: var(--hxx-radius-sm);
  background: var(--hxx-primary);
  color: var(--hxx-tip-fg);
  font-size: 16px;
  font-weight: 800;
  line-height: 1;
}

.rv-brand-text {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.2px;
  white-space: nowrap;
}

/* 菜单分组：标题弱化、条目成组，让 20+ 个菜单项能扫读。 */
.rv-sidebar-group + .rv-sidebar-group {
  margin-top: var(--hxx-gap-md);
}

.rv-sidebar-group-title {
  margin: 0 0 6px;
  padding: 0 10px;
  font-size: 12px;
  font-weight: 600;
  color: var(--hxx-text-3);
}

/* 菜单项：契约 §9 的导航链接是横向胶囊；纵向侧栏里胶囊会串成一条糖葫芦，
   所以圆角取 radius-sm（与标签页、信息行同族），其余（14px/500、hover 底、
   active 主色底 + 字重 600）照契约。 */
.rv-sidebar-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: var(--hxx-radius-sm);
  color: var(--hxx-text-2);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.4;
  transition: background-color var(--hxx-t-fast), color var(--hxx-t-fast);
}

.rv-sidebar-item + .rv-sidebar-item {
  margin-top: 2px;
}

.rv-sidebar-item:hover {
  background: var(--hxx-hover);
  color: var(--hxx-text);
}

.rv-sidebar-item:focus-visible {
  outline: 2px solid var(--hxx-primary);
  outline-offset: -2px;
}

.rv-sidebar-item.is-active {
  background: var(--hxx-primary);
  color: var(--hxx-tip-fg);
  font-weight: 600;
}

.rv-sidebar-item.is-active:hover {
  background: var(--hxx-primary-hover);
  color: var(--hxx-tip-fg);
}

.rv-sidebar-icon {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
}

.rv-sidebar-text {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* 期次徽标：M2 页面已在菜单里预留入口，但还没交付。
   不写这个标记，用户点进去就是 404，会以为系统坏了。 */
.rv-sidebar-tag {
  flex: none;
  margin-left: auto;
  padding: 0 6px;
  border-radius: var(--hxx-radius-full);
  background: var(--hxx-neutral-soft);
  color: var(--hxx-text-3);
  font-size: 11px;
  font-weight: 600;
  line-height: 16px;
}

.rv-sidebar-foot {
  margin-top: auto;
  padding-top: var(--hxx-gap-sm);
  border-top: 1px solid var(--hxx-border);
}

/* 侧栏底部用户块：手机上侧栏是抽屉，退出入口放这里比顶栏更稳。 */
.rv-sidebar-user {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: var(--hxx-radius-sm);
  color: var(--hxx-text-2);
}

.rv-sidebar-user:hover {
  background: var(--hxx-hover);
  color: var(--hxx-text);
}

.rv-sidebar-user-name {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 13px;
}

/* 顶栏：固定、与侧栏错开。层级比侧栏低一档——手机上抽屉打开时要压住顶栏。 */
.rv-topbar {
  position: fixed;
  top: 0;
  left: var(--app-sidebar-w);
  right: 0;
  z-index: 10000;
  display: flex;
  align-items: center;
  gap: var(--hxx-gap-sm);
  height: var(--app-topbar-h);
  padding: 0 var(--hxx-gap-lg);
  background: var(--hxx-surface);
  border-bottom: 1px solid var(--hxx-border);
}

.rv-topbar-right {
  display: flex;
  align-items: center;
  gap: var(--hxx-gap-xs);
  margin-left: auto;
}

/* 面包屑：只读导航信息，弱色；最后一级用主文字色表明「你在哪」。 */
.rv-crumb {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  font-size: 13px;
  color: var(--hxx-text-3);
}

.rv-crumb-item {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.rv-crumb-item.is-current {
  color: var(--hxx-text);
  font-weight: 600;
}

.rv-crumb-sep {
  flex: none;
  color: var(--hxx-text-placeholder);
}

/* 学校名：多校区/多校部署时，用户需要随时确认自己在哪个租户下。 */
.rv-school {
  max-width: 180px;
  overflow: hidden;
  padding: 0 8px;
  color: var(--hxx-text-3);
  font-size: 13px;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* 用户菜单触发器：胶囊、hover 浅底。里面复用库的 .hxx-avatar。 */
.rv-user {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px 4px 4px;
  border: 1px solid transparent;
  border-radius: var(--hxx-radius-full);
  background: transparent;
  color: var(--hxx-text-2);
  font-family: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: background-color var(--hxx-t-fast), border-color var(--hxx-t-fast);
}

.rv-user:hover {
  background: var(--hxx-hover);
  border-color: var(--hxx-border);
}

.rv-user-name {
  max-width: 110px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* 用户菜单面板内容（放在 <template> 里由库的 HXXPopover 克隆）。
   这里只给「菜单行」的观感，面板壳、箭头、出入场全部由库负责。 */
.rv-menu {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 148px;
}

.rv-menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border-radius: var(--hxx-radius-xs);
  color: var(--hxx-text-2);
  font-size: 13px;
}

.rv-menu-item:hover {
  background: var(--hxx-hover);
  color: var(--hxx-text);
}

/* 菜单里的「退出登录」是 <button>（语义正确），但按钮自带边框/底色/字体，
   这里统一抹平成和 <a> 一样的菜单行——同一个视觉，两种语义，是故意的。 */
button.rv-menu-item {
  width: 100%;
  border: 0;
  background: transparent;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}

.rv-menu-sep {
  height: 1px;
  margin: 4px 6px;
  background: var(--hxx-border);
}

/* 手机遮罩：侧栏以抽屉形式打开时，点空白要能关掉。 */
.rv-scrim {
  position: fixed;
  inset: 0;
  z-index: 10000;
  display: none;
  background: var(--hxx-mask);
}

/* 页面主体：唯一的内容容器（骨架约定 §2）。
   注意：**不能**用 `.hxx-page` 当页面容器——那是库的分页按钮类名（撞名事故有先例）。 */
.rv-main {
  min-height: 100vh;
  margin-left: var(--app-sidebar-w);
  padding: calc(var(--app-topbar-h) + var(--hxx-gap-lg)) var(--hxx-gap-lg) var(--hxx-gap-xl);
}

.rv-mobile-actions {
  display: none;
}

/* 首屏淡入：JS 装配完外壳后给 body 加 .rv-ready。
   反向设计——默认（没 .rv-ready）内容是**可见**的：脚本挂了也不会白屏。
   **只做 8px 位移、不动 opacity**：目标环境（Windows Server / RDP / 无头浏览器）
   经常把 CSS 动画判为 reduce 或直接暂停，一旦动画停在第一帧，`opacity:0` 的
   `both` 填充会让整页内容永远看不见（实测踩到过：截图里主体几乎透明）。
   位移停在第一帧只是「偏下 8px」，内容照样读得到。 */
body.rv-ready .rv-main {
  animation: rv-slide-in var(--hxx-t-slow) ease both;
}

@keyframes rv-slide-in {
  from {
    transform: translateY(8px);
  }

  to {
    transform: none;
  }
}

/* 断点 ≤1023px：侧栏改为抽屉。
   1023 而不是 768：教师端阅卷页在 900px 宽的窗口下仍要能用满宽度。 */
@media (max-width: 1023px) {
  .rv-sidebar {
    transform: translateX(-100%);
    transition: transform var(--hxx-t-slow) cubic-bezier(0.22, 1, 0.36, 1);
    box-shadow: none;
  }

  body.rv-nav-open .rv-sidebar {
    transform: none;
    box-shadow: var(--hxx-shadow-lg);
  }

  body.rv-nav-open .rv-scrim {
    display: block;
  }

  .rv-topbar {
    left: 0;
    padding: 0 var(--hxx-gap-md);
  }

  .rv-main {
    margin-left: 0;
    padding: calc(var(--app-topbar-h) + var(--hxx-gap-md)) var(--hxx-gap-md) var(--hxx-gap-lg);
  }

  /* 抽屉打开时页面不跟着滚 */
  body.rv-nav-open {
    overflow: hidden;
  }
}

@media (max-width: 768px) {
  .rv-main {
    padding: calc(var(--app-topbar-h) + 12px) 12px var(--hxx-gap-lg);
    padding-bottom: calc(var(--hxx-gap-lg) + env(safe-area-inset-bottom));
  }

  /* 手机上顶栏只有 44px 可用宽：面包屑会被挤成「我… / 个…」这种残字。
     页面自己的 .rv-page-title 已经说明了「这是哪一页」，所以窄屏直接把
     面包屑与学校名让给汉堡、主题、用户三项。 */
  .rv-topbar .rv-crumb,
  .rv-school {
    display: none;
  }

  .rv-user-name {
    display: none;
  }

  /* 移动端底部操作条：契约 §6.9 第 8 条要求可点区域 ≥44px，
     所以这里给的是 >=44px 高的按钮位，页面往 .rv-mobile-actions 里塞按钮即可。 */
  .rv-mobile-actions {
    position: fixed;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 900;
    display: flex;
    gap: 8px;
    padding: 8px 12px calc(8px + env(safe-area-inset-bottom));
    background: var(--hxx-surface);
    border-top: 1px solid var(--hxx-border);
  }

  .rv-mobile-actions > * {
    flex: 1;
    min-height: 44px;
  }
}

/* ============================================================
   二、页面头与整页状态
   ------------------------------------------------------------
   骨架约定 §8.3 定死了详情页/列表页的头：标题 + 描述 + 右侧动作。
   ============================================================ */

.rv-page-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--hxx-gap-md);
  margin-bottom: var(--hxx-gap-md);
}

/* 页头左侧（标题 + 说明）必须可收缩，否则长标题 + 长说明会把右侧操作区挤出容器。
   实测（2026-11-06）：users 页 1366 宽下 `.rv-page-actions` 只有 146px，
   但内部按钮内容宽 192px → 整个 `.rv-main` 的 scrollWidth 溢出 22px，
   而**任何单个元素都没越过视口右界**（所以"找越界元素"的检查抓不到）。
   flex 的默认 `min-width:auto` 会让子项拒绝收缩到内容宽度以下，这是 flex 布局最经典的坑。 */
.rv-page-head > * {
  min-width: 0;
}

.rv-page-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  /* 操作区优先保持整行；放不下时整块换到下一行（由父级 flex-wrap 处理） */
  flex: 0 0 auto;
}

/* 操作区里的按钮不许被压缩：flex 默认会让按钮收缩到 min-content 以下，
   导致按钮文字溢出自己的盒子、把 .rv-main 的 scrollWidth 顶出去（实测 users 页溢出 22px）。 */
.rv-page-actions .hxx-btn {
  flex-shrink: 0;
  white-space: nowrap;
}

/* 页头在窄容器的兜底：**整块竖排**而不是挤压。
   实测（2026-11-06）：即使按钮加了 flex-shrink:0，`.rv-page-actions` 仍被父级
   flex 布局压到 146px（内容 192px），按钮 scrollWidth 撑出容器 →
   整页出现 18px 横向滚动条。根因是"标题区 412px + 操作区 192px"在 flex-wrap 下
   本该换行却没换（`min-width:auto` 让父项按最小内容算，wrap 判定失效）。
   所以在窄于 1024px 时直接让页头变成竖排块布局，一次性消除整类问题。 */
@media (max-width: 1024px) {
  .rv-page-head {
    flex-direction: column;
    align-items: stretch;
  }

  .rv-page-actions {
    justify-content: flex-end;
    width: 100%;
  }
}

/* 契约 §7：页面头标题 24px/800；§14：≤480px 收到 22px。 */
.rv-page-title {
  margin: 0;
  font-size: 24px;
  font-weight: 800;
  line-height: 1.3;
  color: var(--hxx-text);
}

.rv-page-desc {
  margin: 6px 0 0;
  font-size: 14px;
  color: var(--hxx-text-3);
}

@media (max-width: 480px) {
  .rv-page-title {
    font-size: 22px;
  }
}

/* 整页状态（无权限 / 加载失败 / OAuth 回调结果）：
   骨架约定 §6 要求「无权限时不白屏」，所以这套状态块要能顶替整个内容区。 */
.rv-state-page {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--hxx-gap-sm);
  min-height: 60vh;
  padding: var(--hxx-gap-xl) var(--hxx-gap-md);
  text-align: center;
}

.rv-state-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border-radius: var(--hxx-radius-full);
  background: var(--hxx-muted);
  color: var(--hxx-text-3);
}

.rv-state-icon.is-ok {
  background: var(--hxx-success-soft);
  color: var(--hxx-success);
}

.rv-state-icon.is-bad {
  background: var(--hxx-danger-soft);
  color: var(--hxx-danger);
}

/* 状态图标比正文大一号：它是这块区域唯一的视觉锚点 */
.rv-state-icon .rv-icon {
  width: 24px;
  height: 24px;
}

/* 独立结果页容器（login-callback.html）：没有侧边栏/顶栏，内容垂直居中。
   为什么单独一个类而不用 .rv-main：.rv-main 带 240px 左侧留白（那是外壳的），
   结果页不该有外壳。 */
.rv-callback {
  display: grid;
  place-items: center;
  min-height: 100vh;
  padding: var(--hxx-gap-lg) var(--hxx-gap-md);
}

/* 外壳装不起来时（后端不可达导致 /me 失败）由 app.js 给 body 加 .rv-no-shell：
   此时没有侧栏也没有顶栏，内容区不能还留着 240px 空档与 56px 顶栏让位。
   只在「渲染整页错误」这一条路径上挂，正常页面不受影响。 */
body.rv-no-shell .rv-main {
  margin-left: 0;
  padding-top: var(--hxx-gap-xl);
  padding-bottom: var(--hxx-gap-xl);
}

.rv-state-title {
  margin: 0;
  font-size: 18px;
  font-weight: 700;
  color: var(--hxx-text);
}

.rv-state-desc {
  max-width: 520px;
  margin: 0;
  font-size: 14px;
  color: var(--hxx-text-3);
}

.rv-state-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin-top: var(--hxx-gap-sm);
}

/* ============================================================
   三、卡片与工具条（契约 §10.1 / §10.8）
   ------------------------------------------------------------
   列表页固定结构（骨架约定 §8.1）：工具条 → 表格 → 页脚 都在一张卡里，
   所以卡片默认**不留内边距**，内边距由各分区自己给（否则表格会被 20px 内边距
   挤成一条，或者要写负 margin 去抵）。需要留白的区域用 .rv-card-body。
   ============================================================ */

.rv-card {
  background: var(--hxx-surface);
  border: 1px solid var(--hxx-border);
  border-radius: var(--hxx-radius);
  box-shadow: var(--hxx-shadow-xs);
  overflow: hidden;
}

.rv-card + .rv-card,
.rv-card + .rv-toolbar {
  margin-top: var(--hxx-gap-md);
}

.rv-card-body {
  padding: 20px;
}

.rv-card-body + .rv-card-body {
  border-top: 1px solid var(--hxx-border);
}

/* 工具条：契约 §10.8（flex / gap 8 / padding 10 / 底 --hxx-tile / 圆角 12）。
   卡片内的工具条不再单独给圆角，否则会出现「卡里一张小卡」的双层边框感。 */
.rv-toolbar,
.hxx-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  background: var(--hxx-tile);
}

.rv-card > .rv-toolbar,
.rv-card > .hxx-toolbar {
  border-radius: 0;
}

.rv-toolbar .hxx-input,
.rv-toolbar .hxx-select-wrap .hxx-select-trigger,
.hxx-toolbar .hxx-input {
  /* 契约 §10.8：工具栏里的输入框收到 36px / 13px，比表单里的 38px 矮一档，
     这样一排控件不会把工具条撑得比表格 header 还高。 */
  height: 36px;
  font-size: 13px;
}

.rv-toolbar-grow {
  flex: 1 1 auto;
  min-width: 0;
}

.rv-toolbar-right {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
}

/* ============================================================
   四、表格（契约 §10.9 · 库未实现，本项目落地）
   ------------------------------------------------------------
   为什么自建：契约 §10.9 给了规格（外框圆角 + 1px 边、th 12px/600 弱色 + muted 底、
   td padding 12 + 底边、行 hover），但库里 `.data-table` / `.hxx-table-wrap` 命中数为 0。
   两个名字写同一条规则：`.rv-table*` 是页面作者用的（骨架约定），
   `.data-table` / `.hxx-table-wrap` 是契约的历史命名，只维护一套数值。
   ============================================================ */

.rv-table-wrap,
.hxx-table-wrap {
  border-radius: var(--hxx-radius);
  border: 1px solid var(--hxx-border);
  /* 宽表格在窄屏内部横滚，页面级不出现横向滚动条（契约 §6.9 第 1 条）。 */
  overflow: auto;
}

/* 卡片里的表格不再套一层边框：外框已经由 .rv-card 给了。 */
.rv-card > .rv-table-wrap,
.rv-card > .hxx-table-wrap {
  border: 0;
  border-radius: 0;
}

.rv-table,
.data-table {
  width: 100%;
  /* separate 而非 collapse：collapse 下 sticky 表头会丢掉边框且抖动 */
  border-collapse: separate;
  border-spacing: 0;
  font-size: 14px;
}

.rv-table thead th,
.data-table thead th {
  padding: 10px 12px;
  background: var(--hxx-muted);
  color: var(--hxx-text-3);
  font-size: 12px;
  font-weight: 600;
  text-align: left;
  white-space: nowrap;
}

.rv-table tbody td,
.data-table tbody td {
  padding: 12px;
  border-bottom: 1px solid var(--hxx-border);
  color: var(--hxx-text-2);
  vertical-align: middle;
  /* 窄屏必须禁止逐字换行：375px 下「演示·2026秋高一生物期中」会被压成一列一个字，
     那不是"紧凑"而是不可读。让单元格不换行、由 .rv-table-wrap 内部横滚承担溢出，
     与上面 wrap 的 overflow: auto 是同一条策略的两半（2026-11-06 实测 375px 截图发现）。 */
  white-space: nowrap;
}

/* 需要换行的列（如备注、题干摘要）用这个类显式放开，避免"一刀切 nowrap"把长文本也撑宽。 */
.rv-table td.rv-cell-wrap,
.data-table td.rv-cell-wrap {
  white-space: normal;
  min-width: 200px;
}

.rv-table tbody tr:last-child td,
.data-table tbody tr:last-child td {
  border-bottom: 0;
}

.rv-table tbody tr:hover td,
.data-table tbody tr:hover td {
  background: var(--hxx-hover);
}

/* 操作列：右对齐 + 不换行，行内按钮才不会被挤到第二行。 */
.rv-col-op {
  width: 1%;
  text-align: right;
  white-space: nowrap;
}

/* 表格空态单元格（UI 规范 §2.2）：库只有整块的 `.hxx-empty`，
   而表格里需要「保留表头、只在数据区居中给一句话」，否则用户看不到列结构。 */
.rv-empty-cell {
  padding: 40px 12px;
  color: var(--hxx-text-3);
  font-size: 13px;
  text-align: center;
}

.rv-empty-cell p {
  margin: 8px 0 0;
}

/* 空态图标：36px、淡到 0.4（规范 §2.2）。用遮罩类而不是 <img>，
   因为 assets 里的线稿是 currentColor，<img> 会渲染成黑色实心块。 */
.rv-empty-cell .rv-icon {
  display: block;
  width: 36px;
  height: 36px;
  margin: 0 auto;
  opacity: 0.4;
}

/* 行级骨架（UI 规范 §2.8）：组合库的 .hxx-skeleton，不新造扫光动画。 */
.rv-skeleton-row td {
  padding: 14px 12px;
}

/* 表头吸顶（UI 规范 §2.4）：长名单滚起来后表头会跑丢。
   底色必须是实色 --hxx-muted，半透明会透出下面的行内容，看起来像叠影。 */
.data-table.is-sticky thead th,
.rv-table.is-sticky thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--hxx-muted);
}

/* 列排序指示（UI 规范 §2.6）：三角用边框画，颜色全走令牌。 */
.data-table th[data-sort],
.rv-table th[data-sort] {
  cursor: pointer;
  user-select: none;
}

.data-table th[data-sort]::after,
.rv-table th[data-sort]::after {
  content: "";
  display: inline-block;
  width: 0;
  height: 0;
  margin-left: 6px;
  vertical-align: 1px;
  border-right: 4px solid transparent;
  border-bottom: 5px solid var(--hxx-text-placeholder);
  border-left: 4px solid transparent;
  opacity: 0.5;
}

.data-table th[data-sort].is-sort-asc::after,
.rv-table th[data-sort].is-sort-asc::after {
  border-bottom-color: var(--hxx-primary);
  opacity: 1;
}

.data-table th[data-sort].is-sort-desc::after,
.rv-table th[data-sort].is-sort-desc::after {
  border-top: 5px solid var(--hxx-primary);
  border-bottom-color: transparent;
  opacity: 1;
}

/* 表格页脚：放分页器。上边框把它和最后一行数据分开。 */
.rv-table-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--hxx-gap-sm);
  padding: 12px;
  border-top: 1px solid var(--hxx-border);
}

/* ============================================================
   五、完整分页器 .rv-pager（UI 规范 §2.1）
   ------------------------------------------------------------
   库里只有 `.hxx-pagination` + 上一页/下一页 + `n / total`，
   后台列表要页码、跳页、每页条数——这套在契约里没有，属 B 类补齐。
   ============================================================ */

.rv-pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  margin-top: var(--hxx-gap-md);
  font-variant-numeric: tabular-nums;
}

/* 页脚里的分页器不需要再来一次上距（页脚自己已有 padding）。 */
.rv-table-foot .rv-pager {
  margin-top: 0;
}

.rv-pager-num {
  min-width: 32px;
  height: 32px;
  padding: 0 6px;
  border: 0;
  border-radius: var(--hxx-radius-sm);
  background: transparent;
  color: var(--hxx-text-2);
  font-family: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: background-color var(--hxx-t-fast), color var(--hxx-t-fast);
}

.rv-pager-num:hover {
  background: var(--hxx-hover);
  color: var(--hxx-text);
}

.rv-pager-num:focus-visible {
  outline: 2px solid var(--hxx-primary);
  outline-offset: -2px;
}

.rv-pager-num.is-active {
  background: var(--hxx-primary);
  color: var(--hxx-tip-fg);
  font-weight: 600;
}

.rv-pager-num.is-active:hover {
  background: var(--hxx-primary-hover);
  color: var(--hxx-tip-fg);
}

.rv-pager-num[disabled] {
  color: var(--hxx-text-placeholder);
  pointer-events: none;
}

.rv-pager-ellipsis {
  width: 24px;
  color: var(--hxx-text-3);
  font-size: 13px;
  text-align: center;
}

.rv-pager-jump,
.rv-pager-size {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--hxx-text-3);
  font-size: 13px;
}

/* 跳页输入框：复用库的 .hxx-input-sm（32px/13px），只钳宽度。 */
.rv-pager-jump .hxx-input {
  width: 56px;
  text-align: center;
}

.rv-pager-total {
  color: var(--hxx-text-3);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}

/* ============================================================
   六、行内操作与批量条（UI 规范 §2.3 / §2.7）
   ------------------------------------------------------------
   行内操作为什么不用 `.hxx-btn`：标准按钮最小高 38px（sm 也有 32px），
   在 44px 行高的表格里会把行撑开、表格失去节奏。这里是「文字链接」语义。
   命名：规范 §2.7 写的 `.hxx-bulkbar`，但 §0.2 要求宿主新增一律 `rv-` 前缀，
   按前缀纪律实现为 `.rv-bulkbar`（已在登记块注明）。
   ============================================================ */

.rv-row-action {
  display: inline-flex;
  align-items: center;
  height: 28px;
  padding: 0 10px;
  border: 0;
  border-radius: var(--hxx-radius-xs);
  background: transparent;
  color: var(--hxx-primary);
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 500;
  cursor: pointer;
  transition: background-color var(--hxx-t-fast), color var(--hxx-t-fast);
}

.rv-row-action + .rv-row-action {
  margin-left: 2px;
}

.rv-row-action:hover {
  background: var(--hxx-primary-soft);
}

.rv-row-action.is-danger {
  color: var(--hxx-danger);
}

.rv-row-action.is-danger:hover {
  background: var(--hxx-danger-soft);
}

.rv-row-action[disabled] {
  opacity: 0.5;
  pointer-events: none;
}

.rv-bulkbar {
  position: sticky;
  bottom: var(--hxx-gap-md);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--hxx-gap-md);
  min-height: 52px;
  margin-top: var(--hxx-gap-md);
  padding: 0 var(--hxx-gap-md);
  border: 1px solid var(--hxx-border);
  border-radius: var(--hxx-radius);
  background: var(--hxx-surface);
  box-shadow: var(--hxx-shadow-md);
  animation: rv-bulkbar-in var(--hxx-t) ease both;
}

.rv-bulkbar-count {
  color: var(--hxx-text-2);
  font-size: 13px;
}

.rv-bulkbar-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

@keyframes rv-bulkbar-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

/* ============================================================
   七、状态徽标 .rv-status-chip（UI 规范 §3.3）
   ------------------------------------------------------------
   规范 §3.3 的结论是「不新造颜色，复用库 .hxx-tag 的 color 属性，把状态→颜色
   的映射固定下来」。所以这里**没有**任何颜色/尺寸声明，只留一个稳定的钩子类，
   让页面能统一选中/追加（例如批量改字号），映射表在 js/ui.js 的 statusChip()。
   ============================================================ */

.rv-status-chip {
  vertical-align: middle;
}

/* ============================================================
   八、详情网格与统计磁贴（契约 §10.16 / §10.10）
   ------------------------------------------------------------
   两个名字同一条规则：`.rv-detail-grid`（骨架约定 §8.3 用 dl/dt/dd）
   与 `.hxx-detail-grid`（契约 §10.16 命名，库未实现）。
   ============================================================ */

.rv-detail-grid,
.hxx-detail-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 var(--hxx-gap-md);
  margin: 0;
}

.rv-detail-field,
.hxx-detail-field {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--hxx-border);
}

.rv-detail-label,
.hxx-detail-label {
  margin: 0;
  color: var(--hxx-text-3);
  font-size: 12px;
}

.rv-detail-value,
.hxx-detail-value {
  margin: 0;
  color: var(--hxx-text);
  font-size: 14px;
  font-weight: 500;
  word-break: break-word;
}

.rv-stat-grid,
.hxx-stat-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.rv-stat-item,
.hxx-stat-item {
  flex: 1 1 140px;
  min-width: 140px;
  padding: 16px;
  border: 1px solid var(--hxx-border);
  border-radius: var(--hxx-radius);
  background: var(--hxx-surface);
}

.rv-stat-item:hover,
.hxx-stat-item:hover {
  border-color: var(--hxx-border-strong);
  box-shadow: var(--hxx-shadow);
}

.rv-stat-num,
.hxx-stat-num {
  color: var(--hxx-text);
  font-size: 26px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
}

.rv-stat-num.is-primary,
.hxx-stat-num.primary {
  color: var(--hxx-primary);
}

.rv-stat-label,
.hxx-stat-label {
  margin-top: 2px;
  color: var(--hxx-text-3);
  font-size: 12px;
}

@media (max-width: 768px) {
  .rv-detail-grid,
  .hxx-detail-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ============================================================
   九、图标按钮 .hxx-icon-btn（契约 §10.31，库未实现）
   ------------------------------------------------------------
   只有图标没有文字的按钮（主题切换、汉堡、关闭）。图标说不清自己是什么，
   所以 label 必填（aria-label + 气泡文案由调用方给）。
   图标颜色一律 currentColor，**不用主色蓝**——一排蓝图标会把次级命令
   抬到与主按钮同级（契约 §10.31 明文）。
   ============================================================ */

.hxx-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 0;
  border-radius: var(--hxx-radius-sm);
  background: transparent;
  color: var(--hxx-text-2);
  font-family: inherit;
  cursor: pointer;
  transition: background-color var(--hxx-t-fast), color var(--hxx-t-fast);
}

.hxx-icon-btn:hover {
  background: var(--hxx-hover);
  color: var(--hxx-text);
}

.hxx-icon-btn:focus-visible {
  outline: 2px solid var(--hxx-primary);
  outline-offset: 1px;
}

.hxx-icon-btn[disabled] {
  opacity: 0.6;
  pointer-events: none;
}

.hxx-icon-btn .rv-icon,
.hxx-icon-btn svg {
  width: 20px;
  height: 20px;
}

.hxx-icon-btn.hxx-icon-btn-sm {
  width: 32px;
  height: 32px;
}

.hxx-icon-btn.hxx-icon-btn-sm .rv-icon,
.hxx-icon-btn.hxx-icon-btn-sm svg {
  width: 18px;
  height: 18px;
}

/* ============================================================
   十、表单字段（骨架约定 §8.2）
   ------------------------------------------------------------
   为什么不用库的 `.hxx-form-item`：那是「label 定宽 72px 右对齐」的横排弹窗布局；
   页面级表单要的是竖排（label 在上、控件满宽、错误文案贴底），否则长标签全被截。
   ============================================================ */

.rv-form {
  display: flex;
  flex-direction: column;
  gap: var(--hxx-gap-md);
}

.rv-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.rv-label {
  color: var(--hxx-text-2);
  font-size: 13px;
  font-weight: 500;
}

.rv-field .hxx-input,
.rv-field .hxx-textarea,
.rv-field .hxx-select-wrap {
  width: 100%;
}

.rv-form-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--hxx-gap-sm);
  margin-top: 4px;
}

.rv-form-note {
  margin: 0;
  color: var(--hxx-text-3);
  font-size: 12px;
  line-height: 1.6;
}

/* ============================================================
   十一、图标（CSS 遮罩方案）
   ------------------------------------------------------------
   为什么用 mask-image 而不是内联 SVG 或 <img>：
   · <img> 无法跟随 currentColor，深色主题下图标会是黑块；
   · 内联 SVG 需要 fetch + 注入，页面首帧会闪一下空位；
   · 遮罩 + background-color: currentColor，颜色随文字色自动成立（深浅主题都对），
     且不写一行内联样式、不引脚本。
   图标文件缺失时的表现是「占位空白」而不是裂图——图形由 svg-artist 交付，
   缺文件不能让页面看起来坏掉。所有文件名见 assets/icons/。 */
.rv-icon {
  display: inline-block;
  flex: none;
  width: 18px;
  height: 18px;
  background-color: currentColor;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: contain;
  mask-size: contain;
}

/* 侧栏图标 18px（与 ASCII 行高对齐），顶栏图标由 .hxx-icon-btn 给 20px。 */
.rv-sidebar-icon .rv-icon {
  width: 18px;
  height: 18px;
}

.rv-i-home { -webkit-mask-image: url("../assets/icons/home.svg"); mask-image: url("../assets/icons/home.svg"); }
.rv-i-layers { -webkit-mask-image: url("../assets/icons/layers.svg"); mask-image: url("../assets/icons/layers.svg"); }
.rv-i-clipboard { -webkit-mask-image: url("../assets/icons/clipboard.svg"); mask-image: url("../assets/icons/clipboard.svg"); }
.rv-i-users { -webkit-mask-image: url("../assets/icons/users.svg"); mask-image: url("../assets/icons/users.svg"); }
.rv-i-scan { -webkit-mask-image: url("../assets/icons/scan.svg"); mask-image: url("../assets/icons/scan.svg"); }
.rv-i-card { -webkit-mask-image: url("../assets/icons/card.svg"); mask-image: url("../assets/icons/card.svg"); }
.rv-i-grading { -webkit-mask-image: url("../assets/icons/grading.svg"); mask-image: url("../assets/icons/grading.svg"); }
.rv-i-chart { -webkit-mask-image: url("../assets/icons/chart.svg"); mask-image: url("../assets/icons/chart.svg"); }
.rv-i-student { -webkit-mask-image: url("../assets/icons/student.svg"); mask-image: url("../assets/icons/student.svg"); }
.rv-i-teacher { -webkit-mask-image: url("../assets/icons/teacher.svg"); mask-image: url("../assets/icons/teacher.svg"); }
.rv-i-shield { -webkit-mask-image: url("../assets/icons/shield.svg"); mask-image: url("../assets/icons/shield.svg"); }
.rv-i-sparkles { -webkit-mask-image: url("../assets/icons/sparkles.svg"); mask-image: url("../assets/icons/sparkles.svg"); }
.rv-i-warning { -webkit-mask-image: url("../assets/icons/warning.svg"); mask-image: url("../assets/icons/warning.svg"); }
.rv-i-building { -webkit-mask-image: url("../assets/icons/building.svg"); mask-image: url("../assets/icons/building.svg"); }
.rv-i-grid { -webkit-mask-image: url("../assets/icons/grid.svg"); mask-image: url("../assets/icons/grid.svg"); }
.rv-i-settings { -webkit-mask-image: url("../assets/icons/settings.svg"); mask-image: url("../assets/icons/settings.svg"); }
.rv-i-audit { -webkit-mask-image: url("../assets/icons/audit.svg"); mask-image: url("../assets/icons/audit.svg"); }
.rv-i-menu { -webkit-mask-image: url("../assets/icons/menu.svg"); mask-image: url("../assets/icons/menu.svg"); }
.rv-i-sun { -webkit-mask-image: url("../assets/icons/sun.svg"); mask-image: url("../assets/icons/sun.svg"); }
.rv-i-moon { -webkit-mask-image: url("../assets/icons/moon.svg"); mask-image: url("../assets/icons/moon.svg"); }
.rv-i-chevron-down { -webkit-mask-image: url("../assets/icons/chevron-down.svg"); mask-image: url("../assets/icons/chevron-down.svg"); }
.rv-i-logout { -webkit-mask-image: url("../assets/icons/logout.svg"); mask-image: url("../assets/icons/logout.svg"); }
.rv-i-user { -webkit-mask-image: url("../assets/icons/user.svg"); mask-image: url("../assets/icons/user.svg"); }
.rv-i-key { -webkit-mask-image: url("../assets/icons/key.svg"); mask-image: url("../assets/icons/key.svg"); }
.rv-i-inbox { -webkit-mask-image: url("../assets/icons/inbox.svg"); mask-image: url("../assets/icons/inbox.svg"); }
.rv-i-image { -webkit-mask-image: url("../assets/icons/image.svg"); mask-image: url("../assets/icons/image.svg"); }
.rv-i-percent { -webkit-mask-image: url("../assets/icons/percent.svg"); mask-image: url("../assets/icons/percent.svg"); }
.rv-i-book-open { -webkit-mask-image: url("../assets/icons/book-open.svg"); mask-image: url("../assets/icons/book-open.svg"); }
.rv-i-git-branch { -webkit-mask-image: url("../assets/icons/git-branch.svg"); mask-image: url("../assets/icons/git-branch.svg"); }
.rv-i-search { -webkit-mask-image: url("../assets/icons/search.svg"); mask-image: url("../assets/icons/search.svg"); }
.rv-i-plus { -webkit-mask-image: url("../assets/icons/plus.svg"); mask-image: url("../assets/icons/plus.svg"); }
.rv-i-history { -webkit-mask-image: url("../assets/icons/history.svg"); mask-image: url("../assets/icons/history.svg"); }
.rv-i-check { -webkit-mask-image: url("../assets/icons/check.svg"); mask-image: url("../assets/icons/check.svg"); }
.rv-i-x { -webkit-mask-image: url("../assets/icons/x.svg"); mask-image: url("../assets/icons/x.svg"); }

/* 用户菜单里的下箭头比正文小一档，否则 18px 会顶高菜单行。 */
.rv-user .rv-icon {
  width: 14px;
  height: 14px;
  color: var(--hxx-text-3);
}

/* ============================================================
   十二、登录页（index.html）
   ------------------------------------------------------------
   左右分栏：左侧品牌区（叙事 + 插画），右侧登录卡。
   为什么不是「单列卡片居中」：三端共用的入口，第一屏需要说明这是成绩系统
   而不是某一个端的工具，左侧用来承担这件事；375px 下左栏收成头部条。
   ============================================================ */

.rv-auth {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  min-height: 100vh;
  background: var(--hxx-bg);
}

.rv-auth-panel {
  display: flex;
  flex-direction: column;
  gap: var(--hxx-gap-md);
  padding: 48px 56px;
  border-right: 1px solid var(--hxx-border);
  /* 品牌区用主色软底往表面色渐隐：深浅主题下都成立（浅色 soft 是实色浅蓝，
     深色 soft 是半透明蓝，都是令牌，不写死色值）。 */
  background: linear-gradient(160deg, var(--hxx-primary-soft) 0%, var(--hxx-surface) 58%);
  overflow: hidden;
}

.rv-auth-card {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 48px;
}

.rv-auth-form {
  width: 100%;
  max-width: var(--app-login-card-w);
  margin: 0 auto;
}

.rv-auth-slogan {
  margin: 0;
  color: var(--hxx-text);
  font-size: 30px;
  font-weight: 800;
  line-height: 1.35;
}

.rv-auth-desc {
  max-width: 460px;
  margin: 0;
  color: var(--hxx-text-2);
  font-size: 14px;
}

/* 能力清单：用「·」而不是 emoji（契约 §1 禁止 emoji 当图标）。 */
.rv-auth-points {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 0;
  padding: 0;
  color: var(--hxx-text-2);
  font-size: 13px;
  list-style: none;
}

.rv-auth-points li {
  position: relative;
  padding-left: 14px;
}

.rv-auth-points li::before {
  content: "";
  position: absolute;
  top: 0.7em;
  left: 0;
  width: 4px;
  height: 4px;
  border-radius: var(--hxx-radius-full);
  background: var(--hxx-primary);
}

/* 插画：由 svg-artist 交付；文件不在时整个容器被 JS 移除，不会留白洞。 */
.rv-auth-hero {
  margin-top: auto;
  max-width: 460px;
}

.rv-auth-hero img {
  display: block;
  width: 100%;
  height: auto;
}

.rv-auth-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  /* 压到面板底部：插画缺席时页脚不能浮在半空（`.rv-auth-hero` 也有 margin-top:auto，
     两者按「先推插画、再推页脚」自然叠放）。 */
  margin: auto 0 0;
  color: var(--hxx-text-3);
  font-size: 12px;
}

.rv-auth-title {
  margin: 0;
  color: var(--hxx-text);
  font-size: 22px;
  font-weight: 800;
}

.rv-auth-sub {
  margin: 6px 0 0;
  color: var(--hxx-text-3);
  font-size: 13px;
}

/* 登录失败提示：与字段红边、抖动动画一起构成「三件套」，
   文案本身必须能读（不能只靠动效传达失败，AGENTS.md §5.5）。 */
.rv-auth-error {
  margin: 0;
  padding: 10px 12px;
  border: 1px solid var(--hxx-danger);
  border-radius: var(--hxx-radius-sm);
  background: var(--hxx-danger-soft);
  color: var(--hxx-danger);
  font-size: 13px;
  line-height: 1.6;
}

.rv-auth-tip {
  margin: var(--hxx-gap-md) 0 0;
  color: var(--hxx-text-3);
  font-size: 12px;
  line-height: 1.6;
}

/* 高级选项（学校编码）：单校部署时用不到，折起来减少认知负担；
   多校平台要能改，所以只折叠、不隐藏。 */
.rv-auth-advanced {
  display: flex;
  flex-direction: column;
  gap: var(--hxx-gap-md);
}

.rv-auth-advanced[hidden] {
  display: none;
}

/* 文字按钮：登录页的「高级选项」这类次要动作用它，比 .hxx-btn 轻，
   不会和主按钮抢注意力。 */
.rv-link-btn {
  align-self: flex-start;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--hxx-primary);
  font-family: inherit;
  font-size: 12.5px;
  cursor: pointer;
}

.rv-link-btn:hover {
  color: var(--hxx-primary-deep);
  text-decoration: underline;
}

.rv-link-btn:focus-visible {
  outline: 2px solid var(--hxx-primary);
  outline-offset: 2px;
}

/* 服务状态点：页脚那一颗绿/红点。颜色只用功能色令牌。
   失败态不刷屏——失败时静默保持红点 + 一行文案，不弹 toast。 */
.rv-status-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: var(--hxx-radius-full);
  background: var(--hxx-neutral);
}

.rv-status-dot.is-ok {
  background: var(--hxx-success);
}

.rv-status-dot.is-bad {
  background: var(--hxx-danger);
}

.rv-status-dot.is-checking {
  background: var(--hxx-warning);
}

.rv-status-text {
  color: var(--hxx-text-3);
  font-size: 12px;
}

@media (max-width: 900px) {
  .rv-auth {
    grid-template-columns: minmax(0, 1fr);
  }

  .rv-auth-panel {
    gap: var(--hxx-gap-sm);
    padding: 28px 20px;
    border-right: 0;
    border-bottom: 1px solid var(--hxx-border);
  }

  .rv-auth-slogan {
    font-size: 22px;
  }

  /* 手机竖屏：插画与能力清单让位给登录卡（首屏必须能直接开始输入） */
  .rv-auth-hero,
  .rv-auth-points {
    display: none;
  }

  .rv-auth-card {
    padding: 24px 20px 40px;
  }
}

/* ============================================================
   十三、个人中心（account.html）
   ------------------------------------------------------------
   四区块用库的 .hxx-tabs，不新造分段器；这里只补「绑定状态行」这类页面内小件。
   ============================================================ */

.rv-account-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--hxx-gap-md);
  margin-bottom: var(--hxx-gap-md);
}

.rv-account-id {
  min-width: 0;
}

.rv-account-name {
  margin: 0;
  color: var(--hxx-text);
  font-size: 18px;
  font-weight: 700;
}

.rv-account-meta {
  margin: 2px 0 0;
  color: var(--hxx-text-3);
  font-size: 13px;
}

/* 绑定状态行：一行「当前状态 + 说明 + 按钮」。
   按钮置灰时必须有解释（为什么不能点），所以配 .rv-hint-line 一起用。 */
.rv-bind-state {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--hxx-gap-sm);
  padding: 12px 14px;
  border: 1px solid var(--hxx-border);
  border-radius: var(--hxx-radius-sm);
  background: var(--hxx-muted);
}

.rv-bind-state-text {
  flex: 1 1 240px;
  min-width: 0;
  color: var(--hxx-text-2);
  font-size: 13px;
}

.rv-hint-line {
  margin: 0;
  color: var(--hxx-text-3);
  font-size: 12px;
  line-height: 1.6;
}

/* 首登强制改密：只留一个区块，且页面内没有任何返回入口。
   这条提示要让用户明白「不是系统限制你，是必须走完这一步」。 */
.rv-force-tip {
  margin-bottom: var(--hxx-gap-md);
}

/* ============================================================
   十四、契约 §6.9 第 6 条：表格数据区必须可选中复制
   ------------------------------------------------------------
   全局 `* { user-select: none }` 是品牌默认，但「能看到的数据要能复制」是硬要求
   （老师要把名单贴进 Excel、把错误信息贴给管理员）。这里是唯一的例外清单：
   表格数据、弹窗正文、详情值。只列这几处，不做更宽的放开。 */
.rv-table tbody td,
.data-table tbody td,
.rv-detail-value,
.hxx-detail-value,
.rv-empty-cell,
.hxx-dialog-body,
.hxx-dialog-body * {
  user-select: text;
  -webkit-user-select: text;
}

/* ============================================================
   十六、组织树（admin/org.html）
   ------------------------------------------------------------
   为什么自建：库没有树组件；组织层级（校区 → 年级 → 班级）是管理端
   唯一需要"一眼看全"的结构，用嵌套 ul/li 而不是可视化大图，键盘与读屏都能用。 */
.rv-tree {
  margin: 0;
  padding: 0;
  list-style: none;
}

.rv-tree-node + .rv-tree-node {
  margin-top: 2px;
}

.rv-tree-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 8px;
  border-radius: var(--hxx-radius-sm);
  color: var(--hxx-text-2);
  font-size: 13px;
  cursor: pointer;
}

.rv-tree-row:hover {
  background: var(--hxx-hover);
  color: var(--hxx-text);
}

.rv-tree-row:focus-visible {
  outline: 2px solid var(--hxx-primary);
  outline-offset: -2px;
}

.rv-tree-toggle {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  padding: 0;
  border: 0;
  border-radius: var(--hxx-radius-xs);
  background: transparent;
  color: var(--hxx-text-3);
  cursor: pointer;
  transition: transform var(--hxx-t-fast);
}

.rv-tree-toggle[aria-expanded="true"] {
  transform: rotate(90deg);
}

.rv-tree-toggle:disabled {
  opacity: 0;
  pointer-events: none;
}

.rv-tree-name {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.rv-tree-count {
  margin-left: auto;
  color: var(--hxx-text-3);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.rv-tree-children {
  margin: 2px 0 0;
  padding: 0 0 0 16px;
  list-style: none;
  border-left: 1px dashed var(--hxx-border);
}

.rv-tree-children[hidden] {
  display: none;
}

/* ============================================================
   十七、只读字段与控制台布局（管理端多页共用）
   ------------------------------------------------------------
   `.rv-split`：左侧树/筛选 + 右侧表格的两栏布局（org / templates 等页）。
   `.rv-stack`：页面内多个卡片/区块的纵向堆叠（比每处写 inline style 可控）。
   `.rv-section-title`：卡片内的小节标题（页面头之外的第二层标题）。
   `.rv-readonly`：后端未就绪时给"看起来像表单但不可操作"的字段用，
   比 disabled 输入框更轻（用户不会以为是自己填错了）。 */
.rv-split {
  display: grid;
  grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
  gap: var(--hxx-gap-md);
  align-items: start;
}

/* 纵向堆叠容器：多个卡片的间距只在这里定一次，页面不再写 inline margin。 */
.rv-stack {
  display: flex;
  flex-direction: column;
  gap: var(--hxx-gap-md);
}

/* 卡片内小节标题：比 .rv-page-title（24px/800）低一档，避免每张卡都像页面头。 */
.rv-section-title {
  margin: 0 0 12px;
  color: var(--hxx-text);
  font-size: 16px;
  font-weight: 700;
}

/* 答题卡模板预览缩略（admin/templates.html）：
   版面是 A3 横向（420×297mm），缩略图必须固定高度 + object-fit:contain，
   否则会按原始比例把卡片撑成一条横向长条，把页面横向撑破。 */
.rv-tpl-thumb {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 200px;
  padding: 8px;
  border: 1px solid var(--hxx-border);
  border-radius: var(--hxx-radius-sm);
  background: var(--hxx-muted);
  overflow: hidden;
}

.rv-tpl-thumb img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

.rv-tpl-thumb .rv-icon {
  width: 36px;
  height: 36px;
  opacity: 0.4;
}

.rv-readonly {
  color: var(--hxx-text-2);
  font-size: 13px;
}

.rv-readonly[aria-disabled="true"] {
  color: var(--hxx-text-3);
}

/* 库的 .hxx-empty 只给 svg/img 定尺寸；本项目图标是遮罩 span（.rv-icon），
   这里补上同样的 48px + 0.4 透明度，否则占位块里会是一个 18px 的小图标。 */
.hxx-empty .rv-icon {
  width: 48px;
  height: 48px;
  opacity: 0.4;
}

@media (max-width: 900px) {
  .rv-split {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* 打印兜底（student/paper-view.html）：查卷页要求"禁打印"，
   前端能做的只有"打印时不输出内容"——真正的防线在服务端水印与审计。 */
@media print {
  body[data-print-guard="1"] .rv-main {
    display: none;
  }
}

/* ============================================================
   十八、[hidden] 兜底
   ------------------------------------------------------------
   浏览器内置的 `[hidden] { display: none }` 是**元素级**规则，直接被
   `.rv-field { display: flex }` 这类类选择器压掉——表现是「JS 设了 hidden
   却还是显示」。典型踩坑：`required:false` 时本该整块不出现的人机验证区。
   放在文件最后一条、靠「同特异性后者胜出」生效，而不是用强制优先级标记
   （宿主代码禁用它，AGENTS.md §5.4）。 */
[hidden] {
  display: none;
}

/* ============================================================
   十二、答题卡模板库卡片（UI 规范 §8.4 · admin/templates.html）
   ------------------------------------------------------------
   为什么自建：库只给 `.hxx-card`（内容卡），没有"可选中的预览卡"。
   这一页要"整卡可点 + 版面缩略 + 标签"，属于后台列表外的一种展示形态。

   纪律：
   · `.rv-tpl-thumb` 已登记过（固定 200px 高 + object-fit:contain），
     因为版面是 A3 横向，不定高会把卡片撑成横向长条、把页面撑破。
   · 卡片 hover **只改边框色**，不做位移 —— 位移会让点击热区跟着动，触屏上容易点空。
   ============================================================ */

/* 网格：auto-fill + minmax，1366 / 1920 / 375 三种宽度都自然成立，
   不需要写三档断点（少写两套 CSS 就少两套要维护的规则）。 */
.rv-tpl-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: var(--hxx-gap-md);
  margin-bottom: var(--hxx-gap-lg);
}

.rv-tpl-card {
  padding: 12px;
  border: 1px solid var(--hxx-border);
  border-radius: var(--hxx-radius);
  background: var(--hxx-surface);
  cursor: pointer;
  transition: border-color var(--hxx-t-fast), background-color var(--hxx-t-fast);
}

.rv-tpl-card:hover,
.rv-tpl-card:focus-visible {
  border-color: var(--hxx-primary);
  background: var(--hxx-hover);
  outline: none;
}

.rv-tpl-name {
  margin-top: 8px;
  color: var(--hxx-text);
  font-size: 14px;
  font-weight: 600;
}

.rv-tpl-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}

/* 计数行：一眼看出"本校几张 / 内置几张"，避免用户以为列表没加载完。 */
.rv-tpl-count {
  margin-bottom: 8px;
  color: var(--hxx-text-3);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}

/* 详情弹窗里的版面大图：固定高度 + contain，理由同 .rv-tpl-thumb。 */
.rv-tpl-detail-img {
  display: block;
  width: 100%;
  height: 240px;
  object-fit: contain;
  margin-bottom: 12px;
  border: 1px solid var(--hxx-border);
  border-radius: var(--hxx-radius-sm);
  background: var(--hxx-muted);
}

/* ============================================================
   十三、分页器窄屏排布（2026-11-06 · 主 agent 补，实测发现）
   ------------------------------------------------------------
   375px 下 `.rv-pager` 一行放不下「共 N 条 / 页码 / 跳至 __ 页 / 20 条/页」四组，
   结果是跳页输入框和每页条数被挤成两行、并且**超出卡片右边 30px**。

   为什么不改成"隐藏跳页"：窄屏下用户更需要跳页（滚动翻页很累）。
   正确做法是**降低信息密度、改成纵向堆叠**：
     第一行：共 N 条 · 第 x/y 页
     第二行：页码按钮
     第三行：跳至 __ 页   每页 N 条
   ============================================================ */

@media (max-width: 560px) {
  .rv-pager {
    justify-content: center;
    gap: 10px;
    row-gap: 12px;
  }

  /* 总数与页码信息独占一行，居中 */
  .rv-pager-total {
    flex-basis: 100%;
    text-align: center;
  }

  /* 跳页与每页条数两个小组合并到同一行，右对齐；放不下时各自换行 */
  .rv-pager-jump,
  .rv-pager-size {
    flex: 0 1 auto;
  }

  /* 跳页输入框在窄屏再收窄 8px，保证「跳至 __ 页」整体不超一行 */
  .rv-pager-jump .hxx-input {
    width: 48px;
  }
}

/* ============================================================
   十四、滚动容器必须真正能裁切（2026-11-06 · 实测修正）
   ------------------------------------------------------------
   现象：375px 下 users 页仍然横向溢出 34px。
   实测数据：`table.cw=598`（7 列 nowrap 的真实宽度），
   `.rv-table-wrap.cw=428 / sw=598 / overflow:auto` —— 滚动容器本身是对的，
   但它的父级 `.rv-main.sw=530 > cw=496`，**整页还是溢出**。

   根因：`.rv-table-wrap` 处在 flex/grid 布局里，而 flex/grid 子项的默认值是
   `min-width: auto` —— 它会按"内容最小宽度"撑开，**overflow:auto 根本来不及生效**。
   这是 CSS 里最经典也最隐蔽的一条：`overflow:auto` 不是万能裁切，
   必须同时满足"容器自己能被压缩到比内容更窄"，即 `min-width: 0`。

   铁律：**任何要靠 `overflow` 裁切的容器，都要显式写 `min-width: 0`
   （纵向裁切对应 `min-height: 0`）。** 这条对 flex 与 grid 都成立。
   ============================================================ */

.rv-table-wrap,
.hxx-table-wrap {
  min-width: 0;
}

/* 卡片作为表格的父级也要能收缩，否则它会把外层撑开 */
.rv-card {
  min-width: 0;
}

/* 内容区同理：它是 .rv-main 的直接子级 */
.rv-main > * {
  min-width: 0;
}
/* ============================================================
   十五、平台管理员「退出当前学校」按钮（2026-11-06 · 配套 enter-school）
   ------------------------------------------------------------
   为什么需要它：`POST /api/platform/enter-school/{id}` 是**换发令牌**（不是前端改个字段），
   所以管理员进错校之后，如果没有这个出口，就只能清 localStorage 重新登录。
   这个按钮是 enter-school 的配套安全阀。

   视觉上它沿用 `.rv-school`（顶栏里显示当前学校名），只多一个可点的交互态。
   注意：窄屏时 `.rv-school` 被 `display:none` 隐藏了（为了腾出顶栏空间），
   但那会导致窄屏下**没有退校出口** —— 所以这一条必须用 `display:inline-flex` 覆盖回来。
   ============================================================ */

.rv-school-exit {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  /* 可点区域 ≥32px：顶栏本身 56px 高，32 是与之协调的最小值（同类图标按钮是 36） */
  min-height: 32px;
  padding: 0 10px;
  border: 1px solid var(--hxx-border);
  border-radius: var(--hxx-radius-full);
  background: transparent;
  color: var(--hxx-text-2);
  font-family: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: border-color var(--hxx-t-fast), background-color var(--hxx-t-fast), color var(--hxx-t-fast);
}

.rv-school-exit:hover {
  border-color: var(--hxx-primary);
  background: var(--hxx-hover);
  color: var(--hxx-primary);
}

.rv-school-exit:focus-visible {
  outline: 2px solid var(--hxx-primary);
  outline-offset: 1px;
}

/* 图标尺寸与文本对齐（图标是 1em 的 span 遮罩，这里只钳住大小） */
.rv-school-exit .rv-icon {
  width: 14px;
  height: 14px;
}

/* 窄屏必须保留退校出口：覆盖 `.rv-school { display:none }`（见上条注释） */
@media (max-width: 900px) {
  .rv-topbar .rv-school-exit {
    display: inline-flex;
    /* 只截断文字，不隐藏按钮 */
    max-width: 96px;
  }
}

/* ============================================================
   十六、弹窗内表单排版（2026-11-06 · 建校弹窗首次使用）
   ------------------------------------------------------------
   为什么需要这三个类：库的 `.hxx-form-item` 是**单列纵向**的，
   一个 6 字段的弹窗会被拉得很长（每个字段 label+input 约 66px），
   而"学校编码 + 学段"、"管理员账号 + 姓名"这两对天生适合并排。
   契约里没有"弹窗内两列表单"的类，按 AGENTS §5.4 在这里登记。

   只用令牌（--hxx-*），不写任何硬编码色值；深色主题靠令牌自动成立。
   ============================================================ */

/* 两列网格：窄屏（<560px）自动回落单列，避免弹窗里挤出横向滚动 */
.rv-form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 var(--hxx-gap-md, 12px);
  min-width: 0;
}

@media (max-width: 560px) {
  .rv-form-grid {
    grid-template-columns: 1fr;
  }
}

/* 必填星号：只做视觉提示（真正的校验在前后端都做），用危险色令牌 */
.rv-req {
  color: var(--hxx-danger);
  font-weight: 400;
}

/* 弹窗内的分组分隔线：比 <hr> 默认样式更轻，只作"这一段换了主题"的提示 */
.rv-dlg-sep {
  height: 0;
  margin: var(--hxx-gap-md, 12px) 0;
  border: 0;
  border-top: 1px solid var(--hxx-border);
}

/* ============================================================
   十七、成绩分析页排版（2026-11-06）
   ------------------------------------------------------------
   为什么自己画条形图而不是引图表库：
   AGENTS §4.2 要求"300 行内能自己写清楚且有人看得懂，优先自己写"。
   分数段分布本质就是"标签 + 横条 + 人数"，一个 flex 行就够；
   引 ECharts 之类要 1MB+ 且要处理它的主题与令牌体系，得不偿失。

   全部用令牌，深色主题自动成立。
   ============================================================ */

/* 分数段行：标签定宽 + 条弹性 + 人数定宽 + 占比定宽。
   min-width:0 让条能在窄屏收缩（AGENTS §6.1「overflow 需配 min-width:0」）。 */
.rv-seg-row {
  display: flex;
  align-items: center;
  gap: var(--hxx-gap-sm, 8px);
  padding: 5px 0;
  min-width: 0;
}

.rv-seg-label {
  flex: 0 0 auto;
  width: 84px;
  color: var(--hxx-text-2);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.rv-seg-bar {
  flex: 1 1 auto;
  min-width: 0;
  height: 14px;
  border-radius: var(--hxx-radius-sm, 4px);
  background: var(--hxx-bg);
  overflow: hidden;
}

.rv-seg-bar i {
  display: block;
  height: 100%;
  /* 品牌主色的浅色版：条是"数据"不是"按钮"，不该抢注意力 */
  background: var(--hxx-primary-soft);
  border-radius: inherit;
  transition: width var(--hxx-t-slow) ease;
}

.rv-seg-count {
  flex: 0 0 auto;
  width: 52px;
  color: var(--hxx-text);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}

.rv-seg-percent {
  flex: 0 0 auto;
  width: 56px;
  color: var(--hxx-text-3);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}

/* 窄屏：标签收窄、占比隐藏（占比可由"人数 + N"心算，不占窄屏空间） */
@media (max-width: 560px) {
  .rv-seg-label {
    width: 64px;
  }

  .rv-seg-percent {
    display: none;
  }
}

/* 口径说明列表：读长文本，所以要 text-selectable（库的全局默认是 user-select:none） */
.rv-explain-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0;
  padding-left: 20px;
  color: var(--hxx-text-2);
  font-size: 13px;
  line-height: 1.7;
  user-select: text;
}

.rv-explain-list strong {
  color: var(--hxx-text);
}

/* ============================================================
   十八、审计明细单元格（2026-11-06）
   ------------------------------------------------------------
   detailJson 是"不同动作不同形状"的结构化数据，统一平铺成 key:value。
   要 user-select:text：审计在争议时刻是要被复制出去对质的。
   ============================================================ */
.rv-audit-detail {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 12px;
  line-height: 1.5;
  user-select: text;
  min-width: 0;
}
/* ============================================================
   十九、试算结果区（2026-11-06 · 赋分规则页）
   ------------------------------------------------------------
   试算结果要嵌在弹窗里、与输入表单同屏，所以既要能滚动（表格可能 5 行 × 多列），
   又不能撑破弹窗（AGENTS §6.2 第 7 条：弹窗主内容壳禁止 max-height + overflow，
   滚动交给 .hxx-dialog-body；这里的嵌套区用固定上限）。
   ============================================================ */
.rv-preview-result {
  margin-top: var(--hxx-gap-md, 12px);
  max-height: 280px;
  overflow: auto;
  min-width: 0;
}

.rv-preview-result .rv-table-wrap {
  min-width: 0;
}