PageSourceSearch

https://eastmanbicycles.com/e-app.js?v=h54

js eastmanbicycles.com collected 2026-10-04 04:35:02 UTC 118,981 bytes, 2,677 lines download raw bytes

1/* ===== Eastman — page-specific behaviour ===== */
2/* cache-bust: h46 — PDP gallery restore + shops + Bicycles nav */
3(function () {
4  "use strict";
5  const $ = (s, c = document) => (c || document).querySelector(s);
6  const $$ = (s, c = document) => Array.from((c || document).querySelectorAll(s));
7  const PRODUCTS = window.EASTMAN_PRODUCTS || [];
8  const CATEGORIES = window.EASTMAN_CATEGORIES || [];
9  const BRANDS = window.EASTMAN_BRANDS || [];
10  const INFO = window.EASTMAN_INFO || {};
11  const STATS = window.EASTMAN_STATS || {};
12  const byId = (id) => PRODUCTS.find((p) => p.id === id);
13  const params = new URLSearchParams(location.search);
14  const pathParts = (location.pathname || "").replace(/\/+$/, "").split("/").filter(Boolean);
15  const TAG_LABELS = window.EASTMAN_TAG_LABELS || { new: "New", featured: "Featured", demand: "In Demand" };
16
17  function slugifyName(name) {
18    return String(name || "")
19      .toLowerCase()
20      .replace(/&/g, " and ")
21      .replace(/[^a-z0-9]+/g, "-")
22      .replace(/-+/g, "-")
23      .replace(/^-|-$/g, "") || "item";
24  }
25
26  function productUrl(p) {
27    if (!p) return "/bicycle-parts";
28    if (p.href) return p.href;
29    const id = p.id || "";
30    if (!/^p[0-9]+$/i.test(id)) return "/bicycle-parts";
31    const cat = p.catSlug || "parts";
32    const nameSlug = slugifyName(p.name || id);
33    return "/bicycle-parts/" + encodeURIComponent(cat) + "/" + encodeURIComponent(nameSlug + "-" + id);
34  }
35
36  function bikeUrl(slug) {
37    return "/bicycles/" + encodeURIComponent(slug);
38  }
39
40  function categoryUrl(group, catSlug) {
41    const g = group || "india";
42    if (catSlug) return "/bicycle-parts/" + encodeURIComponent(g) + "/" + encodeURIComponent(catSlug);
43    return "/bicycle-parts/" + encodeURIComponent(g);
44  }
45  window.productUrl = productUrl;
46  window.bikeUrl = bikeUrl;
47  window.categoryUrl = categoryUrl;
48  window.slugifyName = slugifyName;
49
50  function resolvePdpIds() {
51    let bikeSlug = params.get("bike");
52    let partId = params.get("id");
53    // Pretty: /bicycles/{slug}
54    if (!bikeSlug && pathParts[0] === "bicycles" && pathParts[1]) bikeSlug = pathParts[1];
55    // Legacy: /bike/{slug}
56    if (!bikeSlug && pathParts[0] === "bike" && pathParts[1]) bikeSlug = pathParts[1];
57    // Pretty: /bicycle-parts/{catSlug}/{name-slug}-{id}
58    if (!partId && pathParts[0] === "bicycle-parts" && pathParts.length >= 3) {
59      const last = pathParts[pathParts.length - 1] || "";
60      const m = last.match(/-(p[0-9]+)$/i);
61      if (m) partId = m[1];
62    }
63    // Legacy: /product/p####
64    if (!partId && pathParts[0] === "product" && pathParts[1] && /^p[0-9]+$/i.test(pathParts[1])) {
65      partId = pathParts[1];
66    }
67    return { bikeSlug: bikeSlug || "", partId: partId || "" };
68  }
69
70
71  function setCanonical(path) {
72    if (!path) return;
73    const href = path.charAt(0) === "/" ? ("https://eastmanbicycles.com" + path) : path;
74    let link = document.querySelector('link[rel="canonical"]');
75    if (!link) {
76      link = document.createElement("link");
77      link.rel = "canonical";
78      document.head.appendChild(link);
79    }
80    link.href = href;
81    try {
82      const ou = document.querySelector('meta[property="og:url"]');
83      if (ou) ou.setAttribute("content", href);
84    } catch (_) {}
85  }
86
87  function resolveCategoryPath() {
88    let group = params.get("group") || "";
89    let catSlug = params.get("cat") || "";
90    // Pretty: /bicycle-parts/{india|china}/{catSlug}  OR /bicycle-parts/{india|china}
91    if (pathParts[0] === "bicycle-parts" && pathParts[1]) {
92      if (pathParts[1] === "india" || pathParts[1] === "china") {
93        group = pathParts[1];
94        // If third segment looks like a product (ends with -p####), not a category
95        if (pathParts[2] && !/-(p[0-9]+)$/i.test(pathParts[2])) {
96          catSlug = pathParts[2];
97        } else if (!pathParts[2]) {
98          catSlug = catSlug || "";
99        }
100      }
101    }
102    return { group: group || "india", catSlug: catSlug || "" };
103  }
104  const tagPills = (tags, max) => (tags || []).slice(0, max || 3).map((t) => `<span class="cat-tag cat-tag--${t}">${TAG_LABELS[t] || t}</span>`).join("");
105  const PAGE_SIZE = document.body.dataset.page === "home" ? 24 : 24;
106  const isHomeShop = document.body.dataset.page === "home" && $("#sideQuickFilters");
107  const HOME_RENDER_CAP = 120;
108  const MOBILE_MQ = window.matchMedia("(max-width: 960px)");
109  const MOBILE_PRODUCT_CAP = 4;
110
111  const isMobileShop = () => MOBILE_MQ.matches;
112  const shopRenderCap = (desktopCap) => (isMobileShop() ? MOBILE_PRODUCT_CAP : desktopCap);
113
114  function ensureMobileTrack(root) {
115    if (!root) return null;
116    let viewport = $(".mobile-cat-nav__viewport", root);
117    if (!viewport) {
118      viewport = document.createElement("div");
119      viewport.className = "mobile-cat-nav__viewport";
120      root.appendChild(viewport);
121    }
122    let track = $(".mobile-cat-nav__track", viewport);
123    if (!track) {
124      track = document.createElement("div");
125      track.className = "mobile-cat-nav__track";
126      viewport.appendChild(track);
127    }
128    return track;
129  }
130
131  function categoryAllLink(group, slug, name, total) {
132    return `<a href="${categoryUrl(group, slug)}" class="btn btn-primary btn-block prod-more__all">All ${total} products in ${name} &rarr;</a>`;
133  }
134
135  function rangeAllLink(group, label) {
136    return `<a href="${categoryUrl(group)}" class="btn btn-outline btn-block">${label} &rarr;</a>`;
137  }
138
139  function mobileCatPill(c, active) {
140    return `<button type="button" class="mobile-cat-pill${active ? " is-active" : ""}" data-cat-group="${c.group}" data-cat-slug="${c.slug}" aria-pressed="${active ? "true" : "false"}"><span>${c.name}</span><span class="mobile-cat-count">${c.count}</span></button>`;
141  }
142
143  function scrollMobilePill(track, slug) {
144    if (!track || !slug) return;
145    const pills = $$(".mobile-cat-pill", track);
146    const btn = pills.find((b) => b.dataset.catSlug === slug);
147    if (!btn) return;
148    const idx = pills.indexOf(btn);
149    btn.scrollIntoView({ inline: idx <= 1 ? "start" : "center", block: "nearest", behavior: "smooth" });
150  }
151
152  function bindMobileCatPills(track, onPick) {
153    if (!track) return;
154    $$(".mobile-cat-pill", track).forEach((btn) => {
155      btn.addEventListener("click", () => onPick(btn.dataset.catGroup, btn.dataset.catSlug));
156    });
157  }
158
159  function assetUrl(src) {
160    if (src == null || src === "") return "";
161    const s = String(src).trim();
162    if (!s) return "";
163    if (/^https?:\/\//i.test(s) || s.startsWith("//") || s.startsWith("data:") || s.startsWith("blob:")) return s;
164    if (s.startsWith("/")) return s;
165    return "/" + s.replace(/^\.\//, "");
166  }
167
168  const imgTag = (p, cls, opts) => {
169    opts = opts || {};
170    const localRaw = (p && p.img) || "";
171    let remoteRaw = String((p && p.imgRemote) || "").trim();
172    if (remoteRaw && /\/wp-content\/uploads\//i.test(remoteRaw) && remoteRaw.indexOf("/blog/wp-content/") === -1) {
173      remoteRaw = "";
174    }
175    const src = assetUrl(localRaw || remoteRaw);
176    const loading = opts.eager ? "eager" : "lazy";
177    const prio = opts.eager ? ' fetchpriority="high"' : "";
178    const onerr = src ? ` onerror="if(!this.dataset.ebR){this.dataset.ebR=1;var u=this.src.split('?')[0];this.src=u+(u.indexOf('?')>=0?'&':'?')+'r='+Date.now();}"` : "";
179    return `<img class="${cls || ""}" src="${escAttr(src)}" alt="${escAttr((p && p.name) || "")}" width="400" height="300" decoding="async" loading="${loading}"${prio}${onerr} />`;
180  };
181
182  function brandCardHTML(b, className, withName) {
183    const label = escHtml(b.name);
184    const name = withName ? `<span>${label}</span>` : "";
185    return `<div class="${className}"><img src="${escAttr(assetUrl(b.img))}" alt="${label}" loading="lazy" />${name}</div>`;
186  }
187
188  function escHtml(s) {
189    return String(s || "")
190      .replace(/&/g, "&amp;")
191      .replace(/</g, "&lt;")
192      .replace(/>/g, "&gt;")
193      .replace(/"/g, "&quot;");
194  }
195
196  function escAttr(s) {
197    return escHtml(s);
198  }
199
200  function modelFromDesc(p) {
201    const text = String((p && (p.desc || p.short)) || "").replace(/\\n
201/g, "\n").trim();
202    const first = text.split(/\n/)[0].trim();
203    if (/^[A-Z0-9][A-Z0-9._-]{1,24}$/i.test(first) && first.length >= 3) return first;
204    return "";
205  }
206
207  function displayProductName(p) {
208    if (window.EastmanProductSEO && window.EastmanProductSEO.displayName) {
209      return window.EastmanProductSEO.displayName(p);
210    }
211    const name = String((p && p.name) || "").trim();
212    const model = modelFromDesc(p);
213    if (model && name && name.toUpperCase().indexOf(model.toUpperCase()) === -1) {
214      return name + " " + model;
215    }
216    return name;
217  }
218
219  function applyPdpSeo(seo, item) {
220    if (!seo) return;
221    document.title = seo.title;
222    let meta = document.querySelector('meta[name="description"]');
223    if (!meta) {
224      meta = document.createElement("meta");
225      meta.setAttribute("name", "description");
226      document.head.appendChild(meta);
227    }
228    meta.setAttribute("content", seo.description);
229    const imgRaw = (item && (item.img || item.imgRemote)) || seo.image || "";
230    let img = imgRaw;
231    if (img && img.indexOf("http") !== 0) {
232      img = "https://eastmanbicycles.com/" + String(img).replace(/^\//, "");
233    }
234    if (window.EastmanMeta && window.EastmanMeta.applySocial) {
235      window.EastmanMeta.applySocial({
236        title: seo.title,
237        description: seo.description,
238        image: img || undefined,
239        imageAlt: (seo.h1 || seo.title) + " — Eastman Bicycles",
240        type: "product",
241      });
242    }
243  }
244
245  function specsFromDesc(text) {
246    const rows = [];
247    const lines = String(text || "").replace(/\\n/g, "\n").split(/\n+/).map((l) => l.trim()).filter(Boolean);
248    if (lines[0] && /^[A-Z0-9][A-Z0-9._-]{1,24}$/i.test(lines[0])) {
249      rows.push(["Model", lines[0]]);
250    }
251    lines.forEach((line) => {
252      const m = line.match(/^([A-Za-z][A-Za-z0-9 /&-]{1,40})\s*:\s*(.+)$/) || line.match(/^(Size|Net Weight|Weight|Material|Colour|Color)\s*[:\-]\s*(.+)$/i);
253      if (m) rows.push([m[1], m[2]]);
254    });
255    return rows;
256  }
257
258  function productIntroFromDesc(text) {
259    if (!text) return "";
260    let s = String(text)
261      .replace(/\\n/g, "\n")
262      .replace(/\r/g, "")
263      .trim();
264    const cut = s.search(
265      /\n\s*(Product Details|Key Features|Applications|Benefits|Installation Process|Specifications)\s*:/i
266    );
267    if (cut > 0) s = s.slice(0, cut);
268    return s.replace(/\n{3,}/g, "\n\n").trim();
269  }
270
271  function productOverview(p) {
272    const intro = productIntroFromDesc(p.desc || p.short || "");
273    const snippet =
274      intro
275        .split(/\n\n+/)
276        .map((l) => l.replace(/\s+/g, " ").trim())
277        .filter(Boolean)
278        .join(" ")
279        .slice(0, 520) || p.name;
280    const range = p.group === "china" ? "Eastman China" : "Eastman India";
281    const originLine = p.group === "china"
282      ? "sourced from our China range and exported for B2B programmes"
283      : "manufactured and exported from India";
284    return `${p.name} is part of our ${range} ${p.catName || "bicycle parts"} range — ${originLine}. ${snippet} Request a B2B quote for MOQ, pricing and export lead times.`;
285  }
286
287  function pdpCheckList(items) {
288    return `<ul class="pdp-checklist">${items.map((item) => `<li>${escHtml(item)}</li>`).join("")}</ul>`;
289  }
290
291  function quoteChecklistHTML(opts) {
292    opts = opts || {};
293    const origin = opts.origin || "India";
294    const moq = (window.EASTMAN_MOQ && window.EASTMAN_MOQ.policyText) || (window.EASTMAN_FACTS && window.EASTMAN_FACTS.moqPolicyText) || "Minimum Order: 1 × 20 ft container. Mixed products are allowed. Standard MOQ starts from 200 pcs per product; selected/custom products may require higher quantities. Final MOQ depends on specification, packing and destination.";
295    return `<aside class="pdp-quote-check" aria-label="Quote checklist">
296      <p class="pdp-quote-check__title">Quote checklist</p>
297      <ul>
298        <li><strong>Origin</strong> — ${escHtml(origin)}</li>
299        <li><strong>Ask FOB/CIF</strong></li>
300        <li><strong>Share destination</strong></li>
301        <li><strong>Container / volume</strong></li>
302        <li><strong>Optional</strong> — sample ask</li>
303      </ul>
304      <div class="moq-module moq-module--pdp" data-moq-module>
305        <p class="moq-module__title">MOQ</p>
306        <p class="moq-module__text">${escHtml(moq)}</p>
307      </div>
308    </aside>`;
309  }
310
311  function specsTableHTML(rows) {
312    const list = rows || [];
313    if (!list.length) return "";
314    const top = list.slice(0, 5);
315    const rest = list.slice(5);
316    const rowHtml = (pairs) => pairs.map(([k, v]) => `<tr><th>${escHtml(k)}</th><td>${escHtml(v)}</td></tr>`).join("");
317    let html = `<div class="pdp-specs-card"><table class="pdp-attrs"><tbody>${rowHtml(top)}</tbody></table>`;
318    if (rest.length) {
319      html += `<details class="pdp-specs-more"><summary>Full specifications (${rest.length} more)</summary><table class="pdp-attrs"><tbody>${rowHtml(rest)}</tbody></table></details>`;
320    }
321    html += `</div>`;
322    return html;
323  }
324
325  function pdpSpecChipsHTML(pairs, limit) {
326    const list = (pairs || []).slice(0, limit || 6);
327    if (!list.length) return "";
328    return `<div class="pdp-spec-chips" aria-label="Key specifications">${list.map(([k, v]) =>
329      `<span class="pdp-spec-chip"><strong>${escHtml(k)}</strong> ${escHtml(v)}</span>`
330    ).join("")}</div>`;
331  }
332
333  function pdpOemBlockHTML(opts) {
334    opts = opts || {};
335    const enquiry = opts.enquiry || "OEM / Private Label Quote";
336    const sku = opts.sku || "";
337    const origin = opts.origin || "India";
338    const pillar = opts.pillar || "";
339    return `<section class="pdp-oem-block" id="pdpOem">
340      <div class="pdp-oem-block__inner">
341        <div>
342          <span class="eyebrow">OEM / Private Label</span>
343          <h2>Customise this product for your brand</h2>
344          <p>Private-label branding, colourways, packing and documentation for importers and distributors. Share your brand brief for a factory quote.</p>
345        </div>
346        <button type="button" class="btn btn-primary" data-enquiry="${escAttr(enquiry)}" data-enquiry-origin="${escAttr(origin)}" data-enquiry-sku="${escAttr(sku)}" data-enquiry-pillar="${escAttr(pillar)}">Request a Quote</button>
347      </div>
348    </section>`;
349  }
350
351  function pdpExportPackHTML(opts) {
352    opts = opts || {};
353    const moq = (window.EASTMAN_MOQ && window.EASTMAN_MOQ.policyText) || (window.EASTMAN_FACTS && window.EASTMAN_FACTS.moqPolicyText) || "Minimum Order: 1 × 20 ft container. Mixed products are allowed.";
354    return `<section class="pdp-export-pack" id="pdpExportPack">
355      <div class="pdp-block-card">
356        <div class="pdp-block-head"><span class="eyebrow">Export</span><h2>Export &amp; Packing</h2>
357          <p class="pdp-block-lead">Container programmes for B2B buyers — discuss Incoterms with our export desk.</p>
358        </div>
359        <ul class="pdp-checklist">
360          <li><strong>MOQ</strong> — ${escHtml(moq)}</li>
361          <li><strong>Mixed container</strong> — combine SKUs / models in one load</li>
362          <li><strong>Incoterms</strong> — FOB / CIF discussion by destination</li>
363          <li><strong>Docs</strong> — commercial invoice, packing list, B/L, COO if required</li>
364        </ul>
365      </div>
366    </section>`;
367  }
368
369  function pdpCertStripHTML() {
370    return `<div class="pdp-cert-strip" aria-label="Certifications">
371      <span>ISO 9001</span><span>BSCI</span><span>AEO</span><span>ZED</span>
372    </div>`;
373  }
374
375  function pdpFinalCtaHTML(opts) {
376    opts = opts || {};
377    return `<section class="pdp-final-cta">
378      <h2>Ready to quote this product?</h2>
379      <p class="cta-reassurance">For importers, distributors, wholesalers and OEM/private-label buyers.</p>
380      <div class="pdp-final-cta__actions">
381        ${opts.addQuoteBtn || ""}
382        ${opts.requestBtn || ""}
383        <a class="btn btn-text" href="https://wa.me/919115110415" target="_blank" rel="noopener">Request a Quote on WhatsApp</a>
384      </div>
385    </section>`;
386  }
387
388  function pdpPrimaryActionsHTML(opts) {
389    opts = opts || {};
390    const requestBtn = opts.requestBtn || "";
391    return `<div class="pdp-actions pdp-actions--lean">${requestBtn}</div>`;
392  }
393
394  function pdpBlockCard(eyebrow, title, intro, body, alt) {
395    const lead = intro ? `<p class="pdp-block-lead">${escHtml(intro)}</p>` : "";
396    return `
397      <section class="pdp-block${alt ? " pdp-block--alt" : ""}">
398        <div class="pdp-block-card">
399          <div class="pdp-block-head">
400            <span class="eyebrow">${escHtml(eyebrow)}</span>
401            <h2>${escHtml(title)}</h2>
402            ${lead}
403          </div>
404          <div class="pdp-block-body">${body}</div>
405        </div>
406      </section>`;
407  }
408
409  function pdpFaqItems(p) {
410    const cat = p.catName || "bicycle parts";
411    const model = p.name || "this product";
412    const origin = p.group === "china" ? "China range" : "India manufacturing";
413    return [
414      {
415        q: `What is the MOQ for ${model}?`,
416        a: "Standard export MOQ is 1 × 20 ft container. Mixed products are allowed. Typical MOQ starts from 200 pcs per product; custom/OEM may be higher. Final MOQ depends on specification, packing and destination.",
417      },
418      {
419        q: `Can I order ${model} (${cat}) with other SKUs in one container?`,
420        a: "Yes — mixed container loads are available. Share preferred SKUs and destination so we can confirm packing and volume.",
421      },
422      {
423        q: `Do you offer OEM / private label for ${cat}?`,
424        a: "Yes. Branding, colourways and packing can be discussed for OEM/private-label programmes. Use Request a Quote on this page.",
425      },
426      {
427        q: `Where is ${model} from?`,
428        a: `${model} is part of our ${origin}. Origin is shown on the product page; China-range items are not manufactured in India.`,
429      },
430      {
431        q: "Which Incoterms and documents can you support?",
432        a: "FOB and CIF can be discussed by destination. We support commercial invoice, packing list, bill of lading and certificate of origin where required.",
433      },
434    ];
435  }
436
437  function pdpExtraSections(p, opts) {
438    opts = opts || {};
439    const faqs = pdpFaqItems(p)
440      .slice(0, 5)
441      .map(
442        (item) =>
443          `<details class="pdp-faq-item"><summary>${escHtml(item.q)}</summary><p>${escHtml(item.a)}</p></details>`
444      )
445      .join("");
446    return `
447      <div class="pdp-sections pdp-sections--sourcing">
448        ${pdpBlockCard("FAQ", "Product FAQ", "", `<div class="pdp-faq">${faqs}</div>`, true)}
449      </div>`;
450  }
451
452
453  function pdpGalleryHTML(p, images) {
454    const list = (images && images.length ? images : [{ src: p.img || p.imgRemote, thumb: p.img || p.imgRemote }])
455      .filter((img) => img && img.src)
456      .map((img) => ({ src: assetUrl(img.src), thumb: assetUrl(img.thumb || img.src) }));
457    const main = list[0] || { src: assetUrl(p.img || p.imgRemote || ""), thumb: assetUrl(p.img || p.imgRemote || "") };
458    const mainSrc = main.src || assetUrl(p.img || p.imgRemote || "") || "";
459    let remote = p.imgRemote && /^https?:\/\//i.test(String(p.imgRemote).trim()) ? String(p.imgRemote).trim() : "";
460    if (remote && /\/wp-content\/uploads\//i.test(remote) && remote.indexOf("/blog/wp-content/") === -1) remote = "";
461    const onerr = mainSrc ? ` onerror="if(!this.dataset.ebR){this.dataset.ebR=1;var u=this.src.split('?')[0];this.src=u+'?r='+Date.now();}"` : "";
462    const mainImg = `<img id="pdpMainImg" class="pdp-main-img" src="${escAttr(mainSrc)}" alt="${escHtml(p.name || "")}" loading="eager"${onerr} />`;
463    if (list.length <= 1) return `<div class="pdp-gallery">${mainImg}</div>`;
464    const thumbs = list
465      .map(
466        (img, idx) =>
467          `<button type="button" class="pdp-thumb${idx === 0 ? " is-active" : ""}" data-src="${escAttr(img.src)}" aria-label="View image ${idx + 1}"><img src="${escAttr(img.thumb || img.src)}" alt="" loading="lazy" /></button>`
468      )
469      .join("");
470    return `<div class="pdp-gallery">${mainImg}<div class="pdp-thumbs">${thumbs}</div></div>`;
471  }
472
473  function bindPdpGallery(root) {
474    const main = $("#pdpMainImg", root);
475    if (!main) return;
476    $$(".pdp-thumb", root).forEach((btn) => {
477      btn.addEventListener("click", () => {
478        main.src = btn.dataset.src;
479        $$(".pdp-thumb", root).forEach((b) => b.classList.toggle("is-active", b === btn));
480      });
481    });
482  }
483
484  function loadPdpGallery(p, root) {
485    const imgs = (p.images || []).map((src) => ({ src, thumb: src }));
486    if (imgs.length > 1) {
487      const media = $(".pdp-media", root);
488      if (media) {
489        media.innerHTML = (p.tags && p.tags[0] ? `<span class="pdp-badge pcard-badge--${p.tags[0]}">${TAG_LABELS[p.tags[0]] || p.tags[0]}</span>` : "") + pdpGalleryHTML(p, imgs);
490        bindPdpGallery(root);
491      }
492      return;
493    }
494    if (!p.wcId) return;
495    fetch(`https://eastmanbicycles.com/wp-json/wc/store/v1/products/${p.wcId}`)
496      .then((r) => (r.ok ? r.json() : null))
497      .then((data) => {
498        if (!data || !data.images || data.images.length <= 1) return;
499        const gallery = data.images.map((img) => ({ src: img.src, thumb: img.thumbnail || img.src }));
500        const media = $(".pdp-media", root);
501        if (!media) return;
502        media.innerHTML = (p.tags && p.tags[0] ? `<span class="pdp-badge pcard-badge--${p.tags[0]}">${TAG_LABELS[p.tags[0]] || p.tags[0]}</span>` : "") + pdpGalleryHTML(p, gallery);
503        bindPdpGallery(root);
504      })
505      .catch(() => {});
506  }
507
508  const BANNER_CATS = [
509    { group: "india", slug: "saddle-saddle-parts", title: "Premium", grad: "Saddles & Parts", desc: "72+ SKUs — export-ready comfort seats and accessories for every bicycle segment." },
510    { group: "india", slug: "pedals", title: "Heavy-Duty", grad: "Pedals", desc: "71+ models — alloy, steel and composite pedals built for daily riding and bulk orders." },
511    { group: "india", slug: "hub-parts", title: "Precision", grad: "Hub Parts", desc: "Durable hubs, axles and fittings for OEM and aftermarket supply." },
512    { group: "india", slug: "brakes-brake-parts", title: "Reliable", grad: "Brakes & Parts", desc: "69+ SKUs — calipers, levers, cables and brake sets for safe, consistent performance." },
513    { group: "india", slug: "tyres-tubes", title: "Export-Ready", grad: "Tyres & Tubes", desc: "67+ sizes — road, city and MTB tyres with matching tubes for global distributors." },
514    { group: "india", slug: "complete-bicycles", title: "Ready-to-Ship", grad: "Complete Bicycles", desc: "29+ bicycle models — city, MTB and kids ranges for importers and fleet buyers." },
515    { group: "china", slug: "chainwheel-cranks", title: "High-Precision", grad: "Chainwheel & Cranks", desc: "39+ alloy chainwheels and cranks — lightweight performance for modern bicycles." },
516    { group: "china", slug: "hubs-hubs-parts", title: "Engineered", grad: "Hubs & Hub Parts", desc: "39+ precision hubs — sealed bearings and alloy construction for premium builds." },
517  ];
518
519  function productScore(p) {
520    let s = 0;
521    if (p.tags && p.tags.includes("demand")) s += 2;
522    if (p.tags && p.tags.includes("featured")) s += 1;
523    return s;
524  }
525
526  function pickBannerProducts({ group, catSlug, limit = 6, excludeImgs } = {}) {
527    const seen = new Set(excludeImgs || []);
528    const pool = PRODUCTS.filter((p) => {
529      if (group && p.group !== group) return false;
530      if (catSlug && p.catSlug !== catSlug) return false;
531      return p.img || p.imgRemote;
532    }).sort((a, b) => productScore(b) - productScore(a));
533
534    const out = [];
535    for (const p of pool) {
536      const key = p.img || p.imgRemote;
537      if (seen.has(key)) continue;
538      seen.add(key);
539      out.push(p);
540      if (out.length >= limit) break;
541    }
542    return out;
543  }
544
545
546  /* ================= home hero banners (fixed 3 conversion slides) ================= */
547
548  function bikesBySlugs(slugs) {
549    const bikes = (window.getAllIndiaBikes ? window.getAllIndiaBikes() : []).filter(Boolean);
550    const bySlug = new Map(bikes.map((b) => [b.slug, b]));
551    const out = [];
552    slugs.forEach((slug) => {
553      const b = bySlug.get(slug);
554      if (!b) return;
555      out.push({
556        id: "bike-" + b.slug,
557        name: b.name,
558        img: b.img,
559        href: bikeUrl(b.slug),
560      });
561    });
562    return out;
563  }
564
565  function pickIntroBikeProducts() {
566    /* Banner 1 — complete bicycles */
567    return bikesBySlugs(["cornado-26", "brooklyn-29-ms", "tesler-26-ms", "u-bike-20"]).slice(0, 4);
568  }
569
570  function pickBanner2BikeProducts() {
571    /* Banner 2 — complete bicycles */
572    return bikesBySlugs(["cloudia-1620", "hotride-1620", "jessica-20", "highlander-29-ms"]).slice(0, 4);
573  }
574
575  function pickPartsBannerProducts() {
576    const specs = [
577      { group: "india", catSlug: "saddle-saddle-parts", label: "Saddle" },
578      { group: "india", catSlug: "pedals", label: "Pedal" },
579      { group: "india", catSlug: "handle-bar", label: "Handlebars" },
580      { group: "india", catSlug: "chains", label: "Chain" },
581      { group: "india", catSlug: "tyres-tubes", label: "Rim / Tire" },
582    ];
583    const out = [];
584    const seen = new Set();
585    specs.forEach((spec) => {
586      const picked = pickBannerProducts({ group: spec.group, catSlug: spec.catSlug, limit: 1, excludeImgs: [...seen] });
587      if (!picked.length) return;
588      const p = { ...picked[0] };
589      if (spec.label) p.name = spec.label + (p.name ? " — " + p.name : "");
590      const key = p.img || p.imgRemote;
591      if (key) seen.add(key);
592      out.push(p);
593    });
594    /* Rim fallback from rims category if tyre pick failed */
595    if (out.length < 5) {
596      const rim = pickBannerProducts({ group: "india", catSlug: "rims", limit: 1, excludeImgs: [...seen] });
597      if (rim.length) {
598        const p = { ...rim[0], name: "Rim / Tire" + (rim[0].name ? " — " + rim[0].name : "") };
599        out.push(p);
600      }
601    }
602    return out.slice(0, 4);
603  }
604
605  function bannerShortName(name) {
606    if (!name) return "";
607    const base = String(name).split(" — ")[0].split(" - ")[0].trim();
608    return base.length > 24 ? base.slice(0, 22) + "…" : base;
609  }
610
611  /** One large featured product + slim model chips (replaces cramped collage boxes). */
612  function bannerShowcaseHTML(products, fallback) {
613    const items = (products && products.length ? products : fallback ? [fallback] : []).slice(0, 5);
614    if (!items.length) return "";
615    const hero = items[0];
616    const rest = items.slice(1);
617    const heroHref = productUrl(hero);
618    const chips = rest
619      .map((p) => {
620        const href = productUrl(p);
621        return `<a class="banner-chip" href="${href}" title="${escAttr(p.name)}">
622          <span class="banner-chip__img">${imgTag(p, "", { eager: false })}</span>
623        </a>`;
624      })
625      .join("");
626    return `<div class="banner-showcase">
627      <a class="banner-hero" href="${heroHref}" title="${escAttr(hero.name)}">
628        ${imgTag(hero, "", { eager: true })}
629      </a>
630      ${chips ? `<div class="banner-chips">${chips}</div>` : ""}
631    </div>`;
632  }
633
634  function bannerCollageHTML(products, fallback) {
635    return bannerShowcaseHTML(products, fallback);
636  }
637
638  function buildHomeBannerSlides() {
639    const banner = $("#banner");
640    if (!banner || document.body.dataset.page !== "home") return;
641    const controls = $(".banner-controls", banner);
642    const hasStaticIntro = !!$('[data-home-intro="1"]', banner);
643
644    if (hasStaticIntro) {
645      const art = $("#homeIntroArt") || $(".banner-art", $('[data-home-intro="1"]', banner));
646      if (art) art.innerHTML = bannerCollageHTML(pickIntroBikeProducts());
647    }
648
649    const slide2 = `
650      <div class="banner-slide" data-banner="bikes-2">
651        <div class="banner-text">
652          <img class="banner-logo" src="assets/eastman-logo-white.png" alt="Eastman" width="180" height="54" />
653          <span class="pill"><span class="dot"></span> Kids · MTB · Wholesale</span>
654          <p class="banner-heading">Mix that <span class="grad">moves containers</span></p>
655          <p>Cloudia, Hotride, Jessica and Highlander — volume programmes built for distributors.</p>
656          <div class="banner-trust" aria-label="Trust markers">
657            <span class="banner-trust__chip">Factory MOQ / FOB</span>
658            <span class="banner-trust__chip">65+ countries</span>
659            <span class="banner-trust__chip">Three Star Export House</span>
660          </div>
661        </div>
662        <div class="banner-art">${bannerCollageHTML(pickBanner2BikeProducts())}</div>
663      </div>`;
664
665    const slide3 = `
666      <div class="banner-slide" data-banner="parts">
667        <div class="banner-text">
668          <img class="banner-logo" src="assets/eastman-logo-white.png" alt="Eastman" width="180" height="54" />
669          <span class="pill"><span class="dot"></span> Parts · India & China</span>
670          <p class="banner-heading">Fast-moving parts, <span class="grad">bulk ready</span></p>
671          <p>Saddle, pedal, handlebars, chain and rim / tire — QC-backed SKUs for mixed containers.</p>
672          <div class="banner-trust" aria-label="Trust markers">
673            <span class="banner-trust__chip">ZED / AEO</span>
674            <span class="banner-trust__chip">ISO 9001</span>
675            <span class="banner-trust__chip">Factory MOQ / FOB</span>
676          </div>
677        </div>
678        <div class="banner-art">${bannerCollageHTML(pickPartsBannerProducts())}</div>
679      </div>`;
680
681    const extra = slide2 + slide3;
682    if (hasStaticIntro) {
683      if (controls) controls.insertAdjacentHTML("beforebegin", extra);
684      else banner.insertAdjacentHTML("beforeend", extra);
685    } else {
686      const intro = `
687      <div class="banner-slide is-active" data-banner="bikes-1">
688        <div class="banner-text">
689          <img class="banner-logo" src="assets/eastman-logo-white.png" alt="Eastman" width="180" height="54" />
690          <span class="pill"><span class="dot"></span> For Importers · Distributors · Wholesalers</span>
691          <h1 class="banner-heading">Container-ready <span class="grad">complete bicycles</span> from India</h1>
692          <p>Stock <strong>Cornado</strong>, <strong>Brooklyn</strong>, <strong>Tesler Fat Bike</strong> and <strong>U Bike</strong> for your next container programme.</p>
693          <div class="banner-trust" aria-label="Trust markers">
694            <span class="banner-trust__chip">ISO 9001</span>
695            <span class="banner-trust__chip">Three Star Export House</span>
696            <span class="banner-trust__chip">Since 1970</span>
697            <span class="banner-trust__chip">65+ countries</span>
698          </div>
699        </div>
700        <div class="banner-art">${bannerCollageHTML(pickIntroBikeProducts())}</div>
701      </div>`;
702      banner.insertAdjacentHTML("afterbegin", intro + extra);
703    }
704    if (window.fixSiteLinks) window.fixSiteLinks(banner);
705  }
706
707  buildHomeBannerSlides(); /* BANNER_BUILD_H11 */
708
709  /* ---------- scroll reveal ---------- */
710  const io = "IntersectionObserver" in window
711    ? new IntersectionObserver((es) => es.forEach((e) => { if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); } }), { threshold: 0.1 })
712    : null;
713  const reveal = (el) => {
714    if (!io) {
715      el.classList.add("in");
716      return;
717    }
718    el.classList.add("reveal");
719    io.observe(el);
720    /* Cards inside overflow scroll panels can miss the first IO pass — reveal if already on-screen */
721    requestAnimationFrame(() => {
722      const r = el.getBoundingClientRect();
723      if (r.width && r.top < window.innerHeight + 40 && r.bottom > -40) {
724        el.classList.add("in");
725        io.unobserve(el);
726      }
727    });
728  };
729  const revealAll = () => $$(".section-head, .usp-card, .cert-card, .award-card, .testi-card, .catsplit-card, .infra-item, .cap, .trust, .brand-item, .pcard, .bikes-adv-card, .product-adv-card, .value-card, .cat-card, .pdp-block").forEach(reveal);
730
731  /* ---------- product card ---------- */
732  function originBadge(p) {
733    const g = p && p.group;
734    if (g === "china") return `<span class="pcard-badge pcard-badge--china">China</span>`;
735    if (g === "india") return `<span class="pcard-badge pcard-badge--india">India</span>`;
736    return "";
737  }
738
739  function addQuoteBtnHTML(opts) {
740    opts = opts || {};
741    const id = opts.id || "";
742    if (!id) return "";
743    return `<button type="button" class="btn-add-quote" data-add-quote="${escAttr(id)}" data-quote-name="${escAttr(opts.name || "")}" data-quote-sku="${escAttr(opts.sku || "")}" data-quote-origin="${escAttr(opts.origin || "")}" data-quote-type="${escAttr(opts.type || "part")}" data-quote-url="${escAttr(opts.url || "")}">Add to Quote</button>`;
744  }
745
746  function productCard(p) {
747    const href = productUrl(p);
748    const origin = p.group === "china" ? "China" : "India";
749    const sku = p.sku || (p.attrs && p.attrs.SKU) || p.id || "";
750    const badges = [];
751    badges.push(originBadge(p));
752    const tag = (p.tags || []).find((t) => t === "new" || t === "featured" || t === "demand");
753    if (tag && badges.filter(Boolean).length < 2) {
754      badges.push(`<span class="pcard-badge pcard-badge--${tag}">${TAG_LABELS[tag] || tag}</span>`);
755    }
756    const badgeHtml = badges.filter(Boolean).slice(0, 2).join("");
757    return `<a class="bikes-adv-card trend-card product-adv-card product-adv-card--lean trend-card--view" href="${href}">
758      <span class="bikes-adv-card__media">${imgTag(p)}${badgeHtml ? `<span class="pcard-badges">${badgeHtml}</span>` : ""}</span>
759      <span class="bikes-adv-card__shade" aria-hidden="true"></span>
760      <span class="bikes-adv-card__body">
761        <span>
762          <span class="bikes-adv-card__title">${escHtml(displayProductName(p))}</span>
763          <span class="bikes-adv-card__sub">${escHtml(sku ? "SKU " + sku : "")}${sku ? " · " : ""}${escHtml(origin)}</span>
764        </span>
765        <span class="bikes-adv-card__cta">View Details</span>
766      </span>
767    </a>`;
768  }
769
770  function renderGrid(el, list) {
771    if (!el) return;
772    el.innerHTML = list.length ? list.map(productCard).join("") : `<p class="empty">No products found.</p>`;
773    $$(".bikes-adv-card, .product-adv-card, .pcard", el).forEach(reveal);
774  }
775
776  function scrollToProducts() {
777    const target = $("#products") || $("#prodGridScroll") || $("#prodGrid");
778    if (target) window.scrollTo({ top: target.offsetTop - 100, behavior: "smooth" });
779  }
780
781  function renderPagination(el, total, page, onPage) {
782    if (!el) return;
783    const pages = Math.ceil(total / PAGE_SIZE);
784    if (pages <= 1) { el.innerHTML = ""; return; }
785    let html = "";
786    if (page > 1) html += `<button class="page-btn" data-p="${page - 1}">→ Prev</button>`;
787    html += `<span class="page-info" aria-current="page">Page ${page} of ${pages}</span>`;
788    if (page < pages) html += `<button class="page-btn" data-p="${page + 1}">Next →</button>`;
789    el.innerHTML = `<nav class="pagination-nav" aria-label="Pagination">${html}</nav>`;
790    $$(".page-btn", el).forEach((b) => b.addEventListener("click", () => onPage(+b.dataset.p)));
791  }
792
793  function setPressed(btn, on) {
794    if (btn) btn.setAttribute("aria-pressed", on ? "true" : "false");
795  }
796
797  function paginate(list, page) {
798    const start = (page - 1) * PAGE_SIZE;
799    return list.slice(start, start + PAGE_SIZE);
800  }
801
802  const filterByGroup = (g) => {
803    if (!g || g === "all") return PRODUCTS;
804    if (g === "india" || g === "china") return PRODUCTS.filter((p) => p.group === g);
805    return PRODUCTS.filter((p) => (p.tags || []).includes(g));
806  };
807
808  const GROUP_CAT_ORDER = {
809    india: [
810      "saddle-saddle-parts",
811      "pedals",
812      "hub-parts",
813      "brakes-brake-parts",
814      "tyres-tubes",
815      "handle-parts",
816      "single-chainwheel-cranks",
817      "frame-frame-parts",
818      "forks-fork-parts",
819      "stands",
820      "carriers",
821      "tools",
822      "mudguards",
823      "baskets",
824      "new-items",
825    ],
826    china: [],
827  };
828
829  function sortCatsByGroupOrder(list, group) {
830    const order = GROUP_CAT_ORDER[group];
831    if (!order || !order.length) return list;
832    const rank = new Map(order.map((slug, i) => [slug, i]));
833    return list.slice().sort((a, b) => {
834      const ai = rank.has(a.slug) ? rank.get(a.slug) : 9999;
835      const bi = rank.has(b.slug) ? rank.get(b.slug) : 9999;
836      if (ai !== bi) return ai - bi;
837      return (b.count || 0) - (a.count || 0);
838    });
839  }
840
841  const PARTS_EXCLUDE_CAT = { india: ["complete-bicycles"] };
842
843  /* P1 — functional parts grouping (slug → group) */
844  const PART_GROUP_ORDER = [
845    "Wheels",
846    "Drivetrain",
847    "Braking",
848    "Cockpit",
849    "Seating",
850    "Frames & Forks",
851    "Tyres",
852    "Accessories",
853  ];
854  const PART_GROUP_MAP = {
855    "hub-parts": "Wheels",
856    "hub-step": "Wheels",
857    "hubs-hubs-parts": "Wheels",
858    "steel-hubs": "Wheels",
859    "rims": "Wheels",
860    "spoke": "Wheels",
861    "spokes-nipples": "Wheels",
862    "free-wheels": "Wheels",
863    "freewheels": "Wheels",
864    "eva-wheels": "Wheels",
865    "wheels-rims": "Wheels",
866    "side-support-wheels": "Wheels",
867    "pedals": "Drivetrain",
868    "pedal-pedal-parts": "Drivetrain",
869    "chains": "Drivetrain",
870    "chainwheel-cranks": "Drivetrain",
871    "single-chainwheel-cranks": "Drivetrain",
872    "chain-wheel-for-opc": "Drivetrain",
873    "deraileur": "Drivetrain",
874    "bb-fittings": "Drivetrain",
875    "bottom-bracket-cup": "Drivetrain",
876    "grip-shifters": "Drivetrain",
877    "brakes-brake-parts": "Braking",
878    "brake-set": "Braking",
879    "brake-levers": "Braking",
880    "brake-shoes": "Braking",
881    "brake-accessories": "Braking",
882    "disc-brake": "Braking",
883    "handle-parts": "Cockpit",
884    "handle-bar": "Cockpit",
885    "handle-stem-extensions": "Cockpit",
886    "handle-grip-tape": "Cockpit",
887    "head-sets": "Cockpit",
888    "bells": "Cockpit",
889    "bell-horn": "Cockpit",
890    "air-horns-bells": "Cockpit",
891    "mirrors": "Cockpit",
892    "saddle-saddle-parts": "Seating",
893    "seat-post-accessories": "Seating",
894    "seat-clamp": "Seating",
895    "frame-frame-parts": "Frames & Forks",
896    "forks-fork-parts": "Frames & Forks",
897    "fork-fittings": "Frames & Forks",
898    "frames-and-forks": "Frames & Forks",
899    "frames": "Frames & Forks",
900    "forks": "Frames & Forks",
901    "tyres-tubes": "Tyres",
902    "tube-valve-cap": "Tyres",
903    "stands": "Accessories",
904    "side-stand": "Accessories",
905    "carriers": "Accessories",
906    "carrier": "Accessories",
907    "mudguards": "Accessories",
908    "mudguards-and-fenders": "Accessories",
909    "baskets": "Accessories",
910    "tools": "Accessories",
911    "repair-tools": "Accessories",
912    "pumps": "Accessories",
913    "pump-parts": "Accessories",
914    "locks": "Accessories",
915    "reflectors": "Accessories",
916    "chain-covers": "Accessories",
917    "bottle-cage-bottle": "Accessories",
918    "light-computer": "Accessories",
919    "helmet-parts": "Accessories",
920    "leg-guard-spokes": "Accessories",
921    "ball-retainers": "Accessories",
922    "inner-cable": "Accessories",
923    "inner-cable-casing": "Accessories",
924    "dynamo-sets": "Accessories",
925    "steamer": "Accessories",
926    "new-items": "Accessories",
927  };
928  function partGroupForSlug(slug) {
929    return PART_GROUP_MAP[slug] || "Other";
930  }
931  function groupCatsByFunction(cats) {
932    const buckets = {};
933    (cats || []).forEach((c) => {
934      const g = partGroupForSlug(c.slug);
935      if (!buckets[g]) buckets[g] = [];
936      buckets[g].push(c);
937    });
938    const ordered = [];
939    PART_GROUP_ORDER.forEach((name) => {
940      if (buckets[name] && buckets[name].length) ordered.push({ name, cats: buckets[name] });
941    });
942    Object.keys(buckets).forEach((name) => {
943      if (name === "Other" || !PART_GROUP_ORDER.includes(name)) {
944        if (buckets[name] && buckets[name].length && !ordered.find((o) => o.name === name)) {
945          ordered.push({ name, cats: buckets[name] });
946        }
947      }
948    });
949    return ordered;
950  }
951
952
953  const catsForGroup = (g) => sortCatsByGroupOrder(CATEGORIES.filter((c) => c.group === g), g);
954
955  const catsForPartsGroup = (g) => {
956    const exclude = PARTS_EXCLUDE_CAT[g] || [];
957    return catsForGroup(g).filter((c) => !exclude.includes(c.slug));
958  };
959
960  function normCatName(name) {
961    return String(name || "")
962      .toLowerCase()
963      .replace(/&/g, "and")
964      .replace(/[^a-z0-9]+/g, " ")
965      .trim();
966  }
967
968  /** Unified parts categories across India + China (matched by slug, then name). */
969  function unifiedPartsCategories() {
970    const india = catsForPartsGroup("india");
971    const china = catsForPartsGroup("china");
972    const byKey = new Map();
973
974    function upsert(c) {
975      const keySlug = c.slug;
976      const keyName = "name:" + normCatName(c.name);
977      let row = byKey.get(keySlug) || byKey.get(keyName);
978      if (!row) {
979        row = {
980          key: keySlug,
981          name: c.name,
982          indiaSlug: null,
983          chinaSlug: null,
984          indiaCount: 0,
985          chinaCount: 0,
986          img: c.img,
987          imgRemote: c.imgRemote,
988        };
989        byKey.set(keySlug, row);
990        byKey.set(keyName, row);
991      }
992      if (c.group === "india") {
993        row.indiaSlug = c.slug;
994        row.indiaCount = c.count || 0;
995        if (!row.name || row.name === row.name.toUpperCase()) row.name = c.name;
996      } else if (c.group === "china") {
997        row.chinaSlug = c.slug;
998        row.chinaCount = c.count || 0;
999      }
1000      if (!row.img && c.img) {
1001        row.img = c.img;
1002        row.imgRemote = c.imgRemote;
1003      }
1004      row.total = (row.indiaCount || 0) + (row.chinaCount || 0);
1005    }
1006
1007    india.forEach(upsert);
1008    china.forEach(upsert);
1009
1010    const rows = [];
1011    const seen = new Set();
1012    byKey.forEach((row) => {
1013      if (seen.has(row)) return;
1014      seen.add(row);
1015      rows.push(row);
1016    });
1017
1018    /* Prefer India catalogue order, then leftover China-only */
1019    const order = GROUP_CAT_ORDER.india || [];
1020    rows.sort((a, b) => {
1021      const ai = a.indiaSlug ? order.indexOf(a.indiaSlug) : 999;
1022      const bi = b.indiaSlug ? order.indexOf(b.indiaSlug) : 999;
1023      const aa = ai < 0 ? 500 : ai;
1024      const bb = bi < 0 ? 500 : bi;
1025      if (aa !== bb) return aa - bb;
1026      return a.name.localeCompare(b.name);
1027    });
1028    return rows;
1029  }
1030
1031  function trendingProducts(limit) {
1032    const scored = PRODUCTS.map((p) => {
1033      let s = 0;
1034      const tags = p.tags || [];
1035      if (tags.includes("featured")) s += 8;
1036      if (tags.includes("new")) s += 5;
1037      if (tags.includes("demand")) s += 2;
1038      return { p, s };
1039    })
1040      .filter((x) => x.s > 0)
1041      .sort((a, b) => b.s - a.s || a.p.name.localeCompare(b.p.name));
1042    const out = [];
1043    const seen = new Set();
1044    for (const { p } of scored) {
1045      if (seen.has(p.id)) continue;
1046      seen.add(p.id);
1047      out.push(p);
1048      if (limit && out.length >= limit) break;
1049    }
1050    return out;
1051  }
1052
1053  function categoryCard(c) {
1054    const p = { name: c.name, img: c.img, imgRemote: c.imgRemote };
1055    const countLabel = (c.count == null ? "" : String(c.count)) + (c.count == 1 ? " product" : " products");
1056    return `<a href="${categoryUrl(c.group, c.slug)}" class="bikes-adv-card trend-card product-adv-card cat-adv-card">
1057      <span class="bikes-adv-card__media">${imgTag(p)}</span>
1058      <span class="bikes-adv-card__shade" aria-hidden="true"></span>
1059      <span class="bikes-adv-card__body">
1060        <span>
1061          <span class="bikes-adv-card__title">${escHtml(c.name)}</span>
1062          <span class="bikes-adv-card__sub">${escHtml(countLabel.trim() || "Category")}</span>
1063        </span>
1064        <span class="bikes-adv-card__go" aria-hidden="true">→</span>
1065      </span>
1066    </a>`;
1067  }
1068
1069  function renderCategoryGrid(el, group) {
1070    if (!el) return;
1071    const list = catsForGroup(group);
1072    el.innerHTML = list.length ? list.map(categoryCard).join("") : `<p class="empty">No categories found.</p>`;
1073    $$(".bikes-adv-card, .cat-adv-card, .cat-card", el).forEach(reveal);
1074  }
1075
1076  /* ================= banner slider ================= */
1077  const banner = $("#banner");
1078  if (banner) {
1079    const slides = $$(".banner-slide", banner);
1080    const dotsWrap = $("#bDots");
1081    const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1082    let i = 0, timer = null;
1083    slides.forEach((_, idx) => {
1084      const d = document.createElement("button");
1085      d.className = "banner-dot" + (idx === 0 ? " is-active" : "");
1086      d.setAttribute("aria-label", "Slide " + (idx + 1));
1087      d.setAttribute("aria-current", idx === 0 ? "true" : "false");
1088      d.addEventListener("click", () => go(idx, true));
1089      dotsWrap.appendChild(d);
1090    });
1091    const dots = $$(".banner-dot", dotsWrap);
1092    function go(n, manual) {
1093      slides[i].classList.remove("is-active"); dots[i].classList.remove("is-active"); dots[i].setAttribute("aria-current", "false");
1094      i = (n + slides.length) % slides.length;
1095      slides[i].classList.add("is-active"); dots[i].classList.add("is-active"); dots[i].setAttribute("aria-current", "true");
1096      if (manual) restart();
1097    }
1098    function restart() { clearInterval(timer); if (!reduceMotion) timer = setInterval(() => go(i + 1), 5500); }
1099    $("#bNext").addEventListener("click", () => go(i + 1, true));
1100    $("#bPrev").addEventListener("click", () => go(i - 1, true));
1101    if (!reduceMotion) {
1102      banner.addEventListener("mouseenter", () => clearInterval(timer));
1103      banner.addEventListener("mouseleave", restart);
1104      restart();
1105    }
1106
1107    let touchStartX = 0;
1108    let touchStartY = 0;
1109    banner.addEventListener(
1110      "touchstart",
1111      (e) => {
1112        if (!e.changedTouches[0]) return;
1113        touchStartX = e.changedTouches[0].screenX;
1114        touchStartY = e.changedTouches[0].screenY;
1115      },
1116      { passive: true }
1117    );
1118    banner.addEventListener(
1119      "touchend",
1120      (e) => {
1121        if (!e.changedTouches[0]) return;
1122        const dx = e.changedTouches[0].screenX - touchStartX;
1123        const dy = e.changedTouches[0].screenY - touchStartY;
1124        if (Math.abs(dx) < 48 || Math.abs(dx) < Math.abs(dy)) return;
1125        go(dx < 0 ? i + 1 : i - 1, true);
1126        banner.dataset.swiped = "1";
1127        window.setTimeout(() => delete banner.dataset.swiped, 350);
1128      },
1129      { passive: true }
1130    );
1131    banner.addEventListener(
1132      "click",
1133      (e) => {
1134        if (!banner.dataset.swiped) return;
1135        e.preventDefault();
1136        e.stopPropagation();
1137      },
1138      true
1139    );
1140  }
1141
1142  /* ================= category page ================= */
1143  function renderPartsOriginShop() {
1144    const shop = $("#partsOriginShop") || $("#catIndex");
1145    if (!shop && !$("#catProdGrid")) return false;
1146    const catPath = resolveCategoryPath();
1147    let group = catPath.group || "india";
1148    if (group !== "india" && group !== "china") group = "india";
1149    let catSlug = catPath.catSlug || "";
1150    const originLabel = group === "china" ? "Eastman China" : "Eastman India";
1151    const cats = catsForPartsGroup(group);
1152
1153    $$(".parts-origin-tab, .cat-tab", $("#catGroupTabs")).forEach((t) => {
1154      t.classList.toggle("is-active", t.dataset.group === group);
1155    });
1156
1157    if (!catSlug && cats.length) catSlug = cats[0].slug;
1158    const catMeta = cats.find((c) => c.slug === catSlug) || CATEGORIES.find((c) => c.group === group && c.slug === catSlug);
1159
1160    const titleEl = $("#catPageTitle");
1161    const descEl = $("#catPageDesc");
1162    const crumbEl = $("#catCrumbs");
1163    const crumbOrigin = $("#catCrumbOrigin");
1164    const listTitle = $("#catListTitle");
1165    const countEl = $("#catResultCount");
1166    const grid = $("#catProdGrid");
1167    const side = $("#partsOriginCats");
1168
1169    if (titleEl) titleEl.textContent = originLabel;
1170    if (descEl) descEl.textContent = catMeta
1171      ? `${catMeta.name} — ${group === "china" ? "China" : "India"} range for B2B export.`
1172      : `Browse ${originLabel} categories and products.`;
1173    if (crumbOrigin) crumbOrigin.textContent = originLabel;
1174    if (crumbEl) {
1175      crumbEl.innerHTML = catMeta
1176        ? `<a href="/">Home</a> <span>/</span> <a href="/bicycle-parts">Parts</a> <span>/</span> <a href="${categoryUrl(group)}">${originLabel}</a> <span>/</span> <span>${escHtml(catMeta.name)}</span>`
1177        : `<a href="/">Home</a> <span>/</span> <a href="/bicycle-parts">Parts</a> <span>/</span> <span>${originLabel}</span>`;
1178    }
1179    document.title = (catMeta ? catMeta.name + " — " : "") + originLabel + " | Eastman Bicycles";
1180
1181    if (side) {
1182      const catLinks = cats.length
1183        ? cats.map((c) => {
1184            const active = c.slug === catSlug ? " is-active" : "";
1185            return `<a class="parts-origin-cat${active}" href="${categoryUrl(group, c.slug)}"><span>${escHtml(c.name)}</span><span class="side-count">${c.count || 0}</span></a>`;
1186          }).join("")
1187        : `<p class="empty">No categories found.</p>`;
1188      const activeName = (catMeta && catMeta.name) || "Categories";
1189      side.innerHTML = `<button type="button" class="parts-origin-cats-toggle" id="partsCatsToggle" aria-expanded="false"><span>Categories</span><strong>${escHtml(activeName)}</strong><span class="parts-origin-cats-toggle__chev" aria-hidden="true">▾</span></button>
1190        <div class="parts-origin-cats-panel" id="partsCatsPanel">${catLinks}</div>
1191        <div class="parts-origin-pills" id="partsCatsPills" aria-label="Category shortcuts">${cats.slice(0, 12).map((c) => {
1192          const active = c.slug === catSlug ? " is-active" : "";
1193          return `<a class="parts-origin-pill${active}" href="${categoryUrl(group, c.slug)}">${escHtml(c.name)}</a>`;
1194        }).join("")}</div>`;
1195      if (window.fixSiteLinks) window.fixSiteLinks(side);
1196      const tog = $("#partsCatsToggle");
1197      const panel = $("#partsCatsPanel");
1198      if (tog && panel) {
1199        tog.addEventListener("click", () => {
1200          const open = panel.classList.toggle("is-open");
1201          tog.setAttribute("aria-expanded", open ? "true" : "false");
1202          tog.classList.toggle("is-open", open);
1203        });
1204      }
1205    }
1206
1207    const products = catSlug
1208      ? PRODUCTS.filter((p) => p.group === group && p.catSlug === catSlug)
1209      : [];
1210    products.sort((a, b) => productScore(b) - productScore(a) || (a.name || "").localeCompare(b.name || ""));
1211
1212    if (listTitle) listTitle.textContent = catMeta ? catMeta.name : "Products";
1213    if (countEl) countEl.textContent = `${products.length} product${products.length === 1 ? "" : "s"}`;
1214
1215    let page = Math.max(1, parseInt(params.get("page") || "1", 10));
1216    function showPage(p) {
1217      page = p;
1218      renderGrid(grid, paginate(products, page));
1219      renderPagination($("#catPagination"), products.length, page, (np) => {
1220        showPage(np);
1221        window.scrollTo({ top: (shop || grid).offsetTop - 100, behavior: "smooth" });
1222      });
1223    }
1224    if (grid) {
1225      if (!products.length) grid.innerHTML = `<p class="empty">No products in this category.</p>`;
1226      else showPage(page);
1227    }
1228
1229    const catIndex = $("#catIndex");
1230    if (catIndex) catIndex.hidden = true;
1231    const catProducts = $("#catProducts");
1232    if (catProducts) catProducts.hidden = true;
1233    return true;
1234  }
1235
1236  if ($("#partsOriginShop") || ($("#catIndex") && (resolveCategoryPath().group === "india" || resolveCategoryPath().group === "china" || document.body.dataset.page === "products"))) {
1237    try { renderPartsOriginShop(); } catch (err) { console.error("parts origin shop", err); }
1238  }
1239
1240  /* ================= shop layout (home + products page) ================= */
1241  const sideQuick = $("#sideQuickFilters");
1242  if (sideQuick) {
1243    const grid = $("#prodGrid");
1244    const titles = { all: "All Products", india: "Eastman India", china: "Eastman China", new: "New Arrivals", featured: "Featured", demand: "In High Demand" };
1245    let current = params.get("group") || "all";
1246    let currentCat = params.get("cat") ? { group: params.get("group") || "india", slug: params.get("cat") } : null;
1247    let query = (params.get("q") || "").toLowerCase();
1248    let page = Math.max(1, parseInt(params.get("page") || "1", 10));
1249    if (!titles[current] && !currentCat) current = "all";
1250    if (currentCat && !params.get("group")) current = currentCat.group;
1251
1252    const countFor = (g) => filterByGroup(g).length;
1253    $$("button", sideQuick).forEach((b) => {
1254      const g = b.dataset.group;
1255      const label = titles[g] || b.textContent.trim();
1256      const tagBadge = ["new", "featured", "demand"].includes(g) ? tagPills([g], 1) : "";
1257      b.innerHTML = `<span class="side-label">${label}</span><span class="side-meta">${tagBadge}<span class="side-count">${countFor(g)}</span></span>`;
1258    });
1259
1260    const browseList = $("#sideBrowseList");
1261    const sideRange = $("#sideRangeFilters");
1262    const mobileShopRoot = $("#mobileCatShop");
1263    let mobileShopRange = null;
1264    let mobileShopTrack = null;
1265    if (mobileShopRoot) {
1266      mobileShopRange = $(".mobile-cat-nav__range", mobileShopRoot);
1267      if (!mobileShopRange) {
1268        mobileShopRange = document.createElement("div");
1269        mobileShopRange.className = "mobile-cat-nav__range";
1270        mobileShopRange.innerHTML = `<button type="button" data-browse="india">Eastman India</button><button type="button" data-browse="china">Eastman China</button>`;
1271        mobileShopRoot.insertBefore(mobileShopRange, mobileShopRoot.firstChild);
1272      }
1273      mobileShopTrack = ensureMobileTrack(mobileShopRoot);
1274    }
1275
1276    let browseGroup = currentCat ? currentCat.group : (params.get("group") === "china" ? "china" : "india");
1277
1278    function syncMobileRange() {
1279      if (!mobileShopRange) return;
1280      $$("button", mobileShopRange).forEach((t) => {
1281        t.classList.toggle("is-active", t.dataset.browse === browseGroup);
1282      });
1283    }
1284
1285    function pickCategory(group, slug) {
1286      currentCat = { group, slug };
1287      current = group;
1288      browseGroup = group;
1289      query = "";
1290      page = 1;
1291      if (sideRange) $$("button", sideRange).forEach((t) => t.classList.toggle("is-active", t.dataset.browse === group));
1292      syncMobileRange();
1293      renderBrowse(group);
1294      apply();
1295      if (!isMobileShop()) scrollToProducts();
1296      else scrollMobilePill(mobileShopTrack, slug);
1297    }
1298
1299    function switchBrowseGroup(group) {
1300      browseGroup = group;
1301      current = group;
1302      query = "";
1303      page = 1;
1304      if (isMobileShop()) {
1305        const cats = catsForGroup(group);
1306        currentCat = cats.length ? { group, slug: cats[0].slug } : null;
1307      } else {
1308        currentCat = null;
1309      }
1310      if (sideRange) $$("button", sideRange).forEach((t) => t.classList.toggle("is-active", t.dataset.browse === group));
1311      syncMobileRange();
1312      renderBrowse(group);
1313      apply();
1314      if (!isMobileShop()) scrollToProducts();
1315    }
1316
1317    function renderBrowse(group) {
1318      if (!browseList) return;
1319      browseGroup = group;
1320      const list = catsForGroup(group);
1321      browseList.innerHTML = list.map((c) =>
1322        `<button type="button" class="side-browse-item${currentCat && currentCat.group === c.group && currentCat.slug === c.slug ? " is-active" : ""}" data-cat-group="${c.group}" data-cat-slug="${c.slug}"><span class="side-browse-name">${c.name}</span><span class="side-browse-tags"><span class="side-count">${c.count}</span></span></button>`
1323      ).join("");
1324      $$(".side-browse-item", browseList).forEach((btn) => btn.addEventListener("click", () => {
1325        pickCategory(btn.dataset.catGroup, btn.dataset.catSlug);
1326      }));
1327
1328      if (mobileShopTrack) {
1329        mobileShopTrack.innerHTML = list.map((c) => mobileCatPill(c, currentCat && currentCat.group === c.group && currentCat.slug === c.slug)).join("");
1330        bindMobileCatPills(mobileShopTrack, (g, slug) => pickCategory(g, slug));
1331        if (currentCat) scrollMobilePill(mobileShopTrack, currentCat.slug);
1332      }
1333      syncMobileRange();
1334    }
1335
1336    const browseLabels = { india: "Eastman India", china: "Eastman China" };
1337    if (sideRange) {
1338      $$("button", sideRange).forEach((tab) => {
1339        const g = tab.dataset.browse;
1340        tab.innerHTML = `<span class="side-label">${browseLabels[g]}</span><span class="side-count">${countFor(g)}</span>`;
1341        tab.addEventListener("click", () => switchBrowseGroup(tab.dataset.browse));
1342      });
1343      const activeRange = ["india", "china"].includes(current) ? current : browseGroup;
1344      $$("button", sideRange).forEach((t) => t.classList.toggle("is-active", t.dataset.browse === activeRange));
1345      renderBrowse(browseGroup);
1346    }
1347
1348    if (mobileShopRange) {
1349      $$("button", mobileShopRange).forEach((tab) => {
1350        tab.addEventListener("click", () => switchBrowseGroup(tab.dataset.browse));
1351      });
1352    }
1353
1354    function filteredList() {
1355      let list = PRODUCTS;
1356      if (currentCat) {
1357        list = list.filter((p) => p.group === currentCat.group && p.catSlug === currentCat.slug);
1358      } else {
1359        list = filterByGroup(current);
1360      }
1361      if (query) list = list.filter((p) => (p.name + " " + p.sku + " " + p.catName + " " + p.desc).toLowerCase().includes(query));
1362      return list;
1363    }
1364
1365    function listTitle() {
1366      if (query) return `Results for “${query}”`;
1367      if (currentCat) {
1368        const meta = CATEGORIES.find((c) => c.group === currentCat.group && c.slug === currentCat.slug);
1369        return meta ? meta.name : currentCat.slug;
1370      }
1371      return titles[current] || "All Products";
1372    }
1373
1374    function apply() {
1375      const list = filteredList();
1376      const showAll = !!isHomeShop;
1377      const pagEl = $("#prodPagination");
1378      let visible;
1379      if (showAll) {
1380        visible = list.slice(0, HOME_RENDER_CAP);
1381        if (pagEl) pagEl.innerHTML = "";
1382      } else if (isMobileShop()) {
1383        visible = list.slice(0, MOBILE_PRODUCT_CAP);
1384        if (pagEl) pagEl.innerHTML = "";
1385      } else {
1386        visible = paginate(list, page);
1387        renderPagination(pagEl, list.length, page, (np) => { page = np; apply(); window.scrollTo({ top: grid.offsetTop - 100, behavior: "smooth" }); });
1388      }
1389      renderGrid(grid, visible);
1390      const scrollWrap = $("#prodGridScroll");
1391      if (scrollWrap) scrollWrap.scrollTop = 0;
1392      const t = listTitle();
1393      const set = (id, val) => { const el = $("#" + id); if (el) el.textContent = val; };
1394      set("listTitle", t);
1395      set("pageTitle", t);
1396      set("crumbCat", t);
1397      let countText;
1398      if (isMobileShop() && !showAll) {
1399        const shown = Math.min(list.length, MOBILE_PRODUCT_CAP);
1400        countText = currentCat ? `${t} · showing ${shown} of ${list.length}` : `${shown} of ${list.length} products`;
1401      } else {
1402        countText = list.length + " product" + (list.length === 1 ? "" : "s") + (query ? " found" : " available");
1403        if (showAll && list.length > HOME_RENDER_CAP) countText += ` · scroll to browse ${HOME_RENDER_CAP} shown`;
1404      }
1405      set("resultCount", countText);
1406      $$("button", sideQuick).forEach((b) => b.classList.toggle("is-active", !currentCat && ["all", "new", "featured", "demand"].includes(current) && b.dataset.group === current));
1407      if (sideRange) $$("button", sideRange).forEach((t) => {
1408        t.classList.toggle("is-active", !["all", "new", "featured", "demand"].includes(current) && (current === t.dataset.browse || (currentCat && currentCat.group === t.dataset.browse)));
1409      });
1410      if (browseList) $$(".side-browse-item", browseList).forEach((btn) => {
1411        btn.classList.toggle("is-active", currentCat && btn.dataset.catGroup === currentCat.group && btn.dataset.catSlug === currentCat.slug);
1412      });
1413      if (mobileShopRoot) mobileShopRoot.hidden = !isMobileShop();
1414      const moreShop = $("#prodMoreShop");
1415      if (moreShop) {
1416        if (currentCat) {
1417          const meta = CATEGORIES.find((c) => c.group === currentCat.group && c.slug === currentCat.slug);
1418          const total = PRODUCTS.filter((p) => p.group === currentCat.group && p.catSlug === currentCat.slug).length;
1419          const name = meta ? meta.name : currentCat.slug;
1420          const rangeLabels = { india: "Indian", china: "Chinese" };
1421          moreShop.innerHTML =
1422            categoryAllLink(currentCat.group, currentCat.slug, name, total) +
1423            `<div class="prod-more--secondary"><a href="${categoryUrl(currentCat.group)}" class="btn btn-outline">All ${rangeLabels[currentCat.group]} categories &rarr;</a></div>`;
1424          moreShop.hidden = false;
1425        } else {
1426          moreShop.hidden = true;
1427          moreShop.innerHTML = "";
1428        }
1429      }
1430    }
1431
1432    function bootProductsMobile() {
1433      if (!isMobileShop() || currentCat) return;
1434      if (["all", "new", "featured", "demand"].includes(current)) {
1435        const group = browseGroup || "india";
1436        current = group;
1437        browseGroup = group;
1438        const cats = catsForGroup(group);
1439        if (cats.length) currentCat = { group, slug: cats[0].slug };
1440      } else if (["india", "china"].includes(current)) {
1441        const cats = catsForGroup(current);
1442        if (cats.length) currentCat = { group: current, slug: cats[0].slug };
1443      }
1444    }
1445
1446    bootProductsMobile();
1447    if (sideRange) renderBrowse(browseGroup);
1448    $$("button", sideQuick).forEach((b) => b.addEventListener("click", () => {
1449      current = b.dataset.group;
1450      currentCat = null;
1451      query = "";
1452      page = 1;
1453      apply();
1454      scrollToProducts();
1455    }));
1456    apply();
1457    MOBILE_MQ.addEventListener("change", () => {
1458      bootProductsMobile();
1459      renderBrowse(browseGroup);
1460      apply();
1461    });
1462  }
1463
1464  /* ================= Complete Bicycles India (tabbed catalogue) ================= */
1465
1466  /* ================= Complete Bicycles adventure carousel (homepage) ================= */
1467  function initBikesAdventure() {
1468    const track = $("#bikesAdvTrack");
1469    if (!track) return;
1470
1471    const CATS = [
1472      {
1473        id: "mtb",
1474        title: "MTB & Hybrid",
1475        sub: "Highlander · Brooklyn",
1476        slug: "highlander-29-ms",
1477        href: "/bicycles?tab=mtb",
1478      },
1479      {
1480        id: "city",
1481        title: "City Cycles",
1482        sub: "Cornado · Dinamic",
1483        slug: "cornado-26",
1484        href: "/bicycles?tab=city26",
1485      },
1486      {
1487        id: "fat",
1488        title: "Fat Bike",
1489        sub: "Tesler Fat Bike",
1490        slug: "tesler-26-ms",
1491        href: "/bicycles?tab=city26",
1492      },
1493      {
1494        id: "kids",
1495        title: "Kids Cycles",
1496        sub: "Cloudia · Jessica · Hotride",
1497        slug: "cloudia-1620",
1498        href: "/bicycles?tab=kids",
1499      },
1500      {
1501        id: "compact",
1502        title: "Compact 24\"",
1503        sub: "Cameron · Cornado 24",
1504        slug: "cameron-24",
1505        href: "/bicycles?tab=compact24",
1506      },
1507      {
1508        id: "trending",
1509        title: "Trending Models",
1510        sub: "U Bike · Dynamite",
1511        slug: "u-bike-20",
1512        href: "/bicycles?tab=trending",
1513      },
1514    ];
1515
1516    const bySlug = new Map((window.getAllIndiaBikes ? window.getAllIndiaBikes() : []).map((b) => [b.slug, b]));
1517
1518    track.innerHTML = CATS.map((c) => {
1519      const bike = bySlug.get(c.slug);
1520      const img = bike
1521        ? imgTag({ name: bike.name, img: bike.img }, "", { eager: false })
1522        : `<img src="assets/bikes-india/${c.slug}.jpg" alt="${escAttr(c.title)}" loading="lazy" />`;
1523      return `<a class="bikes-adv-card trend-card trend-card--view" href="${c.href}">
1524        <span class="bikes-adv-card__media">${img}</span>
1525        <span class="bikes-adv-card__shade" aria-hidden="true"></span>
1526        <span class="bikes-adv-card__body">
1527          <span>
1528            <span class="bikes-adv-card__title">${escHtml(c.title)}</span>
1529            <span class="bikes-adv-card__sub">${escHtml(c.sub)}</span>
1530          </span>
1531          <span class="bikes-adv-card__cta">View Details</span>
1532        </span>
1533      </a>`;
1534    }).join("");
1535
1536    if (window.fixSiteLinks) window.fixSiteLinks(track);
1537
1538    const step = () => {
1539      const card = track.querySelector(".bikes-adv-card");
1540      if (!card) return Math.floor(track.clientWidth * 0.22);
1541      return Math.round(card.getBoundingClientRect().width + 18);
1542    };
1543    const prev = $("#bikesAdvPrev");
1544    const next = $("#bikesAdvNext");
1545    if (prev) prev.addEventListener("click", () => track.scrollBy({ left: -step(), behavior: "smooth" }));
1546    if (next) next.addEventListener("click", () => track.scrollBy({ left: step(), behavior: "smooth" }));
1547  }
1548
1549  function initIndiaBikesTabs() {
1550    /* Homepage uses adventure carousel instead of tabbed grid */
1551    if ($("#bikesAdvTrack")) return;
1552    const tabsEl = $("#indiaBikesTabs");
1553    const panelsEl = $("#indiaBikesPanels");
1554    const data = window.EASTMAN_INDIA_BIKES;
1555    if (!tabsEl || !panelsEl || !data || !data.tabs || !data.tabs.length) return;
1556
1557    const titleEl = $("#indiaBikesTitle");
1558    const subEl = $("#indiaBikesSubtitle");
1559    if (titleEl && data.title) titleEl.textContent = data.title;
1560    if (subEl && data.subtitle) subEl.textContent = data.subtitle;
1561
1562    let activeId = data.tabs[0].id;
1563    const tabParam = params.get("tab");
1564    if (tabParam && data.tabs.some((t) => t.id === tabParam)) activeId = tabParam;
1565    const isFullPage = document.body.dataset.page === "bicycles" || document.body.dataset.page === "bikes-india";
1566    const scrollClass = isFullPage ? "bikes-india-scroll bikes-india-scroll--full" : "prod-grid-scroll bikes-india-scroll";
1567
1568    function bikeCard(item) {
1569      const catLabel = item.tag || item.category || "Complete Bicycle";
1570      const detailUrl = bikeUrl(item.slug);
1571      return `<a class="bikes-adv-card trend-card product-adv-card bike-pcard trend-card--view" href="${detailUrl}">
1572        <span class="bikes-adv-card__media"><img src="${escAttr(assetUrl(item.img))}" alt="${escAttr(item.name)}" loading="lazy" width="720" height="540" /></span>
1573        <span class="bikes-adv-card__shade" aria-hidden="true"></span>
1574        <span class="bikes-adv-card__body">
1575          <span>
1576            <span class="bikes-adv-card__title">${escHtml(item.name)}</span>
1577            <span class="bikes-adv-card__sub">${escHtml(catLabel)}</span>
1578          </span>
1579          <span class="bikes-adv-card__cta">View Details</span>
1580        </span>
1581      </a>`;
1582    }
1583
1584    function renderPanels() {
1585      panelsEl.innerHTML = data.tabs
1586        .map(
1587          (tab) =>
1588            `<div class="bikes-india-panel${tab.id === activeId ? " is-active" : ""}" id="bikes-panel-${tab.id}" role="tabpanel" aria-labelledby="bikes-tab-${tab.id}">
1589          <div class="${scrollClass}"${isFullPage ? "" : ' tabindex="0"'}>
1590            <div class="prod-grid bikes-india-grid bikes-india-grid--row">${tab.items.map(bikeCard).join("")}</div>
1591          </div>
1592        </div>`
1593        )
1594        .join("");
1595      $$(".bikes-adv-card, .bike-pcard", panelsEl).forEach(reveal);
1596    }
1597
1598    function setTab(id) {
1599      activeId = id;
1600      $$(".bikes-india-tab", tabsEl).forEach((btn) => {
1601        const on = btn.dataset.tabId === id;
1602        btn.classList.toggle("is-active", on);
1603        btn.setAttribute("aria-selected", on ? "true" : "false");
1604      });
1605      renderPanels();
1606    }
1607
1608    tabsEl.innerHTML = data.tabs
1609      .map(
1610        (tab, i) =>
1611          `<button type="button" class="bikes-india-tab${i === 0 ? " is-active" : ""}" id="bikes-tab-${tab.id}" role="tab" data-tab-id="${tab.id}" aria-selected="${i === 0 ? "true" : "false"}" aria-controls="bikes-panel-${tab.id}">${tab.label}</button>`
1612      )
1613      .join("");
1614
1615    renderPanels();
1616    $$(".bikes-india-tab", tabsEl).forEach((btn) => {
1617      btn.addEventListener("click", () => setTab(btn.dataset.tabId));
1618    });
1619  }
1620
1621  /* ================= Trending products (homepage) ================= */
1622  function trendingBikeCard(b) {
1623    const href = bikeUrl(b.slug);
1624    return `<a class="bikes-adv-card trend-card trend-card--view" href="${href}">
1625      <span class="bikes-adv-card__media">${imgTag({ name: b.name, img: b.img }, "", { eager: false })}</span>
1626      <span class="bikes-adv-card__shade" aria-hidden="true"></span>
1627      <span class="bikes-adv-card__body">
1628        <span>
1629          <span class="bikes-adv-card__title">${escHtml(b.name)}</span>
1630          <span class="bikes-adv-card__sub">Trending</span>
1631        </span>
1632        <span class="bikes-adv-card__cta">View Details</span>
1633      </span>
1634    </a>`;
1635  }
1636
1637  function initTrendingHome() {
1638    const track = $("#prodGridTrending");
1639    if (!track) return;
1640
1641    const TRENDING_BIKE_SLUGS = [
1642      "cloudia-1620",
1643      "hotride-1620",
1644      "jessica-20",
1645      "highlander-29-ms",
1646      "cornado-26",
1647      "brooklyn-29-ms",
1648      "dinamic-26-ms",
1649      "dynamite-20",
1650      "tesler-26-ms",
1651      "u-bike-20",
1652    ];
1653    const bySlug = new Map((window.getAllIndiaBikes ? window.getAllIndiaBikes() : []).map((b) => [b.slug, b]));
1654    const bikes = TRENDING_BIKE_SLUGS.map((s) => bySlug.get(s)).filter(Boolean);
1655
1656    if (bikes.length) {
1657      track.innerHTML = bikes.map(trendingBikeCard).join("");
1658      /* no reveal — horizontal track cards stay opacity 0 if IO/width miss */
1659      /* trending count line removed */
1660      const moreEl = $("#prodMoreTrending");
1661      if (moreEl) {
1662        moreEl.innerHTML = `<a href="/bicycles" class="btn btn-outline">Browse complete bicycle range →</a>`;
1663      }
1664    } else {
1665      const list = trendingProducts(12);
1666      track.innerHTML = list.map(productCard).join("");
1667      /* carousel: skip reveal so cards never stick at opacity 0 */
1668    }
1669
1670    const step = () => {
1671      const card = track.querySelector(".bikes-adv-card, .trend-card, .pcard");
1672      if (!card) return Math.floor(track.clientWidth * 0.22);
1673      const gap = 18;
1674      return Math.round(card.getBoundingClientRect().width + gap);
1675    };
1676    const prev = $("#trendingPrev");
1677    const next = $("#trendingNext");
1678    if (prev) prev.addEventListener("click", () => track.scrollBy({ left: -step(), behavior: "smooth" }));
1679    if (next) next.addEventListener("click", () => track.scrollBy({ left: step(), behavior: "smooth" }));
1680  }
1681
1682  /* ================= Bicycle Parts — separate India + China homepage sections ================= */
1683  function initPartsShop() {
1684    function catImgForGroup(cat, group) {
1685      const probe =
1686        PRODUCTS.find((p) => p.group === group && p.catSlug === cat.slug && (p.img || p.imgRemote)) ||
1687        (cat.img || cat.imgRemote ? cat : null);
1688      if (!probe) return "";
1689      return imgTag(
1690        { name: cat.name, img: probe.img, imgRemote: probe.imgRemote },
1691        "",
1692        { eager: false }
1693      );
1694    }
1695
1696    function catCardHTML(c, group) {
1697      const count = c.count || 0;
1698      const href = categoryUrl(group, c.slug);
1699      return `<a class="bikes-adv-card trend-card product-adv-card parts-adv-card" href="${href}" data-group="${escAttr(group)}" data-cat="${escAttr(c.slug)}">
1700            <span class="bikes-adv-card__media">${catImgForGroup(c, group)}</span>
1701            <span class="bikes-adv-card__shade" aria-hidden="true"></span>
1702            <span class="bikes-adv-card__body">
1703              <span>
1704                <span class="bikes-adv-card__title">${escHtml(c.name)}</span>
1705                <span class="bikes-adv-card__sub">${count} products</span>
1706              </span>
1707              <span class="bikes-adv-card__go" aria-hidden="true">→</span>
1708            </span>
1709          </a>`;
1710    }
1711
1712    function bindPartsCarousel(trackId, prevId, nextId) {
1713      const track = $(trackId);
1714      if (!track) return;
1715      const step = () => Math.min(320, Math.floor(track.clientWidth * 0.85));
1716      const prev = $(prevId);
1717      const next = $(nextId);
1718      if (prev) prev.addEventListener("click", () => track.scrollBy({ left: -step(), behavior: "smooth" }));
1719      if (next) next.addEventListener("click", () => track.scrollBy({ left: step(), behavior: "smooth" }));
1720    }
1721
1722    function renderPartsGrid(gridId, group, opts) {
1723      const grid = $(gridId);
1724      if (!grid) return;
1725      opts = opts || {};
1726      let cats = catsForPartsGroup(group);
1727      if (opts.popularFirst && opts.popularFirst.length) {
1728        const byKey = new Map();
1729        CATEGORIES.forEach((c) => byKey.set(c.group + "::" + c.slug, c));
1730        const pinned = [];
1731        const seen = new Set();
1732        opts.popularFirst.forEach((item) => {
1733          const g = item.group || group;
1734          const key = g + "::" + item.slug;
1735          const cat = byKey.get(key);
1736          if (!cat || seen.has(key)) return;
1737          seen.add(key);
1738          pinned.push({ cat, group: g });
1739        });
1740        const rest = cats
1741          .filter((c) => !seen.has(group + "::" + c.slug))
1742          .map((c) => ({ cat: c, group: group }));
1743        grid.innerHTML = pinned.concat(rest).map((x) => catCardHTML(x.cat, x.group)).join("");
1744      } else {
1745        grid.innerHTML = cats.map((c) => catCardHTML(c, group)).join("");
1746      }
1747      if (window.fixSiteLinks) window.fixSiteLinks(grid);
1748    }
1749
1750    renderPartsGrid("#partsCatGridIndia", "india");
1751    renderPartsGrid("#partsCatGridChina", "china");
1752  }
1753
1754  try { initBikesAdventure(); } catch (err) { console.error("bikes adventure", err); }
1755  try { initIndiaBikesTabs(); } catch (err) { console.error("bikes tabs", err); }
1756
1757
1758  function bikeShopCategories() {
1759    const data = window.EASTMAN_INDIA_BIKES || {};
1760    const tabs = (data.tabs || []).filter((t) => t && t.id && t.id !== "trending");
1761    const all = window.getAllIndiaBikes ? window.getAllIndiaBikes() : [];
1762    const uniq = new Map();
1763    all.forEach((b) => { if (b && b.slug && !uniq.has(b.slug)) uniq.set(b.slug, b); });
1764    return {
1765      all: Array.from(uniq.values()),
1766      tabs: tabs.length
1767        ? tabs
1768        : [
1769            { id: "mtb", label: "Mountain / MTB", items: all.filter((b) => /mountain|mtb|hybrid/i.test(`${b.category || ""} ${b.tag || ""} ${b.name || ""}`)) },
1770            { id: "city", label: "City", items: all.filter((b) => /city|road|urban/i.test(`${b.category || ""} ${b.tag || ""} ${b.name || ""}`)) },
1771            { id: "kids", label: "Kids", items: all.filter((b) => /kid|junior|balance|child/i.test(`${b.category || ""} ${b.tag || ""} ${b.name || ""}`)) },
1772            { id: "bmx", label: "BMX", items: all.filter((b) => /bmx|dynamite|impulse|shoorveer/i.test(`${b.category || ""} ${b.tag || ""} ${b.name || ""}`)) },
1773          ],
1774    };
1775  }
1776
1777  function bikeCardHTML(b) {
1778    const url = bikeUrl(b.slug);
1779    const sub = b.tag || b.category || "Bicycle";
1780    return `<a class="bikes-adv-card trend-card product-adv-card bike-pcard trend-card--view" href="${url}">
1781      <span class="bikes-adv-card__media"><img src="${escAttr(assetUrl(b.img))}" alt="${escAttr(b.name)}" loading="lazy" width="720" height="540" /></span>
1782      <span class="bikes-adv-card__shade" aria-hidden="true"></span>
1783      <span class="bikes-adv-card__body">
1784        <span>
1785          <span class="bikes-adv-card__title">${escHtml(b.name)}</span>
1786          <span class="bikes-adv-card__sub">${escHtml(sub)}</span>
1787        </span>
1788        <span class="bikes-adv-card__cta">View Details</span>
1789      </span>
1790    </a>`;
1791  }
1792
1793  function renderBikesOriginShop() {
1794    const shop = $("#bikesOriginShop");
1795    if (!shop) return false;
1796    const { all, tabs } = bikeShopCategories();
1797    const rawCat = (params.get("cat") || "").toLowerCase();
1798    let catId = rawCat;
1799    if (!catId && tabs.length) catId = tabs[0].id; // default first category on hub
1800    if (catId === "all") catId = "";
1801    const activeTab = catId ? tabs.find((t) => t.id === catId) : null;
1802    const products = activeTab
1803      ? (() => {
1804          const seen = new Set();
1805          const out = [];
1806          (activeTab.items || []).forEach((b) => {
1807            if (!b || !b.slug || seen.has(b.slug)) return;
1808            seen.add(b.slug);
1809            out.push(b);
1810          });
1811          return out;
1812        })()
1813      : all;
1814
1815    const titleEl = $("#bikesPageTitle");
1816    const descEl = $("#bikesPageDesc");
1817    const crumbEl = $("#bikesCrumbs");
1818    const listTitle = $("#bikesListTitle");
1819    const countEl = $("#bikesResultCount");
1820    const grid = $("#bikesProdGrid");
1821    const side = $("#bikesOriginCats");
1822
1823    if (titleEl) titleEl.textContent = "Bicycles";
1824    if (descEl) {
1825      descEl.textContent = activeTab
1826        ? `${activeTab.label} — Eastman India complete bicycles for B2B export.`
1827        : "Browse Eastman India complete bicycles for B2B export.";
1828    }
1829    if (crumbEl) {
1830      crumbEl.innerHTML = activeTab
1831        ? `<a href="/">Home</a> <span>/</span> <a href="/bicycles">Bicycles</a> <span>/</span> <span>${escHtml(activeTab.label)}</span>`
1832        : `<a href="/">Home</a> <span>/</span> <span>Bicycles</span>`;
1833    }
1834    document.title = (activeTab ? activeTab.label + " — " : "") + "Bicycles | Eastman Bicycles";
1835
1836    if (side) {
1837      const allCount = all.length;
1838      const rows = [`<a class="parts-origin-cat${activeTab ? "" : " is-active"}" href="/bicycles?cat=all"><span>All bikes</span><span class="side-count">${allCount}</span></a>`];
1839      tabs.forEach((t) => {
1840        const seen = new Set();
1841        (t.items || []).forEach((b) => { if (b && b.slug) seen.add(b.slug); });
1842        const active = t.id === catId ? " is-active" : "";
1843        rows.push(`<a class="parts-origin-cat${active}" href="/bicycles?cat=${encodeURIComponent(t.id)}"><span>${escHtml(t.label)}</span><span class="side-count">${seen.size}</span></a>`);
1844      });
1845      side.innerHTML = rows.join("");
1846      if (window.fixSiteLinks) window.fixSiteLinks(side);
1847    }
1848
1849    if (listTitle) listTitle.textContent = activeTab ? activeTab.label : "All bikes";
1850    if (countEl) countEl.textContent = `${products.length} model${products.length === 1 ? "" : "s"}`;
1851    if (grid) {
1852      grid.innerHTML = products.length
1853        ? products.map(bikeCardHTML).join("")
1854        : `<p class="empty">No bicycles in this category.</p>`;
1855      $$(".bikes-adv-card, .product-adv-card", grid).forEach(reveal);
1856      if (window.fixSiteLinks) window.fixSiteLinks(grid);
1857    }
1858    return true;
1859  }
1860
1861
1862  /* ================= Clean hub pages: image category cards (no text landing cards) ================= */
1863  function initHubCategoryPages() {
1864    const page = document.body.getAttribute("data-page") || "";
1865    const grid = $("#hubCatGrid");
1866    if (!grid) return;
1867
1868    if (page === "bicycle-parts") {
1869      const seen = new Set();
1870      const list = [];
1871      const pushCat = (c, group) => {
1872        if (!c || !c.slug || seen.has(c.slug)) return;
1873        seen.add(c.slug);
1874        list.push(Object.assign({}, c, { group: group || c.group || "india" }));
1875      };
1876      (typeof catsForPartsGroup === "function" ? catsForPartsGroup("india") : []).forEach((c) => pushCat(c, "india"));
1877      (typeof catsForPartsGroup === "function" ? catsForPartsGroup("china") : []).forEach((c) => pushCat(c, "china"));
1878
1879      const grouped = groupCatsByFunction(list);
1880      const MAX_PER = 8;
1881      let html = "";
1882      grouped.forEach((bucket) => {
1883        const show = bucket.cats.slice(0, MAX_PER);
1884        const rest = bucket.cats.length - show.length;
1885        html += `<div class="parts-group">
1886          <h2 class="parts-group__title">${escHtml(bucket.name)}</h2>
1887          <div class="prod-grid hub-adv-grid parts-group__grid">`;
1888        show.forEach((c) => {
1889          const href = categoryUrl(c.group, c.slug);
1890          const probe =
1891            (window.EASTMAN_PRODUCTS || []).find((p) => p.catSlug === c.slug && (p.img || p.imgRemote)) ||
1892            (c.img || c.imgRemote ? c : null);
1893          const media = probe
1894            ? imgTag({ name: c.name, img: probe.img || probe.imgRemote, imgRemote: probe.imgRemote }, "", { eager: false })
1895            : "";
1896          html += `<a class="bikes-adv-card trend-card product-adv-card hub-adv-card" href="${href}">
1897            <span class="bikes-adv-card__media">${media}</span>
1898            <span class="bikes-adv-card__shade" aria-hidden="true"></span>
1899            <span class="bikes-adv-card__body">
1900              <span>
1901                <span class="bikes-adv-card__title">${escHtml(c.name)}</span>
1902                <span class="bikes-adv-card__sub">${c.group === "china" ? "China" : "India"} · Parts</span>
1903              </span>
1904              <span class="bikes-adv-card__go" aria-hidden="true">→</span>
1905            </span>
1906          </a>`;
1907        });
1908        html += `</div>`;
1909        if (rest > 0) {
1910          html += `<p class="parts-group__more"><a href="/bicycle-parts/india">View all in ${escHtml(bucket.name)} (+${rest}) →</a></p>`;
1911        }
1912        html += `</div>`;
1913      });
1914      grid.classList.add("hub-grouped");
1915      grid.innerHTML = html || `<p class="empty">No categories found.</p>`;
1916      if (window.fixSiteLinks) window.fixSiteLinks(grid);
1917      return;
1918    }
1919
1920    if (page === "bicycles") {
1921      const bikes = window.getAllIndiaBikes ? window.getAllIndiaBikes() : [];
1922      const cats = [
1923        { title: "Mountain bikes", href: "/mountain-bikes", match: /mountain|mtb|hybrid/i, fallback: ["cornado-26", "highlander-29-ms"] },
1924        { title: "City bicycles", href: "/city-bicycles", match: /city|road|urban/i, fallback: ["brooklyn-29-ms"] },
1925        { title: "Kids bicycles", href: "/kids-bicycles", match: /kid|junior|balance|child/i, fallback: ["cloudia-1620", "hotride-1620", "jessica-20"] },
1926        { title: "BMX-style", href: "/bmx-bicycles", match: /bmx|dynamite|impulse|shoorveer/i, fallback: ["dynamite-20"] },
1927      ];
1928      const bySlug = new Map(bikes.map((b) => [b.slug, b]));
1929      grid.innerHTML = cats.map((c) => {
1930        let sample = bikes.find((b) => c.match.test(`${b.name} ${b.category || ""} ${b.tag || ""}`));
1931        if (!sample) {
1932          for (const s of c.fallback) { if (bySlug.get(s)) { sample = bySlug.get(s); break; } }
1933        }
1934        if (!sample) sample = bikes[0];
1935        const media = sample
1936          ? imgTag({ name: sample.name, img: sample.img || sample.imgRemote, imgRemote: sample.imgRemote }, "", { eager: false })
1937          : "";
1938        return `<a class="bikes-adv-card trend-card product-adv-card hub-adv-card" href="${c.href}">
1939          <span class="bikes-adv-card__media">${media}</span>
1940          <span class="bikes-adv-card__shade" aria-hidden="true"></span>
1941          <span class="bikes-adv-card__body">
1942            <span>
1943              <span class="bikes-adv-card__title">${escHtml(c.title)}</span>
1944              <span class="bikes-adv-card__sub">Complete bicycles</span>
1945            </span>
1946            <span class="bikes-adv-card__go" aria-hidden="true">→</span>
1947          </span>
1948        </a>`;
1949      }).join("");
1950      if (window.fixSiteLinks) window.fixSiteLinks(grid);
1951    }
1952  }
1953
1954
1955  try { initTrendingHome(); } catch (err) { console.error("trending", err); }
1956  try { if (!renderBikesOriginShop()) initHubCategoryPages(); } catch (err) { console.error("hub pages", err); }
1957  try { initPartsShop(); } catch (err) { console.error("parts shop", err); }
1958
1959  /* ================= Homepage blog (WP REST) ================= */
1960  function stripHtml(html) {
1961    const d = document.createElement("div");
1962    d.innerHTML = html || "";
1963    return (d.textContent || d.innerText || "").replace(/\s+/g, " ").trim();
1964  }
1965
1966  function blogCardFromPost(p) {
1967    const title = stripHtml(p.title && p.title.rendered ? p.title.rendered : "Article");
1968    const excerpt = stripHtml(p.excerpt && p.excerpt.rendered ? p.excerpt.rendered : "").slice(0, 140);
1969    const link = p.link || "/blog/";
1970    let img = "";
1971    try {
1972      img = p._embedded && p._embedded["wp:featuredmedia"] && p._embedded["wp:featuredmedia"][0]
1973        ? p._embedded["wp:featuredmedia"][0].source_url
1974        : "";
1975    } catch (_) { img = ""; }
1976    if (!img) img = "assets/infra/infra-2.webp";
1977    const excerptHtml = excerpt ? `<p class="blog-card-excerpt">${escHtml(excerpt)}${excerpt.length >= 140 ? "…" : ""}</p>` : "";
1978    return `<article class="blog-card blog-card--slide">
1979      <a class="blog-card-img" href="${escAttr(link)}"><img src="${escAttr(img)}" alt="" loading="lazy" onerror="this.src='assets/infra/infra-2.webp'" /></a>
1980      <div class="blog-card-body">
1981        <span class="blog-tag">Blog</span>
1982        <h3><a href="${escAttr(link)}">${escHtml(title)}</a></h3>
1983        ${excerptHtml}
1984        <a href="${escAttr(link)}" class="blog-link">Read article →</a>
1985      </div>
1986    </article>`;
1987  }
1988
1989  function initHomeBlog() {
1990    const track = $("#homeBlogGrid");
1991    if (!track) return;
1992    const api = "https://eastmanbicycles.com/wp-json/wp/v2/posts?per_page=8&_embed=1";
1993    const showEmpty = () => {
1994      track.innerHTML = `<div class="blog-empty"><p>Could not load posts right now.</p><a class="btn btn-outline" href="/blog/">Visit the blog →</a></div>`;
1995    };
1996    fetch(api)
1997      .then((r) => (r.ok ? r.json() : Promise.reject(r.status)))
1998      .then((posts) => {
1999        if (!Array.isArray(posts) || !posts.length) { showEmpty(); return; }
2000        track.innerHTML = posts.map(blogCardFromPost).join("");
2001        const step = () => {
2002          const card = track.querySelector(".blog-card");
2003          if (!card) return Math.floor(track.clientWidth * 0.34);
2004          return Math.round(card.getBoundingClientRect().width + 18);
2005        };
2006        const prev = $("#blogPrev");
2007        const next = $("#blogNext");
2008        if (prev) prev.addEventListener("click", () => track.scrollBy({ left: -step(), behavior: "smooth" }));
2009        if (next) next.addEventListener("click", () => track.scrollBy({ left: step(), behavior: "smooth" }));
2010      })
2011      .catch(() => showEmpty());
2012  }
2013
2014  function initInfraGallery() {
2015    const track = $("#infraTrack");
2016    if (!track) return;
2017    const step = () => {
2018      const tile = track.querySelector(".infra-tile");
2019      if (!tile) return Math.floor(track.clientWidth * 0.4);
2020      return Math.round(tile.getBoundingClientRect().width + 16);
2021    };
2022    const prev = $("#infraPrev");
2023    const next = $("#infraNext");
2024    if (prev) prev.addEventListener("click", () => track.scrollBy({ left: -step(), behavior: "smooth" }));
2025    if (next) next.addEventListener("click", () => track.scrollBy({ left: step(), behavior: "smooth" }));
2026  }
2027
2028  try { initHomeBlog(); } catch (err) { console.error("home blog", err); }
2029  try { initInfraGallery(); } catch (err) { console.error("infra gallery", err); }
2030
2031
2032  /* ================= global export map + chips (homepage) ================= */
2033  const MAP_PIN_POS = {
2034    MX: [18, 42], BR: [32, 68], AR: [30, 82], CL: [26, 78], CO: [24, 52], PE: [24, 62],
2035    BO: [28, 66], PY: [30, 72], UY: [32, 80], EC: [22, 56], CR: [20, 48], PA: [22, 50],
2036    GT: [17, 44], HN: [18, 46], NI: [19, 48], SV: [17, 46], DO: [26, 40],
2037    DE: [50, 28], FR: [46, 30], IT: [51, 34], ES: [44, 34], NL: [48, 26], AT: [51, 30],
2038    BE: [47, 28], PT: [42, 36], PL: [53, 26], CZ: [51, 28], GB: [45, 24],
2039    SE: [52, 18], NO: [50, 16], DK: [49, 22], FI: [55, 16],
2040    MY: [78, 55], TH: [76, 48]
2041  };
2042
2043  function initGlobalMap() {
2044    const flagsEl = $("#globalFlags");
2045    const chipsEl = $("#globalRegionChips");
2046    const markets = window.EASTMAN_MARKETS;
2047    if (!markets || !markets.regions) return;
2048
2049    const preferred = ["Latin America", "Europe", "Asia"];
2050    const regions = preferred
2051      .map((name) => markets.regions.find((r) => (r.name || "").toLowerCase() === name.toLowerCase()))
2052      .filter(Boolean)
2053      .concat(markets.regions.filter((r) => !preferred.some((p) => p.toLowerCase() === (r.name || "").toLowerCase())));
2054
2055    const countries = regions.flatMap((r) =>
2056      (r.countries || []).map((c) => ({ ...c, region: c.region || r.name }))
2057    );
2058
2059    function renderFlags(list) {
2060      if (!flagsEl) return;
2061      flagsEl.innerHTML = list
2062        .map((c) => {
2063          const code = (c.code || "").toLowerCase();
2064          const img = code
2065            ? `<img class="country-flag-img" src="https://flagcdn.com/w40/${code}.png" alt="" width="18" height="12" loading="lazy" />`
2066            : `<span class="country-flag">${c.flag || ""}</span>`;
2067          return `<a href="${c.href}" class="market-pill">${img} ${escHtml(c.name)}</a>`;
2068        })
2069        .join("");
2070    }
2071
2072    if (chipsEl) {
2073      chipsEl.innerHTML = regions
2074        .map((r) => `<button type="button" class="region-chip" data-region="${escAttr(r.name)}">${escHtml(r.name)}</button>`)
2075        .join("");
2076      chipsEl.addEventListener("click", (e) => {
2077        const btn = e.target.closest(".region-chip");
2078        if (!btn) return;
2079        const region = btn.getAttribute("data-region");
2080        $$(".region-chip", chipsEl).forEach((b) => b.classList.toggle("is-active", b === btn));
2081        renderFlags(countries.filter((c) => (c.region || "") === region));
2082      });
2083      const first = chipsEl.querySelector(".region-chip");
2084      if (first) first.click();
2085    } else {
2086      renderFlags(countries);
2087    }
2088
2089    const cta = document.querySelector(".global-export-cta");
2090    if (cta && !cta.querySelector("[data-enquiry]")) {
2091      const btn = document.createElement("button");
2092      btn.type = "button";
2093      btn.className = "btn btn-outline btn-lg";
2094      btn.setAttribute("data-enquiry", "Global Export Markets");
2095      btn.textContent = "Request a Quote";
2096      cta.appendChild(btn);
2097    }
2098  }
2099
2100  function initExportFaqExpand() {
2101    const btn = $("#faqExpandBtn");
2102    const list = $("#exportFaqList") || document.querySelector("#export-faq .faq-list");
2103    if (!btn || !list) return;
2104    const collapsed = () => $$(".faq-item--collapsed", list);
2105    btn.addEventListener("click", () => {
2106      const open = btn.getAttribute("aria-expanded") === "true";
2107      const next = !open;
2108      collapsed().forEach((el) => {
2109        if (next) el.removeAttribute("hidden");
2110        else el.setAttribute("hidden", "");
2111      });
2112      btn.setAttribute("aria-expanded", next ? "true" : "false");
2113      btn.textContent = next ? "Show less" : "Show all FAQs";
2114    });
2115  }
2116
2117  function initTestimonialsCarousel() {
2118    const track = $("#testiTrack");
2119    if (!track) return;
2120    const prev = $("#testiPrev");
2121    const next = $("#testiNext");
2122    const step = () => {
2123      const card = track.querySelector(".testi-card");
2124      return card ? card.getBoundingClientRect().width + 18 : 320;
2125    };
2126    const scrollByDir = (dir) => track.scrollBy({ left: dir * step(), behavior: "smooth" });
2127    if (prev) prev.addEventListener("click", () => scrollByDir(-1));
2128    if (next) next.addEventListener("click", () => scrollByDir(1));
2129    /* h39: no aggressive autoplay — manual nav only */
2130    let timer = null;
2131    const stop = () => { if (timer) clearInterval(timer); timer = null; };
2132    stop();
2133  }
2134
2135  try { initGlobalMap(); } catch (err) { console.error("global markets", err); }
2136  try { initExportFaqExpand(); } catch (err) { console.error("faq expand", err); }
2137  try { initTestimonialsCarousel(); } catch (err) { console.error("testimonials", err); }
2138
2139  function bikeOverview(bike) {
2140    return (
2141      (bike.desc || bike.name) +
2142      " Part of Eastman India complete bicycle export range. Send an enquiry for FOB pricing, MOQ, colour options and container loading plans."
2143    );
2144  }
2145
2146  function bikePdpExtra(bike, related) {
2147    const faqs = [
2148      {
2149        q: `What is the MOQ for ${bike.name}?`,
2150        a: "Standard export MOQ is 1 × 20 ft container. Mixed models/products are allowed. Final MOQ depends on colourway, packing and destination.",
2151      },
2152      {
2153        q: `Can I mix ${bike.name} with other bicycles or parts?`,
2154        a: "Yes — mixed container programmes are common. Share your preferred mix and destination for loading guidance.",
2155      },
2156      {
2157        q: "Do you offer OEM / private label on complete bicycles?",
2158        a: "Yes. Branding, colourways and packing can be quoted for OEM/private-label buyers.",
2159      },
2160      {
2161        q: "Which Incoterms apply?",
2162        a: "FOB and CIF can be discussed based on order size and destination. Our export team confirms the best option.",
2163      },
2164      {
2165        q: "What export documents do you provide?",
2166        a: "Commercial invoice, packing list, bill of lading and certificate of origin where required.",
2167      },
2168    ]
2169      .map(
2170        (item) =>
2171          `<details class="pdp-faq-item"><summary>${escHtml(item.q)}</summary><p>${escHtml(item.a)}</p></details>`
2172      )
2173      .join("");
2174    return `
2175      <div class="pdp-sections pdp-sections--sourcing">
2176        ${pdpBlockCard("FAQ", "Product FAQ", "", `<div class="pdp-faq">${faqs}</div>`, true)}
2177      </div>`;
2178  }
2179
2180  function pdpShareHTML() {
2181    const icon = {
2182      facebook: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M14 8h3V4h-3c-2.2 0-4 1.8-4 4v2H7v4h3v8h4v-8h3.2L18 10h-4V8c0-.6.4-1 1-1z"/></svg>',
2183      linkedin: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M6.5 9.5H3.7V21h2.8V9.5zM5.1 3.5A1.7 1.7 0 1 0 5.1 6.9 1.7 1.7 0 0 0 5.1 3.5zM20.3 21h-2.8v-5.6c0-1.5-.5-2.5-1.9-2.5-1 0-1.6.7-1.9 1.4-.1.2-.1.6-.1.9V21h-2.8s0-9.4 0-10.4h2.8v1.5c.4-.6 1.1-1.7 2.8-1.7 2 0 3.6 1.3 3.6 4.2V21z"/></svg>',
2184      instagram: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M12 7.2A4.8 4.8 0 1 0 12 16.8 4.8 4.8 0 0 0 12 7.2zm0 7.9a3.1 3.1 0 1 1 0-6.2 3.1 3.1 0 0 1 0 6.2z"/><circle fill="currentColor" cx="17.5" cy="6.6" r="1.1"/><path fill="currentColor" d="M16.7 2H7.3A5.3 5.3 0 0 0 2 7.3v9.4A5.3 5.3 0 0 0 7.3 22h9.4A5.3 5.3 0 0 0 22 16.7V7.3A5.3 5.3 0 0 0 16.7 2zm3.5 14.7a3.5 3.5 0 0 1-3.5 3.5H7.3a3.5 3.5 0 0 1-3.5-3.5V7.3a3.5 3.5 0 0 1 3.5-3.5h9.4a3.5 3.5 0 0 1 3.5 3.5v9.4z"/></svg>',
2185      x: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M17.5 3h2.8l-6.1 7 7.2 11h-5.6l-4.4-6.5L6.1 21H3.3l6.5-7.5L3 3h5.8l4 5.9L17.5 3zm-1 16.2h1.6L7.6 4.7H6L16.5 19.2z"/></svg>',
2186      pinterest: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M12 2C6.5 2 2 6.5 2 12c0 4.1 2.5 7.6 6.1 9.1-.1-.8-.2-2 0-2.9.2-.8 1.3-5.4 1.3-5.4s-.3-.7-.3-1.6c0-1.5.9-2.7 2-2.7.9 0 1.4.7 1.4 1.5 0 .9-.6 2.3-.9 3.5-.3 1.1.5 1.9 1.6 1.9 1.9 0 3.2-2.4 3.2-5.3 0-2.2-1.5-3.8-4.2-3.8-3.1 0-5 2.3-5 4.8 0 .9.3 1.5.7 2 .1.1.1.2.1.3l-.3 1c0 .1-.2.2-.3.1-1.3-.6-2-2.1-2-3.8 0-2.8 2.4-6.2 7.1-6.2 3.8 0 6.3 2.7 6.3 5.7 0 3.9-2.2 6.8-5.4 6.8-1.1 0-2.1-.6-2.4-1.3l-.7 2.5c-.2.9-.9 2-1.3 2.7 1 .3 2 .5 3.1.5 5.5 0 10-4.5 10-10S17.5 2 12 2z"/></svg>',
2187      link: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M10.6 13.4a4 4 0 0 1 0-5.7l2.1-2.1a4 4 0 1 1 5.7 5.7l-1.1 1.1a.8.8 0 0 1-1.1-1.1l1.1-1.1a2.4 2.4 0 1 0-3.4-3.4l-2.1 2.1a2.4 2.4 0 0 0 0 3.4.8.8 0 0 1-1.2 1.1zm2.8-2.8a4 4 0 0 1 0 5.7l-2.1 2.1a4 4 0 1 1-5.7-5.7l1.1-1.1a.8.8 0 1 1 1.1 1.1l-1.1 1.1a2.4 2.4 0 1 0 3.4 3.4l2.1-2.1a2.4 2.4 0 0 0 0-3.4.8.8 0 0 1 1.2-1.1z"/></svg>',
2188    };
2189    return `<div class="pdp-share" data-pdp-share>
2190      <span class="pdp-share-label">Share Product</span>
2191      <div class="pdp-share-icons" role="group" aria-label="Share product">
2192        <button type="button" class="pdp-share-btn" data-share="facebook" aria-label="Share on Facebook" title="Facebook">${icon.facebook}</button>
2193        <button type="button" class="pdp-share-btn" data-share="linkedin" aria-label="Share on LinkedIn" title="LinkedIn">${icon.linkedin}</button>
2194        <button type="button" class="pdp-share-btn" data-share="instagram" aria-label="Copy link for Instagram" title="Instagram">${icon.instagram}</button>
2195        <button type="button" class="pdp-share-btn" data-share="x" aria-label="Share on X" title="X">${icon.x}</button>
2196        <button type="button" class="pdp-share-btn" data-share="pinterest" aria-label="Share on Pinterest" title="Pinterest">${icon.pinterest}</button>
2197        <button type="button" class="pdp-share-btn pdp-share-btn--copy" data-share="copy" aria-label="Copy product link" title="Copy link">${icon.link}<span class="pdp-share-copied" hidden>Copied</span></button>
2198      </div>
2199    </div>`;
2200  }
2201
2202  function bindPdpShare(root) {
2203    const wrap = root && $(".pdp-share", root);
2204    if (!wrap || wrap.dataset.bound === "1") return;
2205    wrap.dataset.bound = "1";
2206    const pageUrl = () => location.href.split("#")[0];
2207    const pageTitle = () => document.title || "Eastman Bicycles";
2208    const openShare = (url) => window.open(url, "_blank", "noopener,noreferrer,width=640,height=560");
2209
2210    async function copyLink(msg) {
2211      const url = pageUrl();
2212      try {
2213        if (navigator.clipboard && navigator.clipboard.writeText) await navigator.clipboard.writeText(url);
2214        else {
2215          const ta = document.createElement("textarea");
2216          ta.value = url;
2217          ta.setAttribute("readonly", "");
2218          ta.style.position = "fixed";
2219          ta.style.left = "-9999px";
2220          document.body.appendChild(ta);
2221          ta.select();
2222          document.execCommand("copy");
2223          document.body.removeChild(ta);
2224        }
2225      } catch (_) {}
2226      const tip = $(".pdp-share-copied", wrap);
2227      if (tip) {
2228        tip.textContent = msg || "Copied";
2229        tip.hidden = false;
2230        window.setTimeout(() => { tip.hidden = true; }, 1800);
2231      }
2232    }
2233
2234    wrap.addEventListener("click", (e) => {
2235      const btn = e.target.closest("[data-share]");
2236      if (!btn) return;
2237      e.preventDefault();
2238      const type = btn.getAttribute("data-share");
2239      const url = encodeURIComponent(pageUrl());
2240      const title = encodeURIComponent(pageTitle());
2241      if (type === "facebook") openShare("https://www.facebook.com/sharer/sharer.php?u=" + url);
2242      else if (type === "linkedin") openShare("https://www.linkedin.com/sharing/share-offsite/?url=" + url);
2243      else if (type === "x") openShare("https://twitter.com/intent/tweet?url=" + url + "&text=" + title);
2244      else if (type === "pinterest") openShare("https://pinterest.com/pin/create/button/?url=" + url + "&description=" + title);
2245      else if (type === "instagram") copyLink("Copied — paste in Instagram");
2246      else if (type === "copy") copyLink("Copied");
2247    });
2248  }
2249
2250  function renderBikePdp(bike) {
2251    const specPairs = Object.entries(bike.specs || {});
2252    const enquiry = "Complete Bicycle: " + bike.name;
2253    const galleryImgs = (bike.images || []).map((src) => ({ src, thumb: src }));
2254    if (!galleryImgs.length && bike.img) galleryImgs.push({ src: bike.img, thumb: bike.img });
2255    const img = pdpGalleryHTML({ name: bike.name, img: bike.img }, galleryImgs);
2256    const related = (window.getAllIndiaBikes ? window.getAllIndiaBikes() : [])
2257      .filter((b) => b.category === bike.category && b.slug !== bike.slug)
2258      .slice(0, 6);
2259    const together = (window.getAllIndiaBikes ? window.getAllIndiaBikes() : [])
2260      .filter((b) => b.slug !== bike.slug && b.category !== bike.category)
2261      .slice(0, 4);
2262    const catLabel = bike.category || bike.tag || "Complete Bicycles";
2263    const catHref = /mountain|mtb|hybrid/i.test(catLabel) ? "/mountain-bikes"
2264      : /city|road|urban/i.test(catLabel) ? "/city-bicycles"
2265      : /kid|junior|child|balance/i.test(catLabel) ? "/kids-bicycles"
2266      : /bmx/i.test(catLabel) ? "/bmx-bicycles" : "/bicycles";
2267    const requestBtn = `<button class="btn btn-primary btn-lg" data-enquiry="${escAttr(enquiry)}" data-enquiry-origin="India" data-enquiry-sku="${escAttr(bike.slug || "")}" data-enquiry-pillar="complete-bicycles">Request a Quote</button>`;
2268    pdpBody.classList.add("pdp--bike");
2269    pdpBody.innerHTML = `
2270      <div class="pdp-main">
2271        <div class="pdp-media">${img}</div>
2272        <div class="pdp-info">
2273          <div class="pdp-meta-row">
2274            <span class="pdp-origin-chip pdp-origin-chip--india">India</span>
2275            <span class="pdp-sku">SKU: ${escHtml(bike.slug || "—")}</span>
2276            <span class="pdp-cat"><a href="${catHref}">${escHtml(catLabel)}</a></span>
2277          </div>
2278          <h1>${escHtml((window.EastmanProductSEO && window.EastmanProductSEO.forBike(bike) || {}).h1 || displayProductName(bike))}</h1>
2279          <p class="pdp-lead">${escHtml((window.EastmanProductSEO && window.EastmanProductSEO.forBike(bike) || {}).lead || "Complete bicycle for B2B export from India.")}</p>
2280          ${pdpPrimaryActionsHTML({ requestBtn: requestBtn })}
2281          ${pdpShareHTML()}
2282        </div>
2283      </div>
2284      ${bikePdpExtra(bike, related)}`;
2285    bindPdpShare(pdpBody);
2286    const bikeSeo = window.EastmanProductSEO ? window.EastmanProductSEO.forBike(bike) : null;
2287    if (window.EastmanSchema && window.EastmanSchema.applyProduct) {
2288      window.EastmanSchema.applyProduct({
2289        name: bike.name,
2290        short: (bikeSeo && bikeSeo.description) || "Complete bicycle for B2B export from India.",
2291        img: bike.img,
2292        sku: bike.slug,
2293        id: bike.slug,
2294        catName: "Complete Bicycles India",
2295        slug: bike.slug,
2296      });
2297    }
2298    applyPdpSeo(bikeSeo, bike);
2299    const rg = $("#relatedGrid");
2300    if (related.length && rg) {
2301      $("#relatedWrap").hidden = false;
2302      const relHead = $("#relatedWrap .section-head h2") || $("#relatedWrap h2");
2303      if (relHead) relHead.textContent = "Related Products";
2304      rg.className = "prod-grid pdp-related-grid";
2305      const togetherHtml = together.length ? `<div class="pdp-together"><h3>Buyers Often Source Together</h3><div class="prod-grid pdp-related-grid">${together.map((b) => {
2306          const url = bikeUrl(b.slug);
2307          const sub = b.tag || b.category || "Complete Bicycle";
2308          return `<a class="bikes-adv-card trend-card product-adv-card bike-pcard trend-card--view" href="${url}">
2309            <span class="bikes-adv-card__media"><img src="${escAttr(assetUrl(b.img))}" alt="${escAttr(b.name)}" loading="lazy" /></span>
2310            <span class="bikes-adv-card__shade" aria-hidden="true"></span>
2311            <span class="bikes-adv-card__body"><span><span class="bikes-adv-card__title">${escHtml(b.name)}</span><span class="bikes-adv-card__sub">${escHtml(sub)}</span></span><span class="bikes-adv-card__cta">View Details</span></span></a>`;
2312        }).join("")}</div></div>` : "";
2313      rg.innerHTML = related
2314        .map((b) => {
2315          const url = bikeUrl(b.slug);
2316          const sub = b.tag || b.category || "Complete Bicycle";
2317          return `<a class="bikes-adv-card trend-card product-adv-card bike-pcard trend-card--view" href="${url}">
2318            <span class="bikes-adv-card__media"><img src="${escAttr(assetUrl(b.img))}" alt="${escAttr(b.name)}" loading="lazy" /></span>
2319            <span class="bikes-adv-card__shade" aria-hidden="true"></span>
2320            <span class="bikes-adv-card__body">
2321              <span>
2322                <span class="bikes-adv-card__title">${escHtml(b.name)}</span>
2323                <span class="bikes-adv-card__sub">${escHtml(sub)}</span>
2324              </span>
2325              <span class="bikes-adv-card__cta">View Details</span>
2326            </span>
2327          </a>`;
2328        })
2329        .join("");
2330      $$(".bikes-adv-card, .product-adv-card, .pcard", rg).forEach(reveal);
2331      enhanceRelatedCarousel(rg);
2332      if (togetherHtml) {
2333        const wrap = $("#relatedWrap");
2334        if (wrap && !wrap.querySelector(".pdp-together")) wrap.insertAdjacentHTML("beforeend", '<div class="container">' + togetherHtml + "</div>");
2335        const tg = wrap && wrap.querySelector(".pdp-together .pdp-related-grid");
2336        if (tg) enhanceRelatedCarousel(tg);
2337      }
2338    }
2339  }
2340
2341
2342  function enhanceRelatedCarousel(gridEl) {
2343    if (!gridEl) return;
2344    gridEl.classList.remove("pdp-related-track", "bikes-adventure-track");
2345    gridEl.classList.add("prod-grid", "pdp-related-grid");
2346  }
2347
2348  /* ================= product detail ================= */
2349  const pdpTestimonials = $("#pdpTestimonials");
2350  if (pdpTestimonials) pdpTestimonials.hidden = true;
2351  const pdpBody = $("#pdpBody");
2352  if (pdpBody) try {
2353    const resolved = resolvePdpIds();
2354    const bikeSlug = resolved.bikeSlug;
2355    const bikeItem = bikeSlug && window.getIndiaBikeBySlug ? window.getIndiaBikeBySlug(bikeSlug) : null;
2356    if (bikeItem) {
2357      setCanonical(bikeUrl(bikeItem.slug));
2358      const crumbParent = $("#crumbParent");
2359      const catLabel = bikeItem.category || bikeItem.tag || "Complete Bicycles";
2360      const catHref = /mountain|mtb|hybrid/i.test(catLabel) ? "/mountain-bikes"
2361        : /city|road|urban/i.test(catLabel) ? "/city-bicycles"
2362        : /kid|junior|child|balance/i.test(catLabel) ? "/kids-bicycles"
2363        : /bmx/i.test(catLabel) ? "/bmx-bicycles" : "/bicycles";
2364      if (crumbParent) {
2365        crumbParent.href = catHref;
2366        crumbParent.textContent = catLabel;
2367      }
2368      const crumbsNav = crumbParent && crumbParent.closest(".crumbs");
2369      if (crumbsNav && !crumbsNav.querySelector("[data-bike-hub]")) {
2370        const hub = document.createElement("a");
2371        hub.href = "/bicycles";
2372        hub.textContent = "Bicycles";
2373        hub.setAttribute("data-bike-hub", "1");
2374        const sep = document.createElement("span");
2375        sep.textContent = " / ";
2376        crumbParent.parentNode.insertBefore(hub, crumbParent);
2377        crumbParent.parentNode.insertBefore(sep, crumbParent);
2378      }
2379      $("#crumbName").textContent = bikeItem.name;
2380      renderBikePdp(bikeItem);
2381    } else {
2382    const p = byId(resolved.partId);
2383    if (p) {
2384        setCanonical(productUrl(p));
2385      $("#crumbName").textContent = p.name;
2386      const crumbParent = $("#crumbParent");
2387      const crumbsNav = crumbParent && crumbParent.closest(".crumbs");
2388      if (crumbsNav) {
2389        const pillarHref = "/bicycle-parts";
2390        const pillarLabel = "Bicycle Parts";
2391        let pillar = crumbsNav.querySelector("[data-parts-hub]");
2392        if (!pillar && crumbParent) {
2393          pillar = document.createElement("a");
2394          pillar.href = pillarHref;
2395          pillar.textContent = pillarLabel;
2396          pillar.setAttribute("data-parts-hub", "1");
2397          const sep = document.createElement("span");
2398          sep.textContent = " / ";
2399          crumbParent.parentNode.insertBefore(pillar, crumbParent);
2400          crumbParent.parentNode.insertBefore(sep, crumbParent);
2401        }
2402        if (crumbParent) {
2403          crumbParent.href = categoryUrl(p.group, p.catSlug);
2404          crumbParent.textContent = p.catName || p.cat || "Category";
2405        }
2406      }
2407      const partSeo = window.EastmanProductSEO ? window.EastmanProductSEO.forPart(p) : null;
2408      const allTags = p.tags && p.tags.length ? `<div class="pdp-tags">${tagPills(p.tags)}</div>` : "";
2409      const badge = p.tags && p.tags[0] ? `<span class="pdp-badge pcard-badge--${p.tags[0]}">${TAG_LABELS[p.tags[0]] || p.tags[0]}</span>` : "";
2410      const specRows = specsFromDesc(p.desc || p.short || "");
2411      const seen = {};
2412      const attrPairs = Object.entries(p.attrs || {})
2413        .concat(specRows)
2414        .filter(([k, v]) => {
2415          const key = String(k).toLowerCase();
2416          if (seen[key]) return false;
2417          seen[key] = 1;
2418          if (/^sku$/i.test(k) && /^on request$/i.test(String(v))) return false;
2419          return true;
2420        });
2421      const catUrl = categoryUrl(p.group, p.catSlug);
2422      const shownName = displayProductName(p);
2423      const originLabel = p.group === "china" ? "China" : "India";
2424      const skuVal = p.sku || (p.attrs && p.attrs.SKU) || p.id || "";
2425      const requestBtn = `<button class="btn btn-primary btn-lg" data-enquiry-id="${p.id}" data-enquiry-country="" data-enquiry-origin="${originLabel}">Request a Quote</button>`;
2426      pdpBody.innerHTML = `
2427        <div class="pdp-main">
2428          <div class="pdp-media">${badge}${pdpGalleryHTML(p)}</div>
2429          <div class="pdp-info">
2430            <div class="pdp-meta-row">
2431              <span class="pdp-origin-chip pdp-origin-chip--${p.group === "china" ? "china" : "india"}">${originLabel}</span>
2432              <span class="pdp-sku">SKU: ${escHtml(skuVal || "—")}</span>
2433              <span class="pdp-cat"><a href="${catUrl}">${p.catName || p.cat}</a></span>
2434            </div>
2435            ${allTags}
2436            <h1>${escHtml((partSeo && partSeo.h1) || shownName)}</h1>
2437            <p class="pdp-lead">${escHtml((partSeo && partSeo.lead) || "")}</p>
2438            ${pdpPrimaryActionsHTML({ requestBtn: requestBtn })}
2439            ${pdpShareHTML()}
2440          </div>
2441        </div>
2442        ${pdpExtraSections(p, { shownName, originLabel, skuVal, requestBtn })}`;
2443      bindPdpGallery(pdpBody);
2444      bindPdpShare(pdpBody);
2445      loadPdpGallery(p, pdpBody);
2446      if (window.EastmanSchema && window.EastmanSchema.applyProduct) {
2447        window.EastmanSchema.applyProduct(
2448          Object.assign({}, p, { short: (partSeo && partSeo.description) || p.short })
2449        );
2450      }
2451      applyPdpSeo(partSeo, p);
2452      const related = PRODUCTS.filter((x) => x.catSlug === p.catSlug && x.id !== p.id).slice(0, 6);
2453      const together = PRODUCTS.filter((x) => x.group === p.group && x.catSlug !== p.catSlug && x.id !== p.id).slice(0, 4);
2454      const rg = $("#relatedGrid");
2455      if (related.length && rg) {
2456        $("#relatedWrap").hidden = false;
2457        const relHead = $("#relatedWrap .section-head h2") || $("#relatedWrap h2");
2458        if (relHead) relHead.textContent = "Related Products";
2459        rg.className = "prod-grid pdp-related-grid";
2460        renderGrid(rg, related.slice(0, 6));
2461        enhanceRelatedCarousel(rg);
2462        if (together.length) {
2463          const wrap = $("#relatedWrap");
2464          if (wrap && !wrap.querySelector(".pdp-together")) {
2465            const box = document.createElement("div");
2466            box.className = "container pdp-together";
2467            box.innerHTML = "<h3>Buyers Often Source Together</h3><div class=\"prod-grid pdp-related-grid\" id=\"togetherGrid\"></div>";
2468            wrap.appendChild(box);
2469            renderGrid($("#togetherGrid"), together);
2470            enhanceRelatedCarousel($("#togetherGrid"));
2471          }
2472        }
2473      }
2474    } else {
2475      pdpBody.innerHTML = `<div class="pdp-not-found"><h1>Product not found</h1><p>We could not find this product. It may have been moved or the link is incomplete.</p><p><a class="btn btn-primary" href="/bicycle-parts">Browse bicycle parts</a> <a class="btn btn-outline" href="/bicycles">Browse complete bicycles</a></p></div>`;
2476      document.title = "Product not found | Eastman Bicycles";
2477      let robots = document.querySelector('meta[name="robots"]');
2478      if (!robots) {
2479        robots = document.createElement("meta");
2480        robots.setAttribute("name", "robots");
2481        document.head.appendChild(robots);
2482      }
2483      robots.setAttribute("content", "noindex, follow");
2484      const crumb = $("#crumbName");
2485      if (crumb) crumb.textContent = "Not found";
2486    }
2487    }
2488  } catch (err) {
2489    console.error("PDP render", err);
2490    pdpBody.innerHTML = `<div class="pdp-not-found"><h1>Couldn’t load this product</h1><p>Something went wrong while loading product details. Please try again or browse the catalogue.</p><p><a class="btn btn-primary" href="/bicycle-parts">Browse bicycle parts</a> <a class="btn btn-outline" href="/bicycles">Browse bicycles</a></p></div>`;
2491  }
2492
2493  /* ================= brands ================= */
2494  const brandRow = $("#brandRow");
2495  if (brandRow) brandRow.innerHTML = BRANDS.map((b) => brandCardHTML(b, "brand-item", false)).join("");
2496
2497  /* ================= careers (HR jobs from WordPress) ================= */
2498  function stripHtml(html) {
2499    const el = document.createElement("div");
2500    el.innerHTML = html || "";
2501    return (el.textContent || el.innerText || "").replace(/\s+/g, " ").trim();
2502  }
2503
2504  function jobSlug(title) {
2505    return String(title || "")
2506      .toLowerCase()
2507      .replace(/&/g, "and")
2508      .replace(/[^a-z0-9]+/g, "-")
2509      .replace(/^-+|-+$/g, "");
2510  }
2511
2512  function jobApplyUrl(job) {
2513    var path = "";
2514    if (job.applyUrl) {
2515      path = job.applyUrl;
2516    } else if (job.link) {
2517      try {
2518        var u = new URL(job.link, "https://eastmanbicycles.com");
2519        path = u.pathname || "";
2520      } catch (err) { /* slug fallback */ }
2521    }
2522    if (!path && job.slug) path = "/jobs/" + job.slug + "/";
2523    if (!path && job.title) {
2524      var slug = jobSlug(job.title);
2525      path = slug ? "/jobs/" + slug + "/" : "/career";
2526    }
2527    if (!path) path = "/career";
2528    // Keep trailing slash for WP CPT; add source=career so browsers that cached the
2529    // old /jobs/... redirect-loop still open the apply form reliably.
2530    try {
2531      var abs = new URL(path, "https://eastmanbicycles.com");
2532      if (!abs.pathname.endsWith("/")) abs.pathname += "/";
2533      if (!abs.searchParams.has("source")) abs.searchParams.set("source", "career");
2534      return abs.pathname + abs.search;
2535    } catch (err2) {
2536      return path.indexOf("source=") >= 0 ? path : (path + (path.indexOf("?") >= 0 ? "&" : "?") + "source=career");
2537    }
2538  }
2539
2540  function jobCardHTML(job) {
2541    const title = job.title || "Job Opening";
2542    const dept = job.job_department || "Staff";
2543    const location = job.job_location || "Ludhiana";
2544    const type = job.job_type || "Full Time";
2545    const desc = (job.description || "").slice(0, 280);
2546    const applyUrl = jobApplyUrl(job);
2547    return `<article class="job-card">
2548      <h3>${escHtml(title)}</h3>
2549      <p class="job-meta"><span>${escHtml(dept)}</span><span>${escHtml(location)}</span><span>${escHtml(type)}</span></p>
2550      <p>${escHtml(desc)}</p>
2551      <a class="btn btn-outline btn-sm" href="${escAttr(applyUrl)}" rel="noopener">Apply Now</a>
2552    </article>`;
2553  }
2554
2555  function mapWpJob(post) {
2556    const meta = post.meta || {};
2557    const classes = Array.isArray(post.class_list) ? post.class_list.join(" ") : "";
2558    function classVal(prefix) {
2559      const m = classes.match(new RegExp("(?:^|\\s)" + prefix + "([a-z0-9-]+)", "i"));
2560      if (!m) return "";
2561      return m[1].replace(/-/g, " ").replace(/\b\w/g, function (c) { return c.toUpperCase(); });
2562    }
2563    return {
2564      title: stripHtml(post.title?.rendered || ""),
2565      description: stripHtml(post.excerpt?.rendered || post.content?.rendered || ""),
2566      job_location: meta.job_location || meta._job_location || classVal("job-location-") || "Ludhiana",
2567      job_type: meta.job_type || meta._job_type || classVal("job-type-") || "Full Time",
2568      job_department: meta.job_department || meta._job_category || classVal("job-category-") || "Staff",
2569      link: post.link || "",
2570      slug: post.slug || "",
2571      id: post.id || "",
2572      datePosted: (post.date || "").slice(0, 10),
2573      applyUrl: jobApplyUrl({ link: post.link || "", slug: post.slug || "", title: stripHtml(post.title?.rendered || "") }),
2574    };
2575  }
2576
2577  async function fetchJobPosts(api, perPage) {
2578    const res = await fetch(`${api}?per_page=${perPage}&status=publish&orderby=date&order=desc`, {
2579      headers: { Accept: "application/json" },
2580    });
2581    if (!res.ok) throw new Error(String(res.status));
2582    return res.json();
2583  }
2584
2585  async function loadCareerJobs(el) {
2586    const cfg = window.EASTMAN_JOBS || {};
2587    const perPage = cfg.perPage || 50;
2588    // Root /wp-json works on live Hostinger; /blog/wp-json is 404.
2589    const apis = [
2590      cfg.apiUrl,
2591      "/wp-json/wp/v2/awsm_job_openings",
2592      "/blog/wp-json/wp/v2/awsm_job_openings",
2593    ].filter((url, i, arr) => url && arr.indexOf(url) === i);
2594    el.innerHTML = '<p class="loading-note">Loading current openings…</p>';
2595
2596    try {
2597      let posts = [];
2598      for (const api of apis) {
2599        try {
2600          posts = await fetchJobPosts(api, perPage);
2601          if (Array.isArray(posts) && posts.length) break;
2602        } catch (err) { /* try next REST path */ }
2603      }
2604      if (!Array.isArray(posts) || !posts.length) throw new Error("empty");
2605      const mapped = posts.map((p) => mapWpJob(p));
2606      el.innerHTML = mapped.map((j) => jobCardHTML(j)).join("");
2607      if (window.EastmanSchema && window.EastmanSchema.applyJobs) window.EastmanSchema.applyJobs(mapped);
2608    } catch {
2609      const list = cfg.fallback || [];
2610      el.innerHTML = list.length
2611        ? list.map((j) => jobCardHTML(j)).join("")
2612        : '<p class="empty">Unable to load jobs. Please try again later or contact us.</p>';
2613      if (list.length && window.EastmanSchema && window.EastmanSchema.applyJobs) window.EastmanSchema.applyJobs(list);
2614    }
2615    $$(".job-card", el).forEach(reveal);
2616  }
2617
2618  const jobGrid = $("#jobGrid");
2619  if (jobGrid && document.body.dataset.page === "career") loadCareerJobs(jobGrid);
2620
2621  revealAll();
2622  if (window.fixSiteLinks) window.fixSiteLinks(document);
2623
2624  /* ================= h49: PDP sticky bottom CTA bar ================= */
2625  function initPdpStickyBar() {
2626    const pdp = $("#pdpBody");
2627    if (!pdp) return;
2628    const actions = pdp.querySelector(".pdp-actions");
2629    if (!actions) return;
2630    let bar = $("#pdpStickyBar");
2631    if (!bar) {
2632      bar = document.createElement("div");
2633      bar.id = "pdpStickyBar";
2634      bar.className = "pdp-sticky-bar";
2635      bar.hidden = true;
2636      const title = (document.querySelector("#pdpBody h1") || {}).textContent || "this product";
2637      const waText = encodeURIComponent("Hi Eastman, I want a quote for " + title.trim());
2638      const waSvg = '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="#fff" d="M20.5 3.5A11 11 0 0 0 2.1 17.8L1 23l5.4-1.1A11 11 0 1 0 20.5 3.5zm-8.5 17a9 9 0 0 1-4.6-1.3l-.3-.2-3.2.7.7-3.1-.2-.3a9 9 0 1 1 7.6 4.2zm5-6.7c-.3-.1-1.6-.8-1.8-.9s-.4-.1-.6.2-.7.9-.8 1-.3.2-.6.1a7.4 7.4 0 0 1-2.2-1.4 8.2 8.2 0 0 1-1.5-1.9c-.2-.3 0-.4.1-.6l.4-.5.2-.3a.5.5 0 0 0 0-.5l-.6-1.5c-.2-.4-.3-.4-.6-.4h-.5a1 1 0 0 0-.7.3 2.9 2.9 0 0 0-.9 2.2 5 5 0 0 0 1.1 2.7 11.5 11.5 0 0 0 4.4 3.9 5 5 0 0 0 3 .9 2.5 2.5 0 0 0 1.7-.7 2 2 0 0 0 .4-1.4c0-.2-.2-.3-.5-.4z"/></svg>';
2639      bar.innerHTML = `<button type="button" class="pdp-sticky-bar__quote btn btn-primary" data-enquiry="">Request a Quote</button>
2640        <a class="pdp-sticky-bar__wa" href="https://wa.me/919115110415?text=${waText}" target="_blank" rel="noopener" aria-label="WhatsApp">${waSvg}</a>`;
2641      const fab = document.getElementById("whatsappFabBtn");
2642      const waLink = bar.querySelector(".pdp-sticky-bar__wa");
2643      if (fab && waLink && fab.innerHTML.trim()) waLink.innerHTML = fab.innerHTML;
2644      document.body.appendChild(bar);
2645      document.body.classList.add("has-pdp-sticky");
2646      // Hide generic mobile bar on PDP to avoid double stack
2647      const mob = document.getElementById("mobileStickyBar");
2648      if (mob) mob.classList.add("mobile-bar--pdp-hidden");
2649    }
2650    const quoteBtn = bar.querySelector("[data-enquiry]");
2651    if (quoteBtn) {
2652      // copy enquiry attrs from in-page CTA if any
2653      const src = actions.querySelector("[data-enquiry], [data-enquiry-id]");
2654      if (src) {
2655        ["data-enquiry", "data-enquiry-id", "data-enquiry-origin", "data-enquiry-sku", "data-enquiry-pillar", "data-enquiry-country"].forEach((a) => {
2656          if (src.hasAttribute(a)) quoteBtn.setAttribute(a, src.getAttribute(a));
2657        });
2658      }
2659    }
2660    let past = false;
2661    const update = () => {
2662      const rect = actions.getBoundingClientRect();
2663      const shouldShow = rect.bottom < 8;
2664      if (shouldShow !== past) {
2665        past = shouldShow;
2666        bar.hidden = !shouldShow;
2667        bar.classList.toggle("is-visible", shouldShow);
2668        document.body.classList.toggle("pdp-sticky-visible", shouldShow);
2669      }
2670    };
2671    window.addEventListener("scroll", update, { passive: true });
2672    window.addEventListener("resize", update);
2673    update();
2674  }
2675  try { initPdpStickyBar(); } catch (err) { console.error("pdp sticky", err); }
2676
2677})();

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.