PageSourceSearch

https://imeicheck.com/front/js/imei-scanner.js

js imeicheck.com collected 2026-10-02 14:40:15 UTC 25,868 bytes, 644 lines download raw bytes

1/**
2 * IMEI Scanner — Barcode + OCR from camera/photo
3 * Strategy: barcode-detector polyfill first (fast) → OCR fallback (slower)
4 * Attaches to existing input #imeiFld
5 * Dependencies: All lazy-loaded from CDN only when needed
6 */
7(function () {
8  'use strict';
9
10  // ── Config ──
11  var INPUT_ID = 'imeiFld';
12  var TESSERACT_CDN = 'https://cdnjs.cloudflare.com/ajax/libs/tesseract.js/5.0.4/tesseract.min.js';
13  var BARCODE_CDN = 'https://fastly.jsdelivr.net/npm/barcode-detector@3/dist/iife/index.min.js';
14
15  var _worker = null;
16  var _workerReady = false;
17  var _selectedIMEI = null;
18  var _barcodeDetector = null;
19  var _barcodeLoading = null;
20
21  // ── Validation ─
22  function luhn(imei) {
23    if (!/^\d{15}$/.test(imei)) return false;
24    var s = 0;
25    for (var i = 0; i < 15; i++) {
26      var d = +imei[i];
27      if (i % 2 === 1) { d *= 2; if (d > 9) d -= 9; }
28      s += d;
29    }
30    return s % 10 === 0;
31  }
32
33  function validPrefix(imei) {
34    return /^(35|01|86)/.test(imei);
35  }
36
37  function fmtIMEI(imei) {
38    return imei.replace(/(\d{2})(\d{6})(\d{6})(\d)/, '$1 $2 $3 $4');
39  }
40
41  // ── OCR character fix (only between digits) ──
42  function ocrFix(text) {
43    return text
44      .replace(/(\d)%(\d)/g, '$19$2')
45      .replace(/(\d)[oO](\d)/g, '$10$2')
46      .replace(/(\d)[lI|](\d)/g, '$11$2')
47      .replace(/(\d)[Ss](\d)/g, '$15$2')
48      .replace(/(\d)[Bb](\d)/g, '$18$2')
49      .replace(/(\d)[Gg](\d)/g, '$19$2')
50      .replace(/(\d)[Zz](\d)/g, '$12$2');
51  }
52
53  // ── IMEI extraction from text ──
54  function extractIMEIs(text) {
55    var results = new Map();
56    var lines = text.split(/[\n\r]+/);
57    for (var li = 0; li < lines.length; li++) {
58      var line = ocrFix(lines[li]);
59      var groups = line.match(/\d+/g);
60      if (!groups) continue;
61      for (var gi = 0; gi < groups.length; gi++) {
62        if (groups[gi].length === 15 && validPrefix(groups[gi]) && luhn(groups[gi])) {
63          results.set(groups[gi], true);
64        }
65      }
66      for (var start = 0; start < groups.length; start++) {
67        var combined = '';
68        for (var end = start; end < groups.length; end++) {
69          combined += groups[end];
70          if (combined.length > 20) break;
71          if (combined.length >= 15) {
72            for (var i = 0; i <= combined.length - 15; i++) {
73              var c = combined.substring(i, i + 15);
74              if (validPrefix(c) && luhn(c)) results.set(c, true);
75            }
76            break;
77          }
78        }
79      }
80    }
81    var out = [];
82    results.forEach(function (_, imei) { out.push(imei); });
83    return out;
84  }
85
86  // ── Extract IMEI from barcode raw value ──
87  function extractIMEIsFromBarcode(rawValue) {
88    if (!rawValue) return [];
89    var digits = rawValue.replace(/\D/g, '');
90    var found = [];
91    if (digits.length === 15 && validPrefix(digits) && luhn(digits)) {
92      found.push(digits);
93    }
94    if (digits.length > 15) {
95      for (var i = 0; i <= digits.length - 15; i++) {
96        var c = digits.substring(i, i + 15);
97        if (validPrefix(c) && luhn(c) && found.indexOf(c) === -1) found.push(c);
98      }
99    }
100    return found;
101  }
102
103  // ═════════════════════════════════════════
104  // ── PHASE 1: BARCODE (barcode-detector polyfill) ──
105  // ═════════════════════════════════════════
106
107  function loadBarcodeDetector() {
108    if (_barcodeDetector) return Promise.resolve(true);
109    if (_barcodeLoading) return _barcodeLoading;
110    _barcodeLoading = new Promise(function (resolve) {
111      // Load polyfill script first
112      var s = document.createElement('script');
113      s.src = BARCODE_CDN;
114      s.onload = function () {
115        try {
116          // Always prefer polyfill (zxing-wasm) — native BarcodeDetector is unreliable
117          var BD = (window.BarcodeDetectionAPI && window.BarcodeDetectionAPI.BarcodeDetector) || window.BarcodeDetector;
118          if (BD) {
119            _barcodeDetector = new BD({
120              formats: ['code_128', 'code_39', 'code_93', 'ean_13', 'ean_8', 'itf', 'codabar', 'upc_a', 'upc_e']
121            });
122            console.log('[IMEI Scanner] Using barcode-detector polyfill');
123            // Warmup: trigger WASM load so first real scan is instant
124            var wc = document.createElement('canvas');
125            wc.width = 1; wc.height = 1;
126            _barcodeDetector.detect(wc).then(function () {
127              resolve(true);
128            }).catch(function () {
129              resolve(true); // warmup error is fine, detector still works
130            });
131          } else {
132            console.warn('[IMEI Scanner] barcode-detector loaded but no BarcodeDetector found');
133            resolve(false);
134          }
135        } catch (e) {
136          console.error('[IMEI Scanner] BarcodeDetector init error:', e);
137          resolve(false);
138        }
139      };
140      s.onerror = function () {
141        console.warn('[IMEI Scanner] Failed to load barcode-detector CDN');
142        resolve(false);
143      };
144      document.head.appendChild(s);
145    });
146    return _barcodeLoading;
147  }
148
149  async function scanBarcode(imgElement) {
150    if (!_barcodeDetector) return [];
151    try {
152      var barcodes = await _barcodeDetector.detect(imgElement);
153      console.log('[IMEI Scanner] Barcodes found:', barcodes.length, barcodes.map(function(b){ return b.format + ':' + b.rawValue; }));
154      var imeis = [];
155      for (var i = 0; i < barcodes.length; i++) {
156        var found = extractIMEIsFromBarcode(barcodes[i].rawValue);
157        for (var j = 0; j < found.length; j++) {
158          if (imeis.indexOf(found[j]) === -1) imeis.push(found[j]);
159        }
160      }
161      return imeis;
162    } catch (e) {
163      console.error('[IMEI Scanner] Barcode detect error:', e);
164      return [];
165    }
166  }
167
168  // ═════════════════════════════════════════
169  // ── PHASE 2: OCR FALLBACK ──
170  // ═════════════════════════════════════════
171
172  function toContrast(source) {
173    var c = document.createElement('canvas');
174    c.width = source.width; c.height = source.height;
175    var ctx = c.getContext('2d'); ctx.drawImage(source, 0, 0);
176    var img = ctx.getImageData(0, 0, c.width, c.height); var d = img.data;
177    for (var i = 0; i < d.length; i += 4) {
178      var g = .299 * d[i] + .587 * d[i + 1] + .114 * d[i + 2];
179      g = Math.max(0, Math.min(255, ((g - 128) * 2) + 128));
180      d[i] = d[i + 1] = d[i + 2] = g;
181    }
182    ctx.putImageData(img, 0, 0); return c;
183  }
184
185  function toBinary(source, t) {
186    var c = document.createElement('canvas');
187    c.width = source.width; c.height = source.height;
188    var ctx = c.getContext('2d'); ctx.drawImage(source, 0, 0);
189    var img = ctx.getImageData(0, 0, c.width, c.height); var d = img.data;
190    for (var i = 0; i < d.length; i += 4) {
191      var g = .299 * d[i] + .587 * d[i + 1] + .114 * d[i + 2];
192      g = g > t ? 255 : 0;
193      d[i] = d[i + 1] = d[i + 2] = g;
194    }
195    ctx.putImageData(img, 0, 0); return c;
196  }
197
198  function toInvertBinary(source, t) {
199    var c = document.createElement('canvas');
200    c.width = source.width; c.height = source.height;
201    var ctx = c.getContext('2d'); ctx.drawImage(source, 0, 0);
202    var img = ctx.getImageData(0, 0, c.width, c.height); var d = img.data;
203    for (var i = 0; i < d.length; i += 4) {
204      var g = 255 - Math.round(.299 * d[i] + .587 * d[i + 1] + .114 * d[i + 2]);
205      g = g > t ? 255 : 0;
206      d[i] = d[i + 1] = d[i + 2] = g;
207    }
208    ctx.putImageData(img, 0, 0); return c;
209  }
210
211  function loadTesseract(cb) {
212    if (window.Tesseract) return cb();
213    var s = document.createElement('script');
214    s.src = TESSERACT_CDN;
215    s.onload = cb;
216    s.onerror = function () { cb(); };
217    document.head.appendChild(s);
218  }
219
220  async function initOCR() {
221    if (_worker) return;
222    _worker = await Tesseract.createWorker('eng', 1, { logger: function () { } });
223    await _worker.setParameters({ tessedit_pageseg_mode: '6' });
224    _workerReady = true;
225  }
226
227  async function ocrScan(canvas) {
228    var foundIMEIs = new Map();
229    var variants = [
230      { msg: 'Reading label...', fn: function (s) { return toBinary(s, 150); } },
231      { msg: 'Adjusting brightness...', fn: toContrast },
232      { msg: 'Trying harder...', fn: function (s) { return toInvertBinary(s, 80); } }
233    ];
234    for (var i = 0; i < variants.length; i++) {
235      setLoadingText(variants[i].msg);
236      try {
237        var processed = variants[i].fn(canvas);
238        var r = await _worker.recognize(processed);
239        var text = r.data.text || '';
240        if (text.trim()) {
241          var imeis = extractIMEIs(text);
242          for (var j = 0; j < imeis.length; j++) {
243            var ex = foundIMEIs.get(imeis[j]);
244            foundIMEIs.set(imeis[j], (ex || 0) + 1);
245          }
246          if (foundIMEIs.size > 0) break;
247        }
248      } catch (e) { }
249    }
250    var out = [];
251    foundIMEIs.forEach(function (count, imei) { out.push({ imei: imei, count: count }); });
252    return out;
253  }
254
255  // ═════════════════════════════════════════
256  // ── MAIN PROCESSING PIPELINE ──
257  // ══════════════════════════════════════════
258
259  async function processPhoto(file) {
260    showLoading('Scanning barcode...');
261
262    // Start loading image + barcode detector + Tesseract ALL in parallel
263    var imgPromise = loadImage(file);
264    var barcodePromise = loadBarcodeDetector();
265    var tesseractPreload = new Promise(function (resolve) { loadTesseract(resolve); });
266
267    var imgEl = await imgPromise;
268
269    // Phase 1: Barcode scan (fast path)
270    await barcodePromise;
271    var barcodeIMEIs = await scanBarcode(imgEl);
272
273    // If barcode found 2+ IMEIs, return immediately (fast)
274    if (barcodeIMEIs.length >= 2) {
275      hideLoading();
276      console.log('[IMEI Scanner] Found 2+ via barcode:', barcodeIMEIs);
277      return barcodeIMEIs.map(function (imei) { return { imei: imei, count: 10, source: 'barcode' }; });
278    }
279
280    // Phase 2: OCR — either barcode found 0 or only 1 (might be missing the other)
281    if (barcodeIMEIs.length === 1) {
282      console.log('[IMEI Scanner] Barcode found 1, running OCR to check for second IMEI');
283      setLoadingText('Checking for more IMEIs...');
284    } else {
285      console.log('[IMEI Scanner] No barcode IMEI found, falling back to OCR');
286      setLoadingText('Almost there...');
287    }
288
289    await tesseractPreload;
290    if (!window.Tesseract) {
291      hideLoading();
292      // If we had 1 barcode IMEI but Tesseract failed, return what we have
293      if (barcodeIMEIs.length === 1) {
294        return barcodeIMEIs.map(function (imei) { return { imei: imei, count: 10, source: 'barcode' }; });
295      }
296      return [];
297    }
298    if (!_workerReady) await initOCR();
299
300    var canvas = document.createElement('canvas');
301    var ctx = canvas.getContext('2d');
302    var w = imgEl.naturalWidth || imgEl.width;
303    var h = imgEl.naturalHeight || imgEl.height;
304    if (w > 1200) { h = Math.round(h * 1200 / w); w = 1200; }
305    canvas.width = w; canvas.height = h;
306    ctx.drawImage(imgEl, 0, 0, w, h);
307
308    var ocrResults = await ocrScan(canvas);
309
310    // Merge barcode + OCR results, dedup by IMEI
311    var allIMEIs = new Map();
312    for (var bi = 0; bi < barcodeIMEIs.length; bi++) {
313      allIMEIs.set(barcodeIMEIs[bi], { imei: barcodeIMEIs[bi], count: 10, source: 'barcode' });
314    }
315    for (var oi = 0; oi < ocrResults.length; oi++) {
316      if (!allIMEIs.has(ocrResults[oi].imei)) {
317        allIMEIs.set(ocrResults[oi].imei, { imei: ocrResults[oi].imei, count: ocrResults[oi].count, source: 'ocr' });
318      }
319    }
320
321    hideLoading();
322    var merged = [];
323    allIMEIs.forEach(function (entry) { merged.push(entry); });
324    return merged;
325  }
326
327  function loadImage(file) {
328    return new Promise(function (resolve, reject) {
329      var img = new Image();
330      var url = URL.createObjectURL(file);
331      img.onload = function () { URL.revokeObjectURL(url); resolve(img); };
332      img.onerror = function () { URL.revokeObjectURL(url); reject(new Error('Image load failed')); };
333      img.src = url;
334    });
335  }
336
337  // ══════════════════════════════════════════
338  // ── UI ──
339  // ═════════════════════════════════════════
340
341  function injectUI() {
342    var input = document.getElementById(INPUT_ID);
343    if (!input) return;
344
345    // Find or create wrapper
346    var wrapper = input.parentNode;
347    if (!wrapper.classList || !wrapper.classList.contains('imei-scan-wrap')) {
348      var newWrapper = document.createElement('div');
349      newWrapper.className = 'imei-scan-wrap';
350      newWrapper.style.cssText = 'position:relative;display:flex;align-items:center;';
351      wrapper.insertBefore(newWrapper, input);
352      newWrapper.appendChild(input);
353      wrapper = newWrapper;
354    }
355
356    // Camera button — click triggers hidden file input
357    var scanBtn = document.createElement('div');
358    scanBtn.className = 'imei-scan-btn';
359    scanBtn.title = 'Scan IMEI from photo';
360    scanBtn.innerHTML =
361      '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
362      '<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>' +
363      '<circle cx="12" cy="13" r="4"/></svg>';
364    scanBtn.addEventListener('click', function () {
365      fileInput.click();
366    });
367    wrapper.appendChild(scanBtn);
368
369    // Hidden file input — separate from button to avoid double-click
370    var fileInput = document.createElement('input');
371    fileInput.type = 'file';
372    fileInput.accept = 'image/*';
373    fileInput.setAttribute('capture', 'environment');
374    fileInput.style.display = 'none';
375    fileInput.addEventListener('change', function () {
376      handleFileSelect(this);
377    });
378    wrapper.appendChild(fileInput);
379
380    var loadingEl = document.createElement('div');
381    loadingEl.id = 'imeiScanLoading';
382    loadingEl.className = 'imei-scan-loading';
383    loadingEl.innerHTML =
384      '<div class="imei-scan-loading-inner">' +
385      '<div class="imei-scan-spinner"></div>' +
386      '<div class="imei-scan-loading-text">Scanning barcode...</div>' +
387      '</div>';
388    document.body.appendChild(loadingEl);
389
390    var modalEl = document.createElement('div');
391    modalEl.id = 'imeiScanModal';
392    modalEl.className = 'imei-scan-modal-bg';
393    modalEl.innerHTML =
394      '<div class="imei-scan-modal">' +
395      '<div class="imei-scan-modal-handle"></div>' +
396      '<div class="imei-scan-modal-title" id="imeiScanModalTitle">IMEI detected</div>' +
397      '<div class="imei-scan-modal-sub">Select the correct IMEI</div>' +
398      '<div id="imeiScanModalList"></div>' +
399      '<div class="imei-scan-modal-btns">' +
400      '<button type="button" class="imei-scan-mbtn imei-scan-mbtn-cancel" id="imeiScanCancel">Cancel</button>' +
401      '<button type="button" class="imei-scan-mbtn imei-scan-mbtn-use" id="imeiScanUse" disabled>Use</button>' +
402      '</div>' +
403      '</div>';
404    document.body.appendChild(modalEl);
405
406    var toastEl = document.createElement('div');
407    toastEl.id = 'imeiScanToast';
408    toastEl.className = 'imei-scan-toast';
409    document.body.appendChild(toastEl);
410
411    document.getElementById('imeiScanCancel').addEventListener('click', closePickerModal);
412    document.getElementById('imeiScanUse').addEventListener('click', useSelectedIMEI);
413    modalEl.addEventListener('click', function (e) {
414      if (e.target === modalEl) closePickerModal();
415    });
416  }
417
418  function handleFileSelect(inputEl) {
419    var file = inputEl.files[0];
420    if (!file) return;
421    inputEl.value = '';
422
423    processPhoto(file).then(function (entries) {
424      if (entries.length === 1) {
425        applyIMEI(entries[0].imei);
426        if (navigator.vibrate) navigator.vibrate([40, 20, 40]);
427        showToast('IMEI detected' + (entries[0].source === 'barcode' ? ' from barcode' : '') + '!');
428      } else if (entries.length > 1) {
429        if (navigator.vibrate) navigator.vibrate([40, 20, 40]);
430        showPickerModal(entries);
431      } else {
432        showToast('No valid IMEI detected. Try a clearer photo.');
433      }
434    }).catch(function (err) {
435      console.error('[IMEI Scanner] processPhoto error:', err);
436      hideLoading();
437      showToast('Error processing image. Please try again.');
438    });
439  }
440
441  function applyIMEI(imei) {
442    var input = document.getElementById(INPUT_ID);
443    if (input) {
444      input.value = imei;
445      input.dispatchEvent(new Event('input', { bubbles: true }));
446      input.dispatchEvent(new Event('change', { bubbles: true }));
447      input.focus();
448    }
449  }
450
451  function showLoading(text) {
452    var el = document.getElementById('imeiScanLoading');
453    el.querySelector('.imei-scan-loading-text').textContent = text;
454    el.classList.add('active');
455  }
456
457  function setLoadingText(text) {
458    var el = document.getElementById('imeiScanLoading');
459    el.querySelector('.imei-scan-loading-text').textContent = text;
460  }
461
462  function hideLoading() {
463    document.getElementById('imeiScanLoading').classList.remove('active');
464  }
465
466  function showPickerModal(entries) {
467    var list = document.getElementById('imeiScanModalList');
468    var btn = document.getElementById('imeiScanUse');
469    list.innerHTML = '';
470    _selectedIMEI = null;
471    btn.disabled = true;
472    btn.textContent = 'Use';
473
474    entries.sort(function (a, b) {
475      if (a.imei < b.imei) return -1;
476      if (a.imei > b.imei) return 1;
477      return 0;
478    });
479
480    document.getElementById('imeiScanModalTitle').textContent =
481      entries.length === 1 ? '1 IMEI detected' : entries.length + ' IMEIs detected';
482
483    entries.forEach(function (item, i) {
484      var div = document.createElement('div');
485      div.className = 'imei-scan-pick';
486      var badge = item.source === 'barcode'
487        ? '<span class="imei-scan-pick-badge barcode">⊟ Barcode</span>'
488        : '<span class="imei-scan-pick-badge">✓ Valid</span>';
489      div.innerHTML =
490        '<span class="imei-scan-pick-num">' + fmtIMEI(item.imei) + '</span>' + badge;
491      div.addEventListener('click', function () {
492        var all = list.querySelectorAll('.imei-scan-pick');
493        for (var k = 0; k < all.length; k++) all[k].classList.remove('selected');
494        div.classList.add('selected');
495        _selectedIMEI = item.imei;
496        btn.disabled = false;
497        btn.textContent = 'Use ' + fmtIMEI(item.imei);
498      });
499      if (i === 0) {
500        div.classList.add('selected');
501        _selectedIMEI = item.imei;
502        btn.disabled = false;
503        btn.textContent = 'Use ' + fmtIMEI(item.imei);
504      }
505      list.appendChild(div);
506    });
507
508    if (entries.length >= 2) {
509      var tip = document.createElement('div');
510      tip.className = 'imei-scan-tip';
511      tip.innerHTML = '<strong>Tip:</strong> For dual-IMEI devices, use <strong>IMEI 1</strong> for checks, especially blacklist checks.';
512      list.appendChild(tip);
513    }
514
515    document.getElementById('imeiScanModal').classList.add('active');
516  }
517
518  function closePickerModal() {
519    document.getElementById('imeiScanModal').classList.remove('active');
520    _selectedIMEI = null;
521  }
522
523  function useSelectedIMEI() {
524    if (!_selectedIMEI) return;
525    applyIMEI(_selectedIMEI);
526    closePickerModal();
527  }
528
529  function showToast(msg) {
530    var t = document.getElementById('imeiScanToast');
531    t.textContent = msg;
532    t.classList.add('show');
533    setTimeout(function () { t.classList.remove('show'); }, 3500);
534  }
535
536  function init() {
537    if (!document.getElementById(INPUT_ID)) return;
538    if (document.querySelector('.imei-scan-btn')) return; // already initialized
539    injectCSS();
540    injectUI();
541    // Preload barcode detector + warmup WASM in background
542    loadBarcodeDetector();
543  }
544
545  function injectCSS() {
546    if (document.getElementById('imeiScanStyles')) return;
547    var style = document.createElement('style');
548    style.id = 'imeiScanStyles';
549    style.textContent = '' +
550      '.imei-scan-btn{' +
551      'position:absolute;right:6px;top:50%;transform:translateY(-50%);' +
552      'width:38px;height:38px;display:none;align-items:center;justify-content:center;' +
553      'border-radius:8px;cursor:pointer;color:#888;transition:all .15s;overflow:hidden;z-index:2;' +
554      '}' +
555      '.imei-scan-btn:hover{color:#007bff;background:rgba(0,123,255,.08);}' +
556      '.imei-scan-btn:active{transform:translateY(-50%) scale(.9);}' +
557      '.imei-scan-btn svg{width:20px;height:20px;pointer-events:none;}' +
558      '@media(pointer:coarse){.imei-scan-btn{display:flex;}#' + INPUT_ID + '{padding-right:48px!important;}}' +
559
560      '.imei-scan-loading{' +
561      'display:none;position:fixed;inset:0;z-index:10000;' +
562      'background:rgba(255,255,255,.88);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);' +
563      'align-items:center;justify-content:center;' +
564      '}' +
565      '.imei-scan-loading.active{display:flex;}' +
566      '.imei-scan-loading-inner{text-align:center;}' +
567      '.imei-scan-spinner{' +
568      'width:36px;height:36px;margin:0 auto 14px;' +
569      'border:3px solid #e0e0e0;border-top-color:#007bff;border-radius:50%;' +
570      'animation:imeiScanSpin .8s linear infinite;' +
571      '}' +
572      '@keyframes imeiScanSpin{to{transform:rotate(360deg)}}' +
573      '.imei-scan-loading-text{font-size:14px;color:#888;font-weight:500;}' +
574
575      '.imei-scan-modal-bg{' +
576      'display:none;position:fixed;inset:0;z-index:10001;' +
577      'background:rgba(0,0,0,.45);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);' +
578      'align-items:flex-end;justify-content:center;' +
579      '}' +
580      '.imei-scan-modal-bg.active{display:flex;}' +
581      '.imei-scan-modal{' +
582      'background:#fff;width:100%;max-width:480px;' +
583      'border-radius:18px 18px 0 0;padding:20px 18px calc(28px + env(safe-area-inset-bottom, 0px));' +
584      'max-height:70vh;overflow-y:auto;' +
585      'animation:imeiScanSlideUp .35s cubic-bezier(.2,.8,.2,1);' +
586      '}' +
587      '@keyframes imeiScanSlideUp{from{transform:translateY(100%)}to{transform:translateY(0)}}' +
588      '.imei-scan-modal-handle{width:36px;height:4px;background:#ddd;border-radius:2px;margin:0 auto 16px;}' +
589      '.imei-scan-modal-title{font-size:17px;font-weight:700;margin-bottom:4px;color:#222;}' +
590      '.imei-scan-modal-sub{font-size:13px;color:#999;margin-bottom:14px;}' +
591
592      '.imei-scan-pick{' +
593      'background:#f8f8fa;border:1.5px solid #e5e5ea;border-radius:12px;' +
594      'padding:16px 16px;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between;' +
595      'cursor:pointer;transition:all .15s;min-height:54px;' +
596      '}' +
597      '.imei-scan-pick:active{transform:scale(.98);}' +
598      '.imei-scan-pick.selected{border-color:#007bff;background:rgba(0,123,255,.06);}' +
599      '.imei-scan-pick-num{' +
600      'font-family:"Consolas","Monaco",monospace;font-size:16px;font-weight:600;letter-spacing:.04em;color:#222;' +
601      '}' +
602      '.imei-scan-pick-badge{' +
603      'font-size:11px;font-weight:600;padding:3px 10px;border-radius:6px;' +
604      'background:rgba(52,199,89,.1);color:#34c759;' +
605      '}' +
606      '.imei-scan-pick-badge.barcode{background:rgba(0,123,255,.1);color:#007bff;}' +
607
608      '.imei-scan-modal-btns{display:flex;gap:10px;margin-top:14px;}' +
609
610      '.imei-scan-tip{' +
611      'font-size:11px;color:#999;line-height:1.5;padding:8px 12px;margin-top:4px;' +
612      'background:rgba(0,123,255,.04);border-radius:8px;border-left:2px solid rgba(0,123,255,.3);' +
613      '}' +
614      '.imei-scan-tip strong{color:#777;}' +
615      '.imei-scan-mbtn{' +
616      'flex:1;padding:15px;border:none;border-radius:12px;' +
617      'font-size:15px;font-weight:600;cursor:pointer;transition:transform .1s;font-family:inherit;min-height:50px;' +
618      '}' +
619      '.imei-scan-mbtn:active{transform:scale(.97);}' +
620      '.imei-scan-mbtn-cancel{background:#f0f0f2;color:#888;}' +
621      '.imei-scan-mbtn-use{background:#007bff;color:#fff;}' +
622      '.imei-scan-mbtn-use:disabled{opacity:.4;}' +
623
624      '.imei-scan-toast{' +
625      'position:fixed;top:calc(20px + env(safe-area-inset-top, 0px));left:50%;transform:translateX(-50%) translateY(-80px);' +
626      'background:#333;color:#fff;padding:12px 24px;border-radius:12px;' +
627      'font-size:14px;font-weight:500;z-index:99999;opacity:0;' +
628      'transition:all .4s cubic-bezier(.2,.8,.2,1);pointer-events:none;' +
629      'box-shadow:0 4px 20px rgba(0,0,0,.15);max-width:90%;text-align:center;' +
630      '}' +
631      '.imei-scan-toast.show{opacity:1;transform:translateX(-50%) translateY(0);}' +
632
633      'ins.adsbygoogle,iframe[id^="aswift"],iframe[id^="google_ads"]{z-index:1!important;}' +
634      '';
635    document.head.appendChild(style);
636  }
637
638  if (document.readyState === 'loading') {
639    document.addEventListener('DOMContentLoaded', init);
640  } else {
641    init();
642  }
643
644})();

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.