PageSourceSearch

https://monvane-pdp-deep-dive-2026.behost-lab.chatgpt.site/slides/app.js

js monvane-pdp-deep-dive-2026.behost-lab.chatgpt.site collected 2026-10-03 04:00:12 UTC 87,811 bytes, 1,453 lines download raw bytes

1(() => {
2  "use strict";
3
4  const titles = [
5    "八个官网 PDP,不是八种审美,而是四套购买风险关闭系统",
6    "领先页面不是“更多内容”,而是四类风险都有证据闭环",
7    "八个样本统一到 4–6 座模块沙发,用“三层页面”逐项审计",
8    "八站真正拉开差距的是媒体任务、配置深度与风险承诺",
9    "八个品牌已分化为四种 PDP 原型",
10    "Article 首屏用 7+1 张任务图,一次说清布局、材质与到货",
11    "Article 的“轻 A+”用结构化文本与服务承诺替代长图瀑布",
12    "Article 把 AR 从主图库入口延伸到手机空间验证",
13    "Burrow 首屏用图与动作视频解释“怎么配、怎么变”",
14    "Burrow 下半页反复证明:一套模块能陪用户持续变化",
15    "Albany 首屏的重点:32 款面料如何被筛选,并降低选错风险",
16    "Albany 用 8 Benefits、坐感证据与 AI VOC 完成决策服务",
17    "Lovesac 用 9 张首图、1 个 GIF,把沙发讲成可持续升级系统",
18    "Lovesac 的 A+ 是一套购买后操作系统,而不是卖点长卷",
19    "HONBAY 首屏把 9 图/色、尺寸与 10 包裹变成价格力",
20    "HONBAY 用 2 条 UGC + 6 张 A+ 把功能变成证据",
21    "LINSY:24 张图证明“能升级”,附件瀑布却拖慢证据",
22    "LINSY 的 4 张 A+ 逻辑清楚,问题只在证据到达太晚",
23    "La-Z-Boy 用 3D、662 个包覆选项和体型适配支撑高价",
24    "La-Z-Boy 以服务、工艺、文档和门店完成高价信任",
25    "West Elm 用少图、面料与白手套建立高端确定性",
26    "West Elm 用少而强的模块讲清舒适、工艺与送装",
27    "主图库的有效指标不是张数,而是每张是否回答不同问题",
28    "视频只在解释“变化、动作、真实使用”时有价值",
29    "A+ 的价值不在数量,而在每段是否把卖点变成证据",
30    "高转化页面遵循同一顺序:先选对,再证明,再交付,最后反证",
31    "原稿方向正确,但三处必须升级、五项需要重新定位",
32    "Monvane 首屏只需要 10 个高任务密度媒体",
33    "Monvane 下半页建议 12 段:一疑问、一视觉、一字段",
34    "页面应按“选择—证明—交付—反证”重排",
35    "先补媒体与交易透明,再补筛选与 VOC"
36  ];
37
38  function nativeText(order, bbox, text, options = {}) {
39    const {
40      size = 16,
41      color = "#14202A",
42      bold = false,
43      font = "Arial",
44      align = "left",
45      valign = "top",
46      wrap = "square"
47    } = options;
48    return {
49      order,
50      kind: "shape",
51      name: `AR Text ${order}`,
52      bbox,
53      rotation: 0,
54      geometry: "rect",
55      fillColor: null,
56      lineColor: null,
57      lineWidth: 0,
58      text,
59      textStyle: {
60        alignment: align,
61        verticalAlignment: valign,
62        wrap,
63        insets: { top: 0, right: 0, bottom: 0, left: 0 }
64      },
65      paragraphs: [{
66        alignment: align,
67        marginLeft: 0,
68        runs: [{ text, fontSize: size, typeface: font, color, bold, alignment: null }]
69      }]
70    };
71  }
72
73  function nativePanel(order, bbox, fillColor, lineColor = null, lineWidth = 0) {
74    return {
75      order,
76      kind: "shape",
77      name: `AR Panel ${order}`,
78      bbox,
79      rotation: 0,
80      geometry: "rect",
81      fillColor,
82      lineColor,
83      lineWidth,
84      text: "",
85      textStyle: null,
86      paragraphs: []
87    };
88  }
89
90  function nativeImage(order, bbox, asset) {
91    return {
92      order,
93      kind: "image",
94      name: `AR Image ${order}`,
95      bbox,
96      rotation: 0,
97      asset,
98      text: "",
99      textStyle: null,
100      paragraphs: []
101    };
102  }
103
104  const articleArSlide = {
105    page: 8,
106    background: "#F7F5EF",
107    elements: [
108      nativeText(1, [44, 25, 1168, 50], "Article 把 AR 从主图库入口延伸到手机空间验证", { size: 34, bold: true, font: "Georgia", wrap: "none" }),
109      nativeText(2, [45, 79, 1120, 30], "桌面端只负责触发;扫码后由 iPhone / iPad 完成真实空间落位。", { size: 20, color: "#5C6670", wrap: "none" }),
110      nativePanel(3, [44, 116, 1192, 2], "#C7D6D1"),
111
112      nativePanel(4, [44, 132, 348, 36], "#0F6E5F"),
113      nativeText(5, [62, 140, 300, 22], "01|主图库入口", { size: 16, color: "#FFFFFF", bold: true, wrap: "none" }),
114      nativePanel(6, [44, 172, 348, 383], "#FFFFFF", "#C7D6D1", 1),
115      nativeImage(7, [44, 172, 348, 383], "article-ar/article-ar-entry.png"),
116      nativeText(8, [44, 566, 348, 46], "“View in your space” 与主图同层,\nAR 被定义为图库中的空间验证任务。", { size: 15, color: "#5C6670" }),
117
118      nativePanel(9, [416, 132, 500, 36], "#D18B00"),
119      nativeText(10, [434, 140, 450, 22], "02|扫码跨端接力", { size: 16, color: "#FFFFFF", bold: true, wrap: "none" }),
120      nativePanel(11, [416, 172, 500, 250], "#FFFFFF", "#C7D6D1", 1),
121      nativeImage(12, [416, 172, 500, 250], "article-ar/article-ar-qr.png"),
122      nativePanel(13, [416, 438, 500, 158], "#E8EFEC", "#C7D6D1", 1),
123      nativeText(14, [438, 456, 458, 28], "桌面触发,手机完成", { size: 20, color: "#0F6E5F", bold: true, wrap: "none" }),
124      nativeText(15, [438, 493, 454, 78], "点击后弹出二维码;用户用 iPhone / iPad 扫码进入。\n官网同时写明 iOS / iPadOS 12+ 的设备要求。", { size: 16, color: "#14202A" }),
125
126      nativePanel(16, [940, 132, 296, 36], "#14202A"),
127      nativeText(17, [958, 140, 252, 22], "03|真实空间落位", { size: 16, color: "#FFFFFF", bold: true, wrap: "none" }),
128      nativePanel(18, [940, 172, 296, 137], "#FFFFFF", "#C7D6D1", 1),
129      nativeImage(19, [940, 172, 296, 137], "article-ar/article-ar-result.jpg"),
130      nativePanel(20, [940, 325, 296, 271], "#FFFFFF", "#C7D6D1", 1),
131      nativeText(21, [962, 345, 252, 28], "AR 结果回答空间风险", { size: 20, color: "#0F6E5F", bold: true, wrap: "none" }),
132      nativeText(22, [962, 384, 250, 142], "移动端把选中沙发放入真实房间,\n直接检查:\n\n· 尺度是否适配\n· 围合感是否合适\n· 通行动线是否被占用", { size: 16, color: "#14202A" }),
133      nativeText(23, [962, 548, 250, 28], "从“看图”推进到“试摆”。", { size: 16, color: "#D18B00", bold: true, wrap: "none" }),
134
135      nativePanel(24, [44, 625, 1192, 46], "#14202A"),
136      nativeText(25, [64, 638, 1150, 24], "关键价值|AR 不增加桌面配置负担,而是在下单前关闭“放不放得下、摆起来什么样”的空间风险。", { size: 17, color: "#FFFFFF", bold: true, wrap: "none" }),
137      nativeText(26, [44, 687, 950, 22], "来源:Article Leigh 官方 PDP · 用户提供截图与手机 AR 实测 · 2026-08-04", { size: 14, color: "#5C6670", wrap: "none" }),
138      nativeText(27, [1160, 687, 76, 22], "08 / 31", { size: 14, color: "#0F6E5F", align: "right", wrap: "none" })
139    ]
140  };
141
142  const livePages = {
143    7:  { brand: "Article",  asset: "longpage/article-fullpage.png", bbox: [44, 162, 202, 472] },
144    10: { brand: "Burrow",   asset: "longpage/burrow-fullpage.png", bbox: [44, 162, 152, 472] },
145    12: { brand: "Albany",   asset: "longpage/albany-fullpage.png", bbox: [44, 162, 165, 476] },
146    14: { brand: "Lovesac",  asset: "longpage/lovesac-fullpage.png", bbox: [44, 162, 125, 472] },
147    16: { brand: "HONBAY",   asset: "longpage/honbay-fullpage-verified.jpg", bbox: [260, 132, 310, 258] },
148    18: { brand: "LINSY",    asset: "longpage/linsy-fullpage-verified.jpg", bbox: [44, 252, 172, 386] },
149    20: { brand: "La-Z-Boy", asset: "longpage/lazboy-fullpage.png", bbox: [44, 162, 230, 472] },
150    22: { brand: "West Elm", asset: "longpage/westelm-fullpage.png", bbox: [44, 162, 300, 472] }
151  };
152
153  function makeOfficialGallery({
154    dir,
155    prefix,
156    count,
157    extensions = "jpg",
158    start = 1,
159    titleFor,
160    description,
161    descriptionFor,
162    entries,
163    source
164  }) {
165    return Array.from({ length: count }, (_, index) => {
166      const number = index + start;
167      const fileNumber = String(number).padStart(2, "0");
168      const extension = Array.isArray(extensions) ? extensions[index] : extensions;
169      const entry = entries?.[index] || {};
170      return {
171        src: `assets/evidence/${dir}/${prefix}-${fileNumber}.${extension}`,
172        title: entry.title || (titleFor ? titleFor(number, index) : `${fileNumber}|官网产品图库原图`),
173        description: entry.description || (descriptionFor
174          ? descriptionFor(number, index)
175          : description || `按官网当前主图库顺序展示的第 ${number} 张原图。`),
176        source
177      };
178    });
179  }
180
181  function makeDetailSteps({ src, source, sections, scroll = false, segmentPrefix = "" }) {
182    return sections.map((section, index) => ({
183      src: section.src || (segmentPrefix
184        ? `${segmentPrefix}
184-${String(index + 1).padStart(2, "0")}.webp`
185        : src),
186      title: `${String(index + 1).padStart(2, "0")}|${section.title}`,
187      description: section.description,
188      source,
189      mode: section.mode || (scroll && !segmentPrefix ? "scroll" : "panel"),
190      focus: section.focus ?? 0
191    }));
192  }
193
194  const albanyFrameCopy = [
195    ["正面基准视角", "正面同时看到左侧贵妃位、中央座位和右侧转角,作为判断 Kova Box Cushion Corner Sectional 整体轮廓的基准帧。"],
196    ["左前轻转视角", "轻微转向左侧后,贵妃位伸出长度与中央模块进深更清楚,补充正面图的空间占用信息。"],
197    ["左前斜侧视角", "斜侧角度突出左端扶手厚度、坐垫前缘和转角内侧,便于观察箱型坐垫的立体感。"],
198    ["左侧外轮廓", "视线移向左侧,开始显露连续外侧包覆和低脚结构,说明沙发侧面离地高度与体量。"],
199    ["左后过渡视角", "从左后方观察贵妃模块与长背板的连接,模块接缝和背部包覆关系逐渐清晰。"],
200    ["左后斜视角", "左后角度集中展示背板分段、转角外轮廓以及靠垫高出框架的层次。"],
201    ["后侧偏左视角", "接近后侧时可看到长背板连续性与多个模块底座的拼接点,适合判断开放空间中的背面观感。"],
202    ["后方左移视角", "后方偏左的视线减少座面干扰,主要核对背板高度、纵向拼缝和低脚分布。"],
203    ["背面正视角", "背面正视完整呈现长边包覆、模块分段与统一高度,回答沙发不靠墙摆放时是否整洁。"],
204    ["后方右移视角", "从背面向右移动后,外侧转角和短边背板开始出现,可辨认 L 形组合的外部折线ã€
204‚"],
205    ["右后长边视角", "右后斜视同时呈现长背板与转角短边,突出两条外轮廓在角部的衔接方式。"],
206    ["外转角后视角", "镜头正对组合外转角,清楚看到两侧背板、底座模块和转角处的连续包覆。"],
207    ["外转角展开视角", "稍微展开的后视角把两条背面延伸方向同时纳入画面,便于判断转角组合的占地形态。"],
208    ["右后斜侧视角", "右后方突出短边模块的背面宽度、转角模块厚度和靠垫高低层次。"],
209    ["右后侧面视角", "镜头进一步转向右侧,短边外侧成为主体,展示扶手外形和底座侧面的完整比例。"],
210    ["右侧偏后视角", "右侧偏后角度核对短边长度、后框厚度与低脚位置,座面只保留少量可见信息。"],
211    ["右侧外轮廓视角", "接近纯侧视时,沙发的总体进深、框架高度和靠垫前后落差最容易被识别。"],
212    ["右侧前移视角", "视线由侧面向前移动,扶手、坐垫前缘和短边座位重新进入画面,开始恢复使用面信息。"],
213    ["右前过渡视角", "右前斜视突出右端扶手包覆、相邻坐垫接缝和转角内侧的围合程度。"],
214    ["右前斜侧视角", "从右前方同时看到短边与长边座位,便于比较两条坐区的座深和靠垫排列。"],
215    ["右前结构视角", "镜头继续靠近正面,转角座、中央模块和右端扶手之间的组合关系更明确。"],
216    ["右前开放视角", "右前角度把转角开口和长边座位展开,强调多人围坐时的通行入口与视线关系。"],
217    ["正面偏右视角", "正面偏右画面突出右侧坐区、中央转角和贵妃位远端,验证不对称组合的视觉平衡。"],
218    ["正面右侧全景", "大部分座面已正对镜头,能够核对坐垫数量、靠垫宽度和左右两端的长度差。"],
219    ["正面偏右基准", "接近正面时重点呈现箱型坐垫的分段、低扶手与深座比例,提供多人使用的直观尺度。"],
220    ["正前转角视角", "镜头正对内转角,长边与短边的座位关系最清晰,也能看到靠垫在角部的包围感。"],
221    ["正面全组合视角", "完整展开 L 形座区与贵妃端,清楚交代全部模块、开放边界和整体占地宽度。"],
222    ["正面居中视角", "居中正视减少透视偏差,用于比较各坐垫宽度、靠背分段和两端扶手高度。"],
223    ["正面偏左视角", "从正面向左轻转,贵妃位成为视觉重点,能够判断躺卧长度及其与中央坐区的连接。"],
224    ["左前全景视角", "左前全景同时表现贵妃位、转角和长边座位,最接近用户进入客厅时的观看角度。"],
225    ["左前近正视角", "较小的左向偏转强调贵妃端坐垫厚度、左扶手与中央模块的连续性。"],
226    ["回到正面视角", "旋转序列回到正面附近,以左侧贵妃位和完整长边结束一周观察,便于与首帧连续对照。"]
227  ];
228
229  const evidencePages = {
230    6: {
231      brand: "Article",
232      title: "Article|7 张产品任务图 + 1 张尺寸图",
233      url: "https://www.article.com/product/29338/leigh-121-5-modular-5-seater-u-shaped-sectional-silver-moss",
234      hotspots: [
235        { bbox: [55.84, 128, 714.32, 322], label: "查看 7+1 原图", start: 0 },
236        { bbox: [820.32, 130, 397.36, 188], label: "拆解右侧购买区", start: 0, collection: "detail", count: 7 }
237      ],
238      detail: {
239        title: "Article|右侧配置、加购与权益区拆解",
240        preview: "assets/detail/article-right-hd.webp",
241        items: makeDetailSteps({
242          src: "assets/detail/article-right-hd.webp",
243          source: "Article 官方 PDP · Leigh 5-Seater",
244          scroll: true,
245          segmentPrefix: "assets/detail/article-detail",
246          sections: [
247            { title: "商品身份与价格", description: "标题、$3,799 价格、分期入口与 3.0 评分先完成商品身份和预算判断。", focus: 0.02 },
248            { title: "Arrangement 布局", description: "9 个布局缩略图直接切换模块组合;当前选中 Closed Pit Sectional。", focus: 0.16 },
249            { title: "材质与颜色", description: "Featured Material 区分 Fabric / Velvet;Custom Color 提供 9 个色样并连接 Order Swatches。", focus: 0.32 },
250            { title: "购物车与收藏", description: "醒目的 ADD TO CART 是主动ä
250½œï¼Œå³ä¾§å¿ƒå½¢å…¥å£æ‰¿æŽ¥æ”¶è—æ„å›¾ã€‚", focus: 0.47 },
251            { title: "配送与定制交期", description: "邮编配送日期、定制生产提示与交付排期在加购按钮下即时说明。", focus: 0.56 },
252            { title: "保障与搭配购", description: "30 天满意保证降低退换风险;Complete the look with 用单品搭配扩展客单。", focus: 0.67 },
253            { title: "详情折叠区", description: "Description、Features、Specifications、Care & Assembly 四组折叠项承接深度信息。", focus: 0.84 }
254          ]
255        })
256      },
257      items: [
258        { src: "assets/evidence/article/article-1.jpg", title: "01|客厅情境主图", description: "完整 U 形组合置于真实客厅,回答整体风格、占地感与多人使用场景。", source: "Article 官方产品页 · SKU29338" },
259        { src: "assets/evidence/article/article-2.jpg", title: "02|三分之四产品角度", description: "去除环境干扰,清楚展示贵妃位、转角、扶手和各模块的组合关系。", source: "Article 官方产品页 · SKU29338" },
260        { src: "assets/evidence/article/article-3.jpg", title: "03|正面与坐感标尺", description: "正面视图确认座位数量、靠垫结构,并用软硬标尺补充坐感信息。", source: "Article 官方产品页 · SKU29338" },
261        { src: "assets/evidence/article/article-4.jpg", title: "04|背面结构", description: "从背后展示模块拼接、靠背高度和离墙摆放时的外观。", source: "Article 官方产品页 · SKU29338" },
262        { src: "assets/evidence/article/article-5.jpg", title: "05|后侧横向视图", description: "展示三段靠背的连续性与模块边界,帮助判断开放空间中的背面观感。", source: "Article 官方产品页 · SKU29338" },
263        { src: "assets/evidence/article/article-6.jpg", title: "06|俯视布局", description: "从上方确认 Closed Pit Sectional 的 U 形围合、通道开口与座位关系。", source: "Article 官方产品页 · SKU29338" },
264        { src: "assets/evidence/article/article-7.jpg", title: "07|Silver Moss 面料近景", description: "用织物微距回答颜色、纹理与日常触感想象,而不是重复整套沙发外观。", source: "Article 官方产品页 · SKU29338" },
265        { src: "assets/evidence/article/article-8.jpg", title: "08|独立尺寸图", description: "明确 120.5 英寸宽、81 英寸深、33 英寸高及关键模块尺度,直接关闭空间适配风险。", source: "Article 官方产品页 · SKU29338" }
266      ]
267    },
268    9: {
269      brand: "Burrow",
270      title: "Burrow|16 张静态产品图(官网另含 3 段视频)",
271      url: "https://burrow.com/products/range-plus-4-piece-sectional",
272      hotspots: [
273        { bbox: [55.84, 128, 714.32, 322], label: "查看 16 张原图", start: 0 },
274        { bbox: [820.32, 130, 397.36, 188], label: "拆解右侧购买区", start: 0, collection: "detail", count: 8 }
275      ],
276      detail: {
277        title: "Burrow|右侧配置、升级与购买区拆解",
278        preview: "assets/detail/burrow-right-hd.webp",
279        items: makeDetailSteps({
280          src: "assets/detail/burrow-right-hd.webp",
281          source: "Burrow 官方 PDP · Range Plus 4-Piece Sectional",
282          scroll: true,
283          segmentPrefix: "assets/detail/burrow-detail",
284          sections: [
285            { title: "评分、标题与价格", description: "评论量、商品名、常规价与促销码价格集中在顶部,先建立社会证明和价格锚点。", focus: 0.03 },
286            { title: "Configuration 配置", description: "布局缩略图与 See More Configurations 负责切换模块数量和朝向。", focus: 0.17 },
287            { title: "Fabric 面料", description: "当前 Stone Grey Performance Flatweave,以色块形式展示 5 个面料选择并提供 Order Swatches。", focus: 0.32 },
288            { title: "Leg Finish 腿色", description: "Black Metal 等 3 个腿部饰面独立成组,避免把结构件选择混入面料。", focus: 0.43 },
289            { title: "Upgrades 升级件", description: "Ottoman 与 Sleep Kit 以商品卡形式加入方案,直接展示价格。", focus: 0.54 },
290            { title: "保护与安装服务", description: "Add Protection 与 Add Furniture Assembly 用折叠行承接可选服务。", focus: 0.66 },
291            { title: "数量、加购与分期", description: "数量步进、Add to cart、Shop Pay 分期和折扣说明被压缩为连续交易动作。", focus: 0.76 },
292            { title: "库存与详情", description: "Ready to Ship 提供即时库存信号;下方 Details 继续承接材料养护、配送退货和安装说明。", focus: 0.91 }
293          ]
294        })
295      },
296      items: makeOfficialGallery({
297        dir: "burrow",
298        prefix: "burrow",
299        count: 16,
300        extensions: ["jpg", "jpg", "jpg", "jpg", "jpg", "jpg", "jpg", "png", "jpg", "jpg", "jpg", "jpg", "jpg", "jpg", "jpg", "jpg"],
301        entries: [
302          { title: "01|Stone Grey 白底正面图", description: "白底视角完整交代 4-piece 转角组合的座位数量、开放端、直线扶手与细金属脚,是判断当前配置轮廓的基准图。" },
303          { title: "02|转角组合斜侧视图", description: "从开放端斜看模块关系,突出转角座、靠背连续性与贵妃端伸出长度,补足正面图看不到的进深。" },
304          { title: "03|米色直排客厅场景", description: "用温暖木质客厅展示浅色 Range 系列的真实尺度、抱枕搭配与离墙摆放效果,回答产品进入日常空间后的风格表现。" },
305          { title: "04|双人真实使用场景", description: "两人坐姿把座宽、靠背高度和扶手承托转成可感知的人体尺度,同时证明直排组合的社交使用方式。" },
306          { title: "05|多色模块同场搭配", description: "同一客厅并置棕色与浅色组合,展示 Range 模块在不同颜色、朝向和空间分区中的搭配弹性。" },
307          { title: "06|深蓝转角客厅全景", description: "深蓝 L 形组合置于木质客厅,清楚呈现转角围合、多人座位和细腿架空带来的轻盈感。" },
308          { title: "07|深蓝组合后侧视图", description: "后侧角度重点展示连续包覆的低靠背、转角外轮廓与开放空间中的背面观感。" },
309          { title: "08|浅灰背面与脚凳", description: "从背后观察浅灰模块与独立脚凳,说明背板分段、靠背高度以及脚凳可脱离主体单独摆放。" },
310          { title: "09|开放端坐垫近景", description: "近距离呈现开放端坐垫厚度、圆柱腰枕、扶手缺省后的躺卧边界和 Performance Flatweave 表面。" },
311          { title: "10|贵妃位单人使用", description: "真人坐姿验证贵妃位长度、腿部承托和多人模块旁的个人休息区,补充白底产品图无法提供的坐感尺度。" },
312          { title: "11|不同配置的空间参考", description: "官网明确标注为不同配置示意,用分散模块展示转角、贵妃位和独立座可重新组合,而不是当前 4-piece 的一比一外观。" },
313          { title: "12|坐垫盘腿承托近景", description: "盘腿坐姿集中证明座面宽度、靠背包裹和面料在受力状态下的表现,回答非标准坐姿是否舒适。" },
314          { title: "13|三座直排版本场景", description: "三座直排版本展示同一设计语言在小型布局中的正面比例,并清楚看到圆柱腰枕、连续座垫与细腿。" },
315          { title: "14|单座模块人体尺度", description: "独立单座与真人同框,直接显示座高、座深、扶手高度及圆柱腰枕对侧坐姿的支撑。" },
316          { title: "15|模块功能标注图", description: "在客厅场景上集中标出蓬松靠垫、Performance Fabric、开放端贵妃位与可集成桌面等功能点,承担卖点索引作用。" },
317          { title: "16|三层坐垫结构说明", description: "剖面式放大强调升级后的三层泡棉坐垫:更深的舒适层与更扎实的支撑层共同改善柔软度和回弹。" }
318        ],
319        source: "Burrow 官方 PDP · Range Plus 4-Piece Sectional"
320      })
321    },
322    11: {
323      brand: "Albany Park",
324      title: "Albany Park|32 帧 360° 产品视图 + 7 张图片 + 1 段视频",
325      url: "https://www.albanypark.com/products/kova-corner-sectional?variant=42681627705395",
326      hotspots: [
327        { bbox: [55.84, 128, 714.32, 322], label: "查看 32 帧 + 7 图 + 1 视频", start: 0, count: 40 },
328        { bbox: [820.32, 130, 397.36, 188], label: "拆解右侧购买区", start: 0, collection: "detail", count: 7 }
329      ],
330      detail: {
331        title: "Albany Park|面料筛选、坐垫与服务区拆解",
332        preview: "assets/detail/albany-right-hd.webp",
333        items: makeDetailSteps({
334          src: "assets/detail/albany-right-hd.webp",
335          source: "Albany Park 官方 PDP · Kova Box Cushion Corner Sectional",
336          scroll: true,
337          segmentPrefix: "assets/detail/albany-detail",
338          sections: [
339            { title: "商品、价格与白手套", description: "商品名、现价/划线价、节省金额与 Complimentary White Glove Delivery 价值说明同时出现。", focus: 0.03 },
340            { title: "Choose Fabric", description: "32 款面料由 Filters 与色样网格承载;当前 Almond Performance Velvet 明确高亮。", focus: 0.18 },
341            { title: "免费色样", description: "Order free swatches 紧贴面料选择,解决屏幕颜色不确定性ã€
341‚", focus: 0.38 },
342            { title: "Choose Cushions", description: "Box Cushion / Pillow Cushion 用实拍卡片对比坐垫轮廓和坐感方向。", focus: 0.50 },
343            { title: "Add to Cart 与发货", description: "主加购按钮后直接给出预计发货区间,避免把交期藏到结算页。", focus: 0.64 },
344            { title: "权益说明", description: "Limited Lifetime Warranty、30-Day Returns、Custom Made to Order、Handcrafted in North America 四项承诺集中列出。", focus: 0.72 },
345            { title: "Description 与深度信息", description: "商品说明在同一购买栏继续展开;尺寸、安装与退换信息位于后续折叠模块。", focus: 0.86 }
346          ]
347        })
348      },
349      items: [
350        ...makeOfficialGallery({
351          dir: "albany",
352          prefix: "albany",
353          count: 32,
354          extensions: "webp",
355          titleFor: (number, index) => `${String(number).padStart(2, "0")}|360° ${albanyFrameCopy[index][0]}`,
356          descriptionFor: (number, index) => `${albanyFrameCopy[index][1]}(旋转序列 ${number}/32)`,
357          source: "Albany Park 官方 PDP · Kova Box Cushion Corner Sectional"
358        }),
359        { src: "assets/evidence/albany/albany-dimensions.webp", title: "33|产品尺寸图", description: "官网媒体条中的独立尺寸视图,完整展示 122 × 122 英寸转角布局,以及 31 / 39 英寸关键高度。", source: "Albany Park 官方 PDP · Kova Box Cushion Corner Sectional" },
360        { src: "assets/evidence/albany/albany-material.webp", title: "34|Almond Performance Velvet 面料特写", description: "2048px 面料近景,用于核对当前 Almond Performance Velvet 的颜色与表面纹理。", source: "Albany Park 官方 PDP · Kova Box Cushion Corner Sectional" },
361        { src: "assets/evidence/albany/albany-33.jpeg", title: "35|山景客厅正面场景", description: "大尺度转角组合在通透客厅中的正面视图,展示多人围合、模块比例与整体空间感。", source: "Albany Park 官方 PDP · Kova Box Cushion Corner Sectional" },
362        { src: "assets/evidence/albany/albany-34.png", title: "36|庭院采光客厅场景", description: "侧前方视角呈现坐深、转角关系、靠垫支撑和自然光下的浅色面料表现。", source: "Albany Park 官方 PDP · Kova Box Cushion Corner Sectional" },
363        { src: "assets/evidence/albany/albany-35.jpeg", title: "37|养宠家庭真实使用场景", description: "真实家庭中的宠物共处画面,补充沙发在高频生活环境里的尺度与使用状态。", source: "Albany Park 官方 PDP · Kova Box Cushion Corner Sectional" },
364        { src: "assets/evidence/albany/albany-36.jpeg", title: "38|双人沙发与脚凳搭配场景", description: "较小组合与独立脚凳的陈设方式,展示模块拆分后的空间适应性。", source: "Albany Park 官方 PDP · Kova Box Cushion Corner Sectional" },
365        { src: "assets/evidence/albany/albany-37.jpeg", title: "39|开放式客餐厅模块组合", description: "在开放式客餐厅中展示 L 形组合与脚凳,补充多模块拼接和通行动线信息。", source: "Albany Park 官方 PDP · Kova Box Cushion Corner Sectional" },
366        { type: "video", src: "assets/evidence/albany/albany-product-video.mp4", poster: "assets/evidence/albany/albany-video-poster.jpg", title: "40|官方模块拆装视频", description: "官网媒体条中的 1080p 产品视频,直接演示模块连接、拆分与重新布置的实际过程。", source: "Albany Park 官方 PDP · Kova Box Cushion Corner Sectional" }
367      ]
368    },
369    13: {
370      brand: "Lovesac",
371      title: "Lovesac|9 张官方产品图库原图",
372      url: "https://www.lovesac.com/sactionals/shop/6seats8sides-1424",
373      hotspots: [
374        { bbox: [55.84, 128, 714.32, 322], label: "查看 9 张原图", start: 0 },
375        { bbox: [820.32, 130, 397.36, 188], label: "拆解右侧配置区", start: 0, collection: "detail", count: 6 }
376      ],
377      detail: {
378        title: "Lovesac|组件级配置与购物车保护拆解",
379        preview: "assets/detail/lovesac-right-verified.webp",
380        items: makeDetailSteps({
381          src: "assets/detail/lovesac-right-verified.webp",
382          source: "Lovesac 官方 PDP · 6 Seats + 8 Sides",
383          sections: [
384            { title: "组合规格与当前价格", description: "“6 Seats + 8 Sides”用组件数量定义组合;官网当前将 $8,010.00 划线价与 $6,007.50 售价并置。", src: "assets/detail/lovesac-step-1-ver
384ified.webp" },
385            { title: "评分与评论量", description: "4.28 分与 8,369 条评价直接连接用户评论,为高客单模块组合补充社会证明。", src: "assets/detail/lovesac-step-2-verified.webp" },
386            { title: "扶手与靠背", description: "IN THIS SETUP 先列 Arm Style: Standard Arms 与 Back Style: Standard,把结构选择拆到组件层。", src: "assets/detail/lovesac-step-3-verified.webp" },
387            { title: "面料与填充", description: "Fabric 为 Beachwood Rained Chenille,Fill Type 为 Standard Foam,分别交代外观触感与坐感基础。", src: "assets/detail/lovesac-step-4-verified.webp" },
388            { title: "购物车保护计划", description: "Everyday Accident Protection 标注为 Available in Cart,并提供 What's Covered 入口,保护计划在购物车阶段继续选择。", src: "assets/detail/lovesac-step-5-verified.webp" },
389            { title: "配送、加购与融资", description: "Free Shipping in 1–2 Weeks 位于最终价格和 ADD TO CART 之前;按钮下方继续给出 24 个月融资提示。", src: "assets/detail/lovesac-step-6-verified.webp" }
390          ]
391        })
392      },
393      items: makeOfficialGallery({
394        dir: "lovesac",
395        prefix: "lovesac",
396        count: 9,
397        entries: [
398          { title: "01|6 Seats + 8 Sides 正面拆分图", description: "白底正面同时展示主组合、独立 Seat 与 Side 组件,直接说明 Sactionals 由可拆座位和侧板构成,而不是固定式沙发。" },
399          { title: "02|完整组合斜侧视图", description: "斜侧视角把 U 形围合、中央脚凳、Standard Arms 与 Standard Back 的连续关系完整展开。" },
400          { title: "03|Beachwood Rained Chenille 面料", description: "面料微距同时标注 Machine Washable、Family Friendly、Pet Friendly 与 100% Polyester,承担材质和养护证据。" },
401          { title: "04|独立 Seat 坐垫近景", description: "近距离展示座垫厚度、包边缝线和 Beachwood 色在自然光下的纹理,帮助判断坐垫结构与日常外观。" },
402          { title: "05|Standard Side 与杯架配件", description: "侧板特写呈现竖向绗缝、扶手高度及挂载式杯架,说明 Sactionals 配件可直接附着在 Side 上。" },
403          { title: "06|组合背面结构", description: "白底背面正视清楚显示 8 个 Sides 的分段、连接节奏和统一高度,回答开放式摆放时的背面整洁度。" },
404          { title: "07|大客厅多人围合场景", description: "真实空间中的大尺度围合组合展示多座位、脚凳、抱枕与通行开口,体现家庭聚会和多人使用方式。" },
405          { title: "08|可机洗面料抗污示意", description: "深色面料上的融化冰棒把日常污渍风险可视化,并用“150+ Washable Fabric Options”强化可拆洗包覆的价值。" },
406          { title: "09|后侧三分之四视图", description: "后侧角度同时观察侧板绗缝、转角连接与靠垫高差,补充正面图未展示的外轮廓。" }
407        ],
408        source: "Lovesac 官方 PDP · 6 Seats + 8 Sides"
409      })
410    },
411    15: {
412      brand: "HONBAY",
413      title: "HONBAY|Corduroy Green 当前配色 9 张原图",
414      url: "https://honbaymall.com/products/honbay-6-piece-plush-double-layer-cushion-corduroy-modular-u-shaped-sectional-cloud-sofa",
415      hotspots: [
416        { bbox: [55.84, 128, 714.32, 322], label: "查看 9 张原图", start: 0 },
417        { bbox: [820.32, 130, 397.36, 188], label: "拆解右侧购买区", start: 0, collection: "detail", count: 8 }
418      ],
419      detail: {
420        title: "HONBAY|价格、配置、包裹与权益区拆解",
421        preview: "assets/detail/honbay-right-verified.webp",
422        items: makeDetailSteps({
423          src: "assets/detail/honbay-right-verified.webp",
424          source: "HONBAY 官方 PDP · Opua 6-Piece Corduroy Cloud Sectional",
425          sections: [
426            { title: "商品身份、评价与价格", description: "官网当前为 Opua 6-Piece Corduroy Cloud Sectional;副标题注明 Double Layer Cushion、Modular、Storage,下方接评分、$2,269.99 售价与划线原价。", src: "assets/detail/honbay-step-1-ver
426ified.webp" },
427            { title: "活动价、额外优惠与分期", description: "BACK TO SCHOOL EVENT 显示 35% off;价格卡继续列额外 $100 优惠、枕头和税费 Included、总节省、免运费、4 期免息与 PayPal 月付入口。", src: "assets/detail/honbay-step-2-verified.webp" },
428            { title: "Configuration、Fabric 与 Color", description: "先用组合缩略图选择 Configuration,再确认 Corduroy 面料;当前颜色为 Corduroy Green,并提供另外两枚色样。", src: "assets/detail/honbay-step-3-verified.webp" },
429            { title: "尺寸与包裹入口", description: "View dimension 与 Package 并列放置;尺寸入口用于核对整体、座深、座高、扶手、脚凳与储物空间,包裹入口说明该组合分 10 箱发货且可能分批到达。", src: "assets/detail/honbay-step-4-verified.webp" },
430            { title: "详情、模块加购与安装", description: "The Details、Build Your Own OPUA—Explore、Sofa Installation Service 三个折叠入口依次承接商品细节、模块化加购和安装服务。", src: "assets/detail/honbay-step-5-verified.webp" },
431            { title: "数量、Add To Cart 与库存", description: "数量选择与 Add To Cart · $2,169.99 同行;下方以 In stock! Ready to Ship 给出当前库存状态。", src: "assets/detail/honbay-step-6-verified.webp" },
432            { title: "四项权益与支付方式", description: "Free Size Guide、4 Interest-Free / Afterpay、Free Shipping、30-Day Returns 四项权益成组展示,并继续列出官网当前支付标识。", src: "assets/detail/honbay-step-7-verified.webp" },
433            { title: "说明折叠区", description: "购买动作之后依次提供 Description、Shipping & Returns、Care & Buying Guide;再向下进入 Real Life, Real Comfort 内容。", src: "assets/detail/honbay-step-8-verified.webp" }
434          ]
435        })
436      },
437      items: makeOfficialGallery({
438        dir: "honbay",
439        prefix: "honbay",
440        count: 9,
441        extensions: ["webp", "jpg", "jpg", "jpg", "jpg", "jpg", "jpg", "jpg", "jpg"],
442        entries: [
443          { title: "01|Corduroy Green 白底组合图", description: "白底主图完整展示 6-piece U 形组合、双层靠垫与储物脚凳,并以 OEKO-TEX Standard 100 标识补充面料安全信息。" },
444          { title: "02|U 形客厅正面场景", description: "真实客厅正面视角展示多人围合、左右贵妃ä
444½å’Œæ·±ç»¿è‰²ç¯èŠ¯ç»’åœ¨è‡ªç„¶å…‰ä¸‹çš„è¡¨çŽ°ã€‚" },
445          { title: "03|U 形组合俯视场景", description: "高角度把每个模块、中央开口和两端脚凳的关系一次展开,便于判断整体占地与通行空间。" },
446          { title: "04|可调扶手与靠背", description: "三段连续动作图演示侧翼由直立到外翻的角度变化,说明扶手和靠背可按坐姿、躺姿调整。" },
447          { title: "05|双层蓬松坐垫结构", description: "剖视与手压特写强调表层蓬松填充和下层支撑共同工作,把“Plush double-layer cushion”转成可见证据。" },
448          { title: "06|31.5 英寸宽大座面", description: "座面近景直接标出 31.5 英寸深/宽尺度,突出盘腿、蜷坐和多人使用时的宽松空间。" },
449          { title: "07|三种模块组合示例", description: "同一套模块演示 U 形围合、紧凑转角与长直排三种布局,证明组合可以随房间和人数变化。" },
450          { title: "08|储物脚凳容量展示", description: "打开全部座下储物空间并标注脚凳 27 × 27 × 7.1 英寸,清楚说明毯子、玩具和杂物的收纳位置。" },
451          { title: "09|关键尺寸总览", description: "尺寸图集中标出 105.5 英寸双边长度、31.5 英寸座深、28.7 英寸总高、20.5 英寸座高等关键数据,用于核对房间适配。" }
452        ],
453        source: "HONBAY 官方 PDP · Opua 6-Piece Corduroy Cloud Sectional"
454      })
455    },
456    17: {
457      brand: "LINSY HOME",
458      title: "LINSY HOME|官网当前版本 23 张产品图",
459      url: "https://www.linsyhome.com/products/6-seater-modular-sofa",
460      hotspots: [
461        { bbox: [55.84, 128, 714.32, 322], label: "查看 23 张原图", start: 0 },
462        { bbox: [820.32, 130, 397.36, 188], label: "拆解右侧购买区", start: 0, collection: "detail", count: 8 }
463      ],
464      detail: {
465        title: "LINSY HOME|三步配置、附件与交易区拆解",
466        preview: "assets/detail/linsy-right-verified.webp",
467        items: makeDetailSteps({
468          src: "assets/detail/linsy-right-verified.webp",
469          source: "LINSY HOME 官方 PDP · RUBIK III 6 Seats Sofa",
470          sections: [
471            { title: "标题、评价与活动价格", description: "官网当前为 RUBIK III 6 SEATS SOFA;标题下显示 462 reviews,售价 $1,277.00、划线价 $2,113.00,并明确 Save $836.00。", src: "assets/detail/linsy-step-1-verified.webp" },
472            { title: "第 1 步:Fabric", description: "Egyptian Cotton 与 Velvet 两个大按钮二选一;当前 Velvet 被深色边框选中。", src: "assets/detail/linsy-step-2-verified.webp" },
473            { title: "第 2 步:Color", description: "官网当前显示 Navy Blue 与 Slate Grey 两枚色样;当前 Navy Blue 同步写入步骤标题并以描边高亮。", src: "assets/detail/linsy-step-3-verified.webp" },
474            { title: "第 3 步:Accessory", description: "桌面购买区当前可见 6 个附件:Rolled Armrest Pair、Ottoman、Versatile Console、Seat、Corner、Wide Track Armrest Pair;每项可勾选并单独调数量。", src: "assets/detail/linsy-step-4-verified.webp" },
475            { title: "四个详情折叠项", description: "Description、Material、Delivery Notes、Cleaning Guide 四组信息紧跟在附件列表之后。", src: "assets/detail/linsy-step-5-verified.webp" },
476            { title: "数量与全单免邮", description: "主商品 QUANTITY 数量器之后单列 Free Shipping on All Orders!,再进入主要交易按钮。", src: "assets/detail/linsy-step-6-verified.webp" },
477            { title: "Add to cart", description: "青绿色 Add to cart 是主按钮;下方另设蓝色 Buy it now,区分加入购物车与立即结账。", src: "assets/detail/linsy-step-7-verified.webp" },
478            { title: "立即购买、到货与支付", description: "Buy it now 之后展示预估送达与 Promise 提示,并列出官网当前支持的银行卡及数字支付标识。", src: "assets/detail/linsy-step-8-verified.webp" }
479          ]
480        })
481      },
482      items: makeOfficialGallery({
483        dir: "linsy",
484        prefix: "linsy",
485        count: 23,
486        extensions: ["jpg", "jpg", "png", "jpg", "jpg", "jpg", "jpg", "jpg", "jpg", "jpg", "png", "jpg", "jpg", "png", "jpg", "jpg", "png", "jpg", "jpg", "jpg", "jpg", "jpg", "jpg"],
487        entries: [
488          { title: "01|Slate Grey 转角白底图", description: "白底斜侧视角呈现 RUBIK III 的 L 形 6 座组合、纽扣靠背、宽扶手和模块接缝,是灰色配置的外观基准。" },
489          { title: "02|Slate Grey 客厅组合", description: "灰色转角组合置于真实客厅,通过抱枕、毯子和自然光展示产品尺度及深色面料的居家氛围。" },
490          { title: "03|灰色 Egyptian Cotton 近景", description: "微距图强调 Egyptian Cotton 的织纹,并以“更强韧、更柔软、更轻”说明该面料的触感与使用定位。" },
491          { title: "04|米白双贵妃位白底图", description: "正面白底图展示左右双贵妃位与中央三座的对称布局,便于比较座位数量、开口宽度和扶手高度。" },
492          { title: "05|米白 U 形暖色客厅", description: "双贵妃位组合置于暖色客厅,清楚呈现多人围合、两端躺卧区和米白面料与木色家具的搭配。" },
493          { title: "06|米白组合正面生活场景", description: "接近正面的空间图强调中央座位、两侧贵妃端与抱枕层次,回答大组合在普通客厅中的正面体量。" },
494          { title: "07|Slate Grey 开放端组合", description: "白底斜视展示一端贵妃位与另一端开放座的非对称结构,说明模块可从标准 U 形拆成更灵活的转角方案。" },
495          { title: "08|Slate Grey 暖色客厅场景", description: "灰色 U 形组合在暖色室内与红色地毯同框,展示深色面料对高频家庭空间的视觉压重效果。" },
496          { title: "09|Slate Grey 双贵妃正面图", description: "白底正面完整展示左右贵妃、中央三座、纽扣靠背和宽轨扶手,便于核对对称配置的模块数量。" },
497          { title: "10|Slate Grey 蓝墙客厅", description: "冷色客厅中的灰色 U 形组合突出贵妃位长度、深座和多人休息场景,同时验证与蓝灰空间的配色关系。" },
498          { title: "11|灰色 Velvet 面料近景", description: "丝绒微距说明短而密的绒面与柔和反光,用于区分 Velvet 与 Egyptian Cotton 的表面观感。" },
499          { title: "12|米白双贵妃正面图", description: "与第 09 张相同布局切换为米白配色,提供在明亮面料下的轮廓、接缝和靠背细节对照。" },
500          { title: "13|米白蓝墙客厅场景", description: "米白组合置于蓝墙与蓝色地毯环境,展示浅色面料提亮空间的效果以及双贵妃位的实际占地。" },
501          { title: "14|米白 Egyptian Cotton 近景", description: "浅色 Egyptian Cotton 微距清楚呈现细密织纹和暖米白色调,用于判断屏幕颜色、纹理与日常清洁风险。" },
502          { title: "15|Navy Blue 转角白底图", description: "海军蓝 L 形组合以白底斜侧方式展示一端开放、一端宽扶手的布局及深色 Velvet 的整体轮廓。" },
503          { title: "16|Navy Blue 双贵妃正面图", description: "正面展开海军蓝双贵妃配置,突出中央座位、两端躺卧区和纽扣靠背在深色面料上的层次。" },
504          { title: "17|Navy Blue Velvet 近景", description: "深蓝 Velvet 微距展示绒面方向形成的明暗变化,并说明短绒、致密与柔软的表面特征。" },
505          { title: "18|浅灰直排加脚凳组合", description: "白底正面把直排主体与两端可移动脚凳排成连续坐区,展示模块从贵妃位转换为独立脚凳的方式。" },
506          { title: "19|Slate Grey 大型 U 形场景", description: "大型 U 形组合占据客厅主位,突出双贵妃、宽扶手和多人围坐能力,是判断六座版本整体体量的关键场景。" },
507          { title: "20|浅灰家庭客厅正面场景", description: "浅灰直排与脚凳在壁炉客厅中正面出现,通过抱枕和毯子表现家庭使用状态及浅色面料的真实效果。" },
508          { title: "21|深灰 U 形明亮客厅", description: "深灰双贵妃组合置于大窗前,展示强自然光下的颜色变化、两端躺卧空间和中å¤
508®å¤šäººåº§ä½ã€‚" },
509          { title: "22|米白组合后侧视图", description: "白底后侧三分之四角度展示连续背板、模块拼缝和外转角轮廓,回答开放式客厅中的背面观感。" },
510          { title: "23|多功能 Console 储物尺寸", description: "剖视图展示 Console 内部储物、杯架与台面空间,并标注约 22.44 × 22.44 英寸占地及 8.46 英寸储物深度。" }
511        ],
512        source: "LINSY HOME 官方 PDP · RUBIK III 6 Seats Sofa"
513      })
514    },
515    19: {
516      brand: "La-Z-Boy",
517      title: "La-Z-Boy|16 张产品图(另含 Customize in 3D 入口)",
518      url: "https://www.la-z-boy.com/p/sofas-sectionals/reclining-sofas-sectionals/reclining-sectionals/trouper-modular-reclining-sectional/R-xxs724?colorFam=green",
519      hotspots: [
520        { bbox: [50, 130, 318, 318], label: "查看 16 张原图", start: 0 },
521        { bbox: [402, 133.75, 414, 310.5], label: "拆解右侧配置区", start: 0, collection: "detail", count: 7 }
522      ],
523      detail: {
524        title: "La-Z-Boy|3D、包覆配置与门店服务拆解",
525        preview: "assets/detail/lazboy-right-verified.webp",
526        items: makeDetailSteps({
527          src: "assets/detail/lazboy-right-verified.webp",
528          source: "La-Z-Boy 官方 PDP · Trouper Modular Sectional",
529          sections: [
530            { title: "Customize in 3D", description: "产品主图上的 3D 入口把复杂的模块组合从静态阅读转成可视化配置。", src: "assets/detail/lazboy-step-1-verified.webp" },
531            { title: "Sectional 组合", description: "右侧第一张配置卡标明 Sectional、L-Shape(4 pc.),点击卡片进入组合结构选择。", src: "assets/detail/lazboy-step-2-verified.webp" },
532            { title: "Cover、价格与热门色样", description: "Cover 卡当前显示 593 个选项、Antique D212432、Made to Order 与 $5,346;下方 Popular covers 提供五枚高频色样。", src: "assets/detail/lazboy-step-3-verified.webp" },
533            { title: "Cushion 坐垫", description: "Cushion (optional) 独立成卡;当前为 Standard Cushion、$0,避免把坐垫选择混入包覆颜色。", src: "assets/detail/lazboy-step-4-verified.webp" },
534            { title: "Review Summary 与 Contact Store", description: "评论摘要入口之后以 CONTACT STORE 作为主要交易动作,将询价和下单衔接到本地门店。", src: "assets/detail/lazboy-step-5-verified.webp" },
535            { title: "At a Glance 体型与功能", description: "Perfect for Tall 5'10\"–6'2\" 与家庭风格、组合灵活性、手动/电动躺位等要点共同提供身体尺度和功能证据。", src: "assets/detail/lazboy-step-6-verified.webp" },
536            { title: "融资、色样与设计服务", description: "Still Deciding? 区依次提供 Apply for Financing、Order Free Swatches、Free Design Services。", src: "assets/detail/lazboy-step-7-verified.webp" }
537          ]
538        })
539      },
540      items: makeOfficialGallery({
541        dir: "lazboy",
542        prefix: "lazboy",
543        count: 16,
544        extensions: ["jpg", "png", "png", "png", "png", "png", "png", "png", "png", "png", "png", "png", "png", "png", "png", "png"],
545        entries: [
546          { title: "01|Trouper 模块组合白底图", description: "白底全景展示 Trouper 的 L 形电动躺椅组合、转角座、带杯架中控台和独立脚凳,建立完整配置基准。" },
547          { title: "02|浅灰家庭客厅全景", description: "真实客厅场景把高靠背、厚扶手、转角围合和茶几距离转成可感知的整体体量。" },
548          { title: "03|中控台抽屉与双杯架", description: "俯视近景同时打开前向储物抽屉并展示双杯架,说明遥控器、杂志与饮品的收纳位置。" },
549          { title: "04|分区高靠背细节", description: "靠背近景突出头枕、腰部两段填充和包边缝线,解释 Trouper 高靠背的支撑结构。" },
550          { title: "05|座位、遥控器与杯架", description: "单座近景把遥控器、杯架和厚实靠背同时纳入画面,呈现休闲娱乐时的操作距离。" },
551          { title: "06|软包扶手与底座轮廓", description: "侧面近景展示圆润扶手、连续包覆和靠近地面的厚底座,强调传统美式躺椅的厚重支撑感。" },
552          { title: "07|贵妃位全躺姿态", description: "脚托完全展开并搭配毯子,直接展示腿部承托长度、靠背倾è
552§’和单人深度放松场景。" },
553          { title: "08|转角座与外侧扶手", description: "斜侧场景聚焦转角模块、相邻座位和外侧扶手的连接,说明组合在转弯处仍保持连续包覆。" },
554          { title: "09|双人观影使用场景", description: "两位用户同时展开脚托并使用中控台,验证双人躺卧、饮品放置和娱乐设备同时工作的观影场景。" },
555          { title: "10|多座全躺客厅场景", description: "多个座位处于不同躺卧状态,完整展示模块组合如何同时支持坐、躺和多人共享。" },
556          { title: "11|无线充电与控制面板", description: "中控台特写展示无线充电位、双杯架和圆形控制键,把供电、饮品与调节功能集中在手边。" },
557          { title: "12|侧置储物抽屉", description: "拉开的木质抽屉展示可容纳书籍和遥控器的实际容量,补充中控台隐藏收纳功能。" },
558          { title: "13|外侧电动调节按键", description: "扶手外侧近景清楚呈现电动躺位控制面板及 USB 端口位置,说明坐下后可直接触达的操作方式。" },
559          { title: "14|隐藏式电源接口", description: "储物空间内部展示电源模块和连接线,说明充电与娱乐设备供电被隐藏在中控结构中。" },
560          { title: "15|单躺位加中控组合", description: "紧凑正面场景由单个躺椅与相邻中控台构成,展示模块可缩减为小空间娱乐单元。" },
561          { title: "16|双躺位正面组合", description: "双人正面配置同时展示两只高靠背座椅、中间中控台、杯架和手边置物区,明确家庭影院式布局。" }
562        ],
563        source: "La-Z-Boy 官方 PDP · Trouper Modular Sectional"
564      })
565    },
566    21: {
567      brand: "West Elm",
568      title: "West Elm|6 张官方产品图库原图",
569      url: "https://www.westelm.com/products/harmony-modular-6-piece-pit-sectional-h11450/",
570      hotspots: [
571        { bbox: [63, 128, 322, 322], label: "查看 6 张原图", start: 0 },
572        { bbox: [443, 128, 322, 322], label: "拆解右侧购买区", start: 0, collection: "detail", count: 6 }
573      ],
574      detail: {
575        title: "West Elm|价格、配送与服务动作拆解",
576        preview: "assets/detail/westelm-right-verified.webp",
577        items: makeDetailSteps({
578          src: "assets/detail/westelm-right-verified.webp",
579          source: "West Elm 官方 PDP · Harmony Modular 6-Piece Pit Sectional",
580          sections: [
581            { title: "Contract Grade、商品名与价格", description: "Contract Grade 标签和 121 英寸完整商品名之后,官网当前显示 $5,315.20–$9,044 活动价、划线价格区间与 Up to 20% Off。", src: "assets/detail/westelm-step-1-verified.webp" },
582            { title: "Make It Your Own", description: "配置区明确标注 132 Choices,并提供 Color、Fabric、Features 三个筛选入口。", src: "assets/detail/westelm-step-2-verified.webp" },
583            { title: "Fabric and Color 与免费色样", description: "当前展开 Linen 面料网格,展示 Alabaster、Frost Gray、Graphite 等面料色样;底部用 ORDER FREE SWATCHES 转为实物验证。", src: "assets/detail/westelm-step-3-verified.webp" },
584            { title: "配送、数量与 Add To Cart", description: "Delivery & Pickup Options 先确认 Ship to Home 和邮编,再复述活动价格;数量步进与 ADD TO CART 位于配送确认之后。", src: "assets/detail/westelm-step-4-verified.webp" },
585            { title: "Registry 与配套脚凳", description: "主交易动作之后提供 ADD TO REGISTRY,并用 Choose Your Matching Ottoman 推荐 Harmony Modular Ottoman。", src: "assets/detail/westelm-step-5-verified.webp" },
586            { title: "设计预约与免费色样", description: "Still Deciding? 区提供 Request a Free Design Appointment 与 Order Free Swatches,为高客单面料选择补充真人支持和实物证据。", src: "assets/detail/westelm-step-6-verified.webp" }
587          ]
588        })
589      },
590      items: makeOfficialGallery({
591        dir: "westelm",
592        prefix: "westelm",
593        count: 6,
594        description: "West Elm Harmony Modular 6-Piece Pit Sectional 当前产品图库的官方原图。",
595        source: "West Elm 官方 PDP · Harmony Modular 6-Piece Pit Sectional"
596      })
597    }
598  };
599
600  const sections = [
601    { from: 1,  to: 1,  label: "封面" },
602    { from: 2,  to: 5,  label: "框架与样本" },
603    { from: 6,  to: 22, label: "品牌拆解" },
604    { from: 23, to: 27, label: "跨品牌结论" },
605    { from: 28, to: 31, label: "Monvane 重排建议" }
606  ];
607
608  const TOTAL = titles.length;
609  const deckData = window.MONVANE_DECK;
610  if (!deckData || deckData.slides?.length !== TOTAL - 1) {
611    throw new Error("Native slide data is missing or incomplete.");
612  }
613
614  function getSlideForPage(page) {
615    if (page === 8) return articleArSlide;
616    return deckData.slides[page > 8 ? page - 2 : page - 1];
617  }
618  const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
619  let current = readHash();
620  let changing = false;
621  let touchStartX = 0;
622  let touchStartY = 0;
623  let liveScroller = null;
624  let focusScroller = null;
625  let focusPage = null;
626  let focusScale = 1;
627  let evidencePage = null;
628  let evidencePosition = 0;
629  let evidenceCollection = "items";
630
631  const $ = (selector) => document.querySelector(selector);
632  const slideShell = $("#slideShell");
633  const nativeSlide = $("#nativeSlide");
634  const evidenceLayer = $("#evidenceLayer");
635  const liveLayer = $("#liveLayer");
636  const progressBar = $("#progressBar");
637  const currentPage = $("#currentPage");
638  const topPageNumber = $("#topPageNumber");
639  const sectionLabel = $("#sectionLabel");
640  const announcement = $("#slideAnnouncement");
641  const prevButton = $("#prevButton");
642  const nextButton = $("#nextButton");
643  const overviewDialog = $("#overviewDialog");
644  const overviewPanel = $(".overview__panel");
645  const overviewGrid = $("#overviewGrid");
646  const focusDialog = $("#focusDialog");
647  const focusViewport = $("#focusViewport");
648  const focusImage = $("#focusImage");
649  const focusTitle = $("#focusTitle");
650  const focusPlay = $("#focusPlay");
651  const focusZoomOut = $("#focusZoomOut");
652  const focusZoomReset = $("#focusZoomReset");
653  const focusZoomIn = $("#focusZoomIn");
654  const focusProgress = $("#focusProgress");
655  const evidenceDialog = $("#evidenceDialog");
656  const evidenceMedia = $("#evidenceMedia");
657  const evidenceImage = $("#evidenceImage");
658  const evidenceVideo = $("#evidenceVideo");
659  const evidenceEyebrow = $("#evidenceEyebrow");
660  const evidenceTitle = $("#evidenceTitle");
661  const evidenceCounter = $("#evidenceCounter");
662  const evidenceIndex = $("#evidenceIndex");
663  const evidenceItemTitle = $("#evidenceItemTitle");
664  const evidenceDescription = $("#evidenceDescription");
665  const evidenceSource = $("#evidenceSource");
666  const evidenceThumbs = $("#evidenceThumbs");
667  const evidencePrev = $("#evidencePrev");
668  const evidenceNext = $("#evidenceNext");
669
670  function pad(value) {
671    return String(value).padStart(2, "0");
672  }
673
674  function readHash() {
675    const match = location.hash.match(/slide-(\d{1,2})/);
676    if (!match) return 1;
677    return Math.min(TOTAL, Math.max(1, Number(match[1])));
678  }
679
680  function sectionFor(page) {
681    return sections.find((section) => page >= section.from && page <= section.to)?.label || "";
682  }
683
684  function setHash(page) {
685    const hash = `slide-${pad(page)}`;
686    if (location.hash !== `#${hash}`) history.replaceState(null, "", `#${hash}`);
687  }
688
689  function updateChrome() {
690    const formatted = pad(current);
691    progressBar.style.width = `${(current / TOTAL) * 100}%`;
692    currentPage.textContent = formatted;
693    topPageNumber.textContent = `${formatted} / ${TOTAL}`;
694    sectionLabel.textContent = sectionFor(current);
695    prevButton.disabled = current === 1;
696    nextButton.disabled = current === TOTAL;
697    announcement.textContent = `第 ${current} 页,共 ${TOTAL} 页:${titles[current - 1]}`;
698    document.title = `${formatted} / ${TOTAL}|${titles[current - 1]}`;
699  }
700
701  function preloadAround(page) {
702    [page - 1, page + 1]
703      .filter((candidate) => candidate >= 1 && candidate <= TOTAL)
704      .flatMap((candidate) => getSlideForPage(candidate).elements)
705      .filter((element) => element.kind === "image")
706      .forEach((element) => {
707        const image = new Image();
708        image.src = `assets/media/${element.asset}`;
709      });
710  }
711
712  function setNativeScale() {
713    const scale = slideShell.clientWidth / deckData.width;
714    nativeSlide.style.setProperty("--native-scale", String(scale));
715  }
716
717  const nativeResizeObserver = new ResizeObserver(setNativeScale);
718  nativeResizeObserver.observe(slideShell);
719
720  function appendRunText(parent, text) {
721    String(text).split("\n").forEach((part, index, parts) => {
722      if (part) parent.append(document.createTextNode(part));
723      if (index < parts.length - 1) parent.append(document.createElement("br"));
724    });
725  }
726
727  function renderElementText(node, element) {
728    const textStyle = element.textStyle || {};
729    const insets = textStyle.insets || {};
730    node.classList.add("has-text");
731    node.style.padding = `${insets.top || 0}px ${insets.right || 0}px ${insets.bottom || 0}px ${insets.left || 0}px`;
732    node.style.textAlign = textStyle.alignment || "left";
733    node.style.whiteSpace = textStyle.wrap === "none" ? "nowrap" : "normal";
734    if (textStyle.wrap === "none") node.classList.add("is-no-wrap");
735    node.style.justifyContent = textStyle.verticalAlignment === "middle"
736      ? "center"
737      : textStyle.verticalAlignment === "bottom" ? "flex-end" : "flex-start";
738
739    const paragraphs = element.paragraphs?.length ? element.paragraphs : [{ runs: [{ text: element.text }] }];
740    paragraphs.forEach((paragraph) => {
741      const paragraphNode = document.createElement("div");
742      paragraphNode.className = "native-paragraph";
743      paragraphNode.style.textAlign = paragraph.alignment || textStyle.alignment || "left";
744      if (paragraph.marginLeft) paragraphNode.style.paddingLeft = `${paragraph.marginLeft}px`;
745
746      (paragraph.runs || []).forEach((run) => {
747        const runNode = document.createElement("span");
748        runNode.className = "native-run";
749        if (run.fontSize) runNode.style.fontSize = `${run.fontSize}px`;
750        if (run.typeface) {
751          runNode.style.fontFamily = run.typeface === "Georgia"
752            ? 'Georgia, "Songti SC", "STSong", serif'
753            : 'Arial, "Microsoft YaHei", "PingFang SC", sans-serif';
754        }
755        if (run.color) runNode.style.color = run.color;
756        if (run.bold) runNode.style.fontWeight = "700";
757        appendRunText(runNode, run.text);
758        paragraphNode.append(runNode);
759      });
760      node.append(paragraphNode);
761    });
762  }
763
764  function createNativeElement(element, page) {
765    const [x, y, width, height] = element.bbox;
766    const node = document.createElement(element.kind === "image" ? "figure" : "div");
767    node.className = `native-element native-element--${element.kind}`;
768    node.style.left = `${x}px`;
769    node.style.top = `${y}px`;
770    node.style.width = `${Math.max(width, 1)}px`;
771    node.style.height = `${Math.max(height, 1)}px`;
772    node.style.zIndex = String(element.order || 1);
773    node.style.setProperty("--enter-delay", `${Math.min(900, Math.max(0, y / 720 * 520 + ((element.order || 0) % 5) * 28))}ms`);
774    if (element.rotation) node.style.setProperty("--element-rotation", `${element.rotation}deg`);
775
776    if (element.kind === "image") {
777      const liveConfig = livePages[page];
778      if (liveConfig?.asset === element.asset) return null;
779      const image = document.createElement("img");
780      image.src = `assets/media/${element.asset}`;
781      image.alt = "";
782      image.draggable = false;
783      node.append(image);
784      if (width * height > 40000) node.classList.add("is-major-media");
785      return node;
786    }
787
788    if (element.kind === "line") {
789      if (width > 3 && height > 3) {
790        node.classList.add("is-diagonal-line");
791        node.style.setProperty("--diagonal-color", element.lineColor || "#C7D6D1");
792        node.style.setProperty("--diagonal-angle", `${-Math.atan2(height, width) * 180 / Math.PI}deg`);
793        node.style.setProperty("--diagonal-length", `${Math.hypot(width, height)}px`);
794      } else {
795        node.style.background = element.lineColor || "#C7D6D1";
796        if (width > height) node.classList.add("is-horizontal-line");
797        else node.classList.add("is-vertical-line");
798      }
799      return node;
800    }
801
802    if (element.fillColor) node.style.background = element.fillColor;
803    if (element.lineColor && element.lineWidth) {
804      node.style.border = `${Math.max(1, element.lineWidth)}px solid ${element.lineColor}`;
805    }
806    if (element.geometry === "ellipse") node.style.borderRadius = "50%";
807    if (element.geometry === "roundRect") node.style.borderRadius = "12px";
808    if (element.geometry === "custom" && !element.text) node.classList.add("is-decorative-custom");
809    if (element.fillColor && width * height > 4500) node.classList.add("is-panel");
810    if (element.text) renderElementText(node, element);
811
812    const maxFont = Math.max(0, ...(element.paragraphs || []).flatMap((paragraph) =>
813      (paragraph.runs || []).map((run) => run.fontSize || 0)));
814    if (y < 115 && maxFont >= 20) node.classList.add("is-slide-heading");
815    if (/^[\s\d,.%/+·h]+$/.test(element.text || "") && maxFont >= 24) node.classList.add("is-metric");
816    return node;
817  }
818
819  function fitNativeText(node) {
820    const runs = [...node.querySelectorAll(".native-run")];
821    if (!runs.length) return;
822    const measuredParagraphWidths = node.style.whiteSpace === "nowrap"
823      ? [...node.querySelectorAll(".native-paragraph")].map((paragraph) => {
824      const probe = paragraph.cloneNode(true);
825      Object.assign(probe.style, {
826        position: "fixed",
827        inset: "0 auto auto 0",
828        width: "max-content",
829        maxWidth: "none",
830        visibility: "hidden",
831        whiteSpace: "pre"
832      });
833      document.body.append(probe);
834      const measuredWidth = probe.getBoundingClientRect().width;
835      probe.remove();
836      return measuredWidth;
837      })
838      : [];
839    const widthSafety = node.style.whiteSpace === "nowrap" ? 1.045 : 1;
840    const widestParagraph = Math.max(node.scrollWidth, ...measuredParagraphWidths) * widthSafety;
841    const widthRatio = widestParagraph > node.clientWidth
842      ? node.clientWidth / widestParagraph
843      : 1;
844    const heightRatio = node.scrollHeight > node.clientHeight
845      ? node.clientHeight / node.scrollHeight
846      : 1;
847    const ratio = Math.min(widthRatio, heightRatio);
848    if (ratio >= 0.995) return;
849    const safeRatio = Math.max(0.68, ratio * 0.985);
850    runs.forEach((run) => {
851      const size = Number.parseFloat(run.style.fontSize || getComputedStyle(run).fontSize);
852      if (size) run.style.fontSize = `${size * safeRatio}px`;
853    });
854  }
855
856  function normalizeSlideElement(page, sourceElement) {
857    if (!/^\d{2} \/ 30$/.test(sourceElement.text || "")) return sourceElement;
858    const footer = `${pad(page)} / ${TOTAL}`;
859    return {
860      ...sourceElement,
861      text: footer,
862      paragraphs: sourceElement.paragraphs?.map((paragraph) => ({
863        ...paragraph,
864        runs: paragraph.runs?.map((run) => ({
865          ...run,
866          text: /^\d{2} \/ 30$/.test(run.text || "") ? footer : run.text
867        }))
868      }))
869    };
870  }
871
872  function renderNativeSlide(page) {
873    const slide = getSlideForPage(page);
874    nativeSlide.replaceChildren();
875    nativeSlide.className = `native-slide page-${pad(page)} section-${sections.findIndex((section) => page >= section.from && page <= section.to) + 1}`;
876    nativeSlide.style.background = slide.background || "#F7F5EF";
877    nativeSlide.setAttribute("aria-label", `第 ${page} 页:${titles[page - 1]}`);
878
879    const fragment = document.createDocumentFragment();
880    slide.elements.forEach((sourceElement) => {
881      const element = normalizeSlideElement(page, sourceElement);
882      const node = createNativeElement(element, page);
883      if (node) fragment.append(node);
884    });
885    nativeSlide.append(fragment);
886    nativeSlide.querySelectorAll(".native-element.has-text").forEach(fitNativeText);
887    setNativeScale();
888    requestAnimationFrame(() => requestAnimationFrame(() => nativeSlide.classList.add("is-active")));
889  }
890
891  function renderEvidencePage(page) {
892    evidenceLayer.replaceChildren();
893    const config = evidencePages[page];
894    if (!config) return;
895
896    config.hotspots.forEach((hotspot) => {
897      const [x, y, width, height] = hotspot.bbox;
898      const isDetail = hotspot.collection === "detail";
899      const hotspotItems = isDetail ? config.detail?.items : config.items;
900      const button = document.createElement("button");
901      button.type = "button";
902      button.className = `evidence-hotspot${hotspot.variant ? ` evidence-hotspot--${hotspot.variant}` : ""}${isDetail ? " evidence-hotspot--detail" : ""}`;
903      button.style.left = `${(x / 1280) * 100}%`;
904      button.style.top = `${(y / 720) * 100}%`;
905      button.style.width = `${(width / 1280) * 100}%`;
906      button.style.height = `${(height / 720) * 100}%`;
907      button.setAttribute("aria-label", `${config.brand}:${hotspot.label}`);
908      if (isDetail && config.detail?.preview) {
909        const preview = document.createElement("img");
910        preview.className = "evidence-hotspot__preview";
911        preview.src = config.detail.preview;
912        preview.alt = "";
913        preview.draggable = false;
914        button.append(preview);
915      }
916      const badge = document.createElement("span");
917      badge.className = "evidence-hotspot__badge";
918      badge.innerHTML = `<i aria-hidden="true">+</i>${hotspot.label}<b>${hotspot.count ?? hotspotItems?.length ?? 0}</b>`;
919      button.append(badge);
920      button.addEventListener("click", (event) => {
921        event.stopPropagation();
922        openEvidence(page, hotspot.start || 0, hotspot.collection || "items");
923      });
924      evidenceLayer.append(button);
925    });
926
927    if (config.url) {
928      const sourceLink = document.createElement("a");
929      sourceLink.className = "evidence-source-link";
930      sourceLink.href = config.url;
931      sourceLink.target = "_blank";
932      sourceLink.rel = "noopener noreferrer";
933      sourceLink.setAttribute("aria-label", `打开 ${config.brand} 官方产品页`);
934      sourceLink.innerHTML = `<span>打开 ${config.brand} 官方 PDP ↗</span>`;
935      evidenceLayer.append(sourceLink);
936    }
937  }
938
939  function getEvidencePayload(page = evidencePage, collection = evidenceCollection) {
940    const config = evidencePages[page];
941    if (!config) return null;
942    if (collection === "detail" && config.detail?.items?.length) {
943      return { config, items: config.detail.items, title: config.detail.title, isDetail: true };
944    }
945    return { config, items: config.items, title: config.title, isDetail: false };
946  }
947
948  function buildEvidenceThumbs(config, items) {
949    const fragment = document.createDocumentFragment();
950    items.forEach((item, index) => {
951      const button = document.createElement("button");
952      button.type = "button";
953      button.className = "evidence-thumb";
954      button.dataset.index = String(index);
955      button.setAttribute("aria-label", `查看第 ${index + 1} 项:${item.title}`);
956      const image = document.createElement("img");
957      image.src = item.type === "video" ? item.poster : item.src;
958      image.alt = "";
959      image.draggable = false;
960      if (item.mode === "scroll") image.style.objectPosition = `center ${Math.round((item.focus || 0) * 100)}%`;
961      if (item.type === "video") {
962        button.classList.add("is-video");
963        const play = document.createElement("em");
964        play.textContent = "▶";
965        play.setAttribute("aria-hidden", "true");
966        button.append(play);
967      }
968      const number = document.createElement("span");
969      number.textContent = pad(index + 1);
970      button.append(image, number);
971      button.addEventListener("click", () => showEvidenceItem(index));
972      fragment.append(button);
973    });
974    evidenceThumbs.replaceChildren(fragment);
975  }
976
977  function showEvidenceItem(index) {
978    const payload = getEvidencePayload();
979    if (!payload) return;
980    const { config, items, isDetail } = payload;
981    const total = items.length;
982    evidencePosition = (index + total) % total;
983    const item = items[evidencePosition];
984    evidenceMedia.classList.toggle("is-scroll", item.mode === "scroll");
985    evidenceMedia.scrollTop = 0;
986    evidenceMedia.scrollLeft = 0;
987    const itemPosition = evidencePosition;
988    const applyFocus = () => {
989      if (evidencePosition !== itemPosition || item.mode !== "scroll") return;
990      const maxScroll = Math.max(0, evidenceMedia.scrollHeight - evidenceMedia.clientHeight);
991      evidenceMedia.scrollTop = maxScroll * Math.max(0, Math.min(1, item.focus || 0));
992    };
993    evidenceVideo.pause();
994    evidenceVideo.hidden = true;
995    evidenceVideo.removeAttribute("src");
996    evidenceVideo.removeAttribute("poster");
997    evidenceVideo.removeAttribute("aria-label");
998    evidenceImage.onload = null;
999    if (item.type === "video") {
1000      evidenceImage.hidden = true;
1001      evidenceImage.removeAttribute("src");
1002      evidenceImage.alt = "";
1003      evidenceVideo.hidden = false;
1004      evidenceVideo.poster = item.poster || "";
1005      evidenceVideo.src = item.src;
1006      evidenceVideo.setAttribute("aria-label", `${config.brand} 原图证据视频:${item.title}`);
1007      evidenceVideo.load();
1008    } else {
1009      evidenceImage.hidden = false;
1010      evidenceImage.onload = () => requestAnimationFrame(() => requestAnimationFrame(applyFocus));
1011      evidenceImage.src = item.src;
1012      if (evidenceImage.complete && evidenceImage.naturalWidth) requestAnimationFrame(() => requestAnimationFrame(applyFocus));
1013      evidenceImage.alt = `${config.brand}${isDetail ? "右侧购买区" : "原图证据"}:${item.title}`;
1014    }
1015    evidenceCounter.textContent = `${pad(evidencePosition + 1)} / ${pad(total)}`;
1016    evidenceIndex.textContent = `${isDetail ? "模块" : "证据"} ${pad(evidencePosition + 1)}`;
1017    evidenceItemTitle.textContent = item.title;
1018    evidenceDescription.textContent = item.description;
1019    evidenceSource.replaceChildren();
1020    if (config.url) {
1021      const sourceLink = document.createElement("a");
1022      sourceLink.href = config.url;
1023      sourceLink.target = "_blank";
1024      sourceLink.rel = "noopener noreferrer";
1025      sourceLink.textContent = `${item.source} · 打开官方 PDP ↗`;
1026      evidenceSource.append(sourceLink);
1027    } else {
1028      evidenceSource.textContent = item.source;
1029    }
1030    evidencePrev.disabled = total < 2;
1031    evidenceNext.disabled = total < 2;
1032    evidenceThumbs.querySelectorAll(".evidence-thumb").forEach((thumb) => {
1033      thumb.classList.toggle("is-active", Number(thumb.dataset.index) === evidencePosition);
1034    });
1035  }
1036
1037  function openEvidence(page, start = 0, collection = "items") {
1038    const payload = getEvidencePayload(page, collection);
1039    if (!payload) return;
1040    evidencePage = page;
1041    evidenceCollection = payload.isDetail ? "detail" : "items";
1042    evidenceDialog.classList.toggle("is-detail", payload.isDetail);
1043    evidenceEyebrow.textContent = payload.isDetail ? "PURCHASE PANEL BREAKDOWN" : "ORIGINAL EVIDENCE";
1044    evidenceTitle.textContent = payload.title;
1045    buildEvidenceThumbs(payload.config, payload.items);
1046    showEvidenceItem(start);
1047    if (!evidenceDialog.open) evidenceDialog.showModal();
1048  }
1049
1050  function closeEvidence() {
1051    evidencePage = null;
1052    evidencePosition = 0;
1053    evidenceCollection = "items";
1054    evidenceVideo.pause();
1055    evidenceVideo.removeAttribute("src");
1056    evidenceVideo.removeAttribute("poster");
1057    evidenceVideo.hidden = true;
1058    evidenceImage.onload = null;
1059    evidenceImage.removeAttribute("src");
1060    evidenceImage.hidden = false;
1061    evidenceDialog.classList.remove("is-detail");
1062    evidenceEyebrow.textContent = "ORIGINAL EVIDENCE";
1063    evidenceDialog.close();
1064  }
1065
1066  function cancelScroller(scroller) {
1067    if (scroller) scroller.destroy();
1068  }
1069
1070  function createAutoScroller(viewport, options = {}) {
1071    let frame = 0;
1072    let last = 0;
1073    let direction = 1;
1074    let playing = !reducedMotion;
1075    let position = viewport.scrollTop;
1076    let pauseUntil = performance.now() + (options.delay || 650);
1077    const speed = options.speed || 0.032;
1078    const onProgress = options.onProgress || (() => {});
1079
1080    function report() {
1081      const max = Math.max(0, viewport.scrollHeight - viewport.clientHeight);
1082      if (!playing) position = viewport.scrollTop;
1083      const ratio = max ? viewport.scrollTop / max : 0;
1084      onProgress(ratio, viewport.clientHeight / Math.max(viewport.scrollHeight, 1));
1085      return max;
1086    }
1087
1088    function tick(now) {
1089      const delta = Math.min(42, now - (last || now));
1090      last = now;
1091      const max = report();
1092      if (playing && now >= pauseUntil && max > 1) {
1093        position += direction * speed * delta;
1094        viewport.scrollTop = position;
1095        if (direction > 0 && position >= max - 1) {
1096          position = max;
1097          viewport.scrollTop = position;
1098          direction = -1;
1099          pauseUntil = now + 900;
1100        } else if (direction < 0 && position <= 1) {
1101          position = 0;
1102          viewport.scrollTop = position;
1103          direction = 1;
1104          pauseUntil = now + 900;
1105        }
1106      }
1107      frame = requestAnimationFrame(tick);
1108    }
1109
1110    function pauseTemporarily() {
1111      playing = false;
1112      clearTimeout(pauseTemporarily.timer);
1113      pauseTemporarily.timer = setTimeout(() => {
1114        if (!reducedMotion) playing = true;
1115      }, 1800);
1116    }
1117
1118    viewport.addEventListener("wheel", pauseTemporarily, { passive: true });
1119    viewport.addEventListener("touchstart", pauseTemporarily, { passive: true });
1120    viewport.addEventListener("scroll", report, { passive: true });
1121    frame = requestAnimationFrame(tick);
1122
1123    return {
1124      destroy() {
1125        cancelAnimationFrame(frame);
1126        clearTimeout(pauseTemporarily.timer);
1127        viewport.removeEventListener("wheel", pauseTemporarily);
1128        viewport.removeEventListener("touchstart", pauseTemporarily);
1129        viewport.removeEventListener("scroll", report);
1130      },
1131      setPlaying(value) { playing = value; },
1132      toggle() { playing = !playing; return playing; },
1133      isPlaying() { return playing; },
1134      toTop() { position = 0; viewport.scrollTo({ top: 0, behavior: reducedMotion ? "auto" : "smooth" }); direction = 1; },
1135      toBottom() { position = Math.max(0, viewport.scrollHeight - viewport.clientHeight); viewport.scrollTo({ top: viewport.scrollHeight, behavior: reducedMotion ? "auto" : "smooth" }); direction = -1; },
1136      report
1137    };
1138  }
1139
1140  function renderLivePage(page) {
1141    cancelScroller(liveScroller);
1142    liveScroller = null;
1143    liveLayer.replaceChildren();
1144
1145    const config = livePages[page];
1146    if (!config) return;
1147
1148    const [x, y, w, h] = config.bbox;
1149    const frame = document.createElement("div");
1150    frame.className = "pdp-frame";
1151    frame.style.left = `${(x / 1280) * 100}%`;
1152    frame.style.top = `${(y / 720) * 100}%`;
1153    frame.style.width = `${(w / 1280) * 100}%`;
1154    frame.style.height = `${(h / 720) * 100}%`;
1155    const viewport = document.createElement("div");
1156    viewport.className = "pdp-viewport";
1157    viewport.setAttribute("role", "region");
1158    viewport.setAttribute("aria-label", `${config.brand} 官网长页面动态浏览`);
1159
1160    const image = document.createElement("img");
1161    image.src = `assets/${config.asset}`;
1162    image.alt = `${config.brand} 官网产品详情长页面截图`;
1163    image.draggable = false;
1164    viewport.append(image);
1165
1166    const expand = document.createElement("button");
1167    expand.type = "button";
1168    expand.className = "pdp-expand";
1169    expand.setAttribute("aria-label", `放大浏览 ${config.brand} 页面`);
1170    expand.title = "放大动态浏览";
1171    expand.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5"/></svg>';
1172    expand.addEventListener("click", (event) => {
1173      event.stopPropagation();
1174      openFocus(page);
1175    });
1176
1177    const track = document.createElement("span");
1178    track.className = "pdp-track";
1179    const thumb = document.createElement("span");
1180    thumb.className = "pdp-thumb";
1181    track.append(thumb);
1182
1183    const scanLine = document.createElement("span");
1184    scanLine.className = "scan-line";
1185    frame.append(viewport, expand, track, scanLine);
1186    liveLayer.append(frame);
1187
1188    let activated = false;
1189    const activateLiveScroll = () => {
1190      if (activated) return;
1191      activated = true;
1192      viewport.scrollLeft = Math.max(0, (viewport.scrollWidth - viewport.clientWidth) / 2);
1193      liveScroller = createAutoScroller(viewport, {
1194        speed: 0.045,
1195        delay: 520,
1196        onProgress(ratio, visibleRatio) {
1197          const trackHeight = track.clientHeight;
1198          const thumbHeight = Math.max(10, Math.min(trackHeight, trackHeight * visibleRatio));
1199          thumb.style.height = `${thumbHeight}px`;
1200          thumb.style.transform = `translateY(${Math.max(0, (trackHeight - thumbHeight) * ratio)}px)`;
1201        }
1202      });
1203    };
1204
1205    if (image.complete && image.naturalWidth) requestAnimationFrame(activateLiveScroll);
1206    else image.addEventListener("load", activateLiveScroll, { once: true });
1207
1208    frame.addEventListener("dblclick", () => openFocus(page));
1209  }
1210
1211  function render(page, immediate = false) {
1212    const commit = () => {
1213      current = page;
1214      renderNativeSlide(current);
1215      renderEvidencePage(current);
1216      renderLivePage(current);
1217      updateChrome();
1218      updateOverviewCurrent();
1219      setHash(current);
1220      preloadAround(current);
1221      slideShell.classList.remove("is-leaving");
1222      slideShell.classList.remove("is-entering");
1223      void slideShell.offsetWidth;
1224      slideShell.classList.add("is-entering");
1225      changing = false;
1226    };
1227
1228    commit();
1229  }
1230
1231  function goTo(page, immediate = false) {
1232    const target = Math.min(TOTAL, Math.max(1, page));
1233    if ((target === current && !immediate) || changing) return;
1234    render(target, immediate);
1235  }
1236
1237  function buildOverview() {
1238    const fragment = document.createDocumentFragment();
1239    titles.forEach((title, index) => {
1240      const page = index + 1;
1241      const button = document.createElement("button");
1242      button.type = "button";
1243      button.className = "overview-card";
1244      button.dataset.page = String(page);
1245      button.setAttribute("aria-label", `跳到第 ${page} 页:${title}`);
1246      button.innerHTML = `
1247        <span class="overview-card__visual" aria-hidden="true">
1248          <span class="overview-card__section">${sectionFor(page)}</span>
1249          <span class="overview-card__hero-number">${pad(page)}</span>
1250          <span class="overview-card__signal"><i></i><i></i><i></i></span>
1251        </span>
1252        <span class="overview-card__meta">
1253          <span class="overview-card__number">${pad(page)}</span>
1254          <span class="overview-card__title">${title}</span>
1255        </span>`;
1256      button.addEventListener("click", () => {
1257        overviewDialog.close();
1258        goTo(page);
1259      });
1260      fragment.append(button);
1261    });
1262    overviewGrid.append(fragment);
1263  }
1264
1265  function updateOverviewCurrent() {
1266    overviewGrid.querySelectorAll(".overview-card").forEach((card) => {
1267      card.classList.toggle("is-current", Number(card.dataset.page) === current);
1268    });
1269  }
1270
1271  function openOverview() {
1272    updateOverviewCurrent();
1273    overviewPanel.scrollTop = 0;
1274    overviewDialog.showModal();
1275  }
1276
1277  function openFocus(page) {
1278    const config = livePages[page];
1279    if (!config) return;
1280    focusPage = page;
1281    cancelScroller(focusScroller);
1282    focusScroller = null;
1283    focusViewport.scrollTop = 0;
1284    focusViewport.scrollLeft = 0;
1285    focusScale = 1;
1286    focusImage.style.width = "auto";
1287    focusZoomReset.textContent = "100%";
1288    focusImage.src = `assets/${config.asset}`;
1289    focusImage.alt = `${config.brand} 官网产品详情长页面截图`;
1290    focusTitle.textContent = `${config.brand}|官网 PDP 高清长页`;
1291    focusPlay.textContent = reducedMotion ? "播放" : "暂停";
1292    focusPlay.setAttribute("aria-label", reducedMotion ? "开始自动滚动" : "暂停自动滚动");
1293    focusDialog.showModal();
1294
1295    let activated = false;
1296    const activate = () => {
1297      if (activated) return;
1298      activated = true;
1299      focusImage.style.width = `${focusImage.naturalWidth}px`;
1300      requestAnimationFrame(() => {
1301        focusViewport.scrollLeft = Math.max(0, (focusViewport.scrollWidth - focusViewport.clientWidth) / 2);
1302      });
1303      focusScroller = createAutoScroller(focusViewport, {
1304        speed: 0.052,
1305        delay: 700,
1306        onProgress(ratio) { focusProgress.style.width = `${ratio * 100}%`; }
1307      });
1308      if (reducedMotion) focusScroller.setPlaying(false);
1309    };
1310
1311    if (focusImage.complete && focusImage.naturalWidth) activate();
1312    else focusImage.addEventListener("load", activate, { once: true });
1313  }
1314
1315  function setFocusScale(nextScale) {
1316    if (!focusImage.naturalWidth) return;
1317    focusScale = Math.max(.5, Math.min(2, nextScale));
1318    focusScroller?.setPlaying(false);
1319    focusPlay.textContent = "播放";
1320    focusPlay.setAttribute("aria-label", "开始自动滚动");
1321    focusImage.style.width = `${Math.round(focusImage.naturalWidth * focusScale)}px`;
1322    focusZoomReset.textContent = `${Math.round(focusScale * 100)}%`;
1323  }
1324
1325  function closeFocus() {
1326    cancelScroller(focusScroller);
1327    focusScroller = null;
1328    focusPage = null;
1329    focusScale = 1;
1330    focusImage.style.width = "auto";
1331    focusDialog.close();
1332  }
1333
1334  function toggleFullscreen() {
1335    if (!document.fullscreenElement) {
1336      document.documentElement.requestFullscreen?.();
1337    } else {
1338      document.exitFullscreen?.();
1339    }
1340  }
1341
1342  $("#homeButton").addEventListener("click", () => goTo(1));
1343  $("#menuButton").addEventListener("click", openOverview);
1344  $("#overviewClose").addEventListener("click", () => overviewDialog.close());
1345  $("#fullscreenButton").addEventListener("click", toggleFullscreen);
1346  prevButton.addEventListener("click", () => goTo(current - 1));
1347  nextButton.addEventListener("click", () => goTo(current + 1));
1348  $("#focusClose").addEventListener("click", closeFocus);
1349  $("#focusTop").addEventListener("click", () => focusScroller?.toTop());
1350  $("#focusBottom").addEventListener("click", () => focusScroller?.toBottom());
1351  focusZoomOut.addEventListener("click", () => setFocusScale(focusScale - .25));
1352  focusZoomReset.addEventListener("click", () => setFocusScale(1));
1353  focusZoomIn.addEventListener("click", () => setFocusScale(focusScale + .25));
1354  focusPlay.addEventListener("click", () => {
1355    if (!focusScroller) return;
1356    const playing = focusScroller.toggle();
1357    focusPlay.textContent = playing ? "暂停" : "播放";
1358    focusPlay.setAttribute("aria-label", playing ? "暂停自动滚动" : "开始自动滚动");
1359  });
1360  $("#evidenceClose").addEventListener("click", closeEvidence);
1361  evidencePrev.addEventListener("click", () => showEvidenceItem(evidencePosition - 1));
1362  evidenceNext.addEventListener("click", () => showEvidenceItem(evidencePosition + 1));
1363
1364  overviewDialog.addEventListener("click", (event) => {
1365    if (event.target === overviewDialog) overviewDialog.close();
1366  });
1367  focusDialog.addEventListener("click", (event) => {
1368    if (event.target === focusDialog) closeFocus();
1369  });
1370  evidenceDialog.addEventListener("click", (event) => {
1371    if (event.target === evidenceDialog) closeEvidence();
1372  });
1373
1374  document.addEventListener("keydown", (event) => {
1375    if (evidenceDialog.open) {
1376      if (event.key === "Escape") { event.preventDefault(); closeEvidence(); }
1377      if (event.key === "ArrowLeft") { event.preventDefault(); showEvidenceItem(evidencePosition - 1); }
1378      if (event.key === "ArrowRight") { event.preventDefault(); showEvidenceItem(evidencePosition + 1); }
1379      return;
1380    }
1381    if (focusDialog.open) {
1382      if (event.key === "Escape") { event.preventDefault(); closeFocus(); }
1383      if (event.key === " ") { event.preventDefault(); focusPlay.click(); }
1384      if (event.key === "+" || event.key === "=") { event.preventDefault(); setFocusScale(focusScale + .25); }
1385      if (event.key === "-") { event.preventDefault(); setFocusScale(focusScale - .25); }
1386      if (event.key === "0") { event.preventDefault(); setFocusScale(1); }
1387      return;
1388    }
1389    if (overviewDialog.open) {
1390      if (event.key === "Escape") overviewDialog.close();
1391      return;
1392    }
1393    if (["ArrowRight", "PageDown", " "].includes(event.key)) {
1394      event.preventDefault();
1395      goTo(current + 1);
1396    } else if (["ArrowLeft", "PageUp"].includes(event.key)) {
1397      event.preventDefault();
1398      goTo(current - 1);
1399    } else if (event.key === "Home") {
1400      event.preventDefault();
1401      goTo(1);
1402    } else if (event.key === "End") {
1403      event.preventDefault();
1404      goTo(TOTAL);
1405    } else if (event.key.toLowerCase() === "f") {
1406      event.preventDefault();
1407      toggleFullscreen();
1408    } else if (event.key.toLowerCase() === "o") {
1409      event.preventDefault();
1410      openOverview();
1411    } else if (event.key.toLowerCase() === "a" && liveScroller) {
1412      event.preventDefault();
1413      liveScroller.toggle();
1414    } else if (event.key === "Enter") {
1415      if (evidencePages[current]) {
1416        event.preventDefault();
1417        openEvidence(current, 0);
1418      } else if (livePages[current]) {
1419        event.preventDefault();
1420        openFocus(current);
1421      }
1422    }
1423  });
1424
1425  $("#stage").addEventListener("touchstart", (event) => {
1426    const touch = event.changedTouches[0];
1427    touchStartX = touch.clientX;
1428    touchStartY = touch.clientY;
1429  }, { passive: true });
1430
1431  $("#stage").addEventListener("touchend", (event) => {
1432    const touch = event.changedTouches[0];
1433    const deltaX = touch.clientX - touchStartX;
1434    const deltaY = touch.clientY - touchStartY;
1435    if (Math.abs(deltaX) > 52 && Math.abs(deltaX) > Math.abs(deltaY) * 1.4) {
1436      goTo(current + (deltaX < 0 ? 1 : -1));
1437    }
1438  }, { passive: true });
1439
1440  window.addEventListener("hashchange", () => {
1441    const target = readHash();
1442    if (target !== current) goTo(target);
1443  });
1444
1445  window.addEventListener("beforeunload", () => {
1446    cancelScroller(liveScroller);
1447    cancelScroller(focusScroller);
1448    nativeResizeObserver.disconnect();
1449  });
1450
1451  buildOverview();
1452  render(current, true);
1453})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.