Files
market/skills/guizang-ppt/assets/template-swiss.html
Yige b15fce19bf feat: add guizang-ppt market skill (vendored from op7418, AGPL-3.0) (#20)
## 概述 / Summary

新增市场技能 **`guizang-ppt`**(归藏网页 PPT),vendored 自上游开源项目
[op7418/guizang-ppt-skill](https://github.com/op7418/guizang-ppt-skill)。

Add a new **market skill** `guizang-ppt` — generates single-file HTML
horizontal-swipe slide decks (web PPT) in two visual systems (editorial
"magazine × e-ink" / "Swiss International"). Vendored from the upstream
open-source project.

## 变更内容 / Changes

- `skills/guizang-ppt/`:SKILL.md(DesireCore frontmatter 覆盖层 +
上游正文)、`references/`(10)、`assets/`(2 模板 + motion.min.js + 9 张
webp)、`scripts/validate-swiss-deck.mjs`、`LICENSE`(AGPL-3.0)、`NOTICE.md`(署名与合规)
- `skills/guizang-ppt/_desirecore/`:DesireCore 维护态(不随上游覆盖)
- `frontmatter.yaml`:市场 frontmatter 覆盖层(i18n: zh-CN 源 + en-US 显示串,body
暂回退中文,留给 CI 翻译)
  - `upstream.json`:上游溯源(commit `014c572`、AGPL-3.0、作者 歸藏/op7418)
- `scripts/vendor/guizang-ppt.mjs`:可复用的 vendor 更新脚本(`--src <本地路径>` 或
`--ref <tag>`)
- `manifest.json`:`version` → `1.2.3`,`stats.totalSkills` → 25

## 定位 / Positioning

- **仅市场可选安装**:未加入 `builtin-skills.json`,不随客户端开机自动安装;用户在市场按需安装。
- 分类 `creative`;与已有 `pptx`(生成 .pptx 文件)区分:本技能生成 **HTML deck**。

## 许可与署名 / License & Attribution

上游为 **AGPL-3.0**。本技能保留原 `LICENSE` 与作者署名(`NOTICE.md` + `metadata.author`
+ `market.maintainer`),作为聚合内容分发。

## 校验 / Validation

- `python3 scripts/i18n/validate-i18n.py skills/guizang-ppt` → `OK: no
i18n issues found.`

## 手动更新流程 / Manual update

上游发版时:`node scripts/vendor/guizang-ppt.mjs --ref <tag>` → 核对 diff → 必要时
bump `_desirecore/frontmatter.yaml#version` 与 `manifest.json` → 提交。

---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-04 11:04:05 +08:00

2420 lines
99 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>[必填] 替换为 PPT 标题 · Deck Title</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&family=JetBrains+Mono:wght@300;400;500;600&family=Noto+Sans+SC:wght@200;300;400;500;700;900&display=swap" rel="stylesheet">
<style>
:root{
/* ============ 主题色(默认: 🔵 克莱因蓝 IKB) ============
切换主题: 从 references/themes-swiss.md 复制对应的 :root 块
整体替换标有"主题色"注释的所有变量,其他散落的 var() 引用无需逐处改 */
--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; /* 高亮色: 克莱因蓝 IKB */
--accent-rgb:0,47,167;
--accent-on:#ffffff; /* accent 上的反色文字 */
--accent-bright:#5B7BFF; /* 暗底高亮: IKB 提亮版 */
/* ============ Carbon 文本角色 token (role-based,代替 opacity) ============
亮底: primary 主文 / secondary 次文 / helper 辅助 / placeholder 占位
暗底: inverse 自动反向 */
--text-primary:#0a0a0a; /* = ink, 100% */
--text-secondary:#525252; /* gray 70 */
--text-helper:#737373; /* gray 60 */
--text-placeholder:#a3a3a3; /* gray 40 */
--text-on-color:#ffffff; /* accent/dark 反色 */
--border-subtle:#e0e0e0; /* gray 20, 极细分隔 */
--border-strong:#a3a3a3; /* gray 40, 强分隔 */
/* ============ 字体(跨主题固定) ============ */
--sans:"Inter","Helvetica Neue","Helvetica","Arial","Segoe UI Variable","Segoe UI",system-ui,-apple-system,sans-serif;
--sans-zh:"PingFang SC","Hiragino Sans GB","Source Han Sans SC","Noto Sans SC","Microsoft YaHei UI","Microsoft YaHei","微软雅黑",sans-serif;
--mono:"JetBrains Mono","IBM Plex Mono","SF Mono","Cascadia Code","Consolas","Courier New",ui-monospace,monospace;
/* ============ Carbon 2x Grid 间距模数 (基础 8px) ============
参考: https://carbondesignsystem.com/elements/2x-grid/overview/
任何 padding/gap/margin 优先用这套 token,确保 8px 基线对齐 */
--sp-3:8px; /* 02 token */
--sp-4:12px; /* 03 */
--sp-5:16px; /* 04 */
--sp-6:24px; /* 05 */
--sp-7:32px; /* 06 */
--sp-8:40px; /* 07 */
--sp-9:48px; /* 08 */
--sp-10:64px; /* 09 */
--sp-11:80px; /* 10 */
--sp-12:96px; /* 11 */
--sp-13:160px; /* 12 */
/* ============ Carbon Motion tokens ============
https://carbondesignsystem.com/guidelines/motion/overview/
两套体系:productive (功能) 短 + 锐 / expressive (叙事) 长 + 软 */
--ease-prod:cubic-bezier(.2,0,.38,.9); /* productive standard */
--ease-exp:cubic-bezier(.4,.14,.3,1); /* expressive standard */
--ease-entry-prod:cubic-bezier(0,0,.38,.9); /* productive entrance */
--ease-entry-exp:cubic-bezier(0,0,.3,1); /* expressive entrance */
--dur-fast-1:.07s; /* 70ms */
--dur-fast-2:.11s; /* 110ms */
--dur-mod-1:.15s; /* 150ms */
--dur-mod-2:.24s; /* 240ms */
--dur-slow-1:.4s; /* 400ms */
--dur-slow-2:.7s; /* 700ms */
/* 底部分页组件安全区: 主内容最低处不要进入这条区域 */
--nav-safe-bottom:8vh;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{
width:100%;height:100%;overflow:hidden;
background:var(--paper);color:var(--ink);
font-family:var(--sans),var(--sans-zh);
font-feature-settings:"ss01","cv11";
-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility
}
/* ============ WebGL 网格背景 ============ */
canvas.bg{position:fixed;inset:0;width:100vw;height:100vh;z-index:0;display:block;opacity:.55;mix-blend-mode:multiply;pointer-events:none}
body.dark-bg canvas.bg{mix-blend-mode:screen;opacity:.42}
body.low-power canvas.bg,
body.low-power canvas.ascii-bg{display:none!important}
/* ============ Deck 容器 + 翻页 ============ */
#deck{position:fixed;inset:0;width:10000vw;height:100vh;display:flex;flex-wrap:nowrap;transition:transform .9s cubic-bezier(.77,0,.175,1);z-index:10;will-change:transform}
.slide{
width:100vw;height:100vh;flex:0 0 100vw;
position:relative;
padding:5.5vh 5vw 7vh 5vw;
display:flex;flex-direction:column;
overflow:hidden;
background:var(--paper);color:var(--ink);
}
.slide.grey{background:var(--grey-1)}
.slide.dark{background:var(--ink);color:var(--paper)}
.slide.dark .grey-only{display:none}
.slide.accent{background:var(--accent);color:var(--accent-on)}
.slide.accent .accent-block{background:var(--accent-on);color:var(--accent)}
/* Hero 页透出网格背景多一点 */
.slide.hero{background:transparent}
.slide.hero.grey{background:rgba(var(--paper-rgb),.86);backdrop-filter:blur(1px)}
.slide.hero.dark{background:rgba(var(--ink-rgb),.92);color:var(--paper)}
/* ============ 装饰: 极细分隔线 + 点阵矩阵 ============ */
.rule{width:100%;height:1px;background:currentColor;opacity:.18;margin:0}
.rule.thick{height:2px;opacity:.85}
.rule.accent{background:var(--accent);opacity:1;height:2px}
.rule.v{width:1px;height:100%;margin:0}
/* 点阵矩阵 (dot matrix) - 用 radial-gradient */
.dots{
background-image:radial-gradient(currentColor 1px, transparent 1px);
background-size:12px 12px;
background-position:0 0;
opacity:.18;
}
.dots-fine{
background-image:radial-gradient(currentColor 0.8px, transparent 0.8px);
background-size:8px 8px;
opacity:.14;
}
.dots-bold{
background-image:radial-gradient(currentColor 1.4px, transparent 1.4px);
background-size:18px 18px;
opacity:.22;
}
/* ============ Chrome (顶部 meta) + Foot (底部) ============ */
.chrome{
display:flex;justify-content:space-between;align-items:flex-start;
font-family:var(--mono);
font-size:14px;letter-spacing:.16em;text-transform:uppercase;
opacity:.7;margin-bottom:auto
}
.chrome .l,.chrome .r{display:flex;gap:1.6em;align-items:center}
.chrome .sep{width:24px;height:1px;background:currentColor;opacity:.5}
.foot{
margin-top:auto;
display:flex;justify-content:space-between;align-items:flex-end;
font-family:var(--mono);
font-size:14px;letter-spacing:.14em;text-transform:uppercase;
opacity:.55;
padding-top:2vh;
border-top:1px solid currentColor;
border-color:rgba(127,127,127,.25)
}
.foot .nb{font-family:var(--sans);font-weight:600;letter-spacing:.04em}
/* ============ Tag / Kicker / Meta 标签 ============ */
.kicker{
font-family:var(--mono);
font-size:14px;letter-spacing:.24em;text-transform:uppercase;
opacity:.65;margin-bottom:2.4vh;
display:inline-flex;align-items:center;gap:.8em
}
.kicker::before{
content:"";width:24px;height:1px;background:currentColor;opacity:.6
}
.kicker.no-line::before{display:none}
.kicker.accent{color:var(--accent);opacity:1;font-weight:600}
.tag{
display:inline-block;
font-family:var(--mono);
font-size:14px;letter-spacing:.18em;text-transform:uppercase;
padding:5px 10px;border:1px solid currentColor;opacity:.85
}
.tag.solid{background:currentColor;color:var(--paper);border-color:transparent}
.tag.accent{background:var(--accent);color:var(--accent-on);border-color:transparent;opacity:1}
/* ============ 标题层级 (无衬线 · 极轻字重 · 极致字号对比) ============ */
.h-hero{
font-family:var(--sans),var(--sans-zh);
font-weight:200;
font-size:11vw;
line-height:.92;
letter-spacing:-.04em;
}
.h-hero-zh{
font-family:var(--sans),var(--sans-zh);
font-weight:200;
font-size:8.4vw;
line-height:.96;
letter-spacing:-.025em;
}
.h-xl{
font-family:var(--sans),var(--sans-zh);
font-weight:200;
font-size:6vw;
line-height:1;
letter-spacing:-.03em;
}
.h-xl-zh{
font-family:var(--sans),var(--sans-zh);
font-weight:200;
font-size:5vw;
line-height:1.05;
letter-spacing:-.025em;
}
.h-md{
font-family:var(--sans),var(--sans-zh);
font-weight:300;
font-size:2.6vw;
line-height:1.18;
letter-spacing:-.015em;
}
.h-sub{
font-family:var(--sans),var(--sans-zh);
font-weight:400;
font-size:2.2vw;
line-height:1.3;
letter-spacing:-.01em;
opacity:.7;
}
/* ============ 正文 / 引语 / 元数据 ============ */
.lead{
font-family:var(--sans),var(--sans-zh);
font-weight:400;
font-size:1.55vw;
line-height:1.4;
letter-spacing:-.005em;
opacity:.86;
}
.body{
font-family:var(--sans),var(--sans-zh);
font-weight:400;
font-size:max(18px,1.08vw);
line-height:1.6;
letter-spacing:0;
opacity:.78;
}
.body-sm{
font-family:var(--sans),var(--sans-zh);
font-weight:400;
font-size:max(16px,.92vw);
line-height:1.55;
opacity:.7;
}
.meta{
font-family:var(--mono);
font-size:max(14px,.82vw);
letter-spacing:.18em;text-transform:uppercase;
opacity:.6;
}
.meta-row{
display:flex;gap:1.4em;align-items:baseline;flex-wrap:wrap;
font-family:var(--mono);
font-size:max(14px,.88vw);letter-spacing:.16em;text-transform:uppercase;
opacity:.65;
}
.meta-row span:not(.dot){display:inline-block}
.meta-row .dot{
display:inline-block;width:4px;height:4px;border-radius:50%;
background:currentColor;opacity:.5;vertical-align:middle
}
/* ============ KPI Hero (视觉英雄数据) ============ */
.kpi-hero{
font-family:var(--sans);
font-weight:800;
font-size:22vw;
line-height:.82;
letter-spacing:-.05em;
font-feature-settings:"tnum","ss01";
}
.kpi-hero .unit{
font-family:var(--sans),var(--sans-zh);
font-weight:500;
font-size:.18em;
letter-spacing:0;
opacity:.5;
margin-left:.12em;
vertical-align:.5em;
}
.kpi-hero.accent{color:var(--accent)}
.kpi-big{
font-family:var(--sans);
font-weight:800;
font-size:11vw;
line-height:.85;
letter-spacing:-.04em;
font-feature-settings:"tnum"
}
.kpi-mid{
font-family:var(--sans);
font-weight:700;
font-size:6vw;
line-height:.88;
letter-spacing:-.03em;
font-feature-settings:"tnum"
}
/* ============ Stat Card (数据卡片 · 极简) ============ */
.stat-card{
display:flex;flex-direction:column;
gap:.6vh;align-items:flex-start;
padding-top:1.6vh;
border-top:2px solid currentColor;
}
.stat-card.thin{border-top-width:1px;border-color:rgba(127,127,127,.4)}
.stat-card.accent-top{border-top-color:var(--accent);border-top-width:3px}
.stat-card .stat-label{
font-family:var(--mono);
font-size:max(14px,.82vw);
letter-spacing:.24em;text-transform:uppercase;
opacity:.6;
}
.stat-card .stat-nb{
font-family:var(--sans);
font-weight:800;
font-size:5.6vw;
line-height:.88;
letter-spacing:-.035em;
font-feature-settings:"tnum";
margin-top:.4vh;
}
.stat-card .stat-nb .stat-unit{
font-family:var(--sans),var(--sans-zh);
font-weight:500;
font-size:.32em;
letter-spacing:0;
opacity:.6;
margin-left:.14em;
vertical-align:.4em;
}
.stat-card .stat-note{
font-family:var(--sans),var(--sans-zh);
font-weight:400;
font-size:max(16px,.98vw);
line-height:1.5;
opacity:.7;
margin-top:.6vh;
}
.grid-4 .stat-card .stat-nb{font-size:4.6vw}
.grid-3 .stat-card .stat-nb{font-size:6.4vw}
.grid-6 .stat-card .stat-nb{font-size:4vw}
/* ============ Accent Block (高亮色块包裹内容) ============ */
.accent-block{
background:var(--accent);color:var(--accent-on);
padding:2.4vh 2vw;
}
.accent-block.tight{padding:1.4vh 1.4vw}
.accent-block .h-md,.accent-block .h-xl,.accent-block .kpi-mid{color:var(--accent-on)}
.ink-block{
background:var(--ink);color:var(--paper);
padding:2.4vh 2vw
}
.grey-block{
background:var(--grey-1);
padding:2.4vh 2vw
}
/* 高亮文字 (mark 风格) */
.mark{
background:var(--accent);color:var(--accent-on);
padding:0 .2em;
box-decoration-break:clone;
-webkit-box-decoration-break:clone;
}
.mark.ink{background:var(--ink);color:var(--paper)}
.underline-accent{
background-image:linear-gradient(to bottom, transparent 70%, var(--accent) 70%, var(--accent) 96%, transparent 96%);
padding:0 .05em
}
/* ============ Pipeline / Step ============ */
.pipeline-section{margin-top:3.2vh;padding-top:2.2vh;border-top:1px solid rgba(127,127,127,.3)}
.pipeline-section:first-of-type{border-top:0;padding-top:0;margin-top:2.4vh}
.pipeline-label{
font-family:var(--mono);
font-size:max(14px,.84vw);
letter-spacing:.24em;text-transform:uppercase;
opacity:.6;margin-bottom:1.8vh;
}
.pipeline{
display:grid;
grid-template-columns:repeat(5,1fr);
gap:1vw;
}
.pipeline[data-cols="3"]{grid-template-columns:repeat(3,1fr)}
.pipeline[data-cols="4"]{grid-template-columns:repeat(4,1fr)}
.pipeline[data-cols="6"]{grid-template-columns:repeat(6,1fr)}
.step{
display:flex;flex-direction:column;gap:.6vh;
padding-top:1.2vh;
border-top:2px solid currentColor;
}
.step.accent-top{border-top-color:var(--accent);border-top-width:3px}
.step-nb{
font-family:var(--mono);
font-weight:500;
font-size:max(14px,1vw);
opacity:.5;letter-spacing:.04em
}
.step-title{
font-family:var(--sans),var(--sans-zh);
font-weight:700;
font-size:1.4vw;
letter-spacing:-.01em;
line-height:1.2;
}
.step-desc{
font-family:var(--sans),var(--sans-zh);
font-weight:400;
font-size:max(16px,.94vw);
line-height:1.45;
opacity:.7;
}
/* ============ 网格系统 (模块化网格) ============ */
.frame{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden}
.frame.grid-2-7-5,
.frame.grid-2-6-6,
.frame.grid-2-8-4,
.frame.grid-2-4-8,
.frame.grid-3-3,
.frame.grid-12,
.frame.grid-6,
.frame.grid-4,
.frame.grid-3{display:grid}
/* 12 列模块化网格 (瑞士风核心) */
.grid-12{
display:grid;
grid-template-columns:repeat(12,1fr);
gap:2vh 1.2vw;
align-items:start;
}
.span-2{grid-column:span 2}
.span-3{grid-column:span 3}
.span-4{grid-column:span 4}
.span-5{grid-column:span 5}
.span-6{grid-column:span 6}
.span-7{grid-column:span 7}
.span-8{grid-column:span 8}
.span-9{grid-column:span 9}
.span-12{grid-column:span 12}
/* 经典分栏 */
.grid-2-7-5{display:grid;grid-template-columns:7fr 5fr;gap:3vw 4vh;align-items:start}
.grid-2-6-6{display:grid;grid-template-columns:1fr 1fr;gap:3vw 4vh;align-items:start}
.grid-2-8-4{display:grid;grid-template-columns:8fr 4fr;gap:3vw 4vh;align-items:start}
.grid-2-4-8{display:grid;grid-template-columns:4fr 8fr;gap:3vw 4vh;align-items:start}
.grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:3vw 4vh;align-items:start}
.grid-3-3{display:grid;grid-template-columns:repeat(3,1fr);grid-auto-rows:minmax(0,1fr);gap:2.4vh 2vw}
.grid-4{display:grid;grid-template-columns:repeat(2,1fr);grid-template-rows:repeat(2,1fr);gap:3vh 3vw;flex:1;align-content:center}
.grid-6{display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(2,1fr);gap:3vh 2.4vw;flex:1;align-content:center}
/* 工具类 */
.col{display:flex;flex-direction:column;gap:2vh}
.row{display:flex;align-items:center;gap:2vw}
.fill{flex:1}
.center{align-items:center;justify-content:center;text-align:center}
.right{text-align:right;justify-self:end}
.top{align-self:start}
.bottom{align-self:end}
.va-center{align-self:center}
.nowrap{white-space:nowrap}
/* 非对称定位 */
.pos-absolute{position:absolute}
.bottom-left{position:absolute;left:5vw;bottom:8vh;max-width:50vw}
.bottom-right{position:absolute;right:5vw;bottom:8vh;max-width:50vw;text-align:right}
.top-right{position:absolute;right:5vw;top:5.5vh;text-align:right}
.top-left{position:absolute;left:5vw;top:5.5vh}
/* ============ Callout (引用框 · 极简) ============ */
.callout{
padding:2vh 2vw;
border-left:3px solid var(--accent);
font-family:var(--sans),var(--sans-zh);
font-size:max(14px,1vw);
line-height:1.55;
opacity:.9;
}
.callout.ink{border-left-color:currentColor}
.callout .cite,.callout .callout-src{
display:block;margin-top:1.2vh;
font-family:var(--mono);
font-size:14px;letter-spacing:.16em;text-transform:uppercase;
opacity:.6;
}
/* ============ Icons (Lucide via CDN) ============ */
.ico{width:1em;height:1em;display:inline-block;vertical-align:-.12em;stroke:currentColor;fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}
.ico-lg,.ico-md,.ico-sm{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round}
.ico-lg{width:2.4vw;height:2.4vw;stroke-width:1.4;display:inline-block}
.ico-md{width:1.6vw;height:1.6vw;stroke-width:1.6;display:inline-block;vertical-align:-.4em}
.ico-sm{width:1vw;height:1vw;stroke-width:1.8;display:inline-block;vertical-align:-.15em;opacity:.7}
/* ============ 几何小图标 (装饰) ============ */
.geo-dot{width:.7vw;height:.7vw;border-radius:50%;background:var(--accent);display:inline-block;vertical-align:middle}
.geo-square{width:.7vw;height:.7vw;background:var(--accent);display:inline-block;vertical-align:middle}
.geo-line{width:2vw;height:2px;background:var(--accent);display:inline-block;vertical-align:middle}
.geo-circle-o{width:.9vw;height:.9vw;border:2px solid currentColor;border-radius:50%;display:inline-block;vertical-align:middle}
/* ============ 图片 frame-img ============ */
.frame-img{overflow:hidden;position:relative;background:var(--paper);box-sizing:border-box;width:100%}
.slide.dark .frame-img{background:rgba(255,255,255,.06)}
.frame-img > img{width:100%;height:100%;object-fit:cover;object-position:center center;display:block}
.frame-img.fit-contain > img{object-fit:contain;object-position:center center}
.frame-img.pos-top > img{object-position:top center}
.frame-img.pos-face > img{object-position:center 35%}
.frame-img.r-16x9{aspect-ratio:16/9;max-height:64vh}
.frame-img.r-21x9{aspect-ratio:21/9;max-height:54vh}
.frame-img.r-16x10{aspect-ratio:16/10;max-height:56vh}
.frame-img.r-4x3{aspect-ratio:4/3;max-height:56vh}
.frame-img.r-3x2{aspect-ratio:3/2;max-height:46vh}
.frame-img.r-3x4{aspect-ratio:3/4;max-height:60vh}
.frame-img.r-1x1{aspect-ratio:1/1;max-height:50vh}
.frame-img.h-16{height:16vh}
.frame-img.h-18{height:18vh}
.frame-img.h-22{height:22vh}
.frame-img.h-26{height:26vh}
.frame-img.h-28{height:28vh}
.frame-img.h-32{height:32vh}
.frame-img.swiss-lined{border-top:2px solid var(--accent)}
.frame-img.swiss-keyline{border:0}
/* P22 Image Hero: 下半屏内容区必须和图片拉开距离,不要贴在图下沿 */
.image-hero-body{
display:grid;grid-template-columns:6fr 6fr;gap:3vw;
padding:6.6vh 5vw 4.4vh;flex:1;align-content:start
}
.image-hero-stats{
display:grid;grid-template-columns:repeat(3,1fr);gap:2vw;align-items:start
}
.img-cap{
display:block;margin-top:.8vh;
font-family:var(--mono);
font-size:max(14px,.82vw);
letter-spacing:.2em;text-transform:uppercase;
opacity:.6;
}
figure.frame-img,figure.tile{margin:0;display:flex;flex-direction:column;min-width:0}
figure.tile > .frame-img{flex:0 0 auto}
/* 瑞士风图文混排: 只用直角、发丝线、单一 accent图像容器不加圆角/阴影 */
.swiss-img-split{display:grid;grid-template-columns:5fr 7fr;gap:3vw;align-items:start;flex:1;min-height:0}
.swiss-img-split.reverse{grid-template-columns:7fr 5fr}
.swiss-img-split.align-bottom{align-items:end}
.swiss-img-split.align-bottom .swiss-img-copy{align-self:end}
.swiss-img-split.align-image-bottom{align-items:end;padding-bottom:var(--nav-safe-bottom)}
.swiss-img-split.align-image-bottom .swiss-img-copy{align-self:end}
.swiss-img-split.align-image-bottom figure.tile{align-self:end;position:relative}
.swiss-img-split.align-image-bottom figure.tile > .swiss-img-caption{
position:absolute;left:0;right:0;top:calc(100% + var(--sp-4));margin-top:0
}
.nav-safe-bottom{padding-bottom:var(--nav-safe-bottom)}
.nav-safe-bottom-tight{padding-bottom:calc(var(--nav-safe-bottom) * .65)}
.swiss-img-copy{display:flex;flex-direction:column;gap:var(--sp-6);min-width:0}
.swiss-img-copy .rule{margin:0}
.swiss-img-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--sp-5);align-items:start;margin-top:var(--sp-7)}
.swiss-img-grid.two{grid-template-columns:repeat(2,1fr)}
.swiss-img-grid.tight{margin-top:0}
.swiss-img-caption{
display:flex;justify-content:space-between;gap:var(--sp-5);
margin-top:var(--sp-4);
font-family:var(--mono);font-size:max(14px,.74vw);
letter-spacing:.16em;text-transform:uppercase;color:var(--text-helper);
border-top:1px solid var(--border-subtle);padding-top:var(--sp-4)
}
.swiss-img-caption strong{
font-family:var(--sans),var(--sans-zh);font-size:max(16px,.9vw);
font-weight:600;letter-spacing:0;text-transform:none;color:var(--text-primary)
}
/* ============ Bar Chart (扁平几何图表) ============ */
.bar-chart{display:flex;flex-direction:column;gap:1.4vh}
.bar-row{display:grid;grid-template-columns:8em 1fr 4em;gap:1.4vw;align-items:center}
.bar-row .bar-label{font-family:var(--mono);font-size:max(14px,.84vw);letter-spacing:.14em;text-transform:uppercase;opacity:.7}
.bar-row .bar-track{height:14px;background:rgba(127,127,127,.18);position:relative}
.bar-row .bar-fill{height:100%;background:var(--accent);position:absolute;left:0;top:0}
.bar-row .bar-fill.ink{background:currentColor}
.bar-row .bar-value{font-family:var(--sans);font-weight:700;font-size:max(16px,1.05vw);text-align:right;font-feature-settings:"tnum"}
/* ============ 导航 ============ */
/* 底部分页导航: 仅保留方块本身,无背景无描边 */
#nav{position:fixed;left:50%;bottom:2vh;transform:translateX(-50%);z-index:30;display:flex;gap:10px;padding:0;background:transparent;border:0}
#nav .dot{width:6px;height:6px;background:rgba(0,0,0,.28);cursor:pointer;transition:all .25s ease;border:0;padding:0;border-radius:0}
#nav .dot:hover{background:rgba(0,0,0,.55)}
#nav .dot.active{background:var(--accent);width:18px}
body.dark-bg #nav .dot{background:rgba(255,255,255,.32)}
body.dark-bg #nav .dot.active{background:var(--accent)}
#hint{position:fixed;bottom:2.4vh;right:2.5vw;z-index:30;font-family:var(--mono);font-size:14px;letter-spacing:.14em;text-transform:uppercase;opacity:.4;color:var(--ink-tint, currentColor)}
body.dark-bg #hint{color:var(--paper);opacity:.4}
body.low-power #hint{color:var(--accent);opacity:.72}
body.dark-bg.low-power #hint{color:var(--paper);opacity:.72}
/* ESC 索引页: 动画元素强制可见 (覆盖 motion-ready 的 opacity:0) */
#overview [data-anim]{opacity:1!important;transform:none!important}
#overview .slide *{animation:none!important;transition:none!important}
/* 统一卡片样式 token: card-fill (默认灰底 · 中性) + card-ink (反转高对比) + card-accent (单一焦点) */
.card-fill{background:#f5f5f4;border:0;color:var(--text-primary)}
.card-ink{background:var(--ink);border:0;color:var(--paper)}
.card-ink .t-meta,.card-ink .t-cat{color:rgba(255,255,255,.6)}
.card-accent{background:var(--accent);border:0;color:var(--accent-on)}
.card-accent .t-meta,.card-accent .t-cat{color:var(--accent-on)}
/* ============ 动效 (与原模板一致) ============ */
[data-anim]{opacity:1}
body.motion-ready [data-anim]{opacity:0}
body.motion-ready [data-anim="left"]{transform:translateX(-24px)}
body.motion-ready [data-anim="right"]{transform:translateX(24px)}
body.motion-ready [data-anim="line"]{opacity:0;transform:translateY(10px)}
body.motion-ready [data-animate="pipeline"] [data-anim]{opacity:.15}
body.low-power #deck{transition:none!important}
body.low-power *,
body.low-power *::before,
body.low-power *::after{animation:none!important;transition:none!important}
body.low-power.motion-ready [data-anim],
body.low-power [data-anim]{opacity:1!important;transform:none!important}
/* ============================================================
↓↓↓ V2 EXTENSIONS · Canvas Mode + 参考图新类
验证效果后沉淀回 template-swiss.html
============================================================ */
/* Windows 适配:雅黑没有 ExtraLight 200,中文大字号字重补偿 */
body.is-win .name-mega,
body.is-win .num-mega,
body.is-win .kpi-thin,
body.is-win .tl-node .multi{
font-weight:300;letter-spacing:-.02em;
}
body.is-win [style*="font-weight:200"]{font-weight:300 !important}
/* 全屏铺满模式 · 关闭 WebGL · 卡片即页面 */
body.canvas-mode{background:var(--paper)}
body.canvas-mode canvas.bg{display:none !important}
body.canvas-mode .slide{background:var(--paper);padding:0;align-items:stretch;justify-content:stretch}
body.canvas-mode .slide.hero{background:var(--paper)}
.canvas-card{
width:100vw;
height:100vh;
background:var(--paper);color:var(--ink);
padding:5.6vh 5vw 4.4vh;
display:flex;flex-direction:column;
position:relative;overflow:hidden;
box-shadow:none;
border-radius:0;
}
.slide.dark .canvas-card{background:var(--ink);color:var(--paper)}
.slide.accent .canvas-card{background:var(--accent);color:var(--accent-on)}
.slide.grey .canvas-card{background:var(--grey-1);color:var(--ink)}
.slide.split .canvas-card{padding:0;flex-direction:row}
/* ============ ASCII 点阵呼吸场 · IKB 封面/封底专用 ============
用法:在 .canvas-card(或 split .half.b-accent)内首位插入 <canvas class="ascii-bg" aria-hidden="true">.
动画由本文件底部的 ASCII IIFE 自动启动,所有 canvas.ascii-bg 都会被扫到.
其他内容靠 .canvas-card > *:not(.ascii-bg){z-index:1} 自动浮在上层. */
canvas.ascii-bg{
position:absolute;inset:0;width:100%;height:100%;
pointer-events:none;z-index:0;
mix-blend-mode:screen;opacity:.92;
}
.canvas-card > *:not(.ascii-bg){position:relative;z-index:1}
.slide.accent .canvas-card .chrome-min{color:rgba(255,255,255,.62)}
.slide.accent .canvas-card .t-meta{color:rgba(255,255,255,.7)}
.split-half > .half.b-accent .chrome-min{color:rgba(255,255,255,.62)}
/* 编号目录页 · 超大数字 + 国家名风格 — 越大越细 */
.num-mega{
font-family:var(--sans);font-weight:200;
font-size:9vw;line-height:1;letter-spacing:-.04em;
font-feature-settings:"tnum"
}
.num-mega.thin{font-weight:200}
.name-mega{
font-family:var(--sans);font-weight:200;
font-size:9vw;line-height:1;letter-spacing:-.035em;
}
.name-mega.muted{color:var(--grey-3)}
/* 细体超大 KPI — 字号越大权重越低 */
.kpi-thin{
font-family:var(--sans);font-weight:200;
font-size:14vw;line-height:.92;letter-spacing:-.045em;
font-feature-settings:"tnum"
}
.kpi-thin .unit{font-size:.3em;font-weight:300;opacity:.55;margin-left:.15em;vertical-align:.6em}
.kpi-thin.accent{color:var(--accent)}
.kpi-thin-sm{
font-family:var(--sans);font-weight:250;
font-size:5.6vw;line-height:1.04;letter-spacing:-.03em;
font-feature-settings:"tnum"
}
.kpi-thin-sm .unit{font-size:.3em;font-weight:300;opacity:.55;margin-left:.12em;vertical-align:.45em}
/* 4 列细线 KPI 行 — 顶部一根 hairline,内部不再加竖线 */
/* 4 列 KPI: 用纵向分割线建立网格感 (Carbon 2x grid 模数) */
.kpi-row-4{
display:grid;grid-template-columns:repeat(4,1fr);
gap:0;padding-top:2.4vh;
border-top:1px solid var(--grey-2)
}
.kpi-row-4 > .kpi-cell{
padding:1.6vh 1.6vw 0;
border-left:1px solid var(--grey-2)
}
.kpi-row-4 > .kpi-cell:first-child{padding-left:0;border-left:none}
.kpi-cell .lbl{font-family:var(--mono);font-size:max(14px,.78vw);letter-spacing:.18em;text-transform:uppercase;opacity:.55;margin-bottom:1.2vh}
.kpi-cell .nb{font-family:var(--sans);font-weight:250;font-size:3.2vw;line-height:1;letter-spacing:-.025em;font-feature-settings:"tnum"}
.kpi-cell .nb .unit{font-size:.32em;font-weight:300;opacity:.6;margin-left:.1em;vertical-align:.4em}
.kpi-cell .note{font-family:var(--sans),var(--sans-zh);font-size:max(16px,.92vw);line-height:1.5;opacity:.7;margin-top:1.2vh}
/* 时间线轴 — 通用 axis token, 横纵共享
axis 列 = 24px 固定宽,dot 直径 8px,绝对居中在 axis 列中线 (12px)
虚线绝对定位 left:12px,与 dot 中心严格对齐 */
.timeline-v{
--tl-axis-w:24px; /* axis 列固定宽度 */
--tl-dot:8px; /* 圆点直径 */
position:relative;margin-top:var(--sp-7)
}
.timeline-v::before{
content:"";position:absolute;
left:calc(var(--tl-axis-w) / 2);
transform:translateX(-50%);
top:var(--sp-5);bottom:var(--sp-5);
width:1px;
background:repeating-linear-gradient(to bottom,currentColor 0 4px,transparent 4px 8px);
opacity:.35;pointer-events:none;z-index:0
}
.tl-node{
position:relative;
display:grid;
grid-template-columns:var(--tl-axis-w) minmax(0,7em) minmax(0,7.6em) 1fr;
gap:0 var(--sp-5);
align-items:center;
padding:var(--sp-7) 0
}
/* 进度点: 纯色实心圆,无背景无描边,精确居中在 axis 列中线 */
.tl-node .dot{
width:var(--tl-dot);height:var(--tl-dot);
border-radius:50%;
background:currentColor;
justify-self:center;
z-index:1
}
.tl-node.accent .dot{background:var(--accent)}
.tl-node .yr{
font-family:var(--mono);font-weight:500;
font-size:max(14px,1vw);letter-spacing:.04em
}
.tl-node .multi{
font-family:var(--sans);font-weight:200;
font-size:clamp(28px,2.8vw,56px);
line-height:.95;letter-spacing:-.025em;
white-space:nowrap;
overflow:hidden;
min-width:0
}
.tl-node .multi .unit{
font-size:.36em;font-weight:300;opacity:.6;
margin-left:.2em;
vertical-align:.42em;
letter-spacing:0
}
.tl-node.accent .multi{color:var(--accent)}
.tl-node .desc{
font-family:var(--sans),var(--sans-zh);
font-size:max(16px,.94vw);line-height:1.55;opacity:.78;
min-width:0
}
/* 横向时间线 (.timeline-h) — 与 .timeline-v 共享 axis token, 视觉语言一致 */
.timeline-h{
--tl-axis-w:8px;
--tl-dot:8px;
position:relative;
flex:1;
display:flex;align-items:center
}
.timeline-h::before{
content:"";position:absolute;
top:50%;left:5%;right:5%;height:1px;
transform:translateY(-50%);
background:repeating-linear-gradient(to right,currentColor 0 4px,transparent 4px 8px);
opacity:.35;pointer-events:none;z-index:0
}
.timeline-h .tl-row{
position:relative;width:100%;
display:grid;grid-template-columns:repeat(5,1fr);
align-items:center
}
.timeline-h .th-node{
position:relative;display:flex;justify-content:center
}
/* 横向 dot: 8px 纯色实心,无描边无阴影 (与 P2 保持一致) */
.timeline-h .th-node .dot{
width:var(--tl-dot);height:var(--tl-dot);
border-radius:50%;
background:var(--ink);
z-index:1;position:relative
}
.timeline-h .th-node.accent .dot{background:var(--accent)}
.timeline-h .th-node .label{
position:absolute;left:50%;transform:translateX(-50%);
width:13vw;text-align:center;
display:flex;flex-direction:column;gap:.4vh
}
.timeline-h .th-node.up .label{bottom:calc(50% + 22px)}
.timeline-h .th-node.down .label{top:calc(50% + 22px)}
.timeline-h .th-node .yr{
font-family:var(--mono);font-size:max(14px,.78vw);letter-spacing:.05em;
color:var(--text-helper);font-weight:500
}
.timeline-h .th-node.accent .yr{color:var(--accent)}
.timeline-h .th-node .name{
font-family:var(--sans);font-size:max(16px,1.05vw);font-weight:400;
color:var(--text-primary);line-height:1.2;letter-spacing:-.005em
}
.timeline-h .th-node.accent .name{color:var(--accent)}
.timeline-h .th-node .desc{
font-family:var(--sans),var(--sans-zh);font-size:max(14px,.84vw);
color:var(--text-secondary);font-weight:400;line-height:1.4
}
/* 几何图示 (参考图 5) — SVG-friendly 容器 */
.geo-icon{width:5vw;height:5vw;display:block;margin-bottom:2.2vh;color:var(--ink);flex-shrink:0}
.slide.dark .geo-icon{color:var(--paper)}
.geo-icon svg{width:100%;height:100%;overflow:visible}
.geo-icon .stroke{fill:none;stroke:currentColor;stroke-width:1.4}
.geo-icon .stroke-accent{fill:none;stroke:var(--accent);stroke-width:1.4}
.geo-icon .fill-accent{fill:var(--accent)}
/* 卡片网格 (参考图 1 卡片漂浮) — 在 canvas-card 内部再分卡 */
.sub-grid-3-2{display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(2,1fr);gap:1.4vh 1.4vw;flex:1;align-content:stretch;margin-top:3vh}
.sub-card{
background:var(--grey-1);
padding:2.4vh 1.6vw 2vh;
display:flex;flex-direction:column;
position:relative;border-radius:3px;
min-height:0
}
.slide.dark .sub-card{background:rgba(255,255,255,.06)}
.sub-card.accent{background:var(--accent);color:var(--accent-on)}
.sub-card.ink{background:var(--ink);color:var(--paper)}
.sub-card .nb-corner{
position:absolute;top:1.6vh;right:1.4vw;
font-family:var(--mono);font-size:max(14px,.8vw);
letter-spacing:.16em;opacity:.55
}
.sub-card .ttl{font-family:var(--sans),var(--sans-zh);font-weight:500;font-size:max(17px,1.5vw);line-height:1.2;letter-spacing:-.015em;margin-bottom:1vh}
.sub-card .desc{font-family:var(--sans),var(--sans-zh);font-size:max(16px,.94vw);line-height:1.55;opacity:.78;margin-top:auto}
.sub-card .lucide{width:2.4vw;height:2.4vw;stroke-width:1.4;color:currentColor;margin-bottom:1.6vh;flex-shrink:0}
.sub-card.accent .lucide{color:var(--accent-on)}
/* 三层架构纯色块拼图 (参考图 4 + 6) — 横排等高色块 */
.stack-row{display:grid;grid-template-columns:repeat(3,1fr);gap:1.6vw;flex:1;margin-top:6vh;align-items:stretch}
.stack-block{
display:flex;flex-direction:column;
padding:3.2vh 1.8vw 2.4vh;
position:relative;
min-height:0
}
.stack-block.b-accent{background:var(--accent);color:var(--accent-on)}
.stack-block.b-grey{background:var(--grey-1);color:var(--ink)}
.stack-block.b-ink{background:var(--ink);color:var(--paper)}
.stack-block .layer-nb{font-family:var(--mono);font-size:max(14px,.84vw);letter-spacing:.18em;opacity:.65;margin-bottom:auto}
.stack-block .layer-icon{margin-bottom:1.6vh}
.stack-block .layer-icon svg{width:3vw;height:3vw}
.stack-block .layer-icon .stroke{fill:none;stroke:currentColor;stroke-width:1.6}
.stack-block .layer-ttl{font-family:var(--sans),var(--sans-zh);font-weight:400;font-size:max(17px,2vw);line-height:1.1;margin-top:1vh;letter-spacing:-.02em}
.stack-block .layer-desc{font-family:var(--sans),var(--sans-zh);font-weight:400;font-size:max(16px,.94vw);line-height:1.55;opacity:.88;margin-top:1.4vh}
.stack-block .lucide{width:2.6vw;height:2.6vw;stroke-width:1.4;margin-bottom:1.6vh;flex-shrink:0}
.stack-block .layer-tag{font-family:var(--mono);font-size:max(14px,.74vw);letter-spacing:.16em;text-transform:uppercase;opacity:.7;margin-top:1.6vh;border-top:1px solid currentColor;padding-top:1vh}
/* 不等高柱状 KPI 塔 (参考图 6) */
.bar-towers{
display:grid;grid-template-columns:repeat(4,1fr);
gap:1.2vw;flex:1;align-items:end;margin-top:auto
}
.bar-tower{
display:flex;flex-direction:column;justify-content:flex-end;
min-height:0;height:100%
}
.bar-tower .cap{
background:var(--grey-1);
height:5.6vh;
display:flex;align-items:center;justify-content:center;
margin-bottom:.4vh
}
.bar-tower .cap svg{width:1.6vw;height:1.6vw;stroke:currentColor;fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
.bar-tower .body-block{
flex:0 1 auto;
padding:2vh 1.2vw 2vh;
display:flex;flex-direction:column;justify-content:flex-end;
min-height:18vh
}
.bar-tower .body-block.h-1{min-height:22vh}
.bar-tower .body-block.h-2{min-height:30vh}
.bar-tower .body-block.h-3{min-height:38vh}
.bar-tower .body-block.h-4{min-height:46vh}
/* 默认所有 KPI 塔统一为浅描边卡 — 不抢戏;只有 .b-accent 突出为 IKB */
.bar-tower .body-block{background:var(--paper);color:var(--ink);border:1px solid var(--grey-2)}
.bar-tower .body-block.b-accent{background:var(--accent);color:var(--accent-on);border-color:var(--accent)}
.bar-tower .lbl{font-family:var(--mono);font-size:max(14px,.82vw);letter-spacing:.16em;text-transform:uppercase;opacity:.65;margin-bottom:1vh}
.bar-tower .nb{font-family:var(--sans);font-weight:250;font-size:max(20px,2.8vw);line-height:1;letter-spacing:-.03em;font-feature-settings:"tnum"}
.bar-tower .body-block.b-accent .nb{font-weight:300}
.bar-tower .nb .unit{font-size:.36em;font-weight:300;opacity:.7;margin-left:.08em;vertical-align:.4em}
.bar-tower .sub{font-family:var(--sans),var(--sans-zh);font-size:max(16px,.92vw);opacity:.75;margin-top:1.2vh;line-height:1.5}
.bar-tower .cap{background:var(--grey-1);color:var(--ink)}
.bar-tower .lucide{width:1.6vw;height:1.6vw;stroke-width:1.4}
/* ============ Carbon Productive Type Tokens ============
用于 PPT 中的"productive 时刻": 列表、表格行、说明文、章节标签
固定 px 字号,密集紧凑;与 vw-based 的 Expressive 巨字形成双极对比 */
/* category label / eyebrow / section tag — small, bold, uppercase */
.t-cat{
font-family:var(--mono);
font-size:14px;font-weight:600;
letter-spacing:.15em;text-transform:uppercase;
color:var(--text-helper);line-height:1.3
}
.t-cat.accent{color:var(--accent)}
.t-cat.on-dark{color:rgba(255,255,255,.78)}
/* page chrome / breadcrumb / running header — extra-small mono */
.t-meta{
font-family:var(--mono);
font-size:14px;font-weight:500;
letter-spacing:.14em;text-transform:uppercase;
color:var(--text-helper);line-height:1.45
}
/* helper / caption — secondary text */
.t-helper{
font-family:var(--sans),var(--sans-zh);
font-size:14px;font-weight:400;
color:var(--text-helper);line-height:1.5;
letter-spacing:.005em
}
/* body small — list items, table rows, captions */
.t-body-sm{
font-family:var(--sans),var(--sans-zh);
font-size:16px;font-weight:400;
color:var(--text-secondary);line-height:1.55;
letter-spacing:0
}
/* body — paragraphs, descriptions */
.t-body{
font-family:var(--sans),var(--sans-zh);
font-size:18px;font-weight:400;
color:var(--text-primary);line-height:1.5;
letter-spacing:-.005em
}
/* body emphasis — 强调正文 */
.t-body-emp{
font-family:var(--sans),var(--sans-zh);
font-size:18px;font-weight:600; /* SemiBold per Carbon */
color:var(--text-primary);line-height:1.5;
letter-spacing:-.005em
}
/* productive heading — section title within slide */
.t-h-prod{
font-family:var(--sans),var(--sans-zh);
font-size:22px;font-weight:600;
color:var(--text-primary);line-height:1.4;
letter-spacing:-.01em
}
/* dark background variants */
.slide.dark .t-cat,.slide.dark .t-meta,.slide.dark .t-helper{color:rgba(255,255,255,.62)}
.slide.dark .t-body-sm{color:rgba(255,255,255,.78)}
.slide.dark .t-body,.slide.dark .t-body-emp,.slide.dark .t-h-prod{color:var(--paper)}
.split-half .half.b-ink .t-cat,.split-half .half.b-ink .t-meta,.split-half .half.b-ink .t-helper{color:rgba(255,255,255,.62)}
.split-half .half.b-ink .t-body-sm{color:rgba(255,255,255,.78)}
.split-half .half.b-ink .t-body,.split-half .half.b-ink .t-body-emp,.split-half .half.b-ink .t-h-prod{color:var(--paper)}
/* 斜杠点阵装饰 (参考图 6 左下) */
.hatch{
background-image:repeating-linear-gradient(135deg,currentColor 0 1px,transparent 1px 8px);
opacity:.55
}
/* ========== V3: 高级点阵装饰 — 圆点 / × 号 / 圆圈 ========== */
/* 实心圆点矩阵 — 用于大面积装饰 */
.dot-mat{
--d:14px;
background-image:radial-gradient(currentColor 1.4px,transparent 1.6px);
background-size:var(--d) var(--d);
background-position:0 0;
opacity:.5
}
.dot-mat.lg{--d:22px}
.dot-mat.xl{--d:34px}
.dot-mat.dense{--d:9px;opacity:.62}
/* 描边圆圈矩阵 — 工业感 */
.ring-mat{
--d:18px;
background-image:
radial-gradient(circle at 50% 50%,transparent 2px,currentColor 2px,currentColor 2.6px,transparent 2.7px);
background-size:var(--d) var(--d);
opacity:.55
}
.ring-mat.lg{--d:28px}
/* × 号矩阵 — SVG mask 实现真正的 × 网格平铺 */
.cross-mat{
--d:22px;
background-color:currentColor;
-webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 22'><g stroke='black' stroke-width='1.4' stroke-linecap='round' fill='none'><line x1='8' y1='8' x2='14' y2='14'/><line x1='14' y1='8' x2='8' y2='14'/></g></svg>");
mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 22'><g stroke='black' stroke-width='1.4' stroke-linecap='round' fill='none'><line x1='8' y1='8' x2='14' y2='14'/><line x1='14' y1='8' x2='8' y2='14'/></g></svg>");
-webkit-mask-size:var(--d) var(--d);
mask-size:var(--d) var(--d);
-webkit-mask-repeat:repeat;mask-repeat:repeat;
opacity:.42
}
.cross-mat.lg{--d:32px}
/* ========== V3: 几何水平柱状图 ========== */
/* 横向柱图: 标签列 + 柱体列 + 数值列 — 严格瑞士网格 */
.h-bar-chart{
display:grid;
grid-template-columns:11em minmax(0,1fr) 8em;
gap:1.6vh 1.6vw;
align-items:center;
margin-top:2.4vh;
font-feature-settings:"tnum"
}
.h-bar-chart .row-lbl{
font-family:var(--sans),var(--sans-zh);
font-weight:500;
font-size:max(14px,1vw);
letter-spacing:-.005em;
text-align:left
}
.h-bar-chart .row-track{
height:3.2vh;
background:var(--grey-1);
position:relative;
overflow:hidden
}
.h-bar-chart .row-fill{
height:100%;
background:var(--ink);
transition:width 1s cubic-bezier(.5,0,.2,1)
}
.h-bar-chart .row-fill.accent{background:var(--accent)}
.h-bar-chart .row-fill.grey{background:var(--grey-3)}
.h-bar-chart .row-val{
font-family:var(--sans);font-weight:250;
font-size:max(16px,1.5vw);
letter-spacing:-.02em;
line-height:1
}
.h-bar-chart .row-val .unit{
font-size:.5em;opacity:.55;font-weight:300;
margin-left:.15em;letter-spacing:.04em
}
/* 垂直柱图: 用于 KPI 对比页(章节 P8) */
.v-bar-chart{
display:grid;
grid-template-columns:repeat(var(--cols,4),1fr);
align-items:end;
gap:1.4vw;
height:50vh;
margin-top:3vh
}
.v-bar-chart .col{display:flex;flex-direction:column;gap:1.4vh;align-items:stretch;height:100%}
.v-bar-chart .col-bar{
flex:1 1 auto;
background:var(--grey-1);
border-top:2px solid var(--ink);
position:relative;
display:flex;align-items:flex-start;justify-content:center;
padding-top:1vh
}
.v-bar-chart .col-bar.accent{background:var(--accent);border-top-color:var(--accent);color:var(--accent-on)}
.v-bar-chart .col-bar.ink{background:var(--ink);color:var(--paper);border-top-color:var(--ink)}
.v-bar-chart .col-bar .v{
font-family:var(--sans);font-weight:250;
font-size:max(18px,1.6vw);letter-spacing:-.02em
}
.v-bar-chart .col-lbl{
font-family:var(--mono);font-size:max(14px,.78vw);
letter-spacing:.14em;text-transform:uppercase;opacity:.6;
text-align:center;flex:0 0 auto
}
/* 对仗对比双栏 (章节 P9) */
.duo-compare{
display:grid;grid-template-columns:1fr 1px 1fr;
gap:0 3.4vw;
flex:1;align-items:stretch;margin-top:8vh
}
.duo-compare .vrule{background:var(--grey-2);width:1px;align-self:stretch}
.duo-compare .col{display:flex;flex-direction:column;gap:1.6vh;padding:0 .4vw}
.duo-compare .col-tag{
font-family:var(--mono);font-size:max(14px,.78vw);
letter-spacing:.16em;text-transform:uppercase;
color:var(--grey-3);
display:flex;align-items:center;gap:.6vw
}
.duo-compare .col-tag .num{
font-weight:600;color:var(--ink);
border:1px solid var(--ink);
padding:.2em .6em;font-size:.92em
}
.duo-compare .col.accent .col-tag .num{color:var(--accent);border-color:var(--accent)}
.duo-compare .col-ttl{
font-family:var(--sans),var(--sans-zh);font-weight:200;
font-size:3.6vw;line-height:1;letter-spacing:-.03em
}
.duo-compare .col.accent .col-ttl{color:var(--accent)}
.duo-compare .col-desc{
font-family:var(--sans),var(--sans-zh);
font-size:max(16px,1.04vw);line-height:1.55;opacity:.78;
max-width:30vw
}
.duo-compare .col-list{
list-style:none;display:flex;flex-direction:column;gap:1vh;
margin-top:auto;padding-top:2vh;border-top:1px solid var(--grey-2)
}
.duo-compare .col-list li{
font-family:var(--sans),var(--sans-zh);
font-size:max(16px,.94vw);line-height:1.5;
padding-left:1.4em;position:relative
}
.duo-compare .col-list li::before{
content:"";position:absolute;left:0;top:.6em;
width:.5em;height:1px;background:currentColor;opacity:.55
}
.duo-compare .col.accent .col-list li::before{background:var(--accent);opacity:1}
/* 半屏 statement (参考图 7) */
.split-half{display:grid;grid-template-columns:1fr 1fr;gap:0;flex:1;align-items:stretch}
.split-half > .half{padding:5vh 3.4vw;display:flex;flex-direction:column;min-width:0}
.split-half > .half.r-border{border-left:1px solid rgba(127,127,127,.22)}
.split-half > .half.b-grey{background:var(--grey-1)}
.split-half > .half.b-accent{background:var(--accent);color:var(--accent-on)}
.split-half > .half.b-ink{background:var(--ink);color:var(--paper)}
/* 极简页眉 — t-meta 风格 (Carbon productive label-01) */
.canvas-card .chrome-min{
display:flex;justify-content:space-between;align-items:flex-start;
font-family:var(--mono);font-size:14px;font-weight:500;
letter-spacing:.14em;text-transform:uppercase;
color:var(--text-helper);
flex:0 0 auto;
margin-bottom:var(--sp-9); /* 48px */
}
.canvas-card .chrome-min.tight{margin-bottom:var(--sp-7)} /* 32px */
.canvas-card .chrome-min .l, .canvas-card .chrome-min .r{
max-width:48vw;line-height:1.5
}
.slide.dark .canvas-card .chrome-min,.split-half .half.b-ink .canvas-card .chrome-min{color:rgba(255,255,255,.62)}
/* 响应式降级 */
@media (max-width:900px){
.h-hero{font-size:16vw}
.h-hero-zh{font-size:13vw}
.h-xl{font-size:9vw}
.h-xl-zh{font-size:8vw}
.kpi-hero{font-size:32vw}
.kpi-big{font-size:16vw}
.pipeline{grid-template-columns:repeat(2,1fr)}
.grid-2-7-5,.grid-2-6-6,.grid-2-8-4,.grid-2-4-8{grid-template-columns:1fr}
.grid-12{grid-template-columns:repeat(6,1fr)}
}
</style>
</head>
<body class="canvas-mode">
<script>
// Windows 平台标记 — 雅黑没有 ExtraLight,需要字重补偿
if(/Win/i.test(navigator.platform || navigator.userAgentData?.platform || '')){
document.body.classList.add('is-win');
}
(function(){
const KEY = 'guizang-ppt-low-power';
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
const stored = localStorage.getItem(KEY);
window.__lowPowerMode = stored === '1' || (stored === null && reduced);
function updateHint(){
const hint = document.getElementById('hint');
if(hint) hint.textContent = `← → 翻页 · B ${window.__lowPowerMode ? '动态' : '静态'} · ESC 索引`;
}
window.__setLowPowerMode = function(on, opts={}){
window.__lowPowerMode = !!on;
document.body.classList.toggle('low-power', window.__lowPowerMode);
if(opts.persist !== false) localStorage.setItem(KEY, window.__lowPowerMode ? '1' : '0');
if(window.__lowPowerMode && document.getAnimations){
document.getAnimations().forEach(a=>a.cancel());
}
updateHint();
dispatchEvent(new CustomEvent('swiss-low-power-change', {detail:{on:window.__lowPowerMode}}));
if(window.__playSlide) window.__playSlide(window.__currentSlideIndex || 0);
};
document.body.classList.toggle('low-power', window.__lowPowerMode);
addEventListener('DOMContentLoaded', updateHint, {once:true});
})();
</script>
<canvas id="bg-grid" class="bg"></canvas>
<div id="hint">← → 翻页 · B 静态 · ESC 索引</div>
<div id="deck">
<!-- ============================================================
SLIDES 插入区 · 在此处填充所有 <section class="slide ..."> 页面
页面骨架参考 references/layouts-swiss.md
主题色配置参考 references/themes-swiss.md
============================================================ -->
<!-- SLIDES_HERE · 在此处粘贴 <section class="slide ..."> 页面块
- 页面骨架直接从 references/layouts-swiss.md 拷贝
- data-animate="..." 必须命中下方 RECIPES 字典里的已有 recipe 名之一(P23/P24 可复用 grid-reveal)
- 主题色配置参考 references/themes-swiss.md (默认 IKB 克莱因蓝) -->
<!-- ============ 示例:第 1 页 · Hero Cover · IKB 满屏 + ASCII 呼吸场(默认推荐) ============
⚠️ P0 对齐法则(每页都要过):
1. .canvas-card 已自带 padding:5.6vh 5vw 4.4vh,所有页面内容直接放在 canvas-card 子元素里,
子元素**不要再加水平 padding**,否则会比 chrome-min 内缩一圈、左右不对齐.
2. .slide.split .canvas-card{padding:0} 已被 CSS 覆盖,
split 模式下两个 .half 自己控制 padding(常用 5.6vh 3.6vw 4.4vh),与本规则不冲突.
3. 大字号一律用双约束 font-size:min(Xvw, Yvh),Y ≥ X * 1.6 才不会在 16:9 屏被高度截断.
4. kicker 必须在大标题"上方",不是左侧——禁止 grid-template-columns:auto 1fr 把它们压成左右.
封面/封底设计语言(默认 IKB 满屏 + ASCII 呼吸场):
- section 用 .slide.accent (满屏 IKB,不是 light 白底)
- canvas-card 内首位插入 <canvas class="ascii-bg" aria-hidden="true">,本文件底部 IIFE 自动启动
- 主标题反白 weight 200,强调字用斜体而非 var(--accent)(底已是蓝,蓝压蓝看不见)
- 不要再放编号大字"01"——chrome-min 已经标 01/NN -->
<section class="slide accent" data-animate="hero">
<div class="canvas-card">
<canvas class="ascii-bg" aria-hidden="true"></canvas>
<div class="chrome-min">
<div class="l">[必填] Deck 标题 · Issue/Field Note 编号</div>
<div class="r">SS · 25.05.10 · 01 / NN</div>
</div>
<!-- 主体:padding 必须为 0,不要再叠 5vw,否则左右对不齐 chrome-min -->
<div style="flex:1;padding:0;display:grid;grid-template-rows:auto 1fr auto;gap:2.6vh">
<div data-anim="kicker" class="t-meta" style="color:rgba(255,255,255,.78);letter-spacing:.22em">[必填] 章节英文 / Section En</div>
<h1 data-anim="title" style="align-self:center;font-family:var(--sans),var(--sans-zh);font-weight:200;font-size:min(11.6vw,19vh);line-height:.94;letter-spacing:-.025em;color:#fff">[必填] 中文主标题<br/>(≤ 12 字,可在某字加 <span style="font-style:italic;font-weight:300">italic</span> 微强调)</h1>
<div data-anim="bottom" style="display:grid;grid-template-rows:auto auto;gap:1.6vh;border-top:1px solid rgba(255,255,255,.22);padding-top:2vh">
<div data-anim="lead" class="lead" style="max-width:52ch;color:rgba(255,255,255,.86)">[必填] 一段 1-2 行的副标 / 引子,定调全场.</div>
<div style="display:flex;justify-content:space-between;align-items:end">
<div class="t-meta" style="color:rgba(255,255,255,.6)">[选填] 作者 · 日期 · 出处</div>
<div class="t-meta" style="color:rgba(255,255,255,.6)">→ swipe / arrow keys</div>
</div>
</div>
</div>
</div>
</section>
<!-- ============ 示例:最后一页 · Closing Manifesto · 左 IKB+ASCII / 右白底 takeaway ============
与封面 IKB 首尾呼应,但收束更克制:左半保留 ASCII 呼吸场承载宣言,右半白底列 3 条 takeaway.
第 03 条用 var(--accent) IKB 蓝强调("单一锚点"原则),首尾形成"色彩闭环". -->
<section class="slide split" data-animate="split-statement">
<div class="canvas-card">
<div class="split-half">
<!-- 左半 · IKB 宣言 + ASCII 呼吸场 -->
<div class="half b-accent" style="padding:5.6vh 3.6vw 4.4vh;justify-content:space-between;position:relative;overflow:hidden">
<canvas class="ascii-bg" aria-hidden="true"></canvas>
<div class="chrome-min" style="margin-bottom:0;position:relative;z-index:1">
<div class="l">NN / NN</div>
<div class="r">CLOSING</div>
</div>
<div data-anim="manifesto" style="display:flex;flex-direction:column;gap:2vh;position:relative;z-index:1">
<div class="t-meta" style="color:rgba(255,255,255,.78);letter-spacing:.22em;margin-bottom:1.6vh">MANIFESTO</div>
<h2 style="font-family:var(--sans),var(--sans-zh);font-size:min(8vw,14vh);line-height:.94;letter-spacing:-.025em;font-weight:200;color:#fff">[必填] Build a model.<br/>Run <span style="font-style:italic;font-weight:300">forever</span>.</h2>
<div style="font-family:var(--sans),var(--sans-zh);font-size:max(14px,1vw);line-height:1.6;color:rgba(255,255,255,.82);font-weight:400;max-width:36ch;margin-top:1.4vh">[必填] 一句 1-2 行的中文/英文注脚,把宣言落地.</div>
</div>
<div data-anim="signature" style="display:flex;justify-content:space-between;align-items:end;border-top:1px solid rgba(255,255,255,.22);padding-top:2vh;position:relative;z-index:1">
<div class="t-meta" style="color:rgba(255,255,255,.62)">[选填] 作者 · 头衔</div>
<div class="t-meta" style="color:rgba(255,255,255,.62)">YY.MM.DD</div>
</div>
</div>
<!-- 右半 · 三条 takeaway · 白底承载理性收束 -->
<div class="half" style="padding:5.6vh 3.6vw 4.4vh;justify-content:space-between">
<div class="chrome-min">
<div class="l">TAKEAWAYS</div>
<div class="r">03 RULES</div>
</div>
<div data-anim="rules" style="display:flex;flex-direction:column;gap:0">
<div style="display:grid;grid-template-columns:auto 1fr;gap:2vw;align-items:start;padding:2.6vh 0;border-top:1px solid var(--border-subtle)">
<div style="font-family:var(--sans);font-weight:200;font-size:min(4.4vw,7.8vh);line-height:.9;color:var(--text-primary)">01</div>
<div>
<h3 style="font-family:var(--sans),var(--sans-zh);font-weight:400;font-size:max(18px,1.8vw);line-height:1.2;letter-spacing:-.015em;color:var(--text-primary);margin-bottom:1vh">[必填] takeaway 标题 01</h3>
<p style="font-family:var(--sans),var(--sans-zh);font-size:max(16px,.94vw);line-height:1.6;color:var(--text-secondary);font-weight:400">[必填] 1-2 行展开说明.</p>
</div>
</div>
<div style="display:grid;grid-template-columns:auto 1fr;gap:2vw;align-items:start;padding:2.6vh 0;border-top:1px solid var(--border-subtle)">
<div style="font-family:var(--sans);font-weight:200;font-size:min(4.4vw,7.8vh);line-height:.9;color:var(--text-primary)">02</div>
<div>
<h3 style="font-family:var(--sans),var(--sans-zh);font-weight:400;font-size:max(18px,1.8vw);line-height:1.2;letter-spacing:-.015em;color:var(--text-primary);margin-bottom:1vh">[必填] takeaway 标题 02</h3>
<p style="font-family:var(--sans),var(--sans-zh);font-size:max(16px,.94vw);line-height:1.6;color:var(--text-secondary);font-weight:400">[必填] 1-2 行展开说明.</p>
</div>
</div>
<div style="display:grid;grid-template-columns:auto 1fr;gap:2vw;align-items:start;padding:2.6vh 0;border-top:1px solid var(--border-subtle);border-bottom:2px solid var(--accent)">
<div style="font-family:var(--sans);font-weight:200;font-size:min(4.4vw,7.8vh);line-height:.9;color:var(--accent)">03</div>
<div>
<h3 style="font-family:var(--sans),var(--sans-zh);font-weight:400;font-size:max(18px,1.8vw);line-height:1.2;letter-spacing:-.015em;color:var(--accent);margin-bottom:1vh">[必填] takeaway 标题 03 · accent 强调</h3>
<p style="font-family:var(--sans),var(--sans-zh);font-size:max(16px,.94vw);line-height:1.6;color:var(--text-secondary);font-weight:400">[必填] 最后一条用 IKB 强调,与封面色彩首尾闭环.</p>
</div>
</div>
</div>
<div data-anim="foot" class="t-meta" style="color:var(--text-helper);text-align:right">→ 完 · END OF FIELD NOTE</div>
</div>
</div>
</div>
</section>
</div>
<div id="nav"></div>
<script>
/* =============== WebGL 网格背景 (瑞士风专用) ===============
极简移动网格 + 微弱点阵叠加,营造"工业感、精准感"
- 主网格: 缓慢漂移的细线网格
- 次级: 鼠标附近的极细点阵微扰
- 颜色: 跟随主题(浅底深线 / 深底亮线),配合 mix-blend-mode
*/
const VS = `attribute vec2 position;void main(){gl_Position=vec4(position,0.0,1.0);}`;
const FS = `precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_mouse;
uniform float u_dark; // 0 = light, 1 = dark
uniform vec3 u_accent;
float gridLine(vec2 uv, float spacing, float thickness){
vec2 g = abs(fract(uv / spacing) - 0.5);
float d = min(g.x, g.y);
return 1.0 - smoothstep(thickness - 0.005, thickness + 0.005, d);
}
float dot2(vec2 p){ return dot(p,p); }
void main(){
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
float aspect = u_resolution.x / u_resolution.y;
vec2 p = uv;
p.x *= aspect;
// 缓慢平移
vec2 drift = vec2(u_time * 0.008, u_time * 0.005);
vec2 gp = p + drift;
// 主细网格 (大间距)
float mainGrid = gridLine(gp, 0.12, 0.012);
// 次级网格 (更细更密)
float subGrid = gridLine(gp, 0.024, 0.04) * 0.4;
// 鼠标附近的强化
vec2 m = u_mouse;
m.x *= aspect;
float md = length(p - m);
float mInfluence = exp(-md * 4.0) * 0.5;
float gridStrength = (mainGrid + subGrid * 0.5) * (0.45 + mInfluence);
// 点阵 (作为基底)
vec2 dotGrid = fract(gp * 50.0) - 0.5;
float dotMask = 1.0 - smoothstep(0.05, 0.14, length(dotGrid));
// 用低频噪声调制点阵密度
float wave = sin(gp.x * 1.4 + u_time * 0.15) * cos(gp.y * 1.6 - u_time * 0.12);
dotMask *= smoothstep(-0.3, 0.6, wave) * 0.6;
// 颜色: 浅底用深线条,深底用浅线条;高亮处带 accent 痕迹
vec3 lineColor = mix(vec3(0.08), vec3(0.92), u_dark);
vec3 bgColor = mix(vec3(0.97, 0.97, 0.96), vec3(0.06, 0.06, 0.07), u_dark);
// accent 暗示 (鼠标附近偷渡一点 accent 色)
vec3 col = bgColor;
col = mix(col, lineColor, gridStrength * 0.55);
col = mix(col, lineColor, dotMask * 0.35);
col = mix(col, u_accent, mInfluence * 0.18);
gl_FragColor = vec4(col, 1.0);
}`;
const mouse={x:0.5,y:0.5};
addEventListener('mousemove',e=>{mouse.x=e.clientX/innerWidth;mouse.y=1-e.clientY/innerHeight});
function bootGL(canvasId, fsSrc){
const canvas=document.getElementById(canvasId);
const gl=canvas.getContext('webgl',{alpha:true,antialias:true,premultipliedAlpha:false});
if(!gl) return ()=>false;
const mk=(t,s)=>{const sh=gl.createShader(t);gl.shaderSource(sh,s);gl.compileShader(sh);return sh};
const prog=gl.createProgram();
gl.attachShader(prog,mk(gl.VERTEX_SHADER,VS));
gl.attachShader(prog,mk(gl.FRAGMENT_SHADER,fsSrc));
gl.linkProgram(prog);gl.useProgram(prog);
const buf=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,buf);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]),gl.STATIC_DRAW);
const pos=gl.getAttribLocation(prog,'position');
gl.enableVertexAttribArray(pos);gl.vertexAttribPointer(pos,2,gl.FLOAT,false,0,0);
const lRes=gl.getUniformLocation(prog,'u_resolution');
const lT=gl.getUniformLocation(prog,'u_time');
const lM=gl.getUniformLocation(prog,'u_mouse');
const lD=gl.getUniformLocation(prog,'u_dark');
const lA=gl.getUniformLocation(prog,'u_accent');
const resize=()=>{
const d=Math.min(window.devicePixelRatio||1,2);
canvas.width=innerWidth*d;canvas.height=innerHeight*d;
gl.viewport(0,0,canvas.width,canvas.height);
};
addEventListener('resize',resize);resize();
// 读取 CSS 变量,把 accent 颜色塞进 shader
function readAccent(){
const cs = getComputedStyle(document.documentElement);
const hex = cs.getPropertyValue('--accent').trim() || '#002FA7';
const m = hex.match(/^#([0-9a-f]{6})$/i);
if(!m) return [0, 0.18, 0.65];
const n = parseInt(m[1], 16);
return [((n>>16)&255)/255, ((n>>8)&255)/255, (n&255)/255];
}
let accent = readAccent();
let dark = 0;
return (tSec, isDark)=>{
if(isDark !== undefined) dark = isDark ? 1 : 0;
accent = readAccent();
gl.uniform2f(lRes,canvas.width,canvas.height);
gl.uniform1f(lT,tSec);
gl.uniform2f(lM,mouse.x,mouse.y);
gl.uniform1f(lD,dark);
gl.uniform3f(lA,accent[0],accent[1],accent[2]);
gl.drawArrays(gl.TRIANGLES,0,6);
return true;
};
}
// canvas-mode / low-power: skip WebGL draw loop (no active RAF loop)
let darkMode=false;
let gridCtrl=null, gridRAF=0, gridT0=Date.now();
function startGrid(){
if(document.body.classList.contains('canvas-mode') || window.__lowPowerMode || gridRAF) return;
if(!gridCtrl) gridCtrl = bootGL('bg-grid',FS);
if(!gridCtrl) return;
gridT0=Date.now();
function loop(){
if(window.__lowPowerMode){gridRAF=0;return;}
const t=(Date.now()-gridT0)/1000;
gridCtrl(t, darkMode);
gridRAF=requestAnimationFrame(loop);
}
gridRAF=requestAnimationFrame(loop);
}
function stopGrid(){
if(gridRAF) cancelAnimationFrame(gridRAF);
gridRAF=0;
}
if(document.body.classList.contains('canvas-mode')){
const c=document.getElementById('bg-grid');
if(c) c.remove();
}else{
startGrid();
}
addEventListener('swiss-low-power-change', e=>{e.detail.on ? stopGrid() : startGrid();});
// =============== 导航 ===============
const deck=document.getElementById('deck');
const slides=deck.querySelectorAll('.slide');
const nav=document.getElementById('nav');
let idx=0,total=slides.length,lock=false;
deck.style.width=(total*100)+'vw';
slides.forEach((s,i)=>{
const b=document.createElement('button');
b.className='dot';b.dataset.i=i;b.setAttribute('aria-label','Page '+(i+1));
b.onclick=()=>go(i);
nav.appendChild(b);
});
function go(n){
if(lock)return;
idx=Math.max(0,Math.min(total-1,n));
window.__currentSlideIndex = idx;
deck.style.transform=`translateX(${-idx*100}vw)`;
nav.querySelectorAll('.dot').forEach((d,i)=>d.classList.toggle('active',i===idx));
const el=slides[idx];
const isDark = el.classList.contains('dark') || el.classList.contains('accent');
document.body.classList.toggle('dark-bg', isDark);
darkMode = isDark;
if(window.__playSlide) setTimeout(()=>window.__playSlide(idx), 450);
lock=true;setTimeout(()=>lock=false,700);
}
/* =============== ESC 索引视图 =============== */
let overviewOn=false;
const ov=document.createElement('div');
ov.id='overview';
ov.style.cssText='position:fixed;inset:0;z-index:100;background:rgba(250,250,248,.96);backdrop-filter:blur(12px);display:none;overflow-y:auto;padding:4vh 4vw';
document.body.appendChild(ov);
function buildOverview(){
ov.innerHTML='';
const grid=document.createElement('div');
grid.style.cssText='display:grid;grid-template-columns:repeat(4,1fr);gap:2vh 1.6vw;max-width:90vw;margin:0 auto';
slides.forEach((s,i)=>{
const card=document.createElement('div');
card.style.cssText='cursor:pointer;overflow:hidden;border:2px solid '+(i===idx?'var(--accent)':'rgba(0,0,0,.12)')+';transition:border-color .2s';
card.onmouseenter=()=>card.style.borderColor='rgba(0,0,0,.4)';
card.onmouseleave=()=>card.style.borderColor=i===idx?'var(--accent)':'rgba(0,0,0,.12)';
const wrap=document.createElement('div');
const isDark = s.classList.contains('dark') || s.classList.contains('accent');
wrap.style.cssText='width:100%;aspect-ratio:16/9;overflow:hidden;position:relative;pointer-events:none;background:'+(isDark?'var(--ink)':'var(--paper)');
const clone=s.cloneNode(true);
clone.style.cssText='width:100vw;height:100vh;transform:scale('+(1/4.5)+');transform-origin:top left;position:absolute;top:0;left:0;pointer-events:none';
wrap.appendChild(clone);
const label=document.createElement('div');
/* ESC 索引卡 label */
label.style.cssText='padding:6px 10px;font-family:var(--mono);font-size:14px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink);opacity:.7';
label.textContent=(i+1)+' / '+total;
card.appendChild(wrap);
card.appendChild(label);
card.onclick=()=>{toggleOverview();go(i)};
grid.appendChild(card);
});
ov.appendChild(grid);
}
function toggleOverview(){
overviewOn=!overviewOn;
if(overviewOn){buildOverview();ov.style.display='block';}
else{ov.style.display='none';}
}
addEventListener('keydown',e=>{
if(e.key==='Escape'){e.preventDefault();toggleOverview();return;}
if(e.key && e.key.toLowerCase()==='b' && !e.metaKey && !e.ctrlKey && !e.altKey){
e.preventDefault();
window.__setLowPowerMode(!window.__lowPowerMode);
return;
}
if(overviewOn)return;
if(e.key==='ArrowRight'||e.key==='PageDown'||e.key===' '||e.key==='ArrowDown'){
if(window.__pipeAdvance && window.__pipeAdvance()) return;
go(idx+1);
return;
}
if(e.key==='ArrowLeft'||e.key==='PageUp'||e.key==='ArrowUp')go(idx-1);
if(e.key==='Home')go(0);
if(e.key==='End')go(total-1);
});
let wheelTO=null,wheelAcc=0;
addEventListener('wheel',e=>{
wheelAcc+=e.deltaY+e.deltaX;
if(Math.abs(wheelAcc)>50){
if(wheelAcc>0 && window.__pipeAdvance && window.__pipeAdvance()){
wheelAcc=0;
}else{
go(idx+(wheelAcc>0?1:-1));wheelAcc=0;
}
}
clearTimeout(wheelTO);wheelTO=setTimeout(()=>wheelAcc=0,150);
},{passive:true});
let tx=0,ty=0;
addEventListener('touchstart',e=>{tx=e.touches[0].clientX;ty=e.touches[0].clientY},{passive:true});
addEventListener('touchend',e=>{
const dx=(e.changedTouches[0].clientX-tx);
const dy=(e.changedTouches[0].clientY-ty);
if(Math.abs(dx)>50&&Math.abs(dx)>Math.abs(dy)){
if(dx<0 && window.__pipeAdvance && window.__pipeAdvance()) return;
go(idx+(dx<0?1:-1));
}
},{passive:true});
const initialSlideParam = new URLSearchParams(location.search).get('slide');
const initialSlide = initialSlideParam ? Number(initialSlideParam) - 1 : 0;
go(Number.isFinite(initialSlide) ? initialSlide : 0);
</script>
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<script>lucide.createIcons();</script>
<!-- Motion One 动效引擎 (与原模板一致) -->
<script type="module">
let motion;
try {
motion = await import('./assets/motion.min.js');
} catch(e1) {
try {
motion = await import('https://cdn.jsdelivr.net/npm/motion@11.11.17/+esm');
} catch(e2) {
console.warn('[motion] local + CDN both failed, disabling animations', e1, e2);
document.querySelectorAll('[data-anim]').forEach(el=>{el.style.opacity='1';el.style.transform='none'});
document.querySelectorAll('[data-animate="pipeline"] [data-anim]').forEach(el=>el.style.opacity='1');
}
}
if(motion){
const { animate } = motion;
document.body.classList.add('motion-ready');
/* ============================================================
IBM Carbon Motion · 每个 recipe 服务一种表达
不是一刀切的 stagger,而是把动效绑在内容语义上
============================================================ */
const EASE_PROD = [.2, 0, .38, .9];
const EASE_ENTRY_EXP = [0, 0, .3, 1];
const slides = [...document.querySelectorAll('.slide')];
let lastIdx = -1;
function resetAnims(slide){
slide.querySelectorAll('[data-anim]').forEach(el=>{
el.style.opacity='';
el.style.transform='';
});
/* 同时复位需要被 recipe 接管的元素 */
slide.querySelectorAll('.row-fill,.tl-node,.stack-block,.bar-tower,.sub-card,.col,.vrule,.kpi-cell')
.forEach(el=>{el.style.cssText = el.dataset._origCss || el.style.cssText;});
}
/* ---------- 通用工具 ---------- */
const fade = (el, opts={})=>animate(el,
{opacity:[0,1], y:[opts.y ?? 12, 0]},
{duration:opts.duration ?? .6, delay:opts.delay ?? 0,
easing:opts.easing ?? EASE_ENTRY_EXP});
/* ---------- recipe: hero · 封面索引 ----------
大编号一个个亮起 → 索引行最后落定 */
function rHero(slide, all){
const numRows = [...slide.querySelectorAll('.cover-row')];
const rest = all.filter(el=>!numRows.length || el !== numRows[0]);
/* 先入: chrome 240ms */
const chrome = slide.querySelector('.chrome-min');
if(chrome) animate(chrome, {opacity:[0,1]}, {duration:.24, easing:EASE_PROD});
/* 大编号 01/02/03 像点名一样依次亮 */
numRows.forEach((row, i)=>{
animate(row, {opacity:[0,1], x:[-12,0]},
{duration:.5, delay:.15 + i*.18, easing:EASE_ENTRY_EXP});
});
/* 索引底栏最后慢慢落定 */
const idx = slide.querySelector('[data-anim="line"]');
if(idx) fade(idx, {delay:.15 + numRows.length*.18 + .1, duration:.5, y:6});
}
/* ---------- recipe: progression · 1× → 10× → 1000× ----------
节点依次入场,每个节点的数字单独"递进生长"营造跃迁 */
function rProgression(slide, all){
const head = slide.querySelector('[data-anim="line"]');
if(head) fade(head, {duration:.6, y:10});
const nodes = [...slide.querySelectorAll('.tl-node')];
nodes.forEach((node, i)=>{
const base = .35 + i*.32; /* 节点之间间隔大,营造时间感 */
/* 整个节点先轻微浮入 */
animate(node, {opacity:[0,1], y:[14, 0]},
{duration:.55, delay:base, easing:EASE_ENTRY_EXP});
/* 再让 multi(数字)从 .85 scale 弹到 1,延迟 100ms */
const multi = node.querySelector('.multi');
if(multi) animate(multi, {scale:[.92, 1], opacity:[0,1]},
{duration:.5, delay:base + .12, easing:EASE_ENTRY_EXP});
});
/* 底部 KPI 4 列最后落定,内部 60ms stagger */
const kpis = [...slide.querySelectorAll('.kpi-cell')];
kpis.forEach((cell, i)=>{
animate(cell, {opacity:[0,1], y:[8, 0]},
{duration:.4, delay:1.4 + i*.07, easing:EASE_PROD});
});
}
/* ---------- recipe: statement · 大宣言 ----------
左半屏标题逐行落下,右半屏 leaked 信息晚 600ms 进 */
function rStatement(slide, all){
const halves = [...slide.querySelectorAll('.half')];
if(halves.length === 2){
animate(halves[0], {opacity:[0,1], y:[18,0]},
{duration:.7, delay:0, easing:EASE_ENTRY_EXP});
animate(halves[1], {opacity:[0,1], y:[18,0]},
{duration:.7, delay:.6, easing:EASE_ENTRY_EXP});
} else {
/* P9 Index Card — 三行像盖章一样依次落 */
const head = slide.querySelector('[data-anim="line"]');
if(head) fade(head, {duration:.5, y:6});
const blocks = all.filter(el=>el !== head);
blocks.forEach((el, i)=>{
animate(el, {opacity:[0,1], y:[20,0]},
{duration:.55, delay:.25 + i*.18, easing:EASE_ENTRY_EXP});
});
}
}
/* ---------- recipe: grid-reveal · 五个定义 ----------
卡片按 nb-corner 序号 01→02→03→04→05→Σ 依次揭示 */
function rGridReveal(slide, all){
const head = slide.querySelector('[data-anim="line"]');
if(head) fade(head, {duration:.6, y:10});
const cards = [...slide.querySelectorAll('.sub-card')];
cards.forEach((card, i)=>{
animate(card, {opacity:[0,1], y:[20,0], scale:[.96, 1]},
{duration:.5, delay:.3 + i*.09, easing:EASE_ENTRY_EXP});
});
}
/* ---------- recipe: stack-build · 三层架构 ----------
中间 thin 先入 → 上层 fat skills 从顶推下 → 下层 application 从底推上 */
function rStackBuild(slide, all){
const head = slide.querySelector('[data-anim="line"]');
if(head) fade(head, {duration:.6, y:10});
const blocks = [...slide.querySelectorAll('.stack-block')];
/* 先入: 中间薄层(LAYER 02) */
if(blocks[1]) animate(blocks[1], {opacity:[0,1], scaleY:[.85, 1]},
{duration:.55, delay:.3, easing:EASE_ENTRY_EXP});
/* 上推下: LAYER 01 fat skills 从顶部 push down */
if(blocks[0]) animate(blocks[0], {opacity:[0,1], y:[-22, 0]},
{duration:.6, delay:.6, easing:EASE_ENTRY_EXP});
/* 下推上: LAYER 03 application 从底部 push up */
if(blocks[2]) animate(blocks[2], {opacity:[0,1], y:[22, 0]},
{duration:.6, delay:.6, easing:EASE_ENTRY_EXP});
const foot = slide.querySelector('.t-meta');
if(foot) animate(foot, {opacity:[0,1]}, {duration:.3, delay:1.3, easing:EASE_PROD});
}
/* ---------- recipe: measure-up · YC KPI 塔 ----------
塔从底部 scaleY 0→1 生长 + 数字最后弹入 */
function rMeasureUp(slide, all){
const head = slide.querySelector('[data-anim="line"]');
if(head) fade(head, {duration:.6, y:10});
const towers = [...slide.querySelectorAll('.bar-tower')];
towers.forEach((tower, i)=>{
const block = tower.querySelector('.body-block');
if(block){
block.style.transformOrigin = 'bottom center';
animate(block, {opacity:[0,1], scaleY:[.05, 1]},
{duration:.7, delay:.35 + i*.12, easing:EASE_ENTRY_EXP});
}
/* cap (顶部图标) 等柱体长好后弹入 */
const cap = tower.querySelector('.cap');
if(cap) animate(cap, {opacity:[0,1], y:[-8, 0]},
{duration:.4, delay:.85 + i*.12, easing:EASE_PROD});
});
}
/* ---------- recipe: bar-grow · 90% 价值分布 ----------
标题先入 → hairline 从中点向两侧 stroke draw → bar 依次 width 0→target → 数值 fade in */
function rBarGrow(slide, all){
const head = slide.querySelector('[data-anim="line"]');
if(head) fade(head, {duration:.6, y:10});
/* 中部 hairline:从 100% width 0 拉到 100% (transformOrigin: center) */
const midRow = slide.querySelector('[data-anim="up"]');
if(midRow){
const midLabel = midRow.querySelector('.t-cat');
const midLine = midRow.querySelector('div[style*="height:1px"]');
if(midLabel) animate(midLabel, {opacity:[0,1], x:[-8,0]},
{duration:.4, delay:.4, easing:EASE_PROD});
if(midLine){
midLine.style.transformOrigin = 'center';
animate(midLine, {opacity:[0,1], scaleX:[0, 1]},
{duration:.55, delay:.5, easing:EASE_ENTRY_EXP});
}
}
/* bar 行依次 width 增长 */
const fills = [...slide.querySelectorAll('.row-fill')];
const labels = [...slide.querySelectorAll('.row-lbl')];
const values = [...slide.querySelectorAll('.row-val')];
fills.forEach((fill, i)=>{
const target = fill.style.width;
fill.style.width = '0%';
if(labels[i]) animate(labels[i], {opacity:[0,1], x:[-12,0]},
{duration:.4, delay:.85 + i*.14, easing:EASE_PROD});
animate(fill, {width:['0%', target]},
{duration:.65, delay:.95 + i*.14, easing:EASE_ENTRY_EXP});
if(values[i]) animate(values[i], {opacity:[0,1]},
{duration:.3, delay:1.5 + i*.14, easing:EASE_PROD});
});
}
/* ---------- recipe: duo-mirror · Latent vs Deterministic ----------
左 80ms 入,vrule 从中心 scaleY 0→1,右 240ms 入 */
function rDuoMirror(slide, all){
const head = slide.querySelector('[data-anim="line"]');
if(head) fade(head, {duration:.6, y:10});
const cols = [...slide.querySelectorAll('.duo-compare .col')];
const vrule = slide.querySelector('.duo-compare .vrule');
if(cols[0]) animate(cols[0], {opacity:[0,1], x:[-24, 0]},
{duration:.65, delay:.4, easing:EASE_ENTRY_EXP});
if(vrule){
vrule.style.transformOrigin = 'center';
animate(vrule, {opacity:[0,1], scaleY:[0, 1]},
{duration:.55, delay:.55, easing:EASE_ENTRY_EXP});
}
if(cols[1]) animate(cols[1], {opacity:[0,1], x:[24, 0]},
{duration:.65, delay:.7, easing:EASE_ENTRY_EXP});
const foot = slide.querySelector('.t-meta');
if(foot) animate(foot, {opacity:[0,1]}, {duration:.3, delay:1.3, easing:EASE_PROD});
}
/* ---------- recipe: split-statement · 收尾 ----------
左黑半屏的 once / forever 错位入场;右白半屏 takeaway list 后跟 */
function rSplitStatement(slide, all){
const halves = [...slide.querySelectorAll('.half')];
/* 左黑半屏 — once 先入,forever 间隔 600ms */
if(halves[0]){
animate(halves[0], {opacity:[0,1]}, {duration:.4, easing:EASE_PROD});
const kpis = halves[0].querySelectorAll('.kpi-thin');
kpis.forEach((k, i)=>{
animate(k, {opacity:[0,1], y:[24,0]},
{duration:.7, delay:.25 + i*.55, easing:EASE_ENTRY_EXP});
});
}
/* 右白半屏 — list 三条依次入,在左侧 once 出现后开始 */
if(halves[1]){
animate(halves[1], {opacity:[0,1]}, {duration:.4, delay:.3, easing:EASE_PROD});
const items = halves[1].querySelectorAll('.takeaway-list li');
items.forEach((li, i)=>{
animate(li, {opacity:[0,1], x:[20, 0]},
{duration:.45, delay:1.0 + i*.12, easing:EASE_ENTRY_EXP});
});
}
}
/* ---------- recipe: timeline-walk · P11 横向 evolution ----------
标题先入 → 横轴虚线 scaleX 拉开(伪) → 5 个 dot 按年代依次 scale 入 → label 跟随 */
function rTimelineWalk(slide, all){
const head = slide.querySelector('[data-anim="line"]');
if(head) fade(head, {duration:.55, y:10});
const tl = slide.querySelector('.timeline-h');
if(tl) animate(tl, {opacity:[0,1]}, {duration:.4, delay:.35, easing:EASE_PROD});
const nodes = [...slide.querySelectorAll('.timeline-h .th-node')];
nodes.forEach((node, i)=>{
const base = .55 + i*.18;
const dot = node.querySelector('.dot');
const label = node.querySelector('.label');
if(dot){
dot.style.transformOrigin='center';
animate(dot, {opacity:[0,1], scale:[.2, 1]},
{duration:.45, delay:base, easing:EASE_ENTRY_EXP});
}
if(label){
const fromY = node.classList.contains('up') ? 8 : -8;
/* 保留 CSS 的水平居中 translateX(-50%),避免动效覆盖后 label 与 dot 错位 */
animate(label, {opacity:[0,1], transform:[`translate(-50%, ${fromY}px)`, 'translate(-50%, 0px)']},
{duration:.5, delay:base + .12, easing:EASE_ENTRY_EXP});
}
});
const foot = slide.querySelector('.t-meta');
if(foot) animate(foot, {opacity:[0,1]}, {duration:.3, delay:1.7, easing:EASE_PROD});
}
/* ---------- recipe: manifesto · P12 Form & Found ----------
副标先入 → 大字两段错峰落 → 底部 ink 通栏条从下推上 */
function rManifesto(slide, all){
const head = slide.querySelector('[data-anim="line"]');
if(head){
const cat = head.querySelector('.t-cat');
const title = head.querySelector('div:nth-child(2)');
if(cat) animate(cat, {opacity:[0,1], x:[-10,0]},
{duration:.4, delay:.1, easing:EASE_PROD});
if(title) animate(title, {opacity:[0,1], y:[26, 0]},
{duration:.85, delay:.3, easing:EASE_ENTRY_EXP});
}
/* 底部 ink 条从下推入 */
const foot = [...slide.querySelectorAll('[data-anim="up"]')];
foot.forEach((el, i)=>{
animate(el, {opacity:[0,1], y:[40, 0]},
{duration:.75, delay:.85 + i*.12, easing:EASE_ENTRY_EXP});
});
}
/* ---------- recipe: three-forces · P13 ----------
左 ink hero 先入 → 右 3 张卡按 1/2/3 依次从右滑入 + 每张大数字单独弹入 */
function rThreeForces(slide, all){
const head = slide.querySelector('[data-anim="line"]');
if(head) fade(head, {duration:.5, y:8});
const grid = slide.querySelector('[data-anim="up"]');
if(grid) animate(grid, {opacity:[0,1]}, {duration:.3, delay:.3, easing:EASE_PROD});
const heroBlock = grid?.querySelector(':scope > div:first-child');
if(heroBlock) animate(heroBlock, {opacity:[0,1], x:[-26, 0]},
{duration:.6, delay:.4, easing:EASE_ENTRY_EXP});
const cards = grid ? [...grid.querySelectorAll(':scope > div:nth-child(2) > .card-fill')] : [];
cards.forEach((card, i)=>{
const base = .6 + i*.18;
animate(card, {opacity:[0,1], x:[28, 0]},
{duration:.6, delay:base, easing:EASE_ENTRY_EXP});
const num = card.querySelector(':scope > div:first-child');
if(num) animate(num, {opacity:[0,1], scale:[.7, 1]},
{duration:.5, delay:base + .15, easing:EASE_ENTRY_EXP});
});
}
/* ---------- recipe: loop-form · P14 自学闭环 ----------
左 4 步像台阶依次入 → 右环图节点按时钟顺序入 → 中心 improves scale 入 */
function rLoopForm(slide, all){
const head = slide.querySelector('[data-anim="line"]');
if(head) fade(head, {duration:.55, y:10});
const grid = slide.querySelector('[data-anim="up"]');
if(grid) animate(grid, {opacity:[0,1]}, {duration:.3, delay:.35, easing:EASE_PROD});
/* 左侧 4 步台阶,每步从左滑入 */
const steps = grid ? [...grid.querySelectorAll(':scope > div:first-child > div')] : [];
steps.forEach((step, i)=>{
animate(step, {opacity:[0,1], x:[-18, 0]},
{duration:.5, delay:.5 + i*.14, easing:EASE_ENTRY_EXP});
});
/* 右侧 SVG 节点 (4 个 circle + label) 按 01→04 顺序入 */
const svg = grid?.querySelector('svg');
if(svg){
const ring = svg.querySelector('circle:first-of-type');
if(ring) animate(ring, {opacity:[0,.25]}, {duration:.5, delay:.6, easing:EASE_PROD});
const nodeCircles = [...svg.querySelectorAll('circle')].slice(1);
nodeCircles.forEach((c, i)=>{
c.style.transformOrigin = `${c.getAttribute('cx')}px ${c.getAttribute('cy')}px`;
animate(c, {opacity:[0,1], scale:[.4, 1]},
{duration:.45, delay:.7 + i*.16, easing:EASE_ENTRY_EXP});
});
const arrows = [...svg.querySelectorAll('path[marker-end]')];
arrows.forEach((p, i)=>{
animate(p, {opacity:[0,1]},
{duration:.4, delay:.85 + i*.16, easing:EASE_PROD});
});
const center = [...svg.querySelectorAll('text')].slice(-2);
center.forEach((t, i)=>{
animate(t, {opacity:[0,1], scale:[.7, 1]},
{duration:.5, delay:1.55 + i*.1, easing:EASE_ENTRY_EXP});
});
}
}
/* ---------- recipe: matrix-fill · P15 skill 矩阵 ----------
标题入 → 12 张卡按对角线波 (i+j) 扫入 → 底部 20,000 大数字最后 fade 入 */
function rMatrixFill(slide, all){
const head = slide.querySelector('[data-anim="line"]');
if(head) fade(head, {duration:.55, y:10});
const matrix = slide.querySelector('[data-anim="up"]');
if(!matrix) return;
animate(matrix, {opacity:[0,1]}, {duration:.3, delay:.35, easing:EASE_PROD});
const cards = [...matrix.children];
const cols = 6;
cards.forEach((card, i)=>{
const row = Math.floor(i/cols), col = i%cols;
const wave = (row + col) * .055;
animate(card, {opacity:[0,1], y:[14, 0], scale:[.92, 1]},
{duration:.42, delay:.5 + wave, easing:EASE_ENTRY_EXP});
});
/* 底部 20,000 区块 */
const foot = [...slide.querySelectorAll('[data-anim="up"]')][1];
if(foot){
animate(foot, {opacity:[0,1], y:[18, 0]},
{duration:.7, delay:1.4, easing:EASE_ENTRY_EXP});
const bigNum = foot.querySelector('div:nth-child(1) > div:nth-child(2)');
if(bigNum) animate(bigNum, {opacity:[0,1], scale:[.94, 1]},
{duration:.7, delay:1.55, easing:EASE_ENTRY_EXP});
}
}
/* ---------- recipe: field-notes · P16 散点观察 ----------
标题入 → 6 张卡按"散点"乱序延迟入,微小旋转复位 */
function rFieldNotes(slide, all){
const head = slide.querySelector('[data-anim="line"]');
if(head) fade(head, {duration:.55, y:10});
const grid = slide.querySelector('[data-anim="up"]');
if(!grid) return;
animate(grid, {opacity:[0,1]}, {duration:.3, delay:.35, easing:EASE_PROD});
/* 散点顺序: 用一个稍微打乱的索引数组,营造"乱中有序"感 */
const order = [0, 3, 1, 4, 2, 5];
const cards = [...grid.children];
order.forEach((idx, i)=>{
const card = cards[idx];
if(!card) return;
animate(card, {opacity:[0,1], y:[18, 0], rotate:[(idx%2?-.6:.6), 0]},
{duration:.55, delay:.5 + i*.11, easing:EASE_ENTRY_EXP});
});
}
/* ---------- recipe: system-diagram · P17 三圆系统图 ----------
标题入 → SVG 三组图依次入 + 中间同心圆从外向内 scale 入 → 下方注释列依次入 */
function rSystemDiagram(slide, all){
const head = slide.querySelector('[data-anim="line"]');
if(head) fade(head, {duration:.55, y:10});
const stage = slide.querySelector('[data-anim="up"]');
if(!stage) return;
animate(stage, {opacity:[0,1]}, {duration:.3, delay:.35, easing:EASE_PROD});
const svgs = [...stage.querySelectorAll('svg')];
svgs.forEach((svg, i)=>{
const base = .55 + i*.22;
const circles = [...svg.querySelectorAll('circle')];
/* 中间是同心圆: 从外圈到内圈依次 scale 入 */
if(circles.length > 1){
circles.forEach((c, j)=>{
c.style.transformOrigin = `${c.getAttribute('cx')}px ${c.getAttribute('cy')}px`;
animate(c, {opacity:[0,1], scale:[.4, 1]},
{duration:.5, delay:base + j*.13, easing:EASE_ENTRY_EXP});
});
} else if(circles[0]){
circles[0].style.transformOrigin = `${circles[0].getAttribute('cx')}px ${circles[0].getAttribute('cy')}px`;
animate(circles[0], {opacity:[0,1], scale:[.4, 1]},
{duration:.5, delay:base, easing:EASE_ENTRY_EXP});
}
const labels = [...svg.querySelectorAll('text')];
labels.forEach((t, j)=>{
animate(t, {opacity:[0,1]},
{duration:.4, delay:base + .25 + j*.06, easing:EASE_PROD});
});
});
/* 下方注释列 */
const cols = [...stage.querySelectorAll(':scope > div:last-child > div')];
cols.forEach((col, i)=>{
animate(col, {opacity:[0,1], y:[12, 0]},
{duration:.45, delay:1.3 + i*.1, easing:EASE_ENTRY_EXP});
});
}
/* ---------- recipe: why-now · P18 三列 + 巨大底数 ----------
标题入 → 三列文本入 → 三个底部巨数 01/02/03 错峰 scale 落定 */
function rWhyNow(slide, all){
const head = slide.querySelector('[data-anim="line"]');
if(head) fade(head, {duration:.55, y:10});
const grid = slide.querySelector('[data-anim="up"]');
if(!grid) return;
animate(grid, {opacity:[0,1]}, {duration:.3, delay:.35, easing:EASE_PROD});
const cols = [...grid.children];
cols.forEach((col, i)=>{
const base = .5 + i*.16;
const body = col.querySelector(':scope > div:not(:last-child)');
const big = col.querySelector(':scope > div:last-child');
if(body) animate(body, {opacity:[0,1], y:[14, 0]},
{duration:.55, delay:base, easing:EASE_ENTRY_EXP});
if(big) animate(big, {opacity:[0,1], scale:[.7, 1]},
{duration:.7, delay:base + .35, easing:EASE_ENTRY_EXP});
});
}
/* ---------- recipe: four-cards · P19 4 列卡片 ----------
顶部红线 scaleX 0→1 → 标题入 → 4 卡按 01-04 依次入 */
function rFourCards(slide, all){
/* 顶部红线 */
const topRule = slide.querySelector('[data-anim="line"] > div:first-child');
if(topRule){
topRule.style.transformOrigin = 'left center';
animate(topRule, {opacity:[0,1], scaleX:[0, 1]},
{duration:.5, delay:.1, easing:EASE_ENTRY_EXP});
}
const head = slide.querySelector('[data-anim="line"]');
if(head){
const title = head.querySelector(':scope > div:nth-child(2)');
if(title) animate(title, {opacity:[0,1], y:[14, 0]},
{duration:.55, delay:.4, easing:EASE_ENTRY_EXP});
}
const grid = slide.querySelector('[data-anim="up"]');
if(!grid) return;
animate(grid, {opacity:[0,1]}, {duration:.3, delay:.55, easing:EASE_PROD});
const cards = [...grid.children];
cards.forEach((card, i)=>{
animate(card, {opacity:[0,1], y:[18, 0]},
{duration:.55, delay:.7 + i*.13, easing:EASE_ENTRY_EXP});
});
}
/* ============ P20 · Stacked KPI Ledger · 4 行账单逐行点亮 + 行间发丝从左画 ============ */
function rStackedLedger(slide, all){
const ledger = slide.querySelector('[data-anim="ledger"]');
if(!ledger) return;
animate(ledger, {opacity:[0,1]}, {duration:.3, delay:.1, easing:EASE_PROD});
const rows = [...ledger.querySelectorAll('.ledger-row')];
rows.forEach((row, i)=>{
const base = .25 + i*.18;
const num = row.querySelector('.ledger-num');
const label = row.querySelector('.ledger-label');
const icon = row.querySelector('.ledger-icon');
if(num) animate(num, {opacity:[0,1], y:[20, 0]}, {duration:.7, delay:base, easing:EASE_ENTRY_EXP});
if(label) animate(label, {opacity:[0,1], x:[-12, 0]}, {duration:.55, delay:base + .12, easing:EASE_ENTRY_EXP});
if(icon) animate(icon, {opacity:[0,1], scale:[.6,1]},{duration:.55, delay:base + .22, easing:EASE_ENTRY_EXP});
});
}
/* ============ P21 · Tech Spec Sheet · 标题分行 / KPI 顶线画出 + count 风感 / 竖线弹起 / 底巨数 ============ */
function rTechSpec(slide, all){
const head = slide.querySelector('[data-anim="line"]');
if(head) fade(head, {duration:.5, y:8});
const main = slide.querySelector('[data-anim="up"]');
if(main){
animate(main, {opacity:[0,1]}, {duration:.3, delay:.25, easing:EASE_PROD});
/* 左大标题分行 */
const titleLines = main.querySelector(':scope > div:first-child > div:first-child');
if(titleLines){
animate(titleLines, {opacity:[0,1], y:[18, 0]}, {duration:.7, delay:.35, easing:EASE_ENTRY_EXP});
}
const titleNote = main.querySelector(':scope > div:first-child > div:nth-child(2)');
if(titleNote){
animate(titleNote, {opacity:[0,1], y:[10, 0]}, {duration:.5, delay:.95, easing:EASE_ENTRY_EXP});
}
/* 三 KPI · 顶线 scaleX + 数字 fade-up + 副文字 */
const kpis = [...main.querySelectorAll(':scope > div:not([data-anim]):not(:first-child)')];
kpis.forEach((kpi, i)=>{
const base = .55 + i*.18;
const topRule = kpi.querySelector(':scope > div:first-child');
if(topRule){
topRule.style.transformOrigin = 'left center';
animate(topRule, {scaleX:[0,1], opacity:[0,1]}, {duration:.5, delay:base, easing:EASE_ENTRY_EXP});
}
const num = kpi.querySelector('.kpi-num');
if(num) animate(num, {opacity:[0,1], y:[14, 0]}, {duration:.6, delay:base + .15, easing:EASE_ENTRY_EXP});
const otherKids = [...kpi.children].filter(el=>el !== topRule && el !== num);
otherKids.forEach((el, j)=>{
animate(el, {opacity:[0,1]}, {duration:.4, delay:base + .25 + j*.05, easing:EASE_PROD});
});
});
}
/* 底部 hero 区: 巨数 + goal + tags + 右下竖线 */
const hero = slide.querySelector('[data-anim="hero"]');
if(hero){
animate(hero, {opacity:[0,1]}, {duration:.3, delay:1.3, easing:EASE_PROD});
const bottomHero = hero.querySelector('.bottom-hero');
if(bottomHero) animate(bottomHero, {opacity:[0,1], y:[24, 0], scale:[.92, 1]}, {duration:.7, delay:1.4, easing:EASE_ENTRY_EXP});
const middle = hero.querySelector(':scope > div:nth-child(2)');
if(middle){
const kids = [...middle.children];
kids.forEach((el, i)=>{
if(el.style && el.style.background === 'var(--ink)'){
el.style.transformOrigin = 'left center';
animate(el, {scaleX:[0,1], opacity:[0,1]}, {duration:.5, delay:1.6 + i*.1, easing:EASE_ENTRY_EXP});
} else {
animate(el, {opacity:[0,1], y:[10, 0]}, {duration:.5, delay:1.55 + i*.1, easing:EASE_ENTRY_EXP});
}
});
}
/* 右下: 文字先入, 9 根竖线再从底部 scaleY 弹起 */
const right = hero.querySelector(':scope > div:nth-child(3)');
if(right){
const rightText = right.querySelector(':scope > div:last-child');
if(rightText) animate(rightText, {opacity:[0,1], y:[10, 0]}, {duration:.5, delay:1.85, easing:EASE_ENTRY_EXP});
}
const bars = slide.querySelectorAll('[data-anim="bars"] .vbar');
bars.forEach((bar, i)=>{
bar.style.transformOrigin = 'bottom';
animate(bar, {scaleY:[0,1], opacity:[0,1]}, {duration:.5, delay:2.0 + i*.04, easing:EASE_ENTRY_EXP});
});
}
}
/* ============ P22 · Image Hero · 图缓推 + 标题白块从左滑入 + 三 KPI 顶线画出 ============ */
function rImageHero(slide, all){
const img = slide.querySelector('[data-anim="img"] img');
if(img){
animate(img, {opacity:[0,1], scale:[1.06, 1]}, {duration:1.1, delay:.05, easing:EASE_ENTRY_EXP});
}
const titleBlock = slide.querySelector('[data-anim="title-block"]');
if(titleBlock){
titleBlock.style.transformOrigin = 'left center';
animate(titleBlock, {opacity:[0,1], scaleX:[0, 1]}, {duration:.7, delay:.45, easing:EASE_ENTRY_EXP});
const titleText = titleBlock.querySelector('div');
if(titleText) animate(titleText, {opacity:[0,1]}, {duration:.4, delay:.85, easing:EASE_PROD});
}
const kpiWrap = slide.querySelector('[data-anim="kpi"]');
if(kpiWrap){
animate(kpiWrap, {opacity:[0,1]}, {duration:.3, delay:.7, easing:EASE_PROD});
/* 段落 */
const para = kpiWrap.querySelector(':scope > div:first-child');
if(para) animate(para, {opacity:[0,1], y:[14, 0]}, {duration:.6, delay:.85, easing:EASE_ENTRY_EXP});
/* 三列 KPI · 顶线 scaleX + 数字升起 */
const cols = [...kpiWrap.querySelectorAll(':scope > div:nth-child(2) > div')];
cols.forEach((col, i)=>{
const base = 1.1 + i*.18;
const topRule = col.querySelector(':scope > div:first-child');
if(topRule){
topRule.style.transformOrigin = 'left center';
animate(topRule, {scaleX:[0,1], opacity:[0,1]}, {duration:.5, delay:base, easing:EASE_ENTRY_EXP});
}
const cat = col.querySelector('.t-meta');
if(cat) animate(cat, {opacity:[0,1]}, {duration:.4, delay:base + .15, easing:EASE_PROD});
const hero = col.querySelector('.kpi-hero');
if(hero) animate(hero, {opacity:[0,1], y:[18, 0]}, {duration:.7, delay:base + .25, easing:EASE_ENTRY_EXP});
const handled = new Set([topRule, cat, hero]);
[...col.children]
.filter(el => !handled.has(el))
.forEach((el, j)=>{
animate(el, {opacity:[0,1]}, {duration:.4, delay:base + .45 + j*.05, easing:EASE_PROD});
});
});
}
}
const RECIPES = {
'hero': rHero,
'progression': rProgression,
'statement': rStatement,
'grid-reveal': rGridReveal,
'stack-build': rStackBuild,
'measure-up': rMeasureUp,
'bar-grow': rBarGrow,
'duo-mirror': rDuoMirror,
'split-statement': rSplitStatement,
'timeline-walk': rTimelineWalk,
'manifesto': rManifesto,
'three-forces': rThreeForces,
'loop-form': rLoopForm,
'matrix-fill': rMatrixFill,
'field-notes': rFieldNotes,
'system-diagram': rSystemDiagram,
'why-now': rWhyNow,
'four-cards': rFourCards,
'stacked-ledger': rStackedLedger,
'tech-spec': rTechSpec,
'image-hero': rImageHero,
};
function revealStatic(slide){
resetAnims(slide);
document.getAnimations?.().forEach(a=>a.cancel());
slide.querySelectorAll('[data-anim],.row-fill,.tl-node,.stack-block,.bar-tower,.sub-card,.col,.vrule,.kpi-cell,.card-fill,.card-accent,.card-ink')
.forEach(el=>{
el.style.opacity='1';
el.style.transform='none';
});
}
function playSlide(i){
const slide = slides[i];
if(!slide) return;
lastIdx = i;
if(window.__lowPowerMode){
revealStatic(slide);
return;
}
resetAnims(slide);
/* 关键:[data-anim] 容器很多时候只是占位标记,真正的几何动画在子元素上.
默认强制 reveal 所有 [data-anim] 容器, recipe 想做块入场时用 motion 的 {opacity:[0,1]} 会自动覆盖 */
slide.querySelectorAll('[data-anim]').forEach(el=>{
el.style.opacity = '1';
el.style.transform = 'none';
});
const all = [...slide.querySelectorAll('[data-anim]')];
const recipe = slide.dataset.animate;
const fn = RECIPES[recipe];
if(fn){ fn(slide, all); return; }
/* fallback: 平凡 fade */
if(all.length) animate(all, {opacity:[0,1], y:[12,0]},
{duration:.6, delay:i=>i*.08, easing:EASE_ENTRY_EXP});
}
window.__playSlide = playSlide;
window.__pipeAdvance = ()=>false; /* 当前 deck 不用 pipeline recipe */
playSlide(window.__currentSlideIndex || 0);
}
</script>
<script>
/* ============== ASCII 点阵呼吸场 · IKB 封面/封底专用 ==============
sin/cos 二维噪声场驱动字符显隐,营造工业仪表板的"涌动呼吸"质感.
纯 canvas 2D, mix-blend-mode:screen 让字符在 IKB 底色上自然发亮.
用法:在需要呼吸场的容器(.canvas-card 或 split .half.b-accent)内首位插入
<canvas class="ascii-bg" aria-hidden="true">,本脚本会自动扫描并启动. */
(function(){
const canvases = [...document.querySelectorAll('canvas.ascii-bg')];
if(!canvases.length) return;
const PALETTE = ' ...:::---+++***◦◦••▢▣';
const CELL = 16;
const FONT_SIZE = 13;
function setup(c){
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const rect = c.getBoundingClientRect();
if(rect.width < 4 || rect.height < 4) return false;
c.width = Math.round(rect.width * dpr);
c.height = Math.round(rect.height * dpr);
c.__dpr = dpr;
c.__w = rect.width;
c.__h = rect.height;
const ctx = c.getContext('2d');
ctx.setTransform(dpr,0,0,dpr,0,0);
const mono = (getComputedStyle(document.documentElement).getPropertyValue('--mono') || 'JetBrains Mono, monospace').trim();
ctx.font = `500 ${FONT_SIZE}px ${mono}`;
ctx.textBaseline = 'top';
c.__ctx = ctx;
return true;
}
function draw(c, t){
if(!c.__ctx) return;
const ctx = c.__ctx, w = c.__w, h = c.__h;
ctx.clearRect(0, 0, w, h);
const cols = Math.ceil(w / CELL);
const rows = Math.ceil(h / CELL);
for(let r=0; r<rows; r++){
for(let cc=0; cc<cols; cc++){
const n = (
Math.sin(cc * 0.18 + t) +
Math.sin(r * 0.24 - t * 0.7) +
Math.sin((cc + r) * 0.12 + t * 0.45) +
Math.sin(Math.hypot(cc - cols * 0.5, r - rows * 0.5) * 0.16 - t * 0.55)
) / 4; // [-1, 1]
const v = (n + 1) / 2; // [0, 1]
if(v < 0.22) continue;
const idx = Math.min(PALETTE.length - 1, Math.floor(v * PALETTE.length));
const ch = PALETTE[idx];
if(ch === ' ') continue;
const alpha = 0.08 + (v - 0.22) * 0.55;
ctx.fillStyle = `rgba(255,255,255,${alpha.toFixed(3)})`;
ctx.fillText(ch, cc * CELL, r * CELL);
}
}
}
function resizeAll(){ canvases.forEach(setup); }
let pending = null;
window.addEventListener('resize', ()=>{
if(window.__lowPowerMode) return;
if(pending) cancelAnimationFrame(pending);
pending = requestAnimationFrame(resizeAll);
}, {passive:true});
let t0 = performance.now();
let frame = 0, asciiRAF = 0, running = false;
function tick(now){
if(!running || window.__lowPowerMode){running=false;asciiRAF=0;return;}
const t = (now - t0) / 1000 * 0.55;
frame++;
canvases.forEach(c=>{
// 离屏 slide 降帧:每 4 帧渲染一次,在屏 slide 每帧渲染
const slide = c.closest('.slide');
const rect = slide ? slide.getBoundingClientRect() : null;
const onscreen = rect && rect.right > 0 && rect.left < window.innerWidth;
if(!onscreen && (frame & 3) !== 0) return;
draw(c, t);
});
asciiRAF = requestAnimationFrame(tick);
}
function start(){
if(running || window.__lowPowerMode) return;
resizeAll();
t0 = performance.now();
frame = 0;
running = true;
asciiRAF = requestAnimationFrame(tick);
}
function stop(){
running = false;
if(asciiRAF) cancelAnimationFrame(asciiRAF);
if(pending) cancelAnimationFrame(pending);
asciiRAF = 0;
pending = null;
canvases.forEach(c=>{
if(c.__ctx) c.__ctx.clearRect(0,0,c.__w || 0,c.__h || 0);
});
}
addEventListener('swiss-low-power-change', e=>{e.detail.on ? stop() : start();});
start();
})();
</script>
</body>
</html>