吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1290|回复: 36
收起左侧

[其他转载] 补仓实验室 - 「股票补仓可视化」工具

  [复制链接]
实事求是 发表于 2026-10-25 23:13

补仓实验室 - 「股票补仓可视化」工具

软件信息

只算一件事:现在补多少股,股价涨到多少钱能回本。

解决什么问题

10 元买 1000 股,跌到 8 元。很多人第一反应是补仓,但补 100 股、200 股、1000 股,回本价完全不一样。

按现有股数的比例补:

  1. 补 10%:100 股,800 元,回本价 9.82 元,需涨 22.73%
  2. 补 20%:200 股,1600 元,回本价 9.67 元,需涨 20.83%
  3. 补 30%:300 股,2400 元,回本价 9.54 元,需涨 19.23%
  4. 补 50%:500 股,4000 元,回本价 9.33 元,需涨 16.67%
  5. 补 100%:1000 股,8000 元,回本价 9.00 元,需涨 12.50%
  6. 补 200%:2000 股,16000 元,回本价 8.67 元,需涨 8.33%

补得越多,回本价越接近现价,但投入和仓位风险也一起放大。
不管补多少,按现价补的部分不亏不赚,所以当前绝对亏损仍然是 -2000 元,变化的是平均成本和亏损率。

主要功能

  1. 四种补仓方式:按比例、按股数、按金额、按目标成本。
  2. 回本价曲线:横轴是补仓比例,纵轴是补后回本价,可切到补仓金额。
  3. 价格尺:现价、回本价、原成本画在一根轴上。
  4. 成本对比柱状图和持仓结构圆环。
  5. 比例阶梯表:0% 到 200%,每一档的补仓股数、金额、回本价、需涨、补后盈亏。
  6. 100 股取整开关:A 股可以打开,按真实下单股数计算。
  7. 手动填价格,也可以填代码后点获取现价。
  8. 批量:添加多只股票,每只单独设补仓比例;支持导入 获取股票信息.txt,自动提取 sh / sz / bj 代码并拉取现价;底部合计补仓金额、补后股数、补后总成本、组合浮亏。

怎么用

  1. 填当前持股、持仓成本、当前股价。
  2. 选补仓方式。按比例里的 10% / 20% / 30% 是按现有股数算,比如 1000 股补 20% 就是再买 200 股。
  3. 看四个数:回本价、需补股数、补仓金额、补后亏损率。
  4. 多只股票就往下用批量区,填代码、持股、成本、现价、比例,或者导入 txt 后自动拉价。
    1.png

2.png

3.png

按目标成本模式也可以反推:1000 股、成本 30.26 元、现价 27.71 元、目标 28.00 元,需要补 7794 股,补仓金额 215971.74 元,补后总股数 8794 股,回本价 28.00 元。

四个前提

  1. 补仓不改变当前绝对亏损。按现价补的部分不亏不赚,变的是平均成本和亏损率。
  2. 回本价就是补后平均成本。股价涨回这个价才不亏不赚;它可以低于原成本,但不可能低于现价。
  3. 补得越多,回本价越接近现价,但每多补一档,下降幅度越来越小,仓位风险越来越大。
  4. 没有计佣金、印花税、过户费和成交滑点;A 股可以打开 100 股取整。

免责声明

本工具仅用于补仓成本的算术演示,不构成投资建议。



补仓实验室.zip (30.68 KB, 下载次数: 235)

[Asm] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<title>补仓实验室 · 回本价与批量补仓</title>
<style>
  :root {
    --bg: #f3efe6;
    --paper: #fffdf8;
    --paper-2: #faf6ec;
    --ink: #18231d;
    --ink-2: #3d4a42;
    --muted: #6f7972;
    --faint: #8c948d;
    --line: #ded8c9;
    --line-strong: #c9c1ae;
    --green: #0f5c47;
    --green-2: #1f7a5e;
    --green-soft: #dceae2;
    --green-wash: #f0f6f2;
    --terra: #bd583b;
    --terra-2: #d97757;
    --terra-soft: #f6e3da;
    --gold: #b28733;
    --gold-soft: #f2e7c9;
    --dark: #12241d;
    --cream: #f7f1e3;
    --shadow-sm: 0 4px 16px rgba(31, 42, 35, .05);
    --shadow: 0 18px 52px rgba(31, 42, 35, .08);
    --radius: 22px;
    --font: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif;
    --num: "Bahnschrift", "DIN Alternate", "Segoe UI", "Arial", sans-serif;
  }

  * { box-sizing: border-box; }
  [hidden] { display: none !important; }
  html { scroll-behavior: smooth; }
  body {
    margin: 0;
    color: var(--ink);
    background:
      radial-gradient(1100px 560px at 8% -8%, rgba(15, 92, 71, .09), transparent 62%),
      radial-gradient(900px 520px at 102% 2%, rgba(189, 88, 59, .075), transparent 58%),
      var(--bg);
    font-family: var(--font);
    font-size: 15px;
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
  }
  body::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background-image:
      linear-gradient(rgba(24, 35, 29, .032) 1px, transparent 1px),
      linear-gradient(90deg, rgba(24, 35, 29, .032) 1px, transparent 1px);
    background-size: 48px 48px;
    -webkit-mask-image: linear-gradient(to bottom, #000 0%, transparent 78%);
    mask-image: linear-gradient(to bottom, #000 0%, transparent 78%);
  }

  ::selection { background: var(--green); color: var(--cream); }
  button, input { font: inherit; }
  button { cursor: pointer; }
  button, input, summary { -webkit-tap-highlight-color: transparent; }
  :focus-visible { outline: 2px solid var(--green); outline-offset: 3px; border-radius: 5px; }

  .page {
    width: min(1360px, calc(100% - 56px));
    margin: 0 auto;
    padding: 22px 0 46px;
  }

  .num {
    font-family: var(--num);
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
    letter-spacing: -.02em;
  }

  .topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 20px;
    padding-bottom: 22px;
    border-bottom: 1px solid var(--line);
  }
  .brand { display: flex; align-items: center; gap: 12px; }
  .brand-mark {
    display: grid;
    place-items: center;
    width: 42px;
    height: 42px;
    border-radius: 13px;
    background: var(--dark);
    color: var(--cream);
    font-size: 21px;
    font-weight: 700;
    box-shadow: inset 0 0 0 1px rgba(255,255,255,.12);
  }
  .brand-copy { display: flex; flex-direction: column; line-height: 1.2; }
  .brand-copy strong { font-size: 15px; letter-spacing: .02em; }
  .brand-copy span {
    margin-top: 3px;
    color: var(--muted);
    font-size: 9.5px;
    letter-spacing: .18em;
    font-family: var(--num);
  }
  .top-note {
    display: flex;
    align-items: center;
    gap: 8px;
    color: var(--muted);
    font-size: 12px;
    letter-spacing: .03em;
  }
  .top-note i {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--green-2);
    box-shadow: 0 0 0 4px rgba(31, 122, 94, .1);
  }

  .hero {
    display: grid;
    grid-template-columns: minmax(0, 1.34fr) minmax(350px, .66fr);
    gap: 44px;
    align-items: stretch;
    padding: 42px 0 38px;
  }
  .hero-copy { display: flex; flex-direction: column; justify-content: center; min-width: 0; }
  .eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    width: fit-content;
    color: var(--green);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .16em;
  }
  .eyebrow-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--gold);
    box-shadow: 0 0 0 4px rgba(178, 135, 51, .12);
  }
  .hero h1 {
    margin: 18px 0 20px;
    font-size: clamp(44px, 5.25vw, 76px);
    line-height: 1.04;
    letter-spacing: -.045em;
    font-weight: 800;
  }
  .hero h1 em {
    position: relative;
    z-index: 0;
    color: var(--green);
    font-style: normal;
    white-space: nowrap;
  }
  .hero h1 em::after {
    content: "";
    position: absolute;
    left: 1%;
    right: -1%;
    bottom: 4%;
    height: .15em;
    border-radius: 99px;
    background: var(--gold);
    opacity: .36;
    z-index: -1;
    transform: rotate(-.8deg);
  }
  .hero-lead {
    max-width: 650px;
    margin: 0;
    color: var(--ink-2);
    font-size: 15.5px;
    line-height: 1.85;
  }
  .hero-points {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 10px;
    max-width: 620px;
    margin-top: 28px;
  }
  .hero-points > div {
    display: grid;
    grid-template-columns: auto 1fr;
    grid-template-rows: auto auto;
    column-gap: 8px;
    row-gap: 1px;
    padding: 11px 12px;
    border: 1px solid var(--line);
    border-radius: 14px;
    background: rgba(255, 253, 248, .56);
  }
  .hero-points span {
    grid-row: 1 / 3;
    align-self: center;
    color: var(--gold);
    font-family: var(--num);
    font-size: 11px;
    font-weight: 700;
  }
  .hero-points b { font-size: 12.5px; line-height: 1.3; }
  .hero-points small { color: var(--muted); font-size: 10.5px; line-height: 1.3; }
  .hero-foot {
    margin: 20px 0 0;
    color: var(--muted);
    font-size: 11.5px;
    letter-spacing: .03em;
  }

  .exposure-card {
    position: relative;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-height: 390px;
    padding: 28px 28px 24px;
    border-radius: 29px;
    background:
      radial-gradient(540px 280px at 100% 0%, rgba(95, 156, 126, .23), transparent 64%),
      linear-gradient(148deg, #17372b 0%, #10251d 54%, #0b1a15 100%);
    color: var(--cream);
    box-shadow: 0 28px 70px rgba(15, 36, 28, .22);
  }
  .exposure-card::before,
  .exposure-card::after {
    content: "";
    position: absolute;
    border-radius: 50%;
    pointer-events: none;
  }
  .exposure-card::before {
    width: 310px;
    height: 310px;
    right: -132px;
    top: -138px;
    border: 1px solid rgba(247, 241, 227, .12);
  }
  .exposure-card::after {
    width: 210px;
    height: 210px;
    right: -82px;
    top: -88px;
    border: 1px solid rgba(247, 241, 227, .08);
  }
  .exposure-top {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    color: rgba(247, 241, 227, .7);
    font-size: 12px;
    letter-spacing: .06em;
  }
  .risk-badge {
    padding: 4px 9px;
    border: 1px solid rgba(247, 241, 227, .18);
    border-radius: 999px;
    color: var(--cream);
    font-size: 10.5px;
    letter-spacing: .04em;
    background: rgba(255, 255, 255, .06);
  }
  .risk-badge[data-level="mid"] { background: rgba(178, 135, 51, .18); border-color: rgba(226, 192, 113, .36); }
  .risk-badge[data-level="high"] { background: rgba(189, 88, 59, .2); border-color: rgba(217, 119, 87, .4); }
  .risk-badge[data-level="custom"] { background: rgba(178, 135, 51, .16); border-color: rgba(226, 192, 113, .34); color: var(--gold-soft); }
  .exposure-value {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: flex-start;
    gap: 5px;
    margin-top: 6px;
    line-height: 1;
  }
  .exposure-value > span:first-child {
    font-family: var(--num);
    font-variant-numeric: tabular-nums;
    font-size: clamp(62px, 5.8vw, 82px);
    font-weight: 600;
    letter-spacing: -.055em;
  }
  .pct-sign {
    margin-top: 9px;
    color: var(--gold-soft);
    font-family: var(--num);
    font-size: 25px;
    font-weight: 500;
  }
  .exposure-caption {
    position: relative;
    z-index: 1;
    min-height: 44px;
    margin: 8px 0 17px;
    color: rgba(247, 241, 227, .78);
    font-size: 12.5px;
    line-height: 1.65;
  }
  .gauge { position: relative; z-index: 1; margin-bottom: 14px; }
  .gauge-track {
    position: relative;
    height: 7px;
    border-radius: 99px;
    background: linear-gradient(90deg, #2c8063 0%, #c6a04b 55%, #c65f42 100%);
    opacity: .92;
  }
  .gauge-track::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: rgba(10, 24, 19, .3);
    clip-path: inset(0 0 0 var(--risk-pos, 11%));
  }
  .gauge-marker {
    position: absolute;
    top: 50%;
    left: var(--risk-pos, 11%);
    width: 15px;
    height: 15px;
    border: 3px solid var(--cream);
    border-radius: 50%;
    background: var(--dark);
    transform: translate(-50%, -50%);
    box-shadow: 0 0 0 4px rgba(247, 241, 227, .1);
  }
  .gauge-labels {
    display: flex;
    justify-content: space-between;
    margin-top: 7px;
    color: rgba(247, 241, 227, .42);
    font-family: var(--num);
    font-size: 8.5px;
    letter-spacing: .03em;
  }
  .formula-box {
    position: relative;
    z-index: 1;
    padding: 10px 12px;
    border: 1px solid rgba(247, 241, 227, .12);
    border-radius: 10px;
    background: rgba(255, 255, 255, .055);
    color: rgba(247, 241, 227, .8);
    font-size: 11px;
    letter-spacing: .01em;
  }
  .formula-box b { color: var(--cream); font-weight: 650; }
  .fraction-badge {
    position: relative;
    z-index: 1;
    margin-top: 9px;
    color: rgba(247, 241, 227, .64);
    font-size: 11.5px;
  }
  .fraction-badge b {
    display: inline-block;
    margin-left: 2px;
    color: var(--gold-soft);
    font-family: var(--num);
    font-size: 16px;
    letter-spacing: 0;
    vertical-align: -1px;
  }
  .exposure-facts {
    position: relative;
    z-index: 1;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1px;
    margin-top: 17px;
    overflow: hidden;
    border: 1px solid rgba(247, 241, 227, .1);
    border-radius: 11px;
    background: rgba(247, 241, 227, .08);
  }
  .exposure-facts > div {
    min-width: 0;
    padding: 8px 9px;
    background: rgba(15, 34, 27, .78);
  }
  .exposure-facts span {
    display: block;
    overflow: hidden;
    color: rgba(247, 241, 227, .48);
    font-size: 8.5px;
    letter-spacing: .03em;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .exposure-facts b {
    display: block;
    overflow: hidden;
    margin-top: 1px;
    color: var(--cream);
    font-family: var(--num);
    font-size: 14px;
    font-weight: 550;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  .layout {
    display: grid;
    grid-template-columns: 366px minmax(0, 1fr);
    gap: 24px;
    align-items: start;
  }
  .panel {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: rgba(255, 253, 248, .82);
    box-shadow: var(--shadow-sm);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
  }
  .controls {
    position: sticky;
    top: 20px;
    max-height: calc(100vh - 40px);
    overflow: auto;
    padding: 20px 20px 18px;
    scrollbar-width: thin;
    scrollbar-color: var(--line-strong) transparent;
  }
  .controls::-webkit-scrollbar { width: 5px; }
  .controls::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 99px; }
  .panel-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
    padding-bottom: 13px;
  }
  .eyebrow-sm {
    display: block;
    margin-bottom: 4px;
    color: var(--muted);
    font-family: var(--num);
    font-size: 8.5px;
    font-weight: 700;
    letter-spacing: .18em;
  }
  .panel-head h2, .card-head h2, .section-head h2, .method-card h2 {
    margin: 0;
    font-size: 18px;
    line-height: 1.35;
    letter-spacing: -.02em;
    font-weight: 750;
  }
  .reset-btn {
    flex: none;
    padding: 5px 10px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: transparent;
    color: var(--muted);
    font-size: 11px;
    transition: .18s ease;
  }
  .reset-btn:hover { border-color: var(--green); color: var(--green); background: var(--green-wash); }
  .preset-label {
    margin: 2px 0 7px;
    color: var(--faint);
    font-size: 10px;
    letter-spacing: .05em;
  }
  .preset-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 6px;
    margin-bottom: 16px;
  }
  .preset {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    padding: 9px 9px 8px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: var(--paper-2);
    color: var(--ink-2);
    text-align: left;
    transition: .18s ease;
  }
  .preset b { font-size: 11.5px; line-height: 1.35; font-weight: 700; white-space: nowrap; }
  .preset span { color: var(--muted); font-size: 9px; line-height: 1.35; white-space: nowrap; }
  .preset:hover { border-color: var(--green-2); transform: translateY(-1px); }
  .preset.active {
    border-color: var(--green);
    background: var(--green);
    color: var(--cream);
    box-shadow: 0 6px 16px rgba(15, 92, 71, .18);
  }
  .preset.active span { color: rgba(247, 241, 227, .68); }

  .control {
    padding: 14px 0;
    border-top: 1px solid var(--line);
  }
  .control-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 9px;
  }
  .control-head label {
    color: var(--ink-2);
    font-size: 12.5px;
    font-weight: 650;
    letter-spacing: .01em;
  }
  .control-head output {
    flex: none;
    color: var(--green);
    font-family: var(--num);
    font-size: 14px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
  }
  .mini-note {
    flex: none;
    color: var(--faint);
    font-size: 9.5px;
    letter-spacing: .04em;
  }
  .hint {
    margin: 7px 0 0;
    color: var(--muted);
    font-size: 10px;
    line-height: 1.55;
  }

  input[type="range"] {
    --fill: 50%;
    display: block;
    width: 100%;
    height: 18px;
    margin: 0;
    padding: 0;
    border: 0;
    outline: none;
    background: transparent;
    -webkit-appearance: none;
    appearance: none;
    cursor: pointer;
  }
  input[type="range"]::-webkit-slider-runnable-track {
    width: 100%;
    height: 4px;
    border-radius: 99px;
    background: linear-gradient(90deg, var(--green-2) 0 var(--fill), #e6e0d3 var(--fill) 100%);
  }
  input[type="range"]::-moz-range-track {
    width: 100%;
    height: 4px;
    border-radius: 99px;
    background: #e6e0d3;
  }
  input[type="range"]::-moz-range-progress {
    height: 4px;
    border-radius: 99px;
    background: var(--green-2);
  }
  input[type="range"]::-webkit-slider-thumb {
    width: 17px;
    height: 17px;
    margin-top: -6.5px;
    border: 4px solid var(--green);
    border-radius: 50%;
    background: var(--paper);
    box-shadow: 0 1px 5px rgba(15, 36, 28, .2);
    -webkit-appearance: none;
    appearance: none;
    transition: transform .12s ease;
  }
  input[type="range"]::-moz-range-thumb {
    width: 11px;
    height: 11px;
    border: 4px solid var(--green);
    border-radius: 50%;
    background: var(--paper);
    box-shadow: 0 1px 5px rgba(15, 36, 28, .2);
  }
  input[type="range"]:hover::-webkit-slider-thumb { transform: scale(1.08); }
  input[type="range"]:active::-webkit-slider-thumb { transform: scale(.96); }
  input[type="range"].range-loss::-webkit-slider-runnable-track {
    background: linear-gradient(90deg, var(--terra-2) 0 var(--fill), #e6e0d3 var(--fill) 100%);
  }
  input[type="range"].range-loss::-moz-range-progress { background: var(--terra-2); }
  input[type="range"].range-loss::-webkit-slider-thumb { border-color: var(--terra); }
  input[type="range"].range-loss::-moz-range-thumb { border-color: var(--terra); }

  .chip-row {
    display: flex;
    flex-wrap: wrap;
    gap: 5px;
    margin-top: 9px;
  }
  .chip {
    padding: 3px 8px;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: transparent;
    color: var(--muted);
    font-family: var(--num);
    font-size: 9.5px;
    line-height: 1.5;
    transition: .15s ease;
  }
  .chip:hover { border-color: var(--green-2); color: var(--green); background: var(--green-wash); }
  .chip.warm:hover { border-color: var(--terra); color: var(--terra); background: var(--terra-soft); }

  .stepper-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-top: 6px;
  }
  .stepper {
    display: inline-flex;
    align-items: center;
    gap: 0;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--paper);
  }
  .stepper button {
    width: 28px;
    height: 28px;
    border: 0;
    background: transparent;
    color: var(--muted);
    font-size: 15px;
    line-height: 1;
  }
  .stepper button:hover { background: var(--green-wash); color: var(--green); }
  .stepper input {
    width: 30px;
    height: 28px;
    padding: 0;
    border: 0;
    border-right: 1px solid var(--line);
    border-left: 1px solid var(--line);
    background: transparent;
    color: var(--ink-2);
    font-family: var(--num);
    font-size: 12px;
    text-align: center;
    -moz-appearance: textfield;
  }
  .stepper input::-webkit-outer-spin-button,
  .stepper input::-webkit-inner-spin-button { margin: 0; -webkit-appearance: none; }
  .stepper span { padding: 0 8px 0 5px; color: var(--muted); font-size: 10px; }
  .hint-inline { color: var(--faint); font-size: 9.5px; }

  .segmented {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 3px;
    padding: 3px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: #ede8dc;
  }
  .segmented button {
    padding: 7px 8px;
    border: 0;
    border-radius: 7px;
    background: transparent;
    color: var(--muted);
    font-size: 11px;
    font-weight: 600;
    transition: .16s ease;
  }
  .segmented button.active {
    background: var(--paper);
    color: var(--ink);
    box-shadow: 0 2px 7px rgba(31, 42, 35, .09);
  }
  .segmented-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .segmented-3 button { padding-right: 4px; padding-left: 4px; font-size: 10.5px; }
  .segmented-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .mode-switch { margin-top: 2px; }
  .mode-panel { margin-top: 8px; }
  .control-head.sub { margin-top: 5px; margin-bottom: 7px; }
  .control-head.sub label { color: var(--muted); font-size: 11px; font-weight: 550; }
  .mode-total {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1px;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: 9px;
    background: var(--line);
  }
  .mode-total > div { min-width: 0; padding: 7px 9px; background: var(--paper-2); }
  .mode-total span { display: block; color: var(--muted); font-size: 8.5px; }
  .mode-total b {
    display: block;
    overflow: hidden;
    margin-top: 1px;
    color: var(--green);
    font-family: var(--num);
    font-size: 13px;
    font-weight: 600;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .select-wrap { position: relative; }
  .select-wrap::after {
    content: "&#8964;";
    position: absolute;
    top: 50%;
    right: 11px;
    color: var(--muted);
    font-size: 13px;
    transform: translateY(-55%);
    pointer-events: none;
  }
  .select-wrap select {
    width: 100%;
    height: 36px;
    padding: 0 32px 0 10px;
    border: 1px solid var(--line);
    border-radius: 9px;
    outline: none;
    background: var(--paper);
    color: var(--ink-2);
    font-size: 12px;
    -webkit-appearance: none;
    appearance: none;
    cursor: pointer;
  }
  .select-wrap select:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
  .jump-btn {
    width: 100%;
    padding: 8px 10px;
    border: 1px dashed var(--green-2);
    border-radius: 9px;
    background: var(--green-wash);
    color: var(--green);
    font-size: 11px;
    font-weight: 650;
    text-align: left;
    transition: .16s ease;
  }
  .jump-btn:hover { border-style: solid; background: var(--green-soft); }

  .loss-input-wrap {
    display: inline-flex;
    align-items: center;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--paper);
  }
  .loss-input-wrap input {
    width: 48px;
    height: 27px;
    padding: 0 2px 0 8px;
    border: 0;
    background: transparent;
    color: var(--terra);
    font-family: var(--num);
    font-size: 14px;
    font-weight: 600;
    text-align: right;
    -moz-appearance: textfield;
  }
  .loss-input-wrap input::-webkit-outer-spin-button,
  .loss-input-wrap input::-webkit-inner-spin-button { margin: 0; -webkit-appearance: none; }
  .loss-input-wrap span { padding: 0 8px 0 2px; color: var(--terra); font-size: 11px; }
  .loss-input-wrap.compact input { width: 38px; height: 24px; padding-left: 5px; font-size: 12px; }
  .loss-input-wrap.compact span { padding-right: 5px; font-size: 10px; }

  .capital-wrap {
    display: flex;
    align-items: center;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: 9px;
    background: var(--paper);
  }
  .capital-wrap span {
    padding-left: 11px;
    color: var(--muted);
    font-family: var(--num);
    font-size: 13px;
  }
  .capital-wrap input {
    width: 100%;
    height: 36px;
    padding: 0 10px 0 5px;
    border: 0;
    background: transparent;
    color: var(--ink);
    font-family: var(--num);
    font-size: 14px;
    font-variant-numeric: tabular-nums;
    -moz-appearance: textfield;
  }
  .capital-wrap input::-webkit-outer-spin-button,
  .capital-wrap input::-webkit-inner-spin-button { margin: 0; -webkit-appearance: none; }

  .results { min-width: 0; }
  .kpi-grid {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 10px;
  }
  .kpi {
    position: relative;
    overflow: hidden;
    min-width: 0;
    padding: 15px 15px 13px;
    border: 1px solid var(--line);
    border-radius: 16px;
    background: var(--paper);
    box-shadow: var(--shadow-sm);
  }
  .kpi::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 3px;
    background: var(--line-strong);
  }
  .kpi-loss { background: linear-gradient(180deg, #fdf1ea 0%, var(--paper) 62%); }
  .kpi-loss::before { background: var(--terra); }
  .kpi-gain::before { background: var(--gold); }
  .kpi-left::before { background: var(--green-2); }
  .kpi-saved::before { background: var(--green); }
  .kpi-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    color: var(--muted);
    font-size: 10.5px;
    font-weight: 550;
    letter-spacing: .02em;
  }
  .kpi-code {
    color: var(--faint);
    font-family: var(--num);
    font-size: 7.5px;
    font-weight: 700;
    letter-spacing: .14em;
  }
  .kpi-value {
    overflow: hidden;
    margin: 9px 0 1px;
    color: var(--ink);
    font-family: var(--num);
    font-size: clamp(22px, 2vw, 30px);
    font-weight: 600;
    line-height: 1.15;
    letter-spacing: -.035em;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .kpi-value.loss { color: var(--terra); }
  .kpi-value.green { color: var(--green); }
  .kpi-value.is-long { font-size: clamp(18px, 1.55vw, 23px); letter-spacing: -.02em; }
  .kpi-meta {
    display: flex;
    align-items: baseline;
    gap: 5px;
    color: var(--muted);
    font-size: 9.5px;
    white-space: nowrap;
  }
  .kpi-meta b {
    color: var(--ink-2);
    font-family: var(--num);
    font-size: 11.5px;
    font-weight: 600;
  }
  .kpi-loss .kpi-meta b { color: var(--terra); }
  .kpi-saved .kpi-meta b { color: var(--green); }
  .kpi-foot {
    overflow: hidden;
    margin: 8px 0 0;
    padding-top: 7px;
    border-top: 1px dashed var(--line);
    color: var(--muted);
    font-size: 9px;
    line-height: 1.45;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  .strip-card {
    display: grid;
    grid-template-columns: minmax(220px, .72fr) minmax(0, 1.28fr);
    gap: 26px;
    align-items: center;
    margin-top: 10px;
    padding: 18px 20px;
    border: 1px solid var(--line);
    border-radius: 16px;
    background: var(--paper);
    box-shadow: var(--shadow-sm);
  }
  .strip-copy h3 {
    margin: 0 0 4px;
    font-size: 14px;
    font-weight: 750;
    letter-spacing: -.01em;
  }
  .strip-copy p {
    margin: 0;
    color: var(--muted);
    font-size: 10.5px;
    line-height: 1.55;
  }
  .strip-labels {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 6px;
    color: var(--muted);
    font-size: 9.5px;
  }
  .strip-labels b {
    margin-left: 3px;
    color: var(--terra);
    font-family: var(--num);
    font-size: 11px;
  }
  .strip-labels span:last-child b { color: var(--green); }
  .strip-bar {
    display: flex;
    gap: 2px;
    width: 100%;
    height: 17px;
    overflow: hidden;
    border-radius: 99px;
    background: #e7e4d9;
  }
  .strip-risk {
    flex: none;
    width: 11%;
    min-width: 2px;
    border-radius: 99px 3px 3px 99px;
    background: linear-gradient(90deg, #c96346, var(--terra));
    box-shadow: inset 0 0 0 1px rgba(255,255,255,.14);
    transition: width .25s ease;
  }
  .strip-safe { flex: 1; border-radius: 3px 99px 99px 3px; background: linear-gradient(90deg, #cfe1d6, #e1ece4); }
  .strip-amounts {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    margin-top: 6px;
    color: var(--muted);
    font-size: 9.5px;
  }
  .strip-amounts b {
    margin-left: 3px;
    color: var(--ink-2);
    font-family: var(--num);
    font-size: 11px;
  }
  .strip-amounts span:first-child b { color: var(--terra); }

  .card {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--paper);
    box-shadow: var(--shadow);
  }
  .chart-card { margin-top: 10px; padding: 20px 20px 14px; }
  .card-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 20px;
  }
  .chart-heading p, .card-head p {
    margin: 3px 0 0;
    color: var(--muted);
    font-size: 10.5px;
    line-height: 1.55;
  }
  .tabs {
    display: inline-flex;
    flex: none;
    gap: 3px;
    padding: 3px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: var(--paper-2);
  }
  .tab {
    padding: 6px 10px;
    border: 0;
    border-radius: 7px;
    background: transparent;
    color: var(--muted);
    font-size: 10.5px;
    font-weight: 600;
    white-space: nowrap;
    transition: .16s ease;
  }
  .tab:hover { color: var(--ink); }
  .tab.active { background: var(--dark); color: var(--cream); box-shadow: 0 2px 7px rgba(15,36,28,.16); }

  .chart-wrap {
    position: relative;
    width: 100%;
    margin-top: 8px;
    touch-action: pan-y;
  }
  #chartSvg { display: block; width: 100%; height: auto; overflow: visible; }
  #chartSvg text { font-family: var(--font); }
  .chart-tooltip {
    position: absolute;
    z-index: 5;
    width: 208px;
    padding: 10px 11px;
    border: 1px solid rgba(247, 241, 227, .12);
    border-radius: 10px;
    background: rgba(18, 36, 29, .96);
    color: var(--cream);
    box-shadow: 0 12px 32px rgba(15, 36, 28, .24);
    pointer-events: none;
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
  }
  .chart-tooltip[hidden] { display: none; }
  #chartSvg[hidden] { display: none; }
  .custom-chart { padding: 3px 0 1px; }
  .custom-summary {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1px;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--line);
  }
  .custom-summary > div { min-width: 0; padding: 10px 12px; background: var(--paper-2); }
  .custom-summary span { display: block; color: var(--muted); font-size: 9px; letter-spacing: .02em; }
  .custom-summary b {
    display: block;
    overflow: hidden;
    margin-top: 2px;
    color: var(--ink);
    font-family: var(--num);
    font-size: clamp(16px, 1.8vw, 22px);
    font-weight: 600;
    line-height: 1.2;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .custom-summary > div:first-child b { color: var(--terra); }
  .custom-list { margin-top: 10px; }
  .custom-row {
    display: grid;
    grid-template-columns: 86px minmax(0, 1fr) 112px;
    gap: 10px;
    align-items: center;
    padding: 7px 0;
    border-bottom: 1px dashed var(--line);
  }
  .custom-row:last-child { border-bottom: 0; }
  .custom-label { min-width: 0; }
  .custom-label b {
    display: block;
    overflow: hidden;
    color: var(--ink-2);
    font-size: 10px;
    font-weight: 650;
    line-height: 1.35;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .custom-label span { display: block; color: var(--muted); font-family: var(--num); font-size: 8.5px; line-height: 1.35; }
  .custom-track {
    height: 10px;
    overflow: hidden;
    border-radius: 99px;
    background: #ece7db;
  }
  .custom-track i {
    display: block;
    width: 0;
    height: 100%;
    border-radius: inherit;
    background: linear-gradient(90deg, #d97757, var(--terra));
    transition: width .18s ease;
  }
  .custom-amount { min-width: 0; text-align: right; }
  .custom-amount b {
    display: block;
    overflow: hidden;
    color: var(--terra);
    font-family: var(--num);
    font-size: 11.5px;
    font-weight: 600;
    line-height: 1.3;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .custom-amount small { display: block; color: var(--muted); font-family: var(--num); font-size: 8px; line-height: 1.35; white-space: nowrap; }
  .custom-total {
    display: grid;
    grid-template-columns: 86px minmax(0, 1fr) 112px;
    gap: 10px;
    align-items: center;
    margin-top: 6px;
    padding: 9px 10px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: var(--paper-2);
  }
  .custom-total .custom-label b { color: var(--ink); }
  .custom-total .custom-track { background: #e7e2d6; }
  .custom-total .custom-track i { background: var(--ink); }
  .custom-total .custom-amount b { color: var(--ink); }
  .custom-empty {
    margin: 9px 0 0;
    padding: 9px 11px;
    border: 1px dashed var(--line);
    border-radius: 9px;
    background: var(--paper-2);
    color: var(--muted);
    font-size: 10px;
    line-height: 1.55;
  }
  .chart-card.is-custom .chart-foot { justify-content: flex-start; }
  .chart-card.is-custom .chart-foot p { text-align: left; }
  .tt-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 7px;
    padding-bottom: 7px;
    border-bottom: 1px solid rgba(247, 241, 227, .12);
    color: rgba(247, 241, 227, .58);
    font-size: 9.5px;
  }
  .tt-head b { color: var(--cream); font-family: var(--num); font-size: 16px; font-weight: 550; }
  .tt-row {
    display: grid;
    grid-template-columns: 7px 1fr auto;
    align-items: center;
    gap: 6px;
    padding: 2px 0;
    color: rgba(247, 241, 227, .72);
    font-size: 9.5px;
  }
  .tt-row b { color: var(--cream); font-family: var(--num); font-size: 11px; font-weight: 550; }
  .tt-row b.loss { color: #ef9a7d; }
  .tt-dot { width: 6px; height: 6px; border-radius: 50%; }
  .tt-dot.current { background: #65b394; }
  .tt-dot.base { background: #d97757; }
  .tt-sep { height: 1px; margin: 5px 0; background: rgba(247, 241, 227, .1); }
  .tt-row.plain { grid-template-columns: 1fr auto; }
  .chart-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 18px;
    margin-top: 3px;
    padding-top: 11px;
    border-top: 1px solid var(--line);
  }
  .legend { display: flex; flex-wrap: wrap; gap: 14px; color: var(--muted); font-size: 9.5px; }
  .legend span { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
  .legend-line { display: inline-block; width: 18px; height: 0; border-top: 2px solid var(--green); }
  .legend-line.base { border-top: 2px dashed var(--terra); }
  .chart-foot p { margin: 0; color: var(--muted); font-size: 9.5px; line-height: 1.5; text-align: right; }

  .ladder-card { margin-top: 24px; padding: 24px 24px 18px; }
  .ladder-badge {
    flex: none;
    padding: 6px 11px;
    border: 1px solid rgba(15, 92, 71, .16);
    border-radius: 999px;
    background: var(--green-wash);
    color: var(--green);
    font-family: var(--num);
    font-size: 11px;
    font-weight: 600;
    white-space: nowrap;
  }
  .ladder { margin-top: 18px; border-top: 1px solid var(--line); }
  .ladder-row {
    display: grid;
    grid-template-columns: 37px minmax(150px, 1.25fr) repeat(4, minmax(74px, .7fr));
    gap: 12px;
    align-items: center;
    padding: 12px 8px 11px;
    border-bottom: 1px solid var(--line);
  }
  .ladder-row.current {
    margin: 0 -8px;
    padding-right: 16px;
    padding-left: 16px;
    border-radius: 12px;
    background: var(--green-wash);
    box-shadow: inset 3px 0 0 var(--green);
  }
  .ladder-no {
    display: grid;
    place-items: center;
    width: 29px;
    height: 29px;
    border: 1px solid var(--line);
    border-radius: 9px;
    background: var(--paper-2);
    color: var(--muted);
    font-family: var(--num);
    font-size: 10px;
    font-weight: 600;
  }
  .current .ladder-no { border-color: var(--green); background: var(--green); color: var(--cream); }
  .ladder-name { min-width: 0; }
  .ladder-name strong { display: block; font-size: 12px; line-height: 1.35; font-weight: 700; }
  .ladder-name small { display: block; margin-top: 1px; color: var(--muted); font-size: 9.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .ladder-metric { min-width: 0; }
  .ladder-metric span { display: block; color: var(--faint); font-size: 8.5px; letter-spacing: .03em; }
  .ladder-metric b {
    display: block;
    margin-top: 1px;
    color: var(--ink);
    font-family: var(--num);
    font-size: 13px;
    font-weight: 600;
    white-space: nowrap;
  }
  .ladder-metric b.loss { color: var(--terra); }
  .ladder-metric b.good { color: var(--green); }
  .ladder-metric small { display: block; color: var(--muted); font-family: var(--num); font-size: 8.5px; white-space: nowrap; }
  .ladder-track {
    grid-column: 1 / -1;
    height: 4px;
    margin-top: -3px;
    overflow: hidden;
    border-radius: 99px;
    background: #eee9dd;
  }
  .ladder-track i {
    display: block;
    width: 50%;
    height: 100%;
    border-radius: inherit;
    background: #c9c1ae;
    transition: width .28s ease;
  }
  .ladder-row.base .ladder-track i { background: linear-gradient(90deg, #d08c74, var(--terra)); }
  .ladder-row.split .ladder-track i { background: linear-gradient(90deg, #d8bd7c, var(--gold)); }
  .ladder-row.current .ladder-track i { background: linear-gradient(90deg, #54a083, var(--green)); }
  .ladder-row.current .ladder-metric b { color: var(--green); }
  .ladder-row.current .ladder-metric b.loss { color: var(--terra); }
  .insight {
    display: flex;
    gap: 11px;
    align-items: flex-start;
    margin-top: 14px;
    padding: 12px 14px;
    border: 1px solid var(--line);
    border-radius: 11px;
    background: var(--paper-2);
    color: var(--ink-2);
    font-size: 11px;
    line-height: 1.7;
  }
  .insight-icon {
    display: grid;
    place-items: center;
    flex: none;
    width: 20px;
    height: 20px;
    margin-top: 1px;
    border-radius: 50%;
    background: var(--gold-soft);
    color: #7d5a18;
    font-family: var(--num);
    font-size: 11px;
    font-weight: 700;
  }
  .insight b { color: var(--green); }
  .insight .warn { color: var(--terra); }

  .stocks-section { margin-top: 30px; }
  .section-head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 20px;
    margin-bottom: 13px;
  }
  .section-head p { margin: 3px 0 0; color: var(--muted); font-size: 10.5px; line-height: 1.55; }
  .stock-head-right {
    display: flex;
    flex: none;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: 7px;
  }
  .stock-summary {
    flex: none;
    padding: 6px 10px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: rgba(255, 253, 248, .55);
    color: var(--muted);
    font-family: var(--num);
    font-size: 10px;
    white-space: nowrap;
  }
  .stock-tools {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
  }
  .stock-tools > span { color: var(--faint); font-size: 9px; }
  .tool-chip {
    padding: 3px 7px;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: var(--paper);
    color: var(--muted);
    font-family: var(--num);
    font-size: 9px;
    line-height: 1.5;
    transition: .15s ease;
  }
  .tool-chip:hover { border-color: var(--terra); color: var(--terra); background: var(--terra-soft); }
  .stock-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 10px;
  }
  .stock-card {
    position: relative;
    overflow: hidden;
    padding: 14px 14px 12px;
    border: 1px solid var(--line);
    border-radius: 15px;
    background: var(--paper);
    box-shadow: var(--shadow-sm);
  }
  .stock-card::after {
    content: attr(data-index);
    position: absolute;
    right: 9px;
    bottom: -15px;
    color: rgba(24, 35, 29, .035);
    font-family: var(--num);
    font-size: 68px;
    font-weight: 700;
    line-height: 1;
    pointer-events: none;
  }
  .stock-top {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    color: var(--muted);
    font-family: var(--num);
    font-size: 9.5px;
    letter-spacing: .04em;
  }
  .stock-tag {
    padding: 2px 6px;
    border-radius: 5px;
    background: var(--green-wash);
    color: var(--green);
    font-family: var(--font);
    font-size: 8.5px;
    letter-spacing: 0;
    white-space: nowrap;
  }
  .stock-tag.empty { background: #eee9dd; color: var(--muted); }
  .stock-tag.idle { background: var(--gold-soft); color: #7d5a18; }
  .stock-tag.target { background: var(--terra-soft); color: var(--terra); }
  .stock-card.is-empty { border-style: dashed; background: #faf7ef; }
  .stock-card.is-empty .stock-alloc b { color: var(--faint); }
  .stock-card.is-empty .stock-split { opacity: .55; }
  .stock-card.is-empty .stock-settings,
  .stock-card.is-empty .custom-editor,
  .stock-card.is-empty .stock-rows { opacity: .6; }
  .stock-card.is-target { border-color: rgba(189, 88, 59, .42); box-shadow: 0 0 0 1px rgba(189, 88, 59, .06), var(--shadow-sm); }
  .stock-alloc {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: baseline;
    gap: 2px;
    margin-top: 8px;
    line-height: 1;
  }
  .stock-alloc b {
    color: var(--ink);
    font-family: var(--num);
    font-size: 27px;
    font-weight: 600;
    letter-spacing: -.035em;
  }
  .stock-alloc span { color: var(--muted); font-family: var(--num); font-size: 12px; }
  .stock-alloc-label {
    position: relative;
    z-index: 1;
    margin: 1px 0 9px;
    color: var(--muted);
    font-size: 9px;
  }
  .stock-split {
    position: relative;
    z-index: 1;
    display: flex;
    gap: 2px;
    height: 5px;
    overflow: hidden;
    border-radius: 99px;
    background: #ece7db;
  }
  .stock-split i { display: block; height: 100%; border-radius: inherit; }
  .stock-split .active { background: linear-gradient(90deg, #55a486, var(--green-2)); }
  .stock-split .reserve { flex: 1; background: #ded8c9; }
  .stock-rows {
    position: relative;
    z-index: 1;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 6px;
    margin-top: 9px;
  }
  .stock-rows > div { min-width: 0; }
  .stock-rows span { display: block; color: var(--faint); font-size: 8.5px; }
  .stock-rows b { display: block; color: var(--ink-2); font-family: var(--num); font-size: 10.5px; font-weight: 600; }
  .stock-settings {
    position: relative;
    z-index: 1;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 6px;
    margin-top: 9px;
    padding-top: 8px;
    border-top: 1px dashed var(--line);
  }
  .stock-settings.one { grid-template-columns: 1fr; }
  .stock-field { min-width: 0; }
  .stock-field-label { display: block; margin-bottom: 3px; color: var(--muted); font-size: 8.5px; }
  .stock-number {
    display: flex;
    align-items: center;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: 7px;
    background: var(--paper);
  }
  .stock-number input {
    width: 100%;
    height: 26px;
    padding: 0 2px 0 7px;
    border: 0;
    background: transparent;
    color: var(--green);
    font-family: var(--num);
    font-size: 12px;
    font-weight: 600;
    text-align: right;
    -moz-appearance: textfield;
  }
  .stock-number input::-webkit-outer-spin-button,
  .stock-number input::-webkit-inner-spin-button { margin: 0; -webkit-appearance: none; }
  .stock-number span { padding: 0 6px 0 2px; color: var(--muted); font-size: 9px; }
  .stock-number input:disabled,
  .stock-card.is-empty .loss-input-wrap input:disabled,
  .custom-range:disabled { opacity: .55; cursor: not-allowed; }
  .stock-loss {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
    margin-top: 10px;
    padding-top: 8px;
    border-top: 1px dashed var(--line);
    color: var(--muted);
    font-size: 9px;
  }
  .stock-loss b { color: var(--terra); font-family: var(--num); font-size: 12px; font-weight: 600; white-space: nowrap; }
  .stock-loss-extra { position: relative; z-index: 1; margin-top: 4px; color: var(--muted); font-family: var(--num); font-size: 8.5px; text-align: right; }
  .custom-editor {
    position: relative;
    z-index: 1;
    margin-top: 9px;
    padding-top: 8px;
    border-top: 1px dashed var(--line);
  }
  .custom-editor-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 4px;
    color: var(--muted);
    font-size: 9px;
  }
  .custom-range { height: 22px; }
  .custom-range::-webkit-slider-runnable-track { height: 4px; }
  .stock-contrib {
    position: relative;
    z-index: 1;
    margin-top: 4px;
    color: var(--muted);
    font-family: var(--num);
    font-size: 8.5px;
    text-align: right;
  }
  .stock-card.custom-card .stock-loss { margin-top: 7px; }
  .stock-card.custom-card::after { display: none; }

  .method {
    display: grid;
    grid-template-columns: minmax(0, 1.18fr) minmax(300px, .82fr);
    gap: 14px;
    margin-top: 34px;
  }
  .method-card, .note-card {
    padding: 22px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: rgba(255, 253, 248, .72);
  }
  .method-card h2 { margin-bottom: 10px; }
  .calc-line {
    padding: 10px 12px;
    border-left: 2px solid var(--green);
    border-radius: 0 8px 8px 0;
    background: var(--paper-2);
    color: var(--ink-2);
    font-family: var(--num);
    font-size: 11px;
    line-height: 1.65;
    word-break: break-word;
  }
  .calc-line b { color: var(--terra); }
  .formula-list { margin-top: 12px; }
  .formula-row {
    display: grid;
    grid-template-columns: 106px minmax(0, 1fr);
    gap: 12px;
    padding: 7px 0;
    border-bottom: 1px dashed var(--line);
    font-size: 10.5px;
  }
  .formula-row:last-child { border-bottom: 0; }
  .formula-row span { color: var(--muted); }
  .formula-row code {
    color: var(--ink-2);
    font-family: var(--num);
    font-size: 10.5px;
    line-height: 1.65;
  }
  .note-card h3 { margin: 0 0 9px; font-size: 14px; font-weight: 750; }
  .note-card ol { margin: 0; padding-left: 17px; }
  .note-card li {
    margin-bottom: 8px;
    color: var(--ink-2);
    font-size: 10.5px;
    line-height: 1.65;
  }
  .note-card li::marker { color: var(--gold); font-family: var(--num); font-size: 9px; }
  .note-card b { color: var(--ink); }

  footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 20px;
    margin-top: 28px;
    padding-top: 17px;
    border-top: 1px solid var(--line);
    color: var(--faint);
    font-size: 9.5px;
    line-height: 1.6;
  }
  footer .footer-mark { font-family: var(--num); letter-spacing: .12em; white-space: nowrap; }

  .mobile-readout { display: none; }

  [url=home.php?mod=space&uid=945662]@media[/url] (max-width: 1180px) {
    .hero { grid-template-columns: minmax(0, 1.2fr) minmax(330px, .8fr); gap: 28px; }
    .layout { grid-template-columns: 338px minmax(0, 1fr); }
    .kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .ladder-row { grid-template-columns: 34px minmax(130px, 1.2fr) repeat(4, minmax(66px, .7fr)); gap: 9px; }
  }
  @media (max-width: 980px) {
    .page { width: min(100% - 36px, 820px); padding-bottom: 44px; }
    .hero { grid-template-columns: 1fr; gap: 24px; padding-top: 32px; }
    .exposure-card { min-height: 0; }
    .layout { grid-template-columns: 1fr; }
    .controls { position: static; max-height: none; overflow: visible; }
    .preset-grid { grid-template-columns: repeat(3, 1fr); }
    .kpi-grid { grid-template-columns: repeat(4, 1fr); }
    .strip-card { grid-template-columns: 1fr; gap: 14px; }
    .method { grid-template-columns: 1fr; }
    .mobile-readout.is-visible {
      position: fixed;
      left: 14px;
      right: 14px;
      bottom: max(12px, env(safe-area-inset-bottom));
      z-index: 40;
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 1px;
      overflow: hidden;
      padding: 9px 11px;
      border: 1px solid rgba(247, 241, 227, .13);
      border-radius: 14px;
      background: rgba(18, 36, 29, .96);
      color: var(--cream);
      box-shadow: 0 14px 38px rgba(15, 36, 28, .26);
      -webkit-backdrop-filter: blur(12px);
      backdrop-filter: blur(12px);
    }
    .mobile-readout > div {
      min-width: 0;
      padding: 0 9px;
      border-left: 1px solid rgba(247, 241, 227, .12);
    }
    .mobile-readout > div:first-child { border-left: 0; padding-left: 2px; }
    .mobile-readout span {
      display: block;
      color: rgba(247, 241, 227, .5);
      font-size: 8px;
      letter-spacing: .03em;
    }
    .mobile-readout b {
      display: block;
      overflow: hidden;
      margin-top: 1px;
      color: var(--cream);
      font-family: var(--num);
      font-size: 13px;
      font-weight: 550;
      white-space: nowrap;
      text-overflow: ellipsis;
    }
    .mobile-readout > div:first-child b { color: #ef9a7d; }
  }
  @media (max-width: 760px) {
    .page { width: calc(100% - 28px); padding-top: 15px; }
    .topbar { padding-bottom: 15px; }
    .top-note { display: none; }
    .hero { padding: 28px 0 26px; }
    .hero h1 { font-size: clamp(40px, 10vw, 58px); }
    .hero-lead { font-size: 13.5px; line-height: 1.8; }
    .hero-points { gap: 6px; margin-top: 21px; }
    .hero-points > div { padding: 8px; }
    .hero-points b { font-size: 11px; }
    .hero-points small { font-size: 9px; }
    .exposure-card { padding: 22px; border-radius: 23px; }
    .exposure-value > span:first-child { font-size: 62px; }
    .kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
    .kpi { padding: 13px 12px 11px; }
    .kpi-value { font-size: 22px; }
    .kpi-foot { font-size: 8.5px; }
    .chart-card { padding: 16px 13px 12px; }
    .card-head { flex-direction: column; gap: 11px; }
    .tabs { align-self: stretch; display: grid; grid-template-columns: 1fr 1fr; }
    .tab { text-align: center; }
    .chart-foot { flex-direction: column; align-items: flex-start; gap: 7px; }
    .chart-foot p { text-align: left; }
    .ladder-card { padding: 18px 14px 14px; }
    .ladder-row {
      grid-template-columns: 30px 1fr 1fr;
      gap: 8px 10px;
      padding: 11px 6px 10px;
    }
    .ladder-row.current { margin: 0 -6px; padding-right: 10px; padding-left: 10px; }
    .ladder-no { grid-column: 1; grid-row: 1; }
    .ladder-name { grid-column: 2 / 4; grid-row: 1; }
    .metric-exposure { grid-column: 2; grid-row: 2; }
    .metric-invested { grid-column: 3; grid-row: 2; }
    .metric-loss { grid-column: 2; grid-row: 3; }
    .metric-gain { grid-column: 3; grid-row: 3; }
    .ladder-track { grid-column: 1 / -1; grid-row: 4; }
    .section-head { align-items: flex-start; flex-direction: column; gap: 8px; }
    .stock-head-right { align-items: flex-start; justify-content: flex-start; }
    .custom-row, .custom-total { grid-template-columns: 66px minmax(0, 1fr) 96px; gap: 8px; }
    .custom-summary > div { padding: 9px 10px; }
    .custom-summary b { font-size: 17px; }
    .stock-grid { grid-template-columns: repeat(auto-fill, minmax(155px, 1fr)); }
    .stock-alloc b { font-size: 23px; }
    .method { margin-top: 26px; }
    .method-card, .note-card { padding: 18px; }
    .formula-row { grid-template-columns: 90px minmax(0, 1fr); gap: 8px; }
    footer { flex-direction: column; align-items: flex-start; gap: 5px; }
  }
  @media (max-width: 460px) {
    .page { width: calc(100% - 20px); }
    .brand-mark { width: 37px; height: 37px; border-radius: 11px; font-size: 18px; }
    .brand-copy strong { font-size: 13px; }
    .brand-copy span { font-size: 8px; }
    .hero h1 { font-size: 39px; letter-spacing: -.045em; }
    .hero h1 em { white-space: normal; }
    .hero-points { grid-template-columns: 1fr; }
    .hero-points > div { grid-template-columns: auto 1fr auto; grid-template-rows: auto; align-items: center; }
    .hero-points span { grid-row: auto; }
    .hero-points small { text-align: right; }
    .preset-grid { grid-template-columns: 1fr 1fr; }
    .preset:last-child { grid-column: 1 / -1; }
    .segmented-3 button { padding-right: 2px; padding-left: 2px; font-size: 9.5px; }
    .kpi-grid { grid-template-columns: 1fr 1fr; }
    .kpi-value { font-size: 20px; }
    .kpi-code { display: none; }
    .strip-card { padding: 15px; }
    .strip-labels, .strip-amounts { font-size: 8.5px; }
    .ladder-row { grid-template-columns: 27px 1fr 1fr; }
    .ladder-metric b { font-size: 11.5px; }
    .stock-grid { grid-template-columns: 1fr 1fr; gap: 7px; }
    .stock-card:last-child:nth-child(odd) { grid-column: 1 / -1; }
    .stock-card { padding: 11px 10px 10px; }
    .stock-alloc b { font-size: 21px; }
    .stock-rows b { font-size: 9.5px; }
    .stock-loss { font-size: 8.5px; }
    .stock-loss b { font-size: 10.5px; }
    .custom-row, .custom-total { grid-template-columns: 56px minmax(0, 1fr) 84px; gap: 6px; }
    .custom-label b { font-size: 9px; }
    .custom-label span { font-size: 8px; }
    .custom-amount b { font-size: 10.5px; }
    .custom-amount small { font-size: 7.5px; }
    .custom-summary span { font-size: 8px; }
    .custom-summary b { font-size: 15px; }
    .stock-tools > span { font-size: 8px; }
    .formula-row { grid-template-columns: 1fr; gap: 2px; }
  }

  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; animation: none !important; }
  }


  /* ============================================================
     补仓计算器 · 新增组件(沿用原页面视觉语言)
     ============================================================ */

  .unit-sign {
    margin-top: 9px;
    color: var(--gold-soft);
    font-family: var(--num);
    font-size: 25px;
    font-weight: 500;
  }

  .kpi-breakeven::before { background: var(--gold); }
  .kpi-add::before { background: var(--green-2); }
  .kpi-total::before { background: var(--dark); }
  .kpi-post::before { background: var(--terra); }
  .kpi-post.is-gain::before { background: var(--green); }
  .kpi-value.gold { color: var(--gold); }
  .kpi-value.green { color: var(--green); }
  .kpi-value.terra { color: var(--terra); }
  .kpi-value.ink { color: var(--ink); }
  .kpi-value .kpi-unit {
    margin-left: 3px;
    font-size: .55em;
    font-weight: 500;
    letter-spacing: 0;
    color: var(--muted);
  }
  .kpi-post .kpi-value { font-size: clamp(18px, 1.65vw, 25px); letter-spacing: -.025em; }

  .segmented-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .segmented-4 button { padding-right: 2px; padding-left: 2px; font-size: 9.5px; }

  .chart-head { align-items: flex-start; }
  .chart-actions {
    display: flex;
    flex: none;
    flex-direction: column;
    align-items: flex-end;
    gap: 7px;
  }
  .chart-tooltip.wide { width: 236px; }
  .legend-line.target { border-top: 2px dashed var(--gold); }
  .legend-line.price { border-top: 2px dashed var(--green-2); }
  .loss-input-wrap.gain input { color: var(--green); }
  .loss-input-wrap.gain span { color: var(--green); }

  .code-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 6px;
  }
  .code-row input {
    width: 100%;
    height: 36px;
    padding: 0 10px;
    border: 1px solid var(--line);
    border-radius: 9px;
    outline: none;
    background: var(--paper);
    color: var(--ink);
    font-family: var(--num);
    font-size: 13px;
    letter-spacing: .02em;
  }
  .code-row input:focus { border-color: var(--green); }
  .fetch-btn {
    flex: none;
    height: 36px;
    padding: 0 11px;
    border: 1px solid var(--green-2);
    border-radius: 9px;
    background: var(--green-wash);
    color: var(--green);
    font-size: 10.5px;
    font-weight: 650;
    white-space: nowrap;
    transition: .16s ease;
  }
  .fetch-btn:hover { background: var(--green-soft); }
  .fetch-btn:disabled { opacity: .55; cursor: wait; }
  .stock-meta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-top: 6px;
    color: var(--muted);
    font-size: 9.5px;
  }
  .stock-name { color: var(--ink-2); font-weight: 650; }
  .status-line { color: var(--muted); font-size: 9.5px; line-height: 1.45; }
  .status-line.ok { color: var(--green-2); }
  .status-line.bad { color: var(--terra); }
  .status-line.busy { color: var(--gold); }

  .unit-wrap {
    display: flex;
    align-items: center;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: 9px;
    background: var(--paper);
  }
  .unit-wrap input {
    width: 100%;
    height: 36px;
    padding: 0 4px 0 10px;
    border: 0;
    outline: none;
    background: transparent;
    color: var(--ink);
    font-family: var(--num);
    font-size: 14px;
    font-variant-numeric: tabular-nums;
    -moz-appearance: textfield;
  }
  .unit-wrap input::-webkit-outer-spin-button,
  .unit-wrap input::-webkit-inner-spin-button { margin: 0; -webkit-appearance: none; }
  .unit-wrap span {
    padding-right: 10px;
    color: var(--muted);
    font-size: 11px;
  }

  .switch-row {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 9px 11px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: var(--paper-2);
    color: var(--ink-2);
    font-size: 11px;
    line-height: 1.45;
    cursor: pointer;
    transition: .16s ease;
  }
  .switch-row:hover { border-color: var(--green-2); }
  .switch-row input {
    flex: none;
    width: 15px;
    height: 15px;
    margin: 0;
    accent-color: var(--green);
    cursor: pointer;
  }

  .ruler { position: relative; height: 40px; margin: 4px 0 0; }
  .ruler-track {
    position: absolute;
    left: 0;
    right: 0;
    top: 17px;
    height: 8px;
    border-radius: 99px;
    background: linear-gradient(90deg, #ded8c9, #ece7db);
    box-shadow: inset 0 0 0 1px rgba(24, 35, 29, .04);
  }
  .ruler-gap {
    position: absolute;
    top: 18px;
    height: 6px;
    border-radius: 99px;
    background: linear-gradient(90deg, var(--terra-2), var(--gold));
    opacity: .8;
    transition: left .2s ease, width .2s ease;
  }
  .ruler-mark {
    position: absolute;
    top: 21px;
    width: 14px;
    height: 14px;
    border: 3px solid var(--paper);
    border-radius: 50%;
    transform: translate(-50%, -50%);
    box-shadow: 0 1px 5px rgba(15, 36, 28, .22);
    transition: left .2s ease;
  }
  .ruler-mark.price { background: var(--terra); }
  .ruler-mark.breakeven { width: 17px; height: 17px; background: var(--gold); }
  .ruler-mark.cost { background: var(--green); }
  .ruler-legend {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 8px;
    margin-top: 8px;
  }
  .ruler-legend > div {
    min-width: 0;
    padding: 7px 9px;
    border: 1px solid var(--line);
    border-radius: 9px;
    background: var(--paper-2);
  }
  .ruler-legend span {
    display: flex;
    align-items: center;
    gap: 5px;
    color: var(--muted);
    font-size: 8.5px;
    white-space: nowrap;
  }
  .ruler-legend i {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    flex: none;
  }
  .ruler-legend i.price { background: var(--terra); }
  .ruler-legend i.breakeven { background: var(--gold); }
  .ruler-legend i.cost { background: var(--green); }
  .ruler-legend b {
    display: block;
    overflow: hidden;
    margin-top: 2px;
    color: var(--ink);
    font-family: var(--num);
    font-size: 13px;
    font-weight: 600;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .ruler-legend > div:first-child b { color: var(--terra); }
  .ruler-legend > div:nth-child(2) b { color: var(--gold); }
  .ruler-legend > div:nth-child(3) b { color: var(--green); }
  .ruler-rise {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 6px;
    margin-top: 6px;
    color: var(--muted);
    font-size: 9.5px;
  }
  .ruler-rise b { color: var(--terra); font-family: var(--num); font-size: 13px; font-weight: 650; }

  #beSvg { display: block; width: 100%; height: auto; overflow: visible; }
  #beSvg text { font-family: var(--font); }

  .cost-card { margin-top: 12px; padding: 22px 22px 18px; }
  .cost-grid {
    display: grid;
    grid-template-columns: minmax(0, 1.12fr) minmax(250px, .88fr);
    gap: 24px;
    align-items: center;
    margin-top: 12px;
  }
  .cost-panel h3, .donut-panel h3 {
    margin: 0 0 8px;
    color: var(--ink-2);
    font-size: 12px;
    font-weight: 700;
    letter-spacing: .01em;
  }
  .cost-bars {
    display: flex;
    align-items: flex-end;
    gap: 18px;
    height: 210px;
    padding: 8px 4px 0;
    border-bottom: 1px solid var(--line);
  }
  .cost-item {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-end;
    height: 100%;
    min-width: 0;
  }
  .cost-value {
    color: var(--ink-2);
    font-family: var(--num);
    font-size: 12px;
    font-weight: 600;
    white-space: nowrap;
  }
  .cost-track {
    display: flex;
    align-items: flex-end;
    width: 100%;
    max-width: 88px;
    height: 150px;
    margin: 5px 0 0;
  }
  .cost-fill {
    width: 100%;
    min-height: 4px;
    border-radius: 7px 7px 2px 2px;
    transition: height .22s ease;
  }
  .cost-item.before .cost-fill { background: linear-gradient(180deg, #d97757, var(--terra)); }
  .cost-item.price .cost-fill { background: linear-gradient(180deg, #67b08f, var(--green-2)); }
  .cost-item.after .cost-fill { background: linear-gradient(180deg, #d8bd7c, var(--gold)); }
  .cost-item.after .cost-value { color: var(--gold); }
  .cost-label {
    margin-top: 7px;
    color: var(--muted);
    font-size: 9.5px;
    white-space: nowrap;
  }
  .cost-delta { margin: 10px 0 0; color: var(--muted); font-size: 10px; text-align: center; }
  .cost-delta b { color: var(--gold); font-family: var(--num); font-size: 12px; }

  .donut-wrap {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
  }
  .donut {
    position: relative;
    display: grid;
    place-items: center;
    width: 172px;
    height: 172px;
    border-radius: 50%;
    background: conic-gradient(var(--green-2) 0 var(--orig, 100%), var(--gold) var(--orig, 100%) 100%);
    box-shadow: 0 10px 28px rgba(15, 36, 28, .08);
  }
  .donut::after {
    content: "";
    position: absolute;
    inset: 27px;
    border-radius: 50%;
    background: var(--paper);
    box-shadow: inset 0 0 0 1px rgba(24, 35, 29, .04);
  }
  .donut-hole { position: relative; z-index: 1; text-align: center; line-height: 1.1; }
  .donut-hole b {
    display: block;
    color: var(--green);
    font-family: var(--num);
    font-size: 23px;
    font-weight: 600;
    letter-spacing: -.03em;
  }
  .donut-hole span { display: block; margin-top: 3px; color: var(--muted); font-size: 8.5px; }
  .donut-legend {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px 14px;
    color: var(--muted);
    font-size: 9px;
  }
  .donut-legend span { display: inline-flex; align-items: center; gap: 5px; }
  .donut-legend i { width: 8px; height: 8px; border-radius: 50%; }
  .donut-legend i.orig { background: var(--green-2); }
  .donut-legend i.add { background: var(--gold); }
  .donut-legend b { color: var(--ink-2); font-family: var(--num); font-size: 10px; }

  .be-table-card { margin-top: 12px; padding: 22px 22px 16px; }
  .be-table-wrap {
    max-height: none;
    margin-top: 14px;
    overflow: auto;
    border: 1px solid var(--line);
    border-radius: 14px;
    scrollbar-width: thin;
    scrollbar-color: var(--line-strong) transparent;
  }
  .be-table-wrap::-webkit-scrollbar { width: 6px; height: 6px; }
  .be-table-wrap::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 99px; }
  table.be-table {
    width: 100%;
    min-width: 620px;
    border-collapse: collapse;
    font-family: var(--num);
    font-size: 11px;
    font-variant-numeric: tabular-nums;
  }
  table.be-table thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    padding: 9px 12px;
    border-bottom: 1px solid var(--line);
    background: #f6f1e6;
    color: var(--muted);
    font-family: var(--font);
    font-size: 9px;
    font-weight: 600;
    text-align: right;
    white-space: nowrap;
  }
  table.be-table thead th:first-child {
    position: sticky;
    left: 0;
    z-index: 3;
    background: #f6f1e6;
    text-align: left;
    box-shadow: 1px 0 0 var(--line);
  }
  table.be-table tbody td {
    padding: 7px 12px;
    border-bottom: 1px dashed var(--line);
    color: var(--ink-2);
    text-align: right;
    white-space: nowrap;
  }
  table.be-table tbody td:first-child {
    position: sticky;
    left: 0;
    z-index: 1;
    background: var(--paper);
    color: var(--muted);
    font-family: var(--font);
    font-size: 10px;
    text-align: left;
    box-shadow: 1px 0 0 var(--line);
  }
  table.be-table tbody tr:last-child td { border-bottom: 0; }
  table.be-table tbody tr.is-selected td { background: var(--gold-soft); }
  table.be-table tbody tr.is-selected td:first-child {
    background: var(--gold-soft);
    color: #7d5a18;
    font-weight: 700;
  }
  table.be-table tbody tr.is-zero td { color: var(--muted); }
  table.be-table td.good { color: var(--green); }
  table.be-table td.gold { color: var(--gold); font-weight: 600; }
  table.be-table td.loss { color: var(--terra); }
  .table-hint { margin: 8px 0 0; color: var(--muted); font-size: 9.5px; }

  .batch-card { margin-top: 24px; padding: 24px 24px 18px; }
  .batch-tools {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    margin-top: 14px;
  }
  .batch-tools .tool-label { color: var(--faint); font-size: 9px; }
  .batch-btn {
    padding: 5px 9px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--paper);
    color: var(--muted);
    font-size: 10px;
    line-height: 1.5;
    white-space: nowrap;
    transition: .16s ease;
  }
  .batch-btn:hover { border-color: var(--green-2); color: var(--green); background: var(--green-wash); }
  .batch-btn.primary { border-color: var(--green); background: var(--green); color: var(--cream); }
  .batch-btn.primary:hover { background: var(--green-2); color: var(--cream); }
  .batch-btn:disabled { opacity: .55; cursor: wait; }
  .batch-status { margin-left: auto; }
  .batch-wrap {
    margin-top: 12px;
    max-height: 640px;
    overflow: auto;
    border: 1px solid var(--line);
    border-radius: 14px;
    scrollbar-width: thin;
    scrollbar-color: var(--line-strong) transparent;
  }
  .batch-wrap::-webkit-scrollbar { width: 7px; height: 7px; }
  .batch-wrap::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 99px; }
  table.batch {
    width: 100%;
    min-width: 1080px;
    border-collapse: collapse;
    table-layout: fixed;
    font-family: var(--num);
    font-size: 11px;
    font-variant-numeric: tabular-nums;
  }
  table.batch th:nth-child(1) { width: 158px; }
  table.batch th:nth-child(2) { width: 92px; }
  table.batch th:nth-child(3) { width: 88px; }
  table.batch th:nth-child(4) { width: 88px; }
  table.batch th:nth-child(5) { width: 82px; }
  table.batch th:nth-child(6) { width: 92px; }
  table.batch th:nth-child(7) { width: 120px; }
  table.batch th:nth-child(8) { width: 90px; }
  table.batch th:nth-child(9) { width: 82px; }
  table.batch th:nth-child(10) { width: 104px; }
  table.batch thead th {
    position: sticky;
    top: 0;
    z-index: 3;
    padding: 8px 8px;
    border-bottom: 1px solid var(--line);
    background: #f6f1e6;
    color: var(--muted);
    font-family: var(--font);
    font-size: 8.5px;
    font-weight: 600;
    text-align: right;
    white-space: nowrap;
  }
  table.batch thead th:first-child {
    position: sticky;
    left: 0;
    z-index: 4;
    background: #f6f1e6;
    text-align: left;
    box-shadow: 1px 0 0 var(--line);
  }
  table.batch thead th:last-child { text-align: center; }
  table.batch tbody td {
    padding: 5px 6px;
    border-bottom: 1px dashed var(--line);
    color: var(--ink-2);
    text-align: right;
    white-space: nowrap;
    vertical-align: middle;
  }
  table.batch tbody tr:last-child td { border-bottom: 0; }
  table.batch td:first-child {
    position: sticky;
    left: 0;
    z-index: 2;
    background: var(--paper);
    text-align: left;
    box-shadow: 1px 0 0 var(--line);
  }
  table.batch td.cell-actions { text-align: center; }
  table.batch input {
    width: 100%;
    max-width: 92px;
    min-width: 0;
    height: 27px;
    padding: 0 6px;
    border: 1px solid rgba(24, 35, 29, .09);
    border-radius: 6px;
    outline: none;
    background: rgba(255, 253, 248, .55);
    color: var(--ink-2);
    font-family: var(--num);
    font-size: 11px;
    text-align: right;
    -moz-appearance: textfield;
    transition: .14s ease;
  }
  table.batch input::-webkit-outer-spin-button,
  table.batch input::-webkit-inner-spin-button { margin: 0; -webkit-appearance: none; }
  table.batch input:hover { border-color: var(--line); background: var(--paper); }
  table.batch input:focus { border-color: var(--green-2); background: var(--paper); }
  table.batch .code-input { width: 100%; max-width: 110px; text-align: left; font-size: 10.5px; }
  table.batch .cell-stock { min-width: 142px; }
  table.batch .row-name {
    display: block;
    overflow: hidden;
    max-width: 136px;
    padding: 0 6px;
    color: var(--muted);
    font-family: var(--font);
    font-size: 8.5px;
    line-height: 1.3;
    text-overflow: ellipsis;
  }
  table.batch .num { color: var(--ink-2); }
  table.batch .rec-gold { color: var(--gold); font-weight: 600; }
  table.batch .rec-good { color: var(--green); }
  table.batch .rec-loss { color: var(--terra); }
  table.batch .cell-ratio { min-width: 66px; }
  table.batch .cell-ratio input { width: 46px; max-width: 46px; }
  table.batch .cell-actions { min-width: 92px; }
  .row-actions { display: flex; justify-content: center; gap: 4px; }
  .row-btn {
    padding: 2px 6px;
    border: 1px solid var(--line);
    border-radius: 5px;
    background: var(--paper);
    color: var(--muted);
    font-family: var(--font);
    font-size: 8.5px;
    line-height: 1.5;
    transition: .14s ease;
  }
  .row-btn:hover { border-color: var(--green-2); color: var(--green); background: var(--green-wash); }
  .row-btn.danger:hover { border-color: var(--terra); color: var(--terra); background: var(--terra-soft); }
  .batch-empty {
    padding: 26px 16px;
    color: var(--muted);
    font-size: 11px;
    text-align: center;
  }
  .batch-totals {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 1px;
    overflow: hidden;
    margin-top: 12px;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--line);
  }
  .batch-totals > div { min-width: 0; padding: 9px 11px; background: var(--paper-2); }
  .batch-totals span { display: block; color: var(--muted); font-size: 8.5px; }
  .batch-totals b {
    display: block;
    overflow: hidden;
    margin-top: 2px;
    color: var(--ink);
    font-family: var(--num);
    font-size: 14px;
    font-weight: 600;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .batch-totals > div:nth-child(1) b { color: var(--gold); }
  .batch-totals > div:nth-child(2) b { color: var(--green); }
  .batch-totals > div:nth-child(4) b { color: var(--terra); }
  .batch-note { margin: 10px 0 0; color: var(--muted); font-size: 9.5px; line-height: 1.6; }

  @media (max-width: 1180px) {
    .cost-grid { grid-template-columns: 1fr; gap: 18px; }
    .donut-wrap { flex-direction: row; justify-content: center; }
    .batch-totals { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  }
  @media (max-width: 980px) {
    .cost-card, .be-table-card, .batch-card { padding: 20px 18px 15px; }
    .batch-status { margin-left: 0; flex-basis: 100%; }
  }
  @media (max-width: 760px) {
    .chart-actions {
      flex-direction: row;
      flex-wrap: wrap;
      align-items: center;
      justify-content: space-between;
      width: 100%;
    }
    .ruler-legend { gap: 5px; }
    .ruler-legend > div { padding: 6px 7px; }
    .ruler-legend b { font-size: 11.5px; }
    .cost-card, .be-table-card, .batch-card { padding: 18px 14px 14px; }
    .cost-bars { height: 190px; gap: 10px; }
    .cost-track { height: 130px; }
    .donut { width: 150px; height: 150px; }
    .donut::after { inset: 24px; }
    .donut-hole b { font-size: 20px; }
    .batch-tools { gap: 5px; }
    .batch-btn { padding: 5px 7px; font-size: 9.5px; }
    .batch-totals b { font-size: 12px; }
  }
  @media (max-width: 460px) {
    .ruler-legend { grid-template-columns: 1fr 1fr 1fr; gap: 4px; }
    .ruler-legend span { font-size: 7.5px; }
    .ruler-legend b { font-size: 10px; }
    .cost-bars { gap: 6px; }
    .donut-wrap { flex-direction: column; }
    .batch-totals > div { padding: 8px 9px; }
  }

</style>
</head>
<body>
<div class="page">
  <header class="topbar">
    <div class="brand">
      <span class="brand-mark">补</span>
      <div class="brand-copy">
        <strong>补仓实验室</strong>
        <span>AVERAGE DOWN LAB</span>
      </div>
    </div>
    <div class="top-note"><i></i> 补多少,才回本</div>
  </header>

  <section class="hero">
    <div class="hero-copy">
      <div class="eyebrow"><span class="eyebrow-dot"></span> 补仓比例 · 回本价 · 多只批量</div>
      <h1>跌了,补多少<br><em>才真正回本。</em></h1>
      <p class="hero-lead">10 元买 1000 股,跌到 8 元:补 200 股,回本价 9.67 元;补 1000 股,回本价 9.00 元。补得越多,回本价越接近现价,但投入和下跌风险也一起放大。</p>
      <div class="hero-points" aria-label="三个核心结果">
        <div><span>01</span><b>补多少股</b><small>按 10% / 20% / 30%</small></div>
        <div><span>02</span><b>回本价</b><small>涨到多少不亏</small></div>
        <div><span>03</span><b>多只批量</b><small>一起算、一起比</small></div>
      </div>
      <p class="hero-foot">只算一件事:现在补多少股,股价涨到多少钱能回本。</p>
    </div>

    <aside class="exposure-card">
      <div class="exposure-top">
        <span>补仓后回本价</span>
        <span class="risk-badge" id="riseBadge" data-level="mid">需涨 +20.83%</span>
      </div>
      <div class="exposure-value"><span id="beValue">9.67</span><span class="unit-sign">元</span></div>
      <p class="exposure-caption" id="heroCaption">成本 10.00 元 × 1000 股 → 现价 8.00 元;补 200 股后,涨到 9.67 元回本。</p>
      <div class="gauge" aria-hidden="true">
        <div class="gauge-track"><span class="gauge-marker" id="gaugeMarker"></span></div>
        <div class="gauge-labels"><span>0</span><span>25</span><span>50</span><span>75</span><span>100%</span></div>
      </div>
      <div class="formula-box" id="heroFormula">补 200 股 × &#165;8.00 = &#165;1,600.00 → 回本价 &#165;9.67</div>
      <div class="fraction-badge" id="heroNote">补后亏损 &#8722;17.24%(补前 &#8722;20.00%)</div>
      <div class="exposure-facts">
        <div><span>补仓股数</span><b id="factAddShares">200 股</b></div>
        <div><span>补仓金额</span><b id="factAddAmount">&#165;1,600.00</b></div>
        <div><span>补后总股数</span><b id="factTotalShares">1,200 股</b></div>
      </div>
    </aside>
  </section>

  <main class="layout">
    <aside class="panel controls" aria-label="补仓参数">
      <div class="panel-head">
        <div>
          <span class="eyebrow-sm">INPUTS</span>
          <h2>填持仓,算回本</h2>
        </div>
        <button class="reset-btn" id="resetBtn" type="button">重置</button>
      </div>

      <div class="control">
        <div class="control-head">
          <label for="codeInput">股票代码</label>
          <span class="mini-note">可手动填</span>
        </div>
        <div class="code-row">
          <input type="text" id="codeInput" value="sh600000" placeholder="sh600519 / sz000858 / 600519" autocomplete="off" spellcheck="false" aria-label="股票代码">
          <button class="fetch-btn" type="button" id="fetchBtn">获取现价</button>
        </div>
        <div class="stock-meta">
          <span class="stock-name" id="stockName">浦发银行</span>
          <span class="status-line" id="fetchStatus">价格来自腾讯行情,可手动改</span>
        </div>
        <p class="hint">支持 sh / sz / bj 前缀;也可以只填 6 位代码。</p>
      </div>

      <div class="control">
        <div class="control-head">
          <label for="sharesInput">当前持股</label>
          <span class="mini-note">股</span>
        </div>
        <div class="unit-wrap">
          <input type="number" id="sharesInput" min="0" max="1000000000" step="100" value="1000" inputmode="numeric" aria-label="当前持股数量">
          <span>股</span>
        </div>
      </div>

      <div class="control">
        <div class="control-head">
          <label for="costInput">持仓成本</label>
          <span class="mini-note">元 / 股</span>
        </div>
        <div class="capital-wrap">
          <span>&#165;</span>
          <input type="number" id="costInput" min="0" max="1000000" step="0.01" value="10.00" inputmode="decimal" aria-label="持仓平均成本">
        </div>
      </div>

      <div class="control">
        <div class="control-head">
          <label for="priceInput">当前股价</label>
          <span class="mini-note">元 / 股</span>
        </div>
        <div class="capital-wrap">
          <span>&#165;</span>
          <input type="number" id="priceInput" min="0" max="1000000" step="0.01" value="8.00" inputmode="decimal" aria-label="当前股价">
        </div>
      </div>

      <div class="control">
        <div class="control-head">
          <label>补仓方式</label>
          <span class="mini-note">怎么补</span>
        </div>
        <div class="segmented segmented-4" role="group" aria-label="补仓方式">
          <button class="active" type="button" data-mode="ratio" aria-pressed="true">按比例</button>
          <button type="button" data-mode="shares" aria-pressed="false">按股数</button>
          <button type="button" data-mode="amount" aria-pressed="false">按金额</button>
          <button type="button" data-mode="target" aria-pressed="false">按目标</button>
        </div>

        <div class="mode-panel" id="ratioPanel">
          <div class="control-head sub">
            <label for="ratioRange">补仓比例(占现有股数)</label>
            <div class="loss-input-wrap gain">
              <input type="number" id="ratioInput" min="0" max="500" step="1" value="20" inputmode="numeric" aria-label="补仓比例百分比">
              <span>%</span>
            </div>
          </div>
          <input type="range" id="ratioRange" min="0" max="200" step="1" value="20">
          <div class="chip-row">
            <button class="chip" type="button" data-ratio="10">10%</button>
            <button class="chip" type="button" data-ratio="20">20%</button>
            <button class="chip" type="button" data-ratio="30">30%</button>
            <button class="chip" type="button" data-ratio="50">50%</button>
            <button class="chip" type="button" data-ratio="100">100%</button>
          </div>
          <p class="hint">10% = 按现有股数补(例如 1000 股再买 100 股),不是按资金;按资金比例请用“按金额”。</p>
        </div>

        <div class="mode-panel" id="sharesPanel" hidden>
          <div class="control-head sub">
            <label for="sharesToAddInput">计划补仓股数</label>
            <span class="mini-note">股</span>
          </div>
          <div class="unit-wrap">
            <input type="number" id="sharesToAddInput" min="0" max="1000000000" step="100" value="200" inputmode="numeric" aria-label="计划补仓股数">
            <span>股</span>
          </div>
          <div class="chip-row">
            <button class="chip" type="button" data-add-shares="100">100</button>
            <button class="chip" type="button" data-add-shares="200">200</button>
            <button class="chip" type="button" data-add-shares="500">500</button>
            <button class="chip" type="button" data-add-shares="1000">1000</button>
          </div>
          <p class="hint">直接输入想补多少股;补得越多,回本价越接近现价。</p>
        </div>

        <div class="mode-panel" id="amountPanel" hidden>
          <div class="control-head sub">
            <label for="amountInput">计划补仓金额</label>
            <span class="mini-note">人民币</span>
          </div>
          <div class="capital-wrap">
            <span>&#165;</span>
            <input type="number" id="amountInput" min="0" max="1000000000" step="1000" value="10000" inputmode="numeric" aria-label="计划补仓金额">
          </div>
          <div class="chip-row">
            <button class="chip" type="button" data-amount="10000">1万</button>
            <button class="chip" type="button" data-amount="50000">5万</button>
            <button class="chip" type="button" data-amount="100000">10万</button>
          </div>
          <p class="hint">按现价买入:股数 = 金额 ÷ 现价;开启取整后按 100 股向下取。</p>
        </div>

        <div class="mode-panel" id="targetPanel" hidden>
          <div class="control-head sub">
            <label for="targetInput">目标平均成本</label>
            <span class="mini-note">元 / 股</span>
          </div>
          <div class="capital-wrap">
            <span>&#165;</span>
            <input type="number" id="targetInput" min="0" max="1000000" step="0.01" value="9.50" inputmode="decimal" aria-label="目标平均成本">
          </div>
          <div class="chip-row">
            <button class="chip" type="button" id="midTarget">用中间价</button>
          </div>
          <p class="hint" id="targetHint">目标均价必须低于成本 10.00、高于现价 8.00。</p>
        </div>
      </div>

      <div class="control">
        <div class="control-head">
          <label>下单取整</label>
          <span class="mini-note">A股可选</span>
        </div>
        <label class="switch-row" for="roundLot">
          <input type="checkbox" id="roundLot">
          <span>按 100 股整数倍取整(更贴近真实下单)</span>
        </label>
        <p class="hint">关闭时按精确股数计算;开启后按比例/股数取最近 100 股、金额向下取、目标成本向上取。</p>
      </div>

      <div class="control">
        <button class="jump-btn" type="button" id="applyToBatch">把当前股票加入批量 ↓</button>
      </div>
    </aside>

    <section class="results" aria-label="补仓结果">
      <div class="kpi-grid">
        <article class="kpi kpi-breakeven">
          <div class="kpi-head"><span>回本价(元)</span><span class="kpi-code">BEP</span></div>
          <div class="kpi-value gold" id="kpiBreakeven">9.67</div>
          <div class="kpi-meta"><b id="kpiBreakevenMeta">需涨 +20.83%</b></div>
          <p class="kpi-foot" id="kpiBreakevenFoot">涨回这个价才不亏不赚</p>
        </article>
        <article class="kpi kpi-add">
          <div class="kpi-head"><span>需补股数</span><span class="kpi-code">ADD</span></div>
          <div class="kpi-value green"><span id="kpiAddShares">200</span><span class="kpi-unit">股</span></div>
          <div class="kpi-meta"><b id="kpiAddAmount">&#165;1,600.00</b><span>补仓金额</span></div>
          <p class="kpi-foot" id="kpiAddFoot">按现价 &#165;8.00 买入</p>
        </article>
        <article class="kpi kpi-total">
          <div class="kpi-head"><span>补后总股数</span><span class="kpi-code">TOTAL</span></div>
          <div class="kpi-value ink"><span id="kpiTotalShares">1,200</span><span class="kpi-unit">股</span></div>
          <div class="kpi-meta"><b id="kpiTotalCost">&#165;11,600.00</b><span>补后总成本</span></div>
          <p class="kpi-foot" id="kpiTotalFoot">原 1,000 股 + 新 200 股</p>
        </article>
        <article class="kpi kpi-post" id="kpiPostCard">
          <div class="kpi-head"><span id="kpiPostLabel">补后浮亏</span><span class="kpi-code">P/L</span></div>
          <div class="kpi-value terra" id="kpiPostPnl">&#8722;&#165;2,000.00</div>
          <div class="kpi-meta"><b id="kpiPostPct">&#8722;17.24%</b><span>补后亏损率</span></div>
          <p class="kpi-foot" id="kpiPostFoot">补前 &#8722;&#165;2,000.00(&#8722;20.00%)</p>
        </article>
      </div>

      <div class="strip-card">
        <div class="strip-copy">
          <span class="eyebrow-sm">PRICE RULER</span>
          <h3>现价离回本,还有多远?</h3>
          <p id="rulerText">补仓把回本价从 10.00 拉到 9.67;股价要从 8.00 涨到 9.67 才回本。</p>
        </div>
        <div class="strip-visual">
          <div class="ruler" aria-hidden="true">
            <div class="ruler-track"></div>
            <div class="ruler-gap" id="rulerGap"></div>
            <span class="ruler-mark price" id="markPrice" title="现价"></span>
            <span class="ruler-mark breakeven" id="markBreakeven" title="回本价"></span>
            <span class="ruler-mark cost" id="markCost" title="原成本"></span>
          </div>
          <div class="ruler-legend">
            <div><span><i class="price"></i>现价</span><b id="legendPrice">&#165;8.00</b></div>
            <div><span><i class="breakeven"></i>回本价</span><b id="legendBreakeven">&#165;9.67</b></div>
            <div><span><i class="cost"></i>原成本</span><b id="legendCost">&#165;10.00</b></div>
          </div>
          <div class="ruler-rise">回本需涨 <b id="rulerRise">+20.83%</b></div>
        </div>
      </div>

      <section class="card chart-card" id="beCard">
        <div class="card-head chart-head">
          <div class="chart-heading">
            <span class="eyebrow-sm">BREAK-EVEN CURVE</span>
            <h2 id="beTitle">补仓比例,怎么改变回本价</h2>
            <p id="beSubtitle">横轴:补仓比例(占现有股数)· 纵轴:回本价(元)</p>
          </div>
          <div class="chart-actions">
            <div class="tabs" id="beTabs" role="tablist" aria-label="图表指标">
              <button class="tab active" type="button" data-betab="price" role="tab" aria-selected="true">回本价</button>
              <button class="tab" type="button" data-betab="amount" role="tab" aria-selected="false">补仓金额</button>
            </div>
          </div>
        </div>
        <div class="chart-wrap" id="beWrap">
          <svg id="beSvg" role="img" aria-label="补仓比例与回本价曲线"></svg>
          <div class="chart-tooltip wide" id="beTip" hidden></div>
        </div>
        <div class="chart-foot">
          <div class="legend" id="beLegend">
            <span><i class="legend-line"></i>补后回本价</span>
            <span><i class="legend-line base"></i>原成本</span>
            <span><i class="legend-line price"></i>现价</span>
          </div>
          <p id="beNote">补得越多,回本价越接近现价,但每多补 10% 带来的下降越来越小。</p>
        </div>
      </section>

      <section class="card cost-card" id="costCard">
        <div class="card-head">
          <div>
            <span class="eyebrow-sm">COST &amp; STRUCTURE</span>
            <h2>成本对比与持仓结构</h2>
            <p id="costSubtitle">补仓后均价 9.67 元,介于现价与原成本之间。</p>
          </div>
        </div>
        <div class="cost-grid">
          <div class="cost-panel">
            <h3>成本对比(元)</h3>
            <div class="cost-bars" id="costBars"></div>
            <p class="cost-delta" id="costDelta">补后均价较原成本 <b>↓ 0.33</b> 元</p>
          </div>
          <div class="donut-panel">
            <h3>补仓后持仓结构</h3>
            <div class="donut-wrap">
              <div class="donut" id="donut" style="--orig: 83.33%">
                <div class="donut-hole"><b id="donutPct">83.33%</b><span>原持仓</span></div>
              </div>
              <div class="donut-legend" id="donutLegend">
                <span><i class="orig"></i>原持股 <b>1,000 股</b></span>
                <span><i class="add"></i>新增 <b>200 股</b></span>
              </div>
            </div>
          </div>
        </div>
      </section>

      <section class="card be-table-card" id="beTableCard">
        <div class="card-head">
          <div>
            <span class="eyebrow-sm">RATIO LADDER</span>
            <h2>不同补仓比例,回本价差多少</h2>
            <p id="beTableSubtitle">从 0%(不补)到 200%,每档都算清楚;当前方案高亮。</p>
          </div>
        </div>
        <div class="be-table-wrap">
          <table class="detail be-table">
            <thead>
              <tr>
                <th>补仓比例</th>
                <th>补仓股数</th>
                <th>补仓金额</th>
                <th>回本价</th>
                <th>回本需涨</th>
                <th>补后盈亏</th>
              </tr>
            </thead>
            <tbody id="beTableBody"></tbody>
          </table>
        </div>
        <p class="table-hint">回本价 = 补后平均成本;补后盈亏按当前股价计算,不含手续费。表格可横向滑动。</p>
      </section>
    </section>
  </main>

  <section class="card batch-card" id="batchSection">
    <div class="card-head">
      <div>
        <span class="eyebrow-sm">BATCH</span>
        <h2>批量补仓:多只股票一起算</h2>
        <p id="batchSubtitle">每只股票按自己的 10% / 20% / 30% 补仓,回本价一列看清楚;表格可横向滑动,首列固定。</p>
      </div>
      <div class="ladder-badge" id="batchBadge">1 只</div>
    </div>

    <div class="batch-tools">
      <span class="tool-label">批量比例</span>
      <button class="chip" type="button" data-batch-ratio="10">10%</button>
      <button class="chip" type="button" data-batch-ratio="20">20%</button>
      <button class="chip" type="button" data-batch-ratio="30">30%</button>
      <button class="chip" type="button" data-batch-ratio="50">50%</button>
      <button class="chip" type="button" data-batch-ratio="100">100%</button>
      <button class="batch-btn primary" type="button" id="addRowBtn">+ 添加股票</button>
      <button class="batch-btn" type="button" id="fetchAllBtn">获取全部现价</button>
      <button class="batch-btn" type="button" id="importBtn">导入 获取股票信息.txt</button>
      <input type="file" id="importFile" accept=".txt,.js,text/plain" hidden>
      <button class="batch-btn" type="button" id="clearBatchBtn">清空</button>
      <span class="status-line batch-status" id="batchStatus"></span>
    </div>

    <div class="batch-wrap">
      <table class="batch">
        <thead>
          <tr>
            <th>股票</th>
            <th>持股数</th>
            <th>成本价</th>
            <th>现价</th>
            <th>补仓比例</th>
            <th>补仓股数</th>
            <th>补仓金额</th>
            <th>回本价</th>
            <th>需涨</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody id="batchBody"></tbody>
      </table>
    </div>

    <div class="batch-totals">
      <div><span>合计补仓金额</span><b id="batchAddAmount">&#165;1,600.00</b></div>
      <div><span>合计补后股数</span><b id="batchTotalShares">1,200 股</b></div>
      <div><span>补后总成本</span><b id="batchTotalCost">&#165;11,600.00</b></div>
      <div><span id="batchPnlLabel">组合浮亏(按现价)</span><b id="batchPnl">&#8722;&#165;2,000.00</b></div>
    </div>
    <p class="batch-note" id="batchNote">合计只统计持股数、成本、现价都有效的股票。导入 txt 后会提取其中的股票代码并自动获取现价;没有网络时仍可手动填写。</p>
  </section>

  <section class="method">
    <div class="method-card">
      <span class="eyebrow-sm">METHOD</span>
      <h2>回本价怎么算?</h2>
      <div class="calc-line" id="calcLine">补仓股数 = 1000 × 20% = 200 股;回本价 = (1000×10.00 + 200×8.00) ÷ 1200 = 9.67 元</div>
      <div class="formula-list">
        <div class="formula-row"><span>补后回本价</span><code>(原持股×原成本 + 补仓股数×现价)÷(原持股 + 补仓股数)</code></div>
        <div class="formula-row"><span>按比例补仓</span><code>补仓股数 = 原持股 × 补仓比例</code></div>
        <div class="formula-row"><span>按股数补仓</span><code>补仓股数 = 输入值;开启取整时按 100 股取整</code></div>
        <div class="formula-row"><span>按金额补仓</span><code>补仓股数 = 补仓金额 ÷ 现价</code></div>
        <div class="formula-row"><span>按目标成本</span><code>补仓股数 = 原持股 ×(原成本 &#8722; 目标)÷(目标 &#8722; 现价)</code></div>
        <div class="formula-row"><span>回本需涨</span><code>回本价 ÷ 现价 &#8722; 1</code></div>
        <div class="formula-row"><span>补后盈亏</span><code>(现价 &#8722; 回本价)× 补后总股数</code></div>
      </div>
    </div>
    <div class="note-card">
      <h3>四个必须说清的前提</h3>
      <ol>
        <li><b>补仓不改变当前浮亏金额。</b>按现价补,新买的部分不亏不赚,所以绝对亏损不变,变化的是亏损率和仓位。</li>
        <li><b>回本价就是补后平均成本。</b>股价涨回这个价才不亏不赚;它可以低于原成本,但不可能低于现价。</li>
        <li><b>补得越多,回本价越接近现价。</b>从 10.00 拉到 9.67 可能只要补 20%,但再往下拉会越来越贵,风险也越集中。</li>
        <li><b>未计税费与滑点。</b>佣金、印花税、过户费和成交滑点都会让真实回本价略高;A 股可勾选 100 股取整。</li>
      </ol>
    </div>
  </section>

  <footer>
    <span>本工具仅用于补仓成本算术演示,不构成投资建议。补仓会放大仓位与下跌风险,请先算清最坏情况。</span>
    <span class="footer-mark">AVERAGE DOWN LAB · 2026</span>
  </footer>
</div>

<div class="mobile-readout" aria-live="polite" aria-label="实时结果">
  <div><span>回本价</span><b id="mBreakeven">&#165;9.67</b></div>
  <div><span>需补股数</span><b id="mAddShares">200 股</b></div>
  <div><span>回本需涨</span><b id="mRise">+20.83%</b></div>
</div>

<script>
(() => {
  "use strict";

  const DEFAULTS = Object.freeze({
    code: "sh600000",
    name: "浦发银行",
    shares: 1000,
    cost: 10,
    price: 8,
    mode: "ratio",
    ratio: 20,
    addShares: 200,
    amount: 10000,
    target: 9.5,
    roundLot: false,
    chartTab: "price"
  });

  const state = {
    ...DEFAULTS,
    batchRatio: 20,
    rows: [],
    rowSeq: 0
  };

  const $ = (selector, root = document) => root.querySelector(selector);
  const $$ = (selector, root = document) => Array.from(root.querySelectorAll(selector));
  const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
  const nf0 = new Intl.NumberFormat("zh-CN", { maximumFractionDigits: 0 });
  const nf2 = new Intl.NumberFormat("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });

  const refs = {
    codeInput: $("#codeInput"),
    fetchBtn: $("#fetchBtn"),
    stockName: $("#stockName"),
    fetchStatus: $("#fetchStatus"),
    sharesInput: $("#sharesInput"),
    costInput: $("#costInput"),
    priceInput: $("#priceInput"),
    ratioPanel: $("#ratioPanel"),
    sharesPanel: $("#sharesPanel"),
    amountPanel: $("#amountPanel"),
    targetPanel: $("#targetPanel"),
    ratioRange: $("#ratioRange"),
    ratioInput: $("#ratioInput"),
    sharesToAddInput: $("#sharesToAddInput"),
    amountInput: $("#amountInput"),
    targetInput: $("#targetInput"),
    targetHint: $("#targetHint"),
    midTarget: $("#midTarget"),
    roundLot: $("#roundLot"),
    applyToBatch: $("#applyToBatch"),

    beValue: $("#beValue"),
    riseBadge: $("#riseBadge"),
    heroCaption: $("#heroCaption"),
    heroFormula: $("#heroFormula"),
    heroNote: $("#heroNote"),
    gaugeMarker: $("#gaugeMarker"),
    gaugeTrack: $(".gauge-track"),
    factAddShares: $("#factAddShares"),
    factAddAmount: $("#factAddAmount"),
    factTotalShares: $("#factTotalShares"),

    kpiBreakeven: $("#kpiBreakeven"),
    kpiBreakevenMeta: $("#kpiBreakevenMeta"),
    kpiBreakevenFoot: $("#kpiBreakevenFoot"),
    kpiAddShares: $("#kpiAddShares"),
    kpiAddAmount: $("#kpiAddAmount"),
    kpiAddFoot: $("#kpiAddFoot"),
    kpiTotalShares: $("#kpiTotalShares"),
    kpiTotalCost: $("#kpiTotalCost"),
    kpiTotalFoot: $("#kpiTotalFoot"),
    kpiPostCard: $("#kpiPostCard"),
    kpiPostLabel: $("#kpiPostLabel"),
    kpiPostPnl: $("#kpiPostPnl"),
    kpiPostPct: $("#kpiPostPct"),
    kpiPostFoot: $("#kpiPostFoot"),

    rulerText: $("#rulerText"),
    rulerGap: $("#rulerGap"),
    markPrice: $("#markPrice"),
    markBreakeven: $("#markBreakeven"),
    markCost: $("#markCost"),
    legendPrice: $("#legendPrice"),
    legendBreakeven: $("#legendBreakeven"),
    legendCost: $("#legendCost"),
    rulerRise: $("#rulerRise"),

    beCard: $("#beCard"),
    beTitle: $("#beTitle"),
    beSubtitle: $("#beSubtitle"),
    beTabs: $("#beTabs"),
    beWrap: $("#beWrap"),
    beSvg: $("#beSvg"),
    beTip: $("#beTip"),
    beLegend: $("#beLegend"),
    beNote: $("#beNote"),

    costSubtitle: $("#costSubtitle"),
    costBars: $("#costBars"),
    costDelta: $("#costDelta"),
    donut: $("#donut"),
    donutPct: $("#donutPct"),
    donutLegend: $("#donutLegend"),

    beTableBody: $("#beTableBody"),
    beTableSubtitle: $("#beTableSubtitle"),

    batchBadge: $("#batchBadge"),
    batchBody: $("#batchBody"),
    batchStatus: $("#batchStatus"),
    batchAddAmount: $("#batchAddAmount"),
    batchTotalShares: $("#batchTotalShares"),
    batchTotalCost: $("#batchTotalCost"),
    batchPnl: $("#batchPnl"),
    batchPnlLabel: $("#batchPnlLabel"),
    addRowBtn: $("#addRowBtn"),
    fetchAllBtn: $("#fetchAllBtn"),
    importBtn: $("#importBtn"),
    importFile: $("#importFile"),
    clearBatchBtn: $("#clearBatchBtn"),

    calcLine: $("#calcLine"),
    mBreakeven: $("#mBreakeven"),
    mAddShares: $("#mAddShares"),
    mRise: $("#mRise")
  };

  const svgNS = "http://www.w3.org/2000/svg";
  let beGeom = null;
  let renderFrame = 0;
  let resizeTimer = 0;

  /* ---------------- formatting ---------------- */

  function trimNum(text) {
    return String(text).replace(/\.0+$/, "").replace(/(\.\d*[1-9])0+$/, "$1");
  }
  function fmtPrice(value) {
    return Number.isFinite(value) ? value.toFixed(2) : "—";
  }
  function fmtShares(value) {
    return Number.isFinite(value) ? nf0.format(Math.round(value)) : "—";
  }
  function fmtMoney(value) {
    return Number.isFinite(value) ? "&#165;" + nf2.format(value) : "—";
  }
  function fmtMoneyInt(value) {
    return Number.isFinite(value) ? "&#165;" + nf0.format(Math.round(value)) : "—";
  }
  function fmtPct(value, digits = 2) {
    if (!Number.isFinite(value)) return "—";
    return (value > 0 ? "+" : "") + trimNum((value * 100).toFixed(digits)) + "%";
  }
  function fmtPctAbs(value, digits = 2) {
    if (!Number.isFinite(value)) return "—";
    return trimNum((value * 100).toFixed(digits)) + "%";
  }
  function fmtSignedMoney(value) {
    if (!Number.isFinite(value)) return "—";
    return (value >= 0 ? "+" : "&#8722;") + "&#165;" + nf2.format(Math.abs(value));
  }
  function fmtLossMoney(value) {
    if (!Number.isFinite(value)) return "—";
    if (Math.abs(value) < 0.005) return "&#165;0.00";
    return (value > 0 ? "+" : "&#8722;") + "&#165;" + nf2.format(Math.abs(value));
  }
  function fmtMoneyShort(value) {
    if (!Number.isFinite(value)) return "—";
    const abs = Math.abs(value);
    if (abs >= 1e8) return "&#165;" + trimNum((value / 1e8).toFixed(2)) + "亿";
    if (abs >= 1e4) return "&#165;" + trimNum((value / 1e4).toFixed(1)) + "万";
    return "&#165;" + nf0.format(Math.round(value));
  }

  /* ---------------- quotes ---------------- */

  function normalizeCode(input) {
    let code = String(input || "").trim().toLowerCase().replace(/\s+/g, "");
    if (/^\d{6}$/.test(code)) {
      if (code[0] === "6") code = "sh" + code;
      else if (code[0] === "0" || code[0] === "3") code = "sz" + code;
      else code = "bj" + code;
    }
    return code;
  }
  function isStockCode(code) {
    return /^(sh|sz|bj)\d{6}$/.test(code);
  }
  function parseQuote(raw, code) {
    const parts = String(raw).split("~");
    if (parts.length < 50) throw new Error("行情返回不完整");
    const price = Number(parts[3]);
    if (!Number.isFinite(price) || price <= 0) throw new Error("没有拿到有效现价");
    return {
      code,
      name: parts[1] || code,
      price,
      prevClose: Number(parts[4]) || 0,
      change: Number(parts[31]) || 0,
      pct: Number(parts[32]) || 0,
      time: parts[30] || ""
    };
  }
  function fetchQuote(inputCode) {
    return new Promise((resolve, reject) => {
      const code = normalizeCode(inputCode);
      if (!isStockCode(code)) {
        reject(new Error("代码格式应为 sh600519 / sz000858 / 600519"));
        return;
      }
      const globalName = "v_" + code;
      const script = document.createElement("script");
      let settled = false;
      const timer = window.setTimeout(() => finish(new Error("请求超时")), 9000);
      function cleanup() {
        window.clearTimeout(timer);
        if (script.parentNode) script.parentNode.removeChild(script);
      }
      function finish(error, quote) {
        if (settled) return;
        settled = true;
        cleanup();
        try { delete window[globalName]; } catch (e) { window[globalName] = undefined; }
        if (error) reject(error);
        else resolve(quote);
      }
      script.charset = "gbk";
      script.src = "https://qt.gtimg.cn/q=" + code + "&_=" + Date.now();
      script.onload = () => {
        const raw = window[globalName];
        if (!raw) {
          finish(new Error("行情接口没有返回数据"));
          return;
        }
        try { finish(null, parseQuote(raw, code)); }
        catch (error) { finish(error); }
      };
      script.onerror = () => finish(new Error("网络请求失败,可手动填写价格"));
      document.head.appendChild(script);
    });
  }
  function extractCodes(text) {
    const matches = String(text || "").match(/\b(?:sh|sz|bj)?\d{6}\b/gi) || [];
    const out = [];
    matches.forEach(match => {
      const code = normalizeCode(match);
      if (isStockCode(code) && !out.includes(code)) out.push(code);
    });
    return out;
  }

  /* ---------------- calculation ---------------- */

  function roundShares(raw, mode) {
    if (!(raw > 0)) return 0;
    if (state.roundLot) {
      if (mode === "target") return Math.ceil(raw / 100) * 100;
      if (mode === "amount") {
        const floored = Math.floor(raw / 100) * 100;
        return floored;
      }
      return Math.max(100, Math.round(raw / 100) * 100);
    }
    if (mode === "target") return Math.ceil(raw);
    return Math.max(1, Math.round(raw));
  }

  function buildCalc(shares, cost, price, add, meta = {}) {
    const addAmount = add * price;
    const totalShares = shares + add;
    const totalCost = shares * cost + addAmount;
    const breakeven = totalShares > 0 ? totalCost / totalShares : cost;
    const rise = price > 0 ? breakeven / price - 1 : 0;
    const prePnl = (price - cost) * shares;
    const postPnl = (price - breakeven) * totalShares;
    const prePct = cost > 0 ? price / cost - 1 : 0;
    const postPct = breakeven > 0 ? price / breakeven - 1 : 0;
    const addRatio = shares > 0 ? (add / shares) * 100 : 0;
    return {
      valid: true,
      shares,
      cost,
      price,
      add,
      addAmount,
      totalShares,
      totalCost,
      breakeven,
      rise,
      prePnl,
      postPnl,
      prePct,
      postPct,
      addRatio,
      ...meta
    };
  }

  function calcSingle() {
    const shares = Number(state.shares);
    const cost = Number(state.cost);
    const price = Number(state.price);
    if (!(shares > 0) || !(cost > 0) || !(price > 0)) {
      return { valid: false, reason: "请填写持股数、成本价和现价(都需大于 0)" };
    }
    let rawAdd = 0;
    let add = 0;
    let warning = "";

    if (state.mode === "ratio") {
      rawAdd = shares * state.ratio / 100;
      add = roundShares(rawAdd, "ratio");
    } else if (state.mode === "shares") {
      rawAdd = state.addShares;
      add = roundShares(rawAdd, "shares");
    } else if (state.mode === "amount") {
      rawAdd = state.amount / price;
      add = roundShares(rawAdd, "amount");
    } else {
      const target = Number(state.target);
      const canTarget = cost > price && target > price && target < cost;
      if (canTarget) {
        rawAdd = shares * (cost - target) / (target - price);
        add = roundShares(rawAdd, "target");
      } else {
        warning = cost > price
          ? "目标均价必须低于成本 " + fmtPrice(cost) + "、高于现价 " + fmtPrice(price) + "。"
          : "当前股价已经不低于成本,按目标成本补仓不适用;请用按比例或按金额。";
      }
    }

    const calc = buildCalc(shares, cost, price, add, {
      rawAdd,
      warning,
      mode: state.mode,
      ratioSetting: state.ratio,
      targetSetting: state.target
    });
    if (state.mode === "ratio") calc.targetRatio = state.ratio;
    return calc;
  }

  function calcAtRatio(ratioPct, applyRounding) {
    const shares = Number(state.shares);
    const cost = Number(state.cost);
    const price = Number(state.price);
    if (!(shares > 0) || !(cost > 0) || !(price > 0)) return null;
    const raw = shares * ratioPct / 100;
    const add = applyRounding ? roundShares(raw, "ratio") : raw;
    return buildCalc(shares, cost, price, add, { rawAdd: raw, ratioSetting: ratioPct });
  }

  function calcForRow(row) {
    const shares = Number(row.shares);
    const cost = Number(row.cost);
    const price = Number(row.price);
    const ratio = Number(row.ratio);
    if (!(shares > 0) || !(cost > 0) || !(price > 0) || !(ratio >= 0)) return null;
    const raw = shares * ratio / 100;
    const add = roundShares(raw, "ratio");
    return buildCalc(shares, cost, price, add, { rawAdd: raw, ratioSetting: ratio });
  }

  /* ---------------- single render ---------------- */

  function setRangeFill(element) {
    if (!element) return;
    const max = Number(element.max) || 100;
    const value = Number(element.value) || 0;
    element.style.setProperty("--fill", clamp((value / max) * 100, 0, 100).toFixed(2) + "%");
  }

  function renderHero(calc) {
    if (!calc || !calc.valid) {
      refs.beValue.textContent = "—";
      refs.riseBadge.textContent = "待输入";
      refs.riseBadge.dataset.level = "custom";
      refs.heroCaption.textContent = calc ? calc.reason : "请填写持股数、成本价和现价。";
      refs.heroFormula.textContent = "填写参数后自动计算";
      refs.heroNote.textContent = "回本价 = 补后平均成本";
      refs.factAddShares.textContent = "—";
      refs.factAddAmount.textContent = "—";
      refs.factTotalShares.textContent = "—";
      refs.gaugeMarker.style.left = "1.5%";
      refs.gaugeTrack.style.setProperty("--risk-pos", "1.5%");
      return;
    }

    const risePct = calc.rise * 100;
    refs.beValue.textContent = fmtPrice(calc.breakeven);
    if (calc.warning) {
      refs.riseBadge.textContent = "目标无效";
      refs.riseBadge.dataset.level = "high";
    } else if (risePct > 0) {
      refs.riseBadge.textContent = "需涨 " + fmtPct(calc.rise, 2);
      refs.riseBadge.dataset.level = risePct > 30 ? "high" : risePct > 15 ? "mid" : "low";
    } else {
      refs.riseBadge.textContent = "已回本";
      refs.riseBadge.dataset.level = "low";
    }

    const noAdd = calc.add <= 0;
    const covered = calc.rise <= 0;
    if (calc.warning) {
      refs.heroCaption.textContent = calc.warning;
    } else if (noAdd) {
      refs.heroCaption.textContent = covered
        ? "没有补仓:现价 " + fmtPrice(calc.price) + " 已不低于成本 " + fmtPrice(calc.cost) + ",当前不亏。"
        : "没有补仓:回本价仍是原成本 " + fmtPrice(calc.cost) + ",股价需涨 " +
          fmtPct(calc.rise, 2) + " 才回本。";
    } else if (covered) {
      refs.heroCaption.textContent = "现价 " + fmtPrice(calc.price) + " 已不低于回本价 " +
        fmtPrice(calc.breakeven) + ";补 " + fmtShares(calc.add) + " 股后,回本价从 " +
        fmtPrice(calc.cost) + " 抬到 " + fmtPrice(calc.breakeven) + "。";
    } else {
      refs.heroCaption.textContent = "成本 " + fmtPrice(calc.cost) + " 元 × " +
        fmtShares(calc.shares) + " 股 → 现价 " + fmtPrice(calc.price) + " 元;补 " +
        fmtShares(calc.add) + " 股后,涨到 " + fmtPrice(calc.breakeven) + " 元回本。";
    }

    refs.heroFormula.textContent = noAdd
      ? "未补仓 · 回本价 " + fmtPrice(calc.breakeven) + " 元"
      : "补 " + fmtShares(calc.add) + " 股 × " + fmtMoney(calc.price) + " = " +
        fmtMoney(calc.addAmount) + " → 回本价 " + fmtMoney(calc.breakeven);

    const delta = calc.cost - calc.breakeven;
    refs.heroNote.textContent = calc.postPnl < 0
      ? "补后亏损 " + fmtPct(calc.postPct, 2) + "(补前 " + fmtPct(calc.prePct, 2) + ")"
      : "补后浮盈 " + fmtPct(calc.postPct, 2) + "(补前 " + fmtPct(calc.prePct, 2) + ")";
    refs.factAddShares.textContent = fmtShares(calc.add) + " 股";
    refs.factAddAmount.textContent = fmtMoney(calc.addAmount);
    refs.factTotalShares.textContent = fmtShares(calc.totalShares) + " 股";

    const gaugePos = clamp(calc.rise * 100, 1.5, 98.5);
    refs.gaugeMarker.style.left = gaugePos.toFixed(2) + "%";
    refs.gaugeTrack.style.setProperty("--risk-pos", gaugePos.toFixed(2) + "%");
  }

  function renderKpis(calc) {
    if (!calc || !calc.valid) {
      ["kpiBreakeven", "kpiAddShares", "kpiTotalShares", "kpiPostPnl"].forEach(id => {
        refs[id].textContent = "—";
      });
      refs.kpiBreakevenMeta.textContent = "等待输入";
      refs.kpiAddAmount.textContent = "—";
      refs.kpiTotalCost.textContent = "—";
      refs.kpiPostPct.textContent = "—";
      refs.kpiBreakevenFoot.textContent = "补后均价 = 回本价";
      refs.kpiAddFoot.textContent = "按现价买入";
      refs.kpiTotalFoot.textContent = "原持股 + 新增";
      refs.kpiPostFoot.textContent = "不含手续费";
      refs.kpiPostLabel.textContent = "补后浮亏";
      refs.kpiPostCard.classList.remove("is-gain");
      refs.kpiPostPnl.className = "kpi-value terra";
      refs.mBreakeven.textContent = "—";
      refs.mAddShares.textContent = "—";
      refs.mRise.textContent = "—";
      return;
    }

    const isGain = calc.postPnl >= 0;
    refs.kpiBreakeven.textContent = fmtPrice(calc.breakeven);
    refs.kpiBreakevenMeta.textContent = calc.warning
      ? "目标无效"
      : (calc.rise > 0 ? "需涨 " + fmtPct(calc.rise, 2) : "已高于回本价");
    refs.kpiBreakevenFoot.textContent = calc.rise > 0
      ? "涨回这个价才不亏不赚 · 补后均价"
      : "现价已不低于补后均价,当前不亏";
    refs.kpiAddShares.textContent = fmtShares(calc.add);
    refs.kpiAddAmount.textContent = fmtMoney(calc.addAmount);
    refs.kpiAddFoot.textContent = "按现价 " + fmtMoney(calc.price) + " 买入";
    refs.kpiTotalShares.textContent = fmtShares(calc.totalShares);
    refs.kpiTotalCost.textContent = fmtMoney(calc.totalCost);
    refs.kpiTotalFoot.textContent = "原 " + fmtShares(calc.shares) + " 股 + 新 " + fmtShares(calc.add) + " 股";
    refs.kpiPostLabel.textContent = isGain ? "补后浮盈" : "补后浮亏";
    refs.kpiPostPnl.textContent = fmtLossMoney(calc.postPnl);
    refs.kpiPostPnl.className = "kpi-value " + (isGain ? "green" : "terra");
    refs.kpiPostPct.textContent = fmtPct(calc.postPct, 2);
    refs.kpiPostFoot.textContent = "补前 " + fmtLossMoney(calc.prePnl) + "(" +
      fmtPct(calc.prePct, 2) + ")· 绝对金额不变";
    refs.kpiPostCard.classList.toggle("is-gain", isGain);

    refs.mBreakeven.textContent = fmtMoney(calc.breakeven);
    refs.mAddShares.textContent = fmtShares(calc.add) + " 股";
    refs.mRise.textContent = calc.rise > 0 ? fmtPct(calc.rise, 2) : "已回本";
  }

  function renderRuler(calc) {
    if (!calc || !calc.valid) {
      refs.legendPrice.textContent = "—";
      refs.legendBreakeven.textContent = "—";
      refs.legendCost.textContent = "—";
      refs.rulerRise.textContent = "—";
      refs.rulerText.textContent = "填写持股数、成本价和现价后显示价格尺。";
      refs.rulerGap.hidden = true;
      refs.markPrice.style.opacity = "0";
      refs.markBreakeven.style.opacity = "0";
      refs.markCost.style.opacity = "0";
      return;
    }
    refs.markPrice.style.opacity = "1";
    refs.markBreakeven.style.opacity = "1";
    refs.markCost.style.opacity = "1";
    const price = calc.price;
    const be = calc.breakeven;
    const cost = calc.cost;
    const values = [price, be, cost];
    const lo = Math.min(...values);
    const hi = Math.max(...values);
    const pad = Math.max((hi - lo) * 0.18, hi * 0.01, 0.01);
    const min = Math.max(0, lo - pad);
    const max = hi + pad;
    const pos = value => clamp(((value - min) / (max - min)) * 100, 1.2, 98.8);

    const pPos = pos(price);
    const bPos = pos(be);
    const cPos = pos(cost);
    refs.markPrice.style.left = pPos.toFixed(2) + "%";
    refs.markBreakeven.style.left = bPos.toFixed(2) + "%";
    refs.markCost.style.left = cPos.toFixed(2) + "%";
    refs.markPrice.title = "现价 " + fmtPrice(price);
    refs.markBreakeven.title = "回本价 " + fmtPrice(be);
    refs.markCost.title = "原成本 " + fmtPrice(cost);

    if (be > price) {
      refs.rulerGap.hidden = false;
      refs.rulerGap.style.left = Math.min(pPos, bPos).toFixed(2) + "%";
      refs.rulerGap.style.width = Math.abs(bPos - pPos).toFixed(2) + "%";
    } else {
      refs.rulerGap.hidden = true;
    }

    refs.legendPrice.textContent = fmtMoney(price);
    refs.legendBreakeven.textContent = fmtMoney(be);
    refs.legendCost.textContent = fmtMoney(cost);
    refs.rulerRise.textContent = be > price ? fmtPct(calc.rise, 2) : "已高于回本价";
    refs.rulerRise.style.color = be > price ? "var(--terra)" : "var(--green)";

    if (be <= price) {
      refs.rulerText.textContent = calc.add > 0
        ? "现价 " + fmtPrice(price) + " 已不低于回本价 " + fmtPrice(be) +
          ";补仓把回本价从 " + fmtPrice(cost) + " 抬到 " + fmtPrice(be) + "。"
        : "不补仓时,现价 " + fmtPrice(price) + " 已不低于成本 " + fmtPrice(cost) + "。";
    } else {
      refs.rulerText.textContent = calc.add > 0
        ? "补仓把回本价从 " + fmtPrice(cost) + " 拉到 " + fmtPrice(be) + ";股价要从 " +
          fmtPrice(price) + " 涨到 " + fmtPrice(be) + " 才回本。"
        : "不补仓时,回本价就是原成本 " + fmtPrice(cost) + ";股价需涨到该价才回本。";
    }
  }

  /* ---------------- break-even chart ---------------- */

  function svgEl(tag, attrs = {}, parent = null) {
    const element = document.createElementNS(svgNS, tag);
    Object.entries(attrs).forEach(([key, value]) => {
      if (value !== undefined && value !== null) element.setAttribute(key, value);
    });
    if (parent) parent.appendChild(element);
    return element;
  }
  function svgText(parent, x, y, text, options = {}) {
    const element = svgEl("text", {
      x,
      y,
      "text-anchor": options.anchor || "start",
      "dominant-baseline": options.baseline || "middle",
      "font-size": options.size || 11,
      "font-weight": options.weight || 400,
      fill: options.fill || "#7a837c"
    }, parent);
    element.textContent = text;
    return element;
  }
  function linePath(points) {
    return points.map((point, index) =>
      (index === 0 ? "M" : "L") + point.x.toFixed(2) + " " + point.y.toFixed(2)
    ).join(" ");
  }
  function niceScale(min, max, count = 4) {
    if (!(max > min)) return { min: min - 0.5, max: min + 0.5, ticks: [min] };
    const raw = (max - min) / count;
    const mag = Math.pow(10, Math.floor(Math.log10(raw)));
    const norm = raw / mag;
    const factor = norm <= 1 ? 1 : norm <= 2 ? 2 : norm <= 2.5 ? 2.5 : norm <= 5 ? 5 : 10;
    const step = factor * mag;
    const lo = Math.floor(min / step) * step;
    const hi = Math.ceil(max / step) * step;
    const ticks = [];
    for (let value = lo; value <= hi + step * 1e-9; value += step) ticks.push(value);
    return { min: lo, max: hi, ticks };
  }

  function renderBeHeader(calc) {
    const isPrice = state.chartTab === "price";
    refs.beTitle.textContent = isPrice ? "补仓比例,怎么改变回本价" : "补仓比例,要花多少钱";
    refs.beSubtitle.textContent = isPrice
      ? "横轴:补仓比例(占现有股数)· 纵轴:回本价(元)"
      : "横轴:补仓比例(占现有股数)· 纵轴:补仓金额(元)";
    refs.beLegend.innerHTML =
      '<span><i class="legend-line"></i>' + (isPrice ? "补后回本价" : "补仓金额") + '</span>' +
      (isPrice ? '<span><i class="legend-line base"></i>原成本</span><span><i class="legend-line price"></i>现价</span>' : "");
    refs.beNote.textContent = isPrice
      ? "补得越多,回本价越接近现价;但每多补 10%,带来的下降越来越小。"
      : "补得越多,投入越大;曲线是直线,回本价的改善却不是。";
    $$("[data-betab]").forEach(button => {
      const active = button.dataset.betab === state.chartTab;
      button.classList.toggle("active", active);
      button.setAttribute("aria-selected", String(active));
    });
  }

  function renderBeChart(calc) {
    const wrap = refs.beWrap;
    const svg = refs.beSvg;
    if (!wrap || !svg) return;

    const width = Math.max(300, Math.floor(wrap.clientWidth || 760));
    const narrow = width < 600;
    const height = narrow ? 340 : 430;
    const margin = {
      left: narrow ? 48 : 66,
      right: narrow ? 16 : 28,
      top: narrow ? 28 : 32,
      bottom: narrow ? 44 : 50
    };
    const iw = width - margin.left - margin.right;
    const ih = height - margin.top - margin.bottom;

    svg.setAttribute("viewBox", "0 0 " + width + " " + height);
    svg.setAttribute("width", width);
    svg.setAttribute("height", height);
    svg.innerHTML = "";
    hideBeTip();

    if (!calc || !calc.valid) {
      beGeom = null;
      svgEl("rect", {
        x: margin.left, y: margin.top, width: iw, height: ih, rx: 9, fill: "#faf7ef"
      }, svg);
      svgText(svg, margin.left + iw / 2, margin.top + ih / 2, "填写持股数、成本价和现价后显示曲线", {
        anchor: "middle", size: 11, fill: "#8a928b"
      });
      return;
    }

    const actualRatio = calc.addRatio;
    let maxRatio = Math.max(200, Math.ceil(actualRatio / 50) * 50);
    maxRatio = clamp(maxRatio, 200, 2000);
    const isPrice = state.chartTab === "price";

    const curve = [];
    for (let ratio = 0; ratio <= maxRatio; ratio += 2) {
      const item = calcAtRatio(ratio, false);
      if (!item) continue;
      curve.push({ ratio, calc: item });
    }
    if (curve[curve.length - 1].ratio < maxRatio) {
      const item = calcAtRatio(maxRatio, false);
      if (item) curve.push({ ratio: maxRatio, calc: item });
    }

    let yMin;
    let yMax;
    if (isPrice) {
      const values = [calc.cost, calc.price, calc.breakeven].concat(curve.map(item => item.calc.breakeven));
      yMin = Math.min(...values);
      yMax = Math.max(...values);
      const pad = Math.max((yMax - yMin) * 0.18, yMax * 0.01, 0.01);
      yMin = Math.max(0, yMin - pad);
      yMax += pad;
    } else {
      const values = curve.map(item => item.calc.addAmount).concat([calc.addAmount, 0]);
      yMin = 0;
      yMax = Math.max(1, ...values) * 1.08;
    }
    const scale = niceScale(yMin, yMax, narrow ? 4 : 5);
    yMin = scale.min;
    yMax = scale.max;
    const ySpan = Math.max(yMax - yMin, 1e-9);

    const xPos = ratio => margin.left + (ratio / maxRatio) * iw;
    const yPos = value => margin.top + ih - ((value - yMin) / ySpan) * ih;

    const defs = svgEl("defs", {}, svg);
    const clip = svgEl("clipPath", { id: "beClip" }, defs);
    svgEl("rect", { x: margin.left, y: margin.top, width: iw, height: ih }, clip);
    const gradient = svgEl("linearGradient", { id: "beFill", x1: "0", y1: "0", x2: "0", y2: "1" }, defs);
    svgEl("stop", { offset: "0%", "stop-color": "#b28733", "stop-opacity": ".2" }, gradient);
    svgEl("stop", { offset: "100%", "stop-color": "#b28733", "stop-opacity": ".01" }, gradient);

    const plot = svgEl("g", { "clip-path": "url(#beClip)" }, svg);
    svgEl("rect", { x: margin.left, y: margin.top, width: iw, height: ih, rx: 9, fill: "#faf7ef" }, plot);

    scale.ticks.forEach(value => {
      const y = yPos(value);
      svgEl("line", {
        x1: margin.left, x2: margin.left + iw, y1: y, y2: y,
        stroke: "rgba(24,35,29,.07)", "stroke-width": 1
      }, plot);
      svgText(svg, margin.left - 9, y + 0.5, isPrice ? fmtPrice(value) : fmtMoneyShort(value), {
        anchor: "end", size: narrow ? 9 : 10, fill: "#8a928b"
      });
    });

    const xStep = maxRatio <= 100 ? 20 : maxRatio <= 200 ? 25 :
      maxRatio <= 500 ? 50 : maxRatio <= 1000 ? 100 : 200;
    for (let ratio = 0; ratio <= maxRatio; ratio += xStep) {
      const x = xPos(ratio);
      svgEl("line", {
        x1: x, x2: x, y1: margin.top, y2: margin.top + ih,
        stroke: "rgba(24,35,29,.045)", "stroke-width": 1
      }, plot);
      svgText(svg, x, margin.top + ih + 19, ratio + "%", {
        anchor: "middle", size: narrow ? 9 : 10, fill: "#8a928b"
      });
    }

    if (isPrice) {
      const costY = yPos(calc.cost);
      svgEl("line", {
        x1: margin.left, x2: margin.left + iw, y1: costY, y2: costY,
        stroke: "rgba(189,88,59,.55)", "stroke-dasharray": "4 5", "stroke-width": 1.2
      }, plot);
      svgText(svg, margin.left + iw - 7, costY - 8, "原成本 " + fmtPrice(calc.cost), {
        anchor: "end", size: 9.5, fill: "#bd583b", weight: 600
      });
      const priceY = yPos(calc.price);
      svgEl("line", {
        x1: margin.left, x2: margin.left + iw, y1: priceY, y2: priceY,
        stroke: "rgba(15,92,71,.48)", "stroke-dasharray": "4 5", "stroke-width": 1.2
      }, plot);
      svgText(svg, margin.left + 7, priceY + 14, "现价 " + fmtPrice(calc.price), {
        size: 9.5, fill: "#0f5c47", weight: 600
      });
    }

    const points = curve.map(item => ({
      x: xPos(item.ratio),
      y: yPos(isPrice ? item.calc.breakeven : item.calc.addAmount)
    }));
    const area = points.map((point, index) =>
      (index === 0 ? "M" : "L") + point.x.toFixed(2) + " " + point.y.toFixed(2)
    ).join(" ") +
      " L" + xPos(maxRatio).toFixed(2) + " " + (margin.top + ih).toFixed(2) +
      " L" + xPos(0).toFixed(2) + " " + (margin.top + ih).toFixed(2) + " Z";

    svgEl("path", { d: area, fill: "url(#beFill)", stroke: "none" }, plot);
    svgEl("path", {
      d: linePath(points),
      fill: "none",
      stroke: "#b28733",
      "stroke-width": 2.6,
      "stroke-linecap": "round",
      "stroke-linejoin": "round"
    }, plot);

    const selRatio = clamp(actualRatio, 0, maxRatio);
    const selX = xPos(selRatio);
    const selY = yPos(isPrice ? calc.breakeven : calc.addAmount);
    svgEl("line", {
      x1: selX, x2: selX, y1: selY, y2: margin.top + ih,
      stroke: "rgba(178,135,51,.4)", "stroke-dasharray": "3 5", "stroke-width": 1
    }, plot);
    svgEl("circle", {
      cx: selX, cy: selY, r: 5.5, fill: "#b28733", stroke: "#fffdf8", "stroke-width": 2.5
    }, plot);

    const ratioLabel = actualRatio > maxRatio
      ? fmtPctAbs(maxRatio / 100, 0) + "+"
      : fmtPctAbs(selRatio / 100, 1);
    const tagText = isPrice
      ? ratioLabel + " · " + fmtPrice(calc.breakeven) + " 元"
      : ratioLabel + " · " + fmtMoneyShort(calc.addAmount);
    addChartTag(svg, { margin, iw, ih }, selX, selY, tagText, "#a17b2d", selY < margin.top + 40);

    svgEl("line", {
      x1: margin.left, x2: margin.left + iw, y1: margin.top + ih, y2: margin.top + ih,
      stroke: "rgba(24,35,29,.2)", "stroke-width": 1
    }, plot);
    svgEl("line", {
      x1: margin.left, x2: margin.left, y1: margin.top, y2: margin.top + ih,
      stroke: "rgba(24,35,29,.2)", "stroke-width": 1
    }, plot);

    svgText(svg, margin.left, margin.top - 12, isPrice ? "回本价(元)" : "补仓金额", {
      size: 10, fill: "#7a837c", weight: 600
    });
    svgText(svg, margin.left + iw / 2, height - 10, "补仓比例(占现有股数)", {
      anchor: "middle", size: 10, fill: "#7a837c"
    });

    const hoverGroup = svgEl("g", { opacity: 0 }, svg);
    const hoverLine = svgEl("line", {
      y1: margin.top, y2: margin.top + ih,
      stroke: "rgba(24,35,29,.38)", "stroke-dasharray": "4 4", "stroke-width": 1
    }, hoverGroup);
    const hoverDot = svgEl("circle", {
      r: 4.6, fill: "#b28733", stroke: "#fffdf8", "stroke-width": 2
    }, hoverGroup);

    beGeom = {
      width, height, margin, iw, ih, maxRatio, isPrice,
      xPos, yPos, calc, curve, hoverGroup, hoverLine, hoverDot
    };
  }

  function addChartTag(parent, geom, cx, cy, text, color, above) {
    const chars = Array.from(text);
    const width = chars.reduce((sum, ch) => sum + (ch.charCodeAt(0) > 255 ? 10 : 6), 0) + 16;
    const height = 20;
    const x = clamp(cx - width / 2, geom.margin.left + 3, geom.margin.left + geom.iw - width - 3);
    let y = above ? cy - height - 8 : cy + 8;
    if (y < geom.margin.top + 3) y = cy + 8;
    if (y + height > geom.margin.top + geom.ih - 3) y = cy - height - 8;
    y = clamp(y, geom.margin.top + 3, geom.margin.top + geom.ih - height - 3);
    const group = svgEl("g", {}, parent);
    svgEl("rect", {
      x, y, width, height, rx: 6, fill: "#fffdf8",
      stroke: color, "stroke-opacity": .34, "stroke-width": 1
    }, group);
    svgText(group, x + 8, y + height / 2 + 0.5, text, {
      size: 10.5, weight: 650, fill: color
    });
  }

  function hideBeTip() {
    if (beGeom) beGeom.hoverGroup.setAttribute("opacity", "0");
    refs.beTip.hidden = true;
  }

  function onBeMove(event) {
    const geom = beGeom;
    if (!geom) return;
    const rect = refs.beSvg.getBoundingClientRect();
    if (!rect.width) return;
    const localX = (event.clientX - rect.left) * (geom.width / rect.width);
    if (localX < geom.margin.left - 6 || localX > geom.margin.left + geom.iw + 6) {
      hideBeTip();
      return;
    }
    const ratio = clamp(Math.round(((localX - geom.margin.left) / geom.iw) * geom.maxRatio), 0, geom.maxRatio);
    const item = calcAtRatio(ratio, true) || calcAtRatio(ratio, false);
    if (!item) { hideBeTip(); return; }
    const px = geom.xPos(ratio);
    const py = geom.yPos(geom.isPrice ? item.breakeven : item.addAmount);
    geom.hoverGroup.setAttribute("opacity", "1");
    geom.hoverLine.setAttribute("x1", px);
    geom.hoverLine.setAttribute("x2", px);
    geom.hoverDot.setAttribute("cx", px);
    geom.hoverDot.setAttribute("cy", py);

    refs.beTip.innerHTML =
      '<div class="tt-head"><span>补仓 ' + ratio + '%</span><b>' + fmtShares(item.add) + ' 股</b></div>' +
      '<div class="tt-row"><i class="tt-dot current"></i><span>回本价</span><b>' + fmtMoney(item.breakeven) + '</b></div>' +
      '<div class="tt-row"><i class="tt-dot base"></i><span>回本需涨</span><b class="loss">' +
        (item.rise > 0 ? fmtPct(item.rise, 2) : "已回本") + '</b></div>' +
      '<div class="tt-sep"></div>' +
      '<div class="tt-row plain"><span>补仓金额</span><b>' + fmtMoney(item.addAmount) + '</b></div>' +
      '<div class="tt-row plain"><span>补后总股数</span><b>' + fmtShares(item.totalShares) + ' 股</b></div>' +
      '<div class="tt-row plain"><span>补后亏损</span><b>' + fmtPct(item.postPct, 2) + '</b></div>';
    refs.beTip.hidden = false;

    const tipWidth = refs.beTip.offsetWidth || 224;
    const tipHeight = refs.beTip.offsetHeight || 140;
    let left = localX + 16;
    if (left + tipWidth > geom.width - 6) left = localX - tipWidth - 16;
    left = clamp(left, 6, geom.width - tipWidth - 6);
    let top = py - tipHeight / 2;
    top = clamp(top, 6, geom.height - tipHeight - 6);
    refs.beTip.style.left = left + "px";
    refs.beTip.style.top = top + "px";
  }

  /* ---------------- cost / structure ---------------- */

  function renderCost(calc) {
    if (!calc || !calc.valid) {
      refs.costBars.innerHTML = "";
      refs.costDelta.textContent = "填写参数后显示成本对比。";
      refs.donut.style.setProperty("--orig", "100%");
      refs.donutPct.textContent = "—";
      refs.donutLegend.innerHTML = "";
      refs.costSubtitle.textContent = "填写参数后显示成本与持仓结构。";
      return;
    }
    const values = [
      { key: "before", label: "原成本", value: calc.cost },
      { key: "price", label: "现价", value: calc.price },
      { key: "after", label: "补后均价", value: calc.breakeven }
    ];
    const max = Math.max(...values.map(item => item.value), 0.0001);
    refs.costBars.innerHTML = values.map(item =>
      '<div class="cost-item ' + item.key + '">' +
        '<span class="cost-value">' + fmtPrice(item.value) + '</span>' +
        '<div class="cost-track"><i class="cost-fill" style="height:' +
          Math.max(4, (item.value / max) * 100).toFixed(2) + '%"></i></div>' +
        '<span class="cost-label">' + item.label + '</span>' +
      '</div>'
    ).join("");

    const delta = calc.cost - calc.breakeven;
    refs.costDelta.innerHTML = Math.abs(delta) < 0.005
      ? "补后均价与原成本持平"
      : "补后均价较原成本 " +
        (delta > 0 ? "<b>↓ " + fmtPrice(delta) + "</b>" : "<b>↑ " + fmtPrice(Math.abs(delta)) + "</b>") +
        " 元";
    refs.costSubtitle.textContent = calc.add > 0
      ? "补后均价 " + fmtPrice(calc.breakeven) + " 元,介于现价 " + fmtPrice(calc.price) +
        " 与成本 " + fmtPrice(calc.cost) + " 之间。"
      : "未补仓,均价仍是原成本 " + fmtPrice(calc.cost) + " 元。";

    const total = calc.totalShares;
    const origPct = total > 0 ? calc.shares / total : 1;
    const addPct = 1 - origPct;
    refs.donut.style.setProperty("--orig", (origPct * 100).toFixed(2) + "%");
    refs.donutPct.textContent = (origPct * 100).toFixed(2) + "%";
    refs.donutLegend.innerHTML =
      '<span><i class="orig"></i>原持股 <b>' + fmtShares(calc.shares) + ' 股</b></span>' +
      '<span><i class="add"></i>新增 <b>' + fmtShares(calc.add) + ' 股</b></span>';
  }

  /* ---------------- ratio table ---------------- */

  function renderBeTable(calc) {
    if (!calc || !calc.valid) {
      refs.beTableBody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--muted)">填写参数后显示</td></tr>';
      return;
    }
    const ratios = [0, 10, 20, 30, 50, 75, 100, 150, 200];
    const actual = calc.addRatio;
    if (actual > 0 && !ratios.some(value => Math.abs(value - actual) < 0.05)) {
      ratios.push(Math.round(actual * 10) / 10);
      ratios.sort((a, b) => a - b);
    }
    refs.beTableBody.innerHTML = ratios.map(ratio => {
      const item = calcAtRatio(ratio, true);
      if (!item) return "";
      const selected = Math.abs(ratio - actual) < 0.05;
      const classes = [];
      if (selected) classes.push("is-selected");
      if (ratio === 0) classes.push("is-zero");
      return '<tr class="' + classes.join(" ") + '">' +
        '<td>' + ratio + '%' + (ratio === 0 ? "(不补)" : "") + '</td>' +
        '<td>' + fmtShares(item.add) + '</td>' +
        '<td>' + fmtMoney(item.addAmount) + '</td>' +
        '<td class="gold">' + fmtPrice(item.breakeven) + '</td>' +
        '<td class="' + (item.rise > 0 ? "loss" : "good") + '">' +
          (item.rise > 0 ? fmtPct(item.rise, 2) : "已回本") + '</td>' +
        '<td class="' + (item.postPct < 0 ? "loss" : "good") + '">' + fmtPct(item.postPct, 2) + '</td>' +
        '</tr>';
    }).join("");
  }

  function shareAdjustText(raw, actual) {
    if (Math.abs(actual - raw) > 0.05) {
      const label = state.roundLot ? "按 100 股取整" : "取整";
      return trimNum(raw.toFixed(1)) + " 股 → " + label + " <b>" + fmtShares(actual) + "</b> 股";
    }
    return "<b>" + fmtShares(actual) + "</b> 股";
  }

  function renderCalcLine(calc) {
    if (!calc || !calc.valid) {
      refs.calcLine.textContent = "填写持股数、成本价和现价后,这里显示计算过程。";
      return;
    }
    if (state.mode === "target" && calc.warning) {
      refs.calcLine.textContent = calc.warning;
      return;
    }
    if (calc.add <= 0) {
      refs.calcLine.textContent = calc.rise > 0
        ? "未补仓;回本价 = 原成本 " + fmtPrice(calc.cost) + " 元,需涨 " +
          fmtPct(calc.rise, 2) + " 才回本。"
        : "未补仓;现价 " + fmtPrice(calc.price) + " 已不低于成本 " + fmtPrice(calc.cost) + ",当前不亏。";
      return;
    }
    if (state.mode === "ratio") {
      refs.calcLine.innerHTML = "补仓股数 = " + fmtShares(calc.shares) + " × " +
        trimNum(Number(state.ratio).toFixed(2)) + "% = " + shareAdjustText(calc.rawAdd, calc.add) +
        ";回本价 = (" + fmtShares(calc.shares) + "×" + fmtPrice(calc.cost) + " + " +
        fmtShares(calc.add) + "×" + fmtPrice(calc.price) + ") ÷ " + fmtShares(calc.totalShares) +
        " = <b>" + fmtPrice(calc.breakeven) + "</b> 元";
    } else if (state.mode === "shares") {
      refs.calcLine.innerHTML = "补仓股数 = " + shareAdjustText(calc.rawAdd, calc.add) +
        ";回本价 = (" + fmtShares(calc.shares) + "×" + fmtPrice(calc.cost) + " + " +
        fmtShares(calc.add) + "×" + fmtPrice(calc.price) + ") ÷ " + fmtShares(calc.totalShares) +
        " = <b>" + fmtPrice(calc.breakeven) + "</b> 元";
    } else if (state.mode === "amount") {
      refs.calcLine.innerHTML = "补仓股数 = " + fmtMoney(state.amount) + " ÷ " + fmtPrice(calc.price) +
        " = " + shareAdjustText(calc.rawAdd, calc.add) +
        ";回本价 = <b>" + fmtPrice(calc.breakeven) + "</b> 元";
    } else {
      refs.calcLine.innerHTML = "补仓股数 = " + fmtShares(calc.shares) + " × (" +
        fmtPrice(calc.cost) + " &#8722; " + fmtPrice(state.target) + ") ÷ (" + fmtPrice(state.target) +
        " &#8722; " + fmtPrice(calc.price) + ") = " + shareAdjustText(calc.rawAdd, calc.add) +
        ";回本价 = <b>" + fmtPrice(calc.breakeven) + "</b> 元";
    }
  }

  function syncSingleControls(calc) {
    $$("[data-mode]").forEach(button => {
      const active = button.dataset.mode === state.mode;
      button.classList.toggle("active", active);
      button.setAttribute("aria-pressed", String(active));
    });
    refs.ratioPanel.hidden = state.mode !== "ratio";
    refs.sharesPanel.hidden = state.mode !== "shares";
    refs.amountPanel.hidden = state.mode !== "amount";
    refs.targetPanel.hidden = state.mode !== "target";
    refs.ratioRange.value = state.ratio;
    setRangeFill(refs.ratioRange);
    refs.ratioInput.value = state.ratio;
    refs.sharesToAddInput.value = state.addShares;
    refs.amountInput.value = state.amount;
    refs.targetInput.value = state.target;
    refs.roundLot.checked = state.roundLot;
    refs.stockName.textContent = state.name || "—";

    if (Number(state.cost) > 0 && Number(state.price) > 0) {
      if (Number(state.cost) > Number(state.price)) {
        refs.targetHint.textContent = "目标均价必须低于成本 " + fmtPrice(Number(state.cost)) +
          "、高于现价 " + fmtPrice(Number(state.price)) + "。";
      } else {
        refs.targetHint.textContent = "当前股价不低于成本,补仓会抬高均价;按目标成本不适用。";
      }
    } else {
      refs.targetHint.textContent = "先填写成本和现价,再设置目标均价。";
    }
    if (calc && calc.warning) refs.targetHint.textContent = calc.warning;
  }

  function renderAll() {
    const calc = calcSingle();
    syncSingleControls(calc);
    renderHero(calc);
    renderKpis(calc);
    renderRuler(calc);
    renderBeHeader(calc);
    renderBeChart(calc);
    renderCost(calc);
    renderBeTable(calc);
    renderCalcLine(calc);
    hideBeTip();
  }

  function scheduleRender() {
    if (renderFrame) return;
    renderFrame = requestAnimationFrame(() => {
      renderFrame = 0;
      renderAll();
    });
  }

  /* ---------------- batch ---------------- */

  function makeRow(overrides = {}) {
    state.rowSeq += 1;
    return {
      id: state.rowSeq,
      code: "sh600000",
      name: "浦发银行",
      shares: 1000,
      cost: 10,
      price: 8,
      ratio: state.batchRatio,
      ...overrides
    };
  }

  function rowHtml(row) {
    const calc = calcForRow(row);
    return '<tr data-row-id="' + row.id + '">' +
      '<td class="cell-stock">' +
        '<input class="code-input" type="text" data-field="code" value="' + escapeAttr(row.code) +
          '" placeholder="sh600519" spellcheck="false" aria-label="股票代码">' +
        '<small class="row-name" data-cell="name">' + escapeHtml(row.name || "—") + '</small>' +
      '</td>' +
      '<td><input type="number" data-field="shares" min="0" step="100" value="' + numberAttr(row.shares) + '" aria-label="持股数"></td>' +
      '<td><input type="number" data-field="cost" min="0" step="0.01" value="' + numberAttr(row.cost) + '" aria-label="成本价"></td>' +
      '<td><input type="number" data-field="price" min="0" step="0.01" value="' + numberAttr(row.price) + '" aria-label="现价"></td>' +
      '<td class="cell-ratio"><input type="number" data-field="ratio" min="0" max="500" step="1" value="' +
        numberAttr(row.ratio) + '" aria-label="补仓比例">%</td>' +
      '<td class="num" data-cell="addShares">' + (calc ? fmtShares(calc.add) : "—") + '</td>' +
      '<td class="num" data-cell="addAmount">' + (calc ? fmtMoney(calc.addAmount) : "—") + '</td>' +
      '<td class="num rec-gold" data-cell="breakeven">' + (calc ? fmtPrice(calc.breakeven) : "—") + '</td>' +
      '<td class="num" data-cell="rise">' + (calc ? (calc.rise > 0 ? fmtPct(calc.rise, 2) : "已回本") : "—") + '</td>' +
      '<td class="cell-actions"><div class="row-actions">' +
        '<button class="row-btn" type="button" data-action="analyze">精算</button>' +
        '<button class="row-btn danger" type="button" data-action="remove">删除</button>' +
      '</div></td>' +
      '</tr>';
  }

  function escapeHtml(value) {
    return String(value == null ? "" : value)
      .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;");
  }
  function escapeAttr(value) {
    return escapeHtml(value).replace(/'/g, "&#39;");
  }
  function numberAttr(value) {
    return Number.isFinite(Number(value)) ? String(Number(value)) : "";
  }

  function renderBatch() {
    if (!state.rows.length) {
      refs.batchBody.innerHTML = '<tr><td colspan="10" class="batch-empty">还没有股票。点击「+ 添加股票」,或导入 获取股票信息.txt。</td></tr>';
    } else {
      refs.batchBody.innerHTML = state.rows.map(rowHtml).join("");
    }
    refs.batchBadge.textContent = state.rows.length + " 只";
    updateBatchTotals();
  }

  function updateBatchRow(row) {
    const tr = refs.batchBody.querySelector('[data-row-id="' + row.id + '"]');
    if (!tr) return;
    const calc = calcForRow(row);
    const nameEl = tr.querySelector('[data-cell="name"]');
    if (nameEl) nameEl.textContent = row.name || "—";
    const set = (cell, text, className) => {
      const el = tr.querySelector('[data-cell="' + cell + '"]');
      if (!el) return;
      el.textContent = text;
      if (className !== undefined) el.className = "num " + className;
    };
    set("addShares", calc ? fmtShares(calc.add) : "—");
    set("addAmount", calc ? fmtMoney(calc.addAmount) : "—");
    set("breakeven", calc ? fmtPrice(calc.breakeven) : "—", "num rec-gold");
    set("rise", calc ? (calc.rise > 0 ? fmtPct(calc.rise, 2) : "已回本") : "—");
    updateBatchTotals();
  }

  function updateBatchTotals() {
    let addAmount = 0;
    let totalShares = 0;
    let totalCost = 0;
    let pnl = 0;
    let count = 0;
    state.rows.forEach(row => {
      const calc = calcForRow(row);
      if (!calc) return;
      count += 1;
      addAmount += calc.addAmount;
      totalShares += calc.totalShares;
      totalCost += calc.totalCost;
      pnl += calc.postPnl;
    });
    if (!count) {
      refs.batchAddAmount.textContent = "—";
      refs.batchTotalShares.textContent = "—";
      refs.batchTotalCost.textContent = "—";
      refs.batchPnl.textContent = "—";
      refs.batchPnlLabel.textContent = "组合盈亏(按现价)";
      return;
    }
    refs.batchAddAmount.textContent = fmtMoney(addAmount);
    refs.batchTotalShares.textContent = fmtShares(totalShares) + " 股";
    refs.batchTotalCost.textContent = fmtMoney(totalCost);
    refs.batchPnl.textContent = fmtLossMoney(pnl);
    refs.batchPnl.style.color = pnl >= 0 ? "var(--green)" : "var(--terra)";
    refs.batchPnlLabel.textContent = pnl >= 0 ? "组合浮盈(按现价)" : "组合浮亏(按现价)";
  }

  function updateAllBatch() {
    state.rows.forEach(row => updateBatchRow(row));
    updateBatchTotals();
  }

  function findRow(id) {
    return state.rows.find(row => String(row.id) === String(id));
  }

  function setBatchStatus(text, tone = "") {
    refs.batchStatus.textContent = text || "";
    refs.batchStatus.className = "status-line batch-status" + (tone ? " " + tone : "");
  }

  function analyzeRow(row) {
    state.code = normalizeCode(row.code);
    state.name = row.name || state.name;
    state.shares = Number(row.shares) || 0;
    state.cost = Number(row.cost) || 0;
    state.price = Number(row.price) || 0;
    state.mode = "ratio";
    state.ratio = Number(row.ratio) || 0;
    renderAll();
    window.scrollTo({ top: 0, behavior: "smooth" });
  }

  async function fetchSingle() {
    const btn = refs.fetchBtn;
    btn.disabled = true;
    refs.fetchStatus.textContent = "获取中…";
    refs.fetchStatus.className = "status-line busy";
    try {
      const quote = await fetchQuote(state.code);
      state.code = quote.code;
      state.name = quote.name;
      state.price = quote.price;
      refs.codeInput.value = quote.code;
      refs.priceInput.value = fmtPrice(quote.price);
      refs.fetchStatus.textContent = "已更新 " + quote.name + " · " + fmtPrice(quote.price) + " 元";
      refs.fetchStatus.className = "status-line ok";
      renderAll();
    } catch (error) {
      refs.fetchStatus.textContent = error.message || "获取失败";
      refs.fetchStatus.className = "status-line bad";
    } finally {
      btn.disabled = false;
    }
  }

  async function fetchAllBatch() {
    const codes = [];
    state.rows.forEach(row => {
      const code = normalizeCode(row.code);
      if (isStockCode(code) && !codes.includes(code)) codes.push(code);
    });
    if (!codes.length) {
      setBatchStatus("没有可获取的有效代码", "bad");
      return;
    }
    refs.fetchAllBtn.disabled = true;
    setBatchStatus("正在获取 " + codes.length + " 只股票的现价…", "busy");
    const results = await Promise.allSettled(codes.map(code => fetchQuote(code)));
    const byCode = new Map();
    let ok = 0;
    let fail = 0;
    results.forEach((result, index) => {
      if (result.status === "fulfilled") {
        ok += 1;
        byCode.set(codes[index], result.value);
      } else {
        fail += 1;
      }
    });
    state.rows.forEach(row => {
      const code = normalizeCode(row.code);
      const quote = byCode.get(code);
      if (!quote) return;
      row.code = quote.code;
      row.name = quote.name;
      row.price = quote.price;
    });
    if (byCode.has(normalizeCode(state.code))) {
      const quote = byCode.get(normalizeCode(state.code));
      state.name = quote.name;
      state.price = quote.price;
      refs.codeInput.value = quote.code;
      refs.priceInput.value = fmtPrice(quote.price);
    }
    renderBatch();
    renderAll();
    refs.fetchAllBtn.disabled = false;
    setBatchStatus(ok + " 只更新成功" + (fail ? "," + fail + " 只失败(可手动填价)" : ""),
      fail ? "bad" : "ok");
  }

  function addBlankRow(overrides = {}) {
    const row = {
      id: ++state.rowSeq,
      code: "",
      name: "",
      shares: 0,
      cost: 0,
      price: 0,
      ratio: state.batchRatio,
      ...overrides
    };
    state.rows.push(row);
    renderBatch();
    const input = refs.batchBody.querySelector('[data-row-id="' + row.id + '"] .code-input');
    if (input) input.focus();
    return row;
  }

  /* ---------------- events ---------------- */

  refs.codeInput.addEventListener("input", event => {
    state.code = event.target.value;
  });
  refs.codeInput.addEventListener("blur", () => {
    const code = normalizeCode(state.code);
    state.code = code;
    refs.codeInput.value = code;
  });
  refs.codeInput.addEventListener("keydown", event => {
    if (event.key === "Enter") {
      event.preventDefault();
      fetchSingle();
    }
  });
  refs.fetchBtn.addEventListener("click", fetchSingle);

  [refs.sharesInput, refs.costInput, refs.priceInput].forEach(input => {
    input.addEventListener("input", () => {
      state.shares = Math.max(0, Number(refs.sharesInput.value) || 0);
      state.cost = Math.max(0, Number(refs.costInput.value) || 0);
      state.price = Math.max(0, Number(refs.priceInput.value) || 0);
      scheduleRender();
    });
    input.addEventListener("blur", () => {
      state.shares = Math.round(clamp(Number(refs.sharesInput.value) || 0, 0, 1e9));
      state.cost = Math.round(clamp(Number(refs.costInput.value) || 0, 0, 1e6) * 100) / 100;
      state.price = Math.round(clamp(Number(refs.priceInput.value) || 0, 0, 1e6) * 100) / 100;
      refs.sharesInput.value = state.shares;
      refs.costInput.value = fmtPrice(state.cost);
      refs.priceInput.value = fmtPrice(state.price);
      renderAll();
    });
  });

  refs.ratioRange.addEventListener("input", event => {
    state.ratio = clamp(Math.round(Number(event.target.value)), 0, 500);
    scheduleRender();
  });
  refs.ratioInput.addEventListener("input", () => {
    state.ratio = clamp(Math.round(Number(refs.ratioInput.value) || 0), 0, 500);
    scheduleRender();
  });
  refs.ratioInput.addEventListener("blur", () => {
    state.ratio = clamp(Math.round(Number(refs.ratioInput.value) || 0), 0, 500);
    refs.ratioInput.value = state.ratio;
    renderAll();
  });
  $$("[data-ratio]").forEach(button => {
    button.addEventListener("click", () => {
      state.ratio = Number(button.dataset.ratio);
      renderAll();
    });
  });

  refs.sharesToAddInput.addEventListener("input", () => {
    state.addShares = Math.max(0, Math.round(Number(refs.sharesToAddInput.value) || 0));
    scheduleRender();
  });
  refs.sharesToAddInput.addEventListener("blur", () => {
    state.addShares = Math.round(clamp(Number(refs.sharesToAddInput.value) || 0, 0, 1e9));
    refs.sharesToAddInput.value = state.addShares;
    renderAll();
  });
  $$("[data-add-shares]").forEach(button => {
    button.addEventListener("click", () => {
      state.addShares = Number(button.dataset.addShares);
      renderAll();
    });
  });

  refs.amountInput.addEventListener("input", () => {
    state.amount = Math.max(0, Number(refs.amountInput.value) || 0);
    scheduleRender();
  });
  refs.amountInput.addEventListener("blur", () => {
    state.amount = Math.round(clamp(Number(refs.amountInput.value) || 0, 0, 1e9));
    refs.amountInput.value = state.amount;
    renderAll();
  });
  $$("[data-amount]").forEach(button => {
    button.addEventListener("click", () => {
      state.amount = Number(button.dataset.amount);
      renderAll();
    });
  });

  refs.targetInput.addEventListener("input", () => {
    state.target = Math.max(0, Number(refs.targetInput.value) || 0);
    scheduleRender();
  });
  refs.targetInput.addEventListener("blur", () => {
    state.target = Math.round(clamp(Number(refs.targetInput.value) || 0, 0, 1e6) * 100) / 100;
    refs.targetInput.value = fmtPrice(state.target);
    renderAll();
  });
  refs.midTarget.addEventListener("click", () => {
    if (Number(state.cost) > Number(state.price)) {
      state.target = Math.round(((Number(state.cost) + Number(state.price)) / 2) * 100) / 100;
      renderAll();
    }
  });

  $$("[data-mode]").forEach(button => {
    button.addEventListener("click", () => {
      state.mode = button.dataset.mode;
      renderAll();
    });
  });

  refs.roundLot.addEventListener("change", () => {
    state.roundLot = refs.roundLot.checked;
    updateAllBatch();
    renderAll();
  });

  refs.applyToBatch.addEventListener("click", () => {
    const calc = calcSingle();
    const code = normalizeCode(state.code);
    const ratio = calc && calc.valid ? Math.round(calc.addRatio * 10) / 10 : Number(state.ratio);
    const existing = state.rows.find(row => normalizeCode(row.code) === code && code);
    if (existing) {
      existing.name = state.name;
      existing.shares = state.shares;
      existing.cost = state.cost;
      existing.price = state.price;
      existing.ratio = ratio;
    } else {
      state.rows.push({
        id: ++state.rowSeq,
        code,
        name: state.name,
        shares: state.shares,
        cost: state.cost,
        price: state.price,
        ratio
      });
    }
    renderBatch();
    setBatchStatus("已加入批量:" + (state.name || code || "当前股票"), "ok");
    const section = $("#batchSection");
    if (section) section.scrollIntoView({ behavior: "smooth", block: "start" });
  });

  refs.addRowBtn.addEventListener("click", () => addBlankRow());

  refs.clearBatchBtn.addEventListener("click", () => {
    if (!state.rows.length) return;
    if (!window.confirm("清空批量里的全部股票?")) return;
    state.rows = [];
    renderBatch();
    setBatchStatus("已清空", "");
  });

  refs.fetchAllBtn.addEventListener("click", fetchAllBatch);

  refs.importBtn.addEventListener("click", () => refs.importFile.click());
  refs.importFile.addEventListener("change", async () => {
    const file = refs.importFile.files && refs.importFile.files[0];
    if (!file) return;
    setBatchStatus("读取 " + file.name + "…", "busy");
    try {
      const text = await file.text();
      const codes = extractCodes(text);
      if (!codes.length) {
        setBatchStatus("文件里没有找到股票代码", "bad");
        return;
      }
      let added = 0;
      codes.forEach(code => {
        const exists = state.rows.some(row => normalizeCode(row.code) === code);
        if (exists) return;
        state.rows.push({
          id: ++state.rowSeq,
          code,
          name: "",
          shares: 0,
          cost: 0,
          price: 0,
          ratio: state.batchRatio
        });
        added += 1;
      });
      renderBatch();
      setBatchStatus("已导入 " + added + " 只,正在获取现价…", "ok");
      await fetchAllBatch();
    } catch (error) {
      setBatchStatus("读取失败:" + (error.message || "未知错误"), "bad");
    } finally {
      refs.importFile.value = "";
    }
  });

  refs.batchBody.addEventListener("input", event => {
    const input = event.target.closest("[data-field]");
    if (!input) return;
    const tr = input.closest("[data-row-id]");
    if (!tr) return;
    const row = findRow(tr.dataset.rowId);
    if (!row) return;
    const field = input.dataset.field;
    if (field === "code") {
      row.code = input.value;
      row.name = "";
    } else {
      row[field] = Math.max(0, Number(input.value) || 0);
    }
    updateBatchRow(row);
  });

  refs.batchBody.addEventListener("change", event => {
    const input = event.target.closest("[data-field]");
    if (!input) return;
    const tr = input.closest("[data-row-id]");
    const row = findRow(tr.dataset.rowId);
    if (!row) return;
    if (input.dataset.field === "code") {
      const code = normalizeCode(input.value);
      row.code = code;
      input.value = code;
      updateBatchRow(row);
    }
  });

  refs.batchBody.addEventListener("keydown", async event => {
    if (event.key !== "Enter") return;
    const input = event.target.closest('[data-field="code"]');
    if (!input) return;
    const tr = input.closest("[data-row-id]");
    const row = findRow(tr.dataset.rowId);
    if (!row) return;
    event.preventDefault();
    const code = normalizeCode(row.code);
    row.code = code;
    input.value = code;
    setBatchStatus("获取 " + code + " 现价…", "busy");
    try {
      const quote = await fetchQuote(code);
      row.name = quote.name;
      row.price = quote.price;
      if (!(Number(row.cost) > 0)) {
        // 只补现价,不猜成本
      }
      renderBatch();
      setBatchStatus(quote.name + " " + fmtPrice(quote.price) + " 元", "ok");
    } catch (error) {
      setBatchStatus(error.message || "获取失败", "bad");
    }
  });

  refs.batchBody.addEventListener("click", event => {
    const action = event.target.closest("[data-action]");
    if (!action) return;
    const tr = action.closest("[data-row-id]");
    const row = findRow(tr.dataset.rowId);
    if (!row) return;
    if (action.dataset.action === "analyze") {
      analyzeRow(row);
    } else if (action.dataset.action === "remove") {
      state.rows = state.rows.filter(item => item.id !== row.id);
      renderBatch();
      setBatchStatus("已删除 1 只", "");
    }
  });

  $$("[data-batch-ratio]").forEach(button => {
    button.addEventListener("click", () => {
      state.batchRatio = Number(button.dataset.batchRatio);
      state.rows.forEach(row => { row.ratio = state.batchRatio; });
      renderBatch();
      setBatchStatus("全部改为 " + state.batchRatio + "%", "ok");
    });
  });

  refs.beTabs.addEventListener("click", event => {
    const button = event.target.closest("[data-betab]");
    if (!button) return;
    state.chartTab = button.dataset.betab;
    renderBeHeader(calcSingle());
    renderBeChart(calcSingle());
    hideBeTip();
  });

  refs.beWrap.addEventListener("pointermove", onBeMove);
  refs.beWrap.addEventListener("pointerleave", hideBeTip);
  refs.beWrap.addEventListener("pointercancel", hideBeTip);

  $("#resetBtn").addEventListener("click", () => {
    state.code = DEFAULTS.code;
    state.name = DEFAULTS.name;
    state.shares = DEFAULTS.shares;
    state.cost = DEFAULTS.cost;
    state.price = DEFAULTS.price;
    state.mode = DEFAULTS.mode;
    state.ratio = DEFAULTS.ratio;
    state.addShares = DEFAULTS.addShares;
    state.amount = DEFAULTS.amount;
    state.target = DEFAULTS.target;
    state.roundLot = DEFAULTS.roundLot;
    state.chartTab = DEFAULTS.chartTab;
    refs.codeInput.value = state.code;
    refs.sharesInput.value = state.shares;
    refs.costInput.value = fmtPrice(state.cost);
    refs.priceInput.value = fmtPrice(state.price);
    refs.fetchStatus.textContent = "价格来自腾讯行情,可手动改";
    refs.fetchStatus.className = "status-line";
    renderAll();
  });

  window.addEventListener("resize", () => {
    window.clearTimeout(resizeTimer);
    resizeTimer = window.setTimeout(() => {
      renderBeChart(calcSingle());
      hideBeTip();
    }, 110);
  });

  if ("ResizeObserver" in window) {
    let lastWidth = 0;
    const observer = new ResizeObserver(entries => {
      const width = Math.round(entries[0].contentRect.width);
      if (lastWidth && Math.abs(width - lastWidth) > 2) {
        lastWidth = width;
        renderBeChart(calcSingle());
      } else if (!lastWidth) {
        lastWidth = width;
      }
    });
    observer.observe(refs.beWrap);
  }

  if ("IntersectionObserver" in window) {
    const readout = $(".mobile-readout");
    const controls = $(".controls");
    if (readout && controls) {
      const readoutObserver = new IntersectionObserver(entries => {
        readout.classList.toggle("is-visible", entries[0].isIntersecting);
      }, { threshold: 0.02 });
      readoutObserver.observe(controls);
    }
  }

  state.rows = [makeRow()];
  refs.codeInput.value = state.code;
  refs.sharesInput.value = state.shares;
  refs.costInput.value = fmtPrice(state.cost);
  refs.priceInput.value = fmtPrice(state.price);
  renderBatch();
  renderAll();
})();
</script>
</body>
</html>

免费评分

参与人数 6吾爱币 +8 热心值 +6 收起 理由
jk998 + 1 + 1 我很赞同!
做风一样的男子 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
junshuiyujianni + 1 + 1 谢谢@Thanks!
九月风行 + 1 + 1 谢谢@Thanks!
妖魔鬼怪 + 1 + 1 我很赞同!
kuwo911 + 3 + 1 非常棒!

查看全部评分

发帖前要善用【论坛搜索】功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

frankcc 发表于 2026-10-25 23:29
支持楼主分享的软件
chenzhouyu 发表于 2026-10-25 23:35
wsdhgd 发表于 2026-10-25 23:41
kuwo911 发表于 2026-10-26 00:27
本帖最后由 kuwo911 于 2026-10-26 00:28 编辑

彩虹知己........................
yueye1122 发表于 2026-10-26 00:31
支持楼主分享的软件
darksetyaer 发表于 2026-10-26 01:07
支持一下
herofkk 发表于 2026-10-26 07:12
支持楼主分享软件
lovehkbs 发表于 2026-10-26 08:04
如今的市场被量化量化精准操控,再好的策略也难翻身。
szhahajing 发表于 2026-10-26 08:24
谢谢大佬分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-10-27 09:09

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表