/* ======================================================================
   components.css —— 跨页复用组件层（P6 第一批，v2.51.0 自 style.css 拆出）
   已迁内容：弹窗（modal-overlay / modal / modal-actions）与操作反馈
   （ui-toast / ui-confirm，配套 ui.js 基础件）；
   第二批（v2.54.0）：按钮 .btn 家族 / 表格基件 table.data-table / 面板 .panel 家族。
   加载顺序：tokens.css → 本文件 → style.css（legacy / 未迁出样式）。
   迁移纪律（FRONTEND.md §2 / 方案 §8.3-2）：只做「整块搬迁 + 相对顺序不变」，
   每批搬完跑明暗 × 高频页视觉回归——视觉零变化是硬标准。
   新写跨页组件样式进本文件；单页特有样式不进这里（页内 <style> 或就近）。
   ====================================================================== */

.modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    display: none;
    align-items: center;
    justify-content: center;
    z-index: 200;
}
/* active 是 admin_kol.js 的历史开态类（与 show 等效；v2.49.0 前缺这条规则致大V弹窗打不开） */
.modal-overlay.show, .modal-overlay.active { display: flex; }
.modal {
    --modal-pad: 20px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--modal-pad);
    width: 90%;
    max-width: 460px;
    /* 高度兜底（2026-10-23 套餐管理「编辑弹窗无法关闭」事故）：套餐表单 9 组 +
       提示语自然高 ~981px，超过多数笔记本视口；overlay 是 fixed + align-items:center
       且不滚，溢出部分上下皆不可达（页面也不滚）⇒「取消/保存」被挤出视口，弹窗
       无法关闭。max-height 截断 + 内部滚动，保证操作键永远可达；dvh 兼顾移动端
       动态工具条，旧浏览器自动回落上一行 vh。 */
    max-height: calc(100vh - 48px);
    max-height: calc(100dvh - 48px);
    overflow-y: auto;
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
}
.modal h3 { margin-bottom: 16px; font-size: var(--font-size-lg); }
.modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    margin-top: 20px;
    /* 粘性操作栏：弹窗内部滚动时「取消/保存」随滚随在——滚到底才找得到取消键，
       用户感知就是「弹窗关不掉」。bottom 取 .modal 底内边距，吸附态与静止态
       （滚到底）位置重合、无跳变。背景色必须给：内容从栏底滑过时不能透视。
       未溢出（多数弹窗）时 sticky 完全不生效，视觉零变化。 */
    position: sticky;
    bottom: var(--modal-pad);
    background: var(--surface);
    /* 高于 .data-table th 的 5：弹窗内表格的粘性表头不能压住操作键
       （2026-10-23 促销弹窗实测：th 从粘性操作栏上方滑过时盖住「取消」）。 */
    z-index: 6;
}

/* ---------- ui.js 基础件（R1，v2.50.0）：通用 toast + 确认对话框 ---------- */
#ui-toast-container {
    position: fixed;
    right: 20px;
    bottom: calc(20px + env(safe-area-inset-bottom, 0px)); /* 与报警列同款底部避让 */
    z-index: 320;               /* 高于报警列(300)：用户操作反馈优先于后台报警 */
    display: flex;
    flex-direction: column;
    gap: 10px;
    max-width: 360px;
}
.ui-toast {
    pointer-events: auto;       /* admin 报警容器 pointer-events:none，子项要重开 */
    display: flex;
    align-items: flex-start;
    gap: 8px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 4px solid var(--info);
    border-radius: var(--radius);
    box-shadow: var(--shadow-md);
    padding: 10px 12px;
    font-size: 13px;
    line-height: 1.5;
    color: var(--text);
    opacity: 0;
    transform: translateX(16px);
    transition: opacity 0.25s ease, transform 0.25s ease;
}
.ui-toast.show { opacity: 1; transform: translateX(0); }
.ui-toast-success { border-left-color: var(--success); }
.ui-toast-error { border-left-color: var(--danger); }
.ui-toast-warning { border-left-color: var(--warning); }
.ui-toast-msg { word-break: break-word; }
.ui-toast-close {
    margin-left: auto;
    flex: none;
    border: none;
    background: none;
    color: var(--text-muted);
    cursor: pointer;
    font-size: 14px;
    line-height: 1;
    padding: 2px;
}
.ui-toast-close:hover { color: var(--text); }
.ui-confirm { max-width: 420px; }
.ui-confirm-msg {
    color: var(--text-secondary);
    font-size: 14px;
    line-height: 1.6;
    white-space: pre-line;      /* 确认文案支持 \n 换行（如重置密码的两行说明） */
}

/* ---------- P6 第二批（v2.54.0）：按钮 / 表格 / 面板基件，整块迁自 style.css ----------
   迁移前置检查（2026-10-04）：三块均为唯一基础定义；所有修饰类（.sl-card/.tc-main-card/
   .login-card .btn-primary 等）原本就位于 style.css 中这三块之后，搬走基础块后胜负关系不变。
   块间顺序保持原 style.css 源序：btn → data-table → panel。 */

/* 按钮 */
.btn {
    display: inline-block;
    padding: 7px 14px;
    border: 1px solid transparent;
    border-radius: var(--radius);
    font-size: var(--font-size-base);
    cursor: pointer;
    text-decoration: none;
    transition: background var(--transition-fast), border-color var(--transition-fast);
    font-family: inherit;
}
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary { background: var(--primary-solid); color: #fff; }
.btn-primary:hover:not(:disabled) { background: var(--primary-solid-hover); }
.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover:not(:disabled) { background: var(--danger-hover); }
.btn-ghost {
    background: transparent;
    border-color: var(--border);
    color: var(--text-secondary);
}
.btn-ghost:hover:not(:disabled) { border-color: var(--text-secondary); color: var(--text); }
/* ghost 按钮 active 态（「邀请海报」展开、admin 用户页 tab 选中）：与 .int-tab.active
   同语义——主色文字 + 主色浅底。触屏设备没有 hover，选中态必须有独立于 hover 的反馈。 */
.btn-ghost.active {
    background: var(--primary-bg);
    border-color: var(--primary);
    color: var(--primary-text-on-bg);
}
.btn-outline-primary {
    background: transparent;
    border-color: var(--primary);
    color: var(--primary);
}
.btn-outline-primary:hover:not(:disabled) { background: var(--primary-bg); }
/* 用户管理-操作列「充值」：主色浅填充，介于 ghost 与实心之间。
   不取实心主色：同列「启用」已是 .btn-primary 实心，两实心并排分不清主次；
   不取绿色：本站 badge 用 --down(绿) 表示禁用/跌，绿色在此语境歧义。
   hover 转实心主色 + 白字，给出明确点击反馈。 */
.btn-recharge {
    background: var(--primary-bg);
    border-color: var(--primary-border);
    color: var(--primary-text-on-bg);
    font-weight: 500;
}
.btn-recharge:hover:not(:disabled) {
    background: var(--primary-solid);
    border-color: var(--primary);
    color: #fff;
}
.btn-sm { padding: 4px 10px; font-size: var(--font-size-sm); }

/* 表格基件（页内变体如 .us-* 列样式仍在 style.css）*/
table.data-table {
    width: 100%;
    border-collapse: collapse;
    /* 表格正文 13px（2026-10-27）：专用 token，与 --font-size-sm(12px 标注级) 解耦。
       表头 .data-table th 不跟随——表头是标签不是数据，保持 12px 才能与
       13px 正文形成层级（两者同号时表头会抢读）。 */
    font-size: var(--font-size-table);
    /* 等宽数字（P1-3，UX-036）：全站数据表以数字列为主体，10s/60s 轮询整表
       swap 时数字宽度随内容变化（999→1000 类跳变）造成列宽/行高微抖。
       基类统一声明后，此前逐类手工加的 .col-* / .idx-pct 等声明成为无害冗余。
       中文字形不受影响（等宽只作用于数字/标点进位）。 */
    font-variant-numeric: tabular-nums;
}
.data-table th,
.data-table td {
    padding: 8px 12px;
    text-align: left;
    border-bottom: 1px solid var(--border-light);
}
/* 表头 sticky 浮在 navbar 下方（Excel 冻结首行），所有 .data-table 通用。
   ⚠️ 要求 table 到视口之间无 overflow!=visible 的祖先，否则该祖先成为 sticky
   滚动容器（即使垂直不滚动）、垂直 sticky 失效——宽表不要用 overflow-x:auto 包裹，
   水平滚动交给 body。各页 .panel 默认无 overflow，符合约束。 */
.data-table th {
    color: var(--text-secondary);
    font-weight: 500;
    /* 表头保持 12px（不跟表格正文 13px 走）：它是标签不是数据，
       比正文小一级才形成层级；同号时表头字重 500 会抢读。 */
    font-size: var(--font-size-xs);
    background: var(--surface-2);
    position: sticky;
    top: var(--navbar-height);   /* 避开 fixed navbar 遮挡 */
    z-index: 5;                  /* 压在 tbody 之上、navbar(100) 之下 */
}
/* 弹窗内的表格：modal 自身就是滚动容器（.modal overflow-y:auto）后，全局的
   top=navbar 高度会把表头永久下推 64px、压在数据行上——必须贴弹窗滚动容器顶。
   同型先行修正：#history-table / .tx-hist-table / .pts-tbl-scroll（皆为模板内联
   滚动容器）；本规则是它们的 modal 通款版。 */
.modal .data-table th { top: 0; }
/* 充值记录弹窗的表在 modal 内部滚动（max-height:60vh），表头贴住弹窗滚动容器顶部而非页面 navbar */
#history-table thead th { top: 0; }
/* 积分管理页·用户积分排行（v2.61.2）：表在 max-height 容器内独立纵向滚动。
   🔴 表头必须 top:0 —— 全局 .data-table th 的 sticky top=64px 是相对**视口**冻结设计的，
   本容器一旦成为滚动容器，64px 即变成相对容器顶边 ⇒ 表头被永久下推 64px，
   **压在第一条数据行上**（2026-10-06 用户报障「标题跑到数据下面」）。
   ⚠️ 这类「模板内联滚动容器」是 .panel/.card 媒体块不变式（test_narrow_panel_sticky）
   的盲区：那条守卫只看 CSS 层的 .panel/.card 是否同时归零表头偏移，管不到本页。
   同型先例：#history-table（modal）/ .tx-hist-table（admin_users）/ .pts-tbl-scroll。
   ⚠️ 容器加 overflow 就会隐式改变 overflow-y 计算值，务必与这一条成对改，不要只留其一。 */
.pts-tbl-scroll { max-height: 480px; overflow: auto; }
.pts-tbl-scroll .data-table th { top: 0; }
/* 充积分弹窗·积分变动历史表（v2.61.1）：同 history-table 的 sticky 修正 +
   列宽控制。展示问题根因（用户报障）：① .data-table th 的 sticky top=navbar 高度
   在 modal 滚动容器内粘错位置；② table-layout 默认 auto，备注列长文本把时间列
   挤变形。修法：表头贴容器顶 + 固定关键列宽 + 备注列限宽换行。 */
.tx-hist-table thead th { top: 0; }
.tx-hist-table { table-layout: fixed; }
.tx-hist-table th:nth-child(1), .tx-hist-table td.tx-time { width: 88px; white-space: nowrap; }
.tx-hist-table th:nth-child(2), .tx-hist-table td.tx-src  { width: 110px; }
.tx-hist-table th:nth-child(3), .tx-hist-table th:nth-child(4) { width: 64px; }
.tx-hist-table td.tx-note {
    white-space: normal;
    word-break: break-all;
    color: var(--text-muted);
}
/* 表头副标题：单元格内含两个数字（今日值 + 环比）时，用小字标明各自含义，
   避免用户猜第二个数是什么。例：主力亿 / 今日·较昨 */
.data-table th .th-sub { display: block; font-size: var(--font-size-xs); color: var(--text-muted); font-weight: 400; }
.data-table tbody tr:hover { background: var(--surface-2); }
.data-table .actions { display: flex; gap: 6px; }

/* ---------- 表内筛选（2026-10-29）：财报表「代码/名称/拼音首字母」即时过滤 ----------
   为什么放在表格上方而不是面板标题行：标题行右侧已被数据源说明（长文本）占满，
   塞进去 1440 下必换行；单独一行只多 30px，且筛选框与它作用的表格紧邻，指向关系明确。
   `.fin-q-hide` 用 !important 而不是依赖 [hidden]：tr 上任何 display 声明都会压过
   UA 的 [hidden]{display:none}（同 2026-10-21 「[hidden] 被 class display 覆盖」教训）。
   空态独占一行（flex-basis:100%）——它是一句完整的话，不该和输入框挤在一行。 */
.fin-qrow { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 8px; }
.fin-qrow input[type="search"] {
    flex: 0 1 280px;
    min-width: 168px;
    padding: 5px 10px;
    font-size: var(--font-size-sm);
    color: var(--text);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}
.fin-qrow input[type="search"]:focus { outline: none; border-color: var(--primary); }
.fin-qrow input[type="search"]::placeholder { color: var(--text-muted); }
.fin-qn {
    font-size: var(--font-size-xs);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}
.fin-qn.hit { color: var(--primary); font-weight: 600; }
.fin-qempty { flex: 0 0 100%; margin: 2px 0 0; font-size: var(--font-size-sm); color: var(--warning); }
.fin-q-hide { display: none !important; }

/* ======================================================================
   管理页表单基件（v2.61.2）
   🔴 此前 `.form-group` / `.form-hint` **被 4 个管理页模板大量使用但 CSS 里从未定义**
   —— 后果是「表单说明文字」以 16px 正文黑色渲染，比它要说明的输入框还抢眼
   （积分规则卡的 referral.grant_points_per_week 一行就是这么扎眼的）。
   裸 `<input>` 也没有任何兜底（`.auth-input` 只服务登录页），全靠浏览器默认边框，
   跟卡片语言对不上。这两条一并补齐：管理页表单从此有基件，不再各页现拼内联样式。
   ====================================================================== */
.form-group { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.form-group > label {
    font-size: var(--font-size-sm);
    font-weight: 500;
    color: var(--text-secondary);
}
/* 说明/提示文字：必须**弱于**它所说明的控件（12px + muted + 等宽用于配置键）。
   配置键类名（referral.*）额外走 code 形态，避免与正文混淆。 */
.form-hint {
    font-size: 12px;
    line-height: 1.55;
    color: var(--text-muted);
}
.form-hint code,
code.form-hint {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 11.5px;
    background: var(--surface-2);
    border: 1px solid var(--border-light);
    border-radius: 4px;
    padding: 1px 5px;
    color: var(--text-secondary);
}
/* 管理页裸输入框基件（number/text/search/date 同源）：与 .btn / .card 同语言
   ⚠️ 边框必须用 --border-strong（≈ text-muted），**不能用 --border/--border-light**：
   后者在白底上对比度≈1.1:1，边框「存在但看不见」⇒ 控件像没有边界，
   这正是本页此前的观感问题（浏览器默认边框同样过浅）。--border-strong 随主题
   自动切换（tokens.css 亮/暗两套），故不硬编码色值。

   🔴🔴 两条形态硬约束（2026-10-07 事故修正，均有守卫测试 test_form_control_base.py）：
   ① 选择器必须整体用 :where() 包裹（特异性归零）。
      本块是「全站最后兜底」，兜底的语义 = **只在没有任何其他样式指定时生效**。
      不加 :where() 时 `input[type="text"]` 特异性 (0,1,1)，会**压过一切页面单类样式**
      (0,1,0)——上线当天（00ac1c7）即造成全站回归：产业链卡点/主线/竞价等页面
      输入框字号被篡改（14px→12px）、圆角被改小（8px→2px）、下拉框在 flex 工具条里
      被 width:100% 撑满整行（「输入框/下拉框/按钮」三件套从同行变竖排）。
  ② 不许有 width: 100%。
      100% 是「纵向表单」的场景假设，不是通用兜底；横向 flex 工具条里它会被当作
      flex-basis 撑爆整行。需要整宽的场景各自负责（.form-group / .filter-bar /
      .cs-form-select 均已自备），本块只管「裸控件别裸着」。 */
:where(input[type="number"],
       input[type="text"],
       input[type="search"],
       input[type="date"],
       select,
       textarea) {
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm, 6px);
    color: var(--text);
    font-size: var(--font-size-sm);
    font-family: inherit;
    padding: 7px 10px;
    transition: border-color .15s, box-shadow .15s;
}
input[type="number"] { font-variant-numeric: tabular-nums; }
input:focus,
select:focus,
textarea:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px var(--primary-bg);
}
input:disabled,
select:disabled,
textarea:disabled {
    background: var(--surface-2);
    color: var(--text-muted);
    cursor: not-allowed;
}

/* 面板 */
.panel {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 14px 16px;
    margin-bottom: 12px;
}
.panel-title {
    font-size: var(--font-size-base);
    font-weight: 600;
    margin: 0 0 10px;
    color: var(--text);
}
/* 卡片右上角注释（ⓘ 悬停说明）：右浮 + tooltip 右缘锚定（kpi-info 默认左缘锚定，
   右缘元素会溢出视口），通用命名（主线识别页等） */
/* 特异性带 .kpi-info：所有 panel-note 实际是 class="kpi-info panel-note"（全库成对，
   2026-10-04 核对），margin-left 与 style.css 的 .kpi-info 同特性对赌——本文件先加载，
   平手输源序会让 8px 打回 4px（迁移实测全站 ⓘ 偏移翻转约 29k 像素） */
.kpi-info.panel-note { float: right; margin-left: 8px; }
.kpi-info.panel-note::after { left: auto; right: 0; }
.panel-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 10px;
}
.panel-head .panel-title { margin: 0; }
/* 国际股市/美股：板块面板的数据来源说明——复用全站次级文字基调（--text-secondary / sm）；
   2026-09-30 起美股行业/概念合并为一块双 tab，该行从「标题右侧」下移到各自 tab 内容顶部
   （两条数据面各标各的新鲜度，见 .us-sector-asof）。 */
.panel-source {
    font-size: var(--font-size-sm);
    color: var(--text-secondary);
    white-space: nowrap;
}

/* ---------- P6 第三批（v2.55.0）：表单基件 / 提示信息 / live-bar 四态 ----------
   迁移前置检查（2026-10-04）：后代平手（.form-group input vs style.css 的
   .ssp-search-row input 等）与同特性对赌（tests/test_css_cascade_lint.py）
   均已核过——style.css 侧相关规则全部位于本批原位置之后，胜负关系不变。
   块间顺序保持原 style.css 源序：form-group → alert → live-bar。 */

/* 表单 */
.form-group { margin-bottom: 14px; }
.form-group label {
    display: block;
    margin-bottom: 6px;
    color: var(--text-secondary);
    font-size: var(--font-size-sm);
}
/* ⚠️ border 必须 --border-strong（2026-10-07 修正）：
   本规则与上面的裸控件基件**同特异性**（均 (0,1,1)）且位置更后 ⇒ 本规则赢。
   此前这里写 var(--border)（浅色）会把基件的深边框**悄悄抵消**——管理页弹窗里的
   输入框全部落在 .form-group 内，等于收口的「边框可见」在真实场景里从未生效
   （实测 rgb(232,234,237)：getComputedStyle 非空、肉眼看不见）。 */
.form-group input,
.form-group select,
.form-group textarea {
    width: 100%;
    padding: 8px 12px;
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    color: var(--text);
    font-size: var(--font-size-base);
    font-family: inherit;
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px var(--primary-bg);
}
.form-group input:disabled { opacity: 0.5; cursor: not-allowed; }

/* 提示信息 */
.alert {
    padding: 10px 14px;
    border-radius: var(--radius);
    margin-bottom: 12px;
    font-size: var(--font-size-sm);
    border: 1px solid;
}
.alert-error { background: var(--up-bg); border-color: var(--danger); color: var(--danger); }
.alert-success { background: var(--down-bg); border-color: var(--success); color: var(--success); }
.alert-warning { background: rgba(245, 158, 11, 0.1); border-color: var(--warning); color: var(--warning); }
.alert-info { background: var(--primary-bg); border-color: var(--primary); color: var(--primary); }

/* live-bar / demo-bar（含 @keyframes pulse——style.css 的 .dt-badge.dt-live::before
   仍引用此动画名，keyframes 跨文件全局解析不受影响） */
.live-bar,
.demo-bar {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: var(--font-size-xs);
    padding: 3px 8px;
    border-radius: var(--radius-pill);
}
.live-bar { color: var(--up); background: var(--up-bg); }
.live-bar::before {
    content: '';
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--up);
    animation: pulse 1.6s infinite;
}
@keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.3; }
}
.demo-bar { color: var(--warning); background: rgba(245, 158, 11, 0.1); }

/* ---- live-bar 四态（P1 状态可见，freshness.js 驱动；docs/FRONTEND.md §3） ----
   stale：数值照常显示，仅条本身变琥珀 + 停脉动（I2 取舍：数据慢 30s 远好过空白）；
   error：可点击重试（freshness.js 捕获阶段接管）；刷新成功后 JS 摘类自动回正常。 */
.live-bar.is-stale {
    color: var(--warning);
    background: rgba(var(--warning-rgb), 0.12);
    cursor: help;
}
.live-bar.is-stale::before { animation: none; opacity: 0.45; }
.live-bar.is-error {
    color: var(--danger);
    background: rgba(var(--danger-rgb), 0.12);
    cursor: pointer;  /* 点击重试（freshness.js 读 #market-body hx-get 原样重发） */
    animation: pulse 1.1s infinite;  /* 失败比陈旧更醒目，闪得更快 */
}
.live-bar.is-error::before { background: var(--danger); }
/* 轮询飞行中：容器轻降透明度 + 禁指针（数值保留，绝不骨架空白——盘中盯着看的数字
   突然消失比慢半拍有害得多）。0.6s 过渡避免整页闪烁感。 */
.is-refreshing {
    opacity: 0.75;
    transition: opacity 0.6s ease;
    pointer-events: none;  /* swap 瞬间防误点（替换后自动恢复） */
}
@media (prefers-reduced-motion: reduce) {
    .live-bar.is-error { animation: none; }
    .is-refreshing { transition: none; }
}

/* ---------- P6 收尾批（v2.56.0）：报告操作按钮 / 空态提示行 ---------- */
/* 报告操作按钮（新窗口 / 下载 HTML / 导出图片）：全站共用一条规则。
   产业链卡点页与个股逻辑页同源——两处曾各写一份（.sc-btn 页内 scoped），
   改样式必漏一处；统一到 .rpt-btn 后只此一处。 */
.rpt-btn {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 5px 12px; font-size: 12px; line-height: 1.6;
    font-family: inherit; white-space: nowrap; cursor: pointer;
    text-decoration: none; border-radius: 6px;
    border: 1px solid var(--border); background: var(--surface);
    color: var(--text-secondary);
    transition: background var(--transition-fast), color var(--transition-fast);
}
.rpt-btn:hover:not(:disabled) { background: var(--surface-hover); color: var(--text); }
.rpt-btn:disabled { opacity: 0.6; cursor: not-allowed; }
/* 危险变体：报告 / 研究记录的删除入口（仅管理员可见）。
   红字 + hover 淡红底，与普通「新窗口 / 下载」区分开，避免手滑点错。 */
.rpt-btn--danger { color: var(--danger); border-color: var(--mix-danger-40-border); }
.rpt-btn--danger:hover:not(:disabled) {
    background: rgba(var(--danger-rgb), .14);
    color: var(--danger); border-color: var(--danger);
}

.empty-hint { color: var(--text-muted); text-align: center; padding: 20px; font-size: var(--font-size-sm); }
.loading-hint { color: var(--text-muted); padding: 8px 0; font-size: var(--font-size-sm); }

/* ---------- P6 顺手批（v2.57.0）：首屏骨架组件（与 market/_skeleton.html 宏成对） ---------- */
/* ---------- 首屏骨架（R2，v2.50.0）：懒加载面板首空态占位（宏 market/_skeleton.html）----------
   红线：骨架只出现在「从未有过数据」的首空态；刷新态一律 freshness.js 的
   「保留数值 + 降透明度」，两者不得混用（FRONTEND.md §3 / 方案 §8.3-1）。 */
.sk-stack { display: flex; flex-direction: column; gap: 14px; padding: 18px 4px; }
/* 🔴 溢出防线（2026-10-27 修）：骨架条宽度是百分比，一行多段时**百分比之和 + gap
   很容易超过容器宽**（旧版 table() 宏是 45+30+55+20=150%）。旧规则 `flex:none`
   禁止收缩 ⇒ 溢出父容器 ⇒ 整页出现横向滚动条（实测 5 页 scrollWidth 2052 vs 视口 1440）。
   现在三管齐下：① 宏的百分比之和收到 ≤90%；② 允许收缩 `0 1 auto`（超了压回容器）；
   ③ overflow:hidden 兜底（无论怎么组合都不外溢）。
   ⚠️ 不要改回 `flex:none`。窄屏 gap 由 16px 收到 8px，因为 3 个 gap=48px 在 342px 宽的
   移动端内容区里占 14%，会把本已收缩的条压得过短。 */
.sk-line { display: flex; gap: 16px; overflow: hidden; }
.sk-line .sk-bar { flex: 0 1 auto; }
.sk-bar {
    display: block;
    height: 12px;
    border-radius: var(--radius-sm);
    background: var(--surface-hover);
    animation: sk-pulse 1.4s ease-in-out infinite;
}
/* 宽度工具类：**全库仅 market/_skeleton.html 宏使用**（三个骨架宏的组合宽度全部 ≤90%）。
   v2.56.0 曾误删 .w-20（grep 正则吞字），2026-10-27 又发现 card_list() 引用的
   .w-25/.w-15 从未定义 ⇒ 骨架条 width 落空、span 无内容 ⇒ 宽度 0、完全不可见。
   新增宽度一律加在这里，并确认对应宏的百分比之和不超过 90%。 */
.w-90 { width: 90%; } .w-85 { width: 85%; } .w-80 { width: 80%; } .w-75 { width: 75%; }
.w-60 { width: 60%; } .w-55 { width: 55%; } .w-45 { width: 45%; }
.w-35 { width: 35%; } .w-30 { width: 30%; }
.w-25 { width: 25%; } .w-20 { width: 20%; } .w-15 { width: 15%; }
.sk-chart {
    height: 240px;
    border-radius: var(--radius);
    background: var(--surface-hover);
    animation: sk-pulse 1.4s ease-in-out infinite;
}
@keyframes sk-pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 0.85; } }
@media (prefers-reduced-motion: reduce) {
    .sk-bar, .sk-chart { animation: none; opacity: 0.6; }
}
/* 窄屏 gap 收窄（2026-10-27）：断点沿用项目规范 768（FRONTEND.md §1，不发明新数字）。
   342px 内容区里 3 个 16px gap 占 14%，会把收缩后的骨架条压得过短。 */
@media (max-width: 768px) {
    .sk-line { gap: 8px; }
}

/* ======================================================================
   市场页一级 tab 范式 .mkt-tabs/.mkt-tab（2026-10-28，自 _lhb.html scoped 样式抽出）
   下划线式：透明底 + 底部 2px 分隔线，active = 红色下划线 + 加粗。
   谁在用：龙虎榜（lhb-tab 钩子类并轨）、财报分析、异动推演。
   层级语言（_lhb.html 2026-10-24 用户拍板）：主 tab = 下划线，段 tab = 胶囊。
   ⚠️ 行为不绑类名：JS 一律走 data-* 属性（dashboard.js 委托），
   lhb 的 lhb_anchor.js 仍用 .lhb-tab 类做选择器 + 自守卫，页面需双类名 mkt-tab lhb-tab。
   ====================================================================== */
.mkt-tabs {
    display: flex; gap: 4px;
    border-bottom: 1px solid var(--border);
    margin: 4px 0 14px;
    overflow-x: auto; scrollbar-width: none;  /* 375px 多 tab 横滑不换行（换行会断 border-bottom，见 _lhb.html） */
}
.mkt-tabs::-webkit-scrollbar { display: none; }
.mkt-tab {
    appearance: none; background: none; border: none;
    border-bottom: 2px solid transparent;
    padding: 8px 14px; margin-bottom: -1px;
    font-size: var(--font-size-base); color: var(--text-muted);
    cursor: pointer; white-space: nowrap; flex-shrink: 0;
}
.mkt-tab:hover { color: var(--text-secondary); }
.mkt-tab.active {
    color: var(--accent);
    border-bottom-color: var(--accent);
    font-weight: 600;
}

/* ======================================================================
   数值列工具类（v2.61.0，admin 运营页表格）
   用法：th.num / td.num —— 数字列右对齐 + 等宽数字，多行数字个位对齐。
   ⚠️ 与顶部导航 .top-tab .num（角标计数）无冲突：那里是 .num 作为子元素
   选择器且只配在 .top-tab 作用域内，此处是独立工具类。
   ====================================================================== */
.data-table th.num,
.data-table td.num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* ======================================================================
   积分管理页 · 卡片化样式收口（v2.61.2）
   目标：两张卡（积分规则 / 用户积分）从「裸 div 堆叠」收进卡片语言——
   层级分明、控件有归属、说明不喧宾夺主。
   ====================================================================== */

/* ---- 积分规则卡：三项参数做成「参数格」 ----
   原为 grid + 裸 form-group，输入框直接贴卡片边、说明文字与正文同权。
   改为带边框的参数格：视觉上从卡片里浮起，读起来是「一组参数」而非「三个孤立输入框」。 */
.pts-cfg-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
    gap: 12px;
    margin-top: 12px;
}
.pts-cfg-item {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 12px 14px;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm, 6px);
    min-width: 0;
}
.pts-cfg-item > label {
    font-size: var(--font-size-sm);
    font-weight: 600;
    color: var(--text-secondary);
    display: flex;
    align-items: center;
    gap: 6px;
}
/* 参数格内的说明：贴底、与输入框同左缘，读作「这个输入框的注解」 */
.pts-cfg-item .form-hint { margin-top: auto; padding-top: 2px; }
/* 长配置键（referral.admin_max_grant_points）在窄格内必须换行而非撑破格子 */
.pts-cfg-item .form-hint code { word-break: break-all; }
/* 标签右侧的限定词（如「公式基准」）：弱化为小徽标，不与标签主文争权重 */
.pts-cfg-tag {
    font-size: var(--font-size-xs);
    font-weight: 400;
    color: var(--text-muted);
    background: var(--surface);
    border: 1px solid var(--border-light);
    border-radius: 3px;
    padding: 0 5px;
    line-height: 1.5;
}

/* 卡片头部副说明（标题右侧的一句话解释）：替代原先压在正文流里的大段灰字 */
.pts-card-sub {
    font-size: 12px;
    color: var(--text-muted);
    margin: -4px 0 12px;
    line-height: 1.6;
}

/* ---- 用户积分卡：表格与卡头之间加分隔 + 表头去重 ----
   表格有 max-height 内滚容器，卡内再加一层分隔线，避免
   「标题 / 说明 / 表头」三者糊成一片。 */
.pts-tbl-card .pts-tbl-scroll {
    border: 1px solid var(--border-light);
    border-radius: var(--radius-sm, 6px);
    overflow: auto;
}
.pts-tbl-card .pts-tbl-scroll .data-table { border: 0; }
/* 表头在滚动容器内已有背景，额外去掉重复的首行描边 */
.pts-tbl-card .pts-tbl-scroll .data-table th { border-bottom: 1px solid var(--border); }
.pts-tbl-card .pts-tbl-scroll .data-table tbody tr:last-child td { border-bottom: 0; }
/* 行高亮：排行表行距偏紧，加极淡斑马纹提升横向扫读（数字列密集） */
.pts-tbl-card .pts-tbl-scroll .data-table tbody tr:nth-child(even) {
    background: var(--surface-2);
}
.pts-tbl-card .pts-tbl-scroll .data-table tbody tr:hover {
    background: var(--primary-bg);
}

/* 行内徽标化：模拟分 / 待结算 的弱化语义（0 值不占视觉重量） */
.pts-val-zero { color: var(--text-muted); }
.pts-val-demo { color: var(--text-muted); }
.pts-val-pend { color: var(--warning); font-weight: 500; }
.pts-row-inactive { opacity: .55; }

/* ---- 升级引导页（分层 M2）：免费层访问 💰 tab 的引导卡片 ----
   个人化页面不进 render_cache 共享体（ADR-0038/0039），样式同样只用主题变量。 */
.upgrade-gate { display: flex; justify-content: center; padding: 48px 16px; }
.upgrade-gate-card {
  max-width: 560px; width: 100%; text-align: center;
  background: var(--bg-elevated, var(--bg));
  border: 1px solid var(--border-strong);
  border-radius: 12px; padding: 36px 28px;
}
.upgrade-gate-badge {
  display: inline-block; font-size: 12px; font-weight: 600;
  color: var(--warning); border: 1px solid var(--warning);
  border-radius: 999px; padding: 2px 12px; margin-bottom: 14px;
}
.upgrade-gate-title { margin: 0 0 8px; font-size: 22px; }
.upgrade-gate-sub { margin: 0 0 20px; color: var(--text-muted); font-size: 14px; line-height: 1.7; }
.upgrade-gate-stats { display: flex; justify-content: center; gap: 28px; margin: 20px 0 8px; }
.upgrade-gate-stat { display: flex; flex-direction: column; gap: 4px; }
.upgrade-gate-stat-num { font-size: 26px; font-weight: 700; color: var(--primary-solid); }
.upgrade-gate-stat-label { font-size: 12px; color: var(--text-muted); }
.upgrade-gate-hint { font-size: 12px; color: var(--text-muted); margin: 6px 0 22px; }
.upgrade-gate-actions { display: flex; justify-content: center; gap: 12px; flex-wrap: wrap; }

/* M2 付费墙：membership 页来源提示 + 侧边栏 🔒 角标 */
.membership-from-note {
  margin: 0 0 16px; padding: 10px 14px; border-radius: 8px;
  background: var(--bg-elevated, var(--bg)); border: 1px solid var(--border-strong);
  font-size: 13px; color: var(--text-muted);
}
.membership-from-note strong { color: var(--text); }
.qt-sidebar-lock { font-size: var(--font-size-xs); margin-left: 2px; opacity: .75; }

/* ---- 表格筛选条（管理页通用，首用于 admin_users 用户管理）----
   为什么要有这套类：此前筛选区是 5 段重复内联 style，带来三个真实缺陷——
     ① 边框写的是 --border（白底对比度≈1.1:1）⇒ 「有边框但看不见」，
        用户看不出哪里是可点的输入框/下拉（与 .form-group 那次同源）；
     ② 全局基件 select{width:100%} 与 flex 容器对赌 ⇒ 每个下拉被拉伸成随缘宽度、
        换行时序也不可控；
     ③ 控件只有 aria-label，视觉上「全部套餐 / 全部到期 / 全部登录」并排，
        语义全靠 placeholder 猜。
   修法：容器统一几何 + 控件上方可见小标签 + 边框走 --border-strong（随主题切换）。
   控件高度统一 34px：label 在上、控件在下，底边不齐会整排看着歪。 */
.filter-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 10px 12px;
    margin-bottom: 12px;
}
.filter-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}
/* 搜索框独占弹性余量（其余下拉按内容宽度排布） */
.filter-field--grow { flex: 1 1 200px; max-width: 300px; }
.filter-label {
    font-size: 11.5px;
    line-height: 1.2;
    font-weight: 500;
    color: var(--text-muted);
    padding-left: 2px;
}
.filter-bar input[type="text"],
.filter-bar input[type="search"],
.filter-bar select {
    width: 100%;
    height: 34px;
    padding: 6px 10px;
    background: var(--surface-2);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm, 6px);
    color: var(--text);
    font-size: var(--font-size-sm);
}
/* 下拉按内容宽度（基件的全站 width:100% 已于 v2.66.0 移除——见上方基件注释；
   此处 width:auto 作为双保险保留），给个下限避免中文选项挤成一团 */
.filter-bar select { width: auto; min-width: 104px; }
/* 部分浏览器（Win/Chrome）的下拉面板沿用系统底色，暗色主题下会白底白字 */
.filter-bar select option { background: var(--surface); color: var(--text); }
/* 搜索框内清空按钮：原生 type=search 的 × 在暗色下几乎不可见，故隐藏后自绘 */
.filter-input { position: relative; display: flex; align-items: center; }
.filter-input input[type="search"] { padding-right: 30px; }
.filter-input input[type="search"]::-webkit-search-cancel-button,
.filter-input input[type="search"]::-webkit-search-decoration {
    -webkit-appearance: none;
    appearance: none;
}
.filter-clear {
    position: absolute;
    right: 4px;
    display: none;
    width: 22px;
    height: 22px;
    padding: 0;
    line-height: 1;
    background: none;
    border: 0;
    border-radius: 50%;
    color: var(--text-muted);
    font-size: 15px;
    cursor: pointer;
}
.filter-clear:hover { background: var(--surface); color: var(--text); }
.filter-input.has-value .filter-clear { display: block; }
/* 状态区：已生效条件 chips（可单条移除）+ 结果数 + 清除。
   独占一行（flex-basis:100%）：5 个控件之后剩余宽度本就放不下 chips，靠 margin-left:auto
   硬挤的结果是「第二行右对齐、左缘留白 700px」，与上方 label 的左缘栅格对不齐。
   故：chips 左对齐、计数与清除用 margin-left:auto 顶到右端；无筛选时只有最右的计数。 */
.filter-status {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    flex-basis: 100%;
    margin-top: 2px;
    font-size: 12px;
    color: var(--text-secondary);
}
.filter-count { margin-left: auto; }
.filter-count strong {
    color: var(--text);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.filter-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 4px 2px 9px;
    border: 1px solid var(--border-strong);
    border-radius: 999px;
    background: var(--surface-2);
    color: var(--text-secondary);
    font-size: 11.5px;
    max-width: 240px;
}
.filter-chip span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.filter-chip button {
    flex: none;
    width: 16px;
    height: 16px;
    padding: 0;
    line-height: 1;
    background: none;
    border: 0;
    border-radius: 50%;
    color: var(--text-muted);
    font-size: 13px;
    cursor: pointer;
}
.filter-chip button:hover { color: var(--danger); }
