Files
guizang-ppt-skill/assets/template-swiss.html
T

2936 lines
160 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
}
/* ============ 网格系统 (模块化网格) ============ */
.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;display:flex;align-items:center;gap:.65em;font-family:var(--mono);font-size:14px;letter-spacing:.14em;text-transform:uppercase;opacity:.4}
#hint button{all:unset;display:inline-flex;align-items:center;gap:.42em;cursor:pointer;color:inherit;white-space:nowrap}
#hint button:hover,#hint button:focus-visible{opacity:1;text-decoration:underline;text-underline-offset:.3em}
#hint .ppt-key{padding:.12em .34em;border:1px solid currentColor;letter-spacing:.06em}
#hint .ppt-sep{opacity:.55}
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}
/* ============ 演讲者模式 ============ */
body.ppt-audience #hint,body.ppt-audience #nav,
body.ppt-preview #hint,body.ppt-preview #nav{display:none!important}
body.ppt-audience{cursor:none}
body.ppt-preview{pointer-events:none}body.ppt-preview #deck{transition:none!important}
body.ppt-presenter>#deck,body.ppt-presenter>#nav,body.ppt-presenter>#hint,
body.ppt-presenter>.bg{visibility:hidden}
#ppt-presenter{position:fixed;inset:0;z-index:90;display:none;min-width:0;min-height:0;overflow:hidden;background:#101216;color:#f5f6f7;font-family:Inter,"Noto Sans SC",sans-serif}
body.ppt-presenter #ppt-presenter{display:grid;grid-template-rows:auto 1fr auto}
.ppt-presenter-top{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:18px 24px;border-bottom:1px solid #2b2e35;background:#15181d}
.ppt-presenter-brand{font-weight:650;letter-spacing:.02em}.ppt-presenter-meta{display:flex;align-items:center;gap:12px;color:#aeb4be;font-size:14px}
.ppt-sync{display:inline-flex;align-items:center;gap:8px;padding:7px 11px;border:1px solid #363a43;border-radius:999px;color:#d2d6dc;background:#1b1e24}
.ppt-sync::before{content:"";width:8px;height:8px;border-radius:50%;background:#818894}
.ppt-sync[data-state="synced"]::before{background:#72df99;box-shadow:0 0 0 4px rgba(114,223,153,.12)}
.ppt-sync[data-state="connecting"]::before,.ppt-sync[data-state="frozen"]::before{background:#f0c766}.ppt-sync[data-state="unsynced"]::before,.ppt-sync[data-state="blocked"]::before{background:#ff9e64}.ppt-sync[data-state="disconnected"]::before{background:#ff6b72}
.ppt-ui-btn{appearance:none;border:1px solid #3a3e48;background:#20242a;color:#f6f7f8;border-radius:8px;padding:9px 13px;font:600 14px/1 Inter,"Noto Sans SC",sans-serif;cursor:pointer}
.ppt-ui-btn:hover{background:#2a2f37;border-color:#525864}.ppt-ui-btn:focus-visible{outline:2px solid #84a8ff;outline-offset:2px}.ppt-ui-btn:disabled{opacity:.28;cursor:not-allowed}
.ppt-presenter-main{display:grid;grid-template-columns:minmax(0,1.75fr) minmax(300px,.75fr);gap:18px;min-height:0;padding:18px 24px}
.ppt-preview-stack{position:relative;display:grid;grid-template-rows:minmax(0,1fr) clamp(132px,22vh,210px);gap:16px;min-width:0;min-height:0}
.ppt-stage-card,.ppt-notes-card,.ppt-presenter-overview{background:#171a20;border:1px solid #2b2f37;border-radius:12px;overflow:hidden}
.ppt-stage-card{display:grid;grid-template-rows:auto minmax(0,1fr);min-width:0;min-height:0}
.ppt-stage-label{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:11px 14px;color:#aeb4be;font-size:13px;border-bottom:1px solid #292d34}
.ppt-annotation-tools{display:flex;align-items:center;gap:5px;margin-left:auto}.ppt-annotation-tools .ppt-ui-btn{padding:6px 9px;font-size:12px}.ppt-annotation-tools .ppt-ui-btn.is-active{color:#111318;background:#ff5b63;border-color:#ff5b63}.ppt-annotation-tools .ppt-grid-btn.is-active{color:#111318;background:#edf1f5;border-color:#edf1f5}.ppt-annotation-tools .ppt-clear-btn{min-width:0}.ppt-grid-btn{display:inline-flex;align-items:center;gap:6px}.ppt-grid-icon{display:grid;grid-template-columns:repeat(2,4px);gap:2px}.ppt-grid-icon i{width:4px;height:4px;background:currentColor}
.ppt-frame-viewport{position:relative;display:grid;place-items:center;min-width:0;min-height:0;overflow:hidden;background:#08090b}
.ppt-frame{display:block;width:0;height:0;max-width:100%;max-height:100%;aspect-ratio:16/9;border:0;background:#08090b}.ppt-next-frame{opacity:.86}#ppt-next-card.is-end .ppt-frame{visibility:hidden}#ppt-next-card.is-end .ppt-frame-viewport::after{content:"演示结束";position:absolute;color:#727a86;font:650 14px/1 Inter,"Noto Sans SC",sans-serif;letter-spacing:.08em}
.ppt-annotation-layer{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:4;width:0;height:0;pointer-events:none;touch-action:none}.ppt-annotation-layer.is-active{pointer-events:auto;cursor:crosshair}
#ppt-audience-ink{position:fixed;inset:0;z-index:70;width:100vw;height:100vh;pointer-events:none}#ppt-audience-cover{position:fixed;inset:0;z-index:80;display:none;pointer-events:none}#ppt-audience-cover.is-ended{display:grid!important;place-items:center;background:#101216!important;color:#f5f6f7;font:650 clamp(24px,3vw,46px)/1.2 Inter,"Noto Sans SC",sans-serif;letter-spacing:.08em}
.ppt-preview-stack.is-overview>.ppt-stage-card{display:none}.ppt-presenter-overview{grid-row:1/-1;display:grid;grid-template-rows:auto minmax(0,1fr);min-width:0;min-height:0}.ppt-presenter-overview[hidden]{display:none}.ppt-overview-head{display:flex;align-items:center;justify-content:space-between;padding:13px 15px;border-bottom:1px solid #292d34}.ppt-overview-head strong{font-size:15px}.ppt-overview-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;align-content:start;overflow:auto;padding:14px}.ppt-overview-page{appearance:none;min-height:112px;display:grid;grid-template-rows:auto 1fr auto;gap:12px;padding:13px;border:1px solid #343943;border-radius:10px;background:#111318;color:#eef1f4;text-align:left;cursor:pointer}.ppt-overview-page:hover{background:#1d2128;border-color:#636b78}.ppt-overview-page.is-current{border-color:#84a8ff;box-shadow:inset 0 0 0 1px #84a8ff}.ppt-overview-page-no{color:#8f97a3;font:600 12px/1 var(--mono,monospace)}.ppt-overview-page-title{font-size:14px;font-weight:650;line-height:1.35}.ppt-overview-page-meta{display:flex;justify-content:space-between;gap:8px;color:#7f8793;font-size:11px}
.ppt-notes-card{display:grid;grid-template-rows:auto minmax(0,1fr);min-height:0}.ppt-notes-head{padding:16px 18px;border-bottom:1px solid #292d34}.ppt-notes-label{margin-bottom:6px;color:#848c98;font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}.ppt-notes-title{font-size:21px;font-weight:680;line-height:1.32}.ppt-purpose-block{margin-top:14px;padding:11px 12px;border-left:3px solid #84a8ff;background:#111318}.ppt-purpose-copy{color:#c9ced6;font-size:14px;line-height:1.55}.ppt-notes-draft-head{display:flex;align-items:flex-end;justify-content:space-between;gap:10px;margin-top:15px;padding-top:13px;border-top:1px solid #2d3139}.ppt-notes-draft-head .ppt-notes-label{margin:0 0 4px}.ppt-note-meta{color:#8f97a3;font-size:12px}
.ppt-notes-tools{display:flex;align-items:center;gap:6px}.ppt-notes-tools .ppt-ui-btn{padding:7px 9px}.ppt-note-editor{width:100%;height:100%;min-height:0;resize:none;border:0;outline:0;background:#171a20;color:#edf0f3;padding:18px;font:400 var(--ppt-note-size,17px)/1.72 Inter,"Noto Sans SC",sans-serif}.ppt-note-editor:focus{box-shadow:inset 0 0 0 1px #3d4654}
.ppt-saved{color:#72df99;white-space:nowrap}.ppt-presenter-bottom{display:grid;grid-template-columns:minmax(330px,1fr) auto minmax(110px,1fr);align-items:center;gap:20px;padding:12px 24px;border-top:1px solid #2b2e35;background:#15181d}
.ppt-time-dashboard{display:flex;align-items:stretch;min-width:0}.ppt-time-stat{min-width:90px;display:grid;align-content:center;gap:4px;padding:0 16px}.ppt-time-stat:first-child{padding-left:0}.ppt-time-stat+.ppt-time-stat{border-left:1px solid #343943}.ppt-time-label{color:#89919d;font-size:11px;font-weight:700;letter-spacing:.1em}.ppt-time-value{color:#f7f8fa;font:650 23px/1 var(--mono,monospace);font-variant-numeric:tabular-nums}.ppt-time-hint{color:#747c88;font-size:11px;white-space:nowrap}.ppt-time-stat.is-over .ppt-time-label,.ppt-time-stat.is-over .ppt-time-value{color:#ff8a90}.ppt-time-stat.is-remaining .ppt-time-value{color:#72df99}
.ppt-control-stack{display:grid;gap:7px;justify-items:center}.ppt-controls,.ppt-session-actions{display:flex;align-items:center;justify-content:center;gap:7px}.ppt-controls .ppt-ui-btn,.ppt-session-actions .ppt-ui-btn{min-width:68px}.ppt-session-actions{padding-top:7px;border-top:1px solid #343943}.ppt-rehearsal-on{color:#ff8a90}.ppt-page-state{text-align:right}.ppt-page-state strong{display:block;color:#fff;font-size:17px;margin-bottom:5px}.ppt-page-state span{color:#8f97a3;font:600 13px/1 var(--mono,monospace)}.ppt-auto-state{display:inline-flex;align-items:center;gap:6px;border-radius:999px}.ppt-auto-state.is-on{color:#72df99;border-color:rgba(114,223,153,.45);background:rgba(114,223,153,.08)}.ppt-auto-state.is-paused{color:#f0c766;border-color:rgba(240,199,102,.42);background:rgba(240,199,102,.08)}
#ppt-presenter[data-layout="slides"] .ppt-presenter-main{grid-template-columns:minmax(0,2.15fr) minmax(260px,.58fr)}#ppt-presenter[data-layout="notes"] .ppt-presenter-main{grid-template-columns:minmax(0,1.15fr) minmax(380px,1fr)}
.ppt-dialog{position:fixed;inset:0;z-index:130;display:grid;place-items:center;padding:24px;background:rgba(4,6,9,.72);backdrop-filter:blur(10px)}.ppt-dialog[hidden]{display:none}.ppt-dialog-panel{width:min(720px,calc(100vw - 48px));max-height:min(760px,calc(100vh - 48px));display:grid;grid-template-rows:auto minmax(0,1fr);overflow:hidden;border:1px solid #343943;border-radius:14px;background:#171a20;box-shadow:0 24px 80px rgba(0,0,0,.45)}.ppt-dialog-head{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 18px;border-bottom:1px solid #2b2f37}.ppt-dialog-title{font-size:20px;font-weight:700}.ppt-dialog-body{overflow:auto;padding:18px;color:#d8dce2;font-size:14px;line-height:1.6}.ppt-dialog-section+.ppt-dialog-section{margin-top:22px}.ppt-dialog-section h3{margin:0 0 10px;color:#fff;font-size:15px}.ppt-dialog-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.ppt-dialog-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:10px 0;border-bottom:1px solid #282c33}.ppt-dialog-row:last-child{border-bottom:0}.ppt-field{display:grid;gap:6px}.ppt-field label{color:#aeb4be;font-size:12px}.ppt-field input{width:100%;border:1px solid #3a3e48;border-radius:8px;background:#101216;color:#fff;padding:9px 10px;font:600 14px Inter,sans-serif}.ppt-check{display:flex;align-items:flex-start;gap:10px}.ppt-check-dot{width:9px;height:9px;margin-top:7px;border-radius:50%;background:#ff6b72}.ppt-check.is-ok .ppt-check-dot{background:#72df99}.ppt-check.is-warn .ppt-check-dot{background:#f0c766}.ppt-summary{width:100%;border-collapse:collapse;font-size:13px}.ppt-summary th,.ppt-summary td{padding:9px 8px;border-bottom:1px solid #2b2f37;text-align:left}.ppt-summary th{color:#9fa6b1;font-weight:600}.ppt-summary td:last-child,.ppt-summary th:last-child{text-align:right}.ppt-muted{color:#8f96a1}.ppt-shortcuts{display:grid;grid-template-columns:auto 1fr;gap:8px 14px}.ppt-shortcuts kbd{min-width:44px;padding:4px 7px;border:1px solid #3a3e48;border-radius:6px;background:#101216;color:#fff;text-align:center;font:600 12px var(--mono,monospace)}
.ppt-settings-card{padding:15px;border:1px solid #30353e;border-radius:12px;background:#12151a}.ppt-settings-card+.ppt-settings-card{margin-top:12px}.ppt-settings-card h3{margin:0 0 3px}.ppt-settings-copy{margin:0 0 14px;color:#858d99;font-size:12px}.ppt-switch-row{display:flex;align-items:center;justify-content:space-between;gap:18px;cursor:pointer}.ppt-switch-row input{position:absolute;opacity:0;pointer-events:none}.ppt-switch{width:48px;height:28px;flex:0 0 auto;padding:3px;border:1px solid #434955;border-radius:999px;background:#252a31;transition:.2s}.ppt-switch::after{content:"";display:block;width:20px;height:20px;border-radius:50%;background:#a8afb9;transition:.2s}.ppt-switch-row input:checked+.ppt-switch{border-color:#72df99;background:rgba(114,223,153,.16)}.ppt-switch-row input:checked+.ppt-switch::after{transform:translateX(20px);background:#72df99}.ppt-stepper{display:grid;grid-template-columns:38px minmax(72px,1fr) auto 38px;align-items:center;margin-top:14px;border:1px solid #363c46;border-radius:10px;background:#0d0f13;overflow:hidden}.ppt-stepper button{height:42px;border:0;background:#20242a;color:#fff;font-size:19px;cursor:pointer}.ppt-stepper button:hover{background:#2c323b}.ppt-stepper input{width:100%;height:42px;border:0!important;border-radius:0!important;background:transparent!important;text-align:center;font:650 16px var(--mono,monospace)!important;-moz-appearance:textfield}.ppt-stepper input::-webkit-inner-spin-button,.ppt-stepper input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.ppt-stepper-unit{color:#858d99;font-size:12px}.ppt-segmented{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}.ppt-segmented.screen{grid-template-columns:repeat(4,1fr)}.ppt-segmented .ppt-ui-btn{min-width:0}.ppt-settings-save{width:100%;margin-top:14px;background:#edf1f5;color:#111318;border-color:#edf1f5}.ppt-settings-save:hover{background:#fff}.ppt-settings-save+.ppt-settings-copy{margin:10px 0 0}
#overview{z-index:110!important}
@media(max-width:1200px){.ppt-presenter-main{grid-template-columns:minmax(0,1.55fr) minmax(280px,.72fr)}}
@media(max-width:1100px){.ppt-presenter-bottom{grid-template-columns:minmax(292px,1fr) auto minmax(86px,.6fr);gap:12px}.ppt-time-stat{min-width:82px;padding:0 11px}.ppt-time-value{font-size:20px}.ppt-controls .ppt-ui-btn,.ppt-session-actions .ppt-ui-btn{min-width:60px;padding:8px 9px}}
@media(max-width:900px){.ppt-presenter-top{padding:12px 16px;flex-wrap:wrap}.ppt-presenter-top .ppt-presenter-meta{gap:6px}.ppt-presenter-top .ppt-ui-btn{padding:8px 9px}.ppt-presenter-main,#ppt-presenter[data-layout="slides"] .ppt-presenter-main,#ppt-presenter[data-layout="notes"] .ppt-presenter-main{grid-template-columns:minmax(0,1.45fr) minmax(250px,.72fr);gap:12px;padding:12px 16px}.ppt-preview-stack{gap:12px;grid-template-rows:minmax(0,1fr) clamp(104px,20vh,150px)}.ppt-saved{display:none}.ppt-presenter-bottom{grid-template-columns:minmax(270px,1fr) auto 74px;gap:8px;padding:9px 16px}.ppt-time-stat{min-width:0;padding:0 9px}.ppt-time-value{font-size:18px}.ppt-time-hint{max-width:78px;overflow:hidden;text-overflow:ellipsis}.ppt-controls,.ppt-session-actions{gap:5px}.ppt-controls .ppt-ui-btn,.ppt-session-actions .ppt-ui-btn{min-width:52px;padding:7px 8px;font-size:12px}.ppt-page-state strong{font-size:14px}.ppt-dialog-grid{grid-template-columns:1fr}.ppt-segmented.screen{grid-template-columns:repeat(2,1fr)}}
@media(max-height:700px){.ppt-presenter-top{padding-top:10px;padding-bottom:10px}.ppt-presenter-main{padding-top:10px;padding-bottom:10px}.ppt-preview-stack{grid-template-rows:minmax(0,1fr) clamp(88px,16vh,118px);gap:10px}.ppt-stage-label{padding:7px 10px}.ppt-notes-head{padding:12px 14px}.ppt-purpose-block{margin-top:10px;padding:8px 10px}.ppt-notes-draft-head{margin-top:10px;padding-top:9px}.ppt-presenter-bottom{padding-top:8px;padding-bottom:8px}.ppt-control-stack{gap:5px}.ppt-session-actions{padding-top:5px}}
/* ============ /演讲者模式 ============ */
/* 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.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}
.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 label = document.querySelector('.ppt-low-power-label');
if(label) label.textContent = window.__lowPowerMode ? '动态' : '静态';
}
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" role="toolbar" aria-label="快捷控制">
<span>← → 翻页</span><span class="ppt-sep">·</span>
<button id="ppt-low-power-btn" type="button"><span class="ppt-key">B</span><span class="ppt-low-power-label">静态</span></button><span class="ppt-sep">·</span>
<button id="ppt-overview-btn" type="button"><span class="ppt-key">ESC</span>总览</button><span class="ppt-sep">·</span>
<button id="ppt-presenter-btn" type="button"><span class="ppt-key">P</span>演讲模式</button>
</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" data-slide-id="cover">
<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" data-slide-id="closing">
<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>
<!-- 演讲备注:替换示例页时同步替换,id 必须与 section[data-slide-id] 一致。 -->
<script>
const SPEAKER_NOTES = [
{
id: 'cover', title: '[必填] Deck 标题', section: '开场', minutes: 0.6,
purpose: '建立主题与现场预期',
talk: ['用一句反差或问题开场', '说明为什么是现在讲这件事', '只预告主线,不在封面展开细节'],
transition: '从开场问题进入核心内容', cue: '停顿,确认观众屏已同步'
},
{
id: 'closing', title: '[必填] Closing', section: '收束', minutes: 0.8,
purpose: '收束全场并留下可记忆的行动点',
talk: ['回扣开场时提出的问题', '把三条 takeaway 说成一个完整判断', '给观众一个会后可以立即采取的动作'],
transition: '结束演讲,进入提问或致谢', cue: '停顿 2 秒后致谢'
}
];
window.__SPEAKER_NOTES__ = SPEAKER_NOTES;
</script>
<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,opts={}){
if(lock&&!opts.force)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);
window.__pptPresenter?.onSlideChange(idx);
if(!opts.force){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=window.__pptPresenter?.slideLabel(i) || ((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';}
}
/* =============== 演讲者模式 / 观众屏同步 =============== */
window.__pptPresenter=(()=>{
const query=new URLSearchParams(location.search);
const requestedMode=query.get('mode')||'deck';
const originalTitle=document.title;
const notes=()=>Array.isArray(window.__SPEAKER_NOTES__)?window.__SPEAKER_NOTES__:[];
const slideId=i=>slides[i]?.dataset.slideId||`slide-${String(i+1).padStart(2,'0')}`;
const storageKey=name=>`guizang-ppt-${name}:${location.pathname}`;
let mode='deck',session=query.get('session')||Math.random().toString(36).slice(2),audienceWindow=null;
let channel=null,transportReady=false,popupBlocked=false,audienceOpenedAt=0,lastAckAt=0,lastAckIndex=-1,lastAckSeq=-1,seq=0,lastPingAt=0;
let renderedNoteId='',saveTimer=0,noteSize=Number(localStorage.getItem('guizang-ppt-note-size')||17),presenterFrameObserver=null;
let elapsed=0,timerRunning=false,timerStartedAt=0,timedSlideIndex=idx,slideTimerStartedAt=0,slideElapsed=Array(total).fill(0);
let rehearsalActive=false,presenterOverviewOn=false,lowPowerBeforePresenter=window.__lowPowerMode;
let layoutMode=localStorage.getItem(storageKey('layout'))||'balanced';
let autoEnabled=localStorage.getItem(storageKey('auto-enabled'))==='1';
let autoGlobalSeconds=Math.max(0,Number(localStorage.getItem(storageKey('auto-seconds'))||0));
let autoDeadline=0,autoRemainingMs=0;
let screenMode='normal',audienceFrozen=false;
let annotationTool='none',annotationPointerDown=false,annotationStart=null,draftCircle=null,circles=[],audienceCircles=[];
let laserPoint=null,audienceLaser=null,laserHideAt=0,audienceLaserHideAt=0,lastLaserSent=0;
const seenNonces=[],seenNonceSet=new Set();
const syncKey=()=>`guizang-ppt-sync:${location.pathname}:${session}`;
const channelName=()=>`guizang-ppt:${location.pathname}:${session}`;
function noteAt(i){
const id=slideId(i);
return notes().find(n=>n.id===id)||{id,title:slides[i]?.dataset.title||`第 ${i+1} 页`,purpose:'—',talk:[],transition:''};
}
function slideLabel(i){return `${i+1} / ${total} · ${noteAt(i).title||slideId(i)}`;}
function escapeHTML(value){return String(value??'').replace(/[&<>"']/g,ch=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]));}
function valueLines(value){
if(Array.isArray(value))return value.filter(Boolean).map(x=>`• ${x}`).join('\n');
return value?String(value):'';
}
function generatedNote(n){
const parts=[];
if(Array.isArray(n.talk)&&n.talk.length)parts.push(`【讲述要点】\n${valueLines(n.talk)}`);
if(n.transition)parts.push(`【转场】\n${n.transition}`);
const optional=[['舞台提示',n.cue],['互动',n.interaction],['表达提示',n.delivery],['翻页时机',n.advance],['备用方案',n.fallback],['读音提醒',n.pronunciation]];
optional.forEach(([label,value])=>{const text=valueLines(value);if(text)parts.push(`【${label}】\n${text}`);});
return parts.join('\n\n')||'—';
}
function noteStorageKey(id){return `guizang-ppt-note:${location.pathname}:${id}`;}
function loadNoteValue(id,i,n){
const saved=localStorage.getItem(noteStorageKey(id));
if(saved!==null)return saved;
const prefix=window.__PPT_LEGACY_NOTE_KEY__;
if(prefix){
const legacy=localStorage.getItem(`${prefix}:${i}`);
if(legacy!==null){localStorage.setItem(noteStorageKey(id),legacy);return legacy;}
}
return generatedNote(n);
}
function frameUrl(targetMode,i){
const url=new URL(location.href);url.hash='';
url.searchParams.set('mode',targetMode);url.searchParams.set('session',session);url.searchParams.set('slide',String(i+1));
return url.href;
}
function postPreviewGoto(frame,i){
try{frame.contentWindow?.postMessage({__guizangPptSync:1,session,role:'presenter',type:'preview-goto',index:i},'*');}catch(e){}
}
function updatePreviewFrame(frame,i){
frame.dataset.slide=String(i);
if(!frame.dataset.previewBound){frame.dataset.previewBound='1';frame.addEventListener('load',()=>postPreviewGoto(frame,Number(frame.dataset.slide)));}
if(!frame.getAttribute('src'))frame.src=frameUrl('preview',i);else postPreviewGoto(frame,i);
}
function plannedMsAt(i){
const minutes=Number(noteAt(i).minutes);
return Number.isFinite(minutes)&&minutes>0?minutes*60000:null;
}
function totalPlannedMs(){
const values=Array.from({length:total},(_,i)=>plannedMsAt(i));
return values.length&&values.every(Number.isFinite)?values.reduce((sum,x)=>sum+x,0):null;
}
function sectionInfo(i){
const name=String(noteAt(i).section||'').trim();
if(!name)return null;
let first=i,last=i;
while(first>0&&String(noteAt(first-1).section||'').trim()===name)first--;
while(last<total-1&&String(noteAt(last+1).section||'').trim()===name)last++;
const targets=[];for(let x=first;x<=last;x++)targets.push(plannedMsAt(x));
const planned=targets.every(Number.isFinite)?targets.reduce((sum,x)=>sum+x,0):null;
const used=slideElapsed.slice(first,last+1).reduce((sum,x)=>sum+x,0)+(timerRunning&&timedSlideIndex>=first&&timedSlideIndex<=last?Date.now()-slideTimerStartedAt:0);
return{name,position:i-first+1,count:last-first+1,planned,used};
}
function durationText(ms){
const value=Math.max(0,Math.round(Number(ms)||0)),seconds=Math.floor(value/1000);
const hours=Math.floor(seconds/3600),minutes=Math.floor((seconds%3600)/60),secs=seconds%60;
return hours?`${String(hours).padStart(2,'0')}:${String(minutes).padStart(2,'0')}:${String(secs).padStart(2,'0')}`:`${String(minutes).padStart(2,'0')}:${String(secs).padStart(2,'0')}`;
}
function signedDuration(ms){return `${ms>=0?'+':'−'}${durationText(Math.abs(ms))}`;}
function clockTime(timestamp){return new Date(timestamp).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});}
function fitFrame(frame){
const viewport=frame?.closest('.ppt-frame-viewport');
if(!viewport)return;
const availableWidth=viewport.clientWidth,availableHeight=viewport.clientHeight;
if(!availableWidth||!availableHeight)return;
const width=Math.min(availableWidth,availableHeight*(16/9)),height=width*(9/16);
frame.style.width=`${width}px`;frame.style.height=`${height}px`;
if(frame.id==='ppt-current-frame')sizeInk(document.getElementById('ppt-presenter-ink'),width,height);
}
function fitPresenterFrames(){
fitFrame(document.getElementById('ppt-current-frame'));
fitFrame(document.getElementById('ppt-next-frame'));
}
function sizeInk(canvas,width,height){
if(!canvas||!width||!height)return;
canvas.style.width=`${width}px`;canvas.style.height=`${height}px`;
const dpr=Math.min(devicePixelRatio||1,2),pixelWidth=Math.round(width*dpr),pixelHeight=Math.round(height*dpr);
if(canvas.width!==pixelWidth||canvas.height!==pixelHeight){canvas.width=pixelWidth;canvas.height=pixelHeight;}
}
function drawEllipse(ctx,circle,width,height,alpha=1){
const x1=circle.x1*width,y1=circle.y1*height,x2=circle.x2*width,y2=circle.y2*height;
ctx.save();ctx.globalAlpha=alpha;ctx.strokeStyle='#ff4f59';ctx.lineWidth=Math.max(3,width*.004);ctx.lineCap='round';
ctx.beginPath();ctx.ellipse((x1+x2)/2,(y1+y2)/2,Math.abs(x2-x1)/2,Math.abs(y2-y1)/2,0,0,Math.PI*2);ctx.stroke();ctx.restore();
}
function drawInk(canvas,list,laser,draft){
if(!canvas)return;const width=canvas.clientWidth,height=canvas.clientHeight;if(!width||!height)return;
sizeInk(canvas,width,height);const dpr=canvas.width/width,ctx=canvas.getContext('2d');ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,width,height);
(list||[]).forEach(circle=>drawEllipse(ctx,circle,width,height));
if(draft)drawEllipse(ctx,draft,width,height,.72);
if(laser){
const x=laser.x*width,y=laser.y*height,r=Math.max(6,width*.007);
const glow=ctx.createRadialGradient(x,y,0,x,y,r*2.8);glow.addColorStop(0,'rgba(255,255,255,.98)');glow.addColorStop(.2,'rgba(255,64,72,.98)');glow.addColorStop(1,'rgba(255,64,72,0)');
ctx.fillStyle=glow;ctx.beginPath();ctx.arc(x,y,r*2.8,0,Math.PI*2);ctx.fill();
}
}
function renderInks(){
const now=Date.now();if(now>laserHideAt)laserPoint=null;if(now>audienceLaserHideAt)audienceLaser=null;
drawInk(document.getElementById('ppt-presenter-ink'),circles,laserPoint,draftCircle);
drawInk(document.getElementById('ppt-audience-ink'),audienceCircles,audienceLaser,null);
}
function setAnnotationTool(tool){
annotationTool=annotationTool===tool?'none':tool;annotationPointerDown=false;draftCircle=null;
const canvas=document.getElementById('ppt-presenter-ink');canvas?.classList.toggle('is-active',annotationTool!=='none');
document.querySelectorAll('[data-ppt-tool]').forEach(button=>button.classList.toggle('is-active',button.dataset.pptTool===annotationTool));
renderInks();
}
function annotationPoint(event){
const canvas=event.currentTarget,rect=canvas.getBoundingClientRect();
return{x:Math.max(0,Math.min(1,(event.clientX-rect.left)/rect.width)),y:Math.max(0,Math.min(1,(event.clientY-rect.top)/rect.height))};
}
function onAnnotationPointerDown(event){
if(annotationTool!=='circle')return;event.preventDefault();annotationPointerDown=true;annotationStart=annotationPoint(event);draftCircle={x1:annotationStart.x,y1:annotationStart.y,x2:annotationStart.x,y2:annotationStart.y};event.currentTarget.setPointerCapture?.(event.pointerId);renderInks();
}
function onAnnotationPointerMove(event){
if(annotationTool==='laser'){
const point=annotationPoint(event);laserPoint=point;laserHideAt=Date.now()+650;
if(Date.now()-lastLaserSent>32){lastLaserSent=Date.now();emit({type:'laser',index:idx,point},{persist:false});}renderInks();return;
}
if(annotationTool==='circle'&&annotationPointerDown&&draftCircle){const point=annotationPoint(event);draftCircle.x2=point.x;draftCircle.y2=point.y;renderInks();}
}
function onAnnotationPointerUp(event){
if(annotationTool!=='circle'||!annotationPointerDown||!draftCircle)return;event.preventDefault();annotationPointerDown=false;
const circle={...draftCircle};draftCircle=null;
if(Math.abs(circle.x2-circle.x1)>.012&&Math.abs(circle.y2-circle.y1)>.012){circles.push(circle);emit({type:'circle',index:idx,circle});}
renderInks();
}
function onAnnotationPointerLeave(){
if(annotationTool==='laser'){laserPoint=null;emit({type:'laser',index:idx,point:null},{persist:false});renderInks();}
}
function clearAnnotations(broadcast=true){circles=[];draftCircle=null;laserPoint=null;if(broadcast)emit({type:'clear',index:idx});renderInks();}
function buildPresenterOverview(){
const grid=document.getElementById('ppt-presenter-overview-grid');if(!grid)return;grid.innerHTML='';
slides.forEach((slide,i)=>{
const n=noteAt(i),button=document.createElement('button');button.type='button';button.className=`ppt-overview-page${i===idx?' is-current':''}`;
button.innerHTML=`<span class="ppt-overview-page-no">${String(i+1).padStart(2,'0')} / ${String(total).padStart(2,'0')}</span><span class="ppt-overview-page-title">${escapeHTML(n.title||slideId(i))}</span><span class="ppt-overview-page-meta"><span>${escapeHTML(n.section||'未分组')}</span><span>${Math.round(((i+1)/total)*100)}%</span></span>`;
button.onclick=()=>{togglePresenterOverview(false);go(i,{force:true});};grid.appendChild(button);
});
requestAnimationFrame(()=>grid.querySelector('.is-current')?.scrollIntoView({block:'nearest',inline:'nearest'}));
}
function togglePresenterOverview(force){
presenterOverviewOn=typeof force==='boolean'?force:!presenterOverviewOn;
if(presenterOverviewOn&&annotationTool!=='none')setAnnotationTool('none');
const stack=document.querySelector('.ppt-preview-stack'),overview=document.getElementById('ppt-presenter-overview');
stack?.classList.toggle('is-overview',presenterOverviewOn);if(overview)overview.hidden=!presenterOverviewOn;
const button=document.getElementById('ppt-grid');if(button){button.classList.toggle('is-active',presenterOverviewOn);button.setAttribute('aria-pressed',String(presenterOverviewOn));}
if(presenterOverviewOn)buildPresenterOverview();else requestAnimationFrame(fitPresenterFrames);renderAutoState();
}
function mountShell(){
if(document.getElementById('ppt-presenter'))return;
document.body.insertAdjacentHTML('beforeend',`<div id="ppt-presenter" data-layout="${layoutMode}" aria-label="演讲者视图">
<div class="ppt-presenter-top"><div><div class="ppt-presenter-brand">演讲者视图</div><div class="ppt-presenter-meta"><span id="ppt-plan"></span><span id="ppt-section" hidden></span></div></div><div class="ppt-presenter-meta"><span id="ppt-sync" class="ppt-sync" data-state="disconnected" title="表示观众页面的软件同步状态,不代表 HDMI 等物理线缆状态">观众屏 · 未连接</span><button id="ppt-auto-toggle" class="ppt-ui-btn ppt-auto-state" type="button">自动翻页 · 关闭</button><button id="ppt-preflight" class="ppt-ui-btn" type="button">检查</button><button id="ppt-settings" class="ppt-ui-btn" type="button">设置</button><button id="ppt-reopen" class="ppt-ui-btn" type="button">重新打开观众屏</button><button id="ppt-exit" class="ppt-ui-btn" type="button">退出</button></div></div>
<div class="ppt-presenter-main"><div class="ppt-preview-stack"><div class="ppt-stage-card"><div class="ppt-stage-label"><span>当前页</span><span class="ppt-annotation-tools"><button id="ppt-grid" class="ppt-ui-btn ppt-grid-btn" type="button" aria-pressed="false" title="宫格选页(Esc)"><span class="ppt-grid-icon" aria-hidden="true"><i></i><i></i><i></i><i></i></span>宫格</button><button class="ppt-ui-btn" data-ppt-tool="laser" type="button" title="激光笔(L)">激光</button><button class="ppt-ui-btn" data-ppt-tool="circle" type="button" title="圈选(C)">圈选</button><button id="ppt-ink-clear" class="ppt-ui-btn ppt-clear-btn" type="button" title="清除标记(X)">清除</button></span><span id="ppt-current-label"></span></div><div class="ppt-frame-viewport"><iframe id="ppt-current-frame" class="ppt-frame" title="当前页预览"></iframe><canvas id="ppt-presenter-ink" class="ppt-annotation-layer" aria-label="演讲标注层"></canvas></div></div><div class="ppt-stage-card" id="ppt-next-card"><div class="ppt-stage-label"><span>下一页</span><span id="ppt-next-label"></span></div><div class="ppt-frame-viewport"><iframe id="ppt-next-frame" class="ppt-frame ppt-next-frame" title="下一页预览"></iframe></div></div><div id="ppt-presenter-overview" class="ppt-presenter-overview" hidden><div class="ppt-overview-head"><strong>选择页面</strong><button id="ppt-overview-close" class="ppt-ui-btn" type="button">返回预览</button></div><div id="ppt-presenter-overview-grid" class="ppt-overview-grid"></div></div></div>
<div class="ppt-notes-card"><div class="ppt-notes-head"><div class="ppt-notes-label">标题</div><div id="ppt-notes-title" class="ppt-notes-title"></div><div class="ppt-purpose-block"><div class="ppt-notes-label">本页目的</div><div id="ppt-notes-purpose" class="ppt-purpose-copy"></div></div><div class="ppt-notes-draft-head"><div><div class="ppt-notes-label">草稿(备注)</div><div id="ppt-note-meta" class="ppt-note-meta"></div></div><span class="ppt-notes-tools"><span id="ppt-saved" class="ppt-saved">已保存</span><button id="ppt-note-smaller" class="ppt-ui-btn" type="button" title="缩小备注字号">A−</button><button id="ppt-note-larger" class="ppt-ui-btn" type="button" title="放大备注字号">A+</button></span></div></div><textarea id="ppt-note-editor" class="ppt-note-editor" spellcheck="false" aria-label="本页演讲备注"></textarea></div></div>
<div class="ppt-presenter-bottom"><div class="ppt-time-dashboard"><div class="ppt-time-stat"><span class="ppt-time-label">已进行</span><strong id="ppt-clock" class="ppt-time-value">00:00</strong><span class="ppt-time-hint">总时长</span></div><div class="ppt-time-stat"><span class="ppt-time-label">本页</span><strong id="ppt-slide-clock" class="ppt-time-value">00:00</strong><span id="ppt-slide-plan" class="ppt-time-hint">计划 —</span></div><div id="ppt-slide-status-box" class="ppt-time-stat"><span id="ppt-slide-status-label" class="ppt-time-label">计划</span><strong id="ppt-slide-status" class="ppt-time-value">—</strong><span id="ppt-slide-status-hint" class="ppt-time-hint">未设置</span></div></div><div class="ppt-control-stack"><div class="ppt-controls"><button id="ppt-first" class="ppt-ui-btn" type="button">首页</button><button id="ppt-prev" class="ppt-ui-btn" type="button">上一页</button><button id="ppt-next" class="ppt-ui-btn" type="button">下一页</button><button id="ppt-last" class="ppt-ui-btn" type="button">尾页</button></div><div class="ppt-session-actions"><button id="ppt-timer-toggle" class="ppt-ui-btn" type="button">开始计时</button><button id="ppt-timer-reset" class="ppt-ui-btn" type="button">重置计时</button><button id="ppt-rehearsal" class="ppt-ui-btn" type="button">排练</button></div></div><div class="ppt-page-state"><strong id="ppt-page-count"></strong><span id="ppt-page-percent"></span></div></div>
<div id="ppt-dialog" class="ppt-dialog" hidden><div class="ppt-dialog-panel" role="dialog" aria-modal="true"><div class="ppt-dialog-head"><div id="ppt-dialog-title" class="ppt-dialog-title"></div><button id="ppt-dialog-close" class="ppt-ui-btn" type="button">关闭</button></div><div id="ppt-dialog-body" class="ppt-dialog-body"></div></div></div>
</div>`);
document.getElementById('ppt-reopen').onclick=reopenAudience;
document.getElementById('ppt-exit').onclick=exitPresenter;
document.getElementById('ppt-preflight').onclick=openPreflight;
document.getElementById('ppt-settings').onclick=openSettings;
document.getElementById('ppt-first').onclick=()=>go(0,{force:true});
document.getElementById('ppt-prev').onclick=()=>go(idx-1,{force:true});
document.getElementById('ppt-grid').onclick=()=>togglePresenterOverview();
document.getElementById('ppt-overview-close').onclick=()=>togglePresenterOverview(false);
document.getElementById('ppt-next').onclick=()=>go(idx+1,{force:true});
document.getElementById('ppt-last').onclick=()=>go(idx===total-1?0:total-1,{force:true});
document.getElementById('ppt-timer-toggle').onclick=toggleTimer;
document.getElementById('ppt-timer-reset').onclick=resetTimer;
document.getElementById('ppt-rehearsal').onclick=toggleRehearsal;
document.getElementById('ppt-auto-toggle').onclick=toggleAutoAdvance;
document.getElementById('ppt-note-smaller').onclick=()=>setNoteSize(noteSize-1);
document.getElementById('ppt-note-larger').onclick=()=>setNoteSize(noteSize+1);
document.getElementById('ppt-note-editor').addEventListener('input',saveNote);
document.querySelectorAll('[data-ppt-tool]').forEach(button=>button.onclick=()=>setAnnotationTool(button.dataset.pptTool));
document.getElementById('ppt-ink-clear').onclick=()=>clearAnnotations(true);
const ink=document.getElementById('ppt-presenter-ink');
ink.addEventListener('pointerdown',onAnnotationPointerDown);ink.addEventListener('pointermove',onAnnotationPointerMove);ink.addEventListener('pointerup',onAnnotationPointerUp);ink.addEventListener('pointercancel',onAnnotationPointerUp);ink.addEventListener('pointerleave',onAnnotationPointerLeave);
document.getElementById('ppt-dialog-close').onclick=closeDialog;
document.getElementById('ppt-dialog').onclick=event=>{if(event.target.id==='ppt-dialog')closeDialog();};
if('ResizeObserver'in window){presenterFrameObserver=new ResizeObserver(()=>requestAnimationFrame(fitPresenterFrames));presenterFrameObserver.observe(document.querySelector('.ppt-preview-stack'));}
else addEventListener('resize',fitPresenterFrames);
setNoteSize(noteSize,false);applyLayout(layoutMode,false);requestAnimationFrame(fitPresenterFrames);
}
function updateShell(){
if(mode!=='presenter')return;mountShell();
const n=noteAt(idx),next=Math.min(idx+1,total-1),id=slideId(idx),editor=document.getElementById('ppt-note-editor');
const currentFrame=document.getElementById('ppt-current-frame'),nextFrame=document.getElementById('ppt-next-frame');
if(currentFrame.dataset.slide!==String(idx))updatePreviewFrame(currentFrame,idx);
if(nextFrame.dataset.slide!==String(next))updatePreviewFrame(nextFrame,next);
document.getElementById('ppt-next-card').classList.toggle('is-end',idx===total-1);
document.getElementById('ppt-current-label').textContent=`${idx+1} / ${total}`;
document.getElementById('ppt-next-label').textContent=idx===total-1?'已到尾页':`${next+1} / ${total}`;
document.getElementById('ppt-notes-title').textContent=n.title||`第 ${idx+1} 页`;
document.getElementById('ppt-notes-purpose').textContent=n.purpose||'—';
const target=plannedMsAt(idx);document.getElementById('ppt-note-meta').textContent=`建议 ${target?(target/60000).toFixed(1):'—'} 分钟 · ${id}`;
document.getElementById('ppt-page-count').textContent=`第 ${idx+1} / ${total} 页`;
document.getElementById('ppt-page-percent').textContent=`${Math.round(((idx+1)/total)*100)}%`;
document.getElementById('ppt-first').disabled=idx===0;document.getElementById('ppt-prev').disabled=idx===0;document.getElementById('ppt-next').disabled=idx===total-1;
document.getElementById('ppt-last').textContent=idx===total-1?'重新开始':'尾页';
if(renderedNoteId!==id){renderedNoteId=id;editor.value=loadNoteValue(id,idx,n);editor.scrollTop=0;document.getElementById('ppt-saved').textContent='已保存';}
if(presenterOverviewOn)buildPresenterOverview();requestAnimationFrame(fitPresenterFrames);renderTimer();updateSyncStatus();
}
function saveNote(){
const saved=document.getElementById('ppt-saved');saved.textContent='保存中…';saved.classList.remove('ppt-saved');clearTimeout(saveTimer);
saveTimer=setTimeout(()=>{localStorage.setItem(noteStorageKey(renderedNoteId),document.getElementById('ppt-note-editor').value);saved.textContent='已保存';saved.classList.add('ppt-saved');},300);
}
function setNoteSize(value,persist=true){noteSize=Math.max(14,Math.min(26,value));document.documentElement.style.setProperty('--ppt-note-size',`${noteSize}px`);if(persist)localStorage.setItem('guizang-ppt-note-size',String(noteSize));}
function currentElapsed(){return elapsed+(timerRunning?Date.now()-timerStartedAt:0);}
function currentSlideElapsed(i=idx){return slideElapsed[i]+(timerRunning&&timedSlideIndex===i?Date.now()-slideTimerStartedAt:0);}
function commitRunningTime(now=Date.now()){
if(!timerRunning)return;elapsed+=now-timerStartedAt;slideElapsed[timedSlideIndex]+=now-slideTimerStartedAt;timerStartedAt=now;slideTimerStartedAt=now;
}
function rollSlideTimer(nextIndex){if(timerRunning)commitRunningTime();timedSlideIndex=nextIndex;slideTimerStartedAt=Date.now();}
function renderTimer(){
const totalUsed=currentElapsed(),pageUsed=currentSlideElapsed(),target=plannedMsAt(idx),clock=document.getElementById('ppt-clock');
if(clock)clock.textContent=durationText(totalUsed);
const slideClock=document.getElementById('ppt-slide-clock'),slidePlan=document.getElementById('ppt-slide-plan'),statusBox=document.getElementById('ppt-slide-status-box'),statusLabel=document.getElementById('ppt-slide-status-label'),statusValue=document.getElementById('ppt-slide-status'),statusHint=document.getElementById('ppt-slide-status-hint');
if(slideClock)slideClock.textContent=durationText(pageUsed);if(slidePlan)slidePlan.textContent=target?`计划 ${durationText(target)}`:'计划 —';
if(statusBox&&statusLabel&&statusValue&&statusHint){
const over=Boolean(target&&pageUsed>target),remaining=Boolean(target&&!over);statusBox.classList.toggle('is-over',over);statusBox.classList.toggle('is-remaining',remaining);
statusLabel.textContent=over?'超时':(remaining?'剩余':'计划');statusValue.textContent=target?durationText(Math.abs(target-pageUsed)):'—';statusHint.textContent=over?'已超出本页计划':(remaining?'距本页计划':'未设置');
}
const totalPlan=totalPlannedMs(),plan=document.getElementById('ppt-plan');
if(plan)plan.textContent=totalPlan?`计划 ${Number(totalPlan/60000).toFixed(1)} 分钟 · 预计 ${clockTime(Date.now()+Math.max(0,totalPlan-totalUsed))}`:'计划 —';
const section=sectionInfo(idx),sectionEl=document.getElementById('ppt-section');
if(sectionEl){sectionEl.hidden=!section;sectionEl.textContent=section?`${section.name} · ${section.position}/${section.count}${section.planned?` · 剩余 ${durationText(Math.max(0,section.planned-section.used))}`:''}`:'';}
const timerButton=document.getElementById('ppt-timer-toggle');if(timerButton)timerButton.textContent=timerRunning?'暂停':(totalUsed?'继续计时':'开始计时');
const rehearsalButton=document.getElementById('ppt-rehearsal');if(rehearsalButton){rehearsalButton.textContent=rehearsalActive?'结束排练':'排练';rehearsalButton.classList.toggle('ppt-rehearsal-on',rehearsalActive);}
renderAutoState();
}
function toggleTimer(){const now=Date.now();if(timerRunning){commitRunningTime(now);timerRunning=false;}else{timerStartedAt=now;slideTimerStartedAt=now;timedSlideIndex=idx;timerRunning=true;}renderTimer();}
function resetTimer(){
elapsed=0;slideElapsed=Array(total).fill(0);timedSlideIndex=idx;
if(timerRunning){timerStartedAt=Date.now();slideTimerStartedAt=timerStartedAt;}else{timerStartedAt=0;slideTimerStartedAt=0;}
renderTimer();
}
function rehearsalHistory(){try{return JSON.parse(localStorage.getItem(storageKey('rehearsals'))||'[]');}catch(e){return[];}}
function toggleRehearsal(){
if(rehearsalActive){finishRehearsal(true);return;}
rehearsalActive=true;if(timerRunning){commitRunningTime();timerRunning=false;}resetTimer();toggleTimer();renderTimer();
}
function finishRehearsal(showSummary){
if(!rehearsalActive)return;if(timerRunning){commitRunningTime();timerRunning=false;}
const result={endedAt:Date.now(),totalMs:elapsed,slides:slideElapsed.map((ms,i)=>({index:i,id:slideId(i),title:noteAt(i).title||slideId(i),ms,targetMs:plannedMsAt(i)}))};
const history=rehearsalHistory();history.unshift(result);try{localStorage.setItem(storageKey('rehearsals'),JSON.stringify(history.slice(0,5)));}catch(e){}
rehearsalActive=false;renderTimer();if(showSummary)openRehearsalSummary(result,history.slice(0,5));
}
function openRehearsalSummary(result,history){
const rows=result.slides.filter(item=>item.ms>0).map(item=>`<tr><td>${item.index+1}</td><td>${escapeHTML(item.title)}</td><td>${durationText(item.ms)}</td><td>${item.targetMs?signedDuration(item.ms-item.targetMs):'—'}</td></tr>`).join('');
const recent=history.map(item=>`<div class="ppt-dialog-row"><span>${new Date(item.endedAt).toLocaleString()}</span><strong>${durationText(item.totalMs)}</strong></div>`).join('');
openDialog('排练记录',`<div class="ppt-dialog-section"><div class="ppt-dialog-row"><span>本次总时长</span><strong>${durationText(result.totalMs)}</strong></div><table class="ppt-summary"><thead><tr><th>#</th><th>页面</th><th>实际</th><th>与计划相比</th></tr></thead><tbody>${rows||'<tr><td colspan="4">没有记录到页面时间</td></tr>'}</tbody></table></div><div class="ppt-dialog-section"><h3>最近排练</h3>${recent}</div>`);
}
function autoDurationMs(i=idx){const explicit=Number(noteAt(i).autoAdvanceSeconds);const seconds=Number.isFinite(explicit)&&explicit>0?explicit:autoGlobalSeconds;return seconds>0?seconds*1000:null;}
function armAutoAdvance(){autoDeadline=0;autoRemainingMs=autoDurationMs()||0;}
function autoBlockedReason(){
if(presenterOverviewOn||overviewOn)return'宫格选页中';if(annotationTool!=='none')return'标注中';if(screenMode!=='normal')return'观众屏已暂停';if(document.hidden)return'窗口不可见';
const dialog=document.getElementById('ppt-dialog');if(dialog&&!dialog.hidden)return'设置中';
if(audienceOpenedAt&&syncState()!=='synced')return'等待观众屏同步';return'';
}
function updateAutoAdvance(){
if(!autoEnabled)return;
const duration=autoDurationMs();if(!duration||idx===total-1)return;
const blocked=autoBlockedReason(),now=Date.now();
if(blocked){if(autoDeadline){autoRemainingMs=Math.max(0,autoDeadline-now);autoDeadline=0;}return;}
if(!autoDeadline)autoDeadline=now+(autoRemainingMs||duration);
autoRemainingMs=Math.max(0,autoDeadline-now);
if(now>=autoDeadline){autoDeadline=0;autoRemainingMs=0;go(idx+1,{force:true});}
}
function renderAutoState(){
const button=document.getElementById('ppt-auto-toggle');if(!button)return;
const duration=autoDurationMs(),blocked=autoEnabled?autoBlockedReason():'';
button.classList.toggle('is-on',autoEnabled&&!blocked);button.classList.toggle('is-paused',autoEnabled&&Boolean(blocked));
if(!autoEnabled)button.textContent='自动翻页 · 关闭';
else if(!duration)button.textContent='自动翻页 · 等待时长';
else if(idx===total-1)button.textContent='自动翻页 · 已结束';
else if(blocked)button.textContent=`自动翻页 · ${blocked}`;
else button.textContent=`自动翻页 · ${durationText(autoRemainingMs||duration)}`;
}
function toggleAutoAdvance(){
if(!autoEnabled&&!autoDurationMs()){openSettings();return;}
autoEnabled=!autoEnabled;localStorage.setItem(storageKey('auto-enabled'),autoEnabled?'1':'0');armAutoAdvance();renderAutoState();
}
function openDialog(title,html){
const dialog=document.getElementById('ppt-dialog');if(!dialog)return;
document.getElementById('ppt-dialog-title').textContent=title;document.getElementById('ppt-dialog-body').innerHTML=html;dialog.hidden=false;renderAutoState();
}
function closeDialog(){const dialog=document.getElementById('ppt-dialog');if(dialog)dialog.hidden=true;renderAutoState();}
function applyLayout(value,persist=true){
layoutMode=['balanced','slides','notes'].includes(value)?value:'balanced';
document.getElementById('ppt-presenter')?.setAttribute('data-layout',layoutMode);
if(persist)localStorage.setItem(storageKey('layout'),layoutMode);requestAnimationFrame(fitPresenterFrames);
}
function applyAudienceScreen(value){
const cover=document.getElementById('ppt-audience-cover');if(!cover)return;
cover.classList.remove('is-ended');cover.textContent='';
if(value==='black'||value==='white'){cover.style.display='block';cover.style.background=value;}else cover.style.display='none';
}
function showAudienceEnded(){
const cover=document.getElementById('ppt-audience-cover');if(cover){cover.style.display='';cover.style.background='';cover.textContent='演示已结束';cover.classList.add('is-ended');}
setTimeout(()=>{try{window.close();}catch(e){}},120);
}
function setScreenMode(value){
const next=['normal','black','white','frozen'].includes(value)?value:'normal',wasFrozen=audienceFrozen;
screenMode=next;audienceFrozen=next==='frozen';emit({type:'screen',mode:next});
if(wasFrozen&&!audienceFrozen)sendGoto(true);updateSyncStatus();armAutoAdvance();renderAutoState();
}
function openSettings(){
openDialog('演示设置',`<div class="ppt-settings-card"><label class="ppt-switch-row" for="ppt-auto-enabled-setting"><span><strong>自动翻页</strong><br><span class="ppt-muted">需要时手动开启,默认关闭</span></span><input id="ppt-auto-enabled-setting" type="checkbox" ${autoEnabled?'checked':''}><span class="ppt-switch" aria-hidden="true"></span></label><div class="ppt-field"><label for="ppt-auto-seconds-setting">全局间隔</label><div class="ppt-stepper"><button id="ppt-auto-minus" type="button" aria-label="减少一秒">−</button><input id="ppt-auto-seconds-setting" type="number" min="1" step="1" value="${autoGlobalSeconds||''}" placeholder="—"><span class="ppt-stepper-unit">秒</span><button id="ppt-auto-plus" type="button" aria-label="增加一秒">+</button></div></div><button id="ppt-settings-save" class="ppt-ui-btn ppt-settings-save" type="button">保存自动翻页</button><p class="ppt-settings-copy">单页 autoAdvanceSeconds 优先于全局间隔;minutes 只是讲述计划。</p></div><div class="ppt-settings-card"><h3>演讲者布局</h3><p class="ppt-settings-copy">根据屏幕宽度调整页面与备注的比例</p><div class="ppt-segmented"><button class="ppt-ui-btn" data-layout-choice="balanced" type="button">均衡</button><button class="ppt-ui-btn" data-layout-choice="slides" type="button">页面优先</button><button class="ppt-ui-btn" data-layout-choice="notes" type="button">备注优先</button></div></div><div class="ppt-settings-card"><h3>观众屏</h3><p class="ppt-settings-copy">临时遮挡或冻结观众屏</p><div class="ppt-segmented screen"><button class="ppt-ui-btn" data-screen-choice="normal" type="button">正常</button><button class="ppt-ui-btn" data-screen-choice="black" type="button">黑屏 B</button><button class="ppt-ui-btn" data-screen-choice="white" type="button">白屏 W</button><button class="ppt-ui-btn" data-screen-choice="frozen" type="button">冻结 F</button></div></div><div class="ppt-dialog-section"><button id="ppt-open-shortcuts" class="ppt-ui-btn" type="button">查看快捷键</button></div>`);
document.querySelectorAll('[data-layout-choice]').forEach(button=>{button.classList.toggle('ppt-primary',button.dataset.layoutChoice===layoutMode);button.onclick=()=>{applyLayout(button.dataset.layoutChoice);openSettings();};});
document.querySelectorAll('[data-screen-choice]').forEach(button=>{button.classList.toggle('ppt-primary',button.dataset.screenChoice===screenMode);button.onclick=()=>{setScreenMode(button.dataset.screenChoice);openSettings();};});
const secondsInput=document.getElementById('ppt-auto-seconds-setting'),stepSeconds=delta=>{const current=Math.max(0,Number(secondsInput.value||0));secondsInput.value=String(Math.max(1,current+delta));};
document.getElementById('ppt-auto-minus').onclick=()=>stepSeconds(-1);document.getElementById('ppt-auto-plus').onclick=()=>stepSeconds(1);
document.getElementById('ppt-settings-save').onclick=()=>{autoEnabled=document.getElementById('ppt-auto-enabled-setting').checked;autoGlobalSeconds=Math.max(0,Number(secondsInput.value||0));localStorage.setItem(storageKey('auto-enabled'),autoEnabled?'1':'0');localStorage.setItem(storageKey('auto-seconds'),String(autoGlobalSeconds));armAutoAdvance();closeDialog();};
document.getElementById('ppt-open-shortcuts').onclick=openShortcuts;
}
function openShortcuts(){
const items=[['← / →','上一页 / 下一页'],['Home / End','首页 / 尾页'],['Esc','宫格选页;标注工具激活时先退出工具'],['L','激光笔'],['C','圈选'],['X','清除标记'],['B / W','观众屏黑屏 / 白屏'],['F','冻结 / 恢复观众屏'],['A','开启 / 关闭自动翻页'],['R','开始 / 结束排练'],['?','快捷键说明']];
openDialog('快捷键',`<div class="ppt-shortcuts">${items.map(([key,label])=>`<kbd>${key}</kbd><span>${label}</span>`).join('')}</div>`);
}
function preflightChecks(){
const images=[...document.images],videos=[...document.querySelectorAll('video')],frame=document.getElementById('ppt-current-frame'),rect=frame?.getBoundingClientRect();
return[
{label:'观众屏连接',ok:['synced','frozen'].includes(syncState()),detail:`当前状态:${document.getElementById('ppt-sync')?.textContent||'未连接'}`},
{label:'弹窗权限',ok:!popupBlocked,detail:popupBlocked?'浏览器阻止了观众窗口':'未检测到弹窗拦截'},
{label:'全屏状态',ok:Boolean(document.fullscreenElement),warn:true,detail:document.fullscreenElement?'已进入全屏':'尚未全屏,可在演讲前进入'},
{label:'字体加载',ok:!document.fonts||document.fonts.status==='loaded',detail:document.fonts?`状态:${document.fonts.status}`:'浏览器不提供字体状态'},
{label:'图片资源',ok:images.every(image=>image.complete&&image.naturalWidth>0),detail:`${images.filter(image=>image.complete&&image.naturalWidth>0).length} / ${images.length} 已加载`},
{label:'视频资源',ok:videos.every(video=>video.readyState>=2),detail:videos.length?`${videos.filter(video=>video.readyState>=2).length} / ${videos.length} 可播放`:'没有视频'},
{label:'当前页 16:9',ok:Boolean(rect&&Math.abs(rect.width/rect.height-16/9)<.003),detail:rect?`${Math.round(rect.width)} × ${Math.round(rect.height)}`:'尚未渲染'}
];
}
function openPreflight(){
const checks=preflightChecks(),html=checks.map(item=>`<div class="ppt-dialog-row"><div class="ppt-check ${item.ok?'is-ok':(item.warn?'is-warn':'')}"><span class="ppt-check-dot"></span><span><strong>${item.label}</strong><br><span class="ppt-muted">${item.detail}</span></span></div><strong>${item.ok?'正常':(item.warn?'建议':'异常')}</strong></div>`).join('');
openDialog('演前检查',`<div class="ppt-dialog-section">${html}</div><div class="ppt-dialog-section"><button id="ppt-preflight-refresh" class="ppt-ui-btn" type="button">重新检查</button> <button id="ppt-enter-fullscreen" class="ppt-ui-btn" type="button">进入全屏</button></div><p class="ppt-muted">这里只能检查浏览器和观众页面的软件状态,不能确认 HDMI、转接器或投影仪线缆是否物理连接。</p>`);
document.getElementById('ppt-preflight-refresh').onclick=openPreflight;
document.getElementById('ppt-enter-fullscreen').onclick=async()=>{try{await document.documentElement.requestFullscreen();}catch(e){}openPreflight();};
}
function initTransport(){
if(transportReady)return;transportReady=true;
if('BroadcastChannel'in window){try{channel=new BroadcastChannel(channelName());channel.onmessage=e=>receive(e.data);}catch(e){}}
addEventListener('message',e=>receive(e.data));
addEventListener('storage',e=>{if(e.key===syncKey()&&e.newValue){try{receive(JSON.parse(e.newValue));}catch(err){}}});
}
function emit(payload,{persist=true}={}){
const message={__guizangPptSync:1,session,role:mode,...payload,at:Date.now(),nonce:Math.random().toString(36).slice(2)};
try{channel?.postMessage(message);}catch(e){}
if(persist)try{localStorage.setItem(syncKey(),JSON.stringify(message));}catch(e){}
try{if(mode==='presenter'&&audienceWindow&&!audienceWindow.closed)audienceWindow.postMessage(message,'*');}catch(e){}
try{if(mode==='audience'&&window.opener&&!window.opener.closed)window.opener.postMessage(message,'*');}catch(e){}
}
function receive(message){
if(!message||message.__guizangPptSync!==1||message.session!==session||message.role===mode)return;
if(message.nonce){if(seenNonceSet.has(message.nonce))return;seenNonceSet.add(message.nonce);seenNonces.push(message.nonce);if(seenNonces.length>256)seenNonceSet.delete(seenNonces.shift());}
if(mode==='presenter'){
if(message.type==='ready'){popupBlocked=false;sendGoto(true);}
if(message.type==='ack'){lastAckAt=Date.now();lastAckIndex=Number(message.index);lastAckSeq=Number(message.seq);updateSyncStatus();}
return;
}
if(mode!=='audience')return;
if(message.type==='bye'){showAudienceEnded();return;}
if(message.type==='screen'){applyAudienceScreen(message.mode);return;}
if(message.type==='laser'&&Number(message.index)===idx){audienceLaser=message.point||null;audienceLaserHideAt=Date.now()+650;renderInks();return;}
if(message.type==='circle'&&Number(message.index)===idx&&message.circle){audienceCircles.push(message.circle);renderInks();return;}
if(message.type==='clear'&&Number(message.index)===idx){audienceCircles=[];renderInks();return;}
if(message.type==='goto'||message.type==='ping'){
if(message.type==='goto'){audienceCircles=Array.isArray(message.circles)?message.circles:[];applyAudienceScreen(message.screenMode);go(Number(message.index),{force:true});}
emit({type:'ack',index:idx,seq:Number(message.seq)});
}
}
function receivePreview(message){
if(!message||message.__guizangPptSync!==1||message.session!==session||message.role!=='presenter'||message.type!=='preview-goto')return;
go(Number(message.index),{force:true});
}
function sendGoto(force=false){
if(mode!=='presenter'||audienceFrozen)return;if(force||lastAckIndex!==idx)seq++;
emit({type:'goto',index:idx,seq,screenMode,circles});updateSyncStatus();
}
function syncState(){
if(popupBlocked)return'blocked';if(audienceWindow&&audienceWindow.closed)return'disconnected';if(audienceFrozen)return'frozen';
if(!lastAckAt)return audienceOpenedAt&&Date.now()-audienceOpenedAt<2600?'connecting':'disconnected';
if(Date.now()-lastAckAt>2600)return'disconnected';if(lastAckIndex!==idx||lastAckSeq<seq)return'unsynced';return'synced';
}
function updateSyncStatus(){
const el=document.getElementById('ppt-sync');if(!el)return;const state=syncState();
const labels={blocked:'弹窗被拦截',disconnected:'未连接',connecting:'连接中',unsynced:'未同步',frozen:'已冻结',synced:`已同步 · 第 ${idx+1} 页`};
el.dataset.state=state;el.textContent=`观众屏 · ${labels[state]}`;
}
function reopenAudience(){
initTransport();popupBlocked=false;audienceOpenedAt=Date.now();lastAckAt=0;lastAckIndex=-1;lastAckSeq=-1;
if(audienceFrozen){audienceFrozen=false;screenMode='normal';}
audienceWindow=window.open(frameUrl('audience',idx),`guizang-ppt-audience-${session}`,'popup=yes,width=1440,height=900');
if(!audienceWindow)popupBlocked=true;updateSyncStatus();
}
function enterPresenter(openAudience=true){
if(mode==='presenter')return;if(overviewOn)toggleOverview();lowPowerBeforePresenter=window.__lowPowerMode;mode='presenter';initTransport();mountShell();
document.body.classList.remove('ppt-audience','ppt-preview');document.body.classList.add('ppt-presenter');document.title=`演讲者视图 · ${originalTitle}`;
window.__setLowPowerMode?.(true,{persist:false});updateShell();armAutoAdvance();if(openAudience)reopenAudience();
}
function exitPresenter(){
if(rehearsalActive)finishRehearsal(false);else if(timerRunning){commitRunningTime();timerRunning=false;}togglePresenterOverview(false);emit({type:'bye'});audienceWindow=null;audienceOpenedAt=0;lastAckAt=0;lastAckIndex=-1;lastAckSeq=-1;mode='deck';document.body.classList.remove('ppt-presenter');document.title=originalTitle;window.__setLowPowerMode?.(lowPowerBeforePresenter,{persist:false});
}
function initAudience(){
mode='audience';document.body.classList.add('ppt-audience');document.title=`观众屏 · ${originalTitle}`;
document.body.insertAdjacentHTML('beforeend','<canvas id="ppt-audience-ink" aria-hidden="true"></canvas><div id="ppt-audience-cover" aria-hidden="true"></div>');
const resize=()=>{sizeInk(document.getElementById('ppt-audience-ink'),innerWidth,innerHeight);renderInks();};addEventListener('resize',resize);resize();
initTransport();emit({type:'ready',index:idx});
}
function initPreview(){mode='preview';document.body.classList.add('ppt-preview');window.__setLowPowerMode?.(true,{persist:false});addEventListener('message',e=>{if(e.source===parent)receivePreview(e.data);});}
function onSlideChange(nextIndex){
if(mode!=='presenter')return;rollSlideTimer(nextIndex);circles=[];draftCircle=null;laserPoint=null;annotationPointerDown=false;armAutoAdvance();updateShell();sendGoto();
}
function handleKey(e){
const editable=e.target?.matches?.('input,textarea,[contenteditable="true"]');
if(editable){if(e.key==='Escape')e.target.blur();return true;}
if(mode==='audience'||mode==='preview'){e.preventDefault();return true;}
if(mode==='deck'&&e.key?.toLowerCase()==='p'&&!e.metaKey&&!e.ctrlKey&&!e.altKey){e.preventDefault();enterPresenter(true);return true;}
if(mode!=='presenter')return false;
const key=e.key?.toLowerCase();
const dialog=document.getElementById('ppt-dialog');
if(!dialog?.hidden){if(e.key==='Escape'){e.preventDefault();closeDialog();}return true;}
if(e.key==='?'||key==='/'){e.preventDefault();openShortcuts();return true;}
if(e.key==='Escape'){e.preventDefault();if(annotationTool!=='none')setAnnotationTool('none');else togglePresenterOverview();return true;}
if(presenterOverviewOn)return true;
if(key==='l'){e.preventDefault();setAnnotationTool('laser');return true;}
if(key==='c'){e.preventDefault();setAnnotationTool('circle');return true;}
if(key==='x'){e.preventDefault();clearAnnotations(true);return true;}
if(key==='b'){e.preventDefault();setScreenMode(screenMode==='black'?'normal':'black');return true;}
if(key==='w'){e.preventDefault();setScreenMode(screenMode==='white'?'normal':'white');return true;}
if(key==='f'){e.preventDefault();setScreenMode(audienceFrozen?'normal':'frozen');return true;}
if(key==='a'){e.preventDefault();toggleAutoAdvance();return true;}
if(key==='r'){e.preventDefault();toggleRehearsal();return true;}
if(['ArrowRight','PageDown',' ','ArrowDown'].includes(e.key)){e.preventDefault();go(idx+1,{force:true});return true;}
if(['ArrowLeft','PageUp','ArrowUp'].includes(e.key)){e.preventDefault();go(idx-1,{force:true});return true;}
if(e.key==='Home'){e.preventDefault();go(0,{force:true});return true;}
if(e.key==='End'){e.preventDefault();go(total-1,{force:true});return true;}
return false;
}
function initMode(){
document.getElementById('ppt-low-power-btn')?.addEventListener('click',()=>window.__setLowPowerMode(!window.__lowPowerMode));
document.getElementById('ppt-overview-btn')?.addEventListener('click',toggleOverview);
document.getElementById('ppt-presenter-btn')?.addEventListener('click',()=>enterPresenter(true));
if(requestedMode==='audience')initAudience();else if(requestedMode==='preview')initPreview();else if(requestedMode==='presenter')enterPresenter(false);
}
setInterval(()=>{
renderTimer();renderInks();updateAutoAdvance();
if(mode==='presenter'&&Date.now()-lastPingAt>900){lastPingAt=Date.now();if(audienceWindow?.closed)updateSyncStatus();emit({type:'ping',index:idx,seq});updateSyncStatus();}
},250);
return{initMode,onSlideChange,handleKey,slideLabel,enterPresenter,reopenAudience,toggleOverview:togglePresenterOverview,debugState:()=>({mode,idx,session,syncState:syncState(),timerRunning,rehearsalActive,presenterOverviewOn,autoEnabled,autoGlobalSeconds,autoRemainingMs,screenMode,annotationTool,circles:[...circles],audienceCircles:[...audienceCircles],layoutMode})};
})();
/* =============== /演讲者模式 / 观众屏同步 =============== */
addEventListener('keydown',e=>{
if(window.__pptPresenter?.handleKey(e))return;
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'){
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){
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)){
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);
window.__pptPresenter?.initMode();
</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'});
}
}
if(motion){
const { animate } = motion;
document.body.classList.add('motion-ready');
/* ============================================================
IBM Carbon Motion · 每个 recipe 服务一种表达
不是一刀切的 stagger,而是把动效绑在内容语义上
============================================================ */
/* 缓动从 :root 的 Carbon motion token 读取,CSS 是唯一事实源(硬编码仅作解析失败兜底) */
const cssEase = (name, fallback) => {
const v = getComputedStyle(document.documentElement).getPropertyValue(name);
const m = v && v.match(/cubic-bezier\(([^)]+)\)/);
const nums = m ? m[1].split(',').map(Number) : [];
return (nums.length === 4 && nums.every(Number.isFinite)) ? nums : fallback;
};
const EASE_PROD = cssEase('--ease-prod', [.2, 0, .38, .9]);
const EASE_ENTRY_EXP = cssEase('--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;
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>