/* ==========================================================================
   Eastudy V3 — 共享组件层（base.css）

   **只放两个 shell 都要用的东西。** 学员端 (index.html) 和控制端
   (admin/index.html) 是两个入口，但按钮、徽章、标签这些"控件"是同一种控件 ——
   控制端的主按钮和学员端的主按钮没有理由长得不一样。

   为什么单独开一个文件，而不是让 admin.html 也引 app.css：
   app.css 里还有 2000 多行是学员端页面专有的（播放器、词卡、课程栅格、
   底部导航）。让控制端把它整份吞下去，等于控制端每次调样式都要先翻一遍
   跟它无关的两千行，而且学员端的类名会顺带把控制端的元素也命中
   （`.chip` 两边都写着，一改就串）。

   载入顺序（两边一致）：
       tokens.css  →  themes-12.css  →  base.css  →  app.css / admin.css
                                        ↑
                    组件默认样式在这里；app/admin 只管自己那部分的**差异**

   所以：改按钮 = 改这一个文件，学员端和控制端同时生效。
        改某个控件在**某一端**的特殊表现 = 改 app.css 或 admin.css 里的覆盖。
        想加一个两端都要的新控件 = 加在这里。

   颜色一律走 token（--brand-accent / --danger / --fg …），本文件里
   **不出现字面色值**，除了那些刻意与主题无关的半透明高光/阴影。
   ========================================================================== */

/* ============================================================ 按钮 */

@font-face{font-family:'Eastudy UI Sans';src:url('/assets/ui/fonts/eastudy-ui-sans.woff2') format('woff2');font-weight:100 900;font-display:swap}
@font-face{font-family:'Eastudy UI Serif';src:url('/assets/ui/fonts/eastudy-ui-serif.woff2') format('woff2');font-weight:200 900;font-display:swap}
:root{--font-sans:'Eastudy UI Sans',Arial,'Microsoft YaHei',sans-serif;--font-read:'Eastudy UI Serif',Georgia,serif}

/* One button material for student, administration and modal actions. */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;padding:10px 16px;border:1px solid var(--control-border);border-radius:var(--radius-control);font:600 14px/1.4 var(--font-sans);color:var(--fg);background:var(--material-control-face);box-shadow:var(--material-control-shadow);cursor:pointer;transition:background var(--motion-ui),box-shadow var(--motion-ui),transform var(--motion-ui)}
.btn--primary{font-size:16px;font-weight:600;background:linear-gradient(180deg,var(--material-button-top),var(--material-button-mid) 48%,var(--material-button-bottom));border-color:var(--primary);color:var(--on-primary);box-shadow:inset 0 1px 0 var(--material-highlight),var(--material-button-shadow)}
.btn--ghost{background:transparent;color:var(--fg-muted);box-shadow:none}
.btn--danger{color:var(--danger);border-color:var(--danger);background:linear-gradient(155deg,color-mix(in srgb,var(--danger-soft) 90%,#fff),var(--danger-soft));box-shadow:inset 0 1px 0 var(--material-highlight),var(--material-control-shadow)}
.btn:hover:not(:disabled){background-color:var(--panel-bg)}
.btn--primary:hover:not(:disabled){box-shadow:inset 0 1px 0 var(--material-highlight),var(--material-button-shadow),var(--material-contact);transform:translateY(-1px)}
.btn:active:not(:disabled){transform:translateY(0)}
.btn:focus-visible{outline:3px solid var(--focus-ring);outline-offset:3px}
.btn:disabled{opacity:.5;cursor:not-allowed;box-shadow:none}
.btn--sm{min-height:36px;padding:6px 10px;font-size:12px}
.btn--block{width:100%}
@media(prefers-reduced-motion:reduce){.btn{transition:none}.btn:hover:not(:disabled){transform:none}}

@supports ((backdrop-filter:blur(8px)) or (-webkit-backdrop-filter:blur(8px))){
 .btn:not(.btn--ghost),.filter-chip,.plan-status__button,.account-theme-modes .btn,.account__links .btn{
  -webkit-backdrop-filter:blur(var(--material-control-blur));
  backdrop-filter:blur(var(--material-control-blur));
 }
}


/* ============================================================ 分段切换 .tabs / .tab

   从 app.css 迁进来。它们不是学员端专有控件 —— 管理端「学员管理」页用的是
   同一套标记，而 admin 页只加载 tokens/themes/base/admin，从不加载 app.css，
   所以那两个 tab 一直是浏览器默认的裸按钮。组件属于谁，看的是标记被谁用，
   不是看它最早写在哪个文件里。

   材质沿用按键那套 --btn-*（同 themes-12.css），理由和进度条一样：
   参考稿里它们是同一块料子，各写各的，以后调主题必然调漏一处。

   选中态是**反向**的横向渐变（暗→中→亮），和 .btn 的左亮右暗正好相反。
   这不是笔误：按钮是凸起的独立块，左边迎光；而 tab 是嵌在一条槽里的分段，
   选中那块看起来该是从槽里浮起来并微微右倾，所以亮端在右。 */

.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); }
.tab {
  position: relative;
  padding: 8px 14px;
  font: inherit; font-size: var(--text-sm);
  color: var(--fg-muted);
  background: transparent; border: none;
  border-bottom: 2px solid transparent;
  cursor: pointer;
  transition: color var(--motion) var(--ease), background-color var(--motion) var(--ease);
}
.tab:hover:not(.is-active) { color: var(--fg); background: var(--surface-hover); }
.tab.is-active {
  color: var(--brand-accent);
  border-bottom-color: transparent;
  font-weight: 500;
}
/* 选中态的下划线换成一小段缎面短杠，而不是整条 2px 平色线。
   它是 ::after 而不是 border：border 会被 .tabs 的 border-bottom 在视觉上
   接成一条通长的线，而参考稿里选中的标记是**断开的一小截**。 */
.tab.is-active::after {
  content: "";
  position: absolute;
  left: 10%; right: 10%; bottom: -2px;
  height: 3px;
  border-radius: var(--radius-pill);
  background-color: var(--brand-accent);
  background-image: linear-gradient(90deg, var(--btn-dark), var(--btn-mid) 48%, var(--btn-light) 92%);
  box-shadow: 0 1px 2px var(--btn-contact);
}
.tab:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -2px; border-radius: var(--radius-sm); }

/* ============================================================ 标签 / 徽章

   三件套: .chip（浅底小标签）、.badge（状态徽章）、.pill（计数药丸）。

   控制端原先完全没有这三样 —— 队列状态、难度、分级在表格里是裸文本,
   和学员端同一份数据的呈现完全是两回事。抽到这里之后两端同一份数据
   长同一个样子, 这是"对齐"里最容易被忽略、又最容易被用户看出来的一半。 */

.chip {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  font-size: var(--text-xs);
  color: var(--fg-muted);
  background: var(--surface-sunken);
  border-radius: var(--radius-pill);
}

.chip--ok { color: var(--success-fg); background: var(--success-bg); }
.chip--warn { color: var(--warn-fg); background: var(--warn-bg); }
.chip--danger { color: var(--danger-fg); background: var(--danger-bg); }
.chip--muted { color: var(--fg-subtle); }
.chip--level { color: var(--level-fg); background: var(--level-bg); font-weight: 600; }
.chip--tag { color: var(--tag-fg); background: var(--tag-bg); }

.badge {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  font-size: var(--text-xs);
  font-weight: 500;
  border-radius: var(--radius-pill);
  white-space: nowrap;
}

.badge--ok { color: var(--success-fg); background: var(--success-bg); }
.badge--warn { color: var(--warn-fg); background: var(--warn-bg); }
.badge--danger { color: var(--danger-fg); background: var(--danger-bg); }
.badge--info { color: var(--info-fg); background: var(--info-bg); }

.pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  font-size: var(--text-xs);
  font-weight: 600;
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  color: var(--fg-muted);
}

/* ============================================================ 表单控件底座

   输入框/下拉/文本域在两端是同一颗控件, 差异只在宽度和容器里。
   原先控制端是靠各自页面里的行内样式撑的。 */
.field { display: flex; flex-direction: column; gap: 6px; }
.field__label { font-size: var(--text-xs); font-weight: 500; color: var(--fg-muted); }
.field__hint { font-size: var(--text-xs); color: var(--fg-subtle); }
.field__error { font-size: var(--text-xs); color: var(--danger); }
.field__req { color: var(--danger); margin-left: 2px; }

/* 带装饰位的输入框外壳。

   左图标和右眼睛按钮都是**绝对定位**的, 不是 flex 里的兄弟节点 —— 因为
   输入框本身要撑满整个宽度, 装饰位得压在它上面。用 flex 排列的话输入框
   会被挤窄, 那个宽度还会随图标有无而变化, 同一张表单里两行输入框的左边缘
   就对不齐了。

   内边距由外壳加到 input 上(见下面两条), 于是"有没有装饰"只影响 input
   自己的 padding, 不影响它占的盒子 —— 这正是对齐需要的不变量。 */
.field__control { position: relative; display: flex; align-items: center; }
.field__control--icon .input { padding-left: 38px; }
.field__control--reveal .input { padding-right: 42px; }

.field__icon {
  position: absolute;
  left: 12px;
  display: flex;
  color: var(--fg-subtle);
  pointer-events: none;   /* 图标压在输入框上, 不挡点击 —— 点它应该聚焦输入框 */
}
.field__control .input:focus-visible ~ .field__reveal { color: var(--fg); }

.field__reveal {
  position: absolute;
  right: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  /* 触控目标不足 44px 时用 padding 把命中区撑开, 视觉大小不变 —— 眼睛图标
     本身直径只有 18px, 直接当按钮点起来会经常点空。 */
  padding: 8px;
  color: var(--fg-subtle);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: color var(--motion-fast) var(--ease), background var(--motion-fast) var(--ease);
}
.field__reveal:hover { color: var(--fg); background: var(--surface-sunken); }
.field__reveal[aria-pressed='true'] { color: var(--brand-accent); }

.input,
.select,
.textarea {
  width: 100%;
  min-height: var(--tap-min);
  padding: 0 12px;
  font: inherit;
  font-size: var(--text-sm);
  color: var(--fg);
  background: var(--surface);
  border: 1px solid var(--border-base);
  border-radius: var(--radius-md);
  transition: border-color var(--motion-fast) var(--ease), box-shadow var(--motion-fast) var(--ease);
}
.textarea { padding: 10px 12px; min-height: 96px; resize: vertical; line-height: 1.5; }
select:not([multiple]):not([size]) {
  appearance: none;
  -webkit-appearance: none;
  padding-inline-end: 36px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m4 6 4 4 4-4' fill='none' stroke='%23334155' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-size: 16px 16px;
  background-position: right 12px center;
}
html[data-mode='dark'] select:not([multiple]):not([size]) {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m4 6 4 4 4-4' fill='none' stroke='%23d7e2ef' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
@media (forced-colors: active) {
  html[data-mode] select:not([multiple]):not([size]) { appearance: auto; background-image: none; }
}
.input:focus-visible,
.select:focus-visible,
.textarea:focus-visible {
  border-color: var(--border-hover);
  box-shadow: 0 0 0 3px var(--brand-ring);
  outline: none;
}
.input:disabled,
.select:disabled,
.textarea:disabled { opacity: 0.55; cursor: not-allowed; }
.input::placeholder,
.textarea::placeholder { color: var(--fg-subtle); }

/* 开关。两端都有"启用/停用"这类二态，用同一颗。 */
.switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  flex: none;
  width: 40px;
  height: 22px;
  padding: 0;
  background: var(--surface-sunken);
  border: 1px solid var(--border-base);
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: background var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease);
}
.switch::after {
  content: '';
  position: absolute;
  left: 2px;
  width: 16px;
  height: 16px;
  background: var(--surface);
  border-radius: 50%;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .22);
  transition: transform var(--motion-fast) var(--ease);
}
.switch[aria-checked="true"] { background: var(--brand-accent); border-color: var(--brand-accent); }
.switch[aria-checked="true"]::after { transform: translateX(18px); }
.switch:disabled { opacity: 0.5; cursor: not-allowed; }

/* ============================================================ 表格

   学员端的表格已经在用这些类（生词本、学习记录），控制端更多 —— 视频管理、
   学员管理、处理队列、邀请码四页全是表格。原先它们两份都不完整：控制端
   连表头对齐都是浏览器默认的粗体左对齐，跟学员端同一张表长得不一样。

   `.is-right` / `.is-clickable` 是修饰类, 由页面在 <th>/<tr> 上手动加。 */

.table-wrap { overflow-x: auto; }
.table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.table th {
  text-align: left;
  padding: 10px 12px;
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--fg-muted);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.table td { padding: 12px; border-bottom: 1px solid var(--border-subtle); vertical-align: middle; }
.table tr.is-clickable:hover { background: var(--surface-hover); cursor: pointer; }
/* 从「处理队列」跳进来的目标行（`?focus=<id>`）。用 ::before 画一道左侧色条
   而不是给 tr 加背景色：tr 的背景在部分浏览器里会被 td 的背景盖住，
   而 td 是透明的、改不了单行。色条画在行内，跟着主题色走，也不用动布局。 */
.table tr.is-focused { position: relative; background: var(--surface-hover); }
.table tr.is-focused > td:first-child { box-shadow: inset 3px 0 0 var(--brand-accent); }
.table tr.is-focused { transition: background var(--motion-slow) var(--ease); }
@media (prefers-reduced-motion: reduce) {
  .table tr.is-focused { transition: none; }
}
.table__empty, .table__loading { text-align: center; padding: var(--space-6); color: var(--fg-subtle); }
.table .is-right { text-align: right; }

/* 单元格里的"标题 + 副标题"两行结构与"一排徽章"结构。 */
.cell-title { display: flex; flex-direction: column; gap: 2px; }
.cell-title__main { font-weight: 500; }
.cell-title__sub { font-size: var(--text-xs); color: var(--fg-muted); }
.cell-badges { display: flex; gap: 4px; flex-wrap: wrap; }

/* ============================================================ 数据展示 */

/* 等宽数字。数字列右对齐 + 用等宽字形, 否则刷新页面时数字宽窄变化会让整列
   左右跳 —— 控制端的队列计数每秒都在变, 这个跳动尤其明显。 */
.num { font-variant-numeric: tabular-nums; text-align: right; }

/* 空态。学员端的 emptyState() 和控制端的表格空行共用同一副骨架。 */
.empty { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: var(--space-7) var(--space-4); text-align: center; }
/* 字号只管字体图标那一路。icon() 出的是 SVG，宽高写死在属性上，
   所以这里用 em 把它的尺寸**跟同样的字号绑定**：两边不打架，
   以后要调大小仍然只改这一个值。 */
.empty__icon { font-size: 32px; color: var(--fg-subtle); line-height: 1; }
.empty__icon svg { width: 1em; height: 1em; display: block; }
.empty__title { font-size: var(--text-base); font-weight: 500; }
.empty__hint { font-size: var(--text-sm); color: var(--fg-muted); max-width: 380px; }

/* 空态 / 错误态在两端的外框一致，内容由各自的 emptyState()/errorState() 填。 */
.state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-6) var(--space-4);
  text-align: center;
  color: var(--fg-muted);
}
.state__title { font-size: var(--text-md); font-weight: 600; color: var(--fg); }
.state__hint { font-size: var(--text-sm); max-width: 42ch; line-height: 1.6; }

.chip{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;height:var(--chip-height);min-height:var(--chip-height);padding:0 8px;font:500 12px/1 var(--font-sans);border-radius:var(--radius-chip);white-space:nowrap}
.chip[data-tone=sage]{--tag-face:var(--tag-sage-bg);--tag-ink:var(--tag-sage-fg)}
.chip[data-tone=teal]{--tag-face:var(--tag-teal-bg);--tag-ink:var(--tag-teal-fg)}
.chip[data-tone=blue]{--tag-face:var(--tag-blue-bg);--tag-ink:var(--tag-blue-fg)}
.chip[data-tone=violet]{--tag-face:var(--tag-violet-bg);--tag-ink:var(--tag-violet-fg)}
.chip[data-tone=amber]{--tag-face:var(--tag-amber-bg);--tag-ink:var(--tag-amber-fg)}
.chip[data-tone=rose]{--tag-face:var(--tag-rose-bg);--tag-ink:var(--tag-rose-fg)}
.input,.select,.textarea{border-radius:var(--radius-control);font-size:16px;min-height:44px;border-color:var(--control-border)}
.tab.is-active{background:var(--selected-bg);color:var(--selection-ink);box-shadow:none}
.field__reveal,.field__reveal:hover,.field__reveal:active{background:transparent;border:0;box-shadow:none}

:is(.panel,.stat-card,.queue-card,.admin-card,.settings-card){--text-secondary:var(--card-text-secondary);--fg-muted:var(--card-text-secondary);border-radius:var(--radius-card);background:var(--card-surface-face);border:1px solid var(--card-edge);border-top-color:var(--card-edge-light);border-left-color:var(--card-edge-light);box-shadow:var(--card-surface-shadow)}

[hidden]{display:none!important}

/* A stable topic hue and six fixed difficulty tones share one tag material. */
.chip[data-tone=topic]{--tag-face:hsl(var(--tag-hue) var(--tag-topic-face-saturation) var(--tag-topic-face-lightness));--tag-ink:hsl(var(--tag-hue) var(--tag-topic-ink-saturation) var(--tag-topic-ink-lightness))}
.chip[data-tone]{color:var(--tag-ink);background:linear-gradient(165deg,color-mix(in srgb,var(--tag-face) 97%,#fff),var(--tag-face) 58%,color-mix(in srgb,var(--tag-face) 96%,#000));border:1px solid color-mix(in srgb,var(--tag-ink) 24%,var(--tag-face));box-shadow:inset 0 1px 0 var(--material-highlight),0 2px 4px #0b172612;font-weight:600}
