PageSourceSearch

https://www.01.ai/assets/app.1.9.23165519.js

js 01.ai collected 2026-09-24 18:08:05 UTC 73,305 bytes, 1,746 lines download raw bytes

1// 01.AI — shared interactions
2(() => {
3  const isEN = (document.documentElement.lang || '').toLowerCase().startsWith('en');
4  const NAV_TRANSITION_KEY = 'oai-nav-transition';
5  const fromNavTransition = (() => {
6    try {
7      const value = window.sessionStorage.getItem(NAV_TRANSITION_KEY);
8      if (value) window.sessionStorage.removeItem(NAV_TRANSITION_KEY);
9      return Boolean(value);
10    } catch {
11      return false;
12    }
13  })();
14  if (fromNavTransition) {
15    document.body.classList.add('page-transition-from-nav');
16  }
17  const rootRelativeHref = (targetPath) => {
18    const cleanTarget = targetPath.replace(/^\/+/, '');
19    const path = window.location.pathname || '';
20    const currentDir = path.endsWith('/') ? path : path.slice(0, path.lastIndexOf('/') + 1);
21    const rootDir = currentDir.replace(/^\/+|\/+$/g, '');
22    const depth = rootDir ? rootDir.split('/').filter(Boolean).length : 0;
23    return `${'../'.repeat(depth)}${cleanTarget}`;
24  };
25  const isLocalPreview = (() => {
26    const host = window.location.hostname;
27    return window.location.protocol === 'file:'
28      || host === 'localhost'
29      || host === '127.0.0.1'
30      || host === '::1';
31  })();
32  const localLanguageHref = (href) => {
33    if (!href) return href;
34    if (!isLocalPreview) return href;
35    if (href === 'https://www.01.ai') return rootRelativeHref('en/');
36    if (href === 'https://www.lingyiwanwu.com') return rootRelativeHref('');
37    if (href.startsWith('https://www.01.ai/')) {
38      // EN origin serves the /en/ folder at its root in production; locally the
39      // /en/ subdir still exists, so restore it for preview.
40      return rootRelativeHref('en/' + href.replace('https://www.01.ai/', ''));
41    }
42    if (href.startsWith('https://www.lingyiwanwu.com/')) {
43      return rootRelativeHref(href.replace('https://www.lingyiwanwu.com/', ''));
44    }
45    return href;
46  };
47  document.querySelectorAll('.nav-lang, .nav-drawer-lang').forEach((link) => {
48    const href = link.getAttribute('href');
49    const localHref = localLanguageHref(href);
50    if (localHref !== href) link.setAttribute('href', localHref);
51  });
52  const T = isEN
53    ? {
54        title: 'Book a Consultation',
55        sub: 'Share your objectives. Our enterprise team will respond within two business days.',
56        kicker: 'Enterprise AI Transformation Services',
57        pitchTitle: 'Start with the opportunity that matters most.',
58        pitchText: 'A focused conversation to clarify business outcomes, data readiness, deployment constraints, and a measurable first phase.',
59        pitchPoints: [
60          'Prioritize use cases against business outcomes',
61          'Plan around data, security, and deployment requirements',
62          'Receive a response within two business days'
63        ],
64        trust: 'For executives, business owners, and AI transformation leaders.',
65        investorTitle: 'Business Partnership',
66        investorSub: 'Share your cooperation focus and contact details. We will direct your request to the right team.',
67        investorKicker: 'Business Partnership',
68        investorPitchTitle: 'Tell us how you would like to collaborate.',
69        investorPitchText: 'Share your cooperation intent, company background, and the support you need so we can route your request to the right team.',
70        investorPitchPoints: [
71          'Industry alignment and ecosystem partnerships',
72          'Capital conversations and strategic investment',
73          'Joint solutions and enterprise go-to-market'
74        ],
75        investorTrust: 'For investors, industry partners, and ecosystem collaborators.',
76        investorMessage: 'Partnership focus *',
77        investorMessagePh: 'Please include your partnership context, focus, available resources, expected timeline, and preferred contact path.',
78        mediaTitle: 'Press inquiry',
79        mediaSub: 'Share your interview, op-ed, or media request. Our communications team will respond.',
80        mediaKicker: 'Press',
81        mediaPitchTitle: 'Clear context for accurate reporting.',
82        mediaPitchText: 'For interviews, fact checks, brand materials, or commentary requests, tell us 
82your outlet, topic, and deadline.',
83        mediaPitchPoints: [
84          'Interview requests and topic alignment',
85          'Facts, brand assets, and company background',
86          'Op-ed and commentary coordination'
87        ],
88        mediaTrust: 'For journalists, editors, analysts, and media partners.',
89        mediaMessage: 'Press request *',
90        mediaMessagePh: 'Please include your outlet, topic, deadline, and the materials or spokesperson you need.',
91        mediaInquiry: {
92          title: 'Media Inquiry',
93          sub: 'Complete the fields below and our communications team will follow up.',
94          kicker: 'Media Inquiry',
95          pitchTitle: 'Share the context for your request.',
96          pitchText: 'Tell us who you are, your outlet or professional profile, and the details needed for accurate follow-up.',
97          pitchPoints: [
98            'Provide your name, email, and phone number',
99            'Share your official website or LinkedIn profile',
100            'Describe your interview, material, or fact-check request'
101          ],
102          trust: 'For journalists, editors, analysts, researchers, and media partners.',
103          name: 'Name *',
104          namePh: 'Your name',
105          email: 'Email *',
106          emailPh: '[email protected]',
107          phone: 'Phone number *',
108          phonePh: 'Phone number',
109          website: 'Official website or LinkedIn *',
110          websitePh: 'Website or LinkedIn URL',
111          intro: 'Please provide an introduction *',
112          introPh: 'Please include your outlet or organization, topic, deadline, and the materials or spokesperson you need.'
113        },
114        businessPartnership: {
115          title: 'Business Partnership',
116          sub: 'Complete the fields below and our business partnership team will follow up.',
117          kicker: 'Business Partnership',
118          pitchTitle: 'Tell us how you would like to collaborate.',
119          pitchText: 'Share your cooperation intent, company background, and the support you need so we can route your request to the right team.',
120          pitchPoints: [
121            'Clarify service offers, partnership ideas, or other requests',
122            'Provide company, website or LinkedIn, and contact details',
123            'Tell us whether you need an online experience'
124          ],
125          trust: 'For solution partners, service providers, ecosystem collaborators, and enterprise contacts.',
126          purpose: 'Purpose of Reaching Out *',
127          purposeOptions: ['Service Offers', 'Partnership Ideas', 'Others'],
128          name: 'Name *',
129          namePh: 'Your name',
130          role: 'Job title *',
131          rolePh: 'Job title',
132          email: 'Email *',
133          emailPh: '[email protected]',
134          phone: 'Phone number *',
135          phonePh: 'Phone number',
136          company: 'Company name *',
137          companyPh: 'Company name',
138          website: 'Official website or LinkedIn *',
139          websitePh: 'Website or LinkedIn URL',
140          intro: 'Please provide an introduction *',
141          introPh: 'Briefly describe your cooperation intent, relevant background, and expected next step.',
142          online: 'Need an online experience? *',
143          onlineCheck: 'Need an online experience',
144          onlineOptions: ['Yes', 'No'],
145          source: 'How did you hear about us? *',
146          sourceOptions: ['News articles', 'Google', 'LinkedIn', 'Twitter', 'Referral', 'Others'],
147          selectPlaceholder: 'Select an option'
148        },
149        splitName: true,
150        firstName: 'First name *',
151        firstNamePh: 'First name',
152        lastName: 'Last name *',
153        lastNamePh: 'Last name',
154        company: 'Company name *',
155        companyPh: 'Company',
156        role: 'Job title *',
157        rolePh: 'Job title',
158        phone: 'Mobile number *',
159        phonePh: 'Mobile number',
160        smsCode: 'Verification code *',
161        smsCodePh: '6-digit code',
162        sendCode: 'Send code',
163        sendCodeWait: 'Resend in',
164        phoneInvalid: 'Please enter a valid mobile number.',
165        codeRequired: 'Please enter the 6-digit verification code.',
166        codeInvalid: 'Incorrect or expired verification code.',
167        codeSent: 'Verification code sent.',
168        sendCodeFail: 'Unable to send code. Please try again.',
169        sendCodeRateLimit: 'Too many requests. Please try again later.',
170        region: 'Country or region *',
171        regions: ['Mainland China', 'Hong Kong SAR', 'Singapore', 'United States', 'Other'],
172        message: 'Project details *',
173        messagePh: 'Please describe your use case, goals, data readiness, timeline, and deployment requirements.',
174        submit: 'Submit',
175        close: 'Close',
176        success: 'Received · we will respond within 2 business days',
177        emailInvalid: 'Please enter a valid email address.',
178        phoneDigits: 'Phone number must contain digits only.',
179        submitting: 'Submitting…',
180        submitError: 'Submission failed. Please try again later.',
181        successTitle: 'Submitted successfully',
182        successBody: 'Your request has been received. We will respond within two business days.',
183        assistantName: '01.AI Advisor',
184        assistantStatus: 'Online',
185        quickLabel: 'Suggested prompts',
186        quickPrompts: [
187          'Evaluate an enterprise AI scenario',
188          'Book a product walkthrough',
189          'Discuss private deployment'
190        ],
191        contactTitle: 'Contact card',
192        contactHint: 'The advisor only needs a few details to route this.',
193        send: 'Send to advisor'
194      }
195    : {
196        title: '预约咨询',
197        sub: '请留下您的需求,我们将在 2 个工作日内与您联系。',
198        kicker: '企业 AI 转型咨询服务',
199        pitchTitle: '让 AI 创造业务价值',
200        pitchText: '我们将与您梳理业务目标、数据基础和部署要求,明确可验证的首阶段方案。',
201        pitchPoints: [
202          '以业务结果确定优先场景',
203          '评估数据、安全与部署要求',
204          '2 个工作日内与您联系'
205        ],
206        trust: '适用于企业管理者、业务负责人及 AI 转型团队。',
207        investorTitle: '商务合作',
208        investorSub: '请留下合作方向与联系信息,我们会转交对应团队评估并跟进。',
209        investorKicker: '商务合作',
210        investorPitchTitle: '告诉我们您的合作意向',
211        investorPitchText: '请留下合作方向、公司背景和所需支持,我们会将需求转交给合适团队跟进。',
212        investorPitchPoints: [
213          '产业协同与生态合作',
214          '资本沟通与战略投资',
215          '联合方案与重点客户落地'
216        ],
217        investorTrust: '适用于投资机构、产业伙伴及生态合作方。',
218        investorMessage: '合作意向 *',
219        investorMessagePh: '请简述合作背景、合作方向、可提供资源、期望推进时间及联系人信息。',
220        mediaTitle: '媒体问询',
221        mediaSub: '请留下采访、约稿或资料需求,公关团队将与您联系。',
222        mediaKicker: '媒体沟通',
223        mediaPitchTitle: '为报道提供清晰、准确的信息',
224        mediaPitchText: '如需采访、品牌资料、事实核查或观点约稿,请说明媒体名称、报道方向与截止时间。',
225        mediaPitchPoints: [
226          '采访预约与议题沟通',
227          '品牌资料与事实核查',
228          '观点约稿与活动沟通'
229        ],
230        mediaTrust: '适用于媒体、研究机构及内容合作伙伴。',
231        mediaMessage: '媒体需求 *',
232        mediaMessagePh: '请简述媒体名称、采访或约稿主题、希望回复时间及所需资料。',
233        mediaInquiry: {
234          title: '媒体问询',
235          sub: '请填写以下信息,公关团队会尽快与您联系。',
236          kicker: '媒体问询',
237          pitchTitle: '请告诉我们您的媒体需求',
238          pitchText: '请留下您的姓名、单位机构、职务或专业主页,并说明采访、资料获取、信息核实或媒体合作需求,便于我们准确跟进。',
239          pitchPoints: [
240            '留下姓名、邮箱与手机号',
241            '提供微信号',
242            '说明采访、资料或事实核查需求'
243          ],
244          trust: '适用于记者、编辑、分析师、研究机构代表及媒体合作伙伴',
245          name: '姓名 *',
246          namePh: '您的姓名',
247          email: '邮箱 *',
248          emailPh: '请输入邮箱',
249          phone: '手机号 *',
250          phonePh: '请输入手机号',
251          website: '微信号 *',
252          websitePh: '请输入微信号',
253          intro: '简单介绍一下意向详情 *',
254          introPh: '请简要说明媒体名称、报道主题、截止时间及所需资料。'
255        },
256        businessPartnership: {
257          title: '商务合作',
258          sub: '请填写以下信息,商务合作团队会尽快与您联系。',
259          kicker: '商务合作',
260          pitchTitle: '告诉我们您的合作意向',
261          pitchText: '请留下合作方向、公司背景和所需支持,我们会将需求转交给合适团队跟进。',
262          pitchPoints: [
263            '明确提供服务、探索合作或其他意向',
264            '留下公司、网站或领英与联系信息',
265            '说明是否需要在线体验'
266          ],
267          trust: '适用于解决方案伙伴、服务商、生态合作方及企业联系人。',
268          purpose: '意向 *',
269          purposeOptions: ['提供服务', '探索合作', '其他'],
270          name: '姓名 *',
271          namePh: '您的姓名',
272          role: '职务 *',
273          rolePh: '您的职务',
274          email: '邮箱 *',
275          emailPh: '请输入邮箱',
276          phone: '手机号 *',
277          phonePh: '请输入手机号',
278          company: '公司名称 *',
279          companyPh: '公司名称',
280          website: '官网或领英 *',
281          websitePh: '官网或领英链接',
282          intro: '简单介绍一下意向详情 *',
283          introPh: '当前需引入AI应用解决的场景:\n待解决的问题:\n预期效果:\n可投入的预算:',
284          online: '是否需要在线体验?*',
285          onlineCheck: '需要在线体验',
286          onlineOptions: ['是', '否'],
287          source: '您从何种渠道了解到我们?*',
288          sourceOptions: ['小红书', '抖音', '视频号', '微博', '微信公众号', '线下峰会', '其他'],
289          selectPlaceholder: '请选择'
290        },
291        splitName: false,
292        fullName: '姓名 *',
293        fullNamePh: '您的姓名',
294        company: '公司名称 *',
295        companyPh: '公司名称',
296        role: '职位或部门 *',
297        rolePh: '您的职位或部门',
298        phone: '手机号 *',
299        phonePh: '请输入手机号',
300        smsCode: '验证码 *',
301        smsCodePh: '6 位验证码',
302        sendCode: '获取验证码',
303        sendCodeWait: '秒后重发',
304        phoneInvalid: '请输入正确的手机号。',
305        codeRequired: '请输入 6 位验证码。',
306        codeInvalid: '验证码错误或已过期,请重新获取。',
307        codeSent: '验证码已发送,请注意查收。',
308        sendCodeFail: '验证码发送失败,请稍后重试。',
309        sendCodeRateLimit: '请求过于频繁,请稍后重试。',
310        region: '国家或地区 *',
311        regions: ['中国大陆', '中国香港', '新加坡', '美国', '其他国家或地区'],
312        message: '项目概况 *',
313        messagePh: '请简述业务场景、目标、数据基础、计划启动时间和部署要求。',
314        submit: '提交',
315        close: '关闭',
316        success: '已提交 · 2 个工作日内与您联系',
317        emailInvalid: '请输入有效的邮箱地址。',
318        phoneDigits: '手机号请填写纯数字(不含空格、加号或其他符号)。',
319        submitting: '提交中…',
320        submitError: '提交失败,请稍后重试。',
321        successTitle: '提交成功',
322        successBody: '我们已收到您的信息,将在 2 个工作日内与您联系。',
323        assistantName: '01.AI 客服',
324        assistantStatus: '在线',
325        quickLabel: '快捷提问',
326        quickPrompts: [
327          '我想评估一个企业 AI 场景',
328          '我想预约产品演示',
329          '我想了解私有化部署'
330        ],
331        contactTitle: '联系人卡片',
332        contactHint: 'AI 客服只需要几项信息,方便顾问准确回访。',
333        send: '发送给 AI 顾问'
334      };
335
336  const reducePageMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
337
338  /* ─── Sticky nav state ──────────────────────────────────────────────── */
339  const nav = document.getElementById('nav');
340  if (nav) {
341    const onScroll = () => {
342      nav.classList.toggle('scrolled', window.scrollY > 20);
343    };
344    onScroll();
345    window.addEventListener('scroll', onScroll, { passive: true });
346  }
347
348  document.addEventListener('click', (event) => {
349    if (event.target.closest('.nav-dd > button.nav-trigger')) {
350      event.preventDefault();
351    }
352  });
353
354  document.addEventListener('click', (event) => {
355    if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return;
356    const anchor = event.target.closest('.nav a[href], .nav-drawer a[href]');
357    if (!anchor || anchor.target && anchor.target !== '_self') return;
358    let url;
359    try {
360      url = new URL(anchor.getAttribute('href'), window.location.href);
361    } catch {
362      return;
363    }
364    const isSamePageHash = url.origin === window.location.origin
365      && url.pathname === window.location.pathname
366      && url.search === window.location.search
367      && url.hash;
368    if (!isSamePageHash) return;
369    const target = document.getElementById(decodeURIComponent(url.hash.slice(1)));
370    if (!target) return;
371    event.preventDefault();
372    target.scrollIntoView({ behavior: reducePageMotion.matches ? 'auto' : 'smooth', block: 'start' });
373    if (window.location.hash !== url.hash) {
374      window.history.pushState(null, '', url.hash);
375    }
376    const activeDrawer = document.getElementById('drawer');
377    if (activeDrawer) activeDrawer.classList.remove('open');
378    document.body.style.overflow = '';
379  });
380
381  /* Product megamenu — keep open while crossing the gap / adjacent nav items */
382  document.querySelectorAll('.product-nav-dd').forEach((dropdown) => {
383    const panel = dropdown.querySelector('.nav-product-panel');
384    if (!panel) return;
385
386    let closeTimer;
387    const open = () => {
388      clearTimeout(closeTimer);
389      dropdown.classList.add('is-open');
390    };
391    const scheduleClose = () => {
392      clearTimeout(closeTimer);
393      closeTimer = window.setTimeout(() => {
394        dropdown.classList.remove('is-open');
395      }, 140);
396    };
397
398    dropdown.addEventListener('mouseenter', open);
399    dropdown.addEventListener('mouseleave', scheduleClose);
400    dropdown.addEventListener('focusin', open);
401    dropdown.addEventListener('focusout', (event) => {
402      if (!dropdown.contains(event.relatedTarget)) scheduleClose();
403    });
404    panel.addEventListener('mouseenter', open);
405    panel.addEventListener('mouseleave', scheduleClose);
406  });
407
408  /* ─── Scroll reveal ─────────────────────────────────────────────────── */
409  const revealElements = Array.from(document.querySelectorAll('.reveal'));
410  const showReveal = (el) => {
411    el.classList.add('visible');
412  };
413  const isInRevealRange = (el) => {
414    const rect = el.getBoundingClientRect();
415    return rect.top < window.innerHeight && rect.bottom > 0;
416  };
417  const io = new IntersectionObserver((entries) => {
418    entries.forEach((e) => {
419      if (e.isIntersecting) {
420        showReveal(e.target);
421        io.unobserve(e.target);
422      }
423    });
424  }, { threshold: 0.01, rootMargin: '0px 0px -24px 0px' });
425  revealElements.forEach((el) => {
426    if (isInRevealRange(el)) {
427      showReveal(el);
428    } else {
429      io.observe(el);
430    }
431  });
432
433  /* ─── Mobile drawer ─────────────────────────────────────────────────── */
434  const ham = document.getElementById('ham');
435  const drawer = document.getElementById('drawer');
436  if (ham && drawer) {
437    ham.addEventListener('click', () => {
438      drawer.classList.toggle('open');
439      document.body.style.overflow = drawer.classList.contains('open') ? 'hidden' : '';
440    });
441    drawer.querySelectorAll('a').forEach((a) =>
442      a.addEventListener('click', () => {
443        drawer.classList.remove('open');
444        document.body.style.overflow = '';
445      })
446    );
447  }
448
449  /* ─── Page transition between nav tabs ─────────────────────────────── */
450  const markPageReady = () => {
451    document.body.classList.remove('page-transition-leaving');
452    document.body.classList.add('page-transition-ready');
453    if (fromNavTransition) {
454      window.setTimeout(() => {
455        document.body.classList.add('page-transition-nav-complete');
456      }, 1800);
457    }
458  };
459  window.addEventListener('pageshow', markPageReady);
460  requestAnimationFrame(markPageReady);
461
462  const isInternalNavigation = (anchor) => {
463    if (!anchor || reducePageMotion.matches) return false;
464    if (anchor.target && anchor.target !== '_self') return false;
465    if (anchor.hasAttribute('download')) return false;
466    if (anchor.closest('[data-quick], [data-open-contact], [data-business-partnership], [data-media-inquiry]')) return false;
467    const href = anchor.getAttribute('href');
468    if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) return false;
469    if (/(?:^|\/)contact\.html#quick$/.test(href.trim())) return false;
470
471    let url;
472    try {
473      url = new URL(href, window.location.href);
474    } catch {
475      return false;
476    }
477    if (url.origin !== window.location.origin) return false;
478
479    const samePath = url.pathname === window.location.pathname;
480    const sameSearch = url.search === window.location.search;
481    if (samePath && sameSearch && url.hash) return false;
482    if (samePath && sameSearch && !url.hash) return false;
483
484    return Boolean(anchor.closest('.nav, .nav-drawer'));
485  };
486
487  document.addEventListener('click', (e) => {
488    if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
489    const anchor = e.target.closest('a[href]');
490    if (!isInternalNavigation(anchor)) return;
491    e.preventDefault();
492    try {
493      window.sessionStorage.setItem(NAV_TRANSITION_KEY, '1');
494    } catch {
495      /* Ignore storage restrictions; fade-out still applies on the current page. */
496    }
497    document.body.classList.add('page-transition-leaving');
498    window.setTimeout(() => {
499      window.location.href = anchor.href;
500    }, 180);
501  });
502
503  /* Cursor glow removed — Bento style stays still. */
504
505  /* ─── Home industry backdrop carousel ───────────────────────────────── */
506  const heroCarousel = document.querySelector('.home-hero');
507  if (heroCarousel) {
508    const slides = Array.from(heroCarousel.querySelectorAll('[data-hero-backdrop]'));
509    const tabs = Array.from(heroCarousel.querySelectorAll('[data-hero-slide]'));
510    const metrics = Array.from(heroCarousel.querySelectorAll('[data-hero-metrics]'));
511    const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
512    let activeSlide = 0;
513    let rotation;
514    let rotationStartTimer;
515    let transitionTimer;
516    let transitionToken = 0;
517
518    const preloadSlide = (slide) => {
519      const img = slide && slide.querySelector('img');
520      if (!img || img.complete) return Promise.resolve();
521      if (typeof img.decode === 'function') {
522        return img.decode().catch(() => undefined);
523      }
524      return new Promise((resolve) => {
525        img.addEventListener('load', resolve, { once: true });
526        img.addEventListener('error', resolve, { once: true });
527      });
528    };
529    slides.forEach((slide) => preloadSlide(slide));
530
531    const showSlide = async (index) => {
532      if (index === activeSlide || !slides[index]) return;
533      const token = ++transitionToken;
534      const previous = slides[activeSlide];
535      const next = slides[index];
536      await preloadSlide(next);
537      if (token !== transitionToken) return;
538
539      window.clearTimeout(transitionTimer);
540      slides.forEach((slide, slideIndex) => {
541        slide.classList.toggle('is-active', slideIndex === activeSlide);
542        slide.classList.remove('is-leaving', 'is-next');
543      });
544      next.classList.add('is-next');
545      next.getBoundingClientRect();
546      next.classList.add('is-active');
547      if (previous) previous.classList.add('is-leaving');
548      activeSlide = index;
549      tabs.forEach((tab, tabIndex) => {
550        const active = tabIndex === index;
551        tab.classList.toggle('is-active', active);
552        tab.setAttribute('aria-selected', String(active));
553        tab.tabIndex = active ? 0 : -1;
554      });
555      metrics.forEach((card, cardIndex) => {
556        const active = cardIndex === index;
557        card.classList.toggle('is-active', active);
558        card.setAttribute('aria-hidden', String(!active));
559      });
560      transitionTimer = window.setTimeout(() => {
561        slides.forEach((slide, slideIndex) => {
562          slide.classList.toggle('is-active', slideIndex === activeSlide);
563          slide.classList.remove('is-leaving', 'is-next');
564        });
565      }, 1450);
566    };
567    const stopRotation = () => {
568      window.clearInterval(rotation);
569      window.clearTimeout(rotationStartTimer);
570    };
571    const startRotation = () => {
572      stopRotation();
573      if (!reduceMotion.matches && slides.length > 1) {
574        rotation = window.setInterval(() => showSlide((activeSlide + 1) % slides.length), 5600);
575      }
576    };
577    const startRotationAfterFirstSwitch = () => {
578      startRotation();
579      showSlide((activeSlide + 1) % slides.length);
580    };
581    const scheduleRotationStart = (delay = 5000) => {
582      stopRotation();
583      if (!reduceMotion.matches && slides.length > 1) {
584        rotationStartTimer = window.setTimeout(startRotationAfterFirstSwitch, delay);
585      }
586    };
587    const waitForHeroTitle = () => {
588      const titleLines = Array.from(heroCarousel.querySelectorAll('.hero-display .line'));
589      const lastLine = titleLines[titleLines.length - 1];
590      if (!lastLine || getComputedStyle(lastLine).animationName === 'none') {
591        scheduleRotationStart();
592        return;
593      }
594      const fallback = window.setTimeout(() => scheduleRotationStart(), 1700);
595      lastLine.addEventListener('animationend', () => {
596        window.clearTimeout(fallback);
597        scheduleRotationStart();
598      }, { once: true });
599    };
600
601    tabs.forEach((tab, index) => {
602      tab.addEventListener('click', () => {
603        showSlide(index);
604        startRotation();
605      });
606    });
607    document.addEventListener('visibilitychange', () => {
608      if (document.hidden) stopRotation();
609      else scheduleRotationStart();
610    });
611    reduceMotion.addEventListener('change', () => scheduleRotationStart());
612    waitForHeroTitle();
613  }
614
615  /* ─── Customer case carousel ───────────────────────────────────────── */
616  document.querySelectorAll('[data-customer-carousel]').forEach((carousel) => {
617    const track = carousel.querySelector('[data-customer-track]');
618    const prev = carousel.querySelector('[data-customer-prev]');
619    const next = carousel.querySelector('[data-customer-next]');
620    if (!track || !prev || !next) return;
621
622    const cardStep = () => {
623      const card = track.querySelector('.customer-card');
624      const gap = Number.parseFloat(getComputedStyle(track).columnGap || getComputedStyle(track).gap || '0') || 0;
625      return card ? card.getBoundingClientRect().width + gap : track.clientWidth * 0.82;
626    };
627    const updateControls = () => {
628      const maxScroll = track.scrollWidth - track.clientWidth;
629      prev.disabled = track.scrollLeft <= 4;
630      next.disabled = track.scrollLeft >= maxScroll - 4;
631    };
632    const move = (direction) => {
633      track.scrollBy({
634        left: direction * cardStep(),
635        behavior: reducePageMotion.matches ? 'auto' : 'smooth'
636      });
637      window.setTimeout(updateControls, reducePageMotion.matches ? 0 : 260);
638    };
639
640    prev.addEventListener('click', () => move(-1));
641    next.addEventListener('click', () => move(1));
642    track.addEventListener('scroll', updateControls, { passive: true });
643    window.addEventListener('resize', updateControls, { passive: true });
644    updateControls();
645  });
646
647  /* ─── Home solution industry tabs ──────────────────────────────────── */
648  document.querySelectorAll('[data-home-solution-tabs]').forEach((module) => {
649    const tabs = Array.from(module.querySelectorAll('[data-home-solution-tab]'));
650    const panels = Array.from(module.querySelectorAll('[data-home-solution-panel]'));
651    if (!tabs.length || !panels.length) return;
652    let activeIndex = Math.max(0, tabs.findIndex((item) => item.classList.contains('is-active')));
653    let rotation;
654
655    const keepTabVisible = (tab) => {
656      const track = tab.closest('.home-solution-tablist');
657      if (!track || track.scrollWidth <= track.clientWidth) return;
658      const tabLeft = tab.offsetLeft;
659      const tabRight = tabLeft + tab.offsetWidth;
660      const viewLeft = track.scrollLeft;
661      const viewRight = viewLeft + track.clientWidth;
662      let nextLeft = null;
663      if (tabLeft < viewLeft) nextLeft = tabLeft;
664      if (tabRight > viewRight) nextLeft = tabRight - track.clientWidth;
665      if (nextLeft === null) return;
666      track.scrollTo({
667        left: nextLeft,
668        behavior: reducePageMotion.matches ? 'auto' : 'smooth'
669      });
670    };
671
672    const activate = (index, focus = false) => {
673      if (!tabs[index] || !panels[index]) return;
674      activeIndex = index;
675      tabs.forEach((tab, tabIndex) => {
676        const active = tabIndex === index;
677        tab.classList.toggle('is-active', active);
678        tab.setAttribute('aria-selected', String(active));
679        tab.tabIndex = active ? 0 : -1;
680        if (active && focus) tab.focus({ preventScroll: true });
681        if (active && !focus) keepTabVisible(tab);
682      });
683      panels.forEach((panel, panelIndex) => {
684        const active = panelIndex === index;
685        panel.classList.toggle('is-active', active);
686        panel.hidden = !active;
687      });
688    };
689    const stopRotation = () => {
690      window.clearInterval(rotation);
691    };
692    const startRotation = () => {
693      stopRotation();
694      if (!reducePageMotion.matches && tabs.length > 1) {
695        rotation = window.setInterval(() => activate((activeIndex + 1) % tabs.length), 5200);
696      }
697    };
698
699    tabs.forEach((tab, index) => {
700      tab.addEventListener('click', () => {
701        activate(index);
702        startRotation();
703      });
704      tab.addEventListener('keydown', (event) => {
705        const current = tabs.findIndex((item) => item.classList.contains('is-active'));
706        let nextIndex = current;
707        if (event.key === 'ArrowRight') nextIndex = (current + 1) % tabs.length;
708        if (event.key === 'ArrowLeft') nextIndex = (current - 1 + tabs.length) % tabs.length;
709        if (event.key === 'Home') nextIndex = 0;
710        if (event.key === 'End') nextIndex = tabs.length - 1;
711        if (nextIndex !== current) {
712          event.preventDefault();
713          activate(nextIndex, true);
714          startRotation();
715        }
716      });
717    });
718    module.addEventListener('mouseenter', stopRotation);
719    module.addEventListener('mouseleave', startRotation);
720    module.addEventListener('focusin', stopRotation);
721    module.addEventListener('focusout', () => {
722      window.setTimeout(() => {
723        if (!module.contains(document.activeElement)) startRotation();
724      }, 0);
725    });
726    document.addEventListener('visibilitychange', () => {
727      if (document.hidden) stopRotation();
728      else startRotation();
729    });
730    reducePageMotion.addEventListener('change', startRotation);
731    startRotation();
732  });
733
734  /* ─── Contact modal (global) ────────────────────────────────────────── */
735  const businessPartnership = T.businessPartnership;
736  const optionTags = (options) => options.map((option) => `<option value="${option}">${option}</option>`).join('');
737  const phoneInputConstraints = isEN
738    ? 'inputmode="tel"'
739    : 'inputmode="numeric" pattern="1[3-9][0-9]{9}" minlength="11" maxlength="11"';
740  const smsVerificationField = () => isEN ? '' : `
741          <div class="form-field form-field-sms-code">
742            <label>${T.smsCode}</label>
743            <div class="phone-code-input-wrap">
744              <input type="text" name="smsCode" placeholder="${T.smsCodePh}" autocomplete="one-time-code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" required>
745              <button type="button" class="phone-verify-send">${T.sendCode}</button>
746            </div>
747            <p class="phone-verify-feedback" role="status" aria-live="polite" hidden></p>
748          </div>`;
749  const businessPartnershipFields = (topicValue, topicId = '') => `
750          <input type="hidden" name="topic"${topicId ? ` id="${topicId}"` : ''} value="${topicValue}">
751          <div class="form-field">
752            <label>${businessPartnership.name}</label>
753            <input type="text" name="name" placeholder="${businessPartnership.namePh}" autocomplete="name" required>
754          </div>
755          <div class="form-field">
756            <label>${businessPartnership.role}</label>
757            <input type="text" name="role" placeholder="${businessPartnership.rolePh}" autocomplete="organization-title" required>
758          </div>
759          <div class="form-field">
760            <label>${businessPartnership.email}</label>
761            <input type="email" name="email" placeholder="${businessPartnership.emailPh}" autocomplete="email" required>
762          </div>
763          <div class="form-field">
764            <label>${businessPartnership.phone}</label>
765            <input type="tel" name="phone" placeholder="${businessPartnership.phonePh}" autocomplete="tel" ${phoneInputConstraints} required>
766          </div>
767          ${smsVerificationField()}
768          <div class="form-field">
769            <label>${businessPartnership.company}</label>
770            <input type="text" name="company" placeholder="${businessPartnership.companyPh}" autocomplete="organization" required>
771          </div>
772          <div class="form-field">
773            <label>${businessPartnership.website}</label>
774            <input type="text" name="website" placeholder="${businessPartnership.websitePh}" autocomplete="url" required>
775          </div>
776          <div class="form-field">
777            <label>${businessPartnership.purpose}</label>
778            <select name="purpose" required>
779              <option value="" selected disabled>${businessPartnership.selectPlaceholder}</option>
780              ${optionTags(businessPartnership.purposeOptions)}
781            </select>
782          </div>
783          <div class="form-field">
784            <label>${businessPartnership.source}</label>
785            <select name="source" required>
786              <option value="" selected disabled>${businessPartnership.selectPlaceholder}</option>
787              ${optionTags(businessPartnership.sourceOptions)}
788            </select>
789          </div>
790          <div class="form-field full">
791            <label>${businessPartnership.intro}</label>
792            <textarea name="introduction" placeholder="${businessPartnership.introPh}" rows="4" required></textarea>
793          </div>
794          <label class="modal-checkbox-field">
795            <input type="checkbox" name="onlineExperience" value="yes">
796            <span>${businessPartnership.onlineCheck}</span>
797          </label>`;
798  const modal = document.createElement('div');
799  modal.className = 'modal modal-consult';
800  modal.setAttribute('aria-hidden', 'true');
801  modal.innerHTML = `
802    <div class="modal-backdrop" data-close></div>
803    <div class="modal-dialog modal-demo" role="dialog" aria-modal="true" aria-labelledby="modal-title">
804      <button class="modal-close" type="button" data-close aria-label="${T.close}">
805        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
806          <path d="M6 6l12 12M6 18L18 6"/>
807        </svg>
808      </button>
809      <section class="modal-promise">
810        <div class="modal-kicker">${T.kicker}</div>
811        <h3 id="modal-pitch-title">${T.pitchTitle}</h3>
812        <p class="modal-pitch-text" id="modal-pitch-text">${T.pitchText}</p>
813        <ul class="modal-benefits">
814          ${T.pitchPoints.map((point) => `<li>${point}</li>`).join('')}
815        </ul>
816        <p class="modal-trust">${T.trust}</p>
817      </section>
818      <section class="modal-form-side">
819        <div class="modal-head">
820          <h2 id="modal-title">${T.title}</h2>
821          <p id="modal-sub">${T.sub}</p>
822        </div>
823        <form class="modal-form" id="modal-form" data-demo>
824          ${businessPartnershipFields('business', 'modal-topic')}
825          <div class="modal-actions full">
826            <button type="submit" class="btn btn-primary">
827              ${T.submit}
828            </button>
829          </div>
830        </form>
831      </section>
832    </div>`;
833  document.body.appendChild(modal);
834
835  // Build-time placeholder replaced by scripts/build-deploy.sh (FORM_API_BASE).
836  // Can also be overridden at runtime via window.OAI_FORM_API_BASE.
837  const FORM_API_BASE = String(window.OAI_FORM_API_BASE || 'https://www.wanzhi.com/app').replace(/\/+$/, '');
838  // Split so the build-time sed replacement never rewrites this guard token.
839  const FORM_API_PLACEHOLDER = '__FORM' + '_API_BASE__';
840  const HAS_FORM_API = Boolean(FORM_API_BASE && FORM_API_BASE !== FORM_API_PLACEHOLDER);
841  const FORM_ENDPOINT = HAS_FORM_API
842    ? `${FORM_API_BASE}/api/v1/landing/form_submit`
843    : null;
844  const SMS_ENDPOINT = window.OAI_SMS_ENDPOINT || (HAS_FORM_API
845    ? `${FORM_API_BASE}/api/v1/landing/send_verification_code`
846    : null);
847  const apiRequestHeaders = (endpoint) => {
848    const headers = { 'Content-Type': 'application/json' };
849    try {
850      if (new URL(endpoint, window.location.href).hostname === 'test-app.lingyiwanwu.net') {
851        headers['X-Target-Service-appdevhub'] = 'boe-landing';
852      }
853    } catch {
854      // A malformed runtime override will fail naturally when fetch is called.
855    }
856    return headers;
857  };
858  const parseApiResponse = async (response, errorPrefix, options = {}) => {
859    let data = null;
860    try {
861      data = await response.json();
862    } catch {
863      // Some successful gateway responses have no JSON body.
864    }
865
866    const hasBusinessCode = data && Object.prototype.hasOwnProperty.call(data, 'code');
867    const businessCode = hasBusinessCode ? Number(data.code) : null;
868    const businessFailed = hasBusinessCode && businessCode !== 200;
869    const missingBusinessCode = options.requireBusinessCode && !hasBusinessCode;
870    if (!response.ok || businessFailed || missingBusinessCode) {
871      const err = new Error(data?.message || `${errorPrefix}_${response.status}`);
872      err.status = response.status;
873      err.code = hasBusinessCode ? data.code : null;
874      err.apiMessage = data?.message || '';
875      err.businessError = businessFailed || missingBusinessCode;
876      throw err;
877    }
878    return data;
879  };
880
881  const SMS_CODE_TTL = 5 * 60 * 1000;
882  const SMS_RESEND_SECONDS = 60;
883  const smsStore = new Map();
884  const smsTimers = new WeakMap();
885
886  const normalizePhone = (value) => String(value || '').replace(/\s|-/g, '');
887  const isValidPhone = (phone) => (isEN
888    ? /^\+?\d{7,15}$/.test(normalizePhone(phone))
889    : /^1[3-9]\d{9}$/.test(String(phone || '').trim()));
890
891  const getSmsControls = (form) => ({
892    phoneInput: form?.querySelector('[name="phone"]'),
893    smsCodeInput: form?.querySelector('[name="smsCode"]'),
894    sendCodeBtn: form?.querySelector('.phone-verify-send'),
895    verifyFeedback: form?.querySelector('.phone-verify-feedback')
896  });
897
898  const setVerifyFeedback = (form, message, isError = false) => {
899    const { verifyFeedback }
899 = getSmsControls(form);
900    if (!verifyFeedback) return;
901    verifyFeedback.textContent = message || '';
902    verifyFeedback.hidden = !message;
903    verifyFeedback.classList.toggle('is-error', Boolean(message && isError));
904    verifyFeedback.classList.toggle('is-success', Boolean(message && !isError));
905  };
906
907  const clearSendCodeTimer = (form) => {
908    if (!form) return;
909    const state = smsTimers.get(form);
910    if (state?.timer) window.clearInterval(state.timer);
911    const { sendCodeBtn } = getSmsControls(form);
912    if (sendCodeBtn) {
913      sendCodeBtn.disabled = false;
914      sendCodeBtn.textContent = T.sendCode;
915    }
916    smsTimers.set(form, { timer: null });
917  };
918
919  const clearSmsVerification = (form) => {
920    clearSendCodeTimer(form);
921    setVerifyFeedback(form, '');
922  };
923
924  const startSendCodeCountdown = (form) => {
925    const { sendCodeBtn } = getSmsControls(form);
926    if (!sendCodeBtn) return;
927    let sendCodeRemaining = SMS_RESEND_SECONDS;
928    sendCodeBtn.disabled = true;
929    sendCodeBtn.textContent = isEN
930      ? `${T.sendCodeWait} ${sendCodeRemaining}s`
931      : `${sendCodeRemaining}${T.sendCodeWait}`;
932    const timer = window.setInterval(() => {
933      sendCodeRemaining -= 1;
934      if (sendCodeRemaining <= 0) {
935        clearSendCodeTimer(form);
936        return;
937      }
938      sendCodeBtn.textContent = isEN
939        ? `${T.sendCodeWait} ${sendCodeRemaining}s`
940        : `${sendCodeRemaining}${T.sendCodeWait}`;
941    }, 1000);
942    smsTimers.set(form, { timer });
943  };
944
945  const storeSmsCode = (phone, code) => {
946    smsStore.set(phone, { code, expiresAt: Date.now() + SMS_CODE_TTL });
947  };
948
949  const verifySmsCode = (phone, code) => {
950    const entry = smsStore.get(phone);
951    if (!entry || Date.now() > entry.expiresAt) return false;
952    return entry.code === code;
953  };
954
955  const requestSmsCode = async (phone) => {
956    if (SMS_ENDPOINT) {
957      const response = await fetch(SMS_ENDPOINT, {
958        method: 'POST',
959        headers: apiRequestHeaders(SMS_ENDPOINT),
960        body: JSON.stringify({ phone })
961      });
962      await parseApiResponse(response, 'send_failed', { requireBusinessCode: true });
963      return;
964    }
965
966    const code = String(Math.floor(100000 + Math.random() * 900000));
967    storeSmsCode(phone, code);
968    if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
969      console.info('[01.AI demo] SMS verification code:', code);
970    }
971  };
972
973  document.addEventListener('click', async (e) => {
974    const sendCodeBtn = e.target.closest('.phone-verify-send');
975    if (!sendCodeBtn) return;
976    const form = sendCodeBtn.closest('form[data-demo]');
977    const { phoneInput } = getSmsControls(form);
978    const rawPhone = phoneInput?.value || '';
979    const phone = normalizePhone(rawPhone);
980    if (!isValidPhone(rawPhone)) {
981      setVerifyFeedback(form, T.phoneInvalid, true);
982      phoneInput?.focus();
983      return;
984    }
985
986    setVerifyFeedback(form, '');
987    sendCodeBtn.disabled = true;
988    try {
989      await requestSmsCode(phone);
990      setVerifyFeedback(form, T.codeSent, false);
991      startSendCodeCountdown(form);
992    } catch (err) {
993      const rateLimited = /too many requests/i.test(err.apiMessage || '');
994      if (rateLimited) {
995        setVerifyFeedback(form, T.sendCodeRateLimit, true);
996        startSendCodeCountdown(form);
997      } else {
998        clearSendCodeTimer(form);
999        setVerifyFeedback(form, T.sendCodeFail, true);
1000      }
1001    }
1002  });
1003
1004  document.addEventListener('input', (e) => {
1005    const form = e.target.closest('form[data-demo]');
1006    if (!form || !form.querySelector('[name="smsCode"]')) return;
1007
1008    if (e.target.name === 'phone') {
1009      if (!isEN) e.target.value = e.target.value.replace(/\D/g, '').slice(0, 11);
1010      const phone = normalizePhone(e.target.value);
1011      if (smsStore.has(phone)) return;
1012      setVerifyFeedback(form, '');
1013    }
1014    if (e.target.name === 'smsCode') {
1015      const { verifyFeedback } = getSmsControls(form);
1016      if (verifyFeedback?.classList.contains('is-error')) setVerifyFeedback(form, '');
1017    }
1018  });
1019
1020  const modalTopic = modal.querySelector('#modal-topic');
1021  const modalTitle = modal.querySelector('#modal-title');
1022  const modalSub = modal.querySelector('#modal-sub');
1023  const modalKicker = modal.querySelector('.modal-kicker');
1024  const modalPitchTitle = modal.querySelector('#modal-pitch-title');
1025  const modalPitchText = modal.querySelector('#modal-pitch-text');
1026  const modalBenefits = modal.querySelector('.modal-benefits');
1027  const modalTrust = modal.querySelector('.modal-trust');
1028  const modalIntro = modal.querySelector('[name="introduction"]');
1029  const VALID_TOPICS = ['business', 'investor', 'license', 'media', 'careers'];
1030  const topicContent = {
1031    business: {
1032      title: T.title,
1033      sub: T.sub,
1034      kicker: T.kicker,
1035      pitchTitle: T.pitchTitle,
1036      pitchText: T.pitchText,
1037      pitchPoints: T.pitchPoints,
1038      trust: T.trust,
1039      message: T.message,
1040      messagePh: T.messagePh
1041    },
1042    investor: {
1043      title: T.investorTitle,
1044      sub: T.investorSub,
1045      kicker: T.investorKicker,
1046      pitchTitle: T.investorPitchTitle,
1047      pitchText: T.investorPitchText,
1048      pitchPoints: T.investorPitchPoints,
1049      trust: T.investorTrust,
1050      message: T.investorMessage,
1051      messagePh: T.investorMessagePh
1052    },
1053    media: {
1054      title: T.mediaTitle,
1055      sub: T.mediaSub,
1056      kicker: T.mediaKicker,
1057      pitchTitle: T.mediaPitchTitle,
1058      pitchText: T.mediaPitchText,
1059      pitchPoints: T.mediaPitchPoints,
1060      trust: T.mediaTrust,
1061      message: T.mediaMessage,
1062      messagePh: T.mediaMessagePh
1063    }
1064  };
1065
1066  const resetSubmitSuccess = (scope) => {
1067    scope.classList.remove('is-submit-success');
1068    scope.querySelectorAll('.modal-submit-success').forEach((panel) => {
1069      panel.hidden = true;
1070    });
1071    scope.querySelectorAll('form[data-demo]').forEach((form) => {
1072      form.hidden = false;
1073      const btn = form.querySelector('button[type="submit"]');
1074      if (btn) {
1075        btn.disabled = false;
1076        btn.classList.remove('is-success');
1077      }
1078    });
1079  };
1080
1081  const showSubmitSuccess = (form) => {
1082    const modalScope = form.closest('.modal');
1083    const formSide = form.closest('.modal-form-side') || form.parentElement;
1084    let panel = formSide?.querySelector('.modal-submit-success');
1085    if (!panel) {
1086      panel = document.createElement('section');
1087      panel.className = 'modal-submit-success';
1088      panel.hidden = true;
1089      panel.setAttribute('role', 'status');
1090      panel.setAttribute('tabindex', '-1');
1091      panel.innerHTML = `
1092        <div class="modal-submit-check" aria-hidden="true">
1093          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
1094            <path d="M20 6L9 17l-5-5"/>
1095          </svg>
1096        </div>
1097        <h3>${T.successTitle}</h3>
1098        <p>${T.successBody}</p>
1099        <button type="button" class="btn btn-primary modal-submit-close" data-close>${T.close}</button>`;
1100      form.insertAdjacentElement('afterend', panel);
1101    }
1102    form.reset();
1103    form.hidden = true;
1104    modalScope?.classList.add('is-submit-success');
1105    panel.hidden = false;
1106    window.setTimeout(() => panel.focus({ preventScroll: true }), 30);
1107  };
1108
1109  const openModal = (topic) => {
1110    const nextTopic = topic && VALID_TOPICS.includes(topic) ? topic : 'business';
1111    const content = topicContent[nextTopic] || topicContent.business;
1112    resetSubmitSuccess(modal);
1113    modalTopic.value = nextTopic;
1114    modalTitle.textContent = content.title;
1115    modalSub.textContent = content.sub;
1116    if (modalKicker) modalKicker.textContent = content.kicker;
1117    if (modalPitchTitle) modalPitchTitle.textContent = content.pitchTitle;
1118    modalPitchText.textContent = content.pitchText;
1119    if (modalBenefits) modalBenefits.innerHTML = content.pitchPoints.map((point) => `<li>${point}</li>`).join('');
1120    if (modalTrust) modalTrust.textContent = content.trust;
1121    modal.classList.add('open');
1122    modal.setAttribute('aria-hidden', 'false');
1123    document.body.style.overflow = 'hidden';
1124    setTimeout(() => {
1125      modal.querySelector('[name="name"]')?.focus();
1126    }, 60);
1127  };
1128  const closeModal = () => {
1129    modal.classList.remove('open');
1130    modal.setAttribute('aria-hidden', 'true');
1131    document.body.style.overflow = '';
1132    resetSubmitSuccess(modal);
1133    clearSmsVerification(modal.querySelector('form[data-demo]'));
1134  };
1135
1136  const businessModal = document.createElement('div');
1137  businessModal.className = 'modal modal-consult modal-business-partnership';
1138  businessModal.setAttribute('aria-hidden', 'true');
1139  businessModal.innerHTML = `
1140    <div class="modal-backdrop" data-close></div>
1141    <div class="modal-dialog modal-demo" role="dialog" aria-modal="true" aria-labelledby="business-modal-title">
1142      <button class="modal-close" type="button" data-close aria-label="${T.close}">
1143        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1144          <path d="M6 6l12 12M6 18L18 6"/>
1145        </svg>
1146      </button>
1147      <section class="modal-promise">
1148        <div class="modal-kicker">${businessPartnership.kicker}</div>
1149        <h3>${businessPartnership.pitchTitle}</h3>
1150        <p class="modal-pitch-text">${businessPartnership.pitchText}</p>
1151        <ul class="modal-benefits">
1152          ${businessPartnership.pitchPoints.map((point) => `<li>${point}</li>`).join('')}
1153        </ul>
1154        <p class="modal-trust">${businessPartnership.trust}</p>
1155      </section>
1156      <section class="modal-form-side">
1157        <div class="modal-head">
1158          <h2 id="business-modal-title">${businessPartnership.title}</h2>
1159          <p>${businessPartnership.sub}</p>
1160        </div>
1161        <form class="modal-form business-partnership-form" id="business-partnership-form" data-demo>
1162          ${businessPartnershipFields('business-partnership')}
1163          <div class="modal-actions full">
1164            <button type="submit" class="btn btn-primary">${T.submit}</button>
1165          </div>
1166        </form>
1167      </section>
1168    </div>`;
1169  document.body.appendChild(businessModal);
1170
1171  const mediaInquiry = T.mediaInquiry;
1172  const mediaModal = document.createElement('div');
1173  mediaModal.className = 'modal modal-consult modal-media-inquiry';
1174  mediaModal.setAttribute('aria-hidden', 'true');
1175  mediaModal.innerHTML = `
1176    <div class="modal-backdrop" data-close></div>
1177    <div class="modal-dialog modal-demo" role="dialog" aria-modal="true" aria-labelledby="media-modal-title">
1178      <button class="modal-close" type="button" data-close aria-label="${T.close}">
1179        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1180          <path d="M6 6l12 12M6 18L18 6"/>
1181        </svg>
1182      </button>
1183      <section class="modal-promise">
1184        <div class="modal-kicker">${mediaInquiry.kicker}</div>
1185        <h3>${mediaInquiry.pitchTitle}</h3>
1186        <p class="modal-pitch-text">${mediaInquiry.pitchText}</p>
1187        <p class="modal-trust">${mediaInquiry.trust}</p>
1188      </section>
1189      <section class="modal-form-side">
1190        <div class="modal-head">
1191          <h2 id="media-modal-title">${mediaInquiry.title}</h2>
1192          <p>${mediaInquiry.sub}</p>
1193        </div>
1194        <form class="modal-form media-inquiry-form" id="media-inquiry-form" data-demo>
1195          <input type="hidden" name="topic" value="media-inquiry">
1196          <div class="form-field">
1197            <label>${mediaInquiry.name}</label>
1198            <input type="text" name="name" placeholder="${mediaInquiry.namePh}" autocomplete="name" required>
1199          </div>
1200          <div class="form-field">
1201            <label>${mediaInquiry.website}</label>
1202            <input type="text" name="website" placeholder="${mediaInquiry.websitePh}" autocomplete="url" required>
1203          </div>
1204          <div class="form-field">
1205            <label>${mediaInquiry.phone}</label>
1206            <input type="tel" name="phone" placeholder="${mediaInquiry.phonePh}" autocomplete="tel" ${phoneInputConstraints} required>
1207          </div>
1208          ${smsVerificationField()}
1209          <div class="form-field">
1210            <label>${mediaInquiry.email}</label>
1211            <input type="email" name="email" placeholder="${mediaInquiry.emailPh}" autocomplete="email" required>
1212          </div>
1213          <div class="form-field full">
1214            <label>${mediaInquiry.intro}</label>
1215            <textarea name="introduction" placeholder="${mediaInquiry.introPh}" rows="5" required></textarea>
1216          </div>
1217          <div class="modal-actions full">
1218            <button type="submit" class="btn btn-primary">${T.submit}</button>
1219          </div>
1220        </form>
1221      </section>
1222    </div>`;
1223  document.body.appendChild(mediaModal);
1224
1225  const closeBusinessPartnershipModal = () => {
1226    businessModal.classList.remove('open');
1227    businessModal.setAttribute('aria-hidden', 'true');
1228    document.body.style.overflow = '';
1229    resetSubmitSuccess(businessModal);
1230    clearSmsVerification(businessModal.querySelector('form[data-demo]'));
1231  };
1232  const closeMediaInquiryModal = () => {
1233    mediaModal.classList.remove('open');
1234    mediaModal.setAttribute('aria-hidden', 'true');
1235    document.body.style.overflow = '';
1236    resetSubmitSuccess(mediaModal);
1237    clearSmsVerification(mediaModal.querySelector('form[data-demo]'));
1238  };
1239  const openBusinessPartnershipModal = () => {
1240    closeModal();
1241    closeMediaInquiryModal();
1242    resetSubmitSuccess(businessModal);
1243    businessModal.classList.add('open');
1244    businessModal.setAttribute('aria-hidden', 'false');
1245    document.body.style.overflow = 'hidden';
1246    setTimeout(() => {
1247      businessModal.querySelector('[name="name"]')?.focus();
1248    }, 60);
1249  };
1250  const openMediaInquiryModal = () => {
1251    closeModal();
1252    closeBusinessPartnershipModal();
1253    resetSubmitSuccess(mediaModal);
1254    mediaModal.classList.add('open');
1255    mediaModal.setAttribute('aria-hidden', 'false');
1256    document.body.style.overflow = 'hidden';
1257    setTimeout(() => {
1258      mediaModal.querySelector('[name="name"]')?.focus();
1259    }, 60);
1260  };
1261
1262  modal.addEventListener('click', (e) => {
1263    const quickPrompt = e.target.closest('[data-ai-prompt]');
1264    if (quickPrompt) {
1265      const prompt = quickPrompt.dataset.aiPrompt || quickPrompt.textContent.trim();
1266      if (modalIntro) {
1267        modalIntro.value = modalIntro.value ? `${modalIntro.value}\n${prompt}` : prompt;
1268        modalIntro.focus();
1269      }
1270      return;
1271    }
1272    if (e.target.closest('[data-close]')) closeModal();
1273  });
1274  businessModal.addEventListener('click', (e) => {
1275    if (e.target.closest('[data-close]')) closeBusinessPartnershipModal();
1276  });
1277  mediaModal.addEventListener('click', (e) => {
1278    if (e.target.closest('[data-close]')) closeMediaInquiryModal();
1279  });
1280  document.addEventListener('keydown', (e) => {
1281    if (e.key === 'Escape' && modal.classList.contains('open')) closeModal();
1282    if (e.key === 'Escape' && businessModal.classList.contains('open')) closeBusinessPartnershipModal();
1283    if (e.key === 'Escape' && mediaModal.classList.contains('open')) closeMediaInquiryModal();
1284  });
1285
1286  /* Bind triggers:
1287     - [data-quick="<topic>
1287"] buttons (on contact cards)
1288     - [data-open-contact] explicit triggers
1289     - Any <a> pointing at contact.html#quick, ../contact.html#quick, or #quick on the contact page */
1290  const wantsContactModal = (href) => {
1291    if (!href) return false;
1292    // Strip fragment normalization
1293    const h = href.trim();
1294    return /(?:^|\/)contact\.html#quick$/.test(h) || h === '#quick';
1295  };
1296  document.addEventListener('click', (e) => {
1297    const businessPartnershipBtn = e.target.closest('[data-business-partnership]');
1298    if (businessPartnershipBtn) {
1299      e.preventDefault();
1300      openBusinessPartnershipModal();
1301      return;
1302    }
1303    const mediaInquiryBtn = e.target.closest('[data-media-inquiry]');
1304    if (mediaInquiryBtn) {
1305      e.preventDefault();
1306      openMediaInquiryModal();
1307      return;
1308    }
1309    const quickBtn = e.target.closest('[data-quick]');
1310    if (quickBtn) {
1311      e.preventDefault();
1312      if (quickBtn.dataset.quick === 'investor') {
1313        openBusinessPartnershipModal();
1314        return;
1315      }
1316      if (quickBtn.dataset.quick === 'media') {
1317        openMediaInquiryModal();
1318        return;
1319      }
1320      openModal(quickBtn.dataset.quick);
1321      return;
1322    }
1323    const openBtn = e.target.closest('[data-open-contact]');
1324    if (openBtn) {
1325      e.preventDefault();
1326      openModal(openBtn.dataset.openContact || 'business');
1327      return;
1328    }
1329    const a = e.target.closest('a[href]');
1330    if (a && wantsContactModal(a.getAttribute('href'))) {
1331      e.preventDefault();
1332      openModal('business');
1333    }
1334  });
1335
1336  /* ─── Form submit (landing/form_submit) ──────────────────────────────── */
1337  const FORM_TYPE_BY_ID = {
1338    'modal-form': 'consultation',
1339    'business-partnership-form': 'business_cooperation',
1340    'media-inquiry-form': 'media_inquiry'
1341  };
1342
1343  const fieldValue = (form, name) => {
1344    const el = form.querySelector(`[name="${name}"]`);
1345    return el ? String(el.value || '').trim() : '';
1346  };
1347
1348  // Frontend validation mirroring the server rules:
1349  //   email  -> ^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$
1350  //   phone  -> ^\d+$ (digits only, after stripping spaces/dashes)
1351  const EMAIL_PATTERN = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/;
1352  const isValidEmail = (value) => EMAIL_PATTERN.test(value);
1353  const isDigitsOnlyPhone = (value) => /^\d+$/.test(value);
1354
1355  // Returns the first field that fails validation (with a native message set),
1356  // or null when the form passes. Only checks fields the form actually has.
1357  const validateContactFields = (form) => {
1358    const emailEl = form.querySelector('[name="email"]');
1359    if (emailEl) {
1360      const value = String(emailEl.value || '').trim();
1361      emailEl.setCustomValidity('');
1362      if ((emailEl.required || value) && !isValidEmail(value)) {
1363        emailEl.setCustomValidity(T.emailInvalid);
1364        return emailEl;
1365      }
1366    }
1367    const phoneEl = form.querySelector('[name="phone"]');
1368    if (phoneEl) {
1369      const value = normalizePhone(phoneEl.value);
1370      phoneEl.setCustomValidity('');
1371      if ((phoneEl.required || value) && !isDigitsOnlyPhone(value)) {
1372        phoneEl.setCustomValidity(T.phoneDigits);
1373        return phoneEl;
1374      }
1375    }
1376    return null;
1377  };
1378
1379  const buildFormPayload = (form) => {
1380    const formType = FORM_TYPE_BY_ID[form.id] || 'consultation';
1381    const payload = { form_type: formType };
1382
1383    const name = fieldValue(form, 'name');
1384    const email = fieldValue(form, 'email');
1385    const phone = normalizePhone(fieldValue(form, 'phone'));
1386    const verificationCode = fieldValue(form, 'smsCode');
1387    const website = fieldValue(form, 'website');
1388    const intro = fieldValue(form, 'introduction');
1389    if (name) payload.name = name;
1390    if (email) payload.email = email;
1391    if (phone) payload.phone = phone;
1392    if (verificationCode) payload.verification_code = verificationCode;
1393    if (website) payload.website_or_linkedin = website;
1394    if (intro) payload.intention_details = intro;
1395
1396    if (formType === 'media_inquiry') return payload;
1397
1398    const role = fieldValue(form, 'role');
1399    const company = fieldValue(form, 'company');
1400    const purpose = fieldValue(form, 'purpose');
1401    const source = fieldValue(form, 'source');
1402    if (role) payload.job_title = role;
1403    if (company) payload.company_name = company;
1404    if (purpose) payload.intention = purpose;
1405    if (source) payload.source = source;
1406    payload.online_experience_requested = Boolean(form.querySelector('[name="onlineExperience"]')?.checked);
1407    return payload;
1408  };
1409
1410  const submitFormPayload = async (payload) => {
1411    if (!FORM_ENDPOINT) {
1412      // No endpoint configured (local dev / unbuilt source): keep demo behavior.
1413      await new Promise((resolve) => setTimeout(resolve, 400));
1414      return;
1415    }
1416    const response = await fetch(FORM_ENDPOINT, {
1417      method: 'POST',
1418      headers: apiRequestHeaders(FORM_ENDPOINT),
1419      body: JSON.stringify(payload)
1420    });
1421    await parseApiResponse(response, 'form_submit_failed');
1422  };
1423
1424  const isVerificationCodeError = (err) => Number(err?.code) === 4057;
1425  const formSubmitErrorMessage = (err) =>
1426    isVerificationCodeError(err)
1427      ? T.codeInvalid
1428      : T.submitError;
1429
1430  document.querySelectorAll('form[data-demo]').forEach((form) => {
1431    form.addEventListener('submit', async (e) => {
1432      e.preventDefault();
1433      const btn = form.querySelector('button[type="submit"]');
1434      if (!btn || btn.disabled) return;
1435
1436      let verifiedPhone = '';
1437      if (form.querySelector('[name="smsCode"]')) {
1438        const { phoneInput, smsCodeInput } = getSmsControls(form);
1439        const rawPhone = phoneInput?.value || '';
1440        const phone = normalizePhone(rawPhone);
1441        const code = (smsCodeInput?.value || '').trim();
1442
1443        if (!isValidPhone(rawPhone)) {
1444          setVerifyFeedback(form, T.phoneInvalid, true);
1445          phoneInput?.focus();
1446          return;
1447        }
1448        if (!/^\d{6}$/.test(code)) {
1449          setVerifyFeedback(form, T.codeRequired, true);
1450          smsCodeInput?.focus();
1451          return;
1452        }
1453        // Built deployments let the backend verify the submitted code. The
1454        // in-memory comparison is only for unbuilt local demo previews.
1455        if (!SMS_ENDPOINT && !verifySmsCode(phone, code)) {
1456          setVerifyFeedback(form, T.codeInvalid, true);
1457          smsCodeInput?.focus();
1458          return;
1459        }
1460        verifiedPhone = phone;
1461      }
1462
1463      // Enforce native constraints first (required fields, input types),
1464      // then the stricter server-aligned email/phone checks.
1465      if (!form.reportValidity()) return;
1466
1467      const invalidField = validateContactFields(form);
1468      if (invalidField) {
1469        invalidField.reportValidity?.();
1470        invalidField.focus();
1471        return;
1472      }
1473
1474      const original = btn.innerHTML;
1475      btn.disabled = true;
1476      btn.classList.remove('is-success', 'is-error');
1477      btn.innerHTML = T.submitting;
1478
1479      try {
1480        await submitFormPayload(buildFormPayload(form));
1481      } catch (err) {
1482        const businessError = Boolean(err?.businessError);
1483        const verificationCodeError = isVerificationCodeError(err);
1484        const errorText = formSubmitErrorMessage(err);
1485        btn.innerHTML = businessError ? original : errorText;
1486        btn.classList.add('is-error');
1487        if (form.querySelector('[name="smsCode"]')) {
1488          setVerifyFeedback(form, errorText, true);
1489          if (verificationCodeError) getSmsControls(form).smsCodeInput?.focus();
1490        }
1491        setTimeout(() => {
1492          btn.innerHTML = original;
1493          btn.classList.remove('is-error');
1494          btn.disabled = false;
1495        }, 2600);
1496        return;
1497      }
1498
1499      // Success: show the confirmation panel (demo UI) over the real API result.
1500      btn.innerHTML = original;
1501      showSubmitSuccess(form);
1502      btn.disabled = false;
1503      if (form.querySelector('[name="smsCode"]')) {
1504        clearSmsVerification(form);
1505        if (verifiedPhone) smsStore.delete(verifiedPhone);
1506      }
1507    });
1508
1509    // Clear any custom validity message once the user edits the field again.
1510    form.querySelectorAll('[name="email"], [name="phone"]').forEach((el) => {
1511      el.addEventListener('input', () => el.setCustomValidity(''));
1512    });
1513  });
1514
1515  // Open modal if user lands with #quick hash directly.
1516  if (window.location.hash === '#quick') {
1517    setTimeout(() => openModal('business'), 200);
1518    history.replaceState(null, '', window.location.pathname);
1519  }
1520
1521  const oneMapBody = document.querySelector('.one-project-page .one-project-map-body');
1522  if (oneMapBody) {
1523    const svg = oneMapBody.querySelector('.one-project-map-flow');
1524    const linesGroup = svg?.querySelector('.one-project-map-flow-lines');
1525    const dotsGroup = svg?.querySelector('.one-project-map-flow-dots');
1526    const hub = oneMapBody.querySelector('.one-project-map-hub');
1527    const leftCards = oneMapBody.querySelectorAll('.one-project-map-col:not(.is-right) .one-project-map-card');
1528    const rightCards = oneMapBody.querySelectorAll('.one-project-map-col.is-right .one-project-map-card');
1529
1530    const circlePoint = (cx, cy, r, tx, ty) => {
1531      const dx = tx - cx;
1532      const dy = ty - cy;
1533      const len = Math.hypot(dx, dy) || 1;
1534      return { x: cx + (dx / len) * r, y: cy + (dy / len) * r };
1535    };
1536
1537    const linkPath = (x1, y1, x2, y2, side) => {
1538      const mx = (x1 + x2) / 2;
1539      const my = (y1 + y2) / 2;
1540      const bend = side === 'left' ? 18 : -18;
1541      return `M ${x1} ${y1} Q ${mx + bend} ${my} ${x2} ${y2}`;
1542    };
1543
1544    const addDot = (x, y) => {
1545      const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
1546      dot.setAttribute('cx', String(x));
1547      dot.setAttribute('cy', String(y));
1548      dot.setAttribute('r', '3.5');
1549      dotsGroup.appendChild(dot);
1550    };
1551
1552    const layoutOneProjectMapLinks = () => {
1553      if (!svg || !linesGroup || !dotsGroup || !hub) return;
1554
1555      const bodyRect = oneMapBody.getBoundingClientRect();
1556      if (bodyRect.width < 1 || bodyRect.height < 1) return;
1557
1558      svg.setAttribute('viewBox', `0 0 ${bodyRect.width} ${bodyRect.height}`);
1559      linesGroup.replaceChildren();
1560      dotsGroup.replaceChildren();
1561
1562      const hubRect = hub.getBoundingClientRect();
1563      const cx = hubRect.left + hubRect.width / 2 - bodyRect.left;
1564      const cy = hubRect.top + hubRect.height / 2 - bodyRect.top;
1565      const r = hubRect.width / 2;
1566
1567      const drawLinks = (cards, side) => {
1568        cards.forEach((card) => {
1569          const cardRect = card.getBoundingClientRect();
1570          const sx = side === 'left'
1571            ? cardRect.right - bodyRect.left
1572            : cardRect.left - bodyRect.left;
1573          const sy = cardRect.top + cardRect.height / 2 - bodyRect.top;
1574          const end = circlePoint(cx, cy, r, sx, sy);
1575          const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1576          path.setAttribute('d', linkPath(sx, sy, end.x, end.y, side));
1577          linesGroup.appendChild(path);
1578          addDot(sx, sy);
1579          addDot(end.x, end.y);
1580        });
1581      };
1582
1583      drawLinks(leftCards, 'left');
1584      drawLinks(rightCards, 'right');
1585    };
1586
1587    let linkFrame = 0;
1588    const scheduleOneProjectMapLinks = () => {
1589      cancelAnimationFrame(linkFrame);
1590      linkFrame = requestAnimationFrame(layoutOneProjectMapLinks);
1591    };
1592
1593    scheduleOneProjectMapLinks();
1594    window.addEventListener('resize', scheduleOneProjectMapLinks, { passive: true });
1595    window.addEventListener('load', scheduleOneProjectMapLinks);
1596    if (document.fonts?.ready) document.fonts.ready.then(scheduleOneProjectMapLinks);
1597
1598    const cover = oneMapBody.closest('.article-cover');
1599    cover?.addEventListener('transitionend', scheduleOneProjectMapLinks);
1600    if (cover && typeof MutationObserver !== 'undefined') {
1601      const coverObserver = new MutationObserver(scheduleOneProjectMapLinks);
1602      coverObserver.observe(cover, { attributes: true, attributeFilter: ['class'] });
1603    }
1604
1605    if (typeof ResizeObserver !== 'undefined') {
1606      const observer = new ResizeObserver(scheduleOneProjectMapLinks);
1607      observer.observe(oneMapBody);
1608      observer.observe(hub);
1609    }
1610  }
1611
1612  const driLayout = document.querySelector('.one-project-page .one-dri-layout');
1613  if (driLayout) {
1614    const svg = driLayout.querySelector('.one-dri-connectors');
1615    const linesGroup = svg?.querySelector('.one-dri-connectors-lines');
1616    const core = driLayout.querySelector('.one-dri-core');
1617    const skills = driLayout.querySelectorAll('.one-dri-skill');
1618
1619    const circlePoint = (cx, cy, r, tx, ty) => {
1620      const dx = tx - cx;
1621      const dy = ty - cy;
1622      const len = Math.hypot(dx, dy) || 1;
1623      return { x: cx + (dx / len) * r, y: cy + (dy / len) * r };
1624    };
1625
1626    const layoutDriLinks = () => {
1627      if (!svg || !linesGroup || !core || !skills.length) return;
1628
1629      const layoutRect = driLayout.getBoundingClientRect();
1630      if (layoutRect.width < 1 || layoutRect.height < 1) return;
1631
1632      svg.setAttribute('viewBox', `0 0 ${layoutRect.width} ${layoutRect.height}`);
1633      linesGroup.replaceChildren();
1634
1635      const coreRect = core.getBoundingClientRect();
1636      const ccx = coreRect.left + coreRect.width / 2 - layoutRect.left;
1637      const ccy = coreRect.top + coreRect.height / 2 - layoutRect.top;
1638      const cr = coreRect.width / 2;
1639
1640      skills.forEach((skill) => {
1641        const skillRect = skill.getBoundingClientRect();
1642        const scx = skillRect.left + skillRect.width / 2 - layoutRect.left;
1643        const scy = skillRect.top + skillRect.height / 2 - layoutRect.top;
1644        const sr = skillRect.width / 2;
1645        const start = circlePoint(scx, scy, sr, ccx, ccy);
1646        const end = circlePoint(ccx, ccy, cr, scx, scy);
1647        const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1648        path.setAttribute('d', `M ${start.x} ${start.y} L ${end.x} ${end.y}`);
1649        linesGroup.appendChild(path);
1650      });
1651    };
1652
1653    let driFrame = 0;
1654    const scheduleDriLinks = () => {
1655      cancelAnimationFrame(driFrame);
1656      driFrame = requestAnimationFrame(layoutDriLinks);
1657    };
1658
1659    scheduleDriLinks();
1660    window.addEventListener('resize', scheduleDriLinks, { passive: true });
1661    window.addEventListener('load', scheduleDriLinks);
1662    if (document.fonts?.ready) document.fonts.ready.then(scheduleDriLinks);
1663
1664    const driFigure = driLayout.closest('.one-method-figure');
1665    driFigure?.addEventListener('transitionend', scheduleDriLinks);
1666    if (driFigure && typeof MutationObserver !== 'undefined') {
1667      const driObserver = new MutationObserver(scheduleDriLinks);
1668      driObserver.observe(driFigure, { attributes: true, attributeFilter: ['class'] });
1669    }
1670
1671    if (typeof ResizeObserver !== 'undefined') {
1672      const driResizeObserver = new ResizeObserver(scheduleDriLinks);
1673      driResizeObserver.observe(driLayout);
1674      driResizeObserver.observe(core);
1675      skills.forEach((skill) => driResizeObserver.observe(skill));
1676    }
1677  }
1678
1679  document.querySelectorAll('[data-one-case-carousel]').forEach((carousel) => {
1680    const slides = Array.from(carousel.querySelectorAll('[data-one-case-slide]'));
1681    const dots = Array.from(carousel.querySelectorAll('[data-one-case-dot]'));
1682    const prev = carousel.querySelector('[data-one-case-prev]');
1683    const next = carousel.querySelector('[data-one-case-next]');
1684    const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
1685    let activeIndex = Math.max(0, slides.findIndex((slide) => slide.classList.contains('is-active')));
1686    let timer = 0;
1687
1688    if (slides.length < 2) return;
1689
1690    const showSlide = (index) => {
1691      activeIndex = (index + slides.length) % slides.length;
1692      slides.forEach((slide, slideIndex) => {
1693        const isActive = slideIndex === activeIndex;
1694        slide.classList.toggle('is-active', isActive);
1695        slide.setAttribute('aria-hidden', isActive ? 'false' : 'true');
1696      });
1697      dots.forEach((dot, dotIndex) => {
1698        dot.classList.toggle('is-active', dotIndex === activeIndex);
1699      });
1700    };
1701
1702    const stop = () => {
1703      if (timer) window.clearInterval(timer);
1704      timer = 0;
1705    };
1706
1707    const start = () => {
1708      stop();
1709      if (!reduceMotion.matches) {
1710        timer = window.setInterval(() => showSlide(activeIndex + 1), 4200);
1711      }
1712    };
1713
1714    prev?.addEventListener('click', () => {
1715      showSlide(activeIndex - 1);
1716      start();
1717    });
1718
1719    next?.addEventListener('click', () => {
1720      showSlide(activeIndex + 1);
1721      start();
1722    });
1723
1724    dots.forEach((dot, dotIndex) => {
1725      dot.addEventListener('click', () => {
1726        showSlide(dotIndex);
1727        start();
1728      });
1729    });
1730
1731    carousel.addEventListener('mouseenter', stop);
1732    carousel.addEventListener('mouseleave', start);
1733    carousel.addEventListener('focusin', stop);
1734    carousel.addEventListener('focusout', () => {
1735      if (!carousel.contains(document.activeElement)) start();
1736    });
1737    document.addEventListener('visibilitychange', () => {
1738      if (document.hidden) stop();
1739      else start();
1740    });
1741    reduceMotion.addEventListener?.('change', start);
1742
1743    showSlide(activeIndex);
1744    start();
1745  });
1746})();

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.