1<!doctype html > 2<html xmlns="http://www.w3.org/1999/xhtml" lang="en" 3 class="ios-preview-native-scroll"> 4<head> 5 <meta charset="utf-8"> 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
43<script type="text/javascript"> 44 window._currentDevice = 'desktop'; 45 window.Parameters = window.Parameters || { 46 HomeUrl: 'https://www.jtnelectrical.com/', 47 48 SystemID: 'US_HIBU_PRODUCTION', 49 SiteAlias: 'f6cdfabba1c749a1a3d3b7a9f4c30c76', 50 SiteType: atob('RFVEQU9ORQ=='), 51 PublicationDate: 'Oct 07, 2026', 52 ExternalUid: '700161096', 53 IsSiteMultilingual: false, 54 InitialPostAlias: '', 55 InitialPostPageUuid: '', 56 InitialDynamicItem: '', 57 DynamicPageInfo: { 58 isDynamicPage: false, 59 base64JsonRowData: 'null', 60 }, 61 InitialPageAlias: 'request-service-form', 62 InitialPageUuid: '8d67b06083f34b4a8a328c95f017fec2', 63 InitialPageId: '117870664', 64 InitialEncodedPageAlias: 'cmVxdWVzdC1zZXJ2aWNlLWZvcm0=', 65 InitialHeaderUuid: '3dd8cf0da1894ab5a9043c416f0c4aca', 66 CurrentPageUrl: '', 67 IsCurrentHomePage: false, 68 ThemeVersion: '500000', 69 DefaultPageAlias: '', 70 RemoveDID: true, 71 WidgetStyleID: null, 72 IsHeaderFixed: false, 73 IsHeaderSkinny: false, 74 StorePagesUrls: 'e30=', 75 IsNewStore: 'false', 76 StorePath: '', 77 StoreId: 'null', 78 StoreVersion: 0, 79 StoreBaseUrl: '', 80 StoreCleanUrl: true, 81 StoreDisableScrolling: true, 82 IsStoreSuspended: false, 83 showCookieNotification: false, 84 cookiesNotificationMarkup: 'null', 85 translatedPageUrl: '', 86 sidebarPosition: 'NA', 87 currentLanguage: 'en', 88 currentLocale: 'en', 89 NavItems: '{}', 90 91 mapConsent: { 92 message: 'This content is served by a third party, {0}. If enabled, {0} may collect information about your activity.', 93 enable: 'Enable' 94 }, 95 NavigationAreaParams: { 96 ShowBackToHomeOnInnerPages: true, 97 NavbarSize: -1, 98 NavbarLiveHomePage: 'https://www.jtnelectrical.com/', 99 NavbarSelector: '#dmNav:has(a)', 100 SubNavbarSelector: '#subnav_main' 101 }, 102 planID: '770', 103 disableTracking: false, 104 pageType: 'FROM_SCRATCH', 105 isRuntimeServer: true, 106 isInEditor: false, 107 isInPreview: false, 108 hasNativeStore: false, 109 defaultLang: 'en', 110 hamburgerMigration: null, 111 isFlexSite: true 112 }; 113 114 window.Parameters.LayoutID = {}; 115 window.Parameters.LayoutID[window._currentDevice] = 30; 116 window.Parameters.LayoutVariationID = {}; 117 window.Parameters.LayoutVariationID[window._currentDevice] = 5; 118</script>
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141<!-- Injecting site-wide to the head --> 142 143 144 145 146 147<!-- End Injecting site-wide to the head --> 148 149 150 151 152 153<meta name="d-service-worker" content="register"> 154 155 156 157<link rel="canonical" href="https://www.jtnelectrical.com/request-service-form"> 158 159<meta id="view" name="viewport" content="initial-scale=1, minimum-scale=1, maximum-scale=5, viewport-fit=cover"> 160 161<meta name="apple-mobile-web-app-capable" content="yes"> 162 163 164 165<link rel="icon" type="image/x-icon" href="https://cdn.hibuwebsites.com/f6cdfabba1c749a1a3d3b7a9f4c30c76/site_favicon_16_1790751430838.ico"/> 166 167 168<link rel="preconnect" href="https://le-cdn.hibuwebsites.com"/> 169<link rel="preconnect" href="https://cdn.hibuwebsites.com" crossorigin/> 170<link rel="preconnect" href="https://static-res-cdn.websites.hibu.com"/> 171<link rel="preconnect" href="https://cdn.hibuwebsites.com"/> 172 173 174 175
176<script id='d-js-dmapi'> 177 window.SystemID = 'US_HIBU_PRODUCTION'; 178 179 if (!window.dmAPI) { 180 window.dmAPI = { 181 registerExternalRuntimeComponent: function () { 182 }, 183 getCurrentDeviceType: function () { 184 return window._currentDevice; 185 }, 186 runOnReady: (ns, fn) => { 187 const safeFn = dmAPI.toSafeFn(fn); 188 ns = ns || 'global_' + Math.random().toString(36).slice(2, 11); 189 const eventName = 'afterAjax.' + ns; 190 191 if (document.readyState === 'complete') { 192 $.DM.events.off(eventName).on(eventName, safeFn); 193 setTimeout(function () { 194 safeFn({ 195 isAjax: false, 196 }); 197 }, 0); 198 } else { 199 window?.waitForDeferred?.('dmAjax', () => { 200 $.DM.events.off(eventName).on(eventName, safeFn); 201 safeFn({ 202 isAjax: false, 203 }); 204 }); 205 } 206 }, 207 toSafeFn: (fn) => { 208 if (fn?.safe) { 209 return fn; 210 } 211 const safeFn = function (...args) { 212 try { 213 return fn?.apply(null, args); 214 } catch (e) { 215 console.log('function failed ' + e.message); 216 } 217 }; 218 safeFn.safe = true; 219 return safeFn; 220 } 221 }; 222 } 223 224 if (!window.requestIdleCallback) { 225 window.requestIdleCallback = function (fn) { 226 setTimeout(fn, 0); 227 } 228 } 229</script>
229 230 231 232 233 234 235 236 237
238<script id="d-js-load-css"> 239/** 240 * There are a few <link> tags with CSS resource in them that are preloaded in the page 241 * in each of those there is a "onload" handler which invokes the loadCSS callback 242 * defined here. 243 * We are monitoring 3 main CSS files - the runtime, the global and the page. 244 * When each load we check to see if we can append them all in a batch. If threre 245 * is no page css (which may happen on inner pages) then we do not wait for it 246 */ 247(function () { 248 let cssLinks = {}; 249 function loadCssLink(link) { 250 link.onload = null; 251 link.rel = "stylesheet"; 252 link.type = "text/css"; 253 } 254 255 function checkCss() { 256 const pageCssLink = document.querySelector("[id*='CssLink']"); 257 const widgetCssLink = document.querySelector("[id*='widgetCSS']"); 258 259 if (cssLinks && cssLinks.runtime && cssLinks.global && (!pageCssLink || cssLinks.page) && (!widgetCssLink || cssLinks.widget)) { 260 const storedRuntimeCssLink = cssLinks.runtime; 261 const storedPageCssLink = cssLinks.page; 262 const storedGlobalCssLink = cssLinks.global; 263 const storedWidgetCssLink = cssLinks.widget; 264 265 storedGlobalCssLink.disabled = true; 266 loadCssLink(storedGlobalCssLink); 267 268 if (storedPageCssLink) { 269 storedPageCssLink.disabled = true; 270 loadCssLink(storedPageCssLink); 271 } 272 273 if(storedWidgetCssLink) { 274 storedWidgetCssLink.disabled = true; 275 loadCssLink(storedWidgetCssLink); 276 } 277 278 storedRuntimeCssLink.disabled = true; 279 loadCssLink(storedRuntimeCssLink); 280 281 requestAnimationFrame(() => { 282 setTimeout(() => { 283 storedRuntimeCssLink.disabled = false; 284 storedGlobalCssLink.disabled = false; 285 if (storedPageCssLink) { 286 storedPageCssLink.disabled = false; 287 } 288 if (storedWidgetCssLink) { 289 storedWidgetCssLink.disabled = false; 290 } 291 // (SUP-4179) Clear the accumulated cssLinks only when we're 292 // sure that the document has finished loading and the document 293 // has been parsed. 294 if(document.readyState === 'interactive') { 295 cssLinks = null; 296 } 297 }, 0); 298 }); 299 } 300 } 301 302 303 function loadCSS(link) { 304 try { 305 var urlParams = new URLSearchParams(window.location.search); 306 var noCSS = !!urlParams.get("nocss"); 307 var cssTimeout = urlParams.get("cssTimeout") || 0; 308 309 if (noCSS) { 310 return; 311 } 312 if (link.href && link.href.includes("d-css-runtime")) { 313 cssLinks.runtime = link; 314 checkCss(); 315 } else if (link.id === "siteGlobalCss") { 316 cssLinks.global = link; 317 checkCss(); 318 } 319 320 else if (link.id && link.id.includes("CssLink")) { 321 cssLinks.page = link; 322 checkCss(); 323 } else if (link.id && link.id.includes("widgetCSS")) { 324 cssLinks.widget = link; 325 checkCss(); 326 } 327 328 else { 329 requestIdleCallback(function () { 330 window.setTimeout(function () { 331 loadCssLink(link); 332 }, parseInt(cssTimeout, 10)); 333 }); 334 } 335 } catch (e) { 336 throw e 337 } 338 } 339 window.loadCSS = window.loadCSS || loadCSS; 340})(); 341</script>
341 342 343 344
345<script data-role="deferred-init" type="text/javascript"> 346 /* usage: window.getDeferred(<deferred name>).resolve() or window.getDeferred(<deferred name>).promise.then(...)*/ 347 function Def() { 348 this.promise = new Promise((function (a, b) { 349 this.resolve = a, this.reject = b 350 }).bind(this)) 351 } 352 353 const defs = {}; 354 window.getDeferred = function (a) { 355 return null == defs[a] && (defs[a] = new Def), defs[a] 356 } 357 window.waitForDeferred = function (b, a, c) { 358 let d = window?.getDeferred?.(b); 359 d 360 ? d.promise.then(a) 361 : c && ["complete", "interactive"].includes(document.readyState) 362 ? setTimeout(a, 1) 363 : c 364 ? document.addEventListener("DOMContentLoaded", a) 365 : console.error(`Deferred does not exist`); 366 }; 367</script>
367 368<style id="forceCssIncludes"> 369 /* This file is auto-generated from a `scss` file with the same name */ 370 371.videobgwrapper{overflow:hidden;position:absolute;z-index:0;width:100%;height:100%;top:0;left:0;pointer-events:none;border-radius:inherit}.videobgframe{position:absolute;width:101%;height:100%;top:50%;left:50%;transform:translateY(-50%) translateX(-50%);object-fit:fill}#dm video.videobgframe{margin:0}@media (max-width:767px){.dmRoot .dmPhotoGallery.newPhotoGallery:not(.photo-gallery-done){min-height:80vh}}@media (min-width:1025px){.dmRoot .dmPhotoGallery.newPhotoGallery:not(.photo-gallery-done){min-height:45vh}}@media (min-width:768px) and (max-width:1024px){.responsiveTablet .dmPhotoGallery.newPhotoGallery:not(.photo-gallery-done){min-height:45vh}}#dm [data-show-on-page-only]{display:none!important}#dmRoot div.stickyHeaderFix div.site_content{margin-top:0!important}#dmRoot div.stickyHeaderFix div.hamburger-header-container{position:relative}@media (min-width:768px) and (max-width:1024px){.responsiveTablet #dm .dmInner .hide-for-medium,[data-hidden-on-tablet]{display:none!important}} 372 373 374</style> 375<style id="cssVariables" type="text/css"> 376 :root { 377 --color_1: rgba(0, 0, 0, 1); 378 --color_10: rgba(52, 73, 135, 1); 379 --color_11: rgba(255, 255, 255, 1); 380 --color_12: rgba(204, 54, 79, 1); 381 --color_13: rgba(52, 73, 135, 1); 382 --color_14: rgba(255, 200, 52, 1); 383 --color_15: rgba(52, 73, 135, 1); 384 --color_16: rgba(255, 255, 255, 1); 385 --color_17: rgba(255, 255, 255, 1); 386 --color_18: rgba(52, 73, 135, 1); 387 --color_19: rgba(255, 255, 255, 1); 388 --color_2: rgba(255, 255, 255, 1); 389 --color_20: rgba(204, 54, 79, 1); 390 --color_21: rgba(255, 255, 255, 1); 391 --color_22: rgba(255, 255, 255, 1); 392 --color_3: rgba(52, 73, 135, 1); 393 --color_4: rgba(204, 54, 79, 1); 394 --color_5: rgba(255, 255, 255, 1); 395 --color_6: rgba(52, 73, 135, 1); 396 --color_7: rgba(255, 255, 255, 1); 397 --color_8: rgba(255, 255, 255, 1); 398 --color_9: rgba(0, 0, 0, 1); 399 --max-content-width_1: 1200px; 400} 401</style> 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445<link type="text/css" rel="stylesheet" href="https://cdn.hibuwebsites.com/fonts/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Be+Vietnam:ital,wght@0,100;0,200;0,300;0,400;0,600;0,700;0,800;0,900&family=Kumbh+Sans:ital,wght@0,100..900;1,100..900&family=Racing+Sans+One:ital,wght@0,400&subset=latin-ext&display=swap" /> 446 447 448 449 450 451 452<link rel="stylesheet" type="text/css" href="https://static-res-cdn.websites.hibu.com/mnlt/production/6835/_dm/s/rt/dist/css/d-css-runtime-flex-new-nav.min.css" /> 453 454 455 456<link type="text/css" rel="stylesheet" href="https://cdn.hibuwebsites.com/WIDGET_CSS/f7506721e8d798cf78d4fdf7a9a1e5aa.css" id="widgetCSS" /> 457 458 459<style>img[width][height] { 460 height: auto; 461}</style> 462 463 464<style> 465 body[data-page-alias="request-service-form"] #dm [data-show-on-page-only="request-service-form"] { 466 display: block !important; 467 } 468</style> 469 470 471<style id="pageAdditionalWidgetsCss" type="text/css"> 472</style> 473 474 475 476 477 478<link type="text/css" rel="stylesheet" href="https://cdn.hibuwebsites.com/f6cdfabba1c749a1a3d3b7a9f4c30c76/files/f6cdfabba1c749a1a3d3b7a9f4c30c76_1.min.css?v=da826725d62916479411bfcc83d35978" id="siteGlobalCss" /> 479 480 481 482<link 483 type="text/css" rel="stylesheet" 484 href="/_dm/s/rt/css/hibu/hibu-runtime.css?version=2026-10-07T13_19_14" 485/> 486 487 488<style id="customWidgetStyle" type="text/css"> 489 490</style> 491<style id="innerPagesStyle" type="text/css"> 492 493</style> 494 495 496<style 497 id="additionalGlobalCss" type="text/css" 498> 499</style> 500 501<style id="pagestyle" type="text/css"> 502 @media all { 503 *#dm .dmInner nav.u_1998592992 { color:black !important; } 504 505 *#dm .dmInner div.u_1638807636 .dmforminput input { 506 font-size:14px !important; 507 color:rgba(0,0,0,1) !important; 508 } 509 510 *#dm .dmInner div.u_1638807636 .dmforminput textarea { 511 font-size:14px !important; 512 color:rgba(0,0,0,1) !important; 513 } 514 515 *#dm .dmInner div.u_1638807636 .dmforminput .fileUploadLink { 516 font-size:14px !important; 517 color:rgba(0,0,0,1) !important; 518 } 519 520 *#dm .dmInner div.u_1638807636 .checkboxwrapper span { 521 font-size:14px !important; 522 color:rgba(0,0,0,1) !important; 523 } 524 525 *#dm .dmInner div.u_1638807636 .radiowrapper span { 526 font-size:14px !important; 527 color:rgba(0,0,0,1) !important; 528 } 529 530 *#dm .dmInner div.u_1638807636 .optinwrapper div { 531 font-size:14px !important; 532 color:rgba(0,0,0,1) !important; 533 } 534 535 *#dm .dmInner div.u_1638807636 .freetextwrapper { 536 font-size:14px !important; 537 color:rgba(0,0,0,1) !important; 538 } 539 540 *#dm .dmInner div.u_1638807636 .dmforminput select { 541 font-size:14px !important; 542 color:rgba(0,0,0,1) !important; 543 } 544 545 *#dm .dmInner div.u_1638807636 .dmforminput option { 546 font-size:14px !important; 547 color:rgba(0,0,0,1) !important; 548 } 549 550 *#dm .dmInner div.u_1638807636 .dmforminput label:not(.for-checkable):not(.custom-contact-checkable) { 551 font-weight:700 !important; 552 font-size:14px !important; 553 } 554 555 *#dm .dmInner div.u_1638807636 .m-recaptcha { 556 font-weight:700 !important; 557 font-size:14px !important; 558 } 559 560 *#dm .dmInner div.u_1638807636 input:not([type="submit"]) { 561 background-color:rgba(255,255,255,1) !important; 562 background-image:initial !important; 563 border-style:solid !important; 564 border-color:var(--color_1) var(--color_1) var(--color_1) var(--color_1) !important; 565 border-width:0px 0px 1px 0px !important; 566 } 567 568 *#dm .dmInner div.u_1638807636 textarea { 569 background-color:rgba(255,255,255,1) !important; 570 background-image:initial !important; 571 border-style:solid !important; 572 border-color:var(--color_1) var(--color_1) var(--color_1) var(--color_1) !important; 573 border-width:0px 0px 1px 0px !important; 574 } 575 576 *#dm .dmInner div.u_1638807636 select { 577 background-color:rgba(255,255,255,1) !important; 578 background-image:initial !important; 579 border-style:solid !important; 580 border-color:var(--color_1) var(--color_1) var(--color_1) var(--color_1) !important; 581 border-width:0px 0px 1px 0px !important; 582 } 583 584 *#dm .dmInner div.u_1638807636 .dmformsubmit { 585 margin-left:0 !important; 586 margin-right:auto !important; 587 height:50px !important; 588 float:left !important; 589 display:block !important; 590 width:250px !important; 591 } 592 593 *#dm .dmInner div.u_1359807854 p { 594 color:var(--color_22) !important; 595 direction:ltr !important; 596 text-transform:none !important; 597 text-shadow:none !important; 598 font-weight:bold !important; 599 text-decoration:rgb(255,255,255) !important;
600 letter-spacing:normal !important; 601 font-family:Poppins !important; 602 line-height:normal !important; 603 font-style:normal !important; 604 text-align:left !important; 605 } 606 607 *#dm .dmInner div.u_1359807854 li { 608 color:var(--color_22) !important; 609 direction:ltr !important; 610 text-transform:none !important; 611 text-shadow:none !important; 612 font-weight:bold !important; 613 text-decoration:rgb(255,255,255) !important; 614 letter-spacing:normal !important; 615 font-family:Poppins !important; 616 line-height:normal !important; 617 font-style:normal !important; 618 text-align:left !important; 619 } 620 621 *#dm .dmInner div.u_1359807854 { 622 color:var(--color_22) !important; 623 direction:ltr !important; 624 text-transform:none !important; 625 text-shadow:none !important; 626 font-weight:bold !important; 627 text-decoration:rgb(255,255,255) !important; 628 letter-spacing:normal !important; 629 font-family:Poppins !important; 630 line-height:normal !important; 631 font-style:normal !important; 632 text-align:left !important; 633 } 634} 635 636@media (min-width:0) and (max-width:767px) { 637 *#dm .dmInner div.u_1359807854 p { font-size:18px !important; } 638 639 *#dm .dmInner div.u_1359807854 li { font-size:18px !important; } 640 641 *#dm .dmInner div.u_1359807854 { font-size:18px !important; } 642} 643 644@media (min-width:1025px) { 645 *#dm .dmInner div.u_1359807854 p { font-size:20px !important; } 646 647 *#dm .dmInner div.u_1359807854 li { font-size:20px !important; } 648 649 *#dm .dmInner div.u_1359807854 { font-size:20px !important; } 650} 651 652@media (min-width:768px) and (max-width:1024px) { 653 *#dm .dmInner div.u_1359807854 p { font-size:20px !important; } 654 655 *#dm .dmInner div.u_1359807854 li { font-size:20px !important; } 656 657 *#dm .dmInner div.u_1359807854 { font-size:20px !important; } 658} 659 660</style> 661 662<style id="pagestyleDevice" type="text/css"> 663 664</style> 665 666 667 668 <style data-page-uuid="8d67b06083f34b4a8a328c95f017fec2" id="grid-styles-common-24b6b13b-8d67b06083f34b4a8a328c95f017fec2">@media all { 669 670#\36 3c7b8b76c596a2813341bc5 { 671 height: auto; 672} 673 674#\31 3869723 { 675 width: 100%; 676 height: auto; 677 max-width: 100%; 678} 679 680#d4329249 { 681 min-height: 8px; 682 column-gap: 4%; 683 row-gap: 0px; 684 width: 100%; 685 min-width: 4%; 686 background-color: rgba(0, 0, 0, 0); 687 padding: 0% 0% 0% 0%; 688 margin: 0% 0% 0% 0%; 689} 690 691#b3d39244 { 692 width: 100%; 693 height: auto; 694 max-width: 100%; 695 margin-bottom: -4px; 696} 697 698#\35 30811d6 { 699 border-width: 0px; 700 border-color: rgba(0, 0, 0, 1); 701 border-style: solid; 702 background-color: var(--color_12); 703 background-image: initial; 704} 705 706#\33 4c91571 { 707 min-height: 100px; 708 column-gap: 4%; 709 row-gap: 24px; 710 width: 100%; 711 flex-wrap: nowrap; 712 max-width: none; 713 padding: 0% 4% 4px 4%; 714 margin: 0% 0% 0% 0%; 715} 716}</style> 717 <style data-page-uuid="8d67b06083f34b4a8a328c95f017fec2" id="grid-styles-common-7ef32d84-8d67b06083f34b4a8a328c95f017fec2">@media all { 718 719#\37 9342a6b { 720 width: 100%; 721 height: auto; 722 max-width: 100%; 723 justify-self: auto; 724 align-self: auto; 725 order: 0; 726 grid-area: auto / auto / auto / auto; 727} 728 729#\36 a108e83 { 730 min-height: 0px; 731 column-gap: 4%; 732 row-gap: 24px; 733 width: 100%; 734 flex-wrap: nowrap; 735 max-width: none; 736 padding: 10px 4% 10px 4%; 737 margin-left: 0%; 738 margin-top: 0%; 739 margin-right: 0%; 740 margin-bottom: 0%; 741} 742 743#\38 c30d83a { 744 min-height: 8px; 745 column-gap: 4%; 746 row-gap: 24px; 747 width: 100%; 748 min-width: 4%; 749 padding-top: 0%; 750 padding-right: 0%; 751 padding-bottom: 0%; 752 padding-left: 0%; 753 margin-left: 0%; 754 margin-top: 0%; 755 margin-right: 0%; 756 margin-bottom: 0%; 757} 758}</style> 759 <style data-page-uuid="8d67b06083f34b4a8a328c95f017fec2" id="grid-styles-common-5c4456af-8d67b06083f34b4a8a328c95f017fec2">@media all { 760 761#\35 20e9609 { 762 min-height: 240px; 763 column-gap: 4%; 764 row-gap: 24px; 765 width: 100%; 766 flex-wrap: nowrap; 767 padding: 1% 0% 4% 0%; 768 margin: 0% 0% 0% 0%; 769} 770 771#ffec1cdf { 772 width: 91%; 773 height: auto; 774 max-width: 100%; 775 justify-self: auto; 776 align-self: auto; 777 order: 0; 778 margin-bottom: 0px; 779 margin-right: 6%; 780 margin-left: 6%; 781 grid-area: auto / auto / auto / auto; 782} 783 784#\36 8fb7ec9 { 785 min-height: 8px; 786 column-gap: 4%; 787 row-gap: 24px; 788 width: 100%; 789 min-width: 4%; 790 padding: 16px 40px 16px 40px; 791 margin: 0% 0% 0% 0%; 792} 793 794#b6321765 {
795 align-items: flex-start; 796 width: 100%; 797 min-height: 160px; 798 max-height: 100%; 799 row-gap: 24px; 800 column-gap: 4%; 801 background-color: rgba(255, 255, 255, 1); 802 background-image: initial; 803 box-shadow: rgba(0, 0, 0, 0.25) 3px 0px 11px 0px; 804 border-radius: 30px 30px 30px 30px; 805 justify-self: auto; 806 align-self: auto; 807 order: 0; 808 padding: 40px 0% 40px 0%; 809 margin: 0% 0% 0% 0%; 810 grid-area: auto / auto / auto / auto; 811} 812 813#\39 8f8bad2 { 814 width: 88%; 815 height: auto; 816 max-width: 100%; 817 order: 1; 818 justify-self: auto; 819 align-self: center; 820 grid-area: auto / auto / auto / auto; 821} 822}</style> 823 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-common-flex-footer-3dd8cf0da1894ab5a9043c416f0c4aca">@media all { 824 825#flex-footer { 826 background-color: var(--color_3); 827 margin-left: 0px; 828 padding-top: 0px; 829 padding-left: 0px; 830 padding-bottom: 0px; 831 margin-top: 0px; 832 margin-bottom: 0px; 833 margin-right: 0px; 834 padding-right: 0px; 835} 836}</style> 837 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-common-flex-header-3dd8cf0da1894ab5a9043c416f0c4aca">@media all { 838 839#flex-header[data-changeable-on-scroll][data-scrolling] [data-widget-type="image"] img { 840 transform: scale(1); 841} 842#flex-header { 843 background-color: var(--color_6); 844 845 box-shadow: rgba(0, 0, 0, 0.25) 0px 0px 11px 0px; 846 background-image: initial; 847 border-style: solid; 848 border-color: var(--color_10) #000000 rgba(203, 203, 203, 1) #000000; 849 border-width: 4px 0px 0px 0px; 850 padding: 0% 0% 0% 0%; 851} 852#flex-header[data-changeable-on-scroll] { 853 --header-on-scroll-text-color: var(--color_7); 854} 855#flex-header[data-changeable-on-scroll][data-scrolling] { 856 padding-top: 0px; 857 padding-bottom: 0px; 858 background-color: var(--color_6); 859 background-image: initial; 860} 861}</style> 862 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-common-405ddf11-3dd8cf0da1894ab5a9043c416f0c4aca">@media all { 863 864#\31 d3acdb9 { 865 min-height: 80px; 866 column-gap: 0%; 867 row-gap: 0px; 868 width: 100%; 869 flex-wrap: nowrap; 870 padding: 0% 2% 0% 2%; 871 margin: 0% 0% 0% 0%; 872} 873 874#\39 f3f031f { 875 min-height: 8px; 876 column-gap: 4%; 877 row-gap: 24px; 878 flex-direction: row; 879 width: 100%; 880 justify-content: flex-start; 881 align-items: center; 882 min-width: 4%; 883 background-color: rgba(0, 0, 0, 0); 884 background-image: initial; 885 order: 0; 886 padding: 16px 16px 16px 16px; 887 margin: 0% 0% 0% 0%; 888} 889 890#\31 1d216b4 { 891 flex-direction: row; 892 justify-content: flex-start; 893 align-items: center; 894 width: 33%; 895 min-height: 75px; 896 max-height: 100%; 897 row-gap: 24px; 898 column-gap: 4%; 899 order: 0; 900 padding: 0% 0% 0% 0%; 901 margin: 0% 0% 0% 0%; 902} 903 904#a052d7aa { 905 flex-direction: row; 906 justify-content: flex-end; 907 align-items: center; 908 width: 50%; 909 min-height: 60px; 910 max-height: 100%; 911 align-self: auto; 912 order: 1; 913 justify-self: auto; 914 padding: 0% 4% 0% 0%; 915 margin: 0% 0% 0% 0%; 916 grid-area: auto / auto / auto / auto; 917} 918 919#\30 7327faa { 920 width: 100%; 921 height: 8px; 922 max-width: 100%; 923} 924 925#\39 e5ef93d { 926 flex-direction: row; 927 align-items: center; 928 width: 50%; 929 min-height: 40px; 930 max-height: 100%; 931 order: 0; 932 justify-self: auto; 933 align-self: auto; 934 padding: 0% 0% 0% 4%; 935 margin: 0% 0% 0% 0%; 936 grid-area: auto / auto / auto / auto; 937} 938 939#\35 801a4e8 svg { 940 fill: var(--color_7); 941} 942#\35 801a4e8 { 943 width: 30px; 944 height: 30px; 945 max-width: 100%; 946} 947 948#\33 aa37ebb { 949 flex-direction: row; 950 justify-content: flex-end; 951 align-items: center; 952 width: 33%; 953 min-height: 75px; 954 max-height: 100%; 955 row-gap: 24px; 956 column-gap: 4%; 957 order: 2; 958 justify-self: auto; 959 align-self: auto; 960 padding: 0% 0% 0% 0%; 961 margin: 0% 0% 0% 0%; 962 grid-area: auto / auto / auto / auto; 963} 964 965#\30 ece3d12 { 966 flex-direction: row; 967 justify-content: flex-start;
968 align-items: center; 969 width: 50%; 970 min-height: 60px; 971 max-height: 100%; 972 order: 0; 973 padding: 0% 0% 0% 4%; 974 margin: 0% 0% 0% 0%; 975} 976 977#daf81bcc { 978 width: 280px; 979 height: auto; 980 max-width: 100%; 981} 982 983#\31 48da2d9 { 984 flex-direction: row; 985 justify-content: space-between; 986 align-items: flex-start; 987 width: 100%; 988 min-height: 50px; 989 max-height: 100%; 990 order: 2; 991 justify-self: auto; 992 align-self: auto; 993 background-color: rgba(0, 0, 0, 0); 994 background-image: initial; 995 padding: 0% 0% 0% 0%; 996 margin: 0px 0% 0px 0%; 997 grid-area: auto / auto / auto / auto; 998} 999 1000#ec5d46f2 { 1001 width: auto; 1002 height: auto; 1003 max-width: 100%; 1004} 1005 1006#bff1bb66 { 1007 width: 180px; 1008 height: auto; 1009 max-width: 100%; 1010 margin: 0px 0% 0px 0%; 1011} 1012 1013#\34 cb86e4c { 1014 flex-direction: row; 1015 align-items: flex-start; 1016 width: 100%; 1017 min-height: 40px; 1018 max-height: 100%; 1019 order: 0; 1020 justify-self: auto; 1021 align-self: center; 1022 background-color: rgba(0, 0, 0, 0); 1023 background-image: initial; 1024 padding: 0% 0% 0% 0%; 1025 margin: 0px 0% 0px 0%; 1026 grid-area: auto / auto / auto / auto; 1027} 1028 1029#df845449 { 1030 width: 250px; 1031 height: auto; 1032 max-width: 100%; 1033} 1034 1035#\39 8702d80 { 1036 align-items: flex-start; 1037 width: 100%; 1038 min-height: 230px; 1039 max-height: 100%; 1040 row-gap: 0px; 1041 column-gap: 4%; 1042 justify-self: auto; 1043 align-self: auto; 1044 order: 0; 1045 padding: 0% 0% 0% 0%; 1046 margin: 0px 0% 0px 0%; 1047 grid-area: auto / auto / auto / auto; 1048} 1049 1050#da0131a7 { 1051 align-items: center; 1052 width: 35.64%; 1053 min-height: 90px; 1054 max-height: 100%; 1055 align-self: auto; 1056 justify-self: auto; 1057 order: 1; 1058 flex-wrap: nowrap; 1059 background-color: var(--color_2); 1060 background-image: none; 1061 border-radius: 10px 10px 10px 10px; 1062 padding: 0% 0% 0% 0%; 1063 margin: 0% 0% 0% 0%; 1064 grid-area: auto / auto / auto / auto; 1065} 1066 1067#\32 e9284f4 { 1068 flex-direction: row; 1069 align-items: flex-start; 1070 width: 100%; 1071 min-height: 90px; 1072 max-height: 100%; 1073 justify-self: auto; 1074 align-self: auto; 1075 order: 1; 1076 flex-wrap: nowrap; 1077 padding: 0% 1% 0% 1%; 1078 margin: 0% 0% 0% 0%; 1079 grid-area: auto / auto / auto / auto; 1080} 1081}</style> 1082 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-common-157b4fff-3dd8cf0da1894ab5a9043c416f0c4aca">@media all { 1083 1084#\32 fd7b414 { 1085 min-height: 8px; 1086 column-gap: 4%; 1087 row-gap: 24px; 1088 flex-direction: row; 1089 width: 25%; 1090 justify-content: flex-start; 1091 align-items: center; 1092 min-width: 4%; 1093 background-color: rgba(0, 0, 0, 0); 1094 background-image: initial; 1095 padding: 16px 16px 16px 16px; 1096 margin: 0% 0% 0% 0%; 1097} 1098 1099#\33 2767c2f { 1100 min-height: 80px; 1101 column-gap: 0%; 1102 row-gap: 0px; 1103 width: 100%; 1104 flex-wrap: nowrap; 1105 padding: 0% 2% 0% 2%; 1106 margin: 0% 0% 0% 0%; 1107} 1108 1109#group_7in { 1110 flex-direction: row; 1111 justify-content: flex-start; 1112 align-items: center; 1113 width: 25%; 1114 min-height: 8px; 1115 padding: 0% 0% 0% 0%; 1116 margin: 0% 0% 0% 0%; 1117} 1118 1119#group_jjh { 1120 flex-direction: row; 1121 justify-content: flex-start; 1122 align-items: center; 1123 width: 25%; 1124 min-height: 8px; 1125 background-color: var(--color_2); 1126 background-image: none; 1127 border-radius: 10px 10px 10px 10px; 1128 padding: 0% 0% 0% 0%; 1129 margin: 0% 0% 0% 0%; 1130} 1131 1132#widget_ie6 { 1133 width: auto; 1134 height: auto; 1135 max-width: 100%; 1136} 1137 1138#widget_5h5 { 1139 width: 80px; 1140 height: auto; 1141 max-width: 100%; 1142} 1143 1144#widget_vej { 1145 width: 280px; 1146 height: auto; 1147 max-width: 100%; 1148} 1149 1150#group_23b { 1151 flex-direction: row; 1152 justify-content: flex-start; 1153 align-items: center; 1154 width: 25%; 1155 min-height: 8px; 1156 padding: 0% 0% 0% 0%; 1157 margin: 0% 0% 0% 0%; 1158} 1159 1160#\34 598469b svg { 1161 fill: var(--color_7); 1162} 1163#\34 598469b { 1164 width: 30px; 1165 height: 30px; 1166 max-width: 100%; 1167 justify-self: auto; 1168 align-self: auto; 1169 order: 0; 1170 margin: 0px 0% 0px 0%; 1171 grid-area: auto / auto / auto / auto; 1172} 1173 1174#group_tbn { 1175 flex-direction: row; 1176 justify-content: flex-start;
1177 align-items: center; 1178 padding: 0% 0% 0% 0%; 1179 margin: 0% 0% 0% 0%; 1180} 1181 1182#widget_qp9 { 1183 width: 100%; 1184 height: 200px; 1185 max-width: 100%; 1186} 1187}</style> 1188 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-common-c0ae99ba-3dd8cf0da1894ab5a9043c416f0c4aca">@media all { 1189 1190#\33 710bc01 { 1191 min-height: 8px; 1192 column-gap: 4%; 1193 row-gap: 0px; 1194 width: 100%; 1195 justify-content: flex-start; 1196 align-items: center; 1197 min-width: 4%; 1198 flex-wrap: nowrap; 1199 background-color: rgba(0, 0, 0, 0); 1200 background-image: initial; 1201 padding: 0% 0% 0% 0%; 1202 margin: 0% 0% 0% 0%; 1203} 1204 1205#\39 360b347 { 1206 min-height: 144px; 1207 column-gap: 0%; 1208 row-gap: 0px; 1209 max-width: none; 1210 width: 100%; 1211 flex-wrap: nowrap; 1212 padding: 0% 0% 0% 0%; 1213 margin: 0% 0% 0% 0%; 1214} 1215 1216#\35 1d6dafc { 1217 width: 100%; 1218 height: auto; 1219 max-width: 100%; 1220 align-self: stretch; 1221} 1222 1223#bd946f29 { 1224 align-items: flex-start; 1225 width: 100%; 1226 min-height: 60px; 1227 max-height: 100%; 1228 justify-self: auto; 1229 align-self: auto; 1230 order: 1; 1231 background-color: var(--color_10); 1232 background-image: initial; 1233 border-width: 1px 0 1px 0; 1234 border-color: var(--color_2) #000000 var(--color_2) #000000; 1235 border-style: solid; 1236 padding: 0% 0% 0% 0%; 1237 margin: 0px 0% 0px 0%; 1238 grid-area: auto / auto / auto / auto; 1239} 1240 1241#b7dee1fb { 1242 box-shadow: rgba(0, 0, 0, 0.25) 0px 0px 11px 0px; 1243} 1244 1245#\37 46860ee { 1246 width: 100%; 1247 height: auto; 1248 max-width: 100%; 1249 order: 0; 1250 align-self: auto; 1251 margin-right: 0%; 1252 margin-bottom: -9px; 1253 margin-left: 0%; 1254 grid-area: auto / auto / auto / auto; 1255} 1256 1257#\36 a5bfb4c { 1258 justify-content: flex-end; 1259 align-items: flex-start; 1260 width: 100%; 1261 min-height: 80px; 1262 max-height: 100%; 1263 row-gap: 4px; 1264 column-gap: 4%; 1265 align-self: auto; 1266 order: 1; 1267 justify-self: auto; 1268 padding: 0% 0% 7px 0%; 1269 margin: 0% 0% 0% 0%; 1270 grid-area: auto / auto / auto / auto; 1271} 1272 1273#d3a805c8 {
1274 align-items: flex-start; 1275 width: 100%; 1276 min-height: 75px; 1277 max-height: 100%; 1278 row-gap: 24px; 1279 column-gap: 4%; 1280 align-self: flex-end; 1281 order: 0; 1282 padding: 0% 0% 0% 0%; 1283 margin: 0px 0% 0px 0%; 1284 grid-area: auto / auto / auto / auto; 1285} 1286 1287#\33 ad8c19c.flex-element.widget-wrapper > [data-element-type] { 1288 padding-top: 0px; 1289 padding-bottom: 7px; 1290} 1291#\33 ad8c19c { 1292 width: 100.07%; 1293 height: auto; 1294 max-width: 100%; 1295 align-self: auto; 1296 order: 0; 1297 margin: 0px 0% 0px 0%; 1298 grid-area: auto / auto / auto / auto; 1299} 1300 1301#e77f39e9 { 1302 width: 250px; 1303 height: auto; 1304 max-width: 100%; 1305 min-height: 50px; 1306 justify-self: auto; 1307 align-self: flex-end; 1308 order: 0; 1309 margin-top: 0px; 1310 margin-bottom: 0px; 1311 grid-area: auto / auto / auto / auto; 1312} 1313 1314#\35 19ddbdc { 1315 width: 376px; 1316 height: auto; 1317 max-width: 100%; 1318 justify-self: auto; 1319 align-self: auto; 1320 order: 0; 1321 margin: 0px 0% 0px 0%; 1322 grid-area: auto / auto / auto / auto; 1323} 1324 1325#\33 a2d0b90 { 1326 flex-direction: row; 1327 align-items: center; 1328 width: 100%; 1329 min-height: 80px; 1330 max-height: 100%; 1331 justify-self: auto; 1332 align-self: auto; 1333 order: 0; 1334 flex-wrap: nowrap; 1335 background-color: rgba(0, 0, 0, 0); 1336 background-image: initial; 1337 padding: 0% 2% 0% 2%; 1338 margin: 0px 0% 0px 0%; 1339 grid-area: auto / auto / auto / auto; 1340} 1341 1342#\33 b22d9b0 { 1343 align-items: flex-start; 1344 width: 100%; 1345 min-height: 75px; 1346 max-height: 100%; 1347 row-gap: 24px; 1348 column-gap: 4%; 1349 align-self: auto; 1350 order: 0; 1351 padding: 0% 0% 0% 0%; 1352 margin: 0px 0% 0px 0%; 1353 grid-area: auto / auto / auto / auto; 1354} 1355 1356#a8c6072d { 1357 width: 99.7%; 1358 height: auto; 1359 max-width: 100%; 1360 order: 0; 1361 align-self: auto; 1362 margin: 0px 0% 0px 0%; 1363 grid-area: auto / auto / auto / auto; 1364} 1365 1366#e3dfb55d { 1367 flex-direction: row; 1368 align-items: center; 1369 width: 33.09%; 1370 min-height: 149px; 1371 max-height: 100%; 1372 row-gap: 24px; 1373 column-gap: 4%; 1374 justify-self: auto; 1375 align-self: auto; 1376 order: 1; 1377 height: auto; 1378 padding: 0% 0% 0% 0%; 1379 margin: 0% 0% 0% 0%; 1380 grid-area: auto / auto / auto / auto; 1381} 1382 1383#\34 23fc4a2 { 1384 justify-content: space-between; 1385 align-items: center; 1386 width: 32.45%; 1387 min-height: 160px; 1388 max-height: 100%; 1389 row-gap: 0px; 1390 column-gap: 4%; 1391 flex-wrap: nowrap; 1392 order: 0; 1393 justify-self: auto; 1394 align-self: auto; 1395 padding: 0% 0% 0% 0%; 1396 margin: 0% 0% 0% 0%; 1397 grid-area: auto / auto / auto / auto; 1398} 1399 1400#\35 bfb3718.flex-element.widget-wrapper > [data-element-type] { 1401 padding-bottom: 0px; 1402 padding-top: 0px; 1403} 1404#\35 bfb3718 { 1405 width: 572px; 1406 height: auto; 1407 max-width: 100%; 1408 justify-self: auto; 1409 align-self: center; 1410 order: 0; 1411 margin-left: 0px; 1412 margin-right: 0px; 1413 grid-area: auto / auto / auto / auto; 1414} 1415 1416#\35 cbebde5 { 1417 justify-content: space-between; 1418 align-items: center; 1419 width: 32.51%; 1420 min-height: 160px; 1421 max-height: 100%; 1422 row-gap: 0px; 1423 column-gap: 4%; 1424 justify-self: auto; 1425 align-self: auto; 1426 order: 2; 1427 flex-wrap: nowrap; 1428 padding: 0% 0% 0% 0%; 1429 margin: 0% 0% 0% 0%; 1430 grid-area: auto / auto / auto / auto; 1431} 1432 1433#\36 881434d { 1434 align-items: flex-start; 1435 width: 574px; 1436 min-height: 113px; 1437 max-height: 100%; 1438 row-gap: 24px; 1439 column-gap: 4%; 1440 background-color: var(--color_2); 1441 background-image: none; 1442 border-radius: 8px 8px 8px 8px; 1443 height: auto; 1444 padding: 8px 0% 8px 0%; 1445 margin: 0% 0% 0% 0%; 1446} 1447 1448#\30 ad35782 { 1449 width: 99.99%; 1450 height: auto; 1451 max-width: 100%; 1452 align-self: auto; 1453 order: 1; 1454 justify-self: auto; 1455 margin-bottom: 3px; 1456 grid-area: auto / auto / auto / auto; 1457} 1458 1459#be1b9f91 { 1460 flex-direction: row; 1461 align-items: flex-start; 1462 width: 99.26%; 1463 min-height: 160px; 1464 max-height: 100%; 1465 row-gap: 24px; 1466 column-gap: 1%; 1467 order: 0; 1468 justify-self: auto; 1469 align-self: auto; 1470 padding: 0% 0% 0% 0%; 1471 margin: 0% 0% 0% 0%; 1472 grid-area: auto / auto / auto / auto; 1473} 1474 1475#\38 caf0a70 { 1476 justify-content: flex-end; 1477 align-items: flex-start; 1478 width: 100%; 1479 min-height: 80px; 1480 max-height: 100%; 1481 row-gap: 0px; 1482 column-gap: 4%; 1483 padding: 0% 0% 10px 0%; 1484 margin: 0px 0% 0px 0%; 1485} 1486 1487#a8f32aec {
1488 align-items: flex-start; 1489 width: 100%; 1490 min-height: 60px; 1491 max-height: 100%; 1492 justify-self: auto; 1493 align-self: auto; 1494 order: 2; 1495 background-color: var(--color_10); 1496 background-image: initial; 1497 padding: 0% 0% 0% 0%; 1498 margin: 0px 0% 0px 0%; 1499 grid-area: auto / auto / auto / auto; 1500} 1501 1502#\33 92df140 { 1503 width: 100%; 1504 height: auto; 1505 max-width: 100%; 1506 align-self: stretch; 1507} 1508 1509#widget_7d2 { 1510 width: 100%; 1511 height: auto; 1512 max-width: 100%; 1513 align-self: stretch; 1514 margin-left: 0px; 1515 margin-right: 0px; 1516} 1517}</style> 1518 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-common-4a710abd-3dd8cf0da1894ab5a9043c416f0c4aca">@media all { 1519 1520#a7708a16 { 1521 min-height: 8px; 1522 column-gap: 4%; 1523 row-gap: 24px; 1524 width: 100%; 1525 min-width: 4%; 1526 align-items: flex-end; 1527 padding: 16px 16px 16px 16px; 1528 margin-left: 0%; 1529 margin-top: 0%; 1530 margin-right: 0%; 1531 margin-bottom: 0%; 1532} 1533 1534#\37 8db3927 { 1535 padding-top: 2%; 1536 padding-bottom: 2%; 1537 min-height: 100px; 1538 column-gap: 4%; 1539 row-gap: 24px; 1540 width: 100%; 1541 padding-right: 0%; 1542 padding-left: 0%; 1543 margin-left: 0%; 1544 margin-top: 0%; 1545 margin-right: 0%; 1546 margin-bottom: 0%; 1547} 1548 1549#\38 e45e82b svg { 1550 fill: var(--color_11); 1551} 1552#\38 e45e82b .hamburgerButton { 1553 background-color: rgba(0,0,0,0); 1554} 1555#\38 e45e82b { 1556 width: 30px; 1557 height: 30px; 1558} 1559}</style> 1560 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-common-63a049320d35b55b4ef2a372-3dd8cf0da1894ab5a9043c416f0c4aca">@media all { 1561 1562#\36 3a049320d35b55b4ef2a374 { 1563 min-height: 8px; 1564 column-gap: 4%; 1565 row-gap: 24px; 1566 width: 100%; 1567 min-width: 4%; 1568 background-color: rgba(0, 0, 0, 0); 1569 padding: 16px 16px 16px 16px; 1570 margin: 0% 0% 0% 0%; 1571} 1572 1573#\36 3a049320d35b55b4ef2a376 { 1574 background-color: rgba(0, 0, 0, 0); 1575} 1576 1577#\36 3a049320d35b55b4ef2a377 { 1578 min-height: 200px; 1579 column-gap: 4%; 1580 row-gap: 24px; 1581 width: 100%; 1582 padding: 2% 0% 2% 0%; 1583 margin: 0% 0% 0% 0%; 1584} 1585 1586#\36 2d6a167a824ca526194d9a5 { 1587 background-color: rgba(232, 255, 133, 1); 1588} 1589 1590#widget_la2 { 1591 width: 300px; 1592 height: auto; 1593 position: relative; 1594 min-width: 10px; 1595 min-height: 10px; 1596 max-width: 100%; 1597} 1598}</style> 1599 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-common-6406026c90e1521f6ecb0eca-3dd8cf0da1894ab5a9043c416f0c4aca">@media all { 1600 1601#\36 406026c90e1521f6ecb0ecd { 1602 min-height: 8px; 1603 column-gap: 4%; 1604 row-gap: 24px; 1605 width: 100%; 1606 min-width: 4%; 1607 padding: 16px 16px 16px 16px; 1608 margin-left: 0%; 1609 margin-top: 0%; 1610 margin-right: 0%; 1611 margin-bottom: 0%; 1612} 1613 1614#\36 406026c90e1521f6ecb0ecb { 1615 background-color: var(--color_10); 1616 background-image: initial; 1617} 1618 1619#\36 406026c90e1521f6ecb0ecc { 1620 min-height: 460px; 1621 padding-top: 4%; 1622 padding-bottom: 4%; 1623 column-gap: 4%; 1624 row-gap: 24px; 1625 width: 100%; 1626 padding-right: 0%; 1627 padding-left: 0%; 1628 margin-left: 0%; 1629 margin-top: 0%; 1630 margin-right: 0%; 1631 margin-bottom: 0%; 1632} 1633}</style> 1634 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-common-2c8e291c-3dd8cf0da1894ab5a9043c416f0c4aca">@media all { 1635 1636#b7a700bf { 1637 min-height: 120px; 1638 column-gap: 4%; 1639 row-gap: 24px; 1640 max-width: none; 1641 width: 100%; 1642 flex-wrap: nowrap; 1643 padding: 1% 4% 1% 4%; 1644 margin: 0% 0% 0% 0%; 1645} 1646 1647#f9c6e236 { 1648 width: 100%; 1649 height: auto; 1650 max-width: 100%; 1651} 1652 1653#\36 405d24c { 1654 min-height: 8px; 1655 column-gap: 4%; 1656 row-gap: 24px; 1657 width: 100%; 1658 align-items: center; 1659 min-width: 4%; 1660 padding: 0% 0% 0% 0%; 1661 margin: 0% 0% 0% 0%; 1662} 1663 1664#b5a3be9f { 1665 border-style: solid; 1666 border-color: rgba(204, 204, 204, 1) #000000 #000000 #000000; 1667 border-width: 1px 0 0 0; 1668 background-color: var(--color_2); 1669 background-image: initial; 1670} 1671}</style> 1672 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-common-7b00a5de-3dd8cf0da1894ab5a9043c416f0c4aca">@media all { 1673 1674#\36 3c7b8b76c596a2813341bc5 { 1675 height: auto; 1676} 1677 1678#\39 34bad03 { 1679 width: 100%; 1680 height: auto; 1681 max-width: 100%; 1682 order: 0; 1683 justify-self: auto; 1684 align-self: auto; 1685 grid-area: auto / auto / auto / auto; 1686} 1687 1688#\31 ebb7f19 { 1689 border-width: 0px; 1690 border-color: rgba(0, 0, 0, 1); 1691 border-style: solid; 1692 background-color: var(--color_3); 1693 background-image: initial; 1694} 1695 1696#bd7b2692 { 1697 width: 280px; 1698 height: auto; 1699 max-width: 100%; 1700 justify-self: auto; 1701 align-self: auto; 1702 order: 2; 1703 min-height: 50px; 1704 grid-area: auto / auto / auto / auto; 1705} 1706 1707#\36 e06962d { 1708 min-height: 300px; 1709 column-gap: 4%; 1710 row-gap: 24px; 1711 max-width: none; 1712 width: 100%; 1713 flex-wrap: nowrap; 1714 padding: 4% 4% 4% 4%; 1715 margin: 0% 0% 0% 0%; 1716} 1717 1718#\36 f8e19aa { 1719 width: 100%; 1720 height: auto; 1721 max-width: 100%; 1722 order: 1; 1723} 1724 1725#\30 5e0ee19::before, #\30 5e0ee19 > .bgExtraLayerOverlay { 1726 border-radius: inherit; 1727 content: ''; 1728 display: block; 1729 position: absolute; 1730 width: 100%; 1731 height: 100%; 1732 top: 0; 1733 left: 0; 1734 background-color: rgba(0, 0, 0, 1); 1735 opacity: 0.3; 1736} 1737#\30 5e0ee19 { 1738 min-height: 8px; 1739 column-gap: 4%; 1740 row-gap: 24px; 1741 width: 48%; 1742 min-width: 4%; 1743 background-color: rgba(0, 0, 0, 0); 1744 padding: 50px 16px 50px 16px; 1745 margin: 0% 0% 0% 0%; 1746 background-image: url('https://le-cdn.hibuwebsites.com/f6cdfabba1c749a1a3d3b7a9f4c30c76/dms3rep/multi/opt/video-jtn-electrical-2880w.jpg') !important; 1747} 1748 1749#a1b6269f { 1750 width: 220px; 1751 height: 198px; 1752 max-width: 100%; 1753 align-self: center; 1754} 1755 1756#a019b36e { 1757 min-height: 8px; 1758 column-gap: 4%; 1759 row-gap: 24px; 1760 width: 48%; 1761 min-width: 4%; 1762 background-color: rgba(0, 0, 0, 0); 1763 padding: 16px 0% 16px 0%; 1764 margin: 0% 0% 0% 0%; 1765} 1766}</style> 1767 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-common-b6b5923e-3dd8cf0da1894ab5a9043c416f0c4aca">@media all { 1768 1769#\32 bcd41ae { 1770 background-color: var(--color_8); 1771 background-image: initial; 1772} 1773 1774#\34 efeffee { 1775 min-height: auto; 1776 column-gap: 0%; 1777 row-gap: 0px; 1778 width: 100%; 1779 flex-wrap: nowrap; 1780 max-width: none; 1781 padding: 12px 4% 12px 4%; 1782 margin: 0% 0% 0% 0%; 1783} 1784 1785#group_b0q { 1786 justify-content: flex-start; 1787 width: 33.33%; 1788 min-width: 4%; 1789 min-height: 8px; 1790 column-gap: 4%; 1791 row-gap: 2px; 1792 padding: 16px 2% 16px 0%; 1793 margin: 0% 0% 0% 0%; 1794} 1795 1796#widget_1j0 { 1797 width: 100%; 1798 height: auto; 1799 max-width: 100%; 1800 order: 0; 1801} 1802 1803#widget_p1h { 1804 width: 100%; 1805 height: 1px; 1806 max-width: 100%; 1807 min-height: 4px; 1808 justify-self: auto; 1809 align-self: auto; 1810 order: 5; 1811 grid-area: auto / auto / auto / auto; 1812} 1813 1814#widget_mse.flex-element.widget-wrapper > [data-element-type] { 1815 padding-top: 7px; 1816 padding-bottom: 0px; 1817} 1818#widget_mse { 1819 width: 80px; 1820 height: auto; 1821 min-height: 1px; 1822 max-width: 100%; 1823 order: 1; 1824} 1825 1826#\36 18f39ae { 1827 width: 100%; 1828 height: 1px; 1829 max-width: 100%; 1830 justify-self: auto; 1831 align-self: auto; 1832 order: 2; 1833 min-height: 4px; 1834 grid-area: auto / auto / auto / auto; 1835} 1836 1837#c7f3ddf3.flex-element.widget-wrapper > [data-element-type] { 1838 padding-top: 7px; 1839 padding-bottom: 0px; 1840} 1841#c7f3ddf3 { 1842 width: 80px; 1843 height: auto; 1844 min-height: 1px; 1845 max-width: 100%; 1846 order: 1; 1847} 1848 1849#f2debc4b { 1850 justify-content: flex-start; 1851 width: 33.33%; 1852 min-width: 4%; 1853 min-height: 8px; 1854 column-gap: 4%; 1855 row-gap: 2px; 1856 order: 0; 1857 padding: 16px 2% 16px 0%; 1858 margin: 0% 0% 0% 0%; 1859} 1860 1861#\36 5e14480 { 1862 width: 100%; 1863 height: auto; 1864 max-width: 100%; 1865 order: 0; 1866 justify-self: auto; 1867 align-self: auto; 1868 grid-area: auto / auto / auto / auto; 1869} 1870 1871#cbc8b291 { 1872 width: 100%; 1873 height: auto; 1874 max-width: 100%; 1875 justify-self: auto; 1876 align-self: auto; 1877 order: 0; 1878 grid-area: auto / auto / auto / auto; 1879} 1880 1881#d0234170 { 1882 justify-content: flex-start; 1883 width: 33.33%; 1884 min-width: 4%; 1885 min-height: 8px; 1886 column-gap: 4%; 1887 row-gap: 2px; 1888 order: 0; 1889 padding: 16px 2% 16px 0%; 1890 margin: 0% 0% 0% 0%; 1891} 1892 1893#\35 02c2597 { 1894 width: 100%; 1895 height: 1px; 1896 max-width: 100%; 1897 order: 2; 1898 justify-self: auto; 1899 align-self: auto; 1900 min-height: 4px; 1901 grid-area: auto / auto / auto / auto; 1902} 1903 1904#\30 b55734b.flex-element.widget-wrapper > [data-element-type] { 1905 padding-top: 7px; 1906 padding-bottom: 0px; 1907} 1908#\30 b55734b { 1909 width: 80px; 1910 height: auto; 1911 min-height: 1px; 1912 max-width: 100%; 1913 order: 1; 1914} 1915 1916#widget_ipg { 1917 width: 180px; 1918 height: auto; 1919 max-width: 100%; 1920 order: 6; 1921 margin-top: 5px; 1922} 1923 1924#widget_k5l.flex-element.widget-wrapper > [data-element-type] { 1925 padding: 0px 0px 0px 0px; 1926} 1927#widget_k5l { 1928 width: 99.83%; 1929 height: auto; 1930 max-width: 100%; 1931 order: 2; 1932 justify-self: auto; 1933 align-self: auto; 1934 margin: 6px 0% 0px 0%; 1935 grid-area: auto / auto / auto / auto; 1936} 1937 1938#\31 db7e0c7 { 1939 width: 180px; 1940 height: auto; 1941 max-width: 100%; 1942 justify-self: auto; 1943 align-self: auto; 1944 order: 5; 1945 margin-right: 0%; 1946 margin-left: 0%; 1947 margin-top: 10px; 1948 grid-area: auto / auto / auto / auto; 1949} 1950 1951#widget_6mu.flex-element.widget-wrapper > [data-element-type] { 1952 padding: 0px 0px 0px 0px; 1953} 1954#widget_6mu {
1955 width: 99.76%; 1956 height: auto; 1957 max-width: 100%; 1958 order: 3; 1959 justify-self: auto; 1960 align-self: auto; 1961 margin: 1px 0% 0px 0%; 1962 grid-area: auto / auto / auto / auto; 1963} 1964 1965#widget_391 { 1966 width: 71.45%; 1967 height: auto; 1968 max-width: 100%; 1969 align-self: flex-start; 1970 margin-top: 10px; 1971 order: 6; 1972} 1973 1974#widget_umk.flex-element.widget-wrapper > [data-element-type] { 1975 padding: 0px 0px 0px 0px; 1976} 1977#widget_umk { 1978 width: 83.63%; 1979 height: auto; 1980 max-width: 100%; 1981 order: 2; 1982 margin-top: 6px; 1983} 1984 1985#widget_9dv { 1986 width: 100%; 1987 height: auto; 1988 max-width: 100%; 1989 order: 5; 1990 margin-top: 15px; 1991} 1992 1993#\38 7a53ad2 { 1994 width: 99.8%; 1995 height: auto; 1996 max-width: 100%; 1997 justify-self: auto; 1998 align-self: auto; 1999 order: 3; 2000 grid-area: auto / auto / auto / auto; 2001} 2002 2003#\36 efb2802 { 2004 width: 99.8%; 2005 height: auto; 2006 max-width: 100%; 2007 justify-self: auto; 2008 align-self: auto; 2009 order: 4; 2010 grid-area: auto / auto / auto / auto; 2011} 2012}</style> 2013 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-common-2f831c0f-3dd8cf0da1894ab5a9043c416f0c4aca">@media all { 2014 2015#\30 0841c9d { 2016 min-height: 8px; 2017 column-gap: 4%; 2018 row-gap: 24px; 2019 flex-direction: row; 2020 width: 100%; 2021 justify-content: flex-start; 2022 align-items: center; 2023 min-width: 4%; 2024 padding: 0px 16px 0px 16px; 2025 margin: 0px 0% 0px 0%; 2026} 2027 2028#d027d36f { 2029 min-height: auto; 2030 column-gap: 0%; 2031 row-gap: 0px; 2032 width: 100%; 2033 flex-wrap: nowrap; 2034 padding: 1% 2% 0% 2%; 2035} 2036 2037#\30 253b4c6 { 2038 background-color: var(--color_2); 2039 background-image: none; 2040} 2041 2042#\37 1171ca4 { 2043 margin: 0px 0% 0px 0%; 2044} 2045 2046#\37 e326d80 { 2047 width: 100%; 2048 height: auto; 2049 max-width: 100%; 2050 order: 0; 2051 align-self: auto; 2052 justify-self: auto; 2053 margin: 0px 0% 0px 0%; 2054 grid-area: auto / auto / auto / auto; 2055} 2056}</style> 2057 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-common-54290af5-3dd8cf0da1894ab5a9043c416f0c4aca">@media all { 2058 2059#\37 ca01c83 { 2060 min-height: 8px; 2061 column-gap: 4%; 2062 row-gap: 24px; 2063 width: 100%; 2064 min-width: 4%; 2065 padding: 0% 0% 0% 0%; 2066 margin: 0% 0% 0% 0%; 2067} 2068 2069#\32 4eff763 { 2070 min-height: 0px; 2071 column-gap: 4%; 2072 row-gap: 24px; 2073 width: 100%; 2074 flex-wrap: nowrap; 2075 max-width: none; 2076 padding: 5px 4% 5px 4%; 2077 margin: 0% 0% 0% 0%; 2078} 2079 2080#\33 4b9f983 { 2081 width: 100%; 2082 height: auto; 2083 max-width: 100%; 2084 justify-self: auto; 2085 align-self: flex-start; 2086 order: 0; 2087 grid-area: auto / auto / auto / auto; 2088} 2089 2090#\33 989af5b { 2091 background-color: var(--color_2); 2092 background-image: initial; 2093} 2094}</style> 2095 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-common-4dc8a903-3dd8cf0da1894ab5a9043c416f0c4aca">@media all { 2096 2097#\30 190983d { 2098 justify-content: flex-start; 2099 width: 22.75%; 2100 min-width: 4%; 2101 min-height: 8px; 2102 column-gap: 4%; 2103 row-gap: 32px; 2104 border-style: solid; 2105 border-color: rgba(0, 0, 0, 1) rgba(0, 0, 0, 1) rgba(0, 0, 0, 1) rgba(0, 0, 0, 1); 2106 border-width: 0px 0px 0px 0px; 2107 padding: 2% 1% 2% 2%; 2108 margin: 0% 0% 0% 0%; 2109} 2110 2111#\36 c5257e3 { 2112 margin: 0px 0% 0px 0%; 2113} 2114 2115#d200b2f7 { 2116 justify-content: flex-start; 2117 width: 22.75%; 2118 min-width: 4%; 2119 min-height: 8px; 2120 column-gap: 4%; 2121 row-gap: 32px; 2122 border-style: solid; 2123 border-color: rgba(0, 0, 0, 1) rgba(0, 0, 0, 1) rgba(0, 0, 0, 1) rgba(0, 0, 0, 1); 2124 border-width: 0px 0px 0px 0px; 2125 padding: 2% 1% 2% 2%; 2126 margin: 0% 0% 0% 0%; 2127} 2128 2129#\32 cb3e9f9 { 2130 background-color: var(--color_10); 2131 background-image: url(); 2132 border-style: solid; 2133 border-color: rgba(0, 0, 0, 1) rgba(0, 0, 0, 1) rgba(0, 0, 0, 1) rgba(0, 0, 0, 1); 2134 border-width: 0px 0px 0px 0px; 2135} 2136 2137#\32 7581a62 { 2138 min-height: auto; 2139 flex-wrap: nowrap; 2140 column-gap: 3%; 2141 row-gap: 18px; 2142 max-width: none; 2143 width: 100%; 2144 padding: 0% 0% 0% 0%; 2145 margin: 0px 0% 0px 0%; 2146 grid-area: 1 / 1 / span 1 / span 1; 2147} 2148 2149#ad9cb284 { 2150 justify-content: flex-start; 2151 width: 22.75%; 2152 min-width: 4%; 2153 min-height: 8px; 2154 column-gap: 4%; 2155 row-gap: 32px; 2156 border-style: solid; 2157 border-color: rgba(0, 0, 0, 1) rgba(0, 0, 0, 1) rgba(0, 0, 0, 1) rgba(0, 0, 0, 1); 2158 border-width: 0px 0px 0px 0px; 2159 order: 0; 2160 padding: 2% 1% 2% 2%; 2161 margin: 0% 0% 0% 0%; 2162} 2163 2164#widget_cq9 { 2165 width: 100%; 2166 height: auto; 2167 max-width: 100%; 2168 align-self: stretch; 2169} 2170 2171#group_9n6 { 2172 width: 22.75%; 2173 min-width: 4%; 2174 min-height: 8px; 2175 column-gap: 4%; 2176 row-gap: 32px; 2177 justify-content: flex-start; 2178 padding: 2% 1% 2% 2%; 2179 margin: 0% 0% 0% 0%; 2180} 2181 2182#b51d88ee { 2183 width: 100%; 2184 height: auto; 2185 max-width: 100%; 2186 align-self: auto; 2187 justify-self: auto; 2188 order: 0; 2189 margin: 0px 0% 0px 0%; 2190 grid-area: auto / auto / auto / auto; 2191} 2192 2193#\35 aa033c8 { 2194 width: 100%; 2195 height: auto; 2196 max-width: 100%; 2197 align-self: auto; 2198 justify-self: auto; 2199 order: 0; 2200 margin: 0px 0% 0px 0%; 2201 grid-area: auto / auto / auto / auto; 2202} 2203 2204#\30 4df1da2 { 2205 width: 100%; 2206 height: auto; 2207 max-width: 100%; 2208 align-self: auto; 2209 justify-self: auto; 2210 order: 0; 2211 margin: 0px 0% 0px 0%; 2212 grid-area: auto / auto / auto / auto; 2213} 2214 2215#f09d8243 { 2216 width: 100%; 2217 height: auto; 2218 max-width: 100%; 2219 align-self: auto; 2220 justify-self: auto; 2221 order: 0; 2222 margin: 0px 0% 0px 0%; 2223 grid-area: auto / auto / auto / auto; 2224} 2225 2226#\38 99c2eb2 { 2227 width: 100%; 2228 height: auto; 2229 max-width: 100%; 2230 align-self: auto; 2231 justify-self: auto; 2232 order: 0; 2233 margin: 0px 0% 0px 0%; 2234 grid-area: auto / auto / auto / auto; 2235} 2236 2237#\37 b991c9b { 2238 width: 100%; 2239 height: auto; 2240 max-width: 100%; 2241 align-self: auto; 2242 justify-self: auto; 2243 order: 0; 2244 margin: 0px 0% 0px 0%; 2245 grid-area: auto / auto / auto / auto; 2246} 2247 2248#\33 9a585e9 { 2249 width: 100%; 2250 height: auto; 2251 max-width: 100%; 2252 align-self: auto; 2253 justify-self: auto; 2254 order: 0; 2255 margin: 0px 0% 0px 0%; 2256 grid-area: auto / auto / auto / auto; 2257} 2258 2259#\34 310a1b0 { 2260 width: 100%; 2261 height: auto; 2262 max-width: 100%; 2263 align-self: auto; 2264 justify-self: auto; 2265 order: 0; 2266 margin: 0px 0% 0px 0%; 2267 grid-area: auto / auto / auto / auto; 2268} 2269 2270#fe7f7167 { 2271 width: 100%; 2272 height: auto; 2273 max-width: 100%; 2274 align-self: auto; 2275 order: 0; 2276 margin: 0px 0% 0px 0%; 2277 grid-area: auto / auto / auto / auto; 2278} 2279}</style> 2280 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-common-2a
2280501ecf-3dd8cf0da1894ab5a9043c416f0c4aca">@media all { 2281 2282#\30 8465346 { 2283 min-height: auto; 2284 column-gap: 3%; 2285 row-gap: 18px; 2286 width: 100%; 2287 flex-wrap: nowrap; 2288 max-width: var(--max-content-width_1, 1200px); 2289 padding: 2% 0% 2% 0%; 2290} 2291 2292#\35 a572945 { 2293 min-height: 8px; 2294 column-gap: 4%; 2295 row-gap: 0px; 2296 width: 100%; 2297 min-width: 4%; 2298 padding: 0px 0px 0px 0px; 2299 margin: 0px 0% 0px 0%; 2300} 2301 2302#\35 7b153fb { 2303 background-color: var(--color_10); 2304 background-image: none; 2305} 2306 2307#c55100d8 { 2308 margin: 0px 0% 0px 0%; 2309} 2310 2311#widget_gf3 { 2312 width: auto; 2313 height: auto; 2314 max-width: 100%; 2315} 2316 2317#undefined { 2318 width: 50%; 2319} 2320}</style> 2321 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-common-8a9cfcfc-3dd8cf0da1894ab5a9043c416f0c4aca">@media all { 2322 2323#\37 2168835 { 2324 grid-template-rows: minmax(300px, max-content); 2325 width: 100%; 2326 max-width: 100%; 2327} 2328 2329#\37 5ba1c41 { 2330 min-height: 240px; 2331 column-gap: 4%; 2332 row-gap: 24px; 2333 max-width: none; 2334 width: 100%; 2335 flex-wrap: nowrap; 2336 padding: 0% 0% 0% 0%; 2337 margin: 0% 0% 0% 0%; 2338} 2339 2340#\34 f4d3298 { 2341 min-height: 8px; 2342 column-gap: 4%; 2343 row-gap: 24px; 2344 width: 100%; 2345 min-width: 4%; 2346 padding: 0% 0% 0% 0%; 2347 margin: 0% 0% 0% 0%; 2348} 2349 2350#widget_21v { 2351 width: 60px; 2352 height: auto; 2353 max-width: 100%; 2354 order: 5; 2355 justify-self: end; 2356 align-self: end; 2357 margin: 40px 40px 40px 40px; 2358} 2359 2360#widget_fpt { 2361 width: auto; 2362 height: auto; 2363 max-width: 100%; 2364 justify-self: end; 2365 align-self: end; 2366 margin: 40px 40px 40px 40px; 2367} 2368 2369#widget_mnf { 2370 width: auto; 2371 height: auto; 2372 min-width: auto; 2373 min-height: auto; 2374 justify-self: end; 2375 align-self: end; 2376 order: 0; 2377 max-width: 100%; 2378 margin: 377px 0% 0px 0%; 2379} 2380 2381#widget_c5v { 2382 width: auto; 2383 height: auto; 2384 min-width: auto; 2385 min-height: auto; 2386 justify-self: end; 2387 align-self: end; 2388 order: 0; 2389 max-width: 100%; 2390 max-height: none; 2391 margin: 377px 0% 0px 0%; 2392} 2393 2394#widget_doa { 2395 width: auto; 2396 height: auto; 2397 min-width: auto; 2398 min-height: auto; 2399 justify-self: end; 2400 align-self: end; 2401 order: 0; 2402 max-width: 100%; 2403 margin: 377px 0% 0px 0%; 2404} 2405 2406#widget_301 { 2407 width: auto; 2408 height: auto; 2409 max-width: 100%; 2410 justify-self: end; 2411 align-self: end; 2412 min-height: auto; 2413 min-width: auto; 2414 margin: 40px 40px 40px 40px; 2415} 2416 2417#widget_61p { 2418 width: auto; 2419 height: auto; 2420 position: relative; 2421 min-width: auto; 2422 min-height: auto; 2423 justify-self: end; 2424 align-self: end; 2425 order: 0; 2426 max-width: 100%; 2427 margin: 377px 0% 0px 0%; 2428} 2429}</style> 2430 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-desktop-flex-header-3dd8cf0da1894ab5a9043c416f0c4aca">@media (min-width: 1025px) { 2431}</style> 2432 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-tablet_implicit-flex-header-3dd8cf0da1894ab5a9043c416f0c4aca">@media (min-width: 768px) and (max-width: 1024px) { 2433}</style> 2434 <style data-page-uuid="8d67b06083f34b4a8a328c95f017fec2" id="grid-styles-tablet-7ef32d84-8d67b06083f34b4a8a328c95f017fec2">@media (min-width: 768px) and (max-width: 1024px) { 2435 2436#\36 a108e83 { 2437 padding-left: 4%; 2438 padding-right: 4%; 2439} 2440}</style> 2441 <style data-page-uuid="8d67b06083f34b4a8a328c95f017fec2" id="grid-styles-tablet-5c4456af-8d67b06083f34b4a8a328c95f017fec2">@media (min-width: 768px) and (max-width: 1024px) { 2442 2443#\35 20e9609 { 2444 padding-left: 0%; 2445 padding-right: 0%; 2446} 2447 2448#b6321765 { 2449 justify-self: auto; 2450 align-self: auto; 2451 width: 100%; 2452 order: 0; 2453 margin: 0px 0% 0px 0%; 2454 grid-area: auto / auto / auto / auto; 2455} 2456 2457#\36 8fb7ec9 { 2458 padding-left: 20px; 2459 padding-right: 20px; 2460} 2461}</style> 2462 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-tablet-flex-footer-3dd8cf0da1894ab5a9043c416f0c4aca">@media (min-width: 768px) and (max-width: 1024px) { 2463 2464}</style> 2465 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-tablet-flex-header-3dd8cf0da1894ab5a9043c416f0c4aca">@media (min-width: 768px) and (max-width: 1024px) { 2466 2467#flex-header { 2468 2469 padding-top: 0%; 2470 margin-top: 0px; 2471} 2472}</style> 2473 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-tablet-405ddf11-3dd8cf0da1894ab5a9043c416f0c4aca">@media (min-width: 768px) and (max-width: 1024px) { 2474 2475#\31 d3acdb9 { 2476 padding-left: 4%; 2477 padding-right: 4%; 2478 flex-direction: row; 2479 flex-wrap: nowrap; 2480 min-height: 80px; 2481} 2482}</style> 2483 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-tablet-157b4fff-3dd8cf0da1894ab5a9043c416f0c4aca">@media (min-width: 768px) and (max-width: 1024px) { 2484 2485#\33 2767c2f { 2486 padding-left: 1%; 2487 padding-right: 1%; 2488 min-height: 92px; 2489 flex-direction: row; 2490 flex-wrap: nowrap; 2491 column-gap: 1%; 2492} 2493 2494#\32 fd7b414 { 2495 width: 21.2%; 2496 min-height: 8px; 2497 order: 1; 2498 column-gap: 0%; 2499 justify-content: center; 2500 padding-left: 0px; 2501 padding-right: 0px; 2502} 2503 2504#group_7in { 2505 width: 21.2%; 2506 min-height: 8px; 2507 order: 3; 2508 justify-content: center; 2509} 2510 2511#group_jjh { 2512 width: 34.5%; 2513 min-height: 8px; 2514 justify-content: center; 2515 order: 2; 2516 margin: 10px 2% 10px 2%; 2517} 2518 2519#widget_ie6 {
2520 align-self: auto; 2521 margin: 0px 0% 0px 0%; 2522 grid-area: auto / auto / auto / auto; 2523} 2524 2525#widget_5h5 { 2526 width: 220px; 2527 height: auto; 2528 align-self: auto; 2529 margin: 0px 0% 0px 0%; 2530 grid-area: auto / auto / auto / auto; 2531} 2532 2533#widget_vej { 2534 width: 130px; 2535 align-self: auto; 2536 min-height: 40px; 2537 margin: 0px 0% 0px 0%; 2538 grid-area: auto / auto / auto / auto; 2539} 2540 2541#group_23b { 2542 width: 8.33%; 2543 min-height: 8px; 2544 order: 4; 2545 justify-content: center; 2546} 2547 2548#group_tbn { 2549 order: 0; 2550 width: 8.33%; 2551 min-height: 8px; 2552} 2553 2554#widget_qp9 { 2555 height: 6px; 2556} 2557}</style> 2558 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-tablet-c0ae99ba-3dd8cf0da1894ab5a9043c416f0c4aca">@media (min-width: 768px) and (max-width: 1024px) { 2559 2560#\39 360b347 { 2561 padding-left: 2%; 2562 padding-right: 2%; 2563 flex-direction: row; 2564 flex-wrap: nowrap; 2565 min-height: 100px; 2566 padding-bottom: 12px; 2567} 2568 2569#\33 710bc01 { 2570 width: 100%; 2571 min-height: 8px; 2572} 2573 2574#ac810934 { 2575 margin-top: 0px; 2576} 2577}</style> 2578 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-tablet-4a710abd-3dd8cf0da1894ab5a9043c416f0c4aca">@media (min-width: 768px) and (max-width: 1024px) { 2579 2580#\38 e45e82b { 2581 justify-self: auto; 2582 align-self: auto; 2583 order: 0; 2584 margin: 0px 0% 0px 0%; 2585 grid-area: auto / auto / auto / auto; 2586} 2587}</style> 2588 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-tablet-63a049320d35b55b4ef2a372-3dd8cf0da1894ab5a9043c416f0c4aca">@media (min-width: 768px) and (max-width: 1024px) { 2589 2590#\36 3a049320d35b55b4ef2a374 { 2591 justify-content: flex-start; 2592 padding-top: 10px; 2593 padding-left: 8px; 2594 padding-right: 8px; 2595} 2596 2597#\36 3a049320d35b55b4ef2a377 { 2598 min-height: 672px; 2599 padding: 0% 4% 0% 4%; 2600} 2601 2602#widget_la2 { 2603 width: auto; 2604 height: auto; 2605} 2606}</style> 2607 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-tablet-6406026c90e1521f6ecb0eca-3dd8cf0da1894ab5a9043c416f0c4aca">@media (min-width: 768px) and (max-width: 1024px) { 2608 2609#\36 406026c90e1521f6ecb0ecc { 2610 min-height: 228px; 2611 padding: 0% 4% 0% 4%; 2612} 2613 2614#\36 406026c90e1521f6ecb0ecd { 2615 justify-content: center; 2616 padding-bottom: 6%; 2617 padding-top: 6%; 2618} 2619}</style> 2620 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-tablet-2c8e291c-3dd8cf0da1894ab5a9043c416f0c4aca">@media (min-width: 768px) and (max-width: 1024px) { 2621 2622#b7a700bf { 2623 padding-left: 2%; 2624 padding-right: 2%; 2625} 2626}</style> 2627 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-tablet-7b00a5de-3dd8cf0da1894ab5a9043c416f0c4aca">@media (min-width: 768px) and (max-width: 1024px) { 2628 2629#\30 5e0ee19::before, #\30 5e0ee19 > .bgExtraLayerOverlay { 2630 2631} 2632#\30 5e0ee19 { 2633 background-image: url('https://le-cdn.hibuwebsites.com/f6cdfabba1c749a1a3d3b7a9f4c30c76/dms3rep/multi/opt/video-jtn-electrical-2880w.jpg') !important; 2634} 2635}</style> 2636 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-tablet-b6b5923e-3dd8cf0da1894ab5a9043c416f0c4aca">@media (min-width: 768px) and (max-width: 1024px) { 2637 2638#\34 efeffee { 2639 padding-left: 4%; 2640 padding-right: 4%; 2641 flex-direction: row; 2642 flex-wrap: wrap; 2643 column-gap: 0%; 2644 min-height: 240px; 2645} 2646 2647#widget_391 { 2648 width: 71.00%; 2649} 2650 2651#widget_1j0 { 2652 justify-self: auto; 2653 align-self: auto; 2654 width: 100%; 2655 order: 0; 2656 margin: 0px 0% 0px 0%; 2657 grid-area: auto / auto / auto / auto; 2658} 2659 2660#widget_mse { 2661 order: 1; 2662} 2663 2664#widget_p1h { 2665 order: 2; 2666} 2667 2668#widget_ipg { 2669 order: 5; 2670} 2671 2672#group_b0q { 2673 width: 50%; 2674 flex: 1 1 auto; 2675 margin: 0% 0% 0% 0%; 2676} 2677 2678#d0234170 { 2679 width: 50%; 2680 flex: 1 1 auto; 2681 margin: 0% 0% 0% 0%; 2682} 2683 2684#f2debc4b { 2685 width: 50%; 2686 flex: 1 1 auto; 2687 margin: 0% 0% 0% 0%; 2688} 2689 2690#widget_6mu {
2691 width: 50%; 2692} 2693}</style> 2694 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-tablet-2f831c0f-3dd8cf0da1894ab5a9043c416f0c4aca">@media (min-width: 768px) and (max-width: 1024px) { 2695 2696#d027d36f { 2697 padding-left: 4%; 2698 padding-right: 4%; 2699} 2700}</style> 2701 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-tablet-54290af5-3dd8cf0da1894ab5a9043c416f0c4aca">@media (min-width: 768px) and (max-width: 1024px) { 2702 2703#\32 4eff763 { 2704 padding-left: 4%; 2705 padding-right: 4%; 2706} 2707}</style> 2708 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-tablet-4dc8a903-3dd8cf0da1894ab5a9043c416f0c4aca">@media (min-width: 768px) and (max-width: 1024px) { 2709 2710#\30 190983d { 2711 order: 0; 2712 width: 25%; 2713 min-height: 8px; 2714} 2715 2716#\36 c5257e3 { 2717 margin-left: 0%; 2718 margin-right: 0%; 2719} 2720 2721#d200b2f7 { 2722 order: 0; 2723 width: 25%; 2724 min-height: 8px; 2725} 2726 2727#\32 7581a62 { 2728 flex-direction: row; 2729 min-height: 240px; 2730 flex-wrap: nowrap; 2731 row-gap: 0px; 2732 column-gap: 0%; 2733 padding: 0% 0% 0% 0%; 2734} 2735 2736#ad9cb284 { 2737 width: 25%; 2738 min-height: 8px; 2739} 2740 2741#group_9n6 { 2742 width: 25%; 2743 min-height: 8px; 2744} 2745}</style> 2746 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-tablet-2a501ecf-3dd8cf0da1894ab5a9043c416f0c4aca">@media (min-width: 768px) and (max-width: 1024px) { 2747 2748#\30 8465346 { 2749 padding-left: 2%; 2750 padding-right: 2%; 2751} 2752}</style> 2753 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-tablet-8a9cfcfc-3dd8cf0da1894ab5a9043c416f0c4aca">@media (min-width: 768px) and (max-width: 1024px) { 2754 2755#\37 5ba1c41 { 2756 padding-left: 2%; 2757 padding-right: 2%; 2758} 2759}</style> 2760 2761 2762 2763 2764 2765 2766<style id="globalFontSizeStyle" type="text/css"> 2767 @media (min-width: 1025px) { [data-version] .size-13, [data-version] .font-size-13 {--font-size: 13;}[data-version] .size-24, [data-version] .font-size-24 {--font-size: 24;}[data-version] .size-18, [data-version] .font-size-18 {--font-size: 18;}[data-version] .size-15, [data-version] .font-size-15 {--font-size: 15;} }@media (min-width: 768px) and (max-width: 1024px) { [data-version] .t-size-13, [data-version] .t-font-size-13 {--font-size: 13;}[data-version] .t-size-20, [data-version] .t-font-size-20 {--font-size: 20;}[data-version] .t-size-16, [data-version] .t-font-size-16 {--font-size: 16;}[data-version] .t-size-15, [data-version] .t-font-size-15 {--font-size: 15;} }@media (max-width: 767px) { [data-version] .m-size-13, [data-version] .m-font-size-13 {--font-size: 13;}[data-version] .m-size-19, [data-version] .m-font-size-19 {--font-size: 19;}[data-version] .m-size-16, [data-version] .m-font-size-16 {--font-size: 16;}[data-version] .m-size-15, [data-version] .m-font-size-15 {--font-size: 15;} } 2768</style> 2769<style id="pageFontSizeStyle" type="text/css"> 2770</style> 2771 2772 2773 2774 2775<style id="hideAnimFix"> 2776 .dmDesktopBody:not(.editGrid) [data-anim-desktop]:not([data-anim-desktop='none']), .dmDesktopBody:not(.editGrid) [data-anim-extended] { 2777 visibility: hidden; 2778 } 2779 2780 .dmDesktopBody:not(.editGrid) .dmNewParagraph[data-anim-desktop]:not([data-anim-desktop='none']), .dmDesktopBody:not(.editGrid) .dmNewParagraph[data-anim-extended] { 2781 visibility: hidden !important; 2782 } 2783 2784 #dmRoot:not(.editGrid) .flex-element [data-anim-extended] { 2785 visibility: hidden; 2786 } 2787 2788</style> 2789 2790 2791 2792 2793<style id="fontFallbacks"> 2794 @font-face { 2795 font-family: "Poppins Fallback"; 2796 src: local('Arial'); 2797 ascent-override: 92.326%; 2798 descent-override: 30.7753%; 2799 line-gap-override: 8.793%; 2800 size-adjust: 113.7274%; 2801}@font-face { 2802 font-family: "Kumbh Sans Fallback"; 2803 src: local('Arial'); 2804 ascent-override: 93.9558%; 2805 descent-override: 24.1866%; 2806 size-adjust: 104.9779%; 2807 line-gap-override: 0%; 2808 }@font-face { 2809 font-family: "Racing Sans One Fallback"; 2810 src: local('Arial'); 2811 ascent-override: 93.8589%; 2812 descent-override: 27.3114%; 2813 size-adjust: 103.9858%; 2814 line-gap-override: 0%; 2815 } 2816</style> 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828<meta property="og:type" content="website"> 2829<meta property="og:url" content="https://www.jtnelectrical.com/request-service-form"> 2830 2831 <title> 2832 Request Service | JTN Electrical 2833 </title> 2834 <meta name="description" content="Request a service from JTN Electrical. Emergency electrical services available. Quick response times. Free estimates."/> 2835 2836 <meta name="twitter:card" content="summary"/> 2837 <meta name="twitter:title" content="Request Service | JTN Electrical"/> 2838 <meta name="twitter:description" content="Request a service from JTN Electrical. Emergency electrical services available. Quick response times. Free estimates."/> 2839 <meta name="twitter:image" content="https://le-cdn.hibuwebsites.com/f6cdfabba1c749a1a3d3b7a9f4c30c76/dms3rep/multi/opt/social-image-jtn-electrical-1920w.jpg"/> 2840 <meta property="og:description" content="Request a service from JTN Electrical. Emergency electrical services available. Quick response times. Free estimates."/> 2841 <meta property="og:title" content="Request Service | JTN Electrical"/> 2842 <meta property="og:image" content="https://le-cdn.hibuwebsites.com/f6cdfabba1c749a1a3d3b7a9f4c30c76/dms3rep/multi/opt/social-image-jtn-electrical-1920w.jpg"/> 2843 2844 2845 2846 2847<!-- SYS- VVNfSElCVV9QUk9EVUNUSU9O --> 2848</head> 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870<body id="dmRoot" data-page-alias="request-service-form" class="dmRoot fix-mobile-scrolling flex-site dmResellerSite " 2871 style="padding:0;margin:0;" 2872 2873 data-flex-site > 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890<div id="dm" class='dmwr'> 2891 2892 <div class="dm_wrapper -var5 null "> 2893 <div dmwrapped="true" id="1901957768" class="dm-home-page" themewaschanged="true"> <div dmtemplateid="FlexHeader" data-responsive-name="FlexLayout" class="runtime-module-container dm-bfs dm-layout-sec hasAnimations hasStickyHeader inMiniHeaderMode rows-1200 hamburger-reverse dmPageBody d-page-1716942098 inputs-css-clean dmDHeader" id="dm-outer-wrapper" data-page-class="1716942098" data-soch="true" data-background-parallax-selector=".dmHomeSection1, .dmSectionParallex"> <div id="dmStyle_outerContainer" class="dmOuter"> <div id="dmStyle_innerContainer" class="dmInner" data-auto="page-inner"> <div class="dmLayoutWrapper standard-var dmStandardDesktop"> <div id="site_content"> <div id="dmFlexHeaderContainer" class="flex_hfcontainer"> <div id="flex-header" data-variation-id="variation0" data-sticky="true" data-changeable-on-scroll-properties="|text-color|" data-changeable-on-scroll="true" role="banner"> <div data-layout-section="" data-auto="flex-section" id="1956345911" data-flex-id="405ddf11" class="" data-hidden-on-tablet="true" data-hidden-on-desktop="true"> <div class="flex-widgets-container" id="1211899340"></div> 2894 <section id="6c317a78" class="flex-element section" data-auto="flex-element-section"> <div id="11fd5064" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="1d3acdb9" class="flex-element group" data-auto="flex-element-group" data-hidden-on-tablet="" data-hidden-on-desktop=""> <div id="9f3f031f" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop="" data-hidden-on-tablet=""> <div id="98702d80" class="flex-element group" data-auto="flex-element-group"> <div id="4cb86e4c" class="flex-element group" data-auto="flex-element-group"> <div id="9e5ef93d" class="flex-element group" data-auto="flex-element-group"> <div id="ec5d46f2" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1652217484"> <div class="widget-9015bc u_1652217484 dmCustomWidget" data-lazy-load="true" data-title="" id="1652217484" dmle_extension="custom_extension" data-element-type="custom_extension" data-binding="W3siYmluZGluZ05hbWUiOiJtZXJjaGFudElkIiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJSZXZpZXdzIFdpZGdldHNcIixcImZuXCI6XCJNZXJjaGFudCBJRFwifSJ9XQ==" icon="false" surround="false" data-widget-id="9015bcbf1ace437aa6f0e66d5bb2e7d3" data-widget-version="25" data-widget-config="eyJtZXJjaGFudElkIjoiODA3OTgiLCJtaW4tcmV2aWV3cyI6IjMiLCJtaW4tcmF0aW5nIjoiNCIsInNob3ctd3JpdGUiOnRydWUsImhpZGUtc3VtbWFyeSI6ZmFsc2UsInRoZW1lIjoibGlnaHQifQ=="> <div class="headerWidget" data-merchant="80798" data-theme="light" data-break="" data-count-limit="3" data-rating-limit="4" data-show-write="true" data-hide-summary="false"></div> 2895</div> 2896</div> 2897</div> 2898</div> 2899 <div id="2e9284f4" class="flex-element group" data-auto="flex-element-group"> <div id="da0131a7" class="flex-element group" data-auto="flex-element-group"> <div id="bff1bb66" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="image" data-keep-proportion="true" data-external-id="1301876968"> <div class="imageWidget align-center" data-element-type="image" data-widget-type="image" id="1301876968" data-binding="W3siYmluZGluZ05hbWUiOiJpbWFnZSIsInZhbHVlIjoic2l0ZV9pbWFnZXMucGVyc2lzdGVkLmRhcmstbG9nbyJ9XQ=="> <a href="/" id="1415691439"><img src="https://le-cdn.hibuwebsites.com/f6cdfabba1c749a1a3d3b7a9f4c30c76/dms3rep/multi/opt/logo-jtn-electrical-1920w.jpg" alt="JTN Electrical Logo" id="1635795816" class="" width="529" height="170" data-dm-image-path="https://cdn.hibuwebsites.com/f6cdfabba1c749a1a3d3b7a9f4c30c76/dms3rep/multi/logo-jtn-electrical.jpg" data-diy-image="" onerror="handleImageLoadError(this)"/></a> 2900</div> 2901</div> 2902</div> 2903 <div id="11d216b4" class="flex-element group" data-auto="flex-element-group"> <div id="07327faa" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="spacer" data-external-id="1269358429"> <div data-element-type="spacer" class="dmSpacer" id="1269358429"></div> 2904</div> 2905</div> 2906 <div id="3aa37ebb" class="flex-element group" data-auto="flex-element-group"> <div id="5801a4e8" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="hamburgerButton" data-external-id="1460227856"> <button class="hamburgerButton" data-element-type="hamburgerButton" id="1460227856" aria-label="Open menu" aria-controls="hamburger-drawer" aria-expanded="false"> <svg width="100pt" height="100pt" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1111394517"> <path d="m3.6172 10.754h-0.050782c-1.9727 0-3.5703 1.5977-3.5703 3.5703 0 1.9727 1.5977 3.5703 3.5703 3.5703h0.054688 92.758 0.050782c1.9727 0 3.5703-1.5977 3.5703-3.5703 0-1.9727-1.5977-3.5703-3.5703-3.5703h-0.054688zm0 35.676h-0.050782c-1.9727 0-3.5703 1.5977-3.5703 3.5703s1.5977 3.5703 3.5703 3.5703h0.054688 92.758 0.050782c1.9727 0 3.5703-1.5977 3.5703-3.5703s-1.5977-3.5703-3.5703-3.5703h-0.054688zm0 35.68h-0.050782c-1.9727 0-3.5703 1.5977-3.5703 3.5703 0 1.9727 1.5977 3.5703 3.5703 3.5703h0.054688 92.758 0.050782c1.9727 0 3.5703-1.5977 3.5703-3.5703 0-1.9727-1.5977-3.5703-3.5703-3.5703h-0.054688z"></path> 2907</svg> 2908</button> 2909</div> 2910</div> 2911</div> 2912 <div id="148da2d9" class="flex-element group" data-auto="flex-element-group"> <div id="0ece3d12" class="flex-element group" data-auto="flex-element-group"> <div id="daf81bcc" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1105358164"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton u_1105358164 button_2" file="false" href="tel:(860) 215-2254" data-element-type="dButtonLinkId" id="1105358164"> <span class="iconBg" aria-hidden="true" id="1528562180"> <span class="icon hasFontIcon icon-star" id="1809587092"></span> 2913</span> 2914 <span class="text" id="1035871641">(860) 215-2254</span> 2915</a> 2916</div> 2917</div> 2918 <div id="a052d7aa" class="flex-element group" data-auto="flex-element-group"> <div id="df845449" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1524906790"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton u_1524906790 button_2 align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton u_1524906790 button_2" file="false" href="/request-service-form" data-element-type="dButtonLinkId" id="1524906790" data-binding="W3siYmluZGluZ05hbWUiOiJidXR0b25fdGV4dCIsInZhbHVlIjoiU1RBVElDX1BBR0VfQ09MTEVDVElPTi57XCJjblwiOlwiYnV0dG9uc1wiLFwiZm5cIjpcIk1vYmlsZSBCdXR0b25cIn0ifSx7ImJpbmRpbmdOYW1lIjoibGluayIsInZhbHVlIjoiU1RBVElDX1BBR0VfQ09MTEVDVElPTi57XCJjblwiOlwiYnV0dG9uc1wiLFwiZm5cIjpcIlJlcXVlc3QgQ1RBIExpbmtcIn0ifV0=" data-diy-link=""> <span class="iconBg" aria-hidden="true" id="1847377451"> <span class="icon hasFontIcon icon-star" id="1804309602"></span> 2919</span> 2920 <span class="text" id="1149277214" data-diy-text="">Request Service</span> 2921</a> 2922</div> 2923</div> 2924</div> 2925</div> 2926</div> 2927</div> 2928</div> 2929</section> 2930</div> 2931 <div data-layout-section="" data-auto="flex-section" id="1368876750" data-flex-id="157b4fff" class="" data-hidden-on-desktop="true" data-hidden-on-mobile="true"> <div class="flex-widgets-container" id="1825413659"></div> 2932 <section id="726ad2b1" class="flex-element section" data-auto="flex-element-section"> <div id="30295aaf" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="32767c2f" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop="" data-hidden-on-mobile=""> <div id="2fd7b414" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop=""> <div id="widget_ie6" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-hidden-on-mobile="" data-external-id="1288312793"> <div class="widget-9015bc u_1288312793 dmCustomWidget" data-lazy-load="true" data-title="" id="1288312793" dmle_extension="custom_extension" data-element-type="custom_extension" data-binding="W3siYmluZGluZ05hbWUiOiJtZXJjaGFudElkIiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJSZXZpZXdzIFdpZGdldHNcIixcImZuXCI6XCJNZXJjaGFudCBJRFwifSJ9XQ==" icon="false" surround="false" data-widget-id="9015bcbf1ace437aa6f0e66d5bb2e7d3" data-widget-version="25" data-widget-config="eyJtZXJjaGFudElkIjoiODA3OTgiLCJtaW4tcmV2aWV3cyI6IjMiLCJtaW4tcmF0aW5nIjoiNCIsInNob3ctd3JpdGUiOnRydWUsImhpZGUtc3VtbWFyeSI6dHJ1ZSwidGhlbWUiOiJsaWdodCJ9"> <div class="headerWidget" data-merchant="80798" data-theme="light" data-break="" data-count-limit="3" data-rating-limit="4" data-show-write="true" data-hide-summary="true"></div> 2933</div> 2934</div> 2935</div> 2936 <div id="group_jjh" class="flex-element group" data-auto="flex-element-group"> <div id="widget_5h5" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="image" data-keep-proportion="true" data-external-id="1303640445"> <div class="imageWidget align-center u_1303640445" data-element-type="image" data-widget-type="image" id="1303640445" data-binding="W3siYmluZGluZ05hbWUiOiJpbWFnZSIsInZhbHVlIjoic2l0ZV9pbWFnZXMucGVyc2lzdGVkLmRhcmstbG9nbyJ9XQ=="> <a href="/" id="1035792501"><img src="https://le-cdn.hibuwebsites.com/f6cdfabba1c749a1a3d3b7a9f4c30c76/dms3rep/multi/opt/logo-jtn-electrical-1920w.jpg" alt="JTN Electrical Logo" id="1107341754" class="" width="529" height="170" data-dm-image-path="https://cdn.hibuwebsites.com/f6cdfabba1c749a1a3d3b7a9f4c30c76/dms3rep/multi/logo-jtn-electrical.jpg" data-diy-image="" onerror="handleImageLoadError(this)"/></a> 2937</div> 2938</div> 2939</div> 2940 <div id="group_7in" class="flex-element group" data-auto="flex-element-group"> <div id="widget_vej" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1997567173"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton u_1997567173 button_2 align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton u_1997567173 button_2" file="false" href="/contact" data-element-type="dButtonLinkId" id="1997567173" data-binding="W3siYmluZGluZ05hbWUiOiJsaW5rIiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJidXR0b25zXCIsXCJmblwiOlwiVGFiIC0gQ29udCAtIExpbmtcIn0ifSx7ImJpbmRpbmdOYW1lIjoiYnV0dG9uX3RleHQiLCJ2YWx1ZSI6IlNUQVRJQ19QQUdFX0NPTExFQ1RJT04ue1wiY25cIjpcImJ1dHRvbnNcIixcImZuXCI6XCJUYWIgLSBDb250IEJ1dHRvblwifSJ9XQ==" data-diy-link=""> <span class="iconBg"
2940aria-hidden="true" id="1066072120"> <span class="icon hasFontIcon icon-star" id="1327803529"></span> 2941</span> 2942 <span class="text" id="1641422126" data-diy-text="">Contact Us</span> 2943</a> 2944</div> 2945</div> 2946 <div id="group_23b" class="flex-element group" data-auto="flex-element-group"> <div id="4598469b" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="hamburgerButton" data-external-id="1761070024"> <button class="hamburgerButton" data-element-type="hamburgerButton" id="1761070024" aria-label="Open menu" aria-controls="hamburger-drawer" aria-expanded="false"> <svg width="100pt" height="100pt" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1639062494"> <path d="m3.6172 10.754h-0.050782c-1.9727 0-3.5703 1.5977-3.5703 3.5703 0 1.9727 1.5977 3.5703 3.5703 3.5703h0.054688 92.758 0.050782c1.9727 0 3.5703-1.5977 3.5703-3.5703 0-1.9727-1.5977-3.5703-3.5703-3.5703h-0.054688zm0 35.676h-0.050782c-1.9727 0-3.5703 1.5977-3.5703 3.5703s1.5977 3.5703 3.5703 3.5703h0.054688 92.758 0.050782c1.9727 0 3.5703-1.5977 3.5703-3.5703s-1.5977-3.5703-3.5703-3.5703h-0.054688zm0 35.68h-0.050782c-1.9727 0-3.5703 1.5977-3.5703 3.5703 0 1.9727 1.5977 3.5703 3.5703 3.5703h0.054688 92.758 0.050782c1.9727 0 3.5703-1.5977 3.5703-3.5703 0-1.9727-1.5977-3.5703-3.5703-3.5703h-0.054688z"></path> 2947</svg> 2948</button> 2949</div> 2950</div> 2951 <div id="group_tbn" class="flex-element group" data-auto="flex-element-group"> <div id="widget_qp9" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="spacer" data-external-id="1685550797"> <div data-element-type="spacer" class="dmSpacer" id="1685550797"></div> 2952</div> 2953</div> 2954</div> 2955</div> 2956</section> 2957</div> 2958 <div data-layout-section="" data-auto="flex-section" id="1058362846" data-flex-id="c0ae99ba" class="" data-hidden-on-mobile="true" data-hidden-on-tablet="true"> <div class="flex-widgets-container" id="1227336095"></div> 2959 <section id="ac810934" class="flex-element section" data-auto="flex-element-section"> <div id="b7dee1fb" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="9360b347" class="flex-element group" data-auto="flex-element-group" data-hidden-on-mobile="" data-hidden-on-tablet=""> <div id="3710bc01" class="flex-element group" data-auto="flex-element-group" data-hidden-on-tablet=""> <div id="3a2d0b90" class="flex-element group" data-auto="flex-element-group" data-hidden-on-mobile="" data-hidden-on-tablet=""> <div id="be1b9f91" class="flex-element group" data-auto="flex-element-group"> <div id="423fc4a2" class="flex-element group" data-auto="flex-element-group"> <div id="3b22d9b0" class="flex-element group" data-auto="flex-element-group"> <div id="519ddbdc" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1642529323"> <div class="widget-9015bc u_1642529323 dmCustomWidget" data-lazy-load="true" data-title="" id="1642529323" dmle_extension="custom_extension" data-element-type="custom_extension" data-binding="W3siYmluZGluZ05hbWUiOiJtZXJjaGFudElkIiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJSZXZpZXdzIFdpZGdldHNcIixcImZuXCI6XCJNZXJjaGFudCBJRFwifSJ9XQ==" icon="false" surround="false" data-widget-id="9015bcbf1ace437aa6f0e66d5bb2e7d3" data-widget-version="25" data-widget-config="eyJtZXJjaGFudElkIjoiODA3OTgiLCJtaW4tcmV2aWV3cyI6IjMiLCJtaW4tcmF0aW5nIjoiNCIsInNob3ctd3JpdGUiOnRydWUsImhpZGUtc3VtbWFyeSI6ZmFsc2UsInRoZW1lIjoibGlnaHQifQ=="> <div class="headerWidget" data-merchant="80798" data-theme="light" data-break="" data-count-limit="3" data-rating-limit="4" data-show-write="true" data-hide-summary="false"></div> 2960</div> 2961</div> 2962</div> 2963 <div id="8caf0a70" class="flex-element group" data-auto="flex-element-group"> <div id="a8c6072d" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1857856301"> <div class="dmNewParagraph u_1857856301" data-element-type="paragraph" data-version="5" id="1857856301" style=""><p class="m-size-13 t-size-13 text-align-left size-13"><span class="m-font-size-13 t-font-size-13 font-size-13" style="color: var(--color_7); font-style: normal; display: unset;">Serving</span></p></div> 2964</div> 2965 <div id="3ad8c19c" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1153192226"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1153192226" data-diy-text="" style=""><p class="text-align-left size-24 t-size-20 m-size-19" style="line-height: 1.1;"><strong class="font-size-24 t-font-size-20 m-font-size-19" style="display: initial; color: var(--color_7); font-weight: bold;">Hartford County and</strong></p><p class="text-align-left size-24 t-size-20 m-size-19" style="line-height: 1.1;"><strong class="font-size-24 t-font-size-20 m-font-size-19" style="display: initial; color: var(--color_7); font-weight: bold;">Western Massachusetts</strong></p></div> 2966</div> 2967</div> 2968</div> 2969 <div id="e3dfb55d" class="flex-element group" data-auto="flex-element-group"> <div id="6881434d" class="flex-element group" data-auto="flex-element-group"> <div id="5bfb3718" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="image" data-keep-proportion="true" data-external-id="1220116170"> <div class="imageWidget align-center" data-element-type="image" data-widget-type="image" id="1220116170" data-binding="W3siYmluZGluZ05hbWUiOiJpbWFnZSIsInZhbHVlIjoic2l0ZV9pbWFnZXMucGVyc2lzdGVkLmRhcmstbG9nbyJ9XQ=="> <a href="/" id="1909084297"><img src="https://le-cdn.hibuwebsites.com/f6cdfabba1c749a1a3d3b7a9f4c30c76/dms3rep/multi/opt/logo-jtn-electrical-1920w.jpg" alt="JTN Electrical Logo" id="1563401796" class="" width="529" height="170" data-dm-image-path="https://cdn.hibuwebsites.com/f6cdfabba1c749a1a3d3b7a9f4c30c76/dms3rep/multi/logo-jtn-electrical.jpg" data-diy-image="" onerror="handleImageLoadError(this)"/></a> 2970</div> 2971</div> 2972</div> 2973</div> 2974 <div id="5cbebde5" class="flex-element group" data-auto="flex-element-group"> <div id="d3a805c8" class="flex-element group" data-auto="flex-element-group"> <div id="e77f39e9" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1234177366"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton u_1234177366 button_2 align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton u_1234177366 button_2" file="false" href="/request-service-form" data-element-type="dButtonLinkId" id="1234177366" data-binding="W3siYmluZGluZ05hbWUiOiJidXR0b25fdGV4dCIsInZhbHVlIjoiU1RBVElDX1BBR0VfQ09MTEVDVElPTi57XCJjblwiOlwiYnV0dG9uc1wiLFwiZm5cIjpcIkRlc2sgLyBUYWIgQnV0dG9uXCJ9In0seyJiaW5kaW5nTmFtZSI6ImxpbmsiLCJ2YWx1ZSI6IlNUQVRJQ19QQUdFX0NPTExFQ1RJT04ue1wiY25cIjpcImJ1dHRvbnNcIixcImZuXCI6XCJSZXF1ZXN0IENUQSBMaW5rXCJ9In1d" data-diy-link=""> <span class="iconBg" aria-hidden="true" id="1281457075"> <span class="icon hasFontIcon icon-star" id="1446615084"></span> 2975</span> 2976 <span class="text" id="1422705019" data-diy-text="">Request Service</span> 2977</a> 2978</div> 2979</div> 2980 <div id="6a5bfb4c" class="flex-element group" data-auto="flex-element-group"> <div id="746860ee" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1791170611"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1791170611" style=""><p class="text-align-right m-size-13 t-size-13 size-13"><span class="m-font-size-13 t-font-size-13 font-size-13" style="color: var(--color_7); font-style: normal; display: unset;">Call Us Today</span></p></div> 2981</div> 2982 <div id="0ad35782" class="flex-element widget-wrapper YXTPH" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1127636214"> <div class="dmNewParagraph u_1127636214" data-element-type="paragraph" data-version="5" id="1127636214" data-diy-text="" style="" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJjb250ZW50X2xpYnJhcnkuZ2xvYmFsLnBob25lLmxvYy0wIC0gbWFpbiJ9XQ==">(860) 215-2254</div> 2983</div> 2984</div> 2985</div> 2986</div> 2987</div> 2988 <div id="bd946f29" class="flex-element group" data-auto="flex-element-group" data-hidden-on-mobile="" data-hidden-on-tablet=""> <div id="51d6dafc" class="flex-element widget-wrapper navmasterdefault" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-hidden-on-tablet="" data-hidden-on-mobile="" data-hidden-on-desktop="" data-external-id="1398948466"> <nav class="effect-bottom2 u_1398948466 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="horizontal_nav_layout_1" layout-sub="submenu_horizontal_1" data-show-vertical-sub-items="HOVER" id="1398948466" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="HORIZONTAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/" class="unifiednav__item unav-item unav-top-item dmUDNavigationItem_00 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text=" 2989 Home 2990 " data-auto="page-text-style">Home<span class="icon icon-angle-down"></span> 2991</span> 2992</a> 2993</li> 2994 <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/about" class="unifiednav__item unav-item unav-top-item dmUDNavigationItem_010101599305 unifiednav__item_has-sub-nav" target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="About" data-auto="page-text-style">About<span class="icon icon-angle-down"></span> 2995</span> 2996</a> 2997 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/faqs" class="unifiednav__item unav-item dmUDNavigationItem_010101347615 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="FAQs" data-auto="page-text-style">FAQs<span class="icon icon-angle-right"></span> 2998</span> 2999</a> 3000</li> 3001 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/reviews" class="unifiednav__item unav-item dmUDNavigationItem_010101840192 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Reviews" data-auto="page-text-style">Reviews<span class="icon icon-angle-right"></span> 3002</span> 3003</a> 3004</li> 3005</ul> 3006</li> 3007 <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/contact" class="unifiednav__item unav-item unav-top-item dmUDNavigationItem_010101152840 unifiednav__item_has-sub-nav" target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Contact" data-auto="page-text-style">Contact<span class="icon icon-angle-down"></span> 3008</span> 3009</a> 3010 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/request-service-form" class="unifiednav__item unav-item dmNavItemSelected dmUDNavigationItem_010101150108 " target="" data-target-page-alias="" aria-current="page" data-auto="selected-page"> <span class="nav-item-text " data-link-text="Request Service" data-auto="page-text-style">Request Service<span class="icon icon-angle-right"></span> 3011</span> 3012</a> 3013</li> 3014 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/contact#service-areas" class="unifiednav__item unav-item dmUDNavigationItem_010101813309 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Areas Served" data-auto="page-text-style">Areas Served<span class="icon icon-angle-right"></span> 3015</span> 3016</a> 3017</li> 3018</ul> 3019</li> 3020</ul> 3021</nav> 3022</div> 3023 <div id="widget_7d2" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1963014488"> <div class="widget-5b093d u_1963014488 dmCustomWidget" data-lazy-load="true" data-title="" id="1963014488" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="5b093d0fb9474c1dabfa7807be463b12" data-widget-version="52" data-widget-config="eyJjaGlsZFVybFByZWZpeCI6InNlcnZpY2VzIiwidXNlVW5pdmVyc2FsQ29sb3JzIjp0cnVlLCJsb2FkaW5nRGlzcGxheUdyb3VwIjpudWxsLCJ1c2VVbml2ZXJzYWxUeXBvZ3JhcGh5Ijp0cnVlLCJtZWdhQ2hpbGRGb250U2l6ZSI6IjE0IiwicmVzdG9yZUZ1bGxEZWZhdWx0cyI6ZmFsc2UsIm1lZ2EzTGFiZWwiOiJNZWdhIDMiLCJ0b3BMZXZlbEdhcCI6IjE1IiwibWVnYVRyaWdnZXJDaGV2cm9uR2FwIjoiMTIiLCJtZWdhMkFscGhhYmV0aXplIjpmYWxzZSwibWVnYTFHcm91cCI6bnVsbCwiYW5pbWF0ZVN1Yk5hdiI6ZmFsc2UsIm1lZ2EyTGFiZWwiOiJNZWdhIDIiLCJtZWdhNExhYmVsIjoiTWVnYSA0IiwibWVnYTJMYXlvdXQiOiJncmlkIiwibWVnYTFMYWJlbCI6IkVsZWN0cmljYWwgU2VydmljZXMiLCJhbmltYXRlSG92ZXJFZmZlY3RzIjpmYWxzZSwiY2FwaXRhbGl6ZU5hdiI6ZmFsc2UsInVuaXZlcnNhbEZvbnRXZWlnaHQiOiI1MDAiLCJtZWdhMk1heFdpZHRoIjoiMTYwMCIsInVuaXZlcnNhbEZvbnRTaXplIjoiMTYiLCJhbmltYXRlVG9wQ2hldnJvbnMiOmZhbHNlLCJsb2FkaW5nSXRlbUNvdW50IjoiNCIsImFuaW1hdGVNaXJyb3IiOmZhbHNlLCJ0b3BOYXZCYWNrZ3JvdW5k
3023Q29sb3IiOiIiLCJtYW51YWxOYXZpZ2F0aW9uSXRlbXMiOltdLCJtZWdhRmFtaWx5QmFja2dyb3VuZENvbG9yIjoidmFyKC0tY29sb3JfMTApIiwiYWN0aXZlVGV4dENvbG9yIjoidmFyKC0tY29sb3JfMTEpIiwidG9wQ2hldnJvblN0eWxlIjoiZmlsbGVkIiwiYW5pbWF0ZVRvcE5hdiI6ZmFsc2UsImFuaW1hdGVNZWdhRmFtaWxpZXMiOmZhbHNlLCJ0b3BDaGV2cm9uQ29sb3IiOiIjMjIyMjIyIiwibWVnYUNoZXZyb25Db2xvciI6IiMyMjIyMjIiLCJtZWdhQ2hpbGRGb250V2VpZ2h0IjoiNDAwIiwic3ViU3ViRm9udFdlaWdodCI6IjQwMCIsIm5hdGl2ZVBhcmVudHMiOlt7InNob3dQYXJlbnQiOnRydWUsInB1bGxDaGlsZHJlbiI6dHJ1ZSwibGFiZWwiOiIiLCJ1cmwiOnsidmFsdWUiOiJhYm91dCIsImxhYmVsIjoiIiwidHlwZSI6InBhZ2UiLCJocmVmIjoiL2Fib3V0IiwicmF3X3VybCI6Ii9zaXRlLzUyYWQxOTM1ZTQ1NjRiMjRhNzgyNDBiMzgyZjQxYTM5L2Fib3V0P25lZT10cnVlJmVkPXRydWUmc2hvd09yaWdpbmFsPXRydWUmcHJldmlldz10cnVlJmRtX3RyeV9tb2RlPXRydWUmZG1fY2hlY2tTeW5jPTEifX0seyJzaG93UGFyZW50Ijp0cnVlLCJwdWxsQ2hpbGRyZW4iOnRydWUsImxhYmVsIjoiIiwidXJsIjp7InZhbHVlIjoiY29udGFjdCIsImxhYmVsIjoiIiwidHlwZSI6InBhZ2UiLCJocmVmIjoiL2NvbnRhY3QiLCJyYXdfdXJsIjoiL3NpdGUvNTJhZDE5MzVlNDU2NGIyNGE3ODI0MGIzODJmNDFhMzkvY29udGFjdD9uZWU9dHJ1ZSZlZD10cnVlJnNob3dPcmlnaW5hbD10cnVlJnByZXZpZXc9dHJ1ZSZkbV90cnlfbW9kZT10cnVlJmRtX2NoZWNrU3luYz0xIn19XSwidXNlVW5pdmVyc2FsQmFja2dyb3VuZHMiOnRydWUsInVzZUN1c3RvbU5hdk9ubHkiOmZhbHNlLCJzdWJTdWJUZXh0Q29sb3IiOiIjMjIyMjIyIiwibWVnYTFNYXhXaWR0aCI6IjE2MDAiLCJtZWdhNEdyb3VwIjpudWxsLCJ0b3BGb250U2l6ZSI6IjE2IiwibWlycm9yQWxpZ25tZW50Ijoic3RhcnQiLCJtZWdhNEV4cGFuZEFsbCI6ZmFsc2UsInRleHRDb2xvcnNHcm91cCI6bnVsbCwic3ViRm9udFNpemUiOiIxNCIsInR5cG9ncmFwaHlHcm91cCI6bnVsbCwibWlycm9ySXRlbUdhcCI6IjAiLCJhbmltYXRlTWVnYU1lbnUiOmZhbHNlLCJtZWdhR3JhbmRjaGlsZFRleHRDb2xvciI6IiMyMjIyMjIiLCJtZWdhUGFyZW50VGV4dENvbG9yIjoiIzIyMjIyMiIsIm1pcnJvclN0eWxlcyI6bnVsbCwibWVnYTFMYXlvdXQiOiJtYXNvbnJ5IiwibWVnYUdyYW5kY2hpbGRGb250U2l6ZSI6IjE0IiwiaG92ZXJUZXh0Q29sb3IiOiJ2YXIoLS1jb2xvcl8xMSkiLCJtZWdhNENvbHVtbnMiOiIzIiwibWVnYTJFeHBhbmRBbGwiOmZhbHNlLCJtZWdhM0NvbHVtbnMiOiIzIiwiY2hldnJvbnNHcm91cCI6bnVsbCwibWVnYTFDb2x1bW5zIjoiMyIsIm1lZ2EyQ29sdW1ucyI6IjMiLCJsYXlvdXRQcmVzZXQiOiJjbGFzc2ljIiwic3ViU3ViTmF2QmFja2dyb3VuZENvbG9yIjoidmFyKC0tY29sb3JfMTApIiwicmVzdG9yZU1pcnJvckRlZmF1bHRzIjpmYWxzZSwic3ViRm9udFdlaWdodCI6IjQwMCIsInRvcEZvbnRXZWlnaHQiOiI1MDAiLCJtZWdhMUV4cGFuZEFsbCI6dHJ1ZSwibmF2QWxpZ25tZW50IjoiY2VudGVyIiwiYW5pbWF0ZU1lZ2FDaGV2cm9ucyI6ZmFsc2UsIndpZGdldE1vZGUiOiJmdWxsIiwic3ViVGV4dENvbG9yIjoiIzIyMjIyMiIsInN1Yk5hdkJhY2tncm91bmRDb2xvciI6InZhcigtLWNvbG9yXzEwKSIsIm1lZ2E0TWF4V2lkdGgiOiIxNjAwIiwiYmFja2dyb3VuZENvbG9yc0dyb3VwIjpudWxsLCJ0b3BUZXh0Q29sb3IiOiIjMjIyMjIyIiwibWlycm9yT3Blbk1lbnVzT25Ib3ZlciI6IiIsIm1lZ2FQYXJlbnRGb250U2l6ZSI6IjE2IiwibWVudUludGVyYWN0aW9uR3JvdXAiOm51bGwsIm1pcnJvclN0eWxpbmdHcm91cCI6bnVsbCwiZW5hYmxlQ3VzdG9tTmF2aWdhdGlvbkJ1aWxkZXIiOmZhbHNlLCJtZWdhMUFscGhhYmV0aXplIjpmYWxzZSwibWVnYTNHcm91cCI6bnVsbCwibWVnYTRBbHBoYWJldGl6ZSI6ZmFsc2UsImFuaW1hdGlvbnNFbmFibGVkIjpmYWxzZSwiY2F0ZWdvcnlVcmxQcmVmaXgiOiJ3aGF0LXdlLWRvIiwidW5pdmVyc2FsQ2hldnJvblN0eWxlIjoiZmlsbGVkIiwidW5pdmVyc2FsVGV4dENvbG9yIjoidmFyKC0tY29sb3JfMTEpIiwidXNlU2VwYXJhdGVNaXJyb3JTdHlsaW5nIjpmYWxzZSwibG9hZGluZ0FuaW1hdGlvblN0eWxlIjoic2hpbW1lciIsIm1lZ2EyR3JvdXAiOm51bGwsIm1lZ2FDaGV2cm9uU3R5bGUiOiJmaWxsZWQiLCJ1bml2ZXJzYWxDaGV2cm9uQ29sb3IiOiJ2YXIoLS1jb2xvcl8xMSkiLCJob3ZlckJhY2tncm91bmRDb2xvciI6InZhcigtLWNvbG9yXzEwKSIsIm1lZ2FNZW51QmFja2dyb3VuZENvbG9yIjoidmFyKC0tY29sb3JfMTApIiwidW5pdmVyc2FsQmFja2dyb3VuZENvbG9yIjoidmFyKC0tY29sb3JfMTApIiwibWVnYTNMYXlvdXQiOiJncmlkIiwibmF2aWdhdGlvblRvb2xzR3JvdXAiOm51bGwsIm1lZ2FQYXJlbnRGb250V2VpZ2h0IjoiNjAwIiwibWVnYUNoaWxkVGV4dENvbG9yIjoiIzIyMjIyMiIsInN1YlN1YkZvbnRTaXplIjoiMTQiLCJtZWdhM0FscGhhYmV0aXplIjpmYWxzZSwibGF5b3V0R3JvdXAiOm51bGwsInVuaXZlcnNhbFVuZGVybGluZSI6dHJ1ZSwiZW5hYmxlTmF2aWdhdGlvblRyZWVQcmV2aWV3IjpmYWxzZSwibWVnYUdyYW5kY2hpbGRGb250V2VpZ2h0IjoiNDAwIiwid2lkZ2V0TW9kZVVybHNHcm91cCI6bnVsbCwiYW5pbWF0aW9uc0dyb3VwIjpudWxsLCJvcGVuTWVudXNPbkhvdmVyIjp0cnVlLCJ1bml2ZXJzYWxDaGV2cm9uV2VpZ2h0IjoiMiIsIm1lZ2EzTWF4V2lkdGgiOiIxNjAwIiwibWVnYTRMYXlvdXQiOiJncmlkIiwiYW5pbWF0ZVN1YlN1Yk5hdiI6ZmFsc2UsIm1lZ2EzRXhwYW5kQWxsIjpmYWxzZSwidW5pdmVyc2FsQ2hldnJvblNpemUiOiIxMCIsInVzZVVuaXZlcnNhbENoZXZyb25TZXR0aW5ncyI6dHJ1ZX0="><!-- NMW Full Feature Stable: Manual Navigation Items live-update fix --><!-- 3024 Use this same HTML for both widget instances. 3025 3026 Content Panel widgetMode: 3027 full = loads Collection, Native, CUSTOMNAV, and Manual navigation data 3028 mirror = renders the Full widget's shared navigation vertically 3029 3030 The hidden manual source is required because Duda link inputs must be output 3031 through the custom_link helper. JavaScript reads the Duda-generated anchor 3032 attributes and moves them into the rendered navigation. 3033 3034 Manual Folder items do not require a link. An empty manual Folder is shown as 3035 a disabled preview inside the Duda editor, then omitted from preview/live pages 3036 until a valid child is assigned. 3037--> <div class="nmw-root" data-nmw-mode="full"> <a class="nmw-skip-link" href="#nmw-full-main-nav">Skip to navigation</a> 3038 <div class="nmw-loading" data-nmw-loading-view="" role="status" aria-l
3038ive="polite" aria-label="Loading navigation" hidden=""></div> 3039 <div class="nmw-mobile-nav-wrapper nmw-render" data-nmw-render=""></div> 3040 <div class="nmw-tools" data-nmw-tools="" aria-live="polite"></div> 3041 <div class="nmw-manual-items-source" data-nmw-manual-source="" aria-hidden="true" hidden=""></div> 3042</div> 3043</div> 3044</div> 3045</div> 3046 <div id="a8f32aec" class="flex-element group" data-auto="flex-element-group" data-hidden-on-mobile="" data-hidden-on-tablet="" data-hidden-on-desktop=""> <div id="392df140" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-hidden-on-tablet="" data-hidden-on-desktop="" data-external-id="1915042179"> <nav class="effect-bottom2 u_1915042179 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="horizontal_nav_layout_1" layout-sub="submenu_horizontal_1" data-show-vertical-sub-items="HOVER" id="1915042179" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="HORIZONTAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/" class="unifiednav__item unav-item unav-top-item " target="" data-target-page-alias="home"> <span class="nav-item-text " data-link-text=" 3047 Home 3048 " data-auto="page-text-style">Home<span class="icon icon-angle-down" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 3049</span> 3050</a> 3051</li> 3052 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <button type="button" class="unifiednav__item unav-item unav-top-item " data-target-page-alias="mega-menu-4dc8a903"> <span class="nav-item-text " data-link-text="Services" data-auto="page-text-style">Services<span class="icon icon-angle-down" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 3053</span> 3054</button> 3055</li> 3056 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/reviews" class="unifiednav__item unav-item unav-top-item " target="" data-target-page-alias="reviews"> <span class="nav-item-text " data-link-text="Reviews" data-auto="page-text-style">Reviews<span class="icon icon-angle-down" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 3057</span> 3058</a> 3059</li> 3060 <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/about" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="about"> <span class="nav-item-text " data-link-text="About" data-auto="page-text-style">About<span class="icon icon-angle-down"></span> 3061</span> 3062</a> 3063 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/faqs" class="unifiednav__item unav-item " target="" data-target-page-alias="faqs"> <span class="nav-item-text " data-link-text="FAQs" data-auto="page-text-style">FAQs<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 3064</span> 3065</a> 3066</li> 3067</ul> 3068</li> 3069 <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/contact" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="contact"> <span class="nav-item-text " data-link-text="Contact" data-auto="page-text-style">Contact<span class="icon icon-angle-down"></span> 3070</span> 3071</a> 3072 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/request-service-form" class="unifiednav__item unav-item dmNavItemSelected " target="" data-target-page-alias="request-form-page" aria-current="page" data-auto="selected-page"> <span class="nav-item-text " data-link-text="Request Service" data-auto="page-text-style">Request Service<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 3073</span> 3074</a> 3075</li> 3076 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/service-area" class="unifiednav__item unav-item " target="" data-target-page-alias="service-area"> <span class="nav-item-text " data-link-text="Serving Area" data-auto="page-text-style">Serving Area<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 3077</span> 3078</a> 3079</li> 3080</ul> 3081</li> 3082 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/locations" class="unifiednav__item unav-item unav-top-item " target="" data-target-page-alias="locations"> <span class="nav-item-text " data-link-text="Locations" data-auto="page-text-style">Locations<span class="icon icon-angle-down" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 3083</span> 3084</a> 3085</li> 3086</ul> 3087</nav> 3088</div> 3089</div> 3090</div> 3091</div> 3092</div> 3093</section> 3094</div> 3095</div> 3096 <div id="hamburger-drawer" class="hamburger-drawer layout-drawer" layout="5ff40d2f36ae4e5ea1bc96b2e2fcf67e===header" data-origin="side-reverse"> <div data-layout-section="" data-auto="flex-section" id="1825997552" data-flex-id="4a710abd"> <div class="flex-widgets-container" id="1217094164"></div>
3097 <section id="bb7ac507" class="flex-element section" data-auto="flex-element-section"> <div id="d3f5447c" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="78db3927" class="flex-element group" data-auto="flex-element-group"> <div id="a7708a16" class="flex-element group" data-auto="flex-element-group"> <div id="8e45e82b" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="hamburgerButton" data-external-id="1202219406"> <button class="hamburgerButton" data-element-type="hamburgerButton" id="1202219406" aria-label="Close menu" aria-controls="hamburger-drawer" aria-expanded="false"> <svg width="100pt" height="100pt" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1928507629"> <path d="m82 87.883c1.625 1.625 4.2656 1.625 5.8906 0 1.6289-1.6289 1.6289-4.2656 0-5.8945l-32.414-32.41 32.406-32.406c1.6289-1.625 1.6289-4.2656 3098 0-5.8906-1.6289-1.6289-4.2656-1.6289-5.8906 0l-32.406 32.402-32.406-32.406c-1.625-1.625-4.2656-1.625-5.8906 0-1.6289 1.6289-1.6289 4.2656 0 3099 5.8945l32.402 32.406-32.41 32.414c-1.6289 1.625-1.6289 4.2617 0 5.8906 1.625 1.6289 4.2656 1.6289 5.8906 0l32.414-32.414z" fill-rule="evenodd"></path> 3100</svg> 3101</button> 3102</div> 3103</div> 3104</div> 3105</div> 3106</section> 3107</div> 3108 <div data-auto="flex-section" id="1099442592" data-flex-id="63a049320d35b55b4ef2a372" data-layout-section=""> <div class="flex-widgets-container" id="1247649854"></div> 3109 <section id="63a049320d35b55b4ef2a373" class="flex-element section" data-auto="flex-element-section"> <div id="63a049320d35b55b4ef2a376" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="63a049320d35b55b4ef2a377" class="flex-element group" data-auto="flex-element-group"> <div id="63a049320d35b55b4ef2a374" class="flex-element group" data-auto="flex-element-group"> <div id="widget_la2" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="4003453145"> <div class="widget-5b093d dmCustomWidget" data-lazy-load="true" data-title="" id="4003453145" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="5b093d0fb9474c1dabfa7807be463b12" data-widget-version="52" data-widget-config="eyJjaGlsZFVybFByZWZpeCI6InNlcnZpY2VzIiwidXNlVW5pdmVyc2FsQ29sb3JzIjp0cnVlLCJsb2FkaW5nRGlzcGxheUdyb3VwIjpudWxsLCJ1c2VVbml2ZXJzYWxUeXBvZ3JhcGh5Ijp0cnVlLCJtZWdhQ2hpbGRGb250U2l6ZSI6IjE0IiwicmVzdG9yZUZ1bGxEZWZhdWx0cyI6ZmFsc2UsIm1lZ2EzTGFiZWwiOiJNZWdhIDMiLCJ0b3BMZXZlbEdhcCI6IjMyIiwibWVnYVRyaWdnZXJDaGV2cm9uR2FwIjoiMTIiLCJtZWdhMkFscGhhYmV0aXplIjpmYWxzZSwibWVnYTFHcm91cCI6bnVsbCwiYW5pbWF0ZVN1Yk5hdiI6dHJ1ZSwibWVnYTJMYWJlbCI6Ik1lZ2EgMiIsIm1lZ2E0TGFiZWwiOiJNZWdhIDQiLCJtZWdhMkxheW91dCI6ImdyaWQiLCJtZWdhMUxhYmVsIjoiTWVnYSAxIiwiYW5pbWF0ZUhvdmVyRWZmZWN0cyI6dHJ1ZSwiY2FwaXRhbGl6ZU5hdiI6ZmFsc2UsInVuaXZlcnNhbEZvbnRXZWlnaHQiOiI1MDAiLCJtZWdhMk1heFdpZHRoIjoiMTYwMCIsInVuaXZlcnNhbEZvbnRTaXplIjoiMTYiLCJhbmltYXRlVG9wQ2hldnJvbnMiOnRydWUsImxvYWRpbmdJdGVtQ291bnQiOiI0IiwiYW5pbWF0ZU1pcnJvciI6dHJ1ZSwidG9wTmF2QmFja2dyb3VuZENvbG9yIjoiIiwibWFudWFsTmF2aWdhdGlvbkl0ZW1zIjpbXSwibWVnYUZhbWlseUJhY2tncm91bmRDb2xvciI6IiIsImFjdGl2ZVRleHRDb2xvciI6ImN1cnJlbnRDb2xvciIsInRvcENoZXZyb25TdHlsZSI6ImZpbGxlZCIsImFuaW1hdGVUb3BOYXYiOnRydWUsImFuaW1hdGVNZWdhRmFtaWxpZXMiOnRydWUsInRvcENoZXZyb25Db2xvciI6IiMyMjIyMjIiLCJtZWdhQ2hldnJvbkNvbG9yIjoiIzIyMjIyMiIsIm1lZ2FDaGlsZEZvbnRXZWlnaHQiOiI0MDAiLCJzdWJTdWJGb250V2VpZ2h0IjoiNDAwIiwibmF0aXZlUGFyZW50cyI6W10sInVzZVVuaXZlcnNhbEJhY2tncm91bmRzIjp0cnVlLCJ1c2VDdXN0b21OYXZPbmx5IjpmYWxzZSwic3ViU3ViVGV4dENvbG9yIjoiIzIyMjIyMiIsIm1lZ2ExTWF4V2lkdGgiOiIxNjAwIiwibWVnYTRHcm91cCI6bnVsbCwidG9wRm9udFNpemUiOiIxNiIsIm1pcnJvckFsaWdubWVudCI6InN0YXJ0IiwibWVnYTRFeHBhbmRBbGwiOmZhbHNlLCJ0ZXh0Q29sb3JzR3JvdXAiOm51bGwsInN1YkZvbnRTaXplIjoiMTQiLCJ0eXBvZ3JhcGh5R3JvdXAiOm51bGwsIm1pcnJvckl0ZW1HYXAiOiIwIiwiYW5pbWF0ZU1lZ2FNZW51Ijp0cnVlLCJtZWdhR3JhbmRjaGlsZFRleHRDb2xvciI6IiMyMjIyMjIiLCJtZWdhUGFyZW50VGV4dENvbG9yIjoiIzIyMjIyMiIsIm1pcnJvclN0eWxlcyI6bnVsbCwibWVnYTFMYXlvdXQiOiJncmlkIiwibWVnYUdyYW5kY2hpbGRGb250U2l6ZSI6IjE0IiwiaG92ZXJUZXh0Q29sb3IiOiJjdXJyZW50Q29sb3IiLCJtZWdhNENvbHVtbnMiOiIzIiwibWVnYTJFeHBhbmRBbGwiOmZhbHNlLCJtZWdhM0NvbHVtbnMiOiIzIiwiY2hldnJvbnNHcm91cCI6bnVsbCwibWVnYTFDb2x1bW5zIjoiMyIsIm1lZ2EyQ29sdW1ucyI6IjMiLCJsYXlvdXRQcmVzZXQiOiJjbGFzc2ljIiwic3ViU3ViTmF2QmFja2dyb3VuZENvbG9yIjoiI2ZmZmZmZiIsInJlc3RvcmVNaXJyb3JEZWZhdWx0cyI6ZmFsc2UsInN1YkZvbnRXZWlnaHQiOiI0MDAiLCJ0b3BGb250V2VpZ2h0IjoiNTAwIiwibWVnYTFFeHBhbmRBbGwiOmZhbHNlLCJuYXZBbGlnbm1lbnQiOiJjZW50ZXIiLCJhbmltYXRlTWVnYUNoZXZyb25zIjp0cnVlLCJ3aWRnZXRNb2RlIjoibWlycm9yIiwic3ViVGV4dENvbG9yIjoiIzIyMjIyMiIsInN1Yk5hdkJhY2tncm91bmRDb2xvciI6IiNmZmZmZmYiLCJtZWdhNE1heFdpZHRoIjoiMTYwMCIsImJhY2tncm91bmRDb2xvcnNHcm91cCI6bnVsbCwidG9wVGV4dENvbG9yIjoiIzIyMjIyMiIsIm1pcnJvck9wZW5NZW51c09uSG92ZXIiOiIiLCJtZWdhUGFyZW50Rm9udFNpemUiOiIxNiIsIm1lbnVJbnRlcmFjdGlvbkdyb3VwIjpudWxsLCJtaXJyb3JTdHlsaW5nR3JvdXAiOm51bGwsImVuYWJsZUN1c3RvbU5hdmlnYXRpb25CdWlsZGVyIjpmYWxzZSwibWVnYTFBbHBoYWJldGl6ZSI6ZmFsc2UsIm1lZ2EzR3JvdXAiOm51bGwsIm1lZ2E0QWxwaGFiZXRpemUiOmZ
3109hbHNlLCJhbmltYXRpb25zRW5hYmxlZCI6dHJ1ZSwiY2F0ZWdvcnlVcmxQcmVmaXgiOiJ3aGF0LXdlLWRvIiwidW5pdmVyc2FsQ2hldnJvblN0eWxlIjoiZmlsbGVkIiwidW5pdmVyc2FsVGV4dENvbG9yIjoiIzExMTExMSIsInVzZVNlcGFyYXRlTWlycm9yU3R5bGluZyI6ZmFsc2UsImxvYWRpbmdBbmltYXRpb25TdHlsZSI6InNoaW1tZXIiLCJtZWdhMkdyb3VwIjpudWxsLCJtZWdhQ2hldnJvblN0eWxlIjoiZmlsbGVkIiwidW5pdmVyc2FsQ2hldnJvbkNvbG9yIjoiIzIyMjIyMiIsImhvdmVyQmFja2dyb3VuZENvbG9yIjoiIiwibWVnYU1lbnVCYWNrZ3JvdW5kQ29sb3IiOiIjZmZmZmZmIiwidW5pdmVyc2FsQmFja2dyb3VuZENvbG9yIjoiI2ZmZmZmZiIsIm1lZ2EzTGF5b3V0IjoiZ3JpZCIsIm5hdmlnYXRpb25Ub29sc0dyb3VwIjpudWxsLCJtZWdhUGFyZW50Rm9udFdlaWdodCI6IjYwMCIsIm1lZ2FDaGlsZFRleHRDb2xvciI6IiMyMjIyMjIiLCJzdWJTdWJGb250U2l6ZSI6IjE0IiwibWVnYTNBbHBoYWJldGl6ZSI6ZmFsc2UsImxheW91dEdyb3VwIjpudWxsLCJ1bml2ZXJzYWxVbmRlcmxpbmUiOmZhbHNlLCJlbmFibGVOYXZpZ2F0aW9uVHJlZVByZXZpZXciOmZhbHNlLCJtZWdhR3JhbmRjaGlsZEZvbnRXZWlnaHQiOiI0MDAiLCJ3aWRnZXRNb2RlVXJsc0dyb3VwIjpudWxsLCJhbmltYXRpb25zR3JvdXAiOm51bGwsIm9wZW5NZW51c09uSG92ZXIiOmZhbHNlLCJ1bml2ZXJzYWxDaGV2cm9uV2VpZ2h0IjoiMiIsIm1lZ2EzTWF4V2lkdGgiOiIxNjAwIiwibWVnYTRMYXlvdXQiOiJncmlkIiwiYW5pbWF0ZVN1YlN1Yk5hdiI6dHJ1ZSwibWVnYTNFeHBhbmRBbGwiOmZhbHNlLCJ1bml2ZXJzYWxDaGV2cm9uU2l6ZSI6IjE0IiwidXNlVW5pdmVyc2FsQ2hldnJvblNldHRpbmdzIjp0cnVlfQ=="><!-- NMW Full Feature Stable: Manual Navigation Items live-update fix --><!-- 3110 Use this same HTML for both widget instances. 3111 3112 Content Panel widgetMode: 3113 full = loads Collection, Native, CUSTOMNAV, and Manual navigation data 3114 mirror = renders the Full widget's shared navigation vertically 3115 3116 The hidden manual source is required because Duda link inputs must be output 3117 through the custom_link helper. JavaScript reads the Duda-generated anchor 3118 attributes and moves them into the rendered navigation. 3119 3120 Manual Folder items do not require a link. An empty manual Folder is shown as 3121 a disabled preview inside the Duda editor, then omitted from preview/live pages 3122 until a valid child is assigned. 3123--> <div class="nmw-root" data-nmw-mode="full"> <a class="nmw-skip-link" href="#nmw-full-main-nav">Skip to navigation</a> 3124 <div class="nmw-loading" data-nmw-loading-view="" role="status" aria-live="polite" aria-label="Loading navigation" hidden=""></div> 3125 <div class="nmw-mobile-nav-wrapper nmw-render" data-nmw-render=""></div> 3126 <div class="nmw-tools" data-nmw-tools="" aria-live="polite"></div> 3127 <div class="nmw-manual-items-source" data-nmw-manual-source="" aria-hidden="true" hidden=""></div> 3128</div> 3129</div> 3130</div> 3131</div> 3132</div> 3133</div> 3134</section> 3135</div> 3136 <div data-auto="flex-section" id="1029527021" data-flex-id="6406026c90e1521f6ecb0eca" data-layout-section=""> <div class="flex-widgets-container" id="1659590659"></div> 3137 <section id="6406026c90e1521f6ecb0ec9" class="flex-element section" data-auto="flex-element-section"> <div id="6406026c90e1521f6ecb0ecb" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="6406026c90e1521f6ecb0ecc" class="flex-element group" data-auto="flex-element-group"> <div id="6406026c90e1521f6ecb0ecd" class="flex-element group" data-auto="flex-element-group"></div> 3138</div> 3139</div> 3140</section> 3141</div> 3142</div> 3143 <div class="layout-drawer-overlay" id="layout-drawer-overlay"></div> 3144</div> 3145 <div dmwrapped="true" id="dmFirstContainer" class="dmBody u_dmStyle_template_request-service-form dm-home-page" themewaschanged="true"> <div id="allWrapper" class="allWrapper" data-auto="all-wrapper"><!-- navigation placeholders --> <div id="dm_content" class="dmContent" role="main"> <div dm:templateorder="170" class="dmHomeRespTmpl mainBorder dmRespRowsWrapper dmFullRowRespTmpl" id="1716942098"> <div data-auto="flex-section" id="1899014018" data-flex-id="24b6b13b" data-layout-section="" class=""> <div class="flex-widgets-container" id="1800163092"></div> 3146 <section id="3c671c77" class="flex-element section" data-auto="flex-element-section"> <div id="530811d6" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="34c91571" class="flex-element group" data-auto="flex-element-group"> <div id="d4329249" class="flex-element group" data-auto="flex-element-group" data-anim-extended="eyJkZXNrdG9wIjp7InRyaWdnZXIiOiJlbnRyYW5jZSIsImFuaW1hdGlvbiI6ImZhZGVJbkNvbWJvIiwiZHVyYXRpb24iOjAuNSwiZGVsYXkiOjAsImludGVuc2l0eSI6MSwiZGlyIjoiaW4tcGxhY2UifX0="> <div id="b3d39244" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1243815943"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1243815943" style=""> <h1><span style="text-shadow: none; display: initial;">Request Service</span></h1> 3147</div> 3148</div> 3149 <div id="13869723" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1359807854"> <div class="dmNewParagraph u_1359807854" data-element-type="paragraph" data-version="5" id="1359807854" data-diy-text="" style="" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJjb250ZW50X2xpYnJhcnkuZ2xvYmFsLmNvbXBhbnlfbmFtZSJ9XQ==">JTN Electrical</div> 3150</div> 3151</div> 3152</div> 3153</div> 3154</section> 3155</div> 3156 <div data-layout-section="" data-auto="flex-section" id="1479948
3156179" data-flex-id="7ef32d84" class="" data-hidden-on-mobile="true"> <div class="flex-widgets-container" id="1830000386"></div> 3157 <section id="12933559" class="flex-element section" data-auto="flex-element-section"> <div id="d80bb809" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="6a108e83" class="flex-element group" data-auto="flex-element-group" data-hidden-on-mobile=""> <div id="8c30d83a" class="flex-element group" data-auto="flex-element-group"> <div id="79342a6b" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ssrbreadcrumbs" data-external-id="1795077117"> <div id="1795077117" dmle_extension="ssrbreadcrumbs" data-element-type="ssrbreadcrumbs" class=""><span id="ssrWrap-1795077117" ><style data-styled="true" data-styled-version="5.3.11">#dm .euwdXq.euwdXq{color:var(--color_1);}/*!sc*/ 3158data-styled.g17[id="sc-cPiKLX"]{content:"euwdXq,"}/*!sc*/ 3159#dm .faqkoQ.faqkoQ{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:10px;}/*!sc*/ 3160@media (min-width:768px){#dm .faqkoQ.faqkoQ{font-size:11px;}}/*!sc*/ 3161@media (max-width:767px){#dm .faqkoQ.faqkoQ{font-size:12px;}}/*!sc*/ 3162data-styled.g19[id="sc-eDPEul"]{content:"faqkoQ,"}/*!sc*/ 3163#dm .evEVyk.evEVyk{-webkit-transform:rotate(0);-ms-transform:rotate(0);transform:rotate(0);display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;width:16px;}/*!sc*/ 3164#dm .evEVyk.evEVyk svg{width:100%;height:100%;fill:currentColor;}/*!sc*/ 3165data-styled.g20[id="sc-eldPxv"]{content:"evEVyk,"}/*!sc*/ 3166</style><div data-auto="runtime-breadcrumbs-widget">
3166<script data-auto="schema" type="application/ld+json">{"@context":"http://schema.org/","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"name":"Home","id":"/"}},{"@type":"ListItem","position":2,"item":{"name":"Contact","id":"/contact"}},{"@type":"ListItem","position":3,"item":{"name":"Request Service"}}]}</script>
3166<nav class="sc-eDPEul faqkoQ bc-root dmBlockElement" aria-label="Breadcrumbs"><a href="/" class="sc-cPiKLX euwdXq bc-item"><div class="sc-dLMFU">Home</div></a><span direction="ltr" class="sc-eldPxv evEVyk bc-separator" aria-hidden="true"><svg data-auto="arrow" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.46967 3.46967C5.76256 3.17678 6.23744 3.17678 6.53033 3.46967L10.5303 7.46967C10.8232 7.76256 10.8232 8.23744 10.5303 8.53033L6.53033 12.5303C6.23744 12.8232 5.76256 12.8232 5.46967 12.5303C5.17678 12.2374 5.17678 11.7626 5.46967 11.4697L8.93934 8L5.46967 4.53033C5.17678 4.23744 5.17678 3.76256 5.46967 3.46967Z" fill="currentColor"></path></svg></span><a href="/contact" class="sc-cPiKLX euwdXq bc-item"><div class="sc-dLMFU">Contact</div></a><span direction="ltr" class="sc-eldPxv evEVyk bc-separator" aria-hidden="true"><svg data-auto="arrow" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.46967 3.46967C5.76256 3.17678 6.23744 3.17678 6.53033 3.46967L10.5303 7.46967C10.8232 7.76256 10.8232 8.23744 10.5303 8.53033L6.53033 12.5303C6.23744 12.8232 5.76256 12.8232 5.46967 12.5303C5.17678 12.2374 5.17678 11.7626 5.46967 11.4697L8.93934 8L5.46967 4.53033C5.17678 4.23744 5.17678 3.76256 5.46967 3.46967Z" fill="currentColor"></path></svg></span><div class="sc-dLMFU bc-current-item" data-auto="bc-current-item">Request Service</div></nav></div></span></div> 3167</div> 3168</div> 3169</div> 3170</div> 3171</section> 3172</div> 3173 <div data-layout-section="" data-auto="flex-section" id="1391831113" data-flex-id="5c4456af" data-page-element-type="section" data-page-element-id="1273255118"> <div class="flex-widgets-container" id="1237728867"></div> 3174 <section id="f0d2e0ac" class="flex-element section" data-auto="flex-element-section"> <div id="8ac9db8b" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="520e9609" class="flex-element group" data-auto="flex-element-group"> <div id="68fb7ec9" class="flex-element group" data-auto="flex-element-group"> <div id="b6321765" class="flex-element group" data-auto="flex-element-group"> <div id="ffec1cdf" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1177470371"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1177470371" style="" new-inline-bind-applied="true" data-diy-text=""><p class="text-align-left m-text-align-left"><span style="display: initial;">Fill out this short form and a <span class="inline-data-binding" data-encoded-value="SlROIEVsZWN0cmljYWw=" data-inline-binding="" data-inline-binding-encoded="Y29udGVudF9saWJyYXJ5Lmdsb2JhbC5jb21wYW55X25hbWU=">JTN Electrical</span> 3175representative will contact you within 24 hours to discuss your service request.</span></p></div> 3176</div> 3177 <div id="98f8bad2" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="form" data-external-id="1638807636"> <div class="dmform default native-inputs u_1638807636" data-element-type="dContactUsRespId" captcha="true" data-require-captcha="true" data-captcha-position="bottomleft" id="1638807636" data-captcha-message="PHAgY2xhc3M9InJ0ZUJsb2NrIj5UaGlzIHNpdGUgaXMgcHJvdGVjdGVkIGJ5IHJlQ0FQVENIQS48L3A+" data-layout="layout-1" data-binding="W3siYmluZGluZ05hbWUiOiJlbWFpbCIsInZhbHVlIjoiY29udGVudF9saWJyYXJ5Lmdsb2JhbC5lbWFpbC5lbWFpbCAtIGxvYyAxIn1d"> <h3 class="dmform-title dmwidget-title" id="1960386174" hide="true">Request Service Form</h3> 3178 <div class="dmform-wrapper" id="1074145252" captcha-lang="en"> <form method="post" class="dmRespDesignRow" locale="ENGLISH" id="1775640877"> <div class="spacing-container" id="1844431240"> <div class="dmforminput required small-12 dmRespDesignCol medium-6 large-6" id="1037458903"> <label for="1619570817" id="1960303221" data-dm-for="dmform-0">Full Name*</label> 3179 <input type="text" class="" name="dmform-0" id="1619570817"/> <input type="hidden" name="label-dmform-0" value="Full Name*" id="1423678312" class=""/> </div>
3180 <div class="dmforminput required small-12 dmRespDesignCol medium-6 large-6" id="1228931911"> <label for="1954896164" id="1383818621" data-dm-for="dmform-2">Phone*</label> 3181 <input type="tel" class="" name="dmform-2" id="1954896164"/> <input type="hidden" name="label-dmform-2" value="Phone*" id="1309416493" class=""/> </div> 3182 <div class="dmforminput required small-12 medium-4 large-4 dmRespDesignCol newLine" id="1015672356"> <label for="1584259392" id="1629508607" data-dm-for="dmform-1">Email*</label> 3183 <input type="email" class="" name="dmform-1" id="1584259392"/> <input type="hidden" name="label-dmform-1" value="Email*" id="1009507548" class=""/> </div> 3184 <div class="dmforminput small-12 medium-4 large-4 dmRespDesignCol" id="1426506771"> <label for="1365400678" id="1091567039" data-dm-for="dmform-5">Contact Method*</label> 3185 <select name="dmform-5" id="1365400678"> <option selected="" disabled="" hidden="" value="" id="1490390531"></option> 3186 <option id="1031555580">Email</option> 3187 <option id="1938379120">Phone</option> 3188</select> 3189 <input type="hidden" name="label-dmform-5" value="Contact Method*" id="1372293369" class=""/> </div> 3190 <div class="dmforminput small-12 dmRespDesignCol required medium-4 large-4" id="1116687880"> <label for="1110605039" id="1304437688" data-dm-for="dmform-7">Services*</label> 3191<input type="text" name="dmform-7" placeholder="" id="1110605039"/><input type="hidden" name="label-dmform-7" value="Services*" id="1889619553" class=""/></div> 3192 <div class="dmforminput large-12 medium-12 dmRespDesignCol" id="1469959121"> <label for="1896362933" id="1048395248" data-dm-for="dmform-3">Message</label> 3193 <textarea name="dmform-3" id="1896362933" class=""></textarea> 3194 <input type="hidden" name="label-dmform-3" value="Message" id="1880951334" class=""/> </div> 3195</div> 3196 <div class="dmformsubmit dmWidget R" id="1363170918"> <input class="" name="submit" type="submit" value="Request Service" id="1487091419"/> </div> 3197 <input name="dmformsendto" type="hidden" id="1572965529" class="" value="O0wrseIfFLsS3rYZJm4ScOK8+BPBIktPNlH8uTOA3M/nZvgTXmfYrnLGKsXSZRIU73kO4KQzX2o=" data-dec="true"/> <input class="dmActionInput" type="hidden" name="action" value="/_dm/s/rt/widgets/dmform.submit.jsp" id="1278708902"/> <input name="dmformsubject" type="hidden" id="1144743687" class=""/> <input name="dmformfrom" type="hidden" value="" id="1512941668"/> <input name="dmformautoreplyenabled" type="hidden" value="false" id="1885237052" class=""/> <input name="dmformautoreplyfrom" type="hidden" value="NOB" id="1345104531" class=""/> <input name="dmformautoreplysubject" type="hidden" value="Client Provided Email Subject Line" id="1783791813" class=""/> <input name="dmformautoreplymsg" type="hidden" value="PHAgY2xhc3M9InJ0ZUJsb2NrIj5DbGllbnQgcHJvdmlkZWQgYXV0byByZXNwb25zZSBlbWFpbCBjb3B5LjwvcD4=" id="1146584700" class=""/> <input name="dmformautoreplyincludeformcopy" type="hidden" value="true" id="1080179296"/> <input name="dmformsubmitparams" type="hidden" value="8mpKnCSiNQXK/d9M7IDrSxJJnhBlHGKyYHAeNdkm917QQ7YczOvxrHCQLkvDvi7nrz90/AiWb36ZTEM0BsBl+e6biYRh3kpZrhpU2pYWFBcJH4lY+hgQtxSdqk06xMzLJhcKxjf2PMUVXvyhc20mVbEmuqt0CxC3/1EoHDWgQof9twwj16TbiEhPywzY052PYnu3UgKLBgKi4RXqyPl9M2Z0Jbmysd3+A5TJwXpMwAZ5jklP6MeAydrFvUDkU47Ac4Kmk/RE7Icy3HcTf+BbPVv9MEhalgfNkEikJfArWQlx4B37c0n9+ioZuhlP53Xspir+wysry/TbjPHhOWYL54WuFW2vJDsb6A8nZWZbrQSxcfPK5kPa8m58uVwq0EGRbxMDCzFZxTwRwmsS/768kzh5pml5l1Tz5Ner1oefi3TFbjcPvOFGPWKK4L9QEx8Qe9XyfMTi1LToO03FpoTIig==" data-dec="true"/><input type="hidden" name="page_uuid" value="8d67b06083f34b4a8a328c95f017fec2"/></form> 3198</div> 3199 <div class="dmform-success" style="display:none" id="1772601894" data-success-page="thank-you"><br/><p class="rteBlock"><strong>We appreciate you contacting us.</strong> One of our colleagues will get back in touch with you soon! </p><br/><p class="rteBlock">Have a great day!</p></div> 3200 <div class="dmform-error" style="display:none" id="1134856273"><p class="rteBlock">Oops, there was an error sending your message. </p><br/><p class="rteBlock">Please try again later.</p></div> 3201</div> 3202</div> 3203</div> 3204</div> 3205</div> 3206</div> 3207</section> 3208</div> 3209</div> 3210</div> 3211</div> 3212</div> 3213 <div class="dmFooterContainer flex_hfcontainer"> <div id="flex-footer" role="contentinfo"> <div data-layout-section="" data-auto="flex-section" id="1959439239" data-flex-id="2c8e291c" data-page-element-type="section" data-page-element-id="1601394852" class="" data-hidden-on-mobile="true"> <div class="flex-widgets-container" id="1357377095"></div> 3214 <section id="21379086" class="flex-element section" data-auto="flex-element-section"> <div id="b5a3be9f" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="b7a700bf" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop="" data-hidden-on-tablet="" data-hidden-on-mobile=""> <div id="6405d24c" class="flex-element group" data-auto="flex-element-group"> <div id="f9c6e236" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="photoGallery" data-external-id="1289582650"> <div class="dmPhotoGallery newPhotoGallery dmPhotoGalleryResp u_1289582650 photo-gallery-done text-layout-over captionAlignment-center_center photoGallery" galleryoptionsparams="{thumbnailsPerRow: 3, rowsToShow: 3, imageScaleMethod: true}" data-desktop-layout="square" data-desktop-columns="4" data-element-type="dPhotoGalleryId" data-desktop-text-layout="over" id="1289582650" data-auto-adjust-columns="true" data-image-hover-effect="zoomout" data-binding="W3siYmluZGluZ05hbWUiOiJwaG90b19nYWxsZXJ5IiwidmFsdWUiOiJzaXRlX2NvbGxlY3Rpb24uQnJhbmQgSWNvbnMifV0=" data-binding-metadata="eyJsaW5rIjoiIiwiaW1hZ2UiOiJpbWFnZSIsImRlc2NyaXB0aW9uIjoiIiwidGl0bGUiOiIiLCJhbHRfdGV4dCI6IiIsImxpbmtfdGV4dCI6IiJ9" data-paginate-total-elements="4"> <div class="dmPhotoGalleryHolder clearfix gallery shadowEffectToChildren gallery4inArow" id="1362986591"></div> 3215 <div class="layout-container square"> <div class="photogallery-row " data-index=""> <div class="photogallery-column column-4" data-index="0"> <div index="0" class="photoGalleryThumbs animated " data-index="0"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" data-dm-force-device="mobile" class="u_" data-image-url="https://cdn.hibuwebsites.com/33453d75968e405abe6f06a3faaa502b/dms3rep/multi/brand-logo-02.png" style="background-image: url('https://le-cdn.hibuwebsites.com/33453d75968e405abe6f06a3faaa502b/dms3rep/multi/opt/brand-logo-02-640w.png');"><img id="" data-src="https://le-cdn.hibuwebsites.com/33453d75968e405abe6f06a3faaa502b/dms3rep/multi/opt/brand-logo-02-1920w.png" alt="" onerror="handleImageLoadError(this)"/></a> 3216</div> 3217</div> 3218</div> 3219</div> 3220 <div class="photogallery-column column-4" data-index="1"> <div index="1" class="photoGalleryThumbs animated " data-index="1"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" data-dm-force-device="mobile" class="u_" data-image-url="https://cdn.hibuwebsites.com/33453d75968e405abe6f06a3faaa502b/dms3rep/multi/brand-logo-01.png" style="background-image: url('https://le-cdn.hibuwebsites.com/33453d75968e405abe6f06a3faaa502b/dms3rep/multi/opt/brand-logo-01-640w.png');"><img id="" data-src="https://le-cdn.hibuwebsites.com/33453d75968e405abe6f06a3faaa502b/dms3rep/multi/opt/brand-logo-01-1920w.png" alt="" onerror="handleImageLoadError(this)"/></a> 3221</div> 3222</div> 3223</div> 3224</div> 3225 <div class="photogallery-column column-4" data-index="2"> <div index="2" class="photoGalleryThumbs animated " data-index="2"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" data-dm-force-device="mobile" class="u_" data-image-url="https://cdn.hibuwebsites.com/33453d75968e405abe6f06a3faaa502b/dms3rep/multi/brand-logo-02.png" style="background-image: url('https://le-cdn.hibuwebsites.com/33453d75968e405abe6f06a3faaa502b/dms3rep/multi/opt/brand-logo-02-640w.png');"><img id="" data-src="https://le-cdn.hibuwebsites.com/33453d75968e405abe6f06a3faaa502b/dms3rep/multi/opt/brand-logo-02-1920w.png" alt="" onerror="handleImageLoadError(this)"/></a> 3226</div> 3227</div> 3228</div> 3229</div> 3230 <div class="photogallery-column column-4" data-index="3"> <div index="3" class="photoGalleryThumbs animated " data-index="3"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" data-dm-force-device="mobile" class="u_" data-image-url="https://cdn.hibuwebsites.com/33453d75968e405abe6f06a3faaa502b/dms3rep/multi/brand-logo-01.png" style="background-image: url('https://le-cdn.hibuwebsites.com/33453d75968e405abe6f06a3faaa502b/dms3rep/multi/opt/brand-logo-01-640w.png');"><img id="" data-src="https://le-cdn.hibuwebsites.com/33453d75968e405abe6f06a3faaa502b/dms3rep/multi/opt/brand-logo-01-1920w.png" alt="" onerror="handleImageLoadError(this)"/></a> 3231</div> 3232</div> 3233</div> 3234</div> 3235</div> 3236</div> 3237</div> 3238</div> 3239</div> 3240</div> 3241</div> 3242</section> 3243</div> 3244 <div data-auto="flex-section" id="1518098037" data-flex-id="7b00a5de" data-layout-section="" data-page-element-type="section" data-page-element-id="1761552858"> <div class="flex-widgets-container" id="1613662762"></div> 3245 <section id="9328cbef" class="flex-element section" data-auto="flex-element-section"> <div id="1ebb7f19" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="6e06962d" class="flex-element group" data-auto="flex-element-group"> <div id="a019b36e" class="flex-element group" data-auto="flex-element-group"> <div id="934bad03" class="flex-element widget-wrapper YXTBUSNAME" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1506966255"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1506966255" new-inline-bind-applied="true" data-diy-text="" style=""> <h3 class="m-size-16 t-size-16 size-18"><span class="m-font-size-16 t-font-size-16 font-size-18" style="display: unset; color: var(--color_2);">Learn more about</span></h3> 3246 <h3><span style="color: var(--color_2); display: initial;"><span class="inline-data-binding" data-encoded-value="SGlidSBQbHVtYmVycw==" data-inline-binding="content_library.global.company_name" data-inline-binding-encoded="Y29udGVudF9saWJyYXJ5Lmdsb2JhbC5jb21wYW5
32465X25hbWU=">JTN Electrical</span></span></h3> 3247</div> 3248</div> 3249 <div id="6f8e19aa" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1661506068"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1661506068" new-inline-bind-applied="true" data-diy-text="" style=""><p class="m-text-align-left text-align-left"><span style="color: var(--color_2); display: initial;"><span class="inline-data-binding" data-encoded-value="VGhpcyBpcyB0aGUgVmlkZW8gVHJhbnNjcmlwdC4gRWRpdCBpbiB0aGUgQ29udGVudCBMaWJyYXJ5Lg==" data-inline-binding="site_text.video transcript" data-inline-binding-encoded="c2l0ZV90ZXh0LnZpZGVvIHRyYW5zY3JpcHQ=">Serving the Windsor Locks, CT area, JTN Electrical specializes in residential and commercial electrical services. Emergency electrical services. Quick response times. Free, in-person estimates available. Call today.</span></span></p></div> 3250</div> 3251 <div id="bd7b2692" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1035845042"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton u_1035845042 button_2" file="false" href="/hibu-video-splash" data-element-type="dButtonLinkId" id="1035845042" link_type="popup" popup_target="hibu-video-splash"> <span class="iconBg" aria-hidden="true" id="1299251327"> <span class="icon hasFontIcon icon-star" id="1881817341"></span> 3252</span> 3253 <span class="text" id="1018125776">Watch Video</span> 3254</a> 3255</div> 3256</div> 3257 <div id="05e0ee19" class="flex-element group" data-auto="flex-element-group" data-binding="eyJiaW5kaW5nTmFtZSI6ImJhY2tncm91bmQiLCJ2YWx1ZSI6InNpdGVfaW1hZ2VzLnBlcnNpc3RlZC5WaWRlbyBTcGxhc2ggSW1hZ2UifQ==" style="background-image: url('https://le-cdn.hibuwebsites.com/f6cdfabba1c749a1a3d3b7a9f4c30c76/dms3rep/multi/opt/video-jtn-electrical-1920w.jpg') !important;"> <div id="a1b6269f" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1518332866"> <div class="widget-ca4188 u_1518332866 dmCustomWidget" data-lazy-load="" data-title="" id="1518332866" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="ca4188fe92024514a23a858f63ab6658" data-widget-version="18" data-widget-config="eyJ2aWRUaXRsZTEiOiIiLCJ2aWRUaXRsZTIiOiIiLCJwbGF5SWNvbiI6IjxzdmcgeG1sbnM9XCJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Z1wiIHdpZHRoPVwiMTAwXCIgaGVpZ2h0PVwiMTAwXCIgdmlld0JveD1cIjAgMCAxMDAgMTAwXCI+XG4gIDxwYXRoIGlkPVwiSWNvbl9mYS1yZWd1bGFyLWNpcmNsZS1wbGF5XCIgZGF0YS1uYW1lPVwiSWNvbiBmYS1yZWd1bGFyLWNpcmNsZS1wbGF5XCIgZD1cIk05MC42MjUsNTBBNDAuNjI1LDQwLjYyNSwwLDEsMCw1MCw5MC42MjUsNDAuNjI1LDQwLjYyNSwwLDAsMCw5MC42MjUsNTBaTTAsNTBhNTAsNTAsMCwxLDEsNTAsNTBBNTAsNTAsMCwwLDEsMCw1MFpNMzYuNzc3LDI4LjczYTQuNjkzLDQuNjkzLDAsMCwxLDQuNzQ2LjFMNjkuNjQ4LDQ2LjAxNmE0LjY5Miw0LjY5MiwwLDAsMSwwLDguMDA4TDQxLjUyMyw3MS4yMTFhNC43MzgsNC43MzgsMCwwLDEtNC43NDYuMSw0LjY1OCw0LjY1OCwwLDAsMS0yLjQtNC4wODJWMzIuODEzQTQuNjU4LDQuNjU4LDAsMCwxLDM2Ljc3NywyOC43M1pcIi8+XG48L3N2Zz5cbiIsInBsYXlMaW5rIjp7InZhbHVlIjoiaGlidS12aWRlby1zcGxhc2giLCJ0eXBlIjoicG9wdXAiLCJocmVmIjoiL2hpYnUtdmlkZW8tc3BsYXNoIiwicmF3X3VybCI6Ii9zaXRlL2Y2Y2RmYWJiYTFjNzQ5YTFhM2QzYjdhOWY0YzMwYzc2L2hpYnUtdmlkZW8tc3BsYXNoP25lZT10cnVlJmVkPXRydWUmc2hvd09yaWdpbmFsPXRydWUmcHJldmlldz10cnVlJmRtX3RyeV9tb2RlPXRydWUmZG1fY2hlY2tTeW5jPTEiLCJwb3B1cF90YXJnZXQiOiJoaWJ1LXZpZGVvLXNwbGFzaCIsImxpbmtfdHlwZSI6InBvcHVwIn0sInRyYW5zY3JpcHRpb24iOiJTZXJ2aW5nIHRoZSBXaW5kc29yIExvY2tzLCBDVCBhcmVhLCBKVE4gRWxlY3RyaWNhbCBzcGVjaWFsaXplcyBpbiByZXNpZGVudGlhbCBhbmQgY29tbWVyY2lhbCBlbGVjdHJpY2FsIHNlcnZpY2VzLiBFbWVyZ2VuY3kgZWxlY3RyaWNhbCBzZXJ2aWNlcy4gUXVpY2sgcmVzcG9uc2UgdGltZXMuIEZyZWUsIGluLXBlcnNvbiBlc3RpbWF0ZXMgYXZhaWxhYmxlLiBDYWxsIHRvZGF5LiIsIm5hbWUiOiJKVE4gRWxlY3RyaWNhbCIsImRlc2NyaXB0aW9uIjoiV2Ugb2ZmZXIgZWxlY3RyaWNhbCBpbnN0YWxsYXRpb25zLCBlbGVjdHJpY2FsIGV2YWx1YXRpb25zLCAmIGVsZWN0cmljYWwgcmVwYWlycy4gRW1lcmdlbmN5IGVsZWN0cmljYWwgc2VydmljZXMuIFF1aWNrIHJlc3BvbnNlIHRpbWVzLiBGcmVlIGVzdGltYXRlcyBhdmFpbGFibGUuIiwidGh1bWJuYWlsVXJsIjoiaHR0cHM6Ly9jZG4uaGlidXdlYnNpdGVzLmNvbS9mNmNkZmFiYmExYzc0OWExYTNkM2I3YTlmNGMzMGM3Ni9kbXMzcmVwL211bHRpL3ZpZGVvLWp0bi1lbGVjdHJpY2FsLmpwZyIsImNvbn
3257RlbnRVcmwiOiJ2aWRlb3MuaGlidXN0dWRpby5jb20vMjAyNi9qdG4tZWxlY3RyaWNhbC03MDAzNzkyNTEwLm1wNCIsInVwbG9hZERhdGUiOiIyMDI2LTA5LTI0In0="> <div class="vidWrapper" itemprop="video" itemscope="" itemtype="http://schema.org/VideoObject"> <meta itemprop="name" content="JTN Electrical"></meta> 3258 <meta itemprop="duration" content="T1M36S"></meta> 3259 <meta itemprop="uploadDate" content="2026-09-24"></meta> 3260 <meta itemprop="thumbnailUrl" content="https://cdn.hibuwebsites.com/f6cdfabba1c749a1a3d3b7a9f4c30c76/dms3rep/multi/video-jtn-electrical.jpg"></meta> 3261 <meta itemprop="contentURL" content="videos.hibustudio.com/2026/jtn-electrical-7003792510.mp4"></meta> 3262 <meta itemprop="description" content="We offer electrical installations, electrical evaluations, & electrical repairs. Emergency electrical services. Quick response times. Free estimates available."></meta> 3263 <meta itemprop="transcript" content=""></meta> 3264 <div class="vidInner"> <center> <p class="vidTitle1"></p> 3265 <p class="vidTitle2"></p> 3266 <div class="playIcon"> <a value="hibu-video-splash" type="popup" href="/hibu-video-splash" raw_url="/site/f6cdfabba1c749a1a3d3b7a9f4c30c76/hibu-video-splash?nee=true&ed=true&showOriginal=true&preview=true&dm_try_mode=true&dm_checkSync=1" popup_target="hibu-video-splash" link_type="popup"> <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewbox="0 0 100 100"> <path id="Icon_fa-regular-circle-play" data-name="Icon fa-regular-circle-play" d="M90.625,50A40.625,40.625,0,1,0,50,90.625,40.625,40.625,0,0,0,90.625,50ZM0,50a50,50,0,1,1,50,50A50,50,0,0,1,0,50ZM36.777,28.73a4.693,4.693,0,0,1,4.746.1L69.648,46.016a4.692,4.692,0,0,1,0,8.008L41.523,71.211a4.738,4.738,0,0,1-4.746.1,4.658,4.658,0,0,1-2.4-4.082V32.813A4.658,4.658,0,0,1,36.777,28.73Z"></path> 3267</svg> 3268</a> 3269</div> 3270</center> 3271</div> 3272 <div class="vidOverlay"></div> 3273</div> 3274</div> 3275</div> 3276</div> 3277</div> 3278</div> 3279</section> 3280</div> 3281 <div data-layout-section="" data-auto="flex-section" id="1248306277" data-flex-id="b6b5923e"> <div class="flex-widgets-container" id="1193597235"></div> 3282 <section id="e4ad708c" class="flex-element section" data-auto="flex-element-section"> <div id="2bcd41ae" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="4efeffee" class="flex-element group" data-auto="flex-element-group"> <div id="group_b0q" class="flex-element group" data-auto="flex-element-group"> <div id="widget_1j0" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1411819092"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1411819092" style=""> <h6><span style="color: var(--color_9); display: initial;">VISIT US</span></h6> 3283</div> 3284</div> 3285 <div id="widget_mse" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="divider" data-external-id="1644223884"> <div class="dmDividerWrapper clearfix u_1644223884" data-element-type="dDividerId" data-layout="divider-style-1" data-widget-version="2" id="1644223884" layout="divider-gradient-line"><hr class="dmDivider" style="border-width:2px; border-top-style:solid; color:grey;" id="1089896382"/></div> 3286</div> 3287 <div id="widget_umk" class="flex-element widget-wrapper YXTLOCATION" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="3884292117"> <div class="dmNewParagraph u_3884292117" data-element-type="paragraph" data-version="5" id="3884292117" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJMb2MtMFwiLFwiZm5cIjpcIkFkZHJlc3Nlc1wifSJ9XQ==" data-diy-text=""> <div class="rteBlock">50C King Spring Rd<br/>Windsor Locks, CT 06096</div> 3288</div> 3289</div> 3290 <div id="87a53ad2" class="flex-element widget-wrapper YXTSERVICEAREA" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-hidden-on-desktop="" data-hidden-on-tablet="" data-hidden-on-mobile="" data-external-id="1928174169"> <div class="dmNewParagraph u_1928174169" data-element-type="paragraph" data-version="5" id="1928
3290174169" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJMb2MtMFwiLFwiZm5cIjpcIkZvb3Rlcl9TZXJ2aWNlX0FyZWFzXCJ9In1d" data-diy-text=""><p class="rteBlock">Agawam, MA</p><p class="rteBlock">East Longmeadow, MA</p><p class="rteBlock">Longmeadow, MA</p></div> 3291</div> 3292 <div id="6efb2802" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-hidden-on-desktop="" data-hidden-on-tablet="" data-hidden-on-mobile="" data-external-id="1673689127"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1673689127" style=""><p class="text-align-left m-text-align-left t-size-15 m-size-15 size-15"><span class="font-size-15 t-font-size-15 m-font-size-15" style="color: var(--color_9); display: unset;">and surrounding areas</span></p></div> 3293</div> 3294 <div id="widget_p1h" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="spacer" data-external-id="1070152015"> <div data-element-type="spacer" class="dmSpacer" id="1070152015"></div> 3295</div> 3296 <div id="widget_ipg" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="clicktomap" data-external-id="1178389292"> <a class="default align-center flexButton u_1178389292 dmMap dmWidget dmNoMark dmWwr" href="https://maps.google.com/maps?q=50C+King+Spring+Rd%2C+Windsor+Locks%2C+CT+06096%2C+US&sll=41.9448313,-72.66665" contenteditable="false" onclick=";return dm_gaq_push_event('ClickToMap', 'Map',null,'f6cdfabba1c749a1a3d3b7a9f4c30c76', this,{hitCallBack:true});" id="1178389292" dmle_extension="mapextension" data-element-type="mapextension" modedesktop="" data-popup-title="" data-binding="W3siYmluZGluZ05hbWUiOiJhZGRyZXNzIiwidmFsdWUiOiJjb250ZW50X2xpYnJhcnkuZ2xvYmFsIn1d" data-popup-description-visible="false" modemobile="" addresstodisplay="50C King Spring Rd, Windsor Locks, CT 06096, US" geocompleteaddress="50C King Spring Rd, Windsor Locks, CT 06096, US" data-popup-display-desktop="" data-popup-display-mobile="" data-display-type="block" data-popup-title-visible="false" data-popup-description="" dmmapsource="dm" modetablet="" wr="true" data-popup-show="false" icon="true" surround="true" description="Get Directions" adwords="" icon-name="icon-map-marker" street="" city="" country="US" country_full="United States" state="" zip="" business="" provider="mapbox" lon="-72.66665" lat="41.9448313" faddr="50C King Spring Rd, Windsor Locks, CT 06096, US"> <span class="iconBg" data-auto="button-icon-bg" aria-hidden="true"> <span class="icon hasFontIcon icon-map-marker" data-auto="button-icon"></span> 3297</span> 3298 <span class="text" data-auto="button-text">Get Directions</span> 3299</a> 3300</div> 3301</div> 3302 <div id="d0234170" class="flex-element group" data-auto="flex-element-group"> <div id="cbc8b291" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1563947725"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1563947725" style=""> <h6><span style="display: unset; color: var(--color_9);">CONTACT US</span></h6> 3303</div> 3304</div> 3305 <div id="0b55734b" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="divider" data-external-id="1078075131"> <div class="dmDividerWrapper clearfix u_1078075131" data-element-type="dDividerId" data-layout="divider-style-1" data-widget-version="2" id="1078075131" layout="divider-gradient-line"><hr class="dmDivider" style="border-width:2px; border-top-style:solid; color:grey;" id="1927534912"/></div> 3306</div> 3307 <div id="widget_k5l" class="flex-element widget-wrapper YXTPH" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1675397205"> <div class="dmNewParagraph u_1675397205" data-element-type="paragraph" data-version="5" id="1675397205" data-diy-text="" style="" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJjb250ZW50X2xpYnJhcnkuZ2xvYmFsLnBob25lLmxvYy0wIC0gbWFpbiJ9XQ==">(860) 215-2254</div> 3308</div> 3309 <div id="502c2597" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="spacer" data-external-id="1918773725"> <div data-element-type="spacer" class="dmSpacer" id="1918773725"></div> 3310</div> 3311 <div id="1db7e0c7" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1199369758"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton u_1199369758" file="false" href="/contact#ICcontactform" data-element-type="dButtonLinkId" id="1199369758"> <span class="iconBg" aria-hidden="true" id="1508234354"> <span class="icon hasFontIcon icon-star" id="1201354356"></span> 3312</span> 3313 <span class="text" id="1706213487">Send a Message</span> 3314</a> 3315</div> 3316 <div id="widget_391" class="flex-element widget-wrapper YXTSOCIAL" data-auto="flex-element-widget-wrapper" data-widget-type="socialhub" data-external-id="1064639846"> <div class="align-center text-align-center u_1064639846 dmSocialHub gapSpacing" id="1064639846" dmle_extension="social_hub" data-element-type="social_hub" data-binding="W3siYmluZGluZ05hbWUiOiJzb2NpYWwiLCJ2YWx1ZSI6ImNvbnRlbn
3316RfbGlicmFyeS5nbG9iYWwuc29jaWFsIn1d" wr="true" networks="" icon="true" surround="true" adwords=""> <div class="socialHubWrapper"> <div class="socialHubInnerDiv " data-auto="social-hub-inner"> <a href="https://facebook.com/JTNelectrical" target="_blank" dm_dont_rewrite_url="true" aria-label="facebook" onclick="dm_gaq_push_event && dm_gaq_push_event('socialLink', 'click', 'Facebook')"> <span class="dmSocialFacebook dm-social-icons-facebook oneIcon socialHubIcon style5" aria-hidden="true" data-hover-effect="zoomout"></span> 3317</a> 3318 <a href="https://instagram.com/jtnelectric" target="_blank" dm_dont_rewrite_url="true" aria-label="instagram" onclick="dm_gaq_push_event && dm_gaq_push_event('socialLink', 'click', 'Instagram')"> <span class="dmSocialInstagram dm-social-icons-instagram oneIcon socialHubIcon style5" aria-hidden="true" data-hover-effect="zoomout"></span> 3319</a> 3320</div> 3321</div> 3322</div> 3323</div> 3324</div> 3325 <div id="f2debc4b" class="flex-element group" data-auto="flex-element-group"> <div id="65e14480" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1332286019"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1332286019" style=""> <h6><span style="display: unset; color: var(--color_9);">BUSINESS HOURS</span></h6> 3326</div> 3327</div> 3328 <div id="c7f3ddf3" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="divider" data-external-id="1463975724"> <div class="dmDividerWrapper clearfix u_1463975724" data-element-type="dDividerId" data-layout="divider-style-1" data-widget-version="2" id="1463975724" layout="divider-gradient-line"><hr class="dmDivider" style="border-width:2px; border-top-style:solid; color:grey;" id="1907202694"/></div> 3329</div> 3330 <div id="618f39ae" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="spacer" data-external-id="1700615418"> <div data-element-type="spacer" class="dmSpacer" id="1700615418"></div> 3331</div> 3332 <div id="widget_6mu" class="flex-element widget-wrapper YXTBUSHOURS" data-auto="flex-element-widget-wrapper" data-widget-type="hoursOfOperation" data-external-id="1185800986"> <div class="default u_1185800986 dmHoursOfOperation" id="1185800986" dmle_extension="open_hours" data-element-type="open_hours" data-binding="W3siYmluZGluZ05hbWUiOiJidXNpbmVzc19ob3VycyIsInZhbHVlIjoiY29udGVudF9saWJyYXJ5Lmdsb2JhbC5idXNpbmVzc19ob3VycyJ9XQ==" time_format="12" wr="true" displaytitle="false" lang="" forcedisplay="false" icon="true" surround="true" adwords=""> <dl class="open-hours-data"> <div class="open-hours-item"> <dt day="0">Mon - Fri</dt> 3333 <dd> <time>7:30 am</time> 3334- <time>4:00 pm</time> 3335</dd> 3336</div> 3337 <div class="open-hours-item"> <dt day="5">Sat - Sun</dt> 3338 <dd>Closed</dd> 3339</div> 3340</dl> 3341</div> 3342</div> 3343</div> 3344</div> 3345</div> 3346</section> 3347</div> 3348 <div data-layout-section="" data-auto="flex-section" id="1344609592" data-flex-id="2f831c0f"> <div class="flex-widgets-container" id="1374258866"></div> 3349 <section id="71171ca4" class="flex-element section" data-auto="flex-element-section"> <div id="0253b4c6" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="d027d36f" class="flex-element group" data-auto="flex-element-group" data-hidden-on-tablet="" data-hidden-on-mobile=""> <div id="00841c9d" class="flex-element group" data-auto="flex-element-group"> <div id="7e326d80" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-hidden-on-desktop="" data-external-id="1869341664"> <nav class="effect-bottom2 u_1869341664 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="horizontal_nav_layout_1" layout-sub="submenu_horizontal_1" data-show-vertical-sub-items="HOVER" id="1869341664" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="HORIZONTAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/" class="unifiednav__item unav-item unav-top-item dmUDNavigationItem_00 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text=" 3350 Home 3351 " data-auto="page-text-style">Home<span class="icon icon-angle-down"></span> 3352</span> 3353</a> 3354</li> 3355 <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/about" class="unifiednav__item unav-item unav-top-item dmUDNavigationItem_010101599305 unifiednav__item_has-sub-nav" target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="About" data-auto="page-text-style">About<span class="icon icon-angle-down"></span> 3356</span> 3357</a> 3358 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/faqs" class="unifiednav__item unav-item dmUDNavigationItem_010101347615 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="FAQs" data-auto="page-text-style">FAQs<span class="icon icon-angle-right"></span> 3359</span> 3360</a> 3361</li> 3362 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/reviews" class="unifiednav__item unav-item dmUDNavigationItem_010101840192 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Reviews" data-auto="page-text-style">Reviews<span class="icon icon-angle-right"></span> 3363</span> 3364</a> 3365</li> 3366</ul> 3367</li> 3368 <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/contact" class="unifiednav__item unav-item unav-top-item dmUDNavigationItem_010101152840 unifiednav__item_has-sub-nav" target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Contact" data-auto="page-text-style">Contact<span class="icon icon-angle-down"></span> 3369</span> 3370</a> 3371 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/request-service-form" class="unifiednav__item unav-item dmNavItemSelected dmUDNavigationItem_010101150108 " target="" data-target-page-alias="" aria-current="page" data-auto="selected-page"> <span class="nav-item-text " data-link-text="Request Service" data-auto="page-text-style">Request Service<span class="icon icon-angle-right"></span> 3372</span> 3373</a> 3374</li> 3375 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/contact#service-areas" class="unifiednav__item unav-item dmUDNavigationItem_010101813309 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Areas Served" data-auto="page-text-style">Areas Served<span class="icon icon-angle-right"></span> 3376</span> 3377</a> 3378</li> 3379</ul> 3380</li> 3381</ul> 3382</nav> 3383</div> 3384</div> 3385</div> 3386</div> 3387</section> 3388</div> 3389 <div data-layout-section="" data-auto="flex-section" id="1741943752" data-flex-id="54290af5" class="" data-hidden-on-desktop="true"> <div class="flex-widgets-container" id="1020340060"></div> 3390 <section id="0c6cf26f" class="flex-element section" data-auto="flex-element-section"> <div id="3989af5b" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="24eff763" class="flex-element group" data-auto="flex-element-group" data-hidden-on-tablet="" data-hidden-on-desktop="">
3390 <div id="7ca01c83" class="flex-element group" data-auto="flex-element-group"> <div id="34b9f983" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ssrbreadcrumbs" data-external-id="1424178368"> <div id="1424178368" dmle_extension="ssrbreadcrumbs" data-element-type="ssrbreadcrumbs" class=""><span id="ssrWrap-1424178368" ><style data-styled="true" data-styled-version="5.3.11">#dm .hwJXuA.hwJXuA{color:rgba(102,102,102,1);}/*!sc*/ 3391data-styled.g17[id="sc-cPiKLX"]{content:"hwJXuA,"}/*!sc*/ 3392.gCAEgf{color:rgba(102,102,102,1);}/*!sc*/ 3393data-styled.g18[id="sc-dLMFU"]{content:"gCAEgf,"}/*!sc*/ 3394#dm .dewijn.dewijn{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:10px;}/*!sc*/ 3395@media (min-width:768px){#dm .dewijn.dewijn{font-size:11px;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;}}/*!sc*/ 3396@media (max-width:767px){#dm .dewijn.dewijn{font-size:11px;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;}}/*!sc*/ 3397data-styled.g19[id="sc-eDPEul"]{content:"dewijn,"}/*!sc*/ 3398#dm .cZcQVQ.cZcQVQ{color:rgba(102,102,102,1);-webkit-transform:rotate(0);-ms-transform:rotate(0);transform:rotate(0);display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;width:16px;}/*!sc*/ 3399#dm .cZcQVQ.cZcQVQ svg{width:100%;height:100%;fill:currentColor;}/*!sc*/ 3400data-styled.g20[id="sc-eldPxv"]{content:"cZcQVQ,"}/*!sc*/ 3401</style><div data-auto="runtime-breadcrumbs-widget">
3401<script data-auto="schema" type="application/ld+json">{"@context":"http://schema.org/","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"name":"Home","id":"/"}},{"@type":"ListItem","position":2,"item":{"name":"Contact","id":"/contact"}},{"@type":"ListItem","position":3,"item":{"name":"Request Service"}}]}</script>
3401<nav class="sc-eDPEul dewijn bc-root dmBlockElement" aria-label="Breadcrumbs"><a href="/" class="sc-cPiKLX hwJXuA bc-item"><div class="sc-dLMFU">Home</div></a><span direction="ltr" class="sc-eldPxv cZcQVQ bc-separator" aria-hidden="true"><svg data-auto="arrow" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.46967 3.46967C5.76256 3.17678 6.23744 3.17678 6.53033 3.46967L10.5303 7.46967C10.8232 7.76256 10.8232 8.23744 10.5303 8.53033L6.53033 12.5303C6.23744 12.8232 5.76256 12.8232 5.46967 12.5303C5.17678 12.2374 5.17678 11.7626 5.46967 11.4697L8.93934 8L5.46967 4.53033C5.17678 4.23744 5.17678 3.76256 5.46967 3.46967Z" fill="currentColor"></path></svg></span><a href="/contact" class="sc-cPiKLX hwJXuA bc-item"><div class="sc-dLMFU">Contact</div></a><span direction="ltr" class="sc-eldPxv cZcQVQ bc-separator" aria-hidden="true"><svg data-auto="arrow" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.46967 3.46967C5.76256 3.17678 6.23744 3.17678 6.53033 3.46967L10.5303 7.46967C10.8232 7.76256 10.8232 8.23744 10.5303 8.53033L6.53033 12.5303C6.23744 12.8232 5.76256 12.8232 5.46967 12.5303C5.17678 12.2374 5.17678 11.7626 5.46967 11.4697L8.93934 8L5.46967 4.53033C5.17678 4.23744 5.17678 3.76256 5.46967 3.46967Z" fill="currentColor"></path></svg></span><div class="sc-dLMFU gCAEgf bc-current-item" data-auto="bc-current-item">Request Service</div></nav></div></span></div> 3402</div> 3403</div> 3404</div> 3405</div> 3406</section> 3407</div> 3408</div> 3409 <div id="poweredBy" class="poweredBy poweredByHibu"> <div class="poweredLine1"> <a href="http://budurl.com/hibuprivacy" target="_blank">Privacy Policy</a>| <a href="http://budurl.com/hibucookie" target="_blank">Do Not Share My Information</a>| <a href="http://budurl.com/hibuconditionsofuse" target="_blank">Conditions of Use</a>| <a href="http://budurl.com/hibunotice" target="_blank">Notice and Take Down Policy</a>| <a href="http://b.link/accessibility" target="_blank">Website Accessibility Policy</a> 3410</div> 3411 <div class="poweredLine2">© <span id="hibuYear">2026</span> 3412The content on this website is owned by us and our licensors. Do not copy any content (including images) without our consent.</div> 3413</div> 3414</div> 3415 <div id="dmFlexStickyContainer" class="flex_hfcontainer"> <div id="flex-sticky"> <div data-layout-section="" data-auto="flex-section" id="1801865596" data-flex-id="8a9cfcfc"> <div class="flex-widgets-container" id="1548435334"></div> 3416 <section id="4a1fde56" class="flex-element section" data-auto="flex-element-section"> <div id="381d99f1" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="75ba1c41" class="flex-element group" data-auto="flex-element-group"> <div id="4f4d3298" class="flex-element group" data-auto="flex-element-group"> <div id="72168835" class="flex-element inner-grid" data-auto="flex-element-inner-grid"> <div id="widget_21v" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="graphic" data-keep-proportion="true" data-hidden-on-tablet="" data-hidden-on-desktop="" data-external-id="1389791254"> <div class="graphicWidget graphicWidgetV2 graphicWidgetV3 u_1389791254" data-element-type="graphic" data-widget-type="graphic" id="1389791254" data-layout="graphic-style-5"> <a href="/hibu-eng-menu" id="1685777839" link_type="popup" file="false" popup_target="hibu-eng-menu" aria-label="Contact Us"> <svg width="100%" height="100%" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1047488693" class="svg u_1047488693" data-icon-custom="true" aria-labelledby="1665903033"> <title id="1665903033">Contact Us</title> 3417 <path d="m90.793 43.289h-34.086v-34.078c0-3.707-3.0039-6.707-6.707-6.707s-6.707 3-6.707 6.707v34.078h-34.086c-3.7031 0-6.707 3.0039-6.707 6.707s3.0039 6.707 6.707 6.707h34.086v34.086c0 3.707 3.0039 6.707 6.707 6.707s6.707-3 6.707-6.707v-34.086h34.086c3.7031 0 6.707-3 6.707-6.707 0-3.707-3.0039-6.707-6.707-6.707z"></path> 3418</svg> 3419</a> 3420</div> 3421</div> 3422 <div id="widget_fpt" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="3053959938"> <div class="widget-0c06e7 dmCustomWidget" data-lazy-load="true" data-title="" id="3053959938" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="0c06e70da5474879a21daa0d3b1192c0" data-widget-version="3" data-widget-config="e30="> <div class="cb1-theme-injector"></div> 3423</div> 3424</div> 3425 <div id="widget_mnf" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="3931136021"> <div class="widget-8eedf9 dmCustomWidget" data-lazy-load="true" data-title="" id="3931136021" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="8eedf98f346a42e0a55f9d6152653187" data-widget-version="2" data-widget-config="e30="> <div class="cb0-theme-injector"></div> 3426</div> 3427</div> 3428 <div id="widget_c5v" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="3659398620"> <div class="widget-1c20f5 dmCustomWidget" data-lazy-load="true" data-title="" id="3659398620" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="1c20f508b5474ea2b929959f7d5eff3d" data-widget-version="3" data-widget-config="e30="> <div class="dcbh3-theme-injector"></div> 3429</div> 3430</div> 3431 <div id="widget_doa" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1889303332"> <div class="widget-403a95 dmCustomWidget" data-lazy-load="true" data-title="" id="1889303332" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="403a957a34f84bc8a342137c59c93b31" data-widget-version="3" data-widget-config="e30="> <div class="cb2-theme-injector"></div> 3432</div> 3433</div> 3434 <div id="widget_301" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="986065271"> <div class="widget-e0b1e5 dmCustomWidget" data-lazy-load="true" data-title="" id="986065271" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="e0b1e5b2d4394e69a98bd276fd6afe8b" data-widget-version="8" data-widget-config="e30="> <div class="cb1-theme-injector"></div> 3435</div> 3436</div> 3437 <div id="widget_61p" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="3078440210"> <div class="widget-2133bf dmCustomWidget" data-lazy-load="true" data-title="" id="3078440210" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="2133bfee2cb24e0c9db9468154881b94" data-widget-version="3" data-widget-config="e30="> <div class="cb1-theme-injector"></div> 3438</div> 3439</div> 3440</div> 3441</div> 3442</div> 3443</div> 3444</section> 3445</div> 3446</div> 3447</div> 3448 <div id="dmFlexMegaMenuContainer" class="flex_hfcontainer"> <div id="multi-mega-menu"> <div id="mega-menu-4dc8a903" name="Services"> <div data-flex-id="4dc8a903" data-auto="flex-section" id="1276875426" data-layout-section="" data-page-element-type="section" data-page-element-id="1232093299" class="" data-hidden-on-desktop="true"> <div class="flex-widgets-container" id="1497300766"></div> 3449 <section id="6c5257e3" class="flex-element section" data-auto="flex-element-section"> <div id="2cb3e9f9" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="27581a62" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop=""> <div id="d200b2f7" class="flex-element group" data-auto="flex-element-group"> <div id="widget_cq9" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1459911792"> <nav class="u_1459911792 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1459911792" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/what-we-do/~page-item~" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="cat-landing"> <span class="nav-item-text " data-link-text="Cat-Landing" data-auto="page-text-style">Cat-Landing<span class="icon icon-angle-down"></span> 3450</span> 3451</a> 3452 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/services/~page-item~" class="unifiednav__item unav-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">
3452Core Page<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 3453</span> 3454</a> 3455</li> 3456</ul> 3457</li> 3458</ul> 3459</nav> 3460</div> 3461 <div id="b51d88ee" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1723750523"> <nav class="u_1723750523 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1723750523" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/what-we-do/~page-item~" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="cat-landing"> <span class="nav-item-text " data-link-text="Cat-Landing" data-auto="page-text-style">Cat-Landing<span class="icon icon-angle-down"></span> 3462</span> 3463</a> 3464 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/services/~page-item~" class="unifiednav__item unav-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">Core Page<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 3465</span> 3466</a> 3467</li> 3468</ul> 3469</li> 3470</ul> 3471</nav> 3472</div> 3473 <div id="5aa033c8" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1610592242"> <nav class="u_1610592242 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1610592242" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/what-we-do/~page-item~" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="cat-landing"> <span class="nav-item-text " data-link-text="Cat-Landing" data-auto="page-text-style">Cat-Landing<span class="icon icon-angle-down"></span> 3474</span> 3475</a> 3476 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/services/~page-item~" class="unifiednav__item unav-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">Core Page<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 3477</span> 3478</a> 3479</li> 3480</ul> 3481</li> 3482</ul> 3483</nav> 3484</div> 3485</div> 3486 <div id="ad9cb284" class="flex-element group" data-auto="flex-element-group"> <div id="04df1da2" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1136789265"> <nav class="u_1136789265 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1136789265" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/what-we-do/~page-item~" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="cat-landing"> <span class="nav-item-text " data-link-text="Cat-Landing" data-auto="page-text-style">Cat-Landing<span class="icon icon-angle-down"></span> 3487</span> 3488</a> 3489 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/services/~page-item~" class="unifiednav__item unav-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">
3489Core Page<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 3490</span> 3491</a> 3492</li> 3493</ul> 3494</li> 3495</ul> 3496</nav> 3497</div> 3498 <div id="f09d8243" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1968129260"> <nav class="u_1968129260 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1968129260" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/what-we-do/~page-item~" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="cat-landing"> <span class="nav-item-text " data-link-text="Cat-Landing" data-auto="page-text-style">Cat-Landing<span class="icon icon-angle-down"></span> 3499</span> 3500</a> 3501 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/services/~page-item~" class="unifiednav__item unav-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">Core Page<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 3502</span> 3503</a> 3504</li> 3505</ul> 3506</li> 3507</ul> 3508</nav> 3509</div> 3510 <div id="899c2eb2" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1507374835"> <nav class="u_1507374835 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1507374835" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/what-we-do/~page-item~" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="cat-landing"> <span class="nav-item-text " data-link-text="Cat-Landing" data-auto="page-text-style">Cat-Landing<span class="icon icon-angle-down"></span> 3511</span> 3512</a> 3513 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/services/~page-item~" class="unifiednav__item unav-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">Core Page<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 3514</span> 3515</a> 3516</li> 3517</ul> 3518</li> 3519</ul> 3520</nav> 3521</div> 3522</div> 3523 <div id="0190983d" class="flex-element group" data-auto="flex-element-group"> <div id="7b991c9b" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1801032432"> <nav class="u_1801032432 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1801032432" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/what-we-do/~page-item~" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="cat-landing"> <span class="nav-item-text " data-link-text="Cat-Landing" data-auto="page-text-style">Cat-Landing<span class="icon icon-angle-down"></span> 3524</span> 3525</a> 3526 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/services/~page-item~" class="unifiednav__item unav-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">
3526Core Page<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 3527</span> 3528</a> 3529</li> 3530</ul> 3531</li> 3532</ul> 3533</nav> 3534</div> 3535 <div id="39a585e9" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1338154768"> <nav class="u_1338154768 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1338154768" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/what-we-do/~page-item~" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="cat-landing"> <span class="nav-item-text " data-link-text="Cat-Landing" data-auto="page-text-style">Cat-Landing<span class="icon icon-angle-down"></span> 3536</span> 3537</a> 3538 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/services/~page-item~" class="unifiednav__item unav-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">Core Page<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 3539</span> 3540</a> 3541</li> 3542</ul> 3543</li> 3544</ul> 3545</nav> 3546</div> 3547 <div id="4310a1b0" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1201338092"> <nav class="u_1201338092 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1201338092" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/what-we-do/~page-item~" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="cat-landing"> <span class="nav-item-text " data-link-text="Cat-Landing" data-auto="page-text-style">Cat-Landing<span class="icon icon-angle-down"></span> 3548</span> 3549</a> 3550 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/services/~page-item~" class="unifiednav__item unav-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">Core Page<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 3551</span> 3552</a> 3553</li> 3554</ul> 3555</li> 3556</ul> 3557</nav> 3558</div> 3559</div> 3560 <div id="group_9n6" class="flex-element group" data-auto="flex-element-group"> <div id="fe7f7167" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1401180572"> <nav class="u_1401180572 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1401180572" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/services/~page-item~" class="unifiednav__item unav-item unav-top-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">
3560Core Page<span class="icon icon-angle-down" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 3561</span> 3562</a> 3563</li> 3564 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/services/~page-item~" class="unifiednav__item unav-item unav-top-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">Core Page<span class="icon icon-angle-down" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 3565</span> 3566</a> 3567</li> 3568</ul> 3569</nav> 3570</div> 3571</div> 3572</div> 3573</div> 3574</section> 3575</div> 3576 <div data-layout-section="" data-auto="flex-section" id="1582186687" data-flex-id="2a501ecf"> <div class="flex-widgets-container" id="1924641454"></div> 3577 <section id="c55100d8" class="flex-element section" data-auto="flex-element-section"> <div id="57b153fb" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="08465346" class="flex-element group" data-auto="flex-element-group"> <div id="5a572945" class="flex-element group" data-auto="flex-element-group"> <div id="widget_gf3" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="224877366"> <div class="widget-3e38ec u_224877366 dmCustomWidget" data-lazy-load="true" data-title="" id="224877366" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="3e38ec713838428586c343d139994b6f" data-widget-version="58" data-widget-config="eyJjb2x1bW5Db3VudCI6IjMiLCJ1bmRlcmxpbmVTZWxlY3RlZCI6dHJ1ZSwic2VsZWN0ZWRCZ0NvbG9yIjoidHJhbnNwYXJlbnQiLCJleGNsdWRlUGFyZW50cyI6ImhvbWUsIGFib3V0LCBjb250YWN0LCBnYWxsZXJ5IiwiaW5jbHVkZVBhcmVudHMiOiIiLCJjaGV2cm9uSWNvbiI6IjxzdmcgeG1sbnM9XCJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Z1wiIHZpZXdCb3g9XCIwIDAgMjQgMjRcIiBmaWxsPVwiY3VycmVudENvbG9yXCIgd2lkdGg9XCIxNlwiIGhlaWdodD1cIjE2XCI+PHBhdGggZD1cIk03LjQxIDguNTlMMTIgMTMuMTdsNC41OS00LjU4TDE4IDEwbC02IDYtNi02IDEuNDEtMS40MXpcIi8+PC9zdmc+IiwiZXhwYW5kQWxsIjp0cnVlfQ=="><!-- === Real-Data Navigation Grid Widget (parent remains a hyperlink) === --> <div id="nav-grid-widget" data-columns="3" data-source="auto"></div> 3578</div> 3579</div> 3580</div> 3581</div> 3582</div> 3583</section> 3584</div> 3585</div> 3586</div> 3587</div> 3588 <div id="swh-492bf3ca8d6f4982917ac4c553c6727d"> <div data-wrapper-source="appstore"><!--audioeye-managed:accessibility-->
3588<script data-audioeye-managed="accessibility"> if (window.location.search.match(/[?,&]preview=true/) && window.top !== window) { window.__AudioEyePreviewMode=true; } </script>
3588 3589
3589<script type="text/javascript">!function(){ var b=function(){ window.__AudioEyeSiteHash = "c450edbb1099533195e4a26d444b2b1b"; window.__AudioEyeInstallSource = "duda"; var a=document.createElement("script"); a.src="https://wsv3cdn.audioeye-services.com/aem.js"; a.type="text/javascript"; a.setAttribute("async",""); document.getElementsByTagName("body")[0].appendChild(a)}; "complete"!==document.readyState?window.addEventListener?window.addEventListener("load",b):window.attachEvent&&window.attachEvent("onload",b):b()}(); </script>
3589 3590</div> 3591</div> 3592</div> 3593</div> 3594</div> 3595</div> 3596</div> 3597</div> 3598 3599 </div> 3600</div> 3601 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-mobile_implicit-flex-header-3dd8cf0da1894ab5a9043c416f0c4aca">@media (max-width: 767px) { 3602}</style> 3603 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-mobile_implicit-4dc8a903-3dd8cf0da1894ab5a9043c416f0c4aca">@media (max-width: 767px) { 3604 3605#\32 cb3e9f9 { 3606 grid-template-rows: minmax(8px, max-content); 3607} 3608 3609#\32 7581a62 { 3610 grid-area: 1 / 1 / span 1 / span 1; 3611} 3612}</style> 3613 <style data-page-uuid="8d67b06083f34b4a8a328c95f017fec2" id="grid-styles-mobile-24b6b13b-8d67b06083f34b4a8a328c95f017fec2">@media (max-width: 767px) { 3614 3615#d4329249 { 3616 min-height: 80px; 3617 align-items: center; 3618 width: 100%; 3619} 3620 3621#\33 4c91571 { 3622 min-height: 0; 3623 flex-direction: column; 3624 padding-left: 4%; 3625 padding-right: 4%; 3626 flex-wrap: nowrap; 3627} 3628}</style> 3629 <style data-page-uuid="8d67b06083f34b4a8a328c95f017fec2" id="grid-styles-mobile-7ef32d84-8d67b06083f34b4a8a328c95f017fec2">@media (max-width: 767px) { 3630 3631#\36 a108e83 { 3632 min-height: 0; 3633 flex-direction: column; 3634 padding-left: 4%; 3635 padding-right: 4%; 3636 flex-wrap: nowrap; 3637} 3638 3639#\38 c30d83a { 3640 width: 100%; 3641 min-height: 0px; 3642 align-items: flex-start; 3643} 3644}</style> 3645 <style data-page-uuid="8d67b06083f34b4a8a328c95f017fec2" id="grid-styles-mobile-5c4456af-8d67b06083f34b4a8a328c95f017fec2">@media (max-width: 767px) { 3646 3647#\35 20e9609 { 3648 min-height: 0; 3649 flex-direction: column; 3650 padding-left: 4%; 3651 padding-right: 4%; 3652 flex-wrap: nowrap; 3653} 3654 3655#\36 8fb7ec9 { 3656 width: 100%; 3657 min-height: 80px; 3658 align-items: center; 3659 padding-left: 1px; 3660 padding-right: 1px; 3661} 3662}</style> 3663 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-mobile-flex-footer-3dd8cf0da1894ab5a9043c416f0c4aca">@media (max-width: 767px) { 3664 3665}</style> 3666 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-mobile-flex-header-3dd8cf0da1894ab5a9043c416f0c4aca">@media (max-width: 767px) { 3667 3668#flex-header[data-changeable-on-scroll][data-scrolling] [data-widget-type="image"] img { 3669 transform: scale(1); 3670} 3671#flex-header { 3672 3673 padding-top: 0%; 3674 padding-bottom: 0%; 3675} 3676}</style> 3677 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-mobile-405ddf11-3dd8cf0da1894ab5a9043c416f0c4aca">@media (max-width: 767px) { 3678 3679#\31 d3acdb9 { 3680 flex-direction: row; 3681 padding-left: 0%; 3682 padding-right: 0%; 3683 flex-wrap: nowrap; 3684 min-height: 60px; 3685} 3686 3687#\39 f3f031f { 3688 width: 100%; 3689 align-items: center; 3690 min-height: 8px; 3691 column-gap: 0%; 3692 justify-content: center; 3693 flex-direction: column; 3694 flex-wrap: nowrap; 3695 row-gap: 0px; 3696 padding: 0px 0px 0px 0px; 3697} 3698 3699#\31 1d216b4 { 3700 width: 15%; 3701 order: 0; 3702 justify-self: auto; 3703 align-self: auto; 3704 margin: 0px 0% 0px 0%; 3705 grid-area: auto / auto / auto / auto; 3706} 3707 3708#bff1bb66 { 3709 height: auto; 3710 width: 227px; 3711} 3712 3713#a052d7aa { 3714 order: 0; 3715 justify-self: auto; 3716 align-self: center; 3717 width: 50%; 3718 justify-content: center; 3719 padding-right: 1%; 3720 padding-left: 1%; 3721 margin: 0px 0% 0px 0%; 3722 grid-area: auto / auto / auto / auto; 3723} 3724 3725#\39 8702d80 { 3726 min-height: 170px; 3727} 3728 3729#da0131a7 { 3730 width: 70%; 3731 min-height: 70px; 3732 justify-self: auto; 3733 align-self: auto; 3734 order: 1; 3735 margin: 0px 0% 0px 0%; 3736 grid-area: auto / auto / auto / auto; 3737} 3738 3739#\39 e5ef93d { 3740 justify-self: auto; 3741 width: 100%; 3742 order: 0; 3743 min-height: 32px; 3744 align-self: auto; 3745 padding: 0% 4% 0% 4%; 3746 margin: 0px 0% 0px 0%; 3747 grid-area: auto / auto / auto / auto; 3748} 3749 3750#\33 aa37ebb { 3751 width: 15%; 3752 justify-content: center; 3753 order: 2; 3754} 3755 3756#\32 e9284f4 { 3757 align-self: center; 3758 margin-left: 0px; 3759 margin-right: 0px; 3760 align-items: center; 3761 min-height: 80px; 3762} 3763 3764#\30 ece3d12 { 3765 justify-self: auto; 3766 align-self: center; 3767 width: 50%; 3768 order: 1; 3769 justify-content: center; 3770 padding-left: 1%; 3771 padding-right: 1%; 3772 margin: 0px 0% 0px 0%; 3773 grid-area: auto / auto / auto / auto; 3774} 3775 3776#daf81bcc.flex-element.widget-wrapper >
3776 [data-element-type].flexButton.dmWidget { 3777 padding: 10px 0px 10px 0px; 3778} 3779#daf81bcc { 3780 width: 250px; 3781 min-height: 40px; 3782 margin: 0px 0% 0px 0%; 3783} 3784 3785#\34 cb86e4c { 3786 align-items: center; 3787 min-height: 32px; 3788} 3789 3790#df845449 { 3791 min-height: 40px; 3792} 3793 3794#ec5d46f2 { 3795 width: 340px; 3796 height: auto; 3797} 3798}</style> 3799 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-mobile-157b4fff-3dd8cf0da1894ab5a9043c416f0c4aca">@media (max-width: 767px) { 3800 3801#\32 fd7b414 { 3802 width: 25%; 3803 align-items: center; 3804 min-height: 8px; 3805 justify-content: center; 3806 padding-top: 4px; 3807 padding-bottom: 4px; 3808 column-gap: 2%; 3809} 3810 3811#\33 2767c2f { 3812 flex-direction: row; 3813 padding-left: 0%; 3814 padding-right: 0%; 3815 flex-wrap: nowrap; 3816 min-height: 60px; 3817} 3818 3819#group_7in { 3820 width: 25%; 3821 min-height: 8px; 3822} 3823 3824#group_jjh { 3825 width: 25%; 3826 min-height: 8px; 3827} 3828 3829#group_23b { 3830 width: 25%; 3831 min-height: 8px; 3832} 3833}</style> 3834 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-mobile-c0ae99ba-3dd8cf0da1894ab5a9043c416f0c4aca">@media (max-width: 767px) { 3835 3836#\33 710bc01 { 3837 width: 100%; 3838 align-items: center; 3839 min-height: 8px; 3840} 3841 3842#\39 360b347 { 3843 flex-direction: row; 3844 padding-left: 0%; 3845 padding-right: 0%; 3846 flex-wrap: nowrap; 3847 min-height: 60px; 3848} 3849 3850#bd946f29 { 3851 order: 2; 3852} 3853 3854#\35 bfb3718 { 3855 height: auto; 3856} 3857 3858#\33 a2d0b90 { 3859 order: 4; 3860} 3861 3862#a8f32aec { 3863 order: 2; 3864} 3865}</style> 3866 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-mobile-4a710abd-3dd8cf0da1894ab5a9043c416f0c4aca">@media (max-width: 767px) { 3867 3868#a7708a16 { 3869 width: 100%; 3870 min-height: 80px; 3871} 3872 3873#\37 8db3927 { 3874 min-height: unset; 3875 flex-direction: column; 3876} 3877}</style> 3878 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-mobile-63a049320d35b55b4ef2a372-3dd8cf0da1894ab5a9043c416f0c4aca">@media (max-width: 767px) { 3879 3880#\36 3a049320d35b55b4ef2a374 { 3881 min-height: 695px; 3882 justify-content: flex-start; 3883 padding-top: 10px; 3884 padding-left: 8px; 3885 padding-right: 0px; 3886} 3887 3888#\36 3a049320d35b55b4ef2a377 { 3889 min-height: unset; 3890 flex-direction: column; 3891 padding: 0% 4% 0% 4%; 3892} 3893}</style> 3894 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-mobile-6406026c90e1521f6ecb0eca-3dd8cf0da1894ab5a9043c416f0c4aca">@media (max-width: 767px) { 3895 3896#\36 406026c90e1521f6ecb0ecd { 3897 min-height: 209px; 3898 justify-content: center; 3899 padding-bottom: 8%; 3900 padding-top: 8%; 3901} 3902 3903#\36 406026c90e1521f6ecb0ecc { 3904 min-height: unset; 3905 flex-direction: column; 3906 padding: 0% 4% 0% 4%; 3907} 3908}</style> 3909 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-mobile-2c8e291c-3dd8cf0da1894ab5a9043c416f0c4aca">@media (max-width: 767px) { 3910 3911#b7a700bf { 3912 min-height: 0; 3913 flex-direction: column; 3914 padding-left: 4%; 3915 padding-right: 4%; 3916 flex-wrap: nowrap; 3917} 3918 3919#\36 405d24c { 3920 width: 100%; 3921 min-height: 80px; 3922 align-items: center; 3923 padding: 0px 0px 0px 0px; 3924} 3925}</style> 3926 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-mobile-7b00a5de-3dd8cf0da1894ab5a9043c416f0c4aca">@media (max-width: 767px) { 3927 3928#\39 34bad03 { 3929 width: 100.00%; 3930} 3931 3932#\36 e06962d { 3933 min-height: unset; 3934 flex-direction: column; 3935 padding-left: 4%; 3936 padding-right: 4%; 3937} 3938 3939#\30 5e0ee19::before, #\30 5e0ee19 > .bgExtraLayerOverlay { 3940 3941} 3942#\30 5e0ee19 { 3943 min-height: 160px; 3944 align-items: center; 3945 width: 100%; 3946 background-image: url('https://le-cdn.hibuwebsites.com/f6cdfabba1c749a1a3d3b7a9f4c30c76/dms3rep/multi/opt/video-jtn-electrical-2880w.jpg') !important; 3947} 3948 3949#a019b36e { 3950 min-height: 160px; 3951 align-items: center; 3952 width: 100%; 3953 padding-left: 0px; 3954 padding-right: 0px; 3955} 3956}</style> 3957 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-mobile-b6b5923e-3dd8cf0da1894ab5a9043c416f0c4aca">@media (max-width: 767px) { 3958 3959#\34 efeffee { 3960 flex-direction: column; 3961 padding-left: 4%; 3962 padding-right: 4%; 3963 flex-wrap: nowrap; 3964 min-height: 0; 3965} 3966 3967#group_b0q { 3968 width: 100%; 3969 min-height: 80px; 3970} 3971 3972#f2debc4b { 3973 width: 100%; 3974 min-height: 80px; 3975} 3976 3977#d0234170 { 3978 width: 100%; 3979 min-height: 80px; 3980} 3981}</style> 3982 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-mobile-2f831c0f-3dd8cf0da1894ab5a9043c416f0c4aca">@media (max-width: 767px) { 3983 3984#\30 0841c9d { 3985 width: 100%; 3986 align-items: center; 3987 min-height: 8px; 3988} 3989 3990#d027d36f { 3991 flex-direction: row; 3992 padding-left: 4%; 3993 padding-right: 4%; 3994 flex-wrap: nowrap; 3995 min-height: 80px; 3996} 3997}</style> 3998 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-mobile-54290af5-3dd8cf0da1894ab5a9043c416f0c4aca">@media (max-width: 767px) { 3999 4000#\37 ca01c83 { 4001 width: 100%; 4002 min-height: 0px; 4003 align-items: flex-start; 4004} 4005 4006#\32 4eff763 { 4007 min-height: 0; 4008 flex-direction: column; 4009 flex-wrap: nowrap; 4010 padding: 0px 4% 0px 4%; 4011} 4012 4013#\30 c6cf26f { 4014 margin: 0px 0% 0px 0%; 4015} 4016}</style> 4017 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-mobile-4dc8a903-3dd8cf0da1894ab5a9043c416f0c4aca">@media (max-width: 767px) { 4018 4019#\30 190983d { 4020 width: 100%; 4021 min-height: 80px; 4022 align-items: center; 4023} 4024 4025#d200b2f7 { 4026 width: 100%; 4027 min-height: 80px; 4028 align-items: center; 4029} 4030 4031#\32 7581a62 { 4032 flex-direction: column; 4033 min-height: 0; 4034 flex-wrap: nowrap; 4035 row-gap: 0px; 4036 column-gap: 0%; 4037 padding: 0px 0px 0px 0px; 4038} 4039 4040#ad9cb284 { 4041 width: 100%; 4042 min-height: 80px; 4043 align-items: center; 4044} 4045 4046#group_9n6 { 4047 width: 100%; 4048 min-height: 80px; 4049 align-items: center; 4050} 4051}</style> 4052 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-mobile-2a501ecf-3dd8cf0da1894ab5a9043c416f0c4aca">@media (max-width: 767px) { 4053 4054#\30 8465346 { 4055 min-height: 0; 4056 flex-direction: column; 4057 padding-left: 4%; 4058 padding-right: 4%; 4059 flex-wrap: nowrap; 4060} 4061 4062#\35 a572945 { 4063 width: 100%; 4064 min-height: 80px; 4065 align-items: center; 4066} 4067}</style> 4068 <style data-page-uuid="3dd8cf0da1894ab5a9043c416f0c4aca" data-is-header="true" id="grid-styles-mobile-8a9cfcfc-3dd8cf0da1894ab5a9043c416f0c4aca">@media (max-width: 767px) { 4069 4070#\37 5ba1c41 { 4071 min-height: 0; 4072 flex-direction: column; 4073 padding-left: 4%; 4074 padding-right: 4%; 4075 flex-wrap: nowrap; 4076} 4077 4078#\34 f4d3298 { 4079 width: 100%; 4080 min-height: 80px; 4081 align-items: center; 4082} 4083 4084#widget_21v { 4085 max-width: 100%; 4086 width: 55px; 4087 margin: 20px 20px 20px 20px; 4088} 4089 4090#widget_fpt { 4091 max-width: 100%; 4092 margin: 20px 20px 20px 20px; 4093} 4094 4095#widget_301 { 4096 max-width: 100%; 4097 margin: 20px 20px 20px 20px; 4098} 4099}</style> 4100 4101 4102 4103 4104 4105 4106 4107 4108 4109 4110 4111 4112 4113 4114 4115 4116 4117 4118 4119 4120 4121 4122 4123 4124 4125 4126 4127 4128 4129 4130 4131 4132 4133 4134 4135 4136 4137 4138
4139<script> 4140 var isWLR = true; 4141 4142 window.customWidgetsFunctions = {}; 4143 window.customWidgetsStrings = {}; 4144 window.collections = {}; 4145 window.currentLanguage = "ENGLISH" 4146 window.isSitePreview = false; 4147</script>
4147 4148 4149 4150
4151<script> 4152 window.customWidgetsFunctions["5b093d0fb9474c1dabfa7807be463b12~52"] = function (element, data, api) { 4153 window.dmAPI.runOnReady('nmw-mega-nav', () => { 4154 'use strict'; 4155 4156 /* 4157 NMW NAVIGATION ARCHITECTURE 4158 4159 FULL WIDGET 4160 - Fetches 06 NAV MASTER, 02 CATEGORY, 03 CORE, and optionally CUSTOMNAV. 4161 - Reads configured native pages from the hidden .navmasterdefault Duda nav. 4162 - Builds one normalized three-level navigation model. 4163 - Renders the Full navigation and publishes the shared model through window.NMW. 4164 4165 MIRROR WIDGET 4166 - Never fetches collections. 4167 - Waits for the Full widget to publish shared data. 4168 - Renders the same data vertically and stays synchronized. 4169 4170 SHARED BEHAVIOR 4171 - Click and optional hover opening. 4172 - 200 ms hover-close delay. 4173 - Escape, focus-leave, link-click, and outside-click closing. 4174 - Active-page detection from the actual current URL. 4175 - Prefix-aware dynamic matching (for example /category/x and /services/x). 4176 - JavaScript owns menu state; CSS owns appearance, positioning, and animation. 4177 */ 4178 4179 const root = element.querySelector('.nmw-root'); 4180 if (!root) return; 4181 4182 /* 4183 DUDA EDITOR LIFECYCLE 4184 4185 Duda can execute the widget JavaScript again against the same DOM root when 4186 a panel value changes. The prior closure is not automatically destroyed. 4187 Without explicit cleanup, the old instance continues observing/fetching and 4188 can overwrite the navigation built with the new configuration. 4189 */ 4190 window.NMW_RUNTIME = window.NMW_RUNTIME || { instances: new WeakMap() }; 4191 const previousInstanceCleanup = window.NMW_RUNTIME.instances.get(root); 4192 if (typeof previousInstanceCleanup === 'function') { 4193 try { 4194 previousInstanceCleanup('reinitialized'); 4195 } catch (cleanupError) { 4196 console.warn('[NMW] Previous instance cleanup failed', cleanupError); 4197 } 4198 } 4199 4200 // Support both the rebuilt HTML and the original widget HTML. 4201 // Rebuilt HTML uses [data-nmw-render]; the original widget uses 4202 // .nmw-mobile-nav-wrapper as its render container. 4203 let renderHost = root.querySelector('[data-nmw-render]'); 4204 if (!renderHost) renderHost = root.querySelector('.nmw-mobile-nav-wrapper'); 4205 if (!renderHost) { 4206 renderHost = document.createElement('div'); 4207 renderHost.className = 'nmw-render'; 4208 renderHost.setAttribute('data-nmw-render', ''); 4209 root.appendChild(renderHost); 4210 } 4211 4212 // Startup marker used to confirm that Duda executed the runOnReady callback. 4213 root.dataset.nmwScriptStarted = 'true'; 4214 4215 // Show an immediate status only when the HTML does not provide a separate 4216 // loading host. The supplied Full/feature HTML uses [data-nmw-loading-view]. 4217 const separateLoadingHost = root.querySelector('[data-nmw-loading-view]'); 4218 if (renderHost && !renderHost.innerHTML.trim() && !separateLoadingHost) { 4219 renderHost.innerHTML = '<div class="nmw-runtime-status" data-nmw-runtime-status>Loading navigationâ¦</div>'; 4220 } 4221 4222 const loadingHost = separateLoadingHost || root.querySelector('.nmw-skeleton-nav'); 4223 const toolsHost = root.querySelector('[data-nmw-tools]'); 4224 const cfg = data.config || {}; 4225 const widgetMode = readString(configValue('widgetMode'), 'full').toLowerCase() === 'mirror' ? 'mirror' : 'full'; 4226 const instanceId = createInstanceId(widgetMode); 4227 const HOVER_CLOSE_DELAY = 200; 4228 const REBUILD_DEBOUNCE = 200; 4229 const NATIVE_RETRY_INTERVAL = 500; 4230 const NATIVE_MAX_WAIT = 15000; 4231 const MAX_DEPTH = 2; 4232 4233 // Dynamic page path controls from the widget panel. 4234 // Example: childUrlPrefix = "services" resolves HVAC against /services/hvac. 4235 // Multiple aliases are supported so existing or AI-created panel fields work. 4236 const CATEGORY_URL_PREFIX = normalizeUrlPrefix( 4237 configValue('categoryUrlPrefix', 'categoryPrefix', 'categoryPagePrefix'), 4238 'category' 4239 ); 4240 const CHILD_URL_PREFIX = normalizeUrlPrefix( 4241 configValue('childUrlPrefix', 'childPrefix', 'childPagePrefix', 'coreUrlPrefix', 'corePrefix'), 4242 'services' 4243 ); 4244 4245 root.dataset.nmwMode = widgetMode; 4246 root.classList.toggle('nmw-mirror', widgetMode === 'mirror'); 4247 root.classList.toggle('nmw-vertical', widgetMode === 'mirror'); 4248 root.dataset.nmwCategoryPrefix = CATEGORY_URL_PREFIX; 4249 root.dataset.nmwChildPrefix = CHILD_URL_PREFIX; 4250 4251 // The shared namespace survives Duda widget rerenders so Mirror can keep using 4252 // the last valid navigation while Full reinitializes. 4253 window.NMW = window.NMW || {}; 4254 Object.assign(window.NMW, { 4255 loading: Boolean(window.NMW.loading), 4256 ready: Boolean(window.NMW.ready), 4257 version: Number(window.NMW.version) || 0, 4258 data: Array.isArray(window.NMW.data) ? window.NMW.data : null, 4259 error: Boolean(window.NMW.error), 4260 warnings: Array.isArray(window.NMW.warnings) ? window.NMW.warnings : [], 4261 ownerId: window.NMW.ownerId || null, 4262 mirrorOwnerId: window.NMW.mirrorOwnerId || null, 4263 fullConfig: window.NMW.fullConfig || null, 4264 fullConfigSignature: window.NMW.fullConfigSignature || '', 4265 presentationVersion: Number(window.NMW.presentationVersion) || 0, 4266 megaGroupsUsed: Array.isArray(window.NMW.megaGroupsUsed) ? window.NMW.megaGroupsUsed : [], 4267 megaGroupCount: Number(window.NMW.megaGroupCount) || 0,
4268 megaLabels: window.NMW.megaLabels || {}, 4269 slugWarnings: Array.isArray(window.NMW.slugWarnings) ? window.NMW.slugWarnings : [] 4270 }); 4271 4272 const local = { 4273 cleaned: false, 4274 listenersAttached: false, 4275 hoverTimers: new Map(), 4276 anchorTimers: new Set(), 4277 closeJobs: new WeakMap(), 4278 closeTimers: new Set(), 4279 rebuildTimer: null, 4280 rebuildRunning: false, 4281 pendingRebuild: false, 4282 nativeObserver: null, 4283 nativeFinderObserver: null, 4284 manualObserver: null, 4285 nativeRetryTimer: null, 4286 nativeRetryStarted: 0, 4287 historyCleanup: null, 4288 customDraft: null, 4289 latestBuildWarnings: [], 4290 slugValidationWarnings: [], 4291 eventCleanups: [], 4292 removalObserver: null 4293 }; 4294 4295 function listen(targetNode, eventName, handler, options) { 4296 if (!targetNode || typeof targetNode.addEventListener !== 'function') return; 4297 targetNode.addEventListener(eventName, handler, options); 4298 local.eventCleanups.push(() => { 4299 try { targetNode.removeEventListener(eventName, handler, options); } 4300 catch (error) {} 4301 }); 4302 } 4303 4304 // --------------------------------------------------------------------------- 4305 // Generic helpers 4306 // --------------------------------------------------------------------------- 4307 4308 function readString(value, fallback = '') { 4309 if (value == null) return fallback; 4310 return String(value).trim() || fallback; 4311 } 4312 4313 function readBoolean(value, fallback = false) { 4314 if (value == null || value === '') return fallback; 4315 if (typeof value === 'boolean') return value; 4316 const normalized = String(value).trim().toLowerCase(); 4317 if (['true', '1', 'yes', 'on'].includes(normalized)) return true; 4318 if (['false', '0', 'no', 'off'].includes(normalized)) return false; 4319 return fallback; 4320 } 4321 4322 function readNumber(value, fallback, min = -Infinity, max = Infinity) { 4323 const parsed = Number(value); 4324 if (!Number.isFinite(parsed)) return fallback; 4325 return Math.min(max, Math.max(min, parsed)); 4326 } 4327 4328 function escapeHtml(value) { 4329 return String(value == null ? '' : value) 4330 .replace(/&/g, '&') 4331 .replace(/</g, '<') 4332 .replace(/>/g, '>') 4333 .replace(/"/g, '"') 4334 .replace(/'/g, '''); 4335 } 4336 4337 function cssValue(value, fallback) {
4338 let candidate = value; 4339 if (candidate && typeof candidate === 'object') { 4340 candidate = candidate.value 4341 ?? candidate.css 4342 ?? candidate.color 4343 ?? candidate.hex 4344 ?? candidate.fontFamily 4345 ?? candidate.family 4346 ?? candidate.name 4347 ?? ''; 4348 } 4349 const result = readString(candidate, ''); 4350 return result || fallback; 4351 } 4352 4353 function cssLength(value, fallback) { 4354 const result = cssValue(value, ''); 4355 if (!result) return fallback; 4356 return /^-?(?:\d+|\d*\.\d+)$/.test(result) ? `${result}px` : result; 4357 } 4358 4359 // Duda font controls may return a plain string, a number, or a nested 4360 // typography object. Read the requested part without assuming one exact shape. 4361 function readFontPart(value, part, fallback) { 4362 const visited = new Set(); 4363 4364 function parse(candidate, depth = 0) { 4365 if (candidate == null || depth > 6) return undefined; 4366 4367 if (typeof candidate === 'string') { 4368 const text = candidate.trim(); 4369 if (!text) return undefined; 4370 4371 // Some Duda controls serialize their configuration as JSON text. 4372 if ((text.startsWith('{') && text.endsWith('}')) || (text.startsWith('[') && text.endsWith(']'))) { 4373 try { 4374 const parsed = JSON.parse(text); 4375 const nested = parse(parsed, depth + 1); 4376 if (nested !== undefined) return nested; 4377 } catch (error) {} 4378 } 4379 4380 if (part === 'size') { 4381 const directLength = text.match(/^-?(?:\d+|\d*\.\d+)(?:px|rem|em|%|vw|vh|pt)?$/i); 4382 if (directLength) return cssLength(text, fallback); 4383 const embeddedLength = text.match(/(?:^|\s)(\d+(?:\.\d+)?(?:px|rem|em|%|vw|vh|pt))(?:\s|\/|$)/i); 4384 return embeddedLength ? embeddedLength[1] : undefined; 4385 } 4386 4387 if (part === 'weight') { 4388 if (/^(?:normal|bold|bolder|lighter|[1-9]00)$/i.test(text)) return text; 4389 return undefined; 4390 } 4391 4392 // A family string should not be mistaken for a size or weight. 4393 if (/^-?(?:\d+|\d*\.\d+)(?:px|rem|em|%|vw|vh|pt)?$/i.test(text)) return undefined; 4394 if (/^(?:normal|bold|bolder|lighter|[1-9]00)$/i.test(text)) return undefined; 4395 return text; 4396 } 4397 4398 if (typeof candidate === 'number') { 4399 if (part === 'size') return `${candidate}px`; 4400 if (part === 'weight') return String(candidate); 4401 return undefined; 4402 } 4403 4404 if (typeof candidate !== 'object') return undefined; 4405 if (visited.has(candidate)) return undefined; 4406 visited.add(candidate); 4407 4408 const keyMap = { 4409 family: ['fontFamily', 'font-family', 'family', 'fontName', 'font', 'name'], 4410 size: ['fontSize', 'font-size', 'size', 'textSize', 'text-size'], 4411 weight: ['fontWeight', 'font-weight', 'weight'] 4412 }; 4413 4414 for (const key of keyMap[part] || []) { 4415 if (Object.prototype.hasOwnProperty.call(candidate, key)) { 4416 const found = parse(candidate[key], depth + 1); 4417 if (found !== undefined) return found; 4418 } 4419 } 4420 4421 // Duda controls may wrap the typography value inside one of these keys. 4422 for (const key of ['value', 'typography', 'style', 'styles', 'settings', 'fontSettings', 'data']) { 4423 if (Object.prototype.hasOwnProperty.call(candidate, key)) { 4424 const found = parse(candidate[key], depth + 1); 4425 if (found !== undefined) return found; 4426 } 4427 } 4428 4429 // Last-resort recursive scan for an unfamiliar wrapper object. 4430 for (const child of Object.values(candidate)) { 4431 if (child && typeof child === 'object') { 4432 const found = parse(child, depth + 1); 4433 if (found !== undefined) return found; 4434 } 4435 } 4436 4437 return undefined; 4438 } 4439 4440 const result = parse(value); 4441 return result === undefined || result === '' ? fallback : result; 4442 } 4443 4444 function fontFamilyValue(value, fallback = 'inherit') { 4445 return readFontPart(value, 'family', fallback); 4446 } 4447 4448 function fontSizeValue(value, fallback = '16px') { 4449 return readFontPart(value, 'size', fallback); 4450 } 4451
4452 function fontWeightValue(value, fallback = '400') { 4453 return readFontPart(value, 'weight', fallback); 4454 } 4455 4456 function normalizeChevronStyle(value, fallback = 'filled') { 4457 const normalized = readString(value, fallback).toLowerCase(); 4458 if (['none', 'off', 'hidden'].includes(normalized)) return 'none'; 4459 if (['chevron', 'outline'].includes(normalized)) return 'chevron'; 4460 if (normalized === 'caret') return 'caret'; 4461 if (normalized === 'triangle') return 'triangle'; 4462 return 'filled'; 4463 } 4464 4465 function createInstanceId(mode) { 4466 return `nmw-${mode}-${Math.random().toString(36).slice(2, 9)}`; 4467 } 4468 4469 function field(row, ...names) { 4470 if (!row || typeof row !== 'object') return ''; 4471 for (const name of names) { 4472 if (Object.prototype.hasOwnProperty.call(row, name)) return row[name]; 4473 const key = Object.keys(row).find(item => item.toLowerCase() === String(name).toLowerCase()); 4474 if (key) return row[key]; 4475 } 4476 return ''; 4477 } 4478 4479 // Read a widget configuration value by key, including values placed inside 4480 // Duda panel groups. This makes fields such as childUrlPrefix work whether the 4481 // panel stores them directly on data.config or inside a grouped object. 4482 function configValue(...names) { 4483 const wanted = names.map(name => String(name).toLowerCase()); 4484 const visited = new Set(); 4485 4486 function search(value, depth) { 4487 if (!value || typeof value !== 'object' || Array.isArray(value) || depth > 6 || visited.has(value)) { 4488 return undefined; 4489 } 4490 visited.add(value); 4491 4492 for (const key of Object.keys(value)) { 4493 if (wanted.includes(key.toLowerCase()) && value[key] !== undefined) { 4494 return value[key]; 4495 } 4496 } 4497 4498 for (const key of Object.keys(value)) { 4499 const found = search(value[key], depth + 1); 4500 if (found !== undefined) return found; 4501 } 4502 4503 return undefined; 4504 } 4505 4506 return search(cfg, 0); 4507 } 4508 4509 4510 function configValueFrom(source, ...names) { 4511 const wanted = names.map(name => String(name).toLowerCase()); 4512 const visited = new Set(); 4513 4514 function search(value, depth) { 4515 if (!value || typeof value !== 'object' || depth > 8 || visited.has(value)) return undefined; 4516 visited.add(value); 4517 4518 if (!Array.isArray(value)) { 4519 for (const key of Object.keys(value)) { 4520 if (wanted.includes(key.toLowerCase()) && value[key] !== undefined) return value[key]; 4521 } 4522 } 4523 4524 for (const child of Object.values(value)) { 4525 if (child && typeof child === 'object') { 4526 const found = search(child, depth + 1); 4527 if (found !== undefined) return found; 4528 } 4529 } 4530 return undefined; 4531 } 4532 4533 return search(source, 0); 4534 } 4535
4536 // Search a configuration object while refusing to descend into named groups. 4537 // This is important for Duda Design Panels because Full and Mirror can contain 4538 // identical child keys (for example universalFontSize). Without excluding 4539 // mirrorStyles, the Full widget may accidentally read the hidden Mirror default. 4540 function configValueFromExcluding(source, excludedNames, ...names) { 4541 const wanted = names.map(name => String(name).toLowerCase()); 4542 const excluded = new Set((excludedNames || []).map(name => String(name).toLowerCase())); 4543 const visited = new Set(); 4544 4545 function search(value, depth) { 4546 if (!value || typeof value !== 'object' || depth > 8 || visited.has(value)) return undefined; 4547 visited.add(value); 4548 4549 if (!Array.isArray(value)) { 4550 for (const key of Object.keys(value)) { 4551 const lower = key.toLowerCase(); 4552 if (excluded.has(lower)) continue; 4553 if (wanted.includes(lower) && value[key] !== undefined) return value[key]; 4554 } 4555 } 4556 4557 for (const [key, child] of Object.entries(value)) { 4558 if (excluded.has(String(key).toLowerCase())) continue; 4559 if (child && typeof child === 'object') { 4560 const found = search(child, depth + 1); 4561 if (found !== undefined) return found; 4562 } 4563 } 4564 return undefined; 4565 } 4566 4567 return search(source, 0); 4568 } 4569 4570 function cloneSerializable(value) { 4571 try { return JSON.parse(JSON.stringify(value)); } 4572 catch (error) { return value; } 4573 } 4574 4575 function fullConfigRoot() { 4576 return widgetMode === 'mirror' && window.NMW.fullConfig ? window.NMW.fullConfig : cfg; 4577 } 4578 4579 function fullConfigValue(...names) { 4580 const found = configValueFrom(fullConfigRoot(), ...names); 4581 return found; 4582 } 4583 4584 function mirrorUsesSeparateStyling() { 4585 return readBoolean(configValue('useSeparateMirrorStyling'), false); 4586 } 4587 4588 function designConfigRoot(mode) { 4589 if (mode === 'mirror' && mirrorUsesSeparateStyling()) { 4590 // Search only inside the dedicated Mirror group when separate styling is on. 4591 return configValueFrom(cfg, 'mirrorStyles') || {}; 4592 } 4593 4594 // Full styling, and Mirror inheritance, must never descend into mirrorStyles. 4595 const base = mode === 'mirror' ? fullConfigRoot() : cfg; 4596 const fullStyles = configValueFromExcluding(base, ['mirrorStyles'], 'fullStyles'); 4597 return fullStyles && typeof fullStyles === 'object' ? fullStyles : base; 4598 } 4599 4600 function designValue(mode, names, fallback) { 4601 const list = Array.isArray(names) ? names : [names]; 4602 4603 if (mode === 'mirror' && mirrorUsesSeparateStyling()) { 4604 const mirrorRoot = designConfigRoot(mode); 4605 const mirrorFound = configValueFrom(mirrorRoot, ...list); 4606 return mirrorFound === undefined ? fallback : mirrorFound; 4607 } 4608 4609 // Exclude mirrorStyles so identically named Mirror fields cannot shadow the 4610 // active Full Design Panel values. This applies to Full and inherited Mirror. 4611 const base = mode === 'mirror' ? fullConfigRoot() : cfg; 4612 const designRoot = designConfigRoot(mode); 4613 let found = configValueFromExcluding(designRoot, ['mirrorStyles'], ...list); 4614 if (found === undefined && designRoot !== base) { 4615 found = configValueFromExcluding(base, ['mirrorStyles'], ...list); 4616 } 4617 return found === undefined ? fallback : found; 4618 } 4619 4620 // URL prefixes are entered in the Duda panel as simple values such as 4621 // "category" or "services". Leading/trailing slashes are removed, but an 4622 // intentionally blank prefix is allowed and resolves pages from the site root. 4623 function normalizeUrlPrefix(value, fallback) { 4624 const supplied = value === undefined || value === null ? fallback : String(value).trim(); 4625 return supplied.replace(/^\/+|\/+$/g, ''); 4626 } 4627 4628 function unique(values) { 4629 return [...new Set(values.filter(Boolean))]; 4630 } 4631 4632 // Display labels preserve their original punctuation. Slug candidates are used 4633 // only for collection matching and stable keys. 4634 function createSlugCandidates(value) { 4635 const original = readString(value, '').toLowerCase().replace(/\//g, '-'); 4636 const withAnd = original 4637 .replace(/\s*&\s*/g, '-and-') 4638 .replace(/[^a-z0-9\s-]/g, '') 4639 .replace(/\s+/g, '-') 4640 .replace(/-+/g, '-') 4641 .replace(/^-+|-+$/g, ''); 4642 const withoutAnd = original 4643 .replace(/\s*&\s*/g, '-') 4644 .replace(/[^a-z0-9\s-]/g, '') 4645 .replace(/\s+/g, '-') 4646 .replace(/-+/g, '-') 4647 .replace(/^-+|-+$/g, ''); 4648 return unique([withAnd, withoutAnd]); 4649 } 4650 4651 function slugify(value) { 4652 return createSlugCandidates(value)[0] || ''; 4653 } 4654 4655 function canonicalPath(value) { 4656 const raw = readString(value, ''); 4657 if (!raw) return ''; 4658 if (raw.startsWith('#')) return `${canonicalPathWithoutHash(window.location.href)}${raw}`; 4659 try { 4660 const url = new URL(raw, window.location.origin); 4661 let path = url.pathname || '/'; 4662 path = path.replace(/^\/(?:home\/)?site\/[^/]+/i, '') || '/'; 4663 path = path.replace(/\/+$/, '') || '/'; 4664 return `${path.toLowerCase()}${url.hash || ''}`; 4665 } catch (error) { 4666 const [pathPart, hashPart = ''] = raw.split('#'); 4667 let path = pathPart.split('?')[0].replace(/^\/(?:home\/)?site\/[^/]+/i, '').replace(/\/+$/, '') || '/'; 4668 return `${path.toLowerCase()}${hashPart ? `#${hashPart}` : ''}`; 4669 } 4670 } 4671 4672 function canonicalPathWithoutHash(value) { 4673 return canonicalPath(value).split('#')[0] || '/'; 4674 } 4675 4676 function getDudaBasePath() { 4677 try { 4678 const match = (window.location.pathname || '').match(/^\/site\/([0-9a-f]{32})(?:\/.*)?$/i); 4679 return match ? `/site/${match[1]}` : ''; 4680 } catch (error) { 4681 return ''; 4682 } 4683 } 4684 4685 function getDudaPreviewQuery() { 4686 try { 4687 const input = new URLSearchParams(window.location.search || ''); 4688 const output = new URLSearchParams(); 4689 ['showOriginal', 'preview', 'insitepreview', 'dm_device', 't', 'dm_try_mode', 'dm_checkSync'].forEach(key => { 4690 if (input.has(key)) output.set(key, input.get(key)); 4691 }); 4692 const value = output.toString(); 4693 return value ? `?${value}` : ''; 4694 } catch (error) { 4695 return ''; 4696 } 4697 } 4698 4699 function buildRuntimeFallbackUrl(pathValue) { 4700 const raw = readString(pathValue, '/') || '/'; 4701 if (/^(https?:)?\/\//i.test(raw) || raw.startsWith('#')) return raw; 4702 const hashIndex = raw.indexOf('#'); 4703 const hash = hashIndex >= 0 ? raw.slice(hashIndex) : ''; 4704 const withoutHash = hashIndex >= 0 ? raw.slice(0, hashIndex) : raw; 4705 const queryIndex = withoutHash.indexOf('?'); 4706 const existingQuery = queryIndex >= 0 ? withoutHash.slice(queryIndex) : ''; 4707 const pathOnly = queryIndex >= 0 ? withoutHash.slice(0, queryIndex) : withoutHash; 4708 const normalized = pathOnly.startsWith('/') ? pathOnly : `/${pathOnly}`; 4709 const basePath = getDudaBasePath(); 4710 const prefix = basePath && !normalized.startsWith(basePath + '/') && normalized !== basePath ? basePath
4710: ''; 4711 const previewQuery = existingQuery ? '' : getDudaPreviewQuery(); 4712 return `${prefix}${normalized}${existingQuery || previewQuery}${hash}`; 4713 } 4714 4715 function getLastPathSegment(value) { 4716 const path = canonicalPathWithoutHash(value); 4717 return path.split('/').filter(Boolean).pop() || ''; 4718 } 4719 4720 function isExternalUrl(value) { 4721 const raw = readString(value, ''); 4722 if (!raw || raw.startsWith('/') || raw.startsWith('#')) return false; 4723 try { 4724 return new URL(raw, window.location.origin).origin !== window.location.origin; 4725 } catch (error) { 4726 return false; 4727 } 4728 } 4729 4730 function stableStringify(value) { 4731 return JSON.stringify(value); 4732 } 4733 4734 function warn(code, message, context) { 4735 const warning = { code, message, context: context || null }; 4736 local.latestBuildWarnings.push(warning); 4737 console.warn(`[NMW:${code}] ${message}`, context || ''); 4738 } 4739 4740 // --------------------------------------------------------------------------- 4741 // Editor, preview, and Site Comments environment handling 4742 // --------------------------------------------------------------------------- 4743 4744 function isEditorCanvas() { 4745 try { 4746 const topHref = window.top && window.top.location ? window.top.location.href : ''; 4747 return topHref.includes('/home/site/') && !topHref.includes('#preview'); 4748 } catch (error) { 4749 return false; 4750 } 4751 } 4752 4753 /* 4754 Site Comments customer state may be written into the parent editor 4755 document after this widget has already rendered. Inspect the current 4756 document and every same-origin ancestor document each time the state 4757 is requested. 4758 4759 Do not depend on console output such as "Customer Detected". Console 4760 messages are not a reliable application-state API and may be emitted 4761 before this widget initializes. 4762 */ 4763 function isSiteCommentsCustomerMode() { 4764 try { 4765 const documents = []; 4766 const seenDocuments = new Set(); 4767 4768 function addDocument(candidateDocument) { 4769 if ( 4770 !candidateDocument || 4771 !candidateDocument.documentElement || 4772 seenDocuments.has(candidateDocument) 4773 ) { 4774 return; 4775 } 4776 4777 seenDocuments.add(candidateDocument); 4778 documents.push(candidateDocument); 4779 } 4780 4781 addDocument(document); 4782 4783 /* 4784 Walk upward only while the parent documents are same-origin. 4785 Cross-origin access is safely ignored. 4786 */ 4787 try { 4788 let currentWindow = window; 4789 4790 while ( 4791 currentWindow.parent && 4792 currentWindow.parent !== currentWindow 4793 ) { 4794 currentWindow = currentWindow.parent; 4795 addDocument(currentWindow.document); 4796 } 4797 } catch (error) { 4798 // A cross-origin ancestor cannot be inspected. 4799 } 4800 4801 const html = documents 4802 .map(candidateDocument => { 4803 try { 4804 return ( 4805 candidateDocument.documentElement.innerHTML || 4806 '' 4807 ); 4808 } catch (error) { 4809 return ''; 4810 } 4811 }) 4812 .join('\n'); 4813 4814 const hasSiteCommentsMarker = 4815 /SITE[_\s-]?COMMENTS/i.test(html); 4816 4817 const isCustomer = 4818 /["']?isCustomer["']?\s*:\s*true/i 4819 .test(html); 4820 4821 const isNonStaff = 4822 /["']?isStaffMember["']?\s*:\s*false/i 4823 .test(html); 4824 4825 const isStaff = 4826 /["']?isStaffMember["']?\s*:\s*true/i 4827 .test(html); 4828 4829 return ( 4830 hasSiteCommentsMarker && 4831 isCustomer && 4832 isNonStaff && 4833 !isStaff 4834 ); 4835 } catch (error) { 4836 return false; 4837 } 4838 } 4839 4840 function navigateSiteComments(link, event) { 4841 if (!isSiteCommentsCustomerMode()) return false; 4842 const href = link.getAttribute('href') || ''; 4843 const target = (link.getAttribute('target') || '_self').toLowerCase(); 4844 if (!href || href.startsWith('#') || target === '_blank' || /^(?:mailto:|tel:|sms:|javascript:|data:|blob:)/i.test(href)) return false; 4845 try { 4846 const parsed = new URL(href, window.location.origin); 4847 let topOrigin = window.location.origin; 4848 let topPath = '';
4849 try { 4850 topOrigin = window.top.location.origin || topOrigin; 4851 topPath = window.top.location.pathname || ''; 4852 } catch (error) {} 4853 // External web addresses must retain their original domain. Only rewrite 4854 // same-site links that need to escape the Site Comments preview iframe. 4855 if (parsed.origin !== window.location.origin && parsed.origin !== topOrigin) return false; 4856 const needsHomePrefix = parsed.pathname.startsWith('/site/') && topPath.startsWith('/home/site/'); 4857 const destination = `${topOrigin}${needsHomePrefix ? '/home' : ''}${parsed.pathname}${parsed.hash || ''}`; 4858 event.preventDefault(); 4859 event.stopPropagation(); 4860 try { 4861 window.top.location.href = destination; 4862 } catch (error) { 4863 window.location.href = destination; 4864 } 4865 return true; 4866 } catch (error) { 4867 return false; 4868 } 4869 } 4870 4871 // --------------------------------------------------------------------------- 4872 // Same-page anchor navigation 4873 // --------------------------------------------------------------------------- 4874 4875 /* 4876 Duda's native navigation can contain links to anchors on the current page. 4877 When the hash changes for the first time, normal browser navigation usually 4878 scrolls correctly. Once the visitor is already on that page, however, Duda's 4879 copied/native link behavior is not guaranteed to re-scroll for a second hash 4880 selection (or for the same hash selected again). 4881 4882 NMW therefore owns same-page anchor clicks. Cross-page anchor links continue 4883 to use normal navigation so Duda can load the destination page first. 4884 */ 4885 function decodeAnchorHash(hash) { 4886 const raw = readString(hash, '').replace(/^#/, ''); 4887 if (!raw) return ''; 4888 try { 4889 return decodeURIComponent(raw); 4890 } catch (error) { 4891 return raw; 4892 } 4893 } 4894 4895 function findAnchorTarget(hash) { 4896 const decoded = decodeAnchorHash(hash); 4897 if (!decoded) return null; 4898 4899 // IDs are the modern/default anchor mechanism. Check the decoded value first 4900 // and the literal hash value second in case an authored ID contains % signs. 4901 let target = document.getElementById(decoded); 4902 if (!target) { 4903 const literal = readString(hash, '').replace(/^#/, ''); 4904 if (literal && literal !== decoded) target = document.getElementById(literal); 4905 } 4906 4907 // Legacy named anchors are still supported by browsers and may exist on 4908 // older Duda sites. 4909 if (!target) { 4910 const named = document.getElementsByName(decoded); 4911 if (named && named.length) target = named[0]; 4912 } 4913 4914 return target || null; 4915 } 4916 4917 function collectTopOverlayCandidates() { 4918 const candidates = new Set(); 4919 4920 // Common Duda/site header selectors plus generic semantic header selectors. 4921 document.querySelectorAll([ 4922 'header', 4923 '[role="banner"]', 4924 '.dmHeader', 4925 '.dmHeaderContainer', 4926 '[data-element-type="header"]', 4927 '[class*="sticky"]', 4928 '[class*="fixed"]' 4929 ].join(',')) 4930 .forEach(node => candidates.add(node)); 4931 4932 // Also inspect what is physically covering the top-center of the viewport. 4933 // This catches custom sticky/fixed wrappers whose class names are unknown. 4934 try { 4935 const viewportWidth = document.documentElement.clientWidth || window.innerWidth || 0; 4936 const x = Math.max(1, Math.min(viewportWidth - 1, Math.round(viewportWidth / 2))); 4937 [1, 8, 16, 32].forEach(y => { 4938 const stack = typeof document.elementsFromPoint === 'function' 4939 ? document.elementsFromPoint(x, y) 4940 : []; 4941 4942 stack.forEach(node => { 4943 let current = node; 4944 while (current && current !== document.documentElement) { 4945 candidates.add(current); 4946 current = current.parentElement; 4947 } 4948 }); 4949 }); 4950 } catch (error) {} 4951 4952 return candidates; 4953 } 4954 4955 function getAnchorScrollOffset(target) { 4956 const viewportWidth = document.documentElement.clientWidth || window.innerWidth || 0; 4957 const viewportHeight = document.documentElement.clientHeight || window.innerHeight || 0; 4958 let overlayBottom = 0; 4959
4960 collectTopOverlayCandidates().forEach(node => { 4961 if (!node || node === target || node.contains?.(target)) return; 4962 4963 let style; 4964 let rect; 4965 try { 4966 style = window.getComputedStyle(node); 4967 rect = node.getBoundingClientRect(); 4968 } catch (error) { 4969 return; 4970 } 4971 4972 if (!style || !rect) return; 4973 if (!['fixed', 'sticky'].includes(style.position)) return; 4974 if (style.display === 'none' || style.visibility === 'hidden') return; 4975 if (Number(style.opacity) === 0) return; 4976 if (rect.height <= 0 || rect.width <= 0) return; 4977 if (rect.bottom <= 0 || rect.top > 12) return; 4978 4979 // Ignore small floating controls at the top edge; a navigation/header 4980 // overlay should span a meaningful portion of the viewport. 4981 if (viewportWidth > 0 && rect.width < viewportWidth * 0.30) return; 4982 4983 // Ignore pathological full-screen overlays. Those are not header offsets. 4984 if (viewportHeight > 0 && rect.height > viewportHeight * 0.60) return; 4985 4986 overlayBottom = Math.max(overlayBottom, Math.max(0, rect.bottom)); 4987 }); 4988 4989 let scrollMarginTop = 0; 4990 try { 4991 const targetStyle = window.getComputedStyle(target); 4992 scrollMarginTop = parseFloat(targetStyle.scrollMarginTop) || 0; 4993 } catch (error) {} 4994 4995 // Respect an authored scroll-margin if it is larger than the detected header. 4996 // Add a small breathing room so headings do not sit flush against the header. 4997 return Math.max(0, Math.max(overlayBottom, scrollMarginTop) + 8); 4998 } 4999 5000 function scrollAnchorTargetIntoPlace(target) { 5001 if (!target || !document.documentElement.contains(target)) return false; 5002 5003 const rect = target.getBoundingClientRect(); 5004 const currentScroll = window.pageYOffset 5005 || document.documentElement.scrollTop 5006 || document.body.scrollTop 5007 || 0; 5008 const offset = getAnchorScrollOffset(target); 5009 const absoluteTargetTop = currentScroll + rect.top; 5010 const scrollingElement = document.scrollingElement || document.documentElement; 5011 const maxScroll = Math.max(0, scrollingElement.scrollHeight - window.innerHeight); 5012 const desiredTop = Math.max(0, Math.min(maxScroll, absoluteTargetTop - offset)); 5013 5014 try { 5015 window.scrollTo({ top: desiredTop, left: window.pageXOffset || 0, behavior: 'auto' }); 5016 } catch (error) { 5017 window.scrollTo(window.pageXOffset || 0, desiredTop); 5018 } 5019 5020 return true; 5021 } 5022 5023 function scheduleAnchorCorrection(target) { 5024 // First pass occurs immediately after the click. A second pass happens after 5025 // menu close animations/layout changes so Mirror/mobile menus cannot shift the 5026 // target after it was measured. A final lightweight pass handles late sticky 5027 // header changes that appear only after scrolling. 5028 const delays = [0, 260, 520]; 5029 5030 delays.forEach(delay => { 5031 const timer = setTimeout(() => { 5032 local.anchorTimers.delete(timer); 5033 if (local.cleaned || !document.documentElement.contains(target)) return; 5034 scrollAnchorTargetIntoPlace(target); 5035 }, delay); 5036 5037 local.anchorTimers.add(timer); 5038 }); 5039 } 5040 5041 function samePageAnchorInfo(link) { 5042 if (!link) return null; 5043 5044 const href = readString(link.getAttribute('href'), ''); 5045 const target = readString(link.getAttribute('target'), '_self').toLowerCase(); 5046 5047 if ( 5048 !href || 5049 target === '_blank' || 5050 link.hasAttribute('download') || 5051 /^(?:mailto:|tel:|sms:|javascript:|data:|blob:)/i.test(href) 5052 ) { 5053 return null; 5054 } 5055 5056 let destination; 5057 try { 5058 destination = new URL(href, window.location.href); 5059 } catch (error) { 5060 return null; 5061 } 5062 5063 if (destination.origin !== window.location.origin || !destination.hash) return null; 5064 5065 const destinationPath = canonicalPathWithoutHash(destination.href); 5066 const currentPath = canonicalPathWithoutHash(window.location.href); 5067 if (!destinationPath || destinationPath !== currentPath) return null; 5068 5069 const anchorTarget = findAnchorTarget(destination.hash); 5070 if (!anchorTarget) return null; 5071 5072 return { 5073 destination, 5074 hash: destination.hash, 5075 target: anchorTarget 5076 }; 5077 } 5078 5079 function handleSamePageAnchorClick(link, event) { 5080 // Preserve normal browser behavior for modified clicks such as Ctrl/Cmd-click. 5081 if ( 5082 event && 5083 (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || (typeof event.button === 'number' && event.button !== 0)) 5084 ) { 5085 return false;
5086 } 5087 5088 const info = samePageAnchorInfo(link); 5089 if (!info) return false; 5090 5091 event.preventDefault(); 5092 event.stopPropagation(); 5093 5094 closeAllMenus(root, false); 5095 5096 // pushState changes the URL without triggering the browser's unreliable 5097 // one-time hash scrolling. Selecting the same anchor again still scrolls 5098 // because the explicit scroll routine below always runs. 5099 try { 5100 if (window.location.hash !== info.hash) { 5101 history.pushState(history.state, '', info.destination.href); 5102 } else { 5103 refreshActiveStates(root); 5104 } 5105 } catch (error) { 5106 // If history manipulation is unavailable, keep the current URL and still 5107 // perform the requested scroll. 5108 refreshActiveStates(root); 5109 } 5110 5111 scheduleAnchorCorrection(info.target); 5112 return true; 5113 } 5114 5115 // --------------------------------------------------------------------------- 5116 // Configuration model 5117 // --------------------------------------------------------------------------- 5118 5119 function getModeConfig(mode) { 5120 return designConfigRoot(mode); 5121 } 5122 5123 function behaviorValue(mode, names, fallback) { 5124 const list = Array.isArray(names) ? names : [names]; 5125 const fullFound = configValueFrom(fullConfigRoot(), ...list); 5126 5127 if (mode === 'mirror' && mirrorUsesSeparateStyling()) { 5128 const mirrorFound = configValueFrom(designConfigRoot('mirror'), ...list); 5129 if (mirrorFound !== undefined) return mirrorFound; 5130 } 5131 5132 return fullFound === undefined ? fallback : fullFound; 5133 } 5134 5135 function getBehaviorConfig(mode) { 5136 const fullHover = readBoolean(behaviorValue('full', 'openMenusOnHover', false), false); 5137 const mirrorOverride = mode === 'mirror' 5138 ? configValue('mirrorOpenMenusOnHover') 5139 : undefined; 5140 5141 return { 5142 hoverEnabled: mode === 'mirror' && mirrorOverride !== undefined && mirrorOverride !== null && mirrorOverride !== '' 5143 ? readBoolean(mirrorOverride, fullHover) 5144 : fullHover, 5145 animationsEnabled: readBoolean(behaviorValue(mode, ['animationsEnabled', 'animatedEffectsEnabled'], true), true), 5146 animateTopNav: readBoolean(behaviorValue(mode, 'animateTopNav', true), true), 5147 animateSubNav: readBoolean(behaviorValue(mode, 'animateSubNav', true), true), 5148 animateSubSubNav: readBoolean(behaviorValue(mode, 'animateSubSubNav', true), true), 5149 animateMegaMenu: readBoolean(behaviorValue(mode, 'animateMegaMenu', true), true), 5150 animateMegaFamilies: readBoolean(behaviorValue(mode, 'animateMegaFamilies', true), true), 5151 animateMirror: readBoolean(behaviorValue(mode, 'animateMirror', true), true), 5152 animateTopChevrons: readBoolean(behaviorValue(mode, 'animateTopChevrons', true), true), 5153 animateMegaChevrons: readBoolean(behaviorValue(mode, 'animateMegaChevrons', true), true), 5154 animateHoverEffects: readBoolean(behaviorValue(mode, 'animateHoverEffects', true), true) 5155 }; 5156 } 5157 5158 function getMegaGroupObject(groupNumber) { 5159 const safeGroup = Math.min(4, Math.max(1, Math.round(Number(groupNumber) || 1))); 5160 const key = `mega${safeGroup}`; 5161 const rootConfig = fullConfigRoot(); 5162 const direct = rootConfig && typeof rootConfig[key] === 'object' ? rootConfig[key] : null; 5163 return direct || configValueFrom(rootConfig, key) || {}; 5164 } 5165 5166 function getMegaLabel(groupNumber) { 5167 const safeGroup = Math.min(4, Math.max(1, Math.round(Number(groupNumber) || 1))); 5168 const key = `mega${safeGroup}`; 5169 const rootConfig = fullConfigRoot(); 5170 const group = getMegaGroupObject(safeGroup); 5171 const configured = group.label 5172 ?? configValueFrom(rootConfig, `${key}Label`, `megaLabel${safeGroup}`, `mega${safeGroup}Name`) 5173 ?? (safeGroup === 1 ? configValueFrom(rootConfig, 'servicesLabel') : undefined); 5174 5175 return readString(configured, `Mega ${safeGroup}`) || `Mega ${safeGroup}`; 5176 } 5177 5178 function getMegaExpandAll(groupNumber) { 5179 const safeGroup = Math.min(4, Math.max(1, Math.round(Number(groupNumber) || 1))); 5180 const key = `mega${safeGroup}`; 5181 const rootConfig = fullConfigRoot(); 5182 const group = getMegaGroupObject(safeGroup); 5183 const candidates = [ 5184 group.expandAll, 5185 group.childrenExpanded, 5186 group.childrenOpen, 5187 group.openChildren, 5188 configValueFrom(rootConfig, 5189 `${key}ExpandAll`, `${key}Expanded`, `${key}ChildrenExpanded`, 5190 `${key}ChildrenOpen`, `${key}OpenChildren`, 5191 `expandMega${safeGroup}`, `expandedMega${safeGroup}` 5192 ) 5193 ]; 5194 5195 if (safeGroup === 1) candidates.push(configValueFrom(rootConfig, 'expandedServices')); 5196 const configured = candidates.find(value => value !== undefined && value !== null && value !== ''); 5197 return readBoolean(configured, false); 5198 } 5199 5200 function getMegaConfig(groupNumber) { 5201 const safeGroup = Math.min(4, Math.max(1, Math.round(Number(groupNumber) || 1)));
5202 const key = `mega${safeGroup}`; 5203 const rootConfig = fullConfigRoot(); 5204 const group = getMegaGroupObject(safeGroup); 5205 5206 const columnsValue = group.columns ?? group.columnCount ?? configValueFrom( 5207 rootConfig, 5208 `${key}Columns`, `${key}ColumnCount`, `megaColumns${safeGroup}`, 5209 safeGroup === 1 ? 'compactColumnCount' : '__unused__', 5210 'megaColumns' 5211 ); 5212 const layoutValue = group.layout ?? group.layoutMode ?? configValueFrom( 5213 rootConfig, 5214 `${key}Layout`, `${key}LayoutMode`, `megaLayout${safeGroup}`, 'megaLayoutMode' 5215 ); 5216 const maxWidthValue = group.maxWidth ?? group.menuMaxWidth ?? configValueFrom( 5217 rootConfig, 5218 `${key}MaxWidth`, `${key}MenuMaxWidth`, `megaMaxWidth${safeGroup}`, 'megaMenuMaxWidth' 5219 ); 5220 const alphabetizeValue = group.alphabetize ?? configValueFrom( 5221 rootConfig, 5222 `${key}Alphabetize`, `alphabetizeMega${safeGroup}`, 'alphabetizeMega' 5223 ); 5224 const layout = readString(layoutValue, 'grid').toLowerCase(); 5225 5226 return { 5227 label: getMegaLabel(safeGroup), 5228 columns: Math.round(readNumber(columnsValue, 3, 1, 6)), 5229 layout: ['grid', 'masonry'].includes(layout) ? layout : 'grid', 5230 expandAll: getMegaExpandAll(safeGroup), 5231 maxWidth: readNumber(maxWidthValue, 1600, 320, 3000), 5232 alphabetize: readBoolean(alphabetizeValue, false) 5233 }; 5234 } 5235 5236 function updateMegaIndicators(items) { 5237 const groups = new Set(); 5238 5239 function walk(list) { 5240 (list || []).forEach(item => { 5241 if (item && item.megaGroup >= 1 && item.megaGroup <= 4) groups.add(item.megaGroup); 5242 walk(item && item.children); 5243 }); 5244 } 5245 5246 walk(items); 5247 const used = [...groups].sort((a, b) => a - b); 5248 const labels = Object.fromEntries([1, 2, 3, 4].map(number => [number, getMegaLabel(number)])); 5249 5250 window.NMW.megaGroupsUsed = used; 5251 window.NMW.megaGroupCount = used.length; 5252 window.NMW.megaLabels = labels; 5253 5254 root.dataset.nmwMegaCount = String(used.length); 5255 root.dataset.nmwMegaGroups = used.map(number => `mega${number}`).join(','); 5256 root.dataset.nmwMegaLabels = used.map(number => labels[number]).join(','); 5257 5258 [1, 2, 3, 4].forEach(number => { 5259 root.dataset[`nmwMega${number}Label`] = labels[number]; 5260 }); 5261 } 5262 5263 function applyDesignSettings(mode) { 5264 const behavior = getBehaviorConfig(mode); 5265 const universalColors = readBoolean(designValue(mode, 'useUniversalColors', false), false); 5266 // A separate background toggle is required because Duda color controls often 5267 // save individual default values (for example #ffffff). Without this switch, 5268 // those saved defaults always override universalBackgroundColor. 5269 const universalBackgrounds = readBoolean( 5270 designValue(mode, ['useUniversalBackgrounds', 'useUniversalBackground'], true), 5271 true 5272 ); 5273 const universalTypography = readBoolean(designValue(mode, 'useUniversalTypography', true), true); 5274 const universalChevrons = readBoolean(designValue(mode, 'useUniversalChevronSettings', false), false); 5275 5276 root.dataset.nmwUniversalColors = universalColors ? 'true' : 'false'; 5277 root.dataset.nmwUniversalBackgrounds = universalBackgrounds ? 'true' : 'false'; 5278 root.dataset.nmwUniversalTypography = universalTypography ? 'true' : 'false'; 5279 root.dataset.nmwUniversalChevrons = universalChevrons ? 'true' : 'false'; 5280 root.dataset.nmwAnimations = behavior.animationsEnabled ? 'on' : 'off'; 5281 root.dataset.nmwAnimateTop = behavior.animateTopNav ? 'on' : 'off'; 5282 root.dataset.nmwAnimateSub = behavior.animateSubNav ? 'on' : 'off'; 5283 root.dataset.nmwAnimateSubsub = behavior.animateSubSubNav ? 'on' : 'off'; 5284 root.dataset.nmwAnimateMega = behavior.animateMegaMenu ? 'on' : 'off'; 5285 root.dataset.nmwAnimateMegaFamilies = behavior.animateMegaFamilies ? 'on' : 'off'; 5286 root.dataset.nmwAnimateMirror = behavior.animateMirror ? 'on' : 'off'; 5287 root.dataset.nmwAnimateTopChevrons = behavior.animateTopChevrons ? 'on' : 'off'; 5288 root.dataset.nmwAnimateMegaChevrons = behavior.animateMegaChevrons ? 'on' : 'off'; 5289 root.dataset.nmwAnimateHover = behavior.animateHoverEffects ? 'on' : 'off';
5290 root.dataset.nmwHoverEnabled = behavior.hoverEnabled ? 'true' : 'false'; 5291 root.dataset.nmwAlignment = mode === 'mirror' 5292 ? readString(designValue(mode, 'mirrorAlignment', 'start'), 'start') 5293 : readString(designValue(mode, 'navAlignment', 'center'), 'center'); 5294 root.dataset.nmwPreset = readString( 5295 designValue( 5296 mode, 5297 mode === 'mirror' 5298 ? ['layoutPreset', 'mirrorLayoutPreset', 'mirrorPreset'] 5299 : ['layoutPreset', 'desktopPresetValue', 'desktopPreset'], 5300 mode === 'mirror' ? 'vertical-standard' : 'classic' 5301 ), 5302 mode === 'mirror' ? 'vertical-standard' : 'classic' 5303 ).toLowerCase(); 5304 root.dataset.nmwSeparateMirrorStyling = mode === 'mirror' && mirrorUsesSeparateStyling() ? 'true' : 'false'; 5305 root.classList.toggle('nmw-capitalize', readBoolean(behaviorValue(mode, 'capitalizeNav', false), false)); 5306 root.dataset.nmwUniversalUnderline = readBoolean( 5307 designValue( 5308 mode, 5309 ['universalUnderline', 'universalAutomaticUnderline', 'underlineLinksOnHover'], 5310 false 5311 ), 5312 false 5313 ) ? 'true' : 'false'; 5314 5315 const universalChevronStyle = normalizeChevronStyle( 5316 designValue(mode, ['universalChevronStyle', 'universalChevronType', 'chevronStyle', 'chevronType'], 'filled'), 5317 'filled' 5318 ); 5319 const topChevronStyle = universalChevrons 5320 ? universalChevronStyle 5321 : normalizeChevronStyle(designValue(mode, ['topChevronStyle', 'topChevronType', 'chevronType'], universalChevronStyle), universalChevronStyle); 5322 const megaChevronStyle = universalChevrons 5323 ? universalChevronStyle 5324 : normalizeChevronStyle(designValue(mode, ['megaChevronStyle', 'megaChevronType'], universalChevronStyle), universalChevronStyle); 5325 root.dataset.nmwTopChevronStyle = topChevronStyle; 5326 root.dataset.nmwMegaChevronStyle = megaChevronStyle; 5327 5328 const universalText = cssValue(designValue(mode, 'universalTextColor', '#111111'), '#111111'); 5329 const universalBg = cssValue(designValue(mode, 'universalBackgroundColor', '#ffffff'), '#ffffff'); 5330 5331 // The universal background controls every dropdown surface, including 5332 // Mega family/cards. The top navigation background remains independent. 5333 const subBackground = universalBackgrounds 5334 ? universalBg 5335 : cssValue(designValue(mode, 'subNavBackgroundColor', '#ffffff'), '#ffffff'); 5336 const subSubBackground = universalBackgrounds 5337 ? universalBg 5338 : cssValue(designValue(mode, 'subSubNavBackgroundColor', '#ffffff'), '#ffffff'); 5339 const megaBackground = universalBackgrounds 5340 ? universalBg 5341 : cssValue(designValue(mode, 'megaMenuBackgroundColor', '#ffffff'), '#ffffff'); 5342 const megaFamilyBackground = universalBackgrounds 5343 ? universalBg 5344 : cssValue(designValue(mode, 'megaFamilyBackgroundColor', 'transparent'), 'transparent'); 5345 5346 const universalFontSource = designValue( 5347 mode, 5348 ['universalFontFamily', 'universalFont', 'universalTypographyFont'], 5349 'inherit' 5350 ); 5351 const universalSizeSource = designValue( 5352 mode, 5353 ['universalFontSize', 'universalTextSize', 'universalSize', 'universalFontFamily', 'universalFont'], 5354 '16px' 5355 ); 5356 const universalWeightSource = designValue( 5357 mode, 5358 ['universalFontWeight', 'universalWeight', 'universalFontFamily', 'universalFont'], 5359 '500' 5360 ); 5361 5362 const universalFont = fontFamilyValue(universalFontSource, 'inherit'); 5363 const universalSize = fontSizeValue(universalSizeSource, '16px'); 5364 const universalWeight = fontWeightValue(universalWeightSource, '500'); 5365 const universalChevronColor = cssValue(designValue(mode, 'universalChevronColor', universalText), universalText); 5366 const universalChevronSize = cssLength(designValue(mode, ['universalChevronSize', 'chevronSize'], '10px'), '10px'); 5367 const universalChevronWeight = cssLength(designValue(mode, ['universalChevronWeight', 'chevronWeight'], '1.5px'), '1.5px'); 5368 5369 const variables = { 5370 '--nmw-universal-text': universalText, 5371 '--nmw-top-text': universalColors ? universalText : cssValue(designValue(mode, 'topTextColor', universalText), universalText), 5372 '--nmw-sub-text': universalColors ? universalText : cssValue(designValue(mode, 'subTextColor', universalText), universalText), 5373 '--nmw-subsub-text': universalColors ? universalText : cssValue(designValue(mode, 'subSubTextColor', universalText), universalText), 5374 '--nmw-mega-parent-text': universalColors ? universalText : cssValue(designValue(mode, 'megaParentTextColor', universalText), universalText), 5375 '--nmw-mega-child-text': universalColors ? universalText : cssValue(designValue(mode, 'megaChildTextColor', universalText), universalText), 5376 '--nmw-mega-grandchild-text': universalColors ? universalText : cssValue(designValue(mode, 'megaGrandchildTextColor', universalText), universalText), 5377 '--nmw-top-bg': cssValue(designValue(mode, 'topNavBackgroun
5377dColor', 'transparent'), 'transparent'), 5378 '--nmw-universal-bg': universalBg, 5379 '--nmw-sub-bg': subBackground, 5380 '--nmw-subsub-bg': subSubBackground, 5381 '--nmw-mega-bg': megaBackground, 5382 '--nmw-mega-family-bg': megaFamilyBackground, 5383 '--nmw-hover-text': cssValue(designValue(mode, ['hoverTextColor', 'globalHoverTextColor'], 'currentColor'), 'currentColor'), 5384 '--nmw-hover-bg': cssValue(designValue(mode, ['hoverBackgroundColor', 'globalSubNavHoverBg'], 'transparent'), 'transparent'), 5385 '--nmw-active-text': cssValue(designValue(mode, 'activeTextColor', 'currentColor'), 'currentColor'), 5386 '--nmw-universal-font': universalFont, 5387 '--nmw-universal-size': universalSize, 5388 '--nmw-universal-weight': universalWeight, 5389 '--nmw-top-font': universalTypography ? universalFont : fontFamilyValue(designValue(mode, ['topFontFamily', 'topFont'], universalFont), universalFont), 5390 '--nmw-top-size': universalTypography ? universalSize : fontSizeValue(designValue(mode, ['topFontSize', 'topTextSize', 'topFontFamily', 'topFont'], universalSize), universalSize), 5391 '--nmw-top-weight': universalTypography ? universalWeight : fontWeightValue(designValue(mode, ['topFontWeight', 'topWeight', 'topFontFamily', 'topFont'], universalWeight), universalWeight), 5392 '--nmw-sub-font': universalTypography ? universalFont : fontFamilyValue(designValue(mode, ['subFontFamily', 'subFont'], universalFont), universalFont), 5393 '--nmw-sub-size': universalTypography ? universalSize : fontSizeValue(designValue(mode, ['subFontSize', 'subTextSize', 'subFontFamily', 'subFont'], universalSize), universalSize), 5394 '--nmw-sub-weight': universalTypography ? universalWeight : fontWeightValue(designValue(mode, ['subFontWeight', 'subWeight', 'subFontFamily', 'subFont'], universalWeight), universalWeight), 5395 '--nmw-subsub-font': universalTypography ? universalFont : fontFamilyValue(designValue(mode, ['subSubFontFamily', 'subSubFont'], universalFont), universalFont), 5396 '--nmw-subsub-size': universalTypography ? universalSize : fontSizeValue(designValue(mode, ['subSubFontSize', 'subSubTextSize', 'subSubFontFamily', 'subSubFont'], universalSize), universalSize), 5397 '--nmw-subsub-weight': universalTypography ? universalWeight : fontWeightValue(designValue(mode, ['subSubFontWeight', 'subSubWeight', 'subSubFontFamily', 'subSubFont'], universalWeight), universalWeight), 5398 '--nmw-mega-parent-font': universalTypography ? universalFont : fontFamilyValue(designValue(mode, ['megaParentFontFamily', 'megaParentFont'], universalFont), universalFont), 5399 '--nmw-mega-parent-size': universalTypography ? universalSize : fontSizeValue(designValue(mode, ['megaParentFontSize', 'megaParentTextSize', 'megaParentFontFamily', 'megaParentFont'], universalSize), universalSize), 5400 '--nmw-mega-parent-weight': universalTypography ? universalWeight : fontWeightValue(designValue(mode, ['megaParentFontWeight', 'megaParentWeight', 'megaParentFontFamily', 'megaParentFont'], universalWeight), universalWeight), 5401 '--nmw-mega-child-font': universalTypography ? universalFont : fontFamilyValue(designValue(mode, ['megaChildFontFamily', 'megaChildFont'], universalFont), universalFont), 5402 '--nmw-mega-child-size': universalTypography ? universalSize : fontSizeValue(designValue(mode, ['megaChildFontSize', 'megaChildTextSize', 'megaChildFontFamily', 'megaChildFont'], universalSize), universalSize), 5403 '--nmw-mega-child-weight': universalTypography ? universalWeight : fontWeightValue(designValue(mode, ['megaChildFontWeight', 'megaChildWeight', 'megaChildFontFamily', 'megaChildFont'], universalWeight), universalWeight), 5404 '--nmw-mega-grandchild-font': universalTypography ? universalFont : fontFamilyValue(designValue(mode, ['megaGrandchildFontFamily', 'megaGrandchildFont'], universalFont), universalFont), 5405 '--nmw-mega-grandchild-size': universalTypography ? universalSize : fontSizeValue(designValue(mode, ['megaGrandchildFontSize', 'megaGrandchildTextSize', 'megaGrandchildFontFamily', 'megaGrandchildFont'], universalSize), universalSize), 5406 '--nmw-mega-grandchild-weight': universalTypography ? universalWeight : fontWeightV
5406alue(designValue(mode, ['megaGrandchildFontWeight', 'megaGrandchildWeight', 'megaGrandchildFontFamily', 'megaGrandchildFont'], universalWeight), universalWeight), 5407 '--nmw-top-chevron-color': universalChevrons ? universalChevronColor : cssValue(designValue(mode, 'topChevronColor', universalChevronColor), universalChevronColor), 5408 '--nmw-mega-chevron-color': universalChevrons ? universalChevronColor : cssValue(designValue(mode, 'megaChevronColor', universalChevronColor), universalChevronColor), 5409 '--nmw-chevron-size': universalChevronSize, 5410 '--nmw-chevron-weight': universalChevronWeight, 5411 '--nmw-top-gap': `${readNumber(designValue(mode, 'topLevelGap', 32), 32, 0, 120)}px`, 5412 // Controls the spacing between a synthetic Mega 1â4 label and its chevron. 5413 // Duda Design Panel field: megaTriggerChevronGap 5414 '--nmw-mega-trigger-chevron-gap': `${readNumber(designValue(mode, 'megaTriggerChevronGap', 12), 12, 0, 60)}px`, 5415 '--nmw-mirror-gap': `${readNumber(designValue(mode, 'mirrorItemGap', 0), 0, 0, 80)}px` 5416 }; 5417 5418 Object.entries(variables).forEach(([name, value]) => root.style.setProperty(name, value)); 5419 } 5420 5421 // --------------------------------------------------------------------------- 5422 // Shared state and root status 5423 // --------------------------------------------------------------------------- 5424 5425 function updateRootStatus() { 5426 const state = window.NMW; 5427 root.dataset.nmwLoading = state.loading ? 'true' : 'false'; 5428 root.dataset.nmwReady = state.ready ? 'true' : 'false'; 5429 root.dataset.nmwError = state.error ? 'true' : 'false'; 5430 root.dataset.nmwEmpty = Array.isArray(state.data) && state.data.length === 0 ? 'true' : 'false'; 5431 updateLoadingView(); 5432 } 5433 5434 function dispatchStateChanged() { 5435 window.dispatchEvent(new CustomEvent('nmw:state-changed', { 5436 detail: { loading: window.NMW.loading, ready: window.NMW.ready, error: window.NMW.error } 5437 })); 5438 } 5439 5440 function dispatchDataChanged() { 5441 window.dispatchEvent(new CustomEvent('nmw:data-changed', { 5442 detail: { version: window.NMW.version } 5443 })); 5444 } 5445 5446 function updateLoadingView() { 5447 if (!loadingHost) return; 5448 const initialLoading = window.NMW.loading && !window.NMW.ready; 5449 loadingHost.hidden = !initialLoading; 5450 if (!initialLoading) { 5451 loadingHost.innerHTML = ''; 5452 return; 5453 } 5454 const style = readString(configValueFrom(fullConfigRoot(), 'loadingAnimationStyle') ?? 'shimmer', 'shimmer').toLowerCase(); 5455 const count = Math.round(readNumber(configValueFrom(fullConfigRoot(), 'loadingItemCount'), 4, 1, 12)); 5456 if (style === 'spinner') { 5457 loadingHost.innerHTML = '<span class="nmw-loading-spinner" aria-hidden="true"></span>'; 5458 return; 5459 } 5460 loadingHost.innerHTML = `<div class="nmw-loading-items nmw-loading-${escapeHtml(style)}">${Array.from({ length: count }, (_, index) => `<span class="nmw-loading-item" style="--nmw-load-width:${55 + ((index * 17) % 40)}px"></span>`).join('')}</div>`; 5461 } 5462 5463 function showEditorMessage(message, duplicate = false) { 5464 if (!renderHost) return; 5465 if (!isEditorCanvas()) { 5466 renderHost.innerHTML = ''; 5467 return; 5468 } 5469 renderHost.innerHTML = `<div class="nmw-editor-warning${duplicate ? ' nmw-editor-warning-duplicate' : ''}">${escapeHtml(message)}</div>`; 5470 } 5471 5472 // --------------------------------------------------------------------------- 5473 // Ownership: exactly one Full and one Mirror are expected on a page 5474 // --------------------------------------------------------------------------- 5475 5476 function claimOwnership() { 5477 const key = widgetMode === 'full' ? 'ownerId' : 'mirrorOwnerId'; 5478 const existingId = window.NMW[key]; 5479 5480 // Duda can rerun widget JavaScript against the same existing DOM root when 5481 // code or panel settings change. In that case the old instance ID remains 5482 // on this same root even though its previous script lifecycle is obsolete. 5483 // Treat that as a replacement, not as a duplicate widget. 5484 if (existingId && existingId !== instanceId) { 5485 const existingRoot = Array.from(document.querySelectorAll('[data-nm
5485w-instance]')) 5486 .find(node => node.dataset.nmwInstance === existingId); 5487 5488 if (existingRoot && existingRoot !== root && document.documentElement.contains(existingRoot)) { 5489 warn('DUPLICATE_INSTANCE', `A second ${widgetMode} widget was found. Only one is allowed.`); 5490 showEditorMessage(`Duplicate ${widgetMode} navigation widget. Only one instance is allowed.`, true); 5491 return false; 5492 } 5493 5494 // The recorded owner is stale or belongs to this same widget root. 5495 // Clear it so the newly executed script can initialize normally. 5496 window.NMW[key] = null; 5497 } 5498 5499 window.NMW[key] = instanceId; 5500 root.dataset.nmwInstance = instanceId; 5501 return true; 5502 } 5503 5504 function releaseOwnership() { 5505 const key = widgetMode === 'full' ? 'ownerId' : 'mirrorOwnerId'; 5506 if (window.NMW[key] === instanceId) window.NMW[key] = null; 5507 } 5508 5509 // --------------------------------------------------------------------------- 5510 // Duda collection fetching 5511 // --------------------------------------------------------------------------- 5512 5513 let collectionsApiPromise = null; 5514 5515 /* 5516 COLLECTION REQUEST CACHE 5517 5518 A single Full-widget lifecycle can rebuild several times because Duda changes 5519 native-nav attributes or the hidden manual-item source. Those rebuilds do not 5520 mean the underlying collections changed, so repeating the same collection API 5521 requests only adds latency. 5522 5523 Cache the request Promise per collection for this JavaScript instance. Duda 5524 reruns the widget JavaScript when its panel/configuration lifecycle is replaced, 5525 which naturally creates a fresh cache. Failed requests are removed immediately 5526 so a later rebuild can retry. The debug helper can explicitly force a refresh. 5527 */ 5528 const collectionRequestCache = new Map(); 5529 5530 // Load Duda's collection API once and reuse the same API object for every 5531 // collection request. The original working widget followed this pattern. 5532 function getCollectionsApi() { 5533 if (!collectionsApiPromise) { 5534 collectionsApiPromise = dmAPI.loadCollectionsAPI(); 5535 } 5536 return collectionsApiPromise; 5537 } 5538 5539 // Preserve both layers of a Duda collection result. Dynamic collection 5540 // identifiers such as item, slug, uuid, and page_item_url may live on the 5541 // outer entry while editable collection fields live inside entry.data. 5542 // Flattening only entry.data discards the slug metadata and causes every 5543 // Navigation Tree validation check to report a false missing-slug warning. 5544 function normalizeFetchedCollectionEntry(entry) { 5545 if (!entry || typeof entry !== 'object') return entry; 5546 const nestedData = entry.data; 5547 if (!nestedData || typeof nestedData !== 'object' || Array.isArray(nestedData)) { 5548 return entry; 5549 } 5550 5551 // Nested collection fields are copied first and outer Duda metadata wins 5552 // when the same key exists at both levels. Keep data itself as well so any 5553 // future code can still inspect the original nested object. 5554 return { ...nestedData, ...entry }; 5555 } 5556 5557 async function fetchCollection(name, options = {}) { 5558 const forceRefresh = Boolean(options && options.forceRefresh); 5559 5560 if (!forceRefresh && collectionRequestCache.has(name)) { 5561 return collectionRequestCache.get(name); 5562 } 5563 5564 const request = (async () => { 5565 const api = await getCollectionsApi(); 5566 5567 // Duda allows a maximum page size of 100. 02 CATEGORY intentionally
5568 // stays capped at 100 rows. Other collections may load a second page, 5569 // giving them a maximum of 200 rows without an unnecessary extra request. 5570 const maxRows = name === '02 CATEGORY' ? 100 : 200; 5571 5572 const firstResult = await api 5573 .data(name) 5574 .pageSize(100) 5575 .pageNumber(0) 5576 .get(); 5577 5578 const firstRows = firstResult && Array.isArray(firstResult.values) 5579 ? firstResult.values 5580 : []; 5581 5582 let rows = [...firstRows]; 5583 5584 if (maxRows > 100 && firstRows.length === 100) { 5585 const secondResult = await api 5586 .data(name) 5587 .pageSize(100) 5588 .pageNumber(1) 5589 .get(); 5590 5591 const secondRows = secondResult && Array.isArray(secondResult.values) 5592 ? secondResult.values 5593 : []; 5594 5595 rows = rows.concat(secondRows); 5596 } 5597 5598 return rows 5599 .slice(0, maxRows) 5600 .map(normalizeFetchedCollectionEntry) 5601 .filter(Boolean); 5602 })(); 5603 5604 // Cache the Promise itself. This also coalesces two overlapping requests for 5605 // the same collection into one network request. 5606 collectionRequestCache.set(name, request); 5607 5608 try { 5609 return await request; 5610 } catch (error) { 5611 // Never cache a failed request. The next rebuild should be able to retry. 5612 if (collectionRequestCache.get(name) === request) { 5613 collectionRequestCache.delete(name); 5614 } 5615 throw error; 5616 } 5617 } 5618 5619 /* 5620 PROGRESSIVE FIRST RENDER 5621 5622 On the first Full-widget build, 06 NAV MASTER is enough to construct a 5623 working provisional navigation model. Render that model as soon as NAV 5624 MASTER is available instead of making the visitor wait for 02 CATEGORY and 5625 03 CORE validation to finish. 5626 5627 The provisional model is intentionally NOT published to window.NMW.data. 5628 Mirror therefore continues to wait for the final validated model. Once all 5629 required collections finish loading, runRebuild() replaces this provisional 5630 render with the normal final navigation. 5631 */ 5632 function renderProvisionalNavigation(navMasterRows) { 5633 if ( 5634 widgetMode !== 'full' || 5635 window.NMW.ready || 5636 !isCurrentInstance() || 5637 !Array.isArray(navMasterRows) || 5638 !navMasterRows.length 5639 ) { 5640 return; 5641 } 5642 5643 try {
5644 let provisionalModel = normalizeNavMasterFallback(navMasterRows); 5645 5646 // Include any configured native navigation that is already available. 5647 // If Duda inserts the native wrapper later, the existing discovery 5648 // observer will schedule the normal rebuild and the final model will pick 5649 // it up without delaying this first visible render. 5650 const nativeItems = normalizeConfiguredNativeNavigation(); 5651 provisionalModel = [...provisionalModel, ...nativeItems]; 5652 5653 // Manual Content Panel items are available without waiting for Category 5654 // or Core and should appear in the same first render when configured. 5655 provisionalModel = layerManualNavigationItems(provisionalModel, false); 5656 5657 // Preserve Mega grouping and the configured top-line presentation so the 5658 // provisional navigation visually matches the final navigation. 5659 provisionalModel = groupMegaNavigationItems(provisionalModel); 5660 5661 if ( 5662 !provisionalModel.length || 5663 !isCurrentInstance() || 5664 window.NMW.ready 5665 ) { 5666 return; 5667 } 5668 5669 root.dataset.nmwProvisional = 'true'; 5670 renderNavigation(provisionalModel, 'full'); 5671 } catch (error) { 5672 // A provisional-render failure must never prevent the normal final build. 5673 console.warn('[NMW] Provisional navigation render failed', error); 5674 } 5675 } 5676 5677 async function fetchRequiredCollections() { 5678 const useCustomNavOnly = readBoolean( 5679 configValueFrom(cfg, 'useCustomNavOnly'), 5680 false 5681 ); 5682 5683 const output = {}; 5684 let hadError = false; 5685 5686 // CUSTOMNAV mode retains its existing loading behavior because 06 NAV 5687 // MASTER is not the active source in that mode. 5688 if (useCustomNavOnly) { 5689 try { 5690 output.CUSTOMNAV = await fetchCollection('CUSTOMNAV'); 5691 } catch (error) { 5692 output.CUSTOMNAV = []; 5693 hadError = true; 5694 warn( 5695 'COLLECTION_FETCH_FAILED', 5696 'Could not fetch collection "CUSTOMNAV".', 5697 error 5698 ); 5699 } 5700 5701 return { collections: output, hadError }; 5702 } 5703 5704 // Normal mode deliberately loads NAV MASTER first. As soon as it resolves, 5705 // render a provisional top navigation before waiting for validation data. 5706 try { 5707 output['06 NAV MASTER'] = await fetchCollection('06 NAV MASTER'); 5708 } catch (error) { 5709 output['06 NAV MASTER'] = []; 5710 hadError = true; 5711 warn( 5712 'COLLECTION_FETCH_FAILED', 5713 'Could not fetch collection "06 NAV MASTER".', 5714 error 5715 ); 5716 } 5717 5718 if (output['06 NAV MASTER'].length) { 5719 renderProvisionalNavigation(output['06 NAV MASTER']); 5720 } 5721 5722 // Preserve the existing sequential loading behavior for Category and Core. 5723 // This remains safer in the Duda editor than starting simultaneous loaders. 5724 for (const name of ['02 CATEGORY', '03 CORE']) { 5725 try { 5726 output[name] = await fetchCollection(name); 5727 } catch (error) { 5728 output[name] = []; 5729 hadError = true; 5730 warn( 5731 'COLLECTION_FETCH_FAILED', 5732 `Could not fetch collection "${name}".`, 5733 error 5734 ); 5735 } 5736 } 5737 5738 return { collections: output, hadError }; 5739 } 5740 5741 5742 // Lightweight runtime diagnostic available from the browser console. 5743 // Run nmwDebug() to confirm startup, render-host detection, shared state, 5744 // and whether the required Duda collections can be loaded. 5745 const runtimeDebug = async function nmwRuntimeDebug() { 5746 const report = { 5747 widgetMode, 5748 instanceId, 5749 rootFound: Boolean(root), 5750 renderHostFound: Boolean(renderHost), 5751 renderHostClass: renderHost ? renderHost.className : '', 5752 categoryUrlPrefix: CATEGORY_URL_PREFIX, 5753 childUrlPrefix: CHILD_URL_PREFIX, 5754 design: { 5755 useUniversalBackgrounds: readBoolean( 5756 designValue(widgetMode, ['useUniversalBackgrounds', 'useUniversalBackground'], true), 5757 true 5758 ), 5759 universalBackgroundColor: designValue(widgetMode, 'universalBackgroundColor', '#ffffff'), 5760 resolvedSubmenuBackground: root.style.getPropertyValue('--nmw-sub-bg'),
5761 resolvedThirdLevelBackground: root.style.getPropertyValue('--nmw-subsub-bg'), 5762 resolvedMegaBackground: root.style.getPropertyValue('--nmw-mega-bg'), 5763 resolvedTopBackground: root.style.getPropertyValue('--nmw-top-bg'), 5764 resolvedMegaFamilyBackground: root.style.getPropertyValue('--nmw-mega-family-bg'), 5765 useUniversalTypography: readBoolean( 5766 designValue(widgetMode, 'useUniversalTypography', true), 5767 true 5768 ), 5769 universalFontFamily: designValue(widgetMode, ['universalFontFamily', 'universalFont'], 'inherit'), 5770 universalFontSize: designValue(widgetMode, ['universalFontSize', 'universalTextSize', 'universalFontFamily', 'universalFont'], '16px'), 5771 universalFontWeight: designValue(widgetMode, ['universalFontWeight', 'universalFontFamily', 'universalFont'], '500'), 5772 resolvedUniversalFontFamily: root.style.getPropertyValue('--nmw-universal-font'), 5773 resolvedUniversalFontSize: root.style.getPropertyValue('--nmw-universal-size'), 5774 resolvedUniversalFontWeight: root.style.getPropertyValue('--nmw-universal-weight') 5775 }, 5776 sharedState: window.NMW 5777 }; 5778 5779 console.group(`NMW Runtime Debug (${widgetMode})`); 5780 console.log('Startup report:', report); 5781 5782 // Full is the collection owner. Mirror intentionally does not fetch. 5783 if (widgetMode === 'full') { 5784 const collectionNames = readBoolean(configValueFrom(cfg, 'useCustomNavOnly'), false) 5785 ? ['CUSTOMNAV'] 5786 : ['06 NAV MASTER', '02 CATEGORY', '03 CORE']; 5787 5788 for (const name of collectionNames) { 5789 try { 5790 const rows = await fetchCollection(name, { forceRefresh: true }); 5791 const sampleSlugs = name === '02 CATEGORY' || name === '03 CORE' 5792 ? rows.slice(0, 10).flatMap(extractCollectionSlugs) 5793 : []; 5794 console.log(`${name}: ${rows.length} row(s) loaded`, { 5795 sampleRows: rows.slice(0, 3), 5796 sampleSlugs 5797 }); 5798 } catch (error) { 5799 console.error(`${name}: FAILED`, error); 5800 } 5801 } 5802 } else { 5803 console.log('Mirror uses Full shared data and does not fetch collections.'); 5804 } 5805 5806 console.groupEnd(); 5807 return report; 5808 }; 5809 5810 window[`nmwDebug${widgetMode === 'full' ? 'Full' : 'Mirror'}`] = runtimeDebug; 5811 // Do not let a later Mirror initialization replace the primary Full debug helper. 5812 if (widgetMode === 'full' || typeof window.nmwDebug !== 'function') { 5813 window.nmwDebug = runtimeDebug; 5814 } 5815 5816 // --------------------------------------------------------------------------- 5817 // Dynamic collection indexes 5818 // --------------------------------------------------------------------------- 5819 5820 function extractPageItemUrl(row) { 5821 const raw = field(row, 'page_item_url', 'Page Item URL', 'PAGE ITEM URL', 'item_url', 'URL'); 5822 if (raw && typeof raw === 'object') return readString(raw.href || raw.url || '', ''); 5823 return readString(raw, ''); 5824 } 5825 5826 function buildDynamicIndex(rows) { 5827 const items = (rows || []).map((row, order) => { 5828 const url = extractPageItemUrl(row); 5829 return { 5830 row, 5831 order, 5832 url, 5833 canonicalPath: canonicalPathWithoutHash(url), 5834 slug: getLastPathSegment(url) 5835 }; 5836 }).filter(item => item.url); 5837 5838 /* 5839 Keep the original ordered items for compatibility, but also build an O(1) 5840 path lookup. If duplicate canonical paths exist, preserve the first row just 5841 as Array.find() did in the original implementation. 5842 */ 5843 const byCanonicalPath = new Map(); 5844 items.forEach(item => { 5845 if (item.canonicalPath && !byCanonicalPath.has(item.canonicalPath)) { 5846 byCanonicalPath.set(item.canonicalPath, item); 5847 } 5848 }); 5849 5850 return { items, byCanonicalPath }; 5851 } 5852 5853 5854 // Build a simple combined slug inventory for Navigation Tree warnings. 5855 // This intentionally ignores which collection the slug is stored in. A slug 5856 // is considered valid when it exists in either 02 CATEGORY or 03 CORE. 5857 function normalizeCollectionSlug(value) { 5858 let raw = value; 5859 if (raw && typeof raw === 'object') { 5860 raw = raw.item 5861 ?? raw.slug 5862 ?? raw.value 5863 ?? raw.href 5864 ?? raw.url 5865 ?? raw.name 5866 ?? ''; 5867 } 5868 5869 const textValue = readString(raw, ''); 5870 if (!textValue) return ''; 5871 5872 // URLs and paths use their final segment. Bare collection item values are 5873 // already slugs and are normalized directly. 5874 if (/^(https?:)?\/\//i.test(textValue) || textValue.includes('/') || textValue.includes('?') || textValue.includes('#')) { 5875 const segment = getLastPathSegment(textValue); 5876 if (segment) return segment.toLowerCase(); 5877 } 5878 5879 return textValue 5880 .toLowerCase() 5881 .split(/[?#]/)[0] 5882 .replace(/^\/+|\/+$/g, '') 5883 .split('/') 5884 .filter(Boolean) 5885 .pop() || ''; 5886 } 5887 5888 function extractCollectionSlugs(row) { 5889 if (!row || typeof row !== 'object') return []; 5890 5891 // Inspect both the flattened row and the original nested data object. This 5892 // supports either Duda response shape without depending on where the item 5893 // slug or dynamic page URL happens to be stored. 5894 const sources = [row]; 5895 if (row.data && typeof row.data === 'object' && !Array.isArray(row.data)) { 5896 sources.push(row.data); 5897 } 5898 5899 const rawValues = [];
5900 sources.forEach(source => { 5901 rawValues.push( 5902 field(source, 'item', 'Item', 'ITEM'), 5903 field(source, 'slug', 'Slug', 'SLUG'), 5904 field(source, 'name', 'Name', 'NAME'), 5905 extractPageItemUrl(source) 5906 ); 5907 }); 5908 5909 return unique(rawValues.map(normalizeCollectionSlug)); 5910 } 5911 5912 function buildCombinedCollectionSlugSet(categoryRows, coreRows) { 5913 const slugs = new Set(); 5914 [...(categoryRows || []), ...(coreRows || [])].forEach(row => { 5915 extractCollectionSlugs(row).forEach(slug => { 5916 if (slug) slugs.add(slug); 5917 }); 5918 }); 5919 return slugs; 5920 } 5921 5922 function buildSlugValidationWarnings(tree, categoryRows, coreRows) { 5923 const availableSlugs = buildCombinedCollectionSlugSet(categoryRows, coreRows); 5924 const warnings = []; 5925 5926 function walk(nodes) { 5927 (nodes || []).forEach(node => { 5928 if (!node || node.invalid || node.depth > MAX_DEPTH) return; 5929 5930 const isHome = node.depth === 0 && node.label.toLowerCase() === 'home'; 5931 if (!isHome) { 5932 const candidates = createSlugCandidates(node.label); 5933 const found = candidates.some(slug => availableSlugs.has(slug)); 5934 5935 if (!found) { 5936 const validChildren = (node.children || []).filter(child => child && !child.invalid && child.depth <= MAX_DEPTH); 5937 const suggestedCollection = usesCategorySource(node.depth, validChildren) 5938 ? '02 CATEGORY' 5939 : '03 CORE'; 5940 5941 warnings.push({ 5942 label: node.label, 5943 slugs: candidates, 5944 suggestedCollection 5945 }); 5946 } 5947 } 5948 5949 walk(node.children || []); 5950 }); 5951 } 5952 5953 walk(tree || []); 5954 return warnings; 5955 } 5956 5957 // Build complete expected paths from Page Name and the configured prefix. 5958 // The display label is never changed. Only lookup candidates are normalized. 5959 // "Heating/Cooling" with prefix "services" becomes /services/heating-cooling. 5960 // "Boats & Cars" checks both /.../boats-and-cars and /.../boats-cars. 5961 function buildExpectedDynamicPaths(pageName, prefix) { 5962 return createSlugCandidates(pageName).map(slug => { 5963 const path = prefix ? `/${prefix}/${slug}` : `/${slug}`; 5964 return canonicalPathWithoutHash(path); 5965 }); 5966 } 5967 5968 // Match only the complete expected dynamic-page path. The configured prefix 5969 // is authoritative. A row with the same final slug under a different prefix 5970 // must not be selected, because that would make categoryUrlPrefix and 5971 // childUrlPrefix appear to be ignored. The Map lookup keeps the original 5972 // collection-order behavior when both supported ampersand variants exist. 5973 function findDynamicMatch(index, pageName, prefix) { 5974 const expectedPaths = buildExpectedDynamicPaths(pageName, prefix); 5975 if (!index || !expectedPaths.length) return null; 5976 5977 if (index.byCanonicalPath instanceof Map) { 5978 let earliest = null; 5979 5980 expectedPaths.forEach(path => { 5981 const match = index.byCanonicalPath.get(path); 5982 if (match && (!earliest || match.order < earliest.order)) { 5983 earliest = match; 5984 } 5985 }); 5986 5987 return earliest; 5988 } 5989 5990 // Defensive compatibility fallback for any older array-shaped index. 5991 const items = Array.isArray(index) 5992 ? index 5993 : (Array.isArray(index.items) ? index.items : []); 5994 5995 return items.find(item => expectedPaths.includes(item.canonicalPath)) || null; 5996 } 5997 5998 // Build the internal dynamic URL dictated by the configured Category/Core 5999 // prefix. This is the fallback when the corresponding collection does not yet 6000 // contain an exact page_item_url. It keeps the item visible without allowing a 6001 // legacy Page Link or a same-slug row under another prefix to override the panel. 6002 function buildConfiguredDynamicUrl(pageName, prefix) { 6003 const slug = slugify(pageName); 6004 const path = `/${prefix ? `${prefix}/` : ''}${slug}`; 6005 return buildRuntimeFallbackUrl(path); 6006 } 6007 6008 // Category pages are structural navigation parents. Any navigation item with 6009 // no valid children is treated as a Core page, even when it is top-level or 6010 // assigned to a Mega group. Mega controls presentation only. 6011 function usesCategorySource(depth, children) { 6012 const childCount = Array.isArray(children) ? children.length : 0; 6013 return Number(depth) < MAX_DEPTH && childCount > 0; 6014 } 6015 6016 // --------------------------------------------------------------------------- 6017 // 06 NAV MASTER normalization 6018 // --------------------------------------------------------------------------- 6019 6020 function parseMegaGroup(value) { 6021 // Duda collection fields may arrive as strings, booleans, or small objects 6022 // depending on whether the Mega column is configured as text, select, or switch. 6023 let raw = value; 6024 if (raw && typeof raw === 'object') { 6025 raw = raw.value 6026 ?? raw.label 6027 ?? raw.name 6028 ?? raw.text 6029 ?? raw.selected 6030 ?? raw.checked 6031 ?? raw.id 6032 ?? ''; 6033 } 6034 6035 if (raw === true) return 1; 6036 if (raw === false || raw == null) return 0; 6037 6038 const normalized = readString(raw, 'No') 6039 .toLowerCase() 6040 .replace(/[\s_-]+/g, ''); 6041 6042 if (['yes', 'true', 'on', 'mega', 'mega1', '1'].includes(normalized)) return 1; 6043 if (['mega2', '2'].includes(normalized)) return 2; 6044 if (['mega3', '3'].includes(normalized)) return 3; 6045 if (['mega4', '4'].includes(normalized)) return 4; 6046 return 0; 6047 } 6048 6049 function extractNavMasterPageLink(row) { 6050 const raw = field( 6051 row, 6052 'Page Link', 6053 'page_link', 6054 'PAGE LINK', 6055 'Page URL', 6056 'page_url', 6057 'Link' 6058 ); 6059 6060 if (raw && typeof raw === 'object') { 6061 return readString(raw.href ?? raw.url ?? raw.value ?? '', ''); 6062 } 6063 6064 return readString(raw, ''); 6065 } 6066 6067 function buildCollectionTree(navRows) { 6068 const nodes = []; 6069 const byName = new Map(); 6070
6071 (navRows || []).forEach((row, order) => { 6072 const label = readString(field(row, 'Page Name'), ''); 6073 if (!label) return; 6074 const nameKey = label.toLowerCase(); 6075 if (byName.has(nameKey)) { 6076 warn('DUPLICATE_PAGE_NAME', `Duplicate Page Name "${label}" was ignored.`, row); 6077 return; 6078 } 6079 const node = { 6080 raw: row, 6081 label, 6082 parentName: readString(field(row, 'Parent 1'), ''), 6083 topMegaValue: field(row, 'Mega'), 6084 pageLink: extractNavMasterPageLink(row), 6085 order, 6086 children: [], 6087 parent: null 6088 }; 6089 nodes.push(node); 6090 byName.set(nameKey, node); 6091 }); 6092 6093 const roots = []; 6094 nodes.forEach(node => { 6095 if (!node.parentName) { 6096 roots.push(node); 6097 return; 6098 } 6099 const parent = byName.get(node.parentName.toLowerCase()); 6100 if (!parent || parent === node) { 6101 warn('MISSING_PARENT', `"${node.label}" was skipped because Parent 1 "${node.parentName}" was not found.`, node.raw); 6102 node.invalid = true; 6103 return; 6104 } 6105 node.parent = parent; 6106 parent.children.push(node); 6107 }); 6108 6109 function sortAndValidate(list, depth) { 6110 list.sort((a, b) => a.order - b.order); 6111 list.forEach(node => { 6112 node.depth = depth; 6113 if (depth > MAX_DEPTH) { 6114 node.invalid = true; 6115 warn('MAX_DEPTH_EXCEEDED', `"${node.label}" was ignored because navigation supports only three levels.`, node.raw); 6116 } 6117 sortAndValidate(node.children, depth + 1); 6118 }); 6119 } 6120 sortAndValidate(roots, 0); 6121 return roots.filter(node => !node.invalid); 6122 } 6123 6124 function normalizeCollectionNavigation(navRows, categoryRows, coreRows) { 6125 const tree = buildCollectionTree(navRows); 6126 const categoryIndex = buildDynamicIndex(categoryRows); 6127 const coreIndex = buildDynamicIndex(coreRows); 6128 6129 // Navigation Tree warnings use one combined slug inventory. The actual
6130 // collection only matters as a suggestion when a slug is absent from both. 6131 local.slugValidationWarnings = buildSlugValidationWarnings(tree, categoryRows, coreRows); 6132 6133 function normalizeNode(node, ancestors, inheritedMegaGroup) { 6134 if (node.invalid || node.depth > MAX_DEPTH) return null; 6135 const isRoot = node.depth === 0; 6136 const megaGroup = isRoot ? parseMegaGroup(node.topMegaValue) : inheritedMegaGroup; 6137 if (!isRoot && parseMegaGroup(node.topMegaValue) && parseMegaGroup(node.topMegaValue) !== inheritedMegaGroup) { 6138 warn('DESCENDANT_MEGA_IGNORED', `Mega value on descendant "${node.label}" was ignored; descendants inherit the top-level mega group.`, node.raw); 6139 } 6140 6141 const validChildren = node.children 6142 .map(child => normalizeNode(child, [...ancestors, node], megaGroup)) 6143 .filter(Boolean); 6144 const hasGrandchildren = node.depth === 1 && validChildren.length > 0; 6145 6146 const isHome = node.depth === 0 && node.label.toLowerCase() === 'home'; 6147 const expectsCategory = usesCategorySource(node.depth, validChildren); 6148 const expectedIndex = expectsCategory ? categoryIndex : coreIndex; 6149 const expectedPrefix = expectsCategory ? CATEGORY_URL_PREFIX : CHILD_URL_PREFIX; 6150 const match = isHome ? null : findDynamicMatch(expectedIndex || [], node.label, expectedPrefix); 6151 const navMasterUrl = node.pageLink ? buildRuntimeFallbackUrl(node.pageLink) : ''; 6152 let type = 'page'; 6153 let url = ''; 6154 6155 if (isHome) { 6156 const nativeHome = findTreeItem(parseNativeNavigation(), item => item.type === 'page' && item.canonicalPath === '/'); 6157 url = nativeHome && nativeHome.url 6158 ? nativeHome.url 6159 : (navMasterUrl || buildRuntimeFallbackUrl('/')); 6160 } else { 6161 // Prefix controls are authoritative for collection navigation. Prefer the 6162 // exact page_item_url only when it matches the configured full path. If 6163 // that row is missing, construct the URL from the configured prefix. 6164 // Page Link remains structure/reference data and no longer overrides the 6165 // Category/Core prefix fields for internal dynamic pages. 6166 url = match ? match.url : buildConfiguredDynamicUrl(node.label, expectedPrefix); 6167 } 6168 6169 if (!isHome && !match) { 6170 warn('DYNAMIC_PREFIX_CONSTRUCTED_FALLBACK', `"${node.label}" used the configured prefix because no exact dynamic collection path was found.`, { 6171 depth: node.depth, 6172 expectedCollection: expectsCategory ? '02 CATEGORY' : '03 CORE', 6173 configuredPrefix: expectedPrefix, 6174 ignoredPageLink: node.pageLink || '', 6175 expectedPaths: buildExpectedDynamicPaths(node.label, expectedPrefix), 6176 constructedUrl: url 6177 }); 6178 } 6179 6180 // Standard top-level structural parents without a matching Category page 6181 // may still act as folders when they have valid children and no explicit 6182 // Mega assignment. Mega families remain clickable as Category parents. 6183 if (!isHome && node.depth === 0 && megaGroup === 0 && !match && validChildren.length && !node.pageLink) { 6184 type = 'folder'; 6185 url = ''; 6186 } 6187 6188 const hierarchySlugs = [...ancestors.map(item => slugify(item.label)), slugify(node.label)]; 6189 const key = `collection:${hierarchySlugs.join('/')}`; 6190 const presentation = megaGroup ? 'mega' : 'standard'; 6191 return { 6192 key, 6193 parentKey: ancestors.length ? `collection:${ancestors.map(item => slugify(item.label)).join('/')}` : null, 6194 source: 'collection', 6195 type, 6196 presentation, 6197 megaGroup, 6198 depth: node.depth, 6199 order: node.order, 6200 label: node.label, 6201 slug: slugify(node.label), 6202 canonicalPath: url ? canonicalPath(url) : '', 6203 url, 6204 target: '_self', 6205 isExternal: isExternalUrl(url), 6206 hasChildren: validChildren.length > 0, 6207 children: validChildren 6208 }; 6209 } 6210 6211 let normalized = tree.map(rootNode =>
6211 normalizeNode(rootNode, [], 0)).filter(Boolean); 6212 6213 // Mega family alphabetization is applied only after top-level families are 6214 // grouped under their synthetic Mega trigger. Child and grandchild order is 6215 // always preserved from 06 NAV MASTER. 6216 return normalized; 6217 } 6218 6219 // --------------------------------------------------------------------------- 6220 // Native Duda navigation from .navmasterdefault 6221 // --------------------------------------------------------------------------- 6222 6223 function getNativeWrapper() { 6224 return document.querySelector('.navmasterdefault'); 6225 } 6226 6227 function nativeAnchorLabel(anchor) { 6228 if (!anchor) return ''; 6229 const textNode = anchor.querySelector('[data-link-text]'); 6230 return readString(textNode?.getAttribute('data-link-text') || textNode?.textContent || anchor.textContent, ''); 6231 } 6232 6233 function nativeAnchorHref(anchor) { 6234 if (!anchor) return ''; 6235 const values = [ 6236 anchor.getAttribute('href'), 6237 anchor.getAttribute('runtime_url'), 6238 anchor.getAttribute('data-href'), 6239 anchor.getAttribute('raw_url') 6240 ].map(value => readString(value, '')); 6241 6242 // Preserve Duda's current rendered href whenever it is usable. If href is 6243 // only a folder placeholder, fall through to runtime_url/raw_url. 6244 return values.find(value => value && value !== '#' && !value.toLowerCase().startsWith('javascript:')) 6245 || values.find(Boolean) 6246 || ''; 6247 } 6248 6249 function nativeAnchorRawUrl(anchor) { 6250 if (!anchor) return ''; 6251 return readString( 6252 anchor.getAttribute('raw_url') || 6253 anchor.getAttribute('runtime_url') || 6254 anchor.getAttribute('data-href') || 6255 anchor.getAttribute('href'), 6256 '' 6257 ); 6258 } 6259 6260 function readNativeNode(li, depth, parentKey) { 6261 if (!li || depth > MAX_DEPTH) return null; 6262 const anchor = li.querySelector(':scope > a, :scope > span > a, :scope > div > a'); 6263 const href = nativeAnchorHref(anchor); 6264 const rawUrl = nativeAnchorRawUrl(anchor); 6265 // Preserve Duda-generated link metadata/classes from the native navigation. 6266 // Anchor links can rely on these attributes for Duda's own scrolling and 6267 // fixed-header compensation behavior. linkHtml() already knows how to carry 6268 // linkAttributes/linkClassName into the rebuilt NMW anchor. 6269 const nativeLinkData = anchor ? manualLinkFromAnchor(anchor) : { attributes: {}, className: '' }; 6270 const label = nativeAnchorLabel(anchor) || readString(li.getAttribute('data-link-text'), ''); 6271 const isFolder = !href || href === '#' || !anchor; 6272 const canonical = isFolder ? '' : canonicalPath(rawUrl || href); 6273 const key = isFolder 6274 ? `native-folder:${parentKey || 'root'}/${slugify(label)}` 6275 : `native:${canonical || canonicalPath(href)}`; 6276 const childList = li.querySelector(':scope > ul[data-auto="sub-pages"], :scope > ul.unav-sub, :scope > ul'); 6277 const childLis = childList ? Array.from(childList.querySelectorAll(':scope > li')) : []; 6278 const children = childLis.map(child => readNativeNode(child, depth + 1, key)).filter(Boolean); 6279 return { 6280 key, 6281 parentKey, 6282 source: 'native', 6283 type: isFolder ? 'folder' : 'page', 6284 presentation: 'standard', 6285 megaGroup: 0, 6286 depth, 6287 order: 0, 6288 label, 6289 slug: slugify(label), 6290 canonicalPath: canonical, 6291 url: isFolder ? '' : href, 6292 target: anchor ? readString(anchor.getAttribute('target'), '_self') : '_self', 6293 isExternal: isExternalUrl(href), 6294 linkAttributes: nativeLinkData.attributes || {}, 6295 linkClassName: readString(nativeLinkData.className, ''), 6296 hasChildren: children.length > 0, 6297 children 6298 }; 6299 } 6300 6301 function parseNativeNavigation() { 6302 const wrapper = getNativeWrapper(); 6303 if (!wrapper) return []; 6304 const nav = wrapper.querySelector('nav[data-element-type="onelinksmenu"], nav.unifiednav, nav'); 6305 if (!nav) return []; 6306 const topList = nav.querySelector(':scope > ul[data-auto="navigation-pages"], :scope > ul'); 6307 if (!topList) return []; 6308 return Array.from(topList.querySelectorAll(':scope > li')).map(li =>
6308 readNativeNode(li, 0, null)).filter(Boolean); 6309 } 6310 6311 function configuredNativeParents() { 6312 const configured = configValueFrom(cfg, 'nativeParents'); 6313 const list = Array.isArray(configured) ? configured : []; 6314 return list.map((item, order) => ({ 6315 url: readString(item.url?.href || item.url, ''), 6316 canonicalPath: canonicalPath(item.url?.href || item.url), 6317 label: readString(item.label, ''), 6318 showParent: readBoolean(item.showParent, false), 6319 pullChildren: readBoolean(item.pullChildren, false), 6320 order 6321 })); 6322 } 6323 6324 function cloneNativeBranch(node, config, depth = 0, parentKey = null) { 6325 if (!node || depth > MAX_DEPTH) return null; 6326 const children = config.pullChildren 6327 ? node.children.map(child => cloneNativeBranch(child, { ...config, label: '', showParent: true, pullChildren: true }, depth + 1, node.key)).filter(Boolean) 6328 : []; 6329 6330 // A folder without pulled valid children has no useful action and is omitted. 6331 if (node.type === 'folder' && !children.length) return null; 6332 6333 return { 6334 ...node, 6335 parentKey, 6336 depth, 6337 order: config.order, 6338 label: depth === 0 && config.label ? config.label : node.label, 6339 hasChildren: children.length > 0, 6340 children 6341 }; 6342 } 6343 6344 function normalizeConfiguredNativeNavigation() { 6345 const parsed = parseNativeNavigation(); 6346 const configs = configuredNativeParents(); 6347 const result = []; 6348 const seen = new Set(); 6349 6350 configs.forEach(config => { 6351 if (!config.showParent) return; 6352 if (!config.canonicalPath || seen.has(config.canonicalPath)) { 6353 if (seen.has(config.canonicalPath)) warn('DUPLICATE_NATIVE_CONFIG', `Native URL "${config.url}" was configured more than once; the first entry was kept.`); 6354 return; 6355 } 6356 seen.add(config.canonicalPath); 6357 const match = parsed.find(item => item.canonicalPath === config.canonicalPath); 6358 if (!match) { 6359 warn('NATIVE_URL_NOT_FOUND', `Configured native URL "${config.url}" was not found in .navmasterdefault.`); 6360 return; 6361 } 6362 const normalized = cloneNativeBranch(match, config, 0, null); 6363 if (normalized) result.push(normalized); 6364 }); 6365 6366 return result; 6367 } 6368 6369 // --------------------------------------------------------------------------- 6370 // Manual navigation items from the Duda Content Panel 6371 // --------------------------------------------------------------------------- 6372 6373 /* 6374 Manual items are layered onto either navigation source: 6375 - Normal mode: 06 NAV MASTER + configured native pages + manual items. 6376 - CUSTOMNAV-only mode: CUSTOMNAV collection + manual items. 6377 6378 Duda link inputs must be rendered with the custom_link Handlebars helper so 6379 page, dynamic-page, popup, anchor, email, telephone, and download behavior 6380 retains the attributes Duda generates. The matching HTML writes those links 6381 into a hidden manual-item source. JavaScript reads the generated anchors and 6382 carries their attributes into the final navigation markup. 6383 */ 6384 6385 function normalizeManualLevel(value) { 6386 const normalized = readString(value, 'top').toLowerCase().replace(/[\s_-]+/g, ''); 6387 if (['2', 'grandchild', 'subsub', 'third', 'thirdlevel'].includes(normalized)) return 2; 6388 if (['1', 'child', 'sub', 'second', 'secondlevel'].includes(normalized)) return 1; 6389 return 0; 6390 } 6391 6392 function normalizeManualPlacement(value) { 6393 const normalized = readString(value, 'after-native').toLowerCase().replace(/[\s_]+/g, '-'); 6394 if (['afterhome', 'after-home', 'home'].includes(normalized)) return 'after-home'; 6395 if (['aftercollection', 'after-collection', 'collection'].includes(normalized)) return 'after-collection'; 6396 return 'after-native'; 6397 } 6398 6399 function resolveManualRuntimeHref(value) { 6400 const raw = readString(value, ''); 6401 if (!raw) return ''; 6402 6403 // Duda-generated special links already contain the required scheme/action. 6404 if (/^(?:https?:|mailto:|tel:|sms:|javascript:|data:|blob:)/i.test(raw) || raw.startsWith('//') || raw.startsWith('#')) { 6405 return raw; 6406 } 6407 6408 return buildRuntimeFallbackUrl(raw); 6409 } 6410 6411 function manualLinkValue(value) { 6412 if (value == null) return { href: '', target: '_self', attributes: {}, className: '' }; 6413 if (typeof value === 'string') { 6414 return { href: resolveManualRuntimeHref(value), target: '_self', attributes: {}, className: '' }; 6415 } 6416 6417 if (typeof value !== 'object') { 6418 return { href: '', target: '_self', attributes: {}, className: '' }; 6419 } 6420 6421 const href = readString( 6422 value.href 6423 ?? value.url 6424 ?? value.runtime_url 6425 ?? value.runtimeUrl 6426 ?? value.raw_url 6427 ?? value.rawUrl 6428 ?? value.value 6429 ?? value.path 6430 ?? '', 6431 '' 6432 ); 6433 const opensNewWindow = readBoolean(value.newWindow ?? value.openInNewWindow ?? value.new_window, false); 6434 const target = readString(value.target, opensNewWindow ? '_blank' : '_self'); 6435 const attributes = {}; 6436 6437 // Preserve common Duda link metadata when the configuration object exposes it.
6438 Object.entries(value).forEach(([name, attributeValue]) => { 6439 const lower = String(name).toLowerCase(); 6440 if (!attributeValue && attributeValue !== 0) return; 6441 if (['href', 'url', 'runtime_url', 'runtimeurl', 'raw_url', 'rawurl', 'value', 'path', 'target', 'class', 'classname', 'style', 'id'].includes(lower)) return; 6442 if (lower.startsWith('data-') || ['rel', 'download', 'title', 'onclick'].includes(lower)) { 6443 attributes[name] = String(attributeValue); 6444 } 6445 }); 6446 6447 return { 6448 href: resolveManualRuntimeHref(href), 6449 target, 6450 attributes, 6451 className: readString(value.className ?? value.class, '') 6452 }; 6453 } 6454 6455 function manualLinkFromAnchor(anchor) { 6456 if (!anchor) return { href: '', target: '_self', attributes: {}, className: '' }; 6457 const attributes = {}; 6458 6459 Array.from(anchor.attributes || []).forEach(attribute => { 6460 const name = attribute.name; 6461 const lower = name.toLowerCase(); 6462 if (['href', 'target', 'class', 'style', 'id', 'role', 'aria-current'].includes(lower)) return; 6463 // Retain Duda's link behavior metadata, including popup/download handlers. 6464 if (lower.startsWith('data-') || lower.startsWith('aria-') || lower.startsWith('on') || ['rel', 'download', 'title', 'name', 'type'].includes(lower)) { 6465 attributes[name] = attribute.value; 6466 } 6467 }); 6468 6469 return { 6470 href: readString(anchor.getAttribute('href'), ''), 6471 target: readString(anchor.getAttribute('target'), '_self'), 6472 attributes, 6473 className: readString(anchor.getAttribute('class'), '') 6474 }; 6475 } 6476 6477 function manualRowFromDom(node, index) { 6478 const anchor = node.querySelector('a'); 6479 return { 6480 manualItemKey: node.getAttribute('data-manual-item-key') || '', 6481 manualLabel: node.getAttribute('data-manual-label') || '', 6482 manualType: node.getAttribute('data-manual-type') || 'page', 6483 manualLevel: node.getAttribute('data-manual-level') || 'top', 6484 manualParent: node.getAttribute('data-manual-parent') || '', 6485 manualPlacement: node.getAttribute('data-manual-placement') || 'after-native', 6486 manualOrder: node.getAttribute('data-manual-order') || index, 6487 manualMegaGroup: node.getAttribute('data-manual-mega-group') || '0', 6488 manualShowItem: node.getAttribute('data-manual-show-item'), 6489 // Return null until Duda's custom_link helper has produced a real anchor. 6490 // This allows normalizeManualPanelItems() to fall back to data.config. 6491 __manualLink: anchor ? manualLinkFromAnchor(anchor) : null 6492 }; 6493 } 6494 6495 function configuredManualRows() { 6496 const configured = configValueFrom(cfg, 'manualNavigationItems'); 6497 const configRows = Array.isArray(configured) ? configured : [];
6498 const domNodes = Array.from(root.querySelectorAll('[data-nmw-manual-item]')); 6499 const domRows = domNodes.map(manualRowFromDom); 6500 6501 if (!domRows.length) return configRows; 6502 if (!configRows.length) return domRows; 6503 6504 // Duda can update the repeat-group configuration and the hidden Handlebars 6505 // output a few milliseconds apart. Merge both representations so a new row 6506 // can appear from data.config immediately, then receive the final custom_link 6507 // attributes as soon as Duda writes them into the hidden source. 6508 const configByKey = new Map(); 6509 configRows.forEach((row, index) => { 6510 const key = readString(row && (row.manualItemKey ?? row.itemKey ?? row.key), '').toLowerCase(); 6511 if (key && !configByKey.has(key)) configByKey.set(key, { row, index }); 6512 }); 6513 6514 const usedConfigIndexes = new Set(); 6515 const merged = domRows.map((domRow, index) => { 6516 const key = readString(domRow.manualItemKey, '').toLowerCase(); 6517 const keyed = key ? configByKey.get(key) : null; 6518 const configIndex = keyed ? keyed.index : index; 6519 const configRow = keyed ? keyed.row : (configRows[configIndex] || {}); 6520 usedConfigIndexes.add(configIndex); 6521 6522 // DOM attributes are the newest visible Handlebars output. Config values 6523 // supply fields that are not yet represented in the DOM, especially links. 6524 return { ...configRow, ...domRow }; 6525 }); 6526 6527 configRows.forEach((row, index) => { 6528 if (!usedConfigIndexes.has(index)) merged.push(row); 6529 }); 6530 6531 return merged; 6532 } 6533 6534 function normalizeManualPanelItems() { 6535 const output = []; 6536 const usedKeys = new Set(); 6537 6538 configuredManualRows().forEach((row, index) => { 6539 const showValue = row.manualShowItem ?? row.showItem ?? row.show; 6540 if (showValue !== undefined && showValue !== null && showValue !== '' && !readBoolean(showValue, true)) return; 6541 6542 const label = readString(row.manualLabel ?? row.label ?? row.pageName, ''); 6543 if (!label) return; 6544 6545 const type = readString(row.manualType ?? row.type, 'page').toLowerCase() === 'folder' ? 'folder' : 'page'; 6546 const requestedDepth = normalizeManualLevel(row.manualLevel ?? row.level); 6547 const parentReference = readString(row.manualParent ?? row.parentReference ?? row.parent, ''); 6548 const placement = normalizeManualPlacement(row.manualPlacement ?? row.placement); 6549 const order = readNumber(row.manualOrder ?? row.order, index, -100000, 100000); 6550 const configuredMegaGroup = parseMegaGroup(row.manualMegaGroup ?? row.megaGroup ?? 0); 6551 const configuredLinkData = manualLinkValue(row.manualLink ?? row.link ?? row.url); 6552 const domLinkData = row.__manualLink && readString(row.__manualLink.href, '') 6553 ? row.__manualLink 6554 : null; 6555 const linkData = domLinkData || configuredLinkData; 6556 // Folder items are structural controls and never require or output a link. 6557 const url = type === 'folder' ? '' : resolveManualRuntimeHref(linkData.href); 6558 6559 if (type === 'page' && !url) { 6560 warn('MANUAL_LINK_MISSING', `Manual navigation item "${label}" was skipped because it has no configured link.`); 6561 return; 6562 } 6563 6564 const suppliedKey = readString(row.manualItemKey ?? row.itemKey ?? row.key, ''); 6565 let key = suppliedKey || `manual:${slugify(label) || 'item'}-${index + 1}`; 6566 if (!key.toLowerCase().startsWith('manual:')) key = `manual:${key}`; 6567 if (usedKeys.has(key.toLowerCase())) { 6568 warn('MANUAL_DUPLICATE_KEY', `Manual navigation key "${key}" was used more than once; the later item was skipped.`); 6569 return; 6570 } 6571 usedKeys.add(key.toLowerCase()); 6572 6573 output.push({ 6574 key, 6575 parentKey: null, 6576 source: 'manual', 6577 type, 6578 presentation: configuredMegaGroup ? 'mega' : 'standard', 6579 megaGroup: configuredMegaGroup, 6580 depth: requestedDepth, 6581 order, 6582 label, 6583 slug: slugify(label), 6584 canonicalPath: url && !/^(?:mailto:|tel:|sms:|javascript:)/i.test(url) ? canonicalPath(url) : '', 6585 url, 6586 target: readString(linkData.target, '_self'), 6587 isExternal: isExternalUrl(url), 6588 hasChildren: false, 6589 children: [], 6590 linkAttributes: linkData.attributes || {}, 6591 linkClassName: readString(linkData.className, ''), 6592 _manualRequestedDepth: requestedDepth, 6593 _manualParentReference: parentReference, 6594 _manualPlacement: placement 6595 }); 6596 }); 6597 6598 return output; 6599 } 6600 6601 function buildManualParentIndex(baseItems, manualItems) { 6602 const byKey = new Map(); 6603 const byLabel = new Map(); 6604 const bySlug = new Map(); 6605 6606 function add(item) { 6607 if (!item) return;
6608 const key = readString(item.key, '').toLowerCase(); 6609 const label = readString(item.label, '').toLowerCase(); 6610 const slug = readString(item.slug, slugify(item.label)).toLowerCase(); 6611 if (key && !byKey.has(key)) byKey.set(key, item); 6612 if (label) { 6613 if (!byLabel.has(label)) byLabel.set(label, []); 6614 byLabel.get(label).push(item); 6615 } 6616 if (slug) { 6617 if (!bySlug.has(slug)) bySlug.set(slug, []); 6618 bySlug.get(slug).push(item); 6619 } 6620 (item.children || []).forEach(add); 6621 } 6622 6623 (baseItems || []).forEach(add); 6624 (manualItems || []).forEach(add); 6625 return { byKey, byLabel, bySlug }; 6626 } 6627 6628 function resolveManualParent(reference, index) { 6629 const raw = readString(reference, ''); 6630 if (!raw) return null; 6631 const lower = raw.toLowerCase(); 6632 const keyCandidates = unique([lower, lower.startsWith('manual:') ? lower : `manual:${lower}`]); 6633 for (const key of keyCandidates) { 6634 if (index.byKey.has(key)) return index.byKey.get(key); 6635 } 6636 6637 const labelMatches = index.byLabel.get(lower) || []; 6638 if (labelMatches.length) { 6639 if (labelMatches.length > 1) { 6640 warn('MANUAL_PARENT_AMBIGUOUS', `Parent reference "${raw}" matched more than one Page Name. The first match was used; use an Item Key for an exact match.`); 6641 } 6642 return labelMatches[0]; 6643 } 6644 6645 const slugMatches = index.bySlug.get(slugify(raw)) || []; 6646 if (slugMatches.length) { 6647 if (slugMatches.length > 1) { 6648 warn('MANUAL_PARENT_AMBIGUOUS', `Parent reference "${raw}" matched more than one slug. The first match was used; use an Item Key for an exact match.`); 6649 } 6650 return slugMatches[0]; 6651 } 6652 6653 return null; 6654 } 6655 6656 function manualParentCreatesCycle(item, parent, manualByKey) {
6657 let current = parent; 6658 const seen = new Set([item.key]); 6659 while (current) { 6660 if (seen.has(current.key)) return true; 6661 seen.add(current.key); 6662 const parentReference = current._manualParentReference; 6663 if (!parentReference) return false; 6664 current = manualByKey.get(readString(parentReference, '').toLowerCase()) 6665 || manualByKey.get(`manual:${readString(parentReference, '').toLowerCase()}`) 6666 || null; 6667 } 6668 return false; 6669 } 6670 6671 function finalizeManualLayer(items, depth = 0, parentKey = null, inheritedMegaGroup = 0) { 6672 const sourceItems = (items || []).filter(Boolean); 6673 // Top-level placement is already explicitly assembled as After Home, 6674 // After Collection, and After Native. Preserve that order. Child and 6675 // grandchild siblings use the numeric Order field. 6676 if (depth > 0) sourceItems.sort((a, b) => Number(a.order || 0) - Number(b.order || 0)); 6677 return sourceItems.map(item => { 6678 item.depth = depth; 6679 item.parentKey = parentKey; 6680 const group = depth === 0 ? Math.round(Number(item.megaGroup) || 0) : inheritedMegaGroup; 6681 item.megaGroup = group >= 1 && group <= 4 ? group : 0; 6682 item.presentation = item.megaGroup ? 'mega' : 'standard'; 6683 item.children = finalizeManualLayer(item.children || [], depth + 1, item.key, item.megaGroup); 6684 item.hasChildren = item.children.length > 0; 6685 delete item._manualRequestedDepth; 6686 delete item._manualParentReference; 6687 delete item._manualPlacement; 6688 return item; 6689 }); 6690 } 6691 6692 function insertManualRoots(baseItems, manualRoots, customNavMode) { 6693 const afterHome = manualRoots.filter(item => item._manualPlacement === 'after-home').sort((a, b) => a.order - b.order); 6694 const afterCollection = manualRoots.filter(item => item._manualPlacement === 'after-collection').sort((a, b) => a.order - b.order); 6695 const afterNative = manualRoots.filter(item => item._manualPlacement === 'after-native').sort((a, b) => a.order - b.order); 6696 6697 // CUSTOMNAV is itself the collection-backed base. Preserve its authored order 6698 // and layer the panel items around that complete base rather than re-sorting it 6699 // by each row's SOURCE metadata. 6700 if (customNavMode) { 6701 const base = [...baseItems]; 6702 const homeIndex = base.findIndex(item => readString(item.label, '').toLowerCase() === 'home' || item.canonicalPath === '/'); 6703 if (afterHome.length) base.splice(homeIndex >= 0 ? homeIndex + 1 : 0, 0, ...afterHome); 6704 return [...base, ...afterCollection, ...afterNative]; 6705 } 6706 6707 const firstNativeIndex = baseItems.findIndex(item => item.source === 'native'); 6708 const collectionBlock = firstNativeIndex >= 0 ? baseItems.slice(0, firstNativeIndex) : [...baseItems]; 6709 const nativeBlock = firstNativeIndex >= 0 ? baseItems.slice(firstNativeIndex) : []; 6710 const homeIndex = collectionBlock.findIndex(item => readString(item.label, '').toLowerCase() === 'home' || item.canonicalPath === '/'); 6711 if (afterHome.length) collectionBlock.splice(homeIndex >= 0 ? homeIndex + 1 : 0, 0, ...afterHome);
6712 return [...collectionBlock, ...afterCollection, ...nativeBlock, ...afterNative]; 6713 } 6714 6715 function layerManualNavigationItems(baseItems, customNavMode) { 6716 const manualItems = normalizeManualPanelItems(); 6717 if (!manualItems.length) return baseItems; 6718 6719 const manualByKey = new Map(manualItems.map(item => [item.key.toLowerCase(), item])); 6720 const index = buildManualParentIndex(baseItems, manualItems); 6721 const roots = []; 6722 6723 manualItems.forEach(item => { 6724 const requestedDepth = item._manualRequestedDepth; 6725 if (requestedDepth === 0) { 6726 if (item._manualParentReference) { 6727 warn('MANUAL_TOP_PARENT_IGNORED', `Manual top-level item "${item.label}" has a Parent value; the Parent value was ignored.`); 6728 } 6729 roots.push(item); 6730 return; 6731 } 6732 6733 const parent = resolveManualParent(item._manualParentReference, index); 6734 if (!parent) { 6735 warn('MANUAL_PARENT_NOT_FOUND', `Manual navigation item "${item.label}" was skipped because parent "${item._manualParentReference}" was not found.`); 6736 item.invalid = true; 6737 return; 6738 } 6739 if (manualParentCreatesCycle(item, parent, manualByKey)) { 6740 warn('MANUAL_PARENT_CYCLE', `Manual navigation item "${item.label}" was skipped because its parent configuration creates a cycle.`); 6741 item.invalid = true; 6742 return; 6743 } 6744 6745 const requiredParentDepth = requestedDepth - 1; 6746 const parentDepth = Number(parent._manualRequestedDepth ?? parent.depth ?? 0); 6747 if (parentDepth !== requiredParentDepth) { 6748 warn('MANUAL_LEVEL_MISMATCH', `Manual navigation item "${item.label}" was skipped because its selected level does not immediately follow parent "${parent.label}".`); 6749 item.invalid = true; 6750 return; 6751 } 6752 6753 item.parentKey = parent.key; 6754 parent.children = parent.children || []; 6755 parent.children.push(item); 6756 parent.hasChildren = true; 6757 }); 6758 6759 const validRoots = roots.filter(item =>
6759 !item.invalid); 6760 let merged = insertManualRoots(baseItems, validRoots, customNavMode); 6761 merged = finalizeManualLayer(merged, 0, null, 0); 6762 6763 // On live/preview pages an empty folder has no useful navigation action, so 6764 // omit it. In the editor, retain it as a disabled preview row so changing a 6765 // manual item from Page to Folder produces an immediate visible response and 6766 // clearly indicates that a child still needs to be assigned. 6767 function removeEmptyFolders(items) { 6768 return (items || []).filter(item => { 6769 item.children = removeEmptyFolders(item.children || []); 6770 item.hasChildren = item.children.length > 0; 6771 if (item.source === 'manual' && item.type === 'folder' && !item.hasChildren) { 6772 if (isEditorCanvas()) { 6773 item._manualEmptyFolderPreview = true; 6774 return true; 6775 } 6776 warn('MANUAL_EMPTY_FOLDER', `Manual folder "${item.label}" was skipped because it has no valid children.`); 6777 return false; 6778 } 6779 return true; 6780 }); 6781 } 6782 6783 return removeEmptyFolders(merged); 6784 } 6785 6786 // --------------------------------------------------------------------------- 6787 // CUSTOMNAV mode and export-compatible normalization 6788 // --------------------------------------------------------------------------- 6789 6790 function normalizeCustomNavRows(rows) { 6791 const flat = []; 6792 const byKey = new Map(); 6793 (rows || []).forEach((row, index) => { 6794 const showRaw = field(row, 'SHOW ITEM', 'show_item', 'SHOW', 'show'); 6795 if (showRaw !== '' && showRaw !== undefined && showRaw !== null && !readBoolean(showRaw, true)) return; 6796 6797 const key = readString(field(row, 'ITEM KEY', 'item_key'), `custom:${index}`); 6798 if (byKey.has(key)) { 6799 warn('CUSTOMNAV_DUPLICATE_KEY', `CUSTOMNAV key "${key}" was used more than once; the later row was skipped.`); 6800 return; 6801 } 6802 const source = readString(field(row, 'SOURCE', 'source'), 'manual').toLowerCase(); 6803 const type = readString(field(row, 'TYPE', 'type'), 'page').toLowerCase() === 'folder' ? 'folder' : 'page'; 6804 const label = readString(field(row, 'PAGE NAME', 'LABEL', 'page_name', 'label'), ''); 6805 if (!label) return; 6806 const rawUrl = type === 'folder' ? '' : readString(field(row, 'URL', 'url'), ''); 6807 const url = source === 'manual' && rawUrl ? buildRuntimeFallbackUrl(rawUrl) : rawUrl; 6808 const item = { 6809 key, 6810 parentKey: readString(field(row, 'PARENT KEY', 'parent_key'), '') || null, 6811 source: ['native', 'collection', 'manual'].includes(source) ? source : 'manual', 6812 type, 6813 presentation: readString(field(row, 'PRESENTATION', 'presentation'), 'standard').toLowerCase() === 'mega' ? 'mega' : 'standard', 6814 megaGroup: Math.round(readNumber(field(row, 'MEGA GROUP', 'mega_group'), 0, 0, 4)), 6815 depth: Math.round(readNumber(field(row, 'DEPTH', 'depth'), 0, 0, 2)), 6816 order: Math.round(readNumber(field(row, 'ORDER', 'order'), index, 0, 100000)), 6817 label, 6818 slug: readString(field(row, 'SLUG', 'slug'), slugify(label)), 6819 canonicalPath: readString(field(row, 'CANONICAL PATH', 'canonical_path'), canonicalPath(url)), 6820 url, 6821 target: readString(field(row, 'TARGET', 'target'), '_self'), 6822 isExternal: readBoolean(field(row, 'IS EXTERNAL', 'is_external'), isExternalUrl(url)), 6823 hasChildren: false, 6824 children: [] 6825 }; 6826 flat.push(item); 6827 byKey.set(key, item); 6828 }); 6829 6830 const roots = []; 6831 flat.sort((a, b) => a.order - b.order).forEach(item => { 6832 if (!item.parentKey) { 6833 roots.push(item); 6834 return; 6835 } 6836 const parent = byKey.get(item.parentKey); 6837 if (!parent || parent === item) { 6838 warn('CUSTOMNAV_PARENT_INVALID', `CUSTOMNAV item "${item.label}" was skipped because its parent is invalid.`); 6839 item.invalid = true; 6840 return; 6841 } 6842 parent.children.push(item); 6843 parent.hasChildren = true; 6844 }); 6845 6846 function normalizeDepth(items, depth = 0, parentKey = null) { 6847 return items 6848 .filter(item =>
6848 !item.invalid) 6849 .sort((a, b) => a.order - b.order) 6850 .map(item => { 6851 if (depth > MAX_DEPTH) { 6852 warn('CUSTOMNAV_DEPTH_INVALID', `CUSTOMNAV item "${item.label}" was skipped because navigation supports three content levels.`); 6853 return null; 6854 } 6855 item.depth = depth; 6856 item.parentKey = parentKey; 6857 item.children = normalizeDepth(item.children || [], depth + 1, item.key); 6858 item.hasChildren = item.children.length > 0; 6859 return item; 6860 }) 6861 .filter(Boolean); 6862 } 6863 6864 return normalizeDepth(roots); 6865 } 6866 6867 // Re-resolve source-backed CUSTOMNAV links so preview/live URLs do not become stale. 6868 function refreshCustomUrls(items, categoryRows, coreRows) { 6869 const native = parseNativeNavigation(); 6870 const categoryIndex = buildDynamicIndex(categoryRows || []); 6871 const coreIndex = buildDynamicIndex(coreRows || []); 6872 function walk(item) { 6873 const copy = { ...item, children: item.children.map(walk) }; 6874 if (item.source === 'native' && item.canonicalPath) { 6875 const match = findTreeItem(native, candidate => candidate.canonicalPath === item.canonicalPath); 6876 if (match && match.url) copy.url = match.url; 6877 } 6878 if (item.source === 'collection' && item.slug) { 6879 const isHome = item.depth === 0 && ( 6880 readString(item.label, '').toLowerCase() === 'home' || 6881 canonicalPathWithoutHash(item.canonicalPath || item.url) === '/' 6882 ); 6883 6884 if (isHome) { 6885 copy.url = item.url || buildRuntimeFallbackUrl('/'); 6886 copy.canonicalPath = '/'; 6887 } else { 6888 const expectsCategory = usesCategorySource(item.depth, item.children || []); 6889 const index = expectsCategory ? categoryIndex : coreIndex; 6890 const prefix = expectsCategory ? CATEGORY_URL_PREFIX : CHILD_URL_PREFIX; 6891 const match = findDynamicMatch(index, item.label || item.slug, prefix); 6892 copy.url = match 6893 ? match.url 6894 : buildConfiguredDynamicUrl(item.label || item.slug, prefix); 6895 copy.canonicalPath = canonicalPath(copy.url); 6896 } 6897 } 6898 copy.hasChildren = copy.children.length > 0; 6899 return copy; 6900 } 6901 return items.map(walk); 6902 } 6903 6904 function findTreeItem(items, predicate) { 6905 for (const item of items || []) { 6906 if (predicate(item)) return item; 6907 const nested = findTreeItem(item.children, predicate); 6908 if (nested) return nested; 6909 } 6910 return null; 6911 } 6912 6913 6914 // Build a guaranteed fallback from 06 NAV MASTER alone. Structural items 6915 // with children use the Category prefix; leaf items use the Core prefix, even 6916 // when they are top-level or assigned to a Mega group. Category/Core 6917 // collections are still preferred for exact page_item_url values, but a 6918 // temporarily unavailable validation collection no longer blanks the nav. 6919 function normalizeNavMasterFallback(navRows) { 6920 const tree = buildCollectionTree(navRows || []); 6921 6922 function walk(node, ancestors, inheritedMegaGroup) { 6923 if (!node || node.invalid || node.depth > MAX_DEPTH) return null; 6924 6925 const megaGroup = node.depth === 0 6926 ? parseMegaGroup(node.topMegaValue) 6927 : inheritedMegaGroup; 6928 6929 const children = node.children 6930 .map(child => walk(child, [...ancestors, node], megaGroup)) 6931 .filter(Boolean); 6932 6933 const slug = slugify(node.label); 6934 const isHome = node.depth === 0 && node.label.toLowerCase() === 'home'; 6935 const expectsCategory = usesCategorySource(node.depth, children); 6936 const prefix = expectsCategory ? CATEGORY_URL_PREFIX : CHILD_URL_PREFIX; 6937 const configuredPageLink = node.pageLink ? buildRuntimeFallbackUrl(node.pageLink) : ''; 6938 const url = isHome 6939 ? (configuredPageLink || buildRuntimeFallbackUrl('/')) 6940 : buildConfiguredDynamicUrl(node.label, prefix); 6941 const hierarchySlugs = [...ancestors.map(item => slugify(item.label)), slug]; 6942 6943 return { 6944 key: `collection:${hierarchySlugs.join('/')}`, 6945 parentKey: ancestors.length 6946 ? `collection:${ancestors.map(item => slugify(item.label)).join('/')}` 6947 : null, 6948 source: 'collection', 6949 type: 'page', 6950 presentation: megaGroup ? 'mega' : 'standard', 6951 megaGroup, 6952 depth: node.depth, 6953 order: node.order, 6954 label: node.label, 6955 slug, 6956 canonicalPath: canonicalPath(url), 6957 url, 6958 target: '_self', 6959 isExternal: false, 6960 hasChildren: children.length > 0, 6961 children 6962 }; 6963 } 6964 6965 return tree.map(rootNode =>
6965 walk(rootNode, [], 0)).filter(Boolean); 6966 } 6967 6968 // --------------------------------------------------------------------------- 6969 // Group top-level mega families under one synthetic trigger per Mega group 6970 // --------------------------------------------------------------------------- 6971 6972 function shiftMegaFamilyDepth(item, parentKey, depthOffset = 1) { 6973 const nextDepth = Math.max(0, Number(item.depth) || 0) + depthOffset; 6974 const shifted = { 6975 ...item, 6976 parentKey, 6977 depth: nextDepth, 6978 presentation: 'mega', 6979 children: [] 6980 }; 6981 6982 shifted.children = (item.children || []).map(child => 6983 shiftMegaFamilyDepth(child, shifted.key, depthOffset) 6984 ); 6985 shifted.hasChildren = shifted.children.length > 0; 6986 return shifted; 6987 } 6988 6989 function groupMegaNavigationItems(items) { 6990 const sourceItems = Array.isArray(items) ? items : []; 6991 const groupedFamilies = new Map(); 6992 const firstIndexes = new Map(); 6993 6994 sourceItems.forEach((item, index) => { 6995 const groupNumber = Math.round(Number(item && item.megaGroup) || 0); 6996 const isSyntheticGroup = readString(item && item.key, '').startsWith('mega-group:'); 6997 if (groupNumber < 1 || groupNumber > 4 || isSyntheticGroup) return; 6998 6999 if (!groupedFamilies.has(groupNumber)) { 7000 groupedFamilies.set(groupNumber, []); 7001 firstIndexes.set(groupNumber, index); 7002 } 7003 groupedFamilies.get(groupNumber).push(item); 7004 }); 7005 7006 if (!groupedFamilies.size) return sourceItems; 7007 7008 const output = []; 7009 const insertedGroups = new Set(); 7010 7011 sourceItems.forEach((item, index) => { 7012 const groupNumber = Math.round(Number(item && item.megaGroup) || 0); 7013 const isSyntheticGroup = readString(item && item.key, '').startsWith('mega-group:'); 7014 7015 if (groupNumber >= 1 && groupNumber <= 4 && !isSyntheticGroup) { 7016 if (insertedGroups.has(groupNumber)) return; 7017 if (firstIndexes.get(groupNumber) !== index) return; 7018 7019 const key = `mega-group:${groupNumber}`; 7020 const megaCfg = getMegaConfig(groupNumber); 7021 const sourceFamilies = [...(groupedFamilies.get(groupNumber) || [])]; 7022 if (megaCfg.alphabetize) { 7023 sourceFamilies.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' })); 7024 } 7025 const families = sourceFamilies.map(family => 7026 shiftMegaFamilyDepth(family, key, 1) 7027 ); 7028 7029 output.push({ 7030 key, 7031 parentKey: null, 7032 source: 'generated', 7033 type: 'folder', 7034 presentation: 'mega', 7035 megaGroup: groupNumber, 7036 depth: 0, 7037 order: index, 7038 label: megaCfg.label, 7039 slug: slugify(megaCfg.label), 7040 canonicalPath: '', 7041 url: '', 7042 target: '_self', 7043 isExternal: false, 7044 hasChildren: families.length > 0, 7045 children: families 7046 }); 7047 insertedGroups.add(groupNumber); 7048 return; 7049 } 7050 7051 output.push(item); 7052 }); 7053 7054 return output; 7055 } 7056 7057 // --------------------------------------------------------------------------- 7058 // Build and publish the final shared model 7059 // --------------------------------------------------------------------------- 7060 7061 async function buildNavigationModel() { 7062 local.latestBuildWarnings = []; 7063 local.slugValidationWarnings = []; 7064 const fetched = await fetchRequiredCollections(); 7065 const collections = fetched.collections; 7066 let hadError = fetched.hadError; 7067 let model; 7068 7069 if (readBoolean(configValueFrom(cfg, 'useCustomNavOnly'), false)) { 7070 model = normalizeCustomNavRows(collections.CUSTOMNAV || []); 7071 // Optional URL refresh data is fetched when any CUSTOMNAV item at any 7072 // hierarchy level is backed by a native or collection source. 7073 const needsRefresh = Boolean(findTreeItem(model, item => item.source !== 'manual')); 7074 if (needsRefresh) { 7075 const secondary = await Promise.allSettled([fetchCollection('02 CATEGORY'), fetchCollection('03 CORE')]); 7076 const categories = secondary[0].status === 'fulfilled' ? secondary[0].value : []; 7077 const cores = secondary[1].status === 'fulfilled' ? secondary[1].value : []; 7078 if (secondary.some(result => result.status === 'rejected')) { 7079 hadError = true; 7080 warn('CUSTOMNAV_URL_REFRESH_FAILED', 'One or more source collections could not be loaded while refreshing CUSTOMNAV URLs.'); 7081 } 7082 model = refreshCustomUrls(model, categories, cores); 7083 } 7084 } else { 7085 const navMasterRows = collections['06 NAV MASTER'] || []; 7086 let collectionItems; 7087 7088 if (hadError && navMasterRows.length) {
7089 // Do not report missing slugs when one of the source collections failed 7090 // to load; absence cannot be confirmed until both collections are available. 7091 local.slugValidationWarnings = []; 7092 warn( 7093 'COLLECTION_VALIDATION_UNAVAILABLE', 7094 'One or more validation collections could not be loaded. Rendering NAV MASTER with constructed URLs for this build.' 7095 ); 7096 collectionItems = normalizeNavMasterFallback(navMasterRows); 7097 } else { 7098 collectionItems = normalizeCollectionNavigation( 7099 navMasterRows, 7100 collections['02 CATEGORY'] || [], 7101 collections['03 CORE'] || [] 7102 ); 7103 7104 if (!collectionItems.length && navMasterRows.length) { 7105 warn( 7106 'VALIDATION_FALLBACK', 7107 'Category/Core validation produced no items. Rendering NAV MASTER with constructed URLs instead.' 7108 ); 7109 collectionItems = normalizeNavMasterFallback(navMasterRows); 7110 } 7111 } 7112 7113 const nativeItems = normalizeConfiguredNativeNavigation(); 7114 model = [...collectionItems, ...nativeItems]; 7115 } 7116 7117 // Panel-created manual items are additive in both modes. In CUSTOMNAV-only 7118 // mode they are layered on top of the CUSTOMNAV collection; in normal mode 7119 // they are layered on top of Collection + Native navigation. 7120 model = layerManualNavigationItems( 7121 model, 7122 readBoolean(configValueFrom(cfg, 'useCustomNavOnly'), false) 7123 ); 7124 7125 // A Mega value assigns a top-level family to a shared group. The visible 7126 // trigger is the configured Mega label, while assigned parents become 7127 // families inside that single dropdown. Yes and Mega1 both map to group 1. 7128 model = groupMegaNavigationItems(model); 7129 7130 return { 7131 model, 7132 hadError, 7133 warnings: local.latestBuildWarnings, 7134 slugWarnings: local.slugValidationWarnings 7135 }; 7136 } 7137 7138 function isCurrentInstance() { 7139 const ownerKey = widgetMode === 'full' ? 'ownerId' : 'mirrorOwnerId'; 7140 return !local.cleaned 7141 && root.dataset.nmwInstance === instanceId 7142 && window.NMW[ownerKey] === instanceId; 7143 } 7144 7145 async function runRebuild() { 7146 if (!isCurrentInstance()) return; 7147 if (local.rebuildRunning) { 7148 local.pendingRebuild = true; 7149 return; 7150 } 7151 7152 local.rebuildRunning = true; 7153 window.NMW.loading = true; 7154 updateRootStatus(); 7155 dispatchStateChanged(); 7156 7157 try { 7158 const result = await buildNavigationModel(); 7159 7160 // A newer Duda panel execution may have replaced this instance while the 7161 // asynchronous collection requests were running. Never allow a stale 7162 // build to render or alter shared state. 7163 if (!isCurrentInstance()) return; 7164 7165 const nextData = result.model || []; 7166 const previousSignature = stableStringify(window.NMW.data || []); 7167 const nextSignature = stableStringify(nextData); 7168 const dataChanged = previousSignature !== nextSignature; 7169 7170 const nextFullConfig = cloneSerializable(cfg) || {}; 7171 let nextConfigSignature = ''; 7172 try { nextConfigSignature = stableStringify(nextFullConfig); } 7173 catch (error) { nextConfigSignature = String(Date.now()); } 7174 const presentationChanged = window.NMW.fullConfigSignature !== nextConfigSignature; 7175 7176 window.NMW.error = result.hadError; 7177 window.NMW.warnings = result.warnings; 7178 window.NMW.slugWarnings = Array.isArray(result.slugWarnings) ? result.slugWarnings : []; 7179 window.NMW.ready = true; 7180 window.NMW.data = nextData; 7181 window.NMW.fullConfig = nextFullConfig; 7182 window.NMW.fullConfigSignature = nextConfigSignature; 7183 7184 // Always render the current Full instance. Duda Design/Content Panel 7185 // changes such as Mega columns, expanded/collapsed families, max width, 7186 // hover behavior, or tool visibility may not alter the normalized model. 7187 // Skipping render when data is unchanged is what previously caused panel 7188 // changes to appear stuck until a refresh. This also replaces any 7189 // NAV-MASTER-only provisional render with the final validated model. 7190 root.removeAttribute('data-nmw-provisional'); 7191 renderNavigation(nextData, 'full'); 7192 7193 if (dataChanged || presentationChanged) { 7194 window.NMW.version += 1; 7195 if (presentationChanged) window.NMW.presentationVersion += 1; 7196 dispatchDataChanged(); 7197 } 7198 } catch (error) { 7199 if (!isCurrentInstance()) return; 7200 window.NMW.error = true; 7201 window.NMW.ready = true; 7202 if (!Array.isArray(window.NMW.data)) window.NMW.data = []; 7203 warn('BUILD_FAILED', 'Navigation build failed.', error); 7204 if (!window.NMW.data.length && renderHost) { 7205 renderHost.innerHTML = `<div class="nmw-empty">Navigation build failed: ${escapeHtml(error && error.message ? error.message :
7205String(error))}</div>`; 7206 } 7207 } finally { 7208 local.rebuildRunning = false; 7209 7210 // Only the currently registered instance may clear the shared loading 7211 // state or launch a queued rebuild. This prevents a replaced instance 7212 // from leaving the editor stuck on Loading navigationâ¦. 7213 if (isCurrentInstance()) { 7214 window.NMW.loading = false; 7215 updateRootStatus(); 7216 updateTools(); 7217 dispatchStateChanged(); 7218 if (local.pendingRebuild) { 7219 local.pendingRebuild = false; 7220 runRebuild(); 7221 } 7222 } 7223 } 7224 } 7225 7226 function scheduleRebuild(immediate = false) { 7227 if (local.cleaned) return; 7228 clearTimeout(local.rebuildTimer); 7229 if (immediate) { 7230 runRebuild(); 7231 return; 7232 } 7233 local.rebuildTimer = setTimeout(runRebuild, REBUILD_DEBOUNCE); 7234 } 7235 7236 // --------------------------------------------------------------------------- 7237 // Renderers 7238 // --------------------------------------------------------------------------- 7239 7240 function itemClasses(item) { 7241 return [ 7242 'nmw-item', 7243 `nmw-source-${item.source}`, 7244 `nmw-type-${item.type}`, 7245 `nmw-presentation-${item.presentation}`, 7246 `nmw-depth-${item.depth}`, 7247 item.megaGroup ? `nmw-mega-group-${item.megaGroup}` : '' 7248 ].filter(Boolean).join(' '); 7249 } 7250 7251 function linkHtml(item, mode, extraClass = '') { 7252 const editorDisabled = isEditorCanvas() && !isSiteCommentsCustomerMode(); 7253 const inheritedClasses = readString(item.linkClassName, '') 7254 .split(/\s+/) 7255 .filter(className => className && className !== 'nmw-link') 7256 .join(' '); 7257 const attributes = item.linkAttributes && typeof item.linkAttributes === 'object' 7258 ? Object.entries(item.linkAttributes).map(([name, value]) => { 7259 const safeName = String(name).trim(); 7260 if (!/^(?:data-[\w:-]+|aria-[\w:-]+|rel|download|title|name|type|onclick)$/i.test(safeName)) return ''; 7261 return value === '' ? ` ${safeName}` : ` ${safeName}="${escapeHtml(value)}"`; 7262 }).join('') 7263 : ''; 7264 const className = ['nmw-link', extraClass, inheritedClasses].filter(Boolean).join(' '); 7265 return `<a class="${escapeHtml(className)}" href="${escapeHtml(item.url)}" target="${escapeHtml(item.target || '_self')}" data-nmw-key="${escapeHtml(item.key)}" data-nmw-path="${escapeHtml(item.canonicalPath)}"${attributes}${editorDisabled ? ' data-nmw-editor-disabled="true"' : ''}><span class="nmw-label">${escapeHtml(item.label)}</span></a>`; 7266 } 7267 7268 function toggleHtml(item, submenuId, folder = false, megaFamily = false) { 7269 const label = folder ? item.label : `${item.label} submenu`; 7270 const className = folder ? 'nmw-folder-button' : `nmw-toggle${megaFamily ? ' nmw-mega-family-toggle' : ''}`; 7271 return `<button class="${className}" type="button" data-nmw-toggle data-nmw-key="${escapeHtml(item.key)}" aria-label="Toggle ${escapeHtml(label)}" aria-expanded="false" aria-controls="${escapeHtml(submenuId)}"><span class="nmw-label">${folder ? escapeHtml(item.label) : ''}</span><span class="nmw-toggle-icon" aria-hidden="true"></span></button>`; 7272 } 7273 7274 function emptyFolderPreviewHtml(item) { 7275 return `<button class="nmw-folder-button nmw-empty-folder-preview" type="button" disabled aria-disabled="true" title="Add a valid child item to this folder"><span class="nmw-label">${escapeHtml(item.label)}</span><span class="nmw-empty-folder-note" aria-hidden="true">Add child</span></button>`; 7276 } 7277
7278 function renderStandardItem(item, mode, idState) { 7279 const submenuId = item.hasChildren ? `nmw-${mode}-submenu-${++idState.value}` : ''; 7280 let row; 7281 if (item.type === 'folder' && item._manualEmptyFolderPreview) { 7282 row = `<div class="nmw-row">${emptyFolderPreviewHtml(item)}</div>`; 7283 } else if (item.type === 'folder') { 7284 row = `<div class="nmw-row">${toggleHtml(item, submenuId, true)}</div>`; 7285 } else if (item.hasChildren) { 7286 row = `<div class="nmw-row">${linkHtml(item, mode)}${toggleHtml(item, submenuId, false)}</div>`; 7287 } else { 7288 row = `<div class="nmw-row">${linkHtml(item, mode)}</div>`; 7289 } 7290 7291 const children = item.hasChildren 7292 ? `<ul class="nmw-submenu nmw-standard-submenu nmw-depth-${item.depth + 1}" id="${submenuId}" data-nmw-parent-key="${escapeHtml(item.key)}" hidden>${item.children.map(child => renderStandardItem(child, mode, idState)).join('')}</ul>` 7293 : ''; 7294 return `<li class="${itemClasses(item)}" data-nmw-item data-nmw-key="${escapeHtml(item.key)}">${row}${children}</li>`; 7295 } 7296 7297 function familySize(item) { 7298 return 1 + (item.children || []).reduce((sum, child) => sum + familySize(child), 0); 7299 } 7300 7301 function distributeFamilies(families, columns) { 7302 const output = Array.from({ length: columns }, () => ({ weight: 0, items: [] })); 7303 families.forEach(family => { 7304 let targetIndex = 0; 7305 for (let index = 1; index < output.length; index += 1) { 7306 if (output[index].weight < output[targetIndex].weight) targetIndex = index; 7307 } 7308 output[targetIndex].items.push(family); 7309 output[targetIndex].weight += familySize(family); 7310 }); 7311 return output.map(column => column.items); 7312 } 7313 7314 function renderMegaGrandchild(item, mode) { 7315 const content = item.type === 'folder' 7316 ? `<span class="nmw-mega-grandchild-label"><span class="nmw-label">${escapeHtml(item.label)}</span></span>` 7317 : linkHtml(item, mode, 'nmw-mega-grandchild-link'); 7318 return `<li class="${itemClasses(item)} nmw-mega-grandchild" data-nmw-item data-nmw-key="${escapeHtml(item.key)}"><div class="nmw-row">${content}</div></li>`; 7319 } 7320 7321 function renderMegaChild(item, mode) { 7322 const grandchildren = item.children || []; 7323 const content = item.type === 'folder' 7324 ? `<span class="nmw-mega-child-label"><span class="nmw-label">${escapeHtml(item.label)}</span></span>` 7325 : linkHtml(item, mode, 'nmw-mega-child-link'); 7326 const nested = grandchildren.length 7327 ? `<ul class="nmw-mega-grandchildren" data-nmw-parent-key="${escapeHtml(item.key)}">${grandchildren.map(child => renderMegaGrandchild(child, mode)).join('')}</ul>` 7328 : ''; 7329 return `<li class="${itemClasses(item)} nmw-mega-child${grandchildren.length ? ' nmw-has-grandchildren' : ''}" data-nmw-item data-nmw-key="${escapeHtml(item.key)}"><div class="nmw-row">${content}</div>${nested}</li>`; 7330 } 7331 7332 function renderMegaFamily(family, mode, idState, expandAll) { 7333 const hasChildren = family.children.length > 0; 7334 const familyId = hasChildren ? `nmw-${mode}-mega-family-${++idState.value}` : ''; 7335 const familyLink = family.type === 'page' ? linkHtml(family, mode, 'nmw-mega-family-link') : ''; 7336 let row; 7337 7338 if (!hasChildren || expandAll) { 7339 row = `<div class="nmw-row">${family.type === 'folder' ? `<span class="nmw-folder-label"><span class="nmw-label">${escapeHtml(family.label)}</span></span>` : familyLink}</div>`; 7340 } else if (family.type === 'folder') { 7341 row = `<div class="nmw-row">${toggleHtml(family, familyId, true, true)}</div>`; 7342 } else { 7343 row = `<div class="nmw-row">${familyLink}${toggleHtml(family, familyId, false, true)}</div>`; 7344 } 7345 7346 const children = hasChildren 7347 ? `<ul class="nmw-mega-family-children${expandAll ? ' nmw-open' : ''}" id="${familyId}" data-nmw-parent-key="${escapeHtml(family.key)}"${expandAll ? '' : ' hidden'}>${family.children.map(child => renderMegaChild(child, mode)).join('')}</ul>` 7348 : ''; 7349 7350 return `<div class="nmw-mega-family${expandAll ? ' nmw-open' : ''}" data-nmw-item data-nmw-key="${escapeHtml(family.key)}">${row}${children}</div>`; 7351 } 7352 7353 function renderMegaItem(item, mode, idState) { 7354 const submenuId = `nmw-${mode}-mega-${++idState.value}`;
7355 const row = item.type === 'folder' 7356 ? `<div class="nmw-row">${toggleHtml(item, submenuId, true)}</div>` 7357 : `<div class="nmw-row">${linkHtml(item, mode)}${toggleHtml(item, submenuId, false)}</div>`; 7358 const megaCfg = getMegaConfig(item.megaGroup || 1); 7359 7360 let content; 7361 if (mode === 'mirror') { 7362 // Mirror always uses a single vertical list â layout setting does not apply. 7363 content = `<div class="nmw-mega-vertical">${item.children.map(family => renderMegaFamily(family, mode, idState, megaCfg.expandAll)).join('')}</div>`; 7364 } else if (megaCfg.layout === 'masonry') { 7365 // Masonry: shortest-column balancing. distributeFamilies() assigns each 7366 // family to the column with the lowest accumulated navigation weight so 7367 // differently sized families remain visually balanced across columns. 7368 // Each resulting group is wrapped in a .nmw-mega-column flex container. 7369 const columns = distributeFamilies(item.children, megaCfg.columns); 7370 content = `<div class="nmw-mega-inner" style="--nmw-mega-max-width:${megaCfg.maxWidth}px;--nmw-mega-columns:${megaCfg.columns}" data-nmw-mega-layout="masonry"><div class="nmw-mega-columns">${columns.map(column => `<div class="nmw-mega-column">${column.map(family => renderMegaFamily(family, mode, idState, megaCfg.expandAll)).join('')}</div>`).join('')}</div></div>`; 7371 } else { 7372 // Grid: families are placed directly inside the CSS Grid container in 7373 // their current (or alphabetized) source order. CSS grid-auto-flow: row 7374 // fills each row from left to right before beginning the next row. 7375 // No column wrappers are emitted â each .nmw-mega-family is a direct 7376 // grid child and occupies exactly one grid cell. 7377 const families = item.children 7378 .map(family => renderMegaFamily(family, mode, idState, megaCfg.expandAll)) 7379 .join(''); 7380 content = `<div class="nmw-mega-inner" style="--nmw-mega-max-width:${megaCfg.maxWidth}px;--nmw-mega-columns:${megaCfg.columns}" data-nmw-mega-layout="grid"><div class="nmw-mega-columns">${families}</div></div>`; 7381 } 7382 7383 return `<li class="${itemClasses(item)}" data-nmw-item data-nmw-key="${escapeHtml(item.key)}" data-nmw-mega-label="${escapeHtml(megaCfg.label)}">${row}<div class="nmw-submenu nmw-mega-menu" id="${submenuId}" data-nmw-parent-key="${escapeHtml(item.key)}" data-nmw-mega-group="${item.megaGroup || 1}" data-nmw-mega-label="${escapeHtml(megaCfg.label)}" aria-label="${escapeHtml(megaCfg.label)}" hidden>${content}</div></li>`; 7384 } 7385 7386 function renderNavigation(items, mode) { 7387 if (!renderHost) return; 7388 applyDesignSettings(mode); 7389 updateMegaIndicators(items); 7390 if (!Array.isArray(items) || !items.length) { 7391 renderHost.innerHTML = '<div class="nmw-empty">Navigation could not be built. Check collection data and native navigation configuration.</div>'; 7392 updateTools(); 7393 return; 7394 } 7395 const idState = { value: 0 }; 7396 const navId = mode === 'full' ? 'nmw-full-main-nav' : `nmw-mirror-main-nav-${instanceId}`; 7397 const list = items.map(item => item.presentation === 'mega' 7398 ? renderMegaItem(item, mode, idState) 7399 : renderStandardItem(item, mode, idState)).join(''); 7400 if (loadingHost && loadingHost !== renderHost) loadingHost.hidden = true; 7401 renderHost.innerHTML = `<nav class="nmw-nav nmw-topnav" id="${escapeHtml(navId)}" aria-label="Main navigation"><ul class="nmw-list nmw-top-list">${list}</ul></nav>`; 7402 closeAllMenus(root, false); 7403 refreshActiveStates(root); 7404 updateTools(); 7405 } 7406 7407 // --------------------------------------------------------------------------- 7408 // Menu opening, closing, animation, hover, and delegated interaction 7409 // --------------------------------------------------------------------------- 7410 7411 function findControlledSubmenu(toggle) { 7412 const id = toggle?.getAttribute('aria-controls'); 7413 return id ? root.querySelector(`#${CSS.escape(id)}`) : null; 7414 } 7415 7416 function getTransitionMs(elementNode) { 7417 if (!elementNode || getBehaviorConfig(widgetMode).animationsEnabled === false) return 0; 7418 const style = window.getComputedStyle(elementNode); 7419 if (style.transitionProperty === 'none') return 0; 7420 const durations = style.transitionDuration.split(',').map(value => parseFloat(value) * (value.includes('ms') ? 1 : 1000)); 7421 const delays = style.transitionDelay.split(',').map(value => parseFloat(value) * (value.includes('ms') ? 1 : 1000)); 7422 const measured = Math.max(0, ...durations.map((duration, index) => duration + (delays[index] || delays[0] || 0))); 7423 return measured > 0 ? measured : 0; 7424 } 7425 7426 function cancelCloseJob(submenu) { 7427 const job = local.closeJobs.get(submenu); 7428 if (!job) return; 7429 clearTimeout(job.timer); 7430 local.closeTimers.delete(job.timer); 7431 submenu.removeEventListener('transitionend', job.finish); 7432 local.closeJobs.delete(submenu); 7433 } 7434 7435 function updateFlyoutDirection(submenu) { 7436 if (!submenu || widgetMode === 'mirror' || !submenu.classList.contains('nmw-standard-submenu')) return; 7437 const nested = submenu.parentElement?.closest?.('.nmw-standard-submenu'); 7438 if (!nested) { 7439 submenu.classList.remove('nmw-flip-left'); 7440 return; 7441 } 7442 7443 submenu.classList.remove('nmw-flip-left'); 7444 const rect = submenu.getBoundingClientRect(); 7445 const viewportWidth = document.documentElement.clientWidth || window.innerWidth || 0; 7446 if (rect.right > viewportWidth - 8) submenu.classList.add('nmw-flip-left'); 7447 } 7448 7449 function openMenu(item, toggle, submenu) { 7450 if (!item || !toggle || !submenu) return; 7451 cancelCloseJob(submenu); 7452 closeSiblingMenus(item); 7453 item.classList.remove('nmw-closing'); 7454 submenu.classList.remove('nmw-closing'); 7455 submenu.hidden = false;
7456 requestAnimationFrame(() => { 7457 if (local.cleaned || !document.documentElement.contains(submenu)) return; 7458 updateFlyoutDirection(submenu); 7459 item.classList.add('nmw-open'); 7460 submenu.classList.add('nmw-open'); 7461 toggle.setAttribute('aria-expanded', 'true'); 7462 }); 7463 } 7464 7465 function closeMenu(item, toggle, submenu, returnFocus = false) { 7466 if (!item || !toggle || !submenu || submenu.hidden) return; 7467 cancelCloseJob(submenu); 7468 item.classList.remove('nmw-open'); 7469 submenu.classList.remove('nmw-open'); 7470 item.classList.add('nmw-closing'); 7471 submenu.classList.add('nmw-closing'); 7472 toggle.setAttribute('aria-expanded', 'false'); 7473 7474 let jobTimer = null; 7475 const finish = event => { 7476 if (event && event.target !== submenu) return; 7477 submenu.hidden = true; 7478 item.classList.remove('nmw-closing'); 7479 submenu.classList.remove('nmw-closing'); 7480 submenu.removeEventListener('transitionend', finish); 7481 local.closeJobs.delete(submenu); 7482 if (jobTimer) local.closeTimers.delete(jobTimer); 7483 if (returnFocus) toggle.focus(); 7484 }; 7485 const duration = getTransitionMs(submenu); 7486 if (!duration) { 7487 finish(); 7488 return; 7489 } 7490 submenu.addEventListener('transitionend', finish); 7491 jobTimer = setTimeout(finish, duration + 50); 7492 local.closeTimers.add(jobTimer); 7493 local.closeJobs.set(submenu, { timer: jobTimer, finish }); 7494 } 7495 7496 function closeSiblingMenus(item) { 7497 let siblings = []; 7498 7499 if (item.classList.contains('nmw-mega-family')) { 7500 const megaMenu = item.closest('.nmw-mega-menu'); 7501 siblings = megaMenu 7502 ? Array.from(megaMenu.querySelectorAll('.nmw-mega-family[data-nmw-item]')) 7503 : []; 7504 } else { 7505 const parent = item.parentElement; 7506 siblings = parent ? Array.from(parent.children).filter(node => node.matches?.('[data-nmw-item]')) : []; 7507 } 7508 7509 siblings.forEach(sibling => { 7510 if (sibling === item) return; 7511 const toggle = sibling.querySelector(':scope > .nmw-row [data-nmw-toggle]'); 7512 const submenu = toggle ? findControlledSubmenu(toggle) : null; 7513 if (toggle && submenu) closeMenu(sibling, toggle, submenu, false); 7514 }); 7515 } 7516 7517 function closeAllMenus(scope, returnFocus = false) {
7518 scope.querySelectorAll('[data-nmw-toggle][aria-expanded="true"]').forEach(toggle => { 7519 const item = toggle.closest('[data-nmw-item]'); 7520 const submenu = findControlledSubmenu(toggle); 7521 if (item && submenu) closeMenu(item, toggle, submenu, returnFocus); 7522 }); 7523 local.hoverTimers.forEach(timer => clearTimeout(timer)); 7524 local.hoverTimers.clear(); 7525 } 7526 7527 function toggleMenu(toggle) { 7528 const item = toggle.closest('[data-nmw-item]'); 7529 const submenu = findControlledSubmenu(toggle); 7530 if (!item || !submenu) return; 7531 const isOpen = toggle.getAttribute('aria-expanded') === 'true'; 7532 if (isOpen) closeMenu(item, toggle, submenu, false); 7533 else openMenu(item, toggle, submenu); 7534 } 7535 7536 function nearestOpenToggle(target) { 7537 let node = target && target.closest ? target.closest('[data-nmw-item], .nmw-submenu, .nmw-mega-family-children') : null; 7538 7539 while (node && root.contains(node)) { 7540 if (node.matches?.('[data-nmw-item]')) { 7541 const ownToggle = node.querySelector(':scope > .nmw-row [data-nmw-toggle][aria-expanded="true"]'); 7542 if (ownToggle) return ownToggle; 7543 } 7544 7545 const submenu = node.matches?.('.nmw-submenu, .nmw-mega-family-children') 7546 ? node 7547 : node.closest?.('.nmw-submenu, .nmw-mega-family-children'); 7548 if (!submenu) return null; 7549 7550 const parentKey = submenu.getAttribute('data-nmw-parent-key'); 7551 if (parentKey) { 7552 const parentToggle = root.querySelector(`[data-nmw-toggle][data-nmw-key="${CSS.escape(parentKey)}"][aria-expanded="true"]`); 7553 if (parentToggle) return parentToggle; 7554 } 7555 7556 node = submenu.parentElement?.closest?.('[data-nmw-item], .nmw-submenu, .nmw-mega-family-children') || null; 7557 } 7558 7559 return null; 7560 } 7561 7562 function attachDelegatedInteractions() { 7563 if (local.listenersAttached) return; 7564 local.listenersAttached = true; 7565 7566 listen(root, 'click', event => { 7567 const toggle = event.target.closest('[data-nmw-toggle]'); 7568 if (toggle && root.contains(toggle)) { 7569 event.preventDefault(); 7570 toggleMenu(toggle); 7571 return; 7572 } 7573 7574 const link = event.target.closest('a.nmw-link'); 7575 if (!link || !root.contains(link)) return; 7576 7577 /* 7578 Recheck Site Comments at click time. 7579 7580 The navigation may have rendered before Duda finished identifying 7581 the visitor as a customer. In that case the link can still contain 7582 a stale data-nmw-editor-disabled attribute even though the console 7583 later reports "Customer Detected". 7584 */ 7585 if (isSiteCommentsCustomerMode()) { 7586 /* 7587 Remove stale editor-disabled markers from every rendered link so 7588 the Full and Mirror navigation instances immediately reflect the 7589 confirmed customer state. 7590 */ 7591 root 7592 .querySelectorAll( 7593 'a.nmw-link[data-nmw-editor-disabled="true"]' 7594 ) 7595 .forEach(customerLink => { 7596 customerLink.removeAttribute( 7597 'data-nmw-editor-disabled' 7598 ); 7599 }); 7600 7601 /* 7602 Same-site links use the existing Site Comments top-window 7603 navigation. Links intentionally excluded by that functionâ 7604 anchors, target="_blank", external sites, telephone, email, 7605 SMS, downloads, and similar linksâretain normal browser behavior. 7606 */ 7607 // Same-page anchors are handled inside the current site document so 7608 // repeated anchor selections work without reloading the Site Comments shell. 7609 if (handleSamePageAnchorClick(link, event)) return; 7610 7611 if (navigateSiteComments(link, event)) return; 7612 7613 closeAllMenus(root, false); 7614 return; 7615 } 7616 7617 /* 7618 Outside confirmed customer Site Comments mode, preserve the 7619 original editor safety behavior. 7620 */ 7621 if (link.dataset.nmwEditorDisabled === 'true') { 7622 event.preventDefault(); 7623 return; 7624 } 7625 7626 // Explicitly own same-page anchor navigation. This fixes repeated anchor 7627 // selections on the same page and corrects the final position for sticky 7628 // or fixed headers. Cross-page anchors continue with normal navigation. 7629 if (handleSamePageAnchorClick(link, event)) return; 7630 7631 closeAllMenus(root, false); 7632 }); 7633 7634 listen(root, 'keydown', event => { 7635 if (event.key !== 'Escape') return; 7636 const toggle = nearestOpenToggle(event.target); 7637 if (!toggle) return; 7638 event.preventDefault();
7639 const item = toggle.closest('[data-nmw-item]'); 7640 const submenu = findControlledSubmenu(toggle); 7641 if (item && submenu) closeMenu(item, toggle, submenu, true); 7642 }); 7643 7644 listen(root, 'focusout', event => { 7645 requestAnimationFrame(() => { 7646 if (local.cleaned) return; 7647 if (!root.contains(document.activeElement)) closeAllMenus(root, false); 7648 }); 7649 }); 7650 7651 // Hover uses the same JavaScript-owned state as click. CSS never opens menus 7652 // independently, which keeps hidden and aria-expanded synchronized. 7653 listen(root, 'pointerover', event => { 7654 if (!getBehaviorConfig(widgetMode).hoverEnabled || event.pointerType === 'touch') return; 7655 const item = event.target.closest('[data-nmw-item]'); 7656 if (!item || !root.contains(item)) return; 7657 const toggle = item.querySelector(':scope > .nmw-row [data-nmw-toggle]'); 7658 const submenu = toggle ? findControlledSubmenu(toggle) : null; 7659 if (!toggle || !submenu) return; 7660 const timer = local.hoverTimers.get(item); 7661 if (timer) clearTimeout(timer); 7662 local.hoverTimers.delete(item); 7663 openMenu(item, toggle, submenu); 7664 }); 7665 7666 listen(root, 'pointerout', event => { 7667 if (!getBehaviorConfig(widgetMode).hoverEnabled || event.pointerType === 'touch') return; 7668 const item = event.target.closest('[data-nmw-item]'); 7669 if (!item || !root.contains(item)) return; 7670 const related = event.relatedTarget; 7671 if (related && item.contains(related)) return; 7672 const toggle = item.querySelector(':scope > .nmw-row [data-nmw-toggle]'); 7673 const submenu = toggle ? findControlledSubmenu(toggle) : null; 7674 if (!toggle || !submenu) return; 7675 const timer = setTimeout(() => { 7676 if (!item.matches(':hover')) closeMenu(item, toggle, submenu, false); 7677 local.hoverTimers.delete(item); 7678 }, HOVER_CLOSE_DELAY); 7679 local.hoverTimers.set(item, timer); 7680 }); 7681 7682 listen(document, 'pointerdown', handleOutsidePointer, true); 7683 } 7684 7685 function handleOutsidePointer(event) { 7686 if (!root.contains(event.target)) closeAllMenus(root, false); 7687 } 7688 7689 // --------------------------------------------------------------------------- 7690 // Active-page state from the actual current URL 7691 // --------------------------------------------------------------------------- 7692 7693 function currentComparablePath() { 7694 return canonicalPath(window.location.href); 7695 } 7696 7697 function refreshActiveStates(scope) { 7698 const current = currentComparablePath(); 7699 scope.querySelectorAll('a[data-nmw-path]').forEach(link => { 7700 const path = link.getAttribute('data-nmw-path') || ''; 7701 const active = path && path === current; 7702 link.classList.toggle('nmw-active', active); 7703 if (active) link.setAttribute('aria-current', 'page'); 7704 else link.removeAttribute('aria-current'); 7705 });
7706 scope.querySelectorAll('[data-nmw-item]').forEach(item => { 7707 const ownLink = item.querySelector(':scope > .nmw-row a.nmw-active'); 7708 const activeDescendant = item.querySelector('.nmw-submenu a.nmw-active, .nmw-mega-family-children a.nmw-active'); 7709 item.classList.toggle('nmw-child-active', !ownLink && Boolean(activeDescendant)); 7710 }); 7711 } 7712 7713 function observeHistoryChanges() { 7714 const marker = '__nmwHistoryWrapped'; 7715 if (history[marker]) return () => {}; 7716 history[marker] = true; 7717 const originalPush = history.pushState; 7718 const originalReplace = history.replaceState; 7719 const notify = () => window.dispatchEvent(new Event('nmw:url-changed')); 7720 history.pushState = function (...args) { 7721 const result = originalPush.apply(this, args); 7722 notify(); 7723 return result; 7724 }; 7725 history.replaceState = function (...args) { 7726 const result = originalReplace.apply(this, args); 7727 notify(); 7728 return result; 7729 }; 7730 return () => {}; 7731 } 7732 7733 function handleUrlChanged() { 7734 refreshActiveStates(root); 7735 } 7736 7737 // --------------------------------------------------------------------------- 7738 // Manual-item source observation 7739 // --------------------------------------------------------------------------- 7740 7741 function attachManualSourceObserver() { 7742 const source = root.querySelector('[data-nmw-manual-source]'); 7743 if (!source || local.manualObserver) return false; 7744 7745 local.manualObserver = new MutationObserver(mutations => { 7746 const relevant = mutations.some(mutation => { 7747 if (mutation.type === 'childList') return true; 7748 if (mutation.type !== 'attributes') return false; 7749 const name = mutation.attributeName || ''; 7750 return name === 'href' 7751 || name === 'target' 7752 || name === 'class' 7753 || name.startsWith('data-manual-') 7754 || name.startsWith('data-') 7755 || name.startsWith('aria-'); 7756 }); 7757 7758 if (relevant) scheduleRebuild(false); 7759 }); 7760 7761 local.manualObserver.observe(source, { 7762 childList: true, 7763 subtree: true, 7764 attributes: true 7765 }); 7766 7767 return true; 7768 } 7769 7770 // --------------------------------------------------------------------------- 7771 // Native-nav observation and startup discovery 7772 // --------------------------------------------------------------------------- 7773 7774 function attachNativeObserver() { 7775 const wrapper = getNativeWrapper(); 7776 if (!wrapper || local.nativeObserver) return false; 7777 local.nativeObserver = new MutationObserver(() => scheduleRebuild(false)); 7778 local.nativeObserver.observe(wrapper, { 7779 childList: true, 7780 subtree: true, 7781 attributes: true, 7782 attributeFilter: ['href', 'raw_url', 'runtime_url', 'aria-current', 'class'] 7783 }); 7784 if (local.nativeFinderObserver) { 7785 local.nativeFinderObserver.disconnect(); 7786 local.nativeFinderObserver = null; 7787 } 7788 return true; 7789 } 7790 7791 function beginNativeDiscovery() { 7792 local.nativeRetryStarted = Date.now(); 7793 let isFirstAttempt = true; 7794 7795 const attempt = () => { 7796 if (local.cleaned) return; 7797 7798 // If the document-level finder already attached the native observer, the 7799 // retry loop has nothing left to do. This prevents a later timeout rebuild. 7800 if (local.nativeObserver) return; 7801 7802 if (attachNativeObserver()) { 7803 /* 7804 initializeFull() performs an immediate rebuild right after starting 7805 discovery. When .navmasterdefault already exists on the first attempt, 7806 scheduling another rebuild here caused the collections to be processed 7807 twice during startup. Only rebuild here when native navigation appeared 7808 after that initial attempt. 7809 */ 7810 if (!isFirstAttempt) scheduleRebuild(false); 7811 isFirstAttempt = false; 7812 return; 7813 } 7814 7815 isFirstAttempt = false; 7816 7817 if (Date.now() - local.nativeRetryStarted >= NATIVE_MAX_WAIT) { 7818 warn('NATIVE_NAV_TIMEOUT', '.navmasterdefault was not found within 15 seconds. Collection navigation will still render.'); 7819 scheduleRebuild(true); 7820 return; 7821 } 7822 7823 local.nativeRetryTimer = setTimeout(attempt, NATIVE_RETRY_INTERVAL); 7824 }; 7825 7826 attempt(); 7827 7828 // If the native wrapper already existed, no document-wide finder is needed. 7829 if (local.nativeObserver || local.cleaned) return; 7830 7831 // During delayed Duda insertion, this lightweight observer notices the 7832 // wrapper, attaches the targeted observer, rebuilds once, then disconnects. 7833 local.nativeFinderObserver = new MutationObserver(() => { 7834 if (local.cleaned || local.nativeObserver) return; 7835 if (attachNativeObserver()) scheduleRebuild(false); 7836 }); 7837 local.nativeFinderObserver.observe(document.body, { childList: true, subtree: true }); 7838 } 7839 7840 // --------------------------------------------------------------------------- 7841 // Tree preview and CUSTOMNAV builder 7842 // --------------------------------------------------------------------------- 7843 7844 function updateTools() { 7845 if (!toolsHost) return; 7846 const treeEnabled = readBoolean(configValueFrom(cfg, 'enableNavigationTreePreview'), false); 7847 const builderEnabled = readBoolean(configValueFrom(cfg, 'enableCustomNavigationBuilder'), false); 7848 const visibleEnvironment = isEditorCanvas() || location.href.includes('/preview/') || location.hash.includes('preview'); 7849 if (!visibleEnvironment || (!treeEnabled && !builderEnabled)) { 7850 toolsHost.innerHTML = ''; 7851 return; 7852 } 7853 toolsHost.innerHTML = [ 7854 treeEnabled ? '<button type="button" class="nmw-tool-button" data-nmw-open-tree>View Navigation Tree</button>' : '', 7855 builderEnabled ? '<button type="button" class="nmw-tool-button" data-nmw-open-builder>Open Custom Navigation Builder</button>' : '' 7856 ].join(''); 7857 } 7858 7859 function treeLines(items, prefix = '') { 7860 const output = [];
7861 (items || []).forEach(item => { 7862 output.push(`${prefix}${item.label}`); 7863 output.push(...treeLines(item.children || [], `${prefix} `)); 7864 }); 7865 return output; 7866 } 7867 7868 function renderSlugWarning(item) { 7869 const slugText = (item.slugs || []).join(' or '); 7870 return `<li><strong>${escapeHtml(item.label)}</strong><br>` + 7871 `Slug not found: ${escapeHtml(slugText)}<br>` + 7872 `Suggested collection: ${escapeHtml(item.suggestedCollection)}</li>`; 7873 } 7874 7875 function openTreePreview() { 7876 const warnings = Array.isArray(window.NMW.slugWarnings) ? window.NMW.slugWarnings : []; 7877 const treeOutput = treeLines(window.NMW.data || []).join('\n') || 'No navigation items.'; 7878 const warningSection = warnings.length 7879 ? `<section class="nmw-tree-warnings"><h3>Warnings</h3><ul>${warnings.map(renderSlugWarning).join('')}</ul></section>` 7880 : ''; 7881 const body = `<pre class="nmw-tree-output">${escapeHtml(treeOutput)}</pre>${warningSection}`; 7882 openModal('Navigation Tree Preview', body, 'tree'); 7883 } 7884 7885 function cloneData(value) { 7886 return JSON.parse(JSON.stringify(value || [])); 7887 } 7888 7889 function shiftDraftDepth(item, offset, parentKey = null) { 7890 const copy = cloneData(item); 7891 copy.depth = Math.max(0, (Number(copy.depth) || 0) + offset); 7892 copy.parentKey = parentKey; 7893 copy.children = (copy.children || []).map(child => shiftDraftDepth(child, offset, copy.key)); 7894 copy.hasChildren = copy.children.length > 0; 7895 return copy; 7896 } 7897 7898 // The runtime model contains synthetic Mega trigger rows. CUSTOMNAV should 7899 // store the actual family roots with MEGA GROUP values so loading CUSTOMNAV 7900 // can recreate the trigger without adding an extra persisted hierarchy level. 7901 function draftSourceFromSharedModel() { 7902 const output = []; 7903 (window.NMW.data || []).forEach(item => { 7904 if (readString(item.key, '').startsWith('mega-group:')) { 7905 (item.children || []).forEach(family => { 7906 const copy = shiftDraftDepth(family, -1, null); 7907 copy.megaGroup = item.megaGroup || copy.megaGroup || 0; 7908 copy.presentation = copy.megaGroup ? 'mega' : copy.presentation; 7909 output.push(copy); 7910 }); 7911 } else { 7912 output.push(shiftDraftDepth(item, 0, null)); 7913 } 7914 }); 7915 return output; 7916 } 7917 7918 function flattenDraft(items, output = []) { 7919 items.forEach(item => { 7920 output.push(item); 7921 flattenDraft(item.children, output); 7922 }); 7923 return output; 7924 } 7925 7926 function renderBuilderRows(items) { 7927 return items.map(item => `<div class="nmw-builder-row" draggable="true" data-builder-key="${escapeHtml(item.key)}" style="--nmw-builder-depth:${item.depth}"><span class="nmw-builder-handle" aria-hidden="true">â®â®</span><span class="nmw-builder-label">${escapeHtml(item.label)}</span><span class="nmw-builder-meta">${escapeHtml(item.source)} · ${escapeHtml(item.type)}${item.megaGroup ? ` · ${escapeHtml(getMegaLabel(item.megaGroup))}` : ''}</span><button type="button" data-builder-left data-key="${escapeHtml(item.key)}" aria-label="Move ${escapeHtml(item.label)} left">â</button><button type="button" data-builder-right data-key="${escapeHtml(item.key)}" aria-label="Move ${escapeHtml(item.label)} right">â</button><button type="button" data-builder-remove data-key="${escapeHtml(item.key)}" aria-label="Remove ${escapeHtml(item.label)}">Remove</button></div>`).join(''); 7928 } 7929 7930 function openBuilder() { 7931 local.customDraft = draftSourceFromSharedModel(); 7932 const body = `<div class="nmw-builder-toolbar"><button type="button" data-builder-add>Add Manual Item</button><button type="button" data-builder-reset>Reset Draft</button><button type="button" data-builder-validate>Validate</button><button type="button" data-builder-export="json">Export JSON</button><button type="button" data-builder-export="csv">Export CSV</button><button type="button" data-builder-export="payload">Export Duda Payload</button></div><p class="nmw-builder-help">Drag rows to reorder. Use â and â to promote or demote. Maximum depth is three levels.</p><div class="nmw-builder-list" data-builder-list>${renderBuilderRows(flattenDraft(local.customDraft))}</div><div class="nmw-builder-status" data-builder-status></div>`; 7933 openModal('Custom Navigation Builder', body, 'builder'); 7934 } 7935 7936 function openModal(title, body, type) { 7937 closeModal(); 7938 const modal = document.createElement('div'); 7939 modal.className = 'nmw-modal-backdrop'; 7940 modal.dataset.nmwModal = type; 7941 modal.innerHTML = `<section class="nmw-modal" role="dialog" aria-modal="true" aria-labelledby="nmw-modal-title-${instanceId}"><header class="nmw-modal-header"><h2 id="nmw-modal-title-${instanceId}">${escapeHtml(title)}</h2><button type="button" class="nmw-modal-close" data-nmw-close-modal aria-label="Close">Ã</button></header><div class="nmw-modal-body">${body}</div></section>`; 7942 document.body.appendChild(modal);
7943 modal.querySelector('[data-nmw-close-modal]').focus(); 7944 if (type === 'builder') attachBuilderInteractions(modal); 7945 } 7946 7947 function closeModal() { 7948 document.querySelectorAll('.nmw-modal-backdrop[data-nmw-modal]').forEach(node => node.remove()); 7949 } 7950 7951 function findDraftLocation(key, items = local.customDraft, parent = null) { 7952 for (let index = 0; index < items.length; index += 1) { 7953 if (items[index].key === key) return { item: items[index], items, index, parent }; 7954 const found = findDraftLocation(key, items[index].children, items[index]); 7955 if (found) return found; 7956 } 7957 return null; 7958 } 7959 7960 function recalculateDraft(items = local.customDraft, depth = 0, parentKey = null) { 7961 items.forEach((item, order) => { 7962 item.depth = depth; 7963 item.parentKey = parentKey; 7964 item.order = order; 7965 item.hasChildren = item.children.length > 0; 7966 recalculateDraft(item.children, depth + 1, item.key); 7967 }); 7968 } 7969 7970 function refreshBuilder(modal, message = '') { 7971 recalculateDraft(); 7972 const list = modal.querySelector('[data-builder-list]'); 7973 if (list) list.innerHTML = renderBuilderRows(flattenDraft(local.customDraft)); 7974 const status = modal.querySelector('[data-builder-status]'); 7975 if (status) status.textContent = message; 7976 } 7977 7978 function validateDraft() { 7979 const errors = []; 7980 const keys = new Set(); 7981 function walk(items, ancestors = []) { 7982 items.forEach(item => { 7983 if (keys.has(item.key)) errors.push(`Duplicate key: ${item.key}`); 7984 keys.add(item.key); 7985 if (item.depth > MAX_DEPTH) errors.push(`${item.label} exceeds three levels.`); 7986 if (ancestors.includes(item.key)) errors.push(`${item.label} is nested beneath itself.`); 7987 if (item.type === 'page' && !item.url) errors.push(`${item.label} is a page without a URL.`); 7988 walk(item.children, [...ancestors, item.key]); 7989 }); 7990 } 7991 walk(local.customDraft || []); 7992 return errors; 7993 } 7994 7995 function addManualDraftItem() { 7996 const label = window.prompt('Manual item label:'); 7997 if (!readString(label, '')) return; 7998 const type = window.prompt('Type "page" or "folder":', 'page'); 7999 const isFolder = readString(type, 'page').toLowerCase() === 'folder'; 8000 const url = isFolder ? '' : readString(window.prompt('URL:', '/'), ''); 8001 const key = `manual:${slugify(label)}-${Math.random().toString(36).slice(2, 6)}`; 8002 local.customDraft.push({ 8003 key, 8004 parentKey: null, 8005 source: 'manual', 8006 type: isFolder ? 'folder' : 'page', 8007 presentation: 'standard', 8008 megaGroup: 0, 8009 depth: 0, 8010 order: local.customDraft.length, 8011 label, 8012 slug: slugify(label), 8013 canonicalPath: isFolder ? '' : canonicalPath(url), 8014 url, 8015 target: '_self', 8016 isExternal: isExternalUrl(url), 8017 hasChildren: false, 8018 children: [] 8019 }); 8020 } 8021 8022 function moveDraftLeft(key) { 8023 const location = findDraftLocation(key); 8024 if (!location || !location.parent) return; 8025 const parentLocation = findDraftLocation(location.parent.key); 8026 location.items.splice(location.index, 1); 8027 parentLocation.items.splice(parentLocation.index + 1, 0, location.item); 8028 } 8029 8030 function moveDraftRight(key) { 8031 const location = findDraftLocation(key); 8032 if (!location || location.index === 0 || location.item.depth >= MAX_DEPTH) return; 8033 const previousSibling = location.items[location.index - 1]; 8034 location.items.splice(location.index, 1); 8035 previousSibling.children.push(location.item); 8036 } 8037 8038 function removeDraftItem(key) { 8039 const location = findDraftLocation(key); 8040 if (!location) return; 8041 location.items.splice(location.index, 1); 8042 } 8043 8044 function reorderDraft(dragKey, targetKey) { 8045 if (!dragKey || dragKey === targetKey) return; 8046 const drag = findDraftLocation(dragKey); 8047 const target = findDraftLocation(targetKey); 8048 if (!drag || !target || drag.items !== target.items) return; 8049 drag.items.splice(drag.index, 1); 8050 const newTarget = findDraftLocation(targetKey); 8051 newTarget.items.splice(newTarget.index, 0, drag.item); 8052 } 8053 8054 function customRowsFromDraft() { 8055 recalculateDraft();
8056 return flattenDraft(local.customDraft).map(item => ({ 8057 'ITEM KEY': item.key, 8058 'PARENT KEY': item.parentKey || '', 8059 'PAGE NAME': item.label, 8060 SOURCE: item.source, 8061 TYPE: item.type, 8062 PRESENTATION: item.presentation, 8063 'MEGA GROUP': item.megaGroup, 8064 DEPTH: item.depth, 8065 ORDER: item.order, 8066 URL: item.url || '', 8067 'CANONICAL PATH': item.canonicalPath || '', 8068 SLUG: item.slug || '', 8069 TARGET: item.target || '_self', 8070 'IS EXTERNAL': item.isExternal, 8071 'SHOW ITEM': true 8072 })); 8073 } 8074 8075 function downloadText(filename, content, mime = 'text/plain') { 8076 const blob = new Blob([content], { type: `${mime};charset=utf-8` }); 8077 const url = URL.createObjectURL(blob); 8078 const anchor = document.createElement('a'); 8079 anchor.href = url; 8080 anchor.download = filename; 8081 document.body.appendChild(anchor); 8082 anchor.click(); 8083 anchor.remove(); 8084 URL.revokeObjectURL(url); 8085 } 8086 8087 function exportDraft(format) { 8088 const errors = validateDraft(); 8089 if (errors.length) { 8090 window.alert(`Fix these issues before exporting:\n\n${errors.join('\n')}`); 8091 return; 8092 } 8093 const rows = customRowsFromDraft(); 8094 if (format === 'json') { 8095 downloadText('CUSTOMNAV.json', JSON.stringify(rows, null, 2), 'application/json'); 8096 return; 8097 } 8098 if (format === 'payload') { 8099 downloadText('CUSTOMNAV-duda-payload.json', JSON.stringify({ data: rows }, null, 2), 'application/json'); 8100 return; 8101 } 8102 const headers = Object.keys(rows[0] || {}); 8103 const escapeCsv = value => `"${String(value == null ? '' : value).replace(/"/g, '""')}"`; 8104 const csv = [headers.map(escapeCsv).join(','), ...rows.map(row => headers.map(header => escapeCsv(row[header])).join(','))].join('\n'); 8105 downloadText('CUSTOMNAV.csv', csv, 'text/csv'); 8106 } 8107 8108 function attachBuilderInteractions(modal) { 8109 let draggedKey = ''; 8110 modal.addEventListener('dragstart', event => { 8111 const row = event.target.closest('[data-builder-key]'); 8112 if (!row) return; 8113 draggedKey = row.dataset.builderKey; 8114 event.dataTransfer.effectAllowed = 'move'; 8115 }); 8116 modal.addEventListener('dragover', event => { 8117 if (event.target.closest('[data-builder-key]')) event.preventDefault(); 8118 }); 8119 modal.addEventListener('drop', event => { 8120 const target = event.target.closest('[data-builder-key]'); 8121 if (!target) return; 8122 event.preventDefault(); 8123 reorderDraft(draggedKey, target.dataset.builderKey); 8124 refreshBuilder(modal, 'Draft reordered.'); 8125 }); 8126 modal.addEventListener('click', event => { 8127 const left = event.target.closest('[data-builder-left]'); 8128 const right = event.target.closest('[data-builder-right]'); 8129 const remove = event.target.closest('[data-builder-remove]'); 8130 if (left) { moveDraftLeft(left.dataset.key); refreshBuilder(modal, 'Item promoted.'); return; } 8131 if (right) { moveDraftRight(right.dataset.key); refreshBuilder(modal, 'Item demoted.'); return; } 8132 if (remove) { removeDraftItem(remove.dataset.key); refreshBuilder(modal, 'Item removed from draft.'); return; } 8133 if (event.target.closest('[data-builder-add]')) { addManualDraftItem(); refreshBuilder(modal, 'Manual item added.'); return; } 8134 if (event.target.closest('[data-builder-reset]')) { local.customDraft = draftSourceFromSharedModel(); refreshBuilder(modal, 'Draft reset.'); return; } 8135 if (event.target.closest('[data-builder-validate]')) { 8136 const errors = validateDraft(); 8137 refreshBuilder(modal, errors.length ? errors.join(' ') : 'Structure is valid.'); 8138 return; 8139 } 8140 const exportButton = event.target.closest('[data-builder-export]'); 8141 if (exportButton) exportDraft(exportButton.dataset.builderExport); 8142 }); 8143 } 8144 8145 function handleToolClick(event) { 8146 if (event.target.closest('[data-nmw-open-tree]')) openTreePreview(); 8147 if (event.target.closest('[data-nmw-open-builder]')) openBuilder(); 8148 } 8149 8150 function handleModalClick(event) { 8151 const backdrop = event.target.closest('.nmw-modal-backdrop'); 8152 if (!backdrop) return; 8153 if (event.target.matches('.nmw-modal-backdrop') || event.target.closest('[data-nm
8153w-close-modal]')) closeModal(); 8154 } 8155 8156 function handleModalKeydown(event) { 8157 if (event.key === 'Escape' && document.querySelector('.nmw-modal-backdrop')) closeModal(); 8158 } 8159 8160 // --------------------------------------------------------------------------- 8161 // Reset action hooks 8162 // --------------------------------------------------------------------------- 8163 8164 // Duda panel switches cannot directly erase saved panel values. These hooks 8165 // expose one-shot reset requests as data attributes/classes so the panel or a 8166 // future config migration can repopulate defaults without touching structure. 8167 function applyResetRequests() { 8168 if (readBoolean(configValueFrom(cfg, 'restoreFullDefaults'), false) && widgetMode === 'full') root.dataset.nmwRestoreDefaults = 'full-requested'; 8169 if (readBoolean(configValueFrom(cfg, 'restoreMirrorDefaults'), false) && widgetMode === 'mirror') root.dataset.nmwRestoreDefaults = 'mirror-requested'; 8170 } 8171 8172 // --------------------------------------------------------------------------- 8173 // Cleanup 8174 // --------------------------------------------------------------------------- 8175 8176 function cleanup(reason = 'removed') { 8177 if (local.cleaned) return; 8178 local.cleaned = true; 8179 clearTimeout(local.rebuildTimer); 8180 clearTimeout(local.nativeRetryTimer); 8181 local.hoverTimers.forEach(timer => clearTimeout(timer)); 8182 local.anchorTimers.forEach(timer => clearTimeout(timer)); 8183 local.anchorTimers.clear(); 8184 local.closeTimers.forEach(timer => clearTimeout(timer)); 8185 local.closeTimers.clear(); 8186 local.nativeObserver?.disconnect(); 8187 local.nativeFinderObserver?.disconnect(); 8188 local.manualObserver?.disconnect(); 8189 local.removalObserver?.disconnect(); 8190 if (typeof local.historyCleanup === 'function') local.historyCleanup(); 8191 local.eventCleanups.splice(0).forEach(remove => { 8192 try { remove(); } catch (error) {} 8193 }); 8194 window.removeEventListener('nmw:data-changed', handleMirrorDataChanged); 8195 window.removeEventListener('nmw:state-changed', handleSharedStateChanged); 8196 window.removeEventListener('nmw:url-changed', handleUrlChanged); 8197 window.removeEventListener('popstate', handleUrlChanged); 8198 window.removeEventListener('hashchange', handleUrlChanged); 8199 document.removeEventListener('pointerdown', handleOutsidePointer, true); 8200 document.removeEventListener('click', handleModalClick); 8201 document.removeEventListener('keydown', handleModalKeydown); 8202 toolsHost?.removeEventListener('click', handleToolClick); 8203 closeModal(); 8204 releaseOwnership(); 8205 8206 // Remove this closure only when it is still the registered lifecycle for 8207 // the root. A newer execution may already have replaced it. 8208 if (window.NMW_RUNTIME?.instances?.get(root) === cleanup) { 8209 window.NMW_RUNTIME.instances.delete(root); 8210 } 8211 8212 console.info('[NMW] Cleaned up instance', { widgetMode, instanceId, reason }); 8213 } 8214 8215 // Duda does not provide a universal destroy callback for all widget contexts, 8216 // so this observer performs cleanup if the widget root is removed. 8217 local.removalObserver = new MutationObserver(() => { 8218 if (!document.documentElement.contains(root)) { 8219 local.removalObserver.disconnect(); 8220 cleanup(); 8221 } 8222 }); 8223 local.removalObserver.observe(document.documentElement, { childList: true, subtree: true }); 8224 8225 // --------------------------------------------------------------------------- 8226 // Full and Mirror initialization 8227 // --------------------------------------------------------------------------- 8228 8229 function handleMirrorDataChanged() { 8230 if (widgetMode !== 'mirror') return; 8231 // Full may have changed Design Panel values without changing collection 8232 // structure. Refresh inherited Mirror classes and CSS variables before 8233 // rendering the shared model. 8234 applyDesignSettings('mirror'); 8235 renderNavigation(window.NMW.data || [], 'mirror'); 8236 updateRootStatus(); 8237 } 8238 8239 function handleSharedStateChanged() { 8240 updateRootStatus(); 8241 } 8242 8243 function initializeFull() { 8244 // This instance now owns the Full widget. Reset stale editor state left by 8245 // an interrupted prior execution, then rebuild with the current panel data. 8246 window.NMW.loading = false;
8247 applyDesignSettings('full'); 8248 applyResetRequests(); 8249 attachDelegatedInteractions(); 8250 attachManualSourceObserver(); 8251 beginNativeDiscovery(); 8252 local.historyCleanup = observeHistoryChanges(); 8253 listen(window, 'nmw:url-changed', handleUrlChanged); 8254 listen(window, 'popstate', handleUrlChanged); 8255 listen(window, 'hashchange', handleUrlChanged); 8256 if (toolsHost) listen(toolsHost, 'click', handleToolClick); 8257 listen(document, 'click', handleModalClick); 8258 listen(document, 'keydown', handleModalKeydown); 8259 scheduleRebuild(true); 8260 } 8261 8262 function initializeMirror() { 8263 applyDesignSettings('mirror'); 8264 applyResetRequests(); 8265 attachDelegatedInteractions(); 8266 local.historyCleanup = observeHistoryChanges(); 8267 listen(window, 'nmw:data-changed', handleMirrorDataChanged); 8268 listen(window, 'nmw:state-changed', handleSharedStateChanged); 8269 listen(window, 'nmw:url-changed', handleUrlChanged); 8270 listen(window, 'popstate', handleUrlChanged); 8271 listen(window, 'hashchange', handleUrlChanged); 8272 if (toolsHost) listen(toolsHost, 'click', handleToolClick); 8273 listen(document, 'click', handleModalClick); 8274 listen(document, 'keydown', handleModalKeydown); 8275 updateRootStatus(); 8276 if (window.NMW.ready) renderNavigation(window.NMW.data || [], 'mirror'); 8277 } 8278 8279 try { 8280 if (!claimOwnership()) return; 8281 root.dataset.nmwInstance = instanceId; 8282 8283 // Register this lifecycle only after ownership succeeds. The next Duda 8284 // panel execution on the same root will call this cleanup before starting. 8285 window.NMW_RUNTIME.instances.set(root, cleanup); 8286 console.info('[NMW] Initialized', { widgetMode, instanceId, renderHost }); 8287 updateRootStatus(); 8288 if (widgetMode === 'full') initializeFull(); 8289 else initializeMirror(); 8290 } catch (error) { 8291 console.error('[NMW] Startup failed', error); 8292 if (renderHost) { 8293 renderHost.innerHTML = `<div class="nmw-empty">Navigation startup failed: ${escapeHtml(error && error.message ? error.message : String(error))}</div>`; 8294 } 8295 } 8296}); 8297 }; 8298</script>
8298 8299
8300<script> 8301 window.customWidgetsFunctions["e0b1e5b2d4394e69a98bd276fd6afe8b~8"] = function (element, data, api) { 8302 window.dmAPI.runOnReady('HH1', () => { 8303 8304 const STYLE_ID = 'HH1-theme-style'; 8305 const HH1_SELECTOR = '.widget-wrapper.HH1'; 8306 8307 let updateTimer = null; 8308 let lastSignature = ''; 8309 8310 /** 8311 * Find a usable H1 that is NOT inside the HH1 widget. 8312 */ 8313 function findThemeH1() { 8314 const candidates = Array.from( 8315 document.querySelectorAll('#dm h1, h1') 8316 ); 8317 8318 for (const el of candidates) { 8319 8320 // Do not use our own HH1 widget as the source 8321 if (el.closest(HH1_SELECTOR)) { 8322 continue; 8323 } 8324 8325 // Ignore disconnected elements 8326 if (!el.isConnected) { 8327 continue; 8328 } 8329 8330 const computed = window.getComputedStyle(el); 8331 8332 // Ignore elements that are completely unavailable 8333 if ( 8334 computed.display === 'none' || 8335 computed.visibility === 'hidden' 8336 ) { 8337 continue; 8338 } 8339 8340 // Make sure we actually got useful typography 8341 if (!computed.fontFamily || !computed.fontSize) { 8342 continue; 8343 } 8344 8345 return el; 8346 } 8347 8348 return null; 8349 } 8350 8351 8352 /** 8353 * Read H1 typography and apply it to HH1 widgets. 8354 */ 8355 function applyTheme() { 8356 8357 const h1Sample = findThemeH1(); 8358 8359 if (!h1Sample) { 8360 return false; 8361 } 8362 8363 const computed = window.getComputedStyle(h1Sample); 8364 8365 const styles = { 8366 fontFamily: computed.fontFamily, 8367 fontSize: computed.fontSize, 8368 fontWeight: computed.fontWeight, 8369 lineHeight: computed.lineHeight, 8370 letterSpacing: computed.letterSpacing, 8371 textTransform: computed.textTransform, 8372 fontStyle: computed.fontStyle, 8373 fontVariant: computed.fontVariant, 8374 fontStretch: computed.fontStretch 8375 }; 8376 8377 /* 8378 * Create a signature so we're not constantly rewriting 8379 * the stylesheet when nothing has actually changed. 8380 */ 8381 const signature = JSON.stringify(styles); 8382 8383 if (signature === lastSignature) { 8384 return true; 8385 } 8386 8387 lastSignature = signature; 8388 8389 8390 /* 8391 * Target the widget plus all common Duda RTE descendants. 8392 * The wildcard selector is intentional so inline/nested 8393 * typography cannot override the inherited H1 theme. 8394 */ 8395 const selectors = [ 8396 '#dm .dmInner .widget-wrapper.HH1 .dmNewParagraph', 8397 '#dm .dmInner .widget-wrapper.HH1 .dmNewParagraph p', 8398 '#dm .dmInner .widget-wrapper.HH1 .dmNewParagraph span', 8399 '#dm .dmInner .widget-wrapper.HH1 .dmNewParagraph .rteBlock', 8400 '#dm .dmInner .widget-wrapper.HH1 .dmNewParagraph .rteBlock *', 8401 8402 '.dmInner .widget-wrapper.HH1 .dmNewParagraph', 8403 '.dmInner .widget-wrapper.HH1 .dmNewParagraph p', 8404 '.dmInner .widget-wrapper.HH1 .dmNewParagraph span', 8405 '.dmInner .widget-wrapper.HH1 .dmNewParagraph .rteBlock', 8406 '.dmInner .widget-wrapper.HH1 .dmNewParagraph .rteBlock *', 8407 8408 '.widget-wrapper.HH1 .dmNewParagraph', 8409 '.widget-wrapper.HH1 .dmNewParagraph p', 8410 '.widget-wrapper.HH1 .dmNewParagraph span', 8411 '.widget-wrapper.HH1 .dmNewParagraph .rteBlock', 8412 '.widget-wrapper.HH1 .dmNewParagraph .rteBlock *' 8413 ]; 8414 8415 const css = ` 8416${selectors.join(',\n')} { 8417 font-family: ${styles.fontFamily} !important; 8418 font-size: ${styles.fontSize} !important; 8419 font-weight: ${styles.fontWeight} !important; 8420 line-height: ${styles.lineHeight} !important; 8421 letter-spacing: ${styles.letterSpacing} !important; 8422 text-transform: ${styles.textTransform} !important; 8423 font-style: ${styles.fontStyle} !important; 8424 font-variant: ${styles.fontVariant} !important; 8425 font-stretch: ${styles.fontStretch} !important; 8426} 8427`; 8428 8429 let styleTag = document.getElementById(STYLE_ID); 8430 8431 if (!styleTag) { 8432 styleTag = document.createElement('style'); 8433 styleTag.id = STYLE_ID; 8434 styleTag.setAttribute('data-HH1', 'true'); 8435 document.head.appendChild(styleTag); 8436 } 8437 8438 styleTag.textContent = css; 8439 8440 return true; 8441 } 8442 8443 8444 /** 8445 * Debounce updates so Duda can make several DOM changes 8446 * without us recalculating after every individual mutation. 8447 */ 8448 function scheduleUpdate(delay = 100) { 8449 8450 clearTimeout(updateTimer); 8451 8452 updateTimer = setTimeout(() => { 8453 applyTheme(); 8454 }, delay); 8455 } 8456 8457 8458 /** 8459 * INITIAL LOAD 8460 * 8461 * Run several times because Duda/theme/font loading can occur 8462 * after dmAPI.runOnReady(). 8463 */ 8464 applyTheme(); 8465 8466 setTimeout(applyTheme, 250); 8467 setTimeout(applyTheme, 500); 8468 setTimeout(applyTheme, 1000); 8469 setTimeout(applyTheme, 2000); 8470 setTimeout(applyTheme, 4000); 8471 8472 8473 /** 8474 * WATCH DUDA DOM CHANGES 8475 * 8476 * Dynamic collections, responsive rendering, widgets, and 8477 * editor/preview changes can alter the page after initial load. 8478 */ 8479 const observer = new MutationObserver((mutations) => { 8480 8481 let relevantChange = false;
8482 8483 for (const mutation of mutations) { 8484 8485 /* 8486 * Ignore mutations caused only by our own style tag. 8487 */ 8488 if ( 8489 mutation.target && 8490 ( 8491 mutation.target.id === STYLE_ID || 8492 mutation.target.closest?.(`#${STYLE_ID}`) 8493 ) 8494 ) { 8495 continue; 8496 } 8497 8498 relevantChange = true; 8499 break; 8500 } 8501 8502 if (relevantChange) { 8503 scheduleUpdate(150); 8504 } 8505 8506 }); 8507 8508 observer.observe(document.body, { 8509 childList: true, 8510 subtree: true, 8511 attributes: true, 8512 attributeFilter: [ 8513 'class', 8514 'style' 8515 ] 8516 }); 8517 8518 8519 /** 8520 * RESPONSIVE CHANGES 8521 * 8522 * H1 typography may change between desktop/tablet/mobile. 8523 */ 8524 window.addEventListener('resize', () => { 8525 scheduleUpdate(150); 8526 }); 8527 8528 8529 /** 8530 * ORIENTATION CHANGE 8531 */ 8532 window.addEventListener('orientationchange', () => { 8533 scheduleUpdate(250); 8534 }); 8535 8536 8537 /** 8538 * FONT LOADING 8539 * 8540 * Reapply after web fonts finish loading. 8541 */ 8542 if (document.fonts && document.fonts.ready) { 8543 8544 document.fonts.ready.then(() => { 8545 8546 // Force refresh even if we previously captured fallback font 8547 lastSignature = ''; 8548 8549 applyTheme(); 8550 8551 }).catch(() => { 8552 // Nothing required 8553 }); 8554 8555 } 8556 8557}); 8558 }; 8559</script>
8559 8560
8561<script> 8562 window.customWidgetsFunctions["9015bcbf1ace437aa6f0e66d5bb2e7d3~25"] = function (element, data, api) { 8563 $(document).ready(function(){ 8564 $.getScript('https://widget.hibu.us/static/js/index.js') 8565 //$.getScript('//survlywidget-uat.firebaseapp.com/static/js/index.js') 8566 .done(function(){ 8567 dmAPI.runOnReady('hibu_header_reviews',function(){ 8568 window.renderApp(); 8569 }); 8570 8571 }) 8572 .fail(function(){ 8573 console.log("failed to load header reviews js file") 8574 }); 8575 8576 element.children[0].removeAttribute(`rteblock"`) 8577 var merchantId = data.config.merchantId 8578 merchantId = merchantId.replace(/<p.*?>/g, '') 8579 merchantId = merchantId.replace(/"/g, '"') 8580 merchantId = merchantId.replace(/\<\/p\>/g, '') 8581 8582 element.children[0].setAttribute('data-merchant', merchantId) 8583}) 8584 8585 8586 }; 8587</script>
8587 8588
8589<script> 8590 window.customWidgetsFunctions["ca4188fe92024514a23a858f63ab6658~18"] = function (element, data, api) { 8591 null 8592 }; 8593</script>
8593 8594
8595<script> 8596 window.customWidgetsFunctions["2133bfee2cb24e0c9db9468154881b94~3"] = function (element, data, api) { 8597 window.dmAPI.runOnReady('HH2', () => { 8598 8599 const STYLE_ID = 'HH2-theme-style'; 8600 const HH2_SELECTOR = '.widget-wrapper.HH2'; 8601 8602 let updateTimer = null; 8603 let lastSignature = ''; 8604 8605 /** 8606 * Find a usable H2 that is NOT inside the HH2 widget. 8607 */ 8608 function findThemeH2() { 8609 const candidates = Array.from( 8610 document.querySelectorAll('#dm h2, h2') 8611 ); 8612 8613 for (const el of candidates) { 8614 8615 // Do not use our own HH2 widget as the source 8616 if (el.closest(HH2_SELECTOR)) { 8617 continue; 8618 } 8619 8620 // Ignore disconnected elements 8621 if (!el.isConnected) { 8622 continue; 8623 } 8624 8625 const computed = window.getComputedStyle(el); 8626 8627 // Ignore elements that are completely unavailable 8628 if ( 8629 computed.display === 'none' || 8630 computed.visibility === 'hidden' 8631 ) { 8632 continue; 8633 } 8634 8635 // Make sure we actually got useful typography 8636 if (!computed.fontFamily || !computed.fontSize) { 8637 continue; 8638 } 8639 8640 return el; 8641 } 8642 8643 return null; 8644 } 8645 8646 8647 /** 8648 * Read H2 typography and apply it to HH2 widgets. 8649 */ 8650 function applyTheme() { 8651 8652 const h2Sample = findThemeH2(); 8653 8654 if (!h2Sample) { 8655 return false; 8656 } 8657 8658 const computed = window.getComputedStyle(h2Sample); 8659 8660 const styles = { 8661 fontFamily: computed.fontFamily, 8662 fontSize: computed.fontSize, 8663 fontWeight: computed.fontWeight, 8664 lineHeight: computed.lineHeight, 8665 letterSpacing: computed.letterSpacing, 8666 textTransform: computed.textTransform, 8667 fontStyle: computed.fontStyle, 8668 fontVariant: computed.fontVariant, 8669 fontStretch: computed.fontStretch 8670 }; 8671 8672 /* 8673 * Create a signature so we're not constantly rewriting 8674 * the stylesheet when nothing has actually changed. 8675 */ 8676 const signature = JSON.stringify(styles); 8677 8678 if (signature === lastSignature) { 8679 return true; 8680 } 8681 8682 lastSignature = signature; 8683 8684 8685 /* 8686 * Target the widget plus all common Duda RTE descendants. 8687 * The wildcard selector is intentional so inline/nested 8688 * typography cannot override the inherited H2 theme. 8689 */ 8690 const selectors = [ 8691 '#dm .dmInner .widget-wrapper.HH2 .dmNewParagraph', 8692 '#dm .dmInner .widget-wrapper.HH2 .dmNewParagraph p', 8693 '#dm .dmInner .widget-wrapper.HH2 .dmNewParagraph span', 8694 '#dm .dmInner .widget-wrapper.HH2 .dmNewParagraph .rteBlock', 8695 '#dm .dmInner .widget-wrapper.HH2 .dmNewParagraph .rteBlock *', 8696 8697 '.dmInner .widget-wrapper.HH2 .dmNewParagraph', 8698 '.dmInner .widget-wrapper.HH2 .dmNewParagraph p', 8699 '.dmInner .widget-wrapper.HH2 .dmNewParagraph span', 8700 '.dmInner .widget-wrapper.HH2 .dmNewParagraph .rteBlock', 8701 '.dmInner .widget-wrapper.HH2 .dmNewParagraph .rteBlock *', 8702 8703 '.widget-wrapper.HH2 .dmNewParagraph', 8704 '.widget-wrapper.HH2 .dmNewParagraph p', 8705 '.widget-wrapper.HH2 .dmNewParagraph span', 8706 '.widget-wrapper.HH2 .dmNewParagraph .rteBlock', 8707 '.widget-wrapper.HH2 .dmNewParagraph .rteBlock *' 8708 ]; 8709 8710 const css = ` 8711${selectors.join(',\n')} { 8712 font-family: ${styles.fontFamily} !important; 8713 font-size: ${styles.fontSize} !important; 8714 font-weight: ${styles.fontWeight} !important; 8715 line-height: ${styles.lineHeight} !important; 8716 letter-spacing: ${styles.letterSpacing} !important; 8717 text-transform: ${styles.textTransform} !important; 8718 font-style: ${styles.fontStyle} !important; 8719 font-variant: ${styles.fontVariant} !important; 8720 font-stretch: ${styles.fontStretch} !important; 8721} 8722`; 8723 8724 let styleTag = document.getElementById(STYLE_ID); 8725 8726 if (!styleTag) { 8727 styleTag = document.createElement('style'); 8728 styleTag.id = STYLE_ID; 8729 styleTag.setAttribute('data-HH2', 'true'); 8730 document.head.appendChild(styleTag); 8731 } 8732 8733 styleTag.textContent = css; 8734 8735 return true; 8736 } 8737 8738 8739 /** 8740 * Debounce updates so Duda can make several DOM changes 8741 * without us recalculating after every individual mutation. 8742 */ 8743 function scheduleUpdate(delay = 100) { 8744 8745 clearTimeout(updateTimer); 8746 8747 updateTimer = setTimeout(() => { 8748 applyTheme(); 8749 }, delay); 8750 } 8751 8752 8753 /** 8754 * INITIAL LOAD 8755 * 8756 * Run several times because Duda/theme/font loading can occur 8757 * after dmAPI.runOnReady(). 8758 */ 8759 applyTheme(); 8760 8761 setTimeout(applyTheme, 250); 8762 setTimeout(applyTheme, 500); 8763 setTimeout(applyTheme, 1000); 8764 setTimeout(applyTheme, 2000); 8765 setTimeout(applyTheme, 4000); 8766 8767 8768 /** 8769 * WATCH DUDA DOM CHANGES 8770 */ 8771 const observer = new MutationObserver((mutations) => { 8772 8773 let relevantChange = false;
8774 8775 for (const mutation of mutations) { 8776 8777 /* 8778 * Ignore mutations caused only by our own style tag. 8779 */ 8780 if ( 8781 mutation.target && 8782 ( 8783 mutation.target.id === STYLE_ID || 8784 mutation.target.closest?.(`#${STYLE_ID}`) 8785 ) 8786 ) { 8787 continue; 8788 } 8789 8790 relevantChange = true; 8791 break; 8792 } 8793 8794 if (relevantChange) { 8795 scheduleUpdate(150); 8796 } 8797 8798 }); 8799 8800 observer.observe(document.body, { 8801 childList: true, 8802 subtree: true, 8803 attributes: true, 8804 attributeFilter: [ 8805 'class', 8806 'style' 8807 ] 8808 }); 8809 8810 8811 /** 8812 * RESPONSIVE CHANGES 8813 * 8814 * H2 typography may change between desktop/tablet/mobile. 8815 */ 8816 window.addEventListener('resize', () => { 8817 scheduleUpdate(150); 8818 }); 8819 8820 8821 /** 8822 * ORIENTATION CHANGE 8823 */ 8824 window.addEventListener('orientationchange', () => { 8825 scheduleUpdate(250); 8826 }); 8827 8828 8829 /** 8830 * FONT LOADING 8831 * 8832 * Reapply after web fonts finish loading. 8833 */ 8834 if (document.fonts && document.fonts.ready) { 8835 8836 document.fonts.ready.then(() => { 8837 8838 // Force refresh even if we previously captured fallback font 8839 lastSignature = ''; 8840 8841 applyTheme(); 8842 8843 }).catch(() => { 8844 // Nothing required 8845 }); 8846 8847 } 8848 8849}); 8850 }; 8851</script>
8851 8852
8853<script> 8854 window.customWidgetsFunctions["0c06e70da5474879a21daa0d3b1192c0~3"] = function (element, data, api) { 8855 window.dmAPI.runOnReady('cb1-h1-theme', () => { 8856 8857 function applyTheme() { 8858 // Try multiple candidate selectors for the h1 style source 8859 // to work in both the editor and the published/preview environment 8860 const candidates = [ 8861 '#dm h1', 8862 'h1', 8863 ]; 8864 8865 let h1Sample = null; 8866 for (const sel of candidates) { 8867 h1Sample = document.querySelector(sel); 8868 if (h1Sample) break; 8869 } 8870 8871 if (!h1Sample) return false; 8872 8873 const computed = window.getComputedStyle(h1Sample); 8874 8875 const fontFamily = computed.fontFamily; 8876 const fontSize = computed.fontSize; 8877 const fontWeight = computed.fontWeight; 8878 const lineHeight = computed.lineHeight; 8879 const color = computed.color; 8880 const letterSpacing = computed.letterSpacing; 8881 const textTransform = computed.textTransform; 8882 const fontStyle = computed.fontStyle; 8883 8884 // Skip if nothing meaningful was computed yet 8885 if (!fontFamily || fontFamily === '') return false; 8886 8887 const baseSelectors = [ 8888 `#dm .dmInner .widget-wrapper.DCBH1 .dmNewParagraph`, 8889 `#dm .dmInner .widget-wrapper.DCBH1 .dmNewParagraph p`, 8890 `#dm .dmInner .widget-wrapper.DCBH1 .dmNewParagraph .rteBlock`, 8891 `.dmInner .widget-wrapper.DCBH1 .dmNewParagraph`, 8892 `.dmInner .widget-wrapper.DCBH1 .dmNewParagraph p`, 8893 `.dmInner .widget-wrapper.DCBH1 .dmNewParagraph .rteBlock`, 8894 `.widget-wrapper.DCBH1 .dmNewParagraph`, 8895 `.widget-wrapper.DCBH1 .dmNewParagraph p`, 8896 `.widget-wrapper.DCBH1 .dmNewParagraph .rteBlock`, 8897 ]; 8898 8899 const allSelectors = baseSelectors.join(',\n '); 8900 8901 const css = ` 8902 ${allSelectors} { 8903 font-family: ${fontFamily} !important; 8904 font-size: ${fontSize} !important; 8905 font-weight: ${fontWeight} !important; 8906 line-height: ${lineHeight} !important; 8907 color: ${color} !important; 8908 letter-spacing: ${letterSpacing} !important; 8909 text-transform: ${textTransform} !important; 8910 font-style: ${fontStyle} !important; 8911 } 8912 `; 8913 8914 // Remove any previously injected tag before re-injecting 8915 const existing = document.querySelector('style[data-cb1-h1-theme]'); 8916 if (existing) existing.remove(); 8917 8918 const styleTag = document.createElement('style'); 8919 styleTag.setAttribute('data-cb1-h1-theme', 'true'); 8920 styleTag.textContent = css; 8921 document.head.appendChild(styleTag); 8922 8923 return true; 8924 } 8925 8926 // Try immediately 8927 if (applyTheme()) return; 8928 8929 // If not ready yet, retry with increasing delays to handle 8930 // async rendering in preview / published environments 8931 let attempts = 0; 8932 const maxAttempts = 20; 8933 const interval = setInterval(() => { 8934 attempts++; 8935 if (applyTheme() || attempts >= maxAttempts) { 8936 clearInterval(interval); 8937 } 8938 }, 300); 8939}); 8940 8941 }; 8942</script>
8942 8943
8944<script> 8945 window.customWidgetsFunctions["3e38ec713838428586c343d139994b6f~58"] = function (element, data, api) { 8946 window.dmAPI.runOnReady('navGridWidget', () => { 8947 const container = element.querySelector('#nav-grid-widget'); 8948 if(!container) return; 8949 8950 const cleanText = (el)=> (el?.textContent||'').trim().replace(/\s+/g,' '); 8951 const cleanUrl = (href)=>{ 8952 try{ 8953 const u=new URL(href,location.origin); 8954 const path = u.pathname.replace(/\/+$/,'')||'/'; 8955 return path + u.search; // Preserve query parameters for preview mode 8956 } 8957 catch{ return '#'; } 8958 }; 8959 8960 // Get column count from config (grid uses this; masonry uses this) 8961 const columnCount = parseInt(data.config['columnCount'] || '3', 10); 8962 8963 // Get exclusion list from config 8964 const excludeList = (data.config['excludeParents'] || '') 8965 .split(',') 8966 .map(s => s.trim().toLowerCase()) 8967 .filter(s => s.length > 0); 8968 8969 // Get inclusion list from config 8970 const includeList = (data.config['includeParents'] || '') 8971 .split(',') 8972 .map(s => s.trim().toLowerCase()) 8973 .filter(s => s.length > 0); 8974 8975 // Get chevron icon from config (Option B default: right-pointing) 8976 const chevronIcon = data.config['chevronIcon'] || 'â¸'; 8977 8978 // Get hover setting from config 8979 const openOnHover = data.config['openOnHover'] || false; 8980 8981 // Get expand all setting from config 8982 const expandAll = data.config['expandAll'] || false; 8983 8984 // Get masonry setting from config (NEW - separate from expandAll) 8985 const useMasonry = data.config['useMasonry'] || false; 8986 8987 // Set CSS custom property for column count - this works for both grid AND masonry 8988 container.style.setProperty('--column-count', String(columnCount)); 8989 8990 // Masonry is now controlled by its own toggle 8991 if(useMasonry) { 8992 container.setAttribute('data-masonry', 'true'); 8993 } else { 8994 container.removeAttribute('data-masonry'); 8995 } 8996 8997 // Set uppercase data attributes from config 8998 const uppercaseParents = data.config['uppercaseParents'] || false; 8999 const uppercaseChildren = data.config['uppercaseChildren'] || false; 9000 9001 // Get selected item styling from config 9002 const underlineSelected = data.config['underlineSelected'] !== false; 9003 const selectedBgColor = data.config['selectedBgColor'] || 'transparent'; 9004 9005 if(uppercaseParents) container.setAttribute('data-uppercase-parents', 'true'); 9006 else container.removeAttribute('data-uppercase-parents'); 9007 9008 if(uppercaseChildren) container.setAttribute('data-uppercase-children', 'true'); 9009 else container.removeAttribute('data-uppercase-children'); 9010 9011 if(underlineSelected) container.setAttribute('data-underline-selected', 'true'); 9012 else container.removeAttribute('data-underline-selected'); 9013 9014 container.style.setProperty('--selected-bg-color', selectedBgColor); 9015 9016 // Helpful attribute only 9017 container.setAttribute('data-columns', String(columnCount)); 9018 9019 // Get current page URL for active state detection 9020 const currentPagePath = cleanUrl(window.location.href); 9021 9022 // Recursive function to extract nested children 9023 function extractChildren(parentLi, level = 1) { 9024 const childUL = parentLi.querySelector(':scope > ul, :scope .sub-nav ul, :scope .submenu ul, :scope .dropdown ul, :scope .children, :scope ul'); 9025 const children = []; 9026 9027 if(childUL){ 9028 Array.from(childUL.querySelectorAll(':scope > li')).forEach(cli => { 9029 const a = cli.querySelector('a'); 9030 if(!a) return; 9031 const title = cleanText(a); 9032 const url = cleanUrl(a.getAttribute('href')||'#'); 9033 if(!title || !url || url === '#') return; 9034 9035 const nestedChildren = extractChildren(cli, level + 1); 9036 9037 children.push({ 9038 title, 9039 url, 9040 children: nestedChildren, 9041 level 9042 }); 9043 }); 9044 } 9045 9046 return children; 9047 } 9048 9049 function extractNavFromDOM(){ 9050 const navCandidates = [ 9051 'nav[role="navigation"]','.dmNavWrapper','.dmNav','.navigation','nav','header nav' 9052 ]; 9053 let navRoot=null; 9054 9055 for(const sel of navCandidates){ 9056 const el=document.querySelector(sel); 9057 if(!el) continue; 9058 if(el.querySelector('li ul, li .sub-nav, li .submenu, li .dropdown')){ navRoot=el; break; } 9059 if(!navRoot) navRoot=el; 9060 } 9061 if(!navRoot) return []; 9062 9063 const topUL = navRoot.querySelector('ul, .menu, .dmNav'); 9064 const topLIs = topUL 9065 ? Array.from(topUL.children).filter(li=>li.tagName==='LI') 9066 : Array.from(navRoot.querySelectorAll(':scope > li, :scope ul > li')); 9067 9068 const parents=[]; 9069 topLIs.forEach(li=>{ 9070 const parentLink = 9071 li.querySelector(':scope > a, :scope > .nav-item > a, :scope > .menu-item > a') || 9072 li.querySelector('a'); 9073 9074 const parentTitle = cleanText(parentLink); 9075 const parentUrl = cleanUrl(parentLink?.getAttribute('href')||'#'); 9076 9077 if(!parentTitle) return; 9078 9079 // Skip if parent title is in exclusion list 9080 if(excludeList.includes(parentTitle.toLowerCase())) return; 9081 9082 const children = extractChildren(li, 1); 9083 9084 // Include if: has children OR is in inclusion list 9085 const isInIncludeList = includeList.length > 0 && includeList.includes(parentTitle.toLowerCase()); 9086 9087 if(children.length > 0 || isInIncludeList) { 9088 parents.push({ title: parentTitle, url: parentUrl, children, level: 0 }); 9089 } 9090 }); 9091 9092 return parents; 9093 } 9094 9095 // Recursive function to render nested children 9096 function renderNestedChildren(children, parentPath = '') { 9097 return children.map((child, childIndex) => { 9098 const nodePath = `${parentPath}-${childIndex}`; 9099 const hasChildren = child.children && child.children.length > 0; 9100 const level = child.level || 1; 9101 const isActive = child.url === currentPagePath; 9102 const activeClass = isActive ? ' ngw-active' : ''; 9103 9104 if(hasChildren) { 9105 return ` 9106 <div class="ngw-nested-group" data-level="${level}" data-path="${nodePath}"> 9107 <div class="ngw-nested-parent" data-path="${nodePath}"> 9108 <a href="${child.url}" class="ngw-child-link${activeClass}">${child.title}</a> 9109 <button class="ngw-nested-toggle" type="button" aria-label="Toggle ${child.title}">
9110 <span class="ngw-chevron" aria-hidden="true">${chevronIcon}</span> 9111 </button> 9112 </div> 9113 <div class="ngw-nested-children" data-path="${nodePath}"> 9114 ${renderNestedChildren(child.children, nodePath)} 9115 </div> 9116 </div> 9117 `; 9118 } 9119 9120 return `<a href="${child.url}" class="ngw-child-link${activeClass}" data-level="${level}">${child.title}</a>`; 9121 }).join(''); 9122 } 9123 9124 function render(gridData){ 9125 container.innerHTML = gridData.map((parent, idx) => { 9126 const kids = renderNestedChildren(parent.children, `${idx}`); 9127 const hasChildren = parent.children && parent.children.length > 0; 9128 9129 const isParentActive = parent.url === currentPagePath; 9130 const activeClass = isParentActive ? ' ngw-active' : ''; 9131 9132 if(!hasChildren) { 9133 return ` 9134 <section class="ngw-group ngw-no-children" role="group" data-index="${idx}"> 9135 <div class="ngw-parent" data-index="${idx}"> 9136 <a href="${parent.url}" class="ngw-parent-link${activeClass}">${parent.title}</a> 9137 </div> 9138 </section> 9139 `; 9140 } 9141 9142 const initialExpanded = expandAll ? 'true' : 'false'; 9143 return ` 9144 <section class="ngw-group" role="group" aria-expanded="${initialExpanded}" data-index="${idx}"> 9145 <div class="ngw-parent" data-index="${idx}" aria-controls="ngw-children-${idx}"> 9146 <a href="${parent.url}" class="ngw-parent-link${activeClass}">${parent.title}</a> 9147 <button class="ngw-toggle" type="button" aria-label="Toggle ${parent.title}"> 9148 <span class="ngw-chevron" aria-hidden="true">${chevronIcon}</span> 9149 </button> 9150 </div> 9151 <div id="ngw-children-${idx}" class="ngw-children" role="region" aria-label="${parent.title} subpages"> 9152 ${kids} 9153 </div> 9154 </section> 9155 `; 9156 }).join(''); 9157 9158 // If expandAll is enabled, also expand all nested groups 9159 if(expandAll) { 9160 container.querySelectorAll('.ngw-nested-group').forEach(g => { 9161 g.setAttribute('aria-expanded', 'true'); 9162 }); 9163 } 9164 9165 function collapseAll(){ 9166 container.querySelectorAll('.ngw-group').forEach(g=>g.setAttribute('aria-expanded','false')); 9167 container.querySelectorAll('.ngw-nested-group').forEach(g=>g.removeAttribute('aria-expanded')); 9168 } 9169 function expandOnly(index){ 9170 container.querySelectorAll('.ngw-group').forEach((g)=> { 9171 const i = parseInt(g.getAttribute('data-index')||'-1',10); 9172 g.setAttribute('aria-expanded', String(i===index)); 9173 }); 9174 } 9175 9176 // Hover functionality - only if not in expandAll mode 9177 if(openOnHover && !expandAll) { 9178 container.addEventListener('mouseenter', (e) => { 9179 const group = e.target.closest('.ngw-group'); 9180 if(!group || group.classList.contains('ngw-no-children')) return; 9181 const index = parseInt(group.querySelector('.ngw-parent')?.getAttribute('data-index') || '-1', 10); 9182 if(index >= 0) expandOnly(index); 9183 }, true); 9184 9185 container.addEventListener('mouseleave', (e) => { 9186 if(e.target.closest('.ngw-group')) return; 9187 collapseAll(); 9188 }, true); 9189 9190 // Nested hover 9191 container.addEventListener('mouseenter', (e) => { 9192 const nestedGroup = e.target.closest('.ngw-nested-group'); 9193 if(!nestedGroup) return; 9194 nestedGroup.setAttribute('aria-expanded', 'true'); 9195 }, true); 9196 9197 container.addEventListener('mouseleave', (e) => { 9198 const nestedGroup = e.target.closest('.ngw-nested-group'); 9199 if(!nestedGroup) return; 9200 nestedGroup.removeAttribute('aria-expanded'); 9201 }, true); 9202 } 9203 9204 // ---------------------------- 9205 // Toggle behavior (works in BOTH modes) 9206 // - expandAll=false: row click toggles + only one open at a time 9207 // - expandAll=true : ONLY chevron buttons toggle; everything starts expanded 9208 // ---------------------------- 9209 container.addEventListener('click', (e) => { 9210 // 1) Parent chevron button toggles its group (works even when expandAll=true) 9211 const parentToggleBtn = e.target.closest('.ngw-toggle'); 9212 if (parentToggleBtn) { 9213 const group = parentToggleBtn.closest('.ngw-group'); 9214 if (!group || group.classList.contains('ngw-no-children')) return; 9215 9216 const isOpen = group.getAttribute('aria-expanded') === 'true'; 9217 9218 // In expandAll mode: toggle ONLY this group 9219 if (expandAll) { 9220 group.setAttribute('aria-expanded', String(!isOpen)); 9221 } else { 9222 // In normal mode: preserve "only one open at a time" 9223 const index = parseInt(group.getAttribute('data-index') || '-1', 10); 9224 if (index >= 0) { 9225 if (isOpen) collapseAll(); 9226 else expandOnly(index); 9227 } 9228 } 9229 9230 e.preventDefault(); 9231 e.stopPropagation(); 9232 return; 9233 } 9234 9235 // 2) Nested chevron button toggles its nested group (works even when expandAll=true) 9236 const nestedToggleBtn = e.target.closest('.ngw-nested-toggle'); 9237 if (nestedToggleBtn) { 9238 const nestedGroup = nestedToggleBtn.closest('.ngw-nested-group'); 9239 if (!nestedGroup) return; 9240 9241 const isOpen = nestedGroup.getAttribute('aria-expanded') === 'true'; 9242 if (isOpen) nestedGroup.removeAttribute('aria-expanded'); 9243 else nestedGroup.setAttribute('aria-expanded', 'true'); 9244 9245 e.preventDefault(); 9246 e.stopPropagation(); 9247 return; 9248 } 9249 9250 // 3) In expandAll mode, do NOT allow row-click toggling (only chevrons toggle) 9251 if (expandAll) return; 9252 9253 // 4) Normal mode: clicking anywhere on parent row toggles (except the parent hyperlink) 9254 const parentRow = e.target.closest('.ngw-parent'); 9255 if (!parentRow) return; 9256 9257 // If they clicked the parent hyperlink, let the browser navigate naturally 9258 if (e.target.closest('.ngw-parent-link')) return; 9259 9260 const index = parseInt(parentRow.getAttribute('data-index') || '-1', 10); 9261 if (index < 0) return; 9262 9263 const group = parentRow.closest('.ngw-group'); 9264 if (!group || group.classList.contains('ngw-no-children')) return; 9265 9266 const isOpen = group.getAttribute('aria-expanded') === 'true'; 9267 if (isOpen) collapseAll(); 9268 else expandOnly(index); 9269 }); 9270 9271 // Keyboard support 9272 container.addEventListener('keydown', (e) => { 9273 if (expandAll) return; // chevron buttons already work via native button behavior 9274 9275 const parentRow = e.target.closest('.ngw-parent'); 9276 const nestedParent = e.target.closest('.ngw-nested-parent'); 9277 9278 if (parentRow && !nestedParent) { 9279 if (document.activeElement && document.activeElement.classList.contains('ngw-parent-link') && e.key === 'Enter') { 9280 return; 9281 } 9282 const group = parentRow.closest('.ngw-group'); 9283 if (group && group.classList.contains('ngw-no-children')) return; 9284 9285 if (e.key === ' ' || e.key === 'Enter') { 9286 e.preventDefault(); 9287 const index = parseInt(parentRow.getAttribute('data-index') || '-1', 10); 9288 if (index < 0) return; 9289 const isOpen = group.getAttribute('aria-expanded') === 'true'; 9290 if (isOpen) collapseAll(); 9291 else expandOnly(index); 9292 } 9293 } 9294 9295 if (nestedParent) { 9296 if (document.activeElement && document.activeElement.classList.contains('ngw-child-link') && e.key === 'Enter') { 9297 return; 9298 } 9299 if (e.key === ' ' || e.key === 'Enter') { 9300 e.preventDefault(); 9301 const nestedGroup = nestedParent.closest('.ngw-nested-group'); 9302 if (!nestedGroup) return; 9303 const isOpen = nestedGroup.getAttribute('aria-expanded') === 'true'; 9304 if (isOpen) nestedGroup.removeAttribute('aria-expanded'); 9305 else nestedGroup.setAttribute('aria-expanded', 'true'); 9306 } 9307 } 9308 }); 9309 } 9310
9311 function init(){ 9312 const build = ()=>{ 9313 const navData = extractNavFromDOM(); 9314 if(navData && navData.length){ 9315 render(navData); 9316 return true; 9317 } 9318 return false; 9319 }; 9320 const okNow = build(); 9321 if(!okNow){ setTimeout(build, 350); } // retry once for late-loaded nav 9322 } 9323 9324 init(); 9325}); 9326 }; 9327</script>
9327 9328
9329<script> 9330 window.customWidgetsFunctions["8eedf98f346a42e0a55f9d6152653187~2"] = function (element, data, api) { 9331 window.dmAPI.runOnReady('dcbh2-h2-theme', () => { 9332 9333 const STYLE_ID = 'dcbh2-h2-theme-style'; 9334 const TARGET_SELECTOR = '.widget-wrapper.DCBH2'; 9335 9336 let updateTimer = null; 9337 let lastSignature = ''; 9338 9339 /** 9340 * Find a usable H2 that is NOT inside the DCBH2 widget. 9341 */ 9342 function findThemeH2() { 9343 const candidates = Array.from( 9344 document.querySelectorAll('#dm h2, h2') 9345 ); 9346 9347 for (const el of candidates) { 9348 9349 // Do not use our own DCBH2 widget as the source 9350 if (el.closest(TARGET_SELECTOR)) { 9351 continue; 9352 } 9353 9354 // Ignore disconnected elements 9355 if (!el.isConnected) { 9356 continue; 9357 } 9358 9359 const computed = window.getComputedStyle(el); 9360 9361 // Ignore unavailable / hidden elements 9362 if ( 9363 computed.display === 'none' || 9364 computed.visibility === 'hidden' 9365 ) { 9366 continue; 9367 } 9368 9369 // Make sure useful typography has loaded 9370 if (!computed.fontFamily || !computed.fontSize) { 9371 continue; 9372 } 9373 9374 return el; 9375 } 9376 9377 return null; 9378 } 9379 9380 9381 /** 9382 * Read H2 typography and apply it to DCBH2 widgets. 9383 */ 9384 function applyTheme() { 9385 9386 const h2Sample = findThemeH2(); 9387 9388 if (!h2Sample) { 9389 return false; 9390 } 9391 9392 const computed = window.getComputedStyle(h2Sample); 9393 9394 const styles = { 9395 fontFamily: computed.fontFamily, 9396 fontSize: computed.fontSize, 9397 fontWeight: computed.fontWeight, 9398 lineHeight: computed.lineHeight, 9399 color: computed.color, 9400 letterSpacing: computed.letterSpacing, 9401 textTransform: computed.textTransform, 9402 fontStyle: computed.fontStyle, 9403 fontVariant: computed.fontVariant, 9404 fontStretch: computed.fontStretch 9405 }; 9406 9407 /** 9408 * Only rewrite the stylesheet if something actually changed. 9409 */ 9410 const signature = JSON.stringify(styles); 9411 9412 if (signature === lastSignature) { 9413 return true; 9414 } 9415 9416 lastSignature = signature; 9417 9418 9419 /** 9420 * Target Duda RTE elements, including nested spans. 9421 */ 9422 const selectors = [ 9423 '#dm .dmInner .widget-wrapper.DCBH2 .dmNewParagraph', 9424 '#dm .dmInner .widget-wrapper.DCBH2 .dmNewParagraph p', 9425 '#dm .dmInner .widget-wrapper.DCBH2 .dmNewParagraph span', 9426 '#dm .dmInner .widget-wrapper.DCBH2 .dmNewParagraph .rteBlock', 9427 '#dm .dmInner .widget-wrapper.DCBH2 .dmNewParagraph .rteBlock *', 9428 9429 '.dmInner .widget-wrapper.DCBH2 .dmNewParagraph', 9430 '.dmInner .widget-wrapper.DCBH2 .dmNewParagraph p', 9431 '.dmInner .widget-wrapper.DCBH2 .dmNewParagraph span', 9432 '.dmInner .widget-wrapper.DCBH2 .dmNewParagraph .rteBlock', 9433 '.dmInner .widget-wrapper.DCBH2 .dmNewParagraph .rteBlock *', 9434 9435 '.widget-wrapper.DCBH2 .dmNewParagraph', 9436 '.widget-wrapper.DCBH2 .dmNewParagraph p', 9437 '.widget-wrapper.DCBH2 .dmNewParagraph span', 9438 '.widget-wrapper.DCBH2 .dmNewParagraph .rteBlock', 9439 '.widget-wrapper.DCBH2 .dmNewParagraph .rteBlock *' 9440 ]; 9441 9442 const css = ` 9443${selectors.join(',\n')} { 9444 font-family: ${styles.fontFamily} !important; 9445 font-size: ${styles.fontSize} !important; 9446 font-weight: ${styles.fontWeight} !important; 9447 line-height: ${styles.lineHeight} !important; 9448 color: ${styles.color} !important; 9449 letter-spacing: ${styles.letterSpacing} !important; 9450 text-transform: ${styles.textTransform} !important; 9451 font-style: ${styles.fontStyle} !important; 9452 font-variant: ${styles.fontVariant} !important; 9453 font-stretch: ${styles.fontStretch} !important; 9454} 9455`; 9456 9457 let styleTag = document.getElementById(STYLE_ID); 9458 9459 if (!styleTag) { 9460 styleTag = document.createElement('style'); 9461 styleTag.id = STYLE_ID; 9462 styleTag.setAttribute('data-dcbh2-h2-theme', 'true'); 9463 document.head.appendChild(styleTag); 9464 } 9465 9466 styleTag.textContent = css; 9467 9468 return true; 9469 } 9470 9471 9472 /** 9473 * Debounce repeated Duda DOM changes. 9474 */ 9475 function scheduleUpdate(delay = 100) { 9476 9477 clearTimeout(updateTimer); 9478 9479 updateTimer = setTimeout(() => { 9480 applyTheme(); 9481 }, delay); 9482 } 9483 9484 9485 /** 9486 * Initial passes. 9487 * 9488 * Duda/theme/font rendering can continue after runOnReady. 9489 */ 9490 applyTheme(); 9491 9492 setTimeout(applyTheme, 250); 9493 setTimeout(applyTheme, 500); 9494 setTimeout(applyTheme, 1000); 9495 setTimeout(applyTheme, 2000); 9496 setTimeout(applyTheme, 4000); 9497 9498 9499 /** 9500 * Watch for Duda re-rendering or dynamic content changes. 9501 */ 9502 const observer = new MutationObserver((mutations) => { 9503 9504 let relevantChange = false;
9505 9506 for (const mutation of mutations) { 9507 9508 // Ignore our own injected stylesheet 9509 if ( 9510 mutation.target && 9511 ( 9512 mutation.target.id === STYLE_ID || 9513 mutation.target.closest?.(`#${STYLE_ID}`) 9514 ) 9515 ) { 9516 continue; 9517 } 9518 9519 relevantChange = true; 9520 break; 9521 } 9522 9523 if (relevantChange) { 9524 scheduleUpdate(150); 9525 } 9526 9527 }); 9528 9529 observer.observe(document.body, { 9530 childList: true, 9531 subtree: true, 9532 attributes: true, 9533 attributeFilter: [ 9534 'class', 9535 'style' 9536 ] 9537 }); 9538 9539 9540 /** 9541 * Responsive breakpoint changes. 9542 */ 9543 window.addEventListener('resize', () => { 9544 scheduleUpdate(150); 9545 }); 9546 9547 9548 /** 9549 * Mobile orientation changes. 9550 */ 9551 window.addEventListener('orientationchange', () => { 9552 scheduleUpdate(250); 9553 }); 9554 9555 9556 /** 9557 * Reapply after web fonts finish loading. 9558 */ 9559 if (document.fonts && document.fonts.ready) { 9560 9561 document.fonts.ready.then(() => { 9562 9563 lastSignature = ''; 9564 9565 applyTheme(); 9566 9567 }).catch(() => { 9568 // Nothing required 9569 }); 9570 9571 } 9572 9573}); 9574 }; 9575</script>
9575 9576
9577<script> 9578 window.customWidgetsFunctions["403a957a34f84bc8a342137c59c93b31~3"] = function (element, data, api) { 9579 window.dmAPI.runOnReady('dcbp-p-theme', () => { 9580 'use strict'; 9581 9582 const STYLE_ID = 'dcbp-p-theme-styles'; 9583 9584 function getParagraphFontFamily() { 9585 const source = 9586 document.querySelector( 9587 '.widget-403a95 .cb2-theme-injector .dmNewParagraph p' 9588 ) || 9589 document.querySelector( 9590 '.widget-403a95 .cb2-theme-injector p' 9591 ) || 9592 document.querySelector( 9593 '.widget-403a95 .dmNewParagraph p' 9594 ) || 9595 document.querySelector( 9596 '.widget-403a95 p' 9597 ); 9598 9599 if (!source) { 9600 return null; 9601 } 9602 9603 const computed = window.getComputedStyle(source); 9604 const fontFamily = computed.fontFamily; 9605 9606 if (!fontFamily || fontFamily.trim() === '') { 9607 return null; 9608 } 9609 9610 return fontFamily; 9611 } 9612 9613 function applyTheme() { 9614 const dcbpElements = document.querySelectorAll( 9615 '#dm .DCBP, ' + 9616 '.DCBP, ' + 9617 '#dm .widget-wrapper.DCBP, ' + 9618 '.widget-wrapper.DCBP' 9619 ); 9620 9621 if (!dcbpElements.length) { 9622 return false; 9623 } 9624 9625 const fontFamily = getParagraphFontFamily(); 9626 9627 if (!fontFamily) { 9628 return false; 9629 } 9630 9631 const uSelectors = new Set(); 9632 9633 const paragraphElements = document.querySelectorAll( 9634 '#dm .DCBP .dmNewParagraph, ' + 9635 '.DCBP .dmNewParagraph' 9636 ); 9637 9638 paragraphElements.forEach(element => { 9639 element.classList.forEach(className => { 9640 if (!className.startsWith('u_')) { 9641 return; 9642 } 9643 9644 uSelectors.add(`div.${className}`); 9645 uSelectors.add(`div.${className} p`); 9646 uSelectors.add(`div.${className} li`); 9647 uSelectors.add(`div.${className} span`); 9648 9649 uSelectors.add(`#dm .dmInner div.${className}`); 9650 uSelectors.add(`#dm .dmInner div.${className} p`); 9651 uSelectors.add(`#dm .dmInner div.${className} li`); 9652 uSelectors.add(`#dm .dmInner div.${className} span`); 9653 }); 9654 }); 9655 9656 const baseSelectors = [ 9657 '#dm .dmInner .widget-wrapper.DCBP .dmNewParagraph', 9658 '#dm .dmInner .widget-wrapper.DCBP .dmNewParagraph p', 9659 '#dm .dmInner .widget-wrapper.DCBP .dmNewParagraph li', 9660 '#dm .dmInner .widget-wrapper.DCBP .dmNewParagraph span', 9661 '#dm .dmInner .widget-wrapper.DCBP .dmNewParagraph .rteBlock', 9662 9663 '.dmInner .widget-wrapper.DCBP .dmNewParagraph', 9664 '.dmInner .widget-wrapper.DCBP .dmNewParagraph p', 9665 '.dmInner .widget-wrapper.DCBP .dmNewParagraph li', 9666 '.dmInner .widget-wrapper.DCBP .dmNewParagraph span', 9667 '.dmInner .widget-wrapper.DCBP .dmNewParagraph .rteBlock', 9668 9669 '.widget-wrapper.DCBP .dmNewParagraph', 9670 '.widget-wrapper.DCBP .dmNewParagraph p', 9671 '.widget-wrapper.DCBP .dmNewParagraph li', 9672 '.widget-wrapper.DCBP .dmNewParagraph span', 9673 '.widget-wrapper.DCBP .dmNewParagraph .rteBlock', 9674 9675 '#dm .DCBP .dmNewParagraph', 9676 '#dm .DCBP .dmNewParagraph p', 9677 '#dm .DCBP .dmNewParagraph li', 9678 '#dm .DCBP .dmNewParagraph span', 9679 9680 '.DCBP .dmNewParagraph', 9681 '.DCBP .dmNewParagraph p', 9682 '.DCBP .dmNewParagraph li', 9683 '.DCBP .dmNewParagraph span' 9684 ]; 9685 9686 const allSelectors = [ 9687 ...baseSelectors, 9688 ...uSelectors 9689 ].join(',\n'); 9690 9691 const css = ` 9692${allSelectors} { 9693 font-family: ${fontFamily} !important; 9694} 9695`; 9696 9697 let styleTag = document.getElementById(STYLE_ID); 9698 9699 if (!styleTag) { 9700 styleTag = document.createElement('style'); 9701 styleTag.id = STYLE_ID; 9702 styleTag.setAttribute('data-dcbp-p-theme', 'true'); 9703 document.head.appendChild(styleTag); 9704 } 9705 9706 styleTag.textContent = css; 9707 9708 return true; 9709 } 9710 9711 if (applyTheme()) { 9712 return; 9713 } 9714 9715 let attempts = 0; 9716 const maxAttempts = 30; 9717 9718 const interval = window.setInterval(() => { 9719 attempts += 1; 9720 9721 if (applyTheme() || attempts >= maxAttempts) { 9722 window.clearInterval(interval); 9723 } 9724 }, 300); 9725}); 9726 }; 9727</script>
9727 9728
9729<script> 9730 window.customWidgetsFunctions["1c20f508b5474ea2b929959f7d5eff3d~3"] = function (element, data, api) { 9731 window.dmAPI.runOnReady('dcbh3-h3-theme', () => { 9732 9733 function applyTheme() { 9734 // Try multiple candidate selectors for the H3 style source 9735 // to work in both the editor and the published/preview environment 9736 const candidates = [ 9737 '#dm h3', 9738 'h3', 9739 ]; 9740 9741 let h3Sample = null; 9742 for (const sel of candidates) { 9743 h3Sample = document.querySelector(sel); 9744 if (h3Sample) break; 9745 } 9746 9747 if (!h3Sample) return false; 9748 9749 const computed = window.getComputedStyle(h3Sample); 9750 9751 const fontFamily = computed.fontFamily; 9752 const fontSize = computed.fontSize; 9753 const fontWeight = computed.fontWeight; 9754 const lineHeight = computed.lineHeight; 9755 const color = computed.color; 9756 const letterSpacing = computed.letterSpacing; 9757 const textTransform = computed.textTransform; 9758 const fontStyle = computed.fontStyle; 9759 9760 // Skip if nothing meaningful was computed yet 9761 if (!fontFamily || fontFamily === '') return false; 9762 9763 // Build selectors for every u_ class found on CB1 dmNewParagraph divs 9764 const uSelectors = []; 9765 const paragraphEls = document.querySelectorAll( 9766 '#dm .dmInner .widget-wrapper.DCBH3 .dmNewParagraph, ' + 9767 '.dmInner .widget-wrapper.DCBH3 .dmNewParagraph, ' + 9768 '.widget-wrapper.DCBH3 .dmNewParagraph' 9769 ); 9770 paragraphEls.forEach(el => { 9771 el.classList.forEach(cls => { 9772 if (cls.startsWith('u_')) { 9773 uSelectors.push(`div.${cls} p`); 9774 uSelectors.push(`div.${cls} li`); 9775 uSelectors.push(`*#dm .dmInner div.${cls} p`); 9776 uSelectors.push(`*#dm .dmInner div.${cls} li`); 9777 } 9778 }); 9779 }); 9780 9781 const baseSelectors = [ 9782 `#dm .dmInner .widget-wrapper.DCBH3 .dmNewParagraph`, 9783 `#dm .dmInner .widget-wrapper.DCBH3 .dmNewParagraph p`, 9784 `#dm .dmInner .widget-wrapper.DCBH3 .dmNewParagraph li`, 9785 `#dm .dmInner .widget-wrapper.DCBH3 .dmNewParagraph .rteBlock`, 9786 `.dmInner .widget-wrapper.DCBH3 .dmNewParagraph`, 9787 `.dmInner .widget-wrapper.DCBH3 .dmNewParagraph p`, 9788 `.dmInner .widget-wrapper.DCBH3 .dmNewParagraph li`, 9789 `.dmInner .widget-wrapper.DCBH3 .dmNewParagraph .rteBlock`, 9790 `.widget-wrapper.DCBH3 .dmNewParagraph`, 9791 `.widget-wrapper.DCBH3 .dmNewParagraph p`, 9792 `.widget-wrapper.DCBH3 .dmNewParagraph li`, 9793 `.widget-wrapper.DCBH3 .dmNewParagraph .rteBlock`, 9794 ]; 9795 9796 const allSelectors = [...baseSelectors, ...uSelectors].join(',\n '); 9797 9798 const css = ` 9799 ${allSelectors} { 9800 font-family: ${fontFamily} !important; 9801 font-size: ${fontSize} !important; 9802 font-weight: ${fontWeight} !important; 9803 line-height: ${lineHeight} !important; 9804 color: ${color} !important; 9805 letter-spacing: ${letterSpacing} !important; 9806 text-transform: ${textTransform} !important; 9807 font-style: ${fontStyle} !important; 9808 } 9809 `; 9810 9811 // Remove any previously injected tag before re-injecting 9812 const existing = document.querySelector('style[data-dcbh3-h3-theme]'); 9813 if (existing) existing.remove(); 9814 9815 const styleTag = document.createElement('style'); 9816 styleTag.setAttribute('data-dcbh3-h3-theme', 'true'); 9817 styleTag.textContent = css; 9818 document.head.appendChild(styleTag); 9819 9820 return true; 9821 } 9822 9823 // Try immediately 9824 if (applyTheme()) return; 9825 9826 // If not ready yet, retry with increasing delays to handle 9827 // async rendering in preview / published environments 9828 let attempts = 0; 9829 const maxAttempts = 40; 9830 const interval = setInterval(() => { 9831 attempts++; 9832 if (applyTheme() || attempts >= maxAttempts) { 9833 clearInterval(interval); 9834 } 9835 }, 500); 9836}); 9837 9838 }; 9839</script>
9839 9840 9841
9842<script type="text/javascript"> 9843 9844 var d_version = "production_6835"; 9845 var build = "2026-10-07T13_19_14"; 9846 window['v' + 'ersion'] = d_version; 9847 9848 function buildEditorParent() { 9849 window.isMultiScreen = true; 9850 window.editorParent = {}; 9851 window.previewParent = {}; 9852 try { 9853 var _p = window.parent; 9854 if (_p && _p.document && _p.$ && _p.$.dmfw) { 9855 window.editorParent = _p; 9856 } else if (_p.isSitePreview) { 9857 window.previewParent = _p; 9858 } 9859 } catch (e) { 9860 9861 } 9862 } 9863 9864 buildEditorParent(); 9865</script>
9865 9866 9867 9868 9869<!-- Injecting site-wide before scripts --> 9870 9871
9872<script src="https://app.termly.io/resource-blocker/f83e824a-6087-4027-807f-ad630395035c?autoBlock=on" data-swh-uuid="b147975169d64f0592785eac7c26b01a"> 9873</script>
9873 9874 9875 9876 9877 9878 9879 9880 9881 9882 9883 9884 9885 9886 9887 9888 9889 9890 9891 9892
9893<script type="text/javascript" id="d_track_campaign" data-entry-referrer data-utm-cookies></script>
9893 9894
9895<script type="text/javascript" > 9896 var _dm_gaq = {}; 9897 var _gaq = _gaq || []; 9898 var _dm_insite = []; 9899</script>
9899 9900 9901 9902 9903
9904<script async src="https://www.googletagmanager.com/gtm.js?id=GTM-W487CGFD" id="d-js-gtm" 9905 onload="window.dispatchEvent(new CustomEvent('gtmLoaded', { bubbles: true }))"></script>
9905 9906
9907<script type="text/javascript" id="d_track_ga"> 9908 window.dataLayer = window.dataLayer || []; 9909 _dm_gaq.siteAlias = 'f6cdfabba1c749a1a3d3b7a9f4c30c76'; 9910 _dm_gaq.externalGaqID = 'non-empty'; 9911 9912 function gtag() { dataLayer.push(arguments); } 9913 9914 function pushInsiteImpressions(id) { 9915 _dm_insite.forEach((rule) => { 9916 _dm_gaq.pushEvent(id, 'insite_impression', 'insite', rule.ruleType + '__' + rule.ruleId); 9917 }); 9918 } 9919 9920 _dm_gaq.pushEvent = function(trackingId, event, category, label, value, additionalParams = {}) { 9921 const eventProps = { 9922 event_category: category, 9923 event_label: label, 9924 value, 9925 ...additionalParams 9926 }; 9927 gtag('event', event, eventProps); 9928 } 9929 9930 gtag('js', new Date()) 9931 9932 9933gtag('set', 'developer_id.dMDU5Nj', true); 9934 9935 _dm_gaq.pushEvent('', 'page_view'); 9936 9937 pushInsiteImpressions(_dm_gaq.externalGaqID); 9938</script>
9938 9939 9940 9941 9942 9943 9944 9945<!-- End Injecting site-wide to the head --> 9946 9947 9948 9949 9950 9951
9952<script> 9953 window.cookiesNotificationMarkupPreview = 'null'; 9954</script>
9954 9955 9956
9957<script id='d-js-params'> 9958 window.INSITE = window.INSITE || {}; 9959 window.INSITE.device = "desktop"; 9960 9961 window.rtCommonProps = {}; 9962 rtCommonProps["runtimecollector.url"] = 'https://rtc.hibuwebsites.com'; 9963 rtCommonProps["performance.tabletPreview.removeScroll"] = 'false'; 9964 rtCommonProps["inlineEditGrid.snap"] =true; 9965 rtCommonProps["popup.insite.cookie.ttl"] = '0.5'; 9966 rtCommonProps["common.mapbox.token"] = 'pk.eyJ1IjoiZGFubnliMTIzIiwiYSI6ImNqMGljZ256dzAwMDAycXBkdWxwbDgzeXYifQ.Ck5P-0NKPVKAZ6SH98gxxw'; 9967 rtCommonProps["common.mapbox.js.override"] =false; 9968 rtCommonProps["common.here.appId"] = 'iYvDjIQ2quyEu0rg0hLo'; 9969 rtCommonProps["common.here.appCode"] = '1hcIxLJcbybmtBYTD9Z1UA'; 9970 rtCommonProps["ecommerce.ecwid.script"] = 'https://app.multiscreenstore.com/script.js'; 9971 rtCommonProps["common.build.dist.folder"] = 'production/6835'; 9972 rtCommonProps["common.resources.cdn.host"] = 'https://static-res-cdn.websites.hibu.com'; 9973 rtCommonProps["common.resources.folder"] = 'https://static-res-cdn.websites.hibu.com/mnlt/production/6835'; 9974 rtCommonProps["common.fonts.api.v2.url"] = 'https://cdn.hibuwebsites.com/fonts/css2?family='; 9975 rtCommonProps["feature.flag.runtime.backgroundSlider.preload.slowly"] =true; 9976 rtCommonProps["feature.flag.runtime.newAnimation.jitAnimation.enabled"] =true; 9977 rtCommonProps["feature.flag.sites.google.analytics.gtag"] =true; 9978 rtCommonProps["feature.flag.runOnReadyNewTask"] =true; 9979 rtCommonProps["isAutomation.test"] =false; 9980 rtCommonProps["booking.cal.api.domain"] = 'api.cal.com'; 9981 9982 9983 rtCommonProps['common.mapsProvider'] = 'mapbox'; 9984 9985 rtCommonProps['common.mapsProvider.version'] = '0.52.0'; 9986 rtCommonProps['common.geocodeProvider'] = 'mapbox'; 9987 rtCommonProps['server.for.resources'] = ''; 9988 rtCommonProps['feature.flag.lazy.widgets'] = true; 9989 rtCommonProps['feature.flag.single.wow'] = false; 9990 rtCommonProps['feature.flag.disallowPopupsInEditor'] = true; 9991 rtCommonProps['feature.flag.mark.anchors'] = true; 9992 rtCommonProps['captcha.public.key'] = '6LffcBsUAAAAAMU-MYacU-6QHY4iDtUEYv_Ppwlz'; 9993 rtCommonProps['captcha.invisible.public.key'] = '6LeiWB8UAAAAAHYnVJM7_-7ap6bXCUNGiv7bBPME'; 9994 rtCommonProps["images.sizes.small"] =160; 9995 rtCommonProps["images.sizes.mobile"] =640; 9996 rtCommonProps["images.sizes.tablet"] =1280; 9997 rtCommonProps["images.sizes.desktop"] =1920; 9998 rtCommonProps["import.images.storage.imageCDN"] = 'https://le-cdn.hibuwebsites.com/'; 9999 rtCommonProps["feature.flag.performance.logs"] =false; 10000 rtCommonProps["site.widget.form.captcha.type"] = 'g_recaptcha'; 10001 rtCommonProps["friendly.captcha.site.key"] = 'FCMGSQG9GVNMFS8K'; 10002 rtCommonProps["cookiebot.mapbox.consent.category"] = 'marketing'; 10003 rtCommonProps["termly.mapbox.consent.category"] = 'performance'; 10004 // feature flags that's used out of runtime module (in legacy files) 10005 rtCommonProps["platform.monolith.personalization.dateTimeCondition.popupMsgAction.moveToclient.enabled"] =true; 10006 10007 window.rtFlags = {}; 10008 rtFlags["unsuspendEcwidStoreOnRuntime.enabled"] =false; 10009 rtFlags["scripts.widgetCount.enabled"] =true; 10010 rtFlags["fnb.animations.tracking.enabled"] =true; 10011 rtFlags["ecom.ecwidNewUrlStructure.enabled"] = false; 10012 rtFlags["ecom.ecwid.accountPage.emptyBaseUrl.enabled"] = true; 10013 rtFlags["ecom.ecwid.accountPage.repeatOrderRedirect.enabled"] = false; 10014 rtFlags["ecom.ecwid.oldStore.keepScrollOnLoad.enabled"] = true; 10015 rtFlags["ecom.ecwid.storefrontV3.enabled"] = false; 10016 rtFlags["ecom.monolith.legacy.js.api.disabled.for.new.sites"] = false; 10017 rtFlags["geocode.search.localize"] =false; 10018 rtFlags["feature.flag.runtime.newAnimation.asyncInit.setTimeout.enabled"] =false; 10019 rtFlags["twitter.heightLimit.enabled"] = true; 10020 rtFlags["streamline.monolith.personalization.supportMultipleConditions.enabled"] =false; 10021 rtFlags["flex.animation.design.panel.layout"] =true; 10022 rtFlags["runtime.cwv.report.cls.enabled"] =false; 10023 rtFlags["runtime.cwv.report.lcp.enabled"] =false; 10024 rtFlags["runtime.lcp.preload.selfheal.enabled"] =true; 10025 rtFlags["runtime.lcp.preload.bootstrap.enabled"] =true; 10026 rtFlags["runtime.lcp.preload.monitor.enabled"] =false; 10027 rtFlags["runtime.customwidget.heightpin.enabled"] =true; 10028 rtFlags["runtime.video.widget.lazy.enabled"] =true; 10029 rtFlags["contact.form.useActiveForm"] =true; 10030 rtFlags["contact.form.custom.errors.enabled"] =false; 10031 rtFlags["runtime.ssr.productStore.internal.observer"] =true; 10032 rtFlags["editor.widget.form.a11y"] =true; 10033</script>
vendor: 1 bytes, line 10033
10033
10034<script src="https://static-res-cdn.websites.hibu.com/mnlt/production/6835/_dm/s/rt/dist/scripts/d-js-runtime-flex-package.min.js" id="d-js-core" defer></script>
10034 10035 10036 10037 10038 10039 10040 10041 10042 10043 10044 10045 10046 10047 10048 10049 10050
10051<script> 10052 document.addEventListener('DOMContentLoaded', () => 10053 window.runtime.initLayout() 10054 ); 10055</script>
10055 10056 10057 10058 10059 10060 10061 10062 10063 10064 10065 10066 10067 10068 10069 10070 10071 10072 10073 10074
10075<script type="text/javascript" id="d_track_sp"> 10076 ;(function(p,l,o,w,i,n,g){if(!p[i]){p.GlobalSnowplowNamespace=p.GlobalSnowplowNamespace||[]; 10077 p.GlobalSnowplowNamespace.push(i);p[i]=function(){(p[i].q=p[i].q||[]).push(arguments) 10078 };p[i].q=p[i].q||[];n=l.createElement(o);g=l.getElementsByTagName(o)[0];n.async=1; 10079 n.src=w;g.parentNode.insertBefore(n,g)}}(window,document,"script","//d32hwlnfiv2gyn.cloudfront.net/sp-2.0.0-dm-0.1.min.js","snowplow")); 10080 window.dmsnowplow = window.snowplow; 10081 10082 dmsnowplow('newTracker', 'cf', 'd32hwlnfiv2gyn.cloudfront.net', { // Initialise a tracker 10083 appId: 'f6cdfabba1c749a1a3d3b7a9f4c30c76' 10084 }); 10085 10086 // snowplow queries element styles so we wait until CSS calculations are done. 10087 requestAnimationFrame(() => { 10088 dmsnowplow('trackPageView'); 10089 _dm_insite.forEach((rule) => { 10090 // Specifically in popup only the client knows if it is shown or not so we don't always want to track its impression here 10091 // the tracking is in popup.js 10092 if (rule.actionName !== "popup") { 10093 dmsnowplow('trackStructEvent', 'insite', 'impression', rule.ruleType, rule.ruleId); 10094 } 10095 window?.waitForDeferred?.('dmAjax', () => { 10096 $.DM.events.trigger('event-ruleTriggered', {value: rule}); 10097 }); 10098 }); 10099 }); 10100</script>
10100 10101 10102 10103 10104<div style="display:none;" id="P6iryBW0Wu"></div> 10105 10106 10107 10108 10109 10110 10111 10112 10113 10114 10115 10116<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true"> 10117 10118 10119 <div class="pswp__bg"></div> 10120 10121 10122 <div class="pswp__scroll-wrap"> 10123 10124 10125 <div class="pswp__container"> 10126 <div class="pswp__item"></div> 10127 <div class="pswp__item"></div> 10128 <div class="pswp__item"></div> 10129 </div> 10130 10131 10132 <div class="pswp__ui pswp__ui--hidden"> 10133 10134 <div class="pswp__top-bar"> 10135 10136 10137 10138 <div class="pswp__counter"></div> 10139 10140 <button class="pswp__button pswp__button--close" data-auto="pswp-close" title="Close (Esc)"></button> 10141 10142 <button class="pswp__button pswp__button--share" data-auto="pswp-share" title="Share"></button> 10143 10144 <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button> 10145 10146 <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button> 10147 10148 10149 10150 <div class="pswp__preloader"> 10151 <div class="pswp__preloader__icn"> 10152 <div class="pswp__preloader__cut"> 10153 <div class="pswp__preloader__donut"></div> 10154 </div> 10155 </div> 10156 </div> 10157 </div> 10158 10159 <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap"> 10160 <div class="pswp__share-tooltip"></div> 10161 </div> 10162 10163 <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)"> 10164 </button> 10165 10166 <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)"> 10167 </button> 10168 10169 <div class="pswp__caption"> 10170 <div class="pswp__caption__center"></div> 10171 </div> 10172 10173 </div> 10174 10175 </div> 10176 10177</div> 10178<div id="fb-root" 10179 data-locale="en"></div> 10180<!-- Alias: f6cdfabba1c749a1a3d3b7a9f4c30c76 -->
10181<script id="d_track_personalization"> 10182 window?.waitForDeferred?.('dmAjax', () => { 10183 // Collects client data and updates cookies used by smart sites 10184 window.expireDays = 365; 10185 window.visitLength = 30 * 60000; 10186 $.setCookie("dm_timezone_offset", (new Date()).getTimezoneOffset(), window.expireDays); 10187 setSmartSiteCookiesInternal("dm_this_page_view","dm_last_page_view","dm_total_visits","dm_last_visit"); 10188 }); 10189</script>
vendor: 1 bytes, line 10189
10189
10190<script type="text/javascript"> 10191 10192 Parameters.NavigationAreaParams.MoreButtonText = 'MORE'; 10193 10194 Parameters.NavigationAreaParams.LessButtonText = 'LESS'; 10195 </script>
vendor: 1 bytes, line 10195
10195
10196<script id="ssr-empty-script" type="text/javascript"></script>
10196<!-- Begin Body End Markup --> 10197<!-- Begin Body End Markup --><!-- Begin Name of Code --><!-- End Name of Code --><div id="placeholder_retargeting_pixel" style="display: none;" data-inject="placeholder_retargeting_pixel"> 10198 10199 10200 10201 {{placeholder_retargeting_pixel}} 10202</div> 10203<div id="placeholder_dpni" style="display: none;" data-inject="placeholder_dpni"> 10204 <!-- phone insertion script begins -->
10204<script type="text/javascript" src="https://reports.hibu.com/analytics/js/ybDynamicPhoneInsertion.js"> 10205//<![CDATA[ 10206 10207 10208//]]> 10209 </script>
10209 10210
10210<script> 10211 10212//<![CDATA[ 10213 10214 10215ybFindPhNums = ['18602152254']; 10216ybReplacePhNums = ['18602220150']; 10217 10218 document.addEventListener("YextPhoneChangeEvent", yextPhoneChangeEventHandler, false); 10219 10220 function yextPhoneChangeEventHandler(e) { 10221 e.preventDefault(); 10222 ybFun_ReplaceText(); 10223 } 10224 10225 if (typeof dmAPI != 'undefined') { 10226 dmAPI.runOnReady('dpni', function() { 10227 setTimeout(ybFun_ReplaceText, 500); 10228 }); 10229 window.addEventListener('DOMContentLoaded', function() { 10230 dmAPI.subscribeEvent(dmAPI.EVENTS.SHOW_POPUP, 10231 function(data) { 10232 setTimeout(ybFun_ReplaceText, 500); 10233 }); 10234 }); 10235 } else { 10236 window.onload = function() { 10237 setTimeout(ybFun_ReplaceText, 500); 10238 } 10239 } 10240 10241 10242//]]> 10243 10244 </script>
10244 10245 <!-- phone insertion script ends --> 10246</div> 10247<div id="placeholder_analytics" style="display: none;" data-inject="placeholder_analytics"> 10248
10248<script> 10249 10250 10251//<![CDATA[ 10252 10253//yellAnalyticsConfig need to set before s_code.js 10254var yellAnalyticsConfig = { 10255"global" : { 10256 "trackDownloadLinks": true, 10257 "downloadFileTypes": "exe,zip,wav,mp3,mov,mpg,avi,wmv,pdf,doc,docx,xls,xlsx,ppt,pptx", 10258 "currency": "USD", 10259 "trackExternalLinks": true, 10260 "internalDomains": "javascript:", 10261 "charset": "UTF-8" 10262}, 10263"omniture" : { 10264 "enabled": true, 10265 "server": "yellgroup.122.2o7.net", 10266 "serverSecure": "yellgroup.122.2o7.net", 10267 "namespace": "yellgroup", 10268 "account": "yelldudalive", 10269 "campaign": "CID|cam" 10270}} 10271var hibuWebsiteConfig={ 10272 "prodSKU":"WBS-PRO-01", 10273 "assetID":"700161096", 10274 "customerID":"7003792510" 10275} 10276 10277//]]> 10278 10279 10280 </script>
10280 10281
10281<script type="text/javascript" src="https://dh-static-files.s3.amazonaws.com/prod/AppMeasurement.js"> 10282 10283//<![CDATA[ 10284 10285 10286//]]> 10287 10288 </script>
10288 10289
10289<script type="text/javascript" src="https://dh-static-files.s3.amazonaws.com/prod/hibu-analytics.min.js" defer=""> 10290 10291//<![CDATA[ 10292 10293 10294//]]> 10295 10296 </script>
10296 10297
10297<script type="text/javascript" src="https://dh-static-files.s3.amazonaws.com/prod/omn_setting.js" defer=""> 10298 10299//<![CDATA[ 10300 10301 10302//]]> 10303 10304 </script>
10304 10305</div> 10306<div id="placeholder_footer_reserve1" style="display: none;" data-inject="placeholder_footer_reserve1"> 10307
10307<script> 10308 10309 10310//<![CDATA[ 10311 10312document.addEventListener("DOMContentLoaded", function () { 10313 function deoptimizeImageUrl(url) { 10314 // Only process if URL contains both /opt/ and -640w 10315 if (!url.includes('/opt/') || !url.includes('-640w')) return url; 10316 10317 return url 10318 .replace('https://le-cdn.hibuwebsites.com/', 'https://cdn.hibuwebsites.com/') 10319 .replace('/opt/', '/') 10320 .replace(/-640w(?=\.\w+$)/, ''); 10321 } 10322 10323 // Select all elements with style attributes containing '-640w' 10324 document.querySelectorAll('[style*="-640w"]').forEach(el => { 10325 const originalStyle = el.getAttribute('style'); 10326 10327 // Replace each background-image URL that matches our pattern 10328 const updatedStyle = originalStyle.replace( 10329 /url\(["']?(https:\/\/le-cdn\.hibuwebsites\.com[^"')]+)["']?\)/g, 10330 (match, url) => { 10331 const newUrl = deoptimizeImageUrl(url); 10332 return `url('${newUrl}')`; 10333 } 10334 ); 10335 10336 // Update the element's style attribute 10337 el.setAttribute('style', updatedStyle); 10338 }); 10339}); 10340 10341//]]> 10342 10343 10344 </script>
10344 10345</div> 10346<div id="placeholder_footer_reserve2" style="display: none;" data-inject="placeholder_footer_reserve2"> 10347 <!-- Facebook Pixel Code -->
10347<script nonce="yV7Whw1O"> 10348//<![CDATA[
vendor: 398 bytes, lines 10348-10356
10348 10349 10350!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod? 10351n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n; 10352n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0; 10353t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window, 10354document,'script','https://connect.facebook.net/en_US/fbevents.js'); 10355 10356fbq('init', '
103561382224864124864
vendor: 31 bytes, lines 10356-10357
10356'); 10357fbq('track', "PageView");
10358//]]> 10359 </script>
10359 10360 <noscript> 10361 <img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1382224864124864&ev=PageView&noscript=1"/> 10362 </noscript> 10363 <!-- End Facebook Pixel Code --> 10364</div> 10365<div id="placeholder_footer_reserve3" style="display: none;" data-inject="placeholder_footer_reserve3"> 10366
10366<script> 10367 10368 10369//<![CDATA[ 10370 10371 (() => { 10372 // Hide any flex wrapper whose button text contains "do not use" 10373 function hideDoNotUse(root = document) { 10374 const wrappers = root.querySelectorAll('.flex-element.widget-wrapper'); 10375 wrappers.forEach(wrap => { 10376 const btn = wrap.querySelector('a.dmButtonLink, a.flexButton, a[data-element-type="dButtonLinkId"]'); 10377 if (!btn) return; 10378 const text = (btn.textContent || '').toLowerCase().trim(); 10379 if (text.includes('do not use')) { 10380 wrap.style.display = 'none'; 10381 wrap.setAttribute('data-hidden-reason', 'do-not-use'); 10382 } 10383 }); 10384 } 10385 // Run once on ready 10386 if (document.readyState === 'loading') { 10387 document.addEventListener('DOMContentLoaded', () => hideDoNotUse()); 10388 } else { 10389 hideDoNotUse(); 10390 } 10391 // Watch for dynamically inserted content or text changes 10392 const mo = new MutationObserver(mutations => { 10393 for (const m of mutations) { 10394 m.addedNodes.forEach(node => { 10395 if (node.nodeType !== 1) return; 10396 if (node.matches && node.matches('.flex-element.widget-wrapper')) { 10397 hideDoNotUse(node); 10398 } else { 10399 // Check if a new/changed node sits inside a wrapper 10400 const wrap = node.closest && node.closest('.flex-element.widget-wrapper'); 10401 if (wrap) hideDoNotUse(wrap); 10402 else hideDoNotUse(node); 10403 } 10404 }); 10405 if (m.type === 'characterData' || m.type === 'childList') { 10406 const wrap = m.target.closest && m.target.closest('.flex-element.widget-wrapper'); 10407 if (wrap) hideDoNotUse(wrap); 10408 } 10409 } 10410 }); 10411 mo.observe(document.documentElement, { 10412 childList: true, 10413 subtree: true, 10414 characterData: true 10415 }); 10416})(); 10417 10418//]]> 10419 10420 10421 </script>
10421 10422</div> 10423<div id="placeholder_footer_reserve4" style="display: none;" data-inject="placeholder_footer_reserve4"> 10424 10425 10426 10427 {{placeholder_footer_reserve4}} 10428</div> 10429<div id="placeholder_footer_reserve5" style="display: none;" data-inject="placeholder_footer_reserve5"> 10430 10431 10432 10433 {{placeholder_footer_reserve5}} 10434</div> 10435<div id="placeholder_footer_reserve6" style="display: none;" data-inject="placeholder_footer_reserve6"> 10436 10437 10438 10439 {{placeholder_footer_reserve6}} 10440</div> 10441<div id="placeholder_footer_reserve7" style="display: none;" data-inject="placeholder_footer_reserve7"> 10442 10443 10444 10445 {{placeholder_footer_reserve7}} 10446</div> 10447<!-- End Body End Markup --><!-- form dropdown code begin -->
10447<script> 10448//<![CDATA[ 10449function waitForDmAPI(callback) { 10450 if (typeof dmAPI !== 'undefined') { 10451 callback(); 10452 } else { 10453 setTimeout(() => waitForDmAPI(callback), 100); 10454 } 10455} 10456 10457document.addEventListener("DOMContentLoaded", function () { 10458 waitForDmAPI(async function () { 10459 async function getServicesAndProducts() { 10460 try { 10461 var collection = await dmAPI.loadCollectionsAPI(); 10462 var formData = await collection.data("form").get(); 10463 10464 if (formData?.values?.length > 0) { 10465 let servicesHtml = formData.values[0].data["Services & Products"]; 10466 10467 let tempDiv = document.createElement("div"); 10468 tempDiv.innerHTML = servicesHtml; 10469 10470 return Array.from(tempDiv.querySelectorAll("p")).map(p => 10471 p.textContent.trim() 10472 ); 10473 } 10474 } catch (err) { 10475 console.error("Failed to load form data:", err); 10476 } 10477 10478 return []; 10479 } 10480 10481 async function replaceInputWithDropdown() { 10482 let servicesArray = await getServicesAndProducts(); 10483 if (servicesArray.length === 0) return; 10484 10485 // Add "Services" to the beginning and "Other" to the end 10486 servicesArray = [...servicesArray, "Other"]; 10487 10488 let labels = document.querySelectorAll("label"); 10489 labels.forEach(label => { 10490 let labelText = label.textContent.trim(); 10491 if ( 10492 labelText.includes("Services") || 10493 labelText.includes("Products") || 10494 labelText.includes("Case Study") 10495 ) { 10496 let input = label.parentElement.querySelector("input[type='text']"); 10497 if (input) { 10498 let select = document.createElement("select"); 10499 select.name = input.name; 10500 select.id = input.id; 10501 10502 servicesArray.forEach(service => { 10503 let option = document.createElement("option"); 10504 option.value = service; 10505 option.textContent = service; 10506 select.appendChild(option); 10507 }); 10508 10509 input.replaceWith(select); 10510 } 10511 } 10512 }); 10513 } 10514 10515 await replaceInputWithDropdown(); 10516 }); 10517}); 10518//]]> 10519</script>
10519 10520<!-- form dropdown code end --><!-- form button code begin -->
10520<script> 10521//<![CDATA[ 10522 10523 function waitForDmAPI(callback) { 10524 if (typeof dmAPI !== 'undefined') { 10525 callback(); 10526 } else { 10527 setTimeout(() => waitForDmAPI(callback), 100); 10528 } 10529 } 10530 10531 async function getFormText() { 10532 var collection = await dmAPI.loadCollectionsAPI(); 10533 var buttonsData = await collection.data("buttons").get(); 10534 10535 if (buttonsData.values.length > 0) { 10536 let formHtml = buttonsData.values[0].data.Form_Button; 10537 let tempDiv = document.createElement("div"); 10538 tempDiv.innerHTML = formHtml; 10539 return tempDiv.textContent || tempDiv.innerText; 10540 } 10541 10542 return null; 10543 } 10544 10545 async function updateSubmitButton() { 10546 try { 10547 let formText = await getFormText(); 10548 if (!formText) { 10549 console.warn("No form text found."); 10550 return; 10551 } 10552 10553 let buttons = document.querySelectorAll('input[type="submit"][value="Form Button"]'); 10554 buttons.forEach(button => { 10555 button.value = formText; 10556 }); 10557 10558 console.log("Updated submit buttons with new value:", formText); 10559 } catch (error) { 10560 console.error("Error updating submit buttons:", error); 10561 } 10562 } 10563 10564 document.addEventListener("DOMContentLoaded", function () { 10565 waitForDmAPI(updateSubmitButton); 10566 }); 10567 10568//]]> 10569</script>
10569 10570<!-- form button code end --><!-- End Body End Markup --> 10571<!-- Site Wide Html Markup --> 10572<!-- Site Wide Html Markup --> 10573<!-- Begin Product Custom HTML Markup --> 10574<div id="fb-root"></div>
10575<script>(function(d, s, id) { 10576var js, fjs = d.getElementsByTagName(s)[0]; 10577if (d.getElementById(id)) return; 10578js = d.createElement(s); js.id = id; 10579js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.8&appId=1822295708016537"; 10580fjs.parentNode.insertBefore(js, fjs); 10581}(document, 'script', 'facebook-jssdk'));</script>
vendor: 2 bytes, line 10581
10581
10582<script> 10583//make sure video's on iOS load first screen of the video. 10584dmAPI.runOnReady('russTest',function() { 10585 var _hibu_vid = document.getElementsByTagName('video'); 10586 for (var i = 0; i < _hibu_vid.length; i++) { 10587 _hibu_vid[i].load(); 10588 } 10589}) 10590</script>
vendor: 2 bytes, line 10590
10590
10591<script> 10592//simple addition by Russ to add consent banner link into footer - updated by Adrian to account for Termly installs. 10593(function () { 10594 var poweredByFirstAnchor = document.querySelector('.poweredBy a:first-child'); 10595 if (!poweredByFirstAnchor) return; 10596 10597 if (window.Termly) { 10598 var termlySpan = document.createElement('span'); 10599 termlySpan.className = 'termly-display-preferences'; 10600 termlySpan.textContent = 'Manage Consent Preferences'; 10601 poweredByFirstAnchor.insertAdjacentElement('afterend', termlySpan); 10602 } else { 10603 var spSpan = document.createElement('span'); 10604 spSpan.id = 'spConsentWrapper'; 10605 poweredByFirstAnchor.insertAdjacentElement('afterend', spSpan); 10606 window.localStorage.setItem('__springtime__repop__target', '#spConsentWrapper'); 10607 } 10608})(); 10609</script>
10609 10610<style> 10611 #spConsentWrapper:before, 10612 .poweredBy .termly-display-preferences:before { content: " | " } 10613 .poweredBy .termly-display-preferences { cursor: pointer; } 10614</style>
10615<script> 10616if (window.location.search.indexOf('preview') > 0) { 10617 window.localStorage.setItem('ae_user_consent_decision_has_been_made', true); 10618 var spEl = document.getElementById('spConsentWrapper'); 10619 if (spEl) spEl.remove(); 10620} 10621</script>
10621<!-- End Product Custom HTML Markup --> 10622</body> 10623</html>
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.