Files

2427 lines
120 KiB
HTML
Raw Permalink Normal View History

<!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>