1(function ($) { 2 "use strict"; 3 4 // Breakpoint below which markers open as a bottom-sheet drawer. 5 function isMobile() { 6 return window.matchMedia('(max-width: 768px)').matches; 7 } 8 9 // Per-lookbook "Mobile Display" setting: the full-screen bottom sheet 10 // (default) or the "compact" style, which keeps the desktop box 11 // behaviour (positioned next to its marker, sized down via CSS) instead 12 // of portaling into a sheet. $lb is the ".shopspot" root. 13 function usesSheet($lb) { 14 return isMobile() && ! $lb.hasClass('lookbook-mobile-compact'); 15 } 16 17 // The mobile sheet behaves like a toast: no backdrop, no scroll lock â 18 // the page stays usable while the sheet sits fixed at the bottom. 19 20 // Keep a desktop box inside the viewport. Pages no longer clip their 21 // boxes (flipbook leaves are overflow:visible), so a box opened near the 22 // window edge â or on the outer half of a flipbook spread â can poke 23 // off-screen. Measure it and shift it back with a translate; if it is 24 // wider than the viewport, keeping the left edge visible wins. 25 function clampBoxToViewport($box) { 26 if ( ! $box.length ) { return; } 27 var el = $box[0]; 28 29 // Suspend the box transition while measuring: it transitions "all" 30 // with a 0.4s delay, so a cleared transform would otherwise keep 31 // reporting the previous clamp's geometry and poison the numbers 32 // (a re-position pass would then see the box as fine, drop the 33 // shift, and let it slide back off-screen). 34 el.style.transition = 'none'; 35 el.style.transform = 'none'; 36 37 var rect = el.getBoundingClientRect(), 38 pad = 10, 39 vw = document.documentElement.clientWidth, 40 shift = 0; 41 if ( rect.width ) { 42 if ( rect.right > vw - pad ) { 43 shift = vw - pad - rect.right; 44 } 45 if ( rect.left + shift < pad ) { 46 shift = pad - rect.left; 47 } 48 } 49 el.style.transform = shift ? 'translateX(' + shift + 'px)' : ''; 50 51 // Restore the stylesheet transition on the next frame, after the 52 // instant (un-animated) shift above has been committed. 53 window.requestAnimationFrame(function () { 54 el.style.transition = ''; 55 }); 56 } 57 58 // Position every marker's box relative to the photo so it stays on-screen. 59 function positionLookbooks(root) { 60 $(root).find('.shopspot').each(function () { 61 var $lb = $(this), 62 photo = $lb.find('.lookbook-photo'); 63 if ( ! photo.length || ! photo.offset() ) { 64 return; 65 } 66 67 // Never upscale a STANDALONE lookbook past its photo's natural 68 // width â blown-up images go blurry. The ROOT is capped (not just 69 // the img) so the %-positioned markers keep matching the photo; 70 // the stylesheet's auto margins then centre it in the column. 71 // Gallery slides / Product List wrappers size the photo themselves. 72 // The admin's own "Photo Max Width" setting (data-max-width) is a 73 // further, user-chosen ceiling on top of that natural-size cap. 74 if ( photo[0].naturalWidth && ! $lb.closest('.lookbook-gallery, .lookbook-with-list').length ) { 75 var cap = photo[0].naturalWidth, 76 userMax = parseInt($lb.attr('data-max-width'), 10); 77 if ( userMax > 0 ) { 78 cap = Math.min(cap, userMax); 79 } 80 $lb.css('max-width', cap + 'px'); 81 } 82 83 // In the default "Bottom sheet" style the box becomes a fixed 84 // sheet (styled via CSS), so clear any desktop inline positioning 85 // and skip the math. The "Compact" style keeps the desktop 86 // positioning below, just scaled down by CSS. 87 if ( usesSheet($lb) ) { 88 $lb.find('.lookbook-box').css({ top: '', left: '', right: '', bottom: '', width: '', transform: '' }); 89 return; 90 } 91 var isBottomCard = $(this).hasClass('lookbook-layout-4'), 92 photoLeft = photo.offset().left, 93 photoTop = photo.offset().top, 94 limitTop = $(this).data('limit-top'), 95 limitLeft = $(this).data('limit-left'), 96 marginTop = $(this).data('top'), 97 marginBottom = $(this).data('bottom'), 98 marginLeft = $(this).data('left'), 99 marginRight = $(this).data('right'); 100 101 $(this).find('.lookbook-marker').each(function () { 102 var halfLeft = $(this).offset().left - photoLeft, 103 halfTop = $(this).offset().top - photoTop, 104 $box = $(this).find('.lookbook-box'); 105 106 // Prefer the box's real size over the static per-layout limits: 107 // the limits assume a full-width photo and overshoot badly on 108 // small ones (gallery slides, flipbook pages), letting the box 109 // open past the photo edge where containers clip it. The box is 110 // hidden via visibility (not display), so it stays measurable. 111 var boxH = $box.outerHeight() || limitTop, 112 boxW = $box.outerWidth() || limitLeft; 113 114 // Layout 4 (Bottom card): ignore the marker position and pin 115 // the box inside the photo, centred along its bottom edge. 116 // Use the marker's left/top % against the lookbook's own 117 // client size â same local coordinate space as position:absolute 118 // â so carousel/flipbook transforms can't desync .width() from 119 // .offset() and shove the card to one side. 120 if ( isBottomCard ) { 121 var cardMargin = 12, 122 rootEl = $lb[0], 123 rootW = rootEl.clientWidth || photo.width() || 0, 124 rootH = rootEl.clientHeight || photo.height() || 0, 125 cardW = Math.min( Math.max(0, rootW - cardMargin * 2), 480 ), 126 markerLeftPct = parseFloat( this.style.left ), 127 markerTopPct = parseFloat( this.style.top ), 128 markerLeftPx = ( isNaN( markerLeftPct ) ? 0 : markerLeftPct ) / 100 * rootW, 129 markerTopPx = ( isNaN( markerTopPct ) ? 0 : markerTopPct ) / 100 * rootH, 130 markerH = this.offsetHeight || 0; 131 $box.removeClass('is-top is-bottom is-left is-right').css({ 132 width: cardW + 'px', 133 left: ( ( rootW - cardW ) / 2 - markerLeftPx ) + 'px', 134 right: 'auto', 135 top: 'auto', 136 bottom: ( markerTopPx + markerH - rootH + cardMargin ) + 'px' 137 }); 138 return; 139 } 140 141 // Reset any previously computed position classes. 142 $box.removeClass('is-top is-bottom is-left is-right'); 143 144 //Top/Bottom position 145 if ( halfTop + boxH + 10 > photo.height() ) { 146 $box.addClass('is-bottom').css({'bottom':'calc(6% + ' + marginBottom + 'px)', 'top':'auto'}); 147 } else { 148 $box.addClass('is-top').css({'top':'calc(6% - ' + marginTop + 'px)', 'bottom':'auto'}); 149 } 150 151 //Right/Left position 152 if ( halfLeft + boxW + 10 > photo.width() ) { 153 $box.addClass('is-right').css({'right':marginRight, 'left':'auto'}); 154 } else { 155 $box.addClass('is-left').css({'left':marginLeft, 'right':'auto'}); 156 } 157 158 clampBoxToViewport($box); 159 }); 160 }); 161 } 162 163 // ââ Mobile sheet portal âââââââââââââââââââââââââââââââââââââââââââââââââ 164 // position:fixed breaks inside transformed ancestors (flipbook 3D pages, 165 // gallery slides): the transform becomes the containing block and the 166 // sheet lands inside the page instead of the viewport. So on mobile the 167 // box is moved to a host under <body> while open. The host mirrors the 168 // lookbook's classes so all sheet CSS still matches. 169 170 // The sheet is fixed (viewport-relative). On a standalone lookbook we pin 171 // it to the photo's bounds so theme column padding doesn't leave the 172 // sheet floating past the image. Inside a carousel / flipbook the photo 173 // (and its ancestors) carry transforms â getBoundingClientRect then 174 // drifts and the sheet lands half off-screen â so those stay full-bleed. 175 function positionSheet($lb, $box) { 176 var vw = document.documentElement.clientWidth || window.innerWidth || 0; 177 178 if ( $lb.closest('.lookbook-gallery').length ) { 179 $box.css({ left: '0', width: '100%', right: 'auto' }); 180 return; 181 } 182 183 var photo = $lb.find('.lookbook-photo')[0]; 184 if ( ! photo || ! vw ) { 185 $box.css({ left: '0', width: '100%', right: 'auto' }); 186 return; 187 } 188 var r = photo.getBoundingClientRect(); 189 if ( ! r.width ) { 190 $box.css({ left: '0', width: '100%', right: 'auto' }); 191 return; 192 } 193 var left = Math.max(0, r.left); 194 var right = Math.min(vw, r.right); 195 var width = right - left; 196 // Half-off-screen / degenerate rect â full bleed rather than a stub. 197 if ( width < vw * 0.5 ) { 198 left = 0; 199 width = vw; 200 } 201 $box.css({ left: left + 'px', width: width + 'px', right: 'auto' }); 202 } 203 204 function openSheet($marker) { 205 var $box = $marker.find('.lookbook-box'); 206 if ( ! $box.length || $box.data('lbHosted') ) { return; } 207 208 var $lb = $marker.closest('.shopspot'); 209 var $wrap = $marker.closest('.lookbook-markers'); 210 211 var $host = $('<div></div>') 212 .attr('class', 'lookbook-sheet-host ' + ($lb.attr('class') || 'shoppable-lookbook-image-hotspots')) 213 // Analytics resolves the lookbook from its root id; carry it along. 214 .attr('data-lb-id', ($lb.attr('id') || '').replace('shopspot-', '')) 215 .appendTo('body'); 216 // Copy class AND style â style holds --lb-bg / --lb-radius / etc. Without 217 // it a custom box colour falls back to white while Auto text colour 218 // (lb-text-light) stays white â unreadable on mobile sheets.
219 var $inner = $('<div></div>') 220 .attr('class', $wrap.attr('class') || 'lookbook-markers') 221 .attr('style', $wrap.attr('style') || null) 222 .appendTo($host); 223 var $ghost = $('<div class="lookbook-marker"></div>').appendTo($inner); 224 225 window.clearTimeout($box.data('lbCloseTimer')); 226 $box.data('lbHosted', { marker: $marker, host: $host, lb: $lb }); 227 $ghost.append($box); 228 positionSheet($lb, $box); 229 230 // Let the parked state paint first so the slide-up transition runs. 231 window.requestAnimationFrame(function () { $ghost.addClass('is-active'); }); 232 } 233 234 function closeSheet($box, instant) { 235 var info = $box.data('lbHosted'); 236 if ( ! info ) { return; } 237 238 $box.parent().removeClass('is-active'); 239 $box.removeData('lbHosted'); 240 241 var restore = function () { 242 info.marker.append($box); 243 info.host.remove(); 244 }; 245 if ( instant ) { 246 restore(); 247 } else { 248 $box.data('lbCloseTimer', window.setTimeout(restore, 350)); 249 } 250 } 251 252 // Instantly restore any hosted sheet (used when resizing up to desktop). 253 function collapseHostedSheets() { 254 $('.lookbook-sheet-host .lookbook-box').each(function () { 255 var $box = $(this), info = $box.data('lbHosted'); 256 if ( info && info.marker ) { 257 info.marker.removeClass('is-active').find('.lookbook-grab').attr('aria-expanded', 'false'); 258 } 259 closeSheet($box, true); 260 }); 261 releaseSheet(); 262 } 263 264 function openMarker(grab) { 265 var $grab = $(grab), 266 $marker = $grab.parent(), 267 $lb = $marker.closest('.shopspot'); 268 269 // "Open All" lookbooks allow many boxes open at once on desktop (and 270 // on the "Compact" mobile style, which behaves like desktop); the 271 // "Bottom sheet" style always keeps one open at a time. force=true so 272 // an accordion's open box yields to the new one. 273 var multiOpen = ! usesSheet($lb) && ( $lb.hasClass('lookbook-trigger-openall') || $lb.hasClass('lookbook-trigger-always') );
274 if ( ! multiOpen ) { 275 closeAllMarkers(true); 276 } 277 278 if ( usesSheet($lb) ) { 279 openSheet($marker); 280 } else if ( ! $lb.hasClass('lookbook-layout-4') ) { 281 // Layout 4 boxes live inside the photo and animate via transform â 282 // the clamp would overwrite that transform and kill the slide-up. 283 clampBoxToViewport($marker.find('.lookbook-box')); 284 } 285 286 $marker.addClass('is-active'); 287 $grab.attr('aria-expanded', 'true'); 288 } 289 290 function closeMarker(closeBtn) { 291 var $box = $(closeBtn).closest('.lookbook-box'); 292 var hosted = $box.length ? $box.data('lbHosted') : null; 293 var $marker = hosted ? hosted.marker : $(closeBtn).closest('.lookbook-marker'); 294 295 if ( hosted ) { 296 closeSheet($box); 297 } else { 298 // Remove the clamp translate entirely ('' , not 'inherit') so 299 // stylesheet transforms â layout 4's slide-up â keep working. 300 $marker.find('.lookbook-box').css('transform', ''); 301 } 302 $marker.removeClass('is-active'); 303 $marker.find('.lookbook-grab').attr('aria-expanded', 'false'); 304 releaseSheet(); 305 } 306 307 function closeAllMarkers(force) { 308 // Hosted mobile sheets first â their boxes live outside the marker. 309 $('.lookbook-sheet-host .lookbook-box').each(function () { 310 closeSheet($(this)); 311 }); 312 $('.lookbook-marker.is-active').each(function () { 313 // Accordion keeps one box open on desktop (and on the "Compact" 314 // mobile style); it only yields when a new box is about to be 315 // opened (force). 316 if ( ! force && ! usesSheet($(this).closest('.shopspot')) && $(this).closest('.lookbook-trigger-accordion').length ) { 317 return; 318 } 319 $(this).find('.lookbook-box').css('transform', ''); 320 $(this).removeClass('is-active').find('.lookbook-grab').attr('aria-expanded', 'false'); 321 }); 322 releaseSheet(); 323 } 324 325 // Auto-open boxes for the "Open All" / "Open First" / "Accordion" triggers. 326 // Runs once per lookbook so boxes the visitor closed stay closed. 327 function applyInitialState(root) { 328 $(root).find('.shopspot').addBack('.shopspot').each(function () { 329 var $lb = $(this), 330 openAll = $lb.hasClass('lookbook-trigger-openall') || $lb.hasClass('lookbook-trigger-always'), 331 openOne = $lb.hasClass('lookbook-trigger-openfirst') || $lb.hasClass('lookbook-trigger-accordion'); 332 333 if ( ! openAll && ! openOne ) { return; } 334 if ( $lb.data('lbAutoOpened') ) { return; } 335 $lb.data('lbAutoOpened', 1); 336 337 if ( usesSheet($lb) ) { 338 // Bottom sheet can only show one box. Open First / Accordion / 339 // Open All all open the first marker as a sheet. 340 var $firstGrab = $lb.find('.lookbook-marker .lookbook-grab').first(); 341 if ( $firstGrab.length ) { 342 openMarker($firstGrab[0]); 343 } 344 return; 345 } 346 347 var $markers = openAll ? $lb.find('.lookbook-marker') : $lb.find('.lookbook-marker').first(); 348 $markers.addClass('is-active').find('.lookbook-grab').attr('aria-expanded', 'true'); 349 }); 350 } 351 352 // Clean up any legacy sheet state when nothing is open. 353 function releaseSheet() { 354 if ( ! $('.lookbook-marker.is-active').length ) { 355 $('body').removeClass('lookbook-sheet-open'); 356 } 357 } 358 359 function isActivationKey(e) { 360 return e.key === 'Enter' || e.key === ' ' || e.keyCode === 13 || e.keyCode === 32; 361 } 362 363 // Public initializer so a preview (admin / editor) can (re)position a subtree. 364 window.shoppableLookbookInit = function (root) { 365 positionLookbooks(root || document); 366 applyInitialState(root || document); 367 }; 368 369 // (Re)position once each lookbook photo has actually loaded â offsets are 370 // only correct after the image has real dimensions. The listener stays 371 // bound (not just a one-off check) because a responsive img with 372 // srcset/sizes can swap to a different candidate â and fire another
373 // 'load' with a different naturalWidth â after a later viewport resize; 374 // without this the natural-size cap in positionLookbooks() can get 375 // stuck at whatever (possibly smaller) candidate loaded last. 376 function bindImageLoads(root) { 377 $(root).find('.shopspot .lookbook-photo').each(function () { 378 var img = this; 379 if ( img.complete && img.naturalWidth ) { 380 positionLookbooks(img.closest ? img.closest('.shopspot') : document); 381 } 382 $(img).off('load.lookbook').on('load.lookbook', function () { 383 positionLookbooks(img.closest ? img.closest('.shopspot') : document); 384 }); 385 }); 386 } 387 388 $(document).ready(function () { 389 390 positionLookbooks(document); 391 bindImageLoads(document); 392 applyInitialState(document); 393 394 // After all assets (images/fonts) are loaded. 395 $(window).on('load', function () { positionLookbooks(document); applyInitialState(document); }); 396 397 // Reposition on resize / orientation change (debounced). 398 var resizeTimer; 399 $(window).on('resize orientationchange', function () { 400 clearTimeout(resizeTimer); 401 resizeTimer = setTimeout(function () { 402 // A sheet opened on mobile must not survive a switch to desktop. 403 if ( ! isMobile() ) { 404 collapseHostedSheets(); 405 } else { 406 // Still mobile (e.g. orientation change) â keep any open 407 // sheet aligned with its photo's new width/position. 408 $('.lookbook-sheet-host .lookbook-box').each(function () { 409 var info = $(this).data('lbHosted'); 410 if ( info && info.lb ) { positionSheet(info.lb, $(this)); } 411 }); 412 } 413 positionLookbooks(document); 414 applyInitialState(document); 415 }, 150); 416 }); 417 418 // Elementor: re-init when widgets render (editor preview + live page). 419 $(window).on('elementor/frontend/init', function () { 420 if ( window.elementorFrontend && elementorFrontend.hooks ) { 421 elementorFrontend.hooks.addAction('frontend/element_ready/global', function ($scope) { 422 var node = ( $scope && $scope.get ) ? $scope.get(0) : document; 423 positionLookbooks(node); 424 bindImageLoads(node); 425 }); 426 } 427 }); 428 429 // Fallback for any builder that injects markup via AJAX (WPBakery / 430 // Fusion frontend editors, etc.): observe new lookbooks and re-init. 431 if ( window.MutationObserver ) { 432 var moTimer; 433 var observer = new MutationObserver(function (mutations) { 434 var found = false; 435 mutations.forEach(function (m) { 436 [].forEach.call(m.addedNodes || [], function (n) { 437 if ( n.nodeType === 1 && ( (n.classList && n.classList.contains('shoppable-lookbook-image-hotspots')) || (n.querySelector && n.querySelector('.shopspot')) ) ) { 438 found = true; 439 } 440 }); 441 }); 442 if ( found ) { 443 clearTimeout(moTimer); 444 moTimer = setTimeout(function () { positionLookbooks(document); bindImageLoads(document); }, 200); 445 } 446 }); 447 observer.observe(document.body, { childList: true, subtree: true }); 448 } 449 450 // Hover trigger: open on mouseenter, close when leaving the marker 451 // (desktop only â touch devices keep the tap behaviour). 452 $(document) 453 .on('mouseenter', '.lookbook-trigger-hover .lookbook-grab', function () { 454 if ( ! isMobile() ) { openMarker(this); } 455 }) 456 .on('mouseleave', '.lookbook-trigger-hover .lookbook-marker', function () { 457 if ( ! isMobile() ) { closeMarker($(this).find('.lookbook-close')); } 458 }); 459 460 // Delegated handlers so dynamically injected lookbooks (previews) also work. 461 $(document) 462 .on('click', '.lookbook-grab', function () { openMarker(this); }) 463 .on('keydown', '.lookbook-grab', function (e) { 464 if ( isActivationKey(e) ) { e.preventDefault(); openMarker(this); } 465 }) 466 .on('click', '.lookbook-close', function () { closeMarker(this); }) 467 .on('keydown', '.lookbook-close', function (e) { 468 if ( isActivationKey(e) ) { e.preventDefault(); closeMarker(this); } 469 }) 470 .on('keydown', function (e) { 471 if ( e.key === 'Escape' || e.keyCode === 27 ) { 472 closeAllMarkers(); 473 } 474 }) 475 // Variation select: match attributes â update add-to-cart button. 476 .on('change', '.lookbook-variation-select', function () { 477 var $variation = $(this).closest('.lookbook-variation'); 478 // Works inside a marker box and inside a Product List card (Pro). 479 var $box = $(this).closest('.lookbook-box, .lbpl-item'); 480 var variations = $variation.data('variations') || []; 481 482 // Collect all selected attribute values. 483 var selected = {}; 484 $variation.find('.lookbook-variation-select').each(function () { 485 selected[$(this).data('attr')] = $(this).val(); 486 }); 487 488 // All attributes must be chosen. 489 var allPicked = true; 490 $.each(selected, function (k, v) { if (!v) { allPicked = false; return false; } }); 491 492 var $btn = $box.find('.lookbook-variation-btn'); 493 494 if (!allPicked) { 495 resetVariationBtn($btn); 496 return; 497 } 498 499 // Find the first matching in-stock variation. 500 var match = null; 501 for (var i = 0; i < variations.length; i++) { 502 var v = variations[i]; 503 var ok = true; 504 for (var key in selected) { 505 if (!selected.hasOwnProperty(key)) { continue; } 506 var vAttr = v.attributes[key]; 507 // Empty string in variation data means "any". 508 if (vAttr !== '' && vAttr !== selected[key]) { ok = false; break; } 509 } 510 if (ok && v.in_stock) { match = v; break; } 511 } 512 513 if (match) { 514 $btn.attr('href', '?add-to-cart=' + match.id + '&quantity=1') 515 .attr('data-product_id', match.id) 516 // Set via .data() too â WooCommerce's add-to-cart script 517 // reads $btn.data('product_id'), and jQuery caches that 518 // after the first read, ignoring later attr changes. 519 .data('product_id', match.id) 520 .addClass('ajax_add_to_cart add_to_cart_button') 521 .removeAttr('aria-disabled'); 522 } else { 523 resetVariationBtn($btn); 524 } 525 }) 526 // Clicking a not-yet-enabled variation button: point at the selects 527 // that still need a choice instead of doing nothing. 528 .on('click', '.lookbook-variation-btn', function (e) { 529 if ( $(this).attr('aria-disabled') !== 'true' ) { return; } 530 e.preventDefault(); 531 var $empty = $(this).closest('.lookbook-box, .lbpl-item') 532 .find('.lookbook-variation-select') 533 .filter(function () { return ! this.value; }); 534 $empty.addClass('lb-attention'); 535 setTimeout(function () { $empty.removeClass('lb-attention'); }, 1200); 536 }); 537 }); 538 539 function resetVariationBtn($btn) { 540 $btn.attr('href', '#') 541 .removeAttr('data-product_id') 542 .removeData('product_id') 543 .removeClass('ajax_add_to_cart add_to_cart_button') 544 .attr('aria-disabled', 'true'); 545 } 546 547 // WooCommerce's core AJAX add-to-cart script (enqueued alongside this file) 548 // handles the actual add: it adds/removes the "loading" class itself (styled 549 // as a spinner below) and fires this event on success. Give success an 550 // unmistakable "Added" state instead of WC's default 30%-opacity dim. 551 $(document.body).on('added_to_cart', function (e, fragments, cartHash, $button) { 552 if (!$button || !$button.length || !$button.closest('.lookbook-cart').length) { 553 return; 554 } 555 var i18n = (window.slbihLookbookFront && slbihLookbookFront.i18n) || {}; 556 var original = lbOriginalText($button); 557 clearTimeout($button.data('lbRevertTimer'));
558 $button.addClass('lb-cart-added').text(i18n.added || original); 559 var timer = setTimeout(function () { 560 $button.removeClass('lb-cart-added').text(original); 561 }, 2200); 562 $button.data('lbRevertTimer', timer); 563 }); 564 565 // Swap the label to "Addingâ¦" the moment the click happens, so the spinner 566 // (::before on .loading, see CSS) isn't sitting next to a static "Add to 567 // cart" the whole time it's working. 568 $(document.body).on('click', '.lookbook-cart .ajax_add_to_cart', function () { 569 var i18n = (window.slbihLookbookFront && slbihLookbookFront.i18n) || {}; 570 var $button = $(this); 571 lbOriginalText($button); 572 if (i18n.adding) { 573 $button.text(i18n.adding); 574 } 575 }); 576 577 function lbOriginalText($button) { 578 var original = $button.data('lbOriginalText'); 579 if (original === undefined) { 580 original = $button.text(); 581 $button.data('lbOriginalText', original); 582 } 583 return original; 584 } 585})(jQuery);
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.