吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1022|回复: 16
上一主题 下一主题
收起左侧

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

  [复制链接]
跳转到指定楼层
楼主
实事求是 发表于 2026-10-25 22:59 回帖奖励
本帖最后由 实事求是 于 2026-10-25 23:04 编辑

复利实验室 - 「股票复利可视化」工具

软件信息

如果每一期都稳定赚 X%,时间会把你带到哪。

解决什么问题

100 变 120,120 再赚 20% 变 144,144 再赚 20% 变 172.8。这就是利滚利。

10 万本金,每年 20%:

  1. 到 100 万:12.6 年,第 13 期,10 倍,约 3.3 次翻倍
  2. 到 1000 万:25.3 年,第 26 期,100 倍,约 6.6 次翻倍
  3. 到 1 亿:37.9 年,第 38 期,1000 倍,10 次翻倍
  4. 30 年:2374 万

每 3.8 年翻一倍。

切到每次模式,同样的 20% 按每一次交易结算,不换算成年,只数次数。

10 万本金,每次 20%:

  1. 到 100 万:12.6 次
  2. 到 1000 万:25.3 次
  3. 到 1 亿:37.9 次

不同每次收益的参考:

  1. 每次 3%:到 100 万 77.9 次,到 1000 万 155.8 次,到 1 亿 233.7 次
  2. 每次 5%:到 100 万 47.2 次,到 1000 万 94.4 次,到 1 亿 141.6 次
  3. 每次 10%:到 100 万 24.2 次,到 1000 万 48.3 次,到 1 亿 72.5 次

把起始本金改成 100 元,切到每次,就能看到 100 变 120、120 变 144、144 变 172.8;每次 20% 到 100 万约 50.5 次。

主要功能

  1. 复利频率:每年、每季、每月、每次。
  2. 每次模式:KPI、横轴、里程碑、明细全部从年换成次,不假设一年做多少次。
  3. 四个 KPI:到 100 万、到 1000 万、到 1 亿、期末账户。
  4. 复利曲线:对数 / 线性,鼠标划过看每一期;可切每年收益 / 每次收益柱状图。
  5. 收益率对比:5% 到 30%,多久或多少次到目标。
  6. 里程碑阶梯:20 万到 1 亿,每一档所需年数或次数、倍数。
  7. 前 12 期 / 前 12 次利滚利卡片。
  8. 逐年 / 逐次明细。
  9. 可选每期追加投入。
  10. 快捷场景会跟着模式变:按年是 8 / 15 / 20%,按次是 3 / 5 / 10%。

怎么用

  1. 选复利频率。做短线就选每次。
  2. 填每期收益率、计算年限或次数、起始本金、可选追加、目标金额。
  3. 看 KPI 和复利曲线;鼠标划过看具体年份或次数。
  4. 想看 100 变 120,切到每次,把起始本金改成 100。

四个前提

  1. 恒定收益率只是算术假设。真实市场会回撤,亏 50% 要涨 100% 才回本,波动会明显拖慢复利。
  2. 年化 20% 是极高标准,每次短线 20% 更极端。把它当上限,不是预期。
  3. 没有计佣金、印花税、过户费、滑点和通胀;短线换手的成本更高。
  4. 每次模式假设每次全仓、连续成功,不现实。先活得久,再谈复利。

免责声明

本工具仅用于复利数学演示,不构成投资建议。



复利实验室.zip (30.07 KB, 下载次数: 146)

[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-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .segmented-4 button { padding-right: 2px; padding-left: 2px; 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,
    .segmented-4 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; }
  }

  /* ============================================================
     复利盈利版 · 新增组件(沿用原有视觉语言)
     ============================================================ */

  .kpi-target::before { background: var(--green-2); }
  .kpi-gold::before { background: var(--gold); }
  .kpi-terra::before { background: var(--terra); }
  .kpi-end::before { background: var(--dark); }
  .kpi-value.gold { color: var(--gold); }
  .kpi-value.terra { color: var(--terra); }
  .kpi-value.ink { color: var(--ink); }

  .loss-input-wrap.gain input { color: var(--green); }
  .loss-input-wrap.gain span { color: var(--green); }

  .strip-principal {
    flex: none;
    width: 20%;
    min-width: 2px;
    border-radius: 99px 3px 3px 99px;
    background: linear-gradient(90deg, #8fbfa8, var(--green-2));
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .16);
    transition: width .25s ease;
  }
  .strip-gainbar {
    flex: 1;
    min-width: 0;
    border-radius: 3px 99px 99px 3px;
    background: linear-gradient(90deg, #e6d3a2, var(--gold));
  }
  .strip-card .strip-labels span:first-child b { color: var(--green-2); }
  .strip-card .strip-labels span:last-child b { color: var(--gold); }
  .strip-card .strip-amounts span:first-child b { color: var(--gold); }
  .strip-card .strip-amounts span:last-child b { color: var(--ink-2); }

  .chart-head { align-items: flex-start; }
  .chart-actions {
    display: flex;
    flex: none;
    flex-direction: column;
    align-items: flex-end;
    gap: 7px;
  }
  .scale-switch { display: flex; gap: 4px; }
  .mini-chip {
    padding: 4px 9px;
    border: 1px solid var(--line);
    border-radius: 7px;
    background: var(--paper-2);
    color: var(--muted);
    font-family: var(--num);
    font-size: 9.5px;
    line-height: 1.5;
    transition: .15s ease;
  }
  .mini-chip:hover { border-color: var(--green-2); color: var(--green); }
  .mini-chip.active {
    border-color: var(--dark);
    background: var(--dark);
    color: var(--cream);
    box-shadow: 0 2px 7px rgba(15, 36, 28, .16);
  }
  .mini-chip[hidden] { display: none; }

  #curveSvg, #compareSvg {
    display: block;
    width: 100%;
    height: auto;
    overflow: visible;
  }
  #curveSvg text, #compareSvg text { font-family: var(--font); }
  .chart-xl #curveSvg { margin-top: 2px; }
  .chart-tooltip.wide { width: 224px; }

  .legend-line.target { border-top: 2px dashed var(--gold); }
  .legend-line.you { border-top: 3px solid var(--ink); }

  .compare-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 12px;
  }
  .legend-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 5px 9px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--paper-2);
    color: var(--muted);
    font-size: 9.5px;
    line-height: 1.4;
    white-space: nowrap;
  }
  .legend-chip i { width: 8px; height: 8px; border-radius: 50%; flex: none; }
  .legend-chip b {
    color: var(--ink-2);
    font-family: var(--num);
    font-size: 10px;
    font-weight: 600;
  }
  .legend-chip.you {
    border-color: rgba(15, 92, 71, .32);
    background: var(--green-wash);
    color: var(--green);
  }
  .legend-chip.you b { color: var(--green); }

  .ms-ladder { margin-top: 18px; border-top: 1px solid var(--line); }
  .ms-row {
    display: grid;
    grid-template-columns: 40px minmax(140px, 1.2fr) repeat(4, minmax(74px, .72fr));
    gap: 12px;
    align-items: center;
    padding: 13px 8px 12px;
    border-bottom: 1px solid var(--line);
  }
  .ms-row:last-child { border-bottom: 0; }
  .ms-row.is-target {
    margin: 0 -8px;
    padding-right: 16px;
    padding-left: 16px;
    border-radius: 12px;
    background: var(--green-wash);
    box-shadow: inset 3px 0 0 var(--green);
  }
  .ms-row.is-target .ladder-no {
    border-color: var(--green);
    background: var(--green);
    color: var(--cream);
  }
  .ms-row.is-target .ladder-metric b { color: var(--green); }
  .ms-row.is-target .ladder-track i { background: linear-gradient(90deg, #54a083, var(--green)); }
  .ms-row.is-hit:not(.is-target) .ladder-metric .good { color: var(--green-2); }
  .ms-row .ladder-metric small { white-space: nowrap; }
  .ms-row .ladder-track i { background: linear-gradient(90deg, #cfc7b3, var(--ink-2)); }
  .ms-row.is-hit .ladder-track i { background: linear-gradient(90deg, #d8bd7c, var(--gold)); }
  .ms-row.is-target .ladder-track i { background: linear-gradient(90deg, #54a083, var(--green)); }

  .chain-card { margin-top: 12px; padding: 24px 24px 20px; }
  .detail-card { margin-top: 12px; padding: 24px 24px 18px; }
  .roll-flow {
    display: flex;
    align-items: stretch;
    gap: 5px;
    overflow-x: auto;
    margin-top: 16px;
    padding-bottom: 8px;
    scrollbar-width: thin;
    scrollbar-color: var(--line-strong) transparent;
  }
  .roll-flow::-webkit-scrollbar { height: 5px; }
  .roll-flow::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 99px; }
  .roll-card {
    flex: none;
    min-width: 118px;
    padding: 10px 11px 9px;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--paper-2);
  }
  .roll-card.start { border-color: rgba(15, 92, 71, .22); background: var(--green-wash); }
  .roll-card span { display: block; color: var(--muted); font-size: 8.5px; }
  .roll-card b {
    display: block;
    margin-top: 2px;
    color: var(--ink);
    font-family: var(--num);
    font-size: 14px;
    font-weight: 600;
    white-space: nowrap;
    letter-spacing: -.02em;
  }
  .roll-card.start b { color: var(--green); }
  .roll-card small {
    display: block;
    margin-top: 1px;
    color: var(--gold);
    font-family: var(--num);
    font-size: 9px;
    white-space: nowrap;
  }
  .roll-card.start small { color: var(--muted); }
  .roll-arrow {
    display: grid;
    flex: none;
    place-items: center;
    color: var(--line-strong);
    font-family: var(--num);
    font-size: 12px;
  }

  .detail-wrap {
    margin-top: 16px;
    max-height: 520px;
    overflow: auto;
    border: 1px solid var(--line);
    border-radius: 14px;
    scrollbar-width: thin;
    scrollbar-color: var(--line-strong) transparent;
  }
  .detail-wrap::-webkit-scrollbar { width: 6px; height: 6px; }
  .detail-wrap::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 99px; }
  table.detail {
    width: 100%;
    min-width: 690px;
    border-collapse: collapse;
    font-family: var(--num);
    font-size: 11px;
    font-variant-numeric: tabular-nums;
  }
  table.detail 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.detail thead th:first-child { text-align: left; }
  table.detail tbody td {
    padding: 7px 12px;
    border-bottom: 1px dashed var(--line);
    color: var(--ink-2);
    text-align: right;
    white-space: nowrap;
  }
  table.detail tbody td:first-child {
    color: var(--muted);
    font-family: var(--font);
    font-size: 10px;
    text-align: left;
  }
  table.detail tbody tr:last-child td { border-bottom: 0; }
  table.detail tbody tr.mstone td { background: rgba(242, 231, 201, .5); }
  table.detail tbody tr.mstone td:first-child { color: #7d5a18; font-weight: 650; }
  table.detail tbody tr.is-target td { background: var(--green-wash); }
  table.detail tbody tr.is-target td:first-child { color: var(--green); font-weight: 700; }
  table.detail tbody td.gain { color: var(--green); }
  table.detail tbody td.finish { color: var(--gold); font-weight: 600; }
  .ms-tag {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 5px;
    border-radius: 4px;
    background: var(--gold-soft);
    color: #7d5a18;
    font-family: var(--font);
    font-size: 8px;
    font-weight: 600;
    vertical-align: 1px;
  }
  .ms-row.is-target .ms-tag { background: rgba(15, 92, 71, .12); color: var(--green); }

  .calc-line b { color: var(--green); }

  @media (max-width: 1180px) {
    .ms-row {
      grid-template-columns: 34px minmax(120px, 1.1fr) repeat(4, minmax(62px, .72fr));
      gap: 9px;
    }
    .ms-row.is-target { padding-right: 12px; padding-left: 12px; }
    .roll-card { min-width: 110px; }
  }
  @media (max-width: 980px) {
    .chart-actions {
      flex-direction: row;
      flex-wrap: wrap;
      align-items: center;
      justify-content: space-between;
      width: 100%;
    }
    .chain-card, .detail-card { padding: 22px 20px 16px; }
  }
  @media (max-width: 760px) {
    .ms-row {
      grid-template-columns: 30px 1fr 1fr;
      gap: 8px 10px;
      padding: 11px 6px 10px;
    }
    .ms-row .ladder-no { grid-row: 1; }
    .ms-row .ladder-name { grid-column: 2 / 4; grid-row: 1; }
    .ms-row .ms-years { grid-column: 2; grid-row: 2; }
    .ms-row .ms-periods { grid-column: 3; grid-row: 2; }
    .ms-row .ms-amount { grid-column: 2; grid-row: 3; }
    .ms-row .ms-multiple { grid-column: 3; grid-row: 3; }
    .ms-row .ladder-track { grid-column: 1 / -1; grid-row: 4; }
    .ms-row.is-target { margin: 0 -6px; padding-right: 10px; padding-left: 10px; }
    .chain-card, .detail-card { padding: 18px 14px 14px; }
    .roll-card { min-width: 106px; padding: 9px 10px 8px; }
    .roll-card b { font-size: 12.5px; }
    .detail-wrap { max-height: 372px; }
    table.detail { min-width: 470px; }
    table.detail thead th:nth-child(2),
    table.detail thead th:nth-child(4),
    table.detail tbody td:nth-child(2),
    table.detail tbody td:nth-child(4) { display: none; }
    .compare-legend { gap: 4px; }
    .legend-chip { padding: 4px 7px; font-size: 9px; }
    .chart-xl .chart-wrap { margin-top: 10px; }
  }
  @media (max-width: 460px) {
    .chart-actions { gap: 6px; }
    .tabs .tab { padding: 6px 7px; }
    .mini-chip { padding: 4px 7px; font-size: 9px; }
    .roll-card { min-width: 98px; }
    .roll-card b { font-size: 11.5px; }
    .roll-card small { font-size: 8px; }
  }

</style>
</head>
<body>
<div class="page">
  <header class="topbar">
    <div class="brand">
      <span class="brand-mark">复</span>
      <div class="brand-copy">
        <strong>复利实验室</strong>
        <span>COMPOUND GROWTH 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>20% 的复利,<br><em>能把你带到哪。</em></h1>
      <p class="hero-lead">100 → 120 → 144 → 172.8……每一次都在更大的本金上再赚 20%。按年、按季、按月,或者只数每一次短线买卖——看清翻倍要多久、多少次能摸到 100 万、1000 万,甚至 1 个亿。</p>
      <div class="hero-points" aria-label="三个核心动作">
        <div><span>01</span><b>每次收益</b><small>短线和长线都算</small></div>
        <div><span>02</span><b>利滚利</b><small>收益也去赚钱</small></div>
        <div><span>03</span><b>目标里程</b><small>100万 → 1亿</small></div>
      </div>
      <p class="hero-foot">不预测涨跌,只计算:如果每次稳定赚 X%,复利会给你什么。</p>
    </div>

    <aside class="exposure-card">
      <div class="exposure-top">
        <span id="annualLabel">年化复利</span>
        <span class="risk-badge" id="rateBadge" data-level="mid">进取</span>
      </div>
      <div class="exposure-value"><span id="annualValue">20</span><span class="pct-sign">%</span></div>
      <p class="exposure-caption" id="annualCaption">每期 20%、每年 1 期:年化 20%,约 3.8 年翻一倍。</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>30</span><span>60</span><span>90</span><span>120%</span></div>
      </div>
      <div class="formula-box" id="rateFormula">每期 <b>20%</b> × 每年 <b>1 期</b> → 年化 <b>20%</b></div>
      <div class="fraction-badge" id="doubleBadge">翻倍需要 <b>3.8 年</b>(约 3.8 期)</div>
      <div class="exposure-facts">
        <div><span id="factRateLabel">每期收益</span><b id="factRate">20%</b></div>
        <div><span id="factFreqLabel">每年期数</span><b id="factFreq">1 期</b></div>
        <div><span>到 100 万</span><b id="factTarget">12.6 年</b></div>
      </div>
    </aside>
  </section>

  <main class="layout">
    <aside class="panel controls" aria-label="参数控制">
      <div class="panel-head">
        <div>
          <span class="eyebrow-sm">CONTROLS</span>
          <h2>调一调,看复利</h2>
        </div>
        <button class="reset-btn" id="resetBtn" type="button">重置</button>
      </div>

      <div class="preset-label" id="presetLabel">快速场景(年化)</div>
      <div class="preset-grid">
        <button class="preset" data-preset="steady" type="button" aria-pressed="false"><b>稳健 8%</b><span>红利 · 宽基</span></button>
        <button class="preset" data-preset="pro" type="button" aria-pressed="false"><b>进取 15%</b><span>长期好手</span></button>
        <button class="preset active" data-preset="master" type="button" aria-pressed="true"><b>高手 20%</b><span>年化天花板</span></button>
      </div>

      <div class="control">
        <div class="control-head">
          <label for="rateRange" id="rateLabel">每期收益率</label>
          <div class="loss-input-wrap gain">
            <input type="number" id="rateInput" min="0" max="100" step="0.5" value="20" inputmode="decimal" aria-label="每期收益率百分比">
            <span>%</span>
          </div>
        </div>
        <input type="range" id="rateRange" min="0" max="100" step="0.5" value="20">
        <div class="chip-row">
          <button class="chip" type="button" data-rate="10">10%</button>
          <button class="chip" type="button" data-rate="15">15%</button>
          <button class="chip" type="button" data-rate="20">20%</button>
          <button class="chip" type="button" data-rate="25">25%</button>
          <button class="chip" type="button" data-rate="30">30%</button>
        </div>
        <p class="hint" id="rateHint">每期固定赚这么多,收益不取出,下一期连本带利继续滚。</p>
      </div>

      <div class="control">
        <div class="control-head">
          <label>复利频率</label>
          <span class="mini-note" id="freqAnnual">年化 20%</span>
        </div>
        <div class="segmented segmented-4" role="group" aria-label="复利频率">
          <button class="active" type="button" data-freq="1" aria-pressed="true">每年</button>
          <button type="button" data-freq="4" aria-pressed="false">每季</button>
          <button type="button" data-freq="12" aria-pressed="false">每月</button>
          <button type="button" data-freq="0" aria-pressed="false">每次</button>
        </div>
        <p class="hint" id="freqHint">每年结一次:每期 20%,年化就是 20%。同一每期收益率,结算越频繁,年化越高。</p>
      </div>

      <div class="control">
        <div class="control-head">
          <label for="yearsRange" id="horizonLabel">计算年限</label>
          <output id="yearsValue" for="yearsRange">30 年</output>
        </div>
        <input type="range" id="yearsRange" min="5" max="60" step="1" value="30">
        <div class="chip-row" id="yearChips">
          <button class="chip" type="button" data-years="10">10年</button>
          <button class="chip" type="button" data-years="20">20年</button>
          <button class="chip" type="button" data-years="30">30年</button>
          <button class="chip" type="button" data-years="40">40年</button>
        </div>
        <div class="chip-row" id="tradeChips" hidden>
          <button class="chip" type="button" data-trades="30">30次</button>
          <button class="chip" type="button" data-trades="60">60次</button>
          <button class="chip" type="button" data-trades="120">120次</button>
          <button class="chip" type="button" data-trades="240">240次</button>
        </div>
        <p class="hint" id="horizonHint">只影响曲线和明细显示的长度;里程碑所需年数按实际复利单独计算。</p>
      </div>

      <div class="control">
        <div class="control-head">
          <label for="startInput">起始本金</label>
          <span class="mini-note">人民币</span>
        </div>
        <div class="capital-wrap">
          <span>&#165;</span>
          <input type="number" id="startInput" min="100" max="1000000000" step="1000" value="100000" inputmode="numeric" aria-label="起始本金">
        </div>
        <div class="chip-row">
          <button class="chip" type="button" data-start="10000">1万</button>
          <button class="chip" type="button" data-start="100000">10万</button>
          <button class="chip" type="button" data-start="1000000">100万</button>
        </div>
      </div>

      <div class="control">
        <div class="control-head">
          <label for="extraInput" id="extraLabel">每期追加</label>
          <span class="mini-note">可选</span>
        </div>
        <div class="capital-wrap">
          <span>&#165;</span>
          <input type="number" id="extraInput" min="0" max="10000000" step="1000" value="0" inputmode="numeric" aria-label="每期追加投入">
        </div>
        <div class="chip-row">
          <button class="chip" type="button" data-extra="0">不加</button>
          <button class="chip" type="button" data-extra="5000">5000</button>
          <button class="chip" type="button" data-extra="20000">2万</button>
        </div>
        <p class="hint" id="extraHint">按期末追加,追加额同样进入下一期复利;默认 0,只看纯利滚利。</p>
      </div>

      <div class="control">
        <div class="control-head">
          <label>目标金额</label>
          <span class="mini-note">高亮对比</span>
        </div>
        <div class="segmented segmented-3" role="group" aria-label="目标金额">
          <button class="active" type="button" data-target="1000000" aria-pressed="true">100万</button>
          <button type="button" data-target="10000000" aria-pressed="false">1000万</button>
          <button type="button" data-target="100000000" aria-pressed="false">1亿</button>
        </div>
        <p class="hint" id="targetHint">对比图、阶梯和明细会围绕 &#165;100 万高亮。</p>
      </div>
    </aside>

    <section class="results" aria-label="计算结果">
      <div class="kpi-grid">
        <article class="kpi kpi-target">
          <div class="kpi-head"><span>到 &#165;100 万</span><span class="kpi-code">1M</span></div>
          <div class="kpi-value green" id="kpi1m">12.6 年</div>
          <div class="kpi-meta"><b id="kpi1mMeta">第 13 期</b><span id="kpi1mSub">约 2039 年</span></div>
          <p class="kpi-foot" id="kpi1mFoot">×10 · 3.3 次翻倍</p>
        </article>
        <article class="kpi kpi-gold">
          <div class="kpi-head"><span>到 &#165;1000 万</span><span class="kpi-code">10M</span></div>
          <div class="kpi-value gold" id="kpi10m">25.3 年</div>
          <div class="kpi-meta"><b id="kpi10mMeta">第 26 期</b><span id="kpi10mSub">约 2052 年</span></div>
          <p class="kpi-foot" id="kpi10mFoot">×100 · 6.6 次翻倍</p>
        </article>
        <article class="kpi kpi-terra">
          <div class="kpi-head"><span>到 &#165;1 亿</span><span class="kpi-code">100M</span></div>
          <div class="kpi-value terra" id="kpi100m">37.9 年</div>
          <div class="kpi-meta"><b id="kpi100mMeta">第 38 期</b><span id="kpi100mSub">约 2064 年</span></div>
          <p class="kpi-foot" id="kpi100mFoot">×1000 · 10 次翻倍</p>
        </article>
        <article class="kpi kpi-end">
          <div class="kpi-head"><span id="kpiEndHead">第 30 年末</span><span class="kpi-code">NOW</span></div>
          <div class="kpi-value ink" id="kpiEnd">&#165;2374万</div>
          <div class="kpi-meta"><b id="kpiEndMeta">×237</b><span id="kpiEndSub">第 30 年</span></div>
          <p class="kpi-foot" id="kpiEndFoot">复利收益 &#165;2364万</p>
        </article>
      </div>

      <div class="strip-card">
        <div class="strip-copy">
          <span class="eyebrow-sm">WHO MADE IT</span>
          <h3>这笔钱,是谁赚出来的?</h3>
          <p id="stripText">第 30 年末,账户里 0.4% 是你投入的本金,99.6% 是复利收益。</p>
        </div>
        <div class="strip-visual">
          <div class="strip-labels">
            <span>累计投入 <b id="stripBase">&#165;100,000</b></span>
            <span>复利收益 <b id="stripGainAmount">&#165;23,637,631</b></span>
          </div>
          <div class="strip-bar" aria-hidden="true">
            <div class="strip-principal" id="stripPrincipalBar"></div>
            <div class="strip-gainbar"></div>
          </div>
          <div class="strip-amounts">
            <span>复利贡献 <b id="stripGainPct">99.6%</b></span>
            <span>期末账户 <b id="stripEnd">&#165;23,737,631</b></span>
          </div>
        </div>
      </div>

      <section class="card chart-card chart-xl" id="curveCard">
        <div class="card-head chart-head">
          <div class="chart-heading">
            <span class="eyebrow-sm">COMPOUND CURVE</span>
            <h2 id="curveTitle">复利曲线:从 &#165;10 万到 &#165;2374 万</h2>
            <p id="curveSubtitle">第 30 年 · 年化 20% · 纵轴对数刻度(每格 10 倍)</p>
          </div>
          <div class="chart-actions">
            <div class="tabs" id="curveTabs" role="tablist" aria-label="曲线内容">
              <button class="tab active" type="button" data-ctab="curve" role="tab" aria-selected="true">账户曲线</button>
              <button class="tab" type="button" data-ctab="yearly" id="curveYearlyTab" role="tab" aria-selected="false">每年收益</button>
            </div>
            <div class="scale-switch" id="scaleSwitch" role="group" aria-label="纵轴刻度">
              <button class="mini-chip active" type="button" data-scale="log" aria-pressed="true">对数</button>
              <button class="mini-chip" type="button" data-scale="linear" aria-pressed="false">线性</button>
            </div>
          </div>
        </div>
        <div class="chart-wrap" id="curveWrap">
          <svg id="curveSvg" role="img" aria-label="复利增长曲线图"></svg>
          <div class="chart-tooltip wide" id="curveTip" hidden></div>
        </div>
        <div class="chart-foot">
          <div class="legend" id="curveLegend">
            <span><i class="legend-line"></i>账户金额</span>
            <span><i class="legend-line target"></i>目标里程碑</span>
          </div>
          <p id="curveNote">复利前期慢、后期快;后期一年的收益,可能超过最初的全部本金。</p>
        </div>
      </section>

      <section class="card chart-card" id="compareCard">
        <div class="card-head chart-head">
          <div class="chart-heading">
            <span class="eyebrow-sm">RATE COMPARISON</span>
            <h2 id="compareTitle">年化 5%–30%,多久到 &#165;100 万</h2>
            <p id="compareSubtitle">同一笔 &#165;10 万本金 · 不含追加投入 · 纵轴对数刻度</p>
          </div>
          <div class="ladder-badge" id="compareBadge">目标 &#165;100 万</div>
        </div>
        <div class="chart-wrap" id="compareWrap">
          <svg id="compareSvg" role="img" aria-label="不同收益率的到达时间对比图"></svg>
        </div>
        <div class="compare-legend" id="compareLegend"></div>
        <div class="chart-foot">
          <p id="compareNote">曲线越陡,钱滚得越快;圆点是到达目标的那一年。</p>
        </div>
      </section>
    </section>
  </main>

  <section class="card ladder-card" id="ladderCard">
    <div class="card-head">
      <div>
        <span class="eyebrow-sm">MILESTONE LADDER</span>
        <h2 id="ladderTitle">到每一个里程碑,需要多久</h2>
        <p id="ladderSubtitle">每期 20% · 每年 1 期 · 从 &#165;100,000 出发。</p>
      </div>
      <div class="ladder-badge" id="ladderBadge">目标 &#165;100 万 · 12.6 年</div>
    </div>
    <div class="ms-ladder" id="milestoneLadder"></div>
    <div class="insight">
      <span class="insight-icon">i</span>
      <p id="ladderInsight">从 10 万到 100 万要 12.6 年;再一个 12.6 年就是 1000 万。复利不关心绝对值,只关心倍数。</p>
    </div>
  </section>

  <section class="card chain-card" id="chainCard">
    <div class="card-head">
      <div>
        <span class="eyebrow-sm">ROLL FORWARD</span>
        <h2 id="chainTitle">利滚利:前 12 期长什么样</h2>
        <p id="chainSubtitle">&#165;100,000 → &#165;120,000 → &#165;144,000 …… 每一期的收益,下一期也去赚钱。</p>
      </div>
      <div class="ladder-badge" id="chainBadge">每期 +20%</div>
    </div>
    <div class="roll-flow" id="rollFlow"></div>
  </section>

  <section class="card detail-card" id="detailCard">
    <div class="card-head">
      <div>
        <span class="eyebrow-sm" id="detailEyebrow">YEAR BY YEAR</span>
        <h2 id="detailTitle">逐年明细</h2>
        <p id="detailSubtitle">每年结一次 · 每期 20% · 金色行为里程碑,绿色行为目标达成那年。</p>
      </div>
      <div class="ladder-badge" id="detailBadge">30 年 · 30 期</div>
    </div>
    <div class="detail-wrap">
      <table class="detail">
        <thead>
          <tr>
            <th id="detailYearHead">年份</th>
            <th id="detailStartHead">年初</th>
            <th id="detailGainHead">本年收益</th>
            <th>追加</th>
            <th id="detailEndHead">年末</th>
            <th>累计倍数</th>
            <th>距目标</th>
          </tr>
        </thead>
        <tbody id="detailBody"></tbody>
      </table>
    </div>
  </section>

  <section class="method">
    <div class="method-card">
      <span class="eyebrow-sm">METHOD</span>
      <h2>这些数字怎么来的?</h2>
      <div class="calc-line" id="calcLine">本金 &#165;100,000 × (1 + 20%)^30 = &#165;23,737,631</div>
      <div class="formula-list">
        <div class="formula-row"><span>期末金额</span><code>本金 ×(1 + 每期收益率)^ 期数</code></div>
        <div class="formula-row"><span>到目标期数</span><code>ln(目标 ÷ 本金) ÷ ln(1 + 每期收益率)</code></div>
        <div class="formula-row"><span>翻倍时间</span><code>ln2 ÷ ln(1 + 每期收益率) ÷ 每年期数</code></div>
        <div class="formula-row"><span>年化收益</span><code>(1 + 每期收益率)^每年期数 &#8722; 1</code></div>
        <div class="formula-row"><span>追加投入</span><code>期末 = 上期 ×(1 + r)+ 每期追加</code></div>
      </div>
    </div>
    <div class="note-card">
      <h3>四个必须说清的前提</h3>
      <ol>
        <li><b>恒定收益率只是算术假设。</b>真实市场会回撤:先亏 50%,要涨 100% 才回本;波动会明显拖慢复利。</li>
        <li><b>20% 是极高标准。</b>长期年化 20% 已属全球顶尖;若要求每一次短线都赚 20%,对胜率和执行的要求更极端。把它当上限,而不是预期。</li>
        <li><b>税、费、通胀没有计入。</b>短线换手更多,佣金、印花税、滑点和通胀都会吃掉一部分账面收益。</li>
        <li><b>时间或次数,比收益率更难得。</b>20% 看似凶猛,10 万到 1 亿仍要近 38 年(或 38 次连续成功);先活得久,再谈复利。</li>
      </ol>
    </div>
  </section>

  <footer>
    <span>本工具仅用于复利数学演示,不构成投资建议。稳定高收益不可承诺,本金可能亏损。</span>
    <span class="footer-mark">COMPOUND GROWTH LAB · 2026</span>
  </footer>
</div>

<div class="mobile-readout" aria-live="polite" aria-label="实时结果">
  <div><span>到 100 万</span><b id="m1m">12.6 年</b></div>
  <div><span>到 1000 万</span><b id="m10m">25.3 年</b></div>
  <div><span id="mAnnualLabel">年化</span><b id="mAnnual">20%</b></div>
</div>

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

  const DEFAULTS = Object.freeze({
    start: 100000,
    rate: 20,
    freq: 1,
    years: 30,
    tradeMode: false,
    trades: 120,
    extra: 0,
    target: 1000000,
    ctab: "curve",
    scale: "log"
  });
  const MILESTONES = [200000, 500000, 1000000, 2000000, 5000000, 10000000, 20000000, 50000000, 100000000];
  const COMPARE_RATES = [5, 8, 10, 15, 20, 30];
  const RATE_COLORS = ["#93ab9d", "#63a78b", "#1f7a5e", "#b28733", "#0f5c47", "#bd583b"];
  const CAP_YEARS = 150;
  const CAP_TRADES = 1800;
  const NOW_YEAR = new Date().getFullYear();

  const state = { ...DEFAULTS };

  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 refs = {
    annualLabel: $("#annualLabel"),
    annualValue: $("#annualValue"),
    annualCaption: $("#annualCaption"),
    rateBadge: $("#rateBadge"),
    gaugeMarker: $("#gaugeMarker"),
    gaugeTrack: $(".gauge-track"),
    rateFormula: $("#rateFormula"),
    doubleBadge: $("#doubleBadge"),
    factRate: $("#factRate"),
    factRateLabel: $("#factRateLabel"),
    factFreq: $("#factFreq"),
    factFreqLabel: $("#factFreqLabel"),
    factTarget: $("#factTarget"),

    presetLabel: $("#presetLabel"),
    rateRange: $("#rateRange"),
    rateInput: $("#rateInput"),
    rateLabel: $("#rateLabel"),
    rateHint: $("#rateHint"),
    freqAnnual: $("#freqAnnual"),
    freqHint: $("#freqHint"),
    horizonLabel: $("#horizonLabel"),
    horizonHint: $("#horizonHint"),
    yearChips: $("#yearChips"),
    tradeChips: $("#tradeChips"),
    yearsRange: $("#yearsRange"),
    yearsValue: $("#yearsValue"),
    startInput: $("#startInput"),
    extraInput: $("#extraInput"),
    extraLabel: $("#extraLabel"),
    extraHint: $("#extraHint"),
    targetHint: $("#targetHint"),

    kpi1m: $("#kpi1m"),
    kpi1mMeta: $("#kpi1mMeta"),
    kpi1mSub: $("#kpi1mSub"),
    kpi1mFoot: $("#kpi1mFoot"),
    kpi10m: $("#kpi10m"),
    kpi10mMeta: $("#kpi10mMeta"),
    kpi10mSub: $("#kpi10mSub"),
    kpi10mFoot: $("#kpi10mFoot"),
    kpi100m: $("#kpi100m"),
    kpi100mMeta: $("#kpi100mMeta"),
    kpi100mSub: $("#kpi100mSub"),
    kpi100mFoot: $("#kpi100mFoot"),
    kpiEnd: $("#kpiEnd"),
    kpiEndHead: $("#kpiEndHead"),
    kpiEndMeta: $("#kpiEndMeta"),
    kpiEndSub: $("#kpiEndSub"),
    kpiEndFoot: $("#kpiEndFoot"),

    stripText: $("#stripText"),
    stripBase: $("#stripBase"),
    stripGainAmount: $("#stripGainAmount"),
    stripGainPct: $("#stripGainPct"),
    stripEnd: $("#stripEnd"),
    stripPrincipalBar: $("#stripPrincipalBar"),

    curveCard: $("#curveCard"),
    curveTitle: $("#curveTitle"),
    curveSubtitle: $("#curveSubtitle"),
    curveTabs: $("#curveTabs"),
    curveYearlyTab: $("#curveYearlyTab"),
    scaleSwitch: $("#scaleSwitch"),
    curveWrap: $("#curveWrap"),
    curveSvg: $("#curveSvg"),
    curveTip: $("#curveTip"),
    curveLegend: $("#curveLegend"),
    curveNote: $("#curveNote"),

    compareCard: $("#compareCard"),
    compareTitle: $("#compareTitle"),
    compareSubtitle: $("#compareSubtitle"),
    compareBadge: $("#compareBadge"),
    compareWrap: $("#compareWrap"),
    compareSvg: $("#compareSvg"),
    compareLegend: $("#compareLegend"),
    compareNote: $("#compareNote"),

    ladderTitle: $("#ladderTitle"),
    ladderBadge: $("#ladderBadge"),
    ladderSubtitle: $("#ladderSubtitle"),
    milestoneLadder: $("#milestoneLadder"),
    ladderInsight: $("#ladderInsight"),

    chainTitle: $("#chainTitle"),
    chainSubtitle: $("#chainSubtitle"),
    chainBadge: $("#chainBadge"),
    rollFlow: $("#rollFlow"),

    detailEyebrow: $("#detailEyebrow"),
    detailTitle: $("#detailTitle"),
    detailYearHead: $("#detailYearHead"),
    detailStartHead: $("#detailStartHead"),
    detailGainHead: $("#detailGainHead"),
    detailEndHead: $("#detailEndHead"),
    detailSubtitle: $("#detailSubtitle"),
    detailBadge: $("#detailBadge"),
    detailBody: $("#detailBody"),

    calcLine: $("#calcLine"),
    m1m: $("#m1m"),
    m10m: $("#m10m"),
    mAnnualLabel: $("#mAnnualLabel"),
    mAnnual: $("#mAnnual")
  };

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

  function trimNum(text) {
    return String(text).replace(/\.0+$/, "").replace(/(\.\d*[1-9])0+$/, "$1");
  }

  const SUPERSCRIPT = { "0": "&#8304;", "1": "&#185;", "2": "&#178;", "3": "&#179;", "4": "&#8308;", "5": "&#8309;", "6": "&#8310;", "7": "&#8311;", "8": "&#8312;", "9": "&#8313;" };

  function sciText(value) {
    const abs = Math.abs(value);
    let exponent = Math.floor(Math.log10(abs));
    let mantissa = value / Math.pow(10, exponent);
    if (Math.abs(mantissa) >= 9.9995) {
      mantissa /= 10;
      exponent += 1;
    }
    const digits = String(exponent).split("").map(digit => SUPERSCRIPT[digit]).join("");
    if (Math.abs(Math.abs(mantissa) - 1) < 0.0005) return "10" + digits;
    return trimNum(mantissa.toFixed(2)) + "×10" + digits;
  }

  function money(value) {
    if (!Number.isFinite(value)) return "—";
    return "&#165;" + nf0.format(Math.round(value));
  }

  function numShort(value) {
    if (!Number.isFinite(value)) return "—";
    const abs = Math.abs(value);
    if (abs >= 1e16) return sciText(value);
    if (abs >= 1e12) return trimNum((value / 1e12).toFixed(1)) + "万亿";
    if (abs >= 1e8) return trimNum((value / 1e8).toFixed(abs >= 1e9 ? 1 : 2)) + "亿";
    if (abs >= 1e4) return trimNum((value / 1e4).toFixed(abs >= 1e6 ? 0 : 1)) + "万";
    return nf0.format(Math.round(value));
  }

  function moneyShort(value) {
    if (!Number.isFinite(value)) return "—";
    return "&#165;" + numShort(value);
  }

  function pctText(value, digits = 1) {
    if (!Number.isFinite(value)) return "—";
    return trimNum((value * 100).toFixed(digits)) + "%";
  }

  const isTrade = () => state.tradeMode === true;
  const unitName = () => isTrade() ? "次" : "年";
  const countName = () => isTrade() ? "次" : "期";

  function yearsText(value) {
    if (value == null) return "做不到";
    if (value <= 0.0001) return "已达成";
    const unit = unitName();
    if (value >= 100) return nf0.format(Math.round(value)) + " " + unit;
    return trimNum(value.toFixed(1)) + " " + unit;
  }

  function periodsText(value) {
    if (value == null) return "—";
    if (value >= 100) return nf0.format(Math.ceil(value)) + " " + countName();
    return trimNum(value.toFixed(1)) + " " + countName();
  }

  function multipleText(value) {
    if (!Number.isFinite(value)) return "—";
    if (value >= 1e7) return sciText(value) + " 倍";
    if (value >= 10000) return "×" + nf0.format(value);
    if (value >= 100) return "×" + trimNum(value.toFixed(0));
    if (value >= 10) return "×" + trimNum(value.toFixed(1));
    return "×" + trimNum(value.toFixed(2));
  }

  function moneyChain(value) {
    return Math.abs(value) >= 1e8 ? moneyShort(value) : money(value);
  }

  function rateFraction() {
    return state.rate / 100;
  }

  function effectiveAnnual() {
    if (isTrade()) return null;
    return Math.pow(1 + rateFraction(), state.freq) - 1;
  }

  function buildBalances() {
    const r = rateFraction();
    const extra = state.extra;
    const capPeriods = isTrade() ? CAP_TRADES : CAP_YEARS * state.freq;
    const balances = new Array(capPeriods + 1);
    balances[0] = state.start;
    for (let k = 1; k <= capPeriods; k += 1) {
      balances[k] = balances[k - 1] * (1 + r) + extra;
    }
    return balances;
  }

  function crossAt(balances, target) {
    if (balances[0] >= target) return { periods: 0, years: 0 };
    const last = balances[balances.length - 1];
    if (last < target) return null;
    let lo = 1;
    let hi = balances.length - 1;
    while (lo < hi) {
      const mid = (lo + hi) >> 1;
      if (balances[mid] >= target) hi = mid;
      else lo = mid + 1;
    }
    const prev = balances[lo - 1];
    const cur = balances[lo];
    const frac = cur > prev && prev > 0
      ? clamp(Math.log(target / prev) / Math.log(cur / prev), 0, 1)
      : 1;
    const periods = lo - 1 + frac;
    const f = isTrade() ? 1 : state.freq;
    return { periods, years: periods / f };
  }

  function getData() {
    const trade = isTrade();
    const f = trade ? 1 : state.freq;
    const years = trade ? state.trades : state.years;
    const balances = buildBalances();
    const horizonPeriods = Math.round(years * f);
    const end = balances[horizonPeriods];
    const yearRows = [];

    for (let y = 1; y <= years; y += 1) {
      const startBal = balances[(y - 1) * f];
      const endBal = balances[y * f];
      yearRows.push({
        year: y,
        start: startBal,
        end: endBal,
        interest: endBal - startBal - state.extra * f,
        extra: state.extra * f,
        multiple: endBal / state.start
      });
    }

    const milestones = MILESTONES.map(amount => {
      const cross = crossAt(balances, amount);
      return {
        amount,
        cross,
        years: cross ? cross.years : null,
        periods: cross ? cross.periods : null,
        multiple: amount / state.start
      };
    });

    const targetCross = crossAt(balances, state.target);
    const totalIn = state.start + state.extra * horizonPeriods;
    const rate = rateFraction();

    return {
      f,
      trade,
      units: years,
      years,
      start: state.start,
      extra: state.extra,
      rate,
      balances,
      horizonPeriods,
      end,
      yearRows,
      milestones,
      targetCross,
      totalIn,
      gains: end - totalIn,
      effective: effectiveAnnual(),
      multiple: end / state.start,
      doubleYears: rate > 0 ? (Math.LN2 / Math.log(1 + rate)) / f : null
    };
  }

  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 setValueIfIdle(element, value) {
    if (!element) return;
    if (document.activeElement !== element) element.value = value;
  }

  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",
      "letter-spacing": options.spacing || "0"
    }, 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 logTicks(min, max) {
    const kStart = Math.floor(Math.log10(min));
    const kEnd = Math.ceil(Math.log10(max));
    const mults = (kEnd - kStart) >= 4 ? [1] : [1, 2, 5];
    const out = [];
    for (let k = kStart; k <= kEnd; k += 1) {
      mults.forEach(mult => {
        const value = mult * Math.pow(10, k);
        if (value >= min * 0.999 && value <= max * 1.001) out.push(value);
      });
    }
    if (out.length > 7) {
      const stride = Math.ceil(out.length / 6);
      const kept = [];
      for (let i = 0; i < out.length; i += stride) kept.push(out[i]);
      return kept;
    }
    return out;
  }

  function niceLinearScale(max, targetCount = 4) {
    if (!Number.isFinite(max) || max <= 0) return { top: 1, ticks: [0, 1] };
    const raw = max / targetCount;
    const magnitude = Math.pow(10, Math.floor(Math.log10(raw)));
    const normalized = raw / magnitude;
    const factor = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 2.5 ? 2.5 : normalized <= 5 ? 5 : 10;
    const step = factor * magnitude;
    const top = Math.ceil(max / step) * step;
    const ticks = [];
    for (let value = 0; value <= top + step * 1e-9; value += step) ticks.push(value);
    return { top, ticks };
  }

  function addTag(parent, geom, cx, cy, text, color, above = true) {
    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": 0.34,
      "stroke-width": 1
    }, group);
    svgText(group, x + 8, y + height / 2 + 0.5, text, {
      size: 10.5,
      weight: 650,
      fill: color
    });
    return group;
  }

  function renderHero(data) {
    const trade = isTrade();
    const gaugePct = trade ? state.rate : data.effective * 100;
    const millionCross = crossAt(data.balances, 1000000);

    refs.annualLabel.textContent = trade ? "每次复利" : "年化复利";
    refs.annualValue.textContent = gaugePct >= 100
      ? nf0.format(Math.round(gaugePct))
      : trimNum(gaugePct.toFixed(1));

    const position = clamp((gaugePct / 120) * 100, 1.5, 98.5);
    refs.gaugeTrack.style.setProperty("--risk-pos", position.toFixed(2) + "%");
    refs.gaugeMarker.style.left = position.toFixed(2) + "%";

    let level = "low";
    let label = "平衡";
    if (trade) {
      if (gaugePct >= 20) { level = "high"; label = "极激进"; }
      else if (gaugePct >= 10) { level = "high"; label = "激进"; }
      else if (gaugePct >= 5) { level = "mid"; label = "进取"; }
      else if (gaugePct >= 2) { level = "low"; label = "平衡"; }
      else { level = "low"; label = "稳健"; }
    } else {
      if (gaugePct >= 60) { level = "high"; label = "极激进"; }
      else if (gaugePct >= 30) { level = "high"; label = "激进"; }
      else if (gaugePct >= 12) { level = "mid"; label = "进取"; }
      else if (gaugePct >= 5) { level = "low"; label = "平衡"; }
      else { level = "low"; label = "稳健"; }
    }
    refs.rateBadge.dataset.level = level;
    refs.rateBadge.textContent = label;

    if (state.rate <= 0) {
      refs.annualCaption.textContent = trade
        ? "每次收益 0:做再多次,复利也不会启动。"
        : (state.extra > 0
          ? "收益率 0:只靠每期追加,资金线性增长,没有利滚利。"
          : "收益率 0:复利不会产生任何增长。把收益率调大一点试试。");
    } else if (trade) {
      refs.annualCaption.textContent = "每次 " + pctText(rateFraction(), 1) + ":约 " +
        yearsText(data.doubleYears) + "翻一倍;到 100 万需要 " +
        (millionCross ? yearsText(millionCross.years) : "1800 次以上") + "。";
    } else {
      refs.annualCaption.textContent = "每期 " + pctText(rateFraction(), 1) + "、每年 " + state.freq +
        " 期:年化 " + pctText(data.effective, 1) + ",约 " + yearsText(data.doubleYears) + "翻一倍。";
    }

    refs.rateFormula.innerHTML = trade
      ? "每次 <b>" + pctText(rateFraction(), 2) + "</b> × <b>" + state.trades +
        " 次</b> → 期末 <b>" + moneyShort(data.end) + "</b>"
      : "每期 <b>" + pctText(rateFraction(), 2) + "</b> × 每年 <b>" +
        state.freq + " 期</b> → 年化 <b>" + pctText(data.effective, 1) + "</b>";

    if (data.doubleYears == null) {
      refs.doubleBadge.innerHTML = "收益率 0:<b>不会翻倍</b>";
    } else if (trade) {
      refs.doubleBadge.innerHTML = "翻倍需要 <b>" + yearsText(data.doubleYears) +
        "</b>(第 " + Math.ceil(data.doubleYears) + " 次后)";
    } else {
      refs.doubleBadge.innerHTML = "翻倍需要 <b>" + yearsText(data.doubleYears) + "</b>(约 " +
        periodsText(data.doubleYears * state.freq) + ")";
    }

    refs.factRateLabel.textContent = trade ? "每次收益" : "每期收益";
    refs.factRate.textContent = pctText(rateFraction(), 1);
    refs.factFreqLabel.textContent = trade ? "计算次数" : "每年期数";
    refs.factFreq.textContent = trade ? state.trades + " 次" : state.freq + " 期";
    refs.factTarget.textContent = millionCross
      ? yearsText(millionCross.years)
      : (trade ? "1800 次+" : "150 年+");
  }

  function renderKpis(data) {
    const ratio = value => value / state.start;

    function fillMilestone(prefix, amount) {
      const cross = crossAt(data.balances, amount);
      const valueEl = refs[prefix];
      const text = cross ? yearsText(cross.years) : "做不到";
      const trade = isTrade();
      valueEl.textContent = text;
      valueEl.classList.toggle("is-long", text.length > 5);
      refs[prefix + "Meta"].textContent = cross
        ? "第 " + nf0.format(Math.ceil(cross.periods)) + " " + countName()
        : (trade ? "1800 次内" : "150 年内");
      refs[prefix + "Sub"].textContent = cross
        ? (trade ? "本金 " + multipleText(ratio(amount)) : "约 " + (NOW_YEAR + Math.ceil(cross.years)) + " 年")
        : "需要更高收益";
      if (!cross) {
        refs[prefix + "Foot"].textContent = trade ? "1800 次内无法达成" : "150 年内无法达成";
      } else if (ratio(amount) >= 1) {
        const doublings = Math.log2(ratio(amount));
        refs[prefix + "Foot"].textContent = (trade ? "" : multipleText(ratio(amount)) + " · ") +
          trimNum(doublings.toFixed(1)) + " 次翻倍" +
          (trade ? " · 每次 +" + pctText(rateFraction(), 1) : "");
      } else {
        refs[prefix + "Foot"].textContent = "起始本金已高于目标";
      }
    }

    fillMilestone("kpi1m", 1000000);
    fillMilestone("kpi10m", 10000000);
    fillMilestone("kpi100m", 100000000);

    refs.kpiEndHead.textContent = isTrade()
      ? "第 " + state.trades + " 次后"
      : "第 " + state.years + " 年末";
    refs.kpiEnd.textContent = moneyShort(data.end);
    refs.kpiEndMeta.textContent = multipleText(data.multiple);
    refs.kpiEndSub.textContent = isTrade()
      ? "每次 +" + pctText(rateFraction(), 1)
      : "第 " + state.years + " 年";
    refs.kpiEndFoot.textContent = "复利收益 " + moneyShort(Math.max(0, data.gains));
  }

  function renderStrip(data) {
    const base = data.totalIn;
    const gains = Math.max(0, data.gains);
    const basePct = data.end > 0 ? clamp(base / data.end, 0, 1) : 1;
    const gainPct = 1 - basePct;

    refs.stripBase.textContent = moneyShort(base);
    refs.stripGainAmount.textContent = moneyShort(gains);
    refs.stripPrincipalBar.style.width = (basePct * 100).toFixed(2) + "%";
    refs.stripGainPct.textContent = pctText(gainPct, 1);
    refs.stripEnd.textContent = moneyShort(data.end);
    refs.stripText.textContent = (isTrade()
      ? "第 " + state.trades + " 次后,账户 "
      : "第 " + state.years + " 年末,账户 ") + moneyShort(data.end) +
      ":本金占 " + pctText(basePct, 1) + ",复利收益占 " + pctText(gainPct, 1) + "。";
  }

  function renderCurveHeader(data) {
    const trade = isTrade();
    const unit = trade ? "次" : "年";
    const every = trade ? "每次" : "每期";
    refs.curveYearlyTab.textContent = trade ? "每次收益" : "每年收益";

    if (state.ctab === "yearly") {
      refs.curveTitle.textContent = trade ? "每次赚到的钱:复利收益" : "每年赚到的钱:复利收益";
      if (state.rate <= 0) {
        refs.curveSubtitle.textContent = state.extra > 0
          ? every + " 0%,但" + every + "追加 " + moneyShort(state.extra) + ":柱状图只显示追加投入。"
          : every + " 0%:没有复利收益,也没有加速段。";
        refs.curveNote.textContent = state.extra > 0
          ? "没有收益率时,资金只能靠追加变多,不会利滚利。"
          : "把" + every + "收益率调大,或设置" + every + "追加投入,才能看到资金增长。";
      } else {
        const first = data.yearRows[0] ? Math.max(0, data.yearRows[0].interest + data.yearRows[0].extra) : 0;
        const lastRow = data.yearRows[data.yearRows.length - 1];
        const last = lastRow ? Math.max(0, lastRow.interest + lastRow.extra) : 0;
        refs.curveSubtitle.textContent = "第 1 " + unit + " " + moneyShort(first) + " → 第 " + data.units +
          " " + unit + " " + moneyShort(last) + " · 同一个 " + pctText(rateFraction(), 1) + ",绝对金额越来越大";
        refs.curveNote.textContent = "复利的加速段在后期:越往后,一" + unit + "的收益越大。";
      }
      refs.curveLegend.innerHTML = '<span><i class="legend-line"></i>本' + unit + '复利收益</span>' +
        (state.extra > 0 ? '<span><i class="legend-line target"></i>追加投入</span>' : "");
      return;
    }

    refs.curveTitle.textContent = "复利曲线:从 " + moneyShort(state.start) + "到 " + moneyShort(data.end);
    refs.curveSubtitle.textContent = "第 " + data.units + " " + unit + " · " +
      (trade ? "每次 " + pctText(rateFraction(), 1) : "年化 " + pctText(data.effective, 1)) +
      " · 纵轴" + (state.scale === "log" ? "对数刻度(每格 10 倍)" : "线性刻度(前期会被压扁)");
    if (state.rate <= 0) {
      refs.curveNote.textContent = state.extra > 0
        ? "收益率为 0,增长只来自" + every + "追加;没有利滚利。"
        : "收益率为 0,曲线是一条水平线;提升收益率才能启动复利。";
    } else {
      refs.curveNote.textContent = state.scale === "log"
        ? "对数刻度下,10 倍、100 倍、1000 倍的差距被压缩成等距;曲线越平,滚得越慢。"
        : "线性刻度更直观,但后期金额会把前期压成一条平线——这正是复利的形状。";
    }
    refs.curveLegend.innerHTML = '<span><i class="legend-line"></i>账户金额</span>' +
      '<span><i class="legend-line target"></i>目标 ' + moneyShort(state.target) + '</span>';
  }

  function renderCurve(data) {
    const wrap = refs.curveWrap;
    const svg = refs.curveSvg;
    if (!wrap || !svg) return;

    const width = Math.max(300, Math.floor(wrap.clientWidth || 760));
    const narrow = width < 600;
    const isYearly = state.ctab === "yearly";
    const height = isYearly ? (narrow ? 330 : 410) : (narrow ? 400 : 540);
    const margin = {
      left: narrow ? 46 : 66,
      right: narrow ? 14 : 26,
      top: narrow ? 26 : 32,
      bottom: narrow ? 42 : 48
    };
    const iw = width - margin.left - margin.right;
    const ih = height - margin.top - margin.bottom;
    const years = data.units;
    const target = state.target;
    const unit = data.trade ? "次" : "年";

    svg.setAttribute("viewBox", "0 0 " + width + " " + height);
    svg.setAttribute("width", width);
    svg.setAttribute("height", height);
    svg.setAttribute("aria-label", isYearly
      ? (data.trade ? "每次复利收益柱状图" : "每年复利收益柱状图")
      : (data.trade ? "按交易次数计算的复利增长曲线图" : "复利增长曲线图"));
    svg.innerHTML = "";
    hideCurveTip();

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

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

    const step = years <= 12 ? 2 : years <= 30 ? 5 : years <= 60 ? 10 :
      years <= 120 ? 20 : years <= 300 ? 50 : 100;
    const xTicks = [];
    for (let t = 0; t <= years; t += step) xTicks.push(t);
    if (xTicks[xTicks.length - 1] !== years) xTicks.push(years);

    const drawXAxis = xPos => {
      xTicks.forEach(t => {
        const px = xPos(t);
        svgEl("line", {
          x1: px, x2: px, y1: margin.top, y2: margin.top + ih,
          stroke: "rgba(24,35,29,.045)", "stroke-width": 1
        }, plot);
        svgText(svg, px, margin.top + ih + 19, String(t), {
          anchor: "middle", size: narrow ? 9.5 : 10.5, fill: "#8a928b"
        });
      });
    };

    if (isYearly) {
      const interests = data.yearRows.map(row => Math.max(0, row.interest));
      const extras = data.yearRows.map(row => Math.max(0, row.extra));
      const totals = interests.map((value, index) => value + extras[index]);
      const maxGain = Math.max(0, ...totals);
      const slot = iw / years;

      if (maxGain <= 0) {
        svgText(svg, margin.left + iw / 2, margin.top + ih / 2, "收益率为 0:没有可展示的复利收益", {
          anchor: "middle", size: 11, fill: "#8a928b"
        });
        curveGeom = null;
        return;
      }

      const scale = niceLinearScale(maxGain, 4);
      const yPos = value => margin.top + ih - (value / scale.top) * ih;

      scale.ticks.forEach((value, index) => {
        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": index === 0 ? 1.2 : 1
        }, plot);
        svgText(svg, margin.left - 9, y + 0.5, numShort(value), {
          anchor: "end", size: narrow ? 9 : 10, fill: "#8a928b"
        });
      });

      const barWidth = Math.max(2, Math.min(34, slot * 0.6));
      data.yearRows.forEach((row, index) => {
        const x = margin.left + (index + 0.5) * slot - barWidth / 2;
        const hInterest = Math.max(0, (Math.max(0, row.interest) / scale.top) * ih);
        const hExtra = Math.max(0, (Math.max(0, row.extra) / scale.top) * ih);
        if (hInterest > 0.3) {
          svgEl("rect", {
            x, y: margin.top + ih - hInterest, width: barWidth, height: hInterest,
            rx: Math.min(2.5, barWidth / 3), fill: "#1f7a5e", opacity: 0.9
          }, plot);
        }
        if (hExtra > 0.3) {
          svgEl("rect", {
            x, y: margin.top + ih - hInterest - hExtra, width: barWidth, height: hExtra,
            rx: Math.min(2.5, barWidth / 3), fill: "#b28733", opacity: 0.9
          }, plot);
        }
      });

      drawXAxis(t => margin.left + (t / years) * iw);

      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, data.trade ? "本次收益" : "本年收益", {
        size: 10, fill: "#7a837c", weight: 600
      });
      svgText(svg, margin.left + iw / 2, height - 10, data.trade ? "交易次数" : "持有年数", {
        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,.4)", "stroke-dasharray": "4 4", "stroke-width": 1
      }, hoverGroup);
      const hoverDot = svgEl("circle", {
        r: 4.5, fill: "#1f7a5e", stroke: "#fffdf8", "stroke-width": 2
      }, hoverGroup);

      curveGeom = {
        mode: "yearly",
        width, height, margin, iw, ih, years, yPos, slot, data,
        hoverGroup, hoverLine, hoverDot
      };
      return;
    }

    const yearBalances = [];
    for (let y = 0; y <= years; y += 1) yearBalances.push(data.balances[y * data.f]);

    const maxValue = Math.max(data.end, target, data.start);
    let yMin;
    let yMax;
    let ticks;
    if (state.scale === "log") {
      yMin = Math.max(1, data.start * 0.8);
      yMax = Math.max(maxValue * 1.45, yMin * 10);
      ticks = logTicks(yMin, yMax);
    } else {
      const scale = niceLinearScale(maxValue, 4);
      yMin = 0;
      yMax = scale.top;
      ticks = scale.ticks;
    }

    const logSpan = Math.log10(yMax) - Math.log10(yMin);
    const yPos = state.scale === "log"
      ? value => margin.top + ((Math.log10(yMax) - Math.log10(Math.max(value, yMin))) / logSpan) * ih
      : value => margin.top + ih - ((value - yMin) / (yMax - yMin)) * ih;
    const xPos = t => margin.left + (t / years) * iw;

    ticks.forEach(value => {
      const y = yPos(value);
      if (!Number.isFinite(y)) return;
      const label = state.scale === "log" && yMax >= 1e13 ? sciText(value) : numShort(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, label, {
        anchor: "end", size: narrow ? 9 : 10, fill: "#8a928b"
      });
    });

    [1000000, 10000000, 100000000].forEach(value => {
      if (value === target) return;
      if (value < yMin || value > yMax) return;
      const y = yPos(value);
      svgEl("line", {
        x1: margin.left, x2: margin.left + iw, y1: y, y2: y,
        stroke: "rgba(178,135,51,.22)", "stroke-dasharray": "3 6", "stroke-width": 1
      }, plot);
    });

    if (target >= yMin && target <= yMax) {
      const y = yPos(target);
      svgEl("line", {
        x1: margin.left, x2: margin.left + iw, y1: y, y2: y,
        stroke: "rgba(178,135,51,.56)", "stroke-dasharray": "4 5", "stroke-width": 1.2
      }, plot);
      svgText(svg, margin.left + iw - 7, y - 8, "目标 " + moneyShort(target), {
        anchor: "end", size: 9.5, fill: "#a17b2d", weight: 600
      });
    }

    const points = yearBalances.map((value, year) => ({ x: xPos(year), y: yPos(value) }));
    const areaPath = points
      .map((point, index) => (index === 0 ? "M" : "L") + point.x.toFixed(2) + " " + point.y.toFixed(2))
      .join(" ") +
      " L" + xPos(years).toFixed(2) + " " + (margin.top + ih).toFixed(2) +
      " L" + xPos(0).toFixed(2) + " " + (margin.top + ih).toFixed(2) + " Z";

    svgEl("path", { d: areaPath, fill: "url(#curveFill)", stroke: "none" }, plot);
    svgEl("path", {
      d: linePath(points),
      fill: "none",
      stroke: "#0f5c47",
      "stroke-width": 2.6,
      "stroke-linecap": "round",
      "stroke-linejoin": "round"
    }, plot);

    [1000000, 10000000, 100000000].forEach(value => {
      const cross = crossAt(data.balances, value);
      if (!cross || cross.years > years || value < yMin || value > yMax) return;
      svgEl("circle", {
        cx: xPos(cross.years),
        cy: yPos(value),
        r: value === target ? 4.6 : 3.4,
        fill: value === target ? "#b28733" : "#fffdf8",
        stroke: value === target ? "#fffdf8" : "#b28733",
        "stroke-width": 2
      }, plot);
    });

    const targetCross = crossAt(data.balances, target);
    const geom = { margin, iw, ih, width, height };

    if (targetCross && targetCross.years <= years && target >= yMin && target <= yMax) {
      const cx = xPos(targetCross.years);
      svgEl("line", {
        x1: cx, x2: cx, y1: yPos(target), y2: margin.top + ih,
        stroke: "rgba(178,135,51,.34)", "stroke-dasharray": "3 5", "stroke-width": 1
      }, plot);
      addTag(svg, geom, cx, yPos(target), yearsText(targetCross.years) + " · " + moneyShort(target), "#a17b2d", false);
    }

    drawXAxis(xPos);

    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);

    svgEl("circle", {
      cx: xPos(0), cy: yPos(data.start), r: 4,
      fill: "#0f5c47", stroke: "#fffdf8", "stroke-width": 2
    }, plot);

    svgText(svg, margin.left, margin.top - 12, state.scale === "log" ? "账户金额 · 对数" : "账户金额", {
      size: 10, fill: "#7a837c", weight: 600
    });
    svgText(svg, margin.left + iw / 2, height - 10, data.trade ? "交易次数" : "持有年数", {
      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: "#0f5c47", stroke: "#fffdf8", "stroke-width": 2
    }, hoverGroup);

    curveGeom = {
      mode: "curve",
      width, height, margin, iw, ih, years, xPos, yPos, data,
      hoverGroup, hoverLine, hoverDot
    };
  }

  function hideCurveTip() {
    if (curveGeom) curveGeom.hoverGroup.setAttribute("opacity", "0");
    refs.curveTip.hidden = true;
  }

  function onCurveMove(event) {
    const geom = curveGeom;
    if (!geom) return;
    const rect = refs.curveSvg.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) {
      hideCurveTip();
      return;
    }

    const data = geom.data;
    const f = data.f;
    let year;
    let px;
    let py;

    if (geom.mode === "curve") {
      year = clamp(Math.round(((localX - geom.margin.left) / geom.iw) * geom.years), 0, geom.years);
      px = geom.xPos(year);
      py = geom.yPos(data.balances[year * f]);
      geom.hoverDot.setAttribute("visibility", "visible");
      geom.hoverDot.setAttribute("cx", px);
      geom.hoverDot.setAttribute("cy", py);
    } else {
      year = clamp(Math.floor((localX - geom.margin.left) / geom.slot) + 1, 1, geom.years);
      px = geom.margin.left + (year - 0.5) * geom.slot;
      const row = data.yearRows[year - 1];
      py = geom.yPos(Math.max(0, row.interest + row.extra));
      geom.hoverDot.setAttribute("visibility", "hidden");
    }

    geom.hoverGroup.setAttribute("opacity", "1");
    geom.hoverLine.setAttribute("x1", px);
    geom.hoverLine.setAttribute("x2", px);

    const balance = data.balances[year * f];
    const previous = year > 0 ? data.balances[(year - 1) * f] : data.start;
    const interest = Math.max(0, balance - previous - state.extra * f);
    const invested = state.start + state.extra * year * f;
    const distance = state.target - balance;

    const unit = data.trade ? "次" : "年";
    refs.curveTip.innerHTML =
      '<div class="tt-head"><span>第 ' + year + ' ' + unit + '</span><b>' + moneyShort(balance) + '</b></div>' +
      '<div class="tt-row"><i class="tt-dot current"></i><span>本' + unit + '复利收益</span><b>+' + moneyShort(interest) + '</b></div>' +
      '<div class="tt-row"><i class="tt-dot base"></i><span>累计投入</span><b>' + moneyShort(invested) + '</b></div>' +
      '<div class="tt-sep"></div>' +
      '<div class="tt-row plain"><span>累计倍数</span><b>' + multipleText(balance / state.start) + '</b></div>' +
      '<div class="tt-row plain"><span>距目标 ' + moneyShort(state.target) + '</span><b>' +
        (distance > 0 ? moneyShort(distance) : "已达成") + '</b></div>';
    refs.curveTip.hidden = false;

    const tipWidth = refs.curveTip.offsetWidth || 224;
    const tipHeight = refs.curveTip.offsetHeight || 120;
    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.curveTip.style.left = left + "px";
    refs.curveTip.style.top = top + "px";
  }

  function renderCompareHeader(data) {
    const trade = isTrade();
    refs.compareBadge.textContent = "目标 " + moneyShort(state.target);
    refs.compareTitle.textContent = trade
      ? "每次 5%–30%,多少次到 " + moneyShort(state.target)
      : "年化 5%–30%,多久到 " + moneyShort(state.target);
    refs.compareSubtitle.textContent = trade
      ? "同一笔 " + moneyShort(state.start) + " 本金 · 只数交易次数、不看时间 · 纵轴对数刻度"
      : "同一笔 " + moneyShort(state.start) + " 本金 · 对比曲线不含追加 · 纵轴对数刻度";
  }

  function renderCompare(data) {
    const wrap = refs.compareWrap;
    const svg = refs.compareSvg;
    if (!wrap || !svg) return;

    const width = Math.max(300, Math.floor(wrap.clientWidth || 760));
    const narrow = width < 600;
    const height = narrow ? 372 : 440;
    const margin = {
      left: narrow ? 46 : 66,
      right: narrow ? 16 : 30,
      top: 30,
      bottom: narrow ? 42 : 48
    };
    const iw = width - margin.left - margin.right;
    const ih = height - margin.top - margin.bottom;
    const start = state.start;
    const target = state.target;
    const trade = isTrade();
    const ratePrefix = trade ? "每次" : "年化";
    const unit = trade ? "次" : "年";
    const cap = trade ? CAP_TRADES : CAP_YEARS;

    const rates = COMPARE_RATES.map((rate, index) => ({
      rate,
      color: RATE_COLORS[index],
      cross: start >= target ? 0 : (rate > 0 ? Math.log(target / start) / Math.log(1 + rate / 100) : null)
    }));

    const validCross = rates.map(item => item.cross).filter(value => value != null && value <= cap);
    let maxX = 40;
    if (validCross.length) maxX = Math.ceil(Math.max(...validCross)) + 1;
    if (data.targetCross && data.targetCross.years <= cap) {
      maxX = Math.max(maxX, Math.ceil(data.targetCross.years) + 1);
    }
    maxX = clamp(maxX, 8, cap);

    const yMin = Math.max(1, start * 0.8);
    const yMax = Math.max(target * 2.4, start * 2.2);
    const logSpan = Math.log10(yMax) - Math.log10(yMin);
    const yPos = value => margin.top + ((Math.log10(yMax) - Math.log10(Math.max(value, yMin))) / logSpan) * ih;
    const xPos = t => margin.left + (t / maxX) * iw;

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

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

    logTicks(yMin, yMax).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, numShort(value), {
        anchor: "end", size: narrow ? 9 : 10, fill: "#8a928b"
      });
    });

    const xStep = maxX <= 12 ? 2 : maxX <= 30 ? 5 : maxX <= 60 ? 10 :
      maxX <= 120 ? 20 : maxX <= 300 ? 50 : 100;
    const xValues = [];
    for (let t = 0; t <= maxX; t += xStep) xValues.push(t);
    if (xValues[xValues.length - 1] !== maxX && maxX - xValues[xValues.length - 1] >= xStep * 0.6) {
      xValues.push(maxX);
    }
    xValues.forEach(t => {
      const px = xPos(t);
      svgEl("line", {
        x1: px, x2: px, y1: margin.top, y2: margin.top + ih,
        stroke: "rgba(24,35,29,.045)", "stroke-width": 1
      }, plot);
      svgText(svg, px, margin.top + ih + 19, String(t), {
        anchor: "middle", size: narrow ? 9 : 10, fill: "#8a928b"
      });
    });

    const targetY = yPos(target);
    svgEl("line", {
      x1: margin.left, x2: margin.left + iw, y1: targetY, y2: targetY,
      stroke: "rgba(178,135,51,.55)", "stroke-dasharray": "4 5", "stroke-width": 1.2
    }, plot);
    svgText(svg, margin.left + iw * 0.93, targetY - 8, "目标 " + moneyShort(target), {
      anchor: "end", size: 9.5, fill: "#a17b2d", weight: 600
    });

    rates.forEach((item, index) => {
      const points = [];
      for (let t = 0; t <= maxX; t += 1) {
        points.push({ x: xPos(t), y: yPos(start * Math.pow(1 + item.rate / 100, t)) });
      }
      svgEl("path", {
        d: linePath(points),
        fill: "none",
        stroke: item.color,
        "stroke-width": 1.8,
        "stroke-linecap": "round",
        opacity: 0.9
      }, plot);

      if (item.cross != null && item.cross <= maxX) {
        const cx = xPos(item.cross);
        const group = svgEl("g", {}, plot);
        svgEl("circle", {
          cx, cy: targetY, r: 4,
          fill: item.color, stroke: "#fffdf8", "stroke-width": 1.8
        }, group);
        const title = svgEl("title", {}, group);
        title.textContent = ratePrefix + " " + item.rate + "%:" + yearsText(item.cross) + " 到 " + moneyShort(target);
        const down = index % 2 === 0;
        svgText(plot, cx, targetY + (down ? 16 : -11), trimNum(item.cross.toFixed(1)) + unit, {
          anchor: cx > margin.left + iw - 30 ? "end" : "middle",
          size: 9.5,
          weight: 650,
          fill: item.color
        });
      }
    });

    const userRatePct = trade ? state.rate : data.effective * 100;
    const showUser = state.extra > 0 ||
      !rates.some(item => Math.abs(item.rate - userRatePct) < 0.15);

    if (showUser) {
      const points = [];
      for (let t = 0; t <= maxX; t += 1) {
        const index = Math.min(t * data.f, data.balances.length - 1);
        points.push({ x: xPos(t), y: yPos(data.balances[index]) });
      }
      svgEl("path", {
        d: linePath(points),
        fill: "none",
        stroke: "#18231d",
        "stroke-width": 3,
        "stroke-linecap": "round"
      }, plot);

      if (data.targetCross && data.targetCross.years <= maxX) {
        const cx = xPos(data.targetCross.years);
        const group = svgEl("g", {}, plot);
        svgEl("circle", {
          cx, cy: targetY, r: 5.5,
          fill: "#18231d", stroke: "#fffdf8", "stroke-width": 2
        }, group);
        const title = svgEl("title", {}, group);
        title.textContent = "你的方案:" + yearsText(data.targetCross.years) + " 到 " + moneyShort(target);
      }
    }

    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, "账户金额 · 对数", {
      size: 10, fill: "#7a837c", weight: 600
    });
    svgText(svg, margin.left + iw / 2, height - 10,
      trade ? "到达目标所需交易次数" : "到达目标所需年数", {
        anchor: "middle", size: 10, fill: "#7a837c"
      });

    const fallback = trade ? "1800 次+" : "150 年+";
    const chips = rates.map(item => {
      const span = item.cross == null || item.cross > cap ? fallback : yearsText(item.cross);
      return '<span class="legend-chip"><i style="background:' + item.color + '"></i>' + ratePrefix + ' ' +
        item.rate + '%<b>' + span + '</b></span>';
    });
    if (showUser) {
      chips.push('<span class="legend-chip you"><i style="background:#18231d"></i>你的方案 · ' +
        ratePrefix + ' ' + pctText(trade ? rateFraction() : data.effective, 1) + '<b>' +
        (data.targetCross ? yearsText(data.targetCross.years) : fallback) + '</b></span>');
    }
    refs.compareLegend.innerHTML = chips.join("");

    const rate20 = rates.find(item => item.rate === 20);
    const rate5 = rates.find(item => item.rate === 5);
    refs.compareNote.textContent = ratePrefix + " 20% 到 " + moneyShort(target) + " 需要 " +
      (rate20 && rate20.cross != null && rate20.cross <= cap ? yearsText(rate20.cross) : "超出显示范围") +
      ";" + ratePrefix + " 5% 需要 " +
      (rate5 && rate5.cross != null && rate5.cross <= cap ? yearsText(rate5.cross) : "超出显示范围") +
      "。收益率只提高一点," + (trade ? "交易次数" : "到达时间") + "会明显缩短。";
  }

  function renderLadder(data) {
    const trade = isTrade();
    const startLog = Math.log10(Math.max(1, state.start));
    const endLog = Math.log10(100000000);
    const span = endLog - startLog;

    refs.ladderTitle.textContent = trade
      ? "到每一个里程碑,需要多少次"
      : "到每一个里程碑,需要多久";

    refs.milestoneLadder.innerHTML = data.milestones.map((item, index) => {
      const isTarget = item.amount === state.target;
      const isHit = item.years != null && item.years <= data.units;
      const ratio = item.amount / state.start;
      const doublings = ratio >= 1 ? trimNum(Math.log2(ratio).toFixed(1)) + " 次翻倍" : "起点已高于此";
      const barWidth = state.start >= 100000000
        ? 100
        : clamp(((Math.log10(item.amount) - startLog) / span) * 100, 3, 100);

      const classes = ["ms-row"];
      if (isTarget) classes.push("is-target");
      else if (isHit) classes.push("is-hit");

      const firstMetric = trade
        ? '<div class="ladder-metric ms-years"><span>所需次数</span><b class="' +
            (item.years != null ? "good" : "") + '">' + yearsText(item.years) + '</b><small>' +
            (item.periods != null ? "第 " + nf0.format(Math.ceil(item.periods)) + " 次后" : "1800 次内") + '</small></div>'
        : '<div class="ladder-metric ms-years"><span>所需年数</span><b class="' +
            (item.years != null ? "good" : "") + '">' + yearsText(item.years) + '</b><small>' +
            (item.periods != null ? "第 " + nf0.format(Math.ceil(item.periods)) + " 期后" : "150 年内") + '</small></div>';

      const secondMetric = trade
        ? '<div class="ladder-metric ms-periods"><span>每次收益</span><b>' +
            pctText(rateFraction(), 2) + '</b></div>'
        : '<div class="ladder-metric ms-periods"><span>所需期数</span><b>' + periodsText(item.periods) + '</b></div>';

      return '<div class="' + classes.join(" ") + '">' +
        '<div class="ladder-no">' + String(index + 1).padStart(2, "0") + '</div>' +
        '<div class="ladder-name"><strong>' + moneyShort(item.amount) + '</strong><small>' +
          multipleText(ratio) + " · " + doublings + '</small></div>' +
        firstMetric +
        secondMetric +
        '<div class="ladder-metric ms-amount"><span>达成时账户</span><b>' + money(item.amount) + '</b></div>' +
        '<div class="ladder-metric ms-multiple"><span>相对本金</span><b>' + multipleText(ratio) + '</b></div>' +
        '<div class="ladder-track"><i style="width:' + barWidth.toFixed(2) + '%"></i></div>' +
        '</div>';
    }).join("");

    const targetCross = data.targetCross;
    refs.ladderBadge.textContent = targetCross
      ? "目标 " + moneyShort(state.target) + " · " + yearsText(targetCross.years)
      : "目标 " + moneyShort(state.target) + " · " + (trade ? "1800 次+" : "150 年+");
    refs.ladderSubtitle.textContent = trade
      ? "每次 " + pctText(rateFraction(), 2) + " · 只数交易次数 · 从 " + moneyShort(state.start) +
        " 出发;绿色为当前目标。"
      : "每期 " + pctText(rateFraction(), 2) + " · 每年 " + state.freq +
        " 期 · 从 " + moneyShort(state.start) + " 出发;绿色为当前目标。";

    if (!targetCross) {
      refs.ladderInsight.innerHTML = "按当前参数,<b>" + (trade ? "1800 次内" : "150 年内") + "到不了</b> " +
        moneyShort(state.target) + "。要么提高" + (trade ? "每次" : "每期") +
        "收益率,要么把" + (trade ? "交易次数" : "时间") + "拉长;复利最怕的不是收益低,而是中断。";
      return;
    }

    const targetYears = targetCross.years;
    const nextTarget = state.target * 10;
    const nextCross = crossAt(data.balances, nextTarget);
    const gainShare = data.end > 0 ? Math.max(0, data.gains) / data.end : 0;

    refs.ladderInsight.innerHTML = "从 " + moneyShort(state.start) + " 到 " + moneyShort(state.target) +
      " 需要 <b>" + yearsText(targetYears) + "</b>(约第 " + nf0.format(Math.ceil(targetCross.periods)) +
      " " + countName() + ")。再走 <b>" + yearsText(targetYears) + "</b>,同样的倍数就会把你送到 " +
      (nextCross ? moneyShort(nextTarget) : moneyShort(nextTarget) + " 附近") +
      "。第 " + data.units + " " + (trade ? "次后" : "年末") + ",账户里 <b>" + pctText(gainShare, 1) +
      "</b> 是复利赚出来的。";
  }

  function renderChain(data) {
    const trade = isTrade();
    const n = Math.min(12, data.horizonPeriods);
    const unit = trade ? "次" : "期";
    const parts = [];
    refs.chainTitle.textContent = trade ? "利滚利:前 12 次长什么样" : "利滚利:前 12 期长什么样";
    parts.push('<div class="roll-card start"><span>起点</span><b>' + moneyChain(state.start) +
      '</b><small>本金</small></div>');

    for (let k = 1; k <= n; k += 1) {
      const balance = data.balances[k];
      const previous = data.balances[k - 1];
      const interest = previous * rateFraction();
      parts.push('<span class="roll-arrow">→</span>');
      parts.push('<div class="roll-card"><span>第 ' + k + ' ' + unit + '</span><b>' + moneyChain(balance) +
        '</b><small>+' + moneyChain(interest) + ' 复利' +
        (state.extra > 0 ? ' · +' + moneyChain(state.extra) + ' 追加' : '') + '</small></div>');
    }

    refs.rollFlow.innerHTML = parts.join("");
    refs.chainBadge.textContent = (trade ? "每次 +" : "每期 +") + pctText(rateFraction(), 2);
    const first = data.balances[1];
    const second = data.horizonPeriods >= 2 ? data.balances[2] : null;
    refs.chainSubtitle.textContent = moneyChain(state.start) + " → " + moneyChain(first) +
      (second != null ? " → " + moneyChain(second) : "") +
      " …… 每一" + (trade ? "次" : "期") + "的收益,下一" + (trade ? "次" : "期") +
      "也去赚钱;可横向滑动。";
  }

  function renderDetail(data) {
    const trade = isTrade();
    const unit = trade ? "次" : "年";
    refs.detailEyebrow.textContent = trade ? "TRADE BY TRADE" : "YEAR BY YEAR";
    refs.detailTitle.textContent = trade ? "逐次明细" : "逐年明细";
    refs.detailYearHead.textContent = trade ? "次数" : "年份";
    refs.detailStartHead.textContent = trade ? "期初" : "年初";
    refs.detailGainHead.textContent = trade ? "本次收益" : "本年收益";
    refs.detailEndHead.textContent = trade ? "期末" : "年末";

    const milestoneYears = new Map();
    data.milestones.forEach(item => {
      if (item.years == null || item.years > data.units) return;
      const year = Math.max(1, Math.ceil(item.years));
      if (!milestoneYears.has(year)) milestoneYears.set(year, []);
      milestoneYears.get(year).push(item.amount);
    });

    const targetYear = data.targetCross && data.targetCross.years <= data.units
      ? Math.max(1, Math.ceil(data.targetCross.years))
      : null;

    refs.detailBody.innerHTML = data.yearRows.map(row => {
      const tags = (milestoneYears.get(row.year) || [])
        .map(amount => '<span class="ms-tag">' + numShort(amount) + '</span>')
        .join("");
      const distance = state.target - row.end;
      const classes = [];
      if (row.year === targetYear) classes.push("is-target");
      else if (milestoneYears.has(row.year)) classes.push("mstone");

      return '<tr class="' + classes.join(" ") + '">' +
        '<td>第 ' + row.year + ' ' + unit + tags + '</td>' +
        '<td>' + moneyShort(row.start) + '</td>' +
        '<td class="gain">+' + moneyShort(row.interest) + '</td>' +
        '<td>' + (row.extra > 0 ? moneyShort(row.extra) : "—") + '</td>' +
        '<td>' + moneyShort(row.end) + '</td>' +
        '<td>' + multipleText(row.multiple) + '</td>' +
        '<td class="' + (distance <= 0 ? "finish" : "") + '">' +
          (distance > 0 ? moneyShort(distance) : "已达成") + '</td>' +
        '</tr>';
    }).join("");

    refs.detailSubtitle.textContent = trade
      ? "每次 " + pctText(rateFraction(), 2) + " · 只数交易次数 · 金色行为里程碑,绿色行为目标达成那次;表格可横向滑动。"
      : "每期 " + pctText(rateFraction(), 2) + " · 每年 " + state.freq +
        " 期 · 金色行为里程碑,绿色行为目标达成那年;表格可横向滑动。";
    refs.detailBadge.textContent = trade
      ? state.trades + " 次"
      : state.years + " 年 · " + state.years * state.freq + " 期";
  }

  function renderCalc(data) {
    const trade = isTrade();
    let text = "本金 " + money(state.start) + " × (1 + " + pctText(rateFraction(), 2) + ")^" +
      data.horizonPeriods + " = <b>" + money(data.end) + "</b>";
    if (trade) text += "(共 " + state.trades + " 次)";
    if (state.extra > 0) {
      text += ";另" + (trade ? "每次" : "每期") + "追加 " + money(state.extra) + ",累计 " +
        money(state.extra * data.horizonPeriods);
    }
    refs.calcLine.innerHTML = text;
  }

  function renderMobile(data) {
    const oneM = crossAt(data.balances, 1000000);
    const tenM = crossAt(data.balances, 10000000);
    const trade = isTrade();
    refs.m1m.textContent = oneM ? yearsText(oneM.years) : "做不到";
    refs.m10m.textContent = tenM ? yearsText(tenM.years) : "做不到";
    refs.mAnnualLabel.textContent = trade ? "每次" : "年化";
    refs.mAnnual.textContent = trade
      ? pctText(rateFraction(), 1)
      : pctText(data.effective, 1);
  }

  function syncControls(data) {
    const trade = isTrade();

    refs.presetLabel.textContent = trade ? "快速场景(每次收益)" : "快速场景(年化)";
    const presetCopy = trade
      ? {
          steady: { title: "稳健 3%", sub: "每次小赚" },
          pro: { title: "进取 5%", sub: "短线好手" },
          master: { title: "高手 10%", sub: "极高胜率" }
        }
      : {
          steady: { title: "稳健 8%", sub: "红利 · 宽基" },
          pro: { title: "进取 15%", sub: "长期好手" },
          master: { title: "高手 20%", sub: "年化天花板" }
        };
    $$("[data-preset]").forEach(button => {
      const copy = presetCopy[button.dataset.preset];
      if (!copy) return;
      const title = button.querySelector("b");
      const sub = button.querySelector("span");
      if (title) title.textContent = copy.title;
      if (sub) sub.textContent = copy.sub;
    });

    refs.rateRange.value = state.rate;
    setRangeFill(refs.rateRange);
    setValueIfIdle(refs.rateInput, state.rate);
    refs.rateInput.setAttribute("aria-label", trade ? "每次收益率百分比" : "每期收益率百分比");
    refs.rateLabel.textContent = trade ? "每次收益率" : "每期收益率";
    refs.rateHint.textContent = trade
      ? "每次短线买卖固定赚这么多,收益不取出,下一次连本带利继续滚(按全仓复利计算)。"
      : "每期固定赚这么多,收益不取出,下一期连本带利继续滚。";

    const chipRates = trade ? [2, 3, 5, 10, 20] : [10, 15, 20, 25, 30];
    $$("[data-rate]").forEach((button, index) => {
      const value = chipRates[index];
      if (value == null) return;
      button.dataset.rate = String(value);
      button.textContent = value + "%";
    });

    refs.freqAnnual.textContent = trade
      ? "每次 " + pctText(rateFraction(), 2)
      : "年化 " + pctText(data.effective, 1);

    refs.yearChips.hidden = trade;
    refs.tradeChips.hidden = !trade;
    if (trade) {
      refs.yearsRange.min = "10";
      refs.yearsRange.max = "600";
      refs.yearsRange.value = state.trades;
      refs.yearsValue.textContent = state.trades + " 次";
      refs.horizonLabel.textContent = "计算次数";
      refs.horizonHint.textContent = "只影响曲线和明细显示的长度;里程碑所需次数按实际复利单独计算。每次 = 一次买卖。";
    } else {
      refs.yearsRange.min = "5";
      refs.yearsRange.max = "60";
      refs.yearsRange.value = state.years;
      refs.yearsValue.textContent = state.years + " 年";
      refs.horizonLabel.textContent = "计算年限";
      refs.horizonHint.textContent = "只影响曲线和明细显示的长度;里程碑所需年数按实际复利单独计算。";
    }
    setRangeFill(refs.yearsRange);

    setValueIfIdle(refs.startInput, state.start);
    setValueIfIdle(refs.extraInput, state.extra);
    refs.extraLabel.textContent = trade ? "每次追加" : "每期追加";
    refs.extraInput.setAttribute("aria-label", trade ? "每次追加投入" : "每期追加投入");
    refs.extraHint.textContent = trade
      ? "按每次交易期末追加,追加额同样进入下一次复利;默认 0,只看纯利滚利。"
      : "按期末追加,追加额同样进入下一期复利;默认 0,只看纯利滚利。";

    if (state.rate <= 0) {
      refs.freqHint.textContent = trade
        ? "每次收益是 0:做再多次也不会产生复利。"
        : "收益率是 0:怎么结算都不会产生复利增长。";
    } else if (trade) {
      refs.freqHint.textContent = "每次交易结一次:没有固定时间单位,只数做了多少次;每次 " +
        pctText(rateFraction(), 2) + ",下一次连本带利继续。";
    } else if (state.freq === 1) {
      refs.freqHint.textContent = "每年结一次:每期 " + pctText(rateFraction(), 2) +
        ",年化就是 " + pctText(rateFraction(), 2) + "。";
    } else {
      refs.freqHint.textContent = "每期 " + pctText(rateFraction(), 2) + " × 每年 " + state.freq +
        " 期 &#8658; 年化 " + pctText(data.effective, 1) + ";结算越频繁,年化越高。";
    }

    refs.targetHint.textContent = "对比图、阶梯和明细围绕 " + moneyShort(state.target) + " 高亮;点击切换目标。";

    $$("[data-freq]").forEach(button => {
      const active = Number(button.dataset.freq) === (trade ? 0 : state.freq);
      button.classList.toggle("active", active);
      button.setAttribute("aria-pressed", String(active));
    });

    $$("[data-target]").forEach(button => {
      const active = Number(button.dataset.target) === state.target;
      button.classList.toggle("active", active);
      button.setAttribute("aria-pressed", String(active));
    });

    $$("[data-ctab]").forEach(button => {
      const active = button.dataset.ctab === state.ctab;
      button.classList.toggle("active", active);
      button.setAttribute("aria-selected", String(active));
    });

    $$("[data-scale]").forEach(button => {
      const active = button.dataset.scale === state.scale;
      button.classList.toggle("active", active);
      button.setAttribute("aria-pressed", String(active));
    });

    refs.scaleSwitch.hidden = state.ctab === "yearly";

    const activePreset = state.extra !== 0 ? null : trade
      ? (state.rate === 3 ? "steady" : state.rate === 5 ? "pro" : state.rate === 10 ? "master" : null)
      : (state.freq === 1
        ? (state.rate === 8 ? "steady" : state.rate === 15 ? "pro" : state.rate === 20 ? "master" : null)
        : null);

    $$("[data-preset]").forEach(button => {
      const active = button.dataset.preset === activePreset;
      button.classList.toggle("active", active);
      button.setAttribute("aria-pressed", String(active));
    });
  }

  function renderAll() {
    const data = getData();
    syncControls(data);
    renderHero(data);
    renderKpis(data);
    renderStrip(data);
    renderCurveHeader(data);
    renderCurve(data);
    renderCompareHeader(data);
    renderCompare(data);
    renderLadder(data);
    renderChain(data);
    renderDetail(data);
    renderCalc(data);
    renderMobile(data);
    hideCurveTip();
  }

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

  function scheduleCharts() {
    window.clearTimeout(resizeTimer);
    resizeTimer = window.setTimeout(() => {
      const data = getData();
      renderCurveHeader(data);
      renderCurve(data);
      renderCompareHeader(data);
      renderCompare(data);
      hideCurveTip();
    }, 110);
  }

  function clampNumber(value, min, max, fallback) {
    const number = Number(value);
    if (!Number.isFinite(number)) return fallback;
    return clamp(number, min, max);
  }

  function applyPreset(name) {
    state.extra = 0;
    if (isTrade()) {
      state.rate = name === "steady" ? 3 : name === "pro" ? 5 : 10;
      state.trades = 120;
      renderAll();
      return;
    }
    state.tradeMode = false;
    state.freq = 1;
    if (name === "steady") {
      state.rate = 8;
      state.years = 40;
    } else if (name === "pro") {
      state.rate = 15;
      state.years = 30;
    } else {
      state.rate = 20;
      state.years = 30;
    }
    renderAll();
  }

  function resetAll() {
    Object.assign(state, DEFAULTS);
    renderAll();
  }

  refs.rateRange.addEventListener("input", event => {
    state.rate = Math.round(clamp(Number(event.target.value), 0, 100) * 10) / 10;
    scheduleRender();
  });

  refs.rateInput.addEventListener("input", event => {
    if (event.target.value === "") return;
    const value = Number(event.target.value);
    if (Number.isFinite(value) && value >= 0 && value <= 100) {
      state.rate = Math.round(value * 10) / 10;
      scheduleRender();
    }
  });
  refs.rateInput.addEventListener("blur", () => {
    state.rate = Math.round(clamp(clampNumber(refs.rateInput.value, 0, 100, state.rate), 0, 100) * 10) / 10;
    refs.rateInput.value = state.rate;
    renderAll();
  });

  refs.yearsRange.addEventListener("input", event => {
    if (isTrade()) {
      state.trades = clamp(Math.round(Number(event.target.value)), 10, 600);
    } else {
      state.years = clamp(Math.round(Number(event.target.value)), 5, 60);
    }
    scheduleRender();
  });

  refs.startInput.addEventListener("input", event => {
    const value = Number(event.target.value);
    if (Number.isFinite(value) && value >= 100) {
      state.start = clamp(value, 100, 1000000000);
      scheduleRender();
    }
  });
  refs.startInput.addEventListener("blur", () => {
    state.start = Math.round(clamp(clampNumber(refs.startInput.value, 100, 1000000000, state.start), 100, 1000000000));
    refs.startInput.value = state.start;
    renderAll();
  });

  refs.extraInput.addEventListener("input", event => {
    const value = Number(event.target.value);
    if (Number.isFinite(value) && value >= 0) {
      state.extra = clamp(value, 0, 10000000);
      scheduleRender();
    }
  });
  refs.extraInput.addEventListener("blur", () => {
    state.extra = Math.round(clamp(clampNumber(refs.extraInput.value, 0, 10000000, state.extra), 0, 10000000));
    refs.extraInput.value = state.extra;
    renderAll();
  });

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

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

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

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

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

  $$("[data-freq]").forEach(button => {
    button.addEventListener("click", () => {
      const value = Number(button.dataset.freq);
      if (value === 0) {
        state.tradeMode = true;
      } else {
        state.tradeMode = false;
        state.freq = value;
      }
      renderAll();
    });
  });

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

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

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

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

  $("#resetBtn").addEventListener("click", resetAll);

  refs.curveWrap.addEventListener("pointermove", onCurveMove);
  refs.curveWrap.addEventListener("pointerleave", hideCurveTip);
  refs.curveWrap.addEventListener("pointercancel", hideCurveTip);

  window.addEventListener("resize", scheduleCharts);

  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;
        scheduleCharts();
      } else if (!lastWidth) {
        lastWidth = width;
      }
    });
    observer.observe(refs.curveWrap);
  }

  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);
    }
  }

  renderAll();
})();
</script>
</body>
</html>

免费评分

参与人数 2吾爱币 +2 热心值 +1 收起 理由
bno357 + 1 + 1 我很赞同!
Dlily + 1 谢谢@Thanks!

查看全部评分

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

沙发
chenzhouyu 发表于 2026-10-25 23:36
不想做金融消费者,还是谢谢楼主分享
3#
yueye1122 发表于 2026-10-26 00:32
4#
liouxunhuan 发表于 2026-10-26 05:06
感谢分享
复利真的很重要
不错的工具 试试去
5#
Allin16 发表于 2026-10-26 08:22
咱们还没做,先收藏了。
6#
junshuiyujianni 发表于 2026-10-26 09:25
很直观,对没有概念的学生很有用,感谢分享
7#
sbbsdd 发表于 2026-10-26 09:40
做梦就靠她了
8#
ecao 发表于 2026-10-26 09:59
楼主工具不错,但是20%的收益再哪里啊
9#
feijun 发表于 2026-10-26 10:03
远离股市才是王道,还是感谢高手的分享!
10#
tempxx01 发表于 2026-10-26 10:07
白日梦工具
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-10-27 06:31

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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