PageSourceSearch

https://kievstandard.com.ua/wp-content/plugins/pogoda-main/assets/js/pogoda.js?ver=1.16.1

js kievstandard.com.ua collected 2026-10-07 10:15:57 UTC 46,571 bytes, 971 lines download raw bytes

1/* Pogoda — фронтова логіка віджетів.
2 * Погода: перемикач міст (вибір у localStorage), геолокація відвідувача
3 * (кнопка або авто; координати у localStorage до зміни міста вручну),
4 * дотягування свіжих даних через REST, коли серверний рендер застарів
5 * (кеш сторінок). Втрати: оновлення після TTL. Курс валют і метали: те саме
6 * після TTL. Конвертер: рахунок у браузері за курсами зі сторінки. Карта:
7 * завантаження за кліком.
8 * Плаваючі плашки: закриття до кінця сесії (окремо для кожного віджета);
9 * на телефоні й планшеті (≤1023px) — док: плашки сховані, внизу рядок вкладок, дотик
10 * відкриває одну плашку, ✕ у рядку ховає док до кінця сесії. */
11(function () {
12  'use strict';
13  var cfg = window.pogodaCfg || {};
14  var KEY_CITY = 'pogoda_city';
15  var KEY_GEO = 'pogoda_geo';
16  var KEY_GEO_DENIED = 'pogoda_geo_denied';
17
18  function store(kind, key, value) {
19    try {
20      var s = kind === 'session' ? window.sessionStorage : window.localStorage;
21      if (value === undefined) { return s.getItem(key); }
22      if (value === null) { s.removeItem(key); return null; }
23      s.setItem(key, value);
24    } catch (e) { /* приватний режим / вимкнене сховище */ }
25    return null;
26  }
27  function q(el, sel) { return el.querySelector(sel); }
28  function setText(el, sel, text) {
29    var n = q(el, sel);
30    if (n) { n.textContent = text == null ? '' : text; }
31  }
32  /* Заголовок плашки. У бічній колонці з увімкненим виглядом його малює тема:
33     він стоїть ПЕРЕД плашкою (data-native="1") у розмітці before_title теми,
34     без наших класів — оновлюємо перший непорожній текстовий вузол. */
35  function setTitle(card, sel, text, d) {
36    if (text == null || text === '') { return; }
37    if (card.getAttribute('data-native') !== '1') { setText(card, sel, text); return; }
38    // Нативний заголовок короткий (без дати) — дата стоїть підписом у картці.
39    var dateEl = q(card, '.pogoda-head-date');
40    if (dateEl && d && d.title_short) {
41      text = d.title_short;
42      if (d.date_label) { dateEl.textContent = d.date_label; }
43    }
44    if (!document.createTreeWalker) { return; }
45    // after_title теми може закінчуватись декоративним елементом без тексту
46    // (лінія під заголовком) — тоді заголовок на один-два елементи раніше.
47    var host = card.previousElementSibling;
48    for (var hop = 0; host && hop < 3; hop++, host = host.previousElementSibling) {
49      if (host.classList && host.classList.contains('pogoda')) { return; } // це вже сусідня плашка
50      var walker = document.createTreeWalker(host, 4 /* NodeFilter.SHOW_TEXT */, null, false);
51      var node;
52      while ((node = walker.nextNode())) {
53        if (node.nodeValue && node.nodeValue.replace(/\s+/g, '') !== '') { node.nodeValue = text; return; }
54      }
55    }
56  }
57  function restUrl(base, params) {
58    var qs = [];
59    for (var k in params) {
60      if (Object.prototype.hasOwnProperty.call(params, k) && params[k] !== undefined && params[k] !== null) {
61        qs.push(encodeURIComponent(k) + '=' + encodeURIComponent(params[k]));
62      }
63    }
64    return base + (base.indexOf('?') > -1 ? '&' : '?') + qs.join('&');
65  }
66  function getJSON(url) {
67    return fetch(url, { credentials: 'omit', headers: { 'Accept': 'application/json' } })
68      .then(function (r) { return r.ok ? r.json() : null; });
69  }
70  /* Мова плашки → параметр lang кожного REST-запиту: адреса /wp-json/… мовної
71     версії не має, а підписи у відповіді приходять готовими. Без цього плашка
72     на /ru/ після фонового оновлення ставала наполовину українською. */
73  function withLang(card, params) {
74    var lang = card.getAttribute('data-lang');
75    if (lang) { params.lang = lang; }
76    return params;
77  }
78  function isExpired(card) {
79    var ts = parseInt(card.getAttribute('data-ts'), 10) || 0;
80    var ttl = parseInt(card.getAttribute('data-ttl'), 10) || 1800;
81    return (Date.now() / 1000 - ts) > ttl || card.classList.contains('pogoda-stale') || card.classList.contains('pogoda-unavailable');
82  }
83
84  /* ── Погода ── */
85  function applyWeather(card, d) {
86    var f = d.fmt || {};
87    card.classList.toggle('pogoda-unavailable', !d.ok);
88    card.classList.toggle('pogoda-stale', !!d.stale);
89    card.setAttribute('data-ts', String(d.fetched_at || 0));
90    var icon = q(card, '.pogoda-main-icon-wrap');
91    if (icon && f.icon_svg) { icon.innerHTML = f.icon_svg; }
92    setText(card, '.pogoda-temp', f.temp || '—');
93    setText(card, '.pogoda-wind .pogoda-val', f.wind);
94    setText(card, '.pogoda-wind-dir', f.wind_dir);
95    setText(card, '.pogoda-humidity .pogoda-val', f.humidity);
96    setText(card, '.pogoda-pressure .pogoda-val', f.pressure);
97    setText(card, '.pogoda-desc', f.desc);
98    setText(card, '.pogoda-feels-val', f.feels);
99    var feels = q(card, '.pogoda-feels');
100    if (feels) { feels.hidden = !d.ok; }
101    var sel = q(card, '.pogoda-city-select');
102    if (d.geo) {
103      card.classList.add('pogoda-is-geo');
104      if (sel) {
105        var opt = sel.querySelector('option[value="geo"]');
106        if (!opt) { opt = document.createElement('option'); opt.value = 'geo'; sel.appendChild(opt); }
107        opt.textContent = '⌖ ' + (d.label || '');
108        sel.value = 'geo';
109      }
110      setText(card, '.pogoda-city-name', d.label);
111    } else {
112      card.classList.remove('pogoda-is-geo');
113      card.setAttribute('data-city', String(d.city));
114      if (sel) {
115        var g = sel.querySelector('option[value="geo"]');
116        if (g) { g.parentNode.removeChild(g); }
117        sel.value = String(d.city);
118      }
119      setText(card, '.pogoda-city-name', d.label);
120    }
121    dockRefresh(card);
122  }
123  function loadWeather(card, params) {
124    if (!cfg.rest || !window.fetch) { return; }
125    card.classList.add('pogoda-loading');
126    getJSON(restUrl(cfg.rest, withLang(card, params)))
127      .then(function (d) { if (d && typeof d.ok !== 'undefined' && (d.ok || d.error !== 'rate_limited')) { applyWeather(card, d); } })
128      .catch(function () { /* залишаємо серверний рендер */ })
129      .then(function () { card.classList.remove('pogoda-loading'); });
130  }
131  function storedGeo() {
132    try { var g = JSON.parse(store('local', KEY_GEO) || 'null'); return (g && typeof g.lat === 'number' && typeof g.lon === 'number') ? g : null; } catch (e) { return null; }
133  }
134  function applyGeoToAll(c) {
135    var cards = document.querySelectorAll('.pogoda[data-widget="weather"]');
136    for (var i = 0; i < cards.length; i++) {
137      if ((cards[i].getAttribute('data-geo') || 'off') !== 'off') { loadWeather(cards[i], { lat: c.lat, lon: c.lon }); }
138    }
139  }
140  function geoRequest(card) {
141    if (!navigator.geolocation || !window.isSecureContext) { return; }
142    card.classList.add('pogoda-geo-busy');
143    navigator.geolocation.getCurrentPosition(function (p) {
144      var c = { lat: Math.round(p.coords.latitude * 100) / 100, lon: Math.round(p.coords.longitude * 100) / 100 };
145      store('local', KEY_GEO, JSON.stringify(c));
146      store('local', KEY_GEO_DENIED, null);
147      card.classList.remove('pogoda-geo-busy');
148      applyGeoToAll(c);
149    }, function () {
150      store('local', KEY_GEO_DENIED, String(Date.now()));
151      card.classList.remove('pogoda-geo-busy');
152    }
152, { enableHighAccuracy: false, timeout: 8000, maximumAge: 3600000 });
153  }
154  function initWeather(card) {
155    var sel = q(card, '.pogoda-city-select');
156    var idx = card.getAttribute('data-city') || '0';
157    var geoMode = card.getAttribute('data-geo') || 'off';
158    var geo = geoMode !== 'off' ? storedGeo() : null;
159    var storedCity = store('local', KEY_CITY);
160
161    if (geo) {
162      loadWeather(card, { lat: geo.lat, lon: geo.lon });
163    } else if (sel && storedCity !== null && storedCity !== idx && sel.querySelector('option[value="' + storedCity + '"]')) {
164      sel.value = storedCity;
165      loadWeather(card, { city: storedCity });
166    } else if (isExpired(card)) {
167      loadWeather(card, { city: idx });
168    }
169    if (geoMode === 'auto' && !geo && storedCity === null) {
170      var denied = parseInt(store('local', KEY_GEO_DENIED), 10) || 0;
171      if (Date.now() - denied > 7 * 864e5) { geoRequest(card); }
172    }
173    if (sel) {
174      sel.addEventListener('change', function () {
175        if (sel.value === 'geo') {
176          var g = storedGeo();
177          if (g) { loadWeather(card, { lat: g.lat, lon: g.lon }); }
178          return;
179        }
180        store('local', KEY_CITY, sel.value);
181        store('local', KEY_GEO, null);
182        loadWeather(card, { city: sel.value });
183      });
184    }
185    var geoBtn = q(card, '.pogoda-geo');
186    if (geoBtn) {
187      if (!window.isSecureContext || !navigator.geolocation) { geoBtn.hidden = true; }
188      geoBtn.addEventListener('click', function () { geoRequest(card); });
189    }
190  }
191
192  /* ── Втрати ворога ── */
193  function safeUrl(u) {
194    return (typeof u === 'string' && /^https?:\/\//i.test(u)) ? u : '';
195  }
196  function lossRow(r, withIcon) {
197    var li = document.createElement('li');
198    li.className = 'pogoda-loss pogoda-loss-' + r.key;
199    if (withIcon && r.icon) {
200      var ic = document.createElement('span');
201      ic.className = 'pogoda-loss-icon';
202      ic.innerHTML = r.icon;
203      li.appendChild(ic);
204    }
205    var body = document.createElement('span');
206    body.className = 'pogoda-loss-body';
207    var num = document.createElement('span');
208    num.className = 'pogoda-loss-num';
209    var total = document.createElement('span');
210    total.className = 'pogoda-loss-total';
211    total.textContent = r.total;
212    var inc = document.createElement('em');
213    inc.className = 'pogoda-loss-inc';
214    inc.textContent = r.inc || '';
215    inc.hidden = !r.inc;
216    num.appendChild(total);
217    num.appendChild(document.createTextNode(' '));
218    num.appendChild(inc);
219    var label = document.createElement('span');
220    label.className = 'pogoda-loss-label';
221    label.textContent = r.label;
222    body.appendChild(num);
223    body.appendChild(label);
224    li.appendChild(body);
225    return li;
226  }
227  function applyLosses(card, d) {
228    if (!d || !d.ok) { return; }
229    var rows = d.rows || [];
230    card.classList.toggle('pogoda-stale', !!d.stale);
231    card.setAttribute('data-ts', String(d.fetched_at || 0));
232    setTitle(card, '.pogoda-losses-title', d.title, d);
233
234    // День війни: підпис приходить готовим з сервера — інакше він
235    // розійдеться з датою в заголовку після фонового оновлення.
236    var head = q(card, '.pogoda-head');
237    var dayEl = q(card, '.pogoda-losses-day');
238    if (!dayEl && head && d.show_day && d.day_label) {
239      dayEl = document.createElement('span');
240      dayEl.className = 'pogoda-losses-day';
241      head.insertBefore(dayEl, q(head, '.pogoda-close'));
242    }
243    if (dayEl) { dayEl.textContent = d.day_label || ''; }
244
245    var src = q(card, '.pogoda-losses-source');
246    if (src && safeUrl(d.source_url)) { src.setAttribute('href', safeUrl(d.source_url)); }
247
248    if (!rows.length) { return; }
249    // Плашка могла відрендеритися без даних (джерело лежало чи
250    // сторінка з кешу) — тоді список будуємо з відповіді REST.
251    var list = q(card, '.pogoda-losses-list');
252    if (!list) {
253      list = document.createElement('ul');
254      list.className = 'pogoda-losses-list';
255      var empty = q(card, '.pogoda-losses-empty');
256      if (empty) { empty.parentNode.replaceChild(list, empty); }
257      else { card.insertBefore(list, q(card, '.pogoda-foot')); }
258    }
259    rows.forEach(function (r) {
260      var li = q(list, '.pogoda-loss-' + r.key);
261      if (!li) { list.appendChild(lossRow(r, !!d.show_icons)); return; }
262      setText(li, '.pogoda-loss-total', r.total);
263      var inc = q(li, '.pogoda-loss-inc');
264      if (inc) { inc.textContent = r.inc || ''; inc.hidden = !r.inc; }
265    });
266    card.classList.remove('pogoda-unavailable');
267  }
268  function initLosses(card) {
269    if (!cfg.losses || !window.fetch || !isExpired(card)) { return; }
270    getJSON(restUrl(cfg.losses, withLang(card, { items: card.getAttribute('data-items') || '' })))
271      .then(function (d) { applyLosses(card, d); })
272      .catch(function () {});
273  }
274
275  /* ── Карта ── */
276  function initMap(card) {
277    var btn = q(card, '.pogoda-map-load');
278    if (!btn) { return; }
279    btn.addEventListener('click', function () {
280      var f = document.createElement('iframe');
281      f.src = btn.getAttribute('data-src');
282      f.title = 'DeepStateMap';
283      f.setAttribute('allowfullscreen', '');
284      f.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin');
285      btn.parentNode.replaceChild(f, btn);
286      card.classList.remove('pogoda-map-click');
287      card.classList.add('pogoda-map-eager');
288    });
289  }
290
291  /* ── Гроші: курс валют, метали, конвертер ── */
292  function parseJSONAttr(el, attr) {
293    try { return JSON.parse(el.getAttribute(attr) || '{}') || {}; } catch (e) { return {}; }
294  }
295  /* Те саме форматування, що й на сервері (pogoda_fmt_num): інакше після
296     першого натискання клавіші число «стрибає» в інший вигляд. */
297  function fmtNum(value, dec, decSep, thousands) {
298    var neg = value < 0;
299    var parts = Math.abs(value).toFixed(dec).split('.');
300    parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousands || '');
301    return (neg ? '−' : '') + parts.join(decSep || ',');
302  }
303  /* Як pogoda_money_scale(): дрібні курси котируємо за 10/100/1000 одиниць,
304     а курс близько одиниці лишаємо як є — «1 USD = 0,86 €» читається краще,
305     ніж «10 USD = 8,62 €». */
306  function scaleMult(rate) {
307    if (!(rate > 0) || rate >= 0.2) { return 1; }
308    var steps = [10, 100, 1000];
309    var m = 1;
310    for (var i = 0; i < steps.length; i++) { m = steps[i]; if (rate * m >= 1) { break; } }
311    return m;
312  }
313  function rateDecimals(v) {
314    v = Math.abs(v);
315    if (v >= 1000) { return 0; }
316    return v >= 1 ? 2 : 4;
317  }
318  function setDir(el, prefix, dir) {
319    el.classList.remove(prefix + 'up', prefix + 'down');
320    if (dir) { el.classList.add(prefix + dir); }
321  }
322
323  function ratesRow(r) {
324    var li = document.createElement('li');
325    li.className = 'pogoda-rate pogoda-rate-' + String(r.code).toLowerCase() + (r.dir ? ' pogoda-rate-' + r.dir : '');
326    if (r.title) { li.title = r.title; }
327    var cur = document.createElement('span');
328    cur.className = 'pogoda-rate-cur';
329    if (r.flag) {
330      var fl = document.createElement('span');
331      fl.className = 'pogoda-rate-flag';
332      fl.setAttribute('aria-hidden', 'true');
333      fl.textContent = r.flag;
334      cur.appendChild(fl);
335    }
336    var code = document.createElement('span');
337    code.className = 'pogoda-rate-code';
338    code.textContent = r.unit;
339    cur.appendChild(code);
340    var vals = document.createElement('span');
341    vals.className = 'pogoda-rate-vals';
342    if (r.buy) {
343      var b = document.createElement('span');
344      b.className = 'pogoda-rate-buy';
345      b.textContent = r.buy;
346      var sl = document.createElement('span');
347      sl.className = 'pogoda-rate-sell';
348      sl.textContent = r.sell;
349      vals.appendChild(b);
350      vals.appendChild(sl);
351    } else {
352      var v = document.createElement('span');
353      v.className = 'pogoda-rate-value';
354      v.textContent = r.value;
355      vals.appendChild(v);
356    }
357    var d = document.createElement('em');
358    d.className = 'pogoda-rate-delta';
359    d.textContent = r.delta || '';
360    d.hidden = !r.delta;
361    li.appendChild(cur);
362    li.appendChild(vals);
363    li.appendChild(d);
364    return li;
365  }
366  function ensureList(card, cls, emptyCls) {
367    var list = q(card, '.' + cls);
368    if (list) { return list; }
369    list = document.createElement('ul');
370    list.className = cls;
371    var empty = q(card, '.' + emptyCls);
372    if (empty) { empty.parentNode.replaceChild(list, empty); }
373    else { card.insertBefore(list, q(card, '.pogoda-foot')); }
374    return list;
375  }
376  function applyRates(card, d) {
377    if (!d || !d.ok || !d.rows || !d.rows.length) { return; }
378    card.classList.toggle('pogoda-stale', !!d.stale);
379    card.classList.remove('pogoda-unavailable');
380    card.setAttribute('data-ts', String(d.fetched_at || 0));
381    setTitle(card, '.pogoda-rates-title', d.title, d);
382    var src = q(card, '.pogoda-rates-source');
383    if (src) {
384      if (d.source) { src.textContent = d.source; }
385      if (safeUrl(d.source_url)) { src.setAttribute('href', safeUrl(d.source_url)); }
386    }
387    var list = ensureList(card, 'pogoda-rates-list', 'pogoda-rates-empty');
388    d.rows.forEach(function (r) {
389      var li = q(list, '.pogoda-rate-' + String(r.code).toLowerCase());
390      if (!li) { list.appendChild(ratesRow(r)); return; }
391      setDir(li, 'pogoda-rate-', r.dir);
392      setText(li, '.pogoda-rate-code', r.unit);
393      if (r.buy) {
394        setText(li, '.pogoda-rate-buy', r.buy);
395        setText(li, '.pogoda-rate-sell', r.sell);
396      } else {
397        setText(li, '.pogoda-rate-value', r.value);
398      }
399      var dl = q(li, '.pogoda-rate-delta');
400      if (dl) { dl.textContent = r.delta || ''; dl.hidden = !r.delta; }
401    });
402    dockRefresh(card);
403  }
404  function initRates(card) {
405    if (!cfg.rates || !window.fetch || !isExpired(card)) { return; }
406    getJSON(restUrl(cfg.rates, withLang(card, { items: card.getAttribute('data-items') || '', base: card.getAttribute('data-base') || '' })))
407      .then(function (d) { applyRates(card, d); })
408      .catch(function () {});
409  }
410
411  function metalsRow(r) {
412    var li = document.createElement('li');
413    li.className = 'pogoda-metal pogoda-metal-' + String(r.code).toLowerCase() + (r.dir ? ' pogoda-metal-' + r.dir : '');
414    if (r.title) { li.title = r.title; }
415    var name = document.createElement('span');
416    name.className = 'pogoda-metal-name';
417    if (r.icon) {
418      var ic = document.createElement('span');
419      ic.className = 'pogoda-metal-icon';
420      ic.innerHTML = r.icon;
421      name.appendChild(ic);
422    }
423    var lb = document.createElement('span');
424    lb.className = 'pogoda-metal-label';
425    lb.textContent = r.label;
426    name.appendChild(lb);
427    var vals = document.createElement('span');
428    vals.className = 'pogoda-metal-vals';
429    var price = document.createElement('span');
430    price.className = 'pogoda-metal-price';
431    price.textContent = r.price + ' ';
432    var cur = document.createElement('span');
433    cur.className = 'pogoda-metal-cur';
434    cur.textContent = r.cur;
435    price.appendChild(cur);
436    vals.appendChild(price);
437    if (r.unit) {
438      var u = document.createElement('span');
439      u.className = 'pogoda-metal-unit';
440      u.textContent = '/ ' + r.unit;
441      vals.appendChild(u);
442    }
443    var d = document.createElement('em');
444    d.className = 'pogoda-metal-delta';
445    d.textContent = r.delta || '';
446    d.hidden = !r.delta;
447    li.appendChild(name);
448    li.appendChild(vals);
449    li.appendChild(d);
450    return li;
451  }
452  function applyMetals(card, d) {
453    if (!d || !d.ok || !d.rows || !d.rows.length) { return; }
454    card.classList.toggle('pogoda-stale', !!d.stale);
455    card.classList.remove('pogoda-unavailable');
456    card.setAttribute('data-ts', String(d.fetched_at || 0));
457    setTitle(card, '.pogoda-metals-title', d.title, d);
458    var src = q(card, '.pogoda-metals-source');
459    if (src && safeUrl(d.source_url)) { src.setAttribute('href', safeUrl(d.source_url)); }
460    var list = ensureList(card, 'pogoda-metals-list', 'pogoda-metals-empty');
461    d.rows.forEach(function (r) {
462      var li = q(list, '.pogoda-metal-' + String(r.code).toLowerCase());
463      if (!li) { list.appendChild(metalsRow(r)); return; }
464      setDir(li, 'pogoda-metal-', r.dir);
465      var price = q(li, '.pogoda-metal-price');
466      if (price) {
467        var cur = q(price, '.pogoda-metal-cur');
468        price.textContent = r.price + ' ';
469        if (cur) { cur.textContent = r.cur; price.appendChild(cur); }
470      }
471      var dl = q(li, '.pogoda-metal-delta');
472      if (dl) { dl.textContent = r.delta || ''; dl.hidden = !r.delta; }
473    });
474    dockRefresh(card);
475  }
476  function initMetals(card) {
477    if (!cfg.metals || !window.fetch || !isExpired(card)) { return; }
478    getJSON(restUrl(cfg.metals, withLang(card, { items: card.getAttribute('data-items') || '', unit: card.getAttribute('data-unit') || '' })))
479      .then(function (d) { applyMetals(card, d); })
480      .catch(function () {});
481  }
482
483  function initConverter(card) {
484    var amountEl = q(card, '.pogoda-conv-amount');
485    var fromEl = q(card, '.pogoda-conv-from');
486    var toEl = q(card, '.pogoda-conv-to');
487    var outEl = q(card, '.pogoda-conv-result');
488    if (!amountEl || !fromEl || !toEl || !outEl) { return; }
489    var rateEl = q(card, '.pogoda-conv-rate');
490    var swapEl = q(card, '.pogoda-conv-swap');
491    var decSep = card.getAttribute('data-dec-sep') || ',';
492    var thousands = card.getAttribute('data-thousands') || '';
493    var rates = parseJSONAttr(card, 'data-rates');
494    var syms = parseJSONAttr(card, 'data-sym');
495    var decs = parseJSONAttr(card, 'data-dec');
496
497    function parseAmount(txt) {
498      txt = String(txt == null ? '' : txt);
499      if (thousands) { txt = txt.split(thousands).join(''); }
500      txt = txt.replace(/[\s   ]/g, '').replace(/,/g, '.').replace(/[^0-9.]/g, '');
501      var dot = txt.indexOf('.');
502      if (dot > -1) { txt = txt.slice(0, dot + 1) + txt.slice(dot + 1).replace(/\./g, ''); }
503      var v = parseFloat(txt);
504      return isFinite(v) && v >= 0 ? v : 0;
505    }
506    function decFor(code, value) {
507      var d = (typeof decs[code] === 'number') ? decs[code] : 2;
508      if (Math.abs(value) < 1 && d < 4) { d = 4; }
509      return d;
510    }
511    function recalc() {
512      var from = fromEl.value;
513      var to = toEl.value;
514      var rf = rates[from];
515      var rt = rates[to];
516      if (!rf || !rt) { outEl.textContent = '—'; if (rateEl) { rateEl.hidden = true; } return; }
517      var res = parseAmount(amountEl.value) * rf / rt;
518      outEl.textContent = fmtNum(res, decFor(to, res), decSep, thousands);
519      if (rateEl) {
520        var one = rf / rt;
521        var m = scaleMult(one);
522        rateEl.textContent = (m > 1 ? fmtNum(m, 0, decSep, thousands) : '1') + ' ' + from + ' = '
523          + fmtNum(one * m, rateDecimals(one * m), decSep, thousands) + ' ' + (syms[to] || to);
524        rateEl.hidden = false;
525      }
526    }
527    amountEl.addEventListener('input', recalc);
528    fromEl.addEventListener('change', recalc);
529    toEl.addEventListener('change', recalc);
530    if (swapEl) {
531      swapEl.addEventListener('click', function () {
532        var t = fromEl.value;
533        fromEl.value = toEl.value;
534        toEl.value = t;
535        recalc();
536      });
537    }
538    if (cfg.rates && window.fetch && isExpired(card)) {
539      getJSON(restUrl(cfg.rates, withLang(card, { w: 'converter', items: card.getAttribute('data-items') || '', base: card.getAttribute('data-base') || '' })))
540        .then(function (d) {
541          if (!d || !d.ok || !d.map) { return; }
542          for (var k in d.map) {
543            if (Object.prototype.hasOwnProperty.call(d.map, k)) { rates[k] = d.map[k]; }
544          }
545          card.setAttribute('data-ts', String(d.fetched_at || 0));
546          card.classList.toggle('pogoda-stale', !!d.stale);
547          if (d.title) { setTitle(card, '.pogoda-converter-title', d.title, d); }
548          recalc();
549        })
550        .catch(function () {});
551    }
552  }
553
554  /* ── Калькулятори ──────────────────────────────────────────────────────
555     Ті самі формули, що в includes/calc.php (pogoda_calc_compute): сервер
556     малює перший результат, браузер перераховує під час набору. Якщо тут
557     щось міняється — міняти і там, інакше перше число «стрибне». */
558  function calcRun(kind, v, ctx) {
559    var out = {};
560    var money = function (x, dec) { return fmtNum(x, dec === undefined ? 2 : dec, ctx.sep, ctx.tho) + ' ' + ctx.cur; };
561    var n = function (name) { return parseFloat(String(v[name] === undefined ? 0 : v[name]).replace(',', '.')) || 0; };
562    var u = function (key) { return ctx.units[key] || ''; };
563    var sum, rate, term, i, pay, total, income, tax, net, vat, gross, liters, cost, area, one, pieces, vol;
564
565    switch (kind) {
566      case 'loan':
567        sum = Math.max(0, n('sum')); rate = Math.max(0, n('rate')); term = Math.max(1, Math.round(n('term')));
568        i = rate / 1200;
569        pay = i > 0 ? sum * i / (1 - Math.pow(1 + i, -term)) : sum / term;
570        out.payment = money(pay); out.total = money(pay * term); out.overpay = money(pay * term - sum);
571        break;
572      case 'deposit':
573        sum = Math.max(0, n('sum')); rate = Math.max(0, n('rate')); term = Math.max(1, Math.round(n('term')));
574        total = (v.cap === 'none') ? sum * (1 + rate / 100 * term / 12) : sum * Math.pow(1 + rate / 1200, term);
575        income = total - sum; tax = income * 0.23;
576        out.income = money(income); out.total = money(sum + income - tax); out.tax = money(tax);
577        break;
578      case 'vat':
579        sum = Math.max(0, n('sum')); rate = Math.max(0, n('rate'));
580        if (v.mode === 'extract') { net = sum / (1 + rate / 100); vat = sum - net; gross = sum; }
581        else { net = sum; vat = sum * rate / 100; gross = sum + vat; }
582        out.vat = money(vat); out.net = money(net); out.gross = money(gross);
583        break;
584      case 'fuel':
585        liters = Math.max(0, n('distance')) * Math.max(0, n('rate')) / 100;
586        cost = liters * Math.max(0, n('price'));
587        out.liters = fmtNum(liters, 1, ctx.sep, ctx.tho) + ' ' + u('l');
588        out.cost = money(cost);
589        out.per_person = money(cost / Math.max(1, Math.round(n('people'))));
590        break;
591      case 'customs': {
592        var price = Math.max(0, n('price'));
593        var engine = Math.max(0, n('engine'));
594        var age = Math.max(1, Math.min(15, new Date().getFullYear() - (n('year') > 1900 ? n('year') : new Date().getFullYear())));
595        var excise;
596        if (v.fuel === 'electro') { excise = 60; }
597        else if (v.fuel === 'hybrid') { excise = 100; }
598        else {
599          var baseRate = (v.fuel === 'diesel') ? (engine <= 3500 ? 75 : 150) : (engine <= 3000 ? 50 : 100);
600          excise = baseRate * (engine / 1000) * age;
601        }
602        var duty = (v.origin === 'eu') ? 0 : price * 0.10;
603        var cvat = (price + duty + excise) * 0.20;
604        var ctotal = duty + excise + cvat;
605        var eur = function (x) { return fmtNum(x, 0, ctx.sep, ctx.tho) + ' €'; };
606        out.duty = eur(duty); out.excise = eur(excise); out.vat = eur(cvat); out.total = eur(ctotal);
607        if (ctx.eur > 0) { out.total_local = money(ctotal * ctx.eur, 0); }
608        break;
609      }
610      case 'bmi': {
611        var h = Math.max(0.5, n('height') / 100);
612        var w = Math.max(1, n('weight'));
613        var bmi = w / (h * h);
614        var cat = bmi < 18.5 ? 0 : (bmi < 25 ? 1 : (bmi < 30 ? 2 : (bmi < 35 ? 3 : (bmi < 40 ? 4 : 5))));
615        out.bmi = fmtNum(bmi, 1, ctx.sep, ctx.tho);
616        out.category = (ctx.cats && ctx.cats[cat]) || '';
617        out.normal = fmtNum(18.5 * h * h, 1, ctx.sep, ctx.tho) + '–' + fmtNum(24.9 * h * h, 1, ctx.sep, ctx.tho) + ' ' + u('kg');
618        break;
619      }
620      case 'calories': {
621        var bmr = 10 * Math.max(1, n('weight')) + 6.25 * Math.max(1, n('height')) - 5 * Math.max(1, n('age')) + (v.sex === 'f' ? -161 : 5);
622        var k = { none: 1.2, low: 1.375, mid: 1.55, high: 1.725, max: 1.9 };
623        var norm = bmr * (k[v.activity] || 1.375);
624        out.bmr = fmtNum(bmr, 0, ctx.sep, ctx.tho) + ' ' + u('kcal');
625        out.norm = fmtNum(norm, 0, ctx.sep, ctx.tho) + ' ' + u('kcal');
626        out.loss = fmtNum(norm * 0.85, 0, ctx.sep, ctx.tho) + ' ' + u('kcal');
627        break;
628      }
629      case 'tile':
630        area = Math.max(0, n('length')) * Math.max(0, n('width'));
631        one = (Math.max(1, n('tile_w')) / 100) * (Math.max(1, n('tile_h')) / 100);
632        pieces = one > 0 ? Math.ceil(area * (1 + Math.max(0, n('spare')) / 100) / one) : 0;
633        out.area = fmtNum(area, 2, ctx.sep, ctx.tho) + ' ' + u('m2');
634        out.pieces = fmtNum(pieces, 0, ctx.sep, ctx.tho) + ' ' + u('pcs');
635        out.area_total = fmtNum(pieces * one, 2, ctx.sep, ctx.tho) + ' ' + u('m2');
636        break;
637      case 'concrete': {
638        vol = Math.max(0, n('length')) * Math.max(0, n('width')) * Math.max(0, n('height'));
639        var mix = { m100: [210, 870, 1080], m200: [280, 800, 1080], m300: [380, 705, 1080] };
640        var m = mix[v.grade] || mix.m200;
641        out.volume = fmtNum(vol, 2, ctx.sep, ctx.tho) + ' ' + u('m3');
642        out.cement = fmtNum(vol * m[0], 0, ctx.sep, ctx.tho) + ' ' + u('kg');
643        out.sand = fmtNum(vol * m[1], 0, ctx.sep, ctx.tho) + ' ' + u('kg');
644        out.gravel = fmtNum(vol * m[2], 0, ctx.sep, ctx.tho) + ' ' + u('kg');
645        break;
646      }
647      case 'pregnancy': {
648        var cycle = Math.max(20, Math.min(45, Math.round(n('cycle') || 28)));
649        var ms = Date.parse((v.lmp || '') + 'T12:00:00Z');
650        if (isNaN(ms)) { ms = Date.now() - 56 * 864e5; }
651        var due = ms + (280 + (cycle - 28)) * 864e5;
652        var days = Math.floor((Date.now() - ms) / 864e5);
653        var d = new Date(due);
654        var pad = function (x) { return (x < 10 ? '0' : '') + x; };
655        out.due = pad(d.getUTCDate()) + '.' + pad(d.getUTCMonth() + 1) + '.' + d.getUTCFullYear();
656        out.week = Math.max(0, Math.min(45, Math.floor(days / 7))) + ' ' + u('weeks');
657        out.left = Math.max(0, Math.ceil((due - Date.now()) / 864e5)) + ' ' + u('days');
658        break;
659      }
660    }
661    return out;
662  }
663
664  function initCalc(card) {
665    var kind = card.getAttribute('data-kind') || '';
666    var inputs = card.querySelectorAll('.pogoda-calc-in');
667    if (!inputs.length) { return; }
668    var ctx = {
669      sep: card.getAttribute('data-dec-sep') || ',',
670      tho: card.getAttribute('data-thousands') || '',
671      cur: card.getAttribute('data-cur') || '',
672      units: parseJSONAttr(card, 'data-units'),
673      cats: (function () { try { return JSON.parse(card.getAttribute('data-cats') || '[]'); } catch (e) { return []; } })(),
674      eur: parseFloat(card.getAttribute('data-eur') || '0') || 0
675    };
676    var initial = [];
677    for (var i = 0; i < inputs.length; i++) { initial.push(inputs[i].value); }
678
679    function values() {
680      var v = {};
681      for (var j = 0; j < inputs.length; j++) {
682        v[inputs[j].getAttribute('data-name')] = inputs[j].value;
683      }
684      return v;
685    }
686    function recalc() {
687      var res = calcRun(kind, values(), ctx);
688      for (var key in res) {
689        if (!Object.prototype.hasOwnProperty.call(res, key)) { continue; }
690        var row = card.querySelector('.pogoda-calc-row[data-key="' + key + '"] .pogoda-calc-rval');
691        if (row) { row.textContent = res[key]; }
692      }
693    }
694    for (var k = 0; k < inputs.length; k++) {
695      inputs[k].addEventListener('input', recalc);
696      inputs[k].addEventListener('change', recalc);
697    }
698    var reset = q(card, '.pogoda-calc-reset');
699    if (reset) {
700      reset.addEventListener('click', function () {
701        for (var r = 0; r < inputs.length; r++) { inputs[r].value = initial[r]; }
702        recalc();
703      });
704    }
705  }
706
707  /* ── Партнерська картка: перевірка за VIN ── */
708  function initPartner(card) {
709    var form = q(card, '.pogoda-partner-vin');
710    if (!form) { return; }
711    var input = q(form, '.pogoda-partner-vin-input');
712    if (!input) { return; }
713    form.addEventListener('submit', function (e) {
714      var vin = (input.value || '').replace(/[^A-Za-z0-9]/g, '').toUpperCase();
715      var tpl = input.getAttribute('data-url') || '';
716      if (!vin) { e.preventDefault(); input.focus(); return; }
717      // Шаблон із {vin} веде точно на сторінку перевірки з нашим кодом;
718      // без нього лишаємо звичайну відправку форми (вона теж із кодом).
719      if (tpl.indexOf('%7Bvin%7D') > -1 || tpl.indexOf('{vin}') > -1) {
720        e.preventDefault();
721        var url = tpl.split('%7Bvin%7D').join(encodeURIComponent(vin)).split('{vin}').join(encodeURIComponent(vin));
722        window.open(url, '_blank', 'noopener');
723      }
724    });
725    input.addEventListener('input', function () {
726      var v = (input.value || '').replace(/[^A-Za-z0-9]/g, '').toUpperCase();
727      if (v !== input.value) { input.value = v; }
728    });
729  }
730
731  /* ── Закриття плаваючих ── */
732  function removeCard(card) {
733    var wrap = card.parentNode;
734    if (!wrap) { return; }
735    wrap.removeChild(card);
736    if (!wrap.classList || !wrap.classList.contains('pogoda-floating-wrap')) { return; }
737    // Разом із плашкою зникає і її вкладка в доку (телефон після повороту).
738    var tab = wrap.querySelector('.pogoda-dock-tab[data-for="' + (card.getAttribute('data-widget') || '') + '"]');
739    if (tab && tab.parentNode) { tab.parentNode.removeChild(tab); }
740    if (!wrap.querySelector('.pogoda') && wrap.parentNode) {
741      wrap.parentNode.removeChild(wrap);
742    }
743  }
744  /* Прев'ю адміністратора (?pogoda_preview): «закрито до кінця сесії» не
745     читаємо й не пишемо, інакше один дотик ✕ ховав би плашки з усіх
746     наступних прев'ю в цій вкладці браузера. */
747  function isPreviewWrap(wrap) {
748    return !!(wrap && wrap.getAttribute && wrap.getAttribute('data-preview') === '1');
749  }
750  function initClose(card) {
751    var close = q(card, '.pogoda-close');
752    if (!close) { return; }
753    var id = card.getAttribute('data-widget') || 'w';
754    close.addEventListener('click', function () {
755      var wrap = card.parentNode;
756      // У доку на телефоні ✕ лише згортає плашку назад у вкладку: сховати
757      // все — окрема кнопка в рядку вкладок.
758      if (wrap && wrap.classList && wrap.classList.contains('pogoda-dock') && dockMobile()) {
759        dockOpen(wrap, null);
760        return;
761      }
762      removeCard(card);
763      if (!isPreviewWrap(wrap)) { store('session', 'pogoda_closed_' + id, '1'); }
764    });
765  }
766
767  /* ── Мобільний док плаваючих плашок ──
768     Розмітку вкладок друкує PHP (pogoda_dock_bar); CSS показує її лише на
769     екрані ≤1023px і лише з класом .pogoda-dock на обгортці — його ставимо
770     тут, тож без скрипта плашки видно як раніше. */
771  var DOCK_MQ = '(max-width: 1023px)'; // те саме, що @media у pogoda.css
772  function dockMobile() {
773    return !!(window.matchMedia && window.matchMedia(DOCK_MQ).matches);
774  }
775  /* Живе число для вкладки: температура, курс, індекс повітря… Довше за 12
776     символів у вкладку не лізе — тоді лишається сама назва. Прогноз — з
777     префіксом «до» (data-prefix з i18n), щоб не читався як поточна. */
778  function dockValue(card, tab) {
779    if (card.classList.contains('pogoda-unavailable')) { return ''; }
780    var t = function (sel) {
781      var n = q(card, sel);
782      return n ? (n.textContent || '').replace(/\s+/g, ' ').trim() : '';
783    };
784    var v = '';
785    switch (card.getAttribute('data-widget')) {
786      case 'weather':  v = t('.pogoda-temp'); break;
787      case 'forecast':
788        v = t('.pogoda-fday-max');
789        if (v && v !== '—' && tab && tab.getAttribute('data-prefix')) { v = tab.getAttribute('data-prefix') + ' ' + v; }
790        break;
791      case 'rates':    v = (t('.pogoda-rate-code') + ' ' + (t('.pogoda-rate-value') || t('.pogoda-rate-sell'))).trim(); break;
792      case 'metals':   v = t('.pogoda-metal-price'); break;
793      case 'air':      v = t('.pogoda-air-aqi'); break;
794      case 'sun':      v = t('.pogoda-sun-val'); break;
795    }
796    if (v === '—' || v.length > 12) { v = ''; }
797    return v;
798  }
799  /* Погода («+9°»), прогноз («до +19°») і курс («USD 44,62») упізнаються з
800     самого числа, тож у них число замінює назву — інакше три вкладки не
801     лізуть у 375px. */
802  var DOCK_BRIEF = { weather: 1, forecast: 1, rates: 1 };
803  /* Іконка вкладки погоди/прогнозу — жива, з самої плашки (стан неба на
804     сьогодні), а не статична хмарка, яка на дощі бреше. */
805  function dockIcon(card, tab) {
806    var id = card.getAttribute('data-widget');
807    var src = id === 'weather' ? q(card, '.pogoda-main-icon-wrap svg') : (id === 'forecast' ? q(card, '.pogoda-fday-icon svg') : null);
808    var cur = q(tab, '.pogoda-dock-icon');
809    if (!src || !cur || !src.cloneNode) { return; }
810    var icon = src.cloneNode(true);
811    var cls = (icon.getAttribute('class') || '').replace(/\bpogoda-main-icon\b/, '').replace(/\s+/g, ' ').trim();
812    icon.setAttribute('class', cls + ' pogoda-dock-icon');
813    cur.parentNode.replaceChild(icon, cur);
814  }
815  function dockRefresh(card) {
816    var wrap = card.parentNode;
817    if (!wrap || !wrap.classList || !wrap.classList.contains('pogoda-floating-wrap')) { return; }
818    var id = card.getAttribute('data-widget') || '';
819    var tab = wrap.querySelector('.pogoda-dock-tab[data-for="' + id + '"]');
820    var val = tab ? q(tab, '.pogoda-dock-val') : null;
821    if (!tab || !val) { return; }
822    var v = dockValue(card, tab);
823    val.textContent = v;
824    var label = q(tab, '.pogoda-dock-label');
825    var brief = !!(v && DOCK_BRIEF[id]);
826    tab.classList.toggle('pogoda-dock-brief', brief);
827    if (label) { tab.setAttribute('aria-label', (label.textContent || '') + (v ? ' ' + v : '')); }
828    dockIcon(card, tab);
829  }
830  /* Відкрити плашку id (null — згорнути всі). Одночасно відкрита лише одна. */
831  function dockOpen(wrap, id) {
832    var cards = wrap.querySelectorAll('.pogoda');
833    var tabs = wrap.querySelectorAll('.pogoda-dock-tab');
834    var open = null;
835    var was = wrap.getAttribute('data-dock-open') || '';
836    var active = document.activeElement;
837    for (var i = 0; i < cards.length; i++) {
838      var on = id !== null && cards[i].getAttribute('data-widget') === id;
839      cards[i].classList.toggle('pogoda-dock-active', on);
840      if (on) { open = cards[i]; }
841    }
842    var focusTab = null;
843    for (var j = 0; j < tabs.length; j++) {
844      var mine = id !== null && tabs[j].getAttribute('data-for') === id;
845      tabs[j].setAttribute('aria-expanded', mine ? 'true' : 'false');
846      if (!open && was && tabs[j].getAttribute('data-for') === was) { focusTab = tabs[j]; }
847    }
848    wrap.classList.toggle('pogoda-dock-open', !!open);
849    if (open) {
850      wrap.setAttribute('data-dock-open', id);
851      open.scrollTop = 0;
852    } else {
853      wrap.removeAttribute('data-dock-open');
854      // Фокус був у згорнутій плашці (✕, Escape з клавіатури) — повертаємо
855      // на її вкладку, інакше він падає на <body> і Tab починає з початку.
856      if (focusTab && (!active || active === document.body || !document.body.contains(active) || !wrap.contains(active) || (active.closest && active.closest('.pogoda')))) {
857        try { focusTab.focus(); } catch (e) { /* ignore */ }
858      }
859    }
860  }
861  function initDock(wrap) {
862    var bar = q(wrap, '.pogoda-dock-bar');
863    if (!bar) { return; }
864    // Розмітка нова, а CSS старий (кеш/CDN без ?ver): панель не стилізована
865    // (display: block замість none/flex) — ховаємо її і лишаємо стос як був,
866    // інакше ✕ плашки на телефоні став би порожньою дією.
867    var disp = window.getComputedStyle ? getComputedStyle(bar).display : 'none';
868    if (disp !== 'none' && disp !== 'flex') {
869      bar.style.display = 'none';
870      return;
871    }
872    if (!isPreviewWrap(wrap) && store('session', 'pogoda_dock_closed') === '1') {
873      if (wrap.parentNode) { wrap.parentNode.removeChild(wrap); }
874      return;
875    }
876    wrap.classList.add('pogoda-dock');
877    var cards = wrap.querySelectorAll('.pogoda');
878    for (var i = 0; i < cards.length; i++) {
879      if (!cards[i].id) { cards[i].id = 'pogoda-dock-' + (cards[i].getAttribute('data-widget') || i); }
880      dockRefresh(cards[i]);
881    }
882    var tabs = wrap.querySelectorAll('.pogoda-dock-tab');
883    for (var k = 0; k < tabs.length; k++) {
884      var id = tabs[k].getAttribute('data-for') || '';
885      if (!wrap.querySelector('.pogoda[data-widget="' + id + '"]')) {
886        // плашку закрили раніше в цій сесії — вкладка їй не потрібна
887        tabs[k].parentNode.removeChild(tabs[k]);
888        continue;
889      }
890      tabs[k].addEventListener('click', function () {
891        var wid = this.getAttribute('data-for');
892        var opening = this.getAttribute('aria-expanded') !== 'true';
893        dockOpen(wrap, opening ? wid : null);
894        if (opening) {
895          // Відкрита вкладка має бути видимою у прокрутному рядку (без
896          // scrollIntoView — той міг би прокрутити й саму сторінку).
897          var strip = this.parentNode;
898          if (strip && strip.scrollWidth > strip.clientWidth) {
899            var l = this.offsetLeft, r = l + this.offsetWidth;
900            if (r > strip.scrollLeft + strip.clientWidth) { strip.scrollLeft = r - strip.clientWidth + 4; }
901            else if (l < strip.scrollLeft) { strip.scrollLeft = Math.max(0, l - 4); }
902          }
903        }
904      });
905    }
906    var hide = q(bar, '.pogoda-dock-hide');
907    if (hide) {
908      hide.addEventListener('click', function () {
909        // Поки плашка відкрита, ✕ лише згортає її: сховати весь док
910        // випадковим дотиком поруч із вкладкою не можна.
911        if (wrap.classList.contains('pogoda-dock-open')) { dockOpen(wrap, null); return; }
912        if (wrap.parentNode) { wrap.parentNode.removeChild(wrap); }
913        if (!isPreviewWrap(wrap)) { store('session', 'pogoda_dock_closed', '1'); }
914      });
915    }
916    // Дотик поза доком і Escape згортають відкриту плашку. pointerdown —
917    // бо iOS Safari не шле click на document за дотиком до звичайного тексту.
918    var outside = function (e) {
919      var t = e.target;
920      // Кнопка «Показати карту/сервіс» замінює себе на iframe у власному
921      // обробнику, тож на момент click її вже немає в документі — такий
922      // дотик не «поза доком».
923      if (!t || !document.documentElement.contains(t)) { return; }
924      if (wrap.classList.contains('pogoda-dock-open') && dockMobile() && !wrap.contains(t)) { dockOpen(wrap, null); }
925    };
926    document.addEventListener(window.PointerEvent ? 'pointerdown' : 'touchstart', outside, { passive: true });
927    document.addEventListener('click', outside);
928    document.addEventListener('keydown', function (e) {
929      if ((e.key === 'Escape' || e.key === 'Esc') && wrap.classList.contains('pogoda-dock-open')) { dockOpen(wrap, null); }
930    });
931    // Поворот планшета в десктопну ширину: стос знову цілком, без відкритої вкладки.
932    if (window.matchMedia) {
933      var mq = window.matchMedia(DOCK_MQ);
934      var onChange = function () { if (!mq.matches) { dockOpen(wrap, null); } };
935      if (mq.addEventListener) { mq.addEventListener('change', onChange); }
936      else if (mq.addListener) { mq.addListener(onChange); }
937    }
938  }
939
940  function boot() {
941    var floating = document.querySelectorAll('.pogoda-floating-wrap:not([data-preview]) .pogoda');
942    for (var j = 0; j < floating.length; j++) {
943      var wid = floating[j].getAttribute('data-widget') || 'w';
944      if (store('session', 'pogoda_closed_' + wid) === '1') { removeCard(floating[j]); }
945    }
946    var wraps = document.querySelectorAll('.pogoda-floating-wrap');
947    for (var w = 0; w < wraps.length; w++) { initDock(wraps[w]); }
948    var cards = document.querySelectorAll('.pogoda');
949    for (var i = 0; i < cards.length; i++) {
950      var card = cards[i];
951      initClose(card);
952      switch (card.getAttribute('data-widget')) {
953        case 'weather': initWeather(card); break;
954        case 'losses': initLosses(card); break;
955        case 'map': initMap(card); break;
956        case 'rates': initRates(card); break;
957        case 'metals': initMetals(card); break;
958        case 'converter': initConverter(card); break;
959        case 'crypto': initRates(card); break;
960        case 'calc': initCalc(card); break;
961        case 'partners': initPartner(card); break;
962      }
963    }
964  }
965
966  if (document.readyState === 'loading') {
967    document.addEventListener('DOMContentLoaded', boot);
968  } else {
969    boot();
970  }
971})();

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.