#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
《价格谈判培训 · 攻心为上》横向翻页式 HTML deck 生成器
================================================================
· 单文件、零外部依赖（无 CDN / 无字体外链 / 无图床）
· 瑞士国际主义风（无衬线 + 12 栅格 + 单一高亮色 IKB）
· 横向翻页（键盘 ← → 空格 Home End / 滚轮 / 触控）+ 页码 + 章节幕封 + 目录
· 演讲者视图（S 键：当前页 + 下一页预览 + 逐页讲稿备注 + 计时器）+ 观众屏同步
· 5 章结构 + 3 场景互动沙盒（纯内存）+ 速查卡 + 自检清单 + 自问自答
· 品牌套件（脚本/brand_kit.py）注入页眉 Logo + 页脚 slogan/联系方式
用法：python3 _build_price_deck.py            # 生成
      python3 _build_price_deck.py --verify    # 生成后自动 brand_kit --verify
"""
import base64
import io
import json
import os
import re
import sys

W = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
OUT = os.path.join(W, '培训与上手', '培训', '价格谈判培训_攻心为上_deck.html')
sys.path.insert(0, os.path.join(W, '脚本'))
import brand_kit as BK  # noqa: E402

# ──────────────────────────────────────────────────────────────
# 内容点名称表：把"出处"口径改成内容点口径（沙盒反馈、速查卡统一用）
# ──────────────────────────────────────────────────────────────
PT = {
    3: '思维定型', 6: '客户说不需要', 7: '人性两个弱点', 8: '谁更懂客户需求',
    9: '客户往往说不清想要什么', 11: '需求类型', 13: '谈判无所不在',
    15: '谈判不是讨价还价', 16: '动态应对', 17: '客户为什么不谈',
    18: '上桌三条件', 19: '有问题存在', 20: '认可你能解决问题',
    21: '你是最优选择', 22: '均衡', 23: '审时度势', 24: '开价三原则',
    25: '不接第一次出价 / 故作惊讶', 27: '案例提问', 28: '中间价位试探',
    29: '次级产品', 30: '其他供应方', 32: '建立正确认知', 33: '真正的原因分析',
    34: '异议三大原因', 35: '异议三大策略', 36: '解决思路与模型', 37: '五要素模型',
}


def _points(nums):
    seen, out = set(), []
    for n in nums:
        nm = PT.get(int(n))
        if nm and nm not in seen:
            seen.add(nm)
            out.append(nm)
    return out


def ref_to_point(s):
    """'原稿 p16' → '看点 · 动态应对'；'原稿 p28 · p30' → '看点 · 中间价位试探 + 其他供应方'"""
    nums = re.findall(r'p\s*(\d+)', s)
    pts = _points(nums)
    return '看点 · ' + ' + '.join(pts) if pts else '看点'


def strip_prov(s):
    """把正文里的「原稿 pXX」引用整体替换为内容点说法，并清掉出处性措辞。"""
    def rep(m):
        pts = _points(re.findall(r'\d+', m.group(0)))
        return '（看点 · %s）' % ' + '.join(pts) if pts else ''
    s = re.sub(r'（?原稿\s*p\s*\d+(?:\s*[·\-–—,、/]\s*p?\s*\d+)*\s*）?', rep, s)
    s = s.replace('原稿此处表述存疑', '本页表述待现场确认')
    s = s.replace('原稿仅方法名', '本页只给方法名')
    s = s.replace('原稿仅列策略名', '本页只列策略名')
    s = s.replace('原稿未列', '另有组合')
    s = s.replace('原稿未给出类型名', '本页未给类型名')
    s = s.replace('原稿', '本页')
    return s


# ──────────────────────────────────────────────────────────────
# 沙盒数据（三条场景 · 纯内存 · 反馈已统一为「看点」口径）
# ──────────────────────────────────────────────────────────────
SC = json.load(io.open(os.path.join(W, '培训与上手', '培训', '_deck_sandbox.json'), encoding='utf-8'))
SC_JSON = json.dumps(SC, ensure_ascii=False).replace('</', '<\\/')

# ──────────────────────────────────────────────────────────────
# SVG 组件：方框等宽等间距（所有横向方框统一 W=280 / gap=20）
# ──────────────────────────────────────────────────────────────
BW, BGAP, BM = 280, 20, 20            # box width, gap, margin
VBW = BM * 2 + BW * 3 + BGAP * 2      # 920


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


def _txt(x, y, s, size, fill, weight=400, anchor='middle', ls=0):
    return ('<text x="%g" y="%g" font-size="%g" fill="%s" font-weight="%d" '
            'text-anchor="%s"%s>%s</text>') % (
        x, y, size, fill, weight, anchor,
        (' letter-spacing="%g"' % ls) if ls else '', _esc(s))


def svg_fig(svg_id, title, caption, body, vbw=920, vbh=260):
    return (
        '<figure class="fig" data-fig="%s">'
        '<div class="fig-t">%s</div>'
        '<div class="fig-b"><svg class="sv" viewBox="0 0 %d %d" role="img" '
        'aria-label="%s" xmlns="http://www.w3.org/2000/svg">%s</svg></div>'
        '%s</figure>'
    ) % (svg_id, _esc(title), vbw, vbh, _esc(title), body,
         ('<figcaption class="fig-c">%s</figcaption>' % caption) if caption else '')


def box3(idname, y, h, items, accent_last=False):
    """一行 3 个等宽等间距方框。items: [(kicker, name, sub), ...]"""
    out = []
    for i, (k, nm, sub) in enumerate(items):
        x = BM + i * (BW + BGAP)
        cls = 'ebox accent' if (accent_last and i == len(items) - 1) else 'ebox'
        out.append('<rect class="%s" data-box="%s-%d" x="%d" y="%d" width="%d" height="%d"/>'
                   % (cls, idname, i + 1, x, y, BW, h))
        cy = y
        if k:
            out.append(_txt(x + BW / 2, cy + 46, k, 22, '#002FA7', 600))
            cy += 30
        out.append(_txt(x + BW / 2, cy + 52, nm, 19, '#0a0a0a', 600))
        if sub:
            out.append(_txt(x + BW / 2, cy + 82, sub, 14, '#4a4a4a', 400))
    return ''.join(out)


def fig_types():
    body = box3('t', 25, 145, [
        ('', '完全明确型', '有需求 · 有标准'),
        ('', '半明确型', '有需求 · 没标准'),
        ('', '不明确型', '没需求 · 没标准'),
    ])
    return svg_fig('fig-types', '需求类型矩阵', '另有「没需求 · 有标准」的组合，本页未给类型名。',
                   body, VBW, 195)


def fig3():
    body = box3('c', 25, 145, [
        ('①', '有问题', '满足于现状的客户不会上桌'),
        ('②', '你能解决问题', '对方认可你的能力与价值'),
        ('③', '你是最优选择', '有优势、且难以被替代'),
    ])
    return svg_fig('fig-cond', '三大条件 · 一图对照', '', body, VBW, 195)


def fig_open():
    out = []
    for i, x in enumerate((BM, BM + 440 + BM)):
        out.append('<rect class="ebox" data-box="o-%d" x="%d" y="40" width="440" height="180"/>' % (i + 1, x))
    out.append(_txt(BM + 220, 78, '开价策略：先锁定，后调整', 17, '#002FA7', 600))
    out.append('<line class="track" x1="%d" y1="132" x2="%d" y2="132"/>' % (BM + 40, BM + 400))
    out.append('<line class="tick" x1="%d" y1="122" x2="%d" y2="142"/>' % (BM + 40, BM + 40))
    out.append('<line class="tick" x1="%d" y1="122" x2="%d" y2="142"/>' % (BM + 400, BM + 400))
    out.append(_txt(BM + 70, 166, '实价', 14, '#4a4a4a', 500))
    out.append(_txt(BM + 370, 166, '开价', 14, '#4a4a4a', 500))
    out.append(_txt(BM + 220, 194, '中间即可让步区间', 14, '#4a4a4a', 400))
    x2 = BM + 440 + BM
    out.append(_txt(x2 + 220, 78, '折中策略', 17, '#002FA7', 600))
    out.append('<line class="track" x1="%d" y1="132" x2="%d" y2="132"/>' % (x2 + 40, x2 + 400))
    out.append('<circle class="dot" cx="%d" cy="132" r="9"/>' % (x2 + 220))
    out.append(_txt(x2 + 70, 166, '我方', 14, '#4a4a4a', 500))
    out.append(_txt(x2 + 370, 166, '对方', 14, '#4a4a4a', 500))
    out.append(_txt(x2 + 220, 194, '差距大时取中间值', 14, '#4a4a4a', 400))
    return svg_fig('fig-open', '开价区间与折中', '折中能实现心理平衡，但尽量不要先做让步的那方。',
                   ''.join(out), 920, 260)


def fig_probe():
    body = box3('p', 25, 145, [
        ('①', '中间价位试探', '报出中间价位'),
        ('②', '次级产品', '换较低一档试探'),
        ('③', '其他供应方', '引入参照系比较'),
    ])
    return svg_fig('fig-probe', '三种试探法', '对方不肯说，就自己试出来。',
                   body, VBW, 195)


def fig_obj():
    out = []
    rows = [('r1', 20, [('客户自身', '没时间/没需求/没权限'),
                        ('销售人员', '信任 / 关系 / 能力'),
                        ('产品问题', '误解/怀疑/短板/口碑')]),
            ('r2', 190, [('不理他', ''), ('理一理', ''), ('反问他', '')])]
    for key, y, items in rows:
        for i, (nm, sub) in enumerate(items):
            x = BM + i * (BW + BGAP)
            out.append('<rect class="ebox" data-box="%s-%d" x="%d" y="%d" width="%d" height="115"/>'
                       % (key, i + 1, x, y, BW))
            out.append(_txt(x + BW / 2, y + 52, nm, 19, '#0a0a0a', 600))
            if sub:
                out.append(_txt(x + BW / 2, y + 82, sub, 14, '#4a4a4a', 400))
    out.append('<line class="track" x1="460" y1="140" x2="460" y2="172"/>')
    out.append('<polygon class="arrow" points="460,182 452,170 468,170"/>')
    out.append(_txt(460, 165, '先分诊，再出招', 14, '#4a4a4a', 500))
    return svg_fig('fig-obj', '原因 → 策略', '', ''.join(out), 920, 330)


def fig_model():
    """五要素：一行 5 个等宽方框（不再内置与页面主标题重复的黑色条幅）。"""
    out = []
    names = ['问题', '动机', '信息', '规则', '利益']
    for i, nm in enumerate(names):
        x = 20 + i * 180
        out.append('<rect class="ebox" data-box="m-%d" x="%d" y="25" width="160" height="150"/>'
                   % (i + 1, x))
        out.append(_txt(x + 80, 73, '%02d' % (i + 1), 26, '#002FA7', 600))
        out.append(_txt(x + 80, 127, nm, 20, '#0a0a0a', 600))
    return svg_fig('fig-model', '五要素 · 过一遍关键问题', '', ''.join(out), 920, 195)


FIGS = {'types': fig_types(), 'cond': fig3(), 'open': fig_open(),
        'probe': fig_probe(), 'obj': fig_obj(), 'model': fig_model()}


# ──────────────────────────────────────────────────────────────
# 案例插画 / 程序化装饰（零外链：SVG 以 base64 data URI 内嵌）
# ──────────────────────────────────────────────────────────────
IMGDIR = os.path.join(W, '培训与上手', '培训', '_img_price_deck')
_IMG_CACHE = {}


def img_uri(name):
    """内嵌插图（零外链）。优先级：MiniMax 出图 → 智谱 CogView 出图 → 程序化 SVG。
    全部走 data: URI base64，不落盘、无外链。"""
    if name not in _IMG_CACHE:
        for fn, mime in (('mm_%s_clean.webp' % name, 'image/webp'),
                         ('ai_%s_clean.webp' % name, 'image/webp'),
                         (name + '.svg', 'image/svg+xml')):
            p = os.path.join(IMGDIR, fn)
            if os.path.exists(p):
                with io.open(p, 'rb') as fh:
                    _IMG_CACHE[name] = 'data:%s;base64,%s' % (
                        mime, base64.b64encode(fh.read()).decode('ascii'))
                break
        else:
            raise IOError('插图缺失: %s' % name)
    return _IMG_CACHE[name]


def illus(name, cap='', alt='商务概念插画'):
    return ('<figure class="illus"><img src="%s" alt="%s" width="1024" height="683" '
            'decoding="sync">%s</figure>') % (
        img_uri(name), alt, ('<figcaption>%s</figcaption>' % cap) if cap else '')


def deco_svg(anchor='tr'):
    """程序化装饰 SVG：同心弧 + 点阵（纯装饰，位于文字之下）"""
    d = ''.join('<circle cx="%d" cy="%d" r="2.2"/>' % (26 + i * 22, 30 + j * 22)
                for i in range(4) for j in range(3))
    cx = 0 if anchor == 'tl' else 320
    arcs = ''.join('<circle cx="%d" cy="0" r="%d"/>' % (cx, r) for r in (92, 150, 208))
    return ('<svg class="deco deco-%s" viewBox="0 0 320 320" aria-hidden="true" '
            'focusable="false" xmlns="http://www.w3.org/2000/svg">'
            '<g fill="none" stroke="currentColor" stroke-width="1.4" opacity=".42">%s</g>'
            '<g fill="currentColor" opacity=".5">%s</g></svg>') % (anchor, arcs, d)


# ──────────────────────────────────────────────────────────────
# 页面构造
# ──────────────────────────────────────────────────────────────
def slide(sid, layout, theme, inner, raw=False):
    cls = 'slide %s' % theme
    return ('<section class="%s" data-slide-id="%s" data-layout="%s" data-title="%s">%s</section>'
            % (cls, sid, layout, _SLIDE_TITLES[sid], inner))


_SLIDE_TITLES = {}
_SLIDES = []
_NOTES = []


def add(sid, layout, theme, title, kick, body, note, minutes=0.8, section=None, purpose=None,
        transition=''):
    _SLIDE_TITLES[sid] = title
    _SLIDES.append(slide(sid, layout, theme, body))
    _NOTES.append({'id': sid, 'title': title, 'section': section or kick, 'minutes': minutes,
                   'purpose': purpose or ('讲清「%s」这一页要观众记住什么。' % title),
                   'talk': note, 'transition': transition or '承接下一页的内容。'})


def pad(kick, h1, sub='', extra='', padcls=''):
    h = '<div class="pad %s">' % padcls
    if kick:
        h += '<div class="kick">%s</div>' % kick
    if h1:
        h += '<h1 class="t">%s</h1>' % h1
    if sub:
        h += '<p class="lead">%s</p>' % sub
    h += extra + '</div>'
    return h


def cards_html(items, n, cls='card', group=''):
    return '<div class="cards c%d %s">%s</div>' % (
        n, group, ''.join('<div class="%s"><div class="ct">%s</div><div class="cd">%s</div></div>'
                          % (cls, t, d) for t, d in items))


# ── 1 封面 ───────────────────────────────────────────────────
add('cover', 'SWISS-COVER-ASCII', 'accent',
    '价格谈判培训 · 攻心为上', '',
    deco_svg('tr') + pad('SALES · STRATEGY · TACTICS', '价格谈判培训<br>攻心为上',
        '大客户谈判与商机挖掘 —— 商机洞察 ｜ 需求探询 ｜ 价值博弈 ｜ 谈判策略',
        '<div class="cover-meta"><span>宝锐生物 · 销售培训</span><span>培训对象：全体销售</span></div>'
        '<div class="chips">需求探询 · 上桌三条件 · 开价与让步 · 探寻底价 · 异议处理 · 互动沙盒</div>',
        padcls='cover'),
    ['开场用一句话定调：很多单子不是输在产品上，是输在桌上那几句没谈成。',
     '交代今天的主线：五章方法 + 三段沙盒演练 + 一张上桌速查卡。',
     '提醒大家全程可以随时按 S 看讲稿、按 O 跳章。'],
    minutes=1.0, section='开场', purpose='让全场知道今天不是听概念，而是带走一套能上桌用的动作。',
    transition='先看目录，让大家知道今天走哪五步。')

# ── 2 目录 ───────────────────────────────────────────────────
add('agenda', 'S01', 'light', '目录 · 今天走这五步', 'AGENDA',
    pad('AGENDA', '今天走这五步',
           '每一章都回答一个上桌就会遇到的问题。',
           '<ol class="toc-list">'
           '<li><span class="no">01</span><b>需求探询</b><i>客户说「不需要」背后的真相</i></li>'
           '<li><span class="no">02</span><b>上桌三条件</b><i>怎么把客户拉上谈判桌</i></li>'
           '<li><span class="no">03</span><b>开价与让步</b><i>先手怎么出、什么时候不能接</i></li>'
           '<li><span class="no">04</span><b>探寻底价</b><i>对方不说，就自己试出来</i></li>'
           '<li><span class="no">05</span><b>异议处理</b><i>三大原因 → 三大策略 → 五要素模型</i></li>'
           '<li><span class="no">06</span><b>沙盒 · 速查卡 · 自检</b><i>三段对话演练，一页上桌清单</i></li>'
           '</ol>'),
    ['按顺序过一遍六个板块，让大家对节奏有预期。',
     '强调第 6 块是动手环节：三段真实会遇到的客户原话，现场选怎么接。'],
    minutes=0.6, transition='先从最容易卡住的地方讲起。')

# ── 3 开篇：三个最容易卡住的地方 ──────────────────────────────
add('intro-stuck', 'S04', 'light', '开篇 · 三个最容易卡住的地方', '开篇 · WHY',
    pad('开篇 · WHY', '很多单子不是输在产品上',
           '是输在「没谈成桌上那几句」。先看看我们最容易卡住的三个地方。',
           cards_html([
               ('客户说「不需要」',
                '我们总是假定所有客户都需要自己的产品（服务），以为只要把产品的好处尽量展现，就能打动对方。'),
               ('客户压根不跟我们谈',
                '满足于现状的客户，不会和你走上谈判桌。没有分歧、没有可协商的问题，就没有谈判的必要。'),
               ('谈判变成了讨价还价',
                '谈判不是简单的讨价还价，也不是尔虞我诈。大多数商务人士简单粗暴地认为，谈判就是摆明自己的立场。'),
           ], 3)),
    ['先问大家最近有没有卡在这三个地方的单子，让现场自己举手。',
     '三个都不是产品问题，是「桌上怎么说」的问题——这是今天要解决的对象。'],
    minutes=1.1, transition='三张卡背后其实是同一件事：谈判到底是什么。')

# ── 4 本质：一句话 ───────────────────────────────────────────
add('intro-essence', 'S09', 'dark', '开篇 · 一句话抓住本质', '开篇 · 本质',
    pad('开篇 · 本质', '',
           '',
           '<blockquote class="mega-q">谈判是一个根据对方的决策不断调整自己对策的过程。<br>'
           '它并非是明牌斗地主，而是根据对手的出牌来调整自己的出牌顺序。</blockquote>'
           '<div class="mini3">'
           '<div><b>思维定型，比失败更可怕</b><span>摆脱束缚，先从思维开始。</span></div>'
           '<div><b>谈判，无所不在</b><span>它不是某个环节，而是贯穿整条沟通线。</span></div>'
           '<div><b>双赢谈判其实是追求均衡</b><span>不是谁说服谁，而是回到双方都接受的均衡点。</span></div>'
           '</div>'),
    ['这句话是整场的定盘星：谈判是动态的，不是把条件摊开喊到底。',
     '点出「明牌斗地主」这个比喻——把牌全亮出来，就只剩比价。',
     '顺便预告结尾会回到这句话收口。'],
    minutes=1.2, purpose='用一句最凝练的话，把「谈判」从比价升级成动态应对。',
    transition='定了调，再说学完这五章能拿到什么。')

# ── 5 学完得到什么 ───────────────────────────────────────────
add('intro-gains', 'S16', 'light', '开篇 · 学完这五章你会得到什么', '开篇 · 收获',
    pad('开篇 · 收获', '学完这五章，你会得到什么',
           '五个动作，每一个都能在下次拜访前准备好。',
           cards_html([
               ('需求探询', '听懂客户说「不需要」背后的真相。'),
               ('上桌三条件', '把「不谈」的客户，拉上谈判桌。'),
               ('开价与让步', '先手怎么出、什么时候不能接。'),
               ('探寻底价', '三种试探法，把对方的心理价位问出来。'),
               ('异议处理', '三大原因 → 三大策略 → 五要素模型。'),
           ], 3)
           + '<div class="note-strip">本材料为宝锐生物内部销售培训教材，用于全体销售课前与上桌前查阅。</div>'),
    ['这一页是给大家的「出门条」：五条对照自己的短板挑一条最想练的。',
     '说明材料是我们自己的培训教材，课后可留存反复看。'],
    minutes=0.7, transition='进入第一章：需求到底是怎么一回事。')

# ── 章节幕封工厂 ─────────────────────────────────────────────
def chapter(num, sid, title, sub, body_kick=''):
    add(sid, 'SWISS-COVER-ASCII', 'accent', '第 %s 章 · %s' % (num, title), '',
        deco_svg('tl') + pad('CHAPTER %s' % num, title, sub,
                             '<div class="ch-num">%s</div>' % num, padcls='chapter'),
        body_kick, minutes=0.4, section='第 %s 章' % num,
        purpose='翻页信号：进入新一章，让观众把注意力重新集中。',
        transition='进入本章第一个问题。')



# ── 第 1 章内容 ─────────────────────────────────────────────
chapter('1', 'ch1', '需求探询', '客户说「不需要」背后的真相 —— 先弄明白一件事：什么是需求？谁更懂客户的需求？',
        ['换章时不要停太久，一句话点题：先搞清需求是什么。',
         '可以顺口问一句：客户说的「不需要」，有几次是真的不需要？'])

add('need-question', 'S09', 'dark', '灵魂之问：什么是需求？', 'CH.1 需求探询',
    pad('CH.1 · 灵魂之问', '什么是需求？',
           '先回答这个问题，再谈怎么卖。',
           '<blockquote class="mega-q small">我们总是假定所有的客户都需要自己的产品（服务），'
           '以为只要把产品能提供的好处尽量展现就能打动对方。</blockquote>'
           '<div class="note-strip dark">客户说「不需要」，多半不是真的没有需求，而是我们没有把需求问出来。</div>'),
    ['先不给答案，把「什么是需求」这个问题抛给现场。',
     '接着念那句话：我们总假定客户需要，于是急着展示好处——这正是最常见的错位。'],
    minutes=1.0, transition='不知道需求长什么样，就先看人性里最容易被撬动的两点。')

add('need-weakness', 'S08', 'light', '人性的两个弱点', 'CH.1 人性',
    pad('CH.1 · 人性', '人性的两个弱点',
           '客户嘴上说的是「不需要」，驱动他的往往只有这两样。',
           '<div class="duo">'
           '<div class="duo-h"><div class="ct">恐惧</div><div class="cd">害怕出问题</div>'
           '<ul class="ul"><li>怕换错、怕担责</li><li>怕现在稳定的东西被打乱</li></ul></div>'
           '<div class="duo-h"><div class="ct">贪婪</div><div class="cd">想要更好的</div>'
           '<ul class="ul"><li>想降本、想提效</li><li>想比同行先拿到新东西</li></ul></div>'
           '</div>'
           '<div class="note-strip">小问题：是客户了解他的需求？还是我们更了解客户的需求？</div>'),
    ['恐惧和贪婪是两条不同的切入口——同一句「不需要」，撬法完全不同。',
     '落到下面那个小问题上：谁更了解客户的需求？这个问题的答案决定你是迎合还是影响。'],
    minutes=1.1, transition='答案往往让人不太舒服，看一位公认的产品天才怎么说的。')

add('need-jobs', 'S03', 'dark', '客户往往说不清自己想要什么', 'CH.1 引述',
    pad('CH.1 · 引述', '', '',
           '<div class="split">'
           '<div class="split-l"><h2 class="t2">人们往往<br>不知道自己<br>想要什么</h2>'
           + illus('jobs_cart', '概念 · 更快的马 → 汽车') + '</div>'
           '<div class="split-r"><blockquote class="q">有些人说：「客户想要什么就给他们什么」，'
           '但那不是我的方式。我们的责任是提前一步搞清楚将来他们想要什么？<br><br>'
           '我记得亨利·福特曾说过：「如果我最初问客户想要什么？」他们应该会告诉我：'
           '「想要一辆更好的马车！」</blockquote>'
           '<div class="src-line">——《乔布斯传》 Steve Jobs</div></div></div>'),
    ['用福特和马车那个例子讲透：客户描述的是现状，不是他真正要的东西。',
     '所以我们不能只做「迎合」——回答客户已经说出来的话，而要往上走一步做「影响」。'],
    minutes=1.2, transition='那迎合和影响到底是什么关系？下一页只放两个词。')

add('need-fit', 'S08', 'light', '迎合 与 影响', 'CH.1 姿态',
    pad('CH.1 · 姿态', '迎合 与 影响',
           '面对同一个需求，我们的姿态只有这两种。',
           '<div class="side-2"><div class="duo duo-stack col-l">'
           '<div class="duo-h"><div class="ct">迎合</div><div class="cd">客户说什么，我们就满足什么</div></div>'
           '<div class="duo-h accent-k"><div class="ct">影响</div>'
           '<div class="cd">先一步把客户还没想到的需求讲清楚</div></div>'
           '</div>'
           + illus('scene_weigh', '场景 · 左右权衡') + '</div>'
           '<div class="note-strip">本页只放这两个词：你做单子时，更常做的是哪一个？</div>'),
    ['不要替大家解释这两个词的关系，直接让现场自己认领：你更常做哪个。',
     '提醒一句：只会迎合，就永远在被别人已经写好的标准里比价。'],
    minutes=1.0, transition='要影响，先得分清客户处在哪一种需求状态。')

add('need-types', 'S15', 'light', '客户的需求类型', 'CH.1 分类',
    pad('CH.1 · 分类', '客户的需求类型',
           '按「有没有需求」「有没有标准」分成三类，才知道该用哪套谈法。',
           FIGS['types']),
    ['教大家用两个问题现场给客户分类：有没有需求、有没有标准。',
     '强调分类不是贴标签，而是决定下一步是「帮他把标准找出来」还是「直接对着标准报价」。'],
    minutes=1.1, purpose='给出一把现场就能用的分类尺子：三类客户三种谈法。',
    transition='第 1 章到这里。下一章回答：凭什么把客户拉上谈判桌。')

# ── 第 2 章内容 ─────────────────────────────────────────────
chapter('2', 'ch2', '上桌三条件', '怎么把客户拉上谈判桌 —— 不是所有客户都愿意谈，先让他有理由坐到桌前。',
        ['点题：谈判不是想谈就能谈，得先有上桌的条件。',
         '这一章讲的就是那三个条件。'])

add('table-why', 'S09', 'accent', '为什么需要谈判？', 'CH.2 上桌',
    pad('CH.2 · 上桌', '谈判，无所不在',
           '它不是签约前的那一段，而是贯穿整条沟通线。',
           '<div class="side-2"><div class="col-l">'
           '<blockquote class="mega-q inv">谈判不是简单的讨价还价，也不是尔虞我诈。</blockquote>'
           '</div>' + illus('scene_tug', '场景 · 谈判桌上的博弈') + '</div>'),
    ['先破一个误解：很多人以为谈判就是最后一轮砍价，其实它从第一次接触就开始了。',
     '点出这句话：谈判不是讨价还价，也不是尔虞我诈。'],
    minutes=0.9, purpose='把「谈判」从一个环节，扩展成整条沟通线。',
    transition='那为什么很多谈判谈不成？下页给答案。')

add('table-fail', 'S12', 'light', '为什么你的谈判不成功', 'CH.2 症结',
    pad('CH.2 · 症结', '', '',
           '<div class="manifesto">'
           '<h2 class="t2">所谓谈判不是简单的亮明条件和利益</h2>'
           '<p class="lead">往往大多数商务人士简单粗暴地认为，谈判就是摆明自己的立场。'
           '然而事实是：谈判是一个根据对方的决策不断调整自己对策的过程。</p>'
           '<div class="ink-banner">它并非是明牌斗地主，而是根据对手的出牌来调整自己的出牌顺序。</div>'
           '</div>'),
    ['把「亮明条件」和「调整对策」对比着讲，这是两种完全不同的动作。',
     '举一个自己单子里的例子：条件摊开之后，还能不能改？不能改就是明牌。'],
    minutes=1.1, transition='既然不能明牌，那第一步是先说服客户坐下来谈。')

add('table-three', 'S18', 'dark', '走上谈判桌的三大条件', 'CH.2 三条件',
    pad('CH.2 · 三条件', '走上谈判桌的三大条件',
           '三条都成立，谈判才谈得起来；缺一条，桌上就只剩价格。',
           FIGS['cond']),
    ['逐条念，然后强调「三个都成立」——只有一条成立谈不起来。',
     '现场可以拿一个正在跟的单子，对一遍这三条缺哪条。'],
    minutes=1.2, purpose='给出上桌的判定标准，让销售知道「没得谈」到底缺什么。',
    transition='先看第一条，也是最容易被忽略的一条。')

add('table-c1', 'S08', 'light', '① 有问题存在', 'CH.2 条件一',
    pad('CH.2 · 条件一', '① 有问题存在',
           '满足于现状的客户，不会和你走上谈判桌。',
           '<div class="duo">'
           '<div class="duo-h"><div class="ct">01 问题的必要性</div>'
           '<div class="cd">存在问题或分歧是走上谈判桌的首要条件，没有问题就没有谈判的必要。'
           '问题可以是利益冲突、目标差异或信息不对称，这些问题促使双方寻求解决方案。</div></div>'
           '<div class="duo-h"><div class="ct">02 提示</div>'
           '<div class="cd">问题必须是可沟通协商解决的，否则谈判无法进行。'
           '对于不可调和的矛盾，如价值观的根本冲突，谈判可能难以达成一致。</div></div>'
           '</div>'),
    ['关键动作：问题必须是客户自己认的，不能是我们单方面说的。',
     '顺带提醒，有些矛盾不可调和——这类客户要早点识别，别硬耗。'],
    minutes=1.1, transition='有了问题，还得让对方相信你解决得了。')

add('table-c23', 'S04', 'light', '② 你能解决问题　③ 你是最优选择', 'CH.2 条件二三',
    pad('CH.2 · 条件二 · 三', '② 你能解决问题　③ 你是最优选择',
           '对方认可，才叫条件成立——自说自话不算。',
           cards_html([
               ('能力展现', '展现出解决问题的能力，让对方相信你有实力满足其需求。可以是专业知识、资源掌控或过往成功经验。'),
               ('价值体现', '你的价值要被对方认可，即你能为对方带来利益或好处，这是对方愿意与你谈判的关键。'),
               ('竞争优势', '在众多可选择的对象中具备优势（成本、效率、信誉等），让对方觉得与你合作更有利。'),
               ('独特性', '你提供的解决方案或价值难以被替代（创新技术、稀缺资源、个性化服务），增加对方选择你的意愿。'),
           ], 4)),
    ['四个词分两组讲：前两个解决「信不信你」，后两个解决「为什么是你」。',
     '强调「被对方认可」这四个字：我们自己觉得强没用。'],
    minutes=1.2, transition='三条凑齐之后，谈判的目标是什么？下一页给定义。')

add('table-balance', 'S12', 'accent', '均衡：双赢谈判到底是什么', 'CH.2 均衡',
    pad('CH.2 · 均衡', '', '',
           '<div class="side-2"><div class="col-l center">'
           '<h2 class="mega-word">均衡</h2>'
           '<div class="ink-banner light-banner">所谓的双赢谈判，其实是追求均衡。</div>'
           '</div>' + illus('scene_deal', '场景 · 合作达成') + '</div>'),
    ['纠正一个常见误解：双赢不是两个人都满意，而是回到双方都能接受的均衡点。',
     '收束第 2 章：上桌靠条件，谈成靠均衡。'],
    minutes=0.9, transition='第 2 章到这里。下一章进入最容易失手的一步：开价。')

# ── 第 3 章内容 ─────────────────────────────────────────────
chapter('3', 'ch3', '开价与让步', '先手怎么出、什么时候不能接 —— 审时度势，先手才在你这儿。',
        ['点题：开价是整场谈判里最难改的一次动作，出去了就收不回。',
         '提醒这一章有两组原则：怎么出价、什么时候不能接。'])

add('open-strategy', 'S06', 'light', '审时度势 · 开价策略', 'CH.3 开价',
    pad('CH.3 · 开价', '审时度势 · 开价策略',
           '开价阶段有两组核心原则：一组是「怎么出价」，一组是「什么时候不能接」。',
           FIGS['open']),
    ['先讲左边的区间图：实价与开价之间的那段，就是留给后面的让步空间。',
     '再讲右边折中：差距太大时用中间值找回心理平衡，但主动权在谁先让。'],
    minutes=1.3, purpose='把「开价」从一个动作，变成一次有预谋的区间设计。',
    transition='具体的原则，下一页逐条列出来。')

add('open-3', 'S19', 'light', '开价阶段的核心原则（一）', 'CH.3 三原则',
    pad('CH.3 · 三原则', '开价阶段的核心原则（一）',
           '三条动作，出价前就要想好。',
           cards_html([
               ('先锁定，后调整', '先出招优势明显，能引导对方预期，掌控谈判节奏。'),
               ('开价策略', '开价略高于实价，设置可让步区间。'),
               ('折中策略', '双方差距大时，通过折中实现心理平衡，如价格谈判中取中间值。尽量不要先做让步的那方。'),
           ], 3, group='accent-one')),
    ['逐条念，重点落在第 3 条的末句：不要先做让步的那方。',
     '提醒大家出价前把这三条写进拜访准备里。'],
    minutes=1.2, transition='出价之后，对方的第一次报价怎么接？')

add('open-2', 'S08', 'dark', '开价阶段的核心原则（二）', 'CH.3 接价',
    pad('CH.3 · 接价', '开价阶段的核心原则（二）',
           '对方先出价的时候，这两条决定你不会吃亏。',
           '<div class="side-2"><div class="duo duo-stack col-l">'
           '<div class="duo-h"><div class="ct">千万不要接受对方第一次出价</div>'
           '<div class="cd">心理学依据：轻易接受对方出价会引发怀疑，反而会拉高对方预期。</div></div>'
           '<div class="duo-h"><div class="ct">故作惊讶</div>'
           '<div class="cd">对对方报价表现惊讶，暗示不合理，如「这个价格远超出我们的预期」。</div></div>'
           '</div>'
           + illus('scene_price', '场景 · 价格与取舍') + '</div>'),
    ['这两条是当天就能用上的动作，现场可以两两对练一遍「故作惊讶」。',
     '解释为什么不能秒答应：秒答应会让对方觉得还有空间。'],
    minutes=1.2, transition='第 3 章到这里。下一章：怎么把对方的底价试出来。')

# ── 第 4 章内容 ─────────────────────────────────────────────
chapter('4', 'ch4', '探寻底价', '对方不肯说，就自己试出来。',
        ['点题：底价没人会主动告诉你，只能设计动作试出来。',
         '强调三种方法都是「试探」，不是直接问。'])

add('probe-case', 'S09', 'dark', '案例：报价与还价僵住了怎么办', 'CH.4 案例',
    pad('CH.4 · 案例', '', '',
           '<div class="case">'
           '<blockquote class="mega-q small">在跟客户的合作谈判中，有没有遇到我方的报价和对方的还价之间存在分歧，'
           '导致沟通无法继续的情况？</blockquote>'
           '<div class="ask">如果有——你当时是怎么处理的？</div>'
           '</div>'),
    ['先让大家想 30 秒，再请两三个人讲自己真实遇到的那一次。',
     '把现场的说法先记下来，下一页再用三种试探法对照。'],
    minutes=1.3, purpose='用现场真实案例把「卡在报价上」的状态具体化。',
    transition='遇到僵局，下一页给三种可以主动用的试探法。')

add('probe-3', 'S11', 'light', '探寻底价的三种试探法', 'CH.4 三法',
    pad('CH.4 · 三法', '探寻底价的三种试探法',
           '对方不肯说底价，就用动作试出来。',
           FIGS['probe']),
    ['三种方法依次讲，强调都是「用动作试探」，不是直接问底价多少。',
     '提醒每种方法适合的时机不同，选错时机会把需求做小。'],
    minutes=1.2, purpose='给三个可执行的试探动作，替代直接问价。',
    transition='第 4 章到这里。最后一章讲客户说「不」的时候怎么办。')

# ── 第 5 章内容 ─────────────────────────────────────────────
chapter('5', 'ch5', '异议处理', '三大原因 → 三大策略 → 五要素模型 —— 客户拒绝你，先别急着解释，先分诊。',
        ['点题：异议先分诊，再出招。',
         '预告本章最后会收在一个五要素模型上。'])

add('obj-cog', 'S03', 'dark', '处理异议之前，先建立正确认知', 'CH.5 认知',
    pad('CH.5 · 认知', '', '',
           '<div class="split">'
           '<div class="split-l"><h2 class="t2">处理异议<br>第一步不是<br>话术，是认知</h2></div>'
           '<div class="split-r"><ul class="steps">'
           '<li><span class="no">01</span><b>建立正确认知</b><span>先别急着解释，先接受「客户拒绝是常态」这件事。</span></li>'
           '<li><span class="no">02</span><b>做真正的原因分析</b><span>客户说的理由，往往不是真正的原因。</span></li>'
           '</ul></div></div>'),
    ['先破一个惯性动作：客户一说「不」，我们第一反应就是解释。',
     '正确顺序是先认知、再归因——这两步没走完，话术都是空转。'],
    minutes=1.1, transition='那就先看原因，客户拒绝到底分几类。')

add('obj-3why', 'S08', 'light', '客户拒绝 / 异议的三大原因', 'CH.5 分诊',
    pad('CH.5 · 分诊', '客户拒绝 / 异议的三大原因',
           '先分诊，再出招——三类原因，对应完全不同的动作。',
           FIGS['obj']),
    ['把三类原因和三个策略连起来看：原因不同，接法完全不同。',
     '提醒大家最容易犯的错，是把「产品问题」当「客户自身」来放掉。'],
    minutes=1.2, purpose='给出一张分诊表，让异议处理从凭感觉变成按图索骥。',
    transition='分完诊，下一页看三种接法。')

add('obj-3way', 'S13', 'light', '处理异议的三大策略', 'CH.5 三策略',
    pad('CH.5 · 三策略', '处理异议的三大策略',
           '三种接法都没有对错，只有时机对不对。',
           '<div class="forces">'
           '<div class="f"><div class="fn">①</div><div class="ct">不理他</div>'
           '<div class="cd">对方随口一提、或明显不是真正的顾虑时，先记下来不纠缠。</div></div>'
           '<div class="f"><div class="fn">②</div><div class="ct">理一理</div>'
           '<div class="cd">是客户自己认下的顾虑，就拆开、归因、给证据。</div></div>'
           '<div class="f accent-k"><div class="fn">③</div><div class="ct">反问他</div>'
           '<div class="cd">原因还不清楚时，用提问把模糊的「不行」逼成具体条件。</div></div>'
           '</div>'),
    ['三个策略按「这个顾虑值不值得接」来判断，不是按顺序用。',
     '重点讲第 3 个：反问他，是把定义问题的权力拿回来。'],
    minutes=1.2, transition='最后把所有关键问题收进一个模型。')

add('obj-model', 'S14', 'dark', '关键问题的解决思路与模型', 'CH.5 五要素',
    pad('CH.5 · 五要素', '关键问题的解决思路与模型',
           '把一个问题放进这五个格子里过一遍，思路自然就清了。',
           FIGS['model']),
    ['五个要素逐个解释：问题是什么、对方动机是什么、我们缺哪些信息、受什么规则约束、双方各自要什么利益。',
     '建议大家下次遇到复杂异议，就拿这五个格子现场列一遍。'],
    minutes=1.3, purpose='给一个可以套用的思考框架，把杂乱的信息结构化成可谈的条件。',
    transition='方法讲完了，接下来动手：三段客户原话，现场选怎么接。')

# ── 沙盒 ─────────────────────────────────────────────────────
SB = ('<div class="pad sb-pad">'
      '<div class="kick">PRACTICE · 收尾</div>'
      '<h1 class="t sm">互动沙盒 · 三段对话演练</h1>'
      '<p class="lead">看懂了不等于会接。下面三段对话，每一轮都给你几个接法，'
      '点一下就知道这一句接得对不对、对应的看点是什么。</p>'
      '<div class="sb" id="sbx">'
      '<div class="sb-h"><span class="ttl">试试看 · 客户原话怎么接</span>'
      '<span class="bdg">纯内存 · 不落盘 · 不发请求</span></div>'
      '<div class="sb-note">玩法：点一个场景 → 逐轮选一个接法 → 立刻看到「这样接为什么好 / 问题出在哪」'
      '→ 走完三轮给小结。想重来点「↺ 重来本场景」。</div>'
      '<div class="sb-tabs" id="sb-tabs"></div>'
      '<div class="sb-stage" id="sb-stage" data-scroll="1"></div>'
      '<div class="sb-btns"><button class="btn ghost" id="sb-reset">↺ 重来本场景</button>'
      '<span class="cnt" id="sb-cnt"></span></div>'
      '</div></div>')

add('sandbox', 'S17', 'light', '互动沙盒 · 三段对话演练', 'PRACTICE',
    SB,
    ['让现场举手选，不要一个人闷头点。',
     '每轮先问「你们会怎么接」，再点开看反馈；选错的反馈比选对的更值得读。',
     '三个场景都走完，再对一遍小结里的三条动作。'],
    minutes=6.0, section='互动沙盒', purpose='把前五章的方法放进真实台词里，当场练一遍手。',
    transition='练完手，下一页把五章压成一页速查卡。')

# ── 速查卡 ───────────────────────────────────────────────────
QK = [
    ('需求探询', '客户说「不需要」，多半是我们假定他需要。人性的两个弱点：恐惧（害怕出问题）／贪婪（想要更好的）。需求类型：完全明确型 · 半明确型 · 不明确型。'),
    ('上桌三条件', '① 有问题　② 你能解决问题　③ 你是最优选择。满足于现状的客户，不会和你走上谈判桌。'),
    ('开价与让步', '先锁定后调整；开价略高于实价、留可让步区间；差距大就折中取中间值，但尽量别先让步；不接受对方第一次出价；对报价要故作惊讶。'),
    ('探寻底价三法', '① 中间价位试探　② 次级产品　③ 其他供应方。'),
    ('异议三大原因', '① 客户自身（没时间/没需求/没权限/有替代）　② 销售人员（信任/关系/能力）　③ 产品问题（误解/怀疑/短板/口碑/价值塑造）。'),
    ('异议三大策略', '① 不理他　② 理一理　③ 反问他。先建立正确认知，再做真正的原因分析。'),
    ('五要素模型', '01 问题　02 动机　03 信息　04 规则　05 利益。'),
    ('一句话总纲', '谈判不是明牌斗地主，是根据对手的出牌来调整自己的出牌顺序；双赢谈判其实是追求均衡。'),
]
QK_HTML = ('<div class="qbar"><input id="qk-search" type="text" '
           'placeholder="输入关键词筛选，如：折中 / 异议 / 五要素" autocomplete="off">'
           '<span class="qhint" id="qk-count"></span></div>'
           '<div class="cards c2 qcards" id="qk-cards" data-scroll="1">'
           + ''.join('<div class="qc"><div class="ct">%s</div><div class="cd">%s</div></div>' % c
                     for c in QK)
           + '</div>'
           '<div class="empty" id="qk-empty" style="display:none">没有匹配的卡片，换个词试试。</div>')

add('cheatsheet', 'S16', 'dark', '攻心为上 · 谈判速查卡', '速查卡',
    pad('QUICK REF · 收尾', '攻心为上 · 谈判速查卡',
           '五章压缩成一页。可以截图存手机，上桌前扫一眼；搜索框能按关键词筛卡片。',
           QK_HTML),
    ['不逐条念，告诉大家这一页是留着回去用的。',
     '演示一下搜索：输入「折中」或「异议」，卡片会即时筛出来。'],
    minutes=1.0, section='收尾', purpose='把五章压成一页，方便上桌前快速回看。',
    transition='速查卡之后，还有一张真正要照着做的清单。')

CHK = ['我判断过客户属于哪一型（完全明确 / 半明确 / 不明确）',
       '我找到（或制造）了客户必须跟我谈的那个「问题」',
       '我能说清「你凭什么解决」——能力展现 + 价值体现',
       '我说得出「为什么我是最优选择」——竞争优势 + 独特性',
       '我出价前就定好了开价与可让步区间',
       '我没有在第一时间接受对方的出价，也没先做让步的那方',
       '我用了至少一种探寻底价的方法，而不是直接问「底价多少」',
       '客户有异议时，我先分诊三大原因，再决定不理他 / 理一理 / 反问他',
       '我用五要素（问题·动机·信息·规则·利益）把关键问题过了一遍']
CHK_HTML = ('<div class="chk" id="chk-list">'
            + ''.join('<button class="ci" type="button"><span class="bx"></span>'
                      '<span class="cv">%s</span></button>' % c for c in CHK)
            + '</div>'
            '<div class="note-strip">点一下就能打勾（只在本页临时记录，刷新即清空）。'
            '九项全打勾，就可以上桌了。</div>')

add('checklist', 'S19', 'light', '上桌前自检清单', '自检清单',
    pad('CHECKLIST · 收尾', '上桌前自检清单',
           '每次重要沟通前逐项过一遍——有一项没打勾，就先补。',
           CHK_HTML),
    ['让每个人现在就找一条自己最难打勾的。',
     '强调这九项是出门前的最后一道闸，不是知识考核。'],
    minutes=1.1, section='收尾', purpose='把方法变成九条出门前的硬性检查项。',
    transition='最后一页：大家最常问的八个问题。')

# ── 自问自答 ─────────────────────────────────────────────────
QA = [
    ('客户说「太贵了」，第一句该怎么接？', '故作惊讶，暗示不合理，例如「这个价格远超出我们的预期」。'),
    ('折中时最重要的分寸是什么？', '尽量不要先做让步的那方。'),
    ('探寻底价有哪三种试探法？', '① 中间价位试探　② 次级产品　③ 其他供应方。'),
    ('客户拒绝 / 异议的三大原因是？', '① 客户自身　② 销售人员　③ 产品问题。'),
    ('处理异议的三大策略是？', '① 不理他　② 理一理　③ 反问他。'),
    ('问题解决模型的五个要素是？', '01 问题　02 动机　03 信息　04 规则　05 利益。'),
    ('走上谈判桌的三大条件是？', '① 有问题　② 你能解决问题　③ 你是最优选择。'),
    ('人性的两个弱点是什么？', '恐惧（害怕出问题）／贪婪（想要更好的）。'),
]
QA_HTML = ('<div class="qa" data-scroll="1">'
           + ''.join('<div class="qa-r"><div class="qa-q">%s</div><div class="qa-a">%s</div></div>' % r
                     for r in QA)
           + '</div>')

add('qa', 'S20', 'light', '自问自答', '自问自答',
    pad('FAQ · 收尾', '自问自答',
           '八个高频问题，答案都在前面五章里，方便你回去核对。',
           QA_HTML),
    ['不逐条念，挑现场最常被问到的两三条讲。',
     '告诉大家这一页和速查卡一起留着，回去当索引查。'],
    minutes=1.0, section='收尾', purpose='用问答的形式，把最常被问到的八个点再钉一遍。',
    transition='最后回到开场那句话收口。')

add('closing', 'SWISS-CLOSING-ASCII', 'accent', '收尾 · 回去能用的三件事', 'CLOSING',
    deco_svg('tr') + pad('CLOSING', '回去之后，先做这三件事',
           '',
           '<div class="closing">'
           '<div class="cl-line"><span class="no">01</span>'
           '<b>给你的每个在谈客户分一次类</b><span>完全明确 / 半明确 / 不明确，决定下一步谈法。</span></div>'
           '<div class="cl-line"><span class="no">02</span>'
           '<b>把上桌三条件对一遍</b><span>缺哪条，就先补哪条，别急着报条件。</span></div>'
           '<div class="cl-line"><span class="no">03</span>'
           '<b>出价前写好开价与可让步区间</b><span>并把「不接第一次出价」当成硬动作。</span></div>'
           '</div>'
           '<div class="end-line">谈判不是明牌斗地主，是根据对手的出牌来调整自己的出牌顺序。</div>'),
    ['不要再用新内容收尾，只留三个明天就能做的动作。',
     '最后念一遍那句话：按对手的出牌调整自己的节奏——这就是今天要带走的全部。'],
    minutes=1.0, section='收尾', purpose='用三个具体动作收尾，确保离场就有人开始用。',
    transition='演示结束。')

# ──────────────────────────────────────────────────────────────
# 备注数据 → JS
# ──────────────────────────────────────────────────────────────
NOTES_JS = json.dumps(_NOTES, ensure_ascii=False)
TOC_JS = json.dumps([{'i': i + 1, 'id': n['id'], 't': n['title'], 's': n['section']}
                     for i, n in enumerate(_NOTES)], ensure_ascii=False)
SLIDES_HTML = ''.join(_SLIDES)
N_PAGES = len(_SLIDES)

# ──────────────────────────────────────────────────────────────
# CSS
# ──────────────────────────────────────────────────────────────
CSS = """
/* ===== 瑞士国际主义 · 单一高亮色 IKB ===== */
:root{
  --paper:#fafaf8; --paper-rgb:250,250,248;
  --ink:#0a0a0a;   --ink-rgb:10,10,10;
  --grey-1:#f0f0ee; --grey-2:#d4d4d2; --grey-3:#737373;
  --accent:#002FA7; --accent-rgb:0,47,167; --accent-on:#ffffff;
  --sans:-apple-system,BlinkMacSystemFont,"Helvetica Neue","Inter","PingFang SC",
         "Microsoft YaHei UI","Noto Sans SC",sans-serif;
  --mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0;height:100%;overflow:hidden;background:#0d0d0d;
  font-family:var(--sans);-webkit-font-smoothing:antialiased;color:var(--ink)}
body{display:flex;flex-direction:column;height:100vh}
.bior-header,.bior-footer{flex:none}
#stagewrap{flex:1 1 auto;min-height:0;position:relative;overflow:hidden;
  display:flex;align-items:center;justify-content:center;background:#0d0d0d}
#stage{position:relative;width:1280px;height:720px;flex:none;transform-origin:center center}
#track{position:absolute;inset:0;display:flex;width:100%;height:100%;
  transition:transform .5s cubic-bezier(.22,.61,.36,1)}
body.low-power #track{transition:none}
.slide{flex:0 0 1280px;width:1280px;height:720px;position:relative;overflow:hidden;
  background:var(--paper);color:var(--ink)}
.slide.dark{background:var(--ink);color:var(--paper)}
.slide.accent{background:var(--accent);color:var(--accent-on)}
.pad{position:absolute;inset:0;padding:52px 64px 44px;display:flex;flex-direction:column}
.kick{font:600 13.5px/1 var(--mono);letter-spacing:.24em;text-transform:uppercase;color:var(--accent)}
.slide.accent .kick{color:rgba(255,255,255,.78)}
.slide.dark .kick{color:rgba(250,250,248,.62)}
.kick+.t{margin-top:16px}
h1.t,.t{font-size:60px;font-weight:200;line-height:1.06;letter-spacing:-.012em;margin:0}
h1.t.sm{font-size:46px}
.t2{font-size:38px;font-weight:200;line-height:1.16;letter-spacing:-.01em;margin:0}
.lead{font-size:18px;font-weight:400;line-height:1.6;color:var(--grey-3);
  max-width:74ch;margin:16px 0 0}
.slide.dark .lead{color:rgba(250,250,248,.66)}
.slide.accent .lead{color:rgba(255,255,255,.8)}
.hair{height:1px;background:var(--grey-2);margin:20px 0 0;border:0}

/* 卡片 / 栅格 */
.cards{display:grid;gap:18px;margin-top:26px}
.cards.c2{grid-template-columns:repeat(2,1fr)}
.cards.c3{grid-template-columns:repeat(3,1fr)}
.cards.c4{grid-template-columns:repeat(4,1fr)}
.card{background:var(--grey-1);padding:20px 20px 22px}
.card .ct{font-size:19px;font-weight:600;letter-spacing:.005em}
.card .cd{font-size:15.5px;line-height:1.62;color:#3d3d3d;margin-top:10px}
.card.accent-one:last-child,.card.accent-one:nth-child(3){margin-bottom:0}
.cards.accent-one .card:nth-child(3){background:var(--accent);color:#fff}
.cards.accent-one .card:nth-child(3) .cd{color:rgba(255,255,255,.86)}
.note-strip{margin-top:auto;padding-top:14px;border-top:1px solid var(--grey-2);
  font-size:15px;line-height:1.6;color:var(--grey-3)}
.slide.dark .note-strip{border-top-color:rgba(250,250,248,.22);color:rgba(250,250,248,.66)}
.slide.accent .note-strip{border-top-color:rgba(255,255,255,.3);color:rgba(255,255,255,.8)}

/* 封面 / 章节幕封 */
.pad.cover{justify-content:flex-end}
.pad.cover h1.t{font-size:88px;line-height:1.02;font-weight:200}
.pad.chapter{justify-content:flex-end}
.ch-num{position:absolute;top:26px;right:52px;font-size:190px;font-weight:200;
  line-height:.8;letter-spacing:-.05em;color:rgba(255,255,255,.16)}
.pad.chapter h1.t{font-size:76px}
.cover-meta{display:flex;gap:26px;margin-top:26px;font-size:15px;font-weight:500;
  letter-spacing:.02em;color:rgba(255,255,255,.9)}
.chips{margin-top:12px;font-family:var(--mono);font-size:13px;letter-spacing:.08em;
  color:rgba(255,255,255,.66)}
.dotmat{position:absolute;right:56px;top:60px;width:200px;height:120px;
  background-image:radial-gradient(rgba(255,255,255,.5) 1.6px,transparent 1.7px);
  background-size:20px 20px;pointer-events:none}

/* 目录 */
.toc-list{list-style:none;margin:30px 0 0;padding:0;display:grid;grid-template-columns:1fr 1fr;gap:14px 34px}
.toc-list li{display:flex;align-items:baseline;gap:14px;padding:12px 0;border-top:1px solid var(--grey-2)}
.toc-list .no{font-family:var(--mono);font-size:14px;color:var(--accent);font-weight:600}
.toc-list b{font-size:21px;font-weight:600}
.toc-list i{font-size:15px;color:var(--grey-3);font-style:normal}

/* 大引用 / manifesto */
.mega-q{font-size:33px;font-weight:200;line-height:1.42;letter-spacing:-.005em;
  margin:auto 0;max-width:30ch;border:0;padding:0}
.mega-q.small{font-size:27px;max-width:34ch}
.mega-q.inv{font-size:40px;max-width:26ch;color:#fff}
.mega-word{font-size:150px;font-weight:200;line-height:.9;letter-spacing:-.03em;margin:0}
.mini3{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-top:auto;
  border-top:1px solid rgba(250,250,248,.22);padding-top:18px}
.mini3 b{display:block;font-size:17px;font-weight:600;margin-bottom:6px}
.mini3 span{font-size:14.5px;line-height:1.55;color:rgba(250,250,248,.66)}
.manifesto{display:flex;flex-direction:column;height:100%}
.manifesto .t2{margin-top:14px;max-width:34ch}
.manifesto .lead{margin-top:16px}
.ink-banner{margin-top:auto;background:var(--ink);color:var(--paper);padding:22px 24px;
  font-size:23px;font-weight:400;line-height:1.45}
.light-banner{background:rgba(0,0,0,.22);color:#fff;text-align:center;font-size:26px}
.manifesto.center{align-items:center;justify-content:center;text-align:center;gap:12px}
.manifesto.center .ink-banner{margin-top:18px;width:100%}

/* 双栏 / 分屏 */
.duo{display:grid;grid-template-columns:1fr 1fr;gap:22px;margin-top:28px}
.duo-h{background:var(--grey-1);padding:22px 22px 24px}
.duo-h.accent-k{background:var(--accent);color:#fff}
.duo-h .ct{font-size:26px;font-weight:600;line-height:1.24}
.duo-h .cd{font-size:16px;line-height:1.62;color:#3d3d3d;margin-top:12px}
.duo-h.accent-k .cd{color:rgba(255,255,255,.88)}
.duo.big-pair{margin-top:auto;margin-bottom:auto}
.duo.big-pair .ct{font-size:32px}
.slide.dark .duo-h{background:#171717}
.slide.dark .duo-h .cd{color:rgba(250,250,248,.72)}
.slide.dark .duo-h.accent-k{background:var(--accent)}
.slide.dark .ul li{color:rgba(250,250,248,.72)}
.slide.dark .card{background:#171717}
.slide.dark .card .cd{color:rgba(250,250,248,.72)}
.ul{margin:12px 0 0;padding-left:18px}
.ul li{font-size:15px;line-height:1.7;color:#3d3d3d}
.split{display:grid;grid-template-columns:1fr 1.15fr;gap:40px;height:100%;align-items:center}
.split-l .t2{font-size:50px}
.split-r .q{font-size:19px;line-height:1.8;margin:0;padding:0 0 0 20px;border-left:3px solid var(--accent)}
.src-line{margin-top:18px;font-family:var(--mono);font-size:13px;color:var(--grey-3)}
.slide.dark .steps li{border-top-color:rgba(250,250,248,.22)}
.slide.dark .steps span:last-child{color:rgba(250,250,248,.66)}
.slide.dark .src-line{color:rgba(250,250,248,.6)}
.slide.dark .qc{background:#171717}
.slide.dark .qc .cd{color:rgba(250,250,248,.72)}
.slide.dark .qbar input{background:#141414;color:var(--paper);border-color:rgba(250,250,248,.28)}
.slide.dark .qhint,.slide.dark .empty{color:rgba(250,250,248,.62)}
/* 深色页上的高亮色统一提亮，保证投屏可读（仍为同一 IKB 色系） */
.slide.dark .qc .ct,.slide.dark .fig-t,.slide.dark .steps .no,.slide.dark .forces .fn{color:#9db4f5}
.steps{list-style:none;margin:0;padding:0}
.steps li{display:flex;gap:16px;align-items:baseline;padding:18px 0;border-top:1px solid var(--grey-2)}
.steps .no{font-family:var(--mono);font-size:13px;color:var(--accent);font-weight:600}
.steps b{font-size:19px;font-weight:600;white-space:nowrap}
.steps span:last-child{font-size:15px;color:var(--grey-3);line-height:1.6}

/* 三力 / 案例 / 收尾 */
.forces{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-top:auto;margin-bottom:auto}
.forces .f{background:var(--grey-1);padding:22px}
.forces .f.accent-k{background:var(--accent);color:#fff}
.forces .fn{font-family:var(--mono);font-size:15px;color:var(--accent);font-weight:600}
.forces .f.accent-k .fn{color:rgba(255,255,255,.8)}
.forces .ct{font-size:24px;font-weight:600;margin-top:10px}
.forces .cd{font-size:15px;line-height:1.62;color:#3d3d3d;margin-top:10px}
.forces .f.accent-k .cd{color:rgba(255,255,255,.88)}
.case{display:flex;flex-direction:column;height:100%}
.ask{margin-top:auto;font-size:24px;font-weight:600;border-top:1px solid rgba(250,250,248,.22);padding-top:20px}
.closing{margin-top:auto;margin-bottom:auto}
.cl-line{display:grid;grid-template-columns:44px 1fr;gap:0 16px;align-items:baseline;
  padding:18px 0;border-top:1px solid rgba(255,255,255,.28)}
.cl-line .no{font-family:var(--mono);font-size:14px;color:rgba(255,255,255,.75);font-weight:600}
.cl-line b{font-size:23px;font-weight:600}
.cl-line span:last-child{grid-column:2;font-size:15.5px;color:rgba(255,255,255,.8);margin-top:6px}
.end-line{margin-top:24px;font-size:22px;font-weight:200;line-height:1.5}

/* 图 */
.fig{margin:24px 0 0;display:flex;flex-direction:column;min-height:0}
.fig-t{font-family:var(--mono);font-size:13.5px;letter-spacing:.2em;text-transform:uppercase;
  color:var(--accent);margin-bottom:10px}
.fig-b{background:#fff;border:1px solid var(--grey-2);padding:12px 14px}
.slide.dark .fig-b,.slide.accent .fig-b{background:#fff}
.sv{display:block;width:100%;height:auto;font-family:var(--sans)}
.fig-c{margin-top:10px;font-size:15px;line-height:1.55;color:var(--grey-3)}
.slide.dark .fig-c{color:rgba(250,250,248,.66)}
svg .ebox{fill:#f0f0ee;stroke:#d4d4d2;stroke-width:1}
svg .ebox.accent{fill:#f0f3fc;stroke:var(--accent)}
svg .ebox.ink{fill:#0a0a0a;stroke:#0a0a0a}
svg .track{stroke:#002FA7;stroke-width:4}
svg .tick{stroke:#4a4a4a;stroke-width:1.5}
svg .dot{fill:#fff;stroke:#002FA7;stroke-width:3}
svg .arrow{fill:#737373}

/* ===== 品牌双色 · 程序化装饰（CSS + SVG，零外链）===== */
:root{--brand:#1F4E9C;--brand-rgb:31,78,156;--ikb:#002FA7}
/* accent（IKB）页：程序化 SVG 同心弧 + 点阵 + 品牌蓝径向渐变 */
.slide.accent{background-color:var(--accent);
  background-image:
   url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='430' height='430'%3E%3Cg fill='none' stroke='%23ffffff' stroke-opacity='0.15' stroke-width='1.4'%3E%3Ccircle cx='430' cy='0' r='118'/%3E%3Ccircle cx='430' cy='0' r='186'/%3E%3Ccircle cx='430' cy='0' r='254'/%3E%3C/g%3E%3C/svg%3E"),
   url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Ccircle cx='6' cy='6' r='2' fill='%23ffffff' fill-opacity='0.17'/%3E%3C/svg%3E"),
   radial-gradient(1040px 580px at 112% -14%,rgba(31,78,156,0.96),rgba(31,78,156,0) 64%),
   repeating-linear-gradient(112deg,rgba(255,255,255,0.05) 0 1px,rgba(255,255,255,0) 1px 46px);
  background-repeat:no-repeat,repeat,no-repeat,repeat;
  background-position:right -70px top -80px,0 0,right top,0 0}
/* dark 页：品牌蓝弱光晕 + 细斜纹 */
.slide.dark{background-image:
   radial-gradient(940px 560px at 110% -12%,rgba(31,78,156,0.34),rgba(31,78,156,0) 62%),
   repeating-linear-gradient(112deg,rgba(255,255,255,0.032) 0 1px,rgba(255,255,255,0) 1px 46px)}
/* light 页：极细网格底纹（程序化） */
.slide.light{background-image:
   linear-gradient(rgba(31,78,156,0.045) 1px,transparent 1px),
   linear-gradient(90deg,rgba(31,78,156,0.045) 1px,transparent 1px);
  background-size:64px 64px,64px 64px;background-position:0 0,0 0}
/* 装饰 SVG（幕封角标）：位于文字之下 */
.deco{position:absolute;pointer-events:none;z-index:1}
.deco-tr{right:-26px;top:-30px;width:340px;height:340px}
.deco-tl{left:-26px;top:-30px;width:340px;height:340px}
.slide.accent .deco{color:#fff}
.slide.light .deco{color:var(--brand)}
.pad{z-index:2}

/* ===== 案例插画（内嵌 SVG 位图 · 不压文字）===== */
.side-2{display:grid;grid-template-columns:1fr .95fr;gap:38px;width:100%;
  margin-top:auto;margin-bottom:auto;align-items:center;min-height:0}
.side-2>.col-l{min-width:0}
.side-2>.col-l.center{display:flex;flex-direction:column;align-items:center;
  justify-content:center;text-align:center;gap:16px}
.duo.duo-stack{grid-template-columns:1fr;gap:18px;margin-top:0}
figure.illus{margin:0;display:flex;flex-direction:column;gap:10px;min-width:0}
figure.illus img{display:block;width:100%;height:auto;background:var(--paper);
  border:1px solid var(--grey-2)}
figure.illus figcaption{font-family:var(--mono);font-size:11.5px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--grey-3)}
.side-2>.col-l.center .ink-banner{width:100%;margin-top:8px}
.slide.dark figure.illus img{border-color:rgba(250,250,248,.3)}
.slide.dark figure.illus figcaption{color:rgba(250,250,248,.62)}
.slide.accent figure.illus img{border-color:rgba(255,255,255,.5)}
.slide.accent figure.illus figcaption{color:rgba(255,255,255,.74)}
.split-l{display:flex;flex-direction:column;align-items:flex-start;gap:22px;min-width:0}
.split-l figure.illus{width:100%}

/* 速查卡 / 自检 / 问答 */
.qbar{display:flex;align-items:center;gap:14px;margin-top:22px}
.qbar input{flex:1;max-width:520px;font:15px/1 var(--sans);padding:11px 14px;
  border:1px solid var(--grey-2);background:#fff;color:var(--ink);outline:none}
.qbar input:focus{border-color:var(--accent)}
.qhint{font-family:var(--mono);font-size:13px;color:var(--grey-3)}
.qcards{margin-top:16px;gap:14px}
.qc{background:var(--grey-1);padding:16px 18px}
.qc .ct{font-size:16.5px;font-weight:600;color:var(--accent)}
.qc .cd{font-size:14px;line-height:1.6;color:#3d3d3d;margin-top:8px}
.empty{margin-top:14px;font-size:15px;color:var(--grey-3)}
.chk{display:grid;grid-template-columns:1fr 1fr;gap:10px 26px;margin-top:24px}
.ci{display:flex;gap:13px;align-items:flex-start;text-align:left;background:transparent;
  border:0;border-top:1px solid var(--grey-2);padding:13px 2px;cursor:pointer;
  font-family:var(--sans)}
.ci .bx{flex:none;width:17px;height:17px;border:1.5px solid var(--grey-3);margin-top:1px;
  position:relative}
.ci .cv{font-size:15px;line-height:1.5;color:#3d3d3d}
.ci.on .bx{background:var(--accent);border-color:var(--accent)}
.ci.on .bx::after{content:"";position:absolute;left:5px;top:1.5px;width:4px;height:9px;
  border-right:2px solid #fff;border-bottom:2px solid #fff;transform:rotate(45deg)}
.ci.on .cv{color:var(--ink);font-weight:500}
.qa{display:grid;grid-template-columns:1fr 1fr;gap:0 30px;margin-top:20px;align-content:start}
.qa-r{padding:12px 0;border-top:1px solid var(--grey-2)}
.qa-q{font-size:16px;font-weight:600}
.qa-a{font-size:15px;line-height:1.6;color:var(--grey-3);margin-top:6px}

/* 沙盒 */
.sb-pad{padding-bottom:34px}
.sb{display:flex;flex-direction:column;min-height:0;flex:1;margin-top:16px;
  border:1px solid var(--grey-2);background:#fff}
.sb-h{display:flex;align-items:center;justify-content:space-between;gap:12px;
  padding:11px 16px;background:var(--ink);color:var(--paper)}
.sb-h .ttl{font-size:15px;font-weight:600}
.sb-h .bdg{font-family:var(--mono);font-size:13px;letter-spacing:.06em;
  color:rgba(250,250,248,.7)}
.sb-note{padding:9px 16px;font-size:13px;line-height:1.5;color:var(--grey-3);
  border-bottom:1px solid var(--grey-2)}
.sb-tabs{display:flex;gap:8px;padding:10px 14px;border-bottom:1px solid var(--grey-2);flex-wrap:wrap}
.sb-tab{font:15px/1 var(--sans);padding:9px 14px;border:1px solid var(--grey-2);
  background:var(--paper);cursor:pointer;color:var(--ink)}
.sb-tab.on{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600}
.sb-stage{flex:1;min-height:0;overflow-y:auto;padding:12px 16px}
.sbtitle{font-family:var(--mono);font-size:13px;color:var(--grey-3);letter-spacing:.06em;
  display:flex;justify-content:space-between;margin-bottom:8px}
.msg{display:flex;gap:9px;margin:7px 0;align-items:flex-start}
.msg .av{flex:none;width:26px;height:26px;background:var(--grey-1);color:var(--grey-3);
  font-size:12px;font-weight:600;display:flex;align-items:center;justify-content:center;
  border:1px solid var(--grey-2)}
.msg.me .av{background:var(--accent);color:#fff;border-color:var(--accent)}
.bub{font-size:14.5px;line-height:1.55;background:var(--grey-1);padding:8px 12px;max-width:74%}
.msg.me .bub{background:#eef2fc}
.opts{display:flex;flex-direction:column;gap:7px;margin:9px 0 4px}
.opt{font:14.5px/1.5 var(--sans);text-align:left;padding:10px 13px;border:1px solid var(--grey-2);
  background:#fff;cursor:pointer;color:var(--ink)}
.opt:hover{border-color:var(--accent);color:var(--accent)}
.opthint{font-size:13px;color:var(--grey-3);margin-bottom:6px}
.fb{font-size:14px;line-height:1.6;padding:10px 13px;margin:4px 0 10px 35px}
.fb.good{background:#f0f5ef;border-left:3px solid #1f7a3d}
.fb.bad{background:#fbf1ee;border-left:3px solid #b5491d}
.fb::before{display:block;font-family:var(--mono);font-size:10.5px;letter-spacing:.14em;
  margin-bottom:5px}
.fb.good::before{content:"接得对";color:#1f7a3d}
.fb.bad::before{content:"有问题";color:#b5491d}
.ref{display:block;margin-top:6px;font-family:var(--mono);font-size:13px;
  color:var(--grey-3);letter-spacing:.04em}
.sumcard{margin-top:10px;padding:13px 15px;background:var(--grey-1);font-size:14.5px;
  line-height:1.62;color:#2f2f2f}
.sumcard b{font-weight:600}
.sumcard ul{margin:8px 0 0;padding-left:18px}
.sumcard li{margin:4px 0}
.sb-btns{display:flex;align-items:center;justify-content:space-between;
  padding:10px 16px;border-top:1px solid var(--grey-2)}
.btn{font:14px/1 var(--sans);padding:9px 14px;border:1px solid var(--grey-2);
  background:#fff;cursor:pointer;color:var(--ink)}
.btn.ghost:hover{border-color:var(--accent);color:var(--accent)}
.cnt{font-family:var(--mono);font-size:13px;color:var(--grey-3)}
::-webkit-scrollbar{width:8px;height:8px}
::-webkit-scrollbar-thumb{background:var(--grey-2)}

/* ===== 底部导航（静态占据一行，避免压住品牌页脚）===== */
#nav{position:relative;z-index:60;flex:none;display:flex;align-items:center;
  gap:14px;padding:7px 16px;background:rgba(10,10,10,.95);color:#fafaf8;
  font-family:var(--mono);font-size:13px;letter-spacing:.04em}
#pager{font-weight:600;color:#fff;min-width:74px}
#curtitle{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  color:rgba(250,250,248,.62)}
#nav .nb{font:12px/1 var(--mono);color:#fafaf8;background:transparent;
  border:1px solid rgba(250,250,248,.3);padding:6px 10px;cursor:pointer;white-space:nowrap}
#nav .nb:hover{border-color:#fff;background:rgba(250,250,248,.12)}
#nav .nb b{color:#fff;font-weight:700}

/* ===== 目录浮层 ===== */
#toc{position:fixed;inset:0;z-index:80;background:rgba(10,10,10,.97);color:#fafaf8;
  display:none;padding:52px 60px;overflow-y:auto}
#toc.on{display:block}
#toc h2{font-size:13px;font-family:var(--mono);letter-spacing:.24em;color:rgba(250,250,248,.6);
  margin:0 0 22px;font-weight:600}
#toc .tg{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:10px}
#toc .ti{display:flex;gap:12px;align-items:baseline;text-align:left;background:transparent;
  border:1px solid rgba(250,250,248,.18);padding:12px 14px;cursor:pointer;color:#fafaf8;
  font-family:var(--sans)}
#toc .ti:hover{border-color:#fff;background:rgba(250,250,248,.1)}
#toc .ti .i{font-family:var(--mono);font-size:12px;color:rgba(250,250,248,.55);min-width:22px}
#toc .ti .t{font-size:15px;font-weight:500}
#toc .ti .s{font-family:var(--mono);font-size:11px;color:rgba(250,250,248,.45);margin-left:auto}
#toc .ti.on{border-color:var(--accent);background:rgba(0,47,167,.35)}
#toc .ch{grid-column:1/-1;margin-top:14px;font-family:var(--mono);font-size:11.5px;
  letter-spacing:.2em;color:rgba(250,250,248,.5)}

/* ===== 演讲者视图 ===== */
#pres{position:fixed;inset:0;z-index:90;background:#0f1115;color:#eef1f6;display:none;
  grid-template-columns:minmax(0,1.2fr) minmax(320px,.8fr);gap:18px;padding:18px 20px}
#pres.on{display:grid}
.pv-col{display:flex;flex-direction:column;gap:8px;min-height:0}
.pv-lab{display:flex;align-items:center;gap:10px;font-family:var(--mono);font-size:11.5px;
  letter-spacing:.16em;color:#9aa3b2;text-transform:uppercase;flex:none}
.pv-fit{flex:1 1 0;min-height:0;display:flex;align-items:center;justify-content:center}
.pv-box{position:relative;background:#000;overflow:hidden;
  outline:1px solid rgba(255,255,255,.14)}
.pv-box .pv-inner{position:absolute;inset:0;overflow:hidden}
.pv-box .slide{position:absolute;left:0;top:0;transform-origin:top left}
.pv-notes{display:flex;flex-direction:column;min-height:0;background:#151922;
  border:1px solid rgba(255,255,255,.1);padding:16px 18px;overflow-y:auto}
.pv-notes h3{margin:0;font-size:21px;font-weight:600;line-height:1.3}
.pv-meta{display:flex;flex-wrap:wrap;gap:8px 14px;margin:10px 0 0;
  font-family:var(--mono);font-size:12.5px;color:#9aa3b2;letter-spacing:.06em}
.pv-sec{margin-top:16px;font-family:var(--mono);font-size:11px;letter-spacing:.18em;
  color:#7f8a9c;text-transform:uppercase}
.pv-notes ul{margin:8px 0 0;padding-left:18px}
.pv-notes li{font-size:14.5px;line-height:1.65;color:#dfe5ee;margin:6px 0}
.pv-notes p{font-size:14.5px;line-height:1.65;color:#c3ccdb;margin:8px 0 0}
#prestimer{display:flex;align-items:center;gap:10px;margin-top:auto;padding-top:16px;
  border-top:1px solid rgba(255,255,255,.12);flex-wrap:wrap}
#prestimer .clock{font-family:var(--mono);font-size:30px;font-weight:600;letter-spacing:.04em;
  color:#fff;min-width:118px}
#prestimer .tbtn{font:12px/1 var(--mono);background:transparent;color:#eef1f6;
  border:1px solid rgba(255,255,255,.28);padding:7px 11px;cursor:pointer}
#prestimer .tbtn:hover{border-color:#fff;background:rgba(255,255,255,.1)}
#pres .cls{position:absolute;right:18px;top:14px;font:12px/1 var(--mono);background:transparent;
  color:#cfd6e2;border:1px solid rgba(255,255,255,.24);padding:6px 10px;cursor:pointer}
#presstatus{position:absolute;left:20px;bottom:16px;font-family:var(--mono);font-size:11.5px;
  color:#8d97a8;letter-spacing:.06em}

/* ===== 观众屏 ===== */
body.audience .bior-header,body.audience #nav,body.audience .bior-footer{display:none}
body.audience #stagewrap{background:#000;height:100vh}
#frozen{position:fixed;inset:0;display:none}

/* ===== 打印：每页一页 ===== */
@media print{
  @page{size:1280px 720px;margin:0}
  html,body{overflow:visible !important;height:auto !important;background:#fff !important}
  .bior-header,.bior-footer,#nav,#toc,#pres,#notebar{display:none !important}
  #stagewrap{position:static !important;display:block !important;overflow:visible !important;
    background:#fff !important;height:auto !important}
  #stage{transform:none !important;width:auto !important;height:auto !important;
    position:static !important}
  #track{position:static !important;transform:none !important;display:block !important;
    width:auto !important;height:auto !important}
  .slide{width:1280px !important;height:720px !important;position:relative !important;
    page-break-after:always;break-after:page;overflow:hidden}
  .slide:last-child{page-break-after:auto;break-after:auto}
  .slide::after{content:"BIORI BIOTECH · 宝锐生物　｜　守护生命健康，缔造美好生活";
    position:absolute;left:64px;bottom:14px;font:11px/1 var(--mono);
    letter-spacing:.1em;color:rgba(10,10,10,.42)}
  .slide.dark::after,.slide.accent::after{color:rgba(255,255,255,.5)}
  .sb-stage{overflow:visible !important}
}
@media (max-width:760px){
  #nav{gap:8px;padding:6px 9px;font-size:11px}
  #curtitle{display:none}
  #nav .nb{padding:5px 7px;font-size:10.5px}
}
"""

# ──────────────────────────────────────────────────────────────
# JS
# ──────────────────────────────────────────────────────────────
JS = """
(function(){
  var SPEAKER_NOTES = __NOTES__;
  var TOC = __TOC__;
  var track = document.getElementById('track');
  var stage = document.getElementById('stage');
  var wrap  = document.getElementById('stagewrap');
  var slides = Array.prototype.slice.call(document.querySelectorAll('#track > .slide'));
  var N = slides.length, idx = 0;
  var pager = document.getElementById('pager');
  var curtitle = document.getElementById('curtitle');
  var notesById = {};
  SPEAKER_NOTES.forEach(function(n){ notesById[n.id] = n; });
  window.__SPEAKER_NOTES__ = SPEAKER_NOTES;

  function fit(){
    var r = wrap.getBoundingClientRect();
    var k = Math.min(r.width / 1280, r.height / 720);
    if (!(k > 0)) k = 1;
    stage.style.transform = 'scale(' + k + ')';
  }
  window.addEventListener('resize', fit);

  function pad2(n){ return (n < 10 ? '0' : '') + n; }

  function go(i){
    idx = Math.max(0, Math.min(N - 1, i));
    track.style.transform = 'translateX(' + (-idx * 1280) + 'px)';
    if (pager) pager.textContent = pad2(idx + 1) + ' / ' + N;
    if (curtitle) curtitle.textContent = slides[idx].getAttribute('data-title') || '';
    if (typeof window.__presSync === 'function') window.__presSync();
    try { broadcast(); } catch (e) {}
  }
  window.__go = go;
  window.__idx = function(){ return idx; };
  window.__N = N;

  /* ---------- 目录浮层 ---------- */
  var toc = document.getElementById('toc');
  function buildToc(){
    var g = document.getElementById('toc-grid'), html = '', lastSec = null;
    TOC.forEach(function(t){
      if (t.s !== lastSec){ html += '<div class="ch">' + t.s + '</div>'; lastSec = t.s; }
      html += '<button class="ti" data-i="' + (t.i - 1) + '"><span class="i">' +
              pad2(t.i) + '</span><span class="t">' + t.t + '</span></button>';
    });
    g.innerHTML = html;
  }
  buildToc();
  function markToc(){
    Array.prototype.forEach.call(document.querySelectorAll('#toc .ti'), function(b){
      b.classList.toggle('on', parseInt(b.getAttribute('data-i'), 10) === idx);
    });
  }
  function toggleToc(force){
    var on = (typeof force === 'boolean') ? force : !toc.classList.contains('on');
    toc.classList.toggle('on', on);
    if (on) markToc();
  }
  toc.addEventListener('click', function(e){
    var b = e.target.closest ? e.target.closest('.ti') : null;
    if (b){ go(parseInt(b.getAttribute('data-i'), 10)); toggleToc(false); return; }
    if (e.target === toc) toggleToc(false);
  });

  /* ---------- 演讲者视图 ---------- */
  var pres = document.getElementById('pres');
  var pvCur = document.getElementById('pv-cur');
  var pvNext = document.getElementById('pv-next');
  var pvTitle = document.getElementById('pv-title');
  var pvMeta = document.getElementById('pv-meta');
  var pvPurpose = document.getElementById('pv-purpose');
  var pvTalk = document.getElementById('pv-talk');
  var pvTrans = document.getElementById('pv-trans');
  var presOpen = false;

  function blankClone(){
    var d = document.createElement('div');
    d.className = 'slide';
    d.style.background = '#fafaf8';
    d.innerHTML = '<div class="pad" style="align-items:center;justify-content:center">' +
      '<div class="kick">—</div><h1 class="t" style="font-size:34px">演示结束</h1></div>';
    return d;
  }
  function sizeBox(box){
    var fit = box.parentNode;
    var s = Math.min(fit.clientWidth / 16, Math.max(1, fit.clientHeight) / 9);
    if (!(s > 0)) s = 1;
    box.style.width = (16 * s) + 'px';
    box.style.height = (9 * s) + 'px';
  }
  function mountPreview(box, slide){
    sizeBox(box);
    var inner = box.querySelector('.pv-inner');
    inner.innerHTML = '';
    var node = slide ? slide.cloneNode(true) : blankClone();
    Array.prototype.forEach.call(node.querySelectorAll('[id]'), function(el){
      el.removeAttribute('id');
    });
    node.style.position = 'absolute';
    node.style.left = '0'; node.style.top = '0';
    inner.appendChild(node);
    node.style.transform = 'scale(' + (box.clientWidth / 1280) + ')';
  }
  function resyncPreviewScale(){
    [pvCur, pvNext].forEach(function(box){
      sizeBox(box);
      var n = box.querySelector('.pv-inner .slide');
      if (n) n.style.transform = 'scale(' + (box.clientWidth / 1280) + ')';
    });
  }
  window.addEventListener('resize', resyncPreviewScale);

  function fillNotes(){
    var n = notesById[slides[idx].getAttribute('data-slide-id')] || null;
    pvTitle.textContent = (n && n.title) || slides[idx].getAttribute('data-title') || '';
    var mins = (n && typeof n.minutes === 'number') ? (n.minutes + ' 分钟') : '—';
    pvMeta.innerHTML = '<span>第 ' + (idx + 1) + ' / ' + N + ' 页</span>' +
      '<span>' + ((n && n.section) || '—') + '</span><span>建议 ' + mins + '</span>' +
      '<span>ID ' + slides[idx].getAttribute('data-slide-id') + '</span>';
    pvPurpose.textContent = (n && n.purpose) || '—';
    var talk = (n && n.talk) || [];
    pvTalk.innerHTML = talk.map(function(t){ return '<li>' + t + '</li>'; }).join('') ||
      '<li>—</li>';
    pvTrans.textContent = (n && n.transition) || '—';
    Array.prototype.forEach.call(document.querySelectorAll('#toc .ti'), function(b){
      b.classList.toggle('on', parseInt(b.getAttribute('data-i'), 10) === idx);
    });
  }
  window.__presSync = function(){
    if (!presOpen) return;
    mountPreview(pvCur, slides[idx]);
    mountPreview(pvNext, idx + 1 < N ? slides[idx + 1] : null);
    fillNotes();
    broadcast();
  };
  function togglePres(force){
    presOpen = (typeof force === 'boolean') ? force : !presOpen;
    pres.classList.toggle('on', presOpen);
    document.body.classList.toggle('pres-on', presOpen);
    if (presOpen) window.__presSync();
  }

  /* ---------- 计时器 ---------- */
  var clock = document.getElementById('presclock');
  var tRun = false, tBase = 0, tAcc = 0, tTick = null;
  function fmt(ms){
    var s = Math.floor(ms / 1000);
    return pad2(Math.floor(s / 60)) + ':' + pad2(s % 60);
  }
  function paint(){ clock.textContent = fmt(tAcc + (tRun ? (Date.now() - tBase) : 0)); }
  setInterval(function(){ if (tRun) paint(); }, 250);
  document.getElementById('t-start').addEventListener('click', function(){
    if (tRun){ tAcc += Date.now() - tBase; tRun = false; this.textContent = '继续计时'; }
    else { tBase = Date.now(); tRun = true; this.textContent = '暂停计时'; }
    paint();
  });
  document.getElementById('t-reset').addEventListener('click', function(){
    tRun = false; tAcc = 0; tBase = 0; paint();
    document.getElementById('t-start').textContent = '开始计时';
  });
  paint();

  /* ---------- 观众屏同步 ---------- */
  var bc = null;
  try { bc = new BroadcastChannel('bior-deck'); } catch (e) { bc = null; }
  function broadcast(){
    var msg = { t: 'page', i: idx, n: N };
    if (bc) { try { bc.postMessage(msg); } catch (e) {} }
  }
  var status = document.getElementById('presstatus');
  function audPop(){
    var w = window.open(location.href.split('#')[0] + '#audience', '_blank',
                        'width=1280,height=720');
    if (!w){ if (status) status.textContent = '观众屏：弹窗被浏览器拦截'; return; }
    if (status) status.textContent = '观众屏：已打开（广播当前页）';
    setTimeout(broadcast, 400);
  }
  document.getElementById('nav-aud').addEventListener('click', audPop);
  document.getElementById('nav-pres').addEventListener('click', function(){ togglePres(); });
  document.getElementById('nav-toc').addEventListener('click', function(){ toggleToc(); });
  document.getElementById('nav-full').addEventListener('click', function(){
    if (document.fullscreenElement) document.exitFullscreen();
    else if (document.documentElement.requestFullscreen) document.documentElement.requestFullscreen();
  });
  document.getElementById('nav-low').addEventListener('click', function(){
    document.body.classList.toggle('low-power');
  });
  document.getElementById('pres-close').addEventListener('click', function(){ togglePres(false); });

  /* ---------- 键盘 ---------- */
  function typing(e){
    var t = e.target;
    return !!(t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable));
  }
  function closeAll(){ togglePres(false); toggleToc(false); }
  document.addEventListener('keydown', function(e){
    var k = e.key;
    if (k === 'Escape'){ closeAll(); return; }
    if (window.__audience) return;          /* 观众屏只跟随，不自己翻页 */
    if (typing(e)) return;
    if (k === 'ArrowRight' || k === 'ArrowDown' || k === ' ' || k === 'Spacebar' ||
        k === 'PageDown'){ e.preventDefault(); go(idx + 1); return; }
    if (k === 'ArrowLeft' || k === 'ArrowUp' || k === 'PageUp'){
      e.preventDefault(); go(idx - 1); return; }
    if (k === 'Home'){ e.preventDefault(); go(0); return; }
    if (k === 'End'){ e.preventDefault(); go(N - 1); return; }
    if (k === 's' || k === 'S'){ e.preventDefault(); togglePres(); return; }
    if (k === 'o' || k === 'O'){ e.preventDefault(); toggleToc(); return; }
    if (k === 'b' || k === 'B'){ document.body.classList.toggle('low-power'); return; }
    if (k === 'f' || k === 'F'){
      if (document.fullscreenElement) document.exitFullscreen();
      else if (document.documentElement.requestFullscreen)
        document.documentElement.requestFullscreen();
      return;
    }
    if (presOpen && (k === '1' || k === '2' || k === '3')){ return; }
  });

  /* ---------- 滚轮 / 触控 ---------- */
  var lock = false;
  wrap.addEventListener('wheel', function(e){
    var sc = e.target && e.target.closest ? e.target.closest('[data-scroll]') : null;
    if (sc && sc.scrollHeight > sc.clientHeight + 2){
      e.stopPropagation();
      return;
    }
    e.preventDefault();
    if (lock) return;
    if (Math.abs(e.deltaY) < 10 && Math.abs(e.deltaX) < 10) return;
    var d = (e.deltaY || e.deltaX) > 0 ? 1 : -1;
    lock = true;
    setTimeout(function(){ lock = false; }, 520);
    go(idx + d);
  }, {passive:false});

  var tx = 0, ty = 0, tt = false;
  wrap.addEventListener('touchstart', function(e){
    if (e.touches.length !== 1) return;
    tx = e.touches[0].clientX; ty = e.touches[0].clientY; tt = true;
  }, {passive:true});
  wrap.addEventListener('touchend', function(e){
    if (!tt || !e.changedTouches.length) return;
    tt = false;
    var dx = e.changedTouches[0].clientX - tx, dy = e.changedTouches[0].clientY - ty;
    if (Math.abs(dx) > 45 && Math.abs(dx) > Math.abs(dy)) go(idx + (dx < 0 ? 1 : -1));
  }, {passive:true});

  /* ---------- 观众屏模式 ---------- */
  function audienceMode(){
    document.body.classList.add('audience');
    window.__audience = true;
    function apply(i){
      i = Math.max(0, Math.min(N - 1, i));
      track.style.transform = 'translateX(' + (-i * 1280) + 'px)';
    }
    if (bc) bc.onmessage = function(ev){
      if (ev.data && ev.data.t === 'page') apply(ev.data.i);
    };
    window.addEventListener('message', function(ev){
      if (ev.data && ev.data.t === 'page') apply(ev.data.i);
    });
    apply(0);
  }

  fit();
  if (/[#&?]audience\\b/.test(location.href)){ audienceMode(); } else { go(0); }
  window.addEventListener('keydown', function(e){
    if (e.key === 'ArrowRight' || e.key === ' ') { /* 观众屏也跟随广播 */ }
  });
})();

(function(){
  /* 自检清单：点一下打勾（仅本页内存态） */
  var list = document.getElementById('chk-list');
  if (list){
    list.addEventListener('click', function(e){
      var b = e.target.closest ? e.target.closest('.ci') : null;
      if (b) b.classList.toggle('on');
    });
  }

  /* 速查卡搜索 —— 中文输入法安全：拼音合成期间不重渲染 */
  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();
  }
})();

(function(){
  var SC = __SC__;
  var st = {sc:0, round:0, done:false, picks:[]};
  var tabs = document.getElementById('sb-tabs');
  var stageEl = document.getElementById('sb-stage');
  var cntEl = document.getElementById('sb-cnt');
  if (!tabs || !stageEl) 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 type="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"><span>场景 ' + (st.sc + 1) + ' / ' + SC.length + '　·　' +
         E(sc.title) + '</span><span>已答 ' + 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 type="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>'; }
    stageEl.innerHTML = h;
    stageEl.scrollTop = stageEl.scrollHeight;
    cntEl.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();
  }
  stageEl.addEventListener('click', function(e){
    var b = e.target && e.target.getAttribute ? e.target.getAttribute('data-opt') : null;
    if (b === null) 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') : null;
    if (b === null) return;
    switchTo(parseInt(b, 10));
  });
  document.getElementById('sb-reset').addEventListener('click', function(){ switchTo(st.sc); });
  renderTabs(); renderStage();
  window.__sb = { switchTo: switchTo, state: function(){ return st; }, SC: SC };
})();
"""

JS = (JS.replace('__NOTES__', NOTES_JS).replace('__TOC__', TOC_JS).replace('__SC__', SC_JSON))

# ──────────────────────────────────────────────────────────────
# HTML 骨架
# ──────────────────────────────────────────────────────────────
BODY = (
    '<div id="stagewrap"><div id="stage"><div id="track">' + SLIDES_HTML + '</div></div></div>'
    '<div id="nav">'
    '<span id="pager">01 / %d</span><span id="curtitle"></span>'
    '<button class="nb" id="nav-toc" type="button">目录 <b>O</b></button>'
    '<button class="nb" id="nav-pres" type="button">讲稿 <b>S</b></button>'
    '<button class="nb" id="nav-aud" type="button">观众屏</button>'
    '<button class="nb" id="nav-full" type="button">全屏 <b>F</b></button>'
    '<button class="nb" id="nav-low" type="button">静态 <b>B</b></button>'
    '</div>'
    '<div id="toc"><h2>目录 · 跳页</h2><div class="tg" id="toc-grid"></div>'
    '<p class="tnote" style="font-family:var(--mono);font-size:12px;'
    'color:rgba(250,250,248,.5);margin-top:22px">'
    '← → 翻页　空格 下一页　Home / End 首末页　S 讲稿　O 目录　F 全屏　B 静态　Esc 关闭</p></div>'
    '<div id="pres">'
    '<button class="cls" id="pres-close" type="button">关闭 S / Esc</button>'
    '<div class="pv-col">'
    '<div class="pv-lab"><span>当前页</span></div>'
    '<div class="pv-fit"><div class="pv-box" id="pv-cur"><div class="pv-inner"></div></div></div>'
    '<div class="pv-lab"><span>下一页预览</span></div>'
    '<div class="pv-fit"><div class="pv-box" id="pv-next"><div class="pv-inner"></div></div></div>'
    '</div>'
    '<div class="pv-notes">'
    '<h3 id="pv-title">—</h3>'
    '<div class="pv-meta" id="pv-meta"></div>'
    '<div class="pv-sec">本页目的</div><p id="pv-purpose">—</p>'
    '<div class="pv-sec">讲稿备注</div><ul id="pv-talk"><li>—</li></ul>'
    '<div class="pv-sec">转场</div><p id="pv-trans">—</p>'
    '<div id="prestimer"><span class="clock" id="presclock">00:00</span>'
    '<button class="tbtn" id="t-start" type="button">开始计时</button>'
    '<button class="tbtn" id="t-reset" type="button">重置计时</button></div>'
    '</div>'
    '<div id="presstatus">观众屏：未连接</div>'
    '</div>'
) % N_PAGES

HEAD = (
    '<meta charset="utf-8">'
    '<meta name="viewport" content="width=device-width,initial-scale=1">'
    '<title>价格谈判培训 · 攻心为上</title>'
    '<style>%s</style>' % CSS
)

html = ('<!DOCTYPE html>\n<html lang="zh-CN"><head>' + HEAD + '</head><body>' + BODY +
        '<script>' + JS + '</script></body></html>')

html = BK.inject(html, theme='dark', tag='销售培训')

io.open(OUT, 'w', encoding='utf-8').write(html)
print('✅ 生成 %s  (%d 页, %.1f KB)' % (OUT, N_PAGES, len(html.encode('utf-8')) / 1024.0))

if '--verify' in sys.argv:
    v = BK.verify(html)
    bad = [k for k, ok in v.items() if not ok]
    print('   brand_kit --verify:', '✅ 通过' if not bad else '❌ 缺 ' + ', '.join(bad))
