/* ============================================================
 * components.css — 应用级组件样式（纯原生 CSS，不依赖 Tailwind 编译）
 *
 * 用途：
 *   1. 共享布局（侧边栏/顶栏/抽屉）与登录页直接使用这些类；
 *   2. 13 个业务页面暂保留 Tailwind 原子类写法（设计稿原样），
 *      后续改造时可逐步替换为本文件的组件类，统一维护。
 *
 * 命名规范：app- 前缀，避免与 Tailwind 工具类冲突。
 * 颜色取值与 assets/js/twconfig.js 的 token 保持一致。
 * ============================================================ */

/* ---------- 按钮 ---------- */
.app-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 10px 16px;
  border-radius: 8px;
  border: 1px solid transparent;
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  transition: all 0.15s ease;
  user-select: none;
}
.app-btn:active { transform: scale(0.97); }
.app-btn:disabled { opacity: 0.55; cursor: not-allowed; transform: none; }

.app-btn-primary {
  background: var(--c-primary);
  color: #fff;
  box-shadow: 0 2px 6px rgba(68, 78, 197, 0.35);
}
.app-btn-primary:hover:not(:disabled) { filter: brightness(1.08); }

.app-btn-ghost {
  background: transparent;
  color: var(--c-on-surface);
  border-color: var(--c-border-subtle);
}
.app-btn-ghost:hover:not(:disabled) { background: var(--c-canvas-bg); }

.app-btn-danger {
  background: var(--c-danger-soft);
  color: var(--c-tertiary);
}
.app-btn-danger:hover:not(:disabled) { filter: brightness(0.97); }

.app-btn-block { width: 100%; }

/* ---------- 表单 ---------- */
.app-field-label {
  display: block;
  margin-bottom: 4px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--c-outline);
}

.app-input,
.app-select,
.app-textarea {
  width: 100%;
  box-sizing: border-box;
  padding: 10px 12px;
  font-size: 14px;
  font-family: inherit;
  color: var(--c-on-surface);
  background: var(--c-card-bg);
  border: 1px solid var(--c-border-subtle);
  border-radius: 8px;
  outline: none;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.app-input:focus,
.app-select:focus,
.app-textarea:focus {
  border-color: var(--c-primary);
  box-shadow: 0 0 0 3px rgba(68, 78, 197, 0.12);
}
.app-input.is-invalid,
.app-select.is-invalid,
.app-textarea.is-invalid {
  border-color: var(--c-error);
  background: var(--c-danger-soft);
}
.app-input::placeholder,
.app-textarea::placeholder { color: var(--c-outline); }

/* ---------- 卡片 ---------- */
.app-card {
  background: var(--c-card-bg);
  border: 1px solid var(--c-border-subtle);
  border-radius: 12px;
  box-shadow: 0 1px 2px rgba(20, 29, 35, 0.04);
}

.app-card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 20px;
  border-bottom: 1px solid var(--c-border-subtle);
}

/* ---------- 徽章 ---------- */
.app-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 700;
  line-height: 1.6;
}
.app-badge-primary { background: var(--c-primary-soft); color: var(--c-primary); }
.app-badge-success { background: var(--c-secondary-soft); color: var(--c-secondary); }
.app-badge-warning { background: var(--c-warning-soft); color: var(--c-accent-warning); }
.app-badge-danger  { background: var(--c-danger-soft); color: var(--c-tertiary); }

/* ---------- 表格 ---------- */
.app-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
}
.app-table th {
  padding: 10px 14px;
  text-align: left;
  font-size: 12px;
  font-weight: 700;
  color: var(--c-outline);
  border-bottom: 1px solid var(--c-border-subtle);
  white-space: nowrap;
}
.app-table td {
  padding: 12px 14px;
  border-bottom: 1px solid var(--c-border-subtle);
  color: var(--c-on-surface);
}
.app-table tbody tr:hover { background: var(--c-canvas-bg); }

/* ---------- 提示条 ---------- */
.app-alert {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 10px 14px;
  border-radius: 8px;
  font-size: 13px;
  line-height: 1.5;
}
.app-alert-danger {
  background: var(--c-danger-soft);
  color: var(--c-tertiary);
  border: 1px solid rgba(175, 34, 71, 0.25);
}
.app-alert-warning {
  background: var(--c-warning-soft);
  color: #8a6d00;
  border: 1px solid rgba(255, 188, 0, 0.35);
}
.app-alert-success {
  background: var(--c-secondary-soft);
  color: var(--c-secondary);
  border: 1px solid rgba(0, 108, 77, 0.2);
}

/* ---------- 全局 Toast ---------- */
.app-toast {
  position: fixed;
  left: 50%;
  bottom: 32px;
  transform: translateX(-50%) translateY(16px);
  z-index: 9999;
  padding: 10px 20px;
  border-radius: 10px;
  background: var(--c-sidebar-bg);
  color: #fff;
  font-size: 13px;
  font-weight: 600;
  box-shadow: 0 8px 24px rgba(20, 29, 35, 0.3);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.app-toast.is-show {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* ---------- 移动端侧边抽屉 ---------- */
.app-mobile-sidebar {
  position: fixed;
  top: 0;
  left: 0;
  height: 100%;
  width: 260px;
  z-index: 60;
  transform: translateX(-100%);
  transition: transform 0.25s ease-in-out;
}
.app-mobile-sidebar.is-open { transform: translateX(0); }
.app-mobile-backdrop {
  position: fixed;
  inset: 0;
  z-index: 55;
  background: rgba(0, 0, 0, 0.5);
}

/* ---------- 侧栏导航：菜单项统一 14px ----------
   一级菜单与「配置管理」下的二级菜单同字号、同字重，只靠间距和图标区分层级；
   分组标题（NAVIGATION / 配置管理）保持 11px 辅助层级，不做统一。 */
#app-sidebar-inner .app-nav-link,
.app-mobile-sidebar .app-nav-link {
  font-size: 14px;
  line-height: 20px;
  font-weight: 500;
  letter-spacing: 0.01em;
}
#app-sidebar-inner .app-nav-link > span:not(.material-symbols-outlined),
.app-mobile-sidebar .app-nav-link > span:not(.material-symbols-outlined) {
  font-size: 14px;
}
/* 菜单里的图标统一 20px，与 14px 文字基线对齐 */
#app-sidebar-inner .app-nav-link > span.material-symbols-outlined,
.app-mobile-sidebar .app-nav-link > span.material-symbols-outlined {
  font-size: 20px;
  width: 20px;
  height: 20px;
  line-height: 20px;
}
/* 「班主任」等右侧徽章仍是 10px，避免抢标题视线 */
#app-sidebar-inner .app-nav-link > span.rounded-full { font-size: 10px; }

/* ---------- 低高度屏幕（1366×768 教学机等）----------
   收紧导航间距，保证「帮助/退出」完整可见。 */
@media (max-height: 820px) {
  #app-sidebar-inner { padding-top: 14px; padding-bottom: 14px; }
  #app-sidebar-inner .app-nav-link { padding-top: 9px; padding-bottom: 9px; }
  #app-sidebar-inner > div:last-child > a { padding-top: 8px; padding-bottom: 8px; }
}

/* 侧边栏菜单可滚动时底部渐隐，提示"还有内容" */
.sidebar-scroll.is-scrollable {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent);
}

/* ---------- 侧栏菜单滚动条 ----------
   与主内容区 .app-scroll（常规总览右侧那条）用**同一组数值**：8px / 无轨道 / 2px 透明边
   content-box / 圆角 4 / rgba(131,145,162,.35)，hover 到 .6 —— 换页不换样。
   用 #app-sidebar-inner 提权：各页 <style> 里的 .custom-scroll 覆盖（5px / #c6c5d6 / #DCE2E6）
   不会再渗进来。Chrome 121+ 认 scrollbar-color 后会忽略 ::-webkit-scrollbar，
   两处都写是为了兼容旧内核与 Firefox。 */
#app-sidebar-inner .sidebar-scroll {
  /* 抵掉 aside 的 px-4：滚动条贴到侧栏右缘，菜单内容仍保留原来的右侧留白 */
  margin-right: -16px;
  padding-right: 16px;
  scrollbar-width: thin;
  scrollbar-color: rgba(131, 145, 162, 0.45) transparent;
}
#app-sidebar-inner .sidebar-scroll::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
#app-sidebar-inner .sidebar-scroll::-webkit-scrollbar-track {
  background: transparent;
}
#app-sidebar-inner .sidebar-scroll::-webkit-scrollbar-thumb {
  background: rgba(131, 145, 162, 0.35);
  border-radius: 4px;
  border: 2px solid transparent;
  background-clip: content-box;
}
#app-sidebar-inner .sidebar-scroll::-webkit-scrollbar-thumb:hover {
  background: rgba(131, 145, 162, 0.6);
  background-clip: content-box;
}

/* 年级筛选条：窄屏横向滚动，避免按钮被压成竖排文字 */
#grade-filter {
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
#grade-filter::-webkit-scrollbar { display: none; }
#grade-filter > button { flex: 0 0 auto; white-space: nowrap; }

/* ============================================================
 * 共享外壳首帧（layout.js 注入前）
 * 之前的问题：首帧时 #app-sidebar 还是个空 div（宽 0），主内容先按全宽铺开，
 * 等 fetch 回来（约 30~200ms）侧栏才挤进来，整页横向抖一下；顶栏同理把内容往下推。
 * 这里在 CSS 里先把最终尺寸占住，注入只是「填色」，不再改变布局。
 * ============================================================ */
#app-sidebar {
  flex: 0 0 auto;
  width: 0;
  align-self: stretch;
}
#app-header {
  width: 100%;
}
#app-header:empty {
  height: 70px;
  background: var(--c-card-bg);
  border-bottom: 1px solid var(--c-border-subtle);
}
@media (min-width: 1024px) {
  #app-sidebar {
    width: 260px;
  }
  /* 空壳先画成侧栏底色，避免首帧出现一条白缝 */
  #app-sidebar:empty {
    background: var(--c-sidebar-bg);
  }
}

/* 侧栏品牌区：整块固定高度兜住「单位名 1 行 ↔ 2 行」的高度变化，菜单不会跳。
   注意别再把两行高度写死在 .sidebar-brand-name 上——名字只有一行时，
   min-height:2.6em 会在名字与副标题之间留出半行空白，LOGO 也跟着错位。
   单行时由外层的 items-center 让 LOGO 与文字块自然居中对齐。 */
.sidebar-brand {
  min-height: 58px; /* ≈ 两行单位名(2×20.6px) + 副标题(14px)，留 1px 余量 */
}
.sidebar-brand-name {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* 同一会话内第二次及以后跳转：不再重播入场动画（首帧即终态） */
html[data-revisit] .animate-fadeInUp {
  animation: none !important;
}

/* 跳转反馈：顶部细进度条（延迟出现，快速跳转时不会闪一下） */
#app-nav-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 2px;
  width: 0;
  background: var(--c-primary);
  z-index: 9999;
  opacity: 0;
  pointer-events: none;
  transition: width 0.35s ease, opacity 0.15s ease;
}
#app-nav-progress.is-on {
  opacity: 1;
  width: 72%;
}
#app-nav-progress.is-done {
  opacity: 0;
  width: 100%;
  transition: width 0.1s linear, opacity 0.25s ease 0.08s;
}

/* ============================================================
 * 快速评分抽屉：隐藏状态必须由本地 CSS 保证
 * 之前 #quick-drawer 用 Tailwind 的 translate-x-full 藏起来，
 * 但抽屉是 JS 注入的，注入那一刻 Tailwind 运行时还没编译出这个类，
 * 于是面板先整块显示在右侧 100~300ms，再滑走 —— 这就是每次跳转都"闪一下"的主因。
 * ============================================================ */
#quick-drawer {
  position: fixed;
  top: 0;
  right: 0;
  height: 100%;
  width: 100%;
  max-width: 28rem;
  z-index: 50;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  background: var(--c-card-bg);
  box-shadow: -8px 0 32px rgba(20, 29, 35, 0.18);
  transform: translateX(100%);
  transition: transform 0.3s ease-in-out;
  will-change: transform;
}
#quick-drawer.is-open {
  transform: translateX(0);
}
#quick-drawer-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 40;
  background: rgba(0, 0, 0, 0.5);
}
#quick-drawer-backdrop.is-open {
  display: block;
}
@media (prefers-reduced-motion: reduce) {
  #quick-drawer {
    transition: none;
  }
}

/* ---------- 跳转观感：把"会动的"提前定住 ---------- */

/* 1) 画布底色。跳转瞬间浏览器先铺的是 html 背景，默认白色会闪一下；
      设成与页面同色后，这种闪白基本看不出来。 */
html {
  background: var(--c-canvas-bg);
}

/* 2) 主区不许被压缩：外壳改成「头部/底部固定 + 中间独立滚动」后，
      滚动容器必须能把高度让出来（min-height:0），否则内容会把整页撑高。 */
.app-main-col > .app-scroll {
  min-height: 0;
}

/* 3) 侧栏菜单行高固定：Nunito Sans / Material Symbols 是外链字体，
      晚到时会重排一次，1px 的字体度量差异会把整列菜单顶得上下晃。
      行高写死后，字体换不换都不影响布局。 */
#app-sidebar-inner .app-nav-link {
  height: 44px;
  min-height: 44px;
  align-items: center;
}
#app-sidebar-inner .app-nav-link .material-symbols-outlined {
  line-height: 1;
}

/* 4) 录入页班级矩阵：数据回来前先占住高度，24 个班一次性渲染时不再把整页撑开一次。
      用 .is-loading 而不是 :empty —— 一旦填入卡片 :empty 就失效，高度会瞬间塌回去。 */
#entry-classes.is-loading {
  min-height: 0;
}
/* 骨架格子：尺寸与真实班级卡片一致（24 班 2 列 = 12 行，行高 103px） */
.entry-skeleton {
  height: 103px;
  border-radius: 10px;
  border: 1px solid var(--c-border-subtle);
  background: linear-gradient(100deg, #f7f9fc 30%, #eef2f7 45%, #f7f9fc 60%);
  background-size: 220% 100%;
  animation: skeleton-sweep 1.1s linear infinite;
}
@keyframes skeleton-sweep {
  from { background-position: 120% 0; }
  to { background-position: -20% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .entry-skeleton { animation: none; }
}

/* 录入页进度条：数据回来前是空的，渲染后约 44px 高，会整页下推 60px */
#entry-progress {
  min-height: 44px;
}

/* ---------- 数据回填时不再把版面撑开 ---------- */

/* 看板筛选行：年级芯片是 ajax 填的，填满后会把这行挤成两行，整页下移 34px。
   改成单行横向滚动（与录入页年级筛选一致），高度恒定。 */
.dash-filter-row {
  /* 芯片填满后这一行会从 38px 变 84px（换行），高度先按住，
     视觉上只是下方留白多一点，不会把整页推走 */
  align-content: flex-start;
}
@media (min-width: 640px) {
  .dash-filter-row {
    min-height: 84px;
  }
}

/* 年级报表矩阵：表格 24 行渲染时卡片会长 200px，先把位置占住 */
#gr-matrix-card {
  min-height: 500px;
}

/* 班级页「学校信息」按钮：文案从「学校信息」变成真校名，宽度变化会把整排按钮推走 */
#cl-school-edit {
  min-width: 150px;
}

/* 录入页年级筛选条：数据回来前是空的，回填后 34px，会推一下 */
#entry-grades {
  min-height: 34px;
}

/* 我的班级名片条：班级名/meta 回填后从 42px 长到 102px */
#mc-banner {
  min-height: 102px;
}

/* 我的班级：主标题固定单行，长班级名不换行，避免回填后把头图撑高 60px */
#mc-title {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

/* 班级页「学校信息」按钮占固定宽度，文案长短变化不推动整排按钮 */
#cl-school-edit {
  width: 168px;
  max-width: 168px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* 看板页头：筛选芯片是异步填的，填完这行会比首帧高一截（整页下移 32px）。
   一次性把页头高度按住（窄屏本来就是纵向堆叠，不限制）。 */
@media (min-width: 640px) {
  .dash-head {
    min-height: 124px;
  }
}

/* 看板 KPI 卡片：数值从「—」变成 0/24班后，说明文字会多折一行，卡片从 143 长到 157。
   统一按住高度，五行数字出现时不再把下面整块推移 14px。 */
@media (min-width: 640px) {
  .kpi-row > * {
    min-height: 160px;
  }
}

/* 不要把筛选行撑满：撑满会挤窄左侧标题，导致"实时同步中"折成两行 */

/* 年级报表的年级切换胶囊：8 个年级会折成两行（74px），先把高度占住 */
#gr-grade-pills {
  min-height: 74px;
  align-content: flex-start;
}

/* ============================================================
 * 应用外壳：头部固定、底部固定、中间内容独立滚动
 *
 * 结构（所有 pc 端有壳页面统一）：
 *   body.app-shell            → 视口高度锁定，页面本身不再滚动
 *     └ .app-main-col         → 右侧主列（sidebar 是它的兄弟）
 *         ├ .app-topbar       → 顶栏 / 页头工具条（不参与滚动）
 *         ├ .app-scroll       → 唯一的滚动容器
 *         └ .app-footer       → 页脚（不参与滚动）
 *
 * 为什么不用 position:fixed：
 *   固定定位会把顶栏/页脚从文档流里摘出去，内容宽度要自己减掉滚动条宽度，
 *   窗口缩放时会错位；这里用 flex + overflow，头部/页脚天然定住，宽度也自动对齐。
 *
 * 阴影：只做「轻压边」，alpha 0.10~0.12、16px 柔和过渡，
 *   目的是让内容滚到边上时有一条柔和的边界，而不是一块明显的灰影。
 * ============================================================ */
html {
  height: 100%;
}

body.app-shell {
  height: 100vh;
  height: 100dvh;
  margin: 0;
  overflow: hidden;
  /* 侧栏与主列并排：pages 里 #app-sidebar 直接挂在 body 下，必须是 flex 才不会被挤成上下两段 */
  display: flex;
}

/* 主列：撑满视口高度，纵向 flex；overflow:hidden 保证子元素不会把页面顶出去 */
.app-main-col {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}

/* 顶栏/页头：不参与滚动，下沿一条柔和阴影 */
.app-topbar {
  position: relative;
  z-index: 30;
  flex: 0 0 auto;
  box-shadow: 0 5px 16px rgba(20, 29, 35, 0.12), 0 1px 2px rgba(20, 29, 35, 0.05);
}

/* 顶栏高度兜底：Tailwind CDN 慢或失败时，头部也不会塌成一条 */
#app-header > header {
  min-height: 70px;
}

/* 顶栏内部原本用 sticky 吸顶，改成外层固定后不再需要，避免多一层合成层 */
.app-topbar > header {
  position: static;
}

/* 唯一的滚动容器 */
.app-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  scrollbar-color: rgba(131, 145, 162, 0.45) transparent;
}
.app-scroll::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
.app-scroll::-webkit-scrollbar-track {
  background: transparent;
}
.app-scroll::-webkit-scrollbar-thumb {
  background: rgba(131, 145, 162, 0.35);
  border-radius: 4px;
  border: 2px solid transparent;
  background-clip: content-box;
}
.app-scroll::-webkit-scrollbar-thumb:hover {
  background: rgba(131, 145, 162, 0.6);
  background-clip: content-box;
}

/* 页脚：不参与滚动，上沿一条柔和阴影 */
.app-footer {
  position: relative;
  z-index: 20;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  height: 56px;
  padding: 0 24px;
  font-size: 13px;
  color: var(--c-sidebar-muted);
  background: var(--c-card-bg);
  border-top: 1px solid var(--c-border-subtle);
  box-shadow: 0 -5px 16px rgba(20, 29, 35, 0.10), 0 -1px 2px rgba(20, 29, 35, 0.03);
}
.app-footer a {
  color: inherit;
  text-decoration: none;
  transition: color 0.15s ease;
}
.app-footer a:hover {
  color: var(--c-primary);
}
.app-footer-left {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.app-footer-right {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
}
.app-footer-dot {
  color: var(--c-border-subtle);
}
/* 页脚未注入前先占住高度，避免底部内容回填时抖一下 */
#app-footer:empty,
.app-footer:empty {
  display: block;
  height: 56px;
  background: var(--c-card-bg);
}

/* 窄屏：页脚让位给底部导航（移动端底部固定栏已经承担了页脚角色），
   滚动区补足底部内边距，最后一张卡片不会被导航盖住 */
@media (max-width: 1023px) {
  body.app-shell .app-footer {
    display: none;
  }
  body.app-shell .app-scroll {
    padding-bottom: 84px;
  }
}

/* 外壳内的 fixed 弹层不受影响；打印时恢复自然流，避免只打印出第一屏 */
@media print {
  body.app-shell {
    height: auto;
    overflow: visible;
  }
  .app-main-col {
    height: auto;
    overflow: visible;
  }
  .app-scroll {
    overflow: visible;
    height: auto;
    padding-bottom: 0;
  }
  .app-topbar,
  .app-footer {
    box-shadow: none;
  }
}

/* ============================================================
   修正：自定义组件自带的 display 会盖掉 Tailwind 的 .hidden
   tailwind.css 先加载、components.css 后加载，两者同为单类选择器（同优先级），
   后加载者胜 → 元素即使写了 class="... hidden" 也照样显示。
   这里用双类选择器提高优先级，让 hidden 真正生效。
   ============================================================ */
.app-alert.hidden,
.app-badge.hidden,
.app-btn.hidden,
.app-card-header.hidden,
.app-main-col.hidden,
.app-footer-right.hidden {
  display: none;
}
