PageSourceSearch

https://samuraiswordtokyo.com/wp-content/plugins/sst-product-insig…s.v050.1d1bc116ecb0.js?ver=0.5.0.1790916995

js samuraiswordtokyo.com collected 2026-10-06 04:17:51 UTC 27,038 bytes, 713 lines download raw bytes

1/* SST Product Insights — bulk add to cart, recently viewed, GA4 events.
2   Vanilla JS, no dependencies. Config via window.SST_PI_CFG. */
3(function () {
4  'use strict';
5
6  var CFG = window.SST_PI_CFG || {};
7  var RV_KEY = 'sst_rv';
8  var RV_MAX = 12;
9  var RV_SHOW = 4;
10
11  function ready(fn) {
12    if (document.readyState === 'loading') {
13      document.addEventListener('DOMContentLoaded', fn);
14    } else {
15      fn();
16    }
17  }
18
19  // ------------------------------------------------------------------
20  // GA4 helper (SamuraiGA4 wrapper with dataLayer fallback)
21  // ------------------------------------------------------------------
22
23  function track(payload) {
24    try {
25      if (window.SamuraiGA4 && typeof window.SamuraiGA4.push === 'function') {
26        window.SamuraiGA4.push(payload);
27        return;
28      }
29      window.dataLayer = window.dataLayer || [];
30      if (payload && payload.ecommerce) {
31        window.dataLayer.push({ ecommerce: null });
32      }
33      window.dataLayer.push(payload);
34    } catch (e) { /* never break the page for analytics */ }
35  }
36
37  function itemFromCard(li, index) {
38    return {
39      item_id: li.getAttribute('data-pid') || li.getAttribute('data-sku') || '',
40      item_sku: li.getAttribute('data-sku') || '',
41      item_name: li.getAttribute('data-name') || '',
42      price: parseFloat(li.getAttribute('data-price') || '0') || 0,
43      quantity: 1,
44      index: typeof index === 'number' ? index : parseInt(li.getAttribute('data-position') || '0', 10)
45    };
46  }
47
48  function listItems(section) {
49    var items = [];
50    section.querySelectorAll('[data-sst-pi-item]').forEach(function (li, i) {
51      items.push(itemFromCard(li, i));
52    });
53    return items;
54  }
55
56  function wireListTracking() {
57    var sections = document.querySelectorAll('[data-sst-pi-list]');
58    if (!sections.length) { return; }
59
60    if ('IntersectionObserver' in window) {
61      var seen = {};
62      var io = new IntersectionObserver(function (entries) {
63        entries.forEach(function (entry) {
64          if (!entry.isIntersecting) { return; }
65          var section = entry.target;
66          var listId = section.getAttribute('data-sst-pi-list');
67          if (!listId || seen[listId]) { return; }
68          seen[listId] = true;
69          io.unobserve(section);
70          track({
71            event: 'view_item_list',
72            ecommerce: { item_list_id: listId, item_list_name: listId, items: listItems(section) }
73          });
74        });
75      }, { threshold: 0.3 });
76      sections.forEach(function (s) { io.observe(s); });
77    }
78
79    document.addEventListener('click', function (event) {
80      var link = event.target.closest ? event.target.closest('[data-sst-pi-card-link]') : null;
81      if (link) {
82        var li = link.closest('[data-sst-pi-item]');
83        var section = link.closest('[data-sst-pi-list]');
84        if (li) {
85          track({
86            event: 'select_item',
87            ecommerce: {
88              item_list_id: section ? section.getAttribute('data-sst-pi-list') : '',
89              items: [itemFromCard(li, null)]
90            }
91          });
92        }
93        return;
94      }
95      var resource = event.target.closest ? event.target.closest('[data-sst-pi-link]') : null;
96      if (resource) {
97        track({
98          event: 'select_content',
99          content_type: 'pdp_insights',
100          item_id: resource.getAttribute('data-sst-pi-link') || ''
101        });
102      }
103    }, true);
104  }
105
106  // ------------------------------------------------------------------
107  // Bulk add to cart
108  // ------------------------------------------------------------------
109
110  var bulkNonce = '';
111
112  function post(action, data) {
113    var body = new URLSearchParams();
114    body.set('action', action);
115    Object.keys(data || {}).forEach(function (k) { body.set(k, data[k]); });
116    return fetch(CFG.ajaxUrl || '/wp-admin/admin-ajax.php', {
117      method: 'POST',
118      credentials: 'same-origin',
119      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
120      body: body.toString()
121    }).then(function (res) {
122      return res.json().then(function (json) { return { status: res.status, json: json }; });
123    });
124  }
125
126  function fetchNonce() {
127    return post('sst_pi_bulk_add_nonce', {}).then(function (res) {
128      if (res.json && res.json.success && res.json.data && res.json.data.nonce) {
129        bulkNonce = res.json.data.nonce;
130      }
131      return bulkNonce;
132    });
133  }
134
135  function formatMoney(amount) {
136    var currency = CFG.currency || 'JPY';
137    try {
138      return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency, maximumFractionDigits: currency === 'JPY' ? 0 : 2 }).format(amount);
139    } catch (e) {
140      return 'Â¥' + Math.round(amount).toLocaleString();
141    }
142  }
143
144  function applyFragments(fragments) {
145    if (!fragments) { return false; }
146    var applied = false;
147    Object.keys(fragments).forEach(function (selector) {
148      var el = document.querySelector(selector);
149      if (el) {
150        el.outerHTML = fragments[selector];
151        applied = true;
152      }
153    });
154    return applied;
155  }
156
157  function wireBulkAdd() {
158    var section = document.querySelector('[data-sst-pi-bulk]');
159    if (!section) { return; }
160    var bar = section.querySelector('[data-sst-pi-bulkbar]');
161    var summary = section.querySelector('[data-sst-pi-bulk-summary]');
162    var button = section.querySelector('[data-sst-pi-bulk-btn]');
163    if (!bar || !summary || !button) { return; }
164
165    var checks = section.querySelectorAll('[data-sst-pi-bulk-check]');
166    if (!checks.length) { bar.hidden = true; return; }
167
168    function selected() {
169      var out = [];
170      checks.forEach(function (box) {
171        if (box.checked) {
172          var li = box.closest('[data-sst-pi-item]');
173          if (li) { out.push(li); }
174        }
175      });
176      return out;
177    }
178
179    function refresh() {
180      var lis = selected();
181      if (!lis.length) {
182        bar.hidden = true;
183        return;
184      }
185      var total = 0;
186      lis.forEach(function (li) { total += parseFloat(li.getAttribute('data-price') || '0') || 0; });
187      summary.textContent = lis.length + (lis.length === 1 ? ' item' : ' items') + ' · ' + formatMoney(total);
188      bar.hidden = false;
189    }
190
191    checks.forEach(function (box) {
192      box.addEventListener('change', function () {
193        refresh();
194        if (!bulkNonce) { fetchNonce(); }
195      });
196    });
197
198    button.addEventListener('click', function () {
199      var lis = selected();
200      if (!lis.length || button.disabled) { return; }
201      var max = CFG.bulkMax || 10;
202      lis = lis.slice(0, max);
203      var items = lis.map(function (li) {
204        return {
205          product_id: parseInt(li.getAttribute('data-pid') || '0', 10),
206          variation_id: parseInt(li.getAttribute('data-vid') || '0', 10),
207          quantity: 1
208        };
209      });
210
211      button.disabled = true;
212      var originalLabel = button.textContent;
213      button.textContent = 'Adding…';
214
215      var send = function (retried) {
216        return post('sst_pi_bulk_add', { nonce: bulkNonce, items: JSON.stringify(items) }).then(function (res) {
217          if (res.status === 403 && !retried) {
218            return fetchNonce().then(function () { return send(true); });
219          }
220          return res;
221        });
222      };
223
224      (bulkNonce ? Promise.resolve() : fetchNonce()).then(function () {
225        return send(false);
226      }).then(function (res) {
227        var data = res.json && res.json.success ? res.json.data : null;
228        if (!data) { throw new Error('bulk add failed'); }
229        applyFragments(data.fragments);
230        if (window.jQuery) {
231          window.jQuery(document.body).trigger('wc_fragments_refreshed');
232        }
233        var ga4Items = [];
234        var total = 0;
235        lis.forEach(function (li, i) {
236          if (data.added.indexOf(parseInt(li.getAttribute('data-pid') || '0', 10)) !== -1) {
237            ga4Items.push(itemFromCard(li, i));
238            total += parseFloat(li.getAttribute('data-price') || '0') || 0;
239          }
240        });
241        if (ga4Items.length) {
242          track({
243            event: 'add_to_cart',
244            ecommerce: { currency: CFG.currency || 'JPY', value: total, items: ga4Items }
245          });
246        }
247        button.textContent = data.failed && data.failed.length ? 'Added (some skipped)' : 'Added to cart ✓';
248        checks.forEach(function (box) { box.checked = false; });
249        window.setTimeout(function () {
250          button.textContent = originalLabel;
251          button.disabled = false;
252          refresh();
253        }, 2400);
254      }).catch(function () {
255        button.textContent = 'Could not add — try again';
256        window.setTimeout(function () {
257          button.textContent = originalLabel;
258          button.disabled = false;
259        }, 2400);
260      });
261    });
262  }
263
264  // ------------------------------------------------------------------
265  // Recently viewed (localStorage, schema {id,name,url,img,price})
266  // ------------------------------------------------------------------
267
268  function readRv() {
269    try {
270      var raw = window.localStorage.getItem(RV_KEY);
271      var list = raw ? JSON.parse(raw) : [];
272      return Array.isArray(list) ? list : [];
273    } catch (e) {
274      return [];
275    }
276  }
277
278  function writeRv(list) {
279    try {
280      window.localStorage.setItem(RV_KEY, JSON.stringify(list.slice(0, RV_MAX)));
281    } catch (e) { /* storage full/blocked */ }
282  }
283
284  function currentProductEntry() {
285    var p = CFG.product;
286    if (!p || !p.id) { return null; }
287    var img = '';
288    var el = document.querySelector('.woocommerce-product-gallery__image img, .woocommerce-product-gallery img, .sst-product-main img');
289    if (el) { img = el.currentSrc || el.src || ''; }
290    return { id: p.id, name: p.name || '', url: p.url || window.location.pathname, img: img, price: p.price || '' };
291  }
292
293  function wireRecentlyViewed() {
294    var section = document.querySelector('[data-sst-pi-recent]');
295    var grid = document.querySelector('[data-sst-pi-recent-grid]');
296    var entry = currentProductEntry();
297    var list = readRv();
298
299    if (grid && section) {
300      var currentId = entry ? entry.id : 0;
301      var show = list.filter(function (it) { return it && it.id && it.id !== currentId; }).slice(0, RV_SHOW);
302      if (show.length) {
303        show.forEach(function (it) {
304          var li = document.createElement('li');
305          li.className = 'product sst-product-card sst-fbt__card sst-pi-recent__card';
306          var a = document.createElement('a');
307          a.className = 'sst-product-card__link';
308          a.href = it.url;
309          var media = document.createElement('span');
310          media.className = 'sst-product-card__media';
311          if (it.img) {
312            var img = document.createElement('img');
313            img.src = it.img;
314            img.alt = it.name;
315            img.loading = 'lazy';
316            img.decoding = 'async';
317            media.appendChild(img);
318          }
319          var body = document.createElement('span');
320          body.className = 'sst-product-card__body';
321          var title = document.createElement('span');
322          title.className = 'woocommerce-loop-product__title';
323          title.textContent = it.name;
324          body.appendChild(title);
325          if (it.price) {
326            var price = document.createElement('span');
327            price.className = 'price';
328            price.textContent = it.price;
329            body.appendChild(price);
330          }
331          a.appendChild(media);
332          a.appendChild(body);
333          li.appendChild(a);
334          grid.appendChild(li);
335        });
336        section.hidden = false;
337      }
338    }
339
340    if (entry) {
341      var next = list.filter(function (it) { return it && it.id !== entry.id; });
342      next.unshift(entry);
343      writeRv(next);
344    }
345  }
346
347  // ------------------------------------------------------------------
348  // Points line (v0.4.0, per-product base rate × rank multiplier since v0.5.0) — "Earn N pts" under the price.
349  // The skeleton ships hidden in the shared-cache HTML; the visitor's own
350  // earn rate arrives from GET /sst-points/v1/me (cookie auth, no-store).
351  // Fail-closed: until that answer lands, nothing is shown.
352  // ------------------------------------------------------------------
353
354  // PHP parity: SST_Points_Rates::points_for_rate() = floor(round(b*r, 6)).
355  function pointsFor(base, rate) {
356    return Math.floor(Math.round(base * rate * 1e6) / 1e6);
357  }
358
359  // v0.5.0 (contract §3): effective rate = round(baseRate × multiplier, 8);
360  // points = floor(round(price × rate, 6)) — one floor, as in sst-points.
361  function lineRate(baseRate, mult) {
362    return Math.round(baseRate * mult * 1e8) / 1e8;
363  }
364
365  // Contract §2: a base rate is usable only when 0 < v <= 0.01.
366  function validBaseRate(v) {
367    v = typeof v === 'number' ? v : parseFloat(v);
368    return (isFinite(v) && v > 0 && v <= 0.01) ? v : 0;
369  }
370
371  // "0.5%" / "0.25%" / "0.375%" / "0.1%": at most 3 decimals, trailing zeros
372  // trimmed, at least 1 decimal (PHP rate_percent_label() format).
373  function ratePercentLabel(rate) {
374    var s = (Math.round(rate * 100 * 1000) / 1000).toFixed(3).replace(/0+$/, '');
375    if (s.charAt(s.length - 1) === '.') { s += '0'; }
376    return s + '%';
377  }
378
379  // Multiplier number only ("3", "1.5"); the × sign belongs to the template.
380  function multiplierNumber(m) {
381    var s = (Math.round(m * 10000) / 10000).toFixed(4).replace(/0+$/, '');
382    return s.charAt(s.length - 1) === '.' ? s.slice(0, -1) : s;
383  }
384
385  // Rank multiplier from /me: `multiplier` (sst-points >= 0.6.0), else
386  // rate / baseRateMax (`rate` stays the max-band rate = 0.005 × multiplier).
387  // Guests are ×1. 0 means unknown -> the line stays hidden (fail-closed).
388  function meMultiplier(data, baseRateMax) {
389    if (!data || !data.loggedIn) { return 1; }
390    if (typeof data.multiplier === 'number' && isFinite(data.multiplier) && data.multiplier > 0) {
391      return data.multiplier;
392    }
393    var max = (typeof data.baseRateMax === 'number' && data.baseRateMax > 0) ? data.baseRateMax : baseRateMax;
394    if (typeof data.rate === 'number' && isFinite(data.rate) && data.rate > 0 && max > 0) {
395      return Math.round(data.rate / max * 10000) / 10000;
396    }
397    return 0;
398  }
399
400  // Positional printf placeholders, literal (no $-pattern surprises).
401  function fillTpl(tpl, values) {
402    var out = tpl;
403    for (var i = 0; i < values.length; i++) {
404      out = out.split('%' + (i + 1) + '$s').join(values[i]);
405    }
406    return out;
407  }
408
409  function wireDutyNote() {
410    var el = document.querySelector('[data-sst-pi-duty]');
411    if (!el || !window.fetch) { return; }
412    var ddp = (el.getAttribute('data-ddp') || '').split(',');
413    /* Cloudflare answers this at the edge — it never reaches WordPress and is
414       never cached, so it is safe to call from a fully cached page. */
415    fetch('/cdn-cgi/trace', { credentials: 'omit', cache: 'no-store' })
416      .then(function (r) { return r.ok ? r.text() : ''; })
417      .then(function (t) {
418        var m = /(?:^|\n)loc=([A-Z]{2})/.exec(t || '');
419        if (m && ddp.indexOf(m[1]) !== -1) {
420          el.hidden = false;
421          track({ event: 'sst_pi_duty_note_shown', country: m[1] });
422        }
423      })
424      .catch(function () { /* stay hidden */ });
425  }
426
427  function wirePointsLine() {
428    var el = document.querySelector('[data-sst-pi-points]');
429    if (!el) { return; }
430    // v0.5.0: data-base-rate is this product's (or the price-matching
431    // variation's) base points rate; the visitor's rank multiplier comes
432    // from /me. pts = floor(round(price × round(base × mult, 8), 6)).
433    var pageBaseRate = validBaseRate(el.getAttribute('data-base-rate'));
434    var baseRate = pageBaseRate;
435    var baseRateMax = validBaseRate(el.getAttribute('data-base-rate-max')) || 0.005;
436    var multMax = parseFloat(el.getAttribute('data-mult-max'));
437    var basePrice = parseFloat(el.getAttribute('data-price')) || 0;
438    var price = basePrice;
439    var state = { ready: false, live: false, loggedIn: false, mult: 0, lineModel: false, rate: 0 };
440
441    // sst-signup-banner-impl-20261002 (W3, P4): guest-only sign-up copy. The server prints it
442    // translated as hidden text nodes inside this <p> (the dictionary never reaches data-* or
443    // JS strings); read them once, before the first render clears the element.
444    function tplText(name) {
445      var t = el.querySelector('[data-sst-pi-tpl="' + name + '"]');
446      return t ? (t.textContent || '').trim() : '';
447    }
448    var signup = {
449      lead: tplText('signup-lead'),
450      cta: tplText('signup-cta'),
451      terms: tplText('signup-terms'),
452      url: el.getAttribute('data-signup-url') || '/my-account/',
453      seen: false
454    };
455
456    function signupEvent(action) {
457      try {
458        document.dispatchEvent(new CustomEvent('sst:signup-surface', {
459          detail: { surface: 'pdp', action: action, campaign: 'points-signup' }
460        }));
461      } catch (e) { /* old browser */ }
462    }
463
464    // View = the block is at least 50% on screen once (same rule as W2's surfaces, audit A2 P2-5),
465    // not the moment it is rendered. Re-renders observe the new node; the event fires once per page.
466    var viewIo = null;
467    function observeView(node) {
468      if (signup.seen) { return; }
469      function fire() {
470        if (signup.seen) { return true; }
471        signup.seen = true;
472        signupEvent('view');
473        return true;
474      }
475      if (!('IntersectionObserver' in window)) { fire(); return; }
476      if (viewIo) { viewIo.disconnect(); }
477      viewIo = new IntersectionObserver(function (entries) {
478        entries.forEach(function (entry) {
479          if (entry.isIntersecting && entry.target.isConnected && fire()) { viewIo.disconnect(); }
480        });
481      }, { threshold: 0.5 });
482      viewIo.observe(node);
483    }
484
485    // Appends the guest block (lead + Sign up link, then the conditions line). Guests only.
486    function appendSignup() {
487      if (state.loggedIn || !signup.lead) { return false; }
488      var box = document.createElement('span');
489      box.className = 'sst-pi-points__signup';
490      box.setAttribute('data-sst-signup-surface', 'pdp');
491      var lead = document.createElement('span');
492      lead.className = 'sst-pi-points__signup-lead';
493      lead.appendChild(document.createTextNode(signup.lead + ' '));
494      if (signup.cta) {
495        var a = document.createElement('a');
496        a.className = 'sst-pi-points__signup-cta';
497        a.href = signup.url;
498        a.textContent = signup.cta;
499        a.addEventListener('click', function () { signupEvent('click'); });
500        lead.appendChild(a);
501      }
502      box.appendChild(lead);
503      if (signup.terms) {
504        var terms = document.createElement('span');
505        terms.className = 'sst-pi-points__signup-terms';
506        terms.textContent = signup.terms;
507        box.appendChild(terms);
508      }
509      el.appendChild(box);
510      observeView(box);
511      return true;
512    }
513
514    // No earn figure to show (e.g. < 1 pt): a guest still gets the sign-up copy.
515    function signupOnly() {
516      if (!state.ready || !state.live || state.loggedIn || !signup.lead) { return false; }
517      el.textContent = '';
518      appendSignup();
519      el.hidden = false;
520      return true;
521    }
522
523    // R2-03: the per-item model is shown only when /me (no-store) says the
524    // server pays it (lineModel === true). Missing/false = sst-points < 0.6.0
525    // or the line model switched off -> the legacy whole-order rate is what
526    // gets credited, so show exactly the 0.4.4 line. The cached HTML carries
527    // no mode, so flipping the option needs no PDP purge.
528    function renderLegacy() {
529      var rate = (state.loggedIn && state.rate > 0) ? state.rate : baseRateMax;
530      var pts = pointsFor(price, rate);
531      if (!(pts >= 1)) {
532        if (!signupOnly()) { el.hidden = true; }
533        return;
534      }
535      el.textContent = '';
536      var earn = document.createElement('span');
537      earn.className = 'sst-pi-points__earn';
538      earn.textContent = (el.getAttribute('data-tpl-earn') || 'Earn %s pts')
539        .replace('%s', pts.toLocaleString('en-US'));
540      el.appendChild(earn);
541      if (!state.loggedIn) {
542        var signin = document.createElement('a');
543        signin.className = 'sst-pi-points__signin';
544        signin.href = el.getAttribute('data-signin-url') || '/my-account/';
545        signin.textContent = el.getAttribute('data-tpl-signin') || 'Sign in to collect points';
546        el.appendChild(signin);
547      }
548      appendSignup();
549      el.hidden = false;
550    }
551
552    function render() {
553      if (!state.ready || !state.live || !(price > 0)) {
554        if (!signupOnly()) { el.hidden = true; }
555        return;
556      }
557      if (!state.lineModel) {
558        renderLegacy();
559        return;
560      }
561      if (!(baseRate > 0) || !(state.mult > 0)) {
562        if (!signupOnly()) { el.hidden = true; }
563        return;
564      }
565      var pts = pointsFor(price, lineRate(baseRate, state.mult));
566      if (!(pts >= 1)) {
567        if (!signupOnly()) { el.hidden = true; }
568        return;
569      }
570      var label = fillTpl(
571        el.getAttribute('data-tpl-earn-rate') || 'Earn %1$s pts (%2$s \u00d7 %3$s)',
572        [pts.toLocaleString('en-US'), ratePercentLabel(baseRate), multiplierNumber(state.mult)]
573      );
574      el.textContent = '';
575      var earn = document.createElement('span');
576      earn.className = 'sst-pi-points__earn';
577      earn.textContent = label;
578      el.appendChild(earn);
579      if (!state.loggedIn) {
580        var signin = document.createElement('a');
581        signin.className = 'sst-pi-points__signin';
582        signin.href = el.getAttribute('data-signin-url') || '/my-account/';
583        signin.textContent = (isFinite(multMax) && multMax > 1)
584          ? (el.getAttribute('data-tpl-signin-rank') || 'Sign in to collect points \u2014 up to \u00d7%s with rank')
585            .split('%s').join(multiplierNumber(multMax))
586          : (el.getAttribute('data-tpl-signin') || 'Sign in to collect points');
587        el.appendChild(signin);
588      }
589      appendSignup();
590      el.hidden = false;
591    }
592
593    function gotMe(data) {
594      state.ready = true;
595      state.live = !!(data && data.live);
596      state.loggedIn = !!(data && data.loggedIn);
597      state.mult = meMultiplier(data, baseRateMax);
598      state.lineModel = !!(data && data.lineModel === true);
599      state.rate = (data && typeof data.rate === 'number' && isFinite(data.rate)) ? data.rate : 0;
600      render();
601    }
602
603    // Only an overlapping points GET may be shared with the header. Completed
604    // answers and the header's session cache are never reused here.
605    var pointsRequests = window.sstPointsRequests;
606    function fetchMe() {
607      if (!window.fetch) return;
608      var started = pointsRequests ? pointsRequests.generation() : 0;
609      var options = {
610        method: 'GET',
611        credentials: 'same-origin',
612        headers: { Accept: 'application/json' }
613      };
614      var request = pointsRequests
615        ? pointsRequests.request(el.getAttribute('data-me'), options)
616        : fetch(el.getAttribute('data-me'), options).then(function (res) {
617          if (!res.ok) { throw new Error('me:' + res.status); }
618          return res.json();
619        });
620      request.then(function (data) {
621        if (pointsRequests && started !== pointsRequests.generation()) return;
622        if (data && typeof data.live === 'boolean') { gotMe(data); }
623      }).catch(function () { /* stay hidden */ });
624    }
625    if (pointsRequests) {
626      pointsRequests.onInvalidate(function () {
627        state.ready = false;
628        render();
629        fetchMe();
630      });
631    }
632    fetchMe();
633
634    // Variable products: follow the selected variation's live price; a reset
635    // falls back to the range floor the server rendered.
636    if (window.jQuery) {
637      window.jQuery('form.variations_form')
638        .on('found_variation', function (e, variation) {
639          if (variation && typeof variation.display_price !== 'undefined') {
640            price = parseFloat(variation.display_price) || 0;
641            // No per-variation rate (stale variation data) -> hide, never guess.
642            baseRate = (typeof variation.sst_points_base_rate !== 'undefined')
643              ? validBaseRate(variation.sst_points_base_rate) : 0;
644            render();
645          }
646        })
647        .on('reset_data', function () {
648          price = basePrice;
649          baseRate = pageBaseRate;
650          render();
651        });
652    }
653  }
654
655  // ------------------------------------------------------------------
656  // QO CTA clicks (v0.4.0)
657  // ------------------------------------------------------------------
658
659  function wireQoCtaTracking() {
660    document.addEventListener('click', function (e) {
661      var a = e.target && e.target.closest ? e.target.closest('[data-sst-pi-cta]') : null;
662      if (!a) { return; }
663      track({
664        event: 'select_content',
665        content_type: 'pdp_qo_cta',
666        item_id: a.getAttribute('data-sst-pi-cta') + ':' + (a.getAttribute('data-sst-pi-cta-pos') || '')
667      });
668    }, true);
669  }
670
671  // ------------------------------------------------------------------
672  // Reviews expander (v0.4.0) — long lists clamp to the first few reviews.
673  // ------------------------------------------------------------------
674
675  function wireReviewsExpander() {
676    var section = document.querySelector('[data-sst-pi-reviews]');
677    if (!section) { return; }
678    var list = section.querySelector('.commentlist');
679    if (!list) { return; }
680    var items = list.children;
681    var visible = parseInt(section.getAttribute('data-visible'), 10) || 6;
682    if (items.length <= visible) { return; }
683    // A #comment-N permalink (review-approved mails link like this) must not
684    // land on a display:none review — skip clamping for that visit.
685    if (/^#comment-\d+$/.test(window.location.hash) && section.querySelector(window.location.hash)) { return; }
686    for (var i = visible; i < items.length; i++) {
687      items[i].classList.add('sst-pi-reviews__clamped');
688    }
689    var btn = document.createElement('button');
690    btn.type = 'button';
691    btn.className = 'sst-pi-reviews__showall';
692    btn.textContent = (section.getAttribute('data-tpl-showall') || 'Show all %s reviews')
693      .replace('%s', String(items.length));
694    btn.addEventListener('click', function () {
695      for (var j = 0; j < items.length; j++) {
696        items[j].classList.remove('sst-pi-reviews__clamped');
697      }
698      btn.parentNode.removeChild(btn);
699    });
700    list.parentNode.appendChild(btn);
701  }
702
703  // Duty note: start the edge geo lookup at parse time (footer script — DOM
704  // is already built), so the reveal usually lands before first paint settles.
705  try { wireDutyNote(); } catch (e) { /* never break the page */ }
706
707  ready(function () {
708    // Each wiring is independent — one throwing must not kill the rest.
709    [wireListTracking, wireBulkAdd, wireRecentlyViewed, wirePointsLine, wireQoCtaTracking, wireReviewsExpander].forEach(function (fn) {
710      try { fn(); } catch (e) { /* never break the page */ }
711    });
712  });
713})();

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.