houguyin-v2-render.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. // ==============================================================================
  2. // 江中猴菇饮 OTC · VOC 深度洞察报告 v2 · 主渲染器
  3. // ==============================================================================
  4. // v2 设计哲学:用户决策路径追踪(不再是"方法论展示")
  5. // 叙事线:全景漏斗 → 嵌套洞察 → 痛声地图 → 产品适配 → 定位推导 → 行动蓝图
  6. //
  7. // 报告结构(6 章 × ~24 slides):
  8. // 01 · Cover
  9. // 02 · Agenda(新叙事线说明)
  10. // 03-05 · Ch1 全景漏斗(3 子章):TAM/SAM/SOM + 6级病程 + 目标区间
  11. // 06-10 · Ch2 嵌套洞察(5 子章):每个病程阶段×细分症状×用户选择
  12. // 11-14 · Ch3 痛声地图(4 子章):每层未满足需求 + 需求空白矩阵
  13. // 15-18 · Ch4 产品适配(4 子章):产品力边界 + 米稀归因 + 插入点 + 场景
  14. // 19-22 · Ch5 定位推导(4 子章):心智阶梯 + 差异化 + 聚焦 + 定位陈述
  15. // 23-25 · Ch6 行动蓝图(3 子章):P0/P1 + 验证 + 数据缺口
  16. // ==============================================================================
  17. const STYLES = require('./voc-styles.js');
  18. const SCRIPTS = require('./voc-scripts.js');
  19. const HGY = require('./houguyin-analyze.js');
  20. const CH1 = require('./houguyin-v2-ch1-funnel.js');
  21. const CH2 = require('./houguyin-v2-ch2-nested.js');
  22. const CH3 = require('./houguyin-v2-ch3-pain.js');
  23. const CH4 = require('./houguyin-v2-ch4-fit.js');
  24. const CH5 = require('./houguyin-v2-ch5-position.js');
  25. const CH6 = require('./houguyin-v2-ch6-action.js');
  26. // ============================================================
  27. // 主题色(猴菇饮专属色板 · v2 沿用)
  28. // ============================================================
  29. const BRAND = {
  30. JIANG_GREEN: '#2D9C5D', // 江中主色(品牌/机会/定位)
  31. HOUGU_TAN: '#E8A87C', // 猴菇暖棕(产品适配)
  32. DEEP_AMBER: '#C96E3F', // 猴头菇深色(礼品/传统)
  33. CREAM: '#F5E6D3', // 米黄色(米稀联想)
  34. TECH_BLUE: '#0085CA', // 信任蓝(市场全景/TAM)
  35. WARM_ORANGE: '#FF8C42', // 温暖橙(嵌套洞察/场景)
  36. CORAL: '#FF6B6B', // 珊瑚红(痛声/痛点)
  37. SAGE: '#87A96B', // 鼠尾草绿(行动蓝图)
  38. PURPLE: '#8B5CF6', // 紫色(定位推导/理论)
  39. ROSE: '#FF4D8D', // 玫瑰(竞品对比)
  40. };
  41. // ============================================================
  42. // 6 章元信息(用户决策路径递进)
  43. // ============================================================
  44. const CHAPTERS = [
  45. { id: 1, num: '1', key: 'funnel', title: '全景漏斗', en: 'MARKET FUNNEL', tone: 'blue', color: BRAND.TECH_BLUE, icon: '🔭',
  46. summary: 'TAM 2.5亿胃病人群 → SAM 可服务市场 → SOM 猴菇饮的战场。6级病程分层,锁定我们的目标区间。' },
  47. { id: 2, num: '2', key: 'nested', title: '嵌套洞察', en: 'NESTED INSIGHT', tone: 'orange', color: BRAND.WARM_ORANGE, icon: '🔬',
  48. summary: '每个病程阶段 × 每个细分症状 × 用户选了什么产品。嵌套逻辑:先看阶段,再看症状,再看选择。' },
  49. { id: 3, num: '3', key: 'pain', title: '痛声地图', en: 'PAIN MAP', tone: 'rose', color: BRAND.CORAL, icon: '🔥',
  50. summary: '每一层产品的用户都在痛苦。把 VOC 痛声按层铺开,提炼跨层共性未满足需求,画出"声量×满足度"四象限。' },
  51. { id: 4, num: '4', key: 'fit', title: '产品适配', en: 'PRODUCT FIT', tone: 'amber', color: BRAND.HOUGU_TAN, icon: '🎯',
  52. summary: '猴菇饮的真实能力边界在哪?米稀成功的可迁移因子 × 饼干失败的红线 × 精准插入点 × 场景适配。' },
  53. { id: 5, num: '5', key: 'position', title: '定位推导', en: 'POSITIONING', tone: 'purple', color: BRAND.PURPLE, icon: '💡',
  54. summary: '里斯品类战略 × 特劳特定位。心智阶梯分析 → 差异化锚点 → 聚焦方向 → 一句话定位陈述 → VOC验证。' },
  55. { id: 6, num: '6', key: 'action', title: '行动蓝图', en: 'ACTION PLAN', tone: 'green', color: BRAND.SAGE, icon: '🚀',
  56. summary: 'P0 立即行动 + P1 三个月验证 + 数据缺口与下一轮采集计划。从洞察到落地的最短路径。' },
  57. ];
  58. // ============================================================
  59. // 各章节 subsections 清单
  60. // ============================================================
  61. const CHAPTER_SUBSECTIONS = {
  62. 'funnel': [
  63. 'TAM · 2.5亿胃病人群 × 100亿市场全景',
  64. 'SAM · 可服务市场 × 6级病程分层',
  65. 'SOM · 猴菇饮的目标区间 × 5000万→5亿路径',
  66. ],
  67. 'nested': [
  68. '治疗级+化药级 · 5大症状 × 对应产品 × 未满足需求',
  69. '中成药养护 · 4大症状 × 中成药产品 × 未满足需求',
  70. '保健+功能食品 · 4大需求场景 × 产品 × 未满足需求',
  71. '未病/预防 · 4大触发场景 × 行为 × 空白',
  72. '嵌套全景 · 4阶段×全症状×产品×机会 总览矩阵',
  73. ],
  74. 'pain': [
  75. '化药级痛声 · "有效但不敢停" × PPI依赖焦虑',
  76. '中成药痛声 · "苦涩难咽年轻人不买" × 代际断层',
  77. '功能食品痛声 · "吃了个寂寞" × 无OTC信任',
  78. '跨层汇总 · 5大共性未满足需求 × 声量-满足度四象限',
  79. ],
  80. 'fit': [
  81. '产品力实事求是 · 猴头菇的真实效用边界',
  82. '米稀成功归因 × 可迁移因子 vs 饼干失败红线',
  83. '精准插入点 · 3大机会×无人地带坐标',
  84. '场景适配 · PPI伴侣/日常养护/药店推荐/礼品',
  85. ],
  86. 'position': [
  87. '里斯品类战略 · 品类进化树 × 品类分化机会',
  88. '特劳特心智阶梯 · 胃病用户脑中的产品序列',
  89. '差异化锚点 · vs 药品 / vs 保健品 / vs 食品',
  90. '定位陈述 · 一句话定位 × VOC 验证证据',
  91. ],
  92. 'action': [
  93. 'P0 行动 · 产品/渠道/话术立即落地',
  94. 'P1 验证 · 3个月内数据闭环',
  95. '数据缺口 × 下一轮采集计划',
  96. ],
  97. };
  98. // ============================================================
  99. // 每章 hero line(divider 用)
  100. // ============================================================
  101. const CHAPTER_HERO_LINES = {
  102. 'funnel': '中国 <strong style="color:#0085CA;">2.5 亿</strong>胃病患者,<strong style="color:#FF8C42;">100 亿</strong> OTC 胃药市场。从全量到可服务再到可获取——<strong style="color:#2D9C5D;">TAM → SAM → SOM</strong> 三级漏斗,帮总经理看清"几亿人里我们能切多少"。6 级病程分层锁定猴菇饮的<strong style="color:#E8A87C;">精准战场</strong>。',
  103. 'nested': '嵌套逻辑来了:<strong style="color:#FF8C42;">第一层</strong>按病程阶段(由重到轻 6 级),<strong style="color:#8B5CF6;">第二层</strong>在每个阶段内按细分症状展开(胃酸/胃痛/胃胀/嗳气),<strong style="color:#FF6B6B;">第三层</strong>看这个症状下用户选了什么产品、有什么未满足需求。从用户真实决策路径中找出猴菇饮的切入缝隙。',
  104. 'pain': '每一层产品的用户都在痛苦。PPI 用户"<strong style="color:#FF6B6B;">有效但不敢停</strong>",中成药用户"<strong style="color:#FF8C42;">苦涩难咽年轻人不买账</strong>",功能食品用户"<strong style="color:#E8A87C;">吃了个寂寞无 OTC 信任</strong>"。把每层的 VOC 痛声铺开,提炼出 <strong style="color:#8B5CF6;">5 大跨层共性未满足需求</strong>——哪里最痛、哪里最空。',
  105. 'fit': '猴菇饮的真实能力边界在哪?不吹不黑——<strong style="color:#E8A87C;">猴头菇的效用</strong>是什么?<strong style="color:#2D9C5D;">米稀 7.5 亿成功</strong>的因子哪些可以迁移?<strong style="color:#FF6B6B;">饼干失败</strong>的红线在哪?实事求是画出"我们能接住的需求"和"绝对不能碰的区域",找到<strong style="color:#8B5CF6;">精准插入点</strong>。',
  106. 'position': '洞察做完了,是时候<strong style="color:#8B5CF6;">收敛为定位</strong>。用<strong style="color:#2D9C5D;">里斯品类战略</strong>看"我在哪个品类竞争",用<strong style="color:#8B5CF6;">特劳特定位</strong>看"我在用户心智中占什么位置"。从心智阶梯 → 差异化锚点 → 聚焦方向,推导出一句话定位——<strong style="color:#E8A87C;">能写在盒子上、药店店员能说出来、用户能记住</strong>的那一句。',
  107. 'action': '从洞察到落地的最短路径。<strong style="color:#87A96B;">P0 立即行动</strong>:产品规格/渠道选择/话术定稿。<strong style="color:#0085CA;">P1 三个月验证</strong>:数据闭环 + 定位验证。<strong style="color:#FF8C42;">数据缺口</strong>:哪些结论还需要补充 VOC 采集来夯实。<strong style="color:#2D9C5D;">5000 万 → 5 亿</strong>的 3 阶段路径。',
  108. };
  109. // ============================================================
  110. // utils
  111. // ============================================================
  112. function esc(s) {
  113. return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
  114. }
  115. function fmtNum(n) {
  116. if (n == null || isNaN(n)) return '—';
  117. if (n >= 10000) return (n / 10000).toFixed(1).replace(/\.0$/, '') + 'w';
  118. if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
  119. return String(n);
  120. }
  121. function sn(n) { return String(n).padStart(2, '0'); }
  122. // ============================================================
  123. // SLIDE_RENDERERS 注册表
  124. // ============================================================
  125. const SLIDE_RENDERERS = {
  126. // Ch1 全景漏斗
  127. 'funnel-1': CH1.renderSub1,
  128. 'funnel-2': CH1.renderSub2,
  129. 'funnel-3': CH1.renderSub3,
  130. // Ch2 嵌套洞察
  131. 'nested-1': CH2.renderSub1,
  132. 'nested-2': CH2.renderSub2,
  133. 'nested-3': CH2.renderSub3,
  134. 'nested-4': CH2.renderSub4,
  135. 'nested-5': CH2.renderSub5,
  136. // Ch3 痛声地图
  137. 'pain-1': CH3.renderSub1,
  138. 'pain-2': CH3.renderSub2,
  139. 'pain-3': CH3.renderSub3,
  140. 'pain-4': CH3.renderSub4,
  141. // Ch4 产品适配
  142. 'fit-1': CH4.renderSub1,
  143. 'fit-2': CH4.renderSub2,
  144. 'fit-3': CH4.renderSub3,
  145. 'fit-4': CH4.renderSub4,
  146. // Ch5 定位推导
  147. 'position-1': CH5.renderSub1,
  148. 'position-2': CH5.renderSub2,
  149. 'position-3': CH5.renderSub3,
  150. 'position-4': CH5.renderSub4,
  151. // Ch6 行动蓝图
  152. 'action-1': CH6.renderSub1,
  153. 'action-2': CH6.renderSub2,
  154. 'action-3': CH6.renderSub3,
  155. };
  156. // ============================================================
  157. // 共用组件
  158. // ============================================================
  159. function renderNav() {
  160. const dots = [
  161. { tip: '封面', href: '#s0' },
  162. { tip: '目录', href: '#agenda' },
  163. ...CHAPTERS.map((c) => ({ tip: `${c.num} ${c.title}`, href: `#${c.key}` })),
  164. ];
  165. return `<nav class="nav-rail" aria-label="章节导航">
  166. ${dots.map((d, i) => `<a class="nav-dot${i === 0 ? ' active' : ''}" href="${d.href}" data-tip="${esc(d.tip)}" data-idx="${i}"></a>`).join('')}
  167. </nav>
  168. <div class="top-bar">
  169. <div class="brand-logo">江中<span class="dot">·</span>产品开发部 / 猴菇饮 OTC VOC 深度洞察 v2.0</div>
  170. <div class="text-mono text-xs" style="color:var(--text-3);">用户决策路径追踪 / 嵌套逻辑 / 里斯×特劳特定位</div>
  171. </div>`;
  172. }
  173. function renderCover(opts = {}) {
  174. const data = HGY.loadMerged();
  175. const meta = HGY.getMeta(data);
  176. return `<section class="slide cover-slide" id="s0" data-slide-num="01/${sn(opts.slideTotal || 26)}">
  177. <div class="cover-inner">
  178. <div class="cover-eyebrow">JIANGZHONG · VOC DEEP INSIGHT</div>
  179. <h1 class="cover-title">猴菇饮 OTC<br><span class="accent" style="color:${BRAND.JIANG_GREEN};">用户决策路径 × 定位策略</span></h1>
  180. <div class="cover-subtitle">基于 <strong>${meta.rawItemsCount || '—'}</strong> 条原始 VOC → 筛选去噪去重 → <strong>${meta.comments || '—'}</strong> 条精准 VOC 的嵌套洞察 · v2.0</div>
  181. <div class="cover-meta">
  182. <div class="meta-item"><span class="meta-label">方法论</span><span class="meta-value">用户决策路径追踪 × 里斯品类战略 × 特劳特定位</span></div>
  183. <div class="meta-item"><span class="meta-label">数据源</span><span class="meta-value">${Object.keys(meta.platforms || {}).length} 平台 / ${meta.keywords || 0} 关键词</span></div>
  184. <div class="meta-item"><span class="meta-label">数据管线</span><span class="meta-value">${meta.rawItemsCount || 0} 条采集 → 去噪去重 → ${meta.comments || 0} 条精准 VOC(过滤率 ${meta.rawItemsCount ? Math.round((1 - meta.comments / meta.rawItemsCount) * 100) : 0}%)</span></div>
  185. <div class="meta-item"><span class="meta-label">报告结构</span><span class="meta-value">6 章 · 全景→嵌套→痛声→适配→定位→行动</span></div>
  186. <div class="meta-item"><span class="meta-label">生成日期</span><span class="meta-value">${opts.collectedAt || new Date().toISOString().slice(0, 10)}</span></div>
  187. </div>
  188. </div>
  189. </section>`;
  190. }
  191. function renderAgenda(opts = {}) {
  192. return `<section class="slide agenda-slide" id="agenda" data-slide-num="02/${sn(opts.slideTotal || 26)}">
  193. <div class="section-inner">
  194. <div class="agenda-eyebrow">REPORT STRUCTURE</div>
  195. <h2 class="agenda-title">报告结构 · 用户决策路径追踪</h2>
  196. <p style="color:var(--text-2); font-size:0.92rem; margin:12px 0 28px; max-width:680px; line-height:1.7;">
  197. v2 改变了叙事逻辑:不再是"我用了什么分析方法",而是<strong>"用户在经历什么 → 每一步选了什么 → 哪里没被满足 → 我们切哪里 → 怎么定位"</strong>。<br>
  198. 嵌套结构:病程阶段(第一层)→ 细分症状(第二层)→ 产品选择 × 未满足需求(第三层)。
  199. </p>
  200. <div class="agenda-grid" style="display:grid; grid-template-columns:repeat(3, 1fr); gap:16px;">
  201. ${CHAPTERS.map((c) => `
  202. <a class="agenda-card" href="#${c.key}" style="border-left:4px solid ${c.color}; padding:16px 18px; background:var(--bg-1); border-radius:var(--radius-sm); text-decoration:none; display:block;">
  203. <div style="display:flex; align-items:center; gap:8px; margin-bottom:8px;">
  204. <span style="font-size:1.3rem;">${c.icon}</span>
  205. <span class="text-mono text-xs" style="color:${c.color}; font-weight:700; letter-spacing:0.12em;">${c.en}</span>
  206. </div>
  207. <div style="font-size:0.95rem; font-weight:700; color:var(--text-1); margin-bottom:4px;">Ch${c.num} · ${esc(c.title)}</div>
  208. <div style="font-size:0.78rem; color:var(--text-3); line-height:1.5;">${esc(c.summary).slice(0, 60)}…</div>
  209. </a>`).join('')}
  210. </div>
  211. </div>
  212. </section>`;
  213. }
  214. function renderDivider(chapter, slideNum, slideTotal) {
  215. const c = chapter;
  216. const heroLine = CHAPTER_HERO_LINES[c.key] || c.summary;
  217. return `<section class="slide divider-slide" id="${c.key}" data-tone="${c.tone}" data-slide-num="${sn(slideNum)}/${sn(slideTotal)}">
  218. <div class="divider-inner">
  219. <div class="divider-eyebrow" style="color:${c.color};">${c.icon} CHAPTER ${c.num}</div>
  220. <h2 class="divider-title" style="color:${c.color};">${esc(c.title)}</h2>
  221. <div class="divider-en" style="color:${c.color}80;">${c.en}</div>
  222. <div class="divider-hero">${heroLine}</div>
  223. <div class="divider-subsections">
  224. ${(CHAPTER_SUBSECTIONS[c.key] || []).map((s, i) => `<div class="divider-sub-item"><span class="sub-num" style="color:${c.color};">${c.num}.${i + 1}</span> ${esc(s)}</div>`).join('')}
  225. </div>
  226. </div>
  227. </section>`;
  228. }
  229. function renderComingSoon(ctx) {
  230. const { chapter: c, subIdx, slideNum, slideTotal, subTitle } = ctx;
  231. return `<section class="report-section" id="${c.key}-${subIdx}" data-tone="${c.tone}" data-slide-num="${sn(slideNum)}/${sn(slideTotal)}">
  232. <div class="section-inner" style="display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:480px;">
  233. <div style="font-size:3rem; margin-bottom:16px; opacity:0.5;">${c.icon}</div>
  234. <div class="text-mono text-xs" style="color:${c.color}; letter-spacing:0.15em; margin-bottom:8px;">Ch${c.num}.${subIdx} · ${esc(c.title)}</div>
  235. <h3 style="color:var(--text-1); font-size:1.4rem; margin-bottom:12px;">${esc(subTitle || '')}</h3>
  236. <div style="color:var(--text-3); font-size:0.88rem;">内容将在下一批次填充 · Coming Soon</div>
  237. </div>
  238. </section>`;
  239. }
  240. // ============================================================
  241. // 主组装
  242. // ============================================================
  243. function buildHTML(opts = {}) {
  244. const data = HGY.loadMerged();
  245. const meta = HGY.getMeta(data);
  246. // 计算 slide 总数
  247. let slideTotal = 2;
  248. for (const c of CHAPTERS) {
  249. slideTotal += 1 + (CHAPTER_SUBSECTIONS[c.key] || []).length;
  250. }
  251. const slides = [];
  252. slides.push(renderCover({ ...opts, slideTotal }));
  253. slides.push(renderAgenda({ slideTotal }));
  254. let slideNum = 3;
  255. for (const c of CHAPTERS) {
  256. // divider
  257. slides.push(renderDivider(c, slideNum, slideTotal));
  258. slideNum++;
  259. // subsections
  260. const subs = CHAPTER_SUBSECTIONS[c.key] || [];
  261. for (let i = 0; i < subs.length; i++) {
  262. const subIdx = i + 1;
  263. const rendererKey = `${c.key}-${subIdx}`;
  264. const renderer = SLIDE_RENDERERS[rendererKey];
  265. const ctx = {
  266. chapter: c,
  267. subIdx,
  268. slideNum,
  269. slideTotal,
  270. subTitle: subs[i],
  271. data,
  272. meta,
  273. };
  274. if (renderer) {
  275. try {
  276. slides.push(renderer(ctx));
  277. } catch (err) {
  278. console.warn(` ⚠ ${rendererKey} 渲染失败: ${err.message}`);
  279. slides.push(renderComingSoon(ctx));
  280. }
  281. } else {
  282. slides.push(renderComingSoon(ctx));
  283. }
  284. slideNum++;
  285. }
  286. }
  287. return `<!DOCTYPE html>
  288. <html lang="zh-CN">
  289. <head>
  290. <meta charset="UTF-8">
  291. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  292. <title>江中猴菇饮 OTC · VOC 深度洞察 v2 · 用户决策路径追踪</title>
  293. ${STYLES}
  294. <style>
  295. /* ── v2 专属样式补丁(cover / agenda / divider) ── */
  296. .slide {
  297. min-height: 100vh; position: relative; border-bottom: 1px solid var(--border);
  298. display: flex; flex-direction: column; justify-content: center;
  299. scroll-snap-align: start; scroll-snap-stop: always; overflow: hidden; isolation: isolate;
  300. }
  301. .slide::before {
  302. content: attr(data-slide-num);
  303. position: absolute; top: clamp(20px,3vh,36px); right: clamp(24px,4vw,60px);
  304. font-family: var(--font-mono); font-weight: 600; font-size: clamp(0.68rem,0.75vw,0.78rem);
  305. letter-spacing: 0.25em; color: var(--text-3); z-index: 4; pointer-events: none;
  306. }
  307. /* ── Cover ── */
  308. .cover-slide {
  309. padding: clamp(80px,12vh,140px) clamp(24px,6vw,80px);
  310. background:
  311. radial-gradient(ellipse 60% 80% at 30% 30%, rgba(245,166,35,0.08) 0%, transparent 70%),
  312. radial-gradient(ellipse 40% 60% at 80% 70%, rgba(139,92,246,0.05) 0%, transparent 70%),
  313. var(--bg-0);
  314. }
  315. .cover-inner { max-width: var(--max-w); width: 100%; position: relative; z-index: 2; }
  316. .cover-eyebrow {
  317. font-family: var(--font-mono); font-size: 0.78rem; letter-spacing: 0.22em;
  318. text-transform: uppercase; color: var(--text-3); margin-bottom: 32px;
  319. display: flex; align-items: center; gap: 14px;
  320. }
  321. .cover-eyebrow::before { content: ''; width: 32px; height: 2px; background: var(--amber); }
  322. .cover-slide .cover-title {
  323. font-family: var(--font-head); font-weight: 900; letter-spacing: -0.04em;
  324. line-height: 0.92; font-size: clamp(3.5rem,11vw,9rem); color: #fff;
  325. background: linear-gradient(135deg, #fff 30%, rgba(245,166,35,0.9) 100%);
  326. -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
  327. }
  328. .cover-slide .cover-title .accent { -webkit-text-fill-color: currentColor; }
  329. .cover-subtitle {
  330. font-size: clamp(1rem,1.6vw,1.4rem); color: var(--text-2); max-width: 780px;
  331. margin-top: 28px; line-height: 1.7;
  332. }
  333. .cover-slide .cover-meta {
  334. display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px;
  335. margin-top: clamp(40px,6vh,72px); padding-top: clamp(20px,3vh,36px);
  336. border-top: 1px solid var(--border); max-width: 900px;
  337. }
  338. .meta-item {
  339. display: flex; flex-direction: column; gap: 4px;
  340. padding: 10px 14px; background: var(--bg-1); border: 1px solid var(--border);
  341. border-radius: var(--radius-sm);
  342. }
  343. .meta-label {
  344. font-family: var(--font-mono); font-size: 0.65rem; letter-spacing: 0.15em;
  345. text-transform: uppercase; color: var(--text-3); font-weight: 500;
  346. }
  347. .meta-value { font-size: 0.85rem; color: var(--text-1); font-weight: 600; }
  348. /* ── Agenda ── */
  349. .agenda-slide {
  350. padding: clamp(60px,10vh,120px) clamp(24px,6vw,80px);
  351. background: var(--bg-0);
  352. }
  353. .agenda-eyebrow {
  354. font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.18em;
  355. text-transform: uppercase; color: var(--amber); font-weight: 500; margin-bottom: 14px;
  356. display: flex; align-items: center; gap: 10px;
  357. }
  358. .agenda-eyebrow::before { content: ''; width: 24px; height: 1px; background: var(--amber); }
  359. .agenda-title {
  360. font-family: var(--font-head); font-weight: 700; letter-spacing: -0.02em;
  361. line-height: 1.1; font-size: clamp(2rem,4vw,3rem); color: var(--text-1);
  362. }
  363. .agenda-card { transition: all 0.35s cubic-bezier(.2,.7,.2,1); }
  364. .agenda-card:hover { transform: translateY(-4px); border-color: var(--border-strong); box-shadow: 0 12px 40px rgba(0,0,0,0.35); }
  365. /* ── Chapter Divider ── */
  366. .divider-slide {
  367. padding: clamp(80px,12vh,140px) clamp(24px,6vw,80px);
  368. background: var(--bg-0); text-align: left;
  369. }
  370. .divider-slide::after {
  371. content: ''; position: absolute; inset: 0; pointer-events: none; z-index: -1;
  372. background:
  373. radial-gradient(ellipse 70% 60% at 85% 15%, var(--slide-glow-1, rgba(245,166,35,0.06)), transparent 55%),
  374. radial-gradient(ellipse 60% 50% at 15% 85%, var(--slide-glow-2, rgba(139,92,246,0.04)), transparent 55%);
  375. }
  376. .divider-slide[data-tone="amber"] { --slide-glow-1: rgba(245,166,35,0.10); --slide-glow-2: rgba(245,166,35,0.04); }
  377. .divider-slide[data-tone="rose"] { --slide-glow-1: rgba(255,77,141,0.09); --slide-glow-2: rgba(255,77,141,0.04); }
  378. .divider-slide[data-tone="green"] { --slide-glow-1: rgba(0,220,130,0.08); --slide-glow-2: rgba(0,220,130,0.03); }
  379. .divider-slide[data-tone="purple"] { --slide-glow-1: rgba(139,92,246,0.10); --slide-glow-2: rgba(139,92,246,0.04); }
  380. .divider-slide[data-tone="blue"] { --slide-glow-1: rgba(59,130,246,0.08); --slide-glow-2: rgba(59,130,246,0.03); }
  381. .divider-slide[data-tone="orange"] { --slide-glow-1: rgba(255,140,66,0.10); --slide-glow-2: rgba(255,140,66,0.04); }
  382. .divider-inner { max-width: var(--max-w); width: 100%; position: relative; }
  383. .divider-eyebrow {
  384. font-family: var(--font-mono); font-size: 0.78rem; letter-spacing: 0.22em;
  385. text-transform: uppercase; font-weight: 600; margin-bottom: 16px;
  386. }
  387. .divider-title {
  388. font-family: var(--font-head); font-weight: 900; letter-spacing: -0.03em;
  389. line-height: 1; font-size: clamp(3rem,8vw,6rem); margin-bottom: 8px;
  390. }
  391. .divider-en {
  392. font-family: var(--font-mono); font-size: 0.78rem; letter-spacing: 0.22em;
  393. text-transform: uppercase; margin-bottom: 28px;
  394. }
  395. .divider-hero {
  396. font-size: clamp(0.95rem,1.3vw,1.15rem); color: var(--text-2); line-height: 1.8;
  397. max-width: 860px; margin-bottom: 32px;
  398. }
  399. .divider-subsections { display: flex; flex-direction: column; gap: 10px; max-width: 700px; }
  400. .divider-sub-item {
  401. font-size: 0.88rem; color: var(--text-2); line-height: 1.5;
  402. padding: 8px 14px; background: var(--bg-1); border: 1px solid var(--border);
  403. border-radius: var(--radius-sm); display: flex; align-items: center; gap: 10px;
  404. }
  405. .sub-num {
  406. font-family: var(--font-mono); font-weight: 700; font-size: 0.78rem;
  407. letter-spacing: 0.1em; min-width: 28px;
  408. }
  409. </style>
  410. </head>
  411. <body>
  412. ${renderNav()}
  413. <main class="slides-container">
  414. ${slides.join('\n')}
  415. </main>
  416. <script>${SCRIPTS}</script>
  417. </body>
  418. </html>`;
  419. }
  420. module.exports = {
  421. buildHTML,
  422. CHAPTERS,
  423. CHAPTER_SUBSECTIONS,
  424. BRAND,
  425. CHAPTER_HERO_LINES,
  426. };