# -*- coding: utf-8 -*-
"""
《价格谈判培训 · 攻心为上》—— 自包含 HTML 生成器
=================================================================
素材源：培训/参考PPT/攻心为上-大客户谈判与商机挖掘.md（38 页 OCR 全文）
        培训/参考PPT/攻心为上_转写自检.md（15 条存疑清单）
规则：只重排 + 提炼 + 归纳，不添加原稿以外的实质内容；
      彻底清除讲师个人信息（姓名/肖像/履历/抖音号）；不嵌入任何原页位图。
末尾调用 brand_kit.inject() 注入页眉/页脚。
"""
import io
import json
import os
import sys

W = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
RAW = os.path.join(W, '培训与上手', '培训', '_raw_price.html')
FINAL = os.path.join(W, '培训与上手', '培训', '价格谈判培训_攻心为上.html')
TODAY = '2026-10-07'


def esc(s):
    return (str(s).replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;'))


def wrapcn(s, n):
    """按字符数硬折行（中文每字≈1 字宽），保证文字不溢出方框。"""
    out, cur = [], ''
    for ch in s:
        cur += ch
        if len(cur) >= n:
            out.append(cur)
            cur = ''
    if cur:
        out.append(cur)
    return out


# ── SVG 基础件 ────────────────────────────────────────────────
def rect(x, y, w, h, fill='#FFFFFF', stroke='#CFDDFB', rx=12, sw='1.5', cls=''):
    c = ' class="%s"' % cls if cls else ''
    return ('<rect%s x="%s" y="%s" width="%s" height="%s" rx="%s" fill="%s" stroke="%s" '
            'stroke-width="%s"/>' % (c, x, y, w, h, rx, fill, stroke, sw))


def ebox(x, y, w, h, fill='#FFFFFF', stroke='#CFDDFB', rx=12):
    """等宽等间距自检用：带 class=ebox 的方框。"""
    return rect(x, y, w, h, fill, stroke, rx, cls='ebox')


def tx(x, y, s, size='16', fill='#334155', anchor='start', weight='400'):
    return ('<text x="%s" y="%s" font-size="%s" fill="%s" text-anchor="%s" '
            'font-weight="%s">%s</text>' % (x, y, size, fill, anchor, weight, esc(s)))


def wtext(cx, y0, text, size=15, fill='#475569', weight='400', n=15, lh=22, anchor='middle'):
    """自动折行的多行文本（tspan）。"""
    lines = wrapcn(text, n) if isinstance(text, str) else text
    o = ['<text x="%s" y="%s" font-size="%s" fill="%s" font-weight="%s" text-anchor="%s">'
         % (cx, y0, size, fill, weight, anchor)]
    for i, l in enumerate(lines):
        o.append('<tspan x="%s" dy="%s">%s</tspan>' % (cx, 0 if i == 0 else lh, esc(l)))
    o.append('</text>')
    return ''.join(o)


def line(x1, y1, x2, y2, stroke='#9DB8E8', sw='1.5', dash=None):
    d = ' stroke-dasharray="%s"' % dash if dash else ''
    return ('<line x1="%s" y1="%s" x2="%s" y2="%s" stroke="%s" stroke-width="%s"%s/>'
            % (x1, y1, x2, y2, stroke, sw, d))


def circ(cx, cy, r, fill='#FFFFFF', stroke='#0043C1', sw='2'):
    return ('<circle cx="%s" cy="%s" r="%s" fill="%s" stroke="%s" stroke-width="%s"/>'
            % (cx, cy, r, fill, stroke, sw))


def svg(vbw, vbh, inner):
    return ('<svg viewBox="0 0 %s %s" role="img" xmlns="http://www.w3.org/2000/svg">%s</svg>'
            % (vbw, vbh, inner))


# ═══════════════ 图 1 · 客户的需求类型（原稿 p11） ═══════════════
def fig1():
    o = []
    o.append(tx(421, 34, '客户的需求类型（矩阵）', '21', '#002D80', 'middle', '800'))
    xL, xR, w, h, yT, yB = 150, 427, 265, 120, 70, 202
    cells = [
        (xL, yT, '半明确型', '有需求 · 没标准', '#F1F5FE', '#0043C1'),
        (xR, yT, '完全明确型', '有需求 · 有标准', '#F0FDF4', '#0d904f'),
        (xL, yB, '不明确型', '没需求 · 没标准', '#FFF7ED', '#B45309'),
        (xR, yB, '原稿未列', '没需求 · 有标准（原稿未给出类型名）', '#F8FAFC', '#94A3B8'),
    ]
    for (x, y, name, sub, bg, fg) in cells:
        o.append(ebox(x, y, w, h, bg, fg))
        o.append(tx(x + w / 2, y + 56, name, '21', fg, 'middle', '800'))
        o.append(wtext(x + w / 2, y + 86, sub, 14, '#475569', '400', 14, 20))
    # 轴标签
    o.append(tx(130, 136, '有需求', '15', '#0043C1', 'end', '700'))
    o.append(tx(130, 268, '没需求', '15', '#94A3B8', 'end', '700'))
    o.append(tx(282, 52, '没标准', '15', '#0043C1', 'middle', '700'))
    o.append(tx(559, 52, '有标准', '15', '#0043C1', 'middle', '700'))
    o.append(line(150, 340, 692, 340, '#9DB8E8', '1.5'))
    o.append(tx(692, 366, '标准 →', '14', '#94A3B8', 'end', '400'))
    o.append(tx(150, 366, '↑ 需求', '14', '#94A3B8', 'start', '400'))
    return svg(780, 400, ''.join(o))


FIG1_CAP = '图 1　客户的需求类型：先分清「有没有需求」「有没有标准」，才知道该怎么谈（原稿 p11；第 4 格原稿未给出类型名）'


# ═══════════════ 图 2 · 走上谈判桌的三大条件（原稿 p18-p21） ═══════════════
def fig2():
    o = []
    o.append(rect(30, 20, 800, 50, '#0B3E8F', '#0B3E8F', 12))
    o.append(tx(430, 52, '客户为什么压根不跟我们谈？——走上谈判桌的三大条件', '18', '#FFFFFF', 'middle', '800'))
    w, gap, y, h = 250, 25, 100, 210
    boxes = [
        (30, '① 有问题', '#F1F5FE', '#0043C1',
         '满足于现状的客户，不会和你走上谈判桌。存在问题或分歧是走上谈判桌的首要条件，没有问题就没有谈判的必要。'
         '问题可以是利益冲突、目标差异或信息不对称等。'),
        (305, '② 你能解决问题', '#F0FDF4', '#0d904f',
         '能力展现：让对方相信你有实力满足其需求或解决其问题（专业知识、资源掌控、过往成功经验）。'
         '价值体现：你能为对方带来利益或好处，这是对方愿意与你谈判的关键。'),
        (580, '③ 你是最优选择', '#FFF7ED', '#B45309',
         '竞争优势：在众多可选择的谈判对象中具备优势（成本、效率、信誉等）。'
         '独特性：解决方案或价值难以被替代（创新技术、稀缺资源、个性化服务等）。'),
    ]
    for (x, title, bg, fg, body) in boxes:
        o.append(ebox(x, y, w, h, bg, fg))
        o.append(tx(x + w / 2, y + 40, title, '18', fg, 'middle', '800'))
        o.append(wtext(x + w / 2, y + 74, body, 13.5, '#475569', '400', 16, 21))
    o.append(wtext(430, 350, '问题必须是可以沟通协商解决的，否则谈判无法进行。'
                            '（如价值观的根本冲突这类不可调和的矛盾，谈判可能难以达成一致）',
                   14, '#B45309', '700', 42, 22))
    return svg(860, 400, ''.join(o))


FIG2_CAP = '图 2　走上谈判桌的三大条件：① 有问题　② 你能解决问题　③ 你是最优选择（原稿 p18–p21）'


# ═══════════════ 图 3 · 开价与折中（原稿 p24-p25） ═══════════════
def fig3():
    o = []
    w, gap = 385, 30
    x1, x2 = 30, 445
    # 行 1
    o.append(ebox(x1, 40, w, 170, '#F1F5FE', '#0043C1'))
    o.append(tx(x1 + w / 2, 70, '开价策略：先锁定，后调整', '17', '#0043C1', 'middle', '800'))
    o.append(wtext(x1 + w / 2, 100, '先出招优势明显，能引导对方预期，掌控谈判节奏。', 14, '#475569', '400', 24, 21))
    o.append(line(x1 + 40, 150, x1 + w - 40, 150, '#0043C1', '5'))
    o.append(tx(x1 + 40, 172, '实价', '14', '#475569', 'start', '700'))
    o.append(tx(x1 + w - 40, 172, '开价', '14', '#475569', 'end', '700'))
    o.append(tx(x1 + w / 2, 196, '开价略高于实价 → 中间即「可让步区间」', '13.5', '#B45309', 'middle', '700'))

    o.append(ebox(x2, 40, w, 170, '#F0FDF4', '#0d904f'))
    o.append(tx(x2 + w / 2, 70, '折中策略', '17', '#0d904f', 'middle', '800'))
    o.append(wtext(x2 + w / 2, 100, '双方差距大时，通过折中实现心理平衡，如价格谈判中取中间值。', 14, '#475569', '400', 24, 21))
    o.append(line(x2 + 40, 150, x2 + w - 40, 150, '#0d904f', '5'))
    o.append(circ(x2 + w / 2, 150, 9, '#FFFFFF', '#0d904f', '3'))
    o.append(tx(x2 + 40, 172, '我方', '14', '#475569', 'start', '700'))
    o.append(tx(x2 + w - 40, 172, '对方', '14', '#475569', 'end', '700'))
    o.append(tx(x2 + w / 2, 196, '尽量不要先做让步的那方（原稿表述存疑，见正文标注）', '13', '#B45309', 'middle', '700'))

    # 行 2
    o.append(ebox(x1, 240, w, 150, '#FEF2F2', '#C0271D'))
    o.append(tx(x1 + w / 2, 272, '千万不要接受对方第一次出价', '17', '#C0271D', 'middle', '800'))
    o.append(wtext(x1 + w / 2, 302, '心理学依据：轻易接受对方出价会引发怀疑，反而会拉高对方预期。',
                   14, '#475569', '400', 24, 21))
    o.append(ebox(x2, 240, w, 150, '#FFF7ED', '#B45309'))
    o.append(tx(x2 + w / 2, 272, '故作惊讶', '17', '#B45309', 'middle', '800'))
    o.append(wtext(x2 + w / 2, 302, '对对方报价表现惊讶，暗示不合理，如「这个价格远超出我们的预期」。',
                   14, '#475569', '400', 24, 21))
    return svg(860, 420, ''.join(o))


FIG3_CAP = '图 3　开价阶段的核心原则：开价 / 折中 / 不接受第一次出价 / 故作惊讶（原稿 p24–p25）'


# ═══════════════ 图 4 · 探寻底价的三种试探法（原稿 p28-p30） ═══════════════
def fig4():
    o = []
    o.append(tx(430, 34, '探寻底价的三种试探法', '21', '#002D80', 'middle', '800'))
    w, gap, y, h = 250, 25, 70, 168
    items = [
        (30, '①', '中间价位试探', '报出中间价位，看对方接不接（方法名，原稿无步骤）'),
        (305, '②', '次级产品', '推出较低一档产品，看对方在意价还是值（方法名，原稿无步骤）'),
        (580, '③', '其他供应方', '引入其他供应方作参照，逼出比较基准（方法名，原稿无步骤）'),
    ]
    for (x, no, title, body) in items:
        o.append(ebox(x, y, w, h, '#F1F5FE', '#0043C1'))
        o.append(circ(x + w / 2, y + 40, 20, '#0043C1', '#0043C1', '2'))
        o.append(tx(x + w / 2, y + 47, no, '18', '#FFFFFF', 'middle', '800'))
        o.append(tx(x + w / 2, y + 92, title, '17', '#002D80', 'middle', '800'))
        o.append(wtext(x + w / 2, y + 120, body, 13, '#475569', '400', 16, 20))
    o.append(wtext(430, 272,
                   '原稿第 28–30 页每页仅有一个方法标题、无正文展开；上排括号内为教材对方法名的直译性说明，非原稿原文。',
                   13.5, '#B45309', '700', 50, 22))
    o.append(wtext(430, 320, '自查提问（非原稿内容，供练习）：我这一招打出去，对方的反应告诉了我他的底价在哪里？',
                   13.5, '#94A3B8', '400', 46, 22))
    return svg(860, 350, ''.join(o))


FIG4_CAP = '图 4　探寻底价的三种试探法：① 中间价位试探　② 次级产品　③ 其他供应方（原稿 p28–p30）'


# ═══════════════ 图 5 · 三大原因 → 三大策略（原稿 p34-p35） ═══════════════
def fig5():
    o = []
    o.append(tx(30, 32, '客户拒绝 / 异议的三大原因', '16', '#002D80', 'start', '800'))
    w, gap, y, h = 250, 25, 46, 150
    reasons = [
        (30, '① 客户自身', '没时间、没需求、没权限、有替代产品（方案）……', '#F1F5FE', '#0043C1'),
        (305, '② 销售人员', '信任、关系、能力……', '#F0FDF4', '#0d904f'),
        (580, '③ 产品问题', '误解、怀疑、短板、口碑、价值塑造……', '#FFF7ED', '#B45309'),
    ]
    for (x, t, b, bg, fg) in reasons:
        o.append(ebox(x, y, w, h, bg, fg))
        o.append(tx(x + w / 2, y + 40, t, '17', fg, 'middle', '800'))
        o.append(wtext(x + w / 2, y + 74, b, 14, '#475569', '400', 17, 22))
    # 向下箭头
    o.append(line(430, 206, 430, 240, '#94A3B8', '2'))
    o.append('<polygon points="430,250 423,238 437,238" fill="#94A3B8"/>')
    o.append(tx(430, 234, '先分诊，再出招', '13.5', '#94A3B8', 'middle', '400'))
    o.append(tx(30, 282, '处理异议的三大策略', '16', '#002D80', 'start', '800'))
    strat = [(30, '① 不理他'), (305, '② 理一理'), (580, '③ 反问他')]
    for (x, t) in strat:
        o.append(ebox(x, 296, w, 90, '#F8FAFC', '#0043C1'))
        o.append(tx(x + w / 2, 336, t, '19', '#002D80', 'middle', '800'))
        o.append(tx(x + w / 2, 362, '原稿仅列策略名', '12.5', '#94A3B8', 'middle', '400'))
    return svg(860, 410, ''.join(o))


FIG5_CAP = '图 5　客户拒绝 / 异议的三大原因与处理异议的三大策略（原稿 p34–p35；三个策略原稿仅列名、无展开）'


# ═══════════════ 图 6 · 问题解决模型 · 五要素（原稿 p37） ═══════════════
def fig6():
    import math
    o = []
    o.append(tx(430, 32, '关键问题的解决思路与模型 · 五要素', '21', '#002D80', 'middle', '800'))
    cx, cy, R = 430, 232, 155
    names = ['问题', '动机', '信息', '规则', '利益']
    nums = ['01', '02', '03', '04', '05']
    pts = []
    for i in range(5):
        a = math.radians(-90 + 72 * i)
        pts.append((cx + R * math.cos(a), cy + R * math.sin(a)))
    # 五边形
    poly = ' '.join(['%.1f,%.1f' % p for p in pts])
    o.append('<polygon points="%s" fill="none" stroke="#C7DAFF" stroke-width="1.5" '
             'stroke-dasharray="5 5"/>' % poly)
    for p in pts:
        o.append(line(cx, cy, p[0], p[1], '#DCE5F5', '1.5'))
    # 中心：房子图标 + 文字
    o.append(circ(cx, cy, 62, '#0B3E8F', '#0B3E8F', '2'))
    o.append('<polygon points="%s" fill="#FFFFFF"/>'
             % ' '.join(['%.1f,%.1f' % q for q in
                         [(cx - 22, cy - 6), (cx, cy - 26), (cx + 22, cy - 6),
                          (cx + 16, cy - 6), (cx + 16, cy + 16), (cx - 16, cy + 16), (cx - 16, cy - 6)]]))
    o.append(tx(cx, cy + 48, '问题解决模型', '15', '#FFFFFF', 'middle', '800'))
    # 五要素节点
    for i, (px, py) in enumerate(pts):
        o.append(circ(px, py, 48, '#FFFFFF', '#0043C1', '2.5'))
        o.append(tx(px, py - 12, nums[i], '14', '#0043C1', 'middle', '800'))
        o.append(tx(px, py + 16, names[i], '21', '#002D80', 'middle', '800'))
    o.append(wtext(430, 396,
                   '原稿此页为位图承载的示意图（PPT 内无原生图表 ppt/charts/，无数据可从 XML 提取）；'
                   '五要素名称照原稿列出。', 13, '#94A3B8', '400', 58, 20))
    return svg(860, 430, ''.join(o))


FIG6_CAP = '图 6　问题解决模型（五要素）：01 问题 · 02 动机 · 03 信息 · 04 规则 · 05 利益（原稿 p37）'


def fig(svg_code, cap):
    return ('<figure><div class="fig-scroll">%s</div><figcaption>%s</figcaption></figure>'
            % (svg_code, esc(cap)))


def callout(kind, head, body):
    return ('<div class="callout %s"><span class="h">%s</span>%s</div>' % (kind, head, body))


# ═══════════════════════ 互动沙盒数据 ═══════════════════════
SCEN = [
    {
        'tab': '① 太贵了',
        'title': '客户说「太贵了」',
        'rounds': [
            {'say': '你们这个价格太贵了。',
             'opts': [
                 {'t': '我们的质量好，值这个价。', 'good': False,
                  'fb': '⚠️ 直接辩解＝亮明立场。原稿 p16 指出：谈判不是简单亮明条件和利益，也不是「明牌斗地主」，要根据对手的出牌来调整。',
                  'ref': '原稿 p16'},
                 {'t': '哦？这个价格远超出你们的预期吗？', 'good': True,
                  'fb': '✅ 故作惊讶——对对方报价表现惊讶、暗示不合理，正是原稿 p25 的做法，能把「贵」这个模糊判断逼成一个可谈的问题。',
                  'ref': '原稿 p25'},
                 {'t': '那您觉得多少合适？', 'good': False,
                  'fb': '⚠️ 过早把定价权交给对方。原稿 p24：先出招优势明显，能引导对方预期、掌控谈判节奏——反过来就是先失去主动权。',
                  'ref': '原稿 p24'}]},
            {'say': '比我们现在用的贵不少。',
             'opts': [
                 {'t': '那您现在是哪个价位？我们取个中间值。', 'good': False,
                  'fb': '⚠️ 还没弄清对方在意的点就先折中，而且是「先让步的那方」。原稿 p24 折中策略提醒：尽量不要先做让步的那方。',
                  'ref': '原稿 p24'},
                 {'t': '您说的「贵」，是单价高，还是整体用量算下来成本高？', 'good': True,
                  'fb': '✅ 先把「贵」拆成可协商的具体问题——这正是原稿 p34 三大原因里「客户自身 / 产品问题」的分诊动作，也呼应 p19「问题必须是可以沟通协商解决的」。',
                  'ref': '原稿 p34 · p19'},
                 {'t': '我们还有便宜一点的次级产品。', 'good': False,
                  'fb': '⚠️ 次级产品是探寻底价的方法（原稿 p29），但用在「还没定位顾虑」的阶段，等于把需求做小、把价值让掉。',
                  'ref': '原稿 p29'}]},
            {'say': '主要是用量大，总价下不来。',
             'opts': [
                 {'t': '那我给您报一个中间价位，咱们各让一步。', 'good': False,
                  'fb': '⚠️ 主动先让。原稿 p24：折中能实现心理平衡，但「尽量不要先做让步的那方」。',
                  'ref': '原稿 p24'},
                 {'t': '如果我们按中间价位先做一版，您拿它跟现在的供应商比一比？', 'good': True,
                  'fb': '✅ 中间价位试探（p28）＋ 用「其他供应方」（p30）作参照系，把对方的比较基准带出来，而自己没先让步。',
                  'ref': '原稿 p28 · p30'},
                 {'t': '总价可以谈，咱们先把合同签了？', 'good': False,
                  'fb': '⚠️ 跳过问题直接冲成交：原稿 p18 三个条件里的「① 有问题」还没解决，桌上就只剩价格。',
                  'ref': '原稿 p18'}]},
        ],
        'summary': ('<b>这一段小结 · 客户说「太贵了」</b>'
                    '<ul>'
                    '<li>先别辩解，用<b>故作惊讶</b>把「贵」逼成一个问题（原稿 p25）。</li>'
                    '<li>把模糊的「贵」拆成可协商项：<b>客户自身 / 销售人员 / 产品问题</b>三路分诊（原稿 p34）。</li>'
                    '<li><b>折中</b>与<b>中间价位试探、其他供应方</b>都要用，但别先让步（原稿 p24 / p28 / p30）。</li>'
                    '</ul>'),
    },
    {
        'tab': '② 再考虑考虑',
        'title': '客户说「我再考虑考虑」',
        'rounds': [
            {'say': '我再考虑考虑。',
             'opts': [
                 {'t': '好的，那您考虑好随时联系我。', 'good': False,
                  'fb': '⚠️ 对话到此结束，真正的原因没有暴露出来。原稿 p32 / p33：处理异议之前要先建立正确认知、做「真正的原因分析」。',
                  'ref': '原稿 p32 · p33'},
                 {'t': '行。不过我想确认一下——是方案哪块还没对上，还是有别的顾虑？', 'good': True,
                  'fb': '✅ 把「考虑」逼成具体顾虑，正是原稿 p33「真正的原因分析」要的动作，也回应 p17「客户为什么压根不跟我们谈？」。',
                  'ref': '原稿 p33 · p17'},
                 {'t': '您还有什么不放心的，我一次性说清楚。', 'good': False,
                  'fb': '⚠️ 仍然停在「不放心」这种笼统层面，没有问成具体项，进不了原稿 p34 的原因分诊。',
                  'ref': '原稿 p34'}]},
            {'say': '主要是怕换了以后不稳定。',
             'opts': [
                 {'t': '您多虑了，我们的产品很稳定。', 'good': False,
                  'fb': '⚠️ 直接否定客户顾虑。原稿 p34「产品问题」里本就列着「怀疑」——否定怀疑并不能消除怀疑。',
                  'ref': '原稿 p34'},
                 {'t': '您担心的是哪一类不稳定？上机、批间差，还是储存？', 'good': True,
                  'fb': '✅ 把模糊顾虑拆成可谈的具体问题，符合原稿 p19「问题必须是可以沟通协商解决的」；对应 p37 要素③「信息」。',
                  'ref': '原稿 p19 · p37'},
                 {'t': '那先别换，小批量试用？', 'good': False,
                  'fb': '⚠️ 试用本身是降低门槛的好办法，但在还没弄清「哪一类不稳定」时就给出，等于用一个让步换回一个没定义的问题。',
                  'ref': '原稿 p19'}]},
            {'say': '你们跟我们现在这家比，优势在哪？',
             'opts': [
                 {'t': '我们更便宜。', 'good': False,
                  'fb': '⚠️ 单点比价。原稿 p20「价值体现」＋ p21「独特性」要的是对方认可的价值与难以替代性，不只是价格。',
                  'ref': '原稿 p20 · p21'},
                 {'t': '在您刚说的「稳定」这一点上，我们能给的是……（能力展现），而且是同行较难替代的（独特性）。',
                  'good': True,
                  'fb': '✅ 直接对上原稿 p20 的能力展现 / 价值体现 与 p21 的竞争优势 / 独特性，把优势锚在对方已经承认的痛点上。',
                  'ref': '原稿 p20 · p21'},
                 {'t': '我们服务好。', 'good': False,
                  'fb': '⚠️ 笼统优点。原稿 p20 要求「能力」与「价值」被对方认可，泛泛而谈构不成认可。',
                  'ref': '原稿 p20'}]},
        ],
        'summary': ('<b>这一段小结 · 客户说「我再考虑考虑」</b>'
                    '<ul>'
                    '<li>「考虑」多半＝问题没暴露，先做<b>真正的原因分析</b>（原稿 p33）。</li>'
                    '<li>把模糊顾虑拆成<b>可沟通协商解决</b>的具体问题（原稿 p19），对应五要素里的<b>信息</b>（p37）。</li>'
                    '<li>优势要落在对方承认的痛点上，讲<b>能力 + 独特性</b>，不要只报价格（原稿 p20 / p21）。</li>'
                    '</ul>'),
    },
    {
        'tab': '③ 已有供应商',
        'title': '客户说「我们已经有稳定供应商了」',
        'rounds': [
            {'say': '我们已经有稳定的供应商了，暂时不需要。',
             'opts': [
                 {'t': '没关系，那以后有需要再联系。', 'good': False,
                  'fb': '⚠️ 没有制造或暴露问题。原稿 p19：满足于现状的客户，不会和你走上谈判桌。',
                  'ref': '原稿 p19'},
                 {'t': '稳定是好事。那现在的供应里，有没有哪一块是您觉得还能更好的？', 'good': True,
                  'fb': '✅ 先让对方自己指出问题——对应原稿 p18 条件①「有问题」与 p19「存在问题或分歧是走上谈判桌的首要条件」。',
                  'ref': '原稿 p18 · p19'},
                 {'t': '我们价格更低，您换我们吧。', 'good': False,
                  'fb': '⚠️ 只打价格。原稿 p20 / p21 要求先让对方认可你能解决问题、且你是最优选择。',
                  'ref': '原稿 p20 · p21'}]},
            {'say': '质量还行，就是有时候交期不太稳。',
             'opts': [
                 {'t': '交期我们可以保证。', 'good': False,
                  'fb': '⚠️ 只是空口表态，没有「能力展现」。原稿 p20 要的是让对方相信你有实力。',
                  'ref': '原稿 p20'},
                 {'t': '交期不稳这件事，影响到您哪一步了？', 'good': True,
                  'fb': '✅ 追问影响＝把问题放大成对方自己的事，对应原稿 p19 的问题/分歧，以及 p37 要素① 问题、⑤ 利益。',
                  'ref': '原稿 p19 · p37'},
                 {'t': '我们交期一向是行业里最快的。', 'good': False,
                  'fb': '⚠️ 没有回应对方刚提出的那一件事，等于换了话题，对方的顾虑还在。',
                  'ref': '原稿 p19'}]},
            {'say': '你们能解决？',
             'opts': [
                 {'t': '能，我们肯定能。', 'good': False,
                  'fb': '⚠️ 只有表态、没有证据。原稿 p20「能力展现」要的是专业知识、资源掌控或过往成功经验。',
                  'ref': '原稿 p20'},
                 {'t': '在交期这块，我们靠的是……（专业 / 资源 / 过往经验），可以先按您最急的那条线跑一遍。',
                  'good': True,
                  'fb': '✅ 对应原稿 p20 能力展现的三个来源，并用一次小范围验证把「能解决」变成对方看得见的事。',
                  'ref': '原稿 p20'},
                 {'t': '您先下个小单试试。', 'good': False,
                  'fb': '⚠️ 比表态好，但没回答「凭什么能解决」，对方仍无从判断你是不是最优选择（原稿 p21）。',
                  'ref': '原稿 p21'}]},
        ],
        'summary': ('<b>这一段小结 · 客户说「我们已经有稳定供应商了」</b>'
                    '<ul>'
                    '<li>现状满意＝没有谈判必要，先让对方<b>自己指出问题</b>（原稿 p18 条件① / p19）。</li>'
                    '<li>追问影响，把问题放大成对方的事，落到<b>问题</b>与<b>利益</b>两个要素（原稿 p37）。</li>'
                    '<li>「能解决」要给<b>能力展现</b>的证据（专业 / 资源 / 过往经验），并证明<b>你是最优选择</b>（原稿 p20 / p21）。</li>'
                    '</ul>'),
    },
]

SAMPLE_QA = [
    {'q': '客户说「太贵了」，第一句该怎么接？', 'a': '故作惊讶，暗示不合理（如「这个价格远超出我们的预期」）——原稿 p25。',
     'ref': 'p25'},
    {'q': '折中时最重要的分寸是什么？', 'a': '尽量不要先做让步的那方——原稿 p24。', 'ref': 'p24'},
    {'q': '探寻底价有哪三种试探法？', 'a': '① 中间价位试探　② 次级产品　③ 其他供应方——原稿 p28–p30。', 'ref': 'p28-30'},
    {'q': '客户拒绝 / 异议的三大原因是？', 'a': '① 客户自身　② 销售人员　③ 产品问题——原稿 p34。', 'ref': 'p34'},
    {'q': '处理异议的三大策略是？', 'a': '① 不理他　② 理一理　③ 反问他——原稿 p35。', 'ref': 'p35'},
    {'q': '问题解决模型的五个要素是？', 'a': '01 问题　02 动机　03 信息　04 规则　05 利益——原稿 p37。', 'ref': 'p37'},
    {'q': '走上谈判桌的三大条件是？', 'a': '① 有问题　② 你能解决问题　③ 你是最优选择——原稿 p18。', 'ref': 'p18'},
    {'q': '人性的两个弱点是什么？', 'a': '恐惧（害怕出问题）／贪婪（想要更好的）——原稿 p7。', 'ref': 'p7'},
]

SB_CSS = r"""
/* ===== 互动沙盒 · 对话演练（教学示意 · 纯内存） ===== */
.sb{background:linear-gradient(180deg,#FBFDFF,#F3F8FF);border:1px solid #D6E2FA;border-radius:14px;
  padding:16px 16px 15px;margin:14px 0 6px}
.sb-h{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-bottom:4px}
.sb-h .ttl{font-size:16px;font-weight:800;color:var(--deep)}
.sb-h .bdg{font-size:11px;background:#EAF1FF;color:var(--blue);border:1px solid #C7DAFF;
  border-radius:20px;padding:1px 9px;font-weight:700}
.sb-note{font-size:12.8px;color:var(--ink2);margin:6px 0 12px;line-height:1.75}
.sb-decl{background:#FEF2F2;border:1px solid #FCA5A5;border-left:4px solid #EF4444;border-radius:10px;
  padding:9px 12px;font-size:12.6px;color:#7F1D1D;margin:8px 0 12px;line-height:1.7}
.sb-decl b{color:#991B1B}
.sb-tabs{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}
.sb-tab{border:1px solid var(--line);background:#fff;color:var(--ink2);border-radius:20px;
  padding:6px 14px;font-size:13px;font-weight:700;cursor:pointer;font-family:var(--f)}
.sb-tab.on{background:var(--blue);border-color:var(--blue);color:#fff}
.sb-tab:hover{border-color:var(--blue);color:var(--blue)}
.sb-tab.on:hover{color:#fff}
.sb-stage{background:#fff;border:1px solid var(--line);border-radius:12px;padding:14px 15px;min-height:170px}
.sbtitle{font-size:13.5px;font-weight:800;color:var(--deep);margin-bottom:4px;
  border-bottom:1px dashed var(--line);padding-bottom:8px}
.sbprog{font-size:12px;color:var(--grey);font-weight:400;margin-left:6px}
.msg{display:flex;gap:9px;margin:12px 0;align-items:flex-start}
.msg .av{flex:none;width:30px;height:30px;border-radius:9px;display:flex;align-items:center;
  justify-content:center;font-size:12.5px;font-weight:800;color:#fff}
.msg.cust .av{background:#64748B}
.msg.me .av{background:var(--blue)}
.msg .bub{border-radius:11px;padding:9px 13px;font-size:13.6px;line-height:1.75;max-width:calc(100% - 46px)}
.msg.cust .bub{background:#F1F5F9;color:var(--ink);border:1px solid #E2E8F0}
.msg.me .bub{background:var(--tint);color:var(--deep);border:1px solid #C7DAFF}
.opts{display:flex;flex-direction:column;gap:9px;margin:12px 0 6px;padding-left:39px}
.opt{text-align:left;border:1px solid var(--line);background:#fff;color:var(--ink);border-radius:10px;
  padding:10px 13px;font-size:13.4px;line-height:1.7;cursor:pointer;font-family:var(--f);
  transition:.12s}
.opt:hover{border-color:var(--blue);background:var(--tint);color:var(--deep)}
.opt:active{transform:translateY(1px)}
.opthint{font-size:12px;color:var(--grey);padding-left:39px;margin-bottom:4px}
.fb{border-radius:10px;padding:10px 13px;font-size:13.2px;line-height:1.75;margin:8px 0 4px 39px}
.fb.good{background:#F0FDF4;border:1px solid #BBF7D0;color:#14532D}
.fb.bad{background:#FFF7ED;border:1px solid #FED7AA;color:#7C2D12}
.fb .ref{display:inline-block;margin-left:6px;font-size:11.5px;background:rgba(0,67,193,.08);
  color:var(--blue);border-radius:20px;padding:1px 9px;font-weight:700;white-space:nowrap}
.sumcard{margin:16px 0 4px;border-radius:11px;background:#0d2a52;color:#DCE6FB;padding:14px 16px;
  font-size:13.2px;line-height:1.8}
.sumcard b{color:#fff}
.sumcard ul{margin:8px 0 0;padding-left:20px}
.sumcard li{margin:5px 0;color:#C9D8EE}
.sb-btns{display:flex;flex-wrap:wrap;gap:8px;margin-top:13px;align-items:center}
.sb .btn{border-radius:9px;padding:7px 14px;font-size:13px;font-weight:700;cursor:pointer;
  border:1px solid;font-family:var(--f)}
.sb .btn.ghost{background:#fff;border-color:var(--line);color:var(--ink2)}
.sb .btn.ghost:hover{border-color:var(--blue);color:var(--blue)}
.sb .cnt{font-size:12px;color:var(--grey)}
@media(max-width:560px){.opts,.opthint{padding-left:0}.fb{margin-left:0}}
@media print{.sb .sb-tabs,.sb .sb-btns{display:none}.sb{break-inside:avoid;background:#fff}}
"""

SB_HTML = r'''<section id="s6">
<h2 class="s"><span class="no">6</span>互动沙盒 · 三段对话演练</h2>
<div class="lead">看懂了不等于会接。下面三段对话，每一轮都给你几个接法，点一下就知道这一句接得对不对、依据在原稿哪一页。</div>
<div class="sb" id="sbx">
  <div class="sb-h"><span class="ttl">🧪 试试看 · 客户原话怎么接</span><span class="bdg">纯内存 · 不落盘 · 不发请求</span></div>
  <div class="sb-decl">⚠️ 本沙盒为<b>教学示意</b>：对话与选项均由原稿的原则推演而来（每条反馈都标了原稿页码），<b>不是真实客户记录</b>；所有选择只在这里即时给反馈，<b>不写入任何系统、不留任何记录、不发任何请求</b>。</div>
  <div class="sb-note">玩法：点一个场景 → 逐轮选一个接法 → 立刻看到「✅ 为什么这样接好 / ⚠️ 问题在哪」→ 走完三轮给小结。想重来点「↺ 重来」。</div>
  <div class="sb-tabs" id="sb-tabs"></div>
  <div class="sb-stage" id="sb-stage"></div>
  <div class="sb-btns">
    <button class="btn ghost" id="sb-reset">↺ 重来本场景</button>
    <span class="cnt" id="sb-cnt"></span>
  </div>
</div>
</section>'''

SB_JS = r"""(function(){
  var SC = __SCENARIOS__;
  var st = {sc:0, round:0, done:false, picks:[]};
  var tabs = document.getElementById('sb-tabs');
  var stage = document.getElementById('sb-stage');
  var cnt = document.getElementById('sb-cnt');
  if (!tabs || !stage) return;

  function E(s){ return String(s==null?'':s).replace(/[&<>]/g,function(c){
      return c==='&'?'&amp;':(c==='<'?'&lt;':'&gt;'); }); }

  function renderTabs(){
    tabs.innerHTML = SC.map(function(s,i){
      return '<button class="sb-tab'+(i===st.sc?' on':'')+'" data-sc="'+i+'">'+E(s.tab)+'</button>';
    }).join('');
  }
  function bubble(who, txt){
    return '<div class="msg '+who+'"><span class="av">'+(who==='cust'?'客':'我')+'</span>'+
           '<div class="bub">'+E(txt)+'</div></div>';
  }
  function renderStage(){
    var sc = SC[st.sc], h = '';
    var done = st.done ? sc.rounds.length : st.round;
    h += '<div class="sbtitle">场景 '+(st.sc+1)+' / '+SC.length+'　·　'+E(sc.title)+
         '<span class="sbprog">已答 ' + st.picks.length + ' / ' + sc.rounds.length + ' 轮</span></div>';
    for (var i=0;i<sc.rounds.length;i++){
      var r = sc.rounds[i];
      h += bubble('cust', r.say);
      if (i < done){
        var o = r.opts[st.picks[i]];
        h += bubble('me', o.t);
        h += '<div class="fb '+(o.good?'good':'bad')+'">'+E(o.fb)+
             '<span class="ref">'+E(o.ref)+'</span></div>';
      } else if (i === st.round && !st.done){
        h += '<div class="opts">';
        r.opts.forEach(function(o,j){
          h += '<button class="opt" data-opt="'+j+'">'+String.fromCharCode(65+j)+'. '+E(o.t)+'</button>';
        });
        h += '</div><div class="opthint">↓ 选一个接法（点选后立刻给反馈）</div>';
        break;
      } else { break; }
    }
    if (st.done){ h += '<div class="sumcard">'+sc.summary+'</div>'; }
    stage.innerHTML = h;
    cnt.textContent = st.done ? ('✅ 本场景已走完 ' + sc.rounds.length + ' 轮')
                              : ('第 ' + (st.round+1) + ' / ' + sc.rounds.length + ' 轮');
  }
  function switchTo(i){
    st = {sc:i, round:0, done:false, picks:[]};
    renderTabs(); renderStage();
  }
  stage.addEventListener('click', function(e){
    var b = e.target && e.target.getAttribute && e.target.getAttribute('data-opt');
    if (b === null || b === undefined) return;
    st.picks[st.round] = parseInt(b,10);
    st.round++;
    if (st.round >= SC[st.sc].rounds.length) st.done = true;
    renderStage();
  });
  tabs.addEventListener('click', function(e){
    var b = e.target && e.target.getAttribute && e.target.getAttribute('data-sc');
    if (b === null || b === undefined) return;
    switchTo(parseInt(b,10));
  });
  document.getElementById('sb-reset').addEventListener('click', function(){ switchTo(st.sc); });
  renderTabs(); renderStage();
})();
"""


# ═══════════════════════ 页面骨架 ═══════════════════════
CSS = r"""
:root{
  --blue:#0043C1; --deep:#002D80; --tint:#F1F5FE; --line:#E4E9F2; --bg:#F5F7FA;
  --ink:#0F172A; --ink2:#475569; --grey:#94A3B8; --ok:#0d904f; --warn:#B45309; --bad:#C0271D;
  --f:'Microsoft YaHei','PingFang SC','Helvetica Neue',Arial,sans-serif;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{font-family:var(--f);background:var(--bg);color:var(--ink);font-size:14.5px;line-height:1.75;
  display:flex;flex-direction:column;min-height:100vh}
main{flex:1 0 auto}
.wrap{max-width:1220px;margin:0 auto;padding:20px 18px 70px;display:grid;
  grid-template-columns:236px minmax(0,1fr);gap:22px;align-items:start;width:100%}
.toc{position:sticky;top:14px;background:#fff;border:1px solid var(--line);border-radius:12px;
  padding:12px 10px;max-height:calc(100vh - 40px);overflow:auto}
.toc h4{margin:2px 8px 8px;font-size:12px;color:var(--grey);letter-spacing:.5px}
.toc .mt{margin:10px 8px 4px;font-size:11.5px;color:var(--blue);font-weight:800;letter-spacing:.5px;
  border-top:1px dashed var(--line);padding-top:8px}
.toc a{display:block;padding:6px 10px;border-radius:8px;color:var(--ink2);font-size:12.8px;
  text-decoration:none;border-left:2px solid transparent}
.toc a:hover{background:var(--tint);color:var(--blue)}
.toc a.on{background:var(--tint);color:var(--blue);font-weight:700;border-left-color:var(--blue)}
.toc a i{font-style:normal;display:inline-block;width:20px;color:var(--grey);font-weight:700}
.toc a.on i{color:var(--blue)}
@media(max-width:900px){
  .wrap{grid-template-columns:minmax(0,1fr);padding:12px 12px 56px;gap:14px}
  .toc{position:static;max-height:none}
  .toc a{display:inline-block;border-left:0;padding:5px 10px;border:1px solid var(--line);margin:2px}
  .toc .mt{display:inline-block}
}
.hero{background:linear-gradient(135deg,#0d47a1 0%,#1565c0 55%,#1a73e8 100%);color:#fff;
  border-radius:14px;padding:24px 26px;margin-bottom:20px;box-shadow:0 4px 16px rgba(13,71,161,.18)}
.hero .kicker{font-size:12.5px;letter-spacing:3px;opacity:.85;font-weight:700}
.hero h1{margin:6px 0 8px;font-size:25px;letter-spacing:.5px;line-height:1.4}
.hero p{margin:0;font-size:13.8px;opacity:.94;line-height:1.85}
.hero .tags{margin-top:12px;display:flex;flex-wrap:wrap;gap:7px}
.hero .tags span{font-size:11.8px;background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.3);
  padding:2px 10px;border-radius:20px}
section{background:#fff;border:1px solid var(--line);border-radius:14px;padding:20px 22px;margin-bottom:20px;scroll-margin-top:14px}
.chapter{background:linear-gradient(135deg,#0B3E8F,#0043C1);color:#fff;border-radius:14px;
  padding:18px 22px;margin:4px 0 20px;box-shadow:0 3px 12px rgba(11,62,143,.22);scroll-margin-top:14px}
.chapter .mk{font-size:12px;letter-spacing:2px;opacity:.85;font-weight:700}
.chapter h2{margin:4px 0 6px;font-size:21px;color:#fff;line-height:1.45}
.chapter p{margin:0;font-size:13.4px;opacity:.92}
h2.s{font-size:19px;color:var(--deep);margin:0 0 4px;display:flex;align-items:center;gap:10px;line-height:1.5}
h2.s .no{display:inline-flex;width:26px;height:26px;border-radius:8px;background:var(--blue);color:#fff;
  font-size:14px;align-items:center;justify-content:center;font-weight:800;flex:none}
.lead{color:var(--ink2);font-size:13.5px;margin:8px 0 14px}
h3{margin:22px 0 10px;font-size:15.5px;color:var(--deep);display:flex;align-items:center;gap:8px;line-height:1.5}
h3::before{content:'';width:4px;height:15px;border-radius:2px;background:var(--blue);display:inline-block;flex:none}
h4{margin:16px 0 8px;font-size:14px;color:var(--ink)}
p{margin:8px 0}
ul,ol{margin:8px 0;padding-left:24px}
li{margin:5px 0}
b,strong{color:var(--deep)}
.quote{background:#FBFCFE;border-left:4px solid var(--blue);border-radius:0 11px 11px 0;
  padding:13px 16px;margin:12px 0;font-size:13.8px;color:#1F2937;line-height:1.9}
.quote .src{display:block;margin-top:8px;font-size:12.4px;color:var(--grey);text-align:right}
.tblwrap{border:1px solid var(--line);border-radius:10px;overflow:hidden;margin:12px 0 4px}
.tbl{width:100%;border-collapse:separate;border-spacing:0;font-size:13.5px}
.tbl th{background:#F8FAFC;color:var(--deep);font-weight:700;text-align:left;padding:10px 12px;
  border-bottom:1px solid var(--line);font-size:13px}
.tbl td{padding:10px 12px;border-bottom:1px solid #F1F4F9;vertical-align:top}
.tbl tr:last-child td{border-bottom:0}
.tbl tbody tr:hover{background:#FBFCFE}
.grid{display:grid;gap:12px}
.c2{grid-template-columns:repeat(auto-fit,minmax(250px,1fr))}
.c3{grid-template-columns:repeat(auto-fit,minmax(226px,1fr))}
.mini{background:#FBFCFE;border:1px solid var(--line);border-radius:11px;padding:13px 15px}
.mini .t{font-weight:800;color:var(--deep);font-size:13.6px;margin-bottom:5px;display:flex;align-items:center;gap:7px}
.mini .d{font-size:13px;color:var(--ink2)}
.mini .ic{width:23px;height:23px;border-radius:7px;background:var(--tint);color:var(--blue);
  display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:800;flex:none}
.callout{border-radius:11px;padding:12px 15px;font-size:13.4px;margin:12px 0;border-left:4px solid;line-height:1.8}
.callout.warn{background:#FFFBEB;border-color:#F59E0B;color:#7C2D12}
.callout.bad{background:#FEF2F2;border-color:#EF4444;color:#7F1D1D}
.callout.ok{background:#F0FDF4;border-color:#22C55E;color:#14532D}
.callout.info{background:var(--tint);border-color:var(--blue);color:var(--deep)}
.callout .h{font-weight:800;display:block;margin-bottom:3px}
figure{margin:14px 0;background:#FBFCFE;border:1px solid var(--line);border-radius:12px;padding:14px 10px 8px}
figure svg{display:block;width:100%;height:auto}
.fig-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
.fig-scroll svg{min-width:760px}
figcaption{text-align:center;font-size:12.5px;color:var(--grey);margin-top:8px;line-height:1.7}
.tag{display:inline-block;font-size:11.5px;padding:1px 8px;border-radius:20px;background:var(--tint);
  color:var(--blue);font-weight:700;margin-right:4px}
.tag.w{background:#FEF3C7;color:#92400E}
.tag.g{background:#DCFCE7;color:#15803D}
.chk{display:grid;grid-template-columns:repeat(auto-fit,minmax(268px,1fr));gap:9px;margin:10px 0}
.chk .ci{display:flex;gap:9px;align-items:flex-start;background:#FBFCFE;border:1px solid var(--line);
  border-radius:10px;padding:10px 12px;font-size:13px}
.chk .bx{flex:none;width:19px;height:19px;border:2px solid var(--blue);border-radius:5px;margin-top:2px}
.quick{background:#0d2a52;color:#c9d8ee;border-radius:14px;padding:22px 24px;margin-bottom:18px}
.quick h2{color:#fff;font-size:19px;margin:0 0 10px}
.quick .qbar{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin:10px 0 4px}
.quick input{flex:1;min-width:200px;max-width:340px;border:1px solid rgba(255,255,255,.28);
  background:rgba(255,255,255,.10);color:#fff;border-radius:9px;padding:8px 12px;font-family:var(--f);
  font-size:13px;outline:none}
.quick input::placeholder{color:#93A7C6}
.quick input:focus{border-color:#7FA8E8;background:rgba(255,255,255,.16)}
.quick .qhint{font-size:12px;color:#93A7C6}
.quick .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px;margin-top:10px}
.quick .qc{background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.16);border-radius:11px;
  padding:13px 15px;line-height:1.8}
.quick .qc .t{color:#fff;font-weight:800;font-size:13.4px;margin-bottom:6px}
.quick .qc .d{font-size:12.8px;color:#C9D8EE}
.quick .qc .r{display:inline-block;margin-top:6px;font-size:11.5px;color:#9DC0F5}
.quick .empty{font-size:13px;color:#93A7C6;margin-top:10px}
@media print{
  body{background:#fff}
  .toc{display:none}
  .wrap{grid-template-columns:minmax(0,1fr);padding:0}
  section,.chapter,.hero,.quick,.model{break-inside:avoid}
  figure{break-inside:avoid}
  @page{margin:12mm 10mm}
}
"""


def build_body():
    B = []
    A = B.append

    # ── 页头 hero ──
    A('<div class="hero">')
    A('<div class="kicker">SALES · STRATEGY · TACTICS</div>')
    A('<h1>价格谈判培训 · 攻心为上</h1>')
    A('<p>大客户谈判与商机挖掘 —— 商机洞察 ｜ 需求探询 ｜ 价值博弈 ｜ 谈判策略</p>')
    A('<p style="margin-top:8px"><b>宝锐生物 · 销售培训</b>　｜　培训对象：全体销售</p>')
    A('<div class="tags"><span>需求探询</span><span>上桌三条件</span><span>开价与让步</span>'
      '<span>探寻底价</span><span>异议处理</span><span>互动沙盒</span></div>')
    A('</div>')

    # ── 开篇 ──
    A('<section id="s0">')
    A('<h2 class="s"><span class="no">✦</span>开篇 · 为什么谈判决定单子成败</h2>')
    A('<div class="lead">很多单子不是输在产品上，是输在「没谈成桌上那几句」。先看看我们最容易卡住的三个地方。</div>')
    A('<div class="grid c3">')
    A('<div class="mini"><div class="t"><span class="ic">1</span>客户说「不需要」</div>'
      '<div class="d">我们总是假定所有的客户都需要自己的产品（服务），以为只要把产品能提供的好处尽量展现，就能打动对方。'
      '<span class="tag">原稿 p6</span></div></div>')
    A('<div class="mini"><div class="t"><span class="ic">2</span>客户压根不跟我们谈</div>'
      '<div class="d">满足于现状的客户，不会和你走上谈判桌。没有分歧、没有可协商的问题，就没有谈判的必要。'
      '<span class="tag">原稿 p17 · p19</span></div></div>')
    A('<div class="mini"><div class="t"><span class="ic">3</span>谈判变成了讨价还价</div>'
      '<div class="d">谈判不是简单的讨价还价，也不是尔虞我诈。大多数商务人士简单粗暴地认为谈判就是摆明自己的立场。'
      '<span class="tag">原稿 p15 · p16</span></div></div>')
    A('</div>')
    A('<h3>一句话抓住这件事的本质</h3>')
    A('<div class="quote">所谓谈判不是简单的亮明条件和利益……谈判是一个根据对方的决策不断调整自己对策的过程，'
      '它并非是明牌斗地主，而是根据对手的出牌来调整自己的出牌顺序。'
      '<span class="src">原稿 p16</span></div>')
    A('<div class="grid c3" style="margin-top:14px">')
    A('<div class="mini"><div class="t">思维定型，比失败更可怕</div>'
      '<div class="d">摆脱束缚，先从思维开始。<span class="tag">原稿 p3</span></div></div>')
    A('<div class="mini"><div class="t">谈判，无所不在</div>'
      '<div class="d">谈判不是某个环节，而是贯穿整条沟通线。<span class="tag">原稿 p13</span></div></div>')
    A('<div class="mini"><div class="t">双赢谈判其实是追求均衡</div>'
      '<div class="d">不是谁说服谁，而是回到一个双方都接受的均衡点。<span class="tag">原稿 p22</span></div></div>')
    A('</div>')
    A('<h3>学完这五章，你会得到什么</h3>')
    A('<ol>'
      '<li><b>需求探询</b>：听懂客户说「不需要」背后的真相。</li>'
      '<li><b>上桌三条件</b>：把「不谈」的客户拉上谈判桌。</li>'
      '<li><b>开价与让步</b>：先手怎么出、什么时候不能接。</li>'
      '<li><b>探寻底价</b>：三种试探法把对方的心理价位问出来。</li>'
      '<li><b>异议处理</b>：三大原因 → 三大策略 → 五要素模型。</li>'
      '</ol>')
    A(callout('info', '本教材的来源与边界',
              '本教材是原课件《攻心为上——大客户谈判与商机挖掘》的<b>重排与提炼</b>，'
              '<b>未添加原稿以外的实质内容</b>；原稿为「整页图片型」，文字经 OCR ＋ 视觉核对得到，'
              '凡字形或表述存疑处，已就地标注「原稿此处表述存疑」，未作自行修正。'))
    A('</section>')

    # ── 第 1 章 ──
    A('<div class="chapter" id="c1"><div class="mk">CHAPTER 1</div>'
      '<h2>第 1 章 · 需求探询：客户说「不需要」背后的真相</h2>'
      '<p>先弄明白一件事：什么是需求？谁更懂客户的需求？</p></div>')
    A('<section id="s1">')
    A('<h3>灵魂之问：什么是需求？</h3>')
    A('<p>针对销售人员的灵魂之问，原稿给出的第一个问题就是：<b>什么是需求？</b>'
      '<span class="tag">原稿 p5</span> 原稿此页背景为数学公式黑板（装饰性背景），公式未逐字转写。</p>')
    A('<h3>客户为什么说不需要？</h3>')
    A('<div class="quote">我们总是假定所有的客户都需要自己产品（服务），以为只要把产品能提供的好处尽量展现就能打动对方。'
      '<span class="src">原稿 p6</span></div>')
    A('<h3>人性的两个弱点</h3>')
    A('<div class="grid c2">')
    A('<div class="mini"><div class="t">恐惧</div><div class="d">害怕出问题。'
      '<span class="tag">原稿 p7</span></div></div>')
    A('<div class="mini"><div class="t">贪婪</div><div class="d">想要更好的。'
      '<span class="tag">原稿 p7</span></div></div>')
    A('</div>')
    A('<h3>小问题：谁更了解客户的需求？</h3>')
    A('<div class="quote">是客户了解他的需求？还是我们更了解客户需求？<span class="src">原稿 p8</span></div>')
    A('<h3>原稿引用的乔布斯这句话</h3>')
    A('<div class="quote">有些人说：「客户想要什么就给他们什么」，但那不是我的方式。我们的责任是提前一步搞清楚'
      '将来他们想要什么？我记得亨利·福特曾说过：「如果我最初问客户想要什么？」他们应该会告诉我：'
      '「想要一辆更好的马车！」，人们往往不知道自己想要什么。'
      '<span class="src">——《乔布斯传》 Steve Jobs　1955-2011　（原稿 p9）</span></div>')
    A('<h3>迎合 与 影响</h3>')
    A('<p>原稿此页仅陈列「<b>迎合</b>」「<b>影响</b>」两个词。'
      '<span class="tag w">原稿此处表述存疑</span> 该页疑似以色块/箭头表达二者关系，'
      '但位图中<b>未见任何文字说明二者关系</b>，此处照实只列两词，不代为解释。</p>')
    A('<h3>客户的需求类型</h3>')
    A('<p>把客户按「有没有需求」「有没有标准」分成三类，才知道该用哪套谈法。</p>')
    A(fig(fig1(), FIG1_CAP))
    A('<div class="tblwrap"><table class="tbl">')
    A('<thead><tr><th>类型</th><th>有无需求</th><th>有无标准</th></tr></thead><tbody>')
    A('<tr><td>完全明确型</td><td>有需求</td><td>有标准</td></tr>')
    A('<tr><td>半明确型</td><td>有需求</td><td>没标准</td></tr>')
    A('<tr><td>不明确型</td><td>没需求</td><td>没标准</td></tr>')
    A('</tbody></table></div>')
    A('<div class="callout info"><span class="h">本章记住一句话</span>'
      '别急着展示好处——先判断客户处在「明确 / 半明确 / 不明确」哪一型，再决定是迎合还是影响。'
      '<span class="tag">原稿 p6 · p11</span></div>')
    A('</section>')

    # ── 第 2 章 ──
    A('<div class="chapter" id="c2"><div class="mk">CHAPTER 2</div>'
      '<h2>第 2 章 · 上桌三条件：怎么把客户拉上谈判桌</h2>'
      '<p>不是所有客户都愿意谈。先让他有理由坐到桌前。</p></div>')
    A('<section id="s2">')
    A('<h3>为什么需要谈判？</h3>')
    A('<p><b>谈判，无所不在。</b><span class="tag">原稿 p13</span></p>')
    A('<div class="quote">谈判不是简单的讨价还价，也不是尔虞我诈。<span class="src">原稿 p15</span></div>')
    A('<h3>为什么你的谈判不成功</h3>')
    A('<div class="quote">所谓谈判不是简单的亮明条件和利益，往往大多数商务人士简单粗暴的认为谈判就是摆明自己的立场。'
      '然而事实是谈判是一个根据对方的决策不断调整自己对策的过程，它并非是明牌斗地主，'
      '而是根据对手的出牌来调整自己的出牌顺序。<span class="src">原稿 p16</span></div>')
    A('<h3>思考：客户为什么压根不跟我们谈？</h3>')
    A('<p>因为他还<b>没有非谈不可的理由</b>。原稿把「走上谈判桌」拆成三个条件——三个都成立，谈判才谈得起来。'
      '<span class="tag">原稿 p17 · p18</span></p>')
    A(fig(fig2(), FIG2_CAP))
    A('<h3>逐条展开</h3>')
    A('<h4>① 有问题存在</h4>')
    A('<div class="grid c2">')
    A('<div class="mini"><div class="t">01 问题的必要性</div><div class="d">'
      '满足于现状的客户，不会和你走上谈判桌。存在问题或分歧是走上谈判桌的首要条件，没有问题就没有谈判的必要。'
      '问题可以是利益冲突、目标差异或信息不对称等，这些问题促使双方寻求解决方案。</div></div>')
    A('<div class="mini"><div class="t">02 提示</div><div class="d">'
      '问题必须是可以沟通协商解决的，否则谈判无法进行。对于一些不可调和的矛盾，'
      '如价值观的根本冲突，谈判可能难以达成一致。</div></div>')
    A('</div>')
    A('<h4>② 对方认可你能解决问题</h4>')
    A('<div class="grid c2">')
    A('<div class="mini"><div class="t">能力展现</div><div class="d">'
      '你需要展现出解决问题的能力，让对方相信你有实力满足其需求或解决其问题。'
      '这种能力可以是专业知识、资源掌控或过往成功经验等，给对方信心。</div></div>')
    A('<div class="mini"><div class="t">价值体现</div><div class="d">'
      '你的价值要被对方认可，即你能为对方带来利益或好处，这是对方愿意与你谈判的关键。'
      '价值可以是产品优势、服务特色或合作带来的潜在收益等。</div></div>')
    A('</div>')
    A('<h4>③ 你是最优选择</h4>')
    A('<div class="grid c2">')
    A('<div class="mini"><div class="t">竞争优势</div><div class="d">'
      '在众多可选择的谈判对象中，你要具备竞争优势，成为对方的最优选择。'
      '优势可以体现在成本、效率、信誉等方面，让对方觉得与你合作比与其他方更有利。</div></div>')
    A('<div class="mini"><div class="t">独特性</div><div class="d">'
      '你提供的解决方案或价值具有独特性，难以被替代，增加了对方选择你的意愿。'
      '独特性可以是创新技术、稀缺资源或个性化服务等。</div></div>')
    A('</div>')
    A('<h3>均衡：双赢谈判到底是什么</h3>')
    A('<div class="quote">所谓的双赢谈判其实是追求均衡。<span class="src">原稿 p22</span></div>')
    A(callout('warn', '原稿此处表述存疑',
              '第 22 页标题「均衡」为艺术字体，字形不常规（OCR 曾误读为「均𧗾」），此处按视觉核对取「均衡」。'))
    A('</section>')

    # ── 第 3 章 ──
    A('<div class="chapter" id="c3"><div class="mk">CHAPTER 3</div>'
      '<h2>第 3 章 · 开价与让步：先手怎么出、什么时候不能接</h2>'
      '<p>审时度势，掌握开价阶段的原则，先手才在你这儿。</p></div>')
    A('<section id="s3">')
    A('<h3>审时度势 · 开价策略</h3>')
    A('<p>开价阶段有两组核心原则：一组是<b>怎么出价</b>，一组是<b>什么时候不能接</b>。'
      '<span class="tag">原稿 p23</span></p>')
    A(fig(fig3(), FIG3_CAP))
    A('<h3>开价阶段的核心原则（先锁定，后调整 / 开价策略 / 折中策略）</h3>')
    A('<div class="tblwrap"><table class="tbl">')
    A('<thead><tr><th>栏</th><th>标题</th><th>正文</th></tr></thead><tbody>')
    A('<tr><td>1</td><td>先锁定，后调整</td><td>先出招优势明显，能引导对方预期，掌控谈判节奏。</td></tr>')
    A('<tr><td>2</td><td>开价策略</td><td>开价略高于实价，设置可让步区间。</td></tr>')
    A('<tr><td>3</td><td>折中策略</td><td>双方差距大时，通过折中实现心理平衡，如价格谈判中取中间值。<br>'
      '尽量不要先做让步的那方。</td></tr>')
    A('</tbody></table></div>')
    A(callout('warn', '原稿此处表述存疑（两处）',
              '① 第 24 页三栏标题<b>体例不一致</b>（首栏为动宾短语「先锁定，后调整」，后两栏为策略名），'
              '疑为原稿排版如此，此处照实保留、未作统一。'
              '② 第 3 栏末句「尽量不要先做让步的那方」句意拗口，OCR 曾误读为「先敌让步」，'
              '此处取视觉核对值，仍建议对照原件。'))
    A('<h3>开价阶段的核心原则（不要接受第一次出价 / 故作惊讶）</h3>')
    A('<div class="tblwrap"><table class="tbl">')
    A('<thead><tr><th>栏</th><th>标题</th><th>正文</th></tr></thead><tbody>')
    A('<tr><td>1</td><td>千万不要接受对方第一次出价</td>'
      '<td><b>心理学依据</b>：轻易接受对方出价会引发怀疑，反而会拉高对方预期。</td></tr>')
    A('<tr><td>2</td><td>故作惊讶</td>'
      '<td>对对方报价表现惊讶，暗示不合理，如「这个价格远超出我们的预期」。</td></tr>')
    A('</tbody></table></div>')
    A(callout('warn', '原稿此处表述存疑',
              '第 25 页「反而会<b>拉</b>高对方预期」一句，OCR 曾读作「抗高」，字形相近；'
              '此处取视觉核对值「拉高」，仍属推测。'))
    A('</section>')

    # ── 第 4 章 ──
    A('<div class="chapter" id="c4"><div class="mk">CHAPTER 4</div>'
      '<h2>第 4 章 · 探寻底价：三种试探法</h2>'
      '<p>对方不肯说，就自己试出来。</p></div>')
    A('<section id="s4">')
    A('<h3>先想一想这个场景</h3>')
    A('<div class="quote">在跟客户的合作谈判中，有没遇到我方的报价和对方的还价之间存在分歧导致沟通无法继续的情况？'
      '如果有。那么，你当时是怎么处理的？<span class="src">原稿 p27 · 案例提问</span></div>')
    A('<h3>探寻底价的三种试探法</h3>')
    A(fig(fig4(), FIG4_CAP))
    A('<div class="grid c3">')
    A('<div class="mini"><div class="t"><span class="ic">①</span>中间价位试探</div>'
      '<div class="d">以中间价位回应，看对方接不接。<span class="tag w">原稿仅方法名</span></div></div>')
    A('<div class="mini"><div class="t"><span class="ic">②</span>次级产品</div>'
      '<div class="d">推出较低一档的产品，试探对方在意的是价还是值。<span class="tag w">原稿仅方法名</span></div></div>')
    A('<div class="mini"><div class="t"><span class="ic">③</span>其他供应方</div>'
      '<div class="d">引入其他供应方作参照，逼出对方的比较基准。<span class="tag w">原稿仅方法名</span></div></div>')
    A('</div>')
    A(callout('warn', '原稿此处无正文',
              '原稿第 28–30 页为同一组「探寻底价的策略」的三种方法分页，<b>每页仅有一个标题、无正文</b>。'
              '上表括号内的说明是对方法名的直译性描述，<b>非原稿原文</b>；具体步骤请对照原件或由讲师补充。'))
    A('</section>')

    # ── 第 5 章 ──
    A('<div class="chapter" id="c5"><div class="mk">CHAPTER 5</div>'
      '<h2>第 5 章 · 异议处理：三大原因 → 三大策略 → 五要素模型</h2>'
      '<p>客户拒绝你，先别急着解释，先分诊。</p></div>')
    A('<section id="s5">')
    A('<h3>处理客户异议之前，需要建立正确认知</h3>')
    A('<p>处理异议的第一步不是话术，是<b>认知</b>：先建立正确认知，再做<b>真正的原因分析</b>。'
      '<span class="tag">原稿 p32 · p33</span></p>')
    A(fig(fig5(), FIG5_CAP))
    A('<div class="tblwrap"><table class="tbl">')
    A('<thead><tr><th>序号</th><th>分类</th><th>说明</th></tr></thead><tbody>')
    A('<tr><td>1</td><td>客户自身</td><td>没时间、没需求、没权限、有替代产品（方案）……</td></tr>')
    A('<tr><td>2</td><td>销售人员</td><td>信任、关系、能力……</td></tr>')
    A('<tr><td>3</td><td>产品问题</td><td>误解、怀疑、短板、口碑、价值塑造……</td></tr>')
    A('</tbody></table></div>')
    A('<h3>处理异议的三大策略</h3>')
    A('<div class="grid c3">')
    A('<div class="mini"><div class="t"><span class="ic">①</span>不理他</div>'
      '<div class="d">原稿仅列策略名，未展开适用条件。</div></div>')
    A('<div class="mini"><div class="t"><span class="ic">②</span>理一理</div>'
      '<div class="d">原稿仅列策略名，未展开适用条件。</div></div>')
    A('<div class="mini"><div class="t"><span class="ic">③</span>反问他</div>'
      '<div class="d">原稿仅列策略名，未展开适用条件。</div></div>')
    A('</div>')
    A('<h3>关键问题的解决思路与模型</h3>')
    A('<p>原稿把关键问题收在一个五要素模型里：<b>问题 · 动机 · 信息 · 规则 · 利益</b>。'
      '<span class="tag">原稿 p36 · p37</span></p>')
    A(fig(fig6(), FIG6_CAP))
    A('<div class="tblwrap"><table class="tbl">')
    A('<thead><tr><th>编号</th><th>要素</th></tr></thead><tbody>')
    A('<tr><td>01</td><td>问题</td></tr><tr><td>02</td><td>动机</td></tr><tr><td>03</td><td>信息</td></tr>')
    A('<tr><td>04</td><td>规则</td></tr><tr><td>05</td><td>利益</td></tr>')
    A('</tbody></table></div>')
    A(callout('warn', '原稿此处为位图，无原生图表',
              '第 37 页五要素模型在 PPT 内<b>无原生图表</b>（<code>ppt/charts/</code> 为空），'
              '形似 SmartArt/图表但实为位图，<b>数据无法从 XML 提取</b>；本教材按原图重绘为 SVG，五个要素名称照原稿列出。'))
    A('</section>')

    # ── 沙盒 ──
    A(SB_HTML)

    # ── 速查卡 ──
    A('<section id="s7">')
    A('<h2 class="s"><span class="no">✦</span>收尾 · 一页速查卡</h2>')
    A('<div class="lead">五章压缩成一页。可以截图存手机，上桌前扫一眼。下面的搜索框能按关键词筛卡片。</div>')
    A('<div class="quick">')
    A('<h2>攻心为上 · 谈判速查卡</h2>')
    A('<div class="qbar"><input id="qk-search" type="text" placeholder="输入关键词筛选，如：折中 / 异议 / 五要素">'
      '<span class="qhint" id="qk-count"></span></div>')
    A('<div class="cards" id="qk-cards">')
    A('<div class="qc"><div class="t">需求探询</div><div class="d">'
      '客户说「不需要」，多半是我们假定他需要。人性的两个弱点：恐惧（害怕出问题）／贪婪（想要更好的）。'
      '需求类型：完全明确型 · 半明确型 · 不明确型。</div><span class="r">原稿 p6 · p7 · p11</span></div>')
    A('<div class="qc"><div class="t">上桌三条件</div><div class="d">'
      '① 有问题　② 你能解决问题　③ 你是最优选择。满足于现状的客户，不会和你走上谈判桌。</div>'
      '<span class="r">原稿 p18 · p19</span></div>')
    A('<div class="qc"><div class="t">开价与让步</div><div class="d">'
      '先锁定后调整；开价略高于实价、留可让步区间；差距大就折中取中间值，但尽量别先让步；'
      '不接受对方第一次出价；对报价要故作惊讶。</div><span class="r">原稿 p24 · p25</span></div>')
    A('<div class="qc"><div class="t">探寻底价三法</div><div class="d">'
      '① 中间价位试探　② 次级产品　③ 其他供应方。</div><span class="r">原稿 p28–p30</span></div>')
    A('<div class="qc"><div class="t">异议三大原因</div><div class="d">'
      '① 客户自身（没时间/没需求/没权限/有替代）　② 销售人员（信任/关系/能力）　③ 产品问题（误解/怀疑/短板/口碑/价值塑造）。</div>'
      '<span class="r">原稿 p34</span></div>')
    A('<div class="qc"><div class="t">异议三大策略</div><div class="d">'
      '① 不理他　② 理一理　③ 反问他。（先建立正确认知，再做真正的原因分析）</div>'
      '<span class="r">原稿 p32 · p33 · p35</span></div>')
    A('<div class="qc"><div class="t">五要素模型</div><div class="d">'
      '01 问题　02 动机　03 信息　04 规则　05 利益。</div><span class="r">原稿 p37</span></div>')
    A('<div class="qc"><div class="t">一句话总纲</div><div class="d">'
      '谈判不是明牌斗地主，是根据对手的出牌来调整自己的出牌顺序；双赢谈判其实是追求均衡。</div>'
      '<span class="r">原稿 p16 · p22</span></div>')
    A('</div>')
    A('<div class="empty" id="qk-empty" style="display:none">没有匹配的卡片，换个词试试。</div>')
    A('</div>')
    A('</section>')

    # ── 自检清单 ──
    A('<section id="s8">')
    A('<h2 class="s"><span class="no">✦</span>收尾 · 上桌前自检清单</h2>')
    A('<div class="lead">每次重要沟通前，逐项过一遍——有一项没打勾，就先补。</div>')
    A('<div class="chk">')
    for s in [
        '我判断过客户属于哪一型（完全明确 / 半明确 / 不明确）',
        '我找到（或制造）了客户必须跟我谈的那个「问题」',
        '我能说清「你凭什么解决」——能力展现 + 价值体现',
        '我说得出「为什么我是最优选择」——竞争优势 + 独特性',
        '我出价前就定好了开价与可让步区间',
        '我没有在第一时间接受对方的出价，也没先做让步的那方',
        '我用了至少一种探寻底价的方法，而不是直接问「底价多少」',
        '客户有异议时，我先分诊三大原因，再决定不理他 / 理一理 / 反问他',
        '我用五要素（问题·动机·信息·规则·利益）把关键问题过了一遍',
    ]:
        A('<div class="ci"><span class="bx"></span><div>%s</div></div>' % esc(s))
    A('</div>')
    A(callout('ok', '全部打勾之后',
              '你已经不是在「摆明立场」，而是在按对手的出牌调整自己的出牌顺序——这就是原稿说的谈判。'
              '<span class="tag">原稿 p16</span>'))
    A('</section>')

    # ── 附录：练习与出处 ──
    A('<section id="s9">')
    A('<h2 class="s"><span class="no">✦</span>附录 · 自问自答与内容出处</h2>')
    A('<div class="lead">八个高频问题，答案都标了原稿页码，方便你回去核对。</div>')
    A('<div class="tblwrap"><table class="tbl">')
    A('<thead><tr><th>问题</th><th>答案（照原稿）</th><th>原稿</th></tr></thead><tbody>')
    for qa in SAMPLE_QA:
        A('<tr><td>%s</td><td>%s</td><td>%s</td></tr>' % (esc(qa['q']), esc(qa['a']), esc(qa['ref'])))
    A('</tbody></table></div>')
    A(callout('info', '关于存疑处',
              '原稿为整页图片型 PPT，全部文字为 OCR ＋ 视觉识别所得，不具备逐字节可验证性。'
              '本教材把存疑处全部就地标注为「原稿此处表述存疑 / 原稿仅方法名 / 原稿未列」，'
              '<b>未作任何自行修正，也未补写原稿没有的内容</b>。'))
    A('</section>')

    return '\n'.join(B)


JS = r"""
(function(){
  /* 目录高亮 */
  var links = Array.prototype.slice.call(document.querySelectorAll('.toc a'));
  var secs = links.map(function(a){ return document.querySelector(a.getAttribute('href')); });
  function onScroll(){
    var y = window.scrollY + 130, cur = 0;
    for (var i=0;i<secs.length;i++){ if (secs[i] && secs[i].offsetTop <= y) cur = i; }
    links.forEach(function(a,i){ a.classList.toggle('on', i===cur); });
  }
  window.addEventListener('scroll', onScroll, {passive:true});
  onScroll();

  /* 速查卡搜索 —— 中文输入法安全：拼音合成期间不重渲染 */
  var composing = false;
  document.addEventListener('compositionstart', function(){ composing = true; });
  document.addEventListener('compositionend',   function(){ composing = false; });

  var qk = document.getElementById('qk-search');
  if (qk){
    var cards = Array.prototype.slice.call(document.querySelectorAll('#qk-cards .qc'));
    var cnt = document.getElementById('qk-count');
    var empty = document.getElementById('qk-empty');
    function filter(){
      if (composing) return;                 /* 拼音组合中：不触发过滤/重渲染 */
      var q = (qk.value || '').trim().toLowerCase();
      var n = 0;
      cards.forEach(function(c){
        var hit = !q || c.textContent.toLowerCase().indexOf(q) >= 0;
        c.style.display = hit ? '' : 'none';
        if (hit) n++;
      });
      if (cnt) cnt.textContent = '显示 ' + n + ' / ' + cards.length + ' 张';
      if (empty) empty.style.display = n ? 'none' : '';
    }
    qk.addEventListener('input', filter);
    filter();
  }
})();
"""


def build_html():
    body = build_body()
    scen_json = json.dumps(SCEN, ensure_ascii=False).replace('</', '<\\/')
    sb_js = SB_JS.replace('__SCENARIOS__', scen_json)
    html = ('<!DOCTYPE html>\n<html lang="zh-CN">\n<head>\n<meta charset="utf-8">\n'
            '<meta name="viewport" content="width=device-width,initial-scale=1">\n'
            '<title>价格谈判培训 · 攻心为上 —— 宝锐生物 · 销售培训</title>\n'
            '<style>\n' + CSS + SB_CSS + '\n</style>\n</head>\n<body>\n'
            '<main>\n<div class="wrap">\n'
            '<nav class="toc"><h4>目录</h4>\n'
            '<a href="#s0"><i>✦</i>开篇 · 为什么谈判决定成败</a>\n'
            '<div class="mt">五章</div>\n'
            '<a href="#s1"><i>1</i>需求探询</a>\n'
            '<a href="#s2"><i>2</i>上桌三条件</a>\n'
            '<a href="#s3"><i>3</i>开价与让步</a>\n'
            '<a href="#s4"><i>4</i>探寻底价</a>\n'
            '<a href="#s5"><i>5</i>异议处理</a>\n'
            '<div class="mt">动手</div>\n'
            '<a href="#s6"><i>6</i>🧪 互动沙盒 · 对话演练</a>\n'
            '<div class="mt">收尾</div>\n'
            '<a href="#s7"><i>✦</i>一页速查卡</a>\n'
            '<a href="#s8"><i>✦</i>上桌前自检清单</a>\n'
            '<a href="#s9"><i>✦</i>自问自答与出处</a>\n'
            '</nav>\n'
            '<div class="content">\n'
            + body +
            '\n</div>\n</div>\n</main>\n'
            '<script>\n' + JS + '\n' + sb_js + '\n</script>\n</body>\n</html>\n')
    return html


if __name__ == '__main__':
    raw = build_html()
    assert raw.count('</body>') == 1, '页内出现多个 </body>'
    with io.open(RAW, 'w', encoding='utf-8') as f:
        f.write(raw)
    print('raw written:', RAW, len(raw), 'chars')
    print('svg count in raw:', raw.count('<svg'))

    sys.path.insert(0, os.path.join(W, '脚本'))
    import brand_kit as BK
    out = BK.inject(raw, theme='dark', tag='销售培训 · 全员',
                    footer_extra='内部培训资料 · 培训对象：全体销售 · 内容整理自外部培训课件《攻心为上》'
                                 '（原文重排，未添加实质内容）· 生成于 ' + TODAY)
    v = BK.verify(out)
    assert all(v.values()), '品牌自检未过: %s' % v
    with io.open(FINAL, 'w', encoding='utf-8') as f:
        f.write(out)
    print('FINAL written:', FINAL, len(out), 'chars')
    for k, ok in v.items():
        print('   ', 'OK ' if ok else 'BAD', k)
