PageSourceSearch

https://www.tyconsystems.com/blogs/tycon-cases-applications-articles/

html tyconsystems.com collected 2026-09-24 19:23:52 UTC 482,241 bytes, 6,868 lines download raw bytes

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&amp;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&amp;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> &nbsp;|&nbsp; (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 = "&nbsp;&nbsp;&nbsp;&nbsp;" + text + cartText;
566                    } else {
567                        stockElement.innerHTML = "&nbsp;&nbsp;&nbsp;&nbsp;" + 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 = "&nbsp;&nbsp;&nbsp;&nbsp;" + text + cartText;
601                } else {
602                    stockElement.innerHTML = "&nbsp;&nbsp;&nbsp;&nbsp;" + 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 = "&nbsp;&nbsp;&nbsp;&nbsp;" + 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;">&nbsp;&nbsp;&nbsp;&nbsp;(' + 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 = "&nbsp;&nbsp;&nbsp;&nbsp;" + 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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">&#x25C6; 3D View</span>' +
1748      '    <button class="tycon-3d-settings-btn" id="tycon-3d-settings-btn" title="Settings">&#x2699;</button>' +
1749      '    <button class="tycon-3d-close" id="tycon-3d-close" title="Close">&times;</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&amp;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> &nbsp;|&nbsp; (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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; Trademarks" rel="" style="color:rgb(255, 255, 255);">Patents &amp; 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 &amp; Trademarks" rel="" style="color:rgb(255, 154, 51);">Patents &amp; 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&nbsp;</span><span style="color:rgb(255, 255, 255);"><span style="font-weight:700;">© 2026&nbsp;</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);">®&nbsp;</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.