#!/usr/bin/env python3
"""
Render a 16:9 consulting-style product introduction PDF for
康比特食品安全监督管理系统.

The source content is intentionally kept in this script so later agents can
edit text, ordering, and layout without relying on the current chat context.
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Sequence

from PIL import Image, ImageDraw, ImageFont


ROOT = Path(__file__).resolve().parent
OUT_PDF = ROOT / "康比特食品安全监督管理系统产品简介_视觉版.pdf"
OUT_DIR = ROOT / "rendered"
OUT_DIR.mkdir(parents=True, exist_ok=True)

W, H = 1920, 1080
M = 104

COLORS = {
    "bg": "#F7F8FA",
    "paper": "#FFFFFF",
    "ink": "#111827",
    "muted": "#6B7280",
    "line": "#E5E7EB",
    "soft": "#F3F4F6",
    "orange": "#FF5011",
    "green": "#0F766E",
    "blue": "#2563EB",
    "teal": "#0891B2",
    "amber": "#B45309",
    "red": "#DC2626",
    "slate": "#374151",
    "dark": "#0B1220",
}

FONT_CANDIDATES = [
    "/System/Library/Fonts/STHeiti Medium.ttc",
    "/System/Library/Fonts/Hiragino Sans GB.ttc",
    "/System/Library/Fonts/STHeiti Light.ttc",
    "/System/Library/Fonts/Supplemental/Songti.ttc",
]


def find_font() -> str:
    for candidate in FONT_CANDIDATES:
        if Path(candidate).exists():
            return candidate
    raise FileNotFoundError("No Chinese font found in known macOS font paths")


FONT_PATH = find_font()


def font(size: int) -> ImageFont.FreeTypeFont:
    return ImageFont.truetype(FONT_PATH, size=size)


FONTS = {
    "title": font(68),
    "h1": font(50),
    "h2": font(34),
    "h3": font(28),
    "body": font(25),
    "small": font(20),
    "tiny": font(17),
    "metric": font(44),
}


@dataclass
class Slide:
    title: str
    subtitle: str
    kicker: str
    layout: str
    content: dict
    section: str


SLIDES: list[Slide] = [
    Slide(
        title="食品安全监督管理系统",
        subtitle="面向校园餐与集中供餐的数智监管平台",
        kicker="康比特 · 产品简介视觉版",
        layout="cover",
        section="定位",
        content={
            "statement": "把人员、食材、场所、事件和设备数据接入同一监管闭环，让食品安全从“靠检查”升级为“可预警、可追踪、可复核”。",
            "tags": ["校园餐", "集中供餐", "明厨亮灶", "AI 预警", "设备接入", "交付资产"],
            "version": "V1.0 · 2026-05-14",
        },
    ),
    Slide(
        title="为什么现在必须做",
        subtitle="食品安全治理正在从纸质迎检转向过程监管、数据协同和责任闭环。",
        kicker="01 背景变化",
        layout="cards",
        section="背景",
        content={
            "cards": [
                ("监管方式变化", "从事后抽查转向日常留痕、线上督办和异常复核。", "blue"),
                ("校园餐要求提高", "学校主体责任、膳食经费透明、家长参与监督成为高频关注。", "green"),
                ("一线台账负担重", "纸质记录分散、难检索，迎检和复盘成本持续增加。", "amber"),
                ("设备孤岛明显", "摄像头、晨检仪、留样柜、传感器有数据，但未形成统一预警。", "teal"),
            ],
            "bottom": "关键判断：客户不是只要一套视频系统，而是要能证明责任落实和问题闭环的管理系统。",
        },
    ),
    Slide(
        title="产品定位：不是单点工具，是监管闭环平台",
        subtitle="把功能介绍升级为管理逻辑，先讲清系统边界，再讲功能模块。",
        kicker="02 定位",
        layout="positioning",
        section="定位",
        content={
            "not": [
                "不是单一明厨亮灶视频",
                "不是纸质台账电子化",
                "不是设备采购清单",
                "不是一次性汇报大屏",
            ],
            "is": [
                "人、货、场、事统一管理",
                "PC、移动、大屏、公众端协同",
                "设备数据触发风险预警",
                "整改、复核、指标回流成闭环",
            ],
            "formula": ["客户场景", "软件入口", "硬件能力", "AI/规则预警", "交付资产"],
        },
    ),
    Slide(
        title="总体架构：一个监管底座，六个业务中心，N 类智能终端",
        subtitle="用一张图解释系统如何从数据、业务、设备到交付形成完整产品。",
        kicker="03 总览",
        layout="architecture",
        section="架构",
        content={
            "centers": [
                "食品安全智慧监管",
                "AI 视频巡查与物联网",
                "采购验收与库存追溯",
                "膳食经费与供应商监管",
                "营养食谱与公示评价",
                "应急指挥与社会共治",
            ],
            "devices": ["晨检仪", "AI 摄像头/盒子", "留样柜/秤", "农残检测仪", "温湿度/燃气/烟雾", "入库智能秤", "公示屏"],
        },
    ),
    Slide(
        title="角色地图：让每一方都知道系统替他解决什么",
        subtitle="产品简介要面向决策链路，不只面向操作员。",
        kicker="04 场景产品化",
        layout="role_cards",
        section="场景",
        content={
            "roles": [
                ("监管部门 / 管理者", "看全局态势、重点风险、整改进度和学校排名。", "一屏统览 / 督办下钻"),
                ("学校 / 食堂运营方", "明确每日待办，减少纸质台账，快速准备检查材料。", "任务闭环 / 一键导出"),
                ("从业人员", "用设备和移动端完成上报，减少重复填表。", "自动采集 / 流程提示"),
                ("供应商", "资质、批次、验收和履约记录可追溯。", "批次留痕 / 评价闭环"),
                ("家长 / 就餐者", "后厨可看、信息可查、反馈可追踪。", "透明公示 / 社会监督"),
            ],
        },
    ),
    Slide(
        title="场景地图：人、货、场、事四条监管线",
        subtitle="比按菜单罗列更清楚：每条线都能对应对象、动作、数据和验收指标。",
        kicker="05 场景产品化",
        layout="quadrants",
        section="场景",
        content={
            "items": [
                ("管人", "从业人员、食安总监、食安员、供应商人员", "健康证、晨检、培训、着装行为识别", "晨检完成率 / 资质有效率"),
                ("管货", "食材、商品、供应商、批次、库存", "采购入库、出库领用、农残快检、临期预警", "入库留痕率 / 异常闭环率"),
                ("管场", "后厨、仓库、留样区、洗消间、设备环境", "明厨亮灶、环境监测、消毒、废弃物、设备状态", "设备在线率 / 环境处置率"),
                ("管事", "风险、隐患、投诉、应急、整改", "预警分级、派发处理、复核关闭、指标回流", "整改闭环率 / 超期数量"),
            ],
        },
    ),
    Slide(
        title="多端入口：一套业务，多种使用方式",
        subtitle="让不同角色在正确入口完成正确动作，避免大屏、台账、设备各做各的。",
        kicker="06 软件平台化",
        layout="matrix",
        section="软件",
        content={
            "headers": ["入口", "面向对象", "主要能力", "交付证据"],
            "rows": [
                ["PC 管理端", "学校、食堂、监管科室", "基础数据、台账、预警、出入库、配置", "页面清单 / 权限表"],
                ["移动端", "管理者、一线人员", "预警接收、整改处理、拍照上报、视频查看", "演示脚本 / 操作手册"],
                ["大屏端", "监管领导、汇报场景", "综合驾驶舱、食安、采购库存、营养食谱", "大屏说明 / 指标口径"],
                ["公众入口", "家长、就餐者", "后厨直播、菜谱公示、评价反馈、投诉", "公示清单 / 反馈闭环"],
                ["设备 / API", "智能硬件、物联网平台", "自动采集、异常上报、在线状态同步", "接口契约 / 联调记录"],
            ],
        },
    ),
    Slide(
        title="数据闭环：从发现风险到复核关闭",
        subtitle="真正的产品价值不在采集多少数据，而在异常能否被处理、证明和复盘。",
        kicker="07 监管闭环",
        layout="flow",
        section="闭环",
        content={
            "steps": ["设备采集 / 台账上报", "AI 或规则识别", "预警分级", "人工复核", "整改派发", "责任人处理", "复核关闭", "指标回流"],
            "note": "每一步都要留下时间、责任人、图片/视频/记录和状态，才能从“看见问题”变成“解决问题”。",
        },
    ),
    Slide(
        title="核心能力一：管人",
        subtitle="把从业人员资质、上岗状态和行为规范纳入日常监管。",
        kicker="08 核心功能",
        layout="feature",
        section="功能",
        content={
            "lead": "从“谁在后厨”到“是否合规上岗”，形成可查询、可提醒、可追责的人员管理链路。",
            "features": [
                ("人员资质", "健康证、岗位、责任人、证照临期提醒"),
                ("每日晨检", "体温、手部卫生、人脸识别、异常停岗"),
                ("培训考试", "培训计划、考试记录、合格率统计"),
                ("行为识别", "未戴帽、未戴口罩、工作服不规范等 AI 预警"),
            ],
            "metrics": ["健康证有效率", "晨检完成率", "培训合格率", "行为预警处置率"],
        },
    ),
    Slide(
        title="核心能力二：管货",
        subtitle="把供应商、食材批次、验收入库、库存和检测结果连成追溯链。",
        kicker="09 核心功能",
        layout="feature",
        section="功能",
        content={
            "lead": "管理重点从“买了什么”升级为“谁供的、何时进的、是否检测、去了哪里”。",
            "features": [
                ("商品与供应商", "商品库、供应商资质、证照有效期、履约记录"),
                ("采购验收", "入库图片、重量、凭证、批次、验收人"),
                ("库存追溯", "出入库、领用、盘点、临期/过期预警"),
                ("农残快检", "检测结果、异常食材、批次关联、处置闭环"),
            ],
            "metrics": ["入库留痕率", "库存准确率", "农残异常闭环率", "供应商证照有效率"],
        },
    ),
    Slide(
        title="核心能力三：管场",
        subtitle="把后厨、仓库、留样、洗消和安全环境做成可视化监管对象。",
        kicker="10 核心功能",
        layout="feature",
        section="功能",
        content={
            "lead": "场所监管不只靠视频看见，还要把设备、环境和台账联动起来。",
            "features": [
                ("明厨亮灶", "实时视频、视频回放、重点点位巡查"),
                ("留样管理", "留样人、重量、标签、留存时长和异常提醒"),
                ("洗消与废弃物", "消毒记录、餐厨废弃物记录、图片佐证"),
                ("环境监测", "温湿度、燃气、烟雾、设备离线预警"),
            ],
            "metrics": ["设备在线率", "留样完整率", "洗消记录完整率", "环境异常处置率"],
        },
    ),
    Slide(
        title="核心能力四：管事",
        subtitle="把风险隐患、整改任务、投诉反馈和应急处置纳入统一状态流转。",
        kicker="11 核心功能",
        layout="feature",
        section="功能",
        content={
            "lead": "食品安全管理的关键不是发现问题，而是让问题有责任人、有期限、有复核。",
            "features": [
                ("预警概览", "证件到期、食材过期、AI 违规、设备离线等集中提醒"),
                ("整改派发", "责任人、期限、图片材料、处理说明和超期提示"),
                ("复核关闭", "复核人确认、状态归档、重复问题分析"),
                ("投诉与应急", "反馈登记、流转处理、事件记录和风险通报"),
            ],
            "metrics": ["整改闭环率", "平均处理时长", "超期数量", "重复问题下降率"],
        },
    ),
    Slide(
        title="AI 工程化：从“看视频”升级为“可评估的风险预警”",
        subtitle="AI 不是装饰能力，必须进入识别、复核、评估和知识库闭环。",
        kicker="12 AI 工程化",
        layout="ai",
        section="AI",
        content={
            "business": ["后厨行为识别", "鼠患/异物识别", "着装识别", "违规抓拍", "预警分类"],
            "engineering": ["Prompt 与上下文", "代码索引", "接口测试", "手册生成", "知识库问答"],
            "metrics": ["识别准确率", "误报率", "人工复核通过率", "处置时效", "重复预警下降"],
        },
    ),
    Slide(
        title="硬件体系化：把设备接入变成交付包",
        subtitle="每类设备都要明确数据、安装、联调、异常处理和验收证据。",
        kicker="13 硬件体系化",
        layout="devices",
        section="硬件",
        content={
            "devices": [
                ("晨检仪", "体温 / 手部卫生 / 人脸 / 晨检记录"),
                ("AI 摄像头/盒子", "实时视频 / 抓拍 / 行为识别 / 片段"),
                ("留样柜/秤", "菜品 / 重量 / 留样人 / 留存时长"),
                ("农残检测仪", "检测结果 / 批次 / 异常值"),
                ("环境传感器", "温湿度 / 燃气 / 烟雾 / 阈值告警"),
                ("入库智能秤", "重量 / 图片 / 凭证 / 供应商"),
                ("公示屏", "关键数据 / 视频画面 / 公示内容"),
            ],
            "package": ["设备清单", "协议/SDK", "字段映射", "点位与网络", "联调记录", "故障处理", "测试用例"],
        },
    ),
    Slide(
        title="大屏体系：1 个综合驾驶舱 + 3 个专题监管屏",
        subtitle="大屏不是炫技页面，而是领导汇报、日常监管和问题督办的统一入口。",
        kicker="14 大屏端",
        layout="dashboard",
        section="软件",
        content={
            "main": ("综合监管驾驶舱", "学校总数、今日预警、食安指数、营养评分、设备在线率、学校排名、督办事项"),
            "screens": [
                ("食品安全监管大屏", "视频墙、AI 抓拍、晨检、留样、消毒、农残、整改闭环"),
                ("膳食经费与采购库存", "采购计划、验收金额、库存预警、供应商排行、价格异常"),
                ("营养食谱监管大屏", "营养评分、达标分析、食谱公示率、采购保障率、评价反馈"),
            ],
        },
    ),
    Slide(
        title="平台价值：从“有系统”到“有管理抓手”",
        subtitle="销售表达要把角色收益和管理成果讲清楚。",
        kicker="15 价值",
        layout="value",
        section="价值",
        content={
            "values": [
                ("管理者", "省心安心，掌控全局", "轻松满足政策要求，清晰落实主体责任，数据驱动决策。"),
                ("执行者", "高效规范，管理轻松", "无纸化办公，任务在线分配，流程自动跟踪。"),
                ("从业人员", "简单智能，工作便捷", "设备自动上传，AI 实时提醒，减少重复填表。"),
                ("公众/家长", "透明可见，信任放心", "后厨可看、信息可查、反馈可追踪。"),
            ],
            "shift": [
                ("传统方式", "纸质台账、设备孤立、口头跟进、数据分散"),
                ("平台方式", "电子归档、设备联动、整改闭环、统一指标"),
            ],
        },
    ),
    Slide(
        title="交付资产化：做完以后留下可复用资产",
        subtitle="项目最终输出不能只剩口头结论，必须留下后续可复用的材料。",
        kicker="16 交付资产",
        layout="outputs",
        section="交付",
        content={
            "outputs": [
                ("产品地图", "场景、软件入口、硬件设备、状态、负责人、验收指标"),
                ("项目资料索引", "客户项目、阶段、源文件路径、售前/交付/售后导航"),
                ("API / 测试资产", "接口矩阵、OpenAPI/Apifox、异常和边界测试"),
                ("硬件接入包", "设备清单、协议/SDK、数据流、联调和故障处理"),
                ("AI 工程资产", "Prompt、MCP、上下文、代码索引、benchmark"),
                ("交付资产", "演示记录、用户手册、Word 手册、小视频、上线/复盘"),
            ],
        },
    ),
    Slide(
        title="比传统产品简介更清楚的三个升级",
        subtitle="本版重点不是堆功能，而是把客户决策、产品能力和交付验收连起来。",
        kicker="17 表达升级",
        layout="upgrade",
        section="表达",
        content={
            "upgrades": [
                ("从模块罗列到场景闭环", "先讲人、货、场、事，再对应页面、设备和指标。"),
                ("从设备展示到数据责任链", "每个设备说明采集什么、触发什么、谁处理、如何验收。"),
                ("从视觉包装到可继续改", "PDF 旁保留渲染脚本和证据文件，后续能按客户版本快速调整。"),
            ],
            "standard": "讲清楚、看得懂、能继续改。",
        },
    ),
    Slide(
        title="证据边界与下一步",
        subtitle="对外发布前，必须补齐客户案例、设备参数和 AI 指标，避免把推导写成事实。",
        kicker="18 风险边界",
        layout="evidence",
        section="边界",
        content={
            "confirmed": [
                "已有食品安全监督管理系统、校园食堂数智监管指挥中心、售前 PPT 和产品定义材料。",
                "已有一个平台 + N 类智能设备、PC/移动/大屏、多端能力和监管闭环思路。",
            ],
            "pending": [
                "已落地客户数量、学校/食堂数量、上线时间和客户授权案例。",
                "设备型号、协议、采购成本、售价、质保和售后承诺。",
                "AI 识别准确率、误报率、算法清单和人工复核机制。",
                "接口清单、字段、联调记录、异常测试和可公开界面截图。",
            ],
        },
    ),
]


def rgb(name: str) -> str:
    return COLORS[name]


def canvas() -> Image.Image:
    return Image.new("RGB", (W, H), rgb("bg"))


def rounded(draw: ImageDraw.ImageDraw, box: tuple[int, int, int, int], fill: str, outline: str | None = None, width: int = 1, radius: int = 8):
    draw.rounded_rectangle(box, radius=radius, fill=fill, outline=outline, width=width)


def text_size(draw: ImageDraw.ImageDraw, s: str, fnt: ImageFont.FreeTypeFont) -> tuple[int, int]:
    if not s:
        return 0, 0
    box = draw.textbbox((0, 0), s, font=fnt)
    return box[2] - box[0], box[3] - box[1]


def wrap(draw: ImageDraw.ImageDraw, s: str, fnt: ImageFont.FreeTypeFont, max_width: int) -> list[str]:
    lines: list[str] = []
    for para in s.split("\n"):
        if not para:
            lines.append("")
            continue
        buf = ""
        for ch in para:
            test = buf + ch
            if text_size(draw, test, fnt)[0] <= max_width:
                buf = test
            else:
                if buf:
                    lines.append(buf.rstrip())
                buf = ch.lstrip()
        if buf:
            lines.append(buf.rstrip())
    return lines


def draw_text(draw: ImageDraw.ImageDraw, xy: tuple[int, int], s: str, fnt: ImageFont.FreeTypeFont, fill: str, max_width: int | None = None, line_gap: int = 10) -> int:
    x, y = xy
    lines = wrap(draw, s, fnt, max_width) if max_width else s.split("\n")
    line_h = text_size(draw, "康", fnt)[1] + line_gap
    for line in lines:
        draw.text((x, y), line, font=fnt, fill=fill)
        y += line_h
    return y - xy[1]


def header(draw: ImageDraw.ImageDraw, slide: Slide):
    draw.rounded_rectangle((M, 64, M + 72, 72), radius=4, fill=rgb("orange"))
    draw.text((M, 88), slide.kicker, font=FONTS["small"], fill=rgb("orange"))
    draw.text((M, 126), slide.title, font=FONTS["h1"], fill=rgb("ink"))
    draw_text(draw, (M, 196), slide.subtitle, FONTS["body"], rgb("muted"), max_width=1220, line_gap=8)


def footer(draw: ImageDraw.ImageDraw, page_no: int, slide: Slide):
    y = H - 54
    draw.line((M, y - 18, W - M, y - 18), fill=rgb("line"), width=2)
    draw.text((M, y), "康比特食品安全监督管理系统产品简介 · 视觉版", font=FONTS["tiny"], fill=rgb("muted"))
    marker = f"{page_no:02d} / {len(SLIDES):02d} · {slide.section}"
    tw, _ = text_size(draw, marker, FONTS["tiny"])
    draw.text((W - M - tw, y), marker, font=FONTS["tiny"], fill=rgb("muted"))


def badge(draw: ImageDraw.ImageDraw, box: tuple[int, int, int, int], label: str, color: str):
    rounded(draw, box, "#FFF7ED", outline="#FED7AA", radius=8)
    tw, th = text_size(draw, label, FONTS["small"])
    draw.text(((box[0] + box[2] - tw) // 2, (box[1] + box[3] - th) // 2 - 2), label, font=FONTS["small"], fill=color)


def card(draw: ImageDraw.ImageDraw, box: tuple[int, int, int, int], title: str, body: str, accent: str = "orange"):
    rounded(draw, box, rgb("paper"), outline=rgb("line"), width=2, radius=8)
    x1, y1, x2, _ = box
    draw.rounded_rectangle((x1, y1, x1 + 10, y1 + 64), radius=4, fill=rgb(accent))
    draw.text((x1 + 34, y1 + 26), title, font=FONTS["h3"], fill=rgb("ink"))
    draw_text(draw, (x1 + 34, y1 + 78), body, FONTS["body"], rgb("muted"), max_width=x2 - x1 - 68, line_gap=9)


def layout_cover(draw: ImageDraw.ImageDraw, slide: Slide):
    draw.rectangle((0, 0, W, H), fill="#FBFBFC")
    draw.rounded_rectangle((96, 94, 1050, 986), radius=12, fill=rgb("dark"))
    draw.rounded_rectangle((96, 94, 1050, 986), radius=12, outline="#1F2937", width=2)
    draw.rounded_rectangle((130, 128, 218, 136), radius=4, fill=rgb("orange"))
    draw.text((130, 174), slide.kicker, font=FONTS["small"], fill="#FDBA74")
    draw_text(draw, (130, 250), slide.title, FONTS["title"], "#FFFFFF", max_width=790, line_gap=16)
    draw_text(draw, (132, 380), slide.subtitle, FONTS["h2"], "#E5E7EB", max_width=780, line_gap=12)
    draw_text(draw, (132, 502), slide.content["statement"], FONTS["body"], "#CBD5E1", max_width=780, line_gap=14)

    x, y = 130, 690
    for tag in slide.content["tags"]:
        tw, _ = text_size(draw, tag, FONTS["small"])
        badge(draw, (x, y, x + tw + 42, y + 46), tag, rgb("orange"))
        x += tw + 56
        if x > 870:
            x, y = 130, y + 60
    draw.text((132, 910), slide.content["version"], font=FONTS["small"], fill="#94A3B8")

    # Right-side visual system map.
    cx = 1440
    nodes = [
        ("场景", cx, 210, rgb("green")),
        ("软件", cx - 230, 395, rgb("blue")),
        ("硬件", cx + 230, 395, rgb("teal")),
        ("AI", cx - 150, 620, rgb("orange")),
        ("交付", cx + 150, 620, rgb("amber")),
    ]
    for i, (_, x1, y1, _) in enumerate(nodes):
        for _, x2, y2, _ in nodes[i + 1 :]:
            draw.line((x1, y1, x2, y2), fill="#D1D5DB", width=4)
    for label, x0, y0, color in nodes:
        draw.ellipse((x0 - 86, y0 - 86, x0 + 86, y0 + 86), fill="#FFFFFF", outline="#E5E7EB", width=3)
        draw.ellipse((x0 - 68, y0 - 68, x0 + 68, y0 + 68), outline=color, width=6)
        tw, th = text_size(draw, label, FONTS["h3"])
        draw.text((x0 - tw / 2, y0 - th / 2 - 2), label, font=FONTS["h3"], fill=rgb("ink"))
    draw_text(draw, (1180, 800), "客户场景 -> 软件入口 -> 硬件能力\n-> AI 工程底座 -> 交付资产", FONTS["h3"], rgb("ink"), max_width=660, line_gap=12)


def layout_cards(draw: ImageDraw.ImageDraw, slide: Slide):
    header(draw, slide)
    cards = slide.content["cards"]
    positions = [(M, 312, 890, 520), (930, 312, W - M, 520), (M, 560, 890, 768), (930, 560, W - M, 768)]
    for box, (title, body, accent) in zip(positions, cards):
        card(draw, box, title, body, accent)
    rounded(draw, (M, 824, W - M, 944), "#FFF7ED", outline="#FED7AA", radius=8)
    draw_text(draw, (M + 34, 862), slide.content["bottom"], FONTS["h3"], rgb("ink"), max_width=W - 2 * M - 68, line_gap=12)


def layout_positioning(draw: ImageDraw.ImageDraw, slide: Slide):
    header(draw, slide)
    left = (M, 315, 890, 718)
    right = (930, 315, W - M, 718)
    rounded(draw, left, rgb("paper"), outline=rgb("line"), width=2)
    rounded(draw, right, rgb("paper"), outline="#A7F3D0", width=2)
    draw.text((M + 40, 350), "不要只讲", font=FONTS["h2"], fill=rgb("red"))
    draw.text((970, 350), "应该讲成", font=FONTS["h2"], fill=rgb("green"))
    for i, item in enumerate(slide.content["not"]):
        y = 420 + i * 66
        draw.text((M + 48, y), f"{i + 1}. {item}", font=FONTS["body"], fill=rgb("muted"))
    for i, item in enumerate(slide.content["is"]):
        y = 420 + i * 66
        draw.text((970, y), f"{i + 1}. {item}", font=FONTS["body"], fill=rgb("ink"))
    y = 805
    x = M
    for i, step in enumerate(slide.content["formula"]):
        box = (x, y, x + 260, y + 82)
        rounded(draw, box, "#FFFFFF", outline=rgb("line"), radius=8)
        draw.text((x + 36, y + 25), step, font=FONTS["h3"], fill=rgb("ink"))
        if i < len(slide.content["formula"]) - 1:
            draw.line((x + 270, y + 41, x + 325, y + 41), fill=rgb("orange"), width=5)
            draw.polygon([(x + 325, y + 41), (x + 305, y + 28), (x + 305, y + 54)], fill=rgb("orange"))
        x += 330


def layout_architecture(draw: ImageDraw.ImageDraw, slide: Slide):
    header(draw, slide)
    rounded(draw, (M, 305, W - M, 430), rgb("dark"), radius=8)
    draw.text((M + 42, 344), "监管底座", font=FONTS["h2"], fill="#FFFFFF")
    draw_text(draw, (M + 250, 348), "学校 / 食堂 / 人员 / 供应商 / 商品 / 设备 / 权限 / 消息 / 预警 / 审计", FONTS["body"], "#CBD5E1", max_width=1300)
    centers = slide.content["centers"]
    x0, y0 = M, 486
    w, h, gap = 520, 118, 34
    for idx, center in enumerate(centers):
        row, col = divmod(idx, 3)
        x = x0 + col * (w + gap)
        y = y0 + row * (h + 32)
        card(draw, (x, y, x + w, y + h), center, "业务规则、流程、指标和数据归档", ["green", "blue", "teal", "orange", "amber", "slate"][idx])
    y = 820
    draw.text((M, y), "N 类智能终端", font=FONTS["h2"], fill=rgb("ink"))
    x = M + 300
    for d in slide.content["devices"]:
        tw, _ = text_size(draw, d, FONTS["small"])
        rounded(draw, (x, y - 8, x + tw + 40, y + 40), "#FFFFFF", outline=rgb("line"), radius=8)
        draw.text((x + 20, y + 4), d, font=FONTS["small"], fill=rgb("slate"))
        x += tw + 58


def layout_role_cards(draw: ImageDraw.ImageDraw, slide: Slide):
    header(draw, slide)
    y = 308
    for i, (role, need, value) in enumerate(slide.content["roles"]):
        x = M + (i % 2) * 860
        yy = y + (i // 2) * 185
        width = 810 if i < 4 else W - 2 * M
        if i == 4:
            x = M
        rounded(draw, (x, yy, x + width, yy + 146), rgb("paper"), outline=rgb("line"), width=2)
        draw.text((x + 34, yy + 26), role, font=FONTS["h3"], fill=rgb("ink"))
        draw_text(draw, (x + 34, yy + 72), need, FONTS["body"], rgb("muted"), max_width=width - 300, line_gap=8)
        badge(draw, (x + width - 250, yy + 46, x + width - 34, yy + 94), value, rgb("orange"))


def layout_quadrants(draw: ImageDraw.ImageDraw, slide: Slide):
    header(draw, slide)
    boxes = [(M, 300, 890, 540), (930, 300, W - M, 540), (M, 580, 890, 820), (930, 580, W - M, 820)]
    accents = ["green", "blue", "teal", "orange"]
    for box, item, accent in zip(boxes, slide.content["items"], accents):
        title, obj, actions, metric = item
        rounded(draw, box, rgb("paper"), outline=rgb("line"), width=2)
        draw.text((box[0] + 34, box[1] + 30), title, font=FONTS["h2"], fill=rgb(accent))
        draw_text(draw, (box[0] + 34, box[1] + 92), obj, FONTS["body"], rgb("ink"), max_width=box[2] - box[0] - 68)
        draw_text(draw, (box[0] + 34, box[1] + 146), actions, FONTS["small"], rgb("muted"), max_width=box[2] - box[0] - 68)
        badge(draw, (box[0] + 34, box[3] - 68, box[2] - 34, box[3] - 22), metric, rgb(accent))


def layout_matrix(draw: ImageDraw.ImageDraw, slide: Slide):
    header(draw, slide)
    x0, y0 = M, 300
    widths = [240, 360, 570, 430]
    row_h = 94
    x = x0
    for width, head in zip(widths, slide.content["headers"]):
        rounded(draw, (x, y0, x + width, y0 + 62), rgb("dark"), radius=0)
        draw.text((x + 20, y0 + 18), head, font=FONTS["small"], fill="#FFFFFF")
        x += width
    for r, row in enumerate(slide.content["rows"]):
        y = y0 + 62 + r * row_h
        x = x0
        fill = "#FFFFFF" if r % 2 == 0 else "#F9FAFB"
        for width, val in zip(widths, row):
            draw.rectangle((x, y, x + width, y + row_h), fill=fill, outline=rgb("line"))
            draw_text(draw, (x + 20, y + 22), val, FONTS["small"], rgb("ink") if x == x0 else rgb("muted"), max_width=width - 38, line_gap=5)
            x += width


def layout_flow(draw: ImageDraw.ImageDraw, slide: Slide):
    header(draw, slide)
    steps = slide.content["steps"]
    start_x, start_y = M, 390
    box_w, box_h = 365, 106
    gap_x, gap_y = 70, 145
    for i, step in enumerate(steps):
        row, col = divmod(i, 4)
        x = start_x + col * (box_w + gap_x)
        y = start_y + row * gap_y
        rounded(draw, (x, y, x + box_w, y + box_h), "#FFFFFF", outline=rgb("line"), width=2)
        draw.ellipse((x + 24, y + 28, x + 72, y + 76), fill=rgb("orange"))
        draw.text((x + 39, y + 38), str(i + 1), font=FONTS["small"], fill="#FFFFFF")
        draw_text(draw, (x + 92, y + 30), step, FONTS["body"], rgb("ink"), max_width=box_w - 120, line_gap=8)
        if col < 3:
            draw.line((x + box_w + 12, y + 53, x + box_w + 55, y + 53), fill=rgb("orange"), width=4)
            draw.polygon([(x + box_w + 55, y + 53), (x + box_w + 40, y + 43), (x + box_w + 40, y + 63)], fill=rgb("orange"))
    rounded(draw, (M, 780, W - M, 910), "#FFF7ED", outline="#FED7AA", radius=8)
    draw_text(draw, (M + 34, 824), slide.content["note"], FONTS["h3"], rgb("ink"), max_width=W - 2 * M - 68, line_gap=12)


def layout_feature(draw: ImageDraw.ImageDraw, slide: Slide):
    header(draw, slide)
    rounded(draw, (M, 300, W - M, 410), "#FFFFFF", outline=rgb("line"), width=2)
    draw_text(draw, (M + 34, 330), slide.content["lead"], FONTS["h3"], rgb("ink"), max_width=W - 2 * M - 68, line_gap=10)
    cards = slide.content["features"]
    for i, (title, body) in enumerate(cards):
        x = M + (i % 2) * 860
        y = 460 + (i // 2) * 170
        card(draw, (x, y, x + 810, y + 136), title, body, ["green", "blue", "teal", "orange"][i])
    y = 830
    x = M
    draw.text((x, y + 8), "验收指标", font=FONTS["h3"], fill=rgb("ink"))
    x += 180
    for metric in slide.content["metrics"]:
        tw, _ = text_size(draw, metric, FONTS["small"])
        badge(draw, (x, y, x + tw + 42, y + 48), metric, rgb("orange"))
        x += tw + 58


def layout_ai(draw: ImageDraw.ImageDraw, slide: Slide):
    header(draw, slide)
    left = (M, 312, 890, 720)
    right = (930, 312, W - M, 720)
    card(draw, left, "业务 AI", "把后厨视频、设备数据和台账异常转成可复核预警。", "orange")
    card(draw, right, "工程 AI", "把需求、代码、测试、文档和知识库纳入可复用流程。", "blue")
    for i, item in enumerate(slide.content["business"]):
        draw.text((M + 54, 448 + i * 48), f"· {item}", font=FONTS["body"], fill=rgb("ink"))
    for i, item in enumerate(slide.content["engineering"]):
        draw.text((970, 448 + i * 48), f"· {item}", font=FONTS["body"], fill=rgb("ink"))
    rounded(draw, (M, 795, W - M, 918), "#FFFFFF", outline=rgb("line"), width=2)
    draw.text((M + 34, 832), "评估口径", font=FONTS["h3"], fill=rgb("ink"))
    x = M + 190
    for metric in slide.content["metrics"]:
        tw, _ = text_size(draw, metric, FONTS["small"])
        badge(draw, (x, 824, x + tw + 42, 872), metric, rgb("green"))
        x += tw + 58


def layout_devices(draw: ImageDraw.ImageDraw, slide: Slide):
    header(draw, slide)
    devices = slide.content["devices"]
    for i, (name, data) in enumerate(devices):
        row, col = divmod(i, 3)
        x = M + col * 555
        y = 300 + row * 150
        rounded(draw, (x, y, x + 515, y + 116), "#FFFFFF", outline=rgb("line"), width=2)
        draw.text((x + 28, y + 24), name, font=FONTS["h3"], fill=rgb("ink"))
        draw_text(draw, (x + 28, y + 70), data, FONTS["small"], rgb("muted"), max_width=460, line_gap=5)
    rounded(draw, (M, 790, W - M, 918), "#FFF7ED", outline="#FED7AA", radius=8)
    draw.text((M + 34, 832), "硬件交付包", font=FONTS["h3"], fill=rgb("ink"))
    x = M + 230
    for item in slide.content["package"]:
        tw, _ = text_size(draw, item, FONTS["small"])
        badge(draw, (x, 824, x + tw + 42, 872), item, rgb("orange"))
        x += tw + 54


def layout_dashboard(draw: ImageDraw.ImageDraw, slide: Slide):
    header(draw, slide)
    rounded(draw, (M, 315, W - M, 485), rgb("dark"), radius=8)
    draw.text((M + 42, 352), slide.content["main"][0], font=FONTS["h2"], fill="#FFFFFF")
    draw_text(draw, (M + 42, 410), slide.content["main"][1], FONTS["body"], "#CBD5E1", max_width=W - 2 * M - 84, line_gap=8)
    for i, (name, body) in enumerate(slide.content["screens"]):
        x = M + i * 555
        y = 560
        card(draw, (x, y, x + 515, y + 220), name, body, ["green", "blue", "teal"][i])
    rounded(draw, (M, 842, W - M, 918), "#FFFFFF", outline=rgb("line"), radius=8)
    draw_text(draw, (M + 34, 864), "页面验收不只看视觉：还要能说明数据来源、指标口径、刷新频率、下钻路径和督办动作。", FONTS["h3"], rgb("ink"), max_width=W - 2 * M - 68)


def layout_value(draw: ImageDraw.ImageDraw, slide: Slide):
    header(draw, slide)
    for i, (role, headline, body) in enumerate(slide.content["values"]):
        x = M + (i % 2) * 860
        y = 300 + (i // 2) * 165
        rounded(draw, (x, y, x + 810, y + 132), "#FFFFFF", outline=rgb("line"), width=2)
        draw.text((x + 30, y + 22), role, font=FONTS["h3"], fill=rgb("orange"))
        draw.text((x + 170, y + 24), headline, font=FONTS["h3"], fill=rgb("ink"))
        draw_text(draw, (x + 30, y + 75), body, FONTS["small"], rgb("muted"), max_width=740, line_gap=5)
    y = 704
    for i, (title, body) in enumerate(slide.content["shift"]):
        x = M + i * 860
        rounded(draw, (x, y, x + 810, y + 165), "#F9FAFB" if i == 0 else "#ECFDF5", outline=rgb("line"), width=2)
        draw.text((x + 34, y + 32), title, font=FONTS["h3"], fill=rgb("ink"))
        draw_text(draw, (x + 34, y + 82), body, FONTS["body"], rgb("muted"), max_width=740, line_gap=9)


def layout_outputs(draw: ImageDraw.ImageDraw, slide: Slide):
    header(draw, slide)
    outputs = slide.content["outputs"]
    for i, (name, body) in enumerate(outputs):
        x = M + (i % 2) * 860
        y = 300 + (i // 2) * 160
        card(draw, (x, y, x + 810, y + 125), name, body, ["green", "blue", "teal", "orange", "amber", "slate"][i])


def layout_upgrade(draw: ImageDraw.ImageDraw, slide: Slide):
    header(draw, slide)
    for i, (title, body) in enumerate(slide.content["upgrades"]):
        y = 310 + i * 172
        rounded(draw, (M, y, W - M, y + 126), "#FFFFFF", outline=rgb("line"), width=2)
        draw.ellipse((M + 32, y + 34, M + 88, y + 90), fill=rgb(["green", "blue", "orange"][i]))
        draw.text((M + 52, y + 48), str(i + 1), font=FONTS["small"], fill="#FFFFFF")
        draw.text((M + 124, y + 28), title, font=FONTS["h3"], fill=rgb("ink"))
        draw_text(draw, (M + 124, y + 76), body, FONTS["body"], rgb("muted"), max_width=1350, line_gap=8)
    rounded(draw, (M, 845, W - M, 922), "#FFF7ED", outline="#FED7AA", radius=8)
    draw_text(draw, (M + 34, 866), f"本版验收标准：{slide.content['standard']}", FONTS["h3"], rgb("ink"), max_width=1200)


def layout_evidence(draw: ImageDraw.ImageDraw, slide: Slide):
    header(draw, slide)
    left = (M, 310, 890, 830)
    right = (930, 310, W - M, 830)
    rounded(draw, left, "#ECFDF5", outline="#A7F3D0", width=2)
    rounded(draw, right, "#FFF7ED", outline="#FED7AA", width=2)
    draw.text((M + 34, 350), "已确认依据", font=FONTS["h2"], fill=rgb("green"))
    y = 428
    for item in slide.content["confirmed"]:
        y += draw_text(draw, (M + 40, y), f"· {item}", FONTS["body"], rgb("ink"), max_width=730, line_gap=10) + 18
    draw.text((970, 350), "待确认后再对外承诺", font=FONTS["h2"], fill=rgb("orange"))
    y = 428
    for item in slide.content["pending"]:
        y += draw_text(draw, (970, y), f"· {item}", FONTS["body"], rgb("ink"), max_width=730, line_gap=10) + 14


LAYOUTS = {
    "cover": layout_cover,
    "cards": layout_cards,
    "positioning": layout_positioning,
    "architecture": layout_architecture,
    "role_cards": layout_role_cards,
    "quadrants": layout_quadrants,
    "matrix": layout_matrix,
    "flow": layout_flow,
    "feature": layout_feature,
    "ai": layout_ai,
    "devices": layout_devices,
    "dashboard": layout_dashboard,
    "value": layout_value,
    "outputs": layout_outputs,
    "upgrade": layout_upgrade,
    "evidence": layout_evidence,
}


def render_slide(slide: Slide, page_no: int) -> Image.Image:
    img = canvas()
    draw = ImageDraw.Draw(img)
    LAYOUTS[slide.layout](draw, slide)
    if slide.layout != "cover":
        footer(draw, page_no, slide)
    else:
        draw.text((W - M - 180, H - 80), f"{page_no:02d} / {len(SLIDES):02d}", font=FONTS["small"], fill=rgb("muted"))
    return img


def render() -> list[Image.Image]:
    images: list[Image.Image] = []
    for page_no, slide in enumerate(SLIDES, 1):
        img = render_slide(slide, page_no)
        images.append(img)
        img.save(OUT_DIR / f"page-{page_no:02d}.png", quality=95)
    first, rest = images[0], images[1:]
    first.save(OUT_PDF, save_all=True, append_images=rest, resolution=144.0)
    return images


def main() -> int:
    render()
    print(f"rendered: {OUT_PDF}")
    print(f"pages: {len(SLIDES)}")
    print(f"previews: {OUT_DIR}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
