1<!doctype html><html lang="en-US" dir="ltr" data-nojs><head><title>Remote Power Case Studies & Applications | Tycon Systems®</title><meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><meta name="generator" content="Zoho Commerce, https://www.zoho.com/commerce"/><link rel="icon" href="/favicon.png"/><link rel="canonical" href="https://www.tyconsystems.com/blogs/tycon-cases-applications-articles/"/><meta name="twitter:card" content="summary"><meta name="twitter:url" content="https://www.tyconsystems.com/blogs/tycon-cases-applications-articles/"><meta name="twitter:title" content="Remote Power Case Studies & Applications | Tycon Systems®"><meta name="twitter:description" content="See how off-grid solar, mobile solar, outdoor UPS, PoE and wireless systems support surveillance, networking, monitoring and remote infrastructure."><meta property="og:type" content="website"><meta property="og:url" content="https://www.tyconsystems.com/blogs/tycon-cases-applications-articles/"><meta property="og:title" content="Remote Power Case Studies & Applications | Tycon Systems®"><meta property="og:description" content="See how off-grid solar, mobile solar, outdoor UPS, PoE and wireless systems support surveillance, networking, monitoring and remote infrastructure."><meta name="description" content="See how off-grid solar, mobile solar, outdoor UPS, PoE and wireless systems support surveillance, networking, monitoring and remote infrastructure."/><link href="https://zfcommerce.nimbuspop.com/IDC/css/zstore-core.css" rel="preload" type="text/css" as="style"><link href="https://zfcommerce.nimbuspop.com/IDC/css/zstore-core.css" rel="stylesheet" type="text/css"><link rel="preconnect" href="//img.zohostatic.com"><link rel="preconnect" href="//static.zohocdn.com"><link rel="preconnect" href="//https://cdn1.zohoecommerce.com"><link href="https://cdn1.zohoecommerce.com/template/375ee84bd3084deaabe9df8e5a916f45/stylesheets/style.css?v=1790202815786&storefront_domain=www.tyconsystems.com" onerror="this.onerror=null;this.href='/template/375ee84bd3084deaabe9df8e5a916f45/stylesheets/style.css'" rel="preload" type="text/css" as="style"><link href="https://cdn1.zohoecommerce.com/template/375ee84bd3084deaabe9df8e5a916f45/stylesheets/style.css?v=1790202815786&storefront_domain=www.tyconsystems.com" onerror="this.onerror=null;this.href='/template/375ee84bd3084deaabe9df8e5a916f45/stylesheets/style.css'" rel="stylesheet" type="text/css"><link href="https://cdn1.zohoecommerce.com/template/375ee84bd3084deaabe9df8e5a916f45/stylesheets/sub-style.css?v=1790202815786&storefront_domain=www.tyconsystems.com" onerror="this.onerror=null;this.href='/template/375ee84bd3084deaabe9df8e5a916f45/stylesheets/sub-style.css'" rel="preload" type="text/css" as="style"><link href="https://cdn1.zohoecommerce.com/template/375ee84bd3084deaabe9df8e5a916f45/stylesheets/sub-style.css?v=1790202815786&storefront_domain=www.tyconsystems.com" onerror="this.onerror=null;this.href='/template/375ee84bd3084deaabe9df8e5a916f45/stylesheets/sub-style.css'" rel="stylesheet" type="text/css"><link href="https://cdn1.zohoecommerce.com/template/375ee84bd3084deaabe9df8e5a916f45/stylesheets/blog-style.css?v=1790202815786&storefront_domain=www.tyconsystems.com" onerror="this.onerror=null;this.href='/template/375ee84bd3084deaabe9df8e5a916f45/stylesheets/blog-style.css'" rel="preload" type="text/css" as="style"><link href="https://cdn1.zohoecommerce.com/template/375ee84bd3084deaabe9df8e5a916f45/stylesheets/blog-style.css?v=1790202815786&storefront_domain=www.tyconsystems.com" onerror="this.onerror=null;this.href='/template/375ee84bd3084deaabe9df8e5a916f45/stylesheets/blog-style.css'" rel="stylesheet" type="text/css"><link rel="preload" type="text/css" href="//webfonts.zoho.com/css?family=Alice/Source Sans Pro:300,400,600,700/Source Code Pro/Poppins:100,200,300,400,500,600,700/Oswald:400/Inter:900,800,700,600,500,300,200,100,400,700i,100i,800i,300i,400i,900i,
1600i,500i,200i/Poppins:900,800,700,600,500,200,300,100,400,700i,900i,600i,500i,800i,400i,300i,200i,100i/Manrope:800,700,600,500,300,200,400/Arvo/Arimo/Raleway:700,600,400/Source Sans Pro:100,300,400/Muli:100,300,400/Karla/Noto Sans:100,300,400/Roboto:300,400,500,700,900/Comfortaa/Lato:300,400/OSP DIN:400/Days One:400/Anton:400/Archivo Black:400/Hammersmith One:400/Jura:400/Anonymous Pro:700,400/Michroma:400/Open Sans:400/Prosto One:400/Oswald:700,500,600,300,200,400/Roboto Mono:400/Rubik:300,400,500,700,900/Abel/Lora:400,700&display=swap" as="style"><link rel="stylesheet" type="text/css" href="//webfonts.zoho.com/css?family=Alice/Source Sans Pro:300,400,600,700/Source Code Pro/Poppins:100,200,300,400,500,600,700/Oswald:400/Inter:900,800,700,600,500,300,200,100,400,700i,100i,800i,300i,400i,900i,600i,500i,200i/Poppins:900,800,700,600,500,200,300,100,400,700i,900i,600i,500i,800i,400i,300i,200i,100i/Manrope:800,700,600,500,300,200,400/Arvo/Arimo/Raleway:700,600,400/Source Sans Pro:100,300,400/Muli:100,300,400/Karla/Noto Sans:100,300,400/Roboto:300,400,500,700,900/Comfortaa/Lato:300,400/OSP DIN:400/Days One:400/Anton:400/Archivo Black:400/Hammersmith One:400/Jura:400/Anonymous Pro:700,400/Michroma:400/Open Sans:400/Prosto One:400/Oswald:700,500,600,300,200,400/Roboto Mono:400/Rubik:300,400,500,700,900/Abel/Lora:400,700&display=swap"><link href="https://cdn1.zohoecommerce.com/zs-customcss.css?v=1790202815786&storefront_domain=www.tyconsystems.com" rel="preload" type="text/css" as="style"><link href="https://cdn1.zohoecommerce.com/zs-customcss.css?v=1790202815786&storefront_domain=www.tyconsystems.com" rel="stylesheet" type="text/css">
1<script>document.documentElement.removeAttribute('data-nojs');</script>
1<script>window.zs_content_format="0";window.zs_resource_url = "/blogs";window.isDefaultLogo = "false";window.zs_site_resource_id = "76150000000002011";</script>
1<script>window.is_portal_site="true";</script>
1<script src="https://cdn1.zohoecommerce.com/zs-lang_en_US.js?v=1790202815786&storefront_domain=www.tyconsystems.com" defer></script>
1<script src="https://zfcommerce.nimbuspop.com/IDC/js/zstore-core.js" defer></script>
1<script src="https://cdn1.zohoecommerce.com/template/375ee84bd3084deaabe9df8e5a916f45/js/header.js?v=1790202815786&storefront_domain=www.tyconsystems.com" onerror="this.onerror=null;this.src='/template/375ee84bd3084deaabe9df8e5a916f45/js/header.js'" defer></script>
1<script src="https://cdn1.zohoecommerce.com/template/375ee84bd3084deaabe9df8e5a916f45/js/eventhandler.js?v=1790202815786&storefront_domain=www.tyconsystems.com" onerror="this.onerror=null;this.src='/template/375ee84bd3084deaabe9df8e5a916f45/js/eventhandler.js'" defer></script>
1<script src="https://cdn1.zohoecommerce.com/template/375ee84bd3084deaabe9df8e5a916f45/js/megamenu.js?v=1790202815786&storefront_domain=www.tyconsystems.com" onerror="this.onerror=null;this.src='/template/375ee84bd3084deaabe9df8e5a916f45/js/megamenu.js'" defer></script>
1<script src="https://cdn1.zohoecommerce.com/template/375ee84bd3084deaabe9df8e5a916f45/js/language-list.js?v=1790202815786&storefront_domain=www.tyconsystems.com" onerror="this.onerror=null;this.src='/template/375ee84bd3084deaabe9df8e5a916f45/js/language-list.js'" defer></script>
1<script src="https://cdn1.zohoecommerce.com/template/375ee84bd3084deaabe9df8e5a916f45/js/custom.js?v=1790202815786&storefront_domain=www.tyconsystems.com" onerror="this.onerror=null;this.src='/template/375ee84bd3084deaabe9df8e5a916f45/js/custom.js'" defer></script>
1<script src="https://cdn1.zohoecommerce.com/template/375ee84bd3084deaabe9df8e5a916f45/js/store.js?v=1790202815786&storefront_domain=www.tyconsystems.com" onerror="this.onerror=null;this.src='/template/375ee84bd3084deaabe9df8e5a916f45/js/store.js'" defer></script>
1<script src="https://cdn1.zohoecommerce.com/template/375ee84bd3084deaabe9df8e5a916f45/js/products-pagination.js?v=1790202815786&storefront_domain=www.tyconsystems.com" onerror="this.onerror=null;this.src='/template/375ee84bd3084deaabe9df8e5a916f45/js/products-pagination.js'" defer></script>
1<script>window.zs_data_center="USA";</script>
1<script>window.stand_alone_path="";</script>
1<script>window.zs_rendering_mode="live";</script>
1<script>window.is_social_share_enabled="true";</script>
1<script src="https://zfcommerce.nimbuspop.com/IDC/js/browser_compatibility.js" defer></script>
1<script>if(!document.getElementById('zsiqscript')){window.$zoho=window.$zoho||{};$zoho.salesiq=$zoho.salesiq||{ready:function(){$zoho.salesiq.floatbutton.visible('hide');}}}</script>
1<script id="zsiqscript" src="https://salesiq.zoho.com/widget?wc=siqafe49fe200868ef212d1028885d872e0d6b1d0003a16ca93c90109ceacdf5e35" defer></script>
1<script>window.zs_resource_id = "76150000005987557";window.zs_resource_type = "6";window.zs_resource_full_path = "/blogs/tycon-cases-applications-articles/";window.zs_content_json = {};</script>
1<meta name="google-site-verification" content=""/>
vendor: 63 bytes, line 1
1<script async src='https://www.googletagmanager.com/gtag/js?id=
1G-4H2X513REQ
vendor: 11 bytes, line 1
1'></script>
1<script>
vendor: 124 bytes, line 1
1window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('config', '
1G-4H2X513REQ
vendor: 3 bytes, line 1
1');
1gtag('config', 'AW-1009783359');</script>
1<!-- ============================================================ Devin Hales | Added May 2026 â issue #18 Shared XMLHttpRequest interceptor for the checkout "address" XHR. Other scripts subscribe via window.__cxAddr.subscribe(cb) instead of each one monkey-patching XMLHttpRequest.prototype (which used to stack three patches deep â fragile, and the response got JSON.parsed 2-3x). Callback: cb(xhr, parsedJsonOrNull); xhr also carries the captured _cxMethod/ _cxUrl/ _cxBody.============================================================ -->
1<script> 2(function () { 3 if (window.__cxAddr) return; 4 var subscribers = []; 5 window.__cxAddr = { 6 subscribe: function (cb) { if (typeof cb === 'function') subscribers.push(cb); } 7 }; 8 var _open = XMLHttpRequest.prototype.open; 9 var _send = XMLHttpRequest.prototype.send; 10 XMLHttpRequest.prototype.open = function (method, url) { 11 this._cxMethod = (method || '').toUpperCase(); 12 this._cxUrl = url || ''; 13 return _open.apply(this, arguments); 14 }; 15 XMLHttpRequest.prototype.send = function (body) { 16 this._cxBody = body; 17 if (this._cxUrl && this._cxUrl.indexOf('checkout/address') !== -1) { 18 var xhr = this; 19 this.addEventListener('load', function () { 20 var parsed = null; 21 try { parsed = JSON.parse(xhr.responseText); } catch (e) {} 22 for (var i = 0; i < subscribers.length; i++) { 23 try { subscribers[i](xhr, parsed); } catch (e) { /* one bad subscriber must not break the others */ } 24 } 25 }); 26 } 27 return _send.apply(this, arguments); 28 }; 29})(); 30</script>
30<!-- ============================================================ Devin Hales | Added ~March 31, 2026 3D Product Viewer: Loads Google's model-viewer web component (v3.5.0) for rendering .glb 3D models on product pages. Triggered per-product via the cf_d_images custom field (customfield_id: 6026428000041265039). GLB files hosted at tyconsystems.com/3dimages/.============================================================ -->
30<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js"></script>
30<!-- ============================================================ Devin Hales | Added ~March 24, 2026 Checkout Shipping Notification: Detects "No shipping methods available" during checkout (guest or logged-in) by intercepting XHR responses to checkout/address. Replaces Zoho's cryptic error with a user-friendly message and contact info ([email protected]/ 801-432-0003). Originally developed as part of the Easyship/Zoho guest checkout shipping failure investigation; stripped down from v4 failover script to notification-only after Zoho resolved the backend issue.============================================================ -->
30<script> 31(function () { 32 'use strict'; 33 34 if (window.__shipNotify) return; 35 window.__shipNotify = true; 36 37 if (window.location.pathname.indexOf('checkout') === -1) return; 38 39 function hasNoShipping(data) { 40 try { 41 var html = data.payload.checkout_shipping_methods || ''; 42 return html.indexOf('No shipping methods are available') !== -1; 43 } catch (e) { return false; } 44 } 45 46 function isAddressRequest(body) { 47 try { 48 var data = JSON.parse(body); 49 return !!(data.shipping_address || data.address_id); 50 } catch (e) { return false; } 51 } 52 53 function showNotice() { 54 if (document.getElementById('sf-notice')) return; 55 56 var target = document.querySelector('.shipping-methods') || 57 document.getElementById('zs-checkout-shipping-method'); 58 if (!target) return; 59 60 var div = document.createElement('div'); 61 div.id = 'sf-notice'; 62 div.style.cssText = 63 'background:#fff8e1;border:1px solid #f9a825;border-radius:6px;' + 64 'padding:16px 20px;margin:12px 0;line-height:1.5;'; 65 66 div.innerHTML = 67 '<p style="margin:0 0 8px;color:#e65100;font-weight:600;">' + 68 'Shipping rates are temporarily unavailable.</p>' + 69 '<p style="margin:0 0 12px;color:#333;font-size:14px;">' + 70 'Please try again. If the problem continues, we\'re happy to help ' + 71 'you complete your order.</p>' + 72 '<p style="margin:0;color:#333;font-size:14px;">' + 73 '<a href="mailto:[email protected]" style="color:#004aad;">' + 74 '[email protected]</a> | (801) 432-0003</p>'; 75 76 target.appendChild(div); 77 } 78 79 function clearNotice() { 80 var el = document.getElementById('sf-notice'); 81 if (el) el.remove(); 82 } 83 84 // Shared XHR interceptor (see top of header code): on a POST .../checkout/address 85 // whose body is an address request, show/clear the "no shipping" notice. 86 if (window.__cxAddr) { 87 window.__cxAddr.subscribe(function (xhr, data) { 88 if (xhr._cxMethod !== 'POST' || !isAddressRequest(xhr._cxBody)) return; 89 if (!data) return; 90 if (data.status_code === '0' && hasNoShipping(data)) showNotice(); 91 else clearNotice(); 92 }); 93 } 94})(); 95</script>
95<!-- ============================================================ Tyler Greene 1) Coupon Error Login Link: Listens for coupon submission failure and converts the plain "log in" text in the error message into a clickable link to /signin. 2) Phone Code Default: Sets the shipping phone country code dropdown to "+1" (US) on page load.============================================================ -->
95<script> 96document.addEventListener("zs-event-coupon-submit-failure", () => { 97 setTimeout(() => { 98 let couponError = document.querySelector("#zs-coupon-form .error_msg"); 99 if (couponError) { 100 let loginURL = "/signin"; 101 let updatedHtml = couponError.innerHTML.replace(/log in/i, `<a href="${loginURL}" style="color:#004aad;text-decoration:underline;">log in</a>`); 102 couponError.innerHTML = updatedHtml; 103 } 104 }, 500); 105}); 106 107document.addEventListener("DOMContentLoaded", function () { 108 var phoneSelector = document.getElementById('zs-shipping-mobile_code'); 109 if (phoneSelector) { 110 phoneSelector.value = "+1"; 111 } 112}); 113</script>
113<!-- ============================================================ Devin Hales | Major rewrite/expansion ~Feb 2026, ongoing Stock Availability & Add-to-Cart Restriction System (show_stock_available) Fetches real-time inventory data from the PHP sync system at htm.tyconsystems.com/stockdata/ and displays "X In Stock", dock date ETAs, or "Contact Us" on product and listing pages. Key capabilities added by Devin: - Cart-aware stock enforcement: checks cart API to subtract items already in cart from available qty, preventing customers from exceeding physical inventory - Quantity input max limits with stepper button interception - Cart page quantity restrictions with polling enforcement - Dock date display (expected qty + date from get_dock_dates.php) - Kit item detection (item_type=== 'kit') - "(X in cart)" display alongside stock counts - Listing page add-to-cart button disabling for out-of-stock - 30-second cart refresh interval with event listeners for cart update events - Variant change listener for multi-variant products Data sources: get_all_items.php â store_qty per variant get_dock_dates.php â expected dock dates and quantities /storefront/api/v1/cart â current cart contents Modified by Devin Hales (May 2026): - loadItemsData(): the get_all_items.php/ get_dock_dates.php fetches now go through fetchWithTimeout() â a 10s AbortController timeout and an HTTP status check. A slow htm.tyconsystems.com no longer stalls stock display, and a 5xx surfaces in the console with its status. (issue #14) Originally used cache:'no-store' to force a fresh fetch every nav, but the server now emits ETag/Last-Modified and the browser default cache strategy gives us free cache hits within max-age=300 plus 304 Not Modified after that â saving ~11 KB gzip per nav at the cost of up to 5 min staleness during navigation. (issue #20) - loadItemsData(): also validates the API response shape â requires data.success + a real items/ dock_dates object before using it, and console.warns (with data.error) otherwise instead of failing silently or throwing on Object.keys(undefined). (issue #16) - The verbose [Stock]/[Cart] console.log chatter is now gated behind STOCK_DEBUG (off in prod; enable with ?stockDebug=1 or localStorage.stockDebug='1' ); console.warn/console.error stay on. (issue #17) - The stockdata host is hoisted into STOCKDATA_BASE (one place to change; also window.STOCKDATA_BASE, used by the freight script). The full-catalog get_all_items.php fetch is reached only from the product/ cart/ listing code paths, so it isn't fired on home/checkout/etc. (issue #20) - loadItemsData(): removed the indexOf('{')/ substring trim-prefix hack that masked PHP warnings leaking into the response body. The PHP scripts now suppress display_errors and set Content-Type JSON, so the body is always clean; the parse path is now plain JSON.parse() wrapped in try/catch with a 200-char preview on failure, so a future server regression surfaces in the console instead of being silently trimmed to the wrong byte. (issue #15)============================================================ -->
113<script type="application/javascript"> 114var StockMessage = "In Stock"; 115// Single source of truth for the stockdata host â change it here (and in 116// footer.txt). Also exposed as window.STOCKDATA_BASE for the freight script. 117var STOCKDATA_BASE = "https://htm.tyconsystems.com/stockdata/"; 118var ItemsAPI = STOCKDATA_BASE + "get_all_items.php"; 119var DockDatesAPI = STOCKDATA_BASE + "get_dock_dates.php"; 120var itemsData = null; 121var itemsLoaded = false; 122var dockDatesData = null; 123var dockDatesLoaded = false; 124 125// Verbose [Stock]/[Cart] logging is off in production. Turn it on with 126// ?stockDebug=1 in the URL or localStorage.setItem('stockDebug','1'). 127// (console.warn / console.error stay on regardless.) 128var STOCK_DEBUG = false; 129try { STOCK_DEBUG = STOCK_DEBUG || /[?&]stockDebug=1\b/.test(location.search) || localStorage.getItem('stockDebug') === '1'; } catch (e) {} 130function slog() { if (STOCK_DEBUG) { try { console.log.apply(console, arguments); } catch (e) {} } } 131 132// Fetch with a hard timeout and HTTP status check. The cache strategy is 133// the browser default ('default'), which honors the server's 134// Cache-Control: public, max-age=300 and ETag / Last-Modified headers. 135// Within the 5-minute window the body comes from memory cache (zero 136// bytes on the wire); after the window the browser sends If-None-Match 137// and the server replies 304 Not Modified when the inventory CSV is 138// unchanged. Stock counts can be up to 5 minutes stale during 139// navigation; this is acceptable because: 140// - the inventory sync writes the CSV every few minutes, and the 141// ETag invalidates the cache the moment it does 142// - the cart-event handler doesn't reset itemsData mid-session today 143// either, so the existing UX is already "stock fetched once per 144// page load" â this just makes that fetch free on most navs 145// (issue #20) 146function fetchWithTimeout(url, ms) { 147 var ctrl = new AbortController(); 148 var timer = setTimeout(function() { ctrl.abort(); }, ms); 149 return fetch(url, { cache: 'default', signal: ctrl.signal }) 150 .then(function(response) { 151 if (!response.ok) throw new Error('HTTP ' + response.status); 152 return response; 153 }) 154 .finally(function() { clearTimeout(timer); }); 155} 156 157function show_stock_available(){ 158 var sort_column = { 159 "relevance": "last_modified_time", 160 "popularity": "best_selling", 161 "new": "created_time", 162 "HtoL": "min_rate", 163 "LtoH": "max_rate", 164 "featured": "", 165 "AtoZ": "name", 166 "ZtoA": "name" 167 }; 168 var sort_order = { 169 "relevance": "A", 170 "popularity": "A", 171 "new": "D", 172 "HtoL": "D", 173 "LtoH": "A", 174 "featured":"", 175 "AtoZ": "A", 176 "ZtoA": "D" 177 }; 178 var per_page = 0, current_page_number = 0, flag_sorting = false, default_sortby = "", isNormalPage = false; 179 var include_product_details = true; 180 181 function loadItemsData(callback) { 182 if (itemsLoaded && dockDatesLoaded) { 183 callback(); 184 return; 185 } 186 187 var pending = 0; 188 189 if (!itemsLoaded) { 190 pending++; 191 fetchWithTimeout(ItemsAPI, 10000) 192 .then(function(response) { 193 return response.text(); 194 }) 195 .then(function(text) { 196 // The PHP side (error_reporting(0) + Content-Type) now 197 // guarantees clean JSON; if a future regression slips a 198 // warning into the body, the parse throws, we log a 200- 199 // char preview for diagnosis, and the outer .catch falls 200 // back gracefully instead of masking it silently. (issue #15) 201 try { 202 return JSON.parse(text); 203 } catch (e) { 204 console.warn('[Stock] get_all_items.php returned non-JSON (first 200 chars):', String(text).slice(0, 200)); 205 throw e; 206 } 207 }) 208 .then(function(data) { 209 if (data && data.success && data.items && typeof data.items === 'object') { 210 itemsData = data.items; 211 slog('Items data loaded successfully. Total items:', Object.keys(itemsData).length); 212 } else { 213 console.warn('Items API returned no usable data:', (data && data.error) || data); 214 } 215 itemsLoaded = true; 216 pending--; 217 if (pending === 0) callback(); 218 }) 219 .catch(function(error) { 220 console.error('Error loading items data:', error); 221 itemsLoaded = true; 222 pending--; 223 if (pending === 0) callback(); 224 }); 225 } 226 227 if (!dockDatesLoaded) { 228 pending++; 229 fetchWithTimeout(DockDatesAPI, 10000) 230 .then(function(response) { 231 return response.text(); 232 }) 233 .then(function(text) { 234 // Same guard as get_all_items.php â see comment above. (issue #15) 235 try { 236 return JSON.parse(text); 237 } catch (e) { 238 console.warn('[Stock] get_dock_dates.php returned non-JSON (first 200 chars):', String(text).slice(0, 200)); 239 throw e; 240 } 241 }) 242 .then(function(data) { 243 if (data && data.success && data.dock_dates && typeof data.dock_dates === 'object') { 244 dockDatesData = data.dock_dates; 245 } else { 246 console.warn('Dock dates API returned no usable data:', (data && data.error) || data); 247 } 248 dockDatesLoaded = true; 249 pending--; 250 if (pending === 0) callback(); 251 }) 252 .catch(function(error) { 253 console.error('Error loading dock dates:', error); 254 dockDatesLoaded = true; 255 pending--; 256 if (pending === 0) callback(); 257 }); 258 } 259 260 if (pending === 0) callback(); 261 } 262 263 function getItemQty(variantId) { 264 if (itemsData && itemsData[variantId]) { 265 var qty = itemsData[variantId].store_qty; 266 if (typeof qty === 'number' && qty >= 0 && qty < 1000000) { 267 return qty; 268 } else { 269 console.warn('Invalid store_qty for variant', variantId, ':', qty); 270 return null; 271 } 272 } 273 return null; 274 } 275 276 function getDockDateInfo(variantId) { 277 if (dockDatesData && dockDatesData[variantId]) { 278 return { 279 dock_date: dockDatesData[variantId].dock_date, 280 expected_qty: dockDatesData[variantId].expected_qty 281 }; 282 } 283 return null; 284 } 285 286 /* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 287 * Devin Hales | Added ~Feb 2026 288 * Cart Data Management: Fetches and caches the customer's 289 * current cart via the storefront API. Used to calculate 290 * remaining available quantity (store_qty - cart_qty) for 291 * real-time add-to-cart enforcement. 292 * ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 293 var cartData = null; 294 var cartLoaded = false;
295 var cartLoadPromise = null; 296 297 function loadCartData(callback) { 298 var safeCallback = function(data) { 299 try { 300 if (typeof callback === 'function') { 301 callback(data); 302 } 303 } catch (callbackError) { 304 console.warn('[Cart] Callback error (non-critical):', callbackError); 305 } 306 }; 307 308 try { 309 if (cartLoaded && cartData !== null) { 310 safeCallback(cartData); 311 return; 312 } 313 314 if (cartLoadPromise) { 315 cartLoadPromise 316 .then(function() { 317 safeCallback(cartData); 318 }) 319 .catch(function() { 320 safeCallback({ line_items: [] }); 321 }); 322 return; 323 } 324 325 cartLoadPromise = fetch(window.origin + '/storefront/api/v1/cart') 326 .then(function(response) { 327 if (!response.ok) { 328 console.warn('[Cart] API returned status:', response.status); 329 return { code: -1 }; 330 } 331 return response.json(); 332 }) 333 .then(function(data) { 334 try { 335 if (data && data.status_code === "0" && data.payload) { 336 cartData = data.payload; 337 var itemCount = 0; 338 if (cartData.items && cartData.items.length) { 339 itemCount = cartData.items.length; 340 } else if (cartData.line_items && cartData.line_items.length) { 341 itemCount = cartData.line_items.length; 342 } 343 slog('[Cart] Data loaded successfully. Items in cart:', itemCount); 344 } else if (data && data.code === 0 && data.cart) { 345 cartData = data.cart; 346 } else { 347 cartData = { items: [], line_items: [] }; 348 } 349 } catch (parseError) { 350 console.warn('[Cart] Error parsing response (non-critical):', parseError); 351 cartData = { items: [], line_items: [] }; 352 } 353 cartLoaded = true; 354 cartLoadPromise = null; 355 safeCallback(cartData); 356 }) 357 .catch(function(error) { 358 console.warn('[Cart] Fetch failed (non-critical), assuming empty cart:', error.message || error); 359 cartData = { line_items: [] }; 360 cartLoaded = true; 361 cartLoadPromise = null; 362 safeCallback(cartData); 363 }); 364 } catch (outerError) { 365 console.warn('[Cart] Unexpected error (non-critical):', outerError); 366 cartData = { line_items: [] }; 367 cartLoaded = true; 368 cartLoadPromise = null; 369 safeCallback({ line_items: [] }); 370 } 371 } 372 373 function getCartQtyForVariant(variantId) { 374 try { 375 if (!cartData) return 0; 376 377 var variantIdStr = String(variantId); 378 379 if (cartData.items && Array.isArray(cartData.items)) { 380 for (var i = 0; i < cartData.items.length; i++) { 381 var item = cartData.items[i]; 382 if (item && (String(item.variant_id) === variantIdStr || 383 String(item.item_id) === variantIdStr || 384 String(item.product_id) === variantIdStr)) { 385 return parseInt(item.quantity) || 0; 386 } 387 } 388 } 389 390 if (cartData.line_items && Array.isArray(cartData.line_items)) { 391 for (var j = 0; j < cartData.line_items.length; j++) { 392 var lineItem = cartData.line_items[j]; 393 if (lineItem && (String(lineItem.item_id) === variantIdStr || 394 String(lineItem.variant_id) === variantIdStr || 395 String(lineItem.product_id) === variantIdStr)) { 396 return parseInt(lineItem.quantity) || 0; 397 } 398 } 399 } 400 401 return 0; 402 } catch (error) { 403 console.warn('[Cart] Error checking cart quantity (non-critical):', error); 404 return 0; 405 } 406 } 407 408 function refreshCartData(callback) { 409 try { 410 cartLoaded = false;
411 cartData = null; 412 cartLoadPromise = null; 413 loadCartData(callback || function() {}); 414 } catch (error) { 415 console.warn('[Cart] Error refreshing cart (non-critical):', error); 416 if (typeof callback === 'function') { 417 try { callback({ line_items: [] }); } catch(e) {} 418 } 419 } 420 } 421 422 /* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 423 * Devin Hales | Added ~Feb 2026 424 * Cart Event Listeners: Listens for Zoho Commerce cart 425 * events (add-to-cart, cart-updated, etc.) and click events 426 * on add-to-cart buttons to refresh cart data and re-apply 427 * stock restrictions in real time. Includes a 30-second 428 * polling interval as a safety net. 429 * ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 430 function setupCartRefreshListener() { 431 try { 432 var cartEvents = [ 433 'zs-event-add-to-cart-success', 434 'zs-event-cart-updated', 435 'zs-event-cart-update-success', 436 'zs-event-update-cart-success', 437 'zs-cart-updated', 438 'cart:updated', 439 'cart:added' 440 ]; 441 442 cartEvents.forEach(function(eventName) { 443 document.addEventListener(eventName, function() { 444 try { 445 slog('[Cart] Cart event fired:', eventName, '- refreshing cart data'); 446 refreshCartData(function() { 447 reapplyCurrentRestrictions(); 448 refreshAllStockDisplays(); 449 }); 450 } catch (e) { 451 console.warn('[Cart] Error in cart event handler (non-critical):', e); 452 } 453 }); 454 }); 455 456 document.addEventListener('click', function(e) { 457 try { 458 var target = e.target; 459 var addToCartBtn = target.closest('[data-zs-add-to-cart]') || 460 target.closest('[data-zs-action="addtocart"]'); 461 462 if (addToCartBtn) { 463 var variantId = addToCartBtn.getAttribute('data-zs-product-variant-id') || 464 addToCartBtn.getAttribute('data-zs-variant-id'); 465 466 if (!variantId) { 467 var productContainer = addToCartBtn.closest('[data-zs-product-id]'); 468 if (productContainer) { 469 variantId = productContainer.getAttribute('data-zs-product-id'); 470 var variantEl = productContainer.querySelector('[data-zs-variant-id]'); 471 if (variantEl) { 472 variantId = variantEl.getAttribute('data-zs-variant-id'); 473 } 474 } 475 } 476 477 if (variantId) { 478 var storeQty = getItemQty(variantId); 479 var cartQty = getCartQtyForVariant(variantId); 480 var remainingAvailable = (storeQty !== null) ? storeQty - cartQty : null; 481 482 if (remainingAvailable !== null && remainingAvailable <= 0) { 483 e.preventDefault(); 484 e.stopPropagation(); 485 e.stopImmediatePropagation(); 486 showListingPageWarning(addToCartBtn, storeQty); 487 return false; 488 } 489 } 490 491 setTimeout(function() { 492 refreshCartData(function() { 493 reapplyCurrentRestrictions(); 494 refreshAllStockDisplays(); 495 }); 496 }, 1500); 497 498 setTimeout(function() { 499 refreshCartData(function() { 500 reapplyCurrentRestrictions(); 501 refreshAllStockDisplays(); 502 }); 503 }, 3000); 504 } 505 } catch (e) { 506 console.warn('[Cart] Error in click handler (non-critical):', e); 507 } 508 }, true); 509 510 var lastCartCheck = Date.now(); 511 setInterval(function() { 512 try { 513 if (Date.now() - lastCartCheck > 30000) { 514 lastCartCheck = Date.now(); 515 if (document.querySelector('[data-zs-product-id]')) { 516 refreshCartData(function() { 517 reapplyCurrentRestrictions(); 518 refreshAllStockDisplays(); 519 }); 520 } 521 } 522 } catch (e) {} 523 }, 30000); 524 } catch (error) { 525 console.warn('[Cart] Error setting up cart listeners (non-critical):', error); 526 } 527 } 528 529 function showListingPageWarning(button, maxStock) { 530 try { 531 var existingWarnings = document.querySelectorAll('.listing-stock-warning');
532 existingWarnings.forEach(function(w) { w.remove(); }); 533 534 var warning = document.createElement('div'); 535 warning.className = 'listing-stock-warning'; 536 warning.style.cssText = 'color: #ff9a33; font-size: 12px; text-align: center; margin-top: 5px; padding: 5px; background: #fff3e0; border-radius: 4px;'; 537 warning.textContent = 'Max ' + maxStock + ' already in cart'; 538 539 if (button.parentNode) { 540 button.parentNode.insertBefore(warning, button.nextSibling); 541 } 542 543 setTimeout(function() { 544 if (warning.parentNode) warning.remove(); 545 }, 4000); 546 } catch (e) { 547 console.warn('[Cart] Error showing listing warning (non-critical):', e); 548 } 549 } 550 551 function refreshAllStockDisplays() { 552 try { 553 var stockElements = document.querySelectorAll('p[data-variant-id][stock_available]'); 554 555 stockElements.forEach(function(stockElement) { 556 var variantId = stockElement.getAttribute('data-variant-id'); 557 var storeQty = parseInt(stockElement.getAttribute('stock_available')) || 0; 558 559 if (variantId && storeQty > 0) { 560 var cartQty = getCartQtyForVariant(variantId); 561 var text = '<span style="white-space: nowrap;">' + storeQty + " " + StockMessage + '</span>'; 562 563 if (cartQty > 0) { 564 var cartText = getCartQtyHtml(cartQty); 565 stockElement.innerHTML = " " + text + cartText; 566 } else { 567 stockElement.innerHTML = " " + text; 568 } 569 } 570 }); 571 } catch (e) { 572 console.warn('[Cart] Error refreshing stock displays (non-critical):', e); 573 } 574 } 575 576 var currentVariantId = null; 577 var currentStoreQty = null; 578 579 function reapplyCurrentRestrictions() { 580 try { 581 if (currentVariantId && currentStoreQty !== null) { 582 applyAddToCartRestrictions(currentVariantId, currentStoreQty); 583 updateStockDisplayAfterCartChange(currentVariantId, currentStoreQty); 584 } 585 } catch (e) { 586 console.warn('[Cart] Error re-applying restrictions (non-critical):', e); 587 } 588 } 589 590 function updateStockDisplayAfterCartChange(variantId, storeQty) { 591 try { 592 var stockElement = document.querySelector('p[data-variant-id="' + variantId + '"][stock_available]'); 593 594 if (stockElement) { 595 var cartQty = getCartQtyForVariant(variantId); 596 var text = '<span style="white-space: nowrap;">' + storeQty + " " + StockMessage + '</span>'; 597 598 if (cartQty > 0) { 599 var cartText = getCartQtyHtml(cartQty); 600 stockElement.innerHTML = " " + text + cartText; 601 } else { 602 stockElement.innerHTML = " " + text; 603 } 604 } 605 } catch (e) { 606 console.warn('[Cart] Error updating stock display (non-critical):', e); 607 } 608 } 609 610 setupCartRefreshListener(); 611 612 function isKitItem(variantId) { 613 if (itemsData && itemsData[variantId]) { 614 return itemsData[variantId].item_type === 'kit'; 615 } 616 return false; 617 } 618 619 function formatDate(dateStr) { 620 if (!dateStr) return dateStr; 621 var parts = dateStr.split('-'); 622 if (parts.length === 3) { 623 var year = parts[0]; 624 var month = parseInt(parts[1], 10); 625 var day = parseInt(parts[2], 10); 626 return month + '/' + day + '/' + year; 627 } 628 return dateStr; 629 } 630 631 /* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 632 * Devin Hales | Added ~Feb 2026 633 * Add-to-Cart Restrictions: Enforces quantity limits on 634 * product detail pages and cart page based on real inventory 635 * minus items already in cart. Hides the Add to Cart button 636 * when stock is exhausted, caps the quantity input, and 637 * intercepts stepper button clicks and direct add-to-cart 638 * actions. Shows contextual warnings like "Maximum quantity 639 * (X) already in cart" and "You have X in your cart. Y more 640 * available." 641 * ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 642 var variantStockLimits = {}; 643 644 function applyAddToCartRestrictions(variantId, stockAvailable) { 645 try { 646 currentVariantId = variantId; 647 currentStoreQty = stockAvailable; 648 variantStockLimits[variantId] = stockAvailable; 649 650 loadCartData(function(cart) { 651 applyRestrictionsWithCartData(variantId, stockAvailable); 652 }); 653 } catch (error) { 654 console.warn('[Stock] Error in applyAddToCartRestrictions (non-critical):', error); 655 } 656 } 657 658 function applyRestrictionsWithCartData(variantId, stockAvailable) { 659 try { 660 var cartQty = getCartQtyForVariant(variantId); 661 var remainingAvailable = stockAvailable - cartQty; 662 663 slog('[Stock] Variant', variantId, '- Store qty:', stockAvailable, 664 'In cart:', cartQty, 'Remaining:', remainingAvailable); 665
666 var productContainer = document.querySelector("[data-zs-product-id]"); 667 if (!productContainer) return; 668 669 var qtyInput = productContainer.querySelector("input[data-zs-quantity]"); 670 var addToCartBtn = productContainer.querySelector("button[data-zs-add-to-cart]"); 671 672 var existingMsg = productContainer.querySelector('.out-of-stock-msg'); 673 if (existingMsg) existingMsg.remove(); 674 675 var existingCartInfo = productContainer.querySelector('.cart-qty-info'); 676 if (existingCartInfo) existingCartInfo.remove(); 677 678 if (remainingAvailable <= 0) { 679 if (addToCartBtn) { 680 addToCartBtn.style.display = 'none'; 681 682 var msg = document.createElement('span'); 683 msg.className = 'out-of-stock-msg'; 684 msg.style.cssText = 'color: #ff9a33; font-weight: bold; padding: 10px 20px; display: inline-block;'; 685 686 if (stockAvailable <= 0) { 687 msg.textContent = 'Out of Stock - Request Quote for Availability'; 688 } else if (cartQty >= stockAvailable) { 689 msg.textContent = 'Maximum quantity (' + stockAvailable + ') already in cart'; 690 } else { 691 msg.textContent = 'No more available to add'; 692 } 693 694 addToCartBtn.parentNode.insertBefore(msg, addToCartBtn); 695 } 696 697 if (qtyInput) { 698 qtyInput.disabled = true; 699 qtyInput.value = 0; 700 } 701 } else { 702 if (addToCartBtn) { 703 addToCartBtn.style.display = ''; 704 } 705 706 if (qtyInput) { 707 qtyInput.disabled = false; 708 qtyInput.setAttribute('max', remainingAvailable); 709 qtyInput.setAttribute('data-max-stock', remainingAvailable); 710 qtyInput.setAttribute('data-store-qty', stockAvailable); 711 qtyInput.setAttribute('data-cart-qty', cartQty); 712 713 if (parseInt(qtyInput.value) > remainingAvailable) { 714 qtyInput.value = remainingAvailable; 715 } 716 717 if (parseInt(qtyInput.value) < 1) { 718 qtyInput.value = 1; 719 } 720 721 qtyInput.removeEventListener('change', enforceMaxQty); 722 qtyInput.removeEventListener('input', enforceMaxQty); 723 qtyInput.addEventListener('change', enforceMaxQty); 724 qtyInput.addEventListener('input', enforceMaxQty); 725 726 setupStepperButtonRestrictions(qtyInput, remainingAvailable); 727 } 728 729 if (addToCartBtn) { 730 addToCartBtn.removeEventListener('click', interceptAddToCart, true); 731 addToCartBtn.addEventListener('click', interceptAddToCart, true); 732 } 733 734 if (cartQty > 0 && addToCartBtn && addToCartBtn.parentNode) { 735 var cartInfo = document.createElement('div'); 736 cartInfo.className = 'cart-qty-info'; 737 cartInfo.style.cssText = 'color: #004aad; font-size: 13px; margin-top: 10px; padding: 8px 12px; background: #e8f4fd; border-radius: 4px; text-align: center;'; 738 cartInfo.textContent = 'You have ' + cartQty + ' in your cart. ' + remainingAvailable + ' more available.'; 739 740 var buttonParent = addToCartBtn.parentNode; 741 if (buttonParent.parentNode) { 742 buttonParent.parentNode.insertBefore(cartInfo, buttonParent.nextSibling); 743 } 744 } 745 } 746 } catch (error) { 747 console.warn('[Stock] Error applying restrictions (non-critical):', error); 748 } 749 } 750 751 function setupStepperButtonRestrictions(qtyInput, maxStock) { 752 try { 753 if (!qtyInput || !maxStock) return; 754 755 if (qtyInput._stockCheckInterval) { 756 clearInterval(qtyInput._stockCheckInterval); 757 } 758 759 var lastValue = parseInt(qtyInput.value) || 1; 760 761 var checkInterval = setInterval(function() { 762 try { 763 if (!document.contains(qtyInput)) { 764 clearInterval(checkInterval); 765 return; 766 } 767 768 var currentVal = parseInt(qtyInput.value) || 0; 769 770 if (currentVal !== lastValue && currentVal > maxStock) { 771 qtyInput.value = maxStock; 772 showStockWarning(qtyInput, maxStock);
773 lastValue = maxStock; 774 } else { 775 lastValue = currentVal; 776 } 777 } catch (e) {} 778 }, 100); 779 780 qtyInput._stockCheckInterval = checkInterval; 781 } catch (error) { 782 console.warn('[Stock] Error setting up stepper restrictions (non-critical):', error); 783 } 784 } 785 786 function enforceMaxQty(e) { 787 try { 788 var input = e.target; 789 var maxStock = parseInt(input.getAttribute('data-max-stock')) || parseInt(input.getAttribute('max')); 790 var currentVal = parseInt(input.value) || 0; 791 792 if (maxStock && currentVal > maxStock) { 793 input.value = maxStock; 794 showStockWarning(input, maxStock); 795 } 796 } catch (error) { 797 console.warn('[Stock] Error in enforceMaxQty (non-critical):', error); 798 } 799 } 800 801 function interceptAddToCart(e) { 802 try { 803 var productContainer = document.querySelector("[data-zs-product-id]"); 804 if (!productContainer) return; 805 806 var qtyInput = productContainer.querySelector("input[data-zs-quantity]"); 807 if (!qtyInput) return; 808 809 var maxStock = parseInt(qtyInput.getAttribute('data-max-stock')) || parseInt(qtyInput.getAttribute('max')); 810 var requestedQty = parseInt(qtyInput.value) || 1; 811 812 if (maxStock && requestedQty > maxStock) { 813 e.preventDefault(); 814 e.stopPropagation(); 815 qtyInput.value = maxStock; 816 showStockWarning(qtyInput, maxStock); 817 return false; 818 } 819 } catch (error) { 820 console.warn('[Stock] Error in interceptAddToCart (non-critical):', error); 821 } 822 } 823 824 function showStockWarning(input, maxStock) { 825 try { 826 var existingWarning = document.querySelector('.stock-limit-warning'); 827 if (existingWarning) existingWarning.remove(); 828 829 var warning = document.createElement('div'); 830 warning.className = 'stock-limit-warning'; 831 warning.style.cssText = 'color: #ff9a33; font-size: 14px; margin: 15px 0; padding: 12px 15px; background: #fff3e0; border-radius: 4px; border: 1px solid #ff9a33; width: 100%; box-sizing: border-box; clear: both; display: block; text-align: center;'; 832 warning.textContent = 'Only ' + maxStock + ' available. Quantity has been adjusted.'; 833 834 var productContainer = document.querySelector("[data-zs-product-id]"); 835 var addToCartBtn = productContainer ? productContainer.querySelector("button[data-zs-add-to-cart]") : null; 836 var requestQuoteBtn = productContainer ? productContainer.querySelector("button[data-zs-action='requestquote'], [data-zs-request-quote]") : null; 837 838 if (requestQuoteBtn && requestQuoteBtn.parentNode) { 839 requestQuoteBtn.parentNode.parentNode.insertBefore(warning, requestQuoteBtn.parentNode.nextSibling); 840 } else if (addToCartBtn && addToCartBtn.parentNode && addToCartBtn.parentNode.parentNode) { 841 var buttonRow = addToCartBtn.parentNode; 842 buttonRow.parentNode.insertBefore(warning, buttonRow.nextSibling); 843 } else if (input && input.parentNode && input.parentNode.parentNode) { 844 var inputRow = input.parentNode.parentNode; 845 inputRow.parentNode.insertBefore(warning, inputRow.nextSibling); 846 } else if (productContainer) { 847 productContainer.appendChild(warning); 848 } 849 850 setTimeout(function() { 851 try { if (warning.parentNode) warning.remove(); } catch (e) {} 852 }, 5000); 853 } catch (error) { 854 console.warn('[Stock] Error showing warning (non-critical):', error); 855 } 856 } 857 858 function applyListingPageRestrictions(variantId, stockAvailable, productContainer) { 859 try { 860 if (!productContainer) return;
861 var addToCartBtn = productContainer.querySelector("[data-zs-add-to-cart]"); 862 if (stockAvailable <= 0 && addToCartBtn) { 863 addToCartBtn.style.opacity = '0.5'; 864 addToCartBtn.style.pointerEvents = 'none'; 865 addToCartBtn.setAttribute('title', 'Out of Stock'); 866 } 867 } catch (error) { 868 console.warn('[Stock] Error applying listing page restrictions (non-critical):', error); 869 } 870 } 871 872 function showAvailableStockInProductPage() { 873 if(include_product_details){ 874 loadItemsData(function() { 875 var productContainer = document.querySelectorAll("[data-zs-product-id]"); 876 if (!productContainer || productContainer.length === 0) return; 877 878 if (!window.zs_product || !window.zs_product.variants) return; 879 880 var variants = window.zs_product.variants; 881 882 for (var i = 0; i < variants.length; i++) { 883 var variant = variants[i]; 884 var price_container = productContainer[0].querySelector("[data-zs-pricings][data-zs-variant-id='" + variant.variant_id + "']"); 885 var itemQty = getItemQty(variant.variant_id); 886 var dockDateInfo = getDockDateInfo(variant.variant_id); 887 888 var finalQty; 889 if (itemQty !== null) { 890 finalQty = itemQty; 891 addStockAvailableElement(price_container, itemQty, dockDateInfo, variant.variant_id); 892 } else { 893 finalQty = variant.stock_available; 894 addStockAvailableElement(price_container, variant.stock_available, dockDateInfo, variant.variant_id); 895 } 896 897 if (variants.length === 1 || i === 0) { 898 applyAddToCartRestrictions(variant.variant_id, finalQty); 899 } 900 } 901 902 if (variants.length > 1) { 903 setupVariantChangeListener(); 904 } 905 }); 906 } 907 setTimeout(function(){ 908 var product_ids = []; 909 var productContainer = document.querySelectorAll("[data-zs-product-id]"); 910 for(var i=1; i < productContainer.length; i++){ 911 var prd_id = productContainer[i].getAttribute("data-zs-product-id"); 912 if(!product_ids.includes(prd_id)){ 913 product_ids.push(prd_id); 914 } 915 } 916 if(product_ids.length > 0){ 917 getProductDetailsByIds(product_ids.toString(), false); 918 } 919 }, 200); 920 } 921 922 function setupVariantChangeListener() { 923 var variantSelectors = document.querySelectorAll("[data-zs-variant-selector], .zs-variant-select, select[name*='variant'], .variant-option"); 924 925 variantSelectors.forEach(function(selector) { 926 selector.addEventListener('change', function(e) { 927 var selectedVariantId = e.target.value || e.target.getAttribute('data-variant-id'); 928 if (selectedVariantId && variantStockLimits[selectedVariantId] !== undefined) { 929 applyAddToCartRestrictions(selectedVariantId, variantStockLimits[selectedVariantId]); 930 } 931 }); 932 }); 933 934 var observer = new MutationObserver(function(mutations) { 935 mutations.forEach(function(mutation) { 936 if (mutation.type === 'attributes' && mutation.attributeName === 'data-zs-variant-id') { 937 var variantId = mutation.target.getAttribute('data-zs-variant-id'); 938 if (variantId && variantStockLimits[variantId] !== undefined) { 939 applyAddToCartRestrictions(variantId, variantStockLimits[variantId]); 940 } 941 } 942 }); 943 }); 944 945 var productContainer = document.querySelector("[data-zs-product-id]"); 946 if (productContainer) { 947 observer.observe(productContainer, { attributes: true, subtree: true }); 948 } 949 } 950 951 function displayAvailableStock(products, args) { 952 loadItemsData(function() { 953 var target_container; 954 if (args.collection_id != null) { 955 target_container = document.querySelector("[data-zs-collection-id='" + args.collection_id + "']"); 956 } else if (args.category_id != null) { 957 target_container = document.querySelector("[data-zs-category-id='" + args.category_id + "']"); 958 } else if(args.is_quick_look_page){ 959 var quick_look_element = document.getElementById("product_quick_look"); 960 if(quick_look_element && quick_look_element.innerHTML != ""){ 961 target_container = quick_look_element; 962 } 963 } else { 964 target_container = "null"; 965 } 966 967 for (var i = 0; i < products.length; i++) { 968 var currentProduct = products[i]; 969 var product_containers = (target_container != "null") ? (target_container.querySelectorAll("[data-zs-product-id='" + currentProduct.product_id + "']")) : (document.querySelectorAll("[data-zs-product-id='" + currentProduct.product_id + "']")); 970
971 product_containers.forEach(function(product){ 972 if (product && product.querySelectorAll("[stock_available]").length == 0) { 973 var product_id = product.getAttribute("data-zs-product-id"); 974 if (product_id == currentProduct.product_id) { 975 var variants = currentProduct.variants; 976 var price_container_array = product.querySelectorAll("[data-zs-pricings][data-zs-variant-id]"); 977 if (price_container_array.length > 0) { 978 for (var j = 0; j < price_container_array.length; j++) { 979 var variant_id = price_container_array[j].getAttribute("data-zs-variant-id"); 980 for (var k = 0; k < variants.length; k++) { 981 if (variants[k].variant_id == variant_id) { 982 var variant = variants[k]; 983 var itemQty = getItemQty(variant.variant_id); 984 var dockDateInfo = getDockDateInfo(variant.variant_id); 985 986 var finalQty; 987 if (itemQty !== null) { 988 finalQty = itemQty; 989 addStockAvailableElement(price_container_array[j], itemQty, dockDateInfo, variant.variant_id); 990 } else { 991 finalQty = variant.available_sale_stock; 992 addStockAvailableElement(price_container_array[j], variant.available_sale_stock, dockDateInfo, variant.variant_id); 993 } 994 995 variantStockLimits[variant.variant_id] = finalQty; 996 applyListingPageRestrictions(variant.variant_id, finalQty, product); 997 } 998 } 999 } 1000 } else { 1001 price_container_array = product.querySelector("[data-zs-selling-price]"); 1002 if(!price_container_array){ 1003 price_container_array = product.querySelector("[data-zs-label-price]"); 1004 } 1005 if (price_container_array && variants.length == 1) { 1006 var variant = variants[0]; 1007 var itemQty = getItemQty(variant.variant_id); 1008 var dockDateInfo = getDockDateInfo(variant.variant_id); 1009 1010 var finalQty; 1011 var stock_available_element; 1012 if (itemQty !== null) { 1013 finalQty = itemQty; 1014 stock_available_element = addStockAvailableElement(price_container_array.parentElement, itemQty, dockDateInfo, variant.variant_id); 1015 } else { 1016 finalQty = variant.available_sale_stock; 1017 stock_available_element = addStockAvailableElement(price_container_array.parentElement, variant.available_sale_stock, dockDateInfo, variant.variant_id); 1018 } 1019 1020 variantStockLimits[variant.variant_id] = finalQty; 1021 applyListingPageRestrictions(variant.variant_id, finalQty, product); 1022 1023 if(stock_available_element){ 1024 if(product.className.includes("theme-product-list-style-07")){ 1025 stock_available_element.style = "font-size: 16px;margin:auto;text-align:right"; 1026 } 1027 price_container_array.parentElement.style.display = "block"; 1028 } 1029 } 1030 } 1031 } 1032 } 1033 }); 1034 } 1035 }); 1036 } 1037 1038 function addStockAvailableElement(elem, stock_available, dockDateInfo, variantId){ 1039 if (!elem) return null; 1040 var paragraph_elem = document.createElement("P"); 1041 paragraph_elem.style = "font-size:16px;"; 1042 paragraph_elem.style.fontWeight = "normal"; 1043 1044 var qty = Number(stock_available) || 0; 1045 var text; 1046 1047 if (qty > 0) { 1048 paragraph_elem.style.color = "#004aad"; 1049 text = '<span style="white-space: nowrap;">' + qty + " " + StockMessage + '</span>'; 1050 } else if (dockDateInfo && dockDateInfo.dock_date) { 1051 paragraph_elem.style.color = "#ff9a33"; 1052 text = dockDateInfo.expected_qty + " expected on " + formatDate(dockDateInfo.dock_date); 1053 } else { 1054 paragraph_elem.style.color = "#ff9a33"; 1055 text = "Contact Us"; 1056 } 1057 1058 paragraph_elem.setAttribute("stock_available", qty); 1059 1060 if (variantId) { 1061 paragraph_elem.setAttribute("data-variant-id", variantId); 1062 } 1063 1064 paragraph_elem.innerHTML = " " + text; 1065 elem.appendChild(paragraph_elem); 1066 1067 if (variantId) { 1068 updateStockDisplayWithCartQty(paragraph_elem, qty, variantId); 1069 } 1070 1071 return paragraph_elem; 1072 } 1073 1074 function getCartQtyHtml(cartQty) { 1075 if (window.zs_view === 'product') { 1076 return '<span style="color: #999; font-size: 13px; margin-left: 8px; white-space: nowrap;">
1076(' + cartQty + ' in cart)</span>'; 1077 } else { 1078 return '<br><span style="color: #999; font-size: 13px; white-space: nowrap;"> (' + cartQty + ' in cart)</span>'; 1079 } 1080 } 1081 1082 function updateStockDisplayWithCartQty(stockElement, storeQty, variantId) { 1083 loadCartData(function(cart) { 1084 try { 1085 var cartQty = getCartQtyForVariant(variantId); 1086 if (cartQty > 0 && storeQty > 0) { 1087 var text = '<span style="white-space: nowrap;">' + storeQty + " " + StockMessage + '</span>'; 1088 var cartText = getCartQtyHtml(cartQty); 1089 stockElement.innerHTML = " " + text + cartText; 1090 } 1091 } catch (e) { 1092 console.warn('[Stock] Error updating stock display with cart qty (non-critical):', e); 1093 } 1094 }); 1095 } 1096 1097 var allProducts = []; 1098 var current_page_number = 1; 1099 1100 function getProductDetails(url, params, str, id) { 1101 var productCount = document.querySelectorAll("[data-zs-product-id]").length; 1102 if (productCount == 0) return; 1103 current_page_number = 1; 1104 allProducts = []; 1105 fetchAllProducts(url, params, str, id); 1106 } 1107 1108 function fetchAllProducts(url, params, str, id) { 1109 var urlParams = new URLSearchParams(params); 1110 urlParams.set('page', current_page_number); 1111 urlParams.set('per_page', per_page); 1112 1113 var sortcolumn = sort_column[default_sortby]; 1114 var sortorder = sort_order[default_sortby]; 1115 1116 if (document.querySelector("[data-sort-by-products]")) { 1117 sortcolumn = sort_column[document.querySelector("[data-sort-by-products]").value]; 1118 sortorder = sort_order[document.querySelector("[data-sort-by-products]").value]; 1119 } 1120 1121 if (flag_sorting && !isNormalPage) { 1122 urlParams.set('sort_column', sortcolumn); 1123 urlParams.set('sort_order', sortorder); 1124 } 1125 1126 $X.get({ 1127 url: url + "?" + urlParams.toString(), 1128 args: { 1129 collection_id: (str == "collection") ? id : null, 1130 category_id: (str == "category") ? id : null, 1131 is_quick_look_page: false, 1132 str: str 1133 }, 1134 handler: function(args) { 1135 var response = JSON.parse(this.responseText); 1136 if (response.code == 0) { 1137 var products; 1138 var has_more_page; 1139 1140 if (args.str == "category") { 1141 products = response.category.products; 1142 has_more_page = response.category.page_context.has_more_page; 1143 } 1144 else if (args.str == "collection") { 1145 products = response.collection.products; 1146 has_more_page = response.collection.page_context.has_more_page; 1147 } 1148 else { 1149 products = response.products; 1150 has_more_page = response.page_context.has_more_page; 1151 } 1152 1153 allProducts = allProducts.concat(products); 1154 1155 if (has_more_page) { 1156 current_page_number++; 1157 fetchAllProducts(url, urlParams.toString(), str, id); 1158 } else { 1159 displayAvailableStock(allProducts, args); 1160 } 1161 } 1162 } 1163 }); 1164 } 1165 1166 function renderHomePage() { 1167 isNormalPage = true; 1168 var collections = document.querySelectorAll("[data-zs-collection-id]"); 1169 for (var i = 0; i < collections.length; i++) { 1170 var products_count = collections[i].querySelectorAll("[data-zs-product-id]").length; 1171 current_page_number = 0; 1172 if (products_count > 0) { 1173 var collection_id = collections[i].getAttribute("data-zs-collection-id"); 1174 renderCollections(collection_id); 1175 } 1176 } 1177 var categories = document.querySelectorAll("[data-zs-category-id]"); 1178 for (var i = 0; i < categories.length; i++) { 1179 var products_count = categories[i].querySelectorAll("[data-zs-product-id]").length; 1180 current_page_number = 0; 1181 if (products_count > 0) { 1182 var category_id = categories[i].getAttribute("data-zs-category-id"); 1183 renderCategories(category_id); 1184 } 1185 } 1186 var storeproducts = document.querySelectorAll("[data-element-type='storeproduct']"); 1187 var product_ids = []; 1188 for(var i = 0; i < storeproducts.length; i++) { 1189 var product = storeproducts[i].querySelector("[data-zs-product-id]"); 1190 var prd_id = product.getAttribute("data-zs-product-id"); 1191 if(!product_ids.includes(prd_id)){ 1192 product_ids.push(prd_id); 1193 } 1194 } 1195 for(var i=0; i < product_ids.length; i=i+20){ 1196 var temp = product_ids.slice(i, i+20); 1197 getProductDetailsByIds(temp.toString(), false); 1198 } 1199 } 1200 1201 function getProductDetailsByIds(product_ids, is_quick_look_page) { 1202 $X.get({ 1203 url: window.origin + "/zos-api/products?product_ids=" + product_ids, 1204 args: { 1205 collection_id: null, 1206 is_quick_look_page: is_quick_look_page 1207 }, 1208 handler: function (args) { 1209 var response = JSON.parse(this.responseText); 1210 if(response.code == 0){ 1211 displayAvailableStock(response.products, args); 1212 } 1213 } 1214 }); 1215 } 1216 1217 function getPerPageValue() { 1218 if(per_page == 0){ 1219 $X.get({ 1220 url: window.origin + "/api/store-meta", 1221 handler: function (args) { 1222 var response = JSON.parse(this.responseText); 1223 if (response.status_code == "0") { 1224 per_page = response.payload.settings.products_limit; 1225 default_sortby = response.payload.settings.default_sortby; 1226 flag_sorting = response.payload.settings.flag_sorting; 1227 if (window.zs_view == "category"){ 1228 renderCategories(); 1229 }else if(window.zs_view == "collection"){ 1230 renderCollections(); 1231 }else if(window.zs_view == "search-products"){ 1232 renderSearchProducts(); 1233 }else{ 1234 renderHomePage(); 1235 } 1236 } 1237 } 1238 }); 1239 }else{ 1240 if (window.zs_view == "category"){ 1241 renderCategories(); 1242 }else if(window.zs_view == "collection"){ 1243 renderCollections(); 1244 }else if(window.zs_view == "search-products"){ 1245 renderSearchProducts(); 1246 }else{ 1247 renderHomePage(); 1248 } 1249 } 1250 } 1251 1252 function showAvailableStockInQuickLook(){ 1253 var quick_look_element = document.getElementById("product_quick_look"); 1254 if(quick_look_element){ 1255 var product_container = quick_look_element.querySelector("[data-zs-product-id]"); 1256 if(product_container){ 1257 var product_id = product_container.getAttribute("data-zs-product-id"); 1258 var product_ids = []; 1259 product_ids[0] = product_id; 1260 getProductDetailsByIds(product_ids.toString(), true); 1261 } 1262 } 1263 } 1264 1265 function renderCollections(collection_id){ 1266 if(collection_id == null){ 1267 collection_id = window.location.pathname.substring(window.location.pathname.lastIndexOf('/') + 1); 1268 } 1269 var url = window.origin + "/zos-api/collections/" + collection_id; 1270 var str = "collection"; 1271 if(window.zs_view == "collection"){ 1272 collection_id = null; 1273 } 1274 var params = "include_products=true"; 1275 getProductDetails(url, params, str, collection_id); 1276 } 1277 1278 function renderCategories(category_id){ 1279 if(category_id == null){ 1280 category_id = window.location.pathname.substring(window.location.pathname.lastIndexOf('/') + 1); 1281 } 1282 var url = window.origin + "/zos-api/categories/" + category_id; 1283 var str = "category"; 1284 if(window.zs_view == "category"){ 1285 category_id = null; 1286 } 1287 var params = "include_products=true"; 1288 getProductDetails(url, params, str, category_id); 1289 } 1290
1291 function renderSearchProducts(){ 1292 var params = "search_text=" + window.location.search.substring(3); 1293 var url = window.origin + "/zos-api/products"; 1294 var str = ""; 1295 getProductDetails(url, params, str, null); 1296 } 1297 1298 function searchProductSucess(){ 1299 if(per_page == 0){ 1300 getPerPageValue(); 1301 }else{ 1302 setTimeout(renderSearchProducts, 200); 1303 } 1304 current_page_number = 0; 1305 if(document.querySelector("[data-sort-by-products]")){ 1306 var sort_by_products = document.querySelector("[data-sort-by-products]"); 1307 sort_by_products.addEventListener("change", function() { 1308 current_page_number = 0; 1309 }); 1310 } 1311 } 1312 1313 function lazyloadproductlistsuccess() { 1314 if(per_page == 0){ 1315 getPerPageValue(); 1316 }else{ 1317 if(window.zs_view == "category"){ 1318 setTimeout(renderCategories, 200); 1319 }else if(window.zs_view == "collection"){ 1320 setTimeout(renderCollections, 200); 1321 }else if(window.zs_view == "search-products"){ 1322 setTimeout(renderSearchProducts, 200); 1323 } 1324 } 1325 } 1326 1327 function pageOnLoad() { 1328 if (window.zs_view == "product") { 1329 showAvailableStockInProductPage(); 1330 } else if (window.zs_view == "cart" || window.location.pathname === "/cart") { 1331 applyCartPageRestrictions(); 1332 } else if(window.zs_view != "search-products"){ 1333 getPerPageValue(); 1334 } 1335 if(document.querySelector("[data-sort-by-products]")){ 1336 var sort_by_products = document.querySelector("[data-sort-by-products]"); 1337 sort_by_products.addEventListener("change", function() { 1338 current_page_number = 0; 1339 }); 1340 } 1341 if(include_product_details){ 1342 document.addEventListener("quickview:opened", showAvailableStockInQuickLook, false); 1343 } 1344 } 1345 1346 /* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1347 * Devin Hales | Added ~Feb 2026 1348 * Cart Page Restrictions: Enforces stock limits on the 1349 * /cart page quantity inputs. Sets max attribute, intercepts 1350 * increase button clicks, and polls input values to catch 1351 * changes made by Zoho's stepper buttons (which bypass 1352 * standard change events). 1353 * ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 1354 function applyCartPageRestrictions() { 1355 loadItemsData(function() { 1356 var qtyInputs = document.querySelectorAll('input[data-zs-quantity][data-zs-product-variant-id]'); 1357 1358 qtyInputs.forEach(function(qtyInput) { 1359 var variantId = qtyInput.getAttribute('data-zs-product-variant-id'); 1360 1361 var storeQty = getItemQty(variantId); 1362 if (storeQty === null) return; 1363 1364 qtyInput.setAttribute('max', storeQty); 1365 qtyInput.setAttribute('data-max-stock', storeQty); 1366 1367 qtyInput.addEventListener('change', enforceCartPageMaxQty); 1368 qtyInput.addEventListener('input', enforceCartPageMaxQty); 1369 1370 var container = qtyInput.closest('.theme-cart-qty-container') || 1371 qtyInput.closest('.cart-qty') || 1372 qtyInput.parentNode; 1373 1374 if (container) { 1375 var increaseBtn = container.querySelector('input[onclick*="increaseCount"], button[onclick*="increaseCount"], .theme-cart-qty-inc-dec[aria-label*="Increase"]'); 1376 1377 if (increaseBtn) { 1378 increaseBtn.removeAttribute('onclick'); 1379 increaseBtn.addEventListener('click', function(e) { 1380 var currentVal = parseInt(qtyInput.value) || 0; 1381 var maxStock = parseInt(qtyInput.getAttribute('data-max-stock')) || storeQty; 1382 1383 if (currentVal >= maxStock) { 1384 e.preventDefault(); 1385 e.stopPropagation(); 1386 e.stopImmediatePropagation(); 1387 showCartPageWarning(qtyInput, maxStock); 1388 return false;
1389 } 1390 1391 if (typeof window.increaseCount === 'function') { 1392 window.increaseCount(increaseBtn); 1393 } 1394 }, true); 1395 } 1396 } 1397 1398 setupCartPagePolling(qtyInput, storeQty); 1399 }); 1400 }); 1401 } 1402 1403 function enforceCartPageMaxQty(e) { 1404 try { 1405 var input = e.target; 1406 var maxStock = parseInt(input.getAttribute('data-max-stock')) || parseInt(input.getAttribute('max')); 1407 var currentVal = parseInt(input.value) || 0; 1408 1409 if (maxStock && currentVal > maxStock) { 1410 input.value = maxStock; 1411 showCartPageWarning(input, maxStock); 1412 var event = new Event('change', { bubbles: true }); 1413 input.dispatchEvent(event); 1414 } 1415 } catch (error) { 1416 console.warn('[Cart Page] Error enforcing max qty (non-critical):', error); 1417 } 1418 } 1419 1420 function showCartPageWarning(input, maxStock) { 1421 try { 1422 var existingWarnings = document.querySelectorAll('.cart-stock-warning');
1423 existingWarnings.forEach(function(w) { w.remove(); }); 1424 1425 var qtyContainer = input.closest('.theme-cart-qty-container') || 1426 input.closest('.cart-qty') || 1427 input.parentNode; 1428 1429 var containerStyle = window.getComputedStyle(qtyContainer); 1430 if (containerStyle.position === 'static') { 1431 qtyContainer.style.position = 'relative'; 1432 } 1433 1434 var warning = document.createElement('div'); 1435 warning.className = 'cart-stock-warning'; 1436 warning.style.cssText = 'color: #ff9a33; font-size: 12px; position: absolute; top: 100%; left: 0; right: 0; text-align: center; margin-top: 4px; white-space: nowrap;'; 1437 warning.textContent = 'Max available: ' + maxStock; 1438 1439 qtyContainer.appendChild(warning); 1440 1441 setTimeout(function() { 1442 if (warning.parentNode) warning.remove(); 1443 }, 5000); 1444 } catch (error) { 1445 console.warn('[Cart Page] Error showing warning (non-critical):', error); 1446 } 1447 } 1448 1449 function setupCartPagePolling(qtyInput, maxStock) { 1450 if (qtyInput._cartStockInterval) { 1451 clearInterval(qtyInput._cartStockInterval); 1452 } 1453 1454 var lastValue = parseInt(qtyInput.value) || 0; 1455 1456 var checkInterval = setInterval(function() { 1457 try { 1458 if (!document.contains(qtyInput)) { 1459 clearInterval(checkInterval); 1460 return; 1461 } 1462 1463 var currentVal = parseInt(qtyInput.value) || 0; 1464 1465 if (currentVal !== lastValue && currentVal > maxStock) { 1466 qtyInput.value = maxStock; 1467 showCartPageWarning(qtyInput, maxStock); 1468 var event = new Event('change', { bubbles: true }); 1469 qtyInput.dispatchEvent(event); 1470 lastValue = maxStock; 1471 } else { 1472 lastValue = currentVal; 1473 } 1474 } catch (e) {} 1475 }, 100); 1476 1477 qtyInput._cartStockInterval = checkInterval; 1478 } 1479 1480 document.addEventListener("zs-event-get-products-list-success", lazyloadproductlistsuccess, false); 1481 document.addEventListener("zp-event-search-success", searchProductSucess, false); 1482 document.addEventListener("DOMContentLoaded", pageOnLoad); 1483} 1484show_stock_available(); 1485</script>
1485<!-- ============================================================ Devin Hales | Added ~March 20â31, 2026 PRODUCT TABS + SHORT DESCRIPTION + 3D VIEWER SYSTEM CSS and JS for the tabbed product detail interface that replaces Zoho's default accordion layout. Reads six custom fields from window.zs_product.variants[0].custom_fields: - Features (6026428000040658995) â bulleted list tab - Applications (6026428000040692015) â bulleted list tab - Long Description/ cf_alt_description (6026428000040692021) â paragraphs tab - KnowledgeBase (6026428000040692039) â iframe tab - Short Description/ cf_short_description (6026428000040795988) â replaces product short desc - Technical Links/ cf_technical_links (6026428000040811001) â icon links in short desc + tab - 3D Images/ cf_d_images (6026428000041265039) â triggers 3D viewer modal Falls back to DOM extraction from the existing product description HTML for un-migrated products. Only activates if Features content is found. The 3D Viewer uses Google model-viewer in a full-screen modal overlay with settings panel (exposure, shadow, tone mapping, HDR environment selection/upload). Mobile: tabs collapse to an accordion layout via<details>/<summary> elements. ============================================================ --> <style> /* ---- Hide custom fields from Zoho default accordion ---- */ [data-zs-custom-field-display-id="6026428000040658995"], [data-zs-custom-field-display-id="6026428000040692015"], [data-zs-custom-field-display-id="6026428000040692021"], [data-zs-custom-field-display-id="6026428000040692039"], [data-zs-custom-field-display-id="6026428000040795988"], [data-zs-custom-field-display-id="6026428000040811001"], [data-zs-custom-field-display-id="6026428000041265039"] { display: none !important; } .zc-tabs { font-family: inherit; line-height: 1.6; max-width: 100%; margin: 20px 0; } .zc-tabs input[type="radio"] { display: none !important; position: absolute !important; width: 0 !important; height: 0 !important; opacity: 0 !important; pointer-events: none !important; } .zc-tabs .tab-labels { display: flex; flex-wrap: wrap; gap: 0; margin-bottom: 0; } .zc-tabs .tab-labels label { padding: 10px 16px; border: 1px solid #d9d9d9; border-bottom: none; background: #f5f5f5; cursor: pointer; font-weight: bold; font-size: 18px; white-space: nowrap; transition: background 0.15s ease; } .zc-tabs .tab-labels label:hover { background: #e8e8e8; } .zc-tabs #tab-features:checked ~ .tab-labels label[for="tab-features"], .zc-tabs #tab-applications:checked ~ .tab-labels label[for="tab-applications"], .zc-tabs #tab-desc:checked ~ .tab-labels label[for="tab-desc"], .zc-tabs #tab-techlinks:checked ~ .tab-labels label[for="tab-techlinks"], .zc-tabs #tab-knowledgebase:checked ~ .tab-labels label[for="tab-knowledgebase"] { background: #ffffff; border-bottom: 2px solid #ffffff; color: #004aad; } .zc-tabs .panels .tab-panel { display: none; padding: 20px; border: 1px solid #d9d9d9; background: #ffffff; line-height: 1.7; } .zc-tabs #tab-features:checked ~ .panels #panel-features, .zc-tabs #tab-applications:checked ~ .panels #panel-applications, .zc-tabs #tab-desc:checked ~ .panels #panel-desc, .zc-tabs #tab-techlinks:checked ~ .panels #panel-techlinks, .zc-tabs #tab-knowledgebase:checked ~ .panels #panel-knowledgebase { display: block; } .zc-tabs .tab-panel ul { margin: 0; padding: 0 0 0 20px; } .zc-tabs .tab-panel ul li { margin-bottom: 4px; } .zc-tabs .tab-panel p { margin: 0 0 12px; } .zc-tabs .tab-panel p:last-child { margin-bottom: 0; } .zc-tabs .tab-panel a { color: #004aad; text-decoration: none; } .zc-tabs .tab-panel a:hover { text-decoration: underline; } @media (max-width: 768px) { .zc-tabs .tab-labels, .zc-tabs .panels .tab-panel { display: none !important; } .zc-tabs details { margin: 0 0 8px !important; } .zc-tabs details summary { background: #f5f5f5 !important; color: #004aad !important; border: 1px solid #d9d9d9 !important; padding: 12px 44px 12px 16px !important; font-size: 14px !important; font-weight: bold !important; cursor: pointer; position: relative; list-style: none !important; } .zc-tabs details summary::-webkit-details-marker, .zc-tabs details summary::marker { display: none !important; } .zc-tabs details summary::after { content: '+'; position: absolute; right: 16px; top: 50%; transform: translateY(-50%); font-size: 18px; font-weight: 400; color: #666; } .zc-tabs details[open] summary { background: #004aad !important; color: #ffffff !important; } .zc-tabs details[open] summary::after { content: 'â'; color: #ffffff; } .zc-tabs details > div { padding: 16px !important; border: 1px solid #d9d9d9 !important; border-top: none !important; background: #ffffff !important; line-height: 1.7; } } @media (min-width: 769px) { .zc-tabs details { display: none !important; } } .tyc-sd__text { margin: 0 0 10px; line-height: 1.6; color: #333; } .tyc-sd__features-label { font-weight: bold; color: #004aad; margin: 10px 0 4px; } .tyc-sd__features { margin: 0 0 12px; padding: 0; list-style: none; line-height: 1.4; } .tyc-sd__features li { margin: 0 0 2px; padding: 0; } .tyc-sd__features li::before { content: '⢠'; color: #004aad; } .tyc-sd__links { display: flex; flex-wrap: wrap; gap: 12px; margin: 10px 0 0; align-items: center; } .tyc-sd__links a { display: inline-block; text-decoration: none; text-align: center; } .tyc-sd__links a img { height: 80px; width: auto; display: block; margin: 0 auto; transition: opacity 0.15s ease; } .tyc-sd__links a:hover img { opacity: 0.75; } /* ---- 3D Viewer Modal ---- */ .tyc-3d-icon { cursor: pointer; } .tyc-3d-icon img { height: 74px !important; } .tyc-3d-icon:hover img { opacity: 0.75; } .tycon-3d-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 99999; background: rgba(10, 10, 30, 0.92); backdrop-filter: blur(4px); align-items: center; justify-content: center; padding: 24px; } .tycon-3d-overlay.active { display: flex; } .tycon-3d-container { position: relative; display: flex; width: 100%; max-width: 800px; height: 70vh; max-height: 600px; background: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 8px 40px rgba(0,0,0,0.5); transition: max-width 0.3s ease; } .tycon-3d-container.settings-open { max-width: 1100px; } .tycon-3d-model-area { position: relative; flex: 1; min-width: 0; } .tycon-3d-container model-viewer { width: 100%; height: 100%; --poster-color: transparent; } .tycon-3d-container model-viewer::part(default-progress-bar) { background: #004aad; height: 3px; } .tycon-3d-badge { position: absolute; top: 14px; left: 14px; z-index: 10; display: inline-flex; align-items: center; gap: 5px; background: rgba(0, 74, 173, 0.1); color: #004aad; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.2px; padding: 5px 12px; border-radius: 20px; pointer-events: none; } .tycon-3d-close { position: absolute; top: 12px; right: 12px; z-index: 10; width: 36px; height: 36px; border: none; border-radius: 50%; background: rgba(0,0,0,0.08); color: #333; font-size: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.15s ease; line-height: 1; } .tycon-3d-close:hover { background: rgba(0,0,0,0.15); } .tycon-3d-hint { position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); z-index: 10; background: rgba(0,0,0,0.06); color: #888; font-size: 12px; padding: 5px 14px; border-radius: 20px; pointer-events: none; white-space: nowrap; } @media (max-width: 768px) { .tycon-3d-container { max-width: 100%; height: 80vh; flex-direction: column; } .tycon-3d-container.settings-open { max-width: 100%; } .tycon-3d-model-area { min-height: 0; flex: 1; } .tycon-3d-controls.active { width: 100%; max-height: 40vh; border-left: none; border-top: 1px solid #e8e8e8; } .tycon-3d-overlay { padding: 12px; } } /* ---- 3D Viewer Controls Panel ---- */ .tycon-3d-settings-btn { position: absolute; top: 12px; right: 56px; z-index: 10; width: 36px; height: 36px; border: none; border-radius: 50%; background: rgba(0,0,0,0.08); color: #333; font-size: 18px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.15s ease; line-height: 1; } .tycon-3d-settings-btn:hover { background: rgba(0,0,0,0.15); } .tycon-3d-settings-btn.active { background: #004aad; color: #fff; } .tycon-3d-controls { display: none; width: 280px; flex-shrink: 0; background: #f8f9fa; border-left: 1px solid #e8e8e8; padding: 16px 18px; overflow-y: auto; } .tycon-3d-controls.active { display: block; } .tycon-3d-controls h4 { margin: 0 0 12px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #004aad; } .tycon-3d-ctrl-row { margin-bottom: 12px; } .tycon-3d-ctrl-row:last-child { margin-bottom: 0; } .tycon-3d-ctrl-label { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #555; margin-bottom: 4px; font-weight: 600; } .tycon-3d-ctrl-label .tycon-3d-ctrl-val { font-family: 'SF Mono', 'Consolas', monospace; color: #004aad; font-weight: 700; font-size: 11px; } .tycon-3d-ctrl-row input[type="range"] { width: 100%; height: 6px; -webkit-appearance: none; appearance: none; background: #e0e0e0; border-radius: 3px; outline: none; margin: 0; } .tycon-3d-ctrl-row input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; border-radius: 50%; background: #004aad; cursor: pointer; border: 2px solid #fff; box-shadow: 0 1px 4px rgba(0,0,0,0.2); } .tycon-3d-ctrl-row input[type="range"]::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: #004aad; cursor: pointer; border: 2px solid #fff; box-shadow: 0 1px 4px rgba(0,0,0,0.2); } .tycon-3d-ctrl-row select { width: 100%; padding: 5px 8px; font-size: 12px; border: 1px solid #d9d9d9; border-radius: 5px; background: #fff; color: #333; cursor: pointer; outline: none; } .tycon-3d-ctrl-row select:focus { border-color: #004aad; } .tycon-3d-ctrl-divider { border: none; border-top: 1px solid #e8e8e8; margin: 12px 0; } .tycon-3d-ctrl-copy { display: block; width: 100%; padding: 7px; margin-top: 12px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; border: 1px solid #004aad; border-radius: 5px; background: #fff; color: #004aad; cursor: pointer; text-align: center; transition: all 0.15s ease; } .tycon-3d-ctrl-copy:hover { background: #004aad; color: #fff; } </style>
1485<script> 1486(function() { 1487 'use strict'; 1488 1489 if (window.zs_view !== 'product') return; 1490 1491 var CONFIG = { 1492 kbBaseUrl: 'https://tyconsystemsinc.zohodesk.com/portal/en/kb/', 1493 iconBasePath: '/images/Icons/', 1494 iconExtension: '.png', 1495 maxShortFeatures: 5, 1496 maxShortLinks: 4 1497 }; 1498 1499 var FIELD_IDS = { 1500 features: '6026428000040658995', 1501 applications: '6026428000040692015', 1502 altDescription: '6026428000040692021', 1503 knowledgeBase: '6026428000040692039', 1504 shortDescription: '6026428000040795988', 1505 technicalLinks: '6026428000040811001', 1506 model3d: '6026428000041265039' 1507 }; 1508 1509 /* ââ Utilities âââââââââââââââââââââââââââââââââââââââââââ */ 1510 1511 function decodeHtml(str) { 1512 var textarea = document.createElement('textarea'); 1513 textarea.innerHTML = str; 1514 return textarea.value; 1515 } 1516 1517 function escapeHtml(str) { 1518 var decoded = decodeHtml(str); 1519 return decoded.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); 1520 } 1521 1522 function isHtml(text) { 1523 return /<[a-z][\s\S]*>/i.test(text); 1524 } 1525 1526 function getLines(value) { 1527 if (!value) return []; 1528 return value.split('\n').filter(function(l) { return l.trim(); }).map(function(l) { return l.trim(); }); 1529 } 1530 1531 function getFieldById(fieldId) { 1532 try { 1533 var fields = window.zs_product.variants[0].custom_fields; 1534 for (var i = 0; i < fields.length; i++) { 1535 if (fields[i].customfield_id === fieldId) { 1536 return fields[i].value || ''; 1537 } 1538 } 1539 } catch (e) {} 1540 return ''; 1541 } 1542 1543 function getAllFieldValues() { 1544 var values = {}; 1545 for (var key in FIELD_IDS) { 1546 if (FIELD_IDS.hasOwnProperty(key)) { 1547 values[key] = getFieldById(FIELD_IDS[key]); 1548 } 1549 } 1550 return values; 1551 } 1552 1553 /* ââ Content Formatters ââââââââââââââââââââââââââââââââââ */ 1554 1555 function toList(text) { 1556 if (!text || !text.trim()) return ''; 1557 if (isHtml(text)) return text; 1558 var items = getLines(text); 1559 if (items.length === 0) return ''; 1560 return '<ul>' + items.map(function(l) { 1561 return '<li>' + escapeHtml(l) + '</li>'; 1562 }).join('') + '</ul>'; 1563 } 1564 1565 function toParagraphs(text) { 1566 if (!text || !text.trim()) return ''; 1567 if (isHtml(text)) return text; 1568 var paras = text.split(/\n\s*\n/).filter(function(p) { return p.trim(); }); 1569 if (paras.length === 0) return ''; 1570 return paras.map(function(p) { 1571 return '<p>' + escapeHtml(p.trim()).replace(/\n/g, '<br>') + '</p>'; 1572 }).join(''); 1573 } 1574 1575 function toIframe(value) { 1576 if (!value || !value.trim()) return ''; 1577 var clean = value.trim(); 1578 var fullUrl = clean.indexOf('http') === 0 1579 ? clean 1580 : CONFIG.kbBaseUrl + encodeURI(clean.replace(/^\/+|\/+$/g, '')); 1581 return '<iframe src="' + fullUrl + '" ' + 1582 'title="Knowledge Base" ' + 1583 'style="border:1px solid #fafafa; height:500px; width:100%;"></iframe>'; 1584 } 1585 1586 function parseTechLinks(text) { 1587 if (!text || !text.trim()) return []; 1588 var links = []; 1589 var lines = getLines(text); 1590 for (var i = 0; i < lines.length; i++) { 1591 var httpIdx = lines[i].indexOf('http'); 1592 if (httpIdx > 0) { 1593 var rawLabel = lines[i].substring(0, httpIdx).trim(); 1594 var url = lines[i].substring(httpIdx).trim(); 1595 if (rawLabel && url) { 1596 var hasExtension = /\.(png|jpg|jpeg|gif|svg|webp)$/i.test(rawLabel); 1597 var iconFilename = hasExtension ? rawLabel : rawLabel + CONFIG.iconExtension; 1598 var displayLabel = rawLabel.replace(/\.(png|jpg|jpeg|gif|svg|webp)$/i, ''); 1599 links.push({ 1600 label: displayLabel, 1601 url: url, 1602 iconUrl: CONFIG.iconBasePath + encodeURIComponent(iconFilename) 1603 }); 1604 } 1605 } 1606 } 1607 return links; 1608 } 1609 1610 function techLinksToPanel(links) { 1611 if (!links || links.length === 0) return ''; 1612 var html = '<div style="display:flex; gap:24px; flex-wrap:wrap; padding:8px 0;">'; 1613 for (var i = 0; i < links.length; i++) { 1614 var l = links[i]; 1615 html += '<a href="' + escapeHtml(l.url) + '" target="_blank" title="' + escapeHtml(l.label) + '" ' + 1616 'style="display:inline-block; text-align:center; text-decoration:none;">' + 1617 '<img src="' + escapeHtml(l.iconUrl) + '" alt="' + escapeHtml(l.label) + '" ' + 1618 'style="height:100px; width:auto; display:block; margin:0 auto 6px;">' + 1619 '<span style="font-size:12px; color:#004aad;">' + escapeHtml(l.label) + '</span></a>'; 1620 } 1621 html += '</div>'; 1622 return html; 1623 } 1624 1625 /* ââ DOM Extraction (fallback for un-migrated products) â */ 1626 1627 function extractFromDescription(descContainer) { 1628 var extracted = { features: '', applications: '', description: '', knowledgebase: '' }; 1629 if (!descContainer) return extracted; 1630 1631 var headings = descContainer.querySelectorAll('h5'); 1632 for (var i = 0; i < headings.length; i++) { 1633 if (headings[i].textContent.trim().toLowerCase() === 'features') { 1634 var content = []; 1635 var sibling = headings[i].nextElementSibling; 1636 while (sibling) { 1637 if (sibling.querySelector && sibling.querySelector('details')) break; 1638 content.push(sibling.outerHTML); 1639 sibling = sibling.nextElementSibling; 1640 } 1641 extracted.features = content.join(''); 1642 break; 1643 } 1644 } 1645 1646 if (!extracted.features) { 1647 var divs = descContainer.querySelectorAll('div'); 1648 for (var d = 0; d < divs.length; d++) { 1649 if (divs[d].textContent.trim().toLowerCase() === 'features' && 1650 divs[d].style && divs[d].style.fontWeight) { 1651 var nextDiv = divs[d].nextElementSibling; 1652 if (nextDiv) extracted.features = nextDiv.innerHTML; 1653 break; 1654 } 1655 } 1656 } 1657 1658 var details = descContainer.querySelectorAll('details'); 1659 for (var j = 0; j < details.length; j++) { 1660 var summary = details[j].querySelector('summary'); 1661 if (!summary) continue; 1662 var label = summary.textContent.trim().toLowerCase(); 1663 var contentDiv = details[j].querySelector('summary + div'); 1664 if (!contentDiv) continue; 1665 var html = contentDiv.innerHTML; 1666 1667 if (label === 'features' && !extracted.features) { 1668 extracted.features = html; 1669 } else if (label === 'applications') { 1670 extracted.applications = html; 1671 } else if (label === 'description') { 1672 extracted.description = html; 1673 } else if (label === 'knowledge base') { 1674 extracted.knowledgebase = html; 1675 } 1676 } 1677 1678 return extracted; 1679 } 1680 1681 /* ââ Short Description Override âââââââââââââââââââââââââââ */ 1682 1683 function buildShortDescription(cf, allTechLinks) {
1684 var sdText = cf.shortDescription; 1685 if (!sdText || !sdText.trim()) return; 1686 1687 var target = document.querySelector('[data-zs-product-short-description]'); 1688 if (!target) return; 1689 1690 var html = ''; 1691 1692 html += '<div class="tyc-sd__text">' + 1693 escapeHtml(sdText.trim()).replace(/\n/g, '<br>') + '</div>'; 1694 1695 var featureLines = getLines(cf.features); 1696 if (featureLines.length > 0) { 1697 var show = featureLines.slice(0, CONFIG.maxShortFeatures); 1698 html += '<div class="tyc-sd__features-label">Key Features:</div>'; 1699 html += '<ul class="tyc-sd__features">'; 1700 for (var i = 0; i < show.length; i++) { 1701 html += '<li>' + escapeHtml(show[i]) + '</li>'; 1702 } 1703 html += '</ul>'; 1704 } 1705 1706 var has3dModel = cf.model3d && cf.model3d.trim(); 1707 var hasLinks = allTechLinks.length > 0; 1708 1709 if (hasLinks || has3dModel) { 1710 var showLinks = allTechLinks.slice(0, CONFIG.maxShortLinks); 1711 html += '<div class="tyc-sd__links">'; 1712 for (var j = 0; j < showLinks.length; j++) { 1713 var lk = showLinks[j]; 1714 html += '<a href="' + escapeHtml(lk.url) + '" target="_blank" title="' + escapeHtml(lk.label) + '">' + 1715 '<img src="' + escapeHtml(lk.iconUrl) + '" alt="' + escapeHtml(lk.label) + '">' + 1716 '</a>'; 1717 } 1718 if (has3dModel) { 1719 html += '<a href="javascript:void(0)" class="tyc-3d-icon" id="tycon-3d-trigger" title="3D View">' + 1720 '<img src="' + escapeHtml(CONFIG.iconBasePath) + '3D%20View.png" alt="3D View">' + 1721 '</a>'; 1722 } 1723 html += '</div>'; 1724 } 1725 1726 target.innerHTML = html; 1727 } 1728 1729 /* ââ 3D Viewer Modal âââââââââââââââââââââââââââââââââââââ */ 1730 1731 function build3dViewer(glbFilename) { 1732 if (!glbFilename || !glbFilename.trim()) return; 1733 1734 var file = glbFilename.trim(); 1735 if (!file.toLowerCase().endsWith('.glb')) { 1736 file = file + '.glb'; 1737 } 1738 var modelUrl = 'https://www.tyconsystems.com/3dimages/' + encodeURIComponent(file); 1739 1740 // Create overlay 1741 var overlay = document.createElement('div'); 1742 overlay.className = 'tycon-3d-overlay'; 1743 overlay.id = 'tycon-3d-overlay'; 1744 overlay.innerHTML = 1745 '<div class="tycon-3d-container">' + 1746 ' <div class="tycon-3d-model-area">' + 1747 ' <span class="tycon-3d-badge">◆ 3D View</span>' + 1748 ' <button class="tycon-3d-settings-btn" id="tycon-3d-settings-btn" title="Settings">⚙</button>' + 1749 ' <button class="tycon-3d-close" id="tycon-3d-close" title="Close">×</button>' + 1750 ' <model-viewer' + 1751 ' id="tycon-3d-model"' + 1752 ' alt="3D Product View"' + 1753 ' auto-rotate' + 1754 ' camera-controls' + 1755 ' orientation="0 90deg 0"' + 1756 ' shadow-intensity="2"' + 1757 ' shadow-softness="0.6"' + 1758 ' exposure="1.1"' + 1759 ' environment-image="https://www.tyconsystems.com/3dimages/hdr/abandoned_hopper_terminal_04_1k.hdr"' + 1760 ' tone-mapping="commerce"' + 1761 ' interaction-prompt="auto"' + 1762 ' ar' + 1763 ' ar-modes="webxr scene-viewer quick-look"' + 1764 ' ></model-viewer>' + 1765 ' <div class="tycon-3d-hint">Drag to rotate · Scroll to zoom · Right-click to pan</div>' + 1766 ' </div>' + 1767 ' <div class="tycon-3d-controls" id="tycon-3d-controls">' + 1768 ' <h4>Rendering Settings</h4>' + 1769 ' <div class="tycon-3d-ctrl-row">' + 1770 ' <div class="tycon-3d-ctrl-label">Exposure <span class="tycon-3d-ctrl-val" id="tycon-ctrl-val-exposure">1.1</span></div>' + 1771 ' <input type="range" id="tycon-ctrl-exposure" min="0.1" max="3" step="0.05" value="1.1">' + 1772 ' </div>' + 1773 ' <div class="tycon-3d-ctrl-row">' + 1774 ' <div class="tycon-3d-ctrl-label">Shadow Intensity <span class="tycon-3d-ctrl-val" id="tycon-ctrl-val-shadow-intensity">2</span></div>' + 1775 ' <input type="range" id="tycon-ctrl-shadow-intensity" min="0" max="5" step="0.1" value="2">' + 1776 ' </div>' + 1777 ' <div class="tycon-3d-ctrl-row">' + 1778 ' <div class="tycon-3d-ctrl-label">Shadow Softness <span class="tycon-3d-ctrl-val" id="tycon-ctrl-val-shadow-softness">0.6</span></div>' + 1779 ' <input type="range" id="tycon-ctrl-shadow-softness" min="0" max="1" step="0.05" value="
17790.6">' + 1780 ' </div>' + 1781 ' <hr class="tycon-3d-ctrl-divider">' + 1782 ' <div class="tycon-3d-ctrl-row">' + 1783 ' <div class="tycon-3d-ctrl-label">Tone Mapping</div>' + 1784 ' <select id="tycon-ctrl-tone-mapping">' + 1785 ' <option value="commerce" selected>Commerce</option>' + 1786 ' <option value="neutral">Neutral</option>' + 1787 ' <option value="aces">ACES</option>' + 1788 ' <option value="agx">AgX</option>' + 1789 ' </select>' + 1790 ' </div>' + 1791 ' <div class="tycon-3d-ctrl-row">' + 1792 ' <div class="tycon-3d-ctrl-label">Environment</div>' + 1793 ' <select id="tycon-ctrl-environment">' + 1794 ' <option value="legacy" selected>Legacy (Directional)</option>' + 1795 ' <option value="">Neutral (Default)</option>' + 1796 ' <option value="https://www.tyconsystems.com/3dimages/hdr/abandoned_hopper_terminal_04_1k.hdr" selected>Abandoned Hopper Terminal</option>' + 1797 ' <option value="https://www.tyconsystems.com/3dimages/hdr/countrytrax_midday_1k.hdr">Country Trax Midday</option>' + 1798 ' <option value="https://www.tyconsystems.com/3dimages/hdr/suburban_garden_1k.hdr">Suburban Garden</option>' + 1799 ' </select>' + 1800 ' </div>' + 1801 ' <div class="tycon-3d-ctrl-row">' + 1802 ' <div class="tycon-3d-ctrl-label">Upload HDR</div>' + 1803 ' <input type="file" id="tycon-ctrl-hdr-upload" accept=".hdr,.exr" style="font-size:11px; width:100%;">' + 1804 ' </div>' + 1805 ' <button class="tycon-3d-ctrl-copy" id="tycon-3d-copy-settings">Copy Settings</button>' + 1806 ' </div>' + 1807 '</div>'; 1808 1809 document.body.appendChild(overlay); 1810 1811 var settingsBtn = document.getElementById('tycon-3d-settings-btn'); 1812 var controlsPanel = document.getElementById('tycon-3d-controls'); 1813 var container3d = overlay.querySelector('.tycon-3d-container'); 1814 1815 settingsBtn.addEventListener('click', function(e) { 1816 e.stopPropagation(); 1817 controlsPanel.classList.toggle('active'); 1818 settingsBtn.classList.toggle('active'); 1819 container3d.classList.toggle('settings-open'); 1820 }); 1821 1822 controlsPanel.addEventListener('click', function(e) { 1823 e.stopPropagation(); 1824 }); 1825 1826 var mv = function() { return document.getElementById('tycon-3d-model'); }; 1827 1828 function wireSlider(sliderId, valId, attribute, formatter) { 1829 var slider = document.getElementById(sliderId); 1830 var valDisplay = document.getElementById(valId); 1831 if (!slider) return; 1832 slider.addEventListener('input', function() { 1833 var val = parseFloat(slider.value); 1834 valDisplay.textContent = val; 1835 var el = mv(); 1836 if (el) { 1837 if (formatter) { 1838 el.setAttribute(attribute, formatter(val)); 1839 } else { 1840 el.setAttribute(attribute, val); 1841 } 1842 } 1843 }); 1844 } 1845 1846 wireSlider('tycon-ctrl-exposure', 'tycon-ctrl-val-exposure', 'exposure'); 1847 wireSlider('tycon-ctrl-shadow-intensity', 'tycon-ctrl-val-shadow-intensity', 'shadow-intensity'); 1848 wireSlider('tycon-ctrl-shadow-softness', 'tycon-ctrl-val-shadow-softness', 'shadow-softness'); 1849 1850 document.getElementById('tycon-ctrl-tone-mapping').addEventListener('change', function() { 1851 var el = mv(); 1852 if (el) el.setAttribute('tone-mapping', this.value); 1853 }); 1854 1855 document.getElementById('tycon-ctrl-environment').addEventListener('change', function() { 1856 var el = mv(); 1857 if (el) { 1858 if (this.value === '__custom__') { 1859 var opt = this.querySelector('option[value="__custom__"]'); 1860 var blobUrl = opt ? opt.getAttribute('data-blob') : ''; 1861 if (blobUrl) el.setAttribute('environment-image', blobUrl); 1862 } else if (this.value) { 1863 el.setAttribute('environment-image', this.value); 1864 } else { 1865 el.removeAttribute('environment-image'); 1866 } 1867 } 1868 }); 1869 1870 document.getElementById('tycon-ctrl-hdr-upload').addEventListener('change', function() { 1871 var file = this.files[0]; 1872 if (!file) return; 1873 var blobUrl = URL.createObjectURL(file); 1874 var el = mv(); 1875 if (el) el.setAttribute('environment-image', blobUrl); 1876
1877 var envSelect = document.getElementById('tycon-ctrl-environment'); 1878 var customOpt = envSelect.querySelector('option[value="__custom__"]'); 1879 if (!customOpt) { 1880 customOpt = document.createElement('option'); 1881 customOpt.value = '__custom__'; 1882 envSelect.appendChild(customOpt); 1883 } 1884 customOpt.textContent = file.name; 1885 customOpt.setAttribute('data-blob', blobUrl); 1886 envSelect.value = '__custom__'; 1887 }); 1888 1889 document.getElementById('tycon-3d-copy-settings').addEventListener('click', function() { 1890 var envVal = document.getElementById('tycon-ctrl-environment').value; 1891 if (envVal === '__custom__') { 1892 var customOpt = document.getElementById('tycon-ctrl-environment').querySelector('option[value="__custom__"]'); 1893 envVal = customOpt ? customOpt.textContent : '(custom upload)'; 1894 } 1895 var settings = { 1896 exposure: document.getElementById('tycon-ctrl-exposure').value, 1897 'shadow-intensity': document.getElementById('tycon-ctrl-shadow-intensity').value, 1898 'shadow-softness': document.getElementById('tycon-ctrl-shadow-softness').value, 1899 'tone-mapping': document.getElementById('tycon-ctrl-tone-mapping').value, 1900 'environment-image': envVal || '(default)' 1901 }; 1902 1903 var lines = Object.keys(settings).map(function(k) { 1904 return k + '="' + settings[k] + '"'; 1905 }); 1906 1907 var text = lines.join('\n'); 1908 1909 if (navigator.clipboard && navigator.clipboard.writeText) { 1910 navigator.clipboard.writeText(text); 1911 } else { 1912 var ta = document.createElement('textarea'); 1913 ta.value = text; 1914 document.body.appendChild(ta); 1915 ta.select(); 1916 document.execCommand('copy'); 1917 document.body.removeChild(ta); 1918 } 1919 1920 var btn = document.getElementById('tycon-3d-copy-settings'); 1921 var orig = btn.textContent; 1922 btn.textContent = 'Copied!'; 1923 btn.style.background = '#004aad'; 1924 btn.style.color = '#fff'; 1925 setTimeout(function() { 1926 btn.textContent = orig; 1927 btn.style.background = ''; 1928 btn.style.color = ''; 1929 }, 2000); 1930 }); 1931 1932 document.getElementById('tycon-3d-close').addEventListener('click', function() { 1933 overlay.classList.remove('active'); 1934 controlsPanel.classList.remove('active'); 1935 settingsBtn.classList.remove('active'); 1936 container3d.classList.remove('settings-open'); 1937 }); 1938 1939 overlay.addEventListener('click', function(e) { 1940 if (e.target === overlay) { 1941 overlay.classList.remove('active'); 1942 controlsPanel.classList.remove('active'); 1943 settingsBtn.classList.remove('active'); 1944 container3d.classList.remove('settings-open'); 1945 } 1946 }); 1947 1948 document.addEventListener('keydown', function(e) { 1949 if (e.key === 'Escape' && overlay.classList.contains('active')) { 1950 overlay.classList.remove('active'); 1951 controlsPanel.classList.remove('active'); 1952 settingsBtn.classList.remove('active'); 1953 container3d.classList.remove('settings-open'); 1954 } 1955 }); 1956 1957 var trigger = document.getElementById('tycon-3d-trigger'); 1958 if (trigger) { 1959 var modelLoaded = false; 1960 trigger.addEventListener('click', function(e) { 1961 e.preventDefault(); 1962 if (!modelLoaded) { 1963 document.getElementById('tycon-3d-model').setAttribute('src', modelUrl); 1964 modelLoaded = true; 1965 } 1966 overlay.classList.add('active'); 1967 }); 1968 } 1969 1970 console.log('[3D] Viewer ready for', file); 1971 } 1972 1973 /* ââ Tab Builder ââââââââââââââââââââââââââââââââââââââââââ */ 1974 1975 function buildTabs(tabContent) { 1976 var tabs = [ 1977 { id: 'features', label: 'Features', content: tabContent.features }, 1978 { id: 'applications', label: 'Applications', content: tabContent.applications }, 1979 { id: 'desc', label: 'Description', content: tabContent.description }, 1980 { id: 'techlinks', label: 'Technical Links', content: tabContent.techLinks }, 1981 { id: 'knowledgebase', label: 'Knowledge Base', content: tabContent.knowledgebase } 1982 ]; 1983 1984 var activeTabs = tabs.filter(function(t) { return t.content && t.content.trim(); }); 1985 if (activeTabs.length === 0) return; 1986 1987 var radios = '';
1988 activeTabs.forEach(function(t, i) { 1989 radios += '<input ' + (i === 0 ? 'checked ' : '') + 1990 'id="tab-' + t.id + '" name="zc-tab" type="radio">'; 1991 }); 1992 1993 var labels = '<div class="tab-labels">'; 1994 activeTabs.forEach(function(t) { 1995 labels += '<label for="tab-' + t.id + '">' + escapeHtml(t.label) + '</label>'; 1996 }); 1997 labels += '</div>'; 1998 1999 var panels = '<div class="panels">'; 2000 activeTabs.forEach(function(t) { 2001 panels += '<div id="panel-' + t.id + '" class="tab-panel">' + t.content + '</div>'; 2002 }); 2003 panels += '</div>'; 2004 2005 var accordion = ''; 2006 activeTabs.forEach(function(t, i) { 2007 accordion += '<details' + (i === 0 ? ' open' : '') + '>' + 2008 '<summary>' + escapeHtml(t.label) + '</summary>' + 2009 '<div>' + t.content + '</div>' + 2010 '</details>'; 2011 }); 2012 2013 return '<div class="zc-tabs">' + radios + labels + panels + accordion + '</div>'; 2014 } 2015 2016 /* ââ Init âââââââââââââââââââââââââââââââââââââââââââââââââ */ 2017 2018 function init() { 2019 try { 2020 var acc = document.querySelector('.theme-custom-field-accordion'); 2021 if (acc) acc.style.display = 'none'; 2022 2023 var cf = getAllFieldValues(); 2024 var allTechLinks = parseTechLinks(cf.technicalLinks); 2025 2026 buildShortDescription(cf, allTechLinks); 2027 build3dViewer(cf.model3d); 2028 2029 var descContainer = document.querySelector('[data-zs-description]'); 2030 var domContent = extractFromDescription(descContainer); 2031 2032 var tabContent = { 2033 features: cf.features ? toList(cf.features) : domContent.features, 2034 applications: cf.applications ? toList(cf.applications) : domContent.applications, 2035 description: cf.altDescription ? toParagraphs(cf.altDescription) : domContent.description, 2036 knowledgebase: cf.knowledgeBase ? toIframe(cf.knowledgeBase) : domContent.knowledgebase, 2037 techLinks: techLinksToPanel(allTechLinks) 2038 }; 2039 2040 if (!tabContent.features || !tabContent.features.trim()) { 2041 console.log('[Tabs] No Features content found â skipping tab injection'); 2042 return; 2043 } 2044 2045 if (descContainer) { 2046 var tabContentEl = descContainer.closest('.theme-prod-detail-tab-content'); 2047 if (tabContentEl) { 2048 tabContentEl.style.display = 'none'; 2049 } else { 2050 descContainer.style.display = 'none'; 2051 } 2052 } 2053 2054 var tabsHtml = buildTabs(tabContent); 2055 if (!tabsHtml) return; 2056 2057 var wrapper = document.createElement('div'); 2058 wrapper.id = 'tycon-product-tabs'; 2059 wrapper.innerHTML = tabsHtml; 2060 2061 var insertTarget = descContainer 2062 ? (descContainer.closest('.theme-product-description') || descContainer.parentNode) 2063 : document.querySelector('[data-zs-product-details-tab-section]'); 2064 2065 if (insertTarget) { 2066 insertTarget.appendChild(wrapper); 2067 } else { 2068 var productEl = document.querySelector('[data-zs-product-id]'); 2069 if (productEl) productEl.appendChild(wrapper); 2070 } 2071 2072 console.log('[Tabs] Product tabs injected successfully'); 2073 2074 } catch (err) { 2075 console.warn('[Tabs] Error (non-critical):', err.message || err); 2076 } 2077 } 2078 2079 if (document.readyState === 'loading') { 2080 document.addEventListener('DOMContentLoaded', function() { 2081 setTimeout(init, 300); 2082 }); 2083 } else { 2084 setTimeout(init, 300); 2085 } 2086 2087})(); 2088</script>
2088<!-- ============================================================ Devin Hales | Added ~Feb 12, 2026 Shipping Method Sort: Sorts checkout shipping options from lowest to highest cost. Uses MutationObserver on the shipping-methods container and XHR interception on checkout/address calls to re-sort after address changes or dynamic re-renders by Zoho's core JS.============================================================ -->
2088<script> 2089// Sort shipping options by lowest to highest cost 2090(function() { 2091 if (window.location.pathname.indexOf('checkout') === -1) return; 2092 2093 function sortShippingMethods() { 2094 var form = document.getElementById('zs-shipping-methods-form'); 2095 if (!form) return false; 2096 2097 var shippingItems = form.querySelectorAll('.theme-checkout-list'); 2098 if (shippingItems.length < 2) return false; 2099 2100 var prices = []; 2101 shippingItems.forEach(function(item) { 2102 var p = item.querySelector('.theme-checkout-list-content p'); 2103 if (p) prices.push(parseFloat(p.textContent.replace(/[^0-9.]/g, ''))); 2104 }); 2105 2106 var alreadySorted = true; 2107 for (var i = 1; i < prices.length; i++) { 2108 if (prices[i] < prices[i-1]) { alreadySorted = false; break; } 2109 } 2110 if (alreadySorted) return true; 2111 2112 var itemsArray = Array.prototype.slice.call(shippingItems); 2113 itemsArray.sort(function(a, b) { 2114 var priceA = parseFloat(a.querySelector('.theme-checkout-list-content p').textContent.replace(/[^0-9.]/g, '')); 2115 var priceB = parseFloat(b.querySelector('.theme-checkout-list-content p').textContent.replace(/[^0-9.]/g, '')); 2116 return priceA - priceB; 2117 }); 2118 2119 var parent = shippingItems[0].parentNode; 2120 itemsArray.forEach(function(item) { 2121 parent.appendChild(item); 2122 }); 2123 2124 return true; 2125 } 2126 2127 function sortWithRetry() { 2128 var attempts = 0; 2129 var maxAttempts = 30; 2130 var interval = setInterval(function() { 2131 attempts++; 2132 if (sortShippingMethods() || attempts >= maxAttempts) { 2133 clearInterval(interval); 2134 } 2135 }, 100); 2136 } 2137 2138 var shippingContainer = document.getElementById('shipping-methods'); 2139 if (shippingContainer) { 2140 var observer = new MutationObserver(function() { 2141 var form = document.getElementById('zs-shipping-methods-form'); 2142 if (form && form.querySelectorAll('.theme-checkout-list').length > 1) { 2143 sortWithRetry(); 2144 } 2145 }); 2146 observer.observe(shippingContainer, { childList: true, subtree: true, characterData: true }); 2147 } 2148 2149 var shippingSection = document.getElementById('zs-checkout-shipping-method'); 2150 if (shippingSection) { 2151 var classObserver = new MutationObserver(function() { 2152 if (!shippingSection.classList.contains('theme-hide')) { 2153 sortWithRetry(); 2154 } 2155 }); 2156 classObserver.observe(shippingSection, { attributes: true, attributeFilter: ['class'] }); 2157 } 2158 2159 // Shared XHR interceptor (see top of header code): re-sort the shipping 2160 // methods after each checkout "address" XHR completes. 2161 if (window.__cxAddr) { 2162 window.__cxAddr.subscribe(function () { sortWithRetry(); }); 2163 } 2164})(); 2165</script>
2165<script> 2166 2167/* ============================================================ 2168 Devin Hales | Freight + FedEx fixed-rate checkout display 2169 Added ~April 2026; freight migrated to fixed-rate tiers May 2026. 2170 2171 Heavy carts (>= 150 lb or cf_force_freight) and light/parcel 2172 carts both display on the curated "fixed N" Zoho shipping-rate 2173 ladder. The PHP backend quotes the real carrier rate; this 2174 script rounds it UP to the next fixed tier and shows that one 2175 fixed-method row, relabeled with the carrier/service name and 2176 an ETA. All "fixed N" rows are hidden by default. 2177 2178 Flow: 2179 1. Hide all "fixed N" rows + Zoho-native FedEx/UPS rows on load. 2180 2. Capture shipping zip + cart items from the checkout XHR 2181 (window.__cxAddr) or the Cart API. 2182 3. POST the cart to stockdata/fedex_quote.php and stockdata/ 2183 ups_quote.php in parallel. If FedEx returns eligible:false 2184 (cart >= 150 lb) chain to stockdata/freight_quote.php for an 2185 R&L / Old Dominion LTL quote. 2186 4. Round each returned rate up to a fixed tier; un-hide and 2187 relabel the matching "fixed N" row(s). FedEx may show 2188 several services; freight shows exactly one (the cheaper 2189 carrier). 2190 5. A MutationObserver re-applies the hide/show + labels after 2191 Zoho re-renders the shipping section. Bounded at 3 min. 2192 6. Re-quotes on cart / address change (resetForRequote). 2193 2194 The dynamic freight "slot pool" this script used to drive was 2195 retired in May 2026 â see handoffs/freight_fixed_rate.md. The 2196 fixed-rate display + ladder are documented in 2197 handoffs/fedex_fixed_rate_display.md. 2198 2199 Place AFTER the shipping sort script. 2200 ============================================================ */ 2201(function() { 2202 'use strict'; 2203 2204 const CONFIG = { 2205 // [Freight] logs are off in production. Enable with ?freightDebug=1 2206 // in the URL or localStorage.setItem('freightDebug','1') + reload. 2207 debug: (() => { try { return /[?&]freightDebug=1\b/.test(location.search) || localStorage.getItem('freightDebug') === '1'; } catch (e) { return false; } })(), 2208 }; 2209 2210 // Backend endpoints. Host comes from window.STOCKDATA_BASE (set by the 2211 // stock script) with a hardcoded fallback, so it lives in one place. 2212 const STOCKDATA_BASE = window.STOCKDATA_BASE || 'https://htm.tyconsystems.com/stockdata/'; 2213 // Light-cart orchestrator (carrier_quote.php) -- one server-side fanout 2214 // that hits FedEx + UPS + USPS in parallel, applies the allowlist + 2215 // tier-rounding + single-bump collision rule, and returns one merged 2216 // services list. Replaces the three parallel XHRs the client used to 2217 // fire. The per-carrier endpoints remain reachable for isolated 2218 // debugging. 2219 const CARRIER_QUOTE_ENDPOINT = STOCKDATA_BASE + 'carrier_quote.php'; 2220 const FREIGHT_QUOTE_ENDPOINT = STOCKDATA_BASE + 'freight_quote.php'; 2221 2222 // ââ State ââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2223 2224 let capturedCartItems = null; 2225 let capturedShippingZip = null; 2226 // Destination state/city captured alongside the zip â passed to 2227 // ups_quote.php, which needs them for the ZIPs UPS won't resolve 2228 // from the zip alone. Empty until the checkout address is captured. 2229 let capturedShippingState = ''; 2230 let capturedShippingCity = ''; 2231 // Destination country (ISO-2: 'US' / 'CA' / 'BE'), captured from the 2232 // /checkout/address XHR body or the rendered address block. Sent to 2233 // carrier_quote.php + freight_quote.php as a postal_parse() hint. 2234 // Empty until captured â the server then falls back to inferring the 2235 // country from the postal-code format (US/CA only), same as before. 2236 let capturedShippingCountry = ''; 2237 2238 // Captured-address persistence across checkout reloads (sessionStorage, 2239 // per-tab). A reload re-runs fireInitialQuote, whose DOM scrape only 2240 // knows the US/CA/BE postal shapes â an international customer's 2241 // postal (e.g. the Polish "02-486") would be lost and the round 2242 // downgraded to country-only (observed live 2026-07-14: a Warsaw 2243 // customer's 8 services dropped to 5 on reload). Storing the last 2244 // captured address lets the reload path restore the exact postal.
2245 // All access is wrapped â sessionStorage can throw in private modes. 2246 const SHIP_ADDR_STORE_KEY = 'fx-ship-addr'; 2247 function saveCapturedAddress() { 2248 try { 2249 sessionStorage.setItem(SHIP_ADDR_STORE_KEY, JSON.stringify({ 2250 zip: capturedShippingZip || '', 2251 country: capturedShippingCountry || '', 2252 city: capturedShippingCity || '', 2253 state: capturedShippingState || '', 2254 })); 2255 } catch (e) { /* private mode / quota â persistence is best-effort */ } 2256 } 2257 // Return the stored postal only when it plausibly belongs to the 2258 // address on screen: the stored country must match the country parsed 2259 // from the current page (an address switched to a different country 2260 // must never reuse the old postal). Returns '' when absent/mismatched. 2261 function storedZipForCountry(country) { 2262 try { 2263 const raw = sessionStorage.getItem(SHIP_ADDR_STORE_KEY); 2264 if (!raw) return ''; 2265 const a = JSON.parse(raw); 2266 if (a && a.zip && a.country && country && a.country === country) { 2267 return String(a.zip); 2268 } 2269 } catch (e) { } 2270 return ''; 2271 } 2272 // The full stored address (or null) -- used by fireInitialQuote's 2273 // last-resort restore when a mid-checkout reload yields a page the 2274 // scrape can't parse AT ALL (no zip, no country). Only this tab can 2275 // have written the store, so trusting it wholesale is safe there; 2276 // when the page DOES yield a country, storedZipForCountry's 2277 // country-match guard is the right gate instead. 2278 function storedCapturedAddress() { 2279 try { 2280 const raw = sessionStorage.getItem(SHIP_ADDR_STORE_KEY); 2281 if (!raw) return null; 2282 const a = JSON.parse(raw); 2283 if (a && (a.zip || a.country)) return a; 2284 } catch (e) { } 2285 return null; 2286 } 2287 2288 // AbortControllers for in-flight quote fetches so resetForRequote() 2289 // can cancel them when the cart/address changes mid-quote. Without 2290 // this, a stale in-flight response would race against the fresh 2291 // quote and sometimes win, depending on network timing. 2292 let currentCarrierAbortCtrl = null; 2293 let currentFreightAbortCtrl = null; 2294 // Last (zip|cart-signature) a quote was fired for. Dedupes â we 2295 // trigger from both the XHR interceptor (address changes) and the 2296 // cart-change handler, but skip if the captured state hasn't changed 2297 // since the last fire. Cleared by resetForRequote() so an explicit 2298 // reset always forces a fresh fire. 2299 let lastFedexQuoteKey = null; 2300 2301 function log(...args) { 2302 if (CONFIG.debug) console.log('[Freight]', ...args); 2303 } 2304 2305 /* 2306 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2307 FedEx Fixed-Rate Display 2308 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2309 Author: Devin Hales (May 2026). 2310 2311 Modified by Devin Hales (May 2026): 2312 - Added UPS as a second light-cart carrier. ups_quote.php is 2313 quoted in parallel with fedex_quote.php and its services are 2314 merged onto the same fixed-rate ladder. Native Zoho FedEx AND 2315 UPS rows are hidden â the ladder is the only carrier surface. 2316 - UPS-API-failure fallback: if the UPS quote times out or errors, 2317 the native Zoho UPS rows are un-hidden so the customer still 2318 has UPS rates (showNativeUpsFallback). 2319 - Moved the 150-entry fixed-rate tier ladder + the rate->tier 2320 rounding out to stockdata/fixed_rate_ladder.php. Quote responses 2321 now carry tier_id / tier_name / tier_rate per service plus the 2322 fixed_tier_ids list (fixedTierIds); a tier change no longer 2323 needs a header.txt re-paste. 2324 - Fixed-rate hide is now ONE static CSS rule 2325 (.theme-checkout-list:not([data-fx-show])) injected at init, so 2326 a fixed row is hidden the instant Zoho renders it. A row is 2327 shown only once setRowHiddenAndDisabled() adds data-fx-show. 2328 This fixes a flash of all 150 tiers on load (the prior CSS hid 2329 rows by tier ID, but the ID list was empty until the first 2330 quote returned). 2331 - Fire an initial quote on /checkout mount (fireInitialQuote). 2332 When a customer returns to /checkout with a saved address 2333 (e.g. after editing the cart on /cart and navigating back), 2334 Zoho doesn't fire the /checkout/address XHR -- so the XHR
2335 interceptor doesn't get a cart-snapshot and our quote never 2336 runs, leaving the previous (now-stale) tier visible. The new 2337 path parses zip + city/state from the rendered address block 2338 in the DOM and pulls the current cart from /storefront/api/v1 2339 /cart, then drives the normal merge flow. The dedupe in 2340 maybeFireFedExTestQuote keeps this from racing the XHR 2341 interceptor when both happen to fire. 2342 - Stop hiding USPS (and any other non-managed carrier) rows. The 2343 static CSS rule (theme-checkout-list:not([data-fx-show])) 2344 sweeps every shipping-method row, not just the ones we own. 2345 New markPassthroughRows() helper tags rows that aren't a 2346 fixed-N tier and aren't native FedEx/UPS with data-fx-show 2347 so the CSS rule leaves them alone. Called at init and from 2348 the MutationObserver reapply, so Zoho re-renders don't 2349 re-hide passthrough rows. USPS / Easyship / any manually- 2350 configured Zoho method now displays normally alongside the 2351 fixed-tier ladder. 2352 - Added USPS as a third light-cart carrier. usps_quote.php is 2353 quoted alongside fedex_quote.php and ups_quote.php; its services 2354 merge onto the same fixed-rate ladder. Native Zoho USPS rows are 2355 now hidden (isNativeCarrierRow regex extended). usps_quote.php 2356 branches on destination country: 2357 - US -> USPS Ground Advantage + USPS Priority Mail (domestic 2358 base-rates-list/search, COMMERCIAL priceType, <= 70 lb/box). 2359 - Non-US -> USPS First-Class Package International (<= 4 lb/box) 2360 + USPS Priority Mail International (international 2361 base-rates-list/search, no priceType param, <= 70 lb/box). 2362 - Server-side carrier-quote orchestrator. carrier_quote.php 2363 replaces the prior three parallel client-side XHRs (FedEx + UPS 2364 + USPS). The orchestrator fans out via curl_multi, applies the 2365 allowlist + tier-rounding + single-bump collision rule, and 2366 returns one merged services list. PR #78's single-bump rule: 2367 when two services round to the same tier, the cheaper claims 2368 T-1 if free; otherwise the more expensive bumps to T+1; else 2369 the loser drops. The client now hits one endpoint 2370 (CARRIER_QUOTE_ENDPOINT) instead of three; per-round merge 2371 state (carrierQuote / applyMergedCarrierTiers / 2372 betterFedExService / fxParseDeliveryTime) is gone. The 2373 per-carrier endpoints stay reachable for isolated debugging. 2374 Native-UPS fallback (showNativeUpsFallback / setUpsFallback) 2375 retired -- with three light carriers running in parallel, a 2376 single-carrier failure no longer leaves the customer without 2377 UPS options (the surviving carriers still render). 2378 2379 Modified by Devin Hales (July 2026): 2380 - International destinations beyond Canada, starting with Belgium. 2381 The destination country is now captured explicitly (from the 2382 /checkout/address XHR request body's shipping_address.country, 2383 falling back to the country line of the rendered address block) 2384 instead of being inferred server-side from the postal-code 2385 format alone -- a Belgian postal code is 4 digits, which format 2386 inference can't distinguish from other countries' codes (and a 2387 future Germany would collide with US 5-digit ZIPs outright). 2388 capturedShippingCountry is sent as &country=XX to 2389 carrier_quote.php and freight_quote.php, which forward it to 2390 postal_parse() as a hint. Postal extraction accepts the Belgian 2391 4-digit format only when the captured country is BE, so US/CA 2392 parsing is unchanged. Freight (R&L / Old Dominion) is North- 2393 America-only: freight_quote.php now answers non-US/CA 2394 destinations with unsupported_country:true and the client shows 2395 the manual-quote contact notice instead of an empty methods 2396 list. 2397 - Phase 0 of the worldwide rollout: COUNTRY_CODE_BY_NAME now 2398 carries the 145-country roster (Zoho's exact names; US 2399 territories keep their own ISO codes; eleven sanctioned/
2400 suspended-service/unserviceable entries deliberately excluded 2401 -- see the map). The /checkout/address XHR request body's 2402 shipping_address is parsed for the POSTAL too 2403 (parseShippingAddressFromXHRBody) -- structured and 2404 format-agnostic, so every country's postal captures without an 2405 HTML-scrape regex; the DOM scrape stays as the saved-address 2406 fallback (US/CA/BE shapes only). International destinations may 2407 quote COUNTRY-ONLY when no postal was captured (no-postal 2408 countries like Hong Kong / UAE / Qatar, or a saved intl address 2409 the scrape can't shape-match) -- USPS international rates by 2410 country, so a partial quote beats none. A zero-service round to 2411 any non-US/CA destination now shows the manual-quote contact 2412 notice ('intl' wording) instead of an empty methods list. 2413 Server side: postal_parse() passes through any hinted country's 2414 code (empty allowed); carrier legs omit empty postals from their 2415 API payloads; usps_quote.php rides the domestic endpoint for 2416 PR/GU/VI/AS/MP/UM. US/CA behavior is unchanged throughout. 2417 - Captured-address persistence (sessionStorage, per-tab). A checkout 2418 reload re-runs fireInitialQuote, whose DOM scrape only knows the 2419 US/CA/BE postal shapes -- an international postal was lost on 2420 reload and the round downgraded to country-only (observed live 2421 2026-07-14, Warsaw customer: 8 services -> 5). The last captured 2422 address is stored on every capture; the reload path restores the 2423 postal when the stored country matches the one rendered on 2424 screen. carrier_quote.php also now skips the FedEx leg on 2425 country-only rounds (FedEx's Rate API 400s without a postal). 2426 - Relabel the Review-step "Shipping charge" row. The Review 2427 Order & Make Payment table lists the shipping cost as a 2428 pseudo cart item labeled with the RAW Zoho method name 2429 ("fixed 15 (TBD)") -- patchFixedRowLabel only reaches the 2430 Dispatch-step method rows, so the internal tier name leaked on 2431 the final screen before Make Payment (Scott's 2026-07-29 2432 report). The row and its amount STAY visible (feedback on the 2433 first pass, PR #145, which hid the whole row), and the raw 2434 element is REWRITTEN to the selected service's name ("USPS 2435 Ground Advantage") -- a plain strip left "Shipping charge" as 2436 a one-line block vertically misaligned with the amount, so 2437 keeping a second line restores Zoho's native row geometry and 2438 names the chosen service (second feedback round). The name 2439 resolves from the row's own raw tier text via 2440 visibleFixedRowData first -- after a reload NO radio is 2441 checked (the hide pass unchecks Zoho's restored selection and 2442 the persistence variable died with the page), so the checked 2443 input's dataset is only the fallback for mid-change moments. 2444 While the name is unresolvable (quote round not landed yet) 2445 the raw element is hidden instead. 2446 Non-fixed method names (e.g. a store-pickup option) are left 2447 alone. The pseudo-row shares .theme-cart-view-list with real 2448 product rows -- the discriminators are its "Shipping charge" 2449 label text and the absence of a /products/ link. Re-applied 2450 from the fixed-slot observer's reapply cycle AND a 2451 delegated-click-armed bounded poll (the review table renders 2452 only after user interactions, which can happen after the 2453 3-min observer window closes). 2454 - Mid-checkout refresh left the Shipping Methods section empty 2455 (2026-07-29, reproduced live + HAR-confirmed: zero stockdata 2456 calls after the reload). A reload on the Dispatch/Review step 2457 renders the address as a recap block fireInitialQuote's scrape 2458 can't shape-match AND fires no /checkout/address XHR -- so 2459 neither capture path ran, no quote fired, and every fixed row 2460 stayed hidden. fireInitialQuote's scrape-timeout branch now 2461 restores the address this tab captured before the reload 2462 (sessionStorage, written by both capture paths): guarded by 2463 the country-match rule when the page yielded a country, 2464 wholesale (zip + country + city/state) when it yielded 2465 nothing. Guest tabs are unchanged -- an empty store still 2466 skips exactly as before. 2467 - Gate the "NET 30 / Invoice" offline payment option per 2468 customer (applyNet30PaymentGate). Scott enabled it in Zoho 2469 admin -> Payments for approved customers only, but Zoho has no 2470 per-customer payment gating -- so it is hidden by default 2471 (Phase 1, PR #148) and un-hidden only when the logged-in 2472 customer's Zoho Books contact has Payment Terms = "Net 30" 2473 AND the current checkout total fits inside the contact's 2474 available credit (credit_limit - outstanding receivables + 2475 unused credits -- compared server-side; no credit amounts 2476 reach the browser). checkNet30Eligibility() reads the portal 2477 email (#zs-portal-email hidden input) and the grand total 2478 (#zs-payment-total-summary), POSTs {email, cart_total} to 2479 stockdata/net30_check.php, and honors the verdict only while 2480 it still matches the live email+total -- so a cart/shipping 2481 change hides the option until the re-check answers. Fail 2482 closed everywhere; verdict cached per-tab 30 min 2483 (sessionStorage), contact data 15 min server-side. Matched by 2484 offline-option input class + "NET 30" label text; hidden AND 2485 disabled/unchecked; re-applied by the same review-step 2486 machinery as the shipping-charge relabel. Cosmetic gate only 2487 -- the option stays enabled Zoho-side, so unapproved NET 30 2488 orders still need the normal manual review. 2489 - Mandatory PO Number for NET 30, written to the order's native 2490 Reference# field (syncNet30PoField / installNet30PoGuard). 2491 Selecting NET 30 injects a required "PO Number" input into that 2492 payment option (card customers never see it). The Complete 2493 Order / Make Payment button is DISABLED (greyed, with a 2494 tooltip) until the PO reaches NET30_MIN_PO_LEN (4) characters, 2495 and the click is additionally intercepted in the CAPTURE phase 2496 as a backstop. The disable only ever releases a disable we 2497 applied (data-fx-po-disabled) and re-derives from the terms 2498 checkbox on release, so a satisfied PO can never enable the 2499 button while terms are unaccepted. A valid PO is POSTed to 2500 stockdata/po_capture.php, and order_verify.php writes it onto 2501 the placed order's reference_number -- matched on the NET 30 2502 customer id + time window (the order webhook carries no 2503 cart/checkout id; ZIP + time window remains the fallback for 2504 rows without an id, but international postals often evade the 2505 DOM scrape -- a Canadian order shipped with an empty dest_zip 2506 and lost its Reference#, 2026-07-30). The typed value is 2507 cached in sessionStorage KEYED TO checkout_id so a Zoho 2508 re-render can't lose it while a later cart in the same tab 2509 still starts empty (an unkeyed cache auto-filled the previous 2510 order's PO into the next checkout -- found 2026-07-30). 2511 Client-side enforcement only: a NET 30 order that still 2512 arrives with an empty Reference# needs manual review. 2513 - "Ship on my account" coupon note (syncShipAccountShippingNote). 2514 The "shiponmyaccount" coupon (Zoho Promotions, type 2515 free_shipping / 100%) zeroes the shipping charge for whatever 2516 method is selected -- Zoho handles the money natively -- but 2517 it also DROPS the "Shipping charge" pseudo item from the 2518 Review table (fixed28.har, 2026-07-30), leaving the customer 2519 no cue about the arrangement. While that exact coupon is 2520 applied, a "Shipping charge" row is kept in the Review table 2521 whose amount reads "Please provide your carrier account in 2522 order notes" instead of a dollar figure: Zoho's own row is 2523 rewritten when it renders one (e.g. $0.00 after a method 2524 change), a lookalike row (data-fx-ship-account-note) is 2525 injected when Zoho dropped it. Removing the coupon re-renders 2526 the table from the applycoupon response, so the native amount 2527 returns and the sync only has to remove its own row. Coupon 2528 state is read from the order-summary coupon block, which the
2529 same response re-renders. Other coupons are untouched. Rides 2530 the observer reapply + click-armed poll like the relabel. 2531 Same-day formatting feedback: the plain note wrapped to three 2532 lines in the narrow amount column, so it now renders as a 2533 highlighted amber pill (#sf-notice palette) in the row's wide 2534 detail cell, and the amount cell is hidden while the coupon 2535 is applied (restored on removal). 2536 - NET 30 for secondary contact-person logins (customer-id 2537 capture). Scott's company account has three portal logins; 2538 only the primary (scottmparsons@) saw the NET 30 option. 2539 Cause: net30_check.php resolved the contact via 2540 contacts?email=, which only matches a contact's PRIMARY email 2541 -- dmarc@ matched no contact, sales@ matched an unrelated 2542 "sales sales" record (net30_check.log + live API probes, 2543 2026-07-30; search_text= is blind to contact persons too). 2544 Fix: checkout API responses carry customer.customer_id, which 2545 IS the Inventory/Books contact_id -- captured from the 2546 checkout/address XHRs (shared __cxAddr interceptor) plus a 2547 one-shot /storefront/api/v1/cart fetch for the saved-address 2548 reload path that fires no address XHR, persisted per-tab 2549 (sessionStorage), and POSTed with the eligibility check. 2550 net30_check.php then fetches that contact DIRECTLY, honoring 2551 the id only when the login email is on the contact (contact 2552 persons or contact email), else falling back to the email 2553 search. Verdict caches (client + server) key on the id, so a 2554 late id capture invalidates an email-only verdict. 2555 2556 See handoffs/fedex_fixed_rate_display.md for the full design 2557 and fixed_rates.txt for the tier ladder source-of-truth. 2558 2559 Purpose: 2560 - Map the multi-service response from fedex_quote.php to the 2561 curated "fixed N" Zoho shipping-methods ladder (150 tiers, 2562 $0â$19,000, across 3 Zoho zones). Each allowlisted FedEx 2563 service's rate is rounded UP to the next tier; that 2564 fixed-method row is un-hidden and relabeled with the FedEx 2565 service_name (e.g. "FedEx 2Day®"). 2566 - Hide all fixed-method rows by default. 2567 - Hide all Zoho-native FedEx AND UPS methods permanently â the 2568 fixed-method ladder is the only carrier surface customers see. 2569 2570 Defense-in-depth (hide-by-default is mandatory because a stray 2571 visible fixed method = customer pays the wrong rate): 2572 - CSS layer: display:none + pointer-events:none on the row. 2573 - JS layer: input.disabled=true, input.name='' (so the form 2574 submit drops the value even if CSS fails), input.checked= 2575 false, tabindex=-1. Both layers must fail before a customer 2576 can accidentally select a hidden fixed method. 2577 2578 Re-quote / cart-change handling: resetForRequote() clears all 2579 visible fixed rows AND captures the previously-selected 2580 service_name so it can be re-selected on the new tier (Q5). 2581 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2582 */ 2583 2584 // Fixed-rate tier ladder. The 150-entry ladder + the rate->tier 2585 // rounding now live server-side in stockdata/fixed_rate_ladder.php: 2586 // each carrier quote response carries the matched tier_id / tier_name 2587 // / tier_rate per service, plus the full fixed_tier_ids list. This Set 2588 // is populated from that list (ingestFixedTierIds) so applyFixedRateCSS 2589 // and the hide passes know every fixed-rate row. Before the first 2590 // quote it is empty -- hideFixedRateRowsByLabel still hides fixed rows 2591 // by their "fixed N" label, so there is no exposure window. 2592 let fixedTierIds = new Set(); 2593 // Most-recent quote round's hazmat surcharge (dollar amount baked into 2594 // every visible rate when the cart contained a hazmat item). Stashed 2595 // here so logFixedSelection() can record it alongside the customer's 2596 // selection -- enables invoice reconciliation in 2597 // shipping_rate_log_summary.php. Re-set to 0 on every quote round so 2598 // a non-hazmat cart's selection records 0. 2599 let lastHazmatSurcharge = 0; 2600 2601 function ingestFixedTierIds(data) { 2602 if (data && Array.isArray(data.fixed_tier_ids)) { 2603 data.fixed_tier_ids.forEach(function (id) { fixedTierIds.add(id); }); 2604 } 2605 // carrier_quote.php and freight_quote.php both surface the surcharge. 2606 if (data && typeof data.hazmat_surcharge === 'number') { 2607 lastHazmatSurcharge = data.hazmat_surcharge; 2608 } else { 2609 lastHazmatSurcharge = 0; 2610 } 2611 } 2612 2613 // Carrier service allowlists (FedEx service_types, UPS numeric codes, 2614 // USPS mailClasses) moved server-side to carrier_quote.php. The 2615 // orchestrator filters before applying tier-rounding + the single-bump 2616 // collision rule, so the merged services list this client receives 2617 // is already trimmed to the displayable set. See the $ALLOWLIST_FEDEX / 2618 // $ALLOWLIST_UPS / $ALLOWLIST_USPS constants in carrier_quote.php. 2619 2620 const FIXED_RATE_STYLE_ID = 'fedex-fixed-rate-styles'; 2621 2622 // Calendar days added to FedEx's `delivery_date` to widen the ETA 2623 // into a range. 0 = show the single quoted delivery date, no range. 2624 const DELIVERY_BUFFER_DAYS = 0; 2625 2626 // Customer-facing relabels for FedEx service types, overriding the 2627 // service_name FedEx returns. GROUND_HOME_DELIVERY (residential 2628 // ground) is shown the same as the commercial FEDEX_GROUND service 2629 // so the customer sees one consistent "FedEx Ground" label. 2630 const FEDEX_SERVICE_DISPLAY_NAMES = { 2631 GROUND_HOME_DELIVERY: 'FedEx Ground®', 2632 SMART_POST: 'FedEx Ground® Economy', 2633 FEDEX_GROUND_ECONOMY: 'FedEx Ground® Economy', 2634 }; 2635 function fedexDisplayName(serviceType, fallback) { 2636 return FEDEX_SERVICE_DISPLAY_NAMES[serviceType] || fallback || ''; 2637 } 2638 2639 // ââ State ââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2640 // visibleFixedSlotIds â Set of tier IDs that should be visible 2641 // right now. Empty = hide-all (the default and the safe state). 2642 let visibleFixedSlotIds = new Set(); 2643 // visibleFixedRowData â cache of {svc, tier} per visible tier ID 2644 // so the MutationObserver can re-patch labels if Zoho re-renders. 2645 let visibleFixedRowData = {}; 2646 // previouslySelectedFedexService â captured on cart-change so the 2647 // post-re-quote applyFedExFixedTier can re-select the same service 2648 // on its new tier (Q5 â selection persistence by service name). 2649 let previouslySelectedFedexService = null; 2650 // freightModeActive â true when the cart routed to freight (a 2651 // cf_force_freight item or >= 150 lb). Only the freight carrier 2652 // row(s) are then shown â the FedEx / UPS / USPS / native rows 2653 // can't move a freight item. false = normal mode (light carts show 2654 // the merged fixed-tier rows from carrier_quote.php). 2655 let freightModeActive = false;
2656 // (Why native Zoho UPS rows stay hidden unconditionally now: see 2657 // hideAllNativeCarrierMethods().) 2658 // MutationObserver + safety timer for newly-rendered rows. Bounded 2659 // at 3 minutes per CLAUDE.md (no unbounded polling). 2660 let currentFixedRateObserver = null; 2661 let currentFixedRateSafetyTimer = null; 2662 // Debounce timer for the observer's reapply() â coalesces a burst 2663 // of DOM mutations into one pass (PR #47 self-review). 2664 let currentFixedRateReapplyTimer = null; 2665 2666 function fxLog(...args) { 2667 if (CONFIG.debug) console.log('[FedEx Fixed]', ...args); 2668 } 2669 2670 // Rate -> tier rounding moved server-side to fixed_rate_ladder.php. 2671 // Each carrier quote response now carries the matched tier_id / 2672 // tier_name / tier_rate per service (tier_id null = above top tier). 2673 2674 // ââ DOM row lookup ââââââââââââââââââââââââââââââââââââââââââââ 2675 function getRowByShippingChargeId(id) { 2676 const input = document.querySelector( 2677 `#zs-shipping-methods-form input.shipping-method[value="${id}"]` 2678 ); 2679 if (!input) return null; 2680 return input.closest('.theme-checkout-list') 2681 || input.closest('label') 2682 || input.parentElement; 2683 } 2684 2685 // Identify a Zoho-native FedEx or UPS method row by visible label 2686 // content. The curated fixed-rate ladder is the only carrier surface 2687 // the customer sees â both carriers are now quoted via the API 2688 // (fedex_quote.php / ups_quote.php) and rounded onto the ladder. 2689 // hideAllNativeCarrierMethods() hides the native rows this matches. 2690 // Excludes fixed-method rows (by ID or by our data-fx-fixed marker). 2691 // Label-pattern approach; can be upgraded to an ID allowlist if 2692 // regressions surface. 2693 function isNativeCarrierRow(row) { 2694 const input = row.querySelector('input.shipping-method'); 2695 if (!input) return false; 2696 if (input.dataset.fxFixed === '1') return false; 2697 if (fixedTierIds.has(input.value)) return false; 2698 const labelText = (row.textContent || '').trim(); 2699 // Match "FedEx", "USPS", or "UPS" as a whole word at the start 2700 // of the label. USPS is listed before UPS so a "USPS Ground 2701 // Advantage" label matches USPS, not the UPS-prefix-then-fail- 2702 // word-boundary path. Excludes anything that starts with "fixed". 2703 return /^(?:FedEx|USPS|UPS)\b/i.test(labelText) && !/^fixed\b/i.test(labelText); 2704 } 2705 2706 // Defense-in-depth row hide/show. CSS-layer hide is also applied 2707 // via applyFixedRateCSS(); this is the JS-layer fallback (and the 2708 // only mechanism for native-FedEx rows, since their IDs aren't 2709 // known statically). 2710 function setRowHiddenAndDisabled(row, hidden) { 2711 if (!row) return; 2712 const input = row.querySelector('input.shipping-method'); 2713 if (hidden) { 2714 // data-fx-show drives the CSS-layer hide (see applyFixedRateCSS): 2715 // a row without it is hidden by the static stylesheet rule. 2716 row.removeAttribute('data-fx-show'); 2717 row.style.setProperty('display', 'none', 'important'); 2718 row.style.setProperty('pointer-events', 'none', 'important'); 2719 row.setAttribute('aria-hidden', 'true'); 2720 if (input) { 2721 if (input.dataset.fxOriginalName == null) { 2722 input.dataset.fxOriginalName = input.name || ''; 2723 } 2724 input.disabled = true; 2725 input.name = ''; 2726 input.checked = false; 2727 input.setAttribute('tabindex', '-1'); 2728 } 2729 } else { 2730 // Mark the row approved-to-show for the CSS-layer rule. 2731 row.setAttribute('data-fx-show', '1'); 2732 row.style.removeProperty('display'); 2733 row.style.removeProperty('pointer-events'); 2734 row.removeAttribute('aria-hidden'); 2735 if (input) { 2736 input.disabled = false; 2737 if (input.dataset.fxOriginalName != null) { 2738 input.name = input.dataset.fxOriginalName; 2739 } 2740 input.removeAttribute('tabindex'); 2741 } 2742 } 2743 } 2744 2745 // ââ CSS injection (separate <style> from freight-pool) âââââââ 2746 function injectFixedRateCSS(cssText) { 2747 let el = document.getElementById(FIXED_RATE_STYLE_ID); 2748 if (!el) { 2749 el = document.createElement('style'); 2750 el.id = FIXED_RATE_STYLE_ID; 2751 document.head.appendChild(el); 2752 } 2753 el.textContent = cssText; 2754 } 2755 2756 // Inject the fixed-rate hide CSS. ONE static rule: every shipping 2757 // row is hidden UNLESS it carries the data-fx-show attribute, which 2758 // setRowHiddenAndDisabled() adds to a row only when our code has 2759 // approved it (a matched carrier tier, the freight tier, or a 2760 // native-UPS fallback row). 2761 // 2762 // This is a single declarative rule injected once (in init, before 2763 // Zoho renders the shipping form), so a fixed-rate row is hidden the 2764 // instant it renders â no flash of all-150-tiers before the JS 2765 // catches up. A shown row carries the attribute but no `display` 2766 // override, so Zoho's own `.theme-checkout-list { display: flex }` 2767 // gives it the normal side-by-side layout. 2768 // 2769 // Idempotent â safe to call repeatedly; callers invoke it on every 2770 // state change as belt-and-braces. 2771 function applyFixedRateCSS() { 2772 injectFixedRateCSS( 2773 '#zs-shipping-methods-form .theme-checkout-list:not([data-fx-show])' 2774 + ' { display: none !important; }' 2775 ); 2776 } 2777 2778 // ââ Default-hide functions âââââââââââââââââââââââââââââââââââ 2779 function hideAllFixedSlots() { 2780 visibleFixedSlotIds = new Set(); 2781 visibleFixedRowData = {}; 2782 applyFixedRateCSS(); 2783 // JS layer â disable any already-rendered fixed-method rows 2784 // so a CSS failure still prevents selection. 2785 fixedTierIds.forEach(id => {
2786 const row = getRowByShippingChargeId(id); 2787 if (row) setRowHiddenAndDisabled(row, true); 2788 }); 2789 } 2790 2791 // Hide native Zoho FedEx / UPS / USPS rows. The orchestrator's merged 2792 // fixed-rate ladder is the only carrier surface customers see. Prior 2793 // versions had a native-UPS fallback (showNativeUpsFallback) for the 2794 // case when ups_quote.php specifically failed; that was retired with 2795 // the orchestrator -- carrier_quote.php quotes three carriers in 2796 // parallel, so a single-carrier failure no longer leaves the customer 2797 // without any UPS option (the surviving carriers still render). 2798 function hideAllNativeCarrierMethods() { 2799 document.querySelectorAll( 2800 '#zs-shipping-methods-form .theme-checkout-list' 2801 ).forEach(row => { 2802 if (!isNativeCarrierRow(row)) return; 2803 setRowHiddenAndDisabled(row, true); 2804 }); 2805 } 2806 2807 // Label-based fixed-rate detection â the ID-agnostic safety net. 2808 // The ID-based hide (applyFixedRateCSS / hideAllFixedSlots) only 2809 // knows the tier IDs the carrier quote responses have reported in 2810 // fixed_tier_ids; before the first quote it knows none. Matching on 2811 // the "fixed N" label instead catches every fixed-rate method 2812 // regardless of its ID â so fixed rows are hidden even before a 2813 // quote returns. Case-insensitive â the live store has both 2814 // "fixed 500" and "Fixed 450". (2026-05-16.) 2815 function isFixedRateRow(row) { 2816 const input = row.querySelector('input.shipping-method'); 2817 if (!input) return false; 2818 // A row our FedEx display patched/showed on purpose â leave it. 2819 if (input.dataset.fxFixed === '1') return false; 2820 if (visibleFixedSlotIds.has(input.value)) return false; 2821 const labelText = (row.textContent || '').trim(); 2822 // "fixed 10", "Fixed 450", "fixed 1200" â the word "fixed" 2823 // followed by a number at the very start of the label. 2824 return /^fixed\s+\d/i.test(labelText); 2825 } 2826 2827 // Hide every fixed-rate row by label â the ID-agnostic safety net 2828 // that catches fixed methods regardless of their shippingcharge_id. 2829 // JS-only: CSS cannot match on text content. 2830 function hideFixedRateRowsByLabel() { 2831 document.querySelectorAll( 2832 '#zs-shipping-methods-form .theme-checkout-list' 2833 ).forEach(row => { 2834 if (isFixedRateRow(row)) { 2835 setRowHiddenAndDisabled(row, true); 2836 } 2837 }); 2838 } 2839 2840 // Tag every "passthrough" shipping-method row with data-fx-show so 2841 // the static CSS hide-rule (applyFixedRateCSS) doesn't sweep it up 2842 // alongside the fixed tiers. A passthrough row is one our display 2843 // logic explicitly DOESN'T own: 2844 // - NOT a fixed-N tier row (by ID OR by "fixed N" label) 2845 // - NOT a native FedEx/UPS row (those are managed by 2846 // hideAllNativeCarrierMethods) 2847 // Everything else -- USPS, Easyship, manually-configured Zoho 2848 // methods, etc. -- is a passthrough: visible by default, untouched 2849 // by us. Without this tag the broad CSS rule 2850 // ".theme-checkout-list:not([data-fx-show]) { display:none }" 2851 // would hide them too, which is the regression this function 2852 // prevents. Called at init AND from the MutationObserver reapply 2853 // (Zoho re-renders the form on address-change XHRs, so the tag 2854 // needs to be re-applied to fresh DOM nodes). 2855 function markPassthroughRows() { 2856 const form = document.getElementById('zs-shipping-methods-form'); 2857 if (!form) return; 2858 form.querySelectorAll('.theme-checkout-list').forEach(row => { 2859 const input = row.querySelector('input.shipping-method'); 2860 if (!input) return; 2861 // Fixed-rate tier â our merge logic decides when to show it. 2862 if (fixedTierIds.has(input.value)) return; 2863 if (isFixedRateRow(row)) return; 2864 // Native FedEx / UPS â hideAllNativeCarrierMethods owns these. 2865 if (isNativeCarrierRow(row)) return; 2866 // Everything else is a passthrough; let it render. 2867 if (!row.hasAttribute('data-fx-show')) { 2868 row.setAttribute('data-fx-show', '1'); 2869 } 2870 }); 2871 } 2872 2873 // ââ Mutation observer â re-apply hide-by-default rules âââââââ 2874 // Catches Zoho's deferred rendering of the shipping-methods list 2875 // and any later re-renders. Bounded at 3 minutes per CLAUDE.md. 2876 function watchForFixedSlots() { 2877 if (currentFixedRateObserver) { 2878 try { currentFixedRateObserver.disconnect(); } catch (e) {} 2879 currentFixedRateObserver = null; 2880 } 2881 if (currentFixedRateSafetyTimer) { 2882 clearTimeout(currentFixedRateSafetyTimer); 2883 currentFixedRateSafetyTimer = null; 2884 } 2885 if (currentFixedRateReapplyTimer) { 2886 clearTimeout(currentFixedRateReapplyTimer); 2887 currentFixedRateReapplyTimer = null; 2888 } 2889 2890 const reapply = () => { 2891 fixedTierIds.forEach(id => {
2892 const row = getRowByShippingChargeId(id); 2893 if (!row) return; 2894 if (visibleFixedSlotIds.has(id)) { 2895 // Should be visible â ensure not disabled + re-patch 2896 // label in case Zoho re-rendered the row. 2897 const input = row.querySelector('input.shipping-method'); 2898 const data = visibleFixedRowData[id]; 2899 if (input && (input.disabled || input.dataset.fxFixed !== '1')) { 2900 setRowHiddenAndDisabled(row, false); 2901 if (data) patchFixedRowLabel(row, data.svc, data.tier); 2902 } 2903 } else { 2904 // Should be hidden â ensure disabled. 2905 setRowHiddenAndDisabled(row, true); 2906 } 2907 }); 2908 hideAllNativeCarrierMethods(); 2909 hideFixedRateRowsByLabel(); 2910 markPassthroughRows(); 2911 relabelReviewShippingChargeRows(); 2912 syncShipAccountShippingNote(); 2913 applyNet30PaymentGate(); 2914 syncNet30PoField(); 2915 syncCompleteOrderButton(); 2916 }; 2917 2918 reapply(); 2919 2920 // Debounced trigger. The observer watches document.body's whole 2921 // subtree (needed to catch the shipping form first appearing), 2922 // and Zoho's checkout mutates the DOM frequently. Running 2923 // reapply() â ~40 querySelectors + a querySelectorAll â on every 2924 // mutation batch is wasteful, so coalesce a burst into a single 2925 // pass ~150 ms later. (PR #47 self-review â Important #1.) 2926 const scheduleReapply = () => { 2927 if (currentFixedRateReapplyTimer) return; 2928 currentFixedRateReapplyTimer = setTimeout(() => { 2929 currentFixedRateReapplyTimer = null; 2930 reapply(); 2931 }, 150); 2932 }; 2933 2934 const observer = new MutationObserver(scheduleReapply); 2935 observer.observe(document.body, { childList: true, subtree: true }); 2936 currentFixedRateObserver = observer; 2937 2938 currentFixedRateSafetyTimer = setTimeout(() => { 2939 try { observer.disconnect(); } catch (e) {} 2940 if (currentFixedRateObserver === observer) { 2941 currentFixedRateObserver = null; 2942 } 2943 currentFixedRateSafetyTimer = null; 2944 if (currentFixedRateReapplyTimer) { 2945 clearTimeout(currentFixedRateReapplyTimer); 2946 currentFixedRateReapplyTimer = null; 2947 } 2948 }, 180000); 2949 } 2950 2951 // ââ Patch a fixed-method row to show FedEx service info ââââââ 2952 // - Replaces the "fixed N" label text with the FedEx service_name. 2953 // - Sets data-fx-fixed=1, data-fx-fedex-service=<service_name>, 2954 // data-fx-fedex-service-type=<service_type> on the input so 2955 // re-quote selection persistence can look it back up. 2956 // - Replaces Zoho's existing ETA span (rendered from the method's 2957 // admin-configured ETA, typically "TBD" for the fixed-rate 2958 // ladder) with a formatted estimated-delivery range derived 2959 // from svc.delivery_date + DELIVERY_BUFFER_DAYS. 2960 // Rate column is NOT touched â Zoho already renders it at the 2961 // tier rate. (Q4.) 2962 function patchFixedRowLabel(row, svc, tier) { 2963 const input = row.querySelector('input.shipping-method'); 2964 if (input) { 2965 input.dataset.fxFixed = '1'; 2966 input.dataset.fxFedexService = svc.service_name || ''; 2967 input.dataset.fxFedexServiceType = svc.service_type || ''; 2968 } 2969 const labelNode = row.querySelector('label') || row; 2970 // Find the label text node and replace it. Must be IDEMPOTENT -- 2971 // applyFedExFixedTier fires once per carrier-quote-settle, so the 2972 // same row can be patched 2-3 times in a single quote round as 2973 // FedEx, UPS, and USPS land in arbitrary order; if a slower carrier 2974 // wins the tie-break later, this needs to overwrite the earlier 2975 // carrier's label. Match either the original "fixed N" template 2976 // (first patch) OR the previously-patched label (subsequent 2977 // patches), tracked via row.dataset.fxPatchedLabel. 2978 // (Bug fix: without the previous-label match, an early carrier's 2979 // name stuck on the row for the rest of the round even though 2980 // matchedRowsByTier had been updated to the tie-break winner. 2981 // The ETA span uses textContent= and always updates, which is how 2982 // we caught the desync: UPS Standard label paired with FedEx 2983 // Ground's delivery date.) 2984 const newLabel = ' ' + (svc.service_name || tier.name) + ' '; 2985 const prevPatched = row.dataset.fxPatchedLabel || ''; 2986 const walker = document.createTreeWalker(labelNode, NodeFilter.SHOW_TEXT); 2987 let node; 2988 let labelMatched = false;
2989 while ((node = walker.nextNode())) { 2990 const text = node.nodeValue || ''; 2991 if (/^\s*fixed\s+\d+\s*$/i.test(text) 2992 || (prevPatched && text === prevPatched)) { 2993 node.nodeValue = newLabel; 2994 labelMatched = true; 2995 break; 2996 } 2997 } 2998 if (labelMatched) { 2999 row.dataset.fxPatchedLabel = newLabel; 3000 } 3001 // ETA â patch Zoho's existing <span class="theme-text-color-light"> 3002 // inside the rate <p>. That span renders the method's admin- 3003 // configured ETA (typically "TBD" for the fixed-rate ladder). 3004 // Replacing it gives the customer a real arrival window without 3005 // adding a separate sibling span. 3006 const ratePara = row.querySelector('.theme-checkout-list-content p'); 3007 let etaSpan = ratePara 3008 ? ratePara.querySelector('span.theme-text-color-light') 3009 : null; 3010 if (!etaSpan && ratePara) { 3011 // Defensive: if Zoho didn't render the ETA span (e.g. the 3012 // method's admin ETA field was left blank), create one so 3013 // we still have a place to put the delivery range. 3014 etaSpan = document.createElement('span'); 3015 etaSpan.className = 'theme-text-color-light'; 3016 ratePara.appendChild(document.createTextNode(' ')); 3017 ratePara.appendChild(etaSpan); 3018 } 3019 if (etaSpan) { 3020 const delivery = formatEstimatedDelivery( 3021 svc.delivery_date, DELIVERY_BUFFER_DAYS); 3022 if (delivery) { 3023 etaSpan.textContent = delivery; 3024 } else if (svc.transit_days === 1) { 3025 etaSpan.textContent = 'Overnight'; 3026 } else if (typeof svc.transit_days === 'number' 3027 && svc.transit_days > 1) { 3028 etaSpan.textContent = '~' + svc.transit_days + ' business days'; 3029 } else if (svc.transit_label) { 3030 etaSpan.textContent = svc.transit_label; 3031 } 3032 // else: leave whatever Zoho rendered (e.g. "TBD") â better 3033 // than blanking it out when we have nothing to put there. 3034 } 3035 // Clean up any sibling .fx-transit span left behind by a prior 3036 // header.txt revision (this code used to APPEND a transit 3037 // hint rather than patch the existing ETA span). Idempotent. 3038 const oldTransitSpan = row.querySelector('.fx-transit'); 3039 if (oldTransitSpan) oldTransitSpan.remove(); 3040 } 3041 3042 // Parse a FedEx-style ISO-ish delivery_date ("2026-05-21T23:59:00", 3043 // sometimes with timezone, sometimes without) and return a short 3044 // arrival window string parenthesized to match Zoho's existing 3045 // ETA-span style (the native UPS rows render e.g. "(1 day)"): 3046 // "(May 21-23)" (same month) 3047 // "(May 30 - Jun 1)" (crosses month) 3048 // "(May 21)" (bufferDays === 0) 3049 // Returns null on parse failure or invalid input â caller falls 3050 // back to transit_days / transit_label / leaves Zoho's TBD intact. 3051 // 3052 // Implementation note: parses only the date component (Y-M-D), 3053 // ignoring time and timezone. FedEx's "T23:59:00" suffix is a 3054 // sentinel for "any time on this day" and doesn't reflect a real 3055 // delivery time. Calendar days, not business days â see 3056 // DELIVERY_BUFFER_DAYS comment. 3057 function formatEstimatedDelivery(deliveryDateIso, bufferDays) { 3058 if (!deliveryDateIso || typeof deliveryDateIso !== 'string') { 3059 return null; 3060 } 3061 const m = deliveryDateIso.match(/^(\d{4})-(\d{2})-(\d{2})/); 3062 if (!m) return null; 3063 const year = parseInt(m[1], 10);
3064 const month = parseInt(m[2], 10) - 1; // JS Date is 0-indexed 3065 const day = parseInt(m[3], 10); 3066 if (typeof bufferDays !== 'number' || !Number.isFinite(bufferDays) 3067 || bufferDays < 0) { 3068 bufferDays = 2; 3069 } 3070 const startDate = new Date(year, month, day); 3071 if (isNaN(startDate.getTime())) return null; 3072 const endDate = new Date(year, month, day + bufferDays); 3073 3074 const monthNames = ['Jan','Feb','Mar','Apr','May','Jun', 3075 'Jul','Aug','Sep','Oct','Nov','Dec']; 3076 const startMonth = monthNames[startDate.getMonth()]; 3077 const startDay = startDate.getDate(); 3078 const endMonth = monthNames[endDate.getMonth()]; 3079 const endDay = endDate.getDate(); 3080 // ASCII hyphen â Devin's preferred format (matches typing 3081 // conventions and the native UPS row style). 3082 3083 if (bufferDays === 0 3084 || (startMonth === endMonth && startDay === endDay)) { 3085 return '(' + startMonth + ' ' + startDay + ')'; 3086 } 3087 if (startMonth === endMonth) { 3088 return '(' + startMonth + ' ' + startDay + '-' + endDay + ')'; 3089 } 3090 return '(' + startMonth + ' ' + startDay 3091 + ' - ' + endMonth + ' ' + endDay + ')'; 3092 } 3093 3094 // Same-tier collision resolution (formerly betterFedExService + 3095 // fxParseDeliveryTime) moved server-side to carrier_quote.php with 3096 // PR #78's single-bump rule: the cheaper service tries to claim the 3097 // tier below first, falling back to the new arrival bumping up if 3098 // the lower tier is taken. The client receives a pre-resolved 3099 // services list -- each tier_id appears at most once. 3100 3101 // CSS.escape with fallback â used for the data-fx-fedex-service 3102 // attribute selector when re-selecting after a re-quote. 3103 function fxCssEscape(s) { 3104 if (window.CSS && CSS.escape) return CSS.escape(String(s)); 3105 return String(s).replace(/[^a-zA-Z0-9_-]/g, 3106 ch => '\\' + ch.charCodeAt(0).toString(16) + ' '); 3107 } 3108 3109 // ââ Drive the customer-visible display from a carrier response â 3110 // `services` is the orchestrator's pre-merged-and-bumped services 3111 // list (or [] to clear). carrier_quote.php has already applied the 3112 // allowlist, tier-rounding, and the single-bump collision rule, so 3113 // every entry has a unique tier_id and is safe to render directly. 3114 function applyFedExFixedTier(services) { 3115 if (!Array.isArray(services)) services = []; 3116 3117 // Capture previously-selected service from a currently-visible 3118 // row (before we hide it). resetForRequote() also captures into 3119 // previouslySelectedFedexService before calling hideAllFixedSlots(), 3120 // so this is belt-and-braces. 3121 const checkedInput = document.querySelector( 3122 '#zs-shipping-methods-form input.shipping-method:checked' 3123 ); 3124 if (checkedInput && checkedInput.dataset.fxFedexService) { 3125 previouslySelectedFedexService = checkedInput.dataset.fxFedexService; 3126 } 3127 3128 const newVisibleIds = new Set(); 3129 const matchedRowsByTier = {}; 3130 3131 services.forEach(svc => { 3132 if (!svc || typeof svc !== 'object' || !svc.tier_id) return; 3133 // Customer-facing relabel (e.g. Home Delivery -> FedEx Ground). 3134 // Mutating svc.service_name here keeps the row label, the 3135 // data-fx-fedex-service attr, the selection log and re-quote 3136 // persistence all consistent on the one name. 3137 svc.service_name = fedexDisplayName(svc.service_type, svc.service_name); 3138 const tier = { id: svc.tier_id, name: svc.tier_name, rate: svc.tier_rate }; 3139 // Server guarantees one svc per tier_id (single-bump rule 3140 // resolved any collisions), so no client-side tie-break here. 3141 newVisibleIds.add(tier.id); 3142 matchedRowsByTier[tier.id] = { svc, tier }; 3143 }); 3144 3145 if (newVisibleIds.size === 0) { 3146 if (services.length > 0) { 3147 console.warn('[FedEx Fixed] empty merged services list -- all hidden'); 3148 } 3149 // Defensive: clear state and reapply hide-all. 3150 visibleFixedSlotIds = new Set(); 3151 visibleFixedRowData = {}; 3152 applyFixedRateCSS(); 3153 fixedTierIds.forEach(id => {
3154 const row = getRowByShippingChargeId(id); 3155 if (row) setRowHiddenAndDisabled(row, true); 3156 }); 3157 return; 3158 } 3159 3160 visibleFixedSlotIds = newVisibleIds; 3161 visibleFixedRowData = matchedRowsByTier; 3162 applyFixedRateCSS(); 3163 3164 Object.entries(matchedRowsByTier).forEach(([tierId, { svc, tier }]) => { 3165 const row = getRowByShippingChargeId(tierId); 3166 if (!row) { 3167 fxLog('Row for ' + tier.name + ' not in DOM yet; observer will patch on render'); 3168 return; 3169 } 3170 setRowHiddenAndDisabled(row, false); 3171 patchFixedRowLabel(row, svc, tier); 3172 }); 3173 3174 // Selection persistence (Q5) â re-select the previously-chosen 3175 // FedEx service if it's still in the new visible set. 3176 if (previouslySelectedFedexService) { 3177 const target = document.querySelector( 3178 '#zs-shipping-methods-form input.shipping-method' 3179 + '[data-fx-fedex-service="' + fxCssEscape(previouslySelectedFedexService) + '"]' 3180 ); 3181 if (target && !target.disabled) { 3182 target.checked = true; 3183 target.dispatchEvent(new Event('change', { bubbles: true })); 3184 fxLog('Re-selected previously-chosen FedEx service: ' 3185 + previouslySelectedFedexService); 3186 } 3187 } 3188 3189 fxLog('Showing ' + newVisibleIds.size + ' carrier fixed-rate methods'); 3190 } 3191 3192 // ââ Fixed-rate selection logging (Phase 1) âââââââââââââââââââ 3193 // When a customer selects one of the un-hidden FedEx fixed-rate 3194 // rows, fire a fire-and-forget POST to fixed_rate_log.php so the 3195 // server captures which actual FedEx service the customer chose. 3196 // Zoho records the placed order's shipping method only as 3197 // "fixed 25" â fulfillment needs the real service name to print 3198 // the correct label. See handoffs/fedex_fixed_rate_display.md 3199 // ("Order logging"). Phase 2 (a Zoho order webhook writing the 3200 // cf_ship_via custom field) is not built yet. 3201 // 3202 // Logs EVERY selection, including the programmatic re-selection 3203 // applyFedExFixedTier performs after a re-quote â that keeps the 3204 // most-recent log row per cart accurate for Phase 2's lookup. 3205 3206 const FIXED_RATE_LOG_ENDPOINT = 3207 (window.STOCKDATA_BASE || 'https://htm.tyconsystems.com/stockdata/') 3208 + 'fixed_rate_log.php'; 3209 3210 function fxGetUrlParam(name) { 3211 try { 3212 return new URLSearchParams(window.location.search).get(name) || ''; 3213 } catch (e) { 3214 return ''; 3215 } 3216 } 3217 3218 function logFixedSelection(input) { 3219 const tierId = input.value; 3220 const data = visibleFixedRowData[tierId]; 3221 const tier = (data && data.tier) ? data.tier : null; 3222 const svc = (data && data.svc) ? data.svc : null; 3223 3224 const payload = { 3225 cart_id: fxGetUrlParam('cart_id'), 3226 checkout_id: fxGetUrlParam('checkout_id'), 3227 dest_zip: capturedShippingZip || '', 3228 tier_id: tierId || '', 3229 tier_name: tier ? tier.name : '', 3230 tier_rate: tier ? String(tier.rate) : '', 3231 fedex_service_type: input.dataset.fxFedexServiceType 3232 || (svc ? svc.service_type : '') || '', 3233 fedex_service_name: input.dataset.fxFedexService 3234 || (svc ? svc.service_name : '') || '', 3235 fedex_actual_rate: (svc && svc.rate != null) 3236 ? String(svc.rate) : '', 3237 transit_days: (svc && svc.transit_days != null) 3238 ? String(svc.transit_days) : '', 3239 delivery_date: (svc && svc.delivery_date) 3240 ? svc.delivery_date : '', 3241 // Surcharge baked into fedex_actual_rate when the cart 3242 // contained a hazmat item (cf_hazmat_multiplier > 0). 0 for 3243 // non-hazmat carts. Captured at quote time via 3244 // ingestFixedTierIds() reading data.hazmat_surcharge. 3245 hazmat_surcharge: String(lastHazmatSurcharge || 0), 3246 }; 3247 3248 const ctrl = new AbortController(); 3249 const timer = setTimeout(function () { ctrl.abort(); }, 5000); 3250 fetch(FIXED_RATE_LOG_ENDPOINT, { 3251 method: 'POST', 3252 headers: { 'Content-Type': 'application/json' }, 3253 body: JSON.stringify(payload), 3254 signal: ctrl.signal, 3255 // keepalive lets the POST complete even if the customer 3256 // selects a method and immediately clicks Place Order 3257 // (page navigates away mid-request). 3258 keepalive: true, 3259 }) 3260 .then(function () { 3261 clearTimeout(timer); 3262 fxLog('Logged fixed selection: ' + payload.fedex_service_name 3263 + ' / ' + payload.tier_name); 3264 }) 3265 .catch(function (err) { 3266 clearTimeout(timer); 3267 // Non-critical â a logging failure must never affect the 3268 // customer's checkout. 3269 console.warn('[Fixed Log] selection log POST failed ' 3270 + '(non-critical):', (err && err.message) || err); 3271 }); 3272 } 3273 3274 // Single delegated change-listener â survives Zoho re-renders and 3275 // never accumulates per-row listeners. Only acts on a fixed-rate 3276 // row our code patched (data-fx-fixed === '1'). 3277 function installFixedSelectionLogger() { 3278 document.addEventListener('change', function (e) { 3279 try { 3280 const input = e.target; 3281 if (!input || !input.classList 3282 || !input.classList.contains('shipping-method')) { 3283 return; 3284 } 3285 if (input.dataset.fxFixed !== '1') return; 3286 if (!input.checked) return; 3287 logFixedSelection(input); 3288 } catch (err) { 3289 console.warn('[Fixed Log] change handler error ' 3290 + '(non-critical):', err); 3291 } 3292 }); 3293 } 3294 3295 // ââ Review-step shipping-charge relabel ââââââââââââââââââââââ 3296 // The Review Order & Make Payment table lists the shipping cost 3297 // as a pseudo cart item whose label is the raw Zoho method n
3297ame 3298 // ("fixed 15 (TBD)") -- patchFixedRowLabel only reaches the 3299 // Dispatch-step method rows. Keep the row and its amount 3300 // (2026-07-29 feedback on PR #145's whole-row hide) and REWRITE 3301 // the raw element to the selected service's relabeled name 3302 // ("USPS Ground Advantage"): showing a second line keeps Zoho's 3303 // native two-line row geometry (hiding it left "Shipping charge" 3304 // vertically misaligned with the amount) and tells the customer 3305 // which service they picked. Until the service name is 3306 // resolvable (e.g. reload before the quote round re-patches the 3307 // selection) the raw element is hidden instead -- the internal 3308 // tier name must never show. Non-fixed method names (e.g. a 3309 // store-pickup option) are left alone. 3310 // 3311 // The pseudo-row shares .theme-cart-view-list with real product 3312 // rows; the discriminators are its "Shipping charge" label text 3313 // and the absence of a product link. 3314 3315 // The selected method's customer-facing service name. Two sources: 3316 // 1. The tier the review row itself bills (its raw "fixed N" text) 3317 // looked up in visibleFixedRowData -- authoritative, and the ONLY 3318 // source after a reload: the hide pass unchecks Zoho's restored 3319 // radio and the re-select persistence variable died with the 3320 // page, so NO input is checked until the customer touches the 3321 // methods again (observed 2026-07-29, fixed23.har session). 3322 // 2. The checked input's data-fx-fedex-service -- covers the moment 3323 // a customer picks a different service before Zoho re-renders 3324 // the review row onto its new tier. 3325 function resolveServiceNameForTier(tierName) { 3326 if (tierName) { 3327 const want = tierName.replace(/\s+/g, ' ').trim().toLowerCase(); 3328 for (const id in visibleFixedRowData) { 3329 const data = visibleFixedRowData[id]; 3330 if (data && data.tier && data.tier.name 3331 && data.tier.name.replace(/\s+/g, ' ').trim().toLowerCase() === want 3332 && data.svc && data.svc.service_name) { 3333 return data.svc.service_name; 3334 } 3335 } 3336 } 3337 const checked = document.querySelector( 3338 '#zs-shipping-methods-form input.shipping-method:checked'); 3339 return (checked && checked.dataset.fxFedexService) || ''; 3340 } 3341 3342 function relabelReviewShippingChargeRows() { 3343 document.querySelectorAll( 3344 '#zs-order-review-content .theme-cart-view-list,' 3345 + ' .order-review .theme-cart-view-list' 3346 ).forEach(function (row) { 3347 try { 3348 const detail = row.querySelector('.theme-cart-view-product-detail'); 3349 if (!detail) return; 3350 if (!/^\s*Shipping charge/i.test(detail.textContent || '')) return; 3351 if (row.querySelector('a[href*="/products/"]')) return; 3352 Array.prototype.forEach.call(detail.children, function (child) { 3353 const txt = child.textContent || ''; 3354 const rawMatch = txt.match(/^\s*(fixed\s+\d+)/i); 3355 const isOurs = child.dataset && child.dataset.fxReviewLabel === '1'; 3356 if (!rawMatch && !isOurs) return; 3357 // Remember which tier this row bills -- the raw text 3358 // is gone after the first rewrite, but the tier is 3359 // what the name must be resolved from on later passes. 3360 if (rawMatch) child.dataset.fxTierName = rawMatch[1]; 3361 const svcName = resolveServiceNameForTier( 3362 (child.dataset && child.dataset.fxTierName) || ''); 3363 if (svcName) { 3364 if (txt.trim() !== svcName) { 3365 child.textContent = svcName; 3366 log('Review Shipping charge relabeled: ' + svcName); 3367 } 3368 child.dataset.fxReviewLabel = '1'; 3369 child.style.display = ''; 3370 } else if (rawMatch && child.style.display !== 'none') { 3371 child.style.display = 'none'; 3372 log('Review Shipping charge raw tier hidden (service name not resolvable yet).'); 3373 } 3374 }); 3375 } catch (error) { 3376 console.warn('[Freight] review shipping-charge relabel error (non-critical):', error); 3377 } 3378 }); 3379 } 3380 3381 // ââ "Ship on my account" coupon shipping-charge note âââââââââ 3382 // The "shiponmyaccount" coupon (Zoho admin -> Promotions, type 3383 // free_shipping / 100%) lets a customer ship on their own 3384 // carrier account: Zoho itself zeroes the shipping charge for 3385 // whatever method is selected. But the Review table's "Shipping 3386 // charge" pseudo cart item then either disappears entirely 3387 // (Zoho drops the line item from the checkout -- fixed28.har, 3388 // 2026-07-30) or could render $0.00, so the customer gets no 3389 // cue about the arrangement. While the coupon is applied, keep 3390 // a "Shipping charge" row in the Review table whose amount 3391 // reads a carrier-account prompt instead of a dollar figure: 3392 // - If Zoho rendered the row itself, rewrite its amount text. 3393 // - If Zoho dropped it, inject a lookalike row (marked 3394 // data-fx-ship-account-note) after the last cart row. 3395 // On coupon removal Zoho re-renders the Review table and order 3396 // summary from the applycoupon response, restoring the native 3397 // amounts -- this sync then only removes its own row. Coupon 3398 // state is read from the order-summary coupon block, which that 3399 // same response re-renders, so state and table stay consistent. 3400 // Exact code match only; every other coupon is left alone.
3401 const SHIP_ACCOUNT_COUPON_CODE = 'shiponmyaccount'; 3402 const SHIP_ACCOUNT_NOTE_TEXT = 3403 'Please provide your carrier account in order notes'; 3404 // Highlighted pill the note renders in. It lives in the row's 3405 // WIDE detail cell, not the narrow amount column -- the plain 3406 // text wrapped to three lines there (2026-07-30 feedback) -- so 3407 // it fits one line on desktop, two on mobile. Palette matches 3408 // the existing shipping notice (#sf-notice). 3409 const SHIP_ACCOUNT_NOTE_STYLE = 3410 'display:inline-block; max-width:100%; margin-top:4px;' 3411 + ' padding:3px 10px; background:#fff8e1;' 3412 + ' border:1px solid #f9a825; border-radius:4px;' 3413 + ' color:#e65100; font-weight:600; font-size:13px;' 3414 + ' line-height:1.45;'; 3415 3416 // Ensure the highlighted note pill exists inside a row's detail 3417 // cell (idempotent; the injected row and a Zoho-rendered row get 3418 // the same presentation). 3419 function ensureShipAccountPill(detailEl) { 3420 if (!detailEl || detailEl.querySelector('[data-fx-ship-account-msg]')) return; 3421 const pill = document.createElement('div'); 3422 pill.setAttribute('data-fx-ship-account-msg', '1'); 3423 pill.setAttribute('style', SHIP_ACCOUNT_NOTE_STYLE); 3424 pill.textContent = SHIP_ACCOUNT_NOTE_TEXT; 3425 detailEl.appendChild(pill); 3426 } 3427 3428 function isShipAccountCouponApplied() { 3429 const codeEl = document.querySelector( 3430 '.theme-cart-details-coupon-applied.theme-coupon-active' 3431 + ' .theme-applied-coupon .theme-text-semibold'); 3432 return !!codeEl && (codeEl.textContent || '').trim().toLowerCase() 3433 === SHIP_ACCOUNT_COUPON_CODE; 3434 } 3435 3436 function syncShipAccountShippingNote() { 3437 try { 3438 const noteRows = Array.prototype.slice.call( 3439 document.querySelectorAll('[data-fx-ship-account-note]')); 3440 if (!isShipAccountCouponApplied()) { 3441 noteRows.forEach(function (el) { el.remove(); }); 3442 document.querySelectorAll('[data-fx-ship-account-msg]') 3443 .forEach(function (el) { el.remove(); }); 3444 document.querySelectorAll('[data-fx-ship-account-hidden]') 3445 .forEach(function (el) { 3446 el.style.display = ''; 3447 el.removeAttribute('data-fx-ship-account-hidden'); 3448 }); 3449 return; 3450 } 3451 // Scan the Review cart list(s): Zoho's own shipping-charge 3452 // pseudo-row (same discriminators as the relabel above) 3453 // and the last cart row per list -- the anchor the note 3454 // row is kept after when Zoho dropped the pseudo-row. 3455 const zohoRowByList = new Map(); 3456 const lastRowByList = new Map(); 3457 document.querySelectorAll( 3458 '#zs-order-review-content .theme-cart-view-list,' 3459 + ' .order-review .theme-cart-view-list' 3460 ).forEach(function (row) { 3461 if (row.dataset.fxShipAccountNote) return; 3462 lastRowByList.set(row.parentNode, row); 3463 const detail = row.querySelector('.theme-cart-view-product-detail'); 3464 if (!detail) return; 3465 if (!/^\s*Shipping charge/i.test(detail.textContent || '')) return; 3466 if (row.querySelector('a[href*="/products/"]')) return; 3467 zohoRowByList.set(row.parentNode, row); 3468 }); 3469 // Drop note rows stranded in a list Zoho has since emptied. 3470 noteRows.forEach(function (el) { 3471 if (!lastRowByList.has(el.parentNode)) el.remove(); 3472 }); 3473 lastRowByList.forEach(function (lastRow, list) { 3474 let noteRow = null; 3475 for (let i = 0; i < noteRows.length; i++) { 3476 if (noteRows[i].parentNode === list) { noteRow = noteRows[i]; break; } 3477 } 3478 const zohoRow = zohoRowByList.get(list); 3479 if (zohoRow) { 3480 // Zoho rendered the row itself -- hide its narrow 3481 // amount cell, show the pill in its wide detail 3482 // cell, and drop any injected duplicate. 3483 if (noteRow) noteRow.remove(); 3484 zohoRow.querySelectorAll('.theme-cart-view-product-subtotal-wrap') 3485 .forEach(function (wrapEl) { 3486 if (wrapEl.style.display !== 'none') { 3487 wrapEl.style.display = 'none'; 3488 wrapEl.setAttribute('data-fx-ship-account-hidden', '1'); 3489 log('Ship-on-account: hid the shipping-charge amount; showing the carrier-account note.'); 3490 } 3491 }); 3492 ensureShipAccountPill( 3493 zohoRow.querySelector('.theme-cart-view-product-detail')); 3494 return; 3495 } 3496 if (!noteRow) { 3497 noteRow = document.createElement('div'); 3498 noteRow.className = 'theme-cart-view-list'; 3499 noteRow.setAttribute('data-fx-ship-account-note', '1'); 3500 noteRow.innerHTML = 3501 '<div class="theme-cart-view-product-detail-wrap">' 3502 + '<div class="theme-cart-view-product-detail">Shipping charge' 3503 + '<div data-fx-ship-account-service style="display:none"></div>' 3504 + '</div></div>'; 3505 ensureShipAccountPill( 3506 noteRow.querySelector('.theme-cart-view-product-detail')); 3507 log('Ship-on-account: injected the shipping-charge note row.'); 3508 } 3509 if (lastRow.nextElementSibling !== noteRow) { 3510 lastRow.insertAdjacentElement('afterend', noteRow); 3511 } 3512 // Name the selected service on the second line, like 3513 // the native row would -- hidden until resolvable 3514 // (e.g. after a reload no radio is checked). 3515 const svcEl = noteRow.querySelector('[data-fx-ship-account-service]'); 3516 if (svcEl) { 3517 const svcName = resolveServiceNameForTier(''); 3518 if (svcEl.textContent !== svcName) svcEl.textContent = svcName; 3519 svcEl.style.display = svcName ? '' : 'none'; 3520 } 3521 }); 3522 } catch (error) { 3523 console.warn('[Freight] ship-on-account note error (non-critical):', error); 3524 } 3525 } 3526 3527 // ââ NET 30 / Invoice payment option gate âââââââââââââââââââââ 3528 // Scott enabled a "NET 30 / Invoice" offline payment option 3529 // (Zoho admin -> Payments) intended only for customers approved 3530 // for terms. Zoho has no per-customer payment gating, so the 3531 // option is hidden by default and un-hidden only when BOTH hold 3532 // (Phase 2 + credit gate, July 2026): 3533 // 1. the logged-in customer's Zoho Books contact has Payment 3534 // Terms = "Net 30", AND
3535 // 2. the CURRENT checkout total fits inside that contact's 3536 // available credit (credit_limit - outstanding receivables 3537 // + unused credits; compared server-side so no credit 3538 // amounts ever reach the browser). 3539 // When 1 holds but 2 fails (server reason 'over_credit') the 3540 // option is shown GREYED-OUT and unselectable with "Your cart 3541 // exceeds your available credit." -- an eligible customer learns 3542 // why, instead of the option silently vanishing (2026-07-30). 3543 // Mechanics: 3544 // - The checkout page carries the logged-in portal email in the 3545 // hidden input #zs-portal-email (server-rendered); the grand 3546 // total lives in #zs-payment-total-summary. 3547 // - checkNet30Eligibility() POSTs {email, cart_total} to 3548 // stockdata/net30_check.php and stores the verdict KEYED TO 3549 // that email+total; a cart/shipping/tax change makes the 3550 // verdict stale, the option hides, and the gate cycle 3551 // re-checks with the new total. FAIL CLOSED everywhere. 3552 // - Verdicts cache per-tab in sessionStorage (30 min); contact 3553 // terms+credit data caches server-side 15 min. A terms or 3554 // credit change in Books can take up to ~45 min to reach a 3555 // customer. 3556 // This remains a cosmetic gate -- the option stays enabled 3557 // Zoho-side and a DevTools user could unhide it, so any NET 30 3558 // order from an unapproved customer still needs the normal 3559 // manual review before fulfillment. 3560 // 3561 // Offline options render inside the Review step's Payment Options 3562 // block as .theme-checkout-payment-option containers wrapping an 3563 // input.zs-choose-offline-payment-option; matched by label text so 3564 // a future second offline method (e.g. wire transfer) isn't swept 3565 // up. Re-applied by the same review-step machinery as the 3566 // shipping-charge relabel (observer reapply + click-armed poll). 3567 3568 const NET30_CHECK_ENDPOINT = STOCKDATA_BASE + 'net30_check.php'; 3569 const NET30_STORE_KEY = 'fx-net30'; 3570 const NET30_CACHE_TTL_MS = 30 * 60 * 1000; 3571 // Latest verdict {email, total, allowed, ts} -- a verdict is only 3572 // honored while it still matches the CURRENT email + checkout 3573 // total, so a cart/shipping change automatically invalidates it 3574 // (fail closed until the re-check answers). 3575 let net30Verdict = null; 3576 // "email|total|cid" of the last fetch fired -- dedupes the re-check 3577 // the gate cycle triggers every pass. 3578 let net30FetchKey = ''; 3579 3580 // ââ Customer-id capture (secondary contact-person logins) ââââ 3581 // The Commerce checkout's customer.customer_id IS the Zoho 3582 // Inventory/Books contact_id (verified against the live org, 3583 // 2026-07-30). net30_check.php uses it to fetch the contact 3584 // DIRECTLY, because contacts?email= only matches a contact's 3585 // PRIMARY email -- secondary contact-person logins (Scott's 3586 // dmarc@/sales@) matched nothing or an unrelated record, so the 3587 // option never showed for them. Sources, in order: per-tab 3588 // sessionStorage (survives reloads), the checkout/address XHR 3589 // responses the shared __cxAddr interceptor already parses, and 3590 // a one-shot /storefront/api/v1/cart fetch (the saved-address 3591 // reload path fires no address XHR). The server verifies the 3592 // login email is ON the claimed contact before honoring the id, 3593 // so a stale or wrong id safely degrades to the email search. 3594 const NET30_CID_STORE_KEY = 'fx-net30-cid'; 3595 let net30CustomerId = ''; 3596 3597 function storeNet30CustomerId(id) { 3598 id = String(id || '').trim(); 3599 if (!/^\d{5,25}$/.test(id) || net30CustomerId === id) return; 3600 net30CustomerId = id; 3601 try { sessionStorage.setItem(NET30_CID_STORE_KEY, id); } catch (e) { } 3602 // The fetch dedupe key and verdict freshness both include the 3603 // id, so an email-only verdict is invalidated automatically; 3604 // clearing the key lets the next gate cycle re-check at once. 3605 net30FetchKey = ''; 3606 log('NET 30: checkout customer id captured.'); 3607 } 3608 3609 // Bounded deep search for the customer id in a storefront API
3610 // response (shapes differ per endpoint). A cart/checkout payload 3611 // only ever describes the logged-in customer, and the server-side 3612 // membership guard makes a wrong pick harmless. 3613 function findCustomerIdDeep(node, depth) { 3614 if (!node || typeof node !== 'object' || (depth || 0) > 6) return ''; 3615 if (node.customer && typeof node.customer === 'object' 3616 && node.customer.customer_id) { 3617 return String(node.customer.customer_id); 3618 } 3619 if (node.customer_id) return String(node.customer_id); 3620 for (const k in node) { 3621 const v = node[k]; 3622 if (v && typeof v === 'object') { 3623 const found = findCustomerIdDeep(v, (depth || 0) + 1); 3624 if (found) return found; 3625 } 3626 } 3627 return ''; 3628 } 3629 3630 function initNet30CustomerIdCapture() { 3631 try { 3632 try { 3633 const stored = sessionStorage.getItem(NET30_CID_STORE_KEY); 3634 if (stored && /^\d{5,25}$/.test(stored)) net30CustomerId = stored; 3635 } catch (e) { } 3636 if (window.__cxAddr) { 3637 window.__cxAddr.subscribe(function (xhr, parsed) { 3638 try { 3639 const cust = parsed && parsed.extra && parsed.extra.checkout 3640 && parsed.extra.checkout.customer; 3641 if (cust && cust.customer_id) { 3642 storeNet30CustomerId(cust.customer_id); 3643 } 3644 } catch (e) { /* capture is best-effort */ } 3645 }); 3646 } 3647 if (!net30CustomerId) { 3648 fetch(window.origin + '/storefront/api/v1/cart') 3649 .then(function (r) { return r.json(); }) 3650 .then(function (data) { 3651 const id = findCustomerIdDeep(data); 3652 if (id) storeNet30CustomerId(id); 3653 }) 3654 .catch(function () { /* email-search fallback covers it */ }); 3655 } 3656 } catch (error) { 3657 console.warn('[Freight] NET 30 customer-id capture error (non-critical):', error); 3658 } 3659 } 3660 3661 // The logged-in portal customer's email, from the hidden input the 3662 // checkout page is server-rendered with. Empty for guests (this 3663 // store is portal-only checkout, but stay defensive). 3664 function getPortalEmail() { 3665 const el = document.getElementById('zs-portal-email'); 3666 const v = el && el.value ? String(el.value).trim() : ''; 3667 return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) ? v : ''; 3668 } 3669 3670 // The current checkout grand total (merchandise + shipping + tax) 3671 // from the Order Summary's #zs-payment-total-summary span. null 3672 // until Zoho renders it. The credit gate compares THIS number, so 3673 // it re-checks when shipping/tax land and the total changes. 3674 function getCheckoutTotal() { 3675 const el = document.getElementById('zs-payment-total-summary'); 3676 if (!el) return null; 3677 const n = parseFloat(String(el.textContent || '').replace(/[^0-9.]/g, '')); 3678 return (isFinite(n) && n > 0) ? Math.round(n * 100) / 100 : null; 3679 } 3680 3681 // True while the stored verdict matches the LIVE email + total and 3682 // hasn't aged out -- the shared freshness rule for both states below. 3683 function net30VerdictCurrent() { 3684 if (!net30Verdict) return false; 3685 const email = getPortalEmail(); 3686 const total = getCheckoutTotal(); 3687 if (!email || total === null) return false; 3688 return net30Verdict.email === email 3689 && Math.abs(net30Verdict.total - total) < 0.005 3690 && (net30Verdict.cid || '') === net30CustomerId 3691 && (Date.now() - net30Verdict.ts) < NET30_CACHE_TTL_MS; 3692 } 3693 3694 function isNet30AllowedForSession() { 3695 return net30VerdictCurrent() && !!net30Verdict.allowed; 3696 } 3697 3698 // Eligible on terms but the cart exceeds available credit: the 3699 // option is shown greyed-out + unselectable with an explanation 3700 // instead of hidden (Devin, 2026-07-30). The server disclose
3700s 3701 // reason 'over_credit' only in that state. 3702 function isNet30OverCredit() { 3703 return net30VerdictCurrent() 3704 && !net30Verdict.allowed 3705 && net30Verdict.reason === 'over_credit'; 3706 } 3707 3708 function checkNet30Eligibility() { 3709 try { 3710 const email = getPortalEmail(); 3711 if (!email) return; // guest -- option stays hidden 3712 const total = getCheckoutTotal(); 3713 if (total === null) return; // total not rendered yet; the gate cycle re-calls 3714 const key = email + '|' + total.toFixed(2) + '|' + net30CustomerId; 3715 if (net30FetchKey === key) return; // in flight or already answered 3716 try { 3717 const raw = sessionStorage.getItem(NET30_STORE_KEY); 3718 if (raw) { 3719 const c = JSON.parse(raw); 3720 if (c && c.email === email && typeof c.total === 'number' 3721 && Math.abs(c.total - total) < 0.005 3722 && (c.cid || '') === net30CustomerId 3723 && (Date.now() - c.ts) < NET30_CACHE_TTL_MS) { 3724 net30Verdict = c; 3725 net30FetchKey = key; 3726 log('NET 30 eligibility (cached, total $' + total.toFixed(2) + '): ' 3727 + (c.allowed ? 'approved' : 'not approved')); 3728 applyNet30PaymentGate(); 3729 return; 3730 } 3731 } 3732 } catch (e) { /* private mode -- fall through to the fetch */ } 3733 net30FetchKey = key; 3734 const ctrl = new AbortController(); 3735 const timer = setTimeout(function () { ctrl.abort(); }, 5000); 3736 fetch(NET30_CHECK_ENDPOINT, { 3737 method: 'POST', 3738 headers: { 'Content-Type': 'application/json' }, 3739 body: JSON.stringify(net30CustomerId 3740 ? { email: email, cart_total: total, customer_id: net30CustomerId } 3741 : { email: email, cart_total: total }), 3742 signal: ctrl.signal, 3743 }) 3744 .then(function (r) { return r.json(); }) 3745 .then(function (data) { 3746 clearTimeout(timer); 3747 net30Verdict = { 3748 email: email, total: total, 3749 cid: net30CustomerId, 3750 allowed: !!(data && data.allowed), 3751 reason: (data && data.reason) || '', 3752 ts: Date.now(), 3753 }; 3754 try { 3755 sessionStorage.setItem(NET30_STORE_KEY, JSON.stringify(net30Verdict)); 3756 } catch (e) { } 3757 log('NET 30 eligibility (total $' + total.toFixed(2) + '): ' 3758 + (net30Verdict.allowed ? 'approved' 3759 : (net30Verdict.reason === 'over_credit' ? 'over credit' : 'not approved'))); 3760 applyNet30PaymentGate(); 3761 }) 3762 .catch(function (err) { 3763 clearTimeout(timer); 3764 // Fail closed -- the option simply stays hidden. Clear 3765 // the fetch key so a later gate cycle can retry. 3766 net30FetchKey = ''; 3767 log('NET 30 eligibility check failed (option stays hidden): ' 3768 + ((err && err.message) || err)); 3769 }); 3770 } catch (error) { 3771 console.warn('[Freight] NET 30 eligibility error (non-critical):', error); 3772 } 3773 } 3774 3775 // Re-entry guard for the gate <-> eligibility pair. checkNet30Eligibility() 3776 // calls applyNet30PaymentGate() back (cached path synchronously, fetch path 3777 // later), so without this the recursion terminates only because 3778 // net30FetchKey happens to be assigned BEFORE that nested call -- correct 3779 // today, but a future reordering would hand a customer's checkout an 3780 // infinite recursion. The flag makes termination explicit and 3781 // order-independent. (Self-review finding, 2026-07-30.) 3782 let net30GateBusy = false; 3783 3784 function applyNet30PaymentGate() { 3785 // Keep the verdict current: re-fires the check when the email 3786 // or checkout total changed since the last one (key-deduped, 3787 // so this is a no-op on every other pass). 3788 if (!net30GateBusy) { 3789 net30GateBusy = true; 3790 try { checkNet30Eligibility(); } catch (e) {} 3791 finally { net30GateBusy = false; } 3792 } 3793 document.querySelectorAll( 3794 '.theme-checkout-payment-option' 3795 ).forEach(function (opt) { 3796 try { 3797 const input = opt.querySelector('input.zs-choose-offline-payment-option'); 3798 if (!input) return; 3799 if (!/net\s*30/i.test(opt.textContent || '')) return; 3800 // Three states, re-derived every pass so any transition 3801 // (total change, verdict landing, Zoho re-render) settles 3802 // to the right one: 3803 // allowed -> visible + selectable 3804 // overcredit -> visible, greyed, unselectable, message 3805 // hidden -> display:none (ineligible / unknown / error) 3806 let state = 'hidden'; 3807 if (isNet30AllowedForSession()) state = 'allowed'; 3808 else if (isNet30OverCredit()) state = 'overcredit';
3809 3810 const msgId = 'fx-net30-credit-msg'; 3811 let msg = document.getElementById(msgId); 3812 3813 if (state === 'hidden') { 3814 if (msg) msg.style.display = 'none'; 3815 if (opt.style.display === 'none') return; 3816 // Defensive: never leave a hidden option selected. 3817 input.disabled = true; 3818 input.checked = false; 3819 opt.style.display = 'none'; 3820 log('NET 30 payment option hidden.'); 3821 return; 3822 } 3823 3824 if (state === 'overcredit') { 3825 input.disabled = true; 3826 input.checked = false; 3827 opt.style.display = ''; 3828 opt.style.opacity = '0.55'; 3829 opt.style.cursor = 'not-allowed'; 3830 if (!msg) { 3831 msg = document.createElement('div'); 3832 msg.id = msgId; 3833 msg.textContent = 'Your cart exceeds your available credit.'; 3834 msg.style.color = '#c00'; 3835 msg.style.margin = '4px 0 4px 28px'; 3836 opt.appendChild(msg); 3837 } 3838 if (msg.style.display === 'none' || opt.dataset.fxNet30State !== 'overcredit') { 3839 msg.style.display = ''; 3840 log('NET 30 shown greyed-out: cart exceeds available credit.'); 3841 } 3842 opt.dataset.fxNet30State = 'overcredit'; 3843 return; 3844 } 3845 3846 // allowed 3847 if (msg) msg.style.display = 'none'; 3848 opt.style.opacity = ''; 3849 opt.style.cursor = ''; 3850 input.disabled = false; 3851 if (opt.style.display === 'none' || opt.dataset.fxNet30State !== 'allowed') { 3852 opt.style.display = ''; 3853 log('NET 30 payment option shown (customer approved for terms).'); 3854 } 3855 opt.dataset.fxNet30State = 'allowed'; 3856 } catch (error) { 3857 console.warn('[Freight] NET 30 gate error (non-critical):', error); 3858 } 3859 }); 3860 } 3861 3862 // ââ NET 30 PO Number -> order Reference# âââââââââââââââââââââ 3863 // A NET 30 customer must supply a PO Number, and it has to land in the 3864 // sales order's native Reference# field. Zoho's checkout has no such 3865 // input, so: 3866 // - selecting NET 30 reveals a required "PO Number" field (injected 3867 // into the NET 30 payment option; removed when another method is 3868 // selected, so card customers never see it); 3869 // - Make Payment is intercepted in the CAPTURE phase and blocked 3870 // with an inline error while NET 30 is selected and the field is 3871 // empty; 3872 // - a valid PO is POSTed to stockdata/po_capture.php, and 3873 // order_verify.php writes it onto the placed order's 3874 // reference_number (matched on the NET 30 customer id + time 3875 // window; ZIP + time window is the fallback for legacy rows). 3876 // Client-side enforcement only, like the NET 30 gate itself: a 3877 // determined user could bypass it, so a NET 30 order arriving with an 3878 // empty Reference# still needs the normal manual review. 3879 const PO_CAPTURE_ENDPOINT = STOCKDATA_BASE + 'po_capture.php'; 3880 const NET30_PO_INPUT_ID = 'fx-net30-po'; 3881 const NET30_PO_ERROR_ID = 'fx-net30-po-error'; 3882 const NET30_PO_STORE_KEY = 'fx-net30-po'; 3883 // Typed value cached in memory + sessionStorage so a Zoho re-render 3884 // (which wipes our injected node) doesn't lose the customer's typing. 3885 // The stored copy is KEYED TO THE CHECKOUT SESSION (checkout_id): a 3886 // later cart in the same tab is a different order and must start with 3887 // an empty field. Without that key the previous order's PO auto-filled 3888 // itself into the next checkout and would have been written onto that 3889 // order's Reference# (found by Devin 2026-07-30, second test order). 3890 // When there is no checkout_id in the URL the value is kept in memory 3891 // only -- it survives re-renders but not a reload, which is the right 3892 // trade against resurrecting a stale PO. 3893 function currentCheckoutKey() { 3894 try { return fxGetUrlParam('checkout_id') || ''; } catch (e) { return ''; } 3895 } 3896 function loadStoredPo() { 3897 try { 3898 const raw = sessionStorage.getItem(NET30_PO_STORE_KEY); 3899 if (!raw) return ''; 3900 const s = JSON.parse(raw); 3901 const cid = currentCheckoutKey(); 3902 if (s && cid && s.cid === cid && typeof s.po === 'string') return s.po; 3903 // Different (or unknown) checkout -- drop the stale value. 3904 sessionStorage.removeItem(NET30_PO_STORE_KEY); 3905 } catch (e) { } 3906 return ''; 3907 } 3908 function saveStoredPo(po) { 3909 try { 3910 const cid = currentCheckoutKey(); 3911 if (!cid) return; // memory-only when we can't scope it 3912 sessionStorage.setItem(NET30_PO_STORE_KEY, JSON.stringify({ cid: cid, po: po })); 3913 } catch (e) { } 3914 } 3915 let net30PoValue = loadStoredPo(); 3916 3917 function findNet30Option() { 3918 return Array.prototype.slice.call( 3919 document.querySelectorAll('.theme-checkout-payment-option') 3920 ).find(function (opt) { 3921 return opt.querySelector('input.zs-choose-offline-payment-option') 3922 && /net\s*30/i.test(opt.textContent || ''); 3923 }) || null; 3924 } 3925 3926 function isNet30Selected() { 3927 const opt = findNet30Option(); 3928 if (!opt || opt.offsetHeight === 0) return false;
3929 const input = opt.querySelector('input'); 3930 return !!(input && input.checked); 3931 } 3932 3933 function getNet30PoValue() { 3934 const el = document.getElementById(NET30_PO_INPUT_ID); 3935 return el ? String(el.value || '').trim() : net30PoValue.trim(); 3936 } 3937 3938 // Create / show / hide the PO field to match the current selection. 3939 // Idempotent: safe to call from the observer reapply and the poll. 3940 function syncNet30PoField() { 3941 try { 3942 const opt = findNet30Option(); 3943 let wrap = document.getElementById(NET30_PO_INPUT_ID + '-wrap'); 3944 if (!opt || opt.offsetHeight === 0 || !isNet30Selected()) { 3945 if (wrap) wrap.style.display = 'none'; 3946 return; 3947 } 3948 if (!wrap) { 3949 wrap = document.createElement('div'); 3950 wrap.id = NET30_PO_INPUT_ID + '-wrap'; 3951 wrap.style.margin = '8px 0 4px 28px'; 3952 3953 const label = document.createElement('label'); 3954 label.setAttribute('for', NET30_PO_INPUT_ID); 3955 label.textContent = 'PO Number '; 3956 label.style.display = 'block'; 3957 label.style.marginBottom = '4px'; 3958 const star = document.createElement('span'); 3959 star.textContent = '*'; 3960 star.style.color = '#c00'; 3961 label.appendChild(star); 3962 3963 const input = document.createElement('input'); 3964 input.type = 'text'; 3965 input.id = NET30_PO_INPUT_ID; 3966 input.required = true; 3967 input.maxLength = 60; 3968 input.placeholder = 'Required for NET 30 / Invoice'; 3969 input.autocomplete = 'off'; 3970 input.style.maxWidth = '320px'; 3971 input.style.width = '100%'; 3972 input.value = net30PoValue; 3973 input.addEventListener('input', function () { 3974 net30PoValue = String(input.value || ''); 3975 saveStoredPo(net30PoValue); 3976 if (net30PoValue.trim().length >= NET30_MIN_PO_LEN) hideNet30PoError(); 3977 syncCompleteOrderButton(); 3978 }); 3979 // Capture on blur too -- covers a customer who types the PO 3980 // and then navigates in a way our click guard doesn't see. 3981 // Only once the value is long enough to be a real PO: a 3982 // partial ("AB") would otherwise land in po_capture.log as a 3983 // match candidate, and an abandoned checkout could leave it 3984 // inside another order's match window. (Self-review 3985 // finding, 2026-07-30.) 3986 input.addEventListener('blur', function () { 3987 if (getNet30PoValue().length >= NET30_MIN_PO_LEN) capturePoNumber(); 3988 }); 3989 3990 const err = document.createElement('div'); 3991 err.id = NET30_PO_ERROR_ID; 3992 err.textContent = 'Please enter a PO Number (at least ' 3993 + NET30_MIN_PO_LEN + ' characters) to pay by NET 30 / Invoice.'; 3994 err.style.color = '#c00'; 3995 err.style.marginTop = '4px'; 3996 err.style.display = 'none'; 3997 3998 wrap.appendChild(label); 3999 wrap.appendChild(input); 4000 wrap.appendChild(err); 4001 opt.appendChild(wrap); 4002 log('NET 30 PO field injected.'); 4003 } 4004 wrap.style.display = ''; 4005 } catch (error) { 4006 console.warn('[Freight] NET 30 PO field error (non-critical):', error); 4007 } 4008 } 4009 4010 // ââ Complete Order button gating âââââââââââââââââââââââââââââ 4011 // A click-time block still leaves the button LOOKING clickable, so 4012 // disable it outright until the PO meets the minimum length while 4013 // NET 30 is selected. 4014 // 4015 // Zoho owns this button too (it enables it when the terms checkbox is 4016 // ticked), so: only ever release a disable WE applied (marked with 4017 // data-fx-po-disabled), and when releasing, re-derive the state from 4018 // the terms checkbox instead of blindly enabling -- otherwise a 4019 // satisfied PO would enable the button with terms unaccepted. 4020 const NET30_MIN_PO_LEN = 4; 4021 4022 function findCompleteOrderButton() { 4023 return Array.prototype.slice.call( 4024 document.querySelectorAll('button, input[type=submit]') 4025 ).find(function (b) { 4026 const label = b.textContent || b.value || ''; 4027 return /make\s*payment|complete\s*order|place\s*order/i.test(label) 4028 && b.offsetHeight > 0; 4029 }) || null; 4030 } 4031 4032 function termsAccepted() { 4033 const cb = Array.prototype.slice.call( 4034 document.querySelectorAll('input[type=checkbox]') 4035 ).find(function (c) { 4036 const scope = c.closest('div, p, label'); 4037 return scope && /terms\s*and\s*conditions/i.test(scope.textContent || ''); 4038 }); 4039 // If the checkbox can't be found, don't invent a reason to block. 4040 return cb ? !!cb.checked : true; 4041 } 4042 4043 function net30PoSatisfied() { 4044 if (!isNet30Selected()) return true; // not our concern 4045 return getNet30PoValue().length >= NET30_MIN_PO_LEN; 4046 } 4047 4048 function syncCompleteOrderButton() { 4049 try { 4050 const btn = findCompleteOrderButton(); 4051 if (!btn) return; 4052 if (!net30PoSatisfied()) { 4053 if (!btn.disabled) { 4054 btn.disabled = true; 4055 btn.dataset.fxPoDisabled = '1'; 4056 btn.style.opacity = '0.5'; 4057 btn.style.cursor = 'not-allowed'; 4058 btn.title = 'Enter a PO Number (at least ' + NET30_MIN_PO_LEN 4059 + ' characters) to pay by NET 30 / Invoice.'; 4060 } 4061 return; 4062 } 4063 if (btn.dataset.fxPoDisabled === '1') { 4064 // Ours to release -- but hand control back to Zoho's rule 4065 // rather than forcing the button on. 4066 btn.disabled = !termsAccepted(); 4067 delete btn.dataset.fxPoDisabled; 4068 btn.style.opacity = ''; 4069 btn.style.cursor = ''; 4070 btn.title = ''; 4071 } 4072 } catch (error) { 4073 console.warn('[Freight] Complete Order gate error (non-critical):', error); 4074 } 4075 } 4076 4077 function showNet30PoError() { 4078 const err = document.getElementById(NET30_PO_ERROR_ID); 4079 if (err) err.style.display = ''; 4080 const input = document.getElementById(NET30_PO_INPUT_ID); 4081 if (input) { 4082 input.style.borderColor = '#c00'; 4083 try { input.focus(); } catch (e) {} 4084 } 4085 } 4086 function hideNet30PoError() { 4087 const err = document.getElementById(NET30_PO_ERROR_ID); 4088 if (err) err.style.display = 'none'; 4089 const input = document.getElementById(NET30_PO_INPUT_ID); 4090 if (input) input.style.borderColor = ''; 4091 } 4092 4093 function capturePoNumber() { 4094 try { 4095 const po = getNet30PoValue(); 4096 if (!po) return; 4097 const ctrl = new AbortController(); 4098 const timer = setTimeout(function () { ctrl.abort(); }, 5000); 4099 fetch(PO_CAPTURE_ENDPOINT, { 4100 method: 'POST', 4101 headers: { 'Content-Type': 'application/json' }, 4102 body: JSON.stringify({ 4103 po_number: po, 4104 dest_zip: capturedShippingZip || '', 4105 // Inventory contact id captured by the NET 30 gate -- 4106 // the PO->order join prefers it over the ZIP, which 4107 // international checkouts often leave empty (Canadian 4108 // V6Z 1E0 evaded the DOM scrape 2026-07-30 and the 4109 // order lost its auto Reference#). 4110 customer_id: net30CustomerId || '', 4111 checkout_id: fxGetUrlParam('checkout_id'), 4112 cart_id: fxGetUrlParam('cart_id'), 4113 }), 4114 signal: ctrl.signal, 4115 // keepalive so the POST survives the Make Payment navigation 4116 keepalive: true, 4117 }) 4118 .then(function () { clearTimeout(timer); log('PO number captured: ' + po); }) 4119 .catch(function (err) { 4120 clearTimeout(timer); 4121 console.warn('[Freight] PO capture failed (non-critical):', (err && err.message) || err); 4122 }); 4123 } catch (error) { 4124 console.warn('[Freight] PO capture error (non-critical):', error); 4125 } 4126 } 4127 4128 // Capture-phase guard on Make Payment: blocks the click while NET 30 is 4129 // selected with an empty PO. stopImmediatePropagation keeps Zoho's own 4130 // handler from running on the same click. 4131 function installNet30PoGuard() { 4132 document.addEventListener('click', function (e) { 4133 try { 4134 const btn = e.target && e.target.closest 4135 ? e.target.closest('button, a, input[type=submit]') 4136 : null; 4137 if (!btn) return; 4138 if (!/make\s*payment|place\s*order/i.test(btn.textContent || btn.value || '')) return; 4139 if (!isNet30Selected()) return; 4140 if (!net30PoSatisfied()) { 4141 e.preventDefault(); 4142 e.stopImmediatePropagation(); 4143 showNet30PoError(); 4144 syncCompleteOrderButton(); 4145 log('Make Payment blocked -- NET 30 selected without a valid PO number.'); 4146 return; 4147 } 4148 capturePoNumber(); 4149 } catch (error) { 4150 console.warn('[Freight] NET 30 PO guard error (non-critical):', error); 4151 } 4152 }, true); 4153 // Selecting a payment method re-renders nothing, so keep the field in 4154 // sync off the same change event the selection logger uses. The terms 4155 // checkbox matters too: Zoho enables the button when it is ticked, and 4156 // that must not escape the PO gate. 4157 document.addEventListener('change', function (e) { 4158 try { 4159 const t = e.target; 4160 if (!t) return; 4161 const isPaymentChoice = (t.classList 4162 && t.classList.contains('zs-choose-offline-payment-option')) 4163 || t.id === 'zs-choose-online-payment-option'; 4164 const isCheckbox = t.type === 'checkbox'; 4165 if (isPaymentChoice || isCheckbox) { 4166 syncNet30PoField(); 4167 syncCompleteOrderButton(); 4168 } 4169 } catch (error) { } 4170 }); 4171 } 4172 4173 // The review table only (re)renders after a user interaction -- 4174 // Continue on Dispatch, or a Change to the address / method / cart. 4175 // A delegated click listener re-arms a short BOUNDED poll on every 4176 // click, so the relabel lands whenever the row appears without a 4177 // permanent MutationObserver (the fixed-slot observer's reapply 4178 // also calls the relabel, but that observer disconnects after 3 min). 4179 let reviewRelabelPollTimer = null; 4180 let reviewRelabelPollAttempts = 0; 4181 const REVIEW_RELABEL_MAX_ATTEMPTS = 16; // 16 x 250 ms = 4 s per arm 4182 function armReviewShippingChargeRelabel() { 4183 // Reset the counter on every arm; if a poll is already running 4184 // this just extends its window instead of stacking intervals. 4185 reviewRelabelPollAttempts = 0; 4186 if (reviewRelabelPollTimer) return; 4187 reviewRelabelPollTimer = setInterval(function () { 4188 reviewRelabelPollAttempts++; 4189 try { relabelReviewShippingChargeRows(); } catch (e) {} 4190 try { syncShipAccountShippingNote(); } catch (e) {} 4191 try { applyNet30PaymentGate(); } catch (e) {} 4192 try { syncNet30PoField(); } catch (e) {} 4193 try { syncCompleteOrderButton(); } catch (e) {} 4194 if (reviewRelabelPollAttempts >= REVIEW_RELABEL_MAX_ATTEMPTS) { 4195 clearInterval(reviewRelabelPollTimer); 4196 reviewRelabelPollTimer = null; 4197 } 4198 }, 250); 4199 } 4200 function installReviewShippingChargeRelabel() { 4201 document.addEventListener('click', function () { 4202 try { armReviewShippingChargeRelabel(); } catch (e) {} 4203 try { 4204 // Revive the fixed-slot observer if its 3-min window 4205 // expired. Zoho rebuilds the shipping-method rows on 4206 // step re-entry (e.g. re-confirming the address after 4207 // the tab idled), the static hide sweeps the fresh 4208 // unpatched rows, and the quote dedupe (same zip+cart 4209 // key) suppresses the re-fire that would repair them -- 4210 // leaving ZERO visible methods (reproduced live 4211 // 2026-07-30; the BACKLOG watch item's predicted 4212 // mechanism). watchForFixedSlots() runs reapply() 4213 // immediately (re-shows + re-patches from the in-memory 4214 // round state) and opens a fresh window. Idle tabs 4215 // still shed the observer; any user click revives it.
4216 if (!currentFixedRateObserver) watchForFixedSlots(); 4217 } catch (e) {} 4218 }); 4219 armReviewShippingChargeRelabel(); 4220 } 4221 4222 // ââ Entry Point ââââââââââââââââââââââââââââââââââââââââââââââââ 4223 4224 function init() { 4225 if (!window.location.pathname.includes('/checkout')) return; 4226 log('Checkout page detected.'); 4227 4228 // Fixed-rate display (handoffs/fedex_fixed_rate_display.md + 4229 // handoffs/freight_fixed_rate.md): 4230 // - Hide all "fixed N" rows + Zoho-native FedEx rows by default. 4231 // - Install the observer so newly-rendered rows get the same 4232 // treatment after Zoho re-renders the shipping section. 4233 // - Install the selection logger (fixed_rate_log.php). 4234 hideAllFixedSlots(); 4235 hideAllNativeCarrierMethods(); 4236 hideFixedRateRowsByLabel(); 4237 markPassthroughRows(); 4238 watchForFixedSlots(); 4239 installFixedSelectionLogger(); 4240 installReviewShippingChargeRelabel(); 4241 initNet30CustomerIdCapture(); 4242 checkNet30Eligibility(); 4243 installNet30PoGuard(); 4244 4245 // Capture the cart + zip and re-quote on cart / address change. 4246 installXHRInterceptor(); 4247 installCartChangeListener(); 4248 4249 // Fire an initial quote against whatever the DOM + cart API 4250 // already know. Covers the saved-address re-entry case where 4251 // Zoho doesn't fire the /checkout/address XHR (so the XHR 4252 // interceptor's cart/zip capture doesn't run). Without this, 4253 // a customer who edits the cart on /cart and returns to 4254 // /checkout sees the previous tier with a now-stale rate. 4255 fireInitialQuote(); 4256 } 4257 4258 // Reset state so the next quote runs against the current cart. 4259 function resetForRequote(reason) { 4260 log(`Re-quoting (${reason})...`); 4261 // Abort any in-flight quote so its stale response can't race 4262 // with the new quote we're about to fire. 4263 if (currentCarrierAbortCtrl) { 4264 try { currentCarrierAbortCtrl.abort(); } catch (e) {} 4265 currentCarrierAbortCtrl = null; 4266 } 4267 if (currentFreightAbortCtrl) { 4268 try { currentFreightAbortCtrl.abort(); } catch (e) {} 4269 currentFreightAbortCtrl = null; 4270 } 4271 // Capture the customer's currently-selected service BEFORE hiding 4272 // fixed rows (which unchecks the input). The applyFedExFixedTier / 4273 // applyFreightFixedTier call after the new response re-selects the 4274 // same service on its new tier if it's still in the visible set. 4275 const checkedInput = document.querySelector( 4276 '#zs-shipping-methods-form input.shipping-method:checked' 4277 ); 4278 if (checkedInput && checkedInput.dataset.fxFedexService) { 4279 previouslySelectedFedexService = checkedInput.dataset.fxFedexService; 4280 fxLog('Capturing service for re-quote persistence: ' 4281 + previouslySelectedFedexService); 4282 } 4283 // Clear the dedupe key so the next state-change fires a fresh quote. 4284 lastFedexQuoteKey = null; 4285 // Leave freight mode â the re-quote decides anew whether to re-enter 4286 // it. Until then the light-carrier rows are visible again. 4287 freightModeActive = false; 4288 // Reset fixed-method visibility to default (all hidden). The 4289 // matching row is un-hidden after the new response lands. 4290 hideAllFixedSlots(); 4291 hideAllNativeCarrierMethods(); 4292 hideFixedRateRowsByLabel(); 4293 markPassthroughRows(); 4294 watchForFixedSlots(); 4295 } 4296 4297 // Decoupled carrier-quote trigger. This fires whenever we have a 4298 // captured zip + cart, deduped against the last-fired (zip|cart-sig) 4299 // so repeated XHRs with the same state don't re-fire. Renamed from 4300 // maybeFireFedExTestQuote when the orchestrator replaced the 4301 // per-carrier fanout -- the function name was a holdover from the 4302 // initial FedEx-only freight-skip test wiring. 4303 function maybeFireCarrierQuote(reason) { 4304 // A zip is required for US/CA; an international destination may 4305 // quote country-only (no-postal countries, or a saved intl 4306 // address whose postal the DOM scrape can't shape-match). 4307 if (!capturedShippingZip && !isInternationalDest(capturedShippingCountry)) return; 4308 if (!capturedCartItems || capturedCartItems.length === 0) return; 4309 const sig = capturedCartItems 4310 .map(function (i) { return (i.sku || '?') + 'x' + (i.qty || 1
4310); }) 4311 .sort() 4312 .join(','); 4313 const key = (capturedShippingZip || '') + '|' + capturedShippingCountry + '|' + sig; 4314 if (key === lastFedexQuoteKey) return; 4315 lastFedexQuoteKey = key; 4316 log('Carrier quote triggered (' + reason + ') key=' + key); 4317 fireCarrierQuote(capturedShippingZip || '', capturedCartItems); 4318 } 4319 4320 // Initial-load quote: when /checkout loads with a saved address 4321 // (e.g. the customer just bounced to /cart, removed an item, and 4322 // navigated back), Zoho doesn't fire the /checkout/address XHR -- 4323 // so installXHRInterceptor's subscribe callback never fires and 4324 // capturedShippingZip / capturedCartItems stay null. The address 4325 // IS rendered in the initial /checkout HTML as 4326 // <div class="order-review-address-head">Shipping Address</div> 4327 // followed by <p> lines with the street/city/state-zip, so we 4328 // can pull it from the DOM directly. Pair that with a /storefront 4329 // /api/v1/cart fetch (the same path tryCartAPI takes) and we can 4330 // drive maybeFireCarrierQuote without needing the XHR. 4331 // 4332 // Bounded retry: poll for .order-review-address-head up to 4333 // MAX_ATTEMPTS * INTERVAL_MS ~= 5 s, then give up. If the address 4334 // never renders (no saved address, or Zoho changed its markup), 4335 // the XHR interceptor still runs in the normal address-entered 4336 // path -- this path is purely an additive trigger. 4337 async function fireInitialQuote() { 4338 var MAX_ATTEMPTS = 25; 4339 var INTERVAL_MS = 200; 4340 try { 4341 // Poll until the shipping address VALUES are parseable -- not just 4342 // until the .order-review-address-head element exists. Zoho renders 4343 // the heading first and fills in the <p> address lines (incl. the 4344 // zip) a beat later, so reading the DOM the instant the heading 4345 // appears yields an empty address block: parseShippingZipFromHTML 4346 // returns null and the old code gave up after a single try. On a 4347 // returning customer (cart unchanged -> Zoho fires no 4348 // /checkout/address XHR) that left the quote un-fired until the cart 4349 // was touched. Re-parsing each tick lets a late-populated address 4350 // still trigger the quote. Bounded at MAX_ATTEMPTS * INTERVAL_MS 4351 // (~5 s); a guest / no-saved-address load simply never parses a zip 4352 // and we skip -- the XHR interceptor still quotes once the address 4353 // is entered. 4354 // Country is re-parsed each tick alongside the zip: a saved 4355 // international address only yields a postal once its country 4356 // line has rendered (country-specific extraction is gated on 4357 // it), so both can appear on a later tick than the heading. 4358 // 4359 // COUNTRY_ONLY_AFTER: a saved international address may NEVER 4360 // yield a shape-matchable postal from the DOM â no-postal 4361 // countries (Hong Kong, UAE, ...) have nothing to match, and 4362 // the scrape only knows the US/CA/BE shapes. Once the country 4363 // line has rendered, give the zip a few more ticks and then 4364 // fire country-only â USPS international rates by country, so 4365 // a partial quote beats stranding the customer. The XHR path 4366 // upgrades to the exact postal as soon as the address is 4367 // touched (structured body capture). 4368 var COUNTRY_ONLY_AFTER = 10; 4369 var attempts = 0; 4370 var html = document.body.outerHTML; 4371 var country = parseShippingCountryFromHTML(html); 4372 var zip = parseShippingZipFromHTML(html, country); 4373 while (!zip) { 4374 if (isInternationalDest(country) && attempts >= COUNTRY_ONLY_AFTER) { 4375 // Prefer the postal this tab captured before a reload 4376 // (sessionStorage) over a country-only downgrade; the 4377 // store is only trusted when its country matches the 4378 // one rendered on screen. 4379 zip = storedZipForCountry(country); 4380 log(zip 4381 ? 'Initial-load quote: restored postal for ' + country 4382 + ' from session store.' 4383 : 'Initial-load quote: country ' + country + ' rendered but no ' 4384 + 'shape-matchable postal -- firing country-only.'); 4385 break; 4386 } 4387 if (++attempts >= MAX_ATTEMPTS) { 4388 // Last resort before skipping: the address this tab 4389 // captured before a reload (sessionStorage). A 4390 // mid-checkout refresh renders the address as a recap 4391 // block the scrape can't shape-match and fires no 4392 // /checkout/address XHR, so neither capture path runs 4393 // and every fixed row stays hidden (observed 4394 // 2026-07-29: Dispatch-step reload -> blank Shipping 4395 // Methods; HAR showed zero stockdata calls). When the 4396 // page yielded a country, trust the store only on a 4397 // country match (storedZipForCountry); when it yielded 4398 // nothing, trust the whole stored address -- only this 4399 // tab can have written it, and an address change after 4400 // the restore re-captures + re-quotes via the XHR path. 4401 var stored = storedCapturedAddress(); 4402 if (country) { 4403 zip = storedZipForCountry(country); 4404 } else if (stored) { 4405 zip = stored.zip || ''; 4406 country = stored.country || ''; 4407 if (!capturedShippingCity && stored.city) capturedShippingCity = stored.city; 4408 if (!capturedShippingState && stored.state) capturedShippingState = stored.state; 4409 } 4410 if (zip || isInternationalDest(country)) { 4411 log('Initial-load quote: DOM scrape empty; restored ' 4412 + 'address from session store (zip=' + (zip || '') 4413 + ' country=' + (country || '') + ').'); 4414 break; 4415 } 4416 log('Initial-load quote: no shipping zip parseable after ' 4417 + (MAX_ATTEMPTS * INTERVAL_MS) + ' ms -- skipping ' 4418 + '(guest / no saved address, or Zoho markup changed).'); 4419 return; 4420 } 4421 await new Promise(function (r) { setTimeout(r, INT
4421ERVAL_MS); }); 4422 html = document.body.outerHTML; 4423 country = parseShippingCountryFromHTML(html); 4424 zip = parseShippingZipFromHTML(html, country); 4425 } 4426 var cityState = parseShippingCityStateFromHTML(html); 4427 capturedShippingZip = zip; 4428 if (country) capturedShippingCountry = country; 4429 if (cityState.state) capturedShippingState = cityState.state; 4430 if (cityState.city) capturedShippingCity = cityState.city; 4431 log('Initial-load quote: captured zip=' + zip 4432 + ' country=' + (capturedShippingCountry || '') 4433 + ' city=' + (capturedShippingCity || '') 4434 + ' state=' + (capturedShippingState || '')); 4435 saveCapturedAddress(); 4436 4437 var items = await tryCartAPI(); 4438 if (!items || items.length === 0) { 4439 log('Initial-load quote: cart API returned no items -- skipping.'); 4440 return; 4441 } 4442 capturedCartItems = items; 4443 4444 maybeFireCarrierQuote('initial load'); 4445 } catch (e) { 4446 console.warn('[Freight] initial-load quote error (non-critical):', e); 4447 } 4448 } 4449 4450 // Re-quote when the cart changes mid-checkout (e.g. heavy items removed) 4451 // so a stale LTL quote doesn't stick. Debounced 400 ms. Always fires 4452 // resetForRequote regardless of whether the previous quote finished â 4453 // resetForRequote aborts any in-flight fetch so the older quote can't 4454 // race against the new one. (Previously this gated on quoteCompleted, 4455 // which caused intermittent failures when the customer changed the 4456 // cart while the prior quote was still in flight.) 4457 function installCartChangeListener() { 4458 const cartEvents = [ 4459 'zs-event-add-to-cart-success', 'zs-event-cart-updated', 'zs-event-cart-update-success', 4460 'zs-event-update-cart-success', 'zs-cart-updated', 'cart:updated', 'cart:added', 4461 ]; 4462 let cartChangeTimer = null; 4463 cartEvents.forEach(ev => { 4464 document.addEventListener(ev, () => { 4465 clearTimeout(cartChangeTimer); 4466 cartChangeTimer = setTimeout(async () => { 4467 try { 4468 resetForRequote('cart changed'); 4469 // resetForRequote cleared lastFedexQuoteKey. Refresh 4470 // cart from the cart API (cart-update events don't go 4471 // through /checkout/address so the XHR interceptor 4472 // doesn't pick up the new cart automatically) and 4473 // then fire the FedEx test for the fresh state. 4474 const items = await tryCartAPI(); 4475 if (items && items.length > 0) capturedCartItems = items; 4476 maybeFireCarrierQuote('cart changed'); 4477 } catch (e) { 4478 console.warn('[Freight] cart-change handler error (non-critical):', e); 4479 } 4480 }, 400); 4481 }); 4482 }); 4483 log('Cart-change re-quote listener installed.'); 4484 } 4485 4486 // ââ XHR Interceptor ââââââââââââââââââââââââââââââââââââââââââââ 4487 4488 function installXHRInterceptor() { 4489 if (!window.__cxAddr) { log('Shared XHR interceptor (__cxAddr) missing â freight cart/zip capture disabled.'); return; } 4490 window.__cxAddr.subscribe(function (xhr, data) { 4491 try { 4492 const review = (data && data.payload && data.payload.checkout_order_review) || ''; 4493 const items = parseCartItemsFromHTML(review); 4494 if (items.length > 0) { 4495 capturedCartItems = items; 4496 log('Captured cart items from XHR:', items); 4497 } 4498 4499 const cityState = parseShippingCityStateFromHTML(review); 4500 if (cityState.state) capturedShippingState = cityState.state; 4501 if (cityState.city) capturedShippingCity = cityState.city; 4502 4503 // Country BEFORE zip â country-specific postal extraction 4504 // only runs when the country is known. The XHR request 4505 // body (structured shipping_address) wins for BOTH fields: 4506 // its postal is format-agnostic, so every country works 4507 // without an HTML-scrape regex. The rendered block covers 4508 // the saved-address (address_id-only) posts. An 4509 // unrecognised country leaves the previous value so a Zoho 4510 // markup change degrades to format inference rather than 4511 // clearing a good capture. 4512 const bodyAddr = parseShippingAddressFromXHRBody(xhr._cxBody); 4513 const country = bodyAddr.country || parseShippingCountryFromHTML(review); 4514 const previousCountry = capturedShippingCountry; 4515 if (country) { 4516 capturedShippingCountry = country; 4517 if (country !== previousCountry) {
4518 log('Captured shipping country from XHR:', country); 4519 } 4520 } 4521 4522 // Structured postal first; HTML scrape (US/CA/BE formats) 4523 // as fallback. For an international destination an empty 4524 // capture is still quotable â a long tail of countries has 4525 // no postal codes at all (Hong Kong, UAE, Qatar, ...), so 4526 // country-only is a valid captured state there. 4527 let zip = bodyAddr.postal 4528 || parseShippingZipFromHTML(review, capturedShippingCountry); 4529 // Reload gap-filler: nothing captured in-memory yet and the 4530 // page can't yield a postal (saved intl address) â restore 4531 // the one this tab captured before the reload. 4532 if (!zip && !capturedShippingZip) { 4533 zip = storedZipForCountry(country); 4534 if (zip) log('Restored shipping zip from session store:', zip); 4535 } 4536 const previousZip = capturedShippingZip; 4537 if (zip) { 4538 capturedShippingZip = zip; 4539 if (zip !== previousZip) log('Captured shipping zip from XHR:', zip); 4540 } 4541 4542 // If the address (zip or country) changed since we last 4543 // saw one, re-quote. Drops the previous `quoteCompleted` 4544 // gate because resetForRequote now aborts the in-flight 4545 // fetch, so racing an in-flight quote against a new one is 4546 // no longer a concern. (The previous* checks keep the 4547 // first capture on a fresh page load from triggering a 4548 // spurious re-quote.) 4549 if ((previousZip && zip && previousZip !== zip) 4550 || (previousCountry && country && country !== previousCountry)) { 4551 resetForRequote('address changed'); 4552 } 4553 if (zip || country) saveCapturedAddress(); 4554 4555 // Fire the FedEx test quote independently of the freight 4556 // chain. Zoho excludes our pool slots from very-light carts 4557 // (< 1 lb), so the old "after freight skip:true" trigger 4558 // never ran for those â but FedEx test mode should still 4559 // exercise the endpoint for observability. Deduped via key. 4560 maybeFireCarrierQuote('XHR captured zip/cart'); 4561 // NOTE: We do NOT trigger quote here. We wait for pool 4562 // slots to appear in the DOM (Dispatch step), which means 4563 // the customer has confirmed their address selection. 4564 } catch (e) { 4565 log('XHR subscriber error:', e.message); 4566 } 4567 }); 4568 log('Freight XHR capture subscribed.'); 4569 } 4570 4571 function parseCartItemsFromHTML(html) { 4572 if (!html) return []; 4573 const items = []; 4574 const doc = new DOMParser().parseFromString(html, 'text/html'); 4575 4576 doc.querySelectorAll('.theme-cart-view-list, [data-zs-checkout-cart-item]').forEach(row => { 4577 // Prefer the .theme-cart-view-product-name link â its textContent 4578 // is the SKU/name. Fall back to any /products/ link's textContent. 4579 // Final fallback: <img title> attribute, since Zoho's cart-item 4580 // HTML wraps a thumbnail in an <a href="/products/..."> as the 4581 // FIRST element in the row â querySelector with a comma-selector 4582 // would otherwise pick THAT image link (document order wins) and 4583 // its textContent is empty because <img> has no text content, 4584 // resulting in zero cart items captured. (re-quote fix) 4585 const namedLink = row.querySelector('.theme-cart-view-product-name'); 4586 const anyProductLink = row.querySelector('a[href*="/products/"]'); 4587 const imgWithTitle = row.querySelector('img[title]'); 4588 const name = 4589 (namedLink && namedLink.textContent && namedLink.textContent.trim()) || 4590 (anyProductLink && anyProductLink.textContent && anyProductLink.textContent.trim()) || 4591 (imgWithTitle && imgWithTitle.title && imgWithTitle.title.trim()) || 4592 ''; 4593 let qty = 1;
4594 const qtyWrap = row.querySelector('.theme-cart-view-product-quantity-wrap'); 4595 if (qtyWrap) { 4596 qtyWrap.querySelectorAll('span').forEach(s => { 4597 const val = parseInt(s.textContent.trim()); 4598 if (!isNaN(val) && val > 0 && s.textContent.trim().length < 5) qty = val; 4599 }); 4600 } 4601 if (name) items.push({ sku: name, qty, name }); 4602 }); 4603 return items; 4604 } 4605 4606 // Pull a destination postal identifier out of an address-block text: 4607 // a US ZIP (5 digits) or a Canadian postal code (A1A 1A1). Returns the 4608 // LAST match â in an address block the postal code follows the street, 4609 // so "last" skips a 5-digit street number (e.g. "20724 184th Ave"). 4610 // US -> 5 digits; CA -> 6 chars uppercase, space removed. The server 4611 // (postal.php) infers the country from this format. 4612 // 4613 // `country` (optional ISO-2) switches on country-specific formats that 4614 // are too ambiguous to match unconditionally: Belgium is 4 digits 4615 // (1000-9999), which unguarded would also match street numbers and 4616 // phone-number fragments in US/CA address blocks. BE parsing therefore 4617 // runs ONLY when the captured country says BE; the hyphen/digit 4618 // adjacency guards skip phone fragments like "432-0003" and [1-9] 4619 // skips leading-zero tokens (no Belgian code starts with 0). No 4620 // regex lookbehind â a lookbehind literal is an early SyntaxError on 4621 // older Safari and would take the whole script block down with it. 4622 function extractPostalFromText(text, country) { 4623 if (!text) return null; 4624 if (country === 'BE') { 4625 const beRe = /(^|[^-\d])([1-9]\d{3})(?![-\d])/g; 4626 let beMatch, lastBe = null; 4627 while ((beMatch = beRe.exec(text)) !== null) lastBe = beMatch[2]; 4628 return lastBe; 4629 } 4630 const matches = text.match( 4631 /\b\d{5}(?:-\d{4})?\b|\b[A-Za-z]\d[A-Za-z]\s?\d[A-Za-z]\d\b/g 4632 ); 4633 if (!matches || !matches.length) return null; 4634 const last = matches[matches.length - 1].trim().toUpperCase(); 4635 return /^\d/.test(last) 4636 ? last.substring(0, 5) // US ZIP 4637 : last.replace(/\s+/g, ''); // CA postal -> compact 6 chars 4638 } 4639 4640 function parseShippingZipFromHTML(html, country) { 4641 if (!html) return null; 4642 const doc = new DOMParser().parseFromString(html, 'text/html'); 4643 4644 const headings = doc.querySelectorAll('.order-review-address-head'); 4645 for (const heading of headings) { 4646 if (heading.textContent.trim().toLowerCase().includes('shipping')) { 4647 const addressDiv = heading.nextElementSibling 4648 || heading.parentElement?.querySelector('div'); 4649 if (addressDiv) { 4650 const postal = extractPostalFromText(addressDiv.textContent || '', country); 4651 if (postal) return postal; 4652 } 4653 } 4654 } 4655 4656 const idx = html.indexOf('Shipping Address'); 4657 if (idx !== -1) { 4658 const postal = extractPostalFromText(html.substring(idx, idx + 500), country); 4659 if (postal) return postal; 4660 } 4661 4662 return null; 4663 } 4664 4665 // US state + CA province full-name -> 2-letter code. Zoho renders 4666 // the address with the full state NAME ("Washington"); UPS's 4667 // StateProvinceCode must be the 2-letter form ("WA"). 4668 const STATE_CODE_BY_NAME = { 4669 'alabama':'AL','alaska':'AK','arizona':'AZ','arkansas':'AR', 4670 'california':'CA','colorado':'CO','connecticut':'CT','delaware':'DE', 4671 'district of columbia':'DC','florida':'FL','georgia':'GA','hawaii':'HI', 4672 'idaho':'ID','illinois':'IL','indiana':'IN','iowa':'IA','kansas':'KS', 4673 'kentucky':'KY','louisiana':'LA','maine':'ME','maryland':'MD', 4674 'massachusetts':'MA','michigan':'MI','minnesota':'MN','mississippi':'MS', 4675 'missouri':'MO','montana':'MT','nebraska':'NE','nevada':'NV', 4676 'new hampshire':'NH','new jersey':'NJ','new mexico':'NM','new york':'NY',
4677 'north carolina':'NC','north dakota':'ND','ohio':'OH','oklahoma':'OK', 4678 'oregon':'OR','pennsylvania':'PA','rhode island':'RI', 4679 'south carolina':'SC','south dakota':'SD','tennessee':'TN','texas':'TX', 4680 'utah':'UT','vermont':'VT','virginia':'VA','washington':'WA', 4681 'west virginia':'WV','wisconsin':'WI','wyoming':'WY','puerto rico':'PR', 4682 'alberta':'AB','british columbia':'BC','manitoba':'MB', 4683 'new brunswick':'NB','newfoundland and labrador':'NL', 4684 'nova scotia':'NS','northwest territories':'NT','nunavut':'NU', 4685 'ontario':'ON','prince edward island':'PE','quebec':'QC', 4686 'saskatchewan':'SK','yukon':'YT', 4687 }; 4688 4689 // Resolve a state/province token (full name OR a 2-letter code) to 4690 // its 2-letter code. Returns '' for anything unrecognised. 4691 function toStateCode(token) { 4692 let t = (token || '').trim(); 4693 // A "City, ST" form slipping through â keep the part after the comma. 4694 if (t.indexOf(',') !== -1) t = t.split(',').pop().trim(); 4695 if (/^[A-Za-z]{2}$/.test(t)) return t.toUpperCase(); 4696 return STATE_CODE_BY_NAME[t.toLowerCase()] || ''; 4697 } 4698 4699 // Countries the quote pipeline ships to. Maps the country as Zoho 4700 // renders/posts it (full name, or already an ISO-2 code) to ISO-2. 4701 // 145-country roster (Phase 0 of the worldwide rollout) -- keys are 4702 // Zoho's exact country names, lowercased. A country only becomes 4703 // customer-reachable when it is ALSO enabled in the Zoho shipping 4704 // zones; this map just lets the pipeline understand it. US 4705 // territories keep their own ISO codes (FedEx/UPS address them that 4706 // way); usps_quote.php maps them back onto its domestic endpoint. 4707 // 4708 // DELIBERATELY EXCLUDED (Devin, 2026-07-13) -- do not add here and 4709 // do not zone-enable: Syria, Yemen, Afghanistan, Libya, Somalia, 4710 // Venezuela, Myanmar (sanctions / suspended carrier service; OFAC 4711 // exposure for electronics), Iraq, Lebanon, Haiti (carrier service 4712 // disruptions), and Antarctica (unserviceable). 4713 const COUNTRY_CODE_BY_NAME = { 4714 'united states': 'US', 'united states of america': 'US', 'usa': 'US', 4715 'albania': 'AL', 'algeria': 'DZ', 4716 'american samoa': 'AS', 4717 'antigua and barbuda': 'AG', 'argentina': 'AR', 'armenia': 'AM', 4718 'australia': 'AU', 'austria': 'AT', 'bahamas': 'BS', 4719 'bahrain': 'BH', 'barbados': 'BB', 'belgium': 'BE', 'belize': 'BZ', 4720 'benin': 'BJ', 'bermuda': 'BM', 'bhutan': 'BT', 'botswana': 'BW', 4721 'brazil': 'BR', 'brunei': 'BN', 'bulgaria': 'BG', 'cambodia': 'KH', 4722 'cameroon': 'CM', 'canada': 'CA', 'cayman islands': 'KY', 4723 'chile': 'CL', 'china': 'CN', 'christmas island': 'CX', 4724 'cocos (keeling) islands': 'CC', 'colombia': 'CO', 4725 'cook islands': 'CK', 'costa rica': 'CR', 'croatia': 'HR', 4726 'czech republic': 'CZ', 'denmark': 'DK', 'djibouti': 'DJ', 4727 'dominica': 'DM', 'dominican republic': 'DO', 'ecuador': 'EC', 4728 'egypt': 'EG', 'el salvador': 'SV', 'estonia': 'EE', 4729 'ethiopia': 'ET', 'falkland islands (malvinas)': 'FK', 4730 'fiji': 'FJ', 'finland': 'FI', 'france': 'FR', 4731 'french guiana': 'GF', 'french polynesia': 'PF', 'gabon': 'GA', 4732 'gambia': 'GM', 'georgia': 'GE', 'germany': 'DE', 'ghana': 'GH', 4733 'greece': 'GR', 'greenland': 'GL', 'grenada': 'GD', 4734 'guam': 'GU', 'guatemala': 'GT', 'honduras': 'HN', 4735 'hong kong': 'HK', 'hungary': 'HU', 'iceland': 'IS', 'india': 'IN', 4736 'indonesia': 'ID', 'ireland': 'IE', 'israel': 'IL', 4737 'italy': 'IT', 'jamaica': 'JM', 'japan': 'JP', 'kazakhstan': 'KZ', 4738 'kenya': 'KE', 'kuwait': 'KW', 'kyrgyzstan': 'KG', 'latvia': 'LV', 4739 'liberia': 'LR', 'lithuania': 'LT', 4740 'madagascar': 'MG', 'malaysia': 'MY', 'martinique': 'MQ', 4741 'mauritania': 'MR', 'mexico': 'MX', 'micronesia': 'FM', 4742 'mongolia': 'MN', 'montenegro': 'ME', 'morocco': 'MA', 4743 'mozambique': 'MZ', 'namibia': 'NA', 4744 'nauru': 'NR', 'nepal': 'NP', 'netherlands': 'NL', 4745 'new caledonia': 'NC', 'new zealand': 'NZ', 'nicaragua': 'NI', 4746 'niger': 'NE', 'nigeria': 'NG', 4747 'northern mariana islands': 'MP', 'norway': 'NO', 'oman': 'OM', 4748 'pakistan': 'PK', 'panama': 'PA', 'papua new guinea': 'PG', 4749 'paraguay': 'PY', 'peru': 'PE', 'philippines': 'PH', 4750 'poland': 'PL', 'portugal': 'PT', 'puerto rico': 'PR', 4751 'qatar': 'QA', 'romania': 'RO', 'rwanda': 'RW',
4752 'saint kitts and nevis': 'KN', 'saint lucia': 'LC', 'samoa': 'WS', 4753 'saudi arabia': 'SA', 'serbia': 'RS', 'sierra leone': 'SL', 4754 'singapore': 'SG', 'south africa': 'ZA', 4755 'south georgia and the south sandwich islands': 'GS', 4756 'south korea': 'KR', 'spain': 'ES', 'sri lanka': 'LK', 4757 'swaziland': 'SZ', 'sweden': 'SE', 'switzerland': 'CH', 4758 'taiwan': 'TW', 'tajikistan': 'TJ', 4759 'tanzania': 'TZ', 'thailand': 'TH', 'tonga': 'TO', 4760 'trinidad and tobago': 'TT', 'turkey': 'TR', 4761 'turks and caicos islands': 'TC', 'tuvalu': 'TV', 'uganda': 'UG', 4762 'ukraine': 'UA', 'united arab emirates': 'AE', 4763 'united kingdom': 'GB', 4764 'united states minor outlying islands': 'UM', 'uruguay': 'UY', 4765 'vietnam': 'VN', 4766 'virgin islands (british)': 'VG', 'virgin islands (u.s.)': 'VI', 4767 'zambia': 'ZM', 'zimbabwe': 'ZW', 4768 }; 4769 4770 // A destination outside US/CA rides the international handling: 4771 // pass-through postal validation server-side, country-only quotes 4772 // when no postal was captured, the manual-quote notice on a 4773 // zero-service round, and the freight unsupported_country gate. 4774 function isInternationalDest(code) { 4775 return !!code && code !== 'US' && code !== 'CA'; 4776 } 4777 4778 // Normalise a country token â an ISO-2 code ('BE') or a full name 4779 // ('Belgium') â to ISO-2. Returns '' for anything unrecognised so 4780 // callers fall back to the server's format inference. 4781 function toCountryCode(token) { 4782 const t = (token || '').trim(); 4783 if (!t) return ''; 4784 if (/^[A-Za-z]{2}$/.test(t)) { 4785 const up = t.toUpperCase(); 4786 for (const name in COUNTRY_CODE_BY_NAME) { 4787 if (COUNTRY_CODE_BY_NAME[name] === up) return up; 4788 } 4789 return ''; 4790 } 4791 return COUNTRY_CODE_BY_NAME[t.toLowerCase()] || ''; 4792 } 4793 4794 // Pull the destination country from the rendered shipping-address 4795 // block: scan its <p> lines bottom-up (Zoho renders the country as 4796 // the last line when it shows one) for a known country name. Returns 4797 // '' when no line matches â e.g. a US block with no country line â 4798 // which keeps today's format-inference behavior. 4799 function parseShippingCountryFromHTML(html) { 4800 if (!html) return ''; 4801 const doc = new DOMParser().parseFromString(html, 'text/html'); 4802 const headings = doc.querySelectorAll('.order-review-address-head'); 4803 for (const heading of headings) { 4804 if (!heading.textContent.trim().toLowerCase().includes('shipping')) { 4805 continue; 4806 } 4807 const addressDiv = heading.nextElementSibling 4808 || (heading.parentElement && heading.parentElement.querySelector('div')); 4809 if (!addressDiv) continue; 4810 const lines = Array.from(addressDiv.querySelectorAll('p')) 4811 .map(function (p) { return (p.textContent || '').trim(); }) 4812 .filter(function (t) { return t; }); 4813 for (let i = lines.length - 1; i >= 0; i--) { 4814 const code = COUNTRY_CODE_BY_NAME[lines[i].toLowerCase()]; 4815 if (code) return code; 4816 } 4817 } 4818 return ''; 4819 } 4820 4821 // Pull the destination country AND postal code from the 4822 // /checkout/address XHR REQUEST body (shipping_address) â the 4823 // structured source, available when the customer enters/edits an 4824 // address. The postal from here is format-agnostic: it works for 4825 // every country without the HTML-scrape regexes, which only know 4826 // US/CA/BE shapes. Selecting a saved address posts only an 4827 // address_id, so callers still need the rendered-block fallback. 4828 // Returns { country: ISO-2 or '', postal: trimmed string or '' }. 4829 function parseShippingAddressFromXHRBody(body) { 4830 try { 4831 const data = JSON.parse(body); 4832 const addr = data && data.shipping_address; 4833 if (!addr) return { country: '', postal: '' }; 4834 const postalRaw = addr.zip || addr.zip_code || addr.zipcode 4835 || addr.postal_code || addr.pincode || ''; 4836 return { 4837 country: toCountryCode(addr.country || addr.country_code || ''), 4838 postal: String(postalRaw).trim().slice(0, 12), 4839 }; 4840 } catch (e) { return { country: '', postal: '' }; } 4841 } 4842 4843 // Pull "City" + 2-letter state/province from the shipping address 4844 // block. Zoho renders the address as separate <p> lines, e.g. 4845 // <p>511 N 80th Ave</p><p>Yakima</p><p>Washington 98908</p> 4846 // â so the city is its own <p> and the state shares the next <p> 4847 // with the postal code. ups_quote.php needs city + state for the 4848 // ZIPs UPS won't resolve from the zip alone; an empty result here
4849 // falls back to zip-only. Returns { city, state } ('' when absent). 4850 function parseShippingCityStateFromHTML(html) { 4851 if (!html) return { city: '', state: '' }; 4852 const doc = new DOMParser().parseFromString(html, 'text/html'); 4853 const headings = doc.querySelectorAll('.order-review-address-head'); 4854 for (const heading of headings) { 4855 if (!heading.textContent.trim().toLowerCase().includes('shipping')) { 4856 continue; 4857 } 4858 const addressDiv = heading.nextElementSibling 4859 || (heading.parentElement && heading.parentElement.querySelector('div')); 4860 if (!addressDiv) continue; 4861 const lines = Array.from(addressDiv.querySelectorAll('p')) 4862 .map(function (p) { return (p.textContent || '').trim(); }) 4863 .filter(function (t) { return t; }); 4864 // The state+postal line ends with a US ZIP or a CA postal code; 4865 // the text before it is the state, the prior <p> is the city. 4866 for (let i = 0; i < lines.length; i++) { 4867 const m = lines[i].match( 4868 /^(.+?)\s+(?:\d{5}(?:-\d{4})?|[A-Za-z]\d[A-Za-z]\s?\d[A-Za-z]\d)$/ 4869 ); 4870 if (m) { 4871 return { 4872 city: i > 0 ? lines[i - 1] : '', 4873 state: toStateCode(m[1]), 4874 }; 4875 } 4876 } 4877 } 4878 return { city: '', state: '' }; 4879 } 4880 4881 async function tryCartAPI() { 4882 try { 4883 const resp = await fetch('/storefront/api/v1/cart', { 4884 credentials: 'same-origin', 4885 headers: { 'Accept': 'application/json' }, 4886 }); 4887 if (!resp.ok) return null; 4888 const data = await resp.json(); 4889 4890 for (const items of [ 4891 data?.payload?.cart?.items, data?.payload?.items, 4892 data?.cart?.items, data?.items, 4893 ]) { 4894 if (Array.isArray(items) && items.length > 0) { 4895 return items.map(i => ({ 4896 sku: i.sku || i.variant_sku || i.name || '', 4897 qty: i.quantity || i.qty || 1, 4898 name: i.name || '', 4899 })).filter(i => i.sku); 4900 } 4901 } 4902 return null; 4903 } catch (e) { return null; } 4904 } 4905 4906 // ââ Quote orchestration ââââââââââââââââââââââââââââââââââââââââ 4907 // 4908 // fireCarrierQuote(zip, cart) is the light-cart entry point. It 4909 // POSTs to carrier_quote.php (a server-side orchestrator that fans 4910 // out to fedex_quote.php + ups_quote.php + usps_quote.php in parallel 4911 // via curl_multi, filters each by allowlist, rounds onto the fixed- 4912 // rate ladder, and applies PR #78's single-bump collision rule 4913 // before responding). 4914 // 4915 // Eligibility: carrier_quote.php returns eligible:false when the 4916 // cart routes to freight (>= 150 lb or any cf_force_freight item) -- 4917 // forwarded from whichever per-carrier endpoint detected it first. 4918 // The client then chains to fireFreightQuote() exactly as before. 4919 // 4920 // Hard timeout: 14 s (carrier_quote.php has 12 s per-carrier; the 4921 // orchestrator waits for the slowest leg). Empty services list on 4922 // error / abort so the customer never sees a stale or partial row. 4923 // 4924 // The legacy parallel-fanout client functions (fireFedExQuote / 4925 // fireUpsQuote / fireUspsQuote) and the per-round merge state 4926 // (carrierQuote / applyMergedCarrierTiers / betterFedExService / 4927 // showNativeUpsFallback / setUpsFallback) were retired in PR #78. 4928 // The three per-carrier endpoints stay reachable for isolated 4929 // debugging. See handoffs/fedex_fixed_rate_display.md and 4930 // handoffs/freight_fixed_rate.md. 4931 4932 function fireCarrierQuote(zip, cartItems) { 4933 // zip may be '' for an international country-only quote; the 4934 // orchestrator accepts either as long as one of zip/country is set. 4935 if ((!zip && !isInternationalDest(capturedShippingCountry)) 4936 || !cartItems || cartItems.length === 0) return;
4937 // Wipe any stale manual-quote "contact us" notice from a prior round; 4938 // it is re-shown below only if this round's freight fallback also 4939 // comes up empty (hazmat) or unsupported (international freight). 4940 clearManualQuoteNotice(); 4941 var ctrl = new AbortController(); 4942 currentCarrierAbortCtrl = ctrl; 4943 // 14 s -- absorbs carrier_quote.php's 12 s per-carrier curl_multi 4944 // timeout plus orchestrator boot + merge work. A slow carrier API 4945 // never blocks checkout past this ceiling. 4946 var timer = setTimeout(function () { ctrl.abort(); }, 14000); 4947 4948 // state/city help UPS resolve the ZIPs it won't geocode from the 4949 // ZIP alone; the orchestrator forwards them to ups_quote.php and 4950 // ignores them for FedEx + USPS. Omitted until the checkout 4951 // address is captured. country is the postal_parse() hint the 4952 // orchestrator forwards to ALL carriers â required for Belgium 4953 // (4-digit codes aren't format-inferable); omitted, the server 4954 // infers US/CA from the postal format as before. 4955 var url = CARRIER_QUOTE_ENDPOINT + '?zip=' + encodeURIComponent(zip); 4956 if (capturedShippingState) url += '&state=' + encodeURIComponent(capturedShippingState); 4957 if (capturedShippingCity) url += '&city=' + encodeURIComponent(capturedShippingCity); 4958 if (capturedShippingCountry) url += '&country=' + encodeURIComponent(capturedShippingCountry); 4959 4960 fetch(url, { 4961 method: 'POST', 4962 headers: { 'Content-Type': 'application/json' }, 4963 body: JSON.stringify({ 4964 items: cartItems.map(function (i) { return { sku: i.sku, qty: i.qty }; }) 4965 }), 4966 signal: ctrl.signal, 4967 }) 4968 .then(function (r) { 4969 // text() + JSON.parse so an aborted response (headers in, 4970 // body cut off) surfaces as a clean error with a preview. 4971 return r.text().then(function (text) { 4972 if (ctrl.signal.aborted) { 4973 var abortErr = new Error('aborted'); 4974 abortErr.name = 'AbortError'; 4975 throw abortErr; 4976 } 4977 if (!text) { 4978 throw new Error('empty response body (HTTP ' + r.status + ')'); 4979 } 4980 try { 4981 return JSON.parse(text); 4982 } catch (e) { 4983 console.warn('[Carrier] non-JSON response (first 200):', text.slice(0, 200)); 4984 throw e; 4985 } 4986 }); 4987 }) 4988 .then(function (data) { 4989 clearTimeout(timer); 4990 ingestFixedTierIds(data); 4991 // Heavy cart / force-freight item (or a hazmat cart no parcel 4992 // carrier can ship) -> server forwarded eligible:false. Chain to 4993 // the freight quote. 4994 if (data && data.eligible === false) { 4995 fxLog('Cart routes to freight -- chaining: ' + (data.reason || '')); 4996 applyFedExFixedTier([]); 4997 // hazmat_fallback:true means the orchestrator routed a hazmat 4998 // cart here because NO parcel carrier could ship it -- pass the 4999 // flag so fireFreightQuote() shows a "contact us" message if the 5000 // freight quote is ALSO empty, instead of stranding the customer. 5001 // forceFreight: a non-hazmat freight forward (heavy cart, or the 5002 // parcel-too-heavy sub-150 lb case where FedEx returned freight- 5003 // class services only) tells freight_quote.php to bypass its own 5004 // 150 lb gate. Hazmat carts are NOT forced -- a light hazmat cart 5005 // should still fall through to the contact notice. (PR #87 client 5006 // half, restored July 2026 after the paste-history hold cleared.) 5007 var forceFreight = data.force_freight === true && data.hazmat_fallback !== true; 5008 fireFreightQuote(zip, cartItems, data.hazmat_fallback === true, forceFreight); 5009 return; 5010 } 5011 var services = []; 5012 if (!data) { 5013 fxLog('Carrier: no data returned'); 5014 } else if (data.error) { 5015 console.warn('[Carrier] Error:', data.error, data.error_detail || ''); 5016 } else { 5017 services = Array.isArray(data.services) ? data.services : []; 5018 var statuses = Array.isArray(data.per_carrier_status) ? data.per_carrier_status : []; 5019 fxLog('Carrier: zip ' + data.destination_zip 5020 + ' / weight ' + data.total_weight 5021 + ' / packages=' + data.package_count 5022 + ' / merged=' + services.length 5023 + ' / per-carrier raw counts: ' 5024 + statuses.map(function (s) { 5025 return s.carrier + '=' + s.svc_count_raw
5026 + (s.svc_filtered_out ? '(-' + s.svc_filtered_out + ')' : ''); 5027 }).join(' ')); 5028 } 5029 applyFedExFixedTier(services); 5030 // International parcel dead-end: zero services to a non-US/CA 5031 // destination (carrier no-service lane, embargoed country, 5032 // errors) would otherwise strand the customer with an empty 5033 // methods list -- hand off to sales instead. US/CA keep the 5034 // pre-Phase-0 behavior (Zoho-native rows may still exist). 5035 if (services.length === 0 && isInternationalDest(capturedShippingCountry)) { 5036 fxLog('No parcel services for international destination -- showing contact notice'); 5037 showManualQuoteNotice('intl'); 5038 } 5039 }) 5040 .catch(function (err) { 5041 clearTimeout(timer); 5042 var aborted = (err && err.name === 'AbortError') || ctrl.signal.aborted; 5043 var msg = (err && err.message) || err; 5044 if (aborted) { 5045 fxLog('Carrier quote aborted (cart/address changed or timed out)'); 5046 } else { 5047 console.warn('[Carrier] Fetch failed (non-critical):', msg); 5048 } 5049 // Surface an empty services list so the renderer hides every 5050 // tier rather than leaving a stale one visible. 5051 applyFedExFixedTier([]); 5052 // A genuine (non-abort) failure strands an international 5053 // customer with no methods -- hand off to sales. Aborts are a 5054 // new round in flight, so skip them. 5055 if (!aborted && isInternationalDest(capturedShippingCountry)) { 5056 showManualQuoteNotice('intl'); 5057 } 5058 }) 5059 .finally(function () { 5060 if (currentCarrierAbortCtrl === ctrl) currentCarrierAbortCtrl = null; 5061 }); 5062 } 5063 5064 5065 // ââ Freight quote (heavy carts) ââââââââââââââââââââââââââââââââ 5066 // POSTs the cart to freight_quote.php, which quotes R&L + Old 5067 // Dominion and returns the cheaper carrier. applyFreightFixedTier() 5068 // rounds that rate up to a fixed tier. Chained from fireCarrierQuote() 5069 // when carrier_quote.php returns eligible:false (heavy cart, or the 5070 // hazmat-no-parcel fallback). 5071 function fireFreightQuote(zip, cartItems, hazmatFallback, forceFreight) { 5072 // zip may be '' for an international country-only quote â the 5073 // endpoint's unsupported_country gate answers it before needing one. 5074 if ((!zip && !isInternationalDest(capturedShippingCountry)) 5075 || !cartItems || cartItems.length === 0) return; 5076 var ctrl = new AbortController(); 5077 currentFreightAbortCtrl = ctrl; 5078 // 22 s â an LTL quote calls two carrier APIs and can take 1-3 s+. 5079 var timer = setTimeout(function () { ctrl.abort(); }, 22000); 5080 // Freight quotes are genuinely slow; show the deferred spinner. 5081 scheduleLoadingMessage(); 5082 5083 // force_freight=1 makes freight_quote.php bypass its own 150 lb gate 5084 // for a non-hazmat cart the orchestrator already routed to freight 5085 // (the parcel-too-heavy sub-150 lb case). country is the 5086 // postal_parse() hint; non-US/CA destinations come back as 5087 // unsupported_country (R&L / OD are North-America-only). 5088 var freightUrl = FREIGHT_QUOTE_ENDPOINT + '?zip=' + encodeURIComponent(zip) 5089 + (forceFreight ? '&force_freight=1' : '') 5090 + (capturedShippingCountry ? '&country=' + encodeURIComponent(capturedShippingCountry) : ''); 5091 fetch(freightUrl, { 5092 method: 'POST', 5093 headers: { 'Content-Type': 'application/json' }, 5094 body: JSON.stringify({ 5095 items: cartItems.map(function (i) { return { sku: i.sku, qty: i.qty }; }) 5096 }), 5097 signal: ctrl.signal, 5098 }) 5099 .then(function (r) { 5100 return r.text().then(function (text) { 5101 if (ctrl.signal.aborted) { 5102 var abortErr = new Error('aborted'); 5103 abortErr.name = 'AbortError'; 5104 throw abortErr; 5105 } 5106 if (!text) { 5107 throw new Error('empty response body (HTTP ' + r.status + ')'); 5108 } 5109 try { 5110 return JSON.parse(text); 5111 } catch (e) {
5112 console.warn('[Freight] non-JSON response (first 200 chars):', text.slice(0, 200)); 5113 throw e; 5114 } 5115 }); 5116 }) 5117 .then(function (data) { 5118 clearTimeout(timer); 5119 removeMessage(); 5120 ingestFixedTierIds(data); // learn the fixed-tier ID list 5121 // Freight-capable carriers (R&L / OD) only serve US + CA. A 5122 // freight-routed cart to any other country can never get an 5123 // online rate -- hand off to sales instead of leaving an 5124 // empty methods list. (hazmat wording when that's the cause.) 5125 if (data && data.unsupported_country === true) { 5126 fxLog('Freight unsupported for destination country -- showing contact notice'); 5127 applyFreightFixedTier(null); 5128 showManualQuoteNotice(hazmatFallback ? 'hazmat' : 'intl-freight'); 5129 return; 5130 } 5131 if (!data || data.eligible === false) { 5132 fxLog('Freight not applicable:', (data && data.reason) || 'no data'); 5133 applyFreightFixedTier(null); 5134 // Hazmat cart that couldn't be rated by parcel OR freight -> 5135 // hand off to sales instead of leaving an empty methods list. 5136 if (hazmatFallback) showManualQuoteNotice('hazmat'); 5137 return; 5138 } 5139 if (data.error || !Array.isArray(data.services) || data.services.length === 0) { 5140 console.warn('[Freight] quote error:', (data && data.error) || 'no services', 5141 (data && data.error_detail) || ''); 5142 applyFreightFixedTier(null); 5143 if (hazmatFallback) showManualQuoteNotice('hazmat'); 5144 return; 5145 } 5146 fxLog('Freight quote: ' + data.services.length + ' carrier(s) â ' 5147 + data.services.map(function (s) { return s.service_type; }).join(', ')); 5148 applyFreightFixedTier(data); 5149 }) 5150 .catch(function (err) { 5151 clearTimeout(timer); 5152 removeMessage(); 5153 var aborted = (err && err.name === 'AbortError') || ctrl.signal.aborted; 5154 var msg = (err && err.message) || err; 5155 if (aborted) { 5156 fxLog('Freight quote aborted (cart/address changed or timed out â non-critical)'); 5157 } else { 5158 console.warn('[Freight] Fetch failed (non-critical):', msg); 5159 } 5160 applyFreightFixedTier(null); 5161 // A genuine (non-abort) freight failure on a hazmat-fallback cart 5162 // still strands the customer -- show the contact notice. Aborts are 5163 // a new round in flight, so skip them. 5164 if (hazmatFallback && !aborted) showManualQuoteNotice('hazmat'); 5165 }) 5166 .finally(function () { 5167 if (currentFreightAbortCtrl === ctrl) currentFreightAbortCtrl = null; 5168 }); 5169 } 5170 5171 // ââ Freight fixed-tier display âââââââââââââââââââââââââââââââââ 5172 // Show a "fixed N" row for EACH freight carrier that returned a 5173 // valid quote (R&L and Old Dominion), each rounded onto its own tier 5174 // (server-side, in freight_quote.php) and relabeled with the carrier 5175 // name + ETA. A freight cart's only valid options are these freight 5176 // rows, so when any are shown freight mode is active and the FedEx / 5177 // UPS / native rows stay hidden. The svc objects are the FedEx-shaped 5178 // entries from data.services, so patchFixedRowLabel() and 5179 // logFixedSelection() work unchanged â service_type 'RL' / 'OD' is 5180 // what fixed_rate_log.php's $shipViaMap maps to a cf_ship_via option. 5181 // Pass null / no services to clear (hide everything) â the safe default. 5182 function applyFreightFixedTier(data) { 5183 // Capture the currently-selected service before we hide rows. 5184 const checkedInput = document.querySelector( 5185 '#zs-shipping-methods-form input.shipping-method:checked' 5186 ); 5187 if (checkedInput && checkedInput.dataset.fxFedexService) { 5188 previouslySelectedFedexService = checkedInput.dataset.fxFedexService; 5189 } 5190
5191 const services = (data && Array.isArray(data.services)) ? data.services : []; 5192 5193 // Round each freight carrier onto its tier. The ladder has one 5194 // row per price, so if both carriers land on the SAME tier keep 5195 // the cheaper one. 5196 const matchedRowsByTier = {}; 5197 services.forEach(svc => { 5198 if (!svc || !svc.tier_id) return; 5199 const existing = matchedRowsByTier[svc.tier_id]; 5200 if (existing && !(Number(svc.rate) < Number(existing.svc.rate))) { 5201 return; 5202 } 5203 matchedRowsByTier[svc.tier_id] = { 5204 svc: svc, 5205 tier: { id: svc.tier_id, name: svc.tier_name, rate: svc.tier_rate }, 5206 }; 5207 }); 5208 const tierIds = Object.keys(matchedRowsByTier); 5209 5210 if (tierIds.length === 0) { 5211 // No valid freight quote â clear to hide-all (the safe 5212 // default). Leaving freight mode re-shows the UPS / other 5213 // rows so a failed freight quote doesn't strand the customer. 5214 freightModeActive = false; 5215 visibleFixedSlotIds = new Set(); 5216 visibleFixedRowData = {}; 5217 applyFixedRateCSS(); 5218 fixedTierIds.forEach(id => { 5219 const row = getRowByShippingChargeId(id); 5220 if (row) setRowHiddenAndDisabled(row, true); 5221 }); 5222 return; 5223 } 5224 5225 // Enter freight mode: only these freight tier row(s) may show. 5226 freightModeActive = true; 5227 visibleFixedSlotIds = new Set(tierIds); 5228 visibleFixedRowData = matchedRowsByTier; 5229 applyFixedRateCSS(); 5230 5231 tierIds.forEach(id => { 5232 const entry = matchedRowsByTier[id]; 5233 const row = getRowByShippingChargeId(id); 5234 if (row) { 5235 setRowHiddenAndDisabled(row, false); 5236 patchFixedRowLabel(row, entry.svc, entry.tier); 5237 } else { 5238 fxLog('Freight tier ' + entry.tier.name 5239 + ' not in DOM yet; observer will patch on render'); 5240 } 5241 }); 5242 5243 // A freight cart has no valid non-freight option, so a freight 5244 // row must end up selected. Keep the customer's freight choice 5245 // across a re-quote if it's still offered; otherwise default to 5246 // the cheapest. `change` only â never `click` (which can make 5247 // Zoho advance to the Review step). 5248 if (previouslySelectedFedexService) { 5249 const target = document.querySelector( 5250 '#zs-shipping-methods-form input.shipping-method' 5251 + '[data-fx-fedex-service="' + fxCssEscape(previouslySelectedFedexService) + '"]' 5252 ); 5253 if (target && !target.disabled && !target.checked) { 5254 target.checked = true; 5255 target.dispatchEvent(new Event('change', { bubbles: true })); 5256 } 5257 } 5258 const stillChecked = document.querySelector( 5259 '#zs-shipping-methods-form input.shipping-method:checked' 5260 ); 5261 if (!stillChecked || !visibleFixedSlotIds.has(stillChecked.value)) { 5262 let cheapestId = tierIds[0]; 5263 tierIds.forEach(id => { 5264 if (Number(matchedRowsByTier[id].svc.rate) 5265 < Number(matchedRowsByTier[cheapestId].svc.rate)) { 5266 cheapestId = id; 5267 } 5268 }); 5269 const row = getRowByShippingChargeId(cheapestId); 5270 const input = row && row.querySelector('input.shipping-method'); 5271 if (input && !input.disabled && !input.checked) { 5272 input.checked = true; 5273 input.dispatchEvent(new Event('change', { bubbles: true })); 5274 } 5275 } 5276 5277 fxLog('Showing ' + tierIds.length + ' freight tier(s)'); 5278 } 5279 5280 // ââ Messages âââââââââââââââââââââââââââââââââââââââââââââââââââ 5281 5282 // Skip-aware loading message scheduler. (issue #38) 5283 // - scheduleLoadingMessage() sets a timer to call showLoadingMessage() 5284 // after MESSAGE_DELAY_MS. 5285 // - removeMessage() cancels the pending timer too, so if the quote 5286 // returned skip/success/error before the delay elapsed, the spinner 5287 // is never inserted into the DOM in the first place. 5288 const MESSAGE_DELAY_MS = 800; 5289 let pendingMessageTimer = null; 5290
5291 function scheduleLoadingMessage() { 5292 if (pendingMessageTimer) clearTimeout(pendingMessageTimer); 5293 pendingMessageTimer = setTimeout(() => { 5294 pendingMessageTimer = null; 5295 showLoadingMessage(); 5296 }, MESSAGE_DELAY_MS); 5297 } 5298 5299 function showLoadingMessage() { 5300 removeMessage(); 5301 const msg = document.createElement('div'); 5302 msg.id = 'freight-msg'; 5303 msg.innerHTML = ` 5304 <style> 5305 @keyframes freight-spin { to { transform: rotate(360deg); } } 5306 .freight-spinner { 5307 display: inline-block; width: 18px; height: 18px; 5308 border: 2.5px solid #93c5fd; border-top-color: #1d4ed8; 5309 border-radius: 50%; animation: freight-spin 0.7s linear infinite; 5310 vertical-align: middle; margin-right: 10px; 5311 } 5312 </style> 5313 <div style="background:#f0f9ff;border:1px solid #0ea5e9;border-radius:6px;padding:16px 20px;margin:12px 0;font-size:14px;color:#0c4a6e;display:flex;align-items:center;"> 5314 <span class="freight-spinner"></span> 5315 <div> 5316 <strong>Calculating freight shipping rates...</strong><br> 5317 <span style="font-size:13px;color:#64748b;">Getting live quotes from R&L Carriers and Old Dominion. This may take a few seconds.</span> 5318 </div> 5319 </div>`; 5320 5321 const shippingContainer = document.getElementById('shipping-methods') 5322 || document.getElementById('zs-shipping-methods-form')?.parentElement; 5323 if (shippingContainer) { 5324 const form = document.getElementById('zs-shipping-methods-form'); 5325 if (form) { 5326 form.parentNode.insertBefore(msg, form); 5327 } else { 5328 shippingContainer.prepend(msg); 5329 } 5330 } 5331 } 5332 5333 function removeMessage() { 5334 // Cancel any pending deferred-show so the spinner never appears if 5335 // the quote already returned within MESSAGE_DELAY_MS. (issue #38) 5336 if (pendingMessageTimer) { 5337 clearTimeout(pendingMessageTimer); 5338 pendingMessageTimer = null; 5339 } 5340 const el = document.getElementById('freight-msg'); 5341 if (el) el.remove(); 5342 const legacy = document.getElementById('rl-freight-msg'); 5343 if (legacy) legacy.remove(); 5344 } 5345 5346 // ââ Manual-quote "contact us" safety net âââââââââââââââââââââââ 5347 // Last-resort notice for a cart that NO carrier could rate online.
5348 // Three causes share it (the `kind` param picks the wording): 5349 // 'hazmat' â hazmat cart routed to freight (eligible:false + 5350 // hazmat_fallback) whose freight quote ALSO came 5351 // back empty. 5352 // 'intl-freight' â freight-routed cart to a country R&L / Old 5353 // Dominion don't serve (anything outside US/CA, 5354 // e.g. Belgium); freight_quote.php answered 5355 // unsupported_country:true. 5356 // 'intl' â international parcel cart whose quote round 5357 // returned zero services (carrier no-service 5358 // lane, embargoed country, or all-API failure). 5359 // Without it the customer would see an empty shipping-methods list 5360 // and be stuck. Show a direct contact path instead. Idempotent â 5361 // re-inserting replaces. Style + contact info match the 5362 // top-of-file sf-notice block. 5363 function showManualQuoteNotice(kind) { 5364 clearManualQuoteNotice(); 5365 const msg = document.createElement('div'); 5366 msg.id = 'manual-quote-notice'; 5367 msg.style.cssText = 5368 'background:#fff8e1;border:1px solid #f9a825;border-radius:6px;' + 5369 'padding:16px 20px;margin:12px 0;line-height:1.5;'; 5370 const bodyByKind = { 5371 'intl-freight': 5372 'Orders of this size ship by freight, and we arrange ' + 5373 'international freight quotes individually. We\'re happy to ' + 5374 'quote shipping to your address and complete your order.', 5375 'intl': 5376 'We could not return an automatic shipping rate for your ' + 5377 'destination. We\'re happy to quote shipping to your ' + 5378 'address and complete your order.', 5379 }; 5380 const body = bodyByKind[kind] 5381 || ('It contains items classed as hazardous materials (such as ' + 5382 'lithium batteries) that need special handling, and we could not ' + 5383 'return an automatic rate for your address. We\'re happy to ' + 5384 'arrange shipping and complete your order.'); 5385 msg.innerHTML = 5386 '<p style="margin:0 0 8px;color:#e65100;font-weight:600;">' + 5387 'This order needs a manual shipping quote.</p>' + 5388 '<p style="margin:0 0 12px;color:#333;font-size:14px;">' + 5389 body + '</p>' + 5390 '<p style="margin:0;color:#333;font-size:14px;">' + 5391 '<a href="mailto:[email protected]" style="color:#004aad;">' + 5392 '[email protected]</a> | (801) 432-0003</p>'; 5393 const form = document.getElementById('zs-shipping-methods-form'); 5394 const container = document.getElementById('shipping-methods') 5395 || (form && form.parentElement); 5396 if (form && form.parentNode) { 5397 form.parentNode.insertBefore(msg, form); 5398 } else if (container) { 5399 container.prepend(msg); 5400 } 5401 } 5402 5403 function clearManualQuoteNotice() { 5404 const el = document.getElementById('manual-quote-notice'); 5405 if (el) el.remove(); 5406 } 5407 5408 // ââ Init âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 5409 5410 if (document.readyState === 'loading') { 5411 document.addEventListener('DOMContentLoaded', init); 5412 } else { 5413 init(); 5414 } 5415})(); 5416 5417</script>
5417<!-- Google tag (gtag.js) Tyler Greene -->
vendor: 63 bytes, line 5417
5417<script async src="https://www.googletagmanager.com/gtag/js?id=
5417AW-1009783359
vendor: 11 bytes, line 5417
5417"></script>
5417<script> 5418
vendor: 134 bytes, lines 5418-5422
5418window.dataLayer = window.dataLayer || []; 5419 function gtag(){dataLayer.push(arguments);} 5420 gtag('js', new Date()); 5421 5422 gtag('config', '
5422AW-1009783359
vendor: 4 bytes, line 5422
5422');
5423</script>
5423<!-- On load Functions -->
5423<script> 5424 5425 5426 function isVisible(el) { 5427 const style = getComputedStyle(el); 5428 return ( 5429 style.display !== 'none' && 5430 style.visibility !== 'hidden' && 5431 el.offsetParent !== null 5432 ); 5433 } 5434 5435 function skipToMain() { 5436 let mainContent = document.getElementById('main-content'); 5437 if (mainContent) { 5438 mainContent.scrollIntoView(); 5439 const focusableElements = mainContent.querySelectorAll('a, button, input, textarea, select, [tabindex]:not([tabindex="-1"])'); 5440 const focusableElement = Array.from(focusableElements).find(isVisible); 5441 if (focusableElement) { 5442 focusableElement.focus(); 5443 } 5444 } 5445 } 5446 5447 function onListImageLoaded(elem){ 5448 let productCard = elem.closest('[data-zs-product-id]'); // No i18n 5449 let imageLoader = productCard ? productCard.querySelector('[data-theme-product-image-skeleton-loader]') : null; 5450 if(imageLoader){ 5451 imageLoader.style.opacity = 0; 5452 startFadeInAnimation(elem); 5453 } 5454 } 5455 function startFadeInAnimation(imgElement) { 5456 imgElement.classList.add('theme-shimmer-animation'); 5457 } 5458 5459function handleFallBackForImage(elem) { 5460 elem.onerror = null; 5461 5462 const fallbackSrc = elem.getAttribute('data-fallback'); 5463 const fallBackResolution = elem.getAttribute('data-fallback-resolution'); 5464 const pictureElem = elem.closest('picture'); 5465 5466 const originalImgSrc = elem.src; 5467 5468 elem.src = fallbackSrc; 5469 if(fallBackResolution){ 5470 elem.setAttribute('data-image-resolution', fallBackResolution); 5471 } 5472 // If srcset available change the url to fallback 5473 if (pictureElem) { 5474 const sources = pictureElem.querySelectorAll('source'); 5475 sources.forEach(source => { 5476 const srcset = source.getAttribute('srcset'); 5477 if (!srcset) return; 5478 5479 const newSrcSet = srcset 5480 .split(',') 5481 .map(item => { 5482 const [path, size] = item.trim().split(' '); 5483 return `${fallbackSrc} ${size}`; 5484 }) 5485 .join(', '); 5486 5487 source.setAttribute('srcset', newSrcSet); 5488 }); 5489 } 5490}
5491function validateBackgroundImage(elem) { 5492 const basePath = elem.getAttribute('data-bg'); 5493 const fallback = elem.getAttribute('data-bg-fallback'); 5494 if (!basePath) return; 5495 5496 // Creating a new image with the same url to check if it is a valid url 5497 const img = new Image(); 5498 img.src = basePath; 5499 img.onload = () => { 5500 // image is valid, do nothing 5501 }; 5502 img.onerror = () => { 5503 // image is not valid, change the background url to fallback 5504 elem.style.backgroundImage = `url('${fallback}')`; 5505 }; 5506 5507} 5508document.addEventListener('DOMContentLoaded', () => { 5509 document.querySelectorAll('[data-fallback-init]').forEach(elem => { 5510 validateBackgroundImage(elem); 5511 }); 5512}); 5513</script>
5513<script> 5514 function toggleSearchSuggestions(elem){ 5515 let wrapper = elem.closest('[data-search="zptheme-search-container"]'); 5516 let inputField = wrapper ? wrapper.querySelector('[data-zs-search-input]') : null; 5517 let suggestionWrappers = document.querySelectorAll('.theme-search-suggestion-overlay'); 5518 if (inputField && suggestionWrappers) { 5519 let body = document.querySelector('body'); 5520 body.classList.remove('theme-body-hide-overflow'); 5521 suggestionWrappers.forEach(wrapper => { 5522 wrapper.classList.remove('theme-show-overlay'); 5523 }) 5524 } 5525 } 5526 function closeSearchOverlayOnClick(){ 5527 let searchOverlays = document.querySelectorAll('.theme-search-suggestion-overlay'); searchOverlays.forEach(overlay => { 5528 overlay.addEventListener('click', (e) => { 5529 if (e.target === e.currentTarget) { 5530 toggleSearchSuggestions(e.target); 5531 } 5532 }) 5533 }) 5534 } 5535 document.addEventListener('DOMContentLoaded', function () { 5536 const inputs = document.querySelectorAll('#search-with-suggestion, #search-with-suggestion-mobile'); 5537 let debounceTimer; 5538 inputs.forEach(input => { 5539 input.addEventListener('input', function () { 5540 clearTimeout(debounceTimer); 5541 let query = input.value.trim(); 5542 5543 let searchOverlays = document.querySelectorAll('.theme-search-suggestion-overlay'); 5544 searchOverlays.forEach(searchOverlay => { 5545 const suggestionsContainer = searchOverlay.querySelector('#search-suggestions'); 5546 let searchContainer = searchOverlay.querySelector('.theme-search-suggestion-container'); 5547 let searchStyle = searchOverlay.getAttribute('data-search-style'); 5548 let body = document.querySelector('body'); 5549 if(searchStyle === "01") { 5550 if(query.length > 0) { 5551 body.classList.add('theme-body-hide-overflow'); 5552 searchOverlay.classList.add('theme-show-overlay'); 5553 searchContainer.classList.add('theme-show-live-search'); 5554 suggestionsContainer.style.display = 'block'; 5555 } 5556 else { 5557 body.classList.remove('theme-body-hide-overflow'); 5558 searchOverlay.classList.remove('theme-show-overlay'); 5559 searchContainer.classList.remove('theme-show-live-search'); 5560 suggestionsContainer.style.display = 'none'; 5561 } 5562 } 5563 5564 if (query.length < 2) { 5565 suggestionsContainer.style.display = 'none'; 5566 return; 5567 } 5568 5569 debounceTimer = setTimeout(() => { 5570 fetchSuggestions(query, suggestionsContainer); 5571 }, 300); 5572 }) 5573 }); 5574 }) 5575 5576 function fetchSuggestions(query, suggestionsContainer) { 5577 fetch(`/zos-api/products?search_text=${encodeURIComponent(query)}`) 5578 .then(response => response.json()) 5579 .then(data => { 5580 showSuggestions(data, suggestionsContainer); 5581 var displaySuggestionsSuccess = new CustomEvent("zp-event-search-content-success"); 5582 document.dispatchEvent(displaySuggestionsSuccess); 5583 }) 5584 .catch(error => { 5585 console.error('Error fetching suggestions:', error); 5586 var displaySuggestionsFailure = new CustomEvent("zp-event-search-content-failure"); 5587 document.dispatchEvent(displaySuggestionsFailure); 5588 }); 5589 } 5590 5591 function showSuggestions(result, suggestionsContainer) { 5592 if (result.code != 0 || result.products.length < 1 ) { 5593 suggestionsContainer.innerHTML = `<div class="zpcol-md-12 theme-no-products-error"> 5594 There are no products matching this search term. 5595 </div>`; 5596 } 5597 else { 5598 let productsHeader = document.createElement('h2'); 5599 productsHeader.innerText = "Products"; 5600 productsHeader.classList.add('theme-search-heading'); 5601 let productsContainer = document.createElement('div'); 5602 productsContainer.classList.add('theme-suggested-products'); 5603 productsContainer.innerHTML = result.products.map(item => ` 5604 <div class="theme-search-product-card" data-zs-product-id="${item.product_id}" data-stock-available="${item.overall_stock}" onclick="location.href='/products/${item.product_id}'"> 5605 <div class="theme-product-image"> 5606 ${item.document_id ? `<img src="https://cdn1.zohoecommerce.com/product-images/${item.document_id}/200x200?storefront_domain=www.tyconsystems.com">` : `<img src="/zs-common/images/no-preview-image.jpg">`} 5607 </div> 5608 <div class="theme-product-description"> 5609 <div class="theme-product-name">${item.name}</div> 5610 ${ !item.variants[0].channel_preferences.hide_price ? 5611 `<div class="theme-product-pricing" data-zs-pricings>
5612 <span class='theme-min-rate' data-zs-selling-price="${item.variants[0].rate}" data-zs-currency-code="${item.currency_code}">${item.variants[0].rate_formatted}</span> 5613 <span class='theme-max-rate' data-zs-label-price="${item.variants[0].label_rate}" data-zs-currency-code="${item.currency_code}">${item.variants[0].label_rate_formatted}</span> 5614 </div>` : ``} 5615 </div> 5616 </div> 5617 `).join(''); 5618 suggestionsContainer.innerHTML = ""; 5619 suggestionsContainer.appendChild(productsHeader); 5620 suggestionsContainer.appendChild(productsContainer); 5621 if (multi_currency.convertContainerOnDemand) { 5622 let currencyContainer = document.querySelector('[data-zs-currency-list]'); 5623 if (currencyContainer) { 5624 multi_currency.convertContainerOnDemand('search-suggestions'); 5625 } 5626 } 5627 } 5628 suggestionsContainer.style.display = 'block'; 5629 } 5630 5631 closeSearchOverlayOnClick(); 5632 }); 5633 </script>
5633</head><body data-zs-home="true" data-zs-subsite="" data-zs-display-mode="default"><button onclick="skipToMain()" type="button" class="main-content-nav-btn zpbutton-type-primary zpbutton-style-roundcorner">Skip to main content</button><div data-zs-mobile-header-five-top class="theme-mobile-header-style-05 theme-mobile-header-top "><div class="theme-mobile-header-nav-wrapper "><span data-zs-mobile-header-slide-open data-zs-drawer-open-button="navigation" class="theme-mobile-header-icon theme-mobile-header-slide-open"><svg width="24px" height="24px" fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path 5634 d="M 2 5 L 2 7 L 22 7 L 22 5 L 2 5 z M 2 11 L 2 13 L 22 13 L 22 11 L 2 11 z M 2 17 L 2 19 L 22 19 L 22 17 L 2 17 z"/></svg></span><div data-zs-branding class="theme-branding-info " data-theme-branding-info="zptheme-branding-info"><div data-zs-logo-container class="theme-logo-parent "><a href="/"><picture><img data-zs-logo src="https://cdn1.zohoecommerce.com/images/Icons/Photo Jun 10 2024- 6 37 55 AM -1-.png?storefront_domain=www.tyconsystems.com" data-fallback="/images/Icons/Photo Jun 10 2024- 6 37 55 AM -1-.png" onerror="handleFallBackForImage(this)" alt="Tycon Systems" style="height:50px;width:160px;"/></picture></a></div> 5635</div><span data-zs-mobile-header-search class="theme-mobile-header-search"><span data-zs-mobile-header-search-icon class="theme-mobile-header-search-icon-wrapper"><svg class="theme-mobile-header-search-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><g clip-path="url(#clip0_3266_12325)"><path d="M10.9658 19.9313C6.02208 19.9313 2 15.9092 2 10.9654C2 6.02167 6.02208 2 10.9658 2C15.9096 2 19.9317 6.02208 19.9317 10.9658C19.9317 15.9096 15.9096 19.9317 10.9658 19.9317V19.9313ZM10.9658 3.38167C6.78417 3.38167 3.38208 6.78375 3.38208 10.9654C3.38208 15.1471 6.78417 18.5492 10.9658 18.5492C15.1475 18.5492 18.5496 15.1471 18.5496 10.9654C18.5496 6.78375 15.1475 3.38167 10.9658 3.38167Z" fill="black"></path><path d="M21.3091 22.0003C21.1324 22.0003 20.9553 21.9328 20.8207 21.7978L16.3212 17.2982C16.0512 17.0282 16.0512 16.5912 16.3212 16.3212C16.5912 16.0512 17.0282 16.0512 17.2982 16.3212L21.7978 20.8207C22.0678 21.0907 22.0678 21.5278 21.7978 21.7978C21.6628 21.9328 21.4862 22.0003 21.3095 22.0003H21.3091Z" fill="black"></path></g><defs><clipPath id="clip0_3266_12325"><rect width="24" height="24" fill="white"></rect></clipPath></defs></svg></span><div class="theme-search-field-container" data-search="zptheme-search-container"><div class='theme-search-field-container-floater'><div class='theme-search-field-container-inner'><span data-zs-mobile-header-search-back class="theme-mobile-header-search-back" onclick="toggleSearchSuggestions(this)"><svg width="24px" height="24px" viewBox="0 0 24 24"><path fillrule="evenodd" d="M20.25 11.25H5.555l6.977-6.976a.748.748 0 000-1.056.749.749 0 00-1.056 0L3.262 11.43A.745.745 0 003 12a.745.745 0 00.262.57l8.214 8.212a.75.75 0 001.056 0 .748.748 0 000-1.056L5.555 12.75H20.25a.75.75 0 000-1.5"></path></svg></span><form method="get" action="/search-products"><input type="text" name="q" placeholder="Search" aria-label="Search" maxlength="100" data-search-input="zptheme-search-input" id="search-with-suggestion-mobile" data-zs-search-input/><input type="submit" name="" class="theme-search-go" value="Go" data-zs-search=""/><span data-zs-mobile-header-search-clear class="theme-mobile-header-search-clear"></span></form></div> 5636<div class="theme-search-suggestion-overlay" data-search-style="01" data-search="zptheme-search-container"><div class="theme-search-suggestion-container"><div class="theme-live-search-bar"><form method="get" action="/search-products"><input id="search-with-suggestion" type="text" name="q" aria-label="Search" placeholder="Search" data-search-input="zptheme-search-input" maxlength="100" data-zs-search-input/><button type="submit" name="" class="theme-search-go zpbutton zpbutton-type-primary" value="View all" data-zs-search="">View all</button><div class="theme-search-loader-dots" data-theme-search-loader-dots><span></span></div> 5637<button type="button" name="Close" aria-label="Close" class="theme-close-live-search zpbutton zpbutton-type-primary" onclick="toggleSearchSuggestions(this)"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M10.9395 1L5.93945 6M5.93945 6L0.939453 11M5.93945 6L10.9395 11M5.93945 6L0.939453 1" stroke="#999999" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></button></form></div> 5638<div id="search-suggestions" class="theme-search-suggestion-result zpcontainer"></div> 5639</div></div></div></div></span><div data-zs-mobile-header-five-bottom class="theme-mobile-header-style-05 theme-mobile-header-bottom "><div class="theme-mobile-header-icon-wrapper"><div style="display:none;" tabindex='0' role="button" onkeydown="handleKeyPressForClick(event, this)" class="theme-search theme-search-and-mini-cart-group-item" data-zs-view-wishlist data-show-wishlist-icon aria-label=Wishlist><span class="theme-wishlist-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M7.75 3.5C5.127 3.5 3 5.76 3 8.547C3 14.125 12 20.5 12 20.5C12 20.5 21 14.125 21 8.547C21 5.094 18.873 3.5 16.25 3.5C14.39 3.5 12.78 4.636 12 6.29C11.22 4.636 9.61 3.5 7.75 3.5Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></span></div> 5640<span data-zs-drawer-open-button="portal" class="theme-mobile-header-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M19.8498 22C19.4681 22 19.159 21.6909 19.159 21.3092V19.2404C19.159 17.34 17.6127 15.7938 15.7123 15.7938H7.43771C5.53729 15.7938 3.99104 17.34 3.99104 19.2404V21.3092C3.99104 21.6909 3.68187 22 3.30021 22C2.91854 22 2.60938 21.6909 2.60938 21.3092V19.2404C2.60938 16.5779 4.77521 14.4121 7.43771 14.4121H15.7123C18.3748 14.4121 20.5406 16.5779 20.5406 19.2404V21.3092C20.5406 21.6909 20.2315 22 19.8498 22Z" fill="black"></path><path d="M11.5749 11.6567C8.91242 11.6567 6.74658 9.49084 6.74658 6.82834C6.74658 4.16583 8.91283 2 11.5749 2C14.237 2 16.4033 4.16583 16.4033 6.82834C16.4033 9.49084 14.2374 11.6567 11.5749 11.6567ZM11.5749 3.38167C9.6745 3.38167 8.12825 4.92792 8.12825 6.82834C8.12825 8.72875 9.6745 10.275 11.5749 10.275C13.4753 10.275 15.02
564016 8.72875 15.0216 6.82834C15.0216 4.92792 13.4753 3.38167 11.5749 3.38167Z" fill="black"></path></svg></span><span class="theme-mobile-header-icon" data-zs-cart-iconcount-container data-zs-view-cart><span class="theme-minicart-icon theme-carticon-headerseven-style-02" tabindex='0' data-zs-cart-icon><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20px" height="20px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve"><path d="M512.1,93.8H145.8l-21.2-83.1H-0.1v56h81.2l83.9,329h285.1v-56H208.5l-6.9-27.1h250.5L512.1,93.8z M409.4,256.6H188.2 l-27.2-106.8h277.7L409.4,256.6z"></path><circle cx="204.1" cy="460.6" r="40.7"></circle><circle cx="409.3" cy="460.6" r="40.7"></circle></svg><span class="theme-mini-cart-count" data-zs-view-cart-count>0</span></span></span><div class="theme-quote-container" data-zs-view-quote><span class="theme-quote-icon" tabindex='0'><svg aria-label="?product.label.view_quot" xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 512 512" fill="#000"><g clip-path="url(#clip0_408_10990)"><path d="M334.489 -1.02942e-06C358.575 -1.18586e-06 378.706 -0.043808 394.642 2.09863C411.167 4.32052 426.22 9.22624 438.334 21.3398L438.896 21.9102C450.6 33.9247 455.388 48.7656 457.575 65.0332C459.718 80.9684 459.674 101.099 459.674 125.185V386.815C459.674 410.902 459.718 431.033 457.575 446.968C455.388 463.235 450.6 478.075 438.896 490.09L438.334 490.66C426.22 502.774 411.167 507.679 394.642 509.901C378.706 512.044 358.575 512 334.489 512H177.511C153.424 512 133.294 512.044 117.358 509.901C101.091 507.714 86.2507 502.926 74.2363 491.223L73.666 490.66C61.5523 478.546 56.6466 463.493 54.4248 446.968C52.2823 431.033 52.3261 410.902 52.3261 386.815V125.185C52.3261 101.099 52.2824 80.9684 54.4248 65.0332C56.6466 48.5073 61.5522 33.4536 73.666 21.3398L74.2363 20.7773C86.2507 9.07376 101.091 4.2858 117.358 2.09863C133.294 -0.0438061 153.424 -1.26723e-06 177.511 -1.02942e-06H334.489ZM93.3915 198.044V386.815C93.3915 412.062 93.435 428.932 95.124 441.495C96.7336 453.467 99.4909 458.411 102.703 461.623L103.01 461.923C106.231 465.006 111.233 467.643 122.831 469.202C135.394 470.891 152.264 470.935 177.511 470.935H334.489C359.736 470.935 376.606 470.891 389.169 469.202C401.141 467.593 406.085 464.835 409.297 461.623L409.597 461.316C412.68 458.095 415.317 453.093 416.876 441.495C418.565 428.932 418.608 412.062 418.608 386.815V198.044H93.39
564015ZM203.674 366.282C215.014 366.282 224.206 375.476 224.206 386.815C224.206 398.155 215.014 407.348 203.674 407.348H177.511C166.171 407.348 156.979 398.155 156.978 386.815C156.978 375.476 166.171 366.282 177.511 366.282H203.674ZM334.489 366.282C345.829 366.282 355.021 375.476 355.021 386.815C355.021 398.155 345.829 407.348 334.489 407.348H308.326C296.986 407.348 287.793 398.155 287.793 386.815C287.793 375.476 296.986 366.282 308.326 366.282H334.489ZM203.674 261.631C215.014 261.631 224.206 270.823 224.206 282.163C224.206 293.503 215.014 302.695 203.674 302.695H177.511C166.171 302.695 156.978 293.503 156.978 282.163C156.978 270.823 166.171 261.631 177.511 261.631H203.674ZM334.489 261.631C345.829 261.631 355.021 270.823 355.021 282.163C355.021 293.503 345.829 302.695 334.489 302.695H308.326C296.986 302.695 287.793 293.503 287.793 282.163C287.793 270.823 296.986 261.631 308.326 261.631H334.489ZM177.511 41.0654C152.264 41.0654 135.394 41.1088 122.831 42.7979C111.233 44.3572 106.231 46.9936 103.01 50.0771L102.703 50.377C99.4909 53.5891 96.7336 58.5326 95.124 70.5049C93.4349 83.0678 93.3915 99.9376 93.3915 125.185V156.979H418.608V125.185C418.608 99.9376 418.565 83.0678 416.876 70.5049C415.317 58.907 412.68 53.9052 409.597 50.6836L409.297 50.377C4
564006.085 47.1649 401.141 44.4075 389.169 42.7979C376.606 41.1088 359.736 41.0654 334.489 41.0654H177.511Z" fill="#000"></path></g><defs><clipPath id="clip0_408_10990"><rect width="512" height="512" fill="white"></rect></clipPath></defs></svg><span class="theme-mini-quote-count" data-zs-view-quote-count>0</span></span></div> 5641</div></div></div></div><div data-zs-mobile-header-slide-wrapper data-zs-drawer class="theme-mobile-header-style-05 theme-mobile-header-slidewrapper "><div class="theme-mobile-header-slide"><div class="theme-mobile-header-slide-header"><span data-zs-mobile-header-logo class="theme-mobile-header-logo "><a href="/"><picture><img 5642 src="/images/Icons/Photo Jun 10 2024- 6 37 55 AM -1-.png" alt="Tycon Systems"/></picture></a></span><span data-zs-drawer-close-button class="theme-mobile-header-slide-close"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path 5643 d="M4.707 3.293 3.293 4.707 10.586 12l-7.293 7.293 1.414 1.414L12 13.414l7.293 7.293 1.414-1.414L13.414 12l7.293-7.293-1.414-1.414L12 10.586 4.707 3.293z"/></svg></span></div> 5644<div data-zs-drawer-content="navigation" class="theme-mobile-header-slide-content"><div data-zs-responsive-menu-area class="theme-mobile-header-slide-box theme-mobile-header-navigation theme-responsive-menu-area theme-navigation-and-icons"><div class="theme-responsive-menu theme-menu-area" data-zp-responsive-container="mymenu1"></div> 5645</div></div><div data-zs-drawer-content="portal" class="theme-mobile-header-slide-content"><div class="theme-portal-style" data-theme-portal-responsive="zptheme-portal-responsive"></div> 5646</div></div></div><div data-headercontainer="zptheme-data-headercontainer" data-zs-mobile-headerstyle="05" class="zpheader-style-07 theme-pages-full-stretch theme-mobile-header-style-05"><div class="theme-header-topbar " data-dark-part-applied="false" data-theme-topbar="zptheme-topbar"><div class="zpcontainer"><div data-topbarinfo-position="zptheme-topbarinfo-position"><div class="theme-topbar-contact-info " data-contact-info="zptheme-contact-info"><ul data-contact-info-inner="zptheme-contact-info-inner"><li class="theme-topbar-info"><a href="mailto:[email protected]"><span class="theme-topbar-info-icon" data-zs-contact-mail-icon><svg aria-hidden="true" focusable="false" viewBox="0 0 512 513.5" xmlns="http://www.w3.org/2000/svg"><path d="M48 128h16 384 16v16 256 16h-16-384-16v-16-256-16z m69 32l139 92.5 139-92.5h-278z m-37 14v210h352v-210l-167 111.5-9 5.5-9-5.5z"/></svg></span><span class="theme-topbar-info-icon-text" data-zs-contact-mail-text>[email protected]</span></a></li><li class="theme-topbar-info"><a href="tel:+1.801.432.0003"><span class="theme-topbar-info-icon" data-zs-contact-phone-icon><svg aria-hidden="true" focusable="false" viewBox="0 0 512 513.5" xmlns="http://www.w3.org/2000/svg"><path d="M138.5 48c8.376 0 16.6562 3.0244 23.5 8.5l1 0.5 0.5 0.5 64 66 1.5 1.5-0.5 0.5c14.1738 13.2119 13.9678 35.5322 0.5 49l-32 32c4.9189 11.2773 18.3291 40.0186 44.5 65 26.3584 25.1602 53.9316 39.4668 65.5 45l31.5-31.5 0.5-0.5c13.2793-13.2793 36.7207-13.2793 50 0l0.5 1 65 65c13.2793 13.2793 13.2793 36.2207 0 49.5l-49.5 49.5-0.5 0.5-0.5 0.5c-15.0107 12.8672-36.04 17.4863-55.5 11h-0.5c-37.5488-14.6934-113.509-48.0088-181.5-116-67.7266-67.7266-102.448-142.659-116-181.5-0.0352-0.0996 0.0342-0.4004 0-0.5-6.7031-18.4336-1.915-39.7383 13.5-54l-0.5-0.5 50-51.5 0.5-0.5 1-0.5c6.8438-5.4756 15.124-8.5 23.5-8.5z m0 32c-1.1719 0-2.3438 0.5752-3.5 1.5l-48.5 49.5-0.5 0.5-0.5 0.5c-5.6738 4.8633-7.4394 14.4932-5 21 12.1201 34.8467 45.1992 106.699 108.5 170 62.7842 62.7842 133.224 93.7607 169.5 108 9.3408 3.1133 17.1113 1.833 24.5-4.5l49-49c2.7207-2.7207 2.7207-1.7793 0-4.5l-65.5-65.5c-2.7207-2.7207-2.2793-2.7207-5 0l-40 40-7.5 7.5-10-4.5s-43.8311-18.5518-81-52.5l-3.5-3c-38.5947-36.8408-57-86-57-86l-3.5-10 7.5-7 40-40c1.9658-1.9658 0.874-3.6006 1-3.5l-0.5-0.5-1-1-64-65.5c-1.1563-0.9248-2.3281-1.5-3.5-1.5z"/></svg></span><span class="theme-topbar-info-icon-text" data-zs-contact-phone-text>+1.801.432.0003</span></a></li></ul></div> 5647</div></div></div><div class="theme-header " data-dark-part-applied="false" data-banner-base-header="theme-banner-base-header"><div class="zpcontainer"><button 5648 type="button" class="theme-responsive-goback" data-zs-mobile-header-responsive-goback aria-label="Go back" onclick="history.go(-1);"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
5648<path 5649 fill="#3E4152" fill-rule="evenodd" d="M20.25 11.25H5.555l6.977-6.976a.748.748 0 000-1.056.749.749 0 00-1.056 0L3.262 11.43A.745.745 0 003 12a.745.745 0 00.262.57l8.214 8.212a.75.75 0 001.056 0 .748.748 0 000-1.056L5.555 12.75H20.25a.75.75 0 000-1.5"></path></svg></button><div data-zs-branding class="theme-branding-info " data-theme-branding-info="zptheme-branding-info"><div data-zs-logo-container class="theme-logo-parent "><a href="/"><picture><img data-zs-logo src="https://cdn1.zohoecommerce.com/images/Icons/Photo Jun 10 2024- 6 37 55 AM -1-.png?storefront_domain=www.tyconsystems.com" data-fallback="/images/Icons/Photo Jun 10 2024- 6 37 55 AM -1-.png" onerror="handleFallBackForImage(this)" alt="Tycon Systems" style="height:50px;width:160px;"/></picture></a></div> 5650</div><div class="theme-navigation-and-icons theme-search-cart-non-res" data-search-cart-position-non-responsive="zptheme-search-cart-position-non-responsive"><div class="theme-search-and-mini-cart-group " data-theme-search-cart-group="zptheme-search-cart-group"><div class="theme-search theme-search-and-mini-cart-group-item" data-zs-search-container><div class="theme-search-field-container theme-search-field-container-07" data-search="zptheme-search-container"><form method="get" action="/search-products" onclick="toggleSearchSuggestions(this)"><input id="search-with-suggestion" type="text" name="q" aria-label="Search" placeholder="Search" data-search-input="zptheme-search-input" maxlength="100" data-zs-search-input/><input class="theme-search-go-input" aria-label="Go" type="submit" name="" class="theme-search-go" data-zs-search="" value=""/><span class="theme-search-go-icon" data-zs-search-icon aria-label="Go"><svg aria-hidden="true" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M8.111.5a7.611 7.611 0 015.724 12.628l3.385 3.385a.5.5 0 01-.638.765l-.069-.058-3.385-3.385A7.611 7.611 0 118.11.5zm0 1a6.611 6.611 0 104.393 11.552.499.499 0 01.548-.55A6.611 6.611 0 008.112 1.5z" fill-rule="nonzero"/></svg></span><div class="theme-search-loader-dots" data-theme-search-loader-dots><span></span></div> 5651</form></div></div><div style="display:none;" tabindex='0' role="button" onkeydown="handleKeyPressForClick(event, this)" class="theme-search theme-search-and-mini-cart-group-item" data-zs-view-wishlist data-show-wishlist-icon aria-label=Wishlist><span class="theme-wishlist-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M7.75 3.5C5.127 3.5 3 5.76 3 8.547C3 14.125 12 20.5 12 20.5C12 20.5 21 14.125 21 8.547C21 5.094 18.873 3.5 16.25 3.5C14.39 3.5 12.78 4.636 12 6.29C11.22 4.636 9.61 3.5 7.75 3.5Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></span></div> 5652<div data-theme-portal-non-responsive="zptheme-portal-non-responsive" class="theme-nonresponsive-portal-position-two"><div data-theme-portal="zptheme-portal" aria-label="Member portal" tabindex='0' class="theme-portal-login theme-portal-icon-enabled"><svg aria-hidden="true" class="theme-portal-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M19.8498 22C19.4681 22 19.159 21.6909 19.159 21.3092V19.2404C19.159 17.34 17.6127 15.7938 15.7123 15.7938H7.43771C5.53729 15.7938 3.99104 17.34 3.99104 19.2404V21.3092C3.99104 21.6909 3.68187 22 3.30021 22C2.91854 22 2.60938 21.6909 2.60938 21.3092V19.2404C2.60938 16.5779 4.77521 14.4121 7.43771 14.4121H15.7123C18.3748 14.4121 20.5406 16.5779 20.5406 19.2404V21.3092C20.5406 21.6909 20.2315 22 19.8498 22Z" fill="black"/><path d="M11.5744 11.6567C8.91193 11.6567 6.74609 9.49084 6.74609 6.82834C6.74609 4.16583 8.91235 2 11.5744 2C14.2365 2 16.4028 4.16583 16.4028 6.82834C16.4028 9.49084 14.2369 11.6567 11.5744 11.6567ZM11.5744 3.38167C9.67401 3.38167 8.12776 4.92792 8.12776 6.82834C8.12776 8.72875 9.67401 10.275 11.5744 10.275C13.4748 10.275 15.0211 8.72875 15.0211 6.82834C15.0211 4.92792 13.4748 3.38167 11.5744 3.38167Z" fill="black"/></svg><div class="theme-portal-container-dropdown"><div class="theme-portal-container" data-portal-loggedout style="display:block;"><div class="theme-portal-content"><a data-portal-signin href="javascript:;">Sign In</a></div> 5653<div class="theme-portal-content"><a data-portal-signup href="javascript:;">Sign Up</a></div> 5654</div>
5654<div class="theme-portal-container" data-portal-loggedin style="display:none;"><div class="theme-portal-content theme-portal-username-content "><div class="theme-user-name-dropdown" data-portal-profile><span class="theme-portal-username" id="theme-portal-username-info" data-portal-user-name></span><span class="theme-user-profile-down-arrow" aria-hidden="true"></span></div> 5655<ul aria-labelledby="theme-portal-username-info" class="theme-username-dropdown-list" data-zs-portal-user-dropdown><li data-zs-portal-profile><a href="/account/profile">My Profile</a></li><li data-zs-portal-myorders><a href="/account/orders">My Orders</a></li><li data-zs-portal-addressbook><a href="/account/address">My Addresses</a></li><li data-show-wishlist-icon style="display:none;"><a href="/wishlist">My Wishlist</a></li><li data-zs-quotes><a href="/account/quotes">My Quotes</a></li></ul></div> 5656<div class="theme-portal-content"><a data-portal-logout href="javascript:;">Sign Out</a></div> 5657</div></div></div></div><div class="theme-mini-cart theme-search-and-mini-cart-group-item" data-zs-cart-iconcount-container data-zs-view-cart><span class="theme-minicart-icon theme-carticon-headerseven-style-02" tabindex='0' data-zs-cart-icon><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20px" height="20px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve"><path d="M512.1,93.8H145.8l-21.2-83.1H-0.1v56h81.2l83.9,329h285.1v-56H208.5l-6.9-27.1h250.5L512.1,93.8z M409.4,256.6H188.2 l-27.2-106.8h277.7L409.4,256.6z"></path><circle cx="204.1" cy="460.6" r="40.7"></circle><circle cx="409.3" cy="460.6" r="40.7"></circle></svg><span class="theme-mini-cart-count" data-zs-view-cart-count>0</span></span></div> 5658<div class="theme-quote-container" data-zs-view-quote><span class="theme-quote-icon" tabindex='0'><svg aria-label="?product.label.view_quot" xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 512 512" fill="#000"><g clip-path="url(#clip0_408_10990)"><path d="M334.489 -1.02942e-06C358.575 -1.18586e-06 378.706 -0.043808 394.642 2.09863C411.167 4.32052 426.22 9.22624 438.334 21.3398L438.896 21.9102C450.6 33.9247 455.388 48.7656 457.575 65.0332C459.718 80.9684 459.674 101.099 459.674 125.185V386.815C459.674 410.902 459.718 431.033 457.575 446.968C455.388 463.235 450.6 478.075 438.896 490.09L438.334 490.66C426.22 502.774 411.167 507.679 394.642 509.901C378.706 512.044 358.575 512 334.489 512H177.511C153.424 512 133.294 512.044 117.358 509.901C101.091 507.714 86.2507 502.926 74.2363 491.223L73.666 490.66C61.5523 478.546 56.6466 463.493 54.4248 446.968C52.2823 431.033 52.3261 410.902 52.3261 386.815V125.185C52.3261 101.099 52.2824 80.9684 54.4248 65.0332C56.6466 48.5073 61.5522 33.4536 73.666 21.3398L74.2363 20.7773C86.2507 9.07376 101.091 4.2858 117.358 2.09863C133.294 -0.0438061 153.424 -1.26723e-06 177.511 -1.02942e-06H334.489ZM93.3915 198.044V386.815C93.3915 412.062 93.435 428.932 95.124 441.495C96.7336 453.467 99.4909 458.411 102.703 461.623L103.01 461.923C106.231 465.006 111.233 467.643 122.831 469.202C135.394 470.891 152.264 470.935 177.511 470.935H334.489C359.736 470.935 376.606 470.891 389.169 469.202C401.141 467.593 406.085 464.835 409.297 461.623L409.597 461.316C412.68 458.095 415.317 453.093 416.876 441.495C418.565 428.932 418.608 412.062 418.608 386.815V198.044H93.39
565815ZM203.674 366.282C215.014 366.282 224.206 375.476 224.206 386.815C224.206 398.155 215.014 407.348 203.674 407.348H177.511C166.171 407.348 156.979 398.155 156.978 386.815C156.978 375.476 166.171 366.282 177.511 366.282H203.674ZM334.489 366.282C345.829 366.282 355.021 375.476 355.021 386.815C355.021 398.155 345.829 407.348 334.489 407.348H308.326C296.986 407.348 287.793 398.155 287.793 386.815C287.793 375.476 296.986 366.282 308.326 366.282H334.489ZM203.674 261.631C215.014 261.631 224.206 270.823 224.206 282.163C224.206 293.503 215.014 302.695 203.674 302.695H177.511C166.171 302.695 156.978 293.503 156.978 282.163C156.978 270.823 166.171 261.631 177.511 261.631H203.674ZM334.489 261.631C345.829 261.631 355.021 270.823 355.021 282.163C355.021 293.503 345.829 302.695 334.489 302.695H308.326C296.986 302.695 287.793 293.503 287.793 282.163C287.793 270.823 296.986 261.631 308.326 261.631H334.489ZM177.511 41.0654C152.264 41.0654 135.394 41.1088 122.831 42.7979C111.233 44.3572 106.231 46.9936 103.01 50.0771L102.703 50.377C99.4909 53.5891 96.7336 58.5326 95.124 70.5049C93.4349 83.0678 93.3915 99.9376 93.3915 125.185V156.979H418.608V125.185C418.608 99.9376 418.565 83.0678 416.876 70.5049C415.317 58.907 412.68 53.9052 409.597 50.6836L409.297 50.377C4
565806.085 47.1649 401.141 44.4075 389.169 42.7979C376.606 41.1088 359.736 41.0654 334.489 41.0654H177.511Z" fill="#000"></path></g><defs><clipPath id="clip0_408_10990"><rect width="512" height="512" fill="white"></rect></clipPath></defs></svg><span class="theme-mini-quote-count" data-zs-view-quote-count>0</span></span></div> 5659</div></div></div><div class="theme-search-suggestion-overlay" data-search-style="01" data-search="zptheme-search-container"><div class="theme-search-suggestion-container"><div class="theme-live-search-bar"><form method="get" action="/search-products"><input id="search-with-suggestion" type="text" name="q" aria-label="Search" placeholder="Search" data-search-input="zptheme-search-input" maxlength="100" data-zs-search-input/><button type="submit" name="" class="theme-search-go zpbutton zpbutton-type-primary" value="View all" data-zs-search="">View all</button><div class="theme-search-loader-dots" data-theme-search-loader-dots><span></span></div> 5660<button type="button" name="Close" aria-label="Close" class="theme-close-live-search zpbutton zpbutton-type-primary" onclick="toggleSearchSuggestions(this)"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M10.9395 1L5.93945 6M5.93945 6L0.939453 11M5.93945 6L10.9395 11M5.93945 6L0.939453 1" stroke="#999999" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></button></form></div> 5661<div id="search-suggestions" class="theme-search-suggestion-result zpcontainer"></div> 5662</div></div></div><div data-megamenu-content-container class="theme-header zpheader-style-navigation-07" data-header="zptheme-data-header" data-dark-part-applied="false" data-banner-base-header="theme-banner-base-header"><div class="zpcontainer"><div class="theme-navigation-and-icons"><div class="theme-menu-area" data-zp-nonresponsive-container="mymenu1"><div class="theme-menu " data-nav-menu-icon-width=15 data-nav-menu-icon-height=15 data-sub-menu-icon-height=15 data-sub-menu-icon-width=15 data-mega-menu-icon-width=15 data-mega-menu-icon-height=15 data-non-res-menu='zptheme-menu-non-res' data-zp-theme-menu="id: mymenu1 ;active: theme-menu-selected; maxitem:5;position: theme-sub-menu-position-change; orientation: horizontal; submenu: theme-sub-menu; moretext: More; nonresponsive-icon-el: theme-non-responsive-menu; responsive-icon-el: theme-responsive-menu; burger-close-icon: theme-close-icon; animate-open: theme-toggle-animate; animate-close: theme-toggle-animate-end;open-icon: theme-submenu-down-arrow; close-icon: theme-submenu-up-arrow; root-icon: theme-submenu-down-arrow; subtree-icon: theme-submenu-right-arrow;"><ul role="navigation" data-zs-menu-container><li data-megamenu="76150000004002677"><a href="javascript:;" target="_self" aria-haspopup="true" aria-expanded="false" aria-controls="submenu-Quick Links-1" onkeydown="handleMenuKeydown(event)"><span class="theme-menu-content "><span class="theme-menu-name" data-theme-menu-name="Quick Links">Quick Links</span></span><span class="theme-sub-li-menu theme-non-responsive-menu theme-submenu-down-arrow"></span><span class="theme-sub-li-menu theme-responsive-menu theme-submenu-down-arrow"></span></a><ul class="theme-sub-menu" data-zs-submenu-container id="submenu-Quick Links-1" style="display:none;"><div data-megamenu-content="76150000004002677" data-megamenu-direction="below" class="zpmm zpmm-container-style-01"><div class="zpcontainer"><div class="zpmm-inner"><div class="zprow zpalign-items- zpjustify-content- seperator-divider"><div class="zpelem-col zpcol-md-3 zpcol-sm-12 zpalign-self-"><!-- title --><div class="zpmm-element zpmm-title "><a class="zpmm-element-anchor"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Support</span></span></a></div> 5663<!-- title end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/support-portal" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Support Portal</span></span></a></div> 5664<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/knowledge-base" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Knowledge Base</span></span></a></div> 5665<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/calculators" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">System Calculators</span></span></a></div> 5666<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/support-request" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Support Request</span></span></a></div> 5667<!-- text end --></div><div class="zpelem-col zpcol-md-3 zpcol-sm-12 zpalign-self-"><!-- title --><div class="zpmm-element zpmm-title "><a class="zpmm-element-anchor"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Sales</span></span></a></div> 5668<!-- title end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/request-a-quote-1" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Request a Quote</span></span></a></div> 5669<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/request-a-system-design" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Request a System Design</span></span></a></div> 5670<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/stock-status" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Stock Status</span></span></a></div> 5671<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/resellers" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Resellers</span></span></a></div> 5672<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/become-a-reseller" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Join Our Reseller Network</span></span></a></div> 5673<!-- text end --></div><div class="zpelem-col zpcol-md-3 zpcol-sm-12 zpalign-self-"><!-- title --><div class="zpmm-element zpmm-title "><a class="zpmm-element-anchor"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Customer Service</span></span></a></div> 5674<!-- title end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/customer-service" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Customer Service</span></span></a></div> 5675<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/warranty-rma-requests" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Warranty & RMA Requests</span></span></a></div> 5676<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/contact" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Contact Us</span></span></a></div> 5677<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/patents-trademarks" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Patents & Trademarks</span></span></a></div> 5678<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/awards" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Awards</span></span></a></div> 5679<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/about" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">About Us</span></span></a></div> 5680<!-- text end --></div><div class="zpelem-col zpcol-md-3 zpcol-sm-12 zpalign-self-"><!-- title --><div class="zpmm-element zpmm-title "><a class="zpmm-element-anchor" href="/blogs/tycon-cases-applications-articles" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Deployments</span></span></a></div> 5681<!-- title end --><!-- text --><div class="zpmm-element zpmm-text ">
5681<a class="zpmm-element-anchor" href="https://www.tyconsystems.com/blogs/post/ensuring-port-connectivity-a-tycon-systems%C2%AE-case-study" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Ensuring Port Connectivity</span></span></a></div> 5682<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="https://www.tyconsystems.com/blogs/post/mobile-surveillance-anywhere-a-tycon-systems%C2%AE-case-study" target="_blank"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Mobile Surveillance Anywhere</span></span></a></div> 5683<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="https://www.tyconsystems.com/blogs/post/powering-remote-living-a-tycon-systems%C2%AE-case-study" target="_blank"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Powering Remote Living</span></span></a></div> 5684<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="https://www.tyconsystems.com/blogs/post/powering-arctic-research-tycon-systems%C2%AE" target="_blank"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Powering Arctic Research</span></span></a></div> 5685<!-- text end --></div></div></div></div></div></ul></li><li data-megamenu="76150000004687029"><a href="javascript:;" target="_self" aria-haspopup="true" aria-expanded="false" aria-controls="submenu- Explore-1" onkeydown="handleMenuKeydown(event)"><span class="theme-menu-content "><span class="theme-menu-name" data-theme-menu-name=" Explore"> Explore</span></span><span class="theme-sub-li-menu theme-non-responsive-menu theme-submenu-down-arrow"></span><span class="theme-sub-li-menu theme-responsive-menu theme-submenu-down-arrow"></span></a><ul class="theme-sub-menu" data-zs-submenu-container id="submenu- Explore-1" style="display:none;"><div data-megamenu-content="76150000004687029" data-megamenu-direction="below" class="zpmm zpmm-container-style-01"><div class="zpcontainer"><div class="zpmm-inner"><div class="zprow zpalign-items- zpjustify-content- seperator-divider"><div class="zpelem-col zpcol-md-3 zpcol-sm-12 zpalign-self-"><!-- image --><div class="zpmm-element zpmm-title zpmm-image"><a class="zpmm-element-anchor" href="/explore-tycon-solar" target="_self"><picture><img class="zpmm-element-imagecontent" src="/New Wide Icon.jpg" data-fallback="/New Wide Icon.jpg" onerror="handleFallBackForImage(this)" alt="Explore Tycon Solar®"/></picture></a></div> 5686<!-- image end --></div><div class="zpelem-col zpcol-md-3 zpcol-sm-12 zpalign-self-"><!-- image --><div class="zpmm-element zpmm-title zpmm-image"><a class="zpmm-element-anchor" href="/explore-tycon-power" target="_self"><picture><img class="zpmm-element-imagecontent" src="/New Wide Icon -1-.jpg" data-fallback="/New Wide Icon -1-.jpg" onerror="handleFallBackForImage(this)" alt="Explore Tycon Power®"/></picture></a></div> 5687<!-- image end --></div><div class="zpelem-col zpcol-md-3 zpcol-sm-12 zpalign-self-"><!-- image --><div class="zpmm-element zpmm-title zpmm-image"><a class="zpmm-element-anchor" href="/explore-mobilesolarpro" target="_self"><picture><img class="zpmm-element-imagecontent" src="/New Wide Icon -2-.jpg" data-fallback="/New Wide Icon -2-.jpg" onerror="handleFallBackForImage(this)" alt="Explore MobileSolarPro®"/></picture></a></div> 5688<!-- image end --></div><div class="zpelem-col zpcol-md-3 zpcol-sm-12 zpalign-self-"><!-- image --><div class="zpmm-element zpmm-title zpmm-image"><a class="zpmm-element-anchor" href="/explore-tycon-wireless" target="_self"><picture><img class="zpmm-element-imagecontent" src="/New Wide Icon -3-.jpg" data-fallback="/New Wide Icon -3-.jpg" onerror="handleFallBackForImage(this)" alt="Explore Tycon Wireless®"/></picture></a></div> 5689<!-- image end --></div></div></div></div></div></ul></li><li data-megamenu="76150000004002757"><a href="/categories/shop-all-products/6026428000053049080" target="_self" aria-haspopup="true" aria-expanded="false" aria-controls="submenu-Shop-1" onkeydown="handleMenuKeydown(event)"><span class="theme-menu-content "><span class="theme-menu-name" data-theme-menu-name="Shop">Shop</span></span><span class="theme-sub-li-menu theme-non-responsive-menu theme-submenu-down-arrow"></span><span class="theme-sub-li-menu theme-responsive-menu theme-submenu-down-arrow"></span></a><ul class="theme-sub-menu" data-zs-submenu-container id="submenu-Shop-1" style="display:none;"><div data-megamenu-content="76150000004002757" data-megamenu-direction="below" class="zpmm zpmm-container-style-01"><div class="zpcontainer"><div class="zpmm-inner"><div class="zprow zpalign-items- zpjustify-content- seperator-divider"><div class="zpelem-col zpcol-md-3 zpcol-sm-12 zpalign-self-"><!-- title --><div class="zpmm-element zpmm-title "><a class="zpmm-element-anchor"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Tycon Solar®</span></span></a></div> 5690<!-- title end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/tycon-solar/6026428000001432234" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">All Tycon® Solar Products</span></span></a></div> 5691<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/remotepro/6026428000001429102" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">RemotePro®</span></span></a></div> 5692<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/mobilesolarpro/6026428000001429197" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">MobileSolarPro®</span></span></a></div> 5693<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/upspro/6026428000001439117" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">UPSPro®</span></span></a></div> 5694<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/products/ups-rm-2kva-115v/6026428000003687155" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Rackmount Lithium UPS</span></span></a></div> 5695<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/batteries/6026428000037520569" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Batteries</span></span></a></div> 5696<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/ac-battery-chargers/6026428000002821672" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">AC Battery Chargers</span></span></a></div> 5697<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/solar-kits/6026428000002821380" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Solar Kits</span></span></a></div> 5698<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/tpdin/6026428000002821412" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">TPDIN® Monitoring</span></span></a></div> 5699<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/solar-charge-controllers/6026428000037556001" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Solar Charge Controllers</span></span></a></div> 5700<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/breeze-pro/6026428000002821796" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">BreezePro®</span></span></a></div> 5701<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/solar-accessories/6026428000002821626" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Solar Accessories</span></span></a></div> 5702<!-- text end --></div><div class="zpelem-col zpcol-md-3 zpcol-sm-12 zpalign-self-"><!-- title --><div class="zpmm-element zpmm-title "><a class="zpmm-element-anchor"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Tycon Power®</span></span></a></div> 5703<!-- title end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/tyconpower/6026428000001429185" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">All Tycon Power® Products</span></span></a></div> 5704<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/poe-injectors/6026428000002821032" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">PoE Injectors</span></span></a></div> 5705<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/poe-switches/6026428000002821090" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">PoE Switches</span></span></a></div> 5706<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/poe-splitters/6026428000002821114" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">PoE Splitters</span></span></a></div> 5707<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/surge-protectors/6026428000002821142" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Surge Protectors</span></span></a></div> 5708<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/dc-power-supplies/6026428000002821160" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">DC Power Supplies</span></span></a></div> 5709<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/dc-voltage-converters/6026428000002821166" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">DC Voltage Converters</span></span></a></div> 5710<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/accessories/6026428000002821172" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">
5710Power Accessories</span></span></a></div> 5711<!-- text end --></div><div class="zpelem-col zpcol-md-3 zpcol-sm-12 zpalign-self-"><!-- title --><div class="zpmm-element zpmm-title "><a class="zpmm-element-anchor"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">MobileSolarPro®</span></span></a></div> 5712<!-- title end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/mobilesolarpro/6026428000001429197" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">All MobileSolarPro® Products</span></span></a></div> 5713<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/mobilesolarpro-mini/6026428000002821324" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">MobileSolarPro® Mini</span></span></a></div> 5714<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/mobilesolarpro-beast/6026428000002821352" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">MobileSolarPro® Beast</span></span></a></div> 5715<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/skid-system/6026428000020019319" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">
5715MobileSolarPro® Skid System</span></span></a></div> 5716<!-- text end --></div><div class="zpelem-col zpcol-md-3 zpcol-sm-12 zpalign-self-"><!-- title --><div class="zpmm-element zpmm-title "><a class="zpmm-element-anchor"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Tycon Wireless®</span></span></a></div> 5717<!-- title end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/ez-bridge/6026428000002821862" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">E-Z Bridge®</span></span></a></div> 5718<!-- text end --><!-- text --><div class="zpmm-element zpmm-text "><a class="zpmm-element-anchor" href="/categories/cellular-wifi-routers/6026428000002821424" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Cellular / WiFi Routers</span></span></a></div> 5719<!-- text end --></div></div></div></div></div></ul></li><li data-megamenu="76150000014619247"><a href="/collections/le-upspro-collection/6026428000050693003" target="_self" aria-haspopup="true" aria-expanded="false" aria-controls="submenu-Limited Edition-1" onkeydown="handleMenuKeydown(event)"><span class="theme-menu-content "><span class="theme-menu-name" data-theme-menu-name="Limited Edition">Limited Edition</span></span><span class="theme-sub-li-menu theme-non-responsive-menu theme-submenu-down-arrow"></span><span class="theme-sub-li-menu theme-responsive-menu theme-submenu-down-arrow"></span></a><ul class="theme-sub-menu" data-zs-submenu-container id="submenu-Limited Edition-1" style="display:none;"><div data-megamenu-content="76150000014619247" data-megamenu-direction="below" class="zpmm zpmm-container-style-01"><div class="zpcontainer"><div class="zpmm-inner"><div class="zprow zpalign-items- zpjustify-content- seperator-divider"><div class="zpelem-col zpcol-md-12 zpcol-sm-12 zpalign-self-"><!-- title --><div class="zpmm-element zpmm-title "><a class="zpmm-element-anchor" href="/collections/le-upspro-collection/6026428000050693003" target="_self"><span class="zpmm-element-menu-content "><span class="zpmm-element-textcontent">Special Builds at Special Prices</span></span></a></div> 5720<!-- title end --></div></div></div></div></div></ul></li></ul><div data-zp-submenu-icon="mymenu1" style="display:none;"><span class="theme-sub-li-menu theme-non-responsive-menu"></span><span class="theme-sub-li-menu theme-responsive-menu theme-submenu-down-arrow"></span></div> 5721</div></div><div data-search-cart-scrollposition-non-responsive="zptheme-search-cart-scrollposition-non-responsive" class="theme-search-cart-non-res theme-search-cart-scrollposition-non-responsive"></div> 5722</div></div></div></div><div data-zs-mobile-content-wrap class="theme-mobile-content-wrap "><div data-theme-content-container="theme-content-container" class="zpsection theme-pages-full-stretch"><main class="zpcontainer" data-zs-app="blogs" id="main-content"><h1 class="theme-blog-h1">Tycon Cases & Applications Articles</h1><div class="zprow" data-zs-app="blog_comments"><div class="zpcol-md-9 zpcol-sm-12 theme-blog-container-column"><h1 class="theme-blog-list-seo"> Blog categorized as Tycon Cases & Applications Articles </h1><div class="theme-blog-container"><div class="theme-blog-part theme-blog-heading-author theme-blog-heading-space"><h3 class="theme-blog-heading"><a href="/blogs/post/off-grid-solar-security-for-remote-infrastructure-tycon®-case-study" data-blogpost-heading="true">Off-Grid Solar Security for Remote Infrastructure | Tycon® Case Study</a></h3><p> By <a href="/blogs/author/tycon-systems11" class="theme-blog-author" data-blog-author="true">Tycon Systems</a></p></div> 5723<div class="theme-blog-cover-img-container"><a href="/blogs/post/off-grid-solar-security-for-remote-infrastructure-tycon®-case-study"><picture><img src="/images/Blog Images/2026 MAY/Case Study -6 - Securing Remote Infrastructure.png" class="theme-blog-cover-img" alt="Off-Grid Solar Security for Remote Infrastructure | Tycon® Case Study"/></picture></a></div> 5724<div class="theme-blog-part theme-blog-author"> See how a 280W Tycon® RemotePro® system powered two security cameras and a 2 km wireless link at a remote water treatment facility access point. </div> 5725<div class="theme-blog-part theme-blog-date-container"><div class="theme-blog-date-comment-count"><span class="theme-blog-date"> 01.09.26 07:00 AM </span><span class="theme-blog-comment-count"><a href="/blogs/post/off-grid-solar-security-for-remote-infrastru
5725cture-tycon®-case-study#comments"> - <span data-post-id="76150000016365016"></span> Comment(s) </a></span></div> 5726<div class="theme-blog-readmore"><a href="/blogs/post/off-grid-solar-security-for-remote-infrastructure-tycon®-case-study" data-blogpost-heading="true">Read more</a></div> 5727</div></div><div class="theme-blog-container"><div class="theme-blog-part theme-blog-heading-author theme-blog-heading-space"><h3 class="theme-blog-heading"><a href="/blogs/post/ensuring-port-connectivity-a-tycon-systems®-case-study" data-blogpost-heading="true">Outdoor UPS Backup for Port WiFi Networks | Tycon®</a></h3><p> By <a href="/blogs/author/tycon-systems11" class="theme-blog-author" data-blog-author="true">Tycon Systems</a></p></div> 5728<div class="theme-blog-cover-img-container"><a href="/blogs/post/ensuring-port-connectivity-a-tycon-systems®-case-study"><picture><img src="/images/Blog Images/2026 MAY/Case Study -5 - Ensuring Port Connectivity.png?v=1777416496" class="theme-blog-cover-img" alt="Outdoor UPS Backup for Port WiFi Networks | Tycon®"/></picture></a></div> 5729<div class="theme-blog-part theme-blog-author"> See how eight Tycon® UPSPro® systems provided at least three hours of backup power for WiFi access points and CPE devices at a container port terminal. </div> 5730<div class="theme-blog-part theme-blog-date-container"><div class="theme-blog-date-comment-count"><span class="theme-blog-date"> 12.05.26 11:20 AM </span><span class="theme-blog-comment-count"><a href="/blogs/post/ensuring-port-connectivity-a-tycon-systems®-case-study#comments"> - <span data-post-id="76150000010005421"></span> Comment(s) </a></span></div> 5731<div class="theme-blog-readmore"><a href="/blogs/post/ensuring-port-connectivity-a-tycon-systems®-case-study" data-blogpost-heading="true">Read more</a></div> 5732</div></div><div class="theme-blog-container"><div class="theme-blog-part theme-blog-heading-author theme-blog-heading-space"><h3 class="theme-blog-heading"><a href="/blogs/post/managing-workforce-compliance-a-tycon-systems®-case-study" data-blogpost-heading="true">Backup Power for Construction Site Access Control | Tycon®</a></h3><p> By <a href="/blogs/author/tycon-systems11" class="theme-blog-author" data-blog-author="true">Tycon Systems</a></p></div> 5733<div class="theme-blog-cover-img-container"><a href="/blogs/post/managing-workforce-compliance-a-tycon-systems®-case-study"><picture><img src="/images/Blog Images/2026 MAY/Case Study -4 - Managing Workforce Compliance.png?v=1777416489" class="theme-blog-cover-img" alt="Backup Power for Construction Site Access Control | Tycon®"/></picture></a></div> 5734<div class="theme-blog-part theme-blog-author"> See how a Tycon® outdoor UPS supported badge readers, a 3G router and control hardware for workforce access and compliance systems on Belgian construction sites. </div> 5735<div class="theme-blog-part theme-blog-date-container"><div class="theme-blog-date-comment-count"><span class="theme-blog-date"> 12.05.26 11:11 AM </span><span class="theme-blog-comment-count"><a href="/blogs/post/managing-workforce-compliance-a-tycon-systems®-case-study#comments"> - <span data-post-id="76150000010005410"></span> Comment(s) </a></span></div> 5736<div class="theme-blog-readmore"><a href="/blogs/post/managing-workforce-compliance-a-tycon-systems®-case-study" data-blogpost-heading="true">Read more</a></div> 5737</div></div><div class="theme-blog-container"><div class="theme-blog-part theme-blog-heading-author theme-blog-heading-space"><h3 class="theme-blog-heading"><a href="/blogs/post/mobile-surveillance-anywhere-a-tycon-systems®-case-study" data-blogpost-heading="true">Off-Grid Solar for Mobile Surveillance | Tycon® Case Study</a></h3><p> By <a href="/blogs/author/tycon-systems11" class="theme-blog-author" data-blog-author="true">Tycon Systems</a></p></div> 5738<div class="theme-blog-cover-img-container"><a href="/blogs/post/mobile-surveillance-anywhere-a-tycon-systems®-case-study"><picture><img src="/images/Blog Images/2026 APRIL/Case Study-3 - Mobile Surveillance Anywhere.png?v=1776348360" class="theme-blog-cover-img" alt="Off-Grid Solar for Mobile Surveillance | Tycon® Case Study"/></picture></a></div> 5739<div class="theme-blog-part theme-blog-author"> See how San Diego CCTV Pros used Tycon® RemotePro® solar power to run mobile surveillance trailers with IP cameras and cellular connectivity off-grid. </div> 5740<div class="theme-blog-part theme-blog-date-container"><div class="theme-blog-date-comment-count"><span class="theme-blog-date"> 29.04.26 07:00 AM </span><span class="theme-blog-comment-count"><a href="/blogs/post/mobile-surveillance-anywhere-a-tycon-systems®-case-study#comments"> - <span data-post-id="76150000009160294"></span> Comment(s) </a></span></div> 5741<div class="theme-blog-readmore"><a href="/blogs/post/mobile-surveillance-anywhere-a-tycon-systems®-case-study" data-blogpost-heading="true">Read more</a></div> 5742</div></div><div class="theme-blog-container"><div class="theme-blog-part theme-blog-heading-author theme-blog-heading-space"><h3 class="theme-blog-heading">
5742<a href="/blogs/post/powering-remote-living-a-tycon-systems®-case-study" data-blogpost-heading="true">Off-Grid Cabin Solar Power Case Study | Tycon®</a></h3><p> By <a href="/blogs/author/tycon-systems11" class="theme-blog-author" data-blog-author="true">Tycon Systems</a></p></div> 5743<div class="theme-blog-cover-img-container"><a href="/blogs/post/powering-remote-living-a-tycon-systems®-case-study"><picture><img src="/images/Blog Images/2026 APRIL/Case Study-2 - Powering Remote Living.png?v=1776348296" class="theme-blog-cover-img" alt="Off-Grid Cabin Solar Power Case Study | Tycon®"/></picture></a></div> 5744<div class="theme-blog-part theme-blog-author"> See how Tycon® solar and battery power reduced generator dependence for a remote Utah cabin with a 210W continuous load and year-round power needs. </div> 5745<div class="theme-blog-part theme-blog-date-container"><div class="theme-blog-date-comment-count"><span class="theme-blog-date"> 28.04.26 10:28 AM </span><span class="theme-blog-comment-count"><a href="/blogs/post/powering-remote-living-a-tycon-systems®-case-study#comments"> - <span data-post-id="76150000009160283"></span> Comment(s) </a></span></div> 5746<div class="theme-blog-readmore"><a href="/blogs/post/powering-remote-living-a-tycon-systems®-case-study" data-blogpost-heading="true">Read more</a></div> 5747</div></div><div class="theme-blog-container theme-blog-prev-next"><a href="/blogs/tycon-cases-applications-articles/page/2" class="theme-blog-next-link" data-blog-nextpage="true"> Next <span><svg aria-hidden="true" focusable="false" viewBox="13 14 17 12" xmlns="http://www.w3.org/2000/svg"><path d="M24.4476024,14.1797714 C24.2160051,13.9400762 23.8305494,13.9400762 23.5908542,14.1797714 C23.3592569,14.4113687 23.3592569,14.7968244 23.5908542,15.0278819 L27.9350587,19.3720864 L13.5997777,19.3720864 C13.2656081,19.3726262 13,19.6382344 13,19.9724039 C13,20.3065735 13.2656081,20.5808193 13.5997777,20.5808193 L27.9350587,20.5808193 L23.5908542,24.916926 C23.3592569,25.1566211 23.3592569,25.5426167 23.5908542,25.7736742 C23.8305494,26.0133693 24.2165449,26.0133693 24.4476024,25.7736742 L29.8202286,20.401048 C30.0599238,20.1694506 30.0599238,19.7839949 29.8202286,19.5529374 L24.4476024,14.1797714 Z"></path></svg></span></a></div> 5748</div><div class="zpcol-md-3 zpcol-sm-12 theme-blog-category-column"><div class="theme-blog-category-container"><div class="theme-blog-category-part theme-blog-category-heading-container"><h4 class="theme-blog-category-heading" id="theme-bloglist-category-heading">Categories</h4></div> 5749<div class="theme-blog-category-part theme-blog-category-list"><ul aria-labelledby="theme-bloglist-category-heading"><li class="theme-blog-category-list"><a href="/blogs/Uncategorized" class="theme-blog-side-menu-link">Uncategorized</a><p class="theme-blog-count">(1)</p></li><li class="theme-blog-category-list"><a href="/blogs/tycon-solar-articles" class="theme-blog-side-menu-link">Tycon Solar Articles</a><p class="theme-blog-count">(11)</p></li><li class="theme-blog-category-list"><a href="/blogs/tycon-power-articles" class="theme-blog-side-menu-link">Tycon Power Articles</a><p class="theme-blog-count">(1)</p></li><li class="theme-blog-category-list"><a href="/blogs/mobilesolarpro-articles" class="theme-blog-side-menu-link">MobileSolarPro Articles</a><p class="theme-blog-count">(1)</p></li><li class="theme-blog-category-list"><a href="/blogs/tycon-wireless-articles" class="theme-blog-side-menu-link">Tycon Wireless Articles</a><p class="theme-blog-count">(0)</p></li><li class="theme-blog-category-list"><a href="/blogs/tycon-guides-articles" class="theme-blog-side-menu-link">Tycon Guides Articles</a><p class="theme-blog-count">(3)</p></li><li class="theme-blog-category-list"><a href="/blogs/tycon-news-articles" class="theme-blog-side-menu-link">Tycon News Articles</a><p class="theme-blog-count">(12)</p></li><li class="theme-blog-category-list"><a href="/blogs/tycon-tradeshows-articles" class="theme-blog-side-menu-link">Tycon Tradeshows Articles</a><p class="theme-blog-count">(1)</p></li><li class="theme-blog-category-list"><a href="/blogs/tycon-cases-applications-articles" class="theme-blog-side-menu-link">Tycon Cases & Applications Articles</a><p class="theme-blog-count">(7)</p></li></ul></div> 5750</div><div class="theme-blog-category-container"><div class="theme-blog-category-part theme-blog-category-heading-container"><h4 class="theme-blog-category-heading" id="theme-bloglist-tags-heading">Tags</h4></div> 5751<div class="theme-blog-category-part theme-blog-tag-list"><ul aria-labelledby="theme-bloglist-tags-heading"><li class="theme-blog-category-tag-list"><a href="/blogs/tag/all" class="theme-blog-category-tag-list-link">All</a></li><li class="theme-blog-category-tag-list"><a href="/blogs/tag/solar-power" class="theme-blog-category-tag-list-link">Solar power</a></li><li class="theme-blog-category-tag-list"><a href="/blogs/tag/remote-power" class="theme-blog-category-tag-list-link">Remote Power</a></li><li class="theme-blog-category-tag-list"><a href="/blogs/tag/tycon-systems" class="theme-blog-category-tag-list-link">Tycon Systems</a></li></ul></div> 5752</div><div class="theme-blog-category-container theme-blog-rss-feed">
5752<a href="https://www.tyconsystems.com/blogs/tycon-cases-applications-articles/feed" target="_blank"><svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-4 -4)" fill="none" fill-rule="evenodd"><path d="M0 0h24v24H0"/><circle fill-rule="nonzero" cx="6.18" cy="17.82" r="2.18"/><path d="M4 4.44v2.83c7.03 0 12.73 5.7 12.73 12.73h2.83c0-8.59-6.97-15.56-15.56-15.56zm0 5.66v2.83c3.9 0 7.07 3.17 7.07 7.07h2.83c0-5.47-4.43-9.9-9.9-9.9z" fill-rule="nonzero"/></g></svg><span>Subscribe to RSS Feed</span></a></div> 5753</div></div></main></div><div class="theme-footer-area zpdark-section zpdark-section-bg theme-pages-full-stretch "><div data-footer-type='site_footer'><div class="zpcontent-container footer-container "><div data-element-id="elm_Kden50nmSB2lL8ZQAoiQ9Q" data-element-type="section" class="zpsection zpdefault-section zpdefault-section-bg footer " style="background-color:rgb(83, 85, 86);background-image:unset;"><style type="text/css"> [data-element-id="elm_Kden50nmSB2lL8ZQAoiQ9Q"].zpsection{ border-radius:1px; padding-block-start:80px; padding-inline-end:100px; padding-block-end:20px; padding-inline-start:100px; } @media (max-width: 767px) { [data-element-id="elm_Kden50nmSB2lL8ZQAoiQ9Q"].zpsection{ padding-block-start:40px; padding-inline-end:12px; padding-block-end:0px; padding-inline-start:12px; } } </style><div class="zpcontainer-fluid"><div data-element-id="elm_NoDQmwtiRkqwzJiWmvd9Mw" data-element-type="row" class="zprow zpalign-items-flex-start zpjustify-content-flex-start zpdefault-section zpdefault-section-bg zp-hidden-sm zp-hidden-xs "><style type="text/css"></style><div data-element-id="elm_00Iei44hSl2kla3CexDpsQ" data-element-type="column" class="zpelem-col zpcol-12 zpcol-md-6 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"> [data-element-id="elm_00Iei44hSl2kla3CexDpsQ"].zpelem-col{ padding-inline-end:0px; padding-inline-start:0px; } </style><div data-element-id="elm_kQC8lSJiT8yqk6RhUKIbgw" data-element-type="image" class="zpelement zpelem-image "><style> @media (min-width: 992px) { [data-element-id="elm_kQC8lSJiT8yqk6RhUKIbgw"] .zpimage-container figure img { width: 200px ; height: 62.63px ; } } @media (max-width: 991px) and (min-width: 768px) { [data-element-id="elm_kQC8lSJiT8yqk6RhUKIbgw"] .zpimage-container figure img { width:200px ; height:62.63px ; } } @media (max-width: 767px) { [data-element-id="elm_kQC8lSJiT8yqk6RhUKIbgw"] .zpimage-container figure img { width:200px ; height:62.63px ; } } [data-element-id="elm_kQC8lSJiT8yqk6RhUKIbgw"].zpelem-image { padding-block-end:32px; margin:0px; } </style><div data-caption-color="" data-size-tablet="" data-size-mobile="" data-align="left" data-tablet-image-separate="false" data-mobile-image-separate="false" class="zpimage-container zpimage-align-left zpimage-tablet-align-center zpimage-mobile-align-center zpimage-size-small zpimage-tablet-fallback-small zpimage-mobile-fallback-small "><figure role="none" class="zpimage-data-ref"><a class="zpimage-anchor" href="/" target="" title="Home" rel=""><picture><img class="zpimage zpimage-style-none zpimage-space-none " src='https://cdn1.zohoecommerce.com/images/Icons/Photo%20Jun%2010%202024-%206%2037%2055%20AM%20-1-.png?v=1763674583&storefront_domain=www.tyconsystems.com' width="200" height="62.63" loading="lazy" size="small" alt=""/></picture></a></figure></div> 5754</div><div data-element-id="elm_ktBDWuwZTKKEPmceWLehRA" data-element-type="row" class="zprow zs-social-profile zpalign-items-flex-start zpjustify-content-flex-start zpdefault-section zpdefault-section-bg "><style type="text/css"> [data-element-id="elm_ktBDWuwZTKKEPmceWLehRA"].zprow{ padding-block-start:32px; margin:0px; } </style><div data-element-id="elm_V-0I0r5ySm2EF3BSjTGb7g" data-element-type="column" class="zpelem-col zpcol-12 zpcol-md-6 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"> [data-element-id="elm_V-0I0r5ySm2EF3BSjTGb7g"].zpelem-col{ border-style:solid; border-color:#ccc !important; border-block-start-width:0px; border-inline-end-width:0px; border-block-end-width:1px; border-inline-start-width:0px; padding:0px; margin-inline-end:20px; } </style><div data-element-id="elm_LDMnr-KjRcuUIl7zVIdCSA" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_LDMnr-KjRcuUIl7zVIdCSA"].zpelem-text{ font-size:16px; line-height:24px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><span style="font-family:poppins;">Follow us</span></p></div> 5755</div></div><div data-element-id="elm_Y8vmpPu_ScmWNynO1fc_5w" data-element-type="column" class="zpelem-col zpcol-12 zpcol-md-6 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"> [data-element-id="elm_Y8vmpPu_ScmWNynO1fc_5w"].zpelem-col{ padding:0px; } </style><div data-element-id="elm_h9Ddw6LiSROdfQmgqyk2Rg" itemscope="" data-element-type="socialprofile" class="zpelement zpelem-socialprofile" data-element-id="elm_h9Ddw6LiSROdfQmgqyk2Rg"><style type="text/css"> [data-element-id="elm_h9Ddw6LiSROdfQmgqyk2Rg"].zpelem-socialprofile{ margin:0px; } </style><div data-socialprofile_container class="zpsocialprofile-container zpsocialprofile-size-lg zpsocialprofile-halign-left zpsocialprofile-style-none zpsocialprofile-type-white "><a href="https://www.linkedin.com/company/tycon-systems-inc-/" class="zpsocialprofile-wrapper zpsocialprofile-linkedin" target="_blank" title="LinkedIn"><svg aria-hidden="true" class="zpsocialprofile" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><path d="M64 4.706v54.588A4.706 4.706 0 0 1 59.294 64H4.706A4.706 4.706 0 0 1 0 59.294V4.706A4.706 4.706 0 0 1 4.706 0h54.588A4.706 4.706 0 0 1 64 4.706zM18.824 24.47H9.412v30.117h9.412V24.471zm.847-10.353a5.421 5.421 0 0 0-5.384-5.46h-.17a5.459 5.459 0 0 0 0 10.918 5.421 5.421 0 0 0 5.554-5.289v-.17zm34.917 22.174c0-9.054-5.76-12.574-11.482-12.574a10.73 10.73 0 0 0-9.525 4.856h-.263v-4.103H24.47v30.117h9.411V38.57a6.25 6.25 0 0 1 5.647-6.738h.358c2.993 0 5.214 1.882 5.214 6.625v16.132h9.412l.075-18.296z"/></svg></a><a href="https://www.facebook.com/tyconsystems/" class="zpsocialprofile-wrapper zpsocialprofile-facebook" target="_blank" title="Facebook"><svg aria-hidden="true" class="zpsocialprofile" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><path d="M64,32.1989637 C64,14.4248705 49.6642636,0 32,0 C14.3357364,0 -8.52651283e-14,14.4248705 -8.52651283e-14,32.1989637 C-8.52651283e-14,48.2818653 11.6992791,61.6124352 27.0236869,64 L27.0236869,41.4839378 L18.8506694,41.4839378 L18.8506694,32.1989637 L27.0236869,32.1989637 L27.0236869,25.1025907 C27.0236869,17.0445596 31.76931,12.5678756 39.0854789,12.5678756 C42.5787848,12.5678756 46.2039135,13.2310881 46.2039135,13.2310881 L46.2039135,21.0901554 L42.1833162,21.0901554 C38.1956746,21.0901554 36.9433574,23.6103627 36.9433574,26.1305699 L36.9433574,32.1658031 L45.8414006,32.1658031 L44.4243048,41.4507772 L36.9433574,41.4507772 L36.9433574,63.9668394 C52.3007209,61.6124352 64,48.2818653 64,32.1989637 Z"></path></svg></a><a href="https://twitter.com/TyconSys" class="zpsocialprofile-wrapper zpsocialprofile-twitter" target="_blank" title="X"><svg aria-hidden="true" class="zpsocialprofile" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill-rule="nonzero" d="M37.97 27.1 61.283 0h-5.525L35.516 23.53 19.348 0H.7l24.449 35.582L.7 64h5.525l21.377-24.849L44.676 64h18.648L37.968 27.1h.002Zm-7.567 8.795-2.477-3.543L8.216 4.16h8.485l15.906 22.753 2.478 3.543L55.76 60.03h-8.486L30.403 35.897v-.002Z"/></svg></a><a href="https://www.youtube.com/@TyconsystemsHQ" class="zpsocialprofile-wrapper zpsocialprofile-youtube" target="_blank" title="YouTube"><svg aria-hidden="true" class="zpsocialprofile" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><path d="M32.786 10l.106.001.392.002c1.94.009 6.896.05 11.873.237l.765.03c4.589.187 9.063.504 11.068 1.04 2.71.73 4.84 2.86 5.56 5.56 1.204 4.505 1.301 13.515 1.31 14.939v.382c-.009 1.424-.106 10.434-1.31 14.939a7.885 7.885 0 0 1-5.56 5.56c-4.455 1.19-21.107 1.3-24.098 1.309h-.923c-2.99-.01-19.635-.118-24.099-1.309a7.885 7.885 0 0 1-5.56-5.56c-.401-1.502-.68-3.504-.873-5.559l-.058-.65c-.314-3.688-.368-7.394-.37
57557-8.552L1 32.124v-.194-.054l.002-.245c.009-1.158.063-4.864.377-8.552l.058-.65c.193-2.055.472-4.057.873-5.559a7.885 7.885 0 0 1 5.56-5.56c4.296-1.146 19.878-1.29 23.706-1.307L31.97 10h.105zM26.15 22.57v18.86L42.48 32l-16.33-9.43z"/></svg></a><a href="https://www.instagram.com/tyconsystems/" class="zpsocialprofile-wrapper zpsocialprofile-instagram" target="_blank" title="Instagram"><svg aria-hidden="true" class="zpsocialprofile" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="72.5875139%" y1="0%" x2="28.281045%" y2="106.524963%" id="linearGradient-1"><stop stop-color="#9A1DF1" offset="0%"></stop><stop stop-color="#ED1E64" offset="49.3618943%"></stop><stop stop-color="#FFD002" offset="100%"></stop></linearGradient><path d="M18.7470755,0.221595251 C15.3422244,0.382299247 13.0166948,0.927668835 10.9845413,1.72394402 C8.8808805,2.5440464 7.09761014,3.64310557 5.32364538,5.42434073 C3.54758782,7.20661909 2.45687425,8.99304465 1.64301187,11.0988238 C0.855031083,13.1361614 0.321066295,15.4627981 0.170711099,18.8697484 C0.0214311023,22.2829708 -0.012802497,23.3716107 0.00383750267,32.0611721 C0.0204135023,40.7496967 0.0587623014,41.8403975 0.221527098,45.254631 C0.384285494,48.6585158 0.927619882,50.9830597 1.72388866,53.0162565 C2.54502785,55.1199428 3.64296062,56.9022276 5.42521338,58.6772547 C7.20645494,60.4522883 8.9939045,61.5419779 11.1006629,62.3569282 C13.1359204,63.1438722 15.46353,63.6799042 18.8694627,63.8291842 C22.2826082,63.9795458 23.3722466,64.0127042 32.0596384,63.9961282 C40.7511454,63.9795458 41.8408478,63.941165 45.2539933,63.7794178 C48.6588444,63.616653 50.9822875,63.0712898 53.0165275,62.2770946 C55.1201883,61.4538563 56.9034586,60.3579395 58.6774234,58.5756547 C60.4513945,56.7943876 61.5421081,55.0069188 62.3560345,52.9001285 C63.1440153,50.8648453 63.6800089,48.5372038 63.8282777,45.1333446 C63.9775577,41.7180679 64.0128665,40.6273351 63.9962265,31.9388169 C63.9796441,23.2492555 63.9402841,22.1606092 63.7785369,18.7484684 C63.6167897,15.3414861 63.0714329,13.0179854 62.2761753,10.9837454 C61.4540249,8.88005905 60.3571033,7.0987919 58.5758618,5.32274074 C56.7946202,3.54875038 55.0071771,2.4559312 52.9004123,1.64411682 C50.8641371,0.856129637 48.5375324,0.319048049 45.1316061,0.171822452 C41.7184606,0.0204496555 40.6288222,-0.0127087437 31.9383264,0.00387365592 C23.2499298,0.0204560555 22.160285,0.0577936547 18.7470755,0.221595251 M19.1203747,58.0696835 C16.0006051,57.9338499 14.3064996,57.4154436 13.1774308,56.9810372 C11.682346,56.4024772 10.6154853,55.7088452 9.49261809,54.5942852 C8.37184371,53.4755524 7.67612533,52.4117765 7.09241974,50.9198021 C6.65381495,49.7906885 6.12611576,48.0986182 5.97993337,44.9788358 C5.82129017,41.6060871 5.78502777,40.5941383 5.76945657,32.0497481 C5.75287417,23.5074443 5.78394617,22.4954955 5.93120377,19.1196428 C6.06491897,16.0019213 6.58648055,14.3057037 7.01983094,13.1776334 C7.59834613,11.6804686 8.28988531,10.6156558 9.40655089,9.49280784 C10.5252389,8.36992787 11.5889956,7.67628948 13.081994,7.0925775 C14.2100452,6.65191191 15.9021283,6.12831512 19.0208227,5.98007832 C22.3956194,5.82038552 23.4064738,5.78618392 31.9486944,5.76960153 C40.4929438,5.75298713 41.5048734,5.78307992 44.8807453,5.93134872 C47.9983644,6.06715032 49.6945564,6.58350231 50.8216028,7.0199951 C52.3176987,7.59852308 53.3845595,8.28801427 54.5064091,9.40674704 C55.628201,10.5254798 56.323913,11.5871694 56.9076186,13.0832846 C57.3482458,14.2082189 57.8718362,15.9023821 58.0190938,19.0201036 C58.1797594,22.3949387 58.2160858,23.4068555 58.231593,31.9491913 C58.2481754,40.4935879 58.2171034,41.5054983 58.0688346,44.8792902 C57.933033,47.9990726 57.4156442,49.6942725 56.9802074,50.8243973 C56.4016858,52.3184581 55.710089,53.3853316 54.5924123,54.5082116 C53.4747995,55.6269444 52.4110427,56.3246916 50.9169691,56.9084356 C49.7910044,57.348026 48.0968988,57.8726723 44.9802333,58.0209411 C41.6055006,58.1795587 40.5945822,58.2158467 32.0492576,58.2314179 C23.507037,58.2480131 22.4961826,58.2158659 19.1203747,58.0696835 M45.2073489,14.8946189 C45.2114717,17.0138765 46.9346204,18.729806 49.053846,18.7256663 C51.1740891,18.7215116 52.8900251,16.9993549 52.8869253,14.8800973 C52.8827483,12.7608398 51.1595931,11.043899 49.03935,11.0480387 C46.9191132,11.052187 45.2031773,12.7753614 45.2073489,14.8946189 M15.5693094,32.0310857 C15.5869347,41.1063495 22.957565,48.4469894 32.0305952,48.4293954 C41.1046942,48.4117446 48.4493852,41.0430983 48.4318231,31.9678665 C48.414134,22.8957323 41.0424926,15.5509133 31.967376,15.5685072 C22.8942818,15.5861645 15.5516836,22.9579403 15.5693094,32.0310857 M21.3328633,32.0196937 C21.3225058,26.1295435 26.0897377,21.344654 31.978768,21.3342528 C37.868867,21.3228812 42.6547101,26.0880651 42.6661227,31.9792585 C42.6775581,37.870452 37.9103198,42.6542983 32.0192032,42.6657427 C26.1301793,42.6771143 21.3442722,37.9108872 21.3328633,32.0196937" id="path-2"></path></defs><g stroke="none" stroke-width="1" fill-rule="evenodd"><mask fill="white"><use xlink:href="#path-2"></use></mask><use class="cl-path-inherit" fill="url(#linearGradient-1)" xlink:href="#path-2"></use></g></svg></a></div> 5756</div></div></div></div><div data-element-id="elm_OQejbnnsS-m-0Bw7n1Phcg" data-element-type="column" class="zpelem-col zpcol-12 zpcol-md-2 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"> [data-element-id="elm_OQejbnnsS-m-0Bw7n1Phcg"].zpelem-col{ padding-inline-end:0px; padding-inline-start:24px; } </style><div data-element-id="elm_ZBZus7QtS5aHzEdcL7UQkA" data-element-type="row" class="zprow zpalign-items-flex-start zpjustify-content-flex-start zpdefault-section zpdefault-section-bg "><style type="text/css"> [data-element-id="elm_ZBZus7QtS5aHzEdcL7UQkA"].zprow{ padding-block-end:48px; } </style><div data-element-id="elm_5-vF7jv0RaK1J2g4RTw0GA" data-element-type="column" class="zpelem-col zpcol-12 zpcol-md-12 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"></style><div data-element-id="elm_LH4K1LY3RNa7GxxxWq2TLA" data-element-type="heading" class="zpelement zpelem-heading "><style> [data-element-id="elm_LH4K1LY3RNa7GxxxWq2TLA"] h2.zpheading{ font-family:'Poppins'; font-size:16px; font-weight:600; line-height:16px; text-decoration:underline; } [data-element-id="elm_LH4K1LY3RNa7GxxxWq2TLA"].zpelem-heading { border-style:none; margin:0px; } </style><h2 5757 class="zpheading zpheading-style-none zpheading-align-left " data-editor="true"><span style="font-family:Oswald, sans-serif;font-size:20px;">Tycon Solar<span style="color:inherit;">®</span></span></h2></div> 5758<div data-element-id="elm_aCA2vCI3S5mHVbykCGfWFQ" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_aCA2vCI3S5mHVbykCGfWFQ"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/mobilesolarpro%C2%AE/6026428000001429197" title="Mobile SolarPro®" rel="" style="color:rgb(255, 255, 255);">Mobile SolarPro®</a></p></div> 5759</div><div data-element-id="elm_FN9tGhZNSKW_jwsfLXPhaw" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_FN9tGhZNSKW_jwsfLXPhaw"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/remotepro%C2%AE/6026428000001429102" title="RemotePro®" rel="" style="color:rgb(255, 255, 255);">RemotePro®</a></p></div> 5760</div><div data-element-id="elm_fiGwDHKARTGYZaaO1-Kcnw" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_fiGwDHKARTGYZaaO1-Kcnw"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/upspro%C2%AE/6026428000001439117" title="UPSPro®" rel="" style="color:rgb(255, 255, 255);">UPSPro®</a></p></div> 5761</div><div data-element-id="elm_BAQUZUPKTyOvVOZo_CQMSw" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_BAQUZUPKTyOvVOZo_CQMSw"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/tpdin/6026428000002821412" title="TPDIN®" rel="" style="color:rgb(255, 255, 255);">TPDIN®</a></p></div> 5762</div><div data-element-id="elm_6027UNDvJQBasis6W4Y-eg" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_6027UNDvJQBasis6W4Y-eg"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/solar-charge-controllers/6026428000037556001" title="MPPT Charge Controllers" rel="" style="color:rgb(255, 255, 255);">MPPT Charge Controllers</a></p></div> 5763</div><div data-element-id="elm_c6FJoQZvkJ1nMayD-1Yxsg" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_c6FJoQZvkJ1nMayD-1Yxsg"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/batteries/6026428000037520569" title="Lithium Batteries" rel="" style="color:rgb(255, 255, 255);">Lithium Batteries</a></p></div> 5764</div></div></div><div data-element-id="elm_3jFHRL6vSMelpy7A4djbIw" data-element-type="row" class="zprow zpalign-items-flex-start zpjustify-content-flex-start zpdefault-section zpdefault-section-bg "><style type="text/css"></style><div data-element-id="elm_xzSyVQzVToic8i04995qug" data-element-type="column" class="zpelem-col zpcol-12 zpcol-md-12 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"></style><div data-element-id="elm_rlUQKKt3RU2_-81AcfZZjg" data-element-type="heading" class="zpelement zpelem-heading "><style> [data-element-id="elm_rlUQKKt3RU2_-81AcfZZjg"] h2.zpheading{ font-family:'Poppins'; font-size:16px; font-weight:600; line-height:16px; text-decoration:underline; } [data-element-id="elm_rlUQKKt3RU2_-81AcfZZjg"].zpelem-heading { margin:0px; } </style><h2 5765 class="zpheading zpheading-style-none zpheading-align-left " data-editor="true"><span style="font-family:Oswald, sans-serif;font-size:20px;">Company</span></h2></div> 5766<div data-element-id="elm_kLlqjV6ySLeIJjoxAt2UPA" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_kLlqjV6ySLeIJjoxAt2UPA"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/patents-trademarks" title="Patents & Trademarks" rel="" style="color:rgb(255, 255, 255);">Patents & Trademarks</a></p></div> 5767</div><div data-element-id="elm_Rxl8qWai76IxInmL9s9TCw" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_Rxl8qWai76IxInmL9s9TCw"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/awards" title="Awards" rel="" style="color:rgb(255, 255, 255);">Awards</a></p></div> 5768</div><div data-element-id="elm_zvFfC4BMS46aRVDfgNIWRw" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_zvFfC4BMS46aRVDfgNIWRw"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/about" title="Who We Are" rel="" style="color:rgb(255, 255, 255);">Who We Are</a></p></div> 5769</div>
5769<div data-element-id="elm_1JchSGp0RveHQac8KiP5PQ" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_1JchSGp0RveHQac8KiP5PQ"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/contact" title="Contact Us" rel="" style="color:rgb(255, 255, 255);">Contact Us</a></p></div> 5770</div><div data-element-id="elm_qI--iz3PJR-3CwlkHKRMAA" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_qI--iz3PJR-3CwlkHKRMAA"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/Tycon%202026%20Product%20Catalog.pdf" target="_blank" rel="" style="color:rgb(255, 255, 255);">Full Product Catalog</a></p></div> 5771</div></div></div></div><div data-element-id="elm_rzq7mlWfTxa1BbwvcAFWqQ" data-element-type="column" class="zpelem-col zpcol-12 zpcol-md-2 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"> [data-element-id="elm_rzq7mlWfTxa1BbwvcAFWqQ"].zpelem-col{ padding-inline-end:0px; padding-inline-start:24px; } </style><div data-element-id="elm_TK1qy9cyRIa6jawzI5Q4dQ" data-element-type="row" class="zprow zpalign-items-flex-start zpjustify-content-flex-start zpdefault-section zpdefault-section-bg "><style type="text/css"> [data-element-id="elm_TK1qy9cyRIa6jawzI5Q4dQ"].zprow{ padding-block-end:48px; } </style><div data-element-id="elm_ypYGIb1WSNCv3PWSNHa3_A" data-element-type="column" class="zpelem-col zpcol-12 zpcol-md-12 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"></style><div data-element-id="elm_-7BJcJkURhKFJOnex5AVNQ" data-element-type="heading" class="zpelement zpelem-heading "><style> [data-element-id="elm_-7BJcJkURhKFJOnex5AVNQ"] h2.zpheading{ font-family:'Poppins'; font-size:16px; font-weight:600; line-height:16px; text-decoration:underline; } [data-element-id="elm_-7BJcJkURhKFJOnex5AVNQ"].zpelem-heading { margin:0px; } </style><h2 5772 class="zpheading zpheading-style-none zpheading-align-left " data-editor="true"><span style="font-family:Oswald, sans-serif;font-size:20px;">Tycon Power<span style="color:inherit;">®</span></span></h2></div> 5773<div data-element-id="elm_IK_3eEtRThSBNdjmTV54zQ" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_IK_3eEtRThSBNdjmTV54zQ"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/rackmount-ups-power/6026428000002821182" title="UPS Backup Power" rel="" style="color:rgb(255, 255, 255);">UPS Backup Power</a></p></div> 5774</div><div data-element-id="elm_8lLX6Sr8QMymO0pbhgCjFQ" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_8lLX6Sr8QMymO0pbhgCjFQ"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/poe-injectors/6026428000002821032" title="Poe Injectors" rel="" style="color:rgb(255, 255, 255);">Poe Injectors</a></p></div> 5775</div><div data-element-id="elm_PlKDMebSSuCDE8IN8448JQ" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_PlKDMebSSuCDE8IN8448JQ"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/poe-switches/6026428000002821090" title="PoE Switches" rel="" style="color:rgb(255, 255, 255);">PoE Switches</a></p></div> 5776</div><div data-element-id="elm_1jQVIyOGSpiYNAX2vVK3bw" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_1jQVIyOGSpiYNAX2vVK3bw"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/dc-power-supplies/6026428000002821160" title="DC Power Supplies" rel="" style="color:rgb(255, 255, 255);">DC Power Supplies</a></p></div> 5777</div><div data-element-id="elm_PRW1Wmdh8G4n2jGMJewMHw" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_PRW1Wmdh8G4n2jGMJewMHw"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/dc-voltage-converters/6026428000002821166" title="Voltage Converters" rel="" style="color:rgb(255, 255, 255);">Voltage Converters</a></p></div> 5778</div><div data-element-id="elm_3kVPb8nJYO5Xsw917RL5uQ" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_3kVPb8nJYO5Xsw917RL5uQ"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/surge-protectors/6026428000002821142" title="Surge Protectors" rel="" style="color:rgb(255, 255, 255);">Surge Protectors</a></p></div> 5779</div></div></div><div data-element-id="elm_5bp4Ovt3Tsyuk_1ZKHAeSg" data-element-type="row" class="zprow zpalign-items-flex-start zpjustify-content-flex-start zpdefault-section zpdefault-section-bg "><style type="text/css"></style><div data-element-id="elm_2q8oxzCfSp63impyPiDQvQ" data-element-type="column" class="zpelem-col zpcol-12 zpcol-md-12 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"></style><div data-element-id="elm_RrHl_JlHSx2IfZtSI0p0lA" data-element-type="heading" class="zpelement zpelem-heading "><style> [data-element-id="elm_RrHl_JlHSx2IfZtSI0p0lA"] h2.zpheading{ font-family:'Poppins'; font-size:16px; font-weight:600; line-height:16px; text-decoration:underline; } [data-element-id="elm_RrHl_JlHSx2IfZtSI0p0lA"].zpelem-heading { margin:0px; } </style><h2 5780 class="zpheading zpheading-style-none zpheading-align-left " data-editor="true"><span style="font-family:Oswald, sans-serif;font-size:20px;">Legal</span></h2></div> 5781<div data-element-id="elm_HHNIGu0lQZK7uEB7sOQ1Uw" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_HHNIGu0lQZK7uEB7sOQ1Uw"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/terms" title="Terms and Conditions" rel="" style="color:rgb(255, 255, 255);">Terms and Conditions</a></p></div> 5782</div><div data-element-id="elm_nEIm5KEbRTq-y_lpCGaNRA" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_nEIm5KEbRTq-y_lpCGaNRA"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/privacy-policy" title="Privacy Policy" rel="" style="color:rgb(255, 255, 255);">Privacy Policy</a></p></div> 5783</div><div data-element-id="elm_XsnI-cy2TxKLC7NVxneJFA" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_XsnI-cy2TxKLC7NVxneJFA"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/customer-service" title="Customer Service" rel="" style="color:rgb(255, 255, 255);">Customer Service</a></p></div> 5784</div></div></div></div><div data-element-id="elm_-CT-54HTSHeqyfXXPxV87g" data-element-type="column" class="zpelem-col zpcol-12 zpcol-md-2 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"> [data-element-id="elm_-CT-54HTSHeqyfXXPxV87g"].zpelem-col{ padding-inline-end:0px; padding-inline-start:24px; } </style><div data-element-id="elm_nL7_E3zRTaur1j7xInOUeA" data-element-type="row" class="zprow zpalign-items-flex-start zpjustify-content-flex-start zpdefault-section zpdefault-section-bg "><style type="text/css"></style><div data-element-id="elm_mAmy3NTaTN2P6SKaeKOj0A" data-element-type="column" class="zpelem-col zpcol-12 zpcol-md-12 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"></style><div data-element-id="elm_sZqGZmVJTumfHTFjdWv3LA" data-element-type="heading" class="zpelement zpelem-heading "><style> [data-element-id="elm_sZqGZmVJTumfHTFjdWv3LA"] h2.zpheading{ font-family:'Poppins'; font-size:16px; font-weight:600; line-height:16px; text-decoration:underline; } [data-element-id="elm_sZqGZmVJTumfHTFjdWv3LA"].zpelem-heading { margin:0px; } </style><h2 5785 class="zpheading zpheading-style-none zpheading-align-left " data-editor="true"><span style="font-family:Oswald, sans-serif;font-size:20px;">Support</span></h2></div> 5786<div data-element-id="elm_vMmpy511TEaCEq9AWtbTfA" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_vMmpy511TEaCEq9AWtbTfA"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/support-portal" title="Support Portal" rel="" style="color:rgb(255, 255, 255);">Support Portal</a></p></div> 5787</div><div data-element-id="elm_klWzgK0c195zD-ECrkgPtQ" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_klWzgK0c195zD-ECrkgPtQ"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/knowledge-base" title="Knowledge Base" rel="" style="color:rgb(255, 255, 255);">Knowledge Base</a></p></div> 5788</div><div data-element-id="elm_Es1ZD3SXQvGNtQ8_iDQwbg" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_Es1ZD3SXQvGNtQ8_iDQwbg"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/calculators" title="Calculators" rel="" style="color:rgb(255, 255, 255);">Calculators</a></p></div> 5789</div><div data-element-id="elm_be_bXiJASXOSavV0kFzyLg" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_be_bXiJASXOSavV0kFzyLg"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/firmware" title="Firmware" rel="" style="color:rgb(255, 255, 255);">Firmware</a></p></div> 5790</div><div data-element-id="elm_AUkGO4vBR_yQtQiDdcDjBw" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_AUkGO4vBR_yQtQiDdcDjBw"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/documentation" title="Documentation" rel="" style="color:rgb(255, 255, 255);">Documentation</a></p></div> 5791</div><div data-element-id="elm_PqH-HTqdTfSLAO8zko6daA" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_PqH-HTqdTfSLAO8zko6daA"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/support-request" title="Submit a Support Request" rel="" style="color:rgb(255, 255, 255);">Submit a Support Request</a></p></div> 5792</div><div data-element-id="elm_gFbwKMnLT7u0pI4n8e_HTQ" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_gFbwKMnLT7u0pI4n8e_HTQ"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/request-a-system-design" title="Request a Design" rel="" style="color:rgb(255, 255, 255);">Request a Design</a></p></div> 5793</div><div data-element-id="elm_HXI99dAdGxv8k-zY5wnF2w" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_HXI99dAdGxv8k-zY5wnF2w"].zpelem-text{ color:#ccc ; font-size:14px; line-height:18px; margin-block-start:16px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/policy-downloads" title="Policy Download Page" rel="" style="color:rgb(255, 255, 255);">Policy Download Page</a></p></div> 5794</div></div></div></div></div>
5794<div data-element-id="elm_BMEJLw7RS_mMbHuj3EoFeA" data-element-type="row" class="zprow zpalign-items-flex-start zpjustify-content-flex-start zpdefault-section zpdefault-section-bg zp-hidden-md "><style type="text/css"></style><div data-element-id="elm__hB8DsgBQJm4wn2EvtOGxw" data-element-type="column" class="zpelem-col zpcol-12 zpcol-md-12 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"> [data-element-id="elm__hB8DsgBQJm4wn2EvtOGxw"].zpelem-col{ padding:0px; } </style><div data-element-id="elm_O86ovZoPSNuE-Q55XFzj6g" data-element-type="image" class="zpelement zpelem-image "><style> @media (min-width: 992px) { [data-element-id="elm_O86ovZoPSNuE-Q55XFzj6g"] .zpimage-container figure img { width: 200px ; height: 62.63px ; } } @media (max-width: 991px) and (min-width: 768px) { [data-element-id="elm_O86ovZoPSNuE-Q55XFzj6g"] .zpimage-container figure img { width:200px ; height:62.63px ; } } @media (max-width: 767px) { [data-element-id="elm_O86ovZoPSNuE-Q55XFzj6g"] .zpimage-container figure img { width:200px ; height:62.63px ; } } [data-element-id="elm_O86ovZoPSNuE-Q55XFzj6g"].zpelem-image { margin:0px; } </style><div data-caption-color="" data-size-tablet="" data-size-mobile="" data-align="left" data-tablet-image-separate="false" data-mobile-image-separate="false" class="zpimage-container zpimage-align-left zpimage-tablet-align-center zpimage-mobile-align-left zpimage-size-small zpimage-tablet-fallback-small zpimage-mobile-fallback-small "><figure role="none" class="zpimage-data-ref"><a class="zpimage-anchor" href="/" target="" title="Home" rel=""><picture><img class="zpimage zpimage-style-none zpimage-space-none " src='https://cdn1.zohoecommerce.com/images/Icons/Photo%20Jun%2010%202024-%206%2037%2055%20AM%20-1-.png?v=1763674583&storefront_domain=www.tyconsystems.com' width="200" height="62.63" loading="lazy" size="small" alt=""/></picture></a></figure></div> 5795</div><div data-element-id="elm_rclJk2ucS9yJvFmWdTipAw" data-element-type="accordion" class="zpelement zpelem-accordion zs-accordion " data-tabs-inactive="true" data-icon-style="3"><style> [data-element-id="elm_rclJk2ucS9yJvFmWdTipAw"] .zpaccordion-container.zpaccordion-style-02 .zpaccordion{ font-family:'Oswald',sans-serif; font-size:16px; font-weight:600; line-height:24px; } @media all and (min-width: 768px) and (max-width:991px){ } @media all and (max-width:767px){ } </style><div class="zpaccordion-container zpaccordion-style-02 zpaccordion-with-icon zpaccord-svg-icon-3 zpaccordion-icon-align-right "><div data-element-id="elm_yL-EFxbmTfyy2PI0fFP7uQ" data-element-type="accordionheader" class="zpelement zpaccordion " data-tab-name="Tycon Solar®" data-content-id="elm_4OCH6W25SRqKVzKX-DMqoQ" style="margin-top:0;" tabindex="0" role="button" aria-label="Tycon Solar®"><span class="zpaccordion-name">Tycon Solar®</span><span class="zpaccordionicon zpaccord-icon-inactive"><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-1"><path d="M98.9,184.7l1.8,2.1l136,156.5c4.6,5.3,11.5,8.6,19.2,8.6c7.7,0,14.6-3.4,19.2-8.6L411,187.1l2.3-2.6 c1.7-2.5,2.7-5.5,2.7-8.7c0-8.7-7.4-15.8-16.6-15.8v0H112.6v0c-9.2,0-16.6,7.1-16.6,15.8C96,179.1,97.1,182.2,98.9,184.7z"></path></svg><svg aria-hidden="true" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-2"><path d="M128,169.174c-1.637,0-3.276-0.625-4.525-1.875l-56.747-56.747c-2.5-2.499-2.5-6.552,0-9.05c2.497-2.5,6.553-2.5,9.05,0 L128,153.722l52.223-52.22c2.496-2.5,6.553-2.5,9.049,0c2.5,2.499,2.5,6.552,0,9.05l-56.746,56.747 C131.277,168.549,129.638,169.174,128,169.174z M256,128C256,57.42,198.58,0,128,0C57.42,0,0,57.42,0,128c0,70.58,57.42,128,128,128 C198.58,256,256,198.58,256,128z M243.2,128c0,63.521-51.679,115.2-115.2,115.2c-63.522,0-115.2-51.679-115.2-115.2 C12.8,64.478,64.478,12.8,128,12.8C191.521,12.8,243.2,64.478,243.2,128z"></path></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-3"><path d="M256,298.3L256,298.3L256,298.3l174.2-167.2c4.3-4.2,11.4-4.1,15.8,0.2l30.6,29.9c4.4,4.3,4.5,11.3,0.2,15.5L264.1,380.9c-2.2,2.2-5.2,3.2-8.1,3c-3,0.1-5.9-0.9-8.1-3L35.2,176.7c-4.3-4.2-4.2-11.2,0.2-15.5L66,131.3c4.4-4.3,11.5-4.4,15.8-0.2L256,298.3z"/></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-4"><path d="M417.4,224H288V94.6c0-16.9-14.3-30.6-32-30.6c-17.7,0-32,13.7-32,30.6V224H94.6C77.7,224,64,238.3,64,256 c0,17.7,13.7,32,30.6,32H224v129.4c0,16.9,14.3,30.6,32,30.6c17.7,0,32-13.7,32-30.6V288h129.4c16.9,0,30.6-14.3,30.6-32 C448,238.3,434.3,224,417.4,224z"></path></svg></span><span class="zpaccordionicon zpaccord-icon-active"><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-1"><path d="M413.1,327.3l-1.8-2.1l-136-156.5c-4.6-5.3-11.5-8.6-19.2-8.6c-7.7,0-14.6,3.4-19.2,8.6L101,324.9l-2.3,2.6 C97,330,9
57956,333,96,336.2c0,8.7,7.4,15.8,16.6,15.8v0h286.8v0c9.2,0,16.6-7.1,16.6-15.8C416,332.9,414.9,329.8,413.1,327.3z"></path></svg><svg aria-hidden="true" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-2"><path d="M184.746,156.373c-1.639,0-3.275-0.625-4.525-1.875L128,102.278l-52.223,52.22c-2.497,2.5-6.55,2.5-9.05,0 c-2.5-2.498-2.5-6.551,0-9.05l56.749-56.747c1.2-1.2,2.828-1.875,4.525-1.875l0,0c1.697,0,3.325,0.675,4.525,1.875l56.745,56.747 c2.5,2.499,2.5,6.552,0,9.05C188.021,155.748,186.383,156.373,184.746,156.373z M256,128C256,57.42,198.58,0,128,0 C57.42,0,0,57.42,0,128c0,70.58,57.42,128,128,128C198.58,256,256,198.58,256,128z M243.2,128c0,63.521-51.679,115.2-115.2,115.2 c-63.522,0-115.2-51.679-115.2-115.2C12.8,64.478,64.478,12.8,128,12.8C191.521,12.8,243.2,64.478,243.2,128z"></path></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-3"><path d="M256,213.7L256,213.7L256,213.7l174.2,167.2c4.3,4.2,11.4,4.1,15.8-0.2l30.6-29.9c4.4-4.3,4.5-11.3,0.2-15.5L264.1,131.1c-2.2-2.2-5.2-3.2-8.1-3c-3-0.1-5.9,0.9-8.1,3L35.2,335.3c-4.3,4.2-4.2,11.2,0.2,15.5L66,380.7c4.4,4.3,11.5,4.4,15.8,0.2L256,213.7z"/></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-4"><path d="M417.4,224H94.6C77.7,224,64,238.3,64,256c0,17.7,13.7,32,30.6,32h322.8c16.9,0,30.6-14.3,30.6-32 C448,238.3,434.3,224,417.4,224z"></path></svg></span></div> 5796<div data-element-id="elm_4OCH6W25SRqKVzKX-DMqoQ" data-element-type="accordioncontainer" class="zpelement zpaccordion-content " style="margin-top:0;"><div class="zpaccordion-element-container"><div data-element-id="elm_P-R7L1MkRUe15AQjjyPQyw" data-element-type="row" class="zprow zpalign-items-flex-start zpjustify-content-flex-start zpdefault-section zpdefault-section-bg "><style type="text/css"></style><div data-element-id="elm_u1ooGO2ES26nUVk3-BXm9Q" data-element-type="column" class="zpelem-col zpcol-12 zpcol-md-12 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"></style><div data-element-id="elm_-8flcixDRAyDu1UYGqz9Ag" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_-8flcixDRAyDu1UYGqz9Ag"].zpelem-text{ font-family:'Poppins'; font-size:14px; font-weight:400; line-height:18px; padding-block-start:16px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/mobilesolarpro%C2%AE/6026428000001429197" title="Mobile SolarPro®" rel="" style="color:rgb(255, 154, 51);">Mobile SolarPro®</a></p></div> 5797</div><div data-element-id="elm_VZfktLT2TTWWmXNfBs4ePA" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_VZfktLT2TTWWmXNfBs4ePA"].zpelem-text{ font-family:'Poppins'; font-size:14px; font-weight:400; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/remotepro%C2%AE/6026428000001429102" title="RemotePro®" rel="" style="color:rgb(255, 154, 51);">RemotePro®</a></p></div> 5798</div><div data-element-id="elm_K6bSgblrTWqOfcjssCxOSQ" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_K6bSgblrTWqOfcjssCxOSQ"].zpelem-text{ font-family:'Poppins'; font-size:14px; font-weight:400; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/upspro%C2%AE/6026428000001439117" title="UPSPro®" rel="" style="color:rgb(255, 154, 51);">UPSPro®</a></p></div> 5799</div><div data-element-id="elm_oxsrcPxsQlevPpSBxvK9GQ" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_oxsrcPxsQlevPpSBxvK9GQ"].zpelem-text{ font-family:'Poppins'; font-size:14px; font-weight:400; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/tpdin/6026428000002821412" title="TPDIN®" rel="" style="color:rgb(255, 154, 51);">TPDIN®</a></p></div> 5800</div><div data-element-id="elm_05M9bRnj2f4RALuVYwzwZA" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_05M9bRnj2f4RALuVYwzwZA"].zpelem-text{ font-family:'Poppins'; font-size:14px; font-weight:400; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/mppt-charge-controllers/6026428000002821418" title="MPPT Charge Controllers" rel="" style="color:rgb(255, 154, 51);">MPPT Charge Controllers</a></p></div> 5801</div><div data-element-id="elm_jbbvNvMn6qzB_boYiYiFyA" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_jbbvNvMn6qzB_boYiYiFyA"].zpelem-text{ font-family:'Poppins'; font-size:14px; font-weight:400; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/lithium-batteries/6026428000002821452" title="Lithium Batteries" rel="" style="color:rgb(255, 154, 51);">Lithium Batteries</a></p></div> 5802</div></div></div></div></div><div data-element-id="elm_xh2BEhLqSPuOTxmOe8h_LQ" data-element-type="accordionheader" class="zpelement zpaccordion " data-tab-name="Tycon Power®" data-content-id="elm_M_3y4H1gQ8ykwZX2XNGPUg" style="margin-top:0;" tabindex="0" role="button" aria-label="Tycon Power®"><span class="zpaccordion-name">Tycon Power®</span><span class="zpaccordionicon zpaccord-icon-inactive"><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-1"><path d="M98.9,184.7l1.8,2.1l136,156.5c4.6,5.3,11.5,8.6,19.2,8.6c7.7,0,14.6-3.4,19.2-8.6L411,187.1l2.3-2.6 c1.7-2.5,2.7-5.5,2.7-8.7c0-8.7-7.4-15.8-16.6-15.8v0H112.6v0c-9.2,0-16.6,7.1-16.6,15.8C96,179.1,97.1,182.2,98.9,184.7z"></path></svg><svg aria-hidden="true" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-2"><path d="M128,169.174c-1.637,0-3.276-0.625-4.525-1.875l-56.747-56.747c-2.5-2.499-2.5-6.552,0-9.05c2.497-2.5,6.553-2.5,9.05,0 L128,153.722l52.223-52.22c2.496-2.5,6.553-2.5,9.049,0c2.5,2.499,2.5,6.552,0,9.05l-56.746,56.747 C131.277,168.549,129.638,169.174,128,169.174z M256,128C256,57.42,198.58,0,128,0C57.42,0,0,57.42,0,128c0,70.58,57.42,128,128,128 C198.58,256,256,198.58,256,128z M243.2,128c0,63.521-51.679,115.2-115.2,115.2c-63.522,0-115.2-51.679-115.2-115.2 C12.8,64.478,64.478,12.8,128,12.8C191.521,12.8,243.2,64.478,243.2,128z"></path></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-3"><path d="M256,298.3L256,298.3L256,298.3l174.2-167.2c4.3-4.2,11.4-4.1,15.8,0.2l30.6,29.9c4.4,4.3,4.5,11.3,0.2,15.5L264.1,380.9c-2.2,2.2-5.2,3.2-8.1,3c-3,0.1-5.9-0.9-8.1-3L35.2,176.7c-4.3-4.2-4.2-11.2,0.2-15.5L66,131.3c4.4-4.3,11.5-4.4,15.8-0.2L256,298.3z"/></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-4"><path d="M417.4,224H288V94.6c0-16.9-14.3-30.6-32-30.6c-17.7,0-32,13.7-32,30.6V224H94.6C77.7,224,64,238.3,64,256 c0,17.7,13.7,32,30.6,32H224v129.4c0,16.9,14.3,30.6,32,30.6c17.7,0,32-13.7,32-30.6V288h129.4c16.9,0,30.6-14.3,30.6-32 C448,238.3,434.3,224,417.4,224z"></path></svg></span><span class="zpaccordionicon zpaccord-icon-active"><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-1"><path d="M413.1,327.3l-1.8-2.1l-136-156.5c-4.6-5.3-11.5-8.6-19.2-8.6c-7.7,0-14.6,3.4-19.2,8.6L101,324.9l-2.3,2.6 C97,330,9
58026,333,96,336.2c0,8.7,7.4,15.8,16.6,15.8v0h286.8v0c9.2,0,16.6-7.1,16.6-15.8C416,332.9,414.9,329.8,413.1,327.3z"></path></svg><svg aria-hidden="true" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-2"><path d="M184.746,156.373c-1.639,0-3.275-0.625-4.525-1.875L128,102.278l-52.223,52.22c-2.497,2.5-6.55,2.5-9.05,0 c-2.5-2.498-2.5-6.551,0-9.05l56.749-56.747c1.2-1.2,2.828-1.875,4.525-1.875l0,0c1.697,0,3.325,0.675,4.525,1.875l56.745,56.747 c2.5,2.499,2.5,6.552,0,9.05C188.021,155.748,186.383,156.373,184.746,156.373z M256,128C256,57.42,198.58,0,128,0 C57.42,0,0,57.42,0,128c0,70.58,57.42,128,128,128C198.58,256,256,198.58,256,128z M243.2,128c0,63.521-51.679,115.2-115.2,115.2 c-63.522,0-115.2-51.679-115.2-115.2C12.8,64.478,64.478,12.8,128,12.8C191.521,12.8,243.2,64.478,243.2,128z"></path></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-3"><path d="M256,213.7L256,213.7L256,213.7l174.2,167.2c4.3,4.2,11.4,4.1,15.8-0.2l30.6-29.9c4.4-4.3,4.5-11.3,0.2-15.5L264.1,131.1c-2.2-2.2-5.2-3.2-8.1-3c-3-0.1-5.9,0.9-8.1,3L35.2,335.3c-4.3,4.2-4.2,11.2,0.2,15.5L66,380.7c4.4,4.3,11.5,4.4,15.8,0.2L256,213.7z"/></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-4"><path d="M417.4,224H94.6C77.7,224,64,238.3,64,256c0,17.7,13.7,32,30.6,32h322.8c16.9,0,30.6-14.3,30.6-32 C448,238.3,434.3,224,417.4,224z"></path></svg></span></div> 5803<div data-element-id="elm_M_3y4H1gQ8ykwZX2XNGPUg" data-element-type="accordioncontainer" class="zpelement zpaccordion-content " style="margin-top:0;"><div class="zpaccordion-element-container"><div data-element-id="elm_GkwYPJErTEm-ISNHEFF4Rw" data-element-type="row" class="zprow zpalign-items-flex-start zpjustify-content-flex-start zpdefault-section zpdefault-section-bg "><style type="text/css"></style><div data-element-id="elm_lhnCekqFSzChfqrBjlBZjw" data-element-type="column" class="zpelem-col zpcol-12 zpcol-md-12 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"></style><div data-element-id="elm_It7d7uOmTrCbRGPtO_7OSQ" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_It7d7uOmTrCbRGPtO_7OSQ"].zpelem-text{ font-size:14px; line-height:18px; padding-block-start:16px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/rackmount-ups-power/6026428000002821182" title="UPS Backup Power" rel="" style="color:rgb(255, 154, 51);">UPS Backup Power</a></p></div> 5804</div><div data-element-id="elm_qdmVMRI-REiw7ngk7AG2yQ" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_qdmVMRI-REiw7ngk7AG2yQ"].zpelem-text{ font-size:14px; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/poe-injectors/6026428000002821032" title="Poe Injectors" rel="" style="color:rgb(255, 154, 51);">Poe Injectors</a></p></div> 5805</div><div data-element-id="elm_Xxi-W1BoRE2AjFIqzk5f1Q" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_Xxi-W1BoRE2AjFIqzk5f1Q"].zpelem-text{ font-size:14px; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/categories/poe-switches/6026428000002821090" title="PoE Switches" rel="" style="color:rgb(255, 154, 51);">PoE Switches</a></p></div> 5806</div><div data-element-id="elm__oaUfE0U56ZO5sgk_TtgFA" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm__oaUfE0U56ZO5sgk_TtgFA"].zpelem-text{ font-size:14px; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><span style="color:inherit;"><a href="/categories/dc-power-supplies/6026428000002821160" title="DC Power Supplies" rel="">DC Power Supplies</a></span></p></div> 5807</div><div data-element-id="elm_7eInLrATRTzIJV3qhC3WMg" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_7eInLrATRTzIJV3qhC3WMg"].zpelem-text{ font-size:14px; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><span style="color:inherit;"><a href="/categories/dc-voltage-converters/6026428000002821166" title="Voltage Converters" rel="">Voltage Converters</a></span></p></div> 5808</div><div data-element-id="elm_JDataXzj7FZX4oMKAgI6JA" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_JDataXzj7FZX4oMKAgI6JA"].zpelem-text{ font-size:14px; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><span style="color:inherit;"><a href="/categories/poe-lighting/6026428000002821132" title="PoE Lighting" rel="">PoE Lighting</a></span></p></div> 5809</div></div></div></div></div><div data-element-id="elm_rKu-6lfOSn2KWoFMpyEdVA" data-element-type="accordionheader" class="zpelement zpaccordion " data-tab-name="Company" data-content-id="elm_HjxKd6s6TSKZIaT0tnCDlw" style="margin-top:0;" tabindex="0" role="button" aria-label="Company"><span class="zpaccordion-name">Company</span><span class="zpaccordionicon zpaccord-icon-inactive"><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-1"><path d="M98.9,184.7l1.8,2.1l136,156.5c4.6,5.3,11.5,8.6,19.2,8.6c7.7,0,14.6-3.4,19.2-8.6L411,187.1l2.3-2.6 c1.7-2.5,2.7-5.5,2.7-8.7c0-8.7-7.4-15.8-16.6-15.8v0H112.6v0c-9.2,0-16.6,7.1-16.6,15.8C96,179.1,97.1,182.2,98.9,184.7z"></path></svg><svg aria-hidden="true" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-2"><path d="M128,169.174c-1.637,0-3.276-0.625-4.525-1.875l-56.747-56.747c-2.5-2.499-2.5-6.552,0-9.05c2.497-2.5,6.553-2.5,9.05,0 L128,153.722l52.223-52.22c2.496-2.5,6.553-2.5,9.049,0c2.5,2.499,2.5,6.552,0,9.05l-56.746,56.747 C131.277,168.549,129.638,169.174,128,169.174z M256,128C256,57.42,198.58,0,128,0C57.42,0,0,57.42,0,128c0,70.58,57.42,128,128,128 C198.58,256,256,198.58,256,128z M243.2,128c0,63.521-51.679,115.2-115.2,115.2c-63.522,0-115.2-51.679-115.2-115.2 C12.8,64.478,64.478,12.8,128,12.8C191.521,12.8,243.2,64.478,243.2,128z"></path></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-3"><path d="M256,298.3L256,298.3L256,298.3l174.2-167.2c4.3-4.2,11.4-4.1,15.8,0.2l30.6,29.9c4.4,4.3,4.5,11.3,0.2,15.5L264.1,380.9c-2.2,2.2-5.2,3.2-8.1,3c-3,0.1-5.9-0.9-8.1-3L35.2,176.7c-4.3-4.2-4.2-11.2,0.2-15.5L66,131.3c4.4-4.3,11.5-4.4,15.8-0.2L256,298.3z"/></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-4"><path d="M417.4,224H288V94.6c0-16.9-14.3-30.6-32-30.6c-17.7,0-32,13.7-32,30.6V224H94.6C77.7,224,64,238.3,64,256 c0,17.7,13.7,32,30.6,32H224v129.4c0,16.9,14.3,30.6,32,30.6c17.7,0,32-13.7,32-30.6V288h129.4c16.9,0,30.6-14.3,30.6-32 C448,238.3,434.3,224,417.4,224z"></path></svg></span><span class="zpaccordionicon zpaccord-icon-active"><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-1"><path d="M413.1,327.3l-1.8-2.1l-136-156.5c-4.6-5.3-11.5-8.6-19.2-8.6c-7.7,0-14.6,3.4-19.2,8.6L101,324.9l-2.3,2.6 C97,330,9
58096,333,96,336.2c0,8.7,7.4,15.8,16.6,15.8v0h286.8v0c9.2,0,16.6-7.1,16.6-15.8C416,332.9,414.9,329.8,413.1,327.3z"></path></svg><svg aria-hidden="true" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-2"><path d="M184.746,156.373c-1.639,0-3.275-0.625-4.525-1.875L128,102.278l-52.223,52.22c-2.497,2.5-6.55,2.5-9.05,0 c-2.5-2.498-2.5-6.551,0-9.05l56.749-56.747c1.2-1.2,2.828-1.875,4.525-1.875l0,0c1.697,0,3.325,0.675,4.525,1.875l56.745,56.747 c2.5,2.499,2.5,6.552,0,9.05C188.021,155.748,186.383,156.373,184.746,156.373z M256,128C256,57.42,198.58,0,128,0 C57.42,0,0,57.42,0,128c0,70.58,57.42,128,128,128C198.58,256,256,198.58,256,128z M243.2,128c0,63.521-51.679,115.2-115.2,115.2 c-63.522,0-115.2-51.679-115.2-115.2C12.8,64.478,64.478,12.8,128,12.8C191.521,12.8,243.2,64.478,243.2,128z"></path></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-3"><path d="M256,213.7L256,213.7L256,213.7l174.2,167.2c4.3,4.2,11.4,4.1,15.8-0.2l30.6-29.9c4.4-4.3,4.5-11.3,0.2-15.5L264.1,131.1c-2.2-2.2-5.2-3.2-8.1-3c-3-0.1-5.9,0.9-8.1,3L35.2,335.3c-4.3,4.2-4.2,11.2,0.2,15.5L66,380.7c4.4,4.3,11.5,4.4,15.8,0.2L256,213.7z"/></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-4"><path d="M417.4,224H94.6C77.7,224,64,238.3,64,256c0,17.7,13.7,32,30.6,32h322.8c16.9,0,30.6-14.3,30.6-32 C448,238.3,434.3,224,417.4,224z"></path></svg></span></div> 5810<div data-element-id="elm_HjxKd6s6TSKZIaT0tnCDlw" data-element-type="accordioncontainer" class="zpelement zpaccordion-content " style="margin-top:0;"><div class="zpaccordion-element-container"><div data-element-id="elm_M3IfVwXSRNatfZ2bOdoing" data-element-type="row" class="zprow zpalign-items-flex-start zpjustify-content-flex-start zpdefault-section zpdefault-section-bg "><style type="text/css"></style><div data-element-id="elm_BN7CEccDRVe-9MT8SW_EEQ" data-element-type="column" class="zpelem-col zpcol-12 zpcol-md-12 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"></style><div data-element-id="elm_r73mhJ54RTK0eWnXwVFfvA" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_r73mhJ54RTK0eWnXwVFfvA"].zpelem-text{ font-size:14px; line-height:18px; padding-block-start:16px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/patents-trademarks" title="Patents & Trademarks" rel="" style="color:rgb(255, 154, 51);">Patents & Trademarks</a></p></div> 5811</div><div data-element-id="elm_wNsbnlJz9O0WsrA3wJGcPQ" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_wNsbnlJz9O0WsrA3wJGcPQ"].zpelem-text{ font-size:14px; line-height:18px; padding-block-start:16px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/awards" title="Awards" rel="">Awards</a></p></div> 5812</div><div data-element-id="elm_vJ9kXhRfQFG8qHWw-hwDdg" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_vJ9kXhRfQFG8qHWw-hwDdg"].zpelem-text{ font-size:14px; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/about" title="About" rel="">About</a></p></div> 5813</div><div data-element-id="elm_g-XFMmRiTayFaANK1KDGGA" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_g-XFMmRiTayFaANK1KDGGA"].zpelem-text{ font-size:14px; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/contact" title="Contact Us" rel="" style="color:rgb(255, 154, 51);">Contact Us</a></p></div> 5814</div><div data-element-id="elm_kAcqD9ValsBpUWCty-GErw" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_kAcqD9ValsBpUWCty-GErw"].zpelem-text{ font-size:14px; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><span style="color:inherit;"><a href="/Tycon%202026%20Product%20Catalog.pdf" target="_blank" rel="">Full Product Catalog</a></span></p></div> 5815</div></div></div></div></div><div data-element-id="elm_44bRD3nQTP2bd8IfhUUPsg" data-element-type="accordionheader" class="zpelement zpaccordion " data-tab-name="Legal" data-content-id="elm_YRa8YGy_TmanzJ59pXukjw" style="margin-top:0;" tabindex="0" role="button" aria-label="Legal"><span class="zpaccordion-name">Legal</span><span class="zpaccordionicon zpaccord-icon-inactive"><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-1"><path d="M98.9,184.7l1.8,2.1l136,156.5c4.6,5.3,11.5,8.6,19.2,8.6c7.7,0,14.6-3.4,19.2-8.6L411,187.1l2.3-2.6 c1.7-2.5,2.7-5.5,2.7-8.7c0-8.7-7.4-15.8-16.6-15.8v0H112.6v0c-9.2,0-16.6,7.1-16.6,15.8C96,179.1,97.1,182.2,98.9,184.7z"></path></svg><svg aria-hidden="true" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-2"><path d="M128,169.174c-1.637,0-3.276-0.625-4.525-1.875l-56.747-56.747c-2.5-2.499-2.5-6.552,0-9.05c2.497-2.5,6.553-2.5,9.05,0 L128,153.722l52.223-52.22c2.496-2.5,6.553-2.5,9.049,0c2.5,2.499,2.5,6.552,0,9.05l-56.746,56.747 C131.277,168.549,129.638,169.174,128,169.174z M256,128C256,57.42,198.58,0,128,0C57.42,0,0,57.42,0,128c0,70.58,57.42,128,128,128 C198.58,256,256,198.58,256,128z M243.2,128c0,63.521-51.679,115.2-115.2,115.2c-63.522,0-115.2-51.679-115.2-115.2 C12.8,64.478,64.478,12.8,128,12.8C191.521,12.8,243.2,64.478,243.2,128z"></path></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-3"><path d="M256,298.3L256,298.3L256,298.3l174.2-167.2c4.3-4.2,11.4-4.1,15.8,0.2l30.6,29.9c4.4,4.3,4.5,11.3,0.2,15.5L264.1,380.9c-2.2,2.2-5.2,3.2-8.1,3c-3,0.1-5.9-0.9-8.1-3L35.2,176.7c-4.3-4.2-4.2-11.2,0.2-15.5L66,131.3c4.4-4.3,11.5-4.4,15.8-0.2L256,298.3z"/></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-4"><path d="M417.4,224H288V94.6c0-16.9-14.3-30.6-32-30.6c-17.7,0-32,13.7-32,30.6V224H94.6C77.7,224,64,238.3,64,256 c0,17.7,13.7,32,30.6,32H224v129.4c0,16.9,14.3,30.6,32,30.6c17.7,0,32-13.7,32-30.6V288h129.4c16.9,0,30.6-14.3,30.6-32 C448,238.3,434.3,224,417.4,224z"></path></svg></span><span class="zpaccordionicon zpaccord-icon-active"><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-1"><path d="M413.1,327.3l-1.8-2.1l-136-156.5c-4.6-5.3-11.5-8.6-19.2-8.6c-7.7,0-14.6,3.4-19.2,8.6L101,324.9l-2.3,2.6 C97,330,9
58156,333,96,336.2c0,8.7,7.4,15.8,16.6,15.8v0h286.8v0c9.2,0,16.6-7.1,16.6-15.8C416,332.9,414.9,329.8,413.1,327.3z"></path></svg><svg aria-hidden="true" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-2"><path d="M184.746,156.373c-1.639,0-3.275-0.625-4.525-1.875L128,102.278l-52.223,52.22c-2.497,2.5-6.55,2.5-9.05,0 c-2.5-2.498-2.5-6.551,0-9.05l56.749-56.747c1.2-1.2,2.828-1.875,4.525-1.875l0,0c1.697,0,3.325,0.675,4.525,1.875l56.745,56.747 c2.5,2.499,2.5,6.552,0,9.05C188.021,155.748,186.383,156.373,184.746,156.373z M256,128C256,57.42,198.58,0,128,0 C57.42,0,0,57.42,0,128c0,70.58,57.42,128,128,128C198.58,256,256,198.58,256,128z M243.2,128c0,63.521-51.679,115.2-115.2,115.2 c-63.522,0-115.2-51.679-115.2-115.2C12.8,64.478,64.478,12.8,128,12.8C191.521,12.8,243.2,64.478,243.2,128z"></path></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-3"><path d="M256,213.7L256,213.7L256,213.7l174.2,167.2c4.3,4.2,11.4,4.1,15.8-0.2l30.6-29.9c4.4-4.3,4.5-11.3,0.2-15.5L264.1,131.1c-2.2-2.2-5.2-3.2-8.1-3c-3-0.1-5.9,0.9-8.1,3L35.2,335.3c-4.3,4.2-4.2,11.2,0.2,15.5L66,380.7c4.4,4.3,11.5,4.4,15.8,0.2L256,213.7z"/></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-4"><path d="M417.4,224H94.6C77.7,224,64,238.3,64,256c0,17.7,13.7,32,30.6,32h322.8c16.9,0,30.6-14.3,30.6-32 C448,238.3,434.3,224,417.4,224z"></path></svg></span></div> 5816<div data-element-id="elm_YRa8YGy_TmanzJ59pXukjw" data-element-type="accordioncontainer" class="zpelement zpaccordion-content " style="margin-top:0;"><div class="zpaccordion-element-container"><div data-element-id="elm_kccUWaOwQMSH_gjxZ7Ngsw" data-element-type="row" class="zprow zpalign-items-flex-start zpjustify-content-flex-start zpdefault-section zpdefault-section-bg "><style type="text/css"></style><div data-element-id="elm_xrvOGvbRQZOCBvMPaGnVnA" data-element-type="column" class="zpelem-col zpcol-12 zpcol-md-12 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"></style><div data-element-id="elm_gQXGnp3sTByQ_yMycBuBjQ" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_gQXGnp3sTByQ_yMycBuBjQ"].zpelem-text{ font-size:14px; line-height:18px; padding-block-start:16px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/terms" title="Terms and Conditions" rel="" style="color:rgb(255, 154, 51);">Terms and Conditions</a></p></div> 5817</div><div data-element-id="elm_JhBiDA67S9iDGLVwy_QKpQ" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_JhBiDA67S9iDGLVwy_QKpQ"].zpelem-text{ font-size:14px; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/privacy-policy" title="Privacy Policy" rel="" style="color:rgb(255, 154, 51);">Privacy Policy</a></p></div> 5818</div><div data-element-id="elm__mWBV1hZQJOthBGznfgUUA" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm__mWBV1hZQJOthBGznfgUUA"].zpelem-text{ font-size:14px; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/customer-service" title="Customer Service" rel="" style="color:rgb(255, 154, 51);">Customer Service</a></p></div> 5819</div></div></div></div></div><div data-element-id="elm_Iop6XuBIRsOYnWOCQadnDg" data-element-type="accordionheader" class="zpelement zpaccordion " data-tab-name="Support" data-content-id="elm_2ZPdcRGDSsG6tzzQ25wIcQ" style="margin-top:0;" tabindex="0" role="button" aria-label="Support"><span class="zpaccordion-name">Support</span><span class="zpaccordionicon zpaccord-icon-inactive"><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-1"><path d="M98.9,184.7l1.8,2.1l136,156.5c4.6,5.3,11.5,8.6,19.2,8.6c7.7,0,14.6-3.4,19.2-8.6L411,187.1l2.3-2.6 c1.7-2.5,2.7-5.5,2.7-8.7c0-8.7-7.4-15.8-16.6-15.8v0H112.6v0c-9.2,0-16.6,7.1-16.6,15.8C96,179.1,97.1,182.2,98.9,184.7z"></path></svg><svg aria-hidden="true" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-2"><path d="M128,169.174c-1.637,0-3.276-0.625-4.525-1.875l-56.747-56.747c-2.5-2.499-2.5-6.552,0-9.05c2.497-2.5,6.553-2.5,9.05,0 L128,153.722l52.223-52.22c2.496-2.5,6.553-2.5,9.049,0c2.5,2.499,2.5,6.552,0,9.05l-56.746,56.747 C131.277,168.549,129.638,169.174,128,169.174z M256,128C256,57.42,198.58,0,128,0C57.42,0,0,57.42,0,128c0,70.58,57.42,128,128,128 C198.58,256,256,198.58,256,128z M243.2,128c0,63.521-51.679,115.2-115.2,115.2c-63.522,0-115.2-51.679-115.2-115.2 C12.8,64.478,64.478,12.8,128,12.8C191.521,12.8,243.2,64.478,243.2,128z"></path></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-3"><path d="M256,298.3L256,298.3L256,298.3l174.2-167.2c4.3-4.2,11.4-4.1,15.8,0.2l30.6,29.9c4.4,4.3,4.5,11.3,0.2,15.5L264.1,380.9c-2.2,2.2-5.2,3.2-8.1,3c-3,0.1-5.9-0.9-8.1-3L35.2,176.7c-4.3-4.2-4.2-11.2,0.2-15.5L66,131.3c4.4-4.3,11.5-4.4,15.8-0.2L256,298.3z"/></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-4"><path d="M417.4,224H288V94.6c0-16.9-14.3-30.6-32-30.6c-17.7,0-32,13.7-32,30.6V224H94.6C77.7,224,64,238.3,64,256 c0,17.7,13.7,32,30.6,32H224v129.4c0,16.9,14.3,30.6,32,30.6c17.7,0,32-13.7,32-30.6V288h129.4c16.9,0,30.6-14.3,30.6-32 C448,238.3,434.3,224,417.4,224z"></path></svg></span><span class="zpaccordionicon zpaccord-icon-active"><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-1"><path d="M413.1,327.3l-1.8-2.1l-136-156.5c-4.6-5.3-11.5-8.6-19.2-8.6c-7.7,0-14.6,3.4-19.2,8.6L101,324.9l-2.3,2.6 C97,330,9
58196,333,96,336.2c0,8.7,7.4,15.8,16.6,15.8v0h286.8v0c9.2,0,16.6-7.1,16.6-15.8C416,332.9,414.9,329.8,413.1,327.3z"></path></svg><svg aria-hidden="true" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-2"><path d="M184.746,156.373c-1.639,0-3.275-0.625-4.525-1.875L128,102.278l-52.223,52.22c-2.497,2.5-6.55,2.5-9.05,0 c-2.5-2.498-2.5-6.551,0-9.05l56.749-56.747c1.2-1.2,2.828-1.875,4.525-1.875l0,0c1.697,0,3.325,0.675,4.525,1.875l56.745,56.747 c2.5,2.499,2.5,6.552,0,9.05C188.021,155.748,186.383,156.373,184.746,156.373z M256,128C256,57.42,198.58,0,128,0 C57.42,0,0,57.42,0,128c0,70.58,57.42,128,128,128C198.58,256,256,198.58,256,128z M243.2,128c0,63.521-51.679,115.2-115.2,115.2 c-63.522,0-115.2-51.679-115.2-115.2C12.8,64.478,64.478,12.8,128,12.8C191.521,12.8,243.2,64.478,243.2,128z"></path></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-3"><path d="M256,213.7L256,213.7L256,213.7l174.2,167.2c4.3,4.2,11.4,4.1,15.8-0.2l30.6-29.9c4.4-4.3,4.5-11.3,0.2-15.5L264.1,131.1c-2.2-2.2-5.2-3.2-8.1-3c-3-0.1-5.9,0.9-8.1,3L35.2,335.3c-4.3,4.2-4.2,11.2,0.2,15.5L66,380.7c4.4,4.3,11.5,4.4,15.8,0.2L256,213.7z"/></svg><svg aria-hidden="true" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="svg-icon-15px zpaccord-svg-icon-4"><path d="M417.4,224H94.6C77.7,224,64,238.3,64,256c0,17.7,13.7,32,30.6,32h322.8c16.9,0,30.6-14.3,30.6-32 C448,238.3,434.3,224,417.4,224z"></path></svg></span></div> 5820<div data-element-id="elm_2ZPdcRGDSsG6tzzQ25wIcQ" data-element-type="accordioncontainer" class="zpelement zpaccordion-content " style="margin-top:0;"><div class="zpaccordion-element-container"><div data-element-id="elm_HOBOvpQpSTmvxzGdkQaBuA" data-element-type="row" class="zprow zpalign-items-flex-start zpjustify-content-flex-start zpdefault-section zpdefault-section-bg "><style type="text/css"></style><div data-element-id="elm_Mgfrs9dlRiiGBxdfQLGRaw" data-element-type="column" class="zpelem-col zpcol-12 zpcol-md-12 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"></style><div data-element-id="elm_JftMXe1UVaIRhGPpAugF1g" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_JftMXe1UVaIRhGPpAugF1g"].zpelem-text{ font-family:'Poppins'; font-size:14px; font-weight:400; line-height:18px; padding-block-start:16px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/knowledge-base" title="Knowledge Base" rel="" style="color:rgb(255, 154, 51);">Knowledge Base</a></p></div> 5821</div><div data-element-id="elm_RZtfqEoOQ96u-Z7vBNbpyQ" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_RZtfqEoOQ96u-Z7vBNbpyQ"].zpelem-text{ font-family:'Poppins'; font-size:14px; font-weight:400; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/support-portal" title="Support Portal" rel="" style="color:rgb(255, 154, 51);">Support Portal</a></p></div> 5822</div><div data-element-id="elm_QwzEQL5ePOQgTWRdz5TV5g" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_QwzEQL5ePOQgTWRdz5TV5g"].zpelem-text{ font-family:'Poppins'; font-size:14px; font-weight:400; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/calculators" title="Calculators" rel="" style="color:rgb(255, 154, 51);">Calculators</a></p></div> 5823</div><div data-element-id="elm_83UZ3ZEvQySlqfzjQ5eQZQ" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_83UZ3ZEvQySlqfzjQ5eQZQ"].zpelem-text{ font-family:'Poppins'; font-size:14px; font-weight:400; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><span style="color:rgb(255, 154, 51);"><a href="/firmware" title="Firmware" rel="">Firmware</a></span></p></div> 5824</div><div data-element-id="elm_tIUbXtUuRZyjb6YdsMa1vA" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_tIUbXtUuRZyjb6YdsMa1vA"].zpelem-text{ font-family:'Poppins'; font-size:14px; font-weight:400; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><span style="color:rgb(255, 154, 51);"><a href="/documentation" title="Documentation" rel="">Documentation</a></span></p></div> 5825</div><div data-element-id="elm_n57BppG7QxipqGdFgZw0Ug" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_n57BppG7QxipqGdFgZw0Ug"].zpelem-text{ font-family:'Poppins'; font-size:14px; font-weight:400; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/support-request" title="Submit a Support Request" rel="" style="color:rgb(255, 154, 51);">Submit a Support Request</a></p></div> 5826</div><div data-element-id="elm_w1Z_pb7mQcu6Fr0zWm-c1A" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_w1Z_pb7mQcu6Fr0zWm-c1A"].zpelem-text{ font-family:'Poppins'; font-size:14px; font-weight:400; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><a href="/request-a-system-design" title="Request a Design" rel="" style="color:rgb(255, 154, 51);">Request a Design</a></p></div> 5827</div><div data-element-id="elm_uFo_HZbhKDX28Pi-x_0Zqg" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_uFo_HZbhKDX28Pi-x_0Zqg"].zpelem-text{ font-family:'Poppins'; font-size:14px; font-weight:400; line-height:18px; padding-block-end:16px; margin:0px; } </style><div class="zptext zptext-align-left " data-editor="true"><p><span style="color:rgb(255, 154, 51);"><a href="/policy-downloads" title="Policy Download Page" rel="">Policy Download Page</a></span></p></div> 5828</div></div></div></div></div></div></div><div data-element-id="elm_EoLFKMgsQ7ug3cj6cS7x3w" data-element-type="row" class="zprow zs-social-profile zpalign-items-flex-start zpjustify-content-flex-start zpdefault-section zpdefault-section-bg "><style type="text/css"> [data-element-id="elm_EoLFKMgsQ7ug3cj6cS7x3w"].zprow{ padding-block-start:40px; padding-block-end:40px; margin:0px; } @media (max-width: 767px) { [data-element-id="elm_EoLFKMgsQ7ug3cj6cS7x3w"].zprow{ border-style:solid; border-color:#4d4d4d !important; border-block-start-width:0px; border-inline-end-width:0px; border-block-end-width:1px; border-inline-start-width:0px; } } </style><div data-element-id="elm_85ERET41RP2i-E0WtNEg6w" data-element-type="column" class="zpelem-col zpcol-3 zpcol-md-6 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"> [data-element-id="elm_85ERET41RP2i-E0WtNEg6w"].zpelem-col{ border-style:none; padding:0px; margin-inline-end:20px; } @media (max-width: 767px) { [data-element-id="elm_85ERET41RP2i-E0WtNEg6w"].zpelem-col{ margin:0px; } } </style><div data-element-id="elm_kLeEsOXVTf2bcmqKXhlVPA" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_kLeEsOXVTf2bcmqKXhlVPA"].zpelem-text{ font-size:16px; line-height:24px; margin:0px; } @media (max-width: 767px) { [data-element-id="elm_kLeEsOXVTf2bcmqKXhlVPA"].zpelem-text{ font-family:'Poppins'; font-size:16px; font-weight:600; line-height:24px; border-style:none; padding-inline-end:24px; } } </style><div class="zptext zptext-align-left " data-editor="true"><p><span style="font-family:poppins;">Follow us</span></p></div> 5829</div></div><div data-element-id="elm_BJ0lpAAiTNq6EbKk0NzFKQ" data-element-type="column" class="zpelem-col zpcol-7 zpcol-md-6 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"> [data-element-id="elm_BJ0lpAAiTNq6EbKk0NzFKQ"].zpelem-col{ padding:0px; } </style><div data-element-id="elm_JPwlcFBOTBSR_rUGxK5W4Q" itemscope="" data-element-type="socialprofile" class="zpelement zpelem-socialprofile" data-element-id="elm_JPwlcFBOTBSR_rUGxK5W4Q"><style type="text/css"> [data-element-id="elm_JPwlcFBOTBSR_rUGxK5W4Q"].zpelem-socialprofile{ margin:0px; } @media (max-width: 767px) { [data-element-id="elm_JPwlcFBOTBSR_rUGxK5W4Q"].zpelem-socialprofile{ padding:0px; margin:0px; } } </style><div data-socialprofile_container class="zpsocialprofile-container zpsocialprofile-size-lg zpsocialprofile-halign-left zpsocialprofile-style-none zpsocialprofile-type-white "><a href="https://www.linkedin.com/company/tycon-systems-inc-/" class="zpsocialprofile-wrapper zpsocialprofile-linkedin" target="_blank" title="LinkedIn"><svg aria-hidden="true" class="zpsocialprofile" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><path d="M64 4.706v54.588A4.706 4.706 0 0 1 59.294 64H4.706A4.706 4.706 0 0 1 0 59.294V4.706A4.706 4.706 0 0 1 4.706 0h54.588A4.706 4.706 0 0 1 64 4.706zM18.824 24.47H9.412v30.117h9.412V24.471zm.847-10.353a5.421 5.421 0 0 0-5.384-5.46h-.17a5.459 5.459 0 0 0 0 10.918 5.421 5.421 0 0 0 5.554-5.289v-.17zm34.917 22.174c0-9.054-5.76-12.574-11.482-12.574a10.73 10.73 0 0 0-9.525 4.856h-.263v-4.103H24.47v30.117h9.411V38.57a6.25 6.25 0 0 1 5.647-6.738h.358c2.993 0 5.214 1.882 5.214 6.625v16.132h9.412l.075-18.296z"/></svg></a><a href="https://www.facebook.com/tyconsystems/" class="zpsocialprofile-wrapper zpsocialprofile-facebook" target="_blank" title="Facebook"><svg aria-hidden="true" class="zpsocialprofile" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><path d="M64,32.1989637 C64,14.4248705 49.6642636,0 32,0 C14.3357364,0 -8.52651283e-14,14.4248705 -8.52651283e-14,32.1989637 C-8.52651283e-14,48.2818653 11.6992791,61.6124352 27.0236869,64 L27.0236869,41.4839378 L18.8506694,41.4839378 L18.8506694,32.1989637 L27.0236869,32.1989637 L27.0236869,25.1025907 C27.0236869,17.0445596 31.76931,12.5678756 39.0854789,12.5678756 C42.5787848,12.5678756 46.2039135,13.2310881 46.2039135,13.2310881 L46.2039135,21.0901554 L42.1833162,21.0901554 C38.1956746,21.0901554 36.9433574,23.6103627 36.9433574,26.1305699 L36.9433574,32.1658031 L45.8414006,32.1658031 L44.4243048,41.4507772 L36.9433574,41.4507772 L36.9433574,63.9668394 C52.3007209,61.6124352 64,48.2818653 64,32.1989637 Z"></path></svg></a><a href="https://twitter.com/TyconSys" class="zpsocialprofile-wrapper zpsocialprofile-twitter" target="_blank" title="X"><svg aria-hidden="true" class="zpsocialprofile" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill-rule="nonzero" d="M37.97 27.1 61.283 0h-5.525L35.516 23.53 19.348 0H.7l24.449 35.582L.7 64h5.525l21.377-24.849L44.676 64h18.648L37.968 27.1h.002Zm-7.567 8.795-2.477-3.543L8.216 4.16h8.485l15.906 22.753 2.478 3.543L55.76 60.03h-8.486L30.403 35.897v-.002Z"/></svg></a><a href="https://www.youtube.com/@TyconsystemsHQ" class="zpsocialprofile-wrapper zpsocialprofile-youtube" target="_blank" title="YouTube"><svg aria-hidden="true" class="zpsocialprofile" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><path d="M32.786 10l.106.001.392.002c1.94.009 6.896.05 11.873.237l.765.03c4.589.187 9.063.504 11.068 1.04 2.71.73 4.84 2.86 5.56 5.56 1.204 4.505 1.301 13.515 1.31 14.939v.382c-.009 1.424-.106 10.434-1.31 14.939a7.885 7.885 0 0 1-5.56 5.56c-4.455 1.19-21.107 1.3-24.098 1.309h-.923c-2.99-.01-19.635-.118-24.099-1.309a7.885 7.885 0 0 1-5.56-5.56c-.401-1.502-.68-3.504-.873-5.559l-.058-.65c-.314-3.688-.368-7.394-.37
58297-8.552L1 32.124v-.194-.054l.002-.245c.009-1.158.063-4.864.377-8.552l.058-.65c.193-2.055.472-4.057.873-5.559a7.885 7.885 0 0 1 5.56-5.56c4.296-1.146 19.878-1.29 23.706-1.307L31.97 10h.105zM26.15 22.57v18.86L42.48 32l-16.33-9.43z"/></svg></a><a href="https://www.instagram.com/tyconsystems/" class="zpsocialprofile-wrapper zpsocialprofile-instagram" target="_blank" title="Instagram"><svg aria-hidden="true" class="zpsocialprofile" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="72.5875139%" y1="0%" x2="28.281045%" y2="106.524963%" id="linearGradient-1"><stop stop-color="#9A1DF1" offset="0%"></stop><stop stop-color="#ED1E64" offset="49.3618943%"></stop><stop stop-color="#FFD002" offset="100%"></stop></linearGradient><path d="M18.7470755,0.221595251 C15.3422244,0.382299247 13.0166948,0.927668835 10.9845413,1.72394402 C8.8808805,2.5440464 7.09761014,3.64310557 5.32364538,5.42434073 C3.54758782,7.20661909 2.45687425,8.99304465 1.64301187,11.0988238 C0.855031083,13.1361614 0.321066295,15.4627981 0.170711099,18.8697484 C0.0214311023,22.2829708 -0.012802497,23.3716107 0.00383750267,32.0611721 C0.0204135023,40.7496967 0.0587623014,41.8403975 0.221527098,45.254631 C0.384285494,48.6585158 0.927619882,50.9830597 1.72388866,53.0162565 C2.54502785,55.1199428 3.64296062,56.9022276 5.42521338,58.6772547 C7.20645494,60.4522883 8.9939045,61.5419779 11.1006629,62.3569282 C13.1359204,63.1438722 15.46353,63.6799042 18.8694627,63.8291842 C22.2826082,63.9795458 23.3722466,64.0127042 32.0596384,63.9961282 C40.7511454,63.9795458 41.8408478,63.941165 45.2539933,63.7794178 C48.6588444,63.616653 50.9822875,63.0712898 53.0165275,62.2770946 C55.1201883,61.4538563 56.9034586,60.3579395 58.6774234,58.5756547 C60.4513945,56.7943876 61.5421081,55.0069188 62.3560345,52.9001285 C63.1440153,50.8648453 63.6800089,48.5372038 63.8282777,45.1333446 C63.9775577,41.7180679 64.0128665,40.6273351 63.9962265,31.9388169 C63.9796441,23.2492555 63.9402841,22.1606092 63.7785369,18.7484684 C63.6167897,15.3414861 63.0714329,13.0179854 62.2761753,10.9837454 C61.4540249,8.88005905 60.3571033,7.0987919 58.5758618,5.32274074 C56.7946202,3.54875038 55.0071771,2.4559312 52.9004123,1.64411682 C50.8641371,0.856129637 48.5375324,0.319048049 45.1316061,0.171822452 C41.7184606,0.0204496555 40.6288222,-0.0127087437 31.9383264,0.00387365592 C23.2499298,0.0204560555 22.160285,0.0577936547 18.7470755,0.221595251 M19.1203747,58.0696835 C16.0006051,57.9338499 14.3064996,57.4154436 13.1774308,56.9810372 C11.682346,56.4024772 10.6154853,55.7088452 9.49261809,54.5942852 C8.37184371,53.4755524 7.67612533,52.4117765 7.09241974,50.9198021 C6.65381495,49.7906885 6.12611576,48.0986182 5.97993337,44.9788358 C5.82129017,41.6060871 5.78502777,40.5941383 5.76945657,32.0497481 C5.75287417,23.5074443 5.78394617,22.4954955 5.93120377,19.1196428 C6.06491897,16.0019213 6.58648055,14.3057037 7.01983094,13.1776334 C7.59834613,11.6804686 8.28988531,10.6156558 9.40655089,9.49280784 C10.5252389,8.36992787 11.5889956,7.67628948 13.081994,7.0925775 C14.2100452,6.65191191 15.9021283,6.12831512 19.0208227,5.98007832 C22.3956194,5.82038552 23.4064738,5.78618392 31.9486944,5.76960153 C40.4929438,5.75298713 41.5048734,5.78307992 44.8807453,5.93134872 C47.9983644,6.06715032 49.6945564,6.58350231 50.8216028,7.0199951 C52.3176987,7.59852308 53.3845595,8.28801427 54.5064091,9.40674704 C55.628201,10.5254798 56.323913,11.5871694 56.9076186,13.0832846 C57.3482458,14.2082189 57.8718362,15.9023821 58.0190938,19.0201036 C58.1797594,22.3949387 58.2160858,23.4068555 58.231593,31.9491913 C58.2481754,40.4935879 58.2171034,41.5054983 58.0688346,44.8792902 C57.933033,47.9990726 57.4156442,49.6942725 56.9802074,50.8243973 C56.4016858,52.3184581 55.710089,53.3853316 54.5924123,54.5082116 C53.4747995,55.6269444 52.4110427,56.3246916 50.9169691,56.9084356 C49.7910044,57.348026 48.0968988,57.8726723 44.9802333,58.0209411 C41.6055006,58.1795587 40.5945822,58.2158467 32.0492576,58.2314179 C23.507037,58.2480131 22.4961826,58.2158659 19.1203747,58.0696835 M45.2073489,14.8946189 C45.2114717,17.0138765 46.9346204,18.729806 49.053846,18.7256663 C51.1740891,18.7215116 52.8900251,16.9993549 52.8869253,14.8800973 C52.8827483,12.7608398 51.1595931,11.043899 49.03935,11.0480387 C46.9191132,11.052187 45.2031773,12.7753614 45.2073489,14.8946189 M15.5693094,32.0310857 C15.5869347,41.1063495 22.957565,48.4469894 32.0305952,48.4293954 C41.1046942,48.4117446 48.4493852,41.0430983 48.4318231,31.9678665 C48.414134,22.8957323 41.0424926,15.5509133 31.967376,15.5685072 C22.8942818,15.5861645 15.5516836,22.9579403 15.5693094,32.0310857 M21.3328633,32.0196937 C21.3225058,26.1295435 26.0897377,21.344654 31.978768,21.3342528 C37.868867,21.3228812 42.6547101,26.0880651 42.6661227,31.9792585 C42.6775581,37.870452 37.9103198,42.6542983 32.0192032,42.6657427 C26.1301793,42.6771143 21.3442722,37.9108872 21.3328633,32.0196937" id="path-2"></path></defs><g stroke="none" stroke-width="1" fill-rule="evenodd"><mask fill="white"><use xlink:href="#path-2"></use></mask><use class="cl-path-inherit" fill="url(#linearGradient-1)" xlink:href="#path-2"></use></g></svg></a></div> 5830</div></div></div></div></div></div></div><div data-element-id="elm_4rYC7QeYR_6Iqq5aKMwrZQ" data-element-type="section" class="zpsection zpdefault-section zpdefault-section-bg " style="background-color:rgb(0, 74, 173);background-image:unset;"><style type="text/css"> [data-element-id="elm_4rYC7QeYR_6Iqq5aKMwrZQ"].zpsection{ padding-block-start:0
5830px; padding-inline-end:100px; padding-block-end:20px; padding-inline-start:100px; } @media (max-width: 767px) { [data-element-id="elm_4rYC7QeYR_6Iqq5aKMwrZQ"].zpsection{ padding-block-start:2px; padding-inline-end:12px; padding-block-end:15px; padding-inline-start:12px; } } </style><div class="zpcontainer-fluid"><div data-element-id="elm_AnqDgsO8S3W_u8-104SsUQ" data-element-type="row" class="zprow zpalign-items-flex-start zpjustify-content-flex-start zpdefault-section zpdefault-section-bg "><style type="text/css"></style><div data-element-id="elm_1FKYfaQESASPVZFefI1u1w" data-element-type="column" class="zpelem-col zpcol-12 zpcol-md-12 zpcol-sm-12 zpalign-self- zpdefault-section zpdefault-section-bg "><style type="text/css"> @media (max-width: 767px) { [data-element-id="elm_1FKYfaQESASPVZFefI1u1w"].zpelem-col{ padding:0px; } } </style><div data-element-id="elm_hcE-9EXQxZYpVxAwdXuEIw" data-element-type="spacer" class="zpelement zpelem-spacer "><style> div[data-element-id="elm_hcE-9EXQxZYpVxAwdXuEIw"] div.zpspacer { height:1px; } @media (max-width: 768px) { div[data-element-id="elm_hcE-9EXQxZYpVxAwdXuEIw"] div.zpspacer { height:calc(1px / 3); } } </style><div class="zpspacer " data-height="1"></div> 5831</div><div data-element-id="elm_S9QWEZ6PR2eu5DnpI7M6JA" data-element-type="text" class="zpelement zpelem-text "><style> [data-element-id="elm_S9QWEZ6PR2eu5DnpI7M6JA"].zpelem-text{ color:#999 ; font-family:'Poppins'; font-size:14px; font-weight:400; line-height:22px; margin:0px; } @media (max-width: 767px) { [data-element-id="elm_S9QWEZ6PR2eu5DnpI7M6JA"].zpelem-text{ font-size:14px; line-height:24px; } } </style><div class="zptext zptext-align-left " data-editor="true"><p style="text-align:center;"><span style="font-size:12px;font-family:poppins;"><span style="color:rgb(255, 255, 255);font-weight:700;">Copyright </span><span style="color:rgb(255, 255, 255);"><span style="font-weight:700;">© 2026 </span></span><span style="color:rgb(255, 255, 255);font-weight:700;">Tycon Systems</span><span style="font-weight:700;color:rgb(255, 255, 255);">® </span><span style="color:rgb(255, 255, 255);font-weight:700;">Inc - All Rights Reserved.</span></span></p></div> 5832</div></div></div></div></div></div></div></div><div id="product_quick_look"></div> 5833<div data-cart-buyagain-success="theme-cart-buyagain-success" class="buyagain-cart-message buyagain-cart-success-message"> Items have been added to cart. </div> 5834<div data-cart-buyagain-failure="theme-cart-buyagain-failure" class="buyagain-cart-message buyagain-cart-failure-message"> One or more items could not be added to cart due to certain restrictions. </div> 5835<div data-cart-add-success="theme-cart-add-success" class="theme-cart-add-success-message" data-theme-message-one><div class="theme-cart-success-inner-container"><button type="button" class="theme-cart-message-close" onclick="closeSuccessMessage();" aria-label="Close"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none"><rect width="32" height="32" rx="16" fill="black"/><path d="M10.4 23L9 21.6L14.6 16L9 10.4L10.4 9L16 14.6L21.6 9L23 10.4L17.4 16L23 21.6L21.6 23L16 17.4L10.4 23Z" fill="white"/></svg></button><div class="theme-cart-item"><div class="theme-cart-message-item theme-cart-message-prod-img"><div class="theme-cart-message-prod-img-inner"><picture><img alt="" width="200" height="200" data-cart-add-success-prod-img="theme-cart-add-success-prod-img"/></picture></div> 5836</div><div class="theme-cart-success-item-details"><div class="theme-cart-message-item theme-cart-message-prod-name" data-cart-add-success-prod-name="theme-cart-add-success-prod-name"></div> 5837</div></div><div class="theme-cart-message-item theme-cart-message-buttons"><a href="javascript:;" class="zpbutton zpbutton-type-primary zpbutton-style-roundcorner zpbutton-size-lg" data-zs-view-cart>View Cart</a><a href="javascript:;" class="zpbutton zpbutton-type-primary zpbutton-style-roundcorner zpbutton-size-lg" data-zs-view-quote style="display:none;">View Quote</a></div> 5838<div class="theme-cart-message-item theme-cart-message-text"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 17 17" fill="none"><path d="M8.25153 0.925049L10.2214 2.36205L12.66 2.35755L13.4089 4.67805L15.3844 6.10755L14.6265 8.42505L15.3844 10.7425L13.4089 12.172L12.66 14.4925L10.2214 14.488L8.25153 15.925L6.28165 14.488L3.84303 14.4925L3.09415 12.172L1.11865 10.7425L1.87653 8.42505L1.11865 6.10755L3.09415 4.67805L3.84303 2.35755L6.28165 2.36205L8.25153 0.925049Z" stroke="#008805" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
5838<path d="M5.62646 8.42505L7.50146 10.3L11.2515 6.55005" stroke="#008805" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg><span class="theme-added-to-cart-msg">Added to cart</span><span class="theme-added-to-quote-msg" style="display:none;">Added</span></div> 5839</div></div><div role="alert" data-cart-add-failure="theme-cart-add-failure" class="theme-cart-add-failure-message"><div class="theme-cart-success-inner-container"><div class="theme-cart-message-item theme-cart-message-text theme-cart-message-failure"><button type="button" onclick="closemessage()" aria-label="Close"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 26 26" fill="none"><rect x="1" y="1" width="24" height="24" rx="12" stroke="#E0242B" stroke-width="2"/><path d="M8.66602 8.66699L17.3327 17.3337" stroke="#E0242B" stroke-width="2" stroke-linecap="round"/><path d="M17.332 8.66699L8.66536 17.3337" stroke="#E0242B" stroke-width="2" stroke-linecap="round"/></svg></button><div class="theme-failure-reason"><span data-theme-failure-reason="theme-failure-reason"></span><span> - </span>Can't add this product to the cart now. Please try again later. </div> 5840</div></div></div><div data-cart-update-success="theme-cart-update-success" class="theme-cart-update-success-message"><div class="theme-cart-success-inner-container"><div class="theme-cart-message-item theme-cart-message-text theme-cart-message-success"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30" fill="none"><rect x="1" y="1" width="28" height="28" rx="14" stroke="#008849" stroke-width="2"/><path d="M9.375 15L12.75 18.75L20.625 11.25" stroke="#008849" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Quantity updated </div> 5841</div></div><div data-cart-update-failure="theme-cart-update-failure" class="theme-cart-update-failure-message"><div class="theme-cart-success-inner-container"><div class="theme-cart-message-item theme-cart-message-text theme-cart-message-failure"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 26 26" fill="none"><rect x="1" y="1" width="24" height="24" rx="12" stroke="#E0242B" stroke-width="2"/><path d="M8.66602 8.66699L17.3327 17.3337" stroke="#E0242B" stroke-width="2" stroke-linecap="round"/><path d="M17.332 8.66699L8.66536 17.3337" stroke="#E0242B" stroke-width="2" stroke-linecap="round"/></svg><div><span data-theme-update-failure-reason="theme-update-failure-reason"></span><span> - </span> An error occurred. Please try again later. </div> 5842</div></div></div><div data-cart-delete-success="theme-cart-delete-success" class="theme-cart-delete-success-message"><div class="theme-cart-success-inner-container"><div class="theme-cart-message-item theme-cart-message-text theme-cart-message-success"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30" fill="none"><rect x="1" y="1" width="28" height="28" rx="14" stroke="#008849" stroke-width="2"/><path d="M9.375 15L12.75 18.75L20.625 11.25" stroke="#008849" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Deleted from cart </div> 5843</div></div><div data-cart-delete-failure="theme-cart-delete-failure" class="theme-cart-delete-failure-message"><div class="theme-cart-success-inner-container"><div class="theme-cart-message-item theme-cart-message-text theme-cart-message-failure"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 26 26" fill="none"><rect x="1" y="1" width="24" height="24" rx="12" stroke="#E0242B" stroke-width="2"/><path d="M8.66602 8.66699L17.3327 17.3337" stroke="#E0242B" stroke-width="2" stroke-linecap="round"/><path d="M17.332 8.66699L8.66536 17.3337" stroke="#E0242B" stroke-width="2" stroke-linecap="round"/></svg><div><span data-theme-delete-failure-reason="theme-delete-failure-reason"></span><span> - </span> Can't delete this product from the cart at the moment. Please try again later. </div> 5844</div></div></div><div data-wishlist-alert-update="theme-wishlist-alert-update" class="theme-wishlist-alert-update theme-wishlist-success-message"><div class="theme-cart-success-inner-container"><div class="theme-cart-message-item theme-cart-message-text" data-zs-alert-inner-container><span style="display:flex;" data-zs-success-icon><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30" fill="none"><rect x="1" y="1" width="28" height="28" rx="14" stroke="#008849" stroke-width="2"/><path d="M9.375 15L12.75 18.75L20.625 11.25" stroke="#008849" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span style="display:flex;" data-zs-failure-icon><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 26 26" fill="none"><rect x="1" y="1" width="24" height="24" rx="12" stroke="#E0242B" stroke-width="2"/><path d="M8.66602 8.66699L17.3327 17.3337" stroke="#E0242B" stroke-width="2" stroke-linecap="round"/>
5844<path d="M17.332 8.66699L8.66536 17.3337" stroke="#E0242B" stroke-width="2" stroke-linecap="round"/></svg></span><span data-zs-message-text></span></div> 5845</div></div><div data-save-for-later-alert-update="theme-save-for-later-alert-update" class="theme-save-for-later-success-message"><div class="theme-cart-success-inner-container"><div class="theme-cart-message-item theme-cart-message-text" data-zs-alert-inner-container><span style="display:flex;" data-zs-success-icon><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30" fill="none"><rect x="1" y="1" width="28" height="28" rx="14" stroke="#008849" stroke-width="2"/><path d="M9.375 15L12.75 18.75L20.625 11.25" stroke="#008849" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span style="display:flex;" data-zs-failure-icon><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 26 26" fill="none"><rect x="1" y="1" width="24" height="24" rx="12" stroke="#E0242B" stroke-width="2"/><path d="M8.66602 8.66699L17.3327 17.3337" stroke="#E0242B" stroke-width="2" stroke-linecap="round"/><path d="M17.332 8.66699L8.66536 17.3337" stroke="#E0242B" stroke-width="2" stroke-linecap="round"/></svg></span><span data-zs-message-text></span></div> 5846</div></div><noscript><div style="overflow:hidden;position:fixed;top:0px;width:100%;background-color:rgb(11, 59, 91);text-align:center;padding:1px;z-index:100001;"><div style="color:rgb(255, 243, 137);text-shadow:1px 1px 1px rgb(0, 0, 0);font-size:13px;font-family:Lucida Grande, Segoe UI, Arial, Helvetica, sans-serif;position:relative;padding:0 36px;">JavaScript is disabled in your browser. Please enable JavaScript to continue.</div></div></noscript><style> @media (min-width: 992px){ html body .theme-category-description p{ font-size:16px !important; line-height:28px !important; } } </style>
5846<script> 5847/* 5848 Summary: 5849 Automatically sets the default selected country phone code to "US" 5850 for both the Shipping and Billing mobile code dropdowns on page load. 5851 Customers can still change the dropdown before submitting the form. 5852 5853 Modified by Devin Hales (May 2026): 5854 - Added a checkout-page path guard so the script no longer polls on 5855 non-checkout pages where the target dropdowns never exist. The 5856 original ran an unbounded retry loop on every page in the store. 5857 - Bounded the retry loop with MAX_ATTEMPTS (50 retries = ~10s ceiling) 5858 instead of recursing forever. 5859 - Added an "already selected" short-circuit so we don't re-dispatch 5860 the change event on every successful retry â the original kept 5861 firing change events even after the value was correct, which could 5862 interfere with other change listeners on the form. 5863*/ 5864 5865(function selectUSMobileCodes() { 5866 // Only run on checkout â the target dropdowns don't exist elsewhere 5867 if (!window.location.pathname.includes('/checkout')) return; 5868 5869 let attempts = 0; 5870 const MAX_ATTEMPTS = 50; // ~10 seconds at 200ms intervals 5871 5872 const setSelectToUS = (selectId) => { 5873 const select = document.querySelector(selectId); 5874 if (!select) return false; 5875 5876 const usOption = 5877 select.querySelector('option[data-zs-country-code="US"]') || 5878 select.querySelector('option[data-zs-country-code="us"]'); 5879 5880 if (!usOption) return false; 5881 if (usOption.selected) return true; // Already set â don't re-fire change 5882 5883 usOption.selected = true; 5884 select.dispatchEvent(new Event("change", { bubbles: true })); 5885 return true; 5886 }; 5887 5888 (function tryOnce() { 5889 const shippingSet = setSelectToUS("#zs-shipping-mobile-code"); 5890 const billingSet = setSelectToUS("#zs-billing-mobile-code"); 5891 5892 if ((!shippingSet || !billingSet) && ++attempts < MAX_ATTEMPTS) { 5893 setTimeout(tryOnce, 200); 5894 } 5895 })(); 5896})(); 5897 5898</script>
5898<script> 5899// Ultra-smooth & slower Back-to-Top (Tyler Greene) 5900// 5901// Modified by Devin Hales (May 2026): 5902// - Throttled the scroll listener via requestAnimationFrame so the class 5903// toggle runs at most once per frame, not once per scroll event. Added 5904// { passive: true } so the browser doesn't have to wait for our handler 5905// before scrolling. Real perceptible win on mobile, where `scroll` fires 5906// dozens of times per inertia gesture and unthrottled handlers cause 5907// jank. (issue #9) 5908const bttBtn = document.createElement('div'); 5909bttBtn.id = 'btt-btn'; 5910bttBtn.title = 'Back to top'; 5911document.body.appendChild(bttBtn); 5912 5913bttBtn.addEventListener('click', () => { 5914 const duration = 1200; // 1.2 seconds 5915 const start = window.pageYOffset; 5916 const startTime = performance.now(); 5917 function scrollStep(currentTime) { 5918 const elapsed = currentTime - startTime; 5919 const progress = Math.min(elapsed / duration, 1); 5920 // easeInOutCubic easing 5921 const ease = progress < 0.5 5922 ? 4 * progress * progress * progress 5923 : 1 - Math.pow(-2 * progress + 2, 3) / 2; 5924 window.scrollTo(0, start * (1 - ease)); 5925 if (progress < 1) { 5926 requestAnimationFrame(scrollStep); 5927 } 5928 } 5929 requestAnimationFrame(scrollStep); 5930}); 5931 5932// Show button after scrolling down 400px. The handler is throttled with 5933// requestAnimationFrame so it runs at most once per frame (~60 Hz), 5934// regardless of how often `scroll` fires. 5935let bttTicking = false; 5936window.addEventListener('scroll', () => { 5937 if (bttTicking) return; 5938 bttTicking = true; 5939 requestAnimationFrame(() => { 5940 if (window.scrollY > 400) { 5941 bttBtn.classList.add('visible'); 5942 } else { 5943 bttBtn.classList.remove('visible'); 5944 } 5945 bttTicking = false; 5946 }); 5947}, { passive: true }); 5948</script>
5948<script> 5949// Product Image Hover Zoom - Tyler Greene - FIXED: Works on product detail pages only 5950// 5951// Modified by Devin Hales (May 2026): 5952// - Path guard now keys on window.zs_view === 'product' (per CLAUDE.md's 5953// path-guard guidance) instead of substring-matching the pathname. The 5954// old guard checked for '/collection/' (singular), but Zoho's collection 5955// pages live under '/collections/' (plural), so the zoom code could run 5956// on collection listings it was meant to skip. Falls back to the corrected 5957// pathname check only if window.zs_view isn't defined yet. (issue #2) 5958// - The zoom modal is now a singleton keyed on id="tycon-zoom-modal" so a 5959// second initZoom() call (lazy-load retry or SPA navigation between 5960// products) reuses the existing modal instead of appending another 5961// <div> to document.body and leaking nodes forever. (issue #3) 5962// - The modal-close click handler now requires event.target to be either 5963// the modal itself (backdrop) or the à span (data-close="1"). Clicks on 5964// the zoomed <img> no longer close the modal â the old behaviour was 5965// that any click anywhere inside dismissed it, which made it impossible 5966// to interact with the zoomed image. (issue #4) 5967// - Escape key now closes the modal. The keydown listener is attached only 5968// while the modal is open (added on modal-open, removed on close) so it 5969// doesn't intercept Escape on the rest of the page. Standard lightbox 5970// accessibility expectation. (issue #6) 5971// - Replaced the setTimeout(initZoom, 0/1500/4000/8000) chain with one 5972// synchronous attempt + a MutationObserver on document.body that fires 5973// initZoom() each time the DOM changes (initZoom is idempotent â bails 5974// if .main-zoom-wrapper exists). The observer disconnects on first 5975// successful wrap OR after a 15 s safety timeout. Catches gallery images 5976// that load later than 8 s, doesn't run unnecessarily when they're ready 5977// immediately, and matches the bounded-retry pattern in CLAUDE.md. 5978// (issue #5) 5979(function(){ 5980 // Early exit if NOT on a single product detail page. Prefer the Zoho 5981 // view variable (set by their storefront framework) over substring 5982 // matching the URL â it tracks SPA navigations and doesn't depend on 5983 // remembering which Zoho path is singular vs plural. 5984 if (typeof window.zs_view === 'string') { 5985 if (window.zs_view !== 'product') { 5986 console.log('Not on product detail page (zs_view=' + window.zs_view + ') - zoom skipped'); 5987 return; 5988 } 5989 } else { 5990 const path = window.location.pathname; 5991 if (!path.includes('/products/') || 5992 path.includes('/categories/') || 5993 path.includes('/collections/')) { 5994 console.log('Not on product detail page - zoom skipped'); 5995 return; 5996 } 5997 } 5998 5999 function initZoom() { 6000 // Broader but safe selectors for main product image in Zoho themes 6001 const candidateImgs = document.querySelectorAll( 6002 'img[src*="product"], img[alt*="product"], ' + 6003 '.product-image img, .product-gallery img, .gallery-item img, ' + 6004 '.product-detail img, .z-product-image img, ' + 6005 '[class*="product"] img, [data-zs-product-name] img' 6006 ); 6007 6008 if (candidateImgs.length === 0) { 6009 console.log('No candidate product images found'); 6010 return; 6011 } 6012 6013 let main = null; 6014 let max = 0; 6015 candidateImgs.forEach(img => { 6016 if (img.offsetWidth > 180 && img.offsetHeight > 180) { 6017 let area = img.offsetWidth * img.offsetHeight; 6018 if (area > max) { 6019 max = area; 6020 main = img; 6021 } 6022 } 6023 }); 6024 6025 if (!main) { 6026 console.log('No suitable main image found (too small or not product-related)'); 6027 return; 6028 } 6029 6030 if (main.closest('.main-zoom-wrapper')) return; 6031 6032 // Mobile/tablet: Skip zoom wrapper + cleanup 6033 const isMobile = window.matchMedia("(max-width: 1024px)").matches; 6034 if (isMobile) { 6035 console.log('Mobile detected - zoom disabled'); 6036 if (main.closest('.main-zoom-wrapper')) { 6037 const wrapper = main.closest('.main-zoom-wrapper'); 6038 wrapper.outerHTML = main.outerHTML; 6039 } 6040 return; 6041 } 6042 6043 // Desktop: Apply zoom 6044 console.log('Applying zoom to main product image'); 6045 const wrapper = document.createElement('div'); 6046 wrapper.className = 'main-zoom-wrapper'; 6047 main.parentNode.insertBefore(wrapper, main); 6048 wrapper.appendChild(main); 6049 6050 const lens = document.createElement('div'); 6051 lens.className = 'lens'; 6052 wrapper.appendChild(lens); 6053 6054 // Reuse a single modal across product-to-product navigations so we 6055 // don't leak <div> nodes into document.body on every initZoom() run. 6056 // (issue #3)
6057 const MODAL_ID = 'tycon-zoom-modal'; 6058 let modal = document.getElementById(MODAL_ID); 6059 if (!modal) { 6060 modal = document.createElement('div'); 6061 modal.id = MODAL_ID; 6062 // The à span carries data-close="1" so the click handler can 6063 // distinguish "clicked the close button" from "clicked the image", 6064 // which is also a descendant of the modal. (issue #4) 6065 modal.innerHTML = `<span data-close="1" style="position:absolute;top:15px;right:55px;font-size:55px;color:#fff;cursor:pointer;z-index:99999;">Ã</span> 6066 <img style="max-width:96vw;max-height:96vh;object-fit:contain;border-radius:14px;box-shadow:0 0 50px #000;">`; 6067 Object.assign(modal.style, { 6068 display: 'none', 6069 position: 'fixed', 6070 inset: '0', 6071 background: 'rgba(0,0,0,0.96)', 6072 zIndex: '9998', 6073 justifyContent: 'center', 6074 alignItems: 'center' 6075 }); 6076 document.body.appendChild(modal); 6077 } 6078 const modalImg = modal.querySelector('img'); 6079 6080 // Escape-to-close: the keydown listener is attached only while the 6081 // modal is open and removed when it closes, so we don't intercept 6082 // Escape elsewhere on the page (e.g. closing other dropdowns). (issue #6) 6083 const onEscape = (e) => { 6084 if (e.key === 'Escape') closeModal(); 6085 }; 6086 const closeModal = () => { 6087 modal.style.display = 'none'; 6088 document.removeEventListener('keydown', onEscape); 6089 }; 6090 6091 // Close only when the user clicks the backdrop (event.target === modal) 6092 // or the à button (carries data-close="1"). Clicks on the zoomed <img> 6093 // itself bubble up but don't match either condition, so the modal stays 6094 // open and the user can keep looking at the image. (issue #4) 6095 modal.onclick = (e) => { 6096 if (e.target === modal || (e.target.dataset && e.target.dataset.close === '1')) { 6097 closeModal(); 6098 } 6099 }; 6100 6101 wrapper.addEventListener('mousemove', e => { 6102 const r = wrapper.getBoundingClientRect(); 6103 const x = e.clientX - r.left; 6104 const y = e.clientY - r.top; 6105 const cx = r.width / 2; 6106 const cy = r.height / 2; 6107 6108 const inZone = x > cx*0.2 && x < cx*1.8 && y > cy*0.2 && y < cy*1.8; 6109 6110 if (inZone) { 6111 lens.style.opacity = '1'; 6112 lens.style.left = (x - 35) + 'px'; 6113 lens.style.top = (y - 35) + 'px'; 6114 6115 const zoom = 1.8; 6116 const bx = -(x*zoom - r.width/2); 6117 const by = -(y*zoom - r.height/2); 6118 main.style.transform = `translate(${bx}px,${by}px) scale(${zoom})`; 6119 } else { 6120 lens.style.opacity = '0'; 6121 main.style.transform = 'scale(1)'; 6122 } 6123 }); 6124 6125 wrapper.addEventListener('mouseleave', () => { 6126 main.style.transform = 'scale(1)'; 6127 lens.style.opacity = '0'; 6128 }); 6129 6130 wrapper.addEventListener('click', e => { 6131 e.preventDefault(); 6132 let src = main.src 6133 .replace(/\/thumbs?\//g, '/original/') 6134 .replace(/\/small\//g, '/large/') 6135 .replace(/_thumb\./g, '.') 6136 .replace(/_small\./g, '.'); 6137 modalImg.src = src; 6138 modal.style.display = 'flex'; 6139 // Attach the Escape listener for this open. closeModal() removes it. 6140 document.addEventListener('keydown', onEscape); 6141 }); 6142 } 6143 6144 // Wait for the gallery to render. Try once on entry for the already- 6145 // loaded case, then attach a MutationObserver that fires initZoom() each 6146 // time DOM changes happen until a wrapper is installed (initZoom() is 6147 // idempotent â it bails out if .main-zoom-wrapper already exists). The 6148 // observer disconnects on first successful wrap OR after a 15 s safety 6149 // timeout so we don't observe forever. Replaces the 6150 // 0/1500/4000/8000 ms setTimeout chain, which missed images that 6151 // loaded after 8 s and ran unnecessarily when they were ready 6152 // immediately. (issue #5) 6153 const ZOOM_INIT_TIMEOUT_MS = 15000; 6154 initZoom(); 6155 if (!document.querySelector('.main-zoom-wrapper')) { 6156 const galleryObserver = new MutationObserver(() => { 6157 initZoom(); 6158 if (document.querySelector('.main-zoom-wrapper')) { 6159 galleryObserver.disconnect(); 6160 clearTimeout(zoomSafetyTimer); 6161 } 6162 }); 6163 galleryObserver.observe(document.body, { childList: true, subtree: true }); 6164 var zoomSafetyTimer = setTimeout(() => { 6165 galleryObserver.disconnect(); 6166 // No warning â many product pages legitimately have no gallery 6167 // image large enough to qualify (kits, downloadable goods, etc.). 6168 }, ZOOM_INIT_TIMEOUT_MS); 6169 } 6170 6171 // Cleanup on resize (mobile detection) 6172 window.addEventListener('resize', () => { 6173 const nowMobile = window.matchMedia("(max-width: 1024px)").matches; 6174 if (nowMobile && document.querySelector('.main-zoom-wrapper')) { 6175 const wrapper = document.querySelector('.main-zoom-wrapper'); 6176 if (wrapper) { 6177 const img = wrapper.querySelector('img'); 6178 if (img) { 6179 wrapper.outerHTML = img.outerHTML; 6180 } 6181 } 6182 } 6183 }); 6184})(); 6185</script>
6185<script> 6186 6187(function() { 6188 6189 /** 6190 * Tycon Systems - Navigation Hover Delay (Scott) 6191 * 6192 * Purpose: Prevents accidental menu openings by requiring the user 6193 * to hover over a menu item briefly before the dropdown opens. 6194 * 6195 * Method: Sets pointer-events:none on each nav item by default, which 6196 * blocks Zoho's built-in hover detection. A timer re-enables pointer 6197 * events after HOVER_DELAY ms of hovering, allowing Zoho to open the 6198 * menu naturally. If the mouse leaves before then, the timer cancels. 6199 * 6200 * Targets: li[data-megamenu] â Zoho Commerce's mega menu nav items 6201 * Menus affected: Explore Tycon®, Quick Links, Shop Tycon® 6202 * 6203 * Delay: 300ms â adjust HOVER_DELAY to change. 6204 * 6205 * Modified by Devin Hales (May 2026): 6206 * - Doc drift: the block comment used to say "1 second / 1000ms" but 6207 * HOVER_DELAY has been 300 for some time. Comment now matches the 6208 * constant (300ms is the intended UX). (issue #7) 6209 * - Replaced the unbounded setTimeout(initHoverDelay, 300) retry loop 6210 * with a MutationObserver on document.body that fires once nav items 6211 * appear, plus a 15 s safety timeout that disconnects if Zoho's nav 6212 * never renders. Matches the bounded-retry pattern in CLAUDE.md and 6213 * avoids the once-every-300ms wakeup on pages where the nav never 6214 * loads (404s, embedded contexts, etc.). (issue #8) 6215 */ 6216 6217 var HOVER_DELAY = 300; // milliseconds to wait before menu opens 6218 var INIT_TIMEOUT_MS = 15000; // give up waiting for nav items after 15 s 6219 6220 function wireUpNavItems(navItems) { 6221 console.log('Tycon menu: hover delay active on', navItems.length, 'items'); 6222 6223 navItems.forEach(function(li) { 6224 var hoverTimer = null; 6225 6226 // Disable pointer events by default so Zoho's hover cannot fire 6227 li.style.pointerEvents = 'none'; 6228 6229 // When mouse enters the parent ul, start the delay timer 6230 li.parentElement.addEventListener('mouseenter', function(e) { 6231 clearTimeout(hoverTimer); 6232 hoverTimer = setTimeout(function() { 6233 // Re-enable pointer events after delay so Zoho opens the menu 6234 li.style.pointerEvents = 'auto'; 6235 console.log('Tycon menu: opened after delay -', li.textContent.trim().substring(0, 20)); 6236 }, HOVER_DELAY); 6237 }); 6238 6239 // When mouse leaves, cancel the timer and disable pointer events again 6240 li.parentElement.addEventListener('mouseleave', function() { 6241 clearTimeout(hoverTimer); 6242 hoverTimer = null; 6243 // Small delay before disabling to allow moving into the submenu 6244 setTimeout(function() { 6245 li.style.pointerEvents = 'none'; 6246 }, 100); 6247 }); 6248 }); 6249 6250 console.log('Tycon menu: ready â delay set to', HOVER_DELAY + 'ms'); 6251 } 6252 6253 // Wait for nav items to appear in the DOM. Zoho injects them after 6254 // its own JS runs, which can be before or after our load handler. 6255 // Tries the DOM once on entry; if empty, attaches a MutationObserver 6256 // and a 15 s safety timeout so we never poll forever (issue #8). 6257 function initHoverDelay() { 6258 var found = document.querySelectorAll('li[data-megamenu]'); 6259 if (found.length > 0) { 6260 wireUpNavItems(found); 6261 return; 6262 } 6263 6264 var observer = new MutationObserver(function() { 6265 var navItems = document.querySelectorAll('li[data-megamenu]'); 6266 if (navItems.length > 0) { 6267 observer.disconnect(); 6268 clearTimeout(safetyTimer); 6269 wireUpNavItems(navItems); 6270 } 6271 }); 6272 6273 observer.observe(document.body, { childList: true, subtree: true }); 6274 6275 var safetyTimer = setTimeout(function() { 6276 observer.disconnect(); 6277 console.warn('Tycon menu: nav items never appeared after ' + 6278 INIT_TIMEOUT_MS + 'ms â hover delay not applied'); 6279 }, INIT_TIMEOUT_MS); 6280 } 6281 6282 // Wait for page load, then initialize after Zoho's own JS has run 6283 window.addEventListener('load', function() { 6284 setTimeout(initHoverDelay, 800); 6285 }); 6286 6287})(); 6288</script>
6288<!-- 6289 Zoho Desk ASAP widget loader. Modified by Devin Hales (May 2026): - Removed the s.nonce="{place_your_nonce_value_here}" assignment. The original snippet contained an unreplaced template placeholder from Zoho Desk's installation wizard. With no CSP enforcing nonces on this site, the assignment was meaningless; if CSP nonce enforcement is added later, a real nonce value must be substituted back here (or generated server-side and injected at render time). -->
6289<script type="text/javascript" id="zohodeskasap">var d=document;s=d.createElement("script"),s.type="text/javascript",s.id="zohodeskasapscript",s.defer=!0,s.src="https://desk.zoho.com/portal/api/web/asapApp/1085849000015025361?orgId=878171070",t=d.getElementsByTagName("script")[0],t.parentNode.insertBefore(s,t),window.ZohoDeskAsapReady=function(s){var e=window.ZohoDeskAsap__asyncalls=window.ZohoDeskAsap__asyncalls||[];window.ZohoDeskAsapReadyStatus?(s&&e.push(s),e.forEach(s=>s&&s()),window.ZohoDeskAsap__asyncalls=null):s&&e.push(s)};</script>
6289<!-- 6290 Tycon Systems - My Portal Customer Menu Link Description: Adds a plain-text "My Portal" link to the logged-in customer profile dropdown. The link is inserted immediately above "Sign Out," inherits Zoho Commerce's native customer-menu styling, and opens the Tycon Zoho Books customer portal in a new browser tab. This version uses the individual "Sign Out" row as the insertion reference, avoiding duplicated menu items and accommodating Zoho's separate Sign Out menu section. No custom CSS is required. Created by Tyler Greene Date: August 3, 2026 -->
6290<script> 6291(function () { 6292 "use strict"; 6293 6294 var PORTAL_URL = 6295 "https://books.zohosecure.com/portal/tyconsystems"; 6296 6297 var PORTAL_LABEL = "My Portal"; 6298 var PORTAL_MARKER = "data-tycon-my-portal"; 6299 6300 function normalizeText(value) { 6301 return (value || "").replace(/\s+/g, " ").trim(); 6302 } 6303 6304 function findCustomerMenu(signOutElement) { 6305 var current = signOutElement.parentElement; 6306 6307 while (current && current !== document.body) { 6308 var menuText = normalizeText(current.textContent); 6309 6310 if ( 6311 menuText.indexOf("My Profile") !== -1 && 6312 menuText.indexOf("My Orders") !== -1 && 6313 menuText.indexOf("My Quotes") !== -1 && 6314 menuText.indexOf("Sign Out") !== -1 6315 ) { 6316 return current; 6317 } 6318 6319 current = current.parentElement; 6320 } 6321 6322 return null; 6323 } 6324 6325 /* 6326 * Finds the largest wrapper containing only the selected 6327 * menu item's text. This prevents an entire group of menu 6328 * items from being cloned. 6329 */ 6330 function findIndividualRow(element, menuContainer) { 6331 var label = normalizeText(element.textContent); 6332 var row = element; 6333 var current = element; 6334 6335 while ( 6336 current.parentElement && 6337 current.parentElement !== menuContainer 6338 ) { 6339 var parent = current.parentElement; 6340 6341 if (normalizeText(parent.textContent) !== label) { 6342 break; 6343 } 6344 6345 row = parent; 6346 current = parent; 6347 } 6348 6349 return row; 6350 } 6351 6352 function sanitizeClone(element) { 6353 var elements = [element].concat( 6354 Array.prototype.slice.call( 6355 element.querySelectorAll("*") 6356 ) 6357 ); 6358 6359 elements.forEach(function (item) { 6360 Array.prototype.slice.call( 6361 item.attributes || [] 6362 ).forEach(function (attribute) { 6363 var name = attribute.name.toLowerCase(); 6364 6365 if ( 6366 name === "id" || 6367 name === "aria-current" || 6368 name.indexOf("on") === 0 || 6369 name.indexOf("data-") === 0 6370 ) { 6371 item.removeAttribute(attribute.name); 6372 } 6373 }); 6374 }); 6375 } 6376 6377 function findClickableElement(row) { 6378 if ( 6379 row.matches && 6380 row.matches("a, button, [role='menuitem']") 6381 ) { 6382 return row; 6383 } 6384 6385 return row.querySelector( 6386 "a, button, [role='menuitem']" 6387 ); 6388 } 6389 6390 function configurePortalLink(clickableElement) { 6391 var portalLink = clickableElement; 6392 6393 /* 6394 * Convert a button or menu control into an anchor while 6395 * preserving its existing Zoho classes and inline styling. 6396 */ 6397 if ( 6398 clickableElement.tagName.toLowerCase() !== "a" 6399 ) { 6400 portalLink = document.createElement("a"); 6401 portalLink.className = clickableElement.className; 6402 6403 if (clickableElement.getAttribute("style")) { 6404 portalLink.setAttribute( 6405 "style", 6406 clickableElement.getAttribute("style") 6407 ); 6408 } 6409 6410 if (clickableElement.getAttribute("role")) { 6411 portalLink.setAttribute( 6412 "role", 6413 clickableElement.getAttribute("role") 6414 ); 6415 } 6416 6417 clickableElement.parentNode.replaceChild( 6418 portalLink, 6419 clickableElement 6420 ); 6421 } 6422 6423 portalLink.href = PORTAL_URL; 6424 portalLink.target = "_blank"; 6425 portalLink.rel = "noopener noreferrer"; 6426 portalLink.textContent = PORTAL_LABEL; 6427 6428 portalLink.setAttribute( 6429 PORTAL_MARKER, 6430 "true" 6431 ); 6432 6433 portalLink.setAttribute( 6434 "aria-label", 6435 "Open the Tycon Customer Portal" 6436 ); 6437 } 6438 6439 function addPortalLink() { 6440 var possibleItems = document.querySelectorAll( 6441 "a, button, [role='menuitem']" 6442 ); 6443 6444 for (var i = 0; i < possibleItems.length; i++) { 6445 var signOutLink = possibleItems[i]; 6446 6447 if ( 6448 normalizeText(signOutLink.textContent) !== 6449 "Sign Out" 6450 ) { 6451 continue; 6452 } 6453 6454 var customerMenu = findCustomerMenu(signOutLink); 6455 6456 if (!customerMenu) { 6457 continue; 6458 } 6459 6460 /* 6461 * Prevent duplicate insertion within this particular 6462 * rendered customer dropdown. 6463 */ 6464 if ( 6465 customerMenu.querySelector( 6466 "[" + PORTAL_MARKER + "='true']" 6467 ) 6468 ) { 6469 return true; 6470 } 6471 6472 var signOutRow = findIndividualRow( 6473 signOutLink, 6474 customerMenu 6475 ); 6476 6477 if ( 6478 !signOutRow || 6479 !signOutRow.parentNode 6480 ) { 6481 continue; 6482 } 6483 6484 /* 6485 * Clone only the single Sign Out row. Its native Zoho 6486 * layout is preserved, but its behavior and label are 6487 * replaced with the My Portal link. 6488 */ 6489 var portalRow = signOutRow.cloneNode(true); 6490 6491 sanitizeClone(portalRow); 6492 6493 var portalClickable = findClickableElement( 6494 portalRow 6495 ); 6496 6497 if (!portalClickable) { 6498 continue; 6499 } 6500 6501 configurePortalLink(portalClickable); 6502 6503 signOutRow.parentNode.insertBefore( 6504 portalRow, 6505 signOutRow 6506 ); 6507 6508 return true; 6509 } 6510 6511 return false;
6512 } 6513 6514 function schedulePortalCheck() { 6515 window.setTimeout(addPortalLink, 0); 6516 window.setTimeout(addPortalLink, 150); 6517 window.setTimeout(addPortalLink, 500); 6518 } 6519 6520 if (document.readyState === "loading") { 6521 document.addEventListener( 6522 "DOMContentLoaded", 6523 schedulePortalCheck 6524 ); 6525 } else { 6526 schedulePortalCheck(); 6527 } 6528 6529 window.addEventListener( 6530 "load", 6531 schedulePortalCheck 6532 ); 6533 6534 /* 6535 * Zoho may not render the customer dropdown until the 6536 * account icon is clicked. Recheck after user interaction. 6537 */ 6538 document.addEventListener( 6539 "click", 6540 schedulePortalCheck, 6541 true 6542 ); 6543 6544 document.addEventListener( 6545 "keyup", 6546 function (event) { 6547 if ( 6548 event.key === "Enter" || 6549 event.key === " " 6550 ) { 6551 schedulePortalCheck(); 6552 } 6553 }, 6554 true 6555 ); 6556})(); 6557</script>
6557<!-- 6558 Tycon Systems - Menu Heading Trademark Superscript Description: Converts registered trademark symbols into smaller superscript symbols only in approved main-navigation and mega-menu headings. Approved headings: - Explore Tycon® - Shop Tycon® - Tycon Solar® - Tycon® Solar - Tycon Power® - MobileSolarPro® - Tycon Wireless® Submenu links such as RemotePro®, UPSPro®, E-Z Bridge®, and individual product or category links are not changed. Created by Tyler Greene Date: August 5, 2026 -->
6558<script> 6559(function () { 6560 "use strict"; 6561 6562 var MARK_CLASS = "tycon-heading-reg-mark"; 6563 6564 /* 6565 * Exact heading labels that are allowed to be modified. 6566 * Both Tycon® Solar and Tycon Solar® are included so the 6567 * script continues working if the heading wording changes. 6568 */ 6569 var TARGET_HEADINGS = { 6570 "Explore Tycon®": true, 6571 "Shop Tycon®": true, 6572 "Tycon® Solar": true, 6573 "Tycon Solar®": true, 6574 "Tycon Power®": true, 6575 "MobileSolarPro®": true, 6576 "Tycon Wireless®": true 6577 }; 6578 6579 /* 6580 * Restricts all searching to the storefront header and 6581 * Zoho mega-menu containers. 6582 */ 6583 var MENU_ROOT_SELECTOR = [ 6584 "header", 6585 ".theme-header", 6586 "li[data-megamenu]", 6587 "[class*='mega-menu']", 6588 "[class*='megamenu']" 6589 ].join(","); 6590 6591 var processingScheduled = false; 6592 6593 function normalizeText(value) { 6594 return (value || "") 6595 .replace(/\s+/g, " ") 6596 .replace(/[âââ¼â²â¾â´â¨â§]+$/g, "") 6597 .trim(); 6598 } 6599 6600 function isApprovedHeading(element) { 6601 if (!element) { 6602 return false; 6603 } 6604 6605 return Boolean( 6606 TARGET_HEADINGS[ 6607 normalizeText(element.textContent) 6608 ] 6609 ); 6610 } 6611 6612 /* 6613 * Starting from the registered-mark text node, walk upward 6614 * until an approved heading wrapper is found. 6615 */ 6616 function findApprovedHeading(textNode, root) { 6617 var current = textNode.parentElement; 6618 6619 while (current) { 6620 if (isApprovedHeading(current)) { 6621 return current; 6622 } 6623 6624 if (current === root) { 6625 break; 6626 } 6627 6628 current = current.parentElement; 6629 } 6630 6631 return null; 6632 } 6633 6634 function wrapRegisteredMarks(root) { 6635 if (!root) { 6636 return; 6637 } 6638 6639 var walker = document.createTreeWalker( 6640 root, 6641 NodeFilter.SHOW_TEXT, 6642 { 6643 acceptNode: function (node) { 6644 var parent = node.parentElement; 6645 6646 if ( 6647 !parent || 6648 node.nodeValue.indexOf("®") === -1 6649 ) { 6650 return NodeFilter.FILTER_REJECT; 6651 } 6652 6653 /* 6654 * Ignore symbols already processed. 6655 */ 6656 if ( 6657 parent.closest( 6658 "sup." + MARK_CLASS 6659 ) 6660 ) { 6661 return NodeFilter.FILTER_REJECT; 6662 } 6663 6664 /* 6665 * Ignore code, form controls, and editable areas. 6666 */ 6667 if ( 6668 parent.closest( 6669 "script, style, textarea, input, select, option" 6670 ) 6671 ) { 6672 return NodeFilter.FILTER_REJECT; 6673 } 6674 6675 /* 6676 * Process the symbol only when it belongs to one 6677 * of the exact approved heading labels. 6678 */ 6679 if (!findApprovedHeading(node, root)) { 6680 return NodeFilter.FILTER_REJECT; 6681 } 6682 6683 return NodeFilter.FILTER_ACCEPT; 6684 } 6685 } 6686 ); 6687 6688 var matchingNodes = []; 6689 var currentNode; 6690 6691 while ((currentNode = walker.nextNode())) { 6692 matchingNodes.push(currentNode); 6693 } 6694 6695 matchingNodes.forEach(function (textNode) { 6696 var parts = textNode.nodeValue.split("®"); 6697 var fragment = document.createDocumentFragment(); 6698 6699 parts.forEach(function (part, index) { 6700 if (part) { 6701 fragment.appendChild( 6702 document.createTextNode(part) 6703 ); 6704 } 6705 6706 if (index < parts.length - 1) { 6707 var registeredMark = 6708 document.createElement("sup"); 6709 6710 registeredMark.className = MARK_CLASS; 6711 registeredMark.textContent = "®"; 6712 6713 fragment.appendChild(registeredMark); 6714 } 6715 }); 6716 6717 if (textNode.parentNode) { 6718 textNode.parentNode.replaceChild( 6719 fragment, 6720 textNode 6721 ); 6722 } 6723 }); 6724 } 6725 6726 function processHeadingTrademarks() { 6727 var menuRoots = document.querySelectorAll( 6728 MENU_ROOT_SELECTOR 6729 ); 6730 6731 Array.prototype.forEach.call( 6732 menuRoots, 6733 function (root) { 6734 wrapRegisteredMarks(root); 6735 } 6736 ); 6737 } 6738 6739 /* 6740 * Run a few bounded checks because Zoho may render 6741 * navigation and mega-menu content after page load 6742 * or after the user opens a menu. 6743 */ 6744 function scheduleTrademarkProcessing() { 6745 if (processingScheduled) { 6746 return; 6747 } 6748 6749 processingScheduled = true; 6750 6751 window.setTimeout( 6752 processHeadingTrademarks, 6753 0 6754 ); 6755 6756 window.setTimeout( 6757 processHeadingTrademarks, 6758 150 6759 ); 6760 6761 window.setTimeout( 6762 processHeadingTrademarks, 6763 400 6764 ); 6765 6766 window.setTimeout( 6767 processHeadingTrademarks, 6768 800 6769 ); 6770 6771 window.setTimeout(function () { 6772 processingScheduled = false;
6773 }, 900); 6774 } 6775 6776 function handleMenuInteraction(event) { 6777 var target = event.target; 6778 6779 if ( 6780 target && 6781 target.closest && 6782 target.closest(MENU_ROOT_SELECTOR) 6783 ) { 6784 scheduleTrademarkProcessing(); 6785 } 6786 } 6787 6788 if (document.readyState === "loading") { 6789 document.addEventListener( 6790 "DOMContentLoaded", 6791 scheduleTrademarkProcessing 6792 ); 6793 } else { 6794 scheduleTrademarkProcessing(); 6795 } 6796 6797 window.addEventListener( 6798 "load", 6799 scheduleTrademarkProcessing 6800 ); 6801 6802 document.addEventListener( 6803 "mouseover", 6804 handleMenuInteraction, 6805 true 6806 ); 6807 6808 document.addEventListener( 6809 "focusin", 6810 handleMenuInteraction, 6811 true 6812 ); 6813 6814 document.addEventListener( 6815 "click", 6816 handleMenuInteraction, 6817 true 6818 ); 6819})(); 6820</script>
6820<script data-zs-snippet="Show portal icon for Logged In User in cart and checkout pages"> 6821 const CONFIG = { 6822 portalIconColor: "#000000" // Change this to your desired color 6823 }; 6824 function checkPortalUserAndShowIcon() { 6825 let isLoggedIn = window.isLoggedInUser; 6826 if (window.zs_view !== "cart" && window.zs_view !== "checkout") { 6827 return; 6828 } 6829 let parentMenu = document.querySelector("[data-banner-base-header] .zpcontainer"); 6830 let mobileHeader = document.querySelector("[data-zs-mobile-header-five-top] .theme-mobile-header-nav-wrapper "); 6831 let portalIcon = document.createElement("div"); 6832 portalIcon.id = "portal-icon"; 6833 portalIcon.innerHTML = ` 6834 <a href=${isLoggedIn ? "account/profile" : "signin"} class="theme-icon-link" style="color: ${CONFIG.portalIconColor};"> 6835 <svg aria-hidden="true" class="theme-portal-icon" width="19" height="19" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg"> 6836 <path d="M10.1533 3.90009C10.1533 5.77397 8.62033 7.30018 6.72046 7.30018C4.82059 7.30018 3.2876 5.77397 3.2876 3.90009C3.2876 2.02621 4.82059 0.5 6.72046 0.5C8.62033 0.5 10.1533 2.02621 10.1533 3.90009Z" fill="none" stroke="${CONFIG.portalIconColor}"></path> 6837 <path d="M0.931864 15.1002C1.01969 11.7605 3.7546 9.08008 7.11555 9.08008C10.4765 9.08008 13.2114 11.7605 13.2992 15.1002H0.931864Z" stroke="${CONFIG.portalIconColor}" fill="none"></path> 6838 </svg> 6839 </a> 6840 `; 6841 parentMenu.appendChild(portalIcon); 6842 mobileHeader.appendChild(portalIcon.cloneNode(true)); 6843 } 6844 document.addEventListener("DOMContentLoaded", function () { 6845 setTimeout(function () { 6846 checkPortalUserAndShowIcon(); 6847 }, 1000); 6848 }); 6849</script>
6849<style> #portal-icon{ margin-left: 4%; margin-top: 1%; } </style>
6849<script type="speculationrules"> 6850 { 6851 "prefetch": [ 6852 { 6853 "where": { 6854 "and": [ 6855 { "href_matches": "/*" }, 6856 { "not": { "href_matches": "/checkout" } }, 6857 { "not": { "href_matches": "/cart" } }, 6858 { "not": { "href_matches": "/account" } }, 6859 { "not": { "selector_matches": ".no-prerender" } }, 6860 { "not": { "selector_matches": "[rel~=nofollow]" } } 6861 ] 6862 }, 6863 "eagerness": "moderate" 6864 } 6865 ] 6866 } 6867 </script>
6867<div class="zpmm-backdrop zpmm-backdrop-disabled"></div> 6868</div></body></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.