/* ============================================================================
 *  1010.WANG · 组件层 —— 页面骨架 + 组件类 + 图表容器
 *  规格书: ../../../DESIGN.md §4 §5 §7
 *
 *  两条纪律:
 *    · 本文件不许出现任何字面色值。见到 # 或 rgb( 就是漏了一个令牌。
 *    · 移动端全 App 零 StyleSheet.create() —— 样式即令牌的组合。此处同理。
 *
 *  依赖 tokens.css → base.css 先于本文件引入。
 * ========================================================================== */

/* ============================================================================
 *  1 · 页面骨架 —— 四档容器,两道断点
 *
 *  移动端是「单列 + 24px 边距」的叙事,直译到 Web 就是一根居中列 —— 这对
 *  「一次只读一件事」的页面永远是对的,故它仍是默认档。
 *
 *  但**移动端是叙事,桌面端是版面**:前者的层次靠顺序表达,后者可以靠位置与
 *  面积表达。只留一根窄列等于把第二个维度扔了。故 720 之上再立一道 1120,
 *  且第二道断点做的是**版面重组**而非加宽 —— 规格见 DESIGN.md §4.4,落地在本
 *  文件末尾的 §19(整层收在一处,桌面语法一屏读完)。
 *
 *  档位由「文本列要多宽才读得下去」决定,不由屏幕决定(法则二):
 *    --page-form(480)   表单列        --page-narrow(720) 连续正文 · 叙事单列
 *    --page-wide(1120)  卡片墙 · 版面  420(--center)      无顶栏的登录屏
 * ========================================================================== */
.page {
  max-inline-size: var(--page-narrow);
  margin-inline: auto;
  padding-inline: var(--gutter);
  padding-block-start: calc(var(--header-clearance) + var(--gutter));
  padding-block-end: 64px;
}
.page--wide { max-inline-size: var(--page-wide); }
/* 表单档。不是断点的产物 —— 一个 672px 宽的密码框在 800 的窗口里同样荒谬,
   故它在每一个宽度上都成立,只是窄屏根本够不到这个上限。 */
.page--form { max-inline-size: var(--page-form); }
/* 无顶栏的整屏居中(登录页)。不复用 .page 的顶部让位 —— 那让位是为固定顶栏留的,
   这一屏没有顶栏,留出来就成了一段莫名其妙的空白。 */
.page--center {
  max-inline-size: 420px;
  min-block-size: 100dvh;
  display: grid;
  align-content: center;
  padding-block: var(--gutter);
}
/* 登录页零散件 */
.login-corner {
  position: fixed;
  inset-block-start: var(--gutter);
  inset-inline-end: var(--gutter);
  z-index: 10;
}
/* 品牌标记 —— 彭罗斯三角,与移动端 app-icon 同一份几何(img/logo.svg)。
   **它取 currentColor**:移动端浅深两套图标各画一份,Web 只留一份 SVG,
   由这里的 color 承担主题差异,恰好就是 --accent 的两个值。
   外部 <use> 引用的 <svg> 没有固有尺寸,长宽比必须显式给。 */
.brand-mark {
  color: var(--accent);
  aspect-ratio: 858 / 748;
  inline-size: auto;
}
.login-logo { block-size: 40px; margin-inline: auto; }
.login-form { margin-block-start: var(--card-pad-lg); }

/* 纵向节奏归容器所有,组件自身零外边距。
   三个都自带 display —— 原先只有 .stack 有,两个修饰符是纯 gap,单写 `class="stack--sm"`
   便什么也不做:七个调用点里六个漏了基类,节标题于是紧贴着卡片。一个必须成对出现
   才生效的类,迟早会被单独写下;让它单独也对,这类错误就不存在了。 */
.stack,
.stack--sm,
.stack--lg { display: grid; grid-template-columns: minmax(0, 1fr); align-content: start; }
.stack     { gap: 20px; }
.stack--sm { gap: 10px; }    /* 节标题 → 卡片 */
.stack--lg { gap: 24px; }
/* 两条收尾规则,一条管一个轴,同一个道理:**.stack 的语义是「块之间 20px」,
   它既不分配富余高度,也不参与宽度谈判。**

   · align-content: start —— grid 的默认值是 normal(即 stretch),auto 行会把多出来的
     高度平摊进去。于是 .stack 一旦被拉高(并列成两栏时矮的那栏必然被拉到等高),
     「节标题 → 卡片」的 10px 就悄悄变成三十几 px:两栏标题齐平而卡片错开。
   · grid-template-columns: minmax(0, 1fr) —— 隐式 auto 轨的下限是**内容的 min-content**,
     于是任何一个「本来打算自己横滚」的子孙(.scroll-x 里 white-space:nowrap 的宽表、
     窄屏塌成一排 chips 的侧栏导航)都会反过来把轨道撑开,把整页顶出横向滚动条 ——
     该滚的地方不滚,不该滚的 <body> 滚了。minmax 的 0 下限把这条路堵死:
     子元素最宽等于容器,超出的部分由它自己那层 overflow 负责。 */

/* 表单列 —— 法则二(约束宽度的是文本列,不是页面)的容器化身。
   `.page--form` 说的是「整页就是一个表单列」,这个说的是「宽页里的一列表单」。
   管理面板是 `.page--wide`,而它有五个面板其实是表单 —— 一个 880px 宽的
   `<select>` 与一个 672px 宽的密码框是同一种荒谬,和页面宽不宽无关。 */
.form-col { max-inline-size: var(--page-form); }

/* 卡片墙 —— 能塞几列塞几列。
   它问的不是「屏幕多宽」而是「一列要多宽才读得下去」,于是**不需要任何断点**,
   在每一个宽度上都对。gap 取 20px 与 .stack 同节奏:一堵卡片墙本身就是一「节」。
   min(100%, …) 是防窄屏溢出的哨兵 —— 没有它,--deck-min 会在比它还窄的视口里
   把网格撑出屏幕。 */
.deck {
  display: grid;
  gap: 20px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--deck-min, 320px)), 1fr));
}
/* 三档下限,与 --card-pad 的三档同一个说法:一列最窄能到多少,由内容定。
   sm = KPI 磁贴(一个标签一个数)· 默认 = 文章卡 · lg = 带整段正文的内容卡。 */
.deck--sm { --deck-min: 240px; }
.deck--lg { --deck-min: 420px; }

.row-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }

/* ============================================================================
 *  2 · 顶栏
 *
 *  移动端导航是贴底毛玻璃 TabBar,Web 不照搬 —— 桌面端的导航语法在顶部,
 *  这是平台差异而非设计不一致。但选中项背后那枚半透明丸照搬,它是签名件。
 * ========================================================================== */
.site-header {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 99;
  block-size: var(--header-clearance);
  background: var(--nav-glass);
  backdrop-filter: blur(40px);
  -webkit-backdrop-filter: blur(40px);
  border-block-end: 1px solid var(--glass-border);
}
.site-header__inner {
  max-inline-size: var(--page-wide);
  margin-inline: auto;
  padding-inline: var(--gutter);
  block-size: 100%;
  display: flex;
  align-items: center;
  gap: 12px;
}
.site-header__logo { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.site-header__logo .brand-mark { block-size: 28px; }
.site-header__tagline {
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
}
.site-header__nav {
  display: flex;
  align-items: center;
  gap: 2px;
  margin-inline-start: auto;
  overflow-x: auto;
  scrollbar-width: none;
}
.site-header__nav::-webkit-scrollbar { display: none; }
.site-header__nav a {
  font-size: 13px;
  font-weight: 600;
  color: var(--secondary);
  padding: 8px 14px;
  border-radius: var(--r-pill);
  white-space: nowrap;
  transition: color var(--dur-standard) var(--ease),
              background-color var(--dur-standard) var(--ease);
}
.site-header__nav a:hover { color: var(--fg); text-decoration: none; }
/* 那枚丸必须半透明 —— 实色压在毛玻璃条上会糊掉整条通栏的通透 */
.site-header__nav a[aria-current="page"] {
  color: var(--accent);
  background: var(--nav-pill);
}
.site-header__actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.site-header__profile {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  font-weight: 600;
  color: var(--fg);
}
.site-header__profile:hover { text-decoration: none; }

/* 汉堡:仅窄屏出现。三条杠用一个元素 + 两个伪元素画,不引第三方图标 ——
   gravity-ui 那套里没有汉堡,为一个形状去混进第二个图标家族不划算。

   **两条规则都带 .site-header 前缀**:这枚钮同时是 .icon-btn(display:grid),
   两者同为单类特异性,谁靠后谁赢 —— .icon-btn 在本文件第 5 节,于是桌面端的
   display:none 被无声吃掉,汉堡常驻左上角。落位是顶栏的事,得压过通用钮外观,
   这一级特异性是**声明该由谁做主**,不是靠源码顺序碰运气。 */
.site-header .site-header__burger { display: none; }
.burger,
.burger::before,
.burger::after {
  display: block;
  inline-size: 16px;
  block-size: 1.5px;
  border-radius: var(--r-pill);
  background: currentColor;
  transition: transform var(--dur-standard) var(--ease);
}
.burger::before,
.burger::after { content: ''; position: absolute; }
.burger::before { transform: translateY(-5px); }
.burger::after  { transform: translateY(5px); }
.site-header__burger { position: relative; }
@media (max-width: 719px) {
  .site-header__tagline { display: none; }
  .site-header .site-header__burger { display: grid; place-items: center; }
  .site-header__nav {
    position: fixed;
    inset-block-start: var(--header-clearance);
    inset-inline: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
    padding: 12px var(--gutter) 20px;
    background: var(--nav-glass);
    backdrop-filter: blur(40px);
    -webkit-backdrop-filter: blur(40px);
    border-block-end: 1px solid var(--glass-border);
    overflow: hidden;
    display: none;
  }
  .site-header__nav--open { display: flex; }
}

/* ============================================================================
 *  3 · 卡片 —— 全 App 每一个内容块都由它框住
 * ========================================================================== */
.card {
  border-radius: var(--r-card);
  background: var(--surface);
  padding: var(--card-pad);
  box-shadow: var(--shadow-card);
  /* 禁止 overflow: hidden —— 会裁掉 box-shadow 的外溢。
     需要裁剪的子元素自己包一层圆角容器。 */
}
[data-theme="dark"] .card {
  border: 1px solid var(--hairline);   /* 深色无阴影,层次由描边承担 */
}
.card--sm       { padding: var(--card-pad-sm); }
.card--lg       { padding: var(--card-pad-lg); }
.card--flush    { padding: 0; }
/* 「这一屏的主角」只能有一个 */
.card--featured { border: 1.5px solid var(--featured-border); }

/* ============================================================================
 *  4 · 列表 —— 唯一的列表
 *
 *  分隔线用相邻选择器自动生成,不写 .divider 元素:首尾两端不该有线,
 *  靠结构消除这个特殊情况比靠条件判断优雅。
 * ========================================================================== */
.list-card {
  padding-inline: 18px;
  padding-block: 0;
}
.list-row {
  display: flex;
  align-items: center;
  gap: 13px;
  padding-block: 14px;
  inline-size: 100%;
  text-align: start;
}
.list-row + .list-row { border-block-start: 1px solid var(--hairline); }
.list-row--faded { opacity: 0.45; }

.list-row__main     { flex: 1; min-inline-size: 0; }
/* 标题/副标题/主值/副值一律 block:它们是**上下两行**,不是一行里的两段。
   页面模板用 <div>,JS 生成的行用 <span>,后者是行内元素 —— 于是「累计收益」与
   「当前市值 − 累计投入」在同一行里挤着排,margin-block-start 也被行内盒静默忽略。
   排版规约写在类上,元素用什么标签便不再是它的自由度。 */
.list-row__title    { display: block; font-size: 13.5px; font-weight: 700; color: var(--fg); }
.list-row__subtitle { display: block; font-size: 11px; color: var(--faint);
                      margin-block-start: 2px; }
.list-row__end      { text-align: end; flex-shrink: 0; }
.list-row__value    { display: block; font-size: 15px; font-weight: 800; color: var(--fg);
                      font-variant-numeric: tabular-nums; }
.list-row__value--with-sub { font-size: 13.5px; }     /* 有副值时让位 */
.list-row__subvalue { display: block; font-size: 11px; color: var(--muted);
                      margin-block-start: 2px; font-variant-numeric: tabular-nums; }
.list-row__chevron  { color: var(--chevron); flex-shrink: 0; }

.list-row__value--up   { color: var(--up); }
.list-row__value--down { color: var(--down); }

/* ============================================================================
 *  5 · 按钮
 * ========================================================================== */
.btn {
  block-size: 52px;
  border-radius: var(--r-btn);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding-inline: 24px;
  font-size: 15px;
  font-weight: 700;
  transition: transform var(--dur-gentle) var(--ease);
}
/* 进快出慢:按下要跟手(150ms),松手要留余韵(400ms)。两个方向同速会显得机械。 */
.btn:active { transform: scale(0.97); transition-duration: var(--dur-fast); }
.btn:disabled { opacity: 0.5; pointer-events: none; }
.btn:hover { text-decoration: none; }

.btn--primary {
  background: var(--accent-gradient);
  color: var(--accent-fg);
  box-shadow: var(--shadow-accent-btn);   /* 紫钮的光晕主题无关 —— 是按钮自身的材质 */
}
.btn--secondary {
  background: var(--surface);
  color: var(--fg);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-soft);
}
[data-theme="dark"] .btn--secondary { background: var(--surface-secondary); }

.btn--danger { background: var(--danger); color: var(--danger-fg); }
.btn--block  { inline-size: 100%; }

/* 圆钮两档:44 主操作 / 36 次操作(刷新等) */
.icon-btn {
  inline-size: 44px;
  block-size: 44px;
  border-radius: var(--r-pill);
  display: grid;
  place-items: center;
  background: var(--surface);
  color: var(--body);
  box-shadow: var(--shadow-soft);
  transition: transform var(--dur-fast) var(--ease);
}
[data-theme="dark"] .icon-btn { background: var(--surface-secondary); }
.icon-btn:active { transform: scale(0.9); }   /* 元素越小,缩放越狠 */
.icon-btn--sm { inline-size: 36px; block-size: 36px; }

/* ============================================================================
 *  6 · tint 色块 —— 底色与前景永远同族
 *
 *  让调用点自由搭配 = 迟早出现紫底绿字。故此处是六个 modifier,不给自由度。
 * ========================================================================== */
.icon-square {
  inline-size: 34px;
  block-size: 34px;
  border-radius: var(--r-icon);
  display: grid;
  place-items: center;
  flex-shrink: 0;
  background: var(--accent-tint);
  color: var(--accent);
}
.icon-square--round { border-radius: var(--r-pill); }
.icon-square--label { font-size: 11px; font-weight: 700; }

/* 六个基名走非方向语义(成功/警示/完成),规则卡与设置菜单用这一套 */
.icon-square--purple { background: var(--accent-tint);  color: var(--accent); }
.icon-square--green  { background: var(--success-tint); color: var(--success); }
.icon-square--red    { background: var(--danger-tint);  color: var(--danger); }
.icon-square--blue   { background: var(--info-tint);    color: var(--info); }
.icon-square--amber  { background: var(--warning-tint); color: var(--warning); }
.icon-square--gold   { background: var(--gold-tint);    color: var(--gold); }

/* 方向语义是上面两个的别名,不是新颜色 —— 跌绿涨红。
   两套名字必须并存:--green 在规则卡上是「完成」,在行情上才是「跌」。
   合并成一个名字,就等于让「成功」和「下跌」变成同一个概念。 */
.icon-square--down { background: var(--down-tint); color: var(--down); }
.icon-square--up   { background: var(--up-tint);   color: var(--up); }

/* ============================================================================
 *  7 · 胶囊
 * ========================================================================== */
.stat-pill {
  block-size: 34px;
  border-radius: var(--r-pill);
  padding-inline: 16px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.stat-pill--surface { background: var(--surface); box-shadow: var(--shadow-soft); }
[data-theme="dark"] .stat-pill--surface { background: var(--surface-secondary); }
/* tint 变体无阴影。down 走更淡的那档 —— 绿底比红底更容易糊,不对称是刻意的。 */
.stat-pill--down { background: var(--down-tint-soft); }
.stat-pill--up   { background: var(--up-tint); }

.stat-pill__label { font-size: 12px; color: var(--muted); }
.stat-pill__value { font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums; }
.stat-pill--down .stat-pill__value { color: var(--down); }
.stat-pill--up   .stat-pill__value { color: var(--up); }

.tint-badge {
  block-size: 22px;
  border-radius: var(--r-pill);
  padding-inline: 10px;
  display: inline-flex;
  align-items: center;
  font-size: 11px;
  font-weight: 700;
}
.tint-badge--down { background: var(--down-tint); color: var(--down); }
.tint-badge--up   { background: var(--up-tint);   color: var(--up); }
.tint-badge--gold { background: var(--gold-tint); color: var(--gold); }

/* ============================================================================
 *  8 · 分段控件
 *
 *  移动端的丸是四条弹簧夹出来的果冻。Web 用 CSS transition 是有意的降级:
 *  纯 CSS 拿不到手指速度,硬模拟只会得到一个假的弹性。
 *  宁可诚实地滑,不要假装有物理。
 * ========================================================================== */
.segmented { position: relative; display: flex; }
.segmented__pill {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  block-size: 100%;
  pointer-events: none;
  transition: transform var(--dur-standard) var(--ease),
              width     var(--dur-standard) var(--ease);
}
.segmented__item {
  position: relative;
  z-index: 1;
  transition: color var(--dur-standard) var(--ease);
  white-space: nowrap;
}
.segmented__item[aria-selected="true"] { color: var(--accent); }

/* chips:透明轨,反色实心丸 */
.segmented--chips { gap: 8px; justify-content: center; }
.segmented--chips .segmented__item {
  block-size: 30px;
  border-radius: var(--r-pill);
  padding-inline: 14px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--muted);
}
.segmented--chips .segmented__pill {
  background: var(--chip-active-bg);
  border-radius: var(--r-pill);
}
.segmented--chips .segmented__item[aria-selected="true"] { color: var(--chip-active-fg); }

/* segment:实轨内嵌丸 */
.segmented--segment {
  border-radius: var(--r-seg);
  background: var(--surface-secondary);
  padding: 3px;
}
.segmented--segment .segmented__item {
  flex: 1;
  block-size: 34px;
  border-radius: var(--r-icon);
  font-size: 13px;
  font-weight: 600;
  color: var(--secondary);
}
.segmented--segment .segmented__pill {
  background: var(--accent-tint);
  border-radius: var(--r-icon);
  block-size: calc(100% - 6px);
  inset-block-start: 3px;
}

/* 竖排导航变体(管理后台侧栏);窄屏塌成横向滚动 chips */
.segmented--vertical { flex-direction: column; gap: 2px; }
.segmented--vertical .segmented__item {
  block-size: 42px;
  border-radius: var(--r-inner);
  padding-inline: 14px;
  font-size: 13.5px;
  font-weight: 700;
  color: var(--secondary);
  text-align: start;
  display: flex;
  align-items: center;
}
.segmented--vertical .segmented__item[aria-selected="true"] {
  color: var(--accent);
  background: var(--accent-tint);
}
/* 窄屏塌成一排横滚 chips。横滚本身归 .scroll-x 管(挂在同一元素上)——
   此前这里自己写了一遍 overflow-x 并把滚动条藏掉,于是七项里看得见五项,
   而「还有两项」这件事没有任何东西告诉手指。 */
@media (max-width: 719px) {
  .segmented--vertical { flex-direction: row; }
  .segmented--vertical .segmented__item { block-size: 34px; flex-shrink: 0; }
}

/* ============================================================================
 *  9 · 表单
 * ========================================================================== */
.field {
  block-size: 52px;
  inline-size: 100%;
  border-radius: var(--r-field);
  background: var(--field-bg);
  color: var(--field-fg);
  border: 1px solid var(--field-border);
  box-shadow: var(--field-shadow);
  padding-inline: 20px;
  font-size: 15px;
}
.field::placeholder { color: var(--field-placeholder); }
.field:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
  border-color: var(--focus);
}
textarea.field { block-size: auto; padding-block: 14px; line-height: 1.5; }
select.field { appearance: none; padding-inline-end: 44px; }

.field-label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  font-weight: 500;
  color: var(--muted);
  margin-block-end: 8px;
}
.field-label > svg { color: var(--accent); }
.field-group { display: block; }

/* ============================================================================
 *  10 · 大金额
 *
 *  两段字号差 18px,纯基线对齐会让小数看起来在往下掉。
 *  ¥ 与小数同为 26px,同沉 3px —— 只沉一边会让二者不在一条线上。
 * ========================================================================== */
.money {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.money__symbol { font-size: 26px; font-weight: 700; margin-inline-end: 2px; margin-block-end: 3px; }
.money__int    { font-size: 44px; font-weight: 800; letter-spacing: -1px; }
.money__frac   { font-size: 26px; font-weight: 700; margin-block-end: 3px; }

/* ============================================================================
 *  11 · 进度条两档
 *
 *  transform-origin: left 是硬性要求。裸 scaleX 从中心展开,
 *  进度条会像开合的嘴。
 * ========================================================================== */
.progress {
  block-size: 6px;
  border-radius: var(--r-pill);
  background: var(--gray-bar);
  overflow: hidden;
}
.progress--fat { block-size: 38px; border-radius: var(--r-bar); }
.progress__fill {
  block-size: 100%;
  inline-size: 100%;
  border-radius: inherit;
  transform-origin: left;
  transform: scaleX(var(--p, 0));
  transition: transform var(--dur-fill) var(--ease-out);
  background: var(--accent-gradient);
}
.progress__fill--down { background: var(--success-bright); }

/* 胖轨内的文字层:压在填充之上,不随填充缩放 */
.progress--fat { position: relative; display: flex; align-items: center; }
.progress__label {
  position: absolute;
  inset-inline: 18px;
  display: flex;
  justify-content: space-between;
  font-size: 13px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* ============================================================================
 *  12 · KPI 磁贴两变体
 *
 *  标签行固定 22px 高:并排两张卡,一张有配件一张没有时,不锁高就会让
 *  两张卡的数值错开一两像素 —— 最刺眼的那种不齐。
 * ========================================================================== */
.stat-card { padding: var(--card-pad-sm); }
.stat-card__head {
  block-size: 22px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.stat-card__label { font-size: 12px; color: var(--muted); }
.stat-card__value {
  margin-block-start: 6px;
  font-size: 22px;
  font-weight: 800;
  color: var(--fg);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* 金额换行会把卡片撑高,一排卡里只要有一个换行整排就参差。宁可小一号。 */
.stat-card__value--long { font-size: 19px; }
.stat-card__sub {
  margin-block-start: 4px;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--muted);
}
.stat-card__sub--down { color: var(--down); }
.stat-card__sub--up   { color: var(--up); }

.stat-card--compact { padding: 15px 16px; }
.stat-card--compact .stat-card__label { font-size: 11.5px; }
.stat-card--compact .stat-card__value { margin-block-start: 4px; font-size: 21px; }
.stat-card__unit { font-size: 12px; font-weight: 600; color: var(--faint); }

/* ============================================================================
 *  13 · 其余原语
 * ========================================================================== */
.section-title { font-size: 14px; font-weight: 800; color: var(--fg); margin: 0; }
.page-title    { font-size: 26px; font-weight: 800; color: var(--fg); }
.page-subtitle { font-size: 13px; color: var(--muted); margin-block-start: 4px; }
.text-center   { text-align: center; }

/* 卡首行:图标 + 标题。与 .list-row 同一套间距,但卡片自己已有内距,不再叠纵向 padding */
.card__head { display: flex; align-items: center; gap: 13px; }
.card__body { margin-block-start: 12px; font-size: 13px; line-height: 1.7; color: var(--body); }
.card__body + .inset { margin-block-start: 12px; }
.card__foot { margin-block-start: 16px; }

/* 重验失败提示条 —— 结构性规约:已经拿到的数据不该因为一次刷新失败就消失。
   取数一律三分支且数据优先,不许有第四个 null 分支。 */
.stale-notice {
  display: flex;
  align-items: center;
  gap: 8px;
  border-radius: var(--r-inner);
  background: var(--warning-tint);
  color: var(--warning-soft-fg);
  padding: 8px 12px;
  font-size: 11.5px;
}

/* 三分支的视图侧。缺一件就会有人拿 null 去补那个位置。 */
.screen-state {
  display: grid;
  place-items: center;
  gap: 12px;
  padding-block: 64px;
  text-align: center;
  color: var(--muted);
  font-size: 13px;
}
.screen-state__icon { color: var(--chevron); }
.empty-list { padding-block: 40px; }

.spinner {
  inline-size: 22px;
  block-size: 22px;
  border-radius: var(--r-pill);
  border: 2px solid var(--gray-bar);
  border-block-start-color: var(--accent);
  animation: spin 800ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* 头像 SVG 自身背景是 transparent(avatar.js:50),圆盘底色由这一层提供 ——
   动这条规则会让全站头像失去底色。这是一条 JS↔CSS 的隐式契约。 */
.avatar {
  border-radius: var(--r-pill);
  background: var(--accent-tint);
  border: 1px solid var(--hairline);
  overflow: hidden;
  flex-shrink: 0;
  display: block;
}
/* 槽(<span data-avatar-slot>)与它里面的 SVG 都必须铺满圆盘。Humation 吐出的
   SVG 自带 width/height="80" 属性,不铺满就按 80px 画,再被上面那条 overflow:hidden
   从中间切掉一块 —— 头像「显示不全」即由此而来。槽自身是行内元素,不摊成块级
   还会在底下留一条基线间隙。 */
.avatar > *,
.avatar svg { display: block; inline-size: 100%; block-size: 100%; }

.avatar--header { inline-size: 32px; block-size: 32px; }
.avatar--lg     { inline-size: 40px; block-size: 40px; }
.avatar--xl     { inline-size: 72px; block-size: 72px; }

.divider { block-size: 1px; background: var(--hairline); border: 0; }

.hint { font-size: 11.5px; color: var(--faint); }

.settings-avatar { margin-inline: auto; margin-block-end: 12px; }

/* ---- 头像构建器(avatar.html) ---- *
 * 预览固定宽、部件网格占余下空间。窄屏塌成单列(唯一断点,不另立)。 */
.avatar-layout { display: grid; gap: 12px; }
@media (min-width: 720px) {
  .avatar-layout { grid-template-columns: 300px 1fr; align-items: start; }
}
/*
 * 原先这几条散在 avatar.html 的内联 <style> 里,不在任何 .css 文件中 ——
 * 那是最容易在改版时被漏掉的一类样式。收进来。 */
.avatar-preview {
  inline-size: 220px;
  block-size: 220px;
  margin-inline: auto;
  border-radius: var(--r-card);
  background: var(--surface-secondary);
  display: grid;
  place-items: center;
  overflow: hidden;   /* 这里可以裁:它不是 .card,没有投影要保 */
}
.avatar-preview svg { inline-size: 100%; block-size: 100%; }

.avatar-part-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
  gap: 10px;
}
.avatar-part {
  border: 2px solid transparent;
  border-radius: var(--r-inner);
  padding: 4px;
  background: var(--surface-secondary);
  cursor: pointer;
  transition: border-color var(--dur-standard) var(--ease),
              transform var(--dur-fast) var(--ease);
}
.avatar-part:active { transform: scale(0.95); }   /* 小格子:cell 档 */
.avatar-part--active { border-color: var(--accent); }
.avatar-part img { inline-size: 100%; block-size: auto; display: block; }

.avatar-color-row { display: flex; flex-wrap: wrap; gap: 14px; }
.avatar-color {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  font-size: 11.5px;
  color: var(--muted);
  cursor: pointer;
}
.avatar-color input[type="color"] {
  inline-size: 44px;
  block-size: 44px;
  border: 1px solid var(--border);
  border-radius: var(--r-icon);
  background: none;
  cursor: pointer;
  padding: 0;
}

/* ---- 文章卡(blog.html) ---- */
.post { padding: 0; overflow: visible; }   /* 图要裁,但裁在内层 —— .card 不许 overflow:hidden */
.post__img {
  display: block;
  aspect-ratio: 16 / 9;
  border-start-start-radius: var(--r-card);
  border-start-end-radius: var(--r-card);
  overflow: hidden;
  background: var(--surface-secondary);
}
.post__img img { inline-size: 100%; block-size: 100%; object-fit: cover; }
.post__body { padding: var(--card-pad); }
.post__tags { display: flex; gap: 6px; flex-wrap: wrap; }
.post__title { font-size: 15px; font-weight: 700; margin-block-start: 8px; }
.post__title a { color: var(--fg); }
.post__meta {
  display: flex;
  gap: 14px;
  margin-block-start: 10px;
  font-size: 11px;
  color: var(--faint);
}
.post__meta span { display: inline-flex; align-items: center; gap: 4px; }
.post__admin {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-block-start: 14px;
  padding-block-start: 12px;
  border-block-start: 1px solid var(--hairline);
}
.post__admin button {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  block-size: 30px;
  padding-inline: 12px;
  border-radius: var(--r-pill);
  background: var(--surface-secondary);
  color: var(--secondary);
  font-size: 12px;
  font-weight: 600;
}
.post__admin button:active { transform: scale(0.95); }
.post__admin .is-danger { color: var(--danger); }

/* ---- 横向可滚区域 ---- *
 * 语义是「一块横向可滚区域」,不是「表格的包装」—— 窄屏下的侧栏导航是同一件东西。
 * 一个原语两处用,少一个特殊情况。
 *
 * **边界必须诚实。** 不告诉手指右边还有内容,横滚就等于不存在:滚动条在触摸端
 * 根本不显示。两层 surface 盖片走 background-attachment:local(跟着内容走,滚到
 * 尽头即自行让位),两层阴影走 scroll(钉在视口边,只在真能滚时露出来)——
 * 纯 CSS,不需要 JS 测宽,也不需要在每个消费点各挂一个 scroll 监听。
 *
 * 它**必须是 .card 的子元素而不是同一个元素**:overflow-x:auto 会把 overflow-y
 * 的计算值一并变成 auto,那正是 .card 注释里明令禁止的「裁掉 box-shadow 外溢」。
 * 故用法恒为 .card.card--flush > .scroll-x,投影与裁剪各归其位;圆角靠 inherit
 * 从卡片取,不再写第二个数。 */
.scroll-x {
  overflow-x: auto;
  scrollbar-width: thin;
  border-radius: inherit;
  background:
    linear-gradient(to right, var(--surface), transparent) left  / 20px 100% no-repeat local,
    linear-gradient(to left,  var(--surface), transparent) right / 20px 100% no-repeat local,
    radial-gradient(farthest-side at 0    50%, color-mix(in srgb, var(--fg) 10%, transparent), transparent) left  / 10px 100% no-repeat scroll,
    radial-gradient(farthest-side at 100% 50%, color-mix(in srgb, var(--fg) 10%, transparent), transparent) right / 10px 100% no-repeat scroll;
}

/* ---- 表格 —— Web 独有原语 ---- *
 * 移动端没有表格,所以 DESIGN.md §5 里也没有。但管理后台的持仓流水是七列数据,
 * 硬塞进 .list-row 会让每一行都要折叠成两行,反而更难比对 —— **数据本来是表状的**。
 * 仅用于 .page--wide 的管理面板;面向普通用户的页面一律用 .list-card。 */
.table {
  inline-size: 100%;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.table th {
  text-align: start;
  font-size: 11px;
  font-weight: 700;
  color: var(--muted);
  padding: 10px 12px;
  border-block-end: 1px solid var(--border);
}
.table td {
  padding: 11px 12px;
  color: var(--body);
  border-block-end: 1px solid var(--hairline);
}
.table tbody tr:last-child td { border-block-end: 0; }
.table td:first-child { color: var(--fg); font-weight: 700; }

/* 表格的移动语法。上面那段注释只写了上半句:数据本来是表状的 —— 下半句是
   **表状只在能横向扫视的眼睛面前成立,而手机只有一根竖直的注意力**。
   列名由 td 的 data-label 就地供给,于是 JS 仍然只有一套渲染路径,折不折叠是
   CSS 的事 —— 若靠脚本按视口宽度切两种 DOM,那个分支从此要维护两遍。
   只给**语义已知的定列表**用(持仓流水六列);列数由外部文件决定的表见下一条。 */
@media (max-width: 719px) {
  .table--stack,
  .table--stack tbody,
  .table--stack tr,
  .table--stack td { display: block; }
  .table--stack thead { display: none; }
  .table--stack tr { padding: 12px 16px; }
  .table--stack tr + tr { border-block-start: 1px solid var(--hairline); }
  .table--stack td {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    padding: 3px 0;
    border: 0;
    white-space: normal;
  }
  .table--stack td::before {
    content: attr(data-label);
    color: var(--muted);
    font-size: 11px;
    font-weight: 700;
  }
  .table--stack td:first-child { font-size: 13.5px; }   /* 折成卡后,首格是这张卡的标题 */
  .table--stack td:first-child::before { display: none; }
}

/* 比例条 —— 「这一项占最大项的多少」。用于全员定投一览那样的**同向比例**。
   带方向的量(盈亏)不归它管:那需要一根零轴,而零轴与「轨道 = 容量」的语法
   互斥,见下面的 .tornado。名字刻意不叫 share:一个叫 share 的类迟早要去表达
   别的比例,那就是熵的起点。 */
/* 两个 display: block 不是凑数,是这条原语能否显形的前提 —— 与 .list-row__* 那段
   注释同一个坑:调用点是 JS 生成的 <span>,行内盒既不吃 block-size 也不吃 overflow,
   而 **transform 根本不施于非替换行内元素**,scaleX 会被静默忽略 —— 漏掉这两行,
   条就是零高零宽,而且不报任何错。 */
.meter {
  display: block;
  block-size: 6px;
  border-radius: var(--r-pill);
  background: var(--gray-bar);
  overflow: hidden;
  margin-block-start: 6px;
}
.meter__fill {
  display: block;
  block-size: 100%;
  border-radius: inherit;
  background: var(--accent-gradient);
  transform-origin: left;
  transform: scaleX(var(--p, 0));
  transition: transform var(--dur-fill) var(--ease-out);
}
/* ---- 正文排版(article.html) ---- *
 * 富文本由 Quill 产出,标签不受我们控制,只能按标签给样式。
 * 这是全站唯一一处「按标签而非按 class」的规则集,原因在此,不要扩散。 */
.prose { font-size: 15px; line-height: 1.8; color: var(--body); }
.prose > * + * { margin-block-start: 1em; }
.prose h1, .prose h2, .prose h3 { color: var(--fg); margin-block-start: 1.6em; }
.prose h1 { font-size: 22px; }
.prose h2 { font-size: 19px; }
.prose h3 { font-size: 16px; }
.prose img { border-radius: var(--r-inner); margin-inline: auto; }
.prose a { color: var(--link); }
.prose ul, .prose ol { padding-inline-start: 1.4em; }
.prose ul { list-style: disc; }
.prose ol { list-style: decimal; }
.prose li + li { margin-block-start: 0.4em; }
.prose blockquote {
  border-inline-start: 3px solid var(--accent);
  padding-inline-start: 14px;
  color: var(--secondary);
}
.prose code {
  background: var(--surface-secondary);
  border-radius: 6px;
  padding: 2px 6px;
  font-size: 0.9em;
}
.prose pre {
  background: var(--surface-secondary);
  border-radius: var(--r-inner);
  padding: 14px;
  overflow-x: auto;
}
.prose hr { border: 0; border-block-start: 1px solid var(--hairline); }

.article-hero {
  inline-size: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: var(--r-card);
}
.article-title { margin-block-start: 10px; }
.back-link {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
}
.back-link:hover { color: var(--fg); text-decoration: none; }

/* ---- 管理面板骨架(profile.html) ---- *
 * 两处 minmax(0, …) 与 .stack 那条同一个理由:面板列里装着宽表与横滚导航,
 * 1fr 的隐含下限是 auto(即 min-content),它们会把整页撑出横向滚动条。 */
.admin-layout { display: grid; gap: 16px; grid-template-columns: minmax(0, 1fr); }
@media (min-width: 720px) {
  .admin-layout { grid-template-columns: 220px minmax(0, 1fr); align-items: start; }
  .admin-nav { position: sticky; inset-block-start: calc(var(--header-clearance) + 16px); }
}
.admin-nav { padding-block: 8px; }

/* ---- 概览页零散件(data.html) ---- */
.overview-head { display: flex; align-items: center; gap: 12px; }
.overview-head__who { flex: 1; min-inline-size: 0; }
.overview-label {
  font-size: 12px;
  color: var(--muted);
  letter-spacing: 3px;   /* 全站唯一另一处字距,与 44px 大金额的 -1px 各司其职 */
}
.money { margin-block-start: 6px; }
.overview-pill { margin-block-start: 12px; }
.hint + .overview-pill,
.overview-pill + .hint { margin-block-start: 10px; }

/* 进度行:整行可点,两行之间一道发丝线 */
.progress-row { display: block; inline-size: 100%; padding-block: 10px; text-align: start; }
.progress-row + .progress-row { border-block-start: 1px solid var(--hairline); }
.progress-row__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-block-end: 8px;
}

/* 抽屉里的事实清单(此指标本无历史时代替图区) */
.facts { display: grid; gap: 0; }
.facts__row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding-block: 12px;
  font-size: 13px;
  color: var(--body);
}
.facts__row + .facts__row { border-block-start: 1px solid var(--hairline); }
.facts__value { font-weight: 700; color: var(--fg); font-variant-numeric: tabular-nums; }
/* 跌绿涨红,与 .list-row__value--* 同一对令牌。**刻意不去借那两个类** ——
   一个叫 list-row__ 的名字出现在抽屉里,读的人会去找那一行在哪。 */
.facts__value--up   { color: var(--up); }
.facts__value--down { color: var(--down); }

/* 抽屉底部统计栏 */
.sheet__stats {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin-block-start: 16px;
  padding-block-start: 14px;
  border-block-start: 1px solid var(--hairline);
}
.sheet__stat { text-align: center; flex: 1; }
.sheet__stat-label { font-size: 11px; color: var(--muted); }
.sheet__stat-value {
  font-size: 15px;
  font-weight: 800;
  color: var(--fg);
  font-variant-numeric: tabular-nums;
  margin-block-start: 2px;
}

/* 免责脚注:全站唯一一份措辞。它顶到内容末尾,不参与 .stack 的节奏 */
.disclaimer {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding-block-start: 8px;
  color: var(--faint);
  font-size: 11.5px;
  line-height: 1.6;
}
.disclaimer > svg { flex-shrink: 0; margin-block-start: 2px; }

/* 卡内嵌块 */
.inset {
  border-radius: var(--r-seg);
  background: var(--surface-secondary);
  padding: var(--card-pad-sm);
}

/* 卡内的编号步骤(结算节点、持仓成分)—— 内嵌行语言,不另起一套列表 */
.step-list { display: grid; gap: 8px; }
.step-list__item {
  display: flex;
  align-items: baseline;
  gap: 10px;
  font-size: 13px;
  color: var(--body);
}
.step-list__no {
  font-size: 11px;
  font-weight: 800;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
.step-list__qty {
  font-size: 12px;
  font-weight: 700;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  margin-inline-start: auto;
}

/* ============================================================================
 *  14 · 抽屉 / 弹层
 *
 *  移动端是贴底 BottomSheet;Web 用原生 <dialog>(自带焦点陷阱与 Esc 关闭)。
 *  窄屏贴底、宽屏居中,是同一个组件的两种落位。
 * ========================================================================== */
.sheet {
  border: 0;
  padding: 0;
  background: var(--overlay);
  color: var(--fg);
  box-shadow: var(--shadow-overlay);
  max-block-size: 68dvh;
  inline-size: 100%;
  max-inline-size: none;
  margin: auto auto 0;
  border-start-start-radius: var(--r-card);
  border-start-end-radius: var(--r-card);
  overflow: hidden;
}
/* 两档高度的判据是「此指标是否声明了 facts」,不是序列长度 —— 若按长度判,
   一次取数失败会让本该有图的指标掉进事实档。 */
.sheet--facts { max-block-size: 50dvh; }
.sheet::backdrop { background: var(--backdrop); }
/* flex 列不是排版趣味,是 .sheet__body 那行 overflow-y: auto 能否成立的前提 ——
   块级布局里子元素拿不到确定高度,滚不起来的内容会被上面 overflow: hidden 直接
   切掉,切掉的恰好是最后几行(事实档一长必撞,窄屏尤甚)。
   **必须写在 [open] 上**:作者样式压过 UA 源,一条无条件的 display 会把 UA 那句
   `dialog:not([open]){display:none}` 顶掉 —— 关着的抽屉会摊在页尾。 */
.sheet[open] {
  display: flex;
  flex-direction: column;
  animation: sheet-in var(--dur-standard) var(--ease-out);
}
@keyframes sheet-in { from { transform: translateY(24px); opacity: 0; } }

@media (min-width: 720px) {
  .sheet {
    inline-size: min(var(--page-narrow), 92vw);
    margin: auto;
    border-radius: var(--r-card);
  }
}
.sheet__grabber {
  inline-size: 36px;
  block-size: 4px;
  border-radius: var(--r-pill);
  background: var(--chevron);
  margin: 10px auto 0;
}
/* min-block-size: 0 —— flex 项默认不肯缩到内容以下,不给这一行 auto 永不触发 */
.sheet__body { padding: var(--card-pad-lg); overflow-y: auto; min-block-size: 0; }
.sheet__title { font-size: 15px; font-weight: 700; color: var(--fg); }
.sheet__value {
  font-size: 28px;
  font-weight: 800;
  color: var(--fg);
  font-variant-numeric: tabular-nums;
  margin-block-start: 2px;
}
.sheet__sub { font-size: 11.5px; color: var(--muted); margin-block-start: 2px; }
/* 与汉堡同一类撞车:.press 也声明 position:relative(它的按压高亮层要个定位上下文),
   在本文件第 16 节、比这里靠后,单类对单类便由它胜出 —— 关闭钮于是退回文档流,
   带着两个 inset 相对偏移,落在抓手下方偏左的位置。加一级 .sheet 前缀申明落位归
   抽屉管。**新增绝对定位且带 .press 的元素,一律照此提一级。** */
.sheet .sheet__close { position: absolute; inset-block-start: 14px; inset-inline-end: 14px; }

/* ============================================================================
 *  15 · 图表
 *
 *  一套视觉语言,三种尺寸。形态恒定,只有画布大小是变量。
 *  线宽属视觉语言而非尺寸档:换档只换画布,笔触粗细全 App 恒定 ——
 *  让线宽跟着尺寸缩放,九条线并列时会呈现九种粗细。
 * ========================================================================== */
.spark         { inline-size: 54px; block-size: 22px; }
.spark--ticker { inline-size: 70px; block-size: 30px; }
/* 迷你折线是纯装饰,且总是挂在可点的列表行中段 —— SVG 会吃掉落在它身上的点击,
   不摘掉命中测试就会「点那条线整行没反应」。
   大图不在此列:它自己就是交互对象(scrub 查数),摘了就没法用。 */
.spark { pointer-events: none; }

/* 画布高由令牌独占:CSS 拿它设 svg 高度,chart.js 拿它算 viewBox —— 同一个变量
   喂两处,不再是「两份必须相等的常量」。且自定义属性会继承,于是换一档画布只需
   在祖先上重定义一次 --chart-h(见 §19 的 .dash__hero),CSS 与几何同时跟上。 */
.chart { position: relative; inline-size: 100%; }
.chart__svg { inline-size: 100%; block-size: var(--chart-h); display: block; touch-action: pan-y; }
.chart__tip {
  position: absolute;
  z-index: 2;
  transform: translate(-50%, -100%);
  background: var(--overlay);
  box-shadow: var(--shadow-overlay);
  border-radius: var(--r-inner);
  padding: 6px 10px;
  font-size: 11.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease);
}
[data-theme="dark"] .chart__tip { border: 1px solid var(--hairline); }
.chart__tip--on { opacity: 1; }
.chart__legend {
  display: flex;
  justify-content: center;
  gap: 16px;
  font-size: 11.5px;
  color: var(--muted);
}
.chart__legend-key {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
/* display 不可省:它是 <i>,而行内盒不吃 inline-size/block-size。此前它只在
   .chart__legend-key(inline-flex)里出现,靠 flex 项被 blockify 侥幸成立 ——
   一挪到普通行内上下文就是零尺寸。同一个坑本文件已记过两次(.list-row__*、.meter)。 */
.chart__swatch { display: inline-block; inline-size: 10px; block-size: 2px; border-radius: var(--r-pill); }
.chart__swatch--value { background: var(--chart-value); }
.chart__swatch--cost  { background: var(--chart-cost); }
/* 环形图的图例键是**方块不是短线** —— 它标的是一块面积,不是一条线。
   色由 chart.js 的 sliceColor(名次) 经 --slice 传进来:图与图例必须取同一个
   序数槽,各算一遍就等于让两者悄悄说两套话。 */
.chart__swatch--slice {
  block-size: 10px;
  border-radius: 3px;
  background: var(--slice, var(--chart-1));
  margin-inline-end: 7px;
  vertical-align: -1px;
}

/* ---- 环形图 ---- *
 * 唯一一个不按像素算的图:正方 viewBox 自己缩放,故这里只需给它一个高度,
 * 不需要令牌与 JS 两头对齐(见 chart.js 那段)。居中的总量是「整体」本身,
 * 用 HTML 而非 <text> 承载 —— 那样字号才归排版管,不随画布缩放。 */
.donut { position: relative; display: grid; place-items: center; }
/* 扇区吃 currentColor,故色由这一行给 —— 不给它,环就会继承正文的近黑色。
   chart.js 里没有任何颜色,主题切换因此不需要重绘。 */
.donut__svg { color: var(--accent); block-size: var(--donut-h); inline-size: var(--donut-h); max-inline-size: 100%; display: block; }
.donut__center { position: absolute; text-align: center; pointer-events: none; }
/* 可点由 chart.js 按「调用方是否给了 onSelect」挂上 —— 图例是它的另一半,
   那份是真 DOM 行、戴 .press,故此处只需要一个光标与一层材质,不必再造按钮。 */
.donut--tappable .donut__slice { cursor: pointer; transition: opacity var(--dur-fast) var(--ease); }
.donut__value  { font-size: 19px; font-weight: 800; color: var(--fg); font-variant-numeric: tabular-nums; }
.donut__label  { font-size: 11.5px; color: var(--muted); margin-block-start: 2px; }

/* ---- 双向条形图 ---- *
 * 盈亏不能画成饼:一块负的扇区不存在。它要的是**共用一根零轴**的横向条 ——
 * 谁在轴右、谁在左、谁离轴最远,一眼可比。
 *
 * **不复用 .meter。** 那是进度条:它的灰色轨道说的是「容量」,而盈亏没有容量 ——
 * 一条铺满整行的底槽会让人去找「满」在哪里,那个问题根本不存在。这里因此没有轨道,
 * 只有一根贯穿全表的零轴和从它长出来的条。少画一件东西,意思反而准了。
 *
 * 刻意不走 SVG:这本就是「名字 | 条 | 数值」三列,用 grid 表达则条随容器伸缩、
 * 数字仍是真文本(可选中、可 tabular 对齐),而 SVG 会把这两样都换成图形。 */
.tornado { display: grid; }
.tornado__row {
  display: grid;
  grid-template-columns: minmax(0, 4.5rem) minmax(0, 1fr) auto;
  gap: 12px;
  align-items: stretch;   /* 让轴格撑满行高,相邻行的轴段才接成一根连续的线 */
}
.tornado__name,
.tornado__value { align-self: center; padding-block: 8px; }
.tornado__name  { font-size: 13.5px; font-weight: 700; color: var(--fg);
                  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tornado__value { font-size: 13.5px; font-weight: 800; font-variant-numeric: tabular-nums; text-align: end; }
.tornado__sub   { display: block; font-size: 11px; font-weight: 600; color: var(--muted);
                  font-variant-numeric: tabular-nums; }
/* 零轴:用背景画而不是加一个元素 —— 它是格子的属性,不是格子里的一件东西。
   逐行绘制而视觉连续,靠的是上面那条 align-items: stretch。 */
.tornado__cell {
  position: relative;
  background: linear-gradient(var(--border), var(--border)) 50% 0 / 1px 100% no-repeat;
}
/* 条从轴长出:正向占右半幅、负向占左半幅,宽度是幅值的线性映射。
   **不用 scaleX** —— 那会把圆角一起横向压扁,短条看上去就是方头的。 */
.tornado__bar {
  position: absolute;
  inset-block-start: 50%;
  translate: 0 -50%;
  block-size: 11px;
  inline-size: calc(var(--w, 0) * 50%);
  border-radius: var(--r-pill);
}
.tornado__bar--pos { inset-inline-start: 50%; background: var(--down); }
.tornado__bar--neg { inset-inline-end: 50%;  background: var(--up); }
/* 轴脚:标出零点与量程,否则「条有多长」没有参照 */
.tornado__axis {
  display: grid;
  grid-template-columns: minmax(0, 4.5rem) minmax(0, 1fr) auto;
  gap: 12px;
  margin-block-start: 8px;
  padding-block-start: 10px;
  border-block-start: 1px solid var(--hairline);
  font-size: 11px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.tornado__scale { display: flex; justify-content: space-between; }

.bars__svg { inline-size: 100%; block-size: var(--bars-h); display: block; }
.bars__footer {
  display: flex;
  justify-content: space-between;
  border-block-start: 1px solid var(--hairline);
  margin-block-start: 12px;
  padding-block-start: 12px;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--muted);
}

/* ============================================================================
 *  16 · 动效
 *
 *  谁发起,是唯一的界线。数据到达 = 声明式层,一律 timing 曲线,禁弹簧。
 *  Web 端几乎全部落在声明式层 —— 用 timing,别想着加回弹。
 * ========================================================================== */
@keyframes fade-up { from { opacity: 0; transform: translateY(14px); } }
.fade-up {
  animation: fade-up var(--dur-enter) var(--ease-out) both;
  animation-delay: calc(var(--i, 0) * 60ms);   /* stagger 封顶 6 项,见调用点 */
}

/* 内容轮换不是进场:替换的幅度更小(8px),退场比进场快。 */
@keyframes swap-in  { from { opacity: 0; transform: translateY(8px); } }
@keyframes fade-out { to   { opacity: 0; } }
.swap-in  { animation: swap-in var(--dur-standard) var(--ease-out) both; }
.swap-out { animation: fade-out var(--dur-fast) var(--ease) both; }

/* 按压反馈四档 —— 元素越小,缩放越狠。
   0.985 施于一枚 34px 图标肉眼看不见;0.9 施于整卡则像塌了。 */
.press {
  position: relative;
  transition: transform var(--dur-fast) var(--ease);
}
.press::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--glass);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-gentle) var(--ease);   /* 余韵交给颜色,不给缩放 */
}
.press:active::after { opacity: 1; transition-duration: var(--dur-fast); }
.press--card:active { transform: scale(0.985); }
.press--cell:active { transform: scale(0.95); }
.press--icon:active { transform: scale(0.9); }
.press--icon::after { display: none; }   /* 纯图标钮无高亮层 */

/* ---- 悬停 —— 按压反馈的鼠标方言 ---- *
 * 桌面上卡片与列表行**本身就是按钮**(点开抽屉),鼠标下却一直毫无反馈。
 *
 * 三条纪律:
 *   · **挂在 .press 上,不挂在 .card 上。**「这东西可按」的标记全站早已存在,
 *     缺的只是它的鼠标读法。于是 how.html 那六张静态卡不用加任何排除规则 ——
 *     它们本就没有 .press。零新类,零特例。
 *   · **锁在 (hover:hover)+(pointer:fine) 内。** 触摸端的 :hover 会粘住不放,
 *     那正是「点完一张卡它一直亮着」的由来。
 *   · **只用材质,不用位移。** translateY 会与上面三条 :active 的 transform 争
 *     同一个属性,逼出一条 :active{translateY(0)} 特例;而材质本就是法则①的手段:
 *     浅色投影上浮、深色 surface 升一级 —— 同一条法则在两个主题下的两个器件。 */
@media (hover: hover) and (pointer: fine) {
  .press {
    transition: transform var(--dur-fast) var(--ease),
                box-shadow var(--dur-standard) var(--ease),
                background-color var(--dur-standard) var(--ease);
  }
  .btn {
    transition: transform var(--dur-gentle) var(--ease),
                box-shadow var(--dur-standard) var(--ease);
  }

  /* 卡片上浮一档 */
  .card.press:hover { box-shadow: var(--shadow-overlay); }
  /* 深色下卡片阴影恒为零(法则②),浮起只能由明度差承担 —— 换器件不换法则 */
  [data-theme="dark"] .card.press:hover { background: var(--surface-tertiary); }

  /* 列表行借按压高亮层的一半,不引第二套机制 */
  .list-row.press:hover::after { opacity: 0.5; }

  /* 环形图的扇区是全站唯一「可按却不能戴 .press」的东西 —— <circle> 上没有伪元素
     可挂高亮层。于是让它自己的不透明度充当那层材质:法则③(只用材质不用位移)
     不变,换的只是器件。 */
  .donut--tappable .donut__slice:hover { opacity: 0.72; }

  /* 圆钮只压 .icon-btn,不压全部 .press--icon —— 头像也戴着 press--icon,
     而给一张脸换底色是错的。两个类的特异性同时压过 [data-theme=dark] .icon-btn,
     不靠源码顺序。 */
  .icon-btn.press:hover                     { background: var(--surface-secondary); }
  [data-theme="dark"] .icon-btn.press:hover { background: var(--surface-tertiary); }

  /* 主钮保留自带的紫光晕再叠一档浮起;副钮与卡片同一手势 */
  .btn--primary:hover   { box-shadow: var(--shadow-accent-btn), var(--shadow-overlay); }
  .btn--secondary:hover { box-shadow: var(--shadow-overlay); }

  .site-header__nav a:hover { background: var(--nav-pill); }
}

/* 登录页那枚呼吸光晕 —— 全 App 唯二的无限循环动画之一。不要再增加第三个。
   fixed 而非 absolute:它属于这一屏的环境光,不属于任何一张卡。 */
.ambient-glow {
  position: fixed;
  inset-block-start: -60px;
  inset-inline-end: -60px;
  inline-size: 288px;
  aspect-ratio: 1;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  animation: breathe 6s linear infinite alternate;
  pointer-events: none;
}
@keyframes breathe { to { opacity: 0.75; } }

/* 刷新钮旋转:仅在途时转 */
.is-spinning { animation: spin var(--dur-count) linear infinite; }

/* ============================================================================
 *  17 · 提示条(toast)
 * ========================================================================== */
.toast-host {
  position: fixed;
  inset-block-start: calc(var(--header-clearance) + 12px);
  inset-inline: 0;
  z-index: 200;
  display: grid;
  justify-items: center;
  gap: 8px;
  pointer-events: none;
}
.toast {
  display: flex;
  align-items: center;
  gap: 8px;
  border-radius: var(--r-inner);
  padding: 10px 16px;
  font-size: 13px;
  font-weight: 600;
  box-shadow: var(--shadow-overlay);
  animation: swap-in var(--dur-standard) var(--ease-out) both;
}
.toast--success { background: var(--success-tint); color: var(--success-soft-fg); }
.toast--danger  { background: var(--danger-tint);  color: var(--danger-soft-fg); }
.toast--info    { background: var(--info-tint);    color: var(--info); }
.toast--out     { animation: fade-out var(--dur-fast) var(--ease) both; }

/* ============================================================================
 *  18 · 图标
 * ========================================================================== */
.icon {
  inline-size: 20px;
  block-size: 20px;
  fill: currentColor;
  flex-shrink: 0;
}
.icon--sm { inline-size: 16px; block-size: 16px; }
.icon--lg { inline-size: 24px; block-size: 24px; }

/* ============================================================================
 *  19 · 桌面版面(≥ --page-wide)
 *
 *  规格书: ../../../DESIGN.md §4.4
 *
 *  **移动端是叙事,桌面端是版面。** 前者的层次靠顺序表达(一次只看一件事),
 *  后者可以靠位置与面积表达(一眼看全)。故这一层重排的是**落位**,不碰任何
 *  组件的外观 —— 全文没有一条改颜色、圆角、字号的规则。
 *
 *  为何整层收在文件末尾而非散在各节旁:
 *    · 它是一个**层**而不是十七处局部微调,收在一起才能一屏读完桌面语法;
 *    · 它压的是落位,而落位必须压过组件自带的外观(css/CLAUDE.md 那条会反复
 *      咬人的规则)。整层最后是**声明该由谁做主**,不是靠源码顺序碰运气 ——
 *      而且本层只碰布局容器(.page--* / .dash*),不与任何组件类正面争特异性。
 *
 *  为何断点取 1120 而非另发明一个数:**断点值 = 容器上限值**(法则三)。
 *  窗口一到刚好容得下宽容器,版面才重组;720–1119 那一档保持窄栏 ——
 *  半宽的仪表盘既不是叙事也不是版面。
 * ========================================================================== */
@media (min-width: 1120px) {

  /* ---- 仪表档页壳。只在本断点内成立,以下它就是一根普通的 .page ---- */
  .page--dash { max-inline-size: var(--page-wide); }

  /* ---- 概览页三栏骨架 ----------------------------------------------------- *
   * 不用 grid-template-areas:空的 #stale-host 会在具名区里占掉一整行(连带一道
   * 20px 幽灵间距),而 span 式落位让它自然坍成零高。且自动流的落子顺序恰好等于
   * DOM 顺序 —— 一个 area 名都不用起。
   *
   *   full ────────────────────────────  #stale-host / 头像问候条
   *   hero ──────────────────┬── rail    大数字 + 通屏折线 │ 双钮·净值余额·双进度
   *   profit ── detail ── holdings       三张同构列表卡
   *   full(hist) ────────────────────    月柱图 2fr │ 最近交易 1fr
   *
   * **读法是三栏,度量是 12 轨。** 因为这两件事同时为真:三张同构列表卡必须严格
   * 等宽(12 ÷ 3 = 4),而主区与侧轨的最佳比不是 2:1 —— 侧轨里那对 KPI 磁贴在
   * 1/3 栏里只剩 166px,一个 ¥ 数值就要缩字号。7:5 给到 211px,主区仍有 617px。
   *
   * **不写 align-items: start。** 那一条曾让每格的高度由自己的内容决定,于是三行
   * 的底边各塌各的(侧轨比主区矮 161、两张三行卡比四行卡矮 66、柱图卡比最近交易
   * 矮 73)。**版面里对齐的单位是行,不是块** —— 移动端是单列叙事,格高即内容高,
   * 天生没有这个问题;一折成版面,短的那格必然在行的底边留洞。默认的 stretch 才
   * 是对的,富余高度交给下面两条去安置。 */
  .dash       { grid-template-columns: repeat(12, minmax(0, 1fr)); }
  .dash__full { grid-column: 1 / -1; }

  /* 每个格子自己申明占几轨**与哪一行生长**,没有一个靠默认或靠特异性打赢别人 ——
     `.dash > .stack--sm` 这种写法会连 .dash__full 的购买历史一起吃掉。
     生长轨用显式 1fr 而**不用 align-content: stretch**:后者把富余平摊进 gap,
     「节标题 → 卡片」的 10px 会悄悄变成三十几 px —— 那正是 .stack 收尾两条规则
     里第一条(align-content: start)守着的东西,别从这一层把它绕过去。
     1fr 吃光富余后那条 start 本就无事可做,不必覆盖。 */
  .dash__hero { grid-column: span 7; grid-template-rows: auto 1fr; }
  .dash__rail { grid-column: span 5; grid-template-rows: auto 1fr 1fr; }
  .dash__col  { grid-column: span 4; grid-template-rows: auto 1fr; }
  .dash__hist { grid-template-columns: 2fr 1fr; --bars-h: 180px; }

  /* 富余高度归卡内:成块的居中,成行的平分。**两条声明消灭三处参差** ——
     不是三处局部微调,是同一条自律的两种落法。

     为何列表卡不跟着居中:三行卡会整体下沉 33px,首行与邻卡的首行错开 ——
     那比底边不齐更刺眼。行平分则首行齐平、底边齐平,只有卡内的发丝线各走各的,
     而 3 行与 4 行的分隔线本就无人指望它对齐。

     这是本层唯一两条碰组件类的规则,靠 .dash 前缀申明落位该由谁做主 ——
     正是「会反复咬人的那条规则」的正解形式,不是靠源码顺序碰运气。
     `.press::after` 是 position:absolute,不会变成多出来的一条网格轨。 */
  .dash .card      { display: grid; align-content: center; }
  .dash .list-card { grid-auto-rows: 1fr; }

  /* 图区内部重新落子。**DOM 一个字不动** —— apps/mobile 的概览屏是「图在前、
     chips 在后」,data.js 与它逐格同构,顺序是双端契约;而 chips 画在图的右上
     还是下方,纯是落位,归 CSS 管。代价记在这里:桌面上视觉顺序 ≠ DOM 顺序,
     chips 的 Tab 序落在图之后(图不可聚焦,故实际 Tab 链与移动端一致)。

     **图例与 chips 同处图上那一行**:图例吊在图的左下时,它右侧是一整条空白,
     而图上方那行本就只有 chips 一件靠右的东西 —— 一左一右恰好是一条图表页眉,
     顺带省掉一整行(连两处 card__foot 的 16px 让位共 ~45px)。两者的 margin
     在这里清零:纵向节奏由 .stack 的 20px 给,组件自带的让位在版面里是重复。

     顺带换一档画布:令牌在这里重定义一次,CSS 的 svg 高度与 chart.js 反算的
     viewBox 同时跟上 —— 600px 以上的宽配 180 高是薄片,配 260 才立得住。 */
  .dash__chart                   { display: grid; grid-template-columns: 1fr auto; gap: 12px;
                                   --chart-h: 260px; }
  .dash__chart .chart__legend    { grid-area: 1 / 1; align-self: center;
                                   justify-content: flex-start; margin-block-start: 0; }
  .dash__chart .segmented--chips { grid-area: 1 / 2; margin-block-start: 0; }
  .dash__chart .chart            { grid-area: 2 / 1 / 3 / 3; }

  /* 版面里大金额左对齐 —— 居中是叙事的语法(一次只看一件事),
     在一条 617px 宽的主区里会让数字与它的标签一起漂在中间无所依傍。
     .money 是 flex 容器,text-align 管不到它的项,必须另给 justify-content ——
     漏掉这一条的样子是:标签贴左、数字居中,两者各说各话。 */
  .dash__hero .text-center { text-align: start; }
  .dash__hero .money       { justify-content: flex-start; }

  /* ---- 管理面板:运营总览折成两栏 ------------------------------------------ *
   * 与概览页同一个 7:5,不发明第二个比例:左边是「这只基金现在值多少、怎么走到
   * 这里的」,右边是「六个刻度 + 两条进度」—— 后者是一叠等宽短行,给它半幅纯属浪费。
   *
   * 包装器在窄屏必须逐像素隐形(§4.4 规约④):.admin-ops 与它两个子块都是 .stack、
   * gap 同为 20px,窄屏渲染与不加它完全相同。
   *
   * 侧栏已由 .admin-layout 在 720 处占掉 220px,故这里的可用宽约 836 —— 配 180 高
   * 是 4.6:1 的薄片,同样换一档画布,令牌只在祖先上重定义一次。 */
  .admin-ops        { grid-template-columns: 7fr 5fr; gap: 20px; align-items: start; }
  .admin-ops__hero  { --chart-h: 240px; }

  /* 大金额在版面里左对齐,理由同上面概览页那条(.money 是 flex,text-align 管不到它)。 */
  .admin-ops__hero .text-center { text-align: start; }
  .admin-ops__hero .money       { justify-content: flex-start; }

  /* ---- 顶栏在桌面端放开呼吸 ---- */
  .site-header__nav     { gap: 6px; }
  .site-header__nav a   { padding: 9px 18px; }
}
