template-swiss.html 99 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>[必填] 替换为 PPT 标题 · Deck Title</title>
  7. <link rel="preconnect" href="https://fonts.googleapis.com">
  8. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  9. <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">
  10. <style>
  11. :root{
  12. /* ============ 主题色(默认: 🔵 克莱因蓝 IKB) ============
  13. 切换主题: 从 references/themes-swiss.md 复制对应的 :root 块
  14. 整体替换标有"主题色"注释的所有变量,其他散落的 var() 引用无需逐处改 */
  15. --paper:#fafaf8; /* 主底色: 高级灰白 */
  16. --paper-rgb:250,250,248;
  17. --ink:#0a0a0a; /* 文字主色: 近黑 */
  18. --ink-rgb:10,10,10;
  19. --grey-1:#f0f0ee; /* 浅灰底 */
  20. --grey-2:#d4d4d2; /* 中灰(分割线) */
  21. --grey-3:#737373; /* 暗灰(辅助文字) */
  22. --accent:#002FA7; /* 高亮色: 克莱因蓝 IKB */
  23. --accent-rgb:0,47,167;
  24. --accent-on:#ffffff; /* accent 上的反色文字 */
  25. --accent-bright:#5B7BFF; /* 暗底高亮: IKB 提亮版 */
  26. /* ============ Carbon 文本角色 token (role-based,代替 opacity) ============
  27. 亮底: primary 主文 / secondary 次文 / helper 辅助 / placeholder 占位
  28. 暗底: inverse 自动反向 */
  29. --text-primary:#0a0a0a; /* = ink, 100% */
  30. --text-secondary:#525252; /* gray 70 */
  31. --text-helper:#737373; /* gray 60 */
  32. --text-placeholder:#a3a3a3; /* gray 40 */
  33. --text-on-color:#ffffff; /* accent/dark 反色 */
  34. --border-subtle:#e0e0e0; /* gray 20, 极细分隔 */
  35. --border-strong:#a3a3a3; /* gray 40, 强分隔 */
  36. /* ============ 字体(跨主题固定) ============ */
  37. --sans:"Inter","Helvetica Neue","Helvetica","Arial","Segoe UI Variable","Segoe UI",system-ui,-apple-system,sans-serif;
  38. --sans-zh:"PingFang SC","Hiragino Sans GB","Source Han Sans SC","Noto Sans SC","Microsoft YaHei UI","Microsoft YaHei","微软雅黑",sans-serif;
  39. --mono:"JetBrains Mono","IBM Plex Mono","SF Mono","Cascadia Code","Consolas","Courier New",ui-monospace,monospace;
  40. /* ============ Carbon 2x Grid 间距模数 (基础 8px) ============
  41. 参考: https://carbondesignsystem.com/elements/2x-grid/overview/
  42. 任何 padding/gap/margin 优先用这套 token,确保 8px 基线对齐 */
  43. --sp-3:8px; /* 02 token */
  44. --sp-4:12px; /* 03 */
  45. --sp-5:16px; /* 04 */
  46. --sp-6:24px; /* 05 */
  47. --sp-7:32px; /* 06 */
  48. --sp-8:40px; /* 07 */
  49. --sp-9:48px; /* 08 */
  50. --sp-10:64px; /* 09 */
  51. --sp-11:80px; /* 10 */
  52. --sp-12:96px; /* 11 */
  53. --sp-13:160px; /* 12 */
  54. /* ============ Carbon Motion tokens ============
  55. https://carbondesignsystem.com/guidelines/motion/overview/
  56. 两套体系:productive (功能) 短 + 锐 / expressive (叙事) 长 + 软 */
  57. --ease-prod:cubic-bezier(.2,0,.38,.9); /* productive standard */
  58. --ease-exp:cubic-bezier(.4,.14,.3,1); /* expressive standard */
  59. --ease-entry-prod:cubic-bezier(0,0,.38,.9); /* productive entrance */
  60. --ease-entry-exp:cubic-bezier(0,0,.3,1); /* expressive entrance */
  61. --dur-fast-1:.07s; /* 70ms */
  62. --dur-fast-2:.11s; /* 110ms */
  63. --dur-mod-1:.15s; /* 150ms */
  64. --dur-mod-2:.24s; /* 240ms */
  65. --dur-slow-1:.4s; /* 400ms */
  66. --dur-slow-2:.7s; /* 700ms */
  67. /* 底部分页组件安全区: 主内容最低处不要进入这条区域 */
  68. --nav-safe-bottom:8vh;
  69. }
  70. *{box-sizing:border-box;margin:0;padding:0}
  71. html,body{
  72. width:100%;height:100%;overflow:hidden;
  73. background:var(--paper);color:var(--ink);
  74. font-family:var(--sans),var(--sans-zh);
  75. font-feature-settings:"ss01","cv11";
  76. -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility
  77. }
  78. /* ============ WebGL 网格背景 ============ */
  79. canvas.bg{position:fixed;inset:0;width:100vw;height:100vh;z-index:0;display:block;opacity:.55;mix-blend-mode:multiply;pointer-events:none}
  80. body.dark-bg canvas.bg{mix-blend-mode:screen;opacity:.42}
  81. body.low-power canvas.bg,
  82. body.low-power canvas.ascii-bg{display:none!important}
  83. /* ============ Deck 容器 + 翻页 ============ */
  84. #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}
  85. .slide{
  86. width:100vw;height:100vh;flex:0 0 100vw;
  87. position:relative;
  88. padding:5.5vh 5vw 7vh 5vw;
  89. display:flex;flex-direction:column;
  90. overflow:hidden;
  91. background:var(--paper);color:var(--ink);
  92. }
  93. .slide.grey{background:var(--grey-1)}
  94. .slide.dark{background:var(--ink);color:var(--paper)}
  95. .slide.dark .grey-only{display:none}
  96. .slide.accent{background:var(--accent);color:var(--accent-on)}
  97. .slide.accent .accent-block{background:var(--accent-on);color:var(--accent)}
  98. /* Hero 页透出网格背景多一点 */
  99. .slide.hero{background:transparent}
  100. .slide.hero.grey{background:rgba(var(--paper-rgb),.86);backdrop-filter:blur(1px)}
  101. .slide.hero.dark{background:rgba(var(--ink-rgb),.92);color:var(--paper)}
  102. /* ============ 装饰: 极细分隔线 + 点阵矩阵 ============ */
  103. .rule{width:100%;height:1px;background:currentColor;opacity:.18;margin:0}
  104. .rule.thick{height:2px;opacity:.85}
  105. .rule.accent{background:var(--accent);opacity:1;height:2px}
  106. .rule.v{width:1px;height:100%;margin:0}
  107. /* 点阵矩阵 (dot matrix) - 用 radial-gradient */
  108. .dots{
  109. background-image:radial-gradient(currentColor 1px, transparent 1px);
  110. background-size:12px 12px;
  111. background-position:0 0;
  112. opacity:.18;
  113. }
  114. .dots-fine{
  115. background-image:radial-gradient(currentColor 0.8px, transparent 0.8px);
  116. background-size:8px 8px;
  117. opacity:.14;
  118. }
  119. .dots-bold{
  120. background-image:radial-gradient(currentColor 1.4px, transparent 1.4px);
  121. background-size:18px 18px;
  122. opacity:.22;
  123. }
  124. /* ============ Chrome (顶部 meta) + Foot (底部) ============ */
  125. .chrome{
  126. display:flex;justify-content:space-between;align-items:flex-start;
  127. font-family:var(--mono);
  128. font-size:14px;letter-spacing:.16em;text-transform:uppercase;
  129. opacity:.7;margin-bottom:auto
  130. }
  131. .chrome .l,.chrome .r{display:flex;gap:1.6em;align-items:center}
  132. .chrome .sep{width:24px;height:1px;background:currentColor;opacity:.5}
  133. .foot{
  134. margin-top:auto;
  135. display:flex;justify-content:space-between;align-items:flex-end;
  136. font-family:var(--mono);
  137. font-size:14px;letter-spacing:.14em;text-transform:uppercase;
  138. opacity:.55;
  139. padding-top:2vh;
  140. border-top:1px solid currentColor;
  141. border-color:rgba(127,127,127,.25)
  142. }
  143. .foot .nb{font-family:var(--sans);font-weight:600;letter-spacing:.04em}
  144. /* ============ Tag / Kicker / Meta 标签 ============ */
  145. .kicker{
  146. font-family:var(--mono);
  147. font-size:14px;letter-spacing:.24em;text-transform:uppercase;
  148. opacity:.65;margin-bottom:2.4vh;
  149. display:inline-flex;align-items:center;gap:.8em
  150. }
  151. .kicker::before{
  152. content:"";width:24px;height:1px;background:currentColor;opacity:.6
  153. }
  154. .kicker.no-line::before{display:none}
  155. .kicker.accent{color:var(--accent);opacity:1;font-weight:600}
  156. .tag{
  157. display:inline-block;
  158. font-family:var(--mono);
  159. font-size:14px;letter-spacing:.18em;text-transform:uppercase;
  160. padding:5px 10px;border:1px solid currentColor;opacity:.85
  161. }
  162. .tag.solid{background:currentColor;color:var(--paper);border-color:transparent}
  163. .tag.accent{background:var(--accent);color:var(--accent-on);border-color:transparent;opacity:1}
  164. /* ============ 标题层级 (无衬线 · 极轻字重 · 极致字号对比) ============ */
  165. .h-hero{
  166. font-family:var(--sans),var(--sans-zh);
  167. font-weight:200;
  168. font-size:11vw;
  169. line-height:.92;
  170. letter-spacing:-.04em;
  171. }
  172. .h-hero-zh{
  173. font-family:var(--sans),var(--sans-zh);
  174. font-weight:200;
  175. font-size:8.4vw;
  176. line-height:.96;
  177. letter-spacing:-.025em;
  178. }
  179. .h-xl{
  180. font-family:var(--sans),var(--sans-zh);
  181. font-weight:200;
  182. font-size:6vw;
  183. line-height:1;
  184. letter-spacing:-.03em;
  185. }
  186. .h-xl-zh{
  187. font-family:var(--sans),var(--sans-zh);
  188. font-weight:200;
  189. font-size:5vw;
  190. line-height:1.05;
  191. letter-spacing:-.025em;
  192. }
  193. .h-md{
  194. font-family:var(--sans),var(--sans-zh);
  195. font-weight:300;
  196. font-size:2.6vw;
  197. line-height:1.18;
  198. letter-spacing:-.015em;
  199. }
  200. .h-sub{
  201. font-family:var(--sans),var(--sans-zh);
  202. font-weight:400;
  203. font-size:2.2vw;
  204. line-height:1.3;
  205. letter-spacing:-.01em;
  206. opacity:.7;
  207. }
  208. /* ============ 正文 / 引语 / 元数据 ============ */
  209. .lead{
  210. font-family:var(--sans),var(--sans-zh);
  211. font-weight:400;
  212. font-size:1.55vw;
  213. line-height:1.4;
  214. letter-spacing:-.005em;
  215. opacity:.86;
  216. }
  217. .body{
  218. font-family:var(--sans),var(--sans-zh);
  219. font-weight:400;
  220. font-size:max(18px,1.08vw);
  221. line-height:1.6;
  222. letter-spacing:0;
  223. opacity:.78;
  224. }
  225. .body-sm{
  226. font-family:var(--sans),var(--sans-zh);
  227. font-weight:400;
  228. font-size:max(16px,.92vw);
  229. line-height:1.55;
  230. opacity:.7;
  231. }
  232. .meta{
  233. font-family:var(--mono);
  234. font-size:max(14px,.82vw);
  235. letter-spacing:.18em;text-transform:uppercase;
  236. opacity:.6;
  237. }
  238. .meta-row{
  239. display:flex;gap:1.4em;align-items:baseline;flex-wrap:wrap;
  240. font-family:var(--mono);
  241. font-size:max(14px,.88vw);letter-spacing:.16em;text-transform:uppercase;
  242. opacity:.65;
  243. }
  244. .meta-row span:not(.dot){display:inline-block}
  245. .meta-row .dot{
  246. display:inline-block;width:4px;height:4px;border-radius:50%;
  247. background:currentColor;opacity:.5;vertical-align:middle
  248. }
  249. /* ============ KPI Hero (视觉英雄数据) ============ */
  250. .kpi-hero{
  251. font-family:var(--sans);
  252. font-weight:800;
  253. font-size:22vw;
  254. line-height:.82;
  255. letter-spacing:-.05em;
  256. font-feature-settings:"tnum","ss01";
  257. }
  258. .kpi-hero .unit{
  259. font-family:var(--sans),var(--sans-zh);
  260. font-weight:500;
  261. font-size:.18em;
  262. letter-spacing:0;
  263. opacity:.5;
  264. margin-left:.12em;
  265. vertical-align:.5em;
  266. }
  267. .kpi-hero.accent{color:var(--accent)}
  268. .kpi-big{
  269. font-family:var(--sans);
  270. font-weight:800;
  271. font-size:11vw;
  272. line-height:.85;
  273. letter-spacing:-.04em;
  274. font-feature-settings:"tnum"
  275. }
  276. .kpi-mid{
  277. font-family:var(--sans);
  278. font-weight:700;
  279. font-size:6vw;
  280. line-height:.88;
  281. letter-spacing:-.03em;
  282. font-feature-settings:"tnum"
  283. }
  284. /* ============ Stat Card (数据卡片 · 极简) ============ */
  285. .stat-card{
  286. display:flex;flex-direction:column;
  287. gap:.6vh;align-items:flex-start;
  288. padding-top:1.6vh;
  289. border-top:2px solid currentColor;
  290. }
  291. .stat-card.thin{border-top-width:1px;border-color:rgba(127,127,127,.4)}
  292. .stat-card.accent-top{border-top-color:var(--accent);border-top-width:3px}
  293. .stat-card .stat-label{
  294. font-family:var(--mono);
  295. font-size:max(14px,.82vw);
  296. letter-spacing:.24em;text-transform:uppercase;
  297. opacity:.6;
  298. }
  299. .stat-card .stat-nb{
  300. font-family:var(--sans);
  301. font-weight:800;
  302. font-size:5.6vw;
  303. line-height:.88;
  304. letter-spacing:-.035em;
  305. font-feature-settings:"tnum";
  306. margin-top:.4vh;
  307. }
  308. .stat-card .stat-nb .stat-unit{
  309. font-family:var(--sans),var(--sans-zh);
  310. font-weight:500;
  311. font-size:.32em;
  312. letter-spacing:0;
  313. opacity:.6;
  314. margin-left:.14em;
  315. vertical-align:.4em;
  316. }
  317. .stat-card .stat-note{
  318. font-family:var(--sans),var(--sans-zh);
  319. font-weight:400;
  320. font-size:max(16px,.98vw);
  321. line-height:1.5;
  322. opacity:.7;
  323. margin-top:.6vh;
  324. }
  325. .grid-4 .stat-card .stat-nb{font-size:4.6vw}
  326. .grid-3 .stat-card .stat-nb{font-size:6.4vw}
  327. .grid-6 .stat-card .stat-nb{font-size:4vw}
  328. /* ============ Accent Block (高亮色块包裹内容) ============ */
  329. .accent-block{
  330. background:var(--accent);color:var(--accent-on);
  331. padding:2.4vh 2vw;
  332. }
  333. .accent-block.tight{padding:1.4vh 1.4vw}
  334. .accent-block .h-md,.accent-block .h-xl,.accent-block .kpi-mid{color:var(--accent-on)}
  335. .ink-block{
  336. background:var(--ink);color:var(--paper);
  337. padding:2.4vh 2vw
  338. }
  339. .grey-block{
  340. background:var(--grey-1);
  341. padding:2.4vh 2vw
  342. }
  343. /* 高亮文字 (mark 风格) */
  344. .mark{
  345. background:var(--accent);color:var(--accent-on);
  346. padding:0 .2em;
  347. box-decoration-break:clone;
  348. -webkit-box-decoration-break:clone;
  349. }
  350. .mark.ink{background:var(--ink);color:var(--paper)}
  351. .underline-accent{
  352. background-image:linear-gradient(to bottom, transparent 70%, var(--accent) 70%, var(--accent) 96%, transparent 96%);
  353. padding:0 .05em
  354. }
  355. /* ============ Pipeline / Step ============ */
  356. .pipeline-section{margin-top:3.2vh;padding-top:2.2vh;border-top:1px solid rgba(127,127,127,.3)}
  357. .pipeline-section:first-of-type{border-top:0;padding-top:0;margin-top:2.4vh}
  358. .pipeline-label{
  359. font-family:var(--mono);
  360. font-size:max(14px,.84vw);
  361. letter-spacing:.24em;text-transform:uppercase;
  362. opacity:.6;margin-bottom:1.8vh;
  363. }
  364. .pipeline{
  365. display:grid;
  366. grid-template-columns:repeat(5,1fr);
  367. gap:1vw;
  368. }
  369. .pipeline[data-cols="3"]{grid-template-columns:repeat(3,1fr)}
  370. .pipeline[data-cols="4"]{grid-template-columns:repeat(4,1fr)}
  371. .pipeline[data-cols="6"]{grid-template-columns:repeat(6,1fr)}
  372. .step{
  373. display:flex;flex-direction:column;gap:.6vh;
  374. padding-top:1.2vh;
  375. border-top:2px solid currentColor;
  376. }
  377. .step.accent-top{border-top-color:var(--accent);border-top-width:3px}
  378. .step-nb{
  379. font-family:var(--mono);
  380. font-weight:500;
  381. font-size:max(14px,1vw);
  382. opacity:.5;letter-spacing:.04em
  383. }
  384. .step-title{
  385. font-family:var(--sans),var(--sans-zh);
  386. font-weight:700;
  387. font-size:1.4vw;
  388. letter-spacing:-.01em;
  389. line-height:1.2;
  390. }
  391. .step-desc{
  392. font-family:var(--sans),var(--sans-zh);
  393. font-weight:400;
  394. font-size:max(16px,.94vw);
  395. line-height:1.45;
  396. opacity:.7;
  397. }
  398. /* ============ 网格系统 (模块化网格) ============ */
  399. .frame{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden}
  400. .frame.grid-2-7-5,
  401. .frame.grid-2-6-6,
  402. .frame.grid-2-8-4,
  403. .frame.grid-2-4-8,
  404. .frame.grid-3-3,
  405. .frame.grid-12,
  406. .frame.grid-6,
  407. .frame.grid-4,
  408. .frame.grid-3{display:grid}
  409. /* 12 列模块化网格 (瑞士风核心) */
  410. .grid-12{
  411. display:grid;
  412. grid-template-columns:repeat(12,1fr);
  413. gap:2vh 1.2vw;
  414. align-items:start;
  415. }
  416. .span-2{grid-column:span 2}
  417. .span-3{grid-column:span 3}
  418. .span-4{grid-column:span 4}
  419. .span-5{grid-column:span 5}
  420. .span-6{grid-column:span 6}
  421. .span-7{grid-column:span 7}
  422. .span-8{grid-column:span 8}
  423. .span-9{grid-column:span 9}
  424. .span-12{grid-column:span 12}
  425. /* 经典分栏 */
  426. .grid-2-7-5{display:grid;grid-template-columns:7fr 5fr;gap:3vw 4vh;align-items:start}
  427. .grid-2-6-6{display:grid;grid-template-columns:1fr 1fr;gap:3vw 4vh;align-items:start}
  428. .grid-2-8-4{display:grid;grid-template-columns:8fr 4fr;gap:3vw 4vh;align-items:start}
  429. .grid-2-4-8{display:grid;grid-template-columns:4fr 8fr;gap:3vw 4vh;align-items:start}
  430. .grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:3vw 4vh;align-items:start}
  431. .grid-3-3{display:grid;grid-template-columns:repeat(3,1fr);grid-auto-rows:minmax(0,1fr);gap:2.4vh 2vw}
  432. .grid-4{display:grid;grid-template-columns:repeat(2,1fr);grid-template-rows:repeat(2,1fr);gap:3vh 3vw;flex:1;align-content:center}
  433. .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}
  434. /* 工具类 */
  435. .col{display:flex;flex-direction:column;gap:2vh}
  436. .row{display:flex;align-items:center;gap:2vw}
  437. .fill{flex:1}
  438. .center{align-items:center;justify-content:center;text-align:center}
  439. .right{text-align:right;justify-self:end}
  440. .top{align-self:start}
  441. .bottom{align-self:end}
  442. .va-center{align-self:center}
  443. .nowrap{white-space:nowrap}
  444. /* 非对称定位 */
  445. .pos-absolute{position:absolute}
  446. .bottom-left{position:absolute;left:5vw;bottom:8vh;max-width:50vw}
  447. .bottom-right{position:absolute;right:5vw;bottom:8vh;max-width:50vw;text-align:right}
  448. .top-right{position:absolute;right:5vw;top:5.5vh;text-align:right}
  449. .top-left{position:absolute;left:5vw;top:5.5vh}
  450. /* ============ Callout (引用框 · 极简) ============ */
  451. .callout{
  452. padding:2vh 2vw;
  453. border-left:3px solid var(--accent);
  454. font-family:var(--sans),var(--sans-zh);
  455. font-size:max(14px,1vw);
  456. line-height:1.55;
  457. opacity:.9;
  458. }
  459. .callout.ink{border-left-color:currentColor}
  460. .callout .cite,.callout .callout-src{
  461. display:block;margin-top:1.2vh;
  462. font-family:var(--mono);
  463. font-size:14px;letter-spacing:.16em;text-transform:uppercase;
  464. opacity:.6;
  465. }
  466. /* ============ Icons (Lucide via CDN) ============ */
  467. .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}
  468. .ico-lg,.ico-md,.ico-sm{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round}
  469. .ico-lg{width:2.4vw;height:2.4vw;stroke-width:1.4;display:inline-block}
  470. .ico-md{width:1.6vw;height:1.6vw;stroke-width:1.6;display:inline-block;vertical-align:-.4em}
  471. .ico-sm{width:1vw;height:1vw;stroke-width:1.8;display:inline-block;vertical-align:-.15em;opacity:.7}
  472. /* ============ 几何小图标 (装饰) ============ */
  473. .geo-dot{width:.7vw;height:.7vw;border-radius:50%;background:var(--accent);display:inline-block;vertical-align:middle}
  474. .geo-square{width:.7vw;height:.7vw;background:var(--accent);display:inline-block;vertical-align:middle}
  475. .geo-line{width:2vw;height:2px;background:var(--accent);display:inline-block;vertical-align:middle}
  476. .geo-circle-o{width:.9vw;height:.9vw;border:2px solid currentColor;border-radius:50%;display:inline-block;vertical-align:middle}
  477. /* ============ 图片 frame-img ============ */
  478. .frame-img{overflow:hidden;position:relative;background:var(--paper);box-sizing:border-box;width:100%}
  479. .slide.dark .frame-img{background:rgba(255,255,255,.06)}
  480. .frame-img > img{width:100%;height:100%;object-fit:cover;object-position:center center;display:block}
  481. .frame-img.fit-contain > img{object-fit:contain;object-position:center center}
  482. .frame-img.pos-top > img{object-position:top center}
  483. .frame-img.pos-face > img{object-position:center 35%}
  484. .frame-img.r-16x9{aspect-ratio:16/9;max-height:64vh}
  485. .frame-img.r-21x9{aspect-ratio:21/9;max-height:54vh}
  486. .frame-img.r-16x10{aspect-ratio:16/10;max-height:56vh}
  487. .frame-img.r-4x3{aspect-ratio:4/3;max-height:56vh}
  488. .frame-img.r-3x2{aspect-ratio:3/2;max-height:46vh}
  489. .frame-img.r-3x4{aspect-ratio:3/4;max-height:60vh}
  490. .frame-img.r-1x1{aspect-ratio:1/1;max-height:50vh}
  491. .frame-img.h-16{height:16vh}
  492. .frame-img.h-18{height:18vh}
  493. .frame-img.h-22{height:22vh}
  494. .frame-img.h-26{height:26vh}
  495. .frame-img.h-28{height:28vh}
  496. .frame-img.h-32{height:32vh}
  497. .frame-img.swiss-lined{border-top:2px solid var(--accent)}
  498. .frame-img.swiss-keyline{border:0}
  499. /* P22 Image Hero: 下半屏内容区必须和图片拉开距离,不要贴在图下沿 */
  500. .image-hero-body{
  501. display:grid;grid-template-columns:6fr 6fr;gap:3vw;
  502. padding:6.6vh 5vw 4.4vh;flex:1;align-content:start
  503. }
  504. .image-hero-stats{
  505. display:grid;grid-template-columns:repeat(3,1fr);gap:2vw;align-items:start
  506. }
  507. .img-cap{
  508. display:block;margin-top:.8vh;
  509. font-family:var(--mono);
  510. font-size:max(14px,.82vw);
  511. letter-spacing:.2em;text-transform:uppercase;
  512. opacity:.6;
  513. }
  514. figure.frame-img,figure.tile{margin:0;display:flex;flex-direction:column;min-width:0}
  515. figure.tile > .frame-img{flex:0 0 auto}
  516. /* 瑞士风图文混排: 只用直角、发丝线、单一 accent;图像容器不加圆角/阴影 */
  517. .swiss-img-split{display:grid;grid-template-columns:5fr 7fr;gap:3vw;align-items:start;flex:1;min-height:0}
  518. .swiss-img-split.reverse{grid-template-columns:7fr 5fr}
  519. .swiss-img-split.align-bottom{align-items:end}
  520. .swiss-img-split.align-bottom .swiss-img-copy{align-self:end}
  521. .swiss-img-split.align-image-bottom{align-items:end;padding-bottom:var(--nav-safe-bottom)}
  522. .swiss-img-split.align-image-bottom .swiss-img-copy{align-self:end}
  523. .swiss-img-split.align-image-bottom figure.tile{align-self:end;position:relative}
  524. .swiss-img-split.align-image-bottom figure.tile > .swiss-img-caption{
  525. position:absolute;left:0;right:0;top:calc(100% + var(--sp-4));margin-top:0
  526. }
  527. .nav-safe-bottom{padding-bottom:var(--nav-safe-bottom)}
  528. .nav-safe-bottom-tight{padding-bottom:calc(var(--nav-safe-bottom) * .65)}
  529. .swiss-img-copy{display:flex;flex-direction:column;gap:var(--sp-6);min-width:0}
  530. .swiss-img-copy .rule{margin:0}
  531. .swiss-img-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--sp-5);align-items:start;margin-top:var(--sp-7)}
  532. .swiss-img-grid.two{grid-template-columns:repeat(2,1fr)}
  533. .swiss-img-grid.tight{margin-top:0}
  534. .swiss-img-caption{
  535. display:flex;justify-content:space-between;gap:var(--sp-5);
  536. margin-top:var(--sp-4);
  537. font-family:var(--mono);font-size:max(14px,.74vw);
  538. letter-spacing:.16em;text-transform:uppercase;color:var(--text-helper);
  539. border-top:1px solid var(--border-subtle);padding-top:var(--sp-4)
  540. }
  541. .swiss-img-caption strong{
  542. font-family:var(--sans),var(--sans-zh);font-size:max(16px,.9vw);
  543. font-weight:600;letter-spacing:0;text-transform:none;color:var(--text-primary)
  544. }
  545. /* ============ Bar Chart (扁平几何图表) ============ */
  546. .bar-chart{display:flex;flex-direction:column;gap:1.4vh}
  547. .bar-row{display:grid;grid-template-columns:8em 1fr 4em;gap:1.4vw;align-items:center}
  548. .bar-row .bar-label{font-family:var(--mono);font-size:max(14px,.84vw);letter-spacing:.14em;text-transform:uppercase;opacity:.7}
  549. .bar-row .bar-track{height:14px;background:rgba(127,127,127,.18);position:relative}
  550. .bar-row .bar-fill{height:100%;background:var(--accent);position:absolute;left:0;top:0}
  551. .bar-row .bar-fill.ink{background:currentColor}
  552. .bar-row .bar-value{font-family:var(--sans);font-weight:700;font-size:max(16px,1.05vw);text-align:right;font-feature-settings:"tnum"}
  553. /* ============ 导航 ============ */
  554. /* 底部分页导航: 仅保留方块本身,无背景无描边 */
  555. #nav{position:fixed;left:50%;bottom:2vh;transform:translateX(-50%);z-index:30;display:flex;gap:10px;padding:0;background:transparent;border:0}
  556. #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}
  557. #nav .dot:hover{background:rgba(0,0,0,.55)}
  558. #nav .dot.active{background:var(--accent);width:18px}
  559. body.dark-bg #nav .dot{background:rgba(255,255,255,.32)}
  560. body.dark-bg #nav .dot.active{background:var(--accent)}
  561. #hint{position:fixed;bottom:2.4vh;right:2.5vw;z-index:30;font-family:var(--mono);font-size:14px;letter-spacing:.14em;text-transform:uppercase;opacity:.4;color:var(--ink-tint, currentColor)}
  562. body.dark-bg #hint{color:var(--paper);opacity:.4}
  563. body.low-power #hint{color:var(--accent);opacity:.72}
  564. body.dark-bg.low-power #hint{color:var(--paper);opacity:.72}
  565. /* ESC 索引页: 动画元素强制可见 (覆盖 motion-ready 的 opacity:0) */
  566. #overview [data-anim]{opacity:1!important;transform:none!important}
  567. #overview .slide *{animation:none!important;transition:none!important}
  568. /* 统一卡片样式 token: card-fill (默认灰底 · 中性) + card-ink (反转高对比) + card-accent (单一焦点) */
  569. .card-fill{background:#f5f5f4;border:0;color:var(--text-primary)}
  570. .card-ink{background:var(--ink);border:0;color:var(--paper)}
  571. .card-ink .t-meta,.card-ink .t-cat{color:rgba(255,255,255,.6)}
  572. .card-accent{background:var(--accent);border:0;color:var(--accent-on)}
  573. .card-accent .t-meta,.card-accent .t-cat{color:var(--accent-on)}
  574. /* ============ 动效 (与原模板一致) ============ */
  575. [data-anim]{opacity:1}
  576. body.motion-ready [data-anim]{opacity:0}
  577. body.motion-ready [data-anim="left"]{transform:translateX(-24px)}
  578. body.motion-ready [data-anim="right"]{transform:translateX(24px)}
  579. body.motion-ready [data-anim="line"]{opacity:0;transform:translateY(10px)}
  580. body.motion-ready [data-animate="pipeline"] [data-anim]{opacity:.15}
  581. body.low-power #deck{transition:none!important}
  582. body.low-power *,
  583. body.low-power *::before,
  584. body.low-power *::after{animation:none!important;transition:none!important}
  585. body.low-power.motion-ready [data-anim],
  586. body.low-power [data-anim]{opacity:1!important;transform:none!important}
  587. /* ============================================================
  588. ↓↓↓ V2 EXTENSIONS · Canvas Mode + 参考图新类
  589. 验证效果后沉淀回 template-swiss.html
  590. ============================================================ */
  591. /* Windows 适配:雅黑没有 ExtraLight 200,中文大字号字重补偿 */
  592. body.is-win .name-mega,
  593. body.is-win .num-mega,
  594. body.is-win .kpi-thin,
  595. body.is-win .tl-node .multi{
  596. font-weight:300;letter-spacing:-.02em;
  597. }
  598. body.is-win [style*="font-weight:200"]{font-weight:300 !important}
  599. /* 全屏铺满模式 · 关闭 WebGL · 卡片即页面 */
  600. body.canvas-mode{background:var(--paper)}
  601. body.canvas-mode canvas.bg{display:none !important}
  602. body.canvas-mode .slide{background:var(--paper);padding:0;align-items:stretch;justify-content:stretch}
  603. body.canvas-mode .slide.hero{background:var(--paper)}
  604. .canvas-card{
  605. width:100vw;
  606. height:100vh;
  607. background:var(--paper);color:var(--ink);
  608. padding:5.6vh 5vw 4.4vh;
  609. display:flex;flex-direction:column;
  610. position:relative;overflow:hidden;
  611. box-shadow:none;
  612. border-radius:0;
  613. }
  614. .slide.dark .canvas-card{background:var(--ink);color:var(--paper)}
  615. .slide.accent .canvas-card{background:var(--accent);color:var(--accent-on)}
  616. .slide.grey .canvas-card{background:var(--grey-1);color:var(--ink)}
  617. .slide.split .canvas-card{padding:0;flex-direction:row}
  618. /* ============ ASCII 点阵呼吸场 · IKB 封面/封底专用 ============
  619. 用法:在 .canvas-card(或 split .half.b-accent)内首位插入 <canvas class="ascii-bg" aria-hidden="true">.
  620. 动画由本文件底部的 ASCII IIFE 自动启动,所有 canvas.ascii-bg 都会被扫到.
  621. 其他内容靠 .canvas-card > *:not(.ascii-bg){z-index:1} 自动浮在上层. */
  622. canvas.ascii-bg{
  623. position:absolute;inset:0;width:100%;height:100%;
  624. pointer-events:none;z-index:0;
  625. mix-blend-mode:screen;opacity:.92;
  626. }
  627. .canvas-card > *:not(.ascii-bg){position:relative;z-index:1}
  628. .slide.accent .canvas-card .chrome-min{color:rgba(255,255,255,.62)}
  629. .slide.accent .canvas-card .t-meta{color:rgba(255,255,255,.7)}
  630. .split-half > .half.b-accent .chrome-min{color:rgba(255,255,255,.62)}
  631. /* 编号目录页 · 超大数字 + 国家名风格 — 越大越细 */
  632. .num-mega{
  633. font-family:var(--sans);font-weight:200;
  634. font-size:9vw;line-height:1;letter-spacing:-.04em;
  635. font-feature-settings:"tnum"
  636. }
  637. .num-mega.thin{font-weight:200}
  638. .name-mega{
  639. font-family:var(--sans);font-weight:200;
  640. font-size:9vw;line-height:1;letter-spacing:-.035em;
  641. }
  642. .name-mega.muted{color:var(--grey-3)}
  643. /* 细体超大 KPI — 字号越大权重越低 */
  644. .kpi-thin{
  645. font-family:var(--sans);font-weight:200;
  646. font-size:14vw;line-height:.92;letter-spacing:-.045em;
  647. font-feature-settings:"tnum"
  648. }
  649. .kpi-thin .unit{font-size:.3em;font-weight:300;opacity:.55;margin-left:.15em;vertical-align:.6em}
  650. .kpi-thin.accent{color:var(--accent)}
  651. .kpi-thin-sm{
  652. font-family:var(--sans);font-weight:250;
  653. font-size:5.6vw;line-height:1.04;letter-spacing:-.03em;
  654. font-feature-settings:"tnum"
  655. }
  656. .kpi-thin-sm .unit{font-size:.3em;font-weight:300;opacity:.55;margin-left:.12em;vertical-align:.45em}
  657. /* 4 列细线 KPI 行 — 顶部一根 hairline,内部不再加竖线 */
  658. /* 4 列 KPI: 用纵向分割线建立网格感 (Carbon 2x grid 模数) */
  659. .kpi-row-4{
  660. display:grid;grid-template-columns:repeat(4,1fr);
  661. gap:0;padding-top:2.4vh;
  662. border-top:1px solid var(--grey-2)
  663. }
  664. .kpi-row-4 > .kpi-cell{
  665. padding:1.6vh 1.6vw 0;
  666. border-left:1px solid var(--grey-2)
  667. }
  668. .kpi-row-4 > .kpi-cell:first-child{padding-left:0;border-left:none}
  669. .kpi-cell .lbl{font-family:var(--mono);font-size:max(14px,.78vw);letter-spacing:.18em;text-transform:uppercase;opacity:.55;margin-bottom:1.2vh}
  670. .kpi-cell .nb{font-family:var(--sans);font-weight:250;font-size:3.2vw;line-height:1;letter-spacing:-.025em;font-feature-settings:"tnum"}
  671. .kpi-cell .nb .unit{font-size:.32em;font-weight:300;opacity:.6;margin-left:.1em;vertical-align:.4em}
  672. .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}
  673. /* 时间线轴 — 通用 axis token, 横纵共享
  674. axis 列 = 24px 固定宽,dot 直径 8px,绝对居中在 axis 列中线 (12px)
  675. 虚线绝对定位 left:12px,与 dot 中心严格对齐 */
  676. .timeline-v{
  677. --tl-axis-w:24px; /* axis 列固定宽度 */
  678. --tl-dot:8px; /* 圆点直径 */
  679. position:relative;margin-top:var(--sp-7)
  680. }
  681. .timeline-v::before{
  682. content:"";position:absolute;
  683. left:calc(var(--tl-axis-w) / 2);
  684. transform:translateX(-50%);
  685. top:var(--sp-5);bottom:var(--sp-5);
  686. width:1px;
  687. background:repeating-linear-gradient(to bottom,currentColor 0 4px,transparent 4px 8px);
  688. opacity:.35;pointer-events:none;z-index:0
  689. }
  690. .tl-node{
  691. position:relative;
  692. display:grid;
  693. grid-template-columns:var(--tl-axis-w) minmax(0,7em) minmax(0,7.6em) 1fr;
  694. gap:0 var(--sp-5);
  695. align-items:center;
  696. padding:var(--sp-7) 0
  697. }
  698. /* 进度点: 纯色实心圆,无背景无描边,精确居中在 axis 列中线 */
  699. .tl-node .dot{
  700. width:var(--tl-dot);height:var(--tl-dot);
  701. border-radius:50%;
  702. background:currentColor;
  703. justify-self:center;
  704. z-index:1
  705. }
  706. .tl-node.accent .dot{background:var(--accent)}
  707. .tl-node .yr{
  708. font-family:var(--mono);font-weight:500;
  709. font-size:max(14px,1vw);letter-spacing:.04em
  710. }
  711. .tl-node .multi{
  712. font-family:var(--sans);font-weight:200;
  713. font-size:clamp(28px,2.8vw,56px);
  714. line-height:.95;letter-spacing:-.025em;
  715. white-space:nowrap;
  716. overflow:hidden;
  717. min-width:0
  718. }
  719. .tl-node .multi .unit{
  720. font-size:.36em;font-weight:300;opacity:.6;
  721. margin-left:.2em;
  722. vertical-align:.42em;
  723. letter-spacing:0
  724. }
  725. .tl-node.accent .multi{color:var(--accent)}
  726. .tl-node .desc{
  727. font-family:var(--sans),var(--sans-zh);
  728. font-size:max(16px,.94vw);line-height:1.55;opacity:.78;
  729. min-width:0
  730. }
  731. /* 横向时间线 (.timeline-h) — 与 .timeline-v 共享 axis token, 视觉语言一致 */
  732. .timeline-h{
  733. --tl-axis-w:8px;
  734. --tl-dot:8px;
  735. position:relative;
  736. flex:1;
  737. display:flex;align-items:center
  738. }
  739. .timeline-h::before{
  740. content:"";position:absolute;
  741. top:50%;left:5%;right:5%;height:1px;
  742. transform:translateY(-50%);
  743. background:repeating-linear-gradient(to right,currentColor 0 4px,transparent 4px 8px);
  744. opacity:.35;pointer-events:none;z-index:0
  745. }
  746. .timeline-h .tl-row{
  747. position:relative;width:100%;
  748. display:grid;grid-template-columns:repeat(5,1fr);
  749. align-items:center
  750. }
  751. .timeline-h .th-node{
  752. position:relative;display:flex;justify-content:center
  753. }
  754. /* 横向 dot: 8px 纯色实心,无描边无阴影 (与 P2 保持一致) */
  755. .timeline-h .th-node .dot{
  756. width:var(--tl-dot);height:var(--tl-dot);
  757. border-radius:50%;
  758. background:var(--ink);
  759. z-index:1;position:relative
  760. }
  761. .timeline-h .th-node.accent .dot{background:var(--accent)}
  762. .timeline-h .th-node .label{
  763. position:absolute;left:50%;transform:translateX(-50%);
  764. width:13vw;text-align:center;
  765. display:flex;flex-direction:column;gap:.4vh
  766. }
  767. .timeline-h .th-node.up .label{bottom:calc(50% + 22px)}
  768. .timeline-h .th-node.down .label{top:calc(50% + 22px)}
  769. .timeline-h .th-node .yr{
  770. font-family:var(--mono);font-size:max(14px,.78vw);letter-spacing:.05em;
  771. color:var(--text-helper);font-weight:500
  772. }
  773. .timeline-h .th-node.accent .yr{color:var(--accent)}
  774. .timeline-h .th-node .name{
  775. font-family:var(--sans);font-size:max(16px,1.05vw);font-weight:400;
  776. color:var(--text-primary);line-height:1.2;letter-spacing:-.005em
  777. }
  778. .timeline-h .th-node.accent .name{color:var(--accent)}
  779. .timeline-h .th-node .desc{
  780. font-family:var(--sans),var(--sans-zh);font-size:max(14px,.84vw);
  781. color:var(--text-secondary);font-weight:400;line-height:1.4
  782. }
  783. /* 几何图示 (参考图 5) — SVG-friendly 容器 */
  784. .geo-icon{width:5vw;height:5vw;display:block;margin-bottom:2.2vh;color:var(--ink);flex-shrink:0}
  785. .slide.dark .geo-icon{color:var(--paper)}
  786. .geo-icon svg{width:100%;height:100%;overflow:visible}
  787. .geo-icon .stroke{fill:none;stroke:currentColor;stroke-width:1.4}
  788. .geo-icon .stroke-accent{fill:none;stroke:var(--accent);stroke-width:1.4}
  789. .geo-icon .fill-accent{fill:var(--accent)}
  790. /* 卡片网格 (参考图 1 卡片漂浮) — 在 canvas-card 内部再分卡 */
  791. .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}
  792. .sub-card{
  793. background:var(--grey-1);
  794. padding:2.4vh 1.6vw 2vh;
  795. display:flex;flex-direction:column;
  796. position:relative;border-radius:3px;
  797. min-height:0
  798. }
  799. .slide.dark .sub-card{background:rgba(255,255,255,.06)}
  800. .sub-card.accent{background:var(--accent);color:var(--accent-on)}
  801. .sub-card.ink{background:var(--ink);color:var(--paper)}
  802. .sub-card .nb-corner{
  803. position:absolute;top:1.6vh;right:1.4vw;
  804. font-family:var(--mono);font-size:max(14px,.8vw);
  805. letter-spacing:.16em;opacity:.55
  806. }
  807. .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}
  808. .sub-card .desc{font-family:var(--sans),var(--sans-zh);font-size:max(16px,.94vw);line-height:1.55;opacity:.78;margin-top:auto}
  809. .sub-card .lucide{width:2.4vw;height:2.4vw;stroke-width:1.4;color:currentColor;margin-bottom:1.6vh;flex-shrink:0}
  810. .sub-card.accent .lucide{color:var(--accent-on)}
  811. /* 三层架构纯色块拼图 (参考图 4 + 6) — 横排等高色块 */
  812. .stack-row{display:grid;grid-template-columns:repeat(3,1fr);gap:1.6vw;flex:1;margin-top:6vh;align-items:stretch}
  813. .stack-block{
  814. display:flex;flex-direction:column;
  815. padding:3.2vh 1.8vw 2.4vh;
  816. position:relative;
  817. min-height:0
  818. }
  819. .stack-block.b-accent{background:var(--accent);color:var(--accent-on)}
  820. .stack-block.b-grey{background:var(--grey-1);color:var(--ink)}
  821. .stack-block.b-ink{background:var(--ink);color:var(--paper)}
  822. .stack-block .layer-nb{font-family:var(--mono);font-size:max(14px,.84vw);letter-spacing:.18em;opacity:.65;margin-bottom:auto}
  823. .stack-block .layer-icon{margin-bottom:1.6vh}
  824. .stack-block .layer-icon svg{width:3vw;height:3vw}
  825. .stack-block .layer-icon .stroke{fill:none;stroke:currentColor;stroke-width:1.6}
  826. .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}
  827. .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}
  828. .stack-block .lucide{width:2.6vw;height:2.6vw;stroke-width:1.4;margin-bottom:1.6vh;flex-shrink:0}
  829. .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}
  830. /* 不等高柱状 KPI 塔 (参考图 6) */
  831. .bar-towers{
  832. display:grid;grid-template-columns:repeat(4,1fr);
  833. gap:1.2vw;flex:1;align-items:end;margin-top:auto
  834. }
  835. .bar-tower{
  836. display:flex;flex-direction:column;justify-content:flex-end;
  837. min-height:0;height:100%
  838. }
  839. .bar-tower .cap{
  840. background:var(--grey-1);
  841. height:5.6vh;
  842. display:flex;align-items:center;justify-content:center;
  843. margin-bottom:.4vh
  844. }
  845. .bar-tower .cap svg{width:1.6vw;height:1.6vw;stroke:currentColor;fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
  846. .bar-tower .body-block{
  847. flex:0 1 auto;
  848. padding:2vh 1.2vw 2vh;
  849. display:flex;flex-direction:column;justify-content:flex-end;
  850. min-height:18vh
  851. }
  852. .bar-tower .body-block.h-1{min-height:22vh}
  853. .bar-tower .body-block.h-2{min-height:30vh}
  854. .bar-tower .body-block.h-3{min-height:38vh}
  855. .bar-tower .body-block.h-4{min-height:46vh}
  856. /* 默认所有 KPI 塔统一为浅描边卡 — 不抢戏;只有 .b-accent 突出为 IKB */
  857. .bar-tower .body-block{background:var(--paper);color:var(--ink);border:1px solid var(--grey-2)}
  858. .bar-tower .body-block.b-accent{background:var(--accent);color:var(--accent-on);border-color:var(--accent)}
  859. .bar-tower .lbl{font-family:var(--mono);font-size:max(14px,.82vw);letter-spacing:.16em;text-transform:uppercase;opacity:.65;margin-bottom:1vh}
  860. .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"}
  861. .bar-tower .body-block.b-accent .nb{font-weight:300}
  862. .bar-tower .nb .unit{font-size:.36em;font-weight:300;opacity:.7;margin-left:.08em;vertical-align:.4em}
  863. .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}
  864. .bar-tower .cap{background:var(--grey-1);color:var(--ink)}
  865. .bar-tower .lucide{width:1.6vw;height:1.6vw;stroke-width:1.4}
  866. /* ============ Carbon Productive Type Tokens ============
  867. 用于 PPT 中的"productive 时刻": 列表、表格行、说明文、章节标签
  868. 固定 px 字号,密集紧凑;与 vw-based 的 Expressive 巨字形成双极对比 */
  869. /* category label / eyebrow / section tag — small, bold, uppercase */
  870. .t-cat{
  871. font-family:var(--mono);
  872. font-size:14px;font-weight:600;
  873. letter-spacing:.15em;text-transform:uppercase;
  874. color:var(--text-helper);line-height:1.3
  875. }
  876. .t-cat.accent{color:var(--accent)}
  877. .t-cat.on-dark{color:rgba(255,255,255,.78)}
  878. /* page chrome / breadcrumb / running header — extra-small mono */
  879. .t-meta{
  880. font-family:var(--mono);
  881. font-size:14px;font-weight:500;
  882. letter-spacing:.14em;text-transform:uppercase;
  883. color:var(--text-helper);line-height:1.45
  884. }
  885. /* helper / caption — secondary text */
  886. .t-helper{
  887. font-family:var(--sans),var(--sans-zh);
  888. font-size:14px;font-weight:400;
  889. color:var(--text-helper);line-height:1.5;
  890. letter-spacing:.005em
  891. }
  892. /* body small — list items, table rows, captions */
  893. .t-body-sm{
  894. font-family:var(--sans),var(--sans-zh);
  895. font-size:16px;font-weight:400;
  896. color:var(--text-secondary);line-height:1.55;
  897. letter-spacing:0
  898. }
  899. /* body — paragraphs, descriptions */
  900. .t-body{
  901. font-family:var(--sans),var(--sans-zh);
  902. font-size:18px;font-weight:400;
  903. color:var(--text-primary);line-height:1.5;
  904. letter-spacing:-.005em
  905. }
  906. /* body emphasis — 强调正文 */
  907. .t-body-emp{
  908. font-family:var(--sans),var(--sans-zh);
  909. font-size:18px;font-weight:600; /* SemiBold per Carbon */
  910. color:var(--text-primary);line-height:1.5;
  911. letter-spacing:-.005em
  912. }
  913. /* productive heading — section title within slide */
  914. .t-h-prod{
  915. font-family:var(--sans),var(--sans-zh);
  916. font-size:22px;font-weight:600;
  917. color:var(--text-primary);line-height:1.4;
  918. letter-spacing:-.01em
  919. }
  920. /* dark background variants */
  921. .slide.dark .t-cat,.slide.dark .t-meta,.slide.dark .t-helper{color:rgba(255,255,255,.62)}
  922. .slide.dark .t-body-sm{color:rgba(255,255,255,.78)}
  923. .slide.dark .t-body,.slide.dark .t-body-emp,.slide.dark .t-h-prod{color:var(--paper)}
  924. .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)}
  925. .split-half .half.b-ink .t-body-sm{color:rgba(255,255,255,.78)}
  926. .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)}
  927. /* 斜杠点阵装饰 (参考图 6 左下) */
  928. .hatch{
  929. background-image:repeating-linear-gradient(135deg,currentColor 0 1px,transparent 1px 8px);
  930. opacity:.55
  931. }
  932. /* ========== V3: 高级点阵装饰 — 圆点 / × 号 / 圆圈 ========== */
  933. /* 实心圆点矩阵 — 用于大面积装饰 */
  934. .dot-mat{
  935. --d:14px;
  936. background-image:radial-gradient(currentColor 1.4px,transparent 1.6px);
  937. background-size:var(--d) var(--d);
  938. background-position:0 0;
  939. opacity:.5
  940. }
  941. .dot-mat.lg{--d:22px}
  942. .dot-mat.xl{--d:34px}
  943. .dot-mat.dense{--d:9px;opacity:.62}
  944. /* 描边圆圈矩阵 — 工业感 */
  945. .ring-mat{
  946. --d:18px;
  947. background-image:
  948. radial-gradient(circle at 50% 50%,transparent 2px,currentColor 2px,currentColor 2.6px,transparent 2.7px);
  949. background-size:var(--d) var(--d);
  950. opacity:.55
  951. }
  952. .ring-mat.lg{--d:28px}
  953. /* × 号矩阵 — SVG mask 实现真正的 × 网格平铺 */
  954. .cross-mat{
  955. --d:22px;
  956. background-color:currentColor;
  957. -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>");
  958. 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>");
  959. -webkit-mask-size:var(--d) var(--d);
  960. mask-size:var(--d) var(--d);
  961. -webkit-mask-repeat:repeat;mask-repeat:repeat;
  962. opacity:.42
  963. }
  964. .cross-mat.lg{--d:32px}
  965. /* ========== V3: 几何水平柱状图 ========== */
  966. /* 横向柱图: 标签列 + 柱体列 + 数值列 — 严格瑞士网格 */
  967. .h-bar-chart{
  968. display:grid;
  969. grid-template-columns:11em minmax(0,1fr) 8em;
  970. gap:1.6vh 1.6vw;
  971. align-items:center;
  972. margin-top:2.4vh;
  973. font-feature-settings:"tnum"
  974. }
  975. .h-bar-chart .row-lbl{
  976. font-family:var(--sans),var(--sans-zh);
  977. font-weight:500;
  978. font-size:max(14px,1vw);
  979. letter-spacing:-.005em;
  980. text-align:left
  981. }
  982. .h-bar-chart .row-track{
  983. height:3.2vh;
  984. background:var(--grey-1);
  985. position:relative;
  986. overflow:hidden
  987. }
  988. .h-bar-chart .row-fill{
  989. height:100%;
  990. background:var(--ink);
  991. transition:width 1s cubic-bezier(.5,0,.2,1)
  992. }
  993. .h-bar-chart .row-fill.accent{background:var(--accent)}
  994. .h-bar-chart .row-fill.grey{background:var(--grey-3)}
  995. .h-bar-chart .row-val{
  996. font-family:var(--sans);font-weight:250;
  997. font-size:max(16px,1.5vw);
  998. letter-spacing:-.02em;
  999. line-height:1
  1000. }
  1001. .h-bar-chart .row-val .unit{
  1002. font-size:.5em;opacity:.55;font-weight:300;
  1003. margin-left:.15em;letter-spacing:.04em
  1004. }
  1005. /* 垂直柱图: 用于 KPI 对比页(章节 P8) */
  1006. .v-bar-chart{
  1007. display:grid;
  1008. grid-template-columns:repeat(var(--cols,4),1fr);
  1009. align-items:end;
  1010. gap:1.4vw;
  1011. height:50vh;
  1012. margin-top:3vh
  1013. }
  1014. .v-bar-chart .col{display:flex;flex-direction:column;gap:1.4vh;align-items:stretch;height:100%}
  1015. .v-bar-chart .col-bar{
  1016. flex:1 1 auto;
  1017. background:var(--grey-1);
  1018. border-top:2px solid var(--ink);
  1019. position:relative;
  1020. display:flex;align-items:flex-start;justify-content:center;
  1021. padding-top:1vh
  1022. }
  1023. .v-bar-chart .col-bar.accent{background:var(--accent);border-top-color:var(--accent);color:var(--accent-on)}
  1024. .v-bar-chart .col-bar.ink{background:var(--ink);color:var(--paper);border-top-color:var(--ink)}
  1025. .v-bar-chart .col-bar .v{
  1026. font-family:var(--sans);font-weight:250;
  1027. font-size:max(18px,1.6vw);letter-spacing:-.02em
  1028. }
  1029. .v-bar-chart .col-lbl{
  1030. font-family:var(--mono);font-size:max(14px,.78vw);
  1031. letter-spacing:.14em;text-transform:uppercase;opacity:.6;
  1032. text-align:center;flex:0 0 auto
  1033. }
  1034. /* 对仗对比双栏 (章节 P9) */
  1035. .duo-compare{
  1036. display:grid;grid-template-columns:1fr 1px 1fr;
  1037. gap:0 3.4vw;
  1038. flex:1;align-items:stretch;margin-top:8vh
  1039. }
  1040. .duo-compare .vrule{background:var(--grey-2);width:1px;align-self:stretch}
  1041. .duo-compare .col{display:flex;flex-direction:column;gap:1.6vh;padding:0 .4vw}
  1042. .duo-compare .col-tag{
  1043. font-family:var(--mono);font-size:max(14px,.78vw);
  1044. letter-spacing:.16em;text-transform:uppercase;
  1045. color:var(--grey-3);
  1046. display:flex;align-items:center;gap:.6vw
  1047. }
  1048. .duo-compare .col-tag .num{
  1049. font-weight:600;color:var(--ink);
  1050. border:1px solid var(--ink);
  1051. padding:.2em .6em;font-size:.92em
  1052. }
  1053. .duo-compare .col.accent .col-tag .num{color:var(--accent);border-color:var(--accent)}
  1054. .duo-compare .col-ttl{
  1055. font-family:var(--sans),var(--sans-zh);font-weight:200;
  1056. font-size:3.6vw;line-height:1;letter-spacing:-.03em
  1057. }
  1058. .duo-compare .col.accent .col-ttl{color:var(--accent)}
  1059. .duo-compare .col-desc{
  1060. font-family:var(--sans),var(--sans-zh);
  1061. font-size:max(16px,1.04vw);line-height:1.55;opacity:.78;
  1062. max-width:30vw
  1063. }
  1064. .duo-compare .col-list{
  1065. list-style:none;display:flex;flex-direction:column;gap:1vh;
  1066. margin-top:auto;padding-top:2vh;border-top:1px solid var(--grey-2)
  1067. }
  1068. .duo-compare .col-list li{
  1069. font-family:var(--sans),var(--sans-zh);
  1070. font-size:max(16px,.94vw);line-height:1.5;
  1071. padding-left:1.4em;position:relative
  1072. }
  1073. .duo-compare .col-list li::before{
  1074. content:"";position:absolute;left:0;top:.6em;
  1075. width:.5em;height:1px;background:currentColor;opacity:.55
  1076. }
  1077. .duo-compare .col.accent .col-list li::before{background:var(--accent);opacity:1}
  1078. /* 半屏 statement (参考图 7) */
  1079. .split-half{display:grid;grid-template-columns:1fr 1fr;gap:0;flex:1;align-items:stretch}
  1080. .split-half > .half{padding:5vh 3.4vw;display:flex;flex-direction:column;min-width:0}
  1081. .split-half > .half.r-border{border-left:1px solid rgba(127,127,127,.22)}
  1082. .split-half > .half.b-grey{background:var(--grey-1)}
  1083. .split-half > .half.b-accent{background:var(--accent);color:var(--accent-on)}
  1084. .split-half > .half.b-ink{background:var(--ink);color:var(--paper)}
  1085. /* 极简页眉 — t-meta 风格 (Carbon productive label-01) */
  1086. .canvas-card .chrome-min{
  1087. display:flex;justify-content:space-between;align-items:flex-start;
  1088. font-family:var(--mono);font-size:14px;font-weight:500;
  1089. letter-spacing:.14em;text-transform:uppercase;
  1090. color:var(--text-helper);
  1091. flex:0 0 auto;
  1092. margin-bottom:var(--sp-9); /* 48px */
  1093. }
  1094. .canvas-card .chrome-min.tight{margin-bottom:var(--sp-7)} /* 32px */
  1095. .canvas-card .chrome-min .l, .canvas-card .chrome-min .r{
  1096. max-width:48vw;line-height:1.5
  1097. }
  1098. .slide.dark .canvas-card .chrome-min,.split-half .half.b-ink .canvas-card .chrome-min{color:rgba(255,255,255,.62)}
  1099. /* 响应式降级 */
  1100. @media (max-width:900px){
  1101. .h-hero{font-size:16vw}
  1102. .h-hero-zh{font-size:13vw}
  1103. .h-xl{font-size:9vw}
  1104. .h-xl-zh{font-size:8vw}
  1105. .kpi-hero{font-size:32vw}
  1106. .kpi-big{font-size:16vw}
  1107. .pipeline{grid-template-columns:repeat(2,1fr)}
  1108. .grid-2-7-5,.grid-2-6-6,.grid-2-8-4,.grid-2-4-8{grid-template-columns:1fr}
  1109. .grid-12{grid-template-columns:repeat(6,1fr)}
  1110. }
  1111. </style>
  1112. </head>
  1113. <body class="canvas-mode">
  1114. <script>
  1115. // Windows 平台标记 — 雅黑没有 ExtraLight,需要字重补偿
  1116. if(/Win/i.test(navigator.platform || navigator.userAgentData?.platform || '')){
  1117. document.body.classList.add('is-win');
  1118. }
  1119. (function(){
  1120. const KEY = 'guizang-ppt-low-power';
  1121. const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
  1122. const stored = localStorage.getItem(KEY);
  1123. window.__lowPowerMode = stored === '1' || (stored === null && reduced);
  1124. function updateHint(){
  1125. const hint = document.getElementById('hint');
  1126. if(hint) hint.textContent = `← → 翻页 · B ${window.__lowPowerMode ? '动态' : '静态'} · ESC 索引`;
  1127. }
  1128. window.__setLowPowerMode = function(on, opts={}){
  1129. window.__lowPowerMode = !!on;
  1130. document.body.classList.toggle('low-power', window.__lowPowerMode);
  1131. if(opts.persist !== false) localStorage.setItem(KEY, window.__lowPowerMode ? '1' : '0');
  1132. if(window.__lowPowerMode && document.getAnimations){
  1133. document.getAnimations().forEach(a=>a.cancel());
  1134. }
  1135. updateHint();
  1136. dispatchEvent(new CustomEvent('swiss-low-power-change', {detail:{on:window.__lowPowerMode}}));
  1137. if(window.__playSlide) window.__playSlide(window.__currentSlideIndex || 0);
  1138. };
  1139. document.body.classList.toggle('low-power', window.__lowPowerMode);
  1140. addEventListener('DOMContentLoaded', updateHint, {once:true});
  1141. })();
  1142. </script>
  1143. <canvas id="bg-grid" class="bg"></canvas>
  1144. <div id="hint">← → 翻页 · B 静态 · ESC 索引</div>
  1145. <div id="deck">
  1146. <!-- ============================================================
  1147. SLIDES 插入区 · 在此处填充所有 <section class="slide ..."> 页面
  1148. 页面骨架参考 references/layouts-swiss.md
  1149. 主题色配置参考 references/themes-swiss.md
  1150. ============================================================ -->
  1151. <!-- SLIDES_HERE · 在此处粘贴 <section class="slide ..."> 页面块
  1152. - 页面骨架直接从 references/layouts-swiss.md 拷贝
  1153. - data-animate="..." 必须命中下方 RECIPES 字典里的已有 recipe 名之一(P23/P24 可复用 grid-reveal)
  1154. - 主题色配置参考 references/themes-swiss.md (默认 IKB 克莱因蓝) -->
  1155. <!-- ============ 示例:第 1 页 · Hero Cover · IKB 满屏 + ASCII 呼吸场(默认推荐) ============
  1156. ⚠️ P0 对齐法则(每页都要过):
  1157. 1. .canvas-card 已自带 padding:5.6vh 5vw 4.4vh,所有页面内容直接放在 canvas-card 子元素里,
  1158. 子元素**不要再加水平 padding**,否则会比 chrome-min 内缩一圈、左右不对齐.
  1159. 2. .slide.split .canvas-card{padding:0} 已被 CSS 覆盖,
  1160. split 模式下两个 .half 自己控制 padding(常用 5.6vh 3.6vw 4.4vh),与本规则不冲突.
  1161. 3. 大字号一律用双约束 font-size:min(Xvw, Yvh),Y ≥ X * 1.6 才不会在 16:9 屏被高度截断.
  1162. 4. kicker 必须在大标题"上方",不是左侧——禁止 grid-template-columns:auto 1fr 把它们压成左右.
  1163. 封面/封底设计语言(默认 IKB 满屏 + ASCII 呼吸场):
  1164. - section 用 .slide.accent (满屏 IKB,不是 light 白底)
  1165. - canvas-card 内首位插入 <canvas class="ascii-bg" aria-hidden="true">,本文件底部 IIFE 自动启动
  1166. - 主标题反白 weight 200,强调字用斜体而非 var(--accent)(底已是蓝,蓝压蓝看不见)
  1167. - 不要再放编号大字"01"——chrome-min 已经标 01/NN -->
  1168. <section class="slide accent" data-animate="hero">
  1169. <div class="canvas-card">
  1170. <canvas class="ascii-bg" aria-hidden="true"></canvas>
  1171. <div class="chrome-min">
  1172. <div class="l">[必填] Deck 标题 · Issue/Field Note 编号</div>
  1173. <div class="r">SS · 25.05.10 · 01 / NN</div>
  1174. </div>
  1175. <!-- 主体:padding 必须为 0,不要再叠 5vw,否则左右对不齐 chrome-min -->
  1176. <div style="flex:1;padding:0;display:grid;grid-template-rows:auto 1fr auto;gap:2.6vh">
  1177. <div data-anim="kicker" class="t-meta" style="color:rgba(255,255,255,.78);letter-spacing:.22em">[必填] 章节英文 / Section En</div>
  1178. <h1 data-anim="title" style="align-self:center;font-family:var(--sans),var(--sans-zh);font-weight:200;font-size:min(11.6vw,19vh);line-height:.94;letter-spacing:-.025em;color:#fff">[必填] 中文主标题<br/>(≤ 12 字,可在某字加 <span style="font-style:italic;font-weight:300">italic</span> 微强调)</h1>
  1179. <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">
  1180. <div data-anim="lead" class="lead" style="max-width:52ch;color:rgba(255,255,255,.86)">[必填] 一段 1-2 行的副标 / 引子,定调全场.</div>
  1181. <div style="display:flex;justify-content:space-between;align-items:end">
  1182. <div class="t-meta" style="color:rgba(255,255,255,.6)">[选填] 作者 · 日期 · 出处</div>
  1183. <div class="t-meta" style="color:rgba(255,255,255,.6)">→ swipe / arrow keys</div>
  1184. </div>
  1185. </div>
  1186. </div>
  1187. </div>
  1188. </section>
  1189. <!-- ============ 示例:最后一页 · Closing Manifesto · 左 IKB+ASCII / 右白底 takeaway ============
  1190. 与封面 IKB 首尾呼应,但收束更克制:左半保留 ASCII 呼吸场承载宣言,右半白底列 3 条 takeaway.
  1191. 第 03 条用 var(--accent) IKB 蓝强调("单一锚点"原则),首尾形成"色彩闭环". -->
  1192. <section class="slide split" data-animate="split-statement">
  1193. <div class="canvas-card">
  1194. <div class="split-half">
  1195. <!-- 左半 · IKB 宣言 + ASCII 呼吸场 -->
  1196. <div class="half b-accent" style="padding:5.6vh 3.6vw 4.4vh;justify-content:space-between;position:relative;overflow:hidden">
  1197. <canvas class="ascii-bg" aria-hidden="true"></canvas>
  1198. <div class="chrome-min" style="margin-bottom:0;position:relative;z-index:1">
  1199. <div class="l">NN / NN</div>
  1200. <div class="r">CLOSING</div>
  1201. </div>
  1202. <div data-anim="manifesto" style="display:flex;flex-direction:column;gap:2vh;position:relative;z-index:1">
  1203. <div class="t-meta" style="color:rgba(255,255,255,.78);letter-spacing:.22em;margin-bottom:1.6vh">MANIFESTO</div>
  1204. <h2 style="font-family:var(--sans),var(--sans-zh);font-size:min(8vw,14vh);line-height:.94;letter-spacing:-.025em;font-weight:200;color:#fff">[必填] Build a model.<br/>Run <span style="font-style:italic;font-weight:300">forever</span>.</h2>
  1205. <div style="font-family:var(--sans),var(--sans-zh);font-size:max(14px,1vw);line-height:1.6;color:rgba(255,255,255,.82);font-weight:400;max-width:36ch;margin-top:1.4vh">[必填] 一句 1-2 行的中文/英文注脚,把宣言落地.</div>
  1206. </div>
  1207. <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">
  1208. <div class="t-meta" style="color:rgba(255,255,255,.62)">[选填] 作者 · 头衔</div>
  1209. <div class="t-meta" style="color:rgba(255,255,255,.62)">YY.MM.DD</div>
  1210. </div>
  1211. </div>
  1212. <!-- 右半 · 三条 takeaway · 白底承载理性收束 -->
  1213. <div class="half" style="padding:5.6vh 3.6vw 4.4vh;justify-content:space-between">
  1214. <div class="chrome-min">
  1215. <div class="l">TAKEAWAYS</div>
  1216. <div class="r">03 RULES</div>
  1217. </div>
  1218. <div data-anim="rules" style="display:flex;flex-direction:column;gap:0">
  1219. <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)">
  1220. <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>
  1221. <div>
  1222. <h3 style="font-family:var(--sans),var(--sans-zh);font-weight:400;font-size:max(18px,1.8vw);line-height:1.2;letter-spacing:-.015em;color:var(--text-primary);margin-bottom:1vh">[必填] takeaway 标题 01</h3>
  1223. <p style="font-family:var(--sans),var(--sans-zh);font-size:max(16px,.94vw);line-height:1.6;color:var(--text-secondary);font-weight:400">[必填] 1-2 行展开说明.</p>
  1224. </div>
  1225. </div>
  1226. <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)">
  1227. <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>
  1228. <div>
  1229. <h3 style="font-family:var(--sans),var(--sans-zh);font-weight:400;font-size:max(18px,1.8vw);line-height:1.2;letter-spacing:-.015em;color:var(--text-primary);margin-bottom:1vh">[必填] takeaway 标题 02</h3>
  1230. <p style="font-family:var(--sans),var(--sans-zh);font-size:max(16px,.94vw);line-height:1.6;color:var(--text-secondary);font-weight:400">[必填] 1-2 行展开说明.</p>
  1231. </div>
  1232. </div>
  1233. <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)">
  1234. <div style="font-family:var(--sans);font-weight:200;font-size:min(4.4vw,7.8vh);line-height:.9;color:var(--accent)">03</div>
  1235. <div>
  1236. <h3 style="font-family:var(--sans),var(--sans-zh);font-weight:400;font-size:max(18px,1.8vw);line-height:1.2;letter-spacing:-.015em;color:var(--accent);margin-bottom:1vh">[必填] takeaway 标题 03 · accent 强调</h3>
  1237. <p style="font-family:var(--sans),var(--sans-zh);font-size:max(16px,.94vw);line-height:1.6;color:var(--text-secondary);font-weight:400">[必填] 最后一条用 IKB 强调,与封面色彩首尾闭环.</p>
  1238. </div>
  1239. </div>
  1240. </div>
  1241. <div data-anim="foot" class="t-meta" style="color:var(--text-helper);text-align:right">→ 完 · END OF FIELD NOTE</div>
  1242. </div>
  1243. </div>
  1244. </div>
  1245. </section>
  1246. </div>
  1247. <div id="nav"></div>
  1248. <script>
  1249. /* =============== WebGL 网格背景 (瑞士风专用) ===============
  1250. 极简移动网格 + 微弱点阵叠加,营造"工业感、精准感"
  1251. - 主网格: 缓慢漂移的细线网格
  1252. - 次级: 鼠标附近的极细点阵微扰
  1253. - 颜色: 跟随主题(浅底深线 / 深底亮线),配合 mix-blend-mode
  1254. */
  1255. const VS = `attribute vec2 position;void main(){gl_Position=vec4(position,0.0,1.0);}`;
  1256. const FS = `precision highp float;
  1257. uniform vec2 u_resolution;
  1258. uniform float u_time;
  1259. uniform vec2 u_mouse;
  1260. uniform float u_dark; // 0 = light, 1 = dark
  1261. uniform vec3 u_accent;
  1262. float gridLine(vec2 uv, float spacing, float thickness){
  1263. vec2 g = abs(fract(uv / spacing) - 0.5);
  1264. float d = min(g.x, g.y);
  1265. return 1.0 - smoothstep(thickness - 0.005, thickness + 0.005, d);
  1266. }
  1267. float dot2(vec2 p){ return dot(p,p); }
  1268. void main(){
  1269. vec2 uv = gl_FragCoord.xy / u_resolution.xy;
  1270. float aspect = u_resolution.x / u_resolution.y;
  1271. vec2 p = uv;
  1272. p.x *= aspect;
  1273. // 缓慢平移
  1274. vec2 drift = vec2(u_time * 0.008, u_time * 0.005);
  1275. vec2 gp = p + drift;
  1276. // 主细网格 (大间距)
  1277. float mainGrid = gridLine(gp, 0.12, 0.012);
  1278. // 次级网格 (更细更密)
  1279. float subGrid = gridLine(gp, 0.024, 0.04) * 0.4;
  1280. // 鼠标附近的强化
  1281. vec2 m = u_mouse;
  1282. m.x *= aspect;
  1283. float md = length(p - m);
  1284. float mInfluence = exp(-md * 4.0) * 0.5;
  1285. float gridStrength = (mainGrid + subGrid * 0.5) * (0.45 + mInfluence);
  1286. // 点阵 (作为基底)
  1287. vec2 dotGrid = fract(gp * 50.0) - 0.5;
  1288. float dotMask = 1.0 - smoothstep(0.05, 0.14, length(dotGrid));
  1289. // 用低频噪声调制点阵密度
  1290. float wave = sin(gp.x * 1.4 + u_time * 0.15) * cos(gp.y * 1.6 - u_time * 0.12);
  1291. dotMask *= smoothstep(-0.3, 0.6, wave) * 0.6;
  1292. // 颜色: 浅底用深线条,深底用浅线条;高亮处带 accent 痕迹
  1293. vec3 lineColor = mix(vec3(0.08), vec3(0.92), u_dark);
  1294. vec3 bgColor = mix(vec3(0.97, 0.97, 0.96), vec3(0.06, 0.06, 0.07), u_dark);
  1295. // accent 暗示 (鼠标附近偷渡一点 accent 色)
  1296. vec3 col = bgColor;
  1297. col = mix(col, lineColor, gridStrength * 0.55);
  1298. col = mix(col, lineColor, dotMask * 0.35);
  1299. col = mix(col, u_accent, mInfluence * 0.18);
  1300. gl_FragColor = vec4(col, 1.0);
  1301. }`;
  1302. const mouse={x:0.5,y:0.5};
  1303. addEventListener('mousemove',e=>{mouse.x=e.clientX/innerWidth;mouse.y=1-e.clientY/innerHeight});
  1304. function bootGL(canvasId, fsSrc){
  1305. const canvas=document.getElementById(canvasId);
  1306. const gl=canvas.getContext('webgl',{alpha:true,antialias:true,premultipliedAlpha:false});
  1307. if(!gl) return ()=>false;
  1308. const mk=(t,s)=>{const sh=gl.createShader(t);gl.shaderSource(sh,s);gl.compileShader(sh);return sh};
  1309. const prog=gl.createProgram();
  1310. gl.attachShader(prog,mk(gl.VERTEX_SHADER,VS));
  1311. gl.attachShader(prog,mk(gl.FRAGMENT_SHADER,fsSrc));
  1312. gl.linkProgram(prog);gl.useProgram(prog);
  1313. const buf=gl.createBuffer();
  1314. gl.bindBuffer(gl.ARRAY_BUFFER,buf);
  1315. gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]),gl.STATIC_DRAW);
  1316. const pos=gl.getAttribLocation(prog,'position');
  1317. gl.enableVertexAttribArray(pos);gl.vertexAttribPointer(pos,2,gl.FLOAT,false,0,0);
  1318. const lRes=gl.getUniformLocation(prog,'u_resolution');
  1319. const lT=gl.getUniformLocation(prog,'u_time');
  1320. const lM=gl.getUniformLocation(prog,'u_mouse');
  1321. const lD=gl.getUniformLocation(prog,'u_dark');
  1322. const lA=gl.getUniformLocation(prog,'u_accent');
  1323. const resize=()=>{
  1324. const d=Math.min(window.devicePixelRatio||1,2);
  1325. canvas.width=innerWidth*d;canvas.height=innerHeight*d;
  1326. gl.viewport(0,0,canvas.width,canvas.height);
  1327. };
  1328. addEventListener('resize',resize);resize();
  1329. // 读取 CSS 变量,把 accent 颜色塞进 shader
  1330. function readAccent(){
  1331. const cs = getComputedStyle(document.documentElement);
  1332. const hex = cs.getPropertyValue('--accent').trim() || '#002FA7';
  1333. const m = hex.match(/^#([0-9a-f]{6})$/i);
  1334. if(!m) return [0, 0.18, 0.65];
  1335. const n = parseInt(m[1], 16);
  1336. return [((n>>16)&255)/255, ((n>>8)&255)/255, (n&255)/255];
  1337. }
  1338. let accent = readAccent();
  1339. let dark = 0;
  1340. return (tSec, isDark)=>{
  1341. if(isDark !== undefined) dark = isDark ? 1 : 0;
  1342. accent = readAccent();
  1343. gl.uniform2f(lRes,canvas.width,canvas.height);
  1344. gl.uniform1f(lT,tSec);
  1345. gl.uniform2f(lM,mouse.x,mouse.y);
  1346. gl.uniform1f(lD,dark);
  1347. gl.uniform3f(lA,accent[0],accent[1],accent[2]);
  1348. gl.drawArrays(gl.TRIANGLES,0,6);
  1349. return true;
  1350. };
  1351. }
  1352. // canvas-mode / low-power: skip WebGL draw loop (no active RAF loop)
  1353. let darkMode=false;
  1354. let gridCtrl=null, gridRAF=0, gridT0=Date.now();
  1355. function startGrid(){
  1356. if(document.body.classList.contains('canvas-mode') || window.__lowPowerMode || gridRAF) return;
  1357. if(!gridCtrl) gridCtrl = bootGL('bg-grid',FS);
  1358. if(!gridCtrl) return;
  1359. gridT0=Date.now();
  1360. function loop(){
  1361. if(window.__lowPowerMode){gridRAF=0;return;}
  1362. const t=(Date.now()-gridT0)/1000;
  1363. gridCtrl(t, darkMode);
  1364. gridRAF=requestAnimationFrame(loop);
  1365. }
  1366. gridRAF=requestAnimationFrame(loop);
  1367. }
  1368. function stopGrid(){
  1369. if(gridRAF) cancelAnimationFrame(gridRAF);
  1370. gridRAF=0;
  1371. }
  1372. if(document.body.classList.contains('canvas-mode')){
  1373. const c=document.getElementById('bg-grid');
  1374. if(c) c.remove();
  1375. }else{
  1376. startGrid();
  1377. }
  1378. addEventListener('swiss-low-power-change', e=>{e.detail.on ? stopGrid() : startGrid();});
  1379. // =============== 导航 ===============
  1380. const deck=document.getElementById('deck');
  1381. const slides=deck.querySelectorAll('.slide');
  1382. const nav=document.getElementById('nav');
  1383. let idx=0,total=slides.length,lock=false;
  1384. deck.style.width=(total*100)+'vw';
  1385. slides.forEach((s,i)=>{
  1386. const b=document.createElement('button');
  1387. b.className='dot';b.dataset.i=i;b.setAttribute('aria-label','Page '+(i+1));
  1388. b.onclick=()=>go(i);
  1389. nav.appendChild(b);
  1390. });
  1391. function go(n){
  1392. if(lock)return;
  1393. idx=Math.max(0,Math.min(total-1,n));
  1394. window.__currentSlideIndex = idx;
  1395. deck.style.transform=`translateX(${-idx*100}vw)`;
  1396. nav.querySelectorAll('.dot').forEach((d,i)=>d.classList.toggle('active',i===idx));
  1397. const el=slides[idx];
  1398. const isDark = el.classList.contains('dark') || el.classList.contains('accent');
  1399. document.body.classList.toggle('dark-bg', isDark);
  1400. darkMode = isDark;
  1401. if(window.__playSlide) setTimeout(()=>window.__playSlide(idx), 450);
  1402. lock=true;setTimeout(()=>lock=false,700);
  1403. }
  1404. /* =============== ESC 索引视图 =============== */
  1405. let overviewOn=false;
  1406. const ov=document.createElement('div');
  1407. ov.id='overview';
  1408. 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';
  1409. document.body.appendChild(ov);
  1410. function buildOverview(){
  1411. ov.innerHTML='';
  1412. const grid=document.createElement('div');
  1413. grid.style.cssText='display:grid;grid-template-columns:repeat(4,1fr);gap:2vh 1.6vw;max-width:90vw;margin:0 auto';
  1414. slides.forEach((s,i)=>{
  1415. const card=document.createElement('div');
  1416. card.style.cssText='cursor:pointer;overflow:hidden;border:2px solid '+(i===idx?'var(--accent)':'rgba(0,0,0,.12)')+';transition:border-color .2s';
  1417. card.onmouseenter=()=>card.style.borderColor='rgba(0,0,0,.4)';
  1418. card.onmouseleave=()=>card.style.borderColor=i===idx?'var(--accent)':'rgba(0,0,0,.12)';
  1419. const wrap=document.createElement('div');
  1420. const isDark = s.classList.contains('dark') || s.classList.contains('accent');
  1421. wrap.style.cssText='width:100%;aspect-ratio:16/9;overflow:hidden;position:relative;pointer-events:none;background:'+(isDark?'var(--ink)':'var(--paper)');
  1422. const clone=s.cloneNode(true);
  1423. 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';
  1424. wrap.appendChild(clone);
  1425. const label=document.createElement('div');
  1426. /* ESC 索引卡 label */
  1427. label.style.cssText='padding:6px 10px;font-family:var(--mono);font-size:14px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink);opacity:.7';
  1428. label.textContent=(i+1)+' / '+total;
  1429. card.appendChild(wrap);
  1430. card.appendChild(label);
  1431. card.onclick=()=>{toggleOverview();go(i)};
  1432. grid.appendChild(card);
  1433. });
  1434. ov.appendChild(grid);
  1435. }
  1436. function toggleOverview(){
  1437. overviewOn=!overviewOn;
  1438. if(overviewOn){buildOverview();ov.style.display='block';}
  1439. else{ov.style.display='none';}
  1440. }
  1441. addEventListener('keydown',e=>{
  1442. if(e.key==='Escape'){e.preventDefault();toggleOverview();return;}
  1443. if(e.key && e.key.toLowerCase()==='b' && !e.metaKey && !e.ctrlKey && !e.altKey){
  1444. e.preventDefault();
  1445. window.__setLowPowerMode(!window.__lowPowerMode);
  1446. return;
  1447. }
  1448. if(overviewOn)return;
  1449. if(e.key==='ArrowRight'||e.key==='PageDown'||e.key===' '||e.key==='ArrowDown'){
  1450. if(window.__pipeAdvance && window.__pipeAdvance()) return;
  1451. go(idx+1);
  1452. return;
  1453. }
  1454. if(e.key==='ArrowLeft'||e.key==='PageUp'||e.key==='ArrowUp')go(idx-1);
  1455. if(e.key==='Home')go(0);
  1456. if(e.key==='End')go(total-1);
  1457. });
  1458. let wheelTO=null,wheelAcc=0;
  1459. addEventListener('wheel',e=>{
  1460. wheelAcc+=e.deltaY+e.deltaX;
  1461. if(Math.abs(wheelAcc)>50){
  1462. if(wheelAcc>0 && window.__pipeAdvance && window.__pipeAdvance()){
  1463. wheelAcc=0;
  1464. }else{
  1465. go(idx+(wheelAcc>0?1:-1));wheelAcc=0;
  1466. }
  1467. }
  1468. clearTimeout(wheelTO);wheelTO=setTimeout(()=>wheelAcc=0,150);
  1469. },{passive:true});
  1470. let tx=0,ty=0;
  1471. addEventListener('touchstart',e=>{tx=e.touches[0].clientX;ty=e.touches[0].clientY},{passive:true});
  1472. addEventListener('touchend',e=>{
  1473. const dx=(e.changedTouches[0].clientX-tx);
  1474. const dy=(e.changedTouches[0].clientY-ty);
  1475. if(Math.abs(dx)>50&&Math.abs(dx)>Math.abs(dy)){
  1476. if(dx<0 && window.__pipeAdvance && window.__pipeAdvance()) return;
  1477. go(idx+(dx<0?1:-1));
  1478. }
  1479. },{passive:true});
  1480. const initialSlideParam = new URLSearchParams(location.search).get('slide');
  1481. const initialSlide = initialSlideParam ? Number(initialSlideParam) - 1 : 0;
  1482. go(Number.isFinite(initialSlide) ? initialSlide : 0);
  1483. </script>
  1484. <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
  1485. <script>lucide.createIcons();</script>
  1486. <!-- Motion One 动效引擎 (与原模板一致) -->
  1487. <script type="module">
  1488. let motion;
  1489. try {
  1490. motion = await import('./assets/motion.min.js');
  1491. } catch(e1) {
  1492. try {
  1493. motion = await import('https://cdn.jsdelivr.net/npm/motion@11.11.17/+esm');
  1494. } catch(e2) {
  1495. console.warn('[motion] local + CDN both failed, disabling animations', e1, e2);
  1496. document.querySelectorAll('[data-anim]').forEach(el=>{el.style.opacity='1';el.style.transform='none'});
  1497. document.querySelectorAll('[data-animate="pipeline"] [data-anim]').forEach(el=>el.style.opacity='1');
  1498. }
  1499. }
  1500. if(motion){
  1501. const { animate } = motion;
  1502. document.body.classList.add('motion-ready');
  1503. /* ============================================================
  1504. IBM Carbon Motion · 每个 recipe 服务一种表达
  1505. 不是一刀切的 stagger,而是把动效绑在内容语义上
  1506. ============================================================ */
  1507. const EASE_PROD = [.2, 0, .38, .9];
  1508. const EASE_ENTRY_EXP = [0, 0, .3, 1];
  1509. const slides = [...document.querySelectorAll('.slide')];
  1510. let lastIdx = -1;
  1511. function resetAnims(slide){
  1512. slide.querySelectorAll('[data-anim]').forEach(el=>{
  1513. el.style.opacity='';
  1514. el.style.transform='';
  1515. });
  1516. /* 同时复位需要被 recipe 接管的元素 */
  1517. slide.querySelectorAll('.row-fill,.tl-node,.stack-block,.bar-tower,.sub-card,.col,.vrule,.kpi-cell')
  1518. .forEach(el=>{el.style.cssText = el.dataset._origCss || el.style.cssText;});
  1519. }
  1520. /* ---------- 通用工具 ---------- */
  1521. const fade = (el, opts={})=>animate(el,
  1522. {opacity:[0,1], y:[opts.y ?? 12, 0]},
  1523. {duration:opts.duration ?? .6, delay:opts.delay ?? 0,
  1524. easing:opts.easing ?? EASE_ENTRY_EXP});
  1525. /* ---------- recipe: hero · 封面索引 ----------
  1526. 大编号一个个亮起 → 索引行最后落定 */
  1527. function rHero(slide, all){
  1528. const numRows = [...slide.querySelectorAll('.cover-row')];
  1529. const rest = all.filter(el=>!numRows.length || el !== numRows[0]);
  1530. /* 先入: chrome 240ms */
  1531. const chrome = slide.querySelector('.chrome-min');
  1532. if(chrome) animate(chrome, {opacity:[0,1]}, {duration:.24, easing:EASE_PROD});
  1533. /* 大编号 01/02/03 像点名一样依次亮 */
  1534. numRows.forEach((row, i)=>{
  1535. animate(row, {opacity:[0,1], x:[-12,0]},
  1536. {duration:.5, delay:.15 + i*.18, easing:EASE_ENTRY_EXP});
  1537. });
  1538. /* 索引底栏最后慢慢落定 */
  1539. const idx = slide.querySelector('[data-anim="line"]');
  1540. if(idx) fade(idx, {delay:.15 + numRows.length*.18 + .1, duration:.5, y:6});
  1541. }
  1542. /* ---------- recipe: progression · 1× → 10× → 1000× ----------
  1543. 节点依次入场,每个节点的数字单独"递进生长"营造跃迁 */
  1544. function rProgression(slide, all){
  1545. const head = slide.querySelector('[data-anim="line"]');
  1546. if(head) fade(head, {duration:.6, y:10});
  1547. const nodes = [...slide.querySelectorAll('.tl-node')];
  1548. nodes.forEach((node, i)=>{
  1549. const base = .35 + i*.32; /* 节点之间间隔大,营造时间感 */
  1550. /* 整个节点先轻微浮入 */
  1551. animate(node, {opacity:[0,1], y:[14, 0]},
  1552. {duration:.55, delay:base, easing:EASE_ENTRY_EXP});
  1553. /* 再让 multi(数字)从 .85 scale 弹到 1,延迟 100ms */
  1554. const multi = node.querySelector('.multi');
  1555. if(multi) animate(multi, {scale:[.92, 1], opacity:[0,1]},
  1556. {duration:.5, delay:base + .12, easing:EASE_ENTRY_EXP});
  1557. });
  1558. /* 底部 KPI 4 列最后落定,内部 60ms stagger */
  1559. const kpis = [...slide.querySelectorAll('.kpi-cell')];
  1560. kpis.forEach((cell, i)=>{
  1561. animate(cell, {opacity:[0,1], y:[8, 0]},
  1562. {duration:.4, delay:1.4 + i*.07, easing:EASE_PROD});
  1563. });
  1564. }
  1565. /* ---------- recipe: statement · 大宣言 ----------
  1566. 左半屏标题逐行落下,右半屏 leaked 信息晚 600ms 进 */
  1567. function rStatement(slide, all){
  1568. const halves = [...slide.querySelectorAll('.half')];
  1569. if(halves.length === 2){
  1570. animate(halves[0], {opacity:[0,1], y:[18,0]},
  1571. {duration:.7, delay:0, easing:EASE_ENTRY_EXP});
  1572. animate(halves[1], {opacity:[0,1], y:[18,0]},
  1573. {duration:.7, delay:.6, easing:EASE_ENTRY_EXP});
  1574. } else {
  1575. /* P9 Index Card — 三行像盖章一样依次落 */
  1576. const head = slide.querySelector('[data-anim="line"]');
  1577. if(head) fade(head, {duration:.5, y:6});
  1578. const blocks = all.filter(el=>el !== head);
  1579. blocks.forEach((el, i)=>{
  1580. animate(el, {opacity:[0,1], y:[20,0]},
  1581. {duration:.55, delay:.25 + i*.18, easing:EASE_ENTRY_EXP});
  1582. });
  1583. }
  1584. }
  1585. /* ---------- recipe: grid-reveal · 五个定义 ----------
  1586. 卡片按 nb-corner 序号 01→02→03→04→05→Σ 依次揭示 */
  1587. function rGridReveal(slide, all){
  1588. const head = slide.querySelector('[data-anim="line"]');
  1589. if(head) fade(head, {duration:.6, y:10});
  1590. const cards = [...slide.querySelectorAll('.sub-card')];
  1591. cards.forEach((card, i)=>{
  1592. animate(card, {opacity:[0,1], y:[20,0], scale:[.96, 1]},
  1593. {duration:.5, delay:.3 + i*.09, easing:EASE_ENTRY_EXP});
  1594. });
  1595. }
  1596. /* ---------- recipe: stack-build · 三层架构 ----------
  1597. 中间 thin 先入 → 上层 fat skills 从顶推下 → 下层 application 从底推上 */
  1598. function rStackBuild(slide, all){
  1599. const head = slide.querySelector('[data-anim="line"]');
  1600. if(head) fade(head, {duration:.6, y:10});
  1601. const blocks = [...slide.querySelectorAll('.stack-block')];
  1602. /* 先入: 中间薄层(LAYER 02) */
  1603. if(blocks[1]) animate(blocks[1], {opacity:[0,1], scaleY:[.85, 1]},
  1604. {duration:.55, delay:.3, easing:EASE_ENTRY_EXP});
  1605. /* 上推下: LAYER 01 fat skills 从顶部 push down */
  1606. if(blocks[0]) animate(blocks[0], {opacity:[0,1], y:[-22, 0]},
  1607. {duration:.6, delay:.6, easing:EASE_ENTRY_EXP});
  1608. /* 下推上: LAYER 03 application 从底部 push up */
  1609. if(blocks[2]) animate(blocks[2], {opacity:[0,1], y:[22, 0]},
  1610. {duration:.6, delay:.6, easing:EASE_ENTRY_EXP});
  1611. const foot = slide.querySelector('.t-meta');
  1612. if(foot) animate(foot, {opacity:[0,1]}, {duration:.3, delay:1.3, easing:EASE_PROD});
  1613. }
  1614. /* ---------- recipe: measure-up · YC KPI 塔 ----------
  1615. 塔从底部 scaleY 0→1 生长 + 数字最后弹入 */
  1616. function rMeasureUp(slide, all){
  1617. const head = slide.querySelector('[data-anim="line"]');
  1618. if(head) fade(head, {duration:.6, y:10});
  1619. const towers = [...slide.querySelectorAll('.bar-tower')];
  1620. towers.forEach((tower, i)=>{
  1621. const block = tower.querySelector('.body-block');
  1622. if(block){
  1623. block.style.transformOrigin = 'bottom center';
  1624. animate(block, {opacity:[0,1], scaleY:[.05, 1]},
  1625. {duration:.7, delay:.35 + i*.12, easing:EASE_ENTRY_EXP});
  1626. }
  1627. /* cap (顶部图标) 等柱体长好后弹入 */
  1628. const cap = tower.querySelector('.cap');
  1629. if(cap) animate(cap, {opacity:[0,1], y:[-8, 0]},
  1630. {duration:.4, delay:.85 + i*.12, easing:EASE_PROD});
  1631. });
  1632. }
  1633. /* ---------- recipe: bar-grow · 90% 价值分布 ----------
  1634. 标题先入 → hairline 从中点向两侧 stroke draw → bar 依次 width 0→target → 数值 fade in */
  1635. function rBarGrow(slide, all){
  1636. const head = slide.querySelector('[data-anim="line"]');
  1637. if(head) fade(head, {duration:.6, y:10});
  1638. /* 中部 hairline:从 100% width 0 拉到 100% (transformOrigin: center) */
  1639. const midRow = slide.querySelector('[data-anim="up"]');
  1640. if(midRow){
  1641. const midLabel = midRow.querySelector('.t-cat');
  1642. const midLine = midRow.querySelector('div[style*="height:1px"]');
  1643. if(midLabel) animate(midLabel, {opacity:[0,1], x:[-8,0]},
  1644. {duration:.4, delay:.4, easing:EASE_PROD});
  1645. if(midLine){
  1646. midLine.style.transformOrigin = 'center';
  1647. animate(midLine, {opacity:[0,1], scaleX:[0, 1]},
  1648. {duration:.55, delay:.5, easing:EASE_ENTRY_EXP});
  1649. }
  1650. }
  1651. /* bar 行依次 width 增长 */
  1652. const fills = [...slide.querySelectorAll('.row-fill')];
  1653. const labels = [...slide.querySelectorAll('.row-lbl')];
  1654. const values = [...slide.querySelectorAll('.row-val')];
  1655. fills.forEach((fill, i)=>{
  1656. const target = fill.style.width;
  1657. fill.style.width = '0%';
  1658. if(labels[i]) animate(labels[i], {opacity:[0,1], x:[-12,0]},
  1659. {duration:.4, delay:.85 + i*.14, easing:EASE_PROD});
  1660. animate(fill, {width:['0%', target]},
  1661. {duration:.65, delay:.95 + i*.14, easing:EASE_ENTRY_EXP});
  1662. if(values[i]) animate(values[i], {opacity:[0,1]},
  1663. {duration:.3, delay:1.5 + i*.14, easing:EASE_PROD});
  1664. });
  1665. }
  1666. /* ---------- recipe: duo-mirror · Latent vs Deterministic ----------
  1667. 左 80ms 入,vrule 从中心 scaleY 0→1,右 240ms 入 */
  1668. function rDuoMirror(slide, all){
  1669. const head = slide.querySelector('[data-anim="line"]');
  1670. if(head) fade(head, {duration:.6, y:10});
  1671. const cols = [...slide.querySelectorAll('.duo-compare .col')];
  1672. const vrule = slide.querySelector('.duo-compare .vrule');
  1673. if(cols[0]) animate(cols[0], {opacity:[0,1], x:[-24, 0]},
  1674. {duration:.65, delay:.4, easing:EASE_ENTRY_EXP});
  1675. if(vrule){
  1676. vrule.style.transformOrigin = 'center';
  1677. animate(vrule, {opacity:[0,1], scaleY:[0, 1]},
  1678. {duration:.55, delay:.55, easing:EASE_ENTRY_EXP});
  1679. }
  1680. if(cols[1]) animate(cols[1], {opacity:[0,1], x:[24, 0]},
  1681. {duration:.65, delay:.7, easing:EASE_ENTRY_EXP});
  1682. const foot = slide.querySelector('.t-meta');
  1683. if(foot) animate(foot, {opacity:[0,1]}, {duration:.3, delay:1.3, easing:EASE_PROD});
  1684. }
  1685. /* ---------- recipe: split-statement · 收尾 ----------
  1686. 左黑半屏的 once / forever 错位入场;右白半屏 takeaway list 后跟 */
  1687. function rSplitStatement(slide, all){
  1688. const halves = [...slide.querySelectorAll('.half')];
  1689. /* 左黑半屏 — once 先入,forever 间隔 600ms */
  1690. if(halves[0]){
  1691. animate(halves[0], {opacity:[0,1]}, {duration:.4, easing:EASE_PROD});
  1692. const kpis = halves[0].querySelectorAll('.kpi-thin');
  1693. kpis.forEach((k, i)=>{
  1694. animate(k, {opacity:[0,1], y:[24,0]},
  1695. {duration:.7, delay:.25 + i*.55, easing:EASE_ENTRY_EXP});
  1696. });
  1697. }
  1698. /* 右白半屏 — list 三条依次入,在左侧 once 出现后开始 */
  1699. if(halves[1]){
  1700. animate(halves[1], {opacity:[0,1]}, {duration:.4, delay:.3, easing:EASE_PROD});
  1701. const items = halves[1].querySelectorAll('.takeaway-list li');
  1702. items.forEach((li, i)=>{
  1703. animate(li, {opacity:[0,1], x:[20, 0]},
  1704. {duration:.45, delay:1.0 + i*.12, easing:EASE_ENTRY_EXP});
  1705. });
  1706. }
  1707. }
  1708. /* ---------- recipe: timeline-walk · P11 横向 evolution ----------
  1709. 标题先入 → 横轴虚线 scaleX 拉开(伪) → 5 个 dot 按年代依次 scale 入 → label 跟随 */
  1710. function rTimelineWalk(slide, all){
  1711. const head = slide.querySelector('[data-anim="line"]');
  1712. if(head) fade(head, {duration:.55, y:10});
  1713. const tl = slide.querySelector('.timeline-h');
  1714. if(tl) animate(tl, {opacity:[0,1]}, {duration:.4, delay:.35, easing:EASE_PROD});
  1715. const nodes = [...slide.querySelectorAll('.timeline-h .th-node')];
  1716. nodes.forEach((node, i)=>{
  1717. const base = .55 + i*.18;
  1718. const dot = node.querySelector('.dot');
  1719. const label = node.querySelector('.label');
  1720. if(dot){
  1721. dot.style.transformOrigin='center';
  1722. animate(dot, {opacity:[0,1], scale:[.2, 1]},
  1723. {duration:.45, delay:base, easing:EASE_ENTRY_EXP});
  1724. }
  1725. if(label){
  1726. const fromY = node.classList.contains('up') ? 8 : -8;
  1727. /* 保留 CSS 的水平居中 translateX(-50%),避免动效覆盖后 label 与 dot 错位 */
  1728. animate(label, {opacity:[0,1], transform:[`translate(-50%, ${fromY}px)`, 'translate(-50%, 0px)']},
  1729. {duration:.5, delay:base + .12, easing:EASE_ENTRY_EXP});
  1730. }
  1731. });
  1732. const foot = slide.querySelector('.t-meta');
  1733. if(foot) animate(foot, {opacity:[0,1]}, {duration:.3, delay:1.7, easing:EASE_PROD});
  1734. }
  1735. /* ---------- recipe: manifesto · P12 Form & Found ----------
  1736. 副标先入 → 大字两段错峰落 → 底部 ink 通栏条从下推上 */
  1737. function rManifesto(slide, all){
  1738. const head = slide.querySelector('[data-anim="line"]');
  1739. if(head){
  1740. const cat = head.querySelector('.t-cat');
  1741. const title = head.querySelector('div:nth-child(2)');
  1742. if(cat) animate(cat, {opacity:[0,1], x:[-10,0]},
  1743. {duration:.4, delay:.1, easing:EASE_PROD});
  1744. if(title) animate(title, {opacity:[0,1], y:[26, 0]},
  1745. {duration:.85, delay:.3, easing:EASE_ENTRY_EXP});
  1746. }
  1747. /* 底部 ink 条从下推入 */
  1748. const foot = [...slide.querySelectorAll('[data-anim="up"]')];
  1749. foot.forEach((el, i)=>{
  1750. animate(el, {opacity:[0,1], y:[40, 0]},
  1751. {duration:.75, delay:.85 + i*.12, easing:EASE_ENTRY_EXP});
  1752. });
  1753. }
  1754. /* ---------- recipe: three-forces · P13 ----------
  1755. 左 ink hero 先入 → 右 3 张卡按 1/2/3 依次从右滑入 + 每张大数字单独弹入 */
  1756. function rThreeForces(slide, all){
  1757. const head = slide.querySelector('[data-anim="line"]');
  1758. if(head) fade(head, {duration:.5, y:8});
  1759. const grid = slide.querySelector('[data-anim="up"]');
  1760. if(grid) animate(grid, {opacity:[0,1]}, {duration:.3, delay:.3, easing:EASE_PROD});
  1761. const heroBlock = grid?.querySelector(':scope > div:first-child');
  1762. if(heroBlock) animate(heroBlock, {opacity:[0,1], x:[-26, 0]},
  1763. {duration:.6, delay:.4, easing:EASE_ENTRY_EXP});
  1764. const cards = grid ? [...grid.querySelectorAll(':scope > div:nth-child(2) > .card-fill')] : [];
  1765. cards.forEach((card, i)=>{
  1766. const base = .6 + i*.18;
  1767. animate(card, {opacity:[0,1], x:[28, 0]},
  1768. {duration:.6, delay:base, easing:EASE_ENTRY_EXP});
  1769. const num = card.querySelector(':scope > div:first-child');
  1770. if(num) animate(num, {opacity:[0,1], scale:[.7, 1]},
  1771. {duration:.5, delay:base + .15, easing:EASE_ENTRY_EXP});
  1772. });
  1773. }
  1774. /* ---------- recipe: loop-form · P14 自学闭环 ----------
  1775. 左 4 步像台阶依次入 → 右环图节点按时钟顺序入 → 中心 improves scale 入 */
  1776. function rLoopForm(slide, all){
  1777. const head = slide.querySelector('[data-anim="line"]');
  1778. if(head) fade(head, {duration:.55, y:10});
  1779. const grid = slide.querySelector('[data-anim="up"]');
  1780. if(grid) animate(grid, {opacity:[0,1]}, {duration:.3, delay:.35, easing:EASE_PROD});
  1781. /* 左侧 4 步台阶,每步从左滑入 */
  1782. const steps = grid ? [...grid.querySelectorAll(':scope > div:first-child > div')] : [];
  1783. steps.forEach((step, i)=>{
  1784. animate(step, {opacity:[0,1], x:[-18, 0]},
  1785. {duration:.5, delay:.5 + i*.14, easing:EASE_ENTRY_EXP});
  1786. });
  1787. /* 右侧 SVG 节点 (4 个 circle + label) 按 01→04 顺序入 */
  1788. const svg = grid?.querySelector('svg');
  1789. if(svg){
  1790. const ring = svg.querySelector('circle:first-of-type');
  1791. if(ring) animate(ring, {opacity:[0,.25]}, {duration:.5, delay:.6, easing:EASE_PROD});
  1792. const nodeCircles = [...svg.querySelectorAll('circle')].slice(1);
  1793. nodeCircles.forEach((c, i)=>{
  1794. c.style.transformOrigin = `${c.getAttribute('cx')}px ${c.getAttribute('cy')}px`;
  1795. animate(c, {opacity:[0,1], scale:[.4, 1]},
  1796. {duration:.45, delay:.7 + i*.16, easing:EASE_ENTRY_EXP});
  1797. });
  1798. const arrows = [...svg.querySelectorAll('path[marker-end]')];
  1799. arrows.forEach((p, i)=>{
  1800. animate(p, {opacity:[0,1]},
  1801. {duration:.4, delay:.85 + i*.16, easing:EASE_PROD});
  1802. });
  1803. const center = [...svg.querySelectorAll('text')].slice(-2);
  1804. center.forEach((t, i)=>{
  1805. animate(t, {opacity:[0,1], scale:[.7, 1]},
  1806. {duration:.5, delay:1.55 + i*.1, easing:EASE_ENTRY_EXP});
  1807. });
  1808. }
  1809. }
  1810. /* ---------- recipe: matrix-fill · P15 skill 矩阵 ----------
  1811. 标题入 → 12 张卡按对角线波 (i+j) 扫入 → 底部 20,000 大数字最后 fade 入 */
  1812. function rMatrixFill(slide, all){
  1813. const head = slide.querySelector('[data-anim="line"]');
  1814. if(head) fade(head, {duration:.55, y:10});
  1815. const matrix = slide.querySelector('[data-anim="up"]');
  1816. if(!matrix) return;
  1817. animate(matrix, {opacity:[0,1]}, {duration:.3, delay:.35, easing:EASE_PROD});
  1818. const cards = [...matrix.children];
  1819. const cols = 6;
  1820. cards.forEach((card, i)=>{
  1821. const row = Math.floor(i/cols), col = i%cols;
  1822. const wave = (row + col) * .055;
  1823. animate(card, {opacity:[0,1], y:[14, 0], scale:[.92, 1]},
  1824. {duration:.42, delay:.5 + wave, easing:EASE_ENTRY_EXP});
  1825. });
  1826. /* 底部 20,000 区块 */
  1827. const foot = [...slide.querySelectorAll('[data-anim="up"]')][1];
  1828. if(foot){
  1829. animate(foot, {opacity:[0,1], y:[18, 0]},
  1830. {duration:.7, delay:1.4, easing:EASE_ENTRY_EXP});
  1831. const bigNum = foot.querySelector('div:nth-child(1) > div:nth-child(2)');
  1832. if(bigNum) animate(bigNum, {opacity:[0,1], scale:[.94, 1]},
  1833. {duration:.7, delay:1.55, easing:EASE_ENTRY_EXP});
  1834. }
  1835. }
  1836. /* ---------- recipe: field-notes · P16 散点观察 ----------
  1837. 标题入 → 6 张卡按"散点"乱序延迟入,微小旋转复位 */
  1838. function rFieldNotes(slide, all){
  1839. const head = slide.querySelector('[data-anim="line"]');
  1840. if(head) fade(head, {duration:.55, y:10});
  1841. const grid = slide.querySelector('[data-anim="up"]');
  1842. if(!grid) return;
  1843. animate(grid, {opacity:[0,1]}, {duration:.3, delay:.35, easing:EASE_PROD});
  1844. /* 散点顺序: 用一个稍微打乱的索引数组,营造"乱中有序"感 */
  1845. const order = [0, 3, 1, 4, 2, 5];
  1846. const cards = [...grid.children];
  1847. order.forEach((idx, i)=>{
  1848. const card = cards[idx];
  1849. if(!card) return;
  1850. animate(card, {opacity:[0,1], y:[18, 0], rotate:[(idx%2?-.6:.6), 0]},
  1851. {duration:.55, delay:.5 + i*.11, easing:EASE_ENTRY_EXP});
  1852. });
  1853. }
  1854. /* ---------- recipe: system-diagram · P17 三圆系统图 ----------
  1855. 标题入 → SVG 三组图依次入 + 中间同心圆从外向内 scale 入 → 下方注释列依次入 */
  1856. function rSystemDiagram(slide, all){
  1857. const head = slide.querySelector('[data-anim="line"]');
  1858. if(head) fade(head, {duration:.55, y:10});
  1859. const stage = slide.querySelector('[data-anim="up"]');
  1860. if(!stage) return;
  1861. animate(stage, {opacity:[0,1]}, {duration:.3, delay:.35, easing:EASE_PROD});
  1862. const svgs = [...stage.querySelectorAll('svg')];
  1863. svgs.forEach((svg, i)=>{
  1864. const base = .55 + i*.22;
  1865. const circles = [...svg.querySelectorAll('circle')];
  1866. /* 中间是同心圆: 从外圈到内圈依次 scale 入 */
  1867. if(circles.length > 1){
  1868. circles.forEach((c, j)=>{
  1869. c.style.transformOrigin = `${c.getAttribute('cx')}px ${c.getAttribute('cy')}px`;
  1870. animate(c, {opacity:[0,1], scale:[.4, 1]},
  1871. {duration:.5, delay:base + j*.13, easing:EASE_ENTRY_EXP});
  1872. });
  1873. } else if(circles[0]){
  1874. circles[0].style.transformOrigin = `${circles[0].getAttribute('cx')}px ${circles[0].getAttribute('cy')}px`;
  1875. animate(circles[0], {opacity:[0,1], scale:[.4, 1]},
  1876. {duration:.5, delay:base, easing:EASE_ENTRY_EXP});
  1877. }
  1878. const labels = [...svg.querySelectorAll('text')];
  1879. labels.forEach((t, j)=>{
  1880. animate(t, {opacity:[0,1]},
  1881. {duration:.4, delay:base + .25 + j*.06, easing:EASE_PROD});
  1882. });
  1883. });
  1884. /* 下方注释列 */
  1885. const cols = [...stage.querySelectorAll(':scope > div:last-child > div')];
  1886. cols.forEach((col, i)=>{
  1887. animate(col, {opacity:[0,1], y:[12, 0]},
  1888. {duration:.45, delay:1.3 + i*.1, easing:EASE_ENTRY_EXP});
  1889. });
  1890. }
  1891. /* ---------- recipe: why-now · P18 三列 + 巨大底数 ----------
  1892. 标题入 → 三列文本入 → 三个底部巨数 01/02/03 错峰 scale 落定 */
  1893. function rWhyNow(slide, all){
  1894. const head = slide.querySelector('[data-anim="line"]');
  1895. if(head) fade(head, {duration:.55, y:10});
  1896. const grid = slide.querySelector('[data-anim="up"]');
  1897. if(!grid) return;
  1898. animate(grid, {opacity:[0,1]}, {duration:.3, delay:.35, easing:EASE_PROD});
  1899. const cols = [...grid.children];
  1900. cols.forEach((col, i)=>{
  1901. const base = .5 + i*.16;
  1902. const body = col.querySelector(':scope > div:not(:last-child)');
  1903. const big = col.querySelector(':scope > div:last-child');
  1904. if(body) animate(body, {opacity:[0,1], y:[14, 0]},
  1905. {duration:.55, delay:base, easing:EASE_ENTRY_EXP});
  1906. if(big) animate(big, {opacity:[0,1], scale:[.7, 1]},
  1907. {duration:.7, delay:base + .35, easing:EASE_ENTRY_EXP});
  1908. });
  1909. }
  1910. /* ---------- recipe: four-cards · P19 4 列卡片 ----------
  1911. 顶部红线 scaleX 0→1 → 标题入 → 4 卡按 01-04 依次入 */
  1912. function rFourCards(slide, all){
  1913. /* 顶部红线 */
  1914. const topRule = slide.querySelector('[data-anim="line"] > div:first-child');
  1915. if(topRule){
  1916. topRule.style.transformOrigin = 'left center';
  1917. animate(topRule, {opacity:[0,1], scaleX:[0, 1]},
  1918. {duration:.5, delay:.1, easing:EASE_ENTRY_EXP});
  1919. }
  1920. const head = slide.querySelector('[data-anim="line"]');
  1921. if(head){
  1922. const title = head.querySelector(':scope > div:nth-child(2)');
  1923. if(title) animate(title, {opacity:[0,1], y:[14, 0]},
  1924. {duration:.55, delay:.4, easing:EASE_ENTRY_EXP});
  1925. }
  1926. const grid = slide.querySelector('[data-anim="up"]');
  1927. if(!grid) return;
  1928. animate(grid, {opacity:[0,1]}, {duration:.3, delay:.55, easing:EASE_PROD});
  1929. const cards = [...grid.children];
  1930. cards.forEach((card, i)=>{
  1931. animate(card, {opacity:[0,1], y:[18, 0]},
  1932. {duration:.55, delay:.7 + i*.13, easing:EASE_ENTRY_EXP});
  1933. });
  1934. }
  1935. /* ============ P20 · Stacked KPI Ledger · 4 行账单逐行点亮 + 行间发丝从左画 ============ */
  1936. function rStackedLedger(slide, all){
  1937. const ledger = slide.querySelector('[data-anim="ledger"]');
  1938. if(!ledger) return;
  1939. animate(ledger, {opacity:[0,1]}, {duration:.3, delay:.1, easing:EASE_PROD});
  1940. const rows = [...ledger.querySelectorAll('.ledger-row')];
  1941. rows.forEach((row, i)=>{
  1942. const base = .25 + i*.18;
  1943. const num = row.querySelector('.ledger-num');
  1944. const label = row.querySelector('.ledger-label');
  1945. const icon = row.querySelector('.ledger-icon');
  1946. if(num) animate(num, {opacity:[0,1], y:[20, 0]}, {duration:.7, delay:base, easing:EASE_ENTRY_EXP});
  1947. if(label) animate(label, {opacity:[0,1], x:[-12, 0]}, {duration:.55, delay:base + .12, easing:EASE_ENTRY_EXP});
  1948. if(icon) animate(icon, {opacity:[0,1], scale:[.6,1]},{duration:.55, delay:base + .22, easing:EASE_ENTRY_EXP});
  1949. });
  1950. }
  1951. /* ============ P21 · Tech Spec Sheet · 标题分行 / KPI 顶线画出 + count 风感 / 竖线弹起 / 底巨数 ============ */
  1952. function rTechSpec(slide, all){
  1953. const head = slide.querySelector('[data-anim="line"]');
  1954. if(head) fade(head, {duration:.5, y:8});
  1955. const main = slide.querySelector('[data-anim="up"]');
  1956. if(main){
  1957. animate(main, {opacity:[0,1]}, {duration:.3, delay:.25, easing:EASE_PROD});
  1958. /* 左大标题分行 */
  1959. const titleLines = main.querySelector(':scope > div:first-child > div:first-child');
  1960. if(titleLines){
  1961. animate(titleLines, {opacity:[0,1], y:[18, 0]}, {duration:.7, delay:.35, easing:EASE_ENTRY_EXP});
  1962. }
  1963. const titleNote = main.querySelector(':scope > div:first-child > div:nth-child(2)');
  1964. if(titleNote){
  1965. animate(titleNote, {opacity:[0,1], y:[10, 0]}, {duration:.5, delay:.95, easing:EASE_ENTRY_EXP});
  1966. }
  1967. /* 三 KPI · 顶线 scaleX + 数字 fade-up + 副文字 */
  1968. const kpis = [...main.querySelectorAll(':scope > div:not([data-anim]):not(:first-child)')];
  1969. kpis.forEach((kpi, i)=>{
  1970. const base = .55 + i*.18;
  1971. const topRule = kpi.querySelector(':scope > div:first-child');
  1972. if(topRule){
  1973. topRule.style.transformOrigin = 'left center';
  1974. animate(topRule, {scaleX:[0,1], opacity:[0,1]}, {duration:.5, delay:base, easing:EASE_ENTRY_EXP});
  1975. }
  1976. const num = kpi.querySelector('.kpi-num');
  1977. if(num) animate(num, {opacity:[0,1], y:[14, 0]}, {duration:.6, delay:base + .15, easing:EASE_ENTRY_EXP});
  1978. const otherKids = [...kpi.children].filter(el=>el !== topRule && el !== num);
  1979. otherKids.forEach((el, j)=>{
  1980. animate(el, {opacity:[0,1]}, {duration:.4, delay:base + .25 + j*.05, easing:EASE_PROD});
  1981. });
  1982. });
  1983. }
  1984. /* 底部 hero 区: 巨数 + goal + tags + 右下竖线 */
  1985. const hero = slide.querySelector('[data-anim="hero"]');
  1986. if(hero){
  1987. animate(hero, {opacity:[0,1]}, {duration:.3, delay:1.3, easing:EASE_PROD});
  1988. const bottomHero = hero.querySelector('.bottom-hero');
  1989. if(bottomHero) animate(bottomHero, {opacity:[0,1], y:[24, 0], scale:[.92, 1]}, {duration:.7, delay:1.4, easing:EASE_ENTRY_EXP});
  1990. const middle = hero.querySelector(':scope > div:nth-child(2)');
  1991. if(middle){
  1992. const kids = [...middle.children];
  1993. kids.forEach((el, i)=>{
  1994. if(el.style && el.style.background === 'var(--ink)'){
  1995. el.style.transformOrigin = 'left center';
  1996. animate(el, {scaleX:[0,1], opacity:[0,1]}, {duration:.5, delay:1.6 + i*.1, easing:EASE_ENTRY_EXP});
  1997. } else {
  1998. animate(el, {opacity:[0,1], y:[10, 0]}, {duration:.5, delay:1.55 + i*.1, easing:EASE_ENTRY_EXP});
  1999. }
  2000. });
  2001. }
  2002. /* 右下: 文字先入, 9 根竖线再从底部 scaleY 弹起 */
  2003. const right = hero.querySelector(':scope > div:nth-child(3)');
  2004. if(right){
  2005. const rightText = right.querySelector(':scope > div:last-child');
  2006. if(rightText) animate(rightText, {opacity:[0,1], y:[10, 0]}, {duration:.5, delay:1.85, easing:EASE_ENTRY_EXP});
  2007. }
  2008. const bars = slide.querySelectorAll('[data-anim="bars"] .vbar');
  2009. bars.forEach((bar, i)=>{
  2010. bar.style.transformOrigin = 'bottom';
  2011. animate(bar, {scaleY:[0,1], opacity:[0,1]}, {duration:.5, delay:2.0 + i*.04, easing:EASE_ENTRY_EXP});
  2012. });
  2013. }
  2014. }
  2015. /* ============ P22 · Image Hero · 图缓推 + 标题白块从左滑入 + 三 KPI 顶线画出 ============ */
  2016. function rImageHero(slide, all){
  2017. const img = slide.querySelector('[data-anim="img"] img');
  2018. if(img){
  2019. animate(img, {opacity:[0,1], scale:[1.06, 1]}, {duration:1.1, delay:.05, easing:EASE_ENTRY_EXP});
  2020. }
  2021. const titleBlock = slide.querySelector('[data-anim="title-block"]');
  2022. if(titleBlock){
  2023. titleBlock.style.transformOrigin = 'left center';
  2024. animate(titleBlock, {opacity:[0,1], scaleX:[0, 1]}, {duration:.7, delay:.45, easing:EASE_ENTRY_EXP});
  2025. const titleText = titleBlock.querySelector('div');
  2026. if(titleText) animate(titleText, {opacity:[0,1]}, {duration:.4, delay:.85, easing:EASE_PROD});
  2027. }
  2028. const kpiWrap = slide.querySelector('[data-anim="kpi"]');
  2029. if(kpiWrap){
  2030. animate(kpiWrap, {opacity:[0,1]}, {duration:.3, delay:.7, easing:EASE_PROD});
  2031. /* 段落 */
  2032. const para = kpiWrap.querySelector(':scope > div:first-child');
  2033. if(para) animate(para, {opacity:[0,1], y:[14, 0]}, {duration:.6, delay:.85, easing:EASE_ENTRY_EXP});
  2034. /* 三列 KPI · 顶线 scaleX + 数字升起 */
  2035. const cols = [...kpiWrap.querySelectorAll(':scope > div:nth-child(2) > div')];
  2036. cols.forEach((col, i)=>{
  2037. const base = 1.1 + i*.18;
  2038. const topRule = col.querySelector(':scope > div:first-child');
  2039. if(topRule){
  2040. topRule.style.transformOrigin = 'left center';
  2041. animate(topRule, {scaleX:[0,1], opacity:[0,1]}, {duration:.5, delay:base, easing:EASE_ENTRY_EXP});
  2042. }
  2043. const cat = col.querySelector('.t-meta');
  2044. if(cat) animate(cat, {opacity:[0,1]}, {duration:.4, delay:base + .15, easing:EASE_PROD});
  2045. const hero = col.querySelector('.kpi-hero');
  2046. if(hero) animate(hero, {opacity:[0,1], y:[18, 0]}, {duration:.7, delay:base + .25, easing:EASE_ENTRY_EXP});
  2047. const handled = new Set([topRule, cat, hero]);
  2048. [...col.children]
  2049. .filter(el => !handled.has(el))
  2050. .forEach((el, j)=>{
  2051. animate(el, {opacity:[0,1]}, {duration:.4, delay:base + .45 + j*.05, easing:EASE_PROD});
  2052. });
  2053. });
  2054. }
  2055. }
  2056. const RECIPES = {
  2057. 'hero': rHero,
  2058. 'progression': rProgression,
  2059. 'statement': rStatement,
  2060. 'grid-reveal': rGridReveal,
  2061. 'stack-build': rStackBuild,
  2062. 'measure-up': rMeasureUp,
  2063. 'bar-grow': rBarGrow,
  2064. 'duo-mirror': rDuoMirror,
  2065. 'split-statement': rSplitStatement,
  2066. 'timeline-walk': rTimelineWalk,
  2067. 'manifesto': rManifesto,
  2068. 'three-forces': rThreeForces,
  2069. 'loop-form': rLoopForm,
  2070. 'matrix-fill': rMatrixFill,
  2071. 'field-notes': rFieldNotes,
  2072. 'system-diagram': rSystemDiagram,
  2073. 'why-now': rWhyNow,
  2074. 'four-cards': rFourCards,
  2075. 'stacked-ledger': rStackedLedger,
  2076. 'tech-spec': rTechSpec,
  2077. 'image-hero': rImageHero,
  2078. };
  2079. function revealStatic(slide){
  2080. resetAnims(slide);
  2081. document.getAnimations?.().forEach(a=>a.cancel());
  2082. slide.querySelectorAll('[data-anim],.row-fill,.tl-node,.stack-block,.bar-tower,.sub-card,.col,.vrule,.kpi-cell,.card-fill,.card-accent,.card-ink')
  2083. .forEach(el=>{
  2084. el.style.opacity='1';
  2085. el.style.transform='none';
  2086. });
  2087. }
  2088. function playSlide(i){
  2089. const slide = slides[i];
  2090. if(!slide) return;
  2091. lastIdx = i;
  2092. if(window.__lowPowerMode){
  2093. revealStatic(slide);
  2094. return;
  2095. }
  2096. resetAnims(slide);
  2097. /* 关键:[data-anim] 容器很多时候只是占位标记,真正的几何动画在子元素上.
  2098. 默认强制 reveal 所有 [data-anim] 容器, recipe 想做块入场时用 motion 的 {opacity:[0,1]} 会自动覆盖 */
  2099. slide.querySelectorAll('[data-anim]').forEach(el=>{
  2100. el.style.opacity = '1';
  2101. el.style.transform = 'none';
  2102. });
  2103. const all = [...slide.querySelectorAll('[data-anim]')];
  2104. const recipe = slide.dataset.animate;
  2105. const fn = RECIPES[recipe];
  2106. if(fn){ fn(slide, all); return; }
  2107. /* fallback: 平凡 fade */
  2108. if(all.length) animate(all, {opacity:[0,1], y:[12,0]},
  2109. {duration:.6, delay:i=>i*.08, easing:EASE_ENTRY_EXP});
  2110. }
  2111. window.__playSlide = playSlide;
  2112. window.__pipeAdvance = ()=>false; /* 当前 deck 不用 pipeline recipe */
  2113. playSlide(window.__currentSlideIndex || 0);
  2114. }
  2115. </script>
  2116. <script>
  2117. /* ============== ASCII 点阵呼吸场 · IKB 封面/封底专用 ==============
  2118. sin/cos 二维噪声场驱动字符显隐,营造工业仪表板的"涌动呼吸"质感.
  2119. 纯 canvas 2D, mix-blend-mode:screen 让字符在 IKB 底色上自然发亮.
  2120. 用法:在需要呼吸场的容器(.canvas-card 或 split .half.b-accent)内首位插入
  2121. <canvas class="ascii-bg" aria-hidden="true">,本脚本会自动扫描并启动. */
  2122. (function(){
  2123. const canvases = [...document.querySelectorAll('canvas.ascii-bg')];
  2124. if(!canvases.length) return;
  2125. const PALETTE = ' ...:::---+++***◦◦••▢▣';
  2126. const CELL = 16;
  2127. const FONT_SIZE = 13;
  2128. function setup(c){
  2129. const dpr = Math.min(window.devicePixelRatio || 1, 2);
  2130. const rect = c.getBoundingClientRect();
  2131. if(rect.width < 4 || rect.height < 4) return false;
  2132. c.width = Math.round(rect.width * dpr);
  2133. c.height = Math.round(rect.height * dpr);
  2134. c.__dpr = dpr;
  2135. c.__w = rect.width;
  2136. c.__h = rect.height;
  2137. const ctx = c.getContext('2d');
  2138. ctx.setTransform(dpr,0,0,dpr,0,0);
  2139. const mono = (getComputedStyle(document.documentElement).getPropertyValue('--mono') || 'JetBrains Mono, monospace').trim();
  2140. ctx.font = `500 ${FONT_SIZE}px ${mono}`;
  2141. ctx.textBaseline = 'top';
  2142. c.__ctx = ctx;
  2143. return true;
  2144. }
  2145. function draw(c, t){
  2146. if(!c.__ctx) return;
  2147. const ctx = c.__ctx, w = c.__w, h = c.__h;
  2148. ctx.clearRect(0, 0, w, h);
  2149. const cols = Math.ceil(w / CELL);
  2150. const rows = Math.ceil(h / CELL);
  2151. for(let r=0; r<rows; r++){
  2152. for(let cc=0; cc<cols; cc++){
  2153. const n = (
  2154. Math.sin(cc * 0.18 + t) +
  2155. Math.sin(r * 0.24 - t * 0.7) +
  2156. Math.sin((cc + r) * 0.12 + t * 0.45) +
  2157. Math.sin(Math.hypot(cc - cols * 0.5, r - rows * 0.5) * 0.16 - t * 0.55)
  2158. ) / 4; // [-1, 1]
  2159. const v = (n + 1) / 2; // [0, 1]
  2160. if(v < 0.22) continue;
  2161. const idx = Math.min(PALETTE.length - 1, Math.floor(v * PALETTE.length));
  2162. const ch = PALETTE[idx];
  2163. if(ch === ' ') continue;
  2164. const alpha = 0.08 + (v - 0.22) * 0.55;
  2165. ctx.fillStyle = `rgba(255,255,255,${alpha.toFixed(3)})`;
  2166. ctx.fillText(ch, cc * CELL, r * CELL);
  2167. }
  2168. }
  2169. }
  2170. function resizeAll(){ canvases.forEach(setup); }
  2171. let pending = null;
  2172. window.addEventListener('resize', ()=>{
  2173. if(window.__lowPowerMode) return;
  2174. if(pending) cancelAnimationFrame(pending);
  2175. pending = requestAnimationFrame(resizeAll);
  2176. }, {passive:true});
  2177. let t0 = performance.now();
  2178. let frame = 0, asciiRAF = 0, running = false;
  2179. function tick(now){
  2180. if(!running || window.__lowPowerMode){running=false;asciiRAF=0;return;}
  2181. const t = (now - t0) / 1000 * 0.55;
  2182. frame++;
  2183. canvases.forEach(c=>{
  2184. // 离屏 slide 降帧:每 4 帧渲染一次,在屏 slide 每帧渲染
  2185. const slide = c.closest('.slide');
  2186. const rect = slide ? slide.getBoundingClientRect() : null;
  2187. const onscreen = rect && rect.right > 0 && rect.left < window.innerWidth;
  2188. if(!onscreen && (frame & 3) !== 0) return;
  2189. draw(c, t);
  2190. });
  2191. asciiRAF = requestAnimationFrame(tick);
  2192. }
  2193. function start(){
  2194. if(running || window.__lowPowerMode) return;
  2195. resizeAll();
  2196. t0 = performance.now();
  2197. frame = 0;
  2198. running = true;
  2199. asciiRAF = requestAnimationFrame(tick);
  2200. }
  2201. function stop(){
  2202. running = false;
  2203. if(asciiRAF) cancelAnimationFrame(asciiRAF);
  2204. if(pending) cancelAnimationFrame(pending);
  2205. asciiRAF = 0;
  2206. pending = null;
  2207. canvases.forEach(c=>{
  2208. if(c.__ctx) c.__ctx.clearRect(0,0,c.__w || 0,c.__h || 0);
  2209. });
  2210. }
  2211. addEventListener('swiss-low-power-change', e=>{e.detail.on ? stop() : start();});
  2212. start();
  2213. })();
  2214. </script>
  2215. </body>
  2216. </html>