Files
2026-08-31 09:21:12 +08:00

2427 lines
120 KiB
HTML
Raw Permalink 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>基于飞书的产物管理工作流 · Swiss Deck</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;font-family:var(--mono);font-size:14px;letter-spacing:.14em;text-transform:uppercase;opacity:.4}
body.dark-bg #hint{color:var(--paper);opacity:.4}
body.low-power #hint{color:var(--accent);opacity:.72}
body.dark-bg.low-power #hint{color:var(--paper);opacity:.72}
/* ESC 索引页: 动画元素强制可见 (覆盖 motion-ready 的 opacity:0) */
#overview [data-anim]{opacity:1!important;transform:none!important}
#overview .slide *{animation:none!important;transition:none!important}
/* 统一卡片样式 token: card-fill (默认灰底 · 中性) + card-ink (反转高对比) + card-accent (单一焦点) */
.card-fill{background:#f5f5f4;border:0;color:var(--text-primary)}
.card-ink{background:var(--ink);border:0;color:var(--paper)}
.card-ink .t-meta,.card-ink .t-cat{color:rgba(255,255,255,.6)}
.card-accent{background:var(--accent);border:0;color:var(--accent-on)}
.card-accent .t-meta,.card-accent .t-cat{color:var(--accent-on)}
/* ============ 动效 (与原模板一致) ============ */
[data-anim]{opacity:1}
body.motion-ready [data-anim]{opacity:0}
body.motion-ready [data-anim="left"]{transform:translateX(-24px)}
body.motion-ready [data-anim="right"]{transform:translateX(24px)}
body.motion-ready [data-anim="line"]{opacity:0;transform:translateY(10px)}
body.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 hint = document.getElementById('hint');
if(hint) hint.textContent = `← → 翻页 · B ${window.__lowPowerMode ? '动态' : '静态'} · ESC 索引`;
}
window.__setLowPowerMode = function(on, opts={}){
window.__lowPowerMode = !!on;
document.body.classList.toggle('low-power', window.__lowPowerMode);
if(opts.persist !== false) localStorage.setItem(KEY, window.__lowPowerMode ? '1' : '0');
if(window.__lowPowerMode && document.getAnimations){
document.getAnimations().forEach(a=>a.cancel());
}
updateHint();
dispatchEvent(new CustomEvent('swiss-low-power-change', {detail:{on:window.__lowPowerMode}}));
if(window.__playSlide) window.__playSlide(window.__currentSlideIndex || 0);
};
document.body.classList.toggle('low-power', window.__lowPowerMode);
addEventListener('DOMContentLoaded', updateHint, {once:true});
})();
</script>
<canvas id="bg-grid" class="bg"></canvas>
<div id="hint">← → 翻页 · B 静态 · ESC 索引</div>
<div id="deck">
<!-- ============================================================
SLIDES 插入区 · 在此处填充所有 <section class="slide ..."> 页面
页面骨架参考 references/layouts-swiss.md
主题色配置参考 references/themes-swiss.md
============================================================ -->
<!-- SLIDES_HERE · 基于文章内容制作的 13 页 Swiss deck -->
<section class="slide accent" data-layout="SWISS-COVER-ASCII" data-animate="hero">
<div class="canvas-card">
<canvas class="ascii-bg" aria-hidden="true"></canvas>
<div class="chrome-min"><div class="l">FEISHU · PRODUCT ARTIFACTS</div><div class="r">SWISS · IKB · 01 / 13</div></div>
<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">FIELD NOTE · 产物管理系统</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/><span style="font-style:italic;font-weight:300">正确的系统</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);font-weight:300">基于飞书的产物管理工作流:从需求澄清、规格、拆票,到实现、验证与收口。</div>
<div style="display:flex;justify-content:space-between;align-items:end"><div class="t-meta" style="color:rgba(255,255,255,.6)">SOURCE · 基于飞书的产物管理工作流.md</div><div class="t-meta" style="color:rgba(255,255,255,.6)">→ swipe / arrow keys</div></div>
</div>
</div>
</div>
</section>
<section class="slide dark hero" data-layout="S03" data-animate="statement">
<div class="canvas-card">
<div class="chrome-min"><div class="l">01 · THE PREMISE</div><div class="r">02 / 13</div></div>
<div style="flex:1;display:flex;flex-direction:column;justify-content:center;gap:4vh">
<div class="t-cat on-dark">THE PREMISE · 先分工,再协作</div>
<h1 style="font-family:var(--sans),var(--sans-zh);font-weight:200;font-size:min(8.6vw,15vh);line-height:.92;letter-spacing:-.045em;max-width:11ch">不是把事情搬进 Base。<br/><span style="font-style:italic;font-weight:300">而是让每个系统只管一段。</span></h1>
<div style="max-width:46ch;border-top:1px solid rgba(255,255,255,.24);padding-top:2vh;color:rgba(255,255,255,.72);font-size:max(18px,1.1vw);line-height:1.55;font-weight:300">工作流的核心不是增加一个工具,而是建立清晰的事实源、状态轴、执行上下文与验证证据。</div>
</div>
</div>
</section>
<section class="slide light hero" data-layout="S17" data-animate="system-diagram">
<div class="canvas-card">
<div class="chrome-min"><div class="l">02 · FACT SOURCES</div><div class="r">03 / 13</div></div>
<div data-anim="line" style="display:flex;flex-direction:column;gap:1.2vh"><div class="t-cat accent">FACT SOURCES · 四个系统,四种职责</div><h2 class="h-xl-zh" style="font-size:min(5.2vw,9.2vh)">同一件工作,不同的事实源</h2></div>
<div data-anim="up" style="flex:1;display:grid;grid-template-columns:5fr 7fr;gap:3vw;align-items:center;margin-top:3vh">
<div style="display:flex;flex-direction:column;gap:2.2vh;max-width:34ch"><p class="t-body" style="font-size:max(18px,1.2vw)">把“长文”“状态”“执行”“实现”分开保存,协作才不会靠记忆和复制粘贴维持。</p><div style="border-left:3px solid var(--accent);padding:1vh 0 1vh 1.2vw;color:var(--text-secondary);font-size:max(16px,1vw);line-height:1.55">规则:Base 管状态和关系;Wiki 管长文;Trellis 管上下文;代码仓库管实现事实。</div></div>
<div style="display:grid;grid-template-rows:1fr auto;gap:2vh;align-items:center">
<div style="position:relative;min-height:35vh;display:flex;align-items:center;justify-content:center">
<svg viewBox="0 0 520 360" style="width:100%;height:35vh;max-height:360px" aria-hidden="true"><circle cx="260" cy="180" r="138" fill="none" stroke="var(--grey-2)" stroke-width="1"/><circle cx="260" cy="180" r="94" fill="none" stroke="var(--accent)" stroke-width="2"/><circle cx="260" cy="180" r="50" fill="var(--ink)"/><path d="M260 42V10M398 180h32M260 318v32M122 180H90" stroke="var(--grey-3)" stroke-width="1"/></svg>
<div style="position:absolute;inset:0;display:grid;place-items:center;color:var(--paper);font-size:clamp(18px,1.5vw,28px);font-weight:400">REPO · 实现事实</div><div style="position:absolute;top:3%;left:50%;transform:translateX(-50%);font-family:var(--mono);font-size:14px;letter-spacing:.15em;color:var(--text-helper)">WIKI · 长文</div><div style="position:absolute;right:2%;top:48%;font-family:var(--mono);font-size:14px;letter-spacing:.15em;color:var(--text-helper)">BASE · 状态</div><div style="position:absolute;bottom:3%;left:50%;transform:translateX(-50%);font-family:var(--mono);font-size:14px;letter-spacing:.15em;color:var(--text-helper)">Trellis · 上下文</div>
</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:1.2vw;border-top:1px solid var(--border-subtle);padding-top:1.6vh"><div><div class="t-cat">WIKI</div><div class="t-body-sm">Spec / Map / 研究 / 决策</div></div><div><div class="t-cat">BASE</div><div class="t-body-sm">身份 / 状态 / 关系 / 摘要</div></div><div><div class="t-cat">REPO</div><div class="t-body-sm">代码 / 测试 / ADR / 证据</div></div></div>
</div>
</div>
</div>
</section>
<section class="slide grey" data-layout="S04" data-animate="grid-reveal">
<div class="canvas-card">
<div class="chrome-min"><div class="l">03 · OPERATING FACTS</div><div class="r">04 / 13</div></div>
<div data-anim="line" style="display:flex;flex-direction:column;gap:1.2vh"><div class="t-cat accent">OPERATING FACTS · 当前版本的六个边界</div><h2 class="h-xl-zh" style="font-size:min(5.2vw,9.2vh)">先记住这六件事</h2></div>
<div data-anim="up" class="sub-grid-3-2" style="margin-top:3.6vh;gap:1.4vh 1.4vw">
<article class="sub-card" style="border-radius:0"><i data-lucide="ban"></i><span class="nb-corner">01</span><div class="ttl">不建 Issue</div><p class="desc">需求先在对话、研究、原型中澄清。</p></article>
<article class="sub-card" style="border-radius:0"><i data-lucide="list-checks"></i><span class="nb-corner">02</span><div class="ttl">24 个字段</div><p class="desc">Base 只保留可查询、可关联的契约。</p></article>
<article class="sub-card" style="border-radius:0"><i data-lucide="orbit"></i><span class="nb-corner">03</span><div class="ttl">6 个状态</div><p class="desc">只保留一个状态轴,不维护工作流阶段。</p></article>
<article class="sub-card" style="border-radius:0"><i data-lucide="file-check-2"></i><span class="nb-corner">04</span><div class="ttl">先 Wiki,后 Base</div><p class="desc">长文回查完整后,才创建 Base 记录。</p></article>
<article class="sub-card" style="border-radius:0"><i data-lucide="link-2"></i><span class="nb-corner">05</span><div class="ttl">真实 ID 关系</div><p class="desc">父子项和依赖都使用 record ID 回写。</p></article>
<article class="sub-card card-accent" style="border-radius:0"><i data-lucide="check-check"></i><span class="nb-corner">06</span><div class="ttl">每次写入都回查</div><p class="desc">不能只看 ok: true 就宣称成功。</p></article>
</div>
</div>
</section>
<section class="slide dark" data-layout="S05" data-animate="stack-build">
<div class="canvas-card">
<div class="chrome-min"><div class="l">04 · THREE LAYERS</div><div class="r">05 / 13</div></div>
<div data-anim="line" style="display:flex;flex-direction:column;gap:1.2vh"><div class="t-cat on-dark">THREE LAYERS · 一条工作,三层事实</div><h2 class="h-xl-zh" style="font-size:min(5.2vw,9.2vh)">长文、队列、执行</h2></div>
<div data-anim="up" class="stack-row" style="margin-top:5vh;gap:1.2vw">
<article class="stack-block b-grey"><div class="layer-nb">LAYER 01 · WHY / WHAT</div><i data-lucide="book-open" class="layer-icon"></i><div class="layer-ttl">Wiki / Docx</div><div class="layer-desc">保存 Engineering Spec、Map、研究、决策与长篇说明。</div><div class="layer-tag">可读的正式产物</div></article>
<article class="stack-block b-accent"><div class="layer-nb">LAYER 02 · WHO / NEXT</div><i data-lucide="table-2" class="layer-icon"></i><div class="layer-ttl">飞书 Base</div><div class="layer-desc">保存 Spec/Ticket 身份、状态、负责人、关系与证据摘要。</div><div class="layer-tag">可查询的工作队列</div></article>
<article class="stack-block b-ink"><div class="layer-nb">LAYER 03 · HOW / PROOF</div><i data-lucide="terminal-square" class="layer-icon"></i><div class="layer-ttl">Trellis + Repo</div><div class="layer-desc">保存执行上下文、计划、多会话证据、代码、测试与 ADR。</div><div class="layer-tag">可验证的实现事实</div></article>
</div>
<div class="t-meta" style="margin-top:2vh;color:rgba(255,255,255,.55)">ONE FLOW · THREE SOURCES · NO SILENT DUPLICATION</div>
</div>
</section>
<section class="slide light" data-layout="S11" data-animate="timeline-walk">
<div class="canvas-card">
<div class="chrome-min"><div class="l">05 · THE FLOW</div><div class="r">06 / 13</div></div>
<div data-anim="line" style="display:flex;flex-direction:column;gap:1.2vh"><div class="t-cat accent">THE FLOW · 从澄清到收口</div><h2 class="h-xl-zh" style="font-size:min(5.2vw,9.2vh)">一条可回查的生产线</h2></div>
<div class="timeline-h" style="margin-top:2vh"><div class="tl-row"><div class="th-node up"><span class="dot"></span><div class="label"><span class="yr">01</span><span class="name">Setup</span><span class="desc">身份、Base、Wiki、项目</span></div></div><div class="th-node down"><span class="dot"></span><div class="label"><span class="yr">02</span><span class="name">Clarify</span><span class="desc">目标、约束、验收</span></div></div><div class="th-node up"><span class="dot"></span><div class="label"><span class="yr">03</span><span class="name">To Spec</span><span class="desc">先 Wiki,后 Base</span></div></div><div class="th-node down"><span class="dot"></span><div class="label"><span class="yr">04</span><span class="name">To Tickets</span><span class="desc">垂直切片与依赖</span></div></div><div class="th-node accent up"><span class="dot"></span><div class="label"><span class="yr">05—07</span><span class="name">Start → Build → Close</span><span class="desc">认领、实现、证据、人审</span></div></div></div></div>
<div class="t-meta" style="border-top:1px solid var(--border-subtle);padding-top:1.6vh;margin-top:1vh">HITL 在关键节点拍板,AFK 负责确认后的重复执行。</div>
</div>
</section>
<section class="slide light" data-layout="S08" data-animate="duo-mirror">
<div class="canvas-card">
<div class="chrome-min"><div class="l">06 · THE SHIFT</div><div class="r">07 / 13</div></div>
<div data-anim="line" style="display:flex;flex-direction:column;gap:1.2vh"><div class="t-cat accent">THE SHIFT · 从副本链到事实链</div><h2 class="h-xl-zh" style="font-size:min(5.2vw,9.2vh)">工作流改变的不是工具,是信息流</h2></div>
<div class="duo-compare" style="margin-top:6vh">
<div class="col"><div class="col-tag"><span class="num">BEFORE</span>副本链</div><div class="col-ttl">Issue → Triage → Brief</div><div class="col-desc">需求先进入 Base,再被分诊、转译、复制到多个上下文。</div><ul class="col-list"><li>Issue 变成第二份 Spec</li><li>状态轴越来越多</li><li>Agent Brief 与 Human Brief 分裂</li></ul></div>
<div class="vrule"></div>
<div class="col accent"><div class="col-tag"><span class="num">AFTER</span>事实链</div><div class="col-ttl">Clarify → Spec → Evidence</div><div class="col-desc">需求先完成澄清,再让 Wiki、Base、Trellis、Repo 各自保存一段事实。</div><ul class="col-list"><li>没有 Issue 进入 Base</li><li>只有一个状态轴</li><li>每次写入都回查证据</li></ul></div>
</div>
</div>
</section>
<section class="slide grey" data-layout="S13" data-animate="three-forces">
<div class="canvas-card">
<div class="chrome-min"><div class="l">07 · HUMAN GATES</div><div class="r">08 / 13</div></div>
<div data-anim="line" style="display:flex;flex-direction:column;gap:1.2vh"><div class="t-cat accent">HUMAN GATES · HITL 不是阻力,是边界</div><h2 class="h-xl-zh" style="font-size:min(5.2vw,9.2vh)">人在三个地方拍板</h2></div>
<div data-anim="up" style="display:grid;grid-template-columns:5fr 11fr;gap:2vw;flex:1;margin-top:3vh;align-items:stretch">
<div class="card-ink" style="padding:3.2vh 2vw;display:flex;flex-direction:column;justify-content:space-between;position:relative;overflow:hidden"><div class="t-cat on-dark">HITL</div><div style="font-size:min(5.6vw,10vh);font-weight:200;line-height:.92;letter-spacing:-.04em">确认<br/><span style="font-style:italic;font-weight:300">再执行</span></div><div style="font-size:max(16px,1vw);line-height:1.55;color:rgba(255,255,255,.72);max-width:23ch">Agent 可以加速重复动作,但不能替人决定正式产物的边界。</div></div>
<div style="display:flex;flex-direction:column;gap:1.2vh"><article class="card-fill" style="padding:2.2vh 1.6vw;display:grid;grid-template-columns:auto 1fr;gap:1.4vw;align-items:start"><div class="num-mega" style="font-size:min(4.4vw,7.8vh)">01</div><div><div class="t-h-prod">需求澄清</div><p class="t-body-sm">目标、范围、约束、验收,决定是否继续交付。</p></div></article><article class="card-fill" style="padding:2.2vh 1.6vw;display:grid;grid-template-columns:auto 1fr;gap:1.4vw;align-items:start"><div class="num-mega" style="font-size:min(4.4vw,7.8vh)">02</div><div><div class="t-h-prod">拆票与依赖</div><p class="t-body-sm">确认垂直切片粒度、父子关系和阻塞边。</p></div></article><article class="card-fill" style="padding:2.2vh 1.6vw;display:grid;grid-template-columns:auto 1fr;gap:1.4vw;align-items:start"><div class="num-mega" style="font-size:min(4.4vw,7.8vh);color:var(--accent)">03</div><div><div class="t-h-prod" style="color:var(--accent)">收口确认</div><p class="t-body-sm">人工确认代码/功能评审完成,并选择允许关闭的记录。</p></div></article></div>
</div>
</div>
</section>
<section class="slide light" data-layout="S14" data-animate="loop-form">
<div class="canvas-card">
<div class="chrome-min"><div class="l">08 · EXECUTION LOOP</div><div class="r">09 / 13</div></div>
<div data-anim="line" style="display:flex;flex-direction:column;gap:1.2vh"><div class="t-cat accent">EXECUTION LOOP · Start → Inline / Trellis → Close</div><h2 class="h-xl-zh" style="font-size:min(5.2vw,9.2vh)">执行不是直线,是一条可恢复的闭环</h2></div>
<div data-anim="up" style="flex:1;display:grid;grid-template-columns:5fr 7fr;gap:3vw;align-items:center;margin-top:3vh">
<div style="display:flex;flex-direction:column;gap:0"><div style="display:grid;grid-template-columns:auto 1fr;gap:1.4vw;padding:1.7vh 0;border-top:1px solid var(--border-subtle)"><span class="num-mega" style="font-size:min(4.2vw,7.5vh)">01</span><div><div class="t-h-prod">认领</div><div class="t-body-sm">冻结 record ID 与最新快照,状态改为进行中。</div></div></div><div style="display:grid;grid-template-columns:auto 1fr;gap:1.4vw;padding:1.7vh 0;border-top:1px solid var(--border-subtle)"><span class="num-mega" style="font-size:min(4.2vw,7.5vh)">02</span><div><div class="t-h-prod">执行</div><div class="t-body-sm">范围窄走 Inline;跨模块、多会话走 Trellis。</div></div></div><div style="display:grid;grid-template-columns:auto 1fr;gap:1.4vw;padding:1.7vh 0;border-top:1px solid var(--border-subtle)"><span class="num-mega" style="font-size:min(4.2vw,7.5vh)">03</span><div><div class="t-h-prod">取证</div><div class="t-body-sm">代码、测试、命令结果、快照和回查事实一起整理。</div></div></div><div style="display:grid;grid-template-columns:auto 1fr;gap:1.4vw;padding:1.7vh 0;border-top:1px solid var(--border-subtle);border-bottom:1px solid var(--border-subtle)"><span class="num-mega" style="font-size:min(4.2vw,7.5vh);color:var(--accent)">04</span><div><div class="t-h-prod" style="color:var(--accent)">收口</div><div class="t-body-sm">Ticket 和 Spec 分两次确认,不把 finish 当成完成。</div></div></div></div>
<div style="display:flex;align-items:center;justify-content:center;position:relative;min-height:38vh"><svg viewBox="0 0 420 420" style="width:min(34vw,420px);height:min(34vw,420px);max-height:50vh" aria-hidden="true"><circle cx="210" cy="210" r="150" fill="none" stroke="var(--grey-2)" stroke-width="1" stroke-dasharray="4 8"/><circle cx="210" cy="210" r="100" fill="none" stroke="var(--accent)" stroke-width="2"/><path d="M210 60 A150 150 0 0 1 344 292" fill="none" stroke="var(--ink)" stroke-width="1.5" marker-end="url(#arrow)"/><path d="M344 292 A150 150 0 0 1 76 292" fill="none" stroke="var(--ink)" stroke-width="1.5" marker-end="url(#arrow)"/><path d="M76 292 A150 150 0 0 1 210 60" fill="none" stroke="var(--ink)" stroke-width="1.5" marker-end="url(#arrow)"/><defs><marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="var(--ink)"/></marker></defs></svg><div style="position:absolute;display:flex;flex-direction:column;align-items:center;gap:.8vh"><div style="font-family:var(--mono);font-size:14px;letter-spacing:.18em;color:var(--accent)">LOOP</div><div style="font-size:min(3vw,5.2vh);font-weight:200;letter-spacing:-.03em">可恢复</div><div class="t-helper">状态、上下文、证据持续刷新</div></div></div>
</div>
</div>
</section>
<section class="slide dark" data-layout="S18" data-animate="why-now">
<div class="canvas-card">
<div class="chrome-min"><div class="l">09 · HARD NUMBERS</div><div class="r">10 / 13</div></div>
<div data-anim="line" style="display:flex;flex-direction:column;gap:1.2vh"><div class="t-cat on-dark">HARD NUMBERS · 规则已经被压缩成几个数字</div><h2 class="h-xl-zh" style="font-size:min(5.2vw,9.2vh)">少一点状态,多一点可验证性</h2></div>
<div data-anim="up" style="display:grid;grid-template-columns:repeat(3,1fr);gap:2.2vw;flex:1;margin-top:5vh;align-items:stretch">
<div style="display:flex;flex-direction:column;justify-content:space-between;border-top:1px solid rgba(255,255,255,.25);padding-top:1.8vh"><div><div class="t-cat on-dark">FIELD CONTRACT</div><div style="font-size:max(22px,1.8vw);font-weight:300;line-height:1.2;margin-top:1.4vh">一套字段契约</div><p style="font-size:max(16px,1vw);line-height:1.55;color:rgba(255,255,255,.68);margin-top:1.6vh">Base 当前保留 24 个字段,所有关系都能被查询和回查。</p></div><div style="font-size:min(8.4vw,14vh);font-weight:200;line-height:.84;letter-spacing:-.05em">24</div></div>
<div style="display:flex;flex-direction:column;justify-content:space-between;border-top:1px solid rgba(255,255,255,.25);padding-top:1.8vh"><div><div class="t-cat on-dark">STATE AXIS</div><div style="font-size:max(22px,1.8vw);font-weight:300;line-height:1.2;margin-top:1.4vh">一个状态轴</div><p style="font-size:max(16px,1vw);line-height:1.55;color:rgba(255,255,255,.68);margin-top:1.6vh">只允许 6 个状态:ready-for-agent、进行中、阻塞、待评审、已完成、wontfix。</p></div><div style="font-size:min(8.4vw,14vh);font-weight:200;line-height:.84;letter-spacing:-.05em">6</div></div>
<div style="display:flex;flex-direction:column;justify-content:space-between;border-top:2px solid var(--accent);padding-top:1.8vh"><div><div class="t-cat" style="color:var(--accent-bright)">EXECUTION BINDING</div><div style="font-size:max(22px,1.8vw);font-weight:300;line-height:1.2;margin-top:1.4vh;color:var(--paper)">一个 Spec,一个 task</div><p style="font-size:max(16px,1vw);line-height:1.55;color:rgba(255,255,255,.68);margin-top:1.6vh">Trellis 只保存执行上下文,不成为第二份业务 Spec。</p></div><div style="font-size:min(8.4vw,14vh);font-weight:200;line-height:.84;letter-spacing:-.05em;color:var(--accent-bright)">1:1</div></div>
</div>
</div>
</section>
<section class="slide light" data-layout="S19" data-animate="four-cards">
<div class="canvas-card">
<div class="chrome-min"><div class="l">10 · RUNTIME BOUNDARIES</div><div class="r">11 / 13</div></div>
<div data-anim="line" style="display:flex;flex-direction:column;gap:1.4vh"><div style="height:2px;background:var(--accent);width:100%"></div><div><div class="t-cat accent">RUNTIME BOUNDARIES · 不靠“看起来对”</div><h2 class="h-xl-zh" style="font-size:min(5.2vw,9.2vh);margin-top:1.2vh">四条不能越过的线</h2></div></div>
<div data-anim="up" style="display:grid;grid-template-columns:repeat(4,1fr);gap:2vw;flex:1;align-items:start;margin-top:6vh">
<article style="border-top:1px solid var(--border-subtle);padding-top:1.6vh"><div class="t-meta">— 01 / ID</div><div style="font-size:min(3.4vw,6vh);font-weight:200;line-height:1.02;letter-spacing:-.04em;margin-top:2.2vh">稳定 ID</div><p class="t-body-sm" style="margin-top:2vh">不用标题更新记录,始终使用真实 record ID。</p></article>
<article style="border-top:1px solid var(--border-subtle);padding-top:1.6vh"><div class="t-meta">— 02 / SOURCE</div><div style="font-size:min(3.4vw,6vh);font-weight:200;line-height:1.02;letter-spacing:-.04em;margin-top:2.2vh">不猜坐标</div><p class="t-body-sm" style="margin-top:2vh">Base、table、view、Wiki 节点都来自项目契约。</p></article>
<article style="border-top:1px solid var(--border-subtle);padding-top:1.6vh"><div class="t-meta">— 03 / VERIFY</div><div style="font-size:min(3.4vw,6vh);font-weight:200;line-height:1.02;letter-spacing:-.04em;margin-top:2.2vh">写入回查</div><p class="t-body-sm" style="margin-top:2vh">未知 JSON 不是空结果,ok: true 也不是完成证据。</p></article>
<article style="border-top:2px solid var(--accent);padding-top:1.6vh"><div class="t-meta" style="color:var(--accent)">— 04 / HUMAN</div><div style="font-size:min(3.4vw,6vh);font-weight:200;line-height:1.02;letter-spacing:-.04em;margin-top:2.2vh;color:var(--accent)">人工门禁</div><p class="t-body-sm" style="margin-top:2vh">测试通过、评审通过、归档,都不能自动关闭飞书记录。</p></article>
</div>
</div>
</section>
<section class="slide grey" data-layout="S20" data-animate="stacked-ledger">
<div class="canvas-card">
<div class="chrome-min"><div class="l">11 · CLOSURE LEDGER</div><div class="r">12 / 13</div></div>
<div data-anim="line" style="display:flex;flex-direction:column;gap:1.2vh"><div class="t-cat accent">CLOSURE LEDGER · 收口不是一句“完成”</div><h2 class="h-xl-zh" style="font-size:min(5.2vw,9.2vh)">最终状态,需要一串证据</h2></div>
<div data-anim="ledger" style="display:flex;flex-direction:column;flex:1;margin-top:3vh">
<div class="ledger-row" style="display:grid;grid-template-columns:2fr 6fr auto;gap:2vw;align-items:center;border-top:1px solid var(--border-subtle);padding:1.5vh 0"><div class="ledger-num" style="font-size:min(4.8vw,8.5vh);font-weight:200;line-height:.9;letter-spacing:-.04em">24</div><div class="ledger-label"><div class="t-h-prod">字段契约</div><div class="t-body-sm">身份、状态、关系、验收、验证证据都可追溯。</div></div><i data-lucide="database" class="ledger-icon ico-lg"></i></div>
<div class="ledger-row" style="display:grid;grid-template-columns:2fr 6fr auto;gap:2vw;align-items:center;border-top:1px solid var(--border-subtle);padding:1.5vh 0"><div class="ledger-num" style="font-size:min(4.8vw,8.5vh);font-weight:200;line-height:.9;letter-spacing:-.04em">6</div><div class="ledger-label"><div class="t-h-prod">状态轴</div><div class="t-body-sm">只有已完成能满足依赖,wontfix 不是交付证明。</div></div><i data-lucide="route" class="ledger-icon ico-lg"></i></div>
<div class="ledger-row" style="display:grid;grid-template-columns:2fr 6fr auto;gap:2vw;align-items:center;border-top:1px solid var(--border-subtle);padding:1.5vh 0"><div class="ledger-num" style="font-size:min(4.8vw,8.5vh);font-weight:200;line-height:.9;letter-spacing:-.04em">1:1</div><div class="ledger-label"><div class="t-h-prod">Spec / Trellis</div><div class="t-body-sm">一个飞书 Spec 绑定一个 Trellis task,Ticket 只是其计划单元。</div></div><i data-lucide="link" class="ledger-icon ico-lg"></i></div>
<div class="ledger-row" style="display:grid;grid-template-columns:2fr 6fr auto;gap:2vw;align-items:center;border-top:1px solid var(--border-subtle);padding:1.5vh 0"><div class="ledger-num" style="font-size:min(4.8vw,8.5vh);font-weight:200;line-height:.9;letter-spacing:-.04em">2×</div><div class="ledger-label"><div class="t-h-prod">人工确认</div><div class="t-body-sm">Ticket 关闭一次,Spec 最终关闭再确认一次。</div></div><i data-lucide="user-check" class="ledger-icon ico-lg"></i></div>
<div class="ledger-row" style="display:grid;grid-template-columns:2fr 6fr auto;gap:2vw;align-items:center;border-top:1px solid var(--accent);border-bottom:1px solid var(--accent);padding:1.5vh 0;color:var(--accent)"><div class="ledger-num" style="font-size:min(4.8vw,8.5vh);font-weight:200;line-height:.9;letter-spacing:-.04em">0</div><div class="ledger-label"><div class="t-h-prod" style="color:var(--accent)">自动关闭</div><div class="t-body-sm" style="color:var(--text-secondary)">没有自动关闭。证据不足时,记录保留原状并给出下一步。</div></div><i data-lucide="circle-off" class="ledger-icon ico-lg"></i></div>
</div>
</div>
</section>
<section class="slide split" data-layout="SWISS-CLOSING-ASCII" data-animate="split-statement">
<div class="canvas-card"><div class="split-half">
<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">FEISHU · WORKFLOW</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">让事实<br/>自己<span style="font-style:italic;font-weight:300">流动</span>。</h2><div style="font-family:var(--sans),var(--sans-zh);font-size:max(16px,1vw);line-height:1.6;color:rgba(255,255,255,.82);font-weight:300;max-width:36ch;margin-top:1.4vh">当每个系统只保存它应该保存的事实,协作就不再依赖复制、猜测和记忆。</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)">13 PAGES · IKB</div><div class="t-meta" style="color:rgba(255,255,255,.62)">END OF FIELD NOTE</div></div></div>
<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">13 / 13</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">Wiki 保存长文</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">完整 Spec、Map、研究与决策,保持可读。</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">Base 保存状态</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">身份、关系、进度和下一步,保持可查询。</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">证据决定完成</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">没有直接验证证据,就记录未运行和下一步。</p></div></div></div><div data-anim="foot" class="t-meta" style="color:var(--text-helper);text-align:right">→ 完 · END OF FIELD NOTE</div></div>
</div></div>
</section>
</div>
<div id="nav"></div>
<script>
/* =============== WebGL 网格背景 (瑞士风专用) ===============
极简移动网格 + 微弱点阵叠加,营造"工业感、精准感"
- 主网格: 缓慢漂移的细线网格
- 次级: 鼠标附近的极细点阵微扰
- 颜色: 跟随主题(浅底深线 / 深底亮线),配合 mix-blend-mode
*/
const VS = `attribute vec2 position;void main(){gl_Position=vec4(position,0.0,1.0);}`;
const FS = `precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_mouse;
uniform float u_dark; // 0 = light, 1 = dark
uniform vec3 u_accent;
float gridLine(vec2 uv, float spacing, float thickness){
vec2 g = abs(fract(uv / spacing) - 0.5);
float d = min(g.x, g.y);
return 1.0 - smoothstep(thickness - 0.005, thickness + 0.005, d);
}
float dot2(vec2 p){ return dot(p,p); }
void main(){
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
float aspect = u_resolution.x / u_resolution.y;
vec2 p = uv;
p.x *= aspect;
// 缓慢平移
vec2 drift = vec2(u_time * 0.008, u_time * 0.005);
vec2 gp = p + drift;
// 主细网格 (大间距)
float mainGrid = gridLine(gp, 0.12, 0.012);
// 次级网格 (更细更密)
float subGrid = gridLine(gp, 0.024, 0.04) * 0.4;
// 鼠标附近的强化
vec2 m = u_mouse;
m.x *= aspect;
float md = length(p - m);
float mInfluence = exp(-md * 4.0) * 0.5;
float gridStrength = (mainGrid + subGrid * 0.5) * (0.45 + mInfluence);
// 点阵 (作为基底)
vec2 dotGrid = fract(gp * 50.0) - 0.5;
float dotMask = 1.0 - smoothstep(0.05, 0.14, length(dotGrid));
// 用低频噪声调制点阵密度
float wave = sin(gp.x * 1.4 + u_time * 0.15) * cos(gp.y * 1.6 - u_time * 0.12);
dotMask *= smoothstep(-0.3, 0.6, wave) * 0.6;
// 颜色: 浅底用深线条,深底用浅线条;高亮处带 accent 痕迹
vec3 lineColor = mix(vec3(0.08), vec3(0.92), u_dark);
vec3 bgColor = mix(vec3(0.97, 0.97, 0.96), vec3(0.06, 0.06, 0.07), u_dark);
// accent 暗示 (鼠标附近偷渡一点 accent 色)
vec3 col = bgColor;
col = mix(col, lineColor, gridStrength * 0.55);
col = mix(col, lineColor, dotMask * 0.35);
col = mix(col, u_accent, mInfluence * 0.18);
gl_FragColor = vec4(col, 1.0);
}`;
const mouse={x:0.5,y:0.5};
addEventListener('mousemove',e=>{mouse.x=e.clientX/innerWidth;mouse.y=1-e.clientY/innerHeight});
function bootGL(canvasId, fsSrc){
const canvas=document.getElementById(canvasId);
const gl=canvas.getContext('webgl',{alpha:true,antialias:true,premultipliedAlpha:false});
if(!gl) return ()=>false;
const mk=(t,s)=>{const sh=gl.createShader(t);gl.shaderSource(sh,s);gl.compileShader(sh);return sh};
const prog=gl.createProgram();
gl.attachShader(prog,mk(gl.VERTEX_SHADER,VS));
gl.attachShader(prog,mk(gl.FRAGMENT_SHADER,fsSrc));
gl.linkProgram(prog);gl.useProgram(prog);
const buf=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,buf);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]),gl.STATIC_DRAW);
const pos=gl.getAttribLocation(prog,'position');
gl.enableVertexAttribArray(pos);gl.vertexAttribPointer(pos,2,gl.FLOAT,false,0,0);
const lRes=gl.getUniformLocation(prog,'u_resolution');
const lT=gl.getUniformLocation(prog,'u_time');
const lM=gl.getUniformLocation(prog,'u_mouse');
const lD=gl.getUniformLocation(prog,'u_dark');
const lA=gl.getUniformLocation(prog,'u_accent');
const resize=()=>{
const d=Math.min(window.devicePixelRatio||1,2);
canvas.width=innerWidth*d;canvas.height=innerHeight*d;
gl.viewport(0,0,canvas.width,canvas.height);
};
addEventListener('resize',resize);resize();
// 读取 CSS 变量,把 accent 颜色塞进 shader
function readAccent(){
const cs = getComputedStyle(document.documentElement);
const hex = cs.getPropertyValue('--accent').trim() || '#002FA7';
const m = hex.match(/^#([0-9a-f]{6})$/i);
if(!m) return [0, 0.18, 0.65];
const n = parseInt(m[1], 16);
return [((n>>16)&255)/255, ((n>>8)&255)/255, (n&255)/255];
}
let accent = readAccent();
let dark = 0;
return (tSec, isDark)=>{
if(isDark !== undefined) dark = isDark ? 1 : 0;
accent = readAccent();
gl.uniform2f(lRes,canvas.width,canvas.height);
gl.uniform1f(lT,tSec);
gl.uniform2f(lM,mouse.x,mouse.y);
gl.uniform1f(lD,dark);
gl.uniform3f(lA,accent[0],accent[1],accent[2]);
gl.drawArrays(gl.TRIANGLES,0,6);
return true;
};
}
// canvas-mode / low-power: skip WebGL draw loop (no active RAF loop)
let darkMode=false;
let gridCtrl=null, gridRAF=0, gridT0=Date.now();
function startGrid(){
if(document.body.classList.contains('canvas-mode') || window.__lowPowerMode || gridRAF) return;
if(!gridCtrl) gridCtrl = bootGL('bg-grid',FS);
if(!gridCtrl) return;
gridT0=Date.now();
function loop(){
if(window.__lowPowerMode){gridRAF=0;return;}
const t=(Date.now()-gridT0)/1000;
gridCtrl(t, darkMode);
gridRAF=requestAnimationFrame(loop);
}
gridRAF=requestAnimationFrame(loop);
}
function stopGrid(){
if(gridRAF) cancelAnimationFrame(gridRAF);
gridRAF=0;
}
if(document.body.classList.contains('canvas-mode')){
const c=document.getElementById('bg-grid');
if(c) c.remove();
}else{
startGrid();
}
addEventListener('swiss-low-power-change', e=>{e.detail.on ? stopGrid() : startGrid();});
// =============== 导航 ===============
const deck=document.getElementById('deck');
const slides=deck.querySelectorAll('.slide');
const nav=document.getElementById('nav');
let idx=0,total=slides.length,lock=false;
deck.style.width=(total*100)+'vw';
slides.forEach((s,i)=>{
const b=document.createElement('button');
b.className='dot';b.dataset.i=i;b.setAttribute('aria-label','Page '+(i+1));
b.onclick=()=>go(i);
nav.appendChild(b);
});
function go(n){
if(lock)return;
idx=Math.max(0,Math.min(total-1,n));
window.__currentSlideIndex = idx;
deck.style.transform=`translateX(${-idx*100}vw)`;
nav.querySelectorAll('.dot').forEach((d,i)=>d.classList.toggle('active',i===idx));
const el=slides[idx];
const isDark = el.classList.contains('dark') || el.classList.contains('accent');
document.body.classList.toggle('dark-bg', isDark);
darkMode = isDark;
if(window.__playSlide) setTimeout(()=>window.__playSlide(idx), 450);
lock=true;setTimeout(()=>lock=false,700);
}
/* =============== ESC 索引视图 =============== */
let overviewOn=false;
const ov=document.createElement('div');
ov.id='overview';
ov.style.cssText='position:fixed;inset:0;z-index:100;background:rgba(250,250,248,.96);backdrop-filter:blur(12px);display:none;overflow-y:auto;padding:4vh 4vw';
document.body.appendChild(ov);
function buildOverview(){
ov.innerHTML='';
const grid=document.createElement('div');
grid.style.cssText='display:grid;grid-template-columns:repeat(4,1fr);gap:2vh 1.6vw;max-width:90vw;margin:0 auto';
slides.forEach((s,i)=>{
const card=document.createElement('div');
card.style.cssText='cursor:pointer;overflow:hidden;border:2px solid '+(i===idx?'var(--accent)':'rgba(0,0,0,.12)')+';transition:border-color .2s';
card.onmouseenter=()=>card.style.borderColor='rgba(0,0,0,.4)';
card.onmouseleave=()=>card.style.borderColor=i===idx?'var(--accent)':'rgba(0,0,0,.12)';
const wrap=document.createElement('div');
const isDark = s.classList.contains('dark') || s.classList.contains('accent');
wrap.style.cssText='width:100%;aspect-ratio:16/9;overflow:hidden;position:relative;pointer-events:none;background:'+(isDark?'var(--ink)':'var(--paper)');
const clone=s.cloneNode(true);
clone.style.cssText='width:100vw;height:100vh;transform:scale('+(1/4.5)+');transform-origin:top left;position:absolute;top:0;left:0;pointer-events:none';
wrap.appendChild(clone);
const label=document.createElement('div');
/* ESC 索引卡 label */
label.style.cssText='padding:6px 10px;font-family:var(--mono);font-size:14px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink);opacity:.7';
label.textContent=(i+1)+' / '+total;
card.appendChild(wrap);
card.appendChild(label);
card.onclick=()=>{toggleOverview();go(i)};
grid.appendChild(card);
});
ov.appendChild(grid);
}
function toggleOverview(){
overviewOn=!overviewOn;
if(overviewOn){buildOverview();ov.style.display='block';}
else{ov.style.display='none';}
}
addEventListener('keydown',e=>{
if(e.key==='Escape'){e.preventDefault();toggleOverview();return;}
if(e.key && e.key.toLowerCase()==='b' && !e.metaKey && !e.ctrlKey && !e.altKey){
e.preventDefault();
window.__setLowPowerMode(!window.__lowPowerMode);
return;
}
if(overviewOn)return;
if(e.key==='ArrowRight'||e.key==='PageDown'||e.key===' '||e.key==='ArrowDown'){
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);
</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>