/* ===========================================================
   stock-assistant 设计系统
   此前全站只有 7 个颜色变量，间距/字号/圆角全是魔法数字，
   且无暗色模式、无平板断点、无打印样式。这里统一成 token + 组件。
   =========================================================== */

/* ---------- CSS Cascade Layers（Round20 Step2.3，借鉴 Open Props 的 @layer 分层） ----------
   两份样式表此前靠"加载顺序 + !important"互相打架（.card/.btn/.nav 各有两套定义）。
   引入原生 @layer 后，**层内顺序由声明决定、与文件加载顺序无关**：
     tokens（设计令牌）< base（重置）< layout（骨架）< components（组件）< cards（卡片）< utilities（工具类）
   未进层的规则**永远优先于**任何层 —— 所以本轮只把**令牌**放进 layers（零视觉变化），
   组件层要合并时逐块搬（每搬一块都跑一遍页面渲染测试），不做一次性大改。 */
@layer tokens, base, layout, components, cards, utilities;

@layer tokens {
:root {
  /* 颜色 —— Material 3 / Google 灰阶。**中性色一律不带色相**，这是本次重做的核心。
     上一版把整条中性色阶染成紫调（bg/txt/line 全带紫）+ body 加紫光晕 + 阴影带紫，
     三者叠加的观感就是"像蒙了一层滤镜" —— 用户的原话"眼花、不真实"，不是错觉。
     Google 的解法相反：中性灰打底，**颜色只出现在"操作"上**（按钮/链接/选中）。
     改动时必须同步三处：本块 + `[data-theme="dark"]` + prefers-color-scheme 媒体查询。 */
  --bg: #f8f9fa; /* Google gray 50 */
  --surface: #ffffff;
  --surface-2: #f1f3f4; /* gray 100 */
  --surface-3: #e8eaed; /* gray 200 */
  --txt: #202124; /* gray 900 */
  --txt-2: #5f6368; /* gray 700 */
  --muted: #80868b; /* gray 600 —— 旧的 #8f8aa8 是紫灰且对比不足，直接导致眼疲劳 */
  --line: #dadce0; /* gray 300 —— 描边回来，卡片重新有"落点" */
  --line-2: #e8eaed; /* gray 200 */
  /* A股习惯：红涨绿跌。取 Google 自己的红/绿，与整体系同源 */
  --up: #d93025;
  --up-bg: #fce8e6;
  --down: #188038;
  --down-bg: #e6f4ea;
  --flat: #80868b;
  --brand: #1a73e8; /* Google Blue 600 */
  --brand-2: #1765cc; /* Blue 700 */
  --brand-bg: #e8f0fe; /* Blue 50 */
  --warn: #b06000;
  --warn-bg: #fef7e0;
  --danger: #d93025;
  --danger-bg: #fce8e6;
  /* 状态色：与涨跌色**严格分离**。A股语境里红=涨、绿=跌，所以"正常/成功"绝不能用绿
     （会被读成"跌"）、异常也不能用红（会被读成"涨"）——看板此前把"任务全绿"渲染成
     --up（红底）、把"异常任务"渲染成 --down（绿底），语义正好相反。
     约定：正常/成功=蓝(--ok)、注意=琥珀(--warn)、危险=红(--danger，仅文字/细边，
     不用大色块)。下面用 var() 引用而非硬编码，暗色主题自动跟随。 */
  --ok: var(--brand);
  --ok-bg: var(--brand-bg);
  /* 导航 = Google 的**浅色侧栏**（Gmail/Drive/Cloud Console 都是浅灰栏 + 浅色内容）。
     改浅色的前提是 app.css 里 7 处硬编码浅色文字已全部换成 token —— 否则会白底白字。 */
  --nav-bg: #f8f9fa;
  --nav-bg-2: #f8f9fa;
  --nav-txt: #3c4043;

  /* 间距刻度 */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;

  /* 字号 */
  --fs-xs: 12px;
  --fs-sm: 13px;
  --fs-md: 14px;
  --fs-lg: 17px;
  --fs-xl: 22px;
  --fs-2xl: 28px;

  /* 形状 */
  --radius: 10px;
  --radius-sm: 6px;
  --radius-pill: 999px;
  /* 阴影 —— Material 标准两层灰影（**不含色相**）。
     上一版用 rgba(35,26,82,…) 带紫调，卡片像浮在染色空气里 —— 这也是"不真实"的一部分。
     Google 的 elevation 用的就是纯灰 rgba(60,64,67,…)。 */
  --shadow: 0 1px 2px rgba(60, 64, 67, .30), 0 1px 3px 1px rgba(60, 64, 67, .15);
  --shadow-md: 0 1px 3px rgba(60, 64, 67, .30), 0 4px 8px 3px rgba(60, 64, 67, .15);
  --shadow-lg: 0 2px 6px rgba(60, 64, 67, .30), 0 8px 24px 4px rgba(60, 64, 67, .15);

  --nav-h: 50px;
  --maxw: 1240px;
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
    "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
}}


/* 暗色调色板。
   两份声明是 CSS 的固有限制，不是疏忽：纯 CSS 无法让"媒体查询内的规则"
   与"属性选择器的规则"共享同一个声明块（没有 mixin/变量间接层）。
   改动调色板时**必须同步改两处**，否则会出现"系统暗色与手动暗色不一致"。
   （若将来引入构建步骤，这里应抽成 @mixin。） */
@layer tokens {
:root[data-theme="dark"] {
  /* Google 官方暗色主题配色（gray 900 底 + Blue 200 主色），同样零色相染色 */
  --bg: #202124;
  --surface: #292a2d;
  --surface-2: #303134;
  --surface-3: #3c4043;
  --txt: #e8eaed;
  --txt-2: #bdc1c6;
  --muted: #9aa0a6;
  --line: #3c4043;
  --line-2: #303134;
  --up: #f28b82;
  --up-bg: #3c1f1e;
  --down: #81c995;
  --down-bg: #1e3226;
  --brand: #8ab4f8;
  --brand-2: #aecbfa;
  --brand-bg: #1e3050;
  --warn: #fdd663;
  --warn-bg: #3c3520;
  --danger: #f28b82;
  --danger-bg: #3c1f1e;
  --nav-bg: #17181b;
  --nav-bg-2: #202124;
  --nav-txt: #e8eaed;
  --shadow: 0 1px 2px rgba(0, 0, 0, .45), 0 1px 3px 1px rgba(0, 0, 0, .3);
  --shadow-md: 0 1px 3px rgba(0, 0, 0, .45), 0 4px 8px 3px rgba(0, 0, 0, .3);
  --shadow-lg: 0 2px 6px rgba(0, 0, 0, .45), 0 8px 24px 4px rgba(0, 0, 0, .3);
}}


@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #202124;
    --surface: #292a2d;
    --surface-2: #303134;
    --surface-3: #3c4043;
    --txt: #e8eaed;
    --txt-2: #bdc1c6;
    --muted: #9aa0a6;
    --line: #3c4043;
    --line-2: #303134;
    --up: #f28b82;
    --up-bg: #3c1f1e;
    --down: #81c995;
    --down-bg: #1e3226;
    --brand: #8ab4f8;
    --brand-2: #aecbfa;
    --brand-bg: #1e3050;
    --warn: #fdd663;
    --warn-bg: #3c3520;
    --danger: #f28b82;
    --danger-bg: #3c1f1e;
    --nav-bg: #17181b;
    --nav-bg-2: #202124;
    --nav-txt: #e8eaed;
    --shadow: 0 1px 2px rgba(0, 0, 0, .45), 0 1px 3px 1px rgba(0, 0, 0, .3);
    --shadow-md: 0 1px 3px rgba(0, 0, 0, .45), 0 4px 8px 3px rgba(0, 0, 0, .3);
    --shadow-lg: 0 2px 6px rgba(0, 0, 0, .45), 0 8px 24px 4px rgba(0, 0, 0, .3);
  }
}

* { box-sizing: border-box; }

html, body { margin: 0; padding: 0; }

body {
  background: var(--bg);
  color: var(--txt);
  font: var(--fs-md)/1.6 var(--font);
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;
}

a { color: var(--brand); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ---------- 导航 ---------- */
.nav {
  position: sticky;
  top: 0;
  z-index: 60;
  /* 不要渐变：Google 的导航是**平的**（浅灰栏没有纵向明暗变化）。 */
  background: var(--nav-bg);
  color: var(--nav-txt);
  box-shadow: var(--shadow-md);
}
/* 导航一行 = 链接区（可横滑）+ 功能区（固定，不参与滚动）。
   必须拆开的原因：手机上链接总宽约 650px，390px 的屏放不下，此前把 主题/登出 也放在
   同一条滚动区里，用户滑不到（滑到的是整页外层，这条内部的横向滚动压根没被触发），
   于是"推送"之后的内容永远够不到。功能区别放滚动区之外 → 始终可见。
   链接区要真的能滑，必须 flex:1 + min-width:0（否则 flex 项按内容撑开，父级不产生溢出）。 */
.nav-wrap {
  max-width: var(--maxw);
  margin: 0 auto;
  height: var(--nav-h);
  padding: 0 var(--sp-3);
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}
.nav-in {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  overflow-x: auto;
  overscroll-behavior-x: contain; /* 滑到头不要把滚动传给整页（否则又变成"滑外面的"） */
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.nav-in::-webkit-scrollbar { display: none; }
/* 右缘渐隐：提示"右边还有内容"。用 mask 而不是贴背景色块——前者只淡化边缘、
   不会盖住文字，且跟随元素自身尺寸。 */
.nav-in.has-hscroll {
  -webkit-mask-image: linear-gradient(to left, transparent, #000 16px);
  mask-image: linear-gradient(to left, transparent, #000 16px);
}
.nav-end { flex: 0 0 auto; display: flex; align-items: center; gap: var(--sp-1); }
/* 用户菜单（手机端）：details 原生下拉。桌面隐藏（.nav-end 保持一行）。 */
.nav-menu { display: none; position: relative; flex: 0 0 auto; }
.nav-menu summary {
  list-style: none; cursor: pointer; -webkit-tap-highlight-color: transparent;
  display: flex; align-items: center; gap: 4px;
  /* 【Round18 修复】深色顶栏时代的硬编码色（#cbd5e1 / rgba(255,255,255,.10)）已经
     与"浅灰白顶栏"（--nav-bg: #f8f9fa）完全脱节 → 手机端右上角的「admin ▾」
     按钮几乎与白底融为一体，看不见也点不着。改用语义 token（深浅色自动跟随）。 */
  color: var(--txt-2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm); padding: 6px 10px; font-size: var(--fs-sm);
  max-width: 8em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.nav-menu summary::-webkit-details-marker { display: none; }
.nav-menu summary::after { content: '▾'; opacity: .7; }
.nav-menu[open] summary { background: var(--surface-2); color: var(--txt); }
.nav-menu-user { overflow: hidden; text-overflow: ellipsis; }
.nav-menu-list {
  display: none; position: absolute; right: 0; top: calc(100% + 6px);
  min-width: 132px; padding: 6px; z-index: 90;
  background: var(--nav-bg-2); border: 1px solid rgba(255, 255, 255, .15);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-md);
}
.nav-menu[open] .nav-menu-list { display: block; }
.nav-menu-h {
  font-size: var(--fs-xs); color: #94a3b8; padding: 4px 10px 8px;
  border-bottom: 1px solid rgba(255, 255, 255, .12); margin-bottom: 4px;
}
.nav-menu-list a, .nav-menu-list button {
  display: block; width: 100%; text-align: left; padding: 8px 10px;
  color: #cbd5e1; background: none; border: 0; border-radius: var(--radius-sm);
  font-size: var(--fs-sm); cursor: pointer; font-family: inherit;
}
.nav-menu-list a:hover, .nav-menu-list button:hover { background: rgba(255, 255, 255, .12); color: #fff; text-decoration: none; }
.nav a {
  color: #cbd5e1;
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  white-space: nowrap;
  transition: background .15s, color .15s;
}
.nav a:hover { background: rgba(255, 255, 255, .10); color: #fff; text-decoration: none; }
/* 导航分组竖线（每天看的 / 持仓 / 分析 / 系统）：不用换行，横向滚动照旧 */
.nav .nav-sep {
  flex: 0 0 auto;
  width: 1px;
  height: 16px;
  margin: 0 2px;
  background: rgba(255, 255, 255, .22);
}
.nav .theme-btn {
  background: rgba(255, 255, 255, .10);
  border: 0;
  color: #cbd5e1;
  border-radius: var(--radius-sm);
  padding: 5px 9px;
  cursor: pointer;
  font-size: var(--fs-sm);
  white-space: nowrap;
}
.nav .theme-btn:hover { background: rgba(255, 255, 255, .2); color: #fff; }
/* 当前登录用户：多用户下必须能一眼确认"我在谁的账户里" */
.nav .nav-user {
  color: #94a3b8;
  font-size: var(--fs-sm);
  padding: 0 2px;
  white-space: nowrap;
  max-width: 8em;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---------- 回到顶部浮钮（仅手机显示，app.js 注入） ---------- */
.back-top {
  display: none;
  position: fixed;
  right: 14px;
  bottom: calc(24px + env(safe-area-inset-bottom));
  z-index: 55;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--txt-2);
  box-shadow: var(--shadow-md);
  font-size: 18px;
  cursor: pointer;
  opacity: 0;
  visibility: hidden;
  transition: opacity .2s, visibility .2s;
}
.back-top.show { opacity: .92; visibility: visible; }

/* ---------- 折叠区开合标识：收起时明确提示"展开 ▾"，点开后变"收起 ▴"
   （纯小箭头太隐蔽，用户看不出内容可展开；文字+主题色最直观） ---------- */
details.fold > summary { list-style: none; cursor: pointer; -webkit-tap-highlight-color: transparent; }
details.fold > summary::-webkit-details-marker { display: none; }
/* 展开/收起提示由 CSS 统一给出（h2 或 b 作标题的折叠都用同一套），
   此前有一处是在文案里手写"（点击展开）"、另一处靠这条 CSS，同一个页面两种提示语 */
details.fold > summary h2::after,
details.fold > summary b::after {
  content: '展开 ▾';
  margin-left: 10px;
  color: var(--brand);
  font-size: var(--fs-sm);
  font-weight: 600;
}
details.fold[open] > summary h2::after,
details.fold[open] > summary b::after { content: '收起 ▴'; }
.fold-body { margin-top: 8px; }

/* 明细列：桌面【单行省略号 + 点"原因"在下方全宽行展开全文】，手机端由媒体查询
   进一步隐藏正文、只留按钮。此前桌面端按钮是 display:none → 桌面只能看到被省略号
   截断的半句话、没有看全文的出口，长原因（策略 reason / 事件说明）看不全。 */
.detail .d-toggle { display: inline-block; }

/* 明细正文单行省略号。此前桌面端是"全文直接铺开"，而 td 又设了 nowrap：
   长原因（如策略 reason 带一串因子说明）会撑破单元格、直接压到右侧"状态"列上——
   td.detail 的 max-width 对 table-cell 不生效（表格布局会忽略它），必须给单元格
   一个确定宽度 + 内层块级容器才能真正裁开（见下方 @media 的 td.detail 定宽）。
   title 属性提供悬停看全文（模板里加）。
   注意：display 用 inline-block、且本条写在手机端媒体查询【之前】——手机端
   .detail .d-full{display:none} 特异性相同、后者胜，行内展开不受影响。 */
.detail .d-full {
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}

/* 展开行：点"原因/明细"后由 JS 在本行下方插入的 colspan 全宽行。
   必须全局生效——此前写在手机端媒体查询里，桌面端展开后仍是 nowrap，
   文字不换行等于没展开。tbody td 是 nowrap，这里显式改成 normal。 */
tr.d-row > td {
  white-space: normal;
  word-break: break-all;
  color: var(--txt-2);
  font-size: var(--fs-xs);
  line-height: 1.7;
  background: var(--surface-2);
}

/* 只省略、不折叠：给"实时表格里的说明列"用（盯盘事件说明 / 成交原因 / 通知错误）。
   这些表格行是 JS 高频重建的，塞"原因"按钮既吵又容易和刷新抢焦点，所以手机端
   不做折叠、直接换行显示；桌面端单行省略号 + title 悬停看全文。
   与 .d-full 的区别：.d-full 手机端会被隐藏、靠按钮在下方展开全宽行。 */
.d-clip {
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}

/* 尾部垫柱（手机端 sticky 右列下方留白，避免压住数字列，app.js 注入；
   宽度由 app.js 按 sticky 列实测宽度以 inline width+min-width 写入） */
th.op-pad, td.op-pad { padding: 0 !important; background: transparent !important; }

/* 触控 polish：去掉 iOS 点按灰块 + 按压反馈 */
* { -webkit-tap-highlight-color: transparent; }
button:active, .btn:active { opacity: .7; }

/* ---------- 布局 ---------- */
main {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: var(--sp-4) var(--sp-4) var(--sp-5);
  /* iPhone 底部 home indicator 不遮挡内容 */
  padding-bottom: calc(var(--sp-5) + env(safe-area-inset-bottom));
}
h1 {
  font-size: var(--fs-xl);
  margin: 0 0 var(--sp-1);
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  flex-wrap: wrap;
}
h1 small { font-size: var(--fs-xs); color: var(--muted); font-weight: 400; }
/* 区块标题（页面级 h1 之下一级）：全站**只有这一种**区块标题样式。
   间距刻意收紧（16/8 而非 24/12）：这是工具型界面，一屏要放得下更多区块。 */
h2 {
  font-size: var(--fs-lg);
  margin: var(--sp-4) 0 var(--sp-2);
  padding-left: 10px;
  border-left: 3px solid var(--brand);
  line-height: 1.3;
}
h3 { font-size: var(--fs-md); margin: var(--sp-3) 0 var(--sp-2); color: var(--txt-2); }
.muted { color: var(--muted); }
.mono { font-family: var(--mono); }

/* 卡片内标题（区块标题的下一级）。此前卡片标题有三种写法混用：卡片内 <b>、
   .chart-box .ttl、details>summary>h2——同一页上就能看到三种字号/间距。
   统一到这里：与 h2 同族的弱化版（无左侧竖条、下缘一条细线做分隔）。 */
.card-h, .chart-box .ttl {
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--txt);
  margin: 0 0 var(--sp-2);
  padding-bottom: 6px;
  border-bottom: 1px solid var(--line-2);
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--sp-2);
  flex-wrap: wrap;
}
.card-h .muted, .chart-box .ttl .muted { font-weight: 400; }

/* 卡片 / 网格（间距比首版更紧凑：16→13/14、区块间 16→12，一屏多放一个区块） */
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 13px 14px;
  box-shadow: var(--shadow);
  margin-bottom: var(--sp-3);
}
.grid { display: grid; gap: var(--sp-3); grid-template-columns: 1fr; }
/* 首页"系统状态 + 今日待办"卡：明细行左对齐、行距紧凑，异常/待办靠颜色区分 */
#sys-card .card-todo { margin-top: var(--sp-3); }
#sys-card .card-todo > div { padding: 2px 0; }
/* 卡片内副标题/说明行（此前用内联 style="margin-top:6px" 手写） */
.card-sub { color: var(--muted); font-size: var(--fs-xs); margin-top: 6px; line-height: 1.6; }
.card-sub b { color: var(--txt-2); }

/* 复盘页顶部锚点导航：该页 7 个区块（对账/净值/信号后续/因子/归因/成交/归档），
   之前只能一路滚。锚点跳转要留出 sticky 导航的高度，否则标题被盖住。 */
.anchor-nav { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin: 0 0 var(--sp-3); }
.anchor-nav a {
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  padding: 3px 10px;
  font-size: var(--fs-xs);
  color: var(--txt-2);
}
.anchor-nav a:hover { background: var(--surface-3); text-decoration: none; }
h2 { scroll-margin-top: calc(var(--nav-h) + var(--sp-3)); }
/* 手机端一律单列：g2/g3/g4 的多列布局只在 ≥620px 媒体查询里启用。
   此前多列写在基础规则里，手机上 3 张 KPI 卡/2 个图表挤一行（实盘页"错乱+展示不全"的根因） */
.cards { display: grid; gap: var(--sp-3); grid-template-columns: 1fr; }

/* 指标卡 */
.kpi {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--sp-3) var(--sp-4);
  box-shadow: var(--shadow);
}
.kpi .k { font-size: var(--fs-xs); color: var(--muted); letter-spacing: .3px; }
.kpi .v { font-size: var(--fs-xl); font-weight: 650; margin-top: 2px; font-variant-numeric: tabular-nums; }
.kpi .s { font-size: var(--fs-xs); color: var(--txt-2); margin-top: 2px; }

/* 【Round34 Step1.2】账户胶囊切换栏 —— 替代被 CSP 拦死的 `<select onchange>`。
   服务端 CSP 是 `script-src 'self' 'nonce-…'`，内联 onchange 在手机上 100% 不执行；
   `<a>` 不需要任何脚本，且带"N只持仓"徽章，用户一眼看出票在哪个账户里。
   minmax(105px,1fr) + auto-fit：3 个账户在手机上排 3 列（每个约 1/3 宽），
   窄到放不下时自动折行，**不会横向溢出**（Round33 的 300,00 截断就是溢出导致的）。 */
.acct-switch-bar {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(105px, 1fr));
	gap: 6px;
	width: 100%;
	box-sizing: border-box;
	margin: 8px 0 10px;
}
.acct-pill {
	display: flex; flex-direction: column; gap: 3px;
	min-width: 0;                 /* 关键：grid 子项默认 min-width:auto 会撑破容器 */
	padding: 7px 9px;
	border: 1px solid var(--line);
	border-radius: 10px;
	background: var(--surface);
	color: var(--txt);
	text-decoration: none;
	line-height: 1.25;
}
.acct-pill:hover { border-color: var(--brand); text-decoration: none; }
.acct-pill.is-active {
	border-color: var(--brand);
	background: var(--brand-bg);
	box-shadow: inset 0 0 0 1px var(--brand);
}
.acct-pill-title {
	font-size: var(--fs-sm); font-weight: 650;
	white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.acct-pill .badge { align-self: flex-start; font-size: 11px; }

/* ---------- 表格 ---------- */
.tw {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: auto;
  max-height: 78vh;
  margin-bottom: var(--sp-3);
}
table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: var(--fs-sm); }
/* 表头吸顶。作用域限定在 .tw 内 —— 它自带 max-height + overflow:auto，是真正的滚动容器；
   没有它的话 sticky 会相对视口生效，表头会飘在整页顶部（比不吸顶更糟）。
   ⚠ 选择器必须同时匹配「裸首行」：本项目所有表格都写成 <tr><th>…</th></tr>，**从不写 <thead>**
   （13 个模板 grep 全无）→ 原实现只写 `thead th`，是**从未生效过的死规则**，于是每个表格
   一滚动就丢列名（用户 2026-09-16 报告"好几个地方都有这个问题"）。
   背景必须不透明：sticky 元素透明时，行内容会从表头下面透出来，看起来像表头压住了数据行。 */
.tw thead th,
.tw table > tbody > tr:first-child th,
.tw table > tr:first-child th {
  position: sticky;
  top: 0;
  z-index: 3;
  background: var(--surface-3);
  color: var(--txt-2);
  font-weight: 600;
  text-align: left;
  padding: 9px 10px;
  white-space: nowrap;
  border-bottom: 1px solid var(--line);
  user-select: none;
}
.tw thead th[data-sort], .tw table > tbody > tr:first-child th[data-sort], .tw table > tr:first-child th[data-sort] { cursor: pointer; }
.tw thead th[data-sort]:hover, .tw table > tbody > tr:first-child th[data-sort]:hover, .tw table > tr:first-child th[data-sort]:hover { color: var(--brand); }
.tw thead th .arrow { opacity: .45; margin-left: 3px; font-size: 10px; }
.tw thead th.sorted .arrow { opacity: 1; color: var(--brand); }
tbody td {
  padding: 8px 10px;
  border-bottom: 1px solid var(--line-2);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
tbody tr:hover td { background: var(--surface-2); }
tbody tr:last-child td { border-bottom: 0; }
/* 数值列右对齐必须压过上方 `.tw thead th` 的左对齐：旧写法 `td.num,th.num` 优先级
   (0,1,1) 输给 (0,1,2) → 表头永远左对齐、数值右对齐，列一宽就明显错位
   （2026-09-17 用户截图「这几个列都没对齐」）。加 .tw 前缀提高优先级，
   并覆盖 thead 与无 thead（首行即表头）两种结构。 */
.tw td.num, .tw thead th.num,
.tw table > tbody > tr:first-child th.num,
.tw table > tr:first-child th.num { text-align: right; }

/* 表格工具条（搜索 / 计数） */
.tbl-tools {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin: 0 0 var(--sp-2);
  flex-wrap: wrap;
}
.tbl-tools input[type="search"] {
  flex: 0 1 260px;
  padding: 6px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--txt);
  font-size: var(--fs-sm);
}
.tbl-tools .count { font-size: var(--fs-xs); color: var(--muted); }

/* 空状态（.empty 是 .state 的历史别名，JS 注入点多处沿用，样式保持同一套） */
.empty {
  padding: var(--sp-5) var(--sp-4);
  text-align: center;
  color: var(--muted);
  font-size: var(--fs-sm);
  line-height: 1.6;
}
.empty::before {
  content: "—";
  display: block;
  font-size: 22px;
  margin-bottom: var(--sp-2);
  opacity: .5;
}

/* ---------- 涨跌 / 徽章 ---------- */
.up { color: var(--up) !important; }
.down { color: var(--down) !important; }
/* 状态色裸类：与涨跌色严格分离。正常/成功=ok 蓝、危险=danger 红、注意=warn 琥珀，
   仅文字色，不用大色块（danger 大面积红会被读成"涨"）。
   只在非 badge 场景使用；badge 场景走 .badge.ok/.danger/.warn。 */
.ok { color: var(--ok) !important; }
.warn { color: var(--warn) !important; }
.danger { color: var(--danger) !important; }
.flat, .na { color: var(--muted) !important; }
.badge {
  display: inline-block;
  padding: 1px 8px;
  border-radius: var(--radius-pill);
  font-size: var(--fs-xs);
  line-height: 18px;
  background: var(--surface-3);
  color: var(--txt-2);
  white-space: nowrap;
}
.badge.up { background: var(--up-bg); color: var(--up); }
.badge.down { background: var(--down-bg); color: var(--down); }
.badge.brand { background: var(--brand-bg); color: var(--brand); }
.badge.warn { background: var(--warn-bg); color: var(--warn); }
.badge.danger { background: var(--danger-bg); color: var(--danger); }
/* 状态徽标（"正常/成功"）：不要用 .up/.down——那是涨跌色，红/绿在 A 股有明确含义 */
.badge.ok { background: var(--ok-bg); color: var(--ok); }

/* ---------- 三态：加载中 / 空 / 失败 ----------
   此前三态是"每页自己写一句话"：全站 20 种文案（"加载中…"与"加载中..."混用、
   "暂无数据/暂无成交/暂无成交记录"、"待积累/暂无净值数据"），样式也有三种（.empty、
   裸 muted 文本、图表内部文案）。统一为 .state 一个组件 + 三种语气：
     .state          空（中性灰 + "—"）
     .state.load     加载中（中性灰 + 脉冲）
     .state.err      失败（琥珀，不用红——红是"涨"） */
.state {
  padding: var(--sp-5) var(--sp-4);
  text-align: center;
  color: var(--muted);
  font-size: var(--fs-sm);
  line-height: 1.6;
}
.state.load { animation: state-pulse 1.4s ease-in-out infinite; }
.state.err { color: var(--warn); }
.state.err::before { content: "⚠"; display: block; font-size: 20px; margin-bottom: var(--sp-2); opacity: .8; }
@keyframes state-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }

/* ---------- 表单 / 按钮 ---------- */
input, select, textarea, button { font-family: inherit; font-size: var(--fs-sm); }
input[type="text"], input[type="number"], input[type="date"], select {
  padding: 6px 9px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--txt);
  max-width: 100%;
}
input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--brand-bg);
  border-color: var(--brand);
}
button, .btn {
  background: var(--brand);
  color: #fff;
  border: 0;
  border-radius: var(--radius-sm);
  padding: 6px 13px;
  cursor: pointer;
  font-size: var(--fs-sm);
  transition: filter .15s, background .15s;
  display: inline-block;
  text-decoration: none;
}
button:hover, .btn:hover { filter: brightness(1.08); text-decoration: none; }
button:disabled, .btn:disabled { opacity: .5; cursor: not-allowed; filter: none; }
.btn.ghost { background: transparent; color: var(--txt-2); border: 1px solid var(--line); }
.btn.ghost:hover { background: var(--surface-3); }
/* 破坏性操作按钮（实盘页"实盘一键清零"、表格里的"删"）：
   ① 可见性 bug 修复 —— 上面的状态裸类 `.danger{color:var(--danger)!important}` 会命中按钮，
      把文字染成与背景同色，于是渲染成【红底红字】：整颗按钮看去就是一个纯红色方块、
      文字完全不可见（实盘设置区那个"红方块"就是这个，watchlist/real 表格里的"删"同样中招）。
      修法：同等 !important + 更高特异性(0,2,0) 覆盖，显式声明文字色。
   ② 顺带降视觉权重 —— 本设计系统约定 danger 是"仅文字/细边，不用大色块"（见 :root 注释），
      而破坏性操作不该是页面上最抢眼的元素：默认描边红、hover 才实心红。
      这样与主操作（实心蓝）形成清晰的层级。 */
.btn.danger {
  background: transparent;
  color: var(--danger) !important;
  border: 1px solid var(--danger);
}
.btn.danger:hover {
  background: var(--danger);
  color: #fff !important;
  filter: none;
}
.btn.sm { padding: 3px 9px; font-size: var(--fs-xs); }
.form-row { display: flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap; margin-bottom: var(--sp-2); }
label { font-size: var(--fs-sm); color: var(--txt-2); }

/* ---------- 图表 ---------- */
.chart { width: 100%; height: 280px; }
.chart.sm { height: 200px; }
.chart.lg { height: 360px; }
/* 图表加载骨架（见 kline.html）：容器高度由 .chart* 决定，这里只做内边距与上留白，
   让骨架不贴着容器顶边。骨架样式本体（.skel / .w40 / .lg）在 app.css。 */
.chart-skel { padding: 18px 4px; }
.chart-box {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: var(--sp-3);
  margin-bottom: var(--sp-3);
}
.chart-box .ttl {
  font-size: var(--fs-sm);
  color: var(--txt-2);
  margin-bottom: var(--sp-2);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

/* ---------- 进度条（因子 IC 等） ---------- */
.bar-wrap { position: relative; background: var(--surface-3); border-radius: 3px; height: 6px; min-width: 60px; }
.bar { position: absolute; left: 0; top: 0; bottom: 0; border-radius: 3px; background: var(--brand); }
.bar.up { background: var(--up); }
.bar.down { background: var(--down); }
.lcell { display: inline-block; width: 19%; text-align: center; font-size: var(--fs-xs); }

/* ---------- 筹码分布（纯 HTML 横条，kline.html） ---------- */
.chip-list { display: flex; flex-direction: column; gap: 2px; height: 100%; overflow-y: auto; }
.chip-row { display: grid; grid-template-columns: 56px 1fr 48px; align-items: center; gap: 6px; font-size: 11px; }
.chip-price { color: var(--txt-2); text-align: right; font-variant-numeric: tabular-nums; }
.chip-bar { position: relative; background: var(--surface-3); border-radius: 2px; height: 10px; }
.chip-bar > span { display: block; height: 100%; border-radius: 2px; }
.chip-pct { color: var(--txt-2); text-align: right; font-variant-numeric: tabular-nums; }


/* ---------- 日报 Markdown ---------- */
.report-md {
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--sp-3) var(--sp-4);
  font-family: var(--mono);
  font-size: var(--fs-xs);
  line-height: 1.65;
  white-space: pre-wrap;
  word-break: break-word;
  overflow-x: auto;
  max-height: 520px;
  overflow-y: auto;
}

/* ---------- Toast ---------- */
.toast-wrap {
  position: fixed;
  right: var(--sp-4);
  bottom: calc(var(--sp-4) + env(safe-area-inset-bottom));
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  max-width: 90vw;
}
.toast {
  background: var(--txt);
  color: var(--bg);
  padding: 9px 14px;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  font-size: var(--fs-sm);
  animation: toast-in .18s ease-out;
}
.toast.ok { background: var(--ok); color: #fff; }
.toast.err { background: var(--danger); color: #fff; }
@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* ---------- 看板专用 ---------- */
.mh-idx { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-2); margin-top: var(--sp-2); }
.mh-item { background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: var(--sp-2) var(--sp-3); }
.mh-item .n { font-size: var(--fs-xs); color: var(--muted); }
.mh-item .p { font-size: var(--fs-lg); font-weight: 600; font-variant-numeric: tabular-nums; }
.mh-item .c { font-size: var(--fs-xs); }
.heat { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-top: var(--sp-2); }
.heat .h { background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 4px 10px; font-size: var(--fs-xs); }
.heat .h b { font-size: var(--fs-md); margin-left: 4px; }

/* ---------- 步进器 / 买卖切换 ---------- */
.stepper { display: inline-flex; align-items: center; gap: 0; }
.stepper button {
  border-radius: 0; padding: 6px 10px; background: var(--surface-3);
  color: var(--txt-2); font-weight: 700; border: 1px solid var(--line);
}
.stepper button:first-child { border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
.stepper button:last-child { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
.stepper input {
  border-radius: 0; border-left: 0; border-right: 0; text-align: center; width: 92px;
}
/* 买卖方向切换（回填表单）。选中态**纯 CSS 由 :checked 决定**，不再用 JS 改 opacity：
   旧实现里"视觉(radio)"与"实际提交值(hidden side)"是两份状态，JS 没跑到就会显示买入、
   实记卖出（2026-10-08 收掉）。选中=实心彩 badge+描边+加粗；未选中=灰底灰字，一眼可辨。 */
.side-toggle label { cursor: pointer; margin-right: 6px; display: inline-flex; align-items: center; }
.side-toggle input { margin: 0 4px 0 0; vertical-align: -2px; }
.side-toggle input:not(:checked) + .badge {
  background: var(--surface-2); color: var(--muted); font-weight: 400;
  box-shadow: inset 0 0 0 1px var(--line);
}
.side-toggle input:checked + .badge { font-weight: 600; box-shadow: inset 0 0 0 2px currentColor; }
/* 提交按钮随方向变色（红=买入 / 绿=卖出，与 badge 同色系），把方向再摆到"落锤点"：
   录错方向的代价是账户口径错乱，比事后删流水+重放便宜得多。
   用 :has()（app.css 已在用）；不支持时仅退化为普通按钮，功能不受影响。 */
#real-form button[type="submit"] { font-weight: 600; }
#real-form:has(input[name="side"][value="buy"]:checked) button[type="submit"] {
  background: var(--up-bg); color: var(--up); box-shadow: inset 0 0 0 1px var(--up);
}
#real-form:has(input[name="side"][value="sell"]:checked) button[type="submit"] {
  background: var(--down-bg); color: var(--down); box-shadow: inset 0 0 0 1px var(--down);
}

/* ---------- 响应式 ---------- */
@media (min-width: 620px) {
  .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid.g2, .grid.g3, .grid.g4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 900px) {
  main { padding: var(--sp-5) var(--sp-5) var(--sp-6); }
  .grid.g3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .grid.g4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (min-width: 1200px) {
  .cards { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (max-width: 619px) {
  main { padding: var(--sp-3) var(--sp-2) var(--sp-5); }
  h1 { font-size: var(--fs-lg); }
  thead th, tbody td { padding: 7px 8px; }
  table { font-size: var(--fs-xs); }
  /* 指数卡在手机端保持 3 列（此前改 1 列 → 上证/深成/创业板 各占一整行、数字 19px，
     吃掉近 200px 高度，把「收益概览」「自选行情」挤到首屏之外）。缩到 3 列紧凑瓦片：
     名称 11px、价格 15px，一屏能同时看到指数与账户收益。 */
  .mh-idx { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-1); }
  .mh-item { padding: 6px 8px; }
  .mh-item .p { font-size: var(--fs-md); }
  /* 窄屏隐藏次要列（模板里用 .hide-m 标注） */
  .hide-m { display: none; }
  .tw { max-height: none; }

  /* iOS 聚焦 <16px 的输入框会强制整页放大（体验灾难），手机上必须 ≥16px */
  input, select, textarea { font-size: 16px; }
  .tbl-tools input[type="search"] { font-size: 16px; flex: 1 1 100%; }
  /* 表单控件弹性占满一行，避免 fixed 宽度溢出 */
  .form-row input[type="text"], .form-row input[type="number"],
  .form-row input[type="date"], .form-row input[type="search"],
  .form-row input:not([type]), .form-row select { flex: 1 1 130px; min-width: 0; }

  /* KPI 卡手机端改两列网格（标签左 / 数值右，说明整行另起）：竖排时三张卡占掉一整屏。
     必须用 grid 而不是 flex+space-between——/paper 的"总资产"卡带一行长说明（.s），
     在 flex 里标签被挤到只能竖排折行（实测渲染成"总/资/产"三行），grid 下标签固定
     不收缩、说明自动落到第二行。 */
  .kpi {
    display: grid; grid-template-columns: auto 1fr;
    align-items: baseline; column-gap: var(--sp-3); row-gap: 2px;
    /* Round34 Step1.2：`300,000` 在手机上被截成 `300,00` —— grid 子项默认 min-width:auto
       会按内容撑宽并溢出卡片；min-width:0 + overflow:hidden 才是正确修法，
       数值再用 clamp 自适应字号，长到 7 位也不会挤破。 */
    min-width: 0; padding: 10px; overflow: hidden;
  }
  .kpi .k { white-space: nowrap; }
  .kpi .v {
    margin: 0; text-align: right;
    font-size: clamp(16px, 4.6vw, 22px);
    word-break: break-all;
  }
  .kpi .s { grid-column: 1 / -1; text-align: right; }
  /* 手机端账户胶囊：字号收一档，保证 3 个并排时标题不被省略号吃掉太多 */
  .acct-pill { padding: 6px 7px; }
  .acct-pill-title { font-size: 12px; }
  /* 图表高度收敛，给表格/下面的区块留视口空间（净值图是"趋势"不是"现在"，
     手机端 190px 足够看清形状，把收益概览与自选行情提上来） */
  .chart { height: 190px; }
  .chart.lg { height: 250px; }

  /* 触控目标放大：btn sm 实际可点区域 ≥40px（Apple HIG 建议 44px） */
  .btn.sm, button { padding-top: 8px; padding-bottom: 8px; }
  .btn.sm { padding-left: 12px; padding-right: 12px; }
  nav a { padding: 10px 12px; }
  /* 功能区收进用户菜单：一行「用户名▾」顶替 用户名+主题+登出 三个元素，
     链接区能多拿回约 100px（多显示 2 个栏目），也不会再互相挤压 */
  .nav-end { display: none; }
  .nav-menu { display: block; }

  /* 横滚时首列固定：代码/名称始终可见，否则滑到右边就不知道哪行是谁 */
  /* 【Round18 修复 3】整段限定在 `.tw:not(.cards)`：卡片模式下表格已改成 CSS Grid
     卡片，"冻结首列 / 吸顶表头 / 固定操作列"这套横滚补救**完全失效**（甚至互相污染：
     sticky 元素在 grid 里会脱离常规流）。此前靠文件末尾用一堆 position:static 打补丁，
     现在从源头隔离：卡片模式不加载这套规则。 */
  .tw:not(.cards) tbody tr:first-child th:first-child,
  .tw:not(.cards) table > tr:first-child th:first-child {
    position: sticky; left: 0; top: 0; z-index: 4;
    background: var(--surface-3); box-shadow: 1px 0 0 var(--line);
  }
  .tw:not(.cards) thead th:first-child,
  .tw:not(.cards) tbody td:first-child {
    position: sticky; left: 0; z-index: 2;
    background: var(--surface);
    box-shadow: 1px 0 0 var(--line);
  }
  .tw:not(.cards) thead th:first-child { background: var(--surface-3); z-index: 4; }
  .tw:not(.cards) tbody tr:hover td:first-child { background: var(--surface); }

  /* 操作列固定右侧（与首列对称）：此前"删/K线"在最右，横滑到底才能点到 */
  .tw:not(.cards) th.op,
  .tw:not(.cards) td.op,
  .tw:not(.cards) td.ops {
    position: sticky; right: 0; z-index: 2;
    background: var(--surface);
    box-shadow: -1px 0 0 var(--line);
  }
  .tw:not(.cards) thead th.op { background: var(--surface-3); z-index: 4; }

  /* 控件多的操作格（.op-stack，目前只有 /real 持仓表：K线 + 豁免理由输入框 + 豁免止损 + 删）
     在手机端必须竖排收窄。注意【写 width 没用】：table-layout:auto 下列宽由内容的
     max-content 决定，单元格上的 width 会被 max-content 夹住（实测 width:104px 仍渲染成
     88px）——只能让控件自己变小。
     不收会怎样：`<input>` 默认 size=20（≈180px）且单元格 nowrap，整列被撑到 **279px**；
     它是 sticky 右列，于是 484px 的屏上"操作列 279px + JS 垫柱 279px = 558px"——
     首屏被它遮住 58%（浮盈/市值全压在它下面），横滑到底看到的又是它和一片空白，
     用户感受就是"往右滑到最后也不能显示全所有的栏目"。
     必须限定 .op-stack：只放一个"K线"按钮的窄操作格（/paper、/watchlist）本来只有
     ~50px、按钮单行，套上竖排+width:100% 反而会把文字拆成竖排（实测 26x56）。
     垫柱宽度由 app.js 按本列实测宽度同步（ensureOpPad）。 */
  td.op-stack { white-space: normal; }
  /* 表单已改用 .inline-form 类（模板不再写行内样式），因此不再需要 !important ——
     行内样式的特异性最高，此前是它逼出了这里的 !important。 */
  td.op-stack form { display: block; margin: 0; }
  td.op-stack .btn {
    display: block; width: 100%; box-sizing: border-box;
    text-align: center; margin-top: 4px;
    padding-left: 6px; padding-right: 6px;   /* 内边距收窄，"豁免止损"才不折行 */
    white-space: nowrap;                    /* 按钮文字永不折行（否则列窄时被拆成竖排） */
  }
  td.op-stack .ack-reason {
    width: 100%; box-sizing: border-box; padding-left: 6px; padding-right: 6px;
  }
  td.op-stack .badge { display: block; white-space: normal; margin-top: 4px; }
  /* 名称列的"止损豁免"徽标另起一行：它是 nowrap 长文本（"止损豁免至09-16"），
     与名称同行时会把名称列从 ~64px 撑到 249px（实测），数据列因此被挤出屏幕 */
  .ack-badge { display: block; white-space: normal; }

  /* 横滚右缘渐变提示"右边还有内容"（.has-hscroll 由 app.js 检测溢出后添加）。
     必须挂在 .tw 自身 background 上：attachment 默认 scroll=钉在可视边缘，
     且画在单元格内容之下。此前用 ::after 绝对定位盖在内容之上，横向滚动后
     会漂进表格中部，把方向徽章/浮盈金额数字拦腰遮断（WebKit 与 Blink 对
     滚动容器内 abs 定位的锚点行为不一致，都无法稳定钉在可视右缘） */
  .tw.has-hscroll {
    background:
      linear-gradient(to left, var(--surface), transparent) right center / 22px 100% no-repeat,
      var(--surface);
  }

  /* 明细列手机端：单元格太窄，连省略号都放不下 → 正文整体隐藏、只留"明细"按钮 */
  .detail .d-full { display: none; }
  .detail .d-toggle { display: inline-block; }

  .back-top { display: block; }
}

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}

/* ---------- 窄屏表格 → 卡片列表（给 .tw 加 .cards 才生效） ----------
   窄屏横向滚动表格是本项目移动端"别扭"的最大来源：为让它勉强可用，前面堆了
   首列 sticky + 操作列 sticky + JS 垫柱(ensureOpPad) + 右缘渐变 四层补救，
   仍然摆脱不了"左右来回滑、滑到底也看不全一行的全貌"。
   业界标准解法是窄屏直接换布局形态：把「行」渲染成「卡片」，表头字段名下沉到
   每个单元格（模板用 data-label 提供），操作按钮改成卡片底部的按钮组。
   设计约束：① 只对 .cards 标记的表格生效（未标记页面保持原状，可逐页迁移/回退）；
            ② 必须放在本文件末尾，才能压过上面那些 sticky/垫柱规则。 */
@media (max-width: 619px) {
  .tw.cards {
    background: transparent; border: 0; box-shadow: none;
    overflow: visible; max-height: none;
  }
  .tw.cards table { display: block; font-size: var(--fs-sm); }
  .tw.cards thead { display: none; }              /* 字段名改由 td[data-label] 承担 */
  .tw.cards tbody, .tw.cards td { display: block; width: auto; }
  /* 每张卡片 = 两列网格：把"12 个字段 → 12 行"压成 6 行。
     单列键值对实测一张实盘持仓卡 13 行、一屏放不下 1.5 只票。 */
  .tw.cards tr {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: var(--sp-3);
    row-gap: 2px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: var(--sp-2) var(--sp-3);
    margin-bottom: var(--sp-2);
  }
  .tw.cards tbody tr:hover td { background: transparent; }
  .tw.cards td {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--sp-3);
    padding: 4px 0;
    border: 0;
    white-space: normal;            /* 解除表格的 nowrap，长文本（说明/明细）可换行 */
    text-align: left;
    font-variant-numeric: tabular-nums;
  }
  /* 字段名：宽屏表头在窄屏的等价物，靠模板写 data-label */
  .tw.cards td::before {
    content: attr(data-label);
    flex: 0 0 auto;
    color: var(--muted); font-size: var(--fs-xs);
  }
  /* 无 data-label 的格子不显示空标签（如首格标题、操作格） */
  .tw.cards td:not([data-label])::before { content: none; }
  /* 首格＝卡片标题：去掉字段名、加大加粗，与下方键值行拉开层级 */
  .tw.cards td.card-title {
    font-size: var(--fs-md); font-weight: 600;
    padding-bottom: var(--sp-1);
  }
  /* 跨列：标题、操作行，以及内容较长的字段（由 buildCards 按字数打标）——
     长值挤进半列会被折成两三行，反而更高。
     ⚠【Round17 Bug A】选择器必须同时覆盖 `td.op` 与 `td.ops`：模板里只有
     watchlist.html 写 `class="ops op op-stack"`，而 real.html / paper.html 写的是
     **单数** `class="op"`（甚至 `class="op op-stack"`）——此前这里只写 `td.ops`，
     于是 /real、/paper 的手机卡片**整条操作栏规则全部失效**：
       ① 操作格没跨列 → "K线 体检"挤在半列里，也没有顶部细分隔线；
       ② `td.op-stack .btn { display:block; width:100% }` 生效 → 手机上每张持仓卡
          底部的 K线/体检/豁免止损/删被拉成 4 根占满宽度的竖排大长条，
          把单张卡片撑得极高。 */
  .tw.cards td.card-title,
  .tw.cards td.op,
  .tw.cards td.ops,
  .tw.cards td.card-wide { grid-column: 1 / -1; }
  /* 空值字段（—/空）隐藏：占一格却不带任何信息。
     实盘持仓卡里的"行业 —""止盈价 —"正是这一类。 */
  .tw.cards td.card-empty { display: none; }
  /* 操作行：按钮组横排右对齐、允许换行，顶部细线与信息区隔开 */
  .tw.cards td.op,
  .tw.cards td.ops {
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: var(--sp-2);
    margin-top: var(--sp-1);
    padding-top: var(--sp-2);
    border-top: 1px solid var(--line-2);
  }
  /* 徽标与按钮文字一律不换行（换行会把按钮撑成两行高、整行更松散） */
  .tw.cards td.op .ack-badge,
  .tw.cards td.ops .ack-badge,
  .tw.cards td.op .btn,
  .tw.cards td.ops .btn { white-space: nowrap; }
  /* 带输入框的操作组（实盘"豁免止损"的理由）：独占一行，
     否则它会跟按钮抢宽度，把"取消豁免"挤成竖排 */
  .tw.cards td.op .ack-form,
  .tw.cards td.ops .ack-form { flex: 1 1 100%; justify-content: flex-end; }
  .tw.cards td.op form,
  .tw.cards td.ops form { display: inline; }
  /* 按钮回到行内（覆盖上面 td.op-stack .btn 的竖排全宽规则：
     那条规则是给"横向滚动时代的操作列"设计的，卡片模式下必须撤销） */
  .tw.cards td.op .btn,
  .tw.cards td.ops .btn { display: inline-block; width: auto; margin: 0; }
  /* 卡片头栏（Round17 Bug C）：代码 + 名称 同行并排、加粗放大。
     此前除 watchlist.html 手写 .card-title 外，其余表格首格都是普通键值行 →
     手机卡片顶部没有标题层级；且名称若带徽章超过 13 字会被判 card-wide 挤到
     下一行，与代码脱节。这里给"首格即代码"的行（/real、/paper 持仓表）打标。 */
  .tw.cards td.card-head {
    font-size: var(--fs-md); font-weight: 700;
    justify-content: flex-start; text-align: left;
  }
  .tw.cards td.card-head-name {
    font-size: var(--fs-md); font-weight: 600;
    justify-content: flex-start; text-align: left;
  }
  /* 【Round18 · 三段式专业金融卡片】
     ① 头栏：左=代码+名称（加粗），右=核心指标（现价/浮盈%，大字）
     ② 指标区：只留核心字段；.hide-m（行业/板块/股数/成本/买入日/止盈价）**默认折叠**
     ③ 底栏：操作按钮组 + 左侧「详情 ▾」
     为什么必须显式 !important：`.hide-m{display:none}`（0,1,0）vs `.tw.cards td{display:flex}`
     （0,2,1）—— 后者胜出，此前**所有 .hide-m 次要列都在手机卡片里被强行复活**，
     一张持仓卡 15 个字段、8 行网格，一屏放不下 1.5 只票。 */
  .tw.cards td.card-hdr-code {
    grid-column: 1; grid-row: 1;
    font-size: var(--fs-md); font-weight: 700;
    align-items: baseline; gap: 6px; flex-wrap: wrap;
    justify-content: flex-start; text-align: left;
  }
  .tw.cards td.card-hdr-code .card-hdr-name { font-weight: 600; }
  .tw.cards td.card-hdr-metric {
    grid-column: 2; grid-row: 1;
    font-size: var(--fs-xl); font-weight: 700;
    justify-content: flex-end; text-align: right;
  }
  .tw.cards td.card-absorb { display: none; }
  /* 次要字段默认折叠，展开后恢复成键值对 */
  .tw.cards tr:not(.card-expanded) td.hide-m { display: none !important; }
  .tw.cards tr.card-expanded td.hide-m { display: flex; }
  /* 底栏：详情靠左、主操作靠右 */
  .tw.cards td.op .card-more-btn,
  .tw.cards td.ops .card-more-btn { margin-right: auto; }
  /* 展开行（点"原因/明细"动态插入的 tr.d-row）必须**独占整张卡**：
     ① HTML 的 colspan 在 CSS Grid 下无效，td 仍会挤进左半列（右边 50% 全是空白）；
     ② d-row 内的 td 没有 data-label（:not([data-label])::before 已隐藏空标签）。 */
  .tw.cards tr.d-row {
    grid-template-columns: 1fr;
    background: var(--surface-2);
  }
  .tw.cards tr.d-row td {
    grid-column: 1 / -1;
    display: block;
    white-space: normal;
    line-height: 1.55;
  }
  /* 干掉横向滚动时代的所有补救：sticky 首列/操作列、JS 垫柱、右缘渐变遮罩 */
  .tw.cards th:first-child, .tw.cards td:first-child,
  .tw.cards th.op, .tw.cards td.op, .tw.cards td.ops {
    position: static; box-shadow: none; background: transparent;
  }
  .tw.cards .op-pad { display: none !important; }
  .tw.cards.has-hscroll { background: var(--surface); }
  /* 长列表性能：盯盘（300 行）/ 信号（200 行）卡片化后是上千个 DOM 节点，
     滚动时浏览器要持续为全部行做布局与绘制。content-visibility:auto 让视口外的行
     直接跳过渲染；contain-intrinsic-size 提供占位高度，避免跳过渲染导致的滚动条跳动
     （高度估小了滚动条会抖，估大了滚到底多一段空白，取一个接近真实卡片的高度）。 */
  .tw.cards tr {
    content-visibility: auto;
    contain-intrinsic-size: auto 96px;
  }
}

/* ---------- 三段式卡片的"合成件"在宽屏表格里必须消失（Round19 Bug2） ----------
   buildCards 2.0 会把"名称"复制进代码格（.card-hdr-name）、在操作格插入「详情 ▾」
   （.card-more-btn）、必要时补一个空操作格（td.card-op）。这些是**卡片模式专属**的合成件：
   宽屏下它们会造成"代码列后面挤着名称"或表格多出一列空白。
   必须 !important：.tw.cards 的规则在窄屏媒体查询里，而这里的 .tw:not(.cards) 不与之冲突，
   但表格本身的 display 规则（td{display:table-cell}）会与之竞争。 */
.tw:not(.cards) .card-hdr-name,
.tw:not(.cards) .card-more-btn,
.tw:not(.cards) td.card-op { display: none !important; }

@media print {
  .nav, .toast-wrap, .tbl-tools, button { display: none !important; }
  body { background: #fff; }
  .card, .tw, .chart-box { box-shadow: none; break-inside: avoid; }
}

/* ===========================================================
   兼容遗留类名（历史模板仍在使用，统一收敛到新 token）
   =========================================================== */
.date { font-size: var(--fs-xs); color: var(--muted); margin-bottom: var(--sp-2); }
.mh-row { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; margin-top: var(--sp-2); }
.mh-label { font-size: var(--fs-xs); color: var(--muted); }
/* 已删除：.mh-cell/.mh-name/.mh-val/.pill/.tag-state/.pill.zt|dt|zb|lb
   —— 全库 grep 确认无任何模板使用（仅剩样式表自身），属遗留死规则 */
.s-tag, .code { font-family: var(--mono); font-size: var(--fs-xs); }
.ic_cls.up { color: var(--up); }
.ic_cls.down { color: var(--down); }
.j-price { font-variant-numeric: tabular-nums; }
.warnbox {
  background: var(--warn-bg); border: 1px solid var(--warn); color: var(--warn);
  border-radius: var(--radius-sm); padding: var(--sp-2) var(--sp-3);
  margin-bottom: var(--sp-3); font-size: var(--fs-sm);
}
details.card summary { cursor: pointer; font-weight: 600; }
.arch-list { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.arch-list a {
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 2px 8px; font-size: var(--fs-xs); background: var(--surface);
}

/* ---------- 原因列折叠（信号/交易计划/成交记录的长文本） ----------
   统一"下方全宽行"模式（见 app.js 明细展开委托）：桌面端 .d-full 单行省略号
   （悬停 title 看全文）+ 点"原因/明细"由 JS 在下方插入 colspan 全宽行展开全文；
   手机端单元格太窄，.d-full 整体隐藏（媒体查询内），只留按钮，点开同样走全宽行。
   勿再引入"单元格内 line-clamp 截断 + td.expanded 展开"方案：td.detail
   .d-full 的特异性高于媒体查询里的 .detail .d-full{display:none}，会覆盖
   手机端隐藏规则，导致单元格内常驻两行、点开把当前行撑得很大。 */
td.detail { max-width: 240px; }

/* 桌面端给明细列一个确定宽度：table-cell 的 max-width 不生效，没有 width 时
   .d-full 的单行省略号也没有换行基准，长文本仍会顶出去。
   手机端不设——那里整行由 JS 在下方插入 colspan 全宽行展示全文，
   定宽只会白占横向空间（9 列本来就要横向滚动）。 */
@media (min-width: 620px) {
  td.detail { width: 240px; }
  th.detail { width: 240px; }
}

/* 手机端：说明列不做省略，换行显示（.d-full 走折叠行，不受影响） */
@media (max-width: 619px) {
  .d-clip { display: inline; white-space: normal; }
}

/* ---------- 操作结果提示条（POST 后 303 回显） ----------
   POST/Redirect/GET 无法直接把结果回给页面，此前"止损豁免被上限拒绝"
   这类失败只能静默（用户以为点成功了）。写操作通过 ?notice= / ?error=
   带回，这里渲染成一次性提示条。 */
.flash {
  border-radius: var(--radius-sm);
  padding: var(--sp-2) var(--sp-3);
  margin-bottom: var(--sp-3);
  font-size: var(--fs-sm);
  border: 1px solid transparent;
}
.flash.ok { background: var(--ok-bg); border-color: var(--ok); color: var(--ok); }
.flash.err { background: var(--danger-bg); border-color: var(--danger); color: var(--danger); }

/* ---------- 止损豁免表单（表格操作列内联） ---------- */
.ack-form { display: inline-flex; align-items: center; gap: 4px; }
.ack-reason {
  width: 8em; min-width: 6em; padding: 3px 6px; font-size: var(--fs-xs);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); color: inherit;
}

/* ---------- 登录 / 注册页（auth）----------
   此前 login.html 与 register.html 各自内嵌一份约 100 行、几乎逐行相同的
   <style>，改一处漏一处；且 .login-err 用硬编码浅红，暗色模式下是一块刺眼亮色。
   这里统一为 .auth-* 并使用主题 token。 */
.auth-wrap {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-5);
  box-sizing: border-box;
}
.auth-card {
  width: 100%;
  max-width: 340px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-md, 12px);
  padding: 28px 24px;
  box-shadow: var(--shadow);
}
.auth-card h1 { margin: 0 0 4px; font-size: var(--fs-lg); }
.auth-sub { color: var(--muted); font-size: var(--fs-xs); margin-bottom: var(--sp-4); }
.auth-card label { display: block; font-size: var(--fs-xs); color: var(--muted); margin-bottom: 14px; }
.auth-card input[type=text],
.auth-card input[type=password] {
  width: 100%;
  box-sizing: border-box;
  margin-top: 5px;
  padding: 10px 12px;
  font-size: var(--fs-md);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--bg);
  color: inherit;
}
.auth-card input[type=text]:focus,
.auth-card input[type=password]:focus { outline: 2px solid var(--brand); outline-offset: -1px; }
.auth-card button {
  width: 100%;
  padding: 11px;
  margin-top: 6px;
  font-size: var(--fs-md);
  cursor: pointer;
  background: var(--brand);
  color: #fff;
  border: 0;
  border-radius: var(--radius-sm);
}
.auth-remember { display: flex !important; align-items: center; gap: 7px; font-size: var(--fs-sm); color: var(--muted); }
.auth-remember input { width: auto !important; margin: 0; }
.auth-err {
  background: var(--danger-bg);
  color: var(--danger);
  border: 1px solid var(--danger);
  border-radius: var(--radius-sm);
  padding: 9px 12px;
  font-size: var(--fs-sm);
  margin-bottom: var(--sp-4);
}
.auth-tip {
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 9px 12px;
  font-size: var(--fs-xs);
  color: var(--muted);
  margin-bottom: var(--sp-4);
  line-height: 1.6;
}
.auth-foot { margin-top: 14px; text-align: center; font-size: var(--fs-sm); color: var(--muted); }
.auth-foot a { color: var(--brand); text-decoration: none; }

/* databar 里的刷新按钮： utility 级，别用品牌蓝大按钮抢视线 */
.databar { flex-wrap: wrap; }
.databar button {
  padding: 3px 10px; font-size: var(--fs-xs);
  background: var(--surface-2); color: var(--txt-2);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.databar button:active { background: var(--surface-3); }

/* ---------- 带字段名的表单单元（.fld：字段名 + 输入框，竖排） ----------
   为什么需要：本站表单此前一律拿 placeholder 当字段名，一旦填入内容 placeholder 就消失，
   用户再也看不出这个框是什么 —— 实盘「设定初始资金」填上 402447 之后就是一串裸数字，
   旁边没有任何说明。用法：<label class="fld"><span>字段名</span><input …></label>，
   作为 .form-row 的 flex 子项参与换行。
   `.form-row .fld input { flex: none }` 是必需的一条：上面 619px 断点给
   `.form-row input[type=…]` 设了 `flex: 1 1 130px`，而 .fld 是**列方向**容器 ——
   flex-basis 在列方向指的是高度，输入框会被拉成一根 130px 高的柱子。
   同特异性(0,2,1)、靠源码顺序（本段在断点之后）压住它。 */
.fld { display: flex; flex-direction: column; gap: 2px; flex: 1 1 140px; min-width: 0; }
.fld > span { font-size: var(--fs-xs); color: var(--muted); line-height: 1.4; }
.form-row .fld input, .form-row .fld select { flex: none; width: 100%; }

/* 危险操作区：与常规设置之间用标题 + 上边框隔开。
   此前三个表单平铺堆叠，「实盘一键清零」夹在两个常规操作中间，层级只靠按钮颜色区分。 */
.danger-zone { margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid var(--line); }
.dz-title { font-size: var(--fs-xs); color: var(--danger); margin-bottom: var(--sp-1); }

/* ---------- 键盘可达性（2026-09-21 前端体检） ----------
   此前只有表单控件有 :focus 样式；链接/按钮/summary 在 Tab 导航时用浏览器默认
   焦点环，深色主题下几乎看不见焦点在哪。
   :focus-visible 只在**键盘**操作时触发，鼠标点击不会出现多余的框。 */
a:focus-visible, button:focus-visible, summary:focus-visible, [tabindex]:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
