# -*- coding: utf-8 -*-
"""ppt_kit2 —— 宣讲 PPT v2 版式库（马桥宣纸风 · 平原水乡）
在 ppt_page_kit 基础上扩展：扉页 / 大表格 / 双栏对比 / 多卡 / 左文右图 / 数据卡 / 占位页
栅格统一：标题 y=0.30 · 朱红线 y=0.92 · 内容区 y0=1.40 · 金句 y=6.62 · 页码(12.35,7.02)
"""
import os, sys, re
sys.path.insert(0, "/home/ubuntu/.hermes/skills/productivity/pptx-page-insert-publish/scripts")
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.dml import MSO_LINE_DASH_STYLE
from ppt_page_kit import Page, KAI, HAN, PAPER, INK, VERM, GREY, WHITE, GOLD

IMG = "/home/ubuntu/speech_assets/ppt_v2_images/prepared"
CARD = RGBColor(0xFB, 0xF7, 0xEC)
CARD2 = RGBColor(0xF2, 0xEE, 0xE4)
DEEP = RGBColor(0x0A, 0x12, 0x1E)
TXT = RGBColor(0x3A, 0x36, 0x2E)
GREEN = RGBColor(0x1B, 0x6B, 0x36)
BLUE = RGBColor(0x1F, 0x5C, 0x88)
AMBER = RGBColor(0xA0, 0x74, 0x10)
SLW, SLH = 13.333, 7.5
ML = 0.62


def img(p, name, x, y, w, h):
    p.s.shapes.add_picture(os.path.join(IMG, name), Inches(x), Inches(y), Inches(w), Inches(h))


def head(p, title, sub=None, ask=None):
    """标题 + 朱红短线；给了 ask 就在标题下加「设问条」——把原稿里的设问放回页面第一句。"""
    p.title(title, x=ML, y=0.30, size=26)
    p.rule(x=ML + 0.03, y=0.92)
    if ask:
        p.rect(ML, 1.02, 12.10, 0.40, RGBColor(0xF3, 0xEE, 0xE0))
        p.rect(ML, 1.02, 0.055, 0.40, VERM)
        p.text(ML + 0.22, 1.09, 0.42, 0.28, "问", size=13, color=VERM, bold=True, font=KAI)
        rich(p, ML + 0.72, 1.09, 11.2, 0.30, ask, size=13.0, color=INK, font=HAN, keycolor=VERM)
    elif sub:
        p.text(ML, 1.10, 12.1, 0.32, sub, size=12.5, color=GREY)


def quote(p, txt, color=VERM):
    p.rect(ML, 6.58, 0.05, 0.40, color)
    rich(p, ML + 0.20, 6.62, 11.6, 0.40, txt, size=13.5, color=color, font=KAI)


def foot(p, no):
    p.page_no(no, x=12.35, y=7.02)


# ── 宣纸底纹 + 卡片投影（「精致感」的两个来源，7 页样板有、57 页版漏掉的）──
SHADOW = RGBColor(0xE0, 0xD8, 0xC6)

# ── 新词表：全篇统一突出显示（陆老师 2026-09-20：「这是『新词新事』，新词要有一个统一的突出显示」）──
# 只收「新词」类词，不收六化路径名（那是章节名，满篇都在）
KW = [
    "超大城市优质乡村生活空间", "乡村优质生活空间", "一体化综合管护", "建管养", "建管并重",
    "新版社区云＋阳光村务", "新版社区云+阳光村务", "阳光村务", "多格合一", "沪派江南",
    "五好两宜", "整片区设计", "整区域设计", "郊野立方", "三师联创", "多师联创", "片—线—点",
    "一主多元", "运营前置", "小而美", "专而精", "土特产", "特色乡村产业", "乡村特色产业",
    "财产性收入", "数字负担三件套", "农业数智化转型", "农业生产信息化率", "闲置住房盘活",
    "系统架构师", "读村", "一村一品", "千万工程", "多村合作", "镇村联营", "企村一体",
    "国资入股", "一片一策", "共富增收", "六业融合", "微度假", "保供基底",
]

def _kw_split(seg):
    """把一段文字按新词表切成 [(片段, 是否新词)]，长词优先。"""
    if not seg:
        return []
    pat = "(" + "|".join(re.escape(w) for w in sorted(KW, key=len, reverse=True)) + ")"
    out = []
    for piece in re.split(pat, seg):
        if piece:
            out.append((piece, piece in KW))
    return out


def rich(p, x, y, w, h, text, size=12.2, color=TXT, font=HAN, spacing=None,
         keycolor=VERM, numcolor=None, align=PP_ALIGN.LEFT, bold=False):
    """富文本：`**关键词**` → 朱红加粗；给了 numcolor 时，其中的数字自动单独染色。

    陆老师 2026-09-20 要求：政策要点**写成完整句、关键词变色**；数字**不要是灰的**。
    """
    bx = p.s.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
    tf = bx.text_frame; tf.word_wrap = True
    tf.margin_left = tf.margin_right = tf.margin_top = tf.margin_bottom = 0
    NUMSPLIT = r"([0-9][0-9\.\,]*\s*(?:%|％|个百分点|万元|万|亿|元|个|家|亩|平方公里|人|户|公里|万人次|年|月|日|点)?)"
    for i, line in enumerate(str(text).split("\n")):
        para = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
        para.alignment = align
        if spacing: para.line_spacing = spacing
        for part in re.split(r"(\*\*[^*]+\*\*)", line):
            if not part: continue
            key = part.startswith("**") and part.endswith("**") and len(part) > 4
            seg, col, bd = (part[2:-2], keycolor, True) if key else (part, color, bold)
            if key:
                r = para.add_run(); r.text = seg
                r.font.size = Pt(size); r.font.name = font; r.font.bold = True
                r.font.color.rgb = keycolor
                continue
            for piece, is_kw in _kw_split(seg):          # 先按新词表切
                if is_kw:
                    r = para.add_run(); r.text = piece
                    r.font.size = Pt(size); r.font.name = font; r.font.bold = True
                    r.font.color.rgb = keycolor
                    continue
                if numcolor is not None:
                    for sub in re.split(NUMSPLIT, piece):
                        if not sub: continue
                        r = para.add_run(); r.text = sub
                        r.font.size = Pt(size); r.font.name = font; r.font.bold = bd
                        r.font.color.rgb = numcolor if re.match(r"^[0-9]", sub) else col
                else:
                    r = para.add_run(); r.text = piece
                    r.font.size = Pt(size); r.font.name = font; r.font.bold = bd
                    r.font.color.rgb = col
    return bx


def p_policy(p, title, sub, items, q, no, ask=None):
    """政策来源页：每条 = 来源标签 ＋ **政策原文整句**（关键词变色）＋ 一句说明。

    陆老师 2026-09-20：「政策要点可以写完整的呀，不是只写几个词 —— 把那一句写完整，
    把重点关键词变个颜色。」所以这里正文就是原文整句，不做删减。
    """
    paper(p)
    head(p, title, sub, ask)
    n = len(items); gap = 0.20; area = 4.94
    CH = min(1.66, (area - gap * (n - 1)) / n)
    y = 1.52 + max(0.0, (area - (CH * n + gap * (n - 1))) / 2.0)
    for tag, quote_txt, note in items:
        card(p, ML, y, 12.10, CH)
        p.rect(ML, y, 0.055, CH, VERM)
        p.text(ML + 0.28, y + 0.12, 6.0, 0.32, tag, size=12.0, color=VERM, bold=True, font=KAI)
        rich(p, ML + 0.28, y + 0.52, 11.5, CH - 0.86, quote_txt, size=13.0, color=INK,
             font=HAN, spacing=1.36, numcolor=VERM)
        if note:
            rich(p, ML + 0.28, y + CH - 0.42, 11.5, 0.34, note, size=10.8, color=GREY,
                 font=HAN, spacing=1.18, numcolor=VERM)
        y += CH + gap
    if q:
        quote(p, q)
    foot(p, no)


def p_claim(p, kicker, big, no, bg="claim-bg.jpg", big_size=40):
    """观点独占页（照着陆老师挑出的样板复刻）：淡水墨整页底 + 朱红竖线 + 大字观点 + 小标。"""
    img(p, bg, 0, 0, SLW, SLH)
    lines = str(big).split("\n")
    y0 = 2.98
    p.rect(1.06, y0 + 0.10, 0.055, len(lines) * 0.78, VERM)
    yy = y0
    for ln in lines:
        p.text(1.38, yy, 10.8, 0.82, ln, size=big_size, color=INK, bold=True, font=HAN)
        yy += 0.80
    p.text(1.40, yy + 0.26, 10.0, 0.44, kicker, size=15, color=RGBColor(0x6E, 0x67, 0x59), font=HAN)
    foot(p, no)


def paper(p, strip=False):
    """宣纸底 + 整页淡水墨肌理；strip=True 时再加左侧水墨竖边（表格页用）。

    ⚠️ 两行都不许删：
    ① `texture-bg.jpg` 那一行 = 整页宣纸肌理（2026-09-20 被覆盖删过一次，40 个内容页集体变"平"）；
    ② 竖边右缘必须 ≤ 0.54in（表格左界 ML=0.62in 留 0.08 余量）。
       踩过的坑：竖边右缘写到 0.915in，压住表格第一列、标题首字与「口径提示」行，
       并留下一条贯穿全页的竖直硬边 —— 一次性影响全部 6 个表格页。
       图宽 1.415 / 高 7.5 是原图比例(0.1887)，不能单独改宽，只能靠 x 左移压缩可见宽度。
    """
    p.background(PAPER)
    img(p, "bg-content.jpg", 0, 0, SLW, SLH)      # 淡墨底（含宣纸肌理＋左上/右下淡水墨）
    if strip:
        SX, SW = -0.88, 1.415
        assert SX + SW <= 0.54, f"左竖边右缘 {SX+SW:.3f}in 越过安全线 0.54in —— 会压表格第一列"
        img(p, "strip.png", SX, 0, SW, SLH)


def card(p, x, y, w, h, color=CARD):
    """带右下投影的卡片。"""
    p.rect(x + 0.045, y + 0.055, w, h, SHADOW)
    return p.rect(x, y, w, h, color)


# ── 01 封面 ──
def p_cover(p):
    img(p, "cover-bg.jpg", 0, 0, SLW, SLH)
    p.text(1.15, 2.28, 8.0, 0.5, "上海乡村振兴的", size=27, color=RGBColor(0xE8, 0xDF, 0xCE), font=KAI)
    p.text(1.15, 2.86, 9.2, 1.2, "新词新事", size=58, color=WHITE, bold=True, font=KAI)
    p.rect(1.22, 4.26, 1.5, 0.06, VERM)
    p.text(1.15, 4.52, 9.0, 0.5, "基于「十五五」政策的深度复盘与业务思考", size=17,
           color=RGBColor(0xC9, 0xC3, 0xB8), font=HAN)
    p.text(1.15, 6.62, 6.0, 0.4, "城测 · 2026", size=12, color=RGBColor(0x9A, 0x94, 0x88), font=HAN)


# ── 02 目录 ──
def p_toc(p, items):
    paper(p)
    p.title("目录", x=ML, y=0.30, size=26)
    p.rule(x=ML + 0.03, y=0.92)
    p.text(ML, 1.10, 12.1, 0.32, "CONTENTS ｜ 七个核心词，一条主线", size=12.5, color=GREY)
    y = 1.72
    for i, (t, s) in enumerate(items, 1):
        p.rect(ML, y, 0.055, 0.86, VERM)
        p.text(ML + 0.26, y + 0.02, 1.0, 0.5, f"0{i}", size=22, color=RGBColor(0xC9, 0xBE, 0xA6), font=HAN)
        p.text(ML + 1.30, y - 0.02, 10.6, 0.45, t, size=17, color=INK, bold=True, font=KAI)
        p.text(ML + 1.32, y + 0.42, 10.6, 0.4, s, size=12, color=GREY, font=HAN)
        y += 1.16
    foot(p, "2")


# ── 03 章节扉页 ──
def p_chapter(p, num, title, sub, bg, no):
    img(p, bg, 0, 0, SLW, SLH)
    p.text(0.95, 0.62, 4.0, 0.4, f"0 {num}", size=15, color=RGBColor(0xE0, 0xA8, 0x70), font=HAN)
    p.text(0.90, 2.06, 9.0, 1.3, title, size=54, color=WHITE, bold=True, font=KAI)
    p.rect(0.96, 3.62, 1.5, 0.06, RGBColor(0xE0, 0x70, 0x50))
    p.text(0.92, 3.92, 10.0, 0.6, sub, size=19, color=RGBColor(0xDA, 0xD5, 0xCB), font=HAN)
    p.text(12.30, 6.98, 0.9, 0.36, no, size=11, color=RGBColor(0xB0, 0xAA, 0xA0), align=PP_ALIGN.CENTER)


# ── 04 政策文件页 ──
def p_files(p, title, sub, files, q, no, ask=None):
    paper(p)
    head(p, title, sub, ask)
    y = 1.56
    for i, (tag, name, meta) in enumerate(files, 1):
        card(p, ML, y, 12.10, 1.18)
        p.rect(ML, y, 0.055, 1.18, VERM)
        p.text(ML + 0.26, y + 0.14, 0.5, 0.4, str(i), size=20, color=RGBColor(0xC9, 0xBE, 0xA6), font=HAN)
        p.text(ML + 0.88, y + 0.13, 10.9, 0.44, name, size=15, color=INK, bold=True, font=KAI)
        rich(p, ML + 0.90, y + 0.62, 10.9, 0.44, meta, size=11.5, color=GREY, font=HAN, numcolor=VERM)
        y += 1.36
    if q:
        quote(p, q)
    foot(p, no)


# ── 05 大表格（字号 ≥11.5pt，最多 12 行）──
def p_table(p, title, sub, header, rows, colw, q, no, hl_col=None, fill_by_status=None, note=None, ask=None):
    paper(p, strip=True)
    head(p, title, sub, ask)
    x = ML
    n = len(rows) + 1
    avail = 5.0
    rh = min(0.52, avail / n)
    sz = 12.5 if n <= 9 else (11.8 if n <= 12 else 11.2)
    gf = p.s.shapes.add_table(n, len(colw), Inches(x), Inches(1.52),
                              Inches(sum(colw)), Inches(rh * n))
    t = gf.table
    t.first_row = False
    t.horz_banding = False
    for i, w in enumerate(colw):
        t.columns[i].width = Inches(w)
    for r in range(n):
        t.rows[r].height = Inches(rh)

    def cell(r, c, txt, size=None, col=INK, b=False, fill=None, al=PP_ALIGN.CENTER, fnt=HAN):
        cl = t.cell(r, c)
        if fill is not None:
            cl.fill.solid(); cl.fill.fore_color.rgb = fill
        cl.margin_left = cl.margin_right = Inches(0.08)
        cl.margin_top = cl.margin_bottom = Inches(0.005)
        cl.vertical_anchor = MSO_ANCHOR.MIDDLE
        tf = cl.text_frame; tf.word_wrap = True
        pp = tf.paragraphs[0]; pp.alignment = al; pp.text = txt
        pp.font.size = Pt(size or sz); pp.font.color.rgb = col; pp.font.bold = b; pp.font.name = fnt

    for c, h in enumerate(header):
        cell(0, c, h, size=sz, col=WHITE, b=True, fill=VERM,
             al=PP_ALIGN.LEFT if c == 0 else PP_ALIGN.CENTER)
    for i, row in enumerate(rows, 1):
        st = fill_by_status(row[-1]) if fill_by_status else None
        bg = st[0] if st else (CARD2 if i % 2 == 0 else RGBColor(0xFA, 0xF7, 0xF0))
        for c, v in enumerate(row):
            col, b = INK, False
            if hl_col is not None and c == hl_col:
                s2 = fill_by_status(row[-1]) if fill_by_status else None
                if s2:
                    col, b = s2[1], 14.0
            if c == 0:
                b = True; col = INK
            cell(i, c, v, col=col, b=b, fill=bg, al=PP_ALIGN.LEFT if c == 0 else PP_ALIGN.CENTER)
    if note:
        ny = 1.52 + rh * n + 0.16
        rich(p, ML, ny, 12.1, min(0.86, 6.50 - ny), note, size=10.8, color=GREY, font=HAN, spacing=1.22, numcolor=VERM)
    if q:
        quote(p, q)
    foot(p, no)


# ── 06 双栏对比 ──
def p_compare(p, title, sub, left, right, q, no, rq=None, ask=None):
    paper(p)
    head(p, title, sub, ask)
    CW = 5.88
    for k, (tag, items, col) in enumerate(((left, left, VERM),)):
        pass
    for idx, (tag, items, col) in enumerate([(left[0], left[1], VERM), (right[0], right[1], BLUE)]):
        x = ML + idx * (CW + 0.34)
        card(p, x, 1.52, CW, 4.62)
        p.rect(x, 1.52, CW, 0.055, col)
        p.text(x + 0.26, 1.72, CW - 0.5, 0.44, tag, size=16, color=col, bold=True, font=KAI)
        yy = 2.34
        for it in items:
            p.rect(x + 0.26, yy + 0.10, 0.055, 0.24, col)
            rich(p, x + 0.46, yy, CW - 0.78, 0.74, it, size=12.2, color=TXT, font=HAN, spacing=1.28, numcolor=VERM)
            yy += 0.76
    if q:
        quote(p, q, rq or VERM)
    foot(p, no)


# ── 07 多卡（2/3/4 列）—— 卡片高度按内容行数自适应，并在内容区垂直居中 ──
def p_cards(p, title, sub, items, q, no, cols=3, rows_cards=None, accent=VERM, icontag=True, ask=None, size=12.2):
    import math
    paper(p)
    head(p, title, sub, ask)
    n = len(items)
    gap = 0.30
    CW = (12.10 - gap * (cols - 1)) / cols
    nr = rows_cards or ((n + cols - 1) // cols)
    AREA_TOP, AREA_BOT = 1.50, 6.44          # 内容区
    per_line = max(8, int((CW - 0.44) * (11.6 / size) / 0.176))   # 中文 ≈ 0.176in/字 @11.6pt

    def est(d):
        L = 0
        for seg in str(d).split("\n"):
            L += max(1, math.ceil(len(seg) / per_line))
        return L

    maxlines = max(est(d) for _, d in items)
    slot = 4.94 / nr
    CH = min(slot, max(0.96 + maxlines * 0.256, slot * 0.78))
    total = nr * CH + (nr - 1) * gap
    y0 = AREA_TOP + max(0.0, (AREA_BOT - AREA_TOP - total) / 2.0)

    for i, (t, d) in enumerate(items):
        r, c = divmod(i, cols)
        x = ML + c * (CW + gap)
        y = y0 + r * (CH + gap)
        card(p, x, y, CW, CH)
        p.rect(x, y, CW, 0.05, accent)
        p.text(x + 0.22, y + 0.34, CW - 0.44, 0.42, t, size=15, color=accent, bold=True, font=KAI)
        txt_h = est(d) * 0.335
        body_y = y + max(0.92, (CH - txt_h) / 2.0 + 0.16)
        rich(p, x + 0.22, body_y, CW - 0.44, CH - (body_y - y) - 0.18, d, size=size,
             color=TXT, font=HAN, spacing=1.28, numcolor=VERM)
    if q:
        quote(p, q)
    foot(p, no)


# ── 08 左文右图 ──
def p_imgright(p, title, sub, items, imgname, cap, q, no, ask=None):
    paper(p)
    head(p, title, sub, ask)
    y = 1.46
    for nm, ds in items:
        card(p, ML, y, 6.70, 1.42)
        p.rect(ML, y, 0.055, 1.42, VERM)
        p.text(ML + 0.24, y + 0.15, 6.2, 0.36, nm, size=14.8, color=VERM, bold=True, font=KAI)
        rich(p, ML + 0.24, y + 0.58, 6.2, 0.72, ds, size=11.4, color=TXT, font=HAN, spacing=1.24, numcolor=VERM)
        y += 1.60
    img(p, imgname, 7.60, 1.46, 4.62, 3.11)
    p.text(7.60, 4.62, 4.62, 0.30, cap, size=10.5, color=GREY, align=PP_ALIGN.CENTER)
    if q:
        quote(p, q)
    foot(p, no)


# ── 09 数据卡 ──
def p_data(p, title, sub, cards, body, q, no, ask=None):
    paper(p)
    head(p, title, sub, ask)
    n = len(cards)
    gap = 0.22
    CW = (12.10 - gap * (n - 1)) / n
    x = ML
    for num, lab, note in cards:
        card(p, x, 1.54, CW, 2.20)
        p.rect(x, 1.54, CW, 0.05, VERM)
        p.text(x + 0.20, 1.84, CW - 0.4, 0.88, num, size=40, color=VERM, bold=True, font=HAN)
        p.text(x + 0.20, 2.74, CW - 0.4, 0.50, lab, size=12, color=INK, bold=True, font=KAI)
        rich(p, x + 0.20, 3.18, CW - 0.4, 0.50, note, size=10.2, color=GREY, font=HAN, numcolor=VERM)
        x += CW + gap
    if body:
        rich(p, ML, 4.00, 12.1, 2.45, body, size=12.6, color=TXT, font=HAN, spacing=1.42, numcolor=VERM)
    if q:
        quote(p, q)
    foot(p, no)


# ── 10 大图占位（留空给陆老师插图）──
def p_placeholder(p, no, label, note=""):
    """大图占位页 —— 设计过的「待插图页」：宣纸底 + 虚线画框 + 图名 + 该页要表达什么。
    （不是空白页：空页会被看成"没做完"）"""
    paper(p)
    x0, y0, x1, y1 = 0.92, 1.28, SLW - 0.92, 5.62
    fr = p.rect(x0, y0, x1 - x0, y1 - y0, RGBColor(0xF6, 0xF2, 0xE8))
    fr.line.color.rgb = RGBColor(0xC0, 0xB5, 0x9E)
    fr.line.width = Pt(1.25)
    fr.line.dash_style = MSO_LINE_DASH_STYLE.DASH
    p.text(0, 2.66, SLW, 0.72, label, size=25, color=RGBColor(0x8C, 0x82, 0x6E),
           bold=True, font=KAI, align=PP_ALIGN.CENTER)
    p.text(1.40, 3.52, SLW - 2.80, 0.70, note or "（此处留空，待插大图）",
           size=13.5, color=RGBColor(0xA4, 0x9A, 0x86), font=HAN,
           align=PP_ALIGN.CENTER, spacing=1.30)
    p.text(0, 4.96, SLW, 0.40, "［ 大图占位 ｜ 待插图 ］", size=11.5,
           color=RGBColor(0xBF, 0xB5, 0xA1), font=HAN, align=PP_ALIGN.CENTER)
    p.page_no(no, x=12.35, y=7.02)


# ── 12 上图下注（实景照）──
def p_bigimg(p, imgname, title, q, no):
    paper(p)
    img(p, imgname, 0, 0, SLW, 5.58)
    p.rect(ML, 5.74, 0.055, 0.42, VERM)
    p.text(ML + 0.22, 5.78, 11.4, 0.44, title, size=21, color=INK, bold=True, font=KAI)
    p.text(ML + 0.22, 6.34, 11.8, 0.42, q, size=13.5, color=VERM, font=KAI)
    foot(p, no)


# ── 13 整页图（信息图/示意图）──
def p_fullimg(p, imgname, no):
    img(p, imgname, 0, 0, SLW, SLH)
    p.text(12.35, 7.02, 0.9, 0.36, no, size=11, color=RGBColor(0xC8, 0xC2, 0xB8),
           align=PP_ALIGN.CENTER)


# ── 14 产品截图三卡（截图 + 名称 + 说明）──
def p_prodcards(p, title, sub, items, q, no, ask=None):
    paper(p)
    head(p, title, sub, ask)
    gap = 0.30
    CW = (12.10 - gap * 2) / 3
    iw = CW - 0.28
    ih = iw / 1.6                      # 截图 2560x1600 = 1.6
    x = ML
    for imn, nm, ds in items:
        card(p, x, 1.50, CW, 4.90)
        p.rect(x, 1.50, CW, 0.05, VERM)
        img(p, imn, x + 0.14, 1.66, iw, ih)
        yb = 1.66 + ih + 0.16
        p.text(x + 0.22, yb, CW - 0.44, 0.40, nm, size=15, color=VERM, bold=True, font=KAI)
        p.text(x + 0.22, yb + 0.46, CW - 0.44, 4.90 - (yb - 1.50) - 0.62, ds,
               size=11.8, color=TXT, font=HAN, spacing=1.30)
        x += CW + gap
    if q:
        quote(p, q)
    foot(p, no)


# ── 11 收尾 ──
def p_summary(p, no, target, paths, landing, q=None):
    """汇总页（收尾倒数第二页）：一个总目标 → 六条路径 × 各自新词 → 一个落脚点。"""
    paper(p)
    head(p, "一页看完：一个总目标 · 六条路径 · 一个落脚点", "全篇的新词，各归各位")
    card(p, ML, 1.42, 12.10, 0.62)
    p.rect(ML, 1.42, 0.055, 0.62, VERM)
    rich(p, ML + 0.26, 1.56, 11.6, 0.36, target, size=13.6, color=INK, font=HAN, keycolor=VERM)
    gap = 0.22
    CW = (12.10 - gap * 2) / 3
    CH = 1.34
    for i, (name, words) in enumerate(paths):
        r, c = divmod(i, 3)
        x = ML + c * (CW + gap); y = 2.14 + r * (CH + 0.16)
        card(p, x, y, CW, CH)
        p.rect(x, y, CW, 0.05, VERM)
        p.text(x + 0.18, y + 0.16, CW - 0.36, 0.32, name, size=14, color=VERM, bold=True, font=KAI)
        rich(p, x + 0.18, y + 0.52, CW - 0.36, CH - 0.62, words, size=11.0, color=TXT,
             font=HAN, spacing=1.22, keycolor=VERM)
    card(p, ML, 5.06, 12.10, 0.62)
    p.rect(ML, 5.06, 0.055, 0.62, BLUE)
    rich(p, ML + 0.26, 5.21, 11.6, 0.36, landing, size=13.2, color=INK, font=HAN, keycolor=BLUE)
    if q:
        quote(p, q)
    foot(p, no)


def p_end(p, main, sub, no):
    img(p, "chapter-bg-b.jpg", 0, 0, SLW, SLH)
    p.rect(0.90, 2.52, 0.06, 1.86, RGBColor(0xE0, 0x70, 0x50))
    lines = main.split("\n")
    yy = 2.52
    for ln in lines:
        p.text(1.24, yy, 11.0, 0.72, ln, size=32, color=WHITE, bold=True, font=KAI)
        yy += 0.82
    p.text(1.26, 5.02, 10.0, 0.45, sub, size=15, color=RGBColor(0xDA, 0xD5, 0xCB), font=HAN)
    p.text(12.30, 6.98, 0.9, 0.36, no, size=11, color=RGBColor(0xB0, 0xAA, 0xA0), align=PP_ALIGN.CENTER)
